blob: 0df0fe473a85aba472fdf0c7a5e7097218f99cdc [file] [log] [blame]
// 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] {
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) }