Open sourcing tatr, a Git-native issue tracker
- YT :: https://www.youtube.com/watch?v=_hxdZtXTDSw
- Original title :: I finally Open Sourced my Project
Tsoding publishes tatr, a command-line issue tracker he has been building for himself for six months, then spends the session implementing an untag subcommand that strips a tag from every task matching a query. Along the way: why issues belong inside the Git repo, how to parse and losslessly reconstruct a task file, and a rant about SQL's DELETE FROM defaults.
What tatr is
The tracker lives in a tasks/ folder committed alongside the code. Each task is its own directory named by a UTC-timestamp ID, containing TASK.md — a title line, a short property block (status, priority, tags), and a free-form body. A task is a folder rather than a file because bugs want attachments: screenshots, logs, screencasts.
The motivation is migration. Moving a Git repo to another host is a git push to a new remote; moving the issue tracker with it is not possible on GitHub. Keeping issues in the repo makes the tracker migrate for free, and gives every task a full per-file history you can read with Magit. Timestamp IDs are chosen so tasks created on separate branches merge without collisions.
The tool is ~1500 lines of C but contains a parser, an interpreter, and a small compiler for TQL, its own query language. tatr ls filters tasks by TQL expressions; the language compiles straight to bytecode with no AST, and is evaluated per task on a small stack. tatr tracks its own issues with itself.
Design philosophy
tatr is a spec first and a tool second — the format is documented so anyone can write their own client. Tsoding declines feature requests that tags already cover: asked for a first-class scoped property, he points out that a scope tag does the job, and states the rule he applies before adding anything — can this workflow already be expressed with the mechanisms present? If yes, add nothing.
He also notes the feedback channel: GitHub notifications and email are both too noisy to read; the Discord server, which has moderation and vetting, is the reliable way to reach him.
Making the format lossless
The session's real work is round-tripping TASK.md. The parser knew about exactly three properties, and anything else would be destroyed on rewrite. The fix is to keep the whole unparsed property map on the task struct plus a body string view pointing at everything after the property block. All views point into the one owned task_md_content buffer, so freeing a task is a single free.
A render_task_md function reconstructs the file into a string builder: known properties (status, priority, tags) are re-emitted from their parsed form so in-memory edits show up, and unknown properties are copied through verbatim. Property order can be reshuffled because the store is a hash table — an acknowledged wart he leaves for later.
Debugging turned up a self-inflicted bug: he had stopped resetting the reused hash table, and stale entries leaked between tasks. Chat spotted the hash function call he had mistyped.
Error handling is worth noting: a malformed task is not a crash. It is loaded with a huge priority so it sorts to the top, and its title becomes the error message.
The untag command
tatr untag --tag scope <query> parses the tags to remove, compiles the TQL query, walks the matching tasks, removes every instance of each tag with an unordered remove, re-renders, and writes TASK.md back. Duplicated tags are allowed and all copies are removed. The command counts tasks actually modified, not tasks matched.
One deliberate divergence from ls: an empty query defaults to "match everything" for listing, but for a mutating command an empty query is an error. Which sets off a tangent on SQL — DELETE FROM table with no WHERE wiping the table is a design he wants to look the author in the eye about, and the "natural language for non-programmers" rationale behind SQL strikes him as the same reasoning that produced LLMs.
A shell-quoting wrinkle: TQL uses square brackets for grouping because parentheses are shell metacharacters, and because the shell's tokenization does not match the language's, tatr rejoins the argv with spaces and re-tokenizes the whole string itself.
Build-time provenance
A feature he added just before the stream bakes the commit hash, build time, and compiler version into the binary. The build script runs git and $CC --version, writes the results into a generated build.h, and the version output picks them up. The part he likes is the failure mode: if git is missing the build prints a warning and simply omits the hash via #ifdef rather than failing. The reason is a grudge — as a teenager he could not build a project he had received by email because its build ran svn to stamp a revision into the about dialog.
Aside: native UI
Noticing that in 2026 the only two options anyone considers for a UI are TUI and web, he argues raylib is the one viable native option and has a real shot at competing with Electron — comma.ai, a commercial company, rewrote its UI in raylib. GTK and Qt he considers overcomplicated for what they give you, though he grants they ship actual widgets while raylib only puts shapes on a screen cross-platform.