about summary refs log tree commit diff
path: root/third_party/go/git-appraise
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@google.com>2019-07-02T13·19+0100
committerVincent Ambo <tazjin@google.com>2019-07-02T13·19+0100
commitfe642c30f01c4f3f6637851595ad1b36032461aa (patch)
treec0d0724f185add97673fb119122964dc95778f09 /third_party/go/git-appraise
parente03f0630523d708e144cf340bb00dfd957e167b6 (diff)
feat(third_party): Check in git-appraise r/10
Diffstat (limited to 'third_party/go/git-appraise')
-rw-r--r--third_party/go/git-appraise/.gitignore2
-rw-r--r--third_party/go/git-appraise/CONTRIBUTING.md24
-rw-r--r--third_party/go/git-appraise/LICENSE202
-rw-r--r--third_party/go/git-appraise/README.md158
-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
-rw-r--r--third_party/go/git-appraise/docs/tutorial.md404
-rw-r--r--third_party/go/git-appraise/git-appraise/git-appraise.go104
-rw-r--r--third_party/go/git-appraise/repository/git.go987
-rw-r--r--third_party/go/git-appraise/repository/git_test.go94
-rw-r--r--third_party/go/git-appraise/repository/mock_repo.go613
-rw-r--r--third_party/go/git-appraise/repository/repo.go221
-rw-r--r--third_party/go/git-appraise/review/analyses/analyses.go160
-rw-r--r--third_party/go/git-appraise/review/analyses/analyses_test.go77
-rw-r--r--third_party/go/git-appraise/review/ci/ci.go95
-rw-r--r--third_party/go/git-appraise/review/ci/ci_test.go85
-rw-r--r--third_party/go/git-appraise/review/comment/comment.go266
-rw-r--r--third_party/go/git-appraise/review/gpg/signable.go129
-rw-r--r--third_party/go/git-appraise/review/request/request.go104
-rw-r--r--third_party/go/git-appraise/review/review.go772
-rw-r--r--third_party/go/git-appraise/review/review_test.go870
-rw-r--r--third_party/go/git-appraise/schema/analysis.json61
-rw-r--r--third_party/go/git-appraise/schema/ci.json42
-rw-r--r--third_party/go/git-appraise/schema/comment.json75
-rw-r--r--third_party/go/git-appraise/schema/request.json58
38 files changed, 7300 insertions, 0 deletions
diff --git a/third_party/go/git-appraise/.gitignore b/third_party/go/git-appraise/.gitignore
new file mode 100644
index 000000000000..385b6eee948f
--- /dev/null
+++ b/third_party/go/git-appraise/.gitignore
@@ -0,0 +1,2 @@
+*~
+bin/
diff --git a/third_party/go/git-appraise/CONTRIBUTING.md b/third_party/go/git-appraise/CONTRIBUTING.md
new file mode 100644
index 000000000000..8532a3336e18
--- /dev/null
+++ b/third_party/go/git-appraise/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+Want to contribute? Great! First, read this page (including the small print at the end).
+
+### Before you contribute
+Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase.  Therefore, we need your permission to use and distribute your code.
+We also need to be sure of various other things—for instance that you'll tell
+us if you know that your code infringes on other people's patents. You don't
+have to sign the CLA until after you've submitted your code for review and a
+member has approved it, but you must do it before we can put your code into our
+codebase. Before you start working on a larger contribution, you should get in
+touch with us first through the issue tracker with your idea so that we can
+help out and possibly guide you. Coordinating up front avoids frustrations later.
+
+### Code reviews
+All submissions, including submissions by project members, require review. You
+may use a Github pull request to start such a review, but the review itself
+will be conducted using this tool.
+
+### The small print
+Contributions made by corporations are covered by a different agreement than
+the one above, the Software Grant and Corporate Contributor License Agreement.
\ No newline at end of file
diff --git a/third_party/go/git-appraise/LICENSE b/third_party/go/git-appraise/LICENSE
new file mode 100644
index 000000000000..d64569567334
--- /dev/null
+++ b/third_party/go/git-appraise/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/third_party/go/git-appraise/README.md b/third_party/go/git-appraise/README.md
new file mode 100644
index 000000000000..0227c41366e0
--- /dev/null
+++ b/third_party/go/git-appraise/README.md
@@ -0,0 +1,158 @@
+# Distributed Code Review For Git
+[![Build Status](https://travis-ci.org/google/git-appraise.svg?branch=master)](https://travis-ci.org/google/git-appraise)
+
+This repo contains a command line tool for performing code reviews on git
+repositories.
+
+## Overview
+
+This tool is a *distributed* code review system for git repos.
+
+By "distributed", we mean that code reviews are stored inside of the repository
+as git objects. Every developer on your team has their own copy of the review
+history that they can push or pull. When pulling, updates from the remote
+repo are automatically merged by the tool.
+
+This design removes the need for any sort of server-side setup. As a result,
+this tool can work with any git hosting provider, and the only setup required
+is installing the client on your workstation.
+
+## Installation
+
+Assuming you have the [Go tools installed](https://golang.org/doc/install), run
+the following command:
+
+    go get github.com/google/git-appraise/git-appraise
+
+Then, either make sure that `${GOPATH}/bin` is in your PATH, or explicitly add the
+"appraise" git alias by running the following command.
+
+    git config --global alias.appraise '!'"${GOPATH}/bin/git-appraise"
+
+#### Windows:
+
+    git config --global alias.appraise "!%GOPATH%/bin/git-appraise.exe"
+
+## Requirements
+
+This tool expects to run in an environment with the following attributes:
+
+1.  The git command line tool is installed, and included in the PATH.
+2.  The tool is run from within a git repo.
+3.  The git command line tool is configured with the credentials it needs to
+    push to and pull from the remote repos.
+
+## Usage
+
+Requesting a code review:
+
+    git appraise request
+
+Pushing code reviews to a remote:
+
+    git appraise push [<remote>]
+
+Pulling code reviews from a remote:
+
+    git appraise pull [<remote>]
+
+Listing open code reviews:
+
+    git appraise list
+
+Showing the status of the current review, including comments:
+
+    git appraise show
+
+Showing the diff of a review:
+
+    git appraise show --diff [--diff-opts "<diff-options>"] [<review-hash>]
+
+Commenting on a review:
+
+    git appraise comment -m "<message>" [-f <file> [-l <line>]] [<review-hash>]
+
+Accepting the changes in a review:
+
+    git appraise accept [-m "<message>"] [<review-hash>]
+
+Submitting the current review:
+
+    git appraise submit [--merge | --rebase]
+
+A more detailed getting started doc is available [here](docs/tutorial.md).
+
+## Metadata
+
+The code review data is stored in [git-notes](https://git-scm.com/docs/git-notes),
+using the formats described below. Each item stored is written as a single
+line of JSON, and is written with at most one such item per line. This allows
+the git notes to be automatically merged using the "cat\_sort\_uniq" strategy.
+
+Since these notes are not in a human-friendly form, all of the refs used to
+track them start with the prefix "refs/notes/devtools". This helps make it
+clear that these are meant to be read and written by automated tools.
+
+When a field named "v" appears in one of these notes, it is used to denote
+the version of the metadata format being used. If that field is missing, then
+it defaults to the value 0, which corresponds to this initial version of the
+formats.
+
+### Code Review Requests
+
+Code review requests are stored in the "refs/notes/devtools/reviews" ref, and
+annotate the first revision in a review. They must conform to the
+[request schema](schema/request.json).
+
+If there are multiple requests for a single commit, then they are sorted by
+timestamp and the final request is treated as the current one. This sorting
+should be done in a stable manner, so that if there are multiple requests
+with the same timestamp, then the last such request in the note is treated
+as the current one.
+
+This design allows a user to update a review request by re-running the
+`git appraise request` command.
+
+### Continuous Integration Status
+
+Continuous integration build and test results are stored in the
+"refs/notes/devtools/ci" ref, and annotate the revision that was built and
+tested. They must conform to the [ci schema](schema/ci.json).
+
+### Robot Comments
+
+Robot comments are comments generated by static analysis tools. These are
+stored in the "refs/notes/devtools/analyses" ref, and annotate the revision.
+They must conform to the [analysis schema](schema/analysis.json).
+
+### Review Comments
+
+Review comments are comments that were written by a person rather than by a
+machine. These are stored in the "refs/notes/devtools/discuss" ref, and
+annotate the first revision in the review. They must conform to the
+[comment schema](schema/comment.json).
+
+## Integrations
+
+### Libraries
+
+  - [Go (use git-appraise itself)](https://github.com/google/git-appraise/blob/master/review/review.go)
+  - [Rust](https://github.com/Nemo157/git-appraise-rs)
+
+### Graphical User Interfaces
+
+  - [Git-Appraise-Web](https://github.com/google/git-appraise-web)
+
+### Plugins
+
+  - [Eclipse](https://github.com/google/git-appraise-eclipse)
+  - [Jenkins](https://github.com/jenkinsci/google-git-notes-publisher-plugin)
+
+### Mirrors to other systems
+
+  - [GitHub Pull Requests](https://github.com/google/git-pull-request-mirror)
+  - [Phabricator Revisions](https://github.com/google/git-phabricator-mirror)
+
+## Contributing
+
+Please see [the CONTRIBUTING file](CONTRIBUTING.md) for information on contributing to Git Appraise.
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 000000000000..6f408e1663c9
--- /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 000000000000..b50f424c252b
--- /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 000000000000..75b8c72d3769
--- /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 000000000000..554ac6dc78b8
--- /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 000000000000..9a8678a8272e
--- /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 000000000000..cc9338dd7e97
--- /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 000000000000..4613cd38576b
--- /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 000000000000..809c20fdbbfe
--- /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 000000000000..c75a25eac738
--- /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 000000000000..2c4595a57693
--- /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 000000000000..e0e45babf8bc
--- /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 000000000000..9a9854c3f8a6
--- /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 000000000000..3e09892e5760
--- /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 000000000000..9eb57dd093c7
--- /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 000000000000..58fa00235087
--- /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)
+	},
+}
diff --git a/third_party/go/git-appraise/docs/tutorial.md b/third_party/go/git-appraise/docs/tutorial.md
new file mode 100644
index 000000000000..6f95bb7e2ff5
--- /dev/null
+++ b/third_party/go/git-appraise/docs/tutorial.md
@@ -0,0 +1,404 @@
+# Getting started with git-appraise
+
+This file gives an example code-review workflow using git-appraise. It starts
+with cloning a repository and goes all the way through to browsing
+your submitted commits.
+
+The git-appraise tool is largely agnostic of what workflow you use, so feel
+free to change things to your liking, but this particular workflow should help
+you get started.
+
+## Cloning your repository
+
+Since you're using a code review tool, we'll assume that you have a URL that
+you can push to and pull from in order to collaborate with the rest of your team.
+
+First we'll create our local clone of the repository:
+```shell
+git clone ${URL} example-repo
+cd example-repo
+```
+
+If you are starting from an empty repository, then it's a good practice to add a
+README file explaining the purpose of the repository:
+
+```shell
+echo '# Example Repository' > README.md
+git add README.md
+git commit -m 'Added a README file to the repo'
+git push
+```
+
+## Creating our first review
+
+Generally, reviews in git-appraise are used to decide if the code in one branch
+(called the "source") is ready to merge into another branch (called the
+"target"). The meaning of each branch and the policies around merging into a
+branch vary from team to team, but for this example we'll use a simple practice
+called [GitHub Flow](https://guides.github.com/introduction/flow/).
+
+Specifically, we'll create a new branch for a particular feature, review the
+changes to that branch against our master branch, and then delete the feature
+branch once we are done.
+
+### Creating our change
+
+Create the feature branch:
+```shell
+git checkout -b ${USER}/getting-started
+git push --set-upstream origin ${USER}/getting-started
+```
+
+... And make some changes to it:
+```shell
+echo "This is an example repository used for coming up to speed" >> README.md
+git commit -a -m "Added an explanation to the README file"
+git push
+```
+
+### Requesting the review
+
+Up to this point we've only used the regular commands that come with git. Now,
+we will use git-appraise to perform a review:
+
+Request a review:
+```shell
+git appraise request
+```
+
+The output of this will be a summary of the newly requested review:
+```
+Review requested:
+Commit: 1e6eb14c8014593843c5b5f29377585e4ed55304
+Target Ref: refs/heads/master
+Review Ref: refs/heads/ojarjur/getting-started
+Message: "Added an explanation to the README file"
+```
+
+Show the details of the current review:
+```shell
+git appraise show
+```
+
+```
+[pending] 1e6eb14c8014
+  Added an explanation to the README file
+  "refs/heads/ojarjur/getting-started" -> "refs/heads/master"
+  reviewers: ""
+  requester: "ojarjur@google.com"
+  build status: unknown
+  analyses:  No analyses available
+  comments (0 threads):
+```
+
+Show the changes included in the review:
+```shell
+git appraise show --diff
+```
+
+```
+diff --git a/README.md b/README.md
+index 08fde78..85c4208 100644
+--- a/README.md
++++ b/README.md
+@@ -1 +1,2 @@
+ # Example Repository
++This is an example repository used for coming up to speed
+```
+
+### Sending our updates to the remote repository
+
+Before a teammate can review our change, we have to make it available to them.
+This involves pushing both our commits, and our code review data to the remote
+repository:
+```shell
+git push
+git appraise pull
+git appraise push
+```
+
+The command `git appraise pull` is used to make sure that our local code review
+data includes everything from the remote repo before we try to push our changes
+back to it. If you forget to run this command, then the subsequent call to
+`git appraise push` might fail with a message that the push was rejected. If
+that happens, simply run `git appraise pull` and try again.
+
+## Reviewing the change
+
+Your teammates can review your changes using the same tool.
+
+Fetch the current data from the remote repository:
+```shell
+git fetch origin
+git appraise pull
+```
+
+List the open reviews:
+```shell
+git appraise list
+```
+
+The output of this command will be a list of entries formatted like this:
+```
+Loaded 1 open reviews:
+[pending] 1e6eb14c8014
+  Added an explanation to the README file
+```
+
+The text within the square brackets is the status of a review, and for open
+reviews will be one of "pending", "accepted", or "rejected". The text which
+follows the status is the hash of the first commit in the review. This is
+used to uniquely identify reviews, and most git-appraise commands will accept
+this hash as an argument in order to select the review to handle.
+
+For instance, we can see the details of a specific review using the "show"
+subcommand:
+```shell
+git appraise show 1e6eb14c8014
+```
+
+```
+[pending] 1e6eb14c8014
+  Added an explanation to the README file
+  "refs/heads/ojarjur/getting-started" -> "refs/heads/master"
+  reviewers: ""
+  requester: "ojarjur@google.com"
+  build status: unknown
+  analyses:  No analyses available
+  comments (0 threads):
+```
+
+... or, we can see the diff of the changes under review:
+```shell
+git appraise show --diff 1e6eb14c8014
+```
+
+```
+diff --git a/README.md b/README.md
+index 08fde78..85c4208 100644
+--- a/README.md
++++ b/README.md
+@@ -1 +1,2 @@
+ # Example Repository
++This is an example repository used for coming up to speed
+```
+
+Comments can be added either for the entire review, or on individual lines:
+```shell
+git appraise comment -f README.md -l 2 -m "Ah, so that's what this is" 1e6eb14c8014
+```
+
+These comments then show up in the output of `git appraise show`:
+```shell
+git appraise show 1e6eb14c8014
+```
+
+```
+[pending] 1e6eb14c8014
+  Added an explanation to the README file
+  "refs/heads/ojarjur/getting-started" -> "refs/heads/master"
+  reviewers: ""
+  requester: "ojarjur@google.com"
+  build status: unknown
+  analyses:  No analyses available
+  comments (1 threads):
+    "README.md"@1e6eb14c8014
+    |# Example Repository
+    |This is an example repository used for coming up to speed
+    comment: bd4c11ecafd443c9d1dde6035e89804160cd7487
+      author: ojarjur@google.com
+      time:   Fri Dec 18 10:58:54 PST 2015
+      status: fyi
+      Ah, so that's what this is
+```
+
+Comments initially only exist in your local repository, so to share them
+with the rest of your team you have to push your review changes back:
+
+```shell
+git appraise pull
+git appraise push
+```
+
+When the change is ready to be merged, you indicate that by accepting the
+review:
+
+```shell
+git appraise accept 1e6eb14c8014
+git appraise pull
+git appraise push
+```
+
+The updated status of the review will be visible in the output of "show":
+```shell
+git appraise show 1e6eb14c8014
+```
+
+```
+[accepted] 1e6eb14c8014
+  Added an explanation to the README file
+  "refs/heads/ojarjur/getting-started" -> "refs/heads/master"
+  reviewers: ""
+  requester: "ojarjur@google.com"
+  build status: unknown
+  analyses:  No analyses available
+  comments (2 threads):
+    "README.md"@1e6eb14c8014
+    |# Example Repository
+    |This is an example repository used for coming up to speed
+    comment: bd4c11ecafd443c9d1dde6035e89804160cd7487
+      author: ojarjur@google.com
+      time:   Fri Dec 18 10:58:54 PST 2015
+      status: fyi
+      Ah, so that's what this is
+    comment: 4034c60e6ed6f24b01e9a581087d1ab86d376b81
+      author: ojarjur@google.com
+      time:   Fri Dec 18 11:02:45 PST 2015
+      status: fyi
+```
+
+## Submitting the change
+
+Once a review has been accepted, you can merge it with the tool:
+
+```shell
+git appraise submit --merge 1e6eb14c8014
+git push
+```
+
+The submit command will pop up a text editor where you can edit the default
+merge message. That message will be used to create a new commit that is a
+merge of the previous commit on the master branch, and the history of all
+of your changes to the review. You can see what this looks like using
+the `git log --graph` command:
+
+```
+*   commit 3a4d1b8cd264b921c858185f2c36aac283b45e49
+|\  Merge: b404fa3 1e6eb14
+| | Author: Omar Jarjur <ojarjur@google.com>
+| | Date:   Fri Dec 18 11:06:24 2015 -0800
+| | 
+| |     Submitting review 1e6eb14c8014
+| |     
+| |     Added an explanation to the README file
+| |   
+| * commit 1e6eb14c8014593843c5b5f29377585e4ed55304
+|/  Author: Omar Jarjur <ojarjur@google.com>
+|   Date:   Fri Dec 18 10:49:56 2015 -0800
+|   
+|       Added an explanation to the README file
+|  
+* commit b404fa39ae98950d95ab06012191f58507e51d12
+  Author: Omar Jarjur <ojarjur@google.com>
+  Date:   Fri Dec 18 10:48:06 2015 -0800
+  
+      Added a README file to the repo
+```
+
+This is sometimes called a "merge bubble". When the review is simply accepted
+as is, these do not add much value. However, reviews often go through several
+rounds of changes before they are accepted. By using these merge commits, we
+can preserve both the full history of individual reviews, and the high-level
+(review-based) history of the repository.
+
+This can be seen with the history of git-appraise itself. We can see the high
+level review history using `git log --first-parent`:
+
+```
+commit 83c4d770cfde25c943de161c0cac54d714b7de38
+Merge: 9a607b8 931d1b4
+Author: Omar Jarjur <ojarjur@google.com>
+Date:   Fri Dec 18 09:46:10 2015 -0800
+
+    Submitting review 8cb887077783
+    
+    Fix a bug where requesting a review would fail with an erroneous message.
+    
+    We were figuring out the set of commits to include in a review by
+    listing the commits between the head of the target ref and the head of
+    the source ref. However, this only works if the source ref is a
+    fast-forward of the target ref.
+    
+    This commit changes it so that we use the merge-base of the target and
+    source refs as the starting point instead of the target ref.
+
+commit 9a607b8529d7483e5b323303c73da05843ff3ca9
+Author: Harry Lawrence <hazbo@gmx.com>
+Date:   Fri Dec 18 10:24:00 2015 +0000
+
+    Added links to Eclipse and Jenkins plugins
+    
+    As suggested in #11
+
+commit 8876cfff2ed848d50cb559c05d44e11b95ca791c
+Merge: 00c0e82 1436c83
+Author: Omar Jarjur <ojarjur@google.com>
+Date:   Thu Dec 17 12:46:32 2015 -0800
+
+    Submitting review 09aecba64027
+    
+    Force default git editor when omitting -m
+    For review comments, the absence of the -m flag will now attempt to load the
+    user's default git editor.
+    
+    i.e. git appraise comment c0a643ff39dd
+    
+    An initial draft as discussed in #8
+    
+    I'm still not sure whether or not the file that is saved is in the most appropriate place or not. I like the idea of it being relative to the project although it could have gone in `/tmp` I suppose.
+
+commit 00c0e827e5b86fb9d200f474d4f65f43677cbc6c
+Merge: 31209ce 41fde0b
+Author: Omar Jarjur <ojarjur@google.com>
+Date:   Wed Dec 16 17:10:06 2015 -0800
+
+    Submitting review 2c9bff89f0f8
+    
+    Improve the error messages returned when a git command fails.
+    
+    Previously, we were simply cascading the error returned by the instance
+    of exec.Command. However, that winds up just being something of the form
+    "exit status 128", with all of the real error message going to the
+    Stderr field.
+    
+    As such, this commit changes the behavior to save the data written to
+    stderr, and use it to construct a new error to return.
+
+...
+```
+
+Here you see a linear view of the reviews that have been submitted, but if we
+run the command `git log --oneline --graph`, then we can see that the full
+history of each individual review is also available:
+
+```
+*   83c4d77 Submitting review 8cb887077783
+|\  
+| *   931d1b4 Merge branch 'master' into ojarjur/fix-request-bug
+| |\  
+| |/  
+|/|   
+* | 9a607b8 Added links to Eclipse and Jenkins plugins
+| *   c7be567 Merge branch 'master' into ojarjur/fix-request-bug
+| |\  
+| |/  
+|/|   
+* |   8876cff Submitting review 09aecba64027
+|\ \  
+| * | 1436c83 Using git var GIT_EDITOR rather than git config
+| * | 09aecba Force default git editor when omitting -m
+|/ /  
+| * 8cb8870 Fix a bug where requesting a review would fail with an erroneous message.
+|/  
+*   00c0e82 Submitting review 2c9bff89f0f8
+...
+```
+
+## Cleaning up
+
+Now that our feature branch has been merged into master, we can delete it:
+
+```shell
+git branch -d ${USER}/getting-started
+git push origin --delete ${USER}/getting-started
+```
diff --git a/third_party/go/git-appraise/git-appraise/git-appraise.go b/third_party/go/git-appraise/git-appraise/git-appraise.go
new file mode 100644
index 000000000000..ca5a30cf6da0
--- /dev/null
+++ b/third_party/go/git-appraise/git-appraise/git-appraise.go
@@ -0,0 +1,104 @@
+/*
+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.
+*/
+
+// Command git-appraise manages code reviews stored as git-notes in the source repo.
+//
+// To install, run:
+//
+//    $ go get github.com/google/git-appraise/git-appraise
+//
+// And for usage information, run:
+//
+//    $ git-appraise help
+package main
+
+import (
+	"fmt"
+	"github.com/google/git-appraise/commands"
+	"github.com/google/git-appraise/repository"
+	"os"
+	"sort"
+	"strings"
+)
+
+const usageMessageTemplate = `Usage: %s <command>
+
+Where <command> is one of:
+  %s
+
+For individual command usage, run:
+  %s help <command>
+`
+
+func usage() {
+	command := os.Args[0]
+	var subcommands []string
+	for subcommand := range commands.CommandMap {
+		subcommands = append(subcommands, subcommand)
+	}
+	sort.Strings(subcommands)
+	fmt.Printf(usageMessageTemplate, command, strings.Join(subcommands, "\n  "), command)
+}
+
+func help() {
+	if len(os.Args) < 3 {
+		usage()
+		return
+	}
+	subcommand, ok := commands.CommandMap[os.Args[2]]
+	if !ok {
+		fmt.Printf("Unknown command %q\n", os.Args[2])
+		usage()
+		return
+	}
+	subcommand.Usage(os.Args[0])
+}
+
+func main() {
+	if len(os.Args) > 1 && os.Args[1] == "help" {
+		help()
+		return
+	}
+	cwd, err := os.Getwd()
+	if err != nil {
+		fmt.Printf("Unable to get the current working directory: %q\n", err)
+		return
+	}
+	repo, err := repository.NewGitRepo(cwd)
+	if err != nil {
+		fmt.Printf("%s must be run from within a git repo.\n", os.Args[0])
+		return
+	}
+	if len(os.Args) < 2 {
+		subcommand, ok := commands.CommandMap["list"]
+		if !ok {
+			fmt.Printf("Unable to list reviews")
+			return
+		}
+		subcommand.Run(repo, []string{})
+		return
+	}
+	subcommand, ok := commands.CommandMap[os.Args[1]]
+	if !ok {
+		fmt.Printf("Unknown command: %q\n", os.Args[1])
+		usage()
+		return
+	}
+	if err := subcommand.Run(repo, os.Args[2:]); err != nil {
+		fmt.Println(err.Error())
+		os.Exit(1)
+	}
+}
diff --git a/third_party/go/git-appraise/repository/git.go b/third_party/go/git-appraise/repository/git.go
new file mode 100644
index 000000000000..31d27ea6d2ec
--- /dev/null
+++ b/third_party/go/git-appraise/repository/git.go
@@ -0,0 +1,987 @@
+/*
+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 repository contains helper methods for working with the Git repo.
+package repository
+
+import (
+	"bufio"
+	"bytes"
+	"crypto/sha1"
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+)
+
+const branchRefPrefix = "refs/heads/"
+
+// GitRepo represents an instance of a (local) git repository.
+type GitRepo struct {
+	Path string
+}
+
+// Run the given git command with the given I/O reader/writers, returning an error if it fails.
+func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
+	cmd := exec.Command("git", args...)
+	cmd.Dir = repo.Path
+	cmd.Stdin = stdin
+	cmd.Stdout = stdout
+	cmd.Stderr = stderr
+	return cmd.Run()
+}
+
+// Run the given git command and return its stdout, or an error if the command fails.
+func (repo *GitRepo) runGitCommandRaw(args ...string) (string, string, error) {
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	err := repo.runGitCommandWithIO(nil, &stdout, &stderr, args...)
+	return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
+}
+
+// Run the given git command and return its stdout, or an error if the command fails.
+func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
+	stdout, stderr, err := repo.runGitCommandRaw(args...)
+	if err != nil {
+		if stderr == "" {
+			stderr = "Error running git command: " + strings.Join(args, " ")
+		}
+		err = fmt.Errorf(stderr)
+	}
+	return stdout, err
+}
+
+// Run the given git command using the same stdin, stdout, and stderr as the review tool.
+func (repo *GitRepo) runGitCommandInline(args ...string) error {
+	return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...)
+}
+
+// NewGitRepo determines if the given working directory is inside of a git repository,
+// and returns the corresponding GitRepo instance if it is.
+func NewGitRepo(path string) (*GitRepo, error) {
+	repo := &GitRepo{Path: path}
+	_, _, err := repo.runGitCommandRaw("rev-parse")
+	if err == nil {
+		return repo, nil
+	}
+	if _, ok := err.(*exec.ExitError); ok {
+		return nil, err
+	}
+	return nil, err
+}
+
+// GetPath returns the path to the repo.
+func (repo *GitRepo) GetPath() string {
+	return repo.Path
+}
+
+// GetRepoStateHash returns a hash which embodies the entire current state of a repository.
+func (repo *GitRepo) GetRepoStateHash() (string, error) {
+	stateSummary, error := repo.runGitCommand("show-ref")
+	return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), error
+}
+
+// GetUserEmail returns the email address that the user has used to configure git.
+func (repo *GitRepo) GetUserEmail() (string, error) {
+	return repo.runGitCommand("config", "user.email")
+}
+
+// GetUserSigningKey returns the key id the user has configured for
+// sigining git artifacts.
+func (repo *GitRepo) GetUserSigningKey() (string, error) {
+	return repo.runGitCommand("config", "user.signingKey")
+}
+
+// GetCoreEditor returns the name of the editor that the user has used to configure git.
+func (repo *GitRepo) GetCoreEditor() (string, error) {
+	return repo.runGitCommand("var", "GIT_EDITOR")
+}
+
+// GetSubmitStrategy returns the way in which a review is submitted
+func (repo *GitRepo) GetSubmitStrategy() (string, error) {
+	submitStrategy, _ := repo.runGitCommand("config", "appraise.submit")
+	return submitStrategy, nil
+}
+
+// HasUncommittedChanges returns true if there are local, uncommitted changes.
+func (repo *GitRepo) HasUncommittedChanges() (bool, error) {
+	out, err := repo.runGitCommand("status", "--porcelain")
+	if err != nil {
+		return false, err
+	}
+	if len(out) > 0 {
+		return true, nil
+	}
+	return false, nil
+}
+
+// VerifyCommit verifies that the supplied hash points to a known commit.
+func (repo *GitRepo) VerifyCommit(hash string) error {
+	out, err := repo.runGitCommand("cat-file", "-t", hash)
+	if err != nil {
+		return err
+	}
+	objectType := strings.TrimSpace(string(out))
+	if objectType != "commit" {
+		return fmt.Errorf("Hash %q points to a non-commit object of type %q", hash, objectType)
+	}
+	return nil
+}
+
+// VerifyGitRef verifies that the supplied ref points to a known commit.
+func (repo *GitRepo) VerifyGitRef(ref string) error {
+	_, err := repo.runGitCommand("show-ref", "--verify", ref)
+	return err
+}
+
+// GetHeadRef returns the ref that is the current HEAD.
+func (repo *GitRepo) GetHeadRef() (string, error) {
+	return repo.runGitCommand("symbolic-ref", "HEAD")
+}
+
+// GetCommitHash returns the hash of the commit pointed to by the given ref.
+func (repo *GitRepo) GetCommitHash(ref string) (string, error) {
+	return repo.runGitCommand("show", "-s", "--format=%H", ref)
+}
+
+// ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref.
+//
+// This differs from GetCommitHash which only works on exact matches, in that it will try to
+// intelligently handle the scenario of a ref not existing locally, but being known to exist
+// in a remote repo.
+//
+// This method should be used when a command may be performed by either the reviewer or the
+// reviewee, while GetCommitHash should be used when the encompassing command should only be
+// performed by the reviewee.
+func (repo *GitRepo) ResolveRefCommit(ref string) (string, error) {
+	if err := repo.VerifyGitRef(ref); err == nil {
+		return repo.GetCommitHash(ref)
+	}
+	if strings.HasPrefix(ref, "refs/heads/") {
+		// The ref is a branch. Check if it exists in exactly one remote
+		pattern := strings.Replace(ref, "refs/heads", "**", 1)
+		matchingOutput, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", pattern)
+		if err != nil {
+			return "", err
+		}
+		matchingRefs := strings.Split(matchingOutput, "\n")
+		if len(matchingRefs) == 1 && matchingRefs[0] != "" {
+			// There is exactly one match
+			return repo.GetCommitHash(matchingRefs[0])
+		}
+		return "", fmt.Errorf("Unable to find a git ref matching the pattern %q", pattern)
+	}
+	return "", fmt.Errorf("Unknown git ref %q", ref)
+}
+
+// GetCommitMessage returns the message stored in the commit pointed to by the given ref.
+func (repo *GitRepo) GetCommitMessage(ref string) (string, error) {
+	return repo.runGitCommand("show", "-s", "--format=%B", ref)
+}
+
+// GetCommitTime returns the commit time of the commit pointed to by the given ref.
+func (repo *GitRepo) GetCommitTime(ref string) (string, error) {
+	return repo.runGitCommand("show", "-s", "--format=%ct", ref)
+}
+
+// GetLastParent returns the last parent of the given commit (as ordered by git).
+func (repo *GitRepo) GetLastParent(ref string) (string, error) {
+	return repo.runGitCommand("rev-list", "--skip", "1", "-n", "1", ref)
+}
+
+// GetCommitDetails returns the details of a commit's metadata.
+func (repo GitRepo) GetCommitDetails(ref string) (*CommitDetails, error) {
+	var err error
+	show := func(formatString string) (result string) {
+		if err != nil {
+			return ""
+		}
+		result, err = repo.runGitCommand("show", "-s", ref, fmt.Sprintf("--format=tformat:%s", formatString))
+		return result
+	}
+
+	jsonFormatString := "{\"tree\":\"%T\", \"time\": \"%at\"}"
+	detailsJSON := show(jsonFormatString)
+	if err != nil {
+		return nil, err
+	}
+	var details CommitDetails
+	err = json.Unmarshal([]byte(detailsJSON), &details)
+	if err != nil {
+		return nil, err
+	}
+	details.Author = show("%an")
+	details.AuthorEmail = show("%ae")
+	details.Summary = show("%s")
+	parentsString := show("%P")
+	details.Parents = strings.Split(parentsString, " ")
+	if err != nil {
+		return nil, err
+	}
+	return &details, nil
+}
+
+// MergeBase determines if the first commit that is an ancestor of the two arguments.
+func (repo *GitRepo) MergeBase(a, b string) (string, error) {
+	return repo.runGitCommand("merge-base", a, b)
+}
+
+// IsAncestor determines if the first argument points to a commit that is an ancestor of the second.
+func (repo *GitRepo) IsAncestor(ancestor, descendant string) (bool, error) {
+	_, _, err := repo.runGitCommandRaw("merge-base", "--is-ancestor", ancestor, descendant)
+	if err == nil {
+		return true, nil
+	}
+	if _, ok := err.(*exec.ExitError); ok {
+		return false, nil
+	}
+	return false, fmt.Errorf("Error while trying to determine commit ancestry: %v", err)
+}
+
+// Diff computes the diff between two given commits.
+func (repo *GitRepo) Diff(left, right string, diffArgs ...string) (string, error) {
+	args := []string{"diff"}
+	args = append(args, diffArgs...)
+	args = append(args, fmt.Sprintf("%s..%s", left, right))
+	return repo.runGitCommand(args...)
+}
+
+// Show returns the contents of the given file at the given commit.
+func (repo *GitRepo) Show(commit, path string) (string, error) {
+	return repo.runGitCommand("show", fmt.Sprintf("%s:%s", commit, path))
+}
+
+// SwitchToRef changes the currently-checked-out ref.
+func (repo *GitRepo) SwitchToRef(ref string) error {
+	// If the ref starts with "refs/heads/", then we have to trim that prefix,
+	// or else we will wind up in a detached HEAD state.
+	if strings.HasPrefix(ref, branchRefPrefix) {
+		ref = ref[len(branchRefPrefix):]
+	}
+	_, err := repo.runGitCommand("checkout", ref)
+	return err
+}
+
+// mergeArchives merges two archive refs.
+func (repo *GitRepo) mergeArchives(archive, remoteArchive string) error {
+	remoteHash, err := repo.GetCommitHash(remoteArchive)
+	if err != nil {
+		return err
+	}
+	if remoteHash == "" {
+		// The remote archive does not exist, so we have nothing to do
+		return nil
+	}
+
+	archiveHash, err := repo.GetCommitHash(archive)
+	if err != nil {
+		return err
+	}
+	if archiveHash == "" {
+		// The local archive does not exist, so we merely need to set it
+		_, err := repo.runGitCommand("update-ref", archive, remoteHash)
+		return err
+	}
+
+	isAncestor, err := repo.IsAncestor(archiveHash, remoteHash)
+	if err != nil {
+		return err
+	}
+	if isAncestor {
+		// The archive can simply be fast-forwarded
+		_, err := repo.runGitCommand("update-ref", archive, remoteHash, archiveHash)
+		return err
+	}
+
+	// Create a merge commit of the two archives
+	refDetails, err := repo.GetCommitDetails(remoteArchive)
+	if err != nil {
+		return err
+	}
+	newArchiveHash, err := repo.runGitCommand("commit-tree", "-p", remoteHash, "-p", archiveHash, "-m", "Merge local and remote archives", refDetails.Tree)
+	if err != nil {
+		return err
+	}
+	newArchiveHash = strings.TrimSpace(newArchiveHash)
+	_, err = repo.runGitCommand("update-ref", archive, newArchiveHash, archiveHash)
+	return err
+}
+
+// ArchiveRef adds the current commit pointed to by the 'ref' argument
+// under the ref specified in the 'archive' argument.
+//
+// Both the 'ref' and 'archive' arguments are expected to be the fully
+// qualified names of git refs (e.g. 'refs/heads/my-change' or
+// 'refs/devtools/archives/reviews').
+//
+// If the ref pointed to by the 'archive' argument does not exist
+// yet, then it will be created.
+func (repo *GitRepo) ArchiveRef(ref, archive string) error {
+	refHash, err := repo.GetCommitHash(ref)
+	if err != nil {
+		return err
+	}
+	refDetails, err := repo.GetCommitDetails(ref)
+	if err != nil {
+		return err
+	}
+
+	commitTreeArgs := []string{"commit-tree"}
+	archiveHash, err := repo.GetCommitHash(archive)
+	if err != nil {
+		archiveHash = ""
+	} else {
+		commitTreeArgs = append(commitTreeArgs, "-p", archiveHash)
+	}
+	commitTreeArgs = append(commitTreeArgs, "-p", refHash, "-m", fmt.Sprintf("Archive %s", refHash), refDetails.Tree)
+	newArchiveHash, err := repo.runGitCommand(commitTreeArgs...)
+	if err != nil {
+		return err
+	}
+	newArchiveHash = strings.TrimSpace(newArchiveHash)
+	updateRefArgs := []string{"update-ref", archive, newArchiveHash}
+	if archiveHash != "" {
+		updateRefArgs = append(updateRefArgs, archiveHash)
+	}
+	_, err = repo.runGitCommand(updateRefArgs...)
+	return err
+}
+
+// MergeRef merges the given ref into the current one.
+//
+// The ref argument is the ref to merge, and fastForward indicates that the
+// current ref should only move forward, as opposed to creating a bubble merge.
+// The messages argument(s) provide text that should be included in the default
+// merge commit message (separated by blank lines).
+func (repo *GitRepo) MergeRef(ref string, fastForward bool, messages ...string) error {
+	args := []string{"merge"}
+	if fastForward {
+		args = append(args, "--ff", "--ff-only")
+	} else {
+		args = append(args, "--no-ff")
+	}
+	if len(messages) > 0 {
+		commitMessage := strings.Join(messages, "\n\n")
+		args = append(args, "-e", "-m", commitMessage)
+	}
+	args = append(args, ref)
+	return repo.runGitCommandInline(args...)
+}
+
+// MergeAndSignRef merges the given ref into the current one and signs the
+// merge.
+//
+// The ref argument is the ref to merge, and fastForward indicates that the
+// current ref should only move forward, as opposed to creating a bubble merge.
+// The messages argument(s) provide text that should be included in the default
+// merge commit message (separated by blank lines).
+func (repo *GitRepo) MergeAndSignRef(ref string, fastForward bool,
+	messages ...string) error {
+
+	args := []string{"merge"}
+	if fastForward {
+		args = append(args, "--ff", "--ff-only", "-S")
+	} else {
+		args = append(args, "--no-ff", "-S")
+	}
+	if len(messages) > 0 {
+		commitMessage := strings.Join(messages, "\n\n")
+		args = append(args, "-e", "-m", commitMessage)
+	}
+	args = append(args, ref)
+	return repo.runGitCommandInline(args...)
+}
+
+// RebaseRef rebases the current ref onto the given one.
+func (repo *GitRepo) RebaseRef(ref string) error {
+	return repo.runGitCommandInline("rebase", "-i", ref)
+}
+
+// RebaseAndSignRef rebases the current ref onto the given one and signs the
+// result.
+func (repo *GitRepo) RebaseAndSignRef(ref string) error {
+	return repo.runGitCommandInline("rebase", "-S", "-i", ref)
+}
+
+// ListCommits returns the list of commits reachable from the given ref.
+//
+// The generated list is in chronological order (with the oldest commit first).
+//
+// If the specified ref does not exist, then this method returns an empty result.
+func (repo *GitRepo) ListCommits(ref string) []string {
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "rev-list", "--reverse", ref); err != nil {
+		return nil
+	}
+
+	byteLines := bytes.Split(stdout.Bytes(), []byte("\n"))
+	var commits []string
+	for _, byteLine := range byteLines {
+		commits = append(commits, string(byteLine))
+	}
+	return commits
+}
+
+// ListCommitsBetween returns the list of commits between the two given revisions.
+//
+// The "from" parameter is the starting point (exclusive), and the "to"
+// parameter is the ending point (inclusive).
+//
+// The "from" commit does not need to be an ancestor of the "to" commit. If it
+// is not, then the merge base of the two is used as the starting point.
+// Admittedly, this makes calling these the "between" commits is a bit of a
+// misnomer, but it also makes the method easier to use when you want to
+// generate the list of changes in a feature branch, as it eliminates the need
+// to explicitly calculate the merge base. This also makes the semantics of the
+// method compatible with git's built-in "rev-list" command.
+//
+// The generated list is in chronological order (with the oldest commit first).
+func (repo *GitRepo) ListCommitsBetween(from, to string) ([]string, error) {
+	out, err := repo.runGitCommand("rev-list", "--reverse", from+".."+to)
+	if err != nil {
+		return nil, err
+	}
+	if out == "" {
+		return nil, nil
+	}
+	return strings.Split(out, "\n"), nil
+}
+
+// GetNotes uses the "git" command-line tool to read the notes from the given ref for a given revision.
+func (repo *GitRepo) GetNotes(notesRef, revision string) []Note {
+	var notes []Note
+	rawNotes, err := repo.runGitCommand("notes", "--ref", notesRef, "show", revision)
+	if err != nil {
+		// We just assume that this means there are no notes
+		return nil
+	}
+	for _, line := range strings.Split(rawNotes, "\n") {
+		notes = append(notes, Note([]byte(line)))
+	}
+	return notes
+}
+
+func stringsReader(s []*string) io.Reader {
+	var subReaders []io.Reader
+	for _, strPtr := range s {
+		subReader := strings.NewReader(*strPtr)
+		subReaders = append(subReaders, subReader, strings.NewReader("\n"))
+	}
+	return io.MultiReader(subReaders...)
+}
+
+// splitBatchCheckOutput parses the output of a 'git cat-file --batch-check=...' command.
+//
+// The output is expected to be formatted as a series of entries, with each
+// entry consisting of:
+// 1. The SHA1 hash of the git object being output, followed by a space.
+// 2. The git "type" of the object (commit, blob, tree, missing, etc), followed by a newline.
+//
+// To generate this format, make sure that the 'git cat-file' command includes
+// the argument '--batch-check=%(objectname) %(objecttype)'.
+//
+// The return value is a map from object hash to a boolean indicating if that object is a commit.
+func splitBatchCheckOutput(out *bytes.Buffer) (map[string]bool, error) {
+	isCommit := make(map[string]bool)
+	reader := bufio.NewReader(out)
+	for {
+		nameLine, err := reader.ReadString(byte(' '))
+		if err == io.EOF {
+			return isCommit, nil
+		}
+		if err != nil {
+			return nil, fmt.Errorf("Failure while reading the next object name: %v", err)
+		}
+		nameLine = strings.TrimSuffix(nameLine, " ")
+		typeLine, err := reader.ReadString(byte('\n'))
+		if err != nil && err != io.EOF {
+			return nil, fmt.Errorf("Failure while reading the next object type: %q - %v", nameLine, err)
+		}
+		typeLine = strings.TrimSuffix(typeLine, "\n")
+		if typeLine == "commit" {
+			isCommit[nameLine] = true
+		}
+	}
+}
+
+// splitBatchCatFileOutput parses the output of a 'git cat-file --batch=...' command.
+//
+// The output is expected to be formatted as a series of entries, with each
+// entry consisting of:
+// 1. The SHA1 hash of the git object being output, followed by a newline.
+// 2. The size of the object's contents in bytes, followed by a newline.
+// 3. The objects contents.
+//
+// To generate this format, make sure that the 'git cat-file' command includes
+// the argument '--batch=%(objectname)\n%(objectsize)'.
+func splitBatchCatFileOutput(out *bytes.Buffer) (map[string][]byte, error) {
+	contentsMap := make(map[string][]byte)
+	reader := bufio.NewReader(out)
+	for {
+		nameLine, err := reader.ReadString(byte('\n'))
+		if strings.HasSuffix(nameLine, "\n") {
+			nameLine = strings.TrimSuffix(nameLine, "\n")
+		}
+		if err == io.EOF {
+			return contentsMap, nil
+		}
+		if err != nil {
+			return nil, fmt.Errorf("Failure while reading the next object name: %v", err)
+		}
+		sizeLine, err := reader.ReadString(byte('\n'))
+		if strings.HasSuffix(sizeLine, "\n") {
+			sizeLine = strings.TrimSuffix(sizeLine, "\n")
+		}
+		if err != nil {
+			return nil, fmt.Errorf("Failure while reading the next object size: %q - %v", nameLine, err)
+		}
+		size, err := strconv.Atoi(sizeLine)
+		if err != nil {
+			return nil, fmt.Errorf("Failure while parsing the next object size: %q - %v", nameLine, err)
+		}
+		contentBytes := make([]byte, size, size)
+		readDest := contentBytes
+		len := 0
+		err = nil
+		for err == nil && len < size {
+			nextLen := 0
+			nextLen, err = reader.Read(readDest)
+			len += nextLen
+			readDest = contentBytes[len:]
+		}
+		contentsMap[nameLine] = contentBytes
+		if err == io.EOF {
+			return contentsMap, nil
+		}
+		if err != nil {
+			return nil, err
+		}
+		for bs, err := reader.Peek(1); err == nil && bs[0] == byte('\n'); bs, err = reader.Peek(1) {
+			reader.ReadByte()
+		}
+	}
+}
+
+// notesMapping represents the association between a git object and the notes for that object.
+type notesMapping struct {
+	ObjectHash *string
+	NotesHash  *string
+}
+
+// notesOverview represents a high-level overview of all the notes under a single notes ref.
+type notesOverview struct {
+	NotesMappings      []*notesMapping
+	ObjectHashesReader io.Reader
+	NotesHashesReader  io.Reader
+}
+
+// notesOverview returns an overview of the git notes stored under the given ref.
+func (repo *GitRepo) notesOverview(notesRef string) (*notesOverview, error) {
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "notes", "--ref", notesRef, "list"); err != nil {
+		return nil, err
+	}
+
+	var notesMappings []*notesMapping
+	var objHashes []*string
+	var notesHashes []*string
+	outScanner := bufio.NewScanner(&stdout)
+	for outScanner.Scan() {
+		line := outScanner.Text()
+		lineParts := strings.Split(line, " ")
+		if len(lineParts) != 2 {
+			return nil, fmt.Errorf("Malformed output line from 'git-notes list': %q", line)
+		}
+		objHash := &lineParts[1]
+		notesHash := &lineParts[0]
+		notesMappings = append(notesMappings, &notesMapping{
+			ObjectHash: objHash,
+			NotesHash:  notesHash,
+		})
+		objHashes = append(objHashes, objHash)
+		notesHashes = append(notesHashes, notesHash)
+	}
+	err := outScanner.Err()
+	if err != nil && err != io.EOF {
+		return nil, fmt.Errorf("Failure parsing the output of 'git-notes list': %v", err)
+	}
+	return &notesOverview{
+		NotesMappings:      notesMappings,
+		ObjectHashesReader: stringsReader(objHashes),
+		NotesHashesReader:  stringsReader(notesHashes),
+	}, nil
+}
+
+// getIsCommitMap returns a mapping of all the annotated objects that are commits.
+func (overview *notesOverview) getIsCommitMap(repo *GitRepo) (map[string]bool, error) {
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	if err := repo.runGitCommandWithIO(overview.ObjectHashesReader, &stdout, &stderr, "cat-file", "--batch-check=%(objectname) %(objecttype)"); err != nil {
+		return nil, fmt.Errorf("Failure performing a batch file check: %v", err)
+	}
+	isCommit, err := splitBatchCheckOutput(&stdout)
+	if err != nil {
+		return nil, fmt.Errorf("Failure parsing the output of a batch file check: %v", err)
+	}
+	return isCommit, nil
+}
+
+// getNoteContentsMap returns a mapping from all the notes hashes to their contents.
+func (overview *notesOverview) getNoteContentsMap(repo *GitRepo) (map[string][]byte, error) {
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	if err := repo.runGitCommandWithIO(overview.NotesHashesReader, &stdout, &stderr, "cat-file", "--batch=%(objectname)\n%(objectsize)"); err != nil {
+		return nil, fmt.Errorf("Failure performing a batch file read: %v", err)
+	}
+	noteContentsMap, err := splitBatchCatFileOutput(&stdout)
+	if err != nil {
+		return nil, fmt.Errorf("Failure parsing the output of a batch file read: %v", err)
+	}
+	return noteContentsMap, nil
+}
+
+// GetAllNotes reads the contents of the notes under the given ref for every commit.
+//
+// The returned value is a mapping from commit hash to the list of notes for that commit.
+//
+// This is the batch version of the corresponding GetNotes(...) method.
+func (repo *GitRepo) GetAllNotes(notesRef string) (map[string][]Note, error) {
+	// This code is unfortunately quite complicated, but it needs to be so.
+	//
+	// Conceptually, this is equivalent to:
+	//   result := make(map[string][]Note)
+	//   for _, commit := range repo.ListNotedRevisions(notesRef) {
+	//     result[commit] = repo.GetNotes(notesRef, commit)
+	//   }
+	//   return result, nil
+	//
+	// However, that logic would require separate executions of the 'git'
+	// command for every annotated commit. For a repo with 10s of thousands
+	// of reviews, that would mean calling Cmd.Run(...) 10s of thousands of
+	// times. That, in turn, would take so long that the tool would be unusable.
+	//
+	// This method avoids that by taking advantage of the 'git cat-file --batch="..."'
+	// command. That allows us to use a single invocation of Cmd.Run(...) to
+	// inspect multiple git objects at once.
+	//
+	// As such, regardless of the number of reviews in a repo, we can get all
+	// of the notes using a total of three invocations of Cmd.Run(...):
+	//  1. One to list all the annotated objects (and their notes hash)
+	//  2. A second one to filter out all of the annotated objects that are not commits.
+	//  3. A final one to get the contents of all of the notes blobs.
+	overview, err := repo.notesOverview(notesRef)
+	if err != nil {
+		return nil, err
+	}
+	isCommit, err := overview.getIsCommitMap(repo)
+	if err != nil {
+		return nil, fmt.Errorf("Failure building the set of commit objects: %v", err)
+	}
+	noteContentsMap, err := overview.getNoteContentsMap(repo)
+	if err != nil {
+		return nil, fmt.Errorf("Failure building the mapping from notes hash to contents: %v", err)
+	}
+	commitNotesMap := make(map[string][]Note)
+	for _, notesMapping := range overview.NotesMappings {
+		if !isCommit[*notesMapping.ObjectHash] {
+			continue
+		}
+		noteBytes := noteContentsMap[*notesMapping.NotesHash]
+		byteSlices := bytes.Split(noteBytes, []byte("\n"))
+		var notes []Note
+		for _, slice := range byteSlices {
+			notes = append(notes, Note(slice))
+		}
+		commitNotesMap[*notesMapping.ObjectHash] = notes
+	}
+
+	return commitNotesMap, nil
+}
+
+// AppendNote appends a note to a revision under the given ref.
+func (repo *GitRepo) AppendNote(notesRef, revision string, note Note) error {
+	_, err := repo.runGitCommand("notes", "--ref", notesRef, "append", "-m", string(note), revision)
+	return err
+}
+
+// ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref.
+func (repo *GitRepo) ListNotedRevisions(notesRef string) []string {
+	var revisions []string
+	notesListOut, err := repo.runGitCommand("notes", "--ref", notesRef, "list")
+	if err != nil {
+		return nil
+	}
+	notesList := strings.Split(notesListOut, "\n")
+	for _, notePair := range notesList {
+		noteParts := strings.SplitN(notePair, " ", 2)
+		if len(noteParts) == 2 {
+			objHash := noteParts[1]
+			objType, err := repo.runGitCommand("cat-file", "-t", objHash)
+			// If a note points to an object that we do not know about (yet), then err will not
+			// be nil. We can safely just ignore those notes.
+			if err == nil && objType == "commit" {
+				revisions = append(revisions, objHash)
+			}
+		}
+	}
+	return revisions
+}
+
+// PushNotes pushes git notes to a remote repo.
+func (repo *GitRepo) PushNotes(remote, notesRefPattern string) error {
+	refspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern)
+
+	// The push is liable to fail if the user forgot to do a pull first, so
+	// we treat errors as user errors rather than fatal errors.
+	err := repo.runGitCommandInline("push", remote, refspec)
+	if err != nil {
+		return fmt.Errorf("Failed to push to the remote '%s': %v", remote, err)
+	}
+	return nil
+}
+
+// PushNotesAndArchive pushes the given notes and archive refs to a remote repo.
+func (repo *GitRepo) PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error {
+	notesRefspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern)
+	archiveRefspec := fmt.Sprintf("%s:%s", archiveRefPattern, archiveRefPattern)
+	err := repo.runGitCommandInline("push", remote, notesRefspec, archiveRefspec)
+	if err != nil {
+		return fmt.Errorf("Failed to push the local archive to the remote '%s': %v", remote, err)
+	}
+	return nil
+}
+
+func getRemoteNotesRef(remote, localNotesRef string) string {
+	relativeNotesRef := strings.TrimPrefix(localNotesRef, "refs/notes/")
+	return "refs/notes/" + remote + "/" + relativeNotesRef
+}
+
+// MergeNotes merges in the remote's state of the notes reference into the
+// local repository's.
+func (repo *GitRepo) MergeNotes(remote, notesRefPattern string) error {
+	remoteRefs, err := repo.runGitCommand("ls-remote", remote, notesRefPattern)
+	if err != nil {
+		return err
+	}
+	for _, line := range strings.Split(remoteRefs, "\n") {
+		lineParts := strings.Split(line, "\t")
+		if len(lineParts) == 2 {
+			ref := lineParts[1]
+			remoteRef := getRemoteNotesRef(remote, ref)
+			_, err := repo.runGitCommand("notes", "--ref", ref, "merge", remoteRef, "-s", "cat_sort_uniq")
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// PullNotes fetches the contents of the given notes ref from a remote repo,
+// and then merges them with the corresponding local notes using the
+// "cat_sort_uniq" strategy.
+func (repo *GitRepo) PullNotes(remote, notesRefPattern string) error {
+	remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern)
+	fetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern)
+	err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
+	if err != nil {
+		return err
+	}
+
+	return repo.MergeNotes(remote, notesRefPattern)
+}
+
+func getRemoteArchiveRef(remote, archiveRefPattern string) string {
+	relativeArchiveRef := strings.TrimPrefix(archiveRefPattern, "refs/devtools/archives/")
+	return "refs/devtools/remoteArchives/" + remote + "/" + relativeArchiveRef
+}
+
+// MergeArchives merges in the remote's state of the archives reference into
+// the local repository's.
+func (repo *GitRepo) MergeArchives(remote, archiveRefPattern string) error {
+	remoteRefs, err := repo.runGitCommand("ls-remote", remote, archiveRefPattern)
+	if err != nil {
+		return err
+	}
+	for _, line := range strings.Split(remoteRefs, "\n") {
+		lineParts := strings.Split(line, "\t")
+		if len(lineParts) == 2 {
+			ref := lineParts[1]
+			remoteRef := getRemoteArchiveRef(remote, ref)
+			if err := repo.mergeArchives(ref, remoteRef); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (repo *GitRepo) fetchNotes(remote, notesRefPattern,
+	archiveRefPattern string) error {
+
+	remoteArchiveRef := getRemoteArchiveRef(remote, archiveRefPattern)
+	archiveFetchRefSpec := fmt.Sprintf("+%s:%s", archiveRefPattern, remoteArchiveRef)
+
+	remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern)
+	notesFetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern)
+
+	return repo.runGitCommandInline("fetch", remote, notesFetchRefSpec, archiveFetchRefSpec)
+}
+
+// PullNotesAndArchive fetches the contents of the notes and archives refs from
+// a remote repo, and merges them with the corresponding local refs.
+//
+// For notes refs, we assume that every note can be automatically merged using
+// the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement),
+// so we automatically merge the remote notes into the local notes.
+//
+// For "archive" refs, they are expected to be used solely for maintaining
+// reachability of commits that are part of the history of any reviews,
+// so we do not maintain any consistency with their tree objects. Instead,
+// we merely ensure that their history graph includes every commit that we
+// intend to keep.
+func (repo *GitRepo) PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error {
+	err := repo.fetchNotes(remote, notesRefPattern, archiveRefPattern)
+	if err != nil {
+		return err
+	}
+
+	err = repo.MergeNotes(remote, notesRefPattern)
+	if err != nil {
+		return err
+	}
+	return repo.MergeArchives(remote, archiveRefPattern)
+}
+
+// FetchAndReturnNewReviewHashes fetches the notes "branches" and then susses
+// out the IDs (the revision the review points to) of any new reviews, then
+// returns that list of IDs.
+//
+// This is accomplished by determining which files in the notes tree have
+// changed because the _names_ of these files correspond to the revisions they
+// point to.
+func (repo *GitRepo) FetchAndReturnNewReviewHashes(remote, notesRefPattern,
+	archiveRefPattern string) ([]string, error) {
+
+	// Record the current state of the reviews and comments refs.
+	var (
+		getAllRevs, getAllComs    bool
+		reviewsList, commentsList []string
+	)
+	reviewBeforeHash, err := repo.GetCommitHash(
+		"notes/" + remote + "/devtools/reviews")
+	getAllRevs = err != nil
+
+	commentBeforeHash, err := repo.GetCommitHash(
+		"notes/" + remote + "/devtools/discuss")
+	getAllComs = err != nil
+
+	// Update them from the remote.
+	err = repo.fetchNotes(remote, notesRefPattern, archiveRefPattern)
+	if err != nil {
+		return nil, err
+	}
+
+	// Now, if either of these are new refs, we just use the whole tree at that
+	// new ref. Otherwise we see which reviews or comments changed and collect
+	// them into a list.
+	if getAllRevs {
+		hash, err := repo.GetCommitHash(
+			"notes/" + remote + "/devtools/reviews")
+		// It is possible that even after we've pulled that this ref still
+		// isn't present (because there are no reviews yet).
+		if err == nil {
+			rvws, err := repo.runGitCommand("ls-tree", "-r", "--name-only",
+				hash)
+			if err != nil {
+				return nil, err
+			}
+			reviewsList = strings.Split(strings.Replace(rvws, "/", "", -1),
+				"\n")
+		}
+	} else {
+		reviewAfterHash, err := repo.GetCommitHash(
+			"notes/" + remote + "/devtools/reviews")
+		if err != nil {
+			return nil, err
+		}
+
+		// Only run through this if the fetch fetched new revisions.
+		// Otherwise leave reviewsList as its default value, an empty slice
+		// of strings.
+		if reviewBeforeHash != reviewAfterHash {
+			newReviewsRaw, err := repo.runGitCommand("diff", "--name-only",
+				reviewBeforeHash, reviewAfterHash)
+			if err != nil {
+				return nil, err
+			}
+			reviewsList = strings.Split(strings.Replace(newReviewsRaw,
+				"/", "", -1), "\n")
+		}
+	}
+
+	if getAllComs {
+		hash, err := repo.GetCommitHash(
+			"notes/" + remote + "/devtools/discuss")
+		// It is possible that even after we've pulled that this ref still
+		// isn't present (because there are no comments yet).
+		if err == nil {
+			rvws, err := repo.runGitCommand("ls-tree", "-r", "--name-only",
+				hash)
+			if err != nil {
+				return nil, err
+			}
+			commentsList = strings.Split(strings.Replace(rvws, "/", "", -1),
+				"\n")
+		}
+	} else {
+		commentAfterHash, err := repo.GetCommitHash(
+			"notes/" + remote + "/devtools/discuss")
+		if err != nil {
+			return nil, err
+		}
+
+		// Only run through this if the fetch fetched new revisions.
+		// Otherwise leave commentsList as its default value, an empty slice
+		// of strings.
+		if commentBeforeHash != commentAfterHash {
+			newCommentsRaw, err := repo.runGitCommand("diff", "--name-only",
+				commentBeforeHash, commentAfterHash)
+			if err != nil {
+				return nil, err
+			}
+			commentsList = strings.Split(strings.Replace(newCommentsRaw,
+				"/", "", -1), "\n")
+		}
+	}
+
+	// Now that we have our two lists, we need to merge them.
+	updatedReviewSet := make(map[string]struct{})
+	for _, hash := range append(reviewsList, commentsList...) {
+		updatedReviewSet[hash] = struct{}{}
+	}
+
+	updatedReviews := make([]string, 0, len(updatedReviewSet))
+	for key, _ := range updatedReviewSet {
+		updatedReviews = append(updatedReviews, key)
+	}
+	return updatedReviews, nil
+}
diff --git a/third_party/go/git-appraise/repository/git_test.go b/third_party/go/git-appraise/repository/git_test.go
new file mode 100644
index 000000000000..e1a9e2b2eace
--- /dev/null
+++ b/third_party/go/git-appraise/repository/git_test.go
@@ -0,0 +1,94 @@
+/*
+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 repository
+
+import (
+	"bytes"
+	"testing"
+)
+
+const (
+	simpleBatchCheckOutput = `ddbdcb9d5aa71d35de481789bacece9a2f8138d0 commit
+de9ebcdf2a1e93365eefc2739f73f2c68a280c11 commit
+def9abf52f9a17d4f168e05bc420557a87a55961 commit
+df324616ea2bc9bf6fc7025fc80a373ecec687b6 missing
+dfdd159c9c11c08d84c8c050d2a1a4db29147916 commit
+e4e48e2b4d76ac305cf76fee1d1c8c0283127d71 commit
+e6ae4ed08704fe3c258ab486b07a36e28c3c238a commit
+e807a993d1807b154294b9875b9d926b6f246d0c commit
+e90f75882526e9bc5a71af64d60ea50092ed0b1d commit`
+	simpleBatchCatFileOutput = `c1f5a5f135b171cc963b822d338000d185f1ae4f
+342
+{"timestamp":"1450315153","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/105/"}
+
+{"timestamp":"1450315161","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/105/","status":"success"}
+
+31ea4952450bbe5db0d6a7a7903e451925106c0f
+141
+{"timestamp":"1440202534","url":"https://travis-ci.org/google/git-appraise/builds/76722074","agent":"continuous-integration/travis-ci/push"}
+
+bde25250a9f6dc9c56f16befa5a2d73c8558b472
+342
+{"timestamp":"1450434854","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/112/"}
+
+{"timestamp":"1450434860","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/112/","status":"success"}
+
+3128dc6881bf7647aea90fef1f4fbf883df6a8fe
+342
+{"timestamp":"1457445850","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/191/"}
+
+{"timestamp":"1457445856","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/191/","status":"success"}
+
+`
+)
+
+func TestSplitBatchCheckOutput(t *testing.T) {
+	buf := bytes.NewBuffer([]byte(simpleBatchCheckOutput))
+	commitsMap, err := splitBatchCheckOutput(buf)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !commitsMap["ddbdcb9d5aa71d35de481789bacece9a2f8138d0"] {
+		t.Fatal("Failed to recognize the first commit as valid")
+	}
+	if !commitsMap["de9ebcdf2a1e93365eefc2739f73f2c68a280c11"] {
+		t.Fatal("Failed to recognize the second commit as valid")
+	}
+	if !commitsMap["e90f75882526e9bc5a71af64d60ea50092ed0b1d"] {
+		t.Fatal("Failed to recognize the last commit as valid")
+	}
+	if commitsMap["df324616ea2bc9bf6fc7025fc80a373ecec687b6"] {
+		t.Fatal("Failed to filter out a missing object")
+	}
+}
+
+func TestSplitBatchCatFileOutput(t *testing.T) {
+	buf := bytes.NewBuffer([]byte(simpleBatchCatFileOutput))
+	notesMap, err := splitBatchCatFileOutput(buf)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(notesMap["c1f5a5f135b171cc963b822d338000d185f1ae4f"]) != 342 {
+		t.Fatal("Failed to parse the contents of the first cat'ed file")
+	}
+	if len(notesMap["31ea4952450bbe5db0d6a7a7903e451925106c0f"]) != 141 {
+		t.Fatal("Failed to parse the contents of the second cat'ed file")
+	}
+	if len(notesMap["3128dc6881bf7647aea90fef1f4fbf883df6a8fe"]) != 342 {
+		t.Fatal("Failed to parse the contents of the last cat'ed file")
+	}
+}
diff --git a/third_party/go/git-appraise/repository/mock_repo.go b/third_party/go/git-appraise/repository/mock_repo.go
new file mode 100644
index 000000000000..2d8debe48387
--- /dev/null
+++ b/third_party/go/git-appraise/repository/mock_repo.go
@@ -0,0 +1,613 @@
+/*
+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 repository
+
+import (
+	"crypto/sha1"
+	"encoding/json"
+	"fmt"
+	"strings"
+)
+
+// Constants used for testing.
+// We initialize our mock repo with two branches (one of which holds a pending review),
+// and commit history that looks like this:
+//
+//  Master Branch:    A--B--D--E--F--J
+//                     \   /    \  \
+//                       C       \  \
+//                                \  \
+//  Review Branch:                 G--H--I
+//
+// Where commits "B" and "D" represent reviews that have been submitted, and "G"
+// is a pending review.
+const (
+	TestTargetRef          = "refs/heads/master"
+	TestReviewRef          = "refs/heads/ojarjur/mychange"
+	TestAlternateReviewRef = "refs/review/mychange"
+	TestRequestsRef        = "refs/notes/devtools/reviews"
+	TestCommentsRef        = "refs/notes/devtools/discuss"
+
+	TestCommitA = "A"
+	TestCommitB = "B"
+	TestCommitC = "C"
+	TestCommitD = "D"
+	TestCommitE = "E"
+	TestCommitF = "F"
+	TestCommitG = "G"
+	TestCommitH = "H"
+	TestCommitI = "I"
+	TestCommitJ = "J"
+
+	TestRequestB = `{"timestamp": "0000000001", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "B"}`
+	TestRequestD = `{"timestamp": "0000000002", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "D"}`
+	TestRequestG = `{"timestamp": "0000000004", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "G"}
+
+{"timestamp": "0000000005", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "Updated description of G"}
+
+{"timestamp": "0000000005", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "Final description of G"}`
+
+	TestDiscussB = `{"timestamp": "0000000001", "author": "ojarjur", "location": {"commit": "B"}, "resolved": true}`
+	TestDiscussD = `{"timestamp": "0000000003", "author": "ojarjur", "location": {"commit": "E"}, "resolved": true}`
+)
+
+type mockCommit struct {
+	Message string   `json:"message,omitempty"`
+	Time    string   `json:"time,omitempty"`
+	Parents []string `json:"parents,omitempty"`
+}
+
+// mockRepoForTest defines an instance of Repo that can be used for testing.
+type mockRepoForTest struct {
+	Head    string
+	Refs    map[string]string            `json:"refs,omitempty"`
+	Commits map[string]mockCommit        `json:"commits,omitempty"`
+	Notes   map[string]map[string]string `json:"notes,omitempty"`
+}
+
+func (r *mockRepoForTest) createCommit(message string, time string, parents []string) (string, error) {
+	newCommit := mockCommit{
+		Message: message,
+		Time:    time,
+		Parents: parents,
+	}
+	newCommitJSON, err := json.Marshal(newCommit)
+	if err != nil {
+		return "", err
+	}
+	newCommitHash := fmt.Sprintf("%x", sha1.Sum([]byte(newCommitJSON)))
+	r.Commits[newCommitHash] = newCommit
+	return newCommitHash, nil
+}
+
+// NewMockRepoForTest returns a mocked-out instance of the Repo interface that has been pre-populated with test data.
+func NewMockRepoForTest() Repo {
+	commitA := mockCommit{
+		Message: "First commit",
+		Time:    "0",
+		Parents: nil,
+	}
+	commitB := mockCommit{
+		Message: "Second commit",
+		Time:    "1",
+		Parents: []string{TestCommitA},
+	}
+	commitC := mockCommit{
+		Message: "No, I'm the second commit",
+		Time:    "1",
+		Parents: []string{TestCommitA},
+	}
+	commitD := mockCommit{
+		Message: "Fourth commit",
+		Time:    "2",
+		Parents: []string{TestCommitB, TestCommitC},
+	}
+	commitE := mockCommit{
+		Message: "Fifth commit",
+		Time:    "3",
+		Parents: []string{TestCommitD},
+	}
+	commitF := mockCommit{
+		Message: "Sixth commit",
+		Time:    "4",
+		Parents: []string{TestCommitE},
+	}
+	commitG := mockCommit{
+		Message: "No, I'm the sixth commit",
+		Time:    "4",
+		Parents: []string{TestCommitE},
+	}
+	commitH := mockCommit{
+		Message: "Seventh commit",
+		Time:    "5",
+		Parents: []string{TestCommitG, TestCommitF},
+	}
+	commitI := mockCommit{
+		Message: "Eighth commit",
+		Time:    "6",
+		Parents: []string{TestCommitH},
+	}
+	commitJ := mockCommit{
+		Message: "No, I'm the eighth commit",
+		Time:    "6",
+		Parents: []string{TestCommitF},
+	}
+	return &mockRepoForTest{
+		Head: TestTargetRef,
+		Refs: map[string]string{
+			TestTargetRef:          TestCommitJ,
+			TestReviewRef:          TestCommitI,
+			TestAlternateReviewRef: TestCommitI,
+		},
+		Commits: map[string]mockCommit{
+			TestCommitA: commitA,
+			TestCommitB: commitB,
+			TestCommitC: commitC,
+			TestCommitD: commitD,
+			TestCommitE: commitE,
+			TestCommitF: commitF,
+			TestCommitG: commitG,
+			TestCommitH: commitH,
+			TestCommitI: commitI,
+			TestCommitJ: commitJ,
+		},
+		Notes: map[string]map[string]string{
+			TestRequestsRef: map[string]string{
+				TestCommitB: TestRequestB,
+				TestCommitD: TestRequestD,
+				TestCommitG: TestRequestG,
+			},
+			TestCommentsRef: map[string]string{
+				TestCommitB: TestDiscussB,
+				TestCommitD: TestDiscussD,
+			},
+		},
+	}
+}
+
+// GetPath returns the path to the repo.
+func (r *mockRepoForTest) GetPath() string { return "~/mockRepo/" }
+
+// GetRepoStateHash returns a hash which embodies the entire current state of a repository.
+func (r *mockRepoForTest) GetRepoStateHash() (string, error) {
+	repoJSON, err := json.Marshal(r)
+	if err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%x", sha1.Sum([]byte(repoJSON))), nil
+}
+
+// GetUserEmail returns the email address that the user has used to configure git.
+func (r *mockRepoForTest) GetUserEmail() (string, error) { return "user@example.com", nil }
+
+// GetUserSigningKey returns the key id the user has configured for
+// sigining git artifacts.
+func (r *mockRepoForTest) GetUserSigningKey() (string, error) {
+	return "gpgsig", nil
+}
+
+// GetCoreEditor returns the name of the editor that the user has used to configure git.
+func (r *mockRepoForTest) GetCoreEditor() (string, error) { return "vi", nil }
+
+// GetSubmitStrategy returns the way in which a review is submitted
+func (r *mockRepoForTest) GetSubmitStrategy() (string, error) { return "merge", nil }
+
+// HasUncommittedChanges returns true if there are local, uncommitted changes.
+func (r *mockRepoForTest) HasUncommittedChanges() (bool, error) { return false, nil }
+
+func (r *mockRepoForTest) resolveLocalRef(ref string) (string, error) {
+	if ref == "HEAD" {
+		ref = r.Head
+	}
+	if commit, ok := r.Refs[ref]; ok {
+		return commit, nil
+	}
+	if _, ok := r.Commits[ref]; ok {
+		return ref, nil
+	}
+	return "", fmt.Errorf("The ref %q does not exist", ref)
+}
+
+// VerifyCommit verifies that the supplied hash points to a known commit.
+func (r *mockRepoForTest) VerifyCommit(hash string) error {
+	if _, ok := r.Commits[hash]; !ok {
+		return fmt.Errorf("The given hash %q is not a known commit", hash)
+	}
+	return nil
+}
+
+// VerifyGitRef verifies that the supplied ref points to a known commit.
+func (r *mockRepoForTest) VerifyGitRef(ref string) error {
+	_, err := r.resolveLocalRef(ref)
+	return err
+}
+
+// GetHeadRef returns the ref that is the current HEAD.
+func (r *mockRepoForTest) GetHeadRef() (string, error) { return r.Head, nil }
+
+// GetCommitHash returns the hash of the commit pointed to by the given ref.
+func (r *mockRepoForTest) GetCommitHash(ref string) (string, error) {
+	err := r.VerifyGitRef(ref)
+	if err != nil {
+		return "", err
+	}
+	return r.resolveLocalRef(ref)
+}
+
+// ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref.
+//
+// This differs from GetCommitHash which only works on exact matches, in that it will try to
+// intelligently handle the scenario of a ref not existing locally, but being known to exist
+// in a remote repo.
+//
+// This method should be used when a command may be performed by either the reviewer or the
+// reviewee, while GetCommitHash should be used when the encompassing command should only be
+// performed by the reviewee.
+func (r *mockRepoForTest) ResolveRefCommit(ref string) (string, error) {
+	if commit, err := r.resolveLocalRef(ref); err == nil {
+		return commit, err
+	}
+	return r.resolveLocalRef(strings.Replace(ref, "refs/heads/", "refs/remotes/origin/", 1))
+}
+
+func (r *mockRepoForTest) getCommit(ref string) (mockCommit, error) {
+	commit, err := r.resolveLocalRef(ref)
+	return r.Commits[commit], err
+}
+
+// GetCommitMessage returns the message stored in the commit pointed to by the given ref.
+func (r *mockRepoForTest) GetCommitMessage(ref string) (string, error) {
+	commit, err := r.getCommit(ref)
+	if err != nil {
+		return "", err
+	}
+	return commit.Message, nil
+}
+
+// GetCommitTime returns the commit time of the commit pointed to by the given ref.
+func (r *mockRepoForTest) GetCommitTime(ref string) (string, error) {
+	commit, err := r.getCommit(ref)
+	if err != nil {
+		return "", err
+	}
+	return commit.Time, nil
+}
+
+// GetLastParent returns the last parent of the given commit (as ordered by git).
+func (r *mockRepoForTest) GetLastParent(ref string) (string, error) {
+	commit, err := r.getCommit(ref)
+	if len(commit.Parents) > 0 {
+		return commit.Parents[len(commit.Parents)-1], err
+	}
+	return "", err
+}
+
+// GetCommitDetails returns the details of a commit's metadata.
+func (r *mockRepoForTest) GetCommitDetails(ref string) (*CommitDetails, error) {
+	commit, err := r.getCommit(ref)
+	if err != nil {
+		return nil, err
+	}
+	var details CommitDetails
+	details.Author = "Test Author"
+	details.AuthorEmail = "author@example.com"
+	details.Summary = commit.Message
+	details.Time = commit.Time
+	details.Parents = commit.Parents
+	return &details, nil
+}
+
+// ancestors returns the breadth-first traversal of a commit's ancestors
+func (r *mockRepoForTest) ancestors(commit string) ([]string, error) {
+	queue := []string{commit}
+	var ancestors []string
+	for queue != nil {
+		var nextQueue []string
+		for _, c := range queue {
+			commit, err := r.getCommit(c)
+			if err != nil {
+				return nil, err
+			}
+			parents := commit.Parents
+			nextQueue = append(nextQueue, parents...)
+			ancestors = append(ancestors, parents...)
+		}
+		queue = nextQueue
+	}
+	return ancestors, nil
+}
+
+// IsAncestor determines if the first argument points to a commit that is an ancestor of the second.
+func (r *mockRepoForTest) IsAncestor(ancestor, descendant string) (bool, error) {
+	var err error
+	ancestor, err = r.resolveLocalRef(ancestor)
+	if err != nil {
+		return false, err
+	}
+	descendant, err = r.resolveLocalRef(descendant)
+	if err != nil {
+		return false, err
+	}
+	if ancestor == descendant {
+		return true, nil
+	}
+	descendantCommit, err := r.getCommit(descendant)
+	if err != nil {
+		return false, err
+	}
+	for _, parent := range descendantCommit.Parents {
+		if t, e := r.IsAncestor(ancestor, parent); e == nil && t {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+// MergeBase determines if the first commit that is an ancestor of the two arguments.
+func (r *mockRepoForTest) MergeBase(a, b string) (string, error) {
+	ancestors, err := r.ancestors(a)
+	if err != nil {
+		return "", err
+	}
+	for _, ancestor := range ancestors {
+		if t, e := r.IsAncestor(ancestor, b); e == nil && t {
+			return ancestor, nil
+		}
+	}
+	return "", nil
+}
+
+// Diff computes the diff between two given commits.
+func (r *mockRepoForTest) Diff(left, right string, diffArgs ...string) (string, error) {
+	return fmt.Sprintf("Diff between %q and %q", left, right), nil
+}
+
+// Show returns the contents of the given file at the given commit.
+func (r *mockRepoForTest) Show(commit, path string) (string, error) {
+	return fmt.Sprintf("%s:%s", commit, path), nil
+}
+
+// SwitchToRef changes the currently-checked-out ref.
+func (r *mockRepoForTest) SwitchToRef(ref string) error {
+	r.Head = ref
+	return nil
+}
+
+// ArchiveRef adds the current commit pointed to by the 'ref' argument
+// under the ref specified in the 'archive' argument.
+//
+// Both the 'ref' and 'archive' arguments are expected to be the fully
+// qualified names of git refs (e.g. 'refs/heads/my-change' or
+// 'refs/archive/devtools').
+//
+// If the ref pointed to by the 'archive' argument does not exist
+// yet, then it will be created.
+func (r *mockRepoForTest) ArchiveRef(ref, archive string) error {
+	commitToArchive, err := r.resolveLocalRef(ref)
+	if err != nil {
+		return err
+	}
+	var archiveParents []string
+	if archiveCommit, err := r.resolveLocalRef(archive); err == nil {
+		archiveParents = []string{archiveCommit, commitToArchive}
+	} else {
+		archiveParents = []string{commitToArchive}
+	}
+	archiveCommit, err := r.createCommit("Archiving", "Nowish", archiveParents)
+	if err != nil {
+		return err
+	}
+	r.Refs[archive] = archiveCommit
+	return nil
+}
+
+// MergeRef merges the given ref into the current one.
+//
+// The ref argument is the ref to merge, and fastForward indicates that the
+// current ref should only move forward, as opposed to creating a bubble merge.
+func (r *mockRepoForTest) MergeRef(ref string, fastForward bool, messages ...string) error {
+	newCommitHash, err := r.resolveLocalRef(ref)
+	if err != nil {
+		return err
+	}
+	if !fastForward {
+		origCommit, err := r.resolveLocalRef(r.Head)
+		if err != nil {
+			return err
+		}
+		newCommit, err := r.getCommit(ref)
+		if err != nil {
+			return err
+		}
+		message := strings.Join(messages, "\n\n")
+		time := newCommit.Time
+		parents := []string{origCommit, newCommitHash}
+		newCommitHash, err = r.createCommit(message, time, parents)
+		if err != nil {
+			return err
+		}
+	}
+	r.Refs[r.Head] = newCommitHash
+	return nil
+}
+
+// MergeAndSignRef merges the given ref into the current one and signs the
+// merge.
+//
+// The ref argument is the ref to merge, and fastForward indicates that the
+// current ref should only move forward, as opposed to creating a bubble merge.
+func (r *mockRepoForTest) MergeAndSignRef(ref string, fastForward bool,
+	messages ...string) error {
+	return nil
+}
+
+// RebaseRef rebases the current ref onto the given one.
+func (r *mockRepoForTest) RebaseRef(ref string) error {
+	parentHash := r.Refs[ref]
+	origCommit, err := r.getCommit(r.Head)
+	if err != nil {
+		return err
+	}
+	newCommitHash, err := r.createCommit(origCommit.Message, origCommit.Time, []string{parentHash})
+	if err != nil {
+		return err
+	}
+	if strings.HasPrefix(r.Head, "refs/heads/") {
+		r.Refs[r.Head] = newCommitHash
+	} else {
+		// The current head is not a branch, so updating
+		// it should leave us in a detached-head state.
+		r.Head = newCommitHash
+	}
+	return nil
+}
+
+// RebaseAndSignRef rebases the current ref onto the given one and signs the
+// result.
+func (r *mockRepoForTest) RebaseAndSignRef(ref string) error { return nil }
+
+// ListCommits returns the list of commits reachable from the given ref.
+//
+// The generated list is in chronological order (with the oldest commit first).
+//
+// If the specified ref does not exist, then this method returns an empty result.
+func (r *mockRepoForTest) ListCommits(ref string) []string { return nil }
+
+// ListCommitsBetween returns the list of commits between the two given revisions.
+//
+// The "from" parameter is the starting point (exclusive), and the "to"
+// parameter is the ending point (inclusive).
+//
+// The "from" commit does not need to be an ancestor of the "to" commit. If it
+// is not, then the merge base of the two is used as the starting point.
+// Admittedly, this makes calling these the "between" commits is a bit of a
+// misnomer, but it also makes the method easier to use when you want to
+// generate the list of changes in a feature branch, as it eliminates the need
+// to explicitly calculate the merge base. This also makes the semantics of the
+// method compatible with git's built-in "rev-list" command.
+//
+// The generated list is in chronological order (with the oldest commit first).
+func (r *mockRepoForTest) ListCommitsBetween(from, to string) ([]string, error) {
+	commits := []string{to}
+	potentialCommits, _ := r.ancestors(to)
+	for _, commit := range potentialCommits {
+		blocked, err := r.IsAncestor(commit, from)
+		if err != nil {
+			return nil, err
+		}
+		if !blocked {
+			commits = append(commits, commit)
+		}
+	}
+	return commits, nil
+}
+
+// GetNotes reads the notes from the given ref that annotate the given revision.
+func (r *mockRepoForTest) GetNotes(notesRef, revision string) []Note {
+	notesText := r.Notes[notesRef][revision]
+	var notes []Note
+	for _, line := range strings.Split(notesText, "\n") {
+		notes = append(notes, Note(line))
+	}
+	return notes
+}
+
+// GetAllNotes reads the contents of the notes under the given ref for every commit.
+//
+// The returned value is a mapping from commit hash to the list of notes for that commit.
+//
+// This is the batch version of the corresponding GetNotes(...) method.
+func (r *mockRepoForTest) GetAllNotes(notesRef string) (map[string][]Note, error) {
+	notesMap := make(map[string][]Note)
+	for _, commit := range r.ListNotedRevisions(notesRef) {
+		notesMap[commit] = r.GetNotes(notesRef, commit)
+	}
+	return notesMap, nil
+}
+
+// AppendNote appends a note to a revision under the given ref.
+func (r *mockRepoForTest) AppendNote(ref, revision string, note Note) error {
+	existingNotes := r.Notes[ref][revision]
+	newNotes := existingNotes + "\n" + string(note)
+	r.Notes[ref][revision] = newNotes
+	return nil
+}
+
+// ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref.
+func (r *mockRepoForTest) ListNotedRevisions(notesRef string) []string {
+	var revisions []string
+	for revision := range r.Notes[notesRef] {
+		if _, ok := r.Commits[revision]; ok {
+			revisions = append(revisions, revision)
+		}
+	}
+	return revisions
+}
+
+// PushNotes pushes git notes to a remote repo.
+func (r *mockRepoForTest) PushNotes(remote, notesRefPattern string) error { return nil }
+
+// PullNotes fetches the contents of the given notes ref from a remote repo,
+// and then merges them with the corresponding local notes using the
+// "cat_sort_uniq" strategy.
+func (r *mockRepoForTest) PullNotes(remote, notesRefPattern string) error { return nil }
+
+// PushNotesAndArchive pushes the given notes and archive refs to a remote repo.
+func (r *mockRepoForTest) PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error {
+	return nil
+}
+
+// PullNotesAndArchive fetches the contents of the notes and archives refs from
+// a remote repo, and merges them with the corresponding local refs.
+//
+// For notes refs, we assume that every note can be automatically merged using
+// the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement),
+// so we automatically merge the remote notes into the local notes.
+//
+// For "archive" refs, they are expected to be used solely for maintaining
+// reachability of commits that are part of the history of any reviews,
+// so we do not maintain any consistency with their tree objects. Instead,
+// we merely ensure that their history graph includes every commit that we
+// intend to keep.
+func (r *mockRepoForTest) PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error {
+	return nil
+}
+
+// MergeNotes merges in the remote's state of the archives reference into
+// the local repository's.
+func (repo *mockRepoForTest) MergeNotes(remote, notesRefPattern string) error {
+	return nil
+}
+
+// MergeArchives merges in the remote's state of the archives reference into
+// the local repository's.
+func (repo *mockRepoForTest) MergeArchives(remote,
+	archiveRefPattern string) error {
+	return nil
+}
+
+// FetchAndReturnNewReviewHashes fetches the notes "branches" and then susses
+// out the IDs (the revision the review points to) of any new reviews, then
+// returns that list of IDs.
+//
+// This is accomplished by determining which files in the notes tree have
+// changed because the _names_ of these files correspond to the revisions they
+// point to.
+func (repo *mockRepoForTest) FetchAndReturnNewReviewHashes(remote, notesRefPattern,
+	archiveRefPattern string) ([]string, error) {
+	return nil, nil
+}
diff --git a/third_party/go/git-appraise/repository/repo.go b/third_party/go/git-appraise/repository/repo.go
new file mode 100644
index 000000000000..91acd177edf0
--- /dev/null
+++ b/third_party/go/git-appraise/repository/repo.go
@@ -0,0 +1,221 @@
+/*
+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 repository contains helper methods for working with a Git repo.
+package repository
+
+// Note represents the contents of a git-note
+type Note []byte
+
+// CommitDetails represents the contents of a commit.
+type CommitDetails struct {
+	Author      string   `json:"author,omitempty"`
+	AuthorEmail string   `json:"authorEmail,omitempty"`
+	Tree        string   `json:"tree,omitempty"`
+	Time        string   `json:"time,omitempty"`
+	Parents     []string `json:"parents,omitempty"`
+	Summary     string   `json:"summary,omitempty"`
+}
+
+// Repo represents a source code repository.
+type Repo interface {
+	// GetPath returns the path to the repo.
+	GetPath() string
+
+	// GetRepoStateHash returns a hash which embodies the entire current state of a repository.
+	GetRepoStateHash() (string, error)
+
+	// GetUserEmail returns the email address that the user has used to configure git.
+	GetUserEmail() (string, error)
+
+	// GetUserSigningKey returns the key id the user has configured for
+	// sigining git artifacts.
+	GetUserSigningKey() (string, error)
+
+	// GetCoreEditor returns the name of the editor that the user has used to configure git.
+	GetCoreEditor() (string, error)
+
+	// GetSubmitStrategy returns the way in which a review is submitted
+	GetSubmitStrategy() (string, error)
+
+	// HasUncommittedChanges returns true if there are local, uncommitted changes.
+	HasUncommittedChanges() (bool, error)
+
+	// VerifyCommit verifies that the supplied hash points to a known commit.
+	VerifyCommit(hash string) error
+
+	// VerifyGitRef verifies that the supplied ref points to a known commit.
+	VerifyGitRef(ref string) error
+
+	// GetHeadRef returns the ref that is the current HEAD.
+	GetHeadRef() (string, error)
+
+	// GetCommitHash returns the hash of the commit pointed to by the given ref.
+	GetCommitHash(ref string) (string, error)
+
+	// ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref.
+	//
+	// This differs from GetCommitHash which only works on exact matches, in that it will try to
+	// intelligently handle the scenario of a ref not existing locally, but being known to exist
+	// in a remote repo.
+	//
+	// This method should be used when a command may be performed by either the reviewer or the
+	// reviewee, while GetCommitHash should be used when the encompassing command should only be
+	// performed by the reviewee.
+	ResolveRefCommit(ref string) (string, error)
+
+	// GetCommitMessage returns the message stored in the commit pointed to by the given ref.
+	GetCommitMessage(ref string) (string, error)
+
+	// GetCommitTime returns the commit time of the commit pointed to by the given ref.
+	GetCommitTime(ref string) (string, error)
+
+	// GetLastParent returns the last parent of the given commit (as ordered by git).
+	GetLastParent(ref string) (string, error)
+
+	// GetCommitDetails returns the details of a commit's metadata.
+	GetCommitDetails(ref string) (*CommitDetails, error)
+
+	// MergeBase determines if the first commit that is an ancestor of the two arguments.
+	MergeBase(a, b string) (string, error)
+
+	// IsAncestor determines if the first argument points to a commit that is an ancestor of the second.
+	IsAncestor(ancestor, descendant string) (bool, error)
+
+	// Diff computes the diff between two given commits.
+	Diff(left, right string, diffArgs ...string) (string, error)
+
+	// Show returns the contents of the given file at the given commit.
+	Show(commit, path string) (string, error)
+
+	// SwitchToRef changes the currently-checked-out ref.
+	SwitchToRef(ref string) error
+
+	// ArchiveRef adds the current commit pointed to by the 'ref' argument
+	// under the ref specified in the 'archive' argument.
+	//
+	// Both the 'ref' and 'archive' arguments are expected to be the fully
+	// qualified names of git refs (e.g. 'refs/heads/my-change' or
+	// 'refs/archive/devtools').
+	//
+	// If the ref pointed to by the 'archive' argument does not exist
+	// yet, then it will be created.
+	ArchiveRef(ref, archive string) error
+
+	// MergeRef merges the given ref into the current one.
+	//
+	// The ref argument is the ref to merge, and fastForward indicates that the
+	// current ref should only move forward, as opposed to creating a bubble merge.
+	// The messages argument(s) provide text that should be included in the default
+	// merge commit message (separated by blank lines).
+	MergeRef(ref string, fastForward bool, messages ...string) error
+
+	// MergeAndSignRef merges the given ref into the current one and signs the
+	// merge.
+	//
+	// The ref argument is the ref to merge, and fastForward indicates that the
+	// current ref should only move forward, as opposed to creating a bubble merge.
+	// The messages argument(s) provide text that should be included in the default
+	// merge commit message (separated by blank lines).
+	MergeAndSignRef(ref string, fastForward bool, messages ...string) error
+
+	// RebaseRef rebases the current ref onto the given one.
+	RebaseRef(ref string) error
+
+	// RebaseAndSignRef rebases the current ref onto the given one and signs
+	// the result.
+	RebaseAndSignRef(ref string) error
+
+	// ListCommits returns the list of commits reachable from the given ref.
+	//
+	// The generated list is in chronological order (with the oldest commit first).
+	//
+	// If the specified ref does not exist, then this method returns an empty result.
+	ListCommits(ref string) []string
+
+	// ListCommitsBetween returns the list of commits between the two given revisions.
+	//
+	// The "from" parameter is the starting point (exclusive), and the "to"
+	// parameter is the ending point (inclusive).
+	//
+	// The "from" commit does not need to be an ancestor of the "to" commit. If it
+	// is not, then the merge base of the two is used as the starting point.
+	// Admittedly, this makes calling these the "between" commits is a bit of a
+	// misnomer, but it also makes the method easier to use when you want to
+	// generate the list of changes in a feature branch, as it eliminates the need
+	// to explicitly calculate the merge base. This also makes the semantics of the
+	// method compatible with git's built-in "rev-list" command.
+	//
+	// The generated list is in chronological order (with the oldest commit first).
+	ListCommitsBetween(from, to string) ([]string, error)
+
+	// GetNotes reads the notes from the given ref that annotate the given revision.
+	GetNotes(notesRef, revision string) []Note
+
+	// GetAllNotes reads the contents of the notes under the given ref for every commit.
+	//
+	// The returned value is a mapping from commit hash to the list of notes for that commit.
+	//
+	// This is the batch version of the corresponding GetNotes(...) method.
+	GetAllNotes(notesRef string) (map[string][]Note, error)
+
+	// AppendNote appends a note to a revision under the given ref.
+	AppendNote(ref, revision string, note Note) error
+
+	// ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref.
+	ListNotedRevisions(notesRef string) []string
+
+	// PushNotes pushes git notes to a remote repo.
+	PushNotes(remote, notesRefPattern string) error
+
+	// PullNotes fetches the contents of the given notes ref from a remote repo,
+	// and then merges them with the corresponding local notes using the
+	// "cat_sort_uniq" strategy.
+	PullNotes(remote, notesRefPattern string) error
+
+	// PushNotesAndArchive pushes the given notes and archive refs to a remote repo.
+	PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error
+
+	// PullNotesAndArchive fetches the contents of the notes and archives refs from
+	// a remote repo, and merges them with the corresponding local refs.
+	//
+	// For notes refs, we assume that every note can be automatically merged using
+	// the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement),
+	// so we automatically merge the remote notes into the local notes.
+	//
+	// For "archive" refs, they are expected to be used solely for maintaining
+	// reachability of commits that are part of the history of any reviews,
+	// so we do not maintain any consistency with their tree objects. Instead,
+	// we merely ensure that their history graph includes every commit that we
+	// intend to keep.
+	PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error
+
+	// MergeNotes merges in the remote's state of the archives reference into
+	// the local repository's.
+	MergeNotes(remote, notesRefPattern string) error
+	// MergeArchives merges in the remote's state of the archives reference
+	// into the local repository's.
+	MergeArchives(remote, archiveRefPattern string) error
+
+	// FetchAndReturnNewReviewHashes fetches the notes "branches" and then
+	// susses out the IDs (the revision the review points to) of any new
+	// reviews, then returns that list of IDs.
+	//
+	// This is accomplished by determining which files in the notes tree have
+	// changed because the _names_ of these files correspond to the revisions
+	// they point to.
+	FetchAndReturnNewReviewHashes(remote, notesRefPattern, archiveRefPattern string) ([]string, error)
+}
diff --git a/third_party/go/git-appraise/review/analyses/analyses.go b/third_party/go/git-appraise/review/analyses/analyses.go
new file mode 100644
index 000000000000..4828f3b230c2
--- /dev/null
+++ b/third_party/go/git-appraise/review/analyses/analyses.go
@@ -0,0 +1,160 @@
+/*
+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 analyses defines the internal representation of static analysis reports.
+package analyses
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"sort"
+	"strconv"
+
+	"github.com/google/git-appraise/repository"
+)
+
+const (
+	// Ref defines the git-notes ref that we expect to contain analysis reports.
+	Ref = "refs/notes/devtools/analyses"
+
+	// StatusLooksGoodToMe is the status string representing that analyses reported no messages.
+	StatusLooksGoodToMe = "lgtm"
+	// StatusForYourInformation is the status string representing that analyses reported informational messages.
+	StatusForYourInformation = "fyi"
+	// StatusNeedsMoreWork is the status string representing that analyses reported error messages.
+	StatusNeedsMoreWork = "nmw"
+
+	// FormatVersion defines the latest version of the request format supported by the tool.
+	FormatVersion = 0
+)
+
+// Report represents a build/test status report generated by analyses tool.
+// Every field is optional.
+type Report struct {
+	Timestamp string `json:"timestamp,omitempty"`
+	URL       string `json:"url,omitempty"`
+	Status    string `json:"status,omitempty"`
+	// Version represents the version of the metadata format.
+	Version int `json:"v,omitempty"`
+}
+
+// LocationRange represents the location within a source file that an analysis message covers.
+type LocationRange struct {
+	StartLine   uint32 `json:"start_line,omitempty"`
+	StartColumn uint32 `json:"start_column,omitempty"`
+	EndLine     uint32 `json:"end_line,omitempty"`
+	EndColumn   uint32 `json:"end_column,omitempty"`
+}
+
+// Location represents the location within a source tree that an analysis message covers.
+type Location struct {
+	Path  string         `json:"path,omitempty"`
+	Range *LocationRange `json:"range,omitempty"`
+}
+
+// Note represents a single analysis message.
+type Note struct {
+	Location    *Location `json:"location,omitempty"`
+	Category    string    `json:"category,omitempty"`
+	Description string    `json:"description"`
+}
+
+// AnalyzeResponse represents the response from a static-analysis tool.
+type AnalyzeResponse struct {
+	Notes []Note `json:"note,omitempty"`
+}
+
+// ReportDetails represents an entire static analysis run (which might include multiple analysis tools).
+type ReportDetails struct {
+	AnalyzeResponse []AnalyzeResponse `json:"analyze_response,omitempty"`
+}
+
+// GetLintReportResult downloads the details of a lint report and returns the responses embedded in it.
+func (analysesReport Report) GetLintReportResult() ([]AnalyzeResponse, error) {
+	if analysesReport.URL == "" {
+		return nil, nil
+	}
+	res, err := http.Get(analysesReport.URL)
+	if err != nil {
+		return nil, err
+	}
+	analysesResults, err := ioutil.ReadAll(res.Body)
+	res.Body.Close()
+	if err != nil {
+		return nil, err
+	}
+	var details ReportDetails
+	err = json.Unmarshal([]byte(analysesResults), &details)
+	if err != nil {
+		return nil, err
+	}
+	return details.AnalyzeResponse, nil
+}
+
+// GetNotes downloads the details of an analyses report and returns the notes embedded in it.
+func (analysesReport Report) GetNotes() ([]Note, error) {
+	reportResults, err := analysesReport.GetLintReportResult()
+	if err != nil {
+		return nil, err
+	}
+	var reportNotes []Note
+	for _, reportResult := range reportResults {
+		reportNotes = append(reportNotes, reportResult.Notes...)
+	}
+	return reportNotes, nil
+}
+
+// Parse parses an analysis report from a git note.
+func Parse(note repository.Note) (Report, error) {
+	bytes := []byte(note)
+	var report Report
+	err := json.Unmarshal(bytes, &report)
+	return report, err
+}
+
+// GetLatestAnalysesReport takes a collection of analysis reports, and returns the one with the most recent timestamp.
+func GetLatestAnalysesReport(reports []Report) (*Report, error) {
+	timestampReportMap := make(map[int]*Report)
+	var timestamps []int
+
+	for _, report := range reports {
+		timestamp, err := strconv.Atoi(report.Timestamp)
+		if err != nil {
+			return nil, err
+		}
+		timestamps = append(timestamps, timestamp)
+		timestampReportMap[timestamp] = &report
+	}
+	if len(timestamps) == 0 {
+		return nil, nil
+	}
+	sort.Sort(sort.Reverse(sort.IntSlice(timestamps)))
+	return timestampReportMap[timestamps[0]], nil
+}
+
+// ParseAllValid takes collection of git notes and tries to parse a analyses report
+// from each one. Any notes that are not valid analyses reports get ignored.
+func ParseAllValid(notes []repository.Note) []Report {
+	var reports []Report
+	for _, note := range notes {
+		report, err := Parse(note)
+		if err == nil && report.Version == FormatVersion {
+			reports = append(reports, report)
+		}
+	}
+	return reports
+}
diff --git a/third_party/go/git-appraise/review/analyses/analyses_test.go b/third_party/go/git-appraise/review/analyses/analyses_test.go
new file mode 100644
index 000000000000..00a811ef6a40
--- /dev/null
+++ b/third_party/go/git-appraise/review/analyses/analyses_test.go
@@ -0,0 +1,77 @@
+/*
+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 analyses
+
+import (
+	"fmt"
+	"github.com/google/git-appraise/repository"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+const (
+	mockOldReport = `{"timestamp": "0", "url": "https://this-url-does-not-exist.test/analysis.json"}`
+	mockNewReport = `{"timestamp": "1", "url": "%s"}`
+	mockResults   = `{
+  "analyze_response": [{
+    "note": [{
+      "location": {
+        "path": "file.txt",
+        "range": {
+          "start_line": 5
+        }
+      },
+      "category": "test",
+      "description": "This is a test"
+    }]
+  }]
+}`
+)
+
+func mockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		t.Log(r)
+		fmt.Fprintln(w, mockResults)
+		w.WriteHeader(http.StatusOK)
+	}
+}
+
+func TestGetLatestResult(t *testing.T) {
+	mockServer := httptest.NewServer(http.HandlerFunc(mockHandler(t)))
+	defer mockServer.Close()
+
+	reports := ParseAllValid([]repository.Note{
+		repository.Note([]byte(mockOldReport)),
+		repository.Note([]byte(fmt.Sprintf(mockNewReport, mockServer.URL))),
+	})
+
+	report, err := GetLatestAnalysesReport(reports)
+	if err != nil {
+		t.Fatal("Unexpected error while parsing analysis reports", err)
+	}
+	if report == nil {
+		t.Fatal("Unexpected nil report")
+	}
+	reportResult, err := report.GetLintReportResult()
+	if err != nil {
+		t.Fatal("Unexpected error while reading the latest report's results", err)
+	}
+	if len(reportResult) != 1 {
+		t.Fatal("Unexpected report result", reportResult)
+	}
+}
diff --git a/third_party/go/git-appraise/review/ci/ci.go b/third_party/go/git-appraise/review/ci/ci.go
new file mode 100644
index 000000000000..b2cfd22743c2
--- /dev/null
+++ b/third_party/go/git-appraise/review/ci/ci.go
@@ -0,0 +1,95 @@
+/*
+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 ci defines the internal representation of a continuous integration reports.
+package ci
+
+import (
+	"encoding/json"
+	"github.com/google/git-appraise/repository"
+	"sort"
+	"strconv"
+)
+
+const (
+	// Ref defines the git-notes ref that we expect to contain CI reports.
+	Ref = "refs/notes/devtools/ci"
+
+	// StatusSuccess is the status string representing that a build and/or test passed.
+	StatusSuccess = "success"
+	// StatusFailure is the status string representing that a build and/or test failed.
+	StatusFailure = "failure"
+
+	// FormatVersion defines the latest version of the request format supported by the tool.
+	FormatVersion = 0
+)
+
+// Report represents a build/test status report generated by a continuous integration tool.
+//
+// Every field is optional.
+type Report struct {
+	Timestamp string `json:"timestamp,omitempty"`
+	URL       string `json:"url,omitempty"`
+	Status    string `json:"status,omitempty"`
+	Agent     string `json:"agent,omitempty"`
+	// Version represents the version of the metadata format.
+	Version int `json:"v,omitempty"`
+}
+
+// Parse parses a CI report from a git note.
+func Parse(note repository.Note) (Report, error) {
+	bytes := []byte(note)
+	var report Report
+	err := json.Unmarshal(bytes, &report)
+	return report, err
+}
+
+// GetLatestCIReport takes the collection of reports and returns the one with the most recent timestamp.
+func GetLatestCIReport(reports []Report) (*Report, error) {
+	timestampReportMap := make(map[int]*Report)
+	var timestamps []int
+
+	for _, report := range reports {
+		timestamp, err := strconv.Atoi(report.Timestamp)
+		if err != nil {
+			return nil, err
+		}
+		timestamps = append(timestamps, timestamp)
+		timestampReportMap[timestamp] = &report
+	}
+	if len(timestamps) == 0 {
+		return nil, nil
+	}
+	sort.Sort(sort.Reverse(sort.IntSlice(timestamps)))
+	return timestampReportMap[timestamps[0]], nil
+}
+
+// ParseAllValid takes collection of git notes and tries to parse a CI report
+// from each one. Any notes that are not valid CI reports get ignored, as we
+// expect the git notes to be a heterogenous list, with only some of them
+// being valid CI status reports.
+func ParseAllValid(notes []repository.Note) []Report {
+	var reports []Report
+	for _, note := range notes {
+		report, err := Parse(note)
+		if err == nil && report.Version == FormatVersion {
+			if report.Status == "" || report.Status == StatusSuccess || report.Status == StatusFailure {
+				reports = append(reports, report)
+			}
+		}
+	}
+	return reports
+}
diff --git a/third_party/go/git-appraise/review/ci/ci_test.go b/third_party/go/git-appraise/review/ci/ci_test.go
new file mode 100644
index 000000000000..c141f053d94d
--- /dev/null
+++ b/third_party/go/git-appraise/review/ci/ci_test.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 ci
+
+import (
+	"github.com/google/git-appraise/repository"
+	"testing"
+)
+
+const testCINote1 = `{
+	"Timestamp": "4",
+	"URL": "www.google.com",
+	"Status": "success"
+}`
+
+const testCINote2 = `{
+	"Timestamp": "16",
+	"URL": "www.google.com",
+	"Status": "failure"
+}`
+
+const testCINote3 = `{
+	"Timestamp": "30",
+	"URL": "www.google.com",
+	"Status": "something else"
+}`
+
+const testCINote4 = `{
+	"Timestamp": "28",
+	"URL": "www.google.com",
+	"Status": "success"
+}`
+
+const testCINote5 = `{
+	"Timestamp": "27",
+	"URL": "www.google.com",
+	"Status": "success"
+}`
+
+func TestCIReport(t *testing.T) {
+	latestReport, err := GetLatestCIReport(ParseAllValid([]repository.Note{
+		repository.Note(testCINote1),
+		repository.Note(testCINote2),
+	}))
+	if err != nil {
+		t.Fatal("Failed to properly fetch the latest report", err)
+	}
+	expected, err := Parse(repository.Note(testCINote2))
+	if err != nil {
+		t.Fatal("Failed to parse the expected report", err)
+	}
+	if *latestReport != expected {
+		t.Fatal("This is not the latest ", latestReport)
+	}
+	latestReport, err = GetLatestCIReport(ParseAllValid([]repository.Note{
+		repository.Note(testCINote1),
+		repository.Note(testCINote2),
+		repository.Note(testCINote3),
+		repository.Note(testCINote4),
+	}))
+	if err != nil {
+		t.Fatal("Failed to properly fetch the latest report", err)
+	}
+	expected, err = Parse(repository.Note(testCINote4))
+	if err != nil {
+		t.Fatal("Failed to parse the expected report", err)
+	}
+	if *latestReport != expected {
+		t.Fatal("This is not the latest ", latestReport)
+	}
+}
diff --git a/third_party/go/git-appraise/review/comment/comment.go b/third_party/go/git-appraise/review/comment/comment.go
new file mode 100644
index 000000000000..b1dea49c13e4
--- /dev/null
+++ b/third_party/go/git-appraise/review/comment/comment.go
@@ -0,0 +1,266 @@
+/*
+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 comment defines the internal representation of a review comment.
+package comment
+
+import (
+	"crypto/sha1"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review/gpg"
+)
+
+// Ref defines the git-notes ref that we expect to contain review comments.
+const Ref = "refs/notes/devtools/discuss"
+
+// FormatVersion defines the latest version of the comment format supported by the tool.
+const FormatVersion = 0
+
+// ErrInvalidRange inidcates an error during parsing of a user-defined file
+// range
+var ErrInvalidRange = errors.New("invalid file location range. The required form is StartLine[+StartColumn][:EndLine[+EndColumn]]. The first line in a file is considered to be line 1")
+
+// Range represents the range of text that is under discussion.
+type Range struct {
+	StartLine   uint32 `json:"startLine"`
+	StartColumn uint32 `json:"startColumn,omitempty"`
+	EndLine     uint32 `json:"endLine,omitempty"`
+	EndColumn   uint32 `json:"endColumn,omitempty"`
+}
+
+// Location represents the location of a comment within a commit.
+type Location struct {
+	Commit string `json:"commit,omitempty"`
+	// If the path is omitted, then the comment applies to the entire commit.
+	Path string `json:"path,omitempty"`
+	// If the range is omitted, then the location represents an entire file.
+	Range *Range `json:"range,omitempty"`
+}
+
+// Check verifies that this location is valid in the provided
+// repository.
+func (location *Location) Check(repo repository.Repo) error {
+	contents, err := repo.Show(location.Commit, location.Path)
+	if err != nil {
+		return err
+	}
+	lines := strings.Split(contents, "\n")
+	if location.Range.StartLine > uint32(len(lines)) {
+		return fmt.Errorf("Line number %d does not exist in file %q",
+			location.Range.StartLine,
+			location.Path)
+	}
+	if location.Range.StartColumn != 0 &&
+		location.Range.StartColumn > uint32(len(lines[location.Range.StartLine-1])) {
+		return fmt.Errorf("Line %d in %q is too short for column %d",
+			location.Range.StartLine,
+			location.Path,
+			location.Range.StartColumn)
+	}
+	if location.Range.EndLine != 0 &&
+		location.Range.EndLine > uint32(len(lines)) {
+		return fmt.Errorf("End line number %d does not exist in file %q",
+			location.Range.EndLine,
+			location.Path)
+	}
+	if location.Range.EndColumn != 0 &&
+		location.Range.EndColumn > uint32(len(lines[location.Range.EndLine-1])) {
+		return fmt.Errorf("End line %d in %q is too short for column %d",
+			location.Range.EndLine,
+			location.Path,
+			location.Range.EndColumn)
+	}
+	return nil
+}
+
+// Comment represents a review comment, and can occur in any of the following contexts:
+// 1. As a comment on an entire commit.
+// 2. As a comment about a specific file in a commit.
+// 3. As a comment about a specific line in a commit.
+// 4. As a response to another comment.
+type Comment struct {
+	// Timestamp and Author are optimizations that allows us to display comment threads
+	// without having to run git-blame over the notes object. This is done because
+	// git-blame will become more and more expensive as the number of code reviews grows.
+	Timestamp string `json:"timestamp,omitempty"`
+	Author    string `json:"author,omitempty"`
+	// If original is provided, then the comment is an updated version of another comment.
+	Original string `json:"original,omitempty"`
+	// If parent is provided, then the comment is a response to another comment.
+	Parent string `json:"parent,omitempty"`
+	// If location is provided, then the comment is specific to that given location.
+	Location    *Location `json:"location,omitempty"`
+	Description string    `json:"description,omitempty"`
+	// The resolved bit indicates that no further action is needed.
+	//
+	// When the parent of the comment is another comment, this means that comment
+	// has been addressed. Otherwise, the parent is the commit, and this means that the
+	// change has been accepted. If the resolved bit is unset, then the comment is only an FYI.
+	Resolved *bool `json:"resolved,omitempty"`
+	// Version represents the version of the metadata format.
+	Version int `json:"v,omitempty"`
+
+	gpg.Sig
+}
+
+// New returns a new comment with the given description message.
+//
+// The Timestamp and Author fields are automatically filled in with the current time and user.
+func New(author string, description string) Comment {
+	return Comment{
+		Timestamp:   strconv.FormatInt(time.Now().Unix(), 10),
+		Author:      author,
+		Description: description,
+	}
+}
+
+// Parse parses a review comment from a git note.
+func Parse(note repository.Note) (Comment, error) {
+	bytes := []byte(note)
+	var comment Comment
+	err := json.Unmarshal(bytes, &comment)
+	return comment, err
+}
+
+// ParseAllValid takes collection of git notes and tries to parse a review
+// comment from each one. Any notes that are not valid review comments get
+// ignored, as we expect the git notes to be a heterogenous list, with only
+// some of them being review comments.
+func ParseAllValid(notes []repository.Note) map[string]Comment {
+	comments := make(map[string]Comment)
+	for _, note := range notes {
+		comment, err := Parse(note)
+		if err == nil && comment.Version == FormatVersion {
+			hash, err := comment.Hash()
+			if err == nil {
+				comments[hash] = comment
+			}
+		}
+	}
+	return comments
+}
+
+func (comment Comment) serialize() ([]byte, error) {
+	if len(comment.Timestamp) < 10 {
+		// To make sure that timestamps from before 2001 appear in the correct
+		// alphabetical order, we reformat the timestamp to be at least 10 characters
+		// and zero-padded.
+		time, err := strconv.ParseInt(comment.Timestamp, 10, 64)
+		if err == nil {
+			comment.Timestamp = fmt.Sprintf("%010d", time)
+		}
+		// We ignore the other case, as the comment timestamp is not in a format
+		// we expected, so we should just leave it alone.
+	}
+	return json.Marshal(comment)
+}
+
+// Write writes a review comment as a JSON-formatted git note.
+func (comment Comment) Write() (repository.Note, error) {
+	bytes, err := comment.serialize()
+	return repository.Note(bytes), err
+}
+
+// Hash returns the SHA1 hash of a review comment.
+func (comment Comment) Hash() (string, error) {
+	bytes, err := comment.serialize()
+	return fmt.Sprintf("%x", sha1.Sum(bytes)), err
+}
+
+// Set implenents flag.Value for the Range type
+func (r *Range) Set(s string) error {
+	var err error
+	*r = Range{}
+
+	if s == "" {
+		return nil
+	}
+	startEndParts := strings.Split(s, ":")
+	if len(startEndParts) > 2 {
+		return ErrInvalidRange
+	}
+
+	r.StartLine, r.StartColumn, err = parseRangePart(startEndParts[0])
+	if err != nil {
+		return err
+	}
+	if len(startEndParts) == 1 {
+		return nil
+	}
+
+	r.EndLine, r.EndColumn, err = parseRangePart(startEndParts[1])
+	if err != nil {
+		return err
+	}
+
+	if r.StartLine > r.EndLine {
+		return errors.New("start line cannot be greater than end line in range")
+	}
+
+	return nil
+}
+
+func parseRangePart(s string) (uint32, uint32, error) {
+	parts := strings.Split(s, "+")
+	if len(parts) > 2 {
+		return 0, 0, ErrInvalidRange
+	}
+
+	line, err := strconv.ParseUint(parts[0], 10, 32)
+	if err != nil {
+		return 0, 0, ErrInvalidRange
+	}
+
+	if len(parts) == 1 {
+		return uint32(line), 0, nil
+	}
+
+	col, err := strconv.ParseUint(parts[1], 10, 32)
+	if err != nil {
+		return 0, 0, ErrInvalidRange
+	}
+
+	if line == 0 && col != 0 {
+		// line 0 represents the entire file
+		return 0, 0, ErrInvalidRange
+	}
+
+	return uint32(line), uint32(col), nil
+}
+
+func (r *Range) String() string {
+	out := ""
+	if r.StartLine != 0 {
+		out = fmt.Sprintf("%d", r.StartLine)
+	}
+	if r.StartColumn != 0 {
+		out = fmt.Sprintf("%s+%d", out, r.StartColumn)
+	}
+	if r.EndLine != 0 {
+		out = fmt.Sprintf("%s:%d", out, r.EndLine)
+	}
+	if r.EndColumn != 0 {
+		out = fmt.Sprintf("%s+%d", out, r.EndColumn)
+	}
+	return out
+}
diff --git a/third_party/go/git-appraise/review/gpg/signable.go b/third_party/go/git-appraise/review/gpg/signable.go
new file mode 100644
index 000000000000..776764c6fc10
--- /dev/null
+++ b/third_party/go/git-appraise/review/gpg/signable.go
@@ -0,0 +1,129 @@
+// Package gpg provides an interface and an abstraction with which to sign and
+// verify review requests and comments.
+package gpg
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+)
+
+const placeholder = "gpgsig"
+
+// Sig provides an abstraction around shelling out to GPG to sign the
+// content it's given.
+type Sig struct {
+	// Sig holds an object's content's signature.
+	Sig string `json:"signature,omitempty"`
+}
+
+// Signable is an interfaces which provides the pointer to the signable
+// object's stringified signature.
+//
+// This pointer is used by `Sign` and `Verify` to replace its contents with
+// `placeholder` or the signature itself for the purposes of signing or
+// verifying.
+type Signable interface {
+	Signature() *string
+}
+
+// Signature is `Sig`'s implementation of `Signable`. Through this function, an
+// object which needs to implement `Signable` need only embed `Sig`
+// anonymously. See, e.g., review/request.go.
+func (s *Sig) Signature() *string {
+	return &s.Sig
+}
+
+// Sign uses gpg to sign the contents of a request and deposit it into the
+// signature key of the request.
+func Sign(key string, s Signable) error {
+	// First we retrieve the pointer and write `placeholder` as its value.
+	sigPtr := s.Signature()
+	*sigPtr = placeholder
+
+	// Marshal the content and sign it.
+	content, err := json.Marshal(s)
+	if err != nil {
+		return err
+	}
+	sig, err := signContent(key, content)
+	if err != nil {
+		return err
+	}
+
+	// Write the signature as the new value at the pointer.
+	*sigPtr = sig.String()
+	return nil
+}
+
+func signContent(key string, content []byte) (*bytes.Buffer,
+	error) {
+	var stdout, stderr bytes.Buffer
+	cmd := exec.Command("gpg", "-u", key, "--detach-sign", "--armor")
+	cmd.Stdin = bytes.NewReader(content)
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err := cmd.Run()
+	return &stdout, err
+}
+
+// Verify verifies the signatures on the request and its comments with the
+// given key.
+func Verify(s Signable) error {
+	// Retrieve the pointer.
+	sigPtr := s.Signature()
+	// Copy its contents.
+	sig := *sigPtr
+	// Overwrite the value with the placeholder.
+	*sigPtr = placeholder
+
+	defer func() { *sigPtr = sig }()
+
+	// 1. Marshal the content into JSON.
+	// 2. Write the signature and the content to temp files.
+	// 3. Use gpg to verify the signature.
+	content, err := json.Marshal(s)
+	if err != nil {
+		return err
+	}
+	sigFile, err := ioutil.TempFile("", "sig")
+	if err != nil {
+		return err
+	}
+	defer os.Remove(sigFile.Name())
+	_, err = sigFile.Write([]byte(sig))
+	if err != nil {
+		return err
+	}
+	err = sigFile.Close()
+	if err != nil {
+		return err
+	}
+
+	contentFile, err := ioutil.TempFile("", "content")
+	if err != nil {
+		return err
+	}
+	defer os.Remove(contentFile.Name())
+	_, err = contentFile.Write(content)
+	if err != nil {
+		return err
+	}
+	err = contentFile.Close()
+	if err != nil {
+		return err
+	}
+
+	var stdout, stderr bytes.Buffer
+	cmd := exec.Command("gpg", "--verify", sigFile.Name(), contentFile.Name())
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err = cmd.Run()
+	if err != nil {
+		return fmt.Errorf("%s", stderr.String())
+	}
+	return nil
+}
diff --git a/third_party/go/git-appraise/review/request/request.go b/third_party/go/git-appraise/review/request/request.go
new file mode 100644
index 000000000000..c23fd427a8ee
--- /dev/null
+++ b/third_party/go/git-appraise/review/request/request.go
@@ -0,0 +1,104 @@
+/*
+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 request defines the internal representation of a review request.
+package request
+
+import (
+	"encoding/json"
+	"strconv"
+	"time"
+
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review/gpg"
+)
+
+// Ref defines the git-notes ref that we expect to contain review requests.
+const Ref = "refs/notes/devtools/reviews"
+
+// FormatVersion defines the latest version of the request format supported by the tool.
+const FormatVersion = 0
+
+// Request represents an initial request for a code review.
+//
+// Every field is optional.
+type Request struct {
+	// Timestamp and Requester are optimizations that allows us to display reviews
+	// without having to run git-blame over the notes object. This is done because
+	// git-blame will become more and more expensive as the number of reviews grows.
+	Timestamp   string   `json:"timestamp,omitempty"`
+	ReviewRef   string   `json:"reviewRef,omitempty"`
+	TargetRef   string   `json:"targetRef"`
+	Requester   string   `json:"requester,omitempty"`
+	Reviewers   []string `json:"reviewers,omitempty"`
+	Description string   `json:"description,omitempty"`
+	// Version represents the version of the metadata format.
+	Version int `json:"v,omitempty"`
+	// BaseCommit stores the commit ID of the target ref at the time the review was requested.
+	// This is optional, and only used for submitted reviews which were anchored at a merge commit.
+	// This allows someone viewing that submitted review to find the diff against which the
+	// code was reviewed.
+	BaseCommit string `json:"baseCommit,omitempty"`
+	// Alias stores a post-rebase commit ID for the review. This allows the tool
+	// to track the history of a review even if the commit history changes.
+	Alias string `json:"alias,omitempty"`
+
+	gpg.Sig
+}
+
+// New returns a new request.
+//
+// The Timestamp and Requester fields are automatically filled in with the current time and user.
+func New(requester string, reviewers []string, reviewRef, targetRef, description string) Request {
+	return Request{
+		Timestamp:   strconv.FormatInt(time.Now().Unix(), 10),
+		Requester:   requester,
+		Reviewers:   reviewers,
+		ReviewRef:   reviewRef,
+		TargetRef:   targetRef,
+		Description: description,
+	}
+}
+
+// Parse parses a review request from a git note.
+func Parse(note repository.Note) (Request, error) {
+	bytes := []byte(note)
+	var request Request
+	err := json.Unmarshal(bytes, &request)
+	// TODO(ojarjur): If "requester" is not set, then use git-blame to fill it in.
+	return request, err
+}
+
+// ParseAllValid takes collection of git notes and tries to parse a review
+// request from each one. Any notes that are not valid review requests get
+// ignored, as we expect the git notes to be a heterogenous list, with only
+// some of them being review requests.
+func ParseAllValid(notes []repository.Note) []Request {
+	var requests []Request
+	for _, note := range notes {
+		request, err := Parse(note)
+		if err == nil && request.Version == FormatVersion {
+			requests = append(requests, request)
+		}
+	}
+	return requests
+}
+
+// Write writes a review request as a JSON-formatted git note.
+func (request *Request) Write() (repository.Note, error) {
+	bytes, err := json.Marshal(request)
+	return repository.Note(bytes), err
+}
diff --git a/third_party/go/git-appraise/review/review.go b/third_party/go/git-appraise/review/review.go
new file mode 100644
index 000000000000..a23dd17bf798
--- /dev/null
+++ b/third_party/go/git-appraise/review/review.go
@@ -0,0 +1,772 @@
+/*
+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 review contains the data structures used to represent code reviews.
+package review
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"sort"
+
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review/analyses"
+	"github.com/google/git-appraise/review/ci"
+	"github.com/google/git-appraise/review/comment"
+	"github.com/google/git-appraise/review/gpg"
+	"github.com/google/git-appraise/review/request"
+)
+
+const archiveRef = "refs/devtools/archives/reviews"
+
+// CommentThread represents the tree-based hierarchy of comments.
+//
+// The Resolved field represents the aggregate status of the entire thread. If
+// it is set to false, then it indicates that there is an unaddressed comment
+// in the thread. If it is unset, then that means that the root comment is an
+// FYI only, and that there are no unaddressed comments. If it is set to true,
+// then that means that there are no unaddressed comments, and that the root
+// comment has its resolved bit set to true.
+type CommentThread struct {
+	Hash     string             `json:"hash,omitempty"`
+	Comment  comment.Comment    `json:"comment"`
+	Original *comment.Comment   `json:"original,omitempty"`
+	Edits    []*comment.Comment `json:"edits,omitempty"`
+	Children []CommentThread    `json:"children,omitempty"`
+	Resolved *bool              `json:"resolved,omitempty"`
+	Edited   bool               `json:"edited,omitempty"`
+}
+
+// Summary represents the high-level state of a code review.
+//
+// This high-level state corresponds to the data that can be quickly read
+// directly from the repo, so other methods that need to operate on a lot
+// of reviews (such as listing the open reviews) should prefer operating on
+// the summary rather than the details.
+//
+// Review summaries have two status fields which are orthogonal:
+// 1. Resolved indicates if a reviewer has accepted or rejected the change.
+// 2. Submitted indicates if the change has been incorporated into the target.
+type Summary struct {
+	Repo        repository.Repo   `json:"-"`
+	Revision    string            `json:"revision"`
+	Request     request.Request   `json:"request"`
+	AllRequests []request.Request `json:"-"`
+	Comments    []CommentThread   `json:"comments,omitempty"`
+	Resolved    *bool             `json:"resolved,omitempty"`
+	Submitted   bool              `json:"submitted"`
+}
+
+// Review represents the entire state of a code review.
+//
+// This extends Summary to also include a list of reports for both the
+// continuous integration status, and the static analysis runs. Those reports
+// correspond to either the current commit in the review ref (for pending
+// reviews), or to the last commented-upon commit (for submitted reviews).
+type Review struct {
+	*Summary
+	Reports  []ci.Report       `json:"reports,omitempty"`
+	Analyses []analyses.Report `json:"analyses,omitempty"`
+}
+
+type commentsByTimestamp []*comment.Comment
+
+// Interface methods for sorting comment threads by timestamp
+func (cs commentsByTimestamp) Len() int      { return len(cs) }
+func (cs commentsByTimestamp) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] }
+func (cs commentsByTimestamp) Less(i, j int) bool {
+	return cs[i].Timestamp < cs[j].Timestamp
+}
+
+type byTimestamp []CommentThread
+
+// Interface methods for sorting comment threads by timestamp
+func (threads byTimestamp) Len() int      { return len(threads) }
+func (threads byTimestamp) Swap(i, j int) { threads[i], threads[j] = threads[j], threads[i] }
+func (threads byTimestamp) Less(i, j int) bool {
+	return threads[i].Comment.Timestamp < threads[j].Comment.Timestamp
+}
+
+type requestsByTimestamp []request.Request
+
+// Interface methods for sorting review requests by timestamp
+func (requests requestsByTimestamp) Len() int { return len(requests) }
+func (requests requestsByTimestamp) Swap(i, j int) {
+	requests[i], requests[j] = requests[j], requests[i]
+}
+func (requests requestsByTimestamp) Less(i, j int) bool {
+	return requests[i].Timestamp < requests[j].Timestamp
+}
+
+type summariesWithNewestRequestsFirst []Summary
+
+// Interface methods for sorting review summaries in reverse chronological order
+func (summaries summariesWithNewestRequestsFirst) Len() int { return len(summaries) }
+func (summaries summariesWithNewestRequestsFirst) Swap(i, j int) {
+	summaries[i], summaries[j] = summaries[j], summaries[i]
+}
+func (summaries summariesWithNewestRequestsFirst) Less(i, j int) bool {
+	return summaries[i].Request.Timestamp > summaries[j].Request.Timestamp
+}
+
+// updateThreadsStatus calculates the aggregate status of a sequence of comment threads.
+//
+// The aggregate status is the conjunction of all of the non-nil child statuses.
+//
+// This has the side-effect of setting the "Resolved" field of all descendant comment threads.
+func updateThreadsStatus(threads []CommentThread) *bool {
+	sort.Stable(byTimestamp(threads))
+	noUnresolved := true
+	var result *bool
+	for i := range threads {
+		thread := &threads[i]
+		thread.updateResolvedStatus()
+		if thread.Resolved != nil {
+			noUnresolved = noUnresolved && *thread.Resolved
+			result = &noUnresolved
+		}
+	}
+	return result
+}
+
+// updateResolvedStatus calculates the aggregate status of a single comment thread,
+// and updates the "Resolved" field of that thread accordingly.
+func (thread *CommentThread) updateResolvedStatus() {
+	resolved := updateThreadsStatus(thread.Children)
+	if resolved == nil {
+		thread.Resolved = thread.Comment.Resolved
+		return
+	}
+
+	if !*resolved {
+		thread.Resolved = resolved
+		return
+	}
+
+	if thread.Comment.Resolved == nil || !*thread.Comment.Resolved {
+		thread.Resolved = nil
+		return
+	}
+
+	thread.Resolved = resolved
+}
+
+// Verify verifies the signature on a comment.
+func (thread *CommentThread) Verify() error {
+	err := gpg.Verify(&thread.Comment)
+	if err != nil {
+		hash, _ := thread.Comment.Hash()
+		return fmt.Errorf("verification of comment [%s] failed: %s", hash, err)
+	}
+	for _, child := range thread.Children {
+		err = child.Verify()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// mutableThread is an internal-only data structure used to store partially constructed comment threads.
+type mutableThread struct {
+	Hash     string
+	Comment  comment.Comment
+	Edits    []*comment.Comment
+	Children []*mutableThread
+}
+
+// fixMutableThread is a helper method to finalize a mutableThread struct
+// (partially constructed comment thread) as a CommentThread struct
+// (fully constructed comment thread).
+func fixMutableThread(mutableThread *mutableThread) CommentThread {
+	var children []CommentThread
+	edited := len(mutableThread.Edits) > 0
+	for _, mutableChild := range mutableThread.Children {
+		child := fixMutableThread(mutableChild)
+		if (!edited) && child.Edited {
+			edited = true
+		}
+		children = append(children, child)
+	}
+	comment := &mutableThread.Comment
+	if len(mutableThread.Edits) > 0 {
+		sort.Stable(commentsByTimestamp(mutableThread.Edits))
+		comment = mutableThread.Edits[len(mutableThread.Edits)-1]
+	}
+
+	return CommentThread{
+		Hash:     mutableThread.Hash,
+		Comment:  *comment,
+		Original: &mutableThread.Comment,
+		Edits:    mutableThread.Edits,
+		Children: children,
+		Edited:   edited,
+	}
+}
+
+// This function builds the comment thread tree from the log-based list of comments.
+//
+// Since the comments can be processed in any order, this uses an internal mutable
+// data structure, and then converts it to the proper CommentThread structure at the end.
+func buildCommentThreads(commentsByHash map[string]comment.Comment) []CommentThread {
+	threadsByHash := make(map[string]*mutableThread)
+	for hash, comment := range commentsByHash {
+		thread, ok := threadsByHash[hash]
+		if !ok {
+			thread = &mutableThread{
+				Hash:    hash,
+				Comment: comment,
+			}
+			threadsByHash[hash] = thread
+		}
+	}
+	var rootHashes []string
+	for hash, thread := range threadsByHash {
+		if thread.Comment.Original != "" {
+			original, ok := threadsByHash[thread.Comment.Original]
+			if ok {
+				original.Edits = append(original.Edits, &thread.Comment)
+			}
+		} else if thread.Comment.Parent == "" {
+			rootHashes = append(rootHashes, hash)
+		} else {
+			parent, ok := threadsByHash[thread.Comment.Parent]
+			if ok {
+				parent.Children = append(parent.Children, thread)
+			}
+		}
+	}
+	var threads []CommentThread
+	for _, hash := range rootHashes {
+		threads = append(threads, fixMutableThread(threadsByHash[hash]))
+	}
+	return threads
+}
+
+// loadComments reads in the log-structured sequence of comments for a review,
+// and then builds the corresponding tree-structured comment threads.
+func (r *Summary) loadComments(commentNotes []repository.Note) []CommentThread {
+	commentsByHash := comment.ParseAllValid(commentNotes)
+	return buildCommentThreads(commentsByHash)
+}
+
+func getSummaryFromNotes(repo repository.Repo, revision string, requestNotes, commentNotes []repository.Note) (*Summary, error) {
+	requests := request.ParseAllValid(requestNotes)
+	if requests == nil {
+		return nil, fmt.Errorf("Could not find any review requests for %q", revision)
+	}
+	sort.Stable(requestsByTimestamp(requests))
+	reviewSummary := Summary{
+		Repo:        repo,
+		Revision:    revision,
+		Request:     requests[len(requests)-1],
+		AllRequests: requests,
+	}
+	reviewSummary.Comments = reviewSummary.loadComments(commentNotes)
+	reviewSummary.Resolved = updateThreadsStatus(reviewSummary.Comments)
+	return &reviewSummary, nil
+}
+
+// GetSummary returns the summary of the code review specified by its revision
+// and the references which contain that reviews summary and comments.
+//
+// If no review request exists, the returned review summary is nil.
+func GetSummaryViaRefs(repo repository.Repo, requestRef, commentRef,
+	revision string) (*Summary, error) {
+
+	if err := repo.VerifyCommit(revision); err != nil {
+		return nil, fmt.Errorf("Could not find a commit named %q", revision)
+	}
+	requestNotes := repo.GetNotes(requestRef, revision)
+	commentNotes := repo.GetNotes(commentRef, revision)
+	summary, err := getSummaryFromNotes(repo, revision, requestNotes, commentNotes)
+	if err != nil {
+		return nil, err
+	}
+	currentCommit := revision
+	if summary.Request.Alias != "" {
+		currentCommit = summary.Request.Alias
+	}
+
+	if !summary.IsAbandoned() {
+		submitted, err := repo.IsAncestor(currentCommit, summary.Request.TargetRef)
+		if err != nil {
+			return nil, err
+		}
+		summary.Submitted = submitted
+	}
+	return summary, nil
+}
+
+// GetSummary returns the summary of the specified code review.
+//
+// If no review request exists, the returned review summary is nil.
+func GetSummary(repo repository.Repo, revision string) (*Summary, error) {
+	return GetSummaryViaRefs(repo, request.Ref, comment.Ref, revision)
+}
+
+// Details returns the detailed review for the given summary.
+func (r *Summary) Details() (*Review, error) {
+	review := Review{
+		Summary: r,
+	}
+	currentCommit, err := review.GetHeadCommit()
+	if err == nil {
+		review.Reports = ci.ParseAllValid(review.Repo.GetNotes(ci.Ref, currentCommit))
+		review.Analyses = analyses.ParseAllValid(review.Repo.GetNotes(analyses.Ref, currentCommit))
+	}
+	return &review, nil
+}
+
+// IsAbandoned returns whether or not the given review has been abandoned.
+func (r *Summary) IsAbandoned() bool {
+	return r.Request.TargetRef == ""
+}
+
+// IsOpen returns whether or not the given review is still open (neither submitted nor abandoned).
+func (r *Summary) IsOpen() bool {
+	return !r.Submitted && !r.IsAbandoned()
+}
+
+// Verify returns whether or not a summary's comments are a) signed, and b)
+/// that those signatures are verifiable.
+func (r *Summary) Verify() error {
+	err := gpg.Verify(&r.Request)
+	if err != nil {
+		return fmt.Errorf("couldn't verify request targeting: %q: %s",
+			r.Request.TargetRef, err)
+	}
+	for _, thread := range r.Comments {
+		err := thread.Verify()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Get returns the specified code review.
+//
+// If no review request exists, the returned review is nil.
+func Get(repo repository.Repo, revision string) (*Review, error) {
+	summary, err := GetSummary(repo, revision)
+	if err != nil {
+		return nil, err
+	}
+	if summary == nil {
+		return nil, nil
+	}
+	return summary.Details()
+}
+
+func getIsSubmittedCheck(repo repository.Repo) func(ref, commit string) bool {
+	refCommitsMap := make(map[string]map[string]bool)
+
+	getRefCommitsMap := func(ref string) map[string]bool {
+		commitsMap, ok := refCommitsMap[ref]
+		if ok {
+			return commitsMap
+		}
+		commitsMap = make(map[string]bool)
+		for _, commit := range repo.ListCommits(ref) {
+			commitsMap[commit] = true
+		}
+		refCommitsMap[ref] = commitsMap
+		return commitsMap
+	}
+
+	return func(ref, commit string) bool {
+		return getRefCommitsMap(ref)[commit]
+	}
+}
+
+func unsortedListAll(repo repository.Repo) []Summary {
+	reviewNotesMap, err := repo.GetAllNotes(request.Ref)
+	if err != nil {
+		return nil
+	}
+	discussNotesMap, err := repo.GetAllNotes(comment.Ref)
+	if err != nil {
+		return nil
+	}
+
+	isSubmittedCheck := getIsSubmittedCheck(repo)
+	var reviews []Summary
+	for commit, notes := range reviewNotesMap {
+		summary, err := getSummaryFromNotes(repo, commit, notes, discussNotesMap[commit])
+		if err != nil {
+			continue
+		}
+		if !summary.IsAbandoned() {
+			summary.Submitted = isSubmittedCheck(summary.Request.TargetRef, summary.getStartingCommit())
+		}
+		reviews = append(reviews, *summary)
+	}
+	return reviews
+}
+
+// ListAll returns all reviews stored in the git-notes.
+func ListAll(repo repository.Repo) []Summary {
+	reviews := unsortedListAll(repo)
+	sort.Stable(summariesWithNewestRequestsFirst(reviews))
+	return reviews
+}
+
+// ListOpen returns all reviews that are not yet incorporated into their target refs.
+func ListOpen(repo repository.Repo) []Summary {
+	var openReviews []Summary
+	for _, review := range unsortedListAll(repo) {
+		if review.IsOpen() {
+			openReviews = append(openReviews, review)
+		}
+	}
+	sort.Stable(summariesWithNewestRequestsFirst(openReviews))
+	return openReviews
+}
+
+// GetCurrent returns the current, open code review.
+//
+// If there are multiple matching reviews, then an error is returned.
+func GetCurrent(repo repository.Repo) (*Review, error) {
+	reviewRef, err := repo.GetHeadRef()
+	if err != nil {
+		return nil, err
+	}
+	var matchingReviews []Summary
+	for _, review := range ListOpen(repo) {
+		if review.Request.ReviewRef == reviewRef {
+			matchingReviews = append(matchingReviews, review)
+		}
+	}
+	if matchingReviews == nil {
+		return nil, nil
+	}
+	if len(matchingReviews) != 1 {
+		return nil, fmt.Errorf("There are %d open reviews for the ref \"%s\"", len(matchingReviews), reviewRef)
+	}
+	return matchingReviews[0].Details()
+}
+
+// GetBuildStatusMessage returns a string of the current build-and-test status
+// of the review, or "unknown" if the build-and-test status cannot be determined.
+func (r *Review) GetBuildStatusMessage() string {
+	statusMessage := "unknown"
+	ciReport, err := ci.GetLatestCIReport(r.Reports)
+	if err != nil {
+		return fmt.Sprintf("unknown: %s", err)
+	}
+	if ciReport != nil {
+		statusMessage = fmt.Sprintf("%s (%q)", ciReport.Status, ciReport.URL)
+	}
+	return statusMessage
+}
+
+// GetAnalysesNotes returns all of the notes from the most recent static
+// analysis run recorded in the git notes.
+func (r *Review) GetAnalysesNotes() ([]analyses.Note, error) {
+	latestAnalyses, err := analyses.GetLatestAnalysesReport(r.Analyses)
+	if err != nil {
+		return nil, err
+	}
+	if latestAnalyses == nil {
+		return nil, fmt.Errorf("No analyses available")
+	}
+	return latestAnalyses.GetNotes()
+}
+
+// GetAnalysesMessage returns a string summarizing the results of the
+// most recent static analyses.
+func (r *Review) GetAnalysesMessage() string {
+	latestAnalyses, err := analyses.GetLatestAnalysesReport(r.Analyses)
+	if err != nil {
+		return err.Error()
+	}
+	if latestAnalyses == nil {
+		return "No analyses available"
+	}
+	status := latestAnalyses.Status
+	if status != "" && status != analyses.StatusNeedsMoreWork {
+		return status
+	}
+	analysesNotes, err := latestAnalyses.GetNotes()
+	if err != nil {
+		return err.Error()
+	}
+	if analysesNotes == nil {
+		return "passed"
+	}
+	return fmt.Sprintf("%d warnings\n", len(analysesNotes))
+	// TODO(ojarjur): Figure out the best place to display the actual notes
+}
+
+func prettyPrintJSON(jsonBytes []byte) (string, error) {
+	var prettyBytes bytes.Buffer
+	err := json.Indent(&prettyBytes, jsonBytes, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return prettyBytes.String(), nil
+}
+
+// GetJSON returns the pretty printed JSON for a review summary.
+func (r *Summary) GetJSON() (string, error) {
+	jsonBytes, err := json.Marshal(*r)
+	if err != nil {
+		return "", err
+	}
+	return prettyPrintJSON(jsonBytes)
+}
+
+// GetJSON returns the pretty printed JSON for a review.
+func (r *Review) GetJSON() (string, error) {
+	jsonBytes, err := json.Marshal(*r)
+	if err != nil {
+		return "", err
+	}
+	return prettyPrintJSON(jsonBytes)
+}
+
+// findLastCommit returns the later (newest) commit from the union of the provided commit
+// and all of the commits that are referenced in the given comment threads.
+func (r *Review) findLastCommit(startingCommit, latestCommit string, commentThreads []CommentThread) string {
+	isLater := func(commit string) bool {
+		if err := r.Repo.VerifyCommit(commit); err != nil {
+			return false
+		}
+		if t, e := r.Repo.IsAncestor(latestCommit, commit); e == nil && t {
+			return true
+		}
+		if t, e := r.Repo.IsAncestor(startingCommit, commit); e == nil && !t {
+			return false
+		}
+		if t, e := r.Repo.IsAncestor(commit, latestCommit); e == nil && t {
+			return false
+		}
+		ct, err := r.Repo.GetCommitTime(commit)
+		if err != nil {
+			return false
+		}
+		lt, err := r.Repo.GetCommitTime(latestCommit)
+		if err != nil {
+			return true
+		}
+		return ct > lt
+	}
+	updateLatest := func(commit string) {
+		if commit == "" {
+			return
+		}
+		if isLater(commit) {
+			latestCommit = commit
+		}
+	}
+	for _, commentThread := range commentThreads {
+		comment := commentThread.Comment
+		if comment.Location != nil {
+			updateLatest(comment.Location.Commit)
+		}
+		updateLatest(r.findLastCommit(startingCommit, latestCommit, commentThread.Children))
+	}
+	return latestCommit
+}
+
+func (r *Summary) getStartingCommit() string {
+	if r.Request.Alias != "" {
+		return r.Request.Alias
+	}
+	return r.Revision
+}
+
+// GetHeadCommit returns the latest commit in a review.
+func (r *Review) GetHeadCommit() (string, error) {
+	currentCommit := r.getStartingCommit()
+	if r.Request.ReviewRef == "" {
+		return currentCommit, nil
+	}
+
+	if r.Submitted {
+		// The review has already been submitted.
+		// Go through the list of comments and find the last commented upon commit.
+		return r.findLastCommit(currentCommit, currentCommit, r.Comments), nil
+	}
+
+	// It is possible that the review ref is no longer an ancestor of the starting
+	// commit (e.g. if a rebase left us in a detached head), in which case we have to
+	// find the head commit without using it.
+	useReviewRef, err := r.Repo.IsAncestor(currentCommit, r.Request.ReviewRef)
+	if err != nil {
+		return "", err
+	}
+	if useReviewRef {
+		return r.Repo.ResolveRefCommit(r.Request.ReviewRef)
+	}
+
+	return r.findLastCommit(currentCommit, currentCommit, r.Comments), nil
+}
+
+// GetBaseCommit returns the commit against which a review should be compared.
+func (r *Review) GetBaseCommit() (string, error) {
+	if !r.IsOpen() {
+		if r.Request.BaseCommit != "" {
+			return r.Request.BaseCommit, nil
+		}
+
+		// This means the review has been submitted, but did not specify a base commit.
+		// In this case, we have to treat the last parent commit as the base. This is
+		// usually what we want, since merging a target branch into a feature branch
+		// results in the previous commit to the feature branch being the first parent,
+		// and the latest commit to the target branch being the second parent.
+		return r.Repo.GetLastParent(r.Revision)
+	}
+
+	targetRefHead, err := r.Repo.ResolveRefCommit(r.Request.TargetRef)
+	if err != nil {
+		return "", err
+	}
+	leftHandSide := targetRefHead
+	rightHandSide := r.Revision
+	if r.Request.ReviewRef != "" {
+		if reviewRefHead, err := r.Repo.ResolveRefCommit(r.Request.ReviewRef); err == nil {
+			rightHandSide = reviewRefHead
+		}
+	}
+
+	return r.Repo.MergeBase(leftHandSide, rightHandSide)
+}
+
+// ListCommits lists the commits included in a review.
+func (r *Review) ListCommits() ([]string, error) {
+	baseCommit, err := r.GetBaseCommit()
+	if err != nil {
+		return nil, err
+	}
+	headCommit, err := r.GetHeadCommit()
+	if err != nil {
+		return nil, err
+	}
+	return r.Repo.ListCommitsBetween(baseCommit, headCommit)
+}
+
+// GetDiff returns the diff for a review.
+func (r *Review) GetDiff(diffArgs ...string) (string, error) {
+	var baseCommit, headCommit string
+	baseCommit, err := r.GetBaseCommit()
+	if err == nil {
+		headCommit, err = r.GetHeadCommit()
+	}
+	if err == nil {
+		return r.Repo.Diff(baseCommit, headCommit, diffArgs...)
+	}
+	return "", err
+}
+
+// AddComment adds the given comment to the review.
+func (r *Review) AddComment(c comment.Comment) error {
+	commentNote, err := c.Write()
+	if err != nil {
+		return err
+	}
+
+	r.Repo.AppendNote(comment.Ref, r.Revision, commentNote)
+	return nil
+}
+
+// Rebase performs an interactive rebase of the review onto its target ref.
+//
+// If the 'archivePrevious' argument is true, then the previous head of the
+// review will be added to the 'refs/devtools/archives/reviews' ref prior
+// to being rewritten. That ensures the review history is kept from being
+// garbage collected.
+func (r *Review) Rebase(archivePrevious bool) error {
+	if archivePrevious {
+		orig, err := r.GetHeadCommit()
+		if err != nil {
+			return err
+		}
+		if err := r.Repo.ArchiveRef(orig, archiveRef); err != nil {
+			return err
+		}
+	}
+	if err := r.Repo.SwitchToRef(r.Request.ReviewRef); err != nil {
+		return err
+	}
+
+	err := r.Repo.RebaseRef(r.Request.TargetRef)
+	if err != nil {
+		return err
+	}
+
+	alias, err := r.Repo.GetCommitHash("HEAD")
+	if err != nil {
+		return err
+	}
+	r.Request.Alias = alias
+	newNote, err := r.Request.Write()
+	if err != nil {
+		return err
+	}
+	return r.Repo.AppendNote(request.Ref, r.Revision, newNote)
+}
+
+// RebaseAndSign performs an interactive rebase of the review onto its
+// target ref. It signs the result of the rebase as well as (re)signs
+// the review request itself.
+//
+// If the 'archivePrevious' argument is true, then the previous head of the
+// review will be added to the 'refs/devtools/archives/reviews' ref prior
+// to being rewritten. That ensures the review history is kept from being
+// garbage collected.
+func (r *Review) RebaseAndSign(archivePrevious bool) error {
+	if archivePrevious {
+		orig, err := r.GetHeadCommit()
+		if err != nil {
+			return err
+		}
+		if err := r.Repo.ArchiveRef(orig, archiveRef); err != nil {
+			return err
+		}
+	}
+	if err := r.Repo.SwitchToRef(r.Request.ReviewRef); err != nil {
+		return err
+	}
+
+	err := r.Repo.RebaseAndSignRef(r.Request.TargetRef)
+	if err != nil {
+		return err
+	}
+
+	alias, err := r.Repo.GetCommitHash("HEAD")
+	if err != nil {
+		return err
+	}
+	r.Request.Alias = alias
+
+	key, err := r.Repo.GetUserSigningKey()
+	if err != nil {
+		return err
+	}
+	err = gpg.Sign(key, &r.Request)
+	if err != nil {
+		return err
+	}
+
+	newNote, err := r.Request.Write()
+	if err != nil {
+		return err
+	}
+	return r.Repo.AppendNote(request.Ref, r.Revision, newNote)
+}
diff --git a/third_party/go/git-appraise/review/review_test.go b/third_party/go/git-appraise/review/review_test.go
new file mode 100644
index 000000000000..af699afd9aeb
--- /dev/null
+++ b/third_party/go/git-appraise/review/review_test.go
@@ -0,0 +1,870 @@
+/*
+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 review
+
+import (
+	"github.com/google/git-appraise/repository"
+	"github.com/google/git-appraise/review/comment"
+	"github.com/google/git-appraise/review/request"
+	"sort"
+	"testing"
+)
+
+func TestCommentSorting(t *testing.T) {
+	sampleComments := []*comment.Comment{
+		&comment.Comment{
+			Timestamp:   "012400",
+			Description: "Fourth",
+		},
+		&comment.Comment{
+			Timestamp:   "012400",
+			Description: "Fifth",
+		},
+		&comment.Comment{
+			Timestamp:   "012346",
+			Description: "Second",
+		},
+		&comment.Comment{
+			Timestamp:   "012345",
+			Description: "First",
+		},
+		&comment.Comment{
+			Timestamp:   "012347",
+			Description: "Third",
+		},
+	}
+	sort.Stable(commentsByTimestamp(sampleComments))
+	descriptions := []string{}
+	for _, comment := range sampleComments {
+		descriptions = append(descriptions, comment.Description)
+	}
+	if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") {
+		t.Fatalf("Comment ordering failed. Got %v", sampleComments)
+	}
+}
+
+func TestThreadSorting(t *testing.T) {
+	sampleThreads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp:   "012400",
+				Description: "Fourth",
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp:   "012400",
+				Description: "Fifth",
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp:   "012346",
+				Description: "Second",
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp:   "012345",
+				Description: "First",
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp:   "012347",
+				Description: "Third",
+			},
+		},
+	}
+	sort.Stable(byTimestamp(sampleThreads))
+	descriptions := []string{}
+	for _, thread := range sampleThreads {
+		descriptions = append(descriptions, thread.Comment.Description)
+	}
+	if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") {
+		t.Fatalf("Comment thread ordering failed. Got %v", sampleThreads)
+	}
+}
+
+func TestRequestSorting(t *testing.T) {
+	sampleRequests := []request.Request{
+		request.Request{
+			Timestamp:   "012400",
+			Description: "Fourth",
+		},
+		request.Request{
+			Timestamp:   "012400",
+			Description: "Fifth",
+		},
+		request.Request{
+			Timestamp:   "012346",
+			Description: "Second",
+		},
+		request.Request{
+			Timestamp:   "012345",
+			Description: "First",
+		},
+		request.Request{
+			Timestamp:   "012347",
+			Description: "Third",
+		},
+	}
+	sort.Stable(requestsByTimestamp(sampleRequests))
+	descriptions := []string{}
+	for _, r := range sampleRequests {
+		descriptions = append(descriptions, r.Description)
+	}
+	if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") {
+		t.Fatalf("Review request ordering failed. Got %v", sampleRequests)
+	}
+}
+
+func validateUnresolved(t *testing.T, resolved *bool) {
+	if resolved != nil {
+		t.Fatalf("Expected resolved status to be unset, but instead it was %v", *resolved)
+	}
+}
+
+func validateAccepted(t *testing.T, resolved *bool) {
+	if resolved == nil {
+		t.Fatal("Expected resolved status to be true, but it was unset")
+	}
+	if !*resolved {
+		t.Fatal("Expected resolved status to be true, but it was false")
+	}
+}
+
+func validateRejected(t *testing.T, resolved *bool) {
+	if resolved == nil {
+		t.Fatal("Expected resolved status to be false, but it was unset")
+	}
+	if *resolved {
+		t.Fatal("Expected resolved status to be false, but it was true")
+	}
+}
+
+func (commentThread *CommentThread) validateUnresolved(t *testing.T) {
+	validateUnresolved(t, commentThread.Resolved)
+}
+
+func (commentThread *CommentThread) validateAccepted(t *testing.T) {
+	validateAccepted(t, commentThread.Resolved)
+}
+
+func (commentThread *CommentThread) validateRejected(t *testing.T) {
+	validateRejected(t, commentThread.Resolved)
+}
+
+func TestSimpleAcceptedThreadStatus(t *testing.T) {
+	resolved := true
+	simpleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &resolved,
+		},
+	}
+	simpleThread.updateResolvedStatus()
+	simpleThread.validateAccepted(t)
+}
+
+func TestSimpleRejectedThreadStatus(t *testing.T) {
+	resolved := false
+	simpleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &resolved,
+		},
+	}
+	simpleThread.updateResolvedStatus()
+	simpleThread.validateRejected(t)
+}
+
+func TestFYIThenAcceptedThreadStatus(t *testing.T) {
+	accepted := true
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: nil,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  &accepted,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateUnresolved(t)
+}
+
+func TestFYIThenFYIThreadStatus(t *testing.T) {
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: nil,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  nil,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateUnresolved(t)
+}
+
+func TestFYIThenRejectedThreadStatus(t *testing.T) {
+	rejected := false
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: nil,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  &rejected,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateRejected(t)
+}
+
+func TestAcceptedThenAcceptedThreadStatus(t *testing.T) {
+	accepted := true
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &accepted,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  &accepted,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateAccepted(t)
+}
+
+func TestAcceptedThenFYIThreadStatus(t *testing.T) {
+	accepted := true
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &accepted,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  nil,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateAccepted(t)
+}
+
+func TestAcceptedThenRejectedThreadStatus(t *testing.T) {
+	accepted := true
+	rejected := false
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &accepted,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  &rejected,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateRejected(t)
+}
+
+func TestRejectedThenAcceptedThreadStatus(t *testing.T) {
+	accepted := true
+	rejected := false
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &rejected,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  &accepted,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateUnresolved(t)
+}
+
+func TestRejectedThenFYIThreadStatus(t *testing.T) {
+	rejected := false
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &rejected,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  nil,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateRejected(t)
+}
+
+func TestRejectedThenRejectedThreadStatus(t *testing.T) {
+	rejected := false
+	sampleThread := CommentThread{
+		Comment: comment.Comment{
+			Resolved: &rejected,
+		},
+		Children: []CommentThread{
+			CommentThread{
+				Comment: comment.Comment{
+					Timestamp: "012345",
+					Resolved:  &rejected,
+				},
+			},
+		},
+	}
+	sampleThread.updateResolvedStatus()
+	sampleThread.validateRejected(t)
+}
+
+func TestRejectedThenAcceptedThreadsStatus(t *testing.T) {
+	accepted := true
+	rejected := false
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  &rejected,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  &accepted,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateRejected(t, status)
+}
+
+func TestRejectedThenFYIThreadsStatus(t *testing.T) {
+	rejected := false
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  &rejected,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  nil,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateRejected(t, status)
+}
+
+func TestRejectedThenRejectedThreadsStatus(t *testing.T) {
+	rejected := false
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  &rejected,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  &rejected,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateRejected(t, status)
+}
+
+func TestAcceptedThenAcceptedThreadsStatus(t *testing.T) {
+	accepted := true
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  &accepted,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  &accepted,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateAccepted(t, status)
+}
+
+func TestAcceptedThenFYIThreadsStatus(t *testing.T) {
+	accepted := true
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  &accepted,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  nil,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateAccepted(t, status)
+}
+
+func TestAcceptedThenRejectedThreadsStatus(t *testing.T) {
+	accepted := true
+	rejected := false
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  &accepted,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  &rejected,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateRejected(t, status)
+}
+
+func TestFYIThenAcceptedThreadsStatus(t *testing.T) {
+	accepted := true
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  nil,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  &accepted,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateAccepted(t, status)
+}
+
+func TestFYIThenFYIThreadsStatus(t *testing.T) {
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  nil,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  nil,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateUnresolved(t, status)
+}
+
+func TestFYIThenRejectedThreadsStatus(t *testing.T) {
+	rejected := false
+	threads := []CommentThread{
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012345",
+				Resolved:  nil,
+			},
+		},
+		CommentThread{
+			Comment: comment.Comment{
+				Timestamp: "012346",
+				Resolved:  &rejected,
+			},
+		},
+	}
+	status := updateThreadsStatus(threads)
+	validateRejected(t, status)
+}
+
+func TestBuildCommentThreads(t *testing.T) {
+	rejected := false
+	accepted := true
+	root := comment.Comment{
+		Timestamp:   "012345",
+		Resolved:    nil,
+		Description: "root",
+	}
+	rootHash, err := root.Hash()
+	if err != nil {
+		t.Fatal(err)
+	}
+	child := comment.Comment{
+		Timestamp:   "012346",
+		Resolved:    nil,
+		Parent:      rootHash,
+		Description: "child",
+	}
+	childHash, err := child.Hash()
+	updatedChild := comment.Comment{
+		Timestamp:   "012346",
+		Resolved:    &rejected,
+		Original:    childHash,
+		Description: "updated child",
+	}
+	updatedChildHash, err := updatedChild.Hash()
+	if err != nil {
+		t.Fatal(err)
+	}
+	leaf := comment.Comment{
+		Timestamp:   "012347",
+		Resolved:    &accepted,
+		Parent:      childHash,
+		Description: "leaf",
+	}
+	leafHash, err := leaf.Hash()
+	if err != nil {
+		t.Fatal(err)
+	}
+	commentsByHash := map[string]comment.Comment{
+		rootHash:         root,
+		childHash:        child,
+		updatedChildHash: updatedChild,
+		leafHash:         leaf,
+	}
+	threads := buildCommentThreads(commentsByHash)
+	if len(threads) != 1 {
+		t.Fatalf("Unexpected threads: %v", threads)
+	}
+	rootThread := threads[0]
+	if rootThread.Comment.Description != "root" {
+		t.Fatalf("Unexpected root thread: %v", rootThread)
+	}
+	if !rootThread.Edited {
+		t.Fatalf("Unexpected root thread edited status: %v", rootThread)
+	}
+	if len(rootThread.Children) != 1 {
+		t.Fatalf("Unexpected root children: %v", rootThread.Children)
+	}
+	rootChild := rootThread.Children[0]
+	if rootChild.Comment.Description != "updated child" {
+		t.Fatalf("Unexpected updated child: %v", rootChild)
+	}
+	if rootChild.Original.Description != "child" {
+		t.Fatalf("Unexpected original child: %v", rootChild)
+	}
+	if len(rootChild.Edits) != 1 {
+		t.Fatalf("Unexpected child history: %v", rootChild.Edits)
+	}
+	if len(rootChild.Children) != 1 {
+		t.Fatalf("Unexpected leaves: %v", rootChild.Children)
+	}
+	threadLeaf := rootChild.Children[0]
+	if threadLeaf.Comment.Description != "leaf" {
+		t.Fatalf("Unexpected leaf: %v", threadLeaf)
+	}
+	if len(threadLeaf.Children) != 0 {
+		t.Fatalf("Unexpected leaf children: %v", threadLeaf.Children)
+	}
+	if threadLeaf.Edited {
+		t.Fatalf("Unexpected leaf edited status: %v", threadLeaf)
+	}
+}
+
+func TestGetHeadCommit(t *testing.T) {
+	repo := repository.NewMockRepoForTest()
+
+	submittedSimpleReview, err := Get(repo, repository.TestCommitB)
+	if err != nil {
+		t.Fatal(err)
+	}
+	submittedSimpleReviewHead, err := submittedSimpleReview.GetHeadCommit()
+	if err != nil {
+		t.Fatal("Unable to compute the head commit for a known review of a simple commit: ", err)
+	}
+	if submittedSimpleReviewHead != repository.TestCommitB {
+		t.Fatal("Unexpected head commit computed for a known review of a simple commit.")
+	}
+
+	submittedModifiedReview, err := Get(repo, repository.TestCommitD)
+	if err != nil {
+		t.Fatal(err)
+	}
+	submittedModifiedReviewHead, err := submittedModifiedReview.GetHeadCommit()
+	if err != nil {
+		t.Fatal("Unable to compute the head commit for a known, multi-commit review: ", err)
+	}
+	if submittedModifiedReviewHead != repository.TestCommitE {
+		t.Fatal("Unexpected head commit for a known, multi-commit review.")
+	}
+
+	pendingReview, err := Get(repo, repository.TestCommitG)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pendingReviewHead, err := pendingReview.GetHeadCommit()
+	if err != nil {
+		t.Fatal("Unable to compute the head commit for a known review of a merge commit: ", err)
+	}
+	if pendingReviewHead != repository.TestCommitI {
+		t.Fatal("Unexpected head commit computed for a pending review.")
+	}
+}
+
+func TestGetBaseCommit(t *testing.T) {
+	repo := repository.NewMockRepoForTest()
+
+	submittedSimpleReview, err := Get(repo, repository.TestCommitB)
+	if err != nil {
+		t.Fatal(err)
+	}
+	submittedSimpleReviewBase, err := submittedSimpleReview.GetBaseCommit()
+	if err != nil {
+		t.Fatal("Unable to compute the base commit for a known review of a simple commit: ", err)
+	}
+	if submittedSimpleReviewBase != repository.TestCommitA {
+		t.Fatal("Unexpected base commit computed for a known review of a simple commit.")
+	}
+
+	submittedMergeReview, err := Get(repo, repository.TestCommitD)
+	if err != nil {
+		t.Fatal(err)
+	}
+	submittedMergeReviewBase, err := submittedMergeReview.GetBaseCommit()
+	if err != nil {
+		t.Fatal("Unable to compute the base commit for a known review of a merge commit: ", err)
+	}
+	if submittedMergeReviewBase != repository.TestCommitC {
+		t.Fatal("Unexpected base commit computed for a known review of a merge commit.")
+	}
+
+	pendingReview, err := Get(repo, repository.TestCommitG)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pendingReviewBase, err := pendingReview.GetBaseCommit()
+	if err != nil {
+		t.Fatal("Unable to compute the base commit for a known review of a merge commit: ", err)
+	}
+	if pendingReviewBase != repository.TestCommitF {
+		t.Fatal("Unexpected base commit computed for a pending review.")
+	}
+
+	abandonRequest := pendingReview.Request
+	abandonRequest.TargetRef = ""
+	abandonNote, err := abandonRequest.Write()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := repo.AppendNote(request.Ref, repository.TestCommitG, abandonNote); err != nil {
+		t.Fatal(err)
+	}
+	abandonedReview, err := Get(repo, repository.TestCommitG)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if abandonedReview.IsOpen() {
+		t.Fatal("Failed to update a review to be abandoned")
+	}
+	abandonedReviewBase, err := abandonedReview.GetBaseCommit()
+	if err != nil {
+		t.Fatal("Unable to compute the base commit for an abandoned review: ", err)
+	}
+	if abandonedReviewBase != repository.TestCommitE {
+		t.Fatal("Unexpected base commit computed for an abandoned review.")
+	}
+}
+
+func TestGetRequests(t *testing.T) {
+	repo := repository.NewMockRepoForTest()
+	pendingReview, err := Get(repo, repository.TestCommitG)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(pendingReview.AllRequests) != 3 || pendingReview.Request.Description != "Final description of G" {
+		t.Fatal("Unexpected requests for a pending review: ", pendingReview.AllRequests, pendingReview.Request)
+	}
+}
+
+func TestRebase(t *testing.T) {
+	repo := repository.NewMockRepoForTest()
+	pendingReview, err := Get(repo, repository.TestCommitG)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Rebase the review and then confirm that it has been updated correctly.
+	if err := pendingReview.Rebase(true); err != nil {
+		t.Fatal(err)
+	}
+	reviewJSON, err := pendingReview.GetJSON()
+	if err != nil {
+		t.Fatal(err)
+	}
+	headRef, err := repo.GetHeadRef()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if headRef != pendingReview.Request.ReviewRef {
+		t.Fatal("Failed to switch to the review ref during a rebase")
+	}
+	isAncestor, err := repo.IsAncestor(pendingReview.Revision, archiveRef)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !isAncestor {
+		t.Fatalf("Commit %q is not archived", pendingReview.Revision)
+	}
+	reviewCommit, err := repo.GetCommitHash(pendingReview.Request.ReviewRef)
+	if err != nil {
+		t.Fatal(err)
+	}
+	reviewAlias := pendingReview.Request.Alias
+	if reviewAlias == "" || reviewAlias == pendingReview.Revision || reviewCommit != reviewAlias {
+		t.Fatalf("Failed to set the review alias: %q", reviewJSON)
+	}
+
+	// Submit the review.
+	if err := repo.SwitchToRef(pendingReview.Request.TargetRef); err != nil {
+		t.Fatal(err)
+	}
+	if err := repo.MergeRef(pendingReview.Request.ReviewRef, true); err != nil {
+		t.Fatal(err)
+	}
+
+	// Reread the review and confirm that it has been submitted.
+	submittedReview, err := Get(repo, pendingReview.Revision)
+	if err != nil {
+		t.Fatal(err)
+	}
+	submittedReviewJSON, err := submittedReview.GetJSON()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !submittedReview.Submitted {
+		t.Fatalf("Failed to submit the review: %q", submittedReviewJSON)
+	}
+}
+
+func TestRebaseDetachedHead(t *testing.T) {
+	repo := repository.NewMockRepoForTest()
+	pendingReview, err := Get(repo, repository.TestCommitG)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Switch the review to having a review ref that is not a branch.
+	pendingReview.Request.ReviewRef = repository.TestAlternateReviewRef
+	newNote, err := pendingReview.Request.Write()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := repo.AppendNote(request.Ref, pendingReview.Revision, newNote); err != nil {
+		t.Fatal(err)
+	}
+	pendingReview, err = Get(repo, repository.TestCommitG)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Rebase the review and then confirm that it has been updated correctly.
+	if err := pendingReview.Rebase(true); err != nil {
+		t.Fatal(err)
+	}
+	headRef, err := repo.GetHeadRef()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if headRef != pendingReview.Request.Alias {
+		t.Fatal("Failed to switch to a detached head during a rebase")
+	}
+	isAncestor, err := repo.IsAncestor(pendingReview.Revision, archiveRef)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !isAncestor {
+		t.Fatalf("Commit %q is not archived", pendingReview.Revision)
+	}
+
+	// Submit the review.
+	if err := repo.SwitchToRef(pendingReview.Request.TargetRef); err != nil {
+		t.Fatal(err)
+	}
+	reviewHead, err := pendingReview.GetHeadCommit()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := repo.MergeRef(reviewHead, true); err != nil {
+		t.Fatal(err)
+	}
+
+	// Reread the review and confirm that it has been submitted.
+	submittedReview, err := Get(repo, pendingReview.Revision)
+	if err != nil {
+		t.Fatal(err)
+	}
+	submittedReviewJSON, err := submittedReview.GetJSON()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !submittedReview.Submitted {
+		t.Fatalf("Failed to submit the review: %q", submittedReviewJSON)
+	}
+}
diff --git a/third_party/go/git-appraise/schema/analysis.json b/third_party/go/git-appraise/schema/analysis.json
new file mode 100644
index 000000000000..cbecb5416bc9
--- /dev/null
+++ b/third_party/go/git-appraise/schema/analysis.json
@@ -0,0 +1,61 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+
+  "properties": {
+    "timestamp": {
+      "description": "the number of seconds since the Unix epoch",
+      "type": "string",
+      "minLength": 10,
+      "maxLength": 10,
+      "pattern": "[0-9]{10,10}"
+    },
+
+    "status": {
+      "description": "represents the overall status of all messages from the analysis results",
+      "oneOf": [{
+        "$ref": "#/definitions/lgtm"
+      }, {
+        "$ref": "#/definitions/fyi"
+      }, {
+        "$ref": "#/definitions/nmw"
+      }]
+    },
+
+    "url": {
+      "description": "a publicly readable file, which contains JSON formatted analysis results. Those results should conform to the JSON format of the ShipshapeResponse protocol buffer message defined https://github.com/google/shipshape/blob/master/shipshape/proto/shipshape_rpc.proto",
+      "type": "string"
+    },
+
+    "v": {
+      "type": "integer",
+      "enum": [0]
+    }
+  },
+
+  "required": [
+    "timestamp",
+    "url"
+  ],
+
+  "definitions": {
+    "lgtm": {
+      "title": "Looks Good To Me",
+      "description": "indicates the analysis produced no messages",
+      "type": "string",
+      "enum": ["lgtm"]
+    },
+    "fyi": {
+      "title": "For your information",
+      "description": "indicates the analysis produced some messages, but none of them indicate errors",
+      "type": "string",
+      "enum": ["fyi"]
+    },
+    "nmw": {
+      "title": "Needs more work",
+      "description": "indicates the analysis produced at least one message indicating an error",
+      "type": "string",
+      "enum": ["nmw"]
+    }
+  }
+}
diff --git a/third_party/go/git-appraise/schema/ci.json b/third_party/go/git-appraise/schema/ci.json
new file mode 100644
index 000000000000..7436408290ce
--- /dev/null
+++ b/third_party/go/git-appraise/schema/ci.json
@@ -0,0 +1,42 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+
+  "properties": {
+    "timestamp": {
+      "description": "the number of seconds since the Unix epoch",
+      "type": "string",
+      "minLength": 10,
+      "maxLength": 10,
+      "pattern": "[0-9]{10,10}"
+    },
+
+    "agent": {
+      "description": "a free-form string that identifies the build and test runner",
+      "type": "string"
+    },
+
+    "status": {
+      "description": "the final status of a build or test",
+      "type": "string",
+      "enum": [
+        "success",
+        "failure"
+      ]
+    },
+
+    "url": {
+      "type": "string"
+    },
+
+    "v": {
+      "type": "integer",
+      "enum": [0]
+    }
+  },
+
+  "required": [
+    "timestamp",
+    "agent"
+  ]
+}
diff --git a/third_party/go/git-appraise/schema/comment.json b/third_party/go/git-appraise/schema/comment.json
new file mode 100644
index 000000000000..a39b1a2e670b
--- /dev/null
+++ b/third_party/go/git-appraise/schema/comment.json
@@ -0,0 +1,75 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+
+  "properties": {
+    "timestamp": {
+      "description": "the number of seconds since the Unix epoch",
+      "type": "string",
+      "minLength": 10,
+      "maxLength": 10,
+      "pattern": "[0-9]{10,10}"
+    },
+
+    "author": {
+      "type": "string"
+    },
+
+    "original": {
+      "description": "the SHA1 hash of another comment on the same revision, and it means this comment is an updated version of that comment",
+      "type": "string"
+    },
+
+    "parent": {
+      "description": "the SHA1 hash of another comment on the same revision, and it means this comment is a reply to that comment",
+      "type": "string"
+    },
+
+    "location": {
+      "type": "object",
+      "properties": {
+        "commit": {
+          "type": "string"
+        },
+        "path": {
+          "type": "string"
+        },
+        "range": {
+          "type": "object",
+          "properties": {
+            "startLine": {
+              "type": "integer"
+            },
+            "startColumn": {
+              "type": "integer"
+            },
+            "endLine": {
+              "type": "integer"
+            },
+            "endColumn": {
+              "type": "integer"
+            }
+          }
+        }
+      }
+    },
+
+    "description": {
+      "type": "string"
+    },
+
+    "resolved": {
+      "type": "boolean"
+    },
+
+    "v": {
+      "type": "integer",
+      "enum": [0]
+    }
+  },
+
+  "required": [
+    "timestamp",
+    "author"
+  ]
+}
diff --git a/third_party/go/git-appraise/schema/request.json b/third_party/go/git-appraise/schema/request.json
new file mode 100644
index 000000000000..9ec022a16e9e
--- /dev/null
+++ b/third_party/go/git-appraise/schema/request.json
@@ -0,0 +1,58 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+
+  "properties": {
+    "timestamp": {
+      "description": "the number of seconds since the Unix epoch",
+      "type": "string",
+      "minLength": 10,
+      "maxLength": 10,
+      "pattern": "[0-9]{10,10}"
+    },
+
+    "requester": {
+      "type": "string"
+    },
+
+    "baseCommit": {
+      "type": "string"
+    },
+
+    "reviewRef": {
+      "description": "used to specify a git ref that tracks the current revision under review",
+      "type": "string"
+    },
+
+    "targetRef": {
+      "description": "used to specify the git ref that should be updated once the review is approved",
+      "type": "string"
+    },
+
+    "reviewers": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+
+    "description": {
+      "type": "string"
+    },
+
+    "v": {
+      "type": "integer",
+      "enum": [0]
+    },
+
+    "alias": {
+      "description": "used to specify a post-rebase commit hash for the review",
+      "type": "string"
+    }
+  },
+
+  "required": [
+    "timestamp",
+    "requester"
+  ]
+}