Skip to content

Commit a37bebc

Browse files
author
Jay Conrod
committed
cmd/go: stamp VCS revision and uncommitted status into binaries
When the go command builds a binary, it will now stamp the current revision from the local Git or Mercurial repository, and it will also stamp whether there are uncommitted edited or untracked files. Only Git and Mercurial are supported for now. If no repository is found containing the current working directory (where the go command was started), or if either the main package directory or the containing module's root directory is outside the repository, no VCS information will be stamped. If the VCS tool is missing or returns an error, that error is reported on the main package (hinting that -buildvcs may be disabled). This change introduces the -buildvcs flag, which is enabled by default. When disabled, VCS information won't be stamped when it would be otherwise. Stamped information may be read using 'go version -m file' or debug.ReadBuildInfo. For golang#37475 Change-Id: I4e7d3159e1c270d85869ad99f10502e546e7582d Reviewed-on: https://go-review.googlesource.com/c/go/+/353930 Trust: Jay Conrod <jayconrod@google.com> Run-TryBot: Jay Conrod <jayconrod@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Bryan C. Mills <bcmills@google.com>
1 parent a8c5a99 commit a37bebc

File tree

14 files changed

+585
-53
lines changed

14 files changed

+585
-53
lines changed

api/next.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ pkg debug/buildinfo, type BuildInfo = debug.BuildInfo
44
pkg runtime/debug, method (*BuildInfo) MarshalText() ([]byte, error)
55
pkg runtime/debug, method (*BuildInfo) UnmarshalText() ([]byte, error)
66
pkg runtime/debug, type BuildInfo struct, GoVersion string
7+
pkg runtime/debug, type BuildInfo struct, Settings []BuildSetting
8+
pkg runtime/debug, type BuildSetting struct
9+
pkg runtime/debug, type BuildSetting struct, Key string
10+
pkg runtime/debug, type BuildSetting struct, Value string
711
pkg syscall (darwin-amd64), func RecvfromInet4(int, []uint8, int, *SockaddrInet4) (int, error)
812
pkg syscall (darwin-amd64), func RecvfromInet6(int, []uint8, int, *SockaddrInet6) (int, error)
913
pkg syscall (darwin-amd64), func SendtoInet4(int, []uint8, int, SockaddrInet4) error

src/cmd/go/alldocs.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/internal/cfg/cfg.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
var (
2727
BuildA bool // -a flag
2828
BuildBuildmode string // -buildmode flag
29+
BuildBuildvcs bool // -buildvcs flag
2930
BuildContext = defaultContext()
3031
BuildMod string // -mod flag
3132
BuildModExplicit bool // whether -mod was set explicitly

src/cmd/go/internal/get/get.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,10 @@ func download(arg string, parent *load.Package, stk *load.ImportStack, mode int)
417417
// to make the first copy of or update a copy of the given package.
418418
func downloadPackage(p *load.Package) error {
419419
var (
420-
vcsCmd *vcs.Cmd
421-
repo, rootPath string
422-
err error
423-
blindRepo bool // set if the repo has unusual configuration
420+
vcsCmd *vcs.Cmd
421+
repo, rootPath, repoDir string
422+
err error
423+
blindRepo bool // set if the repo has unusual configuration
424424
)
425425

426426
// p can be either a real package, or a pseudo-package whose “import path” is
@@ -446,10 +446,18 @@ func downloadPackage(p *load.Package) error {
446446

447447
if p.Internal.Build.SrcRoot != "" {
448448
// Directory exists. Look for checkout along path to src.
449-
vcsCmd, rootPath, err = vcs.FromDir(p.Dir, p.Internal.Build.SrcRoot)
449+
repoDir, vcsCmd, err = vcs.FromDir(p.Dir, p.Internal.Build.SrcRoot)
450450
if err != nil {
451451
return err
452452
}
453+
if !str.HasFilePathPrefix(repoDir, p.Internal.Build.SrcRoot) {
454+
panic(fmt.Sprintf("repository %q not in source root %q", repo, p.Internal.Build.SrcRoot))
455+
}
456+
rootPath = str.TrimFilePathPrefix(repoDir, p.Internal.Build.SrcRoot)
457+
if err := vcs.CheckGOVCS(vcsCmd, rootPath); err != nil {
458+
return err
459+
}
460+
453461
repo = "<local>" // should be unused; make distinctive
454462

455463
// Double-check where it came from.

src/cmd/go/internal/load/pkg.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"cmd/go/internal/par"
3939
"cmd/go/internal/search"
4040
"cmd/go/internal/trace"
41+
"cmd/go/internal/vcs"
4142
"cmd/internal/str"
4243
"cmd/internal/sys"
4344

@@ -2267,6 +2268,72 @@ func (p *Package) setBuildInfo() {
22672268
Main: main,
22682269
Deps: deps,
22692270
}
2271+
2272+
// Add VCS status if all conditions are true:
2273+
//
2274+
// - -buildvcs is enabled.
2275+
// - p is contained within a main module (there may be multiple main modules
2276+
// in a workspace, but local replacements don't count).
2277+
// - Both the current directory and p's module's root directory are contained
2278+
// in the same local repository.
2279+
// - We know the VCS commands needed to get the status.
2280+
setVCSError := func(err error) {
2281+
setPkgErrorf("error obtaining VCS status: %v\n\tUse -buildvcs=false to disable VCS stamping.", err)
2282+
}
2283+
2284+
var repoDir string
2285+
var vcsCmd *vcs.Cmd
2286+
var err error
2287+
if cfg.BuildBuildvcs && p.Module != nil && p.Module.Version == "" {
2288+
repoDir, vcsCmd, err = vcs.FromDir(base.Cwd(), "")
2289+
if err != nil && !errors.Is(err, os.ErrNotExist) {
2290+
setVCSError(err)
2291+
return
2292+
}
2293+
if !str.HasFilePathPrefix(p.Module.Dir, repoDir) &&
2294+
!str.HasFilePathPrefix(repoDir, p.Module.Dir) {
2295+
// The module containing the main package does not overlap with the
2296+
// repository containing the working directory. Don't include VCS info.
2297+
// If the repo contains the module or vice versa, but they are not
2298+
// the same directory, it's likely an error (see below).
2299+
repoDir, vcsCmd = "", nil
2300+
}
2301+
}
2302+
if repoDir != "" && vcsCmd.Status != nil {
2303+
// Check that the current directory, package, and module are in the same
2304+
// repository. vcs.FromDir allows nested Git repositories, but nesting
2305+
// is not allowed for other VCS tools. The current directory may be outside
2306+
// p.Module.Dir when a workspace is used.
2307+
pkgRepoDir, _, err := vcs.FromDir(p.Dir, "")
2308+
if err != nil {
2309+
setVCSError(err)
2310+
return
2311+
}
2312+
if pkgRepoDir != repoDir {
2313+
setVCSError(fmt.Errorf("main package is in repository %q but current directory is in repository %q", pkgRepoDir, repoDir))
2314+
return
2315+
}
2316+
modRepoDir, _, err := vcs.FromDir(p.Module.Dir, "")
2317+
if err != nil {
2318+
setVCSError(err)
2319+
return
2320+
}
2321+
if modRepoDir != repoDir {
2322+
setVCSError(fmt.Errorf("main module is in repository %q but current directory is in repository %q", modRepoDir, repoDir))
2323+
return
2324+
}
2325+
2326+
st, err := vcsCmd.Status(vcsCmd, repoDir)
2327+
if err != nil {
2328+
setVCSError(err)
2329+
return
2330+
}
2331+
info.Settings = []debug.BuildSetting{
2332+
{Key: vcsCmd.Cmd + "revision", Value: st.Revision},
2333+
{Key: vcsCmd.Cmd + "uncommitted", Value: strconv.FormatBool(st.Uncommitted)},
2334+
}
2335+
}
2336+
22702337
text, err := info.MarshalText()
22712338
if err != nil {
22722339
setPkgErrorf("error formatting build info: %v", err)

src/cmd/go/internal/vcs/vcs.go

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package vcs
66

77
import (
8+
"bytes"
89
"encoding/json"
910
"errors"
1011
"fmt"
@@ -29,7 +30,7 @@ import (
2930
"golang.org/x/mod/module"
3031
)
3132

32-
// A vcsCmd describes how to use a version control system
33+
// A Cmd describes how to use a version control system
3334
// like Mercurial, Git, or Subversion.
3435
type Cmd struct {
3536
Name string
@@ -48,6 +49,13 @@ type Cmd struct {
4849

4950
RemoteRepo func(v *Cmd, rootDir string) (remoteRepo string, err error)
5051
ResolveRepo func(v *Cmd, rootDir, remoteRepo string) (realRepo string, err error)
52+
Status func(v *Cmd, rootDir string) (Status, error)
53+
}
54+
55+
// Status is the current state of a local repository.
56+
type Status struct {
57+
Revision string
58+
Uncommitted bool
5159
}
5260

5361
var defaultSecureScheme = map[string]bool{
@@ -139,6 +147,7 @@ var vcsHg = &Cmd{
139147
Scheme: []string{"https", "http", "ssh"},
140148
PingCmd: "identify -- {scheme}://{repo}",
141149
RemoteRepo: hgRemoteRepo,
150+
Status: hgStatus,
142151
}
143152

144153
func hgRemoteRepo(vcsHg *Cmd, rootDir string) (remoteRepo string, err error) {
@@ -149,6 +158,27 @@ func hgRemoteRepo(vcsHg *Cmd, rootDir string) (remoteRepo string, err error) {
149158
return strings.TrimSpace(string(out)), nil
150159
}
151160

161+
func hgStatus(vcsHg *Cmd, rootDir string) (Status, error) {
162+
out, err := vcsHg.runOutputVerboseOnly(rootDir, "identify -i")
163+
if err != nil {
164+
return Status{}, err
165+
}
166+
rev := strings.TrimSpace(string(out))
167+
uncommitted := strings.HasSuffix(rev, "+")
168+
if uncommitted {
169+
// "+" means a tracked file is edited.
170+
rev = rev[:len(rev)-len("+")]
171+
} else {
172+
// Also look for untracked files.
173+
out, err = vcsHg.runOutputVerboseOnly(rootDir, "status -u")
174+
if err != nil {
175+
return Status{}, err
176+
}
177+
uncommitted = len(out) > 0
178+
}
179+
return Status{Revision: rev, Uncommitted: uncommitted}, nil
180+
}
181+
152182
// vcsGit describes how to use Git.
153183
var vcsGit = &Cmd{
154184
Name: "Git",
@@ -182,6 +212,7 @@ var vcsGit = &Cmd{
182212
PingCmd: "ls-remote {scheme}://{repo}",
183213

184214
RemoteRepo: gitRemoteRepo,
215+
Status: gitStatus,
185216
}
186217

187218
// scpSyntaxRe matches the SCP-like addresses used by Git to access
@@ -232,6 +263,20 @@ func gitRemoteRepo(vcsGit *Cmd, rootDir string) (remoteRepo string, err error) {
232263
return "", errParse
233264
}
234265

266+
func gitStatus(cmd *Cmd, repoDir string) (Status, error) {
267+
out, err := cmd.runOutputVerboseOnly(repoDir, "rev-parse HEAD")
268+
if err != nil {
269+
return Status{}, err
270+
}
271+
rev := string(bytes.TrimSpace(out))
272+
out, err = cmd.runOutputVerboseOnly(repoDir, "status --porcelain")
273+
if err != nil {
274+
return Status{}, err
275+
}
276+
uncommitted := len(out) != 0
277+
return Status{Revision: rev, Uncommitted: uncommitted}, nil
278+
}
279+
235280
// vcsBzr describes how to use Bazaar.
236281
var vcsBzr = &Cmd{
237282
Name: "Bazaar",
@@ -395,6 +440,12 @@ func (v *Cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error
395440
return v.run1(dir, cmd, keyval, true)
396441
}
397442

443+
// runOutputVerboseOnly is like runOutput but only generates error output to
444+
// standard error in verbose mode.
445+
func (v *Cmd) runOutputVerboseOnly(dir string, cmd string, keyval ...string) ([]byte, error) {
446+
return v.run1(dir, cmd, keyval, false)
447+
}
448+
398449
// run1 is the generalized implementation of run and runOutput.
399450
func (v *Cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) {
400451
m := make(map[string]string)
@@ -550,58 +601,62 @@ type vcsPath struct {
550601

551602
// FromDir inspects dir and its parents to determine the
552603
// version control system and code repository to use.
553-
// On return, root is the import path
554-
// corresponding to the root of the repository.
555-
func FromDir(dir, srcRoot string) (vcs *Cmd, root string, err error) {
604+
// If no repository is found, FromDir returns an error
605+
// equivalent to os.ErrNotExist.
606+
func FromDir(dir, srcRoot string) (repoDir string, vcsCmd *Cmd, err error) {
556607
// Clean and double-check that dir is in (a subdirectory of) srcRoot.
557608
dir = filepath.Clean(dir)
558-
srcRoot = filepath.Clean(srcRoot)
559-
if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {
560-
return nil, "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot)
609+
if srcRoot != "" {
610+
srcRoot = filepath.Clean(srcRoot)
611+
if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {
612+
return "", nil, fmt.Errorf("directory %q is outside source root %q", dir, srcRoot)
613+
}
561614
}
562615

563-
var vcsRet *Cmd
564-
var rootRet string
565-
566616
origDir := dir
567617
for len(dir) > len(srcRoot) {
568618
for _, vcs := range vcsList {
569619
if _, err := os.Stat(filepath.Join(dir, "."+vcs.Cmd)); err == nil {
570-
root := filepath.ToSlash(dir[len(srcRoot)+1:])
571620
// Record first VCS we find, but keep looking,
572621
// to detect mistakes like one kind of VCS inside another.
573-
if vcsRet == nil {
574-
vcsRet = vcs
575-
rootRet = root
622+
if vcsCmd == nil {
623+
vcsCmd = vcs
624+
repoDir = dir
576625
continue
577626
}
578627
// Allow .git inside .git, which can arise due to submodules.
579-
if vcsRet == vcs && vcs.Cmd == "git" {
628+
if vcsCmd == vcs && vcs.Cmd == "git" {
580629
continue
581630
}
582631
// Otherwise, we have one VCS inside a different VCS.
583-
return nil, "", fmt.Errorf("directory %q uses %s, but parent %q uses %s",
584-
filepath.Join(srcRoot, rootRet), vcsRet.Cmd, filepath.Join(srcRoot, root), vcs.Cmd)
632+
return "", nil, fmt.Errorf("directory %q uses %s, but parent %q uses %s",
633+
repoDir, vcsCmd.Cmd, dir, vcs.Cmd)
585634
}
586635
}
587636

588637
// Move to parent.
589638
ndir := filepath.Dir(dir)
590639
if len(ndir) >= len(dir) {
591-
// Shouldn't happen, but just in case, stop.
592640
break
593641
}
594642
dir = ndir
595643
}
596-
597-
if vcsRet != nil {
598-
if err := checkGOVCS(vcsRet, rootRet); err != nil {
599-
return nil, "", err
600-
}
601-
return vcsRet, rootRet, nil
644+
if vcsCmd == nil {
645+
return "", nil, &vcsNotFoundError{dir: origDir}
602646
}
647+
return repoDir, vcsCmd, nil
648+
}
649+
650+
type vcsNotFoundError struct {
651+
dir string
652+
}
653+
654+
func (e *vcsNotFoundError) Error() string {
655+
return fmt.Sprintf("directory %q is not using a known version control system", e.dir)
656+
}
603657

604-
return nil, "", fmt.Errorf("directory %q is not using a known version control system", origDir)
658+
func (e *vcsNotFoundError) Is(err error) bool {
659+
return err == os.ErrNotExist
605660
}
606661

607662
// A govcsRule is a single GOVCS rule like private:hg|svn.
@@ -707,7 +762,11 @@ var defaultGOVCS = govcsConfig{
707762
{"public", []string{"git", "hg"}},
708763
}
709764

710-
func checkGOVCS(vcs *Cmd, root string) error {
765+
// CheckGOVCS checks whether the policy defined by the environment variable
766+
// GOVCS allows the given vcs command to be used with the given repository
767+
// root path. Note that root may not be a real package or module path; it's
768+
// the same as the root path in the go-import meta tag.
769+
func CheckGOVCS(vcs *Cmd, root string) error {
711770
if vcs == vcsMod {
712771
// Direct module (proxy protocol) fetches don't
713772
// involve an external version control system
@@ -885,7 +944,7 @@ func repoRootFromVCSPaths(importPath string, security web.SecurityMode, vcsPaths
885944
if vcs == nil {
886945
return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
887946
}
888-
if err := checkGOVCS(vcs, match["root"]); err != nil {
947+
if err := CheckGOVCS(vcs, match["root"]); err != nil {
889948
return nil, err
890949
}
891950
var repoURL string
@@ -1012,7 +1071,7 @@ func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.Se
10121071
}
10131072
}
10141073

1015-
if err := checkGOVCS(vcs, mmi.Prefix); err != nil {
1074+
if err := CheckGOVCS(vcs, mmi.Prefix); err != nil {
10161075
return nil, err
10171076
}
10181077

0 commit comments

Comments
 (0)