]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog-comment-add/main.go
Cleanup dependencies
[sgblog.git] / cmd / sgblog-comment-add / main.go
1 /*
2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020-2021 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-backed CGI/inetd blogging/phlogging engine
19 package main
20
21 import (
22         "bytes"
23         "crypto/sha1"
24         "encoding/hex"
25         "flag"
26         "fmt"
27         "io/ioutil"
28         "log"
29         "mime"
30         "net/mail"
31         "os"
32         "os/exec"
33         "regexp"
34         "strconv"
35         "strings"
36         "syscall"
37         "time"
38
39         "go.cypherpunks.ru/recfile"
40         "go.stargrave.org/sgblog"
41 )
42
43 var hashFinder = regexp.MustCompile("([0-9a-f]{40})")
44
45 // Remove various whitespaces and excess lines, because git-notes-add
46 // will remove and we have to know exact bytes count
47 func cleanupBody(body string) []string {
48         lines := strings.Split(string(body), "\n")
49         for i, line := range lines {
50                 line = strings.ReplaceAll(line, "       ", "    ")
51                 line = strings.TrimRight(line, " \r")
52                 lines[i] = line
53         }
54         for lines[0] == "" {
55                 lines = lines[1:]
56         }
57         for lines[len(lines)-1] == "" {
58                 lines = lines[:len(lines)-1]
59         }
60         withoutDups := make([]string, 0, len(lines))
61         emptyMet := false
62         for _, line := range lines {
63                 if line == "" {
64                         if emptyMet {
65                                 continue
66                         }
67                         emptyMet = true
68                 } else {
69                         emptyMet = false
70                 }
71                 withoutDups = append(withoutDups, line)
72         }
73         return withoutDups
74 }
75
76 func main() {
77         gitCmd := flag.String("git-cmd", "/usr/local/bin/git", "Path to git executable")
78         gitDir := flag.String("git-dir", "", "Path to .git repository")
79         notesRef := flag.String("ref", "comments", "notes reference name")
80         umask := flag.String("umask", "027", "umask value")
81         dryRun := flag.Bool("dryrun", false, "Show comment, do not add")
82         committerEmail := flag.String(
83                 "committer-email",
84                 "comment@blog.example.com",
85                 "Git committer's email",
86         )
87         flag.Parse()
88         uid := syscall.Geteuid()
89         if err := syscall.Setuid(uid); err != nil {
90                 log.Fatal(err)
91         }
92         umaskInt, err := strconv.ParseUint(*umask, 8, 16)
93         if err != nil {
94                 panic(err)
95         }
96         syscall.Umask(int(umaskInt))
97
98         msg, err := mail.ReadMessage(os.Stdin)
99         if err != nil {
100                 log.Fatal(err)
101         }
102         subj, r, err := parseEmail(msg)
103         if err != nil {
104                 log.Fatal(err)
105         }
106         body, err := ioutil.ReadAll(r)
107         if err != nil {
108                 log.Fatal(err)
109         }
110         from := msg.Header.Get("From")
111         if from == "" {
112                 log.Fatal("From is missing")
113         }
114         if len(body) == 0 {
115                 log.Fatal("no body")
116         }
117         from, err = new(mime.WordDecoder).DecodeHeader(from)
118         if err != nil {
119                 log.Fatal(err)
120         }
121
122         subj = hashFinder.FindString(subj)
123         if subj == "" {
124                 log.Fatal("no commit hash found in subject")
125         }
126         if h, err := hex.DecodeString(subj); err != nil || len(h) != sha1.Size {
127                 os.Exit(0)
128         }
129         fromCols := strings.Fields(from)
130         if len(fromCols) == 1 {
131                 if idx := strings.Index(from, "@"); idx != -1 {
132                         from = strings.Trim(from[:idx], "<>")
133                 }
134         } else {
135                 from = strings.Join(fromCols[:len(fromCols)-1], " ")
136         }
137
138         cmd := exec.Command(
139                 *gitCmd, "--git-dir", *gitDir,
140                 "notes", "--ref", *notesRef, "show", subj,
141         )
142         note, _ := cmd.Output()
143         note = bytes.TrimRight(note, "\r\n")
144
145         buf := bytes.NewBuffer(note)
146         recfileW := recfile.NewWriter(buf)
147         if _, err = recfileW.RecordStart(); err != nil {
148                 log.Fatal(err)
149         }
150         // We trimmed newline, so have to start record twice
151         if _, err = recfileW.RecordStart(); err != nil {
152                 log.Fatal(err)
153         }
154         if _, err = recfileW.WriteFields(
155                 recfile.Field{Name: "From", Value: from},
156                 recfile.Field{Name: "Date", Value: time.Now().UTC().Format(sgblog.WhenFmt)},
157         ); err != nil {
158                 log.Fatal(err)
159         }
160         if _, err = recfileW.WriteFieldMultiline(
161                 "Body", append([]string{""}, cleanupBody(string(body))...),
162         ); err != nil {
163                 log.Fatal(err)
164         }
165
166         if *dryRun {
167                 fmt.Print(buf.String())
168                 os.Exit(0)
169         }
170
171         cmd = exec.Command(
172                 *gitCmd, "--git-dir", *gitDir,
173                 "notes", "--ref", *notesRef, "add",
174                 "-F", "-", "-f", subj,
175         )
176         cmd.Env = append(
177                 cmd.Env,
178                 "GIT_AUTHOR_NAME=SGBlog "+sgblog.Version,
179                 "GIT_AUTHOR_EMAIL="+*committerEmail,
180                 "GIT_COMMITTER_NAME=SGBlog "+sgblog.Version,
181                 "GIT_COMMITTER_EMAIL="+*committerEmail,
182         )
183         cmd.Stdin = buf
184         if err = cmd.Run(); err != nil {
185                 log.Fatal(err)
186         }
187 }