git-codereview: add interactive mode to submit

This adds a -i option to submit that brings up a list of commits to
submit in an editor (a la git rebase -i), lets the user edit the list,
and then submits the specified commits in the specified order.

Change-Id: I88149140527c987ae856aac2598f0a992fe5654d
Reviewed-on: https://go-review.googlesource.com/16677
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/git-codereview/branch.go b/git-codereview/branch.go
index 5eacb10..b451507 100644
--- a/git-codereview/branch.go
+++ b/git-codereview/branch.go
@@ -344,7 +344,11 @@
 		for _, c := range work {
 			fmt.Fprintf(&buf, "\n\t%s %s", c.ShortHash, c.Subject)
 		}
-		dief("cannot %s: multiple changes pending; must specify commit hash on command line:%s", action, buf.String())
+		extra := ""
+		if action == "submit" {
+			extra = " or use submit -i"
+		}
+		dief("cannot %s: multiple changes pending; must specify commit hash on command line%s:%s", action, extra, buf.String())
 	}
 	return work[0]
 }
diff --git a/git-codereview/editor.go b/git-codereview/editor.go
new file mode 100644
index 0000000..c3de65f
--- /dev/null
+++ b/git-codereview/editor.go
@@ -0,0 +1,50 @@
+// Copyright 2015 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 (
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+)
+
+// editor invokes an interactive editor on a temporary file containing
+// initial, blocks until the editor exits, and returns the (possibly
+// edited) contents of the temporary file. It follows the conventions
+// of git for selecting and invoking the editor (see git-var(1)).
+func editor(initial string) string {
+	// Query the git editor command.
+	gitEditor := trim(cmdOutput("git", "var", "GIT_EDITOR"))
+
+	// Create temporary file.
+	temp, err := ioutil.TempFile("", "git-codereview")
+	if err != nil {
+		dief("creating temp file: %v", err)
+	}
+	tempName := temp.Name()
+	defer os.Remove(tempName)
+	if _, err := io.WriteString(temp, initial); err != nil {
+		dief("%v", err)
+	}
+	if err := temp.Close(); err != nil {
+		dief("%v", err)
+	}
+
+	// Invoke the editor. See git's prepare_shell_cmd.
+	cmd := exec.Command("sh", "-c", gitEditor+" \"$@\"", gitEditor, tempName)
+	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+	if err := cmd.Run(); err != nil {
+		os.Remove(tempName)
+		dief("editor exited with: %v", err)
+	}
+
+	// Read the edited file.
+	b, err := ioutil.ReadFile(tempName)
+	if err != nil {
+		dief("%v", err)
+	}
+	return string(b)
+}
diff --git a/git-codereview/review.go b/git-codereview/review.go
index 1898463..f4db94a 100644
--- a/git-codereview/review.go
+++ b/git-codereview/review.go
@@ -95,7 +95,7 @@
 		If -l is specified, only use locally available information.
 		If -s is specified, show short output.
 
-	submit [commit-hash...]
+	submit [-i | commit-hash...]
 		Push the pending change to the Gerrit server and tell Gerrit to
 		submit it to the master branch.
 
diff --git a/git-codereview/submit.go b/git-codereview/submit.go
index f6b48d4..809bbe7 100644
--- a/git-codereview/submit.go
+++ b/git-codereview/submit.go
@@ -5,22 +5,39 @@
 package main
 
 import (
+	"bytes"
 	"fmt"
 	"os"
+	"strings"
 	"time"
 )
 
 // TODO(rsc): Add -tbr, along with standard exceptions (doc/go1.5.txt)
 
 func cmdSubmit(args []string) {
+	var interactive bool
+	flags.BoolVar(&interactive, "i", false, "interactively select commits to submit")
 	flags.Usage = func() {
-		fmt.Fprintf(stderr(), "Usage: %s submit %s [commit-hash...]\n", os.Args[0], globalFlags)
+		fmt.Fprintf(stderr(), "Usage: %s submit %s [-i | commit-hash...]\n", os.Args[0], globalFlags)
 	}
 	flags.Parse(args)
+	if interactive && flags.NArg() > 0 {
+		flags.Usage()
+		os.Exit(2)
+	}
 
 	b := CurrentBranch()
 	var cs []*Commit
-	if args := flags.Args(); len(args) >= 1 {
+	if interactive {
+		hashes := submitHashes(b)
+		if len(hashes) == 0 {
+			printf("nothing to submit")
+			return
+		}
+		for _, hash := range hashes {
+			cs = append(cs, b.CommitByHash("submit", hash))
+		}
+	} else if args := flags.Args(); len(args) >= 1 {
 		for _, arg := range args {
 			cs = append(cs, b.CommitByHash("submit", arg))
 		}
@@ -179,3 +196,57 @@
 
 	return nil
 }
+
+// submitHashes interactively prompts for commits to submit.
+func submitHashes(b *Branch) []string {
+	// Get pending commits on b.
+	pending := b.Pending()
+	for _, c := range pending {
+		// Note that DETAILED_LABELS does not imply LABELS.
+		c.g, c.gerr = b.GerritChange(c, "CURRENT_REVISION", "LABELS", "DETAILED_LABELS")
+		if c.g == nil {
+			c.g = new(GerritChange)
+		}
+	}
+
+	// Construct submit script.
+	var script bytes.Buffer
+	for i := len(pending) - 1; i >= 0; i-- {
+		c := pending[i]
+
+		if c.g.ID == "" {
+			fmt.Fprintf(&script, "# change not on Gerrit:\n#")
+		} else if err := submitCheck(c.g); err != nil {
+			fmt.Fprintf(&script, "# %v:\n#", err)
+		}
+
+		formatCommit(&script, c, true)
+	}
+
+	fmt.Fprintf(&script, `
+# The above commits will be submitted in order from top to bottom
+# when you exit the editor.
+#
+# These lines can be re-ordered, removed, and commented out.
+#
+# If you remove all lines, the submit will be aborted.
+`)
+
+	// Edit the script.
+	final := editor(script.String())
+
+	// Parse the final script.
+	var hashes []string
+	for _, line := range lines(final) {
+		line := strings.TrimSpace(line)
+		if len(line) == 0 || line[0] == '#' {
+			continue
+		}
+		if i := strings.Index(line, " "); i >= 0 {
+			line = line[:i]
+		}
+		hashes = append(hashes, line)
+	}
+
+	return hashes
+}
diff --git a/git-codereview/submit_test.go b/git-codereview/submit_test.go
index ead9157..02c8a12 100644
--- a/git-codereview/submit_test.go
+++ b/git-codereview/submit_test.go
@@ -5,6 +5,7 @@
 package main
 
 import (
+	"os"
 	"strings"
 	"testing"
 )
@@ -171,6 +172,29 @@
 	srv := newGerritServer(t)
 	defer srv.done()
 
+	cl1, cl2 := testSubmitMultiple(t, gt, srv)
+	testMain(t, "submit", cl1.CurrentRevision, cl2.CurrentRevision)
+}
+
+func TestSubmitInteractive(t *testing.T) {
+	gt := newGitTest(t)
+	defer gt.done()
+
+	srv := newGerritServer(t)
+	defer srv.done()
+
+	cl1, cl2 := testSubmitMultiple(t, gt, srv)
+	os.Setenv("GIT_EDITOR", "echo "+cl1.CurrentRevision+" > ")
+	testMain(t, "submit", "-i")
+	if cl1.Status != "MERGED" {
+		t.Fatalf("want cl1.Status == MERGED; got %v", cl1.Status)
+	}
+	if cl2.Status != "NEW" {
+		t.Fatalf("want cl2.Status == NEW; got %v", cl1.Status)
+	}
+}
+
+func testSubmitMultiple(t *testing.T, gt *gitTest, srv *gerritServer) (*GerritChange, *GerritChange) {
 	write(t, gt.client+"/file1", "")
 	trun(t, gt.client, "git", "add", "file1")
 	trun(t, gt.client, "git", "commit", "-m", "msg\n\nChange-Id: I0000001\n")
@@ -218,5 +242,5 @@
 		cl2.Status = "MERGED"
 		return gerritReply{json: cl2}
 	}})
-	testMain(t, "submit", hash1, hash2)
+	return &cl1, &cl2
 }