Concepts
otisk is a git host, and everything you know about git still applies. What differs is the layer above: how a piece of work is proposed, discussed and accepted. If you have used GitHub or GitLab, roughly half of what you expect is here under a different name, and the other half is deliberately absent.
If you have used Gerrit, you already know this model. otisk borrows it wholesale, because it is the one that treats a commit as the unit of review.
The short version
| GitHub or GitLab | otisk |
|---|---|
| A pull request, holding a branch of commits | A change, which is one commit |
| Push more commits to fix review comments | Amend the commit and push again |
| Force-push, and the old version is gone | Each push is a revision, kept forever |
| Merge, usually making a merge commit | Land, which only ever fast-forwards |
| Stacked PRs, and the tooling to survive them | A push of several commits, each its own change |
| Comments in the forge's database | Comments in the repository, as git objects |
| An account, a password, a token, a session | Your SSH key, and nothing else |
| A web interface | The command line, and a terminal interface |
A change is a commit
On GitHub, the unit of review is a branch: you make a branch, put commits on it, open a pull request, and the reviewer reads the diff of the whole branch. The individual commits on it are usually noise — "fix typo", "address review" — and get squashed away at the end.
In otisk, the unit of review is one commit. You write it, push it for review, and when the reviewer asks for something you amend that commit and push it again. There is no branch to name, no pull request to open, and no squash at the end, because there was never anything to squash.
git commit -m "Write down an idea"
otisk change push # this commit is now change 1
What makes the amended commit the same change is a Change-Id trailer in
its message, added by a hook. The commit's SHA changes every time you amend;
the Change-Id does not. That trailer is the whole trick.
Nothing you pushed is lost
Amending rewrites history, which on GitHub means force-pushing, which means the version your reviewer read yesterday is gone — and the comments they left point at line numbers in a diff that no longer exists.
Here, every push of a change is kept as an immutable ref:
refs/changes/1/1 what you pushed first
refs/changes/1/2 what you pushed after the review
refs/changes/1/3 ...
Those are called revisions, and they are ordinary git refs you can fetch,
diff and check out. otisk change diff 1 shows what changed between the last
two — a range-diff, so you see how the change changed, not what the change
does.
A comment records the revision and line it was written about, so it keeps pointing at what its author actually read.
Landing is a fast-forward
Merging a pull request usually makes a merge commit, or squashes, or rebases,
depending on which button someone pressed. main ends up with a history that
is some mixture of what happened and what the tooling did.
Landing a change fast-forwards the branch to the change's latest revision. If
the branch has moved on, the land is refused, and you rebase and push a new
revision. That is the only thing landing does. The result is that main is a
straight line of reviewed commits, each of which is a change somebody read.
Stacks are ordinary
Stacked pull requests are painful everywhere: the second PR's diff includes the first, and rebasing one rewrites the others.
Here a push may carry several commits, and each becomes its own change:
git commit -m "Add a lexer" # on top of the change before it
otisk change push # both commits, a change each
Each is reviewed on its own, and they land bottom-up — a change whose parent has not landed is refused. Amending any of them and pushing again gives that change a new revision and leaves the others alone.
The conversation is in the repository
On GitHub, your code is in git and your review is in a database you cannot clone. Export it and you get JSON, if the API still exists.
otisk keeps comments in the repository, as a commit per comment on
refs/changes/<number>/meta. They are stored, replicated and restored exactly
as your code is, and you can read them with git alone:
git fetch origin refs/changes/1/meta:refs/otisk/meta
git log refs/otisk/meta
The database has an index of them, which exists to answer questions quickly. It is a fold of the log and can be thrown away and rebuilt.
Your key is your account
There is nothing to log into. Authentication is the SSH key you already push with: the host recognises its fingerprint and knows who you are. No password, no token, no session, no OAuth app, no second factor to lose.
The consequence is that someone with access to the host must add your key before you can do anything — there is no self-service sign-up, because there is no sign-up.
Disk is a cache
This one is invisible in use, and shapes everything underneath. A repository lives in an object store; the copy on the server's disk is a cache that can be deleted at any moment. A repository reaches disk only by being hydrated from durable storage.
So a server that has never seen your repository is a normal state rather than a disaster, and losing the disk is not an outage. How it works has the detail.
What you give up
Honestly, a lot:
- No web interface. No browsing code, no reading diffs in a browser, no sending someone a link to a line.
- No issues, no wikis, no project boards, no discussions.
- No CI. Nothing runs when you push.
- No forks and no pull requests across repositories. Review happens in the repository the work is for.
- No organizations, teams, or fine-grained permissions. There are accounts, and repositories they own.
- Almost nobody else uses this. Gerrit's model is well proven; this particular implementation of it is small, new, and maintained by one person.
What you get for that is a review model where a change is one reviewable commit, nothing you pushed is ever lost, history is a straight line, and the whole conversation is in git — plus a server whose durability story is "the object store is the truth" rather than "do not lose the disk".
If that trade is not obviously worth it for you, it probably is not.