2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging 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/>.
31 "github.com/go-git/go-git/v5"
32 "github.com/go-git/go-git/v5/plumbing"
33 "github.com/go-git/go-git/v5/plumbing/object"
34 "go.stargrave.org/sgblog"
38 TmplGemMenu = `{{$CR := printf "\r"}}20 text/gemini{{$CR}}
39 # {{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}})
40 {{if .Cfg.AboutURL}}=> {{.Cfg.AboutURL}} About{{end}}
41 {{if .Offset}}=> /?offset={{.OffsetPrev}}{{if .Topic}}&topic={{.Topic}}{{end}} Prev{{end}}
42 {{if not .LogEnded}}=> /?offset={{.OffsetNext}}{{if .Topic}}&topic={{.Topic}}{{end}} Next{{end}}
44 {{$datePrev := "0001-01-01" -}}
45 {{- range .Entries -}}
46 {{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}}
47 {{- if ne $dateCur $datePrev}}{{$datePrev = $dateCur}}## {{$dateCur}}
49 => /{{.Commit.Hash.String}} [{{.Commit.Author.When.Format "15:04"}}] {{.Title}} ({{.LinesNum}}L){{with .CommentsNum}} ({{.}}C){{end}}{{if .Topics}}{{range .Topics}} {{.}}{{end}}{{end}}
52 {{range .Topics -}}=> /?topic={{.}} Topic: {{.}}
54 Generated by: SGBlog {{.Version}}
56 TmplGemEntry = `{{$CR := printf "\r"}}20 text/gemini{{$CR}}
58 What: {{.Commit.Hash.String}}
60 {{if .Topics}}Topics:{{range .Topics}} {{.}}{{end}}{{end}}
62 {{.Commit.Message}}` + "```" + `
69 {{- if .Cfg.CommentsEmail}}
70 => mailto:{{.Cfg.CommentsEmail}}?subject={{.TitleEscaped}} leave comment
71 {{end}}{{range $idx, $comment := .Comments}}
77 Generated by: SGBlog {{.Version}}
81 func makeGemErr(err error) {
82 fmt.Print("59 " + err.Error() + "\r\n")
86 func serveGemini(cfgPath string) {
87 cfg, err := readCfg(cfgPath)
92 headHash, err := initRepo(cfg)
97 scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10))
99 makeGemErr(errors.New("no CRLF found"))
101 urlRaw := scanner.Text()
102 u, err := url.Parse(urlRaw)
106 if u.Scheme != "gemini" {
107 makeGemErr(errors.New("only gemini:// is supported" + u.String()))
112 if offsetRaw, exists := u.Query()["offset"]; exists {
113 offset, err = strconv.Atoi(offsetRaw[0])
118 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
122 topicsCache, err := getTopicsCache(cfg, repoLog)
126 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
132 var commits CommitIterNext
134 if t, exists := u.Query()["topic"]; exists {
136 hashes := topicsCache[topic]
138 makeGemErr(errors.New("no posts with that topic"))
140 if len(hashes) > offset {
141 hashes = hashes[offset:]
144 commits = &HashesIter{hashes}
146 for i := 0; i < offset; i++ {
147 if _, err = repoLog.Next(); err != nil {
156 entries := make([]TableMenuEntry, 0, PageEntries)
157 for i := 0; i < PageEntries; i++ {
158 commit, err := commits.Next()
163 lines := msgSplit(commit.Message)
164 entries = append(entries, TableMenuEntry{
167 LinesNum: len(lines) - 2,
168 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
169 repo, commentsTree, commit.Hash,
171 Topics: sgblog.ParseTopics(sgblog.GetNote(
172 repo, topicsTree, commit.Hash,
176 tmpl := template.Must(template.New("menu").Parse(TmplGemMenu))
177 offsetPrev := offset - PageEntries
181 err = tmpl.Execute(os.Stdout, struct {
188 Entries []TableMenuEntry
195 OffsetPrev: offsetPrev,
196 OffsetNext: offset + PageEntries,
199 Topics: topicsCache.Topics(),
200 Version: sgblog.Version,
205 } else if sha1DigestRe.MatchString(u.Path[1:]) {
206 commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:]))
210 tmpl := template.Must(template.New("entry").Parse(TmplGemEntry))
211 title := msgSplit(commit.Message)[0]
212 err = tmpl.Execute(os.Stdout, struct {
214 Commit *object.Commit
225 When: commit.Author.When.Format(sgblog.WhenFmt),
227 Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)),
228 Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
229 Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
230 Version: sgblog.Version,
231 TitleEscaped: url.PathEscape(fmt.Sprintf(
232 "Re: %s (%s)", title, commit.Hash,
239 makeGemErr(errors.New("unknown URL action"))