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  }