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/>.
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 //go:embed gemini-menu.tmpl
41 //go:embed gemini-entry.tmpl
42 TmplGemEntryRaw string
44 TmplGemMenu = template.Must(template.New("menu").Parse(TmplGemMenuRaw))
45 TmplGemEntry = template.Must(template.New("entry").Parse(TmplGemEntryRaw))
48 func makeGemErr(err error) {
49 fmt.Print("59 " + err.Error() + "\r\n")
53 func serveGemini(cfgPath string) {
54 cfg, err := readCfg(cfgPath)
59 headHash, err := initRepo(cfg)
64 scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10))
66 makeGemErr(errors.New("no CRLF found"))
68 urlRaw := scanner.Text()
69 u, err := url.Parse(urlRaw)
73 if u.Scheme != "gemini" {
74 makeGemErr(errors.New("only gemini:// is supported" + u.String()))
79 if offsetRaw, exists := u.Query()["offset"]; exists {
80 offset, err = strconv.Atoi(offsetRaw[0])
85 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
89 topicsCache, err := getTopicsCache(cfg, repoLog)
93 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
99 var commits CommitIterNext
101 if t, exists := u.Query()["topic"]; exists {
103 hashes := topicsCache[topic]
105 makeGemErr(errors.New("no posts with that topic"))
107 if len(hashes) > offset {
108 hashes = hashes[offset:]
111 commits = &HashesIter{hashes}
113 for i := 0; i < offset; i++ {
114 if _, err = repoLog.Next(); err != nil {
123 entries := make([]TableMenuEntry, 0, PageEntries)
124 for i := 0; i < PageEntries; i++ {
125 commit, err := commits.Next()
130 lines := msgSplit(commit.Message)
131 entries = append(entries, TableMenuEntry{
134 LinesNum: len(lines) - 2,
135 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
136 repo, commentsTree, commit.Hash,
138 Topics: sgblog.ParseTopics(sgblog.GetNote(
139 repo, topicsTree, commit.Hash,
143 offsetPrev := offset - PageEntries
147 err = TmplGemMenu.Execute(os.Stdout, struct {
154 Entries []TableMenuEntry
161 OffsetPrev: offsetPrev,
162 OffsetNext: offset + PageEntries,
165 Topics: topicsCache.Topics(),
166 Version: sgblog.Version,
171 } else if sha1DigestRe.MatchString(u.Path[1:]) {
172 commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:]))
176 title := msgSplit(commit.Message)[0]
177 err = TmplGemEntry.Execute(os.Stdout, struct {
179 Commit *object.Commit
190 When: commit.Author.When.Format(sgblog.WhenFmt),
192 Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)),
193 Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
194 Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
195 Version: sgblog.Version,
196 TitleEscaped: url.PathEscape(fmt.Sprintf(
197 "Re: %s (%s)", title, commit.Hash,
204 makeGemErr(errors.New("unknown URL action"))