Comparing: prikk diff
prikk diff shows what changed between two states, where a state is a point in history (a ref or a
bare block id — see Reading a Point) or the worktree. It shows content: every text
change as unified hunks, the way diff -u prints them.
prikk diff # the worktree against the current branch: what `commit` would author
prikk diff --from <ref|block-id> # the worktree against that point
prikk diff --from <ref|block-id> --to <ref|block-id> # two points
prikk diff --path src/main.rs --path README.md # only these paths in the output
prikk diff --format json # diff-report-v1
There is no fourth shape: a lone --to is a usage error (exit 2), because it names a right side and no left
one. Compare two points with --from and --to, or a point with the worktree with --from alone.
What each side is
- A point is a ref — a received ref (
remotes/…) too, read and not adopted — or a bare block id.--from heads/main --to <block-id>and--from <block-id> --to <block-id>are both fine, and a ref and the block it points at give the same answer. - The worktree is what
prikk commitwould author, and the report is built fromcommit’s own rules so that it cannot disagree with the commit it predicts. Runprikk commitand thenprikk diff: it is empty again.
That has consequences worth knowing:
- A new file is
added, with its content. prikk has no index, andcommitauthors every worktree file.prikkignoredoes not exclude, so there is no separate “untracked” list here. A file thatprikk worktree-statuscalls untracked is anaddedentry inprikk diff. - Ignored paths never appear — as an entry or as anything else — and an ignore rule cannot hide a path that is already tracked: a tracked file that a later rule covers still shows its edits (Ignoring Worktree Paths).
- The left side of a bare
diffis the current branch’s tip with its queued commits folded on top, because that is whatcommitauthors against. Thefrom:line says so when commits are queued and not yet sealed:from: heads/main (block 7c1e…, plus 2 queued commits not yet sealed). In JSON the left side carriesqueued_patches.--from <point>compares against that point exactly, queue or no queue. - A fresh repository — a current branch that has never been sealed — has the empty state on the left:
every file
commitwould author isadded, and the exit code is0(from: heads/main (not published: the empty state)) — the current branch is never absent, published or not, so naming it explicitly with--fromrefuses no more than leaving--fromoff does. A queued, unsealed commit is where the two forms part ways, by the same rule as the previous bullet: the bare left side folds the queue on top of the empty state (a queued file reads as already there, unchanged);--from heads/maincompares against the empty state exactly, queue or no queue, so a queued file reads asaddedthere instead.--fromnaming any other ref that does not exist refuses. - It writes nothing. No lock, no cache, no index, no marker:
prikk diffreads. It works while another process holds the active lock, and while the worktree is provisional (materialized from a snapshot and not yet verified) or an interrupted materialization left its marker — states in whichcommitrefuses. It does not clear either marker. In an interrupted-materialization state, a file the interrupted write never created reads asdeleted, which is whatcommitwould have signed; that is the reasoncommitrefuses there.
Statuses
Each differing path has one status, in canonical path order. A path that is the same on both sides is not listed.
| Status | Meaning |
|---|---|
added | On the right only. |
deleted | On the left only. |
modified | A text file whose content differs. |
renamed | A declared rename: the same file at a different path (below). |
mode | Same content, different mode (the executable bit). |
binary | A binary file whose content differs — ids and sizes, never bytes. |
symlink | In the schema, not produced today: replay applies no symlink operation, so a history holding one fails the call, and a worktree symlink is an unsupported path (below). |
diff repository: /path/to/repo/.prikk
from: heads/main (block 7c1e…)
to: worktree
entries: 3
modified src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,7 +10,7 @@
…three lines of context…
- let old = 1;
+ let new = 2;
…three lines of context…
renamed docs/old.md -> docs/new.md
added notes.txt
--- /dev/null
+++ b/notes.txt
@@ -0,0 +1 @@
+a new file
Hunks, and patch
Text changes are unified hunks with 3 lines of context, headed by the ---/+++ lines patch(1) reads
(/dev/null on the side that has no file). A last line without a newline is marked
\ No newline at end of file, exactly as diff -u marks it, and a line ending in \r\n is a different line from
one ending in \n. Applying an entry’s hunks to its left content reproduces its right content byte for byte, on
Windows as everywhere; patch on Unix applies the printed text.
Renames are declared, never guessed
A renamed entry exists only where history (or a live prikk mv on the worktree side) declares a
rename. Deleting a file and creating another with the same bytes is a deleted and an added, never a rename:
nothing here infers one from similar content. A rename is the same file at a new path, so:
- a chain of renames
a → b → cis onerenamedentry fromatoc, whatever the two points are; - a file deleted and then created again — at its old path or at another — is a different file, so it is
never
renamed: at a new path it isdeletedplusadded, at the same path it ismodified; - a rename that also changes the content shows its hunks.
--path selects a renamed entry by either its old or its new path.
With --from <a point on another branch>, the worktree side takes node identity from the current branch’s
baseline, so a rename is paired only where the two agree: across branches, read a deleted plus an added as
“these two paths were not paired here”, not as “no rename was declared”.
Binary files
A binary entry shows the size of each side and an id — and never a byte. The prose names prikk cat for
reading a side, with the block each side resolved to rather than the name you gave (a ref may move before you
run the command). The worktree side has no block: it is the file itself. The left side of a bare diff with
queued, unsealed commits has no block either — it is the sealed tip plus that queue, a state no block names —
so no --ref line is printed for it; the prose instead says the side “can be read only after prikk seal”. For
a binary file on the worktree side, the id is the one the file would have once committed. content_id exists
for binary files only, exactly as it does in prikk tree: a text file is its own content.
Paths commit refuses
Some worktree entries commit cannot author: a symlink (Unix), a FIFO or socket, a name that is not a safe
repository path, an existing text file whose content is no longer valid UTF-8. prikk diff names each one,
once, with the refusal commit prints for it, and shows no content:
unsupported paths: 1
link: precondition not met: link: worktree symlink authoring is out of scope
A tracked file replaced by a symlink is named here and is not also reported deleted. The rest of the diff is
still shown. unsupported_paths is always empty between two points; only the worktree form fills it.
A live prikk mv that commit would refuse (both paths present, say) refuses the diff too, with commit’s own
message and nothing on stdout: there is no honest answer to what a commit that cannot happen would author.
The cost, and the bound on the line diff
Two points cost two anchored replays, as checkout --patch-plan does. The
worktree costs one read of the baseline plus a read of every worktree file, as worktree-status does, and
materializes the baseline text of the files that changed only. --path filters the output; it does not shrink
the work.
The line diff finds the shortest edit script within a work bound: 45,000,000 search steps for one file (a step is one diagonal visited or one line followed inside the search, so the bound counts work, never time, and is the same on every machine). Almost every diff finishes far inside it. Two large files that share many lines in very different orders — a file with its lines reversed, say — can reach it; the search then stops and the region it had not resolved is shown as deleted and re-added. The hunks are larger than the shortest, and they still apply: applying them reproduces the right side byte for byte.
You are always told. Every entry in --format json has a minimal boolean — true when its script is the
shortest, false when the bound engaged — and the prose puts a note: not the shortest edit script line under
that entry’s header. The same two inputs give the same output on every run and every machine.
--format json: diff-report-v1
{
"schema_version": "diff-report-v1",
"from": {"point": "heads/main", "target_block_id": "7c1e…", "queued_patches": 0},
"to": {"point": "worktree", "target_block_id": "worktree"},
"entries": [
{
"path": "docs/new.md",
"status": "renamed",
"from_path": "docs/old.md",
"from": {"kind": "file", "encoding": "text", "mode": 33188, "size": 812},
"to": {"kind": "file", "encoding": "text", "mode": 33188, "size": 830},
"minimal": true,
"hunks": ["@@ -3 +3 @@\n-old\n+new\n"]
}
],
"unsupported_paths": [{"path": "link", "refusal": "precondition not met: link: worktree symlink authoring is out of scope"}]
}
from/to—pointis how the side was named: a ref, a block id, orworktree.target_block_idis the block it resolved to; for the worktree it is the string"worktree"; for the current branch of a fresh repository (the empty state) it isnull.queued_patchesappears only on the implicit left side of a barediff: how many unsealed commits were folded on top of that block.entries— canonical path order.pathis the right side’s path (the left’s for a deletion);from_pathis present for a rename only.fromandtoare the fields atree-listing-v1entry carries (kind,encoding,mode,size, andcontent_idfor binary), emitted by the same code; the path is the entry’s own. A side is absent when the path is not there (addedhas nofrom,deletednoto).content_idappears for binary sides only.hunksis a list of strings, each starting at its@@line and ending in a newline; empty for a binary, mode-only or content-identical entry.minimalis always present.unsupported_paths— objects withpathandrefusal, in path order; empty between two points.- No
untrackedlist. A new worktree file is anaddedentry.
diff-report-v1 settles the format for diff and nothing else.
When diff refuses
- An absent ref, or a block id the repository does not hold — precondition, exit
1:ref heads/x does not exist in this repository,block <id> is not in this repository. An id naming a patch, not a block, refuses too. The current branch is never absent when it is the worktree’s baseline (--fromalone, or left off); named as one side of two points with--to, it refuses exactly like any other unpublished ref. - A value that is neither a ref name nor a block id, a
--paththat is not a repository-relative path, a lone--to, a repeated flag — usage, exit2. - An operation replay does not support, anywhere in either history — the whole call fails, and nothing is printed.
- A blob that is missing or no longer recomputes — fails the whole call as
integrity error: a comparison with a hole in it is not a comparison, so no entry is shown as unavailable (RFC 157 §5a). - A live rename declaration
commitwould refuse — the same messagecommitprints.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| The line diff is a shortest edit script within a counted work bound, and marks what it could not finish. | line_diff.rs |
| Each entry is classified by path first, and a rename pairs only the same declared node. | diff.rs |
The worktree side is read with commit’s own baseline, walk, ignore rules, declarations and refusals, and writes nothing. | worktree_read.rs, worktree_files.rs |
Each side of an entry is described by the function tree uses. | point_reading.rs |
The prose and diff-report-v1 are rendered from one report. | diff.rs |