mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
Move GetDefaultBranch and GetDefaultBranchShort to gitserver client (#35804)
* Move GetDefaultBranch to gitserver client fix mocks * fix unit test
This commit is contained in:
parent
d7b228c66b
commit
404d5ed10d
@ -12,10 +12,10 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/errcode"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/repoupdater/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace/ot"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/internal/vcs/git"
|
||||
)
|
||||
|
||||
// Repository returns the external links for a repository.
|
||||
@ -43,7 +43,7 @@ func FileOrDir(ctx context.Context, db database.DB, repo *types.Repo, rev, path
|
||||
phabRepo, link, serviceType := linksForRepository(ctx, db, repo)
|
||||
if phabRepo != nil {
|
||||
// We need a branch name to construct the Phabricator URL.
|
||||
branchName, _, err := git.GetDefaultBranchShort(ctx, db, repo.Name)
|
||||
branchName, _, err := gitserver.NewClient(db).GetDefaultBranchShort(ctx, repo.Name)
|
||||
if err == nil && branchName != "" {
|
||||
links = append(links, NewResolver(
|
||||
fmt.Sprintf("%s/source/%s/browse/%s/%s;%s", strings.TrimSuffix(phabRepo.URL, "/"), phabRepo.Callsign, url.PathEscape(branchName), path, rev),
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/gitlab"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/internal/vcs/git"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -164,7 +165,7 @@ func TestFileOrDir(t *testing.T) {
|
||||
db := database.NewMockDB()
|
||||
db.PhabricatorFunc.SetDefaultReturn(phabricator)
|
||||
|
||||
git.Mocks.GetDefaultBranchShort = func(repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
gitserver.Mocks.GetDefaultBranchShort = func(repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
return "mybranch", "", nil
|
||||
}
|
||||
defer git.ResetMocks()
|
||||
|
||||
@ -23,7 +23,6 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/search/result"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace/ot"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/internal/vcs/git"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
@ -195,7 +194,7 @@ func (r *RepositoryResolver) CommitFromID(ctx context.Context, args *RepositoryC
|
||||
|
||||
func (r *RepositoryResolver) DefaultBranch(ctx context.Context) (*GitRefResolver, error) {
|
||||
do := func() (*GitRefResolver, error) {
|
||||
refName, _, err := git.GetDefaultBranch(ctx, r.db, r.RepoName())
|
||||
refName, _, err := gitserver.NewClient(r.db).GetDefaultBranch(ctx, r.RepoName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -18,7 +18,6 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/search/result"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/internal/vcs/git"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
@ -206,11 +205,11 @@ func TestRepository_DefaultBranch(t *testing.T) {
|
||||
}
|
||||
for _, tt := range ts {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
git.Mocks.ExecSafe = func(params []string) (stdout []byte, stderr []byte, exitCode int, err error) {
|
||||
gitserver.Mocks.ExecSafe = func(params []string) (stdout []byte, stderr []byte, exitCode int, err error) {
|
||||
return []byte(tt.symbolicRef), nil, tt.symbolicRefExitCode, tt.symbolicRefErr
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
git.Mocks.ExecSafe = nil
|
||||
gitserver.Mocks.ExecSafe = nil
|
||||
})
|
||||
|
||||
gitserver.Mocks.ResolveRevision = func(spec string, opt gitserver.ResolveRevisionOptions) (api.CommitID, error) {
|
||||
|
||||
@ -46,7 +46,7 @@ func TestRetrievingAndDeduplicatingIndexedRefs(t *testing.T) {
|
||||
}
|
||||
return api.CommitID("deadbeef"), nil
|
||||
}
|
||||
git.Mocks.ExecSafe = func(params []string) (stdout, stderr []byte, exitCode int, err error) {
|
||||
gitserver.Mocks.ExecSafe = func(params []string) (stdout, stderr []byte, exitCode int, err error) {
|
||||
// Mock default branch lookup in (*RepsitoryResolver).DefaultBranch.
|
||||
return []byte(defaultBranchRef), nil, 0, nil
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/goroutine"
|
||||
"github.com/sourcegraph/sourcegraph/internal/inventory"
|
||||
"github.com/sourcegraph/sourcegraph/internal/search/job/jobutil"
|
||||
@ -17,7 +18,6 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/search/result"
|
||||
"github.com/sourcegraph/sourcegraph/internal/search/streaming"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/internal/vcs/git"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
@ -131,7 +131,7 @@ func searchResultsStatsLanguages(ctx context.Context, db database.DB, matches []
|
||||
defer run.Release()
|
||||
|
||||
repoName := repoMatch.RepoName()
|
||||
_, oid, err := git.GetDefaultBranch(ctx, db, repoName.Name)
|
||||
_, oid, err := gitserver.NewClient(db).GetDefaultBranch(ctx, repoName.Name)
|
||||
if err != nil {
|
||||
run.Error(err)
|
||||
return
|
||||
|
||||
@ -40,7 +40,7 @@ func TestSearchResultsStatsLanguages(t *testing.T) {
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
const wantDefaultBranchRef = "refs/heads/foo"
|
||||
git.Mocks.ExecSafe = func(params []string) (stdout, stderr []byte, exitCode int, err error) {
|
||||
gitserver.Mocks.ExecSafe = func(params []string) (stdout, stderr []byte, exitCode int, err error) {
|
||||
// Mock default branch lookup in (*RepositoryResolver).DefaultBranch.
|
||||
return []byte(wantDefaultBranchRef), nil, 0, nil
|
||||
}
|
||||
|
||||
@ -428,7 +428,7 @@ func repoToRepoRevisionWithDefaultBranch(ctx context.Context, db database.DB, re
|
||||
tr.Finish()
|
||||
}()
|
||||
|
||||
branch, commit, err := git.GetDefaultBranch(ctx, db, repo.Name)
|
||||
branch, commit, err := gitserver.NewClient(db).GetDefaultBranch(ctx, repo.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -451,13 +451,13 @@ type defaultBranch struct {
|
||||
}
|
||||
|
||||
func mockDefaultBranches(t *testing.T, defaultBranches map[api.RepoName]defaultBranch) {
|
||||
git.Mocks.GetDefaultBranch = func(repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
gitserver.Mocks.GetDefaultBranch = func(repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
if res, ok := defaultBranches[repo]; ok {
|
||||
return res.branch, res.commit, nil
|
||||
}
|
||||
return "", "", &gitdomain.RepoNotExistError{Repo: repo}
|
||||
}
|
||||
t.Cleanup(func() { git.Mocks.GetDefaultBranch = nil })
|
||||
t.Cleanup(func() { gitserver.Mocks.GetDefaultBranch = nil })
|
||||
}
|
||||
|
||||
func mockBatchIgnores(t *testing.T, m map[api.CommitID]bool) {
|
||||
|
||||
@ -1124,3 +1124,91 @@ func parseTags(in []byte) ([]*gitdomain.Tag, error) {
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetDefaultBranch returns the name of the default branch and the commit it's
|
||||
// currently at from the given repository.
|
||||
//
|
||||
// If the repository is empty or currently being cloned, empty values and no
|
||||
// error are returned.
|
||||
func (c *ClientImplementor) GetDefaultBranch(ctx context.Context, repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
if Mocks.GetDefaultBranch != nil {
|
||||
return Mocks.GetDefaultBranch(repo)
|
||||
}
|
||||
return c.getDefaultBranch(ctx, repo, false)
|
||||
}
|
||||
|
||||
// GetDefaultBranchShort returns the short name of the default branch for the
|
||||
// given repository and the commit it's currently at. A short name would return
|
||||
// something like `main` instead of `refs/heads/main`.
|
||||
//
|
||||
// If the repository is empty or currently being cloned, empty values and no
|
||||
// error are returned.
|
||||
func (c *ClientImplementor) GetDefaultBranchShort(ctx context.Context, repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
if Mocks.GetDefaultBranchShort != nil {
|
||||
return Mocks.GetDefaultBranchShort(repo)
|
||||
}
|
||||
return c.getDefaultBranch(ctx, repo, true)
|
||||
}
|
||||
|
||||
// GetDefaultBranch returns the name of the default branch and the commit it's
|
||||
// currently at from the given repository.
|
||||
//
|
||||
// If the repository is empty or currently being cloned, empty values and no
|
||||
// error are returned.
|
||||
func (c *ClientImplementor) getDefaultBranch(ctx context.Context, repo api.RepoName, short bool) (refName string, commit api.CommitID, err error) {
|
||||
args := []string{"symbolic-ref", "HEAD"}
|
||||
if short {
|
||||
args = append(args, "--short")
|
||||
}
|
||||
refBytes, _, exitCode, err := c.execSafe(ctx, repo, args)
|
||||
refName = string(bytes.TrimSpace(refBytes))
|
||||
|
||||
if err == nil && exitCode == 0 {
|
||||
// Check that our repo is not empty
|
||||
commit, err = c.ResolveRevision(ctx, repo, "HEAD", ResolveRevisionOptions{NoEnsureRevision: true})
|
||||
}
|
||||
|
||||
// If we fail to get the default branch due to cloning or being empty, we return nothing.
|
||||
if err != nil {
|
||||
if gitdomain.IsCloneInProgress(err) || errors.HasType(err, &gitdomain.RevisionNotFoundError{}) {
|
||||
return "", "", nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return refName, commit, nil
|
||||
}
|
||||
|
||||
// execSafe executes a Git subcommand iff it is allowed according to a allowlist.
|
||||
//
|
||||
// An error is only returned when there is a failure unrelated to the actual
|
||||
// command being executed. If the executed command exits with a nonzero exit
|
||||
// code, err == nil. This is similar to how http.Get returns a nil error for HTTP
|
||||
// non-2xx responses.
|
||||
//
|
||||
// execSafe should NOT be exported. We want to limit direct git calls to this
|
||||
// package.
|
||||
func (c *ClientImplementor) execSafe(ctx context.Context, repo api.RepoName, params []string) (stdout, stderr []byte, exitCode int, err error) {
|
||||
if Mocks.ExecSafe != nil {
|
||||
return Mocks.ExecSafe(params)
|
||||
}
|
||||
|
||||
span, ctx := ot.StartSpanFromContext(ctx, "Git: execSafe")
|
||||
defer span.Finish()
|
||||
|
||||
if len(params) == 0 {
|
||||
return nil, nil, 0, errors.New("at least one argument required")
|
||||
}
|
||||
|
||||
if !gitdomain.IsAllowedGitCmd(params) {
|
||||
return nil, nil, 0, errors.Errorf("command failed: %q is not a allowed git command", params)
|
||||
}
|
||||
|
||||
cmd := c.GitCommand(repo, params...)
|
||||
stdout, stderr, err = cmd.DividedOutput(ctx)
|
||||
exitCode = cmd.ExitStatus()
|
||||
if exitCode != 0 && err != nil {
|
||||
err = nil // the error must just indicate that the exit code was nonzero
|
||||
}
|
||||
return stdout, stderr, exitCode, err
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package gitserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
@ -817,3 +818,77 @@ func TestParseTags_WithoutCreatorDate(t *testing.T) {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecSafe(t *testing.T) {
|
||||
ClientMocks.LocalGitserver = true
|
||||
defer ResetClientMocks()
|
||||
|
||||
tests := []struct {
|
||||
args []string
|
||||
wantStdout, wantStderr string
|
||||
wantExitCode int
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?"},
|
||||
wantStdout: "ea167fe3d76b1e5fd3ed8ca44cbd2fe3897684f8 -\nauthor a\nauthor-date 2006-01-02 15:04:05 +0000\nparents \nsummary foo\n\nfilename ?\n",
|
||||
},
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?", "-m", "-i", "-n200", "--author=a@a.com"},
|
||||
wantStdout: "ea167fe3d76b1e5fd3ed8ca44cbd2fe3897684f8 -\nauthor a\nauthor-date 2006-01-02 15:04:05 +0000\nparents \nsummary foo\n\nfilename ?\n",
|
||||
},
|
||||
{
|
||||
args: []string{"show"},
|
||||
wantStdout: "commit ea167fe3d76b1e5fd3ed8ca44cbd2fe3897684f8\nAuthor: a <a@a.com>\nDate: Mon Jan 2 15:04:05 2006 +0000\n\n foo\n",
|
||||
},
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?", ";show"},
|
||||
wantStderr: "fatal: ambiguous argument ';show': unknown revision or path not in the working tree.\nUse '--' to separate paths from revisions, like this:\n'git <command> [<revision>...] -- [<file>...]'\n",
|
||||
wantExitCode: 128,
|
||||
},
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?;", "show"},
|
||||
wantStderr: "fatal: ambiguous argument 'show': unknown revision or path not in the working tree.\nUse '--' to separate paths from revisions, like this:\n'git <command> [<revision>...] -- [<file>...]'\n",
|
||||
wantExitCode: 128,
|
||||
},
|
||||
{
|
||||
args: []string{"rm"},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
args: []string{"checkout"},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
args: []string{"show;", "echo", "hello"},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
repo := MakeGitRepository(t, "GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit --allow-empty -m foo --author='a <a@a.com>' --date 2006-01-02T15:04:05Z")
|
||||
|
||||
client := NewClient(database.NewMockDB())
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprint(test.args), func(t *testing.T) {
|
||||
stdout, stderr, exitCode, err := client.execSafe(context.Background(), repo, test.args)
|
||||
if err == nil && test.wantError {
|
||||
t.Errorf("got error %v, want error %v", err, test.wantError)
|
||||
}
|
||||
if test.wantError {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(stdout) != test.wantStdout {
|
||||
t.Errorf("got stdout %q, want %q", stdout, test.wantStdout)
|
||||
}
|
||||
if string(stderr) != test.wantStderr {
|
||||
t.Errorf("got stderr %q, want %q", stderr, test.wantStderr)
|
||||
}
|
||||
if exitCode != test.wantExitCode {
|
||||
t.Errorf("got exitCode %d, want %d", exitCode, test.wantExitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,10 +17,13 @@ import (
|
||||
// that's done, we should take advantage of the generated mock client in this
|
||||
// package instead.
|
||||
var Mocks, emptyMocks struct {
|
||||
ExecReader func(args []string) (reader io.ReadCloser, err error)
|
||||
ReadDir func(commit api.CommitID, name string, recurse bool) ([]fs.FileInfo, error)
|
||||
ResolveRevision func(spec string, opt ResolveRevisionOptions) (api.CommitID, error)
|
||||
LsFiles func(repo api.RepoName, commit api.CommitID) ([]string, error)
|
||||
ExecReader func(args []string) (reader io.ReadCloser, err error)
|
||||
ExecSafe func(params []string) (stdout, stderr []byte, exitCode int, err error)
|
||||
ReadDir func(commit api.CommitID, name string, recurse bool) ([]fs.FileInfo, error)
|
||||
ResolveRevision func(spec string, opt ResolveRevisionOptions) (api.CommitID, error)
|
||||
LsFiles func(repo api.RepoName, commit api.CommitID) ([]string, error)
|
||||
GetDefaultBranch func(repo api.RepoName) (refName string, commit api.CommitID, err error)
|
||||
GetDefaultBranchShort func(repo api.RepoName) (refName string, commit api.CommitID, err error)
|
||||
}
|
||||
|
||||
// ResetMocks clears the mock functions set on Mocks (so that subsequent tests don't inadvertently
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
// GetDefaultBranch returns the name of the default branch and the commit it's
|
||||
// currently at from the given repository.
|
||||
//
|
||||
// If the repository is empty or currently being cloned, empty values and no
|
||||
// error are returned.
|
||||
func GetDefaultBranch(ctx context.Context, db database.DB, repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
if Mocks.GetDefaultBranch != nil {
|
||||
return Mocks.GetDefaultBranch(repo)
|
||||
}
|
||||
return getDefaultBranch(ctx, db, repo, false)
|
||||
}
|
||||
|
||||
// GetDefaultBranchShort returns the short name of the default branch for the
|
||||
// given repository and the commit it's currently at. A short name would return
|
||||
// something like `main` instead of `refs/heads/main`.
|
||||
//
|
||||
// If the repository is empty or currently being cloned, empty values and no
|
||||
// error are returned.
|
||||
func GetDefaultBranchShort(ctx context.Context, db database.DB, repo api.RepoName) (refName string, commit api.CommitID, err error) {
|
||||
if Mocks.GetDefaultBranchShort != nil {
|
||||
return Mocks.GetDefaultBranchShort(repo)
|
||||
}
|
||||
return getDefaultBranch(ctx, db, repo, true)
|
||||
}
|
||||
|
||||
// GetDefaultBranch returns the name of the default branch and the commit it's
|
||||
// currently at from the given repository.
|
||||
//
|
||||
// If the repository is empty or currently being cloned, empty values and no
|
||||
// error are returned.
|
||||
func getDefaultBranch(ctx context.Context, db database.DB, repo api.RepoName, short bool) (refName string, commit api.CommitID, err error) {
|
||||
args := []string{"symbolic-ref", "HEAD"}
|
||||
if short {
|
||||
args = append(args, "--short")
|
||||
}
|
||||
refBytes, _, exitCode, err := execSafe(ctx, db, repo, args)
|
||||
refName = string(bytes.TrimSpace(refBytes))
|
||||
|
||||
if err == nil && exitCode == 0 {
|
||||
// Check that our repo is not empty
|
||||
commit, err = gitserver.NewClient(db).ResolveRevision(ctx, repo, "HEAD", gitserver.ResolveRevisionOptions{NoEnsureRevision: true})
|
||||
}
|
||||
|
||||
// If we fail to get the default branch due to cloning or being empty, we return nothing.
|
||||
if err != nil {
|
||||
if gitdomain.IsCloneInProgress(err) || errors.HasType(err, &gitdomain.RevisionNotFoundError{}) {
|
||||
return "", "", nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return refName, commit, nil
|
||||
}
|
||||
@ -1,51 +1,11 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace/ot"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
// execSafe executes a Git subcommand iff it is allowed according to a allowlist.
|
||||
//
|
||||
// An error is only returned when there is a failure unrelated to the actual
|
||||
// command being executed. If the executed command exits with a nonzero exit
|
||||
// code, err == nil. This is similar to how http.Get returns a nil error for HTTP
|
||||
// non-2xx responses.
|
||||
//
|
||||
// execSafe should NOT be exported. We want to limit direct git calls to this
|
||||
// package.
|
||||
func execSafe(ctx context.Context, db database.DB, repo api.RepoName, params []string) (stdout, stderr []byte, exitCode int, err error) {
|
||||
if Mocks.ExecSafe != nil {
|
||||
return Mocks.ExecSafe(params)
|
||||
}
|
||||
|
||||
span, ctx := ot.StartSpanFromContext(ctx, "Git: execSafe")
|
||||
defer span.Finish()
|
||||
|
||||
if len(params) == 0 {
|
||||
return nil, nil, 0, errors.New("at least one argument required")
|
||||
}
|
||||
|
||||
if !gitdomain.IsAllowedGitCmd(params) {
|
||||
return nil, nil, 0, errors.Errorf("command failed: %q is not a allowed git command", params)
|
||||
}
|
||||
|
||||
cmd := gitserver.NewClient(db).GitCommand(repo, params...)
|
||||
stdout, stderr, err = cmd.DividedOutput(ctx)
|
||||
exitCode = cmd.ExitStatus()
|
||||
if exitCode != 0 && err != nil {
|
||||
err = nil // the error must just indicate that the exit code was nonzero
|
||||
}
|
||||
return stdout, stderr, exitCode, err
|
||||
}
|
||||
|
||||
// checkSpecArgSafety returns a non-nil err if spec begins with a "-", which
|
||||
// could cause it to be interpreted as a git command line argument.
|
||||
func checkSpecArgSafety(spec string) error {
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
)
|
||||
|
||||
func TestExecSafe(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
args []string
|
||||
wantStdout, wantStderr string
|
||||
wantExitCode int
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?"},
|
||||
wantStdout: "ea167fe3d76b1e5fd3ed8ca44cbd2fe3897684f8 -\nauthor a\nauthor-date 2006-01-02 15:04:05 +0000\nparents \nsummary foo\n\nfilename ?\n",
|
||||
},
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?", "-m", "-i", "-n200", "--author=a@a.com"},
|
||||
wantStdout: "ea167fe3d76b1e5fd3ed8ca44cbd2fe3897684f8 -\nauthor a\nauthor-date 2006-01-02 15:04:05 +0000\nparents \nsummary foo\n\nfilename ?\n",
|
||||
},
|
||||
{
|
||||
args: []string{"show"},
|
||||
wantStdout: "commit ea167fe3d76b1e5fd3ed8ca44cbd2fe3897684f8\nAuthor: a <a@a.com>\nDate: Mon Jan 2 15:04:05 2006 +0000\n\n foo\n",
|
||||
},
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?", ";show"},
|
||||
wantStderr: "fatal: ambiguous argument ';show': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this: 'git <command> [<revision>...] -- [<file>...]'",
|
||||
wantExitCode: 128,
|
||||
},
|
||||
{
|
||||
args: []string{"log", "--name-status", "--full-history", "-M", "--date=iso8601", "--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?;", "show"},
|
||||
wantStderr: "fatal: ambiguous argument 'show': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this: 'git <command> [<revision>...] -- [<file>...]'",
|
||||
wantExitCode: 128,
|
||||
},
|
||||
{
|
||||
args: []string{"rm"},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
args: []string{"checkout"},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
args: []string{"show;", "echo", "hello"},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
repo := MakeGitRepository(t, "GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit --allow-empty -m foo --author='a <a@a.com>' --date 2006-01-02T15:04:05Z")
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprint(test.args), func(t *testing.T) {
|
||||
stdout, stderr, exitCode, err := execSafe(context.Background(), database.NewMockDB(), repo, test.args)
|
||||
if err == nil && test.wantError {
|
||||
t.Errorf("got error %v, want error %v", err, test.wantError)
|
||||
}
|
||||
if test.wantError {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(stdout) != test.wantStdout {
|
||||
t.Errorf("got stdout %q, want %q", stdout, test.wantStdout)
|
||||
}
|
||||
if string(stderr) != test.wantStderr {
|
||||
t.Errorf("got stderr %q, want %q", stderr, test.wantStderr)
|
||||
}
|
||||
if exitCode != test.wantExitCode {
|
||||
t.Errorf("got exitCode %d, want %d", exitCode, test.wantExitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -13,16 +13,13 @@ import (
|
||||
//
|
||||
// (The emptyMocks is used by ResetMocks to zero out Mocks without needing to use a named type.)
|
||||
var Mocks, emptyMocks struct {
|
||||
GetCommit func(api.CommitID) (*gitdomain.Commit, error)
|
||||
ExecSafe func(params []string) (stdout, stderr []byte, exitCode int, err error)
|
||||
ExecReader func(args []string) (reader io.ReadCloser, err error)
|
||||
NewFileReader func(commit api.CommitID, name string) (io.ReadCloser, error)
|
||||
ReadFile func(commit api.CommitID, name string) ([]byte, error)
|
||||
Stat func(commit api.CommitID, name string) (fs.FileInfo, error)
|
||||
Commits func(repo api.RepoName, opt CommitsOptions) ([]*gitdomain.Commit, error)
|
||||
MergeBase func(repo api.RepoName, a, b api.CommitID) (api.CommitID, error)
|
||||
GetDefaultBranch func(repo api.RepoName) (refName string, commit api.CommitID, err error)
|
||||
GetDefaultBranchShort func(repo api.RepoName) (refName string, commit api.CommitID, err error)
|
||||
GetCommit func(api.CommitID) (*gitdomain.Commit, error)
|
||||
ExecReader func(args []string) (reader io.ReadCloser, err error)
|
||||
NewFileReader func(commit api.CommitID, name string) (io.ReadCloser, error)
|
||||
ReadFile func(commit api.CommitID, name string) ([]byte, error)
|
||||
Stat func(commit api.CommitID, name string) (fs.FileInfo, error)
|
||||
Commits func(repo api.RepoName, opt CommitsOptions) ([]*gitdomain.Commit, error)
|
||||
MergeBase func(repo api.RepoName, a, b api.CommitID) (api.CommitID, error)
|
||||
}
|
||||
|
||||
// ResetMocks clears the mock functions set on Mocks (so that subsequent tests don't inadvertently
|
||||
|
||||
Loading…
Reference in New Issue
Block a user