/*
SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
-Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
+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
import (
"bufio"
- "encoding/json"
"errors"
"fmt"
"io"
- "io/ioutil"
"log"
+ "net/url"
"os"
"strconv"
"strings"
"text/template"
- "github.com/hjson/hjson-go"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
"go.stargrave.org/sgblog"
- "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 (
- TmplGopherMenu = `{{$Cfg := .Cfg}}{{$CR := .CR}}i{{.Cfg.Title}} ({{.Offset}}-{{.OffsetNext}}) err {{.Cfg.GopherDomain}} 70{{.CR}}
-{{if .Cfg.AboutURL}}hAbout URL:{{.Cfg.AboutURL}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end}}
-{{if .Offset}}1Prev offset/{{.OffsetPrev}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end}}
-{{if not .LogEnded}}1Next offset/{{.OffsetNext}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end -}}
-{{- $yearPrev := 0}}{{$monthPrev := 0}}{{$dayPrev := 0 -}}
-{{- range .Entries }}{{$yearCur := .Commit.Author.When.Year}}{{$monthCur := .Commit.Author.When.Month}}{{$dayCur := .Commit.Author.When.Day -}}
-{{- if or (ne $dayCur $dayPrev) (ne $monthCur $monthPrev) (ne $yearCur $yearPrev)}}{{$dayPrev = $dayCur}}{{$monthPrev = $monthCur}}{{$yearPrev = $yearCur}}
-i{{$yearCur | printf "%04d"}}-{{$monthCur | printf "%02d"}}-{{$dayCur | printf "%02d"}} err {{$Cfg.GopherDomain}} 70{{$CR}}{{end}}
-0[{{.Commit.Author.When.Hour | printf "%02d"}}:{{.Commit.Author.When.Minute | printf "%02d"}}] {{.Title}} ({{.LinesNum}}L){{if .CommentsNum}} ({{.CommentsNum}}C){{end}} /{{.Commit.Hash.String}} {{$Cfg.GopherDomain}} 70{{$CR}}{{end}}
-iGenerated by: SGBlog {{.Version}} err {{.Cfg.GopherDomain}} 70{{.CR}}
-.{{.CR}}
-`
+ 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}}{{end -}}
{{- if .Cfg.CommentsEmail}}
------------------------------------------------------------------------
-leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}
+leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.TitleEscaped}}
{{end}}{{range $idx, $comment := .Comments}}
------------------------------------------------------------------------
comment {{$idx}}:
Title string
LinesNum int
CommentsNum int
+ Topics []string
}
-func serveGopher() {
- cfgPath := os.Args[2]
- cfgRaw, err := ioutil.ReadFile(cfgPath)
- if err != nil {
- log.Fatalln(err)
- }
- var cfgGeneral map[string]interface{}
- if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
- log.Fatalln(err)
- }
- cfgRaw, err = json.Marshal(cfgGeneral)
+func serveGopher(cfgPath string) {
+ cfg, err := readCfg(cfgPath)
if err != nil {
log.Fatalln(err)
}
- var cfg *Cfg
- if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
- log.Fatalln(err)
- }
if cfg.GopherDomain == "" {
log.Fatalln("GopherDomain is not configured")
}
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
+ TitleEscaped 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,
+ TitleEscaped: url.PathEscape(fmt.Sprintf(
+ "Re: %s (%s)", msgSplit(commit.Message)[0], commit.Hash,
+ )),
+ })
+ 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)
}
if err != nil {
log.Fatalln(err)
}
- 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"))
}
+ if len(hashes) > offset {
+ hashes = hashes[offset:]
+ }
+ commits = &HashesIter{hashes}
}
+
logEnded := false
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
Title: lines[0],
LinesNum: len(lines) - 2,
CommentsNum: len(parseComments(getNote(commentsTree, commit.Hash))),
+ Topics: parseTopics(getNote(topicsTree, commit.Hash)),
})
}
tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
+ offsetPrev := offset - PageEntries
+ if offsetPrev < 0 {
+ offsetPrev = 0
+ }
err = tmpl.Execute(os.Stdout, struct {
Cfg *Cfg
+ Topic string
Offset int
OffsetPrev int
OffsetNext int
LogEnded bool
Entries []TableMenuEntry
+ Topics []string
Version string
- CR string
}{
Cfg: cfg,
+ Topic: topic,
Offset: offset,
- OffsetPrev: offset - PageEntries,
+ OffsetPrev: offsetPrev,
OffsetNext: offset + PageEntries,
LogEnded: logEnded,
Entries: entries,
+ Topics: topicsCache.Topics(),
Version: sgblog.Version,
- CR: "\r",
- })
- if err != nil {
- log.Fatalln(err)
- }
- } 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:]))
- 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
- 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)),
- Version: sgblog.Version,
})
if err != nil {
log.Fatalln(err)