internal/gerrit: add ChangeRevisions, no Comments error, various cleanups
This CL contains various cleanups while still working on the reviews dashboard:
- Canonicalize accounts in ChangeMessages.
- Rename ChangeReviewed to ChangeReviewers to reflect what it actually returns.
- Add ChangeRevisions.
- Stop returning an error from Comments to be consistent with other functions.
Also, canonicalize accounts.
- Add a test for ChangeCommentCounts.
- Change more slice fields in the Gerrit types to use struct pointers.
Change-Id: Iedb99765f542a5ad97b04f622a63a69abb8cb986
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/617758
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Ian Lance Taylor <iant@golang.org>
diff --git a/internal/gerrit/change.go b/internal/gerrit/change.go
index 78ca091..be59b6d 100644
--- a/internal/gerrit/change.go
+++ b/internal/gerrit/change.go
@@ -5,6 +5,7 @@
package gerrit
import (
+ "cmp"
"encoding/json"
"fmt"
"slices"
@@ -135,12 +136,52 @@
// These are the top-level messages created by clicking on
// the top REPLY button when reviewing a change.
// Inline file comments are returned by [Client.Comments].
-func (c *Client) ChangeMessages(ch *Change) []ChangeMessageInfo {
+func (c *Client) ChangeMessages(ch *Change) []*ChangeMessageInfo {
var messages struct {
- Messages []ChangeMessageInfo `json:"messages"`
+ Messages []json.RawMessage `json:"messages"`
}
c.unmarshal(ch, "messages", &messages)
- return messages.Messages
+
+ // Unpack into a changeMessageInfo struct, and then convert to
+ // ChangeMessageInfo, so that we don't have to unpack the lengthy
+ // AccountInfo data each time.
+ type changeMessageInfo struct {
+ ID string `json:"id"`
+ Author json.RawMessage `json:"author,omitempty"`
+ RealAuthor json.RawMessage `json:"real_author,omitempty"`
+ Date TimeStamp `json:"date"`
+ Message string `json:"message"`
+ AccountsInMessage []json.RawMessage `json:"accounts_in_message,omitempty"`
+ Tag string `json:"tag,omitempty"`
+ RevisionNumber int `json:"_revision_number,omitempty"`
+ }
+
+ ret := make([]*ChangeMessageInfo, 0, len(messages.Messages))
+ for _, msg := range messages.Messages {
+ var cmi changeMessageInfo
+ if err := json.Unmarshal(msg, &cmi); err != nil {
+ c.slog.Error("gerrit message decode failure", "num", ch.num, "data", msg, "err", err)
+ c.db.Panic("gerrit message decode failure", "num", ch.num, "err", err)
+ }
+
+ var aims []*AccountInfo
+ for _, aim := range cmi.AccountsInMessage {
+ aims = append(aims, c.loadAccount(aim))
+ }
+ r := &ChangeMessageInfo{
+ ID: cmi.ID,
+ Author: c.loadAccount(cmi.Author),
+ RealAuthor: c.loadAccount(cmi.RealAuthor),
+ Date: cmi.Date,
+ Message: cmi.Message,
+ AccountsInMessage: aims,
+ Tag: cmi.Tag,
+ RevisionNumber: cmi.RevisionNumber,
+ }
+ ret = append(ret, r)
+ }
+
+ return ret
}
// ChangeDescription returns the current description of the change.
@@ -176,10 +217,11 @@
return workInProgress.WorkInProgress
}
-// ChangeReviewed returns a list of accounts that have reviewed this change.
+// ChangeReviewers returns a list of accounts that are listed as
+// reviewers of this change.
// Note that this is not identical to ChangeInfo.Reviewers,
// which includes both reviewers and people CC'ed.
-func (c *Client) ChangeReviewed(ch *Change) []*AccountInfo {
+func (c *Client) ChangeReviewers(ch *Change) []*AccountInfo {
var reviewers struct {
Reviewers map[string][]json.RawMessage `json:"reviewers"`
}
@@ -227,6 +269,9 @@
// unmarshalLabel unmarshals a LabelInfo.
func (c *Client) unmarshalLabel(ch *Change, input json.RawMessage) *LabelInfo {
+ // Unpack into approvalInfo and labelInfo structs, and then convert to
+ // ApprovalInfo and LabelInfo, so that we don't have to unpack the
+ // lengthy AccountInfo data each time.
type approvalInfo struct {
Value int `json:"value,omitempty"`
PermittedVotingRange VotingRangeInfo `json:"permitted_voting_range,omitempty"`
@@ -348,6 +393,72 @@
return counts.TotalCommentCount, counts.UnresolvedCommentCount
}
+// ChangeRevisions returns information about all the patch sets,
+// ordered by patch set number.
+func (c *Client) ChangeRevisions(ch *Change) []*RevisionInfo {
+ // Unpack into commitInfo and revisionInfo structs,
+ // and then convert to CommitInfo and RevisionInfo,
+ // so that we don't have to unpack the lengthy AccountInfo
+ // data each time. We also skip some CommitInfo fields.
+ type commitInfo struct {
+ Commit string `json:"commit,omitempty"`
+ Author *GitPersonInfo `json:"author,omitempty"`
+ Committer *GitPersonInfo `json:"committer,omitempty"`
+ Subject string `json:"subject"`
+ Message string `json:"message,omitempty"`
+ }
+ type revisionInfo struct {
+ Kind string `json:"kind"`
+ Number int `json:"_number"`
+ Created TimeStamp `json:"created"`
+ Uploader json.RawMessage `json:"uploader"`
+ RealUploader json.RawMessage `json:"real_uploader,omitempty"`
+ Ref string `json:"ref"`
+ Commit *commitInfo `json:"commit,omitempty"`
+ Branch string `json:"branch,omitempty"`
+ Description string `json:"description,omitempty"`
+ }
+ var revisions struct {
+ Revisions map[string]*revisionInfo `json:"revisions"`
+ }
+ c.unmarshal(ch, "revisions", &revisions)
+
+ toCommitInfo := func(from *commitInfo) *CommitInfo {
+ if from == nil {
+ return nil
+ }
+ return &CommitInfo{
+ Commit: from.Commit,
+ Author: from.Author,
+ Committer: from.Committer,
+ Subject: from.Subject,
+ Message: from.Message,
+ }
+ }
+
+ revs := make([]*RevisionInfo, 0, len(revisions.Revisions))
+ for _, rev := range revisions.Revisions {
+ ri := &RevisionInfo{
+ Kind: rev.Kind,
+ Number: rev.Number,
+ Created: rev.Created,
+ Uploader: c.loadAccount(rev.Uploader),
+ RealUploader: c.loadAccount(rev.RealUploader),
+ Ref: rev.Ref,
+ Commit: toCommitInfo(rev.Commit),
+ Branch: rev.Branch,
+ Description: rev.Description,
+ }
+ revs = append(revs, ri)
+ }
+
+ slices.SortFunc(revs, func(a, b *RevisionInfo) int {
+ return cmp.Compare(a.Number, b.Number)
+ })
+
+ return revs
+}
+
// unmarshal unmarshals ch.data into a value. If the unmarshal fails, it
// crashes with an error.
func (c *Client) unmarshal(ch *Change, msg string, val any) {
diff --git a/internal/gerrit/data.go b/internal/gerrit/data.go
index f78041d..595e772 100644
--- a/internal/gerrit/data.go
+++ b/internal/gerrit/data.go
@@ -56,21 +56,71 @@
// Comments returns the comments on a change, if any. These are the
// inline comments placed on files in the change. The top-level
-// replies are stored in a [Change] and are returned by [Change.Messages].
+// replies are stored in a [Change] and are returned by [Client.ChangeMessages].
//
// This returns a map from file names to a list of comments on each file.
-// Gerrit stores top-level comments on the file "/PATCHSET_LEVEL".
-// The result may be nil, with no error, if no comment information exists.
-func (c *Client) Comments(project string, changeNum int) (map[string][]*CommentInfo, error) {
+// The result is nil if no comment information exists.
+func (c *Client) Comments(project string, changeNum int) map[string][]*CommentInfo {
val, ok := c.db.Get(o(commentKind, c.instance, project, changeNum))
if !ok {
- return nil, nil
+ return nil
}
- var comments map[string][]*CommentInfo
+
+ // Unpack into a commentInfo struct, and then convert to CommentInfo,
+ // so that we don't have to unpack the lengthy AccountInfo data
+ // each time.
+ type commentInfo struct {
+ PatchSet int `json:"patch_set,omitempty"`
+ ID string `json:"id"`
+ Path string `json:"path,omitempty"`
+ Side string `json:"side,omitempty"`
+ Parent int `json:"parent,omitempty"`
+ Line int `json:"line,omitempty"`
+ Range *CommentRange `json:"range,omitempty"`
+ InReplyTo string `json:"in_reply_to,omitempty"`
+ Message string `json:"message,omitempty"`
+ Updated TimeStamp `json:"updated"`
+ Author json.RawMessage `json:"author,omitempty"`
+ Tag string `json:"tag,omitempty"`
+ Unresolved bool `json:"unresolved,omitempty"`
+ ChangeMessageID string `json:"change_message_id,omitempty"`
+ CommitID string `json:"commit_id,omitempty"`
+ FixSuggestions []*FixSuggestionInfo `json:"fix_suggestions,omitempty"`
+ }
+ var comments map[string][]*commentInfo
if err := json.Unmarshal(val, &comments); err != nil {
- return nil, fmt.Errorf("can't decode change %d comments: %v", changeNum, err)
+ c.slog.Error("gerrit comment decode failure", "num", changeNum, "data", val, "err", err)
+ c.db.Panic("gerrit comment decode failure", "num", changeNum, "err", err)
}
- return comments, nil
+
+ ret := make(map[string][]*CommentInfo, len(comments))
+ for key, val := range comments {
+ rcs := make([]*CommentInfo, 0, len(val))
+ for _, comment := range val {
+ rc := &CommentInfo{
+ PatchSet: comment.PatchSet,
+ ID: comment.ID,
+ Path: comment.Path,
+ Side: comment.Side,
+ Parent: comment.Parent,
+ Line: comment.Line,
+ Range: comment.Range,
+ InReplyTo: comment.InReplyTo,
+ Message: comment.Message,
+ Updated: comment.Updated,
+ Author: c.loadAccount(comment.Author),
+ Tag: comment.Tag,
+ Unresolved: comment.Unresolved,
+ ChangeMessageID: comment.ChangeMessageID,
+ CommitID: comment.CommitID,
+ FixSuggestions: comment.FixSuggestions,
+ }
+ rcs = append(rcs, rc)
+ }
+ ret[key] = rcs
+ }
+
+ return ret
}
// A ChangeEvent is a Gerrit CL change event returned by ChangeWatcher.
diff --git a/internal/gerrit/sync_test.go b/internal/gerrit/sync_test.go
index dfa1a1b..2058e31 100644
--- a/internal/gerrit/sync_test.go
+++ b/internal/gerrit/sync_test.go
@@ -227,7 +227,7 @@
"Patch Set 9:\n\nNaah, just roll forward and fix the test.",
},
func(got, want any) bool {
- g := got.([]ChangeMessageInfo)
+ g := got.([]*ChangeMessageInfo)
w := want.([]string)
for i, m := range g {
if m.Message != w[i] {
@@ -250,8 +250,8 @@
nil,
},
{
- "ChangeReviewed",
- wa(c.ChangeReviewed),
+ "ChangeReviewers",
+ wa(c.ChangeReviewers),
[]string{
"bradfitz@golang.org",
"iant@golang.org",
@@ -328,9 +328,32 @@
return slices.Equal(g, w)
},
},
+ {
+ "ChangeRevisions",
+ wa(c.ChangeRevisions),
+ "errgroup: add package",
+ func(got, want any) bool {
+ g := got.([]*RevisionInfo)
+ if len(g) != 9 {
+ return false
+ }
+ for _, r := range g {
+ if r.Commit.Subject != want {
+ return false
+ }
+ }
+ return true
+ },
+ },
}
testChangeTests(t, ch, tests)
+
+ total, unresolved := c.ChangeCommentCounts(ch)
+ wantTotal, wantUnresolved := 22, 0
+ if total != wantTotal || unresolved != wantUnresolved {
+ t.Errorf("CommentCounts = %d, %d; want %d, %d", total, unresolved, wantTotal, wantUnresolved)
+ }
}
// checkChange verifies that we can unpack CL information, and that it
@@ -343,11 +366,7 @@
}
// Fetch and unpack comments for the change.
- commentsInfo, err := c.Comments(project, changeNum)
- if err != nil {
- t.Error(err)
- return
- }
+ commentsInfo := c.Comments(project, changeNum)
if commentsInfo == nil {
t.Errorf("no comment information for CL %d", changeNum)
return
diff --git a/internal/gerrit/testing_test.go b/internal/gerrit/testing_test.go
index 2681160..d32841a 100644
--- a/internal/gerrit/testing_test.go
+++ b/internal/gerrit/testing_test.go
@@ -79,7 +79,7 @@
"message 2",
},
func(got, want any) bool {
- g := got.([]ChangeMessageInfo)
+ g := got.([]*ChangeMessageInfo)
w := want.([]string)
for i, m := range g {
if m.Message != w[i] {
diff --git a/internal/gerrit/types.go b/internal/gerrit/types.go
index 196b820..e7daa5a 100644
--- a/internal/gerrit/types.go
+++ b/internal/gerrit/types.go
@@ -205,7 +205,7 @@
UserName string `json:"username,omitempty"`
// List of [AvatarInfo] entities that provide information about
// avatar images of the account.
- Avatars []AvatarInfo `json:"avatars,omitempty"`
+ Avatars []*AvatarInfo `json:"avatars,omitempty"`
// Status message of the account.
Status string `json:"status,omitempty"`
// Whether the account is inactive.
@@ -265,7 +265,7 @@
// * label: the label name.
// * status:
// * appliedBy:
- Labels []struct {
+ Labels []*struct {
// The label name.
Label string `json:"label"`
// The label status: {OK, REJECT, MAY, NEED, IMPOSSIBLE}.
@@ -275,7 +275,7 @@
} `json:"labels,omitempty"`
// List of the requirements to be met before this change can
// be submitted.
- Requirements []Requirement `json:"requirements,omitempty"`
+ Requirements []*Requirement `json:"requirements,omitempty"`
// When status is RULE_ERROR this message provides some text
// describing the failure of the rule predicate.
ErrorMessage string `json:"error_message,omitempty"`
@@ -428,7 +428,7 @@
// branch name if the parent is a merged commit in the target
// branch. Otherwise, we include the change and patch-set
// numbers of the parent change.
- ParentsData []ParentInfo `json:"parents_data,omitempty"`
+ ParentsData []*ParentInfo `json:"parents_data,omitempty"`
// The name of the target branch that this revision is set to
// be merged into. Note that if the change is moved with the
// Move Change endpoint, this field can be different for
@@ -461,7 +461,7 @@
// The parent commits of this commit as a list of CommitInfo
// entities. In each parent only the commit and subject fields
// are populated.
- Parents []CommitInfo `json:"parents,omitempty"`
+ Parents []*CommitInfo `json:"parents,omitempty"`
// The author of the commit as a GitPersonInfo entity.
Author *GitPersonInfo `json:"author,omitempty"`
// The committer of the commit as a GitPersonInfo entity.
@@ -472,10 +472,10 @@
Message string `json:"message,omitempty"`
// Links to the patch set in external sites as a list of
// WebLinkInfo entities.
- WebLinks []WebLinkInfo `json:"web_links,omitempty"`
+ WebLinks []*WebLinkInfo `json:"web_links,omitempty"`
// Links to the commit in external sites for resolving
// conflicts as a list of WebLinkInfo entities.
- ResolveConflictsWebLinks []WebLinkInfo `json:"resolve_conflicts_web_links,omitempty"`
+ ResolveConflictsWebLinks []*WebLinkInfo `json:"resolve_conflicts_web_links,omitempty"`
}
// GitPersonInfo holds information about the author/committer of a commit.
@@ -579,7 +579,7 @@
// the patchset to which this comment applies.
CommitID string `json:"commit_id,omitempty"`
// Suggested fixes for this comment.
- FixSuggestions []FixSuggestionInfo `json:"fix_suggestions,omitempty"`
+ FixSuggestions []*FixSuggestionInfo `json:"fix_suggestions,omitempty"`
// The remaining fields are defined by Gerrit but we don't
// request their values.
@@ -598,7 +598,7 @@
// A list of FixReplacementInfo entities indicating how the
// content of one or several files should be modified. Within
// a file, they should refer to non-overlapping regions.
- Replacements []FixReplacementInfo `json:"replacements"`
+ Replacements []*FixReplacementInfo `json:"replacements"`
}
// FixReplacementInfo describes how the content of a file should be
diff --git a/internal/gerritdocs/sync.go b/internal/gerritdocs/sync.go
index de000cf..2fec090 100644
--- a/internal/gerritdocs/sync.go
+++ b/internal/gerritdocs/sync.go
@@ -146,10 +146,7 @@
// comments returns file comments for the gerrit change.
func comments(gr *gerrit.Client, c *changeInfo) ([]*gerrit.CommentInfo, error) {
var cmts []*gerrit.CommentInfo
- cmtsMap, err := gr.Comments(c.project, c.number)
- if err != nil {
- return nil, err
- }
+ cmtsMap := gr.Comments(c.project, c.number)
for _, cs := range cmtsMap { // we don't care about comment file locations
cmts = append(cmts, cs...)
}