vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 22m ago
4b8c9e1d
| 1 | {{/* Small components shared by more than one surface: the label/assignee token |
| 2 | input, the commit list, the comment card, the file tree, and the pager. All |
| 3 | of them are rendered from a page's own data rather than by a call to a |
| 4 | handler. */}} |
| 5 | {{define "token_input"}} |
| 6 | <div class="label-input" data-label-input data-label-item="{{.Item}}"> |
| 7 | <div class="label-tokens" data-label-tokens> |
| 8 | <input type="text" class="label-entry" value="{{join .Selected ", "}}" data-label-entry autocomplete="off" placeholder="{{.Placeholder}}" aria-label="{{.Placeholder}}"> |
| 9 | </div> |
| 10 | <div class="label-suggest" data-label-suggest hidden> |
| 11 | {{range .All}}<button type="button" class="label-option" data-label-option="{{.}}">{{.}}</button>{{end}} |
| 12 | </div> |
| 13 | </div> |
| 14 | {{end}} |
| 15 | |
| 16 | {{define "commitlist"}} |
| 17 | {{if .Commits}} |
| 18 | <table class="commits"> |
| 19 | <tbody> |
| 20 | {{range .Commits}} |
| 21 | <tr> |
| 22 | <td class="sha"><a href="/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a></td> |
| 23 | <td><a href="/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a></td> |
| 24 | <td class="muted">{{.Author}}</td> |
| 25 | <td class="muted nowrap">{{since .When}}</td> |
| 26 | </tr> |
| 27 | {{end}} |
| 28 | </tbody> |
| 29 | </table> |
| 30 | {{else}} |
| 31 | <div class="empty-state"><p>No commits yet.</p></div> |
| 32 | {{end}} |
| 33 | {{end}} |
| 34 | |
| 35 | {{/* One tree for every browser. A code row opens a blob and carries the file |
| 36 | icon for its language; a changed-file row opens the file's diff and carries |
| 37 | the A/M/D/R mark instead. The context says which is which: with Review and |
| 38 | Commit both empty this is the code browser. This template lives here |
| 39 | because the code browser owns the tree; request_files.html and commit.html |
| 40 | render it with a change or a commit. */}} |
| 41 | {{define "tree_nodes"}} |
| 42 | <ul class="tree-list"> |
| 43 | {{range .Nodes}} |
| 44 | <li> |
| 45 | {{if eq .Type "tree"}} |
| 46 | <details class="tree-dir{{if .Active}} active{{end}}" data-path="{{.Path}}"{{if .Open}} open{{end}}> |
| 47 | <summary class="tree-row"> |
| 48 | <span class="tree-chevron">{{icon "chevron-right"}}</span> |
| 49 | <span class="tree-icon">{{icon "folder"}}</span> |
| 50 | <span class="tree-name" title="{{.Path}}">{{.Name}}</span> |
| 51 | </summary> |
| 52 | <div class="tree-children">{{template "tree_nodes" ($.Branch .Children)}}</div> |
| 53 | </details> |
| 54 | {{else}} |
| 55 | {{$href := $.Href .Path}} |
| 56 | <a class="tree-row tree-file{{if .Active}} active{{end}}" data-path="{{.Path}}" href="{{$href}}" hx-get="{{$href}}" hx-target="{{if $.Diff}}#review-main{{else}}#code-main{{end}}" hx-swap="outerHTML" hx-push-url="true" title="{{.Path}}"> |
| 57 | {{if $.Diff}}<span class="review-mark" data-status="{{.Status}}">{{.Status}}</span>{{else}}<span class="tree-icon {{fileclass .Name}}">{{icon (fileicon .Name)}}</span>{{end}} |
| 58 | <span class="tree-name">{{.Name}}</span> |
| 59 | </a> |
| 60 | {{end}} |
| 61 | </li> |
| 62 | {{end}} |
| 63 | </ul> |
| 64 | {{end}} |
| 65 | |
| 66 | {{/* One page control for every list that can run past a screen: issues, change |
| 67 | requests, and the instance activity feed. The handler owns the URLs and the |
| 68 | words ("Issue pages"); this only draws them. */}} |
| 69 | {{/* One card for a comment, on either surface. An issue comment has no anchor, so |
| 70 | it shows no snippet; a change comment anchored to a line shows the lines it |
| 71 | was written about, read from the blob as it stood then, and marks the card as |
| 72 | stale when the file has moved on. Edit swaps .Target, so the card does not |
| 73 | need to know which thread it is in. */}} |
| 74 | {{define "comment_card"}} |
| 75 | <article class="comment{{if .Deleted}} deleted{{end}}" id="c-{{.ID}}"> |
| 76 | {{if .Deleted}} |
| 77 | <div class="comment-deleted">{{icon "circle-x"}}Comment deleted</div> |
| 78 | {{else}} |
| 79 | <header class="comment-head"> |
| 80 | {{$u := user .Author}} |
| 81 | <span class="comment-author">{{template "userlink" $u}}</span> |
| 82 | {{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}} |
| 83 | <span class="muted">{{since .Created.Unix}}</span> |
| 84 | {{if and .Anchor (not .Current)}}<span class="conflict">the file has changed since this comment</span>{{end}} |
| 85 | {{if and .Anchor (not .Snippet)}}<span class="muted">on <code>{{.Anchor.Path}}:{{.Anchor.Line}}</code> ({{.Anchor.Side}} side)</span>{{end}} |
| 86 | <div class="comment-menu" data-comment-menu> |
| 87 | <button type="button" class="comment-menu-toggle" data-comment-menu-toggle aria-haspopup="menu" aria-expanded="false" aria-label="Comment actions">⋯</button> |
| 88 | <div class="comment-menu-list" data-comment-menu-list role="menu" hidden> |
| 89 | {{if .CanReply}}<button type="button" role="menuitem" data-reply-to="{{.ID}}" data-reply-author="{{who .Author}}">Reply</button>{{end}} |
| 90 | <button type="button" role="menuitem" data-copy-query="{{urlquery .Source}}">Copy text</button> |
| 91 | <button type="button" role="menuitem" data-copy-link="{{.ID}}">Copy link</button> |
| 92 | {{if .CanEdit}}<button type="button" role="menuitem" data-comment-edit>Edit</button>{{end}} |
| 93 | {{if .CanDelete}} |
| 94 | <form method="post" action="{{.DeleteURL}}" data-confirm="Delete this comment?"> |
| 95 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 96 | <button type="submit" role="menuitem" class="menu-danger">Delete</button> |
| 97 | </form> |
| 98 | {{end}} |
| 99 | </div> |
| 100 | </div> |
| 101 | </header> |
| 102 | <div class="comment-body md">{{.Body}}</div> |
| 103 | {{if .Snippet}} |
| 104 | <details class="comment-snippet"> |
| 105 | <summary> |
| 106 | <span class="snippet-chevron">{{icon "chevron-right"}}</span> |
| 107 | <code>{{.Snippet.Path}}:{{.Snippet.Line}}</code> |
| 108 | <span class="muted">({{.Snippet.Side}} side)</span> |
| 109 | {{if not .Current}}<span class="conflict">original line</span>{{end}} |
| 110 | </summary> |
| 111 | <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> |
| 112 | <div class="snippet-actions"><a href="{{.Snippet.FilesURL}}">View in Files</a></div> |
| 113 | </details> |
| 114 | {{end}} |
| 115 | {{if .CanEdit}} |
| 116 | <form class="comment-edit-form" data-comment-edit-form hidden method="post" action="{{.EditURL}}" hx-post="{{.EditURL}}" hx-target="{{.Target}}" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"> |
| 117 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 118 | <textarea name="body" required data-autosize>{{.Source}}</textarea> |
| 119 | <div class="form-actions"> |
| 120 | <button class="secondary" type="button" data-comment-edit-cancel>Cancel</button> |
| 121 | <button type="submit">Save</button> |
| 122 | </div> |
| 123 | </form> |
| 124 | {{end}} |
| 125 | {{end}} |
| 126 | </article> |
| 127 | {{end}} |
| 128 | |
| 129 | {{/* The discussion section of an issue or a change request: heading, threaded |
| 130 | comments, one composer. The two surfaces pass the same view, so the section |
| 131 | id, the composer's URL and the heading word come from the handler rather than |
| 132 | from the template. */}} |
| 133 | {{define "comment_thread"}} |
| 134 | <div id="{{.Section}}"> |
| 135 | <h2>{{icon "message-square"}}{{.Title}} <span class="muted">({{.CommentCount}})</span></h2> |
| 136 | {{if .ThreadError}}<p class="error">{{.ThreadError}}</p>{{end}} |
| 137 | {{with .Target}}<p class="anchor-note muted">Commenting on <code>{{.Path}}:{{.Line}}</code> ({{.Side}} side) · <a href="{{$.BackURL}}#{{$.Composer}}">cancel</a></p>{{end}} |
| 138 | {{range .Comments}} |
| 139 | {{template "comment_card" .}} |
| 140 | {{if .Replies}} |
| 141 | <details class="thread-replies" open> |
| 142 | <summary><span class="thread-chevron">{{icon "chevron-right"}}</span>{{len .Replies}} {{if eq (len .Replies) 1}}reply{{else}}replies{{end}}</summary> |
| 143 | <div class="comment-replies">{{range .Replies}}{{template "comment_card" .}}{{end}}</div> |
| 144 | </details> |
| 145 | {{end}} |
| 146 | {{else}} |
| 147 | <div class="empty-state"><p>{{.EmptyText}}</p></div> |
| 148 | {{end}} |
| 149 | |
| 150 | {{if .CanDiscuss}} |
| 151 | <form id="{{.Composer}}" method="post" action="{{.PostURL}}" class="form-block discussion-composer" hx-post="{{.PostURL}}" hx-target="#{{.Section}}" hx-swap="outerHTML"{{if .PushURL}} hx-push-url="{{.PushURL}}"{{end}} hx-disabled-elt="find button[type='submit']"> |
| 152 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 153 | <input type="hidden" name="reply_to" value="" data-reply-input> |
| 154 | {{with .Target}}<input type="hidden" name="path" value="{{.Path}}"><input type="hidden" name="line" value="{{.Line}}"><input type="hidden" name="side" value="{{.Side}}">{{end}} |
| 155 | <div class="reply-target" data-reply-target hidden> |
| 156 | <span>Replying to <strong data-reply-name></strong></span> |
| 157 | <button class="link" type="button" data-reply-clear>Cancel reply</button> |
| 158 | </div> |
| 159 | <textarea name="body" required placeholder="Leave a comment. Markdown is welcome." aria-label="Comment"{{if .Target}} autofocus{{end}}></textarea> |
| 160 | <div class="form-actions"><button type="submit">{{icon "message-square"}}Comment</button></div> |
| 161 | </form> |
| 162 | {{end}} |
| 163 | </div> |
| 164 | {{end}} |
| 165 | |
| 166 | {{define "pager"}} |
| 167 | {{if gt .Pages 1}} |
| 168 | <nav class="pager" aria-label="{{.Label}}"> |
| 169 | {{if .PrevURL}}<a class="button secondary" href="{{.PrevURL}}"{{if .Target}} hx-get="{{.PrevURL}}" hx-target="{{.Target}}" hx-swap="outerHTML" hx-push-url="true"{{end}}>Previous</a>{{else}}<span></span>{{end}} |
| 170 | <span class="muted">Showing {{.From}}–{{.To}} of {{.Total}} · page {{.Page}} of {{.Pages}}</span> |
| 171 | {{if .NextURL}}<a class="button secondary" href="{{.NextURL}}"{{if .Target}} hx-get="{{.NextURL}}" hx-target="{{.Target}}" hx-swap="outerHTML" hx-push-url="true"{{end}}>Next</a>{{else}}<span></span>{{end}} |
| 172 | </nav> |
| 173 | {{end}} |
| 174 | {{end}} |
| 175 |