checkout: honor core.symlinks in the dirtiness check (#7363) by AetherAI3 · Pull Request #7368 · libgit2/libgit2 · GitHub
Skip to content

checkout: honor core.symlinks in the dirtiness check (#7363) - #7368

Merged
ethomson merged 1 commit into
libgit2:mainfrom
AetherAI3:fix/7363-checkout-honor-core-symlinks
Oct 13, 2026
Merged

ethomson merged 1 commit into
libgit2:mainfrom
AetherAI3:fix/7363-checkout-honor-core-symlinks

Conversation

@AetherAI3

@AetherAI3 AetherAI3 commented Sep 7, 2026 •

Copy link
Copy Markdown

Fix for #7363.

What's happening

On a repo with core.symlinks=false (a filesystem without symlink
support, or a caller that asks libgit2 to materialize symlinks as
regular files via git_futils_fake_symlink in blob_content_to_link),
a GIT_CHECKOUT_SAFE that only needs to delete an unmodified
symlink entry fails with GIT_ECONFLICT / "N conflicts prevent
checkout" — while git_status on the same workdir reports fully
clean, and the git CLI does the same checkout without complaint.

The status/diff side already handles this: diff_file_content__load_workdir
looks up GIT_CONFIGMAP_SYMLINKS and treats a link entry as file
content when symlinks are unsupported. Only checkout.c's dirtiness
check never consulted that config — is_filemode_changed normalizes
S_IFLNK only for core.filemode=false, and the workdir plain file
that libgit2 itself wrote is compared against an S_IFLNK index/base
entry as if the mode had changed.

The same phantom conflict hits GIT_DELTA_MODIFIED (symlink whose
target text changed between branches) and GIT_DELTA_TYPECHANGE
(symlink ↔ blob/tree) — anything that flows through
checkout_is_workdir_modified with a link on either side.

The change

Add a respect_symlinks parameter to is_filemode_changed and pass
data->can_symlink at the three call sites in
checkout_is_workdir_modified. When !respect_symlinks, both sides
are normalized to GIT_FILEMODE_BLOB before comparison. The two
branches (respect_filemode and respect_symlinks) are independent —
respect_filemode=false still handles the executable bit as before,
and the new block only touches S_IFLNK.

+15 / −4, src/libgit2/checkout.c only.

How it was verified

Ran the reporter's self-contained C reproducer verbatim, against a
Debug build of main at 0551dfd:

  • Baseline: workdir status entries: 0 (clean),
    SAFE checkout B: rc=-13 message="1 conflict prevents checkout" —
    matches the report exactly.
  • With this change: workdir status entries: 0 (clean),
    SAFE checkout B: rc=0 message="", and the materialized
    AGENTS.md is removed. CLAUDE.md (unchanged blob) still evaluates
    as unmodified on the same path — verified with a stderr trace at
    is_filemode_changed showing the second call returns false the same
    way it always did (a=100644 b=100644 rs=0).

Reporter

Thanks to @liz7up — the report has the syscall/API-level trace and
the exact fix direction, so this could ship as a small change with
GIT_CONFIGMAP_SYMLINKS already plumbed through checkout_data.

AI assistance

I used an AI assistant to help draft the change, the reproduction and
this description. Every line was read, checked and run by me.

  • I have re-read every line and this description is in my own words.

When core.symlinks=false — a filesystem without symlink support, or
one where the caller wants libgit2 to materialize symlinks as regular
files via git_futils_fake_symlink (see blob_content_to_link) — a
SAFE-checkout that only needs to *delete* an unmodified symlink entry
was refused with GIT_ECONFLICT / "1 conflict prevents checkout",
while git_status on the same workdir reported clean.

is_filemode_changed already normalizes S_IFLNK when core.filemode=false
(the respect_filemode branch). The status/diff side already does the
right thing for symlinks via GIT_CONFIGMAP_SYMLINKS (diff_file.c
diff_file_content__load_workdir). Only checkout's dirtiness check
never consulted that config, so a workdir plain file (that libgit2
itself wrote) was compared to an S_IFLNK index/base entry and judged
modified.

Add a respect_symlinks parameter to is_filemode_changed and pass
data->can_symlink at the three call sites in
checkout_is_workdir_modified. When !respect_symlinks, both sides are
normalized to GIT_FILEMODE_BLOB before comparison, matching what
diff_file already does.

The same phantom conflict also hit GIT_DELTA_MODIFIED (symlink whose
target text changed between branches) and GIT_DELTA_TYPECHANGE
(symlink <-> blob/tree); those flow through the same
checkout_is_workdir_modified check and are fixed by this too.

Reproduction: the standalone C harness in the issue, or the sequence
git init && git config core.symlinks false && add a symlink entry via
update-index --cacheinfo 120000 && commit && drop it && checkout —
libgit2 1.8.0 (and current main pre-fix) returned rc=-13 with the
'1 conflict' message; the CLI git accepts the same workdir as clean.
With this change libgit2 rc=0 and removes the materialized file, and
the untouched valid-audio path still takes the scaled compare.

Reported-by: liz7up
Signed-off-by: Brandon Barrante <aetherai@aethersystems.net>
@ethomson

Copy link
Copy Markdown
Member

@ethomson
ethomson merged commit a138e01 into libgit2:main Oct 13, 2026
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.

3 participants