retrogit/app/digest.go
2014-12-06 14:51:58 -08:00

312 lines
8.7 KiB
Go

package retrogit
import (
"bytes"
"fmt"
"sort"
"strings"
"time"
"appengine"
"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
}
func newDigest(c appengine.Context, githubClient *github.Client, account *Account) (*Digest, error) {
user, _, err := githubClient.Users.Get("")
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)
// 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,
}
err = digest.fetch(githubClient)
return digest, err
}
func (digest *Digest) fetch(githubClient *github.Client) error {
type RepoDigestResponse struct {
intervalDigest *IntervalDigest
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(
*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{nil, 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, &RepoDigest{repo, digestCommits}, nil}
}(intervalDigest, repo)
fetchCount++
}
}
for i := 0; i < fetchCount; i++ {
select {
case r := <-ch:
if r.err != nil {
return r.err
}
if len(r.repoDigest.Commits) > 0 {
r.intervalDigest.RepoDigests = append(r.intervalDigest.RepoDigests, r.repoDigest)
}
}
}
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
return nil
}
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"
}
}
}
}