package main import ( "fmt" "os" "strings" "time" "sigs.k8s.io/yaml" ) type User struct { Name string `json:"name"` URL string `json:"url"` } type Contribution struct { ID string URLs []string `json:"urls"` Tags []string `json:"tags"` SponsoredBy string `json:"sponsored-by"` Desc string `json:"desc"` SubmittedAt time.Time `json:"submitted-at"` LastUpdatedAt time.Time `json:"last-updated-at"` LastUpdatedBy User `json:"last-updated-by"` Status string `json:"status"` StatusClass string `json:"-"` } func ReadContribs(filename string) ([]Contribution, error) { bs, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("contribs: %q: %w", filename, err) } var ret []Contribution if err := yaml.UnmarshalStrict(bs, &ret); err != nil { return nil, fmt.Errorf("contribs: %q: %w", filename, err) } for i := range ret { contrib := ret[i] if err := contrib.Fill(); err != nil { return nil, fmt.Errorf("contribs: %q: %w", filename, err) } ret[i] = contrib } return ret, nil } func (c *Contribution) Fill() error { var err error if c.SubmittedAt.IsZero() { c.SubmittedAt, err = c.fetchSubmittedAt() if err != nil { return err } } if c.LastUpdatedAt.IsZero() { c.LastUpdatedAt, c.LastUpdatedBy, err = c.fetchLastUpdated() if err != nil { return err } } if c.Status == "" { c.Status, err = c.fetchStatus() if err != nil { return err } } c.StatusClass, err = classifyStatus(c.Status) if err != nil { return err } for _, u := range c.URLs { if m := reGoogleGerritCL.FindStringSubmatch(u); m != nil && m[1] == "go-review.googlesource.com" { c.URLs = append(c.URLs, "https://golang.org/cl/"+m[3]) } } return nil } func classifyStatus(status string) (string, error) { switch { case strings.Contains(status, "released") || strings.Contains(status, "deployed"): return "released", nil case strings.Contains(status, "merged"): return "merged", nil case strings.Contains(status, "open"): return "open", nil case strings.Contains(status, "closed") || strings.Contains(status, "locked"): return "closed", nil default: return "", fmt.Errorf("unrecognized status string: %q", status) } } const ( statusOpen = "open" statusMerged = "merged, not yet in a release" statusReleasedFmt = "merged, released in %s" ) type Forge interface { FetchStatus(urls []string) (string, error) FetchSubmittedAt(urls []string) (time.Time, error) FetchLastUpdated(urls []string) (time.Time, User, error) } var forges = []Forge{ // precedence only matters for .FetchStatus. // highest precedence Gerrit{}, GitHub{}, GitLab{}, Forgejo{"codeberg.org"}, PartPiperMail{}, PartGit{}, // lowest precedence } func fetchPerURLStatus(urls []string, perURL func(string) (string, error)) (string, error) { for _, u := range urls { status, err := perURL(u) if err != nil { return "", err } if status != "" { return status, nil } } return "", nil } func (c Contribution) fetchStatus() (string, error) { for _, forge := range forges { status, err := forge.FetchStatus(c.URLs) if err != nil { return "", err } if status != "" { return status, nil } } return "", fmt.Errorf("idk how to get status for %q", c.URLs[0]) } func fetchPerURLSubmittedAt(urls []string, perURL func(string) (time.Time, error)) (time.Time, error) { var ret time.Time for _, u := range urls { submittedAt, err := perURL(u) if err != nil { return time.Time{}, err } if !submittedAt.IsZero() && (ret.IsZero() || submittedAt.Before(ret)) { ret = submittedAt } } return ret, nil } func (c Contribution) fetchSubmittedAt() (time.Time, error) { var ret time.Time for _, forge := range forges { submittedAt, err := forge.FetchSubmittedAt(c.URLs) if err != nil { return time.Time{}, err } if !submittedAt.IsZero() && (ret.IsZero() || submittedAt.Before(ret)) { ret = submittedAt } } if !ret.IsZero() { return ret, nil } return time.Time{}, fmt.Errorf("idk how to get created timestamp for %q", c.URLs[0]) } func withinOneSecond(a, b time.Time) bool { d := a.Sub(b) if d < 0 { d = -d } return d <= time.Second } func fetchPerURLLastUpdated(urls []string, perURL func(string) (time.Time, User, error)) (time.Time, User, error) { var ret struct { time.Time User } for _, u := range urls { updatedAt, updatedBy, err := perURL(u) if err != nil { return time.Time{}, User{}, err } if !updatedAt.IsZero() && (ret.Time.IsZero() || updatedAt.After(ret.Time)) { ret.Time, ret.User = updatedAt, updatedBy } } return ret.Time, ret.User, nil } func (c Contribution) fetchLastUpdated() (time.Time, User, error) { var ret struct { time.Time User } for _, forge := range forges { updatedAt, updatedBy, err := forge.FetchLastUpdated(c.URLs) if err != nil { return time.Time{}, User{}, err } if !updatedAt.IsZero() && (ret.Time.IsZero() || updatedAt.After(ret.Time)) { ret.Time, ret.User = updatedAt, updatedBy } } if !ret.Time.IsZero() { return ret.Time, ret.User, nil } return time.Time{}, User{}, nil //fmt.Errorf("idk how to get updated timestamp for %q", c.URLs[0]) }