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  }