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 · 1h ago
4b8c9e1d
| 1 | package instancerepo |
| 2 | |
| 3 | // Creating the repository Vole reads for its presentation. `vole admin init` |
| 4 | // calls Ensure so a new instance starts with a repository an admin can open, |
| 5 | // edit, and push to like any other. The Admin page calls the same function, so |
| 6 | // an instance that predates this behaviour has the same repair path. |
| 7 | |
| 8 | import ( |
| 9 | "errors" |
| 10 | "net/url" |
| 11 | "os" |
| 12 | "strings" |
| 13 | |
| 14 | "rodent.software/vole/core/git" |
| 15 | "rodent.software/vole/forge/repo" |
| 16 | ) |
| 17 | |
| 18 | // DefaultDescription is what the repository list shows before an admin edits |
| 19 | // it. |
| 20 | const DefaultDescription = "Instance customization: profile README, custom CSS, favicon, and assets" |
| 21 | |
| 22 | // Ensure creates name when it is missing and seeds the starter tree when it has |
| 23 | // no commits. An existing repository with history is left alone. owner may be |
| 24 | // empty, which creates a private repository that instance admins can still |
| 25 | // administer; author names the commit when the starter files are written. |
| 26 | // |
| 27 | // It reports whether it wrote anything, so a caller can audit a creation |
| 28 | // without inventing its own answer to "was this new?". |
| 29 | func Ensure(repos *repo.Store, name, owner, author string) (*repo.Meta, bool, error) { |
| 30 | name = strings.TrimSpace(name) |
| 31 | if name == "" || repos == nil { |
| 32 | return nil, false, nil |
| 33 | } |
| 34 | created := false |
| 35 | m, err := repos.Meta(name) |
| 36 | if err != nil { |
| 37 | if !errors.Is(err, os.ErrNotExist) { |
| 38 | return nil, false, err |
| 39 | } |
| 40 | m, err = repos.Create(name, owner, DefaultDescription) |
| 41 | if err != nil { |
| 42 | return nil, false, err |
| 43 | } |
| 44 | created = true |
| 45 | } |
| 46 | g := m.Git() |
| 47 | tip, err := g.RevParseRaw("refs/heads/" + m.DefaultBranch) |
| 48 | if err != nil { |
| 49 | return nil, created, err |
| 50 | } |
| 51 | if tip != "" { |
| 52 | return m, created, nil |
| 53 | } |
| 54 | tree, err := git.WriteFlatTree(g, Template(name)) |
| 55 | if err != nil { |
| 56 | return nil, created, err |
| 57 | } |
| 58 | commit, err := g.CommitTreeAs(tree, "", "Create the instance repository", author) |
| 59 | if err != nil { |
| 60 | return nil, created, err |
| 61 | } |
| 62 | if err := g.UpdateRef("refs/heads/"+m.DefaultBranch, commit); err != nil { |
| 63 | return nil, created, err |
| 64 | } |
| 65 | return m, true, nil |
| 66 | } |
| 67 | |
| 68 | // Template is the starter tree. The root README explains the fixed paths; the |
| 69 | // profile files give an admin something to edit rather than a blank page, and |
| 70 | // the CSS names the variables worth overriding. repoName is the repository the |
| 71 | // starter tree is written to, so the introduction can link back to its overview. |
| 72 | func Template(repoName string) map[string][]byte { |
| 73 | repoName = strings.TrimSpace(repoName) |
| 74 | // The page supplies the heading (instance_name), so the starter file carries |
| 75 | // no header of its own: it explains how to replace itself and links to the |
| 76 | // repository where the other files live. |
| 77 | intro := "This is the landing page introduction. Tell people what this forge is for." |
| 78 | if repoName != "" { |
| 79 | intro = "This is the landing page introduction. To customize it, edit `profile/README.md` in the [" + |
| 80 | repoName + "](/" + url.PathEscape(repoName) + ") repository; its README lists every file Vole reads." |
| 81 | } |
| 82 | return map[string][]byte{ |
| 83 | "README.md": []byte(`# Instance customization |
| 84 | |
| 85 | This repository customizes the Vole instance. Vole creates it with ` + "`vole admin init`" + ` and |
| 86 | reloads it on a short timer, so a push is live within a couple of seconds. |
| 87 | |
| 88 | Only these paths are read; other files are ignored, so this README and any |
| 89 | notes can live here too. |
| 90 | |
| 91 | profile/README.md the landing page introduction (markdown) |
| 92 | profile/private/README.md replaces it for signed-in viewers |
| 93 | profile/custom.css instance-wide styles, loaded after the theme |
| 94 | profile/themes/<id>.css one stylesheet per theme in Account -> Appearance |
| 95 | profile/favicon.svg replaces the built-in favicon |
| 96 | assets/ served at /-/static/custom/assets/ |
| 97 | |
| 98 | Edit a file here and push, or open it in the web interface and use the editor. |
| 99 | This repository has the usual pages, history, issues, and change requests. Its |
| 100 | visibility is a repository setting, so an instance admin can publish it or keep |
| 101 | it private. |
| 102 | |
| 103 | The files above are served to visitors (the private README only to signed-in |
| 104 | viewers), even when the repository itself is private. |
| 105 | |
| 106 | ## Themes |
| 107 | |
| 108 | The Account page's Appearance picker lists the built-in Everforest variants and |
| 109 | every CSS file in profile/themes/. A theme is self-describing: its file name is |
| 110 | the id and the label, and its rules are scoped to that id. Add one by creating |
| 111 | the stylesheet: |
| 112 | |
| 113 | profile/themes/acme-ocean.css |
| 114 | :root[data-theme="acme-ocean"] { |
| 115 | color-scheme: dark; |
| 116 | --bg: #0b1720; |
| 117 | --bg-panel: #102433; |
| 118 | --bg-raised: #163041; |
| 119 | --fg: #d7e8f0; |
| 120 | --fg-muted: #8fb0c0; |
| 121 | --fg-faint: #6f8f9f; |
| 122 | --accent: #38bdf8; |
| 123 | --accent-fg: #04121a; |
| 124 | --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent); |
| 125 | } |
| 126 | |
| 127 | profile/themes/acme-paper.css |
| 128 | :root[data-theme="acme-paper"] { |
| 129 | color-scheme: light; |
| 130 | --bg: #fffdf7; |
| 131 | --bg-panel: #f8f3e8; |
| 132 | --bg-raised: #f1eadc; |
| 133 | --fg: #2b2a26; |
| 134 | --fg-muted: #6b675d; |
| 135 | --fg-faint: #8d887c; |
| 136 | --accent: #9a5b13; |
| 137 | --accent-fg: #fffdf7; |
| 138 | --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent); |
| 139 | } |
| 140 | |
| 141 | The file name becomes the id and the label: acme-ocean.css is the "Acme Ocean" |
| 142 | theme. It must use lowercase letters, digits, and dashes, and cannot collide with |
| 143 | a built-in value: system, nord, dracula, tokyo-night, or a name starting with |
| 144 | everforest-, catppuccin-, solarized-, rose-pine-, kanagawa-, or ayu-. Each stylesheet is |
| 145 | loaded after the built-in sheets and before profile/custom.css, and its rules |
| 146 | apply only while that theme is selected. Set color-scheme and the tokens the |
| 147 | theme changes: a light theme needs color-scheme: light and light surface and |
| 148 | text tokens. |
| 149 | |
| 150 | profile/custom.css applies to every theme, so keep instance-wide overrides |
| 151 | there and put a selectable palette in profile/themes/<id>.css. Keep each theme |
| 152 | file small: every theme is linked on every page. |
| 153 | |
| 154 | The picker stores the choice in the browser, not on the account. A theme whose |
| 155 | file is deleted falls back to System the next time the page loads. |
| 156 | `), |
| 157 | "profile/README.md": []byte(intro + "\n"), |
| 158 | "profile/custom.css": []byte(`/* Custom styles. This file is loaded last: after vole.css, everforest.css, and |
| 159 | * any profile/themes/<id>.css, so it can override every theme. |
| 160 | * |
| 161 | * For a palette that appears in Account -> Appearance, add |
| 162 | * profile/themes/<id>.css; its file name is the id and label. This file is for |
| 163 | * overrides that apply no matter which theme is selected. |
| 164 | * |
| 165 | * Prefer tokens over rules: a token keeps the layout, contrast, and responsive |
| 166 | * behaviour that the built-in sheets already got right. |
| 167 | * |
| 168 | * Use :root[data-theme], not plain :root. Everforest sets its tokens on |
| 169 | * :root[data-theme^="everforest-"], which is more specific than :root, so a |
| 170 | * plain :root override can lose even though this file loads later. |
| 171 | */ |
| 172 | |
| 173 | :root[data-theme] { |
| 174 | /* Uncomment one line at a time and reload the page. |
| 175 | * |
| 176 | * --accent: #c678dd; |
| 177 | * --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent); |
| 178 | * --radius: 4px; |
| 179 | * --radius-lg: 6px; |
| 180 | * --page-max: 1200px; |
| 181 | * --font: ui-sans-serif, system-ui, sans-serif; |
| 182 | */ |
| 183 | } |
| 184 | |
| 185 | /* A value that has to differ between dark and light modes belongs in one of |
| 186 | * these blocks. They match every Everforest variant. |
| 187 | * |
| 188 | * :root[data-theme^="everforest-dark"] { --bg: #101418; } |
| 189 | * :root[data-theme^="everforest-light"] { --bg: #fffdf7; } |
| 190 | */ |
| 191 | |
| 192 | /* Tokens, grouped by what they change: |
| 193 | * |
| 194 | * surfaces --bg --bg-panel --bg-raised --bg-hover --bg-active |
| 195 | * --border --border-strong |
| 196 | * text --fg --fg-muted --fg-faint |
| 197 | * identity --accent --accent-fg --accent-soft |
| 198 | * status --good --good-soft --bad --bad-soft --warn --warn-soft |
| 199 | * --running --merged --merged-soft --status-fg |
| 200 | * shape --radius --radius-lg --page-max --page-gutter --search-w |
| 201 | * type --font --mono --fs --lh |
| 202 | * (--font-scale is the reader's text-size preset: leave it alone) |
| 203 | * syntax --syn-comment --syn-punctuation --syn-operator --syn-number |
| 204 | * --syn-string --syn-keyword --syn-function --syn-type |
| 205 | * --syn-variable --syn-added --syn-deleted |
| 206 | */ |
| 207 | |
| 208 | /* If a token cannot express the change, these page-level classes cover most |
| 209 | * re-skins. They are not a public API; an upgrade may move markup, so prefer |
| 210 | * tokens when one exists. |
| 211 | * |
| 212 | * .panel .readme .md .repo-card .repo-grid .home-grid .home-rail |
| 213 | * .repohead .tabs .chip .label .status .config-table .diff .blob |
| 214 | */ |
| 215 | `), |
| 216 | } |
| 217 | } |
| 218 |