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 }