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