Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 2h ago 4b8c9e1d
source preview history blame raw 7.6 KiB

Issues

Your issues belong to the repository, not to a vendor. They clone with the code, keep working when a hosting service has an outage, and stay readable in any text editor.

An issue is a Markdown file stored on a special branch called vole. Every clone of the repository carries the issues and their history, so there is no separate issue database to back up.

Where issues live

issues/<ulid>/issue.md           # the issue
issues/<ulid>/comments/<ulid>.md # a comment

Here is a complete issue:

---
title: "Nothing renders on the docs page"
state: open
author: you
created: 2026-10-22T21:30:00Z
labels:
  - bug
---
Steps to reproduce...

The top section, between the --- lines, is YAML metadata. The rest is the Markdown body. Titles and other free text are always quoted, so characters like # or a leading - cannot change the meaning of the file.

An issue id is a 26-character ULID. It uses uppercase letters and digits, but not I, L, O, or U. A UUID will not work.

Numbers

Nobody types 26 characters, so every issue and change request also has a small number: [#1](/r/vole/n/1), [#42](/r/vole/n/42). Issues and change requests share one sequence, oldest first, the way one forge counts both in a single run.

The number is derived, not stored. It is the position of the commit that created the entity in the metadata branch’s history. Nothing is added to the file, no counter is kept, and there is nothing to migrate: an existing repository gets its numbers the first time anyone asks. Two clones of the same history produce the same numbers, and vole admin index rebuild reproduces them from git.

The ULID is still the identity — it is the path, the front matter’s neighbour in closes:, the thing in the commit messages — so a number is a convenience, not a reference you should quote in a script. The one way to make one wrong is to rewrite the branch history: an admin redaction force-push renumbers everything after the commit it rewrote.

Every command that takes an id takes a number instead, with or without the #:

vole issue show 42
vole issue close '#42'
vole change show 1

Writing an issue without the CLI

git switch --orphan vole
ID=01M5JR2JK29KKMAA1T9MDNTPBP
mkdir -p issues/$ID
cat > issues/$ID/issue.md <<'MD'
---
title: "Nothing renders on the docs page"
state: open
author: you
created: 2026-10-22T21:30:00Z
labels:
  - bug
---
Steps to reproduce...
MD
git add -A
git commit -m "issue: docs page blank"
git push origin vole

Vole checks the file when you push. The author field must be the account you push as unless you hold maintain, which is the one place the browser is stricter than a push: a contributor writes as itself. If someone else pushed first, the CLI puts your change on top of theirs and tries again.

From the browser

When browser sign-in is enabled (see Accounts and access), a signed-in contributor or maintainer can open an issue or comment from the repository’s issue page. The author is the account whose passkey opened the session; the form cannot choose a different name. The change is written with the same metadata writer the CLI uses, so it is the same file on the same branch. A contributor can use this form or the CLI; pre-receive asks the same capabilities the form does, so neither offers more than the other.

The issue’s author or a maintainer can correct its title with the pencil beside the heading, or its description from the card’s menu. Each opens its own field in place, so saving does not leave the page or replace the discussion. The discussion, labels, and assignees are left alone, and the edit is an ordinary metadata commit like the one the CLI writes.

Using the CLI

vole issue new --title "Nothing renders on the docs page" -m "Steps..." -l bug -a wilbur
vole issue comment -m "Also affects search" 01M5JR2JK29KKMAA1T9MDNTPBP
vole issue edit-comment 01M5JR2JK29KKMAA1T9MDNTPBP 01M5N1PRC4GJPZNEEG8PR8J4D6 -m "Corrected."
vole issue rm-comment 01M5JR2JK29KKMAA1T9MDNTPBP 01M5N1PRC4GJPZNEEG8PR8J4D6
vole issue edit 01M5JR2JK29KKMAA1T9MDNTPBP --title "A better title" -l bug -l docs
vole issue assign 01M5JR2JK29KKMAA1T9MDNTPBP wilbur
vole issue unassign 01M5JR2JK29KKMAA1T9MDNTPBP wilbur
vole issue close 01M5JR2JK29KKMAA1T9MDNTPBP
vole issue reopen 01M5JR2JK29KKMAA1T9MDNTPBP
vole issue list --state open -l bug --assignee wilbur --format '%(number) %(title)'
vole issue show 01M5JR2JK29KKMAA1T9MDNTPBP     # a prefix is enough
vole issue show 42                             # the number is enough

--format understands %(number), %(id), %(shortid), %(state), %(author), %(title), %(labels), %(assignees), and %(created). %(shortid) is a prefix of the ULID, which is mostly timestamp; %(number) is the alias people read and quote.

The CLI commits and pushes for you. It uses the account your SSH key belongs to, not the name in git config. If you want to commit without pushing, add --no-push.

You can also create an issue in a new directory with no remote configured:

git init bug-report
cd bug-report
vole issue new --title "the docs page is blank" \
  --remote ssh://git@forge.example.com/demo.git

Rules

  • Issues and comments live under issues/ and changes/ on the vole branch. Other paths are rejected. Releases have their own paths, written by Vole. CI records are not stored on the branch; they live in data/runs.db.
  • History on the vole branch must be linear. Merge commits are not allowed.
  • Force-pushing vole requires repository admin and is recorded in data/audit.log. It is meant for redaction, not normal work.
  • Writing [#42](/r/vole/n/42) in a body links to that entity. Because one sequence holds both kinds, the link goes through /n/42, and the server resolves it to the issue or the change request that holds the number. #<ulid> links by id and is the form that keeps working if history is ever rewritten. Commits that mention an id are shown as backlinks on the issue page.
  • Writing @name links to that account’s profile. It is the same syntax a workflow’s mentioned filter matches, so the page and a job agree about who was addressed. Code samples are skipped, and a name that is not an account stays text.
  • An assignee must be a valid account name: lowercase letters, digits, -, and _. The name is stored as written because account names are never reused. A value that is not an account name cannot be an assignee.
  • The web page can filter by state, label, and assignee, and search titles and bodies.

Comments

A comment is a file at issues/<id>/comments/<ulid>.md with author, created, and optionally reply_to metadata. reply_to names any comment in the same thread. The web renders one level of nesting by collecting a root’s descendants beneath it.

Only the author of a comment can edit or delete it. Vole compares the author on the old version of the file with the account behind the SSH key. An admin can override this, for example to repair a thread after someone leaves, and the override is recorded in data/audit.log.

Deleting a comment replaces it with a tombstone (“Comment deleted”) so replies keep the thread they answer. The text remains in Git history, and a deleted comment cannot be edited. Editing a comment does not add an “edited” field; the commit carries the time. Deleting is not redaction. Redaction means an admin force-push.

The web comment menu offers Reply, Copy, Edit and Delete. Editing and deleting are limited to the author or an admin, and deletion leaves a tombstone so replies keep their thread.

The issue itself works differently. Closing, relabeling, or assigning someone else’s issue is normal triage, not a claim of authorship.