git-review: add gofmt command

- move gofmt invocation code from hook.go into gofmt.go
- use true branch point instead of HEAD^
- handle gofmt errors (syntax errors in Go files)
- apply gofmt command to both index and working tree, independently
- fix gofmt pre-commit hook to use index, not working tree

Change-Id: I7ed11c52531bb63b8af9ca5dd881bbfcdf5d5413
Reviewed-on: https://go-review.googlesource.com/1682
Reviewed-by: Andrew Gerrand <adg@golang.org>
Reviewed-by: Austin Clements <austin@google.com>
diff --git a/git-review/branch.go b/git-review/branch.go
index 4cc8a0e..ad82a46 100644
--- a/git-review/branch.go
+++ b/git-review/branch.go
@@ -81,6 +81,18 @@
 	return b.commitHash
 }
 
+// Branchpoint returns an identifier for the latest revision
+// common to both this branch and its upstream branch.
+// If this branch has not split from upstream,
+// Branchpoint returns "HEAD".
+func (b *Branch) Branchpoint() string {
+	b.loadPending()
+	if b.parentHash == "" {
+		return "HEAD"
+	}
+	return b.parentHash
+}
+
 func (b *Branch) loadPending() {
 	if b.loadedPending {
 		return
@@ -99,17 +111,20 @@
 		parent := fields[i+2]
 		subject := fields[i+3]
 		msg := fields[i+4]
-		if i == 0 {
-			b.commitHash = hash
-			b.shortCommitHash = shortHash
-			b.parentHash = parent
-			b.subject = subject
-			b.message = msg
-			for _, line := range strings.Split(msg, "\n") {
-				if strings.HasPrefix(line, "Change-Id: ") {
-					b.changeID = line[len("Change-Id: "):]
-					break
-				}
+
+		// Overwrite each time through the loop.
+		// We want to save the info about the *first* commit
+		// after the branch point, and the log is ordered
+		// starting at the most recent and working backward.
+		b.commitHash = hash
+		b.shortCommitHash = shortHash
+		b.parentHash = parent
+		b.subject = subject
+		b.message = msg
+		for _, line := range strings.Split(msg, "\n") {
+			if strings.HasPrefix(line, "Change-Id: ") {
+				b.changeID = line[len("Change-Id: "):]
+				break
 			}
 		}
 		b.commitsAhead++
diff --git a/git-review/gofmt.go b/git-review/gofmt.go
new file mode 100644
index 0000000..4cde005
--- /dev/null
+++ b/git-review/gofmt.go
@@ -0,0 +1,315 @@
+// Copyright 2014 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"sort"
+	"strings"
+)
+
+var gofmtList bool
+
+func gofmt(args []string) {
+	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])
+		os.Exit(2)
+	}
+
+	f := gofmtCommand
+	if !gofmtList {
+		f |= gofmtWrite
+	}
+
+	files, stderr := runGofmt(f)
+	if gofmtList {
+		var out io.Writer = os.Stdout
+		if stdoutTrap != nil {
+			out = stdoutTrap
+		}
+		for _, file := range files {
+			fmt.Fprintf(out, "%s\n", file)
+		}
+	}
+	if stderr != "" {
+		dief("gofmt reported errors:\n\t%s", strings.Replace(strings.TrimSpace(stderr), "\n", "\n\t", -1))
+	}
+}
+
+const (
+	gofmtPreCommit = 1 << iota
+	gofmtCommand
+	gofmtWrite
+)
+
+// runGofmt runs the external gofmt command over modified files.
+//
+// The definition of "modified files" depends on the bit flags.
+// If gofmtPreCommit is set, then runGofmt considers *.go files that
+// differ between the index (staging area) and the branchpoint
+// (the latest commit before the branch diverged from upstream).
+// If gofmtCommand is set, then runGofmt considers all those files
+// in addition to files with unstaged modifications.
+// It never considers untracked files.
+//
+// As a special case for the main repo (but applied everywhere)
+// *.go files under a top-level test directory are excluded from the
+// formatting requirement, except those in test/bench/.
+//
+// If gofmtWrite is set (only with gofmtCommand, meaning this is 'git gofmt'),
+// runGofmt replaces the original files with their formatted equivalents.
+// Git makes this difficult. In general the file in the working tree
+// (the local file system) can have unstaged changes that make it different
+// from the equivalent file in the index. To help pass the precommit hook,
+// 'git gofmt'  must make it easy to update the files in the index.
+// One option is to run gofmt on all the files of the same name in the
+// working tree and leave it to the user to sort out what should be staged
+// back into the index. Another is to refuse to reformat files for which
+// different versions exist in the index vs the working tree. Both of these
+// options are unsatisfying: they foist busy work onto the user,
+// and it's exactly the kind of busy work that a program is best for.
+// Instead, when runGofmt finds files in the index that need
+// reformatting, it reformats them there, bypassing the working tree.
+// It also reformats files in the working tree that need reformatting.
+// For both, only files modified since the branchpoint are considered.
+// The result should be that both index and working tree get formatted
+// correctly and diffs between the two remain meaningful (free of
+// formatting distractions). Modifying files in the index directly may
+// surprise Git users, but it seems the best of a set of bad choices, and
+// of course those users can choose not to use 'git gofmt'.
+// This is a bit more work than the other git commands do, which is
+// a little worrying, but the choice being made has the nice property
+// that if 'git gofmt' is interrupted, a second 'git gofmt' will put things into
+// the same state the first would have.
+//
+// runGofmt returns a list of files that need (or needed) reformatting.
+// If gofmtPreCommit is set, the names always refer to files in the index.
+// If gofmtCommand is set, then a name without a suffix (see below)
+// refers to both the copy in the index and the copy in the working tree
+// and implies that the two copies are identical. Otherwise, in the case
+// that the index and working tree differ, the file name will have an explicit
+// " (staged)" or " (unstaged)" suffix saying which is meant.
+//
+// runGofmt also returns any standard error output from gofmt,
+// usually indicating syntax errors in the Go source files.
+// If gofmtCommand is set, syntax errors in index files that do not match
+// the working tree show a " (staged)" suffix after the file name.
+// The errors never use the " (unstaged)" suffix, in order to keep
+// references to the local file system in the standard file:line form.
+func runGofmt(flags int) (files []string, stderrText string) {
+	pwd, err := os.Getwd()
+	if err != nil {
+		dief("%v", err)
+	}
+	pwd = filepath.Clean(pwd) // convert to host \ syntax
+	if !strings.HasSuffix(pwd, string(filepath.Separator)) {
+		pwd += string(filepath.Separator)
+	}
+
+	b := CurrentBranch()
+	repo := repoRoot()
+	if !strings.HasSuffix(repo, string(filepath.Separator)) {
+		repo += string(filepath.Separator)
+	}
+
+	// Find files modified in the index compared to the branchpoint.
+	indexFiles := addRoot(repo, filter(gofmtRequired, getLines("git", "diff", "--name-only", "--diff-filter=ACM", "--cached", b.Branchpoint())))
+	localFiles := addRoot(repo, filter(gofmtRequired, getLines("git", "diff", "--name-only", "--diff-filter=ACM")))
+	localFilesMap := stringMap(localFiles)
+	isUnstaged := func(file string) bool {
+		return localFilesMap[file]
+	}
+
+	if len(indexFiles) == 0 && ((flags&gofmtCommand) == 0 || len(localFiles) == 0) {
+		return
+	}
+
+	// Determine which files have unstaged changes and are therefore
+	// different from their index versions. For those, the index version must
+	// be copied into a temporary file in the local file system.
+	needTemp := filter(isUnstaged, indexFiles)
+
+	// Map between temporary file name and place in file tree where
+	// file would be checked out (if not for the unstaged changes).
+	tempToFile := map[string]string{}
+	fileToTemp := map[string]string{}
+	cleanup := func() {} // call before dying (defer won't run)
+	if len(needTemp) > 0 {
+		// Ask Git to copy the index versions into temporary files.
+		// Git stores the temporary files, named .merge_*, in the repo root.
+		// Unlike the Git commands above, the non-temp file names printed
+		// here are relative to the current directory, not the repo root.
+		args := []string{"checkout-index", "--temp", "--"}
+		args = append(args, needTemp...)
+		for _, line := range getLines("git", args...) {
+			i := strings.Index(line, "\t")
+			if i < 0 {
+				continue
+			}
+			temp, file := line[:i], line[i+1:]
+			temp = filepath.Join(repo, temp)
+			file = filepath.Join(pwd, file)
+			tempToFile[temp] = file
+			fileToTemp[file] = temp
+		}
+		cleanup = func() {
+			for temp := range tempToFile {
+				os.Remove(temp)
+			}
+			tempToFile = nil
+		}
+		defer cleanup()
+	}
+	dief := func(format string, args ...interface{}) {
+		cleanup()
+		dief(format, args...) // calling top-level dief function
+	}
+
+	// Run gofmt to find out which files need reformatting;
+	// if gofmtWrite is set, reformat them in place.
+	// For references to local files, remove leading pwd if present
+	// to make relative to current directory.
+	// Temp files and local-only files stay as absolute paths for easy matching in output.
+	args := []string{"-l"}
+	if flags&gofmtWrite != 0 {
+		args = append(args, "-w")
+	}
+	for _, file := range indexFiles {
+		if isUnstaged(file) {
+			args = append(args, fileToTemp[file])
+		} else {
+			args = append(args, strings.TrimPrefix(file, pwd))
+		}
+	}
+	if flags&gofmtCommand != 0 {
+		for _, file := range localFiles {
+			args = append(args, file)
+		}
+	}
+
+	cmd := exec.Command("gofmt", args...)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err = cmd.Run()
+
+	if stderr.Len() == 0 && err != nil {
+		// Error but no stderr: usually can't find gofmt.
+		dief("invoking gofmt: %v", err)
+	}
+
+	// Build file list.
+	files = strings.Split(stdout.String(), "\n")
+	if len(files) > 0 && files[len(files)-1] == "" {
+		files = files[:len(files)-1]
+	}
+
+	// Restage files that need to be restaged.
+	if flags&gofmtWrite != 0 {
+		add := []string{"add"}
+		write := []string{"hash-object", "-w", "--"}
+		updateIndex := []string{}
+		for _, file := range files {
+			if real := tempToFile[file]; real != "" {
+				write = append(write, file)
+				updateIndex = append(updateIndex, strings.TrimPrefix(real, repo))
+			} else if !isUnstaged(file) {
+				add = append(add, file)
+			}
+		}
+		if len(add) > 1 {
+			run("git", add...)
+		}
+		if len(updateIndex) > 0 {
+			hashes := getLines("git", write...)
+			if len(hashes) != len(write)-3 {
+				dief("git hash-object -w did not write expected number of objects")
+			}
+			var buf bytes.Buffer
+			for i, name := range updateIndex {
+				fmt.Fprintf(&buf, "100644 %s\t%s\n", hashes[i], name)
+			}
+			cmd := exec.Command("git", "update-index", "--index-info")
+			cmd.Stdin = &buf
+			out, err := cmd.CombinedOutput()
+			if err != nil {
+				dief("git update-index: %v\n%s", err, out)
+			}
+		}
+	}
+
+	// Remap temp files back to original names for caller.
+	for i, file := range files {
+		if real := tempToFile[file]; real != "" {
+			if flags&gofmtCommand != 0 {
+				real += " (staged)"
+			}
+			files[i] = strings.TrimPrefix(real, pwd)
+		} else if isUnstaged(file) {
+			files[i] = strings.TrimPrefix(file+" (unstaged)", pwd)
+		}
+	}
+
+	// Rewrite temp names in stderr, and shorten local file names.
+	// No suffix added for local file names (see comment above).
+	text := "\n" + stderr.String()
+	for temp, file := range tempToFile {
+		if flags&gofmtCommand != 0 {
+			file += " (staged)"
+		}
+		text = strings.Replace(text, "\n"+temp+":", "\n"+strings.TrimPrefix(file, pwd)+":", -1)
+	}
+	for _, file := range localFiles {
+		text = strings.Replace(text, "\n"+file+":", "\n"+strings.TrimPrefix(file, pwd)+":", -1)
+	}
+	text = text[1:]
+
+	sort.Strings(files)
+	return files, text
+}
+
+// gofmtRequired reports whether the specified file should be checked
+// for gofmt'dness by the pre-commit hook.
+// The file name is relative to the repo root.
+func gofmtRequired(file string) bool {
+	return strings.HasSuffix(file, ".go") &&
+		!(strings.HasPrefix(file, "test/") && !strings.HasPrefix(file, "test/bench/"))
+}
+
+// stringMap returns a map m such that m[s] == true if s was in the original list.
+func stringMap(list []string) map[string]bool {
+	m := map[string]bool{}
+	for _, x := range list {
+		m[x] = true
+	}
+	return m
+}
+
+// filter returns the elements in list satisfying f.
+func filter(f func(string) bool, list []string) []string {
+	var out []string
+	for _, x := range list {
+		if f(x) {
+			out = append(out, x)
+		}
+	}
+	return out
+}
+
+func addRoot(root string, list []string) []string {
+	var out []string
+	for _, x := range list {
+		out = append(out, filepath.Join(root, x))
+	}
+	return out
+}
diff --git a/git-review/gofmt_test.go b/git-review/gofmt_test.go
new file mode 100644
index 0000000..a4b884c
--- /dev/null
+++ b/git-review/gofmt_test.go
@@ -0,0 +1,171 @@
+// Copyright 2014 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+)
+
+const (
+	goodGo      = "package good\n"
+	badGo       = " package bad1 "
+	badGoFixed  = "package bad1\n"
+	bad2Go      = " package bad2 "
+	bad2GoFixed = "package bad2\n"
+	brokenGo    = "package B R O K E N"
+)
+
+func TestGofmt(t *testing.T) {
+	// Test of basic operations.
+	gt := newGitTest(t)
+	defer gt.done()
+
+	gt.work(t)
+
+	if err := os.MkdirAll(gt.client+"/test/bench", 0755); err != nil {
+		t.Fatal(err)
+	}
+	write(t, gt.client+"/bad.go", badGo)
+	write(t, gt.client+"/good.go", goodGo)
+	write(t, gt.client+"/test/bad.go", badGo)
+	write(t, gt.client+"/test/good.go", goodGo)
+	write(t, gt.client+"/test/bench/bad.go", badGo)
+	write(t, gt.client+"/test/bench/good.go", goodGo)
+	trun(t, gt.client, "git", "add", ".") // make files tracked
+
+	testMain(t, "gofmt", "-l")
+	testPrintedStdout(t, "bad.go\n", "!good.go", "!test/bad", "test/bench/bad.go")
+
+	testMain(t, "gofmt", "-l")
+	testPrintedStdout(t, "bad.go\n", "!good.go", "!test/bad", "test/bench/bad.go")
+
+	testMain(t, "gofmt")
+	testPrintedStdout(t, "!.go") // no output
+
+	testMain(t, "gofmt", "-l")
+	testPrintedStdout(t, "!.go") // no output
+
+	write(t, gt.client+"/bad.go", badGo)
+	write(t, gt.client+"/broken.go", brokenGo)
+	trun(t, gt.client, "git", "add", ".")
+	testMainDied(t, "gofmt", "-l")
+	testPrintedStdout(t, "bad.go")
+	testPrintedStderr(t, "gofmt reported errors", "broken.go")
+}
+
+func TestGofmtUnstaged(t *testing.T) {
+	// Test when unstaged files are different from staged ones.
+	// See TestHookPreCommitUnstaged for an explanation.
+	// In this test we use two different kinds of bad files, so that
+	// we can test having a bad file in the index and a different
+	// bad file in the working directory.
+
+	gt := newGitTest(t)
+	defer gt.done()
+	gt.work(t)
+
+	name := []string{"good", "bad", "bad2", "broken"}
+	orig := []string{goodGo, badGo, bad2Go, brokenGo}
+	fixed := []string{goodGo, badGoFixed, bad2GoFixed, brokenGo}
+	const N = 4
+
+	var allFiles, wantOut, wantErr []string
+	writeFiles := func(n int) {
+		allFiles = nil
+		wantOut = nil
+		wantErr = nil
+		for i := 0; i < N*N*N; i++ {
+			// determine n'th digit of 3-digit base-N value i
+			j := i
+			for k := 0; k < (3 - 1 - n); k++ {
+				j /= N
+			}
+			text := orig[j%N]
+			file := fmt.Sprintf("%s-%s-%s.go", name[i/N/N], name[(i/N)%N], name[i%N])
+			allFiles = append(allFiles, file)
+			write(t, gt.client+"/"+file, text)
+
+			if (i/N)%N != i%N {
+				staged := file + " (staged)"
+				switch {
+				case strings.Contains(file, "-bad-"), strings.Contains(file, "-bad2-"):
+					wantOut = append(wantOut, staged)
+					wantErr = append(wantErr, "!"+staged)
+				case strings.Contains(file, "-broken-"):
+					wantOut = append(wantOut, "!"+staged)
+					wantErr = append(wantErr, staged)
+				default:
+					wantOut = append(wantOut, "!"+staged)
+					wantErr = append(wantErr, "!"+staged)
+				}
+			}
+			switch {
+			case strings.Contains(file, "-bad.go"), strings.Contains(file, "-bad2.go"):
+				if (i/N)%N != i%N {
+					file += " (unstaged)"
+				}
+				wantOut = append(wantOut, file+"\n")
+				wantErr = append(wantErr, "!"+file+":", "!"+file+" (unstaged)")
+			case strings.Contains(file, "-broken.go"):
+				wantOut = append(wantOut, "!"+file+"\n", "!"+file+" (unstaged)")
+				wantErr = append(wantErr, file+":")
+			default:
+				wantOut = append(wantOut, "!"+file+"\n", "!"+file+":", "!"+file+" (unstaged)")
+				wantErr = append(wantErr, "!"+file+"\n", "!"+file+":", "!"+file+" (unstaged)")
+			}
+		}
+	}
+
+	// committed files
+	writeFiles(0)
+	trun(t, gt.client, "git", "add", ".")
+	trun(t, gt.client, "git", "commit", "-m", "msg")
+
+	// staged files
+	writeFiles(1)
+	trun(t, gt.client, "git", "add", ".")
+
+	// unstaged files
+	writeFiles(2)
+
+	// Check that gofmt -l shows the right output and errors.
+	testMainDied(t, "gofmt", "-l")
+	testPrintedStdout(t, wantOut...)
+	testPrintedStderr(t, wantErr...)
+
+	// Again (last command should not have written anything).
+	testMainDied(t, "gofmt", "-l")
+	testPrintedStdout(t, wantOut...)
+	testPrintedStderr(t, wantErr...)
+
+	// Reformat in place.
+	testMainDied(t, "gofmt")
+	testPrintedStdout(t, "!.go")
+	testPrintedStderr(t, wantErr...)
+
+	// Read files to make sure unstaged did not bleed into staged.
+	for i, file := range allFiles {
+		if data, err := ioutil.ReadFile(gt.client + "/" + file); err != nil {
+			t.Errorf("%v", err)
+		} else if want := fixed[i%N]; string(data) != want {
+			t.Errorf("%s: working tree = %q, want %q", file, string(data), want)
+		}
+		if data, want := trun(t, gt.client, "git", "show", ":"+file), fixed[i/N%N]; data != want {
+			t.Errorf("%s: index = %q, want %q", file, data, want)
+		}
+		if data, want := trun(t, gt.client, "git", "show", "HEAD:"+file), orig[i/N/N]; data != want {
+			t.Errorf("%s: commit = %q, want %q", file, data, want)
+		}
+	}
+
+	// Check that gofmt -l still shows the errors.
+	testMainDied(t, "gofmt", "-l")
+	testPrintedStdout(t, "!.go")
+	testPrintedStderr(t, wantErr...)
+}
diff --git a/git-review/hook.go b/git-review/hook.go
index f79d78d..fd8a9ef 100644
--- a/git-review/hook.go
+++ b/git-review/hook.go
@@ -151,41 +151,26 @@
 		dief("cannot commit on %s branch", b.Name)
 	}
 
-	root := repoRoot()
+	hookGofmt()
+}
 
-	// Run the gofmt check over the pending commit, if any.
-	rev := "HEAD"
-	if b.HasPendingCommit() {
-		rev = "HEAD^" // branch point, hopefully
-	}
-	// This command prints file names relative to the repo root.
-	files := getLines("git", "diff", "--cached", "--name-only", "--diff-filter=ACM", rev)
-	var goFiles []string // absolute paths of files that need gofmting
-	for _, file := range files {
-		if gofmtRequired(file) {
-			goFiles = append(goFiles, filepath.Join(root, file))
+func hookGofmt() {
+	files, stderr := runGofmt(gofmtPreCommit)
+
+	if stderr != "" {
+		msgf := printf
+		if len(files) == 0 {
+			msgf = dief
 		}
-	}
-	if len(goFiles) == 0 {
-		return
+		msgf("gofmt reported errors:\n\t%s", strings.Replace(strings.TrimSpace(stderr), "\n", "\n\t", -1))
 	}
 
-	// Check gofmt.
-	unfmted := getLines("gofmt", append([]string{"-l"}, goFiles...)...)
-	if len(unfmted) == 0 {
+	if len(files) == 0 {
 		return
 	}
 
 	dief("gofmt needs to format these files (run 'git gofmt'):\n\t%s",
-		strings.Join(unfmted, "\n\t"))
-}
-
-// gofmtRequired reports whether the specified file should be checked
-// for gofmt'dness by the pre-commit hook.
-// The file name is relative to the repo root.
-func gofmtRequired(file string) bool {
-	return strings.HasSuffix(file, ".go") &&
-		!(strings.HasPrefix(file, "test/") && !strings.HasPrefix(file, "test/bench/"))
+		strings.Join(files, "\n\t"))
 }
 
 // This is NOT USED ANYMORE.
diff --git a/git-review/hook_test.go b/git-review/hook_test.go
index 2ccc7c4..20b4b68 100644
--- a/git-review/hook_test.go
+++ b/git-review/hook_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"bytes"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -51,21 +52,15 @@
 	gt := newGitTest(t)
 	defer gt.done()
 
-	gt.removeStubHooks()
-	testMain(t, "hooks") // install hooks
-
 	// Write out a non-Go file.
 	testMain(t, "change", "mybranch")
 	write(t, gt.client+"/msg.txt", "A test message.")
 	trun(t, gt.client, "git", "add", "msg.txt")
 	testMain(t, "hook-invoke", "pre-commit") // should be no-op
 
-	// Write out a badly formatted Go files.
 	if err := os.MkdirAll(gt.client+"/test/bench", 0755); err != nil {
 		t.Fatal(err)
 	}
-	const badGo = "package main\nfunc main(){}"
-	const goodGo = "package main\n\nfunc main() {\n}\n"
 	write(t, gt.client+"/bad.go", badGo)
 	write(t, gt.client+"/good.go", goodGo)
 	write(t, gt.client+"/test/bad.go", badGo)
@@ -77,6 +72,79 @@
 	testMainDied(t, "hook-invoke", "pre-commit")
 	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):",
 		"bad.go", "!good.go", "!test/bad", "test/bench/bad.go")
+
+	write(t, gt.client+"/broken.go", brokenGo)
+	trun(t, gt.client, "git", "add", "broken.go")
+	testMainDied(t, "hook-invoke", "pre-commit")
+	testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):",
+		"bad.go", "!good.go", "!test/bad", "test/bench/bad.go",
+		"gofmt reported errors:", "broken.go")
+}
+
+func TestHookPreCommitUnstaged(t *testing.T) {
+	gt := newGitTest(t)
+	defer gt.done()
+	gt.work(t)
+
+	write(t, gt.client+"/bad.go", badGo)
+	write(t, gt.client+"/good.go", goodGo)
+
+	// The pre-commit hook is being asked about files in the index.
+	// Make sure it is not looking at files in the working tree (current directory) instead.
+	// There are three possible kinds of file: good, bad (misformatted), and broken (syntax error).
+	// There are also three possible places files live: the most recent commit, the index,
+	// and the working tree. We write a sequence of files that cover all possible
+	// combination of kinds of file in the various places. For example,
+	// good-bad-broken.go is a good file in the most recent commit,
+	// a bad file in the index, and a broken file in the working tree.
+	// After creating these files, we check that the gofmt hook reports
+	// about the index only.
+
+	const N = 3
+	name := []string{"good", "bad", "broken"}
+	content := []string{goodGo, badGo, brokenGo}
+	var wantErr []string
+	var allFiles []string
+	writeFiles := func(n int) {
+		allFiles = nil
+		wantErr = nil
+		for i := 0; i < N*N*N; i++ {
+			// determine n'th digit of 3-digit base-N value i
+			j := i
+			for k := 0; k < (3 - 1 - n); k++ {
+				j /= N
+			}
+			file := fmt.Sprintf("%s-%s-%s.go", name[i/N/N], name[(i/N)%N], name[i%N])
+			allFiles = append(allFiles, file)
+			write(t, gt.client+"/"+file, content[j%N])
+
+			switch {
+			case strings.Contains(file, "-bad-"):
+				wantErr = append(wantErr, "\t"+file+"\n")
+			case strings.Contains(file, "-broken-"):
+				wantErr = append(wantErr, "\t"+file+":")
+			default:
+				wantErr = append(wantErr, "!"+file)
+			}
+		}
+	}
+
+	// committed files
+	writeFiles(0)
+	trun(t, gt.client, "git", "add", ".")
+	trun(t, gt.client, "git", "commit", "-m", "msg")
+
+	// staged files
+	writeFiles(1)
+	trun(t, gt.client, "git", "add", ".")
+
+	// unstaged files
+	writeFiles(2)
+
+	wantErr = append(wantErr, "gofmt reported errors", "gofmt needs to format these files")
+
+	testMainDied(t, "hook-invoke", "pre-commit")
+	testPrintedStderr(t, wantErr...)
 }
 
 func TestHooks(t *testing.T) {
diff --git a/git-review/mail.go b/git-review/mail.go
index 99821e3..01dd4b8 100644
--- a/git-review/mail.go
+++ b/git-review/mail.go
@@ -36,7 +36,7 @@
 	}
 
 	if *diff {
-		run("git", "diff", "HEAD^..HEAD")
+		run("git", "diff", b.Branchpoint()+"..HEAD")
 		return
 	}
 
diff --git a/git-review/review.go b/git-review/review.go
index fa049a3..7097e8e 100644
--- a/git-review/review.go
+++ b/git-review/review.go
@@ -53,10 +53,12 @@
 		Create a change commit, or amend an existing change commit,
 		with the staged changes. If a branch name is provided, check
 		out that branch (creating it if it does not exist).
-		(Does not amend the existing commit when switching branches.)
+		Does not amend the existing commit when switching branches.
 
-	gofmt
-		TBD
+	gofmt [-l]
+		Run gofmt on all tracked files in the staging area and the working tree.
+		If -l is specified, list files that need formatting.
+		Otherwise, reformat files in place.
 
 	help
 		Show this help text.
@@ -111,7 +113,7 @@
 	case "change":
 		change(args)
 	case "gofmt":
-		dief("gofmt not implemented")
+		gofmt(args)
 	case "hook-invoke":
 		hookInvoke(args)
 	case "hooks":
@@ -148,9 +150,13 @@
 	}
 }
 
+func runErr(command string, args ...string) error {
+	return runDirErr("", command, args...)
+}
+
 var runLogTrap []string
 
-func runErr(command string, args ...string) error {
+func runDirErr(dir, command string, args ...string) error {
 	if *verbose > 0 || *noRun {
 		fmt.Fprintln(os.Stderr, commandString(command, args))
 	}
diff --git a/git-review/sync.go b/git-review/sync.go
index d96ba77..8df3427 100644
--- a/git-review/sync.go
+++ b/git-review/sync.go
@@ -31,7 +31,7 @@
 	// Pull should have done this for us, but check just in case.
 	b.loadedPending = false
 	if b.Submitted(id) && b.HasPendingCommit() {
-		run("git", "reset", "HEAD^")
+		run("git", "reset", b.Branchpoint())
 	}
 }
 
diff --git a/git-review/util_test.go b/git-review/util_test.go
index c2406dc..b095a5f 100644
--- a/git-review/util_test.go
+++ b/git-review/util_test.go
@@ -147,6 +147,9 @@
 }
 
 func testMain(t *testing.T, args ...string) {
+	*noRun = false
+	*verbose = 0
+
 	t.Logf("git-review %s", strings.Join(args, " "))
 
 	canDie := mainCanDie