package main import ( "fmt" "net/url" "regexp" "time" ) var ( reGitHubPR = regexp.MustCompile(`^https://github\.com/([^/?#]+)/([^/?#]+)/pull/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`) reGitHubCommit = regexp.MustCompile(`^https://github\.com/([^/?#]+)/([^/?#]+)/commit/([0-9a-f]+)(?:\?[^#]*)?(?:#.*)?$`) ) 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) { // PR 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 } // Commits from a non-PR 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 { ret := statusMerged tag, err := getGitTagThatContainsAll(gitURL, gitCommits...) if err != nil { return "", err } if tag != "" { ret = fmt.Sprintf(statusReleasedFmt, tag) } return ret, nil } // Nope 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) { // PR 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 { 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(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.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 } // Commits from a non-PR { 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 } } // Nope return time.Time{}, User{}, nil }