Left: | ||
Right: |
LEFT | RIGHT |
---|---|
1 // Copyright 2012 The Go Authors. All rights reserved. | 1 // Copyright 2012 The Go Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style | 2 // Use of this source code is governed by a BSD-style |
3 // license that can be found in the LICENSE file. | 3 // license that can be found in the LICENSE file. |
4 | 4 |
5 package main | 5 package main |
6 | 6 |
7 import ( | 7 import ( |
8 "bytes" | 8 "bytes" |
9 "encoding/json" | 9 "encoding/json" |
10 "encoding/xml" | |
11 "errors" | 10 "errors" |
12 "fmt" | 11 "fmt" |
13 "io" | |
14 "log" | 12 "log" |
15 "os" | 13 "os" |
16 "os/exec" | 14 "os/exec" |
15 "path/filepath" | |
17 "regexp" | 16 "regexp" |
18 "strings" | 17 "strings" |
19 ) | 18 ) |
20 | 19 |
21 // A vcsCmd describes how to use a version control system | 20 // A vcsCmd describes how to use a version control system |
22 // like Mercurial, Git, or Subversion. | 21 // like Mercurial, Git, or Subversion. |
23 type vcsCmd struct { | 22 type vcsCmd struct { |
24 name string | 23 name string |
25 cmd string // name of binary to invoke command | 24 cmd string // name of binary to invoke command |
26 | 25 |
(...skipping 239 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
266 prefix string // prefix this description ap plies to | 265 prefix string // prefix this description ap plies to |
267 re string // pattern for import path | 266 re string // pattern for import path |
268 repo string // repository to use (expand with match of re) | 267 repo string // repository to use (expand with match of re) |
269 vcs string // version control system to use (expand with match of re) | 268 vcs string // version control system to use (expand with match of re) |
270 check func(match map[string]string) error // additional checks | 269 check func(match map[string]string) error // additional checks |
271 ping bool // ping for scheme to use to download repo | 270 ping bool // ping for scheme to use to download repo |
272 | 271 |
273 regexp *regexp.Regexp // cached compiled form of re | 272 regexp *regexp.Regexp // cached compiled form of re |
274 } | 273 } |
275 | 274 |
275 // vcsForDir inspects dir and its parents to determine the | |
276 // version control system and code repository to use. | |
277 // On return, root is the import path | |
278 // corresponding to the root of the repository | |
279 // (thus root is a prefix of importPath). | |
280 func vcsForDir(p *Package) (vcs *vcsCmd, root string, err error) { | |
281 // Clean and double-check that dir is in (a subdirectory of) srcRoot. | |
282 dir := filepath.Clean(p.Dir) | |
283 srcRoot := filepath.Clean(p.build.SrcRoot) | |
284 if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator { | |
285 return nil, "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot) | |
286 } | |
287 | |
288 for len(dir) > len(srcRoot) { | |
289 for _, vcs := range vcsList { | |
290 if fi, err := os.Stat(filepath.Join(dir, "."+vcs.cmd)); err == nil && fi.IsDir() { | |
291 return vcs, dir[len(srcRoot)+1:], nil | |
292 } | |
293 } | |
294 | |
295 // Move to parent. | |
296 ndir := filepath.Dir(dir) | |
297 if len(ndir) >= len(dir) { | |
298 // Shouldn't happen, but just in case, stop. | |
299 break | |
300 } | |
301 dir = ndir | |
302 } | |
303 | |
304 return nil, "", fmt.Errorf("directory %q is not using a known version co ntrol system", dir) | |
305 } | |
306 | |
276 // repoRoot represents a version control system, a repo, and a root of | 307 // repoRoot represents a version control system, a repo, and a root of |
277 // where to put it on disk. | 308 // where to put it on disk. |
278 type repoRoot struct { | 309 type repoRoot struct { |
279 vcs *vcsCmd | 310 vcs *vcsCmd |
280 | 311 |
281 // repo is the repository URL, including scheme | 312 // repo is the repository URL, including scheme |
282 repo string | 313 repo string |
283 | 314 |
284 » // root is the import path corresponding to the root of the repository | 315 » // root is the import path corresponding to the root of the |
285 » // (thus root is a prefix of importPath) | 316 » // repository |
niemeyer
2012/02/23 13:40:35
s/importPath/the import path/?
bradfitz
2012/02/23 22:26:58
Deleted. This was moved from an old comment. It no
| |
286 root string | 317 root string |
287 } | 318 } |
288 | 319 |
289 // repoRootForImportPath analyzes importPath to determine the | 320 // repoRootForImportPath analyzes importPath to determine the |
290 // version control system, and code repository to use. | 321 // version control system, and code repository to use. |
291 func repoRootForImportPath(importPath string) (*repoRoot, error) { | 322 func repoRootForImportPath(importPath string) (*repoRoot, error) { |
292 rr, err := repoRootForImportPathStatic(importPath, "") | 323 rr, err := repoRootForImportPathStatic(importPath, "") |
293 if err == errUnknownSite { | 324 if err == errUnknownSite { |
294 » » rr, err = repoRootForImportCustom(importPath) | 325 » » rr, err = repoRootForImportDynamic(importPath) |
295 } | 326 } |
296 return rr, err | 327 return rr, err |
297 } | 328 } |
298 | 329 |
299 var errUnknownSite = errors.New("dynamic lookup required to find mapping") | 330 var errUnknownSite = errors.New("dynamic lookup required to find mapping") |
300 | 331 |
301 // repoRootForImportPathStatic attempts to map importPath to a | 332 // repoRootForImportPathStatic attempts to map importPath to a |
niemeyer
2012/02/23 13:40:35
On the nitpicky side, these names seem to be getti
bradfitz
2012/02/23 22:26:58
I fought with the names for a bit and I'm not a hu
niemeyer
2012/02/23 23:51:39
Sorry, it's just that it jumped in the eye. I gues
| |
302 // repoRoot using the commonly-used VCS hosting sites in vcsPaths | 333 // repoRoot using the commonly-used VCS hosting sites in vcsPaths |
303 // (github.com/user/dir), or from a fully-qualified importPath already | 334 // (github.com/user/dir), or from a fully-qualified importPath already |
304 // containing its VCS type (foo.com/repo.git/dir) | 335 // containing its VCS type (foo.com/repo.git/dir) |
305 // | 336 // |
306 // If scheme is non-empty, that scheme is forced. | 337 // If scheme is non-empty, that scheme is forced. |
307 func repoRootForImportPathStatic(importPath, scheme string) (*repoRoot, error) { | 338 func repoRootForImportPathStatic(importPath, scheme string) (*repoRoot, error) { |
308 if strings.Contains(importPath, "://") { | 339 if strings.Contains(importPath, "://") { |
309 » » return nil, fmt.Errorf("scheme in import path %q", importPath) | 340 » » return nil, fmt.Errorf("invalid import path %q", importPath) |
310 } | 341 } |
311 for _, srv := range vcsPaths { | 342 for _, srv := range vcsPaths { |
312 if !strings.HasPrefix(importPath, srv.prefix) { | 343 if !strings.HasPrefix(importPath, srv.prefix) { |
313 continue | 344 continue |
314 } | 345 } |
315 m := srv.regexp.FindStringSubmatch(importPath) | 346 m := srv.regexp.FindStringSubmatch(importPath) |
316 if m == nil { | 347 if m == nil { |
317 if srv.prefix != "" { | 348 if srv.prefix != "" { |
318 return nil, fmt.Errorf("invalid %s import path % q", srv.prefix, importPath) | 349 return nil, fmt.Errorf("invalid %s import path % q", srv.prefix, importPath) |
319 } | 350 } |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
360 rr := &repoRoot{ | 391 rr := &repoRoot{ |
361 vcs: vcs, | 392 vcs: vcs, |
362 repo: match["repo"], | 393 repo: match["repo"], |
363 root: match["root"], | 394 root: match["root"], |
364 } | 395 } |
365 return rr, nil | 396 return rr, nil |
366 } | 397 } |
367 return nil, errUnknownSite | 398 return nil, errUnknownSite |
368 } | 399 } |
369 | 400 |
370 func repoRootForImportCustom(importPath string) (*repoRoot, error) { | 401 // repoRootForImportDynamic finds a *repoRoot for a custom domain that's not |
niemeyer
2012/02/23 13:40:35
A quick comment above this function regarding what
bradfitz
2012/02/23 22:26:58
Done.
| |
402 // statically known by repoRootForImportPathStatic. | |
403 // | |
404 // This handles "vanity import paths" like "name.tld/pkg/foo". | |
405 func repoRootForImportDynamic(importPath string) (*repoRoot, error) { | |
371 slash := strings.Index(importPath, "/") | 406 slash := strings.Index(importPath, "/") |
372 » if slash == -1 { | 407 » if slash < 0 { |
373 » » return nil, fmt.Errorf("no slash in import %q", importPath) | 408 » » return nil, fmt.Errorf("missing / in import %q", importPath) |
374 » } | 409 » } |
375 » body, err := httpsOrHTTP(importPath) | 410 » urlStr, body, err := httpsOrHTTP(importPath) |
376 if err != nil { | 411 if err != nil { |
377 » » return nil, fmt.Errorf("error fetching %q: %v", importPath, err) | 412 » » return nil, fmt.Errorf("http/https fetch for import %q: %v", imp ortPath, err) |
378 } | 413 } |
379 defer body.Close() | 414 defer body.Close() |
380 metaImport, err := matchGoImport(parseMetaGoImports(body), importPath) | 415 metaImport, err := matchGoImport(parseMetaGoImports(body), importPath) |
381 if err != nil { | 416 if err != nil { |
382 if err != errNoMatch { | 417 if err != errNoMatch { |
383 » » » return nil, fmt.Errorf("error parsing %q: %v", importPat h, err) | 418 » » » return nil, fmt.Errorf("parse %s: %v", urlStr, err) |
384 » » } | 419 » » } |
385 » » return nil, fmt.Errorf("no go-import meta tags fetching %q", imp ortPath) | 420 » » return nil, fmt.Errorf("parse %s: no go-import meta tags", urlSt r) |
386 » } | 421 » } |
387 | 422 » if buildV { |
423 » » log.Printf("get %q: found meta tag %#v at %s", importPath, metaI mport, urlStr) | |
424 » } | |
388 // If the import was "uni.edu/bob/project", which said the | 425 // If the import was "uni.edu/bob/project", which said the |
389 // prefix was "uni.edu" and the RepoRoot was "evilroot.com", | 426 // prefix was "uni.edu" and the RepoRoot was "evilroot.com", |
390 // make sure we don't trust Bob and check out evilroot.com to | 427 // make sure we don't trust Bob and check out evilroot.com to |
391 // "uni.edu" yet (possibly overwriting/preempting another | 428 // "uni.edu" yet (possibly overwriting/preempting another |
392 // non-evil student). Instead, first verify the root and see | 429 // non-evil student). Instead, first verify the root and see |
393 // if it matches Bob's claim. | 430 // if it matches Bob's claim. |
394 if metaImport.Prefix != importPath { | 431 if metaImport.Prefix != importPath { |
395 » » log.Printf("discovery of %q finds prefix %q; verifying prefix... ", importPath, metaImport.Prefix) | 432 » » if buildV { |
396 » » body, err = httpsOrHTTP(metaImport.Prefix) | 433 » » » log.Printf("get %q: verifying non-authoritative meta tag ", importPath) |
434 » » } | |
435 » » urlStr0 := urlStr | |
436 » » urlStr, body, err = httpsOrHTTP(metaImport.Prefix) | |
397 if err != nil { | 437 if err != nil { |
398 » » » log.Printf("meta's prefix %q doesn't match desired impor t path %q, but fetching %q's meta to verify it failed", | 438 » » » return nil, fmt.Errorf("fetch %s: %v", urlStr, err) |
399 » » » » metaImport.Prefix, importPath, metaImport.Prefix ) | |
400 » » » return nil, fmt.Errorf("error fetching %q: %v", metaImpo rt.Prefix, err) | |
401 } | 439 } |
402 imports := parseMetaGoImports(body) | 440 imports := parseMetaGoImports(body) |
403 if len(imports) == 0 { | 441 if len(imports) == 0 { |
404 » » » return nil, fmt.Errorf("no go-imports meta tags found pa rsing %q", metaImport.Prefix) | 442 » » » return nil, fmt.Errorf("fetch %s: no go-import meta tag" , urlStr) |
405 » » } | 443 » » } |
406 » » metaImport, err = matchGoImport(imports, importPath) | 444 » » metaImport2, err := matchGoImport(imports, importPath) |
407 » » if err != nil { | 445 » » if err != nil || metaImport != metaImport2 { |
408 » » » return nil, fmt.Errorf("meta's prefix %q doesn't match d esired import path %q, and parsing %q's HTML = %v", | 446 » » » return nil, fmt.Errorf("%s and %s disagree about go-impo rt for %s", urlStr0, urlStr, metaImport.Prefix) |
409 » » » » metaImport.Prefix, importPath, metaImport.Prefix , err) | 447 » » } |
410 » » } | 448 » } |
411 » } | 449 |
412 | 450 » if !strings.Contains(metaImport.RepoRoot, "://") { |
413 » repoScheme := "" // "http://", "https://", or "" | 451 » » return nil, fmt.Errorf("%s: invalid repo root %q; no scheme", ur lStr, metaImport.RepoRoot) |
414 » if i := strings.Index(metaImport.RepoRoot, "://"); i != -1 { | 452 » } |
415 » » repoScheme = metaImport.RepoRoot[:i] | 453 » rr := &repoRoot{ |
416 » » metaImport.RepoRoot = metaImport.RepoRoot[i+3:] | 454 » » vcs: vcsByCmd(metaImport.VCS), |
417 » } | 455 » » repo: metaImport.RepoRoot, |
418 | 456 » » root: metaImport.Prefix, |
419 » newImportPath := metaImport.RepoRoot | 457 » } |
420 » rr, err := repoRootForImportPathStatic(newImportPath, repoScheme) | 458 » if rr.vcs == nil { |
421 » if err != nil { | 459 » » return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, metaImport. VCS) |
422 » » // Try again, with the ".vcs" suffix. | 460 » } |
niemeyer
2012/02/23 13:40:35
Is this necessary? If the static import path needs
bradfitz
2012/02/23 22:26:58
I thought so too, and was thinking along those lin
niemeyer
2012/02/23 23:51:39
We can easily not make an error to provide a URL i
| |
423 » » newImportPath += "." + strings.ToLower(metaImport.VCS) | 461 » return rr, nil |
424 » » rr, err = repoRootForImportPathStatic(newImportPath, repoScheme) | |
425 » » if err != nil { | |
426 » » » return nil, err | |
427 » » } | |
428 » } | |
429 » rr.root = metaImport.Prefix | |
430 » return rr, err | |
431 } | 462 } |
432 | 463 |
433 // metaImport represents the parsed <meta name="go-import" | 464 // metaImport represents the parsed <meta name="go-import" |
434 // content="prefix vcs reporoot" /> tags from HTML files. | 465 // content="prefix vcs reporoot" /> tags from HTML files. |
435 type metaImport struct { | 466 type metaImport struct { |
436 Prefix, VCS, RepoRoot string | 467 Prefix, VCS, RepoRoot string |
437 } | 468 } |
438 | 469 |
439 // errNoMatch is returned from matchGoImport when there's no applicable match. | 470 // errNoMatch is returned from matchGoImport when there's no applicable match. |
440 var errNoMatch = errors.New("no import match") | 471 var errNoMatch = errors.New("no import match") |
(...skipping 11 matching lines...) Expand all Loading... | |
452 err = fmt.Errorf("multiple meta tags match import path % q", importPath) | 483 err = fmt.Errorf("multiple meta tags match import path % q", importPath) |
453 return | 484 return |
454 } | 485 } |
455 match = i | 486 match = i |
456 } | 487 } |
457 if match == -1 { | 488 if match == -1 { |
458 err = errNoMatch | 489 err = errNoMatch |
459 return | 490 return |
460 } | 491 } |
461 return imports[match], nil | 492 return imports[match], nil |
462 } | |
463 | |
464 // parseMetaGoImports returns meta imports from the HTML in r. | |
465 // Parsing ends at the end of the <head> section or the beginning of the <body>. | |
466 func parseMetaGoImports(r io.Reader) (imports []metaImport) { | |
467 d := xml.NewDecoder(r) | |
468 d.Strict = false | |
469 for { | |
470 t, err := d.Token() | |
471 if err != nil { | |
472 return | |
473 } | |
474 if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name .Local, "body") { | |
475 return | |
476 } | |
477 if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.L ocal, "head") { | |
478 return | |
479 } | |
480 e, ok := t.(xml.StartElement) | |
481 if !ok || !strings.EqualFold(e.Name.Local, "meta") { | |
niemeyer
2012/02/23 13:40:35
Shouldn't this be checking for "go-import" in the
bradfitz
2012/02/23 22:26:58
Whoops, indeed. Fixed.
| |
482 continue | |
483 } | |
484 if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 { | |
485 imports = append(imports, metaImport{ | |
486 Prefix: f[0], | |
487 VCS: f[1], | |
488 RepoRoot: f[2], | |
489 }) | |
490 } | |
491 } | |
492 return | |
493 } | |
494 | |
495 // attrValue returns the attribute value for the case-insensitive key | |
496 // `name', or the empty string if nothing is found. | |
497 func attrValue(attrs []xml.Attr, name string) string { | |
498 for _, a := range attrs { | |
499 if strings.EqualFold(a.Name.Local, name) { | |
500 return a.Value | |
501 } | |
502 } | |
503 return "" | |
504 } | 493 } |
505 | 494 |
506 // expand rewrites s to replace {k} with match[k] for each key k in match. | 495 // expand rewrites s to replace {k} with match[k] for each key k in match. |
507 func expand(match map[string]string, s string) string { | 496 func expand(match map[string]string, s string) string { |
508 for k, v := range match { | 497 for k, v := range match { |
509 s = strings.Replace(s, "{"+k+"}", v, -1) | 498 s = strings.Replace(s, "{"+k+"}", v, -1) |
510 } | 499 } |
511 return s | 500 return s |
512 } | 501 } |
513 | 502 |
(...skipping 145 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
659 if match["project"] == "" || match["series"] == "" { | 648 if match["project"] == "" || match["series"] == "" { |
660 return nil | 649 return nil |
661 } | 650 } |
662 _, err := httpGET(expand(match, "https://code.launchpad.net/{project}{se ries}/.bzr/branch-format")) | 651 _, err := httpGET(expand(match, "https://code.launchpad.net/{project}{se ries}/.bzr/branch-format")) |
663 if err != nil { | 652 if err != nil { |
664 match["root"] = expand(match, "launchpad.net/{project}") | 653 match["root"] = expand(match, "launchpad.net/{project}") |
665 match["repo"] = expand(match, "https://{root}") | 654 match["repo"] = expand(match, "https://{root}") |
666 } | 655 } |
667 return nil | 656 return nil |
668 } | 657 } |
LEFT | RIGHT |