2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
3 Copyright (C) 2020-2023 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/>.
33 "github.com/go-git/go-git/v5"
34 "github.com/go-git/go-git/v5/plumbing"
35 "github.com/go-git/go-git/v5/plumbing/object"
36 "github.com/vorlif/spreak"
37 "go.stargrave.org/sgblog"
41 //go:embed gopher-menu.tmpl
42 TmplGopherMenuRaw string
43 //go:embed gopher-entry.tmpl
44 TmplGopherEntryRaw string
46 TmplGopherMenu = template.Must(template.New("gopher-menu").Parse(TmplGopherMenuRaw))
47 TmplGopherEntry = template.Must(template.New("gopher-entry").Parse(TmplGopherEntryRaw))
50 type TableMenuEntry struct {
59 func serveGopher(cfgPath string) {
60 cfg, err := readCfg(cfgPath)
64 if cfg.GopherDomain == "" {
65 log.Fatalln("GopherDomain is not configured")
67 initLocalizer(cfg.Lang)
69 headHash, err := initRepo(cfg)
74 scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
76 log.Fatalln(errors.New("no CRLF found"))
78 selector := scanner.Text()
82 selectorParts := strings.Split(selector, "/")
83 if strings.HasPrefix(selector, "URL:") {
84 selector = selector[len("URL:"):]
85 fmt.Printf(`<!DOCTYPE html>
88 <meta http-equiv="Refresh" content="1; url=%s" />
89 <title>Redirect to non-gopher URL</title>
92 Redirecting to <a href="%s">%s</a>...
95 `, selector, selector, selector)
96 } else if sha1DigestRe.MatchString(selector) {
97 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
101 err = TmplGopherEntry.Execute(os.Stdout, struct {
103 Commit *object.Commit
115 When: commit.Author.When.Format(sgblog.WhenFmt),
117 Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)),
118 Images: listImgs(cfg, commit.Hash),
119 Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
120 Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
121 Version: sgblog.Version,
122 TitleEscaped: url.PathEscape(fmt.Sprintf(
123 "Re: %s (%s)", msgSplit(commit.Message)[0], commit.Hash,
129 } else if len(selectorParts) > 1 &&
130 selectorParts[len(selectorParts)-2] == "offset" {
131 offset, err := strconv.Atoi(selectorParts[len(selectorParts)-1])
135 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
139 topicsCache, err := getTopicsCache(cfg, repoLog)
143 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
149 if len(selectorParts) == 3 {
150 topic = selectorParts[0]
152 var commits CommitIterNext
154 for i := 0; i < offset; i++ {
155 if _, err = repoLog.Next(); err != nil {
161 hashes := topicsCache[topic]
163 log.Fatalln(errors.New("no posts with that topic"))
165 if len(hashes) > offset {
166 hashes = hashes[offset:]
168 commits = &HashesIter{hashes}
172 entries := make([]TableMenuEntry, 0, PageEntries)
173 for i := 0; i < PageEntries; i++ {
174 commit, err := commits.Next()
179 lines := msgSplit(commit.Message)
180 entries = append(entries, TableMenuEntry{
183 LinesNum: len(lines) - 2,
184 ImagesNum: len(listImgs(cfg, commit.Hash)),
185 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
186 repo, commentsTree, commit.Hash,
188 Topics: sgblog.ParseTopics(sgblog.GetNote(
189 repo, topicsTree, commit.Hash,
193 offsetPrev := offset - PageEntries
197 err = TmplGopherMenu.Execute(os.Stdout, struct {
205 Entries []TableMenuEntry
213 OffsetPrev: offsetPrev,
214 OffsetNext: offset + PageEntries,
217 Topics: topicsCache.Topics(),
218 Version: sgblog.Version,
224 log.Fatalln(errors.New("unknown selector"))