1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-12-12 05:20:26 +00:00

Merge pull request #106 from pyelias/rss-grouping

Group recent-changes feeds (#61)
This commit is contained in:
handlerug 2021-10-28 13:15:06 +07:00 committed by GitHub
commit 80f37420db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1231 additions and 556 deletions

39
help/en/feeds.myco Normal file
View 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
View 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)
}

View File

@ -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))

View File

@ -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
}

View File

@ -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
View 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
View 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
View 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
}

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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)}

View File

@ -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") %}

View File

@ -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
}

View File

@ -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())