vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Build out change review and threaded discussions
f6f4b1db24292d1df877680714952a9c568e2b78 · Keenan Nemetz · 15d ago · parent 564acd4b
Build out change review and threaded discussions - Add Conversation, Files and Runs tabs to change requests. Files uses the code-browser shape with a changed-file tree and one selected-file diff; Runs lists the head commit's workflow results. - Show anchored review comments beneath the line, with a collapsed excerpt read from the exact blob that was reviewed, and return posted anchors to their line note. - Add comment threads: any comment can be replied to, the UI flattens the descendants under one root, reply context links back to the parent, and reply targets feed a single composer at the foot of the discussion. - Collapse reply groups while keeping the root visible, and scroll to the composer when Reply is selected. - Add comment context menus with Reply, Copy text, Copy link, Edit and Delete. Add web edit/delete routes with author or admin checks; deletion leaves a tombstone so replies keep their thread. - Support --reply-to in the issue and change CLI commands, and treat a missing parent as a tombstone rather than an invalid tree. - Fix the diff comment gutter, action button colours, reply labels, and keep the Files tree and diff pane aligned. Update metadata validation, web templates, CSS, JavaScript, tests, and the CLI/change-request/issue docs.
server/web/templates/requests.html +141−46
| @@ -75,8 +75,27 @@ | |||
| 75 | 75 | </div> | |
| 76 | 76 | {{end}} | |
| 77 | 77 | ||
| 78 | {{define "request_landing"}} | ||
| 79 | <section> | ||
| 80 | <h2>{{icon "git-pull-request"}}What would land</h2> | ||
| 81 | {{if .MergeChecked}}{{if .Conflicts}} | ||
| 82 | <p class="conflict">This branch no longer merges into {{.Request.Base}} cleanly: | ||
| 83 | {{range .Conflicts}}<code>{{.}}</code> {{end}}— rebase or merge {{.Request.Base}} before landing it.</p> | ||
| 84 | {{else}} | ||
| 85 | <p class="muted">Merges into {{.Request.Base}} cleanly.</p> | ||
| 86 | {{end}}{{end}} | ||
| 87 | {{if .AlreadyMerged}} | ||
| 88 | <p class="muted">Already merged into {{.Request.Base}}.</p> | ||
| 89 | {{else if .FileCount}} | ||
| 90 | <p class="muted">{{.FileCount}} file(s) changed{{if .Added}} · <span class="adds">+{{.Added}}</span>{{end}}{{if .Removed}} · <span class="dels">−{{.Removed}}</span>{{end}} · <a href="/r/{{.Name}}/requests/{{.Request.ID}}/files">Review files</a></p> | ||
| 91 | {{else}} | ||
| 92 | <p class="muted">No changes, or the head is not reachable from this repository yet.</p> | ||
| 93 | {{end}} | ||
| 94 | </section> | ||
| 95 | {{end}} | ||
| 96 | |||
| 78 | 97 | {{define "request_sidebar"}} | |
| 79 | <aside class="thread-side" id="request-sidebar"> | ||
| 98 | <div id="request-sidebar"> | ||
| 80 | 99 | {{if .Error}}<p class="error">{{.Error}}</p>{{end}} | |
| 81 | 100 | <section> | |
| 82 | 101 | <h2>{{icon "git-branch"}}Review</h2> | |
| @@ -120,48 +139,121 @@ | |||
| 120 | 139 | {{else if not .ContributorCI}} | |
| 121 | 140 | <p class="muted">No CI run for this commit yet.</p> | |
| 122 | 141 | {{end}} | |
| 142 | <p class="section-link"><a href="/r/{{.Name}}/requests/{{.Request.ID}}/runs">View runs</a></p> | ||
| 123 | 143 | </section> | |
| 124 | </aside> | ||
| 144 | </div> | ||
| 125 | 145 | {{end}} | |
| 126 | 146 | ||
| 127 | {{define "request_discussion"}} | ||
| 128 | <div id="request-discussion"> | ||
| 129 | {{if .ThreadError}}<p class="error">{{.ThreadError}}</p>{{end}} | ||
| 130 | {{with .CommentTarget}}<p class="anchor-note muted">Commenting on <code>{{.Path}}:{{.Line}}</code> ({{.Side}} side) · <a href="/r/{{$.Name}}/requests/{{$.Request.ID}}#request-comment-form">cancel</a></p>{{end}} | ||
| 131 | <h2>{{icon "message-square"}}Discussion <span class="muted">({{len .Comments}})</span></h2> | ||
| 132 | {{range .Comments}} | ||
| 133 | <article class="comment" id="c-{{.ID}}"> | ||
| 147 | {{define "request_inline_comment"}} | ||
| 148 | <tr class="inline-comment"> | ||
| 149 | <td colspan="4"> | ||
| 150 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/comments" class="inline-comment-form"> | ||
| 151 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 152 | <input type="hidden" name="path" value="{{.Target.Path}}"> | ||
| 153 | <input type="hidden" name="line" value="{{.Target.Line}}"> | ||
| 154 | <input type="hidden" name="side" value="{{.Target.Side}}"> | ||
| 155 | <div class="inline-comment-card"> | ||
| 156 | <div class="inline-comment-head"> | ||
| 157 | <span>Comment on <code>{{.Target.Path}}:{{.Target.Line}}</code> <span class="muted">({{.Target.Side}} side)</span></span> | ||
| 158 | <button class="link" type="button" data-inline-comment-cancel>Cancel</button> | ||
| 159 | </div> | ||
| 160 | <textarea name="body" required autofocus placeholder="Leave an inline comment. Markdown is welcome." aria-label="Inline comment"></textarea> | ||
| 161 | </div> | ||
| 162 | <div class="form-actions"><button type="submit">{{icon "message-square"}}Comment</button></div> | ||
| 163 | </form> | ||
| 164 | </td> | ||
| 165 | </tr> | ||
| 166 | {{end}} | ||
| 167 | |||
| 168 | {{define "request_comment_card"}} | ||
| 169 | <article class="comment{{if .Deleted}} deleted{{end}}" id="c-{{.ID}}"> | ||
| 170 | {{if .Deleted}} | ||
| 171 | <div class="comment-deleted">{{icon "circle-x"}}Comment deleted</div> | ||
| 172 | {{else}} | ||
| 134 | 173 | <header class="comment-head"> | |
| 135 | 174 | <span class="avatar c{{tone .Author}}" aria-hidden="true">{{initial (who .Author)}}</span> | |
| 136 | 175 | <span class="comment-author">{{who .Author}}</span> | |
| 176 | {{if .ReplyTo}}<span class="reply-context">replying to {{if .ReplyToDeleted}}<a href="#c-{{.ReplyTo}}">a deleted comment</a>{{else}}<a href="#c-{{.ReplyTo}}">{{who .ReplyToName}}</a>{{end}}</span>{{end}} | ||
| 137 | 177 | <span class="muted">{{since .Created.Unix}}</span> | |
| 138 | {{if .Anchor}}<span class="muted">on <code>{{.Anchor.Path}}:{{.Anchor.Line}}</code> ({{.Anchor.Side}} side){{if not .Current}} · <span class="conflict">the file has changed since this comment</span>{{end}}</span>{{end}} | ||
| 178 | {{if and .Anchor (not .Current)}}<span class="conflict">the file has changed since this comment</span>{{end}} | ||
| 179 | <div class="comment-menu" data-comment-menu> | ||
| 180 | <button type="button" class="comment-menu-toggle" data-comment-menu-toggle aria-haspopup="menu" aria-expanded="false" aria-label="Comment actions">⋯</button> | ||
| 181 | <div class="comment-menu-list" data-comment-menu-list role="menu" hidden> | ||
| 182 | {{if .CanReply}}<button type="button" role="menuitem" data-reply-to="{{.ID}}" data-reply-author="{{who .Author}}">Reply</button>{{end}} | ||
| 183 | <button type="button" role="menuitem" data-copy="{{.Source}}">Copy text</button> | ||
| 184 | <button type="button" role="menuitem" data-copy-link="{{.ID}}">Copy link</button> | ||
| 185 | {{if .CanEdit}}<button type="button" role="menuitem" data-comment-edit>Edit</button>{{end}} | ||
| 186 | {{if .CanDelete}} | ||
| 187 | <form method="post" action="{{.DeleteURL}}" data-confirm="Delete this comment?"> | ||
| 188 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 189 | <button type="submit" role="menuitem" class="menu-danger">Delete</button> | ||
| 190 | </form> | ||
| 191 | {{end}} | ||
| 192 | </div> | ||
| 193 | </div> | ||
| 139 | 194 | </header> | |
| 140 | 195 | <div class="comment-body md">{{.Body}}</div> | |
| 196 | {{if .Snippet}} | ||
| 197 | <details class="comment-snippet"> | ||
| 198 | <summary> | ||
| 199 | <span class="snippet-chevron">{{icon "chevron-right"}}</span> | ||
| 200 | <code>{{.Snippet.Path}}:{{.Snippet.Line}}</code> | ||
| 201 | <span class="muted">({{.Snippet.Side}} side)</span> | ||
| 202 | {{if not .Current}}<span class="conflict">original line</span>{{end}} | ||
| 203 | </summary> | ||
| 204 | <div class="snippet-code">{{range .Snippet.Lines}}<div class="snippet-line{{if .Anchor}} anchor{{end}}"><span class="snippet-no">{{.No}}</span><span class="snippet-text">{{.Text}}</span></div>{{end}}</div> | ||
| 205 | <div class="snippet-actions"><a href="{{.Snippet.FilesURL}}">View in Files</a></div> | ||
| 206 | </details> | ||
| 207 | {{end}} | ||
| 208 | {{if .CanEdit}} | ||
| 209 | <form class="comment-edit-form" data-comment-edit-form hidden method="post" action="{{.EditURL}}" hx-post="{{.EditURL}}" hx-target="#request-discussion" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"> | ||
| 210 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 211 | <textarea name="body" required>{{.Source}}</textarea> | ||
| 212 | <div class="form-actions"> | ||
| 213 | <button class="secondary" type="button" data-comment-edit-cancel>Cancel</button> | ||
| 214 | <button type="submit">Save</button> | ||
| 215 | </div> | ||
| 216 | </form> | ||
| 217 | {{end}} | ||
| 218 | {{end}} | ||
| 141 | 219 | </article> | |
| 142 | {{else}} | ||
| 143 | <p class="muted">No discussion yet. From a clone of this repository: | ||
| 144 | <code>vole change comment {{.Request.ID}} -m "..."</code></p> | ||
| 145 | 220 | {{end}} | |
| 146 | 221 | ||
| 147 | {{if .CanDiscuss}} | ||
| 148 | <form id="request-comment-form" method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/comments" class="form-block" hx-post="/r/{{.Name}}/requests/{{.Request.ID}}/comments" hx-target="#request-discussion" hx-swap="outerHTML" hx-push-url="/r/{{.Name}}/requests/{{.Request.ID}}" hx-disabled-elt="find button[type='submit']"> | ||
| 149 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 150 | {{with .CommentTarget}}<input type="hidden" name="path" value="{{.Path}}"><input type="hidden" name="line" value="{{.Line}}"><input type="hidden" name="side" value="{{.Side}}">{{end}} | ||
| 151 | <div class="card form-card"> | ||
| 152 | <label>Comment <textarea name="body" required placeholder="Leave a comment. Markdown is welcome."{{if .CommentTarget}} autofocus{{end}}></textarea></label> | ||
| 153 | </div> | ||
| 154 | <div class="form-actions"><button type="submit">{{icon "message-square"}}Comment</button></div> | ||
| 155 | </form> | ||
| 156 | {{end}} | ||
| 222 | {{define "request_discussion"}} | ||
| 223 | <div id="request-discussion"> | ||
| 224 | <h2>{{icon "message-square"}}Discussion <span class="muted">({{len .Request.Comments}})</span></h2> | ||
| 225 | {{if .ThreadError}}<p class="error">{{.ThreadError}}</p>{{end}} | ||
| 226 | {{with .CommentTarget}}<p class="anchor-note muted">Commenting on <code>{{.Path}}:{{.Line}}</code> ({{.Side}} side) · <a href="/r/{{$.Name}}/requests/{{$.Request.ID}}#request-comment-form">cancel</a></p>{{end}} | ||
| 227 | {{range .Comments}} | ||
| 228 | {{template "request_comment_card" .}} | ||
| 229 | {{if .Replies}} | ||
| 230 | <details class="thread-replies" open> | ||
| 231 | <summary><span class="thread-chevron">{{icon "chevron-right"}}</span>{{len .Replies}} {{if eq (len .Replies) 1}}reply{{else}}replies{{end}}</summary> | ||
| 232 | <div class="comment-replies">{{range .Replies}}{{template "request_comment_card" .}}{{end}}</div> | ||
| 233 | </details> | ||
| 234 | {{end}} | ||
| 235 | {{else}} | ||
| 236 | <p class="muted">No discussion yet. From a clone of this repository: | ||
| 237 | <code>vole change comment {{.Request.ID}} -m "..."</code></p> | ||
| 238 | {{end}} | ||
| 157 | 239 | ||
| 240 | {{if .CanDiscuss}} | ||
| 241 | <form id="request-comment-form" method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/comments" class="form-block discussion-composer" hx-post="/r/{{.Name}}/requests/{{.Request.ID}}/comments" hx-target="#request-discussion" hx-swap="outerHTML" hx-push-url="/r/{{.Name}}/requests/{{.Request.ID}}" hx-disabled-elt="find button[type='submit']"> | ||
| 242 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 243 | <input type="hidden" name="reply_to" value="" data-reply-input> | ||
| 244 | {{with .CommentTarget}}<input type="hidden" name="path" value="{{.Path}}"><input type="hidden" name="line" value="{{.Line}}"><input type="hidden" name="side" value="{{.Side}}">{{end}} | ||
| 245 | <div class="reply-target" data-reply-target hidden> | ||
| 246 | <span>Replying to <strong data-reply-name></strong></span> | ||
| 247 | <button class="link" type="button" data-reply-clear>Cancel reply</button> | ||
| 248 | </div> | ||
| 249 | <textarea name="body" required placeholder="Leave a comment. Markdown is welcome." aria-label="Comment"{{if .CommentTarget}} autofocus{{end}}></textarea> | ||
| 250 | <div class="form-actions"><button type="submit">{{icon "message-square"}}Comment</button></div> | ||
| 251 | </form> | ||
| 252 | {{end}} | ||
| 158 | 253 | </div> | |
| 159 | 254 | {{end}} | |
| 160 | 255 | ||
| 161 | {{define "request"}} | ||
| 162 | {{template "header" .}} | ||
| 163 | {{template "repohead" (nav .Name "requests")}} | ||
| 164 | |||
| 256 | {{define "request_head"}} | ||
| 165 | 257 | <div class="page-head"> | |
| 166 | 258 | <div class="page-head-main"> | |
| 167 | 259 | <h1>{{.Request.Title}}</h1> | |
| @@ -178,25 +270,40 @@ | |||
| 178 | 270 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> | |
| 179 | 271 | <input type="hidden" name="request" value="{{$.Request.ID}}"> | |
| 180 | 272 | <input type="hidden" name="ref" value="refs/heads/{{.Ref}}"> | |
| 181 | <button type="submit">{{icon "play"}}Allow CI for this tip</button> | ||
| 273 | <button class="warn" type="submit">{{icon "play"}}Allow CI</button> | ||
| 182 | 274 | </form> | |
| 183 | 275 | {{end}}{{end}} | |
| 184 | 276 | {{if .CanApprove}} | |
| 185 | 277 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/approve"> | |
| 186 | 278 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | |
| 187 | <button type="submit">{{icon "check"}}Approve this head</button> | ||
| 279 | <button class="success" type="submit">{{icon "check"}}Approve</button> | ||
| 188 | 280 | </form> | |
| 189 | 281 | {{end}} | |
| 190 | 282 | {{if .CanClose}} | |
| 191 | 283 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/state"> | |
| 192 | 284 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | |
| 193 | 285 | <input type="hidden" name="state" value="{{if eq .Request.State "open"}}closed{{else}}open{{end}}"> | |
| 194 | <button class="secondary" type="submit">{{if eq .Request.State "open"}}{{icon "circle-x"}}Close change{{else}}{{icon "rotate-ccw"}}Reopen change{{end}}</button> | ||
| 286 | <button class="{{if eq .Request.State "open"}}danger{{else}}secondary{{end}}" type="submit">{{if eq .Request.State "open"}}{{icon "circle-x"}}Close{{else}}{{icon "rotate-ccw"}}Reopen{{end}}</button> | ||
| 195 | 287 | </form> | |
| 196 | 288 | {{end}} | |
| 197 | 289 | </div> | |
| 198 | 290 | {{end}} | |
| 199 | 291 | </div> | |
| 292 | {{end}} | ||
| 293 | |||
| 294 | {{define "request_tabs"}} | ||
| 295 | <nav class="subtabs" aria-label="Change sections"> | ||
| 296 | <a class="{{if eq .Tab "conversation"}}active{{end}}" href="/r/{{.Name}}/requests/{{.Request.ID}}"{{if eq .Tab "conversation"}} aria-current="page"{{end}}>{{icon "message-square"}}Conversation</a> | ||
| 297 | <a class="{{if eq .Tab "files"}}active{{end}}" href="/r/{{.Name}}/requests/{{.Request.ID}}/files"{{if eq .Tab "files"}} aria-current="page"{{end}}>{{icon "file"}}Files <span class="chip-count">{{.FileCount}}</span></a> | ||
| 298 | <a class="{{if eq .Tab "runs"}}active{{end}}" href="/r/{{.Name}}/requests/{{.Request.ID}}/runs"{{if eq .Tab "runs"}} aria-current="page"{{end}}>{{icon "play"}}Runs <span class="chip-count">{{len .Checks}}</span></a> | ||
| 299 | </nav> | ||
| 300 | {{end}} | ||
| 301 | |||
| 302 | {{define "request"}} | ||
| 303 | {{template "header" .}} | ||
| 304 | {{template "repohead" (nav .Name "requests")}} | ||
| 305 | {{template "request_head" .}} | ||
| 306 | {{template "request_tabs" .}} | ||
| 200 | 307 | ||
| 201 | 308 | <div class="thread"> | |
| 202 | 309 | <div class="thread-main"> | |
| @@ -212,24 +319,12 @@ | |||
| 212 | 319 | </article> | |
| 213 | 320 | ||
| 214 | 321 | {{template "request_discussion" .}} | |
| 215 | |||
| 216 | <h2>{{icon "git-pull-request"}}What would land</h2> | ||
| 217 | {{if .MergeChecked}}{{if .Conflicts}} | ||
| 218 | <p class="conflict">This branch no longer merges into {{.Request.Base}} cleanly: | ||
| 219 | {{range .Conflicts}}<code>{{.}}</code> {{end}}— rebase or merge {{.Request.Base}} before landing it.</p> | ||
| 220 | {{else}} | ||
| 221 | <p class="muted">Merges into {{.Request.Base}} cleanly.</p> | ||
| 222 | {{end}}{{end}} | ||
| 223 | {{if .AlreadyMerged}} | ||
| 224 | <p class="muted">Already merged into {{.Request.Base}}.</p> | ||
| 225 | {{else if .Files}} | ||
| 226 | {{template "diff" .}} | ||
| 227 | {{else}} | ||
| 228 | <p class="muted">No changes, or the head is not reachable from this repository yet.</p> | ||
| 229 | {{end}} | ||
| 230 | 322 | </div> | |
| 231 | 323 | ||
| 232 | {{template "request_sidebar" .}} | ||
| 324 | <aside class="thread-side"> | ||
| 325 | {{template "request_landing" .}} | ||
| 326 | {{template "request_sidebar" .}} | ||
| 327 | </aside> | ||
| 233 | 328 | </div> | |
| 234 | 329 | {{template "footer" .}} | |
| 235 | 330 | {{end}} | |