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
}
ListOpen returns all reviews that are not yet incorporated into their target refs.
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"
var emptyTree = repository.NewTree(map[string]repository.TreeChild{})
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"`
}
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"`
}
type Review struct {
	*Summary
	Reports  []ci.Report       `json:"reports,omitempty"`
	Analyses []analyses.Report `json:"analyses,omitempty"`
}
type commentsByTimestamp []*comment.Comment
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
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
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
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
}
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
}
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
}
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
}
type mutableThread struct {
	Hash     string
	Comment  comment.Comment
	Edits    []*comment.Comment
	Children []*mutableThread
}
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,
	}
}
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
}
func getCommentsFromNotes(repo repository.Repo, revision string, commentNotes []repository.Note) ([]CommentThread, *bool) {
	commentsByHash := comment.ParseAllValid(commentNotes)
	comments := buildCommentThreads(commentsByHash)
	resolved := updateThreadsStatus(comments)
	return comments, resolved
}
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,
	}
	comments, resolved := getCommentsFromNotes(repo, revision, commentNotes)
	reviewSummary.Comments = comments
	reviewSummary.Resolved = resolved
	return &reviewSummary, nil
}
func GetComments(repo repository.Repo, revision string) ([]CommentThread, error) {
	commentNotes := repo.GetNotes(comment.Ref, revision)
	c, _ := getCommentsFromNotes(repo, revision, commentNotes)
	return c, 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
}
func GetSummary(repo repository.Repo, revision string) (*Summary, error) {
	return GetSummaryViaRefs(repo, request.Ref, comment.Ref, revision)
}
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
}
func (r *Summary) IsAbandoned() bool {
	return r.Request.TargetRef == ""
}
func (r *Summary) IsOpen() bool {
	return !r.Submitted && !r.IsAbandoned()
}
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
}
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
}
func ListAll(repo repository.Repo) []Summary {
	reviews := unsortedListAll(repo)
	sort.Stable(summariesWithNewestRequestsFirst(reviews))
	return reviews
}