git-review: add gofmt pre-commit hook
Also change getLines to not strip spaces, making it more generally
useful, and the use it more generally.
Change-Id: I0969a2bee5ec5f331fba82e5902ca9879b988fe8
Reviewed-on: https://go-review.googlesource.com/1610
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/git-review/branch.go b/git-review/branch.go
index 8c31d8a..c4a44c5 100644
--- a/git-review/branch.go
+++ b/git-review/branch.go
@@ -104,8 +104,7 @@
var stagedRE = regexp.MustCompile(`^[ACDMR] `)
func HasStagedChanges() bool {
- // NOTE: Cannot use getLines, because it throws away leading spaces.
- for _, s := range strings.Split(getOutput("git", "status", "-b", "--porcelain"), "\n") {
+ for _, s := range getLines("git", "status", "-b", "--porcelain") {
if stagedRE.MatchString(s) {
return true
}
@@ -116,8 +115,7 @@
var unstagedRE = regexp.MustCompile(`^.[ACDMR]`)
func HasUnstagedChanges() bool {
- // NOTE: Cannot use getLines, because it throws away leading spaces.
- for _, s := range strings.Split(getOutput("git", "status", "-b", "--porcelain"), "\n") {
+ for _, s := range getLines("git", "status", "-b", "--porcelain") {
if unstagedRE.MatchString(s) {
return true
}
@@ -128,7 +126,8 @@
func LocalBranches() []*Branch {
var branches []*Branch
for _, s := range getLines("git", "branch", "-q") {
- branches = append(branches, &Branch{Name: strings.TrimPrefix(s, "* ")})
+ s = strings.TrimPrefix(strings.TrimSpace(s), "* ")
+ branches = append(branches, &Branch{Name: s})
}
return branches
}
@@ -136,6 +135,7 @@
func OriginBranches() []string {
var branches []string
for _, line := range getLines("git", "branch", "-a", "-q") {
+ line = strings.TrimSpace(line)
if i := strings.Index(line, " -> "); i >= 0 {
line = line[:i]
}
diff --git a/git-review/hook.go b/git-review/hook.go
index 7eed33f..f79d78d 100644
--- a/git-review/hook.go
+++ b/git-review/hook.go
@@ -14,11 +14,13 @@
"path/filepath"
"regexp"
"runtime"
+ "strings"
)
var hookPath = ".git/hooks/"
var hookFiles = []string{
"commit-msg",
+ "pre-commit",
}
func installHook() {
@@ -93,6 +95,8 @@
switch args[0] {
case "commit-msg":
hookCommitMsg(args[1:])
+ case "pre-commit":
+ hookPreCommit(args[1:])
}
}
@@ -135,6 +139,55 @@
return regexp.MustCompile(`(?m)^#.*\n`).ReplaceAll(in, nil)
}
+// hookPreCommit is installed as the git pre-commit hook.
+// It prevents commits to the master branch.
+// It checks that the Go files added, copied, or modified by
+// the change are gofmt'd, and if not it prints gofmt instructions
+// and exits with nonzero status.
+func hookPreCommit(args []string) {
+ // Prevent commits to master branches.
+ b := CurrentBranch()
+ if !b.IsLocalOnly() {
+ dief("cannot commit on %s branch", b.Name)
+ }
+
+ root := repoRoot()
+
+ // 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))
+ }
+ }
+ if len(goFiles) == 0 {
+ return
+ }
+
+ // Check gofmt.
+ unfmted := getLines("gofmt", append([]string{"-l"}, goFiles...)...)
+ if len(unfmted) == 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/"))
+}
+
// This is NOT USED ANYMORE.
// It is here only for comparing against old commit-hook files.
var oldCommitMsgHook = `#!/bin/sh
diff --git a/git-review/hook_test.go b/git-review/hook_test.go
index eaa6016..2ccc7c4 100644
--- a/git-review/hook_test.go
+++ b/git-review/hook_test.go
@@ -47,6 +47,38 @@
}
}
+func TestHookPreCommit(t *testing.T) {
+ 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)
+ 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", ".")
+
+ 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")
+}
+
func TestHooks(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
@@ -92,6 +124,7 @@
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")
trun(t, gt.client, "git", "commit", "-m", "mymsg")
diff --git a/git-review/review.go b/git-review/review.go
index 054c510..8f5843c 100644
--- a/git-review/review.go
+++ b/git-review/review.go
@@ -204,7 +204,9 @@
func getLines(command string, args ...string) []string {
var s []string
for _, l := range strings.Split(getOutput(command, args...), "\n") {
- s = append(s, strings.TrimSpace(l))
+ if len(strings.TrimSpace(l)) > 0 {
+ s = append(s, l)
+ }
}
return s
}