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 · 27m ago
4b8c9e1d
| 1 | {{/* Shared page chrome: the document shell, the global bar and account menu, |
| 2 | the repository header with its tabs, and the one error page every 404 and |
| 3 | 500 renders through. Every page template opens with {{template "header" .}} |
| 4 | and closes with {{template "footer" .}}. */}} |
| 5 | {{define "header"}}<!doctype html> |
| 6 | <html lang="en" data-theme="everforest-dark-medium" data-font-scale="medium"{{if .InstanceThemes}} data-themes="{{range $i, $t := .InstanceThemes}}{{if $i}},{{end}}{{$t.ID}}{{end}}"{{end}}> |
| 7 | <head> |
| 8 | <meta charset="utf-8"> |
| 9 | <meta name="viewport" content="width=device-width,initial-scale=1"> |
| 10 | <meta name="csrf" content="{{.CSRF}}"> |
| 11 | <meta name="color-scheme" content="dark light"> |
| 12 | {{if .NoIndex}}<meta name="robots" content="noindex">{{end}} |
| 13 | <meta name="htmx-config" content='{"includeIndicatorStyles":false}'> |
| 14 | <title>{{.Title}} · {{.InstanceName}}</title> |
| 15 | <link rel="icon" type="image/png" sizes="32x32" href="/-/static/favicon-32.png" media="(prefers-color-scheme: light)"> |
| 16 | <link rel="icon" type="image/png" sizes="32x32" href="/-/static/favicon-32-dark.png" media="(prefers-color-scheme: dark)"> |
| 17 | <link rel="icon" type="image/png" sizes="16x16" href="/-/static/favicon-16.png" media="(prefers-color-scheme: light)"> |
| 18 | <link rel="icon" type="image/png" sizes="16x16" href="/-/static/favicon-16-dark.png" media="(prefers-color-scheme: dark)"> |
| 19 | <link rel="icon" type="image/svg+xml" href="/-/static/favicon.svg" sizes="any"> |
| 20 | {{if .InstanceFavicon}}<link rel="icon" type="image/svg+xml" href="{{.InstanceFavicon}}" sizes="any">{{end}} |
| 21 | <script src="/-/static/theme.js"></script> |
| 22 | <link rel="stylesheet" href="/-/static/vole.css"> |
| 23 | <link rel="stylesheet" href="/-/static/everforest.css"> |
| 24 | <link rel="stylesheet" href="/-/static/palettes.css"> |
| 25 | {{range .InstanceThemes}}<link rel="stylesheet" href="{{.URL}}"> |
| 26 | {{end}}{{if .InstanceCSS}}<link rel="stylesheet" href="{{.InstanceCSS}}">{{end}} |
| 27 | <script src="/-/static/htmx.min.js" defer></script> |
| 28 | <script src="/-/static/auth.js" defer></script> |
| 29 | <script src="/-/static/app.js" defer></script> |
| 30 | </head> |
| 31 | <body> |
| 32 | <header class="topbar"> |
| 33 | <a class="brand" href="/" aria-label="{{.InstanceName}} home"><span class="mark" aria-hidden="true"></span></a> |
| 34 | <nav class="topnav" aria-label="Global"> |
| 35 | <a class="{{if eq .Section "home"}}active{{end}}" href="/"{{if eq .Section "home"}} aria-current="page"{{end}}>{{icon "house"}}Home</a> |
| 36 | <a class="{{if eq .Section "activity"}}active{{end}}" href="/-/activity"{{if eq .Section "activity"}} aria-current="page"{{end}}>{{icon "activity"}}Activity</a> |
| 37 | <a class="{{if eq .Section "people"}}active{{end}}" href="/-/u"{{if eq .Section "people"}} aria-current="page"{{end}}>{{icon "users"}}Users</a> |
| 38 | {{if .InstanceAdmin}}<a class="{{if eq .Section "admin"}}active{{end}}" href="/-/admin"{{if eq .Section "admin"}} aria-current="page"{{end}}>{{icon "sliders-horizontal"}}Admin</a>{{end}} |
| 39 | </nav> |
| 40 | <div class="topsearch" role="search"> |
| 41 | <button type="button" class="topsearch-box" data-palette-open aria-haspopup="dialog" aria-controls="search-palette" aria-keyshortcuts="Control+K Meta+K"> |
| 42 | {{icon "search"}} |
| 43 | <span class="topsearch-label">Search issues and changes</span> |
| 44 | <kbd class="topsearch-key" data-palette-key aria-hidden="true">Ctrl K</kbd> |
| 45 | </button> |
| 46 | </div> |
| 47 | <div class="topright"> |
| 48 | {{if .LoggedIn}} |
| 49 | <details class="account-menu"> |
| 50 | <summary> |
| 51 | {{icon "user"}}<span class="account-name">{{.User}}</span> |
| 52 | {{if .DevLogin}}<span class="label" title="Development sign-in">dev</span>{{end}} |
| 53 | <span class="account-caret">{{icon "chevron-right"}}</span> |
| 54 | </summary> |
| 55 | <div class="account-pop"> |
| 56 | <a href="/-/account">{{icon "user"}}Account</a> |
| 57 | <div class="account-sep"></div> |
| 58 | <form method="post" action="/-/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button type="submit">{{icon "log-out"}}Sign out</button></form> |
| 59 | </div> |
| 60 | </details> |
| 61 | {{else}} |
| 62 | {{if .DevLogin}}<a href="/-/dev/login">{{icon "user"}}Dev sign-in</a>{{end}} |
| 63 | <a href="/-/login">{{icon "user"}}Sign in</a> |
| 64 | {{end}} |
| 65 | </div> |
| 66 | </header> |
| 67 | {{/* The command palette is the whole search surface: one field over every |
| 68 | issue, change request and comment the reader can see. The launcher in the |
| 69 | global bar opens it; the dialog itself never navigates to a page. */}} |
| 70 | <dialog class="palette" id="search-palette" aria-label="Search issues, changes and comments"> |
| 71 | <form class="palette-search" role="search" |
| 72 | hx-get="/-/search/palette" hx-trigger="input changed delay:250ms, search" |
| 73 | hx-target="#palette-results" hx-swap="innerHTML" hx-sync="this:replace"> |
| 74 | <span aria-hidden="true">{{icon "search"}}</span> |
| 75 | <input type="search" name="q" id="search-palette-input" |
| 76 | placeholder="Search issues, changes and comments" |
| 77 | aria-label="Search issues, changes and comments" autocomplete="off" |
| 78 | role="combobox" aria-expanded="true" aria-controls="palette-results" aria-autocomplete="list"> |
| 79 | <button type="button" class="palette-close" data-palette-close aria-label="Close search">{{icon "x"}}</button> |
| 80 | </form> |
| 81 | <div class="palette-results" id="palette-results" role="listbox" aria-label="Search results"> |
| 82 | {{template "palette_hint"}} |
| 83 | </div> |
| 84 | <div class="palette-foot" aria-hidden="true"> |
| 85 | <span><kbd>↑</kbd> <kbd>↓</kbd> move</span> |
| 86 | <span><kbd>Enter</kbd> open</span> |
| 87 | <span><kbd>Esc</kbd> close</span> |
| 88 | </div> |
| 89 | </dialog> |
| 90 | <a class="skip-link" href="#main">Skip to content</a> |
| 91 | <main class="page" id="main"> |
| 92 | {{if .Error}}<div class="flash bad">{{.Error}}</div>{{end}} |
| 93 | {{end}} |
| 94 | |
| 95 | {{define "footer"}} |
| 96 | </main> |
| 97 | </body> |
| 98 | </html> |
| 99 | {{end}} |
| 100 | |
| 101 | {{/* The open counts beside the Issues and Changes tabs. They are templates of |
| 102 | their own so the header's poll can replace the badges and nothing else: the |
| 103 | tab the reader is on, the clone menu and any open form stay where they are. |
| 104 | The ids are what the out-of-band swap matches, so the page and the poll |
| 105 | agree on where a count goes. */}} |
| 106 | {{define "repo_issue_count"}}<span class="chip-count" id="repo-issue-count"{{if .Live}} hx-swap-oob="outerHTML"{{end}}>{{.OpenIssues}}</span>{{end}} |
| 107 | {{define "repo_request_count"}}<span class="chip-count" id="repo-request-count"{{if .Live}} hx-swap-oob="outerHTML"{{end}}>{{.OpenRequests}}</span>{{end}} |
| 108 | {{define "repo_counts_live"}} |
| 109 | {{template "repo_issue_count" .}} |
| 110 | {{template "repo_request_count" .}} |
| 111 | {{end}} |
| 112 | |
| 113 | {{define "repohead"}} |
| 114 | <div class="repohead"> |
| 115 | <div class="repo-title"> |
| 116 | <div class="repo-title-row"> |
| 117 | <div class="repo-name"><span class="repo-vis" title="{{if .Private}}Private repository{{else}}Public repository{{end}}">{{if .Private}}{{icon "lock"}}{{else}}{{icon "globe"}}{{end}}</span>{{.Name}}</div> |
| 118 | <div class="repo-title-actions"> |
| 119 | {{if .Site}}<a class="button secondary" href="{{.Site}}">{{icon "external-link"}}Site</a>{{end}} |
| 120 | <details class="clone"> |
| 121 | <summary>{{icon "copy"}}Clone</summary> |
| 122 | <div class="clone-pop"> |
| 123 | <p class="muted">Read only, no credentials:</p> |
| 124 | <div class="clone-field"> |
| 125 | <code>{{.CloneURL}}</code> |
| 126 | <button class="secondary" type="button" data-copy="{{.CloneURL}}" title="Copy HTTPS URL" aria-label="Copy HTTPS URL">{{icon "copy"}}</button> |
| 127 | </div> |
| 128 | <p class="muted">Write over SSH with a registered key:</p> |
| 129 | <div class="clone-field"> |
| 130 | <code>git clone {{.SSHURL}}</code> |
| 131 | <button class="secondary" type="button" data-copy="git clone {{.SSHURL}}" title="Copy SSH command" aria-label="Copy SSH command">{{icon "copy"}}</button> |
| 132 | </div> |
| 133 | </div> |
| 134 | </details> |
| 135 | </div> |
| 136 | </div> |
| 137 | {{if .Description}}<p class="repo-desc">{{.Description}}</p>{{end}} |
| 138 | </div> |
| 139 | {{/* One poll for the header's counts. The response is out-of-band updates to |
| 140 | the two badges, so the tabs are not re-rendered and the page keeps its |
| 141 | state. It pauses while the tab is hidden, so a forgotten page does not |
| 142 | keep asking. */}} |
| 143 | <nav class="tabs" aria-label="Repository" |
| 144 | hx-get="/{{.Name}}/counts" |
| 145 | hx-trigger="every 30s [document.visibilityState === 'visible']" |
| 146 | hx-sync="this:replace" |
| 147 | hx-swap="none"> |
| 148 | <a class="repotab{{if eq .Tab "overview"}} active{{end}}" href="/{{.Name}}"{{if eq .Tab "overview"}} aria-current="page"{{end}}>{{icon "layout-dashboard"}}Overview</a> |
| 149 | <a href="/{{.Name}}/activity"{{if eq .Tab "activity"}} class="active" aria-current="page"{{end}}>{{icon "activity"}}Activity</a> |
| 150 | <a href="/{{.Name}}/code"{{if eq .Tab "code"}} class="active" aria-current="page"{{end}}>{{icon "code"}}Code</a> |
| 151 | <a href="/{{.Name}}/issues"{{if eq .Tab "issues"}} class="active" aria-current="page"{{end}}>{{icon "circle-dot"}}Issues {{template "repo_issue_count" .}}</a> |
| 152 | <a href="/{{.Name}}/changes"{{if eq .Tab "changes"}} class="active" aria-current="page"{{end}}>{{icon "git-pull-request"}}Changes {{template "repo_request_count" .}}</a> |
| 153 | <a href="/{{.Name}}/releases"{{if eq .Tab "releases"}} class="active" aria-current="page"{{end}}>{{icon "tag"}}Releases</a> |
| 154 | <a href="/{{.Name}}/runs"{{if eq .Tab "runs"}} class="active" aria-current="page"{{end}}>{{icon "play"}}Runs</a> |
| 155 | <a href="/{{.Name}}/locks"{{if eq .Tab "locks"}} class="active" aria-current="page"{{end}}>{{icon "lock"}}Locks</a> |
| 156 | {{if .CanAdmin}}<a href="/{{.Name}}/settings"{{if eq .Tab "settings"}} class="active" aria-current="page"{{end}}>{{icon "sliders-horizontal"}}Settings</a>{{end}} |
| 157 | </nav> |
| 158 | </div> |
| 159 | {{end}} |
| 160 | |
| 161 | {{define "errorpage"}} |
| 162 | {{template "header" .}} |
| 163 | <div class="empty-page"> |
| 164 | <p class="eyebrow">Error</p> |
| 165 | <h1>{{.Title}}</h1> |
| 166 | <p class="muted">{{.Message}}</p> |
| 167 | <p><a class="button" href="/">Back to repositories</a></p> |
| 168 | </div> |
| 169 | {{template "footer" .}} |
| 170 | {{end}} |
| 171 | |
| 172 | {{define "notfound"}}{{template "errorpage" .}}{{end}} |
| 173 | |
| 174 | {{define "servererror"}}{{template "errorpage" .}}{{end}} |
| 175 | |
| 176 | {{/* An account name, as a link to the profile it belongs to. `user` returns an |
| 177 | href only when the name belongs to an account, so a commit author who never |
| 178 | registered, and a typo in a mention, stay text rather than becoming a link |
| 179 | that leads to a 404. Use `who` instead where a link would be wrong: inside |
| 180 | another link, inside a button, in an <option>, or in an attribute. */}} |
| 181 | {{define "userlink"}}{{if .Known}}<a class="user" href="{{.Href}}">{{.Name}}</a>{{else}}{{.Name}}{{end}}{{end}} |
| 182 | {{/* The command palette's initial state and the rows HTMX swaps in. The dialog |
| 183 | shell above carries both, so they live beside the chrome that owns them. */}} |
| 184 | {{define "palette_hint"}} |
| 185 | <p class="palette-empty" role="option" aria-disabled="true">Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.</p> |
| 186 | {{end}} |
| 187 | |
| 188 | {{define "palette_results"}} |
| 189 | {{if .Message}} |
| 190 | <p class="palette-empty" role="option" aria-disabled="true">{{.Message}}</p> |
| 191 | {{else if not .Query}} |
| 192 | {{template "palette_hint"}} |
| 193 | {{else if .Results}} |
| 194 | {{range .Results}} |
| 195 | <a class="palette-row" data-palette-row role="option" aria-selected="false" href="{{.URL}}"> |
| 196 | <span class="palette-row-title">{{.Title}}</span> |
| 197 | <span class="palette-row-where">{{.KindLabel}} · {{.Repo}}{{if .Number}} · <span class="sha">{{num .Number}}</span>{{end}}</span> |
| 198 | {{if .Snippet}}<span class="search-snippet">{{.Snippet}}</span>{{end}} |
| 199 | </a> |
| 200 | {{end}} |
| 201 | {{if .Limited}}<p class="palette-empty" role="option" aria-disabled="true">Showing the first results. Add a word to narrow the search.</p>{{end}} |
| 202 | {{else}} |
| 203 | <p class="palette-empty" role="option" aria-disabled="true">Nothing matches “{{.Query}}”.</p> |
| 204 | {{end}} |
| 205 | {{end}} |
| 206 |