mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-20 23:36:51 +00:00
reorganize the history package (and rewrite some parts with qtpl)
start work on grouping edits in feeds
This commit is contained in:
parent
e4cd5e4a5f
commit
924b011e06
84
history/feed.go
Normal file
84
history/feed.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package history
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
|
)
|
||||||
|
|
||||||
|
var groupPeriod, _ = time.ParseDuration("30m")
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
revs := RecentChanges(30)
|
||||||
|
groups := groupRevisionsByPeriod(revs, groupPeriod)
|
||||||
|
for _, grp := range groups {
|
||||||
|
item := grp.feedItem()
|
||||||
|
feed.Add(&item)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) feedItem() feeds.Item {
|
||||||
|
return feeds.Item{
|
||||||
|
Title: grp.title(),
|
||||||
|
Author: grp.author(),
|
||||||
|
Id: grp[0].Hash,
|
||||||
|
Description: grp.descriptionForFeed(),
|
||||||
|
Created: grp[len(grp)-1].Time, // earliest revision
|
||||||
|
Updated: grp[0].Time, // latest revision
|
||||||
|
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) title() string {
|
||||||
|
if len(grp) == 1 {
|
||||||
|
return grp[0].Message
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%d edits (%s, ...)", len(grp), grp[0].Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) author() *feeds.Author {
|
||||||
|
author := grp[0].Username
|
||||||
|
for _, rev := range grp[1:] {
|
||||||
|
// if they don't all have the same author, return nil
|
||||||
|
if rev.Username != author {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &feeds.Author{Name: author}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) descriptionForFeed() string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
for _, rev := range grp {
|
||||||
|
builder.WriteString(rev.descriptionForFeed())
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
@ -4,14 +4,10 @@ package history
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/files"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"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͡ʃ].
|
// I pronounce it as [gɪt͡ʃ].
|
||||||
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
||||||
func gitsh(args ...string) (out bytes.Buffer, err error) {
|
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
|
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`.
|
// Rename renames from `from` to `to` using `git mv`.
|
||||||
func Rename(from, to string) error {
|
func Rename(from, to string) error {
|
||||||
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
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)
|
hop.Errs = append(hop.Errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := Rename(from, to); err != nil {
|
hop.gitop("mv", "--force", from, to)
|
||||||
hop.Errs = append(hop.Errs, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hop
|
return hop
|
||||||
|
255
history/revision.go
Normal file
255
history/revision.go
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice, ordered most recent first.
|
||||||
|
func RecentChanges(n int) []Revision {
|
||||||
|
var (
|
||||||
|
out, err = silentGitsh(
|
||||||
|
"log", "--oneline", "--no-merges",
|
||||||
|
"--pretty=format:%h\t%ae\t%at\t%s",
|
||||||
|
"--max-count="+strconv.Itoa(n),
|
||||||
|
)
|
||||||
|
revs []Revision
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
for _, line := range strings.Split(out.String(), "\n") {
|
||||||
|
revs = append(revs, parseRevisionLine(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("Found %d recent changes", len(revs))
|
||||||
|
return revs
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileChanged tells you if the file has been changed since the last commit.
|
||||||
|
func FileChanged(path string) bool {
|
||||||
|
_, err := gitsh("diff", "--exit-code", path)
|
||||||
|
return err != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revisions returns slice of revisions for the given hypha name, ordered most recent first.
|
||||||
|
func Revisions(hyphaName string) ([]Revision, error) {
|
||||||
|
var (
|
||||||
|
out, err = silentGitsh(
|
||||||
|
"log", "--oneline", "--no-merges",
|
||||||
|
// Hash, author email, author time, commit msg separated by tab
|
||||||
|
"--pretty=format:%h\t%ae\t%at\t%s",
|
||||||
|
"--", hyphaName+".*",
|
||||||
|
)
|
||||||
|
revs []Revision
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
for _, line := range strings.Split(out.String(), "\n") {
|
||||||
|
if line != "" {
|
||||||
|
revs = append(revs, parseRevisionLine(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName)
|
||||||
|
return revs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return time like 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type revisionGroup []Revision
|
||||||
|
|
||||||
|
func newRevisionGroup(rev Revision) revisionGroup {
|
||||||
|
return revisionGroup([]Revision{rev})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp *revisionGroup) addRevision(rev Revision) {
|
||||||
|
*grp = append(*grp, rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
|
||||||
|
var (
|
||||||
|
currentYear int
|
||||||
|
currentMonth time.Month
|
||||||
|
)
|
||||||
|
for _, rev := range revs {
|
||||||
|
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
|
||||||
|
currentYear = rev.Time.Year()
|
||||||
|
currentMonth = rev.Time.Month()
|
||||||
|
res = append(res, newRevisionGroup(rev))
|
||||||
|
} else {
|
||||||
|
res[len(res)-1].addRevision(rev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupRevisionsByPeriod groups revisions by how long ago they were.
|
||||||
|
// Revisions less than one period ago are placed in one group, revisions between one and two periods are in another, and so on.
|
||||||
|
func groupRevisionsByPeriod(revs []Revision, period time.Duration) (res []revisionGroup) {
|
||||||
|
now := time.Now()
|
||||||
|
currentPeriod := -1
|
||||||
|
for _, rev := range revs {
|
||||||
|
newPeriod := int(now.Sub(rev.Time).Seconds() / period.Seconds())
|
||||||
|
if newPeriod != currentPeriod {
|
||||||
|
currentPeriod = newPeriod
|
||||||
|
res = append(res, newRevisionGroup(rev))
|
||||||
|
} else {
|
||||||
|
res[len(res)-1].addRevision(rev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
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="/hypha/{%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 %}
|
326
history/view.qtpl.go
Normal file
326
history/view.qtpl.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
// 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="/hypha/`)
|
||||||
|
//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
|
||||||
|
}
|
1
main.go
1
main.go
@ -1,5 +1,6 @@
|
|||||||
//go:generate qtc -dir=views
|
//go:generate qtc -dir=views
|
||||||
//go:generate qtc -dir=tree
|
//go:generate qtc -dir=tree
|
||||||
|
//go:generate qtc -dir=history
|
||||||
//go:generate go-localize -input l18n_src -output l18n
|
//go:generate go-localize -input l18n_src -output l18n
|
||||||
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
||||||
package main
|
package main
|
||||||
|
Loading…
Reference in New Issue
Block a user