gitserver: Utilize Commits method for RevList (#62369)

This PR replaces the gitserver client method RevList by the Commits method.
The Commits method can do the same, but it saves us from implementing and testing a second API. Also, the name RevList sounded very generic but it was super opinionated in the flags it uses, so it was really "ListCommitsForRockskip" but that is too application-specific so Commits is probably just as good.
We'll learn a bunch about our API endpoints as we finish migrating them all, so we can do proper optimizations afterwards.

Closes https://github.com/sourcegraph/sourcegraph/issues/62098

Test plan:

Integration and unit test for rockskip are still passing.
This commit is contained in:
Erik Seliger 2024-05-06 16:54:59 +02:00 committed by GitHub
parent 186f4a212c
commit 75afb74586
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 131 additions and 378 deletions

View File

@ -28,5 +28,12 @@ go_test(
timeout = "short",
srcs = ["client_test.go"],
embed = [":gitserver"],
deps = ["@com_github_google_go_cmp//cmp"],
deps = [
"//internal/api",
"//internal/gitserver",
"//internal/gitserver/gitdomain",
"//internal/observation",
"@com_github_google_go_cmp//cmp",
"@com_github_stretchr_testify//require",
],
)

View File

@ -108,7 +108,7 @@ func (c *gitserverClient) RevList(ctx context.Context, repo string, commit strin
for {
var commits []api.CommitID
var err error
commits, nextCursor, err = c.innerClient.RevList(ctx, api.RepoName(repo), nextCursor, revListPageSize)
commits, nextCursor, err = c.paginatedRevList(ctx, api.RepoName(repo), nextCursor, revListPageSize)
if err != nil {
return err
}
@ -127,6 +127,31 @@ func (c *gitserverClient) RevList(ctx context.Context, repo string, commit strin
}
}
func (c *gitserverClient) paginatedRevList(ctx context.Context, repo api.RepoName, commit string, count int) ([]api.CommitID, string, error) {
commits, err := c.innerClient.Commits(ctx, repo, gitserver.CommitsOptions{
N: uint(count + 1),
Range: commit,
FirstParent: true,
})
if err != nil {
return nil, "", err
}
commitIDs := make([]api.CommitID, 0, count+1)
for _, commit := range commits {
commitIDs = append(commitIDs, commit.ID)
}
var nextCursor string
if len(commitIDs) > count {
nextCursor = string(commitIDs[len(commitIDs)-1])
commitIDs = commitIDs[:count]
}
return commitIDs, nextCursor, nil
}
var NUL = []byte{0}
// parseGitDiffOutput parses the output of a git diff command, which consists

View File

@ -1,9 +1,16 @@
package gitserver
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
"github.com/sourcegraph/sourcegraph/internal/observation"
)
func TestParseGitDiffOutput(t *testing.T) {
@ -66,3 +73,56 @@ func combineBytes(bss ...[]byte) (combined []byte) {
return combined
}
func TestGitserverClient_PaginatedRevList(t *testing.T) {
allCommits := []*gitdomain.Commit{
{ID: "4ac04f2761285633cd35188c696a6e08de03c00c"},
{ID: "e7d0b23cb4e2e975ad657b163793bc83926c21b2"},
{ID: "a04652fa1998a0a7d2f2f77ecb7021de943d3aab"},
}
allCommitIDs := []api.CommitID{}
for _, c := range allCommits {
allCommitIDs = append(allCommitIDs, c.ID)
}
t.Run("returns commits in reverse chronological order", func(t *testing.T) {
inner := gitserver.NewMockClient()
inner.CommitsFunc.SetDefaultReturn(allCommits, nil)
client := &gitserverClient{
innerClient: inner,
operations: newOperations(&observation.TestContext),
}
commits, _, err := client.paginatedRevList(context.Background(), "repo", "HEAD", 999)
require.NoError(t, err)
require.Equal(t, allCommitIDs, commits)
})
t.Run("returns next cursor when more commits exist", func(t *testing.T) {
inner := gitserver.NewMockClient()
for i := range allCommits {
if len(allCommits) > i+1 {
inner.CommitsFunc.PushReturn(allCommits[i:i+2], nil)
} else {
inner.CommitsFunc.PushReturn(allCommits[i:i+1], nil)
}
}
client := &gitserverClient{
innerClient: inner,
operations: newOperations(&observation.TestContext),
}
nextCursor := "HEAD"
haveCommits := []api.CommitID{}
for {
commits, next, err := client.paginatedRevList(context.Background(), "repo", nextCursor, 1)
require.NoError(t, err)
nextCursor = next
haveCommits = append(haveCommits, commits...)
if nextCursor == "" {
break
}
}
require.Equal(t, allCommitIDs, haveCommits)
})
}

View File

@ -11085,9 +11085,6 @@ type MockGitserverClient struct {
// RevAtTimeFunc is an instance of a mock function object controlling
// the behavior of the method RevAtTime.
RevAtTimeFunc *GitserverClientRevAtTimeFunc
// RevListFunc is an instance of a mock function object controlling the
// behavior of the method RevList.
RevListFunc *GitserverClientRevListFunc
// ScopedFunc is an instance of a mock function object controlling the
// behavior of the method Scoped.
ScopedFunc *GitserverClientScopedFunc
@ -11287,11 +11284,6 @@ func NewMockGitserverClient() *MockGitserverClient {
return
},
},
RevListFunc: &GitserverClientRevListFunc{
defaultHook: func(context.Context, api.RepoName, string, int) (r0 []api.CommitID, r1 string, r2 error) {
return
},
},
ScopedFunc: &GitserverClientScopedFunc{
defaultHook: func(string) (r0 gitserver.Client) {
return
@ -11504,11 +11496,6 @@ func NewStrictMockGitserverClient() *MockGitserverClient {
panic("unexpected invocation of MockGitserverClient.RevAtTime")
},
},
RevListFunc: &GitserverClientRevListFunc{
defaultHook: func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error) {
panic("unexpected invocation of MockGitserverClient.RevList")
},
},
ScopedFunc: &GitserverClientScopedFunc{
defaultHook: func(string) gitserver.Client {
panic("unexpected invocation of MockGitserverClient.Scoped")
@ -11652,9 +11639,6 @@ func NewMockGitserverClientFrom(i gitserver.Client) *MockGitserverClient {
RevAtTimeFunc: &GitserverClientRevAtTimeFunc{
defaultHook: i.RevAtTime,
},
RevListFunc: &GitserverClientRevListFunc{
defaultHook: i.RevList,
},
ScopedFunc: &GitserverClientScopedFunc{
defaultHook: i.Scoped,
},
@ -15637,123 +15621,6 @@ func (c GitserverClientRevAtTimeFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1, c.Result2}
}
// GitserverClientRevListFunc describes the behavior when the RevList method
// of the parent MockGitserverClient instance is invoked.
type GitserverClientRevListFunc struct {
defaultHook func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error)
hooks []func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error)
history []GitserverClientRevListFuncCall
mutex sync.Mutex
}
// RevList delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockGitserverClient) RevList(v0 context.Context, v1 api.RepoName, v2 string, v3 int) ([]api.CommitID, string, error) {
r0, r1, r2 := m.RevListFunc.nextHook()(v0, v1, v2, v3)
m.RevListFunc.appendCall(GitserverClientRevListFuncCall{v0, v1, v2, v3, r0, r1, r2})
return r0, r1, r2
}
// SetDefaultHook sets function that is called when the RevList method of
// the parent MockGitserverClient instance is invoked and the hook queue is
// empty.
func (f *GitserverClientRevListFunc) SetDefaultHook(hook func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// RevList method of the parent MockGitserverClient 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 *GitserverClientRevListFunc) PushHook(hook func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, 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 *GitserverClientRevListFunc) SetDefaultReturn(r0 []api.CommitID, r1 string, r2 error) {
f.SetDefaultHook(func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error) {
return r0, r1, r2
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *GitserverClientRevListFunc) PushReturn(r0 []api.CommitID, r1 string, r2 error) {
f.PushHook(func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error) {
return r0, r1, r2
})
}
func (f *GitserverClientRevListFunc) nextHook() func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, 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 *GitserverClientRevListFunc) appendCall(r0 GitserverClientRevListFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of GitserverClientRevListFuncCall objects
// describing the invocations of this function.
func (f *GitserverClientRevListFunc) History() []GitserverClientRevListFuncCall {
f.mutex.Lock()
history := make([]GitserverClientRevListFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// GitserverClientRevListFuncCall is an object that describes an invocation
// of method RevList on an instance of MockGitserverClient.
type GitserverClientRevListFuncCall 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 api.RepoName
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 string
// Arg3 is the value of the 4th argument passed to this method
// invocation.
Arg3 int
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 []api.CommitID
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 string
// Result2 is the value of the 3rd result returned from this method
// invocation.
Result2 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c GitserverClientRevListFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c GitserverClientRevListFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1, c.Result2}
}
// GitserverClientScopedFunc describes the behavior when the Scoped method
// of the parent MockGitserverClient instance is invoked.
type GitserverClientScopedFunc struct {

View File

@ -23,7 +23,6 @@ go_library(
"//internal/actor",
"//internal/api",
"//internal/authz",
"//internal/byteutils",
"//internal/conf",
"//internal/conf/conftypes",
"//internal/extsvc/gitolite",

View File

@ -488,11 +488,6 @@ type Client interface {
// LogReverseEach runs git log in reverse order and calls the given callback for each entry.
LogReverseEach(ctx context.Context, repo string, commit string, n int, onLogEntry func(entry gitdomain.LogEntry) error) error
// RevList makes a git rev-list call and returns up to count commits. if nextCursor
// is non-empty, it is used as the starting point for the next call, use it to iterate
// to iterate over the whole history.
RevList(ctx context.Context, repo api.RepoName, commit string, count int) (_ []api.CommitID, nextCursor string, err error)
// SystemsInfo returns information about all gitserver instances associated with a Sourcegraph instance.
SystemsInfo(ctx context.Context) ([]protocol.SystemInfo, error)

View File

@ -28,7 +28,6 @@ import (
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/byteutils"
"github.com/sourcegraph/sourcegraph/internal/fileutil"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
@ -1063,53 +1062,6 @@ func (c *clientImplementor) MergeBase(ctx context.Context, repo api.RepoName, ba
return api.CommitID(res.GetMergeBaseCommitSha()), nil
}
func (c *clientImplementor) RevList(ctx context.Context, repo api.RepoName, commit string, count int) (_ []api.CommitID, nextCursor string, err error) {
ctx, _, endObservation := c.operations.revList.With(ctx, &err, observation.Args{
MetricLabelValues: []string{c.scope},
Attrs: []attribute.KeyValue{
repo.Attr(),
attribute.String("commit", commit),
},
})
defer endObservation(1, observation.Args{})
command := c.gitCommand(repo, revListArgs(count, commit)...)
stdout, err := command.Output(ctx)
if err != nil {
return nil, "", err
}
commits := make([]api.CommitID, 0, count+1)
lr := byteutils.NewLineReader(stdout)
for lr.Scan() {
line := lr.Line()
line = bytes.TrimSpace(line)
if len(line) == 0 {
continue
}
commit := api.CommitID(line)
commits = append(commits, commit)
}
if len(commits) > count {
nextCursor = string(commits[len(commits)-1])
commits = commits[:count]
}
return commits, nextCursor, nil
}
func revListArgs(count int, givenCommit string) []string {
return []string{
"rev-list",
"--first-parent",
fmt.Sprintf("--max-count=%d", count+1),
givenCommit,
}
}
// BehindAhead returns the behind/ahead commit counts information for right vs. left (both Git
// revspecs).
func (c *clientImplementor) BehindAhead(ctx context.Context, repo api.RepoName, left, right string) (_ *gitdomain.BehindAhead, err error) {
@ -1307,6 +1259,14 @@ type CommitsOptions struct {
Follow bool // follow the history of the path beyond renames (works only for a single path)
// When finding commits to include, follow only the first parent commit upon
// seeing a merge commit. This option can give a better overview when viewing
// the evolution of a particular topic branch, because merges into a topic
// branch tend to be only about adjusting to updated upstream from time to time,
// and this option allows you to ignore the individual commits brought in to
// your history by such a merge.
FirstParent bool
// When true return the names of the files changed in the commit
NameOnly bool
}
@ -1747,6 +1707,10 @@ func commitLogArgs(initialArgs []string, opt CommitsOptions) (args []string, err
args = append(args, "--fixed-strings", "--regexp-ignore-case", "--grep="+opt.MessageQuery)
}
if opt.FirstParent {
args = append(args, "--first-parent")
}
if opt.Range != "" {
args = append(args, opt.Range)
}

View File

@ -2324,57 +2324,3 @@ func TestClient_GetBehindAhead(t *testing.T) {
})
})
}
func TestRevList(t *testing.T) {
ClientMocks.LocalGitserver = true
defer ResetClientMocks()
gitCommands := []string{
"git commit --allow-empty -m commit1",
"git commit --allow-empty -m commit2",
"git commit --allow-empty -m commit3",
}
repo := MakeGitRepository(t, gitCommands...)
allCommits := []api.CommitID{
"4ac04f2761285633cd35188c696a6e08de03c00c",
"e7d0b23cb4e2e975ad657b163793bc83926c21b2",
"a04652fa1998a0a7d2f2f77ecb7021de943d3aab",
}
t.Run("returns commits in reverse chronological order", func(t *testing.T) {
client := NewTestClient(t)
commits, _, err := client.RevList(context.Background(), repo, "HEAD", 999)
require.NoError(t, err)
require.Equal(t, allCommits, commits)
})
t.Run("returns next cursor when more commits exist", func(t *testing.T) {
gitCommands := []string{
"git commit --allow-empty -m commit1",
"git commit --allow-empty -m commit2",
"git commit --allow-empty -m commit3",
}
repo := MakeGitRepository(t, gitCommands...)
client := NewTestClient(t)
nextCursor := "HEAD"
haveCommits := []api.CommitID{}
for {
commits, next, err := client.RevList(context.Background(), repo, nextCursor, 1)
require.NoError(t, err)
nextCursor = next
haveCommits = append(haveCommits, commits...)
if nextCursor == "" {
break
}
}
require.Equal(t, allCommits, haveCommits)
})
t.Run("returns error when commit does not exist", func(t *testing.T) {
repo := MakeGitRepository(t)
client := NewTestClient(t)
_, _, err := client.RevList(context.Background(), repo, "nonexistent", 10)
require.Error(t, err)
require.Contains(t, err.Error(), "exit status 128")
})
}

View File

@ -129,9 +129,6 @@ type MockClient struct {
// RevAtTimeFunc is an instance of a mock function object controlling
// the behavior of the method RevAtTime.
RevAtTimeFunc *ClientRevAtTimeFunc
// RevListFunc is an instance of a mock function object controlling the
// behavior of the method RevList.
RevListFunc *ClientRevListFunc
// ScopedFunc is an instance of a mock function object controlling the
// behavior of the method Scoped.
ScopedFunc *ClientScopedFunc
@ -331,11 +328,6 @@ func NewMockClient() *MockClient {
return
},
},
RevListFunc: &ClientRevListFunc{
defaultHook: func(context.Context, api.RepoName, string, int) (r0 []api.CommitID, r1 string, r2 error) {
return
},
},
ScopedFunc: &ClientScopedFunc{
defaultHook: func(string) (r0 Client) {
return
@ -548,11 +540,6 @@ func NewStrictMockClient() *MockClient {
panic("unexpected invocation of MockClient.RevAtTime")
},
},
RevListFunc: &ClientRevListFunc{
defaultHook: func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error) {
panic("unexpected invocation of MockClient.RevList")
},
},
ScopedFunc: &ClientScopedFunc{
defaultHook: func(string) Client {
panic("unexpected invocation of MockClient.Scoped")
@ -695,9 +682,6 @@ func NewMockClientFrom(i Client) *MockClient {
RevAtTimeFunc: &ClientRevAtTimeFunc{
defaultHook: i.RevAtTime,
},
RevListFunc: &ClientRevListFunc{
defaultHook: i.RevList,
},
ScopedFunc: &ClientScopedFunc{
defaultHook: i.Scoped,
},
@ -4613,122 +4597,6 @@ func (c ClientRevAtTimeFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1, c.Result2}
}
// ClientRevListFunc describes the behavior when the RevList method of the
// parent MockClient instance is invoked.
type ClientRevListFunc struct {
defaultHook func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error)
hooks []func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error)
history []ClientRevListFuncCall
mutex sync.Mutex
}
// RevList delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockClient) RevList(v0 context.Context, v1 api.RepoName, v2 string, v3 int) ([]api.CommitID, string, error) {
r0, r1, r2 := m.RevListFunc.nextHook()(v0, v1, v2, v3)
m.RevListFunc.appendCall(ClientRevListFuncCall{v0, v1, v2, v3, r0, r1, r2})
return r0, r1, r2
}
// SetDefaultHook sets function that is called when the RevList method of
// the parent MockClient instance is invoked and the hook queue is empty.
func (f *ClientRevListFunc) SetDefaultHook(hook func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// RevList method of the parent MockClient 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 *ClientRevListFunc) PushHook(hook func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, 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 *ClientRevListFunc) SetDefaultReturn(r0 []api.CommitID, r1 string, r2 error) {
f.SetDefaultHook(func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error) {
return r0, r1, r2
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *ClientRevListFunc) PushReturn(r0 []api.CommitID, r1 string, r2 error) {
f.PushHook(func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, error) {
return r0, r1, r2
})
}
func (f *ClientRevListFunc) nextHook() func(context.Context, api.RepoName, string, int) ([]api.CommitID, string, 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 *ClientRevListFunc) appendCall(r0 ClientRevListFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of ClientRevListFuncCall objects describing
// the invocations of this function.
func (f *ClientRevListFunc) History() []ClientRevListFuncCall {
f.mutex.Lock()
history := make([]ClientRevListFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// ClientRevListFuncCall is an object that describes an invocation of method
// RevList on an instance of MockClient.
type ClientRevListFuncCall 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 api.RepoName
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 string
// Arg3 is the value of the 4th argument passed to this method
// invocation.
Arg3 int
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 []api.CommitID
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 string
// Result2 is the value of the 3rd result returned from this method
// invocation.
Result2 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c ClientRevListFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c ClientRevListFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1, c.Result2}
}
// ClientScopedFunc describes the behavior when the Scoped method of the
// parent MockClient instance is invoked.
type ClientScopedFunc struct {

View File

@ -28,7 +28,6 @@ type operations struct {
readDir *observation.Operation
resolveRevision *observation.Operation
revAtTime *observation.Operation
revList *observation.Operation
search *observation.Operation
stat *observation.Operation
streamBlameFile *observation.Operation
@ -117,7 +116,6 @@ func newOperations(observationCtx *observation.Context) *operations {
readDir: op("ReadDir"),
resolveRevision: resolveRevisionOperation,
revAtTime: op("RevAtTime"),
revList: op("RevList"),
search: op("Search"),
stat: op("Stat"),
streamBlameFile: op("StreamBlameFile"),

View File

@ -246,7 +246,7 @@ func (g *SubprocessGit) RevList(ctx context.Context, repo string, commit string,
for {
var commits []api.CommitID
var err error
commits, nextCursor, err = g.gs.RevList(ctx, api.RepoName(repo), nextCursor, 100)
commits, nextCursor, err = g.paginatedRevList(ctx, api.RepoName(repo), nextCursor, 100)
if err != nil {
return err
}
@ -265,6 +265,30 @@ func (g *SubprocessGit) RevList(ctx context.Context, repo string, commit string,
}
}
func (g *SubprocessGit) paginatedRevList(ctx context.Context, repo api.RepoName, commit string, count int) (_ []api.CommitID, nextCursor string, _ error) {
commits, err := g.gs.Commits(ctx, repo, gitserver.CommitsOptions{
N: uint(count + 1),
Range: commit,
FirstParent: true,
})
if err != nil {
return nil, "", err
}
commitIDs := make([]api.CommitID, 0, count+1)
for _, commit := range commits {
commitIDs = append(commitIDs, commit.ID)
}
if len(commitIDs) > count {
nextCursor = string(commitIDs[len(commitIDs)-1])
commitIDs = commitIDs[:count]
}
return commitIDs, nextCursor, nil
}
func newMockRepositoryFetcher(git *SubprocessGit) fetcher.RepositoryFetcher {
return &mockRepositoryFetcher{git: git}
}