Subject: [PATCH 1/2] preload-index: respect --really-refresh override of assume-unchanged

From: "George Giorgidze via GitGitGadget" <[email-masked]>
Date: Tue, 26 May 2026 08:03:09 +0000

From: George Giorgidze <[email-masked]>

When refresh_index() is invoked with REFRESH_REALLY (e.g. via
"git update-index --really-refresh"), the documented behaviour is that
the "assume unchanged" bit on cache entries is disregarded so that
stale stat data on those entries is still refreshed.

The preload pass runs before the single-threaded refresh loop and is
intended to mark up-to-date entries quickly so the slow path only has
to deal with the leftovers. However, preload_thread() unconditionally
called ie_match_stat() with CE_MATCH_RACY_IS_DIRTY|CE_MATCH_IGNORE_FSMONITOR
and never with CE_MATCH_IGNORE_VALID, so it honoured the "assume
unchanged" bit. When a modified file's entry was marked
assume-unchanged, preload would conclude the entry was clean and call
ce_mark_uptodate(); the subsequent --really-refresh loop would then
skip the entry (because ce_uptodate(ce) is true) and never report it
as needing an update.

This only manifests when preload is active, so it has been latent in
default configurations. It is observable today via GIT_TEST_PRELOAD_INDEX=1.

Plumb the refresh flags through to the preload threads via a new
refresh_flags field on struct thread_data, and have preload_thread()
add CE_MATCH_IGNORE_VALID to its match options when REFRESH_REALLY is
in effect. Update refresh_index() to pass "flags & REFRESH_REALLY" to
preload_index() instead of a bare 0.

Add a regression test under t2106 that forces preload on and confirms
that "update-index --really-refresh" reports a modified
assume-unchanged entry as needing update.

Signed-off-by: George Giorgidze <[email-masked]>
---
 preload-index.c                          |  7 ++++++-
 read-cache.c                             |  2 +-
 t/t2106-update-index-assume-unchanged.sh | 11 +++++++++++
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/preload-index.c b/preload-index.c
index b222821b44..88bb486329 100644
--- a/preload-index.c
+++ b/preload-index.c
@@ -42,6 +42,7 @@ struct thread_data {
 	struct progress_data *progress;
 	int offset, nr;
 	int t2_nr_lstat;
+	unsigned int refresh_flags;
 };
 
 static void *preload_thread(void *_data)
@@ -60,6 +61,7 @@ static void *preload_thread(void *_data)
 	do {
 		struct cache_entry *ce = *cep++;
 		struct stat st;
+		unsigned int ce_option = CE_MATCH_RACY_IS_DIRTY | CE_MATCH_IGNORE_FSMONITOR;
 
 		if (ce_stage(ce))
 			continue;
@@ -87,7 +89,9 @@ static void *preload_thread(void *_data)
 		p->t2_nr_lstat++;
 		if (lstat(ce->name, &st))
 			continue;
-		if (ie_match_stat(index, ce, &st, CE_MATCH_RACY_IS_DIRTY|CE_MATCH_IGNORE_FSMONITOR))
+		if (p->refresh_flags & REFRESH_REALLY)
+			ce_option |= CE_MATCH_IGNORE_VALID;
+		if (ie_match_stat(index, ce, &st, ce_option))
 			continue;
 		ce_mark_uptodate(ce);
 		mark_fsmonitor_valid(index, ce);
@@ -150,6 +154,7 @@ void preload_index(struct index_state *index,
 			copy_pathspec(&p->pathspec, pathspec);
 		p->offset = offset;
 		p->nr = work;
+		p->refresh_flags = refresh_flags;
 		if (pd.progress)
 			p->progress = &pd;
 		offset += work;
diff --git a/read-cache.c b/read-cache.c
index 38a04b8de3..f5023b9a8b 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1520,7 +1520,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 	 * cache entries quickly then in the single threaded loop below,
 	 * we only have to do the special cases that are left.
 	 */
-	preload_index(istate, pathspec, 0);
+	preload_index(istate, pathspec, flags & REFRESH_REALLY);
 	trace2_region_enter("index", "refresh", NULL);
 
 	for (i = 0; i < istate->cache_nr; i++) {
diff --git a/t/t2106-update-index-assume-unchanged.sh b/t/t2106-update-index-assume-unchanged.sh
index 6b2ccc21a9..266c88d45d 100755
--- a/t/t2106-update-index-assume-unchanged.sh
+++ b/t/t2106-update-index-assume-unchanged.sh
@@ -24,4 +24,15 @@ test_expect_success 'do not switch branches with dirty file' '
 	test_grep overwritten err
 '
 
+test_expect_success '--really-refresh overrides assume-unchanged under preload' '
+	git reset --hard &&
+	test_commit really-refresh really-refresh original &&
+	git update-index --assume-unchanged really-refresh &&
+	printf "modified\n" >really-refresh &&
+	test-tool chmtime -100000 really-refresh &&
+	test_must_fail env GIT_TEST_PRELOAD_INDEX=1 \
+		git update-index --really-refresh >out 2>err &&
+	test_grep "needs update" out
+'
+
 test_done
-- 
gitgitgadget