"github.com/go-git/go-git/v5/plumbing/object"
"github.com/hjson/hjson-go"
"go.stargrave.org/sgblog"
+ "go.stargrave.org/sgblog/cmd/sgblog/atom"
"golang.org/x/crypto/blake2b"
- "golang.org/x/tools/blog/atom"
)
const (
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="generator" content="SGBlog {{.Version}}">
- <title>{{.Cfg.Title}} ({{.Offset}}-{{.OffsetNext}})</title>
+ <title>{{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}})</title>
{{with .Cfg.CSS}}<link rel="stylesheet" type="text/css" href="{{.}}">{{end}}
{{with .Cfg.Webmaster}}<link rev="made" href="mailto:{{.}}">{{end}}
{{range .Cfg.GitURLs}}<link rel="vcs-git" href="{{.}}" title="Git repository">{{end}}
<link rel="top" href="{{.Cfg.URLPrefix}}/" title="top">
- <link rel="alternate" title="Posts feed" href="{{.Cfg.AtomBaseURL}}{{.Cfg.URLPrefix}}/{{.AtomPostsFeed}}" type="application/atom+xml">
+ <link rel="alternate" title="Posts feed" href="{{.Cfg.AtomBaseURL}}{{.Cfg.URLPrefix}}/{{.AtomPostsFeed}}{{if .Topic}}?topic={{.Topic}}{{end}}" type="application/atom+xml">
{{if .CommentsEnabled}}<link rel="alternate" title="Comments feed" href="{{.Cfg.AtomBaseURL}}{{.Cfg.URLPrefix}}/{{.AtomCommentsFeed}}" type="application/atom+xml">{{end}}
- {{if .Offset}}<link rel="prev" href="{{.Cfg.URLPrefix}}/{{if .OffsetPrev}}?offset={{.OffsetPrev}}{{end}}" title="prev">{{end}}
- {{if not .LogEnded}}<link rel="next" href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}" title="next">{{end}}
+ {{if .Offset}}<link rel="prev" href="{{.Cfg.URLPrefix}}/?offset={{.OffsetPrev}}{{if .Topic}}&topic={{.Topic}}{{end}}" title="prev">{{end}}
+ {{if not .LogEnded}}<link rel="next" href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}{{if .Topic}}&topic={{.Topic}}{{end}}" title="next">{{end}}
</head>
<body>
{{with .Cfg.AboutURL}}[<a href="{{.}}">about</a>]{{end}}
{{block "links" .}}
-{{if .Offset}}[<a href="{{.Cfg.URLPrefix}}/{{if .OffsetPrev}}?offset={{.OffsetPrev}}{{end}}">prev</a>]{{end}}
-{{if not .LogEnded}}[<a href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}">next</a>]{{end}}
+{{if .Offset}}[<a href="{{.Cfg.URLPrefix}}/?offset={{.OffsetPrev}}{{if .Topic}}&topic={{.Topic}}{{end}}">prev</a>]{{end}}
+{{if not .LogEnded}}[<a href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}{{if .Topic}}&topic={{.Topic}}{{end}}">next</a>]{{end}}
{{end}}
{{- $Cfg := .Cfg -}}
+{{if .Topics}}<hr/>
+Topics: [<tt><a href="{{$Cfg.URLPrefix}}/">ALL</a></tt>]
+{{range .Topics}}[<tt><a href="{{$Cfg.URLPrefix}}?topic={{.}}">{{.}}</a></tt>]
+{{end}}
+{{end}}
+{{- $TopicsEnabled := .TopicsEnabled -}}
{{- $datePrev := "0001-01-01" -}}
<table border=1>
<tr>
<th size="5%"><a title="Lines">L</a></th>
<th size="5%"><a title="Comments">C</a></th>
<th>Linked to</th>
+ {{if .TopicsEnabled}}<th>Topics</th>{{end}}
</tr>
{{range .Entries -}}
{{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}}
{{- if ne $dateCur $datePrev -}}
- <tr><td colspan=6><center><tt>{{$dateCur}}</tt></center></td></tr>
+ <tr><td colspan={{if $TopicsEnabled}}7{{else}}7{{end}}><center><tt>{{$dateCur}}</tt></center></td></tr>
{{- $datePrev = $dateCur -}}
{{- end -}}
<tr>
<td>{{.LinesNum}}</td>
<td>{{if .CommentsNum}}{{.CommentsNum}}{{else}} {{end}}</td>
<td>{{if .DomainURLs}}{{range .DomainURLs}} {{.}} {{end}}{{else}} {{end}}</td>
+ {{if $TopicsEnabled}}<td>{{if .Topics}}{{range .Topics}} <a href="{{$Cfg.URLPrefix}}/?topic={{.}}">{{.}}</a> {{end}}{{else}} {{end}}</td>{{end}}
</tr>
{{end}}</table>
{{template "links" .}}
[<tt><a title="When">{{.When}}</a></tt>]
[<tt><a title="What">{{.Commit.Hash.String}}</a></tt>]
+{{if .Topics}}
+<hr/>
+Topics: {{range .Topics}}[<tt><a href="{{$Cfg.URLPrefix}}?topic={{.}}">{{.}}</a></tt>]{{end}}
+{{end}}
+
<hr/>
<h2>{{.Title}}</h2>
<pre>
{{end}}</pre>
<hr/>{{end}}
-{{if .Cfg.CommentsEmail}}[<a href="mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}">leave comment</a>]{{end}}
+{{if .Cfg.CommentsEmail}}[<a href="mailto:{{.Cfg.CommentsEmail}}?subject={{.TitleEscaped}}">leave comment</a>]{{end}}
<dl>{{range $idx, $comment := .Comments}}
<dt><a name="comment{{$idx}}"><a href="#comment{{$idx}}">comment {{$idx}}</a>:</dt>
type TableEntry struct {
Commit *object.Commit
CommentsRaw []byte
+ TopicsRaw []byte
Num int
Title string
LinesNum int
CommentsNum int
DomainURLs []string
+ Topics []string
}
type CommentEntry struct {
return fmt.Sprintf("%x-%x-%x-%x-%x", raw[0:4], raw[4:6], raw[6:8], raw[8:10], raw[10:])
}
+type CommitIterNext interface {
+ Next() (*object.Commit, error)
+}
+
func serveHTTP() {
cfgPath := os.Getenv("SGBLOG_CFG")
if cfgPath == "" {
if err != nil {
makeErr(err)
}
+ topicsCache, err := getTopicsCache(cfg, repoLog)
+ if err != nil {
+ makeErr(err)
+ }
+ repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
+ if err != nil {
+ makeErr(err)
+ }
+
commitN := 0
- for i := 0; i < offset; i++ {
- if _, err = repoLog.Next(); err != nil {
- break
+ var commits CommitIterNext
+ var topic string
+ if t, exists := queryValues["topic"]; exists {
+ topic = t[0]
+ hashes := topicsCache[topic]
+ if hashes == nil {
+ makeErr(errors.New("no posts with that topic"))
}
- commitN++
+ if len(hashes) > offset {
+ hashes = hashes[offset:]
+ commitN += offset
+ }
+ commits = &HashesIter{hashes}
+ } else {
+ for i := 0; i < offset; i++ {
+ if _, err = repoLog.Next(); err != nil {
+ break
+ }
+ commitN++
+ }
+ commits = repoLog
}
entries := make([]TableEntry, 0, PageEntries)
etagHash.Write([]byte(data))
}
etagHash.Write([]byte("INDEX"))
+ etagHash.Write([]byte(topic))
for i := 0; i < PageEntries; i++ {
- commit, err := repoLog.Next()
+ commit, err := commits.Next()
if err != nil {
logEnded = true
break
etagHash.Write(commit.Hash[:])
commentsRaw := getNote(commentsTree, commit.Hash)
etagHash.Write(commentsRaw)
+ topicsRaw := getNote(topicsTree, commit.Hash)
+ etagHash.Write(topicsRaw)
entries = append(entries, TableEntry{
Commit: commit,
CommentsRaw: commentsRaw,
+ TopicsRaw: topicsRaw,
})
}
checkETag(etagHash)
entry.DomainURLs = append(entry.DomainURLs, makeA(line, u.Host))
}
entry.CommentsNum = len(parseComments(entry.CommentsRaw))
+ entry.Topics = parseTopics(entry.TopicsRaw)
entries[i] = entry
}
+ offsetPrev := offset - PageEntries
+ if offsetPrev < 0 {
+ offsetPrev = 0
+ }
tmpl := template.Must(template.New("index").Parse(TmplHTMLIndex))
os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
err = tmpl.Execute(out, struct {
Version string
Cfg *Cfg
+ Topic string
+ TopicsEnabled bool
+ Topics []string
CommentsEnabled bool
AtomPostsFeed string
AtomCommentsFeed string
}{
Version: sgblog.Version,
Cfg: cfg,
+ Topic: topic,
+ TopicsEnabled: topicsTree != nil,
+ Topics: topicsCache.Topics(),
CommentsEnabled: commentsTree != nil,
AtomPostsFeed: AtomPostsFeed,
AtomCommentsFeed: AtomCommentsFeed,
Offset: offset,
- OffsetPrev: offset - PageEntries,
+ OffsetPrev: offsetPrev,
OffsetNext: offset + PageEntries,
LogEnded: logEnded,
Entries: entries,
if err != nil {
makeErr(err)
}
+
+ var topic string
+ if t, exists := queryValues["topic"]; exists {
+ topic = t[0]
+ }
+
etagHash.Write([]byte("ATOM POSTS"))
+ etagHash.Write([]byte(topic))
etagHash.Write(commit.Hash[:])
checkETag(etagHash)
+ var title string
+ if topic == "" {
+ title = cfg.Title
+ } else {
+ title = fmt.Sprintf("%s (topic: %s)", cfg.Title, topic)
+ }
+ idHasher, err := blake2b.New256(nil)
+ if err != nil {
+ panic(err)
+ }
+ idHasher.Write([]byte("ATOM POSTS"))
+ idHasher.Write([]byte(cfg.AtomId))
+ idHasher.Write([]byte(topic))
feed := atom.Feed{
- Title: cfg.Title,
- ID: cfg.AtomId,
+ Title: title,
+ ID: "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
Updated: atom.Time(commit.Author.When),
Link: []atom.Link{{
Rel: "self",
}},
Author: &atom.Person{Name: cfg.AtomAuthor},
}
+
repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
if err != nil {
makeErr(err)
}
+ var commits CommitIterNext
+ if topic == "" {
+ commits = repoLog
+ } else {
+ topicsCache, err := getTopicsCache(cfg, repoLog)
+ if err != nil {
+ makeErr(err)
+ }
+ hashes := topicsCache[topic]
+ if hashes == nil {
+ makeErr(errors.New("no posts with that topic"))
+ }
+ commits = &HashesIter{hashes}
+ }
+
for i := 0; i < PageEntries; i++ {
- commit, err = repoLog.Next()
+ commit, err = commits.Next()
if err != nil {
break
}
lines := msgSplit(commit.Message)
+ var categories []atom.Category
+ for _, topic := range parseTopics(getNote(topicsTree, commit.Hash)) {
+ categories = append(categories, atom.Category{Term: topic})
+ }
+ htmlized := make([]string, 0, len(lines))
+ htmlized = append(htmlized, "<pre>")
+ for _, l := range lines[2:] {
+ htmlized = append(htmlized, lineURLize(cfg.AtomBaseURL+cfg.URLPrefix, l))
+ }
+ htmlized = append(htmlized, "</pre>")
feed.Entry = append(feed.Entry, &atom.Entry{
Title: lines[0],
ID: "urn:uuid:" + bytes2uuid(commit.Hash[:]),
Updated: atom.Time(commit.Author.When),
Summary: &atom.Text{Type: "text", Body: lines[0]},
Content: &atom.Text{
- Type: "text",
- Body: strings.Join(lines[2:], "\n"),
+ Type: "html",
+ Body: strings.Join(htmlized, "\n"),
},
+ Category: categories,
})
}
data, err := xml.MarshalIndent(&feed, "", " ")
lines := strings.Split(comments[len(comments)-1], "\n")
from := strings.TrimPrefix(lines[0], "From: ")
date := strings.TrimPrefix(lines[1], "Date: ")
+ htmlized := make([]string, 0, len(lines))
+ htmlized = append(htmlized, "<pre>")
+ for _, l := range lines[2:] {
+ htmlized = append(htmlized, lineURLize(cfg.AtomBaseURL+cfg.URLPrefix, l))
+ }
+ htmlized = append(htmlized, "</pre>")
idHasher.Reset()
idHasher.Write([]byte("COMMENT"))
idHasher.Write(commit.Hash[:])
Published: atom.TimeStr(date),
Updated: atom.TimeStr(date),
Content: &atom.Text{
- Type: "text",
- Body: strings.Join(lines[2:], "\n"),
+ Type: "html",
+ Body: strings.Join(htmlized, "\n"),
},
})
}
}, "")
commentsRaw := getNote(commentsTree, commit.Hash)
etagHash.Write(commentsRaw)
+ topicsRaw := getNote(topicsTree, commit.Hash)
+ etagHash.Write(topicsRaw)
if strings.HasSuffix(pathInfo, AtomCommentsFeed) {
etagHash.Write([]byte("ATOM COMMENTS"))
checkETag(etagHash)
idHasher.Write([]byte("COMMENT"))
idHasher.Write(commit.Hash[:])
idHasher.Write([]byte(comment.n))
+ htmlized := make([]string, 0, len(comment.body))
+ htmlized = append(htmlized, "<pre>")
+ for _, l := range comment.body {
+ htmlized = append(
+ htmlized,
+ lineURLize(cfg.AtomBaseURL+cfg.URLPrefix, l),
+ )
+ }
+ htmlized = append(htmlized, "</pre>")
feed.Entry = append(feed.Entry, &atom.Entry{
Title: fmt.Sprintf("Comment %s by %s", comment.n, comment.from),
Author: &atom.Person{Name: comment.from},
Published: atom.TimeStr(comment.date),
Updated: atom.TimeStr(comment.date),
Content: &atom.Text{
- Type: "text",
- Body: strings.Join(comment.body, "\n"),
+ Type: "html",
+ Body: strings.Join(htmlized, "\n"),
},
})
}
Version string
Cfg *Cfg
Title string
+ TitleEscaped string
When string
AtomCommentsURL string
Parent string
Lines []string
NoteLines []string
Comments []CommentEntry
+ Topics []string
}{
Version: sgblog.Version,
Cfg: cfg,
Title: title,
+ TitleEscaped: url.PathEscape(fmt.Sprintf("Re: %s (%s)", title, commit.Hash)),
When: when,
AtomCommentsURL: atomCommentsURL,
Parent: parent,
Lines: lines[2:],
NoteLines: notesLines,
Comments: comments,
+ Topics: parseTopics(topicsRaw),
})
if err != nil {
makeErr(err)