github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/project/operations.go (about) 1 // Copyright 2017 The Fuchsia 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 "fmt" 9 "io" 10 "os" 11 "path" 12 "path/filepath" 13 "sort" 14 "strings" 15 "sync" 16 17 "github.com/btwiuse/jiri" 18 "github.com/btwiuse/jiri/gitutil" 19 "github.com/btwiuse/jiri/log" 20 "github.com/btwiuse/jiri/osutil" 21 ) 22 23 // fsUpdates is used to track filesystem updates made by operations. 24 // TODO(nlacasse): Currently we only use fsUpdates to track deletions so that 25 // jiri can delete and create a project in the same directory in one update. 26 // There are lots of other cases that should be covered though, like detecting 27 // when two projects would be created in the same directory. 28 type fsUpdates struct { 29 deletedDirs map[string]bool 30 } 31 32 func newFsUpdates() *fsUpdates { 33 return &fsUpdates{ 34 deletedDirs: map[string]bool{}, 35 } 36 } 37 38 func (u *fsUpdates) deleteDir(dir string) { 39 dir = filepath.Clean(dir) 40 u.deletedDirs[dir] = true 41 } 42 43 func (u *fsUpdates) isDeleted(dir string) bool { 44 _, ok := u.deletedDirs[filepath.Clean(dir)] 45 return ok 46 } 47 48 type operation interface { 49 // Project identifies the project this operation pertains to. 50 Project() Project 51 // Kind returns the kind of operation. 52 Kind() string 53 // Run executes the operation. 54 Run(jirix *jiri.X) error 55 // String returns a string representation of the operation. 56 String() string 57 // Test checks whether the operation would fail. 58 Test(jirix *jiri.X, updates *fsUpdates) error 59 } 60 61 // commonOperation represents a project operation. 62 type commonOperation struct { 63 // project holds information about the project such as its 64 // name, local path, and the protocol it uses for version 65 // control. 66 project Project 67 // destination is the new project path. 68 destination string 69 // source is the current project path. 70 source string 71 // state is the state of the local project 72 state ProjectState 73 } 74 75 func (op commonOperation) Project() Project { 76 return op.project 77 } 78 79 // createOperation represents the creation of a project. 80 type createOperation struct { 81 commonOperation 82 } 83 84 func (op createOperation) Kind() string { 85 return "create" 86 } 87 88 func (op createOperation) checkoutProject(jirix *jiri.X, cache string) error { 89 var err error 90 remote := rewriteRemote(jirix, op.project.Remote) 91 // Hack to make fuchsia.git happen 92 if op.destination == jirix.Root { 93 scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path)) 94 if err = scm.Init(op.destination); err != nil { 95 return err 96 } 97 if err = scm.AddOrReplaceRemote("origin", remote); err != nil { 98 return err 99 } 100 // We must specify a refspec here in order for patch to be able to set 101 // upstream to 'origin/master'. 102 if op.project.HistoryDepth > 0 && cache != "" { 103 if err = scm.FetchRefspec(cache, "+refs/heads/*:refs/remotes/origin/*", gitutil.DepthOpt(op.project.HistoryDepth)); err != nil { 104 return err 105 } 106 } else if cache != "" { 107 if err = scm.FetchRefspec(cache, "+refs/heads/*:refs/remotes/origin/*"); err != nil { 108 return err 109 } 110 } else { 111 if err = scm.FetchRefspec(remote, "+refs/heads/*:refs/remotes/origin/*"); err != nil { 112 return err 113 } 114 } 115 } else { 116 opts := []gitutil.CloneOpt{gitutil.NoCheckoutOpt(true)} 117 if op.project.HistoryDepth > 0 { 118 opts = append(opts, gitutil.DepthOpt(op.project.HistoryDepth)) 119 } else { 120 // Shallow clones can not be used as as local git reference 121 opts = append(opts, gitutil.ReferenceOpt(cache)) 122 } 123 if jirix.Partial { 124 opts = append(opts, gitutil.OmitBlobsOpt(true)) 125 } 126 if cache != "" { 127 if err = clone(jirix, cache, op.destination, opts...); err != nil { 128 return err 129 } 130 scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path)) 131 if err = scm.AddOrReplaceRemote("origin", remote); err != nil { 132 return err 133 } 134 } else { 135 if err = clone(jirix, remote, op.destination, opts...); err != nil { 136 return err 137 } 138 } 139 } 140 141 if err := os.Chmod(op.destination, os.FileMode(0755)); err != nil { 142 return fmtError(err) 143 } 144 145 if err := checkoutHeadRevision(jirix, op.project, false); err != nil { 146 return err 147 } 148 149 if err := writeMetadata(jirix, op.project, op.project.Path); err != nil { 150 return err 151 } 152 scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path)) 153 154 // Reset remote to point to correct location so that shared cache does not cause problem. 155 if err := scm.SetRemoteUrl("origin", remote); err != nil { 156 return err 157 } 158 159 // Delete inital branch(es) 160 if branches, _, err := scm.GetBranches(); err != nil { 161 jirix.Logger.Warningf("not able to get branches for newly created project %s(%s)\n\n", op.project.Name, op.project.Path) 162 } else { 163 scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path)) 164 for _, b := range branches { 165 if err := scm.DeleteBranch(b); err != nil { 166 jirix.Logger.Warningf("not able to delete branch %s for project %s(%s)\n\n", b, op.project.Name, op.project.Path) 167 } 168 } 169 } 170 return nil 171 } 172 173 func (op createOperation) Run(jirix *jiri.X) (e error) { 174 path, perm := filepath.Dir(op.destination), os.FileMode(0755) 175 176 // Check the local file system. 177 if op.destination != jirix.Root { 178 if _, err := os.Stat(op.destination); err != nil { 179 if !os.IsNotExist(err) { 180 return fmtError(err) 181 } 182 } else { 183 if isEmpty, err := isEmpty(op.destination); err != nil { 184 return err 185 } else if !isEmpty { 186 return fmt.Errorf("cannot create %q as it already exists and is not empty", op.destination) 187 } else { 188 if err := os.RemoveAll(op.destination); err != nil { 189 return fmt.Errorf("Not able to delete %q", op.destination) 190 } 191 } 192 } 193 194 if err := os.MkdirAll(path, perm); err != nil { 195 return fmtError(err) 196 } 197 } 198 199 cache, err := op.project.CacheDirPath(jirix) 200 if err != nil { 201 return err 202 } 203 if !isPathDir(cache) { 204 cache = "" 205 } 206 207 if err := op.checkoutProject(jirix, cache); err != nil { 208 if op.destination != jirix.Root { 209 if err := os.RemoveAll(op.destination); err != nil { 210 jirix.Logger.Warningf("Not able to remove %q after create failed: %s", op.destination, err) 211 } 212 } 213 return err 214 } 215 return nil 216 } 217 218 func (op createOperation) String() string { 219 return fmt.Sprintf("create project %q in %q and advance it to %q", op.project.Name, op.destination, fmtRevision(op.project.Revision)) 220 } 221 222 func (op createOperation) Test(jirix *jiri.X, updates *fsUpdates) error { 223 return nil 224 } 225 226 // deleteOperation represents the deletion of a project. 227 type deleteOperation struct { 228 commonOperation 229 } 230 231 func (op deleteOperation) Kind() string { 232 return "delete" 233 } 234 235 func (op deleteOperation) Run(jirix *jiri.X) error { 236 if op.project.LocalConfig.Ignore { 237 jirix.Logger.Warningf("Project %s(%s) won't be deleted due to it's local-config\n\n", op.project.Name, op.source) 238 return nil 239 } 240 // Never delete projects with non-master branches, uncommitted 241 // work, or untracked content. 242 scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path)) 243 branches, _, err := scm.GetBranches() 244 if err != nil { 245 return fmt.Errorf("Cannot get branches for project %q: %s", op.Project().Name, err) 246 } 247 uncommitted, err := scm.HasUncommittedChanges() 248 if err != nil { 249 return fmt.Errorf("Cannot get uncommited changes for project %q: %s", op.Project().Name, err) 250 } 251 untracked, err := scm.HasUntrackedFiles() 252 if err != nil { 253 return fmt.Errorf("Cannot get untracked changes for project %q: %s", op.Project().Name, err) 254 } 255 extraBranches := false 256 for _, branch := range branches { 257 if !strings.Contains(branch, "HEAD detached") { 258 extraBranches = true 259 break 260 } 261 } 262 263 if extraBranches || uncommitted || untracked { 264 rmCommand := jirix.Color.Yellow("rm -rf %q", op.source) 265 unManageCommand := jirix.Color.Yellow("rm -rf %q", filepath.Join(op.source, jiri.ProjectMetaDir)) 266 msg := "" 267 if extraBranches { 268 msg = fmt.Sprintf("Project %q won't be deleted as it contains branches", op.project.Name) 269 } else { 270 msg = fmt.Sprintf("Project %q won't be deleted as it might contain changes", op.project.Name) 271 } 272 msg += fmt.Sprintf("\nIf you no longer need it, invoke '%s'", rmCommand) 273 msg += fmt.Sprintf("\nIf you no longer want jiri to manage it, invoke '%s'\n\n", unManageCommand) 274 jirix.Logger.Warningf(msg) 275 return nil 276 } 277 278 if err := os.RemoveAll(op.source); err != nil { 279 return fmtError(err) 280 } 281 return removeEmptyParents(jirix, path.Dir(op.source)) 282 } 283 284 func removeEmptyParents(jirix *jiri.X, dir string) error { 285 isEmpty := func(name string) (bool, error) { 286 f, err := os.Open(name) 287 if err != nil { 288 return false, err 289 } 290 defer f.Close() 291 _, err = f.Readdirnames(1) 292 if err == io.EOF { 293 // empty dir 294 return true, nil 295 } 296 if err != nil { 297 return false, err 298 } 299 return false, nil 300 } 301 if jirix.Root == dir || dir == "" || dir == "." { 302 return nil 303 } 304 empty, err := isEmpty(dir) 305 if err != nil { 306 return err 307 } 308 if empty { 309 if err := os.Remove(dir); err != nil { 310 return err 311 } 312 jirix.Logger.Debugf("gc deleted empty parent directory: %v", dir) 313 return removeEmptyParents(jirix, path.Dir(dir)) 314 } 315 return nil 316 } 317 318 func (op deleteOperation) String() string { 319 return fmt.Sprintf("delete project %q from %q", op.project.Name, op.source) 320 } 321 322 func (op deleteOperation) Test(jirix *jiri.X, updates *fsUpdates) error { 323 if _, err := os.Stat(op.source); err != nil { 324 if os.IsNotExist(err) { 325 return fmt.Errorf("cannot delete %q as it does not exist", op.source) 326 } 327 return fmtError(err) 328 } 329 updates.deleteDir(op.source) 330 return nil 331 } 332 333 // moveOperation represents the relocation of a project. 334 type moveOperation struct { 335 commonOperation 336 rebaseTracked bool 337 rebaseUntracked bool 338 rebaseAll bool 339 snapshot bool 340 } 341 342 func (op moveOperation) Kind() string { 343 return "move" 344 } 345 346 func (op moveOperation) Run(jirix *jiri.X) error { 347 if op.project.LocalConfig.Ignore { 348 jirix.Logger.Warningf("Project %s(%s) won't be moved or updated due to it's local-config\n\n", op.project.Name, op.source) 349 return nil 350 } 351 // If it was nested project it might have been moved with its parent project 352 if op.source != op.destination { 353 path, perm := filepath.Dir(op.destination), os.FileMode(0755) 354 if err := os.MkdirAll(path, perm); err != nil { 355 return fmtError(err) 356 } 357 if err := osutil.Rename(op.source, op.destination); err != nil { 358 return fmtError(err) 359 } 360 } 361 if err := syncProjectMaster(jirix, op.project, op.state, op.rebaseTracked, op.rebaseUntracked, op.rebaseAll, op.snapshot); err != nil { 362 return err 363 } 364 return writeMetadata(jirix, op.project, op.project.Path) 365 } 366 367 func (op moveOperation) String() string { 368 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)) 369 } 370 371 func (op moveOperation) Test(jirix *jiri.X, updates *fsUpdates) error { 372 if _, err := os.Stat(op.source); err != nil { 373 if os.IsNotExist(err) { 374 return fmt.Errorf("cannot move %q to %q as the source does not exist", op.source, op.destination) 375 } 376 return fmtError(err) 377 } 378 if _, err := os.Stat(op.destination); err != nil { 379 if !os.IsNotExist(err) { 380 return fmtError(err) 381 } 382 } else { 383 return fmt.Errorf("cannot move %q to %q as the destination already exists", op.source, op.destination) 384 } 385 updates.deleteDir(op.source) 386 return nil 387 } 388 389 // changeRemoteOperation represents the chnage of remote URL 390 type changeRemoteOperation struct { 391 commonOperation 392 rebaseTracked bool 393 rebaseUntracked bool 394 rebaseAll bool 395 snapshot bool 396 } 397 398 func (op changeRemoteOperation) Kind() string { 399 return "change-remote" 400 } 401 402 func (op changeRemoteOperation) Run(jirix *jiri.X) error { 403 if op.project.LocalConfig.Ignore || op.project.LocalConfig.NoUpdate { 404 jirix.Logger.Warningf("Project %s(%s) won't be updated due to it's local-config. It has a changed remote\n\n", op.project.Name, op.project.Path) 405 return nil 406 } 407 git := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path)) 408 tempRemote := "new-remote-origin" 409 if err := git.AddRemote(tempRemote, op.project.Remote); err != nil { 410 return err 411 } 412 defer git.DeleteRemote(tempRemote) 413 414 if err := fetch(jirix, op.project.Path, tempRemote); err != nil { 415 return err 416 } 417 418 // Check for all leaf commits in new remote 419 for _, branch := range op.state.Branches { 420 if containingBranches, err := git.GetRemoteBranchesContaining(branch.Revision); err != nil { 421 return err 422 } else { 423 foundBranch := false 424 for _, remoteBranchName := range containingBranches { 425 if strings.HasPrefix(remoteBranchName, tempRemote) { 426 foundBranch = true 427 break 428 } 429 } 430 if !foundBranch { 431 jirix.Logger.Errorf("Note: For project %q(%v), remote url has changed. Its branch %q is on a commit", op.project.Name, op.project.Path, branch.Name) 432 jirix.Logger.Errorf("which is not in new remote(%v). Please manually reset your branches or move", op.project.Remote) 433 jirix.Logger.Errorf("your project folder out of the root and try again") 434 return nil 435 } 436 437 } 438 } 439 440 // Everything ok, change the remote url 441 if err := git.SetRemoteUrl("origin", op.project.Remote); err != nil { 442 return err 443 } 444 445 if err := fetch(jirix, op.project.Path, "", gitutil.AllOpt(true), gitutil.PruneOpt(true)); err != nil { 446 return err 447 } 448 449 if err := syncProjectMaster(jirix, op.project, op.state, op.rebaseTracked, op.rebaseUntracked, op.rebaseAll, op.snapshot); err != nil { 450 return err 451 } 452 453 return writeMetadata(jirix, op.project, op.project.Path) 454 } 455 456 func (op changeRemoteOperation) String() string { 457 return fmt.Sprintf("Change remote of project %q to %q and update it to %q", op.project.Name, op.project.Remote, fmtRevision(op.project.Revision)) 458 } 459 460 func (op changeRemoteOperation) Test(jirix *jiri.X, _ *fsUpdates) error { 461 return nil 462 } 463 464 // updateOperation represents the update of a project. 465 type updateOperation struct { 466 commonOperation 467 rebaseTracked bool 468 rebaseUntracked bool 469 rebaseAll bool 470 snapshot bool 471 } 472 473 func (op updateOperation) Kind() string { 474 return "update" 475 } 476 477 func (op updateOperation) Run(jirix *jiri.X) error { 478 if err := syncProjectMaster(jirix, op.project, op.state, op.rebaseTracked, op.rebaseUntracked, op.rebaseAll, op.snapshot); err != nil { 479 return err 480 } 481 return writeMetadata(jirix, op.project, op.project.Path) 482 } 483 484 func (op updateOperation) String() string { 485 return fmt.Sprintf("advance/rebase project %q located in %q to %q", op.project.Name, op.source, fmtRevision(op.project.Revision)) 486 } 487 488 func (op updateOperation) Test(jirix *jiri.X, _ *fsUpdates) error { 489 return nil 490 } 491 492 // nullOperation represents a noop. It is used for logging and adding project 493 // information to the current manifest. 494 type nullOperation struct { 495 commonOperation 496 } 497 498 func (op nullOperation) Kind() string { 499 return "null" 500 } 501 502 func (op nullOperation) Run(jirix *jiri.X) error { 503 return writeMetadata(jirix, op.project, op.project.Path) 504 } 505 506 func (op nullOperation) String() string { 507 return fmt.Sprintf("project %q located in %q at revision %q is up-to-date", op.project.Name, op.source, fmtRevision(op.project.Revision)) 508 } 509 510 func (op nullOperation) Test(jirix *jiri.X, _ *fsUpdates) error { 511 return nil 512 } 513 514 // operations is a sortable collection of operations 515 type operations []operation 516 517 // Len returns the length of the collection. 518 func (ops operations) Len() int { 519 return len(ops) 520 } 521 522 // Less defines the order of operations. Operations are ordered first 523 // by their type and then by their project path. 524 // 525 // The order in which operation types are defined determines the order 526 // in which operations are performed. For correctness and also to 527 // minimize the chance of a conflict, the delete operations should 528 // happen before change-remote operations, which should happen before move 529 // operations. If two create operations make nested directories, the 530 // outermost should be created first. 531 func (ops operations) Less(i, j int) bool { 532 vals := make([]int, 2) 533 for idx, op := range []operation{ops[i], ops[j]} { 534 switch op.Kind() { 535 case "delete": 536 vals[idx] = 0 537 case "change-remote": 538 vals[idx] = 1 539 case "move": 540 vals[idx] = 2 541 case "create": 542 vals[idx] = 3 543 case "update": 544 vals[idx] = 4 545 case "null": 546 vals[idx] = 5 547 } 548 } 549 if vals[0] != vals[1] { 550 return vals[0] < vals[1] 551 } 552 if vals[0] == 0 { 553 // delete sub folder first 554 return ops[i].Project().Path+string(filepath.Separator) > ops[j].Project().Path+string(filepath.Separator) 555 } else { 556 return ops[i].Project().Path+string(filepath.Separator) < ops[j].Project().Path+string(filepath.Separator) 557 } 558 } 559 560 // Swap swaps two elements of the collection. 561 func (ops operations) Swap(i, j int) { 562 ops[i], ops[j] = ops[j], ops[i] 563 } 564 565 // computeOperations inputs a set of projects to update and the set of 566 // current and new projects (as defined by contents of the local file 567 // system and manifest file respectively) and outputs a collection of 568 // operations that describe the actions needed to update the target 569 // projects. 570 func computeOperations(localProjects, remoteProjects Projects, states map[ProjectKey]*ProjectState, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot bool) operations { 571 result := operations{} 572 allProjects := map[ProjectKey]bool{} 573 for _, p := range localProjects { 574 allProjects[p.Key()] = true 575 } 576 for _, p := range remoteProjects { 577 allProjects[p.Key()] = true 578 } 579 for key, _ := range allProjects { 580 var local, remote *Project 581 var state *ProjectState 582 if project, ok := localProjects[key]; ok { 583 local = &project 584 } 585 if project, ok := remoteProjects[key]; ok { 586 // update remote local config 587 if local != nil { 588 project.LocalConfig = local.LocalConfig 589 remoteProjects[key] = project 590 } 591 remote = &project 592 } 593 if s, ok := states[key]; ok { 594 state = s 595 } 596 result = append(result, computeOp(local, remote, state, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot)) 597 } 598 sort.Sort(result) 599 return result 600 } 601 602 func computeOp(local, remote *Project, state *ProjectState, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot bool) operation { 603 switch { 604 case local == nil && remote != nil: 605 return createOperation{commonOperation{ 606 destination: remote.Path, 607 project: *remote, 608 source: "", 609 }} 610 case local != nil && remote == nil: 611 return deleteOperation{commonOperation{ 612 destination: "", 613 project: *local, 614 source: local.Path, 615 }} 616 case local != nil && remote != nil: 617 618 localBranchesNeedUpdating := false 619 if !snapshot { 620 cb := state.CurrentBranch 621 if rebaseAll { 622 for _, branch := range state.Branches { 623 if branch.Tracking != nil { 624 if branch.Revision != branch.Tracking.Revision { 625 localBranchesNeedUpdating = true 626 break 627 } 628 } else if rebaseUntracked && rebaseAll { 629 // We put checks for untracked-branch updation in syncProjectMaster funtion 630 localBranchesNeedUpdating = true 631 break 632 } 633 } 634 } else if cb.Name != "" && cb.Tracking != nil && cb.Revision != cb.Tracking.Revision { 635 localBranchesNeedUpdating = true 636 } 637 } 638 switch { 639 case local.Remote != remote.Remote: 640 return changeRemoteOperation{commonOperation{ 641 destination: remote.Path, 642 project: *remote, 643 source: local.Path, 644 state: *state, 645 }, rebaseTracked, rebaseUntracked, rebaseAll, snapshot} 646 case local.Path != remote.Path: 647 // moveOperation also does an update, so we don't need to check the 648 // revision here. 649 return moveOperation{commonOperation{ 650 destination: remote.Path, 651 project: *remote, 652 source: local.Path, 653 state: *state, 654 }, rebaseTracked, rebaseUntracked, rebaseAll, snapshot} 655 case snapshot && local.Revision != remote.Revision: 656 return updateOperation{commonOperation{ 657 destination: remote.Path, 658 project: *remote, 659 source: local.Path, 660 state: *state, 661 }, rebaseTracked, rebaseUntracked, rebaseAll, snapshot} 662 case localBranchesNeedUpdating || (state.CurrentBranch.Name == "" && local.Revision != remote.Revision): 663 return updateOperation{commonOperation{ 664 destination: remote.Path, 665 project: *remote, 666 source: local.Path, 667 state: *state, 668 }, rebaseTracked, rebaseUntracked, rebaseAll, snapshot} 669 case state.CurrentBranch.Tracking == nil && local.Revision != remote.Revision: 670 return updateOperation{commonOperation{ 671 destination: remote.Path, 672 project: *remote, 673 source: local.Path, 674 state: *state, 675 }, rebaseTracked, rebaseUntracked, rebaseAll, snapshot} 676 default: 677 return nullOperation{commonOperation{ 678 destination: remote.Path, 679 project: *remote, 680 source: local.Path, 681 state: *state, 682 }} 683 } 684 default: 685 panic("jiri: computeOp called with nil local and remote") 686 } 687 } 688 689 // This function creates worktree and runs create operation in parallel 690 func runCreateOperations(jirix *jiri.X, ops []createOperation) MultiError { 691 count := len(ops) 692 if count == 0 { 693 return nil 694 } 695 696 type workTree struct { 697 // dir is the top level directory in which operations will be performed 698 dir string 699 // op is an ordered list of operations that must be performed serially, 700 // affecting dir 701 ops []operation 702 // after contains a tree of work that must be performed after ops 703 after map[string]*workTree 704 } 705 head := &workTree{ 706 dir: "", 707 ops: []operation{}, 708 after: make(map[string]*workTree), 709 } 710 711 for _, op := range ops { 712 713 node := head 714 parts := strings.Split(op.Project().Path, string(filepath.Separator)) 715 // walk down the file path tree, creating any work tree nodes as required 716 for _, part := range parts { 717 if part == "" { 718 continue 719 } 720 next, ok := node.after[part] 721 if !ok { 722 next = &workTree{ 723 dir: part, 724 ops: []operation{}, 725 after: make(map[string]*workTree), 726 } 727 node.after[part] = next 728 } 729 node = next 730 } 731 node.ops = append(node.ops, op) 732 } 733 734 workQueue := make(chan *workTree, count) 735 errs := make(chan error, count) 736 var wg sync.WaitGroup 737 processTree := func(tree *workTree) { 738 defer wg.Done() 739 for _, op := range tree.ops { 740 logMsg := fmt.Sprintf("Creating project %q", op.Project().Name) 741 task := jirix.Logger.AddTaskMsg(logMsg) 742 jirix.Logger.Debugf("%v", op) 743 if err := op.Run(jirix); err != nil { 744 task.Done() 745 errs <- fmt.Errorf("%s: %s", logMsg, err) 746 return 747 } 748 task.Done() 749 } 750 for _, v := range tree.after { 751 wg.Add(1) 752 workQueue <- v 753 } 754 } 755 wg.Add(1) 756 workQueue <- head 757 for i := uint(0); i < jirix.Jobs; i++ { 758 go func() { 759 for tree := range workQueue { 760 processTree(tree) 761 } 762 }() 763 } 764 wg.Wait() 765 close(workQueue) 766 close(errs) 767 768 var multiErr MultiError 769 for err := range errs { 770 multiErr = append(multiErr, err) 771 } 772 return multiErr 773 } 774 775 type PathTrie struct { 776 current string 777 children map[string]*PathTrie 778 } 779 780 func NewPathTrie() *PathTrie { 781 return &PathTrie{ 782 current: "", 783 children: make(map[string]*PathTrie), 784 } 785 } 786 func (p *PathTrie) Contains(path string) bool { 787 parts := strings.Split(path, string(filepath.Separator)) 788 node := p 789 for _, part := range parts { 790 if part == "" { 791 continue 792 } 793 child, ok := node.children[part] 794 if !ok { 795 return false 796 } 797 node = child 798 } 799 return true 800 } 801 802 func (p *PathTrie) Insert(path string) { 803 parts := strings.Split(path, string(filepath.Separator)) 804 node := p 805 for _, part := range parts { 806 if part == "" { 807 continue 808 } 809 child, ok := node.children[part] 810 if !ok { 811 child = &PathTrie{ 812 current: part, 813 children: make(map[string]*PathTrie), 814 } 815 node.children[part] = child 816 } 817 node = child 818 } 819 } 820 821 func runDeleteOperations(jirix *jiri.X, ops []deleteOperation, gc bool) error { 822 if len(ops) == 0 { 823 return nil 824 } 825 notDeleted := NewPathTrie() 826 if !gc { 827 msg := fmt.Sprintf("%d project(s) is/are marked to be deleted. Run '%s' to delete them.", len(ops), jirix.Color.Yellow("jiri update -gc")) 828 if jirix.Logger.LoggerLevel < log.DebugLevel { 829 msg = fmt.Sprintf("%s\nOr run '%s' or '%s' to see the list of projects.", msg, jirix.Color.Yellow("jiri update -v"), jirix.Color.Yellow("jiri status -d")) 830 } 831 jirix.Logger.Warningf("%s\n\n", msg) 832 if jirix.Logger.LoggerLevel >= log.DebugLevel { 833 msg = "List of project(s) marked to be deleted:" 834 for _, op := range ops { 835 msg = fmt.Sprintf("%s\nName: %s, Path: '%s'", msg, jirix.Color.Yellow(op.project.Name), jirix.Color.Yellow(op.source)) 836 } 837 jirix.Logger.Debugf("%s\n\n", msg) 838 } 839 return nil 840 } 841 for _, op := range ops { 842 if notDeleted.Contains(op.Project().Path) { 843 // not deleting project, add it to trie 844 notDeleted.Insert(op.source) 845 rmCommand := jirix.Color.Yellow("rm -rf %q", op.source) 846 msg := fmt.Sprintf("Project %q won't be deleted because of its sub project(s)", op.project.Name) 847 msg += fmt.Sprintf("\nIf you no longer need it, invoke '%s'\n\n", rmCommand) 848 jirix.Logger.Warningf(msg) 849 continue 850 } 851 logMsg := fmt.Sprintf("Deleting project %q", op.Project().Name) 852 task := jirix.Logger.AddTaskMsg(logMsg) 853 jirix.Logger.Debugf("%s", op) 854 if err := op.Run(jirix); err != nil { 855 task.Done() 856 return fmt.Errorf("%s: %s", logMsg, err) 857 } 858 task.Done() 859 if _, err := os.Stat(op.source); err == nil { 860 // project not deleted, add it to trie 861 notDeleted.Insert(op.source) 862 } else if err != nil && !os.IsNotExist(err) { 863 return fmt.Errorf("Checking if %q exists", op.source) 864 } 865 } 866 return nil 867 } 868 869 func runMoveOperations(jirix *jiri.X, ops []moveOperation) error { 870 parentSrcPath := "" 871 parentDestPath := "" 872 for _, op := range ops { 873 if parentSrcPath != "" && strings.HasPrefix(op.source, parentSrcPath) { 874 op.source = filepath.Join(parentDestPath, strings.Replace(op.source, parentSrcPath, "", 1)) 875 } else { 876 parentSrcPath = op.source 877 parentDestPath = op.destination 878 } 879 logMsg := fmt.Sprintf("Moving and updating project %q", op.Project().Name) 880 task := jirix.Logger.AddTaskMsg(logMsg) 881 jirix.Logger.Debugf("%s", op) 882 if err := op.Run(jirix); err != nil { 883 task.Done() 884 return fmt.Errorf("%s: %s", logMsg, err) 885 } 886 task.Done() 887 } 888 return nil 889 } 890 891 func runCommonOperations(jirix *jiri.X, ops operations, loglevel log.LogLevel) error { 892 for _, op := range ops { 893 logMsg := fmt.Sprintf("Updating project %q", op.Project().Name) 894 task := jirix.Logger.AddTaskMsg(logMsg) 895 jirix.Logger.Logf(loglevel, "%s", op) 896 if err := op.Run(jirix); err != nil { 897 task.Done() 898 return fmt.Errorf("%s: %s", logMsg, err) 899 } 900 task.Done() 901 } 902 return nil 903 }