// 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 git import ( "context" "fmt" "math" "strings" "github.com/harness/gitness/errors" "github.com/harness/gitness/git/api" "github.com/harness/gitness/git/enum" "github.com/harness/gitness/git/hook" "github.com/harness/gitness/git/sha" ) type GetRefParams struct { ReadParams Name string Type enum.RefType } func (p *GetRefParams) Validate() error { if p == nil { return ErrNoParamsProvided } if err := p.ReadParams.Validate(); err != nil { return err } if p.Name == "" { return errors.InvalidArgument("ref name cannot be empty") } return nil } type GetRefResponse struct { SHA sha.SHA } func (s *Service) GetRef(ctx context.Context, params GetRefParams) (GetRefResponse, error) { if err := params.Validate(); err != nil { return GetRefResponse{}, err } repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) reference, err := GetRefPath(params.Name, params.Type) if err != nil { return GetRefResponse{}, fmt.Errorf("GetRef: failed to fetch reference '%s': %w", params.Name, err) } refSHA, err := s.git.GetRef(ctx, repoPath, reference) if err != nil { return GetRefResponse{}, err } return GetRefResponse{SHA: refSHA}, nil } type UpdateRefParams struct { WriteParams Type enum.RefType Name string // NewValue specified the new value the reference should point at. // An empty value will lead to the deletion of the branch. NewValue sha.SHA // OldValue is an optional value that can be used to ensure that the reference // is updated iff its current value is matching the provided value. OldValue sha.SHA } func (s *Service) UpdateRef(ctx context.Context, params UpdateRefParams) error { if err := params.Validate(); err != nil { return err } repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) reference, err := GetRefPath(params.Name, params.Type) if err != nil { return fmt.Errorf("UpdateRef: failed to fetch reference '%s': %w", params.Name, err) } refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, reference) if err != nil { return fmt.Errorf("UpdateRef: failed to create ref updater: %w", err) } if err := refUpdater.Do(ctx, params.OldValue, params.NewValue); err != nil { return fmt.Errorf("failed to update ref: %w", err) } return 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 api.BranchPrefix + refName, nil case enum.RefTypeTag: return api.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 "", errors.InvalidArgument("provided reference type '%s' is invalid", refType) } } // wrapInstructorWithOptionalPagination wraps the provided walkInstructor with pagination. // If no paging is enabled, the original instructor is returned. func wrapInstructorWithOptionalPagination( inner api.WalkReferencesInstructor, page int32, pageSize int32, ) (api.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 api.WalkReferencesEntry) (api.WalkInstruction, error) { // execute inner instructor inst, err := inner(e) if err != nil { return inst, err } // no pagination if element is filtered out if inst != api.WalkInstructionHandle { return inst, nil } // increase count iff element is part of filtered output c++ // add pagination on filtered output switch { case c <= startAfter: return api.WalkInstructionSkip, nil case c > endAfter: return api.WalkInstructionStop, nil default: return api.WalkInstructionHandle, nil } }, endAfter, nil } // 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 } } // 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 }