Reviewing
otisk's review model is one commit per change. You amend it rather than stacking fix-ups on top, and each push of it is kept as a revision, so nothing you pushed is ever lost. Concepts explains why it is shaped that way and how it differs from a pull request; this page is how to use it.
A change
From inside a clone:
otisk change hook # new commits get a Change-Id trailer
echo "a first idea" > idea.md
git add idea.md && git commit -m "Write down an idea"
otisk change push # git push origin HEAD:refs/for/main
otisk: change 1 revision 1 -- Write down an idea
To ssh://git.otisk.dev/yuritomanek/notes.git
* [new reference] HEAD -> refs/changes/1/1
Revise it by amending, and push again:
echo "a better idea" > idea.md
git add idea.md && git commit --amend --no-edit
otisk change push # revision 2 of the same change
otisk change list # open changes
otisk change diff 1 # what changed between revisions 1 and 2
otisk change land 1 # fast-forward main to revision 2
The Change-Id trailer is what makes an amended commit the same change, so
keep it when you amend. Landing only ever fast-forwards: if main has moved
on, rebase onto it and push a new revision first.
Several at once
A push may carry several commits, and each one becomes its own change, stacked in order:
git commit -m "Add a lexer" # on top of the change above
otisk change push # both commits, a change each
otisk: change 1: no new changes
otisk: change 2 revision 1 -- Add a lexer
The commits are reported bottom-up: the one already pushed has nothing new, and the one on top became change 2.
Amend any of them and push again, and each change gains a revision; nothing
new means nothing recorded. Land them from the bottom up — a change whose
parent has not landed is refused. If any commit in the push is unusable, for
example because it has no Change-Id, the whole push is refused and nothing
is recorded, naming the commit at fault.
Comments
Comments go on a change, or on a line of one of its revisions:
otisk change comment 1 --file idea.md --line 1 -m "is this the best idea?"
otisk change comment 1 -m "otherwise this looks fine" # about the change
otisk change reply 1 1 -m "probably not, but it is mine" # thread 1
otisk change comments 1
t1 open r2 idea.md:1
yuritomanek 2026-09-18 10:02
is this the best idea?
yuritomanek 2026-09-18 10:05
probably not, but it is mine
A comment names the revision and line it was written about, and stays there when you push a new revision, so it always points at what was actually read. Mark one dealt with, or bring it back:
otisk change resolve 1 1
otisk change comments 1 --all # resolved threads too
otisk change reopen 1 1
otisk change show 1 counts the threads still open, and otisk change list
marks changes that have any, so nothing is landed with a question outstanding
unless you mean it.
Comments live 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
All of it in one place
Everything above is also a terminal interface:
otisk ui # in a clone; or otisk ui --repo yuritomanek/notes
┌─ 1 yuritomanek/notes — open changes ─────────┐┌─ 3 revisions ────────────────────────────────┐
│> 1 open r2 main (1) Write down an idea ││ change 1 (open) → main │
│ ││ Change-Id: I3f2a9c7e1b4d8a60f5c2e9b17d4a8c3…│
│ ││> r1 b71d4e05c3a9 Write down an idea │
│ ││ r2 8f1c0b2a9d44 Write down an idea │
│ ││ │
└──────────────────────────────────────────────┘└──────────────────────────────────────────────┘
┌─ 2 local work — origin/main..HEAD ───────────┐┌─ 4 comments ─────────────────────────────────┐
│> new change Say why it is worth writing down││> t1 open r2 idea.md:1 │
│ ││ yuritomanek say why, not what │
│ ││ │
│ ││ │
└──────────────────────────────────────────────┘└──────────────────────────────────────────────┘
Four panels: the repository's changes and the commits you have not pushed yet
on the left, the selected change's revisions and conversation on the right.
Moving in the change list changes what the right-hand panels show, so reading
a change never costs you sight of the others. tab moves to the next panel
and 1–4 straight to one — the number each panel wears. The line below
names the panel the keys are in and what it offers, and ? shows every
panel's keys. Each panel scrolls when it holds more than fits, and says on its
border how much is out of sight.
local work is what git log <remote>/<branch>..HEAD holds, which is what
the panel is titled — the branch being the one the selected change targets, or
the repository's default, and fetched first so that landing something
elsewhere takes it off the list. Each commit is labelled with what pushing it
would do: start a new change, add a revision to change N, or nothing, because
it is already the latest revision, has landed, or has no Change-Id. p
pushes them for review, as one series, and what the host answers is what you
then see.
In changes, a lists every change, l lands, x abandons, and c
comments. In revisions, d shows the selected revision's diff and D
compares the last two — both in your pager, through git, returning when you
quit it. In comments, R replies and s resolves or reopens. Colour says
what a change's status is, and which threads are still open.
It does nothing the commands cannot: each keystroke is one of them, over one reused ssh connection, so a refusal reads exactly as it does in the shell.