/* SGBlog -- Git-backed CGI/inetd blogging/phlogging engine Copyright (C) 2020 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/inetd blogging/phlogging engine package main import ( "bytes" "crypto/sha1" "fmt" "io/ioutil" "os" "regexp" "sort" "strings" "text/scanner" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "go.cypherpunks.ru/netstring/v2" ) 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 ) type Cfg struct { GitPath string Branch string Title 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 } 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 getNote(tree *object.Tree, what plumbing.Hash) []byte { if tree == nil { return nil } var entry *object.TreeEntry var err error paths := make([]string, 3) paths[0] = what.String() paths[1] = paths[0][:2] + "/" + paths[0][2:] paths[2] = paths[1][:4+1] + "/" + paths[1][4+1:] for _, p := range paths { entry, err = tree.FindEntry(p) if err == nil { break } } if entry == nil { return nil } blob, err := repo.BlobObject(entry.Hash) if err != nil { return nil } r, err := blob.Reader() if err != nil { return nil } data, err := ioutil.ReadAll(r) if err != nil { return nil } return bytes.TrimSuffix(data, []byte{'\n'}) } func parseComments(data []byte) []string { comments := []string{} nsr := netstring.NewReader(bytes.NewReader(data)) for { if _, err := nsr.Next(); err != nil { break } if comment, err := ioutil.ReadAll(nsr); err == nil { comments = append(comments, string(comment)) } } return comments } func parseTopics(data []byte) []string { var s scanner.Scanner s.Init(bytes.NewBuffer(data)) topics := []string{} for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() { topics = append(topics, s.TokenText()) } sort.Strings(topics) return topics } 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 main() { if len(os.Args) == 3 && os.Args[1] == "-gopher" { serveGopher() } else { serveHTTP() } }