mirror of
https://github.com/harness/drone.git
synced 2025-05-05 15:32:56 +00:00
[code-1016] Replace gitea
Usages (not wrapper) (#1063)
This commit is contained in:
parent
8dc82433c5
commit
cecfecdb06
@ -141,7 +141,7 @@ func (c *Controller) Report(
|
||||
|
||||
_, err = c.git.GetCommit(ctx, &git.GetCommitParams{
|
||||
ReadParams: git.ReadParams{RepoUID: repo.GitUID},
|
||||
SHA: commitSHA,
|
||||
Revision: commitSHA,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to commit sha=%s: %w", commitSHA, err)
|
||||
|
@ -99,19 +99,19 @@ func (c *Controller) reportBranchEvent(
|
||||
branchUpdate hook.ReferenceUpdate,
|
||||
) {
|
||||
switch {
|
||||
case branchUpdate.Old == types.NilSHA:
|
||||
case branchUpdate.Old.String() == types.NilSHA:
|
||||
c.gitReporter.BranchCreated(ctx, &events.BranchCreatedPayload{
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: principalID,
|
||||
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{
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: principalID,
|
||||
Ref: branchUpdate.Ref,
|
||||
SHA: branchUpdate.Old,
|
||||
SHA: branchUpdate.Old.String(),
|
||||
})
|
||||
default:
|
||||
result, err := c.git.IsAncestor(ctx, git.IsAncestorParams{
|
||||
@ -132,8 +132,8 @@ func (c *Controller) reportBranchEvent(
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: principalID,
|
||||
Ref: branchUpdate.Ref,
|
||||
OldSHA: branchUpdate.Old,
|
||||
NewSHA: branchUpdate.New,
|
||||
OldSHA: branchUpdate.Old.String(),
|
||||
NewSHA: branchUpdate.New.String(),
|
||||
Forced: forced,
|
||||
})
|
||||
}
|
||||
@ -146,27 +146,27 @@ func (c *Controller) reportTagEvent(
|
||||
tagUpdate hook.ReferenceUpdate,
|
||||
) {
|
||||
switch {
|
||||
case tagUpdate.Old == types.NilSHA:
|
||||
case tagUpdate.Old.String() == types.NilSHA:
|
||||
c.gitReporter.TagCreated(ctx, &events.TagCreatedPayload{
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: principalID,
|
||||
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{
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: principalID,
|
||||
Ref: tagUpdate.Ref,
|
||||
SHA: tagUpdate.Old,
|
||||
SHA: tagUpdate.Old.String(),
|
||||
})
|
||||
default:
|
||||
c.gitReporter.TagUpdated(ctx, &events.TagUpdatedPayload{
|
||||
RepoID: repo.ID,
|
||||
PrincipalID: principalID,
|
||||
Ref: tagUpdate.Ref,
|
||||
OldSHA: tagUpdate.Old,
|
||||
NewSHA: tagUpdate.New,
|
||||
OldSHA: tagUpdate.Old.String(),
|
||||
NewSHA: tagUpdate.New.String(),
|
||||
// tags can only be force updated!
|
||||
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.
|
||||
if len(in.RefUpdates) != 1 ||
|
||||
!strings.HasPrefix(in.RefUpdates[0].Ref, gitReferenceNamePrefixBranch) ||
|
||||
in.RefUpdates[0].New == types.NilSHA {
|
||||
in.RefUpdates[0].New.String() == types.NilSHA {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -188,9 +188,9 @@ type changes struct {
|
||||
|
||||
func (c *changes) groupByAction(refUpdate hook.ReferenceUpdate, name string) {
|
||||
switch {
|
||||
case refUpdate.Old == types.NilSHA:
|
||||
case refUpdate.Old.String() == types.NilSHA:
|
||||
c.created = append(c.created, name)
|
||||
case refUpdate.New == types.NilSHA:
|
||||
case refUpdate.New.String() == types.NilSHA:
|
||||
c.deleted = append(c.deleted, name)
|
||||
default:
|
||||
c.updated = append(c.updated, name)
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
events "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git"
|
||||
gittypes "github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"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.CodeComment = &types.CodeCommentFields{
|
||||
Outdated: falseBool,
|
||||
MergeBaseSHA: cut.MergeBaseSHA,
|
||||
MergeBaseSHA: cut.MergeBaseSHA.String(),
|
||||
SourceSHA: sourceCommitSHA,
|
||||
Path: path,
|
||||
LineNew: cut.Header.NewLine,
|
||||
@ -332,7 +331,7 @@ func (c *Controller) fetchDiffCut(
|
||||
LineEnd: in.LineEnd,
|
||||
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))
|
||||
}
|
||||
if err != nil {
|
||||
@ -351,7 +350,7 @@ func (c *Controller) migrateCodeComment(
|
||||
cut git.DiffCutOutput,
|
||||
) {
|
||||
needsNewLineMigrate := in.SourceCommitSHA != pr.SourceSHA
|
||||
needsOldLineMigrate := cut.MergeBaseSHA != pr.MergeBaseSHA
|
||||
needsOldLineMigrate := cut.MergeBaseSHA.String() != pr.MergeBaseSHA
|
||||
if !needsNewLineMigrate && !needsOldLineMigrate {
|
||||
return
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ func (c *Controller) verifyBranchExistence(ctx context.Context,
|
||||
branch, repo.Identifier, err)
|
||||
}
|
||||
|
||||
return ref.SHA, nil
|
||||
return ref.SHA.String(), nil
|
||||
}
|
||||
|
||||
func (c *Controller) getRepoCheckAccess(ctx context.Context,
|
||||
|
@ -21,8 +21,8 @@ import (
|
||||
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git"
|
||||
gittypes "github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
@ -80,7 +80,7 @@ func (c *Controller) FileViewAdd(
|
||||
Path: in.Path,
|
||||
IncludeLatestCommit: false,
|
||||
})
|
||||
if err != nil && !gittypes.IsPathNotFoundError(err) {
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to get tree node '%s' for provided sha '%s': %w",
|
||||
in.Path,
|
||||
@ -102,7 +102,7 @@ func (c *Controller) FileViewAdd(
|
||||
Path: in.Path,
|
||||
IncludeLatestCommit: false,
|
||||
})
|
||||
if err != nil && !gittypes.IsPathNotFoundError(err) {
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to get tree node '%s' for MergeBaseSHA '%s': %w",
|
||||
in.Path,
|
||||
|
@ -32,9 +32,11 @@ import (
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git"
|
||||
gitenum "github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/gotidy/ptr"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@ -235,27 +237,27 @@ func (c *Controller) Merge(
|
||||
HeadRepoUID: sourceRepo.GitUID,
|
||||
HeadBranch: pr.SourceBranch,
|
||||
RefType: gitenum.RefTypeUndefined, // update no refs -> no commit will be created
|
||||
HeadExpectedSHA: in.SourceSHA,
|
||||
HeadExpectedSHA: sha.Must(in.SourceSHA),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("merge check execution failed: %w", err)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
if len(mergeOutput.ConflictFiles) > 0 {
|
||||
pr.MergeCheckStatus = enum.MergeCheckStatusConflict
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA
|
||||
pr.MergeTargetSHA = &mergeOutput.BaseSHA
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
|
||||
pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
|
||||
pr.MergeSHA = nil
|
||||
pr.MergeConflicts = mergeOutput.ConflictFiles
|
||||
} else {
|
||||
pr.MergeCheckStatus = enum.MergeCheckStatusMergeable
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA
|
||||
pr.MergeTargetSHA = &mergeOutput.BaseSHA
|
||||
pr.MergeSHA = &mergeOutput.MergeSHA
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
|
||||
pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
|
||||
pr.MergeSHA = ptr.String(mergeOutput.MergeSHA.String())
|
||||
pr.MergeConflicts = nil
|
||||
}
|
||||
pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount)
|
||||
@ -345,23 +347,23 @@ func (c *Controller) Merge(
|
||||
AuthorDate: &now,
|
||||
RefType: gitenum.RefTypeBranch,
|
||||
RefName: pr.TargetBranch,
|
||||
HeadExpectedSHA: in.SourceSHA,
|
||||
HeadExpectedSHA: sha.Must(in.SourceSHA),
|
||||
Method: gitenum.MergeMethod(in.Method),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("merge check execution failed: %w", err)
|
||||
}
|
||||
//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 {
|
||||
if pr.SourceSHA != mergeOutput.HeadSHA {
|
||||
if pr.SourceSHA != mergeOutput.HeadSHA.String() {
|
||||
return errors.New("source SHA has changed")
|
||||
}
|
||||
|
||||
// update all Merge specific information
|
||||
pr.MergeCheckStatus = enum.MergeCheckStatusConflict
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA
|
||||
pr.MergeTargetSHA = &mergeOutput.BaseSHA
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
|
||||
pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
|
||||
pr.MergeSHA = nil
|
||||
pr.MergeConflicts = mergeOutput.ConflictFiles
|
||||
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)
|
||||
// since this is the final operation on the PR, we update any sha that might've changed by now.
|
||||
pr.MergeCheckStatus = enum.MergeCheckStatusMergeable
|
||||
pr.SourceSHA = mergeOutput.HeadSHA
|
||||
pr.MergeTargetSHA = &mergeOutput.BaseSHA
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA
|
||||
pr.MergeSHA = &mergeOutput.MergeSHA
|
||||
pr.SourceSHA = mergeOutput.HeadSHA.String()
|
||||
pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
|
||||
pr.MergeSHA = ptr.String(mergeOutput.MergeSHA.String())
|
||||
pr.MergeConflicts = nil
|
||||
pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount)
|
||||
|
||||
@ -421,9 +423,9 @@ func (c *Controller) Merge(
|
||||
pr.ActivitySeq = activitySeqMerge
|
||||
activityPayload := &types.PullRequestActivityPayloadMerge{
|
||||
MergeMethod: in.Method,
|
||||
MergeSHA: mergeOutput.MergeSHA,
|
||||
TargetSHA: mergeOutput.BaseSHA,
|
||||
SourceSHA: mergeOutput.HeadSHA,
|
||||
MergeSHA: mergeOutput.MergeSHA.String(),
|
||||
TargetSHA: mergeOutput.BaseSHA.String(),
|
||||
SourceSHA: mergeOutput.HeadSHA.String(),
|
||||
}
|
||||
if _, errAct := c.activityStore.CreateWithPayload(ctx, pr, session.Principal.ID, activityPayload); errAct != nil {
|
||||
// non-critical error
|
||||
@ -433,9 +435,9 @@ func (c *Controller) Merge(
|
||||
c.eventReporter.Merged(ctx, &pullreqevents.MergedPayload{
|
||||
Base: eventBase(pr, &session.Principal),
|
||||
MergeMethod: in.Method,
|
||||
MergeSHA: mergeOutput.MergeSHA,
|
||||
TargetSHA: mergeOutput.BaseSHA,
|
||||
SourceSHA: mergeOutput.HeadSHA,
|
||||
MergeSHA: mergeOutput.MergeSHA.String(),
|
||||
TargetSHA: mergeOutput.BaseSHA.String(),
|
||||
SourceSHA: mergeOutput.HeadSHA.String(),
|
||||
})
|
||||
|
||||
var branchDeleted bool
|
||||
@ -467,7 +469,7 @@ func (c *Controller) Merge(
|
||||
}
|
||||
|
||||
return &types.MergeResponse{
|
||||
SHA: mergeOutput.MergeSHA,
|
||||
SHA: mergeOutput.MergeSHA.String(),
|
||||
BranchDeleted: branchDeleted,
|
||||
RuleViolations: violations,
|
||||
}, nil, nil
|
||||
|
@ -95,7 +95,7 @@ func (c *Controller) Create(
|
||||
|
||||
mergeBaseSHA := mergeBaseResult.MergeBaseSHA
|
||||
|
||||
if mergeBaseSHA == sourceSHA {
|
||||
if mergeBaseSHA.String() == sourceSHA {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"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/enum"
|
||||
)
|
||||
|
@ -129,7 +129,7 @@ func (c *Controller) State(ctx context.Context,
|
||||
return nil, fmt.Errorf("failed to find merge base: %w", err)
|
||||
}
|
||||
|
||||
mergeBaseSHA = mergeBaseResult.MergeBaseSHA
|
||||
mergeBaseSHA = mergeBaseResult.MergeBaseSHA.String()
|
||||
|
||||
stateChange = changeReopen
|
||||
} else if pr.State == enum.PullReqStateOpen && in.State != enum.PullReqStateOpen {
|
||||
|
@ -82,7 +82,7 @@ func (c *Controller) ReviewSubmit(
|
||||
|
||||
commit, err := c.git.GetCommit(ctx, &git.GetCommitParams{
|
||||
ReadParams: git.ReadParams{RepoUID: repo.GitUID},
|
||||
SHA: in.CommitSHA,
|
||||
Revision: in.CommitSHA,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get git branch sha: %w", err)
|
||||
@ -101,7 +101,7 @@ func (c *Controller) ReviewSubmit(
|
||||
Updated: now,
|
||||
PullReqID: pr.ID,
|
||||
Decision: in.Decision,
|
||||
SHA: commitSHA,
|
||||
SHA: commitSHA.String(),
|
||||
}
|
||||
|
||||
err = c.reviewStore.Create(ctx, review)
|
||||
@ -114,7 +114,7 @@ func (c *Controller) ReviewSubmit(
|
||||
ReviewerID: review.CreatedBy,
|
||||
})
|
||||
|
||||
_, err = c.updateReviewer(ctx, session, pr, review, commitSHA)
|
||||
_, err = c.updateReviewer(ctx, session, pr, review, commitSHA.String())
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@ -127,7 +127,7 @@ func (c *Controller) ReviewSubmit(
|
||||
}
|
||||
|
||||
payload := &types.PullRequestActivityPayloadReviewSubmit{
|
||||
CommitSHA: commitSHA,
|
||||
CommitSHA: commitSHA.String(),
|
||||
Decision: in.Decision,
|
||||
}
|
||||
_, err = c.activityStore.CreateWithPayload(ctx, pr, session.Principal.ID, payload)
|
||||
|
@ -158,7 +158,7 @@ func (c *Controller) CommitFiles(ctx context.Context,
|
||||
}
|
||||
|
||||
return types.CommitFilesResponse{
|
||||
CommitID: commit.CommitID,
|
||||
CommitID: commit.CommitID.String(),
|
||||
RuleViolations: violations,
|
||||
}, nil, nil
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"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/enum"
|
||||
)
|
||||
@ -58,7 +58,7 @@ func (c *Controller) CommitDiff(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
repoRef string,
|
||||
sha string,
|
||||
rev string,
|
||||
w io.Writer,
|
||||
) error {
|
||||
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{
|
||||
ReadParams: git.CreateReadParams(repo),
|
||||
SHA: sha,
|
||||
Revision: rev,
|
||||
}, w)
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ func (c *Controller) GetCommit(ctx context.Context,
|
||||
|
||||
rpcOut, err := c.git.GetCommit(ctx, &git.GetCommitParams{
|
||||
ReadParams: git.CreateReadParams(repo),
|
||||
SHA: sha,
|
||||
Revision: sha,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit: %w", err)
|
||||
|
@ -106,7 +106,7 @@ func mapBranch(b git.Branch) (Branch, error) {
|
||||
}
|
||||
return Branch{
|
||||
Name: b.Name,
|
||||
SHA: b.SHA,
|
||||
SHA: b.SHA.String(),
|
||||
Commit: commit,
|
||||
}, nil
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ func mapCommitTag(t git.CommitTag) (CommitTag, error) {
|
||||
|
||||
return CommitTag{
|
||||
Name: t.Name,
|
||||
SHA: t.SHA,
|
||||
SHA: t.SHA.String(),
|
||||
IsAnnotated: t.IsAnnotated,
|
||||
Title: t.Title,
|
||||
Message: t.Message,
|
||||
|
@ -31,7 +31,7 @@ func (c *Controller) Raw(ctx context.Context,
|
||||
session *auth.Session,
|
||||
repoRef string,
|
||||
gitRef string,
|
||||
repoPath string,
|
||||
path string,
|
||||
) (io.ReadCloser, int64, error) {
|
||||
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true)
|
||||
if err != nil {
|
||||
@ -48,7 +48,7 @@ func (c *Controller) Raw(ctx context.Context,
|
||||
treeNodeOutput, err := c.git.GetTreeNode(ctx, &git.GetTreeNodeParams{
|
||||
ReadParams: readParams,
|
||||
GitREF: gitRef,
|
||||
Path: repoPath,
|
||||
Path: path,
|
||||
IncludeLatestCommit: false,
|
||||
})
|
||||
if err != nil {
|
||||
@ -59,7 +59,7 @@ func (c *Controller) Raw(ctx context.Context,
|
||||
if treeNodeOutput.Node.Type != git.TreeNodeTypeBlob {
|
||||
return nil, 0, usererror.BadRequestf(
|
||||
"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{
|
||||
|
@ -101,9 +101,14 @@ func MapCommit(c *git.Commit) (*types.Commit, error) {
|
||||
deletions += stat.Deletions
|
||||
}
|
||||
|
||||
parentSHAs := make([]string, len(c.ParentSHAs))
|
||||
for i, sha := range c.ParentSHAs {
|
||||
parentSHAs[i] = sha.String()
|
||||
}
|
||||
|
||||
return &types.Commit{
|
||||
SHA: c.SHA,
|
||||
ParentSHAs: c.ParentSHAs,
|
||||
SHA: c.SHA.String(),
|
||||
ParentSHAs: parentSHAs,
|
||||
Title: c.Title,
|
||||
Message: c.Message,
|
||||
Author: *author,
|
||||
@ -145,8 +150,8 @@ func MapRenameDetails(c *git.RenameDetails) *types.RenameDetails {
|
||||
return &types.RenameDetails{
|
||||
OldPath: c.OldPath,
|
||||
NewPath: c.NewPath,
|
||||
CommitShaBefore: c.CommitShaBefore,
|
||||
CommitShaAfter: c.CommitShaAfter,
|
||||
CommitShaBefore: c.CommitShaBefore.String(),
|
||||
CommitShaAfter: c.CommitShaAfter.String(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
"github.com/harness/gitness/app/api/render"
|
||||
"github.com/harness/gitness/app/api/request"
|
||||
"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.
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
"github.com/harness/gitness/app/api/render"
|
||||
"github.com/harness/gitness/app/api/request"
|
||||
"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.
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
"github.com/harness/gitness/app/api/request"
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"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/enum"
|
||||
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"github.com/harness/gitness/app/services/protection"
|
||||
"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/enum"
|
||||
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/enum"
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/api"
|
||||
)
|
||||
|
||||
func TestGetFileDiffRequestsFromQuery(t *testing.T) {
|
||||
@ -30,7 +30,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantFiles types.FileDiffRequests
|
||||
wantFiles api.FileDiffRequests
|
||||
}{
|
||||
{
|
||||
name: "full range",
|
||||
@ -42,7 +42,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
wantFiles: types.FileDiffRequests{
|
||||
wantFiles: api.FileDiffRequests{
|
||||
{
|
||||
Path: "file.txt",
|
||||
StartLine: 1,
|
||||
@ -60,7 +60,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
wantFiles: types.FileDiffRequests{
|
||||
wantFiles: api.FileDiffRequests{
|
||||
{
|
||||
Path: "file.txt",
|
||||
StartLine: 1,
|
||||
@ -77,7 +77,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
wantFiles: types.FileDiffRequests{
|
||||
wantFiles: api.FileDiffRequests{
|
||||
{
|
||||
Path: "file.txt",
|
||||
EndLine: 20,
|
||||
@ -94,7 +94,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
wantFiles: types.FileDiffRequests{
|
||||
wantFiles: api.FileDiffRequests{
|
||||
{
|
||||
Path: "file.txt",
|
||||
EndLine: 20,
|
||||
@ -119,7 +119,7 @@ func TestGetFileDiffRequestsFromQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
wantFiles: types.FileDiffRequests{
|
||||
wantFiles: api.FileDiffRequests{
|
||||
{
|
||||
Path: "file.txt",
|
||||
EndLine: 20,
|
||||
|
@ -24,7 +24,6 @@ import (
|
||||
"github.com/harness/gitness/app/services/webhook"
|
||||
"github.com/harness/gitness/blob"
|
||||
"github.com/harness/gitness/errors"
|
||||
gittypes "github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/lock"
|
||||
"github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types/check"
|
||||
@ -40,7 +39,6 @@ func Translate(ctx context.Context, err error) *Error {
|
||||
maxBytesErr *http.MaxBytesError
|
||||
codeOwnersTooLargeError *codeowners.TooLargeError
|
||||
lockError *lock.Error
|
||||
pathNotFoundError *gittypes.PathNotFoundError
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
// git errors
|
||||
case errors.As(err, &pathNotFoundError):
|
||||
return Newf(
|
||||
http.StatusNotFound,
|
||||
pathNotFoundError.Error(),
|
||||
)
|
||||
|
||||
// application errors
|
||||
case errors.As(err, &appError):
|
||||
if appError.Err != nil {
|
||||
log.Ctx(ctx).Warn().Err(appError.Err).Msgf("Application error translation is omitting internal details.")
|
||||
|
@ -57,14 +57,14 @@ func (f *service) FindRef(
|
||||
func (f *service) FindCommit(
|
||||
ctx context.Context,
|
||||
repo *types.Repository,
|
||||
sha string,
|
||||
rawSHA string,
|
||||
) (*types.Commit, error) {
|
||||
readParams := git.ReadParams{
|
||||
RepoUID: repo.GitUID,
|
||||
}
|
||||
commitOutput, err := f.git.GetCommit(ctx, &git.GetCommitParams{
|
||||
ReadParams: readParams,
|
||||
SHA: sha,
|
||||
Revision: rawSHA,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git"
|
||||
gittypes "github.com/harness/gitness/git/types"
|
||||
gitness_store "github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
@ -237,7 +236,7 @@ func (s *Service) getCodeOwnerFile(
|
||||
|
||||
return &File{
|
||||
Content: string(content),
|
||||
SHA: output.SHA,
|
||||
SHA: output.SHA.String(),
|
||||
TotalSize: output.Size,
|
||||
}, nil
|
||||
}
|
||||
@ -256,7 +255,7 @@ func (s *Service) getCodeOwnerFileNode(
|
||||
Path: path,
|
||||
})
|
||||
|
||||
if gittypes.IsPathNotFoundError(err) {
|
||||
if errors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -97,7 +97,7 @@ func (s *Service) triggerPREventOnBranchUpdate(ctx context.Context,
|
||||
|
||||
pr.Edited = time.Now().UnixMilli()
|
||||
pr.SourceSHA = event.Payload.NewSHA
|
||||
pr.MergeBaseSHA = newMergeBase
|
||||
pr.MergeBaseSHA = newMergeBase.String()
|
||||
|
||||
// reset merge-check fields for new run
|
||||
pr.MergeCheckStatus = enum.MergeCheckStatusUnchecked
|
||||
@ -137,7 +137,7 @@ func (s *Service) triggerPREventOnBranchUpdate(ctx context.Context,
|
||||
OldSHA: event.Payload.OldSHA,
|
||||
NewSHA: event.Payload.NewSHA,
|
||||
OldMergeBaseSHA: oldMergeBase,
|
||||
NewMergeBaseSHA: newMergeBase,
|
||||
NewMergeBaseSHA: newMergeBase.String(),
|
||||
Forced: event.Payload.Forced,
|
||||
})
|
||||
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/harness/gitness/events"
|
||||
"github.com/harness/gitness/git"
|
||||
gitenum "github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
// createHeadRefOnCreated handles pull request Created events.
|
||||
@ -46,8 +47,8 @@ func (s *Service) createHeadRefOnCreated(ctx context.Context,
|
||||
WriteParams: writeParams,
|
||||
Name: strconv.Itoa(int(event.Payload.Number)),
|
||||
Type: gitenum.RefTypePullReqHead,
|
||||
NewValue: event.Payload.SourceSHA,
|
||||
OldValue: "", // this is a new pull request, so we expect that the ref doesn't exist
|
||||
NewValue: sha.Must(event.Payload.SourceSHA),
|
||||
OldValue: sha.None, // this is a new pull request, so we expect that the ref doesn't exist
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update PR head ref: %w", err)
|
||||
@ -77,8 +78,8 @@ func (s *Service) updateHeadRefOnBranchUpdate(ctx context.Context,
|
||||
WriteParams: writeParams,
|
||||
Name: strconv.Itoa(int(event.Payload.Number)),
|
||||
Type: gitenum.RefTypePullReqHead,
|
||||
NewValue: event.Payload.NewSHA,
|
||||
OldValue: event.Payload.OldSHA,
|
||||
NewValue: sha.Must(event.Payload.NewSHA),
|
||||
OldValue: sha.Must(event.Payload.OldSHA),
|
||||
})
|
||||
if err != nil {
|
||||
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,
|
||||
Name: strconv.Itoa(int(event.Payload.Number)),
|
||||
Type: gitenum.RefTypePullReqHead,
|
||||
NewValue: event.Payload.SourceSHA,
|
||||
OldValue: "", // the request is re-opened, so anything can be the old value
|
||||
NewValue: sha.Must(event.Payload.SourceSHA),
|
||||
OldValue: sha.None, // the request is re-opened, so anything can be the old value
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update PR head ref after pull request reopen: %w", err)
|
||||
|
@ -25,10 +25,12 @@ import (
|
||||
"github.com/harness/gitness/events"
|
||||
"github.com/harness/gitness/git"
|
||||
gitenum "github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/pubsub"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/gotidy/ptr"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@ -109,8 +111,8 @@ func (s *Service) deleteMergeRef(ctx context.Context, repoID int64, prNum int64)
|
||||
WriteParams: writeParams,
|
||||
Name: strconv.Itoa(int(prNum)),
|
||||
Type: gitenum.RefTypePullReqMerge,
|
||||
NewValue: "", // when NewValue is empty will delete the ref.
|
||||
OldValue: "", // we don't care about the old value
|
||||
NewValue: sha.None, // when NewValue is empty will delete the ref.
|
||||
OldValue: sha.None, // we don't care about the old value
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove PR merge ref: %w", err)
|
||||
@ -205,7 +207,7 @@ func (s *Service) updateMergeDataInner(
|
||||
HeadBranch: pr.SourceBranch,
|
||||
RefType: gitenum.RefTypePullReqMerge,
|
||||
RefName: strconv.Itoa(int(pr.Number)),
|
||||
HeadExpectedSHA: newSHA,
|
||||
HeadExpectedSHA: sha.Must(newSHA),
|
||||
Force: true,
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if mergeOutput.MergeSHA == "" || len(mergeOutput.ConflictFiles) > 0 {
|
||||
if mergeOutput.MergeSHA.IsEmpty() || len(mergeOutput.ConflictFiles) > 0 {
|
||||
pr.MergeCheckStatus = enum.MergeCheckStatusConflict
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA
|
||||
pr.MergeTargetSHA = &mergeOutput.BaseSHA
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
|
||||
pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
|
||||
pr.MergeSHA = nil
|
||||
pr.MergeConflicts = mergeOutput.ConflictFiles
|
||||
} else {
|
||||
pr.MergeCheckStatus = enum.MergeCheckStatusMergeable
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA
|
||||
pr.MergeTargetSHA = &mergeOutput.BaseSHA
|
||||
pr.MergeSHA = &mergeOutput.MergeSHA
|
||||
pr.MergeBaseSHA = mergeOutput.MergeBaseSHA.String()
|
||||
pr.MergeTargetSHA = ptr.String(mergeOutput.BaseSHA.String())
|
||||
pr.MergeSHA = ptr.String(mergeOutput.MergeSHA.String())
|
||||
pr.MergeConflicts = nil
|
||||
}
|
||||
pr.Stats.DiffStats = types.NewDiffStats(mergeOutput.CommitCount, mergeOutput.ChangedFileCount)
|
||||
|
@ -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{
|
||||
ReadParams: git.ReadParams{
|
||||
RepoUID: repoUID,
|
||||
},
|
||||
SHA: sha,
|
||||
Revision: commitSHA,
|
||||
})
|
||||
|
||||
if errors.AsStatus(err) == errors.StatusNotFound {
|
||||
// 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.
|
||||
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 {
|
||||
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
|
||||
|
@ -208,7 +208,7 @@ func commitInfoFrom(commit git.Commit) CommitInfo {
|
||||
}
|
||||
|
||||
return CommitInfo{
|
||||
SHA: commit.SHA,
|
||||
SHA: commit.SHA.String(),
|
||||
Message: commit.Message,
|
||||
Author: signatureInfoFrom(commit.Author),
|
||||
Committer: signatureInfoFrom(commit.Committer),
|
||||
|
@ -78,7 +78,7 @@ import (
|
||||
"github.com/harness/gitness/encrypt"
|
||||
"github.com/harness/gitness/events"
|
||||
"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/job"
|
||||
"github.com/harness/gitness/livelog"
|
||||
@ -126,7 +126,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
|
||||
pullreqevents.WireSet,
|
||||
repoevents.WireSet,
|
||||
storage.WireSet,
|
||||
adapter.WireSet,
|
||||
api.WireSet,
|
||||
cliserver.ProvideGitConfig,
|
||||
git.WireSet,
|
||||
store.WireSet,
|
||||
|
@ -77,7 +77,7 @@ import (
|
||||
"github.com/harness/gitness/encrypt"
|
||||
"github.com/harness/gitness/events"
|
||||
"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/job"
|
||||
"github.com/harness/gitness/livelog"
|
||||
@ -135,17 +135,17 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheCache, err := adapter.ProvideLastCommitCache(typesConfig, universalClient)
|
||||
cacheCache, err := api.ProvideLastCommitCache(typesConfig, universalClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientFactory := githook.ProvideFactory()
|
||||
gitAdapter, err := git.ProvideGITAdapter(typesConfig, cacheCache, clientFactory)
|
||||
apiGit, err := git.ProvideGITAdapter(typesConfig, cacheCache, clientFactory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storageStore := storage.ProvideLocalStore()
|
||||
gitInterface, err := git.ProvideService(typesConfig, gitAdapter, storageStore)
|
||||
gitInterface, err := git.ProvideService(typesConfig, apiGit, storageStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
155
git/adapter.go
155
git/adapter.go
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -12,39 +12,26 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/harness/gitness/cache"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"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
|
||||
lastCommitCache cache.Cache[CommitEntryKey, *types.Commit]
|
||||
lastCommitCache cache.Cache[CommitEntryKey, *Commit]
|
||||
githookFactory hook.ClientFactory
|
||||
}
|
||||
|
||||
func New(
|
||||
config types.Config,
|
||||
lastCommitCache cache.Cache[CommitEntryKey, *types.Commit],
|
||||
lastCommitCache cache.Cache[CommitEntryKey, *Commit],
|
||||
githookFactory hook.ClientFactory,
|
||||
) (Adapter, error) {
|
||||
// TODO: should be subdir of gitRoot? What is it being used for?
|
||||
setting.Git.HomePath = "home"
|
||||
|
||||
err := gitea.InitSimple(context.Background())
|
||||
if err != nil {
|
||||
return Adapter{}, err
|
||||
}
|
||||
|
||||
return Adapter{
|
||||
) (*Git, error) {
|
||||
return &Git{
|
||||
traceGit: config.Trace,
|
||||
lastCommitCache: lastCommitCache,
|
||||
githookFactory: githookFactory,
|
@ -12,14 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -70,19 +70,19 @@ func testParseSignatureFromCatFileLineFor(t *testing.T, name string, email strin
|
||||
|
||||
func TestParseTagDataFromCatFile(t *testing.T) {
|
||||
when, _ := time.Parse(defaultGitTimeLayout, "Fri Sep 23 10:57:49 2022 -0700")
|
||||
testParseTagDataFromCatFileFor(t, "sha012", types.GitObjectTypeTag, "name1",
|
||||
types.Signature{Identity: types.Identity{Name: "max", Email: "max@mail.com"}, When: when},
|
||||
testParseTagDataFromCatFileFor(t, sha.EmptyTree, GitObjectTypeTag, "name1",
|
||||
Signature{Identity: Identity{Name: "max", Email: "max@mail.com"}, When: when},
|
||||
"some message", "some message")
|
||||
|
||||
// test with signature
|
||||
testParseTagDataFromCatFileFor(t, "sha012", types.GitObjectTypeCommit, "name2",
|
||||
types.Signature{Identity: types.Identity{Name: "max", Email: "max@mail.com"}, When: when},
|
||||
testParseTagDataFromCatFileFor(t, sha.EmptyTree, GitObjectTypeCommit, "name2",
|
||||
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",
|
||||
"some message")
|
||||
}
|
||||
|
||||
func testParseTagDataFromCatFileFor(t *testing.T, object string, typ types.GitObjectType, name string,
|
||||
tagger types.Signature, remainder string, expectedMessage string) {
|
||||
func testParseTagDataFromCatFileFor(t *testing.T, object string, typ GitObjectType, name string,
|
||||
tagger Signature, remainder string, expectedMessage string) {
|
||||
data := fmt.Sprintf(
|
||||
"object %s\ntype %s\ntag %s\ntagger %s <%s> %s\n%s",
|
||||
object, string(typ), name,
|
||||
@ -92,7 +92,7 @@ func testParseTagDataFromCatFileFor(t *testing.T, object string, typ types.GitOb
|
||||
require.NoError(t, err)
|
||||
|
||||
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, expectedMessage, res.Message, data)
|
||||
require.Equal(t, tagger.Identity.Name, res.Tagger.Identity.Name, data)
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -26,7 +26,7 @@ import (
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -36,14 +36,23 @@ var (
|
||||
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,
|
||||
repoPath string,
|
||||
rev string,
|
||||
file string,
|
||||
lineFrom int,
|
||||
lineTo int,
|
||||
) types.BlameReader {
|
||||
) BlameNextReader {
|
||||
// prepare the git command line arguments
|
||||
cmd := command.New(
|
||||
"blame",
|
||||
@ -84,7 +93,7 @@ func (a Adapter) Blame(
|
||||
|
||||
return &BlameReader{
|
||||
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.
|
||||
}
|
||||
}
|
||||
@ -92,7 +101,7 @@ func (a Adapter) Blame(
|
||||
type BlameReader struct {
|
||||
scanner *bufio.Scanner
|
||||
lastLine string
|
||||
commitCache map[string]*types.Commit
|
||||
commitCache map[string]*Commit
|
||||
errReader io.Reader
|
||||
}
|
||||
|
||||
@ -121,8 +130,8 @@ func (r *BlameReader) unreadLine(line string) {
|
||||
}
|
||||
|
||||
//nolint:complexity,gocognit,nestif // it's ok
|
||||
func (r *BlameReader) NextPart() (*types.BlamePart, error) {
|
||||
var commit *types.Commit
|
||||
func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
var commit *Commit
|
||||
var lines []string
|
||||
var err error
|
||||
|
||||
@ -134,12 +143,12 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
|
||||
}
|
||||
|
||||
if matches := blamePorcelainHeadRE.FindStringSubmatch(line); matches != nil {
|
||||
sha := matches[1]
|
||||
commitSHA := sha.Must(matches[1])
|
||||
|
||||
if commit == nil {
|
||||
commit = r.commitCache[sha]
|
||||
commit = r.commitCache[commitSHA.String()]
|
||||
if commit == nil {
|
||||
commit = &types.Commit{SHA: sha}
|
||||
commit = &Commit{SHA: commitSHA}
|
||||
}
|
||||
|
||||
if matches[5] != "" {
|
||||
@ -153,11 +162,11 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if sha != commit.SHA {
|
||||
if !commit.SHA.Equal(commitSHA) {
|
||||
r.unreadLine(line)
|
||||
r.commitCache[commit.SHA] = commit
|
||||
r.commitCache[commit.SHA.String()] = commit
|
||||
|
||||
return &types.BlamePart{
|
||||
return &BlamePart{
|
||||
Commit: commit,
|
||||
Lines: lines,
|
||||
}, nil
|
||||
@ -210,10 +219,10 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
|
||||
return nil, errors.Internal(err, "failed to start git blame command")
|
||||
}
|
||||
|
||||
var part *types.BlamePart
|
||||
var part *BlamePart
|
||||
|
||||
if commit != nil && len(lines) > 0 {
|
||||
part = &types.BlamePart{
|
||||
part = &BlamePart{
|
||||
Commit: commit,
|
||||
Lines: lines,
|
||||
}
|
||||
@ -222,7 +231,7 @@ func (r *BlameReader) NextPart() (*types.BlamePart, error) {
|
||||
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.
|
||||
const (
|
||||
headerSummary = "summary "
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -23,7 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
@ -64,44 +64,44 @@ filename file_name.go
|
||||
Line 14
|
||||
`
|
||||
|
||||
author := types.Identity{
|
||||
author := Identity{
|
||||
Name: "Marko",
|
||||
Email: "marko.gacesa@harness.io",
|
||||
}
|
||||
committer := types.Identity{
|
||||
committer := Identity{
|
||||
Name: "Committer",
|
||||
Email: "noreply@harness.io",
|
||||
}
|
||||
|
||||
commit1 := &types.Commit{
|
||||
SHA: "16f267ad4f731af1b2e36f42e170ed8921377398",
|
||||
commit1 := &Commit{
|
||||
SHA: sha.Must("16f267ad4f731af1b2e36f42e170ed8921377398"),
|
||||
Title: "Pull request 1",
|
||||
Message: "",
|
||||
Author: types.Signature{
|
||||
Author: Signature{
|
||||
Identity: author,
|
||||
When: time.Unix(1669812989, 0),
|
||||
},
|
||||
Committer: types.Signature{
|
||||
Committer: Signature{
|
||||
Identity: committer,
|
||||
When: time.Unix(1669812989, 0),
|
||||
},
|
||||
}
|
||||
|
||||
commit2 := &types.Commit{
|
||||
SHA: "dcb4b6b63e86f06ed4e4c52fbc825545dc0b6200",
|
||||
commit2 := &Commit{
|
||||
SHA: sha.Must("dcb4b6b63e86f06ed4e4c52fbc825545dc0b6200"),
|
||||
Title: "Pull request 2",
|
||||
Message: "",
|
||||
Author: types.Signature{
|
||||
Author: Signature{
|
||||
Identity: author,
|
||||
When: time.Unix(1673952128, 0),
|
||||
},
|
||||
Committer: types.Signature{
|
||||
Committer: Signature{
|
||||
Identity: committer,
|
||||
When: time.Unix(1673952128, 0),
|
||||
},
|
||||
}
|
||||
|
||||
want := []*types.BlamePart{
|
||||
want := []*BlamePart{
|
||||
{
|
||||
Commit: commit1,
|
||||
Lines: []string{"Line 10", "Line 11"},
|
||||
@ -118,11 +118,11 @@ filename file_name.go
|
||||
|
||||
reader := BlameReader{
|
||||
scanner: bufio.NewScanner(strings.NewReader(blameOut)),
|
||||
commitCache: make(map[string]*types.Commit),
|
||||
commitCache: make(map[string]*Commit),
|
||||
errReader: strings.NewReader(""),
|
||||
}
|
||||
|
||||
var got []*types.BlamePart
|
||||
var got []*BlamePart
|
||||
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
@ -145,7 +145,7 @@ filename file_name.go
|
||||
func TestBlameReader_NextPart_UserError(t *testing.T) {
|
||||
reader := BlameReader{
|
||||
scanner: bufio.NewScanner(strings.NewReader("")),
|
||||
commitCache: make(map[string]*types.Commit),
|
||||
commitCache: make(map[string]*Commit),
|
||||
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) {
|
||||
reader := BlameReader{
|
||||
scanner: bufio.NewScanner(iotest.ErrReader(errors.New("dummy error"))),
|
||||
commitCache: make(map[string]*types.Commit),
|
||||
commitCache: make(map[string]*Commit),
|
||||
errReader: strings.NewReader(""),
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -20,48 +20,58 @@ import (
|
||||
"io"
|
||||
|
||||
"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.
|
||||
func (a Adapter) GetBlob(
|
||||
func (g *Git) GetBlob(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
sha string,
|
||||
sha sha.SHA,
|
||||
sizeLimit int64,
|
||||
) (*types.BlobReader, error) {
|
||||
) (*BlobReader, error) {
|
||||
stdIn, stdOut, cancel := CatFileBatch(ctx, repoPath)
|
||||
|
||||
_, err := stdIn.Write([]byte(sha + "\n"))
|
||||
line := sha.String() + "\n"
|
||||
_, err := stdIn.Write([]byte(line))
|
||||
if err != nil {
|
||||
cancel()
|
||||
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 {
|
||||
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()
|
||||
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()
|
||||
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 {
|
||||
contentSize = sizeLimit
|
||||
}
|
||||
|
||||
return &types.BlobReader{
|
||||
return &BlobReader{
|
||||
SHA: sha,
|
||||
Size: objectSize,
|
||||
Size: output.Size,
|
||||
ContentSize: contentSize,
|
||||
Content: newLimitReaderCloser(stdOut, contentSize, cancel),
|
||||
}, nil
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -21,15 +21,33 @@ import (
|
||||
"strings"
|
||||
|
||||
"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.
|
||||
func (a Adapter) GetBranch(
|
||||
func (g *Git) GetBranch(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
branchName string,
|
||||
) (*types.Branch, error) {
|
||||
) (*Branch, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
@ -38,12 +56,12 @@ func (a Adapter) GetBranch(
|
||||
}
|
||||
|
||||
ref := GetReferenceFromBranchName(branchName)
|
||||
commit, err := GetCommit(ctx, repoPath, ref, "")
|
||||
commit, err := GetCommit(ctx, repoPath, ref+"^{commit}")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find the commit for the branch: %w", err)
|
||||
}
|
||||
|
||||
return &types.Branch{
|
||||
return &Branch{
|
||||
Name: branchName,
|
||||
SHA: commit.SHA,
|
||||
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)
|
||||
// NOTE: This is different from repo.Empty(),
|
||||
// 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,
|
||||
repoPath string,
|
||||
) (bool, error) {
|
||||
@ -68,8 +86,21 @@ func (a Adapter) HasBranches(
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -24,10 +24,10 @@ import (
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/djherbis/buffer"
|
||||
"github.com/djherbis/nio/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function.
|
||||
@ -41,6 +41,7 @@ type WriteCloserError interface {
|
||||
func CatFileBatch(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
flags ...command.CmdOptionFunc,
|
||||
) (WriteCloserError, *bufio.Reader, func()) {
|
||||
const bufferSize = 32 * 1024
|
||||
// We often want to feed the commits in order into cat-file --batch,
|
||||
@ -64,7 +65,10 @@ func CatFileBatch(
|
||||
|
||||
go func() {
|
||||
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,
|
||||
command.WithDir(repoPath),
|
||||
command.WithStdin(batchStdinReader),
|
||||
@ -87,42 +91,48 @@ func CatFileBatch(
|
||||
return batchStdinWriter, batchReader, cancel
|
||||
}
|
||||
|
||||
type BatchHeaderResponse struct {
|
||||
SHA sha.SHA
|
||||
Type string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// ReadBatchHeaderLine reads the header line from cat-file --batch
|
||||
// We expect:
|
||||
// <sha> SP <type> SP <size> LF
|
||||
// sha is a 40byte not 20byte here.
|
||||
func ReadBatchHeaderLine(rd *bufio.Reader) (sha []byte, objType string, size int64, err error) {
|
||||
objType, err = rd.ReadString('\n')
|
||||
func ReadBatchHeaderLine(rd *bufio.Reader) (*BatchHeaderResponse, error) {
|
||||
line, err := rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, "", 0, err
|
||||
return nil, err
|
||||
}
|
||||
if len(objType) == 1 {
|
||||
objType, err = rd.ReadString('\n')
|
||||
if len(line) == 1 {
|
||||
line, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, "", 0, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
idx := strings.IndexByte(objType, ' ')
|
||||
idx := strings.IndexByte(line, ' ')
|
||||
if idx < 0 {
|
||||
log.Debug().Msgf("missing space type: %s", objType)
|
||||
err = errors.NotFound("sha '%s' not found", sha)
|
||||
return nil, "", 0, err
|
||||
return nil, errors.NotFound("missing space char for: %s", line)
|
||||
}
|
||||
sha = []byte(objType[:idx])
|
||||
objType = objType[idx+1:]
|
||||
id := line[:idx]
|
||||
objType := line[idx+1:]
|
||||
|
||||
idx = strings.IndexByte(objType, ' ')
|
||||
if idx < 0 {
|
||||
err = errors.NotFound("sha '%s' not found", sha)
|
||||
return nil, "", 0, err
|
||||
return nil, errors.NotFound("sha '%s' not found", id)
|
||||
}
|
||||
|
||||
sizeStr := objType[idx+1 : len(objType)-1]
|
||||
objType = objType[:idx]
|
||||
|
||||
size, err = strconv.ParseInt(sizeStr, 10, 64)
|
||||
size, err := strconv.ParseInt(sizeStr, 10, 64)
|
||||
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
|
||||
}
|
@ -12,12 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -26,54 +28,115 @@ import (
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func (a Adapter) GetLatestCommit(
|
||||
func (g *Git) GetLatestCommit(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
rev string,
|
||||
treePath string,
|
||||
) (*types.Commit, error) {
|
||||
) (*Commit, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
treePath = cleanTreePath(treePath)
|
||||
|
||||
return GetCommit(ctx, repoPath, rev, treePath)
|
||||
return getCommit(ctx, repoPath, rev, treePath)
|
||||
}
|
||||
|
||||
func getGiteaCommits(
|
||||
giteaRepo *gitea.Repository,
|
||||
func getCommits(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
commitIDs []string,
|
||||
) ([]*gitea.Commit, error) {
|
||||
var giteaCommits []*gitea.Commit
|
||||
) ([]*Commit, error) {
|
||||
if len(commitIDs) == 0 {
|
||||
return giteaCommits, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commits := make([]*Commit, 0, len(commitIDs))
|
||||
for _, commitID := range commitIDs {
|
||||
commit, err := giteaRepo.GetCommit(commitID)
|
||||
commit, err := getCommit(ctx, repoPath, commitID, "")
|
||||
if err != nil {
|
||||
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,
|
||||
repoPath string,
|
||||
ref string,
|
||||
page int,
|
||||
limit int,
|
||||
filter types.CommitFilter,
|
||||
filter CommitFilter,
|
||||
) ([]string, error) {
|
||||
cmd := command.New("rev-list")
|
||||
|
||||
@ -115,7 +178,7 @@ func (a Adapter) listCommitSHAs(
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||
if err != nil {
|
||||
// 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
|
||||
@ -124,69 +187,55 @@ func (a Adapter) listCommitSHAs(
|
||||
// ListCommitSHAs lists the commits reachable from ref.
|
||||
// Note: ref & afterRef can be Branch / Tag / CommitSHA.
|
||||
// Note: commits returned are [ref->...->afterRef).
|
||||
func (a Adapter) ListCommitSHAs(
|
||||
func (g *Git) ListCommitSHAs(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
ref string,
|
||||
page int,
|
||||
limit int,
|
||||
filter types.CommitFilter,
|
||||
filter CommitFilter,
|
||||
) ([]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.
|
||||
// Note: ref & afterRef can be Branch / Tag / CommitSHA.
|
||||
// Note: commits returned are [ref->...->afterRef).
|
||||
func (a Adapter) ListCommits(
|
||||
func (g *Git) ListCommits(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
ref string,
|
||||
page int,
|
||||
limit int,
|
||||
includeStats bool,
|
||||
filter types.CommitFilter,
|
||||
) ([]types.Commit, []types.PathRenameDetails, error) {
|
||||
filter CommitFilter,
|
||||
) ([]*Commit, []PathRenameDetails, error) {
|
||||
if repoPath == "" {
|
||||
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 {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
giteaCommits, err := getGiteaCommits(giteaRepo, commitSHAs)
|
||||
commits, err := getCommits(ctx, repoPath, commitSHAs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
commits := make([]types.Commit, len(giteaCommits))
|
||||
for i := range giteaCommits {
|
||||
var commit *types.Commit
|
||||
commit, err = mapGiteaCommit(giteaCommits[i])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if includeStats {
|
||||
fileStats, err := getCommitFileStats(ctx, giteaRepo, commit.SHA)
|
||||
if includeStats {
|
||||
for _, commit := range commits {
|
||||
fileStats, err := getCommitFileStats(ctx, repoPath, commit.SHA)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("encountered error getting commit file stats: %w", err)
|
||||
}
|
||||
commit.FileStats = fileStats
|
||||
}
|
||||
|
||||
commits[i] = *commit
|
||||
}
|
||||
|
||||
if len(filter.Path) != 0 {
|
||||
renameDetailsList, err := getRenameDetails(ctx, giteaRepo, commits, filter.Path)
|
||||
renameDetailsList, err := getRenameDetails(ctx, repoPath, commits, filter.Path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -199,49 +248,48 @@ func (a Adapter) ListCommits(
|
||||
|
||||
func getCommitFileStats(
|
||||
ctx context.Context,
|
||||
giteaRepo *gitea.Repository,
|
||||
sha string,
|
||||
) ([]types.CommitFileStats, error) {
|
||||
repoPath string,
|
||||
sha sha.SHA,
|
||||
) ([]CommitFileStats, error) {
|
||||
var changeInfoTypes map[string]changeInfoType
|
||||
changeInfoTypes, err := getChangeInfoTypes(ctx, giteaRepo, sha)
|
||||
changeInfoTypes, err := getChangeInfoTypes(ctx, repoPath, sha)
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
for path, info := range changeInfoChanges {
|
||||
fileStats[i] = types.CommitFileStats{
|
||||
fileStats[i] = CommitFileStats{
|
||||
Path: changeInfoTypes[path].Path,
|
||||
OldPath: changeInfoTypes[path].OldPath,
|
||||
Status: changeInfoTypes[path].Status,
|
||||
ChangeType: changeInfoTypes[path].Status,
|
||||
Insertions: info.Insertions,
|
||||
Deletions: info.Deletions,
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
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.
|
||||
// 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(
|
||||
commits []types.Commit,
|
||||
renameDetails []types.PathRenameDetails,
|
||||
commits []*Commit,
|
||||
renameDetails []PathRenameDetails,
|
||||
path string,
|
||||
) []types.Commit {
|
||||
) []*Commit {
|
||||
if len(commits) == 0 {
|
||||
return commits
|
||||
}
|
||||
for _, renameDetail := range renameDetails {
|
||||
// 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:]
|
||||
}
|
||||
}
|
||||
@ -250,17 +298,17 @@ func cleanupCommitsForRename(
|
||||
|
||||
func getRenameDetails(
|
||||
ctx context.Context,
|
||||
giteaRepo *gitea.Repository,
|
||||
commits []types.Commit,
|
||||
repoPath string,
|
||||
commits []*Commit,
|
||||
path string,
|
||||
) ([]types.PathRenameDetails, error) {
|
||||
) ([]PathRenameDetails, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -273,7 +321,7 @@ func getRenameDetails(
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -285,52 +333,56 @@ func getRenameDetails(
|
||||
return renameDetailsList, nil
|
||||
}
|
||||
|
||||
func giteaGetRenameDetails(
|
||||
func gitGetRenameDetails(
|
||||
ctx context.Context,
|
||||
giteaRepo *gitea.Repository,
|
||||
ref string,
|
||||
repoPath string,
|
||||
sha sha.SHA,
|
||||
path string,
|
||||
) (*types.PathRenameDetails, error) {
|
||||
changeInfos, err := getChangeInfoTypes(ctx, giteaRepo, ref)
|
||||
) (*PathRenameDetails, error) {
|
||||
changeInfos, err := getChangeInfoTypes(ctx, repoPath, sha)
|
||||
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 {
|
||||
if c.Status == enum.FileDiffStatusRenamed && (c.OldPath == path || c.Path == path) {
|
||||
return &types.PathRenameDetails{
|
||||
return &PathRenameDetails{
|
||||
OldPath: c.OldPath,
|
||||
Path: c.Path,
|
||||
}, 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",
|
||||
command.WithFlag("--name-status"),
|
||||
command.WithFlag("--format="),
|
||||
command.WithFlag("--max-count=1"),
|
||||
command.WithArg(ref),
|
||||
command.WithArg(sha.String()),
|
||||
)
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to trigger log command: %w", err)
|
||||
}
|
||||
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",
|
||||
command.WithFlag("--numstat"),
|
||||
command.WithFlag("--format="),
|
||||
command.WithArg(ref),
|
||||
command.WithArg(sha.String()),
|
||||
)
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to trigger show command: %w", err)
|
||||
}
|
||||
@ -343,10 +395,10 @@ var renameRegex = regexp.MustCompile(`\t(.+)\t(.+)`)
|
||||
|
||||
func getChangeInfoTypes(
|
||||
ctx context.Context,
|
||||
giteaRepo *gitea.Repository,
|
||||
ref string,
|
||||
repoPath string,
|
||||
sha sha.SHA,
|
||||
) (map[string]changeInfoType, error) {
|
||||
lines, err := gitLogNameStatus(giteaRepo, ref)
|
||||
lines, err := gitLogNameStatus(ctx, repoPath, sha)
|
||||
if err != nil {
|
||||
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(.+)`)
|
||||
|
||||
func getChangeInfoChanges(
|
||||
giteaRepo *gitea.Repository,
|
||||
ref string,
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
sha sha.SHA,
|
||||
) (map[string]changeInfoChange, error) {
|
||||
lines, err := gitShowNumstat(giteaRepo, ref)
|
||||
lines, err := gitShowNumstat(ctx, repoPath, sha)
|
||||
if err != nil {
|
||||
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.
|
||||
func (a Adapter) GetCommit(
|
||||
func (g *Git) GetCommit(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
rev string,
|
||||
) (*types.Commit, error) {
|
||||
) (*Commit, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
|
||||
return GetCommit(ctx, repoPath, rev, "")
|
||||
return getCommit(ctx, repoPath, rev, "")
|
||||
}
|
||||
|
||||
func (a Adapter) GetFullCommitID(
|
||||
func (g *Git) GetFullCommitID(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
shortID string,
|
||||
) (string, error) {
|
||||
) (sha.SHA, error) {
|
||||
if repoPath == "" {
|
||||
return "", ErrRepositoryPathEmpty
|
||||
return sha.None, ErrRepositoryPathEmpty
|
||||
}
|
||||
cmd := command.New("rev-parse",
|
||||
command.WithArg(shortID),
|
||||
@ -484,66 +537,45 @@ func (a Adapter) GetFullCommitID(
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||
if err != nil {
|
||||
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.
|
||||
// Note: ref can be Branch / Tag / CommitSHA.
|
||||
func (a Adapter) GetCommits(
|
||||
func (g *Git) GetCommits(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
refs []string,
|
||||
) ([]types.Commit, error) {
|
||||
) ([]*Commit, error) {
|
||||
if repoPath == "" {
|
||||
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))
|
||||
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
|
||||
return getCommits(ctx, repoPath, refs)
|
||||
}
|
||||
|
||||
// GetCommitDivergences returns the count of the diverging commits for all branch pairs.
|
||||
// IMPORTANT: If a max is provided it limits the overal count of diverging commits
|
||||
// (max 10 could lead to (0, 10) while it's actually (2, 12)).
|
||||
func (a Adapter) GetCommitDivergences(
|
||||
func (g *Git) GetCommitDivergences(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
requests []types.CommitDivergenceRequest,
|
||||
requests []CommitDivergenceRequest,
|
||||
max int32,
|
||||
) ([]types.CommitDivergence, error) {
|
||||
) ([]CommitDivergence, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
var err error
|
||||
res := make([]types.CommitDivergence, len(requests))
|
||||
res := make([]CommitDivergence, len(requests))
|
||||
for i, req := range requests {
|
||||
res[i], err = a.getCommitDivergence(ctx, repoPath, req, max)
|
||||
if types.IsNotFoundError(err) {
|
||||
res[i] = types.CommitDivergence{Ahead: -1, Behind: -1}
|
||||
res[i], err = g.getCommitDivergence(ctx, repoPath, req, max)
|
||||
if errors.IsNotFound(err) {
|
||||
res[i] = CommitDivergence{Ahead: -1, Behind: -1}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
@ -555,42 +587,37 @@ func (a Adapter) GetCommitDivergences(
|
||||
}
|
||||
|
||||
// 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)).
|
||||
// NOTE: Gitea implementation makes two git cli calls, but it can be done with one
|
||||
// (downside is the max behavior explained above).
|
||||
func (a Adapter) getCommitDivergence(
|
||||
func (g *Git) getCommitDivergence(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
req types.CommitDivergenceRequest,
|
||||
req CommitDivergenceRequest,
|
||||
max int32,
|
||||
) (types.CommitDivergence, error) {
|
||||
// prepare args
|
||||
args := []string{
|
||||
"rev-list",
|
||||
"--count",
|
||||
"--left-right",
|
||||
}
|
||||
) (CommitDivergence, error) {
|
||||
cmd := command.New("rev-list",
|
||||
command.WithFlag("--count"),
|
||||
command.WithFlag("--left-right"),
|
||||
)
|
||||
// limit count if requested.
|
||||
if max > 0 {
|
||||
args = append(args, "--max-count")
|
||||
args = append(args, fmt.Sprint(max))
|
||||
cmd.Add(command.WithFlag("--max-count", strconv.Itoa(int(max))))
|
||||
}
|
||||
// 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
|
||||
cmd := gitea.NewCommand(ctx, args...)
|
||||
stdOut, stdErr, err := cmd.RunStdString(&gitea.RunOpts{Dir: repoPath})
|
||||
stdout := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(stdout))
|
||||
if err != nil {
|
||||
return types.CommitDivergence{},
|
||||
processGiteaErrorf(err, "git rev-list failed for '%s...%s' (stdErr: '%s')", req.From, req.To, stdErr)
|
||||
return CommitDivergence{},
|
||||
processGitErrorf(err, "git rev-list failed for '%s...%s'", req.From, req.To)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
@ -600,16 +627,18 @@ func (a Adapter) getCommitDivergence(
|
||||
// parse numbers
|
||||
left, err := strconv.ParseInt(rawLeft, 10, 32)
|
||||
if err != nil {
|
||||
return types.CommitDivergence{},
|
||||
fmt.Errorf("failed to parse git rev-list output for ahead '%s' (full: '%s')): %w", rawLeft, stdOut, err)
|
||||
return CommitDivergence{},
|
||||
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)
|
||||
if err != nil {
|
||||
return types.CommitDivergence{},
|
||||
fmt.Errorf("failed to parse git rev-list output for behind '%s' (full: '%s')): %w", rawRight, stdOut, err)
|
||||
return CommitDivergence{},
|
||||
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),
|
||||
Behind: int32(right),
|
||||
}, nil
|
||||
@ -630,14 +659,14 @@ func parseLinesToSlice(output []byte) []string {
|
||||
return slice
|
||||
}
|
||||
|
||||
// GetCommit returns info about a commit.
|
||||
// TODO: Move this function outside of the adapter package.
|
||||
func GetCommit(
|
||||
// getCommit returns info about a commit.
|
||||
// TODO: This function is used only for last used cache
|
||||
func getCommit(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
rev string,
|
||||
path string,
|
||||
) (*types.Commit, error) {
|
||||
) (*Commit, error) {
|
||||
const format = "" +
|
||||
fmtCommitHash + fmtZero + // 0
|
||||
fmtParentHashes + fmtZero + // 1
|
||||
@ -681,10 +710,12 @@ func GetCommit(
|
||||
"unexpected git log formatted output, expected %d, but got %d columns", columnCount, len(commitData))
|
||||
}
|
||||
|
||||
sha := commitData[0]
|
||||
var parentSHAs []string
|
||||
commitSHA := sha.Must(commitData[0])
|
||||
var parentSHAs []sha.SHA
|
||||
if commitData[1] != "" {
|
||||
parentSHAs = strings.Split(commitData[1], " ")
|
||||
for _, parentSHA := range strings.Split(commitData[1], " ") {
|
||||
parentSHAs = append(parentSHAs, sha.Must(parentSHA))
|
||||
}
|
||||
}
|
||||
authorName := commitData[2]
|
||||
authorEmail := commitData[3]
|
||||
@ -698,20 +729,20 @@ func GetCommit(
|
||||
authorTime, _ := time.Parse(time.RFC3339Nano, authorTimestamp)
|
||||
committerTime, _ := time.Parse(time.RFC3339Nano, committerTimestamp)
|
||||
|
||||
return &types.Commit{
|
||||
SHA: sha,
|
||||
return &Commit{
|
||||
SHA: commitSHA,
|
||||
ParentSHAs: parentSHAs,
|
||||
Title: subject,
|
||||
Message: body,
|
||||
Author: types.Signature{
|
||||
Identity: types.Identity{
|
||||
Author: Signature{
|
||||
Identity: Identity{
|
||||
Name: authorName,
|
||||
Email: authorEmail,
|
||||
},
|
||||
When: authorTime,
|
||||
},
|
||||
Committer: types.Signature{
|
||||
Identity: types.Identity{
|
||||
Committer: Signature{
|
||||
Identity: Identity{
|
||||
Name: committerName,
|
||||
Email: committerEmail,
|
||||
},
|
||||
@ -719,3 +750,173 @@ func GetCommit(
|
||||
},
|
||||
}, 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
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
// Config set local git key and value configuration.
|
||||
func (a Adapter) Config(
|
||||
func (g *Git) Config(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
key string,
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -21,23 +21,37 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/parser"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
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
|
||||
// and end line with calculated span.
|
||||
// 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.
|
||||
// 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
|
||||
newStartLine := hunk.NewLine
|
||||
oldSpan := hunk.OldSpan
|
||||
@ -142,34 +156,42 @@ func cutLinesFromFullFileDiff(w io.Writer, r io.Reader, startLine, endLine int)
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func (a Adapter) RawDiff(
|
||||
func (g *Git) RawDiff(
|
||||
ctx context.Context,
|
||||
w io.Writer,
|
||||
repoPath string,
|
||||
baseRef string,
|
||||
headRef string,
|
||||
mergeBase bool,
|
||||
files ...types.FileDiffRequest,
|
||||
alternates []string,
|
||||
files ...FileDiffRequest,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
|
||||
baseTag, err := a.GetAnnotatedTag(ctx, repoPath, baseRef)
|
||||
baseTag, err := g.GetAnnotatedTag(ctx, repoPath, baseRef)
|
||||
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 {
|
||||
headRef = headTag.TargetSha
|
||||
headRef = headTag.TargetSha.String()
|
||||
}
|
||||
|
||||
args := make([]string, 0, 8)
|
||||
args = append(args, "diff", "-M", "--full-index")
|
||||
cmd := command.New("diff",
|
||||
command.WithFlag("-M"),
|
||||
command.WithFlag("--full-index"),
|
||||
)
|
||||
if mergeBase {
|
||||
args = append(args, "--merge-base")
|
||||
cmd.Add(command.WithFlag("--merge-base"))
|
||||
}
|
||||
|
||||
if len(alternates) > 0 {
|
||||
cmd.Add(command.WithAlternateObjectDirs(alternates...))
|
||||
}
|
||||
|
||||
perFileDiffRequired := false
|
||||
paths := make([]string, 0, len(files))
|
||||
if len(files) > 0 {
|
||||
@ -186,8 +208,8 @@ func (a Adapter) RawDiff(
|
||||
again:
|
||||
startLine := 0
|
||||
endLine := 0
|
||||
newargs := make([]string, len(args), len(args)+8)
|
||||
copy(newargs, args)
|
||||
|
||||
newCmd := cmd.Clone()
|
||||
|
||||
if len(files) > 0 {
|
||||
startLine = files[processed].StartLine
|
||||
@ -196,16 +218,15 @@ again:
|
||||
|
||||
if perFileDiffRequired {
|
||||
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}
|
||||
}
|
||||
|
||||
newargs = append(newargs, baseRef, headRef)
|
||||
newCmd.Add(command.WithArg(baseRef, headRef))
|
||||
|
||||
if len(paths) > 0 {
|
||||
newargs = append(newargs, "--")
|
||||
newargs = append(newargs, paths...)
|
||||
newCmd.Add(command.WithPostSepArg(paths...))
|
||||
}
|
||||
|
||||
pipeRead, pipeWrite := io.Pipe()
|
||||
@ -217,7 +238,12 @@ again:
|
||||
_ = 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 {
|
||||
@ -234,67 +260,44 @@ again:
|
||||
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.
|
||||
func (a Adapter) CommitDiff(
|
||||
func (g *Git) CommitDiff(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
sha string,
|
||||
rev string,
|
||||
w io.Writer,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
if sha == "" {
|
||||
return errors.InvalidArgument("commit sha cannot be empty")
|
||||
if rev == "" {
|
||||
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 := git.NewCommand(ctx, args...)
|
||||
if err := cmd.Run(&git.RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
}); err != nil {
|
||||
return processGiteaErrorf(err, "commit diff error: %v", stderr)
|
||||
cmd := command.New("show",
|
||||
command.WithFlag("--full-index"),
|
||||
command.WithFlag("--pretty=format:%b"),
|
||||
command.WithArg(rev),
|
||||
)
|
||||
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(repoPath),
|
||||
command.WithStdout(w),
|
||||
); err != nil {
|
||||
return processGitErrorf(err, "commit diff error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a Adapter) DiffShortStat(
|
||||
func (g *Git) DiffShortStat(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
baseRef string,
|
||||
headRef string,
|
||||
useMergeBase bool,
|
||||
) (types.DiffShortStat, error) {
|
||||
) (DiffShortStat, error) {
|
||||
if repoPath == "" {
|
||||
return types.DiffShortStat{}, ErrRepositoryPathEmpty
|
||||
return DiffShortStat{}, ErrRepositoryPathEmpty
|
||||
}
|
||||
separator := ".."
|
||||
if useMergeBase {
|
||||
@ -302,31 +305,27 @@ func (a Adapter) DiffShortStat(
|
||||
}
|
||||
|
||||
shortstatArgs := []string{baseRef + separator + headRef}
|
||||
if len(baseRef) == 0 || baseRef == git.EmptySHA {
|
||||
shortstatArgs = []string{git.EmptyTreeSHA, headRef}
|
||||
if len(baseRef) == 0 || baseRef == types.NilSHA {
|
||||
shortstatArgs = []string{sha.EmptyTree, headRef}
|
||||
}
|
||||
numFiles, totalAdditions, totalDeletions, err := git.GetDiffShortStat(ctx, repoPath, shortstatArgs...)
|
||||
stat, err := GetDiffShortStat(ctx, repoPath, shortstatArgs...)
|
||||
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)
|
||||
}
|
||||
return types.DiffShortStat{
|
||||
Files: numFiles,
|
||||
Additions: totalAdditions,
|
||||
Deletions: totalDeletions,
|
||||
}, nil
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Hunks' body is ignored.
|
||||
// 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,
|
||||
repoPath string,
|
||||
targetRef string,
|
||||
sourceRef string,
|
||||
) ([]*types.DiffFileHunkHeaders, error) {
|
||||
) ([]*parser.DiffFileHunkHeaders, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
@ -340,13 +339,18 @@ func (a Adapter) GetDiffHunkHeaders(
|
||||
_ = pipeWrite.CloseWithError(err)
|
||||
}()
|
||||
|
||||
cmd := git.NewCommand(ctx,
|
||||
"diff", "--patch", "--no-color", "--unified=0", sourceRef, targetRef)
|
||||
err = cmd.Run(&git.RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: pipeWrite,
|
||||
Stderr: stderr, // We capture stderr output in a buffer.
|
||||
})
|
||||
cmd := command.New("diff",
|
||||
command.WithFlag("--patch"),
|
||||
command.WithFlag("--no-color"),
|
||||
command.WithFlag("--unified=0"),
|
||||
command.WithArg(sourceRef),
|
||||
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)
|
||||
@ -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.
|
||||
//
|
||||
//nolint:gocognit
|
||||
func (a Adapter) DiffCut(
|
||||
func (g *Git) DiffCut(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
targetRef string,
|
||||
sourceRef string,
|
||||
path string,
|
||||
params types.DiffCutParams,
|
||||
) (types.HunkHeader, types.Hunk, error) {
|
||||
params parser.DiffCutParams,
|
||||
) (parser.HunkHeader, parser.Hunk, error) {
|
||||
if repoPath == "" {
|
||||
return types.HunkHeader{}, types.Hunk{}, ErrRepositoryPathEmpty
|
||||
return parser.HunkHeader{}, parser.Hunk{}, ErrRepositoryPathEmpty
|
||||
}
|
||||
|
||||
// first fetch the list of the changed files
|
||||
@ -403,7 +407,7 @@ func (a Adapter) DiffCut(
|
||||
|
||||
diffEntries, err := parser.DiffRaw(pipeRead)
|
||||
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 (
|
||||
@ -420,11 +424,11 @@ func (a Adapter) DiffCut(
|
||||
|
||||
if params.LineStartNew && path == entry.OldPath {
|
||||
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 {
|
||||
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:
|
||||
if entry.Path != path {
|
||||
@ -448,7 +452,7 @@ func (a Adapter) DiffCut(
|
||||
}
|
||||
|
||||
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
|
||||
@ -484,35 +488,101 @@ func (a Adapter) DiffCut(
|
||||
diffCutHeader, linesHunk, err := parser.DiffCut(pipeRead, params)
|
||||
if errStderr := parseDiffStderr(stderr); errStderr != nil {
|
||||
// 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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
func (a Adapter) DiffFileName(ctx context.Context,
|
||||
func (g *Git) DiffFileName(ctx context.Context,
|
||||
repoPath string,
|
||||
baseRef string,
|
||||
headRef string,
|
||||
mergeBase bool,
|
||||
) ([]string, error) {
|
||||
args := make([]string, 0, 8)
|
||||
args = append(args, "diff", "--name-only")
|
||||
cmd := command.New("diff", command.WithFlag("--name-only"))
|
||||
if mergeBase {
|
||||
args = append(args, "--merge-base")
|
||||
cmd.Add(command.WithFlag("--merge-base"))
|
||||
}
|
||||
args = append(args, baseRef, headRef)
|
||||
cmd := git.NewCommand(ctx, args...)
|
||||
stdout, _, runErr := cmd.RunStdBytes(&git.RunOpts{Dir: repoPath})
|
||||
if runErr != nil {
|
||||
return nil, processGiteaErrorf(runErr, "failed to trigger diff command")
|
||||
cmd.Add(command.WithArg(baseRef, headRef))
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx,
|
||||
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 {
|
||||
@ -528,7 +598,7 @@ func parseDiffStderr(stderr *bytes.Buffer) error {
|
||||
errRaw = strings.TrimPrefix(errRaw, "fatal: ") // git errors start with the "fatal: " prefix
|
||||
|
||||
if strings.Contains(errRaw, "bad revision") {
|
||||
return types.ErrSHADoesNotMatch
|
||||
return parser.ErrSHADoesNotMatch
|
||||
}
|
||||
|
||||
return errors.New(errRaw)
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -21,14 +21,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/parser"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func Test_modifyHeader(t *testing.T) {
|
||||
type args struct {
|
||||
hunk types.HunkHeader
|
||||
hunk parser.HunkHeader
|
||||
startLine int
|
||||
endLine int
|
||||
}
|
||||
@ -40,7 +40,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test empty",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 0,
|
||||
OldSpan: 0,
|
||||
NewLine: 0,
|
||||
@ -54,7 +54,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test empty 1",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 0,
|
||||
OldSpan: 0,
|
||||
NewLine: 0,
|
||||
@ -68,7 +68,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test empty old",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 0,
|
||||
OldSpan: 0,
|
||||
NewLine: 1,
|
||||
@ -82,7 +82,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test empty new",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 1,
|
||||
OldSpan: 10,
|
||||
NewLine: 0,
|
||||
@ -96,7 +96,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test 1",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 2,
|
||||
OldSpan: 20,
|
||||
NewLine: 2,
|
||||
@ -110,7 +110,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test 2",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 2,
|
||||
OldSpan: 20,
|
||||
NewLine: 2,
|
||||
@ -124,7 +124,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test 4",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 1,
|
||||
OldSpan: 10,
|
||||
NewLine: 1,
|
||||
@ -138,7 +138,7 @@ func Test_modifyHeader(t *testing.T) {
|
||||
{
|
||||
name: "test 5",
|
||||
args: args{
|
||||
hunk: types.HunkHeader{
|
||||
hunk: parser.HunkHeader{
|
||||
OldLine: 1,
|
||||
OldSpan: 108,
|
||||
NewLine: 1,
|
162
git/api/errors.go
Normal file
162
git/api/errors.go
Normal 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
|
||||
}
|
@ -12,8 +12,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
const (
|
||||
gitTrace = "GIT_TRACE"
|
||||
import (
|
||||
"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
|
||||
}
|
94
git/api/foreachref/format.go
Normal file
94
git/api/foreachref/format.go
Normal 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
|
||||
}
|
140
git/api/foreachref/parser.go
Normal file
140
git/api/foreachref/parser.go
Normal 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
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
const (
|
||||
fmtEOL = "%n"
|
||||
@ -34,5 +34,5 @@ const (
|
||||
fmtCommitterUnix = "%ct" // Unix timestamp
|
||||
|
||||
fmtSubject = "%s"
|
||||
fmtBody = "%b"
|
||||
fmtBody = "%B"
|
||||
)
|
@ -12,36 +12,39 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (a Adapter) InfoRefs(
|
||||
func (g *Git) InfoRefs(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
service string,
|
||||
w io.Writer,
|
||||
env ...string,
|
||||
) error {
|
||||
cmd := &bytes.Buffer{}
|
||||
if err := git.NewCommand(ctx, service, "--stateless-rpc", "--advertise-refs", ".").
|
||||
Run(&git.RunOpts{
|
||||
Env: env,
|
||||
Dir: repoPath,
|
||||
Stdout: cmd,
|
||||
}); err != nil {
|
||||
stdout := &bytes.Buffer{}
|
||||
cmd := command.New(service,
|
||||
command.WithFlag("--stateless-rpc"),
|
||||
command.WithFlag("--advertise-refs"),
|
||||
command.WithArg("."),
|
||||
)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
func (a Adapter) ServicePack(
|
||||
func (g *Git) ServicePack(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
service string,
|
||||
@ -66,24 +69,19 @@ func (a Adapter) ServicePack(
|
||||
stdout io.Writer,
|
||||
env ...string,
|
||||
) error {
|
||||
// set this for allow pre-receive and post-receive execute
|
||||
env = append(env, "SSH_ORIGINAL_COMMAND="+service)
|
||||
|
||||
var (
|
||||
stderr bytes.Buffer
|
||||
cmd := command.New(service,
|
||||
command.WithFlag("--stateless-rpc"),
|
||||
command.WithArg(repoPath),
|
||||
command.WithEnv("SSH_ORIGINAL_COMMAND", service),
|
||||
)
|
||||
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" {
|
||||
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
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -25,15 +25,15 @@ import (
|
||||
|
||||
"github.com/harness/gitness/cache"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
func NewInMemoryLastCommitCache(
|
||||
cacheDuration time.Duration,
|
||||
) cache.Cache[CommitEntryKey, *types.Commit] {
|
||||
return cache.New[CommitEntryKey, *types.Commit](
|
||||
) cache.Cache[CommitEntryKey, *Commit] {
|
||||
return cache.New[CommitEntryKey, *Commit](
|
||||
commitEntryGetter{},
|
||||
cacheDuration)
|
||||
}
|
||||
@ -41,12 +41,12 @@ func NewInMemoryLastCommitCache(
|
||||
func NewRedisLastCommitCache(
|
||||
redisClient redis.UniversalClient,
|
||||
cacheDuration time.Duration,
|
||||
) (cache.Cache[CommitEntryKey, *types.Commit], error) {
|
||||
) (cache.Cache[CommitEntryKey, *Commit], error) {
|
||||
if redisClient == 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,
|
||||
commitEntryGetter{},
|
||||
func(key CommitEntryKey) string {
|
||||
@ -58,8 +58,8 @@ func NewRedisLastCommitCache(
|
||||
cacheDuration), nil
|
||||
}
|
||||
|
||||
func NoLastCommitCache() cache.Cache[CommitEntryKey, *types.Commit] {
|
||||
return cache.NewNoCache[CommitEntryKey, *types.Commit](commitEntryGetter{})
|
||||
func NoLastCommitCache() cache.Cache[CommitEntryKey, *Commit] {
|
||||
return cache.NewNoCache[CommitEntryKey, *Commit](commitEntryGetter{})
|
||||
}
|
||||
|
||||
type CommitEntryKey string
|
||||
@ -68,10 +68,10 @@ const separatorZero = "\x00"
|
||||
|
||||
func makeCommitEntryKey(
|
||||
repoPath string,
|
||||
commitSHA string,
|
||||
commitSHA sha.SHA,
|
||||
path string,
|
||||
) CommitEntryKey {
|
||||
return CommitEntryKey(repoPath + separatorZero + commitSHA + separatorZero + path)
|
||||
return CommitEntryKey(repoPath + separatorZero + commitSHA.String() + separatorZero + path)
|
||||
}
|
||||
|
||||
func (c CommitEntryKey) Split() (
|
||||
@ -93,14 +93,14 @@ func (c CommitEntryKey) Split() (
|
||||
|
||||
type commitValueCodec struct{}
|
||||
|
||||
func (c commitValueCodec) Encode(v *types.Commit) string {
|
||||
func (c commitValueCodec) Encode(v *Commit) string {
|
||||
buffer := &strings.Builder{}
|
||||
_ = gob.NewEncoder(buffer).Encode(v)
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (c commitValueCodec) Decode(s string) (*types.Commit, error) {
|
||||
commit := &types.Commit{}
|
||||
func (c commitValueCodec) Decode(s string) (*Commit, error) {
|
||||
commit := &Commit{}
|
||||
if err := gob.NewDecoder(strings.NewReader(s)).Decode(commit); err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack commit entry value: %w", err)
|
||||
}
|
||||
@ -114,12 +114,12 @@ type commitEntryGetter struct{}
|
||||
func (c commitEntryGetter) Find(
|
||||
ctx context.Context,
|
||||
key CommitEntryKey,
|
||||
) (*types.Commit, error) {
|
||||
) (*Commit, error) {
|
||||
repoPath, commitSHA, path := key.Split()
|
||||
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
|
||||
return GetCommit(ctx, repoPath, commitSHA, path)
|
||||
return getCommit(ctx, repoPath, commitSHA, path)
|
||||
}
|
51
git/api/mapping.go
Normal file
51
git/api/mapping.go
Normal 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
|
||||
}
|
@ -12,39 +12,40 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
type FileContent struct {
|
||||
Path string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
//nolint:gocognit
|
||||
func (a Adapter) MatchFiles(
|
||||
func (g *Git) MatchFiles(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
rev string,
|
||||
treePath string,
|
||||
pattern string,
|
||||
maxSize int,
|
||||
) ([]types.FileContent, error) {
|
||||
) ([]FileContent, error) {
|
||||
nodes, err := lsDirectory(ctx, repoPath, rev, treePath)
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
var files []types.FileContent
|
||||
var files []FileContent
|
||||
for i := range nodes {
|
||||
if nodes[i].NodeType != types.TreeNodeTypeBlob {
|
||||
if nodes[i].NodeType != TreeNodeTypeBlob {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -57,19 +58,19 @@ func (a Adapter) MatchFiles(
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = catFileWriter.Write([]byte(nodes[i].Sha + "\n"))
|
||||
_, err = catFileWriter.Write([]byte(nodes[i].SHA.String() + "\n"))
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to discard a large file: %w", err)
|
||||
@ -89,7 +90,7 @@ func (a Adapter) MatchFiles(
|
||||
continue
|
||||
}
|
||||
|
||||
files = append(files, types.FileContent{
|
||||
files = append(files, FileContent{
|
||||
Path: nodes[i].Path,
|
||||
Content: data,
|
||||
})
|
104
git/api/merge.go
Normal file
104
git/api/merge.go
Normal 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
74
git/api/object.go
Normal 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())
|
||||
}
|
@ -12,35 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
)
|
||||
|
||||
type PathDetails struct {
|
||||
Path string
|
||||
LastCommit *Commit
|
||||
}
|
||||
|
||||
// PathsDetails returns additional details about provided the paths.
|
||||
func (a Adapter) PathsDetails(ctx context.Context,
|
||||
func (g *Git) PathsDetails(ctx context.Context,
|
||||
repoPath string,
|
||||
rev 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.
|
||||
commitSHA, err := a.ResolveRev(ctx, repoPath, rev)
|
||||
commitSHA, err := g.ResolveRev(ctx, repoPath, rev)
|
||||
if err != nil {
|
||||
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 {
|
||||
results[i].Path = path
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to find last commit for path %s: %w", path, err)
|
||||
}
|
@ -12,38 +12,112 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"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/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"
|
||||
)
|
||||
|
||||
// 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(
|
||||
_ types.WalkReferencesEntry,
|
||||
) (types.WalkInstruction, error) {
|
||||
return types.WalkInstructionHandle, nil
|
||||
_ WalkReferencesEntry,
|
||||
) (WalkInstruction, error) {
|
||||
return WalkInstructionHandle, nil
|
||||
}
|
||||
|
||||
// WalkReferences uses the provided options to filter the available references of the repo,
|
||||
// and calls the handle function for every matching node.
|
||||
// The instructor & handler are called with a map that contains the matching value for every field provided in fields.
|
||||
// TODO: walkGiteaReferences related code should be moved to separate file.
|
||||
func (a Adapter) WalkReferences(
|
||||
// TODO: walkReferences related code should be moved to separate file.
|
||||
func (g *Git) WalkReferences(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
handler types.WalkReferencesHandler,
|
||||
opts *types.WalkReferencesOptions,
|
||||
handler WalkReferencesHandler,
|
||||
opts *WalkReferencesOptions,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
@ -53,7 +127,7 @@ func (a Adapter) WalkReferences(
|
||||
opts.Instructor = DefaultInstructor
|
||||
}
|
||||
if len(opts.Fields) == 0 {
|
||||
opts.Fields = []types.GitReferenceField{types.GitReferenceFieldRefName, types.GitReferenceFieldObjectName}
|
||||
opts.Fields = []GitReferenceField{GitReferenceFieldRefName, GitReferenceFieldObjectName}
|
||||
}
|
||||
if opts.MaxWalkDistance <= 0 {
|
||||
opts.MaxWalkDistance = math.MaxInt32
|
||||
@ -62,40 +136,35 @@ func (a Adapter) WalkReferences(
|
||||
opts.Patterns = []string{}
|
||||
}
|
||||
if string(opts.Sort) == "" {
|
||||
opts.Sort = types.GitReferenceFieldRefName
|
||||
opts.Sort = GitReferenceFieldRefName
|
||||
}
|
||||
|
||||
// prepare for-each-ref input
|
||||
sortArg := mapToGiteaReferenceSortingArgument(opts.Sort, opts.Order)
|
||||
sortArg := mapToReferenceSortingArgument(opts.Sort, opts.Order)
|
||||
rawFields := make([]string, len(opts.Fields))
|
||||
for i := range opts.Fields {
|
||||
rawFields[i] = string(opts.Fields[i])
|
||||
}
|
||||
giteaFormat := gitearef.NewFormat(rawFields...)
|
||||
format := foreachref.NewFormat(rawFields...)
|
||||
|
||||
// initializer pipeline for output processing
|
||||
pipeOut, pipeIn := io.Pipe()
|
||||
defer pipeOut.Close()
|
||||
defer pipeIn.Close()
|
||||
stderr := strings.Builder{}
|
||||
rc := &gitea.RunOpts{Dir: repoPath, Stdout: pipeIn, Stderr: &stderr}
|
||||
|
||||
go func() {
|
||||
// create array for args as patterns have to be passed as separate args.
|
||||
args := []string{
|
||||
"for-each-ref",
|
||||
"--format",
|
||||
giteaFormat.Flag(),
|
||||
"--sort",
|
||||
sortArg,
|
||||
"--count",
|
||||
fmt.Sprint(opts.MaxWalkDistance),
|
||||
"--ignore-case",
|
||||
}
|
||||
args = append(args, opts.Patterns...)
|
||||
err := gitea.NewCommand(ctx, args...).Run(rc)
|
||||
cmd := command.New("for-each-ref",
|
||||
command.WithFlag("--format", format.Flag()),
|
||||
command.WithFlag("--sort", sortArg),
|
||||
command.WithFlag("--count", strconv.Itoa(int(opts.MaxWalkDistance))),
|
||||
command.WithFlag("--ignore-case"),
|
||||
)
|
||||
cmd.Add(command.WithArg(opts.Patterns...))
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(repoPath),
|
||||
command.WithStdout(pipeIn),
|
||||
)
|
||||
if err != nil {
|
||||
_ = pipeIn.CloseWithError(gitea.ConcatenateError(err, stderr.String()))
|
||||
_ = pipeIn.CloseWithError(err)
|
||||
} else {
|
||||
_ = pipeIn.Close()
|
||||
}
|
||||
@ -103,14 +172,14 @@ func (a Adapter) WalkReferences(
|
||||
|
||||
// TODO: return error from git command!!!!
|
||||
|
||||
parser := giteaFormat.Parser(pipeOut)
|
||||
return walkGiteaReferenceParser(parser, handler, opts)
|
||||
parser := format.Parser(pipeOut)
|
||||
return walkReferenceParser(parser, handler, opts)
|
||||
}
|
||||
|
||||
func walkGiteaReferenceParser(
|
||||
parser *gitearef.Parser,
|
||||
handler types.WalkReferencesHandler,
|
||||
opts *types.WalkReferencesOptions,
|
||||
func walkReferenceParser(
|
||||
parser *foreachref.Parser,
|
||||
handler WalkReferencesHandler,
|
||||
opts *WalkReferencesOptions,
|
||||
) error {
|
||||
for i := int32(0); i < opts.MaxWalkDistance; i++ {
|
||||
// parse next line - nil if end of output reached or an error occurred.
|
||||
@ -120,7 +189,7 @@ func walkGiteaReferenceParser(
|
||||
}
|
||||
|
||||
// convert to correct map.
|
||||
ref, err := mapGiteaRawRef(rawRef)
|
||||
ref, err := mapRawRef(rawRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -131,10 +200,10 @@ func walkGiteaReferenceParser(
|
||||
return fmt.Errorf("error getting instruction: %w", err)
|
||||
}
|
||||
|
||||
if instruction == types.WalkInstructionSkip {
|
||||
if instruction == WalkInstructionSkip {
|
||||
continue
|
||||
}
|
||||
if instruction == types.WalkInstructionStop {
|
||||
if instruction == WalkInstructionStop {
|
||||
break
|
||||
}
|
||||
|
||||
@ -146,7 +215,7 @@ func walkGiteaReferenceParser(
|
||||
}
|
||||
|
||||
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
|
||||
@ -155,60 +224,63 @@ func walkGiteaReferenceParser(
|
||||
// GetRef get's the target of a reference
|
||||
// IMPORTANT provide full reference name to limit risk of collisions across reference types
|
||||
// (e.g `refs/heads/main` instead of `main`).
|
||||
func (a Adapter) GetRef(
|
||||
func (g *Git) GetRef(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
ref string,
|
||||
) (string, error) {
|
||||
) (sha.SHA, error) {
|
||||
if repoPath == "" {
|
||||
return "", ErrRepositoryPathEmpty
|
||||
return sha.None, ErrRepositoryPathEmpty
|
||||
}
|
||||
cmd := gitea.NewCommand(ctx, "show-ref", "--verify", "-s", "--", ref)
|
||||
stdout, _, err := cmd.RunStdString(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
})
|
||||
cmd := command.New("show-ref",
|
||||
command.WithFlag("--verify"),
|
||||
command.WithFlag("-s"),
|
||||
command.WithArg(ref),
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||
if err != nil {
|
||||
if err.IsExitCode(128) && strings.Contains(err.Stderr(), "not a valid ref") {
|
||||
return "", types.ErrNotFound("reference %q not found", ref)
|
||||
if command.AsError(err).IsExitCode(128) && strings.Contains(err.Error(), "not a valid 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
|
||||
// IMPORTANT provide full reference name to limit risk of collisions across reference types
|
||||
// (e.g `refs/heads/main` instead of `main`).
|
||||
func (a Adapter) UpdateRef(
|
||||
func (g *Git) UpdateRef(
|
||||
ctx context.Context,
|
||||
envVars map[string]string,
|
||||
repoPath string,
|
||||
ref string,
|
||||
oldValue string,
|
||||
newValue string,
|
||||
oldValue sha.SHA,
|
||||
newValue sha.SHA,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
|
||||
// don't break existing interface - user calls with empty value to delete the ref.
|
||||
if newValue == "" {
|
||||
newValue = types.NilSHA
|
||||
if newValue.IsEmpty() {
|
||||
newValue = sha.Nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
//nolint:gocritic,nestif
|
||||
if oldValue == "" {
|
||||
val, err := a.GetRef(ctx, repoPath, ref)
|
||||
if types.IsNotFoundError(err) {
|
||||
if oldValue.IsEmpty() {
|
||||
val, err := g.GetRef(ctx, repoPath, ref)
|
||||
if errors.IsNotFound(err) {
|
||||
// fail in case someone tries to delete a reference that doesn't exist.
|
||||
if newValue == types.NilSHA {
|
||||
return types.ErrNotFound("reference %q not found", ref)
|
||||
if newValue.IsNil() {
|
||||
return errors.NotFound("reference %q not found", ref)
|
||||
}
|
||||
|
||||
oldValue = types.NilSHA
|
||||
oldValue = sha.Nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to get current value of reference: %w", err)
|
||||
} else {
|
||||
@ -216,7 +288,7 @@ func (a Adapter) UpdateRef(
|
||||
}
|
||||
}
|
||||
|
||||
err := a.updateRefWithHooks(
|
||||
err := g.updateRefWithHooks(
|
||||
ctx,
|
||||
envVars,
|
||||
repoPath,
|
||||
@ -234,29 +306,29 @@ func (a Adapter) UpdateRef(
|
||||
// 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).
|
||||
// pre-receice will be called before the update, post-receive after.
|
||||
func (a Adapter) updateRefWithHooks(
|
||||
func (g *Git) updateRefWithHooks(
|
||||
ctx context.Context,
|
||||
envVars map[string]string,
|
||||
repoPath string,
|
||||
ref string,
|
||||
oldValue string,
|
||||
newValue string,
|
||||
oldValue sha.SHA,
|
||||
newValue sha.SHA,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
|
||||
if oldValue == "" {
|
||||
if oldValue.IsEmpty() {
|
||||
return fmt.Errorf("oldValue can't be empty")
|
||||
}
|
||||
if newValue == "" {
|
||||
if newValue.IsEmpty() {
|
||||
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")
|
||||
}
|
||||
|
||||
githookClient, err := a.githookFactory.NewClient(ctx, envVars)
|
||||
githookClient, err := g.githookFactory.NewClient(ctx, envVars)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if a.traceGit {
|
||||
if g.traceGit {
|
||||
log.Ctx(ctx).Trace().
|
||||
Str("git", "pre-receive").
|
||||
Msgf("pre-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n"))
|
||||
}
|
||||
|
||||
args := make([]string, 0, 4)
|
||||
args = append(args, "update-ref")
|
||||
if newValue == types.NilSHA {
|
||||
args = append(args, "-d", ref)
|
||||
cmd := command.New("update-ref")
|
||||
if newValue.IsNil() {
|
||||
cmd.Add(command.WithFlag("-d", ref))
|
||||
} else {
|
||||
args = append(args, ref, newValue)
|
||||
cmd.Add(command.WithArg(ref, newValue.String()))
|
||||
}
|
||||
|
||||
args = append(args, oldValue)
|
||||
|
||||
cmd := gitea.NewCommand(ctx, args...)
|
||||
_, _, err = cmd.RunStdString(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
})
|
||||
cmd.Add(command.WithArg(oldValue.String()))
|
||||
err = cmd.Run(ctx, command.WithDir(repoPath))
|
||||
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
|
||||
@ -319,7 +386,7 @@ func (a Adapter) updateRefWithHooks(
|
||||
return fmt.Errorf("post-receive call returned error: %q", *out.Error)
|
||||
}
|
||||
|
||||
if a.traceGit {
|
||||
if g.traceGit {
|
||||
log.Ctx(ctx).Trace().
|
||||
Str("git", "post-receive").
|
||||
Msgf("post-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n"))
|
@ -12,23 +12,59 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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 (
|
||||
gitReferenceNamePrefixBranch = "refs/heads/"
|
||||
gitReferenceNamePrefixTag = "refs/tags/"
|
||||
@ -37,7 +73,7 @@ const (
|
||||
var lsRemoteHeadRegexp = regexp.MustCompile(`ref: refs/heads/([^\s]+)\s+HEAD`)
|
||||
|
||||
// InitRepository initializes a new Git repository.
|
||||
func (a Adapter) InitRepository(
|
||||
func (g *Git) InitRepository(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
bare bool,
|
||||
@ -57,19 +93,8 @@ func (a Adapter) InitRepository(
|
||||
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.
|
||||
func (a Adapter) SetDefaultBranch(
|
||||
func (g *Git) SetDefaultBranch(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
defaultBranch string,
|
||||
@ -78,102 +103,124 @@ func (a Adapter) SetDefaultBranch(
|
||||
if repoPath == "" {
|
||||
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 !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
|
||||
return fmt.Errorf("branch '%s' does not exist", defaultBranch)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return processGiteaErrorf(err, "failed to set new default branch")
|
||||
return processGitErrorf(err, "failed to set new default branch")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultBranch gets the default branch of a repo.
|
||||
func (a Adapter) GetDefaultBranch(
|
||||
func (g *Git) GetDefaultBranch(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
) (string, error) {
|
||||
if repoPath == "" {
|
||||
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
|
||||
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 {
|
||||
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.
|
||||
// If the repo doesn't have a default branch, types.ErrNoDefaultBranch is returned.
|
||||
func (a Adapter) GetRemoteDefaultBranch(
|
||||
func (g *Git) GetRemoteDefaultBranch(
|
||||
ctx context.Context,
|
||||
remoteURL string,
|
||||
) (string, error) {
|
||||
args := []string{
|
||||
"-c", "credential.helper=",
|
||||
"ls-remote",
|
||||
"--symref",
|
||||
"-q",
|
||||
remoteURL,
|
||||
"HEAD",
|
||||
}
|
||||
|
||||
cmd := gitea.NewCommand(ctx, args...)
|
||||
stdOut, _, err := cmd.RunStdString(nil)
|
||||
if err != nil {
|
||||
return "", processGiteaErrorf(err, "failed to ls remote repo")
|
||||
cmd := command.New("ls-remote",
|
||||
command.WithConfig("credential.helper", ""),
|
||||
command.WithFlag("--symref"),
|
||||
command.WithFlag("-q"),
|
||||
command.WithArg(remoteURL),
|
||||
command.WithArg("HEAD"),
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
if err := cmd.Run(ctx, command.WithStdout(output)); err != nil {
|
||||
return "", processGitErrorf(err, "failed to ls remote repo")
|
||||
}
|
||||
|
||||
// git output looks as follows, and we are looking for the ref that HEAD points to
|
||||
// ref: refs/heads/main HEAD
|
||||
// 46963bc7f0b5e8c5f039d50ac9e6e51933c78cdf HEAD
|
||||
match := lsRemoteHeadRegexp.FindStringSubmatch(stdOut)
|
||||
match := lsRemoteHeadRegexp.FindStringSubmatch(strings.TrimSpace(output.String()))
|
||||
if match == nil {
|
||||
return "", types.ErrNoDefaultBranch
|
||||
return "", ErrNoDefaultBranch
|
||||
}
|
||||
|
||||
return match[1], nil
|
||||
}
|
||||
|
||||
func (a Adapter) Clone(
|
||||
func (g *Git) Clone(
|
||||
ctx context.Context,
|
||||
from string,
|
||||
to string,
|
||||
opts types.CloneRepoOptions,
|
||||
opts CloneRepoOptions,
|
||||
) error {
|
||||
err := gitea.Clone(ctx, from, to, gitea.CloneRepoOptions{
|
||||
Timeout: opts.Timeout,
|
||||
Mirror: opts.Mirror,
|
||||
Bare: opts.Bare,
|
||||
Quiet: opts.Quiet,
|
||||
Branch: opts.Branch,
|
||||
Shared: opts.Shared,
|
||||
NoCheckout: opts.NoCheckout,
|
||||
Depth: opts.Depth,
|
||||
Filter: opts.Filter,
|
||||
SkipTLSVerify: opts.SkipTLSVerify,
|
||||
})
|
||||
if err != nil {
|
||||
return processGiteaErrorf(err, "failed to clone repo")
|
||||
if err := os.MkdirAll(to, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := command.New("clone")
|
||||
if opts.SkipTLSVerify {
|
||||
cmd.Add(command.WithConfig("http.sslVerify", "false"))
|
||||
}
|
||||
if opts.Mirror {
|
||||
cmd.Add(command.WithFlag("--mirror"))
|
||||
}
|
||||
if opts.Bare {
|
||||
cmd.Add(command.WithFlag("--bare"))
|
||||
}
|
||||
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
|
||||
@ -181,7 +228,7 @@ func (a Adapter) Clone(
|
||||
|
||||
// Sync synchronizes the repository to match the provided source.
|
||||
// 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,
|
||||
repoPath string,
|
||||
source string,
|
||||
@ -193,33 +240,31 @@ func (a Adapter) Sync(
|
||||
if len(refSpecs) == 0 {
|
||||
refSpecs = []string{"+refs/*:refs/*"}
|
||||
}
|
||||
args := []string{
|
||||
"-c", "advice.fetchShowForcedUpdates=false",
|
||||
"-c", "credential.helper=",
|
||||
"fetch",
|
||||
"--quiet",
|
||||
"--prune",
|
||||
"--atomic",
|
||||
"--force",
|
||||
"--no-write-fetch-head",
|
||||
"--no-show-forced-updates",
|
||||
source,
|
||||
}
|
||||
args = append(args, refSpecs...)
|
||||
cmd := command.New("fetch",
|
||||
command.WithConfig("advice.fetchShowForcedUpdates", "false"),
|
||||
command.WithConfig("credential.helper", ""),
|
||||
command.WithFlag(
|
||||
"--quiet",
|
||||
"--prune",
|
||||
"--atomic",
|
||||
"--force",
|
||||
"--no-write-fetch-head",
|
||||
"--no-show-forced-updates",
|
||||
),
|
||||
command.WithArg(source),
|
||||
command.WithArg(refSpecs...),
|
||||
)
|
||||
|
||||
cmd := gitea.NewCommand(ctx, args...)
|
||||
_, _, err := cmd.RunStdString(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
UseContextTimeout: true,
|
||||
})
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath))
|
||||
if err != nil {
|
||||
return processGiteaErrorf(err, "failed to sync repo")
|
||||
return processGitErrorf(err, "failed to sync repo")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a Adapter) AddFiles(
|
||||
func (g *Git) AddFiles(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
all bool,
|
||||
files ...string,
|
||||
@ -227,20 +272,26 @@ func (a Adapter) AddFiles(
|
||||
if repoPath == "" {
|
||||
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 {
|
||||
return processGiteaErrorf(err, "failed to add changes")
|
||||
return processGitErrorf(err, "failed to add changes")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit commits the changes of the repository.
|
||||
// NOTE: Modification of gitea implementation that supports commiter_date + author_date.
|
||||
func (a Adapter) Commit(
|
||||
func (g *Git) Commit(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
opts types.CommitChangesOptions,
|
||||
opts CommitChangesOptions,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
@ -262,75 +313,57 @@ func (a Adapter) Commit(
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath))
|
||||
// No stderr but exit status 1 means nothing to commit (see gitea CommitChanges)
|
||||
if err != nil && err.Error() != "exit status 1" {
|
||||
return processGiteaErrorf(err, "failed to commit changes")
|
||||
return processGitErrorf(err, "failed to commit changes")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 adapter.Push method
|
||||
func (a Adapter) Push(
|
||||
// TODOD: return our own error types and move to above api.Push method
|
||||
func (g *Git) Push(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
opts types.PushOptions,
|
||||
opts PushOptions,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
cmd := gitea.NewCommand(ctx,
|
||||
"-c", "credential.helper=",
|
||||
"push",
|
||||
cmd := command.New("push",
|
||||
command.WithConfig("credential.helper", ""),
|
||||
)
|
||||
if opts.Force {
|
||||
cmd.AddArguments("-f")
|
||||
cmd.Add(command.WithFlag("-f"))
|
||||
}
|
||||
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 {
|
||||
cmd.AddArguments("--mirror")
|
||||
cmd.Add(command.WithFlag("--mirror"))
|
||||
}
|
||||
cmd.AddArguments("--", opts.Remote)
|
||||
cmd.Add(command.WithPostSepArg(opts.Remote))
|
||||
|
||||
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
|
||||
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
|
||||
err := cmd.Run(&gitea.RunOpts{
|
||||
Env: opts.Env,
|
||||
Timeout: opts.Timeout,
|
||||
Dir: repoPath,
|
||||
Stdout: &outbuf,
|
||||
Stderr: &errbuf,
|
||||
})
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(repoPath),
|
||||
command.WithStdout(&outbuf),
|
||||
command.WithStderr(&errbuf),
|
||||
command.WithEnvs(opts.Env...),
|
||||
)
|
||||
|
||||
if a.traceGit {
|
||||
if g.traceGit {
|
||||
log.Ctx(ctx).Trace().
|
||||
Str("git", "push").
|
||||
Err(err).
|
||||
@ -340,13 +373,13 @@ func (a Adapter) Push(
|
||||
if err != nil {
|
||||
switch {
|
||||
case strings.Contains(errbuf.String(), "non-fast-forward"):
|
||||
return &gitea.ErrPushOutOfDate{
|
||||
return &PushOutOfDateError{
|
||||
StdOut: outbuf.String(),
|
||||
StdErr: errbuf.String(),
|
||||
Err: err,
|
||||
}
|
||||
case strings.Contains(errbuf.String(), "! [remote rejected]"):
|
||||
err := &gitea.ErrPushRejected{
|
||||
err := &PushRejectedError{
|
||||
StdOut: outbuf.String(),
|
||||
StdErr: errbuf.String(),
|
||||
Err: err,
|
||||
@ -354,7 +387,7 @@ func (a Adapter) Push(
|
||||
err.GenerateMessage()
|
||||
return err
|
||||
case strings.Contains(errbuf.String(), "matches more than one"):
|
||||
err := &gitea.ErrMoreThanOne{
|
||||
err := &MoreThanOneError{
|
||||
StdOut: outbuf.String(),
|
||||
StdErr: errbuf.String(),
|
||||
Err: err,
|
||||
@ -371,31 +404,29 @@ func (a Adapter) Push(
|
||||
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
|
||||
}
|
||||
|
||||
func (a Adapter) CountObjects(ctx context.Context, repoPath string) (types.ObjectCount, error) {
|
||||
cmd := gitea.NewCommand(ctx,
|
||||
"count-objects", "-v",
|
||||
)
|
||||
|
||||
func (g *Git) CountObjects(ctx context.Context, repoPath string) (ObjectCount, error) {
|
||||
var outbuf strings.Builder
|
||||
if err := cmd.Run(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: &outbuf,
|
||||
}); err != nil {
|
||||
return types.ObjectCount{}, fmt.Errorf("error running git count-objects: %w", err)
|
||||
cmd := command.New("count-objects", command.WithFlag("-v"))
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(repoPath),
|
||||
command.WithStdout(&outbuf),
|
||||
)
|
||||
if err != nil {
|
||||
return ObjectCount{}, fmt.Errorf("error running git count-objects: %w", err)
|
||||
}
|
||||
|
||||
objectCount := parseGitCountObjectsOutput(ctx, outbuf.String())
|
||||
return objectCount, nil
|
||||
}
|
||||
|
||||
func parseGitCountObjectsOutput(ctx context.Context, output string) types.ObjectCount {
|
||||
info := types.ObjectCount{}
|
||||
func parseGitCountObjectsOutput(ctx context.Context, output string) ObjectCount {
|
||||
info := ObjectCount{}
|
||||
|
||||
output = strings.TrimSpace(output)
|
||||
lines := strings.Split(output, "\n")
|
@ -12,32 +12,31 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
func (a Adapter) ResolveRev(ctx context.Context,
|
||||
func (g *Git) ResolveRev(ctx context.Context,
|
||||
repoPath string,
|
||||
rev string,
|
||||
) (string, error) {
|
||||
args := []string{"rev-parse", rev}
|
||||
commitSHA, stdErr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath})
|
||||
if strings.Contains(stdErr, "ambiguous argument") {
|
||||
return "", errors.InvalidArgument("could not resolve git revision: %s", rev)
|
||||
}
|
||||
) (sha.SHA, error) {
|
||||
cmd := command.New("rev-parse", command.WithArg(rev))
|
||||
output := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||
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)
|
||||
}
|
||||
|
||||
commitSHA = strings.TrimSpace(commitSHA)
|
||||
|
||||
return commitSHA, nil
|
||||
return sha.New(output.String())
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -29,25 +29,24 @@ import (
|
||||
"time"
|
||||
|
||||
"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/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// SharedRepo is a type to wrap our upload repositories as a shallow clone.
|
||||
type SharedRepo struct {
|
||||
adapter Adapter
|
||||
git *Git
|
||||
repoUID string
|
||||
repo *gitea.Repository
|
||||
remoteRepoPath string
|
||||
tmpPath string
|
||||
RepoPath string
|
||||
}
|
||||
|
||||
// NewSharedRepo creates a new temporary upload repository.
|
||||
func NewSharedRepo(
|
||||
adapter Adapter,
|
||||
adapter *Git,
|
||||
baseTmpDir string,
|
||||
repoUID string,
|
||||
remoteRepoPath string,
|
||||
@ -58,27 +57,18 @@ func NewSharedRepo(
|
||||
}
|
||||
|
||||
t := &SharedRepo{
|
||||
adapter: adapter,
|
||||
git: adapter,
|
||||
repoUID: repoUID,
|
||||
remoteRepoPath: remoteRepoPath,
|
||||
tmpPath: tmpPath,
|
||||
RepoPath: tmpPath,
|
||||
}
|
||||
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.
|
||||
func (r *SharedRepo) Close(ctx context.Context) {
|
||||
defer r.repo.Close()
|
||||
if err := tempdir.RemoveTemporaryPath(r.tmpPath); err != nil {
|
||||
log.Ctx(ctx).Err(err).Msgf("Failed to remove temporary path %s", r.tmpPath)
|
||||
if err := tempdir.RemoveTemporaryPath(r.RepoPath); err != nil {
|
||||
log.Ctx(ctx).Err(err).Msgf("Failed to remove temporary path %s", r.RepoPath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +98,7 @@ type fileEntry struct {
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
var files []fileEntry
|
||||
@ -209,15 +199,13 @@ func (r *SharedRepo) MoveObjects(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (r *SharedRepo) InitAsShared(ctx context.Context) error {
|
||||
args := []string{"init", "--bare"}
|
||||
if _, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{
|
||||
Dir: r.tmpPath,
|
||||
}); err != nil {
|
||||
return errors.Internal(err, "error while creating empty repository: %s", stderr)
|
||||
cmd := command.New("init", command.WithFlag("--bare"))
|
||||
if err := cmd.Run(ctx, command.WithDir(r.RepoPath)); err != nil {
|
||||
return errors.Internal(err, "error while creating empty repository")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
gitRepo, err := gitea.OpenRepository(ctx, r.tmpPath)
|
||||
if err != nil {
|
||||
return processGiteaErrorf(err, "failed to open repo")
|
||||
}
|
||||
|
||||
r.repo = gitRepo
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone the base repository to our path and set branch as the HEAD.
|
||||
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 != "" {
|
||||
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 {
|
||||
stderr := err.Error()
|
||||
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
|
||||
if err := cmd.Run(ctx); err != nil {
|
||||
cmderr := command.AsError(err)
|
||||
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)
|
||||
} 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.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
|
||||
}
|
||||
|
||||
// Init the repository.
|
||||
func (r *SharedRepo) Init(ctx context.Context) error {
|
||||
if err := gitea.InitRepository(ctx, r.tmpPath, false); err != nil {
|
||||
return err
|
||||
}
|
||||
gitRepo, err := gitea.OpenRepository(ctx, r.tmpPath)
|
||||
err := r.git.InitRepository(ctx, r.RepoPath, false)
|
||||
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
|
||||
}
|
||||
|
||||
// SetDefaultIndex sets the git index to our HEAD.
|
||||
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 fmt.Errorf("failed to git read-tree HEAD: %w", err)
|
||||
}
|
||||
return nil
|
||||
return r.SetIndex(ctx, "HEAD")
|
||||
}
|
||||
|
||||
// SetIndex sets the git index to the provided treeish.
|
||||
func (r *SharedRepo) SetIndex(ctx context.Context, treeish string) error {
|
||||
if _, _, err := gitea.NewCommand(ctx, "read-tree", treeish).RunStdString(&gitea.RunOpts{Dir: r.tmpPath}); err != nil {
|
||||
return fmt.Errorf("failed to git read-tree %s: %w", treeish, err)
|
||||
func (r *SharedRepo) SetIndex(ctx context.Context, rev string) error {
|
||||
cmd := command.New("read-tree", command.WithArg(rev))
|
||||
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
|
||||
}
|
||||
@ -303,33 +282,32 @@ func (r *SharedRepo) LsFiles(
|
||||
ctx context.Context,
|
||||
filenames ...string,
|
||||
) ([]string, error) {
|
||||
stdOut := new(bytes.Buffer)
|
||||
stdErr := new(bytes.Buffer)
|
||||
cmd := command.New("ls-files",
|
||||
command.WithFlag("-z"),
|
||||
)
|
||||
|
||||
cmdArgs := []string{"ls-files", "-z", "--"}
|
||||
for _, arg := range filenames {
|
||||
if arg != "" {
|
||||
cmdArgs = append(cmdArgs, arg)
|
||||
cmd.Add(command.WithPostSepArg(arg))
|
||||
}
|
||||
}
|
||||
|
||||
if err := gitea.NewCommand(ctx, cmdArgs...).
|
||||
Run(&gitea.RunOpts{
|
||||
Dir: r.tmpPath,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("unable to run git ls-files for temporary repo of: "+
|
||||
"%s Error: %w\nstdout: %s\nstderr: %s",
|
||||
r.repoUID, err, stdOut.String(), stdErr.String())
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdout(stdout),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list files in shared repository's git index: %w", err)
|
||||
}
|
||||
|
||||
filelist := make([]string, 0)
|
||||
for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
|
||||
filelist = append(filelist, string(line))
|
||||
files := make([]string, 0)
|
||||
for _, line := range bytes.Split(stdout.Bytes(), []byte{'\000'}) {
|
||||
files = append(files, string(line))
|
||||
}
|
||||
|
||||
return filelist, nil
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// RemoveFilesFromIndex removes the given files from the index.
|
||||
@ -338,7 +316,6 @@ func (r *SharedRepo) RemoveFilesFromIndex(
|
||||
filenames ...string,
|
||||
) error {
|
||||
stdOut := new(bytes.Buffer)
|
||||
stdErr := new(bytes.Buffer)
|
||||
stdIn := new(bytes.Buffer)
|
||||
for _, file := range filenames {
|
||||
if file != "" {
|
||||
@ -348,15 +325,19 @@ func (r *SharedRepo) RemoveFilesFromIndex(
|
||||
}
|
||||
}
|
||||
|
||||
if err := gitea.NewCommand(ctx, "update-index", "--remove", "-z", "--index-info").
|
||||
Run(&gitea.RunOpts{
|
||||
Dir: r.tmpPath,
|
||||
Stdin: stdIn,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("unable to update-index for temporary repo: %s Error: %w\nstdout: %s\nstderr: %s",
|
||||
r.repoUID, err, stdOut.String(), stdErr.String())
|
||||
cmd := command.New("update-index",
|
||||
command.WithFlag("--remove"),
|
||||
command.WithFlag("-z"),
|
||||
command.WithFlag("--index-info"),
|
||||
)
|
||||
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
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
|
||||
}
|
||||
@ -365,22 +346,22 @@ func (r *SharedRepo) RemoveFilesFromIndex(
|
||||
func (r *SharedRepo) WriteGitObject(
|
||||
ctx context.Context,
|
||||
content io.Reader,
|
||||
) (string, error) {
|
||||
) (sha.SHA, error) {
|
||||
stdOut := new(bytes.Buffer)
|
||||
stdErr := new(bytes.Buffer)
|
||||
|
||||
if err := gitea.NewCommand(ctx, "hash-object", "-w", "--stdin").
|
||||
Run(&gitea.RunOpts{
|
||||
Dir: r.tmpPath,
|
||||
Stdin: content,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
}); err != nil {
|
||||
return "", fmt.Errorf("unable to hash-object to temporary repo: %s Error: %w\nstdout: %s\nstderr: %s",
|
||||
r.repoUID, err, stdOut.String(), stdErr.String())
|
||||
cmd := command.New("hash-object",
|
||||
command.WithFlag("-w"),
|
||||
command.WithFlag("--stdin"),
|
||||
)
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdin(content),
|
||||
command.WithStdout(stdOut),
|
||||
); err != nil {
|
||||
return sha.None, fmt.Errorf("unable to hash-object to temporary repo: %s Error: %w\nstdout: %s",
|
||||
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.
|
||||
@ -390,15 +371,15 @@ func (r *SharedRepo) ShowFile(
|
||||
commitHash string,
|
||||
writer io.Writer,
|
||||
) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
file := strings.TrimSpace(commitHash) + ":" + strings.TrimSpace(filePath)
|
||||
cmd := gitea.NewCommand(ctx, "show", file)
|
||||
if err := cmd.Run(&gitea.RunOpts{
|
||||
Dir: r.repo.Path,
|
||||
Stdout: writer,
|
||||
Stderr: stderr,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("show file: %w - %s", err, stderr)
|
||||
cmd := command.New("show",
|
||||
command.WithArg(file),
|
||||
)
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdout(writer),
|
||||
); err != nil {
|
||||
return fmt.Errorf("show file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -410,8 +391,13 @@ func (r *SharedRepo) AddObjectToIndex(
|
||||
objectHash string,
|
||||
objectPath string,
|
||||
) error {
|
||||
if _, _, err := gitea.NewCommand(ctx, "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash,
|
||||
objectPath).RunStdString(&gitea.RunOpts{Dir: r.tmpPath}); err != nil {
|
||||
cmd := command.New("update-index",
|
||||
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 {
|
||||
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.
|
||||
func (r *SharedRepo) WriteTree(ctx context.Context) (string, error) {
|
||||
stdout, _, err := gitea.NewCommand(ctx, "write-tree").RunStdString(&gitea.RunOpts{Dir: r.tmpPath})
|
||||
func (r *SharedRepo) WriteTree(ctx context.Context) (sha.SHA, error) {
|
||||
stdout := &bytes.Buffer{}
|
||||
cmd := command.New("write-tree")
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdout(stdout),
|
||||
)
|
||||
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)
|
||||
}
|
||||
return strings.TrimSpace(stdout), nil
|
||||
return sha.New(stdout.String())
|
||||
}
|
||||
|
||||
// GetLastCommit gets the last commit ID SHA of the repo.
|
||||
@ -444,73 +435,79 @@ func (r *SharedRepo) GetLastCommitByRef(
|
||||
if ref == "" {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
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.
|
||||
func (r *SharedRepo) CommitTreeWithDate(
|
||||
ctx context.Context,
|
||||
parent string,
|
||||
author, committer *types.Identity,
|
||||
treeHash, message string,
|
||||
parent sha.SHA,
|
||||
author, committer *Identity,
|
||||
treeHash sha.SHA,
|
||||
message string,
|
||||
signoff bool,
|
||||
authorDate, committerDate time.Time,
|
||||
) (string, 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),
|
||||
}
|
||||
) (sha.SHA, error) {
|
||||
messageBytes := new(bytes.Buffer)
|
||||
_, _ = messageBytes.WriteString(message)
|
||||
_, _ = messageBytes.WriteString("\n")
|
||||
|
||||
var args []string
|
||||
if parent != "" {
|
||||
args = []string{"commit-tree", treeHash, "-p", parent}
|
||||
} else {
|
||||
args = []string{"commit-tree", treeHash}
|
||||
cmd := command.New("commit-tree",
|
||||
command.WithAuthorAndDate(
|
||||
author.Name,
|
||||
author.Email,
|
||||
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
|
||||
args = append(args, "--no-gpg-sign")
|
||||
cmd.Add(command.WithFlag("--no-gpg-sign"))
|
||||
|
||||
if signoff {
|
||||
giteaSignature := &gitea.Signature{
|
||||
Name: committer.Name,
|
||||
Email: committer.Email,
|
||||
When: committerDate,
|
||||
sig := &Signature{
|
||||
Identity: Identity{
|
||||
Name: committer.Name,
|
||||
Email: committer.Email,
|
||||
},
|
||||
When: committerDate,
|
||||
}
|
||||
// Signed-off-by
|
||||
_, _ = messageBytes.WriteString("\n")
|
||||
_, _ = messageBytes.WriteString("Signed-off-by: ")
|
||||
_, _ = messageBytes.WriteString(giteaSignature.String())
|
||||
_, _ = messageBytes.WriteString(sig.String())
|
||||
}
|
||||
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
if err := gitea.NewCommand(ctx, args...).
|
||||
Run(&gitea.RunOpts{
|
||||
Env: env,
|
||||
Dir: r.tmpPath,
|
||||
Stdin: messageBytes,
|
||||
Stdout: 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)
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdin(messageBytes),
|
||||
command.WithStdout(stdout),
|
||||
); err != nil {
|
||||
return sha.None, processGitErrorf(err, "unable to commit-tree in temporary repo: %s Error: %v\nStdout: %s",
|
||||
r.repoUID, err, stdout)
|
||||
}
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
return sha.New(stdout.String())
|
||||
}
|
||||
|
||||
func (r *SharedRepo) PushDeleteBranch(
|
||||
@ -580,7 +577,7 @@ func (r *SharedRepo) push(
|
||||
env ...string,
|
||||
) error {
|
||||
// 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,
|
||||
Branch: sourceRef + ":" + destinationRef,
|
||||
Env: env,
|
||||
@ -592,29 +589,19 @@ func (r *SharedRepo) push(
|
||||
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.
|
||||
func (r *SharedRepo) GetBranch(rev string) (*gitea.Branch, error) {
|
||||
if r.repo == nil {
|
||||
return nil, fmt.Errorf("repository has not been cloned")
|
||||
}
|
||||
return r.repo.GetBranch(rev)
|
||||
func (r *SharedRepo) GetBranch(ctx context.Context, rev string) (*Branch, error) {
|
||||
return r.git.GetBranch(ctx, r.RepoPath, rev)
|
||||
}
|
||||
|
||||
// GetCommit Gets the commit object of the given commit ID.
|
||||
func (r *SharedRepo) GetCommit(commitID string) (*gitea.Commit, error) {
|
||||
if r.repo == nil {
|
||||
return nil, fmt.Errorf("repository has not been cloned")
|
||||
}
|
||||
return r.repo.GetCommit(commitID)
|
||||
func (r *SharedRepo) GetCommit(ctx context.Context, commitID string) (*Commit, error) {
|
||||
return r.git.GetCommit(ctx, r.RepoPath, 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!)
|
||||
@ -643,12 +630,3 @@ func GetReferenceFromTagName(tagName string) string {
|
||||
// return reference
|
||||
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
63
git/api/signature.go
Normal 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
91
git/api/submodule.go
Normal 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
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -25,7 +25,7 @@ import (
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -33,42 +33,65 @@ const (
|
||||
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.
|
||||
func (a Adapter) GetAnnotatedTag(
|
||||
func (g *Git) GetAnnotatedTag(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
sha string,
|
||||
) (*types.Tag, error) {
|
||||
rev string,
|
||||
) (*Tag, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
tags, err := getAnnotatedTags(ctx, repoPath, []string{sha})
|
||||
tags, err := getAnnotatedTags(ctx, repoPath, []string{rev})
|
||||
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
|
||||
}
|
||||
|
||||
// GetAnnotatedTags returns the tags for a specific list of tag sha.
|
||||
func (a Adapter) GetAnnotatedTags(
|
||||
func (g *Git) GetAnnotatedTags(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
shas []string,
|
||||
) ([]types.Tag, error) {
|
||||
revs []string,
|
||||
) ([]Tag, error) {
|
||||
if repoPath == "" {
|
||||
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, ...)
|
||||
func (a Adapter) CreateTag(
|
||||
func (g *Git) CreateTag(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
name string,
|
||||
targetSHA string,
|
||||
opts *types.CreateTagOptions,
|
||||
targetSHA sha.SHA,
|
||||
opts *CreateTagOptions,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
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))
|
||||
if err != nil {
|
||||
return processGiteaErrorf(err, "Service failed to create a tag")
|
||||
return processGitErrorf(err, "Service failed to create a tag")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAnnotatedTag is a custom implementation to retrieve an annotated tag from a sha.
|
||||
// The code is following parts of the gitea implementation.
|
||||
func getAnnotatedTags(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
shas []string,
|
||||
) ([]types.Tag, error) {
|
||||
revs []string,
|
||||
) ([]Tag, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
@ -110,25 +132,27 @@ func getAnnotatedTags(
|
||||
_ = writer.Close()
|
||||
}()
|
||||
|
||||
tags := make([]types.Tag, len(shas))
|
||||
tags := make([]Tag, len(revs))
|
||||
|
||||
for i, sha := range shas {
|
||||
if _, err := writer.Write([]byte(sha + "\n")); err != nil {
|
||||
for i, rev := range revs {
|
||||
line := rev + "\n"
|
||||
if _, err := writer.Write([]byte(line)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagSha, typ, size, err := ReadBatchHeaderLine(reader)
|
||||
output, err := ReadBatchHeaderLine(reader)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
if typ != string(types.GitObjectTypeTag) {
|
||||
return nil, fmt.Errorf("git object is of type '%s', expected tag", typ)
|
||||
if output.Type != string(GitObjectTypeTag) {
|
||||
return nil, fmt.Errorf("git object is of type '%s', expected tag",
|
||||
output.Type)
|
||||
}
|
||||
|
||||
// read the remaining rawData
|
||||
rawData, err := io.ReadAll(io.LimitReader(reader, size))
|
||||
rawData, err := io.ReadAll(io.LimitReader(reader, output.Size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -139,11 +163,11 @@ func getAnnotatedTags(
|
||||
|
||||
tag, err := parseTagDataFromCatFile(rawData)
|
||||
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
|
||||
tag.Sha = string(tagSha)
|
||||
tag.Sha = output.SHA
|
||||
|
||||
tags[i] = tag
|
||||
}
|
||||
@ -152,14 +176,13 @@ func getAnnotatedTags(
|
||||
}
|
||||
|
||||
// parseTagDataFromCatFile parses a tag from a cat-file output.
|
||||
func parseTagDataFromCatFile(data []byte) (tag types.Tag, err error) {
|
||||
p := 0
|
||||
|
||||
func parseTagDataFromCatFile(data []byte) (tag Tag, err error) {
|
||||
// parse object Id
|
||||
tag.TargetSha, p, err = parseCatFileLine(data, p, "object")
|
||||
object, p, err := parseCatFileLine(data, 0, "object")
|
||||
if err != nil {
|
||||
return tag, err
|
||||
}
|
||||
tag.TargetSha = sha.Must(object)
|
||||
|
||||
// parse object type
|
||||
rawType, p, err := parseCatFileLine(data, p, "type")
|
||||
@ -167,7 +190,7 @@ func parseTagDataFromCatFile(data []byte) (tag types.Tag, err error) {
|
||||
return tag, err
|
||||
}
|
||||
|
||||
tag.TargetType, err = types.ParseGitObjectType(rawType)
|
||||
tag.TargetType, err = ParseGitObjectType(rawType)
|
||||
if err != nil {
|
||||
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.
|
||||
// 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> Tue Oct 18 05:13:26 2022 +0530
|
||||
// TODO: method is leaning on gitea code - requires reference?
|
||||
func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
|
||||
sig := types.Signature{}
|
||||
// - author Max Mustermann <mm@gitness.io> Tue Oct 18 05:13:26 2022 +0530.
|
||||
func parseSignatureFromCatFileLine(line string) (Signature, error) {
|
||||
sig := Signature{}
|
||||
emailStart := strings.LastIndexByte(line, '<')
|
||||
emailEnd := strings.LastIndexByte(line, '>')
|
||||
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)
|
||||
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]
|
||||
@ -267,7 +289,7 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
|
||||
|
||||
timeStart := emailEnd + 2
|
||||
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)
|
||||
@ -276,7 +298,7 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
|
||||
var err error
|
||||
sig.When, err = time.Parse(defaultGitTimeLayout, line[timeStart:])
|
||||
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
|
||||
@ -285,19 +307,19 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
|
||||
// Otherwise we have to manually parse unix time and time zone
|
||||
endOfUnixTime := timeStart + strings.IndexByte(line[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)
|
||||
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
|
||||
startOfTimeZone := endOfUnixTime + 1 // +1 for space
|
||||
endOfTimeZone := startOfTimeZone + 5 // +5 for '+0700'
|
||||
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'
|
||||
@ -306,11 +328,11 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
|
||||
rawTimeZoneMin := rawTimeZone[3:] // gets +07[00]
|
||||
timeZoneH, err := strconv.ParseInt(rawTimeZoneH, 10, 64)
|
||||
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)
|
||||
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
|
||||
@ -324,3 +346,55 @@ func parseSignatureFromCatFileLine(line string) (types.Signature, error) {
|
||||
|
||||
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
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -22,34 +22,99 @@ import (
|
||||
"io"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/parser"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"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 {
|
||||
return strings.Trim(path.Clean("/"+treePath), "/")
|
||||
}
|
||||
|
||||
func parseTreeNodeMode(s string) (types.TreeNodeType, types.TreeNodeMode, error) {
|
||||
func parseTreeNodeMode(s string) (TreeNodeType, TreeNodeMode, error) {
|
||||
switch s {
|
||||
case "100644":
|
||||
return types.TreeNodeTypeBlob, types.TreeNodeModeFile, nil
|
||||
return TreeNodeTypeBlob, TreeNodeModeFile, nil
|
||||
case "120000":
|
||||
return types.TreeNodeTypeBlob, types.TreeNodeModeSymlink, nil
|
||||
return TreeNodeTypeBlob, TreeNodeModeSymlink, nil
|
||||
case "100755":
|
||||
return types.TreeNodeTypeBlob, types.TreeNodeModeExec, nil
|
||||
return TreeNodeTypeBlob, TreeNodeModeExec, nil
|
||||
case "160000":
|
||||
return types.TreeNodeTypeCommit, types.TreeNodeModeCommit, nil
|
||||
return TreeNodeTypeCommit, TreeNodeModeCommit, nil
|
||||
case "040000":
|
||||
return types.TreeNodeTypeTree, types.TreeNodeModeTree, nil
|
||||
return TreeNodeTypeTree, TreeNodeModeTree, nil
|
||||
default:
|
||||
return types.TreeNodeTypeBlob, types.TreeNodeModeFile,
|
||||
return TreeNodeTypeBlob, TreeNodeModeFile,
|
||||
fmt.Errorf("unknown git tree node mode: '%s'", s)
|
||||
}
|
||||
}
|
||||
@ -64,7 +129,7 @@ func lsTree(
|
||||
repoPath string,
|
||||
rev string,
|
||||
treePath string,
|
||||
) ([]types.TreeNode, error) {
|
||||
) ([]TreeNode, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
@ -86,12 +151,12 @@ func lsTree(
|
||||
}
|
||||
|
||||
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'})
|
||||
|
||||
list := make([]types.TreeNode, 0, n)
|
||||
list := make([]TreeNode, 0, n)
|
||||
scan := bufio.NewScanner(output)
|
||||
scan.Split(parser.ScanZeroSeparated)
|
||||
for scan.Scan() {
|
||||
@ -114,14 +179,14 @@ func lsTree(
|
||||
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]
|
||||
nodeName := path.Base(nodePath)
|
||||
|
||||
list = append(list, types.TreeNode{
|
||||
list = append(list, TreeNode{
|
||||
NodeType: nodeType,
|
||||
Mode: nodeMode,
|
||||
Sha: nodeSha,
|
||||
SHA: nodeSha,
|
||||
Name: nodeName,
|
||||
Path: nodePath,
|
||||
})
|
||||
@ -136,7 +201,7 @@ func lsDirectory(
|
||||
repoPath string,
|
||||
rev string,
|
||||
treePath string,
|
||||
) ([]types.TreeNode, error) {
|
||||
) ([]TreeNode, error) {
|
||||
treePath = path.Clean(treePath)
|
||||
if treePath == "" {
|
||||
treePath = "."
|
||||
@ -153,22 +218,22 @@ func lsFile(
|
||||
repoPath string,
|
||||
rev string,
|
||||
treePath string,
|
||||
) (types.TreeNode, error) {
|
||||
) (TreeNode, error) {
|
||||
treePath = cleanTreePath(treePath)
|
||||
|
||||
list, err := lsTree(ctx, repoPath, rev, treePath)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
if treePath == "" {
|
||||
if repoPath == "" {
|
||||
@ -177,7 +242,7 @@ func (a Adapter) GetTreeNode(ctx context.Context, repoPath, rev, treePath string
|
||||
cmd := command.New("show",
|
||||
command.WithFlag("--no-patch"),
|
||||
command.WithFlag("--format="+fmtTreeHash),
|
||||
command.WithArg(rev),
|
||||
command.WithArg(rev+"^{commit}"),
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
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 &types.TreeNode{
|
||||
NodeType: types.TreeNodeTypeTree,
|
||||
Mode: types.TreeNodeModeTree,
|
||||
Sha: strings.TrimSpace(output.String()),
|
||||
return &TreeNode{
|
||||
NodeType: TreeNodeTypeTree,
|
||||
Mode: TreeNodeModeTree,
|
||||
SHA: sha.Must(output.String()),
|
||||
Name: "",
|
||||
Path: "",
|
||||
}, 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.
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (a Adapter) ReadTree(
|
||||
func (g *Git) ReadTree(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
ref string,
|
139
git/api/util.go
Normal file
139
git/api/util.go
Normal 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)
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package adapter
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -33,7 +33,7 @@ var WireSet = wire.NewSet(
|
||||
func ProvideLastCommitCache(
|
||||
config types.Config,
|
||||
redisClient redis.UniversalClient,
|
||||
) (cache.Cache[CommitEntryKey, *types.Commit], error) {
|
||||
) (cache.Cache[CommitEntryKey, *Commit], error) {
|
||||
cacheDuration := config.LastCommitCache.Duration
|
||||
|
||||
// no need to cache if it's too short
|
@ -88,7 +88,7 @@ func (s *Service) Blame(ctx context.Context, params *BlameParams) (<-chan *Blame
|
||||
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
|
||||
reader := s.adapter.Blame(ctx,
|
||||
reader := s.git.Blame(ctx,
|
||||
repoPath, params.GitRef, params.Path,
|
||||
params.LineFrom, params.LineTo)
|
||||
|
||||
|
@ -17,6 +17,8 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
type GetBlobParams struct {
|
||||
@ -26,7 +28,7 @@ type GetBlobParams struct {
|
||||
}
|
||||
|
||||
type GetBlobOutput struct {
|
||||
SHA string
|
||||
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.
|
||||
@ -43,7 +45,7 @@ func (s *Service) GetBlob(ctx context.Context, params *GetBlobParams) (*GetBlobO
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -20,9 +20,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@ -35,14 +35,14 @@ const (
|
||||
BranchSortOptionDate
|
||||
)
|
||||
|
||||
var listBranchesRefFields = []types.GitReferenceField{
|
||||
types.GitReferenceFieldRefName,
|
||||
types.GitReferenceFieldObjectName,
|
||||
var listBranchesRefFields = []api.GitReferenceField{
|
||||
api.GitReferenceFieldRefName,
|
||||
api.GitReferenceFieldObjectName,
|
||||
}
|
||||
|
||||
type Branch struct {
|
||||
Name string
|
||||
SHA string
|
||||
SHA sha.SHA
|
||||
Commit *Commit
|
||||
}
|
||||
|
||||
@ -97,17 +97,17 @@ func (s *Service) CreateBranch(ctx context.Context, params *CreateBranchParams)
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to get target commit: %w", err)
|
||||
}
|
||||
branchRef := adapter.GetReferenceFromBranchName(params.BranchName)
|
||||
err = s.adapter.UpdateRef(
|
||||
branchRef := api.GetReferenceFromBranchName(params.BranchName)
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
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,
|
||||
)
|
||||
if errors.IsConflict(err) {
|
||||
@ -139,7 +139,7 @@ func (s *Service) GetBranch(ctx context.Context, params *GetBranchParams) (*GetB
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -160,17 +160,17 @@ func (s *Service) DeleteBranch(ctx context.Context, params *DeleteBranchParams)
|
||||
}
|
||||
|
||||
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,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
branchRef,
|
||||
"", // delete whatever is there
|
||||
types.NilSHA,
|
||||
sha.None, // delete whatever is there
|
||||
sha.Nil,
|
||||
)
|
||||
if types.IsNotFoundError(err) {
|
||||
if errors.IsNotFound(err) {
|
||||
return errors.NotFound("branch %q does not exist", params.BranchName)
|
||||
}
|
||||
if err != nil {
|
||||
@ -187,7 +187,7 @@ func (s *Service) ListBranches(ctx context.Context, params *ListBranchesParams)
|
||||
|
||||
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,
|
||||
Query: params.Query,
|
||||
Sort: mapBranchesSortOption(params.Sort),
|
||||
@ -203,17 +203,17 @@ func (s *Service) ListBranches(ctx context.Context, params *ListBranchesParams)
|
||||
if params.IncludeCommit {
|
||||
commitSHAs := make([]string, len(gitBranches))
|
||||
for i := range gitBranches {
|
||||
commitSHAs[i] = gitBranches[i].SHA
|
||||
commitSHAs[i] = gitBranches[i].SHA.String()
|
||||
}
|
||||
|
||||
var gitCommits []types.Commit
|
||||
gitCommits, err = s.adapter.GetCommits(ctx, repoPath, commitSHAs)
|
||||
var gitCommits []*api.Commit
|
||||
gitCommits, err = s.git.GetCommits(ctx, repoPath, commitSHAs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit: %w", err)
|
||||
}
|
||||
|
||||
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(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
filter types.BranchFilter,
|
||||
) ([]*types.Branch, error) {
|
||||
filter api.BranchFilter,
|
||||
) ([]*api.Branch, error) {
|
||||
// TODO: can we be smarter with slice allocation
|
||||
branches := make([]*types.Branch, 0, 16)
|
||||
branches := make([]*api.Branch, 0, 16)
|
||||
handler := listBranchesWalkReferencesHandler(&branches)
|
||||
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.PageSize,
|
||||
)
|
||||
@ -248,7 +248,7 @@ func (s *Service) listBranchesLoadReferenceData(
|
||||
return nil, errors.InvalidArgument("invalid pagination details: %v", err)
|
||||
}
|
||||
|
||||
opts := &types.WalkReferencesOptions{
|
||||
opts := &api.WalkReferencesOptions{
|
||||
Patterns: createReferenceWalkPatternsFromQuery(gitReferenceNamePrefixBranch, filter.Query),
|
||||
Sort: filter.Sort,
|
||||
Order: filter.Order,
|
||||
@ -258,32 +258,32 @@ func (s *Service) listBranchesLoadReferenceData(
|
||||
MaxWalkDistance: endsAfter,
|
||||
}
|
||||
|
||||
err = s.adapter.WalkReferences(ctx, repoPath, handler, opts)
|
||||
err = s.git.WalkReferences(ctx, repoPath, handler, opts)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func listBranchesWalkReferencesHandler(
|
||||
branches *[]*types.Branch,
|
||||
) types.WalkReferencesHandler {
|
||||
return func(e types.WalkReferencesEntry) error {
|
||||
fullRefName, ok := e[types.GitReferenceFieldRefName]
|
||||
branches *[]*api.Branch,
|
||||
) api.WalkReferencesHandler {
|
||||
return func(e api.WalkReferencesEntry) error {
|
||||
fullRefName, ok := e[api.GitReferenceFieldRefName]
|
||||
if !ok {
|
||||
return fmt.Errorf("entry missing reference name")
|
||||
}
|
||||
objectSHA, ok := e[types.GitReferenceFieldObjectName]
|
||||
objectSHA, ok := e[api.GitReferenceFieldObjectName]
|
||||
if !ok {
|
||||
return fmt.Errorf("entry missing object sha")
|
||||
}
|
||||
|
||||
branch := &types.Branch{
|
||||
branch := &api.Branch{
|
||||
Name: fullRefName[len(gitReferenceNamePrefixBranch):],
|
||||
SHA: objectSHA,
|
||||
SHA: sha.Must(objectSHA),
|
||||
}
|
||||
|
||||
// TODO: refactor to not use slice pointers?
|
||||
|
@ -39,7 +39,8 @@ func (b builder) supportsEndOfOptions() bool {
|
||||
|
||||
// descriptions is a curated list of Git command descriptions.
|
||||
var descriptions = map[string]builder{
|
||||
"am": {},
|
||||
"am": {},
|
||||
"add": {},
|
||||
"apply": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
@ -124,6 +125,9 @@ var descriptions = map[string]builder{
|
||||
"log": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"ls-files": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"ls-remote": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
@ -226,6 +230,9 @@ var descriptions = map[string]builder{
|
||||
"update-ref": {
|
||||
flags: 0,
|
||||
},
|
||||
"update-index": {
|
||||
flags: NoEndOfOptions,
|
||||
},
|
||||
"upload-archive": {
|
||||
// git-upload-archive(1) has a handrolled parser which always interprets the
|
||||
// first argument as directory, so we cannot use `--end-of-options`.
|
||||
@ -240,6 +247,9 @@ var descriptions = map[string]builder{
|
||||
"worktree": {
|
||||
flags: 0,
|
||||
},
|
||||
"write-tree": {
|
||||
flags: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// 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...)
|
||||
|
||||
if b.supportsEndOfOptions() {
|
||||
if b.supportsEndOfOptions() && len(flags) > 0 {
|
||||
cmdArgs = append(cmdArgs, "--end-of-options")
|
||||
}
|
||||
|
||||
|
@ -77,6 +77,33 @@ func New(name string, options ...CmdOptionFunc) *Command {
|
||||
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.
|
||||
func (c *Command) Add(options ...CmdOptionFunc) *Command {
|
||||
for _, opt := range options {
|
||||
@ -109,6 +136,7 @@ func (c *Command) Run(ctx context.Context, opts ...RunOptionFunc) (err error) {
|
||||
if len(c.Envs) > 0 {
|
||||
cmd.Env = c.Envs.Args()
|
||||
}
|
||||
cmd.Env = append(cmd.Env, options.Envs...)
|
||||
cmd.Dir = options.Dir
|
||||
cmd.Stdin = options.Stdin
|
||||
cmd.Stdout = options.Stdout
|
||||
|
@ -28,6 +28,9 @@ const (
|
||||
GitTracePerformance = "GIT_TRACE_PERFORMANCE"
|
||||
GitTraceSetup = "GIT_TRACE_SETUP"
|
||||
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.
|
||||
|
@ -48,6 +48,10 @@ func (e *Error) ExitCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (e *Error) IsExitCode(code int) bool {
|
||||
return e.ExitCode() == code
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if len(e.StdErr) != 0 {
|
||||
return fmt.Sprintf("%s: %s", e.Err.Error(), e.StdErr)
|
||||
|
@ -17,6 +17,7 @@ package command
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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.
|
||||
type RunOption struct {
|
||||
// Dir is location of repo.
|
||||
@ -115,6 +125,9 @@ type RunOption struct {
|
||||
Stdout io.Writer
|
||||
// Stderr is the error output from the command.
|
||||
Stderr io.Writer
|
||||
// Envs is environments slice containing (final) immutable
|
||||
// environment pair "ENV=value"
|
||||
Envs []string
|
||||
}
|
||||
|
||||
type RunOptionFunc func(option *RunOption)
|
||||
@ -147,3 +160,11 @@ func WithStderr(stderr io.Writer) RunOptionFunc {
|
||||
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...)
|
||||
}
|
||||
}
|
||||
|
@ -20,19 +20,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
type GetCommitParams struct {
|
||||
ReadParams
|
||||
// SHA is the git commit sha
|
||||
SHA string
|
||||
Revision string
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
SHA string `json:"sha"`
|
||||
ParentSHAs []string `json:"parent_shas,omitempty"`
|
||||
SHA sha.SHA `json:"sha"`
|
||||
ParentSHAs []sha.SHA `json:"parent_shas,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Author Signature `json:"author"`
|
||||
@ -70,11 +70,8 @@ func (s *Service) GetCommit(ctx context.Context, params *GetCommitParams) (*GetC
|
||||
if params == nil {
|
||||
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)
|
||||
result, err := s.adapter.GetCommit(ctx, repoPath, params.SHA)
|
||||
result, err := s.git.GetCommit(ctx, repoPath, params.Revision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -116,8 +113,8 @@ type ListCommitsParams struct {
|
||||
type RenameDetails struct {
|
||||
OldPath string
|
||||
NewPath string
|
||||
CommitShaBefore string
|
||||
CommitShaAfter string
|
||||
CommitShaBefore sha.SHA
|
||||
CommitShaAfter sha.SHA
|
||||
}
|
||||
|
||||
type ListCommitsOutput struct {
|
||||
@ -141,14 +138,14 @@ func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*
|
||||
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
|
||||
gitCommits, renameDetails, err := s.adapter.ListCommits(
|
||||
gitCommits, renameDetails, err := s.git.ListCommits(
|
||||
ctx,
|
||||
repoPath,
|
||||
params.GitREF,
|
||||
int(params.Page),
|
||||
int(params.Limit),
|
||||
params.IncludeStats,
|
||||
types.CommitFilter{
|
||||
api.CommitFilter{
|
||||
AfterRef: params.After,
|
||||
Path: params.Path,
|
||||
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) {
|
||||
totalCommits = len(gitCommits)
|
||||
} 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},
|
||||
}, 0)
|
||||
if err != nil {
|
||||
@ -178,7 +175,7 @@ func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*
|
||||
|
||||
commits := make([]Commit, len(gitCommits))
|
||||
for i := range gitCommits {
|
||||
commit, err := mapCommit(&gitCommits[i])
|
||||
commit, err := mapCommit(gitCommits[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to map rpc commit: %w", err)
|
||||
}
|
||||
@ -200,7 +197,7 @@ type GetCommitDivergencesParams struct {
|
||||
}
|
||||
|
||||
type GetCommitDivergencesOutput struct {
|
||||
Divergences []types.CommitDivergence
|
||||
Divergences []api.CommitDivergence
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
requests := make([]types.CommitDivergenceRequest, len(params.Requests))
|
||||
requests := make([]api.CommitDivergenceRequest, len(params.Requests))
|
||||
for i, req := range params.Requests {
|
||||
requests[i] = types.CommitDivergenceRequest{
|
||||
requests[i] = api.CommitDivergenceRequest{
|
||||
From: req.From,
|
||||
To: req.To,
|
||||
}
|
||||
}
|
||||
|
||||
// call gitea
|
||||
divergences, err := s.adapter.GetCommitDivergences(
|
||||
divergences, err := s.git.GetCommitDivergences(
|
||||
ctx,
|
||||
repoPath,
|
||||
requests,
|
||||
|
@ -20,7 +20,8 @@ import (
|
||||
|
||||
// ReadParams contains the base parameters for read operations.
|
||||
type ReadParams struct {
|
||||
RepoUID string
|
||||
RepoUID string
|
||||
AlternateObjectDirs []string
|
||||
}
|
||||
|
||||
func (p ReadParams) Validate() error {
|
||||
|
39
git/diff.go
39
git/diff.go
@ -22,9 +22,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/diff"
|
||||
"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"
|
||||
)
|
||||
@ -52,19 +54,27 @@ func (s *Service) RawDiff(
|
||||
ctx context.Context,
|
||||
out io.Writer,
|
||||
params *DiffParams,
|
||||
files ...types.FileDiffRequest,
|
||||
files ...api.FileDiffRequest,
|
||||
) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
if !isValidGitSHA(params.SHA) {
|
||||
return errors.InvalidArgument("the provided commit sha '%s' is of invalid format.", params.SHA)
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -95,7 +102,7 @@ func (s *Service) DiffShortStat(ctx context.Context, params *DiffParams) (DiffSh
|
||||
return DiffShortStatOutput{}, err
|
||||
}
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
stat, err := s.adapter.DiffShortStat(ctx,
|
||||
stat, err := s.git.DiffShortStat(ctx,
|
||||
repoPath,
|
||||
params.BaseRef,
|
||||
params.HeadRef,
|
||||
@ -207,7 +214,7 @@ func (s *Service) GetDiffHunkHeaders(
|
||||
|
||||
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 {
|
||||
return GetDiffHunkHeadersOutput{}, err
|
||||
}
|
||||
@ -233,7 +240,7 @@ type DiffCutOutput struct {
|
||||
Header HunkHeader
|
||||
LinesHeader string
|
||||
Lines []string
|
||||
MergeBaseSHA string
|
||||
MergeBaseSHA sha.SHA
|
||||
}
|
||||
|
||||
type DiffCutParams struct {
|
||||
@ -256,17 +263,17 @@ func (s *Service) DiffCut(ctx context.Context, params *DiffCutParams) (DiffCutOu
|
||||
|
||||
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 {
|
||||
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,
|
||||
params.TargetCommitSHA,
|
||||
params.SourceCommitSHA,
|
||||
params.Path,
|
||||
types.DiffCutParams{
|
||||
parser.DiffCutParams{
|
||||
LineStart: params.LineStart,
|
||||
LineStartNew: params.LineStartNew,
|
||||
LineEnd: params.LineEnd,
|
||||
@ -328,7 +335,7 @@ func parseFileDiffStatus(ftype diff.FileType) enum.FileDiffStatus {
|
||||
func (s *Service) Diff(
|
||||
ctx context.Context,
|
||||
params *DiffParams,
|
||||
files ...types.FileDiffRequest,
|
||||
files ...api.FileDiffRequest,
|
||||
) (<-chan *FileDiff, <-chan error) {
|
||||
wg := sync.WaitGroup{}
|
||||
ch := make(chan *FileDiff)
|
||||
@ -403,7 +410,7 @@ func (s *Service) DiffFileNames(ctx context.Context, params *DiffParams) (DiffFi
|
||||
return DiffFileNamesOutput{}, err
|
||||
}
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
fileNames, err := s.adapter.DiffFileName(
|
||||
fileNames, err := s.git.DiffFileName(
|
||||
ctx,
|
||||
repoPath,
|
||||
params.BaseRef,
|
||||
|
@ -23,6 +23,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
// 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{
|
||||
RefUpdate: ReferenceUpdate{
|
||||
Ref: ref,
|
||||
Old: oldSHA,
|
||||
New: newSHA,
|
||||
Old: sha.Must(oldSHA),
|
||||
New: sha.Must(newSHA),
|
||||
},
|
||||
}
|
||||
|
||||
@ -139,8 +141,8 @@ func getUpdatedReferencesFromStdIn() ([]ReferenceUpdate, error) {
|
||||
}
|
||||
|
||||
updatedRefs = append(updatedRefs, ReferenceUpdate{
|
||||
Old: splitGitHookData[0],
|
||||
New: splitGitHookData[1],
|
||||
Old: sha.Must(splitGitHookData[0]),
|
||||
New: sha.Must(splitGitHookData[1]),
|
||||
Ref: splitGitHookData[2],
|
||||
})
|
||||
}
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
package hook
|
||||
|
||||
import "github.com/harness/gitness/git/sha"
|
||||
|
||||
// Output represents the output of server hook api calls.
|
||||
type Output struct {
|
||||
// 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 string `json:"ref"`
|
||||
// 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 string `json:"new"`
|
||||
New sha.SHA `json:"new"`
|
||||
}
|
||||
|
||||
// PostReceiveInput represents the input of the post-receive git hook.
|
||||
|
@ -43,7 +43,7 @@ func (s *Service) GetInfoRefs(ctx context.Context, w io.Writer, params *InfoRefs
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("failed to execute git %s: %w", params.Service, err)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/api"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
@ -68,8 +68,8 @@ type Interface interface {
|
||||
/*
|
||||
* Diff services
|
||||
*/
|
||||
RawDiff(ctx context.Context, w io.Writer, in *DiffParams, files ...types.FileDiffRequest) error
|
||||
Diff(ctx context.Context, in *DiffParams, files ...types.FileDiffRequest) (<-chan *FileDiff, <-chan error)
|
||||
RawDiff(ctx context.Context, w io.Writer, in *DiffParams, files ...api.FileDiffRequest) error
|
||||
Diff(ctx context.Context, in *DiffParams, files ...api.FileDiffRequest) (<-chan *FileDiff, <-chan error)
|
||||
DiffFileNames(ctx context.Context, in *DiffParams) (DiffFileNamesOutput, error)
|
||||
CommitDiff(ctx context.Context, params *GetCommitParams, w io.Writer) error
|
||||
DiffShortStat(ctx context.Context, params *DiffParams) (DiffShortStatOutput, error)
|
||||
|
@ -17,10 +17,11 @@ package git
|
||||
import (
|
||||
"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 {
|
||||
return nil, fmt.Errorf("rpc branch is nil")
|
||||
}
|
||||
@ -41,7 +42,7 @@ func mapBranch(b *types.Branch) (*Branch, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapCommit(c *types.Commit) (*Commit, error) {
|
||||
func mapCommit(c *api.Commit) (*Commit, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("rpc commit is nil")
|
||||
}
|
||||
@ -67,12 +68,12 @@ func mapCommit(c *types.Commit) (*Commit, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapFileStats(typeStats []types.CommitFileStats) []CommitFileStats {
|
||||
func mapFileStats(typeStats []api.CommitFileStats) []CommitFileStats {
|
||||
var stats = make([]CommitFileStats, len(typeStats))
|
||||
|
||||
for i, tStat := range typeStats {
|
||||
stats[i] = CommitFileStats{
|
||||
Status: tStat.Status,
|
||||
Status: tStat.ChangeType,
|
||||
Path: tStat.Path,
|
||||
OldPath: tStat.OldPath,
|
||||
Insertions: tStat.Insertions,
|
||||
@ -83,7 +84,7 @@ func mapFileStats(typeStats []types.CommitFileStats) []CommitFileStats {
|
||||
return stats
|
||||
}
|
||||
|
||||
func mapSignature(s *types.Signature) (*Signature, error) {
|
||||
func mapSignature(s *api.Signature) (*Signature, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("rpc signature is nil")
|
||||
}
|
||||
@ -99,7 +100,7 @@ func mapSignature(s *types.Signature) (*Signature, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapIdentity(id *types.Identity) (Identity, error) {
|
||||
func mapIdentity(id *api.Identity) (Identity, error) {
|
||||
if id == nil {
|
||||
return Identity{}, fmt.Errorf("rpc identity is nil")
|
||||
}
|
||||
@ -110,12 +111,12 @@ func mapIdentity(id *types.Identity) (Identity, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapBranchesSortOption(o BranchSortOption) types.GitReferenceField {
|
||||
func mapBranchesSortOption(o BranchSortOption) api.GitReferenceField {
|
||||
switch o {
|
||||
case BranchSortOptionName:
|
||||
return types.GitReferenceFieldObjectName
|
||||
return api.GitReferenceFieldObjectName
|
||||
case BranchSortOptionDate:
|
||||
return types.GitReferenceFieldCreatorDate
|
||||
return api.GitReferenceFieldCreatorDate
|
||||
case BranchSortOptionDefault:
|
||||
fallthrough
|
||||
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)
|
||||
return &CommitTag{
|
||||
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 {
|
||||
case TagSortOptionDate:
|
||||
return types.GitReferenceFieldCreatorDate
|
||||
return api.GitReferenceFieldCreatorDate
|
||||
case TagSortOptionName:
|
||||
return types.GitReferenceFieldRefName
|
||||
return api.GitReferenceFieldRefName
|
||||
case TagSortOptionDefault:
|
||||
return types.GitReferenceFieldRefName
|
||||
return api.GitReferenceFieldRefName
|
||||
default:
|
||||
// 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 {
|
||||
return TreeNode{}, fmt.Errorf("rpc tree node is nil")
|
||||
}
|
||||
@ -169,43 +170,43 @@ func mapTreeNode(n *types.TreeNode) (TreeNode, error) {
|
||||
return TreeNode{
|
||||
Type: nodeType,
|
||||
Mode: mode,
|
||||
SHA: n.Sha,
|
||||
SHA: n.SHA.String(),
|
||||
Name: n.Name,
|
||||
Path: n.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapTreeNodeType(t types.TreeNodeType) (TreeNodeType, error) {
|
||||
func mapTreeNodeType(t api.TreeNodeType) (TreeNodeType, error) {
|
||||
switch t {
|
||||
case types.TreeNodeTypeBlob:
|
||||
case api.TreeNodeTypeBlob:
|
||||
return TreeNodeTypeBlob, nil
|
||||
case types.TreeNodeTypeCommit:
|
||||
case api.TreeNodeTypeCommit:
|
||||
return TreeNodeTypeCommit, nil
|
||||
case types.TreeNodeTypeTree:
|
||||
case api.TreeNodeTypeTree:
|
||||
return TreeNodeTypeTree, nil
|
||||
default:
|
||||
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 {
|
||||
case types.TreeNodeModeFile:
|
||||
case api.TreeNodeModeFile:
|
||||
return TreeNodeModeFile, nil
|
||||
case types.TreeNodeModeExec:
|
||||
case api.TreeNodeModeExec:
|
||||
return TreeNodeModeExec, nil
|
||||
case types.TreeNodeModeSymlink:
|
||||
case api.TreeNodeModeSymlink:
|
||||
return TreeNodeModeSymlink, nil
|
||||
case types.TreeNodeModeCommit:
|
||||
case api.TreeNodeModeCommit:
|
||||
return TreeNodeModeCommit, nil
|
||||
case types.TreeNodeModeTree:
|
||||
case api.TreeNodeModeTree:
|
||||
return TreeNodeModeTree, nil
|
||||
default:
|
||||
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))
|
||||
for i, detail := range c {
|
||||
renameDetailsList[i] = &RenameDetails{
|
||||
@ -218,21 +219,21 @@ func mapRenameDetails(c []types.PathRenameDetails) []*RenameDetails {
|
||||
return renameDetailsList
|
||||
}
|
||||
|
||||
func mapToSortOrder(o SortOrder) types.SortOrder {
|
||||
func mapToSortOrder(o SortOrder) api.SortOrder {
|
||||
switch o {
|
||||
case SortOrderAsc:
|
||||
return types.SortOrderAsc
|
||||
return api.SortOrderAsc
|
||||
case SortOrderDesc:
|
||||
return types.SortOrderDesc
|
||||
return api.SortOrderDesc
|
||||
case SortOrderDefault:
|
||||
return types.SortOrderDefault
|
||||
return api.SortOrderDefault
|
||||
default:
|
||||
// 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{
|
||||
OldLine: h.OldLine,
|
||||
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{
|
||||
OldName: h.OldFileName,
|
||||
NewName: h.NewFileName,
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/api"
|
||||
)
|
||||
|
||||
type MatchFilesParams struct {
|
||||
@ -30,7 +30,7 @@ type MatchFilesParams struct {
|
||||
}
|
||||
|
||||
type MatchFilesOutput struct {
|
||||
Files []types.FileContent
|
||||
Files []api.FileContent
|
||||
}
|
||||
|
||||
func (s *Service) MatchFiles(ctx context.Context,
|
||||
@ -38,7 +38,7 @@ func (s *Service) MatchFiles(ctx context.Context,
|
||||
) (*MatchFilesOutput, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("MatchFiles: failed to open repo: %w", err)
|
||||
|
60
git/merge.go
60
git/merge.go
@ -21,9 +21,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/git/merge"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@ -57,7 +58,7 @@ type MergeParams struct {
|
||||
|
||||
// HeadExpectedSHA is commit sha on the head branch, if HeadExpectedSHA is older
|
||||
// than the HeadBranch latest sha then merge will fail.
|
||||
HeadExpectedSHA string
|
||||
HeadExpectedSHA sha.SHA
|
||||
|
||||
Force bool
|
||||
DeleteHeadBranch bool
|
||||
@ -88,13 +89,13 @@ func (p *MergeParams) Validate() error {
|
||||
// base, head and commit sha.
|
||||
type MergeOutput struct {
|
||||
// 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 string
|
||||
HeadSHA sha.SHA
|
||||
// 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 string
|
||||
MergeSHA sha.SHA
|
||||
|
||||
CommitCount int
|
||||
ChangedFileCount int
|
||||
@ -148,7 +149,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
||||
// set up the target reference
|
||||
|
||||
var refPath string
|
||||
var refOldValue string
|
||||
var refOldValue sha.SHA
|
||||
|
||||
if params.RefType != enum.RefTypeUndefined {
|
||||
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)
|
||||
}
|
||||
|
||||
refOldValue, err = s.adapter.GetFullCommitID(ctx, repoPath, refPath)
|
||||
refOldValue, err = s.git.GetFullCommitID(ctx, repoPath, refPath)
|
||||
if errors.IsNotFound(err) {
|
||||
refOldValue = types.NilSHA
|
||||
refOldValue = sha.Nil
|
||||
} else if err != nil {
|
||||
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
|
||||
|
||||
baseCommitSHA, err := s.adapter.GetFullCommitID(ctx, repoPath, params.BaseBranch)
|
||||
baseCommitSHA, err := s.git.GetFullCommitID(ctx, repoPath, params.BaseBranch)
|
||||
if err != nil {
|
||||
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 {
|
||||
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(
|
||||
"head branch '%s' is on SHA '%s' which doesn't match expected SHA '%s'.",
|
||||
params.HeadBranch,
|
||||
@ -196,25 +197,26 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
||||
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 {
|
||||
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.")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return MergeOutput{}, errors.Internal(err,
|
||||
"failed to find short stat between %s and %s", baseCommitSHA, headCommitSHA)
|
||||
}
|
||||
changedFileCount := shortStat.Files
|
||||
|
||||
commitCount, err := merge.CommitCount(ctx, repoPath, baseCommitSHA, headCommitSHA)
|
||||
commitCount, err := merge.CommitCount(ctx, repoPath, baseCommitSHA.String(), headCommitSHA.String())
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
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 {
|
||||
return MergeOutput{}, errors.Internal(err,
|
||||
"Merge check failed to find conflicts between commits %s and %s",
|
||||
baseCommitSHA, headCommitSHA)
|
||||
baseCommitSHA.String(), headCommitSHA.String())
|
||||
}
|
||||
|
||||
log.Debug().Msg("merged check completed")
|
||||
@ -235,7 +237,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
||||
BaseSHA: baseCommitSHA,
|
||||
HeadSHA: headCommitSHA,
|
||||
MergeBaseSHA: mergeBaseCommitSHA,
|
||||
MergeSHA: "",
|
||||
MergeSHA: sha.None,
|
||||
CommitCount: commitCount,
|
||||
ChangedFileCount: changedFileCount,
|
||||
ConflictFiles: conflicts,
|
||||
@ -246,10 +248,10 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
||||
|
||||
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 {
|
||||
committer.Identity = types.Identity(*params.Committer)
|
||||
committer.Identity = api.Identity(*params.Committer)
|
||||
}
|
||||
if params.CommitterDate != nil {
|
||||
committer.When = *params.CommitterDate
|
||||
@ -258,7 +260,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
||||
author := committer
|
||||
|
||||
if params.Author != nil {
|
||||
author.Identity = types.Identity(*params.Author)
|
||||
author.Identity = api.Identity(*params.Author)
|
||||
}
|
||||
if params.AuthorDate != nil {
|
||||
author.When = *params.AuthorDate
|
||||
@ -288,7 +290,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
||||
BaseSHA: baseCommitSHA,
|
||||
HeadSHA: headCommitSHA,
|
||||
MergeBaseSHA: mergeBaseCommitSHA,
|
||||
MergeSHA: "",
|
||||
MergeSHA: sha.None,
|
||||
CommitCount: commitCount,
|
||||
ChangedFileCount: changedFileCount,
|
||||
ConflictFiles: conflicts,
|
||||
@ -299,7 +301,7 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
||||
|
||||
log.Trace().Msg("merge completed - updating git reference")
|
||||
|
||||
err = s.adapter.UpdateRef(
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
@ -350,7 +352,7 @@ func (p *MergeBaseParams) Validate() error {
|
||||
}
|
||||
|
||||
type MergeBaseOutput struct {
|
||||
MergeBaseSHA string
|
||||
MergeBaseSHA sha.SHA
|
||||
}
|
||||
|
||||
func (s *Service) MergeBase(
|
||||
@ -363,7 +365,7 @@ func (s *Service) MergeBase(
|
||||
|
||||
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 {
|
||||
return MergeBaseOutput{}, err
|
||||
}
|
||||
@ -375,8 +377,8 @@ func (s *Service) MergeBase(
|
||||
|
||||
type IsAncestorParams struct {
|
||||
ReadParams
|
||||
AncestorCommitSHA string
|
||||
DescendantCommitSHA string
|
||||
AncestorCommitSHA sha.SHA
|
||||
DescendantCommitSHA sha.SHA
|
||||
}
|
||||
|
||||
type IsAncestorOutput struct {
|
||||
@ -389,7 +391,7 @@ func (s *Service) IsAncestor(
|
||||
) (IsAncestorOutput, error) {
|
||||
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 {
|
||||
return IsAncestorOutput{}, err
|
||||
}
|
||||
|
@ -18,9 +18,9 @@ import (
|
||||
"context"
|
||||
"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/types"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@ -29,19 +29,19 @@ import (
|
||||
type Func func(
|
||||
ctx context.Context,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *types.Signature,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA string,
|
||||
) (mergeSHA string, conflicts []string, err error)
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error)
|
||||
|
||||
// Merge merges two the commits (targetSHA and sourceSHA) using the Merge method.
|
||||
func Merge(
|
||||
ctx context.Context,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *types.Signature,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA string,
|
||||
) (mergeSHA string, conflicts []string, err error) {
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error) {
|
||||
return mergeInternal(ctx,
|
||||
repoPath, tmpDir,
|
||||
author, committer,
|
||||
@ -54,10 +54,10 @@ func Merge(
|
||||
func Squash(
|
||||
ctx context.Context,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *types.Signature,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA string,
|
||||
) (mergeSHA string, conflicts []string, err error) {
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error) {
|
||||
return mergeInternal(ctx,
|
||||
repoPath, tmpDir,
|
||||
author, committer,
|
||||
@ -70,15 +70,15 @@ func Squash(
|
||||
func mergeInternal(
|
||||
ctx context.Context,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *types.Signature,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
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 {
|
||||
var err error
|
||||
|
||||
var treeSHA string
|
||||
var treeSHA sha.SHA
|
||||
|
||||
treeSHA, conflicts, err = s.MergeTree(ctx, mergeBaseSHA, targetSHA, sourceSHA)
|
||||
if err != nil {
|
||||
@ -89,7 +89,7 @@ func mergeInternal(
|
||||
return nil
|
||||
}
|
||||
|
||||
parents := make([]string, 0, 2)
|
||||
parents := make([]sha.SHA, 0, 2)
|
||||
parents = append(parents, targetSHA)
|
||||
if !squash {
|
||||
parents = append(parents, sourceSHA)
|
||||
@ -103,7 +103,7 @@ func mergeInternal(
|
||||
return 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
|
||||
@ -115,10 +115,10 @@ func mergeInternal(
|
||||
func Rebase(
|
||||
ctx context.Context,
|
||||
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
|
||||
mergeBaseSHA, targetSHA, sourceSHA string,
|
||||
) (mergeSHA string, conflicts []string, err error) {
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error) {
|
||||
err = runInSharedRepo(ctx, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
|
||||
sourceSHAs, err := s.CommitSHAsForRebase(ctx, mergeBaseSHA, sourceSHA)
|
||||
if err != nil {
|
||||
@ -126,15 +126,15 @@ func Rebase(
|
||||
}
|
||||
|
||||
lastCommitSHA := targetSHA
|
||||
lastTreeSHA, err := s.GetTreeSHA(ctx, targetSHA)
|
||||
lastTreeSHA, err := s.GetTreeSHA(ctx, targetSHA.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tree sha for target: %w", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("failed to get commit data in rebase merge: %w", err)
|
||||
}
|
||||
@ -146,7 +146,7 @@ func Rebase(
|
||||
message += "\n\n" + commitInfo.Message
|
||||
}
|
||||
|
||||
mergeTreeMergeBaseSHA := ""
|
||||
var mergeTreeMergeBaseSHA sha.SHA
|
||||
if len(commitInfo.ParentSHAs) > 0 {
|
||||
// use parent of commit as merge base to only apply changes introduced by commit.
|
||||
// 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.
|
||||
// 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
|
||||
if treeSHA == lastTreeSHA {
|
||||
if treeSHA.Equal(lastTreeSHA) {
|
||||
log.Ctx(ctx).Debug().Msgf("skipping commit %s as it's empty after rebase", commitSHA)
|
||||
continue
|
||||
}
|
||||
@ -188,7 +188,7 @@ func Rebase(
|
||||
return 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
|
||||
|
@ -23,11 +23,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/adapter"
|
||||
"github.com/harness/gitness/git/types"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
@ -85,7 +83,7 @@ func (p *CommitFilesParams) Validate() error {
|
||||
}
|
||||
|
||||
type CommitFilesResponse struct {
|
||||
CommitID string
|
||||
CommitID sha.SHA
|
||||
}
|
||||
|
||||
//nolint:gocognit
|
||||
@ -116,13 +114,6 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
||||
|
||||
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")
|
||||
|
||||
// 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).
|
||||
// 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.
|
||||
isEmpty, err := s.adapter.HasBranches(ctx, repoPath)
|
||||
isEmpty, err := s.git.HasBranches(ctx, repoPath)
|
||||
if err != nil {
|
||||
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
|
||||
// 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 {
|
||||
return CommitFilesResponse{}, err
|
||||
}
|
||||
|
||||
var oldCommitSHA string
|
||||
var oldCommitSHA sha.SHA
|
||||
if commit != nil {
|
||||
oldCommitSHA = commit.ID.String()
|
||||
oldCommitSHA = commit.SHA
|
||||
}
|
||||
|
||||
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.
|
||||
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 {
|
||||
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)
|
||||
|
||||
// Create bare repository with alternates pointing to the original repository.
|
||||
err = shared.InitAsShared(ctx)
|
||||
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)
|
||||
@ -171,15 +162,15 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
||||
if isEmpty {
|
||||
err = s.prepareTreeEmptyRepo(ctx, shared, params.Actions)
|
||||
} else {
|
||||
err = shared.SetIndex(ctx, oldCommitSHA)
|
||||
err = shared.SetIndex(ctx, oldCommitSHA.String())
|
||||
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)
|
||||
}
|
||||
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")
|
||||
@ -187,7 +178,7 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
||||
// Now write the tree
|
||||
treeHash, err := shared.WriteTree(ctx)
|
||||
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)
|
||||
@ -201,11 +192,11 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
||||
commitSHA, err := shared.CommitTreeWithDate(
|
||||
ctx,
|
||||
oldCommitSHA,
|
||||
&types.Identity{
|
||||
&api.Identity{
|
||||
Name: author.Name,
|
||||
Email: author.Email,
|
||||
},
|
||||
&types.Identity{
|
||||
&api.Identity{
|
||||
Name: committer.Name,
|
||||
Email: committer.Email,
|
||||
},
|
||||
@ -216,12 +207,12 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
||||
committerDate,
|
||||
)
|
||||
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)
|
||||
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
|
||||
@ -232,13 +223,13 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
||||
|
||||
log.Debug().Msg("update ref")
|
||||
|
||||
branchRef := adapter.GetReferenceFromBranchName(params.Branch)
|
||||
branchRef := api.GetReferenceFromBranchName(params.Branch)
|
||||
if params.Branch != params.NewBranch {
|
||||
// we are creating a new branch, rather than updating the existing one
|
||||
oldCommitSHA = types.NilSHA
|
||||
branchRef = adapter.GetReferenceFromBranchName(params.NewBranch)
|
||||
oldCommitSHA = sha.Nil
|
||||
branchRef = api.GetReferenceFromBranchName(params.NewBranch)
|
||||
}
|
||||
err = s.adapter.UpdateRef(
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
@ -252,23 +243,24 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
||||
|
||||
log.Debug().Msg("get commit")
|
||||
|
||||
commit, err = repo.GetCommit(newCommitSHA)
|
||||
commit, err = s.git.GetCommit(ctx, repoPath, newCommitSHA.String())
|
||||
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")
|
||||
|
||||
return CommitFilesResponse{
|
||||
CommitID: commit.ID.String(),
|
||||
CommitID: commit.SHA,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) prepareTree(
|
||||
ctx context.Context,
|
||||
shared *adapter.SharedRepo,
|
||||
shared *api.SharedRepo,
|
||||
actions []CommitFileAction,
|
||||
commit *git.Commit,
|
||||
commit *api.Commit,
|
||||
) error {
|
||||
// execute all actions
|
||||
for i := range actions {
|
||||
@ -282,7 +274,7 @@ func (s *Service) prepareTree(
|
||||
|
||||
func (s *Service) prepareTreeEmptyRepo(
|
||||
ctx context.Context,
|
||||
shared *adapter.SharedRepo,
|
||||
shared *api.SharedRepo,
|
||||
actions []CommitFileAction,
|
||||
) error {
|
||||
for _, action := range actions {
|
||||
@ -290,7 +282,7 @@ func (s *Service) prepareTreeEmptyRepo(
|
||||
return errors.PreconditionFailed("action not allowed on empty repository")
|
||||
}
|
||||
|
||||
filePath := files.CleanUploadFileName(action.Path)
|
||||
filePath := api.CleanUploadFileName(action.Path)
|
||||
if filePath == "" {
|
||||
return errors.InvalidArgument("invalid path")
|
||||
}
|
||||
@ -304,12 +296,13 @@ func (s *Service) prepareTreeEmptyRepo(
|
||||
}
|
||||
|
||||
func (s *Service) validateAndPrepareHeader(
|
||||
repo *git.Repository,
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
isEmpty bool,
|
||||
params *CommitFilesParams,
|
||||
) (*git.Commit, error) {
|
||||
) (*api.Commit, error) {
|
||||
if params.Branch == "" {
|
||||
defaultBranchRef, err := repo.GetDefaultBranch()
|
||||
defaultBranchRef, err := s.git.GetDefaultBranch(ctx, repoPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get default branch: %w", err)
|
||||
}
|
||||
@ -330,37 +323,32 @@ func (s *Service) validateAndPrepareHeader(
|
||||
}
|
||||
|
||||
// ensure source branch exists
|
||||
branch, err := repo.GetBranch(params.Branch)
|
||||
branch, err := s.git.GetBranch(ctx, repoPath, params.Branch)
|
||||
if err != nil {
|
||||
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)
|
||||
if params.Branch != params.NewBranch {
|
||||
existingBranch, err := repo.GetBranch(params.NewBranch)
|
||||
existingBranch, err := s.git.GetBranch(ctx, repoPath, params.NewBranch)
|
||||
if existingBranch != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
commit, err := branch.GetCommit()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get branch commit: %w", err)
|
||||
}
|
||||
|
||||
return commit, nil
|
||||
return branch.Commit, nil
|
||||
}
|
||||
|
||||
func (s *Service) processAction(
|
||||
ctx context.Context,
|
||||
shared SharedRepo,
|
||||
shared *api.SharedRepo,
|
||||
action *CommitFileAction,
|
||||
commit *git.Commit,
|
||||
commit *api.Commit,
|
||||
) (err error) {
|
||||
filePath := files.CleanUploadFileName(action.Path)
|
||||
filePath := api.CleanUploadFileName(action.Path)
|
||||
if filePath == "" {
|
||||
return errors.InvalidArgument("path cannot be empty")
|
||||
}
|
||||
@ -379,11 +367,11 @@ func (s *Service) processAction(
|
||||
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 {
|
||||
// only check path availability if a source commit is available (empty repo won't have such a commit)
|
||||
if commit != nil {
|
||||
if err := checkPathAvailability(commit, filePath, true); err != nil {
|
||||
if err := checkPathAvailability(ctx, repo, commit, filePath, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -394,16 +382,23 @@ func createFile(ctx context.Context, repo SharedRepo, commit *git.Commit,
|
||||
}
|
||||
|
||||
// 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 nil
|
||||
}
|
||||
|
||||
func updateFile(ctx context.Context, repo SharedRepo, commit *git.Commit, filePath, sha,
|
||||
mode string, payload []byte) error {
|
||||
func updateFile(
|
||||
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)
|
||||
entry, err := getFileEntry(commit, sha, filePath)
|
||||
entry, err := getFileEntry(ctx, repo, commit, sha, filePath)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
func moveFile(ctx context.Context, repo SharedRepo, commit *git.Commit,
|
||||
filePath, sha, mode string, payload []byte) error {
|
||||
func moveFile(
|
||||
ctx context.Context,
|
||||
repo *api.SharedRepo,
|
||||
commit *api.Commit,
|
||||
filePath string,
|
||||
sha string,
|
||||
mode string,
|
||||
payload []byte,
|
||||
) error {
|
||||
newPath, newContent, err := parseMovePayload(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure file exists and matches SHA
|
||||
entry, err := getFileEntry(commit, sha, filePath)
|
||||
entry, err := getFileEntry(ctx, repo, commit, sha, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -448,14 +450,14 @@ func moveFile(ctx context.Context, repo SharedRepo, commit *git.Commit,
|
||||
return fmt.Errorf("moveFile: error hashing object: %w", err)
|
||||
}
|
||||
|
||||
fileHash = hash
|
||||
fileHash = hash.String()
|
||||
fileMode = mode
|
||||
if entry.IsExecutable() {
|
||||
fileMode = "100755"
|
||||
}
|
||||
} else {
|
||||
fileHash = entry.ID.String()
|
||||
fileMode = entry.Mode().String()
|
||||
fileHash = entry.SHA.String()
|
||||
fileMode = entry.Mode.String()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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(
|
||||
commit *git.Commit,
|
||||
ctx context.Context,
|
||||
repo *api.SharedRepo,
|
||||
commit *api.Commit,
|
||||
sha string,
|
||||
path string,
|
||||
) (*git.TreeEntry, error) {
|
||||
entry, err := commit.GetTreeEntryByPath(path)
|
||||
if git.IsErrNotExist(err) {
|
||||
) (*api.TreeNode, error) {
|
||||
entry, err := repo.GetTreeNode(ctx, commit.SHA.String(), path)
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, errors.NotFound("path %s not found", path)
|
||||
}
|
||||
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 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]",
|
||||
path, sha, entry.ID.String())
|
||||
path, sha, entry.SHA)
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
@ -510,14 +514,20 @@ func getFileEntry(
|
||||
// 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
|
||||
// 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, "/")
|
||||
subTreePath := ""
|
||||
for index, part := range parts {
|
||||
subTreePath = path.Join(subTreePath, part)
|
||||
entry, err := commit.GetTreeEntryByPath(subTreePath)
|
||||
entry, err := repo.GetTreeNode(ctx, commit.SHA.String(), subTreePath)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
if errors.IsNotFound(err) {
|
||||
// Means there is no item with that name, so we're good
|
||||
break
|
||||
}
|
||||
@ -530,8 +540,8 @@ func checkPathAvailability(commit *git.Commit, filePath string, isNewFile bool)
|
||||
subTreePath)
|
||||
}
|
||||
case entry.IsLink():
|
||||
return fmt.Errorf("a symbolic link %w where you're trying to create a subdirectory [path: %s]",
|
||||
types.ErrAlreadyExists, subTreePath)
|
||||
return errors.Conflict("a symbolic link already exist where you're trying to create a subdirectory [path: %s]",
|
||||
subTreePath)
|
||||
case entry.IsDir():
|
||||
return errors.Conflict("a directory already exists where you're trying to create a subdirectory [path: %s]",
|
||||
subTreePath)
|
||||
@ -554,9 +564,9 @@ func parseMovePayload(payload []byte) (string, []byte, error) {
|
||||
newContent = payload[filePathEnd+1:]
|
||||
}
|
||||
|
||||
newPath = files.CleanUploadFileName(newPath)
|
||||
newPath = api.CleanUploadFileName(newPath)
|
||||
if newPath == "" {
|
||||
return "", nil, types.ErrInvalidPath
|
||||
return "", nil, api.ErrInvalidPath
|
||||
}
|
||||
|
||||
return newPath, newContent, nil
|
||||
|
@ -18,34 +18,48 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"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
|
||||
// (usually generated with large value passed to the "--unified" parameter)
|
||||
// and returns lines specified with the parameters.
|
||||
//
|
||||
//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)
|
||||
|
||||
var err error
|
||||
var hunkHeader types.HunkHeader
|
||||
var hunkHeader HunkHeader
|
||||
|
||||
if _, err = scanFileHeader(scanner); err != nil {
|
||||
return types.HunkHeader{}, types.Hunk{}, err
|
||||
return HunkHeader{}, Hunk{}, err
|
||||
}
|
||||
|
||||
if hunkHeader, err = scanHunkHeader(scanner); err != nil {
|
||||
return types.HunkHeader{}, types.Hunk{}, err
|
||||
return HunkHeader{}, Hunk{}, err
|
||||
}
|
||||
|
||||
currentOldLine := hunkHeader.OldLine
|
||||
currentNewLine := hunkHeader.NewLine
|
||||
|
||||
var inCut bool
|
||||
var diffCutHeader types.HunkHeader
|
||||
var diffCutHeader HunkHeader
|
||||
var diffCut []string
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return types.HunkHeader{}, types.Hunk{}, err
|
||||
return HunkHeader{}, Hunk{}, err
|
||||
}
|
||||
|
||||
if line == "" {
|
||||
@ -103,7 +117,7 @@ func DiffCut(r io.Reader, params types.DiffCutParams) (types.HunkHeader, types.H
|
||||
}
|
||||
|
||||
if !inCut {
|
||||
return types.HunkHeader{}, types.Hunk{}, types.ErrHunkNotFound
|
||||
return HunkHeader{}, Hunk{}, ErrHunkNotFound
|
||||
}
|
||||
|
||||
var (
|
||||
@ -116,7 +130,7 @@ func DiffCut(r io.Reader, params types.DiffCutParams) (types.HunkHeader, types.H
|
||||
for i := 0; i < params.AfterLines; i++ {
|
||||
line, _, err := scanHunkLine(scanner)
|
||||
if err != nil {
|
||||
return types.HunkHeader{}, types.Hunk{}, err
|
||||
return HunkHeader{}, Hunk{}, err
|
||||
}
|
||||
if line == "" {
|
||||
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,
|
||||
Lines: concat(linesBefore, diffCut, linesAfter),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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() {
|
||||
line := scan.Text()
|
||||
if h, ok := ParseDiffFileHeader(line); ok {
|
||||
@ -165,14 +179,14 @@ func scanFileHeader(scan *bufio.Scanner) (types.DiffFileHeader, error) {
|
||||
}
|
||||
|
||||
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.
|
||||
func scanHunkHeader(scan *bufio.Scanner) (types.HunkHeader, error) {
|
||||
func scanHunkHeader(scan *bufio.Scanner) (HunkHeader, error) {
|
||||
for scan.Scan() {
|
||||
line := scan.Text()
|
||||
if h, ok := ParseDiffHunkHeader(line); ok {
|
||||
@ -181,10 +195,10 @@ func scanHunkHeader(scan *bufio.Scanner) (types.HunkHeader, error) {
|
||||
}
|
||||
|
||||
if err := scan.Err(); err != nil {
|
||||
return types.HunkHeader{}, err
|
||||
return HunkHeader{}, err
|
||||
}
|
||||
|
||||
return types.HunkHeader{}, types.ErrHunkNotFound
|
||||
return HunkHeader{}, ErrHunkNotFound
|
||||
}
|
||||
|
||||
type diffAction byte
|
||||
@ -206,7 +220,7 @@ again:
|
||||
|
||||
line = scan.Text()
|
||||
if line == "" {
|
||||
err = types.ErrHunkNotFound // should not happen: empty line in diff output
|
||||
err = ErrHunkNotFound // should not happen: empty line in diff output
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,6 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/harness/gitness/git/types"
|
||||
)
|
||||
|
||||
//nolint:gocognit // it's a unit test!!!
|
||||
@ -49,14 +47,14 @@ func TestDiffCut(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params types.DiffCutParams
|
||||
params DiffCutParams
|
||||
expCutHeader string
|
||||
expCut []string
|
||||
expError error
|
||||
}{
|
||||
{
|
||||
name: "at-'+6,7,8':new",
|
||||
params: types.DiffCutParams{
|
||||
params: DiffCutParams{
|
||||
LineStart: 7, LineStartNew: true,
|
||||
LineEnd: 7, LineEndNew: true,
|
||||
BeforeLines: 0, AfterLines: 0,
|
||||
@ -68,7 +66,7 @@ func TestDiffCut(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "at-'+6,7,8':new-with-lines-around",
|
||||
params: types.DiffCutParams{
|
||||
params: DiffCutParams{
|
||||
LineStart: 7, LineStartNew: true,
|
||||
LineEnd: 7, LineEndNew: true,
|
||||
BeforeLines: 1, AfterLines: 2,
|
||||
@ -80,7 +78,7 @@ func TestDiffCut(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "at-'+0':new-with-lines-around",
|
||||
params: types.DiffCutParams{
|
||||
params: DiffCutParams{
|
||||
LineStart: 1, LineStartNew: true,
|
||||
LineEnd: 1, LineEndNew: true,
|
||||
BeforeLines: 3, AfterLines: 3,
|
||||
@ -92,7 +90,7 @@ func TestDiffCut(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "at-'-13':one-with-lines-around",
|
||||
params: types.DiffCutParams{
|
||||
params: DiffCutParams{
|
||||
LineStart: 13, LineStartNew: false,
|
||||
LineEnd: 13, LineEndNew: false,
|
||||
BeforeLines: 1, AfterLines: 1,
|
||||
@ -104,7 +102,7 @@ func TestDiffCut(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "at-'-13':mixed",
|
||||
params: types.DiffCutParams{
|
||||
params: DiffCutParams{
|
||||
LineStart: 7, LineStartNew: false,
|
||||
LineEnd: 7, LineEndNew: true,
|
||||
BeforeLines: 0, AfterLines: 0,
|
||||
@ -167,7 +165,7 @@ index 541cb64f..047d7ee2 100644
|
||||
|
||||
hh, h, err := DiffCut(
|
||||
strings.NewReader(input),
|
||||
types.DiffCutParams{
|
||||
DiffCutParams{
|
||||
LineStart: 3,
|
||||
LineStartNew: true,
|
||||
LineEnd: 3,
|
||||
@ -182,13 +180,13 @@ index 541cb64f..047d7ee2 100644
|
||||
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 {
|
||||
t.Errorf("expected hunk header: %+v, but got: %+v", expectedHH, hh)
|
||||
}
|
||||
|
||||
expectedHunkLines := types.Hunk{
|
||||
HunkHeader: types.HunkHeader{OldLine: 2, OldSpan: 0, NewLine: 2, NewSpan: 2},
|
||||
expectedHunkLines := Hunk{
|
||||
HunkHeader: HunkHeader{OldLine: 2, OldSpan: 0, NewLine: 2, NewSpan: 2},
|
||||
Lines: []string{"+456", "+789"},
|
||||
}
|
||||
if !reflect.DeepEqual(expectedHunkLines, h) {
|
||||
@ -210,7 +208,7 @@ index af7864ba..541cb64f 100644
|
||||
`
|
||||
hh, h, err := DiffCut(
|
||||
strings.NewReader(input),
|
||||
types.DiffCutParams{
|
||||
DiffCutParams{
|
||||
LineStart: 1,
|
||||
LineStartNew: true,
|
||||
LineEnd: 1,
|
||||
@ -225,13 +223,13 @@ index af7864ba..541cb64f 100644
|
||||
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 {
|
||||
t.Errorf("expected hunk header: %+v, but got: %+v", expectedHH, hh)
|
||||
}
|
||||
|
||||
expectedHunkLines := types.Hunk{
|
||||
HunkHeader: types.HunkHeader{OldLine: 1, OldSpan: 3, NewLine: 1, NewSpan: 1},
|
||||
expectedHunkLines := Hunk{
|
||||
HunkHeader: HunkHeader{OldLine: 1, OldSpan: 3, NewLine: 1, NewSpan: 1},
|
||||
Lines: []string{"-123", "-456", "-789", "+test"},
|
||||
}
|
||||
if !reflect.DeepEqual(expectedHunkLines, h) {
|
||||
|
@ -20,18 +20,22 @@ import (
|
||||
"regexp"
|
||||
|
||||
"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/(.+)$`)
|
||||
|
||||
func ParseDiffFileHeader(line string) (types.DiffFileHeader, bool) {
|
||||
func ParseDiffFileHeader(line string) (DiffFileHeader, bool) {
|
||||
groups := regExpDiffFileHeader.FindStringSubmatch(line)
|
||||
if groups == nil {
|
||||
return types.DiffFileHeader{}, false
|
||||
return DiffFileHeader{}, false
|
||||
}
|
||||
|
||||
return types.DiffFileHeader{
|
||||
return DiffFileHeader{
|
||||
OldFileName: groups[1],
|
||||
NewFileName: groups[2],
|
||||
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.
|
||||
// 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)
|
||||
|
||||
var currentFile *types.DiffFileHunkHeaders
|
||||
var result []*types.DiffFileHunkHeaders
|
||||
var currentFile *DiffFileHunkHeaders
|
||||
var result []*DiffFileHunkHeaders
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
@ -77,7 +81,7 @@ func GetHunkHeaders(r io.Reader) ([]*types.DiffFileHunkHeaders, error) {
|
||||
if currentFile != nil {
|
||||
result = append(result, currentFile)
|
||||
}
|
||||
currentFile = &types.DiffFileHunkHeaders{
|
||||
currentFile = &DiffFileHunkHeaders{
|
||||
FileHeader: h,
|
||||
HunksHeaders: nil,
|
||||
}
|
||||
@ -87,7 +91,7 @@ func GetHunkHeaders(r io.Reader) ([]*types.DiffFileHunkHeaders, error) {
|
||||
|
||||
if currentFile == nil {
|
||||
// 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 {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user