Skip to content

fix: show renamed files correctly for git and jj - #21

Open
jrosiek wants to merge 5 commits into
clabby:mainfrom
jrosiek:fix-git-renames
Open

jrosiek wants to merge 5 commits into
clabby:mainfrom
jrosiek:fix-git-renames

Conversation

@jrosiek

@jrosiek jrosiek commented Oct 7, 2026

Copy link
Copy Markdown

Summary

Renamed files were not shown correctly, for both git and jj:

  • git, edited rename: the old side was read at the new path, so it came up
    empty. Rendering then crashed with an out-of-range end_col, because the
    highlights still pointed into lines that were never loaded.
  • git, single revision (:Difft <rev>): rename detection compared the
    revision with the working tree instead of with its parent. It missed the
    revision's own renames and picked up unrelated renames from the working tree.
  • git, line counts: --numstat reports a rename as old => new (or
    dir/{old => new}), so the lookup by the new path failed and renamed files
    showed +0/-0.
  • git and jj, pure rename (no edits): difft reports the file as unchanged
    with no line alignment, so the view was empty.
  • jj, any rename: jj diff --tool difft gives difft the two revisions as
    whole directory trees. A renamed file reaches difft as a deletion of the old
    path and a creation of the new one, so its contents were never compared. The
    file showed as entirely added, with an empty old side.

Changes

  • The rename map is built before file contents are loaded, so a renamed file's
    old content is read from its old path. For a single revision, the rename map
    now uses the same rev^..rev range as the content diff.
  • --numstat entries for renames are keyed by their new path.
  • A file with identical content and no alignment is aligned line by line, so a
    pure rename shows its content, with no hunks.
  • For jj, each rename that jj diff --summary reports is diffed by running difft
    on the old and new contents directly. The two temporary files keep the new
    file's name, so difft detects the same language. If that run fails, the entry
    is kept as jj reported it.
  • tests/minimal_init.lua now adds the plugin to the runtime path by its
    absolute path. With a relative path, the library loader linked the built
    library through a broken relative symlink and silently fell back to a
    previously downloaded library.

Testing

  • Rust unit tests cover parsing --numstat renames, the jj summary brace
    forms ({a => b}, dir/{a => b}, {a => sub/a}), ignoring copies, rename-map
    arguments for a single revision, aligning a pure rename, and pairing jj
    renames. The tests that run difft skip when it is not installed.
  • New tests/rename_spec.lua runs the native library against real scratch
    repositories:
    • git: single commit, range, staged changes, moves within and across
      directories, a deletion next to an unrelated addition, and a working-tree
      rename that must not leak into a commit diff.
    • jj, both colocated with git and not: single revision, range, working copy,
      several renames in one revision, highlighting only the edited line, language
      detection, and copies kept as additions.
    • Each suite is skipped when the native library, difft or the VCS binary is
      missing.
  • Run against the library built from the current main, 26 of the 32
    end-to-end cases fail. The 6 that pass are guards that unrelated files are
    not paired.

Known limitations

  • Line counts for jj come from git, so they are available only in a repository
    colocated with git. This is unchanged.
  • jj copies (C in --summary) are still shown as plain additions.

🤖 Generated with Claude Code

…e revisions

difft reports only the new path of a renamed file, so the old content was
looked up at the new path and came back empty. With edits, the stale
highlights on the empty lines crashed rendering with an out-of-range
end_col. The rename map is now built before content lookup and supplies the
old path, while the status difft reported is kept so rows are built as a
change.

The rename map also passed a single revision straight to
`git diff --name-status`, which compares it against the working tree. It now
resolves the revision the same way as the content diff (`rev^..rev`).
…e revisions

Explain in the rename map why a single revision is resolved as `rev^..rev`:
the map decides where a renamed file's old content is read from, so it must
compare the same two sides as the content diff.
git --numstat keys a rename as `old => new` (or `dir/{old => new}`), so the
lookup by the new path missed and renamed files showed +0/-0. The numstat
parser now keys renames by their new path.

A pure rename comes from difft as unchanged with no line alignment, so it
rendered as an empty view. Identical content without alignment is now
aligned line by line and shown, with no hunks.
jj hands difft the two revisions as whole directory trees, so a renamed file
arrives as a deletion of the old path and a creation of the new one, and its
contents are never compared. The view showed the renamed file as entirely
added, with an empty old side.

For each rename that `jj diff --summary` reports, the old and new contents
are now diffed by difft directly, as a pair of files named like the new path
so the language is detected the same way. When that fails, the entry is kept
as jj reported it. git diffs are unaffected.
The tests build real git and jj repositories (jj both colocated with git and
not) and check the native library's output for renames: single revisions,
ranges, staged and working-copy changes, moves within and across
directories, several renames at once, and that unrelated deletions and
additions or copies are not paired. Each suite is skipped when the native
library, difft or the VCS binary is missing.

The test init now adds the plugin to the runtime path by its absolute path.
With a relative path, the library loader linked the built library through a
broken relative symlink and silently fell back to a downloaded library.
@jrosiek

jrosiek commented Oct 7, 2026 •

Copy link
Copy Markdown
Author

Disclaimer: shamelessly vibecoded. Manually tested with git only.

I’d appreciate it if you could let me know whether you accept AI-assisted contributions, including bug fixes and/or new features. I’m already making some changes for my own use on my fork, but I’d be happy to prepare them as contributions to the official repository as well (possible followup jrosiek#2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant