diff --git a/history/feed.go b/history/feed.go index b3a0bd4..3b05ede 100644 --- a/history/feed.go +++ b/history/feed.go @@ -1,7 +1,9 @@ package history import ( + "errors" "fmt" + "net/url" "strings" "time" @@ -10,9 +12,7 @@ import ( "github.com/gorilla/feeds" ) -var groupPeriod, _ = time.ParseDuration("30m") - -func recentChangesFeed() *feeds.Feed { +func recentChangesFeed(opts FeedOptions) *feeds.Feed { feed := &feeds.Feed{ Title: "Recent changes", Link: &feeds.Link{Href: cfg.URL}, @@ -21,46 +21,101 @@ func recentChangesFeed() *feeds.Feed { Updated: time.Now(), } revs := RecentChanges(30) - groups := groupRevisionsByPeriod(revs, groupPeriod) + groups := opts.grouping.Group(revs) for _, grp := range groups { - item := grp.feedItem() + item := grp.feedItem(opts) feed.Add(&item) } return feed } // RecentChangesRSS creates recent changes feed in RSS format. -func RecentChangesRSS() (string, error) { - return recentChangesFeed().ToRss() +func RecentChangesRSS(opts FeedOptions) (string, error) { + return recentChangesFeed(opts).ToRss() } // RecentChangesAtom creates recent changes feed in Atom format. -func RecentChangesAtom() (string, error) { - return recentChangesFeed().ToAtom() +func RecentChangesAtom(opts FeedOptions) (string, error) { + return recentChangesFeed(opts).ToAtom() } // RecentChangesJSON creates recent changes feed in JSON format. -func RecentChangesJSON() (string, error) { - return recentChangesFeed().ToJSON() +func RecentChangesJSON(opts FeedOptions) (string, error) { + return recentChangesFeed(opts).ToJSON() } -func (grp revisionGroup) feedItem() feeds.Item { +type revisionGroup []Revision + +func newRevisionGroup(rev Revision) revisionGroup { + return revisionGroup([]Revision{rev}) +} + +func (grp *revisionGroup) addRevision(rev Revision) { + *grp = append(*grp, rev) +} + +func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) { + var ( + currentYear int + currentMonth time.Month + ) + for _, rev := range revs { + if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear { + currentYear = rev.Time.Year() + currentMonth = rev.Time.Month() + res = append(res, newRevisionGroup(rev)) + } else { + res[len(res)-1].addRevision(rev) + } + } + return res +} + +// groupRevisionsByPeriodFromNow groups close-together revisions. +// 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 { + return res + } + + currTime := revs[0].Time + res = append(res, newRevisionGroup(revs[0])) + for _, rev := range revs[1:] { + if currTime.Sub(rev.Time) < period { + res[len(res)-1].addRevision(rev) + } else { + res = append(res, newRevisionGroup(rev)) + } + currTime = rev.Time + } + return res +} + +func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item { return feeds.Item{ - Title: grp.title(), + Title: grp.title(opts.groupOrder), Author: grp.author(), - Id: grp[0].Hash, - Description: grp.descriptionForFeed(), + Id: grp[len(grp)-1].Hash, + Description: grp.descriptionForFeed(opts.groupOrder), Created: grp[len(grp)-1].Time, // earliest revision Updated: grp[0].Time, // latest revision Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()}, } } -func (grp revisionGroup) title() string { +func (grp revisionGroup) title(order FeedGroupOrder) string { + var message string + switch order { + case NewToOld: + message = grp[0].Message + case OldToNew: + message = grp[len(grp)-1].Message + } + if len(grp) == 1 { - return grp[0].Message + return message } else { - return fmt.Sprintf("%d edits (%s, ...)", len(grp), grp[0].Message) + return fmt.Sprintf("%d edits (%s, ...)", len(grp), message) } } @@ -75,10 +130,85 @@ func (grp revisionGroup) author() *feeds.Author { return &feeds.Author{Name: author} } -func (grp revisionGroup) descriptionForFeed() string { +func (grp revisionGroup) descriptionForFeed(order FeedGroupOrder) string { builder := strings.Builder{} - for _, rev := range grp { - builder.WriteString(rev.descriptionForFeed()) + switch order { + case NewToOld: + for _, rev := range grp { + builder.WriteString(rev.descriptionForFeed()) + } + case OldToNew: + for i := len(grp) - 1; i >= 0; i-- { + builder.WriteString(grp[i].descriptionForFeed()) + } } return builder.String() } + +type FeedOptions struct { + grouping FeedGrouping + groupOrder FeedGroupOrder +} + +func ParseFeedOptions(query url.Values) (FeedOptions, error) { + grouping, err := parseFeedGrouping(query) + if err != nil { + return FeedOptions{}, err + } + groupOrder, err := parseFeedGroupOrder(query) + if err != nil { + return FeedOptions{}, err + } + return FeedOptions{grouping, groupOrder}, nil +} + +type FeedGrouping interface { + Group([]Revision) []revisionGroup +} + +func parseFeedGrouping(query url.Values) (FeedGrouping, error) { + if query.Get("period") == "" { + return NormalFeedGrouping{}, nil + } else { + period, err := time.ParseDuration(query.Get("period")) + if err != nil { + return nil, err + } + return PeriodFeedGrouping{Period: period}, nil + } +} + +type NormalFeedGrouping struct{} + +func (NormalFeedGrouping) Group(revs []Revision) (res []revisionGroup) { + for _, rev := range revs { + res = append(res, newRevisionGroup(rev)) + } + return res +} + +type PeriodFeedGrouping struct { + Period time.Duration +} + +func (g PeriodFeedGrouping) Group(revs []Revision) (res []revisionGroup) { + return groupRevisionsByPeriod(revs, g.Period) +} + +type FeedGroupOrder int + +const ( + NewToOld FeedGroupOrder = iota + OldToNew FeedGroupOrder = iota +) + +func parseFeedGroupOrder(query url.Values) (FeedGroupOrder, error) { + switch query.Get("order") { + case "oldtonew": + return OldToNew, nil + case "newtoold": + case "": + return NewToOld, nil + } + return 0, errors.New("unknown order") +} diff --git a/history/revision.go b/history/revision.go index fd41849..b01d79b 100644 --- a/history/revision.go +++ b/history/revision.go @@ -209,47 +209,3 @@ func PrimitiveDiffAtRevision(filepath, hash string) (string, error) { } return out.String(), err } - -type revisionGroup []Revision - -func newRevisionGroup(rev Revision) revisionGroup { - return revisionGroup([]Revision{rev}) -} - -func (grp *revisionGroup) addRevision(rev Revision) { - *grp = append(*grp, rev) -} - -func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) { - var ( - currentYear int - currentMonth time.Month - ) - for _, rev := range revs { - if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear { - currentYear = rev.Time.Year() - currentMonth = rev.Time.Month() - res = append(res, newRevisionGroup(rev)) - } else { - res[len(res)-1].addRevision(rev) - } - } - return res -} - -// groupRevisionsByPeriod groups revisions by how long ago they were. -// Revisions less than one period ago are placed in one group, revisions between one and two periods are in another, and so on. -func groupRevisionsByPeriod(revs []Revision, period time.Duration) (res []revisionGroup) { - now := time.Now() - currentPeriod := -1 - for _, rev := range revs { - newPeriod := int(now.Sub(rev.Time).Seconds() / period.Seconds()) - if newPeriod != currentPeriod { - currentPeriod = newPeriod - res = append(res, newRevisionGroup(rev)) - } else { - res[len(res)-1].addRevision(rev) - } - } - return res -} diff --git a/history/view.qtpl b/history/view.qtpl index b4ec298..2a87825 100644 --- a/history/view.qtpl +++ b/history/view.qtpl @@ -8,7 +8,7 @@ HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by t {% if i > 0 %} {% endif %} - {%s hyphaName %} + {%s hyphaName %} {% endfor %} {% endstripspace %} {% endfunc %} diff --git a/history/view.qtpl.go b/history/view.qtpl.go index be51e18..65c5a71 100644 --- a/history/view.qtpl.go +++ b/history/view.qtpl.go @@ -39,7 +39,11 @@ func (rev Revision) StreamHyphaeLinksHTML(qw422016 *qt422016.Writer) { //line history/view.qtpl:10 } //line history/view.qtpl:10 - qw422016.N().S(`