2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Affero General Public License as
7 published by the Free Software Foundation, version 3 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU Affero General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
32 "github.com/go-git/go-git/v5"
33 "github.com/go-git/go-git/v5/plumbing"
34 "github.com/go-git/go-git/v5/plumbing/object"
35 "go.stargrave.org/sgblog"
39 TmplGopherMenu = `{{$CR := printf "\r"}}{{$CRLF := printf "\r\n" -}}
40 {{- define "domainPort" }} {{.GopherDomain}} 70{{end}}{{$Cfg := .Cfg -}}
41 i{{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}}) err{{template "domainPort" .Cfg}}{{$CRLF -}}
42 {{- if .Cfg.AboutURL}}hAbout URL:{{.Cfg.AboutURL}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
43 {{- if .Offset}}1Prev {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetPrev}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
44 {{- if not .LogEnded}}1Next {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetNext}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
45 {{- $datePrev := "0001-01-01" -}}
46 {{- range .Entries -}}
47 {{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}}
48 {{- if ne $dateCur $datePrev}}{{$datePrev = $dateCur}}
49 i{{$dateCur}} err{{template "domainPort" $Cfg}}{{$CR}}{{end}}
50 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}}
52 1Topic: {{.}} {{.}}/offset/0{{template "domainPort" $Cfg}}{{$CR}}{{end}}
53 iGenerated by: SGBlog {{.Version}} err{{template "domainPort" .Cfg}}{{$CR}}
55 TmplGopherEntry = `What: {{.Commit.Hash.String}}
57 ------------------------------------------------------------------------
58 {{if .Topics}}Topics:{{range .Topics}} {{.}}{{end}}{{end}}
59 ------------------------------------------------------------------------
62 ------------------------------------------------------------------------
65 {{- if .Cfg.CommentsEmail}}
66 ------------------------------------------------------------------------
67 leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.TitleEscaped}}
68 {{end}}{{range $idx, $comment := .Comments}}
69 ------------------------------------------------------------------------
73 ------------------------------------------------------------------------
74 Generated by: SGBlog {{.Version}}
78 type TableMenuEntry struct {
86 func serveGopher(cfgPath string) {
87 cfg, err := readCfg(cfgPath)
91 if cfg.GopherDomain == "" {
92 log.Fatalln("GopherDomain is not configured")
95 headHash, err := initRepo(cfg)
100 scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
102 log.Fatalln(errors.New("no CRLF found"))
104 selector := scanner.Text()
106 selector = "offset/0"
108 selectorParts := strings.Split(selector, "/")
109 if strings.HasPrefix(selector, "URL:") {
110 selector = selector[len("URL:"):]
113 <meta http-equiv="Refresh" content="1; url=%s" />
114 <title>Redirect to non-gopher URL</title>
117 Redirecting to <a href="%s">%s</a>...
120 `, selector, selector, selector)
121 } else if sha1DigestRe.MatchString(selector) {
122 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
126 tmpl := template.Must(template.New("entry").Parse(TmplGopherEntry))
127 err = tmpl.Execute(os.Stdout, struct {
128 Commit *object.Commit
138 When: commit.Author.When.Format(sgblog.WhenFmt),
140 Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)),
141 Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
142 Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
143 Version: sgblog.Version,
144 TitleEscaped: url.PathEscape(fmt.Sprintf(
145 "Re: %s (%s)", msgSplit(commit.Message)[0], commit.Hash,
151 } else if len(selectorParts) > 1 &&
152 selectorParts[len(selectorParts)-2] == "offset" {
153 offset, err := strconv.Atoi(selectorParts[len(selectorParts)-1])
157 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
161 topicsCache, err := getTopicsCache(cfg, repoLog)
165 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
171 if len(selectorParts) == 3 {
172 topic = selectorParts[0]
174 var commits CommitIterNext
176 for i := 0; i < offset; i++ {
177 if _, err = repoLog.Next(); err != nil {
183 hashes := topicsCache[topic]
185 log.Fatalln(errors.New("no posts with that topic"))
187 if len(hashes) > offset {
188 hashes = hashes[offset:]
190 commits = &HashesIter{hashes}
194 entries := make([]TableMenuEntry, 0, PageEntries)
195 for i := 0; i < PageEntries; i++ {
196 commit, err := commits.Next()
201 lines := msgSplit(commit.Message)
202 entries = append(entries, TableMenuEntry{
205 LinesNum: len(lines) - 2,
206 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash))),
207 Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
210 tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
211 offsetPrev := offset - PageEntries
215 err = tmpl.Execute(os.Stdout, struct {
222 Entries []TableMenuEntry
229 OffsetPrev: offsetPrev,
230 OffsetNext: offset + PageEntries,
233 Topics: topicsCache.Topics(),
234 Version: sgblog.Version,
240 log.Fatalln(errors.New("unknown selector"))