mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
gitserver: Implement Stat and ReadDir in gRPC API (#62107)
This commit is contained in:
parent
7b6dd9080e
commit
824337ae6d
@ -320,7 +320,19 @@ func (*rootTreeFileInfo) Size() int64 { return 0 }
|
||||
func (*rootTreeFileInfo) Sys() any { return nil }
|
||||
|
||||
func (r *GitCommitResolver) FileNames(ctx context.Context) ([]string, error) {
|
||||
return r.gitserverClient.LsFiles(ctx, r.gitRepo, api.CommitID(r.oid))
|
||||
fds, err := r.gitserverClient.ReadDir(ctx, r.gitRepo, api.CommitID(r.oid), "", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(fds))
|
||||
for _, fd := range fds {
|
||||
if fd.IsDir() {
|
||||
continue
|
||||
}
|
||||
names = append(names, fd.Name())
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (r *GitCommitResolver) Languages(ctx context.Context) ([]string, error) {
|
||||
|
||||
@ -2,7 +2,9 @@ package graphqlbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
@ -18,6 +20,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
@ -288,7 +291,12 @@ func TestGitCommitFileNames(t *testing.T) {
|
||||
}
|
||||
backend.Mocks.Repos.MockGetCommit_Return_NoCheck(t, &gitdomain.Commit{ID: exampleCommitSHA1})
|
||||
gitserverClient := gitserver.NewMockClient()
|
||||
gitserverClient.LsFilesFunc.SetDefaultReturn([]string{"a", "b"}, nil)
|
||||
gitserverClient.ReadDirFunc.SetDefaultReturn([]fs.FileInfo{
|
||||
&fileutil.FileInfo{Name_: "a"},
|
||||
&fileutil.FileInfo{Name_: "b"},
|
||||
// We also return a dir to check that it's skipped in the output.
|
||||
&fileutil.FileInfo{Name_: "dir", Mode_: os.ModeDir},
|
||||
}, nil)
|
||||
defer func() {
|
||||
backend.Mocks = backend.MockServices{}
|
||||
}()
|
||||
@ -332,7 +340,12 @@ func TestGitCommitAncestors(t *testing.T) {
|
||||
backend.Mocks.Repos.MockGetCommit_Return_NoCheck(t, &gitdomain.Commit{ID: exampleCommitSHA1})
|
||||
|
||||
client := gitserver.NewMockClient()
|
||||
client.LsFilesFunc.SetDefaultReturn([]string{"a", "b"}, nil)
|
||||
client.ReadDirFunc.SetDefaultReturn([]fs.FileInfo{
|
||||
&fileutil.FileInfo{Name_: "a"},
|
||||
&fileutil.FileInfo{Name_: "b"},
|
||||
// We also return a dir to check that it's skipped in the output.
|
||||
&fileutil.FileInfo{Name_: "dir", Mode_: os.ModeDir},
|
||||
}, nil)
|
||||
|
||||
// A linear commit tree:
|
||||
// * -> c1 -> c2 -> c3 -> c4 -> c5 (HEAD)
|
||||
|
||||
@ -22,64 +22,32 @@ import (
|
||||
)
|
||||
|
||||
func TestGitTree_History(t *testing.T) {
|
||||
gitserver.ClientMocks.LocalGitserver = true
|
||||
defer gitserver.ResetClientMocks()
|
||||
|
||||
commands := []string{
|
||||
// |- file1 (added)
|
||||
// `- dir1 (added)
|
||||
// `- file2 (added)
|
||||
"echo -n infile1 > file1",
|
||||
"touch --date=2006-01-02T15:04:05Z file1 || touch -t 200601021704.05 file1",
|
||||
"mkdir dir1",
|
||||
"echo -n infile2 > dir1/file2",
|
||||
"touch --date=2006-01-02T15:04:05Z dir1/file2 || touch -t 200601021704.05 dir1/file2",
|
||||
"git add file1 dir1/file2",
|
||||
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_AUTHOR_DATE=2006-01-02T15:04:05Z GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit1 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
|
||||
|
||||
// |- file1 (modified)
|
||||
// `- dir1 (modified)
|
||||
// |- file2 (unchanged)
|
||||
// `- file3 (added)
|
||||
"echo -n infile3 > dir1/file3",
|
||||
"touch --date=2006-01-02T15:04:05Z dir1/file3 || touch -t 200601021704.05 dir1/file3",
|
||||
"git add dir1/file2 dir1/file3",
|
||||
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_AUTHOR_DATE=2006-01-02T15:04:05Z GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit2 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
|
||||
}
|
||||
repoName := gitserver.MakeGitRepository(t, commands...)
|
||||
oid := api.CommitID("1110324b03e4dc5e98b2543498f44ca269d66d4c")
|
||||
|
||||
ctx := context.Background()
|
||||
gs := gitserver.NewMockClientFrom(gitserver.NewTestClient(t))
|
||||
gs.ResolveRevisionFunc.SetDefaultReturn(oid, nil)
|
||||
gs.ResolveRevisionFunc.SetDefaultReturn("deadbeef", nil)
|
||||
gs.ReadDirFunc.SetDefaultReturn([]fs.FileInfo{
|
||||
&fileutil.FileInfo{Name_: "file1"},
|
||||
}, nil)
|
||||
gs.CommitsFunc.SetDefaultReturn([]*gitdomain.Commit{
|
||||
{ID: "deadbeef"},
|
||||
}, nil)
|
||||
db := dbmocks.NewMockDB()
|
||||
|
||||
rr := NewRepositoryResolver(db, gs, &types.Repo{Name: repoName})
|
||||
gcr := NewGitCommitResolver(db, gs, rr, oid, nil)
|
||||
rr := NewRepositoryResolver(db, gs, &types.Repo{Name: "repo"})
|
||||
gcr := NewGitCommitResolver(db, gs, rr, "deadbeef", nil)
|
||||
|
||||
tree, err := gcr.Tree(ctx, &TreeArgs{Path: ""})
|
||||
require.NoError(t, err)
|
||||
|
||||
entries, err := tree.Entries(ctx, &gitTreeEntryConnectionArgs{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 2)
|
||||
require.Len(t, entries, 1)
|
||||
|
||||
for _, entry := range entries {
|
||||
historyNodes, err := entry.
|
||||
History(ctx, HistoryArgs{}).
|
||||
Nodes(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
switch entry.Path() {
|
||||
case "file1":
|
||||
require.Len(t, historyNodes, 1)
|
||||
case "dir1":
|
||||
require.Len(t, historyNodes, 2)
|
||||
default:
|
||||
panic("unknown")
|
||||
}
|
||||
|
||||
}
|
||||
historyNodes, err := entries[0].
|
||||
History(ctx, HistoryArgs{}).
|
||||
Nodes(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, historyNodes, 1)
|
||||
}
|
||||
|
||||
func TestGitTree_Entries(t *testing.T) {
|
||||
|
||||
@ -120,6 +120,7 @@ go_test(
|
||||
"//internal/database/dbmocks",
|
||||
"//internal/database/dbtest",
|
||||
"//internal/extsvc/gitolite",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/connection",
|
||||
"//internal/gitserver/gitdomain",
|
||||
|
||||
@ -31,12 +31,14 @@ go_library(
|
||||
"//internal/api",
|
||||
"//internal/bytesize",
|
||||
"//internal/byteutils",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver/gitdomain",
|
||||
"//internal/honey",
|
||||
"//internal/lazyregexp",
|
||||
"//internal/trace",
|
||||
"//internal/wrexec",
|
||||
"//lib/errors",
|
||||
"@com_github_go_git_go_git_v5//plumbing/format/config",
|
||||
"@com_github_grafana_regexp//:regexp",
|
||||
"@com_github_hashicorp_golang_lru_v2//:golang-lru",
|
||||
"@com_github_prometheus_client_golang//prometheus",
|
||||
@ -70,10 +72,12 @@ go_test(
|
||||
"//cmd/gitserver/internal/common",
|
||||
"//cmd/gitserver/internal/git",
|
||||
"//internal/api",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/gitdomain",
|
||||
"//internal/wrexec",
|
||||
"//lib/errors",
|
||||
"@com_github_go_git_go_git_v5//plumbing/format/config",
|
||||
"@com_github_google_go_cmp//cmp",
|
||||
"@com_github_google_go_cmp//cmp/cmpopts",
|
||||
"@com_github_sourcegraph_log//logtest",
|
||||
|
||||
@ -1,17 +1,25 @@
|
||||
package gitcli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/format/config"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/git"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/byteutils"
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
@ -276,3 +284,285 @@ func (g *gitCLIBackend) FirstEverCommit(ctx context.Context) (api.CommitID, erro
|
||||
}
|
||||
|
||||
const revListUsageString = `usage: git rev-list [<options>] <commit>... [--] [<path>...]`
|
||||
|
||||
func (g *gitCLIBackend) Stat(ctx context.Context, commit api.CommitID, path string) (_ fs.FileInfo, err error) {
|
||||
if err := checkSpecArgSafety(string(commit)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path = filepath.Clean(rel(path))
|
||||
|
||||
// Special case root, which is not returned by `git ls-tree`.
|
||||
if path == "" || path == "." {
|
||||
rev, err := g.revParse(ctx, string(commit)+"^{tree}")
|
||||
if err != nil {
|
||||
if errors.HasType(err, &gitdomain.RevisionNotFoundError{}) {
|
||||
return nil, &os.PathError{Op: "ls-tree", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
oid, err := decodeOID(rev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fileutil.FileInfo{Mode_: os.ModeDir, Sys_: objectInfo(oid)}, nil
|
||||
}
|
||||
|
||||
it, err := g.lsTree(ctx, commit, path, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := it.Close()
|
||||
if err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
fi, err := it.Next()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, &os.PathError{Op: "ls-tree", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
func (g *gitCLIBackend) ReadDir(ctx context.Context, commit api.CommitID, path string, recursive bool) (git.ReadDirIterator, error) {
|
||||
if err := checkSpecArgSafety(string(commit)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
// Trailing slash is necessary to ls-tree under the dir (not just
|
||||
// to list the dir's tree entry in its parent dir).
|
||||
path = filepath.Clean(rel(path)) + "/"
|
||||
}
|
||||
|
||||
return g.lsTree(ctx, commit, path, recursive)
|
||||
}
|
||||
|
||||
func (g *gitCLIBackend) lsTree(ctx context.Context, commit api.CommitID, path string, recurse bool) (_ git.ReadDirIterator, err error) {
|
||||
// Note: We don't call filepath.Clean(path) because ReadDir needs to pass
|
||||
// path with a trailing slash.
|
||||
|
||||
args := []string{
|
||||
"ls-tree",
|
||||
"--long", // show size
|
||||
"--full-name",
|
||||
"-z",
|
||||
string(commit),
|
||||
}
|
||||
if recurse {
|
||||
args = append(args, "-r", "-t") // -t: Show tree entries even when going to recurse them.
|
||||
}
|
||||
if path != "" {
|
||||
// Note: We need to use :(literal) here to prevent glob expansion which
|
||||
// would lead to incorrect results.
|
||||
args = append(args, "--", pathspecLiteral(filepath.ToSlash(path)))
|
||||
}
|
||||
|
||||
r, err := g.NewCommand(ctx, WithArguments(args...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sc := bufio.NewScanner(r)
|
||||
sc.Split(byteutils.ScanNullLines)
|
||||
|
||||
return &readDirIterator{
|
||||
ctx: ctx,
|
||||
g: g,
|
||||
sc: sc,
|
||||
repoName: g.repoName,
|
||||
commit: commit,
|
||||
path: path,
|
||||
r: r,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type readDirIterator struct {
|
||||
ctx context.Context
|
||||
g *gitCLIBackend
|
||||
sc *bufio.Scanner
|
||||
repoName api.RepoName
|
||||
commit api.CommitID
|
||||
path string
|
||||
fdsSeen int
|
||||
r io.ReadCloser
|
||||
}
|
||||
|
||||
func (it *readDirIterator) Next() (fs.FileInfo, error) {
|
||||
for it.sc.Scan() {
|
||||
line := it.sc.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tabPos := bytes.IndexByte(line, '\t')
|
||||
if tabPos == -1 {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` output: %q", line)
|
||||
}
|
||||
info := bytes.SplitN(line[:tabPos], []byte(" "), 4)
|
||||
|
||||
if len(info) != 4 {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` output: %q", line)
|
||||
}
|
||||
|
||||
name := string(line[tabPos+1:])
|
||||
typ := info[1]
|
||||
sha := info[2]
|
||||
if !gitdomain.IsAbsoluteRevision(string(sha)) {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` SHA output: %q", sha)
|
||||
}
|
||||
oid, err := decodeOID(api.CommitID(sha))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sizeStr := string(bytes.TrimSpace(info[3]))
|
||||
var size int64
|
||||
if sizeStr != "-" {
|
||||
// Size of "-" indicates a dir or submodule.
|
||||
size, err = strconv.ParseInt(sizeStr, 10, 64)
|
||||
if err != nil || size < 0 {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` size output: %q (error: %s)", sizeStr, err)
|
||||
}
|
||||
}
|
||||
|
||||
var sys any
|
||||
modeVal, err := strconv.ParseInt(string(info[0]), 8, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadModConf := sync.OnceValues(func() (config.Config, error) {
|
||||
return it.g.gitModulesConfig(it.ctx, it.commit)
|
||||
})
|
||||
|
||||
mode := os.FileMode(modeVal)
|
||||
switch string(typ) {
|
||||
case "blob":
|
||||
if mode&gitdomain.ModeSymlink != 0 {
|
||||
mode = os.ModeSymlink
|
||||
} else {
|
||||
// Regular file.
|
||||
mode = mode | 0o644
|
||||
}
|
||||
case "commit":
|
||||
mode = gitdomain.ModeSubmodule
|
||||
|
||||
modconf, err := loadModConf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
submodule := gitdomain.Submodule{
|
||||
URL: modconf.Section("submodule").Subsection(name).Option("url"),
|
||||
Path: modconf.Section("submodule").Subsection(name).Option("path"),
|
||||
CommitID: api.CommitID(oid.String()),
|
||||
}
|
||||
|
||||
sys = submodule
|
||||
case "tree":
|
||||
mode = mode | os.ModeDir
|
||||
}
|
||||
|
||||
if sys == nil {
|
||||
// Some callers might find it useful to know the object's OID.
|
||||
sys = objectInfo(oid)
|
||||
}
|
||||
|
||||
it.fdsSeen++
|
||||
|
||||
return &fileutil.FileInfo{
|
||||
Name_: name, // full path relative to root (not just basename)
|
||||
Mode_: mode,
|
||||
Size_: size,
|
||||
Sys_: sys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := it.sc.Err(); err != nil {
|
||||
var cfe *CommandFailedError
|
||||
if errors.As(err, &cfe) {
|
||||
if bytes.Contains(cfe.Stderr, []byte("exists on disk, but not in")) {
|
||||
return nil, &os.PathError{Op: "ls-tree", Path: filepath.ToSlash(it.path), Err: os.ErrNotExist}
|
||||
}
|
||||
if cfe.ExitStatus == 128 && bytes.Contains(cfe.Stderr, []byte("fatal: not a tree object")) {
|
||||
return nil, &gitdomain.RevisionNotFoundError{Repo: it.repoName, Spec: string(it.commit)}
|
||||
}
|
||||
if cfe.ExitStatus == 128 && bytes.Contains(cfe.Stderr, []byte("fatal: Not a valid object name")) {
|
||||
return nil, &gitdomain.RevisionNotFoundError{Repo: it.repoName, Spec: string(it.commit)}
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we are listing the empty root tree, we will have no output.
|
||||
if it.fdsSeen == 0 && filepath.Clean(it.path) != "." {
|
||||
return nil, &os.PathError{Op: "git ls-tree", Path: it.path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
func (it *readDirIterator) Close() error {
|
||||
if err := it.r.Close(); err != nil {
|
||||
var cfe *CommandFailedError
|
||||
if errors.As(err, &cfe) {
|
||||
if bytes.Contains(cfe.Stderr, []byte("exists on disk, but not in")) {
|
||||
return &os.PathError{Op: "ls-tree", Path: filepath.ToSlash(it.path), Err: os.ErrNotExist}
|
||||
}
|
||||
if cfe.ExitStatus == 128 && bytes.Contains(cfe.Stderr, []byte("fatal: not a tree object")) {
|
||||
return &gitdomain.RevisionNotFoundError{Repo: it.repoName, Spec: string(it.commit)}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// gitModulesConfig returns the gitmodules configuration for the given commit.
|
||||
func (g *gitCLIBackend) gitModulesConfig(ctx context.Context, commit api.CommitID) (config.Config, error) {
|
||||
r, err := g.ReadFile(ctx, commit, ".gitmodules")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return config.Config{}, nil
|
||||
}
|
||||
return config.Config{}, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
modfile, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
|
||||
var cfg config.Config
|
||||
err = config.NewDecoder(bytes.NewBuffer(modfile)).Decode(&cfg)
|
||||
if err != nil {
|
||||
return config.Config{}, errors.Wrap(err, "error parsing .gitmodules")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// rel strips the leading "/" prefix from the path string, effectively turning
|
||||
// an absolute path into one relative to the root directory. A path that is just
|
||||
// "/" is treated specially, returning just ".".
|
||||
//
|
||||
// The elements in a file path are separated by slash ('/', U+002F) characters,
|
||||
// regardless of host operating system convention.
|
||||
func rel(path string) string {
|
||||
if path == "/" {
|
||||
return "."
|
||||
}
|
||||
return strings.TrimPrefix(path, "/")
|
||||
}
|
||||
|
||||
type objectInfo gitdomain.OID
|
||||
|
||||
func (oid objectInfo) OID() gitdomain.OID { return gitdomain.OID(oid) }
|
||||
|
||||
@ -4,17 +4,21 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/format/config"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/git"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
@ -170,7 +174,7 @@ func TestGitCLIBackend_ReadFile_GoroutineLeak(t *testing.T) {
|
||||
require.Equal(t, routinesBefore, routinesAfter)
|
||||
}
|
||||
|
||||
func TestRepository_GetCommit(t *testing.T) {
|
||||
func TestGitCLIBackend_GetCommit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Prepare repo state:
|
||||
@ -421,3 +425,397 @@ func TestGitCLIBackend_GetBehindAhead(t *testing.T) {
|
||||
require.True(t, errors.As(err, &e))
|
||||
})
|
||||
}
|
||||
func TestGitCLIBackend_Stat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Prepare repo state:
|
||||
submodDir := RepoWithCommands(t,
|
||||
// simple file
|
||||
"echo abcd > file1",
|
||||
"git add file1",
|
||||
"git commit -m commit --author='Foo Author <foo@sourcegraph.com>'",
|
||||
)
|
||||
|
||||
backend := BackendWithRepoCommands(t,
|
||||
"echo abcd > file1",
|
||||
"git add file1",
|
||||
"mkdir nested",
|
||||
"echo efgh > nested/file",
|
||||
"git add nested/file",
|
||||
"ln -s nested/file link",
|
||||
"git add link",
|
||||
"git -c protocol.file.allow=always submodule add "+filepath.ToSlash(string(submodDir))+" submodule",
|
||||
"git commit -m commit --author='Foo Author <foo@sourcegraph.com>'",
|
||||
"echo defg > file2",
|
||||
"git add file2",
|
||||
"git commit -m commit2 --author='Foo Author <foo@sourcegraph.com>'",
|
||||
)
|
||||
|
||||
commitID, err := backend.RevParseHead(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := backend.GetCommit(ctx, commitID, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Parents))
|
||||
|
||||
t.Run("non existent file", func(t *testing.T) {
|
||||
_, err := backend.Stat(ctx, commitID, "file0")
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("file exists but not at commit", func(t *testing.T) {
|
||||
_, err := backend.Stat(ctx, commitID, "file2")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = backend.Stat(ctx, c.Parents[0], "file0")
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("non existent commit", func(t *testing.T) {
|
||||
_, err := backend.Stat(ctx, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "file1")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.HasType(err, &gitdomain.RevisionNotFoundError{}))
|
||||
})
|
||||
|
||||
t.Run("stat root", func(t *testing.T) {
|
||||
fi, err := backend.Stat(ctx, commitID, "")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "",
|
||||
Size_: 0,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.True(t, fi.IsDir())
|
||||
require.False(t, fi.Mode().IsRegular())
|
||||
fi, err = backend.Stat(ctx, commitID, ".")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "",
|
||||
Size_: 0,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.True(t, fi.IsDir())
|
||||
require.False(t, fi.Mode().IsRegular())
|
||||
})
|
||||
|
||||
t.Run("stat file", func(t *testing.T) {
|
||||
fi, err := backend.Stat(ctx, commitID, "file1")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "file1",
|
||||
Size_: 5,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
require.True(t, fi.Mode().IsRegular())
|
||||
fi, err = backend.Stat(ctx, commitID, "nested/../file1")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "file1",
|
||||
Size_: 5,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
require.True(t, fi.Mode().IsRegular())
|
||||
fi, err = backend.Stat(ctx, commitID, "/file1")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "file1",
|
||||
Size_: 5,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
require.True(t, fi.Mode().IsRegular())
|
||||
})
|
||||
|
||||
t.Run("stat symlink", func(t *testing.T) {
|
||||
fi, err := backend.Stat(ctx, commitID, "link")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "link",
|
||||
Size_: 11,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
|
||||
cfg, err := backend.(*gitCLIBackend).gitModulesConfig(ctx, commitID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, config.Config{
|
||||
Sections: config.Sections{
|
||||
{
|
||||
Name: "submodule",
|
||||
Subsections: config.Subsections{
|
||||
{
|
||||
Name: "submodule",
|
||||
Options: config.Options{
|
||||
{
|
||||
Key: "path",
|
||||
Value: "submodule",
|
||||
},
|
||||
{
|
||||
Key: "url",
|
||||
Value: string(submodDir),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cfg)
|
||||
})
|
||||
|
||||
t.Run("stat submodule", func(t *testing.T) {
|
||||
fi, err := backend.Stat(ctx, commitID, "submodule")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "submodule",
|
||||
Size_: 0,
|
||||
Sys_: gitdomain.Submodule{
|
||||
URL: string(submodDir),
|
||||
Path: "submodule",
|
||||
CommitID: "405b565ed446e271bc1998a91dbf4fb50dbfabfe",
|
||||
},
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.Equal(t, gitdomain.ModeSubmodule, fi.Mode())
|
||||
require.False(t, fi.Mode().IsRegular())
|
||||
})
|
||||
|
||||
t.Run("stat dir", func(t *testing.T) {
|
||||
fi, err := backend.Stat(ctx, commitID, "nested")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "nested",
|
||||
Size_: 0,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.True(t, fi.IsDir())
|
||||
require.False(t, fi.Mode().IsRegular())
|
||||
fi, err = backend.Stat(ctx, commitID, "nested/")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "nested",
|
||||
Size_: 0,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.True(t, fi.IsDir())
|
||||
require.False(t, fi.Mode().IsRegular())
|
||||
})
|
||||
|
||||
t.Run("stat nested file", func(t *testing.T) {
|
||||
fi, err := backend.Stat(ctx, commitID, "nested/file")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "nested/file",
|
||||
Size_: 5,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
require.True(t, fi.Mode().IsRegular())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitCLIBackend_Stat_specialchars(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
backend := BackendWithRepoCommands(t,
|
||||
`touch ⊗.txt '".txt' \\.txt`,
|
||||
`git add ⊗.txt '".txt' \\.txt`,
|
||||
"git commit -m commit --author='Foo Author <foo@sourcegraph.com>'",
|
||||
)
|
||||
|
||||
commitID, err := backend.RevParseHead(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
fi, err := backend.Stat(ctx, commitID, "⊗.txt")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: "⊗.txt",
|
||||
Size_: 0,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
require.True(t, fi.Mode().IsRegular())
|
||||
fi, err = backend.Stat(ctx, commitID, `".txt`)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: `".txt`,
|
||||
Size_: 0,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
require.True(t, fi.Mode().IsRegular())
|
||||
fi, err = backend.Stat(ctx, commitID, `\.txt`)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(&fileutil.FileInfo{
|
||||
Name_: `\.txt`,
|
||||
Size_: 0,
|
||||
Sys_: fi.Sys(),
|
||||
}, fi, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.False(t, fi.IsDir())
|
||||
require.True(t, fi.Mode().IsRegular())
|
||||
}
|
||||
|
||||
func TestGitCLIBackend_ReadDir(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Prepare repo state:
|
||||
submodDir := RepoWithCommands(t,
|
||||
// simple file
|
||||
"echo abcd > file1",
|
||||
"git add file1",
|
||||
"git commit -m commit --author='Foo Author <foo@sourcegraph.com>'",
|
||||
)
|
||||
|
||||
backend := BackendWithRepoCommands(t,
|
||||
"echo abcd > file1",
|
||||
"git add file1",
|
||||
"mkdir nested",
|
||||
"echo efgh > nested/file",
|
||||
"git add nested/file",
|
||||
"ln -s nested/file link",
|
||||
"git add link",
|
||||
"git -c protocol.file.allow=always submodule add "+filepath.ToSlash(string(submodDir))+" submodule",
|
||||
"git commit -m commit --author='Foo Author <foo@sourcegraph.com>'",
|
||||
)
|
||||
|
||||
commitID, err := backend.RevParseHead(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("bad input", func(t *testing.T) {
|
||||
_, err := backend.ReadDir(ctx, "-commit", "file", false)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("non existent path", func(t *testing.T) {
|
||||
it, err := backend.ReadDir(ctx, commitID, "404dir", false)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { it.Close() })
|
||||
_, err = it.Next()
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("non existent tree-ish", func(t *testing.T) {
|
||||
it, err := backend.ReadDir(ctx, "notfound", "nested", false)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { it.Close() })
|
||||
_, err = it.Next()
|
||||
require.Error(t, err)
|
||||
|
||||
t.Log(err)
|
||||
require.True(t, errors.HasType(err, &gitdomain.RevisionNotFoundError{}))
|
||||
})
|
||||
|
||||
t.Run("non existent commit", func(t *testing.T) {
|
||||
it, err := backend.ReadDir(ctx, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "nested", false)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { it.Close() })
|
||||
_, err = it.Next()
|
||||
require.Error(t, err)
|
||||
|
||||
require.True(t, errors.HasType(err, &gitdomain.RevisionNotFoundError{}))
|
||||
})
|
||||
|
||||
t.Run("read root", func(t *testing.T) {
|
||||
it, err := backend.ReadDir(ctx, commitID, "", false)
|
||||
require.NoError(t, err)
|
||||
fis := make([]fs.FileInfo, 0)
|
||||
for {
|
||||
fi, err := it.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
fis = append(fis, fi)
|
||||
}
|
||||
require.Empty(t, cmp.Diff([]fs.FileInfo{
|
||||
&fileutil.FileInfo{
|
||||
Name_: ".gitmodules",
|
||||
Size_: fis[0].Size(),
|
||||
Sys_: fis[0].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "file1",
|
||||
Size_: 5,
|
||||
Sys_: fis[1].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "link",
|
||||
Size_: 11,
|
||||
Sys_: fis[2].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "nested",
|
||||
Size_: 0,
|
||||
Sys_: fis[3].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "submodule",
|
||||
Size_: 0,
|
||||
Sys_: fis[4].Sys(),
|
||||
},
|
||||
}, fis, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.True(t, fis[3].IsDir())
|
||||
it, err = backend.ReadDir(ctx, commitID, ".", false)
|
||||
require.NoError(t, err)
|
||||
dotFis := make([]fs.FileInfo, 0)
|
||||
for {
|
||||
fi, err := it.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
dotFis = append(dotFis, fi)
|
||||
}
|
||||
require.Empty(t, cmp.Diff(fis, dotFis, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
})
|
||||
|
||||
t.Run("read root recursive", func(t *testing.T) {
|
||||
it, err := backend.ReadDir(ctx, commitID, "", true)
|
||||
require.NoError(t, err)
|
||||
fis := make([]fs.FileInfo, 0)
|
||||
for {
|
||||
fi, err := it.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
fis = append(fis, fi)
|
||||
}
|
||||
require.Empty(t, cmp.Diff([]fs.FileInfo{
|
||||
&fileutil.FileInfo{
|
||||
Name_: ".gitmodules",
|
||||
Size_: fis[0].Size(),
|
||||
Sys_: fis[0].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "file1",
|
||||
Size_: 5,
|
||||
Sys_: fis[1].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "link",
|
||||
Size_: 11,
|
||||
Sys_: fis[2].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "nested",
|
||||
Size_: 0,
|
||||
Sys_: fis[3].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "nested/file",
|
||||
Size_: 5,
|
||||
Sys_: fis[4].Sys(),
|
||||
},
|
||||
&fileutil.FileInfo{
|
||||
Name_: "submodule",
|
||||
Size_: 0,
|
||||
Sys_: fis[5].Sys(),
|
||||
},
|
||||
}, fis, cmpopts.IgnoreFields(fileutil.FileInfo{}, "Mode_")))
|
||||
require.True(t, fis[3].IsDir())
|
||||
})
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
@ -92,6 +93,19 @@ type GitBackend interface {
|
||||
// Aggregations are done by email address.
|
||||
// If range does not exist, a RevisionNotFoundError is returned.
|
||||
ContributorCounts(ctx context.Context, opt ContributorCountsOpts) ([]*gitdomain.ContributorCount, error)
|
||||
// Stat returns the file info for the given path at the given commit.
|
||||
// If the file does not exist, a os.PathError is returned.
|
||||
// If the commit does not exist, a RevisionNotFoundError is returned.
|
||||
// Stat supports submodules, symlinks, directories and files.
|
||||
Stat(ctx context.Context, commit api.CommitID, path string) (fs.FileInfo, error)
|
||||
// ReadDir returns the list of files and directories in the given path at the given commit.
|
||||
// Path can be used to read subdirectories.
|
||||
// If the path does not exist, a os.PathError is returned.
|
||||
// If the commit does not exist, a RevisionNotFoundError is returned.
|
||||
// ReadDir supports submodules, symlinks, directories and files.
|
||||
// If recursive is true, ReadDir will return the contents of all subdirectories.
|
||||
// The caller must call Close on the returned ReadDirIterator when done.
|
||||
ReadDir(ctx context.Context, commit api.CommitID, path string, recursive bool) (ReadDirIterator, error)
|
||||
|
||||
// Exec is a temporary helper to run arbitrary git commands from the exec endpoint.
|
||||
// No new usages of it should be introduced and once the migration is done we will
|
||||
@ -245,3 +259,14 @@ type ContributorCountsOpts struct {
|
||||
// (e.g., "foo/bar/").
|
||||
Path string
|
||||
}
|
||||
|
||||
// ReadDirIterator is an iterator for the contents of a directory.
|
||||
// The caller MUST Close() this iterator when done, regardless of whether an error
|
||||
// was returned from Next().
|
||||
type ReadDirIterator interface {
|
||||
// Next returns the next file in the directory. io.EOF is returned at the end
|
||||
// of the stream.
|
||||
Next() (fs.FileInfo, error)
|
||||
// Close closes the iterator.
|
||||
Close() error
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -323,6 +324,9 @@ type MockGitBackend struct {
|
||||
// RawDiffFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method RawDiff.
|
||||
RawDiffFunc *GitBackendRawDiffFunc
|
||||
// ReadDirFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method ReadDir.
|
||||
ReadDirFunc *GitBackendReadDirFunc
|
||||
// ReadFileFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method ReadFile.
|
||||
ReadFileFunc *GitBackendReadFileFunc
|
||||
@ -335,6 +339,9 @@ type MockGitBackend struct {
|
||||
// RevParseHeadFunc is an instance of a mock function object controlling
|
||||
// the behavior of the method RevParseHead.
|
||||
RevParseHeadFunc *GitBackendRevParseHeadFunc
|
||||
// StatFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Stat.
|
||||
StatFunc *GitBackendStatFunc
|
||||
// SymbolicRefHeadFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method SymbolicRefHead.
|
||||
SymbolicRefHeadFunc *GitBackendSymbolicRefHeadFunc
|
||||
@ -409,6 +416,11 @@ func NewMockGitBackend() *MockGitBackend {
|
||||
return
|
||||
},
|
||||
},
|
||||
ReadDirFunc: &GitBackendReadDirFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string, bool) (r0 ReadDirIterator, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
ReadFileFunc: &GitBackendReadFileFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string) (r0 io.ReadCloser, r1 error) {
|
||||
return
|
||||
@ -429,6 +441,11 @@ func NewMockGitBackend() *MockGitBackend {
|
||||
return
|
||||
},
|
||||
},
|
||||
StatFunc: &GitBackendStatFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string) (r0 fs.FileInfo, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
SymbolicRefHeadFunc: &GitBackendSymbolicRefHeadFunc{
|
||||
defaultHook: func(context.Context, bool) (r0 string, r1 error) {
|
||||
return
|
||||
@ -506,6 +523,11 @@ func NewStrictMockGitBackend() *MockGitBackend {
|
||||
panic("unexpected invocation of MockGitBackend.RawDiff")
|
||||
},
|
||||
},
|
||||
ReadDirFunc: &GitBackendReadDirFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string, bool) (ReadDirIterator, error) {
|
||||
panic("unexpected invocation of MockGitBackend.ReadDir")
|
||||
},
|
||||
},
|
||||
ReadFileFunc: &GitBackendReadFileFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string) (io.ReadCloser, error) {
|
||||
panic("unexpected invocation of MockGitBackend.ReadFile")
|
||||
@ -526,6 +548,11 @@ func NewStrictMockGitBackend() *MockGitBackend {
|
||||
panic("unexpected invocation of MockGitBackend.RevParseHead")
|
||||
},
|
||||
},
|
||||
StatFunc: &GitBackendStatFunc{
|
||||
defaultHook: func(context.Context, api.CommitID, string) (fs.FileInfo, error) {
|
||||
panic("unexpected invocation of MockGitBackend.Stat")
|
||||
},
|
||||
},
|
||||
SymbolicRefHeadFunc: &GitBackendSymbolicRefHeadFunc{
|
||||
defaultHook: func(context.Context, bool) (string, error) {
|
||||
panic("unexpected invocation of MockGitBackend.SymbolicRefHead")
|
||||
@ -577,6 +604,9 @@ func NewMockGitBackendFrom(i GitBackend) *MockGitBackend {
|
||||
RawDiffFunc: &GitBackendRawDiffFunc{
|
||||
defaultHook: i.RawDiff,
|
||||
},
|
||||
ReadDirFunc: &GitBackendReadDirFunc{
|
||||
defaultHook: i.ReadDir,
|
||||
},
|
||||
ReadFileFunc: &GitBackendReadFileFunc{
|
||||
defaultHook: i.ReadFile,
|
||||
},
|
||||
@ -589,6 +619,9 @@ func NewMockGitBackendFrom(i GitBackend) *MockGitBackend {
|
||||
RevParseHeadFunc: &GitBackendRevParseHeadFunc{
|
||||
defaultHook: i.RevParseHead,
|
||||
},
|
||||
StatFunc: &GitBackendStatFunc{
|
||||
defaultHook: i.Stat,
|
||||
},
|
||||
SymbolicRefHeadFunc: &GitBackendSymbolicRefHeadFunc{
|
||||
defaultHook: i.SymbolicRefHead,
|
||||
},
|
||||
@ -2032,6 +2065,120 @@ func (c GitBackendRawDiffFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitBackendReadDirFunc describes the behavior when the ReadDir method of
|
||||
// the parent MockGitBackend instance is invoked.
|
||||
type GitBackendReadDirFunc struct {
|
||||
defaultHook func(context.Context, api.CommitID, string, bool) (ReadDirIterator, error)
|
||||
hooks []func(context.Context, api.CommitID, string, bool) (ReadDirIterator, error)
|
||||
history []GitBackendReadDirFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// ReadDir delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockGitBackend) ReadDir(v0 context.Context, v1 api.CommitID, v2 string, v3 bool) (ReadDirIterator, error) {
|
||||
r0, r1 := m.ReadDirFunc.nextHook()(v0, v1, v2, v3)
|
||||
m.ReadDirFunc.appendCall(GitBackendReadDirFuncCall{v0, v1, v2, v3, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the ReadDir method of
|
||||
// the parent MockGitBackend instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *GitBackendReadDirFunc) SetDefaultHook(hook func(context.Context, api.CommitID, string, bool) (ReadDirIterator, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// ReadDir 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 *GitBackendReadDirFunc) PushHook(hook func(context.Context, api.CommitID, string, bool) (ReadDirIterator, 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 *GitBackendReadDirFunc) SetDefaultReturn(r0 ReadDirIterator, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.CommitID, string, bool) (ReadDirIterator, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *GitBackendReadDirFunc) PushReturn(r0 ReadDirIterator, r1 error) {
|
||||
f.PushHook(func(context.Context, api.CommitID, string, bool) (ReadDirIterator, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *GitBackendReadDirFunc) nextHook() func(context.Context, api.CommitID, string, bool) (ReadDirIterator, 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 *GitBackendReadDirFunc) appendCall(r0 GitBackendReadDirFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of GitBackendReadDirFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *GitBackendReadDirFunc) History() []GitBackendReadDirFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]GitBackendReadDirFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// GitBackendReadDirFuncCall is an object that describes an invocation of
|
||||
// method ReadDir on an instance of MockGitBackend.
|
||||
type GitBackendReadDirFuncCall 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.CommitID
|
||||
// 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 bool
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 ReadDirIterator
|
||||
// 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 GitBackendReadDirFuncCall) 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 GitBackendReadDirFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitBackendReadFileFunc describes the behavior when the ReadFile method of
|
||||
// the parent MockGitBackend instance is invoked.
|
||||
type GitBackendReadFileFunc struct {
|
||||
@ -2467,6 +2614,116 @@ func (c GitBackendRevParseHeadFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitBackendStatFunc describes the behavior when the Stat method of the
|
||||
// parent MockGitBackend instance is invoked.
|
||||
type GitBackendStatFunc struct {
|
||||
defaultHook func(context.Context, api.CommitID, string) (fs.FileInfo, error)
|
||||
hooks []func(context.Context, api.CommitID, string) (fs.FileInfo, error)
|
||||
history []GitBackendStatFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// Stat delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockGitBackend) Stat(v0 context.Context, v1 api.CommitID, v2 string) (fs.FileInfo, error) {
|
||||
r0, r1 := m.StatFunc.nextHook()(v0, v1, v2)
|
||||
m.StatFunc.appendCall(GitBackendStatFuncCall{v0, v1, v2, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the Stat method of the
|
||||
// parent MockGitBackend instance is invoked and the hook queue is empty.
|
||||
func (f *GitBackendStatFunc) SetDefaultHook(hook func(context.Context, api.CommitID, string) (fs.FileInfo, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// Stat 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 *GitBackendStatFunc) PushHook(hook func(context.Context, api.CommitID, string) (fs.FileInfo, 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 *GitBackendStatFunc) SetDefaultReturn(r0 fs.FileInfo, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.CommitID, string) (fs.FileInfo, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *GitBackendStatFunc) PushReturn(r0 fs.FileInfo, r1 error) {
|
||||
f.PushHook(func(context.Context, api.CommitID, string) (fs.FileInfo, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *GitBackendStatFunc) nextHook() func(context.Context, api.CommitID, string) (fs.FileInfo, 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 *GitBackendStatFunc) appendCall(r0 GitBackendStatFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of GitBackendStatFuncCall objects describing
|
||||
// the invocations of this function.
|
||||
func (f *GitBackendStatFunc) History() []GitBackendStatFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]GitBackendStatFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// GitBackendStatFuncCall is an object that describes an invocation of
|
||||
// method Stat on an instance of MockGitBackend.
|
||||
type GitBackendStatFuncCall 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.CommitID
|
||||
// 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 fs.FileInfo
|
||||
// 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 GitBackendStatFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c GitBackendStatFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitBackendSymbolicRefHeadFunc describes the behavior when the
|
||||
// SymbolicRefHead method of the parent MockGitBackend instance is invoked.
|
||||
type GitBackendSymbolicRefHeadFunc struct {
|
||||
@ -2974,6 +3231,269 @@ func (c GitConfigBackendUnsetFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// MockReadDirIterator is a mock implementation of the ReadDirIterator
|
||||
// interface (from the package
|
||||
// github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/git) used for
|
||||
// unit testing.
|
||||
type MockReadDirIterator struct {
|
||||
// CloseFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Close.
|
||||
CloseFunc *ReadDirIteratorCloseFunc
|
||||
// NextFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Next.
|
||||
NextFunc *ReadDirIteratorNextFunc
|
||||
}
|
||||
|
||||
// NewMockReadDirIterator creates a new mock of the ReadDirIterator
|
||||
// interface. All methods return zero values for all results, unless
|
||||
// overwritten.
|
||||
func NewMockReadDirIterator() *MockReadDirIterator {
|
||||
return &MockReadDirIterator{
|
||||
CloseFunc: &ReadDirIteratorCloseFunc{
|
||||
defaultHook: func() (r0 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
NextFunc: &ReadDirIteratorNextFunc{
|
||||
defaultHook: func() (r0 fs.FileInfo, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewStrictMockReadDirIterator creates a new mock of the ReadDirIterator
|
||||
// interface. All methods panic on invocation, unless overwritten.
|
||||
func NewStrictMockReadDirIterator() *MockReadDirIterator {
|
||||
return &MockReadDirIterator{
|
||||
CloseFunc: &ReadDirIteratorCloseFunc{
|
||||
defaultHook: func() error {
|
||||
panic("unexpected invocation of MockReadDirIterator.Close")
|
||||
},
|
||||
},
|
||||
NextFunc: &ReadDirIteratorNextFunc{
|
||||
defaultHook: func() (fs.FileInfo, error) {
|
||||
panic("unexpected invocation of MockReadDirIterator.Next")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMockReadDirIteratorFrom creates a new mock of the MockReadDirIterator
|
||||
// interface. All methods delegate to the given implementation, unless
|
||||
// overwritten.
|
||||
func NewMockReadDirIteratorFrom(i ReadDirIterator) *MockReadDirIterator {
|
||||
return &MockReadDirIterator{
|
||||
CloseFunc: &ReadDirIteratorCloseFunc{
|
||||
defaultHook: i.Close,
|
||||
},
|
||||
NextFunc: &ReadDirIteratorNextFunc{
|
||||
defaultHook: i.Next,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ReadDirIteratorCloseFunc describes the behavior when the Close method of
|
||||
// the parent MockReadDirIterator instance is invoked.
|
||||
type ReadDirIteratorCloseFunc struct {
|
||||
defaultHook func() error
|
||||
hooks []func() error
|
||||
history []ReadDirIteratorCloseFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// Close delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockReadDirIterator) Close() error {
|
||||
r0 := m.CloseFunc.nextHook()()
|
||||
m.CloseFunc.appendCall(ReadDirIteratorCloseFuncCall{r0})
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the Close method of the
|
||||
// parent MockReadDirIterator instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *ReadDirIteratorCloseFunc) SetDefaultHook(hook func() error) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// Close method of the parent MockReadDirIterator 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 *ReadDirIteratorCloseFunc) PushHook(hook func() 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 *ReadDirIteratorCloseFunc) SetDefaultReturn(r0 error) {
|
||||
f.SetDefaultHook(func() error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *ReadDirIteratorCloseFunc) PushReturn(r0 error) {
|
||||
f.PushHook(func() error {
|
||||
return r0
|
||||
})
|
||||
}
|
||||
|
||||
func (f *ReadDirIteratorCloseFunc) nextHook() func() 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 *ReadDirIteratorCloseFunc) appendCall(r0 ReadDirIteratorCloseFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of ReadDirIteratorCloseFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *ReadDirIteratorCloseFunc) History() []ReadDirIteratorCloseFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]ReadDirIteratorCloseFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// ReadDirIteratorCloseFuncCall is an object that describes an invocation of
|
||||
// method Close on an instance of MockReadDirIterator.
|
||||
type ReadDirIteratorCloseFuncCall struct {
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c ReadDirIteratorCloseFuncCall) Args() []interface{} {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c ReadDirIteratorCloseFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// ReadDirIteratorNextFunc describes the behavior when the Next method of
|
||||
// the parent MockReadDirIterator instance is invoked.
|
||||
type ReadDirIteratorNextFunc struct {
|
||||
defaultHook func() (fs.FileInfo, error)
|
||||
hooks []func() (fs.FileInfo, error)
|
||||
history []ReadDirIteratorNextFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// Next delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockReadDirIterator) Next() (fs.FileInfo, error) {
|
||||
r0, r1 := m.NextFunc.nextHook()()
|
||||
m.NextFunc.appendCall(ReadDirIteratorNextFuncCall{r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the Next method of the
|
||||
// parent MockReadDirIterator instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *ReadDirIteratorNextFunc) SetDefaultHook(hook func() (fs.FileInfo, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// Next method of the parent MockReadDirIterator 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 *ReadDirIteratorNextFunc) PushHook(hook func() (fs.FileInfo, 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 *ReadDirIteratorNextFunc) SetDefaultReturn(r0 fs.FileInfo, r1 error) {
|
||||
f.SetDefaultHook(func() (fs.FileInfo, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *ReadDirIteratorNextFunc) PushReturn(r0 fs.FileInfo, r1 error) {
|
||||
f.PushHook(func() (fs.FileInfo, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *ReadDirIteratorNextFunc) nextHook() func() (fs.FileInfo, 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 *ReadDirIteratorNextFunc) appendCall(r0 ReadDirIteratorNextFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of ReadDirIteratorNextFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *ReadDirIteratorNextFunc) History() []ReadDirIteratorNextFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]ReadDirIteratorNextFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// ReadDirIteratorNextFuncCall is an object that describes an invocation of
|
||||
// method Next on an instance of MockReadDirIterator.
|
||||
type ReadDirIteratorNextFuncCall struct {
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 fs.FileInfo
|
||||
// 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 ReadDirIteratorNextFuncCall) Args() []interface{} {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c ReadDirIteratorNextFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// MockRefIterator is a mock implementation of the RefIterator interface
|
||||
// (from the package
|
||||
// github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/git) used for
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@ -399,6 +400,66 @@ func (b *observableBackend) ChangedFiles(ctx context.Context, base, head string)
|
||||
return b.backend.ChangedFiles(ctx, base, head)
|
||||
}
|
||||
|
||||
func (b *observableBackend) Stat(ctx context.Context, commit api.CommitID, path string) (_ fs.FileInfo, err error) {
|
||||
ctx, _, endObservation := b.operations.stat.With(ctx, &err, observation.Args{
|
||||
Attrs: []attribute.KeyValue{
|
||||
attribute.String("commit", string(commit)),
|
||||
attribute.String("path", path),
|
||||
},
|
||||
})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
concurrentOps.WithLabelValues("Stat").Inc()
|
||||
defer concurrentOps.WithLabelValues("Stat").Dec()
|
||||
|
||||
return b.backend.Stat(ctx, commit, path)
|
||||
}
|
||||
|
||||
func (b *observableBackend) ReadDir(ctx context.Context, commit api.CommitID, path string, recursive bool) (_ ReadDirIterator, err error) {
|
||||
ctx, errCollector, endObservation := b.operations.readDir.WithErrors(ctx, &err, observation.Args{
|
||||
Attrs: []attribute.KeyValue{
|
||||
attribute.String("commit", string(commit)),
|
||||
attribute.String("path", path),
|
||||
attribute.Bool("recursive", recursive),
|
||||
},
|
||||
})
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
endObservation.OnCancel(ctx, 1, observation.Args{})
|
||||
|
||||
concurrentOps.WithLabelValues("ReadDir").Inc()
|
||||
|
||||
it, err := b.backend.ReadDir(ctx, commit, path, recursive)
|
||||
if err != nil {
|
||||
concurrentOps.WithLabelValues("ReadDir").Dec()
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &observableReadDirIterator{
|
||||
inner: it,
|
||||
onClose: func(err error) {
|
||||
concurrentOps.WithLabelValues("ReadDir").Dec()
|
||||
errCollector.Collect(&err)
|
||||
cancel()
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type observableReadDirIterator struct {
|
||||
inner ReadDirIterator
|
||||
onClose func(err error)
|
||||
}
|
||||
|
||||
func (hr *observableReadDirIterator) Next() (fs.FileInfo, error) {
|
||||
return hr.inner.Next()
|
||||
}
|
||||
|
||||
func (hr *observableReadDirIterator) Close() error {
|
||||
err := hr.inner.Close()
|
||||
hr.onClose(err)
|
||||
return err
|
||||
}
|
||||
|
||||
type operations struct {
|
||||
configGet *observation.Operation
|
||||
configSet *observation.Operation
|
||||
@ -420,6 +481,8 @@ type operations struct {
|
||||
firstEverCommit *observation.Operation
|
||||
getBehindAhead *observation.Operation
|
||||
changedFiles *observation.Operation
|
||||
stat *observation.Operation
|
||||
readDir *observation.Operation
|
||||
}
|
||||
|
||||
func newOperations(observationCtx *observation.Context) *operations {
|
||||
@ -468,6 +531,8 @@ func newOperations(observationCtx *observation.Context) *operations {
|
||||
firstEverCommit: op("first-ever-commit"),
|
||||
getBehindAhead: op("get-behind-ahead"),
|
||||
changedFiles: op("changed-files"),
|
||||
stat: op("stat"),
|
||||
readDir: op("read-dir"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,10 @@
|
||||
package inttests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
@ -21,376 +13,9 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func TestRepository_FileSystem(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
// In all tests, repo should contain three commits. The first commit
|
||||
// (whose ID is in the 'first' field) has a file at dir1/file1 with the
|
||||
// contents "myfile1" and the mtime 2006-01-02T15:04:05Z. The second
|
||||
// commit (whose ID is in the 'second' field) adds a file at file2 (in the
|
||||
// top-level directory of the repository) with the contents "infile2" and
|
||||
// the mtime 2014-05-06T19:20:21Z. The third commit contains an empty
|
||||
// tree.
|
||||
//
|
||||
// TODO(sqs): add symlinks, etc.
|
||||
gitCommands := []string{
|
||||
"mkdir dir1",
|
||||
"echo -n infile1 > dir1/file1",
|
||||
"touch --date=2006-01-02T15:04:05Z dir1 dir1/file1 || touch -t " + Times[0] + " dir1 dir1/file1",
|
||||
"git add dir1/file1",
|
||||
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit1 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
|
||||
"echo -n infile2 > 'file 2'",
|
||||
"touch --date=2014-05-06T19:20:21Z 'file 2' || touch -t " + Times[1] + " 'file 2'",
|
||||
"git add 'file 2'",
|
||||
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2014-05-06T19:20:21Z git commit -m commit2 --author='a <a@a.com>' --date 2014-05-06T19:20:21Z",
|
||||
"git rm 'dir1/file1' 'file 2'",
|
||||
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2018-05-06T19:20:21Z git commit -m commit3 --author='a <a@a.com>' --date 2018-05-06T19:20:21Z",
|
||||
}
|
||||
tests := map[string]struct {
|
||||
repo api.RepoName
|
||||
first, second, third api.CommitID
|
||||
}{
|
||||
"git cmd": {
|
||||
repo: MakeGitRepository(t, gitCommands...),
|
||||
first: "b6602ca96bdc0ab647278577a3c6edcb8fe18fb0",
|
||||
second: "c5151eceb40d5e625716589b745248e1a6c6228d",
|
||||
third: "ba3c51080ed4a5b870952ecd7f0e15f255b24cca",
|
||||
},
|
||||
}
|
||||
|
||||
source := gitserver.NewTestClientSource(t, GitserverAddresses)
|
||||
client := gitserver.NewTestClient(t).WithClientSource(source)
|
||||
for label, test := range tests {
|
||||
// notafile should not exist.
|
||||
if _, err := client.Stat(ctx, test.repo, test.first, "notafile"); !os.IsNotExist(err) {
|
||||
t.Errorf("%s: fs1.Stat(notafile): got err %v, want os.IsNotExist", label, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// dir1 should exist and be a dir.
|
||||
dir1Info, err := client.Stat(ctx, test.repo, test.first, "dir1")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs1.Stat(dir1): %s", label, err)
|
||||
continue
|
||||
}
|
||||
if !dir1Info.Mode().IsDir() {
|
||||
t.Errorf("%s: dir1 stat !IsDir", label)
|
||||
}
|
||||
if name := dir1Info.Name(); name != "dir1" {
|
||||
t.Errorf("%s: got dir1 name %q, want 'dir1'", label, name)
|
||||
}
|
||||
if dir1Info.Size() != 0 {
|
||||
t.Errorf("%s: got dir1 size %d, want 0", label, dir1Info.Size())
|
||||
}
|
||||
if got, want := "ab771ba54f5571c99ffdae54f44acc7993d9f115", dir1Info.Sys().(gitdomain.ObjectInfo).OID().String(); got != want {
|
||||
t.Errorf("%s: got dir1 OID %q, want %q", label, got, want)
|
||||
}
|
||||
source := gitserver.NewTestClientSource(t, GitserverAddresses)
|
||||
client := gitserver.NewTestClient(t).WithClientSource(source)
|
||||
|
||||
// dir1 should contain one entry: file1.
|
||||
dir1Entries, err := client.ReadDir(ctx, test.repo, test.first, "dir1", false)
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs1.ReadDir(dir1): %s", label, err)
|
||||
continue
|
||||
}
|
||||
if len(dir1Entries) != 1 {
|
||||
t.Errorf("%s: got %d dir1 entries, want 1", label, len(dir1Entries))
|
||||
continue
|
||||
}
|
||||
file1Info := dir1Entries[0]
|
||||
if got, want := file1Info.Name(), "dir1/file1"; got != want {
|
||||
t.Errorf("%s: got dir1 entry name == %q, want %q", label, got, want)
|
||||
}
|
||||
if want := int64(7); file1Info.Size() != want {
|
||||
t.Errorf("%s: got dir1 entry size == %d, want %d", label, file1Info.Size(), want)
|
||||
}
|
||||
if got, want := "a20cc2fb45631b1dd262371a058b1bf31702abaa", file1Info.Sys().(gitdomain.ObjectInfo).OID().String(); got != want {
|
||||
t.Errorf("%s: got dir1 entry OID %q, want %q", label, got, want)
|
||||
}
|
||||
|
||||
// dir2 should not exist
|
||||
_, err = client.ReadDir(ctx, test.repo, test.first, "dir2", false)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("%s: fs1.ReadDir(dir2): should not exist: %s", label, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// dir1/file1 should exist, contain "infile1", have the right mtime, and be a file.
|
||||
file1R, err := client.NewFileReader(ctx, test.repo, test.first, "dir1/file1")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs1.ReadFile(dir1/file1): %s", label, err)
|
||||
continue
|
||||
}
|
||||
file1Data, err := io.ReadAll(file1R)
|
||||
file1R.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
if !bytes.Equal(file1Data, []byte("infile1")) {
|
||||
t.Errorf("%s: got file1Data == %q, want %q", label, string(file1Data), "infile1")
|
||||
}
|
||||
file1Info, err = client.Stat(ctx, test.repo, test.first, "dir1/file1")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs1.Stat(dir1/file1): %s", label, err)
|
||||
continue
|
||||
}
|
||||
if !file1Info.Mode().IsRegular() {
|
||||
t.Errorf("%s: file1 stat !IsRegular", label)
|
||||
}
|
||||
if got, want := file1Info.Name(), "dir1/file1"; got != want {
|
||||
t.Errorf("%s: got file1 name %q, want %q", label, got, want)
|
||||
}
|
||||
if want := int64(7); file1Info.Size() != want {
|
||||
t.Errorf("%s: got file1 size == %d, want %d", label, file1Info.Size(), want)
|
||||
}
|
||||
|
||||
// file 2 shouldn't exist in the 1st commit.
|
||||
_, err = client.NewFileReader(ctx, test.repo, test.first, "file 2")
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("%s: fs1.Open(file 2): got err %v, want os.IsNotExist (file 2 should not exist in this commit)", label, err)
|
||||
}
|
||||
|
||||
// file 2 should exist in the 2nd commit.
|
||||
file2R, err := client.NewFileReader(ctx, test.repo, test.second, "file 2")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs2.Open(file 2): %s", label, err)
|
||||
continue
|
||||
}
|
||||
_, err = io.ReadAll(file2R)
|
||||
file2R.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// file1 should also exist in the 2nd commit.
|
||||
if _, err := client.Stat(ctx, test.repo, test.second, "dir1/file1"); err != nil {
|
||||
t.Errorf("%s: fs2.Stat(dir1/file1): %s", label, err)
|
||||
continue
|
||||
}
|
||||
file1R, err = client.NewFileReader(ctx, test.repo, test.second, "dir1/file1")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs2.Open(dir1/file1): %s", label, err)
|
||||
continue
|
||||
}
|
||||
_, err = io.ReadAll(file1R)
|
||||
file1R.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// root should exist (via Stat).
|
||||
root, err := client.Stat(ctx, test.repo, test.second, ".")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs2.Stat(.): %s", label, err)
|
||||
continue
|
||||
}
|
||||
if !root.Mode().IsDir() {
|
||||
t.Errorf("%s: got root !IsDir", label)
|
||||
}
|
||||
|
||||
// root should have 2 entries: dir1 and file 2.
|
||||
rootEntries, err := client.ReadDir(ctx, test.repo, test.second, ".", false)
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs2.ReadDir(.): %s", label, err)
|
||||
continue
|
||||
}
|
||||
if got, want := len(rootEntries), 2; got != want {
|
||||
t.Errorf("%s: got len(rootEntries) == %d, want %d", label, got, want)
|
||||
continue
|
||||
}
|
||||
if e0 := rootEntries[0]; !(e0.Name() == "dir1" && e0.Mode().IsDir()) {
|
||||
t.Errorf("%s: got root entry 0 %q IsDir=%v, want 'dir1' IsDir=true", label, e0.Name(), e0.Mode().IsDir())
|
||||
}
|
||||
if e1 := rootEntries[1]; !(e1.Name() == "file 2" && !e1.Mode().IsDir()) {
|
||||
t.Errorf("%s: got root entry 1 %q IsDir=%v, want 'file 2' IsDir=false", label, e1.Name(), e1.Mode().IsDir())
|
||||
}
|
||||
|
||||
// dir1 should still only contain one entry: file1.
|
||||
dir1Entries, err = client.ReadDir(ctx, test.repo, test.second, "dir1", false)
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs1.ReadDir(dir1): %s", label, err)
|
||||
continue
|
||||
}
|
||||
if len(dir1Entries) != 1 {
|
||||
t.Errorf("%s: got %d dir1 entries, want 1", label, len(dir1Entries))
|
||||
continue
|
||||
}
|
||||
if got, want := dir1Entries[0].Name(), "dir1/file1"; got != want {
|
||||
t.Errorf("%s: got dir1 entry name == %q, want %q", label, got, want)
|
||||
}
|
||||
|
||||
// rootEntries should be empty for third commit
|
||||
rootEntries, err = client.ReadDir(ctx, test.repo, test.third, ".", false)
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs3.ReadDir(.): %s", label, err)
|
||||
continue
|
||||
}
|
||||
if got, want := len(rootEntries), 0; got != want {
|
||||
t.Errorf("%s: got len(rootEntries) == %d, want %d", label, got, want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_FileSystem_quoteChars(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
// The repo contains 3 files: one whose filename includes a
|
||||
// non-ASCII char, one whose filename contains a double quote, and
|
||||
// one whose filename contains a backslash. These should be parsed
|
||||
// and unquoted properly.
|
||||
//
|
||||
// Filenames with double quotes are always quoted in some versions
|
||||
// of git, so we might encounter quoted paths even if
|
||||
// core.quotepath is off. We test twice, with it both on AND
|
||||
// off. (Note: Although
|
||||
// https://www.kernel.org/pub/software/scm/git/docs/git-config.html
|
||||
// says that double quotes, backslashes, and single quotes are
|
||||
// always quoted, this is not true on all git versions, such as
|
||||
// @sqs's current git version 2.7.0.)
|
||||
wantNames := []string{"⊗.txt", `".txt`, `\.txt`}
|
||||
sort.Strings(wantNames)
|
||||
gitCommands := []string{
|
||||
`touch ⊗.txt '".txt' \\.txt`,
|
||||
`git add ⊗.txt '".txt' \\.txt`,
|
||||
"git commit -m commit1",
|
||||
}
|
||||
tests := map[string]struct {
|
||||
repo api.RepoName
|
||||
}{
|
||||
"git cmd (quotepath=on)": {
|
||||
repo: MakeGitRepository(t, append([]string{"git config core.quotepath on"}, gitCommands...)...),
|
||||
},
|
||||
"git cmd (quotepath=off)": {
|
||||
repo: MakeGitRepository(t, append([]string{"git config core.quotepath off"}, gitCommands...)...),
|
||||
},
|
||||
}
|
||||
|
||||
source := gitserver.NewTestClientSource(t, GitserverAddresses)
|
||||
client := gitserver.NewTestClient(t).WithClientSource(source)
|
||||
for label, test := range tests {
|
||||
commitID, err := client.ResolveRevision(ctx, test.repo, "master", gitserver.ResolveRevisionOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entries, err := client.ReadDir(ctx, test.repo, commitID, ".", false)
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs.ReadDir(.): %s", label, err)
|
||||
continue
|
||||
}
|
||||
names := make([]string, len(entries))
|
||||
for i, e := range entries {
|
||||
names[i] = e.Name()
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
if !reflect.DeepEqual(names, wantNames) {
|
||||
t.Errorf("%s: got names %v, want %v", label, names, wantNames)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, name := range wantNames {
|
||||
stat, err := client.Stat(ctx, test.repo, commitID, name)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Stat(%q): %s", label, name, err)
|
||||
continue
|
||||
}
|
||||
if stat.Name() != name {
|
||||
t.Errorf("%s: got Name == %q, want %q", label, stat.Name(), name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_FileSystem_gitSubmodules(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
submodDir := InitGitRepository(t,
|
||||
"touch f",
|
||||
"git add f",
|
||||
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit1 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
|
||||
)
|
||||
const submodCommit = "94aa9078934ce2776ccbb589569eca5ef575f12e"
|
||||
|
||||
gitCommands := []string{
|
||||
"git -c protocol.file.allow=always submodule add " + filepath.ToSlash(submodDir) + " submod",
|
||||
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m 'add submodule' --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
|
||||
}
|
||||
tests := map[string]struct {
|
||||
repo api.RepoName
|
||||
}{
|
||||
"git cmd": {
|
||||
repo: MakeGitRepository(t, gitCommands...),
|
||||
},
|
||||
}
|
||||
|
||||
source := gitserver.NewTestClientSource(t, GitserverAddresses)
|
||||
client := gitserver.NewTestClient(t).WithClientSource(source)
|
||||
for label, test := range tests {
|
||||
commitID, err := client.ResolveRevision(ctx, test.repo, "master", gitserver.ResolveRevisionOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkSubmoduleFileInfo := func(label string, submod fs.FileInfo) {
|
||||
if want := "submod"; submod.Name() != want {
|
||||
t.Errorf("%s: submod.Name(): got %q, want %q", label, submod.Name(), want)
|
||||
}
|
||||
// A submodule should have a special file mode and should
|
||||
// store information about its origin.
|
||||
if submod.Mode().IsRegular() {
|
||||
t.Errorf("%s: IsRegular", label)
|
||||
}
|
||||
if submod.Mode().IsDir() {
|
||||
t.Errorf("%s: IsDir", label)
|
||||
}
|
||||
if mode := submod.Mode(); mode&gitdomain.ModeSubmodule == 0 {
|
||||
t.Errorf("%s: submod.Mode(): got %o, want & ModeSubmodule (%o) != 0", label, mode, gitdomain.ModeSubmodule)
|
||||
}
|
||||
si, ok := submod.Sys().(gitdomain.Submodule)
|
||||
if !ok {
|
||||
t.Errorf("%s: submod.Sys(): got %v, want Submodule", label, si)
|
||||
}
|
||||
if want := filepath.ToSlash(submodDir); si.URL != want {
|
||||
t.Errorf("%s: (Submodule).URL: got %q, want %q", label, si.URL, want)
|
||||
}
|
||||
if si.CommitID != submodCommit {
|
||||
t.Errorf("%s: (Submodule).CommitID: got %q, want %q", label, si.CommitID, submodCommit)
|
||||
}
|
||||
}
|
||||
|
||||
// Check the submodule fs.FileInfo both when it's returned by
|
||||
// Stat and when it's returned in a list by ReadDir.
|
||||
submod, err := client.Stat(ctx, test.repo, commitID, "submod")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs.Stat(submod): %s", label, err)
|
||||
continue
|
||||
}
|
||||
checkSubmoduleFileInfo(label+" (Stat)", submod)
|
||||
entries, err := client.ReadDir(ctx, test.repo, commitID, ".", false)
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs.ReadDir(.): %s", label, err)
|
||||
continue
|
||||
}
|
||||
// .gitmodules file is entries[0]
|
||||
checkSubmoduleFileInfo(label+" (ReadDir)", entries[1])
|
||||
|
||||
r, err := client.NewFileReader(ctx, test.repo, commitID, "submod")
|
||||
if err != nil {
|
||||
t.Errorf("%s: fs.Open(submod): %s", label, err)
|
||||
continue
|
||||
}
|
||||
_, err = io.ReadAll(r)
|
||||
r.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDir_SubRepoFiltering(t *testing.T) {
|
||||
ctx := actor.WithActor(context.Background(), &actor.Actor{
|
||||
UID: 1,
|
||||
|
||||
@ -1206,7 +1206,7 @@ func (gs *grpcServer) ListRefs(req *proto.ListRefsRequest, ss proto.GitserverSer
|
||||
// We use a chunker here to make sure we don't send too large gRPC messages.
|
||||
// For repos with thousands or even millions of refs, sending them all in one
|
||||
// message would be very slow, but sending them all in individual messages
|
||||
// would be slow either, so we chunk them instead.
|
||||
// would also be slow, so we chunk them instead.
|
||||
chunker := chunk.New(sendFunc)
|
||||
|
||||
for {
|
||||
@ -1547,6 +1547,172 @@ func (gs *grpcServer) ChangedFiles(req *proto.ChangedFilesRequest, ss proto.Gits
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gs *grpcServer) Stat(ctx context.Context, req *proto.StatRequest) (*proto.StatResponse, error) {
|
||||
accesslog.Record(
|
||||
ctx,
|
||||
req.GetRepoName(),
|
||||
log.String("commit", req.GetCommitSha()),
|
||||
log.String("path", string(req.GetPath())),
|
||||
)
|
||||
|
||||
if req.GetRepoName() == "" {
|
||||
return nil, status.New(codes.InvalidArgument, "repo must be specified").Err()
|
||||
}
|
||||
|
||||
if req.GetCommitSha() == "" {
|
||||
return nil, status.New(codes.InvalidArgument, "commit_sha must be specified").Err()
|
||||
}
|
||||
|
||||
repoName := api.RepoName(req.GetRepoName())
|
||||
repoDir := gs.fs.RepoDir(repoName)
|
||||
|
||||
if err := gs.checkRepoExists(ctx, repoName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backend := gs.getBackendFunc(repoDir, repoName)
|
||||
|
||||
fi, err := backend.Stat(ctx, api.CommitID(req.GetCommitSha()), string(req.GetPath()))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
var path string
|
||||
var pathError *os.PathError
|
||||
if errors.As(err, &pathError) {
|
||||
path = pathError.Path
|
||||
}
|
||||
s, err := status.New(codes.NotFound, "file not found").WithDetails(&proto.FileNotFoundPayload{
|
||||
Repo: string(repoName),
|
||||
Commit: string(req.GetCommitSha()),
|
||||
Path: path,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, s.Err()
|
||||
}
|
||||
|
||||
var e *gitdomain.RevisionNotFoundError
|
||||
if errors.As(err, &e) {
|
||||
s, err := status.New(codes.NotFound, "revision not found").WithDetails(&proto.RevisionNotFoundPayload{
|
||||
Repo: req.GetRepoName(),
|
||||
Spec: e.Spec,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, s.Err()
|
||||
}
|
||||
|
||||
gs.svc.LogIfCorrupt(ctx, repoName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proto.StatResponse{
|
||||
FileInfo: gitdomain.FSFileInfoToProto(fi),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gs *grpcServer) ReadDir(req *proto.ReadDirRequest, ss proto.GitserverService_ReadDirServer) (err error) {
|
||||
ctx := ss.Context()
|
||||
|
||||
accesslog.Record(
|
||||
ctx,
|
||||
req.GetRepoName(),
|
||||
log.String("commit", req.GetCommitSha()),
|
||||
log.String("path", string(req.GetPath())),
|
||||
)
|
||||
|
||||
if req.GetRepoName() == "" {
|
||||
return status.New(codes.InvalidArgument, "repo must be specified").Err()
|
||||
}
|
||||
|
||||
if len(req.GetCommitSha()) == 0 {
|
||||
return status.New(codes.InvalidArgument, "commit_sha must be specified").Err()
|
||||
}
|
||||
|
||||
repoName := api.RepoName(req.GetRepoName())
|
||||
repoDir := gs.fs.RepoDir(repoName)
|
||||
|
||||
if err := gs.checkRepoExists(ctx, repoName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend := gs.getBackendFunc(repoDir, repoName)
|
||||
|
||||
it, err := backend.ReadDir(ctx, api.CommitID(req.GetCommitSha()), string(req.GetPath()), req.GetRecursive())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
s, err := status.New(codes.NotFound, "file not found").WithDetails(&proto.FileNotFoundPayload{
|
||||
Repo: req.GetRepoName(),
|
||||
Commit: string(req.GetCommitSha()),
|
||||
Path: string(req.GetPath()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
var e *gitdomain.RevisionNotFoundError
|
||||
if errors.As(err, &e) {
|
||||
s, err := status.New(codes.NotFound, "revision not found").WithDetails(&proto.RevisionNotFoundPayload{
|
||||
Repo: req.GetRepoName(),
|
||||
Spec: e.Spec,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
gs.svc.LogIfCorrupt(ctx, repoName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
closeErr := it.Close()
|
||||
if closeErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = errors.Append(err, closeErr)
|
||||
return
|
||||
}
|
||||
|
||||
err = closeErr
|
||||
}()
|
||||
|
||||
sendFunc := func(fis []*proto.FileInfo) error {
|
||||
return ss.Send(&proto.ReadDirResponse{FileInfo: fis})
|
||||
}
|
||||
|
||||
// We use a chunker here to make sure we don't send too large gRPC messages.
|
||||
// For repos with thousands or even millions of files, sending them all in one
|
||||
// message would be very slow, but sending them all in individual messages
|
||||
// would also be slow, so we chunk them instead.
|
||||
chunker := chunk.New(sendFunc)
|
||||
|
||||
for {
|
||||
fi, err := it.Next()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
err = chunker.Send(gitdomain.FSFileInfoToProto(fi))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to send file chunk")
|
||||
}
|
||||
}
|
||||
|
||||
err = chunker.Flush()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to flush files")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRepoExists checks if a given repository is cloned on disk, and returns an
|
||||
// error otherwise.
|
||||
// On Sourcegraph.com, not all repos are managed by the scheduler. We thus
|
||||
|
||||
@ -1033,6 +1033,62 @@ func changedFilesRequestToLogFields(req *proto.ChangedFilesRequest) []log.Field
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loggingGRPCServer) Stat(ctx context.Context, request *proto.StatRequest) (resp *proto.StatResponse, err error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
elapsed := time.Since(start)
|
||||
|
||||
doLog(
|
||||
l.logger,
|
||||
proto.GitserverService_Stat_FullMethodName,
|
||||
status.Code(err),
|
||||
trace.Context(ctx).TraceID,
|
||||
elapsed,
|
||||
|
||||
statRequestToLogFields(request)...,
|
||||
)
|
||||
}()
|
||||
|
||||
return l.base.Stat(ctx, request)
|
||||
}
|
||||
|
||||
func statRequestToLogFields(req *proto.StatRequest) []log.Field {
|
||||
return []log.Field{
|
||||
log.String("repoName", req.GetRepoName()),
|
||||
log.String("commit", string(req.GetCommitSha())),
|
||||
log.String("path", string(req.GetPath())),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loggingGRPCServer) ReadDir(request *proto.ReadDirRequest, server proto.GitserverService_ReadDirServer) error {
|
||||
start := time.Now()
|
||||
|
||||
defer func() {
|
||||
elapsed := time.Since(start)
|
||||
|
||||
doLog(
|
||||
l.logger,
|
||||
proto.GitserverService_ReadDir_FullMethodName,
|
||||
status.Code(server.Context().Err()),
|
||||
trace.Context(server.Context()).TraceID,
|
||||
elapsed,
|
||||
|
||||
readDirRequestToLogFields(request)...,
|
||||
)
|
||||
}()
|
||||
|
||||
return l.base.ReadDir(request, server)
|
||||
}
|
||||
|
||||
func readDirRequestToLogFields(req *proto.ReadDirRequest) []log.Field {
|
||||
return []log.Field{
|
||||
log.String("repoName", req.GetRepoName()),
|
||||
log.String("commit", string(req.GetCommitSha())),
|
||||
log.String("path", string(req.GetPath())),
|
||||
log.Bool("recursive", req.GetRecursive()),
|
||||
}
|
||||
}
|
||||
|
||||
type loggingRepositoryServiceServer struct {
|
||||
base proto.GitserverRepositoryServiceServer
|
||||
logger log.Logger
|
||||
|
||||
@ -29,6 +29,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/gitserverfs"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
@ -1248,6 +1249,200 @@ func TestGRPCServer_BehindAhead(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestGRPCServer_Stat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("argument validation", func(t *testing.T) {
|
||||
gs := &grpcServer{}
|
||||
_, err := gs.Stat(ctx, &proto.StatRequest{RepoName: ""})
|
||||
require.ErrorContains(t, err, "repo must be specified")
|
||||
assertGRPCStatusCode(t, err, codes.InvalidArgument)
|
||||
|
||||
_, err = gs.Stat(ctx, &proto.StatRequest{RepoName: "repo"})
|
||||
require.ErrorContains(t, err, "commit_sha must be specified")
|
||||
assertGRPCStatusCode(t, err, codes.InvalidArgument)
|
||||
})
|
||||
|
||||
t.Run("checks for uncloned repo", func(t *testing.T) {
|
||||
fs := gitserverfs.NewMockFS()
|
||||
fs.RepoClonedFunc.SetDefaultReturn(false, nil)
|
||||
locker := NewMockRepositoryLocker()
|
||||
locker.StatusFunc.SetDefaultReturn("cloning", true)
|
||||
gs := &grpcServer{svc: NewMockService(), fs: fs, locker: locker}
|
||||
_, err := gs.Stat(ctx, &proto.StatRequest{RepoName: "therepo", CommitSha: "HEAD"})
|
||||
require.Error(t, err)
|
||||
assertGRPCStatusCode(t, err, codes.NotFound)
|
||||
assertHasGRPCErrorDetailOfType(t, err, &proto.RepoNotFoundPayload{})
|
||||
require.Contains(t, err.Error(), "repo not found")
|
||||
mockassert.Called(t, fs.RepoClonedFunc)
|
||||
mockassert.Called(t, locker.StatusFunc)
|
||||
})
|
||||
|
||||
t.Run("revision not found", func(t *testing.T) {
|
||||
fs := gitserverfs.NewMockFS()
|
||||
// Repo is cloned, proceed!
|
||||
fs.RepoClonedFunc.SetDefaultReturn(true, nil)
|
||||
gs := &grpcServer{
|
||||
svc: NewMockService(),
|
||||
fs: fs,
|
||||
getBackendFunc: func(common.GitDir, api.RepoName) git.GitBackend {
|
||||
b := git.NewMockGitBackend()
|
||||
b.StatFunc.SetDefaultReturn(nil, &gitdomain.RevisionNotFoundError{Repo: "therepo", Spec: "base...head"})
|
||||
return b
|
||||
},
|
||||
}
|
||||
_, err := gs.Stat(ctx, &proto.StatRequest{RepoName: "therepo", CommitSha: "HEAD"})
|
||||
require.Error(t, err)
|
||||
assertGRPCStatusCode(t, err, codes.NotFound)
|
||||
assertHasGRPCErrorDetailOfType(t, err, &proto.RevisionNotFoundPayload{})
|
||||
require.Contains(t, err.Error(), "revision not found")
|
||||
})
|
||||
|
||||
t.Run("e2e", func(t *testing.T) {
|
||||
expectedStat := fileutil.FileInfo{Name_: "file"}
|
||||
fs := gitserverfs.NewMockFS()
|
||||
// Repo is cloned, proceed!
|
||||
fs.RepoClonedFunc.SetDefaultReturn(true, nil)
|
||||
b := git.NewMockGitBackend()
|
||||
b.StatFunc.SetDefaultReturn(&expectedStat, nil)
|
||||
gs := &grpcServer{
|
||||
svc: NewMockService(),
|
||||
fs: fs,
|
||||
getBackendFunc: func(common.GitDir, api.RepoName) git.GitBackend {
|
||||
return b
|
||||
},
|
||||
}
|
||||
|
||||
cli := spawnServer(t, gs)
|
||||
response, err := cli.Stat(ctx, &proto.StatRequest{
|
||||
RepoName: "therepo",
|
||||
CommitSha: "HEAD",
|
||||
Path: []byte("file"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(&proto.StatResponse{
|
||||
FileInfo: gitdomain.FSFileInfoToProto(&expectedStat),
|
||||
}, response, cmpopts.IgnoreUnexported(proto.StatResponse{}, proto.FileInfo{})); diff != "" {
|
||||
t.Fatalf("unexpected response (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
b.StatFunc.SetDefaultReturn(nil, &gitdomain.RevisionNotFoundError{})
|
||||
_, err = cli.Stat(context.Background(), &v1.StatRequest{
|
||||
RepoName: "therepo",
|
||||
CommitSha: "HEAD",
|
||||
Path: []byte("file"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
assertGRPCStatusCode(t, err, codes.NotFound)
|
||||
assertHasGRPCErrorDetailOfType(t, err, &proto.RevisionNotFoundPayload{})
|
||||
|
||||
b.StatFunc.SetDefaultReturn(nil, os.ErrNotExist)
|
||||
_, err = cli.Stat(context.Background(), &v1.StatRequest{
|
||||
RepoName: "therepo",
|
||||
CommitSha: "HEAD",
|
||||
Path: []byte("file"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
assertGRPCStatusCode(t, err, codes.NotFound)
|
||||
assertHasGRPCErrorDetailOfType(t, err, &proto.FileNotFoundPayload{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestGRPCServer_ReadDir(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockSS := gitserver.NewMockGitserverService_ReadDirServer()
|
||||
mockSS.ContextFunc.SetDefaultReturn(ctx)
|
||||
t.Run("argument validation", func(t *testing.T) {
|
||||
gs := &grpcServer{}
|
||||
err := gs.ReadDir(&v1.ReadDirRequest{RepoName: ""}, mockSS)
|
||||
require.ErrorContains(t, err, "repo must be specified")
|
||||
assertGRPCStatusCode(t, err, codes.InvalidArgument)
|
||||
|
||||
err = gs.ReadDir(&v1.ReadDirRequest{RepoName: "repo"}, mockSS)
|
||||
require.ErrorContains(t, err, "commit_sha must be specified")
|
||||
assertGRPCStatusCode(t, err, codes.InvalidArgument)
|
||||
})
|
||||
t.Run("checks for uncloned repo", func(t *testing.T) {
|
||||
fs := gitserverfs.NewMockFS()
|
||||
fs.RepoClonedFunc.SetDefaultReturn(false, nil)
|
||||
locker := NewMockRepositoryLocker()
|
||||
locker.StatusFunc.SetDefaultReturn("cloning", true)
|
||||
gs := &grpcServer{svc: NewMockService(), fs: fs, locker: locker}
|
||||
err := gs.ReadDir(&v1.ReadDirRequest{RepoName: "therepo", CommitSha: "HEAD"}, mockSS)
|
||||
require.Error(t, err)
|
||||
assertGRPCStatusCode(t, err, codes.NotFound)
|
||||
assertHasGRPCErrorDetailOfType(t, err, &proto.RepoNotFoundPayload{})
|
||||
require.Contains(t, err.Error(), "repo not found")
|
||||
mockassert.Called(t, fs.RepoClonedFunc)
|
||||
mockassert.Called(t, locker.StatusFunc)
|
||||
})
|
||||
t.Run("e2e", func(t *testing.T) {
|
||||
fs := gitserverfs.NewMockFS()
|
||||
// Repo is cloned, proceed!
|
||||
fs.RepoClonedFunc.SetDefaultReturn(true, nil)
|
||||
b := git.NewMockGitBackend()
|
||||
it := git.NewMockReadDirIterator()
|
||||
it.NextFunc.PushReturn(&fileutil.FileInfo{Name_: "file"}, nil)
|
||||
it.NextFunc.PushReturn(&fileutil.FileInfo{Name_: "dir/file"}, nil)
|
||||
it.NextFunc.PushReturn(nil, io.EOF)
|
||||
b.ReadDirFunc.SetDefaultReturn(it, nil)
|
||||
gs := &grpcServer{
|
||||
svc: NewMockService(),
|
||||
fs: fs,
|
||||
getBackendFunc: func(common.GitDir, api.RepoName) git.GitBackend {
|
||||
return b
|
||||
},
|
||||
}
|
||||
|
||||
cli := spawnServer(t, gs)
|
||||
cc, err := cli.ReadDir(ctx, &v1.ReadDirRequest{
|
||||
RepoName: "therepo",
|
||||
CommitSha: "HEAD",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
fis := []*v1.FileInfo{}
|
||||
for {
|
||||
resp, err := cc.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
fis = append(fis, resp.GetFileInfo()...)
|
||||
}
|
||||
if diff := cmp.Diff([]*v1.FileInfo{
|
||||
{
|
||||
Name: []byte("file"),
|
||||
},
|
||||
{
|
||||
Name: []byte("dir/file"),
|
||||
},
|
||||
}, fis, cmpopts.IgnoreUnexported(v1.FileInfo{})); diff != "" {
|
||||
t.Fatalf("unexpected response (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
b.ReadDirFunc.SetDefaultReturn(nil, &gitdomain.RevisionNotFoundError{})
|
||||
cc, err = cli.ReadDir(context.Background(), &v1.ReadDirRequest{
|
||||
RepoName: "therepo",
|
||||
CommitSha: "HEAD",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = cc.Recv()
|
||||
assertGRPCStatusCode(t, err, codes.NotFound)
|
||||
assertHasGRPCErrorDetailOfType(t, err, &proto.RevisionNotFoundPayload{})
|
||||
|
||||
b.ReadDirFunc.SetDefaultReturn(nil, os.ErrNotExist)
|
||||
cc, err = cli.ReadDir(context.Background(), &v1.ReadDirRequest{
|
||||
RepoName: "therepo",
|
||||
CommitSha: "HEAD",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = cc.Recv()
|
||||
assertGRPCStatusCode(t, err, codes.NotFound)
|
||||
assertHasGRPCErrorDetailOfType(t, err, &proto.FileNotFoundPayload{})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func assertGRPCStatusCode(t *testing.T, err error, want codes.Code) {
|
||||
t.Helper()
|
||||
|
||||
2
go.mod
2
go.mod
@ -111,7 +111,7 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/gomodule/oauth1 v0.2.0
|
||||
github.com/gomodule/redigo v2.0.0+incompatible
|
||||
github.com/google/go-cmp v0.6.0
|
||||
|
||||
271
internal/batches/sources/mocks_test.go
generated
271
internal/batches/sources/mocks_test.go
generated
@ -11037,9 +11037,6 @@ type MockGitserverClient struct {
|
||||
// IsRepoCloneableFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method IsRepoCloneable.
|
||||
IsRepoCloneableFunc *GitserverClientIsRepoCloneableFunc
|
||||
// ListDirectoryChildrenFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method ListDirectoryChildren.
|
||||
ListDirectoryChildrenFunc *GitserverClientListDirectoryChildrenFunc
|
||||
// ListGitoliteReposFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method ListGitoliteRepos.
|
||||
ListGitoliteReposFunc *GitserverClientListGitoliteReposFunc
|
||||
@ -11049,9 +11046,6 @@ type MockGitserverClient struct {
|
||||
// LogReverseEachFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method LogReverseEach.
|
||||
LogReverseEachFunc *GitserverClientLogReverseEachFunc
|
||||
// LsFilesFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method LsFiles.
|
||||
LsFilesFunc *GitserverClientLsFilesFunc
|
||||
// MergeBaseFunc is an instance of a mock function object controlling
|
||||
// the behavior of the method MergeBase.
|
||||
MergeBaseFunc *GitserverClientMergeBaseFunc
|
||||
@ -11204,11 +11198,6 @@ func NewMockGitserverClient() *MockGitserverClient {
|
||||
return
|
||||
},
|
||||
},
|
||||
ListDirectoryChildrenFunc: &GitserverClientListDirectoryChildrenFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, []string) (r0 map[string][]string, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
ListGitoliteReposFunc: &GitserverClientListGitoliteReposFunc{
|
||||
defaultHook: func(context.Context, string) (r0 []*gitolite.Repo, r1 error) {
|
||||
return
|
||||
@ -11224,11 +11213,6 @@ func NewMockGitserverClient() *MockGitserverClient {
|
||||
return
|
||||
},
|
||||
},
|
||||
LsFilesFunc: &GitserverClientLsFilesFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) (r0 []string, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
MergeBaseFunc: &GitserverClientMergeBaseFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, string, string) (r0 api.CommitID, r1 error) {
|
||||
return
|
||||
@ -11416,11 +11400,6 @@ func NewStrictMockGitserverClient() *MockGitserverClient {
|
||||
panic("unexpected invocation of MockGitserverClient.IsRepoCloneable")
|
||||
},
|
||||
},
|
||||
ListDirectoryChildrenFunc: &GitserverClientListDirectoryChildrenFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error) {
|
||||
panic("unexpected invocation of MockGitserverClient.ListDirectoryChildren")
|
||||
},
|
||||
},
|
||||
ListGitoliteReposFunc: &GitserverClientListGitoliteReposFunc{
|
||||
defaultHook: func(context.Context, string) ([]*gitolite.Repo, error) {
|
||||
panic("unexpected invocation of MockGitserverClient.ListGitoliteRepos")
|
||||
@ -11436,11 +11415,6 @@ func NewStrictMockGitserverClient() *MockGitserverClient {
|
||||
panic("unexpected invocation of MockGitserverClient.LogReverseEach")
|
||||
},
|
||||
},
|
||||
LsFilesFunc: &GitserverClientLsFilesFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error) {
|
||||
panic("unexpected invocation of MockGitserverClient.LsFiles")
|
||||
},
|
||||
},
|
||||
MergeBaseFunc: &GitserverClientMergeBaseFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, string, string) (api.CommitID, error) {
|
||||
panic("unexpected invocation of MockGitserverClient.MergeBase")
|
||||
@ -11591,9 +11565,6 @@ func NewMockGitserverClientFrom(i gitserver.Client) *MockGitserverClient {
|
||||
IsRepoCloneableFunc: &GitserverClientIsRepoCloneableFunc{
|
||||
defaultHook: i.IsRepoCloneable,
|
||||
},
|
||||
ListDirectoryChildrenFunc: &GitserverClientListDirectoryChildrenFunc{
|
||||
defaultHook: i.ListDirectoryChildren,
|
||||
},
|
||||
ListGitoliteReposFunc: &GitserverClientListGitoliteReposFunc{
|
||||
defaultHook: i.ListGitoliteRepos,
|
||||
},
|
||||
@ -11603,9 +11574,6 @@ func NewMockGitserverClientFrom(i gitserver.Client) *MockGitserverClient {
|
||||
LogReverseEachFunc: &GitserverClientLogReverseEachFunc{
|
||||
defaultHook: i.LogReverseEach,
|
||||
},
|
||||
LsFilesFunc: &GitserverClientLsFilesFunc{
|
||||
defaultHook: i.LsFiles,
|
||||
},
|
||||
MergeBaseFunc: &GitserverClientMergeBaseFunc{
|
||||
defaultHook: i.MergeBase,
|
||||
},
|
||||
@ -13787,124 +13755,6 @@ func (c GitserverClientIsRepoCloneableFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// GitserverClientListDirectoryChildrenFunc describes the behavior when the
|
||||
// ListDirectoryChildren method of the parent MockGitserverClient instance
|
||||
// is invoked.
|
||||
type GitserverClientListDirectoryChildrenFunc struct {
|
||||
defaultHook func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error)
|
||||
hooks []func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error)
|
||||
history []GitserverClientListDirectoryChildrenFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// ListDirectoryChildren delegates to the next hook function in the queue
|
||||
// and stores the parameter and result values of this invocation.
|
||||
func (m *MockGitserverClient) ListDirectoryChildren(v0 context.Context, v1 api.RepoName, v2 api.CommitID, v3 []string) (map[string][]string, error) {
|
||||
r0, r1 := m.ListDirectoryChildrenFunc.nextHook()(v0, v1, v2, v3)
|
||||
m.ListDirectoryChildrenFunc.appendCall(GitserverClientListDirectoryChildrenFuncCall{v0, v1, v2, v3, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// ListDirectoryChildren method of the parent MockGitserverClient instance
|
||||
// is invoked and the hook queue is empty.
|
||||
func (f *GitserverClientListDirectoryChildrenFunc) SetDefaultHook(hook func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// ListDirectoryChildren 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 *GitserverClientListDirectoryChildrenFunc) PushHook(hook func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]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 *GitserverClientListDirectoryChildrenFunc) SetDefaultReturn(r0 map[string][]string, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *GitserverClientListDirectoryChildrenFunc) PushReturn(r0 map[string][]string, r1 error) {
|
||||
f.PushHook(func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *GitserverClientListDirectoryChildrenFunc) nextHook() func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]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 *GitserverClientListDirectoryChildrenFunc) appendCall(r0 GitserverClientListDirectoryChildrenFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// GitserverClientListDirectoryChildrenFuncCall objects describing the
|
||||
// invocations of this function.
|
||||
func (f *GitserverClientListDirectoryChildrenFunc) History() []GitserverClientListDirectoryChildrenFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]GitserverClientListDirectoryChildrenFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// GitserverClientListDirectoryChildrenFuncCall is an object that describes
|
||||
// an invocation of method ListDirectoryChildren on an instance of
|
||||
// MockGitserverClient.
|
||||
type GitserverClientListDirectoryChildrenFuncCall 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 api.CommitID
|
||||
// Arg3 is the value of the 4th argument passed to this method
|
||||
// invocation.
|
||||
Arg3 []string
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 map[string][]string
|
||||
// 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 GitserverClientListDirectoryChildrenFuncCall) 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 GitserverClientListDirectoryChildrenFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitserverClientListGitoliteReposFunc describes the behavior when the
|
||||
// ListGitoliteRepos method of the parent MockGitserverClient instance is
|
||||
// invoked.
|
||||
@ -14243,127 +14093,6 @@ func (c GitserverClientLogReverseEachFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// GitserverClientLsFilesFunc describes the behavior when the LsFiles method
|
||||
// of the parent MockGitserverClient instance is invoked.
|
||||
type GitserverClientLsFilesFunc struct {
|
||||
defaultHook func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error)
|
||||
hooks []func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error)
|
||||
history []GitserverClientLsFilesFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// LsFiles delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockGitserverClient) LsFiles(v0 context.Context, v1 api.RepoName, v2 api.CommitID, v3 ...gitdomain.Pathspec) ([]string, error) {
|
||||
r0, r1 := m.LsFilesFunc.nextHook()(v0, v1, v2, v3...)
|
||||
m.LsFilesFunc.appendCall(GitserverClientLsFilesFuncCall{v0, v1, v2, v3, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the LsFiles method of
|
||||
// the parent MockGitserverClient instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *GitserverClientLsFilesFunc) SetDefaultHook(hook func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// LsFiles 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 *GitserverClientLsFilesFunc) PushHook(hook func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]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 *GitserverClientLsFilesFunc) SetDefaultReturn(r0 []string, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *GitserverClientLsFilesFunc) PushReturn(r0 []string, r1 error) {
|
||||
f.PushHook(func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *GitserverClientLsFilesFunc) nextHook() func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]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 *GitserverClientLsFilesFunc) appendCall(r0 GitserverClientLsFilesFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of GitserverClientLsFilesFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *GitserverClientLsFilesFunc) History() []GitserverClientLsFilesFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]GitserverClientLsFilesFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// GitserverClientLsFilesFuncCall is an object that describes an invocation
|
||||
// of method LsFiles on an instance of MockGitserverClient.
|
||||
type GitserverClientLsFilesFuncCall 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 api.CommitID
|
||||
// Arg3 is a slice containing the values of the variadic arguments
|
||||
// passed to this method invocation.
|
||||
Arg3 []gitdomain.Pathspec
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []string
|
||||
// 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. The variadic slice argument is flattened in this array such
|
||||
// that one positional argument and three variadic arguments would result in
|
||||
// a slice of four, not two.
|
||||
func (c GitserverClientLsFilesFuncCall) Args() []interface{} {
|
||||
trailing := []interface{}{}
|
||||
for _, val := range c.Arg3 {
|
||||
trailing = append(trailing, val)
|
||||
}
|
||||
|
||||
return append([]interface{}{c.Arg0, c.Arg1, c.Arg2}, trailing...)
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c GitserverClientLsFilesFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitserverClientMergeBaseFunc describes the behavior when the MergeBase
|
||||
// method of the parent MockGitserverClient instance is invoked.
|
||||
type GitserverClientMergeBaseFunc struct {
|
||||
|
||||
@ -17,7 +17,6 @@ go_library(
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api",
|
||||
"//internal/authz",
|
||||
"//internal/codeintel/autoindexing/internal/inference/libs",
|
||||
"//internal/codeintel/autoindexing/internal/inference/lua",
|
||||
"//internal/codeintel/autoindexing/internal/inference/luatypes",
|
||||
@ -65,11 +64,10 @@ go_test(
|
||||
deps = [
|
||||
"//internal/api",
|
||||
"//internal/codeintel/dependencies",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/gitdomain",
|
||||
"//internal/luasandbox",
|
||||
"//internal/observation",
|
||||
"//internal/paths",
|
||||
"//internal/ratelimit",
|
||||
"//internal/unpack/unpacktest",
|
||||
"//lib/codeintel/autoindex/config",
|
||||
|
||||
@ -3,11 +3,10 @@ package inference
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/luasandbox"
|
||||
)
|
||||
|
||||
@ -16,31 +15,24 @@ type SandboxService interface {
|
||||
}
|
||||
|
||||
type GitService interface {
|
||||
LsFiles(ctx context.Context, repo api.RepoName, commit string, pathspecs ...gitdomain.Pathspec) ([]string, error)
|
||||
ReadDir(ctx context.Context, repo api.RepoName, commit api.CommitID, path string, recurse bool) ([]fs.FileInfo, error)
|
||||
Archive(ctx context.Context, repo api.RepoName, opts gitserver.ArchiveOptions) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
type gitService struct {
|
||||
checker authz.SubRepoPermissionChecker
|
||||
client gitserver.Client
|
||||
client gitserver.Client
|
||||
}
|
||||
|
||||
func NewDefaultGitService(checker authz.SubRepoPermissionChecker) GitService {
|
||||
if checker == nil {
|
||||
checker = authz.DefaultSubRepoPermsChecker
|
||||
}
|
||||
|
||||
func NewDefaultGitService() GitService {
|
||||
return &gitService{
|
||||
checker: checker,
|
||||
client: gitserver.NewClient("codeintel.inference"),
|
||||
client: gitserver.NewClient("codeintel.inference"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *gitService) LsFiles(ctx context.Context, repo api.RepoName, commit string, pathspecs ...gitdomain.Pathspec) ([]string, error) {
|
||||
return s.client.LsFiles(ctx, repo, api.CommitID(commit), pathspecs...)
|
||||
func (s *gitService) ReadDir(ctx context.Context, repo api.RepoName, commit api.CommitID, path string, recurse bool) ([]fs.FileInfo, error) {
|
||||
return s.client.ReadDir(ctx, repo, commit, path, recurse)
|
||||
}
|
||||
|
||||
func (s *gitService) Archive(ctx context.Context, repo api.RepoName, opts gitserver.ArchiveOptions) (io.ReadCloser, error) {
|
||||
// Note: the sub-repo perms checker is nil here because all paths were already checked via a previous call to s.ListFiles
|
||||
return s.client.ArchiveReader(ctx, repo, opts)
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ func NewService(db database.DB) *Service {
|
||||
return newService(
|
||||
observationCtx,
|
||||
luasandbox.NewService(),
|
||||
NewDefaultGitService(nil),
|
||||
NewDefaultGitService(),
|
||||
ratelimit.NewInstrumentedLimiter("InferenceService", rate.NewLimiter(rate.Limit(gitserverRequestRateLimit), 1)),
|
||||
maximumFilesWithContentCount,
|
||||
maximumFileWithContentSizeBytes,
|
||||
|
||||
@ -9,11 +9,11 @@ package inference
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sync"
|
||||
|
||||
api "github.com/sourcegraph/sourcegraph/internal/api"
|
||||
gitserver "github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
gitdomain "github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
luasandbox "github.com/sourcegraph/sourcegraph/internal/luasandbox"
|
||||
)
|
||||
|
||||
@ -25,9 +25,9 @@ type MockGitService struct {
|
||||
// ArchiveFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Archive.
|
||||
ArchiveFunc *GitServiceArchiveFunc
|
||||
// LsFilesFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method LsFiles.
|
||||
LsFilesFunc *GitServiceLsFilesFunc
|
||||
// ReadDirFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method ReadDir.
|
||||
ReadDirFunc *GitServiceReadDirFunc
|
||||
}
|
||||
|
||||
// NewMockGitService creates a new mock of the GitService interface. All
|
||||
@ -39,8 +39,8 @@ func NewMockGitService() *MockGitService {
|
||||
return
|
||||
},
|
||||
},
|
||||
LsFilesFunc: &GitServiceLsFilesFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) (r0 []string, r1 error) {
|
||||
ReadDirFunc: &GitServiceReadDirFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, string, bool) (r0 []fs.FileInfo, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
@ -56,9 +56,9 @@ func NewStrictMockGitService() *MockGitService {
|
||||
panic("unexpected invocation of MockGitService.Archive")
|
||||
},
|
||||
},
|
||||
LsFilesFunc: &GitServiceLsFilesFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error) {
|
||||
panic("unexpected invocation of MockGitService.LsFiles")
|
||||
ReadDirFunc: &GitServiceReadDirFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error) {
|
||||
panic("unexpected invocation of MockGitService.ReadDir")
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -71,8 +71,8 @@ func NewMockGitServiceFrom(i GitService) *MockGitService {
|
||||
ArchiveFunc: &GitServiceArchiveFunc{
|
||||
defaultHook: i.Archive,
|
||||
},
|
||||
LsFilesFunc: &GitServiceLsFilesFunc{
|
||||
defaultHook: i.LsFiles,
|
||||
ReadDirFunc: &GitServiceReadDirFunc{
|
||||
defaultHook: i.ReadDir,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -188,35 +188,35 @@ func (c GitServiceArchiveFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// GitServiceLsFilesFunc describes the behavior when the LsFiles method of
|
||||
// GitServiceReadDirFunc describes the behavior when the ReadDir method of
|
||||
// the parent MockGitService instance is invoked.
|
||||
type GitServiceLsFilesFunc struct {
|
||||
defaultHook func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error)
|
||||
hooks []func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error)
|
||||
history []GitServiceLsFilesFuncCall
|
||||
type GitServiceReadDirFunc struct {
|
||||
defaultHook func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error)
|
||||
hooks []func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error)
|
||||
history []GitServiceReadDirFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// LsFiles delegates to the next hook function in the queue and stores the
|
||||
// ReadDir delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockGitService) LsFiles(v0 context.Context, v1 api.RepoName, v2 string, v3 ...gitdomain.Pathspec) ([]string, error) {
|
||||
r0, r1 := m.LsFilesFunc.nextHook()(v0, v1, v2, v3...)
|
||||
m.LsFilesFunc.appendCall(GitServiceLsFilesFuncCall{v0, v1, v2, v3, r0, r1})
|
||||
func (m *MockGitService) ReadDir(v0 context.Context, v1 api.RepoName, v2 api.CommitID, v3 string, v4 bool) ([]fs.FileInfo, error) {
|
||||
r0, r1 := m.ReadDirFunc.nextHook()(v0, v1, v2, v3, v4)
|
||||
m.ReadDirFunc.appendCall(GitServiceReadDirFuncCall{v0, v1, v2, v3, v4, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the LsFiles method of
|
||||
// SetDefaultHook sets function that is called when the ReadDir method of
|
||||
// the parent MockGitService instance is invoked and the hook queue is
|
||||
// empty.
|
||||
func (f *GitServiceLsFilesFunc) SetDefaultHook(hook func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error)) {
|
||||
func (f *GitServiceReadDirFunc) SetDefaultHook(hook func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// LsFiles method of the parent MockGitService instance invokes the hook at
|
||||
// ReadDir method of the parent MockGitService 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 *GitServiceLsFilesFunc) PushHook(hook func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error)) {
|
||||
func (f *GitServiceReadDirFunc) PushHook(hook func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
@ -224,20 +224,20 @@ func (f *GitServiceLsFilesFunc) PushHook(hook func(context.Context, api.RepoName
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *GitServiceLsFilesFunc) SetDefaultReturn(r0 []string, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error) {
|
||||
func (f *GitServiceReadDirFunc) SetDefaultReturn(r0 []fs.FileInfo, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *GitServiceLsFilesFunc) PushReturn(r0 []string, r1 error) {
|
||||
f.PushHook(func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error) {
|
||||
func (f *GitServiceReadDirFunc) PushReturn(r0 []fs.FileInfo, r1 error) {
|
||||
f.PushHook(func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *GitServiceLsFilesFunc) nextHook() func(context.Context, api.RepoName, string, ...gitdomain.Pathspec) ([]string, error) {
|
||||
func (f *GitServiceReadDirFunc) nextHook() func(context.Context, api.RepoName, api.CommitID, string, bool) ([]fs.FileInfo, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
@ -250,26 +250,26 @@ func (f *GitServiceLsFilesFunc) nextHook() func(context.Context, api.RepoName, s
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *GitServiceLsFilesFunc) appendCall(r0 GitServiceLsFilesFuncCall) {
|
||||
func (f *GitServiceReadDirFunc) appendCall(r0 GitServiceReadDirFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of GitServiceLsFilesFuncCall objects
|
||||
// History returns a sequence of GitServiceReadDirFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *GitServiceLsFilesFunc) History() []GitServiceLsFilesFuncCall {
|
||||
func (f *GitServiceReadDirFunc) History() []GitServiceReadDirFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]GitServiceLsFilesFuncCall, len(f.history))
|
||||
history := make([]GitServiceReadDirFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// GitServiceLsFilesFuncCall is an object that describes an invocation of
|
||||
// method LsFiles on an instance of MockGitService.
|
||||
type GitServiceLsFilesFuncCall struct {
|
||||
// GitServiceReadDirFuncCall is an object that describes an invocation of
|
||||
// method ReadDir on an instance of MockGitService.
|
||||
type GitServiceReadDirFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
@ -278,34 +278,30 @@ type GitServiceLsFilesFuncCall struct {
|
||||
Arg1 api.RepoName
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 string
|
||||
// Arg3 is a slice containing the values of the variadic arguments
|
||||
// passed to this method invocation.
|
||||
Arg3 []gitdomain.Pathspec
|
||||
Arg2 api.CommitID
|
||||
// Arg3 is the value of the 4th argument passed to this method
|
||||
// invocation.
|
||||
Arg3 string
|
||||
// Arg4 is the value of the 5th argument passed to this method
|
||||
// invocation.
|
||||
Arg4 bool
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []string
|
||||
Result0 []fs.FileInfo
|
||||
// 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. The variadic slice argument is flattened in this array such
|
||||
// that one positional argument and three variadic arguments would result in
|
||||
// a slice of four, not two.
|
||||
func (c GitServiceLsFilesFuncCall) Args() []interface{} {
|
||||
trailing := []interface{}{}
|
||||
for _, val := range c.Arg3 {
|
||||
trailing = append(trailing, val)
|
||||
}
|
||||
|
||||
return append([]interface{}{c.Arg0, c.Arg1, c.Arg2}, trailing...)
|
||||
// invocation.
|
||||
func (c GitServiceReadDirFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3, c.Arg4}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c GitServiceLsFilesFuncCall) Results() []interface{} {
|
||||
func (c GitServiceReadDirFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
|
||||
@ -274,17 +274,29 @@ func (s *Service) resolvePaths(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
globs, pathspecs, err := flattenPatterns(patternsForPaths, false)
|
||||
globs, _, err := flattenPatterns(patternsForPaths, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ideally we can pass the globs we explicitly filter by below
|
||||
paths, err := invocationContext.gitService.LsFiles(ctx, invocationContext.repo, invocationContext.commit, pathspecs...)
|
||||
// Ideally we can pass the pathspecs from flattenPatterns here and avoid returning
|
||||
// all files in the tree, so we don't need to filter as much further down.
|
||||
// This requires implementing either globbing support in the ReadDir call,
|
||||
// or that the pathpatterns are supported by ReadDir (and thus, in git ls-tree)
|
||||
// which is not the case today.
|
||||
fds, err := invocationContext.gitService.ReadDir(ctx, invocationContext.repo, api.CommitID(invocationContext.commit), "", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
paths := make([]string, 0, len(fds))
|
||||
for _, fd := range fds {
|
||||
if fd.IsDir() {
|
||||
continue
|
||||
}
|
||||
paths = append(paths, fd.Name())
|
||||
}
|
||||
|
||||
return filterPaths(paths, globs, nil), nil
|
||||
}
|
||||
|
||||
|
||||
@ -3,17 +3,17 @@ package inference
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/luasandbox"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/internal/paths"
|
||||
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/unpack/unpacktest"
|
||||
)
|
||||
@ -30,18 +30,14 @@ func testService(t *testing.T, repositoryContents map[string]string) *Service {
|
||||
|
||||
// Fake deal
|
||||
gitService := NewMockGitService()
|
||||
gitService.LsFilesFunc.SetDefaultHook(func(ctx context.Context, repo api.RepoName, commit string, pathspecs ...gitdomain.Pathspec) ([]string, error) {
|
||||
var patterns []*paths.GlobPattern
|
||||
for _, spec := range pathspecs {
|
||||
pattern, err := paths.Compile(string(spec))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patterns = append(patterns, pattern)
|
||||
gitService.ReadDirFunc.SetDefaultHook(func(ctx context.Context, _ api.RepoName, _ api.CommitID, path string, recurse bool) ([]fs.FileInfo, error) {
|
||||
var fds []fs.FileInfo
|
||||
for _, repositoryPath := range repositoryPaths {
|
||||
fds = append(fds, &fileutil.FileInfo{
|
||||
Name_: repositoryPath,
|
||||
})
|
||||
}
|
||||
|
||||
return filterPaths(repositoryPaths, patterns, nil), nil
|
||||
return fds, nil
|
||||
})
|
||||
gitService.ArchiveFunc.SetDefaultHook(func(ctx context.Context, repoName api.RepoName, opts gitserver.ArchiveOptions) (io.ReadCloser, error) {
|
||||
files := map[string]string{}
|
||||
|
||||
@ -25,6 +25,8 @@ func filterPathsByPatterns(paths []string, rawPatterns []*luatypes.PathPattern)
|
||||
}
|
||||
|
||||
// flattenPatterns converts a tree of patterns into a flat list of compiled glob and pathspec patterns.
|
||||
//
|
||||
//nolint:unparam // pathspecs aren't used right now but we want to make use of them again to strip down the amount of paths we need to check agains the glob patterns.
|
||||
func flattenPatterns(patterns []*luatypes.PathPattern, inverted bool) ([]*paths.GlobPattern, []gitdomain.Pathspec, error) {
|
||||
var globPatterns []string
|
||||
var pathspecPatterns []string
|
||||
|
||||
@ -63,6 +63,7 @@ go_test(
|
||||
"//internal/database/basestore",
|
||||
"//internal/database/dbmocks",
|
||||
"//internal/executor",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/gitdomain",
|
||||
"//internal/observation",
|
||||
|
||||
@ -6,7 +6,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -218,11 +221,26 @@ func (h *handler) HandleRawUpload(ctx context.Context, logger log.Logger, upload
|
||||
trace.AddEvent("TODO Domain Owner", attribute.Bool("defaultBranch", isDefaultBranch))
|
||||
|
||||
getChildren := func(ctx context.Context, dirnames []string) (map[string][]string, error) {
|
||||
directoryChildren, err := h.gitserverClient.ListDirectoryChildren(ctx, repo.Name, api.CommitID(upload.Commit), dirnames)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gitserverClient.DirectoryChildren")
|
||||
allFDs := make([]fs.FileInfo, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, d := range dirnames {
|
||||
fds, err := h.gitserverClient.ReadDir(ctx, repo.Name, api.CommitID(upload.Commit), d, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gitserverClient.ReadDir")
|
||||
}
|
||||
for _, fd := range fds {
|
||||
if fd.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[fd.Name()]; ok {
|
||||
continue
|
||||
}
|
||||
allFDs = append(allFDs, fd)
|
||||
seen[fd.Name()] = struct{}{}
|
||||
}
|
||||
}
|
||||
return directoryChildren, nil
|
||||
|
||||
return parseDirectoryChildren(dirnames, allFDs), nil
|
||||
}
|
||||
|
||||
return false, withUploadData(ctx, logger, uploadStore, upload.SizeStats(), trace, func(indexReader gzipReadSeeker) (err error) {
|
||||
@ -326,6 +344,45 @@ func (h *handler) HandleRawUpload(ctx context.Context, logger log.Logger, upload
|
||||
})
|
||||
}
|
||||
|
||||
// parseDirectoryChildren converts the flat list of files from git ls-tree into a map. The keys of the
|
||||
// resulting map are the input (unsanitized) dirnames, and the value of that key are the files nested
|
||||
// under that directory. If dirnames contains a directory that encloses another, then the paths will
|
||||
// be placed into the key sharing the longest path prefix.
|
||||
func parseDirectoryChildren(dirnames []string, fis []fs.FileInfo) map[string][]string {
|
||||
childrenMap := map[string][]string{}
|
||||
|
||||
// Ensure each directory has an entry, even if it has no children
|
||||
// listed in the gitserver output.
|
||||
for _, dirname := range dirnames {
|
||||
childrenMap[dirname] = nil
|
||||
}
|
||||
|
||||
// Order directory names by length (biggest first) so that we assign
|
||||
// paths to the most specific enclosing directory in the following loop.
|
||||
sort.Slice(dirnames, func(i, j int) bool {
|
||||
return len(dirnames[i]) > len(dirnames[j])
|
||||
})
|
||||
|
||||
for _, fi := range fis {
|
||||
path := fi.Name()
|
||||
if strings.Contains(path, "/") {
|
||||
for _, dirname := range dirnames {
|
||||
if strings.HasPrefix(path, dirname) {
|
||||
childrenMap[dirname] = append(childrenMap[dirname], path)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if len(dirnames) > 0 && dirnames[len(dirnames)-1] == "" {
|
||||
// No need to loop here. If we have a root input directory it
|
||||
// will necessarily be the last element due to the previous
|
||||
// sorting step.
|
||||
childrenMap[""] = append(childrenMap[""], path)
|
||||
}
|
||||
}
|
||||
|
||||
return childrenMap
|
||||
}
|
||||
|
||||
func inTransaction(ctx context.Context, dbStore store.Store, fn func(tx store.Store) error) (err error) {
|
||||
return dbStore.WithTransaction(ctx, fn)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -23,6 +24,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/codeintel/uploads/internal/store"
|
||||
"github.com/sourcegraph/sourcegraph/internal/codeintel/uploads/shared"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
@ -70,7 +72,17 @@ func TestHandle(t *testing.T) {
|
||||
mockUploadStore.GetFunc.SetDefaultHook(copyTestDumpScip)
|
||||
|
||||
// Allowlist all files in dump
|
||||
gitserverClient.ListDirectoryChildrenFunc.SetDefaultReturn(scipDirectoryChildren, nil)
|
||||
gitserverClient.ReadDirFunc.SetDefaultHook(func(_ context.Context, _ api.RepoName, _ api.CommitID, path string, _ bool) ([]fs.FileInfo, error) {
|
||||
children, ok := scipDirectoryChildren[path]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
fis := make([]fs.FileInfo, 0, len(children))
|
||||
for _, c := range children {
|
||||
fis = append(fis, &fileutil.FileInfo{Name_: c})
|
||||
}
|
||||
return fis, nil
|
||||
})
|
||||
|
||||
expectedCommitDate := time.Unix(1587396557, 0).UTC()
|
||||
expectedCommitDateStr := expectedCommitDate.Format(time.RFC3339)
|
||||
@ -446,3 +458,90 @@ func defaultMockRepoStore() *dbmocks.MockRepoStore {
|
||||
})
|
||||
return repoStore
|
||||
}
|
||||
|
||||
func TestParseDirectoryChildrenRoot(t *testing.T) {
|
||||
dirnames := []string{""}
|
||||
file := func(name string) fs.FileInfo {
|
||||
return &fileutil.FileInfo{
|
||||
Name_: name,
|
||||
}
|
||||
}
|
||||
paths := []fs.FileInfo{
|
||||
file(".github"),
|
||||
file(".gitignore"),
|
||||
file("LICENSE"),
|
||||
file("README.md"),
|
||||
file("cmd"),
|
||||
file("go.mod"),
|
||||
file("go.sum"),
|
||||
file("internal"),
|
||||
file("protocol"),
|
||||
}
|
||||
|
||||
expected := map[string][]string{
|
||||
"": {
|
||||
".github",
|
||||
".gitignore",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"cmd",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"internal",
|
||||
"protocol",
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, parseDirectoryChildren(dirnames, paths)); diff != "" {
|
||||
t.Errorf("unexpected directory children result (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDirectoryChildrenNonRoot(t *testing.T) {
|
||||
dirnames := []string{"cmd/", "protocol/", "cmd/protocol/"}
|
||||
file := func(name string) fs.FileInfo {
|
||||
return &fileutil.FileInfo{
|
||||
Name_: name,
|
||||
}
|
||||
}
|
||||
paths := []fs.FileInfo{
|
||||
file("cmd/lsif-go"),
|
||||
file("protocol/protocol.go"),
|
||||
file("protocol/writer.go"),
|
||||
}
|
||||
|
||||
expected := map[string][]string{
|
||||
"cmd/": {"cmd/lsif-go"},
|
||||
"protocol/": {"protocol/protocol.go", "protocol/writer.go"},
|
||||
"cmd/protocol/": nil,
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, parseDirectoryChildren(dirnames, paths)); diff != "" {
|
||||
t.Errorf("unexpected directory children result (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDirectoryChildrenDifferentDepths(t *testing.T) {
|
||||
dirnames := []string{"cmd/", "protocol/", "cmd/protocol/"}
|
||||
file := func(name string) fs.FileInfo {
|
||||
return &fileutil.FileInfo{
|
||||
Name_: name,
|
||||
}
|
||||
}
|
||||
paths := []fs.FileInfo{
|
||||
file("cmd/lsif-go"),
|
||||
file("protocol/protocol.go"),
|
||||
file("protocol/writer.go"),
|
||||
file("cmd/protocol/main.go"),
|
||||
}
|
||||
|
||||
expected := map[string][]string{
|
||||
"cmd/": {"cmd/lsif-go"},
|
||||
"protocol/": {"protocol/protocol.go", "protocol/writer.go"},
|
||||
"cmd/protocol/": {"cmd/protocol/main.go"},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, parseDirectoryChildren(dirnames, paths)); diff != "" {
|
||||
t.Errorf("unexpected directory children result (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
package fileutil
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -22,14 +20,3 @@ func (fi *FileInfo) Mode() os.FileMode { return fi.Mode_ }
|
||||
func (fi *FileInfo) ModTime() time.Time { return fi.ModTime_ }
|
||||
func (fi *FileInfo) IsDir() bool { return fi.Mode().IsDir() }
|
||||
func (fi *FileInfo) Sys() any { return fi.Sys_ }
|
||||
|
||||
// SortFileInfosByName sorts fis by name, alphabetically.
|
||||
func SortFileInfosByName(fis []fs.FileInfo) {
|
||||
sort.Sort(fileInfosByName(fis))
|
||||
}
|
||||
|
||||
type fileInfosByName []fs.FileInfo
|
||||
|
||||
func (v fileInfosByName) Len() int { return len(v) }
|
||||
func (v fileInfosByName) Less(i, j int) bool { return v[i].Name() < v[j].Name() }
|
||||
func (v fileInfosByName) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
|
||||
|
||||
@ -26,7 +26,6 @@ go_library(
|
||||
"//internal/conf",
|
||||
"//internal/conf/conftypes",
|
||||
"//internal/extsvc/gitolite",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver/connection",
|
||||
"//internal/gitserver/gitdomain",
|
||||
"//internal/gitserver/protocol",
|
||||
@ -39,8 +38,6 @@ go_library(
|
||||
"//internal/search/streaming/http",
|
||||
"//lib/errors",
|
||||
"//lib/pointers",
|
||||
"@com_github_go_git_go_git_v5//plumbing/format/config",
|
||||
"@com_github_golang_groupcache//lru",
|
||||
"@com_github_sourcegraph_conc//pool",
|
||||
"@com_github_sourcegraph_go_diff//diff",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
|
||||
@ -454,11 +454,6 @@ type Client interface {
|
||||
// FirstEverCommit returns the first commit ever made to the repository.
|
||||
FirstEverCommit(ctx context.Context, repo api.RepoName) (*gitdomain.Commit, error)
|
||||
|
||||
// ListDirectoryChildren fetches the list of children under the given directory
|
||||
// names. The result is a map keyed by the directory names with the list of files
|
||||
// under each.
|
||||
ListDirectoryChildren(ctx context.Context, repo api.RepoName, commit api.CommitID, dirnames []string) (map[string][]string, error)
|
||||
|
||||
// Diff returns an iterator that can be used to access the diff between two
|
||||
// commits on a per-file basis. The iterator must be closed with Close when no
|
||||
// longer required.
|
||||
@ -477,9 +472,6 @@ type Client interface {
|
||||
// all commits reachable from HEAD.
|
||||
CommitsUniqueToBranch(ctx context.Context, repo api.RepoName, branchName string, isDefaultBranch bool, maxAge *time.Time) (map[string]time.Time, error)
|
||||
|
||||
// LsFiles returns the output of `git ls-files`.
|
||||
LsFiles(ctx context.Context, repo api.RepoName, commit api.CommitID, pathspecs ...gitdomain.Pathspec) ([]string, error)
|
||||
|
||||
// GetCommit returns the commit with the given commit ID, or RevisionNotFoundError if no such commit
|
||||
// exists.
|
||||
GetCommit(ctx context.Context, repo api.RepoName, id api.CommitID) (*gitdomain.Commit, error)
|
||||
|
||||
@ -4,21 +4,15 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
stdlibpath "path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/format/config"
|
||||
"github.com/golang/groupcache/lru"
|
||||
"github.com/sourcegraph/go-diff/diff"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"google.golang.org/grpc/codes"
|
||||
@ -28,7 +22,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/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
"github.com/sourcegraph/sourcegraph/internal/grpc/streamio"
|
||||
@ -418,7 +411,6 @@ func (i *changedFilesIterator) Close() {
|
||||
})
|
||||
}
|
||||
|
||||
// ReadDir reads the contents of the named directory at commit.
|
||||
func (c *clientImplementor) ReadDir(ctx context.Context, repo api.RepoName, commit api.CommitID, path string, recurse bool) (_ []fs.FileInfo, err error) {
|
||||
ctx, _, endObservation := c.operations.readDir.With(ctx, &err, observation.Args{
|
||||
MetricLabelValues: []string{c.scope},
|
||||
@ -431,23 +423,54 @@ func (c *clientImplementor) ReadDir(ctx context.Context, repo api.RepoName, comm
|
||||
})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
if err := checkSpecArgSafety(string(commit)); err != nil {
|
||||
client, err := c.clientSource.ClientForRepo(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
// Trailing slash is necessary to ls-tree under the dir (not just
|
||||
// to list the dir's tree entry in its parent dir).
|
||||
path = filepath.Clean(rel(path)) + "/"
|
||||
res, err := client.ReadDir(ctx, &proto.ReadDirRequest{
|
||||
RepoName: string(repo),
|
||||
CommitSha: string(commit),
|
||||
Path: []byte(path),
|
||||
Recursive: recurse,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fis := []fs.FileInfo{}
|
||||
|
||||
for {
|
||||
chunk, err := res.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok {
|
||||
// If sub repo permissions deny access to the file, we return os.ErrNotExist.
|
||||
if s.Code() == codes.NotFound {
|
||||
for _, d := range s.Details() {
|
||||
fp, ok := d.(*proto.FileNotFoundPayload)
|
||||
if ok {
|
||||
return nil, &os.PathError{Op: "open", Path: fp.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
for _, fi := range chunk.GetFileInfo() {
|
||||
fis = append(fis, gitdomain.ProtoFileInfoToFS(fi))
|
||||
}
|
||||
}
|
||||
files, err := c.lsTree(ctx, repo, commit, path, recurse)
|
||||
|
||||
if err != nil || !authz.SubRepoEnabled(c.subRepoPermsChecker) {
|
||||
return files, err
|
||||
return fis, err
|
||||
}
|
||||
|
||||
a := actor.FromContext(ctx)
|
||||
filtered, filteringErr := authz.FilterActorFileInfos(ctx, c.subRepoPermsChecker, a, repo, files)
|
||||
filtered, filteringErr := authz.FilterActorFileInfos(ctx, c.subRepoPermsChecker, a, repo, fis)
|
||||
if filteringErr != nil {
|
||||
return nil, errors.Wrap(err, "filtering paths")
|
||||
} else {
|
||||
@ -455,277 +478,20 @@ func (c *clientImplementor) ReadDir(ctx context.Context, repo api.RepoName, comm
|
||||
}
|
||||
}
|
||||
|
||||
// lsTreeRootCache caches the result of running `git ls-tree ...` on a repository's root path
|
||||
// (because non-root paths are likely to have a lower cache hit rate). It is intended to improve the
|
||||
// perceived performance of large monorepos, where the tree for a given repo+commit (usually the
|
||||
// repo's latest commit on default branch) will be requested frequently and would take multiple
|
||||
// seconds to compute if uncached.
|
||||
var (
|
||||
lsTreeRootCacheMu sync.Mutex
|
||||
lsTreeRootCache = lru.New(5)
|
||||
)
|
||||
|
||||
// lsTree returns ls of tree at path.
|
||||
func (c *clientImplementor) lsTree(
|
||||
ctx context.Context,
|
||||
repo api.RepoName,
|
||||
commit api.CommitID,
|
||||
path string,
|
||||
recurse bool,
|
||||
) (files []fs.FileInfo, err error) {
|
||||
if path != "" || !recurse {
|
||||
// Only cache the root recursive ls-tree.
|
||||
return c.lsTreeUncached(ctx, repo, commit, path, recurse)
|
||||
func pathspecsToStrings(pathspecs []gitdomain.Pathspec) []string {
|
||||
var strings []string
|
||||
for _, pathspec := range pathspecs {
|
||||
strings = append(strings, string(pathspec))
|
||||
}
|
||||
|
||||
key := string(repo) + ":" + string(commit) + ":" + path
|
||||
lsTreeRootCacheMu.Lock()
|
||||
v, ok := lsTreeRootCache.Get(key)
|
||||
lsTreeRootCacheMu.Unlock()
|
||||
var entries []fs.FileInfo
|
||||
if ok {
|
||||
// Cache hit.
|
||||
entries = v.([]fs.FileInfo)
|
||||
} else {
|
||||
// Cache miss.
|
||||
var err error
|
||||
start := time.Now()
|
||||
entries, err = c.lsTreeUncached(ctx, repo, commit, path, recurse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// It's only worthwhile to cache if the operation took a while and returned a lot of
|
||||
// data. This is a heuristic.
|
||||
if time.Since(start) > 500*time.Millisecond && len(entries) > 5000 {
|
||||
lsTreeRootCacheMu.Lock()
|
||||
lsTreeRootCache.Add(key, entries)
|
||||
lsTreeRootCacheMu.Unlock()
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
return strings
|
||||
}
|
||||
|
||||
type objectInfo gitdomain.OID
|
||||
|
||||
func (oid objectInfo) OID() gitdomain.OID { return gitdomain.OID(oid) }
|
||||
|
||||
// lStat returns a FileInfo describing the named file at commit. If the file is a
|
||||
// symbolic link, the returned FileInfo describes the symbolic link. lStat makes
|
||||
// no attempt to follow the link.
|
||||
func (c *clientImplementor) lStat(ctx context.Context, repo api.RepoName, commit api.CommitID, path string) (_ fs.FileInfo, err error) {
|
||||
ctx, _, endObservation := c.operations.lstat.With(ctx, &err, observation.Args{
|
||||
MetricLabelValues: []string{c.scope},
|
||||
Attrs: []attribute.KeyValue{
|
||||
commit.Attr(),
|
||||
attribute.String("path", path),
|
||||
},
|
||||
})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
if err := checkSpecArgSafety(string(commit)); err != nil {
|
||||
return nil, err
|
||||
func pathspecsToBytes(pathspecs []gitdomain.Pathspec) [][]byte {
|
||||
var s [][]byte
|
||||
for _, pathspec := range pathspecs {
|
||||
s = append(s, []byte(pathspec))
|
||||
}
|
||||
|
||||
path = filepath.Clean(rel(path))
|
||||
|
||||
if path == "." {
|
||||
// Special case root, which is not returned by `git ls-tree`.
|
||||
obj, err := c.GetObject(ctx, repo, string(commit)+"^{tree}")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fileutil.FileInfo{Mode_: os.ModeDir, Sys_: objectInfo(obj.ID)}, nil
|
||||
}
|
||||
|
||||
fis, err := c.lsTree(ctx, repo, commit, path, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fis) == 0 {
|
||||
return nil, &os.PathError{Op: "ls-tree", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
if !authz.SubRepoEnabled(c.subRepoPermsChecker) {
|
||||
return fis[0], nil
|
||||
}
|
||||
// Applying sub-repo permissions
|
||||
a := actor.FromContext(ctx)
|
||||
include, filteringErr := authz.FilterActorFileInfo(ctx, c.subRepoPermsChecker, a, repo, fis[0])
|
||||
if include && filteringErr == nil {
|
||||
return fis[0], nil
|
||||
} else {
|
||||
if filteringErr != nil {
|
||||
err = errors.Wrap(filteringErr, "filtering paths")
|
||||
} else {
|
||||
err = &os.PathError{Op: "ls-tree", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func errorMessageTruncatedOutput(cmd []string, out []byte) string {
|
||||
const maxOutput = 5000
|
||||
|
||||
message := fmt.Sprintf("git command %v failed", cmd)
|
||||
if len(out) > maxOutput {
|
||||
message += fmt.Sprintf(" (truncated output: %q, %d more)", out[:maxOutput], len(out)-maxOutput)
|
||||
} else {
|
||||
message += fmt.Sprintf(" (output: %q)", out)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func (c *clientImplementor) lsTreeUncached(ctx context.Context, repo api.RepoName, commit api.CommitID, path string, recurse bool) ([]fs.FileInfo, error) {
|
||||
if err := gitdomain.EnsureAbsoluteCommit(commit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Don't call filepath.Clean(path) because ReadDir needs to pass
|
||||
// path with a trailing slash.
|
||||
|
||||
if err := checkSpecArgSafety(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"ls-tree",
|
||||
"--long", // show size
|
||||
"--full-name",
|
||||
"-z",
|
||||
string(commit),
|
||||
}
|
||||
if recurse {
|
||||
args = append(args, "-r", "-t")
|
||||
}
|
||||
if path != "" {
|
||||
args = append(args, "--", filepath.ToSlash(path))
|
||||
}
|
||||
cmd := c.gitCommand(repo, args...)
|
||||
out, err := cmd.CombinedOutput(ctx)
|
||||
if err != nil {
|
||||
if bytes.Contains(out, []byte("exists on disk, but not in")) {
|
||||
return nil, &os.PathError{Op: "ls-tree", Path: filepath.ToSlash(path), Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
message := errorMessageTruncatedOutput(cmd.Args(), out)
|
||||
return nil, errors.WithMessage(err, message)
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
// If we are listing the empty root tree, we will have no output.
|
||||
if stdlibpath.Clean(path) == "." {
|
||||
return []fs.FileInfo{}, nil
|
||||
}
|
||||
return nil, &os.PathError{Op: "git ls-tree", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
trimPath := strings.TrimPrefix(path, "./")
|
||||
lines := strings.Split(string(out), "\x00")
|
||||
fis := make([]fs.FileInfo, len(lines)-1)
|
||||
for i, line := range lines {
|
||||
if i == len(lines)-1 {
|
||||
// last entry is empty
|
||||
continue
|
||||
}
|
||||
|
||||
tabPos := strings.IndexByte(line, '\t')
|
||||
if tabPos == -1 {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` output: %q", out)
|
||||
}
|
||||
info := strings.SplitN(line[:tabPos], " ", 4)
|
||||
name := line[tabPos+1:]
|
||||
if len(name) < len(trimPath) {
|
||||
// This is in a submodule; return the original path to avoid a slice out of bounds panic
|
||||
// when setting the FileInfo._Name below.
|
||||
name = trimPath
|
||||
}
|
||||
|
||||
if len(info) != 4 {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` output: %q", out)
|
||||
}
|
||||
typ := info[1]
|
||||
sha := info[2]
|
||||
if !gitdomain.IsAbsoluteRevision(sha) {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` SHA output: %q", sha)
|
||||
}
|
||||
oid, err := decodeOID(sha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sizeStr := strings.TrimSpace(info[3])
|
||||
var size int64
|
||||
if sizeStr != "-" {
|
||||
// Size of "-" indicates a dir or submodule.
|
||||
size, err = strconv.ParseInt(sizeStr, 10, 64)
|
||||
if err != nil || size < 0 {
|
||||
return nil, errors.Errorf("invalid `git ls-tree` size output: %q (error: %s)", sizeStr, err)
|
||||
}
|
||||
}
|
||||
|
||||
var sys any
|
||||
modeVal, err := strconv.ParseInt(info[0], 8, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mode := os.FileMode(modeVal)
|
||||
switch typ {
|
||||
case "blob":
|
||||
const gitModeSymlink = 0o20000
|
||||
if mode&gitModeSymlink != 0 {
|
||||
mode = os.ModeSymlink
|
||||
} else {
|
||||
// Regular file.
|
||||
mode = mode | 0o644
|
||||
}
|
||||
case "commit":
|
||||
mode = mode | gitdomain.ModeSubmodule
|
||||
cmd := c.gitCommand(repo, "show", fmt.Sprintf("%s:.gitmodules", commit))
|
||||
var submodule gitdomain.Submodule
|
||||
if out, err := cmd.Output(ctx); err == nil {
|
||||
|
||||
var cfg config.Config
|
||||
err := config.NewDecoder(bytes.NewBuffer(out)).Decode(&cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("error parsing .gitmodules: %s", err)
|
||||
}
|
||||
|
||||
submodule.Path = cfg.Section("submodule").Subsection(name).Option("path")
|
||||
submodule.URL = cfg.Section("submodule").Subsection(name).Option("url")
|
||||
}
|
||||
submodule.CommitID = api.CommitID(oid.String())
|
||||
sys = submodule
|
||||
case "tree":
|
||||
mode = mode | os.ModeDir
|
||||
}
|
||||
|
||||
if sys == nil {
|
||||
// Some callers might find it useful to know the object's OID.
|
||||
sys = objectInfo(oid)
|
||||
}
|
||||
|
||||
fis[i] = &fileutil.FileInfo{
|
||||
Name_: name, // full path relative to root (not just basename)
|
||||
Mode_: mode,
|
||||
Size_: size,
|
||||
Sys_: sys,
|
||||
}
|
||||
}
|
||||
fileutil.SortFileInfosByName(fis)
|
||||
|
||||
return fis, nil
|
||||
}
|
||||
|
||||
func decodeOID(sha string) (gitdomain.OID, error) {
|
||||
oidBytes, err := hex.DecodeString(sha)
|
||||
if err != nil {
|
||||
return gitdomain.OID{}, err
|
||||
}
|
||||
var oid gitdomain.OID
|
||||
copy(oid[:], oidBytes)
|
||||
return oid, nil
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *clientImplementor) LogReverseEach(ctx context.Context, repo string, commit string, n int, onLogEntry func(entry gitdomain.LogEntry) error) (err error) {
|
||||
@ -807,12 +573,6 @@ func (c *clientImplementor) StreamBlameFile(ctx context.Context, repo api.RepoNa
|
||||
if err != nil {
|
||||
s, ok := status.FromError(err)
|
||||
if ok {
|
||||
if s.Code() == codes.PermissionDenied {
|
||||
cancel()
|
||||
endObservation(1, observation.Args{})
|
||||
return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
if s.Code() == codes.NotFound {
|
||||
for _, d := range s.Details() {
|
||||
switch d.(type) {
|
||||
@ -949,157 +709,6 @@ func (c *clientImplementor) RevAtTime(ctx context.Context, repo api.RepoName, sp
|
||||
return api.CommitID(res.GetCommitSha()), res.GetCommitSha() != "", nil
|
||||
}
|
||||
|
||||
// LsFiles returns the output of `git ls-files`.
|
||||
func (c *clientImplementor) LsFiles(ctx context.Context, repo api.RepoName, commit api.CommitID, pathspecs ...gitdomain.Pathspec) (_ []string, err error) {
|
||||
ctx, _, endObservation := c.operations.lsFiles.With(ctx, &err, observation.Args{
|
||||
MetricLabelValues: []string{c.scope},
|
||||
Attrs: []attribute.KeyValue{
|
||||
repo.Attr(),
|
||||
attribute.String("commit", string(commit)),
|
||||
attribute.Bool("hasPathSpecs", len(pathspecs) > 0),
|
||||
},
|
||||
})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
args := []string{
|
||||
"ls-files",
|
||||
"-z",
|
||||
"--with-tree",
|
||||
string(commit),
|
||||
}
|
||||
|
||||
if len(pathspecs) > 0 {
|
||||
args = append(args, "--")
|
||||
for _, pathspec := range pathspecs {
|
||||
args = append(args, string(pathspec))
|
||||
}
|
||||
}
|
||||
|
||||
cmd := c.gitCommand(repo, args...)
|
||||
out, err := cmd.CombinedOutput(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, fmt.Sprintf("git command %v failed (output: %q)", cmd.Args(), out))
|
||||
}
|
||||
|
||||
files := strings.Split(string(out), "\x00")
|
||||
// Drop trailing empty string
|
||||
if len(files) > 0 && files[len(files)-1] == "" {
|
||||
files = files[:len(files)-1]
|
||||
}
|
||||
return filterPaths(ctx, c.subRepoPermsChecker, repo, files)
|
||||
}
|
||||
|
||||
// 🚨 SECURITY: All git methods that deal with file or path access need to have
|
||||
// sub-repo permissions applied
|
||||
func filterPaths(ctx context.Context, checker authz.SubRepoPermissionChecker, repo api.RepoName, paths []string) ([]string, error) {
|
||||
if !authz.SubRepoEnabled(checker) {
|
||||
return paths, nil
|
||||
}
|
||||
a := actor.FromContext(ctx)
|
||||
filtered, err := authz.FilterActorPaths(ctx, checker, a, repo, paths)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "filtering paths")
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// ListDirectoryChildren fetches the list of children under the given directory
|
||||
// names. The result is a map keyed by the directory names with the list of files
|
||||
// under each.
|
||||
func (c *clientImplementor) ListDirectoryChildren(
|
||||
ctx context.Context,
|
||||
repo api.RepoName,
|
||||
commit api.CommitID,
|
||||
dirnames []string,
|
||||
) (_ map[string][]string, err error) {
|
||||
ctx, _, endObservation := c.operations.listDirectoryChildren.With(ctx, &err, observation.Args{
|
||||
MetricLabelValues: []string{c.scope},
|
||||
Attrs: []attribute.KeyValue{
|
||||
repo.Attr(),
|
||||
attribute.String("commit", string(commit)),
|
||||
attribute.Int("dirs", len(dirnames)),
|
||||
},
|
||||
})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
args := []string{"ls-tree", "--name-only", string(commit), "--"}
|
||||
args = append(args, cleanDirectoriesForLsTree(dirnames)...)
|
||||
cmd := c.gitCommand(repo, args...)
|
||||
|
||||
out, err := cmd.CombinedOutput(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
paths := strings.Split(string(out), "\n")
|
||||
if authz.SubRepoEnabled(c.subRepoPermsChecker) {
|
||||
paths, err = authz.FilterActorPaths(ctx, c.subRepoPermsChecker, actor.FromContext(ctx), repo, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return parseDirectoryChildren(dirnames, paths), nil
|
||||
}
|
||||
|
||||
// cleanDirectoriesForLsTree sanitizes the input dirnames to a git ls-tree command. There are a
|
||||
// few peculiarities handled here:
|
||||
//
|
||||
// 1. The root of the tree must be indicated with `.`, and
|
||||
// 2. In order for git ls-tree to return a directory's contents, the name must end in a slash.
|
||||
func cleanDirectoriesForLsTree(dirnames []string) []string {
|
||||
var args []string
|
||||
for _, dir := range dirnames {
|
||||
if dir == "" {
|
||||
args = append(args, ".")
|
||||
} else {
|
||||
if !strings.HasSuffix(dir, "/") {
|
||||
dir += "/"
|
||||
}
|
||||
args = append(args, dir)
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// parseDirectoryChildren converts the flat list of files from git ls-tree into a map. The keys of the
|
||||
// resulting map are the input (unsanitized) dirnames, and the value of that key are the files nested
|
||||
// under that directory. If dirnames contains a directory that encloses another, then the paths will
|
||||
// be placed into the key sharing the longest path prefix.
|
||||
func parseDirectoryChildren(dirnames, paths []string) map[string][]string {
|
||||
childrenMap := map[string][]string{}
|
||||
|
||||
// Ensure each directory has an entry, even if it has no children
|
||||
// listed in the gitserver output.
|
||||
for _, dirname := range dirnames {
|
||||
childrenMap[dirname] = nil
|
||||
}
|
||||
|
||||
// Order directory names by length (biggest first) so that we assign
|
||||
// paths to the most specific enclosing directory in the following loop.
|
||||
sort.Slice(dirnames, func(i, j int) bool {
|
||||
return len(dirnames[i]) > len(dirnames[j])
|
||||
})
|
||||
|
||||
for _, path := range paths {
|
||||
if strings.Contains(path, "/") {
|
||||
for _, dirname := range dirnames {
|
||||
if strings.HasPrefix(path, dirname) {
|
||||
childrenMap[dirname] = append(childrenMap[dirname], path)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if len(dirnames) > 0 && dirnames[len(dirnames)-1] == "" {
|
||||
// No need to loop here. If we have a root input directory it
|
||||
// will necessarily be the last element due to the previous
|
||||
// sorting step.
|
||||
childrenMap[""] = append(childrenMap[""], path)
|
||||
}
|
||||
}
|
||||
|
||||
return childrenMap
|
||||
}
|
||||
|
||||
func (c *clientImplementor) GetDefaultBranch(ctx context.Context, repo api.RepoName, short bool) (refName string, commit api.CommitID, err error) {
|
||||
ctx, _, endObservation := c.operations.getDefaultBranch.With(ctx, &err, observation.Args{
|
||||
MetricLabelValues: []string{c.scope},
|
||||
@ -1231,13 +840,6 @@ func (c *clientImplementor) NewFileReader(ctx context.Context, repo api.RepoName
|
||||
firstResp, firstRespErr := cli.Recv()
|
||||
if firstRespErr != nil {
|
||||
if s, ok := status.FromError(firstRespErr); ok {
|
||||
// If sub repo permissions deny access to the file, we return os.ErrNotExist.
|
||||
if s.Code() == codes.PermissionDenied {
|
||||
cancel()
|
||||
err = firstRespErr
|
||||
endObservation(1, observation.Args{})
|
||||
return nil, &os.PathError{Op: "open", Path: req.GetPath(), Err: os.ErrNotExist}
|
||||
}
|
||||
if s.Code() == codes.NotFound {
|
||||
for _, d := range s.Details() {
|
||||
switch d.(type) {
|
||||
@ -1305,18 +907,51 @@ func (c *clientImplementor) Stat(ctx context.Context, repo api.RepoName, commit
|
||||
})
|
||||
defer endObservation(1, observation.Args{})
|
||||
|
||||
if err := checkSpecArgSafety(string(commit)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path = rel(path)
|
||||
|
||||
fi, err := c.lStat(ctx, repo, commit, path)
|
||||
client, err := c.clientSource.ClientForRepo(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fi, nil
|
||||
res, err := client.Stat(ctx, &proto.StatRequest{
|
||||
RepoName: string(repo),
|
||||
CommitSha: string(commit),
|
||||
Path: []byte(path),
|
||||
})
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok {
|
||||
// If sub repo permissions deny access to the file, we return os.ErrNotExist.
|
||||
if s.Code() == codes.NotFound {
|
||||
for _, d := range s.Details() {
|
||||
fp, ok := d.(*proto.FileNotFoundPayload)
|
||||
if ok {
|
||||
return nil, &os.PathError{Op: "open", Path: fp.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi := gitdomain.ProtoFileInfoToFS(res.GetFileInfo())
|
||||
|
||||
if !authz.SubRepoEnabled(c.subRepoPermsChecker) {
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// Applying sub-repo permissions
|
||||
a := actor.FromContext(ctx)
|
||||
include, filteringErr := authz.FilterActorFileInfo(ctx, c.subRepoPermsChecker, a, repo, fi)
|
||||
if include && filteringErr == nil {
|
||||
return fi, nil
|
||||
} else {
|
||||
if filteringErr != nil {
|
||||
err = errors.Wrap(filteringErr, "filtering paths")
|
||||
} else {
|
||||
err = &os.PathError{Op: "ls-tree", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
type CommitsOrder int
|
||||
|
||||
@ -4,12 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -144,374 +141,6 @@ func TestDiffWithSubRepoFiltering(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLsFiles(t *testing.T) {
|
||||
ClientMocks.LocalGitserver = true
|
||||
defer ResetClientMocks()
|
||||
runFileListingTest(t, func(ctx context.Context, checker authz.SubRepoPermissionChecker, repo api.RepoName, commit string) ([]string, error) {
|
||||
client := NewTestClient(t).WithChecker(checker)
|
||||
return client.LsFiles(ctx, repo, api.CommitID(commit))
|
||||
})
|
||||
}
|
||||
|
||||
// runFileListingTest tests the specified function which must return a list of filenames and an error. The test first
|
||||
// tests the basic case (all paths returned), then the case with sub-repo permissions specified.
|
||||
func runFileListingTest(t *testing.T,
|
||||
listingFunctionToTest func(context.Context, authz.SubRepoPermissionChecker, api.RepoName, string) ([]string, error),
|
||||
) {
|
||||
t.Helper()
|
||||
gitCommands := []string{
|
||||
"touch file1",
|
||||
"mkdir dir",
|
||||
"touch dir/file2",
|
||||
"touch dir/file3",
|
||||
"git add file1 dir/file2 dir/file3",
|
||||
"git commit -m commit1",
|
||||
}
|
||||
|
||||
repo, dir := MakeGitRepositoryAndReturnDir(t, gitCommands...)
|
||||
headCommit := GetHeadCommitFromGitDir(t, dir)
|
||||
ctx := context.Background()
|
||||
|
||||
checker := authz.NewMockSubRepoPermissionChecker()
|
||||
// Start disabled
|
||||
checker.EnabledFunc.SetDefaultHook(func() bool {
|
||||
return false
|
||||
})
|
||||
|
||||
files, err := listingFunctionToTest(ctx, checker, repo, headCommit)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{
|
||||
"dir/file2", "dir/file3", "file1",
|
||||
}
|
||||
if diff := cmp.Diff(want, files); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
|
||||
// With filtering
|
||||
checker.EnabledFunc.SetDefaultHook(func() bool {
|
||||
return true
|
||||
})
|
||||
checker.PermissionsFunc.SetDefaultHook(func(ctx context.Context, i int32, content authz.RepoContent) (authz.Perms, error) {
|
||||
if content.Path == "dir/file2" {
|
||||
return authz.Read, nil
|
||||
}
|
||||
return authz.None, nil
|
||||
})
|
||||
usePermissionsForFilePermissionsFunc(checker)
|
||||
ctx = actor.WithActor(ctx, &actor.Actor{
|
||||
UID: 1,
|
||||
})
|
||||
files, err = listingFunctionToTest(ctx, checker, repo, headCommit)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want = []string{
|
||||
"dir/file2",
|
||||
}
|
||||
if diff := cmp.Diff(want, files); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDirectoryChildrenRoot(t *testing.T) {
|
||||
dirnames := []string{""}
|
||||
paths := []string{
|
||||
".github",
|
||||
".gitignore",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"cmd",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"internal",
|
||||
"protocol",
|
||||
}
|
||||
|
||||
expected := map[string][]string{
|
||||
"": paths,
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, parseDirectoryChildren(dirnames, paths)); diff != "" {
|
||||
t.Errorf("unexpected directory children result (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDirectoryChildrenNonRoot(t *testing.T) {
|
||||
dirnames := []string{"cmd/", "protocol/", "cmd/protocol/"}
|
||||
paths := []string{
|
||||
"cmd/lsif-go",
|
||||
"protocol/protocol.go",
|
||||
"protocol/writer.go",
|
||||
}
|
||||
|
||||
expected := map[string][]string{
|
||||
"cmd/": {"cmd/lsif-go"},
|
||||
"protocol/": {"protocol/protocol.go", "protocol/writer.go"},
|
||||
"cmd/protocol/": nil,
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, parseDirectoryChildren(dirnames, paths)); diff != "" {
|
||||
t.Errorf("unexpected directory children result (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDirectoryChildrenDifferentDepths(t *testing.T) {
|
||||
dirnames := []string{"cmd/", "protocol/", "cmd/protocol/"}
|
||||
paths := []string{
|
||||
"cmd/lsif-go",
|
||||
"protocol/protocol.go",
|
||||
"protocol/writer.go",
|
||||
"cmd/protocol/main.go",
|
||||
}
|
||||
|
||||
expected := map[string][]string{
|
||||
"cmd/": {"cmd/lsif-go"},
|
||||
"protocol/": {"protocol/protocol.go", "protocol/writer.go"},
|
||||
"cmd/protocol/": {"cmd/protocol/main.go"},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, parseDirectoryChildren(dirnames, paths)); diff != "" {
|
||||
t.Errorf("unexpected directory children result (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanDirectoriesForLsTree(t *testing.T) {
|
||||
args := []string{"", "foo", "bar/", "baz"}
|
||||
actual := cleanDirectoriesForLsTree(args)
|
||||
expected := []string{".", "foo/", "bar/", "baz/"}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf("unexpected ls-tree args (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDirectoryChildren(t *testing.T) {
|
||||
ClientMocks.LocalGitserver = true
|
||||
defer ResetClientMocks()
|
||||
gitCommands := []string{
|
||||
"mkdir -p dir{1..3}/sub{1..3}",
|
||||
"touch dir1/sub1/file",
|
||||
"touch dir1/sub2/file",
|
||||
"touch dir2/sub1/file",
|
||||
"touch dir2/sub2/file",
|
||||
"touch dir3/sub1/file",
|
||||
"touch dir3/sub3/file",
|
||||
"git add .",
|
||||
"git commit -m commit1",
|
||||
}
|
||||
|
||||
repo := MakeGitRepository(t, gitCommands...)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
checker := authz.NewMockSubRepoPermissionChecker()
|
||||
// Start disabled
|
||||
checker.EnabledFunc.SetDefaultHook(func() bool {
|
||||
return false
|
||||
})
|
||||
client1 := NewTestClient(t).WithChecker(checker)
|
||||
|
||||
dirnames := []string{"dir1/", "dir2/", "dir3/"}
|
||||
children, err := client1.ListDirectoryChildren(ctx, repo, "HEAD", dirnames)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := map[string][]string{
|
||||
"dir1/": {"dir1/sub1", "dir1/sub2"},
|
||||
"dir2/": {"dir2/sub1", "dir2/sub2"},
|
||||
"dir3/": {"dir3/sub1", "dir3/sub3"},
|
||||
}
|
||||
if diff := cmp.Diff(expected, children); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
|
||||
// With filtering
|
||||
checker.EnabledFunc.SetDefaultHook(func() bool {
|
||||
return true
|
||||
})
|
||||
checker.PermissionsFunc.SetDefaultHook(func(ctx context.Context, i int32, content authz.RepoContent) (authz.Perms, error) {
|
||||
if strings.Contains(content.Path, "dir1/") {
|
||||
return authz.Read, nil
|
||||
}
|
||||
return authz.None, nil
|
||||
})
|
||||
usePermissionsForFilePermissionsFunc(checker)
|
||||
client2 := NewTestClient(t).WithChecker(checker)
|
||||
ctx = actor.WithActor(ctx, &actor.Actor{
|
||||
UID: 1,
|
||||
})
|
||||
children, err = client2.ListDirectoryChildren(ctx, repo, "HEAD", dirnames)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected = map[string][]string{
|
||||
"dir1/": {"dir1/sub1", "dir1/sub2"},
|
||||
"dir2/": nil,
|
||||
"dir3/": nil,
|
||||
}
|
||||
if diff := cmp.Diff(expected, children); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_FileSystem_Symlinks(t *testing.T) {
|
||||
ClientMocks.LocalGitserver = true
|
||||
defer ResetClientMocks()
|
||||
|
||||
gitCommands := []string{
|
||||
"touch file1",
|
||||
"mkdir dir1",
|
||||
"ln -s file1 link1",
|
||||
"ln -s ../file1 dir1/link2",
|
||||
"touch --date=2006-01-02T15:04:05Z file1 link1 dir1/link2 || touch -t " + Times[0] + " file1 link1 dir1/link2",
|
||||
"git add link1 file1 dir1/link2",
|
||||
"git commit -m commit1",
|
||||
}
|
||||
|
||||
// map of path to size of content
|
||||
symlinks := map[string]int64{
|
||||
"link1": 5, // file1
|
||||
"dir1/link2": 8, // ../file1
|
||||
}
|
||||
|
||||
dir := InitGitRepository(t, gitCommands...)
|
||||
repo := api.RepoName(filepath.Base(dir))
|
||||
|
||||
client := NewClient("test")
|
||||
|
||||
commitID := api.CommitID(ComputeCommitHash(dir, true))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// file1 should be a file.
|
||||
file1Info, err := client.Stat(ctx, repo, commitID, "file1")
|
||||
if err != nil {
|
||||
t.Fatalf("fs.Stat(file1): %s", err)
|
||||
}
|
||||
if !file1Info.Mode().IsRegular() {
|
||||
t.Errorf("file1 Stat !IsRegular (mode: %o)", file1Info.Mode())
|
||||
}
|
||||
|
||||
checkSymlinkFileInfo := func(name string, link fs.FileInfo) {
|
||||
t.Helper()
|
||||
if link.Mode()&os.ModeSymlink == 0 {
|
||||
t.Errorf("link mode is not symlink (mode: %o)", link.Mode())
|
||||
}
|
||||
if link.Name() != name {
|
||||
t.Errorf("got link.Name() == %q, want %q", link.Name(), name)
|
||||
}
|
||||
}
|
||||
|
||||
// Check symlinks are links
|
||||
for symlink := range symlinks {
|
||||
fi, err := client.Stat(ctx, repo, commitID, symlink)
|
||||
if err != nil {
|
||||
t.Fatalf("fs.Stat(%s): %s", symlink, err)
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
// TODO(alexsaveliev) make it work on Windows too
|
||||
checkSymlinkFileInfo(symlink, fi)
|
||||
}
|
||||
}
|
||||
|
||||
// Also check the FileInfo returned by ReadDir to ensure it's
|
||||
// consistent with the FileInfo returned by lStat.
|
||||
entries, err := client.ReadDir(ctx, repo, commitID, ".", false)
|
||||
if err != nil {
|
||||
t.Fatalf("fs.ReadDir(.): %s", err)
|
||||
}
|
||||
found := false
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == "link1" {
|
||||
found = true
|
||||
if runtime.GOOS != "windows" {
|
||||
checkSymlinkFileInfo("link1", entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("readdir did not return link1")
|
||||
}
|
||||
|
||||
for symlink, size := range symlinks {
|
||||
fi, err := client.Stat(ctx, repo, commitID, symlink)
|
||||
if err != nil {
|
||||
t.Fatalf("fs.Stat(%s): %s", symlink, err)
|
||||
}
|
||||
if fi.Mode()&fs.ModeSymlink == 0 {
|
||||
t.Errorf("%s Stat is not a symlink (mode: %o)", symlink, fi.Mode())
|
||||
}
|
||||
if fi.Name() != symlink {
|
||||
t.Errorf("got Name %q, want %q", fi.Name(), symlink)
|
||||
}
|
||||
if fi.Size() != size {
|
||||
t.Errorf("got %s Size %d, want %d", symlink, fi.Size(), size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStat(t *testing.T) {
|
||||
ClientMocks.LocalGitserver = true
|
||||
defer ResetClientMocks()
|
||||
|
||||
gitCommands := []string{
|
||||
"mkdir dir1",
|
||||
"touch dir1/file1",
|
||||
"git add dir1/file1",
|
||||
"git commit -m commit1",
|
||||
}
|
||||
|
||||
dir := InitGitRepository(t, gitCommands...)
|
||||
repo := api.RepoName(filepath.Base(dir))
|
||||
checker := authz.NewMockSubRepoPermissionChecker()
|
||||
// Start disabled
|
||||
checker.EnabledFunc.SetDefaultHook(func() bool {
|
||||
return false
|
||||
})
|
||||
client := NewTestClient(t).WithChecker(checker)
|
||||
|
||||
commitID := api.CommitID(ComputeCommitHash(dir, true))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fileInfo, err := client.Stat(ctx, repo, commitID, "dir1/file1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "dir1/file1"
|
||||
if diff := cmp.Diff(want, fileInfo.Name()); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
|
||||
ctx = actor.WithActor(ctx, &actor.Actor{
|
||||
UID: 1,
|
||||
})
|
||||
|
||||
// With filtering
|
||||
checker.EnabledFunc.SetDefaultHook(func() bool {
|
||||
return true
|
||||
})
|
||||
checker.PermissionsFunc.SetDefaultHook(func(ctx context.Context, i int32, content authz.RepoContent) (authz.Perms, error) {
|
||||
if strings.HasPrefix(content.Path, "dir2") {
|
||||
return authz.Read, nil
|
||||
}
|
||||
return authz.None, nil
|
||||
})
|
||||
usePermissionsForFilePermissionsFunc(checker)
|
||||
_, err = client.Stat(ctx, repo, commitID, "dir1/file1")
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want = "ls-tree dir1/file1: file does not exist"
|
||||
if diff := cmp.Diff(want, err.Error()); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
var NonExistentCommitID = api.CommitID(strings.Repeat("a", 40))
|
||||
|
||||
func TestLogPartsPerCommitInSync(t *testing.T) {
|
||||
require.Equal(t, partsPerCommit-1, strings.Count(logFormatWithoutRefs, "%x00"))
|
||||
}
|
||||
@ -654,6 +283,8 @@ func TestRepository_HasCommitAfter(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
var nonExistentCommitID = api.CommitID(strings.Repeat("a", 40))
|
||||
|
||||
func TestRepository_Commits(t *testing.T) {
|
||||
ClientMocks.LocalGitserver = true
|
||||
defer ResetClientMocks()
|
||||
@ -703,7 +334,7 @@ func TestRepository_Commits(t *testing.T) {
|
||||
testCommits(ctx, label, test.repo, CommitsOptions{Range: string(test.id)}, checker, test.wantCommits, t)
|
||||
|
||||
// Test that trying to get a nonexistent commit returns RevisionNotFoundError.
|
||||
if _, err := client.Commits(ctx, test.repo, CommitsOptions{Range: string(NonExistentCommitID)}); !errors.HasType(err, &gitdomain.RevisionNotFoundError{}) {
|
||||
if _, err := client.Commits(ctx, test.repo, CommitsOptions{Range: string(nonExistentCommitID)}); !errors.HasType(err, &gitdomain.RevisionNotFoundError{}) {
|
||||
t.Errorf("%s: for nonexistent commit: got err %v, want RevisionNotFoundError", label, err)
|
||||
}
|
||||
})
|
||||
@ -1009,7 +640,7 @@ func TestRepository_Commits_options_path(t *testing.T) {
|
||||
gitCommands := []string{
|
||||
"git commit --allow-empty -m commit1",
|
||||
"touch file1",
|
||||
"touch --date=2006-01-02T15:04:05Z file1 || touch -t " + Times[0] + " file1",
|
||||
"touch --date=2006-01-02T15:04:05Z file1 || touch -t " + times[0] + " file1",
|
||||
"git add file1",
|
||||
"git commit -m commit2",
|
||||
"GIT_COMMITTER_NAME=c GIT_COMMITTER_EMAIL=c@c.com GIT_COMMITTER_DATE=2006-01-02T15:04:07Z git commit --allow-empty -m commit3 --author='a <a@a.com>' --date 2006-01-02T15:04:06Z",
|
||||
@ -1760,30 +1391,6 @@ func Test_CommitLog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorMessageTruncateOutput(t *testing.T) {
|
||||
cmd := []string{"git", "ls-files"}
|
||||
|
||||
t.Run("short output", func(t *testing.T) {
|
||||
shortOutput := "aaaaaaaaaab"
|
||||
message := errorMessageTruncatedOutput(cmd, []byte(shortOutput))
|
||||
want := fmt.Sprintf("git command [git ls-files] failed (output: %q)", shortOutput)
|
||||
|
||||
if diff := cmp.Diff(want, message); diff != "" {
|
||||
t.Fatalf("wrong message. diff: %s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("truncating output", func(t *testing.T) {
|
||||
longOutput := strings.Repeat("a", 5000) + "b"
|
||||
message := errorMessageTruncatedOutput(cmd, []byte(longOutput))
|
||||
want := fmt.Sprintf("git command [git ls-files] failed (truncated output: %q, 1 more)", longOutput[:5000])
|
||||
|
||||
if diff := cmp.Diff(want, message); diff != "" {
|
||||
t.Fatalf("wrong message. diff: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_ArchiveReader(t *testing.T) {
|
||||
t.Run("firstChunk memoization", func(t *testing.T) {
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
@ -2623,3 +2230,221 @@ func TestClient_GetObject(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_Stat(t *testing.T) {
|
||||
t.Run("correctly returns server response", func(t *testing.T) {
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
c.StatFunc.SetDefaultReturn(&proto.StatResponse{
|
||||
FileInfo: &proto.FileInfo{
|
||||
Name: []byte("file"),
|
||||
Size: 10,
|
||||
Mode: 0644,
|
||||
},
|
||||
}, nil)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
c := NewTestClient(t).WithClientSource(source)
|
||||
|
||||
res, err := c.Stat(context.Background(), "repo", "HEAD", "file")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file", res.Name())
|
||||
})
|
||||
|
||||
t.Run("returns common errors correctly", func(t *testing.T) {
|
||||
t.Run("RevisionNotFound", func(t *testing.T) {
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
s, err := status.New(codes.NotFound, "revision not found").WithDetails(&proto.RevisionNotFoundPayload{
|
||||
Repo: "repo",
|
||||
Spec: "HEAD",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
c.StatFunc.PushReturn(nil, s.Err())
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
c := NewTestClient(t).WithClientSource(source)
|
||||
|
||||
_, err := c.Stat(context.Background(), "repo", "HEAD", "file")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.HasType(err, &gitdomain.RevisionNotFoundError{}))
|
||||
})
|
||||
|
||||
t.Run("FileNotFound", func(t *testing.T) {
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
s, err := status.New(codes.NotFound, "file not found").WithDetails(&proto.FileNotFoundPayload{
|
||||
Repo: "repo",
|
||||
Path: "file",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
c.StatFunc.PushReturn(nil, s.Err())
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
c := NewTestClient(t).WithClientSource(source)
|
||||
|
||||
_, err := c.Stat(context.Background(), "repo", "HEAD", "file")
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
})
|
||||
t.Run("subrepo permissions", func(t *testing.T) {
|
||||
ctx := actor.WithActor(context.Background(), actor.FromUser(1))
|
||||
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
c.StatFunc.SetDefaultReturn(&proto.StatResponse{
|
||||
FileInfo: &proto.FileInfo{
|
||||
Name: []byte("file"),
|
||||
Size: 10,
|
||||
Mode: 0644,
|
||||
},
|
||||
}, nil)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
checker := getTestSubRepoPermsChecker("file")
|
||||
c := NewTestClient(t).WithClientSource(source).WithChecker(checker)
|
||||
|
||||
_, err := c.Stat(ctx, "repo", "HEAD", "file")
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_ReadDir(t *testing.T) {
|
||||
t.Run("correctly returns server response", func(t *testing.T) {
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
s := NewMockGitserverService_ReadDirClient()
|
||||
s.RecvFunc.PushReturn(&proto.ReadDirResponse{
|
||||
FileInfo: []*proto.FileInfo{
|
||||
{
|
||||
Name: []byte("file"),
|
||||
Size: 10,
|
||||
Mode: 0644,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
s.RecvFunc.PushReturn(&proto.ReadDirResponse{
|
||||
FileInfo: []*proto.FileInfo{
|
||||
{
|
||||
Name: []byte("dir/file"),
|
||||
Size: 12,
|
||||
Mode: 0644,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
s.RecvFunc.PushReturn(nil, io.EOF)
|
||||
c.ReadDirFunc.SetDefaultReturn(s, nil)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
c := NewTestClient(t).WithClientSource(source)
|
||||
|
||||
res, err := c.ReadDir(context.Background(), "repo", "HEAD", "", true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file", res[0].Name())
|
||||
require.Equal(t, "dir/file", res[1].Name())
|
||||
})
|
||||
|
||||
t.Run("returns common errors correctly", func(t *testing.T) {
|
||||
t.Run("RevisionNotFound", func(t *testing.T) {
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
ss := NewMockGitserverService_ReadDirClient()
|
||||
s, err := status.New(codes.NotFound, "revision not found").WithDetails(&proto.RevisionNotFoundPayload{
|
||||
Repo: "repo",
|
||||
Spec: "HEAD",
|
||||
})
|
||||
ss.RecvFunc.SetDefaultReturn(nil, s.Err())
|
||||
require.NoError(t, err)
|
||||
c.ReadDirFunc.PushReturn(ss, nil)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
c := NewTestClient(t).WithClientSource(source)
|
||||
|
||||
_, err := c.ReadDir(context.Background(), "repo", "HEAD", "file", true)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.HasType(err, &gitdomain.RevisionNotFoundError{}))
|
||||
})
|
||||
|
||||
t.Run("FileNotFound", func(t *testing.T) {
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
ss := NewMockGitserverService_ReadDirClient()
|
||||
s, err := status.New(codes.NotFound, "file not found").WithDetails(&proto.FileNotFoundPayload{
|
||||
Repo: "repo",
|
||||
Path: "file",
|
||||
})
|
||||
ss.RecvFunc.SetDefaultReturn(nil, s.Err())
|
||||
require.NoError(t, err)
|
||||
c.ReadDirFunc.PushReturn(ss, nil)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
c := NewTestClient(t).WithClientSource(source)
|
||||
|
||||
_, err := c.ReadDir(context.Background(), "repo", "HEAD", "file", true)
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
})
|
||||
t.Run("subrepo permissions", func(t *testing.T) {
|
||||
ctx := actor.WithActor(context.Background(), actor.FromUser(1))
|
||||
|
||||
source := NewTestClientSource(t, []string{"gitserver"}, func(o *TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
c := NewMockGitserverServiceClient()
|
||||
s := NewMockGitserverService_ReadDirClient()
|
||||
s.RecvFunc.PushReturn(&proto.ReadDirResponse{
|
||||
FileInfo: []*proto.FileInfo{
|
||||
{
|
||||
Name: []byte("file"),
|
||||
Size: 10,
|
||||
Mode: 0644,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
s.RecvFunc.PushReturn(&proto.ReadDirResponse{
|
||||
FileInfo: []*proto.FileInfo{
|
||||
{
|
||||
Name: []byte("dir/file"),
|
||||
Size: 12,
|
||||
Mode: 0644,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
s.RecvFunc.PushReturn(nil, io.EOF)
|
||||
c.ReadDirFunc.SetDefaultReturn(s, nil)
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
checker := getTestSubRepoPermsChecker("file")
|
||||
c := NewTestClient(t).WithClientSource(source).WithChecker(checker)
|
||||
|
||||
res, err := c.ReadDir(ctx, "repo", "HEAD", "file", true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "dir/file", res[0].Name())
|
||||
})
|
||||
}
|
||||
|
||||
@ -347,4 +347,26 @@ func (r *errorTranslatingClient) ChangedFiles(ctx context.Context, in *proto.Cha
|
||||
return &errorTranslatingChangedFilesClient{cc}, nil
|
||||
}
|
||||
|
||||
func (r *errorTranslatingClient) Stat(ctx context.Context, in *proto.StatRequest, opts ...grpc.CallOption) (*proto.StatResponse, error) {
|
||||
res, err := r.base.Stat(ctx, in, opts...)
|
||||
return res, convertGRPCErrorToGitDomainError(err)
|
||||
}
|
||||
|
||||
func (r *errorTranslatingClient) ReadDir(ctx context.Context, in *proto.ReadDirRequest, opts ...grpc.CallOption) (proto.GitserverService_ReadDirClient, error) {
|
||||
cc, err := r.base.ReadDir(ctx, in, opts...)
|
||||
if err != nil {
|
||||
return nil, convertGRPCErrorToGitDomainError(err)
|
||||
}
|
||||
return &errorTranslatingReadDirClient{cc}, nil
|
||||
}
|
||||
|
||||
type errorTranslatingReadDirClient struct {
|
||||
proto.GitserverService_ReadDirClient
|
||||
}
|
||||
|
||||
func (r *errorTranslatingReadDirClient) Recv() (*proto.ReadDirResponse, error) {
|
||||
res, err := r.GitserverService_ReadDirClient.Recv()
|
||||
return res, convertGRPCErrorToGitDomainError(err)
|
||||
}
|
||||
|
||||
var _ proto.GitserverServiceClient = &errorTranslatingClient{}
|
||||
|
||||
@ -12,6 +12,7 @@ go_library(
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver/v1:gitserver",
|
||||
"//internal/lazyregexp",
|
||||
"//lib/errors",
|
||||
@ -30,6 +31,7 @@ go_test(
|
||||
embed = [":gitdomain"],
|
||||
deps = [
|
||||
"//internal/api",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver/v1:gitserver",
|
||||
"@com_github_google_go_cmp//cmp",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
|
||||
@ -3,6 +3,7 @@ package gitdomain
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@ -10,7 +11,9 @@ import (
|
||||
"github.com/gobwas/glob"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
v1 "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/lazyregexp"
|
||||
@ -39,6 +42,7 @@ const (
|
||||
// (os.ModeDevice) beyond the Git "160000" commit mode bits. The choice of os.ModeDevice is
|
||||
// arbitrary.
|
||||
const ModeSubmodule = 0o160000 | os.ModeDevice
|
||||
const ModeSymlink = 0o20000
|
||||
|
||||
// Submodule holds information about a Git submodule and is
|
||||
// returned in the FileInfo's Sys field by Stat/ReadDir calls.
|
||||
@ -581,3 +585,58 @@ func (gs RefGlobs) Match(ref string) bool {
|
||||
// Pathspec is a git term for a pattern that matches paths using glob-like syntax.
|
||||
// https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec
|
||||
type Pathspec string
|
||||
|
||||
func FSFileInfoToProto(fi fs.FileInfo) *v1.FileInfo {
|
||||
p := &proto.FileInfo{
|
||||
Name: []byte(fi.Name()),
|
||||
Size: fi.Size(),
|
||||
Mode: uint32(fi.Mode()),
|
||||
}
|
||||
sys := fi.Sys()
|
||||
switch s := sys.(type) {
|
||||
case Submodule:
|
||||
p.Submodule = &proto.GitSubmodule{
|
||||
Url: s.URL,
|
||||
CommitSha: string(s.CommitID),
|
||||
Path: []byte(s.Path),
|
||||
}
|
||||
case ObjectInfo:
|
||||
p.BlobOid = s.OID().String()
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func ProtoFileInfoToFS(fi *v1.FileInfo) fs.FileInfo {
|
||||
var sys any
|
||||
if sm := fi.GetSubmodule(); sm != nil {
|
||||
sys = Submodule{
|
||||
URL: sm.GetUrl(),
|
||||
Path: string(sm.GetPath()),
|
||||
CommitID: api.CommitID(sm.GetCommitSha()),
|
||||
}
|
||||
} else {
|
||||
oid, _ := decodeOID(fi.GetBlobOid())
|
||||
sys = objectInfo(oid)
|
||||
}
|
||||
return &fileutil.FileInfo{
|
||||
Name_: string(fi.GetName()),
|
||||
Mode_: fs.FileMode(fi.GetMode()),
|
||||
Size_: fi.GetSize(),
|
||||
ModTime_: time.Time{}, // Not supported.
|
||||
Sys_: sys,
|
||||
}
|
||||
}
|
||||
|
||||
func decodeOID(sha string) (OID, error) {
|
||||
oidBytes, err := hex.DecodeString(sha)
|
||||
if err != nil {
|
||||
return OID{}, err
|
||||
}
|
||||
var oid OID
|
||||
copy(oid[:], oidBytes)
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
type objectInfo OID
|
||||
|
||||
func (oid objectInfo) OID() OID { return OID(oid) }
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package gitdomain
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
@ -12,6 +13,7 @@ import (
|
||||
protobuf "google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
)
|
||||
|
||||
@ -379,3 +381,26 @@ func TestRoundTripBehindAhead(t *testing.T) {
|
||||
t.Fatalf("unexpected diff (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTripFileInfo(t *testing.T) {
|
||||
diff := ""
|
||||
|
||||
err := quick.Check(func(name string, mode fs.FileMode, size int64, oid OID) bool {
|
||||
original := &fileutil.FileInfo{
|
||||
Name_: name,
|
||||
Mode_: mode,
|
||||
Size_: size,
|
||||
Sys_: objectInfo(oid),
|
||||
}
|
||||
converted := ProtoFileInfoToFS(FSFileInfoToProto(original))
|
||||
if diff = cmp.Diff(original, converted); diff != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected diff (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -81,9 +81,6 @@ type MockClient struct {
|
||||
// IsRepoCloneableFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method IsRepoCloneable.
|
||||
IsRepoCloneableFunc *ClientIsRepoCloneableFunc
|
||||
// ListDirectoryChildrenFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method ListDirectoryChildren.
|
||||
ListDirectoryChildrenFunc *ClientListDirectoryChildrenFunc
|
||||
// ListGitoliteReposFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method ListGitoliteRepos.
|
||||
ListGitoliteReposFunc *ClientListGitoliteReposFunc
|
||||
@ -93,9 +90,6 @@ type MockClient struct {
|
||||
// LogReverseEachFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method LogReverseEach.
|
||||
LogReverseEachFunc *ClientLogReverseEachFunc
|
||||
// LsFilesFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method LsFiles.
|
||||
LsFilesFunc *ClientLsFilesFunc
|
||||
// MergeBaseFunc is an instance of a mock function object controlling
|
||||
// the behavior of the method MergeBase.
|
||||
MergeBaseFunc *ClientMergeBaseFunc
|
||||
@ -248,11 +242,6 @@ func NewMockClient() *MockClient {
|
||||
return
|
||||
},
|
||||
},
|
||||
ListDirectoryChildrenFunc: &ClientListDirectoryChildrenFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, []string) (r0 map[string][]string, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
ListGitoliteReposFunc: &ClientListGitoliteReposFunc{
|
||||
defaultHook: func(context.Context, string) (r0 []*gitolite.Repo, r1 error) {
|
||||
return
|
||||
@ -268,11 +257,6 @@ func NewMockClient() *MockClient {
|
||||
return
|
||||
},
|
||||
},
|
||||
LsFilesFunc: &ClientLsFilesFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) (r0 []string, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
MergeBaseFunc: &ClientMergeBaseFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, string, string) (r0 api.CommitID, r1 error) {
|
||||
return
|
||||
@ -460,11 +444,6 @@ func NewStrictMockClient() *MockClient {
|
||||
panic("unexpected invocation of MockClient.IsRepoCloneable")
|
||||
},
|
||||
},
|
||||
ListDirectoryChildrenFunc: &ClientListDirectoryChildrenFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error) {
|
||||
panic("unexpected invocation of MockClient.ListDirectoryChildren")
|
||||
},
|
||||
},
|
||||
ListGitoliteReposFunc: &ClientListGitoliteReposFunc{
|
||||
defaultHook: func(context.Context, string) ([]*gitolite.Repo, error) {
|
||||
panic("unexpected invocation of MockClient.ListGitoliteRepos")
|
||||
@ -480,11 +459,6 @@ func NewStrictMockClient() *MockClient {
|
||||
panic("unexpected invocation of MockClient.LogReverseEach")
|
||||
},
|
||||
},
|
||||
LsFilesFunc: &ClientLsFilesFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error) {
|
||||
panic("unexpected invocation of MockClient.LsFiles")
|
||||
},
|
||||
},
|
||||
MergeBaseFunc: &ClientMergeBaseFunc{
|
||||
defaultHook: func(context.Context, api.RepoName, string, string) (api.CommitID, error) {
|
||||
panic("unexpected invocation of MockClient.MergeBase")
|
||||
@ -634,9 +608,6 @@ func NewMockClientFrom(i Client) *MockClient {
|
||||
IsRepoCloneableFunc: &ClientIsRepoCloneableFunc{
|
||||
defaultHook: i.IsRepoCloneable,
|
||||
},
|
||||
ListDirectoryChildrenFunc: &ClientListDirectoryChildrenFunc{
|
||||
defaultHook: i.ListDirectoryChildren,
|
||||
},
|
||||
ListGitoliteReposFunc: &ClientListGitoliteReposFunc{
|
||||
defaultHook: i.ListGitoliteRepos,
|
||||
},
|
||||
@ -646,9 +617,6 @@ func NewMockClientFrom(i Client) *MockClient {
|
||||
LogReverseEachFunc: &ClientLogReverseEachFunc{
|
||||
defaultHook: i.LogReverseEach,
|
||||
},
|
||||
LsFilesFunc: &ClientLsFilesFunc{
|
||||
defaultHook: i.LsFiles,
|
||||
},
|
||||
MergeBaseFunc: &ClientMergeBaseFunc{
|
||||
defaultHook: i.MergeBase,
|
||||
},
|
||||
@ -2794,121 +2762,6 @@ func (c ClientIsRepoCloneableFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// ClientListDirectoryChildrenFunc describes the behavior when the
|
||||
// ListDirectoryChildren method of the parent MockClient instance is
|
||||
// invoked.
|
||||
type ClientListDirectoryChildrenFunc struct {
|
||||
defaultHook func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error)
|
||||
hooks []func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error)
|
||||
history []ClientListDirectoryChildrenFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// ListDirectoryChildren delegates to the next hook function in the queue
|
||||
// and stores the parameter and result values of this invocation.
|
||||
func (m *MockClient) ListDirectoryChildren(v0 context.Context, v1 api.RepoName, v2 api.CommitID, v3 []string) (map[string][]string, error) {
|
||||
r0, r1 := m.ListDirectoryChildrenFunc.nextHook()(v0, v1, v2, v3)
|
||||
m.ListDirectoryChildrenFunc.appendCall(ClientListDirectoryChildrenFuncCall{v0, v1, v2, v3, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// ListDirectoryChildren method of the parent MockClient instance is invoked
|
||||
// and the hook queue is empty.
|
||||
func (f *ClientListDirectoryChildrenFunc) SetDefaultHook(hook func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// ListDirectoryChildren 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 *ClientListDirectoryChildrenFunc) PushHook(hook func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]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 *ClientListDirectoryChildrenFunc) SetDefaultReturn(r0 map[string][]string, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *ClientListDirectoryChildrenFunc) PushReturn(r0 map[string][]string, r1 error) {
|
||||
f.PushHook(func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *ClientListDirectoryChildrenFunc) nextHook() func(context.Context, api.RepoName, api.CommitID, []string) (map[string][]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 *ClientListDirectoryChildrenFunc) appendCall(r0 ClientListDirectoryChildrenFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of ClientListDirectoryChildrenFuncCall objects
|
||||
// describing the invocations of this function.
|
||||
func (f *ClientListDirectoryChildrenFunc) History() []ClientListDirectoryChildrenFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]ClientListDirectoryChildrenFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// ClientListDirectoryChildrenFuncCall is an object that describes an
|
||||
// invocation of method ListDirectoryChildren on an instance of MockClient.
|
||||
type ClientListDirectoryChildrenFuncCall 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 api.CommitID
|
||||
// Arg3 is the value of the 4th argument passed to this method
|
||||
// invocation.
|
||||
Arg3 []string
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 map[string][]string
|
||||
// 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 ClientListDirectoryChildrenFuncCall) 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 ClientListDirectoryChildrenFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// ClientListGitoliteReposFunc describes the behavior when the
|
||||
// ListGitoliteRepos method of the parent MockClient instance is invoked.
|
||||
type ClientListGitoliteReposFunc struct {
|
||||
@ -3241,126 +3094,6 @@ func (c ClientLogReverseEachFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0}
|
||||
}
|
||||
|
||||
// ClientLsFilesFunc describes the behavior when the LsFiles method of the
|
||||
// parent MockClient instance is invoked.
|
||||
type ClientLsFilesFunc struct {
|
||||
defaultHook func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error)
|
||||
hooks []func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error)
|
||||
history []ClientLsFilesFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// LsFiles delegates to the next hook function in the queue and stores the
|
||||
// parameter and result values of this invocation.
|
||||
func (m *MockClient) LsFiles(v0 context.Context, v1 api.RepoName, v2 api.CommitID, v3 ...gitdomain.Pathspec) ([]string, error) {
|
||||
r0, r1 := m.LsFilesFunc.nextHook()(v0, v1, v2, v3...)
|
||||
m.LsFilesFunc.appendCall(ClientLsFilesFuncCall{v0, v1, v2, v3, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the LsFiles method of
|
||||
// the parent MockClient instance is invoked and the hook queue is empty.
|
||||
func (f *ClientLsFilesFunc) SetDefaultHook(hook func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// LsFiles 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 *ClientLsFilesFunc) PushHook(hook func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]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 *ClientLsFilesFunc) SetDefaultReturn(r0 []string, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *ClientLsFilesFunc) PushReturn(r0 []string, r1 error) {
|
||||
f.PushHook(func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]string, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *ClientLsFilesFunc) nextHook() func(context.Context, api.RepoName, api.CommitID, ...gitdomain.Pathspec) ([]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 *ClientLsFilesFunc) appendCall(r0 ClientLsFilesFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of ClientLsFilesFuncCall objects describing
|
||||
// the invocations of this function.
|
||||
func (f *ClientLsFilesFunc) History() []ClientLsFilesFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]ClientLsFilesFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// ClientLsFilesFuncCall is an object that describes an invocation of method
|
||||
// LsFiles on an instance of MockClient.
|
||||
type ClientLsFilesFuncCall 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 api.CommitID
|
||||
// Arg3 is a slice containing the values of the variadic arguments
|
||||
// passed to this method invocation.
|
||||
Arg3 []gitdomain.Pathspec
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []string
|
||||
// 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. The variadic slice argument is flattened in this array such
|
||||
// that one positional argument and three variadic arguments would result in
|
||||
// a slice of four, not two.
|
||||
func (c ClientLsFilesFuncCall) Args() []interface{} {
|
||||
trailing := []interface{}{}
|
||||
for _, val := range c.Arg3 {
|
||||
trailing = append(trailing, val)
|
||||
}
|
||||
|
||||
return append([]interface{}{c.Arg0, c.Arg1, c.Arg2}, trailing...)
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c ClientLsFilesFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// ClientMergeBaseFunc describes the behavior when the MergeBase method of
|
||||
// the parent MockClient instance is invoked.
|
||||
type ClientMergeBaseFunc struct {
|
||||
|
||||
@ -47,8 +47,6 @@ type operations struct {
|
||||
getObject *observation.Operation
|
||||
commitsUniqueToBranch *observation.Operation
|
||||
getDefaultBranch *observation.Operation
|
||||
listDirectoryChildren *observation.Operation
|
||||
lsFiles *observation.Operation
|
||||
logReverseEach *observation.Operation
|
||||
diffSymbols *observation.Operation
|
||||
commitLog *observation.Operation
|
||||
@ -136,8 +134,6 @@ func newOperations(observationCtx *observation.Context) *operations {
|
||||
getObject: op("GetObject"),
|
||||
commitsUniqueToBranch: op("CommitsUniqueToBranch"),
|
||||
getDefaultBranch: op("GetDefaultBranch"),
|
||||
listDirectoryChildren: op("ListDirectoryChildren"),
|
||||
lsFiles: op("LsFiles"),
|
||||
logReverseEach: op("LogReverseEach"),
|
||||
diffSymbols: op("DiffSymbols"),
|
||||
commitLog: op("CommitLog"),
|
||||
|
||||
@ -180,4 +180,14 @@ func (r *automaticRetryClient) ChangedFiles(ctx context.Context, in *proto.Chang
|
||||
return r.base.ChangedFiles(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (r *automaticRetryClient) Stat(ctx context.Context, in *proto.StatRequest, opts ...grpc.CallOption) (*proto.StatResponse, error) {
|
||||
opts = append(defaults.RetryPolicy, opts...)
|
||||
return r.base.Stat(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (r *automaticRetryClient) ReadDir(ctx context.Context, in *proto.ReadDirRequest, opts ...grpc.CallOption) (proto.GitserverService_ReadDirClient, error) {
|
||||
opts = append(defaults.RetryPolicy, opts...)
|
||||
return r.base.ReadDir(ctx, in, opts...)
|
||||
}
|
||||
|
||||
var _ proto.GitserverServiceClient = &automaticRetryClient{}
|
||||
|
||||
@ -2,7 +2,6 @@ package gitserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@ -120,22 +119,14 @@ func CreateGitCommand(dir, name string, args ...string) *exec.Cmd {
|
||||
return c
|
||||
}
|
||||
|
||||
func AsJSON(v any) string {
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func AppleTime(t string) string {
|
||||
func appleTime(t string) string {
|
||||
ti, _ := time.Parse(time.RFC3339, t)
|
||||
return ti.Local().Format("200601021504.05")
|
||||
}
|
||||
|
||||
var Times = []string{
|
||||
AppleTime("2006-01-02T15:04:05Z"),
|
||||
AppleTime("2014-05-06T19:20:21Z"),
|
||||
var times = []string{
|
||||
appleTime("2006-01-02T15:04:05Z"),
|
||||
appleTime("2014-05-06T19:20:21Z"),
|
||||
}
|
||||
|
||||
// ComputeCommitHash Computes hash of last commit in a given repo dir
|
||||
|
||||
3524
internal/gitserver/v1/gitserver.pb.go
generated
3524
internal/gitserver/v1/gitserver.pb.go
generated
File diff suppressed because it is too large
Load Diff
@ -78,8 +78,11 @@ service GitserverService {
|
||||
// If the given treeish does not exist, an error with a
|
||||
// RevisionNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given paths are not found in the given treeish, an error with a FileNotFoundPayload
|
||||
// is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
rpc Archive(ArchiveRequest) returns (stream ArchiveResponse) {
|
||||
option idempotency_level = NO_SIDE_EFFECTS;
|
||||
}
|
||||
@ -122,8 +125,10 @@ service GitserverService {
|
||||
// hunks as they are found. The --incremental flag is used on the git CLI
|
||||
// level to achieve this behavior.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
rpc Blame(BlameRequest) returns (stream BlameResponse) {
|
||||
option idempotency_level = NO_SIDE_EFFECTS;
|
||||
}
|
||||
@ -138,8 +143,10 @@ service GitserverService {
|
||||
}
|
||||
// ReadFile gets a file from the repo ODB and streams the contents back.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
rpc ReadFile(ReadFileRequest) returns (stream ReadFileResponse) {
|
||||
option idempotency_level = NO_SIDE_EFFECTS;
|
||||
}
|
||||
@ -222,7 +229,6 @@ service GitserverService {
|
||||
rpc ContributorCounts(ContributorCountsRequest) returns (ContributorCountsResponse) {
|
||||
option idempotency_level = NO_SIDE_EFFECTS;
|
||||
}
|
||||
|
||||
// FirstEverCommit returns the first commit ever made to the repository.
|
||||
//
|
||||
// If the given repository is empty, an error with a RevisionNotFoundPayload
|
||||
@ -276,6 +282,32 @@ service GitserverService {
|
||||
rpc ChangedFiles(ChangedFilesRequest) returns (stream ChangedFilesResponse) {
|
||||
option idempotency_level = NO_SIDE_EFFECTS;
|
||||
}
|
||||
// Stat returns a FileInfo describing the named file descriptor at the given commit.
|
||||
// Stat supports submodules, symlinks, directories and files.
|
||||
//
|
||||
// If the commit does not exist, an error with RevisionNotFoundPayload is
|
||||
// returned.
|
||||
//
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
rpc Stat(StatRequest) returns (StatResponse) {
|
||||
option idempotency_level = NO_SIDE_EFFECTS;
|
||||
}
|
||||
// ReadDir returns a list of FileInfos describing the files and subdirectories
|
||||
// in the given directory.
|
||||
//
|
||||
// If the commit does not exist, an error with RevisionNotFoundPayload is
|
||||
// returned.
|
||||
//
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
rpc ReadDir(ReadDirRequest) returns (stream ReadDirResponse) {
|
||||
option idempotency_level = NO_SIDE_EFFECTS;
|
||||
}
|
||||
}
|
||||
|
||||
message ContributorCountsRequest {
|
||||
@ -385,6 +417,59 @@ message GitRef {
|
||||
bool is_head = 7;
|
||||
}
|
||||
|
||||
message StatRequest {
|
||||
// repo_name is the name of the repo to run the blame operation in.
|
||||
// Note: We use field ID 2 here to reserve 1 for a future repo int32 field.
|
||||
string repo_name = 2;
|
||||
// The commit at which we want to stat the file.
|
||||
string commit_sha = 3;
|
||||
// The path to the file to stat.
|
||||
bytes path = 4;
|
||||
}
|
||||
|
||||
message StatResponse {
|
||||
FileInfo file_info = 1;
|
||||
}
|
||||
|
||||
message ReadDirRequest {
|
||||
// repo_name is the name of the repo to run the blame operation in.
|
||||
// Note: We use field ID 2 here to reserve 1 for a future repo int32 field.
|
||||
string repo_name = 2;
|
||||
// The commit at which we want to read the directory.
|
||||
string commit_sha = 3;
|
||||
// The path to the directory to read. Empty means root.
|
||||
optional bytes path = 4;
|
||||
// recursive indicates whether to read the directory recursively. The default is false.
|
||||
bool recursive = 5;
|
||||
}
|
||||
|
||||
message ReadDirResponse {
|
||||
repeated FileInfo file_info = 1;
|
||||
}
|
||||
|
||||
message GitSubmodule {
|
||||
// URL is the submodule repository clone URL.
|
||||
string url = 1;
|
||||
// Path is the path of the submodule relative to the repository root.
|
||||
bytes path = 2;
|
||||
// CommitSHA is the pinned commit ID of the submodule (in the submodule repository's
|
||||
// commit ID space).
|
||||
string commit_sha = 3;
|
||||
}
|
||||
|
||||
message FileInfo {
|
||||
// The file name, relative to the repository root.
|
||||
bytes name = 1;
|
||||
// The file size.
|
||||
int64 size = 2;
|
||||
// The file mode.
|
||||
uint32 mode = 3;
|
||||
// The blob OID in the git ODB.
|
||||
string blob_oid = 4;
|
||||
// If this FileInfo describes a submodule, this field will be populated.
|
||||
optional GitSubmodule submodule = 5;
|
||||
}
|
||||
|
||||
message ResolveRevisionRequest {
|
||||
// repo_name is the name of the repo to run the blame operation in.
|
||||
// Note: We use field ID 2 here to reserve 1 for a future repo int32 field.
|
||||
|
||||
179
internal/gitserver/v1/gitserver_grpc.pb.go
generated
179
internal/gitserver/v1/gitserver_grpc.pb.go
generated
@ -185,6 +185,8 @@ const (
|
||||
GitserverService_FirstEverCommit_FullMethodName = "/gitserver.v1.GitserverService/FirstEverCommit"
|
||||
GitserverService_BehindAhead_FullMethodName = "/gitserver.v1.GitserverService/BehindAhead"
|
||||
GitserverService_ChangedFiles_FullMethodName = "/gitserver.v1.GitserverService/ChangedFiles"
|
||||
GitserverService_Stat_FullMethodName = "/gitserver.v1.GitserverService/Stat"
|
||||
GitserverService_ReadDir_FullMethodName = "/gitserver.v1.GitserverService/ReadDir"
|
||||
)
|
||||
|
||||
// GitserverServiceClient is the client API for GitserverService service.
|
||||
@ -216,8 +218,11 @@ type GitserverServiceClient interface {
|
||||
// If the given treeish does not exist, an error with a
|
||||
// RevisionNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given paths are not found in the given treeish, an error with a FileNotFoundPayload
|
||||
// is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
Archive(ctx context.Context, in *ArchiveRequest, opts ...grpc.CallOption) (GitserverService_ArchiveClient, error)
|
||||
RepoCloneProgress(ctx context.Context, in *RepoCloneProgressRequest, opts ...grpc.CallOption) (*RepoCloneProgressResponse, error)
|
||||
IsPerforcePathCloneable(ctx context.Context, in *IsPerforcePathCloneableRequest, opts ...grpc.CallOption) (*IsPerforcePathCloneableResponse, error)
|
||||
@ -238,8 +243,10 @@ type GitserverServiceClient interface {
|
||||
// hunks as they are found. The --incremental flag is used on the git CLI
|
||||
// level to achieve this behavior.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
Blame(ctx context.Context, in *BlameRequest, opts ...grpc.CallOption) (GitserverService_BlameClient, error)
|
||||
// DefaultBranch resolves HEAD to ref name and current commit SHA it points
|
||||
// to. If HEAD points to an empty branch, it returns an error with a
|
||||
@ -250,8 +257,10 @@ type GitserverServiceClient interface {
|
||||
DefaultBranch(ctx context.Context, in *DefaultBranchRequest, opts ...grpc.CallOption) (*DefaultBranchResponse, error)
|
||||
// ReadFile gets a file from the repo ODB and streams the contents back.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
ReadFile(ctx context.Context, in *ReadFileRequest, opts ...grpc.CallOption) (GitserverService_ReadFileClient, error)
|
||||
// GetCommit gets a commit from the repo ODB.
|
||||
//
|
||||
@ -365,6 +374,28 @@ type GitserverServiceClient interface {
|
||||
// If either the `base` or `head` <tree-ish> id does not exist, an error with
|
||||
// a `RevisionNotFoundPayload` is returned.
|
||||
ChangedFiles(ctx context.Context, in *ChangedFilesRequest, opts ...grpc.CallOption) (GitserverService_ChangedFilesClient, error)
|
||||
// Stat returns a FileInfo describing the named file descriptor at the given commit.
|
||||
// Stat supports submodules, symlinks, directories and files.
|
||||
//
|
||||
// If the commit does not exist, an error with RevisionNotFoundPayload is
|
||||
// returned.
|
||||
//
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
Stat(ctx context.Context, in *StatRequest, opts ...grpc.CallOption) (*StatResponse, error)
|
||||
// ReadDir returns a list of FileInfos describing the files and subdirectories
|
||||
// in the given directory.
|
||||
//
|
||||
// If the commit does not exist, an error with RevisionNotFoundPayload is
|
||||
// returned.
|
||||
//
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
ReadDir(ctx context.Context, in *ReadDirRequest, opts ...grpc.CallOption) (GitserverService_ReadDirClient, error)
|
||||
}
|
||||
|
||||
type gitserverServiceClient struct {
|
||||
@ -854,6 +885,47 @@ func (x *gitserverServiceChangedFilesClient) Recv() (*ChangedFilesResponse, erro
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) Stat(ctx context.Context, in *StatRequest, opts ...grpc.CallOption) (*StatResponse, error) {
|
||||
out := new(StatResponse)
|
||||
err := c.cc.Invoke(ctx, GitserverService_Stat_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) ReadDir(ctx context.Context, in *ReadDirRequest, opts ...grpc.CallOption) (GitserverService_ReadDirClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &GitserverService_ServiceDesc.Streams[9], GitserverService_ReadDir_FullMethodName, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &gitserverServiceReadDirClient{stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
type GitserverService_ReadDirClient interface {
|
||||
Recv() (*ReadDirResponse, error)
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
type gitserverServiceReadDirClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (x *gitserverServiceReadDirClient) Recv() (*ReadDirResponse, error) {
|
||||
m := new(ReadDirResponse)
|
||||
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// GitserverServiceServer is the server API for GitserverService service.
|
||||
// All implementations must embed UnimplementedGitserverServiceServer
|
||||
// for forward compatibility
|
||||
@ -883,8 +955,11 @@ type GitserverServiceServer interface {
|
||||
// If the given treeish does not exist, an error with a
|
||||
// RevisionNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given paths are not found in the given treeish, an error with a FileNotFoundPayload
|
||||
// is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
Archive(*ArchiveRequest, GitserverService_ArchiveServer) error
|
||||
RepoCloneProgress(context.Context, *RepoCloneProgressRequest) (*RepoCloneProgressResponse, error)
|
||||
IsPerforcePathCloneable(context.Context, *IsPerforcePathCloneableRequest) (*IsPerforcePathCloneableResponse, error)
|
||||
@ -905,8 +980,10 @@ type GitserverServiceServer interface {
|
||||
// hunks as they are found. The --incremental flag is used on the git CLI
|
||||
// level to achieve this behavior.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
Blame(*BlameRequest, GitserverService_BlameServer) error
|
||||
// DefaultBranch resolves HEAD to ref name and current commit SHA it points
|
||||
// to. If HEAD points to an empty branch, it returns an error with a
|
||||
@ -917,8 +994,10 @@ type GitserverServiceServer interface {
|
||||
DefaultBranch(context.Context, *DefaultBranchRequest) (*DefaultBranchResponse, error)
|
||||
// ReadFile gets a file from the repo ODB and streams the contents back.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a
|
||||
// NotFound error will be returned, with a RepoNotFoundPayload in the details.
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
ReadFile(*ReadFileRequest, GitserverService_ReadFileServer) error
|
||||
// GetCommit gets a commit from the repo ODB.
|
||||
//
|
||||
@ -1032,6 +1111,28 @@ type GitserverServiceServer interface {
|
||||
// If either the `base` or `head` <tree-ish> id does not exist, an error with
|
||||
// a `RevisionNotFoundPayload` is returned.
|
||||
ChangedFiles(*ChangedFilesRequest, GitserverService_ChangedFilesServer) error
|
||||
// Stat returns a FileInfo describing the named file descriptor at the given commit.
|
||||
// Stat supports submodules, symlinks, directories and files.
|
||||
//
|
||||
// If the commit does not exist, an error with RevisionNotFoundPayload is
|
||||
// returned.
|
||||
//
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
Stat(context.Context, *StatRequest) (*StatResponse, error)
|
||||
// ReadDir returns a list of FileInfos describing the files and subdirectories
|
||||
// in the given directory.
|
||||
//
|
||||
// If the commit does not exist, an error with RevisionNotFoundPayload is
|
||||
// returned.
|
||||
//
|
||||
// If the given path is not found, an error with a FileNotFoundPayload is returned.
|
||||
//
|
||||
// If the given repo is not cloned, it will be enqueued for cloning and a NotFound
|
||||
// error will be returned, with a RepoNotFoundPayload in the details.
|
||||
ReadDir(*ReadDirRequest, GitserverService_ReadDirServer) error
|
||||
mustEmbedUnimplementedGitserverServiceServer()
|
||||
}
|
||||
|
||||
@ -1129,6 +1230,12 @@ func (UnimplementedGitserverServiceServer) BehindAhead(context.Context, *BehindA
|
||||
func (UnimplementedGitserverServiceServer) ChangedFiles(*ChangedFilesRequest, GitserverService_ChangedFilesServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ChangedFiles not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) Stat(context.Context, *StatRequest) (*StatResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) ReadDir(*ReadDirRequest, GitserverService_ReadDirServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ReadDir not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) mustEmbedUnimplementedGitserverServiceServer() {}
|
||||
|
||||
// UnsafeGitserverServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
@ -1714,6 +1821,45 @@ func (x *gitserverServiceChangedFilesServer) Send(m *ChangedFilesResponse) error
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func _GitserverService_Stat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StatRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GitserverServiceServer).Stat(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GitserverService_Stat_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GitserverServiceServer).Stat(ctx, req.(*StatRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GitserverService_ReadDir_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(ReadDirRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(GitserverServiceServer).ReadDir(m, &gitserverServiceReadDirServer{stream})
|
||||
}
|
||||
|
||||
type GitserverService_ReadDirServer interface {
|
||||
Send(*ReadDirResponse) error
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
type gitserverServiceReadDirServer struct {
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func (x *gitserverServiceReadDirServer) Send(m *ReadDirResponse) error {
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
// GitserverService_ServiceDesc is the grpc.ServiceDesc for GitserverService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -1805,6 +1951,10 @@ var GitserverService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "BehindAhead",
|
||||
Handler: _GitserverService_BehindAhead_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Stat",
|
||||
Handler: _GitserverService_Stat_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
@ -1852,6 +2002,11 @@ var GitserverService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _GitserverService_ChangedFiles_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "ReadDir",
|
||||
Handler: _GitserverService_ReadDir_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "gitserver.proto",
|
||||
}
|
||||
|
||||
@ -67,8 +67,8 @@ go_test(
|
||||
"//internal/database",
|
||||
"//internal/database/basestore",
|
||||
"//internal/database/dbtest",
|
||||
"//internal/fileutil",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/gitdomain",
|
||||
"//internal/observation",
|
||||
"//internal/own/types",
|
||||
"//internal/rcache",
|
||||
|
||||
@ -61,23 +61,26 @@ func (r *analyticsIndexer) indexRepo(ctx context.Context, repoId api.RepoID, che
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "repoStore.Get")
|
||||
}
|
||||
files, err := r.client.LsFiles(ctx, repo.Name, "HEAD")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "ls-files")
|
||||
}
|
||||
// Try to compute ownership stats
|
||||
commitID, err := r.client.ResolveRevision(ctx, repo.Name, "HEAD", gitserver.ResolveRevisionOptions{EnsureRevision: false})
|
||||
if err != nil {
|
||||
return errcode.MakeNonRetryable(errors.Wrapf(err, "cannot resolve HEAD"))
|
||||
}
|
||||
files, err := r.client.ReadDir(ctx, repo.Name, commitID, "", true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "ls-tree")
|
||||
}
|
||||
isOwnedViaCodeowners := r.codeowners(ctx, repo, commitID)
|
||||
isOwnedViaAssignedOwnership := r.assignedOwners(ctx, repo, commitID)
|
||||
var totalCount int
|
||||
var ownCounts database.PathAggregateCounts
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
totalCount++
|
||||
countCodeowners := isOwnedViaCodeowners(f)
|
||||
countAssignedOwnership := isOwnedViaAssignedOwnership(f)
|
||||
countCodeowners := isOwnedViaCodeowners(f.Name())
|
||||
countAssignedOwnership := isOwnedViaAssignedOwnership(f.Name())
|
||||
if countCodeowners {
|
||||
ownCounts.CodeownedFileCount++
|
||||
}
|
||||
@ -105,7 +108,7 @@ func (r *analyticsIndexer) indexRepo(ctx context.Context, repoId api.RepoID, che
|
||||
if rowCount == 0 {
|
||||
return errors.New("expected CODEOWNERS-owned file count update")
|
||||
}
|
||||
ownAnalyticsFilesCounter.Add(float64(len(files)))
|
||||
ownAnalyticsFilesCounter.Add(float64(totalCount))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@ -11,6 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/fileutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/rcache"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
@ -18,7 +20,6 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
)
|
||||
@ -29,8 +30,14 @@ type fakeGitServer struct {
|
||||
fileContents map[string]string
|
||||
}
|
||||
|
||||
func (f fakeGitServer) LsFiles(ctx context.Context, repo api.RepoName, commit api.CommitID, pathspecs ...gitdomain.Pathspec) ([]string, error) {
|
||||
return f.files, nil
|
||||
func (f fakeGitServer) ReadDir(ctx context.Context, repo api.RepoName, commit api.CommitID, path string, recursive bool) ([]fs.FileInfo, error) {
|
||||
fis := make([]fs.FileInfo, 0, len(f.files))
|
||||
for _, file := range f.files {
|
||||
fis = append(fis, &fileutil.FileInfo{
|
||||
Name_: file,
|
||||
})
|
||||
}
|
||||
return fis, nil
|
||||
}
|
||||
|
||||
func (f fakeGitServer) ResolveRevision(ctx context.Context, repo api.RepoName, spec string, opt gitserver.ResolveRevisionOptions) (api.CommitID, error) {
|
||||
|
||||
@ -148,6 +148,8 @@
|
||||
- GitserverService_RawDiffClient
|
||||
- GitserverService_ChangedFilesServer
|
||||
- GitserverService_ChangedFilesClient
|
||||
- GitserverService_ReadDirServer
|
||||
- GitserverService_ReadDirClient
|
||||
- filename: cmd/gitserver/internal/git/mock.go
|
||||
path: github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/git
|
||||
interfaces:
|
||||
@ -155,6 +157,7 @@
|
||||
- GitConfigBackend
|
||||
- BlameHunkReader
|
||||
- RefIterator
|
||||
- ReadDirIterator
|
||||
- filename: cmd/gitserver/internal/gitserverfs/mock.go
|
||||
path: github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/gitserverfs
|
||||
interfaces:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user