Skip to content

Commit 8f9f5f7

Browse files
committed
status: preserve authenticated proofs across owned index writes
b65fc91 (status: retain the identity of an index it rewrites, 2026-08-17) lets status keep a race-proof receipt for an index it rewrites. Receipt preparation duplicates the writer descriptor so it can hash the final bytes, and requires the in-memory checksum to match the configured null trailer. Two owned write paths can leave an otherwise valid index without a usable clean proof. A worktree-update repair first writes a checksummed provisional index, then reopens the lockfile write-only before the final skipHash write while the index still records the provisional checksum. Receipt preparation rejects both states, so scoped stash cannot publish a sidecar for the index it installs. Similarly, worktree add creates its linked index before the new worktree has a closed FSMonitor provider epoch. The index has FSMonitor and untracked-cache extensions, but lacks the authenticated clean-config proof. Later read-only status processes remain correct, but cannot persist that proof and repeat the full fallback on every invocation. Reopen tempfiles read-write on macOS so receipt preparation can read the final index, and finish provisional writes through a cold helper that restores the null object ID before the receipt-aware write. After worktree add checks out a worktree with FSMonitor and the untracked cache, run one silent status with optional locks enabled to establish the provider epoch and persist the complete proof. Proof priming is best-effort; ordinary worktree creation and the status hot path are unchanged. Cover receipt publication and adoption after scoped stash, and require a new linked worktree to have a complete proof before its first read-only status. The latter must leave the index byte-identical and avoid a manifest scan.
1 parent 5e6128e commit 8f9f5f7

9 files changed

Lines changed: 112 additions & 2 deletions

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2580,6 +2580,8 @@ LIBS = $(filter-out %.o, $(GITLIBS)) $(EXTLIBS)
25802580

25812581
BASIC_CFLAGS += $(COMPAT_CFLAGS)
25822582
LIB_OBJS += $(COMPAT_OBJS)
2583+
# Keep provisional-index glue after every existing library object.
2584+
LIB_OBJS += clean-status-index-provisional.o
25832585

25842586
# Quote for C
25852587

builtin/worktree.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "copy.h"
1010
#include "dir.h"
1111
#include "environment.h"
12+
#include "fsmonitor-settings.h"
1213
#include "gettext.h"
1314
#include "hex.h"
1415
#include "object-file.h"
@@ -21,12 +22,14 @@
2122
#include "refs.h"
2223
#include "remote.h"
2324
#include "run-command.h"
25+
#include "repo-settings.h"
2426
#include "hook.h"
2527
#include "sigchain.h"
2628
#include "submodule.h"
2729
#include "utf8.h"
2830
#include "worktree.h"
2931
#include "quote.h"
32+
#include "trace2.h"
3033

3134
#define BUILTIN_WORKTREE_ADD_USAGE \
3235
N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \
@@ -409,6 +412,32 @@ static int checkout_worktree(const struct add_opts *opts,
409412
return run_command(&cp);
410413
}
411414

415+
static void prime_worktree_clean_status_proof(const char *path)
416+
{
417+
struct child_process cp = CHILD_PROCESS_INIT;
418+
int ret;
419+
420+
/*
421+
* The checkout creates the linked index before it has a provider epoch
422+
* from which to certify the worktree. Establish that epoch while this
423+
* writer can still update the index; a later read-only status cannot
424+
* persist the missing proof.
425+
*/
426+
cp.git_cmd = 1;
427+
cp.dir = path;
428+
cp.no_stdin = 1;
429+
cp.no_stdout = 1;
430+
cp.no_stderr = 1;
431+
strvec_pushl(&cp.args, "status", "--porcelain=v2",
432+
"--untracked-files=normal", NULL);
433+
strvec_push(&cp.env, GIT_DIR_ENVIRONMENT);
434+
strvec_push(&cp.env, GIT_WORK_TREE_ENVIRONMENT);
435+
strvec_push(&cp.env, "GIT_OPTIONAL_LOCKS=1");
436+
ret = run_command(&cp);
437+
trace2_data_intmax("worktree", the_repository,
438+
"add/clean-status-primed", !ret);
439+
}
440+
412441
static int make_worktree_orphan(const char * ref, const struct add_opts *opts,
413442
struct strvec *child_env)
414443
{
@@ -593,6 +622,15 @@ static int add_worktree(const char *path, const char *refname,
593622
if (opts->checkout &&
594623
(ret = checkout_worktree(opts, &child_env)))
595624
goto done;
625+
if (opts->checkout &&
626+
fsm_settings__get_mode(the_repository) == FSMONITOR_MODE_IPC &&
627+
the_repository->settings.core_untracked_cache ==
628+
UNTRACKED_CACHE_WRITE &&
629+
!getenv(INDEX_ENVIRONMENT) &&
630+
!getenv(GIT_COMMON_DIR_ENVIRONMENT) &&
631+
!getenv(DB_ENVIRONMENT) &&
632+
!getenv(ALTERNATE_DB_ENVIRONMENT))
633+
prime_worktree_clean_status_proof(path);
596634

597635
is_junk = 0;
598636
FREE_AND_NULL(junk_work_tree);

clean-status-index-provisional.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#include "git-compat-util.h"
2+
#include "clean-status-index.h"
3+
#include "lockfile.h"
4+
#include "read-cache-ll.h"
5+
#include "repository.h"
6+
7+
int clean_status_write_index_after_provisional(
8+
struct index_state *istate, struct lock_file *lock, unsigned flags,
9+
struct clean_status_index_write_receipt *receipt)
10+
{
11+
/* Let receipt preparation authenticate the final null trailer. */
12+
if (istate->repo->settings.index_skip_hash)
13+
oidcpy(&istate->oid, istate->repo->hash_algo->null_oid);
14+
return write_locked_index_with_receipt(
15+
istate, lock, flags, receipt);
16+
}

clean-status-index.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "hash.h"
66

77
struct index_state;
8+
struct lock_file;
89
struct repository;
910

1011
/*
@@ -52,6 +53,11 @@ int clean_status_index_adopt_write_receipt(
5253
void clean_status_index_write_receipt_release(
5354
struct clean_status_index_write_receipt *receipt);
5455

56+
/* Finish a provisional write without widening the ordinary status hot path. */
57+
int clean_status_write_index_after_provisional(
58+
struct index_state *istate, struct lock_file *lock, unsigned flags,
59+
struct clean_status_index_write_receipt *receipt);
60+
5561
int clean_status_index_snapshot_open(
5662
struct clean_status_index_snapshot *snapshot, const char *path,
5763
const struct git_hash_algo *algo);

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ libgit_sources = [
340340
'clean-status-history.c',
341341
'clean-status-identity.c',
342342
'clean-status-index.c',
343+
'clean-status-index-provisional.c',
343344
'clean-status-manifest.c',
344345
'clean-status-sidecar.c',
345346
'clean-status-fast.c',

t/t7519-status-fsmonitor.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8164,6 +8164,42 @@ test_expect_success LINUX_SCOPED_HISTORY,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORE
81648164
)
81658165
'
81668166

8167+
test_expect_success MACOS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \
8168+
'worktree add primes a read-only clean status proof' '
8169+
test_when_finished "rm -rf worktree-add-proof worktree-add-proof-linked" &&
8170+
test_when_finished \
8171+
"git -C worktree-add-proof-linked fsmonitor--daemon stop 2>/dev/null || :" &&
8172+
test_create_repo worktree-add-proof &&
8173+
(
8174+
cd worktree-add-proof &&
8175+
sane_unset GIT_TEST_SPLIT_INDEX &&
8176+
test_write_lines tracked >tracked &&
8177+
git add tracked &&
8178+
git commit -qm base &&
8179+
git config core.fsmonitor true &&
8180+
git config core.untrackedCache true &&
8181+
GIT_TRACE2_EVENT="$PWD/.git/worktree-add.trace" \
8182+
git worktree add --detach \
8183+
../worktree-add-proof-linked HEAD &&
8184+
worktree="$PWD/../worktree-add-proof-linked" &&
8185+
gitdir=$(git -C "$worktree" rev-parse --absolute-git-dir) &&
8186+
test_trace2_data worktree add/clean-status-primed 1 \
8187+
<.git/worktree-add.trace &&
8188+
test_fsmonitor_full_proof "$gitdir/index" paired &&
8189+
cp "$gitdir/index" "$gitdir/before.index" &&
8190+
GIT_OPTIONAL_LOCKS=0 \
8191+
GIT_TRACE2_EVENT="$gitdir/status.trace" \
8192+
git -C "$worktree" status --porcelain=v2 \
8193+
>"$gitdir/status.actual" &&
8194+
test_must_be_empty "$gitdir/status.actual" &&
8195+
test_cmp_bin "$gitdir/before.index" "$gitdir/index" &&
8196+
test_trace2_data fsmonitor config/coherent 1 \
8197+
<"$gitdir/status.trace" &&
8198+
! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \
8199+
<"$gitdir/status.trace"
8200+
)
8201+
'
8202+
81678203
test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \
81688204
'repeated provider resets fall back before an unclosable rescan' '
81698205
test_when_finished "rm -rf builtin-closure-terminal-reset" &&

t/t7530-status-clean-sidecar.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3632,16 +3632,23 @@ test_expect_success PERL_TEST_HELPERS \
36323632
'
36333633

36343634
test_expect_success DURABLE_FSMONITOR \
3635-
'scoped stash publishes a sidecar for its final clean index' '
3635+
'scoped stash authenticates its final skipHash index' '
36363636
test_when_finished "stop_daemon sidecar-scoped-stash" &&
36373637
setup_repo sidecar-scoped-stash &&
36383638
git -C sidecar-scoped-stash config core.autocrlf false &&
3639+
git -C sidecar-scoped-stash config feature.manyFiles true &&
36393640
issue_sidecar sidecar-scoped-stash &&
36403641
assert_clean_sidecar_hit sidecar-scoped-stash \
36413642
sidecar-scoped-stash scoped-stash-before &&
36423643
test_write_lines changed >sidecar-scoped-stash/tracked &&
36433644
GIT_TRACE2_EVENT="$PWD/scoped-stash.trace" \
36443645
git -C sidecar-scoped-stash stash push -q -- tracked &&
3646+
test_trace2_data fsmonitor history/own-write-source-recorded 1 \
3647+
<scoped-stash.trace &&
3648+
test_trace2_data fsmonitor history/own-write-source-adopted 1 \
3649+
<scoped-stash.trace &&
3650+
test_trace2_data status clean-proof/writer-sidecar 1 \
3651+
<scoped-stash.trace &&
36453652
test_path_is_file sidecar-scoped-stash/.git/index.csts &&
36463653
assert_clean_sidecar_hit sidecar-scoped-stash \
36473654
sidecar-scoped-stash scoped-stash-after

tempfile.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,11 @@ int reopen_tempfile(struct tempfile *tempfile)
328328
BUG("reopen_tempfile called for an inactive object");
329329
if (0 <= tempfile->fd)
330330
BUG("reopen_tempfile called for an open object");
331+
#ifdef __APPLE__
332+
tempfile->fd = open(tempfile->filename.buf, O_RDWR|O_TRUNC);
333+
#else
331334
tempfile->fd = open(tempfile->filename.buf, O_WRONLY|O_TRUNC);
335+
#endif
332336
return tempfile->fd;
333337
}
334338

wt-status.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2769,7 +2769,7 @@ int wt_status_repair_fsmonitor_proof_after_update_with_sidecar(
27692769

27702770
if (repaired <= 0)
27712771
return repaired;
2772-
if (write_locked_index_with_receipt(
2772+
if (clean_status_write_index_after_provisional(
27732773
repo->index, lock, COMMIT_LOCK | SKIP_IF_UNCHANGED,
27742774
&written_index)) {
27752775
clean_status_index_write_receipt_release(&written_index);

0 commit comments

Comments
 (0)