package main import ( "encoding" "encoding/json" "fmt" "net/url" "regexp" "strings" "time" ) // httpGetGerritJSON is like [httpGetJSON], but // https://gerrit-review.googlesource.com/Documentation/rest-api.html#output func httpGetGerritJSON(u string, hdr map[string]string, out any) error { str, err := httpGet(u, hdr) if err != nil { return err } if _, body, ok := strings.Cut(str, "\n"); ok { str = body } return json.Unmarshal([]byte(str), out) } const GerritTimeFormat = "2006-01-02 15:04:05.000000000" type GerritTime struct { Val time.Time } var ( _ fmt.Stringer = GerritTime{} _ encoding.TextMarshaler = GerritTime{} _ encoding.TextUnmarshaler = (*GerritTime)(nil) ) // String implements [fmt.Stringer]. func (t GerritTime) String() string { return t.Val.Format(GerritTimeFormat) } // MarshalText implements [encoding.TextMarshaler]. func (t GerritTime) MarshalText() ([]byte, error) { return []byte(t.String()), nil } // UnmarshalText implements [encoding.TextUnmarshaler]. func (t *GerritTime) UnmarshalText(data []byte) error { val, err := time.Parse(GerritTimeFormat, string(data)) if err != nil { return err } t.Val = val return nil } type Gerrit struct{} var _ Forge = Gerrit{} var reGoogleGerritCL = regexp.MustCompile(`https://([a-z]+-review\.googlesource\.com)/c/([^?#]+)/\+/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`) func (Gerrit) FetchStatus(urls []string) (string, error) { for _, u := range urls { if reGitHubPR.MatchString(u) { return "", nil } } for _, u := range urls { m := reGoogleGerritCL.FindStringSubmatch(u) if m == nil { continue } authority := m[1] projectID := m[2] changeID := m[3] urlStr := "https://" + authority + "/changes/" + url.PathEscape(projectID) + "~" + changeID + "?o=MESSAGES&o=DETAILED_ACCOUNTS" var obj struct { Status string `json:"status"` } if err := httpGetGerritJSON(urlStr, nil, &obj); err != nil { return "", err } // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info switch obj.Status { case "NEW": return "open", nil case "MERGED": return "merged", nil case "ABANDONED": return "closed", nil } } return "", nil } func (Gerrit) FetchSubmittedAt(urls []string) (time.Time, error) { for _, u := range urls { if reGitHubPR.MatchString(u) { return time.Time{}, nil } } for _, u := range urls { m := reGoogleGerritCL.FindStringSubmatch(u) if m == nil { continue } authority := m[1] projectID := m[2] changeID := m[3] urlStr := "https://" + authority + "/changes/" + url.PathEscape(projectID) + "~" + changeID + "?o=MESSAGES&o=DETAILED_ACCOUNTS" var obj struct { Created GerritTime `json:"created"` } if err := httpGetGerritJSON(urlStr, nil, &obj); err != nil { return time.Time{}, err } return obj.Created.Val, nil } return time.Time{}, nil } func (Gerrit) FetchLastUpdated(urls []string) (time.Time, User, error) { for _, u := range urls { m := reGoogleGerritCL.FindStringSubmatch(u) if m == nil { continue } authority := m[1] projectID := m[2] changeID := m[3] urlStr := "https://" + authority + "/changes/" + url.PathEscape(projectID) + "~" + changeID + "?o=MESSAGES&o=DETAILED_ACCOUNTS" var obj struct { Updated GerritTime `json:"updated"` Messages []struct { Author struct { AccountID int `json:"_account_id"` Name string `json:"name"` DisplayName string `json:"display_name"` } `json:"author"` Date GerritTime `json:"date"` } `json:"messages"` } if err := httpGetGerritJSON(urlStr, nil, &obj); err != nil { return time.Time{}, User{}, err } retUpdatedAt := obj.Updated.Val var retUser User for _, message := range obj.Messages { if withinOneSecond(message.Date.Val, retUpdatedAt) { if message.Author.DisplayName != "" { retUser.Name = message.Author.DisplayName } else { retUser.Name = message.Author.Name } retUser.URL = fmt.Sprintf("https://%s/dashboard/%d", authority, message.Author.AccountID) break } } return retUpdatedAt, retUser, nil } return time.Time{}, User{}, nil }