go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/submodule_update/gitutil/git.go (about) 1 // Copyright 2023 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // Package gitutil is a wrapper layer for handing calls to the git tool. 6 package gitutil 7 8 import ( 9 "bytes" 10 "fmt" 11 "io" 12 "os" 13 "os/exec" 14 "sort" 15 "strconv" 16 "strings" 17 18 "github.com/golang/glog" 19 ) 20 21 // GitError structure for returning git process state. 22 type GitError struct { 23 Root string 24 Args []string 25 Output string 26 ErrorOutput string 27 err error 28 } 29 30 func gitError(output, errorOutput string, err error, root string, args ...string) GitError { 31 return GitError{ 32 Root: root, 33 Args: args, 34 Output: output, 35 ErrorOutput: errorOutput, 36 err: err, 37 } 38 } 39 40 // Error outputs GitError struct as a string. 41 func (ge GitError) Error() string { 42 lines := []string{ 43 fmt.Sprintf("(%s) 'git %s' failed:", ge.Root, strings.Join(ge.Args, " ")), 44 "stdout:", 45 ge.Output, 46 "stderr:", 47 ge.ErrorOutput, 48 "command fail error: " + ge.err.Error(), 49 } 50 return strings.Join(lines, "\n") 51 } 52 53 // Git structure for environmental options passed to git tool. 54 type Git struct { 55 opts map[string]string 56 rootDir string 57 submoduleDir string 58 userName string 59 userEmail string 60 } 61 62 type gitOpt interface { 63 gitOpt() 64 } 65 66 // AuthorDateOpt is the git author date. 67 type AuthorDateOpt string 68 69 // CommitterDateOpt is the git committer date. 70 type CommitterDateOpt string 71 72 // RootDirOpt is the git root directory. 73 type RootDirOpt string 74 75 // SubmoduleDirOpt is the relative path (from git root) to a submodule. 76 type SubmoduleDirOpt string 77 78 // UserNameOpt is the git username. 79 type UserNameOpt string 80 81 // UserEmailOpt is the git user email. 82 type UserEmailOpt string 83 84 func (AuthorDateOpt) gitOpt() {} 85 func (CommitterDateOpt) gitOpt() {} 86 func (RootDirOpt) gitOpt() {} 87 func (SubmoduleDirOpt) gitOpt() {} 88 func (UserNameOpt) gitOpt() {} 89 func (UserEmailOpt) gitOpt() {} 90 91 // Reference structure is a branch reference information. 92 type Reference struct { 93 Name string 94 Revision string 95 IsHead bool 96 } 97 98 // Branch structure tracks the state of a branch. 99 type Branch struct { 100 *Reference 101 Tracking *Reference 102 } 103 104 // Revision is a git revision. 105 type Revision string 106 107 // BranchName is a git branch name. 108 type BranchName string 109 110 // New is the Git factory. 111 func New(opts ...gitOpt) *Git { 112 rootDir := "" 113 submoduleDir := "" 114 userName := "" 115 userEmail := "" 116 env := map[string]string{} 117 for _, opt := range opts { 118 switch typedOpt := opt.(type) { 119 case AuthorDateOpt: 120 env["GIT_AUTHOR_DATE"] = string(typedOpt) 121 case CommitterDateOpt: 122 env["GIT_COMMITTER_DATE"] = string(typedOpt) 123 case RootDirOpt: 124 rootDir = string(typedOpt) 125 case SubmoduleDirOpt: 126 submoduleDir = string(typedOpt) 127 case UserNameOpt: 128 userName = string(typedOpt) 129 case UserEmailOpt: 130 userEmail = string(typedOpt) 131 } 132 } 133 return &Git{ 134 opts: env, 135 rootDir: rootDir, 136 submoduleDir: submoduleDir, 137 userName: userName, 138 userEmail: userEmail, 139 } 140 } 141 142 // RootDir returns the root directory of the Git object. 143 func (g *Git) RootDir() string { 144 return g.rootDir 145 } 146 147 // Update allows updating of an existing Git object. 148 func (g *Git) Update(opts ...gitOpt) { 149 for _, opt := range opts { 150 switch typedOpt := opt.(type) { 151 case AuthorDateOpt: 152 g.opts["GIT_AUTHOR_DATE"] = string(typedOpt) 153 case CommitterDateOpt: 154 g.opts["GIT_COMMITTER_DATE"] = string(typedOpt) 155 case RootDirOpt: 156 g.rootDir = string(typedOpt) 157 case SubmoduleDirOpt: 158 g.submoduleDir = string(typedOpt) 159 case UserNameOpt: 160 g.userName = string(typedOpt) 161 case UserEmailOpt: 162 g.userEmail = string(typedOpt) 163 } 164 } 165 } 166 167 // AddAllFiles adds/updates all file in working tree to staging. 168 func (g *Git) AddAllFiles() error { 169 return g.run("add", "-A") 170 } 171 172 // CheckoutBranch checks out the given branch. 173 func (g *Git) CheckoutBranch(branch string, gitSubmodules bool, opts ...CheckoutOpt) error { 174 args := []string{"checkout"} 175 var force ForceOpt = false 176 var detach DetachOpt = false 177 for _, opt := range opts { 178 switch typedOpt := opt.(type) { 179 case ForceOpt: 180 force = typedOpt 181 case DetachOpt: 182 detach = typedOpt 183 } 184 } 185 if force { 186 args = append(args, "-f") 187 } 188 if detach { 189 args = append(args, "--detach") 190 } 191 192 if gitSubmodules { 193 args = append(args, "--recurse-submodules") 194 } 195 196 args = append(args, branch) 197 if err := g.run(args...); err != nil { 198 return err 199 } 200 // After checkout with submodules update/checkout submodules. 201 if gitSubmodules { 202 return g.SubmoduleUpdate(nil, InitOpt(true)) 203 } 204 return nil 205 } 206 207 // SubmoduleAdd adds submodule to current branch. 208 func (g *Git) SubmoduleAdd(remote string, path string) error { 209 // Use -f to add submodules even if in .gitignore. 210 return g.run("submodule", "add", "-f", remote, path) 211 } 212 213 // SubmoduleStatus returns current current status of submodules in a superproject. 214 func (g *Git) SubmoduleStatus(opts ...SubmoduleStatusOpt) (string, error) { 215 args := []string{"submodule", "status"} 216 for _, opt := range opts { 217 switch typedOpt := opt.(type) { 218 case CachedOpt: 219 if typedOpt { 220 args = append(args, "--cached") 221 } 222 } 223 } 224 out, err := g.runOutput(args...) 225 if err != nil { 226 return "", err 227 } 228 return strings.Join(out, "\n"), nil 229 } 230 231 // SubmoduleUpdate updates submodules for current branch. 232 func (g *Git) SubmoduleUpdate(paths []string, opts ...SubmoduleUpdateOpt) error { 233 args := []string{"submodule", "update"} 234 for _, opt := range opts { 235 switch typedOpt := opt.(type) { 236 case InitOpt: 237 if typedOpt { 238 args = append(args, "--init") 239 } 240 } 241 } 242 args = append(args, "--jobs=50") 243 args = append(args, paths...) 244 return g.run(args...) 245 246 } 247 248 // Clone clones the given repository to the given local path. If reference is 249 // not empty it uses the given path as a reference/shared repo. 250 func (g *Git) Clone(repo, path string, opts ...CloneOpt) error { 251 args := []string{"clone"} 252 for _, opt := range opts { 253 switch typedOpt := opt.(type) { 254 case BareOpt: 255 if typedOpt { 256 args = append(args, "--bare") 257 } 258 case ReferenceOpt: 259 reference := string(typedOpt) 260 if reference != "" { 261 args = append(args, []string{"--reference-if-able", reference}...) 262 } 263 case SharedOpt: 264 if typedOpt { 265 args = append(args, []string{"--shared", "--local"}...) 266 } 267 case NoCheckoutOpt: 268 if typedOpt { 269 args = append(args, "--no-checkout") 270 } 271 case DepthOpt: 272 if typedOpt > 0 { 273 args = append(args, []string{"--depth", strconv.Itoa(int(typedOpt))}...) 274 } 275 case OmitBlobsOpt: 276 if typedOpt { 277 args = append(args, "--filter=blob:none") 278 } 279 case OffloadPackfilesOpt: 280 if typedOpt { 281 args = append([]string{"-c", "fetch.uriprotocols=https"}, args...) 282 } 283 case RecurseSubmodulesOpt: 284 // TODO(iankaz): Add setting submodule.fetchJobs in git config to jiri init 285 if typedOpt { 286 args = append(args, []string{"--recurse-submodules", "--jobs=16"}...) 287 } 288 } 289 } 290 args = append(args, repo) 291 args = append(args, path) 292 return g.run(args...) 293 } 294 295 // CommitWithMessage commits all files in staging with the given 296 // message. 297 func (g *Git) CommitWithMessage(message string) error { 298 return g.run("commit", "--allow-empty", "--allow-empty-message", "-m", message) 299 } 300 301 // GetSymbolicRef returns which branch working tree (HEAD) is on. 302 func (g *Git) GetSymbolicRef() (string, error) { 303 out, err := g.runOutput("symbolic-ref", "-q", "HEAD") 304 if err != nil { 305 return "", err 306 } 307 if got, want := len(out), 1; got != want { 308 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 309 } 310 return out[0], nil 311 } 312 313 // Fetch fetches refs and tags from the given remote. 314 func (g *Git) Fetch(remote string, opts ...FetchOpt) error { 315 return g.FetchRefspec(remote, "", opts...) 316 } 317 318 // FetchRefspec fetches refs and tags from the given remote for a particular refspec. 319 func (g *Git) FetchRefspec(remote, refspec string, opts ...FetchOpt) error { 320 tags := false 321 all := false 322 prune := false 323 updateShallow := false 324 depth := 0 325 fetchTag := "" 326 updateHeadOk := false 327 recurseSubmodules := false 328 jobs := uint(0) 329 for _, opt := range opts { 330 switch typedOpt := opt.(type) { 331 case TagsOpt: 332 tags = bool(typedOpt) 333 case AllOpt: 334 all = bool(typedOpt) 335 case PruneOpt: 336 prune = bool(typedOpt) 337 case DepthOpt: 338 depth = int(typedOpt) 339 case UpdateShallowOpt: 340 updateShallow = bool(typedOpt) 341 case FetchTagOpt: 342 fetchTag = string(typedOpt) 343 case UpdateHeadOkOpt: 344 updateHeadOk = bool(typedOpt) 345 case RecurseSubmodulesOpt: 346 recurseSubmodules = bool(typedOpt) 347 case JobsOpt: 348 jobs = uint(typedOpt) 349 } 350 } 351 args := []string{} 352 args = append(args, "fetch") 353 if prune { 354 args = append(args, "-p") 355 } 356 if tags { 357 args = append(args, "--tags") 358 } 359 if depth > 0 { 360 args = append(args, "--depth", strconv.Itoa(depth)) 361 } 362 if updateShallow { 363 args = append(args, "--update-shallow") 364 } 365 if all { 366 args = append(args, "--all") 367 } 368 if updateHeadOk { 369 args = append(args, "--update-head-ok") 370 } 371 if recurseSubmodules { 372 args = append(args, "--recurse-submodules") 373 } 374 if jobs > 0 { 375 args = append(args, "--jobs="+strconv.FormatUint(uint64(jobs), 10)) 376 } 377 if remote != "" { 378 args = append(args, remote) 379 } 380 if fetchTag != "" { 381 args = append(args, "tag", fetchTag) 382 } 383 if refspec != "" { 384 args = append(args, refspec) 385 } 386 387 return g.run(args...) 388 } 389 390 // FilesWithUncommittedChanges returns the list of files that have 391 // uncommitted changes. 392 func (g *Git) FilesWithUncommittedChanges() ([]string, error) { 393 out, err := g.runOutput("diff", "--name-only", "--no-ext-diff") 394 if err != nil { 395 return nil, err 396 } 397 out2, err := g.runOutput("diff", "--cached", "--name-only", "--no-ext-diff") 398 if err != nil { 399 return nil, err 400 } 401 return append(out, out2...), nil 402 } 403 404 // Remove removes the given files. 405 func (g *Git) Remove(fileNames ...string) error { 406 args := []string{"rm"} 407 args = append(args, fileNames...) 408 return g.run(args...) 409 } 410 411 // ConfigGetKey gets current git configuration value for the given key. 412 func (g *Git) ConfigGetKey(key string) (string, error) { 413 out, err := g.runOutput("config", "--get", key) 414 if err != nil { 415 return "", err 416 } 417 if got, want := len(out), 1; got != want { 418 glog.Warning("wanted one line log, got %d line log: %q", got, out) 419 } 420 return out[0], nil 421 } 422 423 // ConfigGetKeyFromFile gets current git configuration value for the given key from file. 424 // 425 // Returns an empty string if the configuration value is not found. 426 func (g *Git) ConfigGetKeyFromFile(key string, file string) (string, error) { 427 out, err := g.runOutput("config", "--file", file, "--default", "", "--get", key) 428 if err != nil { 429 return "", err 430 } 431 if len(out) == 0 { 432 return "", nil 433 } 434 if len(out) > 1 { 435 glog.Warning("wanted one line log, got %d line log: %q", len(out), out) 436 } 437 return out[0], nil 438 } 439 440 // ConfigAddKeyToFile adds additional git configuration value to file. 441 func (g *Git) ConfigAddKeyToFile(key string, file string, value string) error { 442 return g.run("config", "--file", file, key, value) 443 } 444 445 func (g *Git) run(args ...string) error { 446 var stdout, stderr bytes.Buffer 447 if err := g.runGit(&stdout, &stderr, args...); err != nil { 448 return gitError(stdout.String(), stderr.String(), err, g.rootDir, args...) 449 } 450 return nil 451 } 452 453 func trimOutput(o string) []string { 454 output := strings.TrimSpace(o) 455 if len(output) == 0 { 456 return nil 457 } 458 return strings.Split(output, "\n") 459 } 460 461 func (g *Git) runOutput(args ...string) ([]string, error) { 462 var stdout, stderr bytes.Buffer 463 if err := g.runGit(&stdout, &stderr, args...); err != nil { 464 return nil, gitError(stdout.String(), stderr.String(), err, g.rootDir, args...) 465 } 466 return trimOutput(stdout.String()), nil 467 } 468 469 func (g *Git) runGit(stdout, stderr io.Writer, args ...string) error { 470 if g.submoduleDir != "" { 471 args = append([]string{"-C", g.submoduleDir}, args...) 472 } 473 if g.userName != "" { 474 args = append([]string{"-c", fmt.Sprintf("user.name=%s", g.userName)}, args...) 475 } 476 if g.userEmail != "" { 477 args = append([]string{"-c", fmt.Sprintf("user.email=%s", g.userEmail)}, args...) 478 } 479 var outbuf bytes.Buffer 480 var errbuf bytes.Buffer 481 command := exec.Command("git", args...) 482 command.Stdin = os.Stdin 483 command.Stdout = io.MultiWriter(stdout, &outbuf) 484 command.Stderr = io.MultiWriter(stderr, &errbuf) 485 env := sliceToMap(os.Environ()) 486 env = mergeMaps(g.opts, env) 487 command.Env = mapToSlice(env) 488 dir := g.rootDir 489 if dir == "" { 490 // Use working directory 491 if cwd, err := os.Getwd(); err == nil { 492 dir = cwd 493 } 494 } 495 command.Dir = dir 496 err := command.Run() 497 return err 498 } 499 500 // splitKeyValue splits kv into its key and value components. The format of kv 501 // is "key=value"; the split is performed on the first '=' character. 502 func splitKeyValue(kv string) (string, string) { 503 split := strings.SplitN(kv, "=", 2) 504 if len(split) == 2 { 505 return split[0], split[1] 506 } 507 return split[0], "" 508 } 509 510 type keySorter []string 511 512 func (s keySorter) Len() int { return len(s) } 513 func (s keySorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 514 func (s keySorter) Less(i, j int) bool { 515 ikey, _ := splitKeyValue(s[i]) 516 jkey, _ := splitKeyValue(s[j]) 517 return ikey < jkey 518 } 519 520 // sortByKey sorts vars into ascending key order, where vars is expected to be 521 // in the []"key=value" slice representation. 522 func sortByKey(vars []string) { 523 sort.Sort(keySorter(vars)) 524 } 525 526 // joinKeyValue joins key and value into a single string "key=value". 527 func joinKeyValue(key, value string) string { 528 return key + "=" + value 529 } 530 531 // mapToSlice converts from the map to the slice representation. The returned 532 // slice is in sorted order. 533 func mapToSlice(from map[string]string) []string { 534 to := make([]string, 0, len(from)) 535 for key, value := range from { 536 if key != "" { 537 to = append(to, joinKeyValue(key, value)) 538 } 539 } 540 sortByKey(to) 541 return to 542 } 543 544 // mergeMaps merges together maps, and returns a new map with the merged result. 545 // 546 // As a result of its semantics, mergeMaps called with a single map returns a 547 // copy of the map, with empty keys dropped. 548 func mergeMaps(maps ...map[string]string) map[string]string { 549 merged := make(map[string]string) 550 for _, m := range maps { 551 for key, value := range m { 552 if key != "" { 553 merged[key] = value 554 } 555 } 556 } 557 return merged 558 } 559 560 // SliceToMap converts from the slice to the map representation. If the same 561 // key appears more than once, the last one "wins"; the value is set based on 562 // the last slice element containing that key. 563 func sliceToMap(from []string) map[string]string { 564 to := make(map[string]string, len(from)) 565 for _, kv := range from { 566 if key, value := splitKeyValue(kv); key != "" { 567 to[key] = value 568 } 569 } 570 return to 571 }