gogitter
v0.1.3
Published
An append-only issue tracker stored in Git, built with TypeScript and Effect.
Readme
gogitter
An append-only issue tracker stored in Git, built with TypeScript and Effect.
This is a proof of concept. Issues contain an ID, author, creation date, title,
description, and an open or closed state.
Getting started
Requires Git, Node.js 22.13 or newer, and pnpm 10.9.0. The included
nix-shell supplies the development tools. Node's built-in SQLite module may
print an experimental-feature warning on some Node versions.
pnpm install
pnpm build
node dist/cli.js --helpRun the CLI inside a Git repository with user.name and user.email configured.
No initialization command or existing branch commit is needed.
pnpm start create --title "Fix login" --description "Login fails after a refresh."
# Prints the new issue's full ID. Use that ID or an unambiguous prefix below.
pnpm start list
pnpm start show <id>
pnpm start edit <id> --title "Fix login after refresh"
pnpm start edit <id> --description "Updated details"
pnpm start close <id>
pnpm start reopen <id>
pnpm start history <id>
pnpm start list --state closed
pnpm start list --state allAn edit can change both title and description in one event. Passing
--description "" clears the description. Commands that return issue data or
sync results support --json. Listing defaults to open issues.
When running from another repository, use node /absolute/path/to/gogitter/dist/cli.js.
The packaged executable is named gogitter; --version reads the packaged
package.json, so there is no second version string to maintain.
Terminal UI
Run gogitter tui inside your repository (or pnpm start tui from this checkout)
to open the TUI, or gogitter tui --remote upstream to choose a remote at startup.
It follows lazygit's conventions: a ticket list on the left, details on the right,
the keys for the current context along the bottom, and ? for a menu of every
keybinding (press Enter on an entry to run it). It needs an interactive terminal
of at least 56 columns and 12 rows.
| Keys | Action |
| --- | --- |
| j / k, arrows | Next / previous ticket, or scroll the details |
| h / l, Tab | Focus the tickets or details panel |
| 1 / 2 / 3 | Show open, closed or all tickets |
| [ / ] | Previous / next tab of the focused panel |
| < / >, , / . | Top / bottom, previous / next page |
| / | Search; Enter keeps the filter, Esc clears it |
| n / e | New ticket / edit the selected ticket |
| Space | Close or reopen the selected ticket |
| y / Y | Copy the description / choose what to copy (title, ID, …) |
| p / P | Pull / push |
| u | Choose the remote |
| R | Refresh |
| ? | Keybindings menu |
| q | Close the current popup, or quit |
| Ctrl+c | Quit |
The ticket form opens over the panels, like lazygit's commit message popup.
There are no modes: just type. In the title, Enter saves and Tab (or ↓)
moves to the description. In the description, Enter adds a line and Ctrl+s
or Alt+Enter saves. Esc closes the form, asking before discarding changes.
Shell-style editing keys work in both fields: Ctrl+a / Ctrl+e for the line
start and end, Alt+b / Alt+f (or Ctrl/Alt with arrows) to move by word,
and Ctrl+w, Ctrl+u and Ctrl+k to delete a word, back to the line start,
or to the line end. Pasted text is inserted as text and never runs commands.
Errors, such as a save that conflicts with a newer change, appear in a popup. Your draft stays open so you can recover. Pull and push are explicit, and nothing synchronizes automatically. Push uses the same fetch, validation, merge, and atomic push as the CLI. Local ticket changes leave your working files and branch untouched.
Web UI
pnpm build
pnpm start webui
# With an installed package, run: gogitter webuiwebui starts an Effect HTTP server on 127.0.0.1, chooses an available port,
and opens your browser. Keep the command running; Ctrl+C stops the server.
Use webui --port 3000 to choose a port or webui --no-open to print the URL
without opening a browser. Run the command in the repository you want to use.
The React interface includes a searchable, paginated issue list, open/closed filters, issue details, a plain-text editor, close/reopen actions, and event history. Choose a configured remote and use Pull or Push explicitly. Refreshing the list reads local changes made by CLI commands; it does not pull. An edit based on an outdated issue revision is rejected while keeping the draft visible, so you can review newer changes before saving again.
The frontend assets are bundled in the package and served locally; users do not need a development server or additional frontend dependencies. This is a local, single-user UI, with same-origin requests and a per-process session token. The server does not bind to public network interfaces. Descriptions are always plain text, including HTML or Markdown characters.
Sharing issues
pnpm start pull # Defaults to origin
pnpm start push
pnpm start pull upstream # Or another configured Git remoteThe remote must allow custom refs and atomic pushes. A local bare repository is enough; no separate service is required. The POC requires a single identical fetch/push URL for the selected remote.
Each sync transfers all issue refs in one fetch or push. push first fetches,
validates, and merges remote changes. Concurrent edits are replayed in a
deterministic order: parents first, then lexicographically smallest eligible
event ID. The last replayed change to a field wins. Changes to different fields
are retained. There is no interactive merge-conflict resolution.
Normal git clone, git pull, and git push do not synchronize these custom
refs by default. Run gogitter pull in a new clone. Issue operations do not
check out files or modify the index, current branch, or its history.
Storage and integrity
refs/gogitter/issues/<creation-commit-id>points to each issue's latest commit.- Every user change creates a new commit with one
event.jsonblob and a tree. Commits are never amended. Creating the root commit first makes its hash available before creating the issue ref. - Divergent histories get a two-parent synchronization commit. It carries no user change and does not affect event ordering.
refs/gogitter/checkpoints/<remote-key>/<issue-id>remembers accepted remote history. A remote rollback, deletion, or rewrite that drops accepted history makes both pull and push fail. Validation finishes before local refs change..git/gogitter/cache.sqliteis a disposable projection, indexed by issue state. Every pull invalidates the whole cache, including the pull performed by push. The next query rebuilds it. Local edits also invalidate it, and cached ref tips are checked on every query to recover from interrupted writes.
gogitter rebuild-cache explicitly rebuilds the cache. Deleting the SQLite file
is also safe: rewrite detection lives in Git refs. Linked Git worktrees share
the same issue refs and cache through their common Git directory.
Rewrite detection protects history this clone has already accepted for that remote. A first pull has no previous checkpoint to compare against. This is client-side validation, not a server policy or a signature system: direct Git commands can still rewrite refs, and Git author information is self-reported. Retain the checkpoint refs when backing up the repository.
See the storage format for the event schema and sync protocol.
Removing local gogitter data
gogitter wipe
# From this project's checkout: pnpm start wipewipe immediately deletes all local refs/gogitter/* refs (issues, sync
checkpoints, and temporary refs), their reflogs, and the entire gogitter cache
directory. It supports --json, which reports the number of refs removed. This
command is available only in the CLI, with no web UI or HTTP API action.
Linked worktrees share this data, so wiping from one clears it for all of them.
Stop other gogitter commands and web UI servers in this repository before wiping.
Branches, tags, Git configuration, the index, and working files are preserved.
Unreachable issue commits, trees, and blobs are left for Git's normal garbage
collection; wipe does not prune the repository's shared object database.
Remotes and other clones are unchanged. A later pull restores remote issues
with fresh checkpoints; unpublished local changes are lost. Wiping also resets
this clone's record of previously accepted remote history.
Development and tests
pnpm dev # Restart the CLI when source files change
pnpm typecheck # Check implementation and tests
pnpm test # Build, property tests, real Git/SQLite tests, packaged CLI test
pnpm test:stress # Exercise 3,000 issues, including push and pull
pnpm test:browser # Run Chromium tests against real CLI-launched web servers
pnpm pack # Build and create a distributable tarball; does not publishAll test repositories, SQLite databases, remotes, and working copies live in temporary directories and are removed afterward. Tests set their own Git identity/configuration and use local filesystem remotes. They require no Git hosting account or network connection after dependencies are installed.
Install the browser test runtime once with pnpm exec playwright install chromium
(on Linux, use pnpm exec playwright install --with-deps chromium). HTTP tests
run as part of pnpm test; browser tests run separately with pnpm test:browser.
Both use temporary repositories and loopback ports. pnpm build rebuilds the
browser bundle after changes under web/.
The suite covers generated event DAGs, concurrent edits from three clones, simultaneous local writers, fetch/push races, remote rewrites/deletions, atomic rejection, cache corruption/rebuild, Git worktrees, SHA-256 repositories, garbage collection, and preservation of ordinary branch/worktree state. The packaging test extracts a real pnpm tarball and runs its compiled CLI with only production dependencies linked in, including the packaged web server and assets. Web tests cover API validation, stale edits, search and pagination, explicit synchronization, rewrite errors, plain-text rendering, desktop/mobile workflows, and server shutdown. CI runs browser tests on Linux with Node 24.
The stress test prints timings without imposing machine-specific speed limits.
Use GOGITTER_STRESS_ISSUES=10000 pnpm test:stress to change its size. Cached
queries still enumerate refs to check freshness, but do not read event history.
Full validation and rebuilding are deliberately exhaustive in this POC.
