772 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			772 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2015 Google Inc. All rights reserved.
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| // Package review contains the data structures used to represent code reviews.
 | |
| package review
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"sort"
 | |
| 
 | |
| 	"github.com/google/git-appraise/repository"
 | |
| 	"github.com/google/git-appraise/review/analyses"
 | |
| 	"github.com/google/git-appraise/review/ci"
 | |
| 	"github.com/google/git-appraise/review/comment"
 | |
| 	"github.com/google/git-appraise/review/gpg"
 | |
| 	"github.com/google/git-appraise/review/request"
 | |
| )
 | |
| 
 | |
| const archiveRef = "refs/devtools/archives/reviews"
 | |
| 
 | |
| // CommentThread represents the tree-based hierarchy of comments.
 | |
| //
 | |
| // The Resolved field represents the aggregate status of the entire thread. If
 | |
| // it is set to false, then it indicates that there is an unaddressed comment
 | |
| // in the thread. If it is unset, then that means that the root comment is an
 | |
| // FYI only, and that there are no unaddressed comments. If it is set to true,
 | |
| // then that means that there are no unaddressed comments, and that the root
 | |
| // comment has its resolved bit set to true.
 | |
| type CommentThread struct {
 | |
| 	Hash     string             `json:"hash,omitempty"`
 | |
| 	Comment  comment.Comment    `json:"comment"`
 | |
| 	Original *comment.Comment   `json:"original,omitempty"`
 | |
| 	Edits    []*comment.Comment `json:"edits,omitempty"`
 | |
| 	Children []CommentThread    `json:"children,omitempty"`
 | |
| 	Resolved *bool              `json:"resolved,omitempty"`
 | |
| 	Edited   bool               `json:"edited,omitempty"`
 | |
| }
 | |
| 
 | |
| // Summary represents the high-level state of a code review.
 | |
| //
 | |
| // This high-level state corresponds to the data that can be quickly read
 | |
| // directly from the repo, so other methods that need to operate on a lot
 | |
| // of reviews (such as listing the open reviews) should prefer operating on
 | |
| // the summary rather than the details.
 | |
| //
 | |
| // Review summaries have two status fields which are orthogonal:
 | |
| // 1. Resolved indicates if a reviewer has accepted or rejected the change.
 | |
| // 2. Submitted indicates if the change has been incorporated into the target.
 | |
| type Summary struct {
 | |
| 	Repo        repository.Repo   `json:"-"`
 | |
| 	Revision    string            `json:"revision"`
 | |
| 	Request     request.Request   `json:"request"`
 | |
| 	AllRequests []request.Request `json:"-"`
 | |
| 	Comments    []CommentThread   `json:"comments,omitempty"`
 | |
| 	Resolved    *bool             `json:"resolved,omitempty"`
 | |
| 	Submitted   bool              `json:"submitted"`
 | |
| }
 | |
| 
 | |
| // Review represents the entire state of a code review.
 | |
| //
 | |
| // This extends Summary to also include a list of reports for both the
 | |
| // continuous integration status, and the static analysis runs. Those reports
 | |
| // correspond to either the current commit in the review ref (for pending
 | |
| // reviews), or to the last commented-upon commit (for submitted reviews).
 | |
| type Review struct {
 | |
| 	*Summary
 | |
| 	Reports  []ci.Report       `json:"reports,omitempty"`
 | |
| 	Analyses []analyses.Report `json:"analyses,omitempty"`
 | |
| }
 | |
| 
 | |
| type commentsByTimestamp []*comment.Comment
 | |
| 
 | |
| // Interface methods for sorting comment threads by timestamp
 | |
| func (cs commentsByTimestamp) Len() int      { return len(cs) }
 | |
| func (cs commentsByTimestamp) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] }
 | |
| func (cs commentsByTimestamp) Less(i, j int) bool {
 | |
| 	return cs[i].Timestamp < cs[j].Timestamp
 | |
| }
 | |
| 
 | |
| type byTimestamp []CommentThread
 | |
| 
 | |
| // Interface methods for sorting comment threads by timestamp
 | |
| func (threads byTimestamp) Len() int      { return len(threads) }
 | |
| func (threads byTimestamp) Swap(i, j int) { threads[i], threads[j] = threads[j], threads[i] }
 | |
| func (threads byTimestamp) Less(i, j int) bool {
 | |
| 	return threads[i].Comment.Timestamp < threads[j].Comment.Timestamp
 | |
| }
 | |
| 
 | |
| type requestsByTimestamp []request.Request
 | |
| 
 | |
| // Interface methods for sorting review requests by timestamp
 | |
| func (requests requestsByTimestamp) Len() int { return len(requests) }
 | |
| func (requests requestsByTimestamp) Swap(i, j int) {
 | |
| 	requests[i], requests[j] = requests[j], requests[i]
 | |
| }
 | |
| func (requests requestsByTimestamp) Less(i, j int) bool {
 | |
| 	return requests[i].Timestamp < requests[j].Timestamp
 | |
| }
 | |
| 
 | |
| type summariesWithNewestRequestsFirst []Summary
 | |
| 
 | |
| // Interface methods for sorting review summaries in reverse chronological order
 | |
| func (summaries summariesWithNewestRequestsFirst) Len() int { return len(summaries) }
 | |
| func (summaries summariesWithNewestRequestsFirst) Swap(i, j int) {
 | |
| 	summaries[i], summaries[j] = summaries[j], summaries[i]
 | |
| }
 | |
| func (summaries summariesWithNewestRequestsFirst) Less(i, j int) bool {
 | |
| 	return summaries[i].Request.Timestamp > summaries[j].Request.Timestamp
 | |
| }
 | |
| 
 | |
| // updateThreadsStatus calculates the aggregate status of a sequence of comment threads.
 | |
| //
 | |
| // The aggregate status is the conjunction of all of the non-nil child statuses.
 | |
| //
 | |
| // This has the side-effect of setting the "Resolved" field of all descendant comment threads.
 | |
| func updateThreadsStatus(threads []CommentThread) *bool {
 | |
| 	sort.Stable(byTimestamp(threads))
 | |
| 	noUnresolved := true
 | |
| 	var result *bool
 | |
| 	for i := range threads {
 | |
| 		thread := &threads[i]
 | |
| 		thread.updateResolvedStatus()
 | |
| 		if thread.Resolved != nil {
 | |
| 			noUnresolved = noUnresolved && *thread.Resolved
 | |
| 			result = &noUnresolved
 | |
| 		}
 | |
| 	}
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // updateResolvedStatus calculates the aggregate status of a single comment thread,
 | |
| // and updates the "Resolved" field of that thread accordingly.
 | |
| func (thread *CommentThread) updateResolvedStatus() {
 | |
| 	resolved := updateThreadsStatus(thread.Children)
 | |
| 	if resolved == nil {
 | |
| 		thread.Resolved = thread.Comment.Resolved
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if !*resolved {
 | |
| 		thread.Resolved = resolved
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if thread.Comment.Resolved == nil || !*thread.Comment.Resolved {
 | |
| 		thread.Resolved = nil
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	thread.Resolved = resolved
 | |
| }
 | |
| 
 | |
| // Verify verifies the signature on a comment.
 | |
| func (thread *CommentThread) Verify() error {
 | |
| 	err := gpg.Verify(&thread.Comment)
 | |
| 	if err != nil {
 | |
| 		hash, _ := thread.Comment.Hash()
 | |
| 		return fmt.Errorf("verification of comment [%s] failed: %s", hash, err)
 | |
| 	}
 | |
| 	for _, child := range thread.Children {
 | |
| 		err = child.Verify()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // mutableThread is an internal-only data structure used to store partially constructed comment threads.
 | |
| type mutableThread struct {
 | |
| 	Hash     string
 | |
| 	Comment  comment.Comment
 | |
| 	Edits    []*comment.Comment
 | |
| 	Children []*mutableThread
 | |
| }
 | |
| 
 | |
| // fixMutableThread is a helper method to finalize a mutableThread struct
 | |
| // (partially constructed comment thread) as a CommentThread struct
 | |
| // (fully constructed comment thread).
 | |
| func fixMutableThread(mutableThread *mutableThread) CommentThread {
 | |
| 	var children []CommentThread
 | |
| 	edited := len(mutableThread.Edits) > 0
 | |
| 	for _, mutableChild := range mutableThread.Children {
 | |
| 		child := fixMutableThread(mutableChild)
 | |
| 		if (!edited) && child.Edited {
 | |
| 			edited = true
 | |
| 		}
 | |
| 		children = append(children, child)
 | |
| 	}
 | |
| 	comment := &mutableThread.Comment
 | |
| 	if len(mutableThread.Edits) > 0 {
 | |
| 		sort.Stable(commentsByTimestamp(mutableThread.Edits))
 | |
| 		comment = mutableThread.Edits[len(mutableThread.Edits)-1]
 | |
| 	}
 | |
| 
 | |
| 	return CommentThread{
 | |
| 		Hash:     mutableThread.Hash,
 | |
| 		Comment:  *comment,
 | |
| 		Original: &mutableThread.Comment,
 | |
| 		Edits:    mutableThread.Edits,
 | |
| 		Children: children,
 | |
| 		Edited:   edited,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // This function builds the comment thread tree from the log-based list of comments.
 | |
| //
 | |
| // Since the comments can be processed in any order, this uses an internal mutable
 | |
| // data structure, and then converts it to the proper CommentThread structure at the end.
 | |
| func buildCommentThreads(commentsByHash map[string]comment.Comment) []CommentThread {
 | |
| 	threadsByHash := make(map[string]*mutableThread)
 | |
| 	for hash, comment := range commentsByHash {
 | |
| 		thread, ok := threadsByHash[hash]
 | |
| 		if !ok {
 | |
| 			thread = &mutableThread{
 | |
| 				Hash:    hash,
 | |
| 				Comment: comment,
 | |
| 			}
 | |
| 			threadsByHash[hash] = thread
 | |
| 		}
 | |
| 	}
 | |
| 	var rootHashes []string
 | |
| 	for hash, thread := range threadsByHash {
 | |
| 		if thread.Comment.Original != "" {
 | |
| 			original, ok := threadsByHash[thread.Comment.Original]
 | |
| 			if ok {
 | |
| 				original.Edits = append(original.Edits, &thread.Comment)
 | |
| 			}
 | |
| 		} else if thread.Comment.Parent == "" {
 | |
| 			rootHashes = append(rootHashes, hash)
 | |
| 		} else {
 | |
| 			parent, ok := threadsByHash[thread.Comment.Parent]
 | |
| 			if ok {
 | |
| 				parent.Children = append(parent.Children, thread)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	var threads []CommentThread
 | |
| 	for _, hash := range rootHashes {
 | |
| 		threads = append(threads, fixMutableThread(threadsByHash[hash]))
 | |
| 	}
 | |
| 	return threads
 | |
| }
 | |
| 
 | |
| // loadComments reads in the log-structured sequence of comments for a review,
 | |
| // and then builds the corresponding tree-structured comment threads.
 | |
| func (r *Summary) loadComments(commentNotes []repository.Note) []CommentThread {
 | |
| 	commentsByHash := comment.ParseAllValid(commentNotes)
 | |
| 	return buildCommentThreads(commentsByHash)
 | |
| }
 | |
| 
 | |
| func getSummaryFromNotes(repo repository.Repo, revision string, requestNotes, commentNotes []repository.Note) (*Summary, error) {
 | |
| 	requests := request.ParseAllValid(requestNotes)
 | |
| 	if requests == nil {
 | |
| 		return nil, fmt.Errorf("Could not find any review requests for %q", revision)
 | |
| 	}
 | |
| 	sort.Stable(requestsByTimestamp(requests))
 | |
| 	reviewSummary := Summary{
 | |
| 		Repo:        repo,
 | |
| 		Revision:    revision,
 | |
| 		Request:     requests[len(requests)-1],
 | |
| 		AllRequests: requests,
 | |
| 	}
 | |
| 	reviewSummary.Comments = reviewSummary.loadComments(commentNotes)
 | |
| 	reviewSummary.Resolved = updateThreadsStatus(reviewSummary.Comments)
 | |
| 	return &reviewSummary, nil
 | |
| }
 | |
| 
 | |
| // GetSummary returns the summary of the code review specified by its revision
 | |
| // and the references which contain that reviews summary and comments.
 | |
| //
 | |
| // If no review request exists, the returned review summary is nil.
 | |
| func GetSummaryViaRefs(repo repository.Repo, requestRef, commentRef,
 | |
| 	revision string) (*Summary, error) {
 | |
| 
 | |
| 	if err := repo.VerifyCommit(revision); err != nil {
 | |
| 		return nil, fmt.Errorf("Could not find a commit named %q", revision)
 | |
| 	}
 | |
| 	requestNotes := repo.GetNotes(requestRef, revision)
 | |
| 	commentNotes := repo.GetNotes(commentRef, revision)
 | |
| 	summary, err := getSummaryFromNotes(repo, revision, requestNotes, commentNotes)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	currentCommit := revision
 | |
| 	if summary.Request.Alias != "" {
 | |
| 		currentCommit = summary.Request.Alias
 | |
| 	}
 | |
| 
 | |
| 	if !summary.IsAbandoned() {
 | |
| 		submitted, err := repo.IsAncestor(currentCommit, summary.Request.TargetRef)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		summary.Submitted = submitted
 | |
| 	}
 | |
| 	return summary, nil
 | |
| }
 | |
| 
 | |
| // GetSummary returns the summary of the specified code review.
 | |
| //
 | |
| // If no review request exists, the returned review summary is nil.
 | |
| func GetSummary(repo repository.Repo, revision string) (*Summary, error) {
 | |
| 	return GetSummaryViaRefs(repo, request.Ref, comment.Ref, revision)
 | |
| }
 | |
| 
 | |
| // Details returns the detailed review for the given summary.
 | |
| func (r *Summary) Details() (*Review, error) {
 | |
| 	review := Review{
 | |
| 		Summary: r,
 | |
| 	}
 | |
| 	currentCommit, err := review.GetHeadCommit()
 | |
| 	if err == nil {
 | |
| 		review.Reports = ci.ParseAllValid(review.Repo.GetNotes(ci.Ref, currentCommit))
 | |
| 		review.Analyses = analyses.ParseAllValid(review.Repo.GetNotes(analyses.Ref, currentCommit))
 | |
| 	}
 | |
| 	return &review, nil
 | |
| }
 | |
| 
 | |
| // IsAbandoned returns whether or not the given review has been abandoned.
 | |
| func (r *Summary) IsAbandoned() bool {
 | |
| 	return r.Request.TargetRef == ""
 | |
| }
 | |
| 
 | |
| // IsOpen returns whether or not the given review is still open (neither submitted nor abandoned).
 | |
| func (r *Summary) IsOpen() bool {
 | |
| 	return !r.Submitted && !r.IsAbandoned()
 | |
| }
 | |
| 
 | |
| // Verify returns whether or not a summary's comments are a) signed, and b)
 | |
| /// that those signatures are verifiable.
 | |
| func (r *Summary) Verify() error {
 | |
| 	err := gpg.Verify(&r.Request)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("couldn't verify request targeting: %q: %s",
 | |
| 			r.Request.TargetRef, err)
 | |
| 	}
 | |
| 	for _, thread := range r.Comments {
 | |
| 		err := thread.Verify()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Get returns the specified code review.
 | |
| //
 | |
| // If no review request exists, the returned review is nil.
 | |
| func Get(repo repository.Repo, revision string) (*Review, error) {
 | |
| 	summary, err := GetSummary(repo, revision)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if summary == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	return summary.Details()
 | |
| }
 | |
| 
 | |
| func getIsSubmittedCheck(repo repository.Repo) func(ref, commit string) bool {
 | |
| 	refCommitsMap := make(map[string]map[string]bool)
 | |
| 
 | |
| 	getRefCommitsMap := func(ref string) map[string]bool {
 | |
| 		commitsMap, ok := refCommitsMap[ref]
 | |
| 		if ok {
 | |
| 			return commitsMap
 | |
| 		}
 | |
| 		commitsMap = make(map[string]bool)
 | |
| 		for _, commit := range repo.ListCommits(ref) {
 | |
| 			commitsMap[commit] = true
 | |
| 		}
 | |
| 		refCommitsMap[ref] = commitsMap
 | |
| 		return commitsMap
 | |
| 	}
 | |
| 
 | |
| 	return func(ref, commit string) bool {
 | |
| 		return getRefCommitsMap(ref)[commit]
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func unsortedListAll(repo repository.Repo) []Summary {
 | |
| 	reviewNotesMap, err := repo.GetAllNotes(request.Ref)
 | |
| 	if err != nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 	discussNotesMap, err := repo.GetAllNotes(comment.Ref)
 | |
| 	if err != nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	isSubmittedCheck := getIsSubmittedCheck(repo)
 | |
| 	var reviews []Summary
 | |
| 	for commit, notes := range reviewNotesMap {
 | |
| 		summary, err := getSummaryFromNotes(repo, commit, notes, discussNotesMap[commit])
 | |
| 		if err != nil {
 | |
| 			continue
 | |
| 		}
 | |
| 		if !summary.IsAbandoned() {
 | |
| 			summary.Submitted = isSubmittedCheck(summary.Request.TargetRef, summary.getStartingCommit())
 | |
| 		}
 | |
| 		reviews = append(reviews, *summary)
 | |
| 	}
 | |
| 	return reviews
 | |
| }
 | |
| 
 | |
| // ListAll returns all reviews stored in the git-notes.
 | |
| func ListAll(repo repository.Repo) []Summary {
 | |
| 	reviews := unsortedListAll(repo)
 | |
| 	sort.Stable(summariesWithNewestRequestsFirst(reviews))
 | |
| 	return reviews
 | |
| }
 | |
| 
 | |
| // ListOpen returns all reviews that are not yet incorporated into their target refs.
 | |
| func ListOpen(repo repository.Repo) []Summary {
 | |
| 	var openReviews []Summary
 | |
| 	for _, review := range unsortedListAll(repo) {
 | |
| 		if review.IsOpen() {
 | |
| 			openReviews = append(openReviews, review)
 | |
| 		}
 | |
| 	}
 | |
| 	sort.Stable(summariesWithNewestRequestsFirst(openReviews))
 | |
| 	return openReviews
 | |
| }
 | |
| 
 | |
| // GetCurrent returns the current, open code review.
 | |
| //
 | |
| // If there are multiple matching reviews, then an error is returned.
 | |
| func GetCurrent(repo repository.Repo) (*Review, error) {
 | |
| 	reviewRef, err := repo.GetHeadRef()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	var matchingReviews []Summary
 | |
| 	for _, review := range ListOpen(repo) {
 | |
| 		if review.Request.ReviewRef == reviewRef {
 | |
| 			matchingReviews = append(matchingReviews, review)
 | |
| 		}
 | |
| 	}
 | |
| 	if matchingReviews == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	if len(matchingReviews) != 1 {
 | |
| 		return nil, fmt.Errorf("There are %d open reviews for the ref \"%s\"", len(matchingReviews), reviewRef)
 | |
| 	}
 | |
| 	return matchingReviews[0].Details()
 | |
| }
 | |
| 
 | |
| // GetBuildStatusMessage returns a string of the current build-and-test status
 | |
| // of the review, or "unknown" if the build-and-test status cannot be determined.
 | |
| func (r *Review) GetBuildStatusMessage() string {
 | |
| 	statusMessage := "unknown"
 | |
| 	ciReport, err := ci.GetLatestCIReport(r.Reports)
 | |
| 	if err != nil {
 | |
| 		return fmt.Sprintf("unknown: %s", err)
 | |
| 	}
 | |
| 	if ciReport != nil {
 | |
| 		statusMessage = fmt.Sprintf("%s (%q)", ciReport.Status, ciReport.URL)
 | |
| 	}
 | |
| 	return statusMessage
 | |
| }
 | |
| 
 | |
| // GetAnalysesNotes returns all of the notes from the most recent static
 | |
| // analysis run recorded in the git notes.
 | |
| func (r *Review) GetAnalysesNotes() ([]analyses.Note, error) {
 | |
| 	latestAnalyses, err := analyses.GetLatestAnalysesReport(r.Analyses)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if latestAnalyses == nil {
 | |
| 		return nil, fmt.Errorf("No analyses available")
 | |
| 	}
 | |
| 	return latestAnalyses.GetNotes()
 | |
| }
 | |
| 
 | |
| // GetAnalysesMessage returns a string summarizing the results of the
 | |
| // most recent static analyses.
 | |
| func (r *Review) GetAnalysesMessage() string {
 | |
| 	latestAnalyses, err := analyses.GetLatestAnalysesReport(r.Analyses)
 | |
| 	if err != nil {
 | |
| 		return err.Error()
 | |
| 	}
 | |
| 	if latestAnalyses == nil {
 | |
| 		return "No analyses available"
 | |
| 	}
 | |
| 	status := latestAnalyses.Status
 | |
| 	if status != "" && status != analyses.StatusNeedsMoreWork {
 | |
| 		return status
 | |
| 	}
 | |
| 	analysesNotes, err := latestAnalyses.GetNotes()
 | |
| 	if err != nil {
 | |
| 		return err.Error()
 | |
| 	}
 | |
| 	if analysesNotes == nil {
 | |
| 		return "passed"
 | |
| 	}
 | |
| 	return fmt.Sprintf("%d warnings\n", len(analysesNotes))
 | |
| 	// TODO(ojarjur): Figure out the best place to display the actual notes
 | |
| }
 | |
| 
 | |
| func prettyPrintJSON(jsonBytes []byte) (string, error) {
 | |
| 	var prettyBytes bytes.Buffer
 | |
| 	err := json.Indent(&prettyBytes, jsonBytes, "", "  ")
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return prettyBytes.String(), nil
 | |
| }
 | |
| 
 | |
| // GetJSON returns the pretty printed JSON for a review summary.
 | |
| func (r *Summary) GetJSON() (string, error) {
 | |
| 	jsonBytes, err := json.Marshal(*r)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return prettyPrintJSON(jsonBytes)
 | |
| }
 | |
| 
 | |
| // GetJSON returns the pretty printed JSON for a review.
 | |
| func (r *Review) GetJSON() (string, error) {
 | |
| 	jsonBytes, err := json.Marshal(*r)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return prettyPrintJSON(jsonBytes)
 | |
| }
 | |
| 
 | |
| // findLastCommit returns the later (newest) commit from the union of the provided commit
 | |
| // and all of the commits that are referenced in the given comment threads.
 | |
| func (r *Review) findLastCommit(startingCommit, latestCommit string, commentThreads []CommentThread) string {
 | |
| 	isLater := func(commit string) bool {
 | |
| 		if err := r.Repo.VerifyCommit(commit); err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		if t, e := r.Repo.IsAncestor(latestCommit, commit); e == nil && t {
 | |
| 			return true
 | |
| 		}
 | |
| 		if t, e := r.Repo.IsAncestor(startingCommit, commit); e == nil && !t {
 | |
| 			return false
 | |
| 		}
 | |
| 		if t, e := r.Repo.IsAncestor(commit, latestCommit); e == nil && t {
 | |
| 			return false
 | |
| 		}
 | |
| 		ct, err := r.Repo.GetCommitTime(commit)
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		lt, err := r.Repo.GetCommitTime(latestCommit)
 | |
| 		if err != nil {
 | |
| 			return true
 | |
| 		}
 | |
| 		return ct > lt
 | |
| 	}
 | |
| 	updateLatest := func(commit string) {
 | |
| 		if commit == "" {
 | |
| 			return
 | |
| 		}
 | |
| 		if isLater(commit) {
 | |
| 			latestCommit = commit
 | |
| 		}
 | |
| 	}
 | |
| 	for _, commentThread := range commentThreads {
 | |
| 		comment := commentThread.Comment
 | |
| 		if comment.Location != nil {
 | |
| 			updateLatest(comment.Location.Commit)
 | |
| 		}
 | |
| 		updateLatest(r.findLastCommit(startingCommit, latestCommit, commentThread.Children))
 | |
| 	}
 | |
| 	return latestCommit
 | |
| }
 | |
| 
 | |
| func (r *Summary) getStartingCommit() string {
 | |
| 	if r.Request.Alias != "" {
 | |
| 		return r.Request.Alias
 | |
| 	}
 | |
| 	return r.Revision
 | |
| }
 | |
| 
 | |
| // GetHeadCommit returns the latest commit in a review.
 | |
| func (r *Review) GetHeadCommit() (string, error) {
 | |
| 	currentCommit := r.getStartingCommit()
 | |
| 	if r.Request.ReviewRef == "" {
 | |
| 		return currentCommit, nil
 | |
| 	}
 | |
| 
 | |
| 	if r.Submitted {
 | |
| 		// The review has already been submitted.
 | |
| 		// Go through the list of comments and find the last commented upon commit.
 | |
| 		return r.findLastCommit(currentCommit, currentCommit, r.Comments), nil
 | |
| 	}
 | |
| 
 | |
| 	// It is possible that the review ref is no longer an ancestor of the starting
 | |
| 	// commit (e.g. if a rebase left us in a detached head), in which case we have to
 | |
| 	// find the head commit without using it.
 | |
| 	useReviewRef, err := r.Repo.IsAncestor(currentCommit, r.Request.ReviewRef)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	if useReviewRef {
 | |
| 		return r.Repo.ResolveRefCommit(r.Request.ReviewRef)
 | |
| 	}
 | |
| 
 | |
| 	return r.findLastCommit(currentCommit, currentCommit, r.Comments), nil
 | |
| }
 | |
| 
 | |
| // GetBaseCommit returns the commit against which a review should be compared.
 | |
| func (r *Review) GetBaseCommit() (string, error) {
 | |
| 	if !r.IsOpen() {
 | |
| 		if r.Request.BaseCommit != "" {
 | |
| 			return r.Request.BaseCommit, nil
 | |
| 		}
 | |
| 
 | |
| 		// This means the review has been submitted, but did not specify a base commit.
 | |
| 		// In this case, we have to treat the last parent commit as the base. This is
 | |
| 		// usually what we want, since merging a target branch into a feature branch
 | |
| 		// results in the previous commit to the feature branch being the first parent,
 | |
| 		// and the latest commit to the target branch being the second parent.
 | |
| 		return r.Repo.GetLastParent(r.Revision)
 | |
| 	}
 | |
| 
 | |
| 	targetRefHead, err := r.Repo.ResolveRefCommit(r.Request.TargetRef)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	leftHandSide := targetRefHead
 | |
| 	rightHandSide := r.Revision
 | |
| 	if r.Request.ReviewRef != "" {
 | |
| 		if reviewRefHead, err := r.Repo.ResolveRefCommit(r.Request.ReviewRef); err == nil {
 | |
| 			rightHandSide = reviewRefHead
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return r.Repo.MergeBase(leftHandSide, rightHandSide)
 | |
| }
 | |
| 
 | |
| // ListCommits lists the commits included in a review.
 | |
| func (r *Review) ListCommits() ([]string, error) {
 | |
| 	baseCommit, err := r.GetBaseCommit()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	headCommit, err := r.GetHeadCommit()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return r.Repo.ListCommitsBetween(baseCommit, headCommit)
 | |
| }
 | |
| 
 | |
| // GetDiff returns the diff for a review.
 | |
| func (r *Review) GetDiff(diffArgs ...string) (string, error) {
 | |
| 	var baseCommit, headCommit string
 | |
| 	baseCommit, err := r.GetBaseCommit()
 | |
| 	if err == nil {
 | |
| 		headCommit, err = r.GetHeadCommit()
 | |
| 	}
 | |
| 	if err == nil {
 | |
| 		return r.Repo.Diff(baseCommit, headCommit, diffArgs...)
 | |
| 	}
 | |
| 	return "", err
 | |
| }
 | |
| 
 | |
| // AddComment adds the given comment to the review.
 | |
| func (r *Review) AddComment(c comment.Comment) error {
 | |
| 	commentNote, err := c.Write()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	r.Repo.AppendNote(comment.Ref, r.Revision, commentNote)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Rebase performs an interactive rebase of the review onto its target ref.
 | |
| //
 | |
| // If the 'archivePrevious' argument is true, then the previous head of the
 | |
| // review will be added to the 'refs/devtools/archives/reviews' ref prior
 | |
| // to being rewritten. That ensures the review history is kept from being
 | |
| // garbage collected.
 | |
| func (r *Review) Rebase(archivePrevious bool) error {
 | |
| 	if archivePrevious {
 | |
| 		orig, err := r.GetHeadCommit()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if err := r.Repo.ArchiveRef(orig, archiveRef); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	if err := r.Repo.SwitchToRef(r.Request.ReviewRef); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	err := r.Repo.RebaseRef(r.Request.TargetRef)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	alias, err := r.Repo.GetCommitHash("HEAD")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	r.Request.Alias = alias
 | |
| 	newNote, err := r.Request.Write()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return r.Repo.AppendNote(request.Ref, r.Revision, newNote)
 | |
| }
 | |
| 
 | |
| // RebaseAndSign performs an interactive rebase of the review onto its
 | |
| // target ref. It signs the result of the rebase as well as (re)signs
 | |
| // the review request itself.
 | |
| //
 | |
| // If the 'archivePrevious' argument is true, then the previous head of the
 | |
| // review will be added to the 'refs/devtools/archives/reviews' ref prior
 | |
| // to being rewritten. That ensures the review history is kept from being
 | |
| // garbage collected.
 | |
| func (r *Review) RebaseAndSign(archivePrevious bool) error {
 | |
| 	if archivePrevious {
 | |
| 		orig, err := r.GetHeadCommit()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if err := r.Repo.ArchiveRef(orig, archiveRef); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	if err := r.Repo.SwitchToRef(r.Request.ReviewRef); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	err := r.Repo.RebaseAndSignRef(r.Request.TargetRef)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	alias, err := r.Repo.GetCommitHash("HEAD")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	r.Request.Alias = alias
 | |
| 
 | |
| 	key, err := r.Repo.GetUserSigningKey()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	err = gpg.Sign(key, &r.Request)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	newNote, err := r.Request.Write()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return r.Repo.AppendNote(request.Ref, r.Revision, newNote)
 | |
| }
 |