drone/gitrpc/internal/gitea/commit.go

247 lines
7.4 KiB
Go

// 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
}