Working with diffs
The Main panel always shows a diff for whatever is selected in the Files panel — or, once you compare revisions, in the Log panel. Everything on this page acts on that diff, the one currently on screen.
Reading a diff
Section titled “Reading a diff”Additions, deletions, hunk headers and metadata are each tinted, and the +/- gutter
stays pinned as the text scrolls sideways:
--- a/internal/svn/status.go (revision 42)
+++ b/internal/svn/status.go (working copy)
@@ -12,7 +12,9 @@ func Parse(r io.Reader) ([]Entry, error) {
dec := xml.NewDecoder(r)
var doc statusXML
- if err := dec.Decode(&doc); err != nil {
- return nil, err
+ if err := dec.Decode(&doc); err != nil {
+ return nil, fmt.Errorf("parse status: %w", err)
}
return doc.Entries(), nil
}
Scroll with J / K from any panel, or focus Main with 0 and use the movement keys. ← / → scroll a column at a time, and ^ / $ jump to the start and end of the line.
Directory diffs
Section titled “Directory diffs”Highlight a directory row and Main shows the combined diff of every change beneath it.
The root / row gives you the whole working copy as one patch.
This is on by default. Turn it off globally with
directoryDiff — a directory row then shows a
short hint instead — and press D to reveal one on demand for the current
session.
Selecting a group in the Changelists view works the same way: Main shows the combined diff of the whole list.
Reading a diff side by side
Section titled “Reading a diff side by side”s reopens the diff Main is showing as a two-pane comparison in a centered overlay.
- Each removal sits opposite the addition that replaced it; context lines appear on both sides.
- Every line is numbered in its own revision — the left pane in the base revision, the right in the working copy — and the pane labels name them.
@@hunk headers span the full width.- Both panes scroll together with the movement keys.
- A multi-file diff is paged one file at a time: [ and ] turn between files, and the bottom border names the open one along with the line range and page number.
- The layout behind dims out of the way while you read.
s or esc closes the overlay.
If Main has no diff to show — a directory row with directory diffs off, or a failed load — a toast says so rather than opening an empty overlay.
Saving a diff
Section titled “Saving a diff”w writes the diff on screen to a file. A prompt asks for the name and suggests a default; leaving it blank uses the suggestion.
| Selection | What is saved | Suggested name |
|---|---|---|
| A file | That file’s diff | path-to-file.diff |
| A directory | The combined diff beneath it | path-to-dir.diff |
The root / row |
The whole working copy | working-copy.diff |
| A changelist | The list’s combined diff | feature-x.diff, staged.diff, unstaged.diff |
The extension is added for you unless the name already ends in .diff or .patch, and
only the base name is used — a path in the prompt cannot write outside the output
directory.
Files land in diffOutputDir, which is created
if it does not exist. Empty — the default — means the working copy’s root, and a leading
~ is expanded to your home directory.
Comparing revisions
Section titled “Comparing revisions”Everything above reads the working copy. The Log panel compares what is already committed.
v picks the highlighted revision, and a second press lets it go. Two can be held at once; picking a third drops whichever was picked first, so the far end of a comparison can be moved without unpicking it each time. A dot marks each pick, and the status bar names them.
A pick is held by revision rather than by row, so it survives paging and filtering — the two ends of a comparison need not be on the same page. esc lets them go.
enter then compares them:
| Picked | What you see |
|---|---|
| One revision | What that commit changed, against the revision before it |
| Two revisions | The state at the older against the state at the newer |
Those two are not the same rule, and the difference matters. A range compares two snapshots, so it does not replay the older revision’s own change — that change is already in the left-hand side. Picking r10 alone shows what r10 did; picking r10 and r12 shows what r11 and r12 did to the tree r10 left behind. Pick r9 and r12 to include r10.
The order you pick them in does not matter: the diff always runs from the older revision to the newer.
Browsing the result
Section titled “Browsing the result”The Log panel becomes a tree of the files the range touched, titled with the range, and Main follows the cursor through it exactly as it does the Changes view:
| Row | Main shows |
|---|---|
The root / |
The whole range as one patch |
| A directory | Everything beneath it |
| A file | That file’s section alone |
enter folds a directory away, / filters the tree, and
w saves whatever is on screen — down to a single file’s section — under a
name built from the range, such as r120-r400-src-parser.go.diff. The patch has already
been read, so saving it costs no svn call however large the range.
The status code beside each row is read from the diff itself, so it says what the range did to the file — added, modified, deleted — rather than what the working copy holds now.
esc comes back out to the revisions, leaving the picks held for another look.
What is compared
Section titled “What is compared”The diff covers the directory displayFrom roots
the views at, so a file changed in the range but outside that directory does not appear.
Widen the scope with displayFrom: root, or point revision somewhere else with
P.
Because these files are history rather than the working copy, the keys that act on a working-copy file do nothing here — no staging, reverting, committing, updating or opening in an editor. w is the exception.
A comparison is remembered for the session. Revisions cannot change, so looking at one
again is instant and costs no svn call.
The Diffs view
Section titled “The Diffs view”] twice from the Changes view opens Diffs: the .diff and .patch
files already in diffOutputDir, newest first.
Selecting one shows its contents in Main, where / search and s side-by-side both work as usual. e opens the patch in your editor.
The view lists files, not working-copy changes — stage and revert do nothing here, and
w says the diff is already saved. d deletes the
highlighted patch file from diffOutputDir, after asking. It rescans on
R, on entering the view, after a successful save, and whenever
diffOutputDir changes.
Applying a saved patch
Section titled “Applying a saved patch”p in the Diffs view applies the highlighted patch to the source path
revision is showing, after asking for confirmation. It runs svn patch, so the changes
land as local modifications for you to review and commit as usual.
Two checks stand between the prompt and the patch, because svn patch on its own will
happily apply a patch in the wrong place — creating every target it cannot find and
rejecting the patch’s hunks into it:
- The patch must belong here. Every path in a patch is relative to the directory it was
produced in, so
revisionfirst looks for the files the patch expects to already exist. If none of them are in the source path, the patch was taken from another directory and is refused. - Some of it must land.
revisionthen runssvn patch --dry-run. If svn says nothing in the patch applies — every target missing, or every one conflicting — it is refused, since applying it would leave rejects and nothing else.
Partly applied patches
Section titled “Partly applied patches”A patch that only partly fits is still applied. svn patch takes the hunks it can and
writes the rest to a <file>.svnpatch.rej beside its target, leaving that file to be
finished by hand. The toast says so:
applied feature-x.diff to 3 files, 1 with rejects (.rej), 1 not foundRejects are per file, not per hunk: a file with any rejected hunk is counted there even
though its other hunks did go in. .rej files are svn-ignored, so they never appear
in the Changes view — the Rejects view is where you read them.
Finding rejects
Section titled “Finding rejects”] once more from the Diffs view opens Rejects: every .rej file
beneath the source path, as a tree. The search
is recursive, because a reject lands beside the file it failed to patch rather than in one
directory; .svn is skipped. Directories nest exactly as they do in the Changes view, and
enter folds one shut.
Selecting a reject shows its hunks in Main, where / search and s side-by-side both work as usual. m opens the reject against the file it was written for so you can take its hunks one at a time — see Resolving conflicts and rejects. e opens it in your editor to apply the hunks by hand instead, and d deletes it once you are done, after asking. A directory row has no file of its own, so it summarizes what sits beneath it.
The walk is deferred until you look: the view rescans on R, on entering it, after applying a patch, and after the source path changes — never while another view is on screen.
Searching inside a diff
Section titled “Searching inside a diff”Focus Main with 0, press /, and type. Matching lines are highlighted in place — nothing is hidden — and n / N jump between them. See Filtering & searching.
s, w and D are in the View section of the keybindings reference.