]> Sergey Matveev's repositories - sgblog.git/commitdiff
Separate HTTP and Gopher related functions
authorSergey Matveev <stargrave@stargrave.org>
Sat, 14 Mar 2020 10:34:38 +0000 (13:34 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sat, 14 Mar 2020 10:34:38 +0000 (13:34 +0300)
cmd/sgblog/gopher.go [new file with mode: 0644]
cmd/sgblog/http.go [new file with mode: 0644]
cmd/sgblog/main.go

diff --git a/cmd/sgblog/gopher.go b/cmd/sgblog/gopher.go
new file mode 100644 (file)
index 0000000..808c40f
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+SGBlog -- Git-based CGI blogging engine
+Copyright (C) 2020 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-based CGI blogging engine
+package main
+
+import (
+       "bufio"
+       "bytes"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "log"
+       "os"
+       "strconv"
+       "strings"
+
+       "github.com/hjson/hjson-go"
+       "go.stargrave.org/sgblog"
+       "gopkg.in/src-d/go-git.v4"
+       "gopkg.in/src-d/go-git.v4/plumbing"
+)
+
+const CRLF = "\r\n"
+
+var DashLine = strings.Repeat("-", 72)
+
+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"))
+       }
+}
diff --git a/cmd/sgblog/http.go b/cmd/sgblog/http.go
new file mode 100644 (file)
index 0000000..18477a6
--- /dev/null
@@ -0,0 +1,518 @@
+/*
+SGBlog -- Git-based CGI blogging engine
+Copyright (C) 2020 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-based CGI blogging engine
+package main
+
+import (
+       "bytes"
+       "compress/gzip"
+       "encoding/hex"
+       "encoding/json"
+       "encoding/xml"
+       "errors"
+       "fmt"
+       "hash"
+       "html"
+       "io"
+       "io/ioutil"
+       "log"
+       "net/url"
+       "os"
+       "strconv"
+       "strings"
+
+       "github.com/hjson/hjson-go"
+       "go.stargrave.org/sgblog"
+       "golang.org/x/crypto/blake2b"
+       "golang.org/x/tools/blog/atom"
+       "gopkg.in/src-d/go-git.v4"
+       "gopkg.in/src-d/go-git.v4/plumbing"
+       "gopkg.in/src-d/go-git.v4/plumbing/object"
+)
+
+const (
+       AtomFeed = "feed.atom"
+)
+
+var (
+       defaultLinks = []string{}
+
+       renderableSchemes = map[string]struct{}{
+               "ftp":    struct{}{},
+               "gopher": struct{}{},
+               "http":   struct{}{},
+               "https":  struct{}{},
+               "telnet": struct{}{},
+       }
+)
+
+type TableEntry struct {
+       commit      *object.Commit
+       commentsRaw []byte
+}
+
+func makeA(href, text string) string {
+       return `<a href="` + href + `">` + text + `</a>`
+}
+
+func etagString(etag hash.Hash) string {
+       return `"` + hex.EncodeToString(etag.Sum(nil)) + `"`
+}
+
+func urlParse(what string) *url.URL {
+       if u, err := url.ParseRequestURI(what); err == nil {
+               if _, exists := renderableSchemes[u.Scheme]; exists {
+                       return u
+               }
+       }
+       return nil
+}
+
+func lineURLize(urlPrefix, line string) string {
+       cols := strings.Split(html.EscapeString(line), " ")
+       for i, col := range cols {
+               if u := urlParse(col); u != nil {
+                       cols[i] = makeA(col, col)
+                       continue
+               }
+               cols[i] = sha1DigestRe.ReplaceAllString(col, makeA(
+                       urlPrefix+"/$1", "$1",
+               ))
+       }
+       return strings.Join(cols, " ")
+}
+
+func startHeader(etag hash.Hash, gziped bool) string {
+       lines := []string{
+               "Content-Type: text/html; charset=UTF-8",
+               "ETag: " + etagString(etag),
+       }
+       if gziped {
+               lines = append(lines, "Content-Encoding: gzip")
+       }
+       lines = append(lines, "")
+       lines = append(lines, "")
+       return strings.Join(lines, "\n")
+}
+
+func startHTML(title string, additional []string) string {
+       return fmt.Sprintf(`<html>
+<head>
+       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+       <meta name="generator" content="SGBlog %s">
+       <title>%s</title>
+       %s
+</head>
+<body>
+`,
+               sgblog.Version, title,
+               strings.Join(append(defaultLinks, additional...), "\n   "),
+       )
+}
+
+func makeErr(err error) {
+       fmt.Print("Content-Type: text/plain; charset=UTF-8\n\n")
+       fmt.Println(err)
+       panic(err)
+}
+
+func checkETag(etag hash.Hash) {
+       ifNoneMatch := os.Getenv("HTTP_IF_NONE_MATCH")
+       if ifNoneMatch != "" && ifNoneMatch == etagString(etag) {
+               fmt.Printf("Status: 304\nETag: %s\n\n", ifNoneMatch)
+               os.Exit(0)
+       }
+}
+
+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)
+       }
+       var cfgGeneral map[string]interface{}
+       if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
+               makeErr(err)
+       }
+       cfgRaw, err = json.Marshal(cfgGeneral)
+       if err != nil {
+               makeErr(err)
+       }
+       var cfg *Cfg
+       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 {
+               panic(err)
+       }
+       etagHash.Write([]byte("SGBLOG"))
+       etagHash.Write([]byte(sgblog.Version))
+       etagHash.Write([]byte(cfg.GitPath))
+       etagHash.Write([]byte(cfg.Branch))
+       etagHash.Write([]byte(cfg.Title))
+       etagHash.Write([]byte(cfg.URLPrefix))
+       etagHash.Write([]byte(cfg.AtomBaseURL))
+       etagHash.Write([]byte(cfg.AtomId))
+       etagHash.Write([]byte(cfg.AtomAuthor))
+
+       etagHashForWeb := [][]byte{}
+       if cfg.CSS != "" {
+               defaultLinks = append(defaultLinks, `<link rel="stylesheet" type="text/css" href="`+cfg.CSS+`">`)
+               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CSS))
+       }
+       if cfg.Webmaster != "" {
+               defaultLinks = append(defaultLinks, `<link rev="made" href="mailto:`+cfg.Webmaster+`">`)
+               etagHashForWeb = append(etagHashForWeb, []byte(cfg.Webmaster))
+       }
+       if cfg.AboutURL != "" {
+               etagHashForWeb = append(etagHashForWeb, []byte(cfg.AboutURL))
+       }
+       for _, gitURL := range cfg.GitURLs {
+               defaultLinks = append(defaultLinks, `<link rel="vcs-git" href="`+gitURL+`" title="Git repository">`)
+               etagHashForWeb = append(etagHashForWeb, []byte(gitURL))
+       }
+       if cfg.CommentsNotesRef != "" {
+               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CommentsNotesRef))
+       }
+       if cfg.CommentsEmail != "" {
+               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CommentsEmail))
+       }
+
+       defaultLinks = append(defaultLinks, `<link rel="top" href="`+cfg.URLPrefix+`/" title="top">`)
+       atomURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomFeed
+       defaultLinks = append(defaultLinks, `<link rel="alternate" title="Atom feed" href="`+atomURL+`" type="application/atom+xml">`)
+
+       headHash, err := initRepo(cfg)
+       if err != nil {
+               makeErr(err)
+       }
+
+       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()
+                       }
+               }
+       }
+
+       var outBuf bytes.Buffer
+       var out io.Writer
+       out = &outBuf
+       var gzipWriter *gzip.Writer
+       acceptEncoding := os.Getenv("HTTP_ACCEPT_ENCODING")
+       for _, encoding := range strings.Split(acceptEncoding, ", ") {
+               if encoding == "gzip" {
+                       gzipWriter = gzip.NewWriter(&outBuf)
+                       out = gzipWriter
+               }
+       }
+
+       if pathInfo == "/" {
+               offset := 0
+               if offsetRaw, exists := queryValues["offset"]; exists {
+                       offset, err = strconv.Atoi(offsetRaw[0])
+                       if err != nil {
+                               makeErr(err)
+                       }
+               }
+               repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
+               if err != nil {
+                       makeErr(err)
+               }
+               commitN := 0
+               for i := 0; i < offset; i++ {
+                       if _, err = repoLog.Next(); err != nil {
+                               break
+                       }
+                       commitN++
+               }
+
+               entries := make([]TableEntry, 0, PageEntries)
+               logEnded := false
+               for _, data := range etagHashForWeb {
+                       etagHash.Write(data)
+               }
+               etagHash.Write([]byte("INDEX"))
+               for i := 0; i < PageEntries; i++ {
+                       commit, err := repoLog.Next()
+                       if err != nil {
+                               logEnded = true
+                               break
+                       }
+                       etagHash.Write(commit.Hash[:])
+                       commentsRaw := getNote(commentsTree, commit.Hash)
+                       etagHash.Write(commentsRaw)
+                       entries = append(entries, TableEntry{commit, commentsRaw})
+               }
+               checkETag(etagHash)
+
+               var table bytes.Buffer
+               table.WriteString(
+                       "<table border=1>\n" +
+                               "<caption>Comments</caption>\n<tr>" +
+                               "<th>N</th>" +
+                               "<th>When</th>" +
+                               "<th>Title</th>" +
+                               `<th size="5%"><a title="Lines">L</a></th>` +
+                               `<th size="5%"><a title="Comments">C</a></th>` +
+                               "<th>Linked to</th></tr>\n")
+               for _, entry := range entries {
+                       commitN++
+                       lines := msgSplit(entry.commit.Message)
+                       domains := []string{}
+                       for _, line := range lines[2:] {
+                               if u := urlParse(line); u == nil {
+                                       break
+                               } else {
+                                       domains = append(domains, makeA(line, u.Host))
+                               }
+                       }
+                       var commentsValue string
+                       if l := len(parseComments(entry.commentsRaw)); l > 0 {
+                               commentsValue = strconv.Itoa(l)
+                       } else {
+                               commentsValue = "&nbsp;"
+                       }
+                       table.WriteString(fmt.Sprintf(
+                               "<tr><td>%d</td><td><tt>%s</tt></td>"+
+                                       "<td>%s</td>"+
+                                       "<td>%d</td><td>%s</td>"+
+                                       "<td>%s</td></tr>\n",
+                               commitN, entry.commit.Author.When.Format(sgblog.WhenFmt),
+                               makeA(cfg.URLPrefix+"/"+entry.commit.Hash.String(), lines[0]),
+                               len(lines)-2,
+                               commentsValue,
+                               strings.Join(domains, " "),
+                       ))
+               }
+               table.WriteString("</table>")
+
+               var href string
+               var links []string
+               var refs bytes.Buffer
+               if offset > 0 {
+                       if offsetPrev := offset - PageEntries; offsetPrev > 0 {
+                               href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offsetPrev)
+                       } else {
+                               href = cfg.URLPrefix + "/"
+                       }
+                       links = append(links, `<link rel="prev" href="`+href+`" title="newer">`)
+                       refs.WriteString("\n" + makeA(href, "[prev]"))
+               }
+               if !logEnded {
+                       href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offset+PageEntries)
+                       links = append(links, `<link rel="next" href="`+href+`" title="older">`)
+                       refs.WriteString("\n" + makeA(href, "[next]"))
+               }
+
+               os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
+               out.Write([]byte(startHTML(
+                       fmt.Sprintf("%s (%d-%d)", cfg.Title, offset, offset+PageEntries),
+                       links,
+               )))
+               if cfg.AboutURL != "" {
+                       out.Write([]byte(fmt.Sprintf("[%s]", makeA(cfg.AboutURL, "about"))))
+               }
+               out.Write(refs.Bytes())
+               out.Write(table.Bytes())
+               out.Write(refs.Bytes())
+               out.Write([]byte("\n"))
+       } else if pathInfo == "/"+AtomFeed {
+               commit, err := repo.CommitObject(*headHash)
+               if err != nil {
+                       makeErr(err)
+               }
+               etagHash.Write([]byte("ATOM"))
+               etagHash.Write(commit.Hash[:])
+               checkETag(etagHash)
+               feed := atom.Feed{
+                       Title:   cfg.Title,
+                       ID:      cfg.AtomId,
+                       Updated: atom.Time(commit.Author.When),
+                       Link: []atom.Link{{
+                               Rel:  "self",
+                               Href: atomURL,
+                       }},
+                       Author: &atom.Person{Name: cfg.AtomAuthor},
+               }
+               repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
+               if err != nil {
+                       makeErr(err)
+               }
+               for i := 0; i < PageEntries; i++ {
+                       commit, err = repoLog.Next()
+                       if err != nil {
+                               break
+                       }
+
+                       feedIdRaw := new([16]byte)
+                       copy(feedIdRaw[:], commit.Hash[:])
+                       feedIdRaw[6] = (feedIdRaw[6] & 0x0F) | uint8(4<<4) // version 4
+                       feedId := fmt.Sprintf(
+                               "%x-%x-%x-%x-%x",
+                               feedIdRaw[0:4],
+                               feedIdRaw[4:6],
+                               feedIdRaw[6:8],
+                               feedIdRaw[8:10],
+                               feedIdRaw[10:],
+                       )
+
+                       lines := msgSplit(commit.Message)
+                       feed.Entry = append(feed.Entry, &atom.Entry{
+                               Title: lines[0],
+                               ID:    "urn:uuid:" + feedId,
+                               Link: []atom.Link{{
+                                       Rel:  "alternate",
+                                       Href: cfg.AtomBaseURL + cfg.URLPrefix + "/" + commit.Hash.String(),
+                               }},
+                               Published: atom.Time(commit.Author.When),
+                               Updated:   atom.Time(commit.Author.When),
+                               Summary: &atom.Text{
+                                       Type: "text",
+                                       Body: lines[0],
+                               },
+                               Content: &atom.Text{
+                                       Type: "text",
+                                       Body: strings.Join(lines[2:], "\n"),
+                               },
+                       })
+               }
+               data, err := xml.MarshalIndent(&feed, "", "  ")
+               if err != nil {
+                       makeErr(err)
+               }
+               out.Write(data)
+               os.Stdout.WriteString("Content-Type: text/xml; charset=UTF-8\n")
+               os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
+               if gzipWriter != nil {
+                       os.Stdout.WriteString("Content-Encoding: gzip\n")
+                       gzipWriter.Close()
+               }
+               os.Stdout.WriteString("\n")
+               os.Stdout.Write(outBuf.Bytes())
+               return
+       } else if sha1DigestRe.MatchString(pathInfo[1:]) {
+               commit, err := repo.CommitObject(plumbing.NewHash(pathInfo[1:]))
+               if err != nil {
+                       makeErr(err)
+               }
+               for _, data := range etagHashForWeb {
+                       etagHash.Write(data)
+               }
+               etagHash.Write([]byte("ENTRY"))
+               etagHash.Write(commit.Hash[:])
+               notesRaw := getNote(notesTree, commit.Hash)
+               etagHash.Write(notesRaw)
+               commentsRaw := getNote(commentsTree, commit.Hash)
+               etagHash.Write(commentsRaw)
+               checkETag(etagHash)
+               lines := msgSplit(commit.Message)
+               title := lines[0]
+               when := commit.Author.When.Format(sgblog.WhenFmt)
+               os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
+               links := []string{}
+               var parent string
+               if len(commit.ParentHashes) > 0 {
+                       parent = commit.ParentHashes[0].String()
+                       links = append(links, `<link rel="prev" href="`+cfg.URLPrefix+"/"+parent+`" title="older">`)
+               }
+               out.Write([]byte(startHTML(fmt.Sprintf("%s (%s)", title, when), links)))
+               if cfg.AboutURL != "" {
+                       out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.AboutURL, "about"))))
+               }
+               out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.URLPrefix+"/", "index"))))
+               if parent != "" {
+                       out.Write([]byte(fmt.Sprintf(
+                               "[%s]\n",
+                               makeA(cfg.URLPrefix+"/"+parent, "older"),
+                       )))
+               }
+               out.Write([]byte(fmt.Sprintf(
+                       "[<tt><a title=\"When\">%s</a></tt>]\n"+
+                               "[<tt><a title=\"Hash\">%s</a></tt>]\n"+
+                               "<hr/>\n<h2>%s</h2>\n<pre>\n",
+                       when, commit.Hash.String(), title,
+               )))
+               for _, line := range lines[2:] {
+                       out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
+               }
+               out.Write([]byte("</pre>\n<hr/>\n"))
+               if len(notesRaw) > 0 {
+                       out.Write([]byte("Note:<pre>\n" + string(notesRaw) + "\n</pre>\n<hr/>\n"))
+               }
+               if cfg.CommentsEmail != "" {
+                       out.Write([]byte("[" + makeA(
+                               "mailto:"+cfg.CommentsEmail+"?subject="+commit.Hash.String(),
+                               "write comment",
+                       ) + "]\n"))
+               }
+               out.Write([]byte("<dl>\n"))
+               for i, comment := range parseComments(commentsRaw) {
+                       out.Write([]byte(fmt.Sprintf(
+                               "<dt><a name=\"comment%d\"><a href=\"#comment%d\">comment %d</a>:"+
+                                       "</dt>\n<dd><pre>\n",
+                               i, i, i,
+                       )))
+                       lines = strings.Split(comment, "\n")
+                       for _, line := range lines[:3] {
+                               out.Write([]byte(line + "\n"))
+                       }
+                       for _, line := range lines[3:] {
+                               out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
+                       }
+                       out.Write([]byte("</pre></dd>\n"))
+               }
+               out.Write([]byte("</dl>\n"))
+       } else {
+               makeErr(errors.New("unknown URL action"))
+       }
+       out.Write([]byte("</body></html>\n"))
+       if gzipWriter != nil {
+               gzipWriter.Close()
+       }
+       os.Stdout.Write(outBuf.Bytes())
+}
index a55411f798265291cee0e4f4cfdb0a3241061cb7..f3d009ab464181d04d5df45e031da49dda66f6ff 100644 (file)
@@ -19,30 +19,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 package main
 
 import (
-       "bufio"
        "bytes"
-       "compress/gzip"
-       "encoding/hex"
-       "encoding/json"
-       "encoding/xml"
-       "errors"
-       "fmt"
-       "hash"
-       "html"
-       "io"
        "io/ioutil"
-       "log"
-       "net/url"
        "os"
        "regexp"
-       "strconv"
        "strings"
 
-       "github.com/hjson/hjson-go"
        "go.cypherpunks.ru/netstring/v2"
-       "go.stargrave.org/sgblog"
-       "golang.org/x/crypto/blake2b"
-       "golang.org/x/tools/blog/atom"
        "gopkg.in/src-d/go-git.v4"
        "gopkg.in/src-d/go-git.v4/plumbing"
        "gopkg.in/src-d/go-git.v4/plumbing/object"
@@ -50,32 +33,15 @@ import (
 
 const (
        PageEntries = 50
-       AtomFeed    = "feed.atom"
-       CRLF        = "\r\n"
 )
 
 var (
        sha1DigestRe = regexp.MustCompilePOSIX("([0-9a-f]{40,40})")
-       defaultLinks = []string{}
        repo         *git.Repository
        notesTree    *object.Tree
        commentsTree *object.Tree
-
-       renderableSchemes = map[string]struct{}{
-               "ftp":    struct{}{},
-               "gopher": struct{}{},
-               "http":   struct{}{},
-               "https":  struct{}{},
-               "telnet": struct{}{},
-       }
-       DashLine = strings.Repeat("-", 72)
 )
 
-type TableEntry struct {
-       commit      *object.Commit
-       commentsRaw []byte
-}
-
 type Cfg struct {
        GitPath string
        Branch  string
@@ -98,23 +64,6 @@ type Cfg struct {
        GopherDomain string
 }
 
-func makeA(href, text string) string {
-       return `<a href="` + href + `">` + text + `</a>`
-}
-
-func etagString(etag hash.Hash) string {
-       return `"` + hex.EncodeToString(etag.Sum(nil)) + `"`
-}
-
-func urlParse(what string) *url.URL {
-       if u, err := url.ParseRequestURI(what); err == nil {
-               if _, exists := renderableSchemes[u.Scheme]; exists {
-                       return u
-               }
-       }
-       return nil
-}
-
 func msgSplit(msg string) []string {
        lines := strings.Split(msg, "\n")
        lines = lines[:len(lines)-1]
@@ -124,20 +73,6 @@ func msgSplit(msg string) []string {
        return lines
 }
 
-func lineURLize(urlPrefix, line string) string {
-       cols := strings.Split(html.EscapeString(line), " ")
-       for i, col := range cols {
-               if u := urlParse(col); u != nil {
-                       cols[i] = makeA(col, col)
-                       continue
-               }
-               cols[i] = sha1DigestRe.ReplaceAllString(col, makeA(
-                       urlPrefix+"/$1", "$1",
-               ))
-       }
-       return strings.Join(cols, " ")
-}
-
 func getNote(tree *object.Tree, what plumbing.Hash) []byte {
        if tree == nil {
                return nil
@@ -186,48 +121,6 @@ func parseComments(data []byte) []string {
        return comments
 }
 
-func startHeader(etag hash.Hash, gziped bool) string {
-       lines := []string{
-               "Content-Type: text/html; charset=UTF-8",
-               "ETag: " + etagString(etag),
-       }
-       if gziped {
-               lines = append(lines, "Content-Encoding: gzip")
-       }
-       lines = append(lines, "")
-       lines = append(lines, "")
-       return strings.Join(lines, "\n")
-}
-
-func startHTML(title string, additional []string) string {
-       return fmt.Sprintf(`<html>
-<head>
-       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-       <meta name="generator" content="SGBlog %s">
-       <title>%s</title>
-       %s
-</head>
-<body>
-`,
-               sgblog.Version, title,
-               strings.Join(append(defaultLinks, additional...), "\n   "),
-       )
-}
-
-func makeErr(err error) {
-       fmt.Print("Content-Type: text/plain; charset=UTF-8\n\n")
-       fmt.Println(err)
-       panic(err)
-}
-
-func checkETag(etag hash.Hash) {
-       ifNoneMatch := os.Getenv("HTTP_IF_NONE_MATCH")
-       if ifNoneMatch != "" && ifNoneMatch == etagString(etag) {
-               fmt.Printf("Status: 304\nETag: %s\n\n", ifNoneMatch)
-               os.Exit(0)
-       }
-}
-
 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
        var err error
        repo, err = git.PlainOpen(cfg.GitPath)
@@ -265,519 +158,6 @@ func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
        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)
-       }
-       var cfgGeneral map[string]interface{}
-       if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
-               makeErr(err)
-       }
-       cfgRaw, err = json.Marshal(cfgGeneral)
-       if err != nil {
-               makeErr(err)
-       }
-       var cfg *Cfg
-       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 {
-               panic(err)
-       }
-       etagHash.Write([]byte("SGBLOG"))
-       etagHash.Write([]byte(sgblog.Version))
-       etagHash.Write([]byte(cfg.GitPath))
-       etagHash.Write([]byte(cfg.Branch))
-       etagHash.Write([]byte(cfg.Title))
-       etagHash.Write([]byte(cfg.URLPrefix))
-       etagHash.Write([]byte(cfg.AtomBaseURL))
-       etagHash.Write([]byte(cfg.AtomId))
-       etagHash.Write([]byte(cfg.AtomAuthor))
-
-       etagHashForWeb := [][]byte{}
-       if cfg.CSS != "" {
-               defaultLinks = append(defaultLinks, `<link rel="stylesheet" type="text/css" href="`+cfg.CSS+`">`)
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CSS))
-       }
-       if cfg.Webmaster != "" {
-               defaultLinks = append(defaultLinks, `<link rev="made" href="mailto:`+cfg.Webmaster+`">`)
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.Webmaster))
-       }
-       if cfg.AboutURL != "" {
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.AboutURL))
-       }
-       for _, gitURL := range cfg.GitURLs {
-               defaultLinks = append(defaultLinks, `<link rel="vcs-git" href="`+gitURL+`" title="Git repository">`)
-               etagHashForWeb = append(etagHashForWeb, []byte(gitURL))
-       }
-       if cfg.CommentsNotesRef != "" {
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CommentsNotesRef))
-       }
-       if cfg.CommentsEmail != "" {
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CommentsEmail))
-       }
-
-       defaultLinks = append(defaultLinks, `<link rel="top" href="`+cfg.URLPrefix+`/" title="top">`)
-       atomURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomFeed
-       defaultLinks = append(defaultLinks, `<link rel="alternate" title="Atom feed" href="`+atomURL+`" type="application/atom+xml">`)
-
-       headHash, err := initRepo(cfg)
-       if err != nil {
-               makeErr(err)
-       }
-
-       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()
-                       }
-               }
-       }
-
-       var outBuf bytes.Buffer
-       var out io.Writer
-       out = &outBuf
-       var gzipWriter *gzip.Writer
-       acceptEncoding := os.Getenv("HTTP_ACCEPT_ENCODING")
-       for _, encoding := range strings.Split(acceptEncoding, ", ") {
-               if encoding == "gzip" {
-                       gzipWriter = gzip.NewWriter(&outBuf)
-                       out = gzipWriter
-               }
-       }
-
-       if pathInfo == "/" {
-               offset := 0
-               if offsetRaw, exists := queryValues["offset"]; exists {
-                       offset, err = strconv.Atoi(offsetRaw[0])
-                       if err != nil {
-                               makeErr(err)
-                       }
-               }
-               repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
-               if err != nil {
-                       makeErr(err)
-               }
-               commitN := 0
-               for i := 0; i < offset; i++ {
-                       if _, err = repoLog.Next(); err != nil {
-                               break
-                       }
-                       commitN++
-               }
-
-               entries := make([]TableEntry, 0, PageEntries)
-               logEnded := false
-               for _, data := range etagHashForWeb {
-                       etagHash.Write(data)
-               }
-               etagHash.Write([]byte("INDEX"))
-               for i := 0; i < PageEntries; i++ {
-                       commit, err := repoLog.Next()
-                       if err != nil {
-                               logEnded = true
-                               break
-                       }
-                       etagHash.Write(commit.Hash[:])
-                       commentsRaw := getNote(commentsTree, commit.Hash)
-                       etagHash.Write(commentsRaw)
-                       entries = append(entries, TableEntry{commit, commentsRaw})
-               }
-               checkETag(etagHash)
-
-               var table bytes.Buffer
-               table.WriteString(
-                       "<table border=1>\n" +
-                               "<caption>Comments</caption>\n<tr>" +
-                               "<th>N</th>" +
-                               "<th>When</th>" +
-                               "<th>Title</th>" +
-                               `<th size="5%"><a title="Lines">L</a></th>` +
-                               `<th size="5%"><a title="Comments">C</a></th>` +
-                               "<th>Linked to</th></tr>\n")
-               for _, entry := range entries {
-                       commitN++
-                       lines := msgSplit(entry.commit.Message)
-                       domains := []string{}
-                       for _, line := range lines[2:] {
-                               if u := urlParse(line); u == nil {
-                                       break
-                               } else {
-                                       domains = append(domains, makeA(line, u.Host))
-                               }
-                       }
-                       var commentsValue string
-                       if l := len(parseComments(entry.commentsRaw)); l > 0 {
-                               commentsValue = strconv.Itoa(l)
-                       } else {
-                               commentsValue = "&nbsp;"
-                       }
-                       table.WriteString(fmt.Sprintf(
-                               "<tr><td>%d</td><td><tt>%s</tt></td>"+
-                                       "<td>%s</td>"+
-                                       "<td>%d</td><td>%s</td>"+
-                                       "<td>%s</td></tr>\n",
-                               commitN, entry.commit.Author.When.Format(sgblog.WhenFmt),
-                               makeA(cfg.URLPrefix+"/"+entry.commit.Hash.String(), lines[0]),
-                               len(lines)-2,
-                               commentsValue,
-                               strings.Join(domains, " "),
-                       ))
-               }
-               table.WriteString("</table>")
-
-               var href string
-               var links []string
-               var refs bytes.Buffer
-               if offset > 0 {
-                       if offsetPrev := offset - PageEntries; offsetPrev > 0 {
-                               href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offsetPrev)
-                       } else {
-                               href = cfg.URLPrefix + "/"
-                       }
-                       links = append(links, `<link rel="prev" href="`+href+`" title="newer">`)
-                       refs.WriteString("\n" + makeA(href, "[prev]"))
-               }
-               if !logEnded {
-                       href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offset+PageEntries)
-                       links = append(links, `<link rel="next" href="`+href+`" title="older">`)
-                       refs.WriteString("\n" + makeA(href, "[next]"))
-               }
-
-               os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
-               out.Write([]byte(startHTML(
-                       fmt.Sprintf("%s (%d-%d)", cfg.Title, offset, offset+PageEntries),
-                       links,
-               )))
-               if cfg.AboutURL != "" {
-                       out.Write([]byte(fmt.Sprintf("[%s]", makeA(cfg.AboutURL, "about"))))
-               }
-               out.Write(refs.Bytes())
-               out.Write(table.Bytes())
-               out.Write(refs.Bytes())
-               out.Write([]byte("\n"))
-       } else if pathInfo == "/"+AtomFeed {
-               commit, err := repo.CommitObject(*headHash)
-               if err != nil {
-                       makeErr(err)
-               }
-               etagHash.Write([]byte("ATOM"))
-               etagHash.Write(commit.Hash[:])
-               checkETag(etagHash)
-               feed := atom.Feed{
-                       Title:   cfg.Title,
-                       ID:      cfg.AtomId,
-                       Updated: atom.Time(commit.Author.When),
-                       Link: []atom.Link{{
-                               Rel:  "self",
-                               Href: atomURL,
-                       }},
-                       Author: &atom.Person{Name: cfg.AtomAuthor},
-               }
-               repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
-               if err != nil {
-                       makeErr(err)
-               }
-               for i := 0; i < PageEntries; i++ {
-                       commit, err = repoLog.Next()
-                       if err != nil {
-                               break
-                       }
-
-                       feedIdRaw := new([16]byte)
-                       copy(feedIdRaw[:], commit.Hash[:])
-                       feedIdRaw[6] = (feedIdRaw[6] & 0x0F) | uint8(4<<4) // version 4
-                       feedId := fmt.Sprintf(
-                               "%x-%x-%x-%x-%x",
-                               feedIdRaw[0:4],
-                               feedIdRaw[4:6],
-                               feedIdRaw[6:8],
-                               feedIdRaw[8:10],
-                               feedIdRaw[10:],
-                       )
-
-                       lines := msgSplit(commit.Message)
-                       feed.Entry = append(feed.Entry, &atom.Entry{
-                               Title: lines[0],
-                               ID:    "urn:uuid:" + feedId,
-                               Link: []atom.Link{{
-                                       Rel:  "alternate",
-                                       Href: cfg.AtomBaseURL + cfg.URLPrefix + "/" + commit.Hash.String(),
-                               }},
-                               Published: atom.Time(commit.Author.When),
-                               Updated:   atom.Time(commit.Author.When),
-                               Summary: &atom.Text{
-                                       Type: "text",
-                                       Body: lines[0],
-                               },
-                               Content: &atom.Text{
-                                       Type: "text",
-                                       Body: strings.Join(lines[2:], "\n"),
-                               },
-                       })
-               }
-               data, err := xml.MarshalIndent(&feed, "", "  ")
-               if err != nil {
-                       makeErr(err)
-               }
-               out.Write(data)
-               os.Stdout.WriteString("Content-Type: text/xml; charset=UTF-8\n")
-               os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
-               if gzipWriter != nil {
-                       os.Stdout.WriteString("Content-Encoding: gzip\n")
-                       gzipWriter.Close()
-               }
-               os.Stdout.WriteString("\n")
-               os.Stdout.Write(outBuf.Bytes())
-               return
-       } else if sha1DigestRe.MatchString(pathInfo[1:]) {
-               commit, err := repo.CommitObject(plumbing.NewHash(pathInfo[1:]))
-               if err != nil {
-                       makeErr(err)
-               }
-               for _, data := range etagHashForWeb {
-                       etagHash.Write(data)
-               }
-               etagHash.Write([]byte("ENTRY"))
-               etagHash.Write(commit.Hash[:])
-               notesRaw := getNote(notesTree, commit.Hash)
-               etagHash.Write(notesRaw)
-               commentsRaw := getNote(commentsTree, commit.Hash)
-               etagHash.Write(commentsRaw)
-               checkETag(etagHash)
-               lines := msgSplit(commit.Message)
-               title := lines[0]
-               when := commit.Author.When.Format(sgblog.WhenFmt)
-               os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
-               links := []string{}
-               var parent string
-               if len(commit.ParentHashes) > 0 {
-                       parent = commit.ParentHashes[0].String()
-                       links = append(links, `<link rel="prev" href="`+cfg.URLPrefix+"/"+parent+`" title="older">`)
-               }
-               out.Write([]byte(startHTML(fmt.Sprintf("%s (%s)", title, when), links)))
-               if cfg.AboutURL != "" {
-                       out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.AboutURL, "about"))))
-               }
-               out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.URLPrefix+"/", "index"))))
-               if parent != "" {
-                       out.Write([]byte(fmt.Sprintf(
-                               "[%s]\n",
-                               makeA(cfg.URLPrefix+"/"+parent, "older"),
-                       )))
-               }
-               out.Write([]byte(fmt.Sprintf(
-                       "[<tt><a title=\"When\">%s</a></tt>]\n"+
-                               "[<tt><a title=\"Hash\">%s</a></tt>]\n"+
-                               "<hr/>\n<h2>%s</h2>\n<pre>\n",
-                       when, commit.Hash.String(), title,
-               )))
-               for _, line := range lines[2:] {
-                       out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
-               }
-               out.Write([]byte("</pre>\n<hr/>\n"))
-               if len(notesRaw) > 0 {
-                       out.Write([]byte("Note:<pre>\n" + string(notesRaw) + "\n</pre>\n<hr/>\n"))
-               }
-               if cfg.CommentsEmail != "" {
-                       out.Write([]byte("[" + makeA(
-                               "mailto:"+cfg.CommentsEmail+"?subject="+commit.Hash.String(),
-                               "write comment",
-                       ) + "]\n"))
-               }
-               out.Write([]byte("<dl>\n"))
-               for i, comment := range parseComments(commentsRaw) {
-                       out.Write([]byte(fmt.Sprintf(
-                               "<dt><a name=\"comment%d\"><a href=\"#comment%d\">comment %d</a>:"+
-                                       "</dt>\n<dd><pre>\n",
-                               i, i, i,
-                       )))
-                       lines = strings.Split(comment, "\n")
-                       for _, line := range lines[:3] {
-                               out.Write([]byte(line + "\n"))
-                       }
-                       for _, line := range lines[3:] {
-                               out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
-                       }
-                       out.Write([]byte("</pre></dd>\n"))
-               }
-               out.Write([]byte("</dl>\n"))
-       } else {
-               makeErr(errors.New("unknown URL action"))
-       }
-       out.Write([]byte("</body></html>\n"))
-       if gzipWriter != nil {
-               gzipWriter.Close()
-       }
-       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()