about summary refs log tree commit diff
path: root/third_party/go/git-appraise/commands
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/go/git-appraise/commands')
-rw-r--r--third_party/go/git-appraise/commands/abandon.go139
-rw-r--r--third_party/go/git-appraise/commands/accept.go109
-rw-r--r--third_party/go/git-appraise/commands/commands.go55
-rw-r--r--third_party/go/git-appraise/commands/comment.go165
-rw-r--r--third_party/go/git-appraise/commands/input/input.go118
-rw-r--r--third_party/go/git-appraise/commands/list.go74
-rw-r--r--third_party/go/git-appraise/commands/output/output.go216
-rw-r--r--third_party/go/git-appraise/commands/pull.go93
-rw-r--r--third_party/go/git-appraise/commands/push.go49
-rw-r--r--third_party/go/git-appraise/commands/rebase.go100
-rw-r--r--third_party/go/git-appraise/commands/reject.go119
-rw-r--r--third_party/go/git-appraise/commands/request.go182
-rw-r--r--third_party/go/git-appraise/commands/request_test.go36
-rw-r--r--third_party/go/git-appraise/commands/show.go85
-rw-r--r--third_party/go/git-appraise/commands/submit.go157
15 files changed, 1697 insertions, 0 deletions
diff --git a/third_party/go/git-appraise/commands/abandon.go b/third_party/go/git-appraise/commands/abandon.go
new file mode 100644
index 0000000000..6f408e1663
--- /dev/null
+++ b/third_party/go/git-appraise/commands/abandon.go
@@ -0,0 +1,139 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+
+	"github.com/google/git-appraise/commands/input"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+	"github.com/google/git-appraise/review/comment"
+	"github.com/google/git-appraise/review/gpg"
+	"github.com/google/git-appraise/review/request"
+)
+
+var abandonFlagSet = flag.NewFlagSet("abandon", flag.ExitOnError)
+
+var (
+	abandonMessageFile = abandonFlagSet.String("F", "", "Take the comment from the given file. Use - to read the message from the standard input")
+	abandonMessage     = abandonFlagSet.String("m", "", "Message to attach to the review")
+
+	abandonSign = abandonFlagSet.Bool("S", false,
+		"Sign the contents of the abandonment")
+)
+
+// abandonReview adds an NMW comment to the current code review.
+func abandonReview(repo repository.Repo, args []string) error {
+	abandonFlagSet.Parse(args)
+	args = abandonFlagSet.Args()
+
+	var r *review.Review
+	var err error
+	if len(args) > 1 {
+		return errors.New("Only abandon a single review is supported.")
+	}
+
+	if len(args) == 1 {
+		r, err = review.Get(repo, args[0])
+	} else {
+		r, err = review.GetCurrent(repo)
+	}
+
+	if err != nil {
+		return fmt.Errorf("Failed to load the review: %v\n", err)
+	}
+	if r == nil {
+		return errors.New("There is no matching review.")
+	}
+
+	if *abandonMessageFile != "" && *abandonMessage == "" {
+		*abandonMessage, err = input.FromFile(*abandonMessageFile)
+		if err != nil {
+			return err
+		}
+	}
+	if *abandonMessageFile == "" && *abandonMessage == "" {
+		*abandonMessage, err = input.LaunchEditor(repo, commentFilename)
+		if err != nil {
+			return err
+		}
+	}
+
+	abandonedCommit, err := r.GetHeadCommit()
+	if err != nil {
+		return err
+	}
+	location := comment.Location{
+		Commit: abandonedCommit,
+	}
+	resolved := false
+	userEmail, err := repo.GetUserEmail()
+	if err != nil {
+		return err
+	}
+	c := comment.New(userEmail, *abandonMessage)
+	c.Location = &location
+	c.Resolved = &resolved
+
+	var key string
+	if *abandonSign {
+		key, err := repo.GetUserSigningKey()
+		if err != nil {
+			return err
+		}
+		err = gpg.Sign(key, &c)
+		if err != nil {
+			return err
+		}
+	}
+
+	err = r.AddComment(c)
+	if err != nil {
+		return err
+	}
+
+	// Empty target ref indicates that request was abandoned
+	r.Request.TargetRef = ""
+	// (re)sign the request after clearing out `TargetRef'.
+	if *abandonSign {
+		err = gpg.Sign(key, &r.Request)
+		if err != nil {
+			return err
+		}
+	}
+
+	note, err := r.Request.Write()
+	if err != nil {
+		return err
+	}
+
+	return repo.AppendNote(request.Ref, r.Revision, note)
+}
+
+// abandonCmd defines the "abandon" subcommand.
+var abandonCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s abandon [<option>...] [<commit>]\n\nOptions:\n", arg0)
+		abandonFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return abandonReview(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/accept.go b/third_party/go/git-appraise/commands/accept.go
new file mode 100644
index 0000000000..b50f424c25
--- /dev/null
+++ b/third_party/go/git-appraise/commands/accept.go
@@ -0,0 +1,109 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"github.com/google/git-appraise/commands/input"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+	"github.com/google/git-appraise/review/comment"
+	"github.com/google/git-appraise/review/gpg"
+)
+
+var acceptFlagSet = flag.NewFlagSet("accept", flag.ExitOnError)
+
+var (
+	acceptMessageFile = acceptFlagSet.String("F", "", "Take the comment from the given file. Use - to read the message from the standard input")
+	acceptMessage     = acceptFlagSet.String("m", "", "Message to attach to the review")
+
+	acceptSign = acceptFlagSet.Bool("S", false,
+		"sign the contents of the acceptance")
+)
+
+// acceptReview adds an LGTM comment to the current code review.
+func acceptReview(repo repository.Repo, args []string) error {
+	acceptFlagSet.Parse(args)
+	args = acceptFlagSet.Args()
+
+	var r *review.Review
+	var err error
+	if len(args) > 1 {
+		return errors.New("Only accepting a single review is supported.")
+	}
+
+	if len(args) == 1 {
+		r, err = review.Get(repo, args[0])
+	} else {
+		r, err = review.GetCurrent(repo)
+	}
+
+	if err != nil {
+		return fmt.Errorf("Failed to load the review: %v\n", err)
+	}
+	if r == nil {
+		return errors.New("There is no matching review.")
+	}
+
+	acceptedCommit, err := r.GetHeadCommit()
+	if err != nil {
+		return err
+	}
+	location := comment.Location{
+		Commit: acceptedCommit,
+	}
+	resolved := true
+	userEmail, err := repo.GetUserEmail()
+	if err != nil {
+		return err
+	}
+
+	if *acceptMessageFile != "" && *acceptMessage == "" {
+		*acceptMessage, err = input.FromFile(*acceptMessageFile)
+		if err != nil {
+			return err
+		}
+	}
+
+	c := comment.New(userEmail, *acceptMessage)
+	c.Location = &location
+	c.Resolved = &resolved
+	if *acceptSign {
+		key, err := repo.GetUserSigningKey()
+		if err != nil {
+			return err
+		}
+		err = gpg.Sign(key, &c)
+		if err != nil {
+			return err
+		}
+	}
+	return r.AddComment(c)
+}
+
+// acceptCmd defines the "accept" subcommand.
+var acceptCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s accept [<option>...] [<commit>]\n\nOptions:\n", arg0)
+		acceptFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return acceptReview(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/commands.go b/third_party/go/git-appraise/commands/commands.go
new file mode 100644
index 0000000000..75b8c72d37
--- /dev/null
+++ b/third_party/go/git-appraise/commands/commands.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package commands contains the assorted sub commands supported by the git-appraise tool.
+package commands
+
+import (
+	"github.com/google/git-appraise/repository"
+)
+
+const notesRefPattern = "refs/notes/devtools/*"
+const archiveRefPattern = "refs/devtools/archives/*"
+const commentFilename = "APPRAISE_COMMENT_EDITMSG"
+
+// Command represents the definition of a single command.
+type Command struct {
+	Usage     func(string)
+	RunMethod func(repository.Repo, []string) error
+}
+
+// Run executes a command, given its arguments.
+//
+// The args parameter is all of the command line args that followed the
+// subcommand.
+func (cmd *Command) Run(repo repository.Repo, args []string) error {
+	return cmd.RunMethod(repo, args)
+}
+
+// CommandMap defines all of the available (sub)commands.
+var CommandMap = map[string]*Command{
+	"abandon": abandonCmd,
+	"accept":  acceptCmd,
+	"comment": commentCmd,
+	"list":    listCmd,
+	"pull":    pullCmd,
+	"push":    pushCmd,
+	"rebase":  rebaseCmd,
+	"reject":  rejectCmd,
+	"request": requestCmd,
+	"show":    showCmd,
+	"submit":  submitCmd,
+}
diff --git a/third_party/go/git-appraise/commands/comment.go b/third_party/go/git-appraise/commands/comment.go
new file mode 100644
index 0000000000..554ac6dc78
--- /dev/null
+++ b/third_party/go/git-appraise/commands/comment.go
@@ -0,0 +1,165 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+
+	"github.com/google/git-appraise/commands/input"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+	"github.com/google/git-appraise/review/comment"
+	"github.com/google/git-appraise/review/gpg"
+)
+
+var commentFlagSet = flag.NewFlagSet("comment", flag.ExitOnError)
+var commentLocation = comment.Range{}
+
+var (
+	commentMessageFile = commentFlagSet.String("F", "", "Take the comment from the given file. Use - to read the message from the standard input")
+	commentMessage     = commentFlagSet.String("m", "", "Message to attach to the review")
+	commentParent      = commentFlagSet.String("p", "", "Parent comment")
+	commentFile        = commentFlagSet.String("f", "", "File being commented upon")
+	commentLgtm        = commentFlagSet.Bool("lgtm", false, "'Looks Good To Me'. Set this to express your approval. This cannot be combined with nmw")
+	commentNmw         = commentFlagSet.Bool("nmw", false, "'Needs More Work'. Set this to express your disapproval. This cannot be combined with lgtm")
+	commentSign        = commentFlagSet.Bool("S", false,
+		"Sign the contents of the comment")
+)
+
+func init() {
+	commentFlagSet.Var(&commentLocation, "l",
+		`File location to be commented upon; requires that the -f flag also be set.
+Location follows the following format:
+    <START LINE>[+<START COLUMN>][:<END LINE>[+<END COLUMN>]]
+So, in order to comment starting on the 5th character of the 2nd line until (and
+including) the 4th character of the 7th line, use:
+    -l 2+5:7+4`)
+}
+
+// commentHashExists checks if the given comment hash exists in the given comment threads.
+func commentHashExists(hashToFind string, threads []review.CommentThread) bool {
+	for _, thread := range threads {
+		if thread.Hash == hashToFind {
+			return true
+		}
+		if commentHashExists(hashToFind, thread.Children) {
+			return true
+		}
+	}
+	return false
+}
+
+// commentOnReview adds a comment to the current code review.
+func commentOnReview(repo repository.Repo, args []string) error {
+	commentFlagSet.Parse(args)
+	args = commentFlagSet.Args()
+
+	var r *review.Review
+	var err error
+	if len(args) > 1 {
+		return errors.New("Only accepting a single review is supported.")
+	}
+
+	if len(args) == 1 {
+		r, err = review.Get(repo, args[0])
+	} else {
+		r, err = review.GetCurrent(repo)
+	}
+
+	if err != nil {
+		return fmt.Errorf("Failed to load the review: %v\n", err)
+	}
+	if r == nil {
+		return errors.New("There is no matching review.")
+	}
+
+	if *commentLgtm && *commentNmw {
+		return errors.New("You cannot combine the flags -lgtm and -nmw.")
+	}
+	if commentLocation != (comment.Range{}) && *commentFile == "" {
+		return errors.New("Specifying a line number with the -l flag requires that you also specify a file name with the -f flag.")
+	}
+	if *commentParent != "" && !commentHashExists(*commentParent, r.Comments) {
+		return errors.New("There is no matching parent comment.")
+	}
+
+	if *commentMessageFile != "" && *commentMessage == "" {
+		*commentMessage, err = input.FromFile(*commentMessageFile)
+		if err != nil {
+			return err
+		}
+	}
+	if *commentMessageFile == "" && *commentMessage == "" {
+		*commentMessage, err = input.LaunchEditor(repo, commentFilename)
+		if err != nil {
+			return err
+		}
+	}
+
+	commentedUponCommit, err := r.GetHeadCommit()
+	if err != nil {
+		return err
+	}
+	location := comment.Location{
+		Commit: commentedUponCommit,
+	}
+	if *commentFile != "" {
+		location.Path = *commentFile
+		location.Range = &commentLocation
+		if err := location.Check(r.Repo); err != nil {
+			return fmt.Errorf("Unable to comment on the given location: %v", err)
+		}
+	}
+
+	userEmail, err := repo.GetUserEmail()
+	if err != nil {
+		return err
+	}
+	c := comment.New(userEmail, *commentMessage)
+	c.Location = &location
+	c.Parent = *commentParent
+	if *commentLgtm || *commentNmw {
+		resolved := *commentLgtm
+		c.Resolved = &resolved
+	}
+
+	if *commentSign {
+		key, err := repo.GetUserSigningKey()
+		if err != nil {
+			return err
+		}
+		err = gpg.Sign(key, &c)
+		if err != nil {
+			return err
+		}
+	}
+
+	return r.AddComment(c)
+}
+
+// commentCmd defines the "comment" subcommand.
+var commentCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s comment [<option>...] [<review-hash>]\n\nOptions:\n", arg0)
+		commentFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return commentOnReview(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/input/input.go b/third_party/go/git-appraise/commands/input/input.go
new file mode 100644
index 0000000000..9a8678a827
--- /dev/null
+++ b/third_party/go/git-appraise/commands/input/input.go
@@ -0,0 +1,118 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package input
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"github.com/google/git-appraise/repository"
+	"io/ioutil"
+	"os"
+	"os/exec"
+)
+
+// LaunchEditor launches the default editor configured for the given repo. This
+// method blocks until the editor command has returned.
+//
+// The specified filename should be a temporary file and provided as a relative path
+// from the repo (e.g. "FILENAME" will be converted to ".git/FILENAME"). This file
+// will be deleted after the editor is closed and its contents have been read.
+//
+// This method returns the text that was read from the temporary file, or
+// an error if any step in the process failed.
+func LaunchEditor(repo repository.Repo, fileName string) (string, error) {
+	editor, err := repo.GetCoreEditor()
+	if err != nil {
+		return "", fmt.Errorf("Unable to detect default git editor: %v\n", err)
+	}
+
+	path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
+
+	cmd, err := startInlineCommand(editor, path)
+	if err != nil {
+		// Running the editor directly did not work. This might mean that
+		// the editor string is not a path to an executable, but rather
+		// a shell command (e.g. "emacsclient --tty"). As such, we'll try
+		// to run the command through bash, and if that fails, try with sh
+		args := []string{"-c", fmt.Sprintf("%s %q", editor, path)}
+		cmd, err = startInlineCommand("bash", args...)
+		if err != nil {
+			cmd, err = startInlineCommand("sh", args...)
+		}
+	}
+	if err != nil {
+		return "", fmt.Errorf("Unable to start editor: %v\n", err)
+	}
+
+	if err := cmd.Wait(); err != nil {
+		return "", fmt.Errorf("Editing finished with error: %v\n", err)
+	}
+
+	output, err := ioutil.ReadFile(path)
+	if err != nil {
+		os.Remove(path)
+		return "", fmt.Errorf("Error reading edited file: %v\n", err)
+	}
+	os.Remove(path)
+	return string(output), err
+}
+
+// FromFile loads and returns the contents of a given file. If - is passed
+// through, much like git, it will read from stdin. This can be piped data,
+// unless there is a tty in which case the user will be prompted to enter a
+// message.
+func FromFile(fileName string) (string, error) {
+	if fileName == "-" {
+		stat, err := os.Stdin.Stat()
+		if err != nil {
+			return "", fmt.Errorf("Error reading from stdin: %v\n", err)
+		}
+		if (stat.Mode() & os.ModeCharDevice) == 0 {
+			// There is no tty. This will allow us to read piped data instead.
+			output, err := ioutil.ReadAll(os.Stdin)
+			if err != nil {
+				return "", fmt.Errorf("Error reading from stdin: %v\n", err)
+			}
+			return string(output), err
+		}
+
+		fmt.Printf("(reading comment from standard input)\n")
+		var output bytes.Buffer
+		s := bufio.NewScanner(os.Stdin)
+		for s.Scan() {
+			output.Write(s.Bytes())
+			output.WriteRune('\n')
+		}
+		return output.String(), nil
+	}
+
+	output, err := ioutil.ReadFile(fileName)
+	if err != nil {
+		return "", fmt.Errorf("Error reading file: %v\n", err)
+	}
+	return string(output), err
+}
+
+func startInlineCommand(command string, args ...string) (*exec.Cmd, error) {
+	cmd := exec.Command(command, args...)
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err := cmd.Start()
+	return cmd, err
+}
diff --git a/third_party/go/git-appraise/commands/list.go b/third_party/go/git-appraise/commands/list.go
new file mode 100644
index 0000000000..cc9338dd7e
--- /dev/null
+++ b/third_party/go/git-appraise/commands/list.go
@@ -0,0 +1,74 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"github.com/google/git-appraise/commands/output"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+)
+
+var listFlagSet = flag.NewFlagSet("list", flag.ExitOnError)
+
+var (
+	listAll        = listFlagSet.Bool("a", false, "List all reviews (not just the open ones).")
+	listJSONOutput = listFlagSet.Bool("json", false, "Format the output as JSON")
+)
+
+// listReviews lists all extant reviews.
+// TODO(ojarjur): Add more flags for filtering the output (e.g. filtering by reviewer or status).
+func listReviews(repo repository.Repo, args []string) error {
+	listFlagSet.Parse(args)
+	var reviews []review.Summary
+	if *listAll {
+		reviews = review.ListAll(repo)
+		if !*listJSONOutput {
+			fmt.Printf("Loaded %d reviews:\n", len(reviews))
+		}
+	} else {
+		reviews = review.ListOpen(repo)
+		if !*listJSONOutput {
+			fmt.Printf("Loaded %d open reviews:\n", len(reviews))
+		}
+	}
+	if *listJSONOutput {
+		b, err := json.MarshalIndent(reviews, "", "  ")
+		if err != nil {
+			return err
+		}
+		fmt.Println(string(b))
+		return nil
+	}
+	for _, r := range reviews {
+		output.PrintSummary(&r)
+	}
+	return nil
+}
+
+// listCmd defines the "list" subcommand.
+var listCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s list [<option>...]\n\nOptions:\n", arg0)
+		listFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return listReviews(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/output/output.go b/third_party/go/git-appraise/commands/output/output.go
new file mode 100644
index 0000000000..4613cd3857
--- /dev/null
+++ b/third_party/go/git-appraise/commands/output/output.go
@@ -0,0 +1,216 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package output contains helper methods for pretty-printing code reviews.
+package output
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/google/git-appraise/review"
+)
+
+const (
+	// Template for printing the summary of a code review.
+	reviewSummaryTemplate = `[%s] %.12s
+  %s
+`
+	// Template for printing the summary of a code review.
+	reviewDetailsTemplate = `  %q -> %q
+  reviewers: %q
+  requester: %q
+  build status: %s
+`
+	// Template for printing the location of an inline comment
+	commentLocationTemplate = `%s%q@%.12s
+`
+	// Template for printing a single comment.
+	commentTemplate = `comment: %s
+author: %s
+time:   %s
+status: %s
+%s`
+	// Template for displaying the summary of the comment threads for a review
+	commentSummaryTemplate = `  comments (%d threads):
+`
+	// Number of lines of context to print for inline comments
+	contextLineCount = 5
+)
+
+// getStatusString returns a human friendly string encapsulating both the review's
+// resolved status, and its submitted status.
+func getStatusString(r *review.Summary) string {
+	if r.Resolved == nil && r.Submitted {
+		return "tbr"
+	}
+	if r.Resolved == nil {
+		return "pending"
+	}
+	if *r.Resolved && r.Submitted {
+		return "submitted"
+	}
+	if *r.Resolved {
+		return "accepted"
+	}
+	if r.Submitted {
+		return "danger"
+	}
+	if r.Request.TargetRef == "" {
+		return "abandon"
+	}
+	return "rejected"
+}
+
+// PrintSummary prints a single-line summary of a review.
+func PrintSummary(r *review.Summary) {
+	statusString := getStatusString(r)
+	indentedDescription := strings.Replace(r.Request.Description, "\n", "\n  ", -1)
+	fmt.Printf(reviewSummaryTemplate, statusString, r.Revision, indentedDescription)
+}
+
+// reformatTimestamp takes a timestamp string of the form "0123456789" and changes it
+// to the form "Mon Jan _2 13:04:05 UTC 2006".
+//
+// Timestamps that are not in the format we expect are left alone.
+func reformatTimestamp(timestamp string) string {
+	parsedTimestamp, err := strconv.ParseInt(timestamp, 10, 64)
+	if err != nil {
+		// The timestamp is an unexpected format, so leave it alone
+		return timestamp
+	}
+	t := time.Unix(parsedTimestamp, 0)
+	return t.Format(time.UnixDate)
+}
+
+// showThread prints the detailed output for an entire comment thread.
+func showThread(r *review.Review, thread review.CommentThread) error {
+	comment := thread.Comment
+	indent := "    "
+	if comment.Location != nil && comment.Location.Path != "" && comment.Location.Range != nil && comment.Location.Range.StartLine > 0 {
+		contents, err := r.Repo.Show(comment.Location.Commit, comment.Location.Path)
+		if err != nil {
+			return err
+		}
+		lines := strings.Split(contents, "\n")
+		err = comment.Location.Check(r.Repo)
+		if err != nil {
+			return err
+		}
+		if comment.Location.Range.StartLine <= uint32(len(lines)) {
+			firstLine := comment.Location.Range.StartLine
+			lastLine := comment.Location.Range.EndLine
+
+			if firstLine == 0 {
+				firstLine = 1
+			}
+
+			if lastLine == 0 {
+				lastLine = firstLine
+			}
+
+			if lastLine == firstLine {
+				minLine := int(lastLine) - int(contextLineCount)
+				if minLine <= 0 {
+					minLine = 1
+				}
+				firstLine = uint32(minLine)
+			}
+
+			fmt.Printf(commentLocationTemplate, indent, comment.Location.Path, comment.Location.Commit)
+			fmt.Println(indent + "|" + strings.Join(lines[firstLine-1:lastLine], "\n"+indent+"|"))
+		}
+	}
+	return showSubThread(r, thread, indent)
+}
+
+// showSubThread prints the given comment (sub)thread, indented by the given prefix string.
+func showSubThread(r *review.Review, thread review.CommentThread, indent string) error {
+	statusString := "fyi"
+	if thread.Resolved != nil {
+		if *thread.Resolved {
+			statusString = "lgtm"
+		} else {
+			statusString = "needs work"
+		}
+	}
+	comment := thread.Comment
+	threadHash := thread.Hash
+	timestamp := reformatTimestamp(comment.Timestamp)
+	commentSummary := fmt.Sprintf(indent+commentTemplate, threadHash, comment.Author, timestamp, statusString, comment.Description)
+	indent = indent + "  "
+	indentedSummary := strings.Replace(commentSummary, "\n", "\n"+indent, -1)
+	fmt.Println(indentedSummary)
+	for _, child := range thread.Children {
+		err := showSubThread(r, child, indent)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// printAnalyses prints the static analysis results for the latest commit in the review.
+func printAnalyses(r *review.Review) {
+	fmt.Println("  analyses: ", r.GetAnalysesMessage())
+}
+
+// printComments prints all of the comments for the review, with snippets of the preceding source code.
+func printComments(r *review.Review) error {
+	fmt.Printf(commentSummaryTemplate, len(r.Comments))
+	for _, thread := range r.Comments {
+		err := showThread(r, thread)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// PrintDetails prints a multi-line overview of a review, including all comments.
+func PrintDetails(r *review.Review) error {
+	PrintSummary(r.Summary)
+	fmt.Printf(reviewDetailsTemplate, r.Request.ReviewRef, r.Request.TargetRef,
+		strings.Join(r.Request.Reviewers, ", "),
+		r.Request.Requester, r.GetBuildStatusMessage())
+	printAnalyses(r)
+	if err := printComments(r); err != nil {
+		return err
+	}
+	return nil
+}
+
+// PrintJSON pretty prints the given review in JSON format.
+func PrintJSON(r *review.Review) error {
+	json, err := r.GetJSON()
+	if err != nil {
+		return err
+	}
+	fmt.Println(json)
+	return nil
+}
+
+// PrintDiff prints the diff of the review.
+func PrintDiff(r *review.Review, diffArgs ...string) error {
+	diff, err := r.GetDiff(diffArgs...)
+	if err != nil {
+		return err
+	}
+	fmt.Println(diff)
+	return nil
+}
diff --git a/third_party/go/git-appraise/commands/pull.go b/third_party/go/git-appraise/commands/pull.go
new file mode 100644
index 0000000000..809c20fdbb
--- /dev/null
+++ b/third_party/go/git-appraise/commands/pull.go
@@ -0,0 +1,93 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+)
+
+var (
+	pullFlagSet = flag.NewFlagSet("pull", flag.ExitOnError)
+	pullVerify  = pullFlagSet.Bool("verify-signatures", false,
+		"verify the signatures of pulled reviews")
+)
+
+// pull updates the local git-notes used for reviews with those from a remote
+// repo.
+func pull(repo repository.Repo, args []string) error {
+	pullFlagSet.Parse(args)
+	pullArgs := pullFlagSet.Args()
+
+	if len(pullArgs) > 1 {
+		return errors.New(
+			"Only pulling from one remote at a time is supported.")
+	}
+
+	remote := "origin"
+	if len(pullArgs) == 1 {
+		remote = pullArgs[0]
+	}
+	// This is the easy case. We're not checking signatures so just go the
+	// normal route.
+	if !*pullVerify {
+		return repo.PullNotesAndArchive(remote, notesRefPattern,
+			archiveRefPattern)
+	}
+
+	// Otherwise, we collect the fetched reviewed revisions (their hashes), get
+	// their reviews, and then one by one, verify them. If we make it through
+	// the set, _then_ we merge the remote reference into the local branch.
+	revisions, err := repo.FetchAndReturnNewReviewHashes(remote,
+		notesRefPattern, archiveRefPattern)
+	if err != nil {
+		return err
+	}
+	for _, revision := range revisions {
+		rvw, err := review.GetSummaryViaRefs(repo,
+			"refs/notes/"+remote+"/devtools/reviews",
+			"refs/notes/"+remote+"/devtools/discuss", revision)
+		if err != nil {
+			return err
+		}
+		err = rvw.Verify()
+		if err != nil {
+			return err
+		}
+		fmt.Println("verified review:", revision)
+	}
+
+	err = repo.MergeNotes(remote, notesRefPattern)
+	if err != nil {
+		return err
+	}
+	return repo.MergeArchives(remote, archiveRefPattern)
+}
+
+var pullCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s pull [<option>] [<remote>]\n\nOptions:\n", arg0)
+		pullFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return pull(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/push.go b/third_party/go/git-appraise/commands/push.go
new file mode 100644
index 0000000000..c75a25eac7
--- /dev/null
+++ b/third_party/go/git-appraise/commands/push.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"fmt"
+	"github.com/google/git-appraise/repository"
+)
+
+// push pushes the local git-notes used for reviews to a remote repo.
+func push(repo repository.Repo, args []string) error {
+	if len(args) > 1 {
+		return errors.New("Only pushing to one remote at a time is supported.")
+	}
+
+	remote := "origin"
+	if len(args) == 1 {
+		remote = args[0]
+	}
+
+	if err := repo.PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern); err != nil {
+		return err
+	}
+	return nil
+}
+
+var pushCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s push [<remote>]\n", arg0)
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return push(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/rebase.go b/third_party/go/git-appraise/commands/rebase.go
new file mode 100644
index 0000000000..2c4595a576
--- /dev/null
+++ b/third_party/go/git-appraise/commands/rebase.go
@@ -0,0 +1,100 @@
+/*
+Copyright 2016 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+)
+
+var rebaseFlagSet = flag.NewFlagSet("rebase", flag.ExitOnError)
+
+var (
+	rebaseArchive = rebaseFlagSet.Bool("archive", true, "Prevent the original commit from being garbage collected.")
+	rebaseSign    = rebaseFlagSet.Bool("S", false,
+		"Sign the contents of the request after the rebase")
+)
+
+// Validate that the user's request to rebase a review makes sense.
+//
+// This checks both that the request is well formed, and that the
+// corresponding review is in a state where rebasing is appropriate.
+func validateRebaseRequest(repo repository.Repo, args []string) (*review.Review, error) {
+	var r *review.Review
+	var err error
+	if len(args) > 1 {
+		return nil, errors.New("Only rebasing a single review is supported.")
+	}
+	if len(args) == 1 {
+		r, err = review.Get(repo, args[0])
+	} else {
+		r, err = review.GetCurrent(repo)
+	}
+	if err != nil {
+		return nil, fmt.Errorf("Failed to load the review: %v\n", err)
+	}
+	if r == nil {
+		return nil, errors.New("There is no matching review.")
+	}
+
+	if r.Submitted {
+		return nil, errors.New("The review has already been submitted.")
+	}
+
+	if r.Request.TargetRef == "" {
+		return nil, errors.New("The review was abandoned.")
+	}
+
+	target := r.Request.TargetRef
+	if err := repo.VerifyGitRef(target); err != nil {
+		return nil, err
+	}
+
+	return r, nil
+}
+
+// Rebase the current code review.
+//
+// The "args" parameter contains all of the command line arguments that followed the subcommand.
+func rebaseReview(repo repository.Repo, args []string) error {
+	rebaseFlagSet.Parse(args)
+	args = rebaseFlagSet.Args()
+
+	r, err := validateRebaseRequest(repo, args)
+	if err != nil {
+		return err
+	}
+	if *rebaseSign {
+		return r.RebaseAndSign(*rebaseArchive)
+	}
+	return r.Rebase(*rebaseArchive)
+}
+
+// rebaseCmd defines the "rebase" subcommand.
+var rebaseCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s rebase [<option>...] [<review-hash>]\n\nOptions:\n", arg0)
+		rebaseFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return rebaseReview(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/reject.go b/third_party/go/git-appraise/commands/reject.go
new file mode 100644
index 0000000000..e0e45babf8
--- /dev/null
+++ b/third_party/go/git-appraise/commands/reject.go
@@ -0,0 +1,119 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+
+	"github.com/google/git-appraise/commands/input"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+	"github.com/google/git-appraise/review/comment"
+	"github.com/google/git-appraise/review/gpg"
+)
+
+var rejectFlagSet = flag.NewFlagSet("reject", flag.ExitOnError)
+
+var (
+	rejectMessageFile = rejectFlagSet.String("F", "", "Take the comment from the given file. Use - to read the message from the standard input")
+	rejectMessage     = rejectFlagSet.String("m", "", "Message to attach to the review")
+
+	rejectSign = rejectFlagSet.Bool("S", false,
+		"Sign the contents of the rejection")
+)
+
+// rejectReview adds an NMW comment to the current code review.
+func rejectReview(repo repository.Repo, args []string) error {
+	rejectFlagSet.Parse(args)
+	args = rejectFlagSet.Args()
+
+	var r *review.Review
+	var err error
+	if len(args) > 1 {
+		return errors.New("Only rejecting a single review is supported.")
+	}
+
+	if len(args) == 1 {
+		r, err = review.Get(repo, args[0])
+	} else {
+		r, err = review.GetCurrent(repo)
+	}
+
+	if err != nil {
+		return fmt.Errorf("Failed to load the review: %v\n", err)
+	}
+	if r == nil {
+		return errors.New("There is no matching review.")
+	}
+
+	if r.Request.TargetRef == "" {
+		return errors.New("The review was abandoned.")
+	}
+
+	if *rejectMessageFile != "" && *rejectMessage == "" {
+		*rejectMessage, err = input.FromFile(*rejectMessageFile)
+		if err != nil {
+			return err
+		}
+	}
+	if *rejectMessageFile == "" && *rejectMessage == "" {
+		*rejectMessage, err = input.LaunchEditor(repo, commentFilename)
+		if err != nil {
+			return err
+		}
+	}
+
+	rejectedCommit, err := r.GetHeadCommit()
+	if err != nil {
+		return err
+	}
+	location := comment.Location{
+		Commit: rejectedCommit,
+	}
+	resolved := false
+	userEmail, err := repo.GetUserEmail()
+	if err != nil {
+		return err
+	}
+	c := comment.New(userEmail, *rejectMessage)
+	c.Location = &location
+	c.Resolved = &resolved
+	if *rejectSign {
+		key, err := repo.GetUserSigningKey()
+		if err != nil {
+			return err
+		}
+		err = gpg.Sign(key, &c)
+		if err != nil {
+			return err
+		}
+	}
+	return r.AddComment(c)
+}
+
+// rejectCmd defines the "reject" subcommand.
+var rejectCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s reject [<option>...] [<commit>]\n\nOptions:\n", arg0)
+		rejectFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return rejectReview(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/request.go b/third_party/go/git-appraise/commands/request.go
new file mode 100644
index 0000000000..9a9854c3f8
--- /dev/null
+++ b/third_party/go/git-appraise/commands/request.go
@@ -0,0 +1,182 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"strings"
+
+	"github.com/google/git-appraise/commands/input"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review/gpg"
+	"github.com/google/git-appraise/review/request"
+)
+
+// Template for the "request" subcommand's output.
+const requestSummaryTemplate = `Review requested:
+Commit: %s
+Target Ref: %s
+Review Ref: %s
+Message: "%s"
+`
+
+var requestFlagSet = flag.NewFlagSet("request", flag.ExitOnError)
+
+var (
+	requestMessageFile      = requestFlagSet.String("F", "", "Take the comment from the given file. Use - to read the message from the standard input")
+	requestMessage          = requestFlagSet.String("m", "", "Message to attach to the review")
+	requestReviewers        = requestFlagSet.String("r", "", "Comma-separated list of reviewers")
+	requestSource           = requestFlagSet.String("source", "HEAD", "Revision to review")
+	requestTarget           = requestFlagSet.String("target", "refs/heads/master", "Revision against which to review")
+	requestQuiet            = requestFlagSet.Bool("quiet", false, "Suppress review summary output")
+	requestAllowUncommitted = requestFlagSet.Bool("allow-uncommitted", false, "Allow uncommitted local changes.")
+	requestSign             = requestFlagSet.Bool("S", false,
+		"GPG sign the content of the request")
+)
+
+// Build the template review request based solely on the parsed flag values.
+func buildRequestFromFlags(requester string) (request.Request, error) {
+	var reviewers []string
+	if len(*requestReviewers) > 0 {
+		for _, reviewer := range strings.Split(*requestReviewers, ",") {
+			reviewers = append(reviewers, strings.TrimSpace(reviewer))
+		}
+	}
+	if *requestMessageFile != "" && *requestMessage == "" {
+		var err error
+		*requestMessage, err = input.FromFile(*requestMessageFile)
+		if err != nil {
+			return request.Request{}, err
+		}
+	}
+
+	return request.New(requester, reviewers, *requestSource, *requestTarget, *requestMessage), nil
+}
+
+// Get the commit at which the review request should be anchored.
+func getReviewCommit(repo repository.Repo, r request.Request, args []string) (string, string, error) {
+	if len(args) > 1 {
+		return "", "", errors.New("Only updating a single review is supported.")
+	}
+	if len(args) == 1 {
+		base, err := repo.MergeBase(r.TargetRef, args[0])
+		if err != nil {
+			return "", "", err
+		}
+		return args[0], base, nil
+	}
+
+	base, err := repo.MergeBase(r.TargetRef, r.ReviewRef)
+	if err != nil {
+		return "", "", err
+	}
+	reviewCommits, err := repo.ListCommitsBetween(base, r.ReviewRef)
+	if err != nil {
+		return "", "", err
+	}
+	if reviewCommits == nil {
+		return "", "", errors.New("There are no commits included in the review request")
+	}
+	return reviewCommits[0], base, nil
+}
+
+// Create a new code review request.
+//
+// The "args" parameter is all of the command line arguments that followed the subcommand.
+func requestReview(repo repository.Repo, args []string) error {
+	requestFlagSet.Parse(args)
+	args = requestFlagSet.Args()
+
+	if !*requestAllowUncommitted {
+		// Requesting a code review with uncommited local changes is usually a mistake, so
+		// we want to report that to the user instead of creating the request.
+		hasUncommitted, err := repo.HasUncommittedChanges()
+		if err != nil {
+			return err
+		}
+		if hasUncommitted {
+			return errors.New("You have uncommitted or untracked files. Use --allow-uncommitted to ignore those.")
+		}
+	}
+
+	userEmail, err := repo.GetUserEmail()
+	if err != nil {
+		return err
+	}
+	r, err := buildRequestFromFlags(userEmail)
+	if err != nil {
+		return err
+	}
+	if r.ReviewRef == "HEAD" {
+		headRef, err := repo.GetHeadRef()
+		if err != nil {
+			return err
+		}
+		r.ReviewRef = headRef
+	}
+	if err := repo.VerifyGitRef(r.TargetRef); err != nil {
+		return err
+	}
+	if err := repo.VerifyGitRef(r.ReviewRef); err != nil {
+		return err
+	}
+
+	reviewCommit, baseCommit, err := getReviewCommit(repo, r, args)
+	if err != nil {
+		return err
+	}
+	r.BaseCommit = baseCommit
+	if r.Description == "" {
+		description, err := repo.GetCommitMessage(reviewCommit)
+		if err != nil {
+			return err
+		}
+		r.Description = description
+	}
+	if *requestSign {
+		key, err := repo.GetUserSigningKey()
+		if err != nil {
+			return err
+		}
+		err = gpg.Sign(key, &r)
+		if err != nil {
+			return err
+		}
+	}
+	note, err := r.Write()
+	if err != nil {
+		return err
+	}
+	repo.AppendNote(request.Ref, reviewCommit, note)
+	if !*requestQuiet {
+		fmt.Printf(requestSummaryTemplate, reviewCommit, r.TargetRef, r.ReviewRef, r.Description)
+	}
+	return nil
+}
+
+// requestCmd defines the "request" subcommand.
+var requestCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s request [<option>...] [<review-hash>]\n\nOptions:\n", arg0)
+		requestFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return requestReview(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/request_test.go b/third_party/go/git-appraise/commands/request_test.go
new file mode 100644
index 0000000000..3e09892e57
--- /dev/null
+++ b/third_party/go/git-appraise/commands/request_test.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"testing"
+)
+
+func TestBuildRequestFromFlags(t *testing.T) {
+	args := []string{"-m", "Request message", "-r", "Me, Myself, \nAnd I "}
+	requestFlagSet.Parse(args)
+	r, err := buildRequestFromFlags("user@hostname.com")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.Description != "Request message" {
+		t.Fatalf("Unexpected request description: '%s'", r.Description)
+	}
+	if r.Reviewers == nil || len(r.Reviewers) != 3 || r.Reviewers[0] != "Me" || r.Reviewers[1] != "Myself" || r.Reviewers[2] != "And I" {
+		t.Fatalf("Unexpected reviewers list: '%v'", r.Reviewers)
+	}
+}
diff --git a/third_party/go/git-appraise/commands/show.go b/third_party/go/git-appraise/commands/show.go
new file mode 100644
index 0000000000..9eb57dd093
--- /dev/null
+++ b/third_party/go/git-appraise/commands/show.go
@@ -0,0 +1,85 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"github.com/google/git-appraise/commands/output"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+	"strings"
+)
+
+var showFlagSet = flag.NewFlagSet("show", flag.ExitOnError)
+
+var (
+	showJSONOutput  = showFlagSet.Bool("json", false, "Format the output as JSON")
+	showDiffOutput  = showFlagSet.Bool("diff", false, "Show the current diff for the review")
+	showDiffOptions = showFlagSet.String("diff-opts", "", "Options to pass to the diff tool; can only be used with the --diff option")
+)
+
+// showReview prints the current code review.
+func showReview(repo repository.Repo, args []string) error {
+	showFlagSet.Parse(args)
+	args = showFlagSet.Args()
+	if *showDiffOptions != "" && !*showDiffOutput {
+		return errors.New("The --diff-opts flag can only be used if the --diff flag is set.")
+	}
+
+	var r *review.Review
+	var err error
+	if len(args) > 1 {
+		return errors.New("Only showing a single review is supported.")
+	}
+
+	if len(args) == 1 {
+		r, err = review.Get(repo, args[0])
+	} else {
+		r, err = review.GetCurrent(repo)
+	}
+
+	if err != nil {
+		return fmt.Errorf("Failed to load the review: %v\n", err)
+	}
+	if r == nil {
+		return errors.New("There is no matching review.")
+	}
+	if *showJSONOutput {
+		return output.PrintJSON(r)
+	}
+	if *showDiffOutput {
+		var diffArgs []string
+		if *showDiffOptions != "" {
+			diffArgs = strings.Split(*showDiffOptions, ",")
+		}
+		return output.PrintDiff(r, diffArgs...)
+	}
+	return output.PrintDetails(r)
+}
+
+// showCmd defines the "show" subcommand.
+var showCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s show [<option>...] [<commit>]\n\nOptions:\n", arg0)
+		showFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return showReview(repo, args)
+	},
+}
diff --git a/third_party/go/git-appraise/commands/submit.go b/third_party/go/git-appraise/commands/submit.go
new file mode 100644
index 0000000000..58fa002350
--- /dev/null
+++ b/third_party/go/git-appraise/commands/submit.go
@@ -0,0 +1,157 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review"
+)
+
+var submitFlagSet = flag.NewFlagSet("submit", flag.ExitOnError)
+
+var (
+	submitMerge       = submitFlagSet.Bool("merge", false, "Create a merge of the source and target refs.")
+	submitRebase      = submitFlagSet.Bool("rebase", false, "Rebase the source ref onto the target ref.")
+	submitFastForward = submitFlagSet.Bool("fast-forward", false, "Create a merge using the default fast-forward mode.")
+	submitTBR         = submitFlagSet.Bool("tbr", false, "(To be reviewed) Force the submission of a review that has not been accepted.")
+	submitArchive     = submitFlagSet.Bool("archive", true, "Prevent the original commit from being garbage collected; only affects rebased submits.")
+
+	submitSign = submitFlagSet.Bool("S", false,
+		"Sign the contents of the submission")
+)
+
+// Submit the current code review request.
+//
+// The "args" parameter contains all of the command line arguments that followed the subcommand.
+func submitReview(repo repository.Repo, args []string) error {
+	submitFlagSet.Parse(args)
+	args = submitFlagSet.Args()
+
+	if *submitMerge && *submitRebase {
+		return errors.New("Only one of --merge or --rebase is allowed.")
+	}
+
+	var r *review.Review
+	var err error
+	if len(args) > 1 {
+		return errors.New("Only accepting a single review is supported.")
+	}
+	if len(args) == 1 {
+		r, err = review.Get(repo, args[0])
+	} else {
+		r, err = review.GetCurrent(repo)
+	}
+
+	if err != nil {
+		return fmt.Errorf("Failed to load the review: %v\n", err)
+	}
+	if r == nil {
+		return errors.New("There is no matching review.")
+	}
+
+	if r.Submitted {
+		return errors.New("The review has already been submitted.")
+	}
+
+	if !*submitTBR && (r.Resolved == nil || !*r.Resolved) {
+		return errors.New("Not submitting as the review has not yet been accepted.")
+	}
+
+	target := r.Request.TargetRef
+	if err := repo.VerifyGitRef(target); err != nil {
+		return err
+	}
+	source, err := r.GetHeadCommit()
+	if err != nil {
+		return err
+	}
+
+	isAncestor, err := repo.IsAncestor(target, source)
+	if err != nil {
+		return err
+	}
+	if !isAncestor {
+		return errors.New("Refusing to submit a non-fast-forward review. First merge the target ref.")
+	}
+
+	if !(*submitRebase || *submitMerge || *submitFastForward) {
+		submitStrategy, err := repo.GetSubmitStrategy()
+		if err != nil {
+			return err
+		}
+		if submitStrategy == "merge" && !*submitRebase && !*submitFastForward {
+			*submitMerge = true
+		}
+		if submitStrategy == "rebase" && !*submitMerge && !*submitFastForward {
+			*submitRebase = true
+		}
+		if submitStrategy == "fast-forward" && !*submitRebase && !*submitMerge {
+			*submitFastForward = true
+		}
+	}
+
+	if *submitRebase {
+		var err error
+		if *submitSign {
+			err = r.RebaseAndSign(*submitArchive)
+		} else {
+			err = r.Rebase(*submitArchive)
+		}
+		if err != nil {
+			return err
+		}
+
+		source, err = r.GetHeadCommit()
+		if err != nil {
+			return err
+		}
+	}
+
+	if err := repo.SwitchToRef(target); err != nil {
+		return err
+	}
+	if *submitMerge {
+		submitMessage := fmt.Sprintf("Submitting review %.12s", r.Revision)
+		if *submitSign {
+			return repo.MergeAndSignRef(source, false, submitMessage,
+				r.Request.Description)
+		} else {
+			return repo.MergeRef(source, false, submitMessage,
+				r.Request.Description)
+		}
+	} else {
+		if *submitSign {
+			return repo.MergeAndSignRef(source, true)
+		} else {
+			return repo.MergeRef(source, true)
+		}
+	}
+}
+
+// submitCmd defines the "submit" subcommand.
+var submitCmd = &Command{
+	Usage: func(arg0 string) {
+		fmt.Printf("Usage: %s submit [<option>...] [<review-hash>]\n\nOptions:\n", arg0)
+		submitFlagSet.PrintDefaults()
+	},
+	RunMethod: func(repo repository.Repo, args []string) error {
+		return submitReview(repo, args)
+	},
+}