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.
Phase 7: the CLI knows who you are and where you wrote it
a63475ce8a689107f6572150d7c58be1d623f6f0 · Keenan Nemetz · 17d ago · parent 79c88d72
Phase 7: the CLI knows who you are and where you wrote it Three things a first-time user hit in a row, all measured against a live instance before they were fixed. 1. Identity was guessed. Every issue and request was stamped with a name from `git config user.name`, sanitised: "Keenan Nemetz" became `keenan-nemetz`, an account that does not exist, and nothing complained — the issue page showed it, and the assignee rule accepted it because that rule only checks the shape. `vole-whoami` is a new SSH verb, and `vole whoami` is the CLI command that reads it: the instance is the only place a key maps to an account, and it is one call on the connection already open. `vole issue` and `vole request` stamp the account's current name and id, and `--author` is gone from both, because a name you can type is a name you can get wrong. Read commands still need no identity — `issue list` and `issue show` keep working with the instance unreachable — and the git-config guess survives only as a warned fallback for `--no-push`. 2. `--remote ssh://…` did not fetch. `fetch` checked for a configured remote name, so a URL was skipped: the first write in a fresh repository succeeded (nothing to fast-forward from) and every later one pushed from a tip that had never been updated, was rejected, and then retried as a "lost race" that never happened, reporting `already exists`. A URL is now fetched like a named remote, so `git init` plus a URL is the real no-clone filing path the roadmap described. 3. A command answered with an id and stopped. `new` and `comment` now print the page they wrote, and a comment link lands on that comment: the instance reports its own `public_url` in the same whoami reply (new optional config key), and the web puts an `id="c-<ulid>"` anchor on every comment. Empty states on the issues and requests pages print the command that fills them, because four of the five things a reader wants to do are writes the web cannot take, and "no comments yet" is a dead end. Verified live, not only in tests: `vole whoami` in a clone whose `user.name` is "Keenan Nemetz" printed `keenan` while the issue it then filed carried `author: keenan` on the branch; two repositories that never clone both wrote through the URL (the exact case that failed before); and a comment printed a `#c-01M5N3M0…` permalink whose anchor is on the served page. Tests: TestWhoAmIAnswersFromTheInstance (including `--author` now being an unknown option), TestReadCommandsNeedNoIdentity (closes the SSH listener, then lists), TestRequestAuthorComesFromTheKey, TestAURLRemoteIsFetchedBeforeWriting, TestEmptyStatesPrintAWorkingCommand, and the comment-empty-state assertion added to TestIssuePushedThenRendered. scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
internal/config/config.go +16
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "os" | |
| 7 | 7 | "path/filepath" | |
| 8 | "strings" | ||
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/BurntSushi/toml" | |
| @@ -42,6 +43,12 @@ type Config struct { | |||
| 42 | 43 | // PagesCacheBytes caps the in-memory blob cache. The cache is required, not an | |
| 43 | 44 | // optimisation: cold git reads are ~1.2 ms p50, warm ones 0.075 ms. | |
| 44 | 45 | PagesCacheBytes int64 `toml:"pages_cache_bytes"` | |
| 46 | // PublicURL is where this instance is reachable from a browser, when that is | ||
| 47 | // knowable: "https://forge.example.com" behind a proxy, or the listen address | ||
| 48 | // on a LAN. It is optional and used for one thing — telling a client where to | ||
| 49 | // read what it just wrote (the CLI prints the URL of an issue or comment it | ||
| 50 | // created). Empty means the CLI prints ids only, which it did before. | ||
| 51 | PublicURL string `toml:"public_url"` | ||
| 45 | 52 | ||
| 46 | 53 | Path string `toml:"-"` | |
| 47 | 54 | } | |
| @@ -111,6 +118,10 @@ func (c *Config) Lease() time.Duration { | |||
| 111 | 118 | func (c *Config) InstanceKey() string { return filepath.Join(c.DataDir, "instance.key") } | |
| 112 | 119 | func (c *Config) SSHHostKey() string { return filepath.Join(c.DataDir, "ssh_host_ed25519_key") } | |
| 113 | 120 | ||
| 121 | // BaseURL is PublicURL without a trailing slash, so a caller can append | ||
| 122 | // "/r/<repo>/issues/<id>" without thinking about it. | ||
| 123 | func (c *Config) BaseURL() string { return strings.TrimRight(c.PublicURL, "/") } | ||
| 124 | |||
| 114 | 125 | // Sample renders a starter config for `vole init`. | |
| 115 | 126 | func Sample() string { | |
| 116 | 127 | return `# Vole configuration | |
| @@ -124,6 +135,11 @@ http_addr = "127.0.0.1:8080" | |||
| 124 | 135 | # The only write transport. Identity is the SSH key presented at handshake. | |
| 125 | 136 | ssh_addr = "127.0.0.1:2222" | |
| 126 | 137 | ||
| 138 | # Where a browser reads this instance, if that differs from http_addr — a proxy | ||
| 139 | # endpoint, or a LAN address. Optional: the CLI uses it to print the page for | ||
| 140 | # something it just created. Trailing slash is ignored. | ||
| 141 | # public_url = "https://forge.example.com" | ||
| 142 | |||
| 127 | 143 | # Reject individual blobs above this size (pre-receive). Point people at | |
| 128 | 144 | # git-lfs instead. | |
| 129 | 145 | max_blob_bytes = 104857600 | |