]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/main.go
recfiles instead of netstrings
[sgblog.git] / cmd / sgblog / main.go
1 /*
2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
4
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.
8
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.
13
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/>.
16 */
17
18 // Git-backed CGI/inetd blogging/phlogging engine
19 package main
20
21 import (
22         "bytes"
23         "crypto/sha1"
24         "fmt"
25         "io/ioutil"
26         "os"
27         "regexp"
28         "sort"
29         "strings"
30         "text/scanner"
31
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 )
36
37 const (
38         PageEntries = 50
39 )
40
41 var (
42         sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
43         repo         *git.Repository
44         notesTree    *object.Tree
45         commentsRef  *plumbing.Reference
46         commentsTree *object.Tree
47         topicsRef    *plumbing.Reference
48         topicsTree   *object.Tree
49 )
50
51 type Cfg struct {
52         GitPath string
53         Branch  string
54         Title   string
55
56         URLPrefix string
57
58         AtomBaseURL string
59         AtomId      string
60         AtomAuthor  string
61
62         CSS       string
63         Webmaster string
64         AboutURL  string
65         GitURLs   []string
66
67         CommentsNotesRef string
68         CommentsEmail    string
69
70         TopicsNotesRef  string
71         TopicsCachePath string
72
73         GopherDomain string
74 }
75
76 func msgSplit(msg string) []string {
77         lines := strings.Split(msg, "\n")
78         lines = lines[:len(lines)-1]
79         if len(lines) < 3 {
80                 lines = []string{lines[0], "", ""}
81         }
82         return lines
83 }
84
85 func getNote(tree *object.Tree, what plumbing.Hash) []byte {
86         if tree == nil {
87                 return nil
88         }
89         var entry *object.TreeEntry
90         var err error
91         paths := make([]string, 3)
92         paths[0] = what.String()
93         paths[1] = paths[0][:2] + "/" + paths[0][2:]
94         paths[2] = paths[1][:4+1] + "/" + paths[1][4+1:]
95         for _, p := range paths {
96                 entry, err = tree.FindEntry(p)
97                 if err == nil {
98                         break
99                 }
100         }
101         if entry == nil {
102                 return nil
103         }
104         blob, err := repo.BlobObject(entry.Hash)
105         if err != nil {
106                 return nil
107         }
108         r, err := blob.Reader()
109         if err != nil {
110                 return nil
111         }
112         data, err := ioutil.ReadAll(r)
113         if err != nil {
114                 return nil
115         }
116         return bytes.TrimSuffix(data, []byte{'\n'})
117 }
118
119 func parseComments(data []byte) []string {
120         comments := []string{}
121         isBody := false
122         comment := make([]string, 0, 4)
123         lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
124         if len(lines) == 1 {
125                 return comments
126         }
127         for _, s := range lines {
128                 if s == "" {
129                         comments = append(comments, strings.Join(comment, "\n"))
130                         comment = make([]string, 0, 4)
131                         isBody = false
132                         continue
133                 }
134                 if s == "Body:" {
135                         isBody = true
136                         comment = append(comment, "")
137                         continue
138                 }
139                 if isBody {
140                         if s == "+" {
141                                 comment = append(comment, "")
142                         } else {
143                                 comment = append(comment, strings.TrimPrefix(s, "+ "))
144                         }
145                         continue
146                 }
147                 comment = append(comment, s)
148         }
149         if len(comment) > 1 {
150                 comments = append(comments, strings.Join(comment, "\n"))
151         }
152         return comments
153 }
154
155 func parseTopics(data []byte) []string {
156         var s scanner.Scanner
157         s.Init(bytes.NewBuffer(data))
158         topics := []string{}
159         for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
160                 topics = append(topics, s.TokenText())
161         }
162         sort.Strings(topics)
163         return topics
164 }
165
166 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
167         var err error
168         repo, err = git.PlainOpen(cfg.GitPath)
169         if err != nil {
170                 return nil, err
171         }
172         head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
173         if err != nil {
174                 return nil, err
175         }
176         headHash := head.Hash()
177         if notes, err := repo.Notes(); err == nil {
178                 var notesRef *plumbing.Reference
179                 notes.ForEach(func(ref *plumbing.Reference) error {
180                         switch string(ref.Name()) {
181                         case "refs/notes/commits":
182                                 notesRef = ref
183                         case cfg.CommentsNotesRef:
184                                 commentsRef = ref
185                         case cfg.TopicsNotesRef:
186                                 topicsRef = ref
187                         }
188                         return nil
189                 })
190                 if notesRef != nil {
191                         if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
192                                 notesTree, _ = commentsCommit.Tree()
193                         }
194                 }
195                 if commentsRef != nil {
196                         if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
197                                 commentsTree, _ = commentsCommit.Tree()
198                         }
199                 }
200                 if topicsRef != nil {
201                         if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
202                                 topicsTree, _ = topicsCommit.Tree()
203                         }
204                 }
205         }
206         return &headHash, nil
207 }
208
209 func main() {
210         if len(os.Args) == 3 && os.Args[1] == "-gopher" {
211                 serveGopher()
212         } else {
213                 serveHTTP()
214         }
215 }