4
4
"bytes"
5
5
"errors"
6
6
"fmt"
7
+ "io/fs"
7
8
"os"
8
9
"os/exec"
9
10
"path/filepath"
@@ -15,26 +16,31 @@ type ObjectType string
15
16
16
17
// Repository represents a Git repository on disk.
17
18
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
19
23
20
24
// gitBin is the path of the `git` executable that should be used
21
25
// when running commands in this repository.
22
26
gitBin string
23
27
}
24
28
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.
28
33
func smartJoin (path , relPath string ) string {
29
34
if filepath .IsAbs (relPath ) {
30
35
return relPath
31
36
}
32
37
return filepath .Join (path , relPath )
33
38
}
34
39
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 ) {
38
44
// Find the `git` executable to be used:
39
45
gitBin , err := findGitBin ()
40
46
if err != nil {
@@ -43,6 +49,34 @@ func NewRepository(path string) (*Repository, error) {
43
49
)
44
50
}
45
51
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
+
46
80
//nolint:gosec // `gitBin` is chosen carefully, and `path` is the
47
81
// path to the repository.
48
82
cmd := exec .Command (gitBin , "-C" , path , "rev-parse" , "--git-dir" )
@@ -63,25 +97,28 @@ func NewRepository(path string) (*Repository, error) {
63
97
}
64
98
gitDir := smartJoin (path , string (bytes .TrimSpace (out )))
65
99
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" )
70
106
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
74
108
}
75
- shallow := smartJoin ( gitDir , string ( bytes . TrimSpace ( out )))
109
+
76
110
_ , err = os .Lstat (shallow )
77
111
if err == nil {
78
- return nil , errors . New ( "this appears to be a shallow clone; full clone required" )
112
+ return false , nil
79
113
}
80
114
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
85
122
}
86
123
87
124
func (repo * Repository ) GitCommand (callerArgs ... string ) * exec.Cmd {
@@ -103,15 +140,33 @@ func (repo *Repository) GitCommand(callerArgs ...string) *exec.Cmd {
103
140
104
141
cmd .Env = append (
105
142
os .Environ (),
106
- "GIT_DIR=" + repo .path ,
143
+ "GIT_DIR=" + repo .gitDir ,
107
144
// Disable grafts when running our commands:
108
145
"GIT_GRAFT_FILE=" + os .DevNull ,
109
146
)
110
147
111
148
return cmd
112
149
}
113
150
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
117
172
}
0 commit comments