- commit
- 46ea9e7
- parent
- 59a4a00
- author
- Eric Bower
- date
- 2024-06-04 13:10:09 -0400 EDT
feat: rss feeds
M
cfg.go
+2,
-0
1@@ -12,11 +12,13 @@ type GitCfg struct {
2 DataPath string
3 Admins []ssh.PublicKey
4 Repos []Repo
5+ Url string
6 }
7
8 func NewGitCfg() *GitCfg {
9 return &GitCfg{
10 DataPath: "./ssh_data",
11+ Url: "pr.pico.sh",
12 Repos: []Repo{
13 {
14 ID: "test",
M
cli.go
+58,
-4
1@@ -89,6 +89,60 @@ Here's how it works:
2 return nil
3 },
4 },
5+ {
6+ Name: "logs",
7+ Usage: "List event logs by on filters",
8+ Args: true,
9+ Flags: []cli.Flag{
10+ &cli.Int64Flag{
11+ Name: "pr",
12+ Usage: "show all events related to the provided patch request",
13+ },
14+ &cli.BoolFlag{
15+ Name: "pubkey",
16+ Usage: "show all events related to your pubkey",
17+ },
18+ &cli.StringFlag{
19+ Name: "repo",
20+ Usage: "show all events related to a repo",
21+ },
22+ },
23+ Action: func(cCtx *cli.Context) error {
24+ pubkey := be.Pubkey(sesh.PublicKey())
25+ isPubkey := cCtx.Bool("pubkey")
26+ prID := cCtx.Int64("pr")
27+ repoID := cCtx.String("repo")
28+ var eventLogs []*EventLog
29+ var err error
30+ if isPubkey {
31+ eventLogs, err = pr.GetEventLogsByPubkey(pubkey)
32+ } else if prID != 0 {
33+ eventLogs, err = pr.GetEventLogsByPrID(prID)
34+ } else if repoID != "" {
35+ eventLogs, err = pr.GetEventLogsByRepoID(repoID)
36+ } else {
37+ eventLogs, err = pr.GetEventLogs()
38+ }
39+ if err != nil {
40+ return err
41+ }
42+ writer := NewTabWriter(sesh)
43+ fmt.Fprintln(writer, "RepoID\tPrID\tEvent\tCreated\tData")
44+ for _, eventLog := range eventLogs {
45+ fmt.Fprintf(
46+ writer,
47+ "%s\t%d\t%s\t%s\t%s\n",
48+ eventLog.RepoID,
49+ eventLog.PatchRequestID,
50+ eventLog.Event,
51+ eventLog.CreatedAt.Format(time.RFC3339Nano),
52+ eventLog.Data,
53+ )
54+ }
55+ writer.Flush()
56+ return nil
57+ },
58+ },
59 {
60 Name: "pr",
61 Usage: "Manage Patch Requests (PR)",
62@@ -300,7 +354,7 @@ Here's how it works:
63 if !isAdmin {
64 return fmt.Errorf("you are not authorized to accept a PR")
65 }
66- err = pr.UpdatePatchRequest(prID, "accept")
67+ err = pr.UpdatePatchRequest(prID, pubkey, "accepted")
68 return err
69 },
70 },
71@@ -324,7 +378,7 @@ Here's how it works:
72 if !isAdmin && !isContrib {
73 return fmt.Errorf("you are not authorized to change PR status")
74 }
75- err = pr.UpdatePatchRequest(prID, "closed")
76+ err = pr.UpdatePatchRequest(prID, pubkey, "closed")
77 return err
78 },
79 },
80@@ -349,7 +403,7 @@ Here's how it works:
81 return fmt.Errorf("you are not authorized to change PR status")
82 }
83
84- err = pr.UpdatePatchRequest(prID, "open")
85+ err = pr.UpdatePatchRequest(prID, pubkey, "open")
86 return err
87 },
88 },
89@@ -405,7 +459,7 @@ Here's how it works:
90
91 reviewTxt := ""
92 if isReview {
93- err = pr.UpdatePatchRequest(prID, "reviewed")
94+ err = pr.UpdatePatchRequest(prID, pubkey, "reviewed")
95 if err != nil {
96 return err
97 }
M
db.go
+29,
-1
1@@ -49,7 +49,16 @@ type Comment struct {
2 UpdatedAt time.Time `db:"updated_at"`
3 }
4
5-type GitDB interface {
6+// EventLog is a event log for RSS or other notification systems.
7+type EventLog struct {
8+ ID int64 `db:"id"`
9+ Pubkey string `db:"pubkey"`
10+ RepoID string `db:"repo_id"`
11+ PatchRequestID int64 `db:"patch_request_id"`
12+ CommentID int64 `db:"comment_id"`
13+ Event string `db:"event"`
14+ Data string `db:"data"`
15+ CreatedAt time.Time `db:"created_at"`
16 }
17
18 // DB is the interface for a pico/git database.
19@@ -103,6 +112,25 @@ CREATE TABLE IF NOT EXISTS comments (
20 ON DELETE CASCADE
21 ON UPDATE CASCADE
22 );
23+
24+CREATE TABLE IF NOT EXISTS event_logs (
25+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26+ pubkey TEXT NOT NULL,
27+ repo_id TEXT,
28+ patch_request_id INTEGER,
29+ comment_id INTEGER,
30+ event TEXT NOT NULL,
31+ data TEXT,
32+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
33+ CONSTRAINT event_logs_pr_id_fk
34+ FOREIGN KEY(patch_request_id) REFERENCES patch_requests(id)
35+ ON DELETE CASCADE
36+ ON UPDATE CASCADE,
37+ CONSTRAINT event_logs_comment_id_fk
38+ FOREIGN KEY(comment_id) REFERENCES comments(id)
39+ ON DELETE CASCADE
40+ ON UPDATE CASCADE
41+);
42 `
43
44 // Open opens a database connection.
M
go.mod
+1,
-0
1@@ -8,6 +8,7 @@ require (
2 github.com/charmbracelet/soft-serve v0.7.4
3 github.com/charmbracelet/ssh v0.0.0-20240301204039-e79ff702f5b3
4 github.com/charmbracelet/wish v1.3.2
5+ github.com/gorilla/feeds v1.1.2
6 github.com/jmoiron/sqlx v1.3.5
7 github.com/urfave/cli/v2 v2.27.2
8 golang.org/x/crypto v0.21.0
M
go.sum
+8,
-0
1@@ -50,12 +50,18 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
2 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
3 github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
4 github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5+github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
6+github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
7 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
8 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
9 github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
10 github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
11 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
13+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
14+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
15+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
16+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
17 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
18 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
19 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
20@@ -87,6 +93,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
21 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
22 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
23 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
24+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
25+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
26 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
27 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
28 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
M
pr.go
+117,
-39
1@@ -32,8 +32,13 @@ type GitPatchRequest interface {
2 GetPatchRequests() ([]*PatchRequest, error)
3 GetPatchRequestsByRepoID(repoID string) ([]*PatchRequest, error)
4 GetPatchesByPrID(prID int64) ([]*Patch, error)
5- UpdatePatchRequest(prID int64, status string) error
6+ UpdatePatchRequest(prID int64, pubkey, status string) error
7 DeletePatchesByPrID(prID int64) error
8+ CreateEventLog(eventLog EventLog) error
9+ GetEventLogs() ([]*EventLog, error)
10+ GetEventLogsByRepoID(repoID string) ([]*EventLog, error)
11+ GetEventLogsByPrID(prID int64) ([]*EventLog, error)
12+ GetEventLogsByPubkey(pubkey string) ([]*EventLog, error)
13 }
14
15 type PrCmd struct {
16@@ -107,11 +112,55 @@ func (cmd PrCmd) GetPatchRequestByID(prID int64) (*PatchRequest, error) {
17 return &pr, err
18 }
19
20-// Status types: open, close, accept, review.
21-func (cmd PrCmd) UpdatePatchRequest(prID int64, status string) error {
22+// Status types: open, closed, accepted, reviewed.
23+func (cmd PrCmd) UpdatePatchRequest(prID int64, pubkey string, status string) error {
24 _, err := cmd.Backend.DB.Exec(
25- "UPDATE patch_requests SET status=? WHERE id=?", status, prID,
26+ "UPDATE patch_requests SET status=? WHERE id=?",
27+ status,
28+ prID,
29+ )
30+ _ = cmd.CreateEventLog(EventLog{
31+ Pubkey: pubkey,
32+ PatchRequestID: prID,
33+ Event: "pr_status_changed",
34+ Data: fmt.Sprintf(`{"status":"%s"}`, status),
35+ })
36+ return err
37+}
38+
39+func (cmd PrCmd) CreateEventLog(eventLog EventLog) error {
40+ if eventLog.RepoID == "" && eventLog.PatchRequestID != 0 {
41+ var pr PatchRequest
42+ err := cmd.Backend.DB.Get(
43+ &pr,
44+ "SELECT repo_id FROM patch_requests WHERE id=?",
45+ eventLog.PatchRequestID,
46+ )
47+ if err != nil {
48+ cmd.Backend.Logger.Error(
49+ "could not find pr when creating eventLog",
50+ "err", err,
51+ )
52+ return nil
53+ }
54+ eventLog.RepoID = pr.RepoID
55+ }
56+
57+ _, err := cmd.Backend.DB.Exec(
58+ "INSERT INTO event_logs (pubkey, repo_id, patch_request_id, comment_id, event, data) VALUES (?, ?, ?, ?, ?, ?)",
59+ eventLog.Pubkey,
60+ eventLog.RepoID,
61+ eventLog.PatchRequestID,
62+ eventLog.CommentID,
63+ eventLog.Event,
64+ eventLog.Data,
65 )
66+ if err != nil {
67+ cmd.Backend.Logger.Error(
68+ "could not create eventLog",
69+ "err", err,
70+ )
71+ }
72 return err
73 }
74
75@@ -199,7 +248,7 @@ func (cmd PrCmd) parsePatchSet(patchset io.Reader) ([]*Patch, error) {
76
77 func (cmd PrCmd) createPatch(tx *sqlx.Tx, review bool, patch *Patch) (int64, error) {
78 patchExists := []Patch{}
79- _ = cmd.Backend.DB.Select(&patchExists, "SELECT * FROM patches WHERE patch_request_id = ? AND content_sha = ?", patch.PatchRequestID, patch.ContentSha)
80+ _ = cmd.Backend.DB.Select(&patchExists, "SELECT * FROM patches WHERE patch_request_id=? AND content_sha=?", patch.PatchRequestID, patch.ContentSha)
81 if len(patchExists) > 0 {
82 return 0, ErrPatchExists
83 }
84@@ -283,6 +332,12 @@ func (cmd PrCmd) SubmitPatchRequest(repoID string, pubkey string, patchset io.Re
85 return nil, err
86 }
87
88+ _ = cmd.CreateEventLog(EventLog{
89+ Pubkey: pubkey,
90+ PatchRequestID: prID,
91+ Event: "pr_created",
92+ })
93+
94 var pr PatchRequest
95 err = cmd.Backend.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
96 return &pr, err
97@@ -330,42 +385,19 @@ func (cmd PrCmd) SubmitPatchSet(prID int64, pubkey string, op PatchsetOp, patchs
98 return fin, err
99 }
100
101- return fin, err
102-}
103-
104-func (cmd PrCmd) ReplacePatchSet(prID int64, pubkey string, patchset io.Reader) ([]*Patch, error) {
105- fin := []*Patch{}
106- tx, err := cmd.Backend.DB.Beginx()
107- if err != nil {
108- return fin, err
109- }
110-
111- defer func() {
112- _ = tx.Rollback()
113- }()
114-
115- patches, err := cmd.parsePatchSet(patchset)
116- if err != nil {
117- return fin, err
118- }
119-
120- for _, patch := range patches {
121- patch.Pubkey = pubkey
122- patch.PatchRequestID = prID
123- patchID, err := cmd.createPatch(tx, false, patch)
124- if err == nil {
125- patch.ID = patchID
126- fin = append(fin, patch)
127- } else {
128- if !errors.Is(ErrPatchExists, err) {
129- return fin, err
130- }
131+ if len(fin) > 0 {
132+ event := "pr_patchset_added"
133+ if op == OpReview {
134+ event = "pr_reviewed"
135+ } else if op == OpReplace {
136+ event = "pr_patchset_replaced"
137 }
138- }
139
140- err = tx.Commit()
141- if err != nil {
142- return fin, err
143+ _ = cmd.CreateEventLog(EventLog{
144+ Pubkey: pubkey,
145+ PatchRequestID: prID,
146+ Event: event,
147+ })
148 }
149
150 return fin, err
151@@ -377,3 +409,49 @@ func (cmd PrCmd) DeletePatchesByPrID(prID int64) error {
152 )
153 return err
154 }
155+
156+func (cmd PrCmd) GetEventLogs() ([]*EventLog, error) {
157+ eventLogs := []*EventLog{}
158+ err := cmd.Backend.DB.Select(
159+ &eventLogs,
160+ "SELECT * FROM event_logs ORDER BY created_at DESC",
161+ )
162+ return eventLogs, err
163+}
164+
165+func (cmd PrCmd) GetEventLogsByRepoID(repoID string) ([]*EventLog, error) {
166+ eventLogs := []*EventLog{}
167+ err := cmd.Backend.DB.Select(
168+ &eventLogs,
169+ "SELECT * FROM event_logs WHERE repo_id=? ORDER BY created_at DESC",
170+ repoID,
171+ )
172+ return eventLogs, err
173+}
174+
175+func (cmd PrCmd) GetEventLogsByPrID(prID int64) ([]*EventLog, error) {
176+ eventLogs := []*EventLog{}
177+ err := cmd.Backend.DB.Select(
178+ &eventLogs,
179+ "SELECT * FROM event_logs WHERE patch_request_id=? ORDER BY created_at DESC",
180+ prID,
181+ )
182+ return eventLogs, err
183+}
184+
185+func (cmd PrCmd) GetEventLogsByPubkey(pubkey string) ([]*EventLog, error) {
186+ eventLogs := []*EventLog{}
187+ query := `SELECT * FROM event_logs
188+ WHERE pubkey=?
189+ OR patch_request_id IN (
190+ SELECT id FROM patch_requests WHERE pubkey=?
191+ )
192+ ORDER BY created_at DESC`
193+ err := cmd.Backend.DB.Select(
194+ &eventLogs,
195+ query,
196+ pubkey,
197+ pubkey,
198+ )
199+ return eventLogs, err
200+}
+9,
-0
1@@ -3,6 +3,9 @@
2 {{define "title"}}{{.Pr.Title}} - pr patches{{end}}
3
4 {{define "meta"}}
5+<link rel="alternate" type="application/atom+xml"
6+ title="RSS feed for git collaboration server"
7+ href="/prs/{{.Pr.ID}}/rss" />
8 {{end}}
9
10 {{define "body"}}
11@@ -24,4 +27,10 @@
12 </div>
13 <hr />
14 </main>
15+
16+<hr />
17+
18+<footer>
19+ <a href="/prs/{{.Pr.ID}}/rss">rss</a>
20+</footer>
21 {{end}}
+9,
-0
1@@ -3,6 +3,9 @@
2 {{define "title"}}{{.Pr.Title}} - pr summary{{end}}
3
4 {{define "meta"}}
5+<link rel="alternate" type="application/atom+xml"
6+ title="RSS feed for git collaboration server"
7+ href="/prs/{{.Pr.ID}}/rss" />
8 {{end}}
9
10 {{define "body"}}
11@@ -32,4 +35,10 @@
12 {{end}}
13 </div>
14 </main>
15+
16+<hr />
17+
18+<footer>
19+ <a href="/prs/{{.Pr.ID}}/rss">rss</a>
20+</footer>
21 {{end}}
+9,
-0
1@@ -3,6 +3,9 @@
2 {{define "title"}}{{.ID}} - repo{{end}}
3
4 {{define "meta"}}
5+<link rel="alternate" type="application/atom+xml"
6+ title="RSS feed for git collaboration server"
7+ href="/repos/{{.ID}}/rss" />
8 {{end}}
9
10 {{define "body"}}
11@@ -29,4 +32,10 @@
12 </article>
13 {{end}}
14 </main>
15+
16+<hr />
17+
18+<footer>
19+ <a href="/repos/{{.ID}}/rss">rss</a>
20+</footer>
21 {{end}}
+12,
-5
1@@ -3,19 +3,19 @@
2 {{define "title"}}repo list{{end}}
3
4 {{define "meta"}}
5+<link rel="alternate" type="application/atom+xml"
6+ title="RSS feed for git collaboration server"
7+ href="/rss" />
8 {{end}}
9
10 {{define "body"}}
11 <header>
12 <h1 class="text-2xl mb">Repos</h1>
13- <p>A new git collaboration service</p>
14+ <p>A new git collaboration service.</p>
15 </header>
16 <main>
17 <div class="group">
18- <div>
19- <div>Learn how it works:</div>
20- <div><pre>ssh pr.pico.sh help</pre></div>
21- </div>
22+ <pre>ssh pr.pico.sh help</pre>
23
24 {{range .Repos}}
25 <div class="box">
26@@ -25,4 +25,11 @@
27 {{end}}
28 </div>
29 </main>
30+
31+<hr />
32+
33+<footer>
34+ <div><a href="/rss">rss</a></div>
35+ <div class="text-sm">OR with fingerprint, url encoded: <code>/rss?pubkey=SHA256%3Axxx</code></div>
36+</footer>
37 {{end}}
M
web.go
+98,
-0
1@@ -17,6 +17,7 @@ import (
2 formatterHtml "github.com/alecthomas/chroma/v2/formatters/html"
3 "github.com/alecthomas/chroma/v2/lexers"
4 "github.com/alecthomas/chroma/v2/styles"
5+ "github.com/gorilla/feeds"
6 )
7
8 //go:embed tmpl/*
9@@ -353,6 +354,100 @@ func prPatchesHandler(w http.ResponseWriter, r *http.Request) {
10 }
11 }
12
13+func rssHandler(w http.ResponseWriter, r *http.Request) {
14+ web, err := getWebCtx(r)
15+ if err != nil {
16+ w.WriteHeader(http.StatusUnprocessableEntity)
17+ return
18+ }
19+
20+ desc := fmt.Sprintf(
21+ "Events related to git collaboration server %s",
22+ web.Backend.Cfg.Url,
23+ )
24+ feed := &feeds.Feed{
25+ Title: fmt.Sprintf("%s events", web.Backend.Cfg.Url),
26+ Link: &feeds.Link{Href: web.Backend.Cfg.Url},
27+ Description: desc,
28+ Author: &feeds.Author{Name: "git collaboration server"},
29+ Created: time.Now(),
30+ }
31+
32+ var eventLogs []*EventLog
33+ id := r.PathValue("id")
34+ repoID := r.PathValue("repoid")
35+ pubkey := r.URL.Query().Get("pubkey")
36+ if id != "" {
37+ var prID int64
38+ prID, err = getPrID(id)
39+ if err != nil {
40+ w.WriteHeader(http.StatusUnprocessableEntity)
41+ return
42+ }
43+ eventLogs, err = web.Pr.GetEventLogsByPrID(prID)
44+ } else if pubkey != "" {
45+ eventLogs, err = web.Pr.GetEventLogsByPubkey(pubkey)
46+ } else if repoID != "" {
47+ eventLogs, err = web.Pr.GetEventLogsByRepoID(repoID)
48+ } else {
49+ eventLogs, err = web.Pr.GetEventLogs()
50+ }
51+
52+ if err != nil {
53+ web.Logger.Error("rss could not get eventLogs", "err", err)
54+ w.WriteHeader(http.StatusInternalServerError)
55+ return
56+ }
57+
58+ var feedItems []*feeds.Item
59+ for _, eventLog := range eventLogs {
60+ realUrl := fmt.Sprintf("%s/prs/%d", web.Backend.Cfg.Url, eventLog.PatchRequestID)
61+ content := fmt.Sprintf(
62+ "<div><div>RepoID: %s</div><div>PatchRequestID: %d</div><div>Event: %s</div><div>Created: %s</div><div>Data: %s</div></div>",
63+ eventLog.RepoID,
64+ eventLog.PatchRequestID,
65+ eventLog.Event,
66+ eventLog.CreatedAt.Format(time.RFC3339Nano),
67+ eventLog.Data,
68+ )
69+ pr, err := web.Pr.GetPatchRequestByID(eventLog.PatchRequestID)
70+ if err != nil {
71+ continue
72+ }
73+ title := fmt.Sprintf(
74+ `%s in %s for PR "%s" (#%d)`,
75+ eventLog.Event,
76+ eventLog.RepoID,
77+ pr.Name,
78+ eventLog.PatchRequestID,
79+ )
80+ item := &feeds.Item{
81+ Id: fmt.Sprintf("%d", eventLog.ID),
82+ Title: title,
83+ Link: &feeds.Link{Href: realUrl},
84+ Content: content,
85+ Created: eventLog.CreatedAt,
86+ Description: title,
87+ Author: &feeds.Author{Name: eventLog.Pubkey},
88+ }
89+
90+ feedItems = append(feedItems, item)
91+ }
92+ feed.Items = feedItems
93+
94+ rss, err := feed.ToAtom()
95+ if err != nil {
96+ web.Logger.Error("could not generate atom rss feed", "err", err)
97+ http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
98+ }
99+
100+ w.Header().Add("Content-Type", "application/atom+xml; charset=utf-8")
101+ _, err = w.Write([]byte(rss))
102+ if err != nil {
103+ web.Logger.Error("write error atom rss feed", "err", err)
104+ }
105+}
106+
107 func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
108 web, err := getWebCtx(r)
109 if err != nil {
110@@ -412,9 +507,12 @@ func StartWebServer() {
111 // GODEBUG=httpmuxgo121=0
112 http.HandleFunc("GET /prs/{id}/patches", ctxMdw(ctx, prPatchesHandler))
113 http.HandleFunc("GET /prs/{id}", ctxMdw(ctx, prHandler))
114+ http.HandleFunc("GET /prs/{id}/rss", ctxMdw(ctx, rssHandler))
115 http.HandleFunc("GET /repos/{id}", ctxMdw(ctx, repoHandler))
116+ http.HandleFunc("GET /repos/{repoid}/rss", ctxMdw(ctx, rssHandler))
117 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
118 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
119+ http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
120
121 logger.Info("starting web server", "addr", addr)
122 err = http.ListenAndServe(addr, nil)