Skip to content

Working with diffs

The Main panel always shows a diff for whatever is selected in the Files panel. Everything on this page acts on that diff — the one currently on screen.

Additions, deletions, hunk headers and metadata are each tinted, and the +/- gutter stays pinned as the text scrolls sideways:

internal/svn/status.go
--- 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.

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.

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.

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.

] 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.

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 revision first 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. revision then runs svn 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.

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 found

Rejects 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.

] 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.

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.