// 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 ( "bytes" "context" "errors" "fmt" "strconv" "strings" "github.com/harness/gitness/gitrpc/internal/types" gitea "code.gitea.io/gitea/modules/git" ) const ( giteaPrettyLogFormat = `--pretty=format:%H` ) // GetLatestCommit gets the latest commit of a path relative from the provided reference. // Note: ref can be Branch / Tag / CommitSHA. func (g Adapter) GetLatestCommit(ctx context.Context, repoPath string, ref string, treePath string) (*types.Commit, error) { treePath = cleanTreePath(treePath) giteaRepo, err := gitea.OpenRepository(ctx, repoPath) if err != nil { return nil, err } defer giteaRepo.Close() giteaCommit, err := giteaGetCommitByPath(giteaRepo, ref, treePath) if err != nil { return nil, processGiteaErrorf(err, "error getting latest commit for '%s'", treePath) } return mapGiteaCommit(giteaCommit) } // giteaGetCommitByPath is a copy of gitea code - required as we want latest commit per specific branch. func giteaGetCommitByPath(giteaRepo *gitea.Repository, ref string, treePath string) (*gitea.Commit, error) { if treePath == "" { treePath = "." } // NOTE: the difference to gitea implementation is passing `ref`. stdout, _, runErr := gitea.NewCommand(giteaRepo.Ctx, "log", ref, "-1", giteaPrettyLogFormat, "--", treePath). RunStdBytes(&gitea.RunOpts{Dir: giteaRepo.Path}) if runErr != nil { return nil, fmt.Errorf("failed to trigger log command: %w", runErr) } giteaCommits, err := giteaParsePrettyFormatLogToList(giteaRepo, stdout) if err != nil { return nil, err } return giteaCommits[0], nil } // giteaParsePrettyFormatLogToList is an exact copy of gitea code. func giteaParsePrettyFormatLogToList(giteaRepo *gitea.Repository, logs []byte) ([]*gitea.Commit, error) { var giteaCommits []*gitea.Commit if len(logs) == 0 { return giteaCommits, nil } parts := bytes.Split(logs, []byte{'\n'}) for _, commitID := range parts { commit, err := giteaRepo.GetCommit(string(commitID)) if err != nil { return nil, fmt.Errorf("failed to get commit '%s': %w", string(commitID), err) } giteaCommits = append(giteaCommits, commit) } return giteaCommits, nil } // ListCommits lists the commits reachable from ref. // Note: ref can be Branch / Tag / CommitSHA. func (g Adapter) ListCommits(ctx context.Context, repoPath string, ref string, page int, pageSize int) ([]types.Commit, int64, error) { giteaRepo, err := gitea.OpenRepository(ctx, repoPath) if err != nil { return nil, 0, err } defer giteaRepo.Close() // Get the giteaTopCommit object for the ref giteaTopCommit, err := giteaRepo.GetCommit(ref) if err != nil { return nil, 0, processGiteaErrorf(err, "error getting commit top commit for ref '%s'", ref) } giteaCommits, err := giteaTopCommit.CommitsByRange(page, pageSize) if err != nil { return nil, 0, processGiteaErrorf(err, "error getting commits") } totalCount, err := giteaTopCommit.CommitsCount() if err != nil { return nil, 0, processGiteaErrorf(err, "error getting total commit count") } commits := make([]types.Commit, len(giteaCommits)) for i := range giteaCommits { var commit *types.Commit commit, err = mapGiteaCommit(giteaCommits[i]) if err != nil { return nil, 0, err } commits[i] = *commit } // TODO: save to cast to int from int64, or we expect exceeding int.MaxValue? return commits, totalCount, nil } // GetCommit returns the (latest) commit for a specific ref. // Note: ref can be Branch / Tag / CommitSHA. func (g Adapter) GetCommit(ctx context.Context, repoPath string, ref string) (*types.Commit, error) { giteaRepo, err := gitea.OpenRepository(ctx, repoPath) if err != nil { return nil, err } defer giteaRepo.Close() commit, err := giteaRepo.GetCommit(ref) if err != nil { return nil, processGiteaErrorf(err, "error getting commit for ref '%s'", ref) } return mapGiteaCommit(commit) } // GetCommits returns the (latest) commits for a specific list of refs. // Note: ref can be Branch / Tag / CommitSHA. func (g Adapter) GetCommits(ctx context.Context, repoPath string, refs []string) ([]types.Commit, error) { giteaRepo, err := gitea.OpenRepository(ctx, repoPath) if err != nil { return nil, err } defer giteaRepo.Close() commits := make([]types.Commit, len(refs)) for i, sha := range refs { var giteaCommit *gitea.Commit giteaCommit, err = giteaRepo.GetCommit(sha) if err != nil { return nil, processGiteaErrorf(err, "error getting commit '%s'", sha) } var commit *types.Commit commit, err = mapGiteaCommit(giteaCommit) if err != nil { return nil, err } commits[i] = *commit } return commits, nil } // GetCommitDivergences returns the count of the diverging commits for all branch pairs. // IMPORTANT: If a max is provided it limits the overal count of diverging commits // (max 10 could lead to (0, 10) while it's actually (2, 12)). func (g Adapter) GetCommitDivergences(ctx context.Context, repoPath string, requests []types.CommitDivergenceRequest, max int32) ([]types.CommitDivergence, error) { var err error res := make([]types.CommitDivergence, len(requests)) for i, req := range requests { res[i], err = g.getCommitDivergence(ctx, repoPath, req, max) if errors.Is(err, types.ErrNotFound) { res[i] = types.CommitDivergence{Ahead: -1, Behind: -1} continue } if err != nil { return nil, err } } return res, nil } // getCommitDivergence returns the count of diverging commits for a pair of branches. // IMPORTANT: If a max is provided it limits the overal count of diverging commits // (max 10 could lead to (0, 10) while it's actually (2, 12)). // NOTE: Gitea implementation makes two git cli calls, but it can be done with one // (downside is the max behavior explained above). func (g Adapter) getCommitDivergence(ctx context.Context, repoPath string, req types.CommitDivergenceRequest, max int32) (types.CommitDivergence, error) { // prepare args args := []string{ "rev-list", "--count", "--left-right", } // limit count if requested. if max > 0 { args = append(args, "--max-count") args = append(args, fmt.Sprint(max)) } // add query to get commits without shared base commits args = append(args, fmt.Sprintf("%s...%s", req.From, req.To)) var err error cmd := gitea.NewCommand(ctx, args...) stdOut, stdErr, err := cmd.RunStdString(&gitea.RunOpts{Dir: repoPath}) if err != nil { return types.CommitDivergence{}, processGiteaErrorf(err, "git rev-list failed for '%s...%s' (stdErr: '%s')", req.From, req.To, stdErr) } // parse output, e.g.: `1 2\n` rawLeft, rawRight, ok := strings.Cut(stdOut, "\t") if !ok { return types.CommitDivergence{}, fmt.Errorf("git rev-list returned unexpected output '%s'", stdOut) } // trim any unnecessary characters rawLeft = strings.TrimRight(rawLeft, " \t") rawRight = strings.TrimRight(rawRight, " \t\n") // parse numbers left, err := strconv.ParseInt(rawLeft, 10, 32) if err != nil { return types.CommitDivergence{}, fmt.Errorf("failed to parse git rev-list output for ahead '%s' (full: '%s')): %w", rawLeft, stdOut, err) } right, err := strconv.ParseInt(rawRight, 10, 32) if err != nil { return types.CommitDivergence{}, fmt.Errorf("failed to parse git rev-list output for behind '%s' (full: '%s')): %w", rawRight, stdOut, err) } return types.CommitDivergence{ Ahead: int32(left), Behind: int32(right), }, nil }