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/>.
18 // Git-backed CGI/inetd blogging/phlogging engine
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/hjson/hjson-go"
37 "go.cypherpunks.ru/recfile"
45 sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
47 notesTree *object.Tree
48 commentsRef *plumbing.Reference
49 commentsTree *object.Tree
50 topicsRef *plumbing.Reference
51 topicsTree *object.Tree
70 CommentsNotesRef string
74 TopicsCachePath string
79 func msgSplit(msg string) []string {
80 lines := strings.Split(msg, "\n")
81 lines = lines[:len(lines)-1]
83 lines = []string{lines[0], "", ""}
88 func getNote(tree *object.Tree, what plumbing.Hash) []byte {
92 var entry *object.TreeEntry
94 paths := make([]string, 3)
95 paths[0] = what.String()
96 paths[1] = paths[0][:2] + "/" + paths[0][2:]
97 paths[2] = paths[1][:4+1] + "/" + paths[1][4+1:]
98 for _, p := range paths {
99 entry, err = tree.FindEntry(p)
107 blob, err := repo.BlobObject(entry.Hash)
111 r, err := blob.Reader()
115 data, err := ioutil.ReadAll(r)
119 return bytes.TrimSuffix(data, []byte{'\n'})
122 func parseComments(data []byte) []string {
123 comments := []string{}
124 r := recfile.NewReader(bytes.NewReader(data))
126 fields, err := r.Next()
130 if len(fields) != 3 ||
131 fields[0].Name != "From" ||
132 fields[1].Name != "Date" ||
133 fields[2].Name != "Body" {
136 comments = append(comments, fmt.Sprintf(
137 "%s: %s\n%s: %s\n%s",
138 fields[0].Name, fields[0].Value,
139 fields[1].Name, fields[1].Value,
146 func parseTopics(data []byte) []string {
147 var s scanner.Scanner
148 s.Init(bytes.NewBuffer(data))
150 for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
151 topics = append(topics, s.TokenText())
157 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
159 repo, err = git.PlainOpen(cfg.GitPath)
163 head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
167 headHash := head.Hash()
168 if notes, err := repo.Notes(); err == nil {
169 var notesRef *plumbing.Reference
170 notes.ForEach(func(ref *plumbing.Reference) error {
171 switch string(ref.Name()) {
172 case "refs/notes/commits":
174 case cfg.CommentsNotesRef:
176 case cfg.TopicsNotesRef:
182 if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
183 notesTree, _ = commentsCommit.Tree()
186 if commentsRef != nil {
187 if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
188 commentsTree, _ = commentsCommit.Tree()
191 if topicsRef != nil {
192 if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
193 topicsTree, _ = topicsCommit.Tree()
197 return &headHash, nil
200 func readCfg(cfgPath string) (*Cfg, error) {
201 cfgRaw, err := ioutil.ReadFile(cfgPath)
205 var cfgGeneral map[string]interface{}
206 if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
209 cfgRaw, err = json.Marshal(cfgGeneral)
214 if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
221 gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file")
222 flag.Usage = func() {
223 fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog:
224 sgblog -- run CGI HTTP backend
225 sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend
229 if *gopherCfgPath != "" {
230 serveGopher(*gopherCfgPath)