v.io/jiri@v0.0.0-20160715023856-abfb8b131290/gitutil/git.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 gitutil 6 7 import ( 8 "bytes" 9 "fmt" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strconv" 14 "strings" 15 16 "v.io/jiri/runutil" 17 ) 18 19 // PlatformSpecificGitArgs returns a git command line with platform specific, 20 // if any, modifications. The code is duplicated here because of the dependency 21 // structure in the jiri tool. 22 // TODO(cnicolaou,bprosnitz): remove this once ssl certs are installed. 23 func platformSpecificGitArgs(args ...string) []string { 24 if os.Getenv("FNL_SYSTEM") != "" { 25 // TODO(bprosnitz) Remove this after certificates are installed on FNL 26 // Disable SSL verification because certificates are not present on FNL.func 27 return append([]string{"-c", "http.sslVerify=false"}, args...) 28 } 29 return args 30 } 31 32 type GitError struct { 33 args []string 34 output string 35 errorOutput string 36 } 37 38 func Error(output, errorOutput string, args ...string) GitError { 39 return GitError{ 40 args: args, 41 output: output, 42 errorOutput: errorOutput, 43 } 44 } 45 46 func (ge GitError) Error() string { 47 result := "'git " 48 result += strings.Join(ge.args, " ") 49 result += "' failed:\n" 50 result += ge.errorOutput 51 return result 52 } 53 54 type Git struct { 55 s runutil.Sequence 56 opts map[string]string 57 rootDir string 58 } 59 60 type gitOpt interface { 61 gitOpt() 62 } 63 type AuthorDateOpt string 64 type CommitterDateOpt string 65 type RootDirOpt string 66 67 func (AuthorDateOpt) gitOpt() {} 68 func (CommitterDateOpt) gitOpt() {} 69 func (RootDirOpt) gitOpt() {} 70 71 // New is the Git factory. 72 func New(s runutil.Sequence, opts ...gitOpt) *Git { 73 rootDir := "" 74 env := map[string]string{} 75 for _, opt := range opts { 76 switch typedOpt := opt.(type) { 77 case AuthorDateOpt: 78 env["GIT_AUTHOR_DATE"] = string(typedOpt) 79 case CommitterDateOpt: 80 env["GIT_COMMITTER_DATE"] = string(typedOpt) 81 case RootDirOpt: 82 rootDir = string(typedOpt) 83 } 84 } 85 return &Git{ 86 s: s, 87 opts: env, 88 rootDir: rootDir, 89 } 90 } 91 92 // Add adds a file to staging. 93 func (g *Git) Add(file string) error { 94 return g.run("add", file) 95 } 96 97 // AddRemote adds a new remote with the given name and path. 98 func (g *Git) AddRemote(name, path string) error { 99 return g.run("remote", "add", name, path) 100 } 101 102 // BranchExists tests whether a branch with the given name exists in 103 // the local repository. 104 func (g *Git) BranchExists(branch string) bool { 105 return g.run("show-branch", branch) == nil 106 } 107 108 // BranchesDiffer tests whether two branches have any changes between them. 109 func (g *Git) BranchesDiffer(branch1, branch2 string) (bool, error) { 110 out, err := g.runOutput("--no-pager", "diff", "--name-only", branch1+".."+branch2) 111 if err != nil { 112 return false, err 113 } 114 // If output is empty, then there is no difference. 115 if len(out) == 0 { 116 return false, nil 117 } 118 // Otherwise there is a difference. 119 return true, nil 120 } 121 122 // CheckoutBranch checks out the given branch. 123 func (g *Git) CheckoutBranch(branch string, opts ...CheckoutOpt) error { 124 args := []string{"checkout"} 125 force := false 126 for _, opt := range opts { 127 switch typedOpt := opt.(type) { 128 case ForceOpt: 129 force = bool(typedOpt) 130 } 131 } 132 if force { 133 args = append(args, "-f") 134 } 135 args = append(args, branch) 136 return g.run(args...) 137 } 138 139 // Clone clones the given repository to the given local path. 140 func (g *Git) Clone(repo, path string) error { 141 return g.run("clone", repo, path) 142 } 143 144 // CloneRecursive clones the given repository recursively to the given local path. 145 func (g *Git) CloneRecursive(repo, path string) error { 146 return g.run("clone", "--recursive", repo, path) 147 } 148 149 // Commit commits all files in staging with an empty message. 150 func (g *Git) Commit() error { 151 return g.run("commit", "--allow-empty", "--allow-empty-message", "--no-edit") 152 } 153 154 // CommitAmend amends the previous commit with the currently staged 155 // changes. Empty commits are allowed. 156 func (g *Git) CommitAmend() error { 157 return g.run("commit", "--amend", "--allow-empty", "--no-edit") 158 } 159 160 // CommitAmendWithMessage amends the previous commit with the 161 // currently staged changes, and the given message. Empty commits are 162 // allowed. 163 func (g *Git) CommitAmendWithMessage(message string) error { 164 return g.run("commit", "--amend", "--allow-empty", "-m", message) 165 } 166 167 // CommitAndEdit commits all files in staging and allows the user to 168 // edit the commit message. 169 func (g *Git) CommitAndEdit() error { 170 args := []string{"commit", "--allow-empty"} 171 return g.runInteractive(args...) 172 } 173 174 // CommitFile commits the given file with the given commit message. 175 func (g *Git) CommitFile(fileName, message string) error { 176 if err := g.Add(fileName); err != nil { 177 return err 178 } 179 return g.CommitWithMessage(message) 180 } 181 182 // CommitMessages returns the concatenation of all commit messages on 183 // <branch> that are not also on <baseBranch>. 184 func (g *Git) CommitMessages(branch, baseBranch string) (string, error) { 185 out, err := g.runOutput("log", "--no-merges", baseBranch+".."+branch) 186 if err != nil { 187 return "", err 188 } 189 return strings.Join(out, "\n"), nil 190 } 191 192 // CommitNoVerify commits all files in staging with the given 193 // message and skips all git-hooks. 194 func (g *Git) CommitNoVerify(message string) error { 195 return g.run("commit", "--allow-empty", "--allow-empty-message", "--no-verify", "-m", message) 196 } 197 198 // CommitWithMessage commits all files in staging with the given 199 // message. 200 func (g *Git) CommitWithMessage(message string) error { 201 return g.run("commit", "--allow-empty", "--allow-empty-message", "-m", message) 202 } 203 204 // CommitWithMessage commits all files in staging and allows the user 205 // to edit the commit message. The given message will be used as the 206 // default. 207 func (g *Git) CommitWithMessageAndEdit(message string) error { 208 args := []string{"commit", "--allow-empty", "-e", "-m", message} 209 return g.runInteractive(args...) 210 } 211 212 // Committers returns a list of committers for the current repository 213 // along with the number of their commits. 214 func (g *Git) Committers() ([]string, error) { 215 out, err := g.runOutput("shortlog", "-s", "-n", "-e") 216 if err != nil { 217 return nil, err 218 } 219 return out, nil 220 } 221 222 // CountCommits returns the number of commits on <branch> that are not 223 // on <base>. 224 func (g *Git) CountCommits(branch, base string) (int, error) { 225 args := []string{"rev-list", "--count", branch} 226 if base != "" { 227 args = append(args, "^"+base) 228 } 229 args = append(args, "--") 230 out, err := g.runOutput(args...) 231 if err != nil { 232 return 0, err 233 } 234 if got, want := len(out), 1; got != want { 235 return 0, fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 236 } 237 count, err := strconv.Atoi(out[0]) 238 if err != nil { 239 return 0, fmt.Errorf("Atoi(%v) failed: %v", out[0], err) 240 } 241 return count, nil 242 } 243 244 // CreateBranch creates a new branch with the given name. 245 func (g *Git) CreateBranch(branch string) error { 246 return g.run("branch", branch) 247 } 248 249 // CreateAndCheckoutBranch creates a new branch with the given name 250 // and checks it out. 251 func (g *Git) CreateAndCheckoutBranch(branch string) error { 252 return g.run("checkout", "-b", branch) 253 } 254 255 // CreateBranchWithUpstream creates a new branch and sets the upstream 256 // repository to the given upstream. 257 func (g *Git) CreateBranchWithUpstream(branch, upstream string) error { 258 return g.run("branch", branch, upstream) 259 } 260 261 // CurrentBranchName returns the name of the current branch. 262 func (g *Git) CurrentBranchName() (string, error) { 263 out, err := g.runOutput("rev-parse", "--abbrev-ref", "HEAD") 264 if err != nil { 265 return "", err 266 } 267 if got, want := len(out), 1; got != want { 268 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 269 } 270 return out[0], nil 271 } 272 273 // CurrentRevision returns the current revision. 274 func (g *Git) CurrentRevision() (string, error) { 275 return g.CurrentRevisionOfBranch("HEAD") 276 } 277 278 // CurrentRevisionOfBranch returns the current revision of the given branch. 279 func (g *Git) CurrentRevisionOfBranch(branch string) (string, error) { 280 out, err := g.runOutput("rev-parse", branch) 281 if err != nil { 282 return "", err 283 } 284 if got, want := len(out), 1; got != want { 285 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 286 } 287 return out[0], nil 288 } 289 290 // DeleteBranch deletes the given branch. 291 func (g *Git) DeleteBranch(branch string, opts ...DeleteBranchOpt) error { 292 args := []string{"branch"} 293 force := false 294 for _, opt := range opts { 295 switch typedOpt := opt.(type) { 296 case ForceOpt: 297 force = bool(typedOpt) 298 } 299 } 300 if force { 301 args = append(args, "-D") 302 } else { 303 args = append(args, "-d") 304 } 305 args = append(args, branch) 306 return g.run(args...) 307 } 308 309 // DirExistsOnBranch returns true if a directory with the given name 310 // exists on the branch. If branch is empty it defaults to "master". 311 func (g *Git) DirExistsOnBranch(dir, branch string) bool { 312 if dir == "." { 313 dir = "" 314 } 315 if branch == "" { 316 branch = "master" 317 } 318 args := []string{"ls-tree", "-d", branch + ":" + dir} 319 return g.run(args...) == nil 320 } 321 322 // Fetch fetches refs and tags from the given remote. 323 func (g *Git) Fetch(remote string, opts ...FetchOpt) error { 324 return g.FetchRefspec(remote, "", opts...) 325 } 326 327 // FetchRefspec fetches refs and tags from the given remote for a particular refspec. 328 func (g *Git) FetchRefspec(remote, refspec string, opts ...FetchOpt) error { 329 args := []string{"fetch"} 330 tags := false 331 for _, opt := range opts { 332 switch typedOpt := opt.(type) { 333 case TagsOpt: 334 tags = bool(typedOpt) 335 } 336 } 337 if tags { 338 args = append(args, "--tags") 339 } 340 341 args = append(args, remote) 342 if refspec != "" { 343 args = append(args, refspec) 344 } 345 346 return g.run(args...) 347 } 348 349 // FilesWithUncommittedChanges returns the list of files that have 350 // uncommitted changes. 351 func (g *Git) FilesWithUncommittedChanges() ([]string, error) { 352 out, err := g.runOutput("diff", "--name-only", "--no-ext-diff") 353 if err != nil { 354 return nil, err 355 } 356 out2, err := g.runOutput("diff", "--cached", "--name-only", "--no-ext-diff") 357 if err != nil { 358 return nil, err 359 } 360 return append(out, out2...), nil 361 } 362 363 // GetBranches returns a slice of the local branches of the current 364 // repository, followed by the name of the current branch. The 365 // behavior can be customized by providing optional arguments 366 // (e.g. --merged). 367 func (g *Git) GetBranches(args ...string) ([]string, string, error) { 368 args = append([]string{"branch"}, args...) 369 out, err := g.runOutput(args...) 370 if err != nil { 371 return nil, "", err 372 } 373 branches, current := []string{}, "" 374 for _, branch := range out { 375 if strings.HasPrefix(branch, "*") { 376 branch = strings.TrimSpace(strings.TrimPrefix(branch, "*")) 377 current = branch 378 } 379 branches = append(branches, strings.TrimSpace(branch)) 380 } 381 return branches, current, nil 382 } 383 384 // HasUncommittedChanges checks whether the current branch contains 385 // any uncommitted changes. 386 func (g *Git) HasUncommittedChanges() (bool, error) { 387 out, err := g.FilesWithUncommittedChanges() 388 if err != nil { 389 return false, err 390 } 391 return len(out) != 0, nil 392 } 393 394 // HasUntrackedFiles checks whether the current branch contains any 395 // untracked files. 396 func (g *Git) HasUntrackedFiles() (bool, error) { 397 out, err := g.UntrackedFiles() 398 if err != nil { 399 return false, err 400 } 401 return len(out) != 0, nil 402 } 403 404 // Init initializes a new git repository. 405 func (g *Git) Init(path string) error { 406 return g.run("init", path) 407 } 408 409 // IsFileCommitted tests whether the given file has been committed to 410 // the repository. 411 func (g *Git) IsFileCommitted(file string) bool { 412 // Check if file is still in staging enviroment. 413 if out, _ := g.runOutput("status", "--porcelain", file); len(out) > 0 { 414 return false 415 } 416 // Check if file is unknown to git. 417 return g.run("ls-files", file, "--error-unmatch") == nil 418 } 419 420 // LatestCommitMessage returns the latest commit message on the 421 // current branch. 422 func (g *Git) LatestCommitMessage() (string, error) { 423 out, err := g.runOutput("log", "-n", "1", "--format=format:%B") 424 if err != nil { 425 return "", err 426 } 427 return strings.Join(out, "\n"), nil 428 } 429 430 // Log returns a list of commits on <branch> that are not on <base>, 431 // using the specified format. 432 func (g *Git) Log(branch, base, format string) ([][]string, error) { 433 n, err := g.CountCommits(branch, base) 434 if err != nil { 435 return nil, err 436 } 437 result := [][]string{} 438 for i := 0; i < n; i++ { 439 skipArg := fmt.Sprintf("--skip=%d", i) 440 formatArg := fmt.Sprintf("--format=%s", format) 441 branchArg := fmt.Sprintf("%v..%v", base, branch) 442 out, err := g.runOutput("log", "-1", skipArg, formatArg, branchArg) 443 if err != nil { 444 return nil, err 445 } 446 result = append(result, out) 447 } 448 return result, nil 449 } 450 451 // Merge merges all commits from <branch> to the current branch. If 452 // <squash> is set, then all merged commits are squashed into a single 453 // commit. 454 func (g *Git) Merge(branch string, opts ...MergeOpt) error { 455 args := []string{"merge"} 456 squash := false 457 strategy := "" 458 resetOnFailure := true 459 for _, opt := range opts { 460 switch typedOpt := opt.(type) { 461 case SquashOpt: 462 squash = bool(typedOpt) 463 case StrategyOpt: 464 strategy = string(typedOpt) 465 case ResetOnFailureOpt: 466 resetOnFailure = bool(typedOpt) 467 } 468 } 469 if squash { 470 args = append(args, "--squash") 471 } else { 472 args = append(args, "--no-squash") 473 } 474 if strategy != "" { 475 args = append(args, fmt.Sprintf("--strategy=%v", strategy)) 476 } 477 args = append(args, branch) 478 if out, err := g.runOutput(args...); err != nil { 479 if resetOnFailure { 480 if err2 := g.run("reset", "--merge"); err2 != nil { 481 return fmt.Errorf("%v\nCould not git reset while recovering from error: %v", err, err2) 482 } 483 } 484 return fmt.Errorf("%v\n%v", err, strings.Join(out, "\n")) 485 } 486 return nil 487 } 488 489 // MergeInProgress returns a boolean flag that indicates if a merge 490 // operation is in progress for the current repository. 491 func (g *Git) MergeInProgress() (bool, error) { 492 repoRoot, err := g.TopLevel() 493 if err != nil { 494 return false, err 495 } 496 mergeFile := filepath.Join(repoRoot, ".git", "MERGE_HEAD") 497 if _, err := g.s.Stat(mergeFile); err != nil { 498 if runutil.IsNotExist(err) { 499 return false, nil 500 } 501 return false, err 502 } 503 return true, nil 504 } 505 506 // ModifiedFiles returns a slice of filenames that have changed 507 // between <baseBranch> and <currentBranch>. 508 func (g *Git) ModifiedFiles(baseBranch, currentBranch string) ([]string, error) { 509 out, err := g.runOutput("diff", "--name-only", baseBranch+".."+currentBranch) 510 if err != nil { 511 return nil, err 512 } 513 return out, nil 514 } 515 516 // Pull pulls the given branch from the given remote. 517 func (g *Git) Pull(remote, branch string) error { 518 if out, err := g.runOutput("pull", remote, branch); err != nil { 519 g.run("reset", "--merge") 520 return fmt.Errorf("%v\n%v", err, strings.Join(out, "\n")) 521 } 522 major, minor, err := g.Version() 523 if err != nil { 524 return err 525 } 526 // Starting with git 1.8, "git pull <remote> <branch>" does not 527 // create the branch "<remote>/<branch>" locally. To avoid the need 528 // to account for this, run "git pull", which fails but creates the 529 // missing branch, for git 1.7 and older. 530 if major < 2 && minor < 8 { 531 // This command is expected to fail (with desirable side effects). 532 // Use exec.Command instead of runner to prevent this failure from 533 // showing up in the console and confusing people. 534 command := exec.Command("git", "pull") 535 command.Run() 536 } 537 return nil 538 } 539 540 // Push pushes the given branch to the given remote. 541 func (g *Git) Push(remote, branch string, opts ...PushOpt) error { 542 args := []string{"push"} 543 force := false 544 verify := true 545 // TODO(youngseokyoon): consider making followTags option default to true, after verifying that 546 // it works well for the madb repository. 547 followTags := false 548 for _, opt := range opts { 549 switch typedOpt := opt.(type) { 550 case ForceOpt: 551 force = bool(typedOpt) 552 case VerifyOpt: 553 verify = bool(typedOpt) 554 case FollowTagsOpt: 555 followTags = bool(typedOpt) 556 } 557 } 558 if force { 559 args = append(args, "--force") 560 } 561 if verify { 562 args = append(args, "--verify") 563 } else { 564 args = append(args, "--no-verify") 565 } 566 if followTags { 567 args = append(args, "--follow-tags") 568 } 569 args = append(args, remote, branch) 570 return g.run(args...) 571 } 572 573 // Rebase rebases to a particular upstream branch. 574 func (g *Git) Rebase(upstream string) error { 575 return g.run("rebase", upstream) 576 } 577 578 // RebaseAbort aborts an in-progress rebase operation. 579 func (g *Git) RebaseAbort() error { 580 return g.run("rebase", "--abort") 581 } 582 583 // Remove removes the given files. 584 func (g *Git) Remove(fileNames ...string) error { 585 args := []string{"rm"} 586 args = append(args, fileNames...) 587 return g.run(args...) 588 } 589 590 // RemoteUrl gets the url of the remote with the given name. 591 func (g *Git) RemoteUrl(name string) (string, error) { 592 configKey := fmt.Sprintf("remote.%s.url", name) 593 out, err := g.runOutput("config", "--get", configKey) 594 if err != nil { 595 return "", err 596 } 597 if got, want := len(out), 1; got != want { 598 return "", fmt.Errorf("RemoteUrl: unexpected length of remotes %v: got %v, want %v", out, got, want) 599 } 600 return out[0], nil 601 } 602 603 // RemoveUntrackedFiles removes untracked files and directories. 604 func (g *Git) RemoveUntrackedFiles() error { 605 return g.run("clean", "-d", "-f") 606 } 607 608 // Reset resets the current branch to the target, discarding any 609 // uncommitted changes. 610 func (g *Git) Reset(target string, opts ...ResetOpt) error { 611 args := []string{"reset"} 612 mode := "hard" 613 for _, opt := range opts { 614 switch typedOpt := opt.(type) { 615 case ModeOpt: 616 mode = string(typedOpt) 617 } 618 } 619 args = append(args, fmt.Sprintf("--%v", mode), target, "--") 620 return g.run(args...) 621 } 622 623 // SetRemoteUrl sets the url of the remote with given name to the given url. 624 func (g *Git) SetRemoteUrl(name, url string) error { 625 return g.run("remote", "set-url", name, url) 626 } 627 628 // Stash attempts to stash any unsaved changes. It returns true if 629 // anything was actually stashed, otherwise false. An error is 630 // returned if the stash command fails. 631 func (g *Git) Stash() (bool, error) { 632 oldSize, err := g.StashSize() 633 if err != nil { 634 return false, err 635 } 636 if err := g.run("stash", "save"); err != nil { 637 return false, err 638 } 639 newSize, err := g.StashSize() 640 if err != nil { 641 return false, err 642 } 643 return newSize > oldSize, nil 644 } 645 646 // StashSize returns the size of the stash stack. 647 func (g *Git) StashSize() (int, error) { 648 out, err := g.runOutput("stash", "list") 649 if err != nil { 650 return 0, err 651 } 652 // If output is empty, then stash is empty. 653 if len(out) == 0 { 654 return 0, nil 655 } 656 // Otherwise, stash size is the length of the output. 657 return len(out), nil 658 } 659 660 // StashPop pops the stash into the current working tree. 661 func (g *Git) StashPop() error { 662 return g.run("stash", "pop") 663 } 664 665 // TopLevel returns the top level path of the current repository. 666 func (g *Git) TopLevel() (string, error) { 667 // TODO(sadovsky): If g.rootDir is set, perhaps simply return that? 668 out, err := g.runOutput("rev-parse", "--show-toplevel") 669 if err != nil { 670 return "", err 671 } 672 return strings.Join(out, "\n"), nil 673 } 674 675 // TrackedFiles returns the list of files that are tracked. 676 func (g *Git) TrackedFiles() ([]string, error) { 677 out, err := g.runOutput("ls-files") 678 if err != nil { 679 return nil, err 680 } 681 return out, nil 682 } 683 684 // UntrackedFiles returns the list of files that are not tracked. 685 func (g *Git) UntrackedFiles() ([]string, error) { 686 out, err := g.runOutput("ls-files", "--others", "--directory", "--exclude-standard") 687 if err != nil { 688 return nil, err 689 } 690 return out, nil 691 } 692 693 // Version returns the major and minor git version. 694 func (g *Git) Version() (int, int, error) { 695 out, err := g.runOutput("version") 696 if err != nil { 697 return 0, 0, err 698 } 699 if got, want := len(out), 1; got != want { 700 return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 701 } 702 words := strings.Split(out[0], " ") 703 if got, want := len(words), 3; got < want { 704 return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want at least %v", words, got, want) 705 } 706 version := strings.Split(words[2], ".") 707 if got, want := len(version), 3; got < want { 708 return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want at least %v", version, got, want) 709 } 710 major, err := strconv.Atoi(version[0]) 711 if err != nil { 712 return 0, 0, fmt.Errorf("failed parsing %q to integer", major) 713 } 714 minor, err := strconv.Atoi(version[1]) 715 if err != nil { 716 return 0, 0, fmt.Errorf("failed parsing %q to integer", minor) 717 } 718 return major, minor, nil 719 } 720 721 func (g *Git) run(args ...string) error { 722 var stdout, stderr bytes.Buffer 723 capture := func(s runutil.Sequence) runutil.Sequence { return s.Capture(&stdout, &stderr) } 724 if err := g.runWithFn(capture, args...); err != nil { 725 return Error(stdout.String(), stderr.String(), args...) 726 } 727 return nil 728 } 729 730 func trimOutput(o string) []string { 731 output := strings.TrimSpace(o) 732 if len(output) == 0 { 733 return nil 734 } 735 return strings.Split(output, "\n") 736 } 737 738 func (g *Git) runOutput(args ...string) ([]string, error) { 739 var stdout, stderr bytes.Buffer 740 fn := func(s runutil.Sequence) runutil.Sequence { return s.Capture(&stdout, &stderr) } 741 if err := g.runWithFn(fn, args...); err != nil { 742 return nil, Error(stdout.String(), stderr.String(), args...) 743 } 744 return trimOutput(stdout.String()), nil 745 } 746 747 func (g *Git) runInteractive(args ...string) error { 748 var stderr bytes.Buffer 749 // In order for the editing to work correctly with 750 // terminal-based editors, notably "vim", use os.Stdout. 751 capture := func(s runutil.Sequence) runutil.Sequence { return s.Capture(os.Stdout, &stderr) } 752 if err := g.runWithFn(capture, args...); err != nil { 753 return Error("", stderr.String(), args...) 754 } 755 return nil 756 } 757 758 func (g *Git) runWithFn(fn func(s runutil.Sequence) runutil.Sequence, args ...string) error { 759 g.s.Dir(g.rootDir) 760 args = platformSpecificGitArgs(args...) 761 if fn == nil { 762 fn = func(s runutil.Sequence) runutil.Sequence { return s } 763 } 764 return fn(g.s).Env(g.opts).Last("git", args...) 765 } 766 767 // Committer encapsulates the process of create a commit. 768 type Committer struct { 769 commit func() error 770 commitWithMessage func(message string) error 771 } 772 773 // Commit creates a commit. 774 func (c *Committer) Commit(message string) error { 775 if len(message) == 0 { 776 // No commit message supplied, let git supply one. 777 return c.commit() 778 } 779 return c.commitWithMessage(message) 780 } 781 782 // NewCommitter is the Committer factory. The boolean <edit> flag 783 // determines whether the commit commands should prompt users to edit 784 // the commit message. This flag enables automated testing. 785 func (g *Git) NewCommitter(edit bool) *Committer { 786 if edit { 787 return &Committer{ 788 commit: g.CommitAndEdit, 789 commitWithMessage: g.CommitWithMessageAndEdit, 790 } 791 } else { 792 return &Committer{ 793 commit: g.Commit, 794 commitWithMessage: g.CommitWithMessage, 795 } 796 } 797 }