gitserver: Implement Stat and ReadDir in gRPC API (#62107)

This commit is contained in:
Erik Seliger 2024-05-11 00:58:28 +02:00 committed by GitHub
parent 7b6dd9080e
commit 824337ae6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 6774 additions and 3481 deletions

View File

@ -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) {

View File

@ -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)

View File

@ -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) {

View File

@ -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",

View File

@ -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",

View File

@ -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) }

View File

@ -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())
})
}

View File

@ -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
}

View File

@ -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

View File

@ -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"),
}
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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 {

View File

@ -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",

View File

@ -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)
}

View File

@ -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,

View File

@ -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}
}

View File

@ -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
}

View File

@ -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{}

View File

@ -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

View File

@ -63,6 +63,7 @@ go_test(
"//internal/database/basestore",
"//internal/database/dbmocks",
"//internal/executor",
"//internal/fileutil",
"//internal/gitserver",
"//internal/gitserver/gitdomain",
"//internal/observation",

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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] }

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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())
})
}

View File

@ -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{}

View File

@ -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",

View File

@ -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) }

View File

@ -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

View File

@ -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 {

View File

@ -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"),

View File

@ -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{}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -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",
}

View File

@ -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",

View File

@ -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
}

View File

@ -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) {

View File

@ -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: