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.
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 ./...).
docs/roadmap.md +22−8
| @@ -585,14 +585,25 @@ tag marked annotated, the download returned the bytes, and `git log vole` read | |||
| 585 | 585 | ||
| 586 | 586 | Planned: | |
| 587 | 587 | ||
| 588 | - Identity without ceremony: `vole whoami` resolves the key a person pushes with to an | ||
| 589 | account (the roster is one SSH verb away, `vole-roster`), and `vole request approve` | ||
| 590 | stops requiring `--as`. The name someone types has to match their key anyway, so | ||
| 591 | asking for it is a trap rather than a check. | ||
| 592 | - Filing an issue or opening a request without a clone (`--remote ssh://…`), because | ||
| 593 | "clone the repository to report a bug" is the roughest edge in the current flow. | ||
| 594 | - Navigation and empty states: repo tabs with counts, breadcrumbs everywhere, and an | ||
| 595 | empty state that prints the exact command that fills it. | ||
| 588 | - ✅ **Identity without ceremony.** `vole whoami` asks the instance which account | ||
| 589 | the key resolves to (`vole-whoami` over the same SSH channel), and `vole issue` | ||
| 590 | and `vole request` stamp that account's name on what they write. `--author` is | ||
| 591 | gone from both: a name you can type is a name you can get wrong, and the old | ||
| 592 | default — `git config user.name`, sanitised — turned "Keenan Nemetz" into | ||
| 593 | `keenan-nemetz`, an account that does not exist, silently. The fallback when the | ||
| 594 | instance cannot be reached is the git-config guess **with a warning**, because a | ||
| 595 | `--no-push` run on a train still has to commit something. `vole request approve | ||
| 596 | --as <user>` still exists; it is the one remaining place a name can be typed, and | ||
| 597 | the hook refuses a forged approval anyway. | ||
| 598 | - ✅ **Filing an issue or opening a request without a clone** (`--remote ssh://…`). | ||
| 599 | It worked once per repository and then failed, because `fetch` skipped a URL: | ||
| 600 | the push came from a tip that had never been updated and the retry reported a | ||
| 601 | "lost race" that had not happened. A URL is now fetched like a named remote, so | ||
| 602 | `git init` in an empty directory plus a URL is a working filing path (measured by | ||
| 603 | two repositories that never clone, both landing). | ||
| 604 | - ✅ **Empty states print the command that fills them**, and `new`/`comment` print | ||
| 605 | the page they wrote when `public_url` is set. Repo tabs with counts and | ||
| 606 | breadcrumbs everywhere are still open. | ||
| 596 | 607 | - Per-file history (`/commits/<ref>/<path>`), so "when did this change" is one click. | |
| 597 | 608 | ||
| 598 | 609 | Releases: decisions and what is left. Design settled by | |
| @@ -776,6 +787,9 @@ Filled in as phases land. Append; don't rewrite. | |||
| 776 | 787 | | 2026-10-23 | An edit that changes nothing is not a commit | `vole issue assign` on an already-assigned person would otherwise add an empty commit to a linear branch, and a script that reruns has to be idempotent without growing the log. `Update` reports `ErrNoChange` and the branch does not move | | |
| 777 | 788 | | 2026-10-23 | A comment is edited in place, deleted by removing its file, and has no `edited` field | The file is the comment: keeping the path keeps the id, the thread order and one `git log -p -- <path>` that answers "what did it say". A deleted comment leaves its text in history, which is honest — redaction is still an admin force-push — and a flat ULID-keyed thread has no replies to orphan. `edited:` would repeat the argument that dropped `updated`: a line rewritten on every edit, when the commit already carries the time | | |
| 778 | 789 | | 2026-10-23 | Comment authorship is checked against the old blob, and only for comments | The old blob was validated when it landed; the new one is whatever the pusher wrote, so comparing it would let anyone adopt a comment by editing its author line in the same push. Restricting the rule to comments is deliberate: `state`, `labels` and `assignees` are shared fields and a maintainer who cannot close or assign someone else's issue has no triage. Admins may still repair a thread, audited | | |
| 790 | | 2026-10-23 | The author of a metadata write is the account the key resolves to, asked with `vole-whoami`; `--author` is gone | `git config user.name` is a name somebody typed for commits: "Keenan Nemetz" sanitises to `keenan-nemetz`, which is not an account, so every issue filed from a normal machine was attributed to nobody and nothing complained. The instance is the only place that maps a key to an account, and it is one SSH verb away on the connection already open. A fallback exists for `--no-push` with the instance unreachable, and it warns loudly rather than guessing silently | | ||
| 791 | | 2026-10-23 | A remote given as an SSH URL is fetched like a named one | Skipping the fetch for a URL is what made `--remote ssh://…` write on a branch that had never been updated: the first write in a fresh repository succeeded (nothing to fast-forward from) and every later one failed as a "lost race" that never happened, re-applying on the same stale tip and reporting "already exists". `git init` plus a URL is now the real no-clone filing path | | ||
| 792 | | 2026-10-23 | The instance tells the client where it is served (`public_url`), so a command can print the page it wrote | A command that answers with an id leaves the reader to assemble a URL, and the instance is the only party that knows its own address behind a proxy. Optional: unset means ids only, which is what it printed before | | ||
| 779 | 793 | ||
| 780 | 794 | ## Open questions | |
| 781 | 795 | ||