github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cmd/jiri/generate-gitmodules.go (about)

     1  // Copyright 2019 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 main
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  
    16  	"github.com/btwiuse/jiri"
    17  	"github.com/btwiuse/jiri/cmdline"
    18  	"github.com/btwiuse/jiri/project"
    19  )
    20  
    21  var cmdGenGitModule = &cmdline.Command{
    22  	Runner: jiri.RunnerFunc(runGenGitModule),
    23  	Name:   "generate-gitmodules",
    24  	Short:  "Create a .gitmodule and a .gitattributes files for git submodule repository",
    25  	Long: `
    26  The "jiri generate-gitmodules command captures the current project state and
    27  create a .gitmodules file and an optional .gitattributes file for building
    28  a git submodule based super repository.
    29  `,
    30  	ArgsName: "<.gitmodule path> [<.gitattributes path>]",
    31  	ArgsLong: `
    32  <.gitmodule path> is the path to the output .gitmodule file.
    33  <.gitattributes path> is the path to the output .gitattribute file, which is optional.`,
    34  }
    35  
    36  var genGitModuleFlags struct {
    37  	genScript    string
    38  	redirectRoot bool
    39  }
    40  
    41  func init() {
    42  	flags := &cmdGenGitModule.Flags
    43  	flags.StringVar(&genGitModuleFlags.genScript, "generate-script", "", "File to save generated git commands for seting up a superproject.")
    44  	flags.BoolVar(&genGitModuleFlags.redirectRoot, "redir-root", false, "When set to true, jiri will add the root repository as a submodule into {name}-mirror directory and create necessary setup commands in generated script.")
    45  }
    46  
    47  type projectTree struct {
    48  	project  *project.Project
    49  	children map[string]*projectTree
    50  }
    51  
    52  type projectTreeRoot struct {
    53  	root    *projectTree
    54  	dropped project.Projects
    55  }
    56  
    57  func runGenGitModule(jirix *jiri.X, args []string) error {
    58  	gitmodulesPath := ".gitmodules"
    59  	gitattributesPath := ""
    60  	if len(args) >= 1 {
    61  		gitmodulesPath = args[0]
    62  	}
    63  	if len(args) == 2 {
    64  		gitattributesPath = args[1]
    65  	}
    66  
    67  	if len(args) > 2 {
    68  		return jirix.UsageErrorf("unexpected number of arguments")
    69  	}
    70  
    71  	localProjects, err := project.LocalProjects(jirix, project.FullScan)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	return writeGitModules(jirix, localProjects, gitmodulesPath, gitattributesPath)
    76  }
    77  
    78  func (p *projectTreeRoot) add(jirix *jiri.X, proj project.Project) error {
    79  	if p == nil || p.root == nil {
    80  		return errors.New("add called with nil root pointer")
    81  	}
    82  
    83  	if proj.Path == "." || proj.Path == "" || proj.Path == string(filepath.Separator) {
    84  		// Skip fuchsia.git project
    85  		p.dropped[proj.Key()] = proj
    86  		return nil
    87  	}
    88  
    89  	// git submodule does not support one submodule to be placed under the path
    90  	// of another submodule, therefore, it is necessary to detect nested
    91  	// projects in jiri manifests and drop them from gitmodules file.
    92  	//
    93  	// The nested project detection is based on only 1 rule:
    94  	// If the path of project A (pathA) is the parent directory of project B,
    95  	// project B will be considered as nested under project A. It will be recorded
    96  	// in "dropped" map.
    97  	//
    98  	// Due to the introduction of fuchsia.git, based on the rule above, all
    99  	// other projects will be considered as nested project under fuchsia.git,
   100  	// therefore, fuchsia.git is excluded in this detection process.
   101  	//
   102  	// The detection algorithm works in following ways:
   103  	//
   104  	// Assuming we have two project: "projA" and "projB", "projA" is located at
   105  	// "$JIRI_ROOT/a" and projB is located as "$JIRI_ROOT/b/c".
   106  	// The projectTree will look like the following chart:
   107  	//
   108  	//                   a    +-------+
   109  	//               +--------+ projA |
   110  	//               |        +-------+
   111  	// +---------+   |
   112  	// |nil(root)+---+
   113  	// +---------+   |
   114  	//               |   b    +-------+   c   +-------+
   115  	//               +--------+  nil  +-------+ projB |
   116  	//                        +-------+       +-------+
   117  	//
   118  	// The text inside each block represents the projectTree.project field,
   119  	// each edge represents a key of projectTree.children field.
   120  	//
   121  	// Assuming we adds project "projC" whose path is "$JIRI_ROOT/a/d", it will
   122  	// be dropped as the children of root already have key "a" and
   123  	// children["a"].project is not pointed to nil, which means "projC" is
   124  	// nested under "projA".
   125  	//
   126  	// Assuming we adds project "projD" whose path is "$JIRI_ROOT/d", it will
   127  	// be added successfully since root.children does not have key "d" yet,
   128  	// which means "projD" is not nested under any known project and no project
   129  	// is currently nested under "projD" yet.
   130  	//
   131  	// Assuming we adds project "projE" whose path is "$JIRI_ROOT/b", it will
   132  	// be added successfully and "projB" will be dropped. The reason is that
   133  	// root.children["b"].project is nil but root.children["b"].children is not
   134  	// empty, so any projects that can be reached from root.children["b"]
   135  	// should be dropped as they are nested under "projE".
   136  	elmts := strings.Split(proj.Path, string(filepath.Separator))
   137  	pin := p.root
   138  	for i := 0; i < len(elmts); i++ {
   139  		if child, ok := pin.children[elmts[i]]; ok {
   140  			if child.project != nil {
   141  				// proj is nested under next.project, drop proj
   142  				jirix.Logger.Debugf("project %q:%q nested under project %q:%q", proj.Path, proj.Remote, proj.Path, child.project.Remote)
   143  				p.dropped[proj.Key()] = proj
   144  				return nil
   145  			}
   146  			pin = child
   147  		} else {
   148  			child = &projectTree{nil, make(map[string]*projectTree)}
   149  			pin.children[elmts[i]] = child
   150  			pin = child
   151  		}
   152  	}
   153  	if len(pin.children) != 0 {
   154  		// There is one or more project nested under proj.
   155  		jirix.Logger.Debugf("following project nested under project %q:%q", proj.Path, proj.Remote)
   156  		if err := p.prune(jirix, pin); err != nil {
   157  			return err
   158  		}
   159  		jirix.Logger.Debugf("\n")
   160  	}
   161  	pin.project = &proj
   162  	return nil
   163  }
   164  
   165  func (p *projectTreeRoot) prune(jirix *jiri.X, node *projectTree) error {
   166  	// Looking for projects nested under node using BFS
   167  	workList := make([]*projectTree, 0)
   168  	workList = append(workList, node)
   169  
   170  	for len(workList) > 0 {
   171  		item := workList[0]
   172  		if item == nil {
   173  			return errors.New("purgeLeaves encountered a nil node")
   174  		}
   175  		workList = workList[1:]
   176  		if item.project != nil {
   177  			p.dropped[item.project.Key()] = *item.project
   178  			jirix.Logger.Debugf("\tnested project %q:%q", item.project.Path, item.project.Remote)
   179  		}
   180  		for _, v := range item.children {
   181  			workList = append(workList, v)
   182  		}
   183  	}
   184  
   185  	// Purge leaves under node
   186  	node.children = make(map[string]*projectTree)
   187  	return nil
   188  }
   189  
   190  func writeGitModules(jirix *jiri.X, projects project.Projects, gitmodulesPath, gitattributesPath string) error {
   191  	projEntries := make([]project.Project, len(projects))
   192  
   193  	// relativaize the paths and copy projects from map to slice for sorting.
   194  	i := 0
   195  	for _, v := range projects {
   196  		relPath, err := makePathRel(jirix.Root, v.Path)
   197  		if err != nil {
   198  			return err
   199  		}
   200  		v.Path = relPath
   201  		projEntries[i] = v
   202  		i++
   203  	}
   204  	sort.Slice(projEntries, func(i, j int) bool {
   205  		return string(projEntries[i].Key()) < string(projEntries[j].Key())
   206  	})
   207  
   208  	// Create path prefix tree to collect all nested projects
   209  	root := projectTree{nil, make(map[string]*projectTree)}
   210  	treeRoot := projectTreeRoot{&root, make(project.Projects)}
   211  	for _, v := range projEntries {
   212  		if err := treeRoot.add(jirix, v); err != nil {
   213  			return err
   214  		}
   215  	}
   216  
   217  	// Start creating .gitmodule and set up script.
   218  	var gitmoduleBuf bytes.Buffer
   219  	var commandBuf bytes.Buffer
   220  	var gitattributeBuf bytes.Buffer
   221  	commandBuf.WriteString("#!/bin/sh\n")
   222  
   223  	// Special hack for fuchsia.git
   224  	// When -redir-root is set to true, fuchsia.git will be added as submodule
   225  	// to fuchsia-mirror directory
   226  	reRootRepoName := ""
   227  	if genGitModuleFlags.redirectRoot {
   228  		// looking for root repository, there should be no more than 1
   229  		rIndex := -1
   230  		for i, v := range projEntries {
   231  			if v.Path == "." || v.Path == "" || v.Path == string(filepath.Separator) {
   232  				if rIndex == -1 {
   233  					rIndex = i
   234  				} else {
   235  					return fmt.Errorf("more than 1 project defined at path \".\", projects %+v:%+v", projEntries[rIndex], projEntries[i])
   236  				}
   237  			}
   238  		}
   239  		if rIndex != -1 {
   240  			v := projEntries[rIndex]
   241  			v.Name = v.Name + "-mirror"
   242  			v.Path = v.Name
   243  			reRootRepoName = v.Path
   244  			gitmoduleBuf.WriteString(moduleDecl(v))
   245  			gitmoduleBuf.WriteString("\n")
   246  			commandBuf.WriteString(commandDecl(v))
   247  			commandBuf.WriteString("\n")
   248  			if v.GitAttributes != "" {
   249  				gitattributeBuf.WriteString(attributeDecl(v))
   250  				gitattributeBuf.WriteString("\n")
   251  			}
   252  		}
   253  	}
   254  
   255  	for _, v := range projEntries {
   256  		if reRootRepoName != "" && reRootRepoName == v.Path {
   257  			return fmt.Errorf("path collision for root repo and project %+v", v)
   258  		}
   259  		if _, ok := treeRoot.dropped[v.Key()]; ok {
   260  			jirix.Logger.Debugf("dropped project %+v", v)
   261  			continue
   262  		}
   263  		gitmoduleBuf.WriteString(moduleDecl(v))
   264  		gitmoduleBuf.WriteString("\n")
   265  		commandBuf.WriteString(commandDecl(v))
   266  		commandBuf.WriteString("\n")
   267  		if v.GitAttributes != "" {
   268  			gitattributeBuf.WriteString(attributeDecl(v))
   269  			gitattributeBuf.WriteString("\n")
   270  		}
   271  	}
   272  	jirix.Logger.Debugf("generated gitmodule content \n%v\n", gitmoduleBuf.String())
   273  	if err := ioutil.WriteFile(gitmodulesPath, gitmoduleBuf.Bytes(), 0644); err != nil {
   274  		return err
   275  	}
   276  
   277  	if genGitModuleFlags.genScript != "" {
   278  		jirix.Logger.Debugf("generated set up script for gitmodule content \n%v\n", commandBuf.String())
   279  		if err := ioutil.WriteFile(genGitModuleFlags.genScript, commandBuf.Bytes(), 0755); err != nil {
   280  			return err
   281  		}
   282  	}
   283  
   284  	if gitattributesPath != "" {
   285  		jirix.Logger.Debugf("generated gitattributes content \n%v\n", gitattributeBuf.String())
   286  		if err := ioutil.WriteFile(gitattributesPath, gitattributeBuf.Bytes(), 0644); err != nil {
   287  			return err
   288  		}
   289  	}
   290  	return nil
   291  }
   292  
   293  func makePathRel(basepath, targpath string) (string, error) {
   294  	if filepath.IsAbs(targpath) {
   295  		relPath, err := filepath.Rel(basepath, targpath)
   296  		if err != nil {
   297  			return "", err
   298  		}
   299  		return relPath, nil
   300  	}
   301  	return targpath, nil
   302  }
   303  
   304  func moduleDecl(p project.Project) string {
   305  	tmpl := "[submodule \"%s\"]\n\tbranch = %s\n\tpath = %s\n\turl = %s"
   306  	return fmt.Sprintf(tmpl, p.Name, p.Revision, p.Path, p.Remote)
   307  }
   308  
   309  func commandDecl(p project.Project) string {
   310  	tmpl := "git update-index --add --cacheinfo 160000 %s \"%s\""
   311  	return fmt.Sprintf(tmpl, p.Revision, p.Path)
   312  }
   313  
   314  func attributeDecl(p project.Project) string {
   315  	tmpl := "%s %s"
   316  	attrs := strings.ReplaceAll(p.GitAttributes, ",", " ")
   317  	return fmt.Sprintf(tmpl, p.Path, strings.TrimSpace(attrs))
   318  }