]> Sergey Matveev's repositories - sgblog.git/commitdiff
Gopher protocol support
authorSergey Matveev <stargrave@stargrave.org>
Sat, 14 Mar 2020 10:22:03 +0000 (13:22 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sat, 14 Mar 2020 10:22:03 +0000 (13:22 +0300)
cmd/sgblog/main.go

index 2db6dbeb2316f37e943bfe955b8ad6f407cdfe4d..a55411f798265291cee0e4f4cfdb0a3241061cb7 100644 (file)
@@ -19,6 +19,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 package main
 
 import (
+       "bufio"
        "bytes"
        "compress/gzip"
        "encoding/hex"
@@ -50,6 +51,7 @@ import (
 const (
        PageEntries = 50
        AtomFeed    = "feed.atom"
+       CRLF        = "\r\n"
 )
 
 var (
@@ -66,6 +68,7 @@ var (
                "https":  struct{}{},
                "telnet": struct{}{},
        }
+       DashLine = strings.Repeat("-", 72)
 )
 
 type TableEntry struct {
@@ -91,6 +94,8 @@ type Cfg struct {
 
        CommentsNotesRef string
        CommentsEmail    string
+
+       GopherDomain string
 }
 
 func makeA(href, text string) string {
@@ -223,20 +228,48 @@ func checkETag(etag hash.Hash) {
        }
 }
 
-func main() {
-       cfgPath := os.Getenv("SGBLOG_CFG")
-       if cfgPath == "" {
-               log.Fatalln("SGBLOG_CFG is not set")
-       }
-       pathInfo, exists := os.LookupEnv("PATH_INFO")
-       if !exists {
-               pathInfo = "/"
+func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
+       var err error
+       repo, err = git.PlainOpen(cfg.GitPath)
+       if err != nil {
+               return nil, err
        }
-       queryValues, err := url.ParseQuery(os.Getenv("QUERY_STRING"))
+       head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
        if err != nil {
-               makeErr(err)
+               return nil, err
        }
+       headHash := head.Hash()
+       if notes, err := repo.Notes(); err == nil {
+               var notesRef *plumbing.Reference
+               var commentsRef *plumbing.Reference
+               notes.ForEach(func(ref *plumbing.Reference) error {
+                       switch string(ref.Name()) {
+                       case "refs/notes/commits":
+                               notesRef = ref
+                       case cfg.CommentsNotesRef:
+                               commentsRef = 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()
+                       }
+               }
+       }
+       return &headHash, nil
+}
 
+func serveHTTP() {
+       cfgPath := os.Getenv("SGBLOG_CFG")
+       if cfgPath == "" {
+               log.Fatalln("SGBLOG_CFG is not set")
+       }
        cfgRaw, err := ioutil.ReadFile(cfgPath)
        if err != nil {
                makeErr(err)
@@ -253,6 +286,14 @@ func main() {
        if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
                makeErr(err)
        }
+       pathInfo, exists := os.LookupEnv("PATH_INFO")
+       if !exists {
+               pathInfo = "/"
+       }
+       queryValues, err := url.ParseQuery(os.Getenv("QUERY_STRING"))
+       if err != nil {
+               makeErr(err)
+       }
 
        etagHash, err := blake2b.New256(nil)
        if err != nil {
@@ -295,11 +336,7 @@ func main() {
        atomURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomFeed
        defaultLinks = append(defaultLinks, `<link rel="alternate" title="Atom feed" href="`+atomURL+`" type="application/atom+xml">`)
 
-       repo, err = git.PlainOpen(cfg.GitPath)
-       if err != nil {
-               makeErr(err)
-       }
-       head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
+       headHash, err := initRepo(cfg)
        if err != nil {
                makeErr(err)
        }
@@ -451,7 +488,7 @@ func main() {
                out.Write(refs.Bytes())
                out.Write([]byte("\n"))
        } else if pathInfo == "/"+AtomFeed {
-               commit, err := repo.CommitObject(head.Hash())
+               commit, err := repo.CommitObject(*headHash)
                if err != nil {
                        makeErr(err)
                }
@@ -605,3 +642,146 @@ func main() {
        }
        os.Stdout.Write(outBuf.Bytes())
 }
+
+func serveGopher() {
+       cfgPath := os.Args[2]
+       cfgRaw, err := ioutil.ReadFile(cfgPath)
+       if err != nil {
+               log.Fatalln(err)
+       }
+       var cfgGeneral map[string]interface{}
+       if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
+               log.Fatalln(err)
+       }
+       cfgRaw, err = json.Marshal(cfgGeneral)
+       if err != nil {
+               log.Fatalln(err)
+       }
+       var cfg *Cfg
+       if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
+               log.Fatalln(err)
+       }
+       if cfg.GopherDomain == "" {
+               log.Fatalln("GopherDomain is not configured")
+       }
+
+       headHash, err := initRepo(cfg)
+       if err != nil {
+               log.Fatalln(err)
+       }
+
+       scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
+       if !scanner.Scan() {
+               log.Fatalln(errors.New("no CRLF found"))
+       }
+       selector := scanner.Text()
+       if selector == "" {
+               selector = "offset/0"
+       }
+       if strings.HasPrefix(selector, "offset/") {
+               offset, err := strconv.Atoi(selector[len("offset/"):])
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               commitN := 0
+               for i := 0; i < offset; i++ {
+                       if _, err = repoLog.Next(); err != nil {
+                               break
+                       }
+                       commitN++
+               }
+
+               logEnded := false
+               var menu bytes.Buffer
+               for i := 0; i < PageEntries; i++ {
+                       commit, err := repoLog.Next()
+                       if err != nil {
+                               logEnded = true
+                               break
+                       }
+                       commitN++
+                       lines := msgSplit(commit.Message)
+
+                       var commentsValue string
+                       if l := len(parseComments(getNote(commentsTree, commit.Hash))); l > 0 {
+                               commentsValue = fmt.Sprintf(" (%dC)", l)
+                       }
+                       menu.WriteString(fmt.Sprintf(
+                               "0[%s] %s (%dL)%s\t/%s\t%s\t%d%s",
+                               commit.Author.When.Format(sgblog.WhenFmt),
+                               lines[0],
+                               len(lines)-2,
+                               commentsValue,
+                               commit.Hash.String(),
+                               cfg.GopherDomain, 70, CRLF,
+                       ))
+               }
+
+               var links bytes.Buffer
+               if offset > 0 {
+                       offsetPrev := offset - PageEntries
+                       if offsetPrev < 0 {
+                               offsetPrev = 0
+                       }
+                       links.WriteString(fmt.Sprintf(
+                               "1Prev\toffset/%d\t%s\t%d%s",
+                               offsetPrev,
+                               cfg.GopherDomain, 70, CRLF,
+                       ))
+               }
+               if !logEnded {
+                       links.WriteString(fmt.Sprintf(
+                               "1Next\toffset/%d\t%s\t%d%s",
+                               offset+PageEntries,
+                               cfg.GopherDomain, 70, CRLF,
+                       ))
+               }
+
+               fmt.Printf(
+                       "i%s (%d-%d)\t\tnull.host\t1%s",
+                       cfg.Title,
+                       offset,
+                       offset+PageEntries,
+                       CRLF,
+               )
+               if cfg.AboutURL != "" {
+                       fmt.Printf("iAbout: %s\t\tnull.host\t1%s", cfg.AboutURL, CRLF)
+               }
+               fmt.Print(links.String())
+               fmt.Print(menu.String())
+               fmt.Print("." + CRLF)
+       } else if sha1DigestRe.MatchString(selector) {
+               commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               fmt.Printf(
+                       "What: %s\nWhen: %s\n%s\n%s",
+                       commit.Hash.String(),
+                       commit.Author.When.Format(sgblog.WhenFmt),
+                       DashLine,
+                       commit.Message,
+               )
+               notesRaw := getNote(notesTree, commit.Hash)
+               if len(notesRaw) > 0 {
+                       fmt.Printf("%s\nNote:\n%s\n", DashLine, string(notesRaw))
+               }
+               for i, comment := range parseComments(getNote(commentsTree, commit.Hash)) {
+                       fmt.Printf("%s\ncomment %d:\n%s\n", DashLine, i, comment)
+               }
+       } else {
+               log.Fatalln(errors.New("unknown selector"))
+       }
+}
+
+func main() {
+       if len(os.Args) == 3 && os.Args[1] == "-gopher" {
+               serveGopher()
+       } else {
+               serveHTTP()
+       }
+}