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.
1718type 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.
2833func 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
87124func (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}
0 commit comments