github.com/knieriem/gointernal@v0.2.0-pre2/cmd/go/modfetch/codehost/vcs.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package codehost 6 7 import ( 8 "errors" 9 "fmt" 10 "github.com/knieriem/gointernal/internal/lazyregexp" 11 "io" 12 "io/fs" 13 "os" 14 "path/filepath" 15 "sort" 16 "strconv" 17 "strings" 18 "sync" 19 "time" 20 21 "github.com/knieriem/gointernal/cmd/go/internal/lockedfile" 22 "github.com/knieriem/gointernal/cmd/go/internal/par" 23 "github.com/knieriem/gointernal/cmd/go/internal/str" 24 ) 25 26 // A VCSError indicates an error using a version control system. 27 // The implication of a VCSError is that we know definitively where 28 // to get the code, but we can't access it due to the error. 29 // The caller should report this error instead of continuing to probe 30 // other possible module paths. 31 // 32 // TODO(golang.org/issue/31730): See if we can invert this. (Return a 33 // distinguished error for “repo not found” and treat everything else 34 // as terminal.) 35 type VCSError struct { 36 Err error 37 } 38 39 func (e *VCSError) Error() string { return e.Err.Error() } 40 41 func vcsErrorf(format string, a ...interface{}) error { 42 return &VCSError{Err: fmt.Errorf(format, a...)} 43 } 44 45 func NewRepo(vcs, remote string) (Repo, error) { 46 type key struct { 47 vcs string 48 remote string 49 } 50 type cached struct { 51 repo Repo 52 err error 53 } 54 c := vcsRepoCache.Do(key{vcs, remote}, func() interface{} { 55 repo, err := newVCSRepo(vcs, remote) 56 if err != nil { 57 err = &VCSError{err} 58 } 59 return cached{repo, err} 60 }).(cached) 61 62 return c.repo, c.err 63 } 64 65 var vcsRepoCache par.Cache 66 67 type vcsRepo struct { 68 mu lockedfile.Mutex // protects all commands, so we don't have to decide which are safe on a per-VCS basis 69 70 remote string 71 cmd *vcsCmd 72 dir string 73 74 tagsOnce sync.Once 75 tags map[string]bool 76 77 branchesOnce sync.Once 78 branches map[string]bool 79 80 fetchOnce sync.Once 81 fetchErr error 82 } 83 84 func newVCSRepo(vcs, remote string) (Repo, error) { 85 if vcs == "git" { 86 return newGitRepo(remote, false) 87 } 88 cmd := vcsCmds[vcs] 89 if cmd == nil { 90 return nil, fmt.Errorf("unknown vcs: %s %s", vcs, remote) 91 } 92 if !strings.Contains(remote, "://") { 93 return nil, fmt.Errorf("invalid vcs remote: %s %s", vcs, remote) 94 } 95 96 r := &vcsRepo{remote: remote, cmd: cmd} 97 var err error 98 r.dir, r.mu.Path, err = WorkDir(vcsWorkDirType+vcs, r.remote) 99 if err != nil { 100 return nil, err 101 } 102 103 if cmd.init == nil { 104 return r, nil 105 } 106 107 unlock, err := r.mu.Lock() 108 if err != nil { 109 return nil, err 110 } 111 defer unlock() 112 113 if _, err := os.Stat(filepath.Join(r.dir, "."+vcs)); err != nil { 114 if _, err := Run(r.dir, cmd.init(r.remote)); err != nil { 115 os.RemoveAll(r.dir) 116 return nil, err 117 } 118 } 119 return r, nil 120 } 121 122 const vcsWorkDirType = "vcs1." 123 124 type vcsCmd struct { 125 vcs string // vcs name "hg" 126 init func(remote string) []string // cmd to init repo to track remote 127 tags func(remote string) []string // cmd to list local tags 128 tagRE *lazyregexp.Regexp // regexp to extract tag names from output of tags cmd 129 branches func(remote string) []string // cmd to list local branches 130 branchRE *lazyregexp.Regexp // regexp to extract branch names from output of tags cmd 131 badLocalRevRE *lazyregexp.Regexp // regexp of names that must not be served out of local cache without doing fetch first 132 statLocal func(rev, remote string) []string // cmd to stat local rev 133 parseStat func(rev, out string) (*RevInfo, error) // cmd to parse output of statLocal 134 fetch []string // cmd to fetch everything from remote 135 latest string // name of latest commit on remote (tip, HEAD, etc) 136 readFile func(rev, file, remote string) []string // cmd to read rev's file 137 readZip func(rev, subdir, remote, target string) []string // cmd to read rev's subdir as zip file 138 doReadZip func(dst io.Writer, workDir, rev, subdir, remote string) error // arbitrary function to read rev's subdir as zip file 139 } 140 141 var re = lazyregexp.New 142 143 var vcsCmds = map[string]*vcsCmd{ 144 "hg": { 145 vcs: "hg", 146 init: func(remote string) []string { 147 return []string{"hg", "clone", "-U", "--", remote, "."} 148 }, 149 tags: func(remote string) []string { 150 return []string{"hg", "tags", "-q"} 151 }, 152 tagRE: re(`(?m)^[^\n]+$`), 153 branches: func(remote string) []string { 154 return []string{"hg", "branches", "-c", "-q"} 155 }, 156 branchRE: re(`(?m)^[^\n]+$`), 157 badLocalRevRE: re(`(?m)^(tip)$`), 158 statLocal: func(rev, remote string) []string { 159 return []string{"hg", "log", "-l1", "-r", rev, "--template", "{node} {date|hgdate} {tags}"} 160 }, 161 parseStat: hgParseStat, 162 fetch: []string{"hg", "pull", "-f"}, 163 latest: "tip", 164 readFile: func(rev, file, remote string) []string { 165 return []string{"hg", "cat", "-r", rev, file} 166 }, 167 readZip: func(rev, subdir, remote, target string) []string { 168 pattern := []string{} 169 if subdir != "" { 170 pattern = []string{"-I", subdir + "/**"} 171 } 172 return str.StringList("hg", "archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/", pattern, "--", target) 173 }, 174 }, 175 176 "svn": { 177 vcs: "svn", 178 init: nil, // no local checkout 179 tags: func(remote string) []string { 180 return []string{"svn", "list", "--", strings.TrimSuffix(remote, "/trunk") + "/tags"} 181 }, 182 tagRE: re(`(?m)^(.*?)/?$`), 183 statLocal: func(rev, remote string) []string { 184 suffix := "@" + rev 185 if rev == "latest" { 186 suffix = "" 187 } 188 return []string{"svn", "log", "-l1", "--xml", "--", remote + suffix} 189 }, 190 parseStat: svnParseStat, 191 latest: "latest", 192 readFile: func(rev, file, remote string) []string { 193 return []string{"svn", "cat", "--", remote + "/" + file + "@" + rev} 194 }, 195 doReadZip: svnReadZip, 196 }, 197 198 "bzr": { 199 vcs: "bzr", 200 init: func(remote string) []string { 201 return []string{"bzr", "branch", "--use-existing-dir", "--", remote, "."} 202 }, 203 fetch: []string{ 204 "bzr", "pull", "--overwrite-tags", 205 }, 206 tags: func(remote string) []string { 207 return []string{"bzr", "tags"} 208 }, 209 tagRE: re(`(?m)^\S+`), 210 badLocalRevRE: re(`^revno:-`), 211 statLocal: func(rev, remote string) []string { 212 return []string{"bzr", "log", "-l1", "--long", "--show-ids", "-r", rev} 213 }, 214 parseStat: bzrParseStat, 215 latest: "revno:-1", 216 readFile: func(rev, file, remote string) []string { 217 return []string{"bzr", "cat", "-r", rev, file} 218 }, 219 readZip: func(rev, subdir, remote, target string) []string { 220 extra := []string{} 221 if subdir != "" { 222 extra = []string{"./" + subdir} 223 } 224 return str.StringList("bzr", "export", "--format=zip", "-r", rev, "--root=prefix/", "--", target, extra) 225 }, 226 }, 227 228 "fossil": { 229 vcs: "fossil", 230 init: func(remote string) []string { 231 return []string{"fossil", "clone", "--", remote, ".fossil"} 232 }, 233 fetch: []string{"fossil", "pull", "-R", ".fossil"}, 234 tags: func(remote string) []string { 235 return []string{"fossil", "tag", "-R", ".fossil", "list"} 236 }, 237 tagRE: re(`XXXTODO`), 238 statLocal: func(rev, remote string) []string { 239 return []string{"fossil", "info", "-R", ".fossil", rev} 240 }, 241 parseStat: fossilParseStat, 242 latest: "trunk", 243 readFile: func(rev, file, remote string) []string { 244 return []string{"fossil", "cat", "-R", ".fossil", "-r", rev, file} 245 }, 246 readZip: func(rev, subdir, remote, target string) []string { 247 extra := []string{} 248 if subdir != "" && !strings.ContainsAny(subdir, "*?[],") { 249 extra = []string{"--include", subdir} 250 } 251 // Note that vcsRepo.ReadZip below rewrites this command 252 // to run in a different directory, to work around a fossil bug. 253 return str.StringList("fossil", "zip", "-R", ".fossil", "--name", "prefix", extra, "--", rev, target) 254 }, 255 }, 256 } 257 258 func (r *vcsRepo) loadTags() { 259 out, err := Run(r.dir, r.cmd.tags(r.remote)) 260 if err != nil { 261 return 262 } 263 264 // Run tag-listing command and extract tags. 265 r.tags = make(map[string]bool) 266 for _, tag := range r.cmd.tagRE.FindAllString(string(out), -1) { 267 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(tag) { 268 continue 269 } 270 r.tags[tag] = true 271 } 272 } 273 274 func (r *vcsRepo) loadBranches() { 275 if r.cmd.branches == nil { 276 return 277 } 278 279 out, err := Run(r.dir, r.cmd.branches(r.remote)) 280 if err != nil { 281 return 282 } 283 284 r.branches = make(map[string]bool) 285 for _, branch := range r.cmd.branchRE.FindAllString(string(out), -1) { 286 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(branch) { 287 continue 288 } 289 r.branches[branch] = true 290 } 291 } 292 293 func (r *vcsRepo) Tags(prefix string) ([]string, error) { 294 unlock, err := r.mu.Lock() 295 if err != nil { 296 return nil, err 297 } 298 defer unlock() 299 300 r.tagsOnce.Do(r.loadTags) 301 302 tags := []string{} 303 for tag := range r.tags { 304 if strings.HasPrefix(tag, prefix) { 305 tags = append(tags, tag) 306 } 307 } 308 sort.Strings(tags) 309 return tags, nil 310 } 311 312 func (r *vcsRepo) Stat(rev string) (*RevInfo, error) { 313 unlock, err := r.mu.Lock() 314 if err != nil { 315 return nil, err 316 } 317 defer unlock() 318 319 if rev == "latest" { 320 rev = r.cmd.latest 321 } 322 r.branchesOnce.Do(r.loadBranches) 323 revOK := (r.cmd.badLocalRevRE == nil || !r.cmd.badLocalRevRE.MatchString(rev)) && !r.branches[rev] 324 if revOK { 325 if info, err := r.statLocal(rev); err == nil { 326 return info, nil 327 } 328 } 329 330 r.fetchOnce.Do(r.fetch) 331 if r.fetchErr != nil { 332 return nil, r.fetchErr 333 } 334 info, err := r.statLocal(rev) 335 if err != nil { 336 return nil, err 337 } 338 if !revOK { 339 info.Version = info.Name 340 } 341 return info, nil 342 } 343 344 func (r *vcsRepo) fetch() { 345 if len(r.cmd.fetch) > 0 { 346 _, r.fetchErr = Run(r.dir, r.cmd.fetch) 347 } 348 } 349 350 func (r *vcsRepo) statLocal(rev string) (*RevInfo, error) { 351 out, err := Run(r.dir, r.cmd.statLocal(rev, r.remote)) 352 if err != nil { 353 return nil, &UnknownRevisionError{Rev: rev} 354 } 355 return r.cmd.parseStat(rev, string(out)) 356 } 357 358 func (r *vcsRepo) Latest() (*RevInfo, error) { 359 return r.Stat("latest") 360 } 361 362 func (r *vcsRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) { 363 if rev == "latest" { 364 rev = r.cmd.latest 365 } 366 _, err := r.Stat(rev) // download rev into local repo 367 if err != nil { 368 return nil, err 369 } 370 371 // r.Stat acquires r.mu, so lock after that. 372 unlock, err := r.mu.Lock() 373 if err != nil { 374 return nil, err 375 } 376 defer unlock() 377 378 out, err := Run(r.dir, r.cmd.readFile(rev, file, r.remote)) 379 if err != nil { 380 return nil, fs.ErrNotExist 381 } 382 return out, nil 383 } 384 385 func (r *vcsRepo) RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error) { 386 // We don't technically need to lock here since we're returning an error 387 // uncondititonally, but doing so anyway will help to avoid baking in 388 // lock-inversion bugs. 389 unlock, err := r.mu.Lock() 390 if err != nil { 391 return "", err 392 } 393 defer unlock() 394 395 return "", vcsErrorf("RecentTag not implemented") 396 } 397 398 func (r *vcsRepo) DescendsFrom(rev, tag string) (bool, error) { 399 unlock, err := r.mu.Lock() 400 if err != nil { 401 return false, err 402 } 403 defer unlock() 404 405 return false, vcsErrorf("DescendsFrom not implemented") 406 } 407 408 func (r *vcsRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) { 409 if r.cmd.readZip == nil && r.cmd.doReadZip == nil { 410 return nil, vcsErrorf("ReadZip not implemented for %s", r.cmd.vcs) 411 } 412 413 unlock, err := r.mu.Lock() 414 if err != nil { 415 return nil, err 416 } 417 defer unlock() 418 419 if rev == "latest" { 420 rev = r.cmd.latest 421 } 422 f, err := os.CreateTemp("", "go-readzip-*.zip") 423 if err != nil { 424 return nil, err 425 } 426 if r.cmd.doReadZip != nil { 427 lw := &limitedWriter{ 428 W: f, 429 N: maxSize, 430 ErrLimitReached: errors.New("ReadZip: encoded file exceeds allowed size"), 431 } 432 err = r.cmd.doReadZip(lw, r.dir, rev, subdir, r.remote) 433 if err == nil { 434 _, err = f.Seek(0, io.SeekStart) 435 } 436 } else if r.cmd.vcs == "fossil" { 437 // If you run 438 // fossil zip -R .fossil --name prefix trunk /tmp/x.zip 439 // fossil fails with "unable to create directory /tmp" [sic]. 440 // Change the command to run in /tmp instead, 441 // replacing the -R argument with an absolute path. 442 args := r.cmd.readZip(rev, subdir, r.remote, filepath.Base(f.Name())) 443 for i := range args { 444 if args[i] == ".fossil" { 445 args[i] = filepath.Join(r.dir, ".fossil") 446 } 447 } 448 _, err = Run(filepath.Dir(f.Name()), args) 449 } else { 450 _, err = Run(r.dir, r.cmd.readZip(rev, subdir, r.remote, f.Name())) 451 } 452 if err != nil { 453 f.Close() 454 os.Remove(f.Name()) 455 return nil, err 456 } 457 return &deleteCloser{f}, nil 458 } 459 460 // deleteCloser is a file that gets deleted on Close. 461 type deleteCloser struct { 462 *os.File 463 } 464 465 func (d *deleteCloser) Close() error { 466 defer os.Remove(d.File.Name()) 467 return d.File.Close() 468 } 469 470 func hgParseStat(rev, out string) (*RevInfo, error) { 471 f := strings.Fields(string(out)) 472 if len(f) < 3 { 473 return nil, vcsErrorf("unexpected response from hg log: %q", out) 474 } 475 hash := f[0] 476 version := rev 477 if strings.HasPrefix(hash, version) { 478 version = hash // extend to full hash 479 } 480 t, err := strconv.ParseInt(f[1], 10, 64) 481 if err != nil { 482 return nil, vcsErrorf("invalid time from hg log: %q", out) 483 } 484 485 var tags []string 486 for _, tag := range f[3:] { 487 if tag != "tip" { 488 tags = append(tags, tag) 489 } 490 } 491 sort.Strings(tags) 492 493 info := &RevInfo{ 494 Name: hash, 495 Short: ShortenSHA1(hash), 496 Time: time.Unix(t, 0).UTC(), 497 Version: version, 498 Tags: tags, 499 } 500 return info, nil 501 } 502 503 func bzrParseStat(rev, out string) (*RevInfo, error) { 504 var revno int64 505 var tm time.Time 506 for _, line := range strings.Split(out, "\n") { 507 if line == "" || line[0] == ' ' || line[0] == '\t' { 508 // End of header, start of commit message. 509 break 510 } 511 if line[0] == '-' { 512 continue 513 } 514 i := strings.Index(line, ":") 515 if i < 0 { 516 // End of header, start of commit message. 517 break 518 } 519 key, val := line[:i], strings.TrimSpace(line[i+1:]) 520 switch key { 521 case "revno": 522 if j := strings.Index(val, " "); j >= 0 { 523 val = val[:j] 524 } 525 i, err := strconv.ParseInt(val, 10, 64) 526 if err != nil { 527 return nil, vcsErrorf("unexpected revno from bzr log: %q", line) 528 } 529 revno = i 530 case "timestamp": 531 j := strings.Index(val, " ") 532 if j < 0 { 533 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line) 534 } 535 t, err := time.Parse("2006-01-02 15:04:05 -0700", val[j+1:]) 536 if err != nil { 537 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line) 538 } 539 tm = t.UTC() 540 } 541 } 542 if revno == 0 || tm.IsZero() { 543 return nil, vcsErrorf("unexpected response from bzr log: %q", out) 544 } 545 546 info := &RevInfo{ 547 Name: fmt.Sprintf("%d", revno), 548 Short: fmt.Sprintf("%012d", revno), 549 Time: tm, 550 Version: rev, 551 } 552 return info, nil 553 } 554 555 func fossilParseStat(rev, out string) (*RevInfo, error) { 556 for _, line := range strings.Split(out, "\n") { 557 if strings.HasPrefix(line, "uuid:") || strings.HasPrefix(line, "hash:") { 558 f := strings.Fields(line) 559 if len(f) != 5 || len(f[1]) != 40 || f[4] != "UTC" { 560 return nil, vcsErrorf("unexpected response from fossil info: %q", line) 561 } 562 t, err := time.Parse("2006-01-02 15:04:05", f[2]+" "+f[3]) 563 if err != nil { 564 return nil, vcsErrorf("unexpected response from fossil info: %q", line) 565 } 566 hash := f[1] 567 version := rev 568 if strings.HasPrefix(hash, version) { 569 version = hash // extend to full hash 570 } 571 info := &RevInfo{ 572 Name: hash, 573 Short: ShortenSHA1(hash), 574 Time: t, 575 Version: version, 576 } 577 return info, nil 578 } 579 } 580 return nil, vcsErrorf("unexpected response from fossil info: %q", out) 581 } 582 583 type limitedWriter struct { 584 W io.Writer 585 N int64 586 ErrLimitReached error 587 } 588 589 func (l *limitedWriter) Write(p []byte) (n int, err error) { 590 if l.N > 0 { 591 max := len(p) 592 if l.N < int64(max) { 593 max = int(l.N) 594 } 595 n, err = l.W.Write(p[:max]) 596 l.N -= int64(n) 597 if err != nil || n >= len(p) { 598 return n, err 599 } 600 } 601 602 return n, l.ErrLimitReached 603 }