downloadModule := func(m *moduleJSON) {
var err error
- m.Info, err = modfetch.InfoFile(m.Path, m.Version)
+ _, m.Info, err = modfetch.InfoFile(m.Path, m.Version)
if err != nil {
m.Error = err.Error()
return
}
// A cachingRepo is a cache around an underlying Repo,
-// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
+// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not CheckReuse or Zip).
// It is also safe for simultaneous use by multiple goroutines
// (so that it can be returned from Lookup multiple times).
// It serializes calls to the underlying Repo.
return r.r
}
+func (r *cachingRepo) CheckReuse(old *codehost.Origin) error {
+ return r.repo().CheckReuse(old)
+}
+
func (r *cachingRepo) ModulePath() string {
return r.path
}
-func (r *cachingRepo) Versions(prefix string) ([]string, error) {
+func (r *cachingRepo) Versions(prefix string) (*Versions, error) {
type cached struct {
- list []string
- err error
+ v *Versions
+ err error
}
c := r.cache.Do("versions:"+prefix, func() any {
- list, err := r.repo().Versions(prefix)
- return cached{list, err}
+ v, err := r.repo().Versions(prefix)
+ return cached{v, err}
}).(cached)
if c.err != nil {
return nil, c.err
}
- return append([]string(nil), c.list...), nil
+ v := &Versions{
+ Origin: c.v.Origin,
+ List: append([]string(nil), c.v.List...),
+ }
+ return v, nil
}
type cachedInfo struct {
return r.repo().Zip(dst, version)
}
-// InfoFile is like Lookup(path).Stat(version) but returns the name of the file
+// InfoFile is like Lookup(path).Stat(version) but also returns the name of the file
// containing the cached information.
-func InfoFile(path, version string) (string, error) {
+func InfoFile(path, version string) (*RevInfo, string, error) {
if !semver.IsValid(version) {
- return "", fmt.Errorf("invalid version %q", version)
+ return nil, "", fmt.Errorf("invalid version %q", version)
}
- if file, _, err := readDiskStat(path, version); err == nil {
- return file, nil
+ if file, info, err := readDiskStat(path, version); err == nil {
+ return info, file, nil
}
+ var info *RevInfo
err := TryProxies(func(proxy string) error {
- _, err := Lookup(proxy, path).Stat(version)
+ i, err := Lookup(proxy, path).Stat(version)
+ if err == nil {
+ info = i
+ }
return err
})
if err != nil {
- return "", err
+ return nil, "", err
}
// Stat should have populated the disk cache for us.
file, err := CachePath(module.Version{Path: path, Version: version}, "info")
if err != nil {
- return "", err
+ return nil, "", err
}
- return file, nil
+ return info, file, nil
}
// GoMod is like Lookup(path).GoMod(rev) but avoids the
"cmd/go/internal/cfg"
"cmd/go/internal/lockedfile"
"cmd/go/internal/str"
+
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
)
// Downloaded size limits.
// remote version control servers, and code hosting sites.
// A Repo must be safe for simultaneous use by multiple goroutines.
type Repo interface {
+ // CheckReuse checks whether the old origin information
+ // remains up to date. If so, whatever cached object it was
+ // taken from can be reused.
+ // The subdir gives subdirectory name where the module root is expected to be found,
+ // "" for the root or "sub/dir" for a subdirectory (no trailing slash).
+ CheckReuse(old *Origin, subdir string) error
+
// List lists all tags with the given prefix.
- Tags(prefix string) (tags []string, err error)
+ Tags(prefix string) (*Tags, error)
// Stat returns information about the revision rev.
// A revision can be any identifier known to the underlying service:
DescendsFrom(rev, tag string) (bool, error)
}
-// A Rev describes a single revision in a source code repository.
+// An Origin describes the provenance of a given repo method result.
+// It can be passed to CheckReuse (usually in a different go command invocation)
+// to see whether the result remains up-to-date.
+type Origin struct {
+ VCS string `json:",omitempty"` // "git" etc
+ URL string `json:",omitempty"` // URL of repository
+ Subdir string `json:",omitempty"` // subdirectory in repo
+
+ // If TagSum is non-empty, then the resolution of this module version
+ // depends on the set of tags present in the repo, specifically the tags
+ // of the form TagPrefix + a valid semver version.
+ // If the matching repo tags and their commit hashes still hash to TagSum,
+ // the Origin is still valid (at least as far as the tags are concerned).
+ // The exact checksum is up to the Repo implementation; see (*gitRepo).Tags.
+ TagPrefix string `json:",omitempty"`
+ TagSum string `json:",omitempty"`
+
+ // If Ref is non-empty, then the resolution of this module version
+ // depends on Ref resolving to the revision identified by Hash.
+ // If Ref still resolves to Hash, the Origin is still valid (at least as far as Ref is concerned).
+ // For Git, the Ref is a full ref like "refs/heads/main" or "refs/tags/v1.2.3",
+ // and the Hash is the Git object hash the ref maps to.
+ // Other VCS might choose differently, but the idea is that Ref is the name
+ // with a mutable meaning while Hash is a name with an immutable meaning.
+ Ref string `json:",omitempty"`
+ Hash string `json:",omitempty"`
+}
+
+// Checkable reports whether the Origin contains anything that can be checked.
+// If not, it's purely informational and should fail a CheckReuse call.
+func (o *Origin) Checkable() bool {
+ return o.TagSum != "" || o.Ref != "" || o.Hash != ""
+}
+
+func (o *Origin) Merge(other *Origin) {
+ if o.TagSum == "" {
+ o.TagPrefix = other.TagPrefix
+ o.TagSum = other.TagSum
+ }
+ if o.Ref == "" {
+ o.Ref = other.Ref
+ o.Hash = other.Hash
+ }
+}
+
+// A Tags describes the available tags in a code repository.
+type Tags struct {
+ Origin *Origin
+ List []Tag
+}
+
+// A Tag describes a single tag in a code repository.
+type Tag struct {
+ Name string
+ Hash string // content hash identifying tag's content, if available
+}
+
+// isOriginTag reports whether tag should be preserved
+// in the Tags method's Origin calculation.
+// We can safely ignore tags that are not look like pseudo-versions,
+// because ../coderepo.go's (*codeRepo).Versions ignores them too.
+// We can also ignore non-semver tags, but we have to include semver
+// tags with extra suffixes, because the pseudo-version base finder uses them.
+func isOriginTag(tag string) bool {
+ // modfetch.(*codeRepo).Versions uses Canonical == tag,
+ // but pseudo-version calculation has a weaker condition that
+ // the canonical is a prefix of the tag.
+ // Include those too, so that if any new one appears, we'll invalidate the cache entry.
+ // This will lead to spurious invalidation of version list results,
+ // but tags of this form being created should be fairly rare
+ // (and invalidate pseudo-version results anyway).
+ c := semver.Canonical(tag)
+ return c != "" && strings.HasPrefix(tag, c) && !module.IsPseudoVersion(tag)
+}
+
+// A RevInfo describes a single revision in a source code repository.
type RevInfo struct {
+ Origin *Origin
Name string // complete ID in underlying repository
Short string // shortened ID, for use in pseudo-version
Version string // version used in lookup
import (
"bytes"
+ "crypto/sha256"
+ "encoding/base64"
"errors"
"fmt"
"io"
}
}
+func (r *gitRepo) CheckReuse(old *Origin, subdir string) error {
+ if old == nil {
+ return fmt.Errorf("missing origin")
+ }
+ if old.VCS != "git" || old.URL != r.remoteURL {
+ return fmt.Errorf("origin moved from %v %q to %v %q", old.VCS, old.URL, "git", r.remoteURL)
+ }
+ if old.Subdir != subdir {
+ return fmt.Errorf("origin moved from %v %q %q to %v %q %q", old.VCS, old.URL, old.Subdir, "git", r.remoteURL, subdir)
+ }
+
+ // Note: Can have Hash with no Ref and no TagSum,
+ // meaning the Hash simply has to remain in the repo.
+ // In that case we assume it does in the absence of any real way to check.
+ // But if neither Hash nor TagSum is present, we have nothing to check,
+ // which we take to mean we didn't record enough information to be sure.
+ if old.Hash == "" && old.TagSum == "" {
+ return fmt.Errorf("non-specific origin")
+ }
+
+ r.loadRefs()
+ if r.refsErr != nil {
+ return r.refsErr
+ }
+
+ if old.Ref != "" {
+ hash, ok := r.refs[old.Ref]
+ if !ok {
+ return fmt.Errorf("ref %q deleted", old.Ref)
+ }
+ if hash != old.Hash {
+ return fmt.Errorf("ref %q moved from %s to %s", old.Ref, old.Hash, hash)
+ }
+ }
+ if old.TagSum != "" {
+ tags, err := r.Tags(old.TagPrefix)
+ if err != nil {
+ return err
+ }
+ if tags.Origin.TagSum != old.TagSum {
+ return fmt.Errorf("tags changed")
+ }
+ }
+
+ return nil
+}
+
// loadRefs loads heads and tags references from the remote into the map r.refs.
// The result is cached in memory.
func (r *gitRepo) loadRefs() (map[string]string, error) {
return r.refs, r.refsErr
}
-func (r *gitRepo) Tags(prefix string) ([]string, error) {
+func (r *gitRepo) Tags(prefix string) (*Tags, error) {
refs, err := r.loadRefs()
if err != nil {
return nil, err
}
- tags := []string{}
- for ref := range refs {
+ tags := &Tags{
+ Origin: &Origin{
+ VCS: "git",
+ URL: r.remoteURL,
+ TagPrefix: prefix,
+ },
+ List: []Tag{},
+ }
+ for ref, hash := range refs {
if !strings.HasPrefix(ref, "refs/tags/") {
continue
}
if !strings.HasPrefix(tag, prefix) {
continue
}
- tags = append(tags, tag)
+ tags.List = append(tags.List, Tag{tag, hash})
}
- sort.Strings(tags)
+ sort.Slice(tags.List, func(i, j int) bool {
+ return tags.List[i].Name < tags.List[j].Name
+ })
+
+ dir := prefix[:strings.LastIndex(prefix, "/")+1]
+ h := sha256.New()
+ for _, tag := range tags.List {
+ if isOriginTag(strings.TrimPrefix(tag.Name, dir)) {
+ fmt.Fprintf(h, "%q %s\n", tag.Name, tag.Hash)
+ }
+ }
+ tags.Origin.TagSum = "t1:" + base64.StdEncoding.EncodeToString(h.Sum(nil))
return tags, nil
}
if refs["HEAD"] == "" {
return nil, ErrNoCommits
}
- return r.Stat(refs["HEAD"])
+ info, err := r.Stat(refs["HEAD"])
+ if err != nil {
+ return nil, err
+ }
+ info.Origin.Ref = "HEAD"
+ info.Origin.Hash = refs["HEAD"]
+ return info, nil
}
// findRef finds some ref name for the given hash,
// stat stats the given rev in the local repository,
// or else it fetches more info from the remote repository and tries again.
-func (r *gitRepo) stat(rev string) (*RevInfo, error) {
+func (r *gitRepo) stat(rev string) (info *RevInfo, err error) {
if r.local {
return r.statLocal(rev, rev)
}
return nil, &UnknownRevisionError{Rev: rev}
}
+ defer func() {
+ if info != nil {
+ info.Origin.Ref = ref
+ info.Origin.Hash = info.Name
+ }
+ }()
+
// Protect r.fetchLevel and the "fetch more and more" sequence.
unlock, err := r.mu.Lock()
if err != nil {
}
info := &RevInfo{
+ Origin: &Origin{
+ VCS: "git",
+ URL: r.remoteURL,
+ Hash: hash,
+ },
Name: hash,
Short: ShortenSHA1(hash),
Time: time.Unix(t, 0).UTC(),
Version: hash,
}
+ if !strings.HasPrefix(hash, rev) {
+ info.Origin.Ref = rev
+ }
// Add tags. Output looks like:
// ede458df7cd0fdca520df19a33158086a8a68e81 1523994202 HEAD -> master, tag: v1.2.4-annotated, tag: v1.2.3, origin/master, origin/HEAD
if err != nil {
return "", err
}
- if len(tags) == 0 {
+ if len(tags.List) == 0 {
return "", nil
}
if err != nil {
return false, err
}
- if len(tags) == 0 {
+ if len(tags.List) == 0 {
return false, nil
}
// For now, at least the hgrepo1 tests check the general vcs.go logic.
// localGitRepo is like gitrepo1 but allows archive access.
-var localGitRepo string
+var localGitRepo, localGitURL string
func testMain(m *testing.M) int {
dir, err := os.MkdirTemp("", "gitrepo-test-")
if _, err := Run(localGitRepo, "git", "config", "daemon.uploadarch", "true"); err != nil {
log.Fatal(err)
}
+
+ // Convert absolute path to file URL. LocalGitRepo will not accept
+ // Windows absolute paths because they look like a host:path remote.
+ // TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
+ if strings.HasPrefix(localGitRepo, "/") {
+ localGitURL = "file://" + localGitRepo
+ } else {
+ localGitURL = "file:///" + filepath.ToSlash(localGitRepo)
+ }
}
}
func testRepo(t *testing.T, remote string) (Repo, error) {
if remote == "localGitRepo" {
- // Convert absolute path to file URL. LocalGitRepo will not accept
- // Windows absolute paths because they look like a host:path remote.
- // TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
- var url string
- if strings.HasPrefix(localGitRepo, "/") {
- url = "file://" + localGitRepo
- } else {
- url = "file:///" + filepath.ToSlash(localGitRepo)
- }
testenv.MustHaveExecPath(t, "git")
- return LocalGitRepo(url)
+ return LocalGitRepo(localGitURL)
}
vcs := "git"
for _, k := range []string{"hg"} {
var tagsTests = []struct {
repo string
prefix string
- tags []string
+ tags []Tag
}{
- {gitrepo1, "xxx", []string{}},
- {gitrepo1, "", []string{"v1.2.3", "v1.2.4-annotated", "v2.0.1", "v2.0.2", "v2.3"}},
- {gitrepo1, "v", []string{"v1.2.3", "v1.2.4-annotated", "v2.0.1", "v2.0.2", "v2.3"}},
- {gitrepo1, "v1", []string{"v1.2.3", "v1.2.4-annotated"}},
- {gitrepo1, "2", []string{}},
+ {gitrepo1, "xxx", []Tag{}},
+ {gitrepo1, "", []Tag{
+ {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
+ {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
+ {"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
+ {"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"},
+ {"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
+ }},
+ {gitrepo1, "v", []Tag{
+ {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
+ {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
+ {"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
+ {"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"},
+ {"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
+ }},
+ {gitrepo1, "v1", []Tag{
+ {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
+ {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
+ }},
+ {gitrepo1, "2", []Tag{}},
}
func TestTags(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if !reflect.DeepEqual(tags, tt.tags) {
- t.Errorf("Tags: incorrect tags\nhave %v\nwant %v", tags, tt.tags)
+ if tags == nil || !reflect.DeepEqual(tags.List, tt.tags) {
+ t.Errorf("Tags(%q): incorrect tags\nhave %v\nwant %v", tt.prefix, tags, tt.tags)
}
}
t.Run(path.Base(tt.repo)+"/"+tt.prefix, f)
if tt.repo == gitrepo1 {
+ // Clear hashes.
+ clearTags := []Tag{}
+ for _, tag := range tt.tags {
+ clearTags = append(clearTags, Tag{tag.Name, ""})
+ }
+ tags := tt.tags
for _, tt.repo = range altRepos {
+ if strings.Contains(tt.repo, "Git") {
+ tt.tags = tags
+ } else {
+ tt.tags = clearTags
+ }
t.Run(path.Base(tt.repo)+"/"+tt.prefix, f)
}
}
{
gitrepo1,
&RevInfo{
+ Origin: &Origin{
+ VCS: "git",
+ URL: "https://vcs-test.golang.org/git/gitrepo1",
+ Ref: "HEAD",
+ Hash: "ede458df7cd0fdca520df19a33158086a8a68e81",
+ },
Name: "ede458df7cd0fdca520df19a33158086a8a68e81",
Short: "ede458df7cd0",
Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
{
hgrepo1,
&RevInfo{
+ Origin: &Origin{
+ VCS: "hg",
+ URL: "https://vcs-test.golang.org/hg/hgrepo1",
+ Hash: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
+ },
Name: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
Short: "18518c07eb8e",
Version: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
t.Fatal(err)
}
if !reflect.DeepEqual(info, tt.info) {
- t.Errorf("Latest: incorrect info\nhave %+v\nwant %+v", *info, *tt.info)
+ t.Errorf("Latest: incorrect info\nhave %+v (origin %+v)\nwant %+v (origin %+v)", info, info.Origin, tt.info, tt.info.Origin)
}
}
t.Run(path.Base(tt.repo), f)
if tt.repo == gitrepo1 {
tt.repo = "localGitRepo"
+ info := *tt.info
+ tt.info = &info
+ o := *info.Origin
+ info.Origin = &o
+ o.URL = localGitURL
t.Run(path.Base(tt.repo), f)
}
}
if !strings.Contains(err.Error(), tt.err) {
t.Fatalf("Stat: wrong error %q, want %q", err, tt.err)
}
- if info != nil {
- t.Errorf("Stat: non-nil info with error %q", err)
+ if info != nil && info.Origin == nil {
+ t.Errorf("Stat: non-nil info with nil Origin with error %q", err)
}
return
}
+ info.Origin = nil // TestLatest and ../../../testdata/script/reuse_git.txt test Origin well enough
if !reflect.DeepEqual(info, tt.info) {
t.Errorf("Stat: incorrect info\nhave %+v\nwant %+v", *info, *tt.info)
}
}
}
-func (r *vcsRepo) Tags(prefix string) ([]string, error) {
+var ErrNoRepoHash = errors.New("RepoHash not supported")
+
+func (r *vcsRepo) CheckReuse(old *Origin, subdir string) error {
+ return fmt.Errorf("vcs %s does not implement CheckReuse", r.cmd.vcs)
+}
+
+func (r *vcsRepo) Tags(prefix string) (*Tags, error) {
unlock, err := r.mu.Lock()
if err != nil {
return nil, err
defer unlock()
r.tagsOnce.Do(r.loadTags)
-
- tags := []string{}
+ tags := &Tags{
+ // None of the other VCS provide a reasonable way to compute TagSum
+ // without downloading the whole repo, so we only include VCS and URL
+ // in the Origin.
+ Origin: &Origin{
+ VCS: r.cmd.vcs,
+ URL: r.remote,
+ },
+ List: []Tag{},
+ }
for tag := range r.tags {
if strings.HasPrefix(tag, prefix) {
- tags = append(tags, tag)
+ tags.List = append(tags.List, Tag{tag, ""})
}
}
- sort.Strings(tags)
+ sort.Slice(tags.List, func(i, j int) bool {
+ return tags.List[i].Name < tags.List[j].Name
+ })
return tags, nil
}
if err != nil {
return nil, &UnknownRevisionError{Rev: rev}
}
- return r.cmd.parseStat(rev, string(out))
+ info, err := r.cmd.parseStat(rev, string(out))
+ if err != nil {
+ return nil, err
+ }
+ if info.Origin == nil {
+ info.Origin = new(Origin)
+ }
+ info.Origin.VCS = r.cmd.vcs
+ info.Origin.URL = r.remote
+ return info, nil
}
func (r *vcsRepo) Latest() (*RevInfo, error) {
sort.Strings(tags)
info := &RevInfo{
+ Origin: &Origin{
+ Hash: hash,
+ },
Name: hash,
Short: ShortenSHA1(hash),
Time: time.Unix(t, 0).UTC(),
version = hash // extend to full hash
}
info := &RevInfo{
+ Origin: &Origin{
+ Hash: hash,
+ },
Name: hash,
Short: ShortenSHA1(hash),
Time: t,
return r.modPath
}
-func (r *codeRepo) Versions(prefix string) ([]string, error) {
+func (r *codeRepo) CheckReuse(old *codehost.Origin) error {
+ return r.code.CheckReuse(old, r.codeDir)
+}
+
+func (r *codeRepo) Versions(prefix string) (*Versions, error) {
// Special case: gopkg.in/macaroon-bakery.v2-unstable
// does not use the v2 tags (those are for macaroon-bakery.v2).
// It has no possible tags at all.
if strings.HasPrefix(r.modPath, "gopkg.in/") && strings.HasSuffix(r.modPath, "-unstable") {
- return nil, nil
+ return &Versions{}, nil
}
p := prefix
}
var list, incompatible []string
- for _, tag := range tags {
- if !strings.HasPrefix(tag, p) {
+ for _, tag := range tags.List {
+ if !strings.HasPrefix(tag.Name, p) {
continue
}
- v := tag
+ v := tag.Name
if r.codeDir != "" {
v = v[len(r.codeDir)+1:]
}
+ // Note: ./codehost/codehost.go's isOriginTag knows about these conditions too.
+ // If these are relaxed, isOriginTag will need to be relaxed as well.
if v == "" || v != semver.Canonical(v) {
// Ignore non-canonical tags: Stat rewrites those to canonical
// pseudo-versions. Note that we compare against semver.Canonical here
semver.Sort(list)
semver.Sort(incompatible)
- return r.appendIncompatibleVersions(list, incompatible)
+ return r.appendIncompatibleVersions(tags.Origin, list, incompatible)
}
// appendIncompatibleVersions appends "+incompatible" versions to list if
// prefix.
//
// Both list and incompatible must be sorted in semantic order.
-func (r *codeRepo) appendIncompatibleVersions(list, incompatible []string) ([]string, error) {
+func (r *codeRepo) appendIncompatibleVersions(origin *codehost.Origin, list, incompatible []string) (*Versions, error) {
+ versions := &Versions{
+ Origin: origin,
+ List: list,
+ }
if len(incompatible) == 0 || r.pathMajor != "" {
// No +incompatible versions are possible, so no need to check them.
- return list, nil
+ return versions, nil
}
versionHasGoMod := func(v string) (bool, error) {
// (github.com/russross/blackfriday@v2.0.0 and
// github.com/libp2p/go-libp2p@v6.0.23), and (as of 2019-10-29) have no
// concrete examples for which it is undesired.
- return list, nil
+ return versions, nil
}
}
// bounds.
continue
}
- list = append(list, v+"+incompatible")
+ versions.List = append(versions.List, v+"+incompatible")
}
- return list, nil
+ return versions, nil
}
func (r *codeRepo) Stat(rev string) (*RevInfo, error) {
return nil, errIncompatible
}
+ origin := info.Origin
+ if module.IsPseudoVersion(v) {
+ // Add tags that are relevant to pseudo-version calculation to origin.
+ prefix := ""
+ if r.codeDir != "" {
+ prefix = r.codeDir + "/"
+ }
+ if r.pathMajor != "" { // "/v2" or "/.v2"
+ prefix += r.pathMajor[1:] + "." // += "v2."
+ }
+ tags, err := r.code.Tags(prefix)
+ if err != nil {
+ return nil, err
+ }
+ o := *origin
+ origin = &o
+ origin.TagPrefix = tags.Origin.TagPrefix
+ origin.TagSum = tags.Origin.TagSum
+ }
+
return &RevInfo{
+ Origin: origin,
Name: info.Name,
Short: info.Short,
Time: info.Time,
var lastTag string // Prefer to log some real tag rather than a canonically-equivalent base.
ancestorFound := false
- for _, tag := range tags {
- versionOnly := strings.TrimPrefix(tag, tagPrefix)
+ for _, tag := range tags.List {
+ versionOnly := strings.TrimPrefix(tag.Name, tagPrefix)
if semver.Compare(versionOnly, base) == 0 {
- lastTag = tag
- ancestorFound, err = r.code.DescendsFrom(info.Name, tag)
+ lastTag = tag.Name
+ ancestorFound, err = r.code.DescendsFrom(info.Name, tag.Name)
if ancestorFound {
break
}
}
func (r *codeRepo) retractedVersions() (func(string) bool, error) {
- versions, err := r.Versions("")
+ vs, err := r.Versions("")
if err != nil {
return nil, err
}
+ versions := vs.List
for i, v := range versions {
if strings.HasSuffix(v, "+incompatible") {
if err != nil {
t.Fatalf("Versions(%q): %v", tt.prefix, err)
}
- if !reflect.DeepEqual(list, tt.versions) {
+ if !reflect.DeepEqual(list.List, tt.versions) {
t.Fatalf("Versions(%q):\nhave %v\nwant %v", tt.prefix, list, tt.versions)
}
})
codehost.Repo
}
-func (ch *fixedTagsRepo) Tags(string) ([]string, error) { return ch.tags, nil }
+func (ch *fixedTagsRepo) Tags(string) (*codehost.Tags, error) {
+ tags := &codehost.Tags{}
+ for _, t := range ch.tags {
+ tags.List = append(tags.List, codehost.Tag{Name: t})
+ }
+ return tags, nil
+}
func TestNonCanonicalSemver(t *testing.T) {
root := "golang.org/x/issue24476"
if err != nil {
t.Fatal(err)
}
- if len(v) != 1 || v[0] != "v1.0.1" {
+ if len(v.List) != 1 || v.List[0] != "v1.0.1" {
t.Fatal("unexpected versions returned:", v)
}
}
return p.path
}
+var errProxyReuse = fmt.Errorf("proxy does not support CheckReuse")
+
+func (p *proxyRepo) CheckReuse(old *codehost.Origin) error {
+ return errProxyReuse
+}
+
// versionError returns err wrapped in a ModuleError for p.path.
func (p *proxyRepo) versionError(version string, err error) error {
if version != "" && version != module.CanonicalVersion(version) {
return resp.Body, nil
}
-func (p *proxyRepo) Versions(prefix string) ([]string, error) {
+func (p *proxyRepo) Versions(prefix string) (*Versions, error) {
data, err := p.getBytes("@v/list")
if err != nil {
p.listLatestOnce.Do(func() {
p.listLatest, p.listLatestErr = p.latestFromList(allLine)
})
semver.Sort(list)
- return list, nil
+ return &Versions{List: list}, nil
}
func (p *proxyRepo) latest() (*RevInfo, error) {
func (p *proxyRepo) latestFromList(allLine []string) (*RevInfo, error) {
var (
- bestTime time.Time
- bestTimeIsFromPseudo bool
- bestVersion string
+ bestTime time.Time
+ bestVersion string
)
for _, line := range allLine {
f := strings.Fields(line)
// If the proxy includes timestamps, prefer the timestamp it reports.
// Otherwise, derive the timestamp from the pseudo-version.
var (
- ft time.Time
- ftIsFromPseudo = false
+ ft time.Time
)
if len(f) >= 2 {
ft, _ = time.Parse(time.RFC3339, f[1])
} else if module.IsPseudoVersion(f[0]) {
ft, _ = module.PseudoVersionTime(f[0])
- ftIsFromPseudo = true
} else {
// Repo.Latest promises that this method is only called where there are
// no tagged versions. Ignore any tagged versions that were added in the
}
if bestTime.Before(ft) {
bestTime = ft
- bestTimeIsFromPseudo = ftIsFromPseudo
bestVersion = f[0]
}
}
return nil, p.versionError("", codehost.ErrNoCommits)
}
- if bestTimeIsFromPseudo {
- // We parsed bestTime from the pseudo-version, but that's in UTC and we're
- // supposed to report the timestamp as reported by the VCS.
- // Stat the selected version to canonicalize the timestamp.
- //
- // TODO(bcmills): Should we also stat other versions to ensure that we
- // report the correct Name and Short for the revision?
- return p.Stat(bestVersion)
- }
-
- return &RevInfo{
- Version: bestVersion,
- Name: bestVersion,
- Short: bestVersion,
- Time: bestTime,
- }, nil
+ // Call Stat to get all the other fields, including Origin information.
+ return p.Stat(bestVersion)
}
func (p *proxyRepo) Stat(rev string) (*RevInfo, error) {
// ModulePath returns the module path.
ModulePath() string
+ // CheckReuse checks whether the validation criteria in the origin
+ // are still satisfied on the server corresponding to this module.
+ // If so, the caller can reuse any cached Versions or RevInfo containing
+ // this origin rather than redownloading those from the server.
+ CheckReuse(old *codehost.Origin) error
+
// Versions lists all known versions with the given prefix.
// Pseudo-versions are not included.
//
//
// If the underlying repository does not exist,
// Versions returns an error matching errors.Is(_, os.NotExist).
- Versions(prefix string) ([]string, error)
+ Versions(prefix string) (*Versions, error)
// Stat returns information about the revision rev.
// A revision can be any identifier known to the underlying service:
Zip(dst io.Writer, version string) error
}
-// A Rev describes a single revision in a module repository.
+// A Versions describes the available versions in a module repository.
+type Versions struct {
+ Origin *codehost.Origin `json:",omitempty"` // origin information for reuse
+
+ List []string // semver versions
+}
+
+// A RevInfo describes a single revision in a module repository.
type RevInfo struct {
Version string // suggested version string for this revision
Time time.Time // commit time
// but they are not recorded when talking about module versions.
Name string `json:"-"` // complete ID in underlying repository
Short string `json:"-"` // shortened ID, for use in pseudo-version
+
+ Origin *codehost.Origin `json:",omitempty"` // provenance for reuse
}
// Re: module paths, import paths, repository roots, and lookups
return l.r.ModulePath()
}
-func (l *loggingRepo) Versions(prefix string) (tags []string, err error) {
+func (l *loggingRepo) CheckReuse(old *codehost.Origin) (err error) {
+ defer func() {
+ logCall("CheckReuse[%s]: %v", l.r.ModulePath(), err)
+ }()
+ return l.r.CheckReuse(old)
+}
+
+func (l *loggingRepo) Versions(prefix string) (*Versions, error) {
defer logCall("Repo[%s]: Versions(%q)", l.r.ModulePath(), prefix)()
return l.r.Versions(prefix)
}
func (r errRepo) ModulePath() string { return r.modulePath }
-func (r errRepo) Versions(prefix string) (tags []string, err error) { return nil, r.err }
-func (r errRepo) Stat(rev string) (*RevInfo, error) { return nil, r.err }
-func (r errRepo) Latest() (*RevInfo, error) { return nil, r.err }
-func (r errRepo) GoMod(version string) ([]byte, error) { return nil, r.err }
-func (r errRepo) Zip(dst io.Writer, version string) error { return r.err }
+func (r errRepo) CheckReuse(old *codehost.Origin) error { return r.err }
+func (r errRepo) Versions(prefix string) (*Versions, error) { return nil, r.err }
+func (r errRepo) Stat(rev string) (*RevInfo, error) { return nil, r.err }
+func (r errRepo) Latest() (*RevInfo, error) { return nil, r.err }
+func (r errRepo) GoMod(version string) ([]byte, error) { return nil, r.err }
+func (r errRepo) Zip(dst io.Writer, version string) error { return r.err }
// A notExistError is like fs.ErrNotExist, but with a custom message
type notExistError struct {
if err != nil {
return err
}
- allowedVersions := make([]string, 0, len(allVersions))
- for _, v := range allVersions {
+ allowedVersions := make([]string, 0, len(allVersions.List))
+ for _, v := range allVersions.List {
if err := allowed(ctx, module.Version{Path: path, Version: v}); err == nil {
allowedVersions = append(allowedVersions, v)
} else if !errors.Is(err, ErrDisallowed) {
if err != nil {
return nil, err
}
- releases, prereleases, err := qm.filterVersions(ctx, versions)
+ releases, prereleases, err := qm.filterVersions(ctx, versions.List)
if err != nil {
return nil, err
}
// available versions, but cannot fetch specific source files.
type versionRepo interface {
ModulePath() string
- Versions(prefix string) ([]string, error)
+ Versions(prefix string) (*modfetch.Versions, error)
Stat(rev string) (*modfetch.RevInfo, error)
Latest() (*modfetch.RevInfo, error)
}
var _ versionRepo = emptyRepo{}
-func (er emptyRepo) ModulePath() string { return er.path }
-func (er emptyRepo) Versions(prefix string) ([]string, error) { return nil, nil }
+func (er emptyRepo) ModulePath() string { return er.path }
+func (er emptyRepo) Versions(prefix string) (*modfetch.Versions, error) {
+ return &modfetch.Versions{}, nil
+}
func (er emptyRepo) Stat(rev string) (*modfetch.RevInfo, error) { return nil, er.err }
func (er emptyRepo) Latest() (*modfetch.RevInfo, error) { return nil, er.err }
// Versions returns the versions from rr.repo augmented with any matching
// replacement versions.
-func (rr *replacementRepo) Versions(prefix string) ([]string, error) {
+func (rr *replacementRepo) Versions(prefix string) (*modfetch.Versions, error) {
repoVersions, err := rr.repo.Versions(prefix)
- if err != nil && !errors.Is(err, os.ErrNotExist) {
- return nil, err
+ if err != nil {
+ if !errors.Is(err, os.ErrNotExist) {
+ return nil, err
+ }
+ repoVersions = new(modfetch.Versions)
}
- versions := repoVersions
+ versions := repoVersions.List
for _, mm := range MainModules.Versions() {
if index := MainModules.Index(mm); index != nil && len(index.replace) > 0 {
path := rr.ModulePath()
}
}
- if len(versions) == len(repoVersions) { // No replacement versions added.
- return versions, nil
+ if len(versions) == len(repoVersions.List) { // replacement versions added
+ return repoVersions, nil
}
sort.Slice(versions, func(i, j int) bool {
return semver.Compare(versions[i], versions[j]) < 0
})
str.Uniq(&versions)
- return versions, nil
+ return &modfetch.Versions{List: versions}, nil
}
func (rr *replacementRepo) Stat(rev string) (*modfetch.RevInfo, error) {