Repository navigation
Conversation
mingw_lstat() uses GetFileAttributesExW(), and mingw_stat() and mingw_fstat() use GetFileInformationByHandle(), through get_file_info_by_handle(). Neither returns ChangeTime, which a later commit needs. In mingw_lstat(), use NtQueryFullAttributesFile() instead: it returns the same data as GetFileAttributesExW(), plus ChangeTime. The path is converted with RtlDosPathNameToNtPathName_U_WithStatus(), and a failed conversion is reported as ERROR_PATH_NOT_FOUND, as GetFileAttributesExW() does. On windows-2022 and windows-2025 runners, both return the same attributes, times, sizes and errors for 30 kinds of paths, including symlinks, junctions, locked and delete-pending files, UNC and \\?\ paths. In get_file_info_by_handle(), use GetFileInformationByHandleEx() instead, with FileBasicInfo for the times and attributes, and FileStandardInfo for the size. st_ctim still holds CreationTime. Both changes make the calls faster on the same runners: lstat() of a relative path takes about 10% less time, fstat() about two thirds less. Assisted-by: Claude Opus 5.5 Signed-off-by: Jason Zbakh <jzbakh@gmail.com>
Cloud file placeholders (OneDrive "Files On-Demand" and others) are reparse points, but Windows may hide that from lstat() and not from the FSCache: on Windows Server 2025, lstat() saw an in-sync placeholder as a regular file (attributes 0x20), and the FSCache as a reparse point (0x420). The next commit needs both to see the same attributes. Expose placeholders in every Git process with RtlSetProcessPlaceholderCompatibilityMode(), as the MSYS2 runtime does in its programs since Cygwin 3.5.2. This is a per-process setting: programs that Git starts are not affected. Where the function does not exist (before Windows 10 1803), nothing changes. Without the FSCache, lstat() now also reads the reparse tag of placeholders, as it does for other reparse points, to tell them from symlinks: they stay regular files and directories. `git status` on 2000 in-sync placeholders took 118 ms instead of 79 ms. With the FSCache, and for regular files, the times did not change. Assisted-by: Claude Opus 5.5 Signed-off-by: Jason Zbakh <jzbakh@gmail.com>
When a file is rewritten with the same size and its modification time is restored, only st_ctime can tell Git, as st_ino is always 0 on Windows. But st_ctim holds CreationTime, which does not change when a file is rewritten, nor when it is deleted and recreated shortly after (file system tunneling). `git status` does not show such changes. ChangeTime moves on such changes, but cannot simply replace CreationTime: Git for Windows versions without this change and libgit2 keep using CreationTime, so sharing a worktree with them would re-hash every file changed since its creation. FAT32 and exFAT also lack ChangeTime: depending on the API, it reads as 0, 1980-01-01 or LastWriteTime. On NTFS and ReFS, locally and over SMB, data writes set ChangeTime and LastWriteTime to the same value. ChangeTime gets later than both CreationTime and LastWriteTime only when the file changed without its LastWriteTime following: a restored or preserved modification time, an attribute, ACL or name change. Use ChangeTime in that case, and CreationTime otherwise, as before. In a worktree shared with older versions or libgit2, only such files are then re-hashed, each time one tool reads stat data written by the other. On FAT32 and exFAT, ChangeTime is never the latest time, so nothing changes there. Keep CreationTime for reparse points, such as cloud file placeholders (OneDrive and others), which the previous commit lets lstat() recognize: their ChangeTime moves when they are pinned, unpinned, hydrated or dehydrated. Using it would make the next `git status` re-hash them and download the dehydrated ones again or, if the sync client is not running, list them as modified. For reparse points, a change that keeps the size and mtime still goes unnoticed. Do this in the FSCache, in mingw_lstat() and in mingw_stat() and mingw_fstat(): the index and the untracked cache compare stat data from all of them. After upgrading, only files whose ChangeTime is already the latest time are re-hashed, once. From then on, metadata-only changes also cause a re-hash, as on Unix. core.trustCtime=false turns this off. This fixes git-for-windows#3707. Assisted-by: Claude Opus 5.5 Signed-off-by: Jason Zbakh <jzbakh@gmail.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This fixes #3707.
On Windows, only
st_ctimecan reveal a change that keeps a file's size and modification time, andst_ctimholdsCreationTime. That does not change when a file is rewritten, nor when it is deleted and recreated shortly after (file system tunneling), sogit statusmisses such changes.mingw_lstat()toNtQueryFullAttributesFile(), andmingw_stat()/mingw_fstat()toGetFileInformationByHandleEx(); both also returnChangeTime. No behavior change.RtlSetProcessPlaceholderCompatibilityMode(), as the MSYS2 runtime does in all its programs since Cygwin 3.5.2. Otherwise, Windows may hide that fromlstat()but not from the FSCache (on Windows Server 2025: attributes0x20vs0x420for an in-sync placeholder).ChangeTimeforst_ctimwhen it is later than bothCreationTimeandLastWriteTime, andCreationTimeotherwise, as before. Reparse points always keepCreationTime. On NTFS and ReFS, data writes setChangeTimeandLastWriteTimeto the same value, sost_ctimonly differs from today when a file changed without itsLastWriteTimefollowing: a restored or preserved mtime, an attribute, ACL or name change.What I tried first
ChangeTimeinstead ofCreationTime, or the latest of the three timesCreationTime, re-hash every file the other one recorded: 501 of 501 files changed after creation, 1500 of 1500 files rewritten by a branch switch onC:, and libgit2 at everystatus, as it does not write the index back. On FAT32 and exFAT,ChangeTimereads as 0, 1980-01-01 orLastWriteTimedepending on the API, and the latest of the three missed a changedCreationTime.ChangeTimewhen it is the latest time, for all filesChangeTime, but keep theirCreationTimeandLastWriteTime, when they are pinned, unpinned, hydrated or dehydrated. After "Free up space",git statusdownloaded 20 of 20 files again. With the sync client not running, it printederror: read error while indexing f0000: Invalid argumentfor each of them and listed them as modified.lstat()and the FSCache do not see the same attributes; without the FSCache, files were still downloaded.With this pull request: 0 files re-hashed in the first case, 0 downloaded and no errors in the second, the same as with Git for Windows 2.56.0. In a worktree shared with older versions or libgit2, only files whose
ChangeTimeis the latest time, for example after a save through a renamed temporary file, are still re-hashed, each time one tool reads stat data written by the other.Notes for reviewers
RtlDosPathNameToNtPathName_U_WithStatus()is linked directly. Microsoft describes it only in the Win32 dev notes, without a header or import library, and says to call it throughLoadLibrary()andGetProcAddress(). The first commit declares it and links it from ntdll: the MinGW-w64 and Windows SDK import libraries have it, including for the win+VS builds on x64 and arm64 (checks below), andgit.exestarts on every system I tested: Windows Server 2022 and 2025, Windows 11 24H2, Windows 11 on ARM64, Nano Server. I can load it at run time instead, as Git for Windows does forRtlGetLastNtStatus(). Without it,lstat()would need a handle per call to getChangeTime, which took more than three times as long.RtlSetProcessPlaceholderCompatibilityMode()is documented in the WDK (Windows 10 1803 and later). It changes what the whole Git process sees, not onlylstat(). I checked every place in Git that looks at reparse points: placeholders stay regular files and directories there. Programs that Git starts are not affected. Without the FSCache,lstat()now also reads the reparse tag of each placeholder, as it does for other reparse points:git statuson 2000 in-sync placeholders took 118 ms instead of 79 ms (medians of 7). With the FSCache, and on regular files, no difference. Windows 10 1709 has cloud placeholders but not this function: there, they may still be re-hashed or downloaded.CreationTime. For symlinks, cloud placeholders and other reparse points, a change that keeps the size and mtime still goes unnoticed, as today.ChangeTimemoves without changes would cause a re-hash on everygit statusthere. Data Deduplication and ProjFS placeholders (VFS for Git, covered by the same setting according to the phnt headers): a filter that reported different attributes or times in directory listings and in file queries would make the FSCache andlstat()disagree; WOF compression does not (see the checks). The OneDrive client on Windows 10: I tested it on Windows 11.Checks
NtQueryFullAttributesFile()vsGetFileAttributesExW(): same results for 30 kinds of paths (symlinks, junctions, locked and delete-pending files, UNC,\\?\, ...); timings; MinGW and MSVC;windows-2022andwindows-2025st_ctimis the same from all three code paths once files are closed (WinFsp: once its cache expires);ChangeTimeequalsLastWriteTimeafter data writeslstat()andstat()compute the samest_ctim, and Git finds the same stat data with and without the FSCache;windows-2022andwindows-2025CreationTime, attribute change, stable stat data; worktree shared with Git 2.56.0 and libgit2 1.9.7; the issue's reproduction onC:(tunneling); stat-related testscldfltdriver: files re-hashed and downloaded bygit status, this pull request vsChangeTimefor all files, with and without the FSCache; cost of exposing placeholdersChangeTimebut neitherCreationTimenorLastWriteTime(output below)git.exebuilt from the same source as this branch: https://github.com/jzbakh/git/actions/runs/41268097163lstat()andstat()ofgit.exesee of placeholders, by default and exposedwindows-2022andwindows-2025GIT_TEST_FSCACHE=1(t2081.2 and t4067.6 fail the same way onmain: https://github.com/jzbakh/git/actions/runs/41144597474)git.exeon Nano ServerOutput of the OneDrive test:
Git (git/git) has the same
compat/mingw.ccode; I can send that part upstream if you want.I wrote this with an AI assistant (Claude Opus 5.5) and reviewed every change.
result-20261101-121552.txt