summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2024-05-19 02:29:46 -0600
committerLuke T. Shumaker <lukeshu@lukeshu.com>2024-05-19 02:29:46 -0600
commit632b189cfed31bf1a1f1edf0c1ae69f294ef4123 (patch)
treeb375ecf1391247bcd4439b7880a69c86b042aa42
parent75d2ab4f4d2415b1ecead2361acddb1e6d6392dd (diff)
cmd/generate: Factor forge_*.go files out of src_contribs.go
-rw-r--r--cmd/generate/forge_gerrit.go112
-rw-r--r--cmd/generate/forge_github.go186
-rw-r--r--cmd/generate/forge_gitlab.go160
-rw-r--r--cmd/generate/forge_pipermail.go98
-rw-r--r--cmd/generate/gerrit.go54
-rw-r--r--cmd/generate/httpcache.go6
-rw-r--r--cmd/generate/src_contribs.go397
7 files changed, 584 insertions, 429 deletions
diff --git a/cmd/generate/forge_gerrit.go b/cmd/generate/forge_gerrit.go
new file mode 100644
index 0000000..6bdeece
--- /dev/null
+++ b/cmd/generate/forge_gerrit.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "encoding"
+ "encoding/json"
+ "fmt"
+ "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{}
+
+func (Gerrit) FetchStatus(urls []string) (string, error) {
+ return "", nil
+}
+
+func (Gerrit) FetchSubmittedAt(urls []string) (time.Time, error) {
+ return time.Time{}, nil
+}
+
+var reGoLangGerritCL = regexp.MustCompile(`https://go-review\.googlesource\.com/c/([^/?#]+)/\+/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
+
+func (Gerrit) FetchLastUpdated(urls []string) (time.Time, User, error) {
+ for _, u := range urls {
+ m := reGoLangGerritCL.FindStringSubmatch(u)
+ if m == nil {
+ continue
+ }
+ projectID := m[1]
+ changeID := m[2]
+
+ urlStr := "https://go-review.googlesource.com/changes/" + 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://go-review.googlesource.com/dashboard/%d", message.Author.AccountID)
+ break
+ }
+ }
+ return retUpdatedAt, retUser, nil
+ }
+ return time.Time{}, User{}, nil
+}
diff --git a/cmd/generate/forge_github.go b/cmd/generate/forge_github.go
new file mode 100644
index 0000000..77a2919
--- /dev/null
+++ b/cmd/generate/forge_github.go
@@ -0,0 +1,186 @@
+package main
+
+import (
+ "fmt"
+ "net/url"
+ "regexp"
+ "time"
+)
+
+var reGitHubPR = regexp.MustCompile(`^https://github\.com/([^/?#]+)/([^/?#]+)/pull/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
+
+func githubPagination(i int) url.Values {
+ params := make(url.Values)
+ params.Set("page", fmt.Sprintf("%v", i+1))
+ return params
+}
+
+type GitHub struct{}
+
+var _ Forge = GitHub{}
+
+func (GitHub) FetchStatus(urls []string) (string, error) {
+ for _, u := range urls {
+ m := reGitHubPR.FindStringSubmatch(u)
+ if m == nil {
+ continue
+ }
+ user := m[1]
+ repo := m[2]
+ prnum := m[3]
+
+ urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum
+
+ var obj struct {
+ // State values are "open" and "closed".
+ State string `json:"state"`
+ Merged bool `json:"merged"`
+ MergeCommitSha string `json:"merge_commit_sha"`
+ }
+ if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+ return "", err
+ }
+ ret := obj.State
+ if obj.Merged {
+ ret = statusMerged
+ tag, err := getGitTagThatContainsAll("https://github.com/"+user+"/"+repo, obj.MergeCommitSha)
+ if err != nil {
+ return "", err
+ }
+ if tag != "" {
+ ret = fmt.Sprintf(statusReleasedFmt, tag)
+ }
+ }
+
+ return ret, nil
+ }
+ return "", nil
+}
+
+func (GitHub) FetchSubmittedAt(urls []string) (time.Time, error) {
+ for _, u := range urls {
+ m := reGitHubPR.FindStringSubmatch(u)
+ if m == nil {
+ continue
+ }
+ user := m[1]
+ repo := m[2]
+ prnum := m[3]
+
+ urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum
+
+ var obj struct {
+ CreatedAt time.Time `json:"created_at"`
+ }
+ if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+ return time.Time{}, err
+ }
+ return obj.CreatedAt, nil
+ }
+ return time.Time{}, nil
+}
+
+func (GitHub) FetchLastUpdated(urls []string) (time.Time, User, error) {
+ for _, u := range urls {
+ m := reGitHubPR.FindStringSubmatch(u)
+ if m == nil {
+ continue
+ }
+ user := m[1]
+ repo := m[2]
+ prnum := m[3]
+
+ var obj struct {
+ UpdatedAt time.Time `json:"updated_at"`
+
+ CreatedAt time.Time `json:"created_at"`
+ CreatedBy struct {
+ Login string `json:"login"`
+ HTMLURL string `json:"html_url"`
+ } `json:"user"`
+
+ MergedAt time.Time `json:"merged_at"`
+ MergedBy struct {
+ Login string `json:"login"`
+ HTMLURL string `json:"html_url"`
+ } `json:"merged_by"`
+ }
+ if err := httpGetJSON("https://api.github.com/repos/"+user+"/"+repo+"/pulls/"+prnum, nil, &obj); err != nil {
+ return time.Time{}, User{}, err
+ }
+
+ retUpdatedAt := obj.UpdatedAt
+ var retUser User
+
+ if retUser == (User{}) && withinOneSecond(obj.CreatedAt, retUpdatedAt) {
+ retUser.Name = obj.CreatedBy.Login
+ retUser.URL = obj.CreatedBy.HTMLURL
+ }
+ if retUser == (User{}) && withinOneSecond(obj.MergedAt, retUpdatedAt) {
+ retUser.Name = obj.MergedBy.Login
+ retUser.URL = obj.MergedBy.HTMLURL
+ }
+ if retUser == (User{}) {
+ // "normal" comments
+ var comments []struct {
+ UpdatedAt time.Time `json:"updated_at"`
+ User struct {
+ Login string `json:"login"`
+ HTMLURL string `json:"html_url"`
+ } `json:"user"`
+ }
+ if err := httpGetPaginatedJSON("https://api.github.com/repos/"+user+"/"+repo+"/issues/"+prnum+"/comments", nil, &comments, githubPagination); err != nil {
+ return time.Time{}, User{}, err
+ }
+ for _, comment := range comments {
+ if withinOneSecond(comment.UpdatedAt, retUpdatedAt) {
+ retUser.Name = comment.User.Login
+ retUser.URL = comment.User.HTMLURL
+ break
+ }
+ }
+ }
+ if retUser == (User{}) {
+ // comments on a specific part of the diff
+ var reviewComments []struct {
+ UpdatedAt time.Time `json:"updated_at"`
+ User struct {
+ Login string `json:"login"`
+ HTMLURL string `json:"html_url"`
+ } `json:"user"`
+ }
+ if err := httpGetPaginatedJSON("https://api.github.com/repos/"+user+"/"+repo+"/pulls/"+prnum+"/comments", nil, &reviewComments, githubPagination); err != nil {
+ return time.Time{}, User{}, err
+ }
+ for _, comment := range reviewComments {
+ if withinOneSecond(comment.UpdatedAt, retUpdatedAt) {
+ retUser.Name = comment.User.Login
+ retUser.URL = comment.User.HTMLURL
+ break
+ }
+ }
+ }
+ if retUser == (User{}) {
+ var events []struct {
+ CreatedAt time.Time `json:"created_at"`
+ Actor struct {
+ Login string `json:"login"`
+ HTMLURL string `json:"html_url"`
+ } `json:"actor"`
+ }
+ if err := httpGetJSON("https://api.github.com/repos/"+user+"/"+repo+"/issues/"+prnum+"/events", nil, &events); err != nil {
+ return time.Time{}, User{}, err
+ }
+ for _, event := range events {
+ if withinOneSecond(event.CreatedAt, retUpdatedAt) {
+ retUser.Name = event.Actor.Login
+ retUser.URL = event.Actor.HTMLURL
+ break
+ }
+ }
+ }
+
+ return retUpdatedAt, retUser, nil
+ }
+ return time.Time{}, User{}, nil
+}
diff --git a/cmd/generate/forge_gitlab.go b/cmd/generate/forge_gitlab.go
new file mode 100644
index 0000000..41814ec
--- /dev/null
+++ b/cmd/generate/forge_gitlab.go
@@ -0,0 +1,160 @@
+package main
+
+import (
+ "fmt"
+ "net/url"
+ "regexp"
+ "time"
+)
+
+var reGitLabMR = regexp.MustCompile(`^https://([^/]+)/([^?#]+)/-/merge_requests/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
+
+type GitLab struct{}
+
+var _ Forge = GitLab{}
+
+func (GitLab) FetchStatus(urls []string) (string, error) {
+ for _, u := range urls {
+ m := reGitLabMR.FindStringSubmatch(u)
+ if m == nil {
+ continue
+ }
+ authority := m[1]
+ projectID := m[2]
+ mrnum := m[3]
+
+ urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum
+
+ var obj struct {
+ // State values are "opened", "closed", "locked", and "merged".
+ State string `json:"state"`
+ MergeCommitSha string `json:"merge_commit_sha"`
+ SquashCommitSha string `json:"squash_commit_sha"`
+ }
+ if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+ return "", err
+ }
+
+ ret := obj.State
+ if ret == "opened" {
+ ret = statusOpen
+ }
+
+ if ret == "merged" {
+ ret = statusMerged
+ var mergeCommit string
+ if obj.MergeCommitSha != "" {
+ mergeCommit = obj.MergeCommitSha
+ }
+ if obj.SquashCommitSha != "" {
+ mergeCommit = obj.SquashCommitSha
+ }
+ if mergeCommit != "" {
+ tag, err := getGitTagThatContainsAll("https://"+authority+"/"+projectID+".git", mergeCommit)
+ if err != nil {
+ return "", err
+ }
+ if tag != "" {
+ ret = fmt.Sprintf(statusReleasedFmt, tag)
+ }
+ }
+ }
+
+ return ret, nil
+ }
+ return "", nil
+}
+
+func (GitLab) FetchSubmittedAt(urls []string) (time.Time, error) {
+ for _, u := range urls {
+ m := reGitLabMR.FindStringSubmatch(u)
+ if m == nil {
+ continue
+ }
+ authority := m[1]
+ projectID := m[2]
+ mrnum := m[3]
+
+ urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum
+
+ var obj struct {
+ CreatedAt time.Time `json:"created_at"`
+ }
+ if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+ return time.Time{}, err
+ }
+ return obj.CreatedAt, nil
+ }
+ return time.Time{}, nil
+}
+
+func (GitLab) FetchLastUpdated(urls []string) (time.Time, User, error) {
+ for _, u := range urls {
+ m := reGitLabMR.FindStringSubmatch(u)
+ if m == nil {
+ continue
+ }
+ authority := m[1]
+ projectID := m[2]
+ mrnum := m[3]
+
+ urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum
+
+ var obj struct {
+ ID int `json:"id"`
+
+ UpdatedAt time.Time `json:"updated_at"`
+
+ CreatedAt time.Time `json:"created_at"`
+ CreatedBy struct {
+ Username string `json:"username"`
+ WebURL string `json:"web_url"`
+ } `json:"author"`
+
+ MergedAt time.Time `json:"merged_at"`
+ MergedBy struct {
+ Username string `json:"username"`
+ WebURL string `json:"web_url"`
+ } `json:"merged_by"`
+ }
+ if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+ return time.Time{}, User{}, err
+ }
+
+ retUpdatedAt := obj.UpdatedAt
+ var retUser User
+
+ if retUser == (User{}) && withinOneSecond(obj.CreatedAt, retUpdatedAt) {
+ retUser.Name = obj.CreatedBy.Username
+ retUser.URL = obj.CreatedBy.WebURL
+ }
+ if retUser == (User{}) && withinOneSecond(obj.MergedAt, retUpdatedAt) {
+ retUser.Name = obj.MergedBy.Username
+ retUser.URL = obj.MergedBy.WebURL
+ }
+ if retUser == (User{}) {
+ var notes struct {
+ Notes []struct {
+ UpdatedAt time.Time `json:"updated_at"`
+ Author struct {
+ Username string `json:"username"`
+ WebURL string `json:"web_url"`
+ } `json:"author"`
+ } `json:"notes"`
+ }
+ if err := httpGetJSON(fmt.Sprintf("https://%s/%s/noteable/merge_request/%d/notes", authority, projectID, obj.ID), map[string]string{"X-Last-Fetched-At": "0"}, &notes); err != nil {
+ return time.Time{}, User{}, err
+ }
+ for _, note := range notes.Notes {
+ if withinOneSecond(note.UpdatedAt, retUpdatedAt) {
+ retUser.Name = note.Author.Username
+ retUser.URL = note.Author.WebURL
+ break
+ }
+ }
+ }
+
+ return retUpdatedAt, retUser, nil
+ }
+ return time.Time{}, User{}, nil
+}
diff --git a/cmd/generate/forge_pipermail.go b/cmd/generate/forge_pipermail.go
new file mode 100644
index 0000000..2c5cf01
--- /dev/null
+++ b/cmd/generate/forge_pipermail.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+ "time"
+)
+
+var (
+ rePiperMailDate = regexp.MustCompile(`^\s*<I>([^<]+)</I>\s*$`)
+ reGitHubCommit = regexp.MustCompile(`^https://github\.com/([^/?#]+)/([^/?#]+)/commit/([0-9a-f]+)(?:\?[^#]*)?(?:#.*)?$`)
+)
+
+type PiperMail struct{}
+
+var _ Forge = PiperMail{}
+
+func (PiperMail) FetchStatus(urls []string) (string, error) {
+ var gitURL string
+ var gitCommits []string
+ for _, u := range urls {
+ if m := reGitHubCommit.FindStringSubmatch(u); m != nil {
+ user := m[1]
+ repo := m[2]
+ hash := m[3]
+
+ gitURL = "https://github.com/" + user + "/" + repo
+ gitCommits = append(gitCommits, hash)
+ }
+ }
+ if len(gitCommits) == 0 {
+ return "", nil
+ }
+ ret := statusMerged
+ tag, err := getGitTagThatContainsAll(gitURL, gitCommits...)
+ if err != nil {
+ return "", err
+ }
+ if tag != "" {
+ ret = fmt.Sprintf(statusReleasedFmt, tag)
+ }
+ return ret, nil
+}
+
+func (PiperMail) FetchSubmittedAt(urls []string) (time.Time, error) {
+ for _, u := range urls {
+ if !strings.Contains(u, "/pipermail/") {
+ continue
+ }
+ htmlStr, err := httpGet(u, nil)
+ if err != nil {
+ return time.Time{}, err
+ }
+ for _, line := range strings.Split(htmlStr, "\n") {
+ if m := rePiperMailDate.FindStringSubmatch(line); m != nil {
+ return time.Parse(time.UnixDate, m[1])
+ }
+ }
+ }
+ return time.Time{}, nil
+}
+
+func (PiperMail) FetchLastUpdated(urls []string) (time.Time, User, error) {
+ var ret time.Time
+ for _, u := range urls {
+ if m := reGitHubCommit.FindStringSubmatch(u); m != nil {
+ user := m[1]
+ repo := m[2]
+ hash := m[3]
+
+ urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/commits/" + hash
+ var obj struct {
+ Commit struct {
+ Author struct {
+ Date time.Time `json:"date"`
+ } `json:"author"`
+ Committer struct {
+ Date time.Time `json:"date"`
+ } `json:"committer"`
+ } `json:"commit"`
+ }
+ if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+ return time.Time{}, User{}, err
+ }
+ if obj.Commit.Author.Date.After(ret) {
+ ret = obj.Commit.Author.Date
+ }
+ if obj.Commit.Committer.Date.After(ret) {
+ ret = obj.Commit.Committer.Date
+ }
+ }
+ }
+ if ret.IsZero() {
+ return time.Time{}, User{}, nil
+ }
+ return ret, User{}, nil
+}
diff --git a/cmd/generate/gerrit.go b/cmd/generate/gerrit.go
deleted file mode 100644
index d2e9b8b..0000000
--- a/cmd/generate/gerrit.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package main
-
-import (
- "encoding"
- "encoding/json"
- "fmt"
- "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
-}
diff --git a/cmd/generate/httpcache.go b/cmd/generate/httpcache.go
index 08153d1..1fb0429 100644
--- a/cmd/generate/httpcache.go
+++ b/cmd/generate/httpcache.go
@@ -105,9 +105,3 @@ func httpGetPaginatedJSON[T any](uStr string, hdr map[string]string, out *[]T, p
return nil
}
-
-func githubPagination(i int) url.Values {
- params := make(url.Values)
- params.Set("page", fmt.Sprintf("%v", i+1))
- return params
-}
diff --git a/cmd/generate/src_contribs.go b/cmd/generate/src_contribs.go
index 2ace0fd..b5345e3 100644
--- a/cmd/generate/src_contribs.go
+++ b/cmd/generate/src_contribs.go
@@ -2,9 +2,7 @@ package main
import (
"fmt"
- "net/url"
"os"
- "regexp"
"strings"
"time"
@@ -96,163 +94,48 @@ func classifyStatus(status string) (string, error) {
}
}
-var (
- reGoLangGerritCL = regexp.MustCompile(`https://go-review\.googlesource\.com/c/([^/?#]+)/\+/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
- reGitHubPR = regexp.MustCompile(`^https://github\.com/([^/?#]+)/([^/?#]+)/pull/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
- reGitHubCommit = regexp.MustCompile(`^https://github\.com/([^/?#]+)/([^/?#]+)/commit/([0-9a-f]+)(?:\?[^#]*)?(?:#.*)?$`)
- reGitLabMR = regexp.MustCompile(`^https://([^/]+)/([^?#]+)/-/merge_requests/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`)
- rePiperMailDate = regexp.MustCompile(`^\s*<I>([^<]+)</I>\s*$`)
-)
-
const (
statusOpen = "open"
statusMerged = "merged, not yet in a release"
statusReleasedFmt = "merged, released in %s"
)
-func (c Contribution) fetchStatus() (string, error) {
- if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil {
- user := m[1]
- repo := m[2]
- prnum := m[3]
-
- urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum
-
- var obj struct {
- // State values are "open" and "closed".
- State string `json:"state"`
- Merged bool `json:"merged"`
- MergeCommitSha string `json:"merge_commit_sha"`
- }
- if err := httpGetJSON(urlStr, nil, &obj); err != nil {
- return "", err
- }
- ret := obj.State
- if obj.Merged {
- ret = statusMerged
- tag, err := getGitTagThatContainsAll("https://github.com/"+user+"/"+repo, obj.MergeCommitSha)
- if err != nil {
- return "", err
- }
- if tag != "" {
- ret = fmt.Sprintf(statusReleasedFmt, tag)
- }
- }
-
- return ret, nil
- }
- if m := reGitLabMR.FindStringSubmatch(c.URLs[0]); m != nil {
- authority := m[1]
- projectID := m[2]
- mrnum := m[3]
+type Forge interface {
+ FetchStatus(urls []string) (string, error)
+ FetchSubmittedAt(urls []string) (time.Time, error)
+ FetchLastUpdated(urls []string) (time.Time, User, error)
+}
- urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum
+var forges = []Forge{
+ // highest precedence
+ Gerrit{}, // must be higher than GitHub because of golang
+ GitHub{},
+ GitLab{},
+ PiperMail{},
+ // lowest precedence
+}
- var obj struct {
- // State values are "opened", "closed", "locked", and "merged".
- State string `json:"state"`
- MergeCommitSha string `json:"merge_commit_sha"`
- SquashCommitSha string `json:"squash_commit_sha"`
- }
- if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+func (c Contribution) fetchStatus() (string, error) {
+ for _, forge := range forges {
+ status, err := forge.FetchStatus(c.URLs)
+ if err != nil {
return "", err
}
-
- ret := obj.State
- if ret == "opened" {
- ret = statusOpen
- }
-
- if ret == "merged" {
- ret = statusMerged
- var mergeCommit string
- if obj.MergeCommitSha != "" {
- mergeCommit = obj.MergeCommitSha
- }
- if obj.SquashCommitSha != "" {
- mergeCommit = obj.SquashCommitSha
- }
- if mergeCommit != "" {
- tag, err := getGitTagThatContainsAll("https://"+authority+"/"+projectID+".git", mergeCommit)
- if err != nil {
- return "", err
- }
- if tag != "" {
- ret = fmt.Sprintf(statusReleasedFmt, tag)
- }
- }
- }
-
- return ret, nil
- }
- if len(c.URLs) > 1 {
- var gitURL string
- var gitCommits []string
- for _, u := range c.URLs[1:] {
- if m := reGitHubCommit.FindStringSubmatch(u); m != nil {
- user := m[1]
- repo := m[2]
- hash := m[3]
-
- gitURL = "https://github.com/" + user + "/" + repo
- gitCommits = append(gitCommits, hash)
- }
- }
- if len(gitCommits) > 0 {
- ret := statusMerged
- tag, err := getGitTagThatContainsAll(gitURL, gitCommits...)
- if err != nil {
- return "", err
- }
- if tag != "" {
- ret = fmt.Sprintf(statusReleasedFmt, tag)
- }
- return ret, nil
+ if status != "" {
+ return status, nil
}
}
return "", fmt.Errorf("idk how to get status for %q", c.URLs[0])
}
func (c Contribution) fetchSubmittedAt() (time.Time, error) {
- if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil {
- user := m[1]
- repo := m[2]
- prnum := m[3]
-
- urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum
-
- var obj struct {
- CreatedAt time.Time `json:"created_at"`
- }
- if err := httpGetJSON(urlStr, nil, &obj); err != nil {
- return time.Time{}, err
- }
- return obj.CreatedAt, nil
- }
- if m := reGitLabMR.FindStringSubmatch(c.URLs[0]); m != nil {
- authority := m[1]
- projectID := m[2]
- mrnum := m[3]
-
- urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum
-
- var obj struct {
- CreatedAt time.Time `json:"created_at"`
- }
- if err := httpGetJSON(urlStr, nil, &obj); err != nil {
- return time.Time{}, err
- }
- return obj.CreatedAt, nil
- }
- if strings.Contains(c.URLs[0], "/pipermail/") {
- htmlStr, err := httpGet(c.URLs[0], nil)
+ for _, forge := range forges {
+ submittedAt, err := forge.FetchSubmittedAt(c.URLs)
if err != nil {
return time.Time{}, err
}
- for _, line := range strings.Split(htmlStr, "\n") {
- if m := rePiperMailDate.FindStringSubmatch(line); m != nil {
- return time.Parse(time.UnixDate, m[1])
- }
+ if !submittedAt.IsZero() {
+ return submittedAt, nil
}
}
return time.Time{}, fmt.Errorf("idk how to get created timestamp for %q", c.URLs[0])
@@ -267,238 +150,14 @@ func withinOneSecond(a, b time.Time) bool {
}
func (c Contribution) fetchLastUpdated() (time.Time, User, error) {
- for _, u := range c.URLs {
- if m := reGoLangGerritCL.FindStringSubmatch(u); m != nil {
- projectID := m[1]
- changeID := m[2]
-
- urlStr := "https://go-review.googlesource.com/changes/" + 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://go-review.googlesource.com/dashboard/%d", message.Author.AccountID)
- break
- }
- }
- return retUpdatedAt, retUser, nil
- }
- }
- if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil {
- user := m[1]
- repo := m[2]
- prnum := m[3]
-
- var obj struct {
- UpdatedAt time.Time `json:"updated_at"`
-
- CreatedAt time.Time `json:"created_at"`
- CreatedBy struct {
- Login string `json:"login"`
- HTMLURL string `json:"html_url"`
- } `json:"user"`
-
- MergedAt time.Time `json:"merged_at"`
- MergedBy struct {
- Login string `json:"login"`
- HTMLURL string `json:"html_url"`
- } `json:"merged_by"`
- }
- if err := httpGetJSON("https://api.github.com/repos/"+user+"/"+repo+"/pulls/"+prnum, nil, &obj); err != nil {
- return time.Time{}, User{}, err
- }
-
- retUpdatedAt := obj.UpdatedAt
- var retUser User
-
- if retUser == (User{}) && withinOneSecond(obj.CreatedAt, retUpdatedAt) {
- retUser.Name = obj.CreatedBy.Login
- retUser.URL = obj.CreatedBy.HTMLURL
- }
- if retUser == (User{}) && withinOneSecond(obj.MergedAt, retUpdatedAt) {
- retUser.Name = obj.MergedBy.Login
- retUser.URL = obj.MergedBy.HTMLURL
- }
- if retUser == (User{}) {
- // "normal" comments
- var comments []struct {
- UpdatedAt time.Time `json:"updated_at"`
- User struct {
- Login string `json:"login"`
- HTMLURL string `json:"html_url"`
- } `json:"user"`
- }
- if err := httpGetPaginatedJSON("https://api.github.com/repos/"+user+"/"+repo+"/issues/"+prnum+"/comments", nil, &comments, githubPagination); err != nil {
- return time.Time{}, User{}, err
- }
- for _, comment := range comments {
- if withinOneSecond(comment.UpdatedAt, retUpdatedAt) {
- retUser.Name = comment.User.Login
- retUser.URL = comment.User.HTMLURL
- break
- }
- }
- }
- if retUser == (User{}) {
- // comments on a specific part of the diff
- var reviewComments []struct {
- UpdatedAt time.Time `json:"updated_at"`
- User struct {
- Login string `json:"login"`
- HTMLURL string `json:"html_url"`
- } `json:"user"`
- }
- if err := httpGetPaginatedJSON("https://api.github.com/repos/"+user+"/"+repo+"/pulls/"+prnum+"/comments", nil, &reviewComments, githubPagination); err != nil {
- return time.Time{}, User{}, err
- }
- for _, comment := range reviewComments {
- if withinOneSecond(comment.UpdatedAt, retUpdatedAt) {
- retUser.Name = comment.User.Login
- retUser.URL = comment.User.HTMLURL
- break
- }
- }
- }
- if retUser == (User{}) {
- var events []struct {
- CreatedAt time.Time `json:"created_at"`
- Actor struct {
- Login string `json:"login"`
- HTMLURL string `json:"html_url"`
- } `json:"actor"`
- }
- if err := httpGetJSON("https://api.github.com/repos/"+user+"/"+repo+"/issues/"+prnum+"/events", nil, &events); err != nil {
- return time.Time{}, User{}, err
- }
- for _, event := range events {
- if withinOneSecond(event.CreatedAt, retUpdatedAt) {
- retUser.Name = event.Actor.Login
- retUser.URL = event.Actor.HTMLURL
- break
- }
- }
- }
-
- return retUpdatedAt, retUser, nil
- }
- if m := reGitLabMR.FindStringSubmatch(c.URLs[0]); m != nil {
- authority := m[1]
- projectID := m[2]
- mrnum := m[3]
-
- urlStr := "https://" + authority + "/api/v4/projects/" + url.QueryEscape(projectID) + "/merge_requests/" + mrnum
-
- var obj struct {
- ID int `json:"id"`
-
- UpdatedAt time.Time `json:"updated_at"`
-
- CreatedAt time.Time `json:"created_at"`
- CreatedBy struct {
- Username string `json:"username"`
- WebURL string `json:"web_url"`
- } `json:"author"`
-
- MergedAt time.Time `json:"merged_at"`
- MergedBy struct {
- Username string `json:"username"`
- WebURL string `json:"web_url"`
- } `json:"merged_by"`
- }
- if err := httpGetJSON(urlStr, nil, &obj); err != nil {
+ for _, forge := range forges {
+ updatedAt, updatedBy, err := forge.FetchLastUpdated(c.URLs)
+ if err != nil {
return time.Time{}, User{}, err
}
-
- retUpdatedAt := obj.UpdatedAt
- var retUser User
-
- if retUser == (User{}) && withinOneSecond(obj.CreatedAt, retUpdatedAt) {
- retUser.Name = obj.CreatedBy.Username
- retUser.URL = obj.CreatedBy.WebURL
- }
- if retUser == (User{}) && withinOneSecond(obj.MergedAt, retUpdatedAt) {
- retUser.Name = obj.MergedBy.Username
- retUser.URL = obj.MergedBy.WebURL
- }
- if retUser == (User{}) {
- var notes struct {
- Notes []struct {
- UpdatedAt time.Time `json:"updated_at"`
- Author struct {
- Username string `json:"username"`
- WebURL string `json:"web_url"`
- } `json:"author"`
- } `json:"notes"`
- }
- if err := httpGetJSON(fmt.Sprintf("https://%s/%s/noteable/merge_request/%d/notes", authority, projectID, obj.ID), map[string]string{"X-Last-Fetched-At": "0"}, &notes); err != nil {
- return time.Time{}, User{}, err
- }
- for _, note := range notes.Notes {
- if withinOneSecond(note.UpdatedAt, retUpdatedAt) {
- retUser.Name = note.Author.Username
- retUser.URL = note.Author.WebURL
- break
- }
- }
+ if !updatedAt.IsZero() {
+ return updatedAt, updatedBy, nil
}
-
- return retUpdatedAt, retUser, nil
}
-
- var ret time.Time
- if len(c.URLs) > 1 {
- for _, u := range c.URLs[1:] {
- if m := reGitHubCommit.FindStringSubmatch(u); m != nil {
- user := m[1]
- repo := m[2]
- hash := m[3]
-
- urlStr := "https://api.github.com/repos/" + user + "/" + repo + "/commits/" + hash
- var obj struct {
- Commit struct {
- Author struct {
- Date time.Time `json:"date"`
- } `json:"author"`
- Committer struct {
- Date time.Time `json:"date"`
- } `json:"committer"`
- } `json:"commit"`
- }
- if err := httpGetJSON(urlStr, nil, &obj); err != nil {
- return time.Time{}, User{}, err
- }
- if obj.Commit.Author.Date.After(ret) {
- ret = obj.Commit.Author.Date
- }
- if obj.Commit.Committer.Date.After(ret) {
- ret = obj.Commit.Committer.Date
- }
- }
- }
- }
- if !ret.IsZero() {
- return ret, User{}, nil
- }
-
return time.Time{}, User{}, nil //fmt.Errorf("idk how to get updated timestamp for %q", c.URLs[0])
}