Skip to content

Commit b84ee4d

Browse files
authored
Merge pull request #117 from github/improve-repository-detection
Improve repository detection and deal with `safe.bareRepository=explicit`
2 parents a4fb754 + fb78b41 commit b84ee4d

File tree

5 files changed

+126
-44
lines changed

5 files changed

+126
-44
lines changed

git-sizer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func mainImplementation(ctx context.Context, stdout, stderr io.Writer, args []st
134134

135135
// Try to open the repository, but it's not an error yet if this
136136
// fails, because the user might only be asking for `--help`.
137-
repo, repoErr := git.NewRepository(".")
137+
repo, repoErr := git.NewRepositoryFromPath(".")
138138

139139
flags := pflag.NewFlagSet("git-sizer", pflag.ContinueOnError)
140140
flags.Usage = func() {

git/git.go

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"fmt"
7+
"io/fs"
78
"os"
89
"os/exec"
910
"path/filepath"
@@ -15,26 +16,31 @@ type ObjectType string
1516

1617
// Repository represents a Git repository on disk.
1718
type Repository struct {
18-
path string
19+
// gitDir is the path to the `GIT_DIR` for this repository. It
20+
// might be absolute or it might be relative to the current
21+
// directory.
22+
gitDir string
1923

2024
// gitBin is the path of the `git` executable that should be used
2125
// when running commands in this repository.
2226
gitBin string
2327
}
2428

25-
// smartJoin returns the path that can be described as `relPath`
26-
// relative to `path`, given that `path` is either absolute or is
27-
// relative to the current directory.
29+
// smartJoin returns `relPath` if it is an absolute path. If not, it
30+
// assumes that `relPath` is relative to `path`, so it joins them
31+
// together and returns the result. In that case, if `path` itself is
32+
// relative, then the return value is also relative.
2833
func smartJoin(path, relPath string) string {
2934
if filepath.IsAbs(relPath) {
3035
return relPath
3136
}
3237
return filepath.Join(path, relPath)
3338
}
3439

35-
// NewRepository creates a new repository object that can be used for
36-
// running `git` commands within that repository.
37-
func NewRepository(path string) (*Repository, error) {
40+
// NewRepositoryFromGitDir creates a new `Repository` object that can
41+
// be used for running `git` commands, given the value of `GIT_DIR`
42+
// for the repository.
43+
func NewRepositoryFromGitDir(gitDir string) (*Repository, error) {
3844
// Find the `git` executable to be used:
3945
gitBin, err := findGitBin()
4046
if err != nil {
@@ -43,6 +49,34 @@ func NewRepository(path string) (*Repository, error) {
4349
)
4450
}
4551

52+
repo := Repository{
53+
gitDir: gitDir,
54+
gitBin: gitBin,
55+
}
56+
57+
full, err := repo.IsFull()
58+
if err != nil {
59+
return nil, fmt.Errorf("determining whether the repository is a full clone: %w", err)
60+
}
61+
if !full {
62+
return nil, errors.New("this appears to be a shallow clone; full clone required")
63+
}
64+
65+
return &repo, nil
66+
}
67+
68+
// NewRepositoryFromPath creates a new `Repository` object that can be
69+
// used for running `git` commands within `path`. It does so by asking
70+
// `git` what `GIT_DIR` to use. Git, in turn, bases its decision on
71+
// the path and the environment.
72+
func NewRepositoryFromPath(path string) (*Repository, error) {
73+
gitBin, err := findGitBin()
74+
if err != nil {
75+
return nil, fmt.Errorf(
76+
"could not find 'git' executable (is it in your PATH?): %w", err,
77+
)
78+
}
79+
4680
//nolint:gosec // `gitBin` is chosen carefully, and `path` is the
4781
// path to the repository.
4882
cmd := exec.Command(gitBin, "-C", path, "rev-parse", "--git-dir")
@@ -63,25 +97,28 @@ func NewRepository(path string) (*Repository, error) {
6397
}
6498
gitDir := smartJoin(path, string(bytes.TrimSpace(out)))
6599

66-
//nolint:gosec // `gitBin` is chosen carefully.
67-
cmd = exec.Command(gitBin, "rev-parse", "--git-path", "shallow")
68-
cmd.Dir = gitDir
69-
out, err = cmd.Output()
100+
return NewRepositoryFromGitDir(gitDir)
101+
}
102+
103+
// IsFull returns `true` iff `repo` appears to be a full clone.
104+
func (repo *Repository) IsFull() (bool, error) {
105+
shallow, err := repo.GitPath("shallow")
70106
if err != nil {
71-
return nil, fmt.Errorf(
72-
"could not run 'git rev-parse --git-path shallow': %w", err,
73-
)
107+
return false, err
74108
}
75-
shallow := smartJoin(gitDir, string(bytes.TrimSpace(out)))
109+
76110
_, err = os.Lstat(shallow)
77111
if err == nil {
78-
return nil, errors.New("this appears to be a shallow clone; full clone required")
112+
return false, nil
79113
}
80114

81-
return &Repository{
82-
path: gitDir,
83-
gitBin: gitBin,
84-
}, nil
115+
if !errors.Is(err, fs.ErrNotExist) {
116+
return false, err
117+
}
118+
119+
// The `shallow` file is absent, which is what we expect
120+
// for a full clone.
121+
return true, nil
85122
}
86123

87124
func (repo *Repository) GitCommand(callerArgs ...string) *exec.Cmd {
@@ -103,15 +140,33 @@ func (repo *Repository) GitCommand(callerArgs ...string) *exec.Cmd {
103140

104141
cmd.Env = append(
105142
os.Environ(),
106-
"GIT_DIR="+repo.path,
143+
"GIT_DIR="+repo.gitDir,
107144
// Disable grafts when running our commands:
108145
"GIT_GRAFT_FILE="+os.DevNull,
109146
)
110147

111148
return cmd
112149
}
113150

114-
// Path returns the path to `repo`.
115-
func (repo *Repository) Path() string {
116-
return repo.path
151+
// GitDir returns the path to `repo`'s `GIT_DIR`. It might be absolute
152+
// or it might be relative to the current directory.
153+
func (repo *Repository) GitDir() string {
154+
return repo.gitDir
155+
}
156+
157+
// GitPath returns that path of a file within the git repository, by
158+
// calling `git rev-parse --git-path $relPath`. The returned path is
159+
// relative to the current directory.
160+
func (repo *Repository) GitPath(relPath string) (string, error) {
161+
cmd := repo.GitCommand("rev-parse", "--git-path", relPath)
162+
out, err := cmd.Output()
163+
if err != nil {
164+
return "", fmt.Errorf(
165+
"running 'git rev-parse --git-path %s': %w", relPath, err,
166+
)
167+
}
168+
// `git rev-parse --git-path` is documented to return the path
169+
// relative to the current directory. Since we haven't changed the
170+
// current directory, we can use it as-is:
171+
return string(bytes.TrimSpace(out)), nil
117172
}

git/git_bin.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,41 @@ package git
22

33
import (
44
"path/filepath"
5+
"sync"
56

67
"github.com/cli/safeexec"
78
)
89

10+
// This variable will be used to memoize the result of `findGitBin()`,
11+
// since its return value only depends on the environment.
12+
var gitBinMemo struct {
13+
once sync.Once
14+
15+
gitBin string
16+
err error
17+
}
18+
919
// findGitBin finds the `git` binary in PATH that should be used by
1020
// the rest of `git-sizer`. It uses `safeexec` to find the executable,
1121
// because on Windows, `exec.Cmd` looks not only in PATH, but also in
1222
// the current directory. This is a potential risk if the repository
1323
// being scanned is hostile and non-bare because it might possibly
1424
// contain an executable file named `git`.
1525
func findGitBin() (string, error) {
16-
gitBin, err := safeexec.LookPath("git")
17-
if err != nil {
18-
return "", err
19-
}
26+
gitBinMemo.once.Do(func() {
27+
p, err := safeexec.LookPath("git")
28+
if err != nil {
29+
gitBinMemo.err = err
30+
return
31+
}
2032

21-
gitBin, err = filepath.Abs(gitBin)
22-
if err != nil {
23-
return "", err
24-
}
33+
p, err = filepath.Abs(p)
34+
if err != nil {
35+
gitBinMemo.err = err
36+
return
37+
}
2538

26-
return gitBin, nil
39+
gitBinMemo.gitBin = p
40+
})
41+
return gitBinMemo.gitBin, gitBinMemo.err
2742
}

git_sizer_test.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9-
"io/ioutil"
109
"os"
1110
"os/exec"
1211
"path/filepath"
@@ -273,7 +272,10 @@ func TestRefSelections(t *testing.T) {
273272
args := []string{"--show-refs", "--no-progress", "--json", "--json-version=2"}
274273
args = append(args, p.args...)
275274
cmd := exec.Command(executable, args...)
276-
cmd.Dir = repo.Path
275+
cmd.Env = append(
276+
os.Environ(),
277+
"GIT_DIR="+repo.Path,
278+
)
277279
var stdout bytes.Buffer
278280
cmd.Stdout = &stdout
279281
var stderr bytes.Buffer
@@ -520,7 +522,10 @@ References (included references marked with '+'):
520522

521523
args := append([]string{"--show-refs", "-v", "--no-progress"}, p.args...)
522524
cmd := exec.Command(executable, args...)
523-
cmd.Dir = repo.Path
525+
cmd.Env = append(
526+
os.Environ(),
527+
"GIT_DIR="+repo.Path,
528+
)
524529
var stdout bytes.Buffer
525530
cmd.Stdout = &stdout
526531
var stderr bytes.Buffer
@@ -760,7 +765,7 @@ func TestSubmodule(t *testing.T) {
760765

761766
ctx := context.Background()
762767

763-
tmp, err := ioutil.TempDir("", "submodule")
768+
tmp, err := os.MkdirTemp("", "submodule")
764769
require.NoError(t, err, "creating temporary directory")
765770

766771
defer func() {

internal/testutils/repoutils.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"fmt"
66
"io"
7-
"io/ioutil"
87
"os"
98
"os/exec"
109
"path/filepath"
@@ -21,6 +20,7 @@ import (
2120
// TestRepo represents a git repository used for tests.
2221
type TestRepo struct {
2322
Path string
23+
bare bool
2424
}
2525

2626
// NewTestRepo creates and initializes a test repository in a
@@ -29,7 +29,7 @@ type TestRepo struct {
2929
func NewTestRepo(t *testing.T, bare bool, pattern string) *TestRepo {
3030
t.Helper()
3131

32-
path, err := ioutil.TempDir("", pattern)
32+
path, err := os.MkdirTemp("", pattern)
3333
require.NoError(t, err)
3434

3535
repo := TestRepo{Path: path}
@@ -38,6 +38,7 @@ func NewTestRepo(t *testing.T, bare bool, pattern string) *TestRepo {
3838

3939
return &TestRepo{
4040
Path: path,
41+
bare: bare,
4142
}
4243
}
4344

@@ -73,7 +74,7 @@ func (repo *TestRepo) Remove(t *testing.T) {
7374
func (repo *TestRepo) Clone(t *testing.T, pattern string) *TestRepo {
7475
t.Helper()
7576

76-
path, err := ioutil.TempDir("", pattern)
77+
path, err := os.MkdirTemp("", pattern)
7778
require.NoError(t, err)
7879

7980
err = repo.GitCommand(
@@ -90,9 +91,15 @@ func (repo *TestRepo) Clone(t *testing.T, pattern string) *TestRepo {
9091
func (repo *TestRepo) Repository(t *testing.T) *git.Repository {
9192
t.Helper()
9293

93-
r, err := git.NewRepository(repo.Path)
94-
require.NoError(t, err)
95-
return r
94+
if repo.bare {
95+
r, err := git.NewRepositoryFromGitDir(repo.Path)
96+
require.NoError(t, err)
97+
return r
98+
} else {
99+
r, err := git.NewRepositoryFromPath(repo.Path)
100+
require.NoError(t, err)
101+
return r
102+
}
96103
}
97104

98105
// localEnvVars is a list of the variable names that should be cleared

0 commit comments

Comments
 (0)