v.io/jiri@v0.0.0-20160715023856-abfb8b131290/project/project.go (about)

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