github.com/sdboyer/gps@v0.16.3/vcs_source.go (about) 1 package gps 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/Masterminds/semver" 14 "github.com/sdboyer/gps/internal/fs" 15 "github.com/sdboyer/gps/pkgtree" 16 ) 17 18 type baseVCSSource struct { 19 repo ctxRepo 20 } 21 22 func (bs *baseVCSSource) sourceType() string { 23 return string(bs.repo.Vcs()) 24 } 25 26 func (bs *baseVCSSource) existsLocally(ctx context.Context) bool { 27 return bs.repo.CheckLocal() 28 } 29 30 // TODO reimpl for git 31 func (bs *baseVCSSource) existsUpstream(ctx context.Context) bool { 32 return !bs.repo.Ping() 33 } 34 35 func (bs *baseVCSSource) upstreamURL() string { 36 return bs.repo.Remote() 37 } 38 39 func (bs *baseVCSSource) getManifestAndLock(ctx context.Context, pr ProjectRoot, r Revision, an ProjectAnalyzer) (Manifest, Lock, error) { 40 err := bs.repo.updateVersion(ctx, r.String()) 41 if err != nil { 42 return nil, nil, unwrapVcsErr(err) 43 } 44 45 m, l, err := an.DeriveManifestAndLock(bs.repo.LocalPath(), pr) 46 if err != nil { 47 return nil, nil, err 48 } 49 50 if l != nil && l != Lock(nil) { 51 l = prepLock(l) 52 } 53 54 return prepManifest(m), l, nil 55 } 56 57 func (bs *baseVCSSource) revisionPresentIn(r Revision) (bool, error) { 58 return bs.repo.IsReference(string(r)), nil 59 } 60 61 // initLocal clones/checks out the upstream repository to disk for the first 62 // time. 63 func (bs *baseVCSSource) initLocal(ctx context.Context) error { 64 err := bs.repo.get(ctx) 65 66 if err != nil { 67 return unwrapVcsErr(err) 68 } 69 return nil 70 } 71 72 // updateLocal ensures the local data (versions and code) we have about the 73 // source is fully up to date with that of the canonical upstream source. 74 func (bs *baseVCSSource) updateLocal(ctx context.Context) error { 75 err := bs.repo.fetch(ctx) 76 77 if err != nil { 78 return unwrapVcsErr(err) 79 } 80 return nil 81 } 82 83 func (bs *baseVCSSource) listPackages(ctx context.Context, pr ProjectRoot, r Revision) (ptree pkgtree.PackageTree, err error) { 84 err = bs.repo.updateVersion(ctx, r.String()) 85 86 if err != nil { 87 err = unwrapVcsErr(err) 88 } else { 89 ptree, err = pkgtree.ListPackages(bs.repo.LocalPath(), string(pr)) 90 } 91 92 return 93 } 94 95 func (bs *baseVCSSource) exportRevisionTo(ctx context.Context, r Revision, to string) error { 96 // Only make the parent dir, as CopyDir will balk on trying to write to an 97 // empty but existing dir. 98 if err := os.MkdirAll(filepath.Dir(to), 0777); err != nil { 99 return err 100 } 101 102 if err := bs.repo.updateVersion(ctx, r.String()); err != nil { 103 return unwrapVcsErr(err) 104 } 105 106 // TODO(sdboyer) this is a simplistic approach and relying on the tools 107 // themselves might make it faster, but git's the overwhelming case (and has 108 // its own method) so fine for now 109 return fs.CopyDir(bs.repo.LocalPath(), to) 110 } 111 112 // gitSource is a generic git repository implementation that should work with 113 // all standard git remotes. 114 type gitSource struct { 115 baseVCSSource 116 } 117 118 func (s *gitSource) exportRevisionTo(ctx context.Context, rev Revision, to string) error { 119 r := s.repo 120 121 if err := os.MkdirAll(to, 0777); err != nil { 122 return err 123 } 124 125 // Back up original index 126 idx, bak := filepath.Join(r.LocalPath(), ".git", "index"), filepath.Join(r.LocalPath(), ".git", "origindex") 127 err := fs.RenameWithFallback(idx, bak) 128 if err != nil { 129 return err 130 } 131 132 // could have an err here...but it's hard to imagine how? 133 defer fs.RenameWithFallback(bak, idx) 134 135 out, err := runFromRepoDir(ctx, r, "git", "read-tree", rev.String()) 136 if err != nil { 137 return fmt.Errorf("%s: %s", out, err) 138 } 139 140 // Ensure we have exactly one trailing slash 141 to = strings.TrimSuffix(to, string(os.PathSeparator)) + string(os.PathSeparator) 142 // Checkout from our temporary index to the desired target location on 143 // disk; now it's git's job to make it fast. 144 // 145 // Sadly, this approach *does* also write out vendor dirs. There doesn't 146 // appear to be a way to make checkout-index respect sparse checkout 147 // rules (-a supercedes it). The alternative is using plain checkout, 148 // though we have a bunch of housekeeping to do to set up, then tear 149 // down, the sparse checkout controls, as well as restore the original 150 // index and HEAD. 151 out, err = runFromRepoDir(ctx, r, "git", "checkout-index", "-a", "--prefix="+to) 152 if err != nil { 153 return fmt.Errorf("%s: %s", out, err) 154 } 155 156 return nil 157 } 158 159 func (s *gitSource) listVersions(ctx context.Context) (vlist []PairedVersion, err error) { 160 r := s.repo 161 162 var out []byte 163 c := newMonitoredCmd(exec.Command("git", "ls-remote", r.Remote()), 30*time.Second) 164 // Ensure no prompting for PWs 165 c.cmd.Env = mergeEnvLists([]string{"GIT_ASKPASS=", "GIT_TERMINAL_PROMPT=0"}, os.Environ()) 166 out, err = c.combinedOutput(ctx) 167 168 if err != nil { 169 return nil, err 170 } 171 172 all := bytes.Split(bytes.TrimSpace(out), []byte("\n")) 173 if len(all) == 1 && len(all[0]) == 0 { 174 return nil, fmt.Errorf("no data returned from ls-remote") 175 } 176 177 // Pull out the HEAD rev (it's always first) so we know what branches to 178 // mark as default. This is, perhaps, not the best way to glean this, but it 179 // was good enough for git itself until 1.8.5. Also, the alternative is 180 // sniffing data out of the pack protocol, which is a separate request, and 181 // also waaaay more than we want to do right now. 182 // 183 // The cost is that we could potentially have multiple branches marked as 184 // the default. If that does occur, a later check (again, emulating git 185 // <1.8.5 behavior) further narrows the failure mode by choosing master as 186 // the sole default branch if a) master exists and b) master is one of the 187 // branches marked as a default. 188 // 189 // This all reduces the failure mode to a very narrow range of 190 // circumstances. Nevertheless, if we do end up emitting multiple 191 // default branches, it is possible that a user could end up following a 192 // non-default branch, IF: 193 // 194 // * Multiple branches match the HEAD rev 195 // * None of them are master 196 // * The solver makes it into the branch list in the version queue 197 // * The user/tool has provided no constraint (so, anyConstraint) 198 // * A branch that is not actually the default, but happens to share the 199 // rev, is lexicographically less than the true default branch 200 // 201 // If all of those conditions are met, then the user would end up with an 202 // erroneous non-default branch in their lock file. 203 headrev := Revision(all[0][:40]) 204 var onedef, multidef, defmaster bool 205 206 smap := make(map[string]bool) 207 uniq := 0 208 vlist = make([]PairedVersion, len(all)-1) // less 1, because always ignore HEAD 209 for _, pair := range all { 210 var v PairedVersion 211 if string(pair[46:51]) == "heads" { 212 rev := Revision(pair[:40]) 213 214 isdef := rev == headrev 215 n := string(pair[52:]) 216 if isdef { 217 if onedef { 218 multidef = true 219 } 220 onedef = true 221 if n == "master" { 222 defmaster = true 223 } 224 } 225 v = branchVersion{ 226 name: n, 227 isDefault: isdef, 228 }.Is(rev).(PairedVersion) 229 230 vlist[uniq] = v 231 uniq++ 232 } else if string(pair[46:50]) == "tags" { 233 vstr := string(pair[51:]) 234 if strings.HasSuffix(vstr, "^{}") { 235 // If the suffix is there, then we *know* this is the rev of 236 // the underlying commit object that we actually want 237 vstr = strings.TrimSuffix(vstr, "^{}") 238 } else if smap[vstr] { 239 // Already saw the deref'd version of this tag, if one 240 // exists, so skip this. 241 continue 242 // Can only hit this branch if we somehow got the deref'd 243 // version first. Which should be impossible, but this 244 // covers us in case of weirdness, anyway. 245 } 246 v = NewVersion(vstr).Is(Revision(pair[:40])).(PairedVersion) 247 smap[vstr] = true 248 vlist[uniq] = v 249 uniq++ 250 } 251 } 252 253 // Trim off excess from the slice 254 vlist = vlist[:uniq] 255 256 // There were multiple default branches, but one was master. So, go through 257 // and strip the default flag from all the non-master branches. 258 if multidef && defmaster { 259 for k, v := range vlist { 260 pv := v.(PairedVersion) 261 if bv, ok := pv.Unpair().(branchVersion); ok { 262 if bv.name != "master" && bv.isDefault == true { 263 bv.isDefault = false 264 vlist[k] = bv.Is(pv.Underlying()) 265 } 266 } 267 } 268 } 269 270 return 271 } 272 273 // gopkginSource is a specialized git source that performs additional filtering 274 // according to the input URL. 275 type gopkginSource struct { 276 gitSource 277 major uint64 278 } 279 280 func (s *gopkginSource) listVersions(ctx context.Context) ([]PairedVersion, error) { 281 ovlist, err := s.gitSource.listVersions(ctx) 282 if err != nil { 283 return nil, err 284 } 285 286 // Apply gopkg.in's filtering rules 287 vlist := make([]PairedVersion, len(ovlist)) 288 k := 0 289 var dbranch int // index of branch to be marked default 290 var bsv *semver.Version 291 for _, v := range ovlist { 292 // all git versions will always be paired 293 pv := v.(versionPair) 294 switch tv := pv.v.(type) { 295 case semVersion: 296 if tv.sv.Major() == s.major { 297 vlist[k] = v 298 k++ 299 } 300 case branchVersion: 301 // The semver lib isn't exactly the same as gopkg.in's logic, but 302 // it's close enough that it's probably fine to use. We can be more 303 // exact if real problems crop up. The most obvious vector for 304 // problems is that we totally ignore the "unstable" designation 305 // right now. 306 sv, err := semver.NewVersion(tv.name) 307 if err != nil || sv.Major() != s.major { 308 // not a semver-shaped branch name at all, or not the same major 309 // version as specified in the import path constraint 310 continue 311 } 312 313 // Turn off the default branch marker unconditionally; we can't know 314 // which one to mark as default until we've seen them all 315 tv.isDefault = false 316 // Figure out if this is the current leader for default branch 317 if bsv == nil || bsv.LessThan(sv) { 318 bsv = sv 319 dbranch = k 320 } 321 pv.v = tv 322 vlist[k] = pv 323 k++ 324 } 325 // The switch skips plainVersions because they cannot possibly meet 326 // gopkg.in's requirements 327 } 328 329 vlist = vlist[:k] 330 if bsv != nil { 331 dbv := vlist[dbranch].(versionPair) 332 vlist[dbranch] = branchVersion{ 333 name: dbv.v.(branchVersion).name, 334 isDefault: true, 335 }.Is(dbv.r) 336 } 337 338 return vlist, nil 339 } 340 341 // bzrSource is a generic bzr repository implementation that should work with 342 // all standard bazaar remotes. 343 type bzrSource struct { 344 baseVCSSource 345 } 346 347 func (s *bzrSource) listVersions(ctx context.Context) ([]PairedVersion, error) { 348 r := s.repo 349 350 // Now, list all the tags 351 out, err := runFromRepoDir(ctx, r, "bzr", "tags", "--show-ids", "-v") 352 if err != nil { 353 return nil, fmt.Errorf("%s: %s", err, string(out)) 354 } 355 356 all := bytes.Split(bytes.TrimSpace(out), []byte("\n")) 357 358 var branchrev []byte 359 branchrev, err = runFromRepoDir(ctx, r, "bzr", "version-info", "--custom", "--template={revision_id}", "--revision=branch:.") 360 br := string(branchrev) 361 if err != nil { 362 return nil, fmt.Errorf("%s: %s", err, br) 363 } 364 365 vlist := make([]PairedVersion, 0, len(all)+1) 366 367 // Now, all the tags. 368 for _, line := range all { 369 idx := bytes.IndexByte(line, 32) // space 370 v := NewVersion(string(line[:idx])) 371 r := Revision(bytes.TrimSpace(line[idx:])) 372 vlist = append(vlist, v.Is(r)) 373 } 374 375 // Last, add the default branch, hardcoding the visual representation of it 376 // that bzr uses when operating in the workflow mode we're using. 377 v := newDefaultBranch("(default)") 378 vlist = append(vlist, v.Is(Revision(string(branchrev)))) 379 380 return vlist, nil 381 } 382 383 // hgSource is a generic hg repository implementation that should work with 384 // all standard mercurial servers. 385 type hgSource struct { 386 baseVCSSource 387 } 388 389 func (s *hgSource) listVersions(ctx context.Context) ([]PairedVersion, error) { 390 var vlist []PairedVersion 391 392 r := s.repo 393 // Now, list all the tags 394 out, err := runFromRepoDir(ctx, r, "hg", "tags", "--debug", "--verbose") 395 if err != nil { 396 return nil, fmt.Errorf("%s: %s", err, string(out)) 397 } 398 399 all := bytes.Split(bytes.TrimSpace(out), []byte("\n")) 400 lbyt := []byte("local") 401 nulrev := []byte("0000000000000000000000000000000000000000") 402 for _, line := range all { 403 if bytes.Equal(lbyt, line[len(line)-len(lbyt):]) { 404 // Skip local tags 405 continue 406 } 407 408 // tip is magic, don't include it 409 if bytes.HasPrefix(line, []byte("tip")) { 410 continue 411 } 412 413 // Split on colon; this gets us the rev and the tag plus local revno 414 pair := bytes.Split(line, []byte(":")) 415 if bytes.Equal(nulrev, pair[1]) { 416 // null rev indicates this tag is marked for deletion 417 continue 418 } 419 420 idx := bytes.IndexByte(pair[0], 32) // space 421 v := NewVersion(string(pair[0][:idx])).Is(Revision(pair[1])).(PairedVersion) 422 vlist = append(vlist, v) 423 } 424 425 // bookmarks next, because the presence of the magic @ bookmark has to 426 // determine how we handle the branches 427 var magicAt bool 428 out, err = runFromRepoDir(ctx, r, "hg", "bookmarks", "--debug") 429 if err != nil { 430 // better nothing than partial and misleading 431 return nil, fmt.Errorf("%s: %s", err, string(out)) 432 } 433 434 out = bytes.TrimSpace(out) 435 if !bytes.Equal(out, []byte("no bookmarks set")) { 436 all = bytes.Split(out, []byte("\n")) 437 for _, line := range all { 438 // Trim leading spaces, and * marker if present 439 line = bytes.TrimLeft(line, " *") 440 pair := bytes.Split(line, []byte(":")) 441 // if this doesn't split exactly once, we have something weird 442 if len(pair) != 2 { 443 continue 444 } 445 446 // Split on colon; this gets us the rev and the branch plus local revno 447 idx := bytes.IndexByte(pair[0], 32) // space 448 // if it's the magic @ marker, make that the default branch 449 str := string(pair[0][:idx]) 450 var v PairedVersion 451 if str == "@" { 452 magicAt = true 453 v = newDefaultBranch(str).Is(Revision(pair[1])).(PairedVersion) 454 } else { 455 v = NewBranch(str).Is(Revision(pair[1])).(PairedVersion) 456 } 457 vlist = append(vlist, v) 458 } 459 } 460 461 out, err = runFromRepoDir(ctx, r, "hg", "branches", "-c", "--debug") 462 if err != nil { 463 // better nothing than partial and misleading 464 return nil, fmt.Errorf("%s: %s", err, string(out)) 465 } 466 467 all = bytes.Split(bytes.TrimSpace(out), []byte("\n")) 468 for _, line := range all { 469 // Trim inactive and closed suffixes, if present; we represent these 470 // anyway 471 line = bytes.TrimSuffix(line, []byte(" (inactive)")) 472 line = bytes.TrimSuffix(line, []byte(" (closed)")) 473 474 // Split on colon; this gets us the rev and the branch plus local revno 475 pair := bytes.Split(line, []byte(":")) 476 idx := bytes.IndexByte(pair[0], 32) // space 477 str := string(pair[0][:idx]) 478 // if there was no magic @ bookmark, and this is mercurial's magic 479 // "default" branch, then mark it as default branch 480 var v PairedVersion 481 if !magicAt && str == "default" { 482 v = newDefaultBranch(str).Is(Revision(pair[1])).(PairedVersion) 483 } else { 484 v = NewBranch(str).Is(Revision(pair[1])).(PairedVersion) 485 } 486 vlist = append(vlist, v) 487 } 488 489 return vlist, nil 490 } 491 492 type repo struct { 493 // Object for direct repo interaction 494 r ctxRepo 495 } 496 497 // This func copied from Masterminds/vcs so we can exec our own commands 498 func mergeEnvLists(in, out []string) []string { 499 NextVar: 500 for _, inkv := range in { 501 k := strings.SplitAfterN(inkv, "=", 2)[0] 502 for i, outkv := range out { 503 if strings.HasPrefix(outkv, k) { 504 out[i] = inkv 505 continue NextVar 506 } 507 } 508 out = append(out, inkv) 509 } 510 return out 511 }