v.io/jiri@v0.0.0-20160715023856-abfb8b131290/project/project.go (about) 1 // Copyright 2015 The Vanadium 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 project 6 7 import ( 8 "bytes" 9 "encoding/xml" 10 "fmt" 11 "hash/fnv" 12 "io/ioutil" 13 "net/url" 14 "os" 15 "path/filepath" 16 "sort" 17 "strings" 18 "time" 19 20 "v.io/jiri" 21 "v.io/jiri/collect" 22 "v.io/jiri/gitutil" 23 "v.io/jiri/googlesource" 24 "v.io/jiri/runutil" 25 "v.io/x/lib/set" 26 ) 27 28 var JiriProject = "release.go.jiri" 29 var JiriName = "jiri" 30 var JiriPackage = "v.io/jiri" 31 32 // CL represents a changelist. 33 type CL struct { 34 // Author identifies the author of the changelist. 35 Author string 36 // Email identifies the author's email. 37 Email string 38 // Description holds the description of the changelist. 39 Description string 40 } 41 42 // Manifest represents a setting used for updating the universe. 43 type Manifest struct { 44 Imports []Import `xml:"imports>import"` 45 LocalImports []LocalImport `xml:"imports>localimport"` 46 Projects []Project `xml:"projects>project"` 47 Tools []Tool `xml:"tools>tool"` 48 // SnapshotPath is the relative path to the snapshot file from JIRI_ROOT. 49 // It is only set when creating a snapshot. 50 SnapshotPath string `xml:"snapshotpath,attr,omitempty"` 51 XMLName struct{} `xml:"manifest"` 52 } 53 54 // ManifestFromBytes returns a manifest parsed from data, with defaults filled 55 // in. 56 func ManifestFromBytes(data []byte) (*Manifest, error) { 57 m := new(Manifest) 58 if err := xml.Unmarshal(data, m); err != nil { 59 return nil, err 60 } 61 if err := m.fillDefaults(); err != nil { 62 return nil, err 63 } 64 return m, nil 65 } 66 67 // ManifestFromFile returns a manifest parsed from the contents of filename, 68 // with defaults filled in. 69 // 70 // Note that unlike ProjectFromFile, ManifestFromFile does not convert project 71 // paths to absolute paths because it's possible to load a manifest with a 72 // specific root directory different from jirix.Root. The usual way to load a 73 // manifest is through LoadManifest, which does absolutize the paths, and uses 74 // the correct root directory. 75 func ManifestFromFile(jirix *jiri.X, filename string) (*Manifest, error) { 76 data, err := jirix.NewSeq().ReadFile(filename) 77 if err != nil { 78 return nil, err 79 } 80 m, err := ManifestFromBytes(data) 81 if err != nil { 82 return nil, fmt.Errorf("invalid manifest %s: %v", filename, err) 83 } 84 return m, nil 85 } 86 87 var ( 88 newlineBytes = []byte("\n") 89 emptyImportsBytes = []byte("\n <imports></imports>\n") 90 emptyProjectsBytes = []byte("\n <projects></projects>\n") 91 emptyToolsBytes = []byte("\n <tools></tools>\n") 92 93 endElemBytes = []byte("/>\n") 94 endImportBytes = []byte("></import>\n") 95 endLocalImportBytes = []byte("></localimport>\n") 96 endProjectBytes = []byte("></project>\n") 97 endToolBytes = []byte("></tool>\n") 98 99 endImportSoloBytes = []byte("></import>") 100 endProjectSoloBytes = []byte("></project>") 101 endElemSoloBytes = []byte("/>") 102 ) 103 104 // deepCopy returns a deep copy of Manifest. 105 func (m *Manifest) deepCopy() *Manifest { 106 x := new(Manifest) 107 x.SnapshotPath = m.SnapshotPath 108 x.Imports = append([]Import(nil), m.Imports...) 109 x.LocalImports = append([]LocalImport(nil), m.LocalImports...) 110 x.Projects = append([]Project(nil), m.Projects...) 111 x.Tools = append([]Tool(nil), m.Tools...) 112 return x 113 } 114 115 // ToBytes returns m as serialized bytes, with defaults unfilled. 116 func (m *Manifest) ToBytes() ([]byte, error) { 117 m = m.deepCopy() // avoid changing manifest when unfilling defaults. 118 if err := m.unfillDefaults(); err != nil { 119 return nil, err 120 } 121 data, err := xml.MarshalIndent(m, "", " ") 122 if err != nil { 123 return nil, fmt.Errorf("manifest xml.Marshal failed: %v", err) 124 } 125 // It's hard (impossible?) to get xml.Marshal to elide some of the empty 126 // elements, or produce short empty elements, so we post-process the data. 127 data = bytes.Replace(data, emptyImportsBytes, newlineBytes, -1) 128 data = bytes.Replace(data, emptyProjectsBytes, newlineBytes, -1) 129 data = bytes.Replace(data, emptyToolsBytes, newlineBytes, -1) 130 data = bytes.Replace(data, endImportBytes, endElemBytes, -1) 131 data = bytes.Replace(data, endLocalImportBytes, endElemBytes, -1) 132 data = bytes.Replace(data, endProjectBytes, endElemBytes, -1) 133 data = bytes.Replace(data, endToolBytes, endElemBytes, -1) 134 if !bytes.HasSuffix(data, newlineBytes) { 135 data = append(data, '\n') 136 } 137 return data, nil 138 } 139 140 func safeWriteFile(jirix *jiri.X, filename string, data []byte) error { 141 tmp := filename + ".tmp" 142 return jirix.NewSeq(). 143 MkdirAll(filepath.Dir(filename), 0755). 144 WriteFile(tmp, data, 0644). 145 Rename(tmp, filename). 146 Done() 147 } 148 149 // ToFile writes the manifest m to a file with the given filename, with 150 // defaults unfilled and all project paths relative to the jiri root. 151 func (m *Manifest) ToFile(jirix *jiri.X, filename string) error { 152 // Replace absolute paths with relative paths to make it possible to move 153 // the $JIRI_ROOT directory locally. 154 projects := []Project{} 155 for _, project := range m.Projects { 156 if err := project.relativizePaths(jirix.Root); err != nil { 157 return err 158 } 159 projects = append(projects, project) 160 } 161 m.Projects = projects 162 data, err := m.ToBytes() 163 if err != nil { 164 return err 165 } 166 return safeWriteFile(jirix, filename, data) 167 } 168 169 func (m *Manifest) fillDefaults() error { 170 for index := range m.Imports { 171 if err := m.Imports[index].fillDefaults(); err != nil { 172 return err 173 } 174 } 175 for index := range m.LocalImports { 176 if err := m.LocalImports[index].validate(); err != nil { 177 return err 178 } 179 } 180 for index := range m.Projects { 181 if err := m.Projects[index].fillDefaults(); err != nil { 182 return err 183 } 184 } 185 for index := range m.Tools { 186 if err := m.Tools[index].fillDefaults(); err != nil { 187 return err 188 } 189 } 190 return nil 191 } 192 193 func (m *Manifest) unfillDefaults() error { 194 for index := range m.Imports { 195 if err := m.Imports[index].unfillDefaults(); err != nil { 196 return err 197 } 198 } 199 for index := range m.LocalImports { 200 if err := m.LocalImports[index].validate(); err != nil { 201 return err 202 } 203 } 204 for index := range m.Projects { 205 if err := m.Projects[index].unfillDefaults(); err != nil { 206 return err 207 } 208 } 209 for index := range m.Tools { 210 if err := m.Tools[index].unfillDefaults(); err != nil { 211 return err 212 } 213 } 214 return nil 215 } 216 217 // Import represents a remote manifest import. 218 type Import struct { 219 // Manifest file to use from the remote manifest project. 220 Manifest string `xml:"manifest,attr,omitempty"` 221 // Name is the name of the remote manifest project, used to determine the 222 // project key. 223 Name string `xml:"name,attr,omitempty"` 224 // Protocol is the version control protocol used by the remote manifest 225 // project. If not set, "git" is used as the default. 226 Protocol string `xml:"protocol,attr,omitempty"` 227 // Remote is the remote manifest project to import. 228 Remote string `xml:"remote,attr,omitempty"` 229 // RemoteBranch is the name of the remote branch to track. It doesn't affect 230 // the name of the local branch that jiri maintains, which is always 231 // "master". If not set, "master" is used as the default. 232 RemoteBranch string `xml:"remotebranch,attr,omitempty"` 233 // Root path, prepended to all project paths specified in the manifest file. 234 Root string `xml:"root,attr,omitempty"` 235 XMLName struct{} `xml:"import"` 236 } 237 238 func (i *Import) fillDefaults() error { 239 if i.Protocol == "" { 240 i.Protocol = "git" 241 } 242 if i.RemoteBranch == "" { 243 i.RemoteBranch = "master" 244 } 245 return i.validate() 246 } 247 248 func (i *Import) unfillDefaults() error { 249 if i.Protocol == "git" { 250 i.Protocol = "" 251 } 252 if i.RemoteBranch == "master" { 253 i.RemoteBranch = "" 254 } 255 return i.validate() 256 } 257 258 func (i *Import) validate() error { 259 if i.Manifest == "" || i.Remote == "" { 260 return fmt.Errorf("bad import: both manifest and remote must be specified") 261 } 262 return nil 263 } 264 265 func (i *Import) toProject(path string) (Project, error) { 266 p := Project{ 267 Name: i.Name, 268 Path: path, 269 Protocol: i.Protocol, 270 Remote: i.Remote, 271 RemoteBranch: i.RemoteBranch, 272 } 273 err := p.fillDefaults() 274 return p, err 275 } 276 277 // ProjectKey returns the unique ProjectKey for the imported project. 278 func (i *Import) ProjectKey() ProjectKey { 279 return MakeProjectKey(i.Name, i.Remote) 280 } 281 282 // projectKeyFileName returns a file name based on the ProjectKey. 283 func (i *Import) projectKeyFileName() string { 284 // TODO(toddw): Disallow weird characters from project names. 285 hash := fnv.New64a() 286 hash.Write([]byte(i.ProjectKey())) 287 return fmt.Sprintf("%s_%x", i.Name, hash.Sum64()) 288 } 289 290 // cycleKey returns a key based on the remote and manifest, used for 291 // cycle-detection. It's only valid for new-style remote imports; it's empty 292 // for the old-style local imports. 293 func (i *Import) cycleKey() string { 294 if i.Remote == "" { 295 return "" 296 } 297 // We don't join the remote and manifest with a slash or any other url-safe 298 // character, since that might not be unique. E.g. 299 // remote: https://foo.com/a/b remote: https://foo.com/a 300 // manifest: c manifest: b/c 301 // In both cases, the key would be https://foo.com/a/b/c. 302 return i.Remote + " + " + i.Manifest 303 } 304 305 // LocalImport represents a local manifest import. 306 type LocalImport struct { 307 // Manifest file to import from. 308 File string `xml:"file,attr,omitempty"` 309 XMLName struct{} `xml:"localimport"` 310 } 311 312 func (i *LocalImport) validate() error { 313 if i.File == "" { 314 return fmt.Errorf("bad localimport: must specify file: %+v", *i) 315 } 316 return nil 317 } 318 319 // ProjectKey is a unique string for a project. 320 type ProjectKey string 321 322 // MakeProjectKey returns the project key, given the project name and remote. 323 func MakeProjectKey(name, remote string) ProjectKey { 324 return ProjectKey(name + projectKeySeparator + remote) 325 } 326 327 // projectKeySeparator is a reserved string used in ProjectKeys. It cannot 328 // occur in Project names. 329 const projectKeySeparator = "=" 330 331 // ProjectKeys is a slice of ProjectKeys implementing the Sort interface. 332 type ProjectKeys []ProjectKey 333 334 func (pks ProjectKeys) Len() int { return len(pks) } 335 func (pks ProjectKeys) Less(i, j int) bool { return string(pks[i]) < string(pks[j]) } 336 func (pks ProjectKeys) Swap(i, j int) { pks[i], pks[j] = pks[j], pks[i] } 337 338 // Project represents a jiri project. 339 type Project struct { 340 // Name is the project name. 341 Name string `xml:"name,attr,omitempty"` 342 // Path is the path used to store the project locally. Project 343 // manifest uses paths that are relative to the $JIRI_ROOT 344 // environment variable. When a manifest is parsed (e.g. in 345 // RemoteProjects), the program logic converts the relative 346 // paths to an absolute paths, using the current value of the 347 // $JIRI_ROOT environment variable as a prefix. 348 Path string `xml:"path,attr,omitempty"` 349 // Protocol is the version control protocol used by the 350 // project. If not set, "git" is used as the default. 351 Protocol string `xml:"protocol,attr,omitempty"` 352 // Remote is the project remote. 353 Remote string `xml:"remote,attr,omitempty"` 354 // RemoteBranch is the name of the remote branch to track. It doesn't affect 355 // the name of the local branch that jiri maintains, which is always "master". 356 RemoteBranch string `xml:"remotebranch,attr,omitempty"` 357 // Revision is the revision the project should be advanced to during "jiri 358 // update". If Revision is set, RemoteBranch will be ignored. If Revision 359 // is not set, "HEAD" is used as the default. 360 Revision string `xml:"revision,attr,omitempty"` 361 // GerritHost is the gerrit host where project CLs will be sent. 362 GerritHost string `xml:"gerrithost,attr,omitempty"` 363 // GitHooks is a directory containing git hooks that will be installed for 364 // this project. 365 GitHooks string `xml:"githooks,attr,omitempty"` 366 // RunHook is a script that will run when the project is created, updated, 367 // or moved. The argument to the script will be "create", "update" or 368 // "move" depending on the type of operation being performed. 369 RunHook string `xml:"runhook,attr,omitempty"` 370 XMLName struct{} `xml:"project"` 371 } 372 373 // ProjectFromFile returns a project parsed from the contents of filename, 374 // with defaults filled in and all paths absolute. 375 func ProjectFromFile(jirix *jiri.X, filename string) (*Project, error) { 376 data, err := jirix.NewSeq().ReadFile(filename) 377 if err != nil { 378 return nil, err 379 } 380 381 p := new(Project) 382 if err := xml.Unmarshal(data, p); err != nil { 383 return nil, err 384 } 385 if err := p.fillDefaults(); err != nil { 386 return nil, err 387 } 388 p.absolutizePaths(jirix.Root) 389 return p, nil 390 } 391 392 // ToFile writes the project p to a file with the given filename, with defaults 393 // unfilled and all paths relative to the jiri root. 394 func (p Project) ToFile(jirix *jiri.X, filename string) error { 395 if err := p.unfillDefaults(); err != nil { 396 return err 397 } 398 // Replace absolute paths with relative paths to make it possible to move 399 // the $JIRI_ROOT directory locally. 400 if err := p.relativizePaths(jirix.Root); err != nil { 401 return err 402 } 403 data, err := xml.Marshal(p) 404 if err != nil { 405 return fmt.Errorf("project xml.Marshal failed: %v", err) 406 } 407 // Same logic as Manifest.ToBytes, to make the output more compact. 408 data = bytes.Replace(data, endProjectSoloBytes, endElemSoloBytes, -1) 409 if !bytes.HasSuffix(data, newlineBytes) { 410 data = append(data, '\n') 411 } 412 return safeWriteFile(jirix, filename, data) 413 } 414 415 // absolutizePaths makes all relative paths absolute by prepending basepath. 416 func (p *Project) absolutizePaths(basepath string) { 417 if p.Path != "" && !filepath.IsAbs(p.Path) { 418 p.Path = filepath.Join(basepath, p.Path) 419 } 420 if p.GitHooks != "" && !filepath.IsAbs(p.GitHooks) { 421 p.GitHooks = filepath.Join(basepath, p.GitHooks) 422 } 423 if p.RunHook != "" && !filepath.IsAbs(p.RunHook) { 424 p.RunHook = filepath.Join(basepath, p.RunHook) 425 } 426 } 427 428 // relativizePaths makes all absolute paths relative to basepath. 429 func (p *Project) relativizePaths(basepath string) error { 430 if filepath.IsAbs(p.Path) { 431 relPath, err := filepath.Rel(basepath, p.Path) 432 if err != nil { 433 return err 434 } 435 p.Path = relPath 436 } 437 if filepath.IsAbs(p.GitHooks) { 438 relGitHooks, err := filepath.Rel(basepath, p.GitHooks) 439 if err != nil { 440 return err 441 } 442 p.GitHooks = relGitHooks 443 } 444 if filepath.IsAbs(p.RunHook) { 445 relRunHook, err := filepath.Rel(basepath, p.RunHook) 446 if err != nil { 447 return err 448 } 449 p.RunHook = relRunHook 450 } 451 return nil 452 } 453 454 // Key returns the unique ProjectKey for the project. 455 func (p Project) Key() ProjectKey { 456 return MakeProjectKey(p.Name, p.Remote) 457 } 458 459 func (p *Project) fillDefaults() error { 460 if p.Protocol == "" { 461 p.Protocol = "git" 462 } 463 if p.RemoteBranch == "" { 464 p.RemoteBranch = "master" 465 } 466 if p.Revision == "" { 467 p.Revision = "HEAD" 468 } 469 return p.validate() 470 } 471 472 func (p *Project) unfillDefaults() error { 473 if p.Protocol == "git" { 474 p.Protocol = "" 475 } 476 if p.RemoteBranch == "master" { 477 p.RemoteBranch = "" 478 } 479 if p.Revision == "HEAD" { 480 p.Revision = "" 481 } 482 return p.validate() 483 } 484 485 func (p *Project) validate() error { 486 if strings.Contains(p.Name, projectKeySeparator) { 487 return fmt.Errorf("bad project: name cannot contain %q: %+v", projectKeySeparator, *p) 488 } 489 if p.Protocol != "" && p.Protocol != "git" { 490 return fmt.Errorf("bad project: only git protocol is supported: %+v", *p) 491 } 492 return nil 493 } 494 495 // Projects maps ProjectKeys to Projects. 496 type Projects map[ProjectKey]Project 497 498 // toSlice returns a slice of Projects in the Projects map. 499 func (ps Projects) toSlice() []Project { 500 var pSlice []Project 501 for _, p := range ps { 502 pSlice = append(pSlice, p) 503 } 504 return pSlice 505 } 506 507 // Find returns all projects in Projects with the given key or name. 508 func (ps Projects) Find(keyOrName string) Projects { 509 projects := Projects{} 510 if p, ok := ps[ProjectKey(keyOrName)]; ok { 511 projects[ProjectKey(keyOrName)] = p 512 } else { 513 for key, p := range ps { 514 if keyOrName == p.Name { 515 projects[key] = p 516 } 517 } 518 } 519 return projects 520 } 521 522 // FindUnique returns the project in Projects with the given key or name, and 523 // returns an error if none or multiple matching projects are found. 524 func (ps Projects) FindUnique(keyOrName string) (Project, error) { 525 var p Project 526 projects := ps.Find(keyOrName) 527 if len(projects) == 0 { 528 return p, fmt.Errorf("no projects found with key or name %q", keyOrName) 529 } 530 if len(projects) > 1 { 531 return p, fmt.Errorf("multiple projects found with name %q", keyOrName) 532 } 533 // Return the only project in projects. 534 for _, project := range projects { 535 p = project 536 } 537 return p, nil 538 } 539 540 // Tools maps jiri tool names, to their detailed description. 541 type Tools map[string]Tool 542 543 // toSlice returns a slice of Tools in the Tools map. 544 func (ts Tools) toSlice() []Tool { 545 var tSlice []Tool 546 for _, t := range ts { 547 tSlice = append(tSlice, t) 548 } 549 return tSlice 550 } 551 552 // Tool represents a jiri tool. 553 type Tool struct { 554 // Data is a relative path to a directory for storing tool data 555 // (e.g. tool configuration files). The purpose of this field is to 556 // decouple the configuration of the data directory from the tool 557 // itself so that the location of the data directory can change 558 // without the need to change the tool. 559 Data string `xml:"data,attr,omitempty"` 560 // Name is the name of the tool binary. 561 Name string `xml:"name,attr,omitempty"` 562 // Package is the package path of the tool. 563 Package string `xml:"package,attr,omitempty"` 564 // Project identifies the project that contains the tool. If not 565 // set, "https://vanadium.googlesource.com/<JiriProject>" is 566 // used as the default. 567 Project string `xml:"project,attr,omitempty"` 568 XMLName struct{} `xml:"tool"` 569 } 570 571 func (t *Tool) fillDefaults() error { 572 if t.Data == "" { 573 t.Data = "data" 574 } 575 if t.Project == "" { 576 t.Project = "https://vanadium.googlesource.com/" + JiriProject 577 } 578 return nil 579 } 580 581 func (t *Tool) unfillDefaults() error { 582 if t.Data == "data" { 583 t.Data = "" 584 } 585 // Don't unfill the jiri project setting, since that's not meant to be 586 // optional. 587 return nil 588 } 589 590 // ScanMode determines whether LocalProjects should scan the local filesystem 591 // for projects (FullScan), or optimistically assume that the local projects 592 // will match those in the manifest (FastScan). 593 type ScanMode bool 594 595 const ( 596 FastScan = ScanMode(false) 597 FullScan = ScanMode(true) 598 ) 599 600 type UnsupportedProtocolErr string 601 602 func (e UnsupportedProtocolErr) Error() string { 603 return "unsupported protocol: " + string(e) 604 } 605 606 // Update represents an update of projects as a map from 607 // project names to a collections of commits. 608 type Update map[string][]CL 609 610 // CreateSnapshot creates a manifest that encodes the current state of master 611 // branches of all projects and writes this snapshot out to the given file. 612 func CreateSnapshot(jirix *jiri.X, file, snapshotPath string) error { 613 jirix.TimerPush("create snapshot") 614 defer jirix.TimerPop() 615 616 // If snapshotPath is empty, use the file as the path. 617 if snapshotPath == "" { 618 snapshotPath = file 619 } 620 621 // Get a clean, symlink-free, relative path to the snapshot. 622 snapshotPath = filepath.Clean(snapshotPath) 623 if evaledSnapshotPath, err := filepath.EvalSymlinks(snapshotPath); err == nil { 624 snapshotPath = evaledSnapshotPath 625 } 626 if relSnapshotPath, err := filepath.Rel(jirix.Root, snapshotPath); err == nil { 627 snapshotPath = relSnapshotPath 628 } 629 630 manifest := Manifest{ 631 SnapshotPath: snapshotPath, 632 } 633 634 // Add all local projects to manifest. 635 localProjects, err := LocalProjects(jirix, FullScan) 636 if err != nil { 637 return err 638 } 639 for _, project := range localProjects { 640 manifest.Projects = append(manifest.Projects, project) 641 } 642 643 // Add all tools from the current manifest to the snapshot manifest. 644 // We can't just call LoadManifest here, since that determines the 645 // local projects using FastScan, but if we're calling CreateSnapshot 646 // during "jiri update" and we added some new projects, they won't be 647 // found anymore. 648 _, tools, err := loadManifestFile(jirix, jirix.JiriManifestFile(), localProjects) 649 if err != nil { 650 return err 651 } 652 for _, tool := range tools { 653 manifest.Tools = append(manifest.Tools, tool) 654 } 655 return manifest.ToFile(jirix, file) 656 } 657 658 // CheckoutSnapshot updates project state to the state specified in the given 659 // snapshot file. Note that the snapshot file must not contain remote imports. 660 func CheckoutSnapshot(jirix *jiri.X, snapshot string, gc bool) error { 661 // Find all local projects. 662 scanMode := FastScan 663 if gc { 664 scanMode = FullScan 665 } 666 localProjects, err := LocalProjects(jirix, scanMode) 667 if err != nil { 668 return err 669 } 670 remoteProjects, remoteTools, err := LoadSnapshotFile(jirix, snapshot) 671 if err != nil { 672 return err 673 } 674 if err := updateTo(jirix, localProjects, remoteProjects, remoteTools, gc); err != nil { 675 return err 676 } 677 return WriteUpdateHistorySnapshot(jirix, snapshot) 678 } 679 680 // LoadSnapshotFile loads the specified snapshot manifest. If the snapshot 681 // manifest contains a remote import, an error will be returned. 682 func LoadSnapshotFile(jirix *jiri.X, file string) (Projects, Tools, error) { 683 return loadManifestFile(jirix, file, nil) 684 } 685 686 // CurrentProjectKey gets the key of the current project from the current 687 // directory by reading the jiri project metadata located in a directory at the 688 // root of the current repository. 689 func CurrentProjectKey(jirix *jiri.X) (ProjectKey, error) { 690 topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel() 691 if err != nil { 692 return "", nil 693 } 694 metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir) 695 if _, err := jirix.NewSeq().Stat(metadataDir); err == nil { 696 project, err := ProjectFromFile(jirix, filepath.Join(metadataDir, jiri.ProjectMetaFile)) 697 if err != nil { 698 return "", err 699 } 700 return project.Key(), nil 701 } 702 return "", nil 703 } 704 705 // setProjectRevisions sets the current project revision from the master for 706 // each project as found on the filesystem 707 func setProjectRevisions(jirix *jiri.X, projects Projects) (_ Projects, e error) { 708 for name, project := range projects { 709 switch project.Protocol { 710 case "git": 711 revision, err := gitutil.New(jirix.NewSeq(), gitutil.RootDirOpt(project.Path)).CurrentRevisionOfBranch("master") 712 if err != nil { 713 return nil, err 714 } 715 project.Revision = revision 716 default: 717 return nil, UnsupportedProtocolErr(project.Protocol) 718 } 719 projects[name] = project 720 } 721 return projects, nil 722 } 723 724 // LocalProjects returns projects on the local filesystem. If all projects in 725 // the manifest exist locally and scanMode is set to FastScan, then only the 726 // projects in the manifest that exist locally will be returned. Otherwise, a 727 // full scan of the filesystem will take place, and all found projects will be 728 // returned. 729 func LocalProjects(jirix *jiri.X, scanMode ScanMode) (Projects, error) { 730 jirix.TimerPush("local projects") 731 defer jirix.TimerPop() 732 733 latestSnapshot := jirix.UpdateHistoryLatestLink() 734 latestSnapshotExists, err := jirix.NewSeq().IsFile(latestSnapshot) 735 if err != nil { 736 return nil, err 737 } 738 if scanMode == FastScan && latestSnapshotExists { 739 // Fast path: Full scan was not requested, and we have a snapshot containing 740 // the latest update. Check that the projects listed in the snapshot exist 741 // locally. If not, then fall back on the slow path. 742 // 743 // An error will be returned if the snapshot contains remote imports, since 744 // that would cause an infinite loop; we'd need local projects, in order to 745 // load the snapshot, in order to determine the local projects. 746 snapshotProjects, _, err := LoadSnapshotFile(jirix, latestSnapshot) 747 if err != nil { 748 return nil, err 749 } 750 projectsExist, err := projectsExistLocally(jirix, snapshotProjects) 751 if err != nil { 752 return nil, err 753 } 754 if projectsExist { 755 return setProjectRevisions(jirix, snapshotProjects) 756 } 757 } 758 759 // Slow path: Either full scan was requested, or projects exist in manifest 760 // that were not found locally. Do a recursive scan of all projects under 761 // JIRI_ROOT. 762 projects := Projects{} 763 jirix.TimerPush("scan fs") 764 err = findLocalProjects(jirix, jirix.Root, projects) 765 jirix.TimerPop() 766 if err != nil { 767 return nil, err 768 } 769 return setProjectRevisions(jirix, projects) 770 } 771 772 // projectsExistLocally returns true iff all the given projects exist on the 773 // local filesystem. 774 // Note that this may return true even if there are projects on the local 775 // filesystem not included in the provided projects argument. 776 func projectsExistLocally(jirix *jiri.X, projects Projects) (bool, error) { 777 jirix.TimerPush("match manifest") 778 defer jirix.TimerPop() 779 for _, p := range projects { 780 isLocal, err := isLocalProject(jirix, p.Path) 781 if err != nil { 782 return false, err 783 } 784 if !isLocal { 785 return false, nil 786 } 787 } 788 return true, nil 789 } 790 791 // PollProjects returns the set of changelists that exist remotely but not 792 // locally. Changes are grouped by projects and contain author identification 793 // and a description of their content. 794 func PollProjects(jirix *jiri.X, projectSet map[string]struct{}) (_ Update, e error) { 795 jirix.TimerPush("poll projects") 796 defer jirix.TimerPop() 797 798 // Switch back to current working directory when we're done. 799 cwd, err := os.Getwd() 800 if err != nil { 801 return nil, err 802 } 803 defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e) 804 805 // Gather local & remote project data. 806 localProjects, err := LocalProjects(jirix, FastScan) 807 if err != nil { 808 return nil, err 809 } 810 remoteProjects, _, err := LoadManifest(jirix) 811 if err != nil { 812 return nil, err 813 } 814 815 // Compute difference between local and remote. 816 update := Update{} 817 ops := computeOperations(localProjects, remoteProjects, false) 818 s := jirix.NewSeq() 819 for _, op := range ops { 820 name := op.Project().Name 821 822 // If given a project set, limit our results to those projects in the set. 823 if len(projectSet) > 0 { 824 if _, ok := projectSet[name]; !ok { 825 continue 826 } 827 } 828 829 // We only inspect this project if an update operation is required. 830 cls := []CL{} 831 if updateOp, ok := op.(updateOperation); ok { 832 switch updateOp.project.Protocol { 833 case "git": 834 835 // Enter project directory - this assumes absolute paths. 836 if err := s.Chdir(updateOp.destination).Done(); err != nil { 837 return nil, err 838 } 839 840 // Fetch the latest from origin. 841 if err := gitutil.New(jirix.NewSeq()).FetchRefspec("origin", updateOp.project.RemoteBranch); err != nil { 842 return nil, err 843 } 844 845 // Collect commits visible from FETCH_HEAD that aren't visible from master. 846 commitsText, err := gitutil.New(jirix.NewSeq()).Log("FETCH_HEAD", "master", "%an%n%ae%n%B") 847 if err != nil { 848 return nil, err 849 } 850 851 // Format those commits and add them to the results. 852 for _, commitText := range commitsText { 853 if got, want := len(commitText), 3; got < want { 854 return nil, fmt.Errorf("Unexpected length of %v: got %v, want at least %v", commitText, got, want) 855 } 856 cls = append(cls, CL{ 857 Author: commitText[0], 858 Email: commitText[1], 859 Description: strings.Join(commitText[2:], "\n"), 860 }) 861 } 862 default: 863 return nil, UnsupportedProtocolErr(updateOp.project.Protocol) 864 } 865 } 866 update[name] = cls 867 } 868 return update, nil 869 } 870 871 // LoadManifest loads the manifest, starting with the .jiri_manifest file, 872 // resolving remote and local imports. Returns the projects and tools specified 873 // by the manifest. 874 // 875 // WARNING: LoadManifest cannot be run multiple times in parallel! It invokes 876 // git operations which require a lock on the filesystem. If you see errors 877 // about ".git/index.lock exists", you are likely calling LoadManifest in 878 // parallel. 879 func LoadManifest(jirix *jiri.X) (Projects, Tools, error) { 880 jirix.TimerPush("load manifest") 881 defer jirix.TimerPop() 882 file := jirix.JiriManifestFile() 883 localProjects, err := LocalProjects(jirix, FastScan) 884 if err != nil { 885 return nil, nil, err 886 } 887 return loadManifestFile(jirix, file, localProjects) 888 } 889 890 // loadManifestFile loads the manifest starting with the given file, resolving 891 // remote and local imports. Local projects are used to resolve remote imports; 892 // if nil, encountering any remote import will result in an error. 893 // 894 // WARNING: loadManifestFile cannot be run multiple times in parallel! It 895 // invokes git operations which require a lock on the filesystem. If you see 896 // errors about ".git/index.lock exists", you are likely calling 897 // loadManifestFile in parallel. 898 func loadManifestFile(jirix *jiri.X, file string, localProjects Projects) (Projects, Tools, error) { 899 ld := newManifestLoader(localProjects, false) 900 if err := ld.Load(jirix, "", file, ""); err != nil { 901 return nil, nil, err 902 } 903 return ld.Projects, ld.Tools, nil 904 } 905 906 // getManifestRemote returns the remote url of the origin from the manifest 907 // repo. 908 // TODO(nlacasse,toddw): Once the manifest project is specified in the 909 // manifest, we should get the remote directly from the manifest, and not from 910 // the filesystem. 911 func getManifestRemote(jirix *jiri.X, manifestPath string) (string, error) { 912 var remote string 913 return remote, jirix.NewSeq().Pushd(manifestPath).Call( 914 func() (e error) { 915 remote, e = gitutil.New(jirix.NewSeq()).RemoteUrl("origin") 916 return 917 }, "get manifest origin").Done() 918 } 919 920 func loadUpdatedManifest(jirix *jiri.X, localProjects Projects) (Projects, Tools, string, error) { 921 jirix.TimerPush("load updated manifest") 922 defer jirix.TimerPop() 923 ld := newManifestLoader(localProjects, true) 924 if err := ld.Load(jirix, "", jirix.JiriManifestFile(), ""); err != nil { 925 return nil, nil, ld.TmpDir, err 926 } 927 return ld.Projects, ld.Tools, ld.TmpDir, nil 928 } 929 930 // UpdateUniverse updates all local projects and tools to match the remote 931 // counterparts identified in the manifest. Optionally, the 'gc' flag can be 932 // used to indicate that local projects that no longer exist remotely should be 933 // removed. 934 func UpdateUniverse(jirix *jiri.X, gc bool) (e error) { 935 jirix.TimerPush("update universe") 936 defer jirix.TimerPop() 937 938 // Find all local projects. 939 scanMode := FastScan 940 if gc { 941 scanMode = FullScan 942 } 943 localProjects, err := LocalProjects(jirix, scanMode) 944 if err != nil { 945 return err 946 } 947 948 // Load the manifest, updating all manifest projects to match their remote 949 // counterparts. 950 s := jirix.NewSeq() 951 remoteProjects, remoteTools, tmpLoadDir, err := loadUpdatedManifest(jirix, localProjects) 952 if tmpLoadDir != "" { 953 defer collect.Error(func() error { return s.RemoveAll(tmpLoadDir).Done() }, &e) 954 } 955 if err != nil { 956 return err 957 } 958 return updateTo(jirix, localProjects, remoteProjects, remoteTools, gc) 959 } 960 961 // updateTo updates the local projects and tools to the state specified in 962 // remoteProjects and remoteTools. 963 func updateTo(jirix *jiri.X, localProjects, remoteProjects Projects, remoteTools Tools, gc bool) (e error) { 964 s := jirix.NewSeq() 965 // 1. Update all local projects to match the specified projects argument. 966 if err := updateProjects(jirix, localProjects, remoteProjects, gc); err != nil { 967 return err 968 } 969 // 2. Build all tools in a temporary directory. 970 tmpToolsDir, err := s.TempDir("", "tmp-jiri-tools-build") 971 if err != nil { 972 return fmt.Errorf("TempDir() failed: %v", err) 973 } 974 defer collect.Error(func() error { return s.RemoveAll(tmpToolsDir).Done() }, &e) 975 if err := buildToolsFromMaster(jirix, remoteProjects, remoteTools, tmpToolsDir); err != nil { 976 return err 977 } 978 // 3. Install the tools into $JIRI_ROOT/.jiri_root/bin. 979 if err := InstallTools(jirix, tmpToolsDir); err != nil { 980 return err 981 } 982 // 4. If we have the jiri project, then update the jiri script in 983 // $JIRI_ROOT/.jiri_root/scripts. 984 jiriProject, err := remoteProjects.FindUnique(JiriProject) 985 if err != nil { 986 // jiri project not found. This happens often in tests. Ok to ignore. 987 return nil 988 } 989 return updateJiriScript(jirix, jiriProject) 990 } 991 992 // WriteUpdateHistorySnapshot creates a snapshot of the current state of all 993 // projects and writes it to the update history directory. 994 func WriteUpdateHistorySnapshot(jirix *jiri.X, snapshotPath string) error { 995 seq := jirix.NewSeq() 996 snapshotFile := filepath.Join(jirix.UpdateHistoryDir(), time.Now().Format(time.RFC3339)) 997 if err := CreateSnapshot(jirix, snapshotFile, snapshotPath); err != nil { 998 return err 999 } 1000 1001 latestLink, secondLatestLink := jirix.UpdateHistoryLatestLink(), jirix.UpdateHistorySecondLatestLink() 1002 1003 // If the "latest" symlink exists, point the "second-latest" symlink to its value. 1004 latestLinkExists, err := seq.IsFile(latestLink) 1005 if err != nil { 1006 return err 1007 } 1008 if latestLinkExists { 1009 latestFile, err := os.Readlink(latestLink) 1010 if err != nil { 1011 return err 1012 } 1013 if err := seq.RemoveAll(secondLatestLink).Symlink(latestFile, secondLatestLink).Done(); err != nil { 1014 return err 1015 } 1016 } 1017 1018 // Point the "latest" update history symlink to the new snapshot file. Try 1019 // to keep the symlink relative, to make it easy to move or copy the entire 1020 // update_history directory. 1021 if rel, err := filepath.Rel(filepath.Dir(latestLink), snapshotFile); err == nil { 1022 snapshotFile = rel 1023 } 1024 return seq.RemoveAll(latestLink).Symlink(snapshotFile, latestLink).Done() 1025 } 1026 1027 // ApplyToLocalMaster applies an operation expressed as the given function to 1028 // the local master branch of the given projects. 1029 func ApplyToLocalMaster(jirix *jiri.X, projects Projects, fn func() error) (e error) { 1030 cwd, err := os.Getwd() 1031 if err != nil { 1032 return err 1033 } 1034 defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e) 1035 1036 s := jirix.NewSeq() 1037 git := gitutil.New(s) 1038 1039 // Loop through all projects, checking out master and stashing any unstaged 1040 // changes. 1041 for _, project := range projects { 1042 p := project 1043 if err := s.Chdir(p.Path).Done(); err != nil { 1044 return err 1045 } 1046 switch p.Protocol { 1047 case "git": 1048 branch, err := git.CurrentBranchName() 1049 if err != nil { 1050 return err 1051 } 1052 stashed, err := git.Stash() 1053 if err != nil { 1054 return err 1055 } 1056 if err := git.CheckoutBranch("master"); err != nil { 1057 return err 1058 } 1059 // After running the function, return to this project's directory, 1060 // checkout the original branch, and stash pop if necessary. 1061 defer collect.Error(func() error { 1062 if err := s.Chdir(p.Path).Done(); err != nil { 1063 return err 1064 } 1065 if err := git.CheckoutBranch(branch); err != nil { 1066 return err 1067 } 1068 if stashed { 1069 return git.StashPop() 1070 } 1071 return nil 1072 }, &e) 1073 default: 1074 return UnsupportedProtocolErr(p.Protocol) 1075 } 1076 } 1077 return fn() 1078 } 1079 1080 // BuildTools builds the given tools and places the resulting binaries into the 1081 // given directory. 1082 func BuildTools(jirix *jiri.X, projects Projects, tools Tools, outputDir string) (e error) { 1083 jirix.TimerPush("build tools") 1084 defer jirix.TimerPop() 1085 if len(tools) == 0 { 1086 // Nothing to do here... 1087 return nil 1088 } 1089 toolPkgs := []string{} 1090 workspaceSet := map[string]bool{} 1091 for _, tool := range tools { 1092 toolPkgs = append(toolPkgs, tool.Package) 1093 toolProject, err := projects.FindUnique(tool.Project) 1094 if err != nil { 1095 return err 1096 } 1097 // Identify the Go workspace the tool is in. To this end we use a 1098 // heuristic that identifies the maximal suffix of the project path 1099 // that corresponds to a prefix of the package name. 1100 workspace := "" 1101 for i := 0; i < len(toolProject.Path); i++ { 1102 if toolProject.Path[i] == filepath.Separator { 1103 if strings.HasPrefix("src/"+tool.Package, filepath.ToSlash(toolProject.Path[i+1:])) { 1104 workspace = toolProject.Path[:i] 1105 break 1106 } 1107 } 1108 } 1109 if workspace == "" { 1110 return fmt.Errorf("could not identify go workspace for tool %v", tool.Name) 1111 } 1112 workspaceSet[workspace] = true 1113 } 1114 workspaces := []string{} 1115 for workspace := range workspaceSet { 1116 workspaces = append(workspaces, workspace) 1117 } 1118 if envGoPath := os.Getenv("GOPATH"); envGoPath != "" { 1119 workspaces = append(workspaces, strings.Split(envGoPath, string(filepath.ListSeparator))...) 1120 } 1121 s := jirix.NewSeq() 1122 // Put pkg files in a tempdir. BuildTools uses the system go, and if 1123 // jiri-go uses a different go version than the system go, then you can get 1124 // weird errors when they share a pkgdir. 1125 tmpPkgDir, err := s.TempDir("", "tmp-pkg-dir") 1126 if err != nil { 1127 return fmt.Errorf("TempDir() failed: %v", err) 1128 } 1129 defer collect.Error(func() error { return jirix.NewSeq().RemoveAll(tmpPkgDir).Done() }, &e) 1130 1131 // We unset GOARCH and GOOS because jiri update should always build for the 1132 // native architecture and OS. Also, as of go1.5, setting GOBIN is not 1133 // compatible with GOARCH or GOOS. 1134 env := map[string]string{ 1135 "GOARCH": "", 1136 "GOOS": "", 1137 "GOBIN": outputDir, 1138 "GOPATH": strings.Join(workspaces, string(filepath.ListSeparator)), 1139 } 1140 args := append([]string{"install", "-pkgdir", tmpPkgDir}, toolPkgs...) 1141 var stderr bytes.Buffer 1142 if err := s.Env(env).Capture(ioutil.Discard, &stderr).Last("go", args...); err != nil { 1143 return fmt.Errorf("tool build failed\n%v", stderr.String()) 1144 } 1145 return nil 1146 } 1147 1148 // buildToolsFromMaster builds and installs all jiri tools using the version 1149 // available in the local master branch of the tools repository. Notably, this 1150 // function does not perform any version control operation on the master 1151 // branch. 1152 func buildToolsFromMaster(jirix *jiri.X, projects Projects, tools Tools, outputDir string) error { 1153 toolsToBuild := Tools{} 1154 toolNames := []string{} // Used for logging purposes. 1155 for _, tool := range tools { 1156 // Skip tools with no package specified. Besides increasing 1157 // robustness, this step also allows us to create jiri root 1158 // fakes without having to provide an implementation for the "jiri" 1159 // tool, which every manifest needs to specify. 1160 if tool.Package == "" { 1161 continue 1162 } 1163 toolsToBuild[tool.Name] = tool 1164 toolNames = append(toolNames, tool.Name) 1165 } 1166 1167 updateFn := func() error { 1168 return ApplyToLocalMaster(jirix, projects, func() error { 1169 return BuildTools(jirix, projects, toolsToBuild, outputDir) 1170 }) 1171 } 1172 1173 // Always log the output of updateFn, irrespective of the value of the 1174 // verbose flag. 1175 return jirix.NewSeq().Verbose(true). 1176 Call(updateFn, "build tools: %v", strings.Join(toolNames, " ")). 1177 Done() 1178 } 1179 1180 // CleanupProjects restores the given jiri projects back to their master 1181 // branches, resets to the specified revision if there is one, and gets rid of 1182 // all the local changes. If "cleanupBranches" is true, it will also delete all 1183 // the non-master branches. 1184 func CleanupProjects(jirix *jiri.X, projects Projects, cleanupBranches bool) (e error) { 1185 wd, err := os.Getwd() 1186 if err != nil { 1187 return fmt.Errorf("Getwd() failed: %v", err) 1188 } 1189 defer collect.Error(func() error { return jirix.NewSeq().Chdir(wd).Done() }, &e) 1190 for _, project := range projects { 1191 if err := resetLocalProject(jirix, project, cleanupBranches); err != nil { 1192 return err 1193 } 1194 } 1195 return nil 1196 } 1197 1198 // resetLocalProject checks out the master branch, cleans up untracked files 1199 // and uncommitted changes, and optionally deletes all the other branches. 1200 func resetLocalProject(jirix *jiri.X, project Project, cleanupBranches bool) error { 1201 git := gitutil.New(jirix.NewSeq()) 1202 if err := jirix.NewSeq().Chdir(project.Path).Done(); err != nil { 1203 return err 1204 } 1205 // Check out master. 1206 curBranchName, err := git.CurrentBranchName() 1207 if err != nil { 1208 return err 1209 } 1210 if curBranchName != "master" { 1211 if err := git.CheckoutBranch("master", gitutil.ForceOpt(true)); err != nil { 1212 return err 1213 } 1214 } 1215 // Cleanup changes. 1216 if err := git.RemoveUntrackedFiles(); err != nil { 1217 return err 1218 } 1219 if err := resetProjectCurrentBranch(jirix, project); err != nil { 1220 return err 1221 } 1222 if !cleanupBranches { 1223 return nil 1224 } 1225 1226 // Delete all the other branches. 1227 // At this point we should be at the master branch. 1228 branches, _, err := gitutil.New(jirix.NewSeq()).GetBranches() 1229 if err != nil { 1230 return err 1231 } 1232 for _, branch := range branches { 1233 if branch == "master" { 1234 continue 1235 } 1236 if err := git.DeleteBranch(branch, gitutil.ForceOpt(true)); err != nil { 1237 return err 1238 } 1239 } 1240 return nil 1241 } 1242 1243 // isLocalProject returns true if there is a project at the given path. 1244 func isLocalProject(jirix *jiri.X, path string) (bool, error) { 1245 // Existence of a metadata directory is how we know we've found a 1246 // Jiri-maintained project. 1247 metadataDir := filepath.Join(path, jiri.ProjectMetaDir) 1248 if _, err := jirix.NewSeq().Stat(metadataDir); err != nil { 1249 if runutil.IsNotExist(err) { 1250 return false, nil 1251 } 1252 return false, err 1253 } 1254 return true, nil 1255 } 1256 1257 // ProjectAtPath returns a Project struct corresponding to the project at the 1258 // path in the filesystem. 1259 func ProjectAtPath(jirix *jiri.X, path string) (Project, error) { 1260 metadataFile := filepath.Join(path, jiri.ProjectMetaDir, jiri.ProjectMetaFile) 1261 project, err := ProjectFromFile(jirix, metadataFile) 1262 if err != nil { 1263 return Project{}, err 1264 } 1265 return *project, nil 1266 } 1267 1268 // findLocalProjects scans the filesystem for all projects. Note that project 1269 // directories can be nested recursively. 1270 func findLocalProjects(jirix *jiri.X, path string, projects Projects) error { 1271 isLocal, err := isLocalProject(jirix, path) 1272 if err != nil { 1273 return err 1274 } 1275 if isLocal { 1276 project, err := ProjectAtPath(jirix, path) 1277 if err != nil { 1278 return err 1279 } 1280 if path != project.Path { 1281 return fmt.Errorf("project %v has path %v but was found in %v", project.Name, project.Path, path) 1282 } 1283 if p, ok := projects[project.Key()]; ok { 1284 return fmt.Errorf("name conflict: both %v and %v contain project with key %v", p.Path, project.Path, project.Key()) 1285 } 1286 projects[project.Key()] = project 1287 } 1288 1289 // Recurse into all the sub directories. 1290 fileInfos, err := jirix.NewSeq().ReadDir(path) 1291 if err != nil { 1292 return err 1293 } 1294 for _, fileInfo := range fileInfos { 1295 if fileInfo.IsDir() && !strings.HasPrefix(fileInfo.Name(), ".") { 1296 if err := findLocalProjects(jirix, filepath.Join(path, fileInfo.Name()), projects); err != nil { 1297 return err 1298 } 1299 } 1300 } 1301 return nil 1302 } 1303 1304 // InstallTools installs the tools from the given directory into 1305 // $JIRI_ROOT/.jiri_root/bin. 1306 func InstallTools(jirix *jiri.X, dir string) error { 1307 jirix.TimerPush("install tools") 1308 defer jirix.TimerPop() 1309 fis, err := ioutil.ReadDir(dir) 1310 if err != nil { 1311 return fmt.Errorf("ReadDir(%v) failed: %v", dir, err) 1312 } 1313 binDir := jirix.BinDir() 1314 if err := jirix.NewSeq().MkdirAll(binDir, 0755).Done(); err != nil { 1315 return fmt.Errorf("MkdirAll(%v) failed: %v", binDir, err) 1316 } 1317 s := jirix.NewSeq() 1318 for _, fi := range fis { 1319 installFn := func() error { 1320 src := filepath.Join(dir, fi.Name()) 1321 dst := filepath.Join(binDir, fi.Name()) 1322 return jirix.NewSeq().Rename(src, dst).Done() 1323 } 1324 if err := s.Verbose(true).Call(installFn, "install tool %q", fi.Name()).Done(); err != nil { 1325 return fmt.Errorf("error installing tool %q: %v", fi.Name(), err) 1326 } 1327 } 1328 return nil 1329 } 1330 1331 // updateJiriScript copies the scripts/jiri script from the jiri repo to 1332 // JIRI_ROOT/.jiri_root/scripts/jiri. 1333 func updateJiriScript(jirix *jiri.X, jiriProject Project) error { 1334 s := jirix.NewSeq() 1335 updateFn := func() error { 1336 return ApplyToLocalMaster(jirix, Projects{jiriProject.Key(): jiriProject}, func() error { 1337 newJiriScriptPath := filepath.Join(jiriProject.Path, "scripts", "jiri") 1338 newJiriScript, err := s.Open(newJiriScriptPath) 1339 if err != nil { 1340 return err 1341 } 1342 s.MkdirAll(jirix.ScriptsDir(), 0755) 1343 jiriScriptOutPath := filepath.Join(jirix.ScriptsDir(), "jiri") 1344 jiriScriptOut, err := s.Create(jiriScriptOutPath) 1345 if err != nil { 1346 return err 1347 } 1348 if _, err := s.Copy(jiriScriptOut, newJiriScript); err != nil { 1349 return err 1350 } 1351 if err := s.Chmod(jiriScriptOutPath, 0750).Done(); err != nil { 1352 return err 1353 } 1354 1355 return nil 1356 }) 1357 } 1358 return jirix.NewSeq().Verbose(true).Call(updateFn, "update jiri script").Done() 1359 } 1360 1361 // TransitionBinDir handles the transition from the old location 1362 // $JIRI_ROOT/devtools/bin to the new $JIRI_ROOT/.jiri_root/bin. In 1363 // InstallTools above we've already installed the tools to the new location. 1364 // 1365 // For now we want $JIRI_ROOT/devtools/bin symlinked to the new location, so 1366 // that users won't perceive a difference in behavior. In addition, we want to 1367 // save the old binaries to $JIRI_ROOT/.jiri_root/bin.BACKUP the first time this 1368 // is run. That way if we screwed something up, the user can recover their old 1369 // binaries. 1370 // 1371 // TODO(toddw): Remove this logic after the transition to .jiri_root is done. 1372 func TransitionBinDir(jirix *jiri.X) error { 1373 s := jirix.NewSeq() 1374 oldDir, newDir := filepath.Join(jirix.Root, "devtools", "bin"), jirix.BinDir() 1375 switch info, err := s.Lstat(oldDir); { 1376 case runutil.IsNotExist(err): 1377 // Drop down to create the symlink below. 1378 case err != nil: 1379 return fmt.Errorf("Failed to stat old bin dir: %v", err) 1380 case info.Mode()&os.ModeSymlink != 0: 1381 link, err := s.Readlink(oldDir) 1382 if err != nil { 1383 return fmt.Errorf("Failed to read link from old bin dir: %v", err) 1384 } 1385 if filepath.Clean(link) == newDir { 1386 // The old dir is already correctly symlinked to the new dir. 1387 return nil 1388 } 1389 fallthrough 1390 default: 1391 // The old dir exists, and either it's not a symlink, or it's a symlink that 1392 // doesn't point to the new dir. Move the old dir to the backup location. 1393 backupDir := newDir + ".BACKUP" 1394 switch _, err := s.Stat(backupDir); { 1395 case runutil.IsNotExist(err): 1396 if err := s.Rename(oldDir, backupDir).Done(); err != nil { 1397 return fmt.Errorf("Failed to backup old bin dir %v to %v: %v", oldDir, backupDir, err) 1398 } 1399 // Drop down to create the symlink below. 1400 case err != nil: 1401 return fmt.Errorf("Failed to stat backup bin dir: %v", err) 1402 default: 1403 return fmt.Errorf("Backup bin dir %v already exists", backupDir) 1404 } 1405 } 1406 // Create the symlink. 1407 if err := s.MkdirAll(filepath.Dir(oldDir), 0755).Symlink(newDir, oldDir).Done(); err != nil { 1408 return fmt.Errorf("Failed to symlink to new bin dir %v from %v: %v", newDir, oldDir, err) 1409 } 1410 return nil 1411 } 1412 1413 // fetchProject fetches from the project remote. 1414 func fetchProject(jirix *jiri.X, project Project) error { 1415 switch project.Protocol { 1416 case "git": 1417 if project.Remote == "" { 1418 return fmt.Errorf("project %q does not have a remote", project.Name) 1419 } 1420 if err := gitutil.New(jirix.NewSeq()).SetRemoteUrl("origin", project.Remote); err != nil { 1421 return err 1422 } 1423 return gitutil.New(jirix.NewSeq()).Fetch("origin") 1424 default: 1425 return UnsupportedProtocolErr(project.Protocol) 1426 } 1427 } 1428 1429 // resetProjectCurrentBranch resets the current branch to the revision and 1430 // branch specified on the project. 1431 func resetProjectCurrentBranch(jirix *jiri.X, project Project) error { 1432 if err := project.fillDefaults(); err != nil { 1433 return err 1434 } 1435 switch project.Protocol { 1436 case "git": 1437 // Having a specific revision trumps everything else. 1438 if project.Revision != "HEAD" { 1439 return gitutil.New(jirix.NewSeq()).Reset(project.Revision) 1440 } 1441 // If no revision, reset to the configured remote branch. 1442 return gitutil.New(jirix.NewSeq()).Reset("origin/" + project.RemoteBranch) 1443 default: 1444 return UnsupportedProtocolErr(project.Protocol) 1445 } 1446 } 1447 1448 // syncProjectMaster fetches from the project remote and resets the local master 1449 // branch to the revision and branch specified on the project. 1450 func syncProjectMaster(jirix *jiri.X, project Project) error { 1451 return ApplyToLocalMaster(jirix, Projects{project.Key(): project}, func() error { 1452 if err := fetchProject(jirix, project); err != nil { 1453 return err 1454 } 1455 return resetProjectCurrentBranch(jirix, project) 1456 }) 1457 } 1458 1459 // newManifestLoader returns a new manifest loader. The localProjects are used 1460 // to resolve remote imports; if nil, encountering any remote import will result 1461 // in an error. If update is true, remote manifest import projects that don't 1462 // exist locally are cloned under TmpDir, and inserted into localProjects. 1463 // 1464 // If update is true, remote changes to manifest projects will be fetched, and 1465 // manifest projects that don't exist locally will be created in temporary 1466 // directories, and added to localProjects. 1467 func newManifestLoader(localProjects Projects, update bool) *loader { 1468 return &loader{ 1469 Projects: make(Projects), 1470 Tools: make(Tools), 1471 localProjects: localProjects, 1472 update: update, 1473 } 1474 } 1475 1476 type loader struct { 1477 Projects Projects 1478 Tools Tools 1479 TmpDir string 1480 localProjects Projects 1481 update bool 1482 cycleStack []cycleInfo 1483 } 1484 1485 type cycleInfo struct { 1486 file, key string 1487 } 1488 1489 // loadNoCycles checks for cycles in imports. There are two types of cycles: 1490 // file - Cycle in the paths of manifest files in the local filesystem. 1491 // key - Cycle in the remote manifests specified by remote imports. 1492 // 1493 // Example of file cycles. File A imports file B, and vice versa. 1494 // file=manifest/A file=manifest/B 1495 // <manifest> <manifest> 1496 // <localimport file="B"/> <localimport file="A"/> 1497 // </manifest> </manifest> 1498 // 1499 // Example of key cycles. The key consists of "remote/manifest", e.g. 1500 // https://vanadium.googlesource.com/manifest/v2/default 1501 // In the example, key x/A imports y/B, and vice versa. 1502 // key=x/A key=y/B 1503 // <manifest> <manifest> 1504 // <import remote="y" manifest="B"/> <import remote="x" manifest="A"/> 1505 // </manifest> </manifest> 1506 // 1507 // The above examples are simple, but the general strategy is demonstrated. We 1508 // keep a single stack for both files and keys, and push onto each stack before 1509 // running the recursive read or update function, and pop the stack when the 1510 // function is done. If we see a duplicate on the stack at any point, we know 1511 // there's a cycle. Note that we know the file for both local and remote 1512 // imports, but we only know the key for remote imports; the key for local 1513 // imports is empty. 1514 // 1515 // A more complex case would involve a combination of local and remote imports, 1516 // using the "root" attribute to change paths on the local filesystem. In this 1517 // case the key will eventually expose the cycle. 1518 func (ld *loader) loadNoCycles(jirix *jiri.X, root, file, cycleKey string) error { 1519 info := cycleInfo{file, cycleKey} 1520 for _, c := range ld.cycleStack { 1521 switch { 1522 case file == c.file: 1523 return fmt.Errorf("import cycle detected in local manifest files: %q", append(ld.cycleStack, info)) 1524 case cycleKey == c.key && cycleKey != "": 1525 return fmt.Errorf("import cycle detected in remote manifest imports: %q", append(ld.cycleStack, info)) 1526 } 1527 } 1528 ld.cycleStack = append(ld.cycleStack, info) 1529 if err := ld.load(jirix, root, file); err != nil { 1530 return err 1531 } 1532 ld.cycleStack = ld.cycleStack[:len(ld.cycleStack)-1] 1533 return nil 1534 } 1535 1536 // shortFileName returns the relative path if file is relative to root, 1537 // otherwise returns the file name unchanged. 1538 func shortFileName(root, file string) string { 1539 if p := root + string(filepath.Separator); strings.HasPrefix(file, p) { 1540 return file[len(p):] 1541 } 1542 return file 1543 } 1544 1545 func (ld *loader) Load(jirix *jiri.X, root, file, cycleKey string) error { 1546 jirix.TimerPush("load " + shortFileName(jirix.Root, file)) 1547 defer jirix.TimerPop() 1548 return ld.loadNoCycles(jirix, root, file, cycleKey) 1549 } 1550 1551 func (ld *loader) load(jirix *jiri.X, root, file string) error { 1552 m, err := ManifestFromFile(jirix, file) 1553 if err != nil { 1554 return err 1555 } 1556 // Process remote imports. 1557 for _, remote := range m.Imports { 1558 nextRoot := filepath.Join(root, remote.Root) 1559 remote.Name = filepath.Join(nextRoot, remote.Name) 1560 key := remote.ProjectKey() 1561 p, ok := ld.localProjects[key] 1562 if !ok { 1563 if !ld.update { 1564 return fmt.Errorf("can't resolve remote import: project %q not found locally", key) 1565 } 1566 // The remote manifest project doesn't exist locally. Clone it into a 1567 // temp directory, and add it to ld.localProjects. 1568 if ld.TmpDir == "" { 1569 if ld.TmpDir, err = jirix.NewSeq().TempDir("", "jiri-load"); err != nil { 1570 return fmt.Errorf("TempDir() failed: %v", err) 1571 } 1572 } 1573 path := filepath.Join(ld.TmpDir, remote.projectKeyFileName()) 1574 if p, err = remote.toProject(path); err != nil { 1575 return err 1576 } 1577 if err := jirix.NewSeq().MkdirAll(path, 0755).Done(); err != nil { 1578 return err 1579 } 1580 if err := gitutil.New(jirix.NewSeq()).Clone(p.Remote, path); err != nil { 1581 return err 1582 } 1583 ld.localProjects[key] = p 1584 } 1585 // Reset the project to its specified branch and load the next file. Note 1586 // that we call load() recursively, so multiple files may be loaded by 1587 // resetAndLoad. 1588 p.Revision = "HEAD" 1589 p.RemoteBranch = remote.RemoteBranch 1590 nextFile := filepath.Join(p.Path, remote.Manifest) 1591 if err := ld.resetAndLoad(jirix, nextRoot, nextFile, remote.cycleKey(), p); err != nil { 1592 return err 1593 } 1594 } 1595 // Process local imports. 1596 for _, local := range m.LocalImports { 1597 // TODO(toddw): Add our invariant check that the file is in the same 1598 // repository as the current remote import repository. 1599 nextFile := filepath.Join(filepath.Dir(file), local.File) 1600 if err := ld.Load(jirix, root, nextFile, ""); err != nil { 1601 return err 1602 } 1603 } 1604 // Collect projects. 1605 for _, project := range m.Projects { 1606 // Make paths absolute by prepending JIRI_ROOT/<root>. 1607 project.absolutizePaths(filepath.Join(jirix.Root, root)) 1608 // Prepend the root to the project name. This will be a noop if the import is not rooted. 1609 project.Name = filepath.Join(root, project.Name) 1610 key := project.Key() 1611 if dup, ok := ld.Projects[key]; ok && dup != project { 1612 // TODO(toddw): Tell the user the other conflicting file. 1613 return fmt.Errorf("duplicate project %q found in %v", key, shortFileName(jirix.Root, file)) 1614 } 1615 ld.Projects[key] = project 1616 } 1617 // Collect tools. 1618 for _, tool := range m.Tools { 1619 name := tool.Name 1620 if dup, ok := ld.Tools[name]; ok && dup != tool { 1621 // TODO(toddw): Tell the user the other conflicting file. 1622 return fmt.Errorf("duplicate tool %q found in %v", name, shortFileName(jirix.Root, file)) 1623 } 1624 ld.Tools[name] = tool 1625 } 1626 return nil 1627 } 1628 1629 func (ld *loader) resetAndLoad(jirix *jiri.X, root, file, cycleKey string, project Project) (e error) { 1630 // Change to the project.Path directory, and revert when done. 1631 pushd := jirix.NewSeq().Pushd(project.Path) 1632 defer collect.Error(pushd.Done, &e) 1633 // Reset the local master branch to what's specified on the project. We only 1634 // fetch on updates; non-updates just perform the reset. 1635 // 1636 // TODO(toddw): Support "jiri update -local=p1,p2" by simply calling ld.Load 1637 // for the given projects, rather than ApplyToLocalMaster(fetch+reset+load). 1638 return ApplyToLocalMaster(jirix, Projects{project.Key(): project}, func() error { 1639 if ld.update { 1640 if err := fetchProject(jirix, project); err != nil { 1641 return err 1642 } 1643 } 1644 if err := resetProjectCurrentBranch(jirix, project); err != nil { 1645 return err 1646 } 1647 return ld.Load(jirix, root, file, cycleKey) 1648 }) 1649 } 1650 1651 // reportNonMaster checks if the given project is on master branch and 1652 // if not, reports this fact along with information on how to update it. 1653 func reportNonMaster(jirix *jiri.X, project Project) (e error) { 1654 cwd, err := os.Getwd() 1655 if err != nil { 1656 return err 1657 } 1658 defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e) 1659 s := jirix.NewSeq() 1660 if err := s.Chdir(project.Path).Done(); err != nil { 1661 return err 1662 } 1663 switch project.Protocol { 1664 case "git": 1665 current, err := gitutil.New(jirix.NewSeq()).CurrentBranchName() 1666 if err != nil { 1667 return err 1668 } 1669 if current != "master" { 1670 line1 := fmt.Sprintf(`NOTE: "jiri update" only updates the "master" branch and the current branch is %q`, current) 1671 line2 := fmt.Sprintf(`to update the %q branch once the master branch is updated, run "git merge master"`, current) 1672 s.Verbose(true).Output([]string{line1, line2}) 1673 } 1674 return nil 1675 default: 1676 return UnsupportedProtocolErr(project.Protocol) 1677 } 1678 } 1679 1680 // groupByGoogleSourceHosts returns a map of googlesource host to a Projects 1681 // map where all project remotes come from that host. 1682 func groupByGoogleSourceHosts(ps Projects) map[string]Projects { 1683 m := make(map[string]Projects) 1684 for _, p := range ps { 1685 if !googlesource.IsGoogleSourceRemote(p.Remote) { 1686 continue 1687 } 1688 u, err := url.Parse(p.Remote) 1689 if err != nil { 1690 continue 1691 } 1692 host := u.Scheme + "://" + u.Host 1693 if _, ok := m[host]; !ok { 1694 m[host] = Projects{} 1695 } 1696 m[host][p.Key()] = p 1697 } 1698 return m 1699 } 1700 1701 // getRemoteHeadRevisions attempts to get the repo statuses from remote for 1702 // projects at HEAD so we can detect when a local project is already 1703 // up-to-date. 1704 func getRemoteHeadRevisions(jirix *jiri.X, remoteProjects Projects) { 1705 projectsAtHead := Projects{} 1706 for _, rp := range remoteProjects { 1707 if rp.Revision == "HEAD" { 1708 projectsAtHead[rp.Key()] = rp 1709 } 1710 } 1711 gsHostsMap := groupByGoogleSourceHosts(projectsAtHead) 1712 for host, projects := range gsHostsMap { 1713 branchesMap := make(map[string]bool) 1714 for _, p := range projects { 1715 branchesMap[p.RemoteBranch] = true 1716 } 1717 branches := set.StringBool.ToSlice(branchesMap) 1718 repoStatuses, err := googlesource.GetRepoStatuses(jirix, host, branches) 1719 if err != nil { 1720 // Log the error but don't fail. 1721 fmt.Fprintf(jirix.Stderr(), "Error fetching repo statuses from remote: %v\n", err) 1722 continue 1723 } 1724 for _, p := range projects { 1725 status, ok := repoStatuses[p.Name] 1726 if !ok { 1727 continue 1728 } 1729 rev, ok := status.Branches[p.RemoteBranch] 1730 if !ok || rev == "" { 1731 continue 1732 } 1733 rp := remoteProjects[p.Key()] 1734 rp.Revision = rev 1735 remoteProjects[p.Key()] = rp 1736 } 1737 } 1738 } 1739 1740 func updateProjects(jirix *jiri.X, localProjects, remoteProjects Projects, gc bool) error { 1741 jirix.TimerPush("update projects") 1742 defer jirix.TimerPop() 1743 1744 getRemoteHeadRevisions(jirix, remoteProjects) 1745 ops := computeOperations(localProjects, remoteProjects, gc) 1746 updates := newFsUpdates() 1747 for _, op := range ops { 1748 if err := op.Test(jirix, updates); err != nil { 1749 return err 1750 } 1751 } 1752 s := jirix.NewSeq() 1753 for _, op := range ops { 1754 updateFn := func() error { return op.Run(jirix) } 1755 // Always log the output of updateFn, irrespective of 1756 // the value of the verbose flag. 1757 if err := s.Verbose(true).Call(updateFn, "%v", op).Done(); err != nil { 1758 return fmt.Errorf("error updating project %q: %v", op.Project().Name, err) 1759 } 1760 } 1761 if err := runHooks(jirix, ops); err != nil { 1762 return err 1763 } 1764 return applyGitHooks(jirix, ops) 1765 } 1766 1767 // runHooks runs all hooks for the given operations. 1768 func runHooks(jirix *jiri.X, ops []operation) error { 1769 jirix.TimerPush("run hooks") 1770 defer jirix.TimerPop() 1771 for _, op := range ops { 1772 if op.Project().RunHook == "" { 1773 continue 1774 } 1775 if op.Kind() != "create" && op.Kind() != "move" && op.Kind() != "update" { 1776 continue 1777 } 1778 s := jirix.NewSeq() 1779 s.Verbose(true).Output([]string{fmt.Sprintf("running hook for project %q", op.Project().Name)}) 1780 if err := s.Dir(op.Project().Path).Capture(os.Stdout, os.Stderr).Last(op.Project().RunHook, op.Kind()); err != nil { 1781 // TODO(nlacasse): Should we delete projectDir or perform some 1782 // other cleanup in the event of a hook failure? 1783 return fmt.Errorf("error running hook for project %q: %v", op.Project().Name, err) 1784 } 1785 } 1786 return nil 1787 } 1788 1789 func applyGitHooks(jirix *jiri.X, ops []operation) error { 1790 jirix.TimerPush("apply githooks") 1791 defer jirix.TimerPop() 1792 s := jirix.NewSeq() 1793 for _, op := range ops { 1794 if op.Kind() == "create" || op.Kind() == "move" { 1795 // Apply exclusion for /.jiri/. Ideally we'd only write this file on 1796 // create, but the remote manifest import is move from the temp directory 1797 // into the final spot, so we need this to apply to both. 1798 // 1799 // TODO(toddw): Find a better way to do this. 1800 excludeDir := filepath.Join(op.Project().Path, ".git", "info") 1801 excludeFile := filepath.Join(excludeDir, "exclude") 1802 excludeString := "/.jiri/\n" 1803 if err := s.MkdirAll(excludeDir, 0755).WriteFile(excludeFile, []byte(excludeString), 0644).Done(); err != nil { 1804 return err 1805 } 1806 } 1807 if op.Project().GitHooks == "" { 1808 continue 1809 } 1810 if op.Kind() != "create" && op.Kind() != "move" && op.Kind() != "update" { 1811 continue 1812 } 1813 // Apply git hooks, overwriting any existing hooks. Jiri is in control of 1814 // writing all hooks. 1815 gitHooksDstDir := filepath.Join(op.Project().Path, ".git", "hooks") 1816 // Copy the specified GitHooks directory into the project's git 1817 // hook directory. We walk the file system, creating directories 1818 // and copying files as we encounter them. 1819 copyFn := func(path string, info os.FileInfo, err error) error { 1820 if err != nil { 1821 return err 1822 } 1823 relPath, err := filepath.Rel(op.Project().GitHooks, path) 1824 if err != nil { 1825 return err 1826 } 1827 dst := filepath.Join(gitHooksDstDir, relPath) 1828 if info.IsDir() { 1829 return s.MkdirAll(dst, 0755).Done() 1830 } 1831 src, err := s.ReadFile(path) 1832 if err != nil { 1833 return err 1834 } 1835 // The file *must* be executable to be picked up by git. 1836 return s.WriteFile(dst, src, 0755).Done() 1837 } 1838 if err := filepath.Walk(op.Project().GitHooks, copyFn); err != nil { 1839 return err 1840 } 1841 } 1842 return nil 1843 } 1844 1845 // writeMetadata stores the given project metadata in the directory 1846 // identified by the given path. 1847 func writeMetadata(jirix *jiri.X, project Project, dir string) (e error) { 1848 metadataDir := filepath.Join(dir, jiri.ProjectMetaDir) 1849 cwd, err := os.Getwd() 1850 if err != nil { 1851 return err 1852 } 1853 defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e) 1854 1855 s := jirix.NewSeq() 1856 if err := s.MkdirAll(metadataDir, os.FileMode(0755)). 1857 Chdir(metadataDir).Done(); err != nil { 1858 return err 1859 } 1860 metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile) 1861 return project.ToFile(jirix, metadataFile) 1862 } 1863 1864 // fsUpdates is used to track filesystem updates made by operations. 1865 // TODO(nlacasse): Currently we only use fsUpdates to track deletions so that 1866 // jiri can delete and create a project in the same directory in one update. 1867 // There are lots of other cases that should be covered though, like detecting 1868 // when two projects would be created in the same directory. 1869 type fsUpdates struct { 1870 deletedDirs map[string]bool 1871 } 1872 1873 func newFsUpdates() *fsUpdates { 1874 return &fsUpdates{ 1875 deletedDirs: map[string]bool{}, 1876 } 1877 } 1878 1879 func (u *fsUpdates) deleteDir(dir string) { 1880 dir = filepath.Clean(dir) 1881 u.deletedDirs[dir] = true 1882 } 1883 1884 func (u *fsUpdates) isDeleted(dir string) bool { 1885 _, ok := u.deletedDirs[filepath.Clean(dir)] 1886 return ok 1887 } 1888 1889 type operation interface { 1890 // Project identifies the project this operation pertains to. 1891 Project() Project 1892 // Kind returns the kind of operation. 1893 Kind() string 1894 // Run executes the operation. 1895 Run(jirix *jiri.X) error 1896 // String returns a string representation of the operation. 1897 String() string 1898 // Test checks whether the operation would fail. 1899 Test(jirix *jiri.X, updates *fsUpdates) error 1900 } 1901 1902 // commonOperation represents a project operation. 1903 type commonOperation struct { 1904 // project holds information about the project such as its 1905 // name, local path, and the protocol it uses for version 1906 // control. 1907 project Project 1908 // destination is the new project path. 1909 destination string 1910 // source is the current project path. 1911 source string 1912 } 1913 1914 func (op commonOperation) Project() Project { 1915 return op.project 1916 } 1917 1918 // createOperation represents the creation of a project. 1919 type createOperation struct { 1920 commonOperation 1921 } 1922 1923 func (op createOperation) Kind() string { 1924 return "create" 1925 } 1926 1927 func (op createOperation) Run(jirix *jiri.X) (e error) { 1928 s := jirix.NewSeq() 1929 1930 path, perm := filepath.Dir(op.destination), os.FileMode(0755) 1931 tmpDirPrefix := strings.Replace(op.Project().Name, "/", ".", -1) + "-" 1932 1933 // Create a temporary directory for the initial setup of the 1934 // project to prevent an untimely termination from leaving the 1935 // $JIRI_ROOT directory in an inconsistent state. 1936 tmpDir, err := s.MkdirAll(path, perm).TempDir(path, tmpDirPrefix) 1937 if err != nil { 1938 return err 1939 } 1940 defer collect.Error(func() error { return jirix.NewSeq().RemoveAll(tmpDir).Done() }, &e) 1941 switch op.project.Protocol { 1942 case "git": 1943 if err := gitutil.New(jirix.NewSeq()).Clone(op.project.Remote, tmpDir); err != nil { 1944 return err 1945 } 1946 cwd, err := os.Getwd() 1947 if err != nil { 1948 return err 1949 } 1950 defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e) 1951 if err := s.Chdir(tmpDir).Done(); err != nil { 1952 return err 1953 } 1954 default: 1955 return UnsupportedProtocolErr(op.project.Protocol) 1956 } 1957 if err := writeMetadata(jirix, op.project, tmpDir); err != nil { 1958 return err 1959 } 1960 if err := s.Chmod(tmpDir, os.FileMode(0755)). 1961 Rename(tmpDir, op.destination).Done(); err != nil { 1962 return err 1963 } 1964 return syncProjectMaster(jirix, op.project) 1965 } 1966 1967 func (op createOperation) String() string { 1968 return fmt.Sprintf("create project %q in %q and advance it to %q", op.project.Name, op.destination, fmtRevision(op.project.Revision)) 1969 } 1970 1971 func (op createOperation) Test(jirix *jiri.X, updates *fsUpdates) error { 1972 // Check the local file system. 1973 if _, err := jirix.NewSeq().Stat(op.destination); err != nil { 1974 if !runutil.IsNotExist(err) { 1975 return err 1976 } 1977 } else if !updates.isDeleted(op.destination) { 1978 return fmt.Errorf("cannot create %q as it already exists", op.destination) 1979 } 1980 return nil 1981 } 1982 1983 // deleteOperation represents the deletion of a project. 1984 type deleteOperation struct { 1985 commonOperation 1986 // gc determines whether the operation should be executed or 1987 // whether it should only print a notification. 1988 gc bool 1989 } 1990 1991 func (op deleteOperation) Kind() string { 1992 return "delete" 1993 } 1994 func (op deleteOperation) Run(jirix *jiri.X) error { 1995 s := jirix.NewSeq() 1996 if op.gc { 1997 // Never delete projects with non-master branches, uncommitted 1998 // work, or untracked content. 1999 git := gitutil.New(jirix.NewSeq(), gitutil.RootDirOpt(op.project.Path)) 2000 branches, _, err := git.GetBranches() 2001 if err != nil { 2002 return err 2003 } 2004 uncommitted, err := git.HasUncommittedChanges() 2005 if err != nil { 2006 return err 2007 } 2008 untracked, err := git.HasUntrackedFiles() 2009 if err != nil { 2010 return err 2011 } 2012 if len(branches) != 1 || uncommitted || untracked { 2013 lines := []string{ 2014 fmt.Sprintf("NOTE: project %v was not found in the project manifest", op.project.Name), 2015 "however this project either contains non-master branches, uncommitted", 2016 "work, or untracked files and will thus not be deleted", 2017 } 2018 s.Verbose(true).Output(lines) 2019 return nil 2020 } 2021 return s.RemoveAll(op.source).Done() 2022 } 2023 lines := []string{ 2024 fmt.Sprintf("NOTE: project %v was not found in the project manifest", op.project.Name), 2025 "it was not automatically removed to avoid deleting uncommitted work", 2026 fmt.Sprintf(`if you no longer need it, invoke "rm -rf %v"`, op.source), 2027 `or invoke "jiri update -gc" to remove all such local projects`, 2028 } 2029 s.Verbose(true).Output(lines) 2030 return nil 2031 } 2032 2033 func (op deleteOperation) String() string { 2034 return fmt.Sprintf("delete project %q from %q", op.project.Name, op.source) 2035 } 2036 2037 func (op deleteOperation) Test(jirix *jiri.X, updates *fsUpdates) error { 2038 if _, err := jirix.NewSeq().Stat(op.source); err != nil { 2039 if runutil.IsNotExist(err) { 2040 return fmt.Errorf("cannot delete %q as it does not exist", op.source) 2041 } 2042 return err 2043 } 2044 updates.deleteDir(op.source) 2045 return nil 2046 } 2047 2048 // moveOperation represents the relocation of a project. 2049 type moveOperation struct { 2050 commonOperation 2051 } 2052 2053 func (op moveOperation) Kind() string { 2054 return "move" 2055 } 2056 func (op moveOperation) Run(jirix *jiri.X) error { 2057 s := jirix.NewSeq() 2058 path, perm := filepath.Dir(op.destination), os.FileMode(0755) 2059 if err := s.MkdirAll(path, perm).Rename(op.source, op.destination).Done(); err != nil { 2060 return err 2061 } 2062 if err := reportNonMaster(jirix, op.project); err != nil { 2063 return err 2064 } 2065 if err := syncProjectMaster(jirix, op.project); err != nil { 2066 return err 2067 } 2068 return writeMetadata(jirix, op.project, op.project.Path) 2069 } 2070 2071 func (op moveOperation) String() string { 2072 return fmt.Sprintf("move project %q located in %q to %q and advance it to %q", op.project.Name, op.source, op.destination, fmtRevision(op.project.Revision)) 2073 } 2074 2075 func (op moveOperation) Test(jirix *jiri.X, updates *fsUpdates) error { 2076 s := jirix.NewSeq() 2077 if _, err := s.Stat(op.source); err != nil { 2078 if runutil.IsNotExist(err) { 2079 return fmt.Errorf("cannot move %q to %q as the source does not exist", op.source, op.destination) 2080 } 2081 return err 2082 } 2083 if _, err := s.Stat(op.destination); err != nil { 2084 if !runutil.IsNotExist(err) { 2085 return err 2086 } 2087 } else { 2088 return fmt.Errorf("cannot move %q to %q as the destination already exists", op.source, op.destination) 2089 } 2090 updates.deleteDir(op.source) 2091 return nil 2092 } 2093 2094 // updateOperation represents the update of a project. 2095 type updateOperation struct { 2096 commonOperation 2097 } 2098 2099 func (op updateOperation) Kind() string { 2100 return "update" 2101 } 2102 func (op updateOperation) Run(jirix *jiri.X) error { 2103 if err := reportNonMaster(jirix, op.project); err != nil { 2104 return err 2105 } 2106 if err := syncProjectMaster(jirix, op.project); err != nil { 2107 return err 2108 } 2109 return writeMetadata(jirix, op.project, op.project.Path) 2110 } 2111 2112 func (op updateOperation) String() string { 2113 return fmt.Sprintf("advance project %q located in %q to %q", op.project.Name, op.source, fmtRevision(op.project.Revision)) 2114 } 2115 2116 func (op updateOperation) Test(jirix *jiri.X, _ *fsUpdates) error { 2117 return nil 2118 } 2119 2120 // nullOperation represents a noop. It is used for logging and adding project 2121 // information to the current manifest. 2122 type nullOperation struct { 2123 commonOperation 2124 } 2125 2126 func (op nullOperation) Kind() string { 2127 return "null" 2128 } 2129 2130 func (op nullOperation) Run(jirix *jiri.X) error { 2131 return writeMetadata(jirix, op.project, op.project.Path) 2132 } 2133 2134 func (op nullOperation) String() string { 2135 return fmt.Sprintf("project %q located in %q at revision %q is up-to-date", op.project.Name, op.source, fmtRevision(op.project.Revision)) 2136 } 2137 2138 func (op nullOperation) Test(jirix *jiri.X, _ *fsUpdates) error { 2139 return nil 2140 } 2141 2142 // operations is a sortable collection of operations 2143 type operations []operation 2144 2145 // Len returns the length of the collection. 2146 func (ops operations) Len() int { 2147 return len(ops) 2148 } 2149 2150 // Less defines the order of operations. Operations are ordered first 2151 // by their type and then by their project path. 2152 // 2153 // The order in which operation types are defined determines the order 2154 // in which operations are performed. For correctness and also to 2155 // minimize the chance of a conflict, the delete operations should 2156 // happen before move operations, which should happen before create 2157 // operations. If two create operations make nested directories, the 2158 // outermost should be created first. 2159 func (ops operations) Less(i, j int) bool { 2160 vals := make([]int, 2) 2161 for idx, op := range []operation{ops[i], ops[j]} { 2162 switch op.Kind() { 2163 case "delete": 2164 vals[idx] = 0 2165 case "move": 2166 vals[idx] = 1 2167 case "create": 2168 vals[idx] = 2 2169 case "update": 2170 vals[idx] = 3 2171 case "null": 2172 vals[idx] = 4 2173 } 2174 } 2175 if vals[0] != vals[1] { 2176 return vals[0] < vals[1] 2177 } 2178 return ops[i].Project().Path < ops[j].Project().Path 2179 } 2180 2181 // Swap swaps two elements of the collection. 2182 func (ops operations) Swap(i, j int) { 2183 ops[i], ops[j] = ops[j], ops[i] 2184 } 2185 2186 // computeOperations inputs a set of projects to update and the set of 2187 // current and new projects (as defined by contents of the local file 2188 // system and manifest file respectively) and outputs a collection of 2189 // operations that describe the actions needed to update the target 2190 // projects. 2191 func computeOperations(localProjects, remoteProjects Projects, gc bool) operations { 2192 result := operations{} 2193 allProjects := map[ProjectKey]bool{} 2194 for _, p := range localProjects { 2195 allProjects[p.Key()] = true 2196 } 2197 for _, p := range remoteProjects { 2198 allProjects[p.Key()] = true 2199 } 2200 for key, _ := range allProjects { 2201 var local, remote *Project 2202 if project, ok := localProjects[key]; ok { 2203 local = &project 2204 } 2205 if project, ok := remoteProjects[key]; ok { 2206 remote = &project 2207 } 2208 result = append(result, computeOp(local, remote, gc)) 2209 } 2210 sort.Sort(result) 2211 return result 2212 } 2213 2214 func computeOp(local, remote *Project, gc bool) operation { 2215 switch { 2216 case local == nil && remote != nil: 2217 return createOperation{commonOperation{ 2218 destination: remote.Path, 2219 project: *remote, 2220 source: "", 2221 }} 2222 case local != nil && remote == nil: 2223 return deleteOperation{commonOperation{ 2224 destination: "", 2225 project: *local, 2226 source: local.Path, 2227 }, gc} 2228 case local != nil && remote != nil: 2229 switch { 2230 case local.Path != remote.Path: 2231 // moveOperation also does an update, so we don't need to check the 2232 // revision here. 2233 return moveOperation{commonOperation{ 2234 destination: remote.Path, 2235 project: *remote, 2236 source: local.Path, 2237 }} 2238 case local.Revision != remote.Revision: 2239 return updateOperation{commonOperation{ 2240 destination: remote.Path, 2241 project: *remote, 2242 source: local.Path, 2243 }} 2244 default: 2245 return nullOperation{commonOperation{ 2246 destination: remote.Path, 2247 project: *remote, 2248 source: local.Path, 2249 }} 2250 } 2251 default: 2252 panic("jiri: computeOp called with nil local and remote") 2253 } 2254 } 2255 2256 // ParseNames identifies the set of projects that a jiri command should be 2257 // applied to. 2258 func ParseNames(jirix *jiri.X, args []string, defaultProjects map[string]struct{}) (Projects, error) { 2259 localProjects, err := LocalProjects(jirix, FullScan) 2260 if err != nil { 2261 return nil, err 2262 } 2263 result := Projects{} 2264 if len(args) == 0 { 2265 // Use the default set of projects. 2266 args = set.String.ToSlice(defaultProjects) 2267 } 2268 for _, name := range args { 2269 projects := localProjects.Find(name) 2270 if len(projects) == 0 { 2271 // Issue a warning if the target project does not exist in the 2272 // project manifest. 2273 fmt.Fprintf(jirix.Stderr(), "project %q does not exist locally\n", name) 2274 } 2275 for _, project := range projects { 2276 result[project.Key()] = project 2277 } 2278 } 2279 return result, nil 2280 } 2281 2282 // fmtRevision returns the first 8 chars of a revision hash. 2283 func fmtRevision(r string) string { 2284 l := 8 2285 if len(r) < l { 2286 return r 2287 } 2288 return r[:l] 2289 }