How to think about TouchQuill
Before you type your first command, three ideas are worth internalizing. The rest of these docs keep coming back to them.
1. Content has an address, not a name
Every file, directory tree and commit is stored under an address computed from its content (a BLAKE3 hash). The consequences are practical: an identical texture used in two places is stored once; the server only sends objects you don't already have; and if anything gets corrupted in transit, the client notices because the hash won't match. You don't have to take integrity on faith โ you can compute it.
2. You don't merge binaries โ you take them
Textures, meshes and blueprints have no meaningful merge. Rather than pretend otherwise, TouchQuill builds locking into the core: whoever holds the lock commits; the server rejects a push touching a locked file from anyone else. The queue means "waiting for a file" no longer involves pinging people on Slack.
3. The change matters more than the commit
A commit is a technical record; the logical change (Change ID, ch-โฆ) lives longer โ it survives fix-ups (amend), message edits and rebases. Reviews, locks and history refer to something stable. On top of that sits the operation log: almost anything can be undone with tq undo, including recovering unsaved work after an accidental restore.
Install and first commit
The client is a single tq binary (Linux, Windows, macOS). You only need the server โ tq-server โ once you want to share work.
# Linux: rpm package ยท Windows: installer ยท or build from source: cargo build --release -p tq-cli
tq init MyGame echo "asset" > Content/hero.txt tq commit -m "first asset" tq log # ch-4f2a91 rev:1 first asset (you) tq status # what changed since the last commit
Made a mistake? Nothing is lost:
tq restore # back to the last committed state tq undo # ...and if restore itself was the mistake โ get your work back
undo can bring back even uncommitted files.Working with a team
tq clone https://vcs.studio.com MyGame # fresh clone tq login tqt_xxxxx # token from your admin tq push tq pull
Your identity on the server comes from the token, never from what the client claims โ which is why nobody can impersonate a teammate in history or in the audit log. Push is always fast-forward: if someone got there first, pull first.
Workflow: an artist's day
Morning: fresh state
Start by pulling the team's changes. If the project uses build flags, you can grab the last stable revision instead of the very latest โ one that's guaranteed to open in the editor.
tq pull # or: tq get stable # last revision with green CI
Take the file
Before you open a binary in the editor โ lock it. If someone's already on it, you join the queue and get notified the moment it's yours.
tq lock Content/Characters/Hero.uasset
โ Lock acquired.Work and save
Commit in small steps โ commits are cheap (milliseconds even with large files). If something goes wrong, tq undo reverses the operation.
tq commit -m "new hero armor"
Ship and release the lock
After pushing, unlock โ the next person in the queue gets the file automatically, notification included.
tq push tq unlock Content/Characters/Hero.uasset
Interrupted mid-work?
Don't force-commit an unfinished change โ shelve it. You can also share the shelf with a teammate for a second pair of eyes.
tq shelf create -m "armor wip" --expires 48h
Workflow: a programmer's task
A channel per task
A task channel inherits from main, but your commits don't affect anyone until you're done.
tq channel create task/GROM-447 --type task tq channel switch task/GROM-447
Commit freely, fix boldly
amend folds a fix into the last commit, reword edits the message. The Change ID stays the same, so nothing gets lost.
tq commit -m "dodge system"
tq amend # fix-up on the same changeStay current with main
integrate pulls in the parent's changes. A conflict won't stop you โ it gets recorded, and you decide when and how to resolve it.
tq integrate
tq conflicts # what's pending
tq conflict resolve cfl-a1 --take theirsClose the task
complete merges the channel into its parent and closes it. The task's history stays readable.
tq complete
Workflow: lead โ releases and order
A release channel with strict rules
No commits with unresolved conflicts land on a release, and only a chosen few can push. The policy lives in a versioned file โ changing it goes through history like everything else.
tq channel create release/1.0 --type release
# tq-channel.toml:
[permissions]
write = ["lead", "server-admin"]
integrate = ["lead"]Lock disputes
Someone left a lock and went on vacation? Request a hand-over, or take it if you must โ every such action lands in the audit log, with the reason.
tq lock --request Content/Boss.uasset
tq steal Content/Boss.uasset # requires lock.stealConflicts under control
Conflicts can be assigned to people and given deadlines. Overdue ones escalate on their own โ nothing drowns in the noise.
tq conflict assign cfl-a1 marta tq conflict defer cfl-b2 --deadline 48h
Who did what
The audit log records administrative actions and permission-denied attempts. Verifying its integrity is one command.
tq-server audit log --denied tq-server audit verify
Workflow: CI and builds
An account with exactly the rights it needs
CI gets a PAT token limited to specific permissions โ even if it leaks, it can't do anything beyond its scope.
tq-server user pat ci-build --user ci --scopes repo.read,build-flag.set
Build and mark
After a successful build, CI flags the revision. That flag means "this version works".
tq build-flag set --rev rev:128 --flag ci --state ok
The team takes stable
An artist never has to land on a programmer's broken commit: get stable fetches the newest revision with all required flags green.
tq get stable
Channels
A channel is a line of work โ the counterpart of a branch, but with a type and a policy. The type carries sensible defaults: a release rejects conflicted commits, a task has an owner and a lifecycle (create โ integrate โ complete), personal is your sandbox.
The virtual channel is worth knowing: a view onto a slice of the repo. Your art team can get just Content/ โ as far as they're concerned the code doesn't exist, yet their commits land in shared history with the invisible files preserved.
tq channel create artview --type virtual --include "Content/..." --exclude "Source/..."
Locks
The model is simple: one owner, everyone else queues. Locks have an inactivity timeout (4 h by default) โ if you forget to release one and go home, the team isn't blocked overnight. You can ask the owner to hand a lock over (--request) or pass yours to a specific person (--give).
The server enforces locks at push: if the channel policy says *.uasset requires a lock, a push touching such a file without one is rejected with a clear message. Offline work is covered too โ declare a lock locally, sync when you're back; if two people took the same file offline, a dispute blocks both sides until a lead decides. Nobody silently overwrites anyone's work.
Conflicts
In most systems a conflict is a wall: the merge stops and you drop everything to defuse it. In TouchQuill a conflict is a stored object (cfl-โฆ) with full context: both versions, the common ancestor, who and when. Integration always completes; you resolve conflicts when you have room for them โ or a lead delegates them.
tq conflict resolve cfl-a1 --take theirs # accept their version tq conflict resolve cfl-a1 --merge # three-way merge via plugin (if the format has a differ)
Change ID
A commit hash changes with every fix-up โ a Change ID doesn't. The distinction sounds subtle, but in practice it means a review comment saying "fix this in ch-4f2a91" stays valid after your amend and rebase. The operation log (tq op log) completes the picture: every operation leaves a trace and most can be undone.
Lens
Classic blame answers "who changed line 40 of file X". Lens answers the question you actually have: "what happened to this function?" โ even when the file was renamed and the function moved between modules. Cosmetic edits (formatting, comments) don't pollute the result.
tq lens blame --symbol calculate_damage
CREATED ch-6f9 rev:1 krzysztof "first version"
MODIFIED ch-a12 rev:3 marta "damage buff"
MOVED ch-c73 rev:5 krzysztof (Weapon.rs โ Combat.rs)
Best practices
A few habits that make the biggest difference in practice:
- Lock before opening, not before saving. The most common way to lose work is an hour of editing a file someone else holds. Make it a reflex: binary file โ
tq lockfirst. - Enforce locks with policy, not culture. Put
require-lock-for = ["*.uasset", "*.fbx"]intq-channel.toml. Human memory fails; the server doesn't. - Commit often โ it costs nothing. With content-addressed storage a commit takes milliseconds even in a big repo, and every commit is a point you can return to.
- Shelve instead of "wip" commits. Unfinished work doesn't have to dirty the channel history. Shelves expire on their own and can be shared for review.
- One task โ one task channel. Small channels integrate easily. A multi-week omnibus will hurt in any version control system.
- CI sets flags, the team runs
get stable. The cheapest way to make sure artists never land on a broken build. - Assign or defer conflicts โ don't leave them hanging. With
assign/deferevery conflict has an owner and a deadline, and escalation kicks in by itself. - Turn on fsmonitor on dev machines.
tq fsmonitor startonce per session and status/commit stop depending on tree size. Works on Linux and Windows.
Migrating from Git and Perforce
Both migrations preserve history: authors, timestamps, messages. Change IDs are derived deterministically from the source, and Lens works on imported history from day one. A sensible order: migrate a copy, verify it, switch the team over, and keep the old repo read-only for a transition period.
tq migrate from-git --repo /path/to/repo --branch main tq migrate from-p4 --port perforce:1666 --user krzysztof --path //depot/... tq migrate verify --repo /path/to/repo
Server and permissions
The server exposes gRPC (clients), REST (integrations) and SSE (a live event feed). Authentication switches on with the first user you add โ before that it runs in open mode, handy for trying things out.
tq-server --data /var/tq --listen 0.0.0.0:7470 --tls-cert cert.pem --tls-key key.pem tq-server user add krzysztof --role developer
Seven built-in roles (from viewer to server-admin), custom roles cloned with permissions subtracted, path restrictions for contractors ("only Content/Weapons/..."), scoped PATs for automation, and per-channel overrides. Add regional replication (locks stay on the primary โ one source of truth), server-to-server mTLS, and one-command backups.
Command reference
| Command | Description |
|---|---|
tq init / clone / setup | create / clone / configure a repo |
tq add / edit / status / diff | worktree changes |
tq commit / amend / reword | record a revision (stable Change ID) |
tq log / show / cat / restore | history and navigation |
tq rebase / revert | rebase / inverse revision |
tq op log / undo / redo | operation log and undo |
tq channel โฆ | create / switch / list / policy |
tq integrate / complete | integrate and close a channel |
tq lock / unlock / locks | queue-based locks, request / give |
tq conflict(s) โฆ | resolve / assign / defer |
tq shelf โฆ | shelve work, share, TTL |
tq lens blame / symbols | symbol history |
tq build-flag / get stable | build flags and the stable revision |
tq workspace โฆ | save / sync / offline / mount / presence |
tq migrate from-git / from-p4 | history import |
tq fsmonitor start / stop / status | file-change daemon (Linux / Windows / macOS) |
tq notify โฆ | inbox, DND, digests |
tq plugin โฆ | WASM plugins: extractor / differ |
tq admin obliterate / channel | permanent content removal, permission overrides |
The technical specification and architecture decision records live in the repository.