// Copyright 2022 Harness Inc. All rights reserved. // Use of this source code is governed by the Polyform Free Trial License // that can be found in the LICENSE.md file for this repository. package gitea import ( "context" "fmt" "io" "math" "strings" "github.com/harness/gitness/gitrpc/enum" "github.com/harness/gitness/gitrpc/internal/types" gitea "code.gitea.io/gitea/modules/git" gitearef "code.gitea.io/gitea/modules/git/foreachref" ) func DefaultInstructor(_ types.WalkReferencesEntry) (types.WalkInstruction, error) { return types.WalkInstructionHandle, nil } // WalkReferences uses the provided options to filter the available references of the repo, // and calls the handle function for every matching node. // The instructor & handler are called with a map that contains the matching value for every field provided in fields. // TODO: walkGiteaReferences related code should be moved to separate file. func (g Adapter) WalkReferences(ctx context.Context, repoPath string, handler types.WalkReferencesHandler, opts *types.WalkReferencesOptions) error { // backfil optional options if opts.Instructor == nil { opts.Instructor = DefaultInstructor } if len(opts.Fields) == 0 { opts.Fields = []types.GitReferenceField{types.GitReferenceFieldRefName, types.GitReferenceFieldObjectName} } if opts.MaxWalkDistance <= 0 { opts.MaxWalkDistance = math.MaxInt32 } if opts.Patterns == nil { opts.Patterns = []string{} } if string(opts.Sort) == "" { opts.Sort = types.GitReferenceFieldRefName } // prepare for-each-ref input sortArg := mapToGiteaReferenceSortingArgument(opts.Sort, opts.Order) rawFields := make([]string, len(opts.Fields)) for i := range opts.Fields { rawFields[i] = string(opts.Fields[i]) } giteaFormat := gitearef.NewFormat(rawFields...) // initializer pipeline for output processing pipeOut, pipeIn := io.Pipe() defer pipeOut.Close() defer pipeIn.Close() stderr := strings.Builder{} rc := &gitea.RunOpts{Dir: repoPath, Stdout: pipeIn, Stderr: &stderr} go func() { // create array for args as patterns have to be passed as separate args. args := []string{ "for-each-ref", "--format", giteaFormat.Flag(), "--sort", sortArg, "--count", fmt.Sprint(opts.MaxWalkDistance), "--ignore-case", } args = append(args, opts.Patterns...) err := gitea.NewCommand(ctx, args...).Run(rc) if err != nil { _ = pipeIn.CloseWithError(gitea.ConcatenateError(err, stderr.String())) } else { _ = pipeIn.Close() } }() parser := giteaFormat.Parser(pipeOut) return walkGiteaReferenceParser(parser, handler, opts) } func walkGiteaReferenceParser(parser *gitearef.Parser, handler types.WalkReferencesHandler, opts *types.WalkReferencesOptions) error { for i := int32(0); i < opts.MaxWalkDistance; i++ { // parse next line - nil if end of output reached or an error occurred. rawRef := parser.Next() if rawRef == nil { break } // convert to correct map. ref, err := mapGiteaRawRef(rawRef) if err != nil { return err } // check with the instructor on the next instruction. instruction, err := opts.Instructor(ref) if err != nil { return fmt.Errorf("error getting instruction: %w", err) } if instruction == types.WalkInstructionSkip { continue } if instruction == types.WalkInstructionStop { break } // otherwise handle the reference. err = handler(ref) if err != nil { return fmt.Errorf("error handling reference: %w", err) } } if err := parser.Err(); err != nil { return processGiteaErrorf(err, "failed to parse reference walk output") } return nil } func (g Adapter) GetRef(ctx context.Context, repoPath, refName string, refType enum.RefType) (string, error) { refName, errRef := g.GetRefPath(refName, refType) if errRef != nil { return "", errRef } cmd := gitea.NewCommand(ctx, "show-ref", "--verify", "-s", "--", refName) stdout, _, err := cmd.RunStdString(&gitea.RunOpts{ Dir: repoPath, }) if err != nil { if err.IsExitCode(128) && strings.Contains(err.Stderr(), "not a valid ref") { return "", types.ErrNotFound } return "", err } return strings.TrimSpace(stdout), nil } func (g Adapter) UpdateRef(ctx context.Context, repoPath, refName string, refType enum.RefType, newValue, oldValue string, ) error { refName, errRef := g.GetRefPath(refName, refType) if errRef != nil { return errRef } args := make([]string, 0, 4) args = append(args, "update-ref") if newValue == "" { // if newvalue is empty, delete ref args = append(args, "-d", refName) } else { args = append(args, refName, newValue) } // if an old value was provided, verify it matches. if oldValue != "" { args = append(args, oldValue) } cmd := gitea.NewCommand(ctx, args...) _, _, err := cmd.RunStdString(&gitea.RunOpts{ Dir: repoPath, }) if err != nil { return processGiteaErrorf(err, "update-ref failed") } return nil } // TBD: IMHO all helper functions like this one should be on caller side // of Git adapter interface, so if we develop our own adapter package in future // we dont need to repeat helper like this again. func (g Adapter) 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 gitea.BranchPrefix + refName, nil case enum.RefTypeTag: return gitea.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 "", types.ErrInvalidArgument } }