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.
|
3e7c5e67
Keenan Nemetz
1d ago
|
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" | |
|
a407c4af
Keenan Nemetz
1d ago
|
10 | "net/url" |
|
3e7c5e67
Keenan Nemetz
1d ago
|
11 | "os" |
| 12 | "strings" | |
| 13 | ||
|
d37d301f
Keenan Nemetz
17h ago
|
14 | "rodent.software/vole/core/git" |
| 15 | "rodent.software/vole/forge/repo" | |
|
3e7c5e67
Keenan Nemetz
1d ago
|
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?". | |
|
a407c4af
Keenan Nemetz
1d ago
|
29 | func Ensure(repos *repo.Store, name, owner, author string) (*repo.Meta, bool, error) { |
|
3e7c5e67
Keenan Nemetz
1d ago
|
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 | } | |
|
a407c4af
Keenan Nemetz
1d ago
|
54 | tree, err := git.WriteFlatTree(g, Template(name)) |
|
3e7c5e67
Keenan Nemetz
1d ago
|
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 | |
|
a407c4af
Keenan Nemetz
1d ago
|
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 [" + | |
|
ed03b3d1
Keenan Nemetz
5h ago
|
80 | repoName + "](/" + url.PathEscape(repoName) + ") repository; its README lists every file Vole reads." |
|
3e7c5e67
Keenan Nemetz
1d ago
|
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 | |
|
f7a34d87
Keenan Nemetz
1d ago
|
93 | profile/custom.css instance-wide styles, loaded after the theme |
|
d222b0cf
Keenan Nemetz
1d ago
|
94 | profile/themes/<id>.css one stylesheet per theme in Account -> Appearance |
|
3e7c5e67
Keenan Nemetz
1d ago
|
95 | profile/favicon.svg replaces the built-in favicon |
|
ed03b3d1
Keenan Nemetz
5h ago
|
96 | assets/ served at /-/static/custom/assets/ |
|
3e7c5e67
Keenan Nemetz
1d ago
|
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. | |
|
f7a34d87
Keenan Nemetz
1d ago
|
105 | |
| 106 | ## Themes | |
| 107 | ||
| 108 | The Account page's Appearance picker lists the built-in Everforest variants and | |
|
d222b0cf
Keenan Nemetz
1d ago
|
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: | |
|
f7a34d87
Keenan Nemetz
1d ago
|
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 | ||
|
d222b0cf
Keenan Nemetz
1d ago
|
141 | The file name becomes the id and the label: acme-ocean.css is the "Acme Ocean" |
|
0584bb70
Keenan Nemetz
1d ago
|
142 | theme. It must use lowercase letters, digits, and dashes, and cannot collide with |
|
97b3b022
Keenan Nemetz
1d ago
|
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 | |
|
f7a34d87
Keenan Nemetz
1d ago
|
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 | |
|
d222b0cf
Keenan Nemetz
1d ago
|
152 | file small: every theme is linked on every page. |
|
f7a34d87
Keenan Nemetz
1d ago
|
153 | |
|
d222b0cf
Keenan Nemetz
1d ago
|
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. | |
|
3e7c5e67
Keenan Nemetz
1d ago
|
156 | `), |
|
a407c4af
Keenan Nemetz
1d ago
|
157 | "profile/README.md": []byte(intro + "\n"), |
|
f7a34d87
Keenan Nemetz
1d ago
|
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 | |
|
d222b0cf
Keenan Nemetz
1d ago
|
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. | |
|
3e7c5e67
Keenan Nemetz
1d ago
|
164 | * |
|
bc95767f
Keenan Nemetz
1d ago
|
165 | * Prefer tokens over rules: a token keeps the layout, contrast, and responsive |
| 166 | * behaviour that the built-in sheets already got right. | |
|
3e7c5e67
Keenan Nemetz
1d ago
|
167 | * |
|
bc95767f
Keenan Nemetz
1d ago
|
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 | |
|
3e7c5e67
Keenan Nemetz
1d ago
|
214 | */ |
| 215 | `), | |
| 216 | } | |
| 217 | } |