| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package gerrit |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "iter" |
| "strconv" |
| "time" |
| |
| "golang.org/x/oscar/internal/storage" |
| "golang.org/x/oscar/internal/storage/timed" |
| "rsc.io/ordered" |
| ) |
| |
| // ChangeNumbers returns an iterator over the change numbers of a project. |
| // The first iterator value is the change number. |
| // The second iterator value is a function that can be called to |
| // return information about the change, as with [storage.DB.Scan]. |
| func (c *Client) ChangeNumbers(project string) iter.Seq2[int, func() *Change] { |
| if c.divertChanges() { // testing |
| return c.testClient.changeNumbers() |
| } |
| |
| return func(yield func(int, func() *Change) bool) { |
| for key, fn := range c.db.Scan(o(changeKind, c.instance, project), o(changeKind, c.instance, project, ordered.Inf)) { |
| var changeNum int |
| if err := ordered.Decode(key, nil, nil, nil, &changeNum); err != nil { |
| c.db.Panic("gerrit client change decode", "key", storage.Fmt(key), "err", err) |
| } |
| cfn := func() *Change { |
| return &Change{ |
| num: changeNum, |
| data: bytes.Clone(fn()), |
| } |
| } |
| if !yield(changeNum, cfn) { |
| return |
| } |
| } |
| } |
| } |
| |
| // Change returns the data for a single change. |
| // This will return nil if no information is recorded. |
| func (c *Client) Change(project string, changeNum int) *Change { |
| if c.divertChanges() { // testing |
| return c.testClient.change(changeNum) |
| } |
| |
| val, ok := c.db.Get(o(changeKind, c.instance, project, changeNum)) |
| if !ok { |
| return nil |
| } |
| return &Change{ |
| num: changeNum, |
| data: val, |
| } |
| } |
| |
| // 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 [Client.ChangeMessages]. |
| // |
| // This returns a map from file names to a list of comments on each file. |
| // 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 |
| } |
| |
| // 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 { |
| c.slog.Error("gerrit comment decode failure", "num", changeNum, "data", val, "err", err) |
| c.db.Panic("gerrit comment decode failure", "num", changeNum, "err", err) |
| } |
| |
| 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. |
| type ChangeEvent struct { |
| DBTime timed.DBTime // when event was created |
| Instance string // Gerrit instance |
| ChangeNum int // change number |
| } |
| |
| // ChangeWatcher returns a new [timed.Watcher] with the given name. |
| // It picks up where any previous Watcher of the same name left odd. |
| func (c *Client) ChangeWatcher(name string) *timed.Watcher[ChangeEvent] { |
| return timed.NewWatcher(c.slog, c.db, name, changeUpdateKind, c.decodeChangeEvent) |
| } |
| |
| // decodeChangeUpdateEntry decodes a changeUpdateKind [timed.Entry] into |
| // a change number. |
| func (c *Client) decodeChangeEvent(t *timed.Entry) ChangeEvent { |
| ce := ChangeEvent{ |
| DBTime: t.ModTime, |
| } |
| if err := ordered.Decode(t.Key, &ce.Instance, &ce.ChangeNum, nil); err != nil { |
| c.db.Panic("gerrit change event decode", "key", storage.Fmt(t.Key), "err", err) |
| } |
| return ce |
| } |
| |
| // timeStampLayout is the timestamp format used by Gerrit. |
| // It is always in UTC. |
| const timeStampLayout = "2006-01-02 15:04:05.999999999" |
| |
| // TimeStamp adds Gerrit timestamp JSON marshaling and unmarshaling |
| // to a [time.Time]. |
| type TimeStamp time.Time |
| |
| // MarshalJSON marshals a TimeStamp into JSON. |
| func (ts *TimeStamp) MarshalJSON() ([]byte, error) { |
| return []byte(`"` + ts.Time().UTC().Format(timeStampLayout) + `"`), nil |
| } |
| |
| // UnmarshalJSON unmarshals JSON into a TimeStamp. |
| func (ts *TimeStamp) UnmarshalJSON(p []byte) error { |
| s, err := strconv.Unquote(string(p)) |
| if err != nil { |
| return fmt.Errorf("failed to unquote Gerrit time stamp %q: %v", p, err) |
| } |
| t, err := time.Parse(timeStampLayout, s) |
| if err != nil { |
| return fmt.Errorf("failed to unmarshal Gerrit time stamp: %v", err) |
| } |
| *ts = TimeStamp(t) |
| return nil |
| } |
| |
| // Time returns the value of the TimeStamp as a [time.Time]. |
| func (ts TimeStamp) Time() time.Time { return time.Time(ts) } |