mirror of https://github.com/harness/drone.git
305 lines
8.4 KiB
Go
305 lines
8.4 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/harness/gitness/gitrpc/enum"
|
|
"github.com/harness/gitness/gitrpc/internal/gitea"
|
|
"github.com/harness/gitness/gitrpc/internal/types"
|
|
"github.com/harness/gitness/gitrpc/rpc"
|
|
|
|
"code.gitea.io/gitea/modules/git"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
type ReferenceService struct {
|
|
rpc.UnimplementedReferenceServiceServer
|
|
adapter GitAdapter
|
|
reposRoot string
|
|
tmpDir string
|
|
}
|
|
|
|
func NewReferenceService(adapter GitAdapter,
|
|
reposRoot string, tmpDir string) (*ReferenceService, error) {
|
|
return &ReferenceService{
|
|
adapter: adapter,
|
|
reposRoot: reposRoot,
|
|
tmpDir: tmpDir,
|
|
}, nil
|
|
}
|
|
|
|
// sanitizeReferenceQuery removes characters that aren't allowd in a branch name.
|
|
// TODO: should we error out instead of ignore bad chars?
|
|
func sanitizeReferenceQuery(query string) (string, bool, bool) {
|
|
if query == "" {
|
|
return "", false, false
|
|
}
|
|
|
|
// get special characters before anything else
|
|
matchPrefix := query[0] == '^' // will be removed by mapping
|
|
matchSuffix := query[len(query)-1] == '$'
|
|
if matchSuffix {
|
|
// Special char $ has to be removed manually as it's a valid char
|
|
// TODO: this restricts the query language to a certain degree, can we do better? (escaping)
|
|
query = query[:len(query)-1]
|
|
}
|
|
|
|
// strip all unwanted characters
|
|
return strings.Map(func(r rune) rune {
|
|
// See https://git-scm.com/docs/git-check-ref-format#_description for more details.
|
|
switch {
|
|
// rule 4.
|
|
case r < 32 || r == 127 || r == ' ' || r == '~' || r == '^' || r == ':':
|
|
return -1
|
|
|
|
// rule 5
|
|
case r == '?' || r == '*' || r == '[':
|
|
return -1
|
|
|
|
// everything else we map as is
|
|
default:
|
|
return r
|
|
}
|
|
}, query),
|
|
matchPrefix,
|
|
matchSuffix
|
|
}
|
|
|
|
// createReferenceWalkPatternsFromQuery returns a list of patterns that
|
|
// ensure only references matching the basePath and query are part of the walk.
|
|
func createReferenceWalkPatternsFromQuery(basePath string, query string) []string {
|
|
if basePath == "" && query == "" {
|
|
return []string{}
|
|
}
|
|
|
|
// ensure non-empty basepath ends with "/" for proper matching and concatenation.
|
|
if basePath != "" && basePath[len(basePath)-1] != '/' {
|
|
basePath += "/"
|
|
}
|
|
|
|
// in case query is empty, we just match the basePath.
|
|
if query == "" {
|
|
return []string{basePath}
|
|
}
|
|
|
|
// sanitze the query and get special chars
|
|
query, matchPrefix, matchSuffix := sanitizeReferenceQuery(query)
|
|
|
|
// In general, there are two search patterns:
|
|
// - refs/tags/**/*QUERY* - finds all refs that have QUERY in the filename.
|
|
// - refs/tags/**/*QUERY*/** - finds all refs that have a parent folder with QUERY in the name.
|
|
//
|
|
// In case the suffix has to match, they will be the same, so we return only one pattern.
|
|
if matchSuffix {
|
|
// exact match (refs/tags/QUERY)
|
|
if matchPrefix {
|
|
return []string{basePath + query}
|
|
}
|
|
|
|
// suffix only match (refs/tags/**/*QUERY)
|
|
//nolint:goconst
|
|
return []string{basePath + "**/*" + query}
|
|
}
|
|
|
|
// prefix only match
|
|
// - refs/tags/QUERY*
|
|
// - refs/tags/QUERY*/**
|
|
if matchPrefix {
|
|
return []string{
|
|
basePath + query + "*", // file
|
|
basePath + query + "*/**", // folder
|
|
}
|
|
}
|
|
|
|
// arbitrary match
|
|
// - refs/tags/**/*QUERY*
|
|
// - refs/tags/**/*QUERY*/**
|
|
return []string{
|
|
basePath + "**/*" + query + "*", // file
|
|
basePath + "**/*" + query + "*/**", // folder
|
|
}
|
|
}
|
|
|
|
// wrapInstructorWithOptionalPagination wraps the provided walkInstructor with pagination.
|
|
// If no paging is enabled, the original instructor is returned.
|
|
func wrapInstructorWithOptionalPagination(inner types.WalkReferencesInstructor,
|
|
page int32, pageSize int32) (types.WalkReferencesInstructor, int32, error) {
|
|
// ensure pagination is requested
|
|
if pageSize < 1 {
|
|
return inner, 0, nil
|
|
}
|
|
|
|
// sanitize page
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
|
|
// ensure we don't overflow
|
|
if int64(page)*int64(pageSize) > int64(math.MaxInt) {
|
|
return nil, 0, fmt.Errorf("page %d with pageSize %d is out of range", page, pageSize)
|
|
}
|
|
|
|
startAfter := (page - 1) * pageSize
|
|
endAfter := page * pageSize
|
|
|
|
// we have to count ourselves for proper pagination
|
|
c := int32(0)
|
|
return func(e types.WalkReferencesEntry) (types.WalkInstruction, error) {
|
|
// execute inner instructor
|
|
inst, err := inner(e)
|
|
if err != nil {
|
|
return inst, err
|
|
}
|
|
|
|
// no pagination if element is filtered out
|
|
if inst != types.WalkInstructionHandle {
|
|
return inst, nil
|
|
}
|
|
|
|
// increase count iff element is part of filtered output
|
|
c++
|
|
|
|
// add pagination on filtered output
|
|
switch {
|
|
case c <= startAfter:
|
|
return types.WalkInstructionSkip, nil
|
|
case c > endAfter:
|
|
return types.WalkInstructionStop, nil
|
|
default:
|
|
return types.WalkInstructionHandle, nil
|
|
}
|
|
},
|
|
endAfter,
|
|
nil
|
|
}
|
|
|
|
func (s ReferenceService) GetRef(ctx context.Context,
|
|
request *rpc.GetRefRequest,
|
|
) (*rpc.GetRefResponse, error) {
|
|
if request.Base == nil {
|
|
return nil, types.ErrBaseCannotBeEmpty
|
|
}
|
|
repoPath := getFullPathForRepo(s.reposRoot, request.Base.GetRepoUid())
|
|
|
|
refType := enum.RefFromRPC(request.GetRefType())
|
|
if refType == enum.RefTypeUndefined {
|
|
return nil, status.Error(codes.InvalidArgument, "invalid value of RefType argument")
|
|
}
|
|
reference, err := GetRefPath(request.GetRefName(), refType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sha, err := s.adapter.GetRef(ctx, repoPath, reference)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &rpc.GetRefResponse{Sha: sha}, nil
|
|
}
|
|
|
|
func (s ReferenceService) UpdateRef(ctx context.Context,
|
|
request *rpc.UpdateRefRequest,
|
|
) (*rpc.UpdateRefResponse, error) {
|
|
base := request.GetBase()
|
|
if base == nil {
|
|
return nil, types.ErrBaseCannotBeEmpty
|
|
}
|
|
repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())
|
|
|
|
refType := enum.RefFromRPC(request.GetRefType())
|
|
if refType == enum.RefTypeUndefined {
|
|
return nil, status.Error(codes.InvalidArgument, "invalid value of RefType argument")
|
|
}
|
|
reference, err := GetRefPath(request.GetRefName(), refType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if ok, err := repoIsEmpty(ctx, repoPath); ok {
|
|
return nil, ErrInvalidArgumentf("reference cannot be updated on empty repository", err)
|
|
}
|
|
|
|
sharedRepo, err := NewSharedRepo(s.tmpDir, base.GetRepoUid(), repoPath)
|
|
if err != nil {
|
|
return nil, processGitErrorf(err, "failed to create new shared repo")
|
|
}
|
|
defer sharedRepo.Close(ctx)
|
|
|
|
// clone repo (with HEAD branch - target might be anything)
|
|
err = sharedRepo.Clone(ctx, "")
|
|
if err != nil {
|
|
return nil, processGitErrorf(err, "failed to clone shared repo")
|
|
}
|
|
|
|
pushOpts := types.PushOptions{
|
|
Remote: repoPath,
|
|
Env: CreateEnvironmentForPush(ctx, base),
|
|
}
|
|
|
|
// handle deletion explicitly to avoid any unwanted side effects
|
|
if request.GetNewValue() == "" {
|
|
pushOpts.Branch = ":" + reference
|
|
} else {
|
|
pushOpts.Branch = request.GetNewValue() + ":" + reference
|
|
}
|
|
|
|
if request.GetOldValue() == "" {
|
|
pushOpts.Force = true
|
|
} else {
|
|
pushOpts.ForceWithLease = reference + ":" + request.GetOldValue()
|
|
}
|
|
|
|
// TODO: why are we using gitea operations here?
|
|
// TODO: our shared repo has so much duplication, that should be changed IMHO.
|
|
err = gitea.Push(ctx, sharedRepo.tmpPath, pushOpts)
|
|
if err != nil {
|
|
return nil, processGitErrorf(err, "failed to push changes to original repo")
|
|
}
|
|
|
|
return &rpc.UpdateRefResponse{}, nil
|
|
}
|
|
|
|
func GetRefPath(refName string, refType enum.RefType) (string, error) {
|
|
const (
|
|
refPullReqPrefix = "refs/pullreq/"
|
|
refPullReqHeadSuffix = "/head"
|
|
refPullReqMergeSuffix = "/merge"
|
|
)
|
|
|
|
switch refType {
|
|
case enum.RefTypeRaw:
|
|
return refName, nil
|
|
case enum.RefTypeBranch:
|
|
return git.BranchPrefix + refName, nil
|
|
case enum.RefTypeTag:
|
|
return git.TagPrefix + refName, nil
|
|
case enum.RefTypePullReqHead:
|
|
return refPullReqPrefix + refName + refPullReqHeadSuffix, nil
|
|
case enum.RefTypePullReqMerge:
|
|
return refPullReqPrefix + refName + refPullReqMergeSuffix, nil
|
|
case enum.RefTypeUndefined:
|
|
fallthrough
|
|
default:
|
|
return "", ErrInvalidArgumentf("provided reference type '%s' is invalid", refType)
|
|
}
|
|
}
|