Skip to content

feat(updating): propagate exec bit on new template files when core.fileMode=false#2640

Open
willemkokke wants to merge 1 commit intocopier-org:masterfrom
willemkokke:feat/intent-to-add-new-files-with-exec-bit
Open

feat(updating): propagate exec bit on new template files when core.fileMode=false#2640
willemkokke wants to merge 1 commit intocopier-org:masterfrom
willemkokke:feat/intent-to-add-new-files-with-exec-bit

Conversation

@willemkokke
Copy link
Copy Markdown
Contributor

Summary

Closes #2630. Follow-up to #2605.

#2605 fixed executable-bit propagation for files that already exist in the destination. Files introduced by the template for the first time on copier update were still affected when core.fileMode=false:

  1. Template v2 adds a new script.sh with 100755 in the index.
  2. Destination has core.fileMode=false (Windows default).
  3. copier writes the file, chmods it on disk (invisible to git with fileMode=false), then calls _sync_git_index_executable_bit — which short-circuited because the file is untracked.
  4. The user's next git add script.sh records 100644 because git ignores on-disk mode with fileMode=false. Exec bit lost.

Changes

copier/_main.py_sync_git_index_executable_bit:

For untracked files that should be executable, run git add --intent-to-add <path> to register an empty index entry, then fall through to the existing --cacheinfo path to set the mode to 100755. The user's eventual git add replaces the empty blob with the file's actual content while preserving the mode bit. The intent-to-add marker (A in git status) is also a more accurate signal than ?? for files that are deliberate template output.

  • Skipped when the file does not need an executable bit (the user's git add would record 100644 either way).
  • Skipped when core.fileMode=true (git picks up the on-disk chmod naturally — pre-staging would be inconsistent with copier's "leave changes unstaged for user review" behavior).

tests/test_updatediff.py — 3 new tests:

Test What it covers
test_update_introduces_new_executable_file[True|False] New +x file in template v2: index records 100755 on fileMode=false; file stays untracked on fileMode=true
test_update_introduces_new_non_executable_file[True|False] New non-exec file is NOT pre-staged in either fileMode case
test_update_introduces_new_executable_file_then_user_commits End-to-end on core.fileMode=false: copier update → git addgit commit → committed tree records 100755 and the file's actual content. The template setup uses git update-index --chmod=+x (rather than Path.chmod) so the test runs identically on every platform — Linux, macOS, and Windows.

Test plan

  • All new tests pass on macOS and windows
  • Full test suite still green on macOS (1073 passed)
  • ruff check, ruff format, mypy, editorconfig-checker all clean

…leMode=false

Files newly introduced by the template on `copier update` were not yet
tracked in the destination's index, so the existing
`_sync_git_index_executable_bit` short-circuited for them. With
`core.fileMode=false` (Windows default) the user's eventual `git add`
would record `100644` regardless of the template's intended mode,
silently losing the executable bit.

When an untracked file with desired exec bits is rendered into a
`core.fileMode=false` repo, copier now registers an intent-to-add
entry via `git add --intent-to-add` and rewrites its mode to `100755`
via `update-index --cacheinfo`. The empty blob is preserved, so the
user's eventual `git add` replaces it with the file's actual content
while preserving `100755`. The intent-to-add marker (`A` in `git
status`) is also a more accurate signal than `??` for files that are
deliberate template output rather than random untracked content.

When `core.fileMode=true`, copier still leaves new files untracked —
git would have picked up the on-disk chmod naturally, and pre-staging
would be inconsistent with copier's normal "leave rendered changes
unstaged for user review" behavior.

Closes copier-org#2630
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 96.47059% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.10%. Comparing base (e05ddfe) to head (88beeaa).

Files with missing lines Patch % Lines
copier/_main.py 66.66% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2640      +/-   ##
==========================================
- Coverage   97.15%   97.10%   -0.06%     
==========================================
  Files          60       60              
  Lines        7101     7185      +84     
==========================================
+ Hits         6899     6977      +78     
- Misses        202      208       +6     
Flag Coverage Δ
unittests 97.10% <96.47%> (-0.06%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

copier update loses executable bit on NEW files introduced by the template when core.fileMode=false

1 participant