summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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/gitcache.go4
-rw-r--r--cmd/generate/httpcache.go48
-rw-r--r--cmd/generate/imworkingon.html.tmpl3
-rw-r--r--cmd/generate/main.go30
-rw-r--r--cmd/generate/src_contribs.go309
-rw-r--r--cmd/generate/src_mastodon.go4
-rw-r--r--imworkingon/contribs.yml11
l---------public/dump2
-rw-r--r--public/imworkingon/imworkingon.scss2
l---------public/resume2
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"}, &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 = "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