]> Sergey Matveev's repositories - sgblog.git/blobdiff - cmd/sgblog/gopher.go
Raise copyright years
[sgblog.git] / cmd / sgblog / gopher.go
index 2fbbeda1479b110463b9cdac4d4b954eb95ccc85..ce820d153cc856ed938ec711b8b07cbc1039037f 100644 (file)
@@ -1,6 +1,6 @@
 /*
-SGBlog -- Git-based CGI/inetd blogging/phlogging engine
-Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
+SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
@@ -19,7 +19,6 @@ package main
 
 import (
        "bufio"
-       "bytes"
        "encoding/json"
        "errors"
        "fmt"
@@ -29,17 +28,62 @@ import (
        "os"
        "strconv"
        "strings"
-       "time"
+       "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"
-       "gopkg.in/src-d/go-git.v4"
-       "gopkg.in/src-d/go-git.v4/plumbing"
 )
 
-const CRLF = "\r\n"
+const (
+       TmplGopherMenu = `{{$CR := printf "\r"}}{{$CRLF := printf "\r\n" -}}
+{{- define "domainPort" }}     {{.GopherDomain}}       70{{end}}{{$Cfg := .Cfg -}}
+i{{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}})  err{{template "domainPort" .Cfg}}{{$CRLF -}}
+{{- if .Cfg.AboutURL}}hAbout   URL:{{.Cfg.AboutURL}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
+{{- if .Offset}}1Prev  {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetPrev}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
+{{- if not .LogEnded}}1Next    {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetNext}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
+{{- $datePrev := "0001-01-01" -}}
+{{- range .Entries -}}
+{{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}}
+{{- if ne $dateCur $datePrev}}{{$datePrev = $dateCur}}
+i{{$dateCur}}  err{{template "domainPort" $Cfg}}{{$CR}}{{end}}
+0[{{.Commit.Author.When.Format "15:04"}}] {{.Title}} ({{.LinesNum}}L){{with .CommentsNum}} ({{.}}C){{end}}{{if .Topics}}{{range .Topics}} {{.}}{{end}}{{end}}  /{{.Commit.Hash.String}}{{template "domainPort" $Cfg}}{{$CR}}{{end}}
+{{range .Topics}}
+1Topic: {{.}}  {{.}}/offset/0{{template "domainPort" $Cfg}}{{$CR}}{{end}}
+iGenerated by: SGBlog {{.Version}}     err{{template "domainPort" .Cfg}}{{$CR}}
+.{{$CRLF}}`
+       TmplGopherEntry = `What: {{.Commit.Hash.String}}
+When: {{.When}}
+------------------------------------------------------------------------
+{{if .Topics}}Topics:{{range .Topics}} {{.}}{{end}}{{end}}
+------------------------------------------------------------------------
+{{.Commit.Message -}}
+{{- if .Note}}
+------------------------------------------------------------------------
+Note:
+{{.Note}}{{end -}}
+{{- if .Cfg.CommentsEmail}}
+------------------------------------------------------------------------
+leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}
+{{end}}{{range $idx, $comment := .Comments}}
+------------------------------------------------------------------------
+comment {{$idx}}:
+{{$comment}}
+{{end}}
+------------------------------------------------------------------------
+Generated by: SGBlog {{.Version}}
+`
+)
 
-var DashLine = strings.Repeat("-", 72)
+type TableMenuEntry struct {
+       Commit      *object.Commit
+       Title       string
+       LinesNum    int
+       CommentsNum int
+       Topics      []string
+}
 
 func serveGopher() {
        cfgPath := os.Args[2]
@@ -76,8 +120,47 @@ func serveGopher() {
        if selector == "" {
                selector = "offset/0"
        }
-       if strings.HasPrefix(selector, "offset/") {
-               offset, err := strconv.Atoi(selector[len("offset/"):])
+       selectorParts := strings.Split(selector, "/")
+       if strings.HasPrefix(selector, "URL:") {
+               selector = selector[len("URL:"):]
+               fmt.Printf(`<html>
+<head>
+       <meta http-equiv="Refresh" content="1; url=%s" />
+       <title>Redirect to non-gopher URL</title>
+</head>
+<body>
+Redirecting to <a href="%s">%s</a>...
+</body>
+</html>
+`, selector, selector, selector)
+       } else if sha1DigestRe.MatchString(selector) {
+               commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               tmpl := template.Must(template.New("entry").Parse(TmplGopherEntry))
+               err = tmpl.Execute(os.Stdout, struct {
+                       Commit   *object.Commit
+                       When     string
+                       Cfg      *Cfg
+                       Note     string
+                       Comments []string
+                       Topics   []string
+                       Version  string
+               }{
+                       Commit:   commit,
+                       When:     commit.Author.When.Format(sgblog.WhenFmt),
+                       Cfg:      cfg,
+                       Note:     string(getNote(notesTree, commit.Hash)),
+                       Comments: parseComments(getNote(commentsTree, commit.Hash)),
+                       Topics:   parseTopics(getNote(topicsTree, commit.Hash)),
+                       Version:  sgblog.Version,
+               })
+               if err != nil {
+                       log.Fatalln(err)
+               }
+       } else if selectorParts[len(selectorParts)-2] == "offset" {
+               offset, err := strconv.Atoi(selectorParts[len(selectorParts)-1])
                if err != nil {
                        log.Fatalln(err)
                }
@@ -85,123 +168,84 @@ func serveGopher() {
                if err != nil {
                        log.Fatalln(err)
                }
-               commitN := 0
-               for i := 0; i < offset; i++ {
-                       if _, err = repoLog.Next(); err != nil {
-                               break
+               topicsCache, err := getTopicsCache(cfg, repoLog)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
+               if err != nil {
+                       log.Fatalln(err)
+               }
+
+               var topic string
+               if len(selectorParts) == 3 {
+                       topic = selectorParts[0]
+               }
+               var commits CommitIterNext
+               if topic == "" {
+                       for i := 0; i < offset; i++ {
+                               if _, err = repoLog.Next(); err != nil {
+                                       break
+                               }
+                       }
+                       commits = repoLog
+               } else {
+                       hashes := topicsCache[topic]
+                       if hashes == nil {
+                               log.Fatalln(errors.New("no posts with that topic"))
                        }
-                       commitN++
+                       if len(hashes) > offset {
+                               hashes = hashes[offset:]
+                       }
+                       commits = &HashesIter{hashes}
                }
 
                logEnded := false
-               var menu bytes.Buffer
-               var yearPrev int
-               var monthPrev time.Month
-               var dayPrev int
+               entries := make([]TableMenuEntry, 0, PageEntries)
                for i := 0; i < PageEntries; i++ {
-                       commit, err := repoLog.Next()
+                       commit, err := commits.Next()
                        if err != nil {
                                logEnded = true
                                break
                        }
-                       yearCur, monthCur, dayCur := commit.Author.When.Date()
-                       if dayCur != dayPrev || monthCur != monthPrev || yearCur != yearPrev {
-                               menu.WriteString(fmt.Sprintf(
-                                       "i%04d-%02d-%02d\tnil\t%s\t%d%s",
-                                       yearCur, monthCur, dayCur,
-                                       cfg.GopherDomain, 70, CRLF,
-                               ))
-                               yearPrev, monthPrev, dayPrev = yearCur, monthCur, dayCur
-                       }
-                       commitN++
                        lines := msgSplit(commit.Message)
-                       var commentsValue string
-                       if l := len(parseComments(getNote(commentsTree, commit.Hash))); l > 0 {
-                               commentsValue = fmt.Sprintf(" (%dC)", l)
-                       }
-                       menu.WriteString(fmt.Sprintf(
-                               "0[%02d:%02d] %s (%dL)%s\t/%s\t%s\t%d%s",
-                               commit.Author.When.Hour(),
-                               commit.Author.When.Minute(),
-                               lines[0],
-                               len(lines)-2,
-                               commentsValue,
-                               commit.Hash.String(),
-                               cfg.GopherDomain, 70, CRLF,
-                       ))
+                       entries = append(entries, TableMenuEntry{
+                               Commit:      commit,
+                               Title:       lines[0],
+                               LinesNum:    len(lines) - 2,
+                               CommentsNum: len(parseComments(getNote(commentsTree, commit.Hash))),
+                               Topics:      parseTopics(getNote(topicsTree, commit.Hash)),
+                       })
                }
-
-               fmt.Printf(
-                       "i%s (%d-%d)\tnil\t%s\t%d%s",
-                       cfg.Title,
-                       offset,
-                       offset+PageEntries,
-                       cfg.GopherDomain, 70, CRLF,
-               )
-               if cfg.AboutURL != "" {
-                       fmt.Printf(
-                               "hAbout\tURL:%s\t%s\t%d%s",
-                               cfg.AboutURL,
-                               cfg.GopherDomain, 70, CRLF,
-                       )
-               }
-               if offset > 0 {
-                       offsetPrev := offset - PageEntries
-                       if offsetPrev < 0 {
-                               offsetPrev = 0
-                       }
-                       fmt.Printf(
-                               "1Prev\toffset/%d\t%s\t%d%s",
-                               offsetPrev,
-                               cfg.GopherDomain, 70, CRLF,
-                       )
+               tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
+               offsetPrev := offset - PageEntries
+               if offsetPrev < 0 {
+                       offsetPrev = 0
                }
-               if !logEnded {
-                       fmt.Printf(
-                               "1Next\toffset/%d\t%s\t%d%s",
-                               offset+PageEntries,
-                               cfg.GopherDomain, 70, CRLF,
-                       )
-               }
-               fmt.Print(menu.String())
-               fmt.Printf(
-                       "iGenerated by: SGBlog %s\terr\t%s\t%d%s",
-                       sgblog.Version,
-                       cfg.GopherDomain, 70, CRLF,
-               )
-               fmt.Print("." + CRLF)
-       } else if strings.HasPrefix(selector, "URL:") {
-               selector = selector[len("URL:"):]
-               fmt.Printf(`<html>
-<head>
-       <meta http-equiv="Refresh" content="1; url=%s" />
-       <title>Redirect to non-gopher URL</title>
-</head>
-<body>
-Redirecting to <a href="%s">%s</a>...
-</body>
-</html>
-`, selector, selector, selector)
-       } else if sha1DigestRe.MatchString(selector) {
-               commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
+               err = tmpl.Execute(os.Stdout, struct {
+                       Cfg        *Cfg
+                       Topic      string
+                       Offset     int
+                       OffsetPrev int
+                       OffsetNext int
+                       LogEnded   bool
+                       Entries    []TableMenuEntry
+                       Topics     []string
+                       Version    string
+               }{
+                       Cfg:        cfg,
+                       Topic:      topic,
+                       Offset:     offset,
+                       OffsetPrev: offsetPrev,
+                       OffsetNext: offset + PageEntries,
+                       LogEnded:   logEnded,
+                       Entries:    entries,
+                       Topics:     topicsCache.Topics(),
+                       Version:    sgblog.Version,
+               })
                if err != nil {
                        log.Fatalln(err)
                }
-               fmt.Printf(
-                       "What: %s\nWhen: %s\n%s\n%s",
-                       commit.Hash.String(),
-                       commit.Author.When.Format(sgblog.WhenFmt),
-                       DashLine,
-                       commit.Message,
-               )
-               notesRaw := getNote(notesTree, commit.Hash)
-               if len(notesRaw) > 0 {
-                       fmt.Printf("%s\nNote:\n%s\n", DashLine, string(notesRaw))
-               }
-               for i, comment := range parseComments(getNote(commentsTree, commit.Hash)) {
-                       fmt.Printf("%s\ncomment %d:\n%s\n", DashLine, i, comment)
-               }
-               fmt.Printf("%s\nGenerated by: SGBlog %s\n", DashLine, sgblog.Version)
        } else {
                log.Fatalln(errors.New("unknown selector"))
        }