Skip to content

Commit 84e091e

Browse files
committed
cmd/go: record origin metadata during module download
This change adds an "Origin" JSON key to the output of go list -json -m and go mod download -json. The associated value is a JSON object with metadata about the source control system. For Git, that metadata is sufficient to evaluate whether the remote server has changed in any interesting way that might invalidate the cached data. In most cases, it will not have, and a fetch could then avoid downloading a full repo from the server. This origin metadata is also now recorded in the .info file for a given module@version, for informational and debugging purposes. This change only adds the metadata. It does not use it to optimize away unnecessary git fetch operations. (That's the next change.) For #53644. Change-Id: I4a1712a2386d1d8ab4e02ffdf0f72ba75d556115 Reviewed-on: https://go-review.googlesource.com/c/go/+/411397 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Russ Cox <[email protected]> Reviewed-by: Bryan Mills <[email protected]>
1 parent ceda93e commit 84e091e

File tree

12 files changed

+435
-121
lines changed

12 files changed

+435
-121
lines changed

src/cmd/go/internal/modcmd/download.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func runDownload(ctx context.Context, cmd *base.Command, args []string) {
149149

150150
downloadModule := func(m *moduleJSON) {
151151
var err error
152-
m.Info, err = modfetch.InfoFile(m.Path, m.Version)
152+
_, m.Info, err = modfetch.InfoFile(m.Path, m.Version)
153153
if err != nil {
154154
m.Error = err.Error()
155155
return

src/cmd/go/internal/modfetch/cache.go

+28-16
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func SideLock() (unlock func(), err error) {
164164
}
165165

166166
// A cachingRepo is a cache around an underlying Repo,
167-
// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
167+
// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not CheckReuse or Zip).
168168
// It is also safe for simultaneous use by multiple goroutines
169169
// (so that it can be returned from Lookup multiple times).
170170
// It serializes calls to the underlying Repo.
@@ -195,24 +195,32 @@ func (r *cachingRepo) repo() Repo {
195195
return r.r
196196
}
197197

198+
func (r *cachingRepo) CheckReuse(old *codehost.Origin) error {
199+
return r.repo().CheckReuse(old)
200+
}
201+
198202
func (r *cachingRepo) ModulePath() string {
199203
return r.path
200204
}
201205

202-
func (r *cachingRepo) Versions(prefix string) ([]string, error) {
206+
func (r *cachingRepo) Versions(prefix string) (*Versions, error) {
203207
type cached struct {
204-
list []string
205-
err error
208+
v *Versions
209+
err error
206210
}
207211
c := r.cache.Do("versions:"+prefix, func() any {
208-
list, err := r.repo().Versions(prefix)
209-
return cached{list, err}
212+
v, err := r.repo().Versions(prefix)
213+
return cached{v, err}
210214
}).(cached)
211215

212216
if c.err != nil {
213217
return nil, c.err
214218
}
215-
return append([]string(nil), c.list...), nil
219+
v := &Versions{
220+
Origin: c.v.Origin,
221+
List: append([]string(nil), c.v.List...),
222+
}
223+
return v, nil
216224
}
217225

218226
type cachedInfo struct {
@@ -310,31 +318,35 @@ func (r *cachingRepo) Zip(dst io.Writer, version string) error {
310318
return r.repo().Zip(dst, version)
311319
}
312320

313-
// InfoFile is like Lookup(path).Stat(version) but returns the name of the file
321+
// InfoFile is like Lookup(path).Stat(version) but also returns the name of the file
314322
// containing the cached information.
315-
func InfoFile(path, version string) (string, error) {
323+
func InfoFile(path, version string) (*RevInfo, string, error) {
316324
if !semver.IsValid(version) {
317-
return "", fmt.Errorf("invalid version %q", version)
325+
return nil, "", fmt.Errorf("invalid version %q", version)
318326
}
319327

320-
if file, _, err := readDiskStat(path, version); err == nil {
321-
return file, nil
328+
if file, info, err := readDiskStat(path, version); err == nil {
329+
return info, file, nil
322330
}
323331

332+
var info *RevInfo
324333
err := TryProxies(func(proxy string) error {
325-
_, err := Lookup(proxy, path).Stat(version)
334+
i, err := Lookup(proxy, path).Stat(version)
335+
if err == nil {
336+
info = i
337+
}
326338
return err
327339
})
328340
if err != nil {
329-
return "", err
341+
return nil, "", err
330342
}
331343

332344
// Stat should have populated the disk cache for us.
333345
file, err := CachePath(module.Version{Path: path, Version: version}, "info")
334346
if err != nil {
335-
return "", err
347+
return nil, "", err
336348
}
337-
return file, nil
349+
return info, file, nil
338350
}
339351

340352
// GoMod is like Lookup(path).GoMod(rev) but avoids the

src/cmd/go/internal/modfetch/codehost/codehost.go

+88-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
"cmd/go/internal/cfg"
2323
"cmd/go/internal/lockedfile"
2424
"cmd/go/internal/str"
25+
26+
"golang.org/x/mod/module"
27+
"golang.org/x/mod/semver"
2528
)
2629

2730
// Downloaded size limits.
@@ -36,8 +39,15 @@ const (
3639
// remote version control servers, and code hosting sites.
3740
// A Repo must be safe for simultaneous use by multiple goroutines.
3841
type Repo interface {
42+
// CheckReuse checks whether the old origin information
43+
// remains up to date. If so, whatever cached object it was
44+
// taken from can be reused.
45+
// The subdir gives subdirectory name where the module root is expected to be found,
46+
// "" for the root or "sub/dir" for a subdirectory (no trailing slash).
47+
CheckReuse(old *Origin, subdir string) error
48+
3949
// List lists all tags with the given prefix.
40-
Tags(prefix string) (tags []string, err error)
50+
Tags(prefix string) (*Tags, error)
4151

4252
// Stat returns information about the revision rev.
4353
// A revision can be any identifier known to the underlying service:
@@ -74,8 +84,84 @@ type Repo interface {
7484
DescendsFrom(rev, tag string) (bool, error)
7585
}
7686

77-
// A Rev describes a single revision in a source code repository.
87+
// An Origin describes the provenance of a given repo method result.
88+
// It can be passed to CheckReuse (usually in a different go command invocation)
89+
// to see whether the result remains up-to-date.
90+
type Origin struct {
91+
VCS string `json:",omitempty"` // "git" etc
92+
URL string `json:",omitempty"` // URL of repository
93+
Subdir string `json:",omitempty"` // subdirectory in repo
94+
95+
// If TagSum is non-empty, then the resolution of this module version
96+
// depends on the set of tags present in the repo, specifically the tags
97+
// of the form TagPrefix + a valid semver version.
98+
// If the matching repo tags and their commit hashes still hash to TagSum,
99+
// the Origin is still valid (at least as far as the tags are concerned).
100+
// The exact checksum is up to the Repo implementation; see (*gitRepo).Tags.
101+
TagPrefix string `json:",omitempty"`
102+
TagSum string `json:",omitempty"`
103+
104+
// If Ref is non-empty, then the resolution of this module version
105+
// depends on Ref resolving to the revision identified by Hash.
106+
// If Ref still resolves to Hash, the Origin is still valid (at least as far as Ref is concerned).
107+
// For Git, the Ref is a full ref like "refs/heads/main" or "refs/tags/v1.2.3",
108+
// and the Hash is the Git object hash the ref maps to.
109+
// Other VCS might choose differently, but the idea is that Ref is the name
110+
// with a mutable meaning while Hash is a name with an immutable meaning.
111+
Ref string `json:",omitempty"`
112+
Hash string `json:",omitempty"`
113+
}
114+
115+
// Checkable reports whether the Origin contains anything that can be checked.
116+
// If not, it's purely informational and should fail a CheckReuse call.
117+
func (o *Origin) Checkable() bool {
118+
return o.TagSum != "" || o.Ref != "" || o.Hash != ""
119+
}
120+
121+
func (o *Origin) Merge(other *Origin) {
122+
if o.TagSum == "" {
123+
o.TagPrefix = other.TagPrefix
124+
o.TagSum = other.TagSum
125+
}
126+
if o.Ref == "" {
127+
o.Ref = other.Ref
128+
o.Hash = other.Hash
129+
}
130+
}
131+
132+
// A Tags describes the available tags in a code repository.
133+
type Tags struct {
134+
Origin *Origin
135+
List []Tag
136+
}
137+
138+
// A Tag describes a single tag in a code repository.
139+
type Tag struct {
140+
Name string
141+
Hash string // content hash identifying tag's content, if available
142+
}
143+
144+
// isOriginTag reports whether tag should be preserved
145+
// in the Tags method's Origin calculation.
146+
// We can safely ignore tags that are not look like pseudo-versions,
147+
// because ../coderepo.go's (*codeRepo).Versions ignores them too.
148+
// We can also ignore non-semver tags, but we have to include semver
149+
// tags with extra suffixes, because the pseudo-version base finder uses them.
150+
func isOriginTag(tag string) bool {
151+
// modfetch.(*codeRepo).Versions uses Canonical == tag,
152+
// but pseudo-version calculation has a weaker condition that
153+
// the canonical is a prefix of the tag.
154+
// Include those too, so that if any new one appears, we'll invalidate the cache entry.
155+
// This will lead to spurious invalidation of version list results,
156+
// but tags of this form being created should be fairly rare
157+
// (and invalidate pseudo-version results anyway).
158+
c := semver.Canonical(tag)
159+
return c != "" && strings.HasPrefix(tag, c) && !module.IsPseudoVersion(tag)
160+
}
161+
162+
// A RevInfo describes a single revision in a source code repository.
78163
type RevInfo struct {
164+
Origin *Origin
79165
Name string // complete ID in underlying repository
80166
Short string // shortened ID, for use in pseudo-version
81167
Version string // version used in lookup

0 commit comments

Comments
 (0)