Skip to content

Store .bat files verbatim so bot commits stop reintroducing line ending churn - #4803

Merged
xiang17 merged 2 commits into
microsoft:mainfrom
xiang17:xili9/gradle-line-ending
Jul 31, 2026
Merged

Store .bat files verbatim so bot commits stop reintroducing line ending churn#4803
xiang17 merged 2 commits into
microsoft:mainfrom
xiang17:xili9/gradle-line-ending

Conversation

@xiang17

@xiang17 xiang17 commented Jul 30, 2026

Copy link
Copy Markdown
Member

Problem

gradlew.bat keeps showing up as an 82-line diff on unrelated PRs. It has been
"fixed" before (#4761) and came straight back. Recent collateral: #4746 and #4802
both picked it up while changing something else entirely.

Root cause

.gitattributes marks *.bat as text eol=crlf. The text attribute is a contract
with two halves: LF in the repository blob, CRLF in the working tree. That contract
is enforced by the git client, inside git add / git checkout.

Dependabot never runs a git client. It pushes through GitHub's Git Data API, where the
bytes it POSTs become the blob verbatim, bypassing .gitattributes entirely. Gradle
generates gradlew.bat with CRLF, so those bytes land in the repo unnormalized.

The result is a file that is permanently modified in every clone — the working tree
can never match the index once the clean filter runs. git status hides this behind
stat caching until something forces a re-hash (git add -A, git stash, a tool
touching the file, git's racy-timestamp check on a fast CI checkout). Then it erupts on
whatever PR happens to be open.

History of the gradlew.bat blob shows a perfect correlation:

Blob Commit Author
CRLF #4780 Bump gradle-wrapper 9.5.1 → 9.6.1 dependabot[bot]
LF #4761 Normalize line endings human
CRLF #4710 Bump gradle-wrapper 9.4.1 → 9.5.1 dependabot[bot]
CRLF #4609 Bump gradle-wrapper 8.14.3 → 9.3.1 dependabot[bot]
LF #4345 Update Gradle to 8.14.3 Copilot
LF #3973 Update to Gradle 8.11.1 human

Every commit made with a real git client produced LF; every Dependabot wrapper bump
produced CRLF. Nobody chose LF — git converted on their behalf during git add.

It only fires when the bump crosses a Gradle release whose script template actually
changed. #4780 rewrote the scripts and drifted; #4776 (9.6.0 → 9.6.1) only touched
distributionUrl and didn't. That is why it feels random and why it will keep
happening on an unpredictable schedule.

#4780 shows the whole story in one commit: gradlew is a clean 4-line diff while
gradlew.bat is 164 lines. Same template change, same bot. The only difference is
that the .bat gets stored with line endings that violate its attribute, burying the
real 2-line change under 82 lines of conversion noise.

Fix

Mark .bat / .cmd as -text. Git then stores and checks out these files byte for
byte, with no conversion in either direction. There is no canonical form, so there is
nothing left to drift from — Dependabot, Copilot, Windows and Linux all agree by
construction. This is a permanent fix rather than another one-time cleanup.

Since -text checks out verbatim, every .bat blob must be CRLF or it would ship a
broken script to Windows. gradlew.bat already is (which is why it does not appear in
this diff); perf-tests/gradlew.bat is converted here.

.github/policies/resourceManagement.yml is the same bug from a different bot —
microsoft-github-policy-service[bot] committed it via the API in #3129 with CRLF,
violating the repo-wide eol=lf. It has been latently modified in every clone since
then and would eventually surface as unrelated churn on someone else's PR. Fixed here
in its own commit.

Why not the alternatives

  • Keep text eol=crlf and renormalize — that is Normalize line endings for gradlew.bat files #4761, which lasted three wrapper
    bumps.
  • A CI job or auto-fix step on Dependabot PRs — treats the symptom on a schedule and
    adds maintenance surface.
  • *.bat text !eol (CRLF only on Windows, LF in the index) — still requires LF in
    the index, which is exactly what Dependabot cannot honor, so the drift is unchanged.
    It would also make correctness depend on each developer's core.autocrlf.

What's in the diff

.gitattributes                            6 +-    <- the actual change
.github/policies/resourceManagement.yml 202 +-    <- line endings only
perf-tests/gradlew.bat                  164 +-    <- line endings only

366 of those lines contain no content change whatsoever. gradlew.bat is untouched and
byte-identical to main (8508ef684d). Reviewing with "Hide whitespace changes"
reduces this to the .gitattributes edit.

Verification

  • git ls-files --eol '*.bat' '*.cmd' → both files i/crlf w/crlf attr/-text
  • git diff --ignore-cr-at-eol main...HEAD → only .gitattributes has real content
    changes; everything else is line endings
  • git ls-files --eol | grep -E 'i/crlf|i/mixed' → only the two .bat files, which is
    now their intended state; resourceManagement.yml no longer appears, so no latent
    drift remains anywhere in the repo
  • git add --renormalize . produces no changes — the tree is a fixed point
  • git status clean
  • ./gradlew.bat --version and perf-tests/gradlew.bat --version both run on Windows
    and report Gradle 9.6.1

Risk

-text gives up the self-healing property: if some tool ever wrote a .bat with LF, git
would no longer silently correct it. In practice the only thing that writes these files
is Gradle's wrapper generator, which emits CRLF, and nobody hand-edits them.

Regressions are already covered — .github/workflows/build-common.yml runs
./gradlew assemble on windows-2022 (line 82) plus a Windows test matrix (line 142),
and ./gradlew resolves to gradlew.bat there, so an LF .bat fails on the first PR.

One gap worth noting: perf-tests/gradlew.bat is not exercised by CI (perf tests run on
ubuntu-latest), so a regression there would only surface when running the perf harness
on Windows.

xiang17 added 2 commits July 30, 2026 16:06
Dependabot commits wrapper updates through the GitHub API, which writes blob
bytes directly and bypasses .gitattributes eol normalization. Gradle generates
gradlew.bat with CRLF, so every wrapper bump that rewrites the scripts deposits
a CRLF blob that violates 	ext, leaving the file permanently modified in every
clone and erupting as an 82-line diff on unrelated PRs.

Marking .bat/.cmd as -text removes the canonical form entirely, so there is
nothing left to drift from. perf-tests/gradlew.bat is converted to CRLF since
-text checks out bytes verbatim.
Committed via the GitHub API by microsoft-github-policy-service[bot] in microsoft#3129
with CRLF, violating the repo-wide �ol=lf. Same root cause as the wrapper
scripts: the file has been latently modified in every clone since then and
would eventually surface as unrelated churn on someone else's PR.
@xiang17
xiang17 merged commit e38f3cf into microsoft:main Jul 31, 2026
290 of 292 checks passed
@xiang17
xiang17 deleted the xili9/gradle-line-ending branch July 31, 2026 02:28
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.

2 participants