diff options
author | Luke T. Shumaker <lukeshu@lukeshu.com> | 2024-05-19 02:29:46 -0600 |
---|---|---|
committer | Luke T. Shumaker <lukeshu@lukeshu.com> | 2024-05-19 02:29:46 -0600 |
commit | 632b189cfed31bf1a1f1edf0c1ae69f294ef4123 (patch) | |
tree | b375ecf1391247bcd4439b7880a69c86b042aa42 /cmd/generate | |
parent | 75d2ab4f4d2415b1ecead2361acddb1e6d6392dd (diff) |
cmd/generate: Factor forge_*.go files out of src_contribs.go
Diffstat (limited to 'cmd/generate')
-rw-r--r-- | cmd/generate/forge_gerrit.go | 112 | ||||
-rw-r--r-- | cmd/generate/forge_github.go | 186 | ||||
-rw-r--r-- | cmd/generate/forge_gitlab.go | 160 | ||||
-rw-r--r-- | cmd/generate/forge_pipermail.go | 98 | ||||
-rw-r--r-- | cmd/generate/gerrit.go | 54 | ||||
-rw-r--r-- | cmd/generate/httpcache.go | 6 | ||||
-rw-r--r-- | cmd/generate/src_contribs.go | 397 |
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"}, ¬es); 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"}, ¬es); 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]) } |