2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020 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/hjson/hjson-go"
34 "go.stargrave.org/sgblog"
35 "gopkg.in/src-d/go-git.v4"
36 "gopkg.in/src-d/go-git.v4/plumbing"
37 "gopkg.in/src-d/go-git.v4/plumbing/object"
41 TmplGopherMenu = `{{$Cfg := .Cfg}}{{$CR := .CR}}i{{.Cfg.Title}} ({{.Offset}}-{{.OffsetNext}}) err {{.Cfg.GopherDomain}} 70{{.CR}}
42 {{if .Cfg.AboutURL}}hAbout URL:{{.Cfg.AboutURL}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end}}
43 {{if .Offset}}1Prev offset/{{.OffsetPrev}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end}}
44 {{if not .LogEnded}}1Next offset/{{.OffsetNext}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end -}}
45 {{- $yearPrev := 0}}{{$monthPrev := 0}}{{$dayPrev := 0 -}}
46 {{- range .Entries }}{{$yearCur := .Commit.Author.When.Year}}{{$monthCur := .Commit.Author.When.Month}}{{$dayCur := .Commit.Author.When.Day -}}
47 {{- if or (ne $dayCur $dayPrev) (ne $monthCur $monthPrev) (ne $yearCur $yearPrev)}}{{$dayPrev = $dayCur}}{{$monthPrev = $monthCur}}{{$yearPrev = $yearCur}}
48 i{{$yearCur | printf "%04d"}}-{{$monthCur | printf "%02d"}}-{{$dayCur | printf "%02d"}} err {{$Cfg.GopherDomain}} 70{{$CR}}{{end}}
49 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}}
50 iGenerated by: SGBlog {{.Version}} err {{.Cfg.GopherDomain}} 70{{.CR}}
53 TmplGopherEntry = `What: {{.Commit.Hash.String}}
55 ------------------------------------------------------------------------
58 ------------------------------------------------------------------------
61 {{- if .Cfg.CommentsEmail}}
62 ------------------------------------------------------------------------
63 leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}
64 {{end}}{{range $idx, $comment := .Comments}}
65 ------------------------------------------------------------------------
69 ------------------------------------------------------------------------
70 Generated by: SGBlog {{.Version}}
74 type TableMenuEntry struct {
83 cfgRaw, err := ioutil.ReadFile(cfgPath)
87 var cfgGeneral map[string]interface{}
88 if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
91 cfgRaw, err = json.Marshal(cfgGeneral)
96 if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
99 if cfg.GopherDomain == "" {
100 log.Fatalln("GopherDomain is not configured")
103 headHash, err := initRepo(cfg)
108 scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
110 log.Fatalln(errors.New("no CRLF found"))
112 selector := scanner.Text()
114 selector = "offset/0"
116 if strings.HasPrefix(selector, "offset/") {
117 offset, err := strconv.Atoi(selector[len("offset/"):])
121 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
125 for i := 0; i < offset; i++ {
126 if _, err = repoLog.Next(); err != nil {
131 entries := make([]TableMenuEntry, 0, PageEntries)
132 for i := 0; i < PageEntries; i++ {
133 commit, err := repoLog.Next()
138 lines := msgSplit(commit.Message)
139 entries = append(entries, TableMenuEntry{
142 LinesNum: len(lines) - 2,
143 CommentsNum: len(parseComments(getNote(commentsTree, commit.Hash))),
146 tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
147 err = tmpl.Execute(os.Stdout, struct {
153 Entries []TableMenuEntry
159 OffsetPrev: offset - PageEntries,
160 OffsetNext: offset + PageEntries,
163 Version: sgblog.Version,
169 } else if strings.HasPrefix(selector, "URL:") {
170 selector = selector[len("URL:"):]
173 <meta http-equiv="Refresh" content="1; url=%s" />
174 <title>Redirect to non-gopher URL</title>
177 Redirecting to <a href="%s">%s</a>...
180 `, selector, selector, selector)
181 } else if sha1DigestRe.MatchString(selector) {
182 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
186 tmpl := template.Must(template.New("entry").Parse(TmplGopherEntry))
187 err = tmpl.Execute(os.Stdout, struct {
188 Commit *object.Commit
196 When: commit.Author.When.Format(sgblog.WhenFmt),
198 Note: string(getNote(notesTree, commit.Hash)),
199 Comments: parseComments(getNote(commentsTree, commit.Hash)),
200 Version: sgblog.Version,
206 log.Fatalln(errors.New("unknown selector"))