git-codereview: fix a few corner case failures
- make it clearer what the random git command at the end of failures means
- avoid some problems with detached HEAD mode in hooks run during git rebase
- redirect all stdout/stderr into test buffers
Change-Id: I102f22fc870f69c884728eaa46ecc95792d5b795
Reviewed-on: https://go-review.googlesource.com/2011
Reviewed-by: Andrew Gerrand <adg@golang.org>
Reviewed-by: Austin Clements <austin@google.com>
diff --git a/git-codereview/branch.go b/git-codereview/branch.go
index 9c9d8e7..40288c7 100644
--- a/git-codereview/branch.go
+++ b/git-codereview/branch.go
@@ -7,7 +7,6 @@
import (
"bytes"
"fmt"
- "os"
"os/exec"
"regexp"
"strings"
@@ -34,10 +33,24 @@
return &Branch{Name: name}
}
+// DetachedHead reports whether branch b corresponds to a detached HEAD
+// (does not have a real branch name).
+func (b *Branch) DetachedHead() bool {
+ return b.Name == "HEAD"
+}
+
// OriginBranch returns the name of the origin branch that branch b tracks.
// The returned name is like "origin/master" or "origin/dev.garbage" or
// "origin/release-branch.go1.4".
func (b *Branch) OriginBranch() string {
+ if b.DetachedHead() {
+ // Detached head mode.
+ // "origin/HEAD" is clearly false, but it should be easy to find when it
+ // appears in other commands. Really any caller of OriginBranch
+ // should check for detached head mode.
+ return "origin/HEAD"
+ }
+
if b.originBranch != "" {
return b.originBranch
}
@@ -52,7 +65,7 @@
b.originBranch = "origin/master"
return b.originBranch
}
- fmt.Fprintf(os.Stderr, "%v\n%s\n", commandString(argv[0], argv[1:]), out)
+ fmt.Fprintf(stderr(), "%v\n%s\n", commandString(argv[0], argv[1:]), out)
dief("%v", err)
panic("not reached")
}
@@ -99,6 +112,10 @@
}
b.loadedPending = true
+ if b.DetachedHead() {
+ return
+ }
+
const numField = 5
all := getOutput("git", "log", "--format=format:%H%x00%h%x00%P%x00%s%x00%B%x00", b.OriginBranch()+".."+b.Name)
fields := strings.Split(all, "\x00")
@@ -192,8 +209,20 @@
func LocalBranches() []*Branch {
var branches []*Branch
+ current := CurrentBranch()
for _, s := range getLines("git", "branch", "-q") {
- s = strings.TrimPrefix(strings.TrimSpace(s), "* ")
+ s = strings.TrimSpace(s)
+ if strings.HasPrefix(s, "* ") {
+ // * marks current branch in output.
+ // Normally the current branch has a name like any other,
+ // but in detached HEAD mode the branch listing shows
+ // a localized (translated) textual description instead of
+ // a branch name. Avoid language-specific differences
+ // by using CurrentBranch().Name for the current branch.
+ // It detects detached HEAD mode in a more portable way.
+ // (git rev-parse --abbrev-ref HEAD returns 'HEAD').
+ s = current.Name
+ }
branches = append(branches, &Branch{Name: s})
}
return branches
diff --git a/git-codereview/branch_test.go b/git-codereview/branch_test.go
index 10949fe..1c18cfa 100644
--- a/git-codereview/branch_test.go
+++ b/git-codereview/branch_test.go
@@ -4,7 +4,10 @@
package main
-import "testing"
+import (
+ "reflect"
+ "testing"
+)
func TestCurrentBranch(t *testing.T) {
gt := newGitTest(t)
@@ -34,6 +37,10 @@
write(t, gt.client+"/file", "i made another change")
trun(t, gt.client, "git", "commit", "-a", "-m", "My other change line.\n\nChange-Id: I1123456789abcdef0123456789abcdef\n")
checkCurrentBranch(t, "newdev", "origin/dev.branch", true, true, "I1123456789abcdef0123456789abcdef", "My other change line.")
+
+ t.Logf("detached head mode")
+ trun(t, gt.client, "git", "checkout", "HEAD^0")
+ checkCurrentBranch(t, "HEAD", "origin/HEAD", false, false, "", "")
}
func checkCurrentBranch(t *testing.T, name, origin string, isLocal, hasPending bool, changeID, subject string) {
@@ -57,3 +64,30 @@
t.Errorf("b.Subject() = %q, want %q", x, subject)
}
}
+
+func TestLocalBranches(t *testing.T) {
+ gt := newGitTest(t)
+ defer gt.done()
+
+ t.Logf("on master")
+ checkLocalBranches(t, "master")
+
+ t.Logf("on dev branch")
+ trun(t, gt.client, "git", "checkout", "-b", "newbranch")
+ checkLocalBranches(t, "master", "newbranch")
+
+ t.Logf("detached head mode")
+ trun(t, gt.client, "git", "checkout", "HEAD^0")
+ checkLocalBranches(t, "HEAD", "master", "newbranch")
+}
+
+func checkLocalBranches(t *testing.T, want ...string) {
+ var names []string
+ branches := LocalBranches()
+ for _, b := range branches {
+ names = append(names, b.Name)
+ }
+ if !reflect.DeepEqual(names, want) {
+ t.Errorf("LocalBranches() = %v, want %v", names, want)
+ }
+}
diff --git a/git-codereview/change.go b/git-codereview/change.go
index f7b8ec1..4281b6a 100644
--- a/git-codereview/change.go
+++ b/git-codereview/change.go
@@ -19,7 +19,7 @@
flags.BoolVar(&changeQuick, "q", false, "do not edit pending commit msg")
flags.Parse(args)
if len(flags.Args()) > 1 {
- fmt.Fprintf(os.Stderr, "Usage: %s change %s [branch]\n", os.Args[0], globalFlags)
+ fmt.Fprintf(stderr(), "Usage: %s change %s [branch]\n", os.Args[0], globalFlags)
os.Exit(2)
}
@@ -71,6 +71,10 @@
var testCommitMsg string
func commitChanges(amend bool) {
+ // git commit will run the gofmt hook.
+ // Run it now to give a better error (won't show a git commit command failing).
+ hookGofmt()
+
if HasUnstagedChanges() && !HasStagedChanges() && !changeAuto {
printf("warning: unstaged changes and no staged changes; use 'git add' or 'git change -a'")
}
@@ -102,6 +106,13 @@
}
func checkoutOrCreate(target string) {
+ if strings.ToUpper(target) == "HEAD" {
+ // Git gets very upset and confused if you 'git change head'
+ // on systems with case-insensitive file names: the branch
+ // head conflicts with the usual HEAD.
+ dief("invalid branch name %q: ref name HEAD is reserved for git.", target)
+ }
+
// If local branch exists, check it out.
for _, b := range LocalBranches() {
if b.Name == target {
diff --git a/git-codereview/change_test.go b/git-codereview/change_test.go
index 4a24c6a..daf06e3 100644
--- a/git-codereview/change_test.go
+++ b/git-codereview/change_test.go
@@ -36,6 +36,14 @@
testRan(t, "git checkout -q -t -b dev.branch origin/dev.branch")
}
+func TestChangeHEAD(t *testing.T) {
+ gt := newGitTest(t)
+ defer gt.done()
+
+ testMainDied(t, "change", "HeAd")
+ testPrintedStderr(t, "invalid branch name \"HeAd\": ref name HEAD is reserved for git")
+}
+
func TestMessageRE(t *testing.T) {
for _, c := range []struct {
in string
diff --git a/git-codereview/gofmt.go b/git-codereview/gofmt.go
index 4cde005..648b795 100644
--- a/git-codereview/gofmt.go
+++ b/git-codereview/gofmt.go
@@ -8,7 +8,6 @@
"bytes"
"flag"
"fmt"
- "io"
"os"
"os/exec"
"path/filepath"
@@ -22,7 +21,7 @@
flags.BoolVar(&gofmtList, "l", false, "list files that need to be formatted")
flags.Parse(args)
if len(flag.Args()) > 0 {
- fmt.Fprintf(os.Stderr, "Usage: %s gofmt %s [-l]\n", globalFlags, os.Args[0])
+ fmt.Fprintf(stderr(), "Usage: %s gofmt %s [-l]\n", globalFlags, os.Args[0])
os.Exit(2)
}
@@ -33,12 +32,9 @@
files, stderr := runGofmt(f)
if gofmtList {
- var out io.Writer = os.Stdout
- if stdoutTrap != nil {
- out = stdoutTrap
- }
+ w := stdout()
for _, file := range files {
- fmt.Fprintf(out, "%s\n", file)
+ fmt.Fprintf(w, "%s\n", file)
}
}
if stderr != "" {
diff --git a/git-codereview/gofmt_test.go b/git-codereview/gofmt_test.go
index a4b884c..8cd3d81 100644
--- a/git-codereview/gofmt_test.go
+++ b/git-codereview/gofmt_test.go
@@ -46,10 +46,10 @@
testPrintedStdout(t, "bad.go\n", "!good.go", "!test/bad", "test/bench/bad.go")
testMain(t, "gofmt")
- testPrintedStdout(t, "!.go") // no output
+ testNoStdout(t)
testMain(t, "gofmt", "-l")
- testPrintedStdout(t, "!.go") // no output
+ testNoStdout(t)
write(t, gt.client+"/bad.go", badGo)
write(t, gt.client+"/broken.go", brokenGo)
@@ -146,7 +146,7 @@
// Reformat in place.
testMainDied(t, "gofmt")
- testPrintedStdout(t, "!.go")
+ testNoStdout(t)
testPrintedStderr(t, wantErr...)
// Read files to make sure unstaged did not bleed into staged.
@@ -166,6 +166,6 @@
// Check that gofmt -l still shows the errors.
testMainDied(t, "gofmt", "-l")
- testPrintedStdout(t, "!.go")
+ testNoStdout(t)
testPrintedStderr(t, wantErr...)
}
diff --git a/git-codereview/hook.go b/git-codereview/hook.go
index 165e6e8..921fd91 100644
--- a/git-codereview/hook.go
+++ b/git-codereview/hook.go
@@ -156,6 +156,11 @@
func hookPreCommit(args []string) {
// Prevent commits to master branches.
b := CurrentBranch()
+ if b.DetachedHead() {
+ // This is an internal commit such as during git rebase.
+ // Don't die, and don't force gofmt.
+ return
+ }
if !b.IsLocalOnly() {
dief("cannot commit on %s branch", b.Name)
}
diff --git a/git-codereview/hook_test.go b/git-codereview/hook_test.go
index 6cd822f..15b04a0 100644
--- a/git-codereview/hook_test.go
+++ b/git-codereview/hook_test.go
@@ -43,7 +43,7 @@
write(t, gt.client+"/empty.txt", "\n\n# just a file with\n# comments\n")
testMainDied(t, "hook-invoke", "commit-msg", gt.client+"/empty.txt")
const want = "git-codereview: empty commit message\n"
- if got := stderr.String(); got != want {
+ if got := testStderr.String(); got != want {
t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, want)
}
}
@@ -81,6 +81,55 @@
"gofmt reported errors:", "broken.go")
}
+func TestHookChangeGofmt(t *testing.T) {
+ // During git change, we run the gofmt check before invoking commit,
+ // so we should not see the line about 'git commit' failing.
+ // That is, the failure should come from git change, not from
+ // the commit hook.
+ gt := newGitTest(t)
+ defer gt.done()
+ gt.work(t)
+
+ // Write out a non-Go file.
+ write(t, gt.client+"/bad.go", badGo)
+ trun(t, gt.client, "git", "add", ".")
+
+ t.Logf("invoking commit hook explicitly")
+ testMainDied(t, "hook-invoke", "pre-commit")
+ testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go")
+
+ t.Logf("change without hook installed")
+ testCommitMsg = "foo: msg"
+ testMainDied(t, "change")
+ testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go", "!running: git")
+
+ t.Logf("change with hook installed")
+ restore := testInstallHook(t, gt)
+ defer restore()
+ testCommitMsg = "foo: msg"
+ testMainDied(t, "change")
+ testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go", "!running: git")
+}
+
+func TestHookPreCommitDetachedHead(t *testing.T) {
+ // If we're in detached head mode, something special is going on,
+ // like git rebase. We disable the gofmt-checking precommit hook,
+ // since we expect it would just get in the way at that point.
+ // (It also used to crash.)
+
+ gt := newGitTest(t)
+ defer gt.done()
+ gt.work(t)
+
+ write(t, gt.client+"/bad.go", badGo)
+ trun(t, gt.client, "git", "add", ".")
+ trun(t, gt.client, "git", "checkout", "HEAD^0")
+
+ testMain(t, "hook-invoke", "pre-commit")
+ testNoStdout(t)
+ testNoStderr(t)
+}
+
func TestHookPreCommitUnstaged(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
@@ -181,17 +230,25 @@
}
}
+func testInstallHook(t *testing.T, gt *gitTest) (restore func()) {
+ trun(t, gt.pwd, "go", "build", "-o", gt.client+"/git-codereview")
+ path := os.Getenv("PATH")
+ os.Setenv("PATH", gt.client+string(filepath.ListSeparator)+path)
+ gt.removeStubHooks()
+ testMain(t, "hooks") // install hooks
+
+ return func() {
+ os.Setenv("PATH", path)
+ }
+}
+
func TestHookCommitMsgFromGit(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
- trun(t, gt.pwd, "go", "build", "-o", gt.client+"/git-codereview")
- path := os.Getenv("PATH")
- defer os.Setenv("PATH", path)
- os.Setenv("PATH", gt.client+string(filepath.ListSeparator)+path)
+ restore := testInstallHook(t, gt)
+ defer restore()
- gt.removeStubHooks()
- testMain(t, "hooks") // install hooks
testMain(t, "change", "mybranch")
write(t, gt.client+"/file", "more data")
trun(t, gt.client, "git", "add", "file")
diff --git a/git-codereview/mail.go b/git-codereview/mail.go
index 01dd4b8..c06a784 100644
--- a/git-codereview/mail.go
+++ b/git-codereview/mail.go
@@ -22,7 +22,7 @@
flags.Var(ccList, "cc", "comma-separated list of people to CC:")
flags.Usage = func() {
- fmt.Fprintf(os.Stderr, "Usage: %s mail %s [-r reviewer,...] [-cc mail,...]\n", os.Args[0], globalFlags)
+ fmt.Fprintf(stderr(), "Usage: %s mail %s [-r reviewer,...] [-cc mail,...]\n", os.Args[0], globalFlags)
}
flags.Parse(args)
if len(flags.Args()) != 0 {
diff --git a/git-codereview/pending.go b/git-codereview/pending.go
index e4e2a96..a90f6ce 100644
--- a/git-codereview/pending.go
+++ b/git-codereview/pending.go
@@ -52,7 +52,7 @@
flags.BoolVar(&pendingLocal, "l", false, "use only local information - no network operations")
flags.Parse(args)
if len(flags.Args()) > 0 {
- fmt.Fprintf(os.Stderr, "Usage: %s pending %s [-l]\n", os.Args[0], globalFlags)
+ fmt.Fprintf(stderr(), "Usage: %s pending %s [-l]\n", os.Args[0], globalFlags)
os.Exit(2)
}
@@ -219,11 +219,7 @@
fmt.Fprintf(&buf, "\n")
}
- if stdoutTrap != nil {
- stdoutTrap.Write(buf.Bytes())
- } else {
- os.Stdout.Write(buf.Bytes())
- }
+ stdout().Write(buf.Bytes())
}
// errors returns any errors that should be displayed
diff --git a/git-codereview/pending_test.go b/git-codereview/pending_test.go
index b1030e2..68c0f00 100644
--- a/git-codereview/pending_test.go
+++ b/git-codereview/pending_test.go
@@ -241,7 +241,7 @@
want = strings.TrimPrefix(want, "\n")
testMain(t, "pending")
- out := stdout.Bytes()
+ out := testStdout.Bytes()
out = regexp.MustCompile(`\b[0-9a-f]{7}\b`).ReplaceAllLiteral(out, []byte("REVHASH"))
out = regexp.MustCompile(`\b127\.0\.0\.1:\d+\b`).ReplaceAllLiteral(out, []byte("127.0.0.1:PORT"))
diff --git a/git-codereview/review.go b/git-codereview/review.go
index a2a60fb..fe8867e 100644
--- a/git-codereview/review.go
+++ b/git-codereview/review.go
@@ -29,7 +29,7 @@
func initFlags() {
flags = flag.NewFlagSet("", flag.ExitOnError)
flags.Usage = func() {
- fmt.Fprintf(os.Stderr, usage, os.Args[0], os.Args[0])
+ fmt.Fprintf(stderr(), usage, os.Args[0], os.Args[0])
}
flags.Var(verbose, "v", "report git commands")
flags.BoolVar(noRun, "n", false, "print but do not run commands")
@@ -117,7 +117,7 @@
command, args := os.Args[1], os.Args[2:]
if command == "help" {
- fmt.Fprintf(os.Stdout, help, os.Args[0])
+ fmt.Fprintf(stdout(), help, os.Args[0])
return
}
@@ -148,7 +148,7 @@
func expectZeroArgs(args []string, command string) {
flags.Parse(args)
if len(flags.Args()) > 0 {
- fmt.Fprintf(os.Stderr, "Usage: %s %s %s\n", os.Args[0], command, globalFlags)
+ fmt.Fprintf(stderr(), "Usage: %s %s %s\n", os.Args[0], command, globalFlags)
os.Exit(2)
}
}
@@ -158,7 +158,7 @@
if *verbose == 0 {
// If we're not in verbose mode, print the command
// before dying to give context to the failure.
- fmt.Fprintln(os.Stderr, commandString(command, args))
+ fmt.Fprintf(stderr(), "(running: %s)\n", commandString(command, args))
}
dief("%v", err)
}
@@ -172,7 +172,7 @@
func runDirErr(dir, command string, args ...string) error {
if *verbose > 0 || *noRun {
- fmt.Fprintln(os.Stderr, commandString(command, args))
+ fmt.Fprintln(stderr(), commandString(command, args))
}
if *noRun {
return nil
@@ -182,14 +182,8 @@
}
cmd := exec.Command(command, args...)
cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- if stdoutTrap != nil {
- cmd.Stdout = stdoutTrap
- }
- cmd.Stderr = os.Stderr
- if stderrTrap != nil {
- cmd.Stderr = stderrTrap
- }
+ cmd.Stdout = stdout()
+ cmd.Stderr = stderr()
return cmd.Run()
}
@@ -201,7 +195,7 @@
func getOutput(command string, args ...string) string {
s, err := getOutputErr(command, args...)
if err != nil {
- fmt.Fprintf(os.Stderr, "%v\n%s\n", commandString(command, args), s)
+ fmt.Fprintf(stderr(), "%v\n%s\n", commandString(command, args), s)
dief("%v", err)
}
return s
@@ -215,7 +209,7 @@
// the git repo" commands, which is confusing if you are just trying to find
// out what git sync means.
if *verbose > 1 {
- fmt.Fprintln(os.Stderr, commandString(command, args))
+ fmt.Fprintln(stderr(), commandString(command, args))
}
b, err := exec.Command(command, args...).CombinedOutput()
return string(bytes.TrimSpace(b)), err
@@ -257,12 +251,22 @@
var stdoutTrap, stderrTrap *bytes.Buffer
-func printf(format string, args ...interface{}) {
- w := io.Writer(os.Stderr)
- if stderrTrap != nil {
- w = stderrTrap
+func stdout() io.Writer {
+ if stdoutTrap != nil {
+ return stdoutTrap
}
- fmt.Fprintf(w, "%s: %s\n", os.Args[0], fmt.Sprintf(format, args...))
+ return os.Stdout
+}
+
+func stderr() io.Writer {
+ if stderrTrap != nil {
+ return stderrTrap
+ }
+ return os.Stderr
+}
+
+func printf(format string, args ...interface{}) {
+ fmt.Fprintf(stderr(), "%s: %s\n", os.Args[0], fmt.Sprintf(format, args...))
}
// count is a flag.Value that is like a flag.Bool and a flag.Int.
diff --git a/git-codereview/util_test.go b/git-codereview/util_test.go
index 27ed6c9..0be544a 100644
--- a/git-codereview/util_test.go
+++ b/git-codereview/util_test.go
@@ -125,10 +125,10 @@
}
var (
- runLog []string
- stderr *bytes.Buffer
- stdout *bytes.Buffer
- died bool
+ runLog []string
+ testStderr *bytes.Buffer
+ testStdout *bytes.Buffer
+ died bool
)
var mainCanDie bool
@@ -137,7 +137,7 @@
mainCanDie = true
testMain(t, args...)
if !died {
- t.Fatalf("expected to die, did not\nstdout:\n%sstderr:\n%s", stdout, stderr)
+ t.Fatalf("expected to die, did not\nstdout:\n%sstderr:\n%s", testStdout, testStderr)
}
}
@@ -157,8 +157,8 @@
defer func() {
runLog = runLogTrap
- stdout = stdoutTrap
- stderr = stderrTrap
+ testStdout = stdoutTrap
+ testStderr = stderrTrap
dieTrap = nil
runLogTrap = nil
@@ -174,7 +174,7 @@
} else {
msg = fmt.Sprintf("panic: %v", err)
}
- t.Fatalf("%s\nstdout:\n%sstderr:\n%s", msg, stdout, stderr)
+ t.Fatalf("%s\nstdout:\n%sstderr:\n%s", msg, testStdout, testStderr)
}
}()
@@ -221,22 +221,22 @@
}
func testPrintedStdout(t *testing.T, messages ...string) {
- testPrinted(t, stdout, "stdout", messages...)
+ testPrinted(t, testStdout, "stdout", messages...)
}
func testPrintedStderr(t *testing.T, messages ...string) {
- testPrinted(t, stderr, "stderr", messages...)
+ testPrinted(t, testStderr, "stderr", messages...)
}
func testNoStdout(t *testing.T) {
- if stdout.Len() != 0 {
- t.Fatalf("unexpected stdout:\n%s", stdout)
+ if testStdout.Len() != 0 {
+ t.Fatalf("unexpected stdout:\n%s", testStdout)
}
}
func testNoStderr(t *testing.T) {
- if stderr.Len() != 0 {
- t.Fatalf("unexpected stderr:\n%s", stderr)
+ if testStderr.Len() != 0 {
+ t.Fatalf("unexpected stderr:\n%s", testStderr)
}
}