git-codereview: disable Gerrit-specific hook behavior if Gerrit is not in use
Every time I accidentally type 'git pending' in a non-Gerrit repository,
that command installs hooks, and then I have to zero the hook files
to keep working.
First, don't install the hooks if this looks like a non-Gerrit git repo.
That seems like it will play well with others.
But, people might also want to use the non-Gerrit parts of the tool,
so if people do say 'git codereview hooks', let that install them.
And then if the hooks are invoked in a non-Gerrit repo, selectively
disable the Gerrit-specific parts. For example, if it's not a Gerrit repo
we might still want to check and fix 'Fixes #N' lines if directed by the
config, but we don't want to add Change-Id lines.
Now that things are a bit better behaved, also leave the hooks on
in detached head mode. I've found that it's quite frustrating not
to have the hooks on when working midway through git rebase -i.
If we find reasons that the hooks were off we can try to figure out
how to split the difference.
Fixes golang/go#12170.
Change-Id: I69fa156ce4fd11c0c84416088cd972957ce1ce6d
Reviewed-on: https://go-review.googlesource.com/19560
Reviewed-by: Josh Bleecher Snyder <josharian@gmail.com>
Reviewed-by: Austin Clements <austin@google.com>
diff --git a/git-codereview/config.go b/git-codereview/config.go
index 1adf71f..739c4f5 100644
--- a/git-codereview/config.go
+++ b/git-codereview/config.go
@@ -36,6 +36,28 @@
return cachedConfig
}
+func haveGerrit() bool {
+ gerrit := config()["gerrit"]
+ if gerrit == "off" {
+ return false
+ }
+ if gerrit != "" {
+ return true
+ }
+ origin := trim(cmdOutput("git", "config", "remote.origin.url"))
+ if strings.Contains(origin, "github.com") {
+ return false
+ }
+ if !strings.Contains(origin, "https://") {
+ return false
+ }
+ if strings.Count(origin, "/") != 3 {
+ return false
+ }
+ host := origin[:strings.LastIndex(origin, "/")]
+ return strings.HasSuffix(host, ".googlesource.com")
+}
+
func parseConfig(raw string) (map[string]string, error) {
cfg := make(map[string]string)
for _, line := range nonBlankLines(raw) {
diff --git a/git-codereview/hook.go b/git-codereview/hook.go
index 570001e..264c85b 100644
--- a/git-codereview/hook.go
+++ b/git-codereview/hook.go
@@ -124,12 +124,18 @@
dief("usage: git-codereview hook-invoke commit-msg message.txt\n")
}
- b := CurrentBranch()
- if b.DetachedHead() {
- // Likely executing rebase or some other internal operation.
- // Probably a mistake to make commit message changes.
- return
- }
+ // We used to bail in detached head mode, but it's very common
+ // to be modifying things during git rebase -i and it's annoying
+ // that those new commits made don't get Commit-Msg lines.
+ // Let's try keeping the hook on and see what breaks.
+ /*
+ b := CurrentBranch()
+ if b.DetachedHead() {
+ // Likely executing rebase or some other internal operation.
+ // Probably a mistake to make commit message changes.
+ return
+ }
+ */
file := args[0]
oldData, err := ioutil.ReadFile(file)
@@ -161,34 +167,37 @@
oldFixesRE := regexp.MustCompile(fmt.Sprintf(oldFixesRETemplate, regexp.QuoteMeta(issueRepo)))
data = oldFixesRE.ReplaceAll(data, []byte("Fixes "+issueRepo+"#${issueNum}"))
- // Complain if two Change-Ids are present.
- // This can happen during an interactive rebase;
- // it is easy to forget to remove one of them.
- nChangeId := bytes.Count(data, []byte("\nChange-Id: "))
- if nChangeId > 1 {
- dief("multiple Change-Id lines")
- }
-
- // Add Change-Id to commit message if not present.
- if nChangeId == 0 {
- n := len(data)
- for n > 0 && data[n-1] == '\n' {
- n--
+ if haveGerrit() {
+ // Complain if two Change-Ids are present.
+ // This can happen during an interactive rebase;
+ // it is easy to forget to remove one of them.
+ nChangeId := bytes.Count(data, []byte("\nChange-Id: "))
+ if nChangeId > 1 {
+ dief("multiple Change-Id lines")
}
- var id [20]byte
- if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {
- dief("generating Change-Id: %v", err)
- }
- data = append(data[:n], fmt.Sprintf("\n\nChange-Id: I%x\n", id[:])...)
- }
- // Add branch prefix to commit message if not present and not on master
- // and not a special Git fixup! or squash! commit message.
- branch := strings.TrimPrefix(b.OriginBranch(), "origin/")
- if branch != "master" {
- prefix := "[" + branch + "] "
- if !bytes.HasPrefix(data, []byte(prefix)) && !isFixup(data) {
- data = []byte(prefix + string(data))
+ // Add Change-Id to commit message if not present.
+ if nChangeId == 0 {
+ n := len(data)
+ for n > 0 && data[n-1] == '\n' {
+ n--
+ }
+ var id [20]byte
+ if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {
+ dief("generating Change-Id: %v", err)
+ }
+ data = append(data[:n], fmt.Sprintf("\n\nChange-Id: I%x\n", id[:])...)
+ }
+
+ // Add branch prefix to commit message if not present and not on master
+ // and not a special Git fixup! or squash! commit message.
+ b := CurrentBranch()
+ branch := strings.TrimPrefix(b.OriginBranch(), "origin/")
+ if strings.HasPrefix(branch, "dev.") {
+ prefix := "[" + branch + "] "
+ if !bytes.HasPrefix(data, []byte(prefix)) && !isFixup(data) {
+ data = []byte(prefix + string(data))
+ }
}
}
@@ -222,15 +231,25 @@
// 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.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)
+ // We used to bail in detached head mode, but it's very common
+ // to be modifying things during git rebase -i and it's annoying
+ // that those new commits made don't get the gofmt check.
+ // Let's try keeping the hook on and see what breaks.
+ /*
+ b := CurrentBranch()
+ if b.DetachedHead() {
+ // This is an internal commit such as during git rebase.
+ // Don't die, and don't force gofmt.
+ return
+ }
+ */
+
+ // Prevent commits to master branches, but only if we're here for code review.
+ if haveGerrit() {
+ b := CurrentBranch()
+ if !b.IsLocalOnly() && b.Name != "HEAD" {
+ dief("cannot commit on %s branch", b.Name)
+ }
}
hookGofmt()
@@ -238,7 +257,7 @@
func hookGofmt() {
if os.Getenv("GIT_GOFMT_HOOK") == "off" {
- fmt.Fprintf(stderr(), "git-gofmt-hook disabled by $GIT_GOFMT_HOOK=off\n")
+ fmt.Fprintf(stderr(), "git-codereview pre-commit gofmt hook disabled by $GIT_GOFMT_HOOK=off\n")
return
}
diff --git a/git-codereview/hook_test.go b/git-codereview/hook_test.go
index fc17e1e..247666c 100644
--- a/git-codereview/hook_test.go
+++ b/git-codereview/hook_test.go
@@ -14,10 +14,9 @@
"testing"
)
-const lenChangeId = len("\n\nChange-Id: I") + 2*20
-
-func TestHookCommitMsg(t *testing.T) {
+func TestHookCommitMsgGerrit(t *testing.T) {
gt := newGitTest(t)
+ gt.enableGerrit(t)
defer gt.done()
// Check that hook adds Change-Id.
@@ -42,6 +41,11 @@
if got := testStderr.String(); got != multiple {
t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, multiple)
}
+}
+
+func TestHookCommitMsg(t *testing.T) {
+ gt := newGitTest(t)
+ defer gt.done()
// Check that hook fails when message is empty.
write(t, gt.client+"/empty.txt", "\n\n# just a file with\n# comments\n")
@@ -76,9 +80,6 @@
t.Fatal(err)
}
- // pull off the Change-Id that got appended
- got = got[:len(got)-lenChangeId]
- want = want[:len(want)-lenChangeId]
if !bytes.Equal(got, want) {
t.Fatalf("failed to rewrite:\n%s\n\ngot:\n\n%s\n\nwant:\n\n%s\n", tt.in, got, want)
}
@@ -96,13 +97,11 @@
"math/big: catch all the rats\n\nFixes issue #99999, at least for now\n",
"math/big: catch all the rats\n\nFixes issue 99999, at least for now\n",
// Don't forget to write back if Change-Id already exists
- "math/big: catch all the rats\n\nFixes issue #99999, at least for now\n\nChange-Id: Ie77358867e38cf976a0688b6e2f80525dae3891e\n",
}
for _, msg := range msgs {
write(t, gt.client+"/msg.txt", msg)
testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
got := read(t, gt.client+"/msg.txt")
- got = got[:len(got)-lenChangeId]
const want = "math/big: catch all the rats\n\nFixes #99999, at least for now\n"
if string(got) != want {
t.Errorf("issue rewrite failed: got\n\n%s\nwant\n\n%s\nlen %d and %d", got, want, len(got), len(want))
@@ -125,13 +124,11 @@
"math/big: catch all the rats\n\nFixes issue #99999, at least for now\n",
"math/big: catch all the rats\n\nFixes issue 99999, at least for now\n",
"math/big: catch all the rats\n\nFixes issue golang/go#99999, at least for now\n",
- "math/big: catch all the rats\n\nFixes issue #99999, at least for now\n\nChange-Id: Ie77358867e38cf976a0688b6e2f80525dae3891e\n",
}
for _, msg := range msgs {
write(t, gt.client+"/msg.txt", msg)
testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
got := read(t, gt.client+"/msg.txt")
- got = got[:len(got)-lenChangeId]
const want = "math/big: catch all the rats\n\nFixes golang/go#99999, at least for now\n"
if string(got) != want {
t.Errorf("issue rewrite failed: got\n\n%s\nwant\n\n%s", got, want)
@@ -144,7 +141,17 @@
}
func TestHookCommitMsgBranchPrefix(t *testing.T) {
+ testHookCommitMsgBranchPrefix(t, false)
+ testHookCommitMsgBranchPrefix(t, true)
+}
+
+func testHookCommitMsgBranchPrefix(t *testing.T, gerrit bool) {
+ t.Logf("gerrit=%v", gerrit)
+
gt := newGitTest(t)
+ if gerrit {
+ gt.enableGerrit(t)
+ }
defer gt.done()
checkPrefix := func(prefix string) {
@@ -179,11 +186,19 @@
trun(t, gt.server, "git", "checkout", "-b", "dev.cc")
trun(t, gt.client, "git", "fetch", "-q")
testMain(t, "change", "dev.cc")
- checkPrefix("[dev.cc] Test message.\n")
+ if gerrit {
+ checkPrefix("[dev.cc] Test message.\n")
+ } else {
+ checkPrefix("Test message.\n")
+ }
// Work branch with server branch as upstream.
testMain(t, "change", "ccwork")
- checkPrefix("[dev.cc] Test message.\n")
+ if gerrit {
+ checkPrefix("[dev.cc] Test message.\n")
+ } else {
+ checkPrefix("Test message.\n")
+ }
// Master has no prefix.
testMain(t, "change", "master")
@@ -271,9 +286,30 @@
trun(t, gt.client, "git", "add", ".")
trun(t, gt.client, "git", "checkout", "HEAD^0")
- testMain(t, "hook-invoke", "pre-commit")
- testNoStdout(t)
- testNoStderr(t)
+ testMainDied(t, "hook-invoke", "pre-commit")
+ testPrintedStderr(t, "gofmt needs to format these files (run 'git gofmt'):", "bad.go")
+
+ /*
+ OLD TEST, back when we disabled gofmt in detached head,
+ in case we go back to that:
+
+ // 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 TestHookPreCommitEnv(t *testing.T) {
@@ -290,7 +326,7 @@
testMain(t, "hook-invoke", "pre-commit")
testNoStdout(t)
- testPrintedStderr(t, "git-gofmt-hook disabled by $GIT_GOFMT_HOOK=off")
+ testPrintedStderr(t, "git-codereview pre-commit gofmt hook disabled by $GIT_GOFMT_HOOK=off")
}
func TestHookPreCommitUnstaged(t *testing.T) {
@@ -407,6 +443,7 @@
func TestHookCommitMsgFromGit(t *testing.T) {
gt := newGitTest(t)
+ gt.enableGerrit(t)
defer gt.done()
restore := testInstallHook(t, gt)
diff --git a/git-codereview/pending.go b/git-codereview/pending.go
index 7d77618..a3b4db8 100644
--- a/git-codereview/pending.go
+++ b/git-codereview/pending.go
@@ -387,7 +387,7 @@
func (b *Branch) errors() string {
b.loadPending()
var buf bytes.Buffer
- if !b.IsLocalOnly() && b.commitsAhead > 0 {
+ if haveGerrit() && !b.IsLocalOnly() && b.commitsAhead > 0 {
fmt.Fprintf(&buf, "Branch contains %d commit%s not on origin/%s.\n", b.commitsAhead, suffix(b.commitsAhead, "s"), b.Name)
fmt.Fprintf(&buf, "\tDo not commit directly to %s branch.\n", b.Name)
}
diff --git a/git-codereview/pending_test.go b/git-codereview/pending_test.go
index 7f2d354..f4168ac 100644
--- a/git-codereview/pending_test.go
+++ b/git-codereview/pending_test.go
@@ -152,6 +152,7 @@
func TestPendingErrors(t *testing.T) {
gt := newGitTest(t)
+ gt.enableGerrit(t)
defer gt.done()
trun(t, gt.client, "git", "checkout", "master")
diff --git a/git-codereview/review.go b/git-codereview/review.go
index 15766ef..779b77c 100644
--- a/git-codereview/review.go
+++ b/git-codereview/review.go
@@ -125,7 +125,10 @@
return
}
- installHook()
+ // Install hooks automatically, but only if this is a Gerrit repo.
+ if haveGerrit() {
+ installHook()
+ }
switch command {
case "branchpoint":
@@ -137,7 +140,7 @@
case "hook-invoke":
cmdHookInvoke(args)
case "hooks":
- // done - installHook already ran
+ installHook() // in case above was bypassed
case "mail", "m":
cmdMail(args)
case "pending":
diff --git a/git-codereview/util_test.go b/git-codereview/util_test.go
index b010f08..a0ff469 100644
--- a/git-codereview/util_test.go
+++ b/git-codereview/util_test.go
@@ -61,6 +61,7 @@
os.Chdir(gt.pwd) // change out of gt.tmpdir first, otherwise following os.RemoveAll fails on windows
resetReadOnlyFlagAll(gt.tmpdir)
os.RemoveAll(gt.tmpdir)
+ cachedConfig = nil
}
// doWork simulates commit 'n' touching 'file' in 'dir'
@@ -176,6 +177,13 @@
}
}
+func (gt *gitTest) enableGerrit(t *testing.T) {
+ write(t, gt.server+"/codereview.cfg", "gerrit: myserver\n")
+ trun(t, gt.server, "git", "add", "codereview.cfg")
+ trun(t, gt.server, "git", "commit", "-m", "add gerrit")
+ trun(t, gt.client, "git", "pull", "-r")
+}
+
func (gt *gitTest) removeStubHooks() {
for _, h := range hookFiles {
os.RemoveAll(gt.client + "/.git/hooks/" + h)
@@ -261,6 +269,7 @@
func testMain(t *testing.T, args ...string) {
*noRun = false
*verbose = 0
+ cachedConfig = nil
t.Logf("git-codereview %s", strings.Join(args, " "))