// SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine // Copyright (C) 2020-2024 Sergey Matveev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, version 3 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine package main import ( "crypto/sha1" "embed" "encoding/json" "flag" "fmt" "io/fs" "log" "os" "regexp" "strings" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/hjson/hjson-go/v4" "github.com/vorlif/spreak" "golang.org/x/text/language" ) const ( PageEntries = 50 ) var ( sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2)) repo *git.Repository notesTree *object.Tree commentsRef *plumbing.Reference commentsTree *object.Tree topicsRef *plumbing.Reference topicsTree *object.Tree localizer *spreak.Localizer //go:embed locale/* locales embed.FS ) type Cfg struct { GitPath string Branch string Title string Lang string URLPrefix string AtomBaseURL string AtomId string AtomAuthor string CSS string Webmaster string AboutURL string GitURLs []string CommentsNotesRef string CommentsEmail string TopicsNotesRef string TopicsCachePath string GopherDomain string ImgPath string ImgDomain string } func msgSplit(msg string) []string { lines := strings.Split(msg, "\n") lines = lines[:len(lines)-1] if len(lines) < 3 { lines = []string{lines[0], "", ""} } return lines } func initRepo(cfg *Cfg) (*plumbing.Hash, error) { var err error repo, err = git.PlainOpen(cfg.GitPath) if err != nil { return nil, err } head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false) if err != nil { return nil, err } headHash := head.Hash() if notes, err := repo.Notes(); err == nil { var notesRef *plumbing.Reference notes.ForEach(func(ref *plumbing.Reference) error { switch string(ref.Name()) { case "refs/notes/commits": notesRef = ref case cfg.CommentsNotesRef: commentsRef = ref case cfg.TopicsNotesRef: topicsRef = ref } return nil }) if notesRef != nil { if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil { notesTree, _ = commentsCommit.Tree() } } if commentsRef != nil { if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil { commentsTree, _ = commentsCommit.Tree() } } if topicsRef != nil { if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil { topicsTree, _ = topicsCommit.Tree() } } } return &headHash, nil } func readCfg(cfgPath string) (*Cfg, error) { cfgRaw, err := os.ReadFile(cfgPath) if err != nil { return nil, err } var cfgGeneral map[string]interface{} if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil { return nil, err } cfgRaw, err = json.Marshal(cfgGeneral) if err != nil { return nil, err } var cfg *Cfg if err = json.Unmarshal(cfgRaw, &cfg); err != nil { return nil, err } return cfg, nil } func initLocalizer(lang string) { fsys, _ := fs.Sub(locales, "locale") bundle, err := spreak.NewBundle( spreak.WithSourceLanguage(language.English), spreak.WithDomainFs(spreak.NoDomain, fsys), spreak.WithLanguage(language.Russian), ) if err != nil { log.Fatalln(err) } if lang == "" { lang = language.English.String() } localizer = spreak.NewLocalizer(bundle, language.MustParse(lang)) } func main() { gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file") geminiCfgPath := flag.String("gemini", "", "Path to gemini-related configuration file") flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog: sgblog -- run CGI HTTP backend sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend sgblog -gemini /path/to/cfg.hjson -- run UCSPI+tlss Gemini backend `) } flag.Parse() log.SetFlags(log.Lshortfile) if *gopherCfgPath != "" { serveGopher(*gopherCfgPath) } else if *geminiCfgPath != "" { serveGemini(*geminiCfgPath) } else { serveHTTP() } }