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

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

View File

@ -141,7 +141,7 @@ func (c *Controller) Report(
_, err = c.git.GetCommit(ctx, &git.GetCommitParams{
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)

View File

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

View File

@ -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)

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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 {

View File

@ -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"
)

View File

@ -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 {

View File

@ -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)

View File

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

View File

@ -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)
}

View File

@ -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)

View File

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

View File

@ -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,

View File

@ -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{

View File

@ -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(),
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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"

View File

@ -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"

View File

@ -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"
)

View File

@ -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,

View File

@ -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.")

View File

@ -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

View File

@ -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 {

View File

@ -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,
})

View File

@ -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)

View File

@ -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)

View File

@ -151,22 +151,22 @@ func (s *Service) handleEventBranchDeleted(ctx context.Context,
})
}
func (s *Service) fetchCommitInfoForEvent(ctx context.Context, repoUID string, sha string) (CommitInfo, error) {
func (s *Service) fetchCommitInfoForEvent(ctx context.Context, repoUID string, commitSHA string) (CommitInfo, error) {
out, err := s.git.GetCommit(ctx, &git.GetCommitParams{
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

View File

@ -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),

View File

@ -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,

View File

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

View File

@ -1,155 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package git
import (
"context"
"io"
"github.com/harness/gitness/git/adapter"
"github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types"
"code.gitea.io/gitea/modules/git"
)
var _ Adapter = (*adapter.Adapter)(nil)
// Adapter for accessing git commands from gitea.
type Adapter interface {
InitRepository(ctx context.Context, path string, bare bool) error
OpenRepository(ctx context.Context, path string) (*git.Repository, error)
SharedRepository(tmp string, repoUID string, remotePath string) (*adapter.SharedRepo, error)
Config(ctx context.Context, repoPath, key, value string) error
CountObjects(ctx context.Context, repoPath string) (types.ObjectCount, error)
SetDefaultBranch(ctx context.Context, repoPath string,
defaultBranch string, allowEmpty bool) error
GetDefaultBranch(ctx context.Context, repoPath string) (string, error)
GetRemoteDefaultBranch(ctx context.Context,
remoteURL string) (string, error)
HasBranches(ctx context.Context, repoPath string) (bool, error)
Clone(ctx context.Context, from, to string, opts types.CloneRepoOptions) error
AddFiles(repoPath string, all bool, files ...string) error
Commit(ctx context.Context, repoPath string, opts types.CommitChangesOptions) error
Push(ctx context.Context, repoPath string, opts types.PushOptions) error
ReadTree(ctx context.Context, repoPath, ref string, w io.Writer, args ...string) error
GetTreeNode(ctx context.Context, repoPath string, ref string, treePath string) (*types.TreeNode, error)
ListTreeNodes(ctx context.Context, repoPath string, ref string, treePath string) ([]types.TreeNode, error)
PathsDetails(ctx context.Context, repoPath string, ref string, paths []string) ([]types.PathDetails, error)
GetSubmodule(ctx context.Context, repoPath string, ref string, treePath string) (*types.Submodule, error)
GetBlob(ctx context.Context, repoPath string, sha string, sizeLimit int64) (*types.BlobReader, error)
WalkReferences(ctx context.Context, repoPath string, handler types.WalkReferencesHandler,
opts *types.WalkReferencesOptions) error
GetCommit(ctx context.Context, repoPath string, ref string) (*types.Commit, error)
GetCommits(ctx context.Context, repoPath string, refs []string) ([]types.Commit, error)
ListCommits(
ctx context.Context,
repoPath string,
ref string,
page int,
limit int,
includeStats bool,
filter types.CommitFilter) ([]types.Commit, []types.PathRenameDetails, error)
ListCommitSHAs(ctx context.Context, repoPath string,
ref string, page int, limit int, filter types.CommitFilter) ([]string, error)
GetLatestCommit(ctx context.Context, repoPath string, ref string, treePath string) (*types.Commit, error)
GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error)
GetAnnotatedTag(ctx context.Context, repoPath string, sha string) (*types.Tag, error)
GetAnnotatedTags(ctx context.Context, repoPath string, shas []string) ([]types.Tag, error)
CreateTag(ctx context.Context, repoPath string, name string, targetSHA string, opts *types.CreateTagOptions) error
GetBranch(ctx context.Context, repoPath string, branchName string) (*types.Branch, error)
GetCommitDivergences(ctx context.Context, repoPath string,
requests []types.CommitDivergenceRequest, max int32) ([]types.CommitDivergence, error)
GetRef(ctx context.Context, repoPath string, reference string) (string, error)
UpdateRef(ctx context.Context, envVars map[string]string, repoPath, reference, newValue, oldValue string) error
CreateTemporaryRepoForPR(ctx context.Context, reposTempPath string, pr *types.PullRequest,
baseBranch, trackingBranch string) (types.TempRepository, error)
Merge(ctx context.Context, pr *types.PullRequest, mergeMethod enum.MergeMethod, baseBranch, trackingBranch string,
tmpBasePath string, mergeMsg string, identity *types.Identity, env ...string) (types.MergeResult, error)
GetMergeBase(ctx context.Context, repoPath, remote, base, head string) (string, string, error)
IsAncestor(ctx context.Context, repoPath, ancestorCommitSHA, descendantCommitSHA string) (bool, error)
Blame(ctx context.Context, repoPath, rev, file string, lineFrom, lineTo int) types.BlameReader
Sync(ctx context.Context, repoPath string, source string, refSpecs []string) error
//
// Diff operations
//
GetDiffTree(ctx context.Context,
repoPath,
baseBranch,
headBranch string) (string, error)
RawDiff(ctx context.Context,
w io.Writer,
repoPath,
base,
head string,
mergeBase bool,
paths ...types.FileDiffRequest) error
CommitDiff(ctx context.Context,
repoPath,
sha string,
w io.Writer) error
DiffShortStat(ctx context.Context,
repoPath string,
baseRef string,
headRef string,
useMergeBase bool) (types.DiffShortStat, error)
GetDiffHunkHeaders(ctx context.Context,
repoPath string,
targetRef string,
sourceRef string) ([]*types.DiffFileHunkHeaders, error)
DiffCut(ctx context.Context,
repoPath string,
targetRef string,
sourceRef string,
path string,
params types.DiffCutParams) (types.HunkHeader, types.Hunk, error)
MatchFiles(ctx context.Context,
repoPath string,
ref string,
dirPath string,
regExpDef string,
maxSize int) ([]types.FileContent, error)
// http
InfoRefs(
ctx context.Context,
repoPath string,
service string,
w io.Writer,
env ...string,
) error
ServicePack(
ctx context.Context,
repoPath string,
service string,
stdin io.Reader,
stdout io.Writer,
env ...string,
) error
DiffFileName(ctx context.Context,
repoPath string,
baseRef string,
headRef string,
mergeBase bool) ([]string, error)
}

View File

@ -1,54 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter_test
import (
"context"
"io"
"testing"
"github.com/harness/gitness/errors"
)
func TestBlameEmptyFile(t *testing.T) {
git := setupGit(t)
repo, teardown := setupRepo(t, git, "testblameemptyfile")
defer teardown()
baseBranch := "main"
// write empty file to main branch
_, parentSHA := writeFile(t, repo, "file.txt", "", nil)
err := repo.SetReference("refs/heads/"+baseBranch, parentSHA.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", baseBranch, err)
}
reader := git.Blame(context.Background(), repo.Path, "main", "file.txt", 0, 0)
part, err := reader.NextPart()
if err != nil {
if errors.Is(err, io.EOF) {
return
}
t.Errorf("Blame reader should return empty string but got error: %v", err)
return
}
if part != nil {
t.Errorf("Blame reader should be nil but got: %v", part)
return
}
}

View File

@ -1,153 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter_test
import (
"bytes"
"context"
"testing"
)
func TestAdapter_RawDiff(t *testing.T) {
git := setupGit(t)
repo, teardown := setupRepo(t, git, "testrawdiff")
defer teardown()
testFileName := "file.txt"
baseBranch := "main"
// write file to main branch
oid1, parentSHA := writeFile(t, repo, testFileName, "some content", nil)
err := repo.SetReference("refs/heads/"+baseBranch, parentSHA.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", baseBranch, err)
}
baseTag := "0.0.1"
err = repo.CreateAnnotatedTag(baseTag, "test tag 1", parentSHA.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", baseTag, err)
}
headBranch := "dev"
// create branch
err = repo.CreateBranch(headBranch, baseBranch)
if err != nil {
t.Fatalf("failed creating a branch '%s': %v", headBranch, err)
}
// write file to main branch
oid2, sha := writeFile(t, repo, testFileName, "new content", []string{parentSHA.String()})
err = repo.SetReference("refs/heads/"+headBranch, sha.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", headBranch, err)
}
headTag := "0.0.2"
err = repo.CreateAnnotatedTag(headTag, "test tag 2", sha.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", headTag, err)
}
want := `diff --git a/` + testFileName + ` b/` + testFileName + `
index ` + oid1.String() + `..` + oid2.String() + ` 100644
--- a/` + testFileName + `
+++ b/` + testFileName + `
@@ -1 +1 @@
-some content
\ No newline at end of file
+new content
\ No newline at end of file
`
type args struct {
ctx context.Context
repoPath string
baseRef string
headRef string
mergeBase bool
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "test branches",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseBranch,
headRef: headBranch,
mergeBase: false,
},
wantW: want,
wantErr: false,
},
{
name: "test annotated tag",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseTag,
headRef: headTag,
mergeBase: false,
},
wantW: want,
wantErr: false,
},
{
name: "test branches using merge-base",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseBranch,
headRef: headBranch,
mergeBase: true,
},
wantW: want,
wantErr: false,
},
{
name: "test annotated tag using merge-base",
args: args{
ctx: context.Background(),
repoPath: repo.Path,
baseRef: baseTag,
headRef: headTag,
mergeBase: true,
},
wantW: want,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := git.RawDiff(tt.args.ctx, w, tt.args.repoPath, tt.args.baseRef, tt.args.headRef, tt.args.mergeBase)
if (err != nil) != tt.wantErr {
t.Errorf("RawDiff() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("RawDiff() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}

View File

@ -1,133 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter
import (
"os/exec"
"strings"
"github.com/harness/gitness/errors"
gitea "code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log"
)
var (
ErrRepositoryPathEmpty = errors.InvalidArgument("repository path cannot be empty")
ErrBranchNameEmpty = errors.InvalidArgument("branch name cannot be empty")
ErrParseDiffHunkHeader = errors.Internal(nil, "failed to parse diff hunk header")
)
type runStdError struct {
err error
stderr string
errMsg string
}
func (r *runStdError) Error() string {
// the stderr must be in the returned error text, some code only checks `strings.Contains(err.Error(), "git error")`
if r.errMsg == "" {
r.errMsg = gitea.ConcatenateError(r.err, r.stderr).Error()
}
return r.errMsg
}
func (r *runStdError) Unwrap() error {
return r.err
}
func (r *runStdError) Stderr() string {
return r.stderr
}
func (r *runStdError) IsExitCode(code int) bool {
var exitError *exec.ExitError
if errors.As(r.err, &exitError) {
return exitError.ExitCode() == code
}
return false
}
// Logs the error and message, returns either the provided message or a git equivalent if possible.
// Always logs the full message with error as warning.
func processGiteaErrorf(err error, format string, args ...interface{}) error {
// create fallback error returned if we can't map it
fallbackErr := errors.Internal(err, format, args...)
// always log internal error together with message.
log.Warn().Msgf("%v: [GITEA] %v", fallbackErr, err)
// check if it's a RunStdError error (contains raw git error)
var runStdErr gitea.RunStdError
if errors.As(err, &runStdErr) {
return mapGiteaRunStdError(runStdErr, fallbackErr)
}
switch {
// gitea is using errors.New(no such file or directory") exclusively for OpenRepository ... (at least as of now)
case err.Error() == "no such file or directory":
return errors.NotFound("repository not found")
case gitea.IsErrNotExist(err):
return errors.NotFound(format, args, err)
case gitea.IsErrBranchNotExist(err):
return errors.NotFound(format, args, err)
default:
return fallbackErr
}
}
// TODO: Improve gitea error handling.
// Doubt this will work for all std errors, as git doesn't seem to have nice error codes.
func mapGiteaRunStdError(err gitea.RunStdError, fallback error) error {
switch {
// exit status 128 - fatal: A branch named 'mybranch' already exists.
// exit status 128 - fatal: cannot lock ref 'refs/heads/a': 'refs/heads/a/b' exists; cannot create 'refs/heads/a'
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "exists"):
return errors.Conflict(err.Stderr())
// exit status 128 - fatal: 'a/bc/d/' is not a valid branch name.
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "not a valid"):
return errors.InvalidArgument(err.Stderr())
// exit status 1 - error: branch 'mybranch' not found.
case err.IsExitCode(1) && strings.Contains(err.Stderr(), "not found"):
return errors.NotFound(err.Stderr())
// exit status 128 - fatal: ambiguous argument 'branch1...branch2': unknown revision or path not in the working tree.
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "unknown revision"):
msg := "unknown revision or path not in the working tree"
// parse the error response from git output
lines := strings.Split(err.Error(), "\n")
if len(lines) > 0 {
cols := strings.Split(lines[0], ": ")
if len(cols) >= 2 {
msg = cols[1] + ", " + cols[2]
}
}
return errors.NotFound(msg)
// exit status 128 - fatal: couldn't find remote ref v1.
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "couldn't find"):
return errors.NotFound(err.Stderr())
// exit status 128 - fatal: unable to access 'http://127.0.0.1:4101/hvfl1xj5fojwlrw77xjflw80uxjous254jrr967rvj/':
// Failed to connect to 127.0.0.1 port 4101 after 4 ms: Connection refused
case err.IsExitCode(128) && strings.Contains(err.Stderr(), "Failed to connect"):
return errors.Internal(err, "failed to connect")
default:
return fallback
}
}

View File

@ -1,104 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter
import (
"fmt"
"strings"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
)
func mapGiteaRawRef(
raw map[string]string,
) (map[types.GitReferenceField]string, error) {
res := make(map[types.GitReferenceField]string, len(raw))
for k, v := range raw {
gitRefField, err := types.ParseGitReferenceField(k)
if err != nil {
return nil, err
}
res[gitRefField] = v
}
return res, nil
}
func mapToGiteaReferenceSortingArgument(
s types.GitReferenceField,
o types.SortOrder,
) string {
sortBy := string(types.GitReferenceFieldRefName)
desc := o == types.SortOrderDesc
if s == types.GitReferenceFieldCreatorDate {
sortBy = string(types.GitReferenceFieldCreatorDate)
if o == types.SortOrderDefault {
desc = true
}
}
if desc {
return "-" + sortBy
}
return sortBy
}
func mapGiteaCommit(giteaCommit *gitea.Commit) (*types.Commit, error) {
if giteaCommit == nil {
return nil, fmt.Errorf("gitea commit is nil")
}
author, err := mapGiteaSignature(giteaCommit.Author)
if err != nil {
return nil, fmt.Errorf("failed to map gitea author: %w", err)
}
committer, err := mapGiteaSignature(giteaCommit.Committer)
if err != nil {
return nil, fmt.Errorf("failed to map gitea commiter: %w", err)
}
parentShas := make([]string, len(giteaCommit.Parents))
for i := range giteaCommit.Parents {
parentShas[i] = giteaCommit.Parents[i].String()
}
return &types.Commit{
SHA: giteaCommit.ID.String(),
ParentSHAs: parentShas,
Title: giteaCommit.Summary(),
// remove potential tailing newlines from message
Message: strings.TrimRight(giteaCommit.Message(), "\n"),
Author: author,
Committer: committer,
}, nil
}
func mapGiteaSignature(
giteaSignature *gitea.Signature,
) (types.Signature, error) {
if giteaSignature == nil {
return types.Signature{}, fmt.Errorf("gitea signature is nil")
}
return types.Signature{
Identity: types.Identity{
Name: giteaSignature.Name,
Email: giteaSignature.Email,
},
When: giteaSignature.When,
}, nil
}

View File

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

View File

@ -1,123 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter_test
import (
"context"
"testing"
"github.com/harness/gitness/git/adapter"
)
func TestAdapter_GetMergeBase(t *testing.T) {
git := setupGit(t)
repo, teardown := setupRepo(t, git, "testmergebase")
defer teardown()
baseBranch := "main"
// write file to main branch
_, parentSHA := writeFile(t, repo, "file1.txt", "some content", nil)
err := repo.SetReference("refs/heads/"+baseBranch, parentSHA.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", baseBranch, err)
}
baseTag := "0.0.1"
err = repo.CreateAnnotatedTag(baseTag, "test tag 1", parentSHA.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", baseTag, err)
}
headBranch := "dev"
// create branch
err = repo.CreateBranch(headBranch, baseBranch)
if err != nil {
t.Fatalf("failed creating a branch '%s': %v", headBranch, err)
}
// write file to main branch
_, sha := writeFile(t, repo, "file1.txt", "new content", []string{parentSHA.String()})
err = repo.SetReference("refs/heads/"+headBranch, sha.String())
if err != nil {
t.Fatalf("failed updating reference '%s': %v", headBranch, err)
}
headTag := "0.0.2"
err = repo.CreateAnnotatedTag(headTag, "test tag 2", sha.String())
if err != nil {
t.Fatalf("error creating annotated tag '%s': %v", headTag, err)
}
type args struct {
ctx context.Context
repoPath string
remote string
base string
head string
}
tests := []struct {
name string
git adapter.Adapter
args args
want string
want1 string
wantErr bool
}{
{
name: "git merge base using branch names",
git: git,
args: args{
ctx: context.Background(),
repoPath: repo.Path,
remote: "",
base: baseBranch,
head: headBranch,
},
want: parentSHA.String(),
want1: baseBranch,
},
{
name: "git merge base using annotated tags",
git: git,
args: args{
ctx: context.Background(),
repoPath: repo.Path,
remote: "",
base: baseTag,
head: headTag,
},
want: parentSHA.String(),
want1: baseTag,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := tt.git.GetMergeBase(tt.args.ctx, tt.args.repoPath, tt.args.remote, tt.args.base, tt.args.head)
if (err != nil) != tt.wantErr {
t.Errorf("GetMergeBase() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetMergeBase() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("GetMergeBase() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

View File

@ -1,36 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter
// ObjectType git object type.
type ObjectType string
const (
// ObjectCommit commit object type.
ObjectCommit ObjectType = "commit"
// ObjectTree tree object type.
ObjectTree ObjectType = "tree"
// ObjectBlob blob object type.
ObjectBlob ObjectType = "blob"
// ObjectTag tag object type.
ObjectTag ObjectType = "tag"
// ObjectBranch branch object type.
ObjectBranch ObjectType = "branch"
)
// Bytes returns the byte array for the Object Type.
func (o ObjectType) Bytes() []byte {
return []byte(o)
}

View File

@ -1,136 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter_test
import (
"context"
"os"
"path"
"strings"
"testing"
"time"
"github.com/harness/gitness/git/adapter"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
)
type teardown func()
var (
testAuthor = &gitea.Signature{
Name: "test",
Email: "test@test.com",
}
testCommitter = &gitea.Signature{
Name: "test",
Email: "test@test.com",
}
)
type mockClientFactory struct{}
func (f *mockClientFactory) NewClient(_ context.Context, _ map[string]string) (hook.Client, error) {
return hook.NewNoopClient([]string{"mocked client"}), nil
}
func setupGit(t *testing.T) adapter.Adapter {
t.Helper()
git, err := adapter.New(
types.Config{Trace: true},
adapter.NewInMemoryLastCommitCache(5*time.Minute),
&mockClientFactory{},
)
if err != nil {
t.Fatalf("error initializing repository: %v", err)
}
return git
}
func setupRepo(t *testing.T, git adapter.Adapter, name string) (*gitea.Repository, teardown) {
t.Helper()
ctx := context.Background()
tmpdir := os.TempDir()
repoPath := path.Join(tmpdir, "test_repos", name)
err := git.InitRepository(ctx, repoPath, true)
if err != nil {
t.Fatalf("error initializing repository: %v", err)
}
repo, err := git.OpenRepository(ctx, repoPath)
if err != nil {
t.Fatalf("error opening repository '%s': %v", name, err)
}
err = repo.SetDefaultBranch("main")
if err != nil {
t.Fatalf("error setting default branch 'main': %v", err)
}
err = git.Config(ctx, repoPath, "user.email", testCommitter.Email)
if err != nil {
t.Fatalf("error setting config user.email %s: %v", testCommitter.Email, err)
}
err = git.Config(ctx, repoPath, "user.name", testCommitter.Name)
if err != nil {
t.Fatalf("error setting config user.name %s: %v", testCommitter.Name, err)
}
return repo, func() {
if err := os.RemoveAll(repoPath); err != nil {
t.Errorf("error while removeng the repository '%s'", repoPath)
}
}
}
func writeFile(
t *testing.T,
repo *gitea.Repository,
path string,
content string,
parents []string,
) (oid gitea.SHA1, commitSha gitea.SHA1) {
t.Helper()
oid, err := repo.HashObject(strings.NewReader(content))
if err != nil {
t.Fatalf("failed to hash object: %v", err)
}
err = repo.AddObjectToIndex("100644", oid, path)
if err != nil {
t.Fatalf("failed to add object to index: %v", err)
}
tree, err := repo.WriteTree()
if err != nil {
t.Fatalf("failed to write tree: %v", err)
}
commitSha, err = repo.CommitTree(testAuthor, testCommitter, tree, gitea.CommitTreeOpts{
Message: "write file operation",
Parents: parents,
})
if err != nil {
t.Fatalf("failed to commit tree: %v", err)
}
return oid, commitSha
}

View File

@ -1,59 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package adapter
import (
"context"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
)
// GetSubmodule returns the submodule at the given path reachable from ref.
// Note: ref can be Branch / Tag / CommitSHA.
func (a Adapter) GetSubmodule(
ctx context.Context,
repoPath string,
ref string,
treePath string,
) (*types.Submodule, error) {
if repoPath == "" {
return nil, ErrRepositoryPathEmpty
}
treePath = cleanTreePath(treePath)
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, processGiteaErrorf(err, "failed to open repository")
}
defer giteaRepo.Close()
// Get the giteaCommit object for the ref
giteaCommit, err := giteaRepo.GetCommit(ref)
if err != nil {
return nil, processGiteaErrorf(err, "error getting commit for ref '%s'", ref)
}
giteaSubmodule, err := giteaCommit.GetSubModule(treePath)
if err != nil {
return nil, processGiteaErrorf(err, "error getting submodule '%s' from commit", treePath)
}
return &types.Submodule{
Name: giteaSubmodule.Name,
URL: giteaSubmodule.URL,
}, nil
}

View File

@ -12,39 +12,26 @@
// See the License for the specific language governing permissions and
// 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,

View File

@ -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)

View File

@ -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 "

View File

@ -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(""),
}

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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)

View File

@ -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
View File

@ -0,0 +1,162 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"strings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/enum"
"github.com/rs/zerolog/log"
)
var (
ErrInvalidPath = errors.New("path is invalid")
ErrRepositoryPathEmpty = errors.InvalidArgument("repository path cannot be empty")
ErrBranchNameEmpty = errors.InvalidArgument("branch name cannot be empty")
ErrParseDiffHunkHeader = errors.Internal(nil, "failed to parse diff hunk header")
ErrNoDefaultBranch = errors.New("no default branch")
ErrInvalidSignature = errors.New("invalid signature")
)
// PushOutOfDateError represents an error if merging fails due to unrelated histories.
type PushOutOfDateError struct {
StdOut string
StdErr string
Err error
}
func (err *PushOutOfDateError) Error() string {
return fmt.Sprintf("PushOutOfDate Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// Unwrap unwraps the underlying error.
func (err *PushOutOfDateError) Unwrap() error {
return fmt.Errorf("%w - %s", err.Err, err.StdErr)
}
// PushRejectedError represents an error if merging fails due to rejection from a hook.
type PushRejectedError struct {
Message string
StdOut string
StdErr string
Err error
}
// IsErrPushRejected checks if an error is a PushRejectedError.
func IsErrPushRejected(err error) bool {
var errPushRejected *PushRejectedError
return errors.As(err, &errPushRejected)
}
func (err *PushRejectedError) Error() string {
return fmt.Sprintf("PushRejected Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// Unwrap unwraps the underlying error.
func (err *PushRejectedError) Unwrap() error {
return fmt.Errorf("%w - %s", err.Err, err.StdErr)
}
// GenerateMessage generates the remote message from the stderr.
func (err *PushRejectedError) GenerateMessage() {
messageBuilder := &strings.Builder{}
i := strings.Index(err.StdErr, "remote: ")
if i < 0 {
err.Message = ""
return
}
for {
if len(err.StdErr) <= i+8 {
break
}
if err.StdErr[i:i+8] != "remote: " {
break
}
i += 8
nl := strings.IndexByte(err.StdErr[i:], '\n')
if nl >= 0 {
messageBuilder.WriteString(err.StdErr[i : i+nl+1])
i = i + nl + 1
} else {
messageBuilder.WriteString(err.StdErr[i:])
i = len(err.StdErr)
}
}
err.Message = strings.TrimSpace(messageBuilder.String())
}
// MoreThanOneError represents an error when there are more
// than one sources (branch, tag) with the same name.
type MoreThanOneError struct {
StdOut string
StdErr string
Err error
}
// IsErrMoreThanOne checks if an error is a MoreThanOneError.
func IsErrMoreThanOne(err error) bool {
var errMoreThanOne *MoreThanOneError
return errors.As(err, &errMoreThanOne)
}
func (err *MoreThanOneError) Error() string {
return fmt.Sprintf("MoreThanOneError Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// Logs the error and message, returns either the provided message or a git equivalent if possible.
// Always logs the full message with error as warning.
func processGitErrorf(err error, format string, args ...interface{}) error {
// create fallback error returned if we can't map it
fallbackErr := errors.Internal(err, format, args...)
// always log internal error together with message.
log.Warn().Msgf("%v: [GIT] %v", fallbackErr, err)
switch {
case err.Error() == "no such file or directory":
return errors.NotFound("repository not found")
default:
return fallbackErr
}
}
// MergeUnrelatedHistoriesError represents an error if merging fails due to unrelated histories.
type MergeUnrelatedHistoriesError struct {
Method enum.MergeMethod
StdOut string
StdErr string
Err error
}
func IsMergeUnrelatedHistoriesError(err error) bool {
return errors.Is(err, &MergeUnrelatedHistoriesError{})
}
func (e *MergeUnrelatedHistoriesError) Error() string {
return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", e.Err, e.StdErr, e.StdOut)
}
func (e *MergeUnrelatedHistoriesError) Unwrap() error {
return e.Err
}
//nolint:errorlint // the purpose of this method is to check whether the target itself if of this type.
func (e *MergeUnrelatedHistoriesError) Is(target error) bool {
_, ok := target.(*MergeUnrelatedHistoriesError)
return ok
}

View File

@ -12,8 +12,22 @@
// See the License for the specific language governing permissions and
// 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
}

View File

@ -0,0 +1,94 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package foreachref
import (
"encoding/hex"
"fmt"
"io"
"strings"
)
var (
nullChar = []byte("\x00")
dualNullChar = []byte("\x00\x00")
)
// Format supports specifying and parsing an output format for 'git
// for-each-ref'. See See git-for-each-ref(1) for available fields.
type Format struct {
// fieldNames hold %(fieldname)s to be passed to the '--format' flag of
// for-each-ref. See git-for-each-ref(1) for available fields.
fieldNames []string
// fieldDelim is the character sequence that is used to separate fields
// for each reference. fieldDelim and refDelim should be selected to not
// interfere with each other and to not be present in field values.
fieldDelim []byte
// fieldDelimStr is a string representation of fieldDelim. Used to save
// us from repetitive reallocation whenever we need the delimiter as a
// string.
fieldDelimStr string
// refDelim is the character sequence used to separate reference from
// each other in the output. fieldDelim and refDelim should be selected
// to not interfere with each other and to not be present in field
// values.
refDelim []byte
}
// NewFormat creates a forEachRefFormat using the specified fieldNames. See
// git-for-each-ref(1) for available fields.
func NewFormat(fieldNames ...string) Format {
return Format{
fieldNames: fieldNames,
fieldDelim: nullChar,
fieldDelimStr: string(nullChar),
refDelim: dualNullChar,
}
}
// Flag returns a for-each-ref --format flag value that captures the fieldNames.
func (f Format) Flag() string {
var formatFlag strings.Builder
for i, field := range f.fieldNames {
// field key and field value
formatFlag.WriteString(fmt.Sprintf("%s %%(%s)", field, field))
if i < len(f.fieldNames)-1 {
// note: escape delimiters to allow control characters as
// delimiters. For example, '%00' for null character or '%0a'
// for newline.
formatFlag.WriteString(f.hexEscaped(f.fieldDelim))
}
}
formatFlag.WriteString(f.hexEscaped(f.refDelim))
return formatFlag.String()
}
// Parser returns a Parser capable of parsing 'git for-each-ref' output produced
// with this Format.
func (f Format) Parser(r io.Reader) *Parser {
return NewParser(r, f)
}
// hexEscaped produces hex-escpaed characters from a string. For example, "\n\0"
// would turn into "%0a%00".
func (f Format) hexEscaped(delim []byte) string {
escaped := ""
for i := 0; i < len(delim); i++ {
escaped += "%" + hex.EncodeToString([]byte{delim[i]})
}
return escaped
}

View File

@ -0,0 +1,140 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package foreachref
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
)
// Parser parses 'git for-each-ref' output according to a given output Format.
type Parser struct {
// tokenizes 'git for-each-ref' output into "reference paragraphs".
scanner *bufio.Scanner
// format represents the '--format' string that describes the expected
// 'git for-each-ref' output structure.
format Format
// err holds the last encountered error during parsing.
err error
}
// NewParser creates a 'git for-each-ref' output parser that will parse all
// references in the provided Reader. The references in the output are assumed
// to follow the specified Format.
func NewParser(r io.Reader, format Format) *Parser {
scanner := bufio.NewScanner(r)
// in addition to the reference delimiter we specified in the --format,
// `git for-each-ref` will always add a newline after every reference.
refDelim := make([]byte, 0, len(format.refDelim)+1)
refDelim = append(refDelim, format.refDelim...)
refDelim = append(refDelim, '\n')
// Split input into delimiter-separated "reference blocks".
scanner.Split(
func(data []byte, atEOF bool) (int, []byte, error) {
// Scan until delimiter, marking end of reference.
delimIdx := bytes.Index(data, refDelim)
if delimIdx >= 0 {
token := data[:delimIdx]
advance := delimIdx + len(refDelim)
return advance, token, nil
}
// If we're at EOF, we have a final, non-terminated reference. Return it.
if atEOF {
return len(data), data, nil
}
// Not yet a full field. Request more data.
return 0, nil, nil
})
return &Parser{
scanner: scanner,
format: format,
err: nil,
}
}
// Next returns the next reference as a collection of key-value pairs. nil
// denotes EOF but is also returned on errors. The Err method should always be
// consulted after Next returning nil.
//
// It could, for example return something like:
//
// { "objecttype": "tag", "refname:short": "v1.16.4", "object": "f460b7543ed500e49c133c2cd85c8c55ee9dbe27" }
func (p *Parser) Next() map[string]string {
if !p.scanner.Scan() {
return nil
}
fields, err := p.parseRef(p.scanner.Text())
if err != nil && !errors.Is(err, io.EOF) {
p.err = err
return nil
}
return fields
}
// Err returns the latest encountered parsing error.
func (p *Parser) Err() error {
return p.err
}
// parseRef parses out all key-value pairs from a single reference block, such as
//
// "type tag\0ref:short v1.16.4\0object f460b7543ed500e49c133c2cd85c8c55ee9dbe27"
func (p *Parser) parseRef(refBlock string) (map[string]string, error) {
if refBlock == "" {
// must be at EOF
return nil, io.EOF
}
fieldValues := make(map[string]string)
fields := strings.Split(refBlock, p.format.fieldDelimStr)
if len(fields) != len(p.format.fieldNames) {
return nil, fmt.Errorf("unexpected number of reference fields: wanted %d, was %d",
len(fields), len(p.format.fieldNames))
}
for i, field := range fields {
field = strings.TrimSpace(field)
var fieldKey string
var fieldVal string
firstSpace := strings.Index(field, " ")
if firstSpace > 0 {
fieldKey = field[:firstSpace]
fieldVal = field[firstSpace+1:]
} else {
// could be the case if the requested field had no value
fieldKey = field
}
// enforce the format order of fields
if p.format.fieldNames[i] != fieldKey {
return nil, fmt.Errorf("unexpected field name at position %d: wanted: '%s', was: '%s'",
i, p.format.fieldNames[i], fieldKey)
}
fieldValues[fieldKey] = fieldVal
}
return fieldValues, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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"
)

View File

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

View File

@ -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
View File

@ -0,0 +1,51 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
func mapRawRef(
raw map[string]string,
) (map[GitReferenceField]string, error) {
res := make(map[GitReferenceField]string, len(raw))
for k, v := range raw {
gitRefField, err := ParseGitReferenceField(k)
if err != nil {
return nil, err
}
res[gitRefField] = v
}
return res, nil
}
func mapToReferenceSortingArgument(
s GitReferenceField,
o SortOrder,
) string {
sortBy := string(GitReferenceFieldRefName)
desc := o == SortOrderDesc
if s == GitReferenceFieldCreatorDate {
sortBy = string(GitReferenceFieldCreatorDate)
if o == SortOrderDefault {
desc = true
}
}
if desc {
return "-" + sortBy
}
return sortBy
}

View File

@ -12,39 +12,40 @@
// See the License for the specific language governing permissions and
// 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
View File

@ -0,0 +1,104 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"bytes"
"context"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/sha"
)
const (
// RemotePrefix is the base directory of the remotes information of git.
RemotePrefix = "refs/remotes/"
)
// GetMergeBase checks and returns merge base of two branches and the reference used as base.
func (g *Git) GetMergeBase(
ctx context.Context,
repoPath string,
remote string,
base string,
head string,
) (sha.SHA, string, error) {
if repoPath == "" {
return sha.None, "", ErrRepositoryPathEmpty
}
if remote == "" {
remote = "origin"
}
if remote != "origin" {
tmpBaseName := RemotePrefix + remote + "/tmp_" + base
// Fetch commit into a temporary branch in order to be able to handle commits and tags
cmd := command.New("fetch",
command.WithFlag("--no-tags"),
command.WithArg(remote),
command.WithPostSepArg(base+":"+tmpBaseName),
)
err := cmd.Run(ctx, command.WithDir(repoPath))
if err == nil {
base = tmpBaseName
}
}
stdout := &bytes.Buffer{}
cmd := command.New("merge-base",
command.WithArg(base, head),
)
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdout(stdout),
)
if err != nil {
return sha.None, "", processGitErrorf(err, "failed to get merge-base [%s, %s]", base, head)
}
result, err := sha.New(stdout.String())
if err != nil {
return sha.None, "", err
}
return result, base, nil
}
// IsAncestor returns if the provided commit SHA is ancestor of the other commit SHA.
func (g *Git) IsAncestor(
ctx context.Context,
repoPath string,
ancestorCommitSHA, descendantCommitSHA sha.SHA,
) (bool, error) {
if repoPath == "" {
return false, ErrRepositoryPathEmpty
}
cmd := command.New("merge-base",
command.WithFlag("--is-ancestor"),
command.WithArg(ancestorCommitSHA.String(), descendantCommitSHA.String()),
)
err := cmd.Run(ctx, command.WithDir(repoPath))
if err != nil {
cmdErr := command.AsError(err)
if cmdErr != nil && cmdErr.IsExitCode(1) && len(cmdErr.StdErr) == 0 {
return false, nil
}
return false, processGitErrorf(err, "failed to check commit ancestry [%s, %s]",
ancestorCommitSHA, descendantCommitSHA)
}
return true, nil
}

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

@ -0,0 +1,74 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"bytes"
"context"
"fmt"
"io"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/sha"
)
type GitObjectType string
const (
GitObjectTypeCommit GitObjectType = "commit"
GitObjectTypeTree GitObjectType = "tree"
GitObjectTypeBlob GitObjectType = "blob"
GitObjectTypeTag GitObjectType = "tag"
)
func ParseGitObjectType(t string) (GitObjectType, error) {
switch t {
case string(GitObjectTypeCommit):
return GitObjectTypeCommit, nil
case string(GitObjectTypeBlob):
return GitObjectTypeBlob, nil
case string(GitObjectTypeTree):
return GitObjectTypeTree, nil
case string(GitObjectTypeTag):
return GitObjectTypeTag, nil
default:
return GitObjectTypeBlob, fmt.Errorf("unknown git object type '%s'", t)
}
}
type SortOrder int
const (
SortOrderDefault SortOrder = iota
SortOrderAsc
SortOrderDesc
)
func (g *Git) HashObject(ctx context.Context, repoPath string, reader io.Reader) (sha.SHA, error) {
cmd := command.New("hash-object",
command.WithFlag("-w"),
command.WithFlag("--stdin"),
)
stdout := new(bytes.Buffer)
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdin(reader),
command.WithStdout(stdout),
)
if err != nil {
return sha.None, fmt.Errorf("failed to hash object: %w", err)
}
return sha.New(stdout.String())
}

View File

@ -12,35 +12,38 @@
// See the License for the specific language governing permissions and
// 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)
}

View File

@ -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"))

View File

@ -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")

View File

@ -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())
}

View File

@ -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
View File

@ -0,0 +1,63 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"time"
"github.com/harness/gitness/errors"
)
// Signature represents the Author or Committer information.
type Signature struct {
Identity Identity
// When is the timestamp of the Signature.
When time.Time
}
// DecodeSignature decodes a byte array representing a signature to signature.
func DecodeSignature(b []byte) (Signature, error) {
sig, err := NewSignatureFromCommitLine(b)
if err != nil {
return Signature{}, fmt.Errorf("failed to read signature from commit: %w", err)
}
return sig, nil
}
func (s Signature) String() string {
return fmt.Sprintf("%s <%s>", s.Identity.Name, s.Identity.Email)
}
type Identity struct {
Name string
Email string
}
func (i Identity) String() string {
return fmt.Sprintf("%s <%s>", i.Name, i.Email)
}
func (i Identity) Validate() error {
if i.Name == "" {
return errors.InvalidArgument("identity name is mandatory")
}
if i.Email == "" {
return errors.InvalidArgument("identity email is mandatory")
}
return nil
}

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

@ -0,0 +1,91 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"bufio"
"context"
"strings"
)
type Submodule struct {
Name string
URL string
}
// GetSubmodule returns the submodule at the given path reachable from ref.
// Note: ref can be Branch / Tag / CommitSHA.
func (g *Git) GetSubmodule(
ctx context.Context,
repoPath string,
ref string,
treePath string,
) (*Submodule, error) {
if repoPath == "" {
return nil, ErrRepositoryPathEmpty
}
treePath = cleanTreePath(treePath)
// Get the commit object for the ref
commit, err := g.GetFullCommitID(ctx, repoPath, ref)
if err != nil {
return nil, processGitErrorf(err, "error getting commit for ref '%s'", ref)
}
node, err := g.GetTreeNode(ctx, repoPath, commit.String(), ".gitmodules")
if err != nil {
return nil, processGitErrorf(err, "error reading tree node for ref '%s' with commit '%s'",
ref, commit)
}
reader, err := g.GetBlob(ctx, repoPath, node.SHA, 0)
if err != nil {
return nil, processGitErrorf(err, "error reading commit for ref '%s'", ref)
}
defer reader.Content.Close()
modules, err := GetSubModules(reader)
if err != nil {
return nil, processGitErrorf(err, "error getting submodule '%s' from commit", treePath)
}
return modules[treePath], nil
}
// GetSubModules get all the sub modules of current revision git tree.
func GetSubModules(rd *BlobReader) (map[string]*Submodule, error) {
var isModule bool
var path string
submodules := make(map[string]*Submodule, 4)
scanner := bufio.NewScanner(rd.Content)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "[submodule") {
isModule = true
continue
}
if isModule {
fields := strings.Split(scanner.Text(), "=")
k := strings.TrimSpace(fields[0])
if k == "path" {
path = strings.TrimSpace(fields[1])
} else if k == "url" {
submodules[path] = &Submodule{path, strings.TrimSpace(fields[1])}
isModule = false
}
}
}
return submodules, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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
}

View File

@ -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
View File

@ -0,0 +1,139 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"bytes"
"fmt"
"strconv"
"time"
"unicode"
"github.com/yuin/goldmark/util"
)
const (
// GitTimeLayout is the (default) time layout used by git.
GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
userPlaceholder = "sanitized-credential"
)
var schemeSep = []byte("://")
func NewSignatureFromCommitLine(line []byte) (Signature, error) {
emailStart := bytes.LastIndexByte(line, '<')
emailEnd := bytes.LastIndexByte(line, '>')
if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
return Signature{}, ErrInvalidSignature
}
sig := Signature{
Identity: Identity{
Name: string(line[:emailStart-1]),
Email: string(line[emailStart+1 : emailEnd]),
},
}
dateStart := emailEnd + 2
hasTime := dateStart < len(line)
if !hasTime {
return sig, nil
}
// Check date format.
firstChar := line[dateStart]
//nolint:nestif
if firstChar >= 48 && firstChar <= 57 {
idx := bytes.IndexByte(line[dateStart:], ' ')
if idx < 0 {
return sig, nil
}
timestring := string(line[dateStart : dateStart+idx])
seconds, _ := strconv.ParseInt(timestring, 10, 64)
sig.When = time.Unix(seconds, 0)
idx += emailEnd + 3
if idx >= len(line) || idx+5 > len(line) {
return sig, nil
}
timezone := string(line[idx : idx+5])
tzhours, err := strconv.ParseInt(timezone[0:3], 10, 64)
if err != nil {
return Signature{}, fmt.Errorf("failed to parse tzhours: %w", err)
}
tzmins, err := strconv.ParseInt(timezone[3:], 10, 64)
if err != nil {
return Signature{}, fmt.Errorf("failed to parse tzmins: %w", err)
}
if tzhours < 0 {
tzmins *= -1
}
tz := time.FixedZone("", int(tzhours*60*60+tzmins*60))
sig.When = sig.When.In(tz)
} else {
t, err := time.Parse(GitTimeLayout, string(line[dateStart:]))
if err != nil {
return Signature{}, fmt.Errorf("failed to parse git time: %w", err)
}
sig.When = t
}
return sig, nil
}
// SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://")
// for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com"
func SanitizeCredentialURLs(s string) string {
bs := util.StringToReadOnlyBytes(s)
schemeSepPos := bytes.Index(bs, schemeSep)
if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 {
return s // fast return if there is no URL scheme or no userinfo
}
out := make([]byte, 0, len(bs)+len(userPlaceholder))
for schemeSepPos != -1 {
schemeSepPos += 3 // skip the "://"
sepAtPos := -1 // the possible '@' position: "https://foo@[^here]host"
sepEndPos := schemeSepPos // the possible end position: "The https://host[^here] in log for test"
sepLoop:
for ; sepEndPos < len(bs); sepEndPos++ {
c := bs[sepEndPos]
if ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') {
continue
}
switch c {
case '@':
sepAtPos = sepEndPos
case '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '%':
continue // due to RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars
default:
break sepLoop // if it is an invalid char for URL (eg: space, '/', and others), stop the loop
}
}
// if there is '@', and the string is like "s://u@h", then hide the "u" part
if sepAtPos != -1 && (schemeSepPos >= 4 && unicode.IsLetter(rune(bs[schemeSepPos-4]))) &&
sepAtPos-schemeSepPos > 0 && sepEndPos-sepAtPos > 0 {
out = append(out, bs[:schemeSepPos]...)
out = append(out, userPlaceholder...)
out = append(out, bs[sepAtPos:sepEndPos]...)
} else {
out = append(out, bs[:sepEndPos]...)
}
bs = bs[sepEndPos:]
schemeSepPos = bytes.Index(bs, schemeSep)
}
out = append(out, bs...)
return util.BytesToReadOnlyString(out)
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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

View File

@ -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)

View File

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

View File

@ -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?

View File

@ -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")
}

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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...)
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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],
})
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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) {

View File

@ -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