feat: files in webhook payload (#1013)

eb/code-1016-2
Abhinav Singh 2024-02-06 02:06:43 +00:00 committed by Harness
parent 646c8fbe75
commit e0f8248ead
12 changed files with 340 additions and 100 deletions

View File

@ -23,6 +23,7 @@ import (
pullreqevents "github.com/harness/gitness/app/events/pullreq" pullreqevents "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/git" "github.com/harness/gitness/git"
"github.com/harness/gitness/git/enum"
) )
// handleFileViewedOnBranchUpdate handles pull request Branch Updated events. // handleFileViewedOnBranchUpdate handles pull request Branch Updated events.
@ -63,15 +64,16 @@ func (s *Service) handleFileViewedOnBranchUpdate(ctx context.Context,
// UPDATED: mark as obsolete - in case pr is closed file SHA is handling it // UPDATED: mark as obsolete - in case pr is closed file SHA is handling it
// This strategy leads to a behavior very similar to what github is doing // This strategy leads to a behavior very similar to what github is doing
switch fileDiff.Status { switch fileDiff.Status {
case git.FileDiffStatusAdded: case enum.FileDiffStatusAdded:
obsoletePaths = append(obsoletePaths, fileDiff.Path) obsoletePaths = append(obsoletePaths, fileDiff.Path)
case git.FileDiffStatusDeleted: case enum.FileDiffStatusDeleted:
obsoletePaths = append(obsoletePaths, fileDiff.OldPath) obsoletePaths = append(obsoletePaths, fileDiff.OldPath)
case git.FileDiffStatusRenamed: case enum.FileDiffStatusRenamed:
obsoletePaths = append(obsoletePaths, fileDiff.OldPath, fileDiff.Path) obsoletePaths = append(obsoletePaths, fileDiff.OldPath, fileDiff.Path)
case git.FileDiffStatusModified: case enum.FileDiffStatusModified:
obsoletePaths = append(obsoletePaths, fileDiff.Path) obsoletePaths = append(obsoletePaths, fileDiff.Path)
case git.FileDiffStatusUndefined: case enum.FileDiffStatusCopied:
case enum.FileDiffStatusUndefined:
// other cases we don't care // other cases we don't care
} }
} }

View File

@ -26,6 +26,8 @@ import (
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
const MaxWebhookCommitFileStats = 20
// ReferencePayload describes the payload of Reference related webhook triggers. // ReferencePayload describes the payload of Reference related webhook triggers.
// Note: Use same payload for all reference operations to make it easier for consumers. // Note: Use same payload for all reference operations to make it easier for consumers.
type ReferencePayload struct { type ReferencePayload struct {
@ -61,8 +63,9 @@ func (s *Service) handleEventBranchCreated(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.SHA, SHA: event.Payload.SHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
}, },
ReferenceUpdateSegment: ReferenceUpdateSegment{ ReferenceUpdateSegment: ReferenceUpdateSegment{
OldSHA: types.NilSHA, OldSHA: types.NilSHA,
@ -79,10 +82,13 @@ func (s *Service) handleEventBranchUpdated(ctx context.Context,
return s.triggerForEventWithRepo(ctx, enum.WebhookTriggerBranchUpdated, return s.triggerForEventWithRepo(ctx, enum.WebhookTriggerBranchUpdated,
event.ID, event.Payload.PrincipalID, event.Payload.RepoID, event.ID, event.Payload.PrincipalID, event.Payload.RepoID,
func(principal *types.Principal, repo *types.Repository) (any, error) { func(principal *types.Principal, repo *types.Repository) (any, error) {
commitInfo, err := s.fetchCommitInfoForEvent(ctx, repo.GitUID, event.Payload.NewSHA) commitsInfo, totalCommits, err := s.fetchCommitsInfoForEvent(ctx, repo.GitUID,
event.Payload.OldSHA, event.Payload.NewSHA)
if err != nil { if err != nil {
return nil, err return nil, err
} }
commitInfo := commitsInfo[0]
repoInfo := repositoryInfoFrom(repo, s.urlProvider) repoInfo := repositoryInfoFrom(repo, s.urlProvider)
return &ReferencePayload{ return &ReferencePayload{
@ -98,8 +104,11 @@ func (s *Service) handleEventBranchUpdated(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.NewSHA, SHA: event.Payload.NewSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
Commits: &commitsInfo,
TotalCommitsCount: totalCommits,
}, },
ReferenceUpdateSegment: ReferenceUpdateSegment{ ReferenceUpdateSegment: ReferenceUpdateSegment{
OldSHA: event.Payload.OldSHA, OldSHA: event.Payload.OldSHA,
@ -152,13 +161,46 @@ func (s *Service) fetchCommitInfoForEvent(ctx context.Context, repoUID string, s
if errors.AsStatus(err) == errors.StatusNotFound { if errors.AsStatus(err) == errors.StatusNotFound {
// this could happen if the commit has been deleted and garbage collected by now // this could happen if the commit has been deleted and garbage collected by now
// or if the sha doesn't point to an event - either way discard the event. // or if the targetSha doesn't point to an event - either way discard the event.
return CommitInfo{}, events.NewDiscardEventErrorf("commit with sha '%s' doesn't exist", sha) return CommitInfo{}, events.NewDiscardEventErrorf("commit with targetSha '%s' doesn't exist", sha)
} }
if err != nil { if err != nil {
return CommitInfo{}, fmt.Errorf("failed to get commit with sha '%s': %w", sha, err) return CommitInfo{}, fmt.Errorf("failed to get commit with targetSha '%s': %w", sha, err)
} }
return commitInfoFrom(out.Commit), nil return commitInfoFrom(out.Commit), nil
} }
func (s *Service) fetchCommitsInfoForEvent(
ctx context.Context,
repoUID string,
oldSHA string,
newSHA string,
) ([]CommitInfo, int, error) {
listCommitsParams := git.ListCommitsParams{
ReadParams: git.ReadParams{RepoUID: repoUID},
GitREF: newSHA,
After: oldSHA,
Page: 0,
Limit: MaxWebhookCommitFileStats,
IncludeFileStats: true,
}
listCommitsOutput, err := s.git.ListCommits(ctx, &listCommitsParams)
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{}, 0, events.NewDiscardEventErrorf("commit with targetSha '%s' doesn't exist", newSHA)
}
if err != nil {
return []CommitInfo{}, 0, fmt.Errorf("failed to get commit with targetSha '%s': %w", newSHA, err)
}
if len(listCommitsOutput.Commits) == 0 {
return nil, 0, fmt.Errorf("no commit found between %s and %s", oldSHA, newSHA)
}
return commitsInfoFrom(listCommitsOutput.Commits), listCommitsOutput.TotalCommits, nil
}

View File

@ -75,8 +75,9 @@ func (s *Service) handleEventPullReqCreated(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.SourceSHA, SHA: event.Payload.SourceSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
}, },
}, nil }, nil
}) })
@ -122,8 +123,9 @@ func (s *Service) handleEventPullReqReopened(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.SourceSHA, SHA: event.Payload.SourceSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
}, },
}, nil }, nil
}) })
@ -147,10 +149,13 @@ func (s *Service) handleEventPullReqBranchUpdated(ctx context.Context,
return s.triggerForEventWithPullReq(ctx, enum.WebhookTriggerPullReqBranchUpdated, return s.triggerForEventWithPullReq(ctx, enum.WebhookTriggerPullReqBranchUpdated,
event.ID, event.Payload.PrincipalID, event.Payload.PullReqID, event.ID, event.Payload.PrincipalID, event.Payload.PullReqID,
func(principal *types.Principal, pr *types.PullReq, targetRepo, sourceRepo *types.Repository) (any, error) { func(principal *types.Principal, pr *types.PullReq, targetRepo, sourceRepo *types.Repository) (any, error) {
commitInfo, err := s.fetchCommitInfoForEvent(ctx, sourceRepo.GitUID, event.Payload.NewSHA) commitsInfo, totalCommits, err := s.fetchCommitsInfoForEvent(ctx, sourceRepo.GitUID,
event.Payload.OldSHA, event.Payload.NewSHA)
if err != nil { if err != nil {
return nil, err return nil, err
} }
commitInfo := commitsInfo[0]
targetRepoInfo := repositoryInfoFrom(targetRepo, s.urlProvider) targetRepoInfo := repositoryInfoFrom(targetRepo, s.urlProvider)
sourceRepoInfo := repositoryInfoFrom(sourceRepo, s.urlProvider) sourceRepoInfo := repositoryInfoFrom(sourceRepo, s.urlProvider)
@ -176,8 +181,11 @@ func (s *Service) handleEventPullReqBranchUpdated(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.NewSHA, SHA: event.Payload.NewSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
Commits: &commitsInfo,
TotalCommitsCount: totalCommits,
}, },
ReferenceUpdateSegment: ReferenceUpdateSegment{ ReferenceUpdateSegment: ReferenceUpdateSegment{
OldSHA: event.Payload.OldSHA, OldSHA: event.Payload.OldSHA,
@ -230,8 +238,9 @@ func (s *Service) handleEventPullReqClosed(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.SourceSHA, SHA: event.Payload.SourceSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
}, },
}, nil }, nil
}) })
@ -280,8 +289,9 @@ func (s *Service) handleEventPullReqMerged(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.SourceSHA, SHA: event.Payload.SourceSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
}, },
}, nil }, nil
}) })
@ -336,8 +346,9 @@ func (s *Service) handleEventPullReqComment(
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.SourceSHA, SHA: event.Payload.SourceSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
}, },
PullReqCommentSegment: PullReqCommentSegment{ PullReqCommentSegment: PullReqCommentSegment{
CommentInfo: CommentInfo{ CommentInfo: CommentInfo{

View File

@ -49,8 +49,9 @@ func (s *Service) handleEventTagCreated(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.SHA, SHA: event.Payload.SHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
}, },
ReferenceUpdateSegment: ReferenceUpdateSegment{ ReferenceUpdateSegment: ReferenceUpdateSegment{
OldSHA: types.NilSHA, OldSHA: types.NilSHA,
@ -67,10 +68,16 @@ func (s *Service) handleEventTagUpdated(ctx context.Context,
return s.triggerForEventWithRepo(ctx, enum.WebhookTriggerTagUpdated, return s.triggerForEventWithRepo(ctx, enum.WebhookTriggerTagUpdated,
event.ID, event.Payload.PrincipalID, event.Payload.RepoID, event.ID, event.Payload.PrincipalID, event.Payload.RepoID,
func(principal *types.Principal, repo *types.Repository) (any, error) { func(principal *types.Principal, repo *types.Repository) (any, error) {
commitInfo, err := s.fetchCommitInfoForEvent(ctx, repo.GitUID, event.Payload.NewSHA) commitsInfo, totalCommits, err := s.fetchCommitsInfoForEvent(ctx, repo.GitUID,
event.Payload.OldSHA, event.Payload.NewSHA)
if err != nil { if err != nil {
return nil, err return nil, err
} }
commitInfo := CommitInfo{}
if len(commitsInfo) > 0 {
commitInfo = commitsInfo[0]
}
repoInfo := repositoryInfoFrom(repo, s.urlProvider) repoInfo := repositoryInfoFrom(repo, s.urlProvider)
return &ReferencePayload{ return &ReferencePayload{
@ -86,8 +93,11 @@ func (s *Service) handleEventTagUpdated(ctx context.Context,
}, },
}, },
ReferenceDetailsSegment: ReferenceDetailsSegment{ ReferenceDetailsSegment: ReferenceDetailsSegment{
SHA: event.Payload.NewSHA, SHA: event.Payload.NewSHA,
Commit: &commitInfo, Commit: &commitInfo,
HeadCommit: &commitInfo,
Commits: &commitsInfo,
TotalCommitsCount: totalCommits,
}, },
ReferenceUpdateSegment: ReferenceUpdateSegment{ ReferenceUpdateSegment: ReferenceUpdateSegment{
OldSHA: event.Payload.OldSHA, OldSHA: event.Payload.OldSHA,

View File

@ -42,9 +42,16 @@ type ReferenceSegment struct {
Ref ReferenceInfo `json:"ref"` Ref ReferenceInfo `json:"ref"`
} }
// ReferenceDetailsSegment contains extra defails for reference related payloads for webhooks. // ReferenceDetailsSegment contains extra details for reference related payloads for webhooks.
type ReferenceDetailsSegment struct { type ReferenceDetailsSegment struct {
SHA string `json:"sha"` SHA string `json:"sha"`
HeadCommit *CommitInfo `json:"head_commit,omitempty"`
Commits *[]CommitInfo `json:"commits,omitempty"`
TotalCommitsCount int `json:"total_commits_count,omitempty"`
// Deprecated
Commit *CommitInfo `json:"commit,omitempty"` Commit *CommitInfo `json:"commit,omitempty"`
} }
@ -168,6 +175,10 @@ type CommitInfo struct {
Message string `json:"message"` Message string `json:"message"`
Author SignatureInfo `json:"author"` Author SignatureInfo `json:"author"`
Committer SignatureInfo `json:"committer"` Committer SignatureInfo `json:"committer"`
Added []string `json:"added"`
Removed []string `json:"removed"`
Modified []string `json:"modified"`
} }
// commitInfoFrom gets the CommitInfo from a git.Commit. // commitInfoFrom gets the CommitInfo from a git.Commit.
@ -177,9 +188,21 @@ func commitInfoFrom(commit git.Commit) CommitInfo {
Message: commit.Message, Message: commit.Message,
Author: signatureInfoFrom(commit.Author), Author: signatureInfoFrom(commit.Author),
Committer: signatureInfoFrom(commit.Committer), Committer: signatureInfoFrom(commit.Committer),
Added: commit.FileStats.Added,
Removed: commit.FileStats.Removed,
Modified: commit.FileStats.Modified,
} }
} }
// commitsInfoFrom gets the ExtendedCommitInfo from a []git.Commit.
func commitsInfoFrom(commits []git.Commit) []CommitInfo {
commitsInfo := make([]CommitInfo, len(commits))
for i, commit := range commits {
commitsInfo[i] = commitInfoFrom(commit)
}
return commitsInfo
}
// SignatureInfo describes the commit signature related info for a webhook payload. // SignatureInfo describes the commit signature related info for a webhook payload.
// NOTE: don't use types package as we want webhook payload to be independent from API calls. // NOTE: don't use types package as we want webhook payload to be independent from API calls.
type SignatureInfo struct { type SignatureInfo struct {

View File

@ -55,8 +55,14 @@ type Adapter interface {
opts *types.WalkReferencesOptions) error opts *types.WalkReferencesOptions) error
GetCommit(ctx context.Context, repoPath string, ref string) (*types.Commit, error) GetCommit(ctx context.Context, repoPath string, ref string) (*types.Commit, error)
GetCommits(ctx context.Context, repoPath string, refs []string) ([]types.Commit, error) GetCommits(ctx context.Context, repoPath string, refs []string) ([]types.Commit, error)
ListCommits(ctx context.Context, repoPath string, ListCommits(
ref string, page int, limit int, filter types.CommitFilter) ([]types.Commit, []types.PathRenameDetails, error) ctx context.Context,
repoPath string,
ref string,
page int,
limit int,
includeFileStats bool,
filter types.CommitFilter) ([]types.Commit, []types.PathRenameDetails, error)
ListCommitSHAs(ctx context.Context, repoPath string, ListCommitSHAs(ctx context.Context, repoPath string,
ref string, page int, limit int, filter types.CommitFilter) ([]string, error) ref string, page int, limit int, filter types.CommitFilter) ([]string, error)
GetLatestCommit(ctx context.Context, repoPath string, ref string, treePath string) (*types.Commit, error) GetLatestCommit(ctx context.Context, repoPath string, ref string, treePath string) (*types.Commit, error)

View File

@ -24,9 +24,11 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command" "github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git" gitea "code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log"
) )
// GetLatestCommit gets the latest commit of a path relative from the provided revision. // GetLatestCommit gets the latest commit of a path relative from the provided revision.
@ -135,11 +137,13 @@ func (a Adapter) ListCommitSHAs(
// ListCommits lists the commits reachable from ref. // ListCommits lists the commits reachable from ref.
// Note: ref & afterRef can be Branch / Tag / CommitSHA. // Note: ref & afterRef can be Branch / Tag / CommitSHA.
// Note: commits returned are [ref->...->afterRef). // Note: commits returned are [ref->...->afterRef).
func (a Adapter) ListCommits(ctx context.Context, func (a Adapter) ListCommits(
ctx context.Context,
repoPath string, repoPath string,
ref string, ref string,
page int, page int,
limit int, limit int,
includeFileStats bool,
filter types.CommitFilter, filter types.CommitFilter,
) ([]types.Commit, []types.PathRenameDetails, error) { ) ([]types.Commit, []types.PathRenameDetails, error) {
if repoPath == "" { if repoPath == "" {
@ -169,10 +173,17 @@ func (a Adapter) ListCommits(ctx context.Context,
return nil, nil, err return nil, nil, err
} }
commits[i] = *commit commits[i] = *commit
if includeFileStats {
err = includeFileStatsInCommits(ctx, giteaRepo, commits)
if err != nil {
return nil, nil, err
}
}
} }
if len(filter.Path) != 0 { if len(filter.Path) != 0 {
renameDetailsList, err := getRenameDetails(giteaRepo, commits, filter.Path) renameDetailsList, err := getRenameDetails(ctx, giteaRepo, commits, filter.Path)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -183,6 +194,52 @@ func (a Adapter) ListCommits(ctx context.Context,
return commits, nil, nil return commits, nil, nil
} }
func includeFileStatsInCommits(
ctx context.Context,
giteaRepo *gitea.Repository,
commits []types.Commit,
) error {
for i, commit := range commits {
fileStats, err := getFileStats(ctx, giteaRepo, commit.SHA)
if err != nil {
return fmt.Errorf("failed to get file stat: %w", err)
}
commits[i].FileStats = fileStats
}
return nil
}
func getFileStats(
ctx context.Context,
giteaRepo *gitea.Repository,
sha string,
) (types.CommitFileStats, error) {
changeInfos, err := getChangeInfos(ctx, giteaRepo, sha)
if err != nil {
return types.CommitFileStats{}, fmt.Errorf("failed to get change infos: %w", err)
}
fileStats := types.CommitFileStats{
Added: make([]string, 0),
Removed: make([]string, 0),
Modified: make([]string, 0),
}
for _, c := range changeInfos {
switch {
case c.ChangeType == enum.FileDiffStatusModified || c.ChangeType == enum.FileDiffStatusRenamed:
fileStats.Modified = append(fileStats.Modified, c.Path)
case c.ChangeType == enum.FileDiffStatusDeleted:
fileStats.Removed = append(fileStats.Removed, c.Path)
case c.ChangeType == enum.FileDiffStatusAdded || c.ChangeType == enum.FileDiffStatusCopied:
fileStats.Added = append(fileStats.Added, c.Path)
case c.ChangeType == enum.FileDiffStatusUndefined:
default:
log.Ctx(ctx).Warn().Msgf("unknown change type %q for path %q",
c.ChangeType, c.Path)
}
}
return fileStats, nil
}
// In case of rename of a file, same commit will be listed twice - Once in old file and second time in new file. // In case of rename of a file, same commit will be listed twice - Once in old file and second time in new file.
// Hence, we are making it a pattern to only list it as part of new file and not as part of old file. // Hence, we are making it a pattern to only list it as part of new file and not as part of old file.
func cleanupCommitsForRename( func cleanupCommitsForRename(
@ -203,6 +260,7 @@ func cleanupCommitsForRename(
} }
func getRenameDetails( func getRenameDetails(
ctx context.Context,
giteaRepo *gitea.Repository, giteaRepo *gitea.Repository,
commits []types.Commit, commits []types.Commit,
path string, path string,
@ -213,7 +271,7 @@ func getRenameDetails(
renameDetailsList := make([]types.PathRenameDetails, 0, 2) renameDetailsList := make([]types.PathRenameDetails, 0, 2)
renameDetails, err := giteaGetRenameDetails(giteaRepo, commits[0].SHA, path) renameDetails, err := giteaGetRenameDetails(ctx, giteaRepo, commits[0].SHA, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -226,7 +284,7 @@ func getRenameDetails(
return renameDetailsList, nil return renameDetailsList, nil
} }
renameDetailsLast, err := giteaGetRenameDetails(giteaRepo, commits[len(commits)-1].SHA, path) renameDetailsLast, err := giteaGetRenameDetails(ctx, giteaRepo, commits[len(commits)-1].SHA, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -239,10 +297,33 @@ func getRenameDetails(
} }
func giteaGetRenameDetails( func giteaGetRenameDetails(
ctx context.Context,
giteaRepo *gitea.Repository, giteaRepo *gitea.Repository,
ref string, ref string,
path string, path string,
) (*types.PathRenameDetails, error) { ) (*types.PathRenameDetails, error) {
changeInfos, err := getChangeInfos(ctx, giteaRepo, ref)
if err != nil {
return &types.PathRenameDetails{}, fmt.Errorf("failed to get change infos %w", err)
}
for _, c := range changeInfos {
if c.ChangeType == enum.FileDiffStatusRenamed && (c.Path == path || c.NewPath == path) {
return &types.PathRenameDetails{
OldPath: c.Path,
NewPath: c.NewPath,
}, nil
}
}
return &types.PathRenameDetails{}, nil
}
func getChangeInfos(
ctx context.Context,
giteaRepo *gitea.Repository,
ref string,
) ([]changeInfo, error) {
cmd := command.New("log", cmd := command.New("log",
command.WithArg(ref), command.WithArg(ref),
command.WithFlag("--name-status"), command.WithFlag("--name-status"),
@ -253,38 +334,60 @@ func giteaGetRenameDetails(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to trigger log command: %w", err) return nil, fmt.Errorf("failed to trigger log command: %w", err)
} }
lines := parseLinesToSlice(output.Bytes()) lines := parseLinesToSlice(output.Bytes())
changeType, oldPath, newPath, err := getFileChangeTypeFromLog(lines, path) changeInfos, err := getFileChangeTypeFromLog(ctx, lines)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return changeInfos, nil
}
if strings.HasPrefix(*changeType, "R") { type changeInfo struct {
return &types.PathRenameDetails{ ChangeType enum.FileDiffStatus
OldPath: *oldPath, Path string
NewPath: *newPath, // populated only in case of renames
}, nil NewPath string
}
return &types.PathRenameDetails{}, nil
} }
func getFileChangeTypeFromLog( func getFileChangeTypeFromLog(
ctx context.Context,
changeStrings []string, changeStrings []string,
filePath string, ) ([]changeInfo, error) {
) (*string, *string, *string, error) { changeInfos := make([]changeInfo, len(changeStrings))
for _, changeString := range changeStrings { for i, changeString := range changeStrings {
if strings.Contains(changeString, filePath) { changeStringSplit := strings.Split(changeString, "\t")
changeInfo := strings.Split(changeString, "\t") if len(changeStringSplit) < 1 {
if len(changeInfo) != 3 { return changeInfos, fmt.Errorf("could not parse changeString %q", changeString)
return &changeInfo[0], nil, nil, nil
}
return &changeInfo[0], &changeInfo[1], &changeInfo[2], nil
} }
c := changeInfo{}
c.ChangeType = convertChangeType(ctx, changeStringSplit[0])
c.Path = changeStringSplit[1]
if len(changeStringSplit) == 3 {
c.NewPath = changeStringSplit[2]
}
changeInfos[i] = c
}
return changeInfos, nil
}
func convertChangeType(ctx context.Context, c string) enum.FileDiffStatus {
switch {
case strings.HasPrefix(c, "A"):
return enum.FileDiffStatusAdded
case strings.HasPrefix(c, "C"):
return enum.FileDiffStatusCopied
case strings.HasPrefix(c, "D"):
return enum.FileDiffStatusDeleted
case strings.HasPrefix(c, "M"):
return enum.FileDiffStatusModified
case strings.HasPrefix(c, "R"):
return enum.FileDiffStatusRenamed
default:
log.Ctx(ctx).Warn().Msgf("encountered unknown change type %s", c)
return enum.FileDiffStatusUndefined
} }
return nil, nil, nil, fmt.Errorf("could not parse change for the file '%s'", filePath)
} }
// GetCommit returns the (latest) commit for a specific revision. // GetCommit returns the (latest) commit for a specific revision.

View File

@ -30,11 +30,12 @@ type GetCommitParams struct {
} }
type Commit struct { type Commit struct {
SHA string `json:"sha"` SHA string `json:"sha"`
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Author Signature `json:"author"` Author Signature `json:"author"`
Committer Signature `json:"committer"` Committer Signature `json:"committer"`
FileStats CommitFileStats `json:"file_stats,omitempty"`
} }
type GetCommitOutput struct { type GetCommitOutput struct {
@ -105,6 +106,9 @@ type ListCommitsParams struct {
// Committer allows to filter for commits based on the committer - Optional, ignored if string is empty. // Committer allows to filter for commits based on the committer - Optional, ignored if string is empty.
Committer string Committer string
// IncludeFileStats allows you to include information about files changed, added and modified.
IncludeFileStats bool
} }
type RenameDetails struct { type RenameDetails struct {
@ -120,6 +124,12 @@ type ListCommitsOutput struct {
TotalCommits int TotalCommits int
} }
type CommitFileStats struct {
Added []string
Modified []string
Removed []string
}
func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*ListCommitsOutput, error) { func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*ListCommitsOutput, error) {
if params == nil { if params == nil {
return nil, ErrNoParamsProvided return nil, ErrNoParamsProvided
@ -133,6 +143,7 @@ func (s *Service) ListCommits(ctx context.Context, params *ListCommitsParams) (*
params.GitREF, params.GitREF,
int(params.Page), int(params.Page),
int(params.Limit), int(params.Limit),
params.IncludeFileStats,
types.CommitFilter{ types.CommitFilter{
AfterRef: params.After, AfterRef: params.After,
Path: params.Path, Path: params.Path,

View File

@ -24,6 +24,7 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/diff" "github.com/harness/gitness/git/diff"
"github.com/harness/gitness/git/enum"
"github.com/harness/gitness/git/types" "github.com/harness/gitness/git/types"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -296,44 +297,31 @@ func (s *Service) DiffCut(ctx context.Context, params *DiffCutParams) (DiffCutOu
} }
type FileDiff struct { type FileDiff struct {
SHA string `json:"sha"` SHA string `json:"sha"`
OldSHA string `json:"old_sha,omitempty"` OldSHA string `json:"old_sha,omitempty"`
Path string `json:"path"` Path string `json:"path"`
OldPath string `json:"old_path,omitempty"` OldPath string `json:"old_path,omitempty"`
Status FileDiffStatus `json:"status"` Status enum.FileDiffStatus `json:"status"`
Additions int64 `json:"additions"` Additions int64 `json:"additions"`
Deletions int64 `json:"deletions"` Deletions int64 `json:"deletions"`
Changes int64 `json:"changes"` Changes int64 `json:"changes"`
Patch []byte `json:"patch,omitempty"` Patch []byte `json:"patch,omitempty"`
IsBinary bool `json:"is_binary"` IsBinary bool `json:"is_binary"`
IsSubmodule bool `json:"is_submodule"` IsSubmodule bool `json:"is_submodule"`
} }
type FileDiffStatus string func parseFileDiffStatus(ftype diff.FileType) enum.FileDiffStatus {
const (
// NOTE: keeping values upper case for now to stay consistent with current API.
// TODO: change drone/go-scm (and potentially new dependencies) to case insensitive.
FileDiffStatusUndefined FileDiffStatus = "UNDEFINED"
FileDiffStatusAdded FileDiffStatus = "ADDED"
FileDiffStatusModified FileDiffStatus = "MODIFIED"
FileDiffStatusDeleted FileDiffStatus = "DELETED"
FileDiffStatusRenamed FileDiffStatus = "RENAMED"
)
func parseFileDiffStatus(ftype diff.FileType) FileDiffStatus {
switch ftype { switch ftype {
case diff.FileAdd: case diff.FileAdd:
return FileDiffStatusAdded return enum.FileDiffStatusAdded
case diff.FileDelete: case diff.FileDelete:
return FileDiffStatusDeleted return enum.FileDiffStatusDeleted
case diff.FileChange: case diff.FileChange:
return FileDiffStatusModified return enum.FileDiffStatusModified
case diff.FileRename: case diff.FileRename:
return FileDiffStatusRenamed return enum.FileDiffStatusRenamed
default: default:
return FileDiffStatusUndefined return enum.FileDiffStatusUndefined
} }
} }

29
git/enum/diff.go Normal file
View File

@ -0,0 +1,29 @@
// 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 enum
type FileDiffStatus string
const (
// NOTE: keeping values upper case for now to stay consistent with current API.
// TODO: change drone/go-scm (and potentially new dependencies) to case insensitive.
FileDiffStatusUndefined FileDiffStatus = "UNDEFINED"
FileDiffStatusAdded FileDiffStatus = "ADDED"
FileDiffStatusModified FileDiffStatus = "MODIFIED"
FileDiffStatusDeleted FileDiffStatus = "DELETED"
FileDiffStatusRenamed FileDiffStatus = "RENAMED"
FileDiffStatusCopied FileDiffStatus = "COPIED"
)

View File

@ -55,16 +55,24 @@ func mapCommit(c *types.Commit) (*Commit, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to map rpc committer: %w", err) return nil, fmt.Errorf("failed to map rpc committer: %w", err)
} }
return &Commit{ return &Commit{
SHA: c.SHA, SHA: c.SHA,
Title: c.Title, Title: c.Title,
Message: c.Message, Message: c.Message,
Author: *author, Author: *author,
Committer: *comitter, Committer: *comitter,
FileStats: *mapFileStats(&c.FileStats),
}, nil }, nil
} }
func mapFileStats(s *types.CommitFileStats) *CommitFileStats {
return &CommitFileStats{
Added: s.Added,
Modified: s.Modified,
Removed: s.Removed,
}
}
func mapSignature(s *types.Signature) (*Signature, error) { func mapSignature(s *types.Signature) (*Signature, error) {
if s == nil { if s == nil {
return nil, fmt.Errorf("rpc signature is nil") return nil, fmt.Errorf("rpc signature is nil")

View File

@ -141,11 +141,18 @@ type WalkReferencesOptions struct {
} }
type Commit struct { type Commit struct {
SHA string `json:"sha"` SHA string `json:"sha"`
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Author Signature `json:"author"` Author Signature `json:"author"`
Committer Signature `json:"committer"` Committer Signature `json:"committer"`
FileStats CommitFileStats `json:"file_stats,omitempty"`
}
type CommitFileStats struct {
Added []string
Modified []string
Removed []string
} }
type Branch struct { type Branch struct {