feat(third_party): Check in git-appraise
This commit is contained in:
parent
e03f063052
commit
fe642c30f0
38 changed files with 7300 additions and 0 deletions
2
third_party/go/git-appraise/.gitignore
vendored
Normal file
2
third_party/go/git-appraise/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*~
|
||||||
|
bin/
|
24
third_party/go/git-appraise/CONTRIBUTING.md
vendored
Normal file
24
third_party/go/git-appraise/CONTRIBUTING.md
vendored
Normal file
|
@ -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.
|
202
third_party/go/git-appraise/LICENSE
vendored
Normal file
202
third_party/go/git-appraise/LICENSE
vendored
Normal file
|
@ -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.
|
158
third_party/go/git-appraise/README.md
vendored
Normal file
158
third_party/go/git-appraise/README.md
vendored
Normal file
|
@ -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.
|
139
third_party/go/git-appraise/commands/abandon.go
vendored
Normal file
139
third_party/go/git-appraise/commands/abandon.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
109
third_party/go/git-appraise/commands/accept.go
vendored
Normal file
109
third_party/go/git-appraise/commands/accept.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
55
third_party/go/git-appraise/commands/commands.go
vendored
Normal file
55
third_party/go/git-appraise/commands/commands.go
vendored
Normal file
|
@ -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,
|
||||||
|
}
|
165
third_party/go/git-appraise/commands/comment.go
vendored
Normal file
165
third_party/go/git-appraise/commands/comment.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
118
third_party/go/git-appraise/commands/input/input.go
vendored
Normal file
118
third_party/go/git-appraise/commands/input/input.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
74
third_party/go/git-appraise/commands/list.go
vendored
Normal file
74
third_party/go/git-appraise/commands/list.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
216
third_party/go/git-appraise/commands/output/output.go
vendored
Normal file
216
third_party/go/git-appraise/commands/output/output.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
93
third_party/go/git-appraise/commands/pull.go
vendored
Normal file
93
third_party/go/git-appraise/commands/pull.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
49
third_party/go/git-appraise/commands/push.go
vendored
Normal file
49
third_party/go/git-appraise/commands/push.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
100
third_party/go/git-appraise/commands/rebase.go
vendored
Normal file
100
third_party/go/git-appraise/commands/rebase.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
119
third_party/go/git-appraise/commands/reject.go
vendored
Normal file
119
third_party/go/git-appraise/commands/reject.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
182
third_party/go/git-appraise/commands/request.go
vendored
Normal file
182
third_party/go/git-appraise/commands/request.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
36
third_party/go/git-appraise/commands/request_test.go
vendored
Normal file
36
third_party/go/git-appraise/commands/request_test.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
85
third_party/go/git-appraise/commands/show.go
vendored
Normal file
85
third_party/go/git-appraise/commands/show.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
157
third_party/go/git-appraise/commands/submit.go
vendored
Normal file
157
third_party/go/git-appraise/commands/submit.go
vendored
Normal file
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
404
third_party/go/git-appraise/docs/tutorial.md
vendored
Normal file
404
third_party/go/git-appraise/docs/tutorial.md
vendored
Normal file
|
@ -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
|
||||||
|
```
|
104
third_party/go/git-appraise/git-appraise/git-appraise.go
vendored
Normal file
104
third_party/go/git-appraise/git-appraise/git-appraise.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
987
third_party/go/git-appraise/repository/git.go
vendored
Normal file
987
third_party/go/git-appraise/repository/git.go
vendored
Normal file
|
@ -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, ¬esMapping{
|
||||||
|
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 ¬esOverview{
|
||||||
|
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
|
||||||
|
}
|
94
third_party/go/git-appraise/repository/git_test.go
vendored
Normal file
94
third_party/go/git-appraise/repository/git_test.go
vendored
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
613
third_party/go/git-appraise/repository/mock_repo.go
vendored
Normal file
613
third_party/go/git-appraise/repository/mock_repo.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
221
third_party/go/git-appraise/repository/repo.go
vendored
Normal file
221
third_party/go/git-appraise/repository/repo.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
160
third_party/go/git-appraise/review/analyses/analyses.go
vendored
Normal file
160
third_party/go/git-appraise/review/analyses/analyses.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
77
third_party/go/git-appraise/review/analyses/analyses_test.go
vendored
Normal file
77
third_party/go/git-appraise/review/analyses/analyses_test.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
95
third_party/go/git-appraise/review/ci/ci.go
vendored
Normal file
95
third_party/go/git-appraise/review/ci/ci.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
85
third_party/go/git-appraise/review/ci/ci_test.go
vendored
Normal file
85
third_party/go/git-appraise/review/ci/ci_test.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
266
third_party/go/git-appraise/review/comment/comment.go
vendored
Normal file
266
third_party/go/git-appraise/review/comment/comment.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
129
third_party/go/git-appraise/review/gpg/signable.go
vendored
Normal file
129
third_party/go/git-appraise/review/gpg/signable.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
104
third_party/go/git-appraise/review/request/request.go
vendored
Normal file
104
third_party/go/git-appraise/review/request/request.go
vendored
Normal file
|
@ -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
|
||||||
|
}
|
772
third_party/go/git-appraise/review/review.go
vendored
Normal file
772
third_party/go/git-appraise/review/review.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
870
third_party/go/git-appraise/review/review_test.go
vendored
Normal file
870
third_party/go/git-appraise/review/review_test.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
61
third_party/go/git-appraise/schema/analysis.json
vendored
Normal file
61
third_party/go/git-appraise/schema/analysis.json
vendored
Normal file
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
third_party/go/git-appraise/schema/ci.json
vendored
Normal file
42
third_party/go/git-appraise/schema/ci.json
vendored
Normal file
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
75
third_party/go/git-appraise/schema/comment.json
vendored
Normal file
75
third_party/go/git-appraise/schema/comment.json
vendored
Normal file
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
58
third_party/go/git-appraise/schema/request.json
vendored
Normal file
58
third_party/go/git-appraise/schema/request.json
vendored
Normal file
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue