]> Sergey Matveev's repositories - sgblog.git/blobdiff - cmd/sgblog/http.go
Topics support
[sgblog.git] / cmd / sgblog / http.go
index 312c02b715d4ec3f496d40f576db27517ec7588f..854ebe6e7895ed9ffce26a1f0b7c084fa1a40fe3 100644 (file)
@@ -37,13 +37,13 @@ import (
        "strings"
        "text/template"
 
+       "github.com/go-git/go-git/v5"
+       "github.com/go-git/go-git/v5/plumbing"
+       "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"
-       "gopkg.in/src-d/go-git.v4"
-       "gopkg.in/src-d/go-git.v4/plumbing"
-       "gopkg.in/src-d/go-git.v4/plumbing/object"
 )
 
 const (
@@ -53,23 +53,29 @@ 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>
@@ -77,11 +83,12 @@ const (
        <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>
@@ -91,6 +98,7 @@ const (
        <td>{{.LinesNum}}</td>
        <td>{{if .CommentsNum}}{{.CommentsNum}}{{else}}&nbsp;{{end}}</td>
        <td>{{if .DomainURLs}}{{range .DomainURLs}} {{.}} {{end}}{{else}}&nbsp;{{end}}</td>
+       {{if $TopicsEnabled}}<td>{{if .Topics}}{{range .Topics}} <a href="{{$Cfg.URLPrefix}}/?topic={{.}}">{{.}}</a> {{end}}{{else}}&nbsp;{{end}}</td>{{end}}
 </tr>
 {{end}}</table>
 {{template "links" .}}
@@ -117,6 +125,11 @@ const (
 [<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>
@@ -157,11 +170,13 @@ var (
 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 {
@@ -236,6 +251,10 @@ func bytes2uuid(b []byte) string {
        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 == "" {
@@ -349,12 +368,37 @@ func serveHTTP() {
                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)
@@ -363,8 +407,9 @@ func serveHTTP() {
                        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
@@ -372,9 +417,12 @@ func serveHTTP() {
                        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)
@@ -393,13 +441,21 @@ func serveHTTP() {
                                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
@@ -411,11 +467,14 @@ func serveHTTP() {
                }{
                        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,
@@ -428,12 +487,32 @@ func serveHTTP() {
                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",
@@ -441,16 +520,36 @@ func serveHTTP() {
                        }},
                        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})
+                       }
                        feed.Entry = append(feed.Entry, &atom.Entry{
                                Title: lines[0],
                                ID:    "urn:uuid:" + bytes2uuid(commit.Hash[:]),
@@ -465,6 +564,7 @@ func serveHTTP() {
                                        Type: "text",
                                        Body: strings.Join(lines[2:], "\n"),
                                },
+                               Category: categories,
                        })
                }
                data, err := xml.MarshalIndent(&feed, "", "  ")
@@ -577,6 +677,8 @@ func serveHTTP() {
                }, "")
                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)
@@ -690,6 +792,7 @@ func serveHTTP() {
                        Lines           []string
                        NoteLines       []string
                        Comments        []CommentEntry
+                       Topics          []string
                }{
                        Version:         sgblog.Version,
                        Cfg:             cfg,
@@ -701,6 +804,7 @@ func serveHTTP() {
                        Lines:           lines[2:],
                        NoteLines:       notesLines,
                        Comments:        comments,
+                       Topics:          parseTopics(topicsRaw),
                })
                if err != nil {
                        makeErr(err)