[code-1016] Replace gitea Usages (not wrapper) (#1063)

This commit is contained in:
Enver Bisevac 2024-03-26 20:31:30 +00:00 committed by Harness
parent 8dc82433c5
commit cecfecdb06
122 changed files with 3392 additions and 5312 deletions

View File

@ -141,7 +141,7 @@ func (c *Controller) Report(
_, err = c.git.GetCommit(ctx, &git.GetCommitParams{ _, err = c.git.GetCommit(ctx, &git.GetCommitParams{
ReadParams: git.ReadParams{RepoUID: repo.GitUID}, ReadParams: git.ReadParams{RepoUID: repo.GitUID},
SHA: commitSHA, Revision: commitSHA,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to commit sha=%s: %w", commitSHA, err) return nil, fmt.Errorf("failed to commit sha=%s: %w", commitSHA, err)

View File

@ -99,19 +99,19 @@ func (c *Controller) reportBranchEvent(
branchUpdate hook.ReferenceUpdate, branchUpdate hook.ReferenceUpdate,
) { ) {
switch { switch {
case branchUpdate.Old == types.NilSHA: case branchUpdate.Old.String() == types.NilSHA:
c.gitReporter.BranchCreated(ctx, &events.BranchCreatedPayload{ c.gitReporter.BranchCreated(ctx, &events.BranchCreatedPayload{
RepoID: repo.ID, RepoID: repo.ID,
PrincipalID: principalID, PrincipalID: principalID,
Ref: branchUpdate.Ref, Ref: branchUpdate.Ref,
SHA: branchUpdate.New, SHA: branchUpdate.New.String(),
}) })
case branchUpdate.New == types.NilSHA: case branchUpdate.New.String() == types.NilSHA:
c.gitReporter.BranchDeleted(ctx, &events.BranchDeletedPayload{ c.gitReporter.BranchDeleted(ctx, &events.BranchDeletedPayload{
RepoID: repo.ID, RepoID: repo.ID,
PrincipalID: principalID, PrincipalID: principalID,
Ref: branchUpdate.Ref, Ref: branchUpdate.Ref,
SHA: branchUpdate.Old, SHA: branchUpdate.Old.String(),
}) })
default: default:
result, err := c.git.IsAncestor(ctx, git.IsAncestorParams{ result, err := c.git.IsAncestor(ctx, git.IsAncestorParams{
@ -132,8 +132,8 @@ func (c *Controller) reportBranchEvent(
RepoID: repo.ID, RepoID: repo.ID,
PrincipalID: principalID, PrincipalID: principalID,
Ref: branchUpdate.Ref, Ref: branchUpdate.Ref,
OldSHA: branchUpdate.Old, OldSHA: branchUpdate.Old.String(),
NewSHA: branchUpdate.New, NewSHA: branchUpdate.New.String(),
Forced: forced, Forced: forced,
}) })
} }
@ -146,27 +146,27 @@ func (c *Controller) reportTagEvent(
tagUpdate hook.ReferenceUpdate, tagUpdate hook.ReferenceUpdate,
) { ) {
switch { switch {
case tagUpdate.Old == types.NilSHA: case tagUpdate.Old.String() == types.NilSHA:
c.gitReporter.TagCreated(ctx, &events.TagCreatedPayload{ c.gitReporter.TagCreated(ctx, &events.TagCreatedPayload{
RepoID: repo.ID, RepoID: repo.ID,
PrincipalID: principalID, PrincipalID: principalID,
Ref: tagUpdate.Ref, Ref: tagUpdate.Ref,
SHA: tagUpdate.New, SHA: tagUpdate.New.String(),
}) })
case tagUpdate.New == types.NilSHA: case tagUpdate.New.String() == types.NilSHA:
c.gitReporter.TagDeleted(ctx, &events.TagDeletedPayload{ c.gitReporter.TagDeleted(ctx, &events.TagDeletedPayload{
RepoID: repo.ID, RepoID: repo.ID,
PrincipalID: principalID, PrincipalID: principalID,
Ref: tagUpdate.Ref, Ref: tagUpdate.Ref,
SHA: tagUpdate.Old, SHA: tagUpdate.Old.String(),
}) })
default: default:
c.gitReporter.TagUpdated(ctx, &events.TagUpdatedPayload{ c.gitReporter.TagUpdated(ctx, &events.TagUpdatedPayload{
RepoID: repo.ID, RepoID: repo.ID,
PrincipalID: principalID, PrincipalID: principalID,
Ref: tagUpdate.Ref, Ref: tagUpdate.Ref,
OldSHA: tagUpdate.Old, OldSHA: tagUpdate.Old.String(),
NewSHA: tagUpdate.New, NewSHA: tagUpdate.New.String(),
// tags can only be force updated! // tags can only be force updated!
Forced: true, Forced: true,
}) })
@ -184,7 +184,7 @@ func (c *Controller) handlePRMessaging(
// skip anything that was a batch push / isn't branch related / isn't updating/creating a branch. // skip anything that was a batch push / isn't branch related / isn't updating/creating a branch.
if len(in.RefUpdates) != 1 || if len(in.RefUpdates) != 1 ||
!strings.HasPrefix(in.RefUpdates[0].Ref, gitReferenceNamePrefixBranch) || !strings.HasPrefix(in.RefUpdates[0].Ref, gitReferenceNamePrefixBranch) ||
in.RefUpdates[0].New == types.NilSHA { in.RefUpdates[0].New.String() == types.NilSHA {
return return
} }

View File

@ -188,9 +188,9 @@ type changes struct {
func (c *changes) groupByAction(refUpdate hook.ReferenceUpdate, name string) { func (c *changes) groupByAction(refUpdate hook.ReferenceUpdate, name string) {
switch { switch {
case refUpdate.Old == types.NilSHA: case refUpdate.Old.String() == types.NilSHA:
c.created = append(c.created, name) c.created = append(c.created, name)
case refUpdate.New == types.NilSHA: case refUpdate.New.String() == types.NilSHA:
c.deleted = append(c.deleted, name) c.deleted = append(c.deleted, name)
default: default:
c.updated = append(c.updated, name) c.updated = append(c.updated, name)

View File

@ -25,7 +25,6 @@ import (
events "github.com/harness/gitness/app/events/pullreq" events "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gittypes "github.com/harness/gitness/git/types"
"github.com/harness/gitness/store" "github.com/harness/gitness/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
@ -307,7 +306,7 @@ func setAsCodeComment(a *types.PullReqActivity, cut git.DiffCutOutput, path, sou
a.Kind = enum.PullReqActivityKindChangeComment a.Kind = enum.PullReqActivityKindChangeComment
a.CodeComment = &types.CodeCommentFields{ a.CodeComment = &types.CodeCommentFields{
Outdated: falseBool, Outdated: falseBool,
MergeBaseSHA: cut.MergeBaseSHA, MergeBaseSHA: cut.MergeBaseSHA.String(),
SourceSHA: sourceCommitSHA, SourceSHA: sourceCommitSHA,
Path: path, Path: path,
LineNew: cut.Header.NewLine, LineNew: cut.Header.NewLine,
@ -332,7 +331,7 @@ func (c *Controller) fetchDiffCut(
LineEnd: in.LineEnd, LineEnd: in.LineEnd,
LineEndNew: in.LineEndNew, LineEndNew: in.LineEndNew,
}) })
if errors.AsStatus(err) == errors.StatusNotFound || gittypes.IsPathNotFoundError(err) { if errors.AsStatus(err) == errors.StatusNotFound {
return git.DiffCutOutput{}, usererror.BadRequest(errors.Message(err)) return git.DiffCutOutput{}, usererror.BadRequest(errors.Message(err))
} }
if err != nil { if err != nil {
@ -351,7 +350,7 @@ func (c *Controller) migrateCodeComment(
cut git.DiffCutOutput, cut git.DiffCutOutput,
) { ) {
needsNewLineMigrate := in.SourceCommitSHA != pr.SourceSHA needsNewLineMigrate := in.SourceCommitSHA != pr.SourceSHA
needsOldLineMigrate := cut.MergeBaseSHA != pr.MergeBaseSHA needsOldLineMigrate := cut.MergeBaseSHA.String() != pr.MergeBaseSHA
if !needsNewLineMigrate && !needsOldLineMigrate { if !needsNewLineMigrate && !needsOldLineMigrate {
return return
} }

View File

@ -134,7 +134,7 @@ func (c *Controller) verifyBranchExistence(ctx context.Context,
branch, repo.Identifier, err) branch, repo.Identifier, err)
} }
return ref.SHA, nil return ref.SHA.String(), nil
} }
func (c *Controller) getRepoCheckAccess(ctx context.Context, func (c *Controller) getRepoCheckAccess(ctx context.Context,

View File

@ -21,8 +21,8 @@ import (
"github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/auth"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gittypes "github.com/harness/gitness/git/types"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
@ -80,7 +80,7 @@ func (c *Controller) FileViewAdd(
Path: in.Path, Path: in.Path,
IncludeLatestCommit: false, IncludeLatestCommit: false,
}) })
if err != nil && !gittypes.IsPathNotFoundError(err) { if err != nil && !errors.IsNotFound(err) {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"failed to get tree node '%s' for provided sha '%s': %w", "failed to get tree node '%s' for provided sha '%s': %w",
in.Path, in.Path,
@ -102,7 +102,7 @@ func (c *Controller) FileViewAdd(
Path: in.Path, Path: in.Path,
IncludeLatestCommit: false, IncludeLatestCommit: false,
}) })
if err != nil && !gittypes.IsPathNotFoundError(err) { if err != nil && !errors.IsNotFound(err) {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"failed to get tree node '%s' for MergeBaseSHA '%s': %w", "failed to get tree node '%s' for MergeBaseSHA '%s': %w",
in.Path, in.Path,

View File

@ -32,9 +32,11 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gitenum "github.com/harness/gitness/git/enum" gitenum "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/sha"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/gotidy/ptr"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -235,27 +237,27 @@ func (c *Controller) Merge(
HeadRepoUID: sourceRepo.GitUID, HeadRepoUID: sourceRepo.GitUID,
HeadBranch: pr.SourceBranch, HeadBranch: pr.SourceBranch,
RefType: gitenum.RefTypeUndefined, // update no refs -> no commit will be created RefType: gitenum.RefTypeUndefined, // update no refs -> no commit will be created
HeadExpectedSHA: in.SourceSHA, HeadExpectedSHA: sha.Must(in.SourceSHA),
}) })
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("merge check execution failed: %w", err) return nil, nil, fmt.Errorf("merge check execution failed: %w", err)
} }
pr, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error { pr, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
if pr.SourceSHA != mergeOutput.HeadSHA { if pr.SourceSHA != mergeOutput.HeadSHA.String() {
return errors.New("source SHA has changed") return errors.New("source SHA has changed")
} }
if len(mergeOutput.ConflictFiles) > 0 { if len(mergeOutput.ConflictFiles) > 0 {
pr.MergeCheckStatus = enum.MergeCheckStatusConflict pr.MergeCheckStatus = enum.MergeCheckStatusConflict
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
pr.MergeTargetSHA = &mergeOutput.BaseSHA pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
pr.MergeSHA = nil pr.MergeSHA = nil
pr.MergeConflicts = mergeOutput.ConflictFiles pr.MergeConflicts = mergeOutput.ConflictFiles
} else { } else {
pr.MergeCheckStatus = enum.MergeCheckStatusMergeable pr.MergeCheckStatus = enum.MergeCheckStatusMergeable
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
pr.MergeTargetSHA = &mergeOutput.BaseSHA pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
pr.MergeSHA = &mergeOutput.MergeSHA pr.MergeSHA = ptr.String(mergeOutput.MergeSHA.String())
pr.MergeConflicts = nil pr.MergeConflicts = nil
} }
pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount) pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount)
@ -345,23 +347,23 @@ func (c *Controller) Merge(
AuthorDate: &now, AuthorDate: &now,
RefType: gitenum.RefTypeBranch, RefType: gitenum.RefTypeBranch,
RefName: pr.TargetBranch, RefName: pr.TargetBranch,
HeadExpectedSHA: in.SourceSHA, HeadExpectedSHA: sha.Must(in.SourceSHA),
Method: gitenum.MergeMethod(in.Method), Method: gitenum.MergeMethod(in.Method),
}) })
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("merge check execution failed: %w", err) return nil, nil, fmt.Errorf("merge check execution failed: %w", err)
} }
//nolint:nestif //nolint:nestif
if mergeOutput.MergeSHA == "" || len(mergeOutput.ConflictFiles) > 0 { if mergeOutput.MergeSHA.String() == "" || len(mergeOutput.ConflictFiles) > 0 {
_, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error { _, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
if pr.SourceSHA != mergeOutput.HeadSHA { if pr.SourceSHA != mergeOutput.HeadSHA.String() {
return errors.New("source SHA has changed") return errors.New("source SHA has changed")
} }
// update all Merge specific information // update all Merge specific information
pr.MergeCheckStatus = enum.MergeCheckStatusConflict pr.MergeCheckStatus = enum.MergeCheckStatusConflict
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
pr.MergeTargetSHA = &mergeOutput.BaseSHA pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
pr.MergeSHA = nil pr.MergeSHA = nil
pr.MergeConflicts = mergeOutput.ConflictFiles pr.MergeConflicts = mergeOutput.ConflictFiles
pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount) pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount)
@ -396,10 +398,10 @@ func (c *Controller) Merge(
// update all Merge specific information (might be empty if previous merge check failed) // update all Merge specific information (might be empty if previous merge check failed)
// since this is the final operation on the PR, we update any sha that might've changed by now. // since this is the final operation on the PR, we update any sha that might've changed by now.
pr.MergeCheckStatus = enum.MergeCheckStatusMergeable pr.MergeCheckStatus = enum.MergeCheckStatusMergeable
pr.SourceSHA = mergeOutput.HeadSHA pr.SourceSHA = mergeOutput.HeadSHA.String()
pr.MergeTargetSHA = &mergeOutput.BaseSHA pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
pr.MergeSHA = &mergeOutput.MergeSHA pr.MergeSHA = ptr.String(mergeOutput.MergeSHA.String())
pr.MergeConflicts = nil pr.MergeConflicts = nil
pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount) pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount)
@ -421,9 +423,9 @@ func (c *Controller) Merge(
pr.ActivitySeq = activitySeqMerge pr.ActivitySeq = activitySeqMerge
activityPayload := &types.PullRequestActivityPayloadMerge{ activityPayload := &types.PullRequestActivityPayloadMerge{
MergeMethod: in.Method, MergeMethod: in.Method,
MergeSHA: mergeOutput.MergeSHA, MergeSHA: mergeOutput.MergeSHA.String(),
TargetSHA: mergeOutput.BaseSHA, TargetSHA: mergeOutput.BaseSHA.String(),
SourceSHA: mergeOutput.HeadSHA, SourceSHA: mergeOutput.HeadSHA.String(),
} }
if _, errAct := c.activityStore.CreateWithPayload(ctx, pr, session.Principal.ID, activityPayload); errAct != nil { if _, errAct := c.activityStore.CreateWithPayload(ctx, pr, session.Principal.ID, activityPayload); errAct != nil {
// non-critical error // non-critical error
@ -433,9 +435,9 @@ func (c *Controller) Merge(
c.eventReporter.Merged(ctx, &pullreqevents.MergedPayload{ c.eventReporter.Merged(ctx, &pullreqevents.MergedPayload{
Base: eventBase(pr, &session.Principal), Base: eventBase(pr, &session.Principal),
MergeMethod: in.Method, MergeMethod: in.Method,
MergeSHA: mergeOutput.MergeSHA, MergeSHA: mergeOutput.MergeSHA.String(),
TargetSHA: mergeOutput.BaseSHA, TargetSHA: mergeOutput.BaseSHA.String(),
SourceSHA: mergeOutput.HeadSHA, SourceSHA: mergeOutput.HeadSHA.String(),
}) })
var branchDeleted bool var branchDeleted bool
@ -467,7 +469,7 @@ func (c *Controller) Merge(
} }
return &types.MergeResponse{ return &types.MergeResponse{
SHA: mergeOutput.MergeSHA, SHA: mergeOutput.MergeSHA.String(),
BranchDeleted: branchDeleted, BranchDeleted: branchDeleted,
RuleViolations: violations, RuleViolations: violations,
}, nil, nil }, nil, nil

View File

@ -95,7 +95,7 @@ func (c *Controller) Create(
mergeBaseSHA := mergeBaseResult.MergeBaseSHA mergeBaseSHA := mergeBaseResult.MergeBaseSHA
if mergeBaseSHA == sourceSHA { if mergeBaseSHA.String() == sourceSHA {
return nil, usererror.BadRequest("The source branch doesn't contain any new commits") return nil, usererror.BadRequest("The source branch doesn't contain any new commits")
} }
@ -107,7 +107,7 @@ func (c *Controller) Create(
return nil, fmt.Errorf("failed to acquire PullReqSeq number: %w", err) return nil, fmt.Errorf("failed to acquire PullReqSeq number: %w", err)
} }
pr := newPullReq(session, targetRepo.PullReqSeq, sourceRepo, targetRepo, in, sourceSHA, mergeBaseSHA) pr := newPullReq(session, targetRepo.PullReqSeq, sourceRepo, targetRepo, in, sourceSHA, mergeBaseSHA.String())
err = c.pullreqStore.Create(ctx, pr) err = c.pullreqStore.Create(ctx, pr)
if err != nil { if err != nil {

View File

@ -21,7 +21,7 @@ import (
"github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/auth"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gittypes "github.com/harness/gitness/git/types" gittypes "github.com/harness/gitness/git/api"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )

View File

@ -129,7 +129,7 @@ func (c *Controller) State(ctx context.Context,
return nil, fmt.Errorf("failed to find merge base: %w", err) return nil, fmt.Errorf("failed to find merge base: %w", err)
} }
mergeBaseSHA = mergeBaseResult.MergeBaseSHA mergeBaseSHA = mergeBaseResult.MergeBaseSHA.String()
stateChange = changeReopen stateChange = changeReopen
} else if pr.State == enum.PullReqStateOpen && in.State != enum.PullReqStateOpen { } else if pr.State == enum.PullReqStateOpen && in.State != enum.PullReqStateOpen {

View File

@ -82,7 +82,7 @@ func (c *Controller) ReviewSubmit(
commit, err := c.git.GetCommit(ctx, &git.GetCommitParams{ commit, err := c.git.GetCommit(ctx, &git.GetCommitParams{
ReadParams: git.ReadParams{RepoUID: repo.GitUID}, ReadParams: git.ReadParams{RepoUID: repo.GitUID},
SHA: in.CommitSHA, Revision: in.CommitSHA,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get git branch sha: %w", err) return nil, fmt.Errorf("failed to get git branch sha: %w", err)
@ -101,7 +101,7 @@ func (c *Controller) ReviewSubmit(
Updated: now, Updated: now,
PullReqID: pr.ID, PullReqID: pr.ID,
Decision: in.Decision, Decision: in.Decision,
SHA: commitSHA, SHA: commitSHA.String(),
} }
err = c.reviewStore.Create(ctx, review) err = c.reviewStore.Create(ctx, review)
@ -114,7 +114,7 @@ func (c *Controller) ReviewSubmit(
ReviewerID: review.CreatedBy, ReviewerID: review.CreatedBy,
}) })
_, err = c.updateReviewer(ctx, session, pr, review, commitSHA) _, err = c.updateReviewer(ctx, session, pr, review, commitSHA.String())
return err return err
}) })
if err != nil { if err != nil {
@ -127,7 +127,7 @@ func (c *Controller) ReviewSubmit(
} }
payload := &types.PullRequestActivityPayloadReviewSubmit{ payload := &types.PullRequestActivityPayloadReviewSubmit{
CommitSHA: commitSHA, CommitSHA: commitSHA.String(),
Decision: in.Decision, Decision: in.Decision,
} }
_, err = c.activityStore.CreateWithPayload(ctx, pr, session.Principal.ID, payload) _, err = c.activityStore.CreateWithPayload(ctx, pr, session.Principal.ID, payload)

View File

@ -158,7 +158,7 @@ func (c *Controller) CommitFiles(ctx context.Context,
} }
return types.CommitFilesResponse{ return types.CommitFilesResponse{
CommitID: commit.CommitID, CommitID: commit.CommitID.String(),
RuleViolations: violations, RuleViolations: violations,
}, nil, nil }, nil, nil
} }

View File

@ -23,7 +23,7 @@ import (
"github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/auth"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gittypes "github.com/harness/gitness/git/types" gittypes "github.com/harness/gitness/git/api"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
@ -58,7 +58,7 @@ func (c *Controller) CommitDiff(
ctx context.Context, ctx context.Context,
session *auth.Session, session *auth.Session,
repoRef string, repoRef string,
sha string, rev string,
w io.Writer, w io.Writer,
) error { ) error {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true) repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true)
@ -68,7 +68,7 @@ func (c *Controller) CommitDiff(
return c.git.CommitDiff(ctx, &git.GetCommitParams{ return c.git.CommitDiff(ctx, &git.GetCommitParams{
ReadParams: git.CreateReadParams(repo), ReadParams: git.CreateReadParams(repo),
SHA: sha, Revision: rev,
}, w) }, w)
} }

View File

@ -38,7 +38,7 @@ func (c *Controller) GetCommit(ctx context.Context,
rpcOut, err := c.git.GetCommit(ctx, &git.GetCommitParams{ rpcOut, err := c.git.GetCommit(ctx, &git.GetCommitParams{
ReadParams: git.CreateReadParams(repo), ReadParams: git.CreateReadParams(repo),
SHA: sha, Revision: sha,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get commit: %w", err) return nil, fmt.Errorf("failed to get commit: %w", err)

View File

@ -106,7 +106,7 @@ func mapBranch(b git.Branch) (Branch, error) {
} }
return Branch{ return Branch{
Name: b.Name, Name: b.Name,
SHA: b.SHA, SHA: b.SHA.String(),
Commit: commit, Commit: commit,
}, nil }, nil
} }

View File

@ -106,7 +106,7 @@ func mapCommitTag(t git.CommitTag) (CommitTag, error) {
return CommitTag{ return CommitTag{
Name: t.Name, Name: t.Name,
SHA: t.SHA, SHA: t.SHA.String(),
IsAnnotated: t.IsAnnotated, IsAnnotated: t.IsAnnotated,
Title: t.Title, Title: t.Title,
Message: t.Message, Message: t.Message,

View File

@ -31,7 +31,7 @@ func (c *Controller) Raw(ctx context.Context,
session *auth.Session, session *auth.Session,
repoRef string, repoRef string,
gitRef string, gitRef string,
repoPath string, path string,
) (io.ReadCloser, int64, error) { ) (io.ReadCloser, int64, error) {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true) repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true)
if err != nil { if err != nil {
@ -48,7 +48,7 @@ func (c *Controller) Raw(ctx context.Context,
treeNodeOutput, err := c.git.GetTreeNode(ctx, &git.GetTreeNodeParams{ treeNodeOutput, err := c.git.GetTreeNode(ctx, &git.GetTreeNodeParams{
ReadParams: readParams, ReadParams: readParams,
GitREF: gitRef, GitREF: gitRef,
Path: repoPath, Path: path,
IncludeLatestCommit: false, IncludeLatestCommit: false,
}) })
if err != nil { if err != nil {
@ -59,7 +59,7 @@ func (c *Controller) Raw(ctx context.Context,
if treeNodeOutput.Node.Type != git.TreeNodeTypeBlob { if treeNodeOutput.Node.Type != git.TreeNodeTypeBlob {
return nil, 0, usererror.BadRequestf( return nil, 0, usererror.BadRequestf(
"Object in '%s' at '/%s' is of type '%s'. Only objects of type %s support raw viewing.", "Object in '%s' at '/%s' is of type '%s'. Only objects of type %s support raw viewing.",
gitRef, repoPath, treeNodeOutput.Node.Type, git.TreeNodeTypeBlob) gitRef, path, treeNodeOutput.Node.Type, git.TreeNodeTypeBlob)
} }
blobReader, err := c.git.GetBlob(ctx, &git.GetBlobParams{ blobReader, err := c.git.GetBlob(ctx, &git.GetBlobParams{

View File

@ -101,9 +101,14 @@ func MapCommit(c *git.Commit) (*types.Commit, error) {
deletions += stat.Deletions deletions += stat.Deletions
} }
parentSHAs := make([]string, len(c.ParentSHAs))
for i, sha := range c.ParentSHAs {
parentSHAs[i] = sha.String()
}
return &types.Commit{ return &types.Commit{
SHA: c.SHA, SHA: c.SHA.String(),
ParentSHAs: c.ParentSHAs, ParentSHAs: parentSHAs,
Title: c.Title, Title: c.Title,
Message: c.Message, Message: c.Message,
Author: *author, Author: *author,
@ -145,8 +150,8 @@ func MapRenameDetails(c *git.RenameDetails) *types.RenameDetails {
return &types.RenameDetails{ return &types.RenameDetails{
OldPath: c.OldPath, OldPath: c.OldPath,
NewPath: c.NewPath, NewPath: c.NewPath,
CommitShaBefore: c.CommitShaBefore, CommitShaBefore: c.CommitShaBefore.String(),
CommitShaAfter: c.CommitShaAfter, CommitShaAfter: c.CommitShaAfter.String(),
} }
} }

View File

@ -24,7 +24,7 @@ import (
"github.com/harness/gitness/app/api/render" "github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request" "github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
gittypes "github.com/harness/gitness/git/types" gittypes "github.com/harness/gitness/git/api"
) )
// HandleDiff returns a http.HandlerFunc that returns diff. // HandleDiff returns a http.HandlerFunc that returns diff.

View File

@ -24,7 +24,7 @@ import (
"github.com/harness/gitness/app/api/render" "github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request" "github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
gittypes "github.com/harness/gitness/git/types" gittypes "github.com/harness/gitness/git/api"
) )
// HandleDiff returns the diff between two commits, branches or tags. // HandleDiff returns the diff between two commits, branches or tags.

View File

@ -21,7 +21,7 @@ import (
"github.com/harness/gitness/app/api/request" "github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gittypes "github.com/harness/gitness/git/types" gittypes "github.com/harness/gitness/git/api"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"

View File

@ -22,7 +22,7 @@ import (
"github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gittypes "github.com/harness/gitness/git/types" gittypes "github.com/harness/gitness/git/api"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"

View File

@ -21,7 +21,7 @@ import (
"strings" "strings"
"github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/api/usererror"
gittypes "github.com/harness/gitness/git/types" gittypes "github.com/harness/gitness/git/api"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )

View File

@ -20,7 +20,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/api"
) )
func TestGetFileDiffRequestsFromQuery(t *testing.T) { func TestGetFileDiffRequestsFromQuery(t *testing.T) {
@ -30,7 +30,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args args args
wantFiles types.FileDiffRequests wantFiles api.FileDiffRequests
}{ }{
{ {
name: "full range", name: "full range",
@ -42,7 +42,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
}, },
}, },
}, },
wantFiles: types.FileDiffRequests{ wantFiles: api.FileDiffRequests{
{ {
Path: "file.txt", Path: "file.txt",
StartLine: 1, StartLine: 1,
@ -60,7 +60,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
}, },
}, },
}, },
wantFiles: types.FileDiffRequests{ wantFiles: api.FileDiffRequests{
{ {
Path: "file.txt", Path: "file.txt",
StartLine: 1, StartLine: 1,
@ -77,7 +77,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
}, },
}, },
}, },
wantFiles: types.FileDiffRequests{ wantFiles: api.FileDiffRequests{
{ {
Path: "file.txt", Path: "file.txt",
EndLine: 20, EndLine: 20,
@ -94,7 +94,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
}, },
}, },
}, },
wantFiles: types.FileDiffRequests{ wantFiles: api.FileDiffRequests{
{ {
Path: "file.txt", Path: "file.txt",
EndLine: 20, EndLine: 20,
@ -119,7 +119,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
}, },
}, },
}, },
wantFiles: types.FileDiffRequests{ wantFiles: api.FileDiffRequests{
{ {
Path: "file.txt", Path: "file.txt",
EndLine: 20, EndLine: 20,

View File

@ -24,7 +24,6 @@ import (
"github.com/harness/gitness/app/services/webhook" "github.com/harness/gitness/app/services/webhook"
"github.com/harness/gitness/blob" "github.com/harness/gitness/blob"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
gittypes "github.com/harness/gitness/git/types"
"github.com/harness/gitness/lock" "github.com/harness/gitness/lock"
"github.com/harness/gitness/store" "github.com/harness/gitness/store"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
@ -40,7 +39,6 @@ func Translate(ctx context.Context, err error) *Error {
maxBytesErr *http.MaxBytesError maxBytesErr *http.MaxBytesError
codeOwnersTooLargeError *codeowners.TooLargeError codeOwnersTooLargeError *codeowners.TooLargeError
lockError *lock.Error lockError *lock.Error
pathNotFoundError *gittypes.PathNotFoundError
) )
// print original error for debugging purposes // print original error for debugging purposes
@ -88,13 +86,6 @@ func Translate(ctx context.Context, err error) *Error {
return RequestTooLargef("The request is too large. maximum allowed size is %d bytes", maxBytesErr.Limit) return RequestTooLargef("The request is too large. maximum allowed size is %d bytes", maxBytesErr.Limit)
// git errors // git errors
case errors.As(err, &pathNotFoundError):
return Newf(
http.StatusNotFound,
pathNotFoundError.Error(),
)
// application errors
case errors.As(err, &appError): case errors.As(err, &appError):
if appError.Err != nil { if appError.Err != nil {
log.Ctx(ctx).Warn().Err(appError.Err).Msgf("Application error translation is omitting internal details.") log.Ctx(ctx).Warn().Err(appError.Err).Msgf("Application error translation is omitting internal details.")

View File

@ -57,14 +57,14 @@ func (f *service) FindRef(
func (f *service) FindCommit( func (f *service) FindCommit(
ctx context.Context, ctx context.Context,
repo *types.Repository, repo *types.Repository,
sha string, rawSHA string,
) (*types.Commit, error) { ) (*types.Commit, error) {
readParams := git.ReadParams{ readParams := git.ReadParams{
RepoUID: repo.GitUID, RepoUID: repo.GitUID,
} }
commitOutput, err := f.git.GetCommit(ctx, &git.GetCommitParams{ commitOutput, err := f.git.GetCommit(ctx, &git.GetCommitParams{
ReadParams: readParams, ReadParams: readParams,
SHA: sha, Revision: rawSHA,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -25,7 +25,6 @@ import (
"github.com/harness/gitness/app/store" "github.com/harness/gitness/app/store"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gittypes "github.com/harness/gitness/git/types"
gitness_store "github.com/harness/gitness/store" gitness_store "github.com/harness/gitness/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
@ -237,7 +236,7 @@ func (s *Service) getCodeOwnerFile(
return &File{ return &File{
Content: string(content), Content: string(content),
SHA: output.SHA, SHA: output.SHA.String(),
TotalSize: output.Size, TotalSize: output.Size,
}, nil }, nil
} }
@ -256,7 +255,7 @@ func (s *Service) getCodeOwnerFileNode(
Path: path, Path: path,
}) })
if gittypes.IsPathNotFoundError(err) { if errors.IsNotFound(err) {
continue continue
} }
if err != nil { if err != nil {

View File

@ -97,7 +97,7 @@ func (s *Service) triggerPREventOnBranchUpdate(ctx context.Context,
pr.Edited = time.Now().UnixMilli() pr.Edited = time.Now().UnixMilli()
pr.SourceSHA = event.Payload.NewSHA pr.SourceSHA = event.Payload.NewSHA
pr.MergeBaseSHA = newMergeBase pr.MergeBaseSHA = newMergeBase.String()
// reset merge-check fields for new run // reset merge-check fields for new run
pr.MergeCheckStatus = enum.MergeCheckStatusUnchecked pr.MergeCheckStatus = enum.MergeCheckStatusUnchecked
@ -137,7 +137,7 @@ func (s *Service) triggerPREventOnBranchUpdate(ctx context.Context,
OldSHA: event.Payload.OldSHA, OldSHA: event.Payload.OldSHA,
NewSHA: event.Payload.NewSHA, NewSHA: event.Payload.NewSHA,
OldMergeBaseSHA: oldMergeBase, OldMergeBaseSHA: oldMergeBase,
NewMergeBaseSHA: newMergeBase, NewMergeBaseSHA: newMergeBase.String(),
Forced: event.Payload.Forced, Forced: event.Payload.Forced,
}) })

View File

@ -23,6 +23,7 @@ import (
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gitenum "github.com/harness/gitness/git/enum" gitenum "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/sha"
) )
// createHeadRefOnCreated handles pull request Created events. // createHeadRefOnCreated handles pull request Created events.
@ -46,8 +47,8 @@ func (s *Service) createHeadRefOnCreated(ctx context.Context,
WriteParams: writeParams, WriteParams: writeParams,
Name: strconv.Itoa(int(event.Payload.Number)), Name: strconv.Itoa(int(event.Payload.Number)),
Type: gitenum.RefTypePullReqHead, Type: gitenum.RefTypePullReqHead,
NewValue: event.Payload.SourceSHA, NewValue: sha.Must(event.Payload.SourceSHA),
OldValue: "", // this is a new pull request, so we expect that the ref doesn't exist OldValue: sha.None, // this is a new pull request, so we expect that the ref doesn't exist
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to update PR head ref: %w", err) return fmt.Errorf("failed to update PR head ref: %w", err)
@ -77,8 +78,8 @@ func (s *Service) updateHeadRefOnBranchUpdate(ctx context.Context,
WriteParams: writeParams, WriteParams: writeParams,
Name: strconv.Itoa(int(event.Payload.Number)), Name: strconv.Itoa(int(event.Payload.Number)),
Type: gitenum.RefTypePullReqHead, Type: gitenum.RefTypePullReqHead,
NewValue: event.Payload.NewSHA, NewValue: sha.Must(event.Payload.NewSHA),
OldValue: event.Payload.OldSHA, OldValue: sha.Must(event.Payload.OldSHA),
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to update PR head ref after new commit: %w", err) return fmt.Errorf("failed to update PR head ref after new commit: %w", err)
@ -108,8 +109,8 @@ func (s *Service) updateHeadRefOnReopen(ctx context.Context,
WriteParams: writeParams, WriteParams: writeParams,
Name: strconv.Itoa(int(event.Payload.Number)), Name: strconv.Itoa(int(event.Payload.Number)),
Type: gitenum.RefTypePullReqHead, Type: gitenum.RefTypePullReqHead,
NewValue: event.Payload.SourceSHA, NewValue: sha.Must(event.Payload.SourceSHA),
OldValue: "", // the request is re-opened, so anything can be the old value OldValue: sha.None, // the request is re-opened, so anything can be the old value
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to update PR head ref after pull request reopen: %w", err) return fmt.Errorf("failed to update PR head ref after pull request reopen: %w", err)

View File

@ -25,10 +25,12 @@ import (
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
gitenum "github.com/harness/gitness/git/enum" gitenum "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/sha"
"github.com/harness/gitness/pubsub" "github.com/harness/gitness/pubsub"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/gotidy/ptr"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -109,8 +111,8 @@ func (s *Service) deleteMergeRef(ctx context.Context, repoID int64, prNum int64)
WriteParams: writeParams, WriteParams: writeParams,
Name: strconv.Itoa(int(prNum)), Name: strconv.Itoa(int(prNum)),
Type: gitenum.RefTypePullReqMerge, Type: gitenum.RefTypePullReqMerge,
NewValue: "", // when NewValue is empty will delete the ref. NewValue: sha.None, // when NewValue is empty will delete the ref.
OldValue: "", // we don't care about the old value OldValue: sha.None, // we don't care about the old value
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to remove PR merge ref: %w", err) return fmt.Errorf("failed to remove PR merge ref: %w", err)
@ -205,7 +207,7 @@ func (s *Service) updateMergeDataInner(
HeadBranch: pr.SourceBranch, HeadBranch: pr.SourceBranch,
RefType: gitenum.RefTypePullReqMerge, RefType: gitenum.RefTypePullReqMerge,
RefName: strconv.Itoa(int(pr.Number)), RefName: strconv.Itoa(int(pr.Number)),
HeadExpectedSHA: newSHA, HeadExpectedSHA: sha.Must(newSHA),
Force: true, Force: true,
// set committer date to ensure repeatability of merge commit across replicas // set committer date to ensure repeatability of merge commit across replicas
@ -222,17 +224,17 @@ func (s *Service) updateMergeDataInner(
return events.NewDiscardEventErrorf("PR SHA %s is newer than %s", pr.SourceSHA, newSHA) return events.NewDiscardEventErrorf("PR SHA %s is newer than %s", pr.SourceSHA, newSHA)
} }
if mergeOutput.MergeSHA == "" || len(mergeOutput.ConflictFiles) > 0 { if mergeOutput.MergeSHA.IsEmpty() || len(mergeOutput.ConflictFiles) > 0 {
pr.MergeCheckStatus = enum.MergeCheckStatusConflict pr.MergeCheckStatus = enum.MergeCheckStatusConflict
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
pr.MergeTargetSHA = &mergeOutput.BaseSHA pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
pr.MergeSHA = nil pr.MergeSHA = nil
pr.MergeConflicts = mergeOutput.ConflictFiles pr.MergeConflicts = mergeOutput.ConflictFiles
} else { } else {
pr.MergeCheckStatus = enum.MergeCheckStatusMergeable pr.MergeCheckStatus = enum.MergeCheckStatusMergeable
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
pr.MergeTargetSHA = &mergeOutput.BaseSHA pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
pr.MergeSHA = &mergeOutput.MergeSHA pr.MergeSHA = ptr.String(mergeOutput.MergeSHA.String())
pr.MergeConflicts = nil pr.MergeConflicts = nil
} }
pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount) pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount)

View File

@ -151,22 +151,22 @@ func (s *Service) handleEventBranchDeleted(ctx context.Context,
}) })
} }
func (s *Service) fetchCommitInfoForEvent(ctx context.Context, repoUID string, sha string) (CommitInfo, error) { func (s *Service) fetchCommitInfoForEvent(ctx context.Context, repoUID string, commitSHA string) (CommitInfo, error) {
out, err := s.git.GetCommit(ctx, &git.GetCommitParams{ out, err := s.git.GetCommit(ctx, &git.GetCommitParams{
ReadParams: git.ReadParams{ ReadParams: git.ReadParams{
RepoUID: repoUID, RepoUID: repoUID,
}, },
SHA: sha, Revision: commitSHA,
}) })
if errors.AsStatus(err) == errors.StatusNotFound { if errors.AsStatus(err) == errors.StatusNotFound {
// this could happen if the commit has been deleted and garbage collected by now // this could happen if the commit has been deleted and garbage collected by now
// or if the targetSha doesn't point to an event - either way discard the event. // or if the targetSha doesn't point to an event - either way discard the event.
return CommitInfo{}, events.NewDiscardEventErrorf("commit with targetSha '%s' doesn't exist", sha) return CommitInfo{}, events.NewDiscardEventErrorf("commit with targetSha '%s' doesn't exist", commitSHA)
} }
if err != nil { if err != nil {
return CommitInfo{}, fmt.Errorf("failed to get commit with targetSha '%s': %w", sha, err) return CommitInfo{}, fmt.Errorf("failed to get commit with targetSha '%s': %w", commitSHA, err)
} }
return commitInfoFrom(out.Commit), nil return commitInfoFrom(out.Commit), nil

View File

@ -208,7 +208,7 @@ func commitInfoFrom(commit git.Commit) CommitInfo {
} }
return CommitInfo{ return CommitInfo{
SHA: commit.SHA, SHA: commit.SHA.String(),
Message: commit.Message, Message: commit.Message,
Author: signatureInfoFrom(commit.Author), Author: signatureInfoFrom(commit.Author),
Committer: signatureInfoFrom(commit.Committer), Committer: signatureInfoFrom(commit.Committer),

View File

@ -78,7 +78,7 @@ import (
"github.com/harness/gitness/encrypt" "github.com/harness/gitness/encrypt"
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
"github.com/harness/gitness/git/adapter" "github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/storage" "github.com/harness/gitness/git/storage"
"github.com/harness/gitness/job" "github.com/harness/gitness/job"
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
@ -126,7 +126,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
pullreqevents.WireSet, pullreqevents.WireSet,
repoevents.WireSet, repoevents.WireSet,
storage.WireSet, storage.WireSet,
adapter.WireSet, api.WireSet,
cliserver.ProvideGitConfig, cliserver.ProvideGitConfig,
git.WireSet, git.WireSet,
store.WireSet, store.WireSet,

View File

@ -77,7 +77,7 @@ import (
"github.com/harness/gitness/encrypt" "github.com/harness/gitness/encrypt"
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
"github.com/harness/gitness/git/adapter" "github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/storage" "github.com/harness/gitness/git/storage"
"github.com/harness/gitness/job" "github.com/harness/gitness/job"
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
@ -135,17 +135,17 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
cacheCache, err := adapter.ProvideLastCommitCache(typesConfig, universalClient) cacheCache, err := api.ProvideLastCommitCache(typesConfig, universalClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }
clientFactory := githook.ProvideFactory() clientFactory := githook.ProvideFactory()
gitAdapter, err := git.ProvideGITAdapter(typesConfig, cacheCache, clientFactory) apiGit, err := git.ProvideGITAdapter(typesConfig, cacheCache, clientFactory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
storageStore := storage.ProvideLocalStore() storageStore := storage.ProvideLocalStore()
gitInterface, err := git.ProvideService(typesConfig, gitAdapter, storageStore) gitInterface, err := git.ProvideService(typesConfig, apiGit, storageStore)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,155 +0,0 @@
// 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"
"io"
"github.com/harness/gitness/git/adapter"
"github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types"
"code.gitea.io/gitea/modules/git"
)
var _ Adapter = (*adapter.Adapter)(nil)
// Adapter for accessing git commands from gitea.
type Adapter interface {
InitRepository(ctx context.Context, path string, bare bool) error
OpenRepository(ctx context.Context, path string) (*git.Repository, error)
SharedRepository(tmp string, repoUID string, remotePath string) (*adapter.SharedRepo, error)
Config(ctx context.Context, repoPath, key, value string) error
CountObjects(ctx context.Context, repoPath string) (types.ObjectCount, error)
SetDefaultBranch(ctx context.Context, repoPath string,
defaultBranch string, allowEmpty bool) error
GetDefaultBranch(ctx context.Context, repoPath string) (string, error)
GetRemoteDefaultBranch(ctx context.Context,
remoteURL string) (string, error)
HasBranches(ctx context.Context, repoPath string) (bool, error)
Clone(ctx context.Context, from, to string, opts types.CloneRepoOptions) error
AddFiles(repoPath string, all bool, files ...string) error
Commit(ctx context.Context, repoPath string, opts types.CommitChangesOptions) error
Push(ctx context.Context, repoPath string, opts types.PushOptions) error
ReadTree(ctx context.Context, repoPath, ref string, w io.Writer, args ...string) error
GetTreeNode(ctx context.Context, repoPath string, ref string, treePath string) (*types.TreeNode, error)
ListTreeNodes(ctx context.Context, repoPath string, ref string, treePath string) ([]types.TreeNode, error)
PathsDetails(ctx context.Context, repoPath string, ref string, paths []string) ([]types.PathDetails, error)
GetSubmodule(ctx context.Context, repoPath string, ref string, treePath string) (*types.Submodule, error)
GetBlob(ctx context.Context, repoPath string, sha string, sizeLimit int64) (*types.BlobReader, error)
WalkReferences(ctx context.Context, repoPath string, handler types.WalkReferencesHandler,
opts *types.WalkReferencesOptions) error
GetCommit(ctx context.Context, repoPath string, ref string) (*types.Commit, error)
GetCommits(ctx context.Context, repoPath string, refs []string) ([]types.Commit, error)
ListCommits(
ctx context.Context,
repoPath string,
ref string,
page int,
limit int,
includeStats bool,
filter types.CommitFilter) ([]types.Commit, []types.PathRenameDetails, error)
ListCommitSHAs(ctx context.Context, repoPath string,
ref string, page int, limit int, filter types.CommitFilter) ([]string, error)
GetLatestCommit(ctx context.Context, repoPath string, ref string, treePath string) (*types.Commit, error)
GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error)
GetAnnotatedTag(ctx context.Context, repoPath string, sha string) (*types.Tag, error)
GetAnnotatedTags(ctx context.Context, repoPath string, shas []string) ([]types.Tag, error)
CreateTag(ctx context.Context, repoPath string, name string, targetSHA string, opts *types.CreateTagOptions) error
GetBranch(ctx context.Context, repoPath string, branchName string) (*types.Branch, error)
GetCommitDivergences(ctx context.Context, repoPath string,
requests []types.CommitDivergenceRequest, max int32) ([]types.CommitDivergence, error)
GetRef(ctx context.Context, repoPath string, reference string) (string, error)
UpdateRef(ctx context.Context, envVars map[string]string, repoPath, reference, newValue, oldValue string) error
CreateTemporaryRepoForPR(ctx context.Context, reposTempPath string, pr *types.PullRequest,
baseBranch, trackingBranch string) (types.TempRepository, error)
Merge(ctx context.Context, pr *types.PullRequest, mergeMethod enum.MergeMethod, baseBranch, trackingBranch string,
tmpBasePath string, mergeMsg string, identity *types.Identity, env ...string) (types.MergeResult, error)
GetMergeBase(ctx context.Context, repoPath, remote, base, head string) (string, string, error)
IsAncestor(ctx context.Context, repoPath, ancestorCommitSHA, descendantCommitSHA string) (bool, error)
Blame(ctx context.Context, repoPath, rev, file string, lineFrom, lineTo int) types.BlameReader
Sync(ctx context.Context, repoPath string, source string, refSpecs []string) error
//
// Diff operations
//
GetDiffTree(ctx context.Context,
repoPath,
baseBranch,
headBranch string) (string, error)
RawDiff(ctx context.Context,
w io.Writer,
repoPath,
base,
head string,
mergeBase bool,
paths ...types.FileDiffRequest) error
CommitDiff(ctx context.Context,
repoPath,
sha string,
w io.Writer) error
DiffShortStat(ctx context.Context,
repoPath string,
baseRef string,
headRef string,
useMergeBase bool) (types.DiffShortStat, error)
GetDiffHunkHeaders(ctx context.Context,
repoPath string,
targetRef string,
sourceRef string) ([]*types.DiffFileHunkHeaders, error)
DiffCut(ctx context.Context,
repoPath string,
targetRef string,
sourceRef string,
path string,
params types.DiffCutParams) (types.HunkHeader, types.Hunk, error)
MatchFiles(ctx context.Context,
repoPath string,
ref string,
dirPath string,
regExpDef string,
maxSize int) ([]types.FileContent, error)
// http
InfoRefs(
ctx context.Context,
repoPath string,
service string,
w io.Writer,
env ...string,
) error
ServicePack(
ctx context.Context,
repoPath string,
service string,
stdin io.Reader,
stdout io.Writer,
env ...string,
) error
DiffFileName(ctx context.Context,
repoPath string,
baseRef string,
headRef string,
mergeBase bool) ([]string, error)
}

View File

@ -1,54 +0,0 @@
// 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 adapter_test
import (
"context"
"io"
"testing"
"github.com/harness/gitness/errors"
)
func TestBlameEmptyFile(t *testing.T) {
git := setupGit(t)
repo, teardown := setupRepo(t, git, "testblameemptyfile")
defer teardown()
baseBranch := "main"
// write empty file to main branch
_, parentSHA := writeFile(t, repo, "file.txt", "", nil)
err := repo.SetReference("refs/heads/"+baseBranch, parentSHA.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", baseBranch, err)
}
reader := git.Blame(context.Background(), repo.Path, "main", "file.txt", 0, 0)
part, err := reader.NextPart()
if err != nil {
if errors.Is(err, io.EOF) {
return
}
t.Errorf("Blame reader should return empty string but got error: %v", err)
return
}
if part != nil {
t.Errorf("Blame reader should be nil but got: %v", part)
return
}
}

View File

@ -1,153 +0,0 @@
// 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 adapter_test
import (
"bytes"
"context"
"testing"
)
func TestAdapter_RawDiff(t *testing.T) {
git := setupGit(t)
repo, teardown := setupRepo(t, git, "testrawdiff")
defer teardown()
testFileName := "file.txt"
baseBranch := "main"
// write file to main branch
oid1, parentSHA := writeFile(t, repo, testFileName, "some content", nil)
err := repo.SetReference("refs/heads/"+baseBranch, parentSHA.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", baseBranch, err)
}
baseTag := "0.0.1"
err = repo.CreateAnnotatedTag(baseTag, "test tag 1", parentSHA.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", baseTag, err)
}
headBranch := "dev"
// create branch
err = repo.CreateBranch(headBranch, baseBranch)
if err != nil {
t.Fatalf("failed creating a branch '%s': %v", headBranch, err)
}
// write file to main branch
oid2, sha := writeFile(t, repo, testFileName, "new content", []string{parentSHA.String()})
err = repo.SetReference("refs/heads/"+headBranch, sha.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", headBranch, err)
}
headTag := "0.0.2"
err = repo.CreateAnnotatedTag(headTag, "test tag 2", sha.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", headTag, err)
}
want := `diff --git a/` + testFileName + ` b/` + testFileName + `
index ` + oid1.String() + `..` + oid2.String() + ` 100644
--- a/` + testFileName + `
+++ b/` + testFileName + `
@@ -1 +1 @@
-some content
\ No newline at end of file
+new content
\ No newline at end of file
`
type args struct {
ctx context.Context
repoPath string
baseRef string
headRef string
mergeBase bool
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "test branches",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseBranch,
headRef: headBranch,
mergeBase: false,
},
wantW: want,
wantErr: false,
},
{
name: "test annotated tag",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseTag,
headRef: headTag,
mergeBase: false,
},
wantW: want,
wantErr: false,
},
{
name: "test branches using merge-base",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseBranch,
headRef: headBranch,
mergeBase: true,
},
wantW: want,
wantErr: false,
},
{
name: "test annotated tag using merge-base",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseTag,
headRef: headTag,
mergeBase: true,
},
wantW: want,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := git.RawDiff(tt.args.ctx, w, tt.args.repoPath, tt.args.baseRef, tt.args.headRef, tt.args.mergeBase)
if (err != nil) != tt.wantErr {
t.Errorf("RawDiff() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("RawDiff() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}

View File

@ -1,133 +0,0 @@
// 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 adapter
import (
"os/exec"
"strings"
"github.com/harness/gitness/errors"
gitea "code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log"
)
var (
ErrRepositoryPathEmpty = errors.InvalidArgument("repository path cannot be empty")
ErrBranchNameEmpty = errors.InvalidArgument("branch name cannot be empty")
ErrParseDiffHunkHeader = errors.Internal(nil, "failed to parse diff hunk header")
)
type runStdError struct {
err error
stderr string
errMsg string
}
func (r *runStdError) Error() string {
// the stderr must be in the returned error text, some code only checks `strings.Contains(err.Error(), "git error")`
if r.errMsg == "" {
r.errMsg = gitea.ConcatenateError(r.err, r.stderr).Error()
}
return r.errMsg
}
func (r *runStdError) Unwrap() error {
return r.err
}
func (r *runStdError) Stderr() string {
return r.stderr
}
func (r *runStdError) IsExitCode(code int) bool {
var exitError *exec.ExitError
if errors.As(r.err, &exitError) {
return exitError.ExitCode() == code
}
return false
}
// Logs the error and message, returns either the provided message or a git equivalent if possible.
// Always logs the full message with error as warning.
func processGiteaErrorf(err error, format string, args ...interface{}) error {
// create fallback error returned if we can't map it
fallbackErr := errors.Internal(err, format, args...)
// always log internal error together with message.
log.Warn().Msgf("%v: [GITEA] %v", fallbackErr, err)
// check if it's a RunStdError error (contains raw git error)
var runStdErr gitea.RunStdError
if errors.As(err, &runStdErr) {
return mapGiteaRunStdError(runStdErr, fallbackErr)
}
switch {
// gitea is using errors.New(no such file or directory") exclusively for OpenRepository ... (at least as of now)
case err.Error() == "no such file or directory":
return errors.NotFound("repository not found")
case gitea.IsErrNotExist(err):
return errors.NotFound(format, args, err)
case gitea.IsErrBranchNotExist(err):
return errors.NotFound(format, args, err)
default:
return fallbackErr
}
}
// TODO: Improve gitea error handling.
// Doubt this will work for all std errors, as git doesn't seem to have nice error codes.
func mapGiteaRunStdError(err gitea.RunStdError, fallback error) error {
switch {
// exit status 128 - fatal: A branch named 'mybranch' already exists.
// exit status 128 - fatal: cannot lock ref 'refs/heads/a': 'refs/heads/a/b' exists; cannot create 'refs/heads/a'
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "exists"):
return errors.Conflict(err.Stderr())
// exit status 128 - fatal: 'a/bc/d/' is not a valid branch name.
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "not a valid"):
return errors.InvalidArgument(err.Stderr())
// exit status 1 - error: branch 'mybranch' not found.
case err.IsExitCode(1) && strings.Contains(err.Stderr(), "not found"):
return errors.NotFound(err.Stderr())
// exit status 128 - fatal: ambiguous argument 'branch1...branch2': unknown revision or path not in the working tree.
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "unknown revision"):
msg := "unknown revision or path not in the working tree"
// parse the error response from git output
lines := strings.Split(err.Error(), "\n")
if len(lines) > 0 {
cols := strings.Split(lines[0], ": ")
if len(cols) >= 2 {
msg = cols[1] + ", " + cols[2]
}
}
return errors.NotFound(msg)
// exit status 128 - fatal: couldn't find remote ref v1.
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "couldn't find"):
return errors.NotFound(err.Stderr())
// exit status 128 - fatal: unable to access 'http://127.0.0.1:4101/hvfl1xj5fojwlrw77xjflw80uxjous254jrr967rvj/':
// Failed to connect to 127.0.0.1 port 4101 after 4 ms: Connection refused
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "Failed to connect"):
return errors.Internal(err, "failed to connect")
default:
return fallback
}
}

View File

@ -1,104 +0,0 @@
// 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 adapter
import (
"fmt"
"strings"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
)
func mapGiteaRawRef(
raw map[string]string,
) (map[types.GitReferenceField]string, error) {
res := make(map[types.GitReferenceField]string, len(raw))
for k, v := range raw {
gitRefField, err := types.ParseGitReferenceField(k)
if err != nil {
return nil, err
}
res[gitRefField] = v
}
return res, nil
}
func mapToGiteaReferenceSortingArgument(
s types.GitReferenceField,
o types.SortOrder,
) string {
sortBy := string(types.GitReferenceFieldRefName)
desc := o == types.SortOrderDesc
if s == types.GitReferenceFieldCreatorDate {
sortBy = string(types.GitReferenceFieldCreatorDate)
if o == types.SortOrderDefault {
desc = true
}
}
if desc {
return "-" + sortBy
}
return sortBy
}
func mapGiteaCommit(giteaCommit *gitea.Commit) (*types.Commit, error) {
if giteaCommit == nil {
return nil, fmt.Errorf("gitea commit is nil")
}
author, err := mapGiteaSignature(giteaCommit.Author)
if err != nil {
return nil, fmt.Errorf("failed to map gitea author: %w", err)
}
committer, err := mapGiteaSignature(giteaCommit.Committer)
if err != nil {
return nil, fmt.Errorf("failed to map gitea commiter: %w", err)
}
parentShas := make([]string, len(giteaCommit.Parents))
for i := range giteaCommit.Parents {
parentShas[i] = giteaCommit.Parents[i].String()
}
return &types.Commit{
SHA: giteaCommit.ID.String(),
ParentSHAs: parentShas,
Title: giteaCommit.Summary(),
// remove potential tailing newlines from message
Message: strings.TrimRight(giteaCommit.Message(), "\n"),
Author: author,
Committer: committer,
}, nil
}
func mapGiteaSignature(
giteaSignature *gitea.Signature,
) (types.Signature, error) {
if giteaSignature == nil {
return types.Signature{}, fmt.Errorf("gitea signature is nil")
}
return types.Signature{
Identity: types.Identity{
Name: giteaSignature.Name,
Email: giteaSignature.Email,
},
When: giteaSignature.When,
}, nil
}

View File

@ -1,635 +0,0 @@
// 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 adapter
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/tempdir"
"github.com/harness/gitness/git/types"
"code.gitea.io/gitea/modules/git"
)
// CreateTemporaryRepo creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
// it also create a second base branch called "original_base".
//
//nolint:funlen,gocognit // need refactor
func (a Adapter) CreateTemporaryRepoForPR(
ctx context.Context,
reposTempPath string,
pr *types.PullRequest,
baseBranch string,
trackingBranch string,
) (types.TempRepository, error) {
if pr.BaseRepoPath == "" && pr.HeadRepoPath != "" {
pr.BaseRepoPath = pr.HeadRepoPath
}
if pr.HeadRepoPath == "" && pr.BaseRepoPath != "" {
pr.HeadRepoPath = pr.BaseRepoPath
}
if pr.BaseBranch == "" {
return types.TempRepository{}, errors.New("empty base branch")
}
if pr.HeadBranch == "" {
return types.TempRepository{}, errors.New("empty head branch")
}
baseRepoPath := pr.BaseRepoPath
headRepoPath := pr.HeadRepoPath
// Clone base repo.
tmpBasePath, err := tempdir.CreateTemporaryPath(reposTempPath, "pull")
if err != nil {
return types.TempRepository{}, err
}
if err = a.InitRepository(ctx, tmpBasePath, false); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return types.TempRepository{}, err
}
remoteRepoName := "head_repo"
// Add head repo remote.
addCacheRepo := func(staging, cache string) error {
var f *os.File
alternates := filepath.Join(staging, ".git", "objects", "info", "alternates")
f, err = os.OpenFile(alternates, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("failed to open alternates file '%s': %w", alternates, err)
}
defer f.Close()
data := filepath.Join(cache, "objects")
if _, err = fmt.Fprintln(f, data); err != nil {
return fmt.Errorf("failed to write alternates file '%s': %w", alternates, err)
}
return nil
}
if err = addCacheRepo(tmpBasePath, baseRepoPath); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return types.TempRepository{},
fmt.Errorf("unable to add base repository to temporary repo [%s -> tmpBasePath]: %w", pr.BaseRepoPath, err)
}
var outbuf, errbuf strings.Builder
if err = git.NewCommand(ctx, "remote", "add", "-t", pr.BaseBranch, "-m", pr.BaseBranch, "origin", baseRepoPath).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return types.TempRepository{}, processGiteaErrorf(giteaErr, "unable to add base repository as origin "+
"[%s -> tmpBasePath]:\n%s\n%s", pr.BaseRepoPath, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
// Fetch base branch
baseCommit, err := a.GetCommit(ctx, pr.BaseRepoPath, pr.BaseBranch)
if err != nil {
return types.TempRepository{}, fmt.Errorf("failed to get commit of base branch '%s', error: %w", pr.BaseBranch, err)
}
baseID := baseCommit.SHA
if err = git.NewCommand(ctx, "fetch", "origin", "--no-tags", "--",
baseID+":"+baseBranch, baseID+":original_"+baseBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return types.TempRepository{}, processGiteaErrorf(giteaErr, "unable to fetch origin base branch "+
"[%s:%s -> base, original_base in tmpBasePath].\n%s\n%s",
pr.BaseRepoPath, pr.BaseBranch, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
if err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+baseBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return types.TempRepository{}, processGiteaErrorf(giteaErr, "unable to set HEAD as base "+
"branch [tmpBasePath]:\n%s\n%s", outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
if err = addCacheRepo(tmpBasePath, headRepoPath); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return types.TempRepository{}, processGiteaErrorf(giteaErr, "unable to head base repository "+
"to temporary repo [%s -> tmpBasePath]", pr.HeadRepoPath)
}
if err = git.NewCommand(ctx, "remote", "add", remoteRepoName, headRepoPath).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return types.TempRepository{}, processGiteaErrorf(giteaErr, "unable to add head repository as head_repo "+
"[%s -> tmpBasePath]:\n%s\n%s", pr.HeadRepoPath, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
headCommit, err := a.GetCommit(ctx, pr.HeadRepoPath, pr.HeadBranch)
if err != nil {
return types.TempRepository{}, fmt.Errorf("failed to get commit of head branch '%s', error: %w", pr.HeadBranch, err)
}
headID := headCommit.SHA
if err = git.NewCommand(ctx, "fetch", "--no-tags", remoteRepoName, headID+":"+trackingBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return types.TempRepository{}, processGiteaErrorf(giteaErr, "unable to fetch head_repo head branch "+
"[%s:%s -> tracking in tmpBasePath]:\n%s\n%s",
pr.HeadRepoPath, pr.HeadBranch, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
return types.TempRepository{
Path: tmpBasePath,
BaseSHA: baseID,
HeadSHA: headID,
}, nil
}
type runMergeResult struct {
conflictFiles []string
}
func runMergeCommand(
ctx context.Context,
pr *types.PullRequest,
mergeMethod enum.MergeMethod,
cmd *git.Command,
tmpBasePath string,
env []string,
) (runMergeResult, error) {
var outbuf, errbuf strings.Builder
if err := cmd.Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
Env: env,
}); err != nil {
if strings.Contains(errbuf.String(), "refusing to merge unrelated histories") {
return runMergeResult{}, &types.MergeUnrelatedHistoriesError{
Method: mergeMethod,
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
}
// Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict
if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil {
// We have a merge conflict error
files, cferr := conflictFiles(ctx, pr, env, tmpBasePath)
if cferr != nil {
return runMergeResult{}, cferr
}
return runMergeResult{
conflictFiles: files,
}, nil
}
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return runMergeResult{}, processGiteaErrorf(giteaErr, "git merge [%s -> %s]\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, outbuf.String(), errbuf.String())
}
return runMergeResult{}, nil
}
func commitAndSignNoAuthor(
ctx context.Context,
pr *types.PullRequest,
message string,
signArg string,
tmpBasePath string,
env []string,
) error {
var outbuf, errbuf strings.Builder
if signArg == "" {
if err := git.NewCommand(ctx, "commit", "-m", message).
Run(&git.RunOpts{
Env: env,
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return processGiteaErrorf(err, "git commit [%s -> %s]\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, outbuf.String(), errbuf.String())
}
} else {
if err := git.NewCommand(ctx, "commit", signArg, "-m", message).
Run(&git.RunOpts{
Env: env,
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return processGiteaErrorf(err, "git commit [%s -> %s]\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, outbuf.String(), errbuf.String())
}
}
return nil
}
// Merge merges changes between 2 refs (branch, commits or tags).
//
//nolint:gocognit,nestif
func (a Adapter) Merge(
ctx context.Context,
pr *types.PullRequest,
mergeMethod enum.MergeMethod,
baseBranch string,
trackingBranch string,
tmpBasePath string,
mergeMsg string,
identity *types.Identity,
env ...string,
) (types.MergeResult, error) {
var (
outbuf, errbuf strings.Builder
)
if mergeMsg == "" {
mergeMsg = "Merge commit"
}
stagingBranch := "staging"
// TODO: sign merge commit
signArg := "--no-gpg-sign"
switch mergeMethod {
case enum.MergeMethodMerge:
cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit", trackingBranch)
result, err := runMergeCommand(ctx, pr, mergeMethod, cmd, tmpBasePath, env)
if err != nil {
return types.MergeResult{}, fmt.Errorf("unable to merge tracking into base: %w", err)
}
if len(result.conflictFiles) > 0 {
return types.MergeResult{ConflictFiles: result.conflictFiles}, nil
}
if err := commitAndSignNoAuthor(ctx, pr, mergeMsg, signArg, tmpBasePath, env); err != nil {
return types.MergeResult{}, fmt.Errorf("unable to make final commit: %w", err)
}
case enum.MergeMethodSquash:
// Merge with squash
cmd := git.NewCommand(ctx, "merge", "--squash", trackingBranch)
result, err := runMergeCommand(ctx, pr, mergeMethod, cmd, tmpBasePath, env)
if err != nil {
return types.MergeResult{}, fmt.Errorf("unable to merge --squash tracking into base: %w", err)
}
if len(result.conflictFiles) > 0 {
return types.MergeResult{ConflictFiles: result.conflictFiles}, nil
}
if signArg == "" {
if err := git.NewCommand(ctx, "commit", fmt.Sprintf("--author='%s'", identity.String()), "-m", mergeMsg).
Run(&git.RunOpts{
Env: env,
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return types.MergeResult{}, processGiteaErrorf(err, "git commit [%s -> %s]\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, outbuf.String(), errbuf.String())
}
} else {
if err := git.NewCommand(ctx, "commit", signArg, fmt.Sprintf("--author='%s'", identity.String()), "-m", mergeMsg).
Run(&git.RunOpts{
Env: env,
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return types.MergeResult{}, processGiteaErrorf(err, "git commit [%s -> %s]\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, outbuf.String(), errbuf.String())
}
}
case enum.MergeMethodRebase:
// Create staging branch
if err := git.NewCommand(ctx, "checkout", "-b", stagingBranch, trackingBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return types.MergeResult{}, fmt.Errorf(
"git checkout base prior to merge post staging rebase [%s -> %s]: %w\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, err, outbuf.String(), errbuf.String(),
)
}
outbuf.Reset()
errbuf.Reset()
var conflicts bool
// Rebase before merging
if err := git.NewCommand(ctx, "rebase", baseBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
// Rebase will leave a REBASE_HEAD file in .git if there is a conflict
if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "REBASE_HEAD")); statErr == nil {
// Rebase works by processing commit by commit. To get the full list of conflict files
// all commits would have to be applied. It's simpler to revert the rebase and
// get the list conflict using git merge.
conflicts = true
} else {
return types.MergeResult{}, fmt.Errorf(
"git rebase staging on to base [%s -> %s]: %w\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, err, outbuf.String(), errbuf.String(),
)
}
}
outbuf.Reset()
errbuf.Reset()
if conflicts {
// Rebase failed because there are conflicts. Abort the rebase.
if err := git.NewCommand(ctx, "rebase", "--abort").
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return types.MergeResult{}, fmt.Errorf(
"git abort rebase [%s -> %s]: %w\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, err, outbuf.String(), errbuf.String(),
)
}
outbuf.Reset()
errbuf.Reset()
// Go back to the base branch.
if err := git.NewCommand(ctx, "checkout", baseBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return types.MergeResult{}, fmt.Errorf(
"return to the base branch [%s -> %s]: %w\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, err, outbuf.String(), errbuf.String(),
)
}
outbuf.Reset()
errbuf.Reset()
// Run the ordinary merge to get the list of conflict files.
cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit", trackingBranch)
result, err := runMergeCommand(ctx, pr, mergeMethod, cmd, tmpBasePath, env)
if err != nil {
return types.MergeResult{}, fmt.Errorf(
"git abort rebase [%s -> %s]: %w\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, err, outbuf.String(), errbuf.String(),
)
}
if len(result.conflictFiles) > 0 {
return types.MergeResult{ConflictFiles: result.conflictFiles}, nil
}
return types.MergeResult{}, errors.New("rebase reported conflicts, but merge gave no conflict files")
}
// Checkout base branch again
if err := git.NewCommand(ctx, "checkout", baseBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return types.MergeResult{}, fmt.Errorf(
"git checkout base prior to merge post staging rebase [%s -> %s]: %w\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, err, outbuf.String(), errbuf.String(),
)
}
outbuf.Reset()
errbuf.Reset()
// Prepare merge with commit
cmd := git.NewCommand(ctx, "merge", "--ff-only", stagingBranch)
result, err := runMergeCommand(ctx, pr, mergeMethod, cmd, tmpBasePath, env)
if err != nil {
return types.MergeResult{}, fmt.Errorf("unable to ff-olny merge tracking into base: %w", err)
}
if len(result.conflictFiles) > 0 {
return types.MergeResult{ConflictFiles: result.conflictFiles}, nil
}
default:
return types.MergeResult{}, fmt.Errorf("wrong merge method provided: %s", mergeMethod)
}
return types.MergeResult{}, nil
}
func conflictFiles(
ctx context.Context,
pr *types.PullRequest,
env []string,
repoPath string,
) (files []string, err error) {
stdout, stderr, err := git.NewCommand(
ctx, "diff", "--name-only", "--diff-filter=U", "--relative",
).RunStdString(&git.RunOpts{
Env: env,
Dir: repoPath,
})
if err != nil {
return nil, processGiteaErrorf(err, "failed to list conflict files [%s -> %s], stderr: %v, err: %v",
pr.HeadBranch, pr.BaseBranch, stderr, err)
}
if len(stdout) > 0 {
files = strings.Split(stdout[:len(stdout)-1], "\n")
}
return files, nil
}
func (a Adapter) GetDiffTree(
ctx context.Context,
repoPath string,
baseBranch string,
headBranch string,
) (string, error) {
if repoPath == "" {
return "", ErrRepositoryPathEmpty
}
getDiffTreeFromBranch := func(repoPath, baseBranch, headBranch string) (string, error) {
var outbuf, errbuf strings.Builder
if err := git.NewCommand(ctx, "diff-tree", "--no-commit-id",
"--name-only", "-r", "-z", "--root", baseBranch, headBranch, "--").
Run(&git.RunOpts{
Dir: repoPath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
giteaErr := &giteaRunStdError{err: err, stderr: errbuf.String()}
return "", processGiteaErrorf(giteaErr, "git diff-tree [%s base:%s head:%s]: %s",
repoPath, baseBranch, headBranch, errbuf.String())
}
return outbuf.String(), nil
}
scanNullTerminatedStrings := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\x00'); i >= 0 {
return i + 1, data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
list, err := getDiffTreeFromBranch(repoPath, baseBranch, headBranch)
if err != nil {
return "", err
}
// Prefixing '/' for each entry, otherwise all files with the same name in subdirectories would be matched.
out := bytes.Buffer{}
scanner := bufio.NewScanner(strings.NewReader(list))
scanner.Split(scanNullTerminatedStrings)
for scanner.Scan() {
filepath := scanner.Text()
// escape '*', '?', '[', spaces and '!' prefix
filepath = escapedSymbols.ReplaceAllString(filepath, `\$1`)
// no necessary to escape the first '#' symbol because the first symbol is '/'
fmt.Fprintf(&out, "/%s\n", filepath)
}
return out.String(), nil
}
// GetMergeBase checks and returns merge base of two branches and the reference used as base.
func (a Adapter) GetMergeBase(
ctx context.Context,
repoPath string,
remote string,
base string,
head string,
) (string, string, error) {
if repoPath == "" {
return "", "", ErrRepositoryPathEmpty
}
if remote == "" {
remote = "origin"
}
if remote != "origin" {
tmpBaseName := git.RemotePrefix + remote + "/tmp_" + base
// Fetch commit into a temporary branch in order to be able to handle commits and tags
_, _, err := git.NewCommand(ctx, "fetch", "--no-tags", remote, "--",
base+":"+tmpBaseName).RunStdString(&git.RunOpts{Dir: repoPath})
if err == nil {
base = tmpBaseName
}
}
stdout, stderr, err := git.NewCommand(ctx, "merge-base", "--", base, head).RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil {
return "", "", processGiteaErrorf(err, "failed to get merge-base: %v", stderr)
}
return strings.TrimSpace(stdout), base, nil
}
// IsAncestor returns if the provided commit SHA is ancestor of the other commit SHA.
func (a Adapter) IsAncestor(
ctx context.Context,
repoPath string,
ancestorCommitSHA, descendantCommitSHA string,
) (bool, error) {
if repoPath == "" {
return false, ErrRepositoryPathEmpty
}
_, stderr, runErr := git.NewCommand(ctx, "merge-base", "--is-ancestor", ancestorCommitSHA, descendantCommitSHA).
RunStdString(&git.RunOpts{Dir: repoPath})
if runErr != nil {
if runErr.IsExitCode(1) && stderr == "" {
return false, nil
}
return false, processGiteaErrorf(runErr, "failed to check commit ancestry: %v", stderr)
}
return true, nil
}
// giteaRunStdError is an implementation of the RunStdError interface in the gitea codebase.
// It allows us to process gitea errors even when using cmd.Run() instead of cmd.RunStdString() or run.StdBytes().
type giteaRunStdError struct {
err error
stderr string
}
func (e *giteaRunStdError) Error() string {
return fmt.Sprintf("failed with %s, error output: %s", e.err, e.stderr)
}
func (e *giteaRunStdError) Unwrap() error {
return e.err
}
func (e *giteaRunStdError) Stderr() string {
return e.stderr
}
func (e *giteaRunStdError) IsExitCode(code int) bool {
var exitError *exec.ExitError
if errors.As(e.err, &exitError) {
return exitError.ExitCode() == code
}
return false
}

View File

@ -1,123 +0,0 @@
// 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 adapter_test
import (
"context"
"testing"
"github.com/harness/gitness/git/adapter"
)
func TestAdapter_GetMergeBase(t *testing.T) {
git := setupGit(t)
repo, teardown := setupRepo(t, git, "testmergebase")
defer teardown()
baseBranch := "main"
// write file to main branch
_, parentSHA := writeFile(t, repo, "file1.txt", "some content", nil)
err := repo.SetReference("refs/heads/"+baseBranch, parentSHA.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", baseBranch, err)
}
baseTag := "0.0.1"
err = repo.CreateAnnotatedTag(baseTag, "test tag 1", parentSHA.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", baseTag, err)
}
headBranch := "dev"
// create branch
err = repo.CreateBranch(headBranch, baseBranch)
if err != nil {
t.Fatalf("failed creating a branch '%s': %v", headBranch, err)
}
// write file to main branch
_, sha := writeFile(t, repo, "file1.txt", "new content", []string{parentSHA.String()})
err = repo.SetReference("refs/heads/"+headBranch, sha.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", headBranch, err)
}
headTag := "0.0.2"
err = repo.CreateAnnotatedTag(headTag, "test tag 2", sha.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", headTag, err)
}
type args struct {
ctx context.Context
repoPath string
remote string
base string
head string
}
tests := []struct {
name string
git adapter.Adapter
args args
want string
want1 string
wantErr bool
}{
{
name: "git merge base using branch names",
git: git,
args: args{
ctx: context.Background(),
repoPath: repo.Path,
remote: "",
base: baseBranch,
head: headBranch,
},
want: parentSHA.String(),
want1: baseBranch,
},
{
name: "git merge base using annotated tags",
git: git,
args: args{
ctx: context.Background(),
repoPath: repo.Path,
remote: "",
base: baseTag,
head: headTag,
},
want: parentSHA.String(),
want1: baseTag,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := tt.git.GetMergeBase(tt.args.ctx, tt.args.repoPath, tt.args.remote, tt.args.base, tt.args.head)
if (err != nil) != tt.wantErr {
t.Errorf("GetMergeBase() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetMergeBase() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("GetMergeBase() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

View File

@ -1,36 +0,0 @@
// 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 adapter
// ObjectType git object type.
type ObjectType string
const (
// ObjectCommit commit object type.
ObjectCommit ObjectType = "commit"
// ObjectTree tree object type.
ObjectTree ObjectType = "tree"
// ObjectBlob blob object type.
ObjectBlob ObjectType = "blob"
// ObjectTag tag object type.
ObjectTag ObjectType = "tag"
// ObjectBranch branch object type.
ObjectBranch ObjectType = "branch"
)
// Bytes returns the byte array for the Object Type.
func (o ObjectType) Bytes() []byte {
return []byte(o)
}

View File

@ -1,136 +0,0 @@
// 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 adapter_test
import (
"context"
"os"
"path"
"strings"
"testing"
"time"
"github.com/harness/gitness/git/adapter"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
)
type teardown func()
var (
testAuthor = &gitea.Signature{
Name: "test",
Email: "test@test.com",
}
testCommitter = &gitea.Signature{
Name: "test",
Email: "test@test.com",
}
)
type mockClientFactory struct{}
func (f *mockClientFactory) NewClient(_ context.Context, _ map[string]string) (hook.Client, error) {
return hook.NewNoopClient([]string{"mocked client"}), nil
}
func setupGit(t *testing.T) adapter.Adapter {
t.Helper()
git, err := adapter.New(
types.Config{Trace: true},
adapter.NewInMemoryLastCommitCache(5*time.Minute),
&mockClientFactory{},
)
if err != nil {
t.Fatalf("error initializing repository: %v", err)
}
return git
}
func setupRepo(t *testing.T, git adapter.Adapter, name string) (*gitea.Repository, teardown) {
t.Helper()
ctx := context.Background()
tmpdir := os.TempDir()
repoPath := path.Join(tmpdir, "test_repos", name)
err := git.InitRepository(ctx, repoPath, true)
if err != nil {
t.Fatalf("error initializing repository: %v", err)
}
repo, err := git.OpenRepository(ctx, repoPath)
if err != nil {
t.Fatalf("error opening repository '%s': %v", name, err)
}
err = repo.SetDefaultBranch("main")
if err != nil {
t.Fatalf("error setting default branch 'main': %v", err)
}
err = git.Config(ctx, repoPath, "user.email", testCommitter.Email)
if err != nil {
t.Fatalf("error setting config user.email %s: %v", testCommitter.Email, err)
}
err = git.Config(ctx, repoPath, "user.name", testCommitter.Name)
if err != nil {
t.Fatalf("error setting config user.name %s: %v", testCommitter.Name, err)
}
return repo, func() {
if err := os.RemoveAll(repoPath); err != nil {
t.Errorf("error while removeng the repository '%s'", repoPath)
}
}
}
func writeFile(
t *testing.T,
repo *gitea.Repository,
path string,
content string,
parents []string,
) (oid gitea.SHA1, commitSha gitea.SHA1) {
t.Helper()
oid, err := repo.HashObject(strings.NewReader(content))
if err != nil {
t.Fatalf("failed to hash object: %v", err)
}
err = repo.AddObjectToIndex("100644", oid, path)
if err != nil {
t.Fatalf("failed to add object to index: %v", err)
}
tree, err := repo.WriteTree()
if err != nil {
t.Fatalf("failed to write tree: %v", err)
}
commitSha, err = repo.CommitTree(testAuthor, testCommitter, tree, gitea.CommitTreeOpts{
Message: "write file operation",
Parents: parents,
})
if err != nil {
t.Fatalf("failed to commit tree: %v", err)
}
return oid, commitSha
}

View File

@ -1,59 +0,0 @@
// 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 adapter
import (
"context"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
)
// GetSubmodule returns the submodule at the given path reachable from ref.
// Note: ref can be Branch / Tag / CommitSHA.
func (a Adapter) GetSubmodule(
ctx context.Context,
repoPath string,
ref string,
treePath string,
) (*types.Submodule, error) {
if repoPath == "" {
return nil, ErrRepositoryPathEmpty
}
treePath = cleanTreePath(treePath)
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, processGiteaErrorf(err, "failed to open repository")
}
defer giteaRepo.Close()
// Get the giteaCommit object for the ref
giteaCommit, err := giteaRepo.GetCommit(ref)
if err != nil {
return nil, processGiteaErrorf(err, "error getting commit for ref '%s'", ref)
}
giteaSubmodule, err := giteaCommit.GetSubModule(treePath)
if err != nil {
return nil, processGiteaErrorf(err, "error getting submodule '%s' from commit", treePath)
}
return &types.Submodule{
Name: giteaSubmodule.Name,
URL: giteaSubmodule.URL,
}, nil
}

View File

@ -12,39 +12,26 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"context"
"github.com/harness/gitness/cache" "github.com/harness/gitness/cache"
"github.com/harness/gitness/git/hook" "github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
) )
type Adapter struct { type Git struct {
traceGit bool traceGit bool
lastCommitCache cache.Cache[CommitEntryKey, *types.Commit] lastCommitCache cache.Cache[CommitEntryKey, *Commit]
githookFactory hook.ClientFactory githookFactory hook.ClientFactory
} }
func New( func New(
config types.Config, config types.Config,
lastCommitCache cache.Cache[CommitEntryKey, *types.Commit], lastCommitCache cache.Cache[CommitEntryKey, *Commit],
githookFactory hook.ClientFactory, githookFactory hook.ClientFactory,
) (Adapter, error) { ) (*Git, error) {
// TODO: should be subdir of gitRoot? What is it being used for? return &Git{
setting.Git.HomePath = "home"
err := gitea.InitSimple(context.Background())
if err != nil {
return Adapter{}, err
}
return Adapter{
traceGit: config.Trace, traceGit: config.Trace,
lastCommitCache: lastCommitCache, lastCommitCache: lastCommitCache,
githookFactory: githookFactory, githookFactory: githookFactory,

View File

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -70,19 +70,19 @@ func testParseSignatureFromCatFileLineFor(t *testing.T, name string, email strin
func TestParseTagDataFromCatFile(t *testing.T) { func TestParseTagDataFromCatFile(t *testing.T) {
when, _ := time.Parse(defaultGitTimeLayout, "Fri Sep 23 10:57:49 2022 -0700") when, _ := time.Parse(defaultGitTimeLayout, "Fri Sep 23 10:57:49 2022 -0700")
testParseTagDataFromCatFileFor(t, "sha012", types.GitObjectTypeTag, "name1", testParseTagDataFromCatFileFor(t, sha.EmptyTree, GitObjectTypeTag, "name1",
types.Signature{Identity: types.Identity{Name: "max", Email: "max@mail.com"}, When: when}, Signature{Identity: Identity{Name: "max", Email: "max@mail.com"}, When: when},
"some message", "some message") "some message", "some message")
// test with signature // test with signature
testParseTagDataFromCatFileFor(t, "sha012", types.GitObjectTypeCommit, "name2", testParseTagDataFromCatFileFor(t, sha.EmptyTree, GitObjectTypeCommit, "name2",
types.Signature{Identity: types.Identity{Name: "max", Email: "max@mail.com"}, When: when}, Signature{Identity: Identity{Name: "max", Email: "max@mail.com"}, When: when},
"gpgsig -----BEGIN PGP SIGNATURE-----\n\nw...B\n-----END PGP SIGNATURE-----\n\nsome message", "gpgsig -----BEGIN PGP SIGNATURE-----\n\nw...B\n-----END PGP SIGNATURE-----\n\nsome message",
"some message") "some message")
} }
func testParseTagDataFromCatFileFor(t *testing.T, object string, typ types.GitObjectType, name string, func testParseTagDataFromCatFileFor(t *testing.T, object string, typ GitObjectType, name string,
tagger types.Signature, remainder string, expectedMessage string) { tagger Signature, remainder string, expectedMessage string) {
data := fmt.Sprintf( data := fmt.Sprintf(
"object %s\ntype %s\ntag %s\ntagger %s <%s> %s\n%s", "object %s\ntype %s\ntag %s\ntagger %s <%s> %s\n%s",
object, string(typ), name, object, string(typ), name,
@ -92,7 +92,7 @@ func testParseTagDataFromCatFileFor(t *testing.T, object string, typ types.GitOb
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, name, res.Name, data) require.Equal(t, name, res.Name, data)
require.Equal(t, object, res.TargetSha, data) require.Equal(t, object, res.TargetSha.String(), data)
require.Equal(t, typ, res.TargetType, data) require.Equal(t, typ, res.TargetType, data)
require.Equal(t, expectedMessage, res.Message, data) require.Equal(t, expectedMessage, res.Message, data)
require.Equal(t, tagger.Identity.Name, res.Tagger.Identity.Name, data) require.Equal(t, tagger.Identity.Name, res.Tagger.Identity.Name, data)

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bufio" "bufio"
@ -26,7 +26,7 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
) )
var ( var (
@ -36,14 +36,23 @@ var (
blamePorcelainOutOfRangeErrorRE = regexp.MustCompile(`has only \d+ lines$`) blamePorcelainOutOfRangeErrorRE = regexp.MustCompile(`has only \d+ lines$`)
) )
func (a Adapter) Blame( type BlamePart struct {
Commit *Commit `json:"commit"`
Lines []string `json:"lines"`
}
type BlameNextReader interface {
NextPart() (*BlamePart, error)
}
func (g *Git) Blame(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
rev string, rev string,
file string, file string,
lineFrom int, lineFrom int,
lineTo int, lineTo int,
) types.BlameReader { ) BlameNextReader {
// prepare the git command line arguments // prepare the git command line arguments
cmd := command.New( cmd := command.New(
"blame", "blame",
@ -84,7 +93,7 @@ func (a Adapter) Blame(
return &BlameReader{ return &BlameReader{
scanner: bufio.NewScanner(pipeRead), scanner: bufio.NewScanner(pipeRead),
commitCache: make(map[string]*types.Commit), commitCache: make(map[string]*Commit),
errReader: stderr, // Any stderr output will cause the BlameReader to fail. errReader: stderr, // Any stderr output will cause the BlameReader to fail.
} }
} }
@ -92,7 +101,7 @@ func (a Adapter) Blame(
type BlameReader struct { type BlameReader struct {
scanner *bufio.Scanner scanner *bufio.Scanner
lastLine string lastLine string
commitCache map[string]*types.Commit commitCache map[string]*Commit
errReader io.Reader errReader io.Reader
} }
@ -121,8 +130,8 @@ func (r *BlameReader) unreadLine(line string) {
} }
//nolint:complexity,gocognit,nestif // it's ok //nolint:complexity,gocognit,nestif // it's ok
func (r *BlameReader) NextPart() (*types.BlamePart, error) { func (r *BlameReader) NextPart() (*BlamePart, error) {
var commit *types.Commit var commit *Commit
var lines []string var lines []string
var err error var err error
@ -134,12 +143,12 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
} }
if matches := blamePorcelainHeadRE.FindStringSubmatch(line); matches != nil { if matches := blamePorcelainHeadRE.FindStringSubmatch(line); matches != nil {
sha := matches[1] commitSHA := sha.Must(matches[1])
if commit == nil { if commit == nil {
commit = r.commitCache[sha] commit = r.commitCache[commitSHA.String()]
if commit == nil { if commit == nil {
commit = &types.Commit{SHA: sha} commit = &Commit{SHA: commitSHA}
} }
if matches[5] != "" { if matches[5] != "" {
@ -153,11 +162,11 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
continue continue
} }
if sha != commit.SHA { if !commit.SHA.Equal(commitSHA) {
r.unreadLine(line) r.unreadLine(line)
r.commitCache[commit.SHA] = commit r.commitCache[commit.SHA.String()] = commit
return &types.BlamePart{ return &BlamePart{
Commit: commit, Commit: commit,
Lines: lines, Lines: lines,
}, nil }, nil
@ -210,10 +219,10 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
return nil, errors.Internal(err, "failed to start git blame command") return nil, errors.Internal(err, "failed to start git blame command")
} }
var part *types.BlamePart var part *BlamePart
if commit != nil && len(lines) > 0 { if commit != nil && len(lines) > 0 {
part = &types.BlamePart{ part = &BlamePart{
Commit: commit, Commit: commit,
Lines: lines, Lines: lines,
} }
@ -222,7 +231,7 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
return part, err return part, err
} }
func parseBlameHeaders(line string, commit *types.Commit) { func parseBlameHeaders(line string, commit *Commit) {
// This is the list of git blame headers that we process. Other headers we ignore. // This is the list of git blame headers that we process. Other headers we ignore.
const ( const (
headerSummary = "summary " headerSummary = "summary "

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bufio" "bufio"
@ -23,7 +23,7 @@ import (
"time" "time"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
@ -64,44 +64,44 @@ filename file_name.go
Line 14 Line 14
` `
author := types.Identity{ author := Identity{
Name: "Marko", Name: "Marko",
Email: "marko.gacesa@harness.io", Email: "marko.gacesa@harness.io",
} }
committer := types.Identity{ committer := Identity{
Name: "Committer", Name: "Committer",
Email: "noreply@harness.io", Email: "noreply@harness.io",
} }
commit1 := &types.Commit{ commit1 := &Commit{
SHA: "16f267ad4f731af1b2e36f42e170ed8921377398", SHA: sha.Must("16f267ad4f731af1b2e36f42e170ed8921377398"),
Title: "Pull request 1", Title: "Pull request 1",
Message: "", Message: "",
Author: types.Signature{ Author: Signature{
Identity: author, Identity: author,
When: time.Unix(1669812989, 0), When: time.Unix(1669812989, 0),
}, },
Committer: types.Signature{ Committer: Signature{
Identity: committer, Identity: committer,
When: time.Unix(1669812989, 0), When: time.Unix(1669812989, 0),
}, },
} }
commit2 := &types.Commit{ commit2 := &Commit{
SHA: "dcb4b6b63e86f06ed4e4c52fbc825545dc0b6200", SHA: sha.Must("dcb4b6b63e86f06ed4e4c52fbc825545dc0b6200"),
Title: "Pull request 2", Title: "Pull request 2",
Message: "", Message: "",
Author: types.Signature{ Author: Signature{
Identity: author, Identity: author,
When: time.Unix(1673952128, 0), When: time.Unix(1673952128, 0),
}, },
Committer: types.Signature{ Committer: Signature{
Identity: committer, Identity: committer,
When: time.Unix(1673952128, 0), When: time.Unix(1673952128, 0),
}, },
} }
want := []*types.BlamePart{ want := []*BlamePart{
{ {
Commit: commit1, Commit: commit1,
Lines: []string{"Line 10", "Line 11"}, Lines: []string{"Line 10", "Line 11"},
@ -118,11 +118,11 @@ filename file_name.go
reader := BlameReader{ reader := BlameReader{
scanner: bufio.NewScanner(strings.NewReader(blameOut)), scanner: bufio.NewScanner(strings.NewReader(blameOut)),
commitCache: make(map[string]*types.Commit), commitCache: make(map[string]*Commit),
errReader: strings.NewReader(""), errReader: strings.NewReader(""),
} }
var got []*types.BlamePart var got []*BlamePart
for { for {
part, err := reader.NextPart() part, err := reader.NextPart()
@ -145,7 +145,7 @@ filename file_name.go
func TestBlameReader_NextPart_UserError(t *testing.T) { func TestBlameReader_NextPart_UserError(t *testing.T) {
reader := BlameReader{ reader := BlameReader{
scanner: bufio.NewScanner(strings.NewReader("")), scanner: bufio.NewScanner(strings.NewReader("")),
commitCache: make(map[string]*types.Commit), commitCache: make(map[string]*Commit),
errReader: strings.NewReader("fatal: no such path\n"), errReader: strings.NewReader("fatal: no such path\n"),
} }
@ -158,7 +158,7 @@ func TestBlameReader_NextPart_UserError(t *testing.T) {
func TestBlameReader_NextPart_CmdError(t *testing.T) { func TestBlameReader_NextPart_CmdError(t *testing.T) {
reader := BlameReader{ reader := BlameReader{
scanner: bufio.NewScanner(iotest.ErrReader(errors.New("dummy error"))), scanner: bufio.NewScanner(iotest.ErrReader(errors.New("dummy error"))),
commitCache: make(map[string]*types.Commit), commitCache: make(map[string]*Commit),
errReader: strings.NewReader(""), errReader: strings.NewReader(""),
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"context" "context"
@ -20,48 +20,58 @@ import (
"io" "io"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
) )
type BlobReader struct {
SHA sha.SHA
// Size is the actual size of the blob.
Size int64
// ContentSize is the total number of bytes returned by the Content Reader.
ContentSize int64
// Content contains the (partial) content of the blob.
Content io.ReadCloser
}
// GetBlob returns the blob for the given object sha. // GetBlob returns the blob for the given object sha.
func (a Adapter) GetBlob( func (g *Git) GetBlob(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
sha string, sha sha.SHA,
sizeLimit int64, sizeLimit int64,
) (*types.BlobReader, error) { ) (*BlobReader, error) {
stdIn, stdOut, cancel := CatFileBatch(ctx, repoPath) stdIn, stdOut, cancel := CatFileBatch(ctx, repoPath)
line := sha.String() + "\n"
_, err := stdIn.Write([]byte(sha + "\n")) _, err := stdIn.Write([]byte(line))
if err != nil { if err != nil {
cancel() cancel()
return nil, fmt.Errorf("failed to write blob sha to git stdin: %w", err) return nil, fmt.Errorf("failed to write blob sha to git stdin: %w", err)
} }
objectSHA, objectType, objectSize, err := ReadBatchHeaderLine(stdOut) output, err := ReadBatchHeaderLine(stdOut)
if err != nil { if err != nil {
cancel() cancel()
return nil, processGiteaErrorf(err, "failed to read cat-file batch line") return nil, processGitErrorf(err, "failed to read cat-file batch line")
} }
if string(objectSHA) != sha { if !output.SHA.Equal(sha) {
cancel() cancel()
return nil, fmt.Errorf("cat-file returned object sha '%s' but expected '%s'", objectSHA, sha) return nil, fmt.Errorf("cat-file returned object sha '%s' but expected '%s'", output.SHA, sha)
} }
if objectType != string(ObjectBlob) { if output.Type != string(GitObjectTypeBlob) {
cancel() cancel()
return nil, errors.InvalidArgument( return nil, errors.InvalidArgument(
"cat-file returned object type '%s' but expected '%s'", objectType, ObjectBlob) "cat-file returned object type '%s' but expected '%s'", output.Type, GitObjectTypeBlob)
} }
contentSize := objectSize contentSize := output.Size
if sizeLimit > 0 && sizeLimit < contentSize { if sizeLimit > 0 && sizeLimit < contentSize {
contentSize = sizeLimit contentSize = sizeLimit
} }
return &types.BlobReader{ return &BlobReader{
SHA: sha, SHA: sha,
Size: objectSize, Size: output.Size,
ContentSize: contentSize, ContentSize: contentSize,
Content: newLimitReaderCloser(stdOut, contentSize, cancel), Content: newLimitReaderCloser(stdOut, contentSize, cancel),
}, nil }, nil

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes" "bytes"
@ -21,15 +21,33 @@ import (
"strings" "strings"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
) )
type Branch struct {
Name string
SHA sha.SHA
Commit *Commit
}
type BranchFilter struct {
Query string
Page int32
PageSize int32
Sort GitReferenceField
Order SortOrder
IncludeCommit bool
}
// BranchPrefix base dir of the branch information file store on git.
const BranchPrefix = "refs/heads/"
// GetBranch gets an existing branch. // GetBranch gets an existing branch.
func (a Adapter) GetBranch( func (g *Git) GetBranch(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
branchName string, branchName string,
) (*types.Branch, error) { ) (*Branch, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
@ -38,12 +56,12 @@ func (a Adapter) GetBranch(
} }
ref := GetReferenceFromBranchName(branchName) ref := GetReferenceFromBranchName(branchName)
commit, err := GetCommit(ctx, repoPath, ref, "") commit, err := GetCommit(ctx, repoPath, ref+"^{commit}")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to find the commit for the branch: %w", err) return nil, fmt.Errorf("failed to find the commit for the branch: %w", err)
} }
return &types.Branch{ return &Branch{
Name: branchName, Name: branchName,
SHA: commit.SHA, SHA: commit.SHA,
Commit: commit, Commit: commit,
@ -53,7 +71,7 @@ func (a Adapter) GetBranch(
// HasBranches returns true iff there's at least one branch in the repo (any branch) // HasBranches returns true iff there's at least one branch in the repo (any branch)
// NOTE: This is different from repo.Empty(), // NOTE: This is different from repo.Empty(),
// as it doesn't care whether the existing branch is the default branch or not. // as it doesn't care whether the existing branch is the default branch or not.
func (a Adapter) HasBranches( func (g *Git) HasBranches(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
) (bool, error) { ) (bool, error) {
@ -68,8 +86,21 @@ func (a Adapter) HasBranches(
) )
output := &bytes.Buffer{} output := &bytes.Buffer{}
if err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output)); err != nil { if err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output)); err != nil {
return false, processGiteaErrorf(err, "failed to trigger rev-list command") return false, processGitErrorf(err, "failed to trigger rev-list command")
} }
return strings.TrimSpace(output.String()) == "", nil return strings.TrimSpace(output.String()) == "", nil
} }
func (g *Git) IsBranchExist(ctx context.Context, repoPath, name string) (bool, error) {
cmd := command.New("show-ref",
command.WithFlag("--verify", BranchPrefix+name),
)
err := cmd.Run(ctx,
command.WithDir(repoPath),
)
if err != nil {
return false, fmt.Errorf("failed to check if branch '%s' exist: %w", name, err)
}
return true, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bufio" "bufio"
@ -24,10 +24,10 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/sha"
"github.com/djherbis/buffer" "github.com/djherbis/buffer"
"github.com/djherbis/nio/v3" "github.com/djherbis/nio/v3"
"github.com/rs/zerolog/log"
) )
// WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function. // WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function.
@ -41,6 +41,7 @@ type WriteCloserError interface {
func CatFileBatch( func CatFileBatch(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
flags ...command.CmdOptionFunc,
) (WriteCloserError, *bufio.Reader, func()) { ) (WriteCloserError, *bufio.Reader, func()) {
const bufferSize = 32 * 1024 const bufferSize = 32 * 1024
// We often want to feed the commits in order into cat-file --batch, // We often want to feed the commits in order into cat-file --batch,
@ -64,7 +65,10 @@ func CatFileBatch(
go func() { go func() {
stderr := bytes.Buffer{} stderr := bytes.Buffer{}
cmd := command.New("cat-file", command.WithFlag("--batch")) cmd := command.New("cat-file",
command.WithFlag("--batch"),
)
cmd.Add(flags...)
err := cmd.Run(ctx, err := cmd.Run(ctx,
command.WithDir(repoPath), command.WithDir(repoPath),
command.WithStdin(batchStdinReader), command.WithStdin(batchStdinReader),
@ -87,42 +91,48 @@ func CatFileBatch(
return batchStdinWriter, batchReader, cancel return batchStdinWriter, batchReader, cancel
} }
type BatchHeaderResponse struct {
SHA sha.SHA
Type string
Size int64
}
// ReadBatchHeaderLine reads the header line from cat-file --batch // ReadBatchHeaderLine reads the header line from cat-file --batch
// We expect:
// <sha> SP <type> SP <size> LF // <sha> SP <type> SP <size> LF
// sha is a 40byte not 20byte here. // sha is a 40byte not 20byte here.
func ReadBatchHeaderLine(rd *bufio.Reader) (sha []byte, objType string, size int64, err error) { func ReadBatchHeaderLine(rd *bufio.Reader) (*BatchHeaderResponse, error) {
objType, err = rd.ReadString('\n') line, err := rd.ReadString('\n')
if err != nil { if err != nil {
return nil, "", 0, err return nil, err
} }
if len(objType) == 1 { if len(line) == 1 {
objType, err = rd.ReadString('\n') line, err = rd.ReadString('\n')
if err != nil { if err != nil {
return nil, "", 0, err return nil, err
} }
} }
idx := strings.IndexByte(objType, ' ') idx := strings.IndexByte(line, ' ')
if idx < 0 { if idx < 0 {
log.Debug().Msgf("missing space type: %s", objType) return nil, errors.NotFound("missing space char for: %s", line)
err = errors.NotFound("sha '%s' not found", sha)
return nil, "", 0, err
} }
sha = []byte(objType[:idx]) id := line[:idx]
objType = objType[idx+1:] objType := line[idx+1:]
idx = strings.IndexByte(objType, ' ') idx = strings.IndexByte(objType, ' ')
if idx < 0 { if idx < 0 {
err = errors.NotFound("sha '%s' not found", sha) return nil, errors.NotFound("sha '%s' not found", id)
return nil, "", 0, err
} }
sizeStr := objType[idx+1 : len(objType)-1] sizeStr := objType[idx+1 : len(objType)-1]
objType = objType[:idx] objType = objType[:idx]
size, err = strconv.ParseInt(sizeStr, 10, 64) size, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil { if err != nil {
return nil, "", 0, err return nil, err
} }
return sha, objType, size, nil return &BatchHeaderResponse{
SHA: sha.Must(id),
Type: objType,
Size: size,
}, nil
} }

View File

@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -26,54 +28,115 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/enum" "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
gitea "code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// CommitGPGSignature represents a git commit signature part.
type CommitGPGSignature struct {
Signature string
Payload string
}
type CommitChangesOptions struct {
Committer Signature
Author Signature
Message string
}
type CommitFileStats struct {
ChangeType enum.FileDiffStatus
Path string
OldPath string // populated only in case of renames
Insertions int64
Deletions int64
}
type Commit struct {
SHA sha.SHA `json:"sha"`
Title string `json:"title"`
Message string `json:"message,omitempty"`
Author Signature `json:"author"`
Committer Signature `json:"committer"`
Signature *CommitGPGSignature
ParentSHAs []sha.SHA
FileStats []CommitFileStats `json:"file_stats,omitempty"`
}
type CommitFilter struct {
Path string
AfterRef string
Since int64
Until int64
Committer string
}
// CommitDivergenceRequest contains the refs for which the converging commits should be counted.
type CommitDivergenceRequest struct {
// From is the ref from which the counting of the diverging commits starts.
From string
// To is the ref at which the counting of the diverging commits ends.
To string
}
// CommitDivergence contains the information of the count of converging commits between two refs.
type CommitDivergence struct {
// Ahead is the count of commits the 'From' ref is ahead of the 'To' ref.
Ahead int32
// Behind is the count of commits the 'From' ref is behind the 'To' ref.
Behind int32
}
type PathRenameDetails struct {
OldPath string
Path string
CommitSHABefore sha.SHA
CommitSHAAfter sha.SHA
}
// GetLatestCommit gets the latest commit of a path relative from the provided revision. // GetLatestCommit gets the latest commit of a path relative from the provided revision.
func (a Adapter) GetLatestCommit( func (g *Git) GetLatestCommit(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
rev string, rev string,
treePath string, treePath string,
) (*types.Commit, error) { ) (*Commit, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
treePath = cleanTreePath(treePath) treePath = cleanTreePath(treePath)
return GetCommit(ctx, repoPath, rev, treePath) return getCommit(ctx, repoPath, rev, treePath)
} }
func getGiteaCommits( func getCommits(
giteaRepo *gitea.Repository, ctx context.Context,
repoPath string,
commitIDs []string, commitIDs []string,
) ([]*gitea.Commit, error) { ) ([]*Commit, error) {
var giteaCommits []*gitea.Commit
if len(commitIDs) == 0 { if len(commitIDs) == 0 {
return giteaCommits, nil return nil, nil
} }
commits := make([]*Commit, 0, len(commitIDs))
for _, commitID := range commitIDs { for _, commitID := range commitIDs {
commit, err := giteaRepo.GetCommit(commitID) commit, err := getCommit(ctx, repoPath, commitID, "")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get commit '%s': %w", commitID, err) return nil, fmt.Errorf("failed to get commit '%s': %w", commitID, err)
} }
giteaCommits = append(giteaCommits, commit) commits = append(commits, commit)
} }
return giteaCommits, nil return commits, nil
} }
func (a Adapter) listCommitSHAs( func (g *Git) listCommitSHAs(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
ref string, ref string,
page int, page int,
limit int, limit int,
filter types.CommitFilter, filter CommitFilter,
) ([]string, error) { ) ([]string, error) {
cmd := command.New("rev-list") cmd := command.New("rev-list")
@ -115,7 +178,7 @@ func (a Adapter) listCommitSHAs(
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output)) err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
if err != nil { if err != nil {
// TODO: handle error in case they don't have a common merge base! // TODO: handle error in case they don't have a common merge base!
return nil, processGiteaErrorf(err, "failed to trigger rev-list command") return nil, processGitErrorf(err, "failed to trigger rev-list command")
} }
return parseLinesToSlice(output.Bytes()), nil return parseLinesToSlice(output.Bytes()), nil
@ -124,69 +187,55 @@ func (a Adapter) listCommitSHAs(
// ListCommitSHAs lists the commits reachable from ref. // ListCommitSHAs lists the commits reachable from ref.
// Note: ref & afterRef can be Branch / Tag / CommitSHA. // Note: ref & afterRef can be Branch / Tag / CommitSHA.
// Note: commits returned are [ref->...->afterRef). // Note: commits returned are [ref->...->afterRef).
func (a Adapter) ListCommitSHAs( func (g *Git) ListCommitSHAs(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
ref string, ref string,
page int, page int,
limit int, limit int,
filter types.CommitFilter, filter CommitFilter,
) ([]string, error) { ) ([]string, error) {
return a.listCommitSHAs(ctx, repoPath, ref, page, limit, filter) return g.listCommitSHAs(ctx, repoPath, ref, page, limit, filter)
} }
// ListCommits lists the commits reachable from ref. // ListCommits lists the commits reachable from ref.
// Note: ref & afterRef can be Branch / Tag / CommitSHA. // Note: ref & afterRef can be Branch / Tag / CommitSHA.
// Note: commits returned are [ref->...->afterRef). // Note: commits returned are [ref->...->afterRef).
func (a Adapter) ListCommits( func (g *Git) ListCommits(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
ref string, ref string,
page int, page int,
limit int, limit int,
includeStats bool, includeStats bool,
filter types.CommitFilter, filter CommitFilter,
) ([]types.Commit, []types.PathRenameDetails, error) { ) ([]*Commit, []PathRenameDetails, error) {
if repoPath == "" { if repoPath == "" {
return nil, nil, ErrRepositoryPathEmpty return nil, nil, ErrRepositoryPathEmpty
} }
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, nil, processGiteaErrorf(err, "failed to open repository")
}
defer giteaRepo.Close()
commitSHAs, err := a.listCommitSHAs(ctx, repoPath, ref, page, limit, filter) commitSHAs, err := g.listCommitSHAs(ctx, repoPath, ref, page, limit, filter)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
giteaCommits, err := getGiteaCommits(giteaRepo, commitSHAs) commits, err := getCommits(ctx, repoPath, commitSHAs)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
commits := make([]types.Commit, len(giteaCommits)) if includeStats {
for i := range giteaCommits { for _, commit := range commits {
var commit *types.Commit fileStats, err := getCommitFileStats(ctx, repoPath, commit.SHA)
commit, err = mapGiteaCommit(giteaCommits[i])
if err != nil {
return nil, nil, err
}
if includeStats {
fileStats, err := getCommitFileStats(ctx, giteaRepo, commit.SHA)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("encountered error getting commit file stats: %w", err) return nil, nil, fmt.Errorf("encountered error getting commit file stats: %w", err)
} }
commit.FileStats = fileStats commit.FileStats = fileStats
} }
commits[i] = *commit
} }
if len(filter.Path) != 0 { if len(filter.Path) != 0 {
renameDetailsList, err := getRenameDetails(ctx, giteaRepo, commits, filter.Path) renameDetailsList, err := getRenameDetails(ctx, repoPath, commits, filter.Path)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -199,49 +248,48 @@ func (a Adapter) ListCommits(
func getCommitFileStats( func getCommitFileStats(
ctx context.Context, ctx context.Context,
giteaRepo *gitea.Repository, repoPath string,
sha string, sha sha.SHA,
) ([]types.CommitFileStats, error) { ) ([]CommitFileStats, error) {
var changeInfoTypes map[string]changeInfoType var changeInfoTypes map[string]changeInfoType
changeInfoTypes, err := getChangeInfoTypes(ctx, giteaRepo, sha) changeInfoTypes, err := getChangeInfoTypes(ctx, repoPath, sha)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get change infos: %w", err) return nil, fmt.Errorf("failed to get change infos: %w", err)
} }
changeInfoChanges, err := getChangeInfoChanges(giteaRepo, sha) changeInfoChanges, err := getChangeInfoChanges(ctx, repoPath, sha)
if err != nil { if err != nil {
return []types.CommitFileStats{}, fmt.Errorf("failed to get change infos: %w", err) return []CommitFileStats{}, fmt.Errorf("failed to get change infos: %w", err)
} }
fileStats := make([]types.CommitFileStats, len(changeInfoChanges)) fileStats := make([]CommitFileStats, len(changeInfoChanges))
i := 0 i := 0
for path, info := range changeInfoChanges { for path, info := range changeInfoChanges {
fileStats[i] = types.CommitFileStats{ fileStats[i] = CommitFileStats{
Path: changeInfoTypes[path].Path, Path: changeInfoTypes[path].Path,
OldPath: changeInfoTypes[path].OldPath, OldPath: changeInfoTypes[path].OldPath,
Status: changeInfoTypes[path].Status, ChangeType: changeInfoTypes[path].Status,
Insertions: info.Insertions, Insertions: info.Insertions,
Deletions: info.Deletions, Deletions: info.Deletions,
} }
i++ i++
} }
return fileStats, nil return fileStats, nil
} }
// In case of rename of a file, same commit will be listed twice - Once in old file and second time in new file. // In case of rename of a file, same commit will be listed twice - Once in old file and second time in new file.
// Hence, we are making it a pattern to only list it as part of new file and not as part of old file. // Hence, we are making it a pattern to only list it as part of new file and not as part of old file.
func cleanupCommitsForRename( func cleanupCommitsForRename(
commits []types.Commit, commits []*Commit,
renameDetails []types.PathRenameDetails, renameDetails []PathRenameDetails,
path string, path string,
) []types.Commit { ) []*Commit {
if len(commits) == 0 { if len(commits) == 0 {
return commits return commits
} }
for _, renameDetail := range renameDetails { for _, renameDetail := range renameDetails {
// Since rename details is present it implies that we have commits and hence don't need null check. // Since rename details is present it implies that we have commits and hence don't need null check.
if commits[0].SHA == renameDetail.CommitSHABefore && path == renameDetail.OldPath { if commits[0].SHA.Equal(renameDetail.CommitSHABefore) && path == renameDetail.OldPath {
return commits[1:] return commits[1:]
} }
} }
@ -250,17 +298,17 @@ func cleanupCommitsForRename(
func getRenameDetails( func getRenameDetails(
ctx context.Context, ctx context.Context,
giteaRepo *gitea.Repository, repoPath string,
commits []types.Commit, commits []*Commit,
path string, path string,
) ([]types.PathRenameDetails, error) { ) ([]PathRenameDetails, error) {
if len(commits) == 0 { if len(commits) == 0 {
return []types.PathRenameDetails{}, nil return []PathRenameDetails{}, nil
} }
renameDetailsList := make([]types.PathRenameDetails, 0, 2) renameDetailsList := make([]PathRenameDetails, 0, 2)
renameDetails, err := giteaGetRenameDetails(ctx, giteaRepo, commits[0].SHA, path) renameDetails, err := gitGetRenameDetails(ctx, repoPath, commits[0].SHA, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -273,7 +321,7 @@ func getRenameDetails(
return renameDetailsList, nil return renameDetailsList, nil
} }
renameDetailsLast, err := giteaGetRenameDetails(ctx, giteaRepo, commits[len(commits)-1].SHA, path) renameDetailsLast, err := gitGetRenameDetails(ctx, repoPath, commits[len(commits)-1].SHA, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -285,52 +333,56 @@ func getRenameDetails(
return renameDetailsList, nil return renameDetailsList, nil
} }
func giteaGetRenameDetails( func gitGetRenameDetails(
ctx context.Context, ctx context.Context,
giteaRepo *gitea.Repository, repoPath string,
ref string, sha sha.SHA,
path string, path string,
) (*types.PathRenameDetails, error) { ) (*PathRenameDetails, error) {
changeInfos, err := getChangeInfoTypes(ctx, giteaRepo, ref) changeInfos, err := getChangeInfoTypes(ctx, repoPath, sha)
if err != nil { if err != nil {
return &types.PathRenameDetails{}, fmt.Errorf("failed to get change infos %w", err) return &PathRenameDetails{}, fmt.Errorf("failed to get change infos %w", err)
} }
for _, c := range changeInfos { for _, c := range changeInfos {
if c.Status == enum.FileDiffStatusRenamed && (c.OldPath == path || c.Path == path) { if c.Status == enum.FileDiffStatusRenamed && (c.OldPath == path || c.Path == path) {
return &types.PathRenameDetails{ return &PathRenameDetails{
OldPath: c.OldPath, OldPath: c.OldPath,
Path: c.Path, Path: c.Path,
}, nil }, nil
} }
} }
return &types.PathRenameDetails{}, nil return &PathRenameDetails{}, nil
} }
func gitLogNameStatus(giteaRepo *gitea.Repository, ref string) ([]string, error) { func gitLogNameStatus(ctx context.Context, repoPath string, sha sha.SHA) ([]string, error) {
cmd := command.New("log", cmd := command.New("log",
command.WithFlag("--name-status"), command.WithFlag("--name-status"),
command.WithFlag("--format="), command.WithFlag("--format="),
command.WithFlag("--max-count=1"), command.WithFlag("--max-count=1"),
command.WithArg(ref), command.WithArg(sha.String()),
) )
output := &bytes.Buffer{} output := &bytes.Buffer{}
err := cmd.Run(giteaRepo.Ctx, command.WithDir(giteaRepo.Path), command.WithStdout(output)) err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to trigger log command: %w", err) return nil, fmt.Errorf("failed to trigger log command: %w", err)
} }
return parseLinesToSlice(output.Bytes()), nil return parseLinesToSlice(output.Bytes()), nil
} }
func gitShowNumstat(giteaRepo *gitea.Repository, ref string) ([]string, error) { func gitShowNumstat(
ctx context.Context,
repoPath string,
sha sha.SHA,
) ([]string, error) {
cmd := command.New("show", cmd := command.New("show",
command.WithFlag("--numstat"), command.WithFlag("--numstat"),
command.WithFlag("--format="), command.WithFlag("--format="),
command.WithArg(ref), command.WithArg(sha.String()),
) )
output := &bytes.Buffer{} output := &bytes.Buffer{}
err := cmd.Run(giteaRepo.Ctx, command.WithDir(giteaRepo.Path), command.WithStdout(output)) err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to trigger show command: %w", err) return nil, fmt.Errorf("failed to trigger show command: %w", err)
} }
@ -343,10 +395,10 @@ var renameRegex = regexp.MustCompile(`\t(.+)\t(.+)`)
func getChangeInfoTypes( func getChangeInfoTypes(
ctx context.Context, ctx context.Context,
giteaRepo *gitea.Repository, repoPath string,
ref string, sha sha.SHA,
) (map[string]changeInfoType, error) { ) (map[string]changeInfoType, error) {
lines, err := gitLogNameStatus(giteaRepo, ref) lines, err := gitLogNameStatus(ctx, repoPath, sha)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -382,10 +434,11 @@ var insertionsDeletionsRegex = regexp.MustCompile(`(\d+|-)\t(\d+|-)\t(.+)`)
var renameRegexWithArrow = regexp.MustCompile(`\d+\t\d+\t.+\s=>\s(.+)`) var renameRegexWithArrow = regexp.MustCompile(`\d+\t\d+\t.+\s=>\s(.+)`)
func getChangeInfoChanges( func getChangeInfoChanges(
giteaRepo *gitea.Repository, ctx context.Context,
ref string, repoPath string,
sha sha.SHA,
) (map[string]changeInfoChange, error) { ) (map[string]changeInfoChange, error) {
lines, err := gitShowNumstat(giteaRepo, ref) lines, err := gitShowNumstat(ctx, repoPath, sha)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -457,25 +510,25 @@ func convertFileDiffStatus(ctx context.Context, c string) enum.FileDiffStatus {
} }
// GetCommit returns the (latest) commit for a specific revision. // GetCommit returns the (latest) commit for a specific revision.
func (a Adapter) GetCommit( func (g *Git) GetCommit(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
rev string, rev string,
) (*types.Commit, error) { ) (*Commit, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
return GetCommit(ctx, repoPath, rev, "") return getCommit(ctx, repoPath, rev, "")
} }
func (a Adapter) GetFullCommitID( func (g *Git) GetFullCommitID(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
shortID string, shortID string,
) (string, error) { ) (sha.SHA, error) {
if repoPath == "" { if repoPath == "" {
return "", ErrRepositoryPathEmpty return sha.None, ErrRepositoryPathEmpty
} }
cmd := command.New("rev-parse", cmd := command.New("rev-parse",
command.WithArg(shortID), command.WithArg(shortID),
@ -484,66 +537,45 @@ func (a Adapter) GetFullCommitID(
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output)) err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
if err != nil { if err != nil {
if strings.Contains(err.Error(), "exit status 128") { if strings.Contains(err.Error(), "exit status 128") {
return "", errors.NotFound("commit not found %s", shortID) return sha.None, errors.NotFound("commit not found %s", shortID)
} }
return "", err return sha.None, err
} }
return strings.TrimSpace(output.String()), nil return sha.New(output.String())
} }
// GetCommits returns the (latest) commits for a specific list of refs. // GetCommits returns the (latest) commits for a specific list of refs.
// Note: ref can be Branch / Tag / CommitSHA. // Note: ref can be Branch / Tag / CommitSHA.
func (a Adapter) GetCommits( func (g *Git) GetCommits(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
refs []string, refs []string,
) ([]types.Commit, error) { ) ([]*Commit, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, processGiteaErrorf(err, "failed to open repository")
}
defer giteaRepo.Close()
commits := make([]types.Commit, len(refs)) return getCommits(ctx, repoPath, 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. // 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 // 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)). // (max 10 could lead to (0, 10) while it's actually (2, 12)).
func (a Adapter) GetCommitDivergences( func (g *Git) GetCommitDivergences(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
requests []types.CommitDivergenceRequest, requests []CommitDivergenceRequest,
max int32, max int32,
) ([]types.CommitDivergence, error) { ) ([]CommitDivergence, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
var err error var err error
res := make([]types.CommitDivergence, len(requests)) res := make([]CommitDivergence, len(requests))
for i, req := range requests { for i, req := range requests {
res[i], err = a.getCommitDivergence(ctx, repoPath, req, max) res[i], err = g.getCommitDivergence(ctx, repoPath, req, max)
if types.IsNotFoundError(err) { if errors.IsNotFound(err) {
res[i] = types.CommitDivergence{Ahead: -1, Behind: -1} res[i] = CommitDivergence{Ahead: -1, Behind: -1}
continue continue
} }
if err != nil { if err != nil {
@ -555,42 +587,37 @@ func (a Adapter) GetCommitDivergences(
} }
// getCommitDivergence returns the count of diverging commits for a pair of branches. // 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 // IMPORTANT: If a max is provided it limits the overall count of diverging commits
// (max 10 could lead to (0, 10) while it's actually (2, 12)). // (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 func (g *Git) getCommitDivergence(
// (downside is the max behavior explained above).
func (a Adapter) getCommitDivergence(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
req types.CommitDivergenceRequest, req CommitDivergenceRequest,
max int32, max int32,
) (types.CommitDivergence, error) { ) (CommitDivergence, error) {
// prepare args cmd := command.New("rev-list",
args := []string{ command.WithFlag("--count"),
"rev-list", command.WithFlag("--left-right"),
"--count", )
"--left-right",
}
// limit count if requested. // limit count if requested.
if max > 0 { if max > 0 {
args = append(args, "--max-count") cmd.Add(command.WithFlag("--max-count", strconv.Itoa(int(max))))
args = append(args, fmt.Sprint(max))
} }
// add query to get commits without shared base commits // add query to get commits without shared base commits
args = append(args, fmt.Sprintf("%s...%s", req.From, req.To)) cmd.Add(command.WithArg(req.From + "..." + req.To))
var err error stdout := &bytes.Buffer{}
cmd := gitea.NewCommand(ctx, args...) err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(stdout))
stdOut, stdErr, err := cmd.RunStdString(&gitea.RunOpts{Dir: repoPath})
if err != nil { if err != nil {
return types.CommitDivergence{}, return CommitDivergence{},
processGiteaErrorf(err, "git rev-list failed for '%s...%s' (stdErr: '%s')", req.From, req.To, stdErr) processGitErrorf(err, "git rev-list failed for '%s...%s'", req.From, req.To)
} }
// parse output, e.g.: `1 2\n` // parse output, e.g.: `1 2\n`
rawLeft, rawRight, ok := strings.Cut(stdOut, "\t") output := stdout.String()
rawLeft, rawRight, ok := strings.Cut(output, "\t")
if !ok { if !ok {
return types.CommitDivergence{}, fmt.Errorf("git rev-list returned unexpected output '%s'", stdOut) return CommitDivergence{}, fmt.Errorf("git rev-list returned unexpected output '%s'", output)
} }
// trim any unnecessary characters // trim any unnecessary characters
@ -600,16 +627,18 @@ func (a Adapter) getCommitDivergence(
// parse numbers // parse numbers
left, err := strconv.ParseInt(rawLeft, 10, 32) left, err := strconv.ParseInt(rawLeft, 10, 32)
if err != nil { if err != nil {
return types.CommitDivergence{}, return CommitDivergence{},
fmt.Errorf("failed to parse git rev-list output for ahead '%s' (full: '%s')): %w", rawLeft, stdOut, err) fmt.Errorf("failed to parse git rev-list output for ahead '%s' (full: '%s')): %w",
rawLeft, output, err)
} }
right, err := strconv.ParseInt(rawRight, 10, 32) right, err := strconv.ParseInt(rawRight, 10, 32)
if err != nil { if err != nil {
return types.CommitDivergence{}, return CommitDivergence{},
fmt.Errorf("failed to parse git rev-list output for behind '%s' (full: '%s')): %w", rawRight, stdOut, err) fmt.Errorf("failed to parse git rev-list output for behind '%s' (full: '%s')): %w",
rawRight, output, err)
} }
return types.CommitDivergence{ return CommitDivergence{
Ahead: int32(left), Ahead: int32(left),
Behind: int32(right), Behind: int32(right),
}, nil }, nil
@ -630,14 +659,14 @@ func parseLinesToSlice(output []byte) []string {
return slice return slice
} }
// GetCommit returns info about a commit. // getCommit returns info about a commit.
// TODO: Move this function outside of the adapter package. // TODO: This function is used only for last used cache
func GetCommit( func getCommit(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
rev string, rev string,
path string, path string,
) (*types.Commit, error) { ) (*Commit, error) {
const format = "" + const format = "" +
fmtCommitHash + fmtZero + // 0 fmtCommitHash + fmtZero + // 0
fmtParentHashes + fmtZero + // 1 fmtParentHashes + fmtZero + // 1
@ -681,10 +710,12 @@ func GetCommit(
"unexpected git log formatted output, expected %d, but got %d columns", columnCount, len(commitData)) "unexpected git log formatted output, expected %d, but got %d columns", columnCount, len(commitData))
} }
sha := commitData[0] commitSHA := sha.Must(commitData[0])
var parentSHAs []string var parentSHAs []sha.SHA
if commitData[1] != "" { if commitData[1] != "" {
parentSHAs = strings.Split(commitData[1], " ") for _, parentSHA := range strings.Split(commitData[1], " ") {
parentSHAs = append(parentSHAs, sha.Must(parentSHA))
}
} }
authorName := commitData[2] authorName := commitData[2]
authorEmail := commitData[3] authorEmail := commitData[3]
@ -698,20 +729,20 @@ func GetCommit(
authorTime, _ := time.Parse(time.RFC3339Nano, authorTimestamp) authorTime, _ := time.Parse(time.RFC3339Nano, authorTimestamp)
committerTime, _ := time.Parse(time.RFC3339Nano, committerTimestamp) committerTime, _ := time.Parse(time.RFC3339Nano, committerTimestamp)
return &types.Commit{ return &Commit{
SHA: sha, SHA: commitSHA,
ParentSHAs: parentSHAs, ParentSHAs: parentSHAs,
Title: subject, Title: subject,
Message: body, Message: body,
Author: types.Signature{ Author: Signature{
Identity: types.Identity{ Identity: Identity{
Name: authorName, Name: authorName,
Email: authorEmail, Email: authorEmail,
}, },
When: authorTime, When: authorTime,
}, },
Committer: types.Signature{ Committer: Signature{
Identity: types.Identity{ Identity: Identity{
Name: committerName, Name: committerName,
Email: committerEmail, Email: committerEmail,
}, },
@ -719,3 +750,173 @@ func GetCommit(
}, },
}, nil }, nil
} }
// GetCommit returns info about a commit.
// TODO: Move this function outside of the api package.
func GetCommit(
ctx context.Context,
repoPath string,
rev string,
) (*Commit, error) {
wr, rd, cancel := CatFileBatch(ctx, repoPath)
defer cancel()
_, _ = wr.Write([]byte(rev + "\n"))
return getCommitFromBatchReader(ctx, repoPath, rd, rev)
}
func getCommitFromBatchReader(
ctx context.Context,
repoPath string,
rd *bufio.Reader,
rev string,
) (*Commit, error) {
output, err := ReadBatchHeaderLine(rd)
if err != nil {
return nil, fmt.Errorf("failed to read cat-file header line: %w", err)
}
switch output.Type {
case "missing":
return nil, errors.NotFound("sha '%s' not found", output.SHA)
case "tag":
// then we need to parse the tag
// and load the commit
data, err := io.ReadAll(io.LimitReader(rd, output.Size))
if err != nil {
return nil, fmt.Errorf("failed to read tag data: %w", err)
}
if _, err = rd.Discard(1); err != nil {
return nil, fmt.Errorf("tag reader Discard failed: %w", err)
}
tag, err := parseTagData(data)
if err != nil {
return nil, fmt.Errorf("failed to parse tag: %w", err)
}
commit, err := GetCommit(ctx, repoPath, tag.TargetSha.String())
if err != nil {
return nil, fmt.Errorf("failed to fetch commit: %w", err)
}
return commit, nil
case "commit":
commit, err := CommitFromReader(output.SHA, io.LimitReader(rd, output.Size))
if err != nil {
return nil, fmt.Errorf("faile to read commit from reader: %w", err)
}
if _, err = rd.Discard(1); err != nil {
return nil, fmt.Errorf("commit reader Discard failed: %w", err)
}
return commit, nil
default:
log.Warn().Msgf("Unknown object type: %s", output.Type)
_, err = rd.Discard(int(output.Size) + 1)
if err != nil {
return nil, fmt.Errorf("reader Discard failed: %w", err)
}
return nil, errors.NotFound("rev '%s' not found", rev)
}
}
// CommitFromReader will generate a Commit from a provided reader
// We need this to interpret commits from cat-file or cat-file --batch
//
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size.
//
//nolint:gocognit,nestif
func CommitFromReader(commitSHA sha.SHA, reader io.Reader) (*Commit, error) {
commit := &Commit{
SHA: commitSHA,
Author: Signature{},
Committer: Signature{},
}
payloadSB := new(strings.Builder)
signatureSB := new(strings.Builder)
messageSB := new(strings.Builder)
message := false
pgpsig := false
bufReader, ok := reader.(*bufio.Reader)
if !ok {
bufReader = bufio.NewReader(reader)
}
readLoop:
for {
line, err := bufReader.ReadBytes('\n')
if err != nil {
if errors.Is(err, io.EOF) {
if message {
_, _ = messageSB.Write(line)
}
_, _ = payloadSB.Write(line)
break readLoop
}
return nil, fmt.Errorf("error occurred while reading a line from buffer: %w", err)
}
if pgpsig {
if len(line) > 0 && line[0] == ' ' {
_, _ = signatureSB.Write(line[1:])
continue
}
pgpsig = false
}
if !message {
// This is probably not correct but is copied from go-gits interpretation...
trimmed := bytes.TrimSpace(line)
if len(trimmed) == 0 {
message = true
_, _ = payloadSB.Write(line)
continue
}
split := bytes.SplitN(trimmed, []byte{' '}, 2)
var data []byte
if len(split) > 1 {
data = split[1]
}
switch string(split[0]) {
case "tree":
_, _ = payloadSB.Write(line)
case "parent":
commit.ParentSHAs = append(commit.ParentSHAs, sha.Must(string(data)))
_, _ = payloadSB.Write(line)
case "author":
commit.Author, err = DecodeSignature(data)
if err != nil {
return nil, fmt.Errorf("failed to parse author signature: %w", err)
}
_, _ = payloadSB.Write(line)
case "committer":
commit.Committer, err = DecodeSignature(data)
if err != nil {
return nil, fmt.Errorf("failed to parse committer signature: %w", err)
}
_, _ = payloadSB.Write(line)
case "gpgsig":
_, _ = signatureSB.Write(data)
_ = signatureSB.WriteByte('\n')
pgpsig = true
}
} else {
_, _ = messageSB.Write(line)
_, _ = payloadSB.Write(line)
}
}
commit.Message = messageSB.String()
commit.Signature = &CommitGPGSignature{
Signature: signatureSB.String(),
Payload: payloadSB.String(),
}
if len(commit.Signature.Signature) == 0 {
commit.Signature = nil
}
return commit, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"context" "context"
@ -24,7 +24,7 @@ import (
) )
// Config set local git key and value configuration. // Config set local git key and value configuration.
func (a Adapter) Config( func (g *Git) Config(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
key string, key string,

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bufio" "bufio"
@ -21,23 +21,37 @@ import (
"fmt" "fmt"
"io" "io"
"math" "math"
"regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/parser" "github.com/harness/gitness/git/parser"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"github.com/harness/gitness/types"
"code.gitea.io/gitea/modules/git"
) )
type FileDiffRequest struct {
Path string `json:"path"`
StartLine int `json:"start_line"`
EndLine int `json:"-"` // warning: changes are possible and this field may not exist in the future
}
type FileDiffRequests []FileDiffRequest
type DiffShortStat struct {
Files int
Additions int
Deletions int
}
// modifyHeader needs to modify diff hunk header with the new start line // modifyHeader needs to modify diff hunk header with the new start line
// and end line with calculated span. // and end line with calculated span.
// if diff hunk header is -100, 50 +100, 50 and startLine = 120, endLine=140 // if diff hunk header is -100, 50 +100, 50 and startLine = 120, endLine=140
// then we need to modify header to -120,20 +120,20. // then we need to modify header to -120,20 +120,20.
// warning: changes are possible and param endLine may not exist in the future. // warning: changes are possible and param endLine may not exist in the future.
func modifyHeader(hunk types.HunkHeader, startLine, endLine int) []byte { func modifyHeader(hunk parser.HunkHeader, startLine, endLine int) []byte {
oldStartLine := hunk.OldLine oldStartLine := hunk.OldLine
newStartLine := hunk.NewLine newStartLine := hunk.NewLine
oldSpan := hunk.OldSpan oldSpan := hunk.OldSpan
@ -142,34 +156,42 @@ func cutLinesFromFullFileDiff(w io.Writer, r io.Reader, startLine, endLine int)
return scanner.Err() return scanner.Err()
} }
func (a Adapter) RawDiff( func (g *Git) RawDiff(
ctx context.Context, ctx context.Context,
w io.Writer, w io.Writer,
repoPath string, repoPath string,
baseRef string, baseRef string,
headRef string, headRef string,
mergeBase bool, mergeBase bool,
files ...types.FileDiffRequest, alternates []string,
files ...FileDiffRequest,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
} }
baseTag, err := a.GetAnnotatedTag(ctx, repoPath, baseRef) baseTag, err := g.GetAnnotatedTag(ctx, repoPath, baseRef)
if err == nil { if err == nil {
baseRef = baseTag.TargetSha baseRef = baseTag.TargetSha.String()
} }
headTag, err := a.GetAnnotatedTag(ctx, repoPath, headRef) headTag, err := g.GetAnnotatedTag(ctx, repoPath, headRef)
if err == nil { if err == nil {
headRef = headTag.TargetSha headRef = headTag.TargetSha.String()
} }
args := make([]string, 0, 8) cmd := command.New("diff",
args = append(args, "diff", "-M", "--full-index") command.WithFlag("-M"),
command.WithFlag("--full-index"),
)
if mergeBase { if mergeBase {
args = append(args, "--merge-base") cmd.Add(command.WithFlag("--merge-base"))
} }
if len(alternates) > 0 {
cmd.Add(command.WithAlternateObjectDirs(alternates...))
}
perFileDiffRequired := false perFileDiffRequired := false
paths := make([]string, 0, len(files)) paths := make([]string, 0, len(files))
if len(files) > 0 { if len(files) > 0 {
@ -186,8 +208,8 @@ func (a Adapter) RawDiff(
again: again:
startLine := 0 startLine := 0
endLine := 0 endLine := 0
newargs := make([]string, len(args), len(args)+8)
copy(newargs, args) newCmd := cmd.Clone()
if len(files) > 0 { if len(files) > 0 {
startLine = files[processed].StartLine startLine = files[processed].StartLine
@ -196,16 +218,15 @@ again:
if perFileDiffRequired { if perFileDiffRequired {
if startLine > 0 || endLine > 0 { if startLine > 0 || endLine > 0 {
newargs = append(newargs, "-U"+strconv.Itoa(math.MaxInt32)) newCmd.Add(command.WithFlag("-U" + strconv.Itoa(math.MaxInt32)))
} }
paths = []string{files[processed].Path} paths = []string{files[processed].Path}
} }
newargs = append(newargs, baseRef, headRef) newCmd.Add(command.WithArg(baseRef, headRef))
if len(paths) > 0 { if len(paths) > 0 {
newargs = append(newargs, "--") newCmd.Add(command.WithPostSepArg(paths...))
newargs = append(newargs, paths...)
} }
pipeRead, pipeWrite := io.Pipe() pipeRead, pipeWrite := io.Pipe()
@ -217,7 +238,12 @@ again:
_ = pipeWrite.CloseWithError(err) _ = pipeWrite.CloseWithError(err)
}() }()
err = a.rawDiff(ctx, pipeWrite, repoPath, baseRef, headRef, newargs...) if err = newCmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(pipeWrite),
); err != nil {
err = processGitErrorf(err, "git diff failed between %q and %q", baseRef, headRef)
}
}() }()
if err = cutLinesFromFullFileDiff(w, pipeRead, startLine, endLine); err != nil { if err = cutLinesFromFullFileDiff(w, pipeRead, startLine, endLine); err != nil {
@ -234,67 +260,44 @@ again:
return nil return nil
} }
func (a Adapter) rawDiff(
ctx context.Context,
w io.Writer,
repoPath string,
baseRef string,
headRef string,
args ...string,
) error {
cmd := git.NewCommand(ctx, args...)
cmd.SetDescription(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath))
errbuf := bytes.Buffer{}
if err := cmd.Run(&git.RunOpts{
Dir: repoPath,
Stderr: &errbuf,
Stdout: w,
}); err != nil {
if errbuf.Len() > 0 {
err = &runStdError{err: err, stderr: errbuf.String()}
}
return processGiteaErrorf(err, "git diff failed between %q and %q", baseRef, headRef)
}
return nil
}
// CommitDiff will stream diff for provided ref. // CommitDiff will stream diff for provided ref.
func (a Adapter) CommitDiff( func (g *Git) CommitDiff(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
sha string, rev string,
w io.Writer, w io.Writer,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
} }
if sha == "" { if rev == "" {
return errors.InvalidArgument("commit sha cannot be empty") return errors.InvalidArgument("git revision cannot be empty")
} }
args := make([]string, 0, 8)
args = append(args, "show", "--full-index", "--pretty=format:%b", sha)
stderr := new(bytes.Buffer) cmd := command.New("show",
cmd := git.NewCommand(ctx, args...) command.WithFlag("--full-index"),
if err := cmd.Run(&git.RunOpts{ command.WithFlag("--pretty=format:%b"),
Dir: repoPath, command.WithArg(rev),
Stdout: w, )
Stderr: stderr,
}); err != nil { if err := cmd.Run(ctx,
return processGiteaErrorf(err, "commit diff error: %v", stderr) command.WithDir(repoPath),
command.WithStdout(w),
); err != nil {
return processGitErrorf(err, "commit diff error")
} }
return nil return nil
} }
func (a Adapter) DiffShortStat( func (g *Git) DiffShortStat(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
baseRef string, baseRef string,
headRef string, headRef string,
useMergeBase bool, useMergeBase bool,
) (types.DiffShortStat, error) { ) (DiffShortStat, error) {
if repoPath == "" { if repoPath == "" {
return types.DiffShortStat{}, ErrRepositoryPathEmpty return DiffShortStat{}, ErrRepositoryPathEmpty
} }
separator := ".." separator := ".."
if useMergeBase { if useMergeBase {
@ -302,31 +305,27 @@ func (a Adapter) DiffShortStat(
} }
shortstatArgs := []string{baseRef + separator + headRef} shortstatArgs := []string{baseRef + separator + headRef}
if len(baseRef) == 0 || baseRef == git.EmptySHA { if len(baseRef) == 0 || baseRef == types.NilSHA {
shortstatArgs = []string{git.EmptyTreeSHA, headRef} shortstatArgs = []string{sha.EmptyTree, headRef}
} }
numFiles, totalAdditions, totalDeletions, err := git.GetDiffShortStat(ctx, repoPath, shortstatArgs...) stat, err := GetDiffShortStat(ctx, repoPath, shortstatArgs...)
if err != nil { if err != nil {
return types.DiffShortStat{}, processGiteaErrorf(err, "failed to get diff short stat between %s and %s", return DiffShortStat{}, processGitErrorf(err, "failed to get diff short stat between %s and %s",
baseRef, headRef) baseRef, headRef)
} }
return types.DiffShortStat{ return stat, nil
Files: numFiles,
Additions: totalAdditions,
Deletions: totalDeletions,
}, nil
} }
// GetDiffHunkHeaders for each file in diff output returns file name (old and new to detect renames), // GetDiffHunkHeaders for each file in diff output returns file name (old and new to detect renames),
// and all hunk headers. The diffs are generated with unified=0 parameter to create minimum sized hunks. // and all hunk headers. The diffs are generated with unified=0 parameter to create minimum sized hunks.
// Hunks' body is ignored. // Hunks' body is ignored.
// The purpose of this function is to get data based on which code comments could be repositioned. // The purpose of this function is to get data based on which code comments could be repositioned.
func (a Adapter) GetDiffHunkHeaders( func (g *Git) GetDiffHunkHeaders(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
targetRef string, targetRef string,
sourceRef string, sourceRef string,
) ([]*types.DiffFileHunkHeaders, error) { ) ([]*parser.DiffFileHunkHeaders, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
@ -340,13 +339,18 @@ func (a Adapter) GetDiffHunkHeaders(
_ = pipeWrite.CloseWithError(err) _ = pipeWrite.CloseWithError(err)
}() }()
cmd := git.NewCommand(ctx, cmd := command.New("diff",
"diff", "--patch", "--no-color", "--unified=0", sourceRef, targetRef) command.WithFlag("--patch"),
err = cmd.Run(&git.RunOpts{ command.WithFlag("--no-color"),
Dir: repoPath, command.WithFlag("--unified=0"),
Stdout: pipeWrite, command.WithArg(sourceRef),
Stderr: stderr, // We capture stderr output in a buffer. command.WithArg(targetRef),
}) )
err = cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(pipeWrite),
command.WithStderr(stderr), // We capture stderr output in a buffer.
)
}() }()
fileHunkHeaders, err := parser.GetHunkHeaders(pipeRead) fileHunkHeaders, err := parser.GetHunkHeaders(pipeRead)
@ -368,16 +372,16 @@ func (a Adapter) GetDiffHunkHeaders(
// The purpose of this function is to get diff data with which code comments could be generated. // The purpose of this function is to get diff data with which code comments could be generated.
// //
//nolint:gocognit //nolint:gocognit
func (a Adapter) DiffCut( func (g *Git) DiffCut(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
targetRef string, targetRef string,
sourceRef string, sourceRef string,
path string, path string,
params types.DiffCutParams, params parser.DiffCutParams,
) (types.HunkHeader, types.Hunk, error) { ) (parser.HunkHeader, parser.Hunk, error) {
if repoPath == "" { if repoPath == "" {
return types.HunkHeader{}, types.Hunk{}, ErrRepositoryPathEmpty return parser.HunkHeader{}, parser.Hunk{}, ErrRepositoryPathEmpty
} }
// first fetch the list of the changed files // first fetch the list of the changed files
@ -403,7 +407,7 @@ func (a Adapter) DiffCut(
diffEntries, err := parser.DiffRaw(pipeRead) diffEntries, err := parser.DiffRaw(pipeRead)
if err != nil { if err != nil {
return types.HunkHeader{}, types.Hunk{}, fmt.Errorf("failed to find the list of changed files: %w", err) return parser.HunkHeader{}, parser.Hunk{}, fmt.Errorf("failed to find the list of changed files: %w", err)
} }
var ( var (
@ -420,11 +424,11 @@ func (a Adapter) DiffCut(
if params.LineStartNew && path == entry.OldPath { if params.LineStartNew && path == entry.OldPath {
msg := "for renamed files provide the new file name if commenting the changed lines" msg := "for renamed files provide the new file name if commenting the changed lines"
return types.HunkHeader{}, types.Hunk{}, errors.InvalidArgument(msg) return parser.HunkHeader{}, parser.Hunk{}, errors.InvalidArgument(msg)
} }
if !params.LineStartNew && path == entry.Path { if !params.LineStartNew && path == entry.Path {
msg := "for renamed files provide the old file name if commenting the old lines" msg := "for renamed files provide the old file name if commenting the old lines"
return types.HunkHeader{}, types.Hunk{}, errors.InvalidArgument(msg) return parser.HunkHeader{}, parser.Hunk{}, errors.InvalidArgument(msg)
} }
default: default:
if entry.Path != path { if entry.Path != path {
@ -448,7 +452,7 @@ func (a Adapter) DiffCut(
} }
if newSHA == "" { if newSHA == "" {
return types.HunkHeader{}, types.Hunk{}, errors.NotFound("file %s not found in the diff", path) return parser.HunkHeader{}, parser.Hunk{}, errors.NotFound("file %s not found in the diff", path)
} }
// next pull the diff cut for the requested file // next pull the diff cut for the requested file
@ -484,35 +488,101 @@ func (a Adapter) DiffCut(
diffCutHeader, linesHunk, err := parser.DiffCut(pipeRead, params) diffCutHeader, linesHunk, err := parser.DiffCut(pipeRead, params)
if errStderr := parseDiffStderr(stderr); errStderr != nil { if errStderr := parseDiffStderr(stderr); errStderr != nil {
// First check if there's something in the stderr buffer, if yes that's the error // First check if there's something in the stderr buffer, if yes that's the error
return types.HunkHeader{}, types.Hunk{}, errStderr return parser.HunkHeader{}, parser.Hunk{}, errStderr
} }
if err != nil { if err != nil {
// Next check if reading the git diff output caused an error // Next check if reading the git diff output caused an error
return types.HunkHeader{}, types.Hunk{}, err return parser.HunkHeader{}, parser.Hunk{}, err
} }
return diffCutHeader, linesHunk, nil return diffCutHeader, linesHunk, nil
} }
func (a Adapter) DiffFileName(ctx context.Context, func (g *Git) DiffFileName(ctx context.Context,
repoPath string, repoPath string,
baseRef string, baseRef string,
headRef string, headRef string,
mergeBase bool, mergeBase bool,
) ([]string, error) { ) ([]string, error) {
args := make([]string, 0, 8) cmd := command.New("diff", command.WithFlag("--name-only"))
args = append(args, "diff", "--name-only")
if mergeBase { if mergeBase {
args = append(args, "--merge-base") cmd.Add(command.WithFlag("--merge-base"))
} }
args = append(args, baseRef, headRef) cmd.Add(command.WithArg(baseRef, headRef))
cmd := git.NewCommand(ctx, args...)
stdout, _, runErr := cmd.RunStdBytes(&git.RunOpts{Dir: repoPath}) stdout := &bytes.Buffer{}
if runErr != nil { err := cmd.Run(ctx,
return nil, processGiteaErrorf(runErr, "failed to trigger diff command") command.WithDir(repoPath),
command.WithStdout(stdout),
)
if err != nil {
return nil, processGitErrorf(err, "failed to trigger diff command")
} }
return parseLinesToSlice(stdout), nil return parseLinesToSlice(stdout.Bytes()), nil
}
// GetDiffShortStat counts number of changed files, number of additions and deletions.
func GetDiffShortStat(
ctx context.Context,
repoPath string,
args ...string,
) (DiffShortStat, error) {
// Now if we call:
// $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875
// we get:
// " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n"
cmd := command.New("diff",
command.WithFlag("--shortstat"),
command.WithArg(args...),
)
stdout := &bytes.Buffer{}
if err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(stdout),
); err != nil {
return DiffShortStat{}, err
}
return parseDiffStat(stdout.String())
}
var shortStatFormat = regexp.MustCompile(
`\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?`)
func parseDiffStat(stdout string) (stat DiffShortStat, err error) {
if len(stdout) == 0 || stdout == "\n" {
return DiffShortStat{}, nil
}
groups := shortStatFormat.FindStringSubmatch(stdout)
if len(groups) != 4 {
return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s groups: %s", stdout, groups)
}
stat.Files, err = strconv.Atoi(groups[1])
if err != nil {
return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumFiles %w",
stdout, err)
}
if len(groups[2]) != 0 {
stat.Additions, err = strconv.Atoi(groups[2])
if err != nil {
return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumAdditions %w",
stdout, err)
}
}
if len(groups[3]) != 0 {
stat.Deletions, err = strconv.Atoi(groups[3])
if err != nil {
return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumDeletions %w",
stdout, err)
}
}
return stat, nil
} }
func parseDiffStderr(stderr *bytes.Buffer) error { func parseDiffStderr(stderr *bytes.Buffer) error {
@ -528,7 +598,7 @@ func parseDiffStderr(stderr *bytes.Buffer) error {
errRaw = strings.TrimPrefix(errRaw, "fatal: ") // git errors start with the "fatal: " prefix errRaw = strings.TrimPrefix(errRaw, "fatal: ") // git errors start with the "fatal: " prefix
if strings.Contains(errRaw, "bad revision") { if strings.Contains(errRaw, "bad revision") {
return types.ErrSHADoesNotMatch return parser.ErrSHADoesNotMatch
} }
return errors.New(errRaw) return errors.New(errRaw)

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes" "bytes"
@ -21,14 +21,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/parser"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
func Test_modifyHeader(t *testing.T) { func Test_modifyHeader(t *testing.T) {
type args struct { type args struct {
hunk types.HunkHeader hunk parser.HunkHeader
startLine int startLine int
endLine int endLine int
} }
@ -40,7 +40,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test empty", name: "test empty",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 0, OldLine: 0,
OldSpan: 0, OldSpan: 0,
NewLine: 0, NewLine: 0,
@ -54,7 +54,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test empty 1", name: "test empty 1",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 0, OldLine: 0,
OldSpan: 0, OldSpan: 0,
NewLine: 0, NewLine: 0,
@ -68,7 +68,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test empty old", name: "test empty old",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 0, OldLine: 0,
OldSpan: 0, OldSpan: 0,
NewLine: 1, NewLine: 1,
@ -82,7 +82,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test empty new", name: "test empty new",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 1, OldLine: 1,
OldSpan: 10, OldSpan: 10,
NewLine: 0, NewLine: 0,
@ -96,7 +96,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test 1", name: "test 1",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 2, OldLine: 2,
OldSpan: 20, OldSpan: 20,
NewLine: 2, NewLine: 2,
@ -110,7 +110,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test 2", name: "test 2",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 2, OldLine: 2,
OldSpan: 20, OldSpan: 20,
NewLine: 2, NewLine: 2,
@ -124,7 +124,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test 4", name: "test 4",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 1, OldLine: 1,
OldSpan: 10, OldSpan: 10,
NewLine: 1, NewLine: 1,
@ -138,7 +138,7 @@ func Test_modifyHeader(t *testing.T) {
{ {
name: "test 5", name: "test 5",
args: args{ args: args{
hunk: types.HunkHeader{ hunk: parser.HunkHeader{
OldLine: 1, OldLine: 1,
OldSpan: 108, OldSpan: 108,
NewLine: 1, NewLine: 1,

162
git/api/errors.go Normal file
View File

@ -0,0 +1,162 @@
// 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 api
import (
"fmt"
"strings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/enum"
"github.com/rs/zerolog/log"
)
var (
ErrInvalidPath = errors.New("path is invalid")
ErrRepositoryPathEmpty = errors.InvalidArgument("repository path cannot be empty")
ErrBranchNameEmpty = errors.InvalidArgument("branch name cannot be empty")
ErrParseDiffHunkHeader = errors.Internal(nil, "failed to parse diff hunk header")
ErrNoDefaultBranch = errors.New("no default branch")
ErrInvalidSignature = errors.New("invalid signature")
)
// PushOutOfDateError represents an error if merging fails due to unrelated histories.
type PushOutOfDateError struct {
StdOut string
StdErr string
Err error
}
func (err *PushOutOfDateError) Error() string {
return fmt.Sprintf("PushOutOfDate Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// Unwrap unwraps the underlying error.
func (err *PushOutOfDateError) Unwrap() error {
return fmt.Errorf("%w - %s", err.Err, err.StdErr)
}
// PushRejectedError represents an error if merging fails due to rejection from a hook.
type PushRejectedError struct {
Message string
StdOut string
StdErr string
Err error
}
// IsErrPushRejected checks if an error is a PushRejectedError.
func IsErrPushRejected(err error) bool {
var errPushRejected *PushRejectedError
return errors.As(err, &errPushRejected)
}
func (err *PushRejectedError) Error() string {
return fmt.Sprintf("PushRejected Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// Unwrap unwraps the underlying error.
func (err *PushRejectedError) Unwrap() error {
return fmt.Errorf("%w - %s", err.Err, err.StdErr)
}
// GenerateMessage generates the remote message from the stderr.
func (err *PushRejectedError) GenerateMessage() {
messageBuilder := &strings.Builder{}
i := strings.Index(err.StdErr, "remote: ")
if i < 0 {
err.Message = ""
return
}
for {
if len(err.StdErr) <= i+8 {
break
}
if err.StdErr[i:i+8] != "remote: " {
break
}
i += 8
nl := strings.IndexByte(err.StdErr[i:], '\n')
if nl >= 0 {
messageBuilder.WriteString(err.StdErr[i : i+nl+1])
i = i + nl + 1
} else {
messageBuilder.WriteString(err.StdErr[i:])
i = len(err.StdErr)
}
}
err.Message = strings.TrimSpace(messageBuilder.String())
}
// MoreThanOneError represents an error when there are more
// than one sources (branch, tag) with the same name.
type MoreThanOneError struct {
StdOut string
StdErr string
Err error
}
// IsErrMoreThanOne checks if an error is a MoreThanOneError.
func IsErrMoreThanOne(err error) bool {
var errMoreThanOne *MoreThanOneError
return errors.As(err, &errMoreThanOne)
}
func (err *MoreThanOneError) Error() string {
return fmt.Sprintf("MoreThanOneError Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// Logs the error and message, returns either the provided message or a git equivalent if possible.
// Always logs the full message with error as warning.
func processGitErrorf(err error, format string, args ...interface{}) error {
// create fallback error returned if we can't map it
fallbackErr := errors.Internal(err, format, args...)
// always log internal error together with message.
log.Warn().Msgf("%v: [GIT] %v", fallbackErr, err)
switch {
case err.Error() == "no such file or directory":
return errors.NotFound("repository not found")
default:
return fallbackErr
}
}
// MergeUnrelatedHistoriesError represents an error if merging fails due to unrelated histories.
type MergeUnrelatedHistoriesError struct {
Method enum.MergeMethod
StdOut string
StdErr string
Err error
}
func IsMergeUnrelatedHistoriesError(err error) bool {
return errors.Is(err, &MergeUnrelatedHistoriesError{})
}
func (e *MergeUnrelatedHistoriesError) Error() string {
return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", e.Err, e.StdErr, e.StdOut)
}
func (e *MergeUnrelatedHistoriesError) Unwrap() error {
return e.Err
}
//nolint:errorlint // the purpose of this method is to check whether the target itself if of this type.
func (e *MergeUnrelatedHistoriesError) Is(target error) bool {
_, ok := target.(*MergeUnrelatedHistoriesError)
return ok
}

View File

@ -12,8 +12,22 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
const ( import (
gitTrace = "GIT_TRACE" "path"
"strings"
) )
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory.
func CleanUploadFileName(name string) string {
// Rebase the filename
name = strings.Trim(path.Clean("/"+name), "/")
// Git disallows any filenames to have a .git directory in them.
for _, part := range strings.Split(name, "/") {
if strings.ToLower(part) == ".git" {
return ""
}
}
return name
}

View File

@ -0,0 +1,94 @@
// 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 foreachref
import (
"encoding/hex"
"fmt"
"io"
"strings"
)
var (
nullChar = []byte("\x00")
dualNullChar = []byte("\x00\x00")
)
// Format supports specifying and parsing an output format for 'git
// for-each-ref'. See See git-for-each-ref(1) for available fields.
type Format struct {
// fieldNames hold %(fieldname)s to be passed to the '--format' flag of
// for-each-ref. See git-for-each-ref(1) for available fields.
fieldNames []string
// fieldDelim is the character sequence that is used to separate fields
// for each reference. fieldDelim and refDelim should be selected to not
// interfere with each other and to not be present in field values.
fieldDelim []byte
// fieldDelimStr is a string representation of fieldDelim. Used to save
// us from repetitive reallocation whenever we need the delimiter as a
// string.
fieldDelimStr string
// refDelim is the character sequence used to separate reference from
// each other in the output. fieldDelim and refDelim should be selected
// to not interfere with each other and to not be present in field
// values.
refDelim []byte
}
// NewFormat creates a forEachRefFormat using the specified fieldNames. See
// git-for-each-ref(1) for available fields.
func NewFormat(fieldNames ...string) Format {
return Format{
fieldNames: fieldNames,
fieldDelim: nullChar,
fieldDelimStr: string(nullChar),
refDelim: dualNullChar,
}
}
// Flag returns a for-each-ref --format flag value that captures the fieldNames.
func (f Format) Flag() string {
var formatFlag strings.Builder
for i, field := range f.fieldNames {
// field key and field value
formatFlag.WriteString(fmt.Sprintf("%s %%(%s)", field, field))
if i < len(f.fieldNames)-1 {
// note: escape delimiters to allow control characters as
// delimiters. For example, '%00' for null character or '%0a'
// for newline.
formatFlag.WriteString(f.hexEscaped(f.fieldDelim))
}
}
formatFlag.WriteString(f.hexEscaped(f.refDelim))
return formatFlag.String()
}
// Parser returns a Parser capable of parsing 'git for-each-ref' output produced
// with this Format.
func (f Format) Parser(r io.Reader) *Parser {
return NewParser(r, f)
}
// hexEscaped produces hex-escpaed characters from a string. For example, "\n\0"
// would turn into "%0a%00".
func (f Format) hexEscaped(delim []byte) string {
escaped := ""
for i := 0; i < len(delim); i++ {
escaped += "%" + hex.EncodeToString([]byte{delim[i]})
}
return escaped
}

View File

@ -0,0 +1,140 @@
// 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 foreachref
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
)
// Parser parses 'git for-each-ref' output according to a given output Format.
type Parser struct {
// tokenizes 'git for-each-ref' output into "reference paragraphs".
scanner *bufio.Scanner
// format represents the '--format' string that describes the expected
// 'git for-each-ref' output structure.
format Format
// err holds the last encountered error during parsing.
err error
}
// NewParser creates a 'git for-each-ref' output parser that will parse all
// references in the provided Reader. The references in the output are assumed
// to follow the specified Format.
func NewParser(r io.Reader, format Format) *Parser {
scanner := bufio.NewScanner(r)
// in addition to the reference delimiter we specified in the --format,
// `git for-each-ref` will always add a newline after every reference.
refDelim := make([]byte, 0, len(format.refDelim)+1)
refDelim = append(refDelim, format.refDelim...)
refDelim = append(refDelim, '\n')
// Split input into delimiter-separated "reference blocks".
scanner.Split(
func(data []byte, atEOF bool) (int, []byte, error) {
// Scan until delimiter, marking end of reference.
delimIdx := bytes.Index(data, refDelim)
if delimIdx >= 0 {
token := data[:delimIdx]
advance := delimIdx + len(refDelim)
return advance, token, nil
}
// If we're at EOF, we have a final, non-terminated reference. Return it.
if atEOF {
return len(data), data, nil
}
// Not yet a full field. Request more data.
return 0, nil, nil
})
return &Parser{
scanner: scanner,
format: format,
err: nil,
}
}
// Next returns the next reference as a collection of key-value pairs. nil
// denotes EOF but is also returned on errors. The Err method should always be
// consulted after Next returning nil.
//
// It could, for example return something like:
//
// { "objecttype": "tag", "refname:short": "v1.16.4", "object": "f460b7543ed500e49c133c2cd85c8c55ee9dbe27" }
func (p *Parser) Next() map[string]string {
if !p.scanner.Scan() {
return nil
}
fields, err := p.parseRef(p.scanner.Text())
if err != nil && !errors.Is(err, io.EOF) {
p.err = err
return nil
}
return fields
}
// Err returns the latest encountered parsing error.
func (p *Parser) Err() error {
return p.err
}
// parseRef parses out all key-value pairs from a single reference block, such as
//
// "type tag\0ref:short v1.16.4\0object f460b7543ed500e49c133c2cd85c8c55ee9dbe27"
func (p *Parser) parseRef(refBlock string) (map[string]string, error) {
if refBlock == "" {
// must be at EOF
return nil, io.EOF
}
fieldValues := make(map[string]string)
fields := strings.Split(refBlock, p.format.fieldDelimStr)
if len(fields) != len(p.format.fieldNames) {
return nil, fmt.Errorf("unexpected number of reference fields: wanted %d, was %d",
len(fields), len(p.format.fieldNames))
}
for i, field := range fields {
field = strings.TrimSpace(field)
var fieldKey string
var fieldVal string
firstSpace := strings.Index(field, " ")
if firstSpace > 0 {
fieldKey = field[:firstSpace]
fieldVal = field[firstSpace+1:]
} else {
// could be the case if the requested field had no value
fieldKey = field
}
// enforce the format order of fields
if p.format.fieldNames[i] != fieldKey {
return nil, fmt.Errorf("unexpected field name at position %d: wanted: '%s', was: '%s'",
i, p.format.fieldNames[i], fieldKey)
}
fieldValues[fieldKey] = fieldVal
}
return fieldValues, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
const ( const (
fmtEOL = "%n" fmtEOL = "%n"
@ -34,5 +34,5 @@ const (
fmtCommitterUnix = "%ct" // Unix timestamp fmtCommitterUnix = "%ct" // Unix timestamp
fmtSubject = "%s" fmtSubject = "%s"
fmtBody = "%b" fmtBody = "%B"
) )

View File

@ -12,36 +12,39 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"strconv" "strconv"
"strings" "strings"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command"
"code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func (a Adapter) InfoRefs( func (g *Git) InfoRefs(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
service string, service string,
w io.Writer, w io.Writer,
env ...string, env ...string,
) error { ) error {
cmd := &bytes.Buffer{} stdout := &bytes.Buffer{}
if err := git.NewCommand(ctx, service, "--stateless-rpc", "--advertise-refs", "."). cmd := command.New(service,
Run(&git.RunOpts{ command.WithFlag("--stateless-rpc"),
Env: env, command.WithFlag("--advertise-refs"),
Dir: repoPath, command.WithArg("."),
Stdout: cmd, )
}); err != nil { if err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(stdout),
command.WithEnvs(env...),
); err != nil {
return errors.Internal(err, "InfoRefs service %s failed", service) return errors.Internal(err, "InfoRefs service %s failed", service)
} }
if _, err := w.Write(packetWrite("# service=git-" + service + "\n")); err != nil { if _, err := w.Write(packetWrite("# service=git-" + service + "\n")); err != nil {
@ -52,13 +55,13 @@ func (a Adapter) InfoRefs(
return errors.Internal(err, "failed to flush data in InfoRefs %s service", service) return errors.Internal(err, "failed to flush data in InfoRefs %s service", service)
} }
if _, err := io.Copy(w, cmd); err != nil { if _, err := io.Copy(w, stdout); err != nil {
return errors.Internal(err, "streaming InfoRefs %s service failed", service) return errors.Internal(err, "streaming InfoRefs %s service failed", service)
} }
return nil return nil
} }
func (a Adapter) ServicePack( func (g *Git) ServicePack(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
service string, service string,
@ -66,24 +69,19 @@ func (a Adapter) ServicePack(
stdout io.Writer, stdout io.Writer,
env ...string, env ...string,
) error { ) error {
// set this for allow pre-receive and post-receive execute cmd := command.New(service,
env = append(env, "SSH_ORIGINAL_COMMAND="+service) command.WithFlag("--stateless-rpc"),
command.WithArg(repoPath),
var ( command.WithEnv("SSH_ORIGINAL_COMMAND", service),
stderr bytes.Buffer )
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(stdout),
command.WithStdin(stdin),
command.WithEnvs(env...),
) )
cmd := git.NewCommand(ctx, service, "--stateless-rpc", repoPath)
cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", repoPath))
err := cmd.Run(&git.RunOpts{
Dir: repoPath,
Env: env,
Stdout: stdout,
Stdin: stdin,
Stderr: &stderr,
UseContextTimeout: true,
})
if err != nil && err.Error() != "signal: killed" { if err != nil && err.Error() != "signal: killed" {
log.Ctx(ctx).Err(err).Msgf("Fail to serve RPC(%s) in %s: %v - %s", service, repoPath, err, stderr.String()) log.Ctx(ctx).Err(err).Msgf("Fail to serve RPC(%s) in %s: %v", service, repoPath, err)
} }
return err return err
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"context" "context"
@ -25,15 +25,15 @@ import (
"github.com/harness/gitness/cache" "github.com/harness/gitness/cache"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
) )
func NewInMemoryLastCommitCache( func NewInMemoryLastCommitCache(
cacheDuration time.Duration, cacheDuration time.Duration,
) cache.Cache[CommitEntryKey, *types.Commit] { ) cache.Cache[CommitEntryKey, *Commit] {
return cache.New[CommitEntryKey, *types.Commit]( return cache.New[CommitEntryKey, *Commit](
commitEntryGetter{}, commitEntryGetter{},
cacheDuration) cacheDuration)
} }
@ -41,12 +41,12 @@ func NewInMemoryLastCommitCache(
func NewRedisLastCommitCache( func NewRedisLastCommitCache(
redisClient redis.UniversalClient, redisClient redis.UniversalClient,
cacheDuration time.Duration, cacheDuration time.Duration,
) (cache.Cache[CommitEntryKey, *types.Commit], error) { ) (cache.Cache[CommitEntryKey, *Commit], error) {
if redisClient == nil { if redisClient == nil {
return nil, errors.New("unable to create redis based LastCommitCache as redis client is nil") return nil, errors.New("unable to create redis based LastCommitCache as redis client is nil")
} }
return cache.NewRedis[CommitEntryKey, *types.Commit]( return cache.NewRedis[CommitEntryKey, *Commit](
redisClient, redisClient,
commitEntryGetter{}, commitEntryGetter{},
func(key CommitEntryKey) string { func(key CommitEntryKey) string {
@ -58,8 +58,8 @@ func NewRedisLastCommitCache(
cacheDuration), nil cacheDuration), nil
} }
func NoLastCommitCache() cache.Cache[CommitEntryKey, *types.Commit] { func NoLastCommitCache() cache.Cache[CommitEntryKey, *Commit] {
return cache.NewNoCache[CommitEntryKey, *types.Commit](commitEntryGetter{}) return cache.NewNoCache[CommitEntryKey, *Commit](commitEntryGetter{})
} }
type CommitEntryKey string type CommitEntryKey string
@ -68,10 +68,10 @@ const separatorZero = "\x00"
func makeCommitEntryKey( func makeCommitEntryKey(
repoPath string, repoPath string,
commitSHA string, commitSHA sha.SHA,
path string, path string,
) CommitEntryKey { ) CommitEntryKey {
return CommitEntryKey(repoPath + separatorZero + commitSHA + separatorZero + path) return CommitEntryKey(repoPath + separatorZero + commitSHA.String() + separatorZero + path)
} }
func (c CommitEntryKey) Split() ( func (c CommitEntryKey) Split() (
@ -93,14 +93,14 @@ func (c CommitEntryKey) Split() (
type commitValueCodec struct{} type commitValueCodec struct{}
func (c commitValueCodec) Encode(v *types.Commit) string { func (c commitValueCodec) Encode(v *Commit) string {
buffer := &strings.Builder{} buffer := &strings.Builder{}
_ = gob.NewEncoder(buffer).Encode(v) _ = gob.NewEncoder(buffer).Encode(v)
return buffer.String() return buffer.String()
} }
func (c commitValueCodec) Decode(s string) (*types.Commit, error) { func (c commitValueCodec) Decode(s string) (*Commit, error) {
commit := &types.Commit{} commit := &Commit{}
if err := gob.NewDecoder(strings.NewReader(s)).Decode(commit); err != nil { if err := gob.NewDecoder(strings.NewReader(s)).Decode(commit); err != nil {
return nil, fmt.Errorf("failed to unpack commit entry value: %w", err) return nil, fmt.Errorf("failed to unpack commit entry value: %w", err)
} }
@ -114,12 +114,12 @@ type commitEntryGetter struct{}
func (c commitEntryGetter) Find( func (c commitEntryGetter) Find(
ctx context.Context, ctx context.Context,
key CommitEntryKey, key CommitEntryKey,
) (*types.Commit, error) { ) (*Commit, error) {
repoPath, commitSHA, path := key.Split() repoPath, commitSHA, path := key.Split()
if path == "" { if path == "" {
path = "." path = "."
} }
return GetCommit(ctx, repoPath, commitSHA, path) return getCommit(ctx, repoPath, commitSHA, path)
} }

51
git/api/mapping.go Normal file
View File

@ -0,0 +1,51 @@
// 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 api
func mapRawRef(
raw map[string]string,
) (map[GitReferenceField]string, error) {
res := make(map[GitReferenceField]string, len(raw))
for k, v := range raw {
gitRefField, err := ParseGitReferenceField(k)
if err != nil {
return nil, err
}
res[gitRefField] = v
}
return res, nil
}
func mapToReferenceSortingArgument(
s GitReferenceField,
o SortOrder,
) string {
sortBy := string(GitReferenceFieldRefName)
desc := o == SortOrderDesc
if s == GitReferenceFieldCreatorDate {
sortBy = string(GitReferenceFieldCreatorDate)
if o == SortOrderDefault {
desc = true
}
}
if desc {
return "-" + sortBy
}
return sortBy
}

View File

@ -12,39 +12,40 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"context" "context"
"fmt" "fmt"
"io" "io"
"path" "path"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
) )
type FileContent struct {
Path string
Content []byte
}
//nolint:gocognit //nolint:gocognit
func (a Adapter) MatchFiles( func (g *Git) MatchFiles(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
rev string, rev string,
treePath string, treePath string,
pattern string, pattern string,
maxSize int, maxSize int,
) ([]types.FileContent, error) { ) ([]FileContent, error) {
nodes, err := lsDirectory(ctx, repoPath, rev, treePath) nodes, err := lsDirectory(ctx, repoPath, rev, treePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list files in match files: %w", err) return nil, fmt.Errorf("failed to list files in match files: %w", err)
} }
catFileWriter, catFileReader, catFileStop := gitea.CatFileBatch(ctx, repoPath) catFileWriter, catFileReader, catFileStop := CatFileBatch(ctx, repoPath)
defer catFileStop() defer catFileStop()
var files []types.FileContent var files []FileContent
for i := range nodes { for i := range nodes {
if nodes[i].NodeType != types.TreeNodeTypeBlob { if nodes[i].NodeType != TreeNodeTypeBlob {
continue continue
} }
@ -57,19 +58,19 @@ func (a Adapter) MatchFiles(
continue continue
} }
_, err = catFileWriter.Write([]byte(nodes[i].Sha + "\n")) _, err = catFileWriter.Write([]byte(nodes[i].SHA.String() + "\n"))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to ask for file content from cat file batch: %w", err) return nil, fmt.Errorf("failed to ask for file content from cat file batch: %w", err)
} }
_, _, size, err := gitea.ReadBatchLine(catFileReader) output, err := ReadBatchHeaderLine(catFileReader)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read cat-file batch header: %w", err) return nil, fmt.Errorf("failed to read cat-file batch header: %w", err)
} }
reader := io.LimitReader(catFileReader, size+1) // plus eol reader := io.LimitReader(catFileReader, output.Size+1) // plus eol
if size > int64(maxSize) { if output.Size > int64(maxSize) {
_, err = io.Copy(io.Discard, reader) _, err = io.Copy(io.Discard, reader)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to discard a large file: %w", err) return nil, fmt.Errorf("failed to discard a large file: %w", err)
@ -89,7 +90,7 @@ func (a Adapter) MatchFiles(
continue continue
} }
files = append(files, types.FileContent{ files = append(files, FileContent{
Path: nodes[i].Path, Path: nodes[i].Path,
Content: data, Content: data,
}) })

104
git/api/merge.go Normal file
View File

@ -0,0 +1,104 @@
// 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 api
import (
"bytes"
"context"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/sha"
)
const (
// RemotePrefix is the base directory of the remotes information of git.
RemotePrefix = "refs/remotes/"
)
// GetMergeBase checks and returns merge base of two branches and the reference used as base.
func (g *Git) GetMergeBase(
ctx context.Context,
repoPath string,
remote string,
base string,
head string,
) (sha.SHA, string, error) {
if repoPath == "" {
return sha.None, "", ErrRepositoryPathEmpty
}
if remote == "" {
remote = "origin"
}
if remote != "origin" {
tmpBaseName := RemotePrefix + remote + "/tmp_" + base
// Fetch commit into a temporary branch in order to be able to handle commits and tags
cmd := command.New("fetch",
command.WithFlag("--no-tags"),
command.WithArg(remote),
command.WithPostSepArg(base+":"+tmpBaseName),
)
err := cmd.Run(ctx, command.WithDir(repoPath))
if err == nil {
base = tmpBaseName
}
}
stdout := &bytes.Buffer{}
cmd := command.New("merge-base",
command.WithArg(base, head),
)
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(stdout),
)
if err != nil {
return sha.None, "", processGitErrorf(err, "failed to get merge-base [%s, %s]", base, head)
}
result, err := sha.New(stdout.String())
if err != nil {
return sha.None, "", err
}
return result, base, nil
}
// IsAncestor returns if the provided commit SHA is ancestor of the other commit SHA.
func (g *Git) IsAncestor(
ctx context.Context,
repoPath string,
ancestorCommitSHA, descendantCommitSHA sha.SHA,
) (bool, error) {
if repoPath == "" {
return false, ErrRepositoryPathEmpty
}
cmd := command.New("merge-base",
command.WithFlag("--is-ancestor"),
command.WithArg(ancestorCommitSHA.String(), descendantCommitSHA.String()),
)
err := cmd.Run(ctx, command.WithDir(repoPath))
if err != nil {
cmdErr := command.AsError(err)
if cmdErr != nil && cmdErr.IsExitCode(1) && len(cmdErr.StdErr) == 0 {
return false, nil
}
return false, processGitErrorf(err, "failed to check commit ancestry [%s, %s]",
ancestorCommitSHA, descendantCommitSHA)
}
return true, nil
}

74
git/api/object.go Normal file
View File

@ -0,0 +1,74 @@
// 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 api
import (
"bytes"
"context"
"fmt"
"io"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/sha"
)
type GitObjectType string
const (
GitObjectTypeCommit GitObjectType = "commit"
GitObjectTypeTree GitObjectType = "tree"
GitObjectTypeBlob GitObjectType = "blob"
GitObjectTypeTag GitObjectType = "tag"
)
func ParseGitObjectType(t string) (GitObjectType, error) {
switch t {
case string(GitObjectTypeCommit):
return GitObjectTypeCommit, nil
case string(GitObjectTypeBlob):
return GitObjectTypeBlob, nil
case string(GitObjectTypeTree):
return GitObjectTypeTree, nil
case string(GitObjectTypeTag):
return GitObjectTypeTag, nil
default:
return GitObjectTypeBlob, fmt.Errorf("unknown git object type '%s'", t)
}
}
type SortOrder int
const (
SortOrderDefault SortOrder = iota
SortOrderAsc
SortOrderDesc
)
func (g *Git) HashObject(ctx context.Context, repoPath string, reader io.Reader) (sha.SHA, error) {
cmd := command.New("hash-object",
command.WithFlag("-w"),
command.WithFlag("--stdin"),
)
stdout := new(bytes.Buffer)
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdin(reader),
command.WithStdout(stdout),
)
if err != nil {
return sha.None, fmt.Errorf("failed to hash object: %w", err)
}
return sha.New(stdout.String())
}

View File

@ -12,35 +12,38 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/harness/gitness/git/types"
) )
type PathDetails struct {
Path string
LastCommit *Commit
}
// PathsDetails returns additional details about provided the paths. // PathsDetails returns additional details about provided the paths.
func (a Adapter) PathsDetails(ctx context.Context, func (g *Git) PathsDetails(ctx context.Context,
repoPath string, repoPath string,
rev string, rev string,
paths []string, paths []string,
) ([]types.PathDetails, error) { ) ([]PathDetails, error) {
// resolve the git revision to the commit SHA - we need the commit SHA for the last commit hash entry key. // resolve the git revision to the commit SHA - we need the commit SHA for the last commit hash entry key.
commitSHA, err := a.ResolveRev(ctx, repoPath, rev) commitSHA, err := g.ResolveRev(ctx, repoPath, rev)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get path details: %w", err) return nil, fmt.Errorf("failed to get path details: %w", err)
} }
results := make([]types.PathDetails, len(paths)) results := make([]PathDetails, len(paths))
for i, path := range paths { for i, path := range paths {
results[i].Path = path results[i].Path = path
path = cleanTreePath(path) // use cleaned-up path for calculations to avoid not-founds. path = cleanTreePath(path) // use cleaned-up path for calculations to avoid not-founds.
commitEntry, err := a.lastCommitCache.Get(ctx, makeCommitEntryKey(repoPath, commitSHA, path)) commitEntry, err := g.lastCommitCache.Get(ctx, makeCommitEntryKey(repoPath, commitSHA, path))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to find last commit for path %s: %w", path, err) return nil, fmt.Errorf("failed to find last commit for path %s: %w", path, err)
} }

View File

@ -12,38 +12,112 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"math" "math"
"strconv"
"strings" "strings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api/foreachref"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/hook" "github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
gitea "code.gitea.io/gitea/modules/git"
gitearef "code.gitea.io/gitea/modules/git/foreachref"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// GitReferenceField represents the different fields available When listing references.
// For the full list, see https://git-scm.com/docs/git-for-each-ref#_field_names
type GitReferenceField string
const (
GitReferenceFieldRefName GitReferenceField = "refname"
GitReferenceFieldObjectType GitReferenceField = "objecttype"
GitReferenceFieldObjectName GitReferenceField = "objectname"
GitReferenceFieldCreatorDate GitReferenceField = "creatordate"
)
func ParseGitReferenceField(f string) (GitReferenceField, error) {
switch f {
case string(GitReferenceFieldCreatorDate):
return GitReferenceFieldCreatorDate, nil
case string(GitReferenceFieldRefName):
return GitReferenceFieldRefName, nil
case string(GitReferenceFieldObjectName):
return GitReferenceFieldObjectName, nil
case string(GitReferenceFieldObjectType):
return GitReferenceFieldObjectType, nil
default:
return GitReferenceFieldRefName, fmt.Errorf("unknown git reference field '%s'", f)
}
}
type WalkInstruction int
const (
WalkInstructionStop WalkInstruction = iota
WalkInstructionHandle
WalkInstructionSkip
)
type WalkReferencesEntry map[GitReferenceField]string
// TODO: can be generic (so other walk methods can use the same)
type WalkReferencesInstructor func(WalkReferencesEntry) (WalkInstruction, error)
// TODO: can be generic (so other walk methods can use the same)
type WalkReferencesHandler func(WalkReferencesEntry) error
type WalkReferencesOptions struct {
// Patterns are the patterns used to pre-filter the references of the repo.
// OPTIONAL. By default all references are walked.
Patterns []string
// Fields indicates the fields that are passed to the instructor & handler
// OPTIONAL. Default fields are:
// - GitReferenceFieldRefName
// - GitReferenceFieldObjectName
Fields []GitReferenceField
// Instructor indicates on how to handle the reference.
// OPTIONAL. By default all references are handled.
// NOTE: once walkInstructionStop is returned, the walking stops.
Instructor WalkReferencesInstructor
// Sort indicates the field by which the references should be sorted.
// OPTIONAL. By default GitReferenceFieldRefName is used.
Sort GitReferenceField
// Order indicates the Order (asc or desc) of the sorted output
Order SortOrder
// MaxWalkDistance is the maximum number of nodes that are iterated over before the walking stops.
// OPTIONAL. A value of <= 0 will walk all references.
// WARNING: Skipped elements count towards the walking distance
MaxWalkDistance int32
}
func DefaultInstructor( func DefaultInstructor(
_ types.WalkReferencesEntry, _ WalkReferencesEntry,
) (types.WalkInstruction, error) { ) (WalkInstruction, error) {
return types.WalkInstructionHandle, nil return WalkInstructionHandle, nil
} }
// WalkReferences uses the provided options to filter the available references of the repo, // WalkReferences uses the provided options to filter the available references of the repo,
// and calls the handle function for every matching node. // 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. // 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. // TODO: walkReferences related code should be moved to separate file.
func (a Adapter) WalkReferences( func (g *Git) WalkReferences(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
handler types.WalkReferencesHandler, handler WalkReferencesHandler,
opts *types.WalkReferencesOptions, opts *WalkReferencesOptions,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
@ -53,7 +127,7 @@ func (a Adapter) WalkReferences(
opts.Instructor = DefaultInstructor opts.Instructor = DefaultInstructor
} }
if len(opts.Fields) == 0 { if len(opts.Fields) == 0 {
opts.Fields = []types.GitReferenceField{types.GitReferenceFieldRefName, types.GitReferenceFieldObjectName} opts.Fields = []GitReferenceField{GitReferenceFieldRefName, GitReferenceFieldObjectName}
} }
if opts.MaxWalkDistance <= 0 { if opts.MaxWalkDistance <= 0 {
opts.MaxWalkDistance = math.MaxInt32 opts.MaxWalkDistance = math.MaxInt32
@ -62,40 +136,35 @@ func (a Adapter) WalkReferences(
opts.Patterns = []string{} opts.Patterns = []string{}
} }
if string(opts.Sort) == "" { if string(opts.Sort) == "" {
opts.Sort = types.GitReferenceFieldRefName opts.Sort = GitReferenceFieldRefName
} }
// prepare for-each-ref input // prepare for-each-ref input
sortArg := mapToGiteaReferenceSortingArgument(opts.Sort, opts.Order) sortArg := mapToReferenceSortingArgument(opts.Sort, opts.Order)
rawFields := make([]string, len(opts.Fields)) rawFields := make([]string, len(opts.Fields))
for i := range opts.Fields { for i := range opts.Fields {
rawFields[i] = string(opts.Fields[i]) rawFields[i] = string(opts.Fields[i])
} }
giteaFormat := gitearef.NewFormat(rawFields...) format := foreachref.NewFormat(rawFields...)
// initializer pipeline for output processing // initializer pipeline for output processing
pipeOut, pipeIn := io.Pipe() pipeOut, pipeIn := io.Pipe()
defer pipeOut.Close() defer pipeOut.Close()
defer pipeIn.Close()
stderr := strings.Builder{}
rc := &gitea.RunOpts{Dir: repoPath, Stdout: pipeIn, Stderr: &stderr}
go func() { go func() {
// create array for args as patterns have to be passed as separate args. cmd := command.New("for-each-ref",
args := []string{ command.WithFlag("--format", format.Flag()),
"for-each-ref", command.WithFlag("--sort", sortArg),
"--format", command.WithFlag("--count", strconv.Itoa(int(opts.MaxWalkDistance))),
giteaFormat.Flag(), command.WithFlag("--ignore-case"),
"--sort", )
sortArg, cmd.Add(command.WithArg(opts.Patterns...))
"--count", err := cmd.Run(ctx,
fmt.Sprint(opts.MaxWalkDistance), command.WithDir(repoPath),
"--ignore-case", command.WithStdout(pipeIn),
} )
args = append(args, opts.Patterns...)
err := gitea.NewCommand(ctx, args...).Run(rc)
if err != nil { if err != nil {
_ = pipeIn.CloseWithError(gitea.ConcatenateError(err, stderr.String())) _ = pipeIn.CloseWithError(err)
} else { } else {
_ = pipeIn.Close() _ = pipeIn.Close()
} }
@ -103,14 +172,14 @@ func (a Adapter) WalkReferences(
// TODO: return error from git command!!!! // TODO: return error from git command!!!!
parser := giteaFormat.Parser(pipeOut) parser := format.Parser(pipeOut)
return walkGiteaReferenceParser(parser, handler, opts) return walkReferenceParser(parser, handler, opts)
} }
func walkGiteaReferenceParser( func walkReferenceParser(
parser *gitearef.Parser, parser *foreachref.Parser,
handler types.WalkReferencesHandler, handler WalkReferencesHandler,
opts *types.WalkReferencesOptions, opts *WalkReferencesOptions,
) error { ) error {
for i := int32(0); i < opts.MaxWalkDistance; i++ { for i := int32(0); i < opts.MaxWalkDistance; i++ {
// parse next line - nil if end of output reached or an error occurred. // parse next line - nil if end of output reached or an error occurred.
@ -120,7 +189,7 @@ func walkGiteaReferenceParser(
} }
// convert to correct map. // convert to correct map.
ref, err := mapGiteaRawRef(rawRef) ref, err := mapRawRef(rawRef)
if err != nil { if err != nil {
return err return err
} }
@ -131,10 +200,10 @@ func walkGiteaReferenceParser(
return fmt.Errorf("error getting instruction: %w", err) return fmt.Errorf("error getting instruction: %w", err)
} }
if instruction == types.WalkInstructionSkip { if instruction == WalkInstructionSkip {
continue continue
} }
if instruction == types.WalkInstructionStop { if instruction == WalkInstructionStop {
break break
} }
@ -146,7 +215,7 @@ func walkGiteaReferenceParser(
} }
if err := parser.Err(); err != nil { if err := parser.Err(); err != nil {
return processGiteaErrorf(err, "failed to parse reference walk output") return processGitErrorf(err, "failed to parse reference walk output")
} }
return nil return nil
@ -155,60 +224,63 @@ func walkGiteaReferenceParser(
// GetRef get's the target of a reference // GetRef get's the target of a reference
// IMPORTANT provide full reference name to limit risk of collisions across reference types // IMPORTANT provide full reference name to limit risk of collisions across reference types
// (e.g `refs/heads/main` instead of `main`). // (e.g `refs/heads/main` instead of `main`).
func (a Adapter) GetRef( func (g *Git) GetRef(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
ref string, ref string,
) (string, error) { ) (sha.SHA, error) {
if repoPath == "" { if repoPath == "" {
return "", ErrRepositoryPathEmpty return sha.None, ErrRepositoryPathEmpty
} }
cmd := gitea.NewCommand(ctx, "show-ref", "--verify", "-s", "--", ref) cmd := command.New("show-ref",
stdout, _, err := cmd.RunStdString(&gitea.RunOpts{ command.WithFlag("--verify"),
Dir: repoPath, command.WithFlag("-s"),
}) command.WithArg(ref),
)
output := &bytes.Buffer{}
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
if err != nil { if err != nil {
if err.IsExitCode(128) && strings.Contains(err.Stderr(), "not a valid ref") { if command.AsError(err).IsExitCode(128) && strings.Contains(err.Error(), "not a valid ref") {
return "", types.ErrNotFound("reference %q not found", ref) return sha.None, errors.NotFound("reference %q not found", ref)
} }
return "", err return sha.None, err
} }
return strings.TrimSpace(stdout), nil return sha.New(output.String())
} }
// UpdateRef allows to update / create / delete references // UpdateRef allows to update / create / delete references
// IMPORTANT provide full reference name to limit risk of collisions across reference types // IMPORTANT provide full reference name to limit risk of collisions across reference types
// (e.g `refs/heads/main` instead of `main`). // (e.g `refs/heads/main` instead of `main`).
func (a Adapter) UpdateRef( func (g *Git) UpdateRef(
ctx context.Context, ctx context.Context,
envVars map[string]string, envVars map[string]string,
repoPath string, repoPath string,
ref string, ref string,
oldValue string, oldValue sha.SHA,
newValue string, newValue sha.SHA,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
} }
// don't break existing interface - user calls with empty value to delete the ref. // don't break existing interface - user calls with empty value to delete the ref.
if newValue == "" { if newValue.IsEmpty() {
newValue = types.NilSHA newValue = sha.Nil
} }
// if no old value was provided, use current value (as required for hooks) // if no old value was provided, use current value (as required for hooks)
// TODO: technically a delete could fail if someone updated the ref in the meanwhile. // TODO: technically a delete could fail if someone updated the ref in the meanwhile.
//nolint:gocritic,nestif //nolint:gocritic,nestif
if oldValue == "" { if oldValue.IsEmpty() {
val, err := a.GetRef(ctx, repoPath, ref) val, err := g.GetRef(ctx, repoPath, ref)
if types.IsNotFoundError(err) { if errors.IsNotFound(err) {
// fail in case someone tries to delete a reference that doesn't exist. // fail in case someone tries to delete a reference that doesn't exist.
if newValue == types.NilSHA { if newValue.IsNil() {
return types.ErrNotFound("reference %q not found", ref) return errors.NotFound("reference %q not found", ref)
} }
oldValue = types.NilSHA oldValue = sha.Nil
} else if err != nil { } else if err != nil {
return fmt.Errorf("failed to get current value of reference: %w", err) return fmt.Errorf("failed to get current value of reference: %w", err)
} else { } else {
@ -216,7 +288,7 @@ func (a Adapter) UpdateRef(
} }
} }
err := a.updateRefWithHooks( err := g.updateRefWithHooks(
ctx, ctx,
envVars, envVars,
repoPath, repoPath,
@ -234,29 +306,29 @@ func (a Adapter) UpdateRef(
// updateRefWithHooks performs a git-ref update for the provided reference. // updateRefWithHooks performs a git-ref update for the provided reference.
// Requires both old and new value to be provided explcitly, or the call fails (ensures consistency across operation). // Requires both old and new value to be provided explcitly, or the call fails (ensures consistency across operation).
// pre-receice will be called before the update, post-receive after. // pre-receice will be called before the update, post-receive after.
func (a Adapter) updateRefWithHooks( func (g *Git) updateRefWithHooks(
ctx context.Context, ctx context.Context,
envVars map[string]string, envVars map[string]string,
repoPath string, repoPath string,
ref string, ref string,
oldValue string, oldValue sha.SHA,
newValue string, newValue sha.SHA,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
} }
if oldValue == "" { if oldValue.IsEmpty() {
return fmt.Errorf("oldValue can't be empty") return fmt.Errorf("oldValue can't be empty")
} }
if newValue == "" { if newValue.IsEmpty() {
return fmt.Errorf("newValue can't be empty") return fmt.Errorf("newValue can't be empty")
} }
if oldValue == types.NilSHA && newValue == types.NilSHA { if oldValue.IsNil() && newValue.IsNil() {
return fmt.Errorf("provided values cannot be both empty") return fmt.Errorf("provided values cannot be both empty")
} }
githookClient, err := a.githookFactory.NewClient(ctx, envVars) githookClient, err := g.githookFactory.NewClient(ctx, envVars)
if err != nil { if err != nil {
return fmt.Errorf("failed to create githook client: %w", err) return fmt.Errorf("failed to create githook client: %w", err)
} }
@ -278,28 +350,23 @@ func (a Adapter) updateRefWithHooks(
return fmt.Errorf("pre-receive call returned error: %q", *out.Error) return fmt.Errorf("pre-receive call returned error: %q", *out.Error)
} }
if a.traceGit { if g.traceGit {
log.Ctx(ctx).Trace(). log.Ctx(ctx).Trace().
Str("git", "pre-receive"). Str("git", "pre-receive").
Msgf("pre-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n")) Msgf("pre-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n"))
} }
args := make([]string, 0, 4) cmd := command.New("update-ref")
args = append(args, "update-ref") if newValue.IsNil() {
if newValue == types.NilSHA { cmd.Add(command.WithFlag("-d", ref))
args = append(args, "-d", ref)
} else { } else {
args = append(args, ref, newValue) cmd.Add(command.WithArg(ref, newValue.String()))
} }
args = append(args, oldValue) cmd.Add(command.WithArg(oldValue.String()))
err = cmd.Run(ctx, command.WithDir(repoPath))
cmd := gitea.NewCommand(ctx, args...)
_, _, err = cmd.RunStdString(&gitea.RunOpts{
Dir: repoPath,
})
if err != nil { if err != nil {
return processGiteaErrorf(err, "update of ref %q from %q to %q failed", ref, oldValue, newValue) return processGitErrorf(err, "update of ref %q from %q to %q failed", ref, oldValue, newValue)
} }
// call post-receive after updating the reference // call post-receive after updating the reference
@ -319,7 +386,7 @@ func (a Adapter) updateRefWithHooks(
return fmt.Errorf("post-receive call returned error: %q", *out.Error) return fmt.Errorf("post-receive call returned error: %q", *out.Error)
} }
if a.traceGit { if g.traceGit {
log.Ctx(ctx).Trace(). log.Ctx(ctx).Trace().
Str("git", "post-receive"). Str("git", "post-receive").
Msgf("post-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n")) Msgf("post-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n"))

View File

@ -12,23 +12,59 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type CloneRepoOptions struct {
Timeout time.Duration
Mirror bool
Bare bool
Quiet bool
Branch string
Shared bool
NoCheckout bool
Depth int
Filter string
SkipTLSVerify bool
}
type PushOptions struct {
Remote string
Branch string
Force bool
ForceWithLease string
Env []string
Timeout time.Duration
Mirror bool
}
// ObjectCount represents the parsed information from the `git count-objects -v` command.
// For field meanings, see https://git-scm.com/docs/git-count-objects#_options.
type ObjectCount struct {
Count int
Size int64
InPack int
Packs int
SizePack int64
PrunePackable int
Garbage int
SizeGarbage int64
}
const ( const (
gitReferenceNamePrefixBranch = "refs/heads/" gitReferenceNamePrefixBranch = "refs/heads/"
gitReferenceNamePrefixTag = "refs/tags/" gitReferenceNamePrefixTag = "refs/tags/"
@ -37,7 +73,7 @@ const (
var lsRemoteHeadRegexp = regexp.MustCompile(`ref: refs/heads/([^\s]+)\s+HEAD`) var lsRemoteHeadRegexp = regexp.MustCompile(`ref: refs/heads/([^\s]+)\s+HEAD`)
// InitRepository initializes a new Git repository. // InitRepository initializes a new Git repository.
func (a Adapter) InitRepository( func (g *Git) InitRepository(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
bare bool, bare bool,
@ -57,19 +93,8 @@ func (a Adapter) InitRepository(
return cmd.Run(ctx, command.WithDir(repoPath)) return cmd.Run(ctx, command.WithDir(repoPath))
} }
func (a Adapter) OpenRepository(
ctx context.Context,
repoPath string,
) (*gitea.Repository, error) {
repo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, processGiteaErrorf(err, "failed to open repository")
}
return repo, nil
}
// SetDefaultBranch sets the default branch of a repo. // SetDefaultBranch sets the default branch of a repo.
func (a Adapter) SetDefaultBranch( func (g *Git) SetDefaultBranch(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
defaultBranch string, defaultBranch string,
@ -78,102 +103,124 @@ func (a Adapter) SetDefaultBranch(
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
} }
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return processGiteaErrorf(err, "failed to open repository")
}
defer giteaRepo.Close()
// if requested, error out if branch doesn't exist. Otherwise, blindly set it. // if requested, error out if branch doesn't exist. Otherwise, blindly set it.
if !allowEmpty && !giteaRepo.IsBranchExist(defaultBranch) { exist, err := g.IsBranchExist(ctx, repoPath, defaultBranch)
if err != nil {
log.Ctx(ctx).Err(err).Msgf("failed to set default branch")
}
if !allowEmpty && !exist {
// TODO: ensure this returns not found error to caller // TODO: ensure this returns not found error to caller
return fmt.Errorf("branch '%s' does not exist", defaultBranch) return fmt.Errorf("branch '%s' does not exist", defaultBranch)
} }
// change default branch // change default branch
err = giteaRepo.SetDefaultBranch(defaultBranch) cmd := command.New("symbolic-ref",
command.WithArg("HEAD", gitReferenceNamePrefixBranch+defaultBranch),
)
err = cmd.Run(ctx, command.WithDir(repoPath))
if err != nil { if err != nil {
return processGiteaErrorf(err, "failed to set new default branch") return processGitErrorf(err, "failed to set new default branch")
} }
return nil return nil
} }
// GetDefaultBranch gets the default branch of a repo. // GetDefaultBranch gets the default branch of a repo.
func (a Adapter) GetDefaultBranch( func (g *Git) GetDefaultBranch(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
) (string, error) { ) (string, error) {
if repoPath == "" { if repoPath == "" {
return "", ErrRepositoryPathEmpty return "", ErrRepositoryPathEmpty
} }
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return "", processGiteaErrorf(err, "failed to open gitea repo")
}
defer giteaRepo.Close()
// get default branch // get default branch
branch, err := giteaRepo.GetDefaultBranch() cmd := command.New("symbolic-ref",
command.WithArg("HEAD"),
)
output := &bytes.Buffer{}
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(output))
if err != nil { if err != nil {
return "", processGiteaErrorf(err, "failed to get default branch") return "", processGitErrorf(err, "failed to get default branch")
} }
return branch, nil return output.String(), nil
} }
// GetRemoteDefaultBranch retrieves the default branch of a remote repository. // GetRemoteDefaultBranch retrieves the default branch of a remote repository.
// If the repo doesn't have a default branch, types.ErrNoDefaultBranch is returned. // If the repo doesn't have a default branch, types.ErrNoDefaultBranch is returned.
func (a Adapter) GetRemoteDefaultBranch( func (g *Git) GetRemoteDefaultBranch(
ctx context.Context, ctx context.Context,
remoteURL string, remoteURL string,
) (string, error) { ) (string, error) {
args := []string{ cmd := command.New("ls-remote",
"-c", "credential.helper=", command.WithConfig("credential.helper", ""),
"ls-remote", command.WithFlag("--symref"),
"--symref", command.WithFlag("-q"),
"-q", command.WithArg(remoteURL),
remoteURL, command.WithArg("HEAD"),
"HEAD", )
} output := &bytes.Buffer{}
if err := cmd.Run(ctx, command.WithStdout(output)); err != nil {
cmd := gitea.NewCommand(ctx, args...) return "", processGitErrorf(err, "failed to ls remote repo")
stdOut, _, err := cmd.RunStdString(nil)
if err != nil {
return "", processGiteaErrorf(err, "failed to ls remote repo")
} }
// git output looks as follows, and we are looking for the ref that HEAD points to // git output looks as follows, and we are looking for the ref that HEAD points to
// ref: refs/heads/main HEAD // ref: refs/heads/main HEAD
// 46963bc7f0b5e8c5f039d50ac9e6e51933c78cdf HEAD // 46963bc7f0b5e8c5f039d50ac9e6e51933c78cdf HEAD
match := lsRemoteHeadRegexp.FindStringSubmatch(stdOut) match := lsRemoteHeadRegexp.FindStringSubmatch(strings.TrimSpace(output.String()))
if match == nil { if match == nil {
return "", types.ErrNoDefaultBranch return "", ErrNoDefaultBranch
} }
return match[1], nil return match[1], nil
} }
func (a Adapter) Clone( func (g *Git) Clone(
ctx context.Context, ctx context.Context,
from string, from string,
to string, to string,
opts types.CloneRepoOptions, opts CloneRepoOptions,
) error { ) error {
err := gitea.Clone(ctx, from, to, gitea.CloneRepoOptions{ if err := os.MkdirAll(to, os.ModePerm); err != nil {
Timeout: opts.Timeout, return err
Mirror: opts.Mirror, }
Bare: opts.Bare,
Quiet: opts.Quiet, cmd := command.New("clone")
Branch: opts.Branch, if opts.SkipTLSVerify {
Shared: opts.Shared, cmd.Add(command.WithConfig("http.sslVerify", "false"))
NoCheckout: opts.NoCheckout, }
Depth: opts.Depth, if opts.Mirror {
Filter: opts.Filter, cmd.Add(command.WithFlag("--mirror"))
SkipTLSVerify: opts.SkipTLSVerify, }
}) if opts.Bare {
if err != nil { cmd.Add(command.WithFlag("--bare"))
return processGiteaErrorf(err, "failed to clone repo") }
if opts.Quiet {
cmd.Add(command.WithFlag("--quiet"))
}
if opts.Shared {
cmd.Add(command.WithFlag("-s"))
}
if opts.NoCheckout {
cmd.Add(command.WithFlag("--no-checkout"))
}
if opts.Depth > 0 {
cmd.Add(command.WithFlag("--depth", strconv.Itoa(opts.Depth)))
}
if opts.Filter != "" {
cmd.Add(command.WithFlag("--filter", opts.Filter))
}
if len(opts.Branch) > 0 {
cmd.Add(command.WithFlag("-b", opts.Branch))
}
cmd.Add(command.WithPostSepArg(from, to))
if err := cmd.Run(ctx); err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
} }
return nil return nil
@ -181,7 +228,7 @@ func (a Adapter) Clone(
// Sync synchronizes the repository to match the provided source. // Sync synchronizes the repository to match the provided source.
// NOTE: This is a read operation and doesn't trigger any server side hooks. // NOTE: This is a read operation and doesn't trigger any server side hooks.
func (a Adapter) Sync( func (g *Git) Sync(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
source string, source string,
@ -193,33 +240,31 @@ func (a Adapter) Sync(
if len(refSpecs) == 0 { if len(refSpecs) == 0 {
refSpecs = []string{"+refs/*:refs/*"} refSpecs = []string{"+refs/*:refs/*"}
} }
args := []string{ cmd := command.New("fetch",
"-c", "advice.fetchShowForcedUpdates=false", command.WithConfig("advice.fetchShowForcedUpdates", "false"),
"-c", "credential.helper=", command.WithConfig("credential.helper", ""),
"fetch", command.WithFlag(
"--quiet", "--quiet",
"--prune", "--prune",
"--atomic", "--atomic",
"--force", "--force",
"--no-write-fetch-head", "--no-write-fetch-head",
"--no-show-forced-updates", "--no-show-forced-updates",
source, ),
} command.WithArg(source),
args = append(args, refSpecs...) command.WithArg(refSpecs...),
)
cmd := gitea.NewCommand(ctx, args...) err := cmd.Run(ctx, command.WithDir(repoPath))
_, _, err := cmd.RunStdString(&gitea.RunOpts{
Dir: repoPath,
UseContextTimeout: true,
})
if err != nil { if err != nil {
return processGiteaErrorf(err, "failed to sync repo") return processGitErrorf(err, "failed to sync repo")
} }
return nil return nil
} }
func (a Adapter) AddFiles( func (g *Git) AddFiles(
ctx context.Context,
repoPath string, repoPath string,
all bool, all bool,
files ...string, files ...string,
@ -227,20 +272,26 @@ func (a Adapter) AddFiles(
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
} }
err := gitea.AddChanges(repoPath, all, files...)
cmd := command.New("add")
if all {
cmd.Add(command.WithFlag("--all"))
}
cmd.Add(command.WithPostSepArg(files...))
err := cmd.Run(ctx, command.WithDir(repoPath))
if err != nil { if err != nil {
return processGiteaErrorf(err, "failed to add changes") return processGitErrorf(err, "failed to add changes")
} }
return nil return nil
} }
// Commit commits the changes of the repository. // Commit commits the changes of the repository.
// NOTE: Modification of gitea implementation that supports commiter_date + author_date. func (g *Git) Commit(
func (a Adapter) Commit(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
opts types.CommitChangesOptions, opts CommitChangesOptions,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
@ -262,75 +313,57 @@ func (a Adapter) Commit(
err := cmd.Run(ctx, command.WithDir(repoPath)) err := cmd.Run(ctx, command.WithDir(repoPath))
// No stderr but exit status 1 means nothing to commit (see gitea CommitChanges) // No stderr but exit status 1 means nothing to commit (see gitea CommitChanges)
if err != nil && err.Error() != "exit status 1" { if err != nil && err.Error() != "exit status 1" {
return processGiteaErrorf(err, "failed to commit changes") return processGitErrorf(err, "failed to commit changes")
} }
return nil return nil
} }
// Push pushs local commits to given remote branch. // Push pushs local commits to given remote branch.
// NOTE: Modification of gitea implementation that supports --force-with-lease. // TODOD: return our own error types and move to above api.Push method
// TODOD: return our own error types and move to above adapter.Push method func (g *Git) Push(
func (a Adapter) Push(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
opts types.PushOptions, opts PushOptions,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
} }
cmd := gitea.NewCommand(ctx, cmd := command.New("push",
"-c", "credential.helper=", command.WithConfig("credential.helper", ""),
"push",
) )
if opts.Force { if opts.Force {
cmd.AddArguments("-f") cmd.Add(command.WithFlag("-f"))
} }
if opts.ForceWithLease != "" { if opts.ForceWithLease != "" {
cmd.AddArguments(fmt.Sprintf("--force-with-lease=%s", opts.ForceWithLease)) cmd.Add(command.WithFlag("--force-with-lease=" + opts.ForceWithLease))
} }
if opts.Mirror { if opts.Mirror {
cmd.AddArguments("--mirror") cmd.Add(command.WithFlag("--mirror"))
} }
cmd.AddArguments("--", opts.Remote) cmd.Add(command.WithPostSepArg(opts.Remote))
if len(opts.Branch) > 0 { if len(opts.Branch) > 0 {
cmd.AddArguments(opts.Branch) cmd.Add(command.WithPostSepArg(opts.Branch))
}
if g.traceGit {
cmd.Add(command.WithEnv(command.GitTrace, "true"))
} }
// remove credentials if there are any // remove credentials if there are any
if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") { if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {
opts.Remote = util.SanitizeCredentialURLs(opts.Remote) opts.Remote = SanitizeCredentialURLs(opts.Remote)
} }
if opts.Timeout == 0 {
opts.Timeout = -1
}
if a.traceGit {
// create copy to not modify original underlying array
opts.Env = append([]string{gitTrace + "=true"}, opts.Env...)
}
cmd.SetDescription(
fmt.Sprintf(
"pushing %s to %s (Force: %t, ForceWithLease: %s)",
opts.Branch,
opts.Remote,
opts.Force,
opts.ForceWithLease,
),
)
var outbuf, errbuf strings.Builder var outbuf, errbuf strings.Builder
err := cmd.Run(&gitea.RunOpts{ err := cmd.Run(ctx,
Env: opts.Env, command.WithDir(repoPath),
Timeout: opts.Timeout, command.WithStdout(&outbuf),
Dir: repoPath, command.WithStderr(&errbuf),
Stdout: &outbuf, command.WithEnvs(opts.Env...),
Stderr: &errbuf, )
})
if a.traceGit { if g.traceGit {
log.Ctx(ctx).Trace(). log.Ctx(ctx).Trace().
Str("git", "push"). Str("git", "push").
Err(err). Err(err).
@ -340,13 +373,13 @@ func (a Adapter) Push(
if err != nil { if err != nil {
switch { switch {
case strings.Contains(errbuf.String(), "non-fast-forward"): case strings.Contains(errbuf.String(), "non-fast-forward"):
return &gitea.ErrPushOutOfDate{ return &PushOutOfDateError{
StdOut: outbuf.String(), StdOut: outbuf.String(),
StdErr: errbuf.String(), StdErr: errbuf.String(),
Err: err, Err: err,
} }
case strings.Contains(errbuf.String(), "! [remote rejected]"): case strings.Contains(errbuf.String(), "! [remote rejected]"):
err := &gitea.ErrPushRejected{ err := &PushRejectedError{
StdOut: outbuf.String(), StdOut: outbuf.String(),
StdErr: errbuf.String(), StdErr: errbuf.String(),
Err: err, Err: err,
@ -354,7 +387,7 @@ func (a Adapter) Push(
err.GenerateMessage() err.GenerateMessage()
return err return err
case strings.Contains(errbuf.String(), "matches more than one"): case strings.Contains(errbuf.String(), "matches more than one"):
err := &gitea.ErrMoreThanOne{ err := &MoreThanOneError{
StdOut: outbuf.String(), StdOut: outbuf.String(),
StdErr: errbuf.String(), StdErr: errbuf.String(),
Err: err, Err: err,
@ -371,31 +404,29 @@ func (a Adapter) Push(
err = fmt.Errorf("%w\ncmd error output: %s", err, errbuf.String()) err = fmt.Errorf("%w\ncmd error output: %s", err, errbuf.String())
} }
return processGiteaErrorf(err, "failed to push changes") return processGitErrorf(err, "failed to push changes")
} }
return nil return nil
} }
func (a Adapter) CountObjects(ctx context.Context, repoPath string) (types.ObjectCount, error) { func (g *Git) CountObjects(ctx context.Context, repoPath string) (ObjectCount, error) {
cmd := gitea.NewCommand(ctx,
"count-objects", "-v",
)
var outbuf strings.Builder var outbuf strings.Builder
if err := cmd.Run(&gitea.RunOpts{ cmd := command.New("count-objects", command.WithFlag("-v"))
Dir: repoPath, err := cmd.Run(ctx,
Stdout: &outbuf, command.WithDir(repoPath),
}); err != nil { command.WithStdout(&outbuf),
return types.ObjectCount{}, fmt.Errorf("error running git count-objects: %w", err) )
if err != nil {
return ObjectCount{}, fmt.Errorf("error running git count-objects: %w", err)
} }
objectCount := parseGitCountObjectsOutput(ctx, outbuf.String()) objectCount := parseGitCountObjectsOutput(ctx, outbuf.String())
return objectCount, nil return objectCount, nil
} }
func parseGitCountObjectsOutput(ctx context.Context, output string) types.ObjectCount { func parseGitCountObjectsOutput(ctx context.Context, output string) ObjectCount {
info := types.ObjectCount{} info := ObjectCount{}
output = strings.TrimSpace(output) output = strings.TrimSpace(output)
lines := strings.Split(output, "\n") lines := strings.Split(output, "\n")

View File

@ -12,32 +12,31 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"strings" "strings"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command"
gitea "code.gitea.io/gitea/modules/git" "github.com/harness/gitness/git/sha"
) )
func (a Adapter) ResolveRev(ctx context.Context, func (g *Git) ResolveRev(ctx context.Context,
repoPath string, repoPath string,
rev string, rev string,
) (string, error) { ) (sha.SHA, error) {
args := []string{"rev-parse", rev} cmd := command.New("rev-parse", command.WithArg(rev))
commitSHA, stdErr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath}) output := &bytes.Buffer{}
if strings.Contains(stdErr, "ambiguous argument") { err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
return "", errors.InvalidArgument("could not resolve git revision: %s", rev)
}
if err != nil { if err != nil {
return "", fmt.Errorf("failed to resolve git revision: %w", err) if strings.Contains(err.Error(), "ambiguous argument") {
return sha.None, errors.InvalidArgument("could not resolve git revision: %s", rev)
}
return sha.None, fmt.Errorf("failed to resolve git revision: %w", err)
} }
return sha.New(output.String())
commitSHA = strings.TrimSpace(commitSHA)
return commitSHA, nil
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes" "bytes"
@ -29,25 +29,24 @@ import (
"time" "time"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/sha"
"github.com/harness/gitness/git/tempdir" "github.com/harness/gitness/git/tempdir"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// SharedRepo is a type to wrap our upload repositories as a shallow clone. // SharedRepo is a type to wrap our upload repositories as a shallow clone.
type SharedRepo struct { type SharedRepo struct {
adapter Adapter git *Git
repoUID string repoUID string
repo *gitea.Repository
remoteRepoPath string remoteRepoPath string
tmpPath string RepoPath string
} }
// NewSharedRepo creates a new temporary upload repository. // NewSharedRepo creates a new temporary upload repository.
func NewSharedRepo( func NewSharedRepo(
adapter Adapter, adapter *Git,
baseTmpDir string, baseTmpDir string,
repoUID string, repoUID string,
remoteRepoPath string, remoteRepoPath string,
@ -58,27 +57,18 @@ func NewSharedRepo(
} }
t := &SharedRepo{ t := &SharedRepo{
adapter: adapter, git: adapter,
repoUID: repoUID, repoUID: repoUID,
remoteRepoPath: remoteRepoPath, remoteRepoPath: remoteRepoPath,
tmpPath: tmpPath, RepoPath: tmpPath,
} }
return t, nil return t, nil
} }
func (r *SharedRepo) Path() string {
return r.repo.Path
}
func (r *SharedRepo) RemotePath() string {
return r.remoteRepoPath
}
// Close the repository cleaning up all files. // Close the repository cleaning up all files.
func (r *SharedRepo) Close(ctx context.Context) { func (r *SharedRepo) Close(ctx context.Context) {
defer r.repo.Close() if err := tempdir.RemoveTemporaryPath(r.RepoPath); err != nil {
if err := tempdir.RemoveTemporaryPath(r.tmpPath); err != nil { log.Ctx(ctx).Err(err).Msgf("Failed to remove temporary path %s", r.RepoPath)
log.Ctx(ctx).Err(err).Msgf("Failed to remove temporary path %s", r.tmpPath)
} }
} }
@ -108,7 +98,7 @@ type fileEntry struct {
} }
func (r *SharedRepo) MoveObjects(ctx context.Context) error { func (r *SharedRepo) MoveObjects(ctx context.Context) error {
srcDir := path.Join(r.tmpPath, "objects") srcDir := path.Join(r.RepoPath, "objects")
dstDir := path.Join(r.remoteRepoPath, "objects") dstDir := path.Join(r.remoteRepoPath, "objects")
var files []fileEntry var files []fileEntry
@ -209,15 +199,13 @@ func (r *SharedRepo) MoveObjects(ctx context.Context) error {
} }
func (r *SharedRepo) InitAsShared(ctx context.Context) error { func (r *SharedRepo) InitAsShared(ctx context.Context) error {
args := []string{"init", "--bare"} cmd := command.New("init", command.WithFlag("--bare"))
if _, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{ if err := cmd.Run(ctx, command.WithDir(r.RepoPath)); err != nil {
Dir: r.tmpPath, return errors.Internal(err, "error while creating empty repository")
}); err != nil {
return errors.Internal(err, "error while creating empty repository: %s", stderr)
} }
if err := func() error { if err := func() error {
alternates := filepath.Join(r.tmpPath, "objects", "info", "alternates") alternates := filepath.Join(r.RepoPath, "objects", "info", "alternates")
f, err := os.OpenFile(alternates, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) f, err := os.OpenFile(alternates, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil { if err != nil {
return fmt.Errorf("failed to open alternates file '%s': %w", alternates, err) return fmt.Errorf("failed to open alternates file '%s': %w", alternates, err)
@ -234,66 +222,57 @@ func (r *SharedRepo) InitAsShared(ctx context.Context) error {
return errors.Internal(err, "failed to create alternate in empty repository: %s", err.Error()) return errors.Internal(err, "failed to create alternate in empty repository: %s", err.Error())
} }
gitRepo, err := gitea.OpenRepository(ctx, r.tmpPath)
if err != nil {
return processGiteaErrorf(err, "failed to open repo")
}
r.repo = gitRepo
return nil return nil
} }
// Clone the base repository to our path and set branch as the HEAD. // Clone the base repository to our path and set branch as the HEAD.
func (r *SharedRepo) Clone(ctx context.Context, branchName string) error { func (r *SharedRepo) Clone(ctx context.Context, branchName string) error {
args := []string{"clone", "-s", "--bare"} cmd := command.New("clone",
command.WithFlag("-s"),
command.WithFlag("--bare"),
)
if branchName != "" { if branchName != "" {
args = append(args, "-b", strings.TrimPrefix(branchName, gitReferenceNamePrefixBranch)) cmd.Add(command.WithFlag("-b", strings.TrimPrefix(branchName, gitReferenceNamePrefixBranch)))
} }
args = append(args, r.remoteRepoPath, r.tmpPath) cmd.Add(command.WithArg(r.remoteRepoPath, r.RepoPath))
if _, _, err := gitea.NewCommand(ctx, args...).RunStdString(nil); err != nil { if err := cmd.Run(ctx); err != nil {
stderr := err.Error() cmderr := command.AsError(err)
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched { if cmderr.StdErr == nil {
return errors.Internal(err, "error while cloning repository")
}
stderr := string(cmderr.StdErr)
matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr)
if matched {
return errors.NotFound("branch '%s' does not exist", branchName) return errors.NotFound("branch '%s' does not exist", branchName)
} else if matched, _ = regexp.MatchString(".* repository .* does not exist.*", stderr); matched { }
matched, _ = regexp.MatchString(".* repository .* does not exist.*", stderr)
if matched {
return errors.NotFound("repository '%s' does not exist", r.repoUID) return errors.NotFound("repository '%s' does not exist", r.repoUID)
} }
return errors.Internal(nil, "error while cloning repository: %s", stderr)
} }
gitRepo, err := gitea.OpenRepository(ctx, r.tmpPath)
if err != nil {
return processGiteaErrorf(err, "failed to open repo")
}
r.repo = gitRepo
return nil return nil
} }
// Init the repository. // Init the repository.
func (r *SharedRepo) Init(ctx context.Context) error { func (r *SharedRepo) Init(ctx context.Context) error {
if err := gitea.InitRepository(ctx, r.tmpPath, false); err != nil { err := r.git.InitRepository(ctx, r.RepoPath, false)
return err
}
gitRepo, err := gitea.OpenRepository(ctx, r.tmpPath)
if err != nil { if err != nil {
return processGiteaErrorf(err, "failed to open repo") return fmt.Errorf("failed to initialize shared repo: %w", err)
} }
r.repo = gitRepo
return nil return nil
} }
// SetDefaultIndex sets the git index to our HEAD. // SetDefaultIndex sets the git index to our HEAD.
func (r *SharedRepo) SetDefaultIndex(ctx context.Context) error { func (r *SharedRepo) SetDefaultIndex(ctx context.Context) error {
if _, _, err := gitea.NewCommand(ctx, "read-tree", "HEAD").RunStdString(&gitea.RunOpts{Dir: r.tmpPath}); err != nil { return r.SetIndex(ctx, "HEAD")
return fmt.Errorf("failed to git read-tree HEAD: %w", err)
}
return nil
} }
// SetIndex sets the git index to the provided treeish. // SetIndex sets the git index to the provided treeish.
func (r *SharedRepo) SetIndex(ctx context.Context, treeish string) error { func (r *SharedRepo) SetIndex(ctx context.Context, rev string) error {
if _, _, err := gitea.NewCommand(ctx, "read-tree", treeish).RunStdString(&gitea.RunOpts{Dir: r.tmpPath}); err != nil { cmd := command.New("read-tree", command.WithArg(rev))
return fmt.Errorf("failed to git read-tree %s: %w", treeish, err) if err := cmd.Run(ctx, command.WithDir(r.RepoPath)); err != nil {
return fmt.Errorf("failed to git read-tree %s: %w", rev, err)
} }
return nil return nil
} }
@ -303,33 +282,32 @@ func (r *SharedRepo) LsFiles(
ctx context.Context, ctx context.Context,
filenames ...string, filenames ...string,
) ([]string, error) { ) ([]string, error) {
stdOut := new(bytes.Buffer) cmd := command.New("ls-files",
stdErr := new(bytes.Buffer) command.WithFlag("-z"),
)
cmdArgs := []string{"ls-files", "-z", "--"}
for _, arg := range filenames { for _, arg := range filenames {
if arg != "" { if arg != "" {
cmdArgs = append(cmdArgs, arg) cmd.Add(command.WithPostSepArg(arg))
} }
} }
if err := gitea.NewCommand(ctx, cmdArgs...). stdout := bytes.NewBuffer(nil)
Run(&gitea.RunOpts{
Dir: r.tmpPath, err := cmd.Run(ctx,
Stdout: stdOut, command.WithDir(r.RepoPath),
Stderr: stdErr, command.WithStdout(stdout),
}); err != nil { )
return nil, fmt.Errorf("unable to run git ls-files for temporary repo of: "+ if err != nil {
"%s Error: %w\nstdout: %s\nstderr: %s", return nil, fmt.Errorf("failed to list files in shared repository's git index: %w", err)
r.repoUID, err, stdOut.String(), stdErr.String())
} }
filelist := make([]string, 0) files := make([]string, 0)
for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) { for _, line := range bytes.Split(stdout.Bytes(), []byte{'\000'}) {
filelist = append(filelist, string(line)) files = append(files, string(line))
} }
return filelist, nil return files, nil
} }
// RemoveFilesFromIndex removes the given files from the index. // RemoveFilesFromIndex removes the given files from the index.
@ -338,7 +316,6 @@ func (r *SharedRepo) RemoveFilesFromIndex(
filenames ...string, filenames ...string,
) error { ) error {
stdOut := new(bytes.Buffer) stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)
stdIn := new(bytes.Buffer) stdIn := new(bytes.Buffer)
for _, file := range filenames { for _, file := range filenames {
if file != "" { if file != "" {
@ -348,15 +325,19 @@ func (r *SharedRepo) RemoveFilesFromIndex(
} }
} }
if err := gitea.NewCommand(ctx, "update-index", "--remove", "-z", "--index-info"). cmd := command.New("update-index",
Run(&gitea.RunOpts{ command.WithFlag("--remove"),
Dir: r.tmpPath, command.WithFlag("-z"),
Stdin: stdIn, command.WithFlag("--index-info"),
Stdout: stdOut, )
Stderr: stdErr,
}); err != nil { if err := cmd.Run(ctx,
return fmt.Errorf("unable to update-index for temporary repo: %s Error: %w\nstdout: %s\nstderr: %s", command.WithDir(r.RepoPath),
r.repoUID, err, stdOut.String(), stdErr.String()) command.WithStdin(stdIn),
command.WithStdout(stdOut),
); err != nil {
return fmt.Errorf("unable to update-index for temporary repo: %s Error: %w\nstdout: %s",
r.repoUID, err, stdOut.String())
} }
return nil return nil
} }
@ -365,22 +346,22 @@ func (r *SharedRepo) RemoveFilesFromIndex(
func (r *SharedRepo) WriteGitObject( func (r *SharedRepo) WriteGitObject(
ctx context.Context, ctx context.Context,
content io.Reader, content io.Reader,
) (string, error) { ) (sha.SHA, error) {
stdOut := new(bytes.Buffer) stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer) cmd := command.New("hash-object",
command.WithFlag("-w"),
if err := gitea.NewCommand(ctx, "hash-object", "-w", "--stdin"). command.WithFlag("--stdin"),
Run(&gitea.RunOpts{ )
Dir: r.tmpPath, if err := cmd.Run(ctx,
Stdin: content, command.WithDir(r.RepoPath),
Stdout: stdOut, command.WithStdin(content),
Stderr: stdErr, command.WithStdout(stdOut),
}); err != nil { ); err != nil {
return "", fmt.Errorf("unable to hash-object to temporary repo: %s Error: %w\nstdout: %s\nstderr: %s", return sha.None, fmt.Errorf("unable to hash-object to temporary repo: %s Error: %w\nstdout: %s",
r.repoUID, err, stdOut.String(), stdErr.String()) r.repoUID, err, stdOut.String())
} }
return strings.TrimSpace(stdOut.String()), nil return sha.New(stdOut.String())
} }
// ShowFile dumps show file and write to io.Writer. // ShowFile dumps show file and write to io.Writer.
@ -390,15 +371,15 @@ func (r *SharedRepo) ShowFile(
commitHash string, commitHash string,
writer io.Writer, writer io.Writer,
) error { ) error {
stderr := new(bytes.Buffer)
file := strings.TrimSpace(commitHash) + ":" + strings.TrimSpace(filePath) file := strings.TrimSpace(commitHash) + ":" + strings.TrimSpace(filePath)
cmd := gitea.NewCommand(ctx, "show", file) cmd := command.New("show",
if err := cmd.Run(&gitea.RunOpts{ command.WithArg(file),
Dir: r.repo.Path, )
Stdout: writer, if err := cmd.Run(ctx,
Stderr: stderr, command.WithDir(r.RepoPath),
}); err != nil { command.WithStdout(writer),
return fmt.Errorf("show file: %w - %s", err, stderr) ); err != nil {
return fmt.Errorf("show file: %w", err)
} }
return nil return nil
} }
@ -410,8 +391,13 @@ func (r *SharedRepo) AddObjectToIndex(
objectHash string, objectHash string,
objectPath string, objectPath string,
) error { ) error {
if _, _, err := gitea.NewCommand(ctx, "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, cmd := command.New("update-index",
objectPath).RunStdString(&gitea.RunOpts{Dir: r.tmpPath}); err != nil { command.WithFlag("--add"),
command.WithFlag("--replace"),
command.WithFlag("--cacheinfo"),
command.WithArg(mode, objectHash, objectPath),
)
if err := cmd.Run(ctx, command.WithDir(r.RepoPath)); err != nil {
if matched, _ := regexp.MatchString(".*Invalid path '.*", err.Error()); matched { if matched, _ := regexp.MatchString(".*Invalid path '.*", err.Error()); matched {
return errors.InvalidArgument("invalid path '%s'", objectPath) return errors.InvalidArgument("invalid path '%s'", objectPath)
} }
@ -422,13 +408,18 @@ func (r *SharedRepo) AddObjectToIndex(
} }
// WriteTree writes the current index as a tree to the object db and returns its hash. // WriteTree writes the current index as a tree to the object db and returns its hash.
func (r *SharedRepo) WriteTree(ctx context.Context) (string, error) { func (r *SharedRepo) WriteTree(ctx context.Context) (sha.SHA, error) {
stdout, _, err := gitea.NewCommand(ctx, "write-tree").RunStdString(&gitea.RunOpts{Dir: r.tmpPath}) stdout := &bytes.Buffer{}
cmd := command.New("write-tree")
err := cmd.Run(ctx,
command.WithDir(r.RepoPath),
command.WithStdout(stdout),
)
if err != nil { if err != nil {
return "", fmt.Errorf("unable to write-tree in temporary repo path for: %s Error: %w", return sha.None, fmt.Errorf("unable to write-tree in temporary repo path for: %s Error: %w",
r.repoUID, err) r.repoUID, err)
} }
return strings.TrimSpace(stdout), nil return sha.New(stdout.String())
} }
// GetLastCommit gets the last commit ID SHA of the repo. // GetLastCommit gets the last commit ID SHA of the repo.
@ -444,73 +435,79 @@ func (r *SharedRepo) GetLastCommitByRef(
if ref == "" { if ref == "" {
ref = "HEAD" ref = "HEAD"
} }
stdout, _, err := gitea.NewCommand(ctx, "rev-parse", ref).RunStdString(&gitea.RunOpts{Dir: r.tmpPath}) stdout := &bytes.Buffer{}
cmd := command.New("rev-parse",
command.WithArg(ref),
)
err := cmd.Run(ctx,
command.WithDir(r.RepoPath),
command.WithStdout(stdout),
)
if err != nil { if err != nil {
return "", processGiteaErrorf(err, "unable to rev-parse %s in temporary repo for: %s", return "", processGitErrorf(err, "unable to rev-parse %s in temporary repo for: %s",
ref, r.repoUID) ref, r.repoUID)
} }
return strings.TrimSpace(stdout), nil return strings.TrimSpace(stdout.String()), nil
} }
// CommitTreeWithDate creates a commit from a given tree for the user with provided message. // CommitTreeWithDate creates a commit from a given tree for the user with provided message.
func (r *SharedRepo) CommitTreeWithDate( func (r *SharedRepo) CommitTreeWithDate(
ctx context.Context, ctx context.Context,
parent string, parent sha.SHA,
author, committer *types.Identity, author, committer *Identity,
treeHash, message string, treeHash sha.SHA,
message string,
signoff bool, signoff bool,
authorDate, committerDate time.Time, authorDate, committerDate time.Time,
) (string, error) { ) (sha.SHA, error) {
// setup environment variables used by git-commit-tree
// See https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables
env := []string{
"GIT_AUTHOR_NAME=" + author.Name,
"GIT_AUTHOR_EMAIL=" + author.Email,
"GIT_AUTHOR_DATE=" + authorDate.Format(time.RFC3339),
"GIT_COMMITTER_NAME=" + committer.Name,
"GIT_COMMITTER_EMAIL=" + committer.Email,
"GIT_COMMITTER_DATE=" + committerDate.Format(time.RFC3339),
}
messageBytes := new(bytes.Buffer) messageBytes := new(bytes.Buffer)
_, _ = messageBytes.WriteString(message) _, _ = messageBytes.WriteString(message)
_, _ = messageBytes.WriteString("\n") _, _ = messageBytes.WriteString("\n")
var args []string cmd := command.New("commit-tree",
if parent != "" { command.WithAuthorAndDate(
args = []string{"commit-tree", treeHash, "-p", parent} author.Name,
} else { author.Email,
args = []string{"commit-tree", treeHash} authorDate,
),
command.WithCommitterAndDate(
committer.Name,
committer.Email,
committerDate,
),
)
if !parent.IsEmpty() {
cmd.Add(command.WithFlag("-p", parent.String()))
} }
cmd.Add(command.WithArg(treeHash.String()))
// temporary no signing // temporary no signing
args = append(args, "--no-gpg-sign") cmd.Add(command.WithFlag("--no-gpg-sign"))
if signoff { if signoff {
giteaSignature := &gitea.Signature{ sig := &Signature{
Name: committer.Name, Identity: Identity{
Email: committer.Email, Name: committer.Name,
When: committerDate, Email: committer.Email,
},
When: committerDate,
} }
// Signed-off-by // Signed-off-by
_, _ = messageBytes.WriteString("\n") _, _ = messageBytes.WriteString("\n")
_, _ = messageBytes.WriteString("Signed-off-by: ") _, _ = messageBytes.WriteString("Signed-off-by: ")
_, _ = messageBytes.WriteString(giteaSignature.String()) _, _ = messageBytes.WriteString(sig.String())
} }
stdout := new(bytes.Buffer) stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer) if err := cmd.Run(ctx,
if err := gitea.NewCommand(ctx, args...). command.WithDir(r.RepoPath),
Run(&gitea.RunOpts{ command.WithStdin(messageBytes),
Env: env, command.WithStdout(stdout),
Dir: r.tmpPath, ); err != nil {
Stdin: messageBytes, return sha.None, processGitErrorf(err, "unable to commit-tree in temporary repo: %s Error: %v\nStdout: %s",
Stdout: stdout, r.repoUID, err, stdout)
Stderr: stderr,
}); err != nil {
return "", processGiteaErrorf(err, "unable to commit-tree in temporary repo: %s Error: %v\nStdout: %s\nStderr: %s",
r.repoUID, err, stdout, stderr)
} }
return strings.TrimSpace(stdout.String()), nil return sha.New(stdout.String())
} }
func (r *SharedRepo) PushDeleteBranch( func (r *SharedRepo) PushDeleteBranch(
@ -580,7 +577,7 @@ func (r *SharedRepo) push(
env ...string, env ...string,
) error { ) error {
// Because calls hooks we need to pass in the environment // Because calls hooks we need to pass in the environment
if err := r.adapter.Push(ctx, r.tmpPath, types.PushOptions{ if err := r.git.Push(ctx, r.RepoPath, PushOptions{
Remote: r.remoteRepoPath, Remote: r.remoteRepoPath,
Branch: sourceRef + ":" + destinationRef, Branch: sourceRef + ":" + destinationRef,
Env: env, Env: env,
@ -592,29 +589,19 @@ func (r *SharedRepo) push(
return nil return nil
} }
// GetBranchCommit Gets the commit object of the given branch.
func (r *SharedRepo) GetBranchCommit(branch string) (*gitea.Commit, error) {
if r.repo == nil {
return nil, fmt.Errorf("repository has not been cloned")
}
return r.repo.GetBranchCommit(strings.TrimPrefix(branch, gitReferenceNamePrefixBranch))
}
// GetBranch gets the branch object of the given ref. // GetBranch gets the branch object of the given ref.
func (r *SharedRepo) GetBranch(rev string) (*gitea.Branch, error) { func (r *SharedRepo) GetBranch(ctx context.Context, rev string) (*Branch, error) {
if r.repo == nil { return r.git.GetBranch(ctx, r.RepoPath, rev)
return nil, fmt.Errorf("repository has not been cloned")
}
return r.repo.GetBranch(rev)
} }
// GetCommit Gets the commit object of the given commit ID. // GetCommit Gets the commit object of the given commit ID.
func (r *SharedRepo) GetCommit(commitID string) (*gitea.Commit, error) { func (r *SharedRepo) GetCommit(ctx context.Context, commitID string) (*Commit, error) {
if r.repo == nil { return r.git.GetCommit(ctx, r.RepoPath, commitID)
return nil, fmt.Errorf("repository has not been cloned") }
}
return r.repo.GetCommit(commitID) // GetTreeNode Gets the tree node object of the given commit ID and path.
func (r *SharedRepo) GetTreeNode(ctx context.Context, commitID, treePath string) (*TreeNode, error) {
return r.git.GetTreeNode(ctx, r.RepoPath, commitID, treePath)
} }
// GetReferenceFromBranchName assumes the provided value is the branch name (not the ref!) // GetReferenceFromBranchName assumes the provided value is the branch name (not the ref!)
@ -643,12 +630,3 @@ func GetReferenceFromTagName(tagName string) string {
// return reference // return reference
return gitReferenceNamePrefixTag + tagName return gitReferenceNamePrefixTag + tagName
} }
// SharedRepository creates new instance of SharedRepo.
func (a Adapter) SharedRepository(
tmpDir string,
repoUID string,
remotePath string,
) (*SharedRepo, error) {
return NewSharedRepo(a, tmpDir, repoUID, remotePath)
}

63
git/api/signature.go Normal file
View File

@ -0,0 +1,63 @@
// 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 api
import (
"fmt"
"time"
"github.com/harness/gitness/errors"
)
// Signature represents the Author or Committer information.
type Signature struct {
Identity Identity
// When is the timestamp of the Signature.
When time.Time
}
// DecodeSignature decodes a byte array representing a signature to signature.
func DecodeSignature(b []byte) (Signature, error) {
sig, err := NewSignatureFromCommitLine(b)
if err != nil {
return Signature{}, fmt.Errorf("failed to read signature from commit: %w", err)
}
return sig, nil
}
func (s Signature) String() string {
return fmt.Sprintf("%s <%s>", s.Identity.Name, s.Identity.Email)
}
type Identity struct {
Name string
Email string
}
func (i Identity) String() string {
return fmt.Sprintf("%s <%s>", i.Name, i.Email)
}
func (i Identity) Validate() error {
if i.Name == "" {
return errors.InvalidArgument("identity name is mandatory")
}
if i.Email == "" {
return errors.InvalidArgument("identity email is mandatory")
}
return nil
}

91
git/api/submodule.go Normal file
View File

@ -0,0 +1,91 @@
// 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 api
import (
"bufio"
"context"
"strings"
)
type Submodule struct {
Name string
URL string
}
// GetSubmodule returns the submodule at the given path reachable from ref.
// Note: ref can be Branch / Tag / CommitSHA.
func (g *Git) GetSubmodule(
ctx context.Context,
repoPath string,
ref string,
treePath string,
) (*Submodule, error) {
if repoPath == "" {
return nil, ErrRepositoryPathEmpty
}
treePath = cleanTreePath(treePath)
// Get the commit object for the ref
commit, err := g.GetFullCommitID(ctx, repoPath, ref)
if err != nil {
return nil, processGitErrorf(err, "error getting commit for ref '%s'", ref)
}
node, err := g.GetTreeNode(ctx, repoPath, commit.String(), ".gitmodules")
if err != nil {
return nil, processGitErrorf(err, "error reading tree node for ref '%s' with commit '%s'",
ref, commit)
}
reader, err := g.GetBlob(ctx, repoPath, node.SHA, 0)
if err != nil {
return nil, processGitErrorf(err, "error reading commit for ref '%s'", ref)
}
defer reader.Content.Close()
modules, err := GetSubModules(reader)
if err != nil {
return nil, processGitErrorf(err, "error getting submodule '%s' from commit", treePath)
}
return modules[treePath], nil
}
// GetSubModules get all the sub modules of current revision git tree.
func GetSubModules(rd *BlobReader) (map[string]*Submodule, error) {
var isModule bool
var path string
submodules := make(map[string]*Submodule, 4)
scanner := bufio.NewScanner(rd.Content)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "[submodule") {
isModule = true
continue
}
if isModule {
fields := strings.Split(scanner.Text(), "=")
k := strings.TrimSpace(fields[0])
if k == "path" {
path = strings.TrimSpace(fields[1])
} else if k == "url" {
submodules[path] = &Submodule{path, strings.TrimSpace(fields[1])}
isModule = false
}
}
}
return submodules, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bytes" "bytes"
@ -25,7 +25,7 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
) )
const ( const (
@ -33,42 +33,65 @@ const (
pgpSignatureEndToken = "\n-----END PGP SIGNATURE-----" //#nosec G101 pgpSignatureEndToken = "\n-----END PGP SIGNATURE-----" //#nosec G101
) )
type Tag struct {
Sha sha.SHA
Name string
TargetSha sha.SHA
TargetType GitObjectType
Title string
Message string
Tagger Signature
Signature *CommitGPGSignature
}
type CreateTagOptions struct {
// Message is the optional message the tag will be created with - if the message is empty
// the tag will be lightweight, otherwise it'll be annotated.
Message string
// Tagger is the information used in case the tag is annotated (Message is provided).
Tagger Signature
}
// TagPrefix tags prefix path on the repository.
const TagPrefix = "refs/tags/"
// GetAnnotatedTag returns the tag for a specific tag sha. // GetAnnotatedTag returns the tag for a specific tag sha.
func (a Adapter) GetAnnotatedTag( func (g *Git) GetAnnotatedTag(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
sha string, rev string,
) (*types.Tag, error) { ) (*Tag, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
tags, err := getAnnotatedTags(ctx, repoPath, []string{sha}) tags, err := getAnnotatedTags(ctx, repoPath, []string{rev})
if err != nil || len(tags) == 0 { if err != nil || len(tags) == 0 {
return nil, processGiteaErrorf(err, "failed to get annotated tag with sha '%s'", sha) return nil, processGitErrorf(err, "failed to get annotated tag with sha '%s'", rev)
} }
return &tags[0], nil return &tags[0], nil
} }
// GetAnnotatedTags returns the tags for a specific list of tag sha. // GetAnnotatedTags returns the tags for a specific list of tag sha.
func (a Adapter) GetAnnotatedTags( func (g *Git) GetAnnotatedTags(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
shas []string, revs []string,
) ([]types.Tag, error) { ) ([]Tag, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
return getAnnotatedTags(ctx, repoPath, shas) return getAnnotatedTags(ctx, repoPath, revs)
} }
// CreateTag creates the tag pointing at the provided SHA (could be any type, e.g. commit, tag, blob, ...) // CreateTag creates the tag pointing at the provided SHA (could be any type, e.g. commit, tag, blob, ...)
func (a Adapter) CreateTag( func (g *Git) CreateTag(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
name string, name string,
targetSHA string, targetSHA sha.SHA,
opts *types.CreateTagOptions, opts *CreateTagOptions,
) error { ) error {
if repoPath == "" { if repoPath == "" {
return ErrRepositoryPathEmpty return ErrRepositoryPathEmpty
@ -85,21 +108,20 @@ func (a Adapter) CreateTag(
) )
} }
cmd.Add(command.WithArg(name, targetSHA)) cmd.Add(command.WithArg(name, targetSHA.String()))
err := cmd.Run(ctx, command.WithDir(repoPath)) err := cmd.Run(ctx, command.WithDir(repoPath))
if err != nil { if err != nil {
return processGiteaErrorf(err, "Service failed to create a tag") return processGitErrorf(err, "Service failed to create a tag")
} }
return nil return nil
} }
// getAnnotatedTag is a custom implementation to retrieve an annotated tag from a sha. // getAnnotatedTag is a custom implementation to retrieve an annotated tag from a sha.
// The code is following parts of the gitea implementation.
func getAnnotatedTags( func getAnnotatedTags(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
shas []string, revs []string,
) ([]types.Tag, error) { ) ([]Tag, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
@ -110,25 +132,27 @@ func getAnnotatedTags(
_ = writer.Close() _ = writer.Close()
}() }()
tags := make([]types.Tag, len(shas)) tags := make([]Tag, len(revs))
for i, sha := range shas { for i, rev := range revs {
if _, err := writer.Write([]byte(sha + "\n")); err != nil { line := rev + "\n"
if _, err := writer.Write([]byte(line)); err != nil {
return nil, err return nil, err
} }
tagSha, typ, size, err := ReadBatchHeaderLine(reader) output, err := ReadBatchHeaderLine(reader)
if err != nil { if err != nil {
if errors.Is(err, io.EOF) || errors.IsNotFound(err) { if errors.Is(err, io.EOF) || errors.IsNotFound(err) {
return nil, fmt.Errorf("tag with sha %s does not exist", sha) return nil, fmt.Errorf("tag with sha %s does not exist", rev)
} }
return nil, err return nil, err
} }
if typ != string(types.GitObjectTypeTag) { if output.Type != string(GitObjectTypeTag) {
return nil, fmt.Errorf("git object is of type '%s', expected tag", typ) return nil, fmt.Errorf("git object is of type '%s', expected tag",
output.Type)
} }
// read the remaining rawData // read the remaining rawData
rawData, err := io.ReadAll(io.LimitReader(reader, size)) rawData, err := io.ReadAll(io.LimitReader(reader, output.Size))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -139,11 +163,11 @@ func getAnnotatedTags(
tag, err := parseTagDataFromCatFile(rawData) tag, err := parseTagDataFromCatFile(rawData)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse tag '%s': %w", sha, err) return nil, fmt.Errorf("failed to parse tag '%s': %w", rev, err)
} }
// fill in the sha // fill in the sha
tag.Sha = string(tagSha) tag.Sha = output.SHA
tags[i] = tag tags[i] = tag
} }
@ -152,14 +176,13 @@ func getAnnotatedTags(
} }
// parseTagDataFromCatFile parses a tag from a cat-file output. // parseTagDataFromCatFile parses a tag from a cat-file output.
func parseTagDataFromCatFile(data []byte) (tag types.Tag, err error) { func parseTagDataFromCatFile(data []byte) (tag Tag, err error) {
p := 0
// parse object Id // parse object Id
tag.TargetSha, p, err = parseCatFileLine(data, p, "object") object, p, err := parseCatFileLine(data, 0, "object")
if err != nil { if err != nil {
return tag, err return tag, err
} }
tag.TargetSha = sha.Must(object)
// parse object type // parse object type
rawType, p, err := parseCatFileLine(data, p, "type") rawType, p, err := parseCatFileLine(data, p, "type")
@ -167,7 +190,7 @@ func parseTagDataFromCatFile(data []byte) (tag types.Tag, err error) {
return tag, err return tag, err
} }
tag.TargetType, err = types.ParseGitObjectType(rawType) tag.TargetType, err = ParseGitObjectType(rawType)
if err != nil { if err != nil {
return tag, err return tag, err
} }
@ -247,19 +270,18 @@ const defaultGitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
// parseSignatureFromCatFileLine parses the signature from a cat-file output. // parseSignatureFromCatFileLine parses the signature from a cat-file output.
// This is used for commit / tag outputs. Input will be similar to (without 'author 'prefix): // This is used for commit / tag outputs. Input will be similar to (without 'author 'prefix):
// - author Max Mustermann <mm@gitness.io> 1666401234 -0700 // - author Max Mustermann <mm@gitness.io> 1666401234 -0700
// - author Max Mustermann <mm@gitness.io> Tue Oct 18 05:13:26 2022 +0530 // - author Max Mustermann <mm@gitness.io> Tue Oct 18 05:13:26 2022 +0530.
// TODO: method is leaning on gitea code - requires reference? func parseSignatureFromCatFileLine(line string) (Signature, error) {
func parseSignatureFromCatFileLine(line string) (types.Signature, error) { sig := Signature{}
sig := types.Signature{}
emailStart := strings.LastIndexByte(line, '<') emailStart := strings.LastIndexByte(line, '<')
emailEnd := strings.LastIndexByte(line, '>') emailEnd := strings.LastIndexByte(line, '>')
if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart { if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
return types.Signature{}, fmt.Errorf("signature is missing email ('%s')", line) return Signature{}, fmt.Errorf("signature is missing email ('%s')", line)
} }
// name requires that there is at least one char followed by a space (so emailStart >= 2) // name requires that there is at least one char followed by a space (so emailStart >= 2)
if emailStart < 2 { if emailStart < 2 {
return types.Signature{}, fmt.Errorf("signature is missing name ('%s')", line) return Signature{}, fmt.Errorf("signature is missing name ('%s')", line)
} }
sig.Identity.Name = line[:emailStart-1] sig.Identity.Name = line[:emailStart-1]
@ -267,7 +289,7 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
timeStart := emailEnd + 2 timeStart := emailEnd + 2
if timeStart >= len(line) { if timeStart >= len(line) {
return types.Signature{}, fmt.Errorf("signature is missing time ('%s')", line) return Signature{}, fmt.Errorf("signature is missing time ('%s')", line)
} }
// Check if time format is written date time format (e.g Thu, 07 Apr 2005 22:13:13 +0200) // Check if time format is written date time format (e.g Thu, 07 Apr 2005 22:13:13 +0200)
@ -276,7 +298,7 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
var err error var err error
sig.When, err = time.Parse(defaultGitTimeLayout, line[timeStart:]) sig.When, err = time.Parse(defaultGitTimeLayout, line[timeStart:])
if err != nil { if err != nil {
return types.Signature{}, fmt.Errorf("failed to time.parse signature time ('%s'): %w", line, err) return Signature{}, fmt.Errorf("failed to time.parse signature time ('%s'): %w", line, err)
} }
return sig, nil return sig, nil
@ -285,19 +307,19 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
// Otherwise we have to manually parse unix time and time zone // Otherwise we have to manually parse unix time and time zone
endOfUnixTime := timeStart + strings.IndexByte(line[timeStart:], ' ') endOfUnixTime := timeStart + strings.IndexByte(line[timeStart:], ' ')
if endOfUnixTime <= timeStart { if endOfUnixTime <= timeStart {
return types.Signature{}, fmt.Errorf("signature is missing unix time ('%s')", line) return Signature{}, fmt.Errorf("signature is missing unix time ('%s')", line)
} }
unixSeconds, err := strconv.ParseInt(line[timeStart:endOfUnixTime], 10, 64) unixSeconds, err := strconv.ParseInt(line[timeStart:endOfUnixTime], 10, 64)
if err != nil { if err != nil {
return types.Signature{}, fmt.Errorf("failed to parse unix time ('%s'): %w", line, err) return Signature{}, fmt.Errorf("failed to parse unix time ('%s'): %w", line, err)
} }
// parse time zone // parse time zone
startOfTimeZone := endOfUnixTime + 1 // +1 for space startOfTimeZone := endOfUnixTime + 1 // +1 for space
endOfTimeZone := startOfTimeZone + 5 // +5 for '+0700' endOfTimeZone := startOfTimeZone + 5 // +5 for '+0700'
if startOfTimeZone >= len(line) || endOfTimeZone > len(line) { if startOfTimeZone >= len(line) || endOfTimeZone > len(line) {
return types.Signature{}, fmt.Errorf("signature is missing time zone ('%s')", line) return Signature{}, fmt.Errorf("signature is missing time zone ('%s')", line)
} }
// get and disect timezone, e.g. '+0700' // get and disect timezone, e.g. '+0700'
@ -306,11 +328,11 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
rawTimeZoneMin := rawTimeZone[3:] // gets +07[00] rawTimeZoneMin := rawTimeZone[3:] // gets +07[00]
timeZoneH, err := strconv.ParseInt(rawTimeZoneH, 10, 64) timeZoneH, err := strconv.ParseInt(rawTimeZoneH, 10, 64)
if err != nil { if err != nil {
return types.Signature{}, fmt.Errorf("failed to parse hours of time zone ('%s'): %w", line, err) return Signature{}, fmt.Errorf("failed to parse hours of time zone ('%s'): %w", line, err)
} }
timeZoneMin, err := strconv.ParseInt(rawTimeZoneMin, 10, 64) timeZoneMin, err := strconv.ParseInt(rawTimeZoneMin, 10, 64)
if err != nil { if err != nil {
return types.Signature{}, fmt.Errorf("failed to parse minutes of time zone ('%s'): %w", line, err) return Signature{}, fmt.Errorf("failed to parse minutes of time zone ('%s'): %w", line, err)
} }
timeZoneOffsetInSec := int(timeZoneH*60+timeZoneMin) * 60 timeZoneOffsetInSec := int(timeZoneH*60+timeZoneMin) * 60
@ -324,3 +346,55 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
return sig, nil return sig, nil
} }
// Parse commit information from the (uncompressed) raw
// data from the commit object.
// \n\n separate headers from message.
func parseTagData(data []byte) (*Tag, error) {
tag := &Tag{
Tagger: Signature{},
}
// we now have the contents of the commit object. Let's investigate...
nextLine := 0
l:
for {
eol := bytes.IndexByte(data[nextLine:], '\n')
switch {
case eol > 0:
line := data[nextLine : nextLine+eol]
spacePos := bytes.IndexByte(line, ' ')
refType := line[:spacePos]
switch string(refType) {
case "object":
tag.TargetSha = sha.Must(string(line[spacePos+1:]))
case "type":
// A commit can have one or more parents
tag.TargetType = GitObjectType(line[spacePos+1:])
case "tagger":
sig, err := NewSignatureFromCommitLine(line[spacePos+1:])
if err != nil {
return nil, fmt.Errorf("failed to parse tagger signature: %w", err)
}
tag.Tagger = sig
}
nextLine += eol + 1
case eol == 0:
tag.Message = string(data[nextLine+1:])
break l
default:
break l
}
}
idx := strings.LastIndex(tag.Message, pgpSignatureBeginToken)
if idx > 0 {
endSigIdx := strings.Index(tag.Message[idx:], pgpSignatureEndToken)
if endSigIdx > 0 {
tag.Signature = &CommitGPGSignature{
Signature: tag.Message[idx+1 : idx+endSigIdx+len(pgpSignatureEndToken)],
Payload: string(data[:bytes.LastIndex(data, []byte(pgpSignatureBeginToken))+1]),
}
tag.Message = tag.Message[:idx+1]
}
}
return tag, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"bufio" "bufio"
@ -22,34 +22,99 @@ import (
"io" "io"
"path" "path"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/parser" "github.com/harness/gitness/git/parser"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type TreeNodeWithCommit struct {
TreeNode
Commit *Commit
}
type TreeNode struct {
NodeType TreeNodeType
Mode TreeNodeMode
SHA sha.SHA
Name string
Path string
}
func (n *TreeNode) IsExecutable() bool {
return n.Mode == TreeNodeModeExec
}
func (n *TreeNode) IsDir() bool {
return n.Mode == TreeNodeModeTree
}
func (n *TreeNode) IsLink() bool {
return n.Mode == TreeNodeModeSymlink
}
// TreeNodeType specifies the different types of nodes in a git tree.
// IMPORTANT: has to be consistent with rpc.TreeNodeType (proto).
type TreeNodeType int
const (
TreeNodeTypeTree TreeNodeType = iota
TreeNodeTypeBlob
TreeNodeTypeCommit
)
// TreeNodeMode specifies the different modes of a node in a git tree.
// IMPORTANT: has to be consistent with rpc.TreeNodeMode (proto).
type TreeNodeMode int
const (
TreeNodeModeFile TreeNodeMode = iota
TreeNodeModeSymlink
TreeNodeModeExec
TreeNodeModeTree
TreeNodeModeCommit
)
func (m TreeNodeMode) String() string {
var result int
switch m {
case TreeNodeModeFile:
result = 0o100644
case TreeNodeModeSymlink:
result = 0o120000
case TreeNodeModeExec:
result = 0o100755
case TreeNodeModeTree:
result = 0o040000
case TreeNodeModeCommit:
result = 0o160000
}
return strconv.FormatInt(int64(result), 8)
}
func cleanTreePath(treePath string) string { func cleanTreePath(treePath string) string {
return strings.Trim(path.Clean("/"+treePath), "/") return strings.Trim(path.Clean("/"+treePath), "/")
} }
func parseTreeNodeMode(s string) (types.TreeNodeType, types.TreeNodeMode, error) { func parseTreeNodeMode(s string) (TreeNodeType, TreeNodeMode, error) {
switch s { switch s {
case "100644": case "100644":
return types.TreeNodeTypeBlob, types.TreeNodeModeFile, nil return TreeNodeTypeBlob, TreeNodeModeFile, nil
case "120000": case "120000":
return types.TreeNodeTypeBlob, types.TreeNodeModeSymlink, nil return TreeNodeTypeBlob, TreeNodeModeSymlink, nil
case "100755": case "100755":
return types.TreeNodeTypeBlob, types.TreeNodeModeExec, nil return TreeNodeTypeBlob, TreeNodeModeExec, nil
case "160000": case "160000":
return types.TreeNodeTypeCommit, types.TreeNodeModeCommit, nil return TreeNodeTypeCommit, TreeNodeModeCommit, nil
case "040000": case "040000":
return types.TreeNodeTypeTree, types.TreeNodeModeTree, nil return TreeNodeTypeTree, TreeNodeModeTree, nil
default: default:
return types.TreeNodeTypeBlob, types.TreeNodeModeFile, return TreeNodeTypeBlob, TreeNodeModeFile,
fmt.Errorf("unknown git tree node mode: '%s'", s) fmt.Errorf("unknown git tree node mode: '%s'", s)
} }
} }
@ -64,7 +129,7 @@ func lsTree(
repoPath string, repoPath string,
rev string, rev string,
treePath string, treePath string,
) ([]types.TreeNode, error) { ) ([]TreeNode, error) {
if repoPath == "" { if repoPath == "" {
return nil, ErrRepositoryPathEmpty return nil, ErrRepositoryPathEmpty
} }
@ -86,12 +151,12 @@ func lsTree(
} }
if output.Len() == 0 { if output.Len() == 0 {
return nil, &types.PathNotFoundError{Path: treePath} return nil, errors.NotFound("path '%s' wasn't found in the repo", treePath)
} }
n := bytes.Count(output.Bytes(), []byte{'\x00'}) n := bytes.Count(output.Bytes(), []byte{'\x00'})
list := make([]types.TreeNode, 0, n) list := make([]TreeNode, 0, n)
scan := bufio.NewScanner(output) scan := bufio.NewScanner(output)
scan.Split(parser.ScanZeroSeparated) scan.Split(parser.ScanZeroSeparated)
for scan.Scan() { for scan.Scan() {
@ -114,14 +179,14 @@ func lsTree(
return nil, fmt.Errorf("failed to parse git node type and file mode: %w", err) return nil, fmt.Errorf("failed to parse git node type and file mode: %w", err)
} }
nodeSha := columns[3] nodeSha := sha.Must(columns[3])
nodePath := columns[4] nodePath := columns[4]
nodeName := path.Base(nodePath) nodeName := path.Base(nodePath)
list = append(list, types.TreeNode{ list = append(list, TreeNode{
NodeType: nodeType, NodeType: nodeType,
Mode: nodeMode, Mode: nodeMode,
Sha: nodeSha, SHA: nodeSha,
Name: nodeName, Name: nodeName,
Path: nodePath, Path: nodePath,
}) })
@ -136,7 +201,7 @@ func lsDirectory(
repoPath string, repoPath string,
rev string, rev string,
treePath string, treePath string,
) ([]types.TreeNode, error) { ) ([]TreeNode, error) {
treePath = path.Clean(treePath) treePath = path.Clean(treePath)
if treePath == "" { if treePath == "" {
treePath = "." treePath = "."
@ -153,22 +218,22 @@ func lsFile(
repoPath string, repoPath string,
rev string, rev string,
treePath string, treePath string,
) (types.TreeNode, error) { ) (TreeNode, error) {
treePath = cleanTreePath(treePath) treePath = cleanTreePath(treePath)
list, err := lsTree(ctx, repoPath, rev, treePath) list, err := lsTree(ctx, repoPath, rev, treePath)
if err != nil { if err != nil {
return types.TreeNode{}, fmt.Errorf("failed to ls file: %w", err) return TreeNode{}, fmt.Errorf("failed to ls file: %w", err)
} }
if len(list) != 1 { if len(list) != 1 {
return types.TreeNode{}, fmt.Errorf("ls file list contains more than one element, len=%d", len(list)) return TreeNode{}, fmt.Errorf("ls file list contains more than one element, len=%d", len(list))
} }
return list[0], nil return list[0], nil
} }
// GetTreeNode returns the tree node at the given path as found for the provided reference. // GetTreeNode returns the tree node at the given path as found for the provided reference.
func (a Adapter) GetTreeNode(ctx context.Context, repoPath, rev, treePath string) (*types.TreeNode, error) { func (g *Git) GetTreeNode(ctx context.Context, repoPath, rev, treePath string) (*TreeNode, error) {
// root path (empty path) is a special case // root path (empty path) is a special case
if treePath == "" { if treePath == "" {
if repoPath == "" { if repoPath == "" {
@ -177,7 +242,7 @@ func (a Adapter) GetTreeNode(ctx context.Context, repoPath, rev, treePath string
cmd := command.New("show", cmd := command.New("show",
command.WithFlag("--no-patch"), command.WithFlag("--no-patch"),
command.WithFlag("--format="+fmtTreeHash), command.WithFlag("--format="+fmtTreeHash),
command.WithArg(rev), command.WithArg(rev+"^{commit}"),
) )
output := &bytes.Buffer{} output := &bytes.Buffer{}
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output)) err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
@ -188,10 +253,10 @@ func (a Adapter) GetTreeNode(ctx context.Context, repoPath, rev, treePath string
return nil, fmt.Errorf("failed to get root tree node: %w", err) return nil, fmt.Errorf("failed to get root tree node: %w", err)
} }
return &types.TreeNode{ return &TreeNode{
NodeType: types.TreeNodeTypeTree, NodeType: TreeNodeTypeTree,
Mode: types.TreeNodeModeTree, Mode: TreeNodeModeTree,
Sha: strings.TrimSpace(output.String()), SHA: sha.Must(output.String()),
Name: "", Name: "",
Path: "", Path: "",
}, err }, err
@ -206,7 +271,7 @@ func (a Adapter) GetTreeNode(ctx context.Context, repoPath, rev, treePath string
} }
// ListTreeNodes lists the child nodes of a tree reachable from ref via the specified path. // ListTreeNodes lists the child nodes of a tree reachable from ref via the specified path.
func (a Adapter) ListTreeNodes(ctx context.Context, repoPath, rev, treePath string) ([]types.TreeNode, error) { func (g *Git) ListTreeNodes(ctx context.Context, repoPath, rev, treePath string) ([]TreeNode, error) {
list, err := lsDirectory(ctx, repoPath, rev, treePath) list, err := lsDirectory(ctx, repoPath, rev, treePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list tree nodes: %w", err) return nil, fmt.Errorf("failed to list tree nodes: %w", err)
@ -215,7 +280,7 @@ func (a Adapter) ListTreeNodes(ctx context.Context, repoPath, rev, treePath stri
return list, nil return list, nil
} }
func (a Adapter) ReadTree( func (g *Git) ReadTree(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
ref string, ref string,

139
git/api/util.go Normal file
View File

@ -0,0 +1,139 @@
// 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 api
import (
"bytes"
"fmt"
"strconv"
"time"
"unicode"
"github.com/yuin/goldmark/util"
)
const (
// GitTimeLayout is the (default) time layout used by git.
GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
userPlaceholder = "sanitized-credential"
)
var schemeSep = []byte("://")
func NewSignatureFromCommitLine(line []byte) (Signature, error) {
emailStart := bytes.LastIndexByte(line, '<')
emailEnd := bytes.LastIndexByte(line, '>')
if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
return Signature{}, ErrInvalidSignature
}
sig := Signature{
Identity: Identity{
Name: string(line[:emailStart-1]),
Email: string(line[emailStart+1 : emailEnd]),
},
}
dateStart := emailEnd + 2
hasTime := dateStart < len(line)
if !hasTime {
return sig, nil
}
// Check date format.
firstChar := line[dateStart]
//nolint:nestif
if firstChar >= 48 && firstChar <= 57 {
idx := bytes.IndexByte(line[dateStart:], ' ')
if idx < 0 {
return sig, nil
}
timestring := string(line[dateStart : dateStart+idx])
seconds, _ := strconv.ParseInt(timestring, 10, 64)
sig.When = time.Unix(seconds, 0)
idx += emailEnd + 3
if idx >= len(line) || idx+5 > len(line) {
return sig, nil
}
timezone := string(line[idx : idx+5])
tzhours, err := strconv.ParseInt(timezone[0:3], 10, 64)
if err != nil {
return Signature{}, fmt.Errorf("failed to parse tzhours: %w", err)
}
tzmins, err := strconv.ParseInt(timezone[3:], 10, 64)
if err != nil {
return Signature{}, fmt.Errorf("failed to parse tzmins: %w", err)
}
if tzhours < 0 {
tzmins *= -1
}
tz := time.FixedZone("", int(tzhours*60*60+tzmins*60))
sig.When = sig.When.In(tz)
} else {
t, err := time.Parse(GitTimeLayout, string(line[dateStart:]))
if err != nil {
return Signature{}, fmt.Errorf("failed to parse git time: %w", err)
}
sig.When = t
}
return sig, nil
}
// SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://")
// for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com"
func SanitizeCredentialURLs(s string) string {
bs := util.StringToReadOnlyBytes(s)
schemeSepPos := bytes.Index(bs, schemeSep)
if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 {
return s // fast return if there is no URL scheme or no userinfo
}
out := make([]byte, 0, len(bs)+len(userPlaceholder))
for schemeSepPos != -1 {
schemeSepPos += 3 // skip the "://"
sepAtPos := -1 // the possible '@' position: "https://foo@[^here]host"
sepEndPos := schemeSepPos // the possible end position: "The https://host[^here] in log for test"
sepLoop:
for ; sepEndPos < len(bs); sepEndPos++ {
c := bs[sepEndPos]
if ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') {
continue
}
switch c {
case '@':
sepAtPos = sepEndPos
case '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '%':
continue // due to RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars
default:
break sepLoop // if it is an invalid char for URL (eg: space, '/', and others), stop the loop
}
}
// if there is '@', and the string is like "s://u@h", then hide the "u" part
if sepAtPos != -1 && (schemeSepPos >= 4 && unicode.IsLetter(rune(bs[schemeSepPos-4]))) &&
sepAtPos-schemeSepPos > 0 && sepEndPos-sepAtPos > 0 {
out = append(out, bs[:schemeSepPos]...)
out = append(out, userPlaceholder...)
out = append(out, bs[sepAtPos:sepEndPos]...)
} else {
out = append(out, bs[:sepEndPos]...)
}
bs = bs[sepEndPos:]
schemeSepPos = bytes.Index(bs, schemeSep)
}
out = append(out, bs...)
return util.BytesToReadOnlyString(out)
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package adapter package api
import ( import (
"fmt" "fmt"
@ -33,7 +33,7 @@ var WireSet = wire.NewSet(
func ProvideLastCommitCache( func ProvideLastCommitCache(
config types.Config, config types.Config,
redisClient redis.UniversalClient, redisClient redis.UniversalClient,
) (cache.Cache[CommitEntryKey, *types.Commit], error) { ) (cache.Cache[CommitEntryKey, *Commit], error) {
cacheDuration := config.LastCommitCache.Duration cacheDuration := config.LastCommitCache.Duration
// no need to cache if it's too short // no need to cache if it's too short

View File

@ -88,7 +88,7 @@ func (s *Service) Blame(ctx context.Context, params *BlameParams) (<-chan *Blame
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
reader := s.adapter.Blame(ctx, reader := s.git.Blame(ctx,
repoPath, params.GitRef, params.Path, repoPath, params.GitRef, params.Path,
params.LineFrom, params.LineTo) params.LineFrom, params.LineTo)

View File

@ -17,6 +17,8 @@ package git
import ( import (
"context" "context"
"io" "io"
"github.com/harness/gitness/git/sha"
) )
type GetBlobParams struct { type GetBlobParams struct {
@ -26,7 +28,7 @@ type GetBlobParams struct {
} }
type GetBlobOutput struct { type GetBlobOutput struct {
SHA string SHA sha.SHA
// Size is the actual size of the blob. // Size is the actual size of the blob.
Size int64 Size int64
// ContentSize is the total number of bytes returned by the Content Reader. // ContentSize is the total number of bytes returned by the Content Reader.
@ -43,7 +45,7 @@ func (s *Service) GetBlob(ctx context.Context, params *GetBlobParams) (*GetBlobO
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
// TODO: do we need to validate request for nil? // TODO: do we need to validate request for nil?
reader, err := s.adapter.GetBlob(ctx, repoPath, params.SHA, params.SizeLimit) reader, err := s.git.GetBlob(ctx, repoPath, sha.Must(params.SHA), params.SizeLimit)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -20,9 +20,9 @@ import (
"strings" "strings"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/adapter" "github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/check" "github.com/harness/gitness/git/check"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -35,14 +35,14 @@ const (
BranchSortOptionDate BranchSortOptionDate
) )
var listBranchesRefFields = []types.GitReferenceField{ var listBranchesRefFields = []api.GitReferenceField{
types.GitReferenceFieldRefName, api.GitReferenceFieldRefName,
types.GitReferenceFieldObjectName, api.GitReferenceFieldObjectName,
} }
type Branch struct { type Branch struct {
Name string Name string
SHA string SHA sha.SHA
Commit *Commit Commit *Commit
} }
@ -97,17 +97,17 @@ func (s *Service) CreateBranch(ctx context.Context, params *CreateBranchParams)
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
targetCommit, err := s.adapter.GetCommit(ctx, repoPath, strings.TrimSpace(params.Target)) targetCommit, err := s.git.GetCommit(ctx, repoPath, strings.TrimSpace(params.Target))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get target commit: %w", err) return nil, fmt.Errorf("failed to get target commit: %w", err)
} }
branchRef := adapter.GetReferenceFromBranchName(params.BranchName) branchRef := api.GetReferenceFromBranchName(params.BranchName)
err = s.adapter.UpdateRef( err = s.git.UpdateRef(
ctx, ctx,
params.EnvVars, params.EnvVars,
repoPath, repoPath,
branchRef, branchRef,
types.NilSHA, // we want to make sure we don't overwrite any parallel create sha.Nil, // we want to make sure we don't overwrite any parallel create
targetCommit.SHA, targetCommit.SHA,
) )
if errors.IsConflict(err) { if errors.IsConflict(err) {
@ -139,7 +139,7 @@ func (s *Service) GetBranch(ctx context.Context, params *GetBranchParams) (*GetB
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
sanitizedBranchName := strings.TrimPrefix(params.BranchName, gitReferenceNamePrefixBranch) sanitizedBranchName := strings.TrimPrefix(params.BranchName, gitReferenceNamePrefixBranch)
gitBranch, err := s.adapter.GetBranch(ctx, repoPath, sanitizedBranchName) gitBranch, err := s.git.GetBranch(ctx, repoPath, sanitizedBranchName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -160,17 +160,17 @@ func (s *Service) DeleteBranch(ctx context.Context, params *DeleteBranchParams)
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
branchRef := adapter.GetReferenceFromBranchName(params.BranchName) branchRef := api.GetReferenceFromBranchName(params.BranchName)
err := s.adapter.UpdateRef( err := s.git.UpdateRef(
ctx, ctx,
params.EnvVars, params.EnvVars,
repoPath, repoPath,
branchRef, branchRef,
"", // delete whatever is there sha.None, // delete whatever is there
types.NilSHA, sha.Nil,
) )
if types.IsNotFoundError(err) { if errors.IsNotFound(err) {
return errors.NotFound("branch %q does not exist", params.BranchName) return errors.NotFound("branch %q does not exist", params.BranchName)
} }
if err != nil { if err != nil {
@ -187,7 +187,7 @@ func (s *Service) ListBranches(ctx context.Context, params *ListBranchesParams)
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
gitBranches, err := s.listBranchesLoadReferenceData(ctx, repoPath, types.BranchFilter{ gitBranches, err := s.listBranchesLoadReferenceData(ctx, repoPath, api.BranchFilter{
IncludeCommit: params.IncludeCommit, IncludeCommit: params.IncludeCommit,
Query: params.Query, Query: params.Query,
Sort: mapBranchesSortOption(params.Sort), Sort: mapBranchesSortOption(params.Sort),
@ -203,17 +203,17 @@ func (s *Service) ListBranches(ctx context.Context, params *ListBranchesParams)
if params.IncludeCommit { if params.IncludeCommit {
commitSHAs := make([]string, len(gitBranches)) commitSHAs := make([]string, len(gitBranches))
for i := range gitBranches { for i := range gitBranches {
commitSHAs[i] = gitBranches[i].SHA commitSHAs[i] = gitBranches[i].SHA.String()
} }
var gitCommits []types.Commit var gitCommits []*api.Commit
gitCommits, err = s.adapter.GetCommits(ctx, repoPath, commitSHAs) gitCommits, err = s.git.GetCommits(ctx, repoPath, commitSHAs)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get commit: %w", err) return nil, fmt.Errorf("failed to get commit: %w", err)
} }
for i := range gitCommits { for i := range gitCommits {
gitBranches[i].Commit = &gitCommits[i] gitBranches[i].Commit = gitCommits[i]
} }
} }
@ -234,13 +234,13 @@ func (s *Service) ListBranches(ctx context.Context, params *ListBranchesParams)
func (s *Service) listBranchesLoadReferenceData( func (s *Service) listBranchesLoadReferenceData(
ctx context.Context, ctx context.Context,
repoPath string, repoPath string,
filter types.BranchFilter, filter api.BranchFilter,
) ([]*types.Branch, error) { ) ([]*api.Branch, error) {
// TODO: can we be smarter with slice allocation // TODO: can we be smarter with slice allocation
branches := make([]*types.Branch, 0, 16) branches := make([]*api.Branch, 0, 16)
handler := listBranchesWalkReferencesHandler(&branches) handler := listBranchesWalkReferencesHandler(&branches)
instructor, endsAfter, err := wrapInstructorWithOptionalPagination( instructor, endsAfter, err := wrapInstructorWithOptionalPagination(
adapter.DefaultInstructor, // branches only have one target type, default instructor is enough api.DefaultInstructor, // branches only have one target type, default instructor is enough
filter.Page, filter.Page,
filter.PageSize, filter.PageSize,
) )
@ -248,7 +248,7 @@ func (s *Service) listBranchesLoadReferenceData(
return nil, errors.InvalidArgument("invalid pagination details: %v", err) return nil, errors.InvalidArgument("invalid pagination details: %v", err)
} }
opts := &types.WalkReferencesOptions{ opts := &api.WalkReferencesOptions{
Patterns: createReferenceWalkPatternsFromQuery(gitReferenceNamePrefixBranch, filter.Query), Patterns: createReferenceWalkPatternsFromQuery(gitReferenceNamePrefixBranch, filter.Query),
Sort: filter.Sort, Sort: filter.Sort,
Order: filter.Order, Order: filter.Order,
@ -258,32 +258,32 @@ func (s *Service) listBranchesLoadReferenceData(
MaxWalkDistance: endsAfter, MaxWalkDistance: endsAfter,
} }
err = s.adapter.WalkReferences(ctx, repoPath, handler, opts) err = s.git.WalkReferences(ctx, repoPath, handler, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to walk branch references: %w", err) return nil, fmt.Errorf("failed to walk branch references: %w", err)
} }
log.Ctx(ctx).Trace().Msgf("git adapter returned %d branches", len(branches)) log.Ctx(ctx).Trace().Msgf("git api returned %d branches", len(branches))
return branches, nil return branches, nil
} }
func listBranchesWalkReferencesHandler( func listBranchesWalkReferencesHandler(
branches *[]*types.Branch, branches *[]*api.Branch,
) types.WalkReferencesHandler { ) api.WalkReferencesHandler {
return func(e types.WalkReferencesEntry) error { return func(e api.WalkReferencesEntry) error {
fullRefName, ok := e[types.GitReferenceFieldRefName] fullRefName, ok := e[api.GitReferenceFieldRefName]
if !ok { if !ok {
return fmt.Errorf("entry missing reference name") return fmt.Errorf("entry missing reference name")
} }
objectSHA, ok := e[types.GitReferenceFieldObjectName] objectSHA, ok := e[api.GitReferenceFieldObjectName]
if !ok { if !ok {
return fmt.Errorf("entry missing object sha") return fmt.Errorf("entry missing object sha")
} }
branch := &types.Branch{ branch := &api.Branch{
Name: fullRefName[len(gitReferenceNamePrefixBranch):], Name: fullRefName[len(gitReferenceNamePrefixBranch):],
SHA: objectSHA, SHA: sha.Must(objectSHA),
} }
// TODO: refactor to not use slice pointers? // TODO: refactor to not use slice pointers?

View File

@ -39,7 +39,8 @@ func (b builder) supportsEndOfOptions() bool {
// descriptions is a curated list of Git command descriptions. // descriptions is a curated list of Git command descriptions.
var descriptions = map[string]builder{ var descriptions = map[string]builder{
"am": {}, "am": {},
"add": {},
"apply": { "apply": {
flags: NoRefUpdates, flags: NoRefUpdates,
}, },
@ -124,6 +125,9 @@ var descriptions = map[string]builder{
"log": { "log": {
flags: NoRefUpdates, flags: NoRefUpdates,
}, },
"ls-files": {
flags: NoRefUpdates,
},
"ls-remote": { "ls-remote": {
flags: NoRefUpdates, flags: NoRefUpdates,
}, },
@ -226,6 +230,9 @@ var descriptions = map[string]builder{
"update-ref": { "update-ref": {
flags: 0, flags: 0,
}, },
"update-index": {
flags: NoEndOfOptions,
},
"upload-archive": { "upload-archive": {
// git-upload-archive(1) has a handrolled parser which always interprets the // git-upload-archive(1) has a handrolled parser which always interprets the
// first argument as directory, so we cannot use `--end-of-options`. // first argument as directory, so we cannot use `--end-of-options`.
@ -240,6 +247,9 @@ var descriptions = map[string]builder{
"worktree": { "worktree": {
flags: 0, flags: 0,
}, },
"write-tree": {
flags: 0,
},
} }
// args validates the given flags and arguments and, if valid, returns the complete command line. // args validates the given flags and arguments and, if valid, returns the complete command line.
@ -248,7 +258,7 @@ func (b builder) args(flags []string, args []string, postSepArgs []string) ([]st
cmdArgs = append(cmdArgs, flags...) cmdArgs = append(cmdArgs, flags...)
if b.supportsEndOfOptions() { if b.supportsEndOfOptions() && len(flags) > 0 {
cmdArgs = append(cmdArgs, "--end-of-options") cmdArgs = append(cmdArgs, "--end-of-options")
} }

View File

@ -77,6 +77,33 @@ func New(name string, options ...CmdOptionFunc) *Command {
return c return c
} }
// Clone clones the command object.
func (c *Command) Clone() *Command {
flags := make([]string, len(c.Flags))
copy(flags, c.Flags)
args := make([]string, len(c.Args))
copy(args, c.Args)
postSepArgs := make([]string, len(c.PostSepArgs))
copy(postSepArgs, c.Flags)
envs := make(Envs, len(c.Envs))
for key, val := range c.Envs {
envs[key] = val
}
return &Command{
Name: c.Name,
Action: c.Action,
Flags: flags,
Args: args,
PostSepArgs: postSepArgs,
Envs: envs,
configEnvCounter: c.configEnvCounter,
}
}
// Add appends given options to the command. // Add appends given options to the command.
func (c *Command) Add(options ...CmdOptionFunc) *Command { func (c *Command) Add(options ...CmdOptionFunc) *Command {
for _, opt := range options { for _, opt := range options {
@ -109,6 +136,7 @@ func (c *Command) Run(ctx context.Context, opts ...RunOptionFunc) (err error) {
if len(c.Envs) > 0 { if len(c.Envs) > 0 {
cmd.Env = c.Envs.Args() cmd.Env = c.Envs.Args()
} }
cmd.Env = append(cmd.Env, options.Envs...)
cmd.Dir = options.Dir cmd.Dir = options.Dir
cmd.Stdin = options.Stdin cmd.Stdin = options.Stdin
cmd.Stdout = options.Stdout cmd.Stdout = options.Stdout

View File

@ -28,6 +28,9 @@ const (
GitTracePerformance = "GIT_TRACE_PERFORMANCE" GitTracePerformance = "GIT_TRACE_PERFORMANCE"
GitTraceSetup = "GIT_TRACE_SETUP" GitTraceSetup = "GIT_TRACE_SETUP"
GitExecPath = "GIT_EXEC_PATH" // tells Git where to find its binaries. GitExecPath = "GIT_EXEC_PATH" // tells Git where to find its binaries.
GitObjectDir = "GIT_OBJECT_DIRECTORY"
GitAlternateObjectDirs = "GIT_ALTERNATE_OBJECT_DIRECTORIES"
) )
// Envs custom key value store for environment variables. // Envs custom key value store for environment variables.

View File

@ -48,6 +48,10 @@ func (e *Error) ExitCode() int {
return 0 return 0
} }
func (e *Error) IsExitCode(code int) bool {
return e.ExitCode() == code
}
func (e *Error) Error() string { func (e *Error) Error() string {
if len(e.StdErr) != 0 { if len(e.StdErr) != 0 {
return fmt.Sprintf("%s: %s", e.Err.Error(), e.StdErr) return fmt.Sprintf("%s: %s", e.Err.Error(), e.StdErr)

View File

@ -17,6 +17,7 @@ package command
import ( import (
"io" "io"
"strconv" "strconv"
"strings"
"time" "time"
) )
@ -105,6 +106,15 @@ func WithConfig(key, value string) CmdOptionFunc {
} }
} }
// WithAlternateObjectDirs function sets alternates directories for object access.
func WithAlternateObjectDirs(dirs ...string) CmdOptionFunc {
return func(c *Command) {
if len(dirs) > 0 {
c.Envs[GitAlternateObjectDirs] = strings.Join(dirs, ":")
}
}
}
// RunOption contains option for running a command. // RunOption contains option for running a command.
type RunOption struct { type RunOption struct {
// Dir is location of repo. // Dir is location of repo.
@ -115,6 +125,9 @@ type RunOption struct {
Stdout io.Writer Stdout io.Writer
// Stderr is the error output from the command. // Stderr is the error output from the command.
Stderr io.Writer Stderr io.Writer
// Envs is environments slice containing (final) immutable
// environment pair "ENV=value"
Envs []string
} }
type RunOptionFunc func(option *RunOption) type RunOptionFunc func(option *RunOption)
@ -147,3 +160,11 @@ func WithStderr(stderr io.Writer) RunOptionFunc {
option.Stderr = stderr option.Stderr = stderr
} }
} }
// WithEnvs sets immutable values as slice, it is always added
// et the end of env slice.
func WithEnvs(envs ...string) RunOptionFunc {
return func(option *RunOption) {
option.Envs = append(option.Envs, envs...)
}
}

View File

@ -20,19 +20,19 @@ import (
"time" "time"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/enum" "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
) )
type GetCommitParams struct { type GetCommitParams struct {
ReadParams ReadParams
// SHA is the git commit sha Revision string
SHA string
} }
type Commit struct { type Commit struct {
SHA string `json:"sha"` SHA sha.SHA `json:"sha"`
ParentSHAs []string `json:"parent_shas,omitempty"` ParentSHAs []sha.SHA `json:"parent_shas,omitempty"`
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Author Signature `json:"author"` Author Signature `json:"author"`
@ -70,11 +70,8 @@ func (s *Service) GetCommit(ctx context.Context, params *GetCommitParams) (*GetC
if params == nil { if params == nil {
return nil, ErrNoParamsProvided return nil, ErrNoParamsProvided
} }
if !isValidGitSHA(params.SHA) {
return nil, errors.InvalidArgument("the provided commit sha '%s' is of invalid format.", params.SHA)
}
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
result, err := s.adapter.GetCommit(ctx, repoPath, params.SHA) result, err := s.git.GetCommit(ctx, repoPath, params.Revision)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -116,8 +113,8 @@ type ListCommitsParams struct {
type RenameDetails struct { type RenameDetails struct {
OldPath string OldPath string
NewPath string NewPath string
CommitShaBefore string CommitShaBefore sha.SHA
CommitShaAfter string CommitShaAfter sha.SHA
} }
type ListCommitsOutput struct { type ListCommitsOutput struct {
@ -141,14 +138,14 @@ func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
gitCommits, renameDetails, err := s.adapter.ListCommits( gitCommits, renameDetails, err := s.git.ListCommits(
ctx, ctx,
repoPath, repoPath,
params.GitREF, params.GitREF,
int(params.Page), int(params.Page),
int(params.Limit), int(params.Limit),
params.IncludeStats, params.IncludeStats,
types.CommitFilter{ api.CommitFilter{
AfterRef: params.After, AfterRef: params.After,
Path: params.Path, Path: params.Path,
Since: params.Since, Since: params.Since,
@ -165,7 +162,7 @@ func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*
if params.Page == 1 && len(gitCommits) < int(params.Limit) { if params.Page == 1 && len(gitCommits) < int(params.Limit) {
totalCommits = len(gitCommits) totalCommits = len(gitCommits)
} else if params.After != "" && params.GitREF != params.After { } else if params.After != "" && params.GitREF != params.After {
div, err := s.adapter.GetCommitDivergences(ctx, repoPath, []types.CommitDivergenceRequest{ div, err := s.git.GetCommitDivergences(ctx, repoPath, []api.CommitDivergenceRequest{
{From: params.GitREF, To: params.After}, {From: params.GitREF, To: params.After},
}, 0) }, 0)
if err != nil { if err != nil {
@ -178,7 +175,7 @@ func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*
commits := make([]Commit, len(gitCommits)) commits := make([]Commit, len(gitCommits))
for i := range gitCommits { for i := range gitCommits {
commit, err := mapCommit(&gitCommits[i]) commit, err := mapCommit(gitCommits[i])
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to map rpc commit: %w", err) return nil, fmt.Errorf("failed to map rpc commit: %w", err)
} }
@ -200,7 +197,7 @@ type GetCommitDivergencesParams struct {
} }
type GetCommitDivergencesOutput struct { type GetCommitDivergencesOutput struct {
Divergences []types.CommitDivergence Divergences []api.CommitDivergence
} }
// CommitDivergenceRequest contains the refs for which the converging commits should be counted. // CommitDivergenceRequest contains the refs for which the converging commits should be counted.
@ -229,16 +226,15 @@ func (s *Service) GetCommitDivergences(
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
requests := make([]types.CommitDivergenceRequest, len(params.Requests)) requests := make([]api.CommitDivergenceRequest, len(params.Requests))
for i, req := range params.Requests { for i, req := range params.Requests {
requests[i] = types.CommitDivergenceRequest{ requests[i] = api.CommitDivergenceRequest{
From: req.From, From: req.From,
To: req.To, To: req.To,
} }
} }
// call gitea divergences, err := s.git.GetCommitDivergences(
divergences, err := s.adapter.GetCommitDivergences(
ctx, ctx,
repoPath, repoPath,
requests, requests,

View File

@ -20,7 +20,8 @@ import (
// ReadParams contains the base parameters for read operations. // ReadParams contains the base parameters for read operations.
type ReadParams struct { type ReadParams struct {
RepoUID string RepoUID string
AlternateObjectDirs []string
} }
func (p ReadParams) Validate() error { func (p ReadParams) Validate() error {

View File

@ -22,9 +22,11 @@ import (
"sync" "sync"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/diff" "github.com/harness/gitness/git/diff"
"github.com/harness/gitness/git/enum" "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/parser"
"github.com/harness/gitness/git/sha"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -52,19 +54,27 @@ func (s *Service) RawDiff(
ctx context.Context, ctx context.Context,
out io.Writer, out io.Writer,
params *DiffParams, params *DiffParams,
files ...types.FileDiffRequest, files ...api.FileDiffRequest,
) error { ) error {
return s.rawDiff(ctx, out, params, files...) return s.rawDiff(ctx, out, params, files...)
} }
func (s *Service) rawDiff(ctx context.Context, w io.Writer, params *DiffParams, files ...types.FileDiffRequest) error { func (s *Service) rawDiff(ctx context.Context, w io.Writer, params *DiffParams, files ...api.FileDiffRequest) error {
if err := params.Validate(); err != nil { if err := params.Validate(); err != nil {
return err return err
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
err := s.adapter.RawDiff(ctx, w, repoPath, params.BaseRef, params.HeadRef, params.MergeBase, files...) err := s.git.RawDiff(ctx,
w,
repoPath,
params.BaseRef,
params.HeadRef,
params.MergeBase,
params.AlternateObjectDirs,
files...,
)
if err != nil { if err != nil {
return err return err
} }
@ -72,11 +82,8 @@ func (s *Service) rawDiff(ctx context.Context, w io.Writer, params *DiffParams,
} }
func (s *Service) CommitDiff(ctx context.Context, params *GetCommitParams, out io.Writer) error { func (s *Service) CommitDiff(ctx context.Context, params *GetCommitParams, out io.Writer) error {
if !isValidGitSHA(params.SHA) {
return errors.InvalidArgument("the provided commit sha '%s' is of invalid format.", params.SHA)
}
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
err := s.adapter.CommitDiff(ctx, repoPath, params.SHA, out) err := s.git.CommitDiff(ctx, repoPath, params.Revision, out)
if err != nil { if err != nil {
return err return err
} }
@ -95,7 +102,7 @@ func (s *Service) DiffShortStat(ctx context.Context, params *DiffParams) (DiffSh
return DiffShortStatOutput{}, err return DiffShortStatOutput{}, err
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
stat, err := s.adapter.DiffShortStat(ctx, stat, err := s.git.DiffShortStat(ctx,
repoPath, repoPath,
params.BaseRef, params.BaseRef,
params.HeadRef, params.HeadRef,
@ -207,7 +214,7 @@ func (s *Service) GetDiffHunkHeaders(
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
hunkHeaders, err := s.adapter.GetDiffHunkHeaders(ctx, repoPath, params.TargetCommitSHA, params.SourceCommitSHA) hunkHeaders, err := s.git.GetDiffHunkHeaders(ctx, repoPath, params.TargetCommitSHA, params.SourceCommitSHA)
if err != nil { if err != nil {
return GetDiffHunkHeadersOutput{}, err return GetDiffHunkHeadersOutput{}, err
} }
@ -233,7 +240,7 @@ type DiffCutOutput struct {
Header HunkHeader Header HunkHeader
LinesHeader string LinesHeader string
Lines []string Lines []string
MergeBaseSHA string MergeBaseSHA sha.SHA
} }
type DiffCutParams struct { type DiffCutParams struct {
@ -256,17 +263,17 @@ func (s *Service) DiffCut(ctx context.Context, params *DiffCutParams) (DiffCutOu
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
mergeBaseSHA, _, err := s.adapter.GetMergeBase(ctx, repoPath, "", params.TargetCommitSHA, params.SourceCommitSHA) mergeBaseSHA, _, err := s.git.GetMergeBase(ctx, repoPath, "", params.TargetCommitSHA, params.SourceCommitSHA)
if err != nil { if err != nil {
return DiffCutOutput{}, fmt.Errorf("DiffCut: failed to find merge base: %w", err) return DiffCutOutput{}, fmt.Errorf("DiffCut: failed to find merge base: %w", err)
} }
header, linesHunk, err := s.adapter.DiffCut(ctx, header, linesHunk, err := s.git.DiffCut(ctx,
repoPath, repoPath,
params.TargetCommitSHA, params.TargetCommitSHA,
params.SourceCommitSHA, params.SourceCommitSHA,
params.Path, params.Path,
types.DiffCutParams{ parser.DiffCutParams{
LineStart: params.LineStart, LineStart: params.LineStart,
LineStartNew: params.LineStartNew, LineStartNew: params.LineStartNew,
LineEnd: params.LineEnd, LineEnd: params.LineEnd,
@ -328,7 +335,7 @@ func parseFileDiffStatus(ftype diff.FileType) enum.FileDiffStatus {
func (s *Service) Diff( func (s *Service) Diff(
ctx context.Context, ctx context.Context,
params *DiffParams, params *DiffParams,
files ...types.FileDiffRequest, files ...api.FileDiffRequest,
) (<-chan *FileDiff, <-chan error) { ) (<-chan *FileDiff, <-chan error) {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
ch := make(chan *FileDiff) ch := make(chan *FileDiff)
@ -403,7 +410,7 @@ func (s *Service) DiffFileNames(ctx context.Context, params *DiffParams) (DiffFi
return DiffFileNamesOutput{}, err return DiffFileNamesOutput{}, err
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
fileNames, err := s.adapter.DiffFileName( fileNames, err := s.git.DiffFileName(
ctx, ctx,
repoPath, repoPath,
params.BaseRef, params.BaseRef,

View File

@ -23,6 +23,8 @@ import (
"os" "os"
"strings" "strings"
"time" "time"
"github.com/harness/gitness/git/sha"
) )
// CLICore implements the core of a githook cli. It uses the client and execution timeout // CLICore implements the core of a githook cli. It uses the client and execution timeout
@ -61,8 +63,8 @@ func (c *CLICore) Update(ctx context.Context, ref string, oldSHA string, newSHA
in := UpdateInput{ in := UpdateInput{
RefUpdate: ReferenceUpdate{ RefUpdate: ReferenceUpdate{
Ref: ref, Ref: ref,
Old: oldSHA, Old: sha.Must(oldSHA),
New: newSHA, New: sha.Must(newSHA),
}, },
} }
@ -139,8 +141,8 @@ func getUpdatedReferencesFromStdIn() ([]ReferenceUpdate, error) {
} }
updatedRefs = append(updatedRefs, ReferenceUpdate{ updatedRefs = append(updatedRefs, ReferenceUpdate{
Old: splitGitHookData[0], Old: sha.Must(splitGitHookData[0]),
New: splitGitHookData[1], New: sha.Must(splitGitHookData[1]),
Ref: splitGitHookData[2], Ref: splitGitHookData[2],
}) })
} }

View File

@ -14,6 +14,8 @@
package hook package hook
import "github.com/harness/gitness/git/sha"
// Output represents the output of server hook api calls. // Output represents the output of server hook api calls.
type Output struct { type Output struct {
// Messages contains standard user facing messages. // Messages contains standard user facing messages.
@ -28,9 +30,9 @@ type ReferenceUpdate struct {
// Ref is the full name of the reference that got updated. // Ref is the full name of the reference that got updated.
Ref string `json:"ref"` Ref string `json:"ref"`
// Old is the old commmit hash (before the update). // Old is the old commmit hash (before the update).
Old string `json:"old"` Old sha.SHA `json:"old"`
// New is the new commit hash (after the update). // New is the new commit hash (after the update).
New string `json:"new"` New sha.SHA `json:"new"`
} }
// PostReceiveInput represents the input of the post-receive git hook. // PostReceiveInput represents the input of the post-receive git hook.

View File

@ -43,7 +43,7 @@ func (s *Service) GetInfoRefs(ctx context.Context, w io.Writer, params *InfoRefs
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
err := s.adapter.InfoRefs(ctx, repoPath, params.Service, w, environ...) err := s.git.InfoRefs(ctx, repoPath, params.Service, w, environ...)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch info references: %w", err) return fmt.Errorf("failed to fetch info references: %w", err)
} }
@ -96,7 +96,7 @@ func (s *Service) ServicePack(ctx context.Context, w io.Writer, params *ServiceP
env = append(env, "GIT_PROTOCOL="+params.GitProtocol) env = append(env, "GIT_PROTOCOL="+params.GitProtocol)
} }
err := s.adapter.ServicePack(ctx, repoPath, params.Service, params.Data, w, env...) err := s.git.ServicePack(ctx, repoPath, params.Service, params.Data, w, env...)
if err != nil { if err != nil {
return fmt.Errorf("failed to execute git %s: %w", params.Service, err) return fmt.Errorf("failed to execute git %s: %w", params.Service, err)
} }

View File

@ -18,7 +18,7 @@ import (
"context" "context"
"io" "io"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/api"
) )
type Interface interface { type Interface interface {
@ -68,8 +68,8 @@ type Interface interface {
/* /*
* Diff services * Diff services
*/ */
RawDiff(ctx context.Context, w io.Writer, in *DiffParams, files ...types.FileDiffRequest) error RawDiff(ctx context.Context, w io.Writer, in *DiffParams, files ...api.FileDiffRequest) error
Diff(ctx context.Context, in *DiffParams, files ...types.FileDiffRequest) (<-chan *FileDiff, <-chan error) Diff(ctx context.Context, in *DiffParams, files ...api.FileDiffRequest) (<-chan *FileDiff, <-chan error)
DiffFileNames(ctx context.Context, in *DiffParams) (DiffFileNamesOutput, error) DiffFileNames(ctx context.Context, in *DiffParams) (DiffFileNamesOutput, error)
CommitDiff(ctx context.Context, params *GetCommitParams, w io.Writer) error CommitDiff(ctx context.Context, params *GetCommitParams, w io.Writer) error
DiffShortStat(ctx context.Context, params *DiffParams) (DiffShortStatOutput, error) DiffShortStat(ctx context.Context, params *DiffParams) (DiffShortStatOutput, error)

View File

@ -17,10 +17,11 @@ package git
import ( import (
"fmt" "fmt"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/parser"
) )
func mapBranch(b *types.Branch) (*Branch, error) { func mapBranch(b *api.Branch) (*Branch, error) {
if b == nil { if b == nil {
return nil, fmt.Errorf("rpc branch is nil") return nil, fmt.Errorf("rpc branch is nil")
} }
@ -41,7 +42,7 @@ func mapBranch(b *types.Branch) (*Branch, error) {
}, nil }, nil
} }
func mapCommit(c *types.Commit) (*Commit, error) { func mapCommit(c *api.Commit) (*Commit, error) {
if c == nil { if c == nil {
return nil, fmt.Errorf("rpc commit is nil") return nil, fmt.Errorf("rpc commit is nil")
} }
@ -67,12 +68,12 @@ func mapCommit(c *types.Commit) (*Commit, error) {
}, nil }, nil
} }
func mapFileStats(typeStats []types.CommitFileStats) []CommitFileStats { func mapFileStats(typeStats []api.CommitFileStats) []CommitFileStats {
var stats = make([]CommitFileStats, len(typeStats)) var stats = make([]CommitFileStats, len(typeStats))
for i, tStat := range typeStats { for i, tStat := range typeStats {
stats[i] = CommitFileStats{ stats[i] = CommitFileStats{
Status: tStat.Status, Status: tStat.ChangeType,
Path: tStat.Path, Path: tStat.Path,
OldPath: tStat.OldPath, OldPath: tStat.OldPath,
Insertions: tStat.Insertions, Insertions: tStat.Insertions,
@ -83,7 +84,7 @@ func mapFileStats(typeStats []types.CommitFileStats) []CommitFileStats {
return stats return stats
} }
func mapSignature(s *types.Signature) (*Signature, error) { func mapSignature(s *api.Signature) (*Signature, error) {
if s == nil { if s == nil {
return nil, fmt.Errorf("rpc signature is nil") return nil, fmt.Errorf("rpc signature is nil")
} }
@ -99,7 +100,7 @@ func mapSignature(s *types.Signature) (*Signature, error) {
}, nil }, nil
} }
func mapIdentity(id *types.Identity) (Identity, error) { func mapIdentity(id *api.Identity) (Identity, error) {
if id == nil { if id == nil {
return Identity{}, fmt.Errorf("rpc identity is nil") return Identity{}, fmt.Errorf("rpc identity is nil")
} }
@ -110,12 +111,12 @@ func mapIdentity(id *types.Identity) (Identity, error) {
}, nil }, nil
} }
func mapBranchesSortOption(o BranchSortOption) types.GitReferenceField { func mapBranchesSortOption(o BranchSortOption) api.GitReferenceField {
switch o { switch o {
case BranchSortOptionName: case BranchSortOptionName:
return types.GitReferenceFieldObjectName return api.GitReferenceFieldObjectName
case BranchSortOptionDate: case BranchSortOptionDate:
return types.GitReferenceFieldCreatorDate return api.GitReferenceFieldCreatorDate
case BranchSortOptionDefault: case BranchSortOptionDefault:
fallthrough fallthrough
default: default:
@ -124,7 +125,7 @@ func mapBranchesSortOption(o BranchSortOption) types.GitReferenceField {
} }
} }
func mapAnnotatedTag(tag *types.Tag) *CommitTag { func mapAnnotatedTag(tag *api.Tag) *CommitTag {
tagger, _ := mapSignature(&tag.Tagger) tagger, _ := mapSignature(&tag.Tagger)
return &CommitTag{ return &CommitTag{
Name: tag.Name, Name: tag.Name,
@ -137,21 +138,21 @@ func mapAnnotatedTag(tag *types.Tag) *CommitTag {
} }
} }
func mapListCommitTagsSortOption(s TagSortOption) types.GitReferenceField { func mapListCommitTagsSortOption(s TagSortOption) api.GitReferenceField {
switch s { switch s {
case TagSortOptionDate: case TagSortOptionDate:
return types.GitReferenceFieldCreatorDate return api.GitReferenceFieldCreatorDate
case TagSortOptionName: case TagSortOptionName:
return types.GitReferenceFieldRefName return api.GitReferenceFieldRefName
case TagSortOptionDefault: case TagSortOptionDefault:
return types.GitReferenceFieldRefName return api.GitReferenceFieldRefName
default: default:
// no need to error out - just use default for sorting // no need to error out - just use default for sorting
return types.GitReferenceFieldRefName return api.GitReferenceFieldRefName
} }
} }
func mapTreeNode(n *types.TreeNode) (TreeNode, error) { func mapTreeNode(n *api.TreeNode) (TreeNode, error) {
if n == nil { if n == nil {
return TreeNode{}, fmt.Errorf("rpc tree node is nil") return TreeNode{}, fmt.Errorf("rpc tree node is nil")
} }
@ -169,43 +170,43 @@ func mapTreeNode(n *types.TreeNode) (TreeNode, error) {
return TreeNode{ return TreeNode{
Type: nodeType, Type: nodeType,
Mode: mode, Mode: mode,
SHA: n.Sha, SHA: n.SHA.String(),
Name: n.Name, Name: n.Name,
Path: n.Path, Path: n.Path,
}, nil }, nil
} }
func mapTreeNodeType(t types.TreeNodeType) (TreeNodeType, error) { func mapTreeNodeType(t api.TreeNodeType) (TreeNodeType, error) {
switch t { switch t {
case types.TreeNodeTypeBlob: case api.TreeNodeTypeBlob:
return TreeNodeTypeBlob, nil return TreeNodeTypeBlob, nil
case types.TreeNodeTypeCommit: case api.TreeNodeTypeCommit:
return TreeNodeTypeCommit, nil return TreeNodeTypeCommit, nil
case types.TreeNodeTypeTree: case api.TreeNodeTypeTree:
return TreeNodeTypeTree, nil return TreeNodeTypeTree, nil
default: default:
return TreeNodeTypeBlob, fmt.Errorf("unknown rpc tree node type: %d", t) return TreeNodeTypeBlob, fmt.Errorf("unknown rpc tree node type: %d", t)
} }
} }
func mapTreeNodeMode(m types.TreeNodeMode) (TreeNodeMode, error) { func mapTreeNodeMode(m api.TreeNodeMode) (TreeNodeMode, error) {
switch m { switch m {
case types.TreeNodeModeFile: case api.TreeNodeModeFile:
return TreeNodeModeFile, nil return TreeNodeModeFile, nil
case types.TreeNodeModeExec: case api.TreeNodeModeExec:
return TreeNodeModeExec, nil return TreeNodeModeExec, nil
case types.TreeNodeModeSymlink: case api.TreeNodeModeSymlink:
return TreeNodeModeSymlink, nil return TreeNodeModeSymlink, nil
case types.TreeNodeModeCommit: case api.TreeNodeModeCommit:
return TreeNodeModeCommit, nil return TreeNodeModeCommit, nil
case types.TreeNodeModeTree: case api.TreeNodeModeTree:
return TreeNodeModeTree, nil return TreeNodeModeTree, nil
default: default:
return TreeNodeModeFile, fmt.Errorf("unknown rpc tree node mode: %d", m) return TreeNodeModeFile, fmt.Errorf("unknown rpc tree node mode: %d", m)
} }
} }
func mapRenameDetails(c []types.PathRenameDetails) []*RenameDetails { func mapRenameDetails(c []api.PathRenameDetails) []*RenameDetails {
renameDetailsList := make([]*RenameDetails, len(c)) renameDetailsList := make([]*RenameDetails, len(c))
for i, detail := range c { for i, detail := range c {
renameDetailsList[i] = &RenameDetails{ renameDetailsList[i] = &RenameDetails{
@ -218,21 +219,21 @@ func mapRenameDetails(c []types.PathRenameDetails) []*RenameDetails {
return renameDetailsList return renameDetailsList
} }
func mapToSortOrder(o SortOrder) types.SortOrder { func mapToSortOrder(o SortOrder) api.SortOrder {
switch o { switch o {
case SortOrderAsc: case SortOrderAsc:
return types.SortOrderAsc return api.SortOrderAsc
case SortOrderDesc: case SortOrderDesc:
return types.SortOrderDesc return api.SortOrderDesc
case SortOrderDefault: case SortOrderDefault:
return types.SortOrderDefault return api.SortOrderDefault
default: default:
// no need to error out - just use default for sorting // no need to error out - just use default for sorting
return types.SortOrderDefault return api.SortOrderDefault
} }
} }
func mapHunkHeader(h *types.HunkHeader) HunkHeader { func mapHunkHeader(h *parser.HunkHeader) HunkHeader {
return HunkHeader{ return HunkHeader{
OldLine: h.OldLine, OldLine: h.OldLine,
OldSpan: h.OldSpan, OldSpan: h.OldSpan,
@ -242,7 +243,7 @@ func mapHunkHeader(h *types.HunkHeader) HunkHeader {
} }
} }
func mapDiffFileHeader(h *types.DiffFileHeader) DiffFileHeader { func mapDiffFileHeader(h *parser.DiffFileHeader) DiffFileHeader {
return DiffFileHeader{ return DiffFileHeader{
OldName: h.OldFileName, OldName: h.OldFileName,
NewName: h.NewFileName, NewName: h.NewFileName,

View File

@ -18,7 +18,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/api"
) )
type MatchFilesParams struct { type MatchFilesParams struct {
@ -30,7 +30,7 @@ type MatchFilesParams struct {
} }
type MatchFilesOutput struct { type MatchFilesOutput struct {
Files []types.FileContent Files []api.FileContent
} }
func (s *Service) MatchFiles(ctx context.Context, func (s *Service) MatchFiles(ctx context.Context,
@ -38,7 +38,7 @@ func (s *Service) MatchFiles(ctx context.Context,
) (*MatchFilesOutput, error) { ) (*MatchFilesOutput, error) {
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
matchedFiles, err := s.adapter.MatchFiles(ctx, repoPath, matchedFiles, err := s.git.MatchFiles(ctx, repoPath,
params.Ref, params.DirPath, params.Pattern, params.MaxSize) params.Ref, params.DirPath, params.Pattern, params.MaxSize)
if err != nil { if err != nil {
return nil, fmt.Errorf("MatchFiles: failed to open repo: %w", err) return nil, fmt.Errorf("MatchFiles: failed to open repo: %w", err)

View File

@ -21,9 +21,10 @@ import (
"time" "time"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/enum" "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/merge" "github.com/harness/gitness/git/merge"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -57,7 +58,7 @@ type MergeParams struct {
// HeadExpectedSHA is commit sha on the head branch, if HeadExpectedSHA is older // HeadExpectedSHA is commit sha on the head branch, if HeadExpectedSHA is older
// than the HeadBranch latest sha then merge will fail. // than the HeadBranch latest sha then merge will fail.
HeadExpectedSHA string HeadExpectedSHA sha.SHA
Force bool Force bool
DeleteHeadBranch bool DeleteHeadBranch bool
@ -88,13 +89,13 @@ func (p *MergeParams) Validate() error {
// base, head and commit sha. // base, head and commit sha.
type MergeOutput struct { type MergeOutput struct {
// BaseSHA is the sha of the latest commit on the base branch that was used for merging. // BaseSHA is the sha of the latest commit on the base branch that was used for merging.
BaseSHA string BaseSHA sha.SHA
// HeadSHA is the sha of the latest commit on the head branch that was used for merging. // HeadSHA is the sha of the latest commit on the head branch that was used for merging.
HeadSHA string HeadSHA sha.SHA
// MergeBaseSHA is the sha of the merge base of the HeadSHA and BaseSHA // MergeBaseSHA is the sha of the merge base of the HeadSHA and BaseSHA
MergeBaseSHA string MergeBaseSHA sha.SHA
// MergeSHA is the sha of the commit after merging HeadSHA with BaseSHA. // MergeSHA is the sha of the commit after merging HeadSHA with BaseSHA.
MergeSHA string MergeSHA sha.SHA
CommitCount int CommitCount int
ChangedFileCount int ChangedFileCount int
@ -148,7 +149,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
// set up the target reference // set up the target reference
var refPath string var refPath string
var refOldValue string var refOldValue sha.SHA
if params.RefType != enum.RefTypeUndefined { if params.RefType != enum.RefTypeUndefined {
refPath, err = GetRefPath(params.RefName, params.RefType) refPath, err = GetRefPath(params.RefName, params.RefType)
@ -158,9 +159,9 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
params.RefType, params.RefName, err) params.RefType, params.RefName, err)
} }
refOldValue, err = s.adapter.GetFullCommitID(ctx, repoPath, refPath) refOldValue, err = s.git.GetFullCommitID(ctx, repoPath, refPath)
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
refOldValue = types.NilSHA refOldValue = sha.Nil
} else if err != nil { } else if err != nil {
return MergeOutput{}, fmt.Errorf("failed to resolve %q: %w", refPath, err) return MergeOutput{}, fmt.Errorf("failed to resolve %q: %w", refPath, err)
} }
@ -178,17 +179,17 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
// find the commit SHAs // find the commit SHAs
baseCommitSHA, err := s.adapter.GetFullCommitID(ctx, repoPath, params.BaseBranch) baseCommitSHA, err := s.git.GetFullCommitID(ctx, repoPath, params.BaseBranch)
if err != nil { if err != nil {
return MergeOutput{}, fmt.Errorf("failed to get merge base branch commit SHA: %w", err) return MergeOutput{}, fmt.Errorf("failed to get merge base branch commit SHA: %w", err)
} }
headCommitSHA, err := s.adapter.GetFullCommitID(ctx, repoPath, params.HeadBranch) headCommitSHA, err := s.git.GetFullCommitID(ctx, repoPath, params.HeadBranch)
if err != nil { if err != nil {
return MergeOutput{}, fmt.Errorf("failed to get merge base branch commit SHA: %w", err) return MergeOutput{}, fmt.Errorf("failed to get merge base branch commit SHA: %w", err)
} }
if params.HeadExpectedSHA != "" && params.HeadExpectedSHA != headCommitSHA { if !params.HeadExpectedSHA.IsEmpty() && !params.HeadExpectedSHA.Equal(headCommitSHA) {
return MergeOutput{}, errors.PreconditionFailed( return MergeOutput{}, errors.PreconditionFailed(
"head branch '%s' is on SHA '%s' which doesn't match expected SHA '%s'.", "head branch '%s' is on SHA '%s' which doesn't match expected SHA '%s'.",
params.HeadBranch, params.HeadBranch,
@ -196,25 +197,26 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
params.HeadExpectedSHA) params.HeadExpectedSHA)
} }
mergeBaseCommitSHA, _, err := s.adapter.GetMergeBase(ctx, repoPath, "origin", baseCommitSHA, headCommitSHA) mergeBaseCommitSHA, _, err := s.git.GetMergeBase(ctx, repoPath, "origin",
baseCommitSHA.String(), headCommitSHA.String())
if err != nil { if err != nil {
return MergeOutput{}, fmt.Errorf("failed to get merge base: %w", err) return MergeOutput{}, fmt.Errorf("failed to get merge base: %w", err)
} }
if headCommitSHA == mergeBaseCommitSHA { if headCommitSHA.Equal(mergeBaseCommitSHA) {
return MergeOutput{}, errors.InvalidArgument("head branch doesn't contain any new commits.") return MergeOutput{}, errors.InvalidArgument("head branch doesn't contain any new commits.")
} }
// find short stat and number of commits // find short stat and number of commits
shortStat, err := s.adapter.DiffShortStat(ctx, repoPath, baseCommitSHA, headCommitSHA, true) shortStat, err := s.git.DiffShortStat(ctx, repoPath, baseCommitSHA.String(), headCommitSHA.String(), true)
if err != nil { if err != nil {
return MergeOutput{}, errors.Internal(err, return MergeOutput{}, errors.Internal(err,
"failed to find short stat between %s and %s", baseCommitSHA, headCommitSHA) "failed to find short stat between %s and %s", baseCommitSHA, headCommitSHA)
} }
changedFileCount := shortStat.Files changedFileCount := shortStat.Files
commitCount, err := merge.CommitCount(ctx, repoPath, baseCommitSHA, headCommitSHA) commitCount, err := merge.CommitCount(ctx, repoPath, baseCommitSHA.String(), headCommitSHA.String())
if err != nil { if err != nil {
return MergeOutput{}, fmt.Errorf("failed to find commit count for merge check: %w", err) return MergeOutput{}, fmt.Errorf("failed to find commit count for merge check: %w", err)
} }
@ -222,11 +224,11 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
// handle simple merge check // handle simple merge check
if params.RefType == enum.RefTypeUndefined { if params.RefType == enum.RefTypeUndefined {
_, _, conflicts, err := merge.FindConflicts(ctx, repoPath, baseCommitSHA, headCommitSHA) _, _, conflicts, err := merge.FindConflicts(ctx, repoPath, baseCommitSHA.String(), headCommitSHA.String())
if err != nil { if err != nil {
return MergeOutput{}, errors.Internal(err, return MergeOutput{}, errors.Internal(err,
"Merge check failed to find conflicts between commits %s and %s", "Merge check failed to find conflicts between commits %s and %s",
baseCommitSHA, headCommitSHA) baseCommitSHA.String(), headCommitSHA.String())
} }
log.Debug().Msg("merged check completed") log.Debug().Msg("merged check completed")
@ -235,7 +237,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
BaseSHA: baseCommitSHA, BaseSHA: baseCommitSHA,
HeadSHA: headCommitSHA, HeadSHA: headCommitSHA,
MergeBaseSHA: mergeBaseCommitSHA, MergeBaseSHA: mergeBaseCommitSHA,
MergeSHA: "", MergeSHA: sha.None,
CommitCount: commitCount, CommitCount: commitCount,
ChangedFileCount: changedFileCount, ChangedFileCount: changedFileCount,
ConflictFiles: conflicts, ConflictFiles: conflicts,
@ -246,10 +248,10 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
now := time.Now().UTC() now := time.Now().UTC()
committer := types.Signature{Identity: types.Identity(params.Actor), When: now} committer := api.Signature{Identity: api.Identity(params.Actor), When: now}
if params.Committer != nil { if params.Committer != nil {
committer.Identity = types.Identity(*params.Committer) committer.Identity = api.Identity(*params.Committer)
} }
if params.CommitterDate != nil { if params.CommitterDate != nil {
committer.When = *params.CommitterDate committer.When = *params.CommitterDate
@ -258,7 +260,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
author := committer author := committer
if params.Author != nil { if params.Author != nil {
author.Identity = types.Identity(*params.Author) author.Identity = api.Identity(*params.Author)
} }
if params.AuthorDate != nil { if params.AuthorDate != nil {
author.When = *params.AuthorDate author.When = *params.AuthorDate
@ -288,7 +290,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
BaseSHA: baseCommitSHA, BaseSHA: baseCommitSHA,
HeadSHA: headCommitSHA, HeadSHA: headCommitSHA,
MergeBaseSHA: mergeBaseCommitSHA, MergeBaseSHA: mergeBaseCommitSHA,
MergeSHA: "", MergeSHA: sha.None,
CommitCount: commitCount, CommitCount: commitCount,
ChangedFileCount: changedFileCount, ChangedFileCount: changedFileCount,
ConflictFiles: conflicts, ConflictFiles: conflicts,
@ -299,7 +301,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
log.Trace().Msg("merge completed - updating git reference") log.Trace().Msg("merge completed - updating git reference")
err = s.adapter.UpdateRef( err = s.git.UpdateRef(
ctx, ctx,
params.EnvVars, params.EnvVars,
repoPath, repoPath,
@ -350,7 +352,7 @@ func (p *MergeBaseParams) Validate() error {
} }
type MergeBaseOutput struct { type MergeBaseOutput struct {
MergeBaseSHA string MergeBaseSHA sha.SHA
} }
func (s *Service) MergeBase( func (s *Service) MergeBase(
@ -363,7 +365,7 @@ func (s *Service) MergeBase(
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
result, _, err := s.adapter.GetMergeBase(ctx, repoPath, "", params.Ref1, params.Ref2) result, _, err := s.git.GetMergeBase(ctx, repoPath, "", params.Ref1, params.Ref2)
if err != nil { if err != nil {
return MergeBaseOutput{}, err return MergeBaseOutput{}, err
} }
@ -375,8 +377,8 @@ func (s *Service) MergeBase(
type IsAncestorParams struct { type IsAncestorParams struct {
ReadParams ReadParams
AncestorCommitSHA string AncestorCommitSHA sha.SHA
DescendantCommitSHA string DescendantCommitSHA sha.SHA
} }
type IsAncestorOutput struct { type IsAncestorOutput struct {
@ -389,7 +391,7 @@ func (s *Service) IsAncestor(
) (IsAncestorOutput, error) { ) (IsAncestorOutput, error) {
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
result, err := s.adapter.IsAncestor(ctx, repoPath, params.AncestorCommitSHA, params.DescendantCommitSHA) result, err := s.git.IsAncestor(ctx, repoPath, params.AncestorCommitSHA, params.DescendantCommitSHA)
if err != nil { if err != nil {
return IsAncestorOutput{}, err return IsAncestorOutput{}, err
} }

View File

@ -18,9 +18,9 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/harness/gitness/git/adapter" "github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/sha"
"github.com/harness/gitness/git/sharedrepo" "github.com/harness/gitness/git/sharedrepo"
"github.com/harness/gitness/git/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -29,19 +29,19 @@ import (
type Func func( type Func func(
ctx context.Context, ctx context.Context,
repoPath, tmpDir string, repoPath, tmpDir string,
author, committer *types.Signature, author, committer *api.Signature,
message string, message string,
mergeBaseSHA, targetSHA, sourceSHA string, mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA string, conflicts []string, err error) ) (mergeSHA sha.SHA, conflicts []string, err error)
// Merge merges two the commits (targetSHA and sourceSHA) using the Merge method. // Merge merges two the commits (targetSHA and sourceSHA) using the Merge method.
func Merge( func Merge(
ctx context.Context, ctx context.Context,
repoPath, tmpDir string, repoPath, tmpDir string,
author, committer *types.Signature, author, committer *api.Signature,
message string, message string,
mergeBaseSHA, targetSHA, sourceSHA string, mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA string, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
return mergeInternal(ctx, return mergeInternal(ctx,
repoPath, tmpDir, repoPath, tmpDir,
author, committer, author, committer,
@ -54,10 +54,10 @@ func Merge(
func Squash( func Squash(
ctx context.Context, ctx context.Context,
repoPath, tmpDir string, repoPath, tmpDir string,
author, committer *types.Signature, author, committer *api.Signature,
message string, message string,
mergeBaseSHA, targetSHA, sourceSHA string, mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA string, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
return mergeInternal(ctx, return mergeInternal(ctx,
repoPath, tmpDir, repoPath, tmpDir,
author, committer, author, committer,
@ -70,15 +70,15 @@ func Squash(
func mergeInternal( func mergeInternal(
ctx context.Context, ctx context.Context,
repoPath, tmpDir string, repoPath, tmpDir string,
author, committer *types.Signature, author, committer *api.Signature,
message string, message string,
mergeBaseSHA, targetSHA, sourceSHA string, mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
squash bool, squash bool,
) (mergeSHA string, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
err = runInSharedRepo(ctx, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error { err = runInSharedRepo(ctx, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
var err error var err error
var treeSHA string var treeSHA sha.SHA
treeSHA, conflicts, err = s.MergeTree(ctx, mergeBaseSHA, targetSHA, sourceSHA) treeSHA, conflicts, err = s.MergeTree(ctx, mergeBaseSHA, targetSHA, sourceSHA)
if err != nil { if err != nil {
@ -89,7 +89,7 @@ func mergeInternal(
return nil return nil
} }
parents := make([]string, 0, 2) parents := make([]sha.SHA, 0, 2)
parents = append(parents, targetSHA) parents = append(parents, targetSHA)
if !squash { if !squash {
parents = append(parents, sourceSHA) parents = append(parents, sourceSHA)
@ -103,7 +103,7 @@ func mergeInternal(
return nil return nil
}) })
if err != nil { if err != nil {
return "", nil, fmt.Errorf("merge method=merge squash=%t: %w", squash, err) return sha.None, nil, fmt.Errorf("merge method=merge squash=%t: %w", squash, err)
} }
return mergeSHA, conflicts, nil return mergeSHA, conflicts, nil
@ -115,10 +115,10 @@ func mergeInternal(
func Rebase( func Rebase(
ctx context.Context, ctx context.Context,
repoPath, tmpDir string, repoPath, tmpDir string,
_, committer *types.Signature, // commit author isn't used here - it's copied from every commit _, committer *api.Signature, // commit author isn't used here - it's copied from every commit
_ string, // commit message isn't used here _ string, // commit message isn't used here
mergeBaseSHA, targetSHA, sourceSHA string, mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA string, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
err = runInSharedRepo(ctx, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error { err = runInSharedRepo(ctx, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
sourceSHAs, err := s.CommitSHAsForRebase(ctx, mergeBaseSHA, sourceSHA) sourceSHAs, err := s.CommitSHAsForRebase(ctx, mergeBaseSHA, sourceSHA)
if err != nil { if err != nil {
@ -126,15 +126,15 @@ func Rebase(
} }
lastCommitSHA := targetSHA lastCommitSHA := targetSHA
lastTreeSHA, err := s.GetTreeSHA(ctx, targetSHA) lastTreeSHA, err := s.GetTreeSHA(ctx, targetSHA.String())
if err != nil { if err != nil {
return fmt.Errorf("failed to get tree sha for target: %w", err) return fmt.Errorf("failed to get tree sha for target: %w", err)
} }
for _, commitSHA := range sourceSHAs { for _, commitSHA := range sourceSHAs {
var treeSHA string var treeSHA sha.SHA
commitInfo, err := adapter.GetCommit(ctx, s.Directory(), commitSHA, "") commitInfo, err := api.GetCommit(ctx, s.Directory(), commitSHA.String())
if err != nil { if err != nil {
return fmt.Errorf("failed to get commit data in rebase merge: %w", err) return fmt.Errorf("failed to get commit data in rebase merge: %w", err)
} }
@ -146,7 +146,7 @@ func Rebase(
message += "\n\n" + commitInfo.Message message += "\n\n" + commitInfo.Message
} }
mergeTreeMergeBaseSHA := "" var mergeTreeMergeBaseSHA sha.SHA
if len(commitInfo.ParentSHAs) > 0 { if len(commitInfo.ParentSHAs) > 0 {
// use parent of commit as merge base to only apply changes introduced by commit. // use parent of commit as merge base to only apply changes introduced by commit.
// See example usage of when --merge-base was introduced: // See example usage of when --merge-base was introduced:
@ -171,7 +171,7 @@ func Rebase(
// 2. The changes of the commit already exist on the target branch. // 2. The changes of the commit already exist on the target branch.
// Git's `git rebase` is dropping such commits on default (and so does Github) // Git's `git rebase` is dropping such commits on default (and so does Github)
// https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---emptydropkeepask // https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---emptydropkeepask
if treeSHA == lastTreeSHA { if treeSHA.Equal(lastTreeSHA) {
log.Ctx(ctx).Debug().Msgf("skipping commit %s as it's empty after rebase", commitSHA) log.Ctx(ctx).Debug().Msgf("skipping commit %s as it's empty after rebase", commitSHA)
continue continue
} }
@ -188,7 +188,7 @@ func Rebase(
return nil return nil
}) })
if err != nil { if err != nil {
return "", nil, fmt.Errorf("merge method=rebase: %w", err) return sha.None, nil, fmt.Errorf("merge method=rebase: %w", err)
} }
return mergeSHA, conflicts, nil return mergeSHA, conflicts, nil

View File

@ -23,11 +23,9 @@ import (
"time" "time"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/adapter" "github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/sha"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/repository/files"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -85,7 +83,7 @@ func (p *CommitFilesParams) Validate() error {
} }
type CommitFilesResponse struct { type CommitFilesResponse struct {
CommitID string CommitID sha.SHA
} }
//nolint:gocognit //nolint:gocognit
@ -116,13 +114,6 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
log.Debug().Msg("open repository")
repo, err := s.adapter.OpenRepository(ctx, repoPath)
if err != nil {
return CommitFilesResponse{}, fmt.Errorf("CommitFiles: failed to open repo: %w", err)
}
log.Debug().Msg("check if empty") log.Debug().Msg("check if empty")
// check if repo is empty // check if repo is empty
@ -130,7 +121,7 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
// This can be an issue in case someone created a branch already in the repo (just default branch is missing). // This can be an issue in case someone created a branch already in the repo (just default branch is missing).
// In that case the user can accidentally create separate git histories (which most likely is unintended). // In that case the user can accidentally create separate git histories (which most likely is unintended).
// If the user wants to actually build a disconnected commit graph they can use the cli. // If the user wants to actually build a disconnected commit graph they can use the cli.
isEmpty, err := s.adapter.HasBranches(ctx, repoPath) isEmpty, err := s.git.HasBranches(ctx, repoPath)
if err != nil { if err != nil {
return CommitFilesResponse{}, fmt.Errorf("CommitFiles: failed to determine if repository is empty: %w", err) return CommitFilesResponse{}, fmt.Errorf("CommitFiles: failed to determine if repository is empty: %w", err)
} }
@ -139,30 +130,30 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
// ensure input data is valid // ensure input data is valid
// the commit will be nil for empty repositories // the commit will be nil for empty repositories
commit, err := s.validateAndPrepareHeader(repo, isEmpty, params) commit, err := s.validateAndPrepareHeader(ctx, repoPath, isEmpty, params)
if err != nil { if err != nil {
return CommitFilesResponse{}, err return CommitFilesResponse{}, err
} }
var oldCommitSHA string var oldCommitSHA sha.SHA
if commit != nil { if commit != nil {
oldCommitSHA = commit.ID.String() oldCommitSHA = commit.SHA
} }
log.Debug().Msg("create shared repo") log.Debug().Msg("create shared repo")
newCommitSHA, err := func() (string, error) { newCommitSHA, err := func() (sha.SHA, error) {
// Create a directory for the temporary shared repository. // Create a directory for the temporary shared repository.
shared, err := s.adapter.SharedRepository(s.tmpDir, params.RepoUID, repo.Path) shared, err := api.NewSharedRepo(s.git, s.tmpDir, params.RepoUID, repoPath)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create shared repository: %w", err) return sha.None, fmt.Errorf("failed to create shared repository: %w", err)
} }
defer shared.Close(ctx) defer shared.Close(ctx)
// Create bare repository with alternates pointing to the original repository. // Create bare repository with alternates pointing to the original repository.
err = shared.InitAsShared(ctx) err = shared.InitAsShared(ctx)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create temp repo with alternates: %w", err) return sha.None, fmt.Errorf("failed to create temp repo with alternates: %w", err)
} }
log.Debug().Msgf("prepare tree (empty: %t)", isEmpty) log.Debug().Msgf("prepare tree (empty: %t)", isEmpty)
@ -171,15 +162,15 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
if isEmpty { if isEmpty {
err = s.prepareTreeEmptyRepo(ctx, shared, params.Actions) err = s.prepareTreeEmptyRepo(ctx, shared, params.Actions)
} else { } else {
err = shared.SetIndex(ctx, oldCommitSHA) err = shared.SetIndex(ctx, oldCommitSHA.String())
if err != nil { if err != nil {
return "", fmt.Errorf("failed to set index to temp repo: %w", err) return sha.None, fmt.Errorf("failed to set index to temp repo: %w", err)
} }
err = s.prepareTree(ctx, shared, params.Actions, commit) err = s.prepareTree(ctx, shared, params.Actions, commit)
} }
if err != nil { if err != nil {
return "", fmt.Errorf("failed to prepare tree: %w", err) return sha.None, fmt.Errorf("failed to prepare tree: %w", err)
} }
log.Debug().Msg("write tree") log.Debug().Msg("write tree")
@ -187,7 +178,7 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
// Now write the tree // Now write the tree
treeHash, err := shared.WriteTree(ctx) treeHash, err := shared.WriteTree(ctx)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to write tree object: %w", err) return sha.None, fmt.Errorf("failed to write tree object: %w", err)
} }
message := strings.TrimSpace(params.Title) message := strings.TrimSpace(params.Title)
@ -201,11 +192,11 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
commitSHA, err := shared.CommitTreeWithDate( commitSHA, err := shared.CommitTreeWithDate(
ctx, ctx,
oldCommitSHA, oldCommitSHA,
&types.Identity{ &api.Identity{
Name: author.Name, Name: author.Name,
Email: author.Email, Email: author.Email,
}, },
&types.Identity{ &api.Identity{
Name: committer.Name, Name: committer.Name,
Email: committer.Email, Email: committer.Email,
}, },
@ -216,12 +207,12 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
committerDate, committerDate,
) )
if err != nil { if err != nil {
return "", fmt.Errorf("failed to commit the tree: %w", err) return sha.None, fmt.Errorf("failed to commit the tree: %w", err)
} }
err = shared.MoveObjects(ctx) err = shared.MoveObjects(ctx)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to move git objects: %w", err) return sha.None, fmt.Errorf("failed to move git objects: %w", err)
} }
return commitSHA, nil return commitSHA, nil
@ -232,13 +223,13 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
log.Debug().Msg("update ref") log.Debug().Msg("update ref")
branchRef := adapter.GetReferenceFromBranchName(params.Branch) branchRef := api.GetReferenceFromBranchName(params.Branch)
if params.Branch != params.NewBranch { if params.Branch != params.NewBranch {
// we are creating a new branch, rather than updating the existing one // we are creating a new branch, rather than updating the existing one
oldCommitSHA = types.NilSHA oldCommitSHA = sha.Nil
branchRef = adapter.GetReferenceFromBranchName(params.NewBranch) branchRef = api.GetReferenceFromBranchName(params.NewBranch)
} }
err = s.adapter.UpdateRef( err = s.git.UpdateRef(
ctx, ctx,
params.EnvVars, params.EnvVars,
repoPath, repoPath,
@ -252,23 +243,24 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
log.Debug().Msg("get commit") log.Debug().Msg("get commit")
commit, err = repo.GetCommit(newCommitSHA) commit, err = s.git.GetCommit(ctx, repoPath, newCommitSHA.String())
if err != nil { if err != nil {
return CommitFilesResponse{}, fmt.Errorf("failed to get commit for SHA %s: %w", newCommitSHA, err) return CommitFilesResponse{}, fmt.Errorf("failed to get commit for SHA %s: %w",
newCommitSHA.String(), err)
} }
log.Debug().Msg("done") log.Debug().Msg("done")
return CommitFilesResponse{ return CommitFilesResponse{
CommitID: commit.ID.String(), CommitID: commit.SHA,
}, nil }, nil
} }
func (s *Service) prepareTree( func (s *Service) prepareTree(
ctx context.Context, ctx context.Context,
shared *adapter.SharedRepo, shared *api.SharedRepo,
actions []CommitFileAction, actions []CommitFileAction,
commit *git.Commit, commit *api.Commit,
) error { ) error {
// execute all actions // execute all actions
for i := range actions { for i := range actions {
@ -282,7 +274,7 @@ func (s *Service) prepareTree(
func (s *Service) prepareTreeEmptyRepo( func (s *Service) prepareTreeEmptyRepo(
ctx context.Context, ctx context.Context,
shared *adapter.SharedRepo, shared *api.SharedRepo,
actions []CommitFileAction, actions []CommitFileAction,
) error { ) error {
for _, action := range actions { for _, action := range actions {
@ -290,7 +282,7 @@ func (s *Service) prepareTreeEmptyRepo(
return errors.PreconditionFailed("action not allowed on empty repository") return errors.PreconditionFailed("action not allowed on empty repository")
} }
filePath := files.CleanUploadFileName(action.Path) filePath := api.CleanUploadFileName(action.Path)
if filePath == "" { if filePath == "" {
return errors.InvalidArgument("invalid path") return errors.InvalidArgument("invalid path")
} }
@ -304,12 +296,13 @@ func (s *Service) prepareTreeEmptyRepo(
} }
func (s *Service) validateAndPrepareHeader( func (s *Service) validateAndPrepareHeader(
repo *git.Repository, ctx context.Context,
repoPath string,
isEmpty bool, isEmpty bool,
params *CommitFilesParams, params *CommitFilesParams,
) (*git.Commit, error) { ) (*api.Commit, error) {
if params.Branch == "" { if params.Branch == "" {
defaultBranchRef, err := repo.GetDefaultBranch() defaultBranchRef, err := s.git.GetDefaultBranch(ctx, repoPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get default branch: %w", err) return nil, fmt.Errorf("failed to get default branch: %w", err)
} }
@ -330,37 +323,32 @@ func (s *Service) validateAndPrepareHeader(
} }
// ensure source branch exists // ensure source branch exists
branch, err := repo.GetBranch(params.Branch) branch, err := s.git.GetBranch(ctx, repoPath, params.Branch)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get source branch '%s': %w", params.Branch, err) return nil, fmt.Errorf("failed to get source branch '%s': %w", params.Branch, err)
} }
// ensure new branch doesn't exist yet (if new branch creation was requested) // ensure new branch doesn't exist yet (if new branch creation was requested)
if params.Branch != params.NewBranch { if params.Branch != params.NewBranch {
existingBranch, err := repo.GetBranch(params.NewBranch) existingBranch, err := s.git.GetBranch(ctx, repoPath, params.NewBranch)
if existingBranch != nil { if existingBranch != nil {
return nil, errors.Conflict("branch %s already exists", existingBranch.Name) return nil, errors.Conflict("branch %s already exists", existingBranch.Name)
} }
if err != nil && !git.IsErrBranchNotExist(err) { if err != nil && !errors.IsNotFound(err) {
return nil, fmt.Errorf("failed to create new branch '%s': %w", params.NewBranch, err) return nil, fmt.Errorf("failed to create new branch '%s': %w", params.NewBranch, err)
} }
} }
commit, err := branch.GetCommit() return branch.Commit, nil
if err != nil {
return nil, fmt.Errorf("failed to get branch commit: %w", err)
}
return commit, nil
} }
func (s *Service) processAction( func (s *Service) processAction(
ctx context.Context, ctx context.Context,
shared SharedRepo, shared *api.SharedRepo,
action *CommitFileAction, action *CommitFileAction,
commit *git.Commit, commit *api.Commit,
) (err error) { ) (err error) {
filePath := files.CleanUploadFileName(action.Path) filePath := api.CleanUploadFileName(action.Path)
if filePath == "" { if filePath == "" {
return errors.InvalidArgument("path cannot be empty") return errors.InvalidArgument("path cannot be empty")
} }
@ -379,11 +367,11 @@ func (s *Service) processAction(
return err return err
} }
func createFile(ctx context.Context, repo SharedRepo, commit *git.Commit, func createFile(ctx context.Context, repo *api.SharedRepo, commit *api.Commit,
filePath, mode string, payload []byte) error { filePath, mode string, payload []byte) error {
// only check path availability if a source commit is available (empty repo won't have such a commit) // only check path availability if a source commit is available (empty repo won't have such a commit)
if commit != nil { if commit != nil {
if err := checkPathAvailability(commit, filePath, true); err != nil { if err := checkPathAvailability(ctx, repo, commit, filePath, true); err != nil {
return err return err
} }
} }
@ -394,16 +382,23 @@ func createFile(ctx context.Context, repo SharedRepo, commit *git.Commit,
} }
// Add the object to the index // Add the object to the index
if err = repo.AddObjectToIndex(ctx, mode, hash, filePath); err != nil { if err = repo.AddObjectToIndex(ctx, mode, hash.String(), filePath); err != nil {
return fmt.Errorf("createFile: error creating object: %w", err) return fmt.Errorf("createFile: error creating object: %w", err)
} }
return nil return nil
} }
func updateFile(ctx context.Context, repo SharedRepo, commit *git.Commit, filePath, sha, func updateFile(
mode string, payload []byte) error { ctx context.Context,
repo *api.SharedRepo,
commit *api.Commit,
filePath string,
sha string,
mode string,
payload []byte,
) error {
// get file mode from existing file (default unless executable) // get file mode from existing file (default unless executable)
entry, err := getFileEntry(commit, sha, filePath) entry, err := getFileEntry(ctx, repo, commit, sha, filePath)
if err != nil { if err != nil {
return err return err
} }
@ -416,27 +411,34 @@ func updateFile(ctx context.Context, repo SharedRepo, commit *git.Commit, filePa
return fmt.Errorf("updateFile: error hashing object: %w", err) return fmt.Errorf("updateFile: error hashing object: %w", err)
} }
if err = repo.AddObjectToIndex(ctx, mode, hash, filePath); err != nil { if err = repo.AddObjectToIndex(ctx, mode, hash.String(), filePath); err != nil {
return fmt.Errorf("updateFile: error updating object: %w", err) return fmt.Errorf("updateFile: error updating object: %w", err)
} }
return nil return nil
} }
func moveFile(ctx context.Context, repo SharedRepo, commit *git.Commit, func moveFile(
filePath, sha, mode string, payload []byte) error { ctx context.Context,
repo *api.SharedRepo,
commit *api.Commit,
filePath string,
sha string,
mode string,
payload []byte,
) error {
newPath, newContent, err := parseMovePayload(payload) newPath, newContent, err := parseMovePayload(payload)
if err != nil { if err != nil {
return err return err
} }
// ensure file exists and matches SHA // ensure file exists and matches SHA
entry, err := getFileEntry(commit, sha, filePath) entry, err := getFileEntry(ctx, repo, commit, sha, filePath)
if err != nil { if err != nil {
return err return err
} }
// ensure new path is available // ensure new path is available
if err = checkPathAvailability(commit, newPath, false); err != nil { if err = checkPathAvailability(ctx, repo, commit, newPath, false); err != nil {
return err return err
} }
@ -448,14 +450,14 @@ func moveFile(ctx context.Context, repo SharedRepo, commit *git.Commit,
return fmt.Errorf("moveFile: error hashing object: %w", err) return fmt.Errorf("moveFile: error hashing object: %w", err)
} }
fileHash = hash fileHash = hash.String()
fileMode = mode fileMode = mode
if entry.IsExecutable() { if entry.IsExecutable() {
fileMode = "100755" fileMode = "100755"
} }
} else { } else {
fileHash = entry.ID.String() fileHash = entry.SHA.String()
fileMode = entry.Mode().String() fileMode = entry.Mode.String()
} }
if err = repo.AddObjectToIndex(ctx, fileMode, fileHash, newPath); err != nil { if err = repo.AddObjectToIndex(ctx, fileMode, fileHash, newPath); err != nil {
@ -468,7 +470,7 @@ func moveFile(ctx context.Context, repo SharedRepo, commit *git.Commit,
return nil return nil
} }
func deleteFile(ctx context.Context, repo SharedRepo, filePath string) error { func deleteFile(ctx context.Context, repo *api.SharedRepo, filePath string) error {
filesInIndex, err := repo.LsFiles(ctx, filePath) filesInIndex, err := repo.LsFiles(ctx, filePath)
if err != nil { if err != nil {
return fmt.Errorf("deleteFile: listing files error: %w", err) return fmt.Errorf("deleteFile: listing files error: %w", err)
@ -484,12 +486,14 @@ func deleteFile(ctx context.Context, repo SharedRepo, filePath string) error {
} }
func getFileEntry( func getFileEntry(
commit *git.Commit, ctx context.Context,
repo *api.SharedRepo,
commit *api.Commit,
sha string, sha string,
path string, path string,
) (*git.TreeEntry, error) { ) (*api.TreeNode, error) {
entry, err := commit.GetTreeEntryByPath(path) entry, err := repo.GetTreeNode(ctx, commit.SHA.String(), path)
if git.IsErrNotExist(err) { if errors.IsNotFound(err) {
return nil, errors.NotFound("path %s not found", path) return nil, errors.NotFound("path %s not found", path)
} }
if err != nil { if err != nil {
@ -497,9 +501,9 @@ func getFileEntry(
} }
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
if sha != "" && sha != entry.ID.String() { if sha != "" && sha != entry.SHA.String() {
return nil, errors.InvalidArgument("sha does not match for path %s [given: %s, expected: %s]", return nil, errors.InvalidArgument("sha does not match for path %s [given: %s, expected: %s]",
path, sha, entry.ID.String()) path, sha, entry.SHA)
} }
return entry, nil return entry, nil
@ -510,14 +514,20 @@ func getFileEntry(
// sure no parts of the path are existing files or links except for the last // sure no parts of the path are existing files or links except for the last
// item in the path which is the file name, and that shouldn't exist IF it is // item in the path which is the file name, and that shouldn't exist IF it is
// a new file OR is being moved to a new path. // a new file OR is being moved to a new path.
func checkPathAvailability(commit *git.Commit, filePath string, isNewFile bool) error { func checkPathAvailability(
ctx context.Context,
repo *api.SharedRepo,
commit *api.Commit,
filePath string,
isNewFile bool,
) error {
parts := strings.Split(filePath, "/") parts := strings.Split(filePath, "/")
subTreePath := "" subTreePath := ""
for index, part := range parts { for index, part := range parts {
subTreePath = path.Join(subTreePath, part) subTreePath = path.Join(subTreePath, part)
entry, err := commit.GetTreeEntryByPath(subTreePath) entry, err := repo.GetTreeNode(ctx, commit.SHA.String(), subTreePath)
if err != nil { if err != nil {
if git.IsErrNotExist(err) { if errors.IsNotFound(err) {
// Means there is no item with that name, so we're good // Means there is no item with that name, so we're good
break break
} }
@ -530,8 +540,8 @@ func checkPathAvailability(commit *git.Commit, filePath string, isNewFile bool)
subTreePath) subTreePath)
} }
case entry.IsLink(): case entry.IsLink():
return fmt.Errorf("a symbolic link %w where you're trying to create a subdirectory [path: %s]", return errors.Conflict("a symbolic link already exist where you're trying to create a subdirectory [path: %s]",
types.ErrAlreadyExists, subTreePath) subTreePath)
case entry.IsDir(): case entry.IsDir():
return errors.Conflict("a directory already exists where you're trying to create a subdirectory [path: %s]", return errors.Conflict("a directory already exists where you're trying to create a subdirectory [path: %s]",
subTreePath) subTreePath)
@ -554,9 +564,9 @@ func parseMovePayload(payload []byte) (string, []byte, error) {
newContent = payload[filePathEnd+1:] newContent = payload[filePathEnd+1:]
} }
newPath = files.CleanUploadFileName(newPath) newPath = api.CleanUploadFileName(newPath)
if newPath == "" { if newPath == "" {
return "", nil, types.ErrInvalidPath return "", nil, api.ErrInvalidPath
} }
return newPath, newContent, nil return newPath, newContent, nil

View File

@ -18,34 +18,48 @@ import (
"bufio" "bufio"
"errors" "errors"
"io" "io"
"github.com/harness/gitness/git/types"
) )
type DiffFileHeader struct {
OldFileName string
NewFileName string
Extensions map[string]string
}
type DiffCutParams struct {
LineStart int
LineStartNew bool
LineEnd int
LineEndNew bool
BeforeLines int
AfterLines int
LineLimit int
}
// DiffCut parses git diff output that should consist of a single hunk // DiffCut parses git diff output that should consist of a single hunk
// (usually generated with large value passed to the "--unified" parameter) // (usually generated with large value passed to the "--unified" parameter)
// and returns lines specified with the parameters. // and returns lines specified with the parameters.
// //
//nolint:funlen,gocognit,nestif,gocognit,gocyclo,cyclop // it's actually very readable //nolint:funlen,gocognit,nestif,gocognit,gocyclo,cyclop // it's actually very readable
func DiffCut(r io.Reader, params types.DiffCutParams) (types.HunkHeader, types.Hunk, error) { func DiffCut(r io.Reader, params DiffCutParams) (HunkHeader, Hunk, error) {
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
var err error var err error
var hunkHeader types.HunkHeader var hunkHeader HunkHeader
if _, err = scanFileHeader(scanner); err != nil { if _, err = scanFileHeader(scanner); err != nil {
return types.HunkHeader{}, types.Hunk{}, err return HunkHeader{}, Hunk{}, err
} }
if hunkHeader, err = scanHunkHeader(scanner); err != nil { if hunkHeader, err = scanHunkHeader(scanner); err != nil {
return types.HunkHeader{}, types.Hunk{}, err return HunkHeader{}, Hunk{}, err
} }
currentOldLine := hunkHeader.OldLine currentOldLine := hunkHeader.OldLine
currentNewLine := hunkHeader.NewLine currentNewLine := hunkHeader.NewLine
var inCut bool var inCut bool
var diffCutHeader types.HunkHeader var diffCutHeader HunkHeader
var diffCut []string var diffCut []string
linesBeforeBuf := newStrCircBuf(params.BeforeLines) linesBeforeBuf := newStrCircBuf(params.BeforeLines)
@ -61,7 +75,7 @@ func DiffCut(r io.Reader, params types.DiffCutParams) (types.HunkHeader, types.H
line, action, err = scanHunkLine(scanner) line, action, err = scanHunkLine(scanner)
if err != nil { if err != nil {
return types.HunkHeader{}, types.Hunk{}, err return HunkHeader{}, Hunk{}, err
} }
if line == "" { if line == "" {
@ -103,7 +117,7 @@ func DiffCut(r io.Reader, params types.DiffCutParams) (types.HunkHeader, types.H
} }
if !inCut { if !inCut {
return types.HunkHeader{}, types.Hunk{}, types.ErrHunkNotFound return HunkHeader{}, Hunk{}, ErrHunkNotFound
} }
var ( var (
@ -116,7 +130,7 @@ func DiffCut(r io.Reader, params types.DiffCutParams) (types.HunkHeader, types.H
for i := 0; i < params.AfterLines; i++ { for i := 0; i < params.AfterLines; i++ {
line, _, err := scanHunkLine(scanner) line, _, err := scanHunkLine(scanner)
if err != nil { if err != nil {
return types.HunkHeader{}, types.Hunk{}, err return HunkHeader{}, Hunk{}, err
} }
if line == "" { if line == "" {
break break
@ -149,14 +163,14 @@ func DiffCut(r io.Reader, params types.DiffCutParams) (types.HunkHeader, types.H
} }
} }
return diffCutHeader, types.Hunk{ return diffCutHeader, Hunk{
HunkHeader: diffCutHeaderLines, HunkHeader: diffCutHeaderLines,
Lines: concat(linesBefore, diffCut, linesAfter), Lines: concat(linesBefore, diffCut, linesAfter),
}, nil }, nil
} }
// scanFileHeader keeps reading lines until file header line is read. // scanFileHeader keeps reading lines until file header line is read.
func scanFileHeader(scan *bufio.Scanner) (types.DiffFileHeader, error) { func scanFileHeader(scan *bufio.Scanner) (DiffFileHeader, error) {
for scan.Scan() { for scan.Scan() {
line := scan.Text() line := scan.Text()
if h, ok := ParseDiffFileHeader(line); ok { if h, ok := ParseDiffFileHeader(line); ok {
@ -165,14 +179,14 @@ func scanFileHeader(scan *bufio.Scanner) (types.DiffFileHeader, error) {
} }
if err := scan.Err(); err != nil { if err := scan.Err(); err != nil {
return types.DiffFileHeader{}, err return DiffFileHeader{}, err
} }
return types.DiffFileHeader{}, types.ErrHunkNotFound return DiffFileHeader{}, ErrHunkNotFound
} }
// scanHunkHeader keeps reading lines until hunk header line is read. // scanHunkHeader keeps reading lines until hunk header line is read.
func scanHunkHeader(scan *bufio.Scanner) (types.HunkHeader, error) { func scanHunkHeader(scan *bufio.Scanner) (HunkHeader, error) {
for scan.Scan() { for scan.Scan() {
line := scan.Text() line := scan.Text()
if h, ok := ParseDiffHunkHeader(line); ok { if h, ok := ParseDiffHunkHeader(line); ok {
@ -181,10 +195,10 @@ func scanHunkHeader(scan *bufio.Scanner) (types.HunkHeader, error) {
} }
if err := scan.Err(); err != nil { if err := scan.Err(); err != nil {
return types.HunkHeader{}, err return HunkHeader{}, err
} }
return types.HunkHeader{}, types.ErrHunkNotFound return HunkHeader{}, ErrHunkNotFound
} }
type diffAction byte type diffAction byte
@ -206,7 +220,7 @@ again:
line = scan.Text() line = scan.Text()
if line == "" { if line == "" {
err = types.ErrHunkNotFound // should not happen: empty line in diff output err = ErrHunkNotFound // should not happen: empty line in diff output
return return
} }

View File

@ -18,8 +18,6 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/harness/gitness/git/types"
) )
//nolint:gocognit // it's a unit test!!! //nolint:gocognit // it's a unit test!!!
@ -49,14 +47,14 @@ func TestDiffCut(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
params types.DiffCutParams params DiffCutParams
expCutHeader string expCutHeader string
expCut []string expCut []string
expError error expError error
}{ }{
{ {
name: "at-'+6,7,8':new", name: "at-'+6,7,8':new",
params: types.DiffCutParams{ params: DiffCutParams{
LineStart: 7, LineStartNew: true, LineStart: 7, LineStartNew: true,
LineEnd: 7, LineEndNew: true, LineEnd: 7, LineEndNew: true,
BeforeLines: 0, AfterLines: 0, BeforeLines: 0, AfterLines: 0,
@ -68,7 +66,7 @@ func TestDiffCut(t *testing.T) {
}, },
{ {
name: "at-'+6,7,8':new-with-lines-around", name: "at-'+6,7,8':new-with-lines-around",
params: types.DiffCutParams{ params: DiffCutParams{
LineStart: 7, LineStartNew: true, LineStart: 7, LineStartNew: true,
LineEnd: 7, LineEndNew: true, LineEnd: 7, LineEndNew: true,
BeforeLines: 1, AfterLines: 2, BeforeLines: 1, AfterLines: 2,
@ -80,7 +78,7 @@ func TestDiffCut(t *testing.T) {
}, },
{ {
name: "at-'+0':new-with-lines-around", name: "at-'+0':new-with-lines-around",
params: types.DiffCutParams{ params: DiffCutParams{
LineStart: 1, LineStartNew: true, LineStart: 1, LineStartNew: true,
LineEnd: 1, LineEndNew: true, LineEnd: 1, LineEndNew: true,
BeforeLines: 3, AfterLines: 3, BeforeLines: 3, AfterLines: 3,
@ -92,7 +90,7 @@ func TestDiffCut(t *testing.T) {
}, },
{ {
name: "at-'-13':one-with-lines-around", name: "at-'-13':one-with-lines-around",
params: types.DiffCutParams{ params: DiffCutParams{
LineStart: 13, LineStartNew: false, LineStart: 13, LineStartNew: false,
LineEnd: 13, LineEndNew: false, LineEnd: 13, LineEndNew: false,
BeforeLines: 1, AfterLines: 1, BeforeLines: 1, AfterLines: 1,
@ -104,7 +102,7 @@ func TestDiffCut(t *testing.T) {
}, },
{ {
name: "at-'-13':mixed", name: "at-'-13':mixed",
params: types.DiffCutParams{ params: DiffCutParams{
LineStart: 7, LineStartNew: false, LineStart: 7, LineStartNew: false,
LineEnd: 7, LineEndNew: true, LineEnd: 7, LineEndNew: true,
BeforeLines: 0, AfterLines: 0, BeforeLines: 0, AfterLines: 0,
@ -167,7 +165,7 @@ index 541cb64f..047d7ee2 100644
hh, h, err := DiffCut( hh, h, err := DiffCut(
strings.NewReader(input), strings.NewReader(input),
types.DiffCutParams{ DiffCutParams{
LineStart: 3, LineStart: 3,
LineStartNew: true, LineStartNew: true,
LineEnd: 3, LineEnd: 3,
@ -182,13 +180,13 @@ index 541cb64f..047d7ee2 100644
return return
} }
expectedHH := types.HunkHeader{OldLine: 2, OldSpan: 0, NewLine: 3, NewSpan: 1} expectedHH := HunkHeader{OldLine: 2, OldSpan: 0, NewLine: 3, NewSpan: 1}
if expectedHH != hh { if expectedHH != hh {
t.Errorf("expected hunk header: %+v, but got: %+v", expectedHH, hh) t.Errorf("expected hunk header: %+v, but got: %+v", expectedHH, hh)
} }
expectedHunkLines := types.Hunk{ expectedHunkLines := Hunk{
HunkHeader: types.HunkHeader{OldLine: 2, OldSpan: 0, NewLine: 2, NewSpan: 2}, HunkHeader: HunkHeader{OldLine: 2, OldSpan: 0, NewLine: 2, NewSpan: 2},
Lines: []string{"+456", "+789"}, Lines: []string{"+456", "+789"},
} }
if !reflect.DeepEqual(expectedHunkLines, h) { if !reflect.DeepEqual(expectedHunkLines, h) {
@ -210,7 +208,7 @@ index af7864ba..541cb64f 100644
` `
hh, h, err := DiffCut( hh, h, err := DiffCut(
strings.NewReader(input), strings.NewReader(input),
types.DiffCutParams{ DiffCutParams{
LineStart: 1, LineStart: 1,
LineStartNew: true, LineStartNew: true,
LineEnd: 1, LineEnd: 1,
@ -225,13 +223,13 @@ index af7864ba..541cb64f 100644
return return
} }
expectedHH := types.HunkHeader{OldLine: 1, OldSpan: 3, NewLine: 1, NewSpan: 1} expectedHH := HunkHeader{OldLine: 1, OldSpan: 3, NewLine: 1, NewSpan: 1}
if expectedHH != hh { if expectedHH != hh {
t.Errorf("expected hunk header: %+v, but got: %+v", expectedHH, hh) t.Errorf("expected hunk header: %+v, but got: %+v", expectedHH, hh)
} }
expectedHunkLines := types.Hunk{ expectedHunkLines := Hunk{
HunkHeader: types.HunkHeader{OldLine: 1, OldSpan: 3, NewLine: 1, NewSpan: 1}, HunkHeader: HunkHeader{OldLine: 1, OldSpan: 3, NewLine: 1, NewSpan: 1},
Lines: []string{"-123", "-456", "-789", "+test"}, Lines: []string{"-123", "-456", "-789", "+test"},
} }
if !reflect.DeepEqual(expectedHunkLines, h) { if !reflect.DeepEqual(expectedHunkLines, h) {

View File

@ -20,18 +20,22 @@ import (
"regexp" "regexp"
"github.com/harness/gitness/git/enum" "github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types"
) )
type DiffFileHunkHeaders struct {
FileHeader DiffFileHeader
HunksHeaders []HunkHeader
}
var regExpDiffFileHeader = regexp.MustCompile(`^diff --git a/(.+) b/(.+)$`) var regExpDiffFileHeader = regexp.MustCompile(`^diff --git a/(.+) b/(.+)$`)
func ParseDiffFileHeader(line string) (types.DiffFileHeader, bool) { func ParseDiffFileHeader(line string) (DiffFileHeader, bool) {
groups := regExpDiffFileHeader.FindStringSubmatch(line) groups := regExpDiffFileHeader.FindStringSubmatch(line)
if groups == nil { if groups == nil {
return types.DiffFileHeader{}, false return DiffFileHeader{}, false
} }
return types.DiffFileHeader{ return DiffFileHeader{
OldFileName: groups[1], OldFileName: groups[1],
NewFileName: groups[2], NewFileName: groups[2],
Extensions: map[string]string{}, Extensions: map[string]string{},
@ -64,11 +68,11 @@ func ParseDiffFileExtendedHeader(line string) (string, string) {
// GetHunkHeaders parses git diff output and returns all diff headers for all files. // GetHunkHeaders parses git diff output and returns all diff headers for all files.
// See for documentation: https://git-scm.com/docs/git-diff#generate_patch_text_with_p // See for documentation: https://git-scm.com/docs/git-diff#generate_patch_text_with_p
func GetHunkHeaders(r io.Reader) ([]*types.DiffFileHunkHeaders, error) { func GetHunkHeaders(r io.Reader) ([]*DiffFileHunkHeaders, error) {
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
var currentFile *types.DiffFileHunkHeaders var currentFile *DiffFileHunkHeaders
var result []*types.DiffFileHunkHeaders var result []*DiffFileHunkHeaders
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
@ -77,7 +81,7 @@ func GetHunkHeaders(r io.Reader) ([]*types.DiffFileHunkHeaders, error) {
if currentFile != nil { if currentFile != nil {
result = append(result, currentFile) result = append(result, currentFile)
} }
currentFile = &types.DiffFileHunkHeaders{ currentFile = &DiffFileHunkHeaders{
FileHeader: h, FileHeader: h,
HunksHeaders: nil, HunksHeaders: nil,
} }
@ -87,7 +91,7 @@ func GetHunkHeaders(r io.Reader) ([]*types.DiffFileHunkHeaders, error) {
if currentFile == nil { if currentFile == nil {
// should not happen: we reached the hunk header without first finding the file header. // should not happen: we reached the hunk header without first finding the file header.
return nil, types.ErrHunkNotFound return nil, ErrHunkNotFound
} }
if h, ok := ParseDiffHunkHeader(line); ok { if h, ok := ParseDiffHunkHeader(line); ok {

Some files were not shown because too many files have changed in this diff Show More