diff --git a/history/feed.go b/history/feed.go index 5cc06ba..b935848 100644 --- a/history/feed.go +++ b/history/feed.go @@ -12,14 +12,16 @@ import ( "github.com/gorilla/feeds" ) +const changeGroupMaxSize = 100 + func recentChangesFeed(opts FeedOptions) *feeds.Feed { feed := &feeds.Feed{ Title: cfg.WikiName + " (recent changes)", Link: &feeds.Link{Href: cfg.URL}, - Description: "List of 30 recent changes on the wiki", + Description: fmt.Sprintf("List of %d recent changes on the wiki", changeGroupMaxSize), Updated: time.Now(), } - revs := RecentChanges(30) + revs := newRecentChangesStream() groups := opts.grouping.Group(revs) for _, grp := range groups { item := grp.feedItem(opts) @@ -43,6 +45,7 @@ func RecentChangesJSON(opts FeedOptions) (string, error) { return recentChangesFeed(opts).ToJSON() } +// revisionGroup is a slice of revisions, ordered most recent first. type revisionGroup []Revision func newRevisionGroup(rev Revision) revisionGroup { @@ -70,25 +73,34 @@ func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) { return res } -// groupRevisionsByPeriodFromNow groups close-together revisions. +// groupRevisionsByPeriodFromNow groups close-together revisions and returns the first changeGroupMaxSize (30) groups. // If two revisions happened within period of each other, they are put in the same group. -func groupRevisionsByPeriod(revs []Revision, period time.Duration) (res []revisionGroup) { - if len(revs) == 0 { +func groupRevisionsByPeriod(revs recentChangesStream, period time.Duration) (res []revisionGroup) { + nextRev := revs.iterator() + rev, empty := nextRev() + if empty { return res } - currTime := revs[0].Time - currGroup := newRevisionGroup(revs[0]) - for _, rev := range revs[1:] { + currTime := rev.Time + currGroup := newRevisionGroup(rev) + for { + rev, done := nextRev() + if done { + return append(res, currGroup) + } + if currTime.Sub(rev.Time) < period && currGroup[0].Username == rev.Username { currGroup.addRevision(rev) } else { res = append(res, currGroup) + if len(res) == changeGroupMaxSize { + return res + } currGroup = newRevisionGroup(rev) } currTime = rev.Time } - return res } func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item { @@ -154,7 +166,7 @@ func ParseFeedOptions(query url.Values) (FeedOptions, error) { } type FeedGrouping interface { - Group([]Revision) []revisionGroup + Group(recentChangesStream) []revisionGroup } func parseFeedGrouping(query url.Values) (FeedGrouping, error) { @@ -171,8 +183,8 @@ func parseFeedGrouping(query url.Values) (FeedGrouping, error) { type NormalFeedGrouping struct{} -func (NormalFeedGrouping) Group(revs []Revision) (res []revisionGroup) { - for _, rev := range revs { +func (NormalFeedGrouping) Group(revs recentChangesStream) (res []revisionGroup) { + for _, rev := range revs.next(changeGroupMaxSize) { res = append(res, newRevisionGroup(rev)) } return res @@ -182,7 +194,7 @@ type PeriodFeedGrouping struct { Period time.Duration } -func (g PeriodFeedGrouping) Group(revs []Revision) (res []revisionGroup) { +func (g PeriodFeedGrouping) Group(revs recentChangesStream) (res []revisionGroup) { return groupRevisionsByPeriod(revs, g.Period) } @@ -195,8 +207,7 @@ const ( func parseFeedGroupOrder(query url.Values) (FeedGroupOrder, error) { switch query.Get("order") { - case "old-to-new": - case "": + case "", "old-to-new": return OldToNew, nil case "new-to-old": return NewToOld, nil diff --git a/history/revision.go b/history/revision.go index 020a425..fd9293a 100644 --- a/history/revision.go +++ b/history/revision.go @@ -21,53 +21,96 @@ type Revision struct { hyphaeAffectedBuf []string } +// gitLog calls `git log` and parses the results. +func gitLog(args ...string) ([]Revision, error) { + args = append([]string{ + "log", "--abbrev-commit", "--no-merges", + "--pretty=format:%h\t%ae\t%at\t%s", + }, args...) + out, err := silentGitsh(args...) + if err != nil { + return nil, err + } + + outStr := out.String() + if outStr == "" { + // if there are no commits to return + return nil, nil + } + + var revs []Revision + for _, line := range strings.Split(outStr, "\n") { + revs = append(revs, parseRevisionLine(line)) + } + return revs, nil +} + +type recentChangesStream struct { + currHash string +} + +func newRecentChangesStream() recentChangesStream { + // next returns the next n revisions from the stream, ordered most recent first. + // If there are less than n revisions remaining, it will return only those. + return recentChangesStream{currHash: ""} +} + +func (stream *recentChangesStream) next(n int) []Revision { + args := []string{"--max-count=" + strconv.Itoa(n)} + if stream.currHash == "" { + args = append(args, "HEAD") + } else { + // currHash is the last revision from the last call, so skip it + args = append(args, "--skip=1", stream.currHash) + } + // I don't think this can fail, so ignore the error + res, _ := gitLog(args...) + if len(res) != 0 { + stream.currHash = res[len(res)-1].Hash + } + return res +} + +// recentChangesIterator returns a function that returns successive revisions from the stream. +// It buffers revisions to avoid calling git every time. +func (stream recentChangesStream) iterator() func() (Revision, bool) { + var buf []Revision + return func() (Revision, bool) { + if len(buf) == 0 { + // no real reason to choose 30, just needs some large number + buf = stream.next(30) + if len(buf) == 0 { + // revs has no revisions left + return Revision{}, true + } + } + rev := buf[0] + buf = buf[1:] + return rev, false + } +} + // RecentChanges gathers an arbitrary number of latest changes in form of revisions slice, ordered most recent first. func RecentChanges(n int) []Revision { - var ( - out, err = silentGitsh( - "log", "--oneline", "--no-merges", - "--pretty=format:%h\t%ae\t%at\t%s", - "--max-count="+strconv.Itoa(n), - ) - revs []Revision - ) - if err == nil { - for _, line := range strings.Split(out.String(), "\n") { - revs = append(revs, parseRevisionLine(line)) - } - } + stream := newRecentChangesStream() + revs := stream.next(n) log.Printf("Found %d recent changes", len(revs)) return revs } +// Revisions returns slice of revisions for the given hypha name, ordered most recent first. +func Revisions(hyphaName string) ([]Revision, error) { + revs, err := gitLog("--", hyphaName+".*") + log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName) + return revs, err +} + // FileChanged tells you if the file has been changed since the last commit. func FileChanged(path string) bool { _, err := gitsh("diff", "--exit-code", path) return err != nil } -// Revisions returns slice of revisions for the given hypha name, ordered most recent first. -func Revisions(hyphaName string) ([]Revision, error) { - var ( - out, err = silentGitsh( - "log", "--oneline", "--no-merges", - // Hash, author email, author time, commit msg separated by tab - "--pretty=format:%h\t%ae\t%at\t%s", - "--", hyphaName+".*", - ) - revs []Revision - ) - if err == nil { - for _, line := range strings.Split(out.String(), "\n") { - if line != "" { - revs = append(revs, parseRevisionLine(line)) - } - } - } - log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName) - return revs, err -} - // Return time like dd — 13:42 func (rev *Revision) timeToDisplay() string { D := rev.Time.Day()