Skip to content

Skip untracked symlinks-to-directories in the review diff - #63

Merged
adamw merged 3 commits into
masterfrom
fix/untracked-symlink-review-diff
Aug 2, 2026
Merged

Skip untracked symlinks-to-directories in the review diff#63
adamw merged 3 commits into
masterfrom
fix/untracked-symlink-review-diff

Conversation

@adamw

@adamw adamw commented Aug 2, 2026

Copy link
Copy Markdown
Member

An untracked symlink pointing at a directory aborted the whole review loop.

git status --porcelain -uall reports such a symlink as a single ?? linkdir
entry — git does not recurse into it. git diff --no-index -- /dev/null linkdir
then follows the link, finds a directory on one side and a file on the other,
and tries to pair /dev/null with linkdir/null:

error: Could not access 'linkdir/null'

exit 1, message on stderr. OsGitTool.untrackedFileDiff classifies that
correctly — its exit 1 && empty stderr predicate means "the sides differ", and
this is exit 1 with stderr — so it calls fail, which throws
OrcaFlowException. One such symlink anywhere in the working tree took down
reviewDiff() and with it the review loop.

The fix

The predicate is untouched. It was tightened deliberately in #46, and it is
correct: before that, exit 1 was unconditionally read as "the sides differ",
which swallowed a real failure — from a subdirectory, git diff --no-index -- /dev/null sub/newfile.txt also exits 1 with an access error, and every new
file's contents were vanishing while reviewDiff() reported success. Loosening
it would restore that bug. The symlink case is the same signal from a different
cause, so the fix is to stop asking git to render the path at all.

untrackedFileDiff now short-circuits a symlink that resolves to a directory
and emits a line naming it instead. It is the single shared path — both
reviewDiff() and pendingChanges() reach it through withNewFileContents
so both are fixed once.

What was measured, not assumed

Against git 2.53.0, for each kind of untracked symlink:

symlink to exit stderr stdout
a directory 1 error: Could not access 'linkdir/null' empty
a file 1 empty new file mode 120000, link target as content
nothing (dangling) 1 empty new file mode 120000, link target as content

Only the directory case is broken. git never follows a symlink to a file — it
renders the link itself as a mode-120000 blob holding the target path.

Only directory symlinks are held back, not all symlinks: a symlink to a file
or to nothing already renders correctly and is a real change the reviewer should
see, so dropping those would trade one silent omission for another.

Skipped symlinks are announced, not silently omitted, as a
# skipped <path>: symlink to a directory line. A reviewer who believes it saw
everything is worse than one told something was skipped — the same reason
CommitDiff marks a truncated diff. It also keeps the reviewer able to flag a
newly created symlink at all, which matters given this repo already treats
symlinks in the working tree as a surface worth refusing (OrcaDir,
ProgressScan).

untrackedPaths() is deliberately left alone, so pendingChanges().newFiles
still lists the symlink — verified that git add -A does commit it, as a
mode-120000 blob, which is exactly what that list promises.

Tests

Two in OsGitToolTest, each mutation-checked to fail alone.

The abort. A repo with an untracked symlink-to-directory alongside an
ordinary new file. Replacing the guard condition with false fails exactly this
test, with orca.OrcaFlowException: git diff --no-index -- /dev/null linkdir failed (exit 1): error: Could not access 'linkdir/null' — the reported error
verbatim.

How narrow the filter is — deliberately not a test of the fix, and not
redundant with the one above. It pins that a symlink to a file still renders
as its own mode-120000 diff. Widening the guard to os.isLink(path) alone fails
exactly this test and nothing else; without it, that widening would silently
swallow every file-symlink's diff — the same "reviewer told nothing was there"
failure this PR exists to prevent. Please don't delete it as a duplicate of the
directory test; it guards the decision, not the bug.

Sequencing vs #59

Branched off master, not off #59. #59 (review-change-set-since-stage-base)
adds a since parameter and touches reviewDiff, withNewFileContents and the
trait scaladoc in the same file, but does not touch untrackedFileDiff or
untrackedPaths — the two changes are independent. Whichever merges second
picks up a small conflict in the reviewDiff scaladoc block and in
withNewFileContents's signature; both resolve by keeping both edits. No
ordering is required either way.

Known adjacent defect, not fixed here

The identical abort fires for an untracked nested git repository: git status -uall does not recurse into one either, reporting ?? nested/ (with a
trailing slash), and git diff --no-index -- /dev/null nested/ fails the same
way with error: Could not access 'nested/null'. Verified on git 2.53.0. It is
a different cause with a different fix and a different message, so it is left
for its own change rather than widened into this one.

adamw added 3 commits August 2, 2026 16:07
`git status --porcelain -uall` reports a symlink to a directory as a single
`?? link` entry — it does not recurse. `git diff --no-index -- /dev/null link`
then walks into the link and pairs `/dev/null` with a path inside it, exiting 1
with `error: Could not access 'link/null'`. `untrackedFileDiff`'s predicate
correctly reads that as a failure rather than "the sides differ", so it threw,
and one untracked symlink-to-directory anywhere in the working tree took down
`reviewDiff()` and with it the whole review loop.

Guard the call instead: a symlink resolving to a directory is named in the
payload rather than handed to `--no-index`. Symlinks to a file or to nothing
render fine — as their own mode-120000 new-file diff carrying the link target —
so they still go through unchanged.
The skip's `&& os.isDir` conjunct survived mutation: widening it to every
symlink would silently replace each file-symlink's mode-120000 diff with a skip
line, and nothing would go red. This test guards how narrow the filter is,
rather than the abort it fixes.
@adamw
adamw merged commit f2ccc8b into master Aug 2, 2026
6 checks passed
@adamw
adamw deleted the fix/untracked-symlink-review-diff branch August 2, 2026 19:08
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