git-codereview: reject commits with multiple Change-Id lines

This should help prevent the situation in CL 8653.

Change-Id: I03e4868656a86e69e11f3e0545feb1681fb0f628
Reviewed-on: https://go-review.googlesource.com/8671
Reviewed-by: Minux Ma <minux@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
diff --git a/git-codereview/hook.go b/git-codereview/hook.go
index 3d4ff9c..18994c8 100644
--- a/git-codereview/hook.go
+++ b/git-codereview/hook.go
@@ -153,9 +153,17 @@
 		data = issueRefRE.ReplaceAll(data, []byte("${space}"+issueRepo+"${ref}"))
 	}
 
+	// 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.
 	edited := false
-	if !bytes.Contains(data, []byte("\nChange-Id: ")) {
+	if nChangeId == 0 {
 		edited = true
 		n := len(data)
 		for n > 0 && data[n-1] == '\n' {
diff --git a/git-codereview/hook_test.go b/git-codereview/hook_test.go
index 34772d0..b3686a4 100644
--- a/git-codereview/hook_test.go
+++ b/git-codereview/hook_test.go
@@ -23,30 +23,32 @@
 	// Check that hook adds Change-Id.
 	write(t, gt.client+"/msg.txt", "Test message.\n")
 	testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
-	data, err := ioutil.ReadFile(gt.client + "/msg.txt")
-	if err != nil {
-		t.Fatal(err)
-	}
+	data := read(t, gt.client+"/msg.txt")
 	if !bytes.Contains(data, []byte("\n\nChange-Id: ")) {
 		t.Fatalf("after hook-invoke commit-msg, missing Change-Id:\n%s", data)
 	}
 
 	// Check that hook is no-op when Change-Id is already present.
 	testMain(t, "hook-invoke", "commit-msg", gt.client+"/msg.txt")
-	data1, err := ioutil.ReadFile(gt.client + "/msg.txt")
-	if err != nil {
-		t.Fatal(err)
-	}
+	data1 := read(t, gt.client+"/msg.txt")
 	if !bytes.Equal(data, data1) {
 		t.Fatalf("second hook-invoke commit-msg changed Change-Id:\nbefore:\n%s\n\nafter:\n%s", data, data1)
 	}
 
+	// Check that hook rejects multiple Change-Ids.
+	write(t, gt.client+"/msgdouble.txt", string(data)+string(data))
+	testMainDied(t, "hook-invoke", "commit-msg", gt.client+"/msgdouble.txt")
+	const multiple = "git-codereview: multiple Change-Id lines\n"
+	if got := testStderr.String(); got != multiple {
+		t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, multiple)
+	}
+
 	// Check that hook fails when message is empty.
 	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 := testStderr.String(); got != want {
-		t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, want)
+	const empty = "git-codereview: empty commit message\n"
+	if got := testStderr.String(); got != empty {
+		t.Fatalf("unexpected output:\ngot: %q\nwant: %q", got, empty)
 	}
 
 	// Check that hook inserts a blank line after the first line as needed.
diff --git a/git-codereview/util_test.go b/git-codereview/util_test.go
index 939590f..034d052 100644
--- a/git-codereview/util_test.go
+++ b/git-codereview/util_test.go
@@ -184,6 +184,14 @@
 	}
 }
 
+func read(t *testing.T, file string) []byte {
+	b, err := ioutil.ReadFile(file)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return b
+}
+
 func remove(t *testing.T, file string) {
 	if err := os.RemoveAll(file); err != nil {
 		t.Fatal(err)