1 // SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Affero General Public License as
6 // published by the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU Affero General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
16 // Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
31 "github.com/go-git/go-git/v5"
32 "github.com/go-git/go-git/v5/plumbing"
33 "github.com/go-git/go-git/v5/plumbing/object"
34 "github.com/hjson/hjson-go/v4"
35 "github.com/vorlif/spreak"
36 "golang.org/x/text/language"
44 sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
46 notesTree *object.Tree
47 commentsRef *plumbing.Reference
48 commentsTree *object.Tree
49 topicsRef *plumbing.Reference
50 topicsTree *object.Tree
52 localizer *spreak.Localizer
75 CommentsNotesRef string
79 TopicsCachePath string
87 func msgSplit(msg string) []string {
88 lines := strings.Split(msg, "\n")
89 lines = lines[:len(lines)-1]
91 lines = []string{lines[0], "", ""}
96 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
98 repo, err = git.PlainOpen(cfg.GitPath)
102 head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
106 headHash := head.Hash()
107 if notes, err := repo.Notes(); err == nil {
108 var notesRef *plumbing.Reference
109 notes.ForEach(func(ref *plumbing.Reference) error {
110 switch string(ref.Name()) {
111 case "refs/notes/commits":
113 case cfg.CommentsNotesRef:
115 case cfg.TopicsNotesRef:
121 if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
122 notesTree, _ = commentsCommit.Tree()
125 if commentsRef != nil {
126 if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
127 commentsTree, _ = commentsCommit.Tree()
130 if topicsRef != nil {
131 if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
132 topicsTree, _ = topicsCommit.Tree()
136 return &headHash, nil
139 func readCfg(cfgPath string) (*Cfg, error) {
140 cfgRaw, err := os.ReadFile(cfgPath)
144 var cfgGeneral map[string]interface{}
145 if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
148 cfgRaw, err = json.Marshal(cfgGeneral)
153 if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
159 func initLocalizer(lang string) {
160 fsys, _ := fs.Sub(locales, "locale")
161 bundle, err := spreak.NewBundle(
162 spreak.WithSourceLanguage(language.English),
163 spreak.WithDomainFs(spreak.NoDomain, fsys),
164 spreak.WithLanguage(language.Russian),
170 lang = language.English.String()
172 localizer = spreak.NewLocalizer(bundle, language.MustParse(lang))
176 gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file")
177 geminiCfgPath := flag.String("gemini", "", "Path to gemini-related configuration file")
178 flag.Usage = func() {
179 fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog:
180 sgblog -- run CGI HTTP backend
181 sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend
182 sgblog -gemini /path/to/cfg.hjson -- run UCSPI+tlss Gemini backend
186 log.SetFlags(log.Lshortfile)
187 if *gopherCfgPath != "" {
188 serveGopher(*gopherCfgPath)
189 } else if *geminiCfgPath != "" {
190 serveGemini(*geminiCfgPath)