drone/gitrpc/diff.go

349 lines
8.6 KiB
Go

// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package gitrpc
import (
"context"
"errors"
"fmt"
"io"
"path"
"github.com/harness/gitness/gitrpc/internal/streamio"
"github.com/harness/gitness/gitrpc/internal/types"
"github.com/harness/gitness/gitrpc/rpc"
"golang.org/x/sync/errgroup"
)
type DiffParams struct {
ReadParams
BaseRef string
HeadRef string
MergeBase bool
IncludePatch bool
}
func (p DiffParams) Validate() error {
if err := p.ReadParams.Validate(); err != nil {
return err
}
if p.HeadRef == "" {
return ErrInvalidArgumentf("head ref cannot be empty")
}
return nil
}
func (c *Client) RawDiff(ctx context.Context, params *DiffParams, out io.Writer) error {
if err := params.Validate(); err != nil {
return err
}
diff, err := c.diffService.RawDiff(ctx, &rpc.DiffRequest{
Base: mapToRPCReadRequest(params.ReadParams),
BaseRef: params.BaseRef,
HeadRef: params.HeadRef,
MergeBase: params.MergeBase,
})
if err != nil {
return err
}
reader := streamio.NewReader(func() ([]byte, error) {
var resp *rpc.RawDiffResponse
resp, err = diff.Recv()
return resp.GetData(), err
})
if _, err = io.Copy(out, reader); err != nil {
return fmt.Errorf("copy rpc data: %w", err)
}
return nil
}
type DiffShortStatOutput struct {
Files int
Additions int
Deletions int
}
// DiffShortStat returns files changed, additions and deletions metadata.
func (c *Client) DiffShortStat(ctx context.Context, params *DiffParams) (DiffShortStatOutput, error) {
if err := params.Validate(); err != nil {
return DiffShortStatOutput{}, err
}
stat, err := c.diffService.DiffShortStat(ctx, &rpc.DiffRequest{
Base: mapToRPCReadRequest(params.ReadParams),
BaseRef: params.BaseRef,
HeadRef: params.HeadRef,
MergeBase: params.MergeBase,
})
if err != nil {
return DiffShortStatOutput{}, err
}
return DiffShortStatOutput{
Files: int(stat.GetFiles()),
Additions: int(stat.GetAdditions()),
Deletions: int(stat.GetDeletions()),
}, nil
}
type DiffStatsOutput struct {
Commits int
FilesChanged int
}
func (c *Client) DiffStats(ctx context.Context, params *DiffParams) (DiffStatsOutput, error) {
// declare variables which will be used in go routines,
// no need for atomic operations because writing and reading variable
// doesn't happen at the same time
var (
totalCommits int
totalFiles int
)
errGroup, groupCtx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
// read total commits
options := &GetCommitDivergencesParams{
ReadParams: params.ReadParams,
Requests: []CommitDivergenceRequest{
{
From: params.HeadRef,
To: params.BaseRef,
},
},
}
rpcOutput, err := c.GetCommitDivergences(groupCtx, options)
if err != nil {
return fmt.Errorf("failed to count pull request commits: %w", err)
}
if len(rpcOutput.Divergences) > 0 {
totalCommits = int(rpcOutput.Divergences[0].Ahead)
}
return nil
})
errGroup.Go(func() error {
// read short stat
stat, err := c.DiffShortStat(groupCtx, &DiffParams{
ReadParams: params.ReadParams,
BaseRef: params.BaseRef,
HeadRef: params.HeadRef,
MergeBase: true, // must be true, because commitDivergences use tripple dot notation
})
if err != nil {
return fmt.Errorf("failed to count pull request file changes: %w", err)
}
totalFiles = stat.Files
return nil
})
err := errGroup.Wait()
if err != nil {
return DiffStatsOutput{}, err
}
return DiffStatsOutput{
Commits: totalCommits,
FilesChanged: totalFiles,
}, nil
}
type GetDiffHunkHeadersParams struct {
ReadParams
SourceCommitSHA string
TargetCommitSHA string
}
type DiffFileHeader struct {
OldName string
NewName string
Extensions map[string]string
}
type HunkHeader struct {
OldLine int
OldSpan int
NewLine int
NewSpan int
Text string
}
type DiffFileHunkHeaders struct {
FileHeader DiffFileHeader
HunkHeaders []HunkHeader
}
type GetDiffHunkHeadersOutput struct {
Files []DiffFileHunkHeaders
}
func (c *Client) GetDiffHunkHeaders(
ctx context.Context,
params GetDiffHunkHeadersParams,
) (GetDiffHunkHeadersOutput, error) {
if params.SourceCommitSHA == params.TargetCommitSHA {
return GetDiffHunkHeadersOutput{}, nil
}
hunkHeaders, err := c.diffService.GetDiffHunkHeaders(ctx, &rpc.GetDiffHunkHeadersRequest{
Base: mapToRPCReadRequest(params.ReadParams),
SourceCommitSha: params.SourceCommitSHA,
TargetCommitSha: params.TargetCommitSHA,
})
if err != nil {
return GetDiffHunkHeadersOutput{}, processRPCErrorf(err, "failed to get git diff hunk headers")
}
files := make([]DiffFileHunkHeaders, len(hunkHeaders.Files))
for i, file := range hunkHeaders.Files {
headers := make([]HunkHeader, len(file.HunkHeaders))
for j, header := range file.HunkHeaders {
headers[j] = mapHunkHeader(header)
}
files[i] = DiffFileHunkHeaders{
FileHeader: mapDiffFileHeader(file.FileHeader),
HunkHeaders: headers,
}
}
return GetDiffHunkHeadersOutput{
Files: files,
}, nil
}
type DiffCutOutput struct {
Header HunkHeader
LinesHeader string
Lines []string
MergeBaseSHA string
LatestSourceSHA string
}
type DiffCutParams struct {
ReadParams
SourceCommitSHA string
SourceBranch string
TargetCommitSHA string
TargetBranch string
Path string
LineStart int
LineStartNew bool
LineEnd int
LineEndNew bool
}
// DiffCut extracts diff snippet from a git diff hunk.
// The snippet is from the specific commit (specified by commit SHA), between refs
// source branch and target branch, from the specific file.
func (c *Client) DiffCut(ctx context.Context, params *DiffCutParams) (DiffCutOutput, error) {
result, err := c.diffService.DiffCut(ctx, &rpc.DiffCutRequest{
Base: mapToRPCReadRequest(params.ReadParams),
SourceCommitSha: params.SourceCommitSHA,
SourceBranch: params.SourceBranch,
TargetCommitSha: params.TargetCommitSHA,
TargetBranch: params.TargetBranch,
Path: params.Path,
LineStart: int32(params.LineStart),
LineStartNew: params.LineStartNew,
LineEnd: int32(params.LineEnd),
LineEndNew: params.LineEndNew,
})
if err != nil {
return DiffCutOutput{}, processRPCErrorf(err, "failed to get git diff sub hunk")
}
hunkHeader := types.HunkHeader{
OldLine: int(result.HunkHeader.OldLine),
OldSpan: int(result.HunkHeader.OldSpan),
NewLine: int(result.HunkHeader.NewLine),
NewSpan: int(result.HunkHeader.NewSpan),
Text: result.HunkHeader.Text,
}
return DiffCutOutput{
Header: HunkHeader(hunkHeader),
LinesHeader: result.LinesHeader,
Lines: result.Lines,
MergeBaseSHA: result.MergeBaseSha,
LatestSourceSHA: result.LatestSourceSha,
}, nil
}
type FileDiff struct {
SHA string `json:"sha"`
OldSHA string `json:"old_sha,omitempty"`
Path string `json:"path"`
OldPath string `json:"old_path,omitempty"`
Status string `json:"status"`
Additions int64 `json:"additions"`
Deletions int64 `json:"deletions"`
Changes int64 `json:"changes"`
ContentURL string `json:"content_url"`
Patch []byte `json:"patch,omitempty"`
IsBinary bool `json:"is_binary"`
IsSubmodule bool `json:"is_submodule"`
}
func (c *Client) Diff(ctx context.Context, params *DiffParams, baseURL string) (<-chan *FileDiff, <-chan error) {
ch := make(chan *FileDiff)
// needs to be buffered so it is not blocking on receiver side when all data is sent
cherr := make(chan error, 1)
go func() {
defer close(ch)
defer close(cherr)
if err := params.Validate(); err != nil {
cherr <- err
return
}
stream, err := c.diffService.Diff(ctx, &rpc.DiffRequest{
Base: mapToRPCReadRequest(params.ReadParams),
BaseRef: params.BaseRef,
HeadRef: params.HeadRef,
MergeBase: params.MergeBase,
IncludePatch: params.IncludePatch,
})
if err != nil {
return
}
for {
resp, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
cherr <- processRPCErrorf(err, "failed to get git diff file from stream")
return
}
ch <- &FileDiff{
SHA: resp.Sha,
OldSHA: resp.OldSha,
Path: resp.Path,
OldPath: resp.OldPath,
Status: resp.Status.String(),
Additions: int64(resp.Additions),
Deletions: int64(resp.Deletions),
Changes: int64(resp.Changes),
ContentURL: path.Join(baseURL, resp.Path),
Patch: resp.Patch,
IsBinary: resp.IsBinary,
IsSubmodule: resp.IsSubmodule,
}
}
}()
return ch, cherr
}