// A Cmd describes how to use a version control system
// like Mercurial, Git, or Subversion.
type Cmd struct {
- Name string
- Cmd string // name of binary to invoke command
- Env []string // any environment values to set/override
- RootNames []rootName // filename and mode indicating the root of a checkout directory
+ Name string
+ Cmd string // name of binary to invoke command
+ Env []string // any environment values to set/override
+ Roots []isVCSRoot // filters to identify repository root directories
Scheme []string
PingCmd string
// HGPLAIN=+strictflags turns off additional output that a user may have
// enabled via config options or certain extensions.
Env: []string{"HGPLAIN=+strictflags"},
- RootNames: []rootName{
- {filename: ".hg", isDir: true},
+ Roots: []isVCSRoot{
+ vcsDirRoot(".hg"),
},
Scheme: []string{"https", "http", "ssh"},
var vcsGit = &Cmd{
Name: "Git",
Cmd: "git",
- RootNames: []rootName{
- {filename: ".git", isDir: true},
+ Roots: []isVCSRoot{
+ vcsGitRoot{},
},
Scheme: []string{"git", "https", "http", "git+ssh", "ssh"},
var vcsBzr = &Cmd{
Name: "Bazaar",
Cmd: "bzr",
- RootNames: []rootName{
- {filename: ".bzr", isDir: true},
+ Roots: []isVCSRoot{
+ vcsDirRoot(".bzr"),
},
Scheme: []string{"https", "http", "bzr", "bzr+ssh"},
var vcsSvn = &Cmd{
Name: "Subversion",
Cmd: "svn",
- RootNames: []rootName{
- {filename: ".svn", isDir: true},
+ Roots: []isVCSRoot{
+ vcsDirRoot(".svn"),
},
// There is no tag command in subversion.
var vcsFossil = &Cmd{
Name: "Fossil",
Cmd: "fossil",
- RootNames: []rootName{
- {filename: ".fslckout", isDir: false},
- {filename: "_FOSSIL_", isDir: false},
+ Roots: []isVCSRoot{
+ vcsFileRoot(".fslckout"),
+ vcsFileRoot("_FOSSIL_"),
},
Scheme: []string{"https", "http"},
origDir := dir
for len(dir) > len(srcRoot) {
for _, vcs := range vcsList {
- if isVCSRoot(dir, vcs.RootNames) {
+ if isVCSRootDir(dir, vcs.Roots) {
if vcsCmd == nil {
// Record first VCS we find.
vcsCmd = vcs
return repoDir, vcsCmd, nil
}
-// isVCSRoot identifies a VCS root by checking whether the directory contains
-// any of the listed root names.
-func isVCSRoot(dir string, rootNames []rootName) bool {
- for _, root := range rootNames {
- fi, err := os.Stat(filepath.Join(dir, root.filename))
- if err == nil && fi.IsDir() == root.isDir {
+// isVCSRootDir reports whether dir is a VCS root according to roots.
+func isVCSRootDir(dir string, roots []isVCSRoot) bool {
+ for _, root := range roots {
+ if root.isRoot(dir) {
return true
}
}
-
return false
}
-type rootName struct {
- filename string
- isDir bool
+type isVCSRoot interface {
+ isRoot(dir string) bool
+}
+
+// vcsFileRoot identifies a VCS root by the presence of a regular file.
+type vcsFileRoot string
+
+func (vfr vcsFileRoot) isRoot(dir string) bool {
+ fi, err := os.Stat(filepath.Join(dir, string(vfr)))
+ return err == nil && fi.Mode().IsRegular()
+}
+
+// vcsDirRoot identifies a VCS root by the presence of a directory.
+type vcsDirRoot string
+
+func (vdr vcsDirRoot) isRoot(dir string) bool {
+ fi, err := os.Stat(filepath.Join(dir, string(vdr)))
+ return err == nil && fi.IsDir()
+}
+
+// vcsGitRoot identifies a Git root by the presence of a .git directory or a .git worktree file.
+// See https://go.dev/issue/58218.
+type vcsGitRoot struct{}
+
+func (vcsGitRoot) isRoot(dir string) bool {
+ path := filepath.Join(dir, ".git")
+ fi, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+ if fi.IsDir() {
+ return true
+ }
+ // Is it a git worktree file?
+ // The format is "gitdir: <path>\n".
+ if !fi.Mode().IsRegular() || fi.Size() == 0 || fi.Size() > 4096 {
+ return false
+ }
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return false
+ }
+ rest, ok := strings.CutPrefix(string(raw), "gitdir:")
+ if !ok {
+ return false
+ }
+ gitdir := strings.TrimSpace(rest)
+ if gitdir == "" {
+ return false
+ }
+ if !filepath.IsAbs(gitdir) {
+ gitdir = filepath.Join(dir, gitdir)
+ }
+ fi, err = os.Stat(gitdir)
+ return err == nil && fi.IsDir()
}
type vcsNotFoundError struct {
import (
"errors"
- "fmt"
"internal/testenv"
"os"
"path/filepath"
// Test that vcs.FromDir correctly inspects a given directory and returns the
// right VCS and repo directory.
func TestFromDir(t *testing.T) {
- tempDir := t.TempDir()
+ tests := []struct {
+ name string
+ vcs string
+ root string
+ create func(path string) error
+ }{
+ {"hg", "Mercurial", ".hg", mkdir},
+ {"git_dir", "Git", ".git", mkdir},
+ {"git_worktree", "Git", ".git", createGitWorktreeFile},
+ {"bzr", "Bazaar", ".bzr", mkdir},
+ {"svn", "Subversion", ".svn", mkdir},
+ {"fossil_fslckout", "Fossil", ".fslckout", touch},
+ {"fossil_FOSSIL_", "Fossil", "_FOSSIL_", touch},
+ }
- for _, vcs := range vcsList {
- for r, root := range vcs.RootNames {
- vcsName := fmt.Sprint(vcs.Name, r)
- dir := filepath.Join(tempDir, "example.com", vcsName, root.filename)
- if root.isDir {
- err := os.MkdirAll(dir, 0755)
- if err != nil {
- t.Fatal(err)
- }
- } else {
- err := os.MkdirAll(filepath.Dir(dir), 0755)
- if err != nil {
- t.Fatal(err)
- }
- f, err := os.Create(dir)
- if err != nil {
- t.Fatal(err)
- }
- f.Close()
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ repoDir := filepath.Join(tempDir, "example.com")
+ if err := mkdir(repoDir); err != nil {
+ t.Fatal(err)
}
-
- wantRepoDir := filepath.Dir(dir)
- gotRepoDir, gotVCS, err := FromDir(dir, tempDir)
+ rootPath := filepath.Join(repoDir, tt.root)
+ if err := tt.create(rootPath); err != nil {
+ t.Fatal(err)
+ }
+ gotRepoDir, gotVCS, err := FromDir(repoDir, tempDir)
if err != nil {
- t.Errorf("FromDir(%q, %q): %v", dir, tempDir, err)
- continue
+ t.Fatal(err)
}
- if gotRepoDir != wantRepoDir || gotVCS.Name != vcs.Name {
- t.Errorf("FromDir(%q, %q) = RepoDir(%s), VCS(%s); want RepoDir(%s), VCS(%s)", dir, tempDir, gotRepoDir, gotVCS.Name, wantRepoDir, vcs.Name)
+ if gotRepoDir != repoDir {
+ t.Errorf("RepoDir = %q, want %q", gotRepoDir, repoDir)
}
- }
+ if gotVCS.Name != tt.vcs {
+ t.Errorf("VCS = %q, want %q", gotVCS.Name, tt.vcs)
+ }
+ })
+ }
+}
+
+func mkdir(path string) error {
+ return os.Mkdir(path, 0o755)
+}
+
+func touch(path string) error {
+ return os.WriteFile(path, nil, 0o644)
+}
+
+func createGitWorktreeFile(path string) error {
+ gitdir := path + ".worktree"
+ // gitdir must point to a real directory
+ if err := mkdir(gitdir); err != nil {
+ return err
}
+ return os.WriteFile(path, []byte("gitdir: "+gitdir+"\n"), 0o644)
}
func TestIsSecure(t *testing.T) {
--- /dev/null
+# Test that 'go build' stamps VCS information when building from a git worktree.
+# See https://go.dev/issue/58218.
+
+[!git] skip
+[short] skip
+
+# Create repo with a commit.
+cd repo
+exec git init
+exec git config user.email g.o.p.h.e.r@go.dev
+exec git config user.name Gopher
+exec git add -A
+exec git commit -m 'initial commit'
+
+# Sanity check: building from main repo includes VCS info.
+go build -o main.exe .
+go version -m main.exe
+stdout '^\tbuild\tvcs=git$'
+stdout '^\tbuild\tvcs.modified=false$'
+
+# Create a worktree and build from it.
+exec git worktree add ../worktree HEAD
+cd ../worktree
+go build -o worktree.exe .
+go version -m worktree.exe
+stdout '^\tbuild\tvcs=git$'
+stdout '^\tbuild\tvcs.modified=false$'
+
+# Verify that vcs.modified is detected in the worktree.
+cp ../changed.go a.go
+go build -o modified.exe .
+go version -m modified.exe
+stdout '^\tbuild\tvcs.modified=true$'
+
+-- repo/go.mod --
+module example.com/worktree
+
+go 1.18
+-- repo/a.go --
+package main
+
+func main() {}
+-- changed.go --
+package main
+
+func main() { _ = 1 }