github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/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 "encoding/hex" 10 "fmt" 11 "io" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "runtime" 16 "strconv" 17 "strings" 18 19 "github.com/btwiuse/jiri" 20 "github.com/btwiuse/jiri/envvar" 21 ) 22 23 type GitError struct { 24 Root string 25 Args []string 26 Output string 27 ErrorOutput string 28 err error 29 } 30 31 func Error(output, errorOutput string, err error, root string, args ...string) GitError { 32 return GitError{ 33 Root: root, 34 Args: args, 35 Output: output, 36 ErrorOutput: errorOutput, 37 err: err, 38 } 39 } 40 41 func (ge GitError) Error() string { 42 result := fmt.Sprintf("(%s)", ge.Root) 43 result = "'git " 44 result += strings.Join(ge.Args, " ") 45 result += "' failed:\n" 46 result += "stdout:\n" 47 result += ge.Output + "\n" 48 result += "stderr:\n" 49 result += ge.ErrorOutput 50 result += "\ncommand fail error: " + ge.err.Error() 51 return result 52 } 53 54 type Git struct { 55 jirix *jiri.X 56 opts map[string]string 57 rootDir string 58 userName string 59 userEmail string 60 } 61 62 type gitOpt interface { 63 gitOpt() 64 } 65 type AuthorDateOpt string 66 type CommitterDateOpt string 67 type RootDirOpt string 68 type UserNameOpt string 69 type UserEmailOpt string 70 71 func (AuthorDateOpt) gitOpt() {} 72 func (CommitterDateOpt) gitOpt() {} 73 func (RootDirOpt) gitOpt() {} 74 func (UserNameOpt) gitOpt() {} 75 func (UserEmailOpt) gitOpt() {} 76 77 type Reference struct { 78 Name string 79 Revision string 80 IsHead bool 81 } 82 83 type Branch struct { 84 *Reference 85 Tracking *Reference 86 } 87 88 type Revision string 89 type BranchName string 90 91 const ( 92 RemoteType = "remote" 93 LocalType = "local" 94 ) 95 96 // New is the Git factory. 97 func New(jirix *jiri.X, opts ...gitOpt) *Git { 98 rootDir := "" 99 userName := "" 100 userEmail := "" 101 env := map[string]string{} 102 for _, opt := range opts { 103 switch typedOpt := opt.(type) { 104 case AuthorDateOpt: 105 env["GIT_AUTHOR_DATE"] = string(typedOpt) 106 case CommitterDateOpt: 107 env["GIT_COMMITTER_DATE"] = string(typedOpt) 108 case RootDirOpt: 109 rootDir = string(typedOpt) 110 case UserNameOpt: 111 userName = string(typedOpt) 112 case UserEmailOpt: 113 userEmail = string(typedOpt) 114 } 115 } 116 return &Git{ 117 jirix: jirix, 118 opts: env, 119 rootDir: rootDir, 120 userName: userName, 121 userEmail: userEmail, 122 } 123 } 124 125 // Add adds a file to staging. 126 func (g *Git) Add(file string) error { 127 return g.run("add", file) 128 } 129 130 // Add adds a file to staging. 131 func (g *Git) AddUpdatedFiles() error { 132 return g.run("add", "-u") 133 } 134 135 // AddRemote adds a new remote with the given name and path. 136 func (g *Git) AddRemote(name, path string) error { 137 return g.run("remote", "add", name, path) 138 } 139 140 // AddOrReplaceRemote adds a new remote with given name and path. If the name 141 // already exists, it replaces the named remote with new path. 142 func (g *Git) AddOrReplaceRemote(name, path string) error { 143 configStr := fmt.Sprintf("remote.%s.url", name) 144 if err := g.Config(configStr, path); err != nil { 145 return err 146 } 147 configStr = fmt.Sprintf("remote.%s.fetch", name) 148 if err := g.Config(configStr, "+refs/heads/*:refs/remotes/origin/*"); err != nil { 149 return err 150 } 151 return nil 152 } 153 154 // GetRemoteBranchesContaining returns a slice of the remote branches 155 // which contains the given commit 156 func (g *Git) GetRemoteBranchesContaining(commit string) ([]string, error) { 157 branches, _, err := g.GetBranches("-r", "--contains", commit) 158 return branches, err 159 } 160 161 // BranchesDiffer tests whether two branches have any changes between them. 162 func (g *Git) BranchesDiffer(branch1, branch2 string) (bool, error) { 163 out, err := g.runOutput("--no-pager", "diff", "--name-only", branch1+".."+branch2) 164 if err != nil { 165 return false, err 166 } 167 // If output is empty, then there is no difference. 168 if len(out) == 0 { 169 return false, nil 170 } 171 // Otherwise there is a difference. 172 return true, nil 173 } 174 175 // GetAllBranchesInfo returns information about all branches. 176 func (g *Git) GetAllBranchesInfo() ([]Branch, error) { 177 branchesInfo, err := g.runOutput("for-each-ref", "--format", "%(refname:short):%(upstream:short):%(objectname):%(HEAD):%(upstream)", "refs/heads") 178 if err != nil { 179 return nil, err 180 } 181 var upstreamRefs []string 182 var branches []Branch 183 for _, branchInfo := range branchesInfo { 184 s := strings.SplitN(branchInfo, ":", 5) 185 branch := Branch{ 186 &Reference{ 187 Name: s[0], 188 Revision: s[2], 189 IsHead: s[3] == "*", 190 }, 191 nil, 192 } 193 if s[1] != "" { 194 upstreamRefs = append(upstreamRefs, s[4]) 195 } 196 branches = append(branches, branch) 197 } 198 199 args := append([]string{"show-ref"}, upstreamRefs...) 200 if refsInfo, err := g.runOutput(args...); err == nil { 201 refs := map[string]string{} 202 for _, info := range refsInfo { 203 strs := strings.SplitN(info, " ", 2) 204 refs[strs[1]] = strs[0] 205 } 206 for i, branchInfo := range branchesInfo { 207 s := strings.SplitN(branchInfo, ":", 5) 208 if s[1] != "" { 209 branches[i].Tracking = &Reference{ 210 Name: s[1], 211 Revision: refs[s[4]], 212 } 213 } 214 } 215 } 216 217 return branches, nil 218 } 219 220 // IsRevAvailable runs cat-file on a commit hash is available locally. 221 func (g *Git) IsRevAvailable(rev string) bool { 222 // TODO: (haowei@)(11517) We are having issues with corrupted 223 // cache data on mac builders. Return a non-nil error 224 // to force the mac builders fetch from remote to avoid 225 // jiri checkout failures. 226 if runtime.GOOS == "darwin" { 227 return false 228 } 229 // test if rev is a legit sha1 hash string 230 if _, err := hex.DecodeString(rev); len(rev) != 40 || err != nil { 231 return false 232 } 233 234 if err := g.run("cat-file", "-e", rev); err != nil { 235 return false 236 } 237 return true 238 } 239 240 // CheckoutBranch checks out the given branch. 241 func (g *Git) CheckoutBranch(branch string, opts ...CheckoutOpt) error { 242 args := []string{"checkout"} 243 var force ForceOpt = false 244 var detach DetachOpt = false 245 for _, opt := range opts { 246 switch typedOpt := opt.(type) { 247 case ForceOpt: 248 force = typedOpt 249 case DetachOpt: 250 detach = typedOpt 251 } 252 } 253 if force { 254 args = append(args, "-f") 255 } 256 if detach { 257 args = append(args, "--detach") 258 } 259 args = append(args, branch) 260 return g.run(args...) 261 } 262 263 // Clone clones the given repository to the given local path. If reference is 264 // not empty it uses the given path as a reference/shared repo. 265 func (g *Git) Clone(repo, path string, opts ...CloneOpt) error { 266 args := []string{"clone"} 267 for _, opt := range opts { 268 switch typedOpt := opt.(type) { 269 case BareOpt: 270 if typedOpt { 271 args = append(args, "--bare") 272 } 273 case ReferenceOpt: 274 reference := string(typedOpt) 275 if reference != "" { 276 args = append(args, []string{"--reference-if-able", reference}...) 277 } 278 case SharedOpt: 279 if typedOpt { 280 args = append(args, []string{"--shared", "--local"}...) 281 } 282 case NoCheckoutOpt: 283 if typedOpt { 284 args = append(args, "--no-checkout") 285 } 286 case DepthOpt: 287 if typedOpt > 0 { 288 args = append(args, []string{"--depth", strconv.Itoa(int(typedOpt))}...) 289 } 290 case OmitBlobsOpt: 291 if typedOpt { 292 args = append(args, "--filter=blob:none") 293 } 294 } 295 } 296 args = append(args, repo) 297 args = append(args, path) 298 return g.run(args...) 299 } 300 301 // CloneMirror clones the given repository using mirror flag. 302 func (g *Git) CloneMirror(repo, path string, depth int) error { 303 args := []string{"clone", "--mirror"} 304 if depth > 0 { 305 args = append(args, []string{"--depth", strconv.Itoa(depth)}...) 306 } 307 args = append(args, []string{repo, path}...) 308 return g.run(args...) 309 } 310 311 // CloneRecursive clones the given repository recursively to the given local path. 312 func (g *Git) CloneRecursive(repo, path string) error { 313 return g.run("clone", "--recursive", repo, path) 314 } 315 316 // Commit commits all files in staging with an empty message. 317 func (g *Git) Commit() error { 318 return g.run("commit", "--allow-empty", "--allow-empty-message", "--no-edit") 319 } 320 321 // CommitAmend amends the previous commit with the currently staged 322 // changes. Empty commits are allowed. 323 func (g *Git) CommitAmend() error { 324 return g.run("commit", "--amend", "--allow-empty", "--no-edit") 325 } 326 327 // CommitAmendWithMessage amends the previous commit with the 328 // currently staged changes, and the given message. Empty commits are 329 // allowed. 330 func (g *Git) CommitAmendWithMessage(message string) error { 331 return g.run("commit", "--amend", "--allow-empty", "-m", message) 332 } 333 334 // CommitAndEdit commits all files in staging and allows the user to 335 // edit the commit message. 336 func (g *Git) CommitAndEdit() error { 337 args := []string{"commit", "--allow-empty"} 338 return g.runInteractive(args...) 339 } 340 341 // CommitFile commits the given file with the given commit message. 342 func (g *Git) CommitFile(fileName, message string) error { 343 if err := g.Add(fileName); err != nil { 344 return err 345 } 346 return g.CommitWithMessage(message) 347 } 348 349 // CommitMessages returns the concatenation of all commit messages on 350 // <branch> that are not also on <baseBranch>. 351 func (g *Git) CommitMessages(branch, baseBranch string) (string, error) { 352 out, err := g.runOutput("log", "--no-merges", baseBranch+".."+branch) 353 if err != nil { 354 return "", err 355 } 356 return strings.Join(out, "\n"), nil 357 } 358 359 // CommitNoVerify commits all files in staging with the given 360 // message and skips all git-hooks. 361 func (g *Git) CommitNoVerify(message string) error { 362 return g.run("commit", "--allow-empty", "--allow-empty-message", "--no-verify", "-m", message) 363 } 364 365 // CommitWithMessage commits all files in staging with the given 366 // message. 367 func (g *Git) CommitWithMessage(message string) error { 368 return g.run("commit", "--allow-empty", "--allow-empty-message", "-m", message) 369 } 370 371 // CommitWithMessage commits all files in staging and allows the user 372 // to edit the commit message. The given message will be used as the 373 // default. 374 func (g *Git) CommitWithMessageAndEdit(message string) error { 375 args := []string{"commit", "--allow-empty", "-e", "-m", message} 376 return g.runInteractive(args...) 377 } 378 379 // Committers returns a list of committers for the current repository 380 // along with the number of their commits. 381 func (g *Git) Committers() ([]string, error) { 382 out, err := g.runOutput("shortlog", "-s", "-n", "-e") 383 if err != nil { 384 return nil, err 385 } 386 return out, nil 387 } 388 389 // Provides list of commits reachable from rev but not from base 390 // rev can be a branch/tag or revision name. 391 func (g *Git) ExtraCommits(rev, base string) ([]string, error) { 392 return g.runOutput("rev-list", base+".."+rev) 393 } 394 395 // CountCommits returns the number of commits on <branch> that are not 396 // on <base>. 397 func (g *Git) CountCommits(branch, base string) (int, error) { 398 args := []string{"rev-list", "--count", branch} 399 if base != "" { 400 args = append(args, "^"+base) 401 } 402 args = append(args, "--") 403 out, err := g.runOutput(args...) 404 if err != nil { 405 return 0, err 406 } 407 if got, want := len(out), 1; got != want { 408 return 0, fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 409 } 410 count, err := strconv.Atoi(out[0]) 411 if err != nil { 412 return 0, fmt.Errorf("Atoi(%v) failed: %v", out[0], err) 413 } 414 return count, nil 415 } 416 417 // Get one line log 418 func (g *Git) OneLineLog(rev string) (string, error) { 419 out, err := g.runOutput("log", "--pretty=oneline", "-n", "1", "--abbrev-commit", rev) 420 if err != nil { 421 return "", err 422 } 423 if got, want := len(out), 1; got != want { 424 g.jirix.Logger.Warningf("wanted one line log, got %d line log: %q", got, out) 425 } 426 return out[0], nil 427 } 428 429 // CreateBranch creates a new branch with the given name. 430 func (g *Git) CreateBranch(branch string) error { 431 return g.run("branch", branch) 432 } 433 434 // CreateBranchFromRef creates a new branch from an existing reference. 435 func (g *Git) CreateBranchFromRef(branch, ref string) error { 436 return g.run("branch", branch, ref) 437 } 438 439 // CreateAndCheckoutBranch creates a new branch with the given name 440 // and checks it out. 441 func (g *Git) CreateAndCheckoutBranch(branch string) error { 442 return g.run("checkout", "-b", branch) 443 } 444 445 // SetUpstream sets the upstream branch to the given one. 446 func (g *Git) SetUpstream(branch, upstream string) error { 447 return g.run("branch", "-u", upstream, branch) 448 } 449 450 // LsRemote lists referneces in a remote repository. 451 func (g *Git) LsRemote(args ...string) (string, error) { 452 a := []string{"ls-remote"} 453 a = append(a, args...) 454 out, err := g.runOutput(a...) 455 if err != nil { 456 return "", err 457 } 458 if got, want := len(out), 1; got != want { 459 return "", fmt.Errorf("git ls-remote %s: unexpected length of %s: got %d, want %d", strings.Join(args, " "), out, got, want) 460 } 461 return out[0], nil 462 } 463 464 // CreateBranchWithUpstream creates a new branch and sets the upstream 465 // repository to the given upstream. 466 func (g *Git) CreateBranchWithUpstream(branch, upstream string) error { 467 return g.run("branch", branch, upstream) 468 } 469 470 // ShortHash returns the short hash for a given reference. 471 func (g *Git) ShortHash(ref string) (string, error) { 472 out, err := g.runOutput("rev-parse", "--short", ref) 473 if err != nil { 474 return "", err 475 } 476 if got, want := len(out), 1; got != want { 477 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 478 } 479 return out[0], nil 480 } 481 482 // UserInfoForCommit returns user name and email for a given reference. 483 func (g *Git) UserInfoForCommit(ref string) (string, string, error) { 484 out, err := g.runOutput("log", "-n", "1", "--format=format:%cn:%ce", ref) 485 if err != nil { 486 return "", "", err 487 } 488 info := strings.SplitN(out[0], ":", 2) 489 return info[0], info[1], nil 490 } 491 492 // CurrentBranchName returns the name of the current branch. 493 func (g *Git) CurrentBranchName() (string, error) { 494 out, err := g.runOutput("rev-parse", "--abbrev-ref", "HEAD") 495 if err != nil { 496 return "", err 497 } 498 if got, want := len(out), 1; got != want { 499 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 500 } 501 return out[0], nil 502 } 503 504 func (g *Git) GetSymbolicRef() (string, error) { 505 out, err := g.runOutput("symbolic-ref", "-q", "HEAD") 506 if err != nil { 507 return "", err 508 } 509 if got, want := len(out), 1; got != want { 510 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 511 } 512 return out[0], nil 513 } 514 515 // RemoteBranchName returns the name of the tracking branch stripping remote name from it. 516 // It will search recursively if current branch tracks a local branch. 517 func (g *Git) RemoteBranchName() (string, error) { 518 branch, err := g.CurrentBranchName() 519 if err != nil || branch == "" { 520 return "", err 521 } 522 523 trackingBranch, err := g.TrackingBranchName() 524 if err != nil || trackingBranch == "" { 525 return "", err 526 } 527 528 for { 529 out, err := g.runOutput("config", "branch."+branch+".remote") 530 if err != nil || len(out) == 0 { 531 return "", err 532 } 533 if got, want := len(out), 1; got != want { 534 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 535 } 536 // check if current branch tracks local branch 537 if out[0] != "." { 538 return strings.Replace(trackingBranch, out[0]+"/", "", 1), nil 539 } else { 540 branch = trackingBranch 541 if trackingBranch, err = g.TrackingBranchFromSymbolicRef("refs/heads/" + trackingBranch); err != nil || trackingBranch == "" { 542 return "", err 543 } 544 } 545 } 546 } 547 548 // TrackingBranchName returns the name of the tracking branch. 549 func (g *Git) TrackingBranchName() (string, error) { 550 currentRef, err := g.GetSymbolicRef() 551 if err != nil { 552 return "", err 553 } 554 return g.TrackingBranchFromSymbolicRef(currentRef) 555 } 556 557 // TrackingBranchFromSymbolicRef returns the name of the tracking branch for provided ref 558 func (g *Git) TrackingBranchFromSymbolicRef(ref string) (string, error) { 559 out, err := g.runOutput("for-each-ref", "--format", "%(upstream:short)", ref) 560 if err != nil || len(out) == 0 { 561 return "", err 562 } 563 if got, want := len(out), 1; got != want { 564 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 565 } 566 return out[0], nil 567 } 568 569 func (g *Git) IsOnBranch() bool { 570 _, err := g.runOutput("symbolic-ref", "-q", "HEAD") 571 return err == nil 572 } 573 574 // CurrentRevision returns the current revision. 575 func (g *Git) CurrentRevision() (string, error) { 576 return g.CurrentRevisionForRef("HEAD") 577 } 578 579 // CurrentRevisionForRef gets current rev for ref/branch/tags 580 func (g *Git) CurrentRevisionForRef(ref string) (string, error) { 581 out, err := g.runOutput("rev-list", "-n", "1", ref) 582 if err != nil { 583 return "", err 584 } 585 if got, want := len(out), 1; got != want { 586 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 587 } 588 return out[0], nil 589 } 590 591 // CurrentRevisionOfBranch returns the current revision of the given branch. 592 func (g *Git) CurrentRevisionOfBranch(branch string) (string, error) { 593 // Using rev-list instead of rev-parse as latter doesn't work well with tag 594 out, err := g.runOutput("rev-list", "-n", "1", branch) 595 if err != nil { 596 return "", err 597 } 598 if got, want := len(out), 1; got != want { 599 return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 600 } 601 return out[0], nil 602 } 603 604 func (g *Git) CherryPick(rev string) error { 605 err := g.run("cherry-pick", rev) 606 return err 607 } 608 609 // DeleteBranch deletes the given branch. 610 func (g *Git) DeleteBranch(branch string, opts ...DeleteBranchOpt) error { 611 args := []string{"branch"} 612 force := false 613 for _, opt := range opts { 614 switch typedOpt := opt.(type) { 615 case ForceOpt: 616 force = bool(typedOpt) 617 } 618 } 619 if force { 620 args = append(args, "-D") 621 } else { 622 args = append(args, "-d") 623 } 624 args = append(args, branch) 625 return g.run(args...) 626 } 627 628 // DirExistsOnBranch returns true if a directory with the given name 629 // exists on the branch. If branch is empty it defaults to "master". 630 func (g *Git) DirExistsOnBranch(dir, branch string) bool { 631 if dir == "." { 632 dir = "" 633 } 634 if branch == "" { 635 branch = "master" 636 } 637 args := []string{"ls-tree", "-d", branch + ":" + dir} 638 return g.run(args...) == nil 639 } 640 641 // CreateLightweightTag creates a lightweight tag with a given name. 642 func (g *Git) CreateLightweightTag(name string) error { 643 return g.run("tag", name) 644 } 645 646 // Fetch fetches refs and tags from the given remote. 647 func (g *Git) Fetch(remote string, opts ...FetchOpt) error { 648 return g.FetchRefspec(remote, "", opts...) 649 } 650 651 // FetchRefspec fetches refs and tags from the given remote for a particular refspec. 652 func (g *Git) FetchRefspec(remote, refspec string, opts ...FetchOpt) error { 653 tags := false 654 all := false 655 prune := false 656 updateShallow := false 657 depth := 0 658 fetchTag := "" 659 for _, opt := range opts { 660 switch typedOpt := opt.(type) { 661 case TagsOpt: 662 tags = bool(typedOpt) 663 case AllOpt: 664 all = bool(typedOpt) 665 case PruneOpt: 666 prune = bool(typedOpt) 667 case DepthOpt: 668 depth = int(typedOpt) 669 case UpdateShallowOpt: 670 updateShallow = bool(typedOpt) 671 case FetchTagOpt: 672 fetchTag = string(typedOpt) 673 } 674 } 675 args := []string{} 676 args = append(args, "fetch") 677 if prune { 678 args = append(args, "-p") 679 } 680 if tags { 681 args = append(args, "--tags") 682 } 683 if depth > 0 { 684 args = append(args, "--depth", strconv.Itoa(depth)) 685 } 686 if updateShallow { 687 args = append(args, "--update-shallow") 688 } 689 if all { 690 args = append(args, "--all") 691 } 692 if remote != "" { 693 args = append(args, remote) 694 } 695 if fetchTag != "" { 696 args = append(args, "tag", fetchTag) 697 } 698 if refspec != "" { 699 args = append(args, refspec) 700 } 701 702 return g.run(args...) 703 } 704 705 // FilesWithUncommittedChanges returns the list of files that have 706 // uncommitted changes. 707 func (g *Git) FilesWithUncommittedChanges() ([]string, error) { 708 out, err := g.runOutput("diff", "--name-only", "--no-ext-diff") 709 if err != nil { 710 return nil, err 711 } 712 out2, err := g.runOutput("diff", "--cached", "--name-only", "--no-ext-diff") 713 if err != nil { 714 return nil, err 715 } 716 return append(out, out2...), nil 717 } 718 719 // MergedBranches returns the list of all branches that were already merged. 720 func (g *Git) MergedBranches(ref string) ([]string, error) { 721 branches, _, err := g.GetBranches("--merged", ref) 722 return branches, err 723 } 724 725 // GetBranches returns a slice of the local branches of the current 726 // repository, followed by the name of the current branch. The 727 // behavior can be customized by providing optional arguments 728 // (e.g. --merged). 729 func (g *Git) GetBranches(args ...string) ([]string, string, error) { 730 args = append([]string{"branch"}, args...) 731 out, err := g.runOutput(args...) 732 if err != nil { 733 return nil, "", err 734 } 735 branches, current := []string{}, "" 736 for _, branch := range out { 737 if strings.HasPrefix(branch, "*") { 738 branch = strings.TrimSpace(strings.TrimPrefix(branch, "*")) 739 if g.IsOnBranch() { 740 current = branch 741 } else { 742 // Do not append detached head 743 continue 744 } 745 } 746 branches = append(branches, strings.TrimSpace(branch)) 747 } 748 return branches, current, nil 749 } 750 751 // BranchExists tests whether a branch with the given name exists in 752 // the local repository. 753 func (g *Git) BranchExists(branch string) (bool, error) { 754 var stdout, stderr bytes.Buffer 755 args := []string{"rev-parse", "--verify", "--quiet", branch} 756 err := g.runGit(&stdout, &stderr, args...) 757 if err != nil && stderr.String() != "" { 758 return false, Error(stdout.String(), stderr.String(), err, g.rootDir, args...) 759 } 760 return stdout.String() != "", nil 761 } 762 763 // ListRemoteBranchesContainingRef returns a slice of the remote branches 764 // which contains the given commit 765 func (g *Git) ListRemoteBranchesContainingRef(commit string) (map[string]bool, error) { 766 branches, _, err := g.GetBranches("-r", "--contains", commit) 767 if err != nil { 768 return nil, err 769 } 770 m := make(map[string]bool) 771 for _, branch := range branches { 772 m[branch] = true 773 } 774 return m, nil 775 } 776 777 // ListBranchesContainingRef returns a slice of the local branches 778 // which contains the given commit 779 func (g *Git) ListBranchesContainingRef(commit string) (map[string]bool, error) { 780 branches, _, err := g.GetBranches("--contains", commit) 781 if err != nil { 782 return nil, err 783 } 784 m := make(map[string]bool) 785 for _, branch := range branches { 786 m[branch] = true 787 } 788 return m, nil 789 } 790 791 // Grep searches for matching text and returns a list of lines from 792 // `git grep`. 793 func (g *Git) Grep(query string, pathSpecs []string, flags ...string) ([]string, error) { 794 args := append([]string{"grep"}, flags...) 795 if query != "" { 796 args = append(args, query) 797 } 798 if len(pathSpecs) != 0 { 799 args = append(args, "--") 800 args = append(args, pathSpecs...) 801 } 802 // TODO(ianloic): handle patterns that start with "-" 803 // TODO(ianloic): handle different pattern types (-i, -P, -E, etc) 804 // TODO(ianloic): handle different response types (--full-name, -v, --name-only, etc) 805 return g.runOutput(args...) 806 } 807 808 // HasUncommittedChanges checks whether the current branch contains 809 // any uncommitted changes. 810 func (g *Git) HasUncommittedChanges() (bool, error) { 811 out, err := g.FilesWithUncommittedChanges() 812 if err != nil { 813 return false, err 814 } 815 return len(out) != 0, nil 816 } 817 818 // HasUntrackedFiles checks whether the current branch contains any 819 // untracked files. 820 func (g *Git) HasUntrackedFiles() (bool, error) { 821 out, err := g.UntrackedFiles() 822 if err != nil { 823 return false, err 824 } 825 return len(out) != 0, nil 826 } 827 828 // Init initializes a new git repository. 829 func (g *Git) Init(path string, opts ...CloneOpt) error { 830 args := []string{"init"} 831 for _, opt := range opts { 832 switch typedOpt := opt.(type) { 833 case BareOpt: 834 if typedOpt { 835 args = append(args, "--bare") 836 } 837 } 838 } 839 args = append(args, path) 840 return g.run(args...) 841 } 842 843 // IsFileCommitted tests whether the given file has been committed to 844 // the repository. 845 func (g *Git) IsFileCommitted(file string) bool { 846 // Check if file is still in staging enviroment. 847 if out, _ := g.runOutput("status", "--porcelain", file); len(out) > 0 { 848 return false 849 } 850 // Check if file is unknown to git. 851 return g.run("ls-files", file, "--error-unmatch") == nil 852 } 853 854 func (g *Git) ShortStatus() (string, error) { 855 out, err := g.runOutput("status", "-s") 856 if err != nil { 857 return "", err 858 } 859 return strings.Join(out, "\n"), nil 860 } 861 862 func (g *Git) CommitMsg(ref string) (string, error) { 863 out, err := g.runOutput("log", "-n", "1", "--format=format:%B", ref) 864 if err != nil { 865 return "", err 866 } 867 return strings.Join(out, "\n"), nil 868 } 869 870 // LatestCommitMessage returns the latest commit message on the 871 // current branch. 872 func (g *Git) LatestCommitMessage() (string, error) { 873 out, err := g.runOutput("log", "-n", "1", "--format=format:%B") 874 if err != nil { 875 return "", err 876 } 877 return strings.Join(out, "\n"), nil 878 } 879 880 // Log returns a list of commits on <branch> that are not on <base>, 881 // using the specified format. 882 func (g *Git) Log(branch, base, format string) ([][]string, error) { 883 n, err := g.CountCommits(branch, base) 884 if err != nil { 885 return nil, err 886 } 887 result := [][]string{} 888 for i := 0; i < n; i++ { 889 skipArg := fmt.Sprintf("--skip=%d", i) 890 formatArg := fmt.Sprintf("--format=%s", format) 891 branchArg := fmt.Sprintf("%v..%v", base, branch) 892 out, err := g.runOutput("log", "-1", skipArg, formatArg, branchArg) 893 if err != nil { 894 return nil, err 895 } 896 result = append(result, out) 897 } 898 return result, nil 899 } 900 901 // Merge merges all commits from <branch> to the current branch. If 902 // <squash> is set, then all merged commits are squashed into a single 903 // commit. 904 func (g *Git) Merge(branch string, opts ...MergeOpt) error { 905 args := []string{"merge"} 906 squash := false 907 strategy := "" 908 resetOnFailure := true 909 for _, opt := range opts { 910 switch typedOpt := opt.(type) { 911 case SquashOpt: 912 squash = bool(typedOpt) 913 case StrategyOpt: 914 strategy = string(typedOpt) 915 case ResetOnFailureOpt: 916 resetOnFailure = bool(typedOpt) 917 case FfOnlyOpt: 918 args = append(args, "--ff-only") 919 } 920 } 921 if squash { 922 args = append(args, "--squash") 923 } else { 924 args = append(args, "--no-squash") 925 } 926 if strategy != "" { 927 args = append(args, fmt.Sprintf("--strategy=%v", strategy)) 928 } 929 args = append(args, branch) 930 if out, err := g.runOutput(args...); err != nil { 931 if resetOnFailure { 932 if err2 := g.run("reset", "--merge"); err2 != nil { 933 return fmt.Errorf("%v\nCould not git reset while recovering from error: %v", err, err2) 934 } 935 } 936 return fmt.Errorf("%v\n%v", err, strings.Join(out, "\n")) 937 } 938 return nil 939 } 940 941 // ModifiedFiles returns a slice of filenames that have changed 942 // between <baseBranch> and <currentBranch>. 943 func (g *Git) ModifiedFiles(baseBranch, currentBranch string) ([]string, error) { 944 out, err := g.runOutput("diff", "--name-only", baseBranch+".."+currentBranch) 945 if err != nil { 946 return nil, err 947 } 948 return out, nil 949 } 950 951 // Pull pulls the given branch from the given remote. 952 func (g *Git) Pull(remote, branch string) error { 953 if out, err := g.runOutput("pull", remote, branch); err != nil { 954 g.run("reset", "--merge") 955 return fmt.Errorf("%v\n%v", err, strings.Join(out, "\n")) 956 } 957 major, minor, err := g.Version() 958 if err != nil { 959 return err 960 } 961 // Starting with git 1.8, "git pull <remote> <branch>" does not 962 // create the branch "<remote>/<branch>" locally. To avoid the need 963 // to account for this, run "git pull", which fails but creates the 964 // missing branch, for git 1.7 and older. 965 if major < 2 && minor < 8 { 966 // This command is expected to fail (with desirable side effects). 967 // Use exec.Command instead of runner to prevent this failure from 968 // showing up in the console and confusing people. 969 command := exec.Command("git", "pull") 970 command.Run() 971 } 972 return nil 973 } 974 975 // Push pushes the given branch to the given remote. 976 func (g *Git) Push(remote, branch string, opts ...PushOpt) error { 977 args := []string{"push"} 978 force := false 979 verify := true 980 // TODO(youngseokyoon): consider making followTags option default to true, after verifying that 981 // it works well for the madb repository. 982 followTags := false 983 for _, opt := range opts { 984 switch typedOpt := opt.(type) { 985 case ForceOpt: 986 force = bool(typedOpt) 987 case VerifyOpt: 988 verify = bool(typedOpt) 989 case FollowTagsOpt: 990 followTags = bool(typedOpt) 991 } 992 } 993 if force { 994 args = append(args, "--force") 995 } 996 if verify { 997 args = append(args, "--verify") 998 } else { 999 args = append(args, "--no-verify") 1000 } 1001 if followTags { 1002 args = append(args, "--follow-tags") 1003 } 1004 args = append(args, remote, branch) 1005 return g.run(args...) 1006 } 1007 1008 // Rebase rebases to a particular upstream branch. 1009 func (g *Git) Rebase(upstream string, opts ...RebaseOpt) error { 1010 args := []string{"rebase"} 1011 rebaseMerges := false 1012 for _, opt := range opts { 1013 switch typedOpt := opt.(type) { 1014 case RebaseMerges: 1015 rebaseMerges = bool(typedOpt) 1016 } 1017 } 1018 1019 if rebaseMerges { 1020 args = append(args, "--rebase-merges") 1021 } 1022 args = append(args, upstream) 1023 return g.run(args...) 1024 } 1025 1026 // CherryPickAbort aborts an in-progress cherry-pick operation. 1027 func (g *Git) CherryPickAbort() error { 1028 // First check if cherry-pick is in progress 1029 path := ".git/CHERRY_PICK_HEAD" 1030 if g.rootDir != "" { 1031 path = filepath.Join(g.rootDir, path) 1032 } 1033 if _, err := os.Stat(path); err != nil { 1034 if os.IsNotExist(err) { 1035 return nil // Not in progress return 1036 } 1037 return err 1038 } 1039 return g.run("cherry-pick", "--abort") 1040 } 1041 1042 // RebaseAbort aborts an in-progress rebase operation. 1043 func (g *Git) RebaseAbort() error { 1044 // First check if rebase is in progress 1045 path := ".git/rebase-apply" 1046 if g.rootDir != "" { 1047 path = filepath.Join(g.rootDir, path) 1048 } 1049 if _, err := os.Stat(path); err != nil { 1050 if os.IsNotExist(err) { 1051 return nil // Not in progress return 1052 } 1053 return err 1054 } 1055 return g.run("rebase", "--abort") 1056 } 1057 1058 // Remove removes the given files. 1059 func (g *Git) Remove(fileNames ...string) error { 1060 args := []string{"rm"} 1061 args = append(args, fileNames...) 1062 return g.run(args...) 1063 } 1064 1065 func (g *Git) Config(configArgs ...string) error { 1066 args := []string{"config"} 1067 args = append(args, configArgs...) 1068 return g.run(args...) 1069 } 1070 1071 func (g *Git) ConfigGetKey(key string) (string, error) { 1072 out, err := g.runOutput("config", "--get", key) 1073 if err != nil { 1074 return "", err 1075 } 1076 if got, want := len(out), 1; got != want { 1077 g.jirix.Logger.Warningf("wanted one line log, got %d line log: %q", got, out) 1078 } 1079 return out[0], nil 1080 } 1081 1082 // RemoteUrl gets the url of the remote with the given name. 1083 func (g *Git) RemoteUrl(name string) (string, error) { 1084 configKey := fmt.Sprintf("remote.%s.url", name) 1085 out, err := g.runOutput("config", "--get", configKey) 1086 if err != nil { 1087 return "", err 1088 } 1089 if got, want := len(out), 1; got != want { 1090 return "", fmt.Errorf("RemoteUrl: unexpected length of remotes %v: got %v, want %v", out, got, want) 1091 } 1092 return out[0], nil 1093 } 1094 1095 // RemoveUntrackedFiles removes untracked files and directories. 1096 func (g *Git) RemoveUntrackedFiles() error { 1097 return g.run("clean", "-d", "-f") 1098 } 1099 1100 // Reset resets the current branch to the target, discarding any 1101 // uncommitted changes. 1102 func (g *Git) Reset(target string, opts ...ResetOpt) error { 1103 args := []string{"reset"} 1104 mode := "hard" 1105 for _, opt := range opts { 1106 switch typedOpt := opt.(type) { 1107 case ModeOpt: 1108 mode = string(typedOpt) 1109 } 1110 } 1111 args = append(args, fmt.Sprintf("--%v", mode), target, "--") 1112 return g.run(args...) 1113 } 1114 1115 // SetRemoteUrl sets the url of the remote with given name to the given url. 1116 func (g *Git) SetRemoteUrl(name, url string) error { 1117 return g.run("remote", "set-url", name, url) 1118 } 1119 1120 // DeleteRemote deletes the named remote 1121 func (g *Git) DeleteRemote(name string) error { 1122 return g.run("remote", "rm", name) 1123 } 1124 1125 // Stash attempts to stash any unsaved changes. It returns true if 1126 // anything was actually stashed, otherwise false. An error is 1127 // returned if the stash command fails. 1128 func (g *Git) Stash() (bool, error) { 1129 oldSize, err := g.StashSize() 1130 if err != nil { 1131 return false, err 1132 } 1133 if err := g.run("stash", "save"); err != nil { 1134 return false, err 1135 } 1136 newSize, err := g.StashSize() 1137 if err != nil { 1138 return false, err 1139 } 1140 return newSize > oldSize, nil 1141 } 1142 1143 // StashSize returns the size of the stash stack. 1144 func (g *Git) StashSize() (int, error) { 1145 out, err := g.runOutput("stash", "list") 1146 if err != nil { 1147 return 0, err 1148 } 1149 // If output is empty, then stash is empty. 1150 if len(out) == 0 { 1151 return 0, nil 1152 } 1153 // Otherwise, stash size is the length of the output. 1154 return len(out), nil 1155 } 1156 1157 // StashPop pops the stash into the current working tree. 1158 func (g *Git) StashPop() error { 1159 return g.run("stash", "pop") 1160 } 1161 1162 // TopLevel returns the top level path of the current repository. 1163 func (g *Git) TopLevel() (string, error) { 1164 // TODO(sadovsky): If g.rootDir is set, perhaps simply return that? 1165 out, err := g.runOutput("rev-parse", "--show-toplevel") 1166 if err != nil { 1167 return "", err 1168 } 1169 return strings.Join(out, "\n"), nil 1170 } 1171 1172 // TrackedFiles returns the list of files that are tracked. 1173 func (g *Git) TrackedFiles() ([]string, error) { 1174 out, err := g.runOutput("ls-files") 1175 if err != nil { 1176 return nil, err 1177 } 1178 return out, nil 1179 } 1180 1181 func (g *Git) Show(ref, file string) (string, error) { 1182 arg := ref 1183 arg = fmt.Sprintf("%s:%s", arg, file) 1184 out, err := g.runOutput("show", arg) 1185 if err != nil { 1186 return "", err 1187 } 1188 return strings.Join(out, "\n"), nil 1189 } 1190 1191 // UntrackedFiles returns the list of files that are not tracked. 1192 func (g *Git) UntrackedFiles() ([]string, error) { 1193 out, err := g.runOutput("ls-files", "--others", "--directory", "--exclude-standard") 1194 if err != nil { 1195 return nil, err 1196 } 1197 return out, nil 1198 } 1199 1200 // Version returns the major and minor git version. 1201 func (g *Git) Version() (int, int, error) { 1202 out, err := g.runOutput("version") 1203 if err != nil { 1204 return 0, 0, err 1205 } 1206 if got, want := len(out), 1; got != want { 1207 return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want) 1208 } 1209 words := strings.Split(out[0], " ") 1210 if got, want := len(words), 3; got < want { 1211 return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want at least %v", words, got, want) 1212 } 1213 version := strings.Split(words[2], ".") 1214 if got, want := len(version), 3; got < want { 1215 return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want at least %v", version, got, want) 1216 } 1217 major, err := strconv.Atoi(version[0]) 1218 if err != nil { 1219 return 0, 0, fmt.Errorf("failed parsing %q to integer", major) 1220 } 1221 minor, err := strconv.Atoi(version[1]) 1222 if err != nil { 1223 return 0, 0, fmt.Errorf("failed parsing %q to integer", minor) 1224 } 1225 return major, minor, nil 1226 } 1227 1228 func (g *Git) run(args ...string) error { 1229 var stdout, stderr bytes.Buffer 1230 if err := g.runGit(&stdout, &stderr, args...); err != nil { 1231 return Error(stdout.String(), stderr.String(), err, g.rootDir, args...) 1232 } 1233 return nil 1234 } 1235 1236 func trimOutput(o string) []string { 1237 output := strings.TrimSpace(o) 1238 if len(output) == 0 { 1239 return nil 1240 } 1241 return strings.Split(output, "\n") 1242 } 1243 1244 func (g *Git) runOutput(args ...string) ([]string, error) { 1245 var stdout, stderr bytes.Buffer 1246 if err := g.runGit(&stdout, &stderr, args...); err != nil { 1247 return nil, Error(stdout.String(), stderr.String(), err, g.rootDir, args...) 1248 } 1249 return trimOutput(stdout.String()), nil 1250 } 1251 1252 func (g *Git) runInteractive(args ...string) error { 1253 var stderr bytes.Buffer 1254 // In order for the editing to work correctly with 1255 // terminal-based editors, notably "vim", use os.Stdout. 1256 if err := g.runGit(os.Stdout, &stderr, args...); err != nil { 1257 return Error("", stderr.String(), err, g.rootDir, args...) 1258 } 1259 return nil 1260 } 1261 1262 func (g *Git) runGit(stdout, stderr io.Writer, args ...string) error { 1263 if g.userName != "" { 1264 args = append([]string{"-c", fmt.Sprintf("user.name=%s", g.userName)}, args...) 1265 } 1266 if g.userEmail != "" { 1267 args = append([]string{"-c", fmt.Sprintf("user.email=%s", g.userEmail)}, args...) 1268 } 1269 var outbuf bytes.Buffer 1270 var errbuf bytes.Buffer 1271 command := exec.Command("git", args...) 1272 command.Dir = g.rootDir 1273 command.Stdin = os.Stdin 1274 command.Stdout = io.MultiWriter(stdout, &outbuf) 1275 command.Stderr = io.MultiWriter(stderr, &errbuf) 1276 env := g.jirix.Env() 1277 env = envvar.MergeMaps(g.opts, env) 1278 command.Env = envvar.MapToSlice(env) 1279 dir := g.rootDir 1280 if dir == "" { 1281 if cwd, err := os.Getwd(); err == nil { 1282 dir = cwd 1283 } else { 1284 // ignore error 1285 } 1286 } 1287 g.jirix.Logger.Tracef("Run: git %s (%s)", strings.Join(args, " "), dir) 1288 err := command.Run() 1289 exitCode := 0 1290 if err != nil { 1291 if exitError, ok := err.(*exec.ExitError); ok { 1292 exitCode = exitError.ExitCode() 1293 } 1294 } 1295 g.jirix.Logger.Tracef("Finished: git %s (%s), \nstdout: %s\nstderr: %s\nexit code: %v\n", strings.Join(args, " "), dir, outbuf.String(), errbuf.String(), exitCode) 1296 return err 1297 } 1298 1299 // Committer encapsulates the process of create a commit. 1300 type Committer struct { 1301 commit func() error 1302 commitWithMessage func(message string) error 1303 } 1304 1305 // Commit creates a commit. 1306 func (c *Committer) Commit(message string) error { 1307 if len(message) == 0 { 1308 // No commit message supplied, let git supply one. 1309 return c.commit() 1310 } 1311 return c.commitWithMessage(message) 1312 } 1313 1314 // NewCommitter is the Committer factory. The boolean <edit> flag 1315 // determines whether the commit commands should prompt users to edit 1316 // the commit message. This flag enables automated testing. 1317 func (g *Git) NewCommitter(edit bool) *Committer { 1318 if edit { 1319 return &Committer{ 1320 commit: g.CommitAndEdit, 1321 commitWithMessage: g.CommitWithMessageAndEdit, 1322 } 1323 } else { 1324 return &Committer{ 1325 commit: g.Commit, 1326 commitWithMessage: g.CommitWithMessage, 1327 } 1328 } 1329 }