mirror of
https://github.com/samsonjs/retrogit.git
synced 2026-03-25 09:25:49 +00:00
322 lines
9.1 KiB
Go
322 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"google.golang.org/appengine/v2/log"
|
|
|
|
"github.com/google/go-github/github"
|
|
)
|
|
|
|
const (
|
|
CommitDisplayDateFormat = "3:04pm"
|
|
CommitDisplayDateFullFormat = "Monday January 2 3:04pm"
|
|
DigestDisplayDateFormat = "January 2, 2006"
|
|
DigestDisplayShortDateFormat = "January 2"
|
|
DigestDisplayDayOfWeekFormat = "Monday"
|
|
)
|
|
|
|
type DigestCommit struct {
|
|
DisplaySHA string
|
|
URL string
|
|
Title string
|
|
Message string
|
|
PushDate time.Time
|
|
CommitDate time.Time
|
|
}
|
|
|
|
func safeFormattedDate(date string) string {
|
|
// Insert zero-width spaces every few characters so that Apple Data
|
|
// Detectors and Gmail's calendar event dection don't pick up on these
|
|
// dates.
|
|
var buffer bytes.Buffer
|
|
dateLength := len(date)
|
|
for i := 0; i < dateLength; i += 2 {
|
|
if i == dateLength-1 {
|
|
buffer.WriteString(date[i : i+1])
|
|
} else {
|
|
buffer.WriteString(date[i : i+2])
|
|
if date[i] != ' ' && date[i+1] != ' ' && i < dateLength-2 {
|
|
buffer.WriteString("\u200b")
|
|
}
|
|
}
|
|
}
|
|
return buffer.String()
|
|
}
|
|
|
|
func newDigestCommit(commit *github.RepositoryCommit, repo *Repo, location *time.Location) DigestCommit {
|
|
messagePieces := strings.SplitN(*commit.Commit.Message, "\n", 2)
|
|
title := messagePieces[0]
|
|
message := ""
|
|
if len(messagePieces) == 2 {
|
|
message = messagePieces[1]
|
|
}
|
|
return DigestCommit{
|
|
DisplaySHA: (*commit.SHA)[:7],
|
|
URL: fmt.Sprintf("https://github.com/%s/commit/%s", *repo.FullName, *commit.SHA),
|
|
Title: title,
|
|
Message: message,
|
|
PushDate: commit.Commit.Committer.Date.In(location),
|
|
CommitDate: commit.Commit.Author.Date.In(location),
|
|
}
|
|
}
|
|
|
|
func (commit DigestCommit) DisplayDate() string {
|
|
// Prefer the date the commit was pushed, since that's what GitHub filters
|
|
// and sorts by.
|
|
return safeFormattedDate(commit.PushDate.Format(CommitDisplayDateFormat))
|
|
}
|
|
|
|
func (commit DigestCommit) WeeklyDisplayDate() string {
|
|
return safeFormattedDate(commit.PushDate.Format(CommitDisplayDateFullFormat))
|
|
}
|
|
|
|
func (commit DigestCommit) DisplayDateTooltip() string {
|
|
// But show the full details in a tooltip
|
|
return fmt.Sprintf(
|
|
"Pushed at %s\nCommited at %s",
|
|
commit.PushDate.Format(CommitDisplayDateFullFormat),
|
|
commit.CommitDate.Format(CommitDisplayDateFullFormat))
|
|
}
|
|
|
|
type RepoDigest struct {
|
|
Repo *Repo
|
|
Commits []DigestCommit
|
|
}
|
|
|
|
// sort.Interface implementation for sorting RepoDigests.
|
|
type ByRepoFullName []*RepoDigest
|
|
|
|
func (a ByRepoFullName) Len() int { return len(a) }
|
|
func (a ByRepoFullName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a ByRepoFullName) Less(i, j int) bool { return *a[i].Repo.FullName < *a[j].Repo.FullName }
|
|
|
|
type IntervalDigest struct {
|
|
yearDelta int
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
Weekly bool
|
|
RepoDigests []*RepoDigest
|
|
repos []*Repo
|
|
}
|
|
|
|
func (digest *IntervalDigest) Empty() bool {
|
|
for i := range digest.RepoDigests {
|
|
if len(digest.RepoDigests[i].Commits) > 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (digest *IntervalDigest) Header() string {
|
|
if digest.yearDelta == -1 {
|
|
return "1 Year Ago"
|
|
}
|
|
return fmt.Sprintf("%d Years Ago", -digest.yearDelta)
|
|
}
|
|
|
|
func (digest *IntervalDigest) Description() string {
|
|
commitCount := 0
|
|
for i := range digest.RepoDigests {
|
|
commitCount += len(digest.RepoDigests[i].Commits)
|
|
}
|
|
var formattedCommitCount string
|
|
if commitCount == 0 {
|
|
formattedCommitCount = "no commits"
|
|
} else if commitCount == 1 {
|
|
formattedCommitCount = "1 commit"
|
|
} else {
|
|
formattedCommitCount = fmt.Sprintf("%d commits", commitCount)
|
|
}
|
|
repoCount := len(digest.RepoDigests)
|
|
var formattedRepoCount string
|
|
if repoCount == 1 {
|
|
formattedRepoCount = "1 repository"
|
|
} else {
|
|
formattedRepoCount = fmt.Sprintf("%d repositories", repoCount)
|
|
}
|
|
|
|
if !digest.Weekly {
|
|
return fmt.Sprintf("%s was a %s. You had %s in %s that day.",
|
|
safeFormattedDate(digest.StartTime.Format(DigestDisplayDateFormat)),
|
|
safeFormattedDate(digest.StartTime.Format(DigestDisplayDayOfWeekFormat)),
|
|
formattedCommitCount,
|
|
formattedRepoCount)
|
|
}
|
|
|
|
formattedEndTime := digest.EndTime.Format(DigestDisplayDateFormat)
|
|
var formattedStartTime string
|
|
if digest.StartTime.Year() == digest.EndTime.Year() {
|
|
formattedStartTime = digest.StartTime.Format(DigestDisplayShortDateFormat)
|
|
} else {
|
|
formattedStartTime = digest.StartTime.Format(DigestDisplayDateFormat)
|
|
}
|
|
return fmt.Sprintf("You had %s in %s the week of %s to %s.",
|
|
formattedCommitCount,
|
|
formattedRepoCount,
|
|
safeFormattedDate(formattedStartTime),
|
|
safeFormattedDate(formattedEndTime))
|
|
}
|
|
|
|
type Digest struct {
|
|
User *github.User
|
|
TimezoneLocation *time.Location
|
|
IntervalDigests []*IntervalDigest
|
|
CommitCount int
|
|
RepoErrors map[string]error
|
|
}
|
|
|
|
func newDigest(c context.Context, githubClient *github.Client, account *Account) (*Digest, error) {
|
|
user, _, err := githubClient.Users.Get(c, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repos, err := getRepos(c, githubClient, account, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldestDigestTime := repos.OldestVintage.In(account.TimezoneLocation)
|
|
intervalDigests := make([]*IntervalDigest, 0)
|
|
now := time.Now().In(account.TimezoneLocation)
|
|
for yearDelta := -1; ; yearDelta-- {
|
|
digestStartTime := time.Date(now.Year()+yearDelta, now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
if digestStartTime.Before(oldestDigestTime) {
|
|
break
|
|
}
|
|
daysInDigest := 1
|
|
if account.Frequency == "weekly" {
|
|
daysInDigest = 7
|
|
}
|
|
digestEndTime := digestStartTime.AddDate(0, 0, daysInDigest).Add(-time.Second)
|
|
|
|
// Only look at repos that may have activity in the digest interval.
|
|
var intervalRepos []*Repo
|
|
for _, repo := range repos.AllRepos {
|
|
if repo.IncludeInDigest && repo.Vintage.Before(digestEndTime) && repo.PushedAt != nil &&
|
|
repo.PushedAt.After(digestStartTime) {
|
|
intervalRepos = append(intervalRepos, repo)
|
|
}
|
|
}
|
|
|
|
intervalDigests = append(intervalDigests, &IntervalDigest{
|
|
yearDelta: yearDelta,
|
|
repos: intervalRepos,
|
|
RepoDigests: make([]*RepoDigest, 0, len(intervalRepos)),
|
|
StartTime: digestStartTime,
|
|
EndTime: digestEndTime,
|
|
Weekly: account.Frequency == "weekly",
|
|
})
|
|
}
|
|
|
|
digest := &Digest{
|
|
User: user,
|
|
TimezoneLocation: account.TimezoneLocation,
|
|
IntervalDigests: intervalDigests,
|
|
CommitCount: 0,
|
|
RepoErrors: make(map[string]error),
|
|
}
|
|
|
|
digest.fetch(c, githubClient)
|
|
for repoFullName, err := range digest.RepoErrors {
|
|
log.Errorf(c, "Error fetching %s: %s", repoFullName, err.Error())
|
|
}
|
|
return digest, nil
|
|
}
|
|
|
|
func (digest *Digest) fetch(c context.Context, githubClient *github.Client) {
|
|
type RepoDigestResponse struct {
|
|
intervalDigest *IntervalDigest
|
|
repo *Repo
|
|
repoDigest *RepoDigest
|
|
err error
|
|
}
|
|
fetchCount := 0
|
|
ch := make(chan *RepoDigestResponse)
|
|
for _, intervalDigest := range digest.IntervalDigests {
|
|
for _, repo := range intervalDigest.repos {
|
|
go func(intervalDigest *IntervalDigest, repo *Repo) {
|
|
commits := make([]*github.RepositoryCommit, 0)
|
|
page := 1
|
|
for {
|
|
pageCommits, response, err := githubClient.Repositories.ListCommits(
|
|
c,
|
|
*repo.Owner.Login,
|
|
*repo.Name,
|
|
&github.CommitsListOptions{
|
|
ListOptions: github.ListOptions{
|
|
Page: page,
|
|
PerPage: 100,
|
|
},
|
|
Author: *digest.User.Login,
|
|
Since: intervalDigest.StartTime.UTC(),
|
|
Until: intervalDigest.EndTime.UTC(),
|
|
})
|
|
if err != nil {
|
|
ch <- &RepoDigestResponse{intervalDigest, repo, nil, err}
|
|
return
|
|
}
|
|
commits = append(commits, pageCommits...)
|
|
if response.NextPage == 0 {
|
|
break
|
|
}
|
|
page = response.NextPage
|
|
}
|
|
digestCommits := make([]DigestCommit, len(commits))
|
|
for i := range commits {
|
|
digestCommits[len(commits)-i-1] = newDigestCommit(commits[i], repo, digest.TimezoneLocation)
|
|
}
|
|
ch <- &RepoDigestResponse{intervalDigest, repo, &RepoDigest{repo, digestCommits}, nil}
|
|
}(intervalDigest, repo)
|
|
fetchCount++
|
|
}
|
|
}
|
|
for i := 0; i < fetchCount; i++ {
|
|
select {
|
|
case r := <-ch:
|
|
if r.err != nil {
|
|
digest.RepoErrors[*r.repo.FullName] = r.err
|
|
continue
|
|
}
|
|
if len(r.repoDigest.Commits) > 0 {
|
|
r.intervalDigest.RepoDigests = append(r.intervalDigest.RepoDigests, r.repoDigest)
|
|
digest.CommitCount += len(r.repoDigest.Commits)
|
|
}
|
|
}
|
|
}
|
|
nonEmptyIntervalDigests := make([]*IntervalDigest, 0, len(digest.IntervalDigests))
|
|
for _, intervalDigest := range digest.IntervalDigests {
|
|
if !intervalDigest.Empty() {
|
|
nonEmptyIntervalDigests = append(nonEmptyIntervalDigests, intervalDigest)
|
|
sort.Sort(ByRepoFullName(intervalDigest.RepoDigests))
|
|
}
|
|
}
|
|
digest.IntervalDigests = nonEmptyIntervalDigests
|
|
}
|
|
|
|
func (digest *Digest) Empty() bool {
|
|
return len(digest.IntervalDigests) == 0
|
|
}
|
|
|
|
func (digest *Digest) Redact() {
|
|
for _, intervalDigest := range digest.IntervalDigests {
|
|
for _, repoDigest := range intervalDigest.RepoDigests {
|
|
*repoDigest.Repo.HTMLURL = "https://redacted"
|
|
*repoDigest.Repo.FullName = "redacted/redacted"
|
|
for i := range repoDigest.Commits {
|
|
commit := &repoDigest.Commits[i]
|
|
commit.DisplaySHA = "0000000"
|
|
commit.URL = "https://redacted"
|
|
commit.Title = "Redacted"
|
|
commit.Message = "Redacted redacted redacted"
|
|
}
|
|
}
|
|
}
|
|
}
|