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(`