mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:12:02 +00:00
gitserver: grpc: port GetBehindAhead from client to gitcli backend (#62212)
Part of https://github.com/sourcegraph/sourcegraph/issues/62101 This PR ports the GetBehindAhead implementation from the gitserver client to the new gitserver.Backend interface. Here is the original implementation from the client for reference: ```go // GetBehindAhead returns the behind/ahead commit counts information for right vs. left (both Git // revspecs). func (c *clientImplementor) GetBehindAhead(ctx context.Context, repo api.RepoName, left, right string) (_ *gitdomain.BehindAhead, err error) { ctx, _, endObservation := c.operations.getBehindAhead.With(ctx, &err, observation.Args{ MetricLabelValues: []string{c.scope}, Attrs: []attribute.KeyValue{ repo.Attr(), attribute.String("left", left), attribute.String("right", right), }, }) defer endObservation(1, observation.Args{}) if err := checkSpecArgSafety(left); err != nil { return nil, err } if err := checkSpecArgSafety(right); err != nil { return nil, err } cmd := c.gitCommand(repo, "rev-list", "--count", "--left-right", fmt.Sprintf("%s...%s", left, right)) out, err := cmd.Output(ctx) if err != nil { return nil, err } behindAhead := strings.Split(strings.TrimSuffix(string(out), "\n"), "\t") b, err := strconv.ParseUint(behindAhead[0], 10, 0) if err != nil { return nil, err } a, err := strconv.ParseUint(behindAhead[1], 10, 0) if err != nil { return nil, err } return &gitdomain.BehindAhead{Behind: uint32(b), Ahead: uint32(a)}, nil } ``` ## Test plan New unit tests
This commit is contained in:
parent
3e05f5cc81
commit
e19c22d0a9
@ -3,6 +3,7 @@ package gitcli
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -188,6 +189,58 @@ func (g *gitCLIBackend) getBlobOID(ctx context.Context, commit api.CommitID, pat
|
||||
return api.CommitID(fields[2]), nil
|
||||
}
|
||||
|
||||
func (g *gitCLIBackend) BehindAhead(ctx context.Context, left, right string) (*gitdomain.BehindAhead, error) {
|
||||
if err := checkSpecArgSafety(left); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkSpecArgSafety(right); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if left == "" {
|
||||
left = "HEAD"
|
||||
}
|
||||
|
||||
if right == "" {
|
||||
right = "HEAD"
|
||||
}
|
||||
|
||||
rc, err := g.NewCommand(ctx, WithArguments("rev-list", "--count", "--left-right", fmt.Sprintf("%s...%s", left, right)))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "running git rev-list")
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
out, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
var e *CommandFailedError
|
||||
if errors.As(err, &e) {
|
||||
switch {
|
||||
case e.ExitStatus == 128 && bytes.Contains(e.Stderr, []byte("fatal: ambiguous argument")):
|
||||
fallthrough
|
||||
case e.ExitStatus == 128 && bytes.Contains(e.Stderr, []byte("fatal: Invalid symmetric difference expression")):
|
||||
return nil, &gitdomain.RevisionNotFoundError{
|
||||
Repo: g.repoName,
|
||||
Spec: fmt.Sprintf("%s...%s", left, right),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "reading git rev-list output")
|
||||
}
|
||||
|
||||
behindAhead := strings.Split(strings.TrimSuffix(string(out), "\n"), "\t")
|
||||
b, err := strconv.ParseUint(behindAhead[0], 10, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse behindahead output %q", out)
|
||||
}
|
||||
a, err := strconv.ParseUint(behindAhead[1], 10, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse behindahead output %q", out)
|
||||
}
|
||||
return &gitdomain.BehindAhead{Behind: uint32(b), Ahead: uint32(a)}, nil
|
||||
}
|
||||
|
||||
func (g *gitCLIBackend) FirstEverCommit(ctx context.Context) (api.CommitID, error) {
|
||||
rc, err := g.NewCommand(ctx, WithArguments("rev-list", "--reverse", "--date-order", "--max-parents=0", "HEAD"))
|
||||
if err != nil {
|
||||
|
||||
@ -325,3 +325,97 @@ func TestRepository_FirstEverCommit(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitCLIBackend_GetBehindAhead(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Prepare repo state:
|
||||
backend := BackendWithRepoCommands(t,
|
||||
// This is the commit graph we are creating
|
||||
//
|
||||
// +-----> 3 -----> 4 (branch1)
|
||||
// |
|
||||
// |
|
||||
// 0 ----> 1 ----> 2 -----> 5 -----> 6 (master)
|
||||
//
|
||||
"echo abcd > file0",
|
||||
"git add file0",
|
||||
"git commit -m commit0 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
|
||||
"echo abcd > file1",
|
||||
"git add file1",
|
||||
"git commit -m commit1 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
|
||||
"git branch branch1",
|
||||
|
||||
"echo efgh > file2",
|
||||
"git add file2",
|
||||
"git commit -m commit2 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
|
||||
"git checkout branch1",
|
||||
|
||||
"echo ijkl > file3",
|
||||
"git add file3",
|
||||
"git commit -m commit3 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
|
||||
"echo ijkl > file4",
|
||||
"git add file4",
|
||||
"git commit -m commit4 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
|
||||
"git checkout master",
|
||||
|
||||
"echo ijkl > file5",
|
||||
"git add file5",
|
||||
"git commit -m commit5 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
|
||||
"echo ijkl > file6",
|
||||
"git add file6",
|
||||
"git commit -m commit6 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
)
|
||||
|
||||
left := "branch1"
|
||||
right := "master"
|
||||
|
||||
t.Run("valid branches", func(t *testing.T) {
|
||||
behindAhead, err := backend.BehindAhead(ctx, left, right)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &gitdomain.BehindAhead{Behind: 2, Ahead: 3}, behindAhead)
|
||||
})
|
||||
|
||||
t.Run("missing left branch", func(t *testing.T) {
|
||||
_, err := backend.BehindAhead(ctx, left, "")
|
||||
require.NoError(t, err) // Should compare to HEAD
|
||||
})
|
||||
|
||||
t.Run("missing right branch", func(t *testing.T) {
|
||||
_, err := backend.BehindAhead(ctx, "", right)
|
||||
require.NoError(t, err) // Should compare to HEAD
|
||||
})
|
||||
|
||||
t.Run("invalid left branch", func(t *testing.T) {
|
||||
_, err := backend.BehindAhead(ctx, "invalid-branch", right)
|
||||
require.Error(t, err)
|
||||
var e *gitdomain.RevisionNotFoundError
|
||||
require.True(t, errors.As(err, &e))
|
||||
})
|
||||
|
||||
t.Run("invalid right branch", func(t *testing.T) {
|
||||
_, err := backend.BehindAhead(ctx, left, "invalid-branch")
|
||||
require.Error(t, err)
|
||||
var e *gitdomain.RevisionNotFoundError
|
||||
require.True(t, errors.As(err, &e))
|
||||
})
|
||||
|
||||
t.Run("same branch", func(t *testing.T) {
|
||||
behindAhead, err := backend.BehindAhead(ctx, left, left)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &gitdomain.BehindAhead{Behind: 0, Ahead: 0}, behindAhead)
|
||||
})
|
||||
|
||||
t.Run("invalid object id", func(t *testing.T) {
|
||||
_, err := backend.BehindAhead(ctx, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", right)
|
||||
require.Error(t, err)
|
||||
var e *gitdomain.RevisionNotFoundError
|
||||
require.True(t, errors.As(err, &e))
|
||||
})
|
||||
}
|
||||
|
||||
@ -101,6 +101,26 @@ type GitBackend interface {
|
||||
// If the repository is empty, a RevisionNotFoundError is returned (as the
|
||||
// "HEAD" ref does not exist).
|
||||
FirstEverCommit(ctx context.Context) (api.CommitID, error)
|
||||
|
||||
// BehindAhead returns the behind/ahead commit counts information for the symmetric difference left...right (both Git
|
||||
// revspecs).
|
||||
//
|
||||
// Behind is the number of commits that are solely reachable in "left" but not "right".
|
||||
// Ahead is the number of commits that are solely reachable in "right" but not "left".
|
||||
//
|
||||
// For the example, given the graph below, BehindAhead("A", "B") would return {Behind: 3, Ahead: 2}.
|
||||
//
|
||||
// y---b---b branch B
|
||||
// / \ /
|
||||
// / .
|
||||
// / / \
|
||||
// o---x---a---a---a branch A
|
||||
//
|
||||
// If either left or right are the empty string (""), the HEAD commit is implicitly used.
|
||||
//
|
||||
// If one of the two given revspecs does not exist, a RevisionNotFoundError
|
||||
// is returned.
|
||||
BehindAhead(ctx context.Context, left, right string) (*gitdomain.BehindAhead, error)
|
||||
}
|
||||
|
||||
type GitDiffComparisonType int
|
||||
|
||||
@ -287,6 +287,9 @@ type MockGitBackend struct {
|
||||
// ArchiveReaderFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method ArchiveReader.
|
||||
ArchiveReaderFunc *GitBackendArchiveReaderFunc
|
||||
// BehindAheadFunc is an instance of a mock function object controlling
|
||||
// the behavior of the method BehindAhead.
|
||||
BehindAheadFunc *GitBackendBehindAheadFunc
|
||||
// BlameFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Blame.
|
||||
BlameFunc *GitBackendBlameFunc
|
||||
@ -343,6 +346,11 @@ func NewMockGitBackend() *MockGitBackend {
|
||||
return
|
||||
},
|
||||
},
|
||||
BehindAheadFunc: &GitBackendBehindAheadFunc{
|
||||
defaultHook: func(context.Context, string, string) (r0 *gitdomain.BehindAhead, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
BlameFunc: &GitBackendBlameFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string, BlameOptions) (r0 BlameHunkReader, r1 error) {
|
||||
return
|
||||
@ -430,6 +438,11 @@ func NewStrictMockGitBackend() *MockGitBackend {
|
||||
panic("unexpected invocation of MockGitBackend.ArchiveReader")
|
||||
},
|
||||
},
|
||||
BehindAheadFunc: &GitBackendBehindAheadFunc{
|
||||
defaultHook: func(context.Context, string, string) (*gitdomain.BehindAhead, error) {
|
||||
panic("unexpected invocation of MockGitBackend.BehindAhead")
|
||||
},
|
||||
},
|
||||
BlameFunc: &GitBackendBlameFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string, BlameOptions) (BlameHunkReader, error) {
|
||||
panic("unexpected invocation of MockGitBackend.Blame")
|
||||
@ -515,6 +528,9 @@ func NewMockGitBackendFrom(i GitBackend) *MockGitBackend {
|
||||
ArchiveReaderFunc: &GitBackendArchiveReaderFunc{
|
||||
defaultHook: i.ArchiveReader,
|
||||
},
|
||||
BehindAheadFunc: &GitBackendBehindAheadFunc{
|
||||
defaultHook: i.BehindAhead,
|
||||
},
|
||||
BlameFunc: &GitBackendBlameFunc{
|
||||
defaultHook: i.Blame,
|
||||
},
|
||||
@ -677,6 +693,117 @@ func (c GitBackendArchiveReaderFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitBackendBehindAheadFunc describes the behavior when the BehindAhead
|
||||
// method of the parent MockGitBackend instance is invoked.
|
||||
type GitBackendBehindAheadFunc struct {
|
||||
defaultHook func(context.Context, string, string) (*gitdomain.BehindAhead, error)
|
||||
hooks []func(context.Context, string, string) (*gitdomain.BehindAhead, error)
|
||||
history []GitBackendBehindAheadFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// BehindAhead delegates to the next hook function in the queue and stores
|
||||
// the parameter and result values of this invocation.
|
||||
func (m *MockGitBackend) BehindAhead(v0 context.Context, v1 string, v2 string) (*gitdomain.BehindAhead, error) {
|
||||
r0, r1 := m.BehindAheadFunc.nextHook()(v0, v1, v2)
|
||||
m.BehindAheadFunc.appendCall(GitBackendBehindAheadFuncCall{v0, v1, v2, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the BehindAhead method
|
||||
// of the parent MockGitBackend instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *GitBackendBehindAheadFunc) SetDefaultHook(hook func(context.Context, string, string) (*gitdomain.BehindAhead, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// BehindAhead method of the parent MockGitBackend instance invokes the hook
|
||||
// at the front of the queue and discards it. After the queue is empty, the
|
||||
// default hook function is invoked for any future action.
|
||||
func (f *GitBackendBehindAheadFunc) PushHook(hook func(context.Context, string, string) (*gitdomain.BehindAhead, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *GitBackendBehindAheadFunc) SetDefaultReturn(r0 *gitdomain.BehindAhead, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, string, string) (*gitdomain.BehindAhead, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *GitBackendBehindAheadFunc) PushReturn(r0 *gitdomain.BehindAhead, r1 error) {
|
||||
f.PushHook(func(context.Context, string, string) (*gitdomain.BehindAhead, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *GitBackendBehindAheadFunc) nextHook() func(context.Context, string, string) (*gitdomain.BehindAhead, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *GitBackendBehindAheadFunc) appendCall(r0 GitBackendBehindAheadFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of GitBackendBehindAheadFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *GitBackendBehindAheadFunc) History() []GitBackendBehindAheadFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]GitBackendBehindAheadFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// GitBackendBehindAheadFuncCall is an object that describes an invocation
|
||||
// of method BehindAhead on an instance of MockGitBackend.
|
||||
type GitBackendBehindAheadFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 string
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 string
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 *gitdomain.BehindAhead
|
||||
// Result1 is the value of the 2nd result returned from this method
|
||||
// invocation.
|
||||
Result1 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c GitBackendBehindAheadFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c GitBackendBehindAheadFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitBackendBlameFunc describes the behavior when the Blame method of the
|
||||
// parent MockGitBackend instance is invoked.
|
||||
type GitBackendBlameFunc struct {
|
||||
|
||||
@ -39,6 +39,16 @@ type observableBackend struct {
|
||||
backend GitBackend
|
||||
}
|
||||
|
||||
func (b *observableBackend) BehindAhead(ctx context.Context, left, right string) (*gitdomain.BehindAhead, error) {
|
||||
ctx, _, endObservation := b.operations.getBehindAhead.With(ctx, nil, observation.Args{})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
concurrentOps.WithLabelValues("BehindAhead").Inc()
|
||||
defer concurrentOps.WithLabelValues("BehindAhead").Dec()
|
||||
|
||||
return b.backend.BehindAhead(ctx, left, right)
|
||||
}
|
||||
|
||||
func (b *observableBackend) Config() GitConfigBackend {
|
||||
return &observableGitConfigBackend{
|
||||
backend: b.backend.Config(),
|
||||
@ -398,6 +408,7 @@ type operations struct {
|
||||
rawDiff *observation.Operation
|
||||
contributorCounts *observation.Operation
|
||||
firstEverCommit *observation.Operation
|
||||
getBehindAhead *observation.Operation
|
||||
}
|
||||
|
||||
func newOperations(observationCtx *observation.Context) *operations {
|
||||
@ -444,6 +455,7 @@ func newOperations(observationCtx *observation.Context) *operations {
|
||||
rawDiff: op("raw-diff"),
|
||||
contributorCounts: op("contributor-counts"),
|
||||
firstEverCommit: op("first-ever-commit"),
|
||||
getBehindAhead: op("get-behind-ahead"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user