mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2024-10-30 03:36:16 +00:00
Merge pull request #106 from pyelias/rss-grouping
Group recent-changes feeds (#61)
This commit is contained in:
commit
80f37420db
39
help/en/feeds.myco
Normal file
39
help/en/feeds.myco
Normal file
@ -0,0 +1,39 @@
|
||||
# Help: Feeds
|
||||
Mycorrhiza Wiki has RSS, Atom, and JSON feeds to track the latest changes on the wiki.
|
||||
These feeds are linked on the [[/recent-changes | recent changes page]].
|
||||
|
||||
## Options
|
||||
These feeds have options to combine related changes into groups:
|
||||
* {
|
||||
**period** Can be set to lengths of time like `5m`, `24h`, etc.
|
||||
Edits by the same author that happen within this time of each other can be grouped into one item in the feed.
|
||||
}
|
||||
* {
|
||||
**same** Can be set to `author`, `message`, or `none`.
|
||||
Edits will only be grouped together if they have the same author or message. By default, edits need to have the same author and message. If it is `none`, all edits can be grouped.
|
||||
}
|
||||
* {
|
||||
**order** Can be set to `old-to-now` (default) or `new-to-old`.
|
||||
This determines what order edits in groups will be shown in in your feed.
|
||||
}
|
||||
|
||||
If none of these options are set, changes will never be grouped.
|
||||
|
||||
## Examples
|
||||
URLs for feeds using these options look like this:
|
||||
* {
|
||||
`/recent-changes-rss?period=1h`
|
||||
Changes within one hour of each other with the same author and message will be grouped together.
|
||||
}
|
||||
* {
|
||||
`/recent-changes-atom?period=1h&order=new-to-old`
|
||||
Same as the last one, but the groups will be shown in the opposite order.
|
||||
}
|
||||
* {
|
||||
`/recent-changes-atom?period=1h&same=none`
|
||||
Changes within one hour of each other will be grouped together, even with different authors and messages.
|
||||
}
|
||||
* {
|
||||
`/recent-changes-atom?same=author&same=message`
|
||||
Changes with the same author and message will be grouped together no matter how much time passes between them.
|
||||
}
|
318
history/feed.go
Normal file
318
history/feed.go
Normal file
@ -0,0 +1,318 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
const changeGroupMaxSize = 30
|
||||
|
||||
func recentChangesFeed(opts FeedOptions) *feeds.Feed {
|
||||
feed := &feeds.Feed{
|
||||
Title: cfg.WikiName + " (recent changes)",
|
||||
Link: &feeds.Link{Href: cfg.URL},
|
||||
Description: fmt.Sprintf("List of %d recent changes on the wiki", changeGroupMaxSize),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
revs := newRecentChangesStream()
|
||||
groups := groupRevisions(revs, opts)
|
||||
for _, grp := range groups {
|
||||
item := grp.feedItem(opts)
|
||||
feed.Add(&item)
|
||||
}
|
||||
return feed
|
||||
}
|
||||
|
||||
// RecentChangesRSS creates recent changes feed in RSS format.
|
||||
func RecentChangesRSS(opts FeedOptions) (string, error) {
|
||||
return recentChangesFeed(opts).ToRss()
|
||||
}
|
||||
|
||||
// RecentChangesAtom creates recent changes feed in Atom format.
|
||||
func RecentChangesAtom(opts FeedOptions) (string, error) {
|
||||
return recentChangesFeed(opts).ToAtom()
|
||||
}
|
||||
|
||||
// RecentChangesJSON creates recent changes feed in JSON format.
|
||||
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 {
|
||||
return revisionGroup([]Revision{rev})
|
||||
}
|
||||
|
||||
func (grp *revisionGroup) addRevision(rev Revision) {
|
||||
*grp = append(*grp, rev)
|
||||
}
|
||||
|
||||
// orderedIndex returns the ith revision in the group following the given order.
|
||||
func (grp *revisionGroup) orderedIndex(i int, order feedGroupOrder) *Revision {
|
||||
switch order {
|
||||
case newToOld:
|
||||
return &(*grp)[i]
|
||||
case oldToNew:
|
||||
return &(*grp)[len(*grp)-1-i]
|
||||
}
|
||||
// unreachable
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// groupRevisions groups revisions for a feed.
|
||||
// It returns the first changeGroupMaxSize (30) groups.
|
||||
// The grouping parameter determines when two revisions will be grouped.
|
||||
func groupRevisions(revs recentChangesStream, opts FeedOptions) (res []revisionGroup) {
|
||||
nextRev := revs.iterator()
|
||||
rev, empty := nextRev()
|
||||
if empty {
|
||||
return res
|
||||
}
|
||||
|
||||
currGroup := newRevisionGroup(rev)
|
||||
for rev, done := nextRev(); !done; rev, done = nextRev() {
|
||||
if opts.canGroup(currGroup, rev) {
|
||||
currGroup.addRevision(rev)
|
||||
} else {
|
||||
res = append(res, currGroup)
|
||||
if len(res) == changeGroupMaxSize {
|
||||
return res
|
||||
}
|
||||
currGroup = newRevisionGroup(rev)
|
||||
}
|
||||
}
|
||||
// no more revisions, haven't added the last group yet
|
||||
return append(res, currGroup)
|
||||
}
|
||||
|
||||
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
|
||||
title, author := grp.titleAndAuthor(opts.order)
|
||||
return feeds.Item{
|
||||
Title: title,
|
||||
Author: author,
|
||||
Id: grp[len(grp)-1].Hash,
|
||||
Description: grp.descriptionForFeed(opts.order),
|
||||
Created: grp[len(grp)-1].Time, // earliest revision
|
||||
Updated: grp[0].Time, // latest revision
|
||||
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
|
||||
}
|
||||
}
|
||||
|
||||
// titleAndAuthor creates a title and author for a feed item.
|
||||
// If all messages and authors are the same (or there's just one rev), "message by author"
|
||||
// If all authors are the same, "num edits (first message, ...) by author"
|
||||
// Else (even if all messages are the same), "num edits (first message, ...)"
|
||||
func (grp revisionGroup) titleAndAuthor(order feedGroupOrder) (title string, author *feeds.Author) {
|
||||
allMessagesSame := true
|
||||
allAuthorsSame := true
|
||||
for _, rev := range grp[1:] {
|
||||
if rev.Message != grp[0].Message {
|
||||
allMessagesSame = false
|
||||
}
|
||||
if rev.Username != grp[0].Username {
|
||||
allAuthorsSame = false
|
||||
}
|
||||
if !allMessagesSame && !allAuthorsSame {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allMessagesSame && allAuthorsSame {
|
||||
title = grp[0].Message
|
||||
} else {
|
||||
title = fmt.Sprintf("%d edits (%s, ...)", len(grp), grp.orderedIndex(0, order).Message)
|
||||
}
|
||||
|
||||
if allAuthorsSame {
|
||||
title += fmt.Sprintf(" by %s", grp[0].Username)
|
||||
author = &feeds.Author{Name: grp[0].Username}
|
||||
} else {
|
||||
author = nil
|
||||
}
|
||||
|
||||
return title, author
|
||||
}
|
||||
|
||||
func (grp revisionGroup) descriptionForFeed(order feedGroupOrder) string {
|
||||
builder := strings.Builder{}
|
||||
for i := 0; i < len(grp); i++ {
|
||||
desc := grp.orderedIndex(i, order).descriptionForFeed()
|
||||
builder.WriteString(desc)
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type feedOptionParserState struct {
|
||||
isAnythingSet bool
|
||||
conds []groupingCondition
|
||||
order feedGroupOrder
|
||||
}
|
||||
|
||||
// feedGrouping represents a set of conditions that must all be satisfied for revisions to be grouped.
|
||||
// If there are no conditions, revisions will never be grouped.
|
||||
type FeedOptions struct {
|
||||
conds []groupingCondition
|
||||
order feedGroupOrder
|
||||
}
|
||||
|
||||
func ParseFeedOptions(query url.Values) (FeedOptions, error) {
|
||||
parser := feedOptionParserState{}
|
||||
|
||||
err := parser.parseFeedGroupingPeriod(query)
|
||||
if err != nil {
|
||||
return FeedOptions{}, err
|
||||
}
|
||||
err = parser.parseFeedGroupingSame(query)
|
||||
if err != nil {
|
||||
return FeedOptions{}, err
|
||||
}
|
||||
err = parser.parseFeedGroupingOrder(query)
|
||||
if err != nil {
|
||||
return FeedOptions{}, err
|
||||
}
|
||||
|
||||
var conds []groupingCondition
|
||||
if parser.isAnythingSet {
|
||||
conds = parser.conds
|
||||
} else {
|
||||
// if no options are applied, do no grouping instead of using the default options
|
||||
conds = nil
|
||||
}
|
||||
return FeedOptions{conds: conds, order: parser.order}, nil
|
||||
}
|
||||
|
||||
func (parser *feedOptionParserState) parseFeedGroupingPeriod(query url.Values) error {
|
||||
if query["period"] != nil {
|
||||
parser.isAnythingSet = true
|
||||
period, err := time.ParseDuration(query.Get("period"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parser.conds = append(parser.conds, periodGroupingCondition{period})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (parser *feedOptionParserState) parseFeedGroupingSame(query url.Values) error {
|
||||
if same := query["same"]; same != nil {
|
||||
parser.isAnythingSet = true
|
||||
if len(same) == 1 && same[0] == "none" {
|
||||
// same=none adds no condition
|
||||
parser.conds = append(parser.conds, sameGroupingCondition{})
|
||||
return nil
|
||||
} else {
|
||||
// handle same=author, same=author&same=message, etc.
|
||||
cond := sameGroupingCondition{}
|
||||
for _, sameCond := range same {
|
||||
switch sameCond {
|
||||
case "author":
|
||||
if cond.author {
|
||||
return errors.New("set same=author twice")
|
||||
}
|
||||
cond.author = true
|
||||
case "message":
|
||||
if cond.message {
|
||||
return errors.New("set same=message twice")
|
||||
}
|
||||
cond.message = true
|
||||
default:
|
||||
return errors.New("unknown same option " + sameCond)
|
||||
}
|
||||
}
|
||||
parser.conds = append(parser.conds, cond)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// same defaults to both author and message
|
||||
// but this won't be applied if no grouping options are set
|
||||
parser.conds = append(parser.conds, sameGroupingCondition{author: true, message: true})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type feedGroupOrder int
|
||||
|
||||
const (
|
||||
newToOld feedGroupOrder = iota
|
||||
oldToNew feedGroupOrder = iota
|
||||
)
|
||||
|
||||
func (parser *feedOptionParserState) parseFeedGroupingOrder(query url.Values) error {
|
||||
if order := query["order"]; order != nil {
|
||||
parser.isAnythingSet = true
|
||||
switch query.Get("order") {
|
||||
case "old-to-new":
|
||||
parser.order = oldToNew
|
||||
case "new-to-old":
|
||||
parser.order = newToOld
|
||||
default:
|
||||
return errors.New("unknown order option " + query.Get("order"))
|
||||
}
|
||||
} else {
|
||||
parser.order = oldToNew
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// canGroup determines whether a revision can be added to a group.
|
||||
func (opts FeedOptions) canGroup(grp revisionGroup, rev Revision) bool {
|
||||
if len(opts.conds) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, cond := range opts.conds {
|
||||
if !cond.canGroup(grp, rev) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type groupingCondition interface {
|
||||
canGroup(grp revisionGroup, rev Revision) bool
|
||||
}
|
||||
|
||||
// periodGroupingCondition will group two revisions if they are within period of each other.
|
||||
type periodGroupingCondition struct {
|
||||
period time.Duration
|
||||
}
|
||||
|
||||
func (cond periodGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||
return grp[len(grp)-1].Time.Sub(rev.Time) < cond.period
|
||||
}
|
||||
|
||||
type sameGroupingCondition struct {
|
||||
author bool
|
||||
message bool
|
||||
}
|
||||
|
||||
func (c sameGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||
return (!c.author || grp[0].Username == rev.Username) &&
|
||||
(!c.message || grp[0].Message == rev.Message)
|
||||
}
|
@ -4,14 +4,10 @@ package history
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/files"
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
@ -54,131 +50,6 @@ func InitGitRepo() {
|
||||
}
|
||||
}
|
||||
|
||||
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
||||
type Revision struct {
|
||||
Hash string
|
||||
Username string
|
||||
Time time.Time
|
||||
Message string
|
||||
filesAffectedBuf []string
|
||||
hyphaeAffectedBuf []string
|
||||
}
|
||||
|
||||
// filesAffected tells what files have been affected by the revision.
|
||||
func (rev *Revision) filesAffected() (filenames []string) {
|
||||
if nil != rev.filesAffectedBuf {
|
||||
return rev.filesAffectedBuf
|
||||
}
|
||||
// List of files affected by this revision, one per line.
|
||||
out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
||||
// There's an error? Well, whatever, let's just assign an empty slice, who cares.
|
||||
if err != nil {
|
||||
rev.filesAffectedBuf = []string{}
|
||||
} else {
|
||||
rev.filesAffectedBuf = strings.Split(out.String(), "\n")
|
||||
}
|
||||
return rev.filesAffectedBuf
|
||||
}
|
||||
|
||||
// determine what hyphae were affected by this revision
|
||||
func (rev *Revision) hyphaeAffected() (hyphae []string) {
|
||||
if nil != rev.hyphaeAffectedBuf {
|
||||
return rev.hyphaeAffectedBuf
|
||||
}
|
||||
hyphae = make([]string, 0)
|
||||
var (
|
||||
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
|
||||
set = make(map[string]bool)
|
||||
isNewName = func(hyphaName string) bool {
|
||||
if _, present := set[hyphaName]; present {
|
||||
return false
|
||||
}
|
||||
set[hyphaName] = true
|
||||
return true
|
||||
}
|
||||
filesAffected = rev.filesAffected()
|
||||
)
|
||||
for _, filename := range filesAffected {
|
||||
if strings.IndexRune(filename, '.') >= 0 {
|
||||
dotPos := strings.LastIndexByte(filename, '.')
|
||||
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
|
||||
if isNewName(hyphaName) {
|
||||
hyphae = append(hyphae, hyphaName)
|
||||
}
|
||||
}
|
||||
}
|
||||
rev.hyphaeAffectedBuf = hyphae
|
||||
return hyphae
|
||||
}
|
||||
|
||||
// TimeString returns a human readable time representation.
|
||||
func (rev Revision) TimeString() string {
|
||||
return rev.Time.Format(time.RFC822)
|
||||
}
|
||||
|
||||
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
||||
func (rev Revision) HyphaeLinksHTML() (html string) {
|
||||
hyphae := rev.hyphaeAffected()
|
||||
for i, hyphaName := range hyphae {
|
||||
if i > 0 {
|
||||
html += `<span aria-hidden="true">, </span>`
|
||||
}
|
||||
html += fmt.Sprintf(`<a href="/hypha/%[1]s">%[1]s</a>`, hyphaName)
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
// descriptionForFeed generates a good enough HTML contents for a web feed.
|
||||
func (rev *Revision) descriptionForFeed() (htmlDesc string) {
|
||||
return fmt.Sprintf(
|
||||
`<p>%s</p>
|
||||
<p><b>Hyphae affected:</b> %s</p>
|
||||
<pre><code>%s</code></pre>`, rev.Message, rev.HyphaeLinksHTML(), html.EscapeString(rev.textDiff()))
|
||||
}
|
||||
|
||||
// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
|
||||
func (rev *Revision) textDiff() (diff string) {
|
||||
filenames, ok := rev.mycoFiles()
|
||||
if !ok {
|
||||
return "No text changes"
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
|
||||
if err != nil {
|
||||
diff += "\nAn error has occurred with " + filename + "\n"
|
||||
}
|
||||
diff += text + "\n"
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
|
||||
func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
|
||||
filenames = []string{}
|
||||
for _, filename := range rev.filesAffected() {
|
||||
if strings.HasSuffix(filename, ".myco") {
|
||||
filenames = append(filenames, filename)
|
||||
}
|
||||
}
|
||||
return filenames, len(filenames) > 0
|
||||
}
|
||||
|
||||
// Try and guess what link is the most important by looking at the message.
|
||||
func (rev *Revision) bestLink() string {
|
||||
var (
|
||||
revs = rev.hyphaeAffected()
|
||||
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
|
||||
)
|
||||
switch {
|
||||
case renameRes != nil:
|
||||
return "/hypha/" + renameRes[1]
|
||||
case len(revs) == 0:
|
||||
return ""
|
||||
default:
|
||||
return "/hypha/" + revs[0]
|
||||
}
|
||||
}
|
||||
|
||||
// I pronounce it as [gɪt͡ʃ].
|
||||
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
||||
func gitsh(args ...string) (out bytes.Buffer, err error) {
|
||||
@ -204,16 +75,6 @@ func silentGitsh(args ...string) (out bytes.Buffer, err error) {
|
||||
return *bytes.NewBuffer(b), err
|
||||
}
|
||||
|
||||
// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
|
||||
func unixTimestampAsTime(ts string) *time.Time {
|
||||
i, err := strconv.ParseInt(ts, 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
tm := time.Unix(i, 0)
|
||||
return &tm
|
||||
}
|
||||
|
||||
// Rename renames from `from` to `to` using `git mv`.
|
||||
func Rename(from, to string) error {
|
||||
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
||||
|
@ -1,199 +0,0 @@
|
||||
package history
|
||||
|
||||
// information.go
|
||||
// Things related to gathering existing information.
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
"github.com/bouncepaw/mycorrhiza/files"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
func recentChangesFeed() *feeds.Feed {
|
||||
feed := &feeds.Feed{
|
||||
Title: "Recent changes",
|
||||
Link: &feeds.Link{Href: cfg.URL},
|
||||
Description: "List of 30 recent changes on the wiki",
|
||||
Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"},
|
||||
Updated: time.Now(),
|
||||
}
|
||||
var (
|
||||
out, err = silentGitsh(
|
||||
"log", "--oneline", "--no-merges",
|
||||
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
|
||||
"--max-count=30",
|
||||
)
|
||||
revs []Revision
|
||||
)
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(out.String(), "\n") {
|
||||
revs = append(revs, parseRevisionLine(line))
|
||||
}
|
||||
}
|
||||
log.Printf("Found %d recent changes", len(revs))
|
||||
for _, rev := range revs {
|
||||
feed.Add(&feeds.Item{
|
||||
Title: rev.Message,
|
||||
Author: &feeds.Author{Name: rev.Username},
|
||||
Id: rev.Hash,
|
||||
Description: rev.descriptionForFeed(),
|
||||
Created: rev.Time,
|
||||
Updated: rev.Time,
|
||||
Link: &feeds.Link{Href: cfg.URL + rev.bestLink()},
|
||||
})
|
||||
}
|
||||
return feed
|
||||
}
|
||||
|
||||
// RecentChangesRSS creates recent changes feed in RSS format.
|
||||
func RecentChangesRSS() (string, error) {
|
||||
return recentChangesFeed().ToRss()
|
||||
}
|
||||
|
||||
// RecentChangesAtom creates recent changes feed in Atom format.
|
||||
func RecentChangesAtom() (string, error) {
|
||||
return recentChangesFeed().ToAtom()
|
||||
}
|
||||
|
||||
// RecentChangesJSON creates recent changes feed in JSON format.
|
||||
func RecentChangesJSON() (string, error) {
|
||||
return recentChangesFeed().ToJSON()
|
||||
}
|
||||
|
||||
// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice.
|
||||
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))
|
||||
}
|
||||
}
|
||||
log.Printf("Found %d recent changes", len(revs))
|
||||
return revs
|
||||
}
|
||||
|
||||
// FileChanged tells you if the file has been changed.
|
||||
func FileChanged(path string) bool {
|
||||
_, err := gitsh("diff", "--exit-code", path)
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// Revisions returns slice of revisions for the given hypha name.
|
||||
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
|
||||
}
|
||||
|
||||
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
|
||||
func WithRevisions(hyphaName string, revs []Revision) (html string) {
|
||||
var (
|
||||
currentYear int
|
||||
currentMonth time.Month
|
||||
)
|
||||
for i, rev := range revs {
|
||||
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
|
||||
currentYear = rev.Time.Year()
|
||||
currentMonth = rev.Time.Month()
|
||||
if i != 0 {
|
||||
html += `
|
||||
</ul>
|
||||
</section>`
|
||||
}
|
||||
html += fmt.Sprintf(`
|
||||
<section class="history__month">
|
||||
<a href="#%[1]d-%[2]d" class="history__month-anchor">
|
||||
<h2 id="%[1]d-%[2]d" class="history__month-title">%[3]s</h2>
|
||||
</a>
|
||||
<ul class="history__entries">`,
|
||||
currentYear, currentMonth,
|
||||
strconv.Itoa(currentYear)+" "+rev.Time.Month().String())
|
||||
}
|
||||
html += rev.asHistoryEntry(hyphaName)
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
func (rev *Revision) asHistoryEntry(hyphaName string) (html string) {
|
||||
author := ""
|
||||
if rev.Username != "anon" {
|
||||
author = fmt.Sprintf(`
|
||||
<span class="history-entry__author">by <a href="/hypha/%[1]s/%[2]s" rel="author">%[2]s</span>`, cfg.UserHypha, rev.Username)
|
||||
}
|
||||
return fmt.Sprintf(`
|
||||
<li class="history__entry">
|
||||
<a class="history-entry" href="/rev/%[3]s/%[1]s">
|
||||
<time class="history-entry__time">%[2]s</time>
|
||||
<span class="history-entry__hash"><a href="/primitive-diff/%[3]s/%[1]s">%[3]s</a></span>
|
||||
<span class="history-entry__msg">%[4]s</span>
|
||||
</a>%[5]s
|
||||
</li>
|
||||
`, hyphaName, rev.timeToDisplay(), rev.Hash, rev.Message, author)
|
||||
}
|
||||
|
||||
// Return time like mm-dd 13:42
|
||||
func (rev *Revision) timeToDisplay() string {
|
||||
D := rev.Time.Day()
|
||||
h, m, _ := rev.Time.Clock()
|
||||
return fmt.Sprintf("%02d — %02d:%02d", D, h, m)
|
||||
}
|
||||
|
||||
// This regex is wrapped in "". For some reason, these quotes appear at some time and we have to get rid of them.
|
||||
var revisionLinePattern = regexp.MustCompile("\"(.*)\t(.*)@.*\t(.*)\t(.*)\"")
|
||||
|
||||
func parseRevisionLine(line string) Revision {
|
||||
results := revisionLinePattern.FindStringSubmatch(line)
|
||||
return Revision{
|
||||
Hash: results[1],
|
||||
Username: results[2],
|
||||
Time: *unixTimestampAsTime(results[3]),
|
||||
Message: results[4],
|
||||
}
|
||||
}
|
||||
|
||||
// FileAtRevision shows how the file with the given file path looked at the commit with the hash. It may return an error if git fails.
|
||||
func FileAtRevision(filepath, hash string) (string, error) {
|
||||
out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, files.HyphaeDir()+"/"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), err
|
||||
}
|
||||
|
||||
// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath at the commit with the given hash. It may return an error if git fails.
|
||||
func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
|
||||
out, err := silentGitsh("diff", "--unified=1", "--no-color", hash+"~", hash, "--", filepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), err
|
||||
}
|
@ -95,9 +95,7 @@ func (hop *Op) WithFilesRenamed(pairs map[string]string) *Op {
|
||||
hop.Errs = append(hop.Errs, err)
|
||||
continue
|
||||
}
|
||||
if err := Rename(from, to); err != nil {
|
||||
hop.Errs = append(hop.Errs, err)
|
||||
}
|
||||
hop.gitop("mv", "--force", from, to)
|
||||
}
|
||||
}
|
||||
return hop
|
||||
|
254
history/revision.go
Normal file
254
history/revision.go
Normal file
@ -0,0 +1,254 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/files"
|
||||
)
|
||||
|
||||
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
||||
type Revision struct {
|
||||
Hash string
|
||||
Username string
|
||||
Time time.Time
|
||||
Message string
|
||||
filesAffectedBuf []string
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// Return time like dd — 13:42
|
||||
func (rev *Revision) timeToDisplay() string {
|
||||
D := rev.Time.Day()
|
||||
h, m, _ := rev.Time.Clock()
|
||||
return fmt.Sprintf("%02d — %02d:%02d", D, h, m)
|
||||
}
|
||||
|
||||
var revisionLinePattern = regexp.MustCompile("(.*)\t(.*)@.*\t(.*)\t(.*)")
|
||||
|
||||
// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
|
||||
func unixTimestampAsTime(ts string) *time.Time {
|
||||
i, err := strconv.ParseInt(ts, 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
tm := time.Unix(i, 0)
|
||||
return &tm
|
||||
}
|
||||
|
||||
func parseRevisionLine(line string) Revision {
|
||||
results := revisionLinePattern.FindStringSubmatch(line)
|
||||
return Revision{
|
||||
Hash: results[1],
|
||||
Username: results[2],
|
||||
Time: *unixTimestampAsTime(results[3]),
|
||||
Message: results[4],
|
||||
}
|
||||
}
|
||||
|
||||
// filesAffected tells what files have been affected by the revision.
|
||||
func (rev *Revision) filesAffected() (filenames []string) {
|
||||
if nil != rev.filesAffectedBuf {
|
||||
return rev.filesAffectedBuf
|
||||
}
|
||||
// List of files affected by this revision, one per line.
|
||||
out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
||||
// There's an error? Well, whatever, let's just assign an empty slice, who cares.
|
||||
if err != nil {
|
||||
rev.filesAffectedBuf = []string{}
|
||||
} else {
|
||||
rev.filesAffectedBuf = strings.Split(out.String(), "\n")
|
||||
}
|
||||
return rev.filesAffectedBuf
|
||||
}
|
||||
|
||||
// determine what hyphae were affected by this revision
|
||||
func (rev *Revision) hyphaeAffected() (hyphae []string) {
|
||||
if nil != rev.hyphaeAffectedBuf {
|
||||
return rev.hyphaeAffectedBuf
|
||||
}
|
||||
hyphae = make([]string, 0)
|
||||
var (
|
||||
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
|
||||
set = make(map[string]bool)
|
||||
isNewName = func(hyphaName string) bool {
|
||||
if _, present := set[hyphaName]; present {
|
||||
return false
|
||||
}
|
||||
set[hyphaName] = true
|
||||
return true
|
||||
}
|
||||
filesAffected = rev.filesAffected()
|
||||
)
|
||||
for _, filename := range filesAffected {
|
||||
if strings.IndexRune(filename, '.') >= 0 {
|
||||
dotPos := strings.LastIndexByte(filename, '.')
|
||||
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
|
||||
if isNewName(hyphaName) {
|
||||
hyphae = append(hyphae, hyphaName)
|
||||
}
|
||||
}
|
||||
}
|
||||
rev.hyphaeAffectedBuf = hyphae
|
||||
return hyphae
|
||||
}
|
||||
|
||||
// TimeString returns a human readable time representation.
|
||||
func (rev Revision) TimeString() string {
|
||||
return rev.Time.Format(time.RFC822)
|
||||
}
|
||||
|
||||
// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
|
||||
func (rev *Revision) textDiff() (diff string) {
|
||||
filenames, ok := rev.mycoFiles()
|
||||
if !ok {
|
||||
return "No text changes"
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
|
||||
if err != nil {
|
||||
diff += "\nAn error has occurred with " + filename + "\n"
|
||||
}
|
||||
diff += text + "\n"
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
|
||||
func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
|
||||
filenames = []string{}
|
||||
for _, filename := range rev.filesAffected() {
|
||||
if strings.HasSuffix(filename, ".myco") {
|
||||
filenames = append(filenames, filename)
|
||||
}
|
||||
}
|
||||
return filenames, len(filenames) > 0
|
||||
}
|
||||
|
||||
// Try and guess what link is the most important by looking at the message.
|
||||
func (rev *Revision) bestLink() string {
|
||||
var (
|
||||
revs = rev.hyphaeAffected()
|
||||
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
|
||||
)
|
||||
switch {
|
||||
case renameRes != nil:
|
||||
return "/hypha/" + renameRes[1]
|
||||
case len(revs) == 0:
|
||||
return ""
|
||||
default:
|
||||
return "/hypha/" + revs[0]
|
||||
}
|
||||
}
|
||||
|
||||
// FileAtRevision shows how the file with the given file path looked at the commit with the hash. It may return an error if git fails.
|
||||
func FileAtRevision(filepath, hash string) (string, error) {
|
||||
out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, files.HyphaeDir()+"/"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), err
|
||||
}
|
||||
|
||||
// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath at the commit with the given hash. It may return an error if git fails.
|
||||
func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
|
||||
out, err := silentGitsh("diff", "--unified=1", "--no-color", hash+"~", hash, "--", filepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), err
|
||||
}
|
55
history/view.qtpl
Normal file
55
history/view.qtpl
Normal file
@ -0,0 +1,55 @@
|
||||
{% import "fmt" %}
|
||||
{% import "github.com/bouncepaw/mycorrhiza/cfg" %}
|
||||
|
||||
HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
||||
{% func (rev Revision) HyphaeLinksHTML() %}
|
||||
{% stripspace %}
|
||||
{% for i, hyphaName := range rev.hyphaeAffected() %}
|
||||
{% if i > 0 %}
|
||||
<span aria-hidden="true">, </span>
|
||||
{% endif %}
|
||||
<a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">{%s hyphaName %}</a>
|
||||
{% endfor %}
|
||||
{% endstripspace %}
|
||||
{% endfunc %}
|
||||
|
||||
descriptionForFeed generates a good enough HTML contents for a web feed.
|
||||
{% func (rev *Revision) descriptionForFeed() %}
|
||||
<p><b>{%s rev.Message %}</b> (by {%s rev.Username %} at {%s rev.TimeString() %})</p>
|
||||
<p>Hyphae affected: {%= rev.HyphaeLinksHTML() %}</p>
|
||||
<pre><code>{%s rev.textDiff() %}</code></pre>
|
||||
{% endfunc %}
|
||||
|
||||
WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
|
||||
{% func WithRevisions(hyphaName string, revs []Revision) %}
|
||||
{% for _, grp := range groupRevisionsByMonth(revs) %}
|
||||
{% code
|
||||
currentYear := grp[0].Time.Year()
|
||||
currentMonth := grp[0].Time.Month()
|
||||
sectionId := fmt.Sprintf("%d-%d", currentYear, currentMonth)
|
||||
%}
|
||||
<section class="history__month">
|
||||
<a href="#{%s sectionId %}" class="history__month-anchor">
|
||||
<h2 id="{%s sectionId %}" class="history__month-title">{%d currentYear %} {%s currentMonth.String() %}</h2>
|
||||
</a>
|
||||
<ul class="history__entries">
|
||||
{% for _, rev := range grp %}
|
||||
{%= rev.asHistoryEntry(hyphaName) %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func (rev *Revision) asHistoryEntry(hyphaName string) %}
|
||||
<li class="history__entry">
|
||||
<a class="history-entry" href="/rev/{%s rev.Hash %}/{%s hyphaName %}">
|
||||
<time class="history-entry__time">{%s rev.timeToDisplay() %}</time>
|
||||
</a>
|
||||
<span class="history-entry__hash"><a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">{%s rev.Hash %}</a></span>
|
||||
<span class="history-entry__msg">{%s rev.Message %}</span>
|
||||
{% if rev.Username != "anon" %}
|
||||
<span class="history-entry__author">by <a href="/hypha/{%s cfg.UserHypha %}/{%s rev.Username %}" rel="author">{%s rev.Username %}</a></span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfunc %}
|
330
history/view.qtpl.go
Normal file
330
history/view.qtpl.go
Normal file
@ -0,0 +1,330 @@
|
||||
// Code generated by qtc from "view.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
//line history/view.qtpl:1
|
||||
package history
|
||||
|
||||
//line history/view.qtpl:1
|
||||
import "fmt"
|
||||
|
||||
//line history/view.qtpl:2
|
||||
import "github.com/bouncepaw/mycorrhiza/cfg"
|
||||
|
||||
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
||||
|
||||
//line history/view.qtpl:5
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line history/view.qtpl:5
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line history/view.qtpl:5
|
||||
func (rev Revision) StreamHyphaeLinksHTML(qw422016 *qt422016.Writer) {
|
||||
//line history/view.qtpl:5
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line history/view.qtpl:7
|
||||
for i, hyphaName := range rev.hyphaeAffected() {
|
||||
//line history/view.qtpl:8
|
||||
if i > 0 {
|
||||
//line history/view.qtpl:8
|
||||
qw422016.N().S(`<span aria-hidden="true">, </span>`)
|
||||
//line history/view.qtpl:10
|
||||
}
|
||||
//line history/view.qtpl:10
|
||||
qw422016.N().S(`<a href="/primitive-diff/`)
|
||||
//line history/view.qtpl:11
|
||||
qw422016.E().S(rev.Hash)
|
||||
//line history/view.qtpl:11
|
||||
qw422016.N().S(`/`)
|
||||
//line history/view.qtpl:11
|
||||
qw422016.E().S(hyphaName)
|
||||
//line history/view.qtpl:11
|
||||
qw422016.N().S(`">`)
|
||||
//line history/view.qtpl:11
|
||||
qw422016.E().S(hyphaName)
|
||||
//line history/view.qtpl:11
|
||||
qw422016.N().S(`</a>`)
|
||||
//line history/view.qtpl:12
|
||||
}
|
||||
//line history/view.qtpl:13
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line history/view.qtpl:14
|
||||
}
|
||||
|
||||
//line history/view.qtpl:14
|
||||
func (rev Revision) WriteHyphaeLinksHTML(qq422016 qtio422016.Writer) {
|
||||
//line history/view.qtpl:14
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line history/view.qtpl:14
|
||||
rev.StreamHyphaeLinksHTML(qw422016)
|
||||
//line history/view.qtpl:14
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line history/view.qtpl:14
|
||||
}
|
||||
|
||||
//line history/view.qtpl:14
|
||||
func (rev Revision) HyphaeLinksHTML() string {
|
||||
//line history/view.qtpl:14
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line history/view.qtpl:14
|
||||
rev.WriteHyphaeLinksHTML(qb422016)
|
||||
//line history/view.qtpl:14
|
||||
qs422016 := string(qb422016.B)
|
||||
//line history/view.qtpl:14
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line history/view.qtpl:14
|
||||
return qs422016
|
||||
//line history/view.qtpl:14
|
||||
}
|
||||
|
||||
// descriptionForFeed generates a good enough HTML contents for a web feed.
|
||||
|
||||
//line history/view.qtpl:17
|
||||
func (rev *Revision) streamdescriptionForFeed(qw422016 *qt422016.Writer) {
|
||||
//line history/view.qtpl:17
|
||||
qw422016.N().S(`
|
||||
<p><b>`)
|
||||
//line history/view.qtpl:18
|
||||
qw422016.E().S(rev.Message)
|
||||
//line history/view.qtpl:18
|
||||
qw422016.N().S(`</b> (by `)
|
||||
//line history/view.qtpl:18
|
||||
qw422016.E().S(rev.Username)
|
||||
//line history/view.qtpl:18
|
||||
qw422016.N().S(` at `)
|
||||
//line history/view.qtpl:18
|
||||
qw422016.E().S(rev.TimeString())
|
||||
//line history/view.qtpl:18
|
||||
qw422016.N().S(`)</p>
|
||||
<p>Hyphae affected: `)
|
||||
//line history/view.qtpl:19
|
||||
rev.StreamHyphaeLinksHTML(qw422016)
|
||||
//line history/view.qtpl:19
|
||||
qw422016.N().S(`</p>
|
||||
<pre><code>`)
|
||||
//line history/view.qtpl:20
|
||||
qw422016.E().S(rev.textDiff())
|
||||
//line history/view.qtpl:20
|
||||
qw422016.N().S(`</code></pre>
|
||||
`)
|
||||
//line history/view.qtpl:21
|
||||
}
|
||||
|
||||
//line history/view.qtpl:21
|
||||
func (rev *Revision) writedescriptionForFeed(qq422016 qtio422016.Writer) {
|
||||
//line history/view.qtpl:21
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line history/view.qtpl:21
|
||||
rev.streamdescriptionForFeed(qw422016)
|
||||
//line history/view.qtpl:21
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line history/view.qtpl:21
|
||||
}
|
||||
|
||||
//line history/view.qtpl:21
|
||||
func (rev *Revision) descriptionForFeed() string {
|
||||
//line history/view.qtpl:21
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line history/view.qtpl:21
|
||||
rev.writedescriptionForFeed(qb422016)
|
||||
//line history/view.qtpl:21
|
||||
qs422016 := string(qb422016.B)
|
||||
//line history/view.qtpl:21
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line history/view.qtpl:21
|
||||
return qs422016
|
||||
//line history/view.qtpl:21
|
||||
}
|
||||
|
||||
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
|
||||
|
||||
//line history/view.qtpl:24
|
||||
func StreamWithRevisions(qw422016 *qt422016.Writer, hyphaName string, revs []Revision) {
|
||||
//line history/view.qtpl:24
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line history/view.qtpl:25
|
||||
for _, grp := range groupRevisionsByMonth(revs) {
|
||||
//line history/view.qtpl:25
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line history/view.qtpl:27
|
||||
currentYear := grp[0].Time.Year()
|
||||
currentMonth := grp[0].Time.Month()
|
||||
sectionId := fmt.Sprintf("%d-%d", currentYear, currentMonth)
|
||||
|
||||
//line history/view.qtpl:30
|
||||
qw422016.N().S(`
|
||||
<section class="history__month">
|
||||
<a href="#`)
|
||||
//line history/view.qtpl:32
|
||||
qw422016.E().S(sectionId)
|
||||
//line history/view.qtpl:32
|
||||
qw422016.N().S(`" class="history__month-anchor">
|
||||
<h2 id="`)
|
||||
//line history/view.qtpl:33
|
||||
qw422016.E().S(sectionId)
|
||||
//line history/view.qtpl:33
|
||||
qw422016.N().S(`" class="history__month-title">`)
|
||||
//line history/view.qtpl:33
|
||||
qw422016.N().D(currentYear)
|
||||
//line history/view.qtpl:33
|
||||
qw422016.N().S(` `)
|
||||
//line history/view.qtpl:33
|
||||
qw422016.E().S(currentMonth.String())
|
||||
//line history/view.qtpl:33
|
||||
qw422016.N().S(`</h2>
|
||||
</a>
|
||||
<ul class="history__entries">
|
||||
`)
|
||||
//line history/view.qtpl:36
|
||||
for _, rev := range grp {
|
||||
//line history/view.qtpl:36
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line history/view.qtpl:37
|
||||
rev.streamasHistoryEntry(qw422016, hyphaName)
|
||||
//line history/view.qtpl:37
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line history/view.qtpl:38
|
||||
}
|
||||
//line history/view.qtpl:38
|
||||
qw422016.N().S(`
|
||||
</ul>
|
||||
</section>
|
||||
`)
|
||||
//line history/view.qtpl:41
|
||||
}
|
||||
//line history/view.qtpl:41
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line history/view.qtpl:42
|
||||
}
|
||||
|
||||
//line history/view.qtpl:42
|
||||
func WriteWithRevisions(qq422016 qtio422016.Writer, hyphaName string, revs []Revision) {
|
||||
//line history/view.qtpl:42
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line history/view.qtpl:42
|
||||
StreamWithRevisions(qw422016, hyphaName, revs)
|
||||
//line history/view.qtpl:42
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line history/view.qtpl:42
|
||||
}
|
||||
|
||||
//line history/view.qtpl:42
|
||||
func WithRevisions(hyphaName string, revs []Revision) string {
|
||||
//line history/view.qtpl:42
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line history/view.qtpl:42
|
||||
WriteWithRevisions(qb422016, hyphaName, revs)
|
||||
//line history/view.qtpl:42
|
||||
qs422016 := string(qb422016.B)
|
||||
//line history/view.qtpl:42
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line history/view.qtpl:42
|
||||
return qs422016
|
||||
//line history/view.qtpl:42
|
||||
}
|
||||
|
||||
//line history/view.qtpl:44
|
||||
func (rev *Revision) streamasHistoryEntry(qw422016 *qt422016.Writer, hyphaName string) {
|
||||
//line history/view.qtpl:44
|
||||
qw422016.N().S(`
|
||||
<li class="history__entry">
|
||||
<a class="history-entry" href="/rev/`)
|
||||
//line history/view.qtpl:46
|
||||
qw422016.E().S(rev.Hash)
|
||||
//line history/view.qtpl:46
|
||||
qw422016.N().S(`/`)
|
||||
//line history/view.qtpl:46
|
||||
qw422016.E().S(hyphaName)
|
||||
//line history/view.qtpl:46
|
||||
qw422016.N().S(`">
|
||||
<time class="history-entry__time">`)
|
||||
//line history/view.qtpl:47
|
||||
qw422016.E().S(rev.timeToDisplay())
|
||||
//line history/view.qtpl:47
|
||||
qw422016.N().S(`</time>
|
||||
</a>
|
||||
<span class="history-entry__hash"><a href="/primitive-diff/`)
|
||||
//line history/view.qtpl:49
|
||||
qw422016.E().S(rev.Hash)
|
||||
//line history/view.qtpl:49
|
||||
qw422016.N().S(`/`)
|
||||
//line history/view.qtpl:49
|
||||
qw422016.E().S(hyphaName)
|
||||
//line history/view.qtpl:49
|
||||
qw422016.N().S(`">`)
|
||||
//line history/view.qtpl:49
|
||||
qw422016.E().S(rev.Hash)
|
||||
//line history/view.qtpl:49
|
||||
qw422016.N().S(`</a></span>
|
||||
<span class="history-entry__msg">`)
|
||||
//line history/view.qtpl:50
|
||||
qw422016.E().S(rev.Message)
|
||||
//line history/view.qtpl:50
|
||||
qw422016.N().S(`</span>
|
||||
`)
|
||||
//line history/view.qtpl:51
|
||||
if rev.Username != "anon" {
|
||||
//line history/view.qtpl:51
|
||||
qw422016.N().S(`
|
||||
<span class="history-entry__author">by <a href="/hypha/`)
|
||||
//line history/view.qtpl:52
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line history/view.qtpl:52
|
||||
qw422016.N().S(`/`)
|
||||
//line history/view.qtpl:52
|
||||
qw422016.E().S(rev.Username)
|
||||
//line history/view.qtpl:52
|
||||
qw422016.N().S(`" rel="author">`)
|
||||
//line history/view.qtpl:52
|
||||
qw422016.E().S(rev.Username)
|
||||
//line history/view.qtpl:52
|
||||
qw422016.N().S(`</a></span>
|
||||
`)
|
||||
//line history/view.qtpl:53
|
||||
}
|
||||
//line history/view.qtpl:53
|
||||
qw422016.N().S(`
|
||||
</li>
|
||||
`)
|
||||
//line history/view.qtpl:55
|
||||
}
|
||||
|
||||
//line history/view.qtpl:55
|
||||
func (rev *Revision) writeasHistoryEntry(qq422016 qtio422016.Writer, hyphaName string) {
|
||||
//line history/view.qtpl:55
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line history/view.qtpl:55
|
||||
rev.streamasHistoryEntry(qw422016, hyphaName)
|
||||
//line history/view.qtpl:55
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line history/view.qtpl:55
|
||||
}
|
||||
|
||||
//line history/view.qtpl:55
|
||||
func (rev *Revision) asHistoryEntry(hyphaName string) string {
|
||||
//line history/view.qtpl:55
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line history/view.qtpl:55
|
||||
rev.writeasHistoryEntry(qb422016, hyphaName)
|
||||
//line history/view.qtpl:55
|
||||
qs422016 := string(qb422016.B)
|
||||
//line history/view.qtpl:55
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line history/view.qtpl:55
|
||||
return qs422016
|
||||
//line history/view.qtpl:55
|
||||
}
|
@ -102,6 +102,7 @@ var localizations = map[string]string{
|
||||
"en.help.empty_error_link": "contributing",
|
||||
"en.help.empty_error_title": "This entry does not exist!",
|
||||
"en.help.entry_not_found": "Entry not found",
|
||||
"en.help.feeds": "Feeds",
|
||||
"en.help.hypha": "Hypha",
|
||||
"en.help.interface": "Interface",
|
||||
"en.help.lock": "Lock",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"sibling_hyphae": "Sibling hyphae",
|
||||
"special_pages": "Special pages",
|
||||
"recent_changes": "Recent changes",
|
||||
"feeds": "Feeds",
|
||||
"configuration": "Configuration (for administrators)",
|
||||
"lock": "Lock",
|
||||
"whitelist": "Whitelist",
|
||||
|
1
main.go
1
main.go
@ -1,5 +1,6 @@
|
||||
//go:generate qtc -dir=views
|
||||
//go:generate qtc -dir=tree
|
||||
//go:generate qtc -dir=history
|
||||
//go:generate go-localize -input l18n_src -output l18n
|
||||
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
||||
package main
|
||||
|
@ -96,7 +96,7 @@ func Tree(hyphaName string) (siblingsHTML, childrenHTML, prev, next string) {
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
children = figureOutChildren(hyphaName, true).children
|
||||
children = figureOutChildren(hyphaName).children
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
@ -124,7 +124,7 @@ type child struct {
|
||||
children []child
|
||||
}
|
||||
|
||||
func figureOutChildren(hyphaName string, exists bool) child {
|
||||
func figureOutChildren(hyphaName string) child {
|
||||
var (
|
||||
descPrefix = hyphaName + "/"
|
||||
child = child{hyphaName, true, make([]child, 0)}
|
||||
|
@ -176,6 +176,7 @@ It outputs a poorly formatted JSON, but it works and is valid.
|
||||
<li>{%s lc.GetWithLocale(lang, "help.special_pages") %}
|
||||
<ul>
|
||||
<li><a href="/help/{%s lang %}/recent_changes">{%s lc.GetWithLocale(lang, "help.recent_changes") %}</a></li>
|
||||
<li><a href="/help/{%s lang %}/feeds">{%s lc.GetWithLocale(lang, "help.feeds") %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>{%s lc.GetWithLocale(lang, "help.configuration") %}
|
||||
|
@ -707,41 +707,50 @@ func streamhelpTopicsHTML(qw422016 *qt422016.Writer, lang string, lc *l18n.Local
|
||||
//line views/stuff.qtpl:178
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.recent_changes"))
|
||||
//line views/stuff.qtpl:178
|
||||
qw422016.N().S(`</a></li>
|
||||
<li><a href="/help/`)
|
||||
//line views/stuff.qtpl:179
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:179
|
||||
qw422016.N().S(`/feeds">`)
|
||||
//line views/stuff.qtpl:179
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.feeds"))
|
||||
//line views/stuff.qtpl:179
|
||||
qw422016.N().S(`</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>`)
|
||||
//line views/stuff.qtpl:181
|
||||
//line views/stuff.qtpl:182
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.configuration"))
|
||||
//line views/stuff.qtpl:181
|
||||
//line views/stuff.qtpl:182
|
||||
qw422016.N().S(`
|
||||
<ul>
|
||||
<li><a href="/help/`)
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.N().S(`/lock">`)
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.lock"))
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.N().S(`</a></li>
|
||||
<li><a href="/help/`)
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.N().S(`/whitelist">`)
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.whitelist"))
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.N().S(`</a></li>
|
||||
<li><a href="/help/`)
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.N().S(`/telegram">`)
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.telegram"))
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.N().S(`</a></li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
@ -749,91 +758,91 @@ func streamhelpTopicsHTML(qw422016 *qt422016.Writer, lang string, lc *l18n.Local
|
||||
</ul>
|
||||
</aside>
|
||||
`)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
func writehelpTopicsHTML(qq422016 qtio422016.Writer, lang string, lc *l18n.Localizer) {
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
streamhelpTopicsHTML(qw422016, lang, lc)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
func helpTopicsHTML(lang string, lc *l18n.Localizer) string {
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
writehelpTopicsHTML(qb422016, lang, lc)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
return qs422016
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:193
|
||||
//line views/stuff.qtpl:194
|
||||
func streamhelpTopicBadgeHTML(qw422016 *qt422016.Writer, lang, topic string) {
|
||||
//line views/stuff.qtpl:193
|
||||
//line views/stuff.qtpl:194
|
||||
qw422016.N().S(`
|
||||
<a class="help-topic-badge" href="/help/`)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
qw422016.E().S(topic)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
qw422016.N().S(`">?</a>
|
||||
`)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
func writehelpTopicBadgeHTML(qq422016 qtio422016.Writer, lang, topic string) {
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
streamhelpTopicBadgeHTML(qw422016, lang, topic)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
func helpTopicBadgeHTML(lang, topic string) string {
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
writehelpTopicBadgeHTML(qb422016, lang, topic)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
return qs422016
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:197
|
||||
//line views/stuff.qtpl:198
|
||||
func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||
//line views/stuff.qtpl:197
|
||||
//line views/stuff.qtpl:198
|
||||
qw422016.N().S(`
|
||||
<div class="layout">
|
||||
<main class="main-width user-list">
|
||||
<h1>`)
|
||||
//line views/stuff.qtpl:200
|
||||
//line views/stuff.qtpl:201
|
||||
qw422016.E().S(lc.Get("ui.users_heading"))
|
||||
//line views/stuff.qtpl:200
|
||||
//line views/stuff.qtpl:201
|
||||
qw422016.N().S(`</h1>
|
||||
`)
|
||||
//line views/stuff.qtpl:202
|
||||
//line views/stuff.qtpl:203
|
||||
var (
|
||||
admins = make([]string, 0)
|
||||
moderators = make([]string, 0)
|
||||
@ -853,149 +862,149 @@ func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||
sort.Strings(moderators)
|
||||
sort.Strings(editors)
|
||||
|
||||
//line views/stuff.qtpl:220
|
||||
//line views/stuff.qtpl:221
|
||||
qw422016.N().S(`
|
||||
<section>
|
||||
<h2>`)
|
||||
//line views/stuff.qtpl:222
|
||||
//line views/stuff.qtpl:223
|
||||
qw422016.E().S(lc.Get("ui.users_admins"))
|
||||
//line views/stuff.qtpl:222
|
||||
//line views/stuff.qtpl:223
|
||||
qw422016.N().S(`</h2>
|
||||
<ol>`)
|
||||
//line views/stuff.qtpl:223
|
||||
//line views/stuff.qtpl:224
|
||||
for _, name := range admins {
|
||||
//line views/stuff.qtpl:223
|
||||
//line views/stuff.qtpl:224
|
||||
qw422016.N().S(`
|
||||
<li><a href="/hypha/`)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.N().S(`</a></li>
|
||||
`)
|
||||
//line views/stuff.qtpl:225
|
||||
//line views/stuff.qtpl:226
|
||||
}
|
||||
//line views/stuff.qtpl:225
|
||||
//line views/stuff.qtpl:226
|
||||
qw422016.N().S(`</ol>
|
||||
</section>
|
||||
<section>
|
||||
<h2>`)
|
||||
//line views/stuff.qtpl:228
|
||||
//line views/stuff.qtpl:229
|
||||
qw422016.E().S(lc.Get("ui.users_moderators"))
|
||||
//line views/stuff.qtpl:228
|
||||
//line views/stuff.qtpl:229
|
||||
qw422016.N().S(`</h2>
|
||||
<ol>`)
|
||||
//line views/stuff.qtpl:229
|
||||
//line views/stuff.qtpl:230
|
||||
for _, name := range moderators {
|
||||
//line views/stuff.qtpl:229
|
||||
//line views/stuff.qtpl:230
|
||||
qw422016.N().S(`
|
||||
<li><a href="/hypha/`)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.N().S(`</a></li>
|
||||
`)
|
||||
//line views/stuff.qtpl:231
|
||||
//line views/stuff.qtpl:232
|
||||
}
|
||||
//line views/stuff.qtpl:231
|
||||
//line views/stuff.qtpl:232
|
||||
qw422016.N().S(`</ol>
|
||||
</section>
|
||||
<section>
|
||||
<h2>`)
|
||||
//line views/stuff.qtpl:234
|
||||
//line views/stuff.qtpl:235
|
||||
qw422016.E().S(lc.Get("ui.users_editors"))
|
||||
//line views/stuff.qtpl:234
|
||||
//line views/stuff.qtpl:235
|
||||
qw422016.N().S(`</h2>
|
||||
<ol>`)
|
||||
//line views/stuff.qtpl:235
|
||||
//line views/stuff.qtpl:236
|
||||
for _, name := range editors {
|
||||
//line views/stuff.qtpl:235
|
||||
//line views/stuff.qtpl:236
|
||||
qw422016.N().S(`
|
||||
<li><a href="/hypha/`)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.N().S(`</a></li>
|
||||
`)
|
||||
//line views/stuff.qtpl:237
|
||||
//line views/stuff.qtpl:238
|
||||
}
|
||||
//line views/stuff.qtpl:237
|
||||
//line views/stuff.qtpl:238
|
||||
qw422016.N().S(`</ol>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
`)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
func WriteUserListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
StreamUserListHTML(qw422016, lc)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
func UserListHTML(lc *l18n.Localizer) string {
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
WriteUserListHTML(qb422016, lc)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
return qs422016
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:243
|
||||
//line views/stuff.qtpl:244
|
||||
func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||
//line views/stuff.qtpl:243
|
||||
//line views/stuff.qtpl:244
|
||||
qw422016.N().S(`
|
||||
<div class="layout">
|
||||
<main class="main-width">
|
||||
<h1>`)
|
||||
//line views/stuff.qtpl:246
|
||||
//line views/stuff.qtpl:247
|
||||
qw422016.E().S(lc.Get("ui.list_heading"))
|
||||
//line views/stuff.qtpl:246
|
||||
//line views/stuff.qtpl:247
|
||||
qw422016.N().S(`</h1>
|
||||
<p>`)
|
||||
//line views/stuff.qtpl:247
|
||||
//line views/stuff.qtpl:248
|
||||
qw422016.E().S(lc.GetPlural("ui.list_desc", hyphae.Count()))
|
||||
//line views/stuff.qtpl:247
|
||||
//line views/stuff.qtpl:248
|
||||
qw422016.N().S(`</p>
|
||||
<ul class="hypha-list">
|
||||
`)
|
||||
//line views/stuff.qtpl:250
|
||||
//line views/stuff.qtpl:251
|
||||
hyphaNames := make(chan string)
|
||||
sortedHypha := hyphae.PathographicSort(hyphaNames)
|
||||
for hypha := range hyphae.YieldExistingHyphae() {
|
||||
@ -1003,252 +1012,252 @@ func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||
}
|
||||
close(hyphaNames)
|
||||
|
||||
//line views/stuff.qtpl:256
|
||||
//line views/stuff.qtpl:257
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line views/stuff.qtpl:257
|
||||
//line views/stuff.qtpl:258
|
||||
for hyphaName := range sortedHypha {
|
||||
//line views/stuff.qtpl:257
|
||||
//line views/stuff.qtpl:258
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line views/stuff.qtpl:258
|
||||
//line views/stuff.qtpl:259
|
||||
hypha := hyphae.ByName(hyphaName)
|
||||
|
||||
//line views/stuff.qtpl:258
|
||||
//line views/stuff.qtpl:259
|
||||
qw422016.N().S(`
|
||||
<li class="hypha-list__entry">
|
||||
<a class="hypha-list__link" href="/hypha/`)
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.E().S(hypha.Name)
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.E().S(util.BeautifulName(hypha.Name))
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.N().S(`</a>
|
||||
`)
|
||||
//line views/stuff.qtpl:261
|
||||
//line views/stuff.qtpl:262
|
||||
if hypha.BinaryPath != "" {
|
||||
//line views/stuff.qtpl:261
|
||||
//line views/stuff.qtpl:262
|
||||
qw422016.N().S(`
|
||||
<span class="hypha-list__amnt-type">`)
|
||||
//line views/stuff.qtpl:262
|
||||
//line views/stuff.qtpl:263
|
||||
qw422016.E().S(filepath.Ext(hypha.BinaryPath)[1:])
|
||||
//line views/stuff.qtpl:262
|
||||
//line views/stuff.qtpl:263
|
||||
qw422016.N().S(`</span>
|
||||
`)
|
||||
//line views/stuff.qtpl:263
|
||||
//line views/stuff.qtpl:264
|
||||
}
|
||||
//line views/stuff.qtpl:263
|
||||
//line views/stuff.qtpl:264
|
||||
qw422016.N().S(`
|
||||
</li>
|
||||
`)
|
||||
//line views/stuff.qtpl:265
|
||||
//line views/stuff.qtpl:266
|
||||
}
|
||||
//line views/stuff.qtpl:265
|
||||
//line views/stuff.qtpl:266
|
||||
qw422016.N().S(`
|
||||
</ul>
|
||||
</main>
|
||||
</div>
|
||||
`)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
func WriteHyphaListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
StreamHyphaListHTML(qw422016, lc)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
func HyphaListHTML(lc *l18n.Localizer) string {
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
WriteHyphaListHTML(qb422016, lc)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
return qs422016
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:271
|
||||
//line views/stuff.qtpl:272
|
||||
func StreamAboutHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||
//line views/stuff.qtpl:271
|
||||
//line views/stuff.qtpl:272
|
||||
qw422016.N().S(`
|
||||
<div class="layout">
|
||||
<main class="main-width">
|
||||
<section>
|
||||
<h1>`)
|
||||
//line views/stuff.qtpl:275
|
||||
//line views/stuff.qtpl:276
|
||||
qw422016.E().S(lc.Get("ui.about_title", &l18n.Replacements{"name": cfg.WikiName}))
|
||||
//line views/stuff.qtpl:275
|
||||
//line views/stuff.qtpl:276
|
||||
qw422016.N().S(`</h1>
|
||||
<ul>
|
||||
<li><b>`)
|
||||
//line views/stuff.qtpl:277
|
||||
//line views/stuff.qtpl:278
|
||||
qw422016.N().S(lc.Get("ui.about_version", &l18n.Replacements{"pre": "<a href=\"https://mycorrhiza.wiki\">", "post": "</a>"}))
|
||||
//line views/stuff.qtpl:277
|
||||
//line views/stuff.qtpl:278
|
||||
qw422016.N().S(`</b> 1.5.0</li>
|
||||
`)
|
||||
//line views/stuff.qtpl:278
|
||||
//line views/stuff.qtpl:279
|
||||
if cfg.UseAuth {
|
||||
//line views/stuff.qtpl:278
|
||||
//line views/stuff.qtpl:279
|
||||
qw422016.N().S(` <li><b>`)
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.E().S(lc.Get("ui.about_usercount"))
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.N().S(`</b> `)
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.N().DUL(user.Count())
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.N().S(`</li>
|
||||
<li><b>`)
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.E().S(lc.Get("ui.about_homepage"))
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.N().S(`</b> <a href="/">`)
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.E().S(cfg.HomeHypha)
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.N().S(`</a></li>
|
||||
<li><b>`)
|
||||
//line views/stuff.qtpl:281
|
||||
//line views/stuff.qtpl:282
|
||||
qw422016.E().S(lc.Get("ui.about_admins"))
|
||||
//line views/stuff.qtpl:281
|
||||
//line views/stuff.qtpl:282
|
||||
qw422016.N().S(`</b>`)
|
||||
//line views/stuff.qtpl:281
|
||||
//line views/stuff.qtpl:282
|
||||
for i, username := range user.ListUsersWithGroup("admin") {
|
||||
//line views/stuff.qtpl:282
|
||||
//line views/stuff.qtpl:283
|
||||
if i > 0 {
|
||||
//line views/stuff.qtpl:282
|
||||
//line views/stuff.qtpl:283
|
||||
qw422016.N().S(`<span aria-hidden="true">, </span>
|
||||
`)
|
||||
//line views/stuff.qtpl:283
|
||||
//line views/stuff.qtpl:284
|
||||
}
|
||||
//line views/stuff.qtpl:283
|
||||
//line views/stuff.qtpl:284
|
||||
qw422016.N().S(` <a href="/hypha/`)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.E().S(username)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.E().S(username)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.N().S(`</a>`)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
}
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.N().S(`</li>
|
||||
`)
|
||||
//line views/stuff.qtpl:285
|
||||
//line views/stuff.qtpl:286
|
||||
} else {
|
||||
//line views/stuff.qtpl:285
|
||||
//line views/stuff.qtpl:286
|
||||
qw422016.N().S(` <li>`)
|
||||
//line views/stuff.qtpl:286
|
||||
//line views/stuff.qtpl:287
|
||||
qw422016.E().S(lc.Get("ui.about_noauth"))
|
||||
//line views/stuff.qtpl:286
|
||||
//line views/stuff.qtpl:287
|
||||
qw422016.N().S(`</li>
|
||||
`)
|
||||
//line views/stuff.qtpl:287
|
||||
//line views/stuff.qtpl:288
|
||||
}
|
||||
//line views/stuff.qtpl:287
|
||||
//line views/stuff.qtpl:288
|
||||
qw422016.N().S(` </ul>
|
||||
<p>`)
|
||||
//line views/stuff.qtpl:289
|
||||
//line views/stuff.qtpl:290
|
||||
qw422016.N().S(lc.Get("ui.about_hyphae", &l18n.Replacements{"link": "<a href=\"/list\">/list</a>"}))
|
||||
//line views/stuff.qtpl:289
|
||||
//line views/stuff.qtpl:290
|
||||
qw422016.N().S(`</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
`)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
func WriteAboutHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
StreamAboutHTML(qw422016, lc)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
func AboutHTML(lc *l18n.Localizer) string {
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
WriteAboutHTML(qb422016, lc)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
return qs422016
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:295
|
||||
//line views/stuff.qtpl:296
|
||||
func StreamCommonScripts(qw422016 *qt422016.Writer) {
|
||||
//line views/stuff.qtpl:295
|
||||
//line views/stuff.qtpl:296
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line views/stuff.qtpl:296
|
||||
//line views/stuff.qtpl:297
|
||||
for _, scriptPath := range cfg.CommonScripts {
|
||||
//line views/stuff.qtpl:296
|
||||
//line views/stuff.qtpl:297
|
||||
qw422016.N().S(`
|
||||
<script src="`)
|
||||
//line views/stuff.qtpl:297
|
||||
//line views/stuff.qtpl:298
|
||||
qw422016.E().S(scriptPath)
|
||||
//line views/stuff.qtpl:297
|
||||
//line views/stuff.qtpl:298
|
||||
qw422016.N().S(`"></script>
|
||||
`)
|
||||
//line views/stuff.qtpl:298
|
||||
//line views/stuff.qtpl:299
|
||||
}
|
||||
//line views/stuff.qtpl:298
|
||||
//line views/stuff.qtpl:299
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
func WriteCommonScripts(qq422016 qtio422016.Writer) {
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
StreamCommonScripts(qw422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
}
|
||||
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
func CommonScripts() string {
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
WriteCommonScripts(qb422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
return qs422016
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
}
|
||||
|
@ -61,8 +61,14 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
|
||||
}
|
||||
|
||||
// genericHandlerOfFeeds is a helper function for the web feed handlers.
|
||||
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string, contentType string) {
|
||||
if content, err := f(); err != nil {
|
||||
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func(history.FeedOptions) (string, error), name string, contentType string) {
|
||||
opts, err := history.ParseFeedOptions(rq.URL.Query())
|
||||
var content string
|
||||
if err == nil {
|
||||
content, err = f(opts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
|
||||
|
Loading…
Reference in New Issue
Block a user