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
}