1 // SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
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.
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.
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/>.
16 // Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
37 "go.cypherpunks.ru/recfile"
38 "go.stargrave.org/sgblog"
41 var hashFinder = regexp.MustCompile("([0-9a-f]{40})")
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")
55 for lines[len(lines)-1] == "" {
56 lines = lines[:len(lines)-1]
58 withoutDups := make([]string, 0, len(lines))
60 for _, line := range lines {
69 withoutDups = append(withoutDups, line)
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(
82 "comment@blog.example.com",
83 "Git committer's email",
86 uid := syscall.Geteuid()
87 if err := syscall.Setuid(uid); err != nil {
90 umaskInt, err := strconv.ParseUint(*umask, 8, 16)
94 syscall.Umask(int(umaskInt))
96 data, err := io.ReadAll(os.Stdin)
100 if bytes.HasPrefix(data, []byte("From ")) {
101 data = data[bytes.Index(data, []byte("\n"))+1:]
103 msg, err := mail.ReadMessage(bytes.NewReader(data))
107 subj, r, err := parseEmail(msg)
111 body, err := io.ReadAll(r)
115 from := msg.Header.Get("From")
117 log.Fatal("From is missing")
122 from, err = new(mime.WordDecoder).DecodeHeader(from)
127 subj = hashFinder.FindString(subj)
129 log.Fatal("no commit hash found in subject")
131 if h, err := hex.DecodeString(subj); err != nil || len(h) != sha1.Size {
134 fromCols := strings.Fields(from)
135 if len(fromCols) == 1 {
136 if idx := strings.Index(from, "@"); idx != -1 {
137 from = strings.Trim(from[:idx], "<>")
140 from = strings.Join(fromCols[:len(fromCols)-1], " ")
144 *gitCmd, "--git-dir", *gitDir,
145 "notes", "--ref", *notesRef, "show", subj,
147 note, _ := cmd.Output()
148 note = bytes.TrimRight(note, "\r\n")
150 buf := bytes.NewBuffer(note)
151 recfileW := recfile.NewWriter(buf)
152 if _, err = recfileW.RecordStart(); err != nil {
155 // We trimmed newline, so have to start record twice
156 if _, err = recfileW.RecordStart(); err != nil {
159 if _, err = recfileW.WriteFields(
160 recfile.Field{Name: "From", Value: from},
161 recfile.Field{Name: "Date", Value: time.Now().UTC().Format(sgblog.WhenFmt)},
165 if _, err = recfileW.WriteFieldMultiline(
166 "Body", append([]string{""}, cleanupBody(string(body))...),
172 fmt.Print(buf.String())
177 *gitCmd, "--git-dir", *gitDir,
178 "notes", "--ref", *notesRef, "add",
179 "-F", "-", "-f", subj,
183 "GIT_AUTHOR_NAME=SGBlog "+sgblog.Version,
184 "GIT_AUTHOR_EMAIL="+*committerEmail,
185 "GIT_COMMITTER_NAME=SGBlog "+sgblog.Version,
186 "GIT_COMMITTER_EMAIL="+*committerEmail,
189 if err = cmd.Run(); err != nil {