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.

One metadata branch: refs/heads/vole, linear history, audited rewrites

92b37587e6e8b4e05f95dfc1e555ba36eaaa424d · Keenan Nemetz · 17d ago · parent f9568748

One metadata branch: refs/heads/vole, linear history, audited rewrites

Issues and change requests now share a single branch. Both are human-authored and
low-frequency, and one ref means one push, one fetch and one validation path
instead of per-branch special cases.

  refs/heads/vole
    issues/<ulid>/...
    requests/...        reserved for change requests

Rules, enforced in pre-receive rather than documented and hoped for:

- The tree layout is closed: only issues/ and requests/ may appear, so a typo in
  a directory name is rejected instead of being silently ignored by the web.
- History must be linear. Merge commits on the metadata branch are refused,
  because a merge node says nothing and makes `git log` harder to read than the
  change it describes. Rebasing is what happens instead.
- Force-push requires repo admin and writes a line to the audit log. Rewriting
  discussion history is sometimes necessary (someone pastes a secret) but must
  never be silent, so it is allowed rather than forbidden and recorded.

The CLI no longer fails on a lost race. Because every entity is its own file,
re-applying a change on the new tip cannot conflict, so it re-fetches, redoes the
mutation and retries with jittered backoff. Two things this turned up that a
single-writer test could never have found:

- A lost race has more than one shape. Besides "non-fast-forward" and "fetch
  first", a push can arrive with a stale old-oid ("incorrect old value
  provided") or lose the creation of the branch itself ("reference already
  exists"). All four are retryable; hook rejections are not.
- Retries without jitter keep writers in lockstep: six concurrent writers failed
  five rounds in a row with a deterministic backoff, and converged immediately
  once the sleep was randomised.

Verified by hand: the CLI writes to the vole branch, `git log vole` is a flat
list, and a merge push is refused with both problems reported at once — the merge
node and a malformed issue id in the same tree.

internal/gitx/gitx.go +37

@@ -338,6 +338,43 @@ func (r *Repo) Log(rev string, n int) ([]Commit, error) {
338 338 return parseCommits(out), nil
339 339 }
340 340
341 // IsAncestor reports whether ancestor is reachable from descendant. Used to tell
342 // a fast-forward push from a rewrite.
343 func (r *Repo) IsAncestor(ancestor, descendant string) (bool, error) {
344 if ancestor == "" || ancestor == ZeroSHA {
345 return true, nil // a new branch cannot rewrite anything
346 }
347 _, _, err := r.run(nil, "merge-base", "--is-ancestor", ancestor, descendant)
348 if err == nil {
349 return true, nil
350 }
351 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
352 return false, nil
353 }
354 return false, fmt.Errorf("merge-base --is-ancestor %s %s: %w", ancestor, descendant, err)
355 }
356
357 // CountMerges counts merge commits in old..new, or in all of new when old is the
358 // zero SHA.
359 func (r *Repo) CountMerges(old, new string) (int, error) {
360 args := []string{"rev-list", "--count", "--merges", new}
361 if old != "" && old != ZeroSHA {
362 args = []string{"rev-list", "--count", "--merges", old + ".." + new}
363 }
364 out, errs, err := r.run(nil, args...)
365 if err != nil {
366 if strings.Contains(errs, "bad revision") || strings.Contains(errs, "unknown revision") {
367 return 0, nil // history we cannot read is not a merge we can prove
368 }
369 return 0, fmt.Errorf("rev-list --merges: %w: %s", err, errs)
370 }
371 n, err := strconv.Atoi(strings.TrimSpace(string(out)))
372 if err != nil {
373 return 0, nil
374 }
375 return n, nil
376 }
377
341 378 // LogGrep returns commits whose message mentions pattern. Used for issue
342 379 // backlinks, so the search happens in git rather than in a second index.
343 380 func (r *Repo) LogGrep(rev, pattern string, n int) ([]Commit, error) {