A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 35cb93f6f1

35cb93f6f17f984193c93df7ff9278f5bb662ffa

parent: 542b37e732

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24T23:06:53Z

Commit-reference entries become system messages with linked shas

Per cmc's feedback on the #31 behavior seen in #10: automated
close/reference entries were rendering as comments from the pusher.
issue_comments and mr_comments gain a kind column (migration 0019);
commit-message actions now write kind=system entries whose author reads
'system' everywhere (the acting user is kept in the row and the audit
log for provenance), the sha is a markdown link to the commit page, and
the web renders them as muted informational lines instead of comment
cards. Comment author names on issue/MR pages are links now too.
e2e/commitrefs_test.go +8
@@ -42,6 +42,14 @@ func TestCommitMessageIssueActions(t *testing.T) {
4242 if !strings.Contains(out, `"state":"closed"`) || !strings.Contains(out, "closed by commit") {
4343 t.Fatalf("issue 1 not closed by commit: %s", out)
4444 }
45 // The entry is a system message with a linked sha, not a user comment.
46 if !strings.Contains(out, `"author":"system"`) || !strings.Contains(out, "](/alice/app/commit/") {
47 t.Fatalf("close entry not a linked system message: %s", out)
48 }
49 if status, body := inst.get(t, "/alice/app/issues/1"); status != 200 ||
50 !strings.Contains(body, `class="syscomment"`) || !strings.Contains(body, `/alice/app/commit/`) {
51 t.Fatalf("web system message: %d", status)
52 }
4553 out, _, _ = inst.ssh(t, aliceKey, "", "issue", "show", "alice/app", "2", "--json")
4654 if !strings.Contains(out, `"state":"open"`) || !strings.Contains(out, "referenced in commit") ||
4755 !strings.Contains(out, "repair the widget") {
internal/control/commitrefs.go +5 −2
@@ -69,14 +69,17 @@ func actOnIssue(st *store.Store, repo store.Repo, actorID int64, sha string, num
6969 if len(short) > 10 {
7070 short = short[:10]
7171 }
72 // Informational system entries, not comments from the pusher; the
73 // linked sha renders clickable on the web.
74 link := fmt.Sprintf("[%s](/%s/commit/%s)", short, repo.Path(), sha)
7275 if close && issue.State == "open" {
7376 if err := st.SetIssueState(issue.ID, "closed"); err != nil {
7477 slog.Error("commit refs: closing issue", "issue", number, "err", err)
7578 return
7679 }
77 st.AddIssueComment(issue.ID, actorID, fmt.Sprintf("closed by commit %s: %s", short, subject))
80 st.AddIssueSystemComment(issue.ID, actorID, fmt.Sprintf("closed by commit %s: %s", link, subject))
7881 st.RecordEvent(repo.ID, actorID, "issue.closed", fmt.Sprintf(`{"number":%d,"sha":%q}`, number, sha))
7982 return
8083 }
81 st.AddIssueComment(issue.ID, actorID, fmt.Sprintf("referenced in commit %s: %s", short, subject))
84 st.AddIssueSystemComment(issue.ID, actorID, fmt.Sprintf("referenced in commit %s: %s", link, subject))
8285 }
internal/httpd/web.go +4 −3
@@ -815,13 +815,14 @@ func (s *Server) ugcFor(r *http.Request, repo store.Repo) func(string) template.
815815 type renderedComment struct {
816816 Author string
817817 CreatedAt string
818 Kind string
818819 BodyHTML template.HTML
819820 }
820821
821822 func renderComments(cs []store.IssueComment, md func(string) template.HTML) []renderedComment {
822823 var out []renderedComment
823824 for _, c := range cs {
824 out = append(out, renderedComment{c.Author, c.CreatedAt, md(c.Body)})
825 out = append(out, renderedComment{c.Author, c.CreatedAt, c.Kind, md(c.Body)})
825826 }
826827 return out
827828 }
@@ -930,11 +931,11 @@ func attachThreads(lines []diffLine, comments []store.DiffComment, headSHA strin
930931 for _, cm := range comments {
931932 if cm.ReplyTo == 0 {
932933 threads[cm.ID] = &diffThread{ID: cm.ID, Resolved: cm.ResolvedBy, Stale: cm.HeadSHA != headSHA,
933 Comments: []renderedComment{{cm.Author, cm.CreatedAt, md(cm.Body)}}}
934 Comments: []renderedComment{{Author: cm.Author, CreatedAt: cm.CreatedAt, BodyHTML: md(cm.Body)}}}
934935 anchors[cm.ID] = anchor{cm.Path, cm.Side, cm.Line}
935936 order = append(order, cm.ID)
936937 } else if th, ok := threads[cm.ReplyTo]; ok {
937 th.Comments = append(th.Comments, renderedComment{cm.Author, cm.CreatedAt, md(cm.Body)})
938 th.Comments = append(th.Comments, renderedComment{Author: cm.Author, CreatedAt: cm.CreatedAt, BodyHTML: md(cm.Body)})
938939 }
939940 }
940941 placed := map[int64]bool{}
internal/store/issues.go +14 −2
@@ -25,6 +25,7 @@ type IssueComment struct {
2525 Author string
2626 Body string
2727 CreatedAt string
28 Kind string // comment | system
2829 }
2930
3031 // CreateIssue allocates the per-repo number from the repo counter inside the
@@ -155,7 +156,8 @@ func (s *Store) AddIssueComment(issueID, authorID int64, body string) error {
155156
156157 func (s *Store) ListIssueComments(issueID int64) ([]IssueComment, error) {
157158 rows, err := s.DB.Query(`
158 SELECT u.username, c.body, c.created_at
159 SELECT CASE WHEN c.kind = 'system' THEN 'system' ELSE u.username END,
160 c.body, c.created_at, c.kind
159161 FROM issue_comments c JOIN users u ON u.id = c.author_id
160162 WHERE c.issue_id = ? ORDER BY c.id`, issueID)
161163 if err != nil {
@@ -165,7 +167,7 @@ func (s *Store) ListIssueComments(issueID int64) ([]IssueComment, error) {
165167 var out []IssueComment
166168 for rows.Next() {
167169 var c IssueComment
168 if err := rows.Scan(&c.Author, &c.Body, &c.CreatedAt); err != nil {
170 if err := rows.Scan(&c.Author, &c.Body, &c.CreatedAt, &c.Kind); err != nil {
169171 return nil, err
170172 }
171173 out = append(out, c)
@@ -173,6 +175,16 @@ func (s *Store) ListIssueComments(issueID int64) ([]IssueComment, error) {
173175 return out, rows.Err()
174176 }
175177
178// AddIssueSystemComment records an informational entry (commit references,
179// automated closes). The actor is kept for provenance but the entry
180// displays as coming from the system, not the user.
181func (s *Store) AddIssueSystemComment(issueID, actorID int64, body string) error {
182 _, err := s.DB.Exec(
183 "INSERT INTO issue_comments (issue_id, author_id, body, kind) VALUES (?, ?, ?, 'system')",
184 issueID, actorID, body)
185 return err
186}
187
176188 // ListIssueLabels returns the label names attached to each issue of a
177189 // repo, keyed by issue id. Used by the web issue listing; ListIssues
178190 // itself stays label-free for the CLI's lean list output.
internal/store/migrations/0019_comment_kind.down.sql added +2
@@ -0,0 +1,2 @@
1ALTER TABLE issue_comments DROP COLUMN kind;
2ALTER TABLE mr_comments DROP COLUMN kind;
internal/store/migrations/0019_comment_kind.up.sql added +2
@@ -0,0 +1,2 @@
1ALTER TABLE issue_comments ADD COLUMN kind TEXT NOT NULL DEFAULT 'comment';
2ALTER TABLE mr_comments ADD COLUMN kind TEXT NOT NULL DEFAULT 'comment';
internal/store/mrs.go +11 −2
@@ -185,9 +185,18 @@ func (s *Store) AddMRComment(mrID, authorID int64, body string) error {
185185 return err
186186 }
187187
188// AddMRSystemComment is the informational counterpart of AddMRComment.
189func (s *Store) AddMRSystemComment(mrID, actorID int64, body string) error {
190 _, err := s.DB.Exec(
191 "INSERT INTO mr_comments (mr_id, author_id, body, kind) VALUES (?, ?, ?, 'system')",
192 mrID, actorID, body)
193 return err
194}
195
188196 func (s *Store) ListMRComments(mrID int64) ([]IssueComment, error) {
189197 rows, err := s.DB.Query(`
190 SELECT u.username, c.body, c.created_at
198 SELECT CASE WHEN c.kind = 'system' THEN 'system' ELSE u.username END,
199 c.body, c.created_at, c.kind
191200 FROM mr_comments c JOIN users u ON u.id = c.author_id
192201 WHERE c.mr_id = ? ORDER BY c.id`, mrID)
193202 if err != nil {
@@ -197,7 +206,7 @@ func (s *Store) ListMRComments(mrID int64) ([]IssueComment, error) {
197206 var out []IssueComment
198207 for rows.Next() {
199208 var c IssueComment
200 if err := rows.Scan(&c.Author, &c.Body, &c.CreatedAt); err != nil {
209 if err := rows.Scan(&c.Author, &c.Body, &c.CreatedAt, &c.Kind); err != nil {
201210 return nil, err
202211 }
203212 out = append(out, c)
internal/web/static/style.css +9
@@ -616,6 +616,15 @@ article.comment .rendered { padding: 0 var(--sp-4); }
616616 article.comment .rendered > :first-child { margin-top: var(--sp-3); }
617617 article.comment .rendered > :last-child { margin-bottom: var(--sp-3); }
618618 form.commentform { margin: var(--sp-4) 0; }
619.syscomment {
620 max-width: 48rem;
621 color: var(--muted);
622 font-size: var(--fs-2);
623 margin: var(--sp-3) 0;
624 padding-left: var(--sp-4);
625 border-left: 3px solid var(--faint);
626}
627.syscomment p { display: inline; margin: 0; }
619628
620629 /* review threads under diff lines */
621630 .thread {
internal/web/templates/issue.html +4 −3
@@ -12,10 +12,11 @@
1212 <div class="rendered">{{.BodyHTML}}</div>
1313 </article>{{end}}
1414 {{range .Comments}}
15<article class="comment">
16 <header class="commenthead"><strong>{{.Author}}</strong> <span class="when">{{when .CreatedAt}}</span></header>
15{{if eq .Kind "system"}}<div class="syscomment">{{.BodyHTML}} <span class="when">{{when .CreatedAt}}</span></div>
16{{else}}<article class="comment">
17 <header class="commenthead"><strong><a href="/{{.Author}}">{{.Author}}</a></strong> <span class="when">{{when .CreatedAt}}</span></header>
1718 <div class="rendered">{{.BodyHTML}}</div>
18</article>
19</article>{{end}}
1920 {{end}}
2021 {{if .Viewer}}
2122 <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/{{.Issue.Number}}/comment" class="commentform">
internal/web/templates/mr.html +4 −3
@@ -15,10 +15,11 @@
1515 </div>{{end}}
1616 {{range .Reviews}}<p class="review">review: <strong>{{.Reviewer}}</strong> — {{.Verdict}}{{if .Stale}} <span class="chip chip-stale">stale</span>{{end}}</p>{{end}}
1717 {{range .Comments}}
18<article class="comment">
19 <header class="commenthead"><strong>{{.Author}}</strong> <span class="when">{{when .CreatedAt}}</span></header>
18{{if eq .Kind "system"}}<div class="syscomment">{{.BodyHTML}} <span class="when">{{when .CreatedAt}}</span></div>
19{{else}}<article class="comment">
20 <header class="commenthead"><strong><a href="/{{.Author}}">{{.Author}}</a></strong> <span class="when">{{when .CreatedAt}}</span></header>
2021 <div class="rendered">{{.BodyHTML}}</div>
21</article>
22</article>{{end}}
2223 {{end}}
2324 {{if .Viewer}}
2425 <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs/{{.MR.Number}}/comment" class="commentform">