diff options
-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/gitcache.go | 4 | ||||
-rw-r--r-- | cmd/generate/httpcache.go | 48 | ||||
-rw-r--r-- | cmd/generate/imworkingon.html.tmpl | 3 | ||||
-rw-r--r-- | cmd/generate/main.go | 30 | ||||
-rw-r--r-- | cmd/generate/src_contribs.go | 309 | ||||
-rw-r--r-- | cmd/generate/src_mastodon.go | 4 | ||||
-rw-r--r-- | imworkingon/contribs.yml | 11 | ||||
l--------- | public/dump | 2 | ||||
-rw-r--r-- | public/imworkingon/imworkingon.scss | 2 | ||||
l--------- | public/resume | 2 |
14 files changed, 666 insertions, 305 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..56f1401 --- /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"` + Path string `json:"path"` + } `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 = "https://" + authority + note.Author.Path + 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/gitcache.go b/cmd/generate/gitcache.go index 7caf024..844408d 100644 --- a/cmd/generate/gitcache.go +++ b/cmd/generate/gitcache.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "strings" + "time" "git.mothstuff.lol/lukeshu/eclipse/lib/gitcache" ) @@ -12,7 +13,8 @@ import ( var gitFetched = map[string]struct{}{} var gitCache = &gitcache.Cache{ - Dir: ".git-cache", + Dir: ".git-cache", + MinPeriod: 1 * time.Hour, } func withGit(u string, fn func(dir string) error) error { diff --git a/cmd/generate/httpcache.go b/cmd/generate/httpcache.go index 04762e3..1fb0429 100644 --- a/cmd/generate/httpcache.go +++ b/cmd/generate/httpcache.go @@ -8,29 +8,47 @@ import ( "net/url" "os" "path/filepath" + "sort" ) var httpCache = map[string]string{} -func httpGet(u string) (string, error) { - if cache, ok := httpCache[u]; ok { +func httpGet(u string, hdr map[string]string) (string, error) { + cacheKey := url.QueryEscape(u) + hdrKeys := make([]string, 0, len(hdr)) + for k := range hdr { + hdrKeys = append(hdrKeys, http.CanonicalHeaderKey(k)) + } + sort.Strings(hdrKeys) + for _, k := range hdrKeys { + cacheKey += "|" + url.QueryEscape(k) + ":" + url.QueryEscape(hdr[k]) + } + + if cache, ok := httpCache[cacheKey]; ok { fmt.Printf("CACHE-GET %q\n", u) return cache, nil } if err := os.Mkdir(".http-cache", 0777); err != nil && !os.IsExist(err) { return "", err } - cacheFile := filepath.Join(".http-cache", url.QueryEscape(u)) + cacheFile := filepath.Join(".http-cache", cacheKey) if bs, err := os.ReadFile(cacheFile); err == nil { - httpCache[u] = string(bs) + httpCache[cacheKey] = string(bs) fmt.Printf("CACHE-GET %q\n", u) - return httpCache[u], nil + return httpCache[cacheKey], nil } else if !os.IsNotExist(err) { return "", err } fmt.Printf("GET %q...", u) - resp, err := http.Get(u) + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return "", err + } + for k, v := range hdr { + req.Header.Add(k, v) + } + resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Printf(" err\n") return "", err @@ -48,19 +66,19 @@ func httpGet(u string) (string, error) { if err := os.WriteFile(cacheFile, bs, 0666); err != nil { return "", err } - httpCache[u] = string(bs) - return httpCache[u], nil + httpCache[cacheKey] = string(bs) + return httpCache[cacheKey], nil } -func httpGetJSON(u string, out any) error { - str, err := httpGet(u) +func httpGetJSON(u string, hdr map[string]string, out any) error { + str, err := httpGet(u, hdr) if err != nil { return err } return json.Unmarshal([]byte(str), out) } -func httpGetPaginatedJSON[T any](uStr string, out *[]T, pageFn func(i int) url.Values) error { +func httpGetPaginatedJSON[T any](uStr string, hdr map[string]string, out *[]T, pageFn func(i int) url.Values) error { u, err := url.Parse(uStr) if err != nil { return err @@ -75,7 +93,7 @@ func httpGetPaginatedJSON[T any](uStr string, out *[]T, pageFn func(i int) url.V u.RawQuery = query.Encode() var resp []T - if err := httpGetJSON(u.String(), &resp); err != nil { + if err := httpGetJSON(u.String(), hdr, &resp); err != nil { return err } fmt.Printf(" -> %d records\n", len(resp)) @@ -87,9 +105,3 @@ func httpGetPaginatedJSON[T any](uStr string, out *[]T, pageFn func(i int) url.V 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/imworkingon.html.tmpl b/cmd/generate/imworkingon.html.tmpl index 85a56e1..13444be 100644 --- a/cmd/generate/imworkingon.html.tmpl +++ b/cmd/generate/imworkingon.html.tmpl @@ -21,6 +21,9 @@ <li><a href="#standups">Daily statuses</a></li> </ol> </nav> + + <p>The "In-progress work" and "Completed work" sections do <em>not</em> include routine maintenance on <a href="https://parabola.nu">Parabola GNU/Linux-libre</a>, which is also a solid chunk of what I do.</p> + <p>If you find this work valuable, please consider <a class="em" href="../sponsor/">sponsoring me</a>.</p> </section> diff --git a/cmd/generate/main.go b/cmd/generate/main.go index e322e5c..f6171b0 100644 --- a/cmd/generate/main.go +++ b/cmd/generate/main.go @@ -23,10 +23,6 @@ func MarkdownToHTML(md string) (template.HTML, error) { return template.HTML(html.String()), nil } -var githubProjects = map[string]string{ - "flori/json": "ruby-json", -} - func main() { if err := mainWithError(); err != nil { fmt.Fprintf(os.Stderr, "%s: error: %v\n", os.Args[0], err) @@ -125,12 +121,34 @@ func mainWithError() error { } } } + // Now try to synthesize an upstream. if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil { user := m[1] repo := m[2] - return Upstream{URLs: []string{c.URLs[0]}, Name: user + "/" + repo} + return Upstream{ + URLs: []string{"https://github.com/" + user + "/" + repo}, + Name: user + "/" + repo, + } + } + if m := reGitLabMR.FindStringSubmatch(c.URLs[0]); m != nil { + authority := m[1] + projectID := m[2] + if authority == "gitlab.archlinux.org" && strings.HasPrefix(projectID, "archlinux/packaging/packages/") { + return Upstream{ + URLs: []string{"https://" + authority + "/" + projectID}, + Name: strings.Replace(projectID, "/packages/", "/", 1), + } + } + return Upstream{ + URLs: []string{"https://" + authority + "/" + projectID}, + Name: projectID, + } + } + // :( + return Upstream{ + URLs: []string{c.URLs[0]}, + Name: "???", } - return Upstream{URLs: []string{c.URLs[0]}, Name: "???"} }, }). Parse(htmlTmplStr)) diff --git a/cmd/generate/src_contribs.go b/cmd/generate/src_contribs.go index 6db6764..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" @@ -75,7 +73,7 @@ func (c *Contribution) Fill() error { } for _, u := range c.URLs { if m := reGoLangGerritCL.FindStringSubmatch(u); m != nil { - c.URLs = append(c.URLs, "https://golang.org/cl/"+m[1]) + c.URLs = append(c.URLs, "https://golang.org/cl/"+m[2]) } } return nil @@ -96,305 +94,70 @@ 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, &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, &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, &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, &obj); err != nil { - return time.Time{}, err - } - return obj.CreatedAt, nil - } - if strings.Contains(c.URLs[0], "/pipermail/") { - htmlStr, err := httpGet(c.URLs[0]) + 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]) } -func (c Contribution) fetchLastUpdated() (time.Time, User, error) { - 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"` - 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, &obj); err != nil { - return time.Time{}, User{}, err - } - - retUpdatedAt := obj.UpdatedAt - var retUser User - - if 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", &comments, githubPagination); err != nil { - return time.Time{}, User{}, err - } - for _, comment := range comments { - if comment.UpdatedAt == retUpdatedAt || comment.UpdatedAt.Add(1*time.Second) == 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", &reviewComments, githubPagination); err != nil { - return time.Time{}, User{}, err - } - for _, comment := range reviewComments { - if 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", &events); err != nil { - return time.Time{}, User{}, err - } - for _, event := range events { - if event.CreatedAt == retUpdatedAt { - retUser.Name = event.Actor.Login - retUser.URL = event.Actor.HTMLURL - break - } - } - } - - return retUpdatedAt, retUser, nil +func withinOneSecond(a, b time.Time) bool { + d := a.Sub(b) + if d < 0 { + d = -d } - 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 + return d <= time.Second +} - var obj struct { - UpdatedAt time.Time `json:"updated_at"` - } - if err := httpGetJSON(urlStr, &obj); err != nil { +func (c Contribution) fetchLastUpdated() (time.Time, User, error) { + for _, forge := range forges { + updatedAt, updatedBy, err := forge.FetchLastUpdated(c.URLs) + if err != nil { return time.Time{}, User{}, err } - return obj.UpdatedAt, User{}, 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, &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 !updatedAt.IsZero() { + return updatedAt, updatedBy, nil } } - 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]) } diff --git a/cmd/generate/src_mastodon.go b/cmd/generate/src_mastodon.go index b4b54a8..52dcfa4 100644 --- a/cmd/generate/src_mastodon.go +++ b/cmd/generate/src_mastodon.go @@ -19,12 +19,12 @@ func ReadStandups(server, username string) ([]*MastodonStatus, error) { var account struct { ID string `json:"id"` } - if err := httpGetJSON(server+"/api/v1/accounts/lookup?acct="+username, &account); err != nil { + if err := httpGetJSON(server+"/api/v1/accounts/lookup?acct="+username, nil, &account); err != nil { return nil, err } var statuses []*MastodonStatus - if err := httpGetPaginatedJSON(server+"/api/v1/accounts/"+account.ID+"/statuses", &statuses, func(_ int) url.Values { + if err := httpGetPaginatedJSON(server+"/api/v1/accounts/"+account.ID+"/statuses", nil, &statuses, func(_ int) url.Values { params := make(url.Values) params.Set("tagged", "DailyStandUp") params.Set("exclude_reblogs", "true") diff --git a/imworkingon/contribs.yml b/imworkingon/contribs.yml index 6394421..56c50ce 100644 --- a/imworkingon/contribs.yml +++ b/imworkingon/contribs.yml @@ -3,7 +3,7 @@ desc: | ruby-json contains code that is not Free under the FSF's definition, not Open Source under the OSI's definition, and not - GPL-compatible. This has coused much consternation among folks + GPL-compatible. This has caused much consternation among folks who care about any of those 3 things. This PR replaces that non-Free code with Free code, removing @@ -71,7 +71,7 @@ implementions for other filesystems take. - urls: [https://github.com/liberapay/liberapay.com/pull/2334] tags: [federated] - status: merged+deployed + status: merged + deployed desc: | When managing your profile, Liberapay nominally supports using your [Libravatar federated avatar](https://www.libravatar.org/) as @@ -124,3 +124,10 @@ Just a minor touch-up to `configure.ac` that I noticed could be made when updating Parabola's `pcr/awf` package. Parabola makes other software better! +- urls: [https://gitlab.archlinux.org/archlinux/packaging/packages/systemd/-/merge_requests/12] + tags: [Parabola, init-freedom] + desc: | + Some changes to the way that Arch Linux packages systemd that + should make it easier for distros downstream of Arch (certainly + Parabola, hopefully Artix) to provide init-freedom and support + other init systems. diff --git a/public/dump b/public/dump index 271a203..507d527 120000 --- a/public/dump +++ b/public/dump @@ -1 +1 @@ -/home/lukeshu/blog/dump/
\ No newline at end of file +/home/lukeshu/dump/
\ No newline at end of file diff --git a/public/imworkingon/imworkingon.scss b/public/imworkingon/imworkingon.scss index c5adece..5405fef 100644 --- a/public/imworkingon/imworkingon.scss +++ b/public/imworkingon/imworkingon.scss @@ -7,7 +7,7 @@ article { border: solid 1px #333333; - border-radius: 1em; + border-radius: 8px; margin: 0.5em; } diff --git a/public/resume b/public/resume index 1201e40..a3e6b53 120000 --- a/public/resume +++ b/public/resume @@ -1 +1 @@ -/home/lukeshu/blog/resume/
\ No newline at end of file +/home/lukeshu/resume/
\ No newline at end of file |