Skip to content

Commit

Permalink
Have sync create module major-release symlinks (robfig#48)
Browse files Browse the repository at this point in the history
When a module containing a go.mod is synced, the module name may contain
a major release suffix (e.g. "rsc.io/quote/v2"), indicating that the
code at the module root is a post-v1 major release and that users of the
module should import it with this suffix. Prior to this change, code
using such a module would have had to import it without the suffix,
requiring different versions of the client code to exist in order to
be buildable in both module-aware and legacy-GOPATH modes.

This change creates a self-referencing major-release symlink if the
imported module contains a go.mod specifying a major release suffix.
This allows both legacy-GOPATH and module-aware builds to succeed,
which can help users migrating from glock to Go modules avoid a hard
cutover due to import path incompatibilities.

Because glock now changes the contents in GOPATH/src/... from the
checked-out source, the git tagSyncCmd was updated with a -f flag, in
order to force checkout and overwrite any created symlinks that might
conflict with to-be-checked-out content.
  • Loading branch information
atavakoliyext authored Mar 5, 2021
1 parent e56214a commit b9663bc
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 3 deletions.
4 changes: 3 additions & 1 deletion glock.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"text/template"
)

var buildV bool // Used in vcs.go and http.go to print detailed stuff about go get
// Used in vcs.go and http.go to print detailed stuff about go get.
// Also used to enable output from the debug() function declared in util.go.
var buildV bool

type Command struct {
Run func(cmd *Command, args []string)
Expand Down
84 changes: 84 additions & 0 deletions sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"os"
"regexp"
"strings"

"github.com/agtorre/gocolorize"
"golang.org/x/mod/modfile"
)

var cmdSync = &Command{
Expand Down Expand Up @@ -40,6 +44,8 @@ var (
critical = gocolorize.NewColor("red").Paint

disabled = func(args ...interface{}) string { return fmt.Sprint(args...) }

majorVersionSuffix = regexp.MustCompile(`/v[\d]+$`)
)

// Running too many syncs at once can exhaust file descriptor limits.
Expand Down Expand Up @@ -156,6 +162,12 @@ func syncPkg(ch chan<- string, importPath, expectedRevision, getOutput string, g
var status bytes.Buffer
defer func() { ch <- status.String() }()

defer func() {
if err := maybeLinkModulePath(importPath); err != nil {
perror(err)
}
}()

// Try to find the repo.
var repo, err = fastRepoRoot(importPath)
if err != nil {
Expand Down Expand Up @@ -216,3 +228,75 @@ func syncPkg(ch chan<- string, importPath, expectedRevision, getOutput string, g
perror(err)
}
}

// maybeLinkModulePath creates a self-referencing major-release symlink in the
// specified import path, if the import contains a go.mod whose module name
// includes a major release suffix.
//
// For example, suppose rsc.io/quote is imported at its v2.0.0 tag; because this
// version contains a go.mod that specifies the module name as rsc.io/quote/v2,
// a symlink (rsc.io/quote/v2 => rsc.io/quote) is created. This allows code to
// import the more go-module-friendly "rsc.io/quote/v2" path instead of the
// legacy "rsc.io/quote" path.
func maybeLinkModulePath(importPath string) error {
var importDir = filepath.Join(gopaths()[0], "src", importPath)

goModPath := filepath.Join(importDir, "go.mod")
data, err := ioutil.ReadFile(goModPath)
if os.IsNotExist(err) {
// No go.mod, so nothing to do.
return nil
} else if err != nil {
return err
}

goModFile, err := modfile.ParseLax(goModPath, data, nil)
if err != nil {
return err
}

// Check if the module doesn't point to an implicit major release,
// in which case, do nothing except maybe warn if the GLOCKFILE import path
// doesn't match the module name in its go.mod.
if !majorVersionSuffix.MatchString(goModFile.Module.Mod.Path) {
if importPath != goModFile.Module.Mod.Path {
debug(warning("[WARN]"), "import path", importPath, "conflicts with go.mod path", goModFile.Module.Mod.Path)
}
return nil
}

// If the version-less module path doesn't match the import path, creating
// the versioned symlink won't help, so warn & return.
moduleBasePath, ver := filepath.Split(goModFile.Module.Mod.Path)
moduleBasePath = strings.TrimRight(moduleBasePath, string(filepath.Separator))
if importPath != moduleBasePath {
debug(warning("[WARN]"), "import path", importPath, "conflicts with go.mod path", goModFile.Module.Mod.Path)
return nil
}

symlinkPath := filepath.Join(importDir, ver)
_, err = os.Lstat(symlinkPath)
if !os.IsNotExist(err) {
if err != nil {
return err
}

if same, err := sameFile(symlinkPath, importDir); err != nil {
return err
} else if same {
// Valid symlink already exists; nothing to do
debug(info("[INFO]"), symlinkPath, "already exists and is valid")
return nil
} else {
// Something else already exists at this path; don't touch it.
debug(warning("[WARN]"), symlinkPath, "already exists, but conflicts with module name in go.mod")
return nil
}
}

if err := os.Symlink(".", symlinkPath); err != nil {
return err
}
debug(info("[INFO]"), "created symlink", symlinkPath, "=>", importPath)
return nil
}
27 changes: 27 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,30 @@ func glockfileWriter(importPath string, n bool) io.WriteCloser {
}
return f
}

// sameFile returns true if path1 and path2 refer to the same file. If either
// are symlinks, then the comparison is done after the links are followed.
func sameFile(path1, path2 string) (bool, error) {
info1, err := os.Stat(path1)
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}

info2, err := os.Stat(path2)
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}

return os.SameFile(info1, info2), nil
}

// debug prints args to STDERR if in verbose mode.
func debug(args ...interface{}) {
if buildV {
fmt.Fprintln(os.Stderr, args...)
}
}
4 changes: 2 additions & 2 deletions vcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ var vcsGit = &vcsCmd{
tagLookupCmd: []tagCmd{
{"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
},
tagSyncCmd: "checkout {tag}",
tagSyncDefault: "checkout master",
tagSyncCmd: "checkout -f {tag} --",
tagSyncDefault: "checkout -f master --",

scheme: []string{"git", "https", "http", "git+ssh"},
pingCmd: "ls-remote {scheme}://{repo}",
Expand Down

0 comments on commit b9663bc

Please sign in to comment.