]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gopher.go
e98d65b9f08d25c50639b39af32aec232495f08b
[sgblog.git] / cmd / sgblog / gopher.go
1 /*
2 SGBlog -- Git-based CGI blogging engine
3 Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Affero General Public License as
7 published by the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU Affero General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 // Git-based CGI blogging engine
19 package main
20
21 import (
22         "bufio"
23         "bytes"
24         "encoding/json"
25         "errors"
26         "fmt"
27         "io"
28         "io/ioutil"
29         "log"
30         "os"
31         "strconv"
32         "strings"
33         "time"
34
35         "github.com/hjson/hjson-go"
36         "go.stargrave.org/sgblog"
37         "gopkg.in/src-d/go-git.v4"
38         "gopkg.in/src-d/go-git.v4/plumbing"
39 )
40
41 const CRLF = "\r\n"
42
43 var DashLine = strings.Repeat("-", 72)
44
45 func serveGopher() {
46         cfgPath := os.Args[2]
47         cfgRaw, err := ioutil.ReadFile(cfgPath)
48         if err != nil {
49                 log.Fatalln(err)
50         }
51         var cfgGeneral map[string]interface{}
52         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
53                 log.Fatalln(err)
54         }
55         cfgRaw, err = json.Marshal(cfgGeneral)
56         if err != nil {
57                 log.Fatalln(err)
58         }
59         var cfg *Cfg
60         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
61                 log.Fatalln(err)
62         }
63         if cfg.GopherDomain == "" {
64                 log.Fatalln("GopherDomain is not configured")
65         }
66
67         headHash, err := initRepo(cfg)
68         if err != nil {
69                 log.Fatalln(err)
70         }
71
72         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
73         if !scanner.Scan() {
74                 log.Fatalln(errors.New("no CRLF found"))
75         }
76         selector := scanner.Text()
77         if selector == "" {
78                 selector = "offset/0"
79         }
80         if strings.HasPrefix(selector, "offset/") {
81                 offset, err := strconv.Atoi(selector[len("offset/"):])
82                 if err != nil {
83                         log.Fatalln(err)
84                 }
85                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
86                 if err != nil {
87                         log.Fatalln(err)
88                 }
89                 commitN := 0
90                 for i := 0; i < offset; i++ {
91                         if _, err = repoLog.Next(); err != nil {
92                                 break
93                         }
94                         commitN++
95                 }
96
97                 logEnded := false
98                 var menu bytes.Buffer
99                 var yearPrev int
100                 var monthPrev time.Month
101                 var dayPrev int
102                 for i := 0; i < PageEntries; i++ {
103                         commit, err := repoLog.Next()
104                         if err != nil {
105                                 logEnded = true
106                                 break
107                         }
108                         yearCur, monthCur, dayCur := commit.Author.When.Date()
109                         if dayCur != dayPrev || monthCur != monthPrev || yearCur != yearPrev {
110                                 menu.WriteString(fmt.Sprintf(
111                                         "i%04d-%02d-%02d\t\tnull.host\t1%s",
112                                         yearCur, monthCur, dayCur, CRLF,
113                                 ))
114                                 yearPrev, monthPrev, dayPrev = yearCur, monthCur, dayCur
115                         }
116                         commitN++
117                         lines := msgSplit(commit.Message)
118                         var commentsValue string
119                         if l := len(parseComments(getNote(commentsTree, commit.Hash))); l > 0 {
120                                 commentsValue = fmt.Sprintf(" (%dC)", l)
121                         }
122                         menu.WriteString(fmt.Sprintf(
123                                 "0[%02d:%02d] %s (%dL)%s\t/%s\t%s\t%d%s",
124                                 commit.Author.When.Hour(),
125                                 commit.Author.When.Minute(),
126                                 lines[0],
127                                 len(lines)-2,
128                                 commentsValue,
129                                 commit.Hash.String(),
130                                 cfg.GopherDomain, 70, CRLF,
131                         ))
132                 }
133
134                 var links bytes.Buffer
135                 if offset > 0 {
136                         offsetPrev := offset - PageEntries
137                         if offsetPrev < 0 {
138                                 offsetPrev = 0
139                         }
140                         links.WriteString(fmt.Sprintf(
141                                 "1Prev\toffset/%d\t%s\t%d%s",
142                                 offsetPrev,
143                                 cfg.GopherDomain, 70, CRLF,
144                         ))
145                 }
146                 if !logEnded {
147                         links.WriteString(fmt.Sprintf(
148                                 "1Next\toffset/%d\t%s\t%d%s",
149                                 offset+PageEntries,
150                                 cfg.GopherDomain, 70, CRLF,
151                         ))
152                 }
153
154                 fmt.Printf(
155                         "i%s (%d-%d)\t\tnull.host\t1%s",
156                         cfg.Title,
157                         offset,
158                         offset+PageEntries,
159                         CRLF,
160                 )
161                 if cfg.AboutURL != "" {
162                         fmt.Printf("iAbout: %s\t\tnull.host\t1%s", cfg.AboutURL, CRLF)
163                 }
164                 fmt.Print(links.String())
165                 fmt.Print(menu.String())
166                 fmt.Print("." + CRLF)
167         } else if sha1DigestRe.MatchString(selector) {
168                 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
169                 if err != nil {
170                         log.Fatalln(err)
171                 }
172                 fmt.Printf(
173                         "What: %s\nWhen: %s\n%s\n%s",
174                         commit.Hash.String(),
175                         commit.Author.When.Format(sgblog.WhenFmt),
176                         DashLine,
177                         commit.Message,
178                 )
179                 notesRaw := getNote(notesTree, commit.Hash)
180                 if len(notesRaw) > 0 {
181                         fmt.Printf("%s\nNote:\n%s\n", DashLine, string(notesRaw))
182                 }
183                 for i, comment := range parseComments(getNote(commentsTree, commit.Hash)) {
184                         fmt.Printf("%s\ncomment %d:\n%s\n", DashLine, i, comment)
185                 }
186         } else {
187                 log.Fatalln(errors.New("unknown selector"))
188         }
189 }