github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/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/json" 10 "encoding/xml" 11 "errors" 12 "fmt" 13 "io" 14 "io/ioutil" 15 "net/http" 16 "net/url" 17 "os" 18 "path" 19 "path/filepath" 20 "reflect" 21 "regexp" 22 "sort" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/btwiuse/jiri" 28 "github.com/btwiuse/jiri/cipd" 29 "github.com/btwiuse/jiri/gerrit" 30 "github.com/btwiuse/jiri/gitutil" 31 "github.com/btwiuse/jiri/log" 32 "github.com/btwiuse/jiri/retry" 33 ) 34 35 var ( 36 errVersionMismatch = errors.New("snapshot file version mismatch") 37 ssoRe = regexp.MustCompile("^sso://(.*?)/") 38 DefaultHookTimeout = uint(5) // DefaultHookTimeout is the time in minutes to wait for a hook to timeout. 39 DefaultPackageTimeout = uint(20) // DefaultPackageTimeout is the time in minutes to wait for cipd fetching packages. 40 ) 41 42 const ( 43 JiriProject = "release.go.jiri" 44 JiriName = "jiri" 45 JiriPackage = "github.com/btwiuse/jiri" 46 ManifestVersion = "1.1" 47 ) 48 49 // Project represents a jiri project. 50 type Project struct { 51 // Name is the project name. 52 Name string `xml:"name,attr,omitempty"` 53 // Path is the path used to store the project locally. Project 54 // manifest uses paths that are relative to the root directory. 55 // When a manifest is parsed (e.g. in RemoteProjects), the program 56 // logic converts the relative paths to an absolute paths, using 57 // the current root as a prefix. 58 Path string `xml:"path,attr,omitempty"` 59 // Remote is the project remote. 60 Remote string `xml:"remote,attr,omitempty"` 61 // RemoteBranch is the name of the remote branch to track. 62 RemoteBranch string `xml:"remotebranch,attr,omitempty"` 63 // Revision is the revision the project should be advanced to during "jiri 64 // update". If Revision is set, RemoteBranch will be ignored. If Revision 65 // is not set, "HEAD" is used as the default. 66 Revision string `xml:"revision,attr,omitempty"` 67 // HistoryDepth is the depth flag passed to git clone and git fetch 68 // commands. It is used to limit downloading large histories for large 69 // projects. 70 HistoryDepth int `xml:"historydepth,attr,omitempty"` 71 // GerritHost is the gerrit host where project CLs will be sent. 72 GerritHost string `xml:"gerrithost,attr,omitempty"` 73 // GitHooks is a directory containing git hooks that will be installed for 74 // this project. 75 GitHooks string `xml:"githooks,attr,omitempty"` 76 77 // Attributes is a list of attributes for a project seperated by comma. 78 // The project will not be fetched by default when attributes are present. 79 Attributes string `xml:"attributes,attr,omitempty"` 80 81 // GitAttributes is a list comma-separated attributes for a project, 82 // which will be helpful to group projects with similar purposes together. 83 // It will be used for .gitattributes file generation. 84 GitAttributes string `xml:"git_attributes,attr,omitempty"` 85 86 // Flag defines the content that should be written to a file when 87 // this project is successfully fetched. 88 Flag string `xml:"flag,attr,omitempty"` 89 90 XMLName struct{} `xml:"project"` 91 92 // This is used to store computed key. This is useful when remote and 93 // local projects are same but have different name or remote 94 ComputedKey ProjectKey `xml:"-"` 95 96 // This stores the local configuration file for the project 97 LocalConfig LocalConfig `xml:"-"` 98 99 // ComputedAttributes stores computed attributes object 100 // which is easiler to perform matching and comparing. 101 ComputedAttributes attributes `xml:"-"` 102 103 // ManifestPath stores the absolute path of the manifest. 104 ManifestPath string `xml:"-"` 105 } 106 107 // ProjectsByPath implements the Sort interface. It sorts Projects by 108 // the Path field. 109 type ProjectsByPath []Project 110 111 func (projects ProjectsByPath) Len() int { 112 return len(projects) 113 } 114 func (projects ProjectsByPath) Swap(i, j int) { 115 projects[i], projects[j] = projects[j], projects[i] 116 } 117 func (projects ProjectsByPath) Less(i, j int) bool { 118 return projects[i].Path+string(filepath.Separator) < projects[j].Path+string(filepath.Separator) 119 } 120 121 // ProjectKey is a unique string for a project. 122 type ProjectKey string 123 124 // MakeProjectKey returns the project key, given the project name and remote. 125 func MakeProjectKey(name, remote string) ProjectKey { 126 return ProjectKey(name + KeySeparator + remote) 127 } 128 129 // KeySeparator is a reserved string used in ProjectKeys and HookKeys. 130 // It cannot occur in Project or Hook names. 131 const KeySeparator = "=" 132 133 // ProjectKeys is a slice of ProjectKeys implementing the Sort interface. 134 type ProjectKeys []ProjectKey 135 136 func (pks ProjectKeys) Len() int { return len(pks) } 137 func (pks ProjectKeys) Less(i, j int) bool { return string(pks[i]) < string(pks[j]) } 138 func (pks ProjectKeys) Swap(i, j int) { pks[i], pks[j] = pks[j], pks[i] } 139 140 // ProjectFromFile returns a project parsed from the contents of filename, 141 // with defaults filled in and all paths absolute. 142 func ProjectFromFile(jirix *jiri.X, filename string) (*Project, error) { 143 data, err := ioutil.ReadFile(filename) 144 if err != nil { 145 return nil, fmtError(err) 146 } 147 148 p := new(Project) 149 if err := xml.Unmarshal(data, p); err != nil { 150 return nil, err 151 } 152 if err := p.fillDefaults(); err != nil { 153 return nil, err 154 } 155 p.absolutizePaths(jirix.Root) 156 return p, nil 157 } 158 159 // ToFile writes the project p to a file with the given filename, with defaults 160 // unfilled and all paths relative to the jiri root. 161 func (p Project) ToFile(jirix *jiri.X, filename string) error { 162 if err := p.unfillDefaults(); err != nil { 163 return err 164 } 165 // Replace absolute paths with relative paths to make it possible to move 166 // the root directory locally. 167 if err := p.relativizePaths(jirix.Root); err != nil { 168 return err 169 } 170 data, err := xml.Marshal(p) 171 if err != nil { 172 return fmt.Errorf("project xml.Marshal failed: %v", err) 173 } 174 // Same logic as Manifest.ToBytes, to make the output more compact. 175 data = bytes.Replace(data, endProjectSoloBytes, endElemSoloBytes, -1) 176 if !bytes.HasSuffix(data, newlineBytes) { 177 data = append(data, '\n') 178 } 179 return safeWriteFile(jirix, filename, data) 180 } 181 182 // absolutizePaths makes all relative paths absolute by prepending basepath. 183 func (p *Project) absolutizePaths(basepath string) { 184 if p.Path != "" && !filepath.IsAbs(p.Path) { 185 p.Path = filepath.Join(basepath, p.Path) 186 } 187 if p.GitHooks != "" && !filepath.IsAbs(p.GitHooks) { 188 p.GitHooks = filepath.Join(basepath, p.GitHooks) 189 } 190 } 191 192 // relativizePaths makes all absolute paths relative to basepath. 193 func (p *Project) relativizePaths(basepath string) error { 194 if filepath.IsAbs(p.Path) { 195 relPath, err := filepath.Rel(basepath, p.Path) 196 if err != nil { 197 return err 198 } 199 p.Path = relPath 200 } 201 if filepath.IsAbs(p.GitHooks) { 202 relGitHooks, err := filepath.Rel(basepath, p.GitHooks) 203 if err != nil { 204 return err 205 } 206 p.GitHooks = relGitHooks 207 } 208 return nil 209 } 210 211 // Key returns the unique ProjectKey for the project. 212 func (p Project) Key() ProjectKey { 213 if p.ComputedKey == "" { 214 p.ComputedKey = MakeProjectKey(p.Name, p.Remote) 215 } 216 return p.ComputedKey 217 } 218 219 func (p *Project) fillDefaults() error { 220 if p.RemoteBranch == "" { 221 p.RemoteBranch = "master" 222 } 223 if p.Revision == "" { 224 p.Revision = "HEAD" 225 } 226 return p.validate() 227 } 228 229 func (p *Project) unfillDefaults() error { 230 if p.RemoteBranch == "master" { 231 p.RemoteBranch = "" 232 } 233 if p.Revision == "HEAD" { 234 p.Revision = "" 235 } 236 return p.validate() 237 } 238 239 func (p *Project) validate() error { 240 if strings.Contains(p.Name, KeySeparator) { 241 return fmt.Errorf("bad project: name cannot contain %q: %+v", KeySeparator, *p) 242 } 243 return nil 244 } 245 246 func (p *Project) update(other *Project) { 247 if other.Path != "" { 248 p.Path = other.Path 249 } 250 if other.RemoteBranch != "" { 251 p.RemoteBranch = other.RemoteBranch 252 } 253 if other.Revision != "" { 254 p.Revision = other.Revision 255 } 256 if other.HistoryDepth != 0 { 257 p.HistoryDepth = other.HistoryDepth 258 } 259 if other.GerritHost != "" { 260 p.GerritHost = other.GerritHost 261 } 262 if other.GitHooks != "" { 263 p.GitHooks = other.GitHooks 264 } 265 if other.Flag != "" { 266 p.Flag = other.Flag 267 } 268 } 269 270 // WriteProjectFlags write flag files into project directory using in "flag" 271 // attribute from projs. 272 func WriteProjectFlags(jirix *jiri.X, projs Projects) error { 273 // The flag attribute has a format of $FILE_NAME|$FLAG_SUCCESSFUL|$FLAG_FAILED 274 // When a package is successfully downloaded, jiri will write $FLAG_SUCCESSFUL 275 // to $FILE_NAME. If the package is not downloaded due to access reasons, 276 // jiri will write $FLAG_FAILED to $FILE_NAME. 277 // '|' is a forbidden symbol in Windows path, which is unlikely 278 // to be used by path. 279 280 // Unlike WritePackageFlags that writes the failure flags when the package was 281 // not fetched due to permission issues, this function will not write failure 282 // flags, as unfetchable projects are considered as errors. 283 flagMap := make(map[string]string) 284 fill := func(file, flag string) error { 285 if v, ok := flagMap[file]; ok { 286 if v != flag { 287 return fmt.Errorf("encountered conflicting flags for file %q: %q conflicts with %q", file, v, flag) 288 } 289 } else { 290 flagMap[file] = flag 291 } 292 return nil 293 } 294 295 for _, v := range projs { 296 if v.Flag == "" { 297 continue 298 } 299 fields := strings.Split(v.Flag, "|") 300 if len(fields) != 3 { 301 return fmt.Errorf("unknown project flag format found in project %+v", v) 302 } 303 if err := fill(fields[0], fields[1]); err != nil { 304 return err 305 } 306 } 307 308 var writeErrorBuf bytes.Buffer 309 for k, v := range flagMap { 310 if err := ioutil.WriteFile(filepath.Join(jirix.Root, k), []byte(v), 0644); err != nil { 311 writeErrorBuf.WriteString(fmt.Sprintf("write package flag %q to file %q failed: %v\n", v, k, err)) 312 } 313 } 314 if writeErrorBuf.Len() > 0 { 315 return errors.New(writeErrorBuf.String()) 316 } 317 return nil 318 } 319 320 type attributes map[string]bool 321 322 // newAttributes will create a new attributes object 323 // which is used in Project and Package objects. 324 func newAttributes(attrs string) attributes { 325 retMap := make(attributes) 326 if strings.HasPrefix(attrs, "+") { 327 attrs = attrs[1:] 328 } 329 for _, v := range strings.Split(attrs, ",") { 330 key := strings.TrimSpace(v) 331 if key != "" { 332 retMap[key] = true 333 } 334 } 335 return retMap 336 } 337 338 func (m attributes) IsEmpty() bool { 339 return len(m) == 0 340 } 341 342 func (m attributes) Add(other attributes) { 343 for k := range other { 344 if _, ok := m[k]; !ok { 345 m[k] = true 346 } 347 } 348 } 349 350 func (m attributes) Match(other attributes) bool { 351 for k := range other { 352 if _, ok := m[k]; ok { 353 return true 354 } 355 } 356 return false 357 } 358 359 func (m attributes) String() string { 360 attrs := make([]string, 0) 361 var buf bytes.Buffer 362 for k := range m { 363 attrs = append(attrs, k) 364 } 365 sort.Strings(attrs) 366 first := true 367 for _, v := range attrs { 368 if !first { 369 buf.WriteString(",") 370 } 371 buf.WriteString(v) 372 first = false 373 } 374 return buf.String() 375 } 376 377 // ProjectLock describes locked version information for a jiri managed project. 378 type ProjectLock struct { 379 Remote string `json:"repository_url"` 380 Name string `json:"name"` 381 Revision string `json:"revision"` 382 } 383 384 // ProjectLockKey defines the key used in ProjectLocks type 385 type ProjectLockKey string 386 387 // ProjectLocks type is a map wrapper over ProjectLock for faster look up. 388 type ProjectLocks map[ProjectLockKey]ProjectLock 389 390 func (p ProjectLock) Key() ProjectLockKey { 391 return ProjectLockKey(p.Name + KeySeparator + p.Remote) 392 } 393 394 // PackageLock describes locked version information for a jiri managed package. 395 type PackageLock struct { 396 PackageName string `json:"package"` 397 LocalPath string `json:"path,omitempty"` 398 VersionTag string `json:"version"` 399 InstanceID string `json:"instance_id"` 400 } 401 402 // PackageLockKey defines the key used in PackageLocks type 403 type PackageLockKey string 404 405 // PackageLocks type is map wrapper over PackageLock for faster look up 406 type PackageLocks map[PackageLockKey]PackageLock 407 408 func (p PackageLock) Key() PackageLockKey { 409 return PackageLockKey(p.PackageName + KeySeparator + p.VersionTag) 410 } 411 412 // ResolveConfig interface provides the configuration 413 // for jiri resolve command. 414 type ResolveConfig interface { 415 AllowFloatingRefs() bool 416 LockFilePath() string 417 LocalManifest() bool 418 EnablePackageLock() bool 419 EnableProjectLock() bool 420 HostnameAllowList() []string 421 } 422 423 // UnmarshalLockEntries unmarshals project locks and package locks from 424 // jsonData. 425 func UnmarshalLockEntries(jsonData []byte) (ProjectLocks, PackageLocks, error) { 426 entries := make([]interface{}, 0) 427 projectLocks := make(ProjectLocks) 428 pkgLocks := make(PackageLocks) 429 if err := json.Unmarshal(jsonData, &entries); err != nil { 430 return nil, nil, err 431 } 432 for _, entry := range entries { 433 entryMap := entry.(map[string]interface{}) 434 if _, ok := entryMap["package"]; ok { 435 pkgName, ok := entryMap["package"].(string) 436 if !ok { 437 return nil, nil, fmt.Errorf("package name %+v is not a valid string", entryMap["package"]) 438 } 439 id, ok := entryMap["instance_id"].(string) 440 if !ok { 441 return nil, nil, fmt.Errorf("package instance_id %+v is not a valid string", entryMap["instance_id"]) 442 } 443 version, ok := entryMap["version"].(string) 444 if !ok { 445 return nil, nil, fmt.Errorf("package version %+v is not a valid string", entryMap["version"]) 446 } 447 pkgLock := PackageLock{ 448 PackageName: pkgName, 449 VersionTag: version, 450 InstanceID: id, 451 } 452 if v, ok := pkgLocks[pkgLock.Key()]; ok { 453 if v != pkgLock { 454 return nil, nil, fmt.Errorf("package %q has more than 1 version lock %q, %q", pkgName, v.InstanceID, id) 455 } 456 } 457 pkgLocks[pkgLock.Key()] = pkgLock 458 } else if _, ok := entryMap["repository_url"]; ok { 459 repoURL, ok := entryMap["repository_url"].(string) 460 if !ok { 461 return nil, nil, fmt.Errorf("project repository url %+v is not a valid string", entryMap["repository_url"]) 462 } 463 revision, ok := entryMap["revision"].(string) 464 if !ok { 465 return nil, nil, fmt.Errorf("project revision %+v is not a valid string", entryMap["revision"]) 466 } 467 name, ok := entryMap["name"].(string) 468 if !ok { 469 return nil, nil, fmt.Errorf("project name %+v is not a valid string", entryMap["name"]) 470 } 471 472 projectLock := ProjectLock{repoURL, name, revision} 473 if v, ok := projectLocks[projectLock.Key()]; ok { 474 if v != projectLock { 475 return nil, nil, fmt.Errorf("package %q has more than 1 revision lock %q, %q", repoURL, v.Revision, revision) 476 } 477 } 478 projectLocks[projectLock.Key()] = projectLock 479 } 480 // Ignore unknown lockfile entries without raising an error 481 } 482 return projectLocks, pkgLocks, nil 483 } 484 485 // MarshalLockEntries marshals project locks and package locks into 486 // json format data. 487 func MarshalLockEntries(projectLocks ProjectLocks, pkgLocks PackageLocks) ([]byte, error) { 488 entries := make([]interface{}, len(projectLocks)+len(pkgLocks)) 489 projEntries := make([]ProjectLock, len(projectLocks)) 490 pkgEntries := make([]PackageLock, len(pkgLocks)) 491 492 i := 0 493 for _, v := range projectLocks { 494 projEntries[i] = v 495 i++ 496 } 497 sort.Slice(projEntries, func(i, j int) bool { 498 if projEntries[i].Remote == projEntries[j].Remote { 499 return projEntries[i].Name < projEntries[j].Name 500 } 501 return projEntries[i].Remote < projEntries[j].Remote 502 }) 503 504 i = 0 505 for _, v := range pkgLocks { 506 pkgEntries[i] = v 507 i++ 508 } 509 sort.Slice(pkgEntries, func(i, j int) bool { 510 if pkgEntries[i].PackageName != pkgEntries[j].PackageName { 511 return pkgEntries[i].PackageName < pkgEntries[j].PackageName 512 } 513 if pkgEntries[i].LocalPath != pkgEntries[j].LocalPath { 514 return pkgEntries[i].LocalPath < pkgEntries[j].LocalPath 515 } 516 return pkgEntries[i].VersionTag < pkgEntries[j].VersionTag 517 }) 518 519 i = 0 520 for _, v := range projEntries { 521 entries[i] = v 522 i++ 523 } 524 for _, v := range pkgEntries { 525 entries[i] = v 526 i++ 527 } 528 529 jsonData, err := json.MarshalIndent(&entries, "", " ") 530 if err != nil { 531 return nil, err 532 } 533 return jsonData, nil 534 } 535 536 // overrideProject performs override on project if matching override declaration is found 537 // in manifest. It will return the original project if no suitable match is found. 538 func overrideProject(jirix *jiri.X, project Project, projectOverrides map[string]Project, importOverrides map[string]Import) (Project, error) { 539 540 key := string(project.Key()) 541 if remoteOverride, ok := importOverrides[key]; ok { 542 project.Revision = remoteOverride.Revision 543 if _, ok := projectOverrides[key]; ok { 544 // It's not allowed to have both import override and project override 545 // on same project. 546 return project, fmt.Errorf("detected both import and project overrides on project \"%s:%s\", which is not allowed", project.Name, project.Remote) 547 } 548 } else if projectOverride, ok := projectOverrides[key]; ok { 549 project.update(&projectOverride) 550 } 551 return project, nil 552 } 553 554 // overrideImport performs override on remote import if matching override declaration is found 555 // in manifest. It will return the original remote import if no suitable match is found 556 func overrideImport(jirix *jiri.X, remote Import, projectOverrides map[string]Project, importOverrides map[string]Import) (Import, error) { 557 key := string(remote.ProjectKey()) 558 if _, ok := projectOverrides[key]; ok { 559 return remote, fmt.Errorf("project override \"%s:%s\" cannot be used to override an import", remote.Name, remote.Remote) 560 } 561 if importOverride, ok := importOverrides[key]; ok { 562 remote.update(&importOverride) 563 } 564 return remote, nil 565 } 566 567 func cacheDirPathFromRemote(cacheRoot, remote string) (string, error) { 568 if cacheRoot != "" { 569 url, err := url.Parse(remote) 570 if err != nil { 571 return "", err 572 } 573 dirname := url.Host + strings.Replace(strings.Replace(url.Path, "-", "--", -1), "/", "-", -1) 574 referenceDir := filepath.Join(cacheRoot, dirname) 575 return referenceDir, nil 576 } 577 return "", nil 578 } 579 580 // CacheDirPath returns a generated path to a directory that can be used as a reference repo 581 // for the given project. 582 func (p *Project) CacheDirPath(jirix *jiri.X) (string, error) { 583 return cacheDirPathFromRemote(jirix.Cache, p.Remote) 584 585 } 586 587 func (p *Project) writeJiriRevisionFiles(jirix *jiri.X) error { 588 scm := gitutil.New(jirix, gitutil.RootDirOpt(p.Path)) 589 file := filepath.Join(p.Path, ".git", "JIRI_HEAD") 590 head := "refs/remotes/origin/master" 591 var err error 592 if p.Revision != "" && p.Revision != "HEAD" { 593 head = p.Revision 594 } else if p.RemoteBranch != "" { 595 head = "refs/remotes/origin/" + p.RemoteBranch 596 } 597 head, err = scm.CurrentRevisionForRef(head) 598 if err != nil { 599 return fmt.Errorf("Cannot find revision for ref %q for project %s(%s): %s", head, p.Name, p.Path, err) 600 } 601 if err := safeWriteFile(jirix, file, []byte(head)); err != nil { 602 return err 603 } 604 file = filepath.Join(p.Path, ".git", "JIRI_LAST_BASE") 605 if rev, err := scm.CurrentRevision(); err != nil { 606 return fmt.Errorf("Cannot find current revision for for project %s(%s): %s", p.Name, p.Path, err) 607 } else { 608 return safeWriteFile(jirix, file, []byte(rev)) 609 } 610 } 611 612 func (p *Project) setupDefaultPushTarget(jirix *jiri.X) error { 613 if p.GerritHost == "" { 614 // Skip projects w/o gerrit host 615 return nil 616 } 617 scm := gitutil.New(jirix, gitutil.RootDirOpt(p.Path)) 618 if err := scm.Config("--get", "remote.origin.push"); err != nil { 619 // remote.origin.push does not exist. 620 if err := scm.Config("remote.origin.push", "HEAD:refs/for/master"); err != nil { 621 return fmt.Errorf("not able to set remote.origin.push for project %s(%s) due to error: %v", p.Name, p.Path, err) 622 } 623 } 624 if err := scm.Config("--get", "push.default"); err != nil { 625 // push.default does not exist. 626 if err := scm.Config("push.default", "nothing"); err != nil { 627 return fmt.Errorf("not able to set push.default for project %s(%s) due to error: %v", p.Name, p.Path, err) 628 } 629 } 630 jirix.Logger.Debugf("set remote.origin.push to \"HEAD:refs/for/master\" for project %s(%s)", p.Name, p.Path) 631 return nil 632 } 633 634 func (p *Project) IsOnJiriHead(jirix *jiri.X) (bool, error) { 635 scm := gitutil.New(jirix, gitutil.RootDirOpt(p.Path)) 636 jiriHead := "refs/remotes/origin/master" 637 var err error 638 if p.Revision != "" && p.Revision != "HEAD" { 639 jiriHead = p.Revision 640 } else if p.RemoteBranch != "" { 641 jiriHead = "refs/remotes/origin/" + p.RemoteBranch 642 } 643 jiriHead, err = scm.CurrentRevisionForRef(jiriHead) 644 if err != nil { 645 return false, fmt.Errorf("Cannot find revision for ref %q for project %s(%s): %s", jiriHead, p.Name, p.Path, err) 646 } 647 head, err := scm.CurrentRevision() 648 if err != nil { 649 return false, fmt.Errorf("Cannot find current revision for project %s(%s): %s", p.Name, p.Path, err) 650 } 651 return head == jiriHead, nil 652 } 653 654 // Projects maps ProjectKeys to Projects. 655 type Projects map[ProjectKey]Project 656 657 // toSlice returns a slice of Projects in the Projects map. 658 func (ps Projects) toSlice() []Project { 659 var pSlice []Project 660 for _, p := range ps { 661 pSlice = append(pSlice, p) 662 } 663 return pSlice 664 } 665 666 // Find returns all projects in Projects with the given key or name. 667 func (ps Projects) Find(keyOrName string) Projects { 668 projects := Projects{} 669 if p, ok := ps[ProjectKey(keyOrName)]; ok { 670 projects[ProjectKey(keyOrName)] = p 671 } else { 672 for key, p := range ps { 673 if keyOrName == p.Name { 674 projects[key] = p 675 } 676 } 677 } 678 return projects 679 } 680 681 // FindUnique returns the project in Projects with the given key or name, and 682 // returns an error if none or multiple matching projects are found. 683 func (ps Projects) FindUnique(keyOrName string) (Project, error) { 684 var p Project 685 projects := ps.Find(keyOrName) 686 if len(projects) == 0 { 687 return p, fmt.Errorf("no projects found with key or name %q", keyOrName) 688 } 689 if len(projects) > 1 { 690 return p, fmt.Errorf("multiple projects found with name %q", keyOrName) 691 } 692 // Return the only project in projects. 693 for _, project := range projects { 694 p = project 695 } 696 return p, nil 697 } 698 699 // ScanMode determines whether LocalProjects should scan the local filesystem 700 // for projects (FullScan), or optimistically assume that the local projects 701 // will match those in the manifest (FastScan). 702 type ScanMode bool 703 704 const ( 705 FastScan = ScanMode(false) 706 FullScan = ScanMode(true) 707 ) 708 709 func (sm ScanMode) String() string { 710 if sm == FastScan { 711 return "FastScan" 712 } else { 713 return "FullScan" 714 } 715 } 716 717 // CreateSnapshot creates a manifest that encodes the current state of 718 // HEAD of all projects and writes this snapshot out to the given file. 719 // if hooks are not passed, jiri will read JiriManifestFile and get hooks from there, 720 // so always pass hooks incase updating from a snapshot 721 func CreateSnapshot(jirix *jiri.X, file string, hooks Hooks, pkgs Packages, localManifest bool) error { 722 jirix.TimerPush("create snapshot") 723 defer jirix.TimerPop() 724 725 // Create a new Manifest with a Jiri version and current attributes 726 // pinned to each snapshot 727 manifest := Manifest{ 728 Version: ManifestVersion, 729 Attributes: jirix.FetchingAttrs, 730 } 731 732 // Add all local projects to manifest. 733 localProjects, err := LocalProjects(jirix, FullScan) 734 if err != nil { 735 return err 736 } 737 738 for _, project := range localProjects { 739 manifest.Projects = append(manifest.Projects, project) 740 } 741 742 if hooks == nil || pkgs == nil { 743 if _, tmpHooks, tmpPkgs, err := LoadManifestFile(jirix, jirix.JiriManifestFile(), localProjects, localManifest); err != nil { 744 return err 745 } else { 746 if hooks == nil { 747 hooks = tmpHooks 748 } 749 if pkgs == nil { 750 pkgs = tmpPkgs 751 } 752 } 753 } 754 755 for _, hook := range hooks { 756 manifest.Hooks = append(manifest.Hooks, hook) 757 } 758 759 for _, pack := range pkgs { 760 manifest.Packages = append(manifest.Packages, pack) 761 } 762 763 return manifest.ToFile(jirix, file) 764 } 765 766 // CheckoutSnapshot updates project state to the state specified in the given 767 // snapshot file. Note that the snapshot file must not contain remote imports. 768 func CheckoutSnapshot(jirix *jiri.X, snapshot string, gc, runHooks, fetchPkgs bool, runHookTimeout, fetchTimeout uint) error { 769 jirix.UsingSnapshot = true 770 // Find all local projects. 771 scanMode := FastScan 772 if gc { 773 scanMode = FullScan 774 } 775 localProjects, err := LocalProjects(jirix, scanMode) 776 if err != nil { 777 return err 778 } 779 remoteProjects, hooks, pkgs, err := LoadSnapshotFile(jirix, snapshot) 780 if err != nil { 781 return err 782 } 783 if err := updateProjects(jirix, localProjects, remoteProjects, hooks, pkgs, gc, runHookTimeout, fetchTimeout, false /*rebaseTracked*/, false /*rebaseUntracked*/, false /*rebaseAll*/, true /*snapshot*/, runHooks, fetchPkgs); err != nil { 784 return err 785 } 786 return WriteUpdateHistorySnapshot(jirix, snapshot, hooks, pkgs, false) 787 } 788 789 // LoadSnapshotFile loads the specified snapshot manifest. If the snapshot 790 // manifest contains a remote import, an error will be returned. 791 func LoadSnapshotFile(jirix *jiri.X, snapshot string) (Projects, Hooks, Packages, error) { 792 // Snapshot files already have pinned Project revisions and Package instance IDs. 793 // They will cause conflicts with current lockfiles. Disable the lockfile for now. 794 enableLockfile := jirix.LockfileEnabled 795 jirix.LockfileEnabled = false 796 defer func() { 797 jirix.LockfileEnabled = enableLockfile 798 }() 799 if _, err := os.Stat(snapshot); err != nil { 800 if !os.IsNotExist(err) { 801 return nil, nil, nil, fmtError(err) 802 } 803 u, err := url.ParseRequestURI(snapshot) 804 if err != nil { 805 return nil, nil, nil, fmt.Errorf("%q is neither a URL nor a valid file path", snapshot) 806 } 807 jirix.Logger.Infof("Getting snapshot from URL %q", u) 808 resp, err := http.Get(u.String()) 809 if err != nil { 810 return nil, nil, nil, fmt.Errorf("Error getting snapshot from URL %q: %v", u, err) 811 } 812 defer resp.Body.Close() 813 tmpFile, err := ioutil.TempFile("", "snapshot") 814 if err != nil { 815 return nil, nil, nil, fmt.Errorf("Error creating tmp file: %v", err) 816 } 817 snapshot = tmpFile.Name() 818 defer os.Remove(snapshot) 819 if _, err = io.Copy(tmpFile, resp.Body); err != nil { 820 return nil, nil, nil, fmt.Errorf("Error writing to tmp file: %v", err) 821 } 822 823 } 824 825 m, err := ManifestFromFile(jirix, snapshot) 826 if err != nil { 827 return nil, nil, nil, err 828 } 829 if ManifestVersion != m.Version { 830 return nil, nil, nil, errVersionMismatch 831 } 832 833 return LoadManifestFile(jirix, snapshot, nil, false) 834 } 835 836 // CurrentProject gets the current project from the current directory by 837 // reading the jiri project metadata located in a directory at the root of the 838 // current repository. 839 func CurrentProject(jirix *jiri.X) (*Project, error) { 840 topLevel, err := gitutil.New(jirix).TopLevel() 841 if err != nil { 842 return nil, nil 843 } 844 metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir) 845 if _, err := os.Stat(metadataDir); err == nil { 846 project, err := ProjectFromFile(jirix, filepath.Join(metadataDir, jiri.ProjectMetaFile)) 847 if err != nil { 848 return nil, err 849 } 850 return project, nil 851 } 852 return nil, nil 853 } 854 855 // setProjectRevisions sets the current project revision for 856 // each project as found on the filesystem 857 func setProjectRevisions(jirix *jiri.X, projects Projects) (Projects, error) { 858 jirix.TimerPush("set revisions") 859 defer jirix.TimerPop() 860 for name, project := range projects { 861 scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 862 revision, err := scm.CurrentRevision() 863 if err != nil { 864 return nil, fmt.Errorf("Can't get revision for project %q: %v", project.Name, err) 865 } 866 project.Revision = revision 867 projects[name] = project 868 } 869 return projects, nil 870 } 871 872 func rewriteRemote(jirix *jiri.X, remote string) string { 873 if !jirix.RewriteSsoToHttps { 874 return remote 875 } 876 if strings.HasPrefix(remote, "sso://") { 877 return ssoRe.ReplaceAllString(remote, "https://$1.googlesource.com/") 878 } 879 return remote 880 } 881 882 // LocalProjects returns projects on the local filesystem. If all projects in 883 // the manifest exist locally and scanMode is set to FastScan, then only the 884 // projects in the manifest that exist locally will be returned. Otherwise, a 885 // full scan of the filesystem will take place, and all found projects will be 886 // returned. 887 func LocalProjects(jirix *jiri.X, scanMode ScanMode) (Projects, error) { 888 jirix.TimerPush("local projects") 889 defer jirix.TimerPop() 890 891 latestSnapshot := jirix.UpdateHistoryLatestLink() 892 latestSnapshotExists, err := isFile(latestSnapshot) 893 if err != nil { 894 return nil, err 895 } 896 if scanMode == FastScan && latestSnapshotExists { 897 // Fast path: Full scan was not requested, and we have a snapshot containing 898 // the latest update. Check that the projects listed in the snapshot exist 899 // locally. If not, then fall back on the slow path. 900 // 901 // An error will be returned if the snapshot contains remote imports, since 902 // that would cause an infinite loop; we'd need local projects, in order to 903 // load the snapshot, in order to determine the local projects. 904 snapshotProjects, _, _, err := LoadSnapshotFile(jirix, latestSnapshot) 905 if err != nil { 906 if err == errVersionMismatch { 907 return loadLocalProjectsSlow(jirix) 908 } 909 return nil, err 910 } 911 projectsExist, err := projectsExistLocally(jirix, snapshotProjects) 912 if err != nil { 913 return nil, err 914 } 915 if projectsExist { 916 for key, p := range snapshotProjects { 917 localConfigFile := filepath.Join(p.Path, jiri.ProjectMetaDir, jiri.ProjectConfigFile) 918 if p.LocalConfig, err = LocalConfigFromFile(jirix, localConfigFile); err != nil { 919 return nil, fmt.Errorf("Error while reading config for project %s(%s): %s", p.Name, p.Path, err) 920 } 921 snapshotProjects[key] = p 922 } 923 return setProjectRevisions(jirix, snapshotProjects) 924 } 925 } 926 927 return loadLocalProjectsSlow(jirix) 928 } 929 930 func loadLocalProjectsSlow(jirix *jiri.X) (Projects, error) { 931 // Slow path: Either full scan was requested, or projects exist in manifest 932 // that were not found locally. Do a recursive scan of all projects under 933 // the root. 934 projects := Projects{} 935 jirix.TimerPush("scan fs") 936 multiErr := findLocalProjects(jirix, jirix.Root, projects) 937 jirix.TimerPop() 938 if multiErr != nil { 939 return nil, multiErr 940 } 941 return setProjectRevisions(jirix, projects) 942 } 943 944 // projectsExistLocally returns true iff all the given projects exist on the 945 // local filesystem. 946 // Note that this may return true even if there are projects on the local 947 // filesystem not included in the provided projects argument. 948 func projectsExistLocally(jirix *jiri.X, projects Projects) (bool, error) { 949 jirix.TimerPush("match manifest") 950 defer jirix.TimerPop() 951 for _, p := range projects { 952 isLocal, err := IsLocalProject(jirix, p.Path) 953 if err != nil { 954 return false, err 955 } 956 if !isLocal { 957 return false, nil 958 } 959 } 960 return true, nil 961 } 962 963 func MatchLocalWithRemote(localProjects, remoteProjects Projects) { 964 localKeysNotInRemote := make(map[ProjectKey]bool) 965 for key, _ := range localProjects { 966 if _, ok := remoteProjects[key]; !ok { 967 localKeysNotInRemote[key] = true 968 } 969 } 970 // no stray local projects 971 if len(localKeysNotInRemote) == 0 { 972 return 973 } 974 975 for remoteKey, remoteProject := range remoteProjects { 976 if _, ok := localProjects[remoteKey]; !ok { 977 for localKey, _ := range localKeysNotInRemote { 978 localProject := localProjects[localKey] 979 if localProject.Path == remoteProject.Path && (localProject.Name == remoteProject.Name || localProject.Remote == remoteProject.Remote) { 980 delete(localProjects, localKey) 981 delete(localKeysNotInRemote, localKey) 982 // Change local project key 983 localProject.ComputedKey = remoteKey 984 localProjects[remoteKey] = localProject 985 // no more stray local projects 986 if len(localKeysNotInRemote) == 0 { 987 return 988 } 989 break 990 } 991 } 992 } 993 } 994 } 995 996 func loadManifestFiles(jirix *jiri.X, manifestFiles []string, localManifest bool) (Projects, Packages, error) { 997 localProjects, err := LocalProjects(jirix, FastScan) 998 if err != nil { 999 return nil, nil, err 1000 } 1001 jirix.Logger.Debugf("Print local projects: ") 1002 for _, v := range localProjects { 1003 jirix.Logger.Debugf("entry: %+v", v) 1004 } 1005 jirix.Logger.Debugf("Print local projects ends") 1006 allProjects := make(Projects) 1007 allPkgs := make(Packages) 1008 1009 addProject := func(projects Projects) error { 1010 for _, project := range projects { 1011 if existingProject, ok := allProjects[project.Key()]; ok { 1012 if !reflect.DeepEqual(existingProject, project) { 1013 return fmt.Errorf("project: %v conflicts with project: %v", existingProject, project) 1014 } 1015 continue 1016 } else { 1017 allProjects[project.Key()] = project 1018 } 1019 } 1020 return nil 1021 } 1022 1023 addPkg := func(pkgs Packages) error { 1024 for _, pkg := range pkgs { 1025 if existingPkg, ok := allPkgs[pkg.Key()]; ok { 1026 if !reflect.DeepEqual(existingPkg, pkg) { 1027 return fmt.Errorf("package: %v conflicts with package: %v", existingPkg, pkg) 1028 } 1029 continue 1030 } else { 1031 allPkgs[pkg.Key()] = pkg 1032 } 1033 } 1034 return nil 1035 } 1036 1037 for _, manifestFile := range manifestFiles { 1038 remoteProjects, _, pkgs, err := LoadManifestFile(jirix, manifestFile, localProjects, localManifest) 1039 if err != nil { 1040 return nil, nil, err 1041 } 1042 if err := addProject(remoteProjects); err != nil { 1043 return nil, nil, err 1044 } 1045 if err := addPkg(pkgs); err != nil { 1046 return nil, nil, err 1047 } 1048 } 1049 1050 return allProjects, allPkgs, nil 1051 } 1052 1053 func writeLockFile(jirix *jiri.X, lockfilePath string, projectLocks ProjectLocks, pkgLocks PackageLocks) error { 1054 data, err := MarshalLockEntries(projectLocks, pkgLocks) 1055 if err != nil { 1056 return err 1057 } 1058 jirix.Logger.Debugf("Generated jiri lockfile content: \n%v", string(data)) 1059 1060 tempFile, err := ioutil.TempFile(path.Dir(lockfilePath), "jirilock.*") 1061 if err != nil { 1062 return err 1063 } 1064 defer tempFile.Close() 1065 defer os.Remove(tempFile.Name()) 1066 if _, err := tempFile.Write(data); err != nil { 1067 return errors.New("I/O error while writing jiri lockfile") 1068 } 1069 tempFile.Close() 1070 if err := os.Rename(tempFile.Name(), lockfilePath); err != nil { 1071 return err 1072 } 1073 1074 return nil 1075 } 1076 1077 // HostnameAllowed determines if hostname is allowed under reference. 1078 // This function allows a single prefix '*' for wildcard matching E.g. 1079 // "*.google.com" will match "fuchsia.google.com" but does not match 1080 // "google.com". 1081 func HostnameAllowed(reference, hostname string) bool { 1082 if strings.Count(reference, "*") > 1 || (strings.Count(reference, "*") == 1 && reference[0] != '*') { 1083 return false 1084 } 1085 if !strings.HasPrefix(reference, "*") { 1086 return reference == hostname 1087 } 1088 reference = reference[1:] 1089 i := len(reference) - 1 1090 j := len(hostname) - 1 1091 for i >= 0 && j >= 0 { 1092 if hostname[j] != reference[i] { 1093 return false 1094 } 1095 i-- 1096 j-- 1097 } 1098 if i >= 0 { 1099 return false 1100 } 1101 return true 1102 } 1103 1104 // CheckProjectsHostnames checks if the hostname of every project is allowed 1105 // under allowList. If allowList is empty, the check is skipped. 1106 func CheckProjectsHostnames(projects Projects, allowList []string) error { 1107 if len(allowList) > 0 { 1108 for _, item := range allowList { 1109 if strings.Count(item, "*") > 1 || (strings.Count(item, "*") == 1 && item[0] != '*') { 1110 return fmt.Errorf("failed to process %q. Only a single * at the beginning of a hostname is supported", item) 1111 } 1112 } 1113 for _, proj := range projects { 1114 projURL, err := url.Parse(proj.Remote) 1115 if err != nil { 1116 return fmt.Errorf("URL of project %q cannot be parsed due to error: %v", proj.Name, err) 1117 } 1118 remoteHost := projURL.Hostname() 1119 allowed := false 1120 for _, item := range allowList { 1121 if HostnameAllowed(item, remoteHost) { 1122 allowed = true 1123 break 1124 } 1125 } 1126 if !allowed { 1127 err := fmt.Errorf("hostname: %s in project %s is not allowed", remoteHost, proj.Name) 1128 return err 1129 } 1130 } 1131 } 1132 return nil 1133 } 1134 1135 // GenerateJiriLockFile generates jiri lockfile to lockFilePath using 1136 // manifests in manifestFiles slice. 1137 func GenerateJiriLockFile(jirix *jiri.X, manifestFiles []string, resolveConfig ResolveConfig) error { 1138 jirix.Logger.Debugf("Generate jiri lockfile for manifests %v to %q", manifestFiles, resolveConfig.LockFilePath()) 1139 1140 resolveLocks := func(jirix *jiri.X, manifestFiles []string, localManifest bool) (projectLocks ProjectLocks, pkgLocks PackageLocks, err error) { 1141 projects, pkgs, err := loadManifestFiles(jirix, manifestFiles, localManifest) 1142 if err != nil { 1143 return nil, nil, err 1144 } 1145 // Check hostnames of projects. 1146 if err := CheckProjectsHostnames(projects, resolveConfig.HostnameAllowList()); err != nil { 1147 return nil, nil, err 1148 } 1149 if resolveConfig.EnableProjectLock() { 1150 projectLocks, err = resolveProjectLocks(jirix, projects) 1151 if err != nil { 1152 return 1153 } 1154 } 1155 if resolveConfig.EnablePackageLock() { 1156 if !resolveConfig.AllowFloatingRefs() { 1157 pkgsForRefCheck := make(map[cipd.PackageInstance]bool) 1158 pkgsPlatformMap := make(map[cipd.PackageInstance][]cipd.Platform) 1159 for _, v := range pkgs { 1160 pkgInstance := cipd.PackageInstance{ 1161 PackageName: v.Name, 1162 VersionTag: v.Version, 1163 } 1164 pkgsForRefCheck[pkgInstance] = false 1165 plats, err := v.GetPlatforms() 1166 if err != nil { 1167 return nil, nil, err 1168 } 1169 pkgsPlatformMap[pkgInstance] = plats 1170 } 1171 if err := cipd.CheckFloatingRefs(jirix, pkgsForRefCheck, pkgsPlatformMap); err != nil { 1172 return nil, nil, err 1173 } 1174 for k, v := range pkgsForRefCheck { 1175 var errBuf bytes.Buffer 1176 if v { 1177 errBuf.WriteString(fmt.Sprintf("package %q used floating ref %q, which is not allowed\n", k.PackageName, k.VersionTag)) 1178 } 1179 if errBuf.Len() != 0 { 1180 errBuf.Truncate(errBuf.Len() - 1) 1181 return nil, nil, errors.New(errBuf.String()) 1182 } 1183 } 1184 } 1185 pkgsWithMultiVersionsMap := make(map[string]map[string]bool) 1186 for _, v := range pkgs { 1187 versionMap := make(map[string]bool) 1188 if _, ok := pkgsWithMultiVersionsMap[v.Name]; ok { 1189 versionMap = pkgsWithMultiVersionsMap[v.Name] 1190 } 1191 versionMap[v.Version] = true 1192 pkgsWithMultiVersionsMap[v.Name] = versionMap 1193 } 1194 for k := range pkgsWithMultiVersionsMap { 1195 if len(pkgsWithMultiVersionsMap[k]) <= 1 { 1196 delete(pkgsWithMultiVersionsMap, k) 1197 } 1198 } 1199 pkgLocks, err = resolvePackageLocks(jirix, projects, pkgs) 1200 if err != nil { 1201 return 1202 } 1203 for _, v := range pkgs { 1204 if _, ok := pkgsWithMultiVersionsMap[v.Name]; ok { 1205 plats, err := v.GetPlatforms() 1206 if err != nil { 1207 return nil, nil, err 1208 } 1209 expandedNames, err := cipd.Expand(v.Name, plats) 1210 if err != nil { 1211 return nil, nil, err 1212 } 1213 for _, expandedName := range expandedNames { 1214 lockKey := PackageLockKey(expandedName + KeySeparator + v.Version) 1215 lockEntry, ok := pkgLocks[lockKey] 1216 if !ok { 1217 jirix.Logger.Errorf("lock key not found in pkgLocks: %v, package: %+v", lockKey, v) 1218 return nil, nil, err 1219 } 1220 lockEntry.LocalPath = v.Path 1221 pkgLocks[lockKey] = lockEntry 1222 } 1223 } 1224 } 1225 } 1226 return 1227 } 1228 1229 projectLocks, pkgLocks, err := resolveLocks(jirix, manifestFiles, resolveConfig.LocalManifest()) 1230 if err != nil { 1231 return err 1232 } 1233 1234 return writeLockFile(jirix, resolveConfig.LockFilePath(), projectLocks, pkgLocks) 1235 } 1236 1237 // UpdateUniverse updates all local projects and tools to match the remote 1238 // counterparts identified in the manifest. Optionally, the 'gc' flag can be 1239 // used to indicate that local projects that no longer exist remotely should be 1240 // removed. 1241 func UpdateUniverse(jirix *jiri.X, gc, localManifest, rebaseTracked, rebaseUntracked, rebaseAll, runHooks, fetchPkgs bool, runHookTimeout, fetchTimeout uint) (e error) { 1242 jirix.Logger.Infof("Updating all projects") 1243 1244 updateFn := func(scanMode ScanMode) error { 1245 jirix.TimerPush(fmt.Sprintf("update universe: %s", scanMode)) 1246 defer jirix.TimerPop() 1247 1248 // Find all local projects. 1249 localProjects, err := LocalProjects(jirix, scanMode) 1250 if err != nil { 1251 return err 1252 } 1253 1254 // Determine the set of remote projects and match them up with the locals. 1255 remoteProjects, hooks, pkgs, err := LoadUpdatedManifest(jirix, localProjects, localManifest) 1256 MatchLocalWithRemote(localProjects, remoteProjects) 1257 1258 if err != nil { 1259 return err 1260 } 1261 1262 // Actually update the projects. 1263 return updateProjects(jirix, localProjects, remoteProjects, hooks, pkgs, gc, runHookTimeout, fetchTimeout, rebaseTracked, rebaseUntracked, rebaseAll, false /*snapshot*/, runHooks, fetchPkgs) 1264 } 1265 1266 // Specifying gc should always force a full filesystem scan. 1267 if gc { 1268 return updateFn(FullScan) 1269 } 1270 1271 // Attempt a fast update, which uses the latest snapshot to avoid doing 1272 // a filesystem scan. Sometimes the latest snapshot can have problems, so if 1273 // any errors come up, fallback to the slow path. 1274 err := updateFn(FastScan) 1275 if err != nil { 1276 if err2 := updateFn(FullScan); err2 != nil { 1277 if err.Error() == err2.Error() { 1278 return err 1279 } 1280 return fmt.Errorf("%v, %v", err, err2) 1281 } 1282 } 1283 1284 return nil 1285 } 1286 1287 // WriteUpdateHistoryLog creates a log file of the current update process. 1288 func WriteUpdateHistoryLog(jirix *jiri.X) error { 1289 logFile := filepath.Join(jirix.UpdateHistoryLogDir(), time.Now().Format((time.RFC3339))) 1290 if err := os.MkdirAll(filepath.Dir(logFile), 0755); err != nil { 1291 return fmtError(err) 1292 } 1293 if err := jirix.Logger.WriteLogToFile(logFile); err != nil { 1294 return err 1295 } 1296 1297 latestLink, secondLatestLink := jirix.UpdateHistoryLogLatestLink(), jirix.UpdateHistoryLogSecondLatestLink() 1298 1299 // If the "latest" symlink exists, point the "second-latest" symlink to its value. 1300 latestLinkExists, err := isFile(latestLink) 1301 if err != nil { 1302 return err 1303 } 1304 if latestLinkExists { 1305 latestFile, err := os.Readlink(latestLink) 1306 if err != nil { 1307 return fmtError(err) 1308 } 1309 if err := os.RemoveAll(secondLatestLink); err != nil { 1310 return fmtError(err) 1311 } 1312 if err := os.Symlink(latestFile, secondLatestLink); err != nil { 1313 return fmtError(err) 1314 } 1315 } 1316 1317 // Point the "latest" update history symlink to the new log file. Try 1318 // to keep the symlink relative, to make it easy to move or copy the entire 1319 // update_history_log directory. 1320 if rel, err := filepath.Rel(filepath.Dir(latestLink), logFile); err == nil { 1321 logFile = rel 1322 } 1323 if err := os.RemoveAll(latestLink); err != nil { 1324 return fmtError(err) 1325 } 1326 return fmtError(os.Symlink(logFile, latestLink)) 1327 } 1328 1329 // WriteUpdateHistorySnapshot creates a snapshot of the current state of all 1330 // projects and writes it to the update history directory. 1331 func WriteUpdateHistorySnapshot(jirix *jiri.X, snapshotPath string, hooks Hooks, pkgs Packages, localManifest bool) error { 1332 snapshotFile := filepath.Join(jirix.UpdateHistoryDir(), time.Now().Format(time.RFC3339)) 1333 if err := CreateSnapshot(jirix, snapshotFile, hooks, pkgs, localManifest); err != nil { 1334 return err 1335 } 1336 1337 latestLink, secondLatestLink := jirix.UpdateHistoryLatestLink(), jirix.UpdateHistorySecondLatestLink() 1338 1339 // If the "latest" symlink exists, point the "second-latest" symlink to its value. 1340 latestLinkExists, err := isFile(latestLink) 1341 if err != nil { 1342 return err 1343 } 1344 if latestLinkExists { 1345 latestFile, err := os.Readlink(latestLink) 1346 if err != nil { 1347 return fmtError(err) 1348 } 1349 if err := os.RemoveAll(secondLatestLink); err != nil { 1350 return fmtError(err) 1351 } 1352 if err := os.Symlink(latestFile, secondLatestLink); err != nil { 1353 return fmtError(err) 1354 } 1355 } 1356 1357 // Point the "latest" update history symlink to the new snapshot file. Try 1358 // to keep the symlink relative, to make it easy to move or copy the entire 1359 // update_history directory. 1360 if rel, err := filepath.Rel(filepath.Dir(latestLink), snapshotFile); err == nil { 1361 snapshotFile = rel 1362 } 1363 if err := os.RemoveAll(latestLink); err != nil { 1364 return fmtError(err) 1365 } 1366 return fmtError(os.Symlink(snapshotFile, latestLink)) 1367 } 1368 1369 // CleanupProjects restores the given jiri projects back to their detached 1370 // heads, resets to the specified revision if there is one, and gets rid of 1371 // all the local changes. If "cleanupBranches" is true, it will also delete all 1372 // the non-master branches. 1373 func CleanupProjects(jirix *jiri.X, localProjects Projects, cleanupBranches bool) (e error) { 1374 remoteProjects, _, _, err := LoadManifest(jirix) 1375 if err != nil { 1376 return err 1377 } 1378 cleanLimit := make(chan struct{}, jirix.Jobs) 1379 errs := make(chan error, len(localProjects)) 1380 var wg sync.WaitGroup 1381 for _, local := range localProjects { 1382 wg.Add(1) 1383 cleanLimit <- struct{}{} 1384 go func(local Project) { 1385 defer func() { <-cleanLimit }() 1386 defer wg.Done() 1387 1388 if local.LocalConfig.Ignore || local.LocalConfig.NoUpdate { 1389 jirix.Logger.Warningf("Project %s(%s) won't be updated due to it's local-config\n\n", local.Name, local.Path) 1390 return 1391 } 1392 remote, ok := remoteProjects[local.Key()] 1393 if !ok { 1394 jirix.Logger.Errorf("Not cleaning project %q(%v). It was not found in manifest\n\n", local.Name, local.Path) 1395 jirix.IncrementFailures() 1396 return 1397 } 1398 if err := resetLocalProject(jirix, local, remote, cleanupBranches); err != nil { 1399 errs <- fmt.Errorf("Erorr cleaning project %q: %v", local.Name, err) 1400 } 1401 }(local) 1402 } 1403 wg.Wait() 1404 close(errs) 1405 1406 multiErr := make(MultiError, 0) 1407 for err := range errs { 1408 multiErr = append(multiErr, err) 1409 } 1410 if len(multiErr) != 0 { 1411 return multiErr 1412 } 1413 return nil 1414 } 1415 1416 // resetLocalProject checks out the detached_head, cleans up untracked files 1417 // and uncommitted changes, and optionally deletes all the branches except master. 1418 func resetLocalProject(jirix *jiri.X, local, remote Project, cleanupBranches bool) error { 1419 scm := gitutil.New(jirix, gitutil.RootDirOpt(local.Path)) 1420 headRev, err := GetHeadRevision(jirix, remote) 1421 if err != nil { 1422 return err 1423 } else { 1424 if headRev, err = scm.CurrentRevisionForRef(headRev); err != nil { 1425 return fmt.Errorf("Cannot find revision for ref %q for project %q: %v", headRev, local.Name, err) 1426 } 1427 } 1428 if local.Revision != headRev { 1429 if err := scm.CheckoutBranch(headRev, gitutil.DetachOpt(true), gitutil.ForceOpt(true)); err != nil { 1430 return err 1431 } 1432 } 1433 // Cleanup changes. 1434 if err := scm.RemoveUntrackedFiles(); err != nil { 1435 return err 1436 } 1437 if !cleanupBranches { 1438 return nil 1439 } 1440 1441 // Delete all the other branches. 1442 branches, _, err := scm.GetBranches() 1443 if err != nil { 1444 return fmt.Errorf("Cannot get branches for project %q: %v", local.Name, err) 1445 } 1446 for _, branch := range branches { 1447 if err := scm.DeleteBranch(branch, gitutil.ForceOpt(true)); err != nil { 1448 return err 1449 } 1450 } 1451 return nil 1452 } 1453 1454 // IsLocalProject returns true if there is a project at the given path. 1455 func IsLocalProject(jirix *jiri.X, path string) (bool, error) { 1456 // Existence of a metadata directory is how we know we've found a 1457 // Jiri-maintained project. 1458 metadataDir := filepath.Join(path, jiri.ProjectMetaDir) 1459 if _, err := os.Stat(metadataDir); err != nil { 1460 if os.IsNotExist(err) { 1461 // Check for old meta directory 1462 oldMetadataDir := filepath.Join(path, jiri.OldProjectMetaDir) 1463 if _, err := os.Stat(oldMetadataDir); err != nil { 1464 if os.IsNotExist(err) { 1465 return false, nil 1466 1467 } 1468 return false, fmtError(err) 1469 } 1470 // Old metadir found, move it 1471 if err := os.Rename(oldMetadataDir, metadataDir); err != nil { 1472 return false, fmtError(err) 1473 } 1474 return true, nil 1475 } else if os.IsPermission(err) { 1476 jirix.Logger.Warningf("Directory %q doesn't have read permission, skipping it\n\n", path) 1477 return false, nil 1478 } 1479 return false, fmtError(err) 1480 } 1481 return true, nil 1482 } 1483 1484 // ProjectAtPath returns a Project struct corresponding to the project at the 1485 // path in the filesystem. 1486 func ProjectAtPath(jirix *jiri.X, path string) (Project, error) { 1487 metadataFile := filepath.Join(path, jiri.ProjectMetaDir, jiri.ProjectMetaFile) 1488 project, err := ProjectFromFile(jirix, metadataFile) 1489 if err != nil { 1490 return Project{}, err 1491 } 1492 localConfigFile := filepath.Join(path, jiri.ProjectMetaDir, jiri.ProjectConfigFile) 1493 if project.LocalConfig, err = LocalConfigFromFile(jirix, localConfigFile); err != nil { 1494 return *project, fmt.Errorf("Error while reading config for project %s(%s): %s", project.Name, path, err) 1495 } 1496 return *project, nil 1497 } 1498 1499 // findLocalProjects scans the filesystem for all projects. Note that project 1500 // directories can be nested recursively. 1501 func findLocalProjects(jirix *jiri.X, path string, projects Projects) MultiError { 1502 log := make(chan string, jirix.Jobs) 1503 var wg sync.WaitGroup 1504 wg.Add(2) 1505 go func() { 1506 defer wg.Done() 1507 for str := range log { 1508 jirix.Logger.Warningf("%s", str) 1509 } 1510 }() 1511 errs := make(chan error, jirix.Jobs) 1512 var multiErr MultiError 1513 go func() { 1514 defer wg.Done() 1515 for err := range errs { 1516 multiErr = append(multiErr, err) 1517 } 1518 }() 1519 var pwg sync.WaitGroup 1520 workq := make(chan string, jirix.Jobs) 1521 projectsMutex := &sync.Mutex{} 1522 processPath := func(path string) { 1523 defer pwg.Done() 1524 isLocal, err := IsLocalProject(jirix, path) 1525 if err != nil { 1526 errs <- fmt.Errorf("Error while processing path %q: %v", path, err) 1527 return 1528 } 1529 if isLocal { 1530 project, err := ProjectAtPath(jirix, path) 1531 if err != nil { 1532 errs <- fmt.Errorf("Error while processing path %q: %v", path, err) 1533 return 1534 } 1535 if path != project.Path { 1536 logs := []string{fmt.Sprintf("Project %q has path %s, but was found in %s.", project.Name, project.Path, path), 1537 fmt.Sprintf("jiri will treat it as a stale project. To remove this warning please delete this or move it out of your root folder\n\n")} 1538 log <- strings.Join(logs, "\n") 1539 return 1540 } 1541 projectsMutex.Lock() 1542 if p, ok := projects[project.Key()]; ok { 1543 projectsMutex.Unlock() 1544 errs <- fmt.Errorf("name conflict: both %s and %s contain project with key %v", p.Path, project.Path, project.Key()) 1545 return 1546 } 1547 projects[project.Key()] = project 1548 projectsMutex.Unlock() 1549 } 1550 1551 // Recurse into all the sub directories. 1552 fileInfos, err := ioutil.ReadDir(path) 1553 if err != nil && !os.IsPermission(err) { 1554 errs <- fmt.Errorf("cannot read dir %q: %v", path, err) 1555 return 1556 } 1557 pwg.Add(1) 1558 go func(fileInfos []os.FileInfo) { 1559 defer pwg.Done() 1560 for _, fileInfo := range fileInfos { 1561 if fileInfo.IsDir() && !strings.HasPrefix(fileInfo.Name(), ".") { 1562 pwg.Add(1) 1563 workq <- filepath.Join(path, fileInfo.Name()) 1564 } 1565 } 1566 }(fileInfos) 1567 } 1568 pwg.Add(1) 1569 workq <- path 1570 for i := uint(0); i < jirix.Jobs; i++ { 1571 wg.Add(1) 1572 go func() { 1573 defer wg.Done() 1574 for path := range workq { 1575 processPath(path) 1576 } 1577 }() 1578 } 1579 pwg.Wait() 1580 close(errs) 1581 close(log) 1582 close(workq) 1583 wg.Wait() 1584 return multiErr 1585 } 1586 1587 func fetchAll(jirix *jiri.X, project Project) error { 1588 if project.Remote == "" { 1589 return fmt.Errorf("project %q does not have a remote", project.Name) 1590 } 1591 scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 1592 remote := rewriteRemote(jirix, project.Remote) 1593 r := remote 1594 cachePath, err := project.CacheDirPath(jirix) 1595 if err != nil { 1596 return err 1597 } 1598 if cachePath != "" { 1599 r = cachePath 1600 } 1601 defer func() { 1602 if err := scm.SetRemoteUrl("origin", remote); err != nil { 1603 jirix.Logger.Errorf("failed to set remote back to %v for project %+v", remote, project) 1604 } 1605 }() 1606 if err := scm.SetRemoteUrl("origin", r); err != nil { 1607 return err 1608 } 1609 if project.HistoryDepth > 0 { 1610 if err := fetch(jirix, project.Path, "origin", gitutil.PruneOpt(true), 1611 gitutil.DepthOpt(project.HistoryDepth), gitutil.UpdateShallowOpt(true)); err != nil { 1612 return err 1613 } 1614 } else { 1615 if err := fetch(jirix, project.Path, "origin", gitutil.PruneOpt(true)); err != nil { 1616 return err 1617 } 1618 } 1619 return nil 1620 } 1621 1622 func GetHeadRevision(jirix *jiri.X, project Project) (string, error) { 1623 if err := project.fillDefaults(); err != nil { 1624 return "", err 1625 } 1626 // Having a specific revision trumps everything else. 1627 if project.Revision != "HEAD" { 1628 return project.Revision, nil 1629 } 1630 return "remotes/origin/" + project.RemoteBranch, nil 1631 } 1632 1633 func checkoutHeadRevision(jirix *jiri.X, project Project, forceCheckout bool) error { 1634 revision, err := GetHeadRevision(jirix, project) 1635 if err != nil { 1636 return err 1637 } 1638 git := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 1639 err = git.CheckoutBranch(revision, gitutil.DetachOpt(true), gitutil.ForceOpt(forceCheckout)) 1640 if err == nil { 1641 return nil 1642 } 1643 jirix.Logger.Debugf("Checkout %s to head revision %s failed, fallback to fetch: %v", project.Name, revision, err) 1644 if project.Revision != "" && project.Revision != "HEAD" { 1645 //might be a tag 1646 if err2 := fetch(jirix, project.Path, "origin", gitutil.FetchTagOpt(project.Revision)); err2 != nil { 1647 // error while fetching tag, return original err and debug log this err 1648 return fmt.Errorf("error while fetching tag after failed to checkout revision %s for project %s (%s): %s\ncheckout error: %v", revision, project.Name, project.Path, err2, err) 1649 } 1650 return git.CheckoutBranch(revision, gitutil.DetachOpt(true), gitutil.ForceOpt(forceCheckout)) 1651 } 1652 return err 1653 } 1654 1655 func tryRebase(jirix *jiri.X, project Project, branch string) (bool, error) { 1656 scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 1657 if err := scm.Rebase(branch); err != nil { 1658 err := scm.RebaseAbort() 1659 return false, err 1660 } 1661 return true, nil 1662 } 1663 1664 // syncProjectMaster checks out latest detached head if project is on one 1665 // else it rebases current branch onto its tracking branch 1666 func syncProjectMaster(jirix *jiri.X, project Project, state ProjectState, rebaseTracked, rebaseUntracked, rebaseAll, snapshot bool) error { 1667 cwd, err := os.Getwd() 1668 if err != nil { 1669 return fmtError(err) 1670 } 1671 relativePath, err := filepath.Rel(cwd, project.Path) 1672 if err != nil { 1673 // Just use the full path if an error occurred. 1674 relativePath = project.Path 1675 } 1676 if project.LocalConfig.Ignore || project.LocalConfig.NoUpdate { 1677 jirix.Logger.Warningf("Project %s(%s) won't be updated due to it's local-config\n\n", project.Name, relativePath) 1678 return nil 1679 } 1680 1681 scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 1682 1683 if diff, err := scm.FilesWithUncommittedChanges(); err != nil { 1684 return fmt.Errorf("Cannot get uncommited changes for project %q: %s", project.Name, err) 1685 } else if len(diff) != 0 { 1686 msg := fmt.Sprintf("Project %s(%s) contains uncommited changes:", project.Name, relativePath) 1687 if jirix.Logger.LoggerLevel >= log.DebugLevel { 1688 for _, item := range diff { 1689 msg += "\n" + item 1690 } 1691 } 1692 msg += fmt.Sprintf("\nCommit or discard the changes and try again.\n\n") 1693 jirix.Logger.Errorf(msg) 1694 jirix.IncrementFailures() 1695 return nil 1696 } 1697 1698 if state.CurrentBranch.Name == "" || snapshot { // detached head 1699 if err := checkoutHeadRevision(jirix, project, false); err != nil { 1700 revision, err2 := GetHeadRevision(jirix, project) 1701 if err2 != nil { 1702 return err2 1703 } 1704 gitCommand := jirix.Color.Yellow("git -C %q checkout --detach %s", relativePath, revision) 1705 msg := fmt.Sprintf("For project %q, not able to checkout latest, error: %s", project.Name, err) 1706 msg += fmt.Sprintf("\nPlease checkout manually use: '%s'\n\n", gitCommand) 1707 jirix.Logger.Errorf(msg) 1708 jirix.IncrementFailures() 1709 } 1710 if snapshot || !rebaseAll { 1711 return nil 1712 } 1713 // This should run after program exit so that detached head can be restored 1714 defer func() { 1715 if err := checkoutHeadRevision(jirix, project, false); err != nil { 1716 // This should not happen, panic 1717 panic(fmt.Sprintf("for project %s(%s), not able to checkout head revision: %s", project.Name, relativePath, err)) 1718 } 1719 }() 1720 } else if rebaseAll { 1721 // This should run after program exit so that original branch can be restored 1722 defer func() { 1723 if err := scm.CheckoutBranch(state.CurrentBranch.Name); err != nil { 1724 // This should not happen, panic 1725 panic(fmt.Sprintf("for project %s(%s), not able to checkout branch %q: %s", project.Name, relativePath, state.CurrentBranch.Name, err)) 1726 } 1727 }() 1728 } 1729 1730 // if rebase flag is false, merge fast forward current branch 1731 if !rebaseTracked && !rebaseAll && state.CurrentBranch.Tracking != nil { 1732 tracking := state.CurrentBranch.Tracking 1733 if tracking.Revision == state.CurrentBranch.Revision { 1734 return nil 1735 } 1736 if project.LocalConfig.NoRebase { 1737 jirix.Logger.Warningf("For project %s(%s), not merging your local branches due to it's local-config\n\n", project.Name, relativePath) 1738 return nil 1739 } 1740 if err := scm.Merge(tracking.Name, gitutil.FfOnlyOpt(true)); err != nil { 1741 msg := fmt.Sprintf("For project %s(%s), not able to fast forward your local branch %q to %q\n\n", project.Name, relativePath, state.CurrentBranch.Name, tracking.Name) 1742 jirix.Logger.Errorf(msg) 1743 jirix.IncrementFailures() 1744 } 1745 return nil 1746 } 1747 1748 branches := state.Branches 1749 if !rebaseAll { 1750 branches = []BranchState{state.CurrentBranch} 1751 } 1752 branchMap := make(map[string]BranchState) 1753 for _, branch := range branches { 1754 branchMap[branch.Name] = branch 1755 } 1756 rebaseUntrackedMessage := false 1757 headRevision, err := GetHeadRevision(jirix, project) 1758 if err != nil { 1759 return err 1760 } 1761 branchesContainingHead, err := scm.ListBranchesContainingRef(headRevision) 1762 if err != nil { 1763 return err 1764 } 1765 for _, branch := range branches { 1766 tracking := branch.Tracking 1767 circularDependencyMap := make(map[string]bool) 1768 circularDependencyMap[branch.Name] = true 1769 rebase := true 1770 if tracking != nil { 1771 circularDependencyMap[tracking.Name] = true 1772 _, ok := branchMap[tracking.Name] 1773 for ok { 1774 t := branchMap[tracking.Name].Tracking 1775 if t == nil { 1776 break 1777 } 1778 if circularDependencyMap[t.Name] { 1779 rebase = false 1780 msg := fmt.Sprintf("For project %s(%s), branch %q has circular dependency, not rebasing it.\n\n", project.Name, relativePath, branch.Name) 1781 jirix.Logger.Errorf(msg) 1782 jirix.IncrementFailures() 1783 break 1784 } 1785 circularDependencyMap[t.Name] = true 1786 tracking = t 1787 _, ok = branchMap[tracking.Name] 1788 } 1789 } 1790 if !rebase { 1791 continue 1792 } 1793 if tracking != nil { // tracked branch 1794 if branch.Revision == tracking.Revision { 1795 continue 1796 } 1797 if project.LocalConfig.NoRebase { 1798 jirix.Logger.Warningf("For project %s(%s), not rebasing your local branches due to it's local-config\n\n", project.Name, relativePath) 1799 break 1800 } 1801 1802 if err := scm.CheckoutBranch(branch.Name); err != nil { 1803 msg := fmt.Sprintf("For project %s(%s), not able to rebase your local branch %q onto %q", project.Name, relativePath, branch.Name, tracking.Name) 1804 msg += "\nPlease do it manually\n\n" 1805 jirix.Logger.Errorf(msg) 1806 jirix.IncrementFailures() 1807 continue 1808 } 1809 rebaseSuccess, err := tryRebase(jirix, project, tracking.Name) 1810 if err != nil { 1811 return err 1812 } 1813 if rebaseSuccess { 1814 jirix.Logger.Debugf("For project %q, rebased your local branch %q on %q", project.Name, branch.Name, tracking.Name) 1815 } else { 1816 msg := fmt.Sprintf("For project %s(%s), not able to rebase your local branch %q onto %q", project.Name, relativePath, branch.Name, tracking.Name) 1817 msg += "\nPlease do it manually\n\n" 1818 jirix.Logger.Errorf(msg) 1819 jirix.IncrementFailures() 1820 continue 1821 } 1822 } else { 1823 if branchesContainingHead[branch.Name] { 1824 continue 1825 } 1826 if rebaseUntracked { 1827 if project.LocalConfig.NoRebase { 1828 jirix.Logger.Warningf("For project %s(%s), not rebasing your local branches due to it's local-config\n\n", project.Name, relativePath) 1829 break 1830 } 1831 1832 if err := scm.CheckoutBranch(branch.Name); err != nil { 1833 msg := fmt.Sprintf("For project %s(%s), not able to rebase your untracked branch %q onto JIRI_HEAD.", project.Name, relativePath, branch.Name) 1834 msg += "\nPlease do it manually\n\n" 1835 jirix.Logger.Errorf(msg) 1836 jirix.IncrementFailures() 1837 continue 1838 } 1839 rebaseSuccess, err := tryRebase(jirix, project, headRevision) 1840 if err != nil { 1841 return err 1842 } 1843 if rebaseSuccess { 1844 jirix.Logger.Debugf("For project %q, rebased your untracked branch %q on %q", project.Name, branch.Name, headRevision) 1845 } else { 1846 msg := fmt.Sprintf("For project %s(%s), not able to rebase your untracked branch %q onto JIRI_HEAD.", project.Name, relativePath, branch.Name) 1847 msg += "\nPlease do it manually\n\n" 1848 jirix.Logger.Errorf(msg) 1849 jirix.IncrementFailures() 1850 continue 1851 } 1852 } else if !rebaseUntrackedMessage { 1853 // Post this message only once 1854 rebaseUntrackedMessage = true 1855 gitCommand := jirix.Color.Yellow("git -C %q checkout %s && git -C %q rebase %s", relativePath, branch.Name, relativePath, headRevision) 1856 msg := fmt.Sprintf("For Project %q, branch %q does not track any remote branch.", project.Name, branch.Name) 1857 msg += fmt.Sprintf("\nTo rebase it update with -rebase-untracked flag, or to rebase it manually run") 1858 msg += fmt.Sprintf("\n%s\n\n", gitCommand) 1859 jirix.Logger.Warningf(msg) 1860 continue 1861 } 1862 } 1863 } 1864 return nil 1865 } 1866 1867 // setRemoteHeadRevisions set the repo statuses from remote for 1868 // projects at HEAD so we can detect when a local project is already 1869 // up-to-date. 1870 func setRemoteHeadRevisions(jirix *jiri.X, remoteProjects Projects, localProjects Projects) MultiError { 1871 jirix.TimerPush("Set Remote Revisions") 1872 defer jirix.TimerPop() 1873 1874 keys := make(chan ProjectKey, len(remoteProjects)) 1875 updatedRemotes := make(chan Project, len(remoteProjects)) 1876 errs := make(chan error, len(remoteProjects)) 1877 var wg sync.WaitGroup 1878 1879 for i := uint(0); i < jirix.Jobs; i++ { 1880 wg.Add(1) 1881 go func() { 1882 defer wg.Done() 1883 for key := range keys { 1884 local := localProjects[key] 1885 remote := remoteProjects[key] 1886 scm := gitutil.New(jirix, gitutil.RootDirOpt(local.Path)) 1887 b := "master" 1888 if remote.RemoteBranch != "" { 1889 b = remote.RemoteBranch 1890 } 1891 rev, err := scm.CurrentRevisionForRef("remotes/origin/" + b) 1892 if err != nil { 1893 errs <- err 1894 return 1895 } 1896 remote.Revision = rev 1897 updatedRemotes <- remote 1898 } 1899 }() 1900 } 1901 1902 for key, local := range localProjects { 1903 remote, ok := remoteProjects[key] 1904 // Don't update when project has pinned revision or it's remote has changed 1905 if !ok || remote.Revision != "HEAD" || local.Remote != remote.Remote { 1906 continue 1907 } 1908 keys <- key 1909 } 1910 1911 close(keys) 1912 wg.Wait() 1913 close(updatedRemotes) 1914 close(errs) 1915 1916 for remote := range updatedRemotes { 1917 remoteProjects[remote.Key()] = remote 1918 } 1919 1920 var multiErr MultiError 1921 for err := range errs { 1922 multiErr = append(multiErr, err) 1923 } 1924 1925 return multiErr 1926 } 1927 1928 func updateOrCreateCache(jirix *jiri.X, dir, remote, branch, revision string, depth int) error { 1929 refspec := "+refs/heads/*:refs/heads/*" 1930 if depth > 0 { 1931 // Shallow cache, fetch only manifest tracked remote branch 1932 refspec = fmt.Sprintf("+refs/heads/%s:refs/heads/%s", branch, branch) 1933 } 1934 errCacheCorruption := errors.New("git cache corrupted") 1935 updateCache := func() error { 1936 // Test if git cache is intact 1937 var objectsDir string 1938 if jirix.Partial { 1939 // Partial clones do not use --bare so objects is in .git/ 1940 objectsDir = filepath.Join(dir, ".git", "objects") 1941 } else { 1942 objectsDir = filepath.Join(dir, "objects") 1943 } 1944 if _, err := os.Stat(objectsDir); err != nil { 1945 jirix.Logger.Warningf("could not access objects directory under git cache directory %q due to error: %v", dir, err) 1946 return errCacheCorruption 1947 } 1948 scm := gitutil.New(jirix, gitutil.RootDirOpt(dir)) 1949 if err := scm.Config("--remove-section", "remote.origin"); err != nil { 1950 jirix.Logger.Warningf("purge git config failed under git cache directory %q due to error: %v", dir, err) 1951 return errCacheCorruption 1952 } 1953 if err := scm.Config("remote.origin.url", remote); err != nil { 1954 jirix.Logger.Warningf("set remote.origin.url failed under git cache directory %q due to error: %v", dir, err) 1955 return errCacheCorruption 1956 } 1957 if err := scm.Config("--replace-all", "remote.origin.fetch", refspec); err != nil { 1958 jirix.Logger.Warningf("set remote.origin.fetch failed under git cache directory %q due to error: %v", dir, err) 1959 return errCacheCorruption 1960 } 1961 // Cache already present, update it 1962 // TODO : update this after implementing FetchAll using g 1963 if scm.IsRevAvailable(revision) { 1964 jirix.Logger.Infof("%s(%s) cache up-to-date; skipping\n", remote, dir) 1965 return nil 1966 } 1967 msg := fmt.Sprintf("Updating cache: %q", dir) 1968 task := jirix.Logger.AddTaskMsg(msg) 1969 defer task.Done() 1970 t := jirix.Logger.TrackTime(msg) 1971 defer t.Done() 1972 // We need to explicitly specify the ref for fetch to update in case 1973 // the cache was created with a previous version and uses "refs/*" 1974 if err := retry.Function(jirix, func() error { 1975 git := gitutil.New(jirix, gitutil.RootDirOpt(dir)) 1976 if err := git.FetchRefspec("origin", refspec, 1977 gitutil.DepthOpt(depth), gitutil.PruneOpt(true), gitutil.UpdateShallowOpt(true)); err != nil { 1978 return err 1979 } 1980 if jirix.Partial { 1981 if err := git.CheckoutBranch(revision, gitutil.DetachOpt(true), gitutil.ForceOpt(true)); err != nil { 1982 return err 1983 } 1984 } 1985 return nil 1986 }, fmt.Sprintf("Fetching for %s:%s", dir, refspec), 1987 retry.AttemptsOpt(jirix.Attempts)); err != nil { 1988 return err 1989 } 1990 return nil 1991 } 1992 1993 createCacheThroughBundle := func() error { 1994 bundlePath, err := gerrit.FetchCloneBundle(jirix, remote, dir) 1995 if err != nil { 1996 return err 1997 } 1998 // Remove clone.bundle file to save space. 1999 defer os.Remove(bundlePath) 2000 scm := gitutil.New(jirix, gitutil.RootDirOpt(dir)) 2001 if err := scm.Init(dir, gitutil.BareOpt(true)); err != nil { 2002 return err 2003 } 2004 if err := scm.Config("remote.origin.url", remote); err != nil { 2005 return err 2006 } 2007 if err := scm.Config("remote.origin.fetch", refspec); err != nil { 2008 return err 2009 } 2010 if err := scm.FetchRefspec(bundlePath, refspec, gitutil.DepthOpt(depth)); err != nil { 2011 return err 2012 } 2013 if err := scm.FetchRefspec("origin", refspec, gitutil.DepthOpt(depth)); err != nil { 2014 return err 2015 } 2016 return nil 2017 } 2018 2019 createCache := func() error { 2020 // Create cache 2021 // TODO : If we in future need to support two projects with same remote url, 2022 // one with shallow checkout and one with full, we should create two caches 2023 msg := fmt.Sprintf("Creating cache: %q", dir) 2024 task := jirix.Logger.AddTaskMsg(msg) 2025 defer task.Done() 2026 t := jirix.Logger.TrackTime(msg) 2027 defer t.Done() 2028 // Try use clone.bundle to speed up the initialization of git cache. 2029 os.MkdirAll(dir, 0755) 2030 if err := createCacheThroughBundle(); err != nil { 2031 jirix.Logger.Debugf("create git cache for %q through clone.bundle failed due to error: %v", remote, err) 2032 os.RemoveAll(dir) 2033 } else { 2034 jirix.Logger.Debugf("git cache for %q created through clone.bundle", remote) 2035 return nil 2036 } 2037 2038 opts := []gitutil.CloneOpt{gitutil.DepthOpt(depth)} 2039 if jirix.Partial { 2040 opts = append(opts, gitutil.OmitBlobsOpt(true)) 2041 } else { 2042 opts = append(opts, gitutil.BareOpt(true)) 2043 } 2044 if err := gitutil.New(jirix).Clone(remote, dir, opts...); err != nil { 2045 return err 2046 } 2047 2048 git := gitutil.New(jirix, gitutil.RootDirOpt(dir)) 2049 if jirix.Partial { 2050 if err := git.CheckoutBranch(revision, gitutil.DetachOpt(true), gitutil.ForceOpt(true)); err != nil { 2051 return err 2052 } 2053 } 2054 // We need to explicitly specify the ref for fetch to update the bare 2055 // repository. 2056 if err := git.Config("remote.origin.fetch", refspec); err != nil { 2057 return err 2058 } 2059 return nil 2060 } 2061 2062 if isPathDir(dir) { 2063 if err := updateCache(); err != nil { 2064 if err == errCacheCorruption { 2065 jirix.Logger.Warningf("Updating git cache %q failed due to cache corruption, cache will be cleared", dir) 2066 if err := os.RemoveAll(dir); err != nil { 2067 return fmt.Errorf("failed to clear cache dir %q due to error: %v", dir, err) 2068 } 2069 return createCache() 2070 } 2071 return err 2072 } 2073 return nil 2074 } 2075 2076 return createCache() 2077 } 2078 2079 // updateCache creates the cache or updates it if already present. 2080 func updateCache(jirix *jiri.X, remoteProjects Projects) error { 2081 jirix.TimerPush("update cache") 2082 defer jirix.TimerPop() 2083 if jirix.Cache == "" { 2084 return nil 2085 } 2086 2087 errs := make(chan error, len(remoteProjects)) 2088 var wg sync.WaitGroup 2089 processingPath := make(map[string]*sync.Mutex) 2090 fetchLimit := make(chan struct{}, jirix.Jobs) 2091 for _, project := range remoteProjects { 2092 if cacheDirPath, err := project.CacheDirPath(jirix); err == nil { 2093 if processingPath[cacheDirPath] == nil { 2094 processingPath[cacheDirPath] = &sync.Mutex{} 2095 } 2096 if err := project.fillDefaults(); err != nil { 2097 errs <- err 2098 continue 2099 } 2100 wg.Add(1) 2101 fetchLimit <- struct{}{} 2102 go func(dir, remote string, depth int, branch, revision string, cacheMutex *sync.Mutex) { 2103 cacheMutex.Lock() 2104 defer func() { <-fetchLimit }() 2105 defer wg.Done() 2106 defer cacheMutex.Unlock() 2107 remote = rewriteRemote(jirix, remote) 2108 if err := updateOrCreateCache(jirix, dir, remote, branch, revision, depth); err != nil { 2109 errs <- err 2110 return 2111 } 2112 }(cacheDirPath, project.Remote, project.HistoryDepth, project.RemoteBranch, project.Revision, processingPath[cacheDirPath]) 2113 } else { 2114 errs <- err 2115 } 2116 } 2117 wg.Wait() 2118 close(errs) 2119 2120 multiErr := make(MultiError, 0) 2121 for err := range errs { 2122 multiErr = append(multiErr, err) 2123 } 2124 if len(multiErr) != 0 { 2125 return multiErr 2126 } 2127 2128 return nil 2129 } 2130 2131 func fetchLocalProjects(jirix *jiri.X, localProjects, remoteProjects Projects) error { 2132 jirix.TimerPush("fetch local projects") 2133 defer jirix.TimerPop() 2134 fetchLimit := make(chan struct{}, jirix.Jobs) 2135 errs := make(chan error, len(localProjects)) 2136 var wg sync.WaitGroup 2137 for key, project := range localProjects { 2138 if r, ok := remoteProjects[key]; ok { 2139 if project.LocalConfig.Ignore || project.LocalConfig.NoUpdate { 2140 jirix.Logger.Warningf("Not updating remotes for project %s(%s) due to its local-config\n\n", project.Name, project.Path) 2141 continue 2142 } 2143 // Don't fetch when remote url has changed as that may cause fetch to fail 2144 if r.Remote != project.Remote { 2145 continue 2146 } 2147 wg.Add(1) 2148 fetchLimit <- struct{}{} 2149 project.HistoryDepth = r.HistoryDepth 2150 go func(project Project) { 2151 defer func() { <-fetchLimit }() 2152 defer wg.Done() 2153 task := jirix.Logger.AddTaskMsg("Fetching remotes for project %q", project.Name) 2154 defer task.Done() 2155 if err := fetchAll(jirix, project); err != nil { 2156 errs <- fmt.Errorf("fetch failed for %v: %v", project.Name, err) 2157 return 2158 } 2159 }(project) 2160 } 2161 } 2162 wg.Wait() 2163 close(errs) 2164 2165 multiErr := make(MultiError, 0) 2166 for err := range errs { 2167 multiErr = append(multiErr, err) 2168 } 2169 if len(multiErr) != 0 { 2170 return multiErr 2171 } 2172 return nil 2173 } 2174 2175 // FilterOptionalProjectsPackages removes projects and packages in place if the Optional field is true and 2176 // attributes in attrs does not match the Attributes field. Currently "match" means the intersection of 2177 // both attributes is not empty. 2178 func FilterOptionalProjectsPackages(jirix *jiri.X, attrs string, projects Projects, pkgs Packages) error { 2179 allowedAttrs := newAttributes(attrs) 2180 2181 for k, v := range projects { 2182 if !v.ComputedAttributes.IsEmpty() { 2183 if v.ComputedAttributes == nil { 2184 return fmt.Errorf("project %+v should have valid ComputedAttributes, but it is nil", v) 2185 } 2186 if !allowedAttrs.Match(v.ComputedAttributes) { 2187 jirix.Logger.Debugf("project %q is filtered (%s:%s)", v.Name, v.ComputedAttributes, allowedAttrs) 2188 delete(projects, k) 2189 } 2190 } 2191 } 2192 2193 for k, v := range pkgs { 2194 if !v.ComputedAttributes.IsEmpty() { 2195 if v.ComputedAttributes == nil { 2196 return fmt.Errorf("package %+v should have valid ComputedAttributes, but it is nil", v) 2197 } 2198 if !allowedAttrs.Match(v.ComputedAttributes) { 2199 jirix.Logger.Debugf("package %q is filtered (%s:%s)", v.Name, v.ComputedAttributes, allowedAttrs) 2200 delete(pkgs, k) 2201 } 2202 } 2203 } 2204 return nil 2205 } 2206 2207 func updateProjects(jirix *jiri.X, localProjects, remoteProjects Projects, hooks Hooks, pkgs Packages, gc bool, runHookTimeout, fetchTimeout uint, rebaseTracked, rebaseUntracked, rebaseAll, snapshot, shouldRunHooks, shouldFetchPkgs bool) error { 2208 jirix.TimerPush("update projects") 2209 defer jirix.TimerPop() 2210 2211 packageFetched := false 2212 hookRun := false 2213 defer func() { 2214 if shouldFetchPkgs && !packageFetched { 2215 jirix.Logger.Infof("Jiri packages are not fetched due to fatal errors when updating projects.") 2216 } 2217 if shouldRunHooks && !hookRun { 2218 jirix.Logger.Infof("Jiri hooks are not run due to fatal errors when updating projects or packages") 2219 } 2220 }() 2221 2222 // filter optional projects 2223 if err := FilterOptionalProjectsPackages(jirix, jirix.FetchingAttrs, remoteProjects, pkgs); err != nil { 2224 return err 2225 } 2226 2227 if err := updateCache(jirix, remoteProjects); err != nil { 2228 return err 2229 } 2230 if err := fetchLocalProjects(jirix, localProjects, remoteProjects); err != nil { 2231 return err 2232 } 2233 states, err := GetProjectStates(jirix, localProjects, false) 2234 if err != nil { 2235 return err 2236 } 2237 if err := setRemoteHeadRevisions(jirix, remoteProjects, localProjects); err != nil { 2238 return err 2239 } 2240 2241 ops := computeOperations(localProjects, remoteProjects, states, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot) 2242 moveOperations := []moveOperation{} 2243 changeRemoteOperations := operations{} 2244 deleteOperations := []deleteOperation{} 2245 updateOperations := operations{} 2246 createOperations := []createOperation{} 2247 nullOperations := operations{} 2248 updates := newFsUpdates() 2249 for _, op := range ops { 2250 if err := op.Test(jirix, updates); err != nil { 2251 return err 2252 } 2253 switch o := op.(type) { 2254 case deleteOperation: 2255 deleteOperations = append(deleteOperations, o) 2256 case changeRemoteOperation: 2257 changeRemoteOperations = append(changeRemoteOperations, o) 2258 case moveOperation: 2259 moveOperations = append(moveOperations, o) 2260 case updateOperation: 2261 updateOperations = append(updateOperations, o) 2262 case createOperation: 2263 createOperations = append(createOperations, o) 2264 case nullOperation: 2265 nullOperations = append(nullOperations, o) 2266 } 2267 } 2268 if err := runDeleteOperations(jirix, deleteOperations, gc); err != nil { 2269 return err 2270 } 2271 if err := runCommonOperations(jirix, changeRemoteOperations, log.DebugLevel); err != nil { 2272 return err 2273 } 2274 if err := runMoveOperations(jirix, moveOperations); err != nil { 2275 return err 2276 } 2277 if err := runCommonOperations(jirix, updateOperations, log.DebugLevel); err != nil { 2278 return err 2279 } 2280 if err := runCreateOperations(jirix, createOperations); err != nil { 2281 return err 2282 } 2283 if err := runCommonOperations(jirix, nullOperations, log.TraceLevel); err != nil { 2284 return err 2285 } 2286 jirix.TimerPush("jiri revision files") 2287 for _, project := range remoteProjects { 2288 if !(project.LocalConfig.Ignore || project.LocalConfig.NoUpdate) { 2289 project.writeJiriRevisionFiles(jirix) 2290 if err := project.setupDefaultPushTarget(jirix); err != nil { 2291 jirix.Logger.Debugf("set up default push target failed due to error: %v", err) 2292 } 2293 } 2294 } 2295 jirix.TimerPop() 2296 2297 jirix.TimerPush("jiri project flag files") 2298 if err := WriteProjectFlags(jirix, remoteProjects); err != nil { 2299 jirix.Logger.Errorf("failures in write jiri project flag files: %v", err) 2300 } 2301 jirix.TimerPop() 2302 2303 if projectStatuses, err := getProjectStatus(jirix, remoteProjects); err != nil { 2304 return fmt.Errorf("Error getting project status: %s", err) 2305 } else if len(projectStatuses) != 0 { 2306 cwd, err := os.Getwd() 2307 if err != nil { 2308 return fmtError(err) 2309 } 2310 msg := "Projects with local changes and/or not on JIRI_HEAD:" 2311 for _, p := range projectStatuses { 2312 relativePath, err := filepath.Rel(cwd, p.Project.Path) 2313 if err != nil { 2314 // Just use the full path if an error occurred. 2315 relativePath = p.Project.Path 2316 } 2317 msg = fmt.Sprintf("%s\n%s (%s):", msg, p.Project.Name, relativePath) 2318 if p.HasChanges { 2319 if jirix.Logger.LoggerLevel >= log.DebugLevel { 2320 msg = fmt.Sprintf("%s (%s: %s)", msg, jirix.Color.Yellow("Has changes"), p.Changes) 2321 } else { 2322 msg = fmt.Sprintf("%s (%s)", msg, jirix.Color.Yellow("Has changes")) 2323 } 2324 } 2325 if !p.IsOnJiriHead { 2326 msg = fmt.Sprintf("%s (%s)", msg, jirix.Color.Yellow("Not on JIRI_HEAD")) 2327 } 2328 } 2329 jirix.Logger.Warningf("%s\n\n", msg) 2330 } 2331 2332 if shouldFetchPkgs { 2333 packageFetched = true 2334 if len(pkgs) > 0 { 2335 if err := FetchPackages(jirix, remoteProjects, pkgs, fetchTimeout); err != nil { 2336 return err 2337 } 2338 } 2339 } 2340 2341 if shouldRunHooks { 2342 hookRun = true 2343 if err := RunHooks(jirix, hooks, runHookTimeout); err != nil { 2344 return err 2345 } 2346 } 2347 2348 if !jirix.KeepGitHooks { 2349 return applyGitHooks(jirix, ops) 2350 } 2351 jirix.Logger.Warningf("Git hooks are not updated. If you would like to update git hooks for all projects, please run 'jiri init -keep-git-hooks=false'.") 2352 return nil 2353 } 2354 2355 type ProjectStatus struct { 2356 Project Project 2357 HasChanges bool 2358 IsOnJiriHead bool 2359 Changes string 2360 } 2361 2362 func getProjectStatus(jirix *jiri.X, ps Projects) ([]ProjectStatus, MultiError) { 2363 jirix.TimerPush("jiri status") 2364 defer jirix.TimerPop() 2365 workQueue := make(chan Project, len(ps)) 2366 projectStatuses := make(chan ProjectStatus, len(ps)) 2367 errs := make(chan error, len(ps)) 2368 var wg sync.WaitGroup 2369 for _, project := range ps { 2370 workQueue <- project 2371 } 2372 close(workQueue) 2373 for i := uint(0); i < jirix.Jobs; i++ { 2374 wg.Add(1) 2375 go func() { 2376 defer wg.Done() 2377 for project := range workQueue { 2378 if project.LocalConfig.Ignore || project.LocalConfig.NoUpdate { 2379 continue 2380 } 2381 scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 2382 diff, err := scm.FilesWithUncommittedChanges() 2383 if err != nil { 2384 errs <- fmt.Errorf("Cannot get uncommited changes for project %q: %s", project.Name, err) 2385 continue 2386 } 2387 uncommitted := false 2388 var changes bytes.Buffer 2389 if len(diff) != 0 { 2390 uncommitted = true 2391 for _, item := range diff { 2392 changes.WriteString(item + "\n") 2393 } 2394 changes.Truncate(changes.Len() - 1) 2395 } 2396 2397 isOnJiriHead, err := project.IsOnJiriHead(jirix) 2398 if err != nil { 2399 errs <- err 2400 continue 2401 } 2402 if uncommitted || !isOnJiriHead { 2403 projectStatuses <- ProjectStatus{project, uncommitted, isOnJiriHead, changes.String()} 2404 } 2405 } 2406 }() 2407 } 2408 wg.Wait() 2409 close(projectStatuses) 2410 close(errs) 2411 2412 var multiErr MultiError 2413 for err := range errs { 2414 multiErr = append(multiErr, err) 2415 } 2416 var psa []ProjectStatus 2417 for projectStatus := range projectStatuses { 2418 psa = append(psa, projectStatus) 2419 } 2420 return psa, multiErr 2421 } 2422 2423 // writeMetadata stores the given project metadata in the directory 2424 // identified by the given path. 2425 func writeMetadata(jirix *jiri.X, project Project, dir string) (e error) { 2426 metadataDir := filepath.Join(dir, jiri.ProjectMetaDir) 2427 if err := os.MkdirAll(metadataDir, os.FileMode(0755)); err != nil { 2428 return fmtError(err) 2429 } 2430 metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile) 2431 return project.ToFile(jirix, metadataFile) 2432 }