feat(third_party): Check in git-appraise

This commit is contained in:
Vincent Ambo 2019-07-02 14:19:12 +01:00
parent e03f063052
commit fe642c30f0
38 changed files with 7300 additions and 0 deletions

View 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)
},
}

View 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)
},
}

View 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,
}

View 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)
},
}

View 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
}

View 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)
},
}

View 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
}

View 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)
},
}

View 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)
},
}

View 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)
},
}

View 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)
},
}

View 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)
},
}

View 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)
}
}

View 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)
},
}

View 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)
},
}