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