Skip to content

Commit 0a572e5

Browse files
committed
unpack-trees: retain clean proofs across policy updates
Configured pulls preserve FSMonitor clean proofs when checkout can authenticate every index change. Tracked policy files were an exception: adding or replacing .gitattributes or .gitignore made the generic semantic transfer reject the whole proof. Later read-only status commands then had to rescan the worktree and could not restore the paired untracked proof. Let checkout retain history across regular policy-file changes that it writes itself. Attribute changes refresh the worktree manifest before the provider boundary is rebound, and fail closed if that refresh cannot authenticate the new sources. Keep the existing untracked-cache invalidation for ignore changes, and transfer that cache only while the full tracked proof remains current. Exercise configured fast-forward pulls in main and linked worktrees. A required-filter control also verifies that changed attributes invalidate the affected tracked entry instead of certifying it.
1 parent 4022f0d commit 0a572e5

4 files changed

Lines changed: 189 additions & 16 deletions

File tree

clean-status-history.c

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,14 +2018,65 @@ int clean_status_transfer_current_proof_if_same_index(
20182018
return replace_current_fsmonitor_proof(dst, src);
20192019
}
20202020

2021-
int clean_status_transfer_current_proof_if_semantically_same_index(
2022-
struct index_state *dst, const struct index_state *src)
2021+
enum checkout_policy_change {
2022+
CHECKOUT_POLICY_NONE = 0,
2023+
CHECKOUT_POLICY_IGNORE = 1 << 0,
2024+
CHECKOUT_POLICY_ATTRIBUTES = 1 << 1,
2025+
};
2026+
2027+
static enum checkout_policy_change checkout_policy_change_kind(
2028+
const struct cache_entry *old, const struct cache_entry *new_entry)
2029+
{
2030+
const struct cache_entry *entry = old ? old : new_entry;
2031+
const char *base;
2032+
2033+
if (!entry ||
2034+
(old && (!S_ISREG(old->ce_mode) || ce_stage(old) ||
2035+
ce_skip_worktree(old) || ce_intent_to_add(old) ||
2036+
(old->ce_flags & CE_VALID))) ||
2037+
(new_entry && (!S_ISREG(new_entry->ce_mode) || ce_stage(new_entry) ||
2038+
ce_skip_worktree(new_entry) ||
2039+
ce_intent_to_add(new_entry) ||
2040+
(new_entry->ce_flags & CE_VALID))) ||
2041+
(old && new_entry &&
2042+
(strcmp(old->name, new_entry->name) || old->ce_mode != new_entry->ce_mode)))
2043+
return CHECKOUT_POLICY_NONE;
2044+
base = strrchr(entry->name, '/');
2045+
base = base ? base + 1 : entry->name;
2046+
if (!fspathcmp(base, GITATTRIBUTES_FILE))
2047+
return CHECKOUT_POLICY_ATTRIBUTES;
2048+
if (!fspathcmp(base, ".gitignore"))
2049+
return CHECKOUT_POLICY_IGNORE;
2050+
return CHECKOUT_POLICY_NONE;
2051+
}
2052+
2053+
static int record_checkout_policy_change(
2054+
enum checkout_policy_change *policy_changes,
2055+
const struct cache_entry *old, const struct cache_entry *new_entry)
2056+
{
2057+
enum checkout_policy_change change =
2058+
checkout_policy_change_kind(old, new_entry);
2059+
2060+
if (!change)
2061+
return 0;
2062+
*policy_changes |= change;
2063+
return 1;
2064+
}
2065+
2066+
static int transfer_current_proof_if_semantically_same_index(
2067+
struct index_state *dst, const struct index_state *src,
2068+
int allow_checkout_policy_changes,
2069+
int *manifest_refresh_required)
20232070
{
20242071
const unsigned int semantic_flags =
20252072
CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS;
20262073
unsigned int src_pos = 0, dst_pos = 0;
2074+
enum checkout_policy_change policy_changes = CHECKOUT_POLICY_NONE;
20272075
int transferred;
20282076

2077+
if (manifest_refresh_required)
2078+
*manifest_refresh_required = 0;
2079+
20292080
if (!current_proof_is_writable(src) ||
20302081
src->repo != dst->repo || src->split_index || dst->split_index ||
20312082
src->sparse_index || dst->sparse_index ||
@@ -2050,25 +2101,37 @@ int clean_status_transfer_current_proof_if_semantically_same_index(
20502101
cmp = strcmp(old->name, new_entry->name);
20512102
if (cmp < 0) {
20522103
if (!clean_status_index_entry_is_semantically_safe(
2053-
src, old, NULL))
2104+
src, old, NULL) &&
2105+
(!allow_checkout_policy_changes ||
2106+
!record_checkout_policy_change(&policy_changes,
2107+
old, NULL)))
20542108
return 0;
20552109
src_pos++;
20562110
} else if (cmp > 0) {
20572111
if (!clean_status_index_entry_is_semantically_safe(
2058-
src, NULL, new_entry))
2112+
src, NULL, new_entry) &&
2113+
(!allow_checkout_policy_changes ||
2114+
!record_checkout_policy_change(&policy_changes,
2115+
NULL, new_entry)))
20592116
return 0;
20602117
dst_pos++;
20612118
} else {
20622119
if ((old->ce_mode != new_entry->ce_mode ||
20632120
!oideq(&old->oid, &new_entry->oid) ||
20642121
((old->ce_flags ^ new_entry->ce_flags) & semantic_flags)) &&
20652122
!clean_status_index_entry_is_semantically_safe(
2066-
src, old, new_entry))
2123+
src, old, new_entry) &&
2124+
(!allow_checkout_policy_changes ||
2125+
!record_checkout_policy_change(&policy_changes,
2126+
old, new_entry)))
20672127
return 0;
20682128
src_pos++;
20692129
dst_pos++;
20702130
}
20712131
}
2132+
if (manifest_refresh_required &&
2133+
(policy_changes & CHECKOUT_POLICY_ATTRIBUTES))
2134+
*manifest_refresh_required = 1;
20722135

20732136
if (current_proof_is_writable(dst)) {
20742137
const struct clean_status_state *src_state = src->clean_status;
@@ -2102,6 +2165,26 @@ int clean_status_transfer_current_proof_if_semantically_same_index(
21022165
return transferred;
21032166
}
21042167

2168+
int clean_status_transfer_current_proof_if_semantically_same_index(
2169+
struct index_state *dst, const struct index_state *src)
2170+
{
2171+
return transfer_current_proof_if_semantically_same_index(
2172+
dst, src, 0, NULL);
2173+
}
2174+
2175+
int clean_status_transfer_current_proof_after_checkout(
2176+
struct index_state *dst, const struct index_state *src,
2177+
int *manifest_refresh_required)
2178+
{
2179+
/*
2180+
* A successful checkout owns these worktree writes. It may therefore
2181+
* retain history across policy-file changes, but the caller must refresh
2182+
* changed attribute sources before pairing the transferred proof.
2183+
*/
2184+
return transfer_current_proof_if_semantically_same_index(
2185+
dst, src, 1, manifest_refresh_required);
2186+
}
2187+
21052188
struct clean_status_commit_checkpoint {
21062189
struct repository *repo;
21072190
struct lock_file *lock;

clean-status.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ int clean_status_transfer_current_proof_if_same_index(
162162
struct index_state *dst, const struct index_state *src);
163163
int clean_status_transfer_current_proof_if_semantically_same_index(
164164
struct index_state *dst, const struct index_state *src);
165+
int clean_status_transfer_current_proof_after_checkout(
166+
struct index_state *dst, const struct index_state *src,
167+
int *manifest_refresh_required);
165168

166169
/*
167170
* A canonical main-index source may lend suspended historical state to an

t/t7519-status-fsmonitor.sh

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2875,7 +2875,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \
28752875

28762876
test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \
28772877
'configured pulls preserve authenticated worktree proofs' '
2878-
test_when_finished "rm -rf pull-proof-origin.git pull-proof-seed pull-proof-ff pull-proof-ff-linked pull-proof-rebase pull-proof-rebase-linked pull-proof-autostash pull-proof-autostash-linked" &&
2878+
test_when_finished "rm -rf pull-proof-origin.git pull-proof-seed pull-proof-ff pull-proof-ff-linked pull-proof-rebase pull-proof-rebase-linked pull-proof-autostash pull-proof-autostash-linked pull-proof-filter" &&
28792879
git init --bare pull-proof-origin.git &&
28802880
test_create_repo pull-proof-seed &&
28812881
(
@@ -2934,11 +2934,11 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \
29342934
case "$role" in
29352935
main)
29362936
worktree="$repo" &&
2937-
invalidated=96
2937+
invalidated=98
29382938
;;
29392939
linked)
29402940
worktree="$linked" &&
2941-
invalidated=192
2941+
invalidated=194
29422942
;;
29432943
esac &&
29442944
if test "$mode" != ff
@@ -2969,9 +2969,14 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \
29692969
do
29702970
test_write_lines "$mode-$role-$file" \
29712971
>"upstream-$mode-$role/nested/$file" ||
2972-
return 1
2972+
return 1
29732973
done &&
2974-
git add "upstream-$mode-$role"
2974+
test_write_lines "# $mode-$role" \
2975+
>.gitattributes &&
2976+
test_write_lines "*.ignored" "# $mode-$role" \
2977+
>.gitignore &&
2978+
git add "upstream-$mode-$role" \
2979+
.gitattributes .gitignore
29752980
else
29762981
test_write_lines "$mode-$role" \
29772982
>"upstream-$mode-$role" &&
@@ -3008,14 +3013,21 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \
30083013
<"$gitdir/pull.trace" &&
30093014
! test_trace2_data fsmonitor untracked/proof-missing 1 \
30103015
<"$gitdir/pull.trace" &&
3011-
! test_trace2_data fsmonitor \
3012-
semantic/manifest-scan-count 1 \
3013-
<"$gitdir/pull.trace" &&
30143016
if test "$mode" = ff
30153017
then
3018+
test_trace2_data fsmonitor \
3019+
semantic/manifest-scan-count 1 \
3020+
<"$gitdir/pull.trace" &&
3021+
test_trace2_data fsmonitor \
3022+
history/checkout-manifest-refreshed 1 \
3023+
<"$gitdir/pull.trace" &&
30163024
test_trace2_data fsmonitor \
30173025
history/untracked-paired-new-directory-invalidated \
30183026
"$invalidated" <"$gitdir/pull.trace"
3027+
else
3028+
! test_trace2_data fsmonitor \
3029+
semantic/manifest-scan-count 1 \
3030+
<"$gitdir/pull.trace"
30193031
fi &&
30203032
perl "$PWD/.git/check-pull-proof.pl" \
30213033
<"$gitdir/index" &&
@@ -3041,7 +3053,51 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \
30413053
semantic/manifest-scan-count 1 \
30423054
<"$gitdir/status.trace" || return 1
30433055
done || return 1
3044-
done
3056+
done &&
3057+
git clone --quiet "$PWD/../pull-proof-origin.git" \
3058+
"$PWD/../pull-proof-filter" &&
3059+
filter="$PWD/../pull-proof-filter" &&
3060+
git -C "$filter" config pull.ff only &&
3061+
git -C "$filter" config core.untrackedCache true &&
3062+
git -C "$filter" config core.fsmonitor true &&
3063+
git -C "$filter" config filter.pullproof.clean false &&
3064+
git -C "$filter" config filter.pullproof.required true &&
3065+
filter_gitdir=$(git -C "$filter" rev-parse --absolute-git-dir) &&
3066+
GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \
3067+
git -C "$filter" update-index --fsmonitor &&
3068+
GIT_INDEX_FILE="$filter_gitdir/index" \
3069+
GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \
3070+
git -C "$filter" status --porcelain=v2 \
3071+
>"$filter_gitdir/prime" &&
3072+
test_must_be_empty "$filter_gitdir/prime" &&
3073+
test_write_lines "tracked filter=pullproof" >.gitattributes &&
3074+
git add .gitattributes &&
3075+
git commit -qm "upstream-filter" &&
3076+
git push --quiet origin main &&
3077+
GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \
3078+
GIT_TEST_FSMONITOR_QUERY_PATH=tracked \
3079+
GIT_TRACE2_EVENT="$filter_gitdir/pull.trace" \
3080+
git -C "$filter" pull --quiet &&
3081+
test_trace2_data fsmonitor semantic/manifest-scan-count 1 \
3082+
<"$filter_gitdir/pull.trace" &&
3083+
test_trace2_data fsmonitor history/checkout-manifest-refreshed 1 \
3084+
<"$filter_gitdir/pull.trace" &&
3085+
test_trace2_data fsmonitor semantic/manifest-invalidated 1 \
3086+
<"$filter_gitdir/pull.trace" &&
3087+
test_trace2_data fsmonitor history/untracked-paired-transfer 1 \
3088+
<"$filter_gitdir/pull.trace" &&
3089+
cp "$filter_gitdir/index" "$filter_gitdir/status.before" &&
3090+
test_must_fail env GIT_OPTIONAL_LOCKS=0 \
3091+
GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \
3092+
GIT_TRACE2_EVENT="$filter_gitdir/status.trace" \
3093+
git -C "$filter" status --porcelain=v2 \
3094+
--untracked-files=no -- tracked \
3095+
>"$filter_gitdir/status" \
3096+
2>"$filter_gitdir/status.err" &&
3097+
test_grep "clean filter .pullproof. failed" \
3098+
"$filter_gitdir/status.err" &&
3099+
test_cmp_bin "$filter_gitdir/status.before" \
3100+
"$filter_gitdir/index"
30453101
)
30463102
'
30473103

unpack-trees.c

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2165,6 +2165,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
21652165
ret = check_updates(o, &o->internal.result) ? (-2) : 0;
21662166
if (o->dst_index) {
21672167
int history_transferred = 0;
2168+
int manifest_refresh_required = 0;
21682169
int new_indexed_directory = 0;
21692170

21702171
if (!ret) {
@@ -2173,14 +2174,44 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
21732174
&o->internal.result, o->src_index);
21742175
if (!history_transferred && o->preserve_semantic_history)
21752176
history_transferred =
2176-
clean_status_transfer_current_proof_if_semantically_same_index(
2177-
&o->internal.result, o->src_index);
2177+
clean_status_transfer_current_proof_after_checkout(
2178+
&o->internal.result, o->src_index,
2179+
&manifest_refresh_required);
21782180
if (history_transferred && o->preserve_semantic_history)
21792181
new_indexed_directory =
21802182
checkout_introduces_new_indexed_directory(
21812183
o->src_index, &o->internal.result);
21822184
}
21832185
move_index_extensions(&o->internal.result, o->src_index);
2186+
if (!ret && history_transferred && manifest_refresh_required) {
2187+
int manifest_refreshed = 0;
2188+
2189+
/*
2190+
* The checkout has installed the new attribute sources. Refresh
2191+
* them before allowing the old provider boundary to authenticate
2192+
* the resulting index and paired untracked cache.
2193+
*/
2194+
if (clean_status_refresh_worktree_manifest(
2195+
&o->internal.result) < 0 ||
2196+
clean_status_manifest_global_fallback(
2197+
&o->internal.result)) {
2198+
clean_status_invalidate_current_proof(
2199+
&o->internal.result);
2200+
history_transferred = 0;
2201+
} else {
2202+
manifest_refreshed = 1;
2203+
clean_status_mark_fsmonitor_config_valid(
2204+
&o->internal.result,
2205+
o->internal.result.fsmonitor_last_update);
2206+
history_transferred =
2207+
clean_status_has_current_full_fsmonitor_proof(
2208+
&o->internal.result);
2209+
}
2210+
trace2_data_intmax(
2211+
"fsmonitor", repo,
2212+
"history/checkout-manifest-refreshed",
2213+
manifest_refreshed);
2214+
}
21842215
if (!ret && o->internal.backoff_transfer)
21852216
clean_status_transfer_backoff_history(
21862217
o->internal.backoff_transfer,

0 commit comments

Comments
 (0)