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