git-codereview: add support for DΟ NΟT MAIL

Also create a local DΟ NΟT SUBMIT check,
although the Gerrit server is also taking care of that.

While we're here, make fixup! and squash! commits
non-mailable as well.

(In the all caps text above the big Os are really Omicrons.
Otherwise the CL would not be mailable or submittable.)

Change-Id: Id1a9806b0d395d1a0bfc50ed7d7d22c64a48a2f1
Reviewed-on: https://go-review.googlesource.com/76877
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/git-codereview/doc.go b/git-codereview/doc.go
index d6a5576..a7b04b5 100644
--- a/git-codereview/doc.go
+++ b/git-codereview/doc.go
@@ -212,6 +212,10 @@
 If no revision is specified, the mail command prints a short summary of
 the pending commits for use in deciding which to mail.
 
+If any commit that would be pushed to the server contains the text
+"DO NOT MAIL" (case insensitive) in its commit message, the mail command
+will refuse to send the commit to the server.
+
 Pending
 
 The pending command prints to standard output the status of all pending changes
diff --git a/git-codereview/mail.go b/git-codereview/mail.go
index 040ef19..a63790f 100644
--- a/git-codereview/mail.go
+++ b/git-codereview/mail.go
@@ -51,6 +51,29 @@
 		dief("cannot mail: commit %s is empty", c.ShortHash)
 	}
 
+	foundCommit := false
+	for _, c1 := range b.Pending() {
+		if c1 == c {
+			foundCommit = true
+		}
+		if !foundCommit {
+			continue
+		}
+		if strings.Contains(strings.ToLower(c1.Message), "do not mail") {
+			dief("%s: CL says DO NOT MAIL", c1.ShortHash)
+		}
+		if strings.HasPrefix(c1.Message, "fixup!") {
+			dief("%s: CL is a fixup! commit", c1.ShortHash)
+		}
+		if strings.HasPrefix(c1.Message, "squash!") {
+			dief("%s: CL is a squash! commit", c1.ShortHash)
+		}
+	}
+	if !foundCommit {
+		// b.CommitByRev and b.DefaultCommit both return a commit on b.
+		dief("internal error: did not find chosen commit on current branch")
+	}
+
 	if !*force && HasStagedChanges() {
 		dief("there are staged changes; aborting.\n"+
 			"Use '%s change' to include them or '%s mail -f' to force it.", os.Args[0], os.Args[0])
diff --git a/git-codereview/mail_test.go b/git-codereview/mail_test.go
index c24370f..8680f0e 100644
--- a/git-codereview/mail_test.go
+++ b/git-codereview/mail_test.go
@@ -30,6 +30,33 @@
 		"git tag -f work.mailed "+h)
 }
 
+func TestDoNotMail(t *testing.T) {
+	gt := newGitTest(t)
+	defer gt.done()
+	gt.work(t)
+	trun(t, gt.client, "git", "commit", "--amend", "-m", "This is my commit.\n\nDO NOT MAIL\n")
+
+	testMainDied(t, "mail")
+	testPrintedStderr(t, "DO NOT MAIL")
+
+	trun(t, gt.client, "git", "commit", "--amend", "-m", "fixup! This is my commit.")
+
+	testMainDied(t, "mail")
+	testPrintedStderr(t, "fixup! commit")
+
+	trun(t, gt.client, "git", "commit", "--amend", "-m", "squash! This is my commit.")
+
+	testMainDied(t, "mail")
+	testPrintedStderr(t, "squash! commit")
+
+	trun(t, gt.client, "git", "commit", "--amend", "-m", "This is my commit.\n\nDO NOT MAIL\n")
+
+	// Do not mail even when the DO NOT MAIL is a parent of the thing we asked to mail.
+	gt.work(t)
+	testMainDied(t, "mail", "HEAD")
+	testPrintedStderr(t, "DO NOT MAIL")
+}
+
 func TestMailGitHub(t *testing.T) {
 	gt := newGitTest(t)
 	defer gt.done()
diff --git a/git-codereview/submit.go b/git-codereview/submit.go
index 78c6339..19d38e3 100644
--- a/git-codereview/submit.go
+++ b/git-codereview/submit.go
@@ -78,6 +78,10 @@
 // submit submits a single commit c on branch b and returns the
 // GerritChange for the submitted change. It dies if the submit fails.
 func submit(b *Branch, c *Commit) *GerritChange {
+	if strings.Contains(strings.ToLower(c.Message), "do not submit") {
+		dief("%s: CL says DO NOT SUBMIT", c.ShortHash)
+	}
+
 	// Fetch Gerrit information about this change.
 	g, err := b.GerritChange(c, "LABELS", "CURRENT_REVISION")
 	if err != nil {
diff --git a/git-codereview/submit_test.go b/git-codereview/submit_test.go
index 29111fc..db783f8 100644
--- a/git-codereview/submit_test.go
+++ b/git-codereview/submit_test.go
@@ -23,7 +23,13 @@
 	testPrintedStderr(t, "cannot submit: no changes pending")
 	write(t, gt.client+"/file1", "")
 	trun(t, gt.client, "git", "add", "file1")
-	trun(t, gt.client, "git", "commit", "-m", "msg\n\nChange-Id: I123456789\n")
+	trun(t, gt.client, "git", "commit", "-m", "msg\n\nDO NOT SUBMIT\n\nChange-Id: I123456789\n")
+
+	// Gerrit checks this too, but add a local check.
+	t.Logf("> do not submit")
+	testMainDied(t, "submit")
+	testPrintedStderr(t, "DO NOT SUBMIT")
+	trun(t, gt.client, "git", "commit", "--amend", "-m", "msg\n\nChange-Id: I123456789\n")
 
 	t.Logf("> staged changes")
 	write(t, gt.client+"/file1", "asdf")