| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package gps |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strings" |
| |
| "github.com/Masterminds/semver" |
| "github.com/golang/dep/gps/pkgtree" |
| "github.com/golang/dep/internal/fs" |
| "github.com/pkg/errors" |
| ) |
| |
| type baseVCSSource struct { |
| repo ctxRepo |
| } |
| |
| func (bs *baseVCSSource) sourceType() string { |
| return string(bs.repo.Vcs()) |
| } |
| |
| func (bs *baseVCSSource) existsLocally(ctx context.Context) bool { |
| return bs.repo.CheckLocal() |
| } |
| |
| func (bs *baseVCSSource) existsUpstream(ctx context.Context) bool { |
| return bs.repo.Ping() |
| } |
| |
| func (*baseVCSSource) existsCallsListVersions() bool { |
| return false |
| } |
| |
| func (*baseVCSSource) listVersionsRequiresLocal() bool { |
| return false |
| } |
| |
| func (bs *baseVCSSource) upstreamURL() string { |
| return bs.repo.Remote() |
| } |
| |
| func (bs *baseVCSSource) disambiguateRevision(ctx context.Context, r Revision) (Revision, error) { |
| ci, err := bs.repo.CommitInfo(string(r)) |
| if err != nil { |
| return "", err |
| } |
| return Revision(ci.Commit), nil |
| } |
| |
| func (bs *baseVCSSource) getManifestAndLock(ctx context.Context, pr ProjectRoot, r Revision, an ProjectAnalyzer) (Manifest, Lock, error) { |
| err := bs.repo.updateVersion(ctx, r.String()) |
| if err != nil { |
| return nil, nil, unwrapVcsErr(err) |
| } |
| |
| m, l, err := an.DeriveManifestAndLock(bs.repo.LocalPath(), pr) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| if l != nil && l != Lock(nil) { |
| l = prepLock(l) |
| } |
| |
| return prepManifest(m), l, nil |
| } |
| |
| func (bs *baseVCSSource) revisionPresentIn(r Revision) (bool, error) { |
| return bs.repo.IsReference(string(r)), nil |
| } |
| |
| // initLocal clones/checks out the upstream repository to disk for the first |
| // time. |
| func (bs *baseVCSSource) initLocal(ctx context.Context) error { |
| err := bs.repo.get(ctx) |
| |
| if err != nil { |
| return unwrapVcsErr(err) |
| } |
| return nil |
| } |
| |
| // updateLocal ensures the local data (versions and code) we have about the |
| // source is fully up to date with that of the canonical upstream source. |
| func (bs *baseVCSSource) updateLocal(ctx context.Context) error { |
| err := bs.repo.fetch(ctx) |
| if err == nil { |
| return nil |
| } |
| |
| ec, ok := bs.repo.(ensureCleaner) |
| if !ok { |
| return err |
| } |
| |
| if err := ec.ensureClean(ctx); err != nil { |
| return unwrapVcsErr(err) |
| } |
| |
| if err := bs.repo.fetch(ctx); err != nil { |
| return unwrapVcsErr(err) |
| } |
| return nil |
| } |
| |
| func (bs *baseVCSSource) maybeClean(ctx context.Context) error { |
| ec, ok := bs.repo.(ensureCleaner) |
| if !ok { |
| return nil |
| } |
| |
| if err := ec.ensureClean(ctx); err != nil { |
| return unwrapVcsErr(err) |
| } |
| return nil |
| } |
| |
| func (bs *baseVCSSource) listPackages(ctx context.Context, pr ProjectRoot, r Revision) (ptree pkgtree.PackageTree, err error) { |
| err = bs.repo.updateVersion(ctx, r.String()) |
| |
| if err != nil { |
| err = unwrapVcsErr(err) |
| } else { |
| ptree, err = pkgtree.ListPackages(bs.repo.LocalPath(), string(pr)) |
| } |
| |
| return |
| } |
| |
| func (bs *baseVCSSource) exportRevisionTo(ctx context.Context, r Revision, to string) error { |
| // Only make the parent dir, as CopyDir will balk on trying to write to an |
| // empty but existing dir. |
| if err := os.MkdirAll(filepath.Dir(to), 0777); err != nil { |
| return err |
| } |
| |
| if err := bs.repo.updateVersion(ctx, r.String()); err != nil { |
| return unwrapVcsErr(err) |
| } |
| |
| return fs.CopyDir(bs.repo.LocalPath(), to) |
| } |
| |
| var ( |
| gitHashRE = regexp.MustCompile(`^[a-f0-9]{40}$`) |
| ) |
| |
| // gitSource is a generic git repository implementation that should work with |
| // all standard git remotes. |
| type gitSource struct { |
| baseVCSSource |
| } |
| |
| func (s *gitSource) exportRevisionTo(ctx context.Context, rev Revision, to string) error { |
| r := s.repo |
| |
| if err := os.MkdirAll(to, 0777); err != nil { |
| return err |
| } |
| |
| // Back up original index |
| idx, bak := filepath.Join(r.LocalPath(), ".git", "index"), filepath.Join(r.LocalPath(), ".git", "origindex") |
| err := fs.RenameWithFallback(idx, bak) |
| if err != nil { |
| return err |
| } |
| |
| // could have an err here...but it's hard to imagine how? |
| defer fs.RenameWithFallback(bak, idx) |
| |
| { |
| cmd := commandContext(ctx, "git", "read-tree", rev.String()) |
| cmd.SetDir(r.LocalPath()) |
| if out, err := cmd.CombinedOutput(); err != nil { |
| return errors.Wrap(err, string(out)) |
| } |
| } |
| |
| // Ensure we have exactly one trailing slash |
| to = strings.TrimSuffix(to, string(os.PathSeparator)) + string(os.PathSeparator) |
| // Checkout from our temporary index to the desired target location on |
| // disk; now it's git's job to make it fast. |
| // |
| // Sadly, this approach *does* also write out vendor dirs. There doesn't |
| // appear to be a way to make checkout-index respect sparse checkout |
| // rules (-a supersedes it). The alternative is using plain checkout, |
| // though we have a bunch of housekeeping to do to set up, then tear |
| // down, the sparse checkout controls, as well as restore the original |
| // index and HEAD. |
| { |
| cmd := commandContext(ctx, "git", "checkout-index", "-a", "--prefix="+to) |
| cmd.SetDir(r.LocalPath()) |
| if out, err := cmd.CombinedOutput(); err != nil { |
| return errors.Wrap(err, string(out)) |
| } |
| } |
| |
| return nil |
| } |
| |
| func (s *gitSource) isValidHash(hash []byte) bool { |
| return gitHashRE.Match(hash) |
| } |
| |
| func (*gitSource) existsCallsListVersions() bool { |
| return true |
| } |
| |
| func (s *gitSource) listVersions(ctx context.Context) (vlist []PairedVersion, err error) { |
| r := s.repo |
| |
| cmd := commandContext(ctx, "git", "ls-remote", r.Remote()) |
| // We want to invoke from a place where it's not possible for there to be a |
| // .git file instead of a .git directory, as git ls-remote will choke on the |
| // former and erroneously quit. However, we can't be sure that the repo |
| // exists on disk yet at this point; if it doesn't, then instead use the |
| // parent of the local path, as that's still likely a good bet. |
| if r.CheckLocal() { |
| cmd.SetDir(r.LocalPath()) |
| } else { |
| cmd.SetDir(filepath.Dir(r.LocalPath())) |
| } |
| // Ensure no prompting for PWs |
| cmd.SetEnv(append([]string{"GIT_ASKPASS=", "GIT_TERMINAL_PROMPT=0"}, os.Environ()...)) |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| return nil, errors.Wrap(err, string(out)) |
| } |
| |
| all := bytes.Split(bytes.TrimSpace(out), []byte("\n")) |
| if len(all) == 1 && len(all[0]) == 0 { |
| return nil, fmt.Errorf("no data returned from ls-remote") |
| } |
| |
| // Pull out the HEAD rev (it's always first) so we know what branches to |
| // mark as default. This is, perhaps, not the best way to glean this, but it |
| // was good enough for git itself until 1.8.5. Also, the alternative is |
| // sniffing data out of the pack protocol, which is a separate request, and |
| // also waaaay more than we want to do right now. |
| // |
| // The cost is that we could potentially have multiple branches marked as |
| // the default. If that does occur, a later check (again, emulating git |
| // <1.8.5 behavior) further narrows the failure mode by choosing master as |
| // the sole default branch if a) master exists and b) master is one of the |
| // branches marked as a default. |
| // |
| // This all reduces the failure mode to a very narrow range of |
| // circumstances. Nevertheless, if we do end up emitting multiple |
| // default branches, it is possible that a user could end up following a |
| // non-default branch, IF: |
| // |
| // * Multiple branches match the HEAD rev |
| // * None of them are master |
| // * The solver makes it into the branch list in the version queue |
| // * The user/tool has provided no constraint (so, anyConstraint) |
| // * A branch that is not actually the default, but happens to share the |
| // rev, is lexicographically less than the true default branch |
| // |
| // If all of those conditions are met, then the user would end up with an |
| // erroneous non-default branch in their lock file. |
| var headrev Revision |
| var onedef, multidef, defmaster bool |
| |
| smap := make(map[string]int) |
| uniq := 0 |
| vlist = make([]PairedVersion, len(all)) |
| for _, pair := range all { |
| var v PairedVersion |
| // Valid `git ls-remote` output should start with hash, be at least |
| // 45 chars long and 40th character should be '\t' |
| // |
| // See: https://github.com/golang/dep/pull/1160#issuecomment-328843519 |
| if len(pair) < 45 || pair[40] != '\t' || !s.isValidHash(pair[:40]) { |
| continue |
| } |
| if string(pair[41:]) == "HEAD" { |
| // If HEAD is present, it's always first |
| headrev = Revision(pair[:40]) |
| } else if string(pair[46:51]) == "heads" { |
| rev := Revision(pair[:40]) |
| |
| isdef := rev == headrev |
| n := string(pair[52:]) |
| if isdef { |
| if onedef { |
| multidef = true |
| } |
| onedef = true |
| if n == "master" { |
| defmaster = true |
| } |
| } |
| v = branchVersion{ |
| name: n, |
| isDefault: isdef, |
| }.Pair(rev).(PairedVersion) |
| |
| vlist[uniq] = v |
| uniq++ |
| } else if string(pair[46:50]) == "tags" { |
| vstr := string(pair[51:]) |
| if strings.HasSuffix(vstr, "^{}") { |
| // If the suffix is there, then we *know* this is the rev of |
| // the underlying commit object that we actually want |
| vstr = strings.TrimSuffix(vstr, "^{}") |
| if i, ok := smap[vstr]; ok { |
| v = NewVersion(vstr).Pair(Revision(pair[:40])) |
| vlist[i] = v |
| continue |
| } |
| } else if _, ok := smap[vstr]; ok { |
| // Already saw the deref'd version of this tag, if one |
| // exists, so skip this. |
| continue |
| // Can only hit this branch if we somehow got the deref'd |
| // version first. Which should be impossible, but this |
| // covers us in case of weirdness, anyway. |
| } |
| v = NewVersion(vstr).Pair(Revision(pair[:40])) |
| smap[vstr] = uniq |
| vlist[uniq] = v |
| uniq++ |
| } |
| } |
| |
| // Trim off excess from the slice |
| vlist = vlist[:uniq] |
| |
| // There were multiple default branches, but one was master. So, go through |
| // and strip the default flag from all the non-master branches. |
| if multidef && defmaster { |
| for k, v := range vlist { |
| pv := v.(PairedVersion) |
| if bv, ok := pv.Unpair().(branchVersion); ok { |
| if bv.name != "master" && bv.isDefault { |
| bv.isDefault = false |
| vlist[k] = bv.Pair(pv.Revision()) |
| } |
| } |
| } |
| } |
| |
| return |
| } |
| |
| // gopkginSource is a specialized git source that performs additional filtering |
| // according to the input URL. |
| type gopkginSource struct { |
| gitSource |
| major uint64 |
| unstable bool |
| // The aliased URL we report as being the one we talk to, even though we're |
| // actually talking directly to GitHub. |
| aliasURL string |
| } |
| |
| func (s *gopkginSource) upstreamURL() string { |
| return s.aliasURL |
| } |
| |
| func (s *gopkginSource) listVersions(ctx context.Context) ([]PairedVersion, error) { |
| ovlist, err := s.gitSource.listVersions(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Apply gopkg.in's filtering rules |
| vlist := make([]PairedVersion, len(ovlist)) |
| k := 0 |
| var dbranch int // index of branch to be marked default |
| var bsv semver.Version |
| var defaultBranch PairedVersion |
| tryDefaultAsV0 := s.major == 0 |
| for _, v := range ovlist { |
| // all git versions will always be paired |
| pv := v.(versionPair) |
| switch tv := pv.v.(type) { |
| case semVersion: |
| tryDefaultAsV0 = false |
| if tv.sv.Major() == s.major && !s.unstable { |
| vlist[k] = v |
| k++ |
| } |
| case branchVersion: |
| if tv.isDefault && defaultBranch == nil { |
| defaultBranch = pv |
| } |
| |
| // The semver lib isn't exactly the same as gopkg.in's logic, but |
| // it's close enough that it's probably fine to use. We can be more |
| // exact if real problems crop up. |
| sv, err := semver.NewVersion(tv.name) |
| if err != nil { |
| continue |
| } |
| tryDefaultAsV0 = false |
| |
| if sv.Major() != s.major { |
| // not the same major version as specified in the import path constraint |
| continue |
| } |
| |
| // Gopkg.in has a special "-unstable" suffix which we need to handle |
| // separately. |
| if s.unstable != strings.HasSuffix(tv.name, gopkgUnstableSuffix) { |
| continue |
| } |
| |
| // Turn off the default branch marker unconditionally; we can't know |
| // which one to mark as default until we've seen them all |
| tv.isDefault = false |
| // Figure out if this is the current leader for default branch |
| if bsv == (semver.Version{}) || bsv.LessThan(sv) { |
| bsv = sv |
| dbranch = k |
| } |
| pv.v = tv |
| vlist[k] = pv |
| k++ |
| } |
| // The switch skips plainVersions because they cannot possibly meet |
| // gopkg.in's requirements |
| } |
| |
| vlist = vlist[:k] |
| if bsv != (semver.Version{}) { |
| dbv := vlist[dbranch].(versionPair) |
| vlist[dbranch] = branchVersion{ |
| name: dbv.v.(branchVersion).name, |
| isDefault: true, |
| }.Pair(dbv.r) |
| } |
| |
| // Treat the default branch as v0 only when no other semver branches/tags exist |
| // See http://labix.org/gopkg.in#VersionZero |
| if tryDefaultAsV0 && defaultBranch != nil { |
| vlist = append(vlist, defaultBranch) |
| } |
| |
| return vlist, nil |
| } |
| |
| // bzrSource is a generic bzr repository implementation that should work with |
| // all standard bazaar remotes. |
| type bzrSource struct { |
| baseVCSSource |
| } |
| |
| func (s *bzrSource) exportRevisionTo(ctx context.Context, rev Revision, to string) error { |
| if err := s.baseVCSSource.exportRevisionTo(ctx, rev, to); err != nil { |
| return err |
| } |
| |
| return os.RemoveAll(filepath.Join(to, ".bzr")) |
| } |
| |
| func (s *bzrSource) listVersionsRequiresLocal() bool { |
| return true |
| } |
| |
| func (s *bzrSource) listVersions(ctx context.Context) ([]PairedVersion, error) { |
| r := s.repo |
| |
| // Now, list all the tags |
| tagsCmd := commandContext(ctx, "bzr", "tags", "--show-ids", "-v") |
| tagsCmd.SetDir(r.LocalPath()) |
| out, err := tagsCmd.CombinedOutput() |
| if err != nil { |
| return nil, errors.Wrap(err, string(out)) |
| } |
| |
| all := bytes.Split(bytes.TrimSpace(out), []byte("\n")) |
| |
| viCmd := commandContext(ctx, "bzr", "version-info", "--custom", "--template={revision_id}", "--revision=branch:.") |
| viCmd.SetDir(r.LocalPath()) |
| branchrev, err := viCmd.CombinedOutput() |
| if err != nil { |
| return nil, errors.Wrap(err, string(branchrev)) |
| } |
| |
| vlist := make([]PairedVersion, 0, len(all)+1) |
| |
| // Now, all the tags. |
| for _, line := range all { |
| idx := bytes.IndexByte(line, 32) // space |
| v := NewVersion(string(line[:idx])) |
| r := Revision(bytes.TrimSpace(line[idx:])) |
| vlist = append(vlist, v.Pair(r)) |
| } |
| |
| // Last, add the default branch, hardcoding the visual representation of it |
| // that bzr uses when operating in the workflow mode we're using. |
| v := newDefaultBranch("(default)") |
| vlist = append(vlist, v.Pair(Revision(string(branchrev)))) |
| |
| return vlist, nil |
| } |
| |
| func (s *bzrSource) disambiguateRevision(ctx context.Context, r Revision) (Revision, error) { |
| // If we used the default baseVCSSource behavior here, we would return the |
| // bazaar revision number, which is not a globally unique identifier - it is |
| // only unique within a branch. This is just the way that |
| // github.com/Masterminds/vcs chooses to handle bazaar. We want a |
| // disambiguated unique ID, though, so we need slightly different behavior: |
| // check whether r doesn't error when we try to look it up. If so, trust that |
| // it's a revision. |
| _, err := s.repo.CommitInfo(string(r)) |
| if err != nil { |
| return "", err |
| } |
| return r, nil |
| } |
| |
| // hgSource is a generic hg repository implementation that should work with |
| // all standard mercurial servers. |
| type hgSource struct { |
| baseVCSSource |
| } |
| |
| func (s *hgSource) exportRevisionTo(ctx context.Context, rev Revision, to string) error { |
| // TODO: use hg instead of the generic approach in |
| // baseVCSSource.exportRevisionTo to make it faster. |
| if err := s.baseVCSSource.exportRevisionTo(ctx, rev, to); err != nil { |
| return err |
| } |
| |
| return os.RemoveAll(filepath.Join(to, ".hg")) |
| } |
| |
| func (s *hgSource) listVersionsRequiresLocal() bool { |
| return true |
| } |
| |
| func (s *hgSource) hgCmd(ctx context.Context, args ...string) cmd { |
| r := s.repo |
| cmd := commandContext(ctx, "hg", args...) |
| cmd.SetDir(r.LocalPath()) |
| // Let's make sure extensions don't interfere with our expectations |
| // regarding the output of commands. |
| cmd.Cmd.Env = append(cmd.Cmd.Env, "HGRCPATH=") |
| return cmd |
| } |
| |
| func (s *hgSource) listVersions(ctx context.Context) ([]PairedVersion, error) { |
| var vlist []PairedVersion |
| |
| // Now, list all the tags |
| tagsCmd := s.hgCmd(ctx, "tags", "--debug", "--verbose") |
| out, err := tagsCmd.CombinedOutput() |
| if err != nil { |
| return nil, errors.Wrap(err, string(out)) |
| } |
| |
| all := bytes.Split(bytes.TrimSpace(out), []byte("\n")) |
| lbyt := []byte("local") |
| nulrev := []byte("0000000000000000000000000000000000000000") |
| for _, line := range all { |
| if bytes.Equal(lbyt, line[len(line)-len(lbyt):]) { |
| // Skip local tags |
| continue |
| } |
| |
| // tip is magic, don't include it |
| if bytes.HasPrefix(line, []byte("tip")) { |
| continue |
| } |
| |
| // Split on colon; this gets us the rev and the tag plus local revno |
| pair := bytes.Split(line, []byte(":")) |
| if bytes.Equal(nulrev, pair[1]) { |
| // null rev indicates this tag is marked for deletion |
| continue |
| } |
| |
| idx := bytes.IndexByte(pair[0], 32) // space |
| v := NewVersion(string(pair[0][:idx])).Pair(Revision(pair[1])).(PairedVersion) |
| vlist = append(vlist, v) |
| } |
| |
| // bookmarks next, because the presence of the magic @ bookmark has to |
| // determine how we handle the branches |
| var magicAt bool |
| bookmarksCmd := s.hgCmd(ctx, "bookmarks", "--debug") |
| out, err = bookmarksCmd.CombinedOutput() |
| if err != nil { |
| // better nothing than partial and misleading |
| return nil, errors.Wrap(err, string(out)) |
| } |
| |
| out = bytes.TrimSpace(out) |
| if !bytes.Equal(out, []byte("no bookmarks set")) { |
| all = bytes.Split(out, []byte("\n")) |
| for _, line := range all { |
| // Trim leading spaces, and * marker if present |
| line = bytes.TrimLeft(line, " *") |
| pair := bytes.Split(line, []byte(":")) |
| // if this doesn't split exactly once, we have something weird |
| if len(pair) != 2 { |
| continue |
| } |
| |
| // Split on colon; this gets us the rev and the branch plus local revno |
| idx := bytes.IndexByte(pair[0], 32) // space |
| // if it's the magic @ marker, make that the default branch |
| str := string(pair[0][:idx]) |
| var v PairedVersion |
| if str == "@" { |
| magicAt = true |
| v = newDefaultBranch(str).Pair(Revision(pair[1])).(PairedVersion) |
| } else { |
| v = NewBranch(str).Pair(Revision(pair[1])).(PairedVersion) |
| } |
| vlist = append(vlist, v) |
| } |
| } |
| |
| cmd := s.hgCmd(ctx, "branches", "-c", "--debug") |
| out, err = cmd.CombinedOutput() |
| if err != nil { |
| // better nothing than partial and misleading |
| return nil, errors.Wrap(err, string(out)) |
| } |
| |
| all = bytes.Split(bytes.TrimSpace(out), []byte("\n")) |
| for _, line := range all { |
| // Trim inactive and closed suffixes, if present; we represent these |
| // anyway |
| line = bytes.TrimSuffix(line, []byte(" (inactive)")) |
| line = bytes.TrimSuffix(line, []byte(" (closed)")) |
| |
| // Split on colon; this gets us the rev and the branch plus local revno |
| pair := bytes.Split(line, []byte(":")) |
| idx := bytes.IndexByte(pair[0], 32) // space |
| str := string(pair[0][:idx]) |
| // if there was no magic @ bookmark, and this is mercurial's magic |
| // "default" branch, then mark it as default branch |
| var v PairedVersion |
| if !magicAt && str == "default" { |
| v = newDefaultBranch(str).Pair(Revision(pair[1])).(PairedVersion) |
| } else { |
| v = NewBranch(str).Pair(Revision(pair[1])).(PairedVersion) |
| } |
| vlist = append(vlist, v) |
| } |
| |
| return vlist, nil |
| } |