github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/project/loader.go (about)

     1  // Copyright 2017 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package project
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"reflect"
    14  	"strings"
    15  
    16  	"github.com/btwiuse/jiri"
    17  	"github.com/btwiuse/jiri/gitutil"
    18  )
    19  
    20  type importCache struct {
    21  	localManifest bool
    22  	ref           string
    23  
    24  	// keeps track of first import tag, this is used to give helpful error
    25  	// message in the case of import conflict
    26  	parentImport string
    27  }
    28  
    29  type loader struct {
    30  	Projects         Projects
    31  	ProjectOverrides map[string]Project
    32  	ImportOverrides  map[string]Import
    33  	ProjectLocks     ProjectLocks
    34  	Hooks            Hooks
    35  	Packages         Packages
    36  	PackageLocks     PackageLocks
    37  	TmpDir           string
    38  	localProjects    Projects
    39  	importProjects   Projects
    40  	importCacheMap   map[string]importCache
    41  	importTree       importTree
    42  	update           bool
    43  	cycleStack       []cycleInfo
    44  	manifests        map[string]bool
    45  	lockfiles        map[string]bool
    46  	parentFile       string
    47  }
    48  
    49  type importTreeNode struct {
    50  	parents            map[*importTreeNode]bool
    51  	children           map[*importTreeNode]bool
    52  	tag                string
    53  	computedAttributes attributes
    54  }
    55  
    56  type importTree struct {
    57  	root          *importTreeNode
    58  	pool          map[string]*importTreeNode
    59  	filenameMap   map[string]bool
    60  	projectKeyMap map[ProjectKey]*importTreeNode
    61  }
    62  
    63  func newImportTree() importTree {
    64  	return importTree{
    65  		root: &importTreeNode{
    66  			make(map[*importTreeNode]bool),
    67  			make(map[*importTreeNode]bool),
    68  			"",
    69  			nil,
    70  		},
    71  		pool:          make(map[string]*importTreeNode),
    72  		filenameMap:   make(map[string]bool),
    73  		projectKeyMap: make(map[ProjectKey]*importTreeNode),
    74  	}
    75  }
    76  
    77  func (t *importTree) getNode(repoPath, file, ref string) *importTreeNode {
    78  	key := filepath.Join(repoPath, file) + ":" + ref
    79  	if v, ok := t.pool[key]; ok {
    80  		return v
    81  	}
    82  	v := &importTreeNode{
    83  		make(map[*importTreeNode]bool),
    84  		make(map[*importTreeNode]bool),
    85  		key,
    86  		nil,
    87  	}
    88  	if len(t.pool) == 0 {
    89  		t.root.addChild(v)
    90  	}
    91  	t.pool[key] = v
    92  	return v
    93  }
    94  
    95  func (t *importTree) buildImportAttributes() {
    96  	// Due to the logic in how remote imports are handled, the
    97  	// manifest loader will create two nodes for a single remote import,
    98  	// causing second one as an orphan. A simple solution is just
    99  	// connect the orphan nodes to the Root to build a connected tree.
   100  	for _, v := range t.pool {
   101  		if len(v.parents) == 0 {
   102  			t.root.addChild(v)
   103  		}
   104  	}
   105  	// The file name of a manifest is used as a git attributes name only if that
   106  	// file name is unique.
   107  	dupMap := make(map[string]bool)
   108  	for k := range t.filenameMap {
   109  		filename := filepath.Base(k)
   110  		if _, ok := dupMap[filename]; ok {
   111  			dupMap[filename] = true
   112  		} else {
   113  			dupMap[filename] = false
   114  		}
   115  	}
   116  	dfsAddAttribute(t.root, newAttributes(""))
   117  	for _, node := range t.pool {
   118  		for attr := range node.computedAttributes {
   119  			if v, ok := dupMap[attr]; ok && v {
   120  				delete(node.computedAttributes, attr)
   121  			}
   122  		}
   123  	}
   124  }
   125  
   126  func dfsAddAttribute(curNode *importTreeNode, parentAttrs attributes) {
   127  	if curNode.computedAttributes == nil {
   128  		curNode.computedAttributes = newAttributes(curNode.tag)
   129  	}
   130  	curNode.computedAttributes.Add(parentAttrs)
   131  	for k := range curNode.children {
   132  		dfsAddAttribute(k, curNode.computedAttributes)
   133  	}
   134  }
   135  
   136  // (TODO:haowei) generateAttributeGraph generate a graphviz .dot file of
   137  // importTree for debugging purpose. It should be removed once .gitattribute
   138  // generator is considered as stable.
   139  func (t *importTree) generateAttributeGraph() string {
   140  	var buf bytes.Buffer
   141  	buf.WriteString("\ndigraph G {\n")
   142  	nodeCount := 1
   143  	nameMap := make(map[*importTreeNode]string)
   144  	nameMap[t.root] = "n0"
   145  	for _, v := range t.pool {
   146  		nameMap[v] = fmt.Sprintf("n%d", nodeCount)
   147  		nodeCount++
   148  	}
   149  
   150  	// The file name of a manifest is used as a git attributes name only if that
   151  	// file name is unique.
   152  	dupMap := make(map[string]bool)
   153  	for k := range t.filenameMap {
   154  		filename := filepath.Base(k)
   155  		if _, ok := dupMap[filename]; ok {
   156  			dupMap[filename] = true
   157  		} else {
   158  			dupMap[filename] = false
   159  		}
   160  	}
   161  	// Output nodes
   162  	for k, v := range nameMap {
   163  		attrs := newAttributes(k.tag)
   164  		for attr := range attrs {
   165  			if v, ok := dupMap[attr]; ok && v {
   166  				delete(attrs, attr)
   167  			}
   168  		}
   169  		buf.WriteString(fmt.Sprintf("\t%s[label=\"%s,%p\"];\n", v, attrs.String(), k))
   170  	}
   171  	buf.WriteString("\n")
   172  	// Output edges
   173  	for k := range nameMap {
   174  		for child := range k.children {
   175  			nameSource := nameMap[k]
   176  			nameTarget, ok := nameMap[child]
   177  			if !ok {
   178  				fmt.Printf("ERROR: cound not find target node %q,%p in nameMap\n", child.tag, child)
   179  				continue
   180  			}
   181  			buf.WriteString(fmt.Sprintf("\t%s -> %s;\n", nameSource, nameTarget))
   182  		}
   183  	}
   184  
   185  	buf.WriteString("\n}")
   186  	return buf.String()
   187  }
   188  
   189  func (n *importTreeNode) addChild(other *importTreeNode) {
   190  	if other == nil {
   191  		return
   192  	}
   193  	n.children[other] = true
   194  	other.parents[n] = true
   195  }
   196  
   197  func (ld *loader) cleanup() {
   198  	if ld.TmpDir != "" {
   199  		os.RemoveAll(ld.TmpDir)
   200  		ld.TmpDir = ""
   201  	}
   202  }
   203  
   204  type cycleInfo struct {
   205  	file, key string
   206  }
   207  
   208  // newManifestLoader returns a new manifest loader.  The localProjects are used
   209  // to resolve remote imports; if nil, encountering any remote import will result
   210  // in an error.  If update is true, remote manifest import projects that don't
   211  // exist locally are cloned under TmpDir, and inserted into localProjects.
   212  //
   213  // If update is true, remote changes to manifest projects will be fetched, and
   214  // manifest projects that don't exist locally will be created in temporary
   215  // directories, and added to localProjects.
   216  func newManifestLoader(localProjects Projects, update bool, file string) *loader {
   217  	return &loader{
   218  		Projects:         make(Projects),
   219  		ProjectOverrides: make(map[string]Project),
   220  		ImportOverrides:  make(map[string]Import),
   221  		ProjectLocks:     make(ProjectLocks),
   222  		Hooks:            make(Hooks),
   223  		Packages:         make(Packages),
   224  		PackageLocks:     make(PackageLocks),
   225  		localProjects:    localProjects,
   226  		importProjects:   make(Projects),
   227  		update:           update,
   228  		importCacheMap:   make(map[string]importCache),
   229  		manifests:        make(map[string]bool),
   230  		lockfiles:        make(map[string]bool),
   231  		importTree:       newImportTree(),
   232  		parentFile:       file,
   233  	}
   234  }
   235  
   236  // loadNoCycles checks for cycles in imports.  There are two types of cycles:
   237  //   file - Cycle in the paths of manifest files in the local filesystem.
   238  //   key  - Cycle in the remote manifests specified by remote imports.
   239  //
   240  // Example of file cycles.  File A imports file B, and vice versa.
   241  //     file=manifest/A              file=manifest/B
   242  //     <manifest>                   <manifest>
   243  //       <localimport file="B"/>      <localimport file="A"/>
   244  //     </manifest>                  </manifest>
   245  //
   246  // Example of key cycles.  The key consists of "remote/manifest", e.g.
   247  //   https://vanadium.googlesource.com/manifest/v2/default
   248  // In the example, key x/A imports y/B, and vice versa.
   249  //     key=x/A                               key=y/B
   250  //     <manifest>                            <manifest>
   251  //       <import remote="y" manifest="B"/>     <import remote="x" manifest="A"/>
   252  //     </manifest>                           </manifest>
   253  //
   254  // The above examples are simple, but the general strategy is demonstrated.  We
   255  // keep a single stack for both files and keys, and push onto each stack before
   256  // running the recursive read or update function, and pop the stack when the
   257  // function is done.  If we see a duplicate on the stack at any point, we know
   258  // there's a cycle.  Note that we know the file for both local and remote
   259  // imports, but we only know the key for remote imports; the key for local
   260  // imports is empty.
   261  //
   262  // A more complex case would involve a combination of local and remote imports,
   263  // using the "root" attribute to change paths on the local filesystem.  In this
   264  // case the key will eventually expose the cycle.
   265  func (ld *loader) loadNoCycles(jirix *jiri.X, root, repoPath, file, ref, cycleKey, parentImport string, localManifest bool) error {
   266  	f := file
   267  	if repoPath != "" {
   268  		f = filepath.Join(repoPath, file)
   269  	}
   270  	info := cycleInfo{f, cycleKey}
   271  	for _, c := range ld.cycleStack {
   272  		switch {
   273  		case f == c.file:
   274  			return fmt.Errorf("import cycle detected in local manifest files: %q", append(ld.cycleStack, info))
   275  		case cycleKey == c.key && cycleKey != "":
   276  			return fmt.Errorf("import cycle detected in remote manifest imports: %q", append(ld.cycleStack, info))
   277  		}
   278  	}
   279  	ld.cycleStack = append(ld.cycleStack, info)
   280  	if err := ld.load(jirix, root, repoPath, file, ref, parentImport, localManifest); err != nil {
   281  		return err
   282  	}
   283  	ld.cycleStack = ld.cycleStack[:len(ld.cycleStack)-1]
   284  	return nil
   285  }
   286  
   287  // shortFileName returns the relative path if file is relative to root,
   288  // otherwise returns the file name unchanged.
   289  func shortFileName(root, repoPath, file, ref string) string {
   290  	if repoPath != "" {
   291  		return fmt.Sprintf("%s %s:%s", shortFileName(root, "", repoPath, ""), ref, file)
   292  	}
   293  	if p := root + string(filepath.Separator); strings.HasPrefix(file, p) {
   294  		return file[len(p):]
   295  	}
   296  	return file
   297  }
   298  
   299  func (ld *loader) Load(jirix *jiri.X, root, repoPath, file, ref, cycleKey, parentImport string, localManifest bool) error {
   300  	jirix.TimerPush("load " + shortFileName(jirix.Root, repoPath, file, ref))
   301  	defer jirix.TimerPop()
   302  	return ld.loadNoCycles(jirix, root, repoPath, file, ref, cycleKey, parentImport, localManifest)
   303  }
   304  
   305  func (ld *loader) cloneManifestRepo(jirix *jiri.X, remote *Import, cacheDirPath string, localManifest bool) error {
   306  	if !ld.update || localManifest {
   307  		jirix.Logger.Warningf("import %q not found locally, getting from server. Please check your manifest file (default: .jiri_manifest).\nMake sure that the 'name' attributes on the 'import' and 'project' tags match and that there is a corresponding 'project' tag for every 'import' tag.\n\n", remote.Name)
   308  	}
   309  	jirix.Logger.Debugf("clone manifest project %q", remote.Name)
   310  	// The remote manifest project doesn't exist locally.  Clone it into a
   311  	// temp directory, and add it to ld.localProjects.
   312  	if ld.TmpDir == "" {
   313  		var err error
   314  		if ld.TmpDir, err = ioutil.TempDir("", "jiri-load"); err != nil {
   315  			return fmt.Errorf("TempDir() failed: %v", err)
   316  		}
   317  	}
   318  	path := filepath.Join(ld.TmpDir, remote.projectKeyFileName())
   319  	p, err := remote.toProject(path)
   320  	if err != nil {
   321  		return err
   322  	}
   323  	if err := os.MkdirAll(path, 0755); err != nil {
   324  		return fmtError(err)
   325  	}
   326  	remoteUrl := rewriteRemote(jirix, p.Remote)
   327  	r := remoteUrl
   328  	task := jirix.Logger.AddTaskMsg("Creating manifest: %s", remote.Name)
   329  	defer task.Done()
   330  	if cacheDirPath != "" {
   331  		logStr := fmt.Sprintf("update/create cache for project %q", remote.Name)
   332  		jirix.Logger.Debugf(logStr)
   333  		task := jirix.Logger.AddTaskMsg(logStr)
   334  		defer task.Done()
   335  		if err := updateOrCreateCache(jirix, cacheDirPath, remoteUrl, remote.RemoteBranch, remote.Revision, 0); err != nil {
   336  			return err
   337  		}
   338  		r = cacheDirPath
   339  	}
   340  	opts := []gitutil.CloneOpt{gitutil.ReferenceOpt(cacheDirPath), gitutil.NoCheckoutOpt(true)}
   341  	if jirix.Partial {
   342  		opts = append(opts, gitutil.OmitBlobsOpt(true))
   343  	}
   344  	if err := clone(jirix, r, path, opts...); err != nil {
   345  		return err
   346  	}
   347  	scm := gitutil.New(jirix, gitutil.RootDirOpt(path))
   348  	defer func() {
   349  		if err := scm.AddOrReplaceRemote("origin", remoteUrl); err != nil {
   350  			jirix.Logger.Errorf("failed to set remote back to %v for project %+v", remoteUrl, p)
   351  		}
   352  	}()
   353  	p.Revision = remote.Revision
   354  	p.RemoteBranch = remote.RemoteBranch
   355  	if err := checkoutHeadRevision(jirix, p, false); err != nil {
   356  		return fmt.Errorf("Not able to checkout head for %s(%s): %v", p.Name, p.Path, err)
   357  	}
   358  	ld.localProjects[remote.ProjectKey()] = p
   359  	return nil
   360  }
   361  
   362  // loadLockFile will recursively load lockfiles from dir to its parent directories until it
   363  // reaches $JIRI_ROOT. It will only report errors on lockfiles such as unknown format or confliting data.
   364  // All I/O related errors will be ignored.
   365  func (ld *loader) loadLockFile(jirix *jiri.X, repoPath, dir, lockFileName, ref string) error {
   366  	lockfile := filepath.Join(dir, lockFileName)
   367  	lockKey := lockfile
   368  	if repoPath != "" {
   369  		lockKey = filepath.Join(repoPath, lockfile) + ":" + ref
   370  	}
   371  	if ld.lockfiles[lockKey] {
   372  		return nil
   373  	}
   374  
   375  	if !(dir == "" || dir == "." || dir == jirix.Root || dir == string(filepath.Separator)) {
   376  		if err := ld.loadLockFile(jirix, repoPath, filepath.Dir(dir), lockFileName, ref); err != nil {
   377  			return err
   378  		}
   379  	}
   380  
   381  	var data []byte
   382  	if repoPath != "" {
   383  		s, err := gitutil.New(jirix, gitutil.RootDirOpt(repoPath)).Show(ref, lockfile)
   384  		if err != nil {
   385  			// It's fine if jiri.lock cannot be find, skip this jiri.lock
   386  			jirix.Logger.Debugf("Could not find %q in repository %q for ref %q", lockfile, repoPath, ref)
   387  			return nil
   388  		}
   389  		data = []byte(s)
   390  	} else {
   391  		if _, err := os.Stat(lockfile); err != nil {
   392  			if os.IsNotExist(err) {
   393  				jirix.Logger.Debugf("could not find %q file at %q", lockFileName, lockfile)
   394  			} else {
   395  				jirix.Logger.Debugf("could not access %q file at %q due to error %v", lockFileName, lockfile, err)
   396  			}
   397  			return nil
   398  		}
   399  		temp, err := ioutil.ReadFile(lockfile)
   400  		if err != nil {
   401  			// Supress I/O errors as it is OK if a lockfile cannot be accessed.
   402  			return nil
   403  		}
   404  		data = temp
   405  	}
   406  	if err := ld.parseLockData(jirix, data); err != nil {
   407  		return err
   408  	}
   409  	if repoPath == "" {
   410  		jirix.Logger.Debugf("loaded lockfile at %s", lockfile)
   411  	} else {
   412  		jirix.Logger.Debugf("loaded lockfile at %q in repository %q for ref %q", lockfile, repoPath, ref)
   413  	}
   414  	ld.lockfiles[lockKey] = true
   415  	return nil
   416  }
   417  
   418  func (ld *loader) parseLockData(jirix *jiri.X, data []byte) error {
   419  	projectLocks, pkgLocks, err := UnmarshalLockEntries(data)
   420  	if err != nil {
   421  		return err
   422  	}
   423  
   424  	for k, v := range projectLocks {
   425  		if projLock, ok := ld.ProjectLocks[k]; ok {
   426  			if projLock != v && !jirix.UsingImportOverride {
   427  				return fmt.Errorf("conflicting project lock entries %+v with %+v", projLock, v)
   428  			}
   429  		} else {
   430  			ld.ProjectLocks[k] = v
   431  		}
   432  	}
   433  
   434  	for k, v := range pkgLocks {
   435  		if pkgLock, ok := ld.PackageLocks[k]; ok {
   436  			// Only package locks may conflict during a normal 'jiri resolve'.
   437  			// Treating conflicts as errors in all other scenarios.
   438  			if pkgLock != v && !jirix.IgnoreLockConflicts && !jirix.UsingImportOverride {
   439  				return fmt.Errorf("conflicting package lock entries %+v with %+v", pkgLock, v)
   440  			}
   441  		} else {
   442  			ld.PackageLocks[k] = v
   443  		}
   444  	}
   445  
   446  	return nil
   447  }
   448  
   449  func (ld *loader) load(jirix *jiri.X, root, repoPath, file, ref, parentImport string, localManifest bool) error {
   450  	f := file
   451  	if repoPath != "" {
   452  		f = filepath.Join(repoPath, file)
   453  	}
   454  	if ld.manifests[f] {
   455  		return nil
   456  	}
   457  	ld.manifests[f] = true
   458  
   459  	loadManifestAndLocks := func(jirix *jiri.X, file string) (*Manifest, error) {
   460  		if repoPath == "" {
   461  			m, err := ManifestFromFile(jirix, file)
   462  			if err != nil {
   463  				return nil, fmt.Errorf("Error reading from manifest file %s %s:%s:error(%s)", repoPath, ref, file, err)
   464  			}
   465  			if jirix.LockfileEnabled {
   466  				if err := ld.loadLockFile(jirix, repoPath, filepath.Dir(file), jirix.LockfileName, ref); err != nil {
   467  					return nil, err
   468  				}
   469  			}
   470  			return m, err
   471  		}
   472  		// repoPath != ""
   473  		s, err := gitutil.New(jirix, gitutil.RootDirOpt(repoPath)).Show(ref, file)
   474  		if err != nil {
   475  			return nil, fmt.Errorf("Unable to get manifest file for %s %s:%s:error(%s)", repoPath, ref, file, err)
   476  		}
   477  		m, err := ManifestFromBytes([]byte(s))
   478  		if err != nil {
   479  			return nil, fmt.Errorf("Error reading from manifest file %s %s:%s:error(%s)", repoPath, ref, file, err)
   480  		}
   481  		if jirix.LockfileEnabled {
   482  			if err := ld.loadLockFile(jirix, repoPath, filepath.Dir(file), jirix.LockfileName, ref); err != nil {
   483  				return nil, err
   484  			}
   485  		}
   486  		return m, nil
   487  	}
   488  
   489  	m, err := loadManifestAndLocks(jirix, file)
   490  	if err != nil {
   491  		return err
   492  	}
   493  
   494  	if jirix.UsingSnapshot && !jirix.OverrideOptional {
   495  		// using attributes defined in snapshot file instead of
   496  		// using predefined ones in jiri init.
   497  		jirix.FetchingAttrs = m.Attributes
   498  	}
   499  
   500  	// Add override information
   501  	if parentImport == "" {
   502  		for _, p := range m.ProjectOverrides {
   503  			// Reuse the MakeProjectKey function in case it is changed
   504  			// in the future.
   505  			key := string(p.Key())
   506  			ld.ProjectOverrides[key] = p
   507  		}
   508  		for _, p := range m.ImportOverrides {
   509  			// Reuse the MakeProjectKey function in case it is changed
   510  			// in the future.
   511  			key := string(p.ProjectKey())
   512  			if !jirix.UsingImportOverride {
   513  				jirix.UsingImportOverride = true
   514  			}
   515  			ld.ImportOverrides[key] = p
   516  		}
   517  	} else if len(m.ProjectOverrides)+len(m.ImportOverrides) > 0 {
   518  		return fmt.Errorf("manifest %q contains overrides but was imported by %q. Overrides are allowed only in the root manifest", shortFileName(jirix.Root, repoPath, file, ref), parentImport)
   519  	}
   520  
   521  	// Use manifest's directory name and file name as default
   522  	// git attributes. It will be later expanded using the
   523  	// import relationships.
   524  	defaultGitAttrs := func() string {
   525  		manifestFile := file
   526  		if repoPath != "" {
   527  			manifestFile = filepath.Join(repoPath, manifestFile)
   528  		}
   529  		containingDir := filepath.Base(filepath.Dir(manifestFile))
   530  		filename := file
   531  		ld.importTree.filenameMap[filename] = false
   532  		return containingDir + "," + filepath.Base(file)
   533  	}
   534  	self := ld.importTree.getNode(repoPath, file, ref)
   535  	self.tag = defaultGitAttrs()
   536  	// Process remote imports.
   537  	for _, remote := range m.Imports {
   538  		// Apply override if it exists.
   539  		remote, err := overrideImport(jirix, remote, ld.ProjectOverrides, ld.ImportOverrides)
   540  		if err != nil {
   541  			return err
   542  		}
   543  		nextRoot := filepath.Join(root, remote.Root)
   544  		remote.Name = filepath.Join(nextRoot, remote.Name)
   545  		key := remote.ProjectKey()
   546  		p, ok := ld.localProjects[key]
   547  		cacheDirPath, err := cacheDirPathFromRemote(jirix.Cache, remote.Remote)
   548  		if err != nil {
   549  			return err
   550  		}
   551  
   552  		if !ok {
   553  			if err := ld.cloneManifestRepo(jirix, &remote, cacheDirPath, localManifest); err != nil {
   554  				return err
   555  			}
   556  			p = ld.localProjects[key]
   557  		}
   558  		// Reset the project to its specified branch and load the next file.  Note
   559  		// that we call load() recursively, so multiple files may be loaded by
   560  		// loadImport.
   561  		p.Revision = remote.Revision
   562  		p.RemoteBranch = remote.RemoteBranch
   563  		ld.importProjects[key] = p
   564  		pi := parentImport
   565  		if pi == "" {
   566  			pi = fmt.Sprintf("import[manifest=%q, remote=%q]", remote.Manifest, remote.Remote)
   567  		}
   568  
   569  		self.addChild(ld.importTree.getNode(repoPath, remote.Manifest, ""))
   570  		if err := ld.loadImport(jirix, nextRoot, remote.Manifest, remote.cycleKey(), cacheDirPath, pi, p, localManifest); err != nil {
   571  			return err
   572  		}
   573  	}
   574  
   575  	// Process local imports.
   576  	for _, local := range m.LocalImports {
   577  		nextFile := filepath.Join(filepath.Dir(file), local.File)
   578  		self.addChild(ld.importTree.getNode(repoPath, nextFile, ref))
   579  		if err := ld.Load(jirix, root, repoPath, nextFile, ref, "", parentImport, localManifest); err != nil {
   580  			return err
   581  		}
   582  	}
   583  
   584  	hookMap := make(map[string][]*Hook)
   585  
   586  	for idx, _ := range m.Hooks {
   587  		hook := &m.Hooks[idx]
   588  		if err := hook.validate(); err != nil {
   589  			return err
   590  		}
   591  		hookMap[hook.ProjectName] = append(hookMap[hook.ProjectName], hook)
   592  	}
   593  
   594  	// Collect projects.
   595  	for _, project := range m.Projects {
   596  		// Apply override if it exists.
   597  		project, err := overrideProject(jirix, project, ld.ProjectOverrides, ld.ImportOverrides)
   598  		if err != nil {
   599  			return err
   600  		}
   601  		// normalize project attributes
   602  		project.ComputedAttributes = newAttributes(project.Attributes)
   603  		project.Attributes = project.ComputedAttributes.String()
   604  		// Make paths absolute by prepending <root>.
   605  		project.absolutizePaths(filepath.Join(jirix.Root, root))
   606  
   607  		if hooks, ok := hookMap[project.Name]; ok {
   608  			for _, hook := range hooks {
   609  				hook.ActionPath = project.Path
   610  			}
   611  		}
   612  
   613  		// Prepend the root to the project name.  This will be a noop if the import is not rooted.
   614  		project.Name = filepath.Join(root, project.Name)
   615  		key := project.Key()
   616  
   617  		if r, ok := ld.importProjects[key]; ok {
   618  			// update revision for this project
   619  			if r.Revision != "" && r.Revision != "HEAD" {
   620  				if project.Revision == "" || project.Revision == "HEAD" {
   621  					project.Revision = r.Revision
   622  				} else if r.Revision != project.Revision {
   623  					return fmt.Errorf("project %q found in %q defines different revision than its corresponding import tag.", key, shortFileName(jirix.Root, repoPath, file, ref))
   624  				}
   625  			}
   626  		}
   627  
   628  		if dup, ok := ld.Projects[key]; ok && !reflect.DeepEqual(dup, project) {
   629  			// TODO(toddw): Tell the user the other conflicting file.
   630  			return fmt.Errorf("duplicate project %q found in %q", key, shortFileName(jirix.Root, repoPath, file, ref))
   631  		}
   632  
   633  		// Record manifest location.
   634  		project.ManifestPath = f
   635  
   636  		// Associate project with importTreeNode for git attributes propagation.
   637  		ld.importTree.projectKeyMap[key] = self
   638  
   639  		ld.Projects[key] = project
   640  	}
   641  
   642  	for _, hook := range m.Hooks {
   643  		if hook.ActionPath == "" {
   644  			return fmt.Errorf("invalid hook %q for project %q. Please make sure you are importing project %q and this hook is in the manifest which directly/indirectly imports that project.", hook.Name, hook.ProjectName, hook.ProjectName)
   645  		}
   646  		key := hook.Key()
   647  		ld.Hooks[key] = hook
   648  	}
   649  
   650  	for _, pkg := range m.Packages {
   651  		// normalize package attributes.
   652  		pkg.ComputedAttributes = newAttributes(pkg.Attributes)
   653  		pkg.Attributes = pkg.ComputedAttributes.String()
   654  		// Record manifest location.
   655  		pkg.ManifestPath = f
   656  		key := pkg.Key()
   657  		ld.Packages[key] = pkg
   658  	}
   659  	return nil
   660  }
   661  
   662  func (ld *loader) loadImport(jirix *jiri.X, root, file, cycleKey, cacheDirPath, parentImport string, project Project, localManifest bool) (e error) {
   663  	lm := localManifest
   664  	ref := ""
   665  
   666  	if v, ok := ld.importCacheMap[strings.Trim(project.Remote, "/")]; ok {
   667  		// local manifest in cache takes precedence as this might be manifest mentioned in .jiri_manifest
   668  		lm = v.localManifest
   669  		ref = v.ref
   670  		// check conflicting imports
   671  		if !lm && ref != "JIRI_HEAD" {
   672  			if tref, err := GetHeadRevision(jirix, project); err != nil {
   673  				return err
   674  			} else if tref != ref {
   675  				return fmt.Errorf("Conflicting ref for import %s - %q and %q. There are conflicting imports in file:\n%s:\n'%s' and '%s'",
   676  					jirix.Color.Red(project.Remote), ref, tref, jirix.Color.Yellow(ld.parentFile),
   677  					jirix.Color.Yellow(v.parentImport), jirix.Color.Yellow(parentImport))
   678  			}
   679  		}
   680  	} else {
   681  		// We don't need to fetch or find ref for local manifest changes
   682  		if !lm {
   683  			// We only fetch on updates.
   684  			if ld.update {
   685  				// Fetch only if project not pinned or revision not available in
   686  				// local git as we anyways update all the projects later.
   687  				fetch := true
   688  				if project.Revision != "" && project.Revision != "HEAD" {
   689  					if _, err := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)).Show(project.Revision, ""); err == nil {
   690  						fetch = false
   691  					}
   692  				}
   693  				if fetch {
   694  					if cacheDirPath != "" {
   695  						remoteUrl := rewriteRemote(jirix, project.Remote)
   696  						if err := updateOrCreateCache(jirix, cacheDirPath, remoteUrl, project.RemoteBranch, project.Revision, 0); err != nil {
   697  							return err
   698  						}
   699  					}
   700  					if err := fetchAll(jirix, project); err != nil {
   701  						return fmt.Errorf("Fetch failed for project(%s), %s", project.Path, err)
   702  					}
   703  				}
   704  			} else {
   705  				// If not updating then try to get file from JIRI_HEAD
   706  				if _, err := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)).Show("JIRI_HEAD", ""); err == nil {
   707  					// JIRI_HEAD available, set ref
   708  					ref = "JIRI_HEAD"
   709  				}
   710  			}
   711  			if ref == "" {
   712  				var err error
   713  				if ref, err = GetHeadRevision(jirix, project); err != nil {
   714  					return err
   715  				}
   716  			}
   717  		}
   718  		ld.importCacheMap[strings.Trim(project.Remote, "/")] = importCache{
   719  			localManifest: lm,
   720  			ref:           ref,
   721  			parentImport:  parentImport,
   722  		}
   723  	}
   724  	if lm {
   725  		// load from local checked out file
   726  		return ld.Load(jirix, root, "", filepath.Join(project.Path, file), "", cycleKey, parentImport, false)
   727  	}
   728  	return ld.Load(jirix, root, project.Path, file, ref, cycleKey, parentImport, false)
   729  }
   730  
   731  func (ld *loader) GenerateGitAttributesForProjects(jirix *jiri.X) {
   732  	ld.importTree.buildImportAttributes()
   733  	for k, v := range ld.Projects {
   734  		if treeNode, ok := ld.importTree.projectKeyMap[k]; ok {
   735  			v.GitAttributes = treeNode.computedAttributes.String()
   736  			ld.Projects[k] = v
   737  		}
   738  	}
   739  	jirix.Logger.Debugf("Generated dot file: %s", ld.importTree.generateAttributeGraph())
   740  }