mirror of
https://github.com/golang/go.git
synced 2026-01-29 07:02:05 +03:00
cmd/go/internal/vcs: support git worktrees
Fixes golang/go#58218 Change-Id: Ia155b26514557cf822caf37e727e5a410b0a36a6 Reviewed-on: https://go-review.googlesource.com/c/go/+/736260 Reviewed-by: Michael Pratt <mpratt@google.com> Reviewed-by: Junyang Shao <shaojunyang@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
@@ -35,10 +35,10 @@ import (
|
||||
// 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
|
||||
@@ -149,8 +149,8 @@ var vcsHg = &Cmd{
|
||||
// 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"},
|
||||
@@ -214,8 +214,8 @@ func parseRevTime(out []byte) (string, time.Time, error) {
|
||||
var vcsGit = &Cmd{
|
||||
Name: "Git",
|
||||
Cmd: "git",
|
||||
RootNames: []rootName{
|
||||
{filename: ".git", isDir: true},
|
||||
Roots: []isVCSRoot{
|
||||
vcsGitRoot{},
|
||||
},
|
||||
|
||||
Scheme: []string{"git", "https", "http", "git+ssh", "ssh"},
|
||||
@@ -262,8 +262,8 @@ func gitStatus(vcsGit *Cmd, rootDir string) (Status, error) {
|
||||
var vcsBzr = &Cmd{
|
||||
Name: "Bazaar",
|
||||
Cmd: "bzr",
|
||||
RootNames: []rootName{
|
||||
{filename: ".bzr", isDir: true},
|
||||
Roots: []isVCSRoot{
|
||||
vcsDirRoot(".bzr"),
|
||||
},
|
||||
|
||||
Scheme: []string{"https", "http", "bzr", "bzr+ssh"},
|
||||
@@ -332,8 +332,8 @@ func bzrStatus(vcsBzr *Cmd, rootDir string) (Status, error) {
|
||||
var vcsSvn = &Cmd{
|
||||
Name: "Subversion",
|
||||
Cmd: "svn",
|
||||
RootNames: []rootName{
|
||||
{filename: ".svn", isDir: true},
|
||||
Roots: []isVCSRoot{
|
||||
vcsDirRoot(".svn"),
|
||||
},
|
||||
|
||||
// There is no tag command in subversion.
|
||||
@@ -381,9 +381,9 @@ const fossilRepoName = ".fossil"
|
||||
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"},
|
||||
@@ -592,7 +592,7 @@ func FromDir(dir, srcRoot string) (repoDir string, vcsCmd *Cmd, err error) {
|
||||
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
|
||||
@@ -631,22 +631,71 @@ func FromDir(dir, srcRoot string) (repoDir string, vcsCmd *Cmd, err error) {
|
||||
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 {
|
||||
|
||||
@@ -6,7 +6,6 @@ package vcs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"internal/testenv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -215,40 +214,61 @@ func TestRepoRootForImportPath(t *testing.T) {
|
||||
// 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()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
wantRepoDir := filepath.Dir(dir)
|
||||
gotRepoDir, gotVCS, err := FromDir(dir, tempDir)
|
||||
if err != nil {
|
||||
t.Errorf("FromDir(%q, %q): %v", dir, tempDir, err)
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 _, 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)
|
||||
}
|
||||
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.Fatal(err)
|
||||
}
|
||||
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) {
|
||||
|
||||
46
src/cmd/go/testdata/script/version_buildvcs_git_worktree.txt
vendored
Normal file
46
src/cmd/go/testdata/script/version_buildvcs_git_worktree.txt
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# 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 }
|
||||
Reference in New Issue
Block a user