--- /dev/null
+/*
+SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+// Git-backed CGI/inetd blogging/phlogging engine
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "sort"
+
+ "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.stargrave.org/sgblog"
+)
+
+func main() {
+ branch := flag.String("branch", "refs/heads/master", "Blog's branch reference name")
+ topicsRefName := flag.String("topics-ref", "refs/notes/topics", "Topics reference name")
+ flag.Usage = func() {
+ fmt.Fprintln(flag.CommandLine.Output(), "Show known topics")
+ flag.PrintDefaults()
+ }
+ flag.Parse()
+ log.SetFlags(0)
+
+ repo, err := git.PlainOpen(".")
+ if err != nil {
+ log.Fatalln(err)
+ }
+ head, err := repo.Reference(plumbing.ReferenceName(*branch), false)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ headHash := head.Hash()
+
+ var topicsRef *plumbing.Reference
+ var topicsTree *object.Tree
+ if notes, err := repo.Notes(); err == nil {
+ notes.ForEach(func(ref *plumbing.Reference) error {
+ if string(ref.Name()) == *topicsRefName {
+ topicsRef = ref
+ }
+ return nil
+ })
+ if topicsRef != nil {
+ if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
+ topicsTree, _ = topicsCommit.Tree()
+ }
+ }
+ }
+
+ repoLog, err := repo.Log(&git.LogOptions{From: headHash})
+ if err != nil {
+ log.Fatalln(err)
+ }
+ topicsCounter := map[string]int{}
+ for {
+ commit, err := repoLog.Next()
+ if err != nil {
+ break
+ }
+ for _, topic := range sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)) {
+ topicsCounter[topic]++
+ }
+ }
+ topics := make([]string, 0, len(topicsCounter))
+ for topic := range topicsCounter {
+ topics = append(topics, topic)
+ }
+ sort.Strings(topics)
+ for _, topic := range topics {
+ fmt.Printf("%s\t%d\n", topic, topicsCounter[topic])
+ }
+}
Commit: commit,
When: commit.Author.When.Format(sgblog.WhenFmt),
Cfg: cfg,
- Note: string(getNote(notesTree, commit.Hash)),
- Comments: parseComments(getNote(commentsTree, commit.Hash)),
- Topics: parseTopics(getNote(topicsTree, commit.Hash)),
+ Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)),
+ Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
+ Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
Version: sgblog.Version,
TitleEscaped: url.PathEscape(fmt.Sprintf(
"Re: %s (%s)", msgSplit(commit.Message)[0], commit.Hash,
Commit: commit,
Title: lines[0],
LinesNum: len(lines) - 2,
- CommentsNum: len(parseComments(getNote(commentsTree, commit.Hash))),
- Topics: parseTopics(getNote(topicsTree, commit.Hash)),
+ CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash))),
+ Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
})
}
tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
break
}
etagHash.Write(commit.Hash[:])
- commentsRaw := getNote(commentsTree, commit.Hash)
+ commentsRaw := sgblog.GetNote(repo, commentsTree, commit.Hash)
etagHash.Write(commentsRaw)
- topicsRaw := getNote(topicsTree, commit.Hash)
+ topicsRaw := sgblog.GetNote(repo, topicsTree, commit.Hash)
etagHash.Write(topicsRaw)
entries = append(entries, TableEntry{
Commit: commit,
}
entry.DomainURLs = append(entry.DomainURLs, makeA(line, u.Host))
}
- entry.CommentsNum = len(parseComments(entry.CommentsRaw))
- entry.Topics = parseTopics(entry.TopicsRaw)
+ entry.CommentsNum = len(sgblog.ParseComments(entry.CommentsRaw))
+ entry.Topics = sgblog.ParseTopics(entry.TopicsRaw)
entries[i] = entry
}
offsetPrev := offset - PageEntries
}
lines := msgSplit(commit.Message)
var categories []atom.Category
- for _, topic := range parseTopics(getNote(topicsTree, commit.Hash)) {
+ for _, topic := range sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)) {
categories = append(categories, atom.Category{Term: topic})
}
htmlized := make([]string, 0, len(lines))
if err != nil {
continue
}
- comments := parseComments(getNote(t, commentedHash))
+ comments := sgblog.ParseComments(sgblog.GetNote(repo, t, commentedHash))
if len(comments) == 0 {
continue
}
cfg.AtomBaseURL, cfg.URLPrefix, "/",
commit.Hash.String(), "/", AtomCommentsFeed,
}, "")
- commentsRaw := getNote(commentsTree, commit.Hash)
+ commentsRaw := sgblog.GetNote(repo, commentsTree, commit.Hash)
etagHash.Write(commentsRaw)
- topicsRaw := getNote(topicsTree, commit.Hash)
+ topicsRaw := sgblog.GetNote(repo, topicsTree, commit.Hash)
etagHash.Write(topicsRaw)
if strings.HasSuffix(pathInfo, AtomCommentsFeed) {
etagHash.Write([]byte("ATOM COMMENTS"))
date string
body []string
}
- commentsRaw := parseComments(commentsRaw)
+ commentsRaw := sgblog.ParseComments(commentsRaw)
var toSkip int
if len(commentsRaw) > PageEntries {
toSkip = len(commentsRaw) - PageEntries
out.Write(data)
goto AtomFinish
}
- notesRaw := getNote(notesTree, commit.Hash)
+ notesRaw := sgblog.GetNote(repo, notesTree, commit.Hash)
etagHash.Write(notesRaw)
checkETag(etagHash)
if len(commit.ParentHashes) > 0 {
parent = commit.ParentHashes[0].String()
}
- commentsParsed := parseComments(commentsRaw)
+ commentsParsed := sgblog.ParseComments(commentsRaw)
comments := make([]CommentEntry, 0, len(commentsParsed))
for _, comment := range commentsParsed {
lines := strings.Split(comment, "\n")
Lines: lines[2:],
NoteLines: notesLines,
Comments: comments,
- Topics: parseTopics(topicsRaw),
+ Topics: sgblog.ParseTopics(topicsRaw),
})
if err != nil {
makeErr(err)
package main
import (
- "bytes"
"crypto/sha1"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"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"
"github.com/hjson/hjson-go"
- "go.cypherpunks.ru/recfile"
)
const (
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{}
- r := recfile.NewReader(bytes.NewReader(data))
- for {
- fields, err := r.Next()
- if err != nil {
- break
- }
- if len(fields) != 3 ||
- fields[0].Name != "From" ||
- fields[1].Name != "Date" ||
- fields[2].Name != "Body" {
- continue
- }
- comments = append(comments, fmt.Sprintf(
- "%s: %s\n%s: %s\n%s",
- fields[0].Name, fields[0].Value,
- fields[1].Name, fields[1].Value,
- fields[2].Value,
- ))
- }
- 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)
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
+ "go.stargrave.org/sgblog"
)
type TopicsCache map[string][]plumbing.Hash
if err != nil {
break
}
- for _, topic := range parseTopics(getNote(topicsTree, commit.Hash)) {
+ for _, topic := range sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)) {
cache[topic] = append(cache[topic], commit.Hash)
}
}
// SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
package sgblog
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "sort"
+ "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/recfile"
+)
+
const (
Version = "0.17.0"
WhenFmt = "2006-01-02 15:04:05Z07:00"
)
+
+func ParseComments(data []byte) []string {
+ comments := []string{}
+ r := recfile.NewReader(bytes.NewReader(data))
+ for {
+ fields, err := r.Next()
+ if err != nil {
+ break
+ }
+ if len(fields) != 3 ||
+ fields[0].Name != "From" ||
+ fields[1].Name != "Date" ||
+ fields[2].Name != "Body" {
+ continue
+ }
+ comments = append(comments, fmt.Sprintf(
+ "%s: %s\n%s: %s\n%s",
+ fields[0].Name, fields[0].Value,
+ fields[1].Name, fields[1].Value,
+ fields[2].Value,
+ ))
+ }
+ 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 GetNote(repo *git.Repository, 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'})
+}