diff --git a/Documentation/technical/status-clean-proof.adoc b/Documentation/technical/status-clean-proof.adoc index fb5f24da58a133..66c2edaac297cd 100644 --- a/Documentation/technical/status-clean-proof.adoc +++ b/Documentation/technical/status-clean-proof.adoc @@ -83,9 +83,12 @@ checksum is accepted only when the pinned index is bound by the durable local-APFS identity used for raced-input checks. The sidecar is installed while the index lock remains held and after -the pinned index is rechecked. Status then rolls back the index lock, so -issuing a sidecar does not itself rewrite the index. With optional locks -disabled, status does not issue a sidecar. +the pinned index is rechecked. If the exact query first has to repair +the index's file system monitor checkpoint, status writes that repair, +refreshes the resumable history checkpoint, and then installs a proof +bound to the rewritten index. Status rolls back the lock used for the +sidecar itself, so issuing a sidecar does not itself rewrite the index. +With optional locks disabled, status does not issue a sidecar. Validation and races -------------------- diff --git a/Makefile b/Makefile index fd867037fcbc4e..cd39446750b87f 100644 --- a/Makefile +++ b/Makefile @@ -2580,6 +2580,8 @@ LIBS = $(filter-out %.o, $(GITLIBS)) $(EXTLIBS) BASIC_CFLAGS += $(COMPAT_CFLAGS) LIB_OBJS += $(COMPAT_OBJS) +# Keep provisional-index glue after every existing library object. +LIB_OBJS += clean-status-index-provisional.o # Quote for C diff --git a/builtin/add.c b/builtin/add.c index d9fe038f7a9061..aaba815a386d44 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -29,6 +29,7 @@ #include "submodule.h" #include "add-interactive.h" #include "merge-ll.h" +#include "wt-status.h" static const char * const builtin_add_usage[] = { N_("git add [] [--] ..."), @@ -467,6 +468,7 @@ int cmd_add(int argc, struct dir_struct dir = DIR_INIT; int flags; int add_new_files; + int had_full_proof = 0; int preserve_add_history = 0; int require_pathspec; char *seen = NULL; @@ -591,7 +593,7 @@ int cmd_add(int argc, if (refresh_only) { clean_status_enable_external_history(repo); clean_status_set_config_digest(repo, &clean_digest); - } else if (!show_only && !intent_to_add && !add_renormalize && + } else if (!show_only && !add_renormalize && !chmod_arg && !include_sparse && !ignore_add_errors) { preserve_add_history = 1; flags |= ADD_CACHE_TRACK_CLEAN_HISTORY; @@ -601,6 +603,8 @@ int cmd_add(int argc, if (repo_read_index_preload(repo, &pathspec, 0) < 0) die(_("index file corrupt")); + had_full_proof = + clean_status_has_persistent_fsmonitor_semantic_history(repo->index); if (preserve_add_history && (repo->index->split_index || repo->index->sparse_index)) clean_status_invalidate_current_proof(repo->index); @@ -717,6 +721,12 @@ int cmd_add(int argc, finish: if (preserve_add_history && exit_status) clean_status_invalidate_current_proof(repo->index); + else if (preserve_add_history && !show_only && !intent_to_add && + (!clean_status_has_current_full_fsmonitor_proof(repo->index) || + !repo->index->fsmonitor_untracked_valid) && + wt_status_repair_fsmonitor_proof_after_index_update( + repo, &lock_file, had_full_proof) < 0) + die(_("unable to repair new index file")); if (show_only) rollback_lock_file(&lock_file); else if (write_locked_index(repo->index, &lock_file, diff --git a/builtin/checkout.c b/builtin/checkout.c index fbc324b8b7dee7..b9b355d7f3ced2 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -554,6 +554,7 @@ static int checkout_paths(const struct checkout_opts *opts, int checkout_index; int preserve_source_tree_history = 0; int source_tree_index_changed = 0; + int repair_after_checkout = 0; trace2_cmd_mode(opts->patch_mode ? "patch" : "path"); @@ -678,6 +679,9 @@ static int checkout_paths(const struct checkout_opts *opts, } if (repo_read_index_preload(the_repository, &opts->pathspec, 0) < 0) return error(_("index file corrupt")); + repair_after_checkout = opts->checkout_worktree && + clean_status_has_current_full_fsmonitor_proof( + the_repository->index); if (preserve_source_tree_history && (the_repository->index->split_index || @@ -764,6 +768,10 @@ static int checkout_paths(const struct checkout_opts *opts, if (!the_repository->index->cache_changed && !hook_exists(the_repository, "post-index-change")) flags |= SKIP_IF_UNCHANGED; + if (wt_status_repair_fsmonitor_proof_after_worktree_update( + the_repository, &lock_file, + repair_after_checkout) < 0) + die(_("unable to repair new index file")); if (write_locked_index(the_repository->index, &lock_file, flags)) die(_("unable to write new index file")); } else { @@ -836,6 +844,9 @@ static int reset_tree(struct tree *tree, const struct checkout_opts *o, opts.verbose_update = o->show_progress; opts.src_index = the_repository->index; opts.dst_index = the_repository->index; + opts.preserve_semantic_history = worktree && + clean_status_revalidated_token_matches(the_repository->index); + opts.preserve_untracked_history = opts.preserve_semantic_history; init_checkout_metadata(&opts.meta, info->refname, info->commit ? &info->commit->object.oid : null_oid(the_hash_algo), NULL); @@ -907,7 +918,7 @@ static int merge_working_tree(const struct checkout_opts *opts, bool quiet, int *writeout_error) { - int ret; + int ret, repair_after_checkout; struct lock_file lock_file = LOCK_INIT; struct tree *new_tree; @@ -927,6 +938,9 @@ static int merge_working_tree(const struct checkout_opts *opts, rollback_lock_file(&lock_file); return error(_("index file corrupt")); } + repair_after_checkout = + clean_status_has_current_full_fsmonitor_proof( + the_repository->index); resolve_undo_clear_index(the_repository->index); if (opts->new_orphan_branch && opts->orphan_from_empty_tree) { @@ -969,6 +983,7 @@ static int merge_working_tree(const struct checkout_opts *opts, init_topts(&topts, opts->show_progress, opts->overwrite_ignore, quiet); topts.preserve_semantic_history = 1; + topts.preserve_untracked_history = 1; init_checkout_metadata(&topts.meta, new_branch_info->refname, new_branch_info->commit ? &new_branch_info->commit->object.oid : @@ -1001,6 +1016,9 @@ static int merge_working_tree(const struct checkout_opts *opts, if (!cache_tree_fully_valid(the_repository->index->cache_tree)) cache_tree_update(the_repository->index, WRITE_TREE_SILENT | WRITE_TREE_REPAIR); + if (wt_status_repair_fsmonitor_proof_after_worktree_update( + the_repository, &lock_file, repair_after_checkout) < 0) + die(_("unable to repair new index file")); if (write_locked_index(the_repository->index, &lock_file, COMMIT_LOCK)) die(_("unable to write new index file")); diff --git a/builtin/commit.c b/builtin/commit.c index 5e8c425fa13f1d..3687b5f07eca52 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1898,21 +1898,35 @@ struct repository *repo UNUSED) int default_status_command = argc == 1 && (!prefix || !*prefix); int exact_clean_command = argc == 2 && !strcmp(argv[1], "--porcelain=v2") && (!prefix || !*prefix); + int short_clean_command = argc == 2 && + (!strcmp(argv[1], "--short") || !strcmp(argv[1], "-s")) && + (!prefix || !*prefix); int exact_clean_query; int normal_clean_query; + int short_clean_query; + int certifying_clean_query; int reusable_clean_query; int normal_has_head; int reissue_clean_sidecar = 0; int repository_inputs_changed = 0; int sidecar_provider_reset = 0; int reissue_after_write = 0; + int issue_exact_after_write = 0; + int issue_certifying_after_write = 0; + int reload_racy_after_write = 0; + int exact_after_write_candidate = 0; int save_history_after_write = 0; + int postwrite_sidecar_candidate = 0; + int postwrite_sidecar_pinned = 0; int deferred_scoped_history = 0; int guarded_scoped_history_source = 0; int optional_status_writes; struct clean_status_index_snapshot scoped_history_source = { .fd = -1, }; + struct clean_status_index_snapshot postwrite_sidecar_source = { + .fd = -1, + }; struct clean_status_index_write_receipt written_index = CLEAN_STATUS_INDEX_WRITE_RECEIPT_INIT; struct object_id oid; @@ -2018,6 +2032,14 @@ struct repository *repo UNUSED) !s.submodule_summary && s.show_untracked_files == SHOW_NORMAL_UNTRACKED_FILES && !repo_config_values(the_repository)->apply_sparse_checkout; + short_clean_query = short_clean_command && + status_format == STATUS_FORMAT_SHORT && normal_has_head && + !s.pathspec.nr && !s.show_branch && !s.show_stash && + !s.show_ignored_mode && !s.null_termination && !s.verbose && + !s.submodule_summary && + s.show_untracked_files == SHOW_NORMAL_UNTRACKED_FILES && + !repo_config_values(the_repository)->apply_sparse_checkout; + certifying_clean_query = normal_clean_query || short_clean_query; reusable_clean_query = normal_has_head && !s.show_ignored_mode && !s.submodule_summary && /* A clean merge still prints a staged-changes header with -vv. */ @@ -2028,7 +2050,7 @@ struct repository *repo UNUSED) !s.submodule_summary && !repo_config_values(the_repository)->apply_sparse_checkout; clean_status_enable_external_history(the_repository); - s.certify_clean_status = exact_clean_query; + s.certify_clean_status = exact_clean_query || short_clean_query; if (reusable_clean_query && clean_status_try_sidecar(the_repository, &clean_digest, &repository_inputs_changed, @@ -2039,7 +2061,7 @@ struct repository *repo UNUSED) return 0; } } - if (normal_clean_query && optional_status_writes && + if (certifying_clean_query && optional_status_writes && clean_status_identity_is_durable()) reissue_clean_sidecar = clean_status_sidecar_needs_reissue( @@ -2089,7 +2111,7 @@ struct repository *repo UNUSED) clean_status_capture_external_history_source( the_repository->index); } - if (normal_clean_query && optional_status_writes && + if (certifying_clean_query && optional_status_writes && clean_status_identity_is_durable() && (reissue_clean_sidecar || clean_status_external_history_was_restored( @@ -2163,6 +2185,8 @@ struct repository *repo UNUSED) the_repository->index); int external_saved = 0; int persist_restored_boundary = 0; + int racy_fresh_history = !external_restored && + has_racy_timestamp(the_repository->index); int preserve_entry_changes = (!external_restored && (the_repository->index->cache_changed & CE_ENTRY_CHANGED)) || @@ -2192,6 +2216,11 @@ struct repository *repo UNUSED) else if (deferred_history && !hook_exists(the_repository, "post-index-change")) save_history_after_write = 1; + if (certifying_clean_query && racy_fresh_history && + !s.change.nr && !s.untracked.nr && !s.ignored.nr && + !external_saved && + !hook_exists(the_repository, "post-index-change")) + save_history_after_write = reload_racy_after_write = 1; if (external_restored && !external_saved && clean_status_external_history_owns_index( the_repository->index) && @@ -2200,10 +2229,21 @@ struct repository *repo UNUSED) trace2_data_intmax("fsmonitor", the_repository, "history/external-racy-index-persisted", 1); } - reissue_after_write = normal_clean_query && + reissue_after_write = certifying_clean_query && reissue_clean_sidecar && preserve_entry_changes && !external_restored && !persist_restored_boundary && !hook_exists(the_repository, "post-index-change"); + /* + * An exact query may have completed a clean scan while repairing + * the provider checkpoint or cached stat data. Bind its proof to + * the repaired index, after the resumable history is durable. + */ + exact_after_write_candidate = exact_clean_query && + preserve_entry_changes && !external_restored && + !persist_restored_boundary && + !hook_exists(the_repository, "post-index-change"); + issue_exact_after_write = + exact_after_write_candidate && external_saved; if (the_repository->index->fsmonitor_legacy_untracked_fallback && !preserve_entry_changes && !external_saved) { @@ -2212,7 +2252,7 @@ struct repository *repo UNUSED) } else if (exact_clean_query) { if (!preserve_entry_changes && external_saved && clean_status_issue_sidecar( - &s, &clean_digest, &index_lock, 0)) + &s, &clean_digest, &index_lock, NULL, 0)) fd = -1; else if (!preserve_entry_changes && !persist_restored_boundary && @@ -2222,10 +2262,10 @@ struct repository *repo UNUSED) } } else if (!preserve_entry_changes && !persist_restored_boundary && - normal_clean_query && + certifying_clean_query && (external_restored || reissue_clean_sidecar) && clean_status_issue_sidecar( - &s, &clean_digest, &index_lock, 1)) { + &s, &clean_digest, &index_lock, NULL, 1)) { fd = -1; } else if (!preserve_entry_changes && !persist_restored_boundary && @@ -2243,30 +2283,68 @@ struct repository *repo UNUSED) "history/scoped-source-epoch-mismatch", 1); } if (0 <= fd) { + postwrite_sidecar_candidate = + reissue_after_write || issue_exact_after_write || + save_history_after_write; repo_update_index_if_able_with_receipt(the_repository, &index_lock, &written_index); + if (postwrite_sidecar_candidate) + postwrite_sidecar_pinned = + !clean_status_sidecar_postwrite_test_barrier(); clean_status_index_adopt_write_receipt(the_repository->index, &written_index); + if (postwrite_sidecar_pinned && + clean_status_index_snapshot_pin( + &postwrite_sidecar_source, + the_repository->index)) { + postwrite_sidecar_pinned = 0; + trace2_data_string("status", the_repository, + "clean-proof/miss", + "postwrite-index-raced"); + } + if (reload_racy_after_write) { + /* + * Re-read the committed index before checkpointing a fresh racy + * checkout. The old in-memory epoch cannot authenticate the new + * file even though this status established its clean contents. + */ + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) + save_history_after_write = 0; + } if (save_history_after_write && !hook_exists(the_repository, "post-index-change") && repo_hold_locked_index(the_repository, &index_lock, 0) >= 0) { if (clean_status_save_external_history( - the_repository->index)) + the_repository->index)) { trace2_data_intmax("fsmonitor", the_repository, "history/external-postwrite-stored", 1); + if (exact_after_write_candidate) + issue_exact_after_write = 1; + else if (certifying_clean_query) + issue_certifying_after_write = 1; + } rollback_lock_file(&index_lock); } - if (reissue_after_write && + if (postwrite_sidecar_pinned && + (reissue_after_write || issue_exact_after_write || + issue_certifying_after_write) && repo_hold_locked_index(the_repository, &index_lock, 0) >= 0) { if (clean_status_issue_sidecar( - &s, &clean_digest, &index_lock, 1)) + &s, &clean_digest, &index_lock, + &postwrite_sidecar_source, + reissue_after_write || + issue_certifying_after_write)) trace2_data_intmax("status", the_repository, - "clean-proof/postwrite-reissued", 1); + reissue_after_write ? + "clean-proof/postwrite-reissued" : + "clean-proof/postwrite-issued", 1); else rollback_lock_file(&index_lock); } } clean_status_index_write_receipt_release(&written_index); + clean_status_index_snapshot_release(&postwrite_sidecar_source); clean_status_index_snapshot_release(&scoped_history_source); if (s.relative_paths) diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c index 1c53a5af4dd6df..7fb7193999f76c 100644 --- a/builtin/fsmonitor--daemon.c +++ b/builtin/fsmonitor--daemon.c @@ -8,12 +8,14 @@ #include "environment.h" #include "gettext.h" #include "parse-options.h" + #include "fsmonitor-ll.h" #include "fsmonitor-ipc.h" #include "fsmonitor-settings.h" #include "compat/fsmonitor/fsm-health.h" #include "compat/fsmonitor/fsm-listen.h" #include "fsmonitor--daemon.h" +#include "khash.h" #include "simple-ipc.h" #include "strmap.h" @@ -30,6 +32,19 @@ static const char * const builtin_fsmonitor__daemon_usage[] = { }; #ifdef HAVE_FSMONITOR_DAEMON_BACKEND +static khint_t fsmonitor_path_hash(const char *path) +{ + return memhash(&path, sizeof(path)); +} + +static int fsmonitor_path_equal(const char *a, const char *b) +{ + return a == b; +} + +KHASH_INIT(fsmonitor_path_sequence, const char *, uint64_t, 1, + fsmonitor_path_hash, fsmonitor_path_equal) + /* * Global state loaded from config. */ @@ -142,6 +157,7 @@ static int do_as_client__status(void) } enum fsmonitor_cookie_item_result { + FCIR_TIMEOUT = -2, FCIR_ERROR = -1, /* could not create cookie file ? */ FCIR_INIT, FCIR_SEEN, @@ -151,6 +167,7 @@ enum fsmonitor_cookie_item_result { struct fsmonitor_cookie_item { struct hashmap_entry entry; char *name; + uint64_t token_generation; enum fsmonitor_cookie_item_result result; }; @@ -177,6 +194,10 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie( struct strbuf cookie_filename = STRBUF_INIT; enum fsmonitor_cookie_item_result result; int my_cookie_seq; +#ifdef __APPLE__ + uint64_t cookie_token_generation; + enum fsm_listen_flush_result flush_result; +#endif CALLOC_ARRAY(cookie, 1); @@ -188,6 +209,7 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie( strbuf_addbuf(&cookie_pathname, &cookie_filename); cookie->name = strbuf_detach(&cookie_filename, NULL); + cookie->token_generation = state->token_generation; cookie->result = FCIR_INIT; hashmap_entry_init(&cookie->entry, strhash(cookie->name)); @@ -218,11 +240,10 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie( unlink(cookie_pathname.buf); /* - * Wait for the listener thread to observe the cookie file. - * Time out after a short interval so that the client - * does not hang forever if the filesystem does not deliver - * events (e.g., on certain container/overlay filesystems - * where inotify watches succeed but events never arrive). + * Wait for the provider to observe the cookie. On Darwin this is the + * first half of the boundary: a later synchronous provider fence is + * still required because FSEvents may report the cookie before a later + * callback containing logically older worktree events. */ { struct timeval now; @@ -240,10 +261,47 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie( if (err == ETIMEDOUT && cookie->result == FCIR_INIT) { trace_printf_key(&trace_fsmonitor, "cookie_wait timed out"); +#ifndef __APPLE__ cookie->result = FCIR_ERROR; +#endif } } +#ifdef __APPLE__ + /* + * A delivered Darwin cookie is not a completeness boundary. Once the + * cookie is visible (or its ordinary wait expires), request the stronger + * provider fence without holding main_lock so callbacks can publish every + * event that preceded the fence. A successful fence may also rescue a + * late cookie, but is accepted only if that cookie is then SEEN. + */ + if (cookie->result == FCIR_INIT || cookie->result == FCIR_SEEN) { + cookie_token_generation = cookie->token_generation; + trace_printf_key(&trace_fsmonitor, + "cookie_wait: requesting Darwin provider fence after cookie wait"); + pthread_mutex_unlock(&state->main_lock); + flush_result = fsm_listen__flush_sync(state); + pthread_mutex_lock(&state->main_lock); + + if (flush_result == FSM_LISTEN_FLUSH_TIMEOUT) { + trace_printf_key(&trace_fsmonitor, + "cookie_wait: synchronous flush timed out"); + cookie->result = FCIR_TIMEOUT; + } else if (flush_result != FSM_LISTEN_FLUSH_OK) { + cookie->result = FCIR_ABORT; + } else if (state->token_generation != + cookie_token_generation) { + trace_printf_key(&trace_fsmonitor, + "cookie_wait: provider fence crossed token generation"); + cookie->result = FCIR_ABORT; + } else if (cookie->result == FCIR_INIT) { + trace_printf_key(&trace_fsmonitor, + "cookie_wait: synchronous flush missed cookie"); + cookie->result = FCIR_ERROR; + } + } +#endif + done: hashmap_remove(&state->cookies, &cookie->entry, NULL); @@ -275,11 +333,18 @@ static void with_lock__mark_cookies_seen(struct fsmonitor_daemon_state *state, hashmap_entry_init(&key.entry, strhash(key.name)); cookie = hashmap_get_entry(&state->cookies, &key, entry, NULL); - if (cookie) { + if (cookie && cookie->result == FCIR_INIT && + cookie->token_generation == state->token_generation) { trace_printf_key(&trace_fsmonitor, "cookie-seen: '%s'", cookie->name); cookie->result = FCIR_SEEN; nr_seen++; + } else if (cookie && cookie->result == FCIR_INIT) { + trace_printf_key(&trace_fsmonitor, + "cookie-abort-generation: '%s'", + cookie->name); + cookie->result = FCIR_ABORT; + nr_seen++; } } @@ -390,6 +455,7 @@ struct fsmonitor_batch { const char **interned_paths; size_t nr, alloc; time_t pinned_time; + kh_fsmonitor_path_sequence_t *overflow_path_seqs; }; static struct fsmonitor_token_data *fsmonitor_new_token_data(void) @@ -410,12 +476,8 @@ static struct fsmonitor_token_data *fsmonitor_new_token_data(void) if (test_env_value < 0) test_env_value = git_env_bool("GIT_TEST_FSMONITOR_TOKEN", 0); -#ifdef __APPLE__ strbuf_addstr(&token->token_id, - FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX); -#endif - strbuf_addstr(&token->token_id, - FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX); + FSMONITOR_IPC_COOKIE_TOKEN_PREFIX); if (!test_env_value) { struct timeval tv; @@ -489,6 +551,7 @@ void fsmonitor_batch__free_list(struct fsmonitor_batch *batch) * are interned, so we don't own them. We only own * the array. */ + kh_destroy_fsmonitor_path_sequence(batch->overflow_path_seqs); free(batch->interned_paths); free(batch); @@ -521,16 +584,102 @@ static void fsmonitor_batch__combine(struct fsmonitor_batch *batch_dest, batch_src->interned_paths[k]; } +static void fsmonitor_batch__add_overflow_path(struct fsmonitor_batch *batch, + const char *path, + uint64_t batch_seq_nr) +{ + khint_t pos; + int added; + + pos = kh_put_fsmonitor_path_sequence( + batch->overflow_path_seqs, path, &added); + if (!added) { + if (kh_value(batch->overflow_path_seqs, pos) < batch_seq_nr) + kh_value(batch->overflow_path_seqs, pos) = batch_seq_nr; + return; + } + kh_value(batch->overflow_path_seqs, pos) = batch_seq_nr; + + ALLOC_GROW(batch->interned_paths, batch->nr + 1, batch->alloc); + batch->interned_paths[batch->nr++] = path; +} + +/* + * Collapse this batch and everything older than it into one deduplicated + * overflow batch. Every path is interned, so pointer identity is sufficient. + * + * Keep the set with the overflow batch. Future compactions then hash only + * newly retired paths instead of repeatedly rebuilding the complete set. + */ +static size_t fsmonitor_batch__compact_tail(struct fsmonitor_batch *batch, + size_t *input_nr) +{ + struct fsmonitor_batch compacted = { 0 }; + struct fsmonitor_batch *item, *overflow = NULL; + + *input_nr = 0; + for (item = batch; item; item = item->next) { + *input_nr = st_add(*input_nr, item->nr); + if (item->overflow_path_seqs) { + overflow = item; + break; + } + } + + if (overflow) { + /* + * Reuse the persistent set and array from the prior overflow + * batch. Only the newly retired paths need a lookup. + */ + if (overflow->next) + BUG("overflow batch is not the batch tail"); + for (item = batch; item != overflow; item = item->next) { + size_t k; + + for (k = 0; k < item->nr; k++) + fsmonitor_batch__add_overflow_path( + overflow, item->interned_paths[k], + item->batch_seq_nr); + } + compacted.interned_paths = overflow->interned_paths; + compacted.nr = overflow->nr; + compacted.alloc = overflow->alloc; + compacted.overflow_path_seqs = overflow->overflow_path_seqs; + overflow->interned_paths = NULL; + overflow->nr = overflow->alloc = 0; + overflow->overflow_path_seqs = NULL; + } else { + compacted.overflow_path_seqs = + kh_init_fsmonitor_path_sequence(); + for (item = batch; item; item = item->next) { + size_t k; + + for (k = 0; k < item->nr; k++) + fsmonitor_batch__add_overflow_path( + &compacted, item->interned_paths[k], + item->batch_seq_nr); + } + } + + free(batch->interned_paths); + batch->interned_paths = compacted.interned_paths; + batch->nr = compacted.nr; + batch->alloc = compacted.alloc; + batch->overflow_path_seqs = compacted.overflow_path_seqs; + + return batch->nr; +} + /* * To keep the batch list from growing unbounded in response to filesystem - * activity, we try to truncate old batches from the end of the list as - * they become irrelevant. + * activity, collapse old batches from the end of the list after a delay. * - * We assume that the .git/index will be updated with the most recent token - * any time the index is updated. And future commands will only ask for - * recent changes *since* that new token. So as tokens advance into the - * future, older batch items will never be requested/needed. So we can - * truncate them without loss of functionality. + * A repository may have multiple durable indexes with different tokens. In + * particular, advancing a private GIT_INDEX_FILE does not advance .git/index. + * We therefore cannot discard old paths just because one client asked for a + * newer token. Instead, keep their deduplicated union in an overflow batch. + * Keep the newest original sequence number for each path so that clients do + * not receive events that they consumed before their requested checkpoint. * * However, multiple commands may be talking to the daemon concurrently * or perform a slow command, so a little "token skew" is possible. @@ -549,6 +698,7 @@ static void fsmonitor_batch__combine(struct fsmonitor_batch *batch_dest, * the official list so that the caller can free it after leaving the lock. */ #define MY_TIME_DELAY_SECONDS (5 * 60) /* seconds */ +static unsigned long truncate_delay_seconds = MY_TIME_DELAY_SECONDS; static struct fsmonitor_batch *with_lock__truncate_old_batches( struct fsmonitor_daemon_state *state, @@ -558,6 +708,7 @@ static struct fsmonitor_batch *with_lock__truncate_old_batches( const struct fsmonitor_batch *batch; struct fsmonitor_batch *remainder; + size_t input_nr, unique_nr; if (!batch_marker) return NULL; @@ -566,13 +717,13 @@ static struct fsmonitor_batch *with_lock__truncate_old_batches( batch_marker->batch_seq_nr, (uint64_t)batch_marker->pinned_time); - for (batch = batch_marker; batch; batch = batch->next) { + for (batch = batch_marker->next; batch; batch = batch->next) { time_t t; - if (!batch->pinned_time) /* an overflow batch */ + if (!batch->pinned_time || batch->overflow_path_seqs) continue; - t = batch->pinned_time + MY_TIME_DELAY_SECONDS; + t = batch->pinned_time + truncate_delay_seconds; if (t > batch_marker->pinned_time) /* too close to marker */ continue; @@ -582,9 +733,20 @@ static struct fsmonitor_batch *with_lock__truncate_old_batches( return NULL; truncate_past_here: + remainder = ((struct fsmonitor_batch *)batch)->next; + if (!remainder) + return NULL; + + unique_nr = fsmonitor_batch__compact_tail( + (struct fsmonitor_batch *)batch, &input_nr); + trace_printf_key(&trace_fsmonitor, + "Compact: batch %"PRIu64" covers %"PRIuMAX + " of %"PRIuMAX" paths", + batch->batch_seq_nr, + (uintmax_t)unique_nr, (uintmax_t)input_nr); + state->current_token_data->batch_tail = (struct fsmonitor_batch *)batch; - remainder = ((struct fsmonitor_batch *)batch)->next; ((struct fsmonitor_batch *)batch)->next = NULL; return remainder; @@ -635,6 +797,9 @@ static void with_lock__do_force_resync(struct fsmonitor_daemon_state *state) if (state->current_token_data->client_ref_count == 0) free_me = state->current_token_data; state->current_token_data = new_one; + state->token_generation++; + if (!state->token_generation) + state->token_generation++; fsmonitor_free_token_data(free_me); @@ -692,6 +857,18 @@ static int fsmonitor_parse_client_token(const char *buf_token, return 0; } +static void fsmonitor_reply_overflow_paths( + const struct fsmonitor_batch *batch, + uint64_t requested_oldest_seq_nr, + int hardlink_aware_query, + ipc_server_reply_cb *reply, + struct ipc_server_reply_data *reply_data, + struct strset *shown, + struct strbuf *payload, + uint64_t *total_response_len, + intmax_t *count, + intmax_t *duplicates); + static int do_handle_client(struct fsmonitor_daemon_state *state, const char *command, ipc_server_reply_cb *reply, @@ -712,6 +889,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, int do_trivial = 0; int do_flush = 0; int do_cookie = 0; + int stop_after_response = 0; int invalid_binding = 0; int hardlink_aware_query = 0; enum fsmonitor_cookie_item_result cookie_result; @@ -760,7 +938,12 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY "\n" #ifdef __APPLE__ FSMONITOR_IPC_HARDLINK_QUERY_VERSION "\n" + FSMONITOR_IPC_DARWIN_PROVIDER_FENCE_CAPABILITY "\n" +#endif +#if FSMONITOR_IPC_HAS_DIR_METADATA FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" +#endif +#ifdef __APPLE__ FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n" #endif ; @@ -863,6 +1046,10 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, */ if (cookie_result == FCIR_ERROR) do_flush = 1; + else if (cookie_result == FCIR_TIMEOUT) { + do_flush = 1; + stop_after_response = 1; + } } } @@ -909,13 +1096,15 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, do_trivial = 1; } else if (requested_oldest_seq_nr < - token_data->batch_tail->batch_seq_nr) { + token_data->batch_tail->batch_seq_nr && + !token_data->batch_tail->overflow_path_seqs) { /* * The client wants older events than we have for - * this token_id. This means that the end of our - * batch list was truncated and we cannot give the - * client a complete snapshot relative to their - * request. + * this token_id. A normal tail means that the end + * of our batch list was truncated and we cannot + * give the client a complete snapshot. An overflow + * tail retains the latest original sequence for each + * older path. */ trace_printf_key(&trace_fsmonitor, "client requested truncated data"); @@ -964,7 +1153,8 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, */ strset_init_with_options(&shown, NULL, 0); for (batch = batch_head; - batch && batch->batch_seq_nr > requested_oldest_seq_nr; + batch && batch->batch_seq_nr > requested_oldest_seq_nr && + !batch->overflow_path_seqs; batch = batch->next) { size_t k; @@ -999,6 +1189,13 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, } } } + if (batch && batch->batch_seq_nr > requested_oldest_seq_nr) { + fsmonitor_reply_overflow_paths( + batch, requested_oldest_seq_nr, hardlink_aware_query, + reply, reply_data, &shown, &payload, + &total_response_len, &count, &duplicates); + batch = batch->next; + } if (payload.len) { reply(reply_data, payload.buf, payload.len); @@ -1052,8 +1249,61 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, strbuf_release(&response_token); strbuf_release(&requested_token_id); strbuf_release(&payload); + return stop_after_response ? SIMPLE_IPC_QUIT : 0; +} - return 0; +static void fsmonitor_reply_overflow_paths( + const struct fsmonitor_batch *batch, + uint64_t requested_oldest_seq_nr, + int hardlink_aware_query, + ipc_server_reply_cb *reply, + struct ipc_server_reply_data *reply_data, + struct strset *shown, + struct strbuf *payload, + uint64_t *total_response_len, + intmax_t *count, + intmax_t *duplicates) +{ + size_t k; + + if (!batch->overflow_path_seqs) + BUG("expected an overflow batch"); + + for (k = 0; k < batch->nr; k++) { + const char *s = batch->interned_paths[k]; + khint_t pos = kh_get_fsmonitor_path_sequence( + batch->overflow_path_seqs, s); + size_t s_len; + + if (pos == kh_end(batch->overflow_path_seqs)) + BUG("overflow path is missing its sequence"); + if (kh_value(batch->overflow_path_seqs, pos) <= + requested_oldest_seq_nr) + continue; + + if (!hardlink_aware_query && + starts_with(s, FSMONITOR_PATH_HARDLINK_INODE_PREFIX)) + s = FSMONITOR_PATH_GLOBAL_INVALIDATE; + + if (!strset_add(shown, s)) + (*duplicates)++; + else { + trace_printf_key(&trace_fsmonitor, + "send[%"PRIuMAX"]: %s", *count, s); + + /* Each path gets written with a trailing NUL */ + s_len = strlen(s) + 1; + + if (payload->len + s_len >= LARGE_PACKET_DATA_MAX) { + reply(reply_data, payload->buf, payload->len); + *total_response_len += payload->len; + strbuf_reset(payload); + } + + strbuf_add(payload, s, s_len); + (*count)++; + } + } } static ipc_server_application_cb handle_client; @@ -1178,14 +1428,11 @@ enum fsmonitor_path_type fsmonitor_classify_path_absolute( */ #define MY_COMBINE_LIMIT (1024) -void fsmonitor_publish(struct fsmonitor_daemon_state *state, - struct fsmonitor_batch *batch, - const struct string_list *cookie_names) +static void with_lock__publish(struct fsmonitor_daemon_state *state, + struct fsmonitor_batch *batch, + const struct string_list *cookie_names) { - if (!batch && !cookie_names->nr) - return; - - pthread_mutex_lock(&state->main_lock); + /* assert current thread holding state->main_lock */ if (batch) { struct fsmonitor_batch *head; @@ -1243,10 +1490,42 @@ void fsmonitor_publish(struct fsmonitor_daemon_state *state, if (cookie_names->nr) with_lock__mark_cookies_seen(state, cookie_names); +} + +void fsmonitor_publish(struct fsmonitor_daemon_state *state, + struct fsmonitor_batch *batch, + const struct string_list *cookie_names) +{ + if (!batch && !cookie_names->nr) + return; + pthread_mutex_lock(&state->main_lock); + with_lock__publish(state, batch, cookie_names); pthread_mutex_unlock(&state->main_lock); } +int fsmonitor_publish_if_current_generation( + struct fsmonitor_daemon_state *state, + struct fsmonitor_batch *batch, + uint64_t token_generation) +{ + struct string_list no_cookies = STRING_LIST_INIT_NODUP; + int current; + + if (!batch) + return 1; + + pthread_mutex_lock(&state->main_lock); + current = state->token_generation == token_generation; + if (current) + with_lock__publish(state, batch, &no_cookies); + pthread_mutex_unlock(&state->main_lock); + + if (!current) + fsmonitor_batch__free_list(batch); + return current; +} + static void *fsm_health__thread_proc(void *_state) { struct fsmonitor_daemon_state *state = _state; @@ -1273,12 +1552,14 @@ static void *fsm_listen__thread_proc(void *_state) fsm_listen__loop(state); - pthread_mutex_lock(&state->main_lock); - if (state->current_token_data && - state->current_token_data->client_ref_count == 0) - fsmonitor_free_token_data(state->current_token_data); - state->current_token_data = NULL; - pthread_mutex_unlock(&state->main_lock); + /* + * Leave token-data teardown to the main thread. The listener can + * initiate IPC shutdown while an existing client is still waiting for + * a filesystem boundary and has not taken a token-data reference yet. + * ipc_server_await() joins every IPC worker before the main thread + * reaches final state cleanup, so that is the first point where it is + * safe to release the current token unconditionally. + */ trace2_thread_exit(); return NULL; @@ -1330,6 +1611,12 @@ static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state) */ if (pthread_create(&state->health_thread, NULL, fsm_health__thread_proc, state)) { + /* + * The listener may still be starting the provider and IPC pool. + * Publish its stop request before stopping IPC so that it cannot + * subsequently start an already-stopped server. + */ + fsm_listen__stop_async(state); ipc_server_stop_async(state->ipc_server_data); err = error(_("could not start fsmonitor health thread")); goto cleanup; @@ -1348,6 +1635,19 @@ static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state) */ ipc_server_await(state->ipc_server_data); +#ifdef __APPLE__ + /* + * If the bounded provider-fence wait expired, the client received a + * conservative response and stopped the IPC pool. Fail-stop rather + * than running normal IPC cleanup: a replacement daemon may already + * own the socket pathname. Normal startup can steal a stale, + * non-listening pathname after this process exits. + */ + if (fsm_listen__flush_failed(state)) { + _exit(1); + } +#endif + /* * The fsmonitor listener thread may have received a shutdown * event from the IPC thread pool, but it doesn't hurt to tell @@ -1355,6 +1655,16 @@ static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state) */ if (listener_started) { fsm_listen__stop_async(state); +#ifdef __APPLE__ + /* + * Normal shutdown can cancel a provider fence which started after + * the first check above. Preserve the same fail-stop IPC teardown + * so a replacement daemon cannot lose its socket pathname. + */ + if (fsm_listen__flush_failed(state)) { + _exit(1); + } +#endif pthread_join(state->listener_thread, NULL); } @@ -1379,6 +1689,9 @@ static int fsmonitor_run_daemon(void) int err; memset(&state, 0, sizeof(state)); + truncate_delay_seconds = git_env_ulong( + "GIT_TEST_FSMONITOR_TRUNCATE_DELAY_SECONDS", + MY_TIME_DELAY_SECONDS); hashmap_init(&state.cookies, cookies_cmp, NULL, 0); pthread_mutex_init(&state.main_lock, NULL); @@ -1386,6 +1699,7 @@ static int fsmonitor_run_daemon(void) state.listen_error_code = 0; state.health_error_code = 0; state.current_token_data = fsmonitor_new_token_data(); + state.token_generation = 1; /* Prepare to (recursively) watch the directory. */ strbuf_init(&state.path_worktree_watch, 0); @@ -1504,6 +1818,9 @@ static int fsmonitor_run_daemon(void) err = fsmonitor_run_daemon_1(&state); done: + /* Stop provider callbacks before releasing the state they publish to. */ + fsm_listen__dtor(&state); + fsmonitor_free_token_data(state.current_token_data); state.current_token_data = NULL; pthread_cond_destroy(&state.cookies_cond); @@ -1517,7 +1834,6 @@ static int fsmonitor_run_daemon(void) hashmap_clear_and_free(&state.cookies, struct fsmonitor_cookie_item, entry); } - fsm_listen__dtor(&state); fsm_health__dtor(&state); ipc_server_free(state.ipc_server_data); diff --git a/builtin/merge.c b/builtin/merge.c index a7fcf6d8080e57..a8438823c591b8 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -44,6 +44,7 @@ #include "merge-ort-wrappers.h" #include "resolve-undo.h" #include "remote.h" +#include "reset.h" #include "fmt-merge-msg.h" #include "sequencer.h" #include "string-list.h" @@ -392,13 +393,13 @@ static void read_empty(const struct object_id *oid) static void reset_hard(const struct object_id *oid) { - struct child_process cmd = CHILD_PROCESS_INIT; - - strvec_pushl(&cmd.args, "read-tree", "-v", "--reset", "-u", - oid_to_hex(oid), NULL); - cmd.git_cmd = 1; + struct reset_working_tree_options opts = { + .oid = oid, + .flags = RESET_WORKING_TREE_HARD | + RESET_WORKING_TREE_PRESERVE_SEMANTIC_HISTORY, + }; - if (run_command(&cmd)) + if (reset_working_tree(the_repository, &opts) < 0) die(_("read-tree failed")); } @@ -759,6 +760,9 @@ static int read_tree_trivial(struct object_id *common, struct object_id *head, opts.trivial_merges_only = 1; opts.merge = 1; opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ + opts.preserve_semantic_history = + clean_status_revalidated_token_matches(the_repository->index); + opts.preserve_untracked_history = opts.preserve_semantic_history; trees[nr_trees] = repo_parse_tree_indirect(the_repository, common); if (!trees[nr_trees++]) return -1; @@ -788,19 +792,50 @@ static void write_tree_trivial(struct object_id *oid) die(_("git write-tree failed to write a tree")); } +/* + * An external strategy may replace the index before it succeeds or fails. + * After a clean result, or after restore_state() has put the index and + * worktree back into a known state, certify that state instead of trusting + * history left by the child process. + */ +static void repair_merge_fsmonitor_proof(int had_full_proof) +{ + struct lock_file lock = LOCK_INIT; + + if (!had_full_proof) + return; + if (repo_hold_locked_index(the_repository, &lock, + LOCK_REPORT_ON_ERROR) < 0) + die(_("unable to write new index file")); + if (wt_status_repair_fsmonitor_proof_after_worktree_update( + the_repository, &lock, had_full_proof) < 0) { + rollback_lock_file(&lock); + die(_("unable to repair new index file")); + } + if (write_locked_index(the_repository->index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("unable to write new index file")); +} + static int try_merge_strategy(const char *strategy, struct commit_list *common, struct commit_list *remoteheads, - struct commit *head) + struct commit *head, int repair_after_merge) { const char *head_arg = "HEAD"; + int use_builtin_strategy = !strcmp(strategy, "recursive") || + !strcmp(strategy, "subtree") || !strcmp(strategy, "ort"); + + if (use_builtin_strategy && !repair_after_merge) + repair_after_merge = + wt_status_prepare_fsmonitor_proof_for_worktree_update( + the_repository); if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, SKIP_IF_UNCHANGED, 0, NULL, NULL, NULL) < 0) die(_("Unable to write index.")); - if (!strcmp(strategy, "recursive") || !strcmp(strategy, "subtree") || - !strcmp(strategy, "ort")) { + if (use_builtin_strategy) { struct lock_file lock = LOCK_INIT; int clean, x; struct commit *result; @@ -841,14 +876,24 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common, rollback_lock_file(&lock); return 2; } + if (wt_status_repair_fsmonitor_proof_after_worktree_update( + the_repository, &lock, repair_after_merge) < 0) { + rollback_lock_file(&lock); + die(_("unable to repair new index file")); + } if (write_locked_index(the_repository->index, &lock, COMMIT_LOCK | SKIP_IF_UNCHANGED)) die(_("unable to write %s"), repo_get_index_file(the_repository)); return clean ? 0 : 1; } else { - return try_merge_command(the_repository, - strategy, xopts.nr, xopts.v, - common, head_arg, remoteheads); + int ret = try_merge_command(the_repository, + strategy, xopts.nr, xopts.v, + common, head_arg, remoteheads); + + if (!ret) + repair_merge_fsmonitor_proof( + repair_after_merge); + return ret; } } @@ -988,14 +1033,28 @@ static void prepare_to_commit(struct commit_list *remoteheads) strbuf_release(&msg); } -static int merge_trivial(struct commit *head, struct commit_list *remoteheads) +static int merge_trivial(struct commit *head, struct commit_list *remoteheads, + int repair_after_merge) { + struct lock_file lock = LOCK_INIT; struct object_id result_tree, result_commit; struct commit_list *parents = NULL, **pptr = &parents; + int refresh_error; - if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, - SKIP_IF_UNCHANGED, 0, NULL, NULL, - NULL) < 0) + if (repo_hold_locked_index(the_repository, &lock, + LOCK_REPORT_ON_ERROR) < 0) + return error(_("Unable to write index.")); + refresh_error = refresh_index(the_repository->index, REFRESH_QUIET, + NULL, NULL, NULL); + if (wt_status_repair_fsmonitor_proof_after_worktree_update( + the_repository, &lock, repair_after_merge) < 0) { + rollback_lock_file(&lock); + return error(_("unable to repair new index file")); + } + if (write_locked_index(the_repository->index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + return error(_("Unable to write index.")); + if (refresh_error) return error(_("Unable to write index.")); write_tree_trivial(&result_tree); @@ -1372,6 +1431,8 @@ int cmd_merge(int argc, struct strbuf buf = STRBUF_INIT; int i, ret = 0, head_subsumed; int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + int merge_committed = 0, reissue_sidecar = 0; + int repair_after_merge = 0; struct commit_list *common = NULL; const char *best_strategy = NULL, *wt_strategy = NULL; struct commit_list *remoteheads = NULL, *p; @@ -1472,10 +1533,12 @@ int cmd_merge(int argc, goto done; } - if (fast_forward != FF_NO && !getenv(INDEX_ENVIRONMENT) && + if (!getenv(INDEX_ENVIRONMENT) && !clean_status_config_read_repository(the_repository, &clean_digest)) { clean_status_enable_external_history(the_repository); clean_status_set_config_digest(the_repository, &clean_digest); + reissue_sidecar = + wt_status_clean_sidecar_present(the_repository); } if (repo_read_index_unmerged(the_repository)) @@ -1732,10 +1795,16 @@ int cmd_merge(int argc, /* See if it is really trivial. */ git_committer_info(IDENT_STRICT); printf(_("Trying really trivial in-index merge...\n")); + repair_after_merge = + wt_status_prepare_fsmonitor_proof_for_worktree_update( + the_repository); if (!read_tree_trivial(&common->item->object.oid, &head_commit->object.oid, &remoteheads->item->object.oid)) { - ret = merge_trivial(head_commit, remoteheads); + ret = merge_trivial(head_commit, remoteheads, + repair_after_merge); + if (!ret) + merge_committed = 1; goto done; } printf(_("Nope.\n")); @@ -1767,6 +1836,10 @@ int cmd_merge(int argc, if (fast_forward == FF_ONLY) die_ff_impossible(); + repair_after_merge = + wt_status_prepare_fsmonitor_proof_for_worktree_update( + the_repository); + if (autostash) create_autostash_ref(the_repository, "MERGE_AUTOSTASH", NULL, false); @@ -1794,6 +1867,7 @@ int cmd_merge(int argc, if (i) { printf(_("Rewinding the tree to pristine...\n")); restore_state(&head_commit->object.oid, &stash); + repair_merge_fsmonitor_proof(repair_after_merge); } if (use_strategies_nr != 1) printf(_("Trying merge strategy %s...\n"), @@ -1806,7 +1880,8 @@ int cmd_merge(int argc, ret = try_merge_strategy(wt_strategy, common, remoteheads, - head_commit); + head_commit, + repair_after_merge); /* * The backend exits with 1 when conflicts are * left to be resolved, with 2 when it does not @@ -1839,6 +1914,8 @@ int cmd_merge(int argc, ret = finish_automerge(head_commit, head_subsumed, common, remoteheads, &result_tree, wt_strategy); + if (!ret) + merge_committed = 1; goto done; } @@ -1848,6 +1925,7 @@ int cmd_merge(int argc, */ if (!best_strategy) { restore_state(&head_commit->object.oid, &stash); + repair_merge_fsmonitor_proof(repair_after_merge); if (use_strategies_nr > 1) fprintf(stderr, _("No merge strategy handled the merge.\n")); @@ -1863,10 +1941,11 @@ int cmd_merge(int argc, else { printf(_("Rewinding the tree to pristine...\n")); restore_state(&head_commit->object.oid, &stash); + repair_merge_fsmonitor_proof(repair_after_merge); printf(_("Using the %s strategy to prepare resolving by hand.\n"), best_strategy); try_merge_strategy(best_strategy, common, remoteheads, - head_commit); + head_commit, repair_after_merge); } if (squash) { @@ -1885,6 +1964,11 @@ int cmd_merge(int argc, printf(_("When finished, apply stashed changes with `git stash pop`\n")); done: + if (!ret && merge_committed && reissue_sidecar && + repair_after_merge && use_optional_locks() && + !hook_exists(the_repository, "post-index-change")) + wt_status_reissue_clean_sidecar_after_worktree_update( + the_repository, repair_after_merge, &clean_digest); if (!automerge_was_ok) { commit_list_free(common); commit_list_free(remoteheads); diff --git a/builtin/reset.c b/builtin/reset.c index 0d8660fa3b9f46..5e7ffa8ffd0dcc 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -41,6 +41,7 @@ #include "trace2.h" #include "dir.h" #include "add-interactive.h" +#include "wt-status.h" #define REFRESH_INDEX_DELAY_WARNING_IN_MS (2 * 1000) @@ -100,6 +101,11 @@ static int reset_index(const char *ref, const struct object_id *oid, int reset_t } repo_read_index_unmerged(the_repository); + if (reset_type == HARD && + clean_status_revalidated_token_matches(the_repository->index)) { + opts.preserve_semantic_history = 1; + opts.preserve_untracked_history = 1; + } if (reset_type == KEEP) { struct object_id head_oid; @@ -529,6 +535,12 @@ int cmd_reset(int argc, if (reset_type != SOFT) { struct lock_file lock = LOCK_INIT; unsigned int write_flags = COMMIT_LOCK; + int repair_after_reset = reset_type == HARD && + clean_status_has_current_full_fsmonitor_proof( + the_repository->index); + /* A missing index has not acquired an on-disk version yet. */ + int prime_after_reset = reset_type == HARD && + !the_repository->index->version; repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); @@ -580,6 +592,14 @@ int cmd_reset(int argc, !the_repository->index->cache_changed && !hook_exists(the_repository, "post-index-change")) write_flags |= SKIP_IF_UNCHANGED; + if (prime_after_reset) { + if (wt_status_prime_fsmonitor_proof_after_worktree_update( + the_repository, &lock) < 0) + die(_("Could not prepare new index file.")); + } else if (reset_type == HARD && + wt_status_repair_fsmonitor_proof_after_worktree_update( + the_repository, &lock, repair_after_reset) < 0) + die(_("Could not repair new index file.")); if (write_locked_index(the_repository->index, &lock, write_flags)) die(_("Could not write new index file.")); } diff --git a/builtin/rm.c b/builtin/rm.c index 39636abb93aedd..20a5d41707de68 100644 --- a/builtin/rm.c +++ b/builtin/rm.c @@ -423,9 +423,12 @@ int cmd_rm(int argc, path, strlen(path)); if (preserve_clean_history && (pos < 0 || - !clean_status_index_entry_is_semantically_safe( - the_repository->index, - the_repository->index->cache[pos], NULL))) + (!clean_status_index_entry_is_semantically_safe( + the_repository->index, + the_repository->index->cache[pos], NULL) && + !clean_status_intent_to_add_change_is_semantically_safe( + the_repository->index, + the_repository->index->cache[pos], NULL)))) clean_status_invalidate_current_proof(the_repository->index); if (remove_file_from_index(the_repository->index, path)) diff --git a/builtin/stash.c b/builtin/stash.c index 60a63ef004435a..6f2ef4ac9aed7b 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -6,10 +6,12 @@ #include "clean-status-config.h" #include "config.h" #include "environment.h" +#include "fsmonitor-ll.h" #include "fsmonitor-settings.h" #include "gettext.h" #include "hash.h" #include "hex.h" +#include "hook.h" #include "object-name.h" #include "parse-options.h" #include "refs.h" @@ -18,6 +20,7 @@ #include "unpack-trees.h" #include "merge-ort-wrappers.h" #include "strvec.h" +#include "trace2.h" #include "run-command.h" #include "dir.h" #include "entry.h" @@ -29,6 +32,7 @@ #include "revision.h" #include "setup.h" #include "sparse-index.h" +#include "wt-status.h" #include "log-tree.h" #include "diffcore.h" #include "reflog.h" @@ -397,6 +401,74 @@ static int reset_tree(struct object_id *i_tree, int update, int reset, return 0; } +static int repair_stash_fsmonitor_proof_after_update(int had_full_proof, + int worktree_updated, + int reissue_sidecar) +{ + struct lock_file lock = LOCK_INIT; + int optional_reissue = worktree_updated && reissue_sidecar; + int lock_flags = optional_reissue ? 0 : LOCK_REPORT_ON_ERROR; + int repaired; + + if (!had_full_proof) + return 0; + /* A failed cache reissue must not change a completed stash result. */ + if (repo_hold_locked_index(the_repository, &lock, lock_flags) < 0) { + if (optional_reissue) { + trace2_data_string("status", the_repository, + "clean-proof/writer-sidecar-skip", + "index-lock"); + return 0; + } + return error(_("could not write index")); + } + + /* Child commands and canonical publications may have replaced the inode. */ + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) { + rollback_lock_file(&lock); + if (optional_reissue) { + trace2_data_string("status", the_repository, + "clean-proof/writer-sidecar-skip", + "index-read"); + return 0; + } + return error(_("could not read index")); + } + if (!the_repository->index->fsmonitor_token_valid) { + the_repository->index->fsmonitor_has_run_once = 0; + refresh_fsmonitor(the_repository->index); + } + repaired = worktree_updated && reissue_sidecar ? + wt_status_repair_fsmonitor_proof_after_update_with_sidecar( + the_repository, &lock, had_full_proof, + &stash_clean_digest) : worktree_updated ? + wt_status_repair_fsmonitor_proof_after_worktree_update( + the_repository, &lock, had_full_proof) : + wt_status_repair_fsmonitor_proof_after_index_update( + the_repository, &lock, had_full_proof); + if (repaired < 0) { + rollback_lock_file(&lock); + if (optional_reissue) { + trace2_data_string("status", the_repository, + "clean-proof/writer-sidecar-skip", + "proof-repair"); + return 0; + } + return error(_("could not repair index")); + } + if (optional_reissue) { + if (!repaired) + rollback_lock_file(&lock); + return 0; + } + if (write_locked_index(the_repository->index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + return error(_("could not write index")); + + return 0; +} + static int create_index_from_tree(const struct object_id *tree_id, const char *index_path) { @@ -678,6 +750,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, const char *label_base) { int clean, ret; + int had_full_proof; int has_index = index; struct merge_options o; struct object_id c_tree; @@ -688,6 +761,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, clean_status_prepare_main_index_history(the_repository); repo_read_index_preload(the_repository, NULL, 0); + had_full_proof = clean_status_has_persistent_fsmonitor_semantic_history( + the_repository->index); if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, NULL, NULL, NULL)) return error(_("could not write index")); @@ -794,6 +869,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, restore_untracked: if (info->has_u && restore_untracked(&info->u_tree)) ret = error(_("could not restore untracked files from stash")); + if (!ret && repair_stash_fsmonitor_proof_after_update( + had_full_proof, 1, 0)) + ret = -1; if (!quiet) { struct child_process cp = CHILD_PROCESS_INIT; @@ -1706,6 +1784,7 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED, struct repository *repo UNUSED) { int ret; + int had_full_proof; struct strbuf stash_msg_buf = STRBUF_INIT; struct stash_info info = STASH_INFO_INIT; struct pathspec ps; @@ -1716,11 +1795,17 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED, memset(&ps, 0, sizeof(ps)); clean_status_enable_external_history(the_repository); clean_status_set_config_digest(the_repository, &stash_clean_digest); + repo_read_index_preload(the_repository, NULL, 0); + had_full_proof = clean_status_has_persistent_fsmonitor_semantic_history( + the_repository->index); if (!check_changes_tracked_files(&ps)) return 0; ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, NULL, 0, &info, NULL, 0); + if (!ret && repair_stash_fsmonitor_proof_after_update( + had_full_proof, 0, 0)) + ret = -1; if (!ret) printf_ln("%s", oid_to_hex(&info.w_commit)); @@ -1736,6 +1821,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q { int ret = 0; int preserve_clean_history = !ps->nr && !include_untracked; + int had_clean_sidecar; + int had_full_proof; struct lock_file index_lock = LOCK_INIT; struct stash_info info = STASH_INFO_INIT; struct strbuf patch = STRBUF_INIT; @@ -1772,6 +1859,9 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q */ clean_status_prepare_main_index_history(the_repository); repo_read_index_preload(the_repository, NULL, 0); + had_full_proof = clean_status_has_persistent_fsmonitor_semantic_history( + the_repository->index); + had_clean_sidecar = wt_status_clean_sidecar_present(the_repository); if (!include_untracked && ps->nr) { char *ps_matched = xcalloc(ps->nr, 1); @@ -1804,12 +1894,6 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q printf_ln(_("No local changes to save")); goto done; } - if (preserve_clean_history && !(patch_mode || only_staged)) { - clean_status_invalidate_current_proof(the_repository->index); - if (clean_status_should_write_fsmonitor_config( - the_repository->index)) - the_repository->index->cache_changed |= FSMONITOR_CHANGED; - } if (write_locked_index(the_repository->index, &index_lock, COMMIT_LOCK | SKIP_IF_UNCHANGED)) { ret = error(_("could not write index")); @@ -1975,6 +2059,13 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q goto done; } } + if (had_clean_sidecar && use_optional_locks() && + !hook_exists(the_repository, "post-index-change") && + repair_stash_fsmonitor_proof_after_update( + had_full_proof, 1, 1)) { + ret = -1; + goto done; + } goto done; } else { struct child_process cp = CHILD_PROCESS_INIT; @@ -2002,6 +2093,12 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q goto done; } } + if (preserve_clean_history && + repair_stash_fsmonitor_proof_after_update( + had_full_proof, 1, 0)) { + ret = -1; + goto done; + } goto done; } diff --git a/builtin/worktree.c b/builtin/worktree.c index 654d27c3e1ce99..c45e5ada889a7c 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -9,6 +9,7 @@ #include "copy.h" #include "dir.h" #include "environment.h" +#include "fsmonitor-settings.h" #include "gettext.h" #include "hex.h" #include "object-file.h" @@ -21,12 +22,14 @@ #include "refs.h" #include "remote.h" #include "run-command.h" +#include "repo-settings.h" #include "hook.h" #include "sigchain.h" #include "submodule.h" #include "utf8.h" #include "worktree.h" #include "quote.h" +#include "trace2.h" #define BUILTIN_WORKTREE_ADD_USAGE \ N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason ]]\n" \ @@ -398,17 +401,76 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir) } static int checkout_worktree(const struct add_opts *opts, - struct strvec *child_env) + struct strvec *child_env, const char *path) { struct child_process cp = CHILD_PROCESS_INIT; cp.git_cmd = 1; strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); if (opts->quiet) strvec_push(&cp.args, "--quiet"); - strvec_pushv(&cp.env, child_env->v); + if (path) { + /* Let the checkout authenticate the registered worktree's index. */ + cp.dir = path; + strvec_pushl(&cp.env, GIT_DIR_ENVIRONMENT, + GIT_WORK_TREE_ENVIRONMENT, NULL); + } else { + strvec_pushv(&cp.env, child_env->v); + } return run_command(&cp); } +static void prime_worktree_clean_status_proof(const char *path) +{ + struct child_process cp = CHILD_PROCESS_INIT; + int ret; + + /* + * The checkout may have certified its new index, but post-checkout can + * change files or replace the index. Recheck while optional writes are + * allowed so that later read-only status can reuse a durable proof. + */ + cp.git_cmd = 1; + cp.dir = path; + cp.no_stdin = 1; + cp.no_stdout = 1; + cp.no_stderr = 1; + strvec_pushl(&cp.args, "status", "--porcelain=v2", + "--untracked-files=normal", NULL); + strvec_push(&cp.env, GIT_DIR_ENVIRONMENT); + strvec_push(&cp.env, GIT_WORK_TREE_ENVIRONMENT); + ret = run_command(&cp); + trace2_data_intmax("worktree", the_repository, + "add/clean-status-primed", !ret); +} + +static int worktree_clean_status_proof_is_enabled(const char *git_dir, + const char *work_tree) +{ + struct repository repo; + int enabled; + + if (repo_init(&repo, git_dir, work_tree)) + return 0; + prepare_repo_settings(&repo); + enabled = fsm_settings__get_mode(&repo) == FSMONITOR_MODE_IPC && + repo.settings.core_untracked_cache == UNTRACKED_CACHE_WRITE; + repo_clear(&repo); + return enabled; +} + +static int worktree_clean_status_context_is_supported(void) +{ + const char *global = getenv("GIT_CONFIG_GLOBAL"); + const char *system = getenv("GIT_CONFIG_SYSTEM"); + + return !getenv(INDEX_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !getenv(DB_ENVIRONMENT) && + !getenv(ALTERNATE_DB_ENVIRONMENT) && + (!global || !*global || is_absolute_path(global)) && + (!system || !*system || is_absolute_path(system)); +} + static int make_worktree_orphan(const char * ref, const struct add_opts *opts, struct strvec *child_env) { @@ -591,9 +653,11 @@ static int add_worktree(const char *path, const char *refname, goto done; if (opts->checkout && - (ret = checkout_worktree(opts, &child_env))) + (ret = checkout_worktree(opts, &child_env, + worktree_clean_status_context_is_supported() && + worktree_clean_status_proof_is_enabled(sb_repo.buf, path) ? + path : NULL))) goto done; - is_junk = 0; FREE_AND_NULL(junk_work_tree); FREE_AND_NULL(junk_git_dir); @@ -622,6 +686,10 @@ static int add_worktree(const char *path, const char *refname, ret = run_hooks_opt(the_repository, "post-checkout", &opt); } + if (!ret && opts->checkout && use_optional_locks() && + worktree_clean_status_context_is_supported() && + worktree_clean_status_proof_is_enabled(sb_repo.buf, path)) + prime_worktree_clean_status_proof(path); strvec_clear(&child_env); strbuf_release(&sb); diff --git a/clean-status-config.c b/clean-status-config.c index e6523e193ccaaa..67c5d780ada36b 100644 --- a/clean-status-config.c +++ b/clean-status-config.c @@ -19,9 +19,11 @@ #define CLEAN_STATUS_FILTER_SMUDGE (1U << 1) #define CLEAN_STATUS_FILTER_PROCESS (1U << 2) #define CLEAN_STATUS_FILTER_REQUIRED (1U << 3) +#define CLEAN_STATUS_FILTER_READ_DISABLED \ + (CLEAN_STATUS_FILTER_CLEAN | CLEAN_STATUS_FILTER_PROCESS | \ + CLEAN_STATUS_FILTER_REQUIRED) #define CLEAN_STATUS_FILTER_COMPLETE \ - (CLEAN_STATUS_FILTER_CLEAN | CLEAN_STATUS_FILTER_SMUDGE | \ - CLEAN_STATUS_FILTER_PROCESS | CLEAN_STATUS_FILTER_REQUIRED) + (CLEAN_STATUS_FILTER_READ_DISABLED | CLEAN_STATUS_FILTER_SMUDGE) struct clean_status_pending_filter_entry { char *key; @@ -90,8 +92,17 @@ static int config_is_command_transport(const char *key, if (!ctx || !ctx->kvi || ctx->kvi->scope != CONFIG_SCOPE_COMMAND) return 0; + if (!strcmp(key, "protocol.version") || + !strcmp(key, "fetch.uriprotocols") || + !strcmp(key, "push.negotiate") || + starts_with(key, "http.")) + return 1; if (starts_with(key, "credential.")) return 1; + if (!parse_config_key(key, "remote", &subsection, &subsection_len, + &subkey) && subsection && subsection_len && + !strcmp(subkey, "pushurl")) + return 1; if (parse_config_key(key, "url", &subsection, &subsection_len, &subkey) || !subsection || !subsection_len) return 0; @@ -246,17 +257,25 @@ static void hash_retained_config_entry( static void flush_pending_filter(struct clean_status_config_digest *digest) { struct clean_status_pending_filter *pending = digest->pending_filter; + int read_disabled; if (!pending) return; - /* Remember command overrides omitted from the authenticated digest. */ - if (pending->mask == CLEAN_STATUS_FILTER_COMPLETE) + /* + * Read-side conversion does not use the smudge command. Disabling clean + * and process, with required=false, is sufficient for an authenticated + * filter-free scope. Keep recording the override so that it cannot + * publish a sidecar without rechecking that scope. + */ + read_disabled = (pending->mask & CLEAN_STATUS_FILTER_READ_DISABLED) == + CLEAN_STATUS_FILTER_READ_DISABLED; + if (read_disabled) digest->normalized_filter_disable = 1; for (unsigned i = 0; i < pending->nr; i++) { struct clean_status_pending_filter_entry *entry = &pending->entries[i]; - if (pending->mask != CLEAN_STATUS_FILTER_COMPLETE) { + if (!read_disabled) { struct key_value_info kvi = KVI_INIT; struct config_context ctx = { .kvi = &kvi }; diff --git a/clean-status-epoch.c b/clean-status-epoch.c index f78bf8fb6bd78f..1041ba45d794db 100644 --- a/clean-status-epoch.c +++ b/clean-status-epoch.c @@ -22,6 +22,7 @@ struct clean_status_proof_epoch { unsigned char attr_hash[GIT_MAX_RAWSZ]; unsigned char attr_namespace_hash[GIT_MAX_RAWSZ]; unsigned char manifest_hash[GIT_MAX_RAWSZ]; + char *index_path; uint32_t manifest_flags; unsigned semantic_explicit : 1; unsigned attr_sources_present : 1; @@ -54,10 +55,10 @@ static int config_matches_epoch( algo->rawsz); } -struct clean_status_proof_epoch *clean_status_capture_proof_epoch( +static struct clean_status_proof_epoch *capture_proof_epoch( struct index_state *istate, const struct attr_source_snapshot *attrs, - int validate_filter_scope) + int validate_filter_scope, const char *index_path) { struct clean_status_state *state = istate->clean_status; struct clean_status_proof_epoch *epoch; @@ -97,12 +98,17 @@ struct clean_status_proof_epoch *clean_status_capture_proof_epoch( istate->repo->hash_algo->rawsz) || memcmp(digest.semantic_hash, state->current_semantic_hash, istate->repo->hash_algo->rawsz) || - clean_status_index_snapshot_pin_proof_epoch(&index, istate)) + (index_path ? + clean_status_index_snapshot_pin_path_proof_epoch( + &index, istate, index_path) : + clean_status_index_snapshot_pin_proof_epoch(&index, istate))) return NULL; CALLOC_ARRAY(epoch, 1); epoch->istate = istate; epoch->index = index; + epoch->index_path = xstrdup(index_path ? index_path : + istate->repo->index_file); epoch->scan_start_token = xstrdup(istate->fsmonitor_last_update_pending); memcpy(epoch->config_hash, state->current_config_hash, istate->repo->hash_algo->rawsz); @@ -127,6 +133,26 @@ struct clean_status_proof_epoch *clean_status_capture_proof_epoch( return epoch; } +struct clean_status_proof_epoch *clean_status_capture_proof_epoch( + struct index_state *istate, + const struct attr_source_snapshot *attrs, + int validate_filter_scope) +{ + return capture_proof_epoch( + istate, attrs, validate_filter_scope, NULL); +} + +struct clean_status_proof_epoch *clean_status_capture_proof_epoch_at_path( + struct index_state *istate, + const struct attr_source_snapshot *attrs, + int validate_filter_scope, const char *index_path) +{ + if (!index_path || !*index_path) + return NULL; + return capture_proof_epoch( + istate, attrs, validate_filter_scope, index_path); +} + int clean_status_proof_epoch_start_token_matches( struct index_state *istate, const struct clean_status_proof_epoch *epoch) @@ -174,8 +200,8 @@ static int proof_epoch_matches( memcmp(state->manifest.current_hash, epoch->manifest_hash, algo->rawsz) || !config_matches_epoch(istate, epoch) || - !clean_status_index_snapshot_still_matches_proof_epoch( - &epoch->index, istate)) + !clean_status_index_snapshot_still_matches_path_proof_epoch( + &epoch->index, istate, epoch->index_path)) goto done; matched = 1; done: @@ -217,6 +243,7 @@ void clean_status_release_proof_epoch( if (!epoch) return; clean_status_index_snapshot_release(&epoch->index); + free(epoch->index_path); free(epoch->scan_start_token); free(epoch); } diff --git a/clean-status-fast.c b/clean-status-fast.c index 57b6d19d4b0179..c81fdda80f648a 100644 --- a/clean-status-fast.c +++ b/clean-status-fast.c @@ -14,7 +14,6 @@ #include "repository.h" #include "semantic-verify-internal.h" #include "trace2.h" -#include "worktree.h" #include "wrapper.h" #if !EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN @@ -188,15 +187,6 @@ static int fast_path_test_barrier(void) return ret; } -static int current_worktree_is_main(struct repository *repo) -{ - struct worktree *worktree = get_current_worktree(repo); - int ret = worktree && is_main_worktree(worktree); - - free_worktree(worktree); - return ret; -} - int clean_status_try_sidecar( struct repository *repo, const struct clean_status_config_digest *config, @@ -222,7 +212,7 @@ int clean_status_try_sidecar( (config->filter_configured && config->normalized_filter_disable) || getenv(INDEX_ENVIRONMENT) || is_bare_repository(repo) || !repo_get_work_tree(repo) || - !current_worktree_is_main(repo) || + !clean_status_worktree_shape_supported(repo) || fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC) { trace_miss(repo, "fast-repository-shape"); goto done; diff --git a/clean-status-history.c b/clean-status-history.c index 97a501424c0f58..5e6ae97d434d79 100644 --- a/clean-status-history.c +++ b/clean-status-history.c @@ -19,6 +19,7 @@ #include "repository.h" #include "semantic-verify-internal.h" #include "strbuf.h" +#include "strmap.h" #include "trace2.h" #include "ewah/ewok.h" @@ -2018,22 +2019,187 @@ int clean_status_transfer_current_proof_if_same_index( return replace_current_fsmonitor_proof(dst, src); } -int clean_status_transfer_current_proof_if_semantically_same_index( - struct index_state *dst, const struct index_state *src) +enum checkout_policy_change { + CHECKOUT_POLICY_NONE = 0, + CHECKOUT_POLICY_IGNORE = 1 << 0, + CHECKOUT_POLICY_ATTRIBUTES = 1 << 1, +}; + +struct checkout_policy_change_context { + enum checkout_policy_change kinds; + struct strset attribute_directories; +}; + +static enum checkout_policy_change checkout_policy_change_kind( + const struct cache_entry *old, const struct cache_entry *new_entry) +{ + const struct cache_entry *entry = old ? old : new_entry; + const char *base; + + if (!entry || + (old && (!S_ISREG(old->ce_mode) || ce_stage(old) || + ce_skip_worktree(old) || ce_intent_to_add(old) || + (old->ce_flags & CE_VALID))) || + (new_entry && (!S_ISREG(new_entry->ce_mode) || ce_stage(new_entry) || + ce_skip_worktree(new_entry) || + ce_intent_to_add(new_entry) || + (new_entry->ce_flags & CE_VALID))) || + (old && new_entry && + (strcmp(old->name, new_entry->name) || old->ce_mode != new_entry->ce_mode))) + return CHECKOUT_POLICY_NONE; + base = strrchr(entry->name, '/'); + base = base ? base + 1 : entry->name; + if (!fspathcmp(base, GITATTRIBUTES_FILE)) + return CHECKOUT_POLICY_ATTRIBUTES; + if (!fspathcmp(base, ".gitignore")) + return CHECKOUT_POLICY_IGNORE; + return CHECKOUT_POLICY_NONE; +} + +static int record_checkout_policy_change( + struct checkout_policy_change_context *context, + const struct cache_entry *old, const struct cache_entry *new_entry) +{ + enum checkout_policy_change change = + checkout_policy_change_kind(old, new_entry); + const struct cache_entry *entry = old ? old : new_entry; + + if (!change) + return 0; + context->kinds |= change; + if (change == CHECKOUT_POLICY_ATTRIBUTES) { + const char *slash = strrchr(entry->name, '/'); + char *directory = slash ? + xmemdupz(entry->name, slash - entry->name + 1) : + xstrdup(""); + + strset_add(&context->attribute_directories, directory); + free(directory); + } + return 1; +} + +static void collect_checkout_policy_changes( + struct checkout_policy_change_context *context, + const struct index_state *dst, const struct index_state *src) { const unsigned int semantic_flags = CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS; unsigned int src_pos = 0, dst_pos = 0; - int transferred; + while (src_pos < src->cache_nr || dst_pos < dst->cache_nr) { + const struct cache_entry *old = src_pos < src->cache_nr ? + src->cache[src_pos] : NULL; + const struct cache_entry *new_entry = dst_pos < dst->cache_nr ? + dst->cache[dst_pos] : NULL; + int cmp; + + if (!old) + cmp = 1; + else if (!new_entry) + cmp = -1; + else + cmp = strcmp(old->name, new_entry->name); + if (cmp < 0) { + record_checkout_policy_change(context, old, NULL); + src_pos++; + } else if (cmp > 0) { + record_checkout_policy_change(context, NULL, new_entry); + dst_pos++; + } else { + if (old->ce_mode != new_entry->ce_mode || + !oideq(&old->oid, &new_entry->oid) || + ((old->ce_flags ^ new_entry->ce_flags) & semantic_flags)) + record_checkout_policy_change(context, old, + new_entry); + src_pos++; + dst_pos++; + } + } +} + +static int checkout_policy_scope_entry_has_safe_shape( + const struct cache_entry *old, const struct cache_entry *new_entry) +{ + const struct cache_entry *entry = old ? old : new_entry; + + if (!entry || + (old && (!S_ISREG(old->ce_mode) && !S_ISLNK(old->ce_mode))) || + (new_entry && (!S_ISREG(new_entry->ce_mode) && + !S_ISLNK(new_entry->ce_mode))) || + (old && (ce_stage(old) || ce_skip_worktree(old) || + ce_intent_to_add(old) || (old->ce_flags & CE_VALID))) || + (new_entry && (ce_stage(new_entry) || ce_skip_worktree(new_entry) || + ce_intent_to_add(new_entry) || + (new_entry->ce_flags & CE_VALID))) || + (old && new_entry && + (strcmp(old->name, new_entry->name) || old->ce_mode != new_entry->ce_mode))) + return 0; + return 1; +} + +static int checkout_entry_is_in_changed_attribute_scope( + struct checkout_policy_change_context *context, + const struct cache_entry *old, const struct cache_entry *new_entry) +{ + const struct cache_entry *entry = old ? old : new_entry; + struct strbuf directory = STRBUF_INIT; + const char *slash = entry->name; + int found = strset_contains(&context->attribute_directories, ""); + + while (!found && (slash = strchr(slash, '/')) != NULL) { + strbuf_reset(&directory); + strbuf_add(&directory, entry->name, slash - entry->name + 1); + found = strset_contains(&context->attribute_directories, + directory.buf); + slash++; + } + strbuf_release(&directory); + return found; +} + +static int checkout_policy_change_allows_entry( + struct checkout_policy_change_context *context, + const struct cache_entry *old, const struct cache_entry *new_entry) +{ + if (record_checkout_policy_change(context, old, new_entry)) + return 1; + return (context->kinds & CHECKOUT_POLICY_ATTRIBUTES) && + checkout_policy_scope_entry_has_safe_shape(old, new_entry) && + checkout_entry_is_in_changed_attribute_scope(context, old, + new_entry); +} + +static int transfer_current_proof_if_semantically_same_index( + struct index_state *dst, const struct index_state *src, + int allow_checkout_policy_changes, + int *manifest_refresh_required) +{ + const unsigned int semantic_flags = + CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS; + unsigned int src_pos = 0, dst_pos = 0; + struct checkout_policy_change_context policy_changes = { + .attribute_directories = STRSET_INIT, + }; + int transferred = 0; + + if (manifest_refresh_required) + *manifest_refresh_required = 0; + + /* + * Clearing resolve-undo changes only its optional index extension. + * A live resolve-undo map is still rejected below, but its removal does + * not change tracked entries or the worktree state certified here. + */ if (!current_proof_is_writable(src) || src->repo != dst->repo || src->split_index || dst->split_index || src->sparse_index || dst->sparse_index || - (src->cache_changed & RESOLVE_UNDO_CHANGED) || src->resolve_undo || !src->fsmonitor_last_update || !dst->fsmonitor_last_update || strcmp(src->fsmonitor_last_update, dst->fsmonitor_last_update)) return 0; + if (allow_checkout_policy_changes) + collect_checkout_policy_changes(&policy_changes, dst, src); while (src_pos < src->cache_nr || dst_pos < dst->cache_nr) { const struct cache_entry *old = src_pos < src->cache_nr ? @@ -2050,25 +2216,37 @@ int clean_status_transfer_current_proof_if_semantically_same_index( cmp = strcmp(old->name, new_entry->name); if (cmp < 0) { if (!clean_status_index_entry_is_semantically_safe( - src, old, NULL)) - return 0; + src, old, NULL) && + (!allow_checkout_policy_changes || + !checkout_policy_change_allows_entry(&policy_changes, + old, NULL))) + goto done; src_pos++; } else if (cmp > 0) { if (!clean_status_index_entry_is_semantically_safe( - src, NULL, new_entry)) - return 0; + src, NULL, new_entry) && + (!allow_checkout_policy_changes || + !checkout_policy_change_allows_entry(&policy_changes, + NULL, new_entry))) + goto done; dst_pos++; } else { if ((old->ce_mode != new_entry->ce_mode || !oideq(&old->oid, &new_entry->oid) || ((old->ce_flags ^ new_entry->ce_flags) & semantic_flags)) && !clean_status_index_entry_is_semantically_safe( - src, old, new_entry)) - return 0; + src, old, new_entry) && + (!allow_checkout_policy_changes || + !checkout_policy_change_allows_entry(&policy_changes, + old, new_entry))) + goto done; src_pos++; dst_pos++; } } + if (manifest_refresh_required && + (policy_changes.kinds & CHECKOUT_POLICY_ATTRIBUTES)) + *manifest_refresh_required = 1; if (current_proof_is_writable(dst)) { const struct clean_status_state *src_state = src->clean_status; @@ -2088,10 +2266,11 @@ int clean_status_transfer_current_proof_if_semantically_same_index( memcmp(src_state->manifest.current.buf, dst_state->manifest.current.buf, src_state->manifest.current.len)) - return 0; + goto done; trace2_data_intmax("fsmonitor", dst->repo, "history/semantic-transferred", 1); - return 1; + transferred = 1; + goto done; } transferred = replace_current_fsmonitor_proof(dst, src); @@ -2099,9 +2278,31 @@ int clean_status_transfer_current_proof_if_semantically_same_index( trace2_data_intmax("fsmonitor", dst->repo, "history/semantic-transferred", 1); +done: + strset_clear(&policy_changes.attribute_directories); return transferred; } +int clean_status_transfer_current_proof_if_semantically_same_index( + struct index_state *dst, const struct index_state *src) +{ + return transfer_current_proof_if_semantically_same_index( + dst, src, 0, NULL); +} + +int clean_status_transfer_current_proof_after_checkout( + struct index_state *dst, const struct index_state *src, + int *manifest_refresh_required) +{ + /* + * A successful checkout owns these worktree writes. It may therefore + * retain history across policy-file changes, but the caller must refresh + * changed attribute sources before pairing the transferred proof. + */ + return transfer_current_proof_if_semantically_same_index( + dst, src, 1, manifest_refresh_required); +} + struct clean_status_commit_checkpoint { struct repository *repo; struct lock_file *lock; diff --git a/clean-status-index-provisional.c b/clean-status-index-provisional.c new file mode 100644 index 00000000000000..5e7b920810a838 --- /dev/null +++ b/clean-status-index-provisional.c @@ -0,0 +1,16 @@ +#include "git-compat-util.h" +#include "clean-status-index.h" +#include "lockfile.h" +#include "read-cache-ll.h" +#include "repository.h" + +int clean_status_write_index_after_provisional( + struct index_state *istate, struct lock_file *lock, unsigned flags, + struct clean_status_index_write_receipt *receipt) +{ + /* Let receipt preparation authenticate the final null trailer. */ + if (istate->repo->settings.index_skip_hash) + oidcpy(&istate->oid, istate->repo->hash_algo->null_oid); + return write_locked_index_with_receipt( + istate, lock, flags, receipt); +} diff --git a/clean-status-index.c b/clean-status-index.c index 50cb6d870d8ecc..813bb92829fdb3 100644 --- a/clean-status-index.c +++ b/clean-status-index.c @@ -115,6 +115,8 @@ static int snapshot_open( memset(snapshot, 0, sizeof(*snapshot)); snapshot->fd = -1; + if (!path || !*path) + return -1; #ifdef O_NONBLOCK flags |= O_NONBLOCK; #endif @@ -215,11 +217,12 @@ static int snapshot_matches_index_state( snapshot, state, istate->repo->hash_algo); } -static int snapshot_pin( +static int snapshot_pin_path( struct clean_status_index_snapshot *snapshot, - struct index_state *istate, int allow_process_local_source) + struct index_state *istate, const char *path, + int allow_process_local_source) { - if (snapshot_open(snapshot, istate->repo->index_file, + if (snapshot_open(snapshot, path, istate->repo->hash_algo, 1)) return -1; if (snapshot_matches_index_state( @@ -233,7 +236,8 @@ int clean_status_index_snapshot_pin( struct clean_status_index_snapshot *snapshot, struct index_state *istate) { - return snapshot_pin(snapshot, istate, 0); + return snapshot_pin_path( + snapshot, istate, istate->repo->index_file, 0); } int clean_status_index_snapshot_pin_proof_epoch( @@ -245,7 +249,15 @@ int clean_status_index_snapshot_pin_proof_epoch( * therefore use the descriptor for the file which populated that state. * Persisted history and sidecars continue to use the generic pin above. */ - return snapshot_pin(snapshot, istate, 1); + return snapshot_pin_path( + snapshot, istate, istate->repo->index_file, 1); +} + +int clean_status_index_snapshot_pin_path_proof_epoch( + struct clean_status_index_snapshot *snapshot, + struct index_state *istate, const char *path) +{ + return snapshot_pin_path(snapshot, istate, path, 1); } static int snapshot_still_matches( @@ -273,6 +285,15 @@ int clean_status_index_snapshot_still_matches_proof_epoch( return snapshot_still_matches(snapshot, istate, 1); } +int clean_status_index_snapshot_still_matches_path_proof_epoch( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate, const char *path) +{ + return snapshot_matches_index_state(snapshot, istate, 1) && + clean_status_index_snapshot_still_matches_path( + snapshot, path, istate->repo->hash_algo); +} + void clean_status_index_snapshot_release( struct clean_status_index_snapshot *snapshot) { diff --git a/clean-status-index.h b/clean-status-index.h index ddc177cef5436e..d2c6e88c896441 100644 --- a/clean-status-index.h +++ b/clean-status-index.h @@ -5,6 +5,7 @@ #include "hash.h" struct index_state; +struct lock_file; struct repository; /* @@ -52,6 +53,11 @@ int clean_status_index_adopt_write_receipt( void clean_status_index_write_receipt_release( struct clean_status_index_write_receipt *receipt); +/* Finish a provisional write without widening the ordinary status hot path. */ +int clean_status_write_index_after_provisional( + struct index_state *istate, struct lock_file *lock, unsigned flags, + struct clean_status_index_write_receipt *receipt); + int clean_status_index_snapshot_open( struct clean_status_index_snapshot *snapshot, const char *path, const struct git_hash_algo *algo); @@ -71,12 +77,18 @@ int clean_status_index_snapshot_pin( int clean_status_index_snapshot_pin_proof_epoch( struct clean_status_index_snapshot *snapshot, struct index_state *istate); +int clean_status_index_snapshot_pin_path_proof_epoch( + struct clean_status_index_snapshot *snapshot, + struct index_state *istate, const char *path); int clean_status_index_snapshot_still_matches( const struct clean_status_index_snapshot *snapshot, const struct index_state *istate); int clean_status_index_snapshot_still_matches_proof_epoch( const struct clean_status_index_snapshot *snapshot, const struct index_state *istate); +int clean_status_index_snapshot_still_matches_path_proof_epoch( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate, const char *path); void clean_status_index_snapshot_release( struct clean_status_index_snapshot *snapshot); int clean_status_index_entries_are_certifiable( diff --git a/clean-status-manifest.c b/clean-status-manifest.c index 8c7e79e0b481ed..7143e65934346f 100644 --- a/clean-status-manifest.c +++ b/clean-status-manifest.c @@ -233,6 +233,93 @@ static int directory_attribute_source_matches( return entry && entry->source == ATTR_MANIFEST_INDEX && !memcmp(entry->hash, indexed->oid.hash, algo->rawsz); } + +static int directory_attribute_sources_match_manifest( + struct index_state *istate, const char *directory, unsigned int first) +{ + struct clean_status_state *state = istate->clean_status; + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + struct attr_manifest_cursor manifest_cursor; + struct attr_manifest_entry manifest_entry; + struct string_list candidates = STRING_LIST_INIT_DUP; + struct strbuf candidate = STRBUF_INIT; + const struct git_hash_algo *algo = istate->repo->hash_algo; + const char *previous = NULL; + unsigned int namespace_unstable = 0; + size_t len = strlen(directory), previous_len = 0; + int manifest_ret, safe = 0; + + if (semantic_verify_root_init(istate->repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path) + goto done; + + strbuf_addstr(&candidate, directory); + strbuf_addstr(&candidate, GITATTRIBUTES_FILE); + string_list_append(&candidates, candidate.buf); + for (unsigned int i = first; i < istate->cache_nr && + starts_with(istate->cache[i]->name, directory); i++) { + const struct cache_entry *ce = istate->cache[i]; + const char *slash = ce->name + len; + + while ((slash = strchr(slash, '/')) != NULL) { + size_t parent_len = slash - ce->name; + + if (!previous || previous_len <= parent_len || + previous[parent_len] != '/' || + memcmp(previous, ce->name, parent_len)) { + strbuf_reset(&candidate); + strbuf_add(&candidate, ce->name, parent_len + 1); + strbuf_addstr(&candidate, GITATTRIBUTES_FILE); + string_list_append(&candidates, candidate.buf); + } + slash++; + } + previous = ce->name; + previous_len = ce_namelen(ce); + } + string_list_sort(&candidates); + string_list_remove_duplicates(&candidates, 0); + if (attr_manifest_cursor_init(&manifest_cursor, + state->manifest.current.buf, + state->manifest.current.len, algo)) + goto done; + manifest_ret = attr_manifest_cursor_next(&manifest_cursor, + &manifest_entry); + for (size_t i = 0; i < candidates.nr; i++) { + const char *name = candidates.items[i].string; + const struct attr_manifest_entry *entry = NULL; + + while (manifest_ret > 0 && + directory_manifest_entry_path_compare( + &manifest_entry, name) < 0) + manifest_ret = attr_manifest_cursor_next( + &manifest_cursor, &manifest_entry); + if (manifest_ret < 0) + goto done; + if (manifest_ret > 0 && + !directory_manifest_entry_path_compare( + &manifest_entry, name)) + entry = &manifest_entry; + if (!directory_attribute_source_matches( + istate, path, name, entry, first + i)) + goto done; + } + + semantic_verify_path_free(path, &namespace_unstable, NULL); + path = NULL; + safe = !namespace_unstable && semantic_verify_root_stable(root); + +done: + if (path) + semantic_verify_path_free(path, NULL, NULL); + semantic_verify_root_clear(root); + string_list_clear(&candidates, 0); + strbuf_release(&candidate); + return safe; +} #endif int clean_status_manifest_path_attributes_unchanged( @@ -328,25 +415,56 @@ int clean_status_manifest_path_attributes_unchanged( #endif } +int clean_status_manifest_directory_sources_unchanged( + const struct index_state *istate, const char *directory) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + unsigned int first; + size_t len; + int pos; + uint32_t required = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + + if (!state || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + state->manifest.global_fallback || + (state->manifest.current_flags & required) != required || + !attr_manifest_valid(state->manifest.current.buf, + state->manifest.current.len, algo)) + return 0; + len = strlen(directory); + if (!len || directory[len - 1] != '/') + return 0; + pos = index_name_pos((struct index_state *)istate, directory, len); + if (pos >= 0) + return 0; + first = -pos - 1; + if (first >= istate->cache_nr || + !starts_with(istate->cache[first]->name, directory)) + return 0; + return directory_attribute_sources_match_manifest( + (struct index_state *)istate, directory, first); +#else + (void)istate; + (void)directory; + return 0; +#endif +} + int clean_status_manifest_directory_unchanged( struct index_state *istate, const char *directory) { #if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN struct clean_status_state *state = istate->clean_status; - struct semantic_verify_root *root = NULL; - struct semantic_verify_path *path = NULL; struct clean_status_index_snapshot snapshot; struct clean_status_config_digest config; struct attr_fingerprint attrs; - struct attr_manifest_cursor manifest_cursor; - struct attr_manifest_entry manifest_entry; - struct string_list candidates = STRING_LIST_INIT_DUP; - struct strbuf candidate = STRBUF_INIT; const struct git_hash_algo *algo = istate->repo->hash_algo; - const char *previous = NULL; - unsigned int first, namespace_unstable = 0; - size_t len, previous_len = 0; - int pos, manifest_ret, pinned = 0, safe = 0; + unsigned int first; + size_t len; + int pos, pinned = 0, safe = 0; uint32_t required = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | FSMONITOR_CLEAN_PROOF_FULL_INDEX; @@ -399,72 +517,17 @@ int clean_status_manifest_directory_unchanged( if (clean_status_index_snapshot_pin_proof_epoch(&snapshot, istate)) goto done; pinned = 1; - if (semantic_verify_root_init(istate->repo, &root)) - goto done; - path = semantic_verify_path_new(root); - if (!path) - goto done; - - strbuf_addstr(&candidate, directory); - strbuf_addstr(&candidate, GITATTRIBUTES_FILE); - string_list_append(&candidates, candidate.buf); for (unsigned int i = first; i < istate->cache_nr && starts_with(istate->cache[i]->name, directory); i++) { const struct cache_entry *ce = istate->cache[i]; - const char *slash = ce->name + len; if (ce_stage(ce) || ce_skip_worktree(ce) || ce_intent_to_add(ce) || (ce->ce_flags & CE_VALID) || S_ISSPARSEDIR(ce->ce_mode)) goto done; - while ((slash = strchr(slash, '/')) != NULL) { - size_t parent_len = slash - ce->name; - - if (!previous || previous_len <= parent_len || - previous[parent_len] != '/' || - memcmp(previous, ce->name, parent_len)) { - strbuf_reset(&candidate); - strbuf_add(&candidate, ce->name, parent_len + 1); - strbuf_addstr(&candidate, GITATTRIBUTES_FILE); - string_list_append(&candidates, candidate.buf); - } - slash++; - } - previous = ce->name; - previous_len = ce_namelen(ce); - } - string_list_sort(&candidates); - string_list_remove_duplicates(&candidates, 0); - if (attr_manifest_cursor_init(&manifest_cursor, - state->manifest.current.buf, - state->manifest.current.len, algo)) - goto done; - manifest_ret = attr_manifest_cursor_next(&manifest_cursor, - &manifest_entry); - for (size_t i = 0; i < candidates.nr; i++) { - const char *name = candidates.items[i].string; - const struct attr_manifest_entry *entry = NULL; - - while (manifest_ret > 0 && - directory_manifest_entry_path_compare( - &manifest_entry, name) < 0) - manifest_ret = attr_manifest_cursor_next( - &manifest_cursor, &manifest_entry); - if (manifest_ret < 0) - goto done; - if (manifest_ret > 0 && - !directory_manifest_entry_path_compare( - &manifest_entry, name)) - entry = &manifest_entry; - if (!directory_attribute_source_matches( - istate, path, name, entry, - first + i)) - goto done; } - - semantic_verify_path_free(path, &namespace_unstable, NULL); - path = NULL; - if (namespace_unstable || !semantic_verify_root_stable(root) || + if (!directory_attribute_sources_match_manifest( + istate, directory, first) || !clean_status_index_snapshot_still_matches_proof_epoch( &snapshot, istate) || !semantic_verify_proof_is_current( @@ -481,13 +544,8 @@ int clean_status_manifest_directory_unchanged( safe = 1; done: - if (path) - semantic_verify_path_free(path, NULL, NULL); - semantic_verify_root_clear(root); if (pinned) clean_status_index_snapshot_release(&snapshot); - string_list_clear(&candidates, 0); - strbuf_release(&candidate); return safe; #else (void)istate; diff --git a/clean-status-manifest.h b/clean-status-manifest.h index 8bc4f1ac28ad28..cc256334d912f0 100644 --- a/clean-status-manifest.h +++ b/clean-status-manifest.h @@ -38,6 +38,9 @@ int clean_status_manifest_end_directory_delta(struct index_state *istate); /* Recheck one path's attribute ancestry for suspended backoff history. */ int clean_status_manifest_path_attributes_unchanged( const struct index_state *istate, const char *path); +/* Recheck each distinct attribute source below an indexed directory. */ +int clean_status_manifest_directory_sources_unchanged( + const struct index_state *istate, const char *directory); int clean_status_manifest_directory_unchanged( struct index_state *istate, const char *directory); int clean_status_manifest_reconcile_deleted_attribute( diff --git a/clean-status-sidecar-issue.c b/clean-status-sidecar-issue.c index 91cd4bc9960d25..6f89c627fbbcc1 100644 --- a/clean-status-sidecar-issue.c +++ b/clean-status-sidecar-issue.c @@ -24,12 +24,11 @@ static void trace_miss(struct repository *repo, const char *reason) trace2_data_string("status", repo, "clean-proof/miss", reason); } -static int issue_test_barrier(void) +static int sidecar_test_barrier(const char *ready_name, + const char *resume_name) { - const char *ready = - getenv("GIT_TEST_STATUS_CLEAN_SIDECAR_ISSUE_BARRIER_READY"); - const char *resume = - getenv("GIT_TEST_STATUS_CLEAN_SIDECAR_ISSUE_BARRIER_RESUME"); + const char *ready = getenv(ready_name); + const char *resume = getenv(resume_name); struct strbuf buf = STRBUF_INIT; int ret; @@ -43,14 +42,30 @@ static int issue_test_barrier(void) return ret; } +static int issue_test_barrier(void) +{ + return sidecar_test_barrier( + "GIT_TEST_STATUS_CLEAN_SIDECAR_ISSUE_BARRIER_READY", + "GIT_TEST_STATUS_CLEAN_SIDECAR_ISSUE_BARRIER_RESUME"); +} + +int clean_status_sidecar_postwrite_test_barrier(void) +{ + return sidecar_test_barrier( + "GIT_TEST_STATUS_CLEAN_SIDECAR_POSTWRITE_BARRIER_READY", + "GIT_TEST_STATUS_CLEAN_SIDECAR_POSTWRITE_BARRIER_RESUME"); +} + static int output_is_certifiable(const struct wt_status *status, - int normal_clean_query) + int certifying_clean_query) { return (status->status_format == STATUS_FORMAT_PORCELAIN_V2 || - (normal_clean_query && - status->status_format == STATUS_FORMAT_NONE)) && + (certifying_clean_query && + (status->status_format == STATUS_FORMAT_NONE || + status->status_format == STATUS_FORMAT_SHORT))) && !status->pathspec.nr && !status->show_branch && - (!status->show_stash || normal_clean_query) && + (!status->show_stash || + status->status_format == STATUS_FORMAT_NONE) && !status->show_ignored_mode && !status->null_termination && !status->verbose && status->show_untracked_files == SHOW_NORMAL_UNTRACKED_FILES && @@ -215,7 +230,8 @@ int clean_status_issue_sidecar( struct wt_status *status, const struct clean_status_config_digest *config, struct lock_file *index_lock, - int normal_clean_query) + const struct clean_status_index_snapshot *scanned_index, + int certifying_clean_query) { struct repository *repo = status->repo; struct index_state *istate = repo->index; @@ -230,7 +246,7 @@ int clean_status_issue_sidecar( if (!is_lock_file_locked(index_lock) || !config->finalized || - !output_is_certifiable(status, normal_clean_query)) { + !output_is_certifiable(status, certifying_clean_query)) { trace_miss(repo, "issue-command-or-output"); goto done; } @@ -249,6 +265,12 @@ int clean_status_issue_sidecar( trace_miss(repo, "issue-test-barrier"); goto done; } + if (scanned_index && + !clean_status_index_snapshot_still_matches( + scanned_index, istate)) { + trace_miss(repo, "issue-index-raced"); + goto done; + } if (!status->attr_source_snapshot || clean_status_index_snapshot_pin(&index, istate) || clean_status_repository_fingerprint( diff --git a/clean-status-sidecar.c b/clean-status-sidecar.c index b40959bcd301ee..24d0618b18b2bb 100644 --- a/clean-status-sidecar.c +++ b/clean-status-sidecar.c @@ -8,6 +8,7 @@ #include "attr-fingerprint.h" #include "clean-status-index.h" #include "clean-status-sidecar.h" +#include "dir.h" #include "fsmonitor-clean-proof.h" #include "hash-framing.h" #include "lockfile.h" @@ -430,12 +431,32 @@ int clean_status_sidecar_install( return ret; } -static int current_worktree_is_main(struct repository *repo) +int clean_status_worktree_shape_supported(struct repository *repo) { - struct worktree *worktree = get_current_worktree(repo); - int ret = worktree && is_main_worktree(worktree); + struct worktree *current = get_current_worktree(repo); + struct worktree *registered = NULL; + int ret = 0; - free_worktree(worktree); + if (!current) + goto done; + if (is_main_worktree(current)) { + ret = 1; + goto done; + } + if (!current->id) + goto done; + /* + * A linked worktree has its own index and sidecar. Accept it only when + * both repository paths still name the worktree registered in the common + * directory; an ad-hoc GIT_DIR/GIT_WORK_TREE pairing must fall back. + */ + registered = get_linked_worktree(repo, current->id, 1); + ret = registered && registered->is_current && + !fspathcmp(current->path, registered->path); + +done: + free_worktree(registered); + free_worktree(current); return ret; } @@ -479,7 +500,7 @@ int clean_status_repository_fingerprint( !index || index->fd < 0 || !scanned_worktree || is_bare_repository(repo) || !repo_get_work_tree(repo) || - !current_worktree_is_main(repo) || + !clean_status_worktree_shape_supported(repo) || repo_has_replace_refs_uncached(repo)) goto done; diff --git a/clean-status-sidecar.h b/clean-status-sidecar.h index a496246c28ddff..fe3829d86cc345 100644 --- a/clean-status-sidecar.h +++ b/clean-status-sidecar.h @@ -70,6 +70,7 @@ int clean_status_sidecar_install( const char *index_path, const struct clean_status_sidecar *sidecar, const struct clean_status_index_snapshot *snapshot, const struct git_hash_algo *algo); +int clean_status_worktree_shape_supported(struct repository *repo); int clean_status_repository_fingerprint( struct repository *repo, const struct attr_source_snapshot *attrs, diff --git a/clean-status.c b/clean-status.c index 6c0dd8f1bebac8..edcd627d9d06de 100644 --- a/clean-status.c +++ b/clean-status.c @@ -456,23 +456,16 @@ static int path_has_no_new_attribute_sources( return safe; } -int clean_status_index_entry_is_semantically_safe( - const struct index_state *istate, - const struct cache_entry *old, +static int clean_status_index_entries_have_safe_shape( + const struct index_state *istate, const struct cache_entry *old, const struct cache_entry *new_entry) { const struct clean_status_state *state = istate->clean_status; const struct cache_entry *entry = old ? old : new_entry; struct conv_attrs attrs; const char *base; - int suspended = clean_status_fsmonitor_backoff_suspended(istate); - if (!state || - (!suspended && !clean_status_revalidated_token_matches(istate)) || - (!suspended && state->filter_configured && - !state->filter_scope_valid) || - istate->split_index || - istate->sparse_index || !entry) + if (!state || !entry) return 0; if ((old && (!S_ISREG(old->ce_mode) && !S_ISLNK(old->ce_mode))) || (new_entry && (!S_ISREG(new_entry->ce_mode) && @@ -490,16 +483,35 @@ int clean_status_index_entry_is_semantically_safe( if (!fspathcmp(base, ".gitattributes") || !fspathcmp(base, ".gitignore")) return 0; - if (suspended && - (!old || !new_entry || !S_ISREG(old->ce_mode) || - !S_ISREG(new_entry->ce_mode) || - !clean_status_manifest_path_attributes_unchanged(istate, entry->name))) - return 0; if (state->filter_configured) { convert_attrs((struct index_state *)istate, &attrs, entry->name); if (convert_attrs_has_clean_filter(&attrs)) return 0; } + return 1; +} + +int clean_status_index_entry_is_semantically_safe( + const struct index_state *istate, + const struct cache_entry *old, + const struct cache_entry *new_entry) +{ + const struct clean_status_state *state = istate->clean_status; + const struct cache_entry *entry = old ? old : new_entry; + int suspended = clean_status_fsmonitor_backoff_suspended(istate); + + if (!state || + (!suspended && !clean_status_revalidated_token_matches(istate)) || + (!suspended && state->filter_configured && + !state->filter_scope_valid) || + istate->split_index || istate->sparse_index || !entry || + !clean_status_index_entries_have_safe_shape(istate, old, new_entry)) + return 0; + if (suspended && + (!old || !new_entry || !S_ISREG(old->ce_mode) || + !S_ISREG(new_entry->ce_mode) || + !clean_status_manifest_path_attributes_unchanged(istate, entry->name))) + return 0; if (!old || !new_entry) return path_has_no_new_attribute_sources(istate, entry->name, old && !new_entry); @@ -508,6 +520,50 @@ int clean_status_index_entry_is_semantically_safe( old->ce_mode == new_entry->ce_mode; } +int clean_status_intent_to_add_change_is_semantically_safe( + const struct index_state *istate, + const struct cache_entry *old, + const struct cache_entry *new_entry) +{ + const struct clean_status_state *state = istate->clean_status; + const struct cache_entry *entry = old ? old : new_entry; + struct conv_attrs attrs; + const char *base; + + /* + * An intent-to-add entry is deliberately dirty, so adding or removing + * one does not invalidate the clean bits of unrelated tracked entries. + * Keep the paired untracked cache after invalidating this path, but do + * not retain a proof when the placeholder can affect policy semantics. + */ + if (!state || !!old == !!new_entry || !entry || + (old && !ce_intent_to_add(old)) || + (new_entry && !ce_intent_to_add(new_entry)) || + !S_ISREG(entry->ce_mode) || ce_stage(entry) || + ce_skip_worktree(entry) || (entry->ce_flags & CE_VALID) || + !clean_status_has_current_full_fsmonitor_proof(istate) || + !istate->fsmonitor_untracked_valid || + !istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_extension_invalid || + !istate->fsmonitor_last_update || + !istate->fsmonitor_untracked_token || + strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token)) + return 0; + base = strrchr(entry->name, '/'); + base = base ? base + 1 : entry->name; + if (!fspathcmp(base, ".gitattributes") || + !fspathcmp(base, ".gitignore")) + return 0; + if (state->filter_configured) { + convert_attrs((struct index_state *)istate, &attrs, entry->name); + if (convert_attrs_has_clean_filter(&attrs)) + return 0; + } + return path_has_no_new_attribute_sources( + istate, entry->name, old && !new_entry); +} + void clean_status_clear_authenticated_new_directories( struct index_state *istate) { @@ -547,7 +603,7 @@ static int clean_status_changed_directory_is_semantically_safe( const char *basename; unsigned int first, i, namespace_unstable = 0; size_t len; - int parent_fd, next, removed, safe = 0; + int bulk, parent_fd, next, removed, safe = 0; if (!state || !fstat_is_reliable() || !state->current_config_valid || !state->current_attr_valid || @@ -570,10 +626,8 @@ static int clean_status_changed_directory_is_semantically_safe( if (first >= istate->cache_nr || !starts_with(istate->cache[first]->name, name)) return 0; - /* Each descendant independently authenticates its attribute ancestry. */ - if (istate->cache_nr - first > 64 && - starts_with(istate->cache[first + 64]->name, name)) - return 0; + bulk = istate->cache_nr - first > 64 && + starts_with(istate->cache[first + 64]->name, name); if (attr_manifest_cursor_init(&cursor, state->manifest.current.buf, @@ -599,12 +653,30 @@ static int clean_status_changed_directory_is_semantically_safe( removed = 0; } - for (i = first; i < istate->cache_nr && - starts_with(istate->cache[i]->name, name); i++) - if (!clean_status_index_entry_is_semantically_safe( - istate, removed ? istate->cache[i] : NULL, - removed ? NULL : istate->cache[i])) + if (bulk) { + /* + * Avoid repeating anchored ancestry checks for every entry in a + * large cone. The manifest helper verifies each distinct attribute + * candidate once. Removed cones retain the conservative fallback. + */ + if (removed || + !clean_status_manifest_directory_sources_unchanged( + istate, name)) goto done; + for (i = first; i < istate->cache_nr && + starts_with(istate->cache[i]->name, name); i++) + if (!clean_status_index_entries_have_safe_shape( + istate, NULL, istate->cache[i])) + goto done; + } else { + for (i = first; i < istate->cache_nr && + starts_with(istate->cache[i]->name, name); i++) + if (!clean_status_index_entry_is_semantically_safe( + istate, + removed ? istate->cache[i] : NULL, + removed ? NULL : istate->cache[i])) + goto done; + } semantic_verify_path_free(path, &namespace_unstable, NULL); path = NULL; diff --git a/clean-status.h b/clean-status.h index 1d17885cbc6d19..2c462af91175c3 100644 --- a/clean-status.h +++ b/clean-status.h @@ -46,6 +46,10 @@ struct clean_status_proof_epoch *clean_status_capture_proof_epoch( struct index_state *istate, const struct attr_source_snapshot *attrs, int validate_filter_scope); +struct clean_status_proof_epoch *clean_status_capture_proof_epoch_at_path( + struct index_state *istate, + const struct attr_source_snapshot *attrs, + int validate_filter_scope, const char *index_path); int clean_status_proof_epoch_start_token_matches( struct index_state *istate, const struct clean_status_proof_epoch *epoch); @@ -112,7 +116,9 @@ int clean_status_issue_sidecar( struct wt_status *status, const struct clean_status_config_digest *config, struct lock_file *index_lock, - int normal_clean_query); + const struct clean_status_index_snapshot *scanned_index, + int certifying_clean_query); +int clean_status_sidecar_postwrite_test_barrier(void); int clean_status_try_sidecar( struct repository *repo, const struct clean_status_config_digest *config, @@ -127,6 +133,10 @@ int clean_status_index_entry_is_semantically_safe( const struct index_state *istate, const struct cache_entry *old, const struct cache_entry *new_entry); +int clean_status_intent_to_add_change_is_semantically_safe( + const struct index_state *istate, + const struct cache_entry *old, + const struct cache_entry *new_entry); void clean_status_set_authenticated_new_directories( struct index_state *istate, const struct index_state *old_index, const struct strbuf *paths); @@ -162,6 +172,9 @@ int clean_status_transfer_current_proof_if_same_index( struct index_state *dst, const struct index_state *src); int clean_status_transfer_current_proof_if_semantically_same_index( struct index_state *dst, const struct index_state *src); +int clean_status_transfer_current_proof_after_checkout( + struct index_state *dst, const struct index_state *src, + int *manifest_refresh_required); /* * A canonical main-index source may lend suspended historical state to an diff --git a/compat/fsmonitor/fsm-darwin-gcc.h b/compat/fsmonitor/fsm-darwin-gcc.h index 959bc88f8f765a..c9aa2a1902d263 100644 --- a/compat/fsmonitor/fsm-darwin-gcc.h +++ b/compat/fsmonitor/fsm-darwin-gcc.h @@ -97,6 +97,8 @@ CFRunLoopRef CFRunLoopGetCurrent(void); extern CFStringRef kCFRunLoopDefaultMode; void FSEventStreamSetDispatchQueue(FSEventStreamRef stream, dispatch_queue_t q); unsigned char FSEventStreamStart(FSEventStreamRef stream); +FSEventStreamEventId FSEventStreamFlushAsync(FSEventStreamRef stream); +void FSEventStreamFlushSync(FSEventStreamRef stream); void FSEventStreamStop(FSEventStreamRef stream); void FSEventStreamInvalidate(FSEventStreamRef stream); void FSEventStreamRelease(FSEventStreamRef stream); diff --git a/compat/fsmonitor/fsm-listen-darwin.c b/compat/fsmonitor/fsm-listen-darwin.c index f25d7cdd907af9..c291ef55ced05e 100644 --- a/compat/fsmonitor/fsm-listen-darwin.c +++ b/compat/fsmonitor/fsm-listen-darwin.c @@ -24,17 +24,25 @@ #endif #include "git-compat-util.h" +#include "abspath.h" #include "fsmonitor.h" #include "fsm-listen.h" #include "fsmonitor--daemon.h" #include "fsmonitor-path-utils.h" #include "gettext.h" +#include "parse.h" #include "simple-ipc.h" #include "string-list.h" #include "trace.h" +#include "trace2.h" +#include +#include + +#define FSMONITOR_FLUSH_TIMEOUT_MS 1000 struct fsm_listen_data { + struct fsmonitor_daemon_state *state; CFStringRef cfsr_worktree_path; CFStringRef cfsr_gitdir_path; CFStringRef cfsr_event_path_key; @@ -49,6 +57,37 @@ struct fsm_listen_data pthread_cond_t dq_finished; pthread_mutex_t dq_lock; + pthread_t flush_thread; + pthread_cond_t flush_requested_cond; + pthread_cond_t flush_finished_cond; + pthread_mutex_t flush_lock; + uint64_t flush_requested; + uint64_t flush_finished; + uint64_t flush_barrier_requested; + uint64_t flush_barrier_finished; + FSEventStreamEventId flush_published_event_id; + uint64_t flush_published_event_epoch; + int watch_root_kq; + int *watch_root_fds; + size_t watch_root_fds_nr; + size_t watch_root_fds_alloc; + struct stat worktree_watch_identity; + struct stat gitdir_watch_identity; + enum flush_worker_state { + FLUSH_WORKER_NOT_STARTED = 0, + FLUSH_WORKER_RUNNING, + FLUSH_WORKER_STOPPING, + FLUSH_WORKER_FAILED, + FLUSH_WORKER_STOPPED, + } flush_state; + unsigned long flush_timeout_ms; + unsigned long test_flush_delay_ms; + unsigned long test_flush_coalesce_delay_ms; + char *test_defer_path; + struct fsmonitor_batch *test_deferred_batch; + uint64_t test_deferred_generation; + unsigned long test_defer_delay_ms; + enum shutdown_style { SHUTDOWN_EVENT = 0, FORCE_SHUTDOWN, @@ -57,8 +96,455 @@ struct fsm_listen_data unsigned int stream_scheduled:1; unsigned int stream_started:1; + unsigned int dq_sync_initialized:1; + unsigned int shutdown_requested:1; + unsigned int flush_sync_initialized:1; + unsigned int flush_thread_created:1; + unsigned int use_async_flush:1; + unsigned int test_deferred_published:1; + unsigned int test_flush_bypass:1; + unsigned int test_cookie_delayed:1; + unsigned int test_ignore_root_changed:1; + unsigned long test_cookie_delay_ms; }; +static void release_watch_root_edges(struct fsm_listen_data *data) +{ + size_t i; + + for (i = 0; i < data->watch_root_fds_nr; i++) + close(data->watch_root_fds[i]); + FREE_AND_NULL(data->watch_root_fds); + data->watch_root_fds_nr = 0; + data->watch_root_fds_alloc = 0; + if (data->watch_root_kq >= 0) { + close(data->watch_root_kq); + data->watch_root_kq = -1; + } +} + +static int watch_root_fd_already_registered(struct fsm_listen_data *data, + int fd) +{ + struct stat candidate; + size_t i; + + if (fstat(fd, &candidate)) + return -1; + for (i = 0; i < data->watch_root_fds_nr; i++) { + struct stat registered; + + if (fstat(data->watch_root_fds[i], ®istered)) + return -1; + if (candidate.st_dev == registered.st_dev && + candidate.st_ino == registered.st_ino) + return 1; + } + return 0; +} + +static int watch_root_fd_supports_edges(int fd) +{ + struct statfs fs; + + if (fstatfs(fd, &fs)) + return 0; + return (fs.f_flags & MNT_LOCAL) && + (!strcmp(fs.f_fstypename, "apfs") || + !strcmp(fs.f_fstypename, "hfs")); +} + +static int add_watch_root_edges(struct fsm_listen_data *data, + const char *path) +{ + struct strbuf canonical = STRBUF_INIT; + struct kevent change; + const char *slash; + int duplicate; + int fd = -1; + int ret = -1; + + if (!strbuf_realpath(&canonical, path, 0)) + goto done; + + for (;;) { + fd = open(canonical.buf, O_EVTONLY | O_CLOEXEC); + if (fd < 0) + goto done; + if (!watch_root_fd_supports_edges(fd)) { + errno = ENOTSUP; + goto done; + } + duplicate = watch_root_fd_already_registered(data, fd); + if (duplicate < 0) + goto done; + if (duplicate) { + close(fd); + fd = -1; + } else { + EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, + NOTE_RENAME | NOTE_DELETE | NOTE_REVOKE, 0, NULL); + if (kevent(data->watch_root_kq, &change, 1, + NULL, 0, NULL) < 0) + goto done; + ALLOC_GROW(data->watch_root_fds, + data->watch_root_fds_nr + 1, + data->watch_root_fds_alloc); + data->watch_root_fds[data->watch_root_fds_nr++] = fd; + fd = -1; + } + + /* The vnode watch on the first component also covers its edge. */ + slash = find_last_dir_sep(canonical.buf); + if (!slash || slash == canonical.buf) + break; + strbuf_setlen(&canonical, slash - canonical.buf); + } + + ret = 0; +done: + if (fd >= 0) + close(fd); + strbuf_release(&canonical); + return ret; +} + +static int init_watch_root_edges(struct fsm_listen_data *data) +{ + data->watch_root_kq = kqueue(); + if (data->watch_root_kq < 0 || + fcntl(data->watch_root_kq, F_SETFD, FD_CLOEXEC) < 0 || + add_watch_root_edges(data, + data->state->path_worktree_watch.buf) || + (data->state->nr_paths_watching > 1 && + add_watch_root_edges(data, + data->state->path_gitdir_watch.buf))) { + release_watch_root_edges(data); + return -1; + } + return 0; +} + +static int watch_root_edge_seen(struct fsm_listen_data *data) +{ + struct kevent event; + struct timespec timeout = { 0 }; + int nr; + + nr = kevent(data->watch_root_kq, NULL, 0, &event, 1, &timeout); + if (!nr) + return 0; + + if (nr < 0) + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence watch-root poll failed: %s", + strerror(errno)); + else + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence observed watch-root edge " + "flags=0x%x fflags=0x%x", + event.flags, event.fflags); + return 1; +} + +static int watch_root_matches(const char *path, const struct stat *expected) +{ + struct stat actual; + + return !stat(path, &actual) && S_ISDIR(actual.st_mode) && + actual.st_dev == expected->st_dev && + actual.st_ino == expected->st_ino && + actual.st_birthtimespec.tv_sec == + expected->st_birthtimespec.tv_sec && + actual.st_birthtimespec.tv_nsec == + expected->st_birthtimespec.tv_nsec && + actual.st_gen == expected->st_gen; +} + +static int watch_roots_match(struct fsm_listen_data *data) +{ + return watch_root_matches(data->state->path_worktree_watch.buf, + &data->worktree_watch_identity) && + (data->state->nr_paths_watching == 1 || + watch_root_matches(data->state->path_gitdir_watch.buf, + &data->gitdir_watch_identity)); +} + +static void force_provider_shutdown(struct fsm_listen_data *data) +{ + pthread_mutex_lock(&data->dq_lock); + data->shutdown_style = FORCE_SHUTDOWN; + data->shutdown_requested = 1; + pthread_cond_broadcast(&data->dq_finished); + pthread_mutex_unlock(&data->dq_lock); +} + +static void publish_test_deferred_batch(struct fsm_listen_data *data) +{ + struct fsmonitor_batch *batch; + uint64_t generation; + + if (data->test_defer_delay_ms) + sleep_millisec(data->test_defer_delay_ms); + + pthread_mutex_lock(&data->flush_lock); + batch = data->test_deferred_batch; + generation = data->test_deferred_generation; + data->test_deferred_batch = NULL; + data->test_deferred_generation = 0; + if (batch) + data->test_deferred_published = 1; + pthread_mutex_unlock(&data->flush_lock); + + if (!batch) + return; + + trace_printf_key(&trace_fsmonitor, + "test-publish-deferred-path-at-provider-fence"); + if (!fsmonitor_publish_if_current_generation(data->state, batch, + generation)) + trace_printf_key(&trace_fsmonitor, + "test-discard-deferred-path-after-token-reset"); +} + +static void discard_test_deferred_batch(struct fsm_listen_data *data) +{ + struct fsmonitor_batch *batch; + + pthread_mutex_lock(&data->flush_lock); + batch = data->test_deferred_batch; + data->test_deferred_batch = NULL; + data->test_deferred_generation = 0; + pthread_mutex_unlock(&data->flush_lock); + + fsmonitor_batch__free_list(batch); +} + +static void drain_dispatch_queue(void *ctx UNUSED) +{ +} + +static void complete_provider_barrier(void *ctx) +{ + struct fsm_listen_data *data = ctx; + + pthread_mutex_lock(&data->flush_lock); + data->flush_barrier_finished++; + pthread_cond_broadcast(&data->flush_finished_cond); + pthread_mutex_unlock(&data->flush_lock); +} + +static void *flush_worker_proc(void *ctx) +{ + struct fsm_listen_data *data = ctx; + + trace2_thread_start("fsm-flush"); + pthread_mutex_lock(&data->flush_lock); + for (;;) { + FSEventStreamEventId target_event_id; + FSEventStreamEventId published_event_id; + uint64_t target_event_epoch; + uint64_t target_barrier; + uint64_t requested; + uint64_t previously_finished; + int event_pending; + + while (data->flush_requested == data->flush_finished && + data->flush_state == FLUSH_WORKER_RUNNING) + pthread_cond_wait(&data->flush_requested_cond, + &data->flush_lock); + if (data->flush_state != FLUSH_WORKER_RUNNING) + break; + if (data->test_flush_coalesce_delay_ms) { + pthread_mutex_unlock(&data->flush_lock); + sleep_millisec(data->test_flush_coalesce_delay_ms); + pthread_mutex_lock(&data->flush_lock); + if (data->flush_state != FLUSH_WORKER_RUNNING) + break; + } + + requested = data->flush_requested; + previously_finished = data->flush_finished; + pthread_mutex_unlock(&data->flush_lock); + + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence begin request=%"PRIu64 + " coalesced=%"PRIu64, + requested, requested - previously_finished); + trace2_data_intmax("fsmonitor", NULL, + "darwin-fence/coalesced", + requested - previously_finished); + trace2_data_intmax("fsmonitor", NULL, + "darwin-fence/count", 1); + if (data->test_flush_delay_ms) + sleep_millisec(data->test_flush_delay_ms); + if (!data->use_async_flush) { + trace2_region_enter("fsmonitor", "darwin-flush-sync", + NULL); + FSEventStreamFlushSync(data->stream); + dispatch_sync_f(data->dq, NULL, drain_dispatch_queue); + trace2_region_leave("fsmonitor", "darwin-flush-sync", + NULL); + } else { + trace2_region_enter("fsmonitor", "darwin-flush-async", + NULL); + pthread_mutex_lock(&data->flush_lock); + target_event_epoch = data->flush_published_event_epoch; + published_event_id = data->flush_published_event_id; + pthread_mutex_unlock(&data->flush_lock); + target_event_id = FSEventStreamFlushAsync(data->stream); + /* A lower target belongs to the next event-ID epoch. */ + if (target_event_id && target_event_id < published_event_id) + target_event_epoch++; + pthread_mutex_lock(&data->flush_lock); + target_barrier = ++data->flush_barrier_requested; + /* + * The FSEvents Programming Guide defines FlushAsync's return + * value as the last pending event and tells callers to use that + * ID in the callback to detect completion. Wait until our + * callback has published through it. The serial queue barrier + * also covers a callback which advanced the stream ID before + * FlushAsync returned but is still publishing its batch. + */ + event_pending = + target_event_epoch > data->flush_published_event_epoch || + (target_event_epoch == + data->flush_published_event_epoch && + target_event_id > data->flush_published_event_id); + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence target=%"PRIu64 + " published=%"PRIu64" event-pending=%d", + target_event_id, + data->flush_published_event_id, + event_pending); + trace2_data_intmax("fsmonitor", NULL, + "darwin-fence/event-pending", + event_pending); + pthread_mutex_unlock(&data->flush_lock); + dispatch_async_f(data->dq, data, + complete_provider_barrier); + pthread_mutex_lock(&data->flush_lock); + while (data->flush_state == FLUSH_WORKER_RUNNING && + (data->flush_barrier_finished < target_barrier || + data->flush_published_event_epoch < + target_event_epoch || + (data->flush_published_event_epoch == + target_event_epoch && + data->flush_published_event_id < target_event_id))) + pthread_cond_wait(&data->flush_finished_cond, + &data->flush_lock); + if (data->flush_state != FLUSH_WORKER_RUNNING) { + pthread_mutex_unlock(&data->flush_lock); + trace2_region_leave("fsmonitor", + "darwin-flush-async", NULL); + break; + } + pthread_mutex_unlock(&data->flush_lock); + trace2_region_leave("fsmonitor", "darwin-flush-async", + NULL); + /* + * WatchRoot notifications carry event ID zero, so they cannot + * be represented by FlushAsync's monotonic completion token. + * Reject the fence if a watched path crossed a namespace edge + * or now names a different directory. A root change after this + * check is a post-fence mutation. + */ + if (watch_root_edge_seen(data) || + !watch_roots_match(data)) { + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence rejected " + "changed watch root"); + pthread_mutex_lock(&data->flush_lock); + if (data->flush_state == FLUSH_WORKER_RUNNING) + data->flush_state = FLUSH_WORKER_STOPPING; + pthread_cond_broadcast( + &data->flush_finished_cond); + pthread_mutex_unlock(&data->flush_lock); + force_provider_shutdown(data); + break; + } + } + publish_test_deferred_batch(data); + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence complete request=%"PRIu64, + requested); + + pthread_mutex_lock(&data->flush_lock); + if (data->flush_finished < requested) + data->flush_finished = requested; + pthread_cond_broadcast(&data->flush_finished_cond); + } + if (data->flush_state == FLUSH_WORKER_STOPPING) + data->flush_state = FLUSH_WORKER_STOPPED; + pthread_cond_broadcast(&data->flush_finished_cond); + pthread_mutex_unlock(&data->flush_lock); + trace2_thread_exit(); + return NULL; +} + +static int start_flush_worker(struct fsm_listen_data *data) +{ + pthread_mutex_lock(&data->flush_lock); + if (data->flush_state != FLUSH_WORKER_NOT_STARTED) + BUG("unexpected Darwin provider fence worker state"); + data->flush_state = FLUSH_WORKER_RUNNING; + pthread_mutex_unlock(&data->flush_lock); + + if (pthread_create(&data->flush_thread, NULL, + flush_worker_proc, data)) { + pthread_mutex_lock(&data->flush_lock); + data->flush_state = FLUSH_WORKER_STOPPED; + pthread_mutex_unlock(&data->flush_lock); + return -1; + } + data->flush_thread_created = 1; + return 0; +} + +static int begin_flush_shutdown(struct fsm_listen_data *data) +{ + int in_flight; + + if (!data || !data->flush_sync_initialized) + return 0; + + pthread_mutex_lock(&data->flush_lock); + in_flight = data->flush_state == FLUSH_WORKER_FAILED; + if (data->flush_state == FLUSH_WORKER_RUNNING) { + in_flight = data->flush_finished < data->flush_requested; + data->flush_state = in_flight ? FLUSH_WORKER_FAILED : + FLUSH_WORKER_STOPPING; + } + pthread_cond_broadcast(&data->flush_requested_cond); + pthread_cond_broadcast(&data->flush_finished_cond); + pthread_mutex_unlock(&data->flush_lock); + return in_flight; +} + +static void stop_flush_worker(struct fsm_listen_data *data) +{ + if (!data->flush_thread_created) + return; + + /* + * Preserve the failed state until the main thread takes the fail-stop + * path. That path deliberately bypasses normal IPC cleanup so an old + * daemon cannot unlink a replacement daemon's socket. + */ + if (begin_flush_shutdown(data)) { + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence abandoned during shutdown"); + return; + } + + pthread_join(data->flush_thread, NULL); + data->flush_thread_created = 0; + pthread_mutex_lock(&data->flush_lock); + if (data->flush_state != FLUSH_WORKER_STOPPED) + BUG("Darwin provider fence worker did not stop cleanly"); + pthread_mutex_unlock(&data->flush_lock); +} + static void log_flags_set(const char *path, const FSEventStreamEventFlags flag) { struct strbuf msg = STRBUF_INIT; @@ -219,7 +705,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, size_t num_of_events, void *event_paths, const FSEventStreamEventFlags event_flags[], - const FSEventStreamEventId event_ids[] UNUSED) + const FSEventStreamEventId event_ids[]) { struct fsmonitor_daemon_state *state = ctx; struct fsm_listen_data *data = state->listen_data; @@ -232,6 +718,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, struct strbuf tmp = STRBUF_INIT; struct strbuf event_path = STRBUF_INIT; enum fsmonitor_path_type path_type; + int event_ids_wrapped = 0; /* * Build a list of all filesystem changes into a private/local @@ -248,6 +735,9 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, CFIndex path_size; int64_t file_id = 0; + if (event_flags[k] & kFSEventStreamEventFlagEventIdsWrapped) + event_ids_wrapped = 1; + /* * Extended events retain their inode even when their pathname has * already been removed by the time this callback runs. @@ -298,6 +788,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, fsmonitor_force_resync(state); fsmonitor_batch__free_list(batch); + discard_test_deferred_batch(data); string_list_clear(&cookie_list, 0); batch = NULL; @@ -328,6 +819,11 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, */ trace_printf_key(&trace_fsmonitor, "event: root changed"); + if (data->test_ignore_root_changed) { + trace_printf_key(&trace_fsmonitor, + "test-ignore-root-change"); + continue; + } goto force_shutdown; } @@ -430,9 +926,45 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, for (const char *relative = tmp.buf; relative < tmp.buf + tmp.len; relative += strlen(relative) + 1) { - if (!batch) - batch = fsmonitor_batch__new(); - my_add_path(batch, relative); + int defer_for_test = 0; + + if (data->test_defer_path && + !strcmp(relative, data->test_defer_path)) { + uint64_t generation; + + pthread_mutex_lock(&state->main_lock); + generation = state->token_generation; + pthread_mutex_unlock(&state->main_lock); + pthread_mutex_lock(&data->flush_lock); + if (!data->test_deferred_published) { + if (data->test_deferred_batch && + data->test_deferred_generation != + generation) { + fsmonitor_batch__free_list( + data->test_deferred_batch); + data->test_deferred_batch = NULL; + } + if (!data->test_deferred_batch) { + data->test_deferred_batch = + fsmonitor_batch__new(); + data->test_deferred_generation = + generation; + } + my_add_path(data->test_deferred_batch, + relative); + defer_for_test = 1; + } + pthread_mutex_unlock(&data->flush_lock); + } + if (defer_for_test) { + trace_printf_key(&trace_fsmonitor, + "test-defer-path: '%s'", + relative); + } else { + if (!batch) + batch = fsmonitor_batch__new(); + my_add_path(batch, relative); + } } break; @@ -448,12 +980,29 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, invalid_event: fsmonitor_force_resync(state); fsmonitor_batch__free_list(batch); + discard_test_deferred_batch(data); string_list_clear(&cookie_list, 0); batch = NULL; } free(resolved); + if (cookie_list.nr && data->test_cookie_delay_ms && + !data->test_cookie_delayed) { + data->test_cookie_delayed = 1; + sleep_millisec(data->test_cookie_delay_ms); + } fsmonitor_publish(state, batch, &cookie_list); + pthread_mutex_lock(&data->flush_lock); + if (num_of_events && event_ids_wrapped) { + data->flush_published_event_epoch++; + data->flush_published_event_id = event_ids[num_of_events - 1]; + } else if (num_of_events && + data->flush_published_event_id < + event_ids[num_of_events - 1]) { + data->flush_published_event_id = event_ids[num_of_events - 1]; + } + pthread_cond_broadcast(&data->flush_finished_cond); + pthread_mutex_unlock(&data->flush_lock); string_list_clear(&cookie_list, 0); strbuf_release(&tmp); strbuf_release(&event_path); @@ -462,14 +1011,12 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, force_shutdown: free(resolved); fsmonitor_batch__free_list(batch); + discard_test_deferred_batch(data); string_list_clear(&cookie_list, 0); strbuf_release(&tmp); strbuf_release(&event_path); - pthread_mutex_lock(&data->dq_lock); - data->shutdown_style = FORCE_SHUTDOWN; - pthread_cond_broadcast(&data->dq_finished); - pthread_mutex_unlock(&data->dq_lock); + force_provider_shutdown(data); strbuf_release(&tmp); return; @@ -511,6 +1058,25 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) CALLOC_ARRAY(data, 1); state->listen_data = data; + data->state = state; + data->watch_root_kq = -1; + data->test_cookie_delay_ms = git_env_ulong( + "GIT_TEST_FSMONITOR_COOKIE_DELAY_MS", 0); + data->test_flush_delay_ms = git_env_ulong( + "GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS", 0); + data->test_flush_coalesce_delay_ms = git_env_ulong( + "GIT_TEST_FSMONITOR_FLUSH_COALESCE_DELAY_MS", 0); + data->test_flush_bypass = git_env_bool( + "GIT_TEST_FSMONITOR_FLUSH_SYNC_BYPASS", 0); + data->test_ignore_root_changed = git_env_bool( + "GIT_TEST_FSMONITOR_IGNORE_ROOT_CHANGED", 0); + data->flush_timeout_ms = git_env_ulong( + "GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS", + FSMONITOR_FLUSH_TIMEOUT_MS); + data->test_defer_path = xstrdup_or_null(getenv( + "GIT_TEST_FSMONITOR_DEFER_PATH")); + data->test_defer_delay_ms = git_env_ulong( + "GIT_TEST_FSMONITOR_DEFER_PATH_DELAY_MS", 0); data->cfsr_event_path_key = CFStringCreateWithCString( NULL, "path", kCFStringEncodingUTF8); @@ -529,6 +1095,20 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) kCFStringEncodingUTF8); dir_array[data->nr_paths_watching++] = data->cfsr_gitdir_path; } + data->use_async_flush = !git_env_bool( + "GIT_TEST_FSMONITOR_FORCE_SYNC_FLUSH", 0); + if (data->use_async_flush && + (stat(state->path_worktree_watch.buf, + &data->worktree_watch_identity) || + (state->nr_paths_watching > 1 && + stat(state->path_gitdir_watch.buf, + &data->gitdir_watch_identity)) || + init_watch_root_edges(data))) { + trace_printf_key(&trace_fsmonitor, + "falling back to synchronous Darwin provider fence"); + data->use_async_flush = 0; + release_watch_root_edges(data); + } data->cfar_paths_to_watch = CFArrayCreate(NULL, dir_array, data->nr_paths_watching, @@ -540,11 +1120,21 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) if (!data->stream) goto failed; + pthread_mutex_init(&data->flush_lock, NULL); + pthread_cond_init(&data->flush_requested_cond, NULL); + pthread_cond_init(&data->flush_finished_cond, NULL); + data->flush_sync_initialized = 1; + pthread_mutex_init(&data->dq_lock, NULL); + pthread_cond_init(&data->dq_finished, NULL); + data->dq_sync_initialized = 1; + return 0; failed: error(_("Unable to create FSEventStream.")); + free(data->test_defer_path); + release_watch_root_edges(data); FREE_AND_NULL(state->listen_data); return -1; } @@ -558,18 +1148,38 @@ void fsm_listen__dtor(struct fsmonitor_daemon_state *state) data = state->listen_data; + if (data->flush_thread_created) + BUG("releasing FSEventStream with provider fence worker alive"); if (data->stream) { if (data->stream_started) FSEventStreamStop(data->stream); if (data->stream_scheduled) FSEventStreamInvalidate(data->stream); + /* + * Invalidation prevents new callbacks from being submitted. A + * synchronous no-op on our serial queue then waits for every + * callback that was already submitted to finish before state owned + * by the daemon is released. + */ + if (data->dq) + dispatch_sync_f(data->dq, NULL, drain_dispatch_queue); FSEventStreamRelease(data->stream); } if (data->dq) dispatch_release(data->dq); - pthread_cond_destroy(&data->dq_finished); - pthread_mutex_destroy(&data->dq_lock); + release_watch_root_edges(data); + fsmonitor_batch__free_list(data->test_deferred_batch); + free(data->test_defer_path); + if (data->flush_sync_initialized) { + pthread_cond_destroy(&data->flush_finished_cond); + pthread_cond_destroy(&data->flush_requested_cond); + pthread_mutex_destroy(&data->flush_lock); + } + if (data->dq_sync_initialized) { + pthread_cond_destroy(&data->dq_finished); + pthread_mutex_destroy(&data->dq_lock); + } FREE_AND_NULL(state->listen_data); } @@ -582,8 +1192,99 @@ void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) pthread_mutex_lock(&data->dq_lock); data->shutdown_style = SHUTDOWN_EVENT; + data->shutdown_requested = 1; pthread_cond_broadcast(&data->dq_finished); pthread_mutex_unlock(&data->dq_lock); + begin_flush_shutdown(data); +} + +enum fsm_listen_flush_result fsm_listen__flush_sync( + struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + struct timeval now; + struct timespec deadline; + uint64_t requested; + int err = 0; + enum fsm_listen_flush_result result = FSM_LISTEN_FLUSH_OK; + + if (!state || !(data = state->listen_data) || + !data->flush_sync_initialized) + return FSM_LISTEN_FLUSH_SHUTDOWN; + + if (data->test_flush_bypass) { + trace_printf_key(&trace_fsmonitor, + "test-bypass-darwin-flush-sync"); + return FSM_LISTEN_FLUSH_OK; + } + + pthread_mutex_lock(&data->flush_lock); + if (data->flush_state == FLUSH_WORKER_FAILED) { + result = FSM_LISTEN_FLUSH_TIMEOUT; + goto done; + } + if (data->flush_state != FLUSH_WORKER_RUNNING) { + result = FSM_LISTEN_FLUSH_SHUTDOWN; + goto done; + } + + requested = ++data->flush_requested; + trace_printf_key(&trace_fsmonitor, + "Darwin provider fence requested request=%"PRIu64, + requested); + trace2_data_intmax("fsmonitor", NULL, + "darwin-fence/request", requested); + pthread_cond_signal(&data->flush_requested_cond); + + gettimeofday(&now, NULL); + deadline.tv_sec = now.tv_sec + data->flush_timeout_ms / 1000; + deadline.tv_nsec = now.tv_usec * 1000 + + (data->flush_timeout_ms % 1000) * 1000000; + if (deadline.tv_nsec >= 1000000000) { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000; + } + + trace2_region_enter("fsmonitor", "darwin-fence-wait", NULL); + while (data->flush_finished < requested && + data->flush_state == FLUSH_WORKER_RUNNING && !err) + err = pthread_cond_timedwait(&data->flush_finished_cond, + &data->flush_lock, &deadline); + trace2_region_leave("fsmonitor", "darwin-fence-wait", NULL); + + if (data->flush_state == FLUSH_WORKER_FAILED) { + result = FSM_LISTEN_FLUSH_TIMEOUT; + } else if (data->flush_state != FLUSH_WORKER_RUNNING) { + result = FSM_LISTEN_FLUSH_SHUTDOWN; + } else if (data->flush_finished >= requested) { + result = FSM_LISTEN_FLUSH_OK; + } else if (err) { + data->flush_state = FLUSH_WORKER_FAILED; + pthread_cond_broadcast(&data->flush_requested_cond); + pthread_cond_broadcast(&data->flush_finished_cond); + trace2_data_intmax("fsmonitor", NULL, + "darwin-fence/timeout", 1); + result = FSM_LISTEN_FLUSH_TIMEOUT; + } + +done: + pthread_mutex_unlock(&data->flush_lock); + return result; +} + +int fsm_listen__flush_failed(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + int timed_out; + + if (!state || !(data = state->listen_data) || + !data->flush_sync_initialized) + return 0; + + pthread_mutex_lock(&data->flush_lock); + timed_out = data->flush_state == FLUSH_WORKER_FAILED; + pthread_mutex_unlock(&data->flush_lock); + return timed_out; } void fsm_listen__loop(struct fsmonitor_daemon_state *state) @@ -592,18 +1293,28 @@ void fsm_listen__loop(struct fsmonitor_daemon_state *state) data = state->listen_data; - pthread_mutex_init(&data->dq_lock, NULL); - pthread_cond_init(&data->dq_finished, NULL); data->dq = dispatch_queue_create("FSMonitor", NULL); FSEventStreamSetDispatchQueue(data->stream, data->dq); data->stream_scheduled = 1; + pthread_mutex_lock(&data->dq_lock); + if (data->shutdown_requested) { + pthread_mutex_unlock(&data->dq_lock); + return; + } + if (!FSEventStreamStart(data->stream)) { + pthread_mutex_unlock(&data->dq_lock); error(_("Failed to start the FSEventStream")); goto force_error_stop_without_loop; } data->stream_started = 1; + if (start_flush_worker(data)) { + pthread_mutex_unlock(&data->dq_lock); + error(_("Failed to start the FSEvents flush worker")); + goto force_error_stop_without_loop; + } /* * Our fs event listener is now running, so it's safe to start @@ -611,10 +1322,12 @@ void fsm_listen__loop(struct fsmonitor_daemon_state *state) */ ipc_server_start_async(state->ipc_server_data); - pthread_mutex_lock(&data->dq_lock); - pthread_cond_wait(&data->dq_finished, &data->dq_lock); + while (!data->shutdown_requested) + pthread_cond_wait(&data->dq_finished, &data->dq_lock); pthread_mutex_unlock(&data->dq_lock); + stop_flush_worker(data); + switch (data->shutdown_style) { case FORCE_ERROR_STOP: state->listen_error_code = -1; diff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c index 6181dcba51472d..681bcebc04cf28 100644 --- a/compat/fsmonitor/fsm-listen-linux.c +++ b/compat/fsmonitor/fsm-listen-linux.c @@ -1,5 +1,6 @@ #include "git-compat-util.h" #include "dir.h" +#include "fsmonitor.h" #include "fsmonitor-ipc.h" #include "fsmonitor-ll.h" #include "fsm-listen.h" @@ -490,7 +491,6 @@ static int process_event(const char *path, struct string_list *cookie_list, struct fsmonitor_daemon_state *state) { - const char *rel; const char *last_sep; switch (fsmonitor_classify_path_absolute(state, path)) { @@ -529,11 +529,22 @@ static int process_event(const char *path, if (trace_pass_fl(&trace_fsmonitor)) log_mask_set(path, event->mask); - if (!*batch) - *batch = fsmonitor_batch__new(); - - rel = path + state->path_worktree_watch.len + 1; - fsmonitor_batch__add_path(*batch, rel); + { + struct strbuf paths = STRBUF_INIT; + + fsmonitor_format_worktree_paths( + &paths, path, state->path_worktree_watch.len, + !(event->mask & IN_ISDIR), + !!(event->mask & IN_ISDIR)); + for (const char *relative = paths.buf; + relative < paths.buf + paths.len; + relative += strlen(relative) + 1) { + if (!*batch) + *batch = fsmonitor_batch__new(); + fsmonitor_batch__add_path(*batch, relative); + } + strbuf_release(&paths); + } if (em_dir_deleted(event->mask)) break; @@ -640,7 +651,23 @@ static void handle_events(struct fsmonitor_daemon_state *state) } strbuf_reset(&path); - strbuf_addf(&path, "%s/%s", w->dir, event->name); + strbuf_addstr(&path, w->dir); + if (event->len) { + size_t name_len = + strnlen(event->name, event->len); + + if (name_len == event->len) { + error(_("unterminated inotify event name")); + state->listen_data->shutdown = + SHUTDOWN_ERROR; + goto done; + } + if (name_len) { + strbuf_addch(&path, '/'); + strbuf_add(&path, event->name, + name_len); + } + } p = fsmonitor__resolve_alias(path.buf, &state->alias); if (!p) diff --git a/compat/fsmonitor/fsm-listen.h b/compat/fsmonitor/fsm-listen.h index 41650bf8972217..b2ce76b515f274 100644 --- a/compat/fsmonitor/fsm-listen.h +++ b/compat/fsmonitor/fsm-listen.h @@ -38,6 +38,24 @@ void fsm_listen__dtor(struct fsmonitor_daemon_state *state); */ void fsm_listen__loop(struct fsmonitor_daemon_state *state); +#ifdef __APPLE__ +enum fsm_listen_flush_result { + FSM_LISTEN_FLUSH_OK = 0, + FSM_LISTEN_FLUSH_SHUTDOWN, + FSM_LISTEN_FLUSH_TIMEOUT, +}; + +/* + * Request and wait for delivery of all FSEvents that occurred before + * this call. The caller must not hold fsmonitor_daemon_state::main_lock. + */ +enum fsm_listen_flush_result fsm_listen__flush_sync( + struct fsmonitor_daemon_state *state); + +/* True when provider-fence failure requires fail-stop IPC teardown. */ +int fsm_listen__flush_failed(struct fsmonitor_daemon_state *state); +#endif + /* * Gently request that the fsmonitor listener thread shutdown. * It does not wait for it to stop. The caller should do a JOIN diff --git a/compat/simple-ipc/ipc-unix-socket.c b/compat/simple-ipc/ipc-unix-socket.c index d27747bc1d0b63..7c2ec2cb638d14 100644 --- a/compat/simple-ipc/ipc-unix-socket.c +++ b/compat/simple-ipc/ipc-unix-socket.c @@ -100,6 +100,18 @@ static enum ipc_active_state connect_to_server( int fd = unix_stream_connect(path, options->uds_disallow_chdir); if (fd != -1) { +#ifdef SO_NOSIGPIPE + int value = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, + &value, sizeof(value)) < 0) { + int saved_errno = errno; + + close(fd); + errno = saved_errno; + return IPC_STATE__OTHER_ERROR; + } +#endif *pfd = fd; return IPC_STATE__LISTENING; } @@ -189,27 +201,99 @@ void ipc_client_close_connection(struct ipc_client_connection *connection) free(connection); } +/* + * A server can close a connection after accept() but before the client has + * finished writing its request. Keep SIGPIPE local to this write sequence so + * that the caller can recover from the resulting EPIPE. + */ +static int ipc_client_write_command(struct ipc_client_connection *connection, + const char *message, size_t message_len, + int gentle) +{ + unsigned write_options = gentle ? + PACKET_WRITE_SILENT_ON_WRITE_ERROR : 0; +#ifndef SO_NOSIGPIPE + sigset_t old_set, pending, sigpipe; + int mask_error, saved_errno, was_pending; +#endif + int ret = -1; + +#ifndef SO_NOSIGPIPE + sigemptyset(&sigpipe); + sigaddset(&sigpipe, SIGPIPE); + mask_error = pthread_sigmask(SIG_BLOCK, &sigpipe, &old_set); + if (mask_error) { + errno = mask_error; + return -1; + } + + if (sigpending(&pending) < 0) { + saved_errno = errno; + goto restore; + } + was_pending = sigismember(&pending, SIGPIPE) == 1; +#endif + + ret = write_packetized_from_buf_no_flush_with_options( + message, message_len, connection->fd, write_options); + if (!ret) + ret = packet_flush_gently_with_options( + connection->fd, write_options); +#ifndef SO_NOSIGPIPE + saved_errno = errno; + + /* + * Consume only a SIGPIPE raised by this write sequence. Otherwise, + * restoring an unblocked caller mask would deliver the pending signal + * after we had already translated the failure into EPIPE. + */ + if (ret < 0 && saved_errno == EPIPE && !was_pending && + !sigpending(&pending) && + sigismember(&pending, SIGPIPE) == 1) { + int received; + + mask_error = sigwait(&sigpipe, &received); + if (mask_error) { + saved_errno = mask_error; + ret = -1; + } + } + +restore: + mask_error = pthread_sigmask(SIG_SETMASK, &old_set, NULL); + if (mask_error) { + saved_errno = mask_error; + ret = -1; + } + errno = saved_errno; +#endif + return ret; +} + static int ipc_client_send_command_to_connection_1( struct ipc_client_connection *connection, const char *message, size_t message_len, struct strbuf *answer, int gentle) { + int read_options = PACKET_READ_GENTLE_ON_EOF | + PACKET_READ_GENTLE_ON_READ_ERROR; int ret = 0; + if (gentle) + read_options |= PACKET_READ_SILENT_ON_READ_ERROR; + strbuf_setlen(answer, 0); trace2_region_enter("ipc-client", "send-command", NULL); - if (write_packetized_from_buf_no_flush(message, message_len, - connection->fd) < 0 || - packet_flush_gently(connection->fd) < 0) { + if (ipc_client_write_command( + connection, message, message_len, gentle) < 0) { ret = gentle ? -1 : error(_("could not send IPC command")); goto done; } if (read_packetized_to_strbuf( - connection->fd, answer, - PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) { + connection->fd, answer, read_options) < 0) { ret = gentle ? -1 : error(_("could not read IPC response")); goto done; } @@ -335,6 +419,7 @@ struct ipc_server_data { pthread_mutex_t work_available_mutex; pthread_cond_t work_available_cond; + pthread_mutex_t lifecycle_mutex; /* * Accepted but not yet processed client connections are kept @@ -895,6 +980,7 @@ int ipc_server_init_async(struct ipc_server_data **returned_server_data, pthread_mutex_init(&server_data->work_available_mutex, NULL); pthread_cond_init(&server_data->work_available_cond, NULL); + pthread_mutex_init(&server_data->lifecycle_mutex, NULL); server_data->queue_size = nr_threads * FIFO_SCALE; CALLOC_ARRAY(server_data->fifo_fds, server_data->queue_size); @@ -945,11 +1031,15 @@ int ipc_server_init_async(struct ipc_server_data **returned_server_data, void ipc_server_start_async(struct ipc_server_data *server_data) { - if (!server_data || server_data->started) + if (!server_data) return; - server_data->started = 1; - pthread_mutex_unlock(&server_data->work_available_mutex); + pthread_mutex_lock(&server_data->lifecycle_mutex); + if (!server_data->started && !server_data->shutdown_requested) { + server_data->started = 1; + pthread_mutex_unlock(&server_data->work_available_mutex); + } + pthread_mutex_unlock(&server_data->lifecycle_mutex); } /* @@ -966,19 +1056,28 @@ int ipc_server_stop_async(struct ipc_server_data *server_data) return 0; trace2_region_enter("ipc-server", "server-stop-async", NULL); + pthread_mutex_lock(&server_data->lifecycle_mutex); + if (server_data->shutdown_requested) { + pthread_mutex_unlock(&server_data->lifecycle_mutex); + trace2_region_leave("ipc-server", "server-stop-async", NULL); + return 0; + } /* If we haven't started yet, we are already holding lock. */ if (server_data->started) pthread_mutex_lock(&server_data->work_available_mutex); - server_data->shutdown_requested = 1; - /* * Write a byte to the shutdown socket pair to wake up the - * accept-thread. + * accept-thread. Do not publish the shutdown transition until the + * byte has been queued: a failed write must leave a later caller able + * to retry it. */ - if (write(server_data->accept_thread->fd_send_shutdown, "Q", 1) < 0) - error_errno("could not write to fd_send_shutdown"); + if (write_in_full(server_data->accept_thread->fd_send_shutdown, + "Q", 1) < 0) + die_errno(_("could not write to fd_send_shutdown")); + + server_data->shutdown_requested = 1; /* * Drain the queue of existing connections. @@ -993,6 +1092,7 @@ int ipc_server_stop_async(struct ipc_server_data *server_data) pthread_cond_broadcast(&server_data->work_available_cond); pthread_mutex_unlock(&server_data->work_available_mutex); + pthread_mutex_unlock(&server_data->lifecycle_mutex); trace2_region_leave("ipc-server", "server-stop-async", NULL); @@ -1058,6 +1158,7 @@ void ipc_server_free(struct ipc_server_data *server_data) pthread_cond_destroy(&server_data->work_available_cond); pthread_mutex_destroy(&server_data->work_available_mutex); + pthread_mutex_destroy(&server_data->lifecycle_mutex); strbuf_release(&server_data->buf_path); diff --git a/compat/simple-ipc/ipc-win32.c b/compat/simple-ipc/ipc-win32.c index f1b4124d3ae8df..341f558609246e 100644 --- a/compat/simple-ipc/ipc-win32.c +++ b/compat/simple-ipc/ipc-win32.c @@ -240,15 +240,23 @@ static int ipc_client_send_command_to_connection_1( const char *message, size_t message_len, struct strbuf *answer, int gentle) { + int read_options = PACKET_READ_GENTLE_ON_EOF | + PACKET_READ_GENTLE_ON_READ_ERROR; + unsigned write_options = gentle ? + PACKET_WRITE_SILENT_ON_WRITE_ERROR : 0; int ret = 0; + if (gentle) + read_options |= PACKET_READ_SILENT_ON_READ_ERROR; + strbuf_setlen(answer, 0); trace2_region_enter("ipc-client", "send-command", NULL); - if (write_packetized_from_buf_no_flush(message, message_len, - connection->fd) < 0 || - packet_flush_gently(connection->fd) < 0) { + if (write_packetized_from_buf_no_flush_with_options( + message, message_len, connection->fd, write_options) < 0 || + packet_flush_gently_with_options( + connection->fd, write_options) < 0) { ret = gentle ? -1 : error(_("could not send IPC command")); goto done; } @@ -256,8 +264,7 @@ static int ipc_client_send_command_to_connection_1( FlushFileBuffers((HANDLE)_get_osfhandle(connection->fd)); if (read_packetized_to_strbuf( - connection->fd, answer, - PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) { + connection->fd, answer, read_options) < 0) { ret = gentle ? -1 : error(_("could not read IPC response")); goto done; } diff --git a/dir.c b/dir.c index b0e8aea8c85b22..261d454db6ee54 100644 --- a/dir.c +++ b/dir.c @@ -2856,6 +2856,56 @@ static struct path_pattern *last_matching_pattern_from_lists( return NULL; } +/* + * A valid cache directory may not have needed its per-directory excludes yet, + * leaving exclude_oid null. When a provider later reports its first child, + * use an exact stage-zero index entry as the comparison point before loading + * the worktree source. A regular stage-zero case-folded alias is also a safe + * comparison point: add_patterns() still opens and hashes the actual worktree + * source because its exact-case index lookup misses the alias. For staged, + * non-regular, removed, or intent-to-add matches, use the empty-blob ID to + * force the same source read and conservative invalidation. If the source is + * actually empty, retaining the cache is semantically safe. + */ +static int prime_cached_exclude_from_index(struct index_state *istate, + const struct strbuf *base, + const char *exclude_per_dir, + struct object_id *oid) +{ + struct cache_entry *ce; + struct strbuf path = STRBUF_INIT; + int pos; + + strbuf_addbuf(&path, base); + strbuf_addstr(&path, exclude_per_dir); + pos = index_name_pos(istate, path.buf, path.len); + if (pos >= 0) { + ce = istate->cache[pos]; + } else { + pos = -1 - pos; + if (pos < istate->cache_nr && + ce_namelen(istate->cache[pos]) == path.len && + !memcmp(istate->cache[pos]->name, path.buf, path.len)) { + ce = istate->cache[pos]; + } else if (repo_ignore_case(istate->repo)) { + ce = index_file_exists(istate, path.buf, path.len, 1); + } else { + ce = NULL; + } + } + if (!ce) { + strbuf_release(&path); + return 0; + } + if (!ce_stage(ce) && S_ISREG(ce->ce_mode) && + !(ce->ce_flags & CE_REMOVE) && !ce_intent_to_add(ce)) + oidcpy(oid, &ce->oid); + else + oidcpy(oid, the_hash_algo->empty_blob); + strbuf_release(&path); + return 1; +} + /* * Loads the per-directory exclude list for the substring of base * which has a char length of baselen. @@ -2958,6 +3008,15 @@ static void prep_exclude(struct dir_struct *dir, /* Try to read per-directory file */ oidclr(&oid_stat.oid, the_repository->hash_algo); oid_stat.valid = 0; + if (dir->exclude_per_dir && untracked && + untracked->valid && untracked->fsmonitor_dirty && + is_null_oid(&untracked->exclude_oid) && + prime_cached_exclude_from_index( + istate, &dir->internal.basebuf, + dir->exclude_per_dir, &untracked->exclude_oid)) { + istate->cache_changed |= UNTRACKED_CHANGED; + istate->fsmonitor_untracked_must_persist = 1; + } if (dir->exclude_per_dir && /* * If we know that no files have been added in diff --git a/fsmonitor--daemon.h b/fsmonitor--daemon.h index 850188f872b783..561aed4c497657 100644 --- a/fsmonitor--daemon.h +++ b/fsmonitor--daemon.h @@ -46,6 +46,7 @@ struct fsmonitor_daemon_state { int nr_paths_watching; struct fsmonitor_token_data *current_token_data; + uint64_t token_generation; struct strbuf path_cookie_prefix; pthread_cond_t cookies_cond; @@ -161,6 +162,15 @@ void fsmonitor_publish(struct fsmonitor_daemon_state *state, struct fsmonitor_batch *batch, const struct string_list *cookie_names); +/* + * Publish a delayed batch only if it still belongs to the named token + * generation. The batch is consumed in either case. + */ +int fsmonitor_publish_if_current_generation( + struct fsmonitor_daemon_state *state, + struct fsmonitor_batch *batch, + uint64_t token_generation); + /* * If the platform-specific layer loses sync with the filesystem, * it should call this to invalidate cached data and abort waiting diff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c index 5ddd7503ca60f4..23ee22432d25ce 100644 --- a/fsmonitor-ipc.c +++ b/fsmonitor-ipc.c @@ -450,9 +450,14 @@ static int server_supports_required_capabilities(void) has_capability(&answer, FSMONITOR_IPC_HARDLINK_QUERY_VERSION) && has_capability(&answer, - FSMONITOR_IPC_DIR_METADATA_CAPABILITY) && + FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY) && has_capability(&answer, - FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY); + FSMONITOR_IPC_DARWIN_PROVIDER_FENCE_CAPABILITY); +#endif +#if FSMONITOR_IPC_HAS_DIR_METADATA + ret = ret && + has_capability(&answer, + FSMONITOR_IPC_DIR_METADATA_CAPABILITY); #endif strbuf_release(&answer); return ret; @@ -463,10 +468,7 @@ static int response_identifies_cookie_retiring_daemon( { static const char prefix[] = "builtin:" -#ifdef __APPLE__ - FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX -#endif - FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX; + FSMONITOR_IPC_COOKIE_TOKEN_PREFIX; const char *end = memchr(answer->buf, '\0', answer->len); return end && @@ -853,7 +855,7 @@ static int restart_incompatible_daemon(void) return ret; } -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__linux__) static int spawn_daemon_serialized(void) { struct strbuf lock_path = STRBUF_INIT; @@ -958,6 +960,7 @@ int fsmonitor_ipc__send_query(const char *since_token, goto try_again; } if (!ret && is_trivial_response(answer) && + !response_identifies_cookie_retiring_daemon(answer) && !server_supports_bound_queries()) { if (!try_send_attested_legacy_query( tok, &identity, answer)) { @@ -991,7 +994,7 @@ int fsmonitor_ipc__send_query(const char *since_token, if (lifecycle_attempts++ >= FSMONITOR_RESTART_ATTEMPTS) goto done; -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__linux__) if (spawn_daemon_serialized()) #else if (spawn_daemon()) diff --git a/fsmonitor-ipc.h b/fsmonitor-ipc.h index ba1c05eea5c065..70eaba651f4830 100644 --- a/fsmonitor-ipc.h +++ b/fsmonitor-ipc.h @@ -19,8 +19,33 @@ struct repository; #define FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY \ "cookie-token-retirement-v1" #define FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX "cookie-v1." +#define FSMONITOR_IPC_DARWIN_PROVIDER_FENCE_CAPABILITY \ + "darwin-provider-fence-v1" +#define FSMONITOR_IPC_DARWIN_PROVIDER_FENCE_TOKEN_PREFIX "fence-v1." #define FSMONITOR_IPC_WORKTREE_ID_HEX 64 +#ifdef __APPLE__ +#define FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX \ + FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX +#define FSMONITOR_IPC_COOKIE_TOKEN_PREFIX \ + FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX \ + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX \ + FSMONITOR_IPC_DARWIN_PROVIDER_FENCE_TOKEN_PREFIX +#define FSMONITOR_IPC_HAS_DIR_METADATA 1 +#elif defined(__linux__) +#define FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX \ + FSMONITOR_IPC_DIR_METADATA_TOKEN_PREFIX +#define FSMONITOR_IPC_COOKIE_TOKEN_PREFIX \ + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX \ + FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX +#define FSMONITOR_IPC_HAS_DIR_METADATA 1 +#else +#define FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX "" +#define FSMONITOR_IPC_COOKIE_TOKEN_PREFIX \ + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX +#define FSMONITOR_IPC_HAS_DIR_METADATA 0 +#endif + /* Hash the canonical worktree root and its stable filesystem identity. */ int fsmonitor_ipc__get_worktree_identity(struct repository *r, struct strbuf *identity); diff --git a/fsmonitor.c b/fsmonitor.c index 4b8a52939ee227..2e463dcf100726 100644 --- a/fsmonitor.c +++ b/fsmonitor.c @@ -943,21 +943,29 @@ void fsmonitor_format_worktree_paths( static int fsmonitor_valid_worktree_path(const char *path, size_t len) { - struct strbuf copy = STRBUF_INIT; - int valid = 0; + size_t component = 0, i; if (!len || is_dir_sep(path[0]) || has_dos_drive_prefix(path)) return 0; - strbuf_add(©, path, len); - if (is_dir_sep(copy.buf[copy.len - 1])) - strbuf_setlen(©, copy.len - 1); - if (!copy.len || is_dir_sep(copy.buf[copy.len - 1])) - goto done; - valid = verify_path(copy.buf, 0); + if (is_dir_sep(path[len - 1]) && + (!--len || is_dir_sep(path[len - 1]))) + return 0; -done: - strbuf_release(©); - return valid; + for (i = 0; i <= len; i++) { + size_t component_len; + + if (i < len && !is_dir_sep(path[i])) + continue; + component_len = i - component; + if (!component_len || + (component_len == 1 && path[component] == '.') || + (component_len == 2 && path[component] == '.' && + path[component + 1] == '.')) + return 0; + component = i + 1; + } + + return 1; } static int fsmonitor_parse_hardlink_inode(const char *path, size_t len, @@ -1795,6 +1803,24 @@ void refresh_fsmonitor(struct index_state *istate) } } +void fsmonitor_refresh_after_worktree_update(struct index_state *istate) +{ + if (!istate->fsmonitor_has_run_once || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC || + !istate->fsmonitor_token_valid || !istate->fsmonitor_last_update) + return; + + /* + * refresh_fsmonitor() is normally once-per-process. An owned checkout + * performed after that query creates a new event interval, so consume it + * before a writer closes and persists the repaired proof. + */ + istate->fsmonitor_has_run_once = 0; + refresh_fsmonitor(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "history/post-worktree-refresh", 1); +} + int fsmonitor_has_pending_token(const struct index_state *istate) { return !!istate->fsmonitor_last_update_pending; diff --git a/fsmonitor.h b/fsmonitor.h index 6d5f3d3bc34c29..ce2e987a64ed12 100644 --- a/fsmonitor.h +++ b/fsmonitor.h @@ -66,6 +66,9 @@ static inline int fsmonitor_stat_can_be_valid(const struct stat *st) void fsmonitor_invalidate_semantics(struct index_state *istate); +/* Query changes created after an owned worktree update in this process. */ +void fsmonitor_refresh_after_worktree_update(struct index_state *istate); + /* Bound conservative bootstrap to one index read; never issue a proof. */ void fsmonitor_begin_scoped_bootstrap(struct index_state *istate); int fsmonitor_scoped_bootstrap_is_active(const struct index_state *istate); diff --git a/lockfile.h b/lockfile.h index 1667612674b52a..660217e3269ac6 100644 --- a/lockfile.h +++ b/lockfile.h @@ -344,6 +344,11 @@ static inline int reopen_lock_file(struct lock_file *lk) return reopen_tempfile(lk->tempfile); } +static inline int reopen_lock_file_for_readwrite(struct lock_file *lk) +{ + return reopen_tempfile_for_readwrite(lk->tempfile); +} + /* * Commit the change represented by `lk`: close the file descriptor * and/or file pointer if they are still open and rename the lockfile diff --git a/merge.c b/merge.c index ac37e84ad87465..9de38c739a00a9 100644 --- a/merge.c +++ b/merge.c @@ -5,6 +5,7 @@ #include "clean-status.h" #include "hash.h" #include "hex.h" +#include "fsmonitor.h" #include "lockfile.h" #include "merge.h" #include "commit.h" @@ -14,6 +15,7 @@ #include "tree.h" #include "tree-walk.h" #include "unpack-trees.h" +#include "wt-status.h" static const char *merge_argument(struct commit *commit) { @@ -58,10 +60,17 @@ int checkout_fast_forward(struct repository *r, struct tree *trees[MAX_UNPACK_TREES]; struct unpack_trees_options opts; struct tree_desc t[MAX_UNPACK_TREES]; - int i, nr_trees = 0; + int i, nr_trees = 0, repair_after_checkout; struct lock_file lock_file = LOCK_INIT; refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL); + repair_after_checkout = + clean_status_has_current_full_fsmonitor_proof(r->index); + if (!repair_after_checkout && + wt_status_fsmonitor_proof_needs_repair(r) && + wt_status_repair_fsmonitor_proof(r)) + repair_after_checkout = + clean_status_has_current_full_fsmonitor_proof(r->index); if (repo_hold_locked_index(r, &lock_file, LOCK_REPORT_ON_ERROR) < 0) return -1; @@ -99,6 +108,7 @@ int checkout_fast_forward(struct repository *r, opts.merge = 1; opts.preserve_semantic_history = clean_status_revalidated_token_matches(r->index); + opts.preserve_untracked_history = opts.preserve_semantic_history; opts.fn = twoway_merge; init_checkout_metadata(&opts.meta, NULL, remote, NULL); setup_unpack_trees_porcelain(&opts, "merge"); @@ -109,6 +119,11 @@ int checkout_fast_forward(struct repository *r, return -1; } clear_unpack_trees_porcelain(&opts); + if (wt_status_repair_fsmonitor_proof_after_worktree_update( + r, &lock_file, repair_after_checkout) < 0) { + rollback_lock_file(&lock_file); + return error(_("unable to repair new index file")); + } if (write_locked_index(r->index, &lock_file, COMMIT_LOCK)) return error(_("unable to write new index file")); diff --git a/meson.build b/meson.build index bad1fd85101cb3..da072f54758d29 100644 --- a/meson.build +++ b/meson.build @@ -340,6 +340,7 @@ libgit_sources = [ 'clean-status-history.c', 'clean-status-identity.c', 'clean-status-index.c', + 'clean-status-index-provisional.c', 'clean-status-manifest.c', 'clean-status-sidecar.c', 'clean-status-fast.c', diff --git a/pkt-line.c b/pkt-line.c index 3fc3e9ea7059be..b6bff1220c69e1 100644 --- a/pkt-line.c +++ b/pkt-line.c @@ -111,14 +111,22 @@ void packet_response_end(int fd) die_errno(_("unable to write response end packet")); } -int packet_flush_gently(int fd) +int packet_flush_gently_with_options(int fd, unsigned options) { packet_trace("0000", 4, 1); - if (write_in_full(fd, "0000", 4) < 0) + if (write_in_full(fd, "0000", 4) < 0) { + if (options & PACKET_WRITE_SILENT_ON_WRITE_ERROR) + return -1; return error(_("flush packet write failed")); + } return 0; } +int packet_flush_gently(int fd) +{ + return packet_flush_gently_with_options(fd, 0); +} + void packet_buf_flush(struct strbuf *buf) { packet_trace("0000", 4, 1); @@ -230,12 +238,17 @@ static int do_packet_write(const int fd_out, const char *buf, size_t size, return 0; } -static int packet_write_gently(const int fd_out, const char *buf, size_t size) +static int packet_write_gently(const int fd_out, const char *buf, size_t size, + unsigned options) { struct strbuf err = STRBUF_INIT; if (do_packet_write(fd_out, buf, size, &err)) { - error("%s", err.buf); + int saved_errno = errno; + + if (!(options & PACKET_WRITE_SILENT_ON_WRITE_ERROR)) + error("%s", err.buf); strbuf_release(&err); + errno = saved_errno; return -1; } return 0; @@ -308,14 +321,15 @@ int write_packetized_from_fd_no_flush(int fd_in, int fd_out) } if (bytes_to_write == 0) break; - err = packet_write_gently(fd_out, buf, bytes_to_write); + err = packet_write_gently(fd_out, buf, bytes_to_write, 0); } free(buf); return err; } -int write_packetized_from_buf_no_flush_count(const char *src_in, size_t len, - int fd_out, int *packet_counter) +static int write_packetized_from_buf_no_flush_1(const char *src_in, size_t len, + int fd_out, int *packet_counter, + unsigned options) { int err = 0; size_t bytes_written = 0; @@ -328,7 +342,8 @@ int write_packetized_from_buf_no_flush_count(const char *src_in, size_t len, bytes_to_write = len - bytes_written; if (bytes_to_write == 0) break; - err = packet_write_gently(fd_out, src_in + bytes_written, bytes_to_write); + err = packet_write_gently(fd_out, src_in + bytes_written, + bytes_to_write, options); bytes_written += bytes_to_write; if (packet_counter) (*packet_counter)++; @@ -336,6 +351,21 @@ int write_packetized_from_buf_no_flush_count(const char *src_in, size_t len, return err; } +int write_packetized_from_buf_no_flush_count(const char *src_in, size_t len, + int fd_out, int *packet_counter) +{ + return write_packetized_from_buf_no_flush_1( + src_in, len, fd_out, packet_counter, 0); +} + +int write_packetized_from_buf_no_flush_with_options(const char *src_in, + size_t len, int fd_out, + unsigned options) +{ + return write_packetized_from_buf_no_flush_1( + src_in, len, fd_out, NULL, options); +} + static int get_packet_data(int fd, char **src_buf, size_t *src_size, void *dst, size_t size, int options) { @@ -353,6 +383,9 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size, } else { ssize_t ret = read_in_full(fd, dst, size); if (ret < 0) { + if ((options & PACKET_READ_GENTLE_ON_READ_ERROR) && + (options & PACKET_READ_SILENT_ON_READ_ERROR)) + return -1; if (options & PACKET_READ_GENTLE_ON_READ_ERROR) return error_errno(_("read error")); die_errno(_("read error")); diff --git a/pkt-line.h b/pkt-line.h index e6cf85e34ee3c4..620cb64cd0306b 100644 --- a/pkt-line.h +++ b/pkt-line.h @@ -27,11 +27,16 @@ void packet_buf_delim(struct strbuf *buf); void set_packet_header(char *buf, int size); void packet_write(int fd_out, const char *buf, size_t size); void packet_buf_write(struct strbuf *buf, const char *fmt, ...) __attribute__((format (printf, 2, 3))); +#define PACKET_WRITE_SILENT_ON_WRITE_ERROR (1u << 0) int packet_flush_gently(int fd); +int packet_flush_gently_with_options(int fd, unsigned options); int packet_write_fmt_gently(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3))); int write_packetized_from_fd_no_flush(int fd_in, int fd_out); int write_packetized_from_buf_no_flush_count(const char *src_in, size_t len, int fd_out, int *packet_counter); +int write_packetized_from_buf_no_flush_with_options(const char *src_in, + size_t len, int fd_out, + unsigned options); static inline int write_packetized_from_buf_no_flush(const char *src_in, size_t len, int fd_out) { @@ -78,6 +83,10 @@ void packet_fflush(FILE *f); * If options contains PACKET_READ_GENTLE_ON_READ_ERROR, we will not die * on read errors, but instead return -1. However, we may still die on an * ERR packet (if requested). + * + * If options also contains PACKET_READ_SILENT_ON_READ_ERROR, an operating + * system read error is returned without first reporting it. This is useful + * when a caller expects a peer to disappear and will reconnect. */ #define PACKET_READ_GENTLE_ON_EOF (1u<<0) #define PACKET_READ_CHOMP_NEWLINE (1u<<1) @@ -85,6 +94,7 @@ void packet_fflush(FILE *f); #define PACKET_READ_GENTLE_ON_READ_ERROR (1u<<3) #define PACKET_READ_REDACT_URI_PATH (1u<<4) #define PACKET_READ_USE_SIDEBAND (1u<<5) +#define PACKET_READ_SILENT_ON_READ_ERROR (1u<<6) int packet_read(int fd, char *buffer, unsigned size, int options); /* diff --git a/read-cache-ll.h b/read-cache-ll.h index da72f6e2fc4b6a..87ddfa59fb3114 100644 --- a/read-cache-ll.h +++ b/read-cache-ll.h @@ -340,6 +340,7 @@ int is_index_unborn(struct index_state *); /* For use with `write_locked_index()`. */ #define COMMIT_LOCK (1 << 0) #define SKIP_IF_UNCHANGED (1 << 1) +#define PROVISIONAL_LOCK (1 << 2) /* * Write the index while holding an already-taken lock. Close the lock, @@ -359,8 +360,14 @@ int is_index_unborn(struct index_state *); * * If `SKIP_IF_UNCHANGED` is given and the index is unchanged, nothing * is written (and the lock is rolled back if `COMMIT_LOCK` is given). + * `PROVISIONAL_LOCK` writes a close-only witness which the lock owner will + * reopen and replace before commit; it therefore defers post-index-change. */ int write_locked_index(struct index_state *, struct lock_file *lock, unsigned flags); +/* Also retain a fail-closed receipt for the canonical file committed. */ +int write_locked_index_with_receipt( + struct index_state *, struct lock_file *, unsigned flags, + struct clean_status_index_write_receipt *); /* Commit's close-only main-index write and optional historical-only repair. */ int write_locked_index_for_commit( diff --git a/read-cache.c b/read-cache.c index 6c1270306438ba..5851ab1bce4787 100644 --- a/read-cache.c +++ b/read-cache.c @@ -945,7 +945,10 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, ce->ce_mode == alias->ce_mode); logical_same = same_persistent_add_entry(alias, ce); semantic_same = clean_status_index_entry_is_semantically_safe( - istate, alias, ce); + istate, alias, ce) || + (intent_only && + clean_status_intent_to_add_change_is_semantically_safe( + istate, alias, ce)); if (!pretend && (flags & ADD_CACHE_TRACK_CLEAN_HISTORY) && !logical_same && !semantic_same) @@ -3381,7 +3384,7 @@ int has_racy_timestamp(struct index_state *istate) return 0; } -static int write_locked_index_with_receipt( +static int write_locked_index_with_receipt_and_checkpoint( struct index_state *istate, struct lock_file *lock, unsigned flags, struct clean_status_index_write_receipt *receipt, struct clean_status_commit_checkpoint *checkpoint); @@ -3395,8 +3398,8 @@ void repo_update_index_if_able_with_receipt( if ((repo->index->cache_changed || has_racy_timestamp(repo->index)) && repo_verify_index(repo)) - write_locked_index_with_receipt(repo->index, lockfile, - COMMIT_LOCK, receipt, NULL); + write_locked_index_with_receipt_and_checkpoint( + repo->index, lockfile, COMMIT_LOCK, receipt, NULL); else rollback_lock_file(lockfile); } @@ -3532,7 +3535,14 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile, f = hashfd(the_repository->hash_algo, tempfile->fd, tempfile->filename.buf); prepare_repo_settings(r); - f->skip_hash = r->settings.index_skip_hash; + /* + * A provisional lock is a proof witness for the current in-memory + * index. Its fresh file identity cannot authenticate a null trailer, + * so give only that witness a checksum. The final index write still + * honors index.skipHash. + */ + f->skip_hash = r->settings.index_skip_hash && + !(flags & PROVISIONAL_LOCK); for (i = removed = extended = 0; i < entries; i++) { if (cache[i]->ce_flags & CE_REMOVE) @@ -3931,11 +3941,13 @@ static int do_write_locked_index( if (!ret && checkpoint && !(flags & COMMIT_LOCK)) clean_status_record_commit_checkpoint(checkpoint, istate, lock); - run_hooks_l(the_repository, "post-index-change", - istate->updated_workdir ? "1" : "0", - istate->updated_skipworktree ? "1" : "0", NULL); - istate->updated_workdir = 0; - istate->updated_skipworktree = 0; + if (!(flags & PROVISIONAL_LOCK)) { + run_hooks_l(the_repository, "post-index-change", + istate->updated_workdir ? "1" : "0", + istate->updated_skipworktree ? "1" : "0", NULL); + istate->updated_workdir = 0; + istate->updated_skipworktree = 0; + } return ret; } @@ -4089,7 +4101,7 @@ static int too_many_not_shared_entries(struct index_state *istate) return (int64_t)istate->cache_nr * max_split < (int64_t)not_shared * 100; } -static int write_locked_index_with_receipt( +static int write_locked_index_with_receipt_and_checkpoint( struct index_state *istate, struct lock_file *lock, unsigned flags, struct clean_status_index_write_receipt *receipt, struct clean_status_commit_checkpoint *checkpoint) @@ -4189,7 +4201,16 @@ static int write_locked_index_with_receipt( int write_locked_index(struct index_state *istate, struct lock_file *lock, unsigned flags) { - return write_locked_index_with_receipt(istate, lock, flags, NULL, NULL); + return write_locked_index_with_receipt_and_checkpoint( + istate, lock, flags, NULL, NULL); +} + +int write_locked_index_with_receipt( + struct index_state *istate, struct lock_file *lock, unsigned flags, + struct clean_status_index_write_receipt *receipt) +{ + return write_locked_index_with_receipt_and_checkpoint( + istate, lock, flags, receipt, NULL); } int write_locked_index_for_commit( @@ -4202,7 +4223,8 @@ int write_locked_index_for_commit( clean_status_release_commit_checkpoint(*checkpoint); *checkpoint = NULL; candidate = clean_status_capture_commit_checkpoint(istate, lock); - ret = write_locked_index_with_receipt(istate, lock, 0, NULL, candidate); + ret = write_locked_index_with_receipt_and_checkpoint( + istate, lock, 0, NULL, candidate); if (ret) clean_status_release_commit_checkpoint(candidate); else diff --git a/reset.c b/reset.c index 6d284f80c622ef..e26a2a76da8b1b 100644 --- a/reset.c +++ b/reset.c @@ -11,6 +11,7 @@ #include "tree.h" #include "unpack-trees.h" #include "hook.h" +#include "wt-status.h" static int update_refs(struct repository *repo, const struct reset_working_tree_options *opts, @@ -104,7 +105,7 @@ int reset_working_tree(struct repository *r, struct index_state scratch_index = INDEX_STATE_INIT(r); struct index_state *istate; const char *action; - int ret = 0, nr = 0; + int ret = 0, nr = 0, repair_after_reset = 0; if (switch_to_branch && !starts_with(switch_to_branch, "refs/")) BUG("Not a fully qualified branch: '%s'", switch_to_branch); @@ -171,7 +172,11 @@ int reset_working_tree(struct repository *r, !dry_run && (!reset_hard || (opts->flags & RESET_WORKING_TREE_PRESERVE_SEMANTIC_HISTORY)) && - clean_status_revalidated_token_matches(istate); + clean_status_revalidated_token_matches(istate); + unpack_tree_opts.preserve_untracked_history = + unpack_tree_opts.preserve_semantic_history; + repair_after_reset = unpack_tree_opts.update && + clean_status_has_current_full_fsmonitor_proof(istate); unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL); if (reset_hard) { @@ -206,6 +211,12 @@ int reset_working_tree(struct repository *r, if (reset_hard) prime_cache_tree(r, r->index, tree); + if (unpack_tree_opts.update && + wt_status_repair_fsmonitor_proof_after_worktree_update( + r, &lock, repair_after_reset) < 0) { + ret = error(_("could not repair index")); + goto leave_reset_head; + } if (write_locked_index(r->index, &lock, COMMIT_LOCK) < 0) { ret = error(_("could not write index")); diff --git a/semantic-verify.c b/semantic-verify.c index dc6af79c2ce796..31b3ab8f60ac26 100644 --- a/semantic-verify.c +++ b/semantic-verify.c @@ -134,9 +134,14 @@ int semantic_verify_prepare(struct index_state *istate, return -1; } if (proof->epoch_required) { - proof->epoch = clean_status_capture_proof_epoch( - istate, options->attr_snapshot, - proof->filter_scope_checked); + proof->epoch = options->index_path ? + clean_status_capture_proof_epoch_at_path( + istate, options->attr_snapshot, + proof->filter_scope_checked, + options->index_path) : + clean_status_capture_proof_epoch( + istate, options->attr_snapshot, + proof->filter_scope_checked); if (!proof->epoch) { for (size_t i = 0; i < proof->cache_nr; i++) { proof->results[i].kind = SEMANTIC_VERIFY_ERROR; diff --git a/semantic-verify.h b/semantic-verify.h index 1895a384e28f2d..a08f447eaa016f 100644 --- a/semantic-verify.h +++ b/semantic-verify.h @@ -7,6 +7,7 @@ struct semantic_verify_proof; struct semantic_verify_options { unsigned int nr_threads; + const char *index_path; const struct attr_source_snapshot *attr_snapshot; unsigned int require_proof_epoch : 1; unsigned int validate_filter_scope : 1; diff --git a/sequencer.c b/sequencer.c index 0751ae0f5bf7e2..94ecd2dd2c1605 100644 --- a/sequencer.c +++ b/sequencer.c @@ -7,6 +7,7 @@ #include "config.h" #include "copy.h" #include "environment.h" +#include "fsmonitor-ll.h" #include "gettext.h" #include "hex.h" #include "lockfile.h" @@ -22,6 +23,7 @@ #include "hook.h" #include "utf8.h" #include "cache-tree.h" +#include "clean-status.h" #include "diff.h" #include "path.h" #include "revision.h" @@ -34,6 +36,7 @@ #include "strvec.h" #include "quote.h" #include "trailer.h" +#include "trace2.h" #include "log-tree.h" #include "wt-status.h" #include "hashmap.h" @@ -431,6 +434,11 @@ int sequencer_remove_state(struct replay_opts *opts) struct strbuf buf = STRBUF_INIT; int ret = 0; + if (is_rebase_i(opts) && + refs_delete_ref(get_main_ref_store(the_repository), NULL, + "REBASE_HEAD", NULL, REF_NO_DEREF)) + ret = -1; + if (is_rebase_i(opts) && strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) { char *p = buf.buf; @@ -752,7 +760,7 @@ static int do_recursive_merge(struct repository *r, struct merge_options o; struct merge_result result; struct tree *next_tree, *base_tree, *head_tree; - int clean, show_output; + int clean, show_output, repair_after_merge; int i; struct lock_file index_lock = LOCK_INIT; @@ -760,6 +768,8 @@ static int do_recursive_merge(struct repository *r, return -1; repo_read_index(r); + repair_after_merge = + clean_status_has_current_full_fsmonitor_proof(r->index); init_ui_merge_options(&o, r); o.ancestor = base ? base_label : "(empty tree)"; @@ -795,6 +805,12 @@ static int do_recursive_merge(struct repository *r, rollback_lock_file(&index_lock); return clean; } + if (wt_status_repair_fsmonitor_proof_after_worktree_update( + r, &index_lock, repair_after_merge) < 0) { + rollback_lock_file(&index_lock); + return error(_("%s: Unable to repair new index file"), + _(action_name(opts))); + } if (write_locked_index(r->index, &index_lock, COMMIT_LOCK | SKIP_IF_UNCHANGED)) @@ -5348,6 +5364,47 @@ static int continue_single_pick(struct repository *r, struct replay_opts *opts) return run_command(&cmd); } +static int reload_index_after_commit(struct repository *r, + int repair_fsmonitor_proof) +{ + struct lock_file lock = LOCK_INIT; + int repaired; + + /* git commit may have rewritten the index in the child process. */ + discard_index(r->index); + if (repo_read_index(r) < 0) + return error(_("could not read index")); + if (!repair_fsmonitor_proof || + (clean_status_has_current_full_fsmonitor_proof(r->index) && + !wt_status_fsmonitor_proof_needs_repair(r))) + return 0; + + if (repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) + return error(_("could not write index")); + + /* Recheck the child result while holding the canonical index lock. */ + discard_index(r->index); + if (repo_read_index(r) < 0) { + rollback_lock_file(&lock); + return error(_("could not read index")); + } + if (!r->index->fsmonitor_token_valid) { + r->index->fsmonitor_has_run_once = 0; + refresh_fsmonitor(r->index); + } + repaired = wt_status_repair_fsmonitor_proof_after_index_update( + r, &lock, repair_fsmonitor_proof); + if (repaired < 0) { + rollback_lock_file(&lock); + return error(_("could not repair index")); + } + if (write_locked_index(r->index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + return error(_("could not write index")); + + return 0; +} + static int commit_staged_changes(struct repository *r, struct replay_opts *opts, struct todo_list *todo_list) @@ -5357,6 +5414,9 @@ static int commit_staged_changes(struct repository *r, unsigned int final_fixup = 0, is_clean; struct strbuf rev = STRBUF_INIT; const char *reflog_action = reflog_message(opts, "continue", NULL); + int repair_fsmonitor_proof = + clean_status_has_persistent_fsmonitor_semantic_history(r->index) || + clean_status_has_worktree_manifest_history(r->index); int ret; if (has_unstaged_changes(r, 1)) { @@ -5526,6 +5586,10 @@ static int commit_staged_changes(struct repository *r, ret = error(_("could not commit staged changes.")); goto out; } + if (reload_index_after_commit(r, repair_fsmonitor_proof)) { + ret = -1; + goto out; + } unlink(rebase_path_amend()); unlink(git_path_merge_head(r)); @@ -5555,13 +5619,21 @@ static int commit_staged_changes(struct repository *r, int sequencer_continue(struct repository *r, struct replay_opts *opts) { struct todo_list todo_list = TODO_LIST_INIT; - int res; + int res, reissue_sidecar; + int had_clean_sidecar = wt_status_clean_sidecar_present(r); + int had_full_proof = 0; if (read_and_refresh_cache(r, opts)) return -1; if (read_populate_opts(opts)) return -1; + reissue_sidecar = is_rebase_i(opts) && had_clean_sidecar; + if (reissue_sidecar) + had_full_proof = + clean_status_has_persistent_fsmonitor_semantic_history( + r->index) || + clean_status_has_worktree_manifest_history(r->index); if (is_rebase_i(opts)) { if ((res = read_populate_todo(r, &todo_list, opts))) goto release_todo_list; @@ -5609,6 +5681,15 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts) } res = pick_commits(r, &todo_list, opts); + if (!res && reissue_sidecar && had_full_proof && + use_optional_locks() && + !hook_exists(r, "post-index-change")) { + struct clean_status_config_digest digest; + + if (!clean_status_config_read_repository(r, &digest)) + wt_status_reissue_clean_sidecar_after_worktree_update( + r, had_full_proof, &digest); + } release_todo_list: todo_list_release(&todo_list); return res; diff --git a/t/helper/test-fsmonitor-client.c b/t/helper/test-fsmonitor-client.c index 653d09455382bb..a3f9eb0bbe7bf2 100644 --- a/t/helper/test-fsmonitor-client.c +++ b/t/helper/test-fsmonitor-client.c @@ -64,6 +64,29 @@ static int do_send_query(const char *token) return 0; } +/* + * Send a protocol-v2 token without the capability and worktree-binding + * prefix used by current clients. This models an older client that does + * not understand hard-link inode events. + */ +static int do_send_legacy_query(const char *token) +{ + struct strbuf answer = STRBUF_INIT; + int ret; + + if (!token || !*token) + token = get_token_from_index(); + + ret = fsmonitor_ipc__send_command(token, &answer); + if (ret < 0) + die("could not query fsmonitor--daemon"); + + write_in_full(1, answer.buf, answer.len); + strbuf_release(&answer); + + return 0; +} + /* * Send a "flush" command to the `git-fsmonitor--daemon` (if running) * and tell it to flush its cache. @@ -221,6 +244,7 @@ int cmd__fsmonitor_client(int argc, const char **argv) const char * const fsmonitor_client_usage[] = { "test-tool fsmonitor-client query []", + "test-tool fsmonitor-client query-legacy []", "test-tool fsmonitor-client flush", "test-tool fsmonitor-client record-watch-limit", "test-tool fsmonitor-client hammer [] [] []", @@ -249,6 +273,9 @@ int cmd__fsmonitor_client(int argc, const char **argv) if (!strcmp(subcmd, "query")) return !!do_send_query(token); + if (!strcmp(subcmd, "query-legacy")) + return !!do_send_legacy_query(token); + if (!strcmp(subcmd, "flush")) return !!do_send_flush(); diff --git a/t/helper/test-mktemp.c b/t/helper/test-mktemp.c index da195640a9dcc8..3edd08de482bf3 100644 --- a/t/helper/test-mktemp.c +++ b/t/helper/test-mktemp.c @@ -3,11 +3,28 @@ */ #include "test-tool.h" #include "git-compat-util.h" +#include "tempfile.h" int cmd__mktemp(int argc, const char **argv) { char *template; int fd; + struct tempfile *tempfile; + + if (argc == 3 && !strcmp(argv[1], "--reopen-write-only")) { + tempfile = mks_tempfile_m(argv[2], 0200); + if (!tempfile) + die_errno("unable to create tempfile"); + if (close_tempfile_gently(tempfile)) + die_errno("unable to close tempfile"); + if (reopen_tempfile_for_readwrite(tempfile) >= 0) + die("unexpectedly reopened write-only tempfile for reading"); + if (reopen_tempfile(tempfile) < 0) + die_errno("unable to reopen write-only tempfile"); + if (delete_tempfile(&tempfile)) + die_errno("unable to delete tempfile"); + return 0; + } if (argc != 2) usage("Expected 1 parameter defining the temporary file template"); diff --git a/t/helper/test-simple-ipc.c b/t/helper/test-simple-ipc.c index a7e4750fc9be2a..0bbee3eaa8488b 100644 --- a/t/helper/test-simple-ipc.c +++ b/t/helper/test-simple-ipc.c @@ -11,6 +11,9 @@ #include "strvec.h" #include "run-command.h" #include "trace2.h" +#ifndef GIT_WINDOWS_NATIVE +#include "unix-stream-server.h" +#endif #ifndef SUPPORTS_SIMPLE_IPC int cmd__simple_ipc(int argc, const char **argv) @@ -164,6 +167,7 @@ static int fsmonitor_legacy; static int fsmonitor_capability_superset; static int fsmonitor_pre_dir_metadata; static int fsmonitor_pre_cookie_retirement; +static int fsmonitor_pre_provider_fence; static int fsmonitor_unmarked_response; static int fsmonitor_disconnect_first; @@ -179,16 +183,29 @@ static int app__fsmonitor_capability_superset( FSMONITOR_IPC_QUERY_VERSION "\n" FSMONITOR_IPC_HARDLINK_QUERY_VERSION "\n" FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY "\n" -#ifdef __APPLE__ +#if FSMONITOR_IPC_HAS_DIR_METADATA FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" +#endif +#ifdef __APPLE__ FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n" + FSMONITOR_IPC_DARWIN_PROVIDER_FENCE_CAPABILITY "\n" #endif ; + static const char pre_provider_fence_capabilities[] = + FSMONITOR_IPC_QUERY_VERSION "\n" + FSMONITOR_IPC_HARDLINK_QUERY_VERSION "\n" + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY "\n" + FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" + FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n"; static const char pre_cookie_capabilities[] = FSMONITOR_IPC_QUERY_VERSION "\n" #ifdef __APPLE__ FSMONITOR_IPC_HARDLINK_QUERY_VERSION "\n" +#endif +#if FSMONITOR_IPC_HAS_DIR_METADATA FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" +#endif +#ifdef __APPLE__ FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n" #endif ; @@ -196,16 +213,28 @@ static int app__fsmonitor_capability_superset( FSMONITOR_IPC_QUERY_VERSION "\n"; static const char current_token[] = "builtin:" -#ifdef __APPLE__ - FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX + FSMONITOR_IPC_COOKIE_TOKEN_PREFIX "test-capable:0"; + static const char pre_dir_metadata_token[] = + "builtin:" +#ifdef __linux__ + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX +#else + FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX #endif - FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX "test-capable:0"; + "test-pre-dir:0"; static const char old_token[] = "builtin:" #ifdef __APPLE__ FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX +#else + FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX #endif "test-pre-cookie:0"; + static const char pre_provider_fence_token[] = + "builtin:" + FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX + "test-pre-fence:0"; const char *token; const char *query; size_t token_len, query_len; @@ -223,13 +252,22 @@ static int app__fsmonitor_capability_superset( return reply_cb(reply_data, pre_cookie_capabilities, sizeof(pre_cookie_capabilities) - 1); + if (fsmonitor_pre_provider_fence) + return reply_cb(reply_data, + pre_provider_fence_capabilities, + sizeof(pre_provider_fence_capabilities) - 1); return reply_cb(reply_data, capabilities, sizeof(capabilities) - 1); } - token = fsmonitor_pre_dir_metadata || - fsmonitor_pre_cookie_retirement || fsmonitor_unmarked_response ? - old_token : current_token; + if (fsmonitor_pre_dir_metadata) + token = pre_dir_metadata_token; + else if (fsmonitor_pre_provider_fence) + token = pre_provider_fence_token; + else if (fsmonitor_pre_cookie_retirement || fsmonitor_unmarked_response) + token = old_token; + else + token = current_token; token_len = strlen(token); query = memchr(command, '\n', command_len); query_len = query ? command_len - (query + 1 - command) : 0; @@ -289,7 +327,8 @@ static int test_app_cb(void *application_data, } if (fsmonitor_capability_superset || fsmonitor_pre_dir_metadata || - fsmonitor_pre_cookie_retirement || fsmonitor_unmarked_response) + fsmonitor_pre_cookie_retirement || + fsmonitor_pre_provider_fence || fsmonitor_unmarked_response) return app__fsmonitor_capability_superset( command, command_len, reply_cb, reply_data); @@ -377,6 +416,28 @@ static int daemon__run_server(void) return ret; } +static int daemon__stop_before_start(void) +{ + struct ipc_server_data *server_data; + struct ipc_server_opts opts = { + .nr_threads = cl_args.nr_threads, + }; + int ret; + + ret = ipc_server_init_async(&server_data, cl_args.path, &opts, + test_app_cb, (void *)&my_app_data); + if (ret) + return ret; + + /* A failed owner and its cleanup path may race before startup. */ + ipc_server_stop_async(server_data); + ipc_server_stop_async(server_data); + ipc_server_start_async(server_data); + ipc_server_await(server_data); + ipc_server_free(server_data); + return 0; +} + static start_bg_wait_cb bg_wait_cb; static int bg_wait_cb(const struct child_process *cp UNUSED, @@ -420,6 +481,8 @@ static int daemon__start_server(void) strvec_push(&cp.args, "--fsmonitor-pre-dir-metadata"); if (fsmonitor_pre_cookie_retirement) strvec_push(&cp.args, "--fsmonitor-pre-cookie-retirement"); + if (fsmonitor_pre_provider_fence) + strvec_push(&cp.args, "--fsmonitor-pre-provider-fence"); if (fsmonitor_unmarked_response) strvec_push(&cp.args, "--fsmonitor-unmarked-response"); if (fsmonitor_disconnect_first) @@ -513,6 +576,234 @@ static int client__send_ipc(void) return error("failed to send '%s' to '%s'", command, cl_args.path); } +#ifndef GIT_WINDOWS_NATIVE +/* + * Close accepted connections before the client writes its request. This is + * deliberately below the simple-IPC server layer so that the peer cannot + * consume any part of the request first. + */ +struct close_peer_server_data { + struct unix_ss_socket *server_socket; + int nr_connections; + int error; +}; + +static void *close_peer_server_proc(void *data) +{ + struct close_peer_server_data *d = data; + int i; + + for (i = 0; i < d->nr_connections; i++) { + int fd; + + do { + fd = accept(d->server_socket->fd_socket, NULL, NULL); + } while (fd < 0 && errno == EINTR); + if (fd < 0) { + d->error = errno; + break; + } + close(fd); + } + return NULL; +} + +struct closed_peer_client_data { + struct ipc_client_connection **connections; + int nr_connections; + int preserve_pending_sigpipe; + int errors; +}; + +static void *closed_peer_client_proc(void *data) +{ + struct closed_peer_client_data *d = data; + sigset_t original, pending, sigpipe; + int expected_blocked, expected_pending, i, mask_error; + + sigemptyset(&sigpipe); + sigaddset(&sigpipe, SIGPIPE); + mask_error = pthread_sigmask(SIG_SETMASK, NULL, &original); + if (mask_error) { + error("could not read initial signal mask: %s", + strerror(mask_error)); + d->errors++; + return NULL; + } + if (sigpending(&pending) < 0) { + error_errno("could not read initial pending signals"); + d->errors++; + return NULL; + } + expected_blocked = sigismember(&original, SIGPIPE); + expected_pending = sigismember(&pending, SIGPIPE) == 1; + + if (d->preserve_pending_sigpipe) { + mask_error = pthread_sigmask(SIG_BLOCK, &sigpipe, NULL); + if (mask_error || raise(SIGPIPE) || sigpending(&pending) < 0 || + sigismember(&pending, SIGPIPE) != 1) { + error("could not establish pending SIGPIPE state"); + d->errors++; + return NULL; + } + expected_blocked = 1; + expected_pending = 1; + } + + for (i = 0; i < d->nr_connections; i++) { + struct strbuf answer = STRBUF_INIT; + sigset_t current; +#ifdef SO_NOSIGPIPE + int no_sigpipe; + socklen_t no_sigpipe_len = sizeof(no_sigpipe); + + if (getsockopt(d->connections[i]->fd, SOL_SOCKET, + SO_NOSIGPIPE, &no_sigpipe, + &no_sigpipe_len) < 0) { + error_errno("connection %d does not suppress SIGPIPE", i); + d->errors++; + ipc_client_close_connection(d->connections[i]); + strbuf_release(&answer); + continue; + } + if (!no_sigpipe) { + error("connection %d does not suppress SIGPIPE", i); + d->errors++; + ipc_client_close_connection(d->connections[i]); + strbuf_release(&answer); + continue; + } +#endif + + if (ipc_client_send_command_to_connection_gently( + d->connections[i], "ping", strlen("ping"), + &answer) >= 0) { + error("connection %d unexpectedly succeeded", i); + d->errors++; + } + ipc_client_close_connection(d->connections[i]); + strbuf_release(&answer); + + mask_error = pthread_sigmask(SIG_SETMASK, NULL, ¤t); + if (mask_error || + sigismember(¤t, SIGPIPE) != expected_blocked || + sigpending(&pending) < 0 || + (sigismember(&pending, SIGPIPE) == 1) != + expected_pending) { + error("connection %d changed SIGPIPE state", i); + d->errors++; + } + } + + if (d->preserve_pending_sigpipe) { + int received; + + mask_error = sigwait(&sigpipe, &received); + if (mask_error || received != SIGPIPE) { + error("could not consume preserved SIGPIPE state"); + d->errors++; + } + } + mask_error = pthread_sigmask(SIG_SETMASK, &original, NULL); + if (mask_error) { + error("could not restore signal mask: %s", strerror(mask_error)); + d->errors++; + } + + return NULL; +} + +/* + * Verify concurrently that a gentle client write reports a closed peer rather + * than dying from SIGPIPE, and that it preserves the caller's signal state. + */ +static int client__gentle_write_failure(void) +{ + struct unix_stream_listen_opts listen_opts = + UNIX_STREAM_LISTEN_OPTS_INIT; + struct ipc_client_connect_options connect_opts = + IPC_CLIENT_CONNECT_OPTIONS_INIT; + struct unix_ss_socket *server_socket = NULL; + struct close_peer_server_data server_data; + struct closed_peer_client_data pending_client; + struct ipc_client_connection *pending_connection; + pthread_t server_thread; + enum { NR_THREADS = 8, CONNECTIONS_PER_THREAD = 8 }; + struct closed_peer_client_data clients[NR_THREADS]; + pthread_t client_threads[NR_THREADS]; + int i, j, ret; + + listen_opts.listen_backlog_size = + NR_THREADS * CONNECTIONS_PER_THREAD + 1; + ret = unix_ss_create(cl_args.path, &listen_opts, -1, + &server_socket); + if (ret) + return error_errno("could not create close-peer server"); + + server_data.server_socket = server_socket; + server_data.nr_connections = + NR_THREADS * CONNECTIONS_PER_THREAD + 1; + server_data.error = 0; + connect_opts.wait_if_busy = 1; + if (ipc_client_try_connect(cl_args.path, &connect_opts, + &pending_connection) != IPC_STATE__LISTENING) + die("could not connect pending-signal client"); + for (i = 0; i < NR_THREADS; i++) { + CALLOC_ARRAY(clients[i].connections, CONNECTIONS_PER_THREAD); + clients[i].nr_connections = CONNECTIONS_PER_THREAD; + clients[i].preserve_pending_sigpipe = 0; + clients[i].errors = 0; + for (j = 0; j < CONNECTIONS_PER_THREAD; j++) + if (ipc_client_try_connect( + cl_args.path, &connect_opts, + &clients[i].connections[j]) != + IPC_STATE__LISTENING) + die("could not connect to close-peer server"); + } + + /* + * Queue every connection before accepting any of them so the client + * completes its post-connect socket setup before the peer closes. + */ + if (pthread_create(&server_thread, NULL, close_peer_server_proc, + &server_data)) + return error("could not start close-peer server"); + + if (pthread_join(server_thread, NULL)) + return error("could not join close-peer server"); + unix_ss_free(server_socket); + if (server_data.error) { + errno = server_data.error; + return error_errno("close-peer server failed"); + } + + /* + * Check preservation of an existing pending SIGPIPE without another + * thread racing to accept the process-directed signal on Darwin. + */ + pending_client.connections = &pending_connection; + pending_client.nr_connections = 1; + pending_client.preserve_pending_sigpipe = 1; + pending_client.errors = 0; + closed_peer_client_proc(&pending_client); + + for (i = 0; i < NR_THREADS; i++) + if (pthread_create(&client_threads[i], NULL, + closed_peer_client_proc, &clients[i])) + return error("could not start closed-peer client"); + for (i = 0; i < NR_THREADS; i++) + if (pthread_join(client_threads[i], NULL)) + return error("could not join closed-peer client"); + + ret = pending_client.errors; + for (i = 0; i < NR_THREADS; i++) { + ret += clients[i].errors; + free(clients[i].connections); + } + return ret ? error("closed-peer clients had %d errors", ret) : 0; +} +#endif + /* * Send an IPC command to an already-running server and ask it to * shutdown. "send quit" is an async request and queues a shutdown @@ -695,9 +986,11 @@ int cmd__simple_ipc(int argc, const char **argv) const char * const simple_ipc_usage[] = { N_("test-helper simple-ipc is-active [] []"), N_("test-helper simple-ipc run-daemon [] []"), + N_("test-helper simple-ipc stop-before-start [] []"), N_("test-helper simple-ipc start-daemon [] [] []"), N_("test-helper simple-ipc stop-daemon [] []"), N_("test-helper simple-ipc send [] []"), + N_("test-helper simple-ipc gentle-write-failure"), N_("test-helper simple-ipc sendbytes [] [] []"), N_("test-helper simple-ipc multiple [] [] [] []"), NULL @@ -727,6 +1020,9 @@ int cmd__simple_ipc(int argc, const char **argv) OPT_BOOL(0, "fsmonitor-pre-cookie-retirement", &fsmonitor_pre_cookie_retirement, N_("emulate a daemon without failed-cookie token retirement")), + OPT_BOOL(0, "fsmonitor-pre-provider-fence", + &fsmonitor_pre_provider_fence, + N_("emulate a Darwin daemon without provider fencing")), OPT_BOOL(0, "fsmonitor-unmarked-response", &fsmonitor_unmarked_response, N_("advertise token retirement but return an unmarked token")), @@ -785,9 +1081,17 @@ int cmd__simple_ipc(int argc, const char **argv) if (!strcmp(cl_args.subcommand, "run-daemon")) return !!daemon__run_server(); + if (!strcmp(cl_args.subcommand, "stop-before-start")) + return !!daemon__stop_before_start(); + if (!strcmp(cl_args.subcommand, "start-daemon")) return !!daemon__start_server(); +#ifndef GIT_WINDOWS_NATIVE + if (!strcmp(cl_args.subcommand, "gentle-write-failure")) + return !!client__gentle_write_failure(); +#endif + /* * Client commands follow. Ensure a server is running before * sending any data. This might be overkill, but then again diff --git a/t/lib-semantic-verify.sh b/t/lib-semantic-verify.sh index b46fd064711daf..710eb56747a393 100644 --- a/t/lib-semantic-verify.sh +++ b/t/lib-semantic-verify.sh @@ -7,3 +7,18 @@ test_lazy_prereq SEMANTIC_VERIFY_ANCHORED_OPEN ' test_grep "^tracked raw-clean " actual ) ' + +test_lazy_prereq CLEAN_STATUS_SIDECAR ' + test_have_prereq MACOS && + /bin/df -l -t apfs "$TRASH_DIRECTORY" >/dev/null +' + +test_remove_clean_status_sidecar () { + if test_have_prereq CLEAN_STATUS_SIDECAR + then + test_path_is_file "$1" && + rm "$1" + else + test_path_is_missing "$1" + fi +} diff --git a/t/t0052-simple-ipc.sh b/t/t0052-simple-ipc.sh index 14cea84920271a..d6a3470e637503 100755 --- a/t/t0052-simple-ipc.sh +++ b/t/t0052-simple-ipc.sh @@ -13,6 +13,17 @@ stop_simple_IPC_server () { test-tool simple-ipc stop-daemon } +test_expect_success !WINDOWS \ + 'pre-start IPC shutdown is one-shot and prevents late start' ' + test-tool simple-ipc stop-before-start --threads=2 && + test_must_fail test-tool simple-ipc is-active +' + +test_expect_success !WINDOWS 'gentle write survives a closed peer' ' + test-tool simple-ipc gentle-write-failure 2>actual && + test_must_be_empty actual +' + test_expect_success 'start simple command server' ' test_atexit stop_simple_IPC_server && test-tool simple-ipc start-daemon --threads=8 && diff --git a/t/t0070-fundamental.sh b/t/t0070-fundamental.sh index 8f573c2a0e7f11..3878a413ed3c64 100755 --- a/t/t0070-fundamental.sh +++ b/t/t0070-fundamental.sh @@ -21,6 +21,11 @@ test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints file test_grep "cannotwrite/test" err ' +test_expect_success POSIXPERM,SANITY \ + 'reopen tempfile retains its write-only contract' ' + test-tool mktemp --reopen-write-only write-only-XXXXXX +' + test_expect_success 'git_mkstemps_mode does not fail if fd 0 is not open' ' git commit --allow-empty -m message <&- ' diff --git a/t/t1602-index-witness.sh b/t/t1602-index-witness.sh index 88c8793bcf035f..2a13a5e1be2f80 100755 --- a/t/t1602-index-witness.sh +++ b/t/t1602-index-witness.sh @@ -381,7 +381,7 @@ test_index_witness_full_proof () { if $token ne $expected_token; } else { die "not a real builtin token in $path\n" - if $token !~ /^builtin:dirmeta-v1\.inode-v1\./; + if $token !~ /^builtin:dirmeta-v1\.inode-v1\.cookie-v1\.fence-v1\./; } for my $name (qw(FSMN FSUC)) { my $body = $ext{$name} // die "missing $name in $path\n"; @@ -615,7 +615,7 @@ test_lazy_prereq INDEX_WITNESS_SCRIPTED_IPC ' # before starting it, and keep all later fixture writes inside .git. if test_have_prereq MACOS then - index_witness_scripted_token=builtin:dirmeta-v1.inode-v1.cookie-v1.test-capable:0 + index_witness_scripted_token=builtin:dirmeta-v1.inode-v1.cookie-v1.fence-v1.test-capable:0 else index_witness_scripted_token=builtin:cookie-v1.test-capable:0 fi diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh index cb5c3a1cb5bc6f..d6bfd73b9a2905 100755 --- a/t/t3418-rebase-continue.sh +++ b/t/t3418-rebase-continue.sh @@ -35,10 +35,25 @@ test_expect_success 'merge based rebase --continue removes .git/MERGE_MSG' ' git checkout -f --detach topic && test_must_fail git rebase --onto main HEAD^ && + test_cmp_rev REBASE_HEAD topic && git read-tree --reset -u HEAD && test_path_is_file .git/MERGE_MSG && git rebase --continue && - test_path_is_missing .git/MERGE_MSG + test_path_is_missing .git/MERGE_MSG && + test_must_fail git rev-parse --verify REBASE_HEAD +' + +test_expect_success REFFILES 'merge based rebase --continue reports REBASE_HEAD cleanup failure' ' + git checkout -f --detach topic && + + test_must_fail git rebase --onto main HEAD^ && + git read-tree --reset -u HEAD && + test_when_finished "rm -f .git/REBASE_HEAD .git/REBASE_HEAD.lock" && + >.git/REBASE_HEAD.lock && + test_must_fail git rebase --continue 2>err && + test_grep "cannot lock ref.*REBASE_HEAD" err && + test_path_is_missing .git/rebase-merge && + test_path_is_file .git/REBASE_HEAD ' test_expect_success 'apply based rebase --continue works with touched file' ' diff --git a/t/t7519-status-fsmonitor.sh b/t/t7519-status-fsmonitor.sh index 9454c11695077f..e83babdc2d20a5 100755 --- a/t/t7519-status-fsmonitor.sh +++ b/t/t7519-status-fsmonitor.sh @@ -793,6 +793,16 @@ test_fsmonitor_full_proof () { EOF } +test_fsmonitor_clean_bitmap () { + bitmap=$(test-tool -C "$1" dump-fsmonitor | tail -n 1) && + case "$bitmap" in + ""|*[!-]*) + echo "dirty fsmonitor bitmap: $bitmap" >&2 && + return 1 + ;; + esac +} + wait_for_fsmonitor_query_barrier () { for attempt in $(test_seq 1 500) do @@ -1377,7 +1387,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELP ' test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'index writers report missing authenticated untracked proofs' ' + 'index writers preserve authenticated untracked proofs' ' test_when_finished "rm -rf missing-untracked-proof" && test_create_repo missing-untracked-proof && ( @@ -1407,19 +1417,24 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TRACE2_EVENT="$PWD/.git/reset.trace" \ git reset --hard HEAD >.git/reset.out && test_region index do_write_index .git/reset.trace && - test_trace2_data fsmonitor untracked/proof-missing 1 \ + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ <.git/reset.trace && test_grep FSMN .git/index && test_grep UNTR .git/index && - test_grep ! FSUC .git/index && + test_grep FSUC .git/index && + test_fsmonitor_full_proof .git/index paired && + cp .git/index .git/reset.index && + GIT_OPTIONAL_LOCKS=0 \ GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ - GIT_TRACE2_EVENT="$PWD/.git/repair.trace" \ - git status --porcelain=v2 >.git/repair && - test_must_be_empty .git/repair && - test_grep FSMN .git/index && - test_grep FSUC .git/index && + GIT_TRACE2_EVENT="$PWD/.git/reset-status.trace" \ + git status --porcelain=v2 >.git/reset-status && + test_must_be_empty .git/reset-status && + test_cmp_bin .git/reset.index .git/index && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/reset-status.trace && + ! test_region index do_write_index .git/reset-status.trace && cp .git/index .git/alternate.index && GIT_INDEX_FILE="$PWD/.git/alternate.index" \ @@ -1443,11 +1458,12 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TRACE2_EVENT="$PWD/.git/stash.trace" \ git stash push -m proof-missing >.git/stash.out && test_region index do_write_index .git/stash.trace && - test_trace2_data fsmonitor untracked/proof-missing 1 \ + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ <.git/stash.trace && test_grep FSMN .git/index && test_grep UNTR .git/index && - test_grep ! FSUC .git/index + test_grep FSUC .git/index && + test_fsmonitor_full_proof .git/index paired ) ' @@ -1630,6 +1646,8 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELP test_must_be_empty "$gitdir/$mode.checkpoint" && test_trace2_data fsmonitor history/external-stored 1 \ <"$gitdir/$mode.checkpoint.trace" && + test_remove_clean_status_sidecar \ + "$gitdir/index.csts" && find "$gitdir" -maxdepth 1 -type f \ -name "index.csh1.*" >"$gitdir/$mode.csh" && test_line_count = 1 "$gitdir/$mode.csh" && @@ -1806,7 +1824,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELP extension/fsmn/read/token builtin:test:3 \ <"$gitdir/$mode.checkout.trace" && test_trace2_data index \ - extension/fsmn/read/token builtin:test:1 \ + extension/fsmn/read/token builtin:test:2 \ <"$gitdir/$mode.checkout.trace" && test_region ! index do_write_index \ "$gitdir/$mode.checkout.trace" && @@ -2518,6 +2536,137 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'clean non-fast-forward merges preserve authenticated worktree proofs' ' + test_when_finished "rm -rf clean-no-ff-proof-*" && + for mode in cli no-commit config + do + repo=clean-no-ff-proof-$mode && + test_create_repo "$repo" && + ( + cd "$repo" && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base base && + primary=$(git symbolic-ref --short HEAD) && + git switch -c side && + test_commit topic topic && + git switch "$primary" && + test_commit primary primary && + git config core.untrackedCache true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_fsmonitor_full_proof .git/index paired && + case "$mode" in + cli) + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCCCCCC \ + git merge --no-ff --no-edit side + ;; + no-commit) + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCCCCCC \ + git merge --no-ff --no-commit side + ;; + config) + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCCCCCC \ + git -c merge.ff=false merge --no-edit side + ;; + esac && + test_fsmonitor_full_proof .git/index paired && + cp .git/index .git/readonly.index && + for run in 1 2 3 + do + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/status-$run.trace" \ + git status --porcelain=v2 \ + >.git/status-$run && + test_cmp_bin .git/readonly.index .git/index && + ! test_trace2_data fsmonitor \ + history/external-proof-invalidated 1 \ + <.git/status-$run.trace && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-count \ + <.git/status-$run.trace && + if test "$mode" = no-commit + then + test_grep "^1 A\\. .* topic$" \ + .git/status-$run + else + test_must_be_empty .git/status-$run + fi || return 1 + done + ) || return 1 + done +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'non-fast-forward conflicts and alternate indexes fail closed' ' + test_when_finished "rm -rf no-ff-alt-proof no-ff-conflict-proof" && + test_create_repo no-ff-alt-proof && + ( + cd no-ff-alt-proof && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base base && + primary=$(git symbolic-ref --short HEAD) && + git switch -c side && + test_commit topic topic && + git switch "$primary" && + test_commit primary primary && + git config core.untrackedCache true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_fsmonitor_full_proof .git/index paired && + cp .git/index .git/alternate.index && + GIT_INDEX_FILE="$PWD/.git/alternate.index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCCCCCC \ + git merge --no-ff --no-commit side && + ! test_fsmonitor_full_proof .git/alternate.index paired \ + 2>.git/alternate.proof && + test_grep ! FSUC .git/alternate.index + ) && + test_create_repo no-ff-conflict-proof && + ( + cd no-ff-conflict-proof && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines base >tracked && + git add tracked && + git commit -m base && + primary=$(git symbolic-ref --short HEAD) && + git switch -c side && + test_write_lines side >tracked && + git commit -am side && + git switch "$primary" && + test_write_lines primary >tracked && + git commit -am primary && + git config core.untrackedCache true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_fsmonitor_full_proof .git/index paired && + test_must_fail env \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCCCCCC \ + git merge --no-ff side && + ! test_fsmonitor_full_proof .git/index paired \ + 2>.git/conflict.proof && + test_grep ! FSUC .git/index && + git ls-files -u >.git/unmerged && + test_file_not_empty .git/unmerged + ) +' + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'full status durably repairs missing mixed-writer index proofs' ' test_when_finished "rm -rf mixed-writer-missing-proofs" && @@ -2603,6 +2752,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ $tokens{"FSMN"} eq $tokens{"FSCF"}; EOF perl .git/check-mixed-writer-proof.pl <.git/index && + rm -f .git/index.csts && for run in first second do cp .git/index ".git/readonly-$run.index" && @@ -2744,7 +2894,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'configured pulls preserve authenticated worktree proofs' ' - 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" && + 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-delete pull-proof-delete-linked pull-proof-filter" && git init --bare pull-proof-origin.git && test_create_repo pull-proof-seed && ( @@ -2801,8 +2951,14 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ for role in main linked do case "$role" in - main) worktree="$repo" ;; - linked) worktree="$linked" ;; + main) + worktree="$repo" && + invalidated=98 + ;; + linked) + worktree="$linked" && + invalidated=196 + ;; esac && if test "$mode" != ff then @@ -2825,9 +2981,25 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_must_be_empty "$gitdir/prime" && perl "$PWD/.git/check-pull-proof.pl" \ <"$gitdir/index" && - test_write_lines "$mode-$role" \ - >"upstream-$mode-$role" && - git add "upstream-$mode-$role" && + if test "$mode" = ff + then + mkdir -p "upstream-$mode-$role/nested" && + for file in $(test_seq 1 96) + do + test_write_lines "$mode-$role-$file" \ + >"upstream-$mode-$role/nested/$file" || + return 1 + done && + test_write_lines "# $mode-$role" \ + >"upstream-$mode-$role/nested/.gitattributes" && + test_write_lines "*.ignored" "# $mode-$role" \ + >"upstream-$mode-$role/nested/.gitignore" && + git add "upstream-$mode-$role" + else + test_write_lines "$mode-$role" \ + >"upstream-$mode-$role" && + git add "upstream-$mode-$role" + fi && git commit -qm "upstream-$mode-$role" && git push --quiet origin main && if test "$mode" = autostash @@ -2840,7 +3012,13 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCCCCCC \ GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ GIT_TRACE2_EVENT="$gitdir/pull.trace" \ - git -C "$worktree" "$@" \ + git -C "$worktree" \ + -c protocol.version=2 \ + -c fetch.uriprotocols=https \ + -c http.https://example.invalid.extraHeader=header \ + -c http.https://example.invalid.proactiveAuth=basic \ + -c http.https://example.invalid.sslVerify=true \ + "$@" \ >"$gitdir/pull" && if test "$mode" != ff then @@ -2853,37 +3031,840 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ <"$gitdir/pull.trace" && ! test_trace2_data fsmonitor untracked/proof-missing 1 \ <"$gitdir/pull.trace" && - ! test_trace2_data fsmonitor \ - semantic/manifest-scan-count 1 \ - <"$gitdir/pull.trace" && + if test "$mode" = ff + then + test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <"$gitdir/pull.trace" && + test_trace2_data fsmonitor \ + history/checkout-manifest-refreshed 1 \ + <"$gitdir/pull.trace" && + test_trace2_data fsmonitor \ + history/untracked-paired-new-directory-invalidated \ + "$invalidated" <"$gitdir/pull.trace" + else + ! test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <"$gitdir/pull.trace" + fi && perl "$PWD/.git/check-pull-proof.pl" \ <"$gitdir/index" && - cp "$gitdir/index" "$gitdir/readonly.index" && + for pass in first second + do + cp "$gitdir/index" \ + "$gitdir/readonly-$pass.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + GIT_TRACE2_EVENT="$gitdir/status-$pass.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/status-$pass" && + if test "$mode" = autostash + then + test_grep "^1 \\.M .* tracked$" \ + "$gitdir/status-$pass" || return 1 + else + test_must_be_empty \ + "$gitdir/status-$pass" || return 1 + fi && + test_cmp_bin "$gitdir/readonly-$pass.index" \ + "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/status-$pass.trace" && + ! test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <"$gitdir/status-$pass.trace" && + ! test_region index do_write_index \ + "$gitdir/status-$pass.trace" || return 1 + done || return 1 + done || return 1 + done && + git clone --quiet "$PWD/../pull-proof-origin.git" \ + "$PWD/../pull-proof-delete" && + delete_repo="$PWD/../pull-proof-delete" && + delete_linked="$PWD/../pull-proof-delete-linked" && + git -C "$delete_repo" worktree add --quiet -b linked-delete \ + "$delete_linked" origin/main && + git -C "$delete_linked" branch --quiet \ + --set-upstream-to=origin/main && + git -C "$delete_repo" config pull.ff only && + git -C "$delete_repo" config core.untrackedCache true && + git -C "$delete_repo" config core.fsmonitor true && + for worktree in "$delete_repo" "$delete_linked" + do + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git -C "$worktree" update-index --fsmonitor && + GIT_INDEX_FILE="$gitdir/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/prime" && + test_must_be_empty "$gitdir/prime" && + perl "$PWD/.git/check-pull-proof.pl" \ + <"$gitdir/index" || return 1 + done && + test_write_lines "# root attributes" >.gitattributes && + test_write_lines "# root ignore" >.gitignore && + git rm --quiet \ + upstream-ff-main/nested/.gitattributes \ + upstream-ff-main/nested/.gitignore && + git add .gitattributes .gitignore && + git commit -qm "change policy sources" && + git push --quiet origin main && + for worktree in "$delete_repo" "$delete_linked" + do + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + GIT_TRACE2_EVENT="$gitdir/pull.trace" \ + git -C "$worktree" pull --quiet && + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ + <"$gitdir/pull.trace" && + perl "$PWD/.git/check-pull-proof.pl" \ + <"$gitdir/index" && + for pass in first second + do + cp "$gitdir/index" \ + "$gitdir/delete-$pass.index" && GIT_OPTIONAL_LOCKS=0 \ - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ - GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ - GIT_TRACE2_EVENT="$gitdir/status.trace" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$gitdir/delete-$pass.trace" \ git -C "$worktree" status --porcelain=v2 \ - >"$gitdir/status" && - if test "$mode" = autostash - then - test_grep "^1 \\.M .* tracked$" \ - "$gitdir/status" || return 1 - else - test_must_be_empty "$gitdir/status" || return 1 - fi && - test_cmp_bin "$gitdir/readonly.index" \ + >"$gitdir/delete-$pass" && + test_must_be_empty "$gitdir/delete-$pass" && + test_cmp_bin "$gitdir/delete-$pass.index" \ "$gitdir/index" && test_trace2_data fsmonitor config/coherent 1 \ - <"$gitdir/status.trace" && + <"$gitdir/delete-$pass.trace" && ! test_trace2_data fsmonitor \ semantic/manifest-scan-count 1 \ - <"$gitdir/status.trace" || return 1 + <"$gitdir/delete-$pass.trace" && + ! test_region index do_write_index \ + "$gitdir/delete-$pass.trace" || return 1 done || return 1 + done && + git clone --quiet "$PWD/../pull-proof-origin.git" \ + "$PWD/../pull-proof-filter" && + filter="$PWD/../pull-proof-filter" && + git -C "$filter" config pull.ff only && + git -C "$filter" config core.untrackedCache true && + git -C "$filter" config core.fsmonitor true && + git -C "$filter" config filter.pullproof.clean false && + git -C "$filter" config filter.pullproof.required true && + filter_gitdir=$(git -C "$filter" rev-parse --absolute-git-dir) && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git -C "$filter" update-index --fsmonitor && + GIT_INDEX_FILE="$filter_gitdir/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git -C "$filter" status --porcelain=v2 \ + >"$filter_gitdir/prime" && + test_must_be_empty "$filter_gitdir/prime" && + test_write_lines "tracked filter=pullproof" >.gitattributes && + git add .gitattributes && + git commit -qm "upstream-filter" && + git push --quiet origin main && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + GIT_TRACE2_EVENT="$filter_gitdir/pull.trace" \ + git -C "$filter" pull --quiet && + test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <"$filter_gitdir/pull.trace" && + test_trace2_data fsmonitor history/checkout-manifest-refreshed 1 \ + <"$filter_gitdir/pull.trace" && + test_trace2_data fsmonitor semantic/manifest-invalidated 1 \ + <"$filter_gitdir/pull.trace" && + test_trace2_data fsmonitor history/untracked-paired-transfer 1 \ + <"$filter_gitdir/pull.trace" && + cp "$filter_gitdir/index" "$filter_gitdir/status.before" && + test_must_fail env GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$filter_gitdir/status.trace" \ + git -C "$filter" status --porcelain=v2 \ + --untracked-files=no -- tracked \ + >"$filter_gitdir/status" \ + 2>"$filter_gitdir/status.err" && + test_grep "clean filter .pullproof. failed" \ + "$filter_gitdir/status.err" && + test_cmp_bin "$filter_gitdir/status.before" \ + "$filter_gitdir/index" + ) +' + +test_expect_success FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'provider restarts preserve authenticated pull proofs' ' + test_when_finished "rm -rf daemon-pull-origin.git daemon-pull-seed daemon-pull-fast daemon-pull daemon-pull-linked daemon-pull-root daemon-pull-root-linked daemon-pull-filter" && + test_when_finished \ + "git -C daemon-pull-fast fsmonitor--daemon stop 2>/dev/null || :" && + test_when_finished \ + "git -C daemon-pull fsmonitor--daemon stop 2>/dev/null || :" && + test_when_finished \ + "git -C daemon-pull-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_when_finished \ + "git -C daemon-pull-root fsmonitor--daemon stop 2>/dev/null || :" && + test_when_finished \ + "git -C daemon-pull-root-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_when_finished \ + "git -C daemon-pull-filter fsmonitor--daemon stop 2>/dev/null || :" && + git init --bare daemon-pull-origin.git && + test_create_repo daemon-pull-seed && + ( + cd daemon-pull-seed && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir stable && + mkdir -p policy-main/nested policy-linked/nested && + for file in $(test_seq 1 128) + do + test_write_lines "stable-$file" >"stable/$file" || + return 1 + done && + test_write_lines "# base main" \ + >policy-main/nested/.gitattributes && + test_write_lines "# base main" \ + >policy-main/nested/.gitignore && + test_write_lines "# base linked" \ + >policy-linked/nested/.gitattributes && + test_write_lines "# base linked" \ + >policy-linked/nested/.gitignore && + git add stable policy-main policy-linked && + test_commit base tracked && + git branch -M main && + git remote add origin "$PWD/../daemon-pull-origin.git" && + git push --quiet -u origin main && + git --git-dir="$PWD/../daemon-pull-origin.git" \ + symbolic-ref HEAD refs/heads/main && + git clone --quiet "$PWD/../daemon-pull-origin.git" \ + "$PWD/../daemon-pull-fast" && + fast="$PWD/../daemon-pull-fast" && + fast_gitdir=$(git -C "$fast" rev-parse --absolute-git-dir) && + git -C "$fast" config pull.ff only && + git -C "$fast" config core.untrackedCache true && + git -C "$fast" config core.fsmonitor true && + git -C "$fast" config core.preloadIndexBulk true && + git -C "$fast" fsmonitor--daemon start --start-timeout=10 && + git -C "$fast" update-index --fsmonitor && + GIT_INDEX_FILE="$fast_gitdir/index" \ + git -C "$fast" status --porcelain=v2 \ + >"$fast_gitdir/prime" && + test_must_be_empty "$fast_gitdir/prime" && + test_write_lines normal >normal-fast-path && + git add normal-fast-path && + git commit -qm "normal fast path" && + git push --quiet origin main && + GIT_TRACE2_EVENT="$fast_gitdir/pull.trace" \ + git -C "$fast" pull --quiet && + test_trace2_data fsmonitor history/post-worktree-refresh 1 \ + <"$fast_gitdir/pull.trace" && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$fast_gitdir/pull.trace" && + test_trace2_data index refresh/sum_lstat 1 \ + <"$fast_gitdir/pull.trace" && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <"$fast_gitdir/pull.trace" && + test_fsmonitor_full_proof "$fast_gitdir/index" paired && + git -C "$fast" fsmonitor--daemon stop && + git clone --quiet "$PWD/../daemon-pull-origin.git" \ + "$PWD/../daemon-pull" && + repo="$PWD/../daemon-pull" && + linked="$PWD/../daemon-pull-linked" && + git -C "$repo" worktree add --quiet -b daemon-linked \ + "$linked" origin/main && + git -C "$linked" branch --quiet \ + --set-upstream-to=origin/main && + git -C "$repo" config pull.ff only && + git -C "$repo" config core.untrackedCache true && + git -C "$repo" config core.fsmonitor true && + git -C "$repo" config core.preloadIndexBulk true && + write_script "$repo/.git/hooks/post-index-change" <<-\EOF && + gitdir=$(git rev-parse --absolute-git-dir) || exit 1 + test ! -f "$gitdir/index.lock" || exit 1 + test -f "$gitdir/index" || exit 1 + printf "%s %s\n" "$1" "$2" >>"$gitdir/post-index-change.log" + EOF + for role in main linked + do + if test "$role" = main + then + worktree="$repo" && + affected=98 && + total=230 + else + worktree="$linked" && + affected=196 && + total=326 + fi && + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + git -C "$worktree" fsmonitor--daemon start \ + --start-timeout=10 && + git -C "$worktree" update-index --fsmonitor && + GIT_INDEX_FILE="$gitdir/index" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/prime" && + test_must_be_empty "$gitdir/prime" && + test_fsmonitor_full_proof "$gitdir/index" paired && + git -C "$worktree" fsmonitor--daemon stop && + git -C "$worktree" fsmonitor--daemon start \ + --start-timeout=10 && + rm -f "$gitdir/post-index-change.log" && + mkdir -p "policy-$role/nested/new" && + for file in $(test_seq 1 96) + do + test_write_lines "$role-$file" \ + >"policy-$role/nested/new/$file" || return 1 + done && + if test "$role" = main + then + test_write_lines "# changed main" \ + >policy-main/nested/.gitattributes && + test_write_lines "*.ignored" "# changed main" \ + >policy-main/nested/.gitignore + else + test_write_lines "* text" \ + >policy-linked/nested/.gitattributes && + test_write_lines "*.ignored" "!keep.ignored" \ + >policy-linked/nested/.gitignore + fi && + git add "policy-$role" && + git commit -qm "upstream-$role" && + git push --quiet origin main && + GIT_TRACE2_EVENT="$gitdir/pull.trace" \ + git -C "$worktree" \ + -c protocol.version=2 \ + -c fetch.uriprotocols=https \ + -c http.https://example.invalid.extraHeader=header \ + -c http.https://example.invalid.proactiveAuth=basic \ + -c http.https://example.invalid.sslVerify=true \ + pull --quiet >"$gitdir/pull" && + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ + <"$gitdir/pull.trace" && + test_trace2_data index refresh/sum_lstat "$affected" \ + <"$gitdir/pull.trace" && + ! test_trace2_data index refresh/sum_lstat "$total" \ + <"$gitdir/pull.trace" && + test_write_lines "1 0" \ + >"$gitdir/post-index-change.expect" && + test_cmp "$gitdir/post-index-change.expect" \ + "$gitdir/post-index-change.log" && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/pull.trace" && + ! test_trace2_data fsmonitor history/writer-proof-repaired 0 \ + <"$gitdir/pull.trace" && + test_fsmonitor_full_proof "$gitdir/index" paired && + for pass in first second + do + cp "$gitdir/index" "$gitdir/readonly-$pass.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$gitdir/readonly-$pass.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/readonly-$pass" && + test_must_be_empty "$gitdir/readonly-$pass" && + test_cmp_bin "$gitdir/readonly-$pass.index" \ + "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/readonly-$pass.trace" && + ! test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <"$gitdir/readonly-$pass.trace" && + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ + <"$gitdir/readonly-$pass.trace" && + ! test_trace2_data read_directory \ + directories-visited "[1-9][0-9]*" \ + <"$gitdir/readonly-$pass.trace" && + ! test_trace2_data read_directory paths-visited \ + "[1-9][0-9]*" \ + <"$gitdir/readonly-$pass.trace" && + ! test_trace2_data read_directory opendir \ + "[1-9][0-9]*" \ + <"$gitdir/readonly-$pass.trace" && + ! test_trace2_data index preload/sum_lstat \ + "[1-9][0-9]*" \ + <"$gitdir/readonly-$pass.trace" && + ! test_region index do_write_index \ + "$gitdir/readonly-$pass.trace" || return 1 + done && + git -C "$worktree" fsmonitor--daemon stop || return 1 + done && + mkdir -p root-tree && + for file in $(test_seq 1 96) + do + mkdir -p "root-tree/$file/nested" && + test_write_lines "root-$file" \ + >"root-tree/$file/nested/tracked.txt" || return 1 + done && + test_write_lines "*.ignored" "# root ignore base" \ + >.gitignore && + test_write_lines "*.txt text" "# root attributes base" \ + >.gitattributes && + git add root-tree .gitignore .gitattributes && + git commit -qm "root policy base" && + git push --quiet origin main && + git clone --quiet "$PWD/../daemon-pull-origin.git" \ + "$PWD/../daemon-pull-root" && + root_repo="$PWD/../daemon-pull-root" && + root_linked="$PWD/../daemon-pull-root-linked" && + git -C "$root_repo" worktree add --quiet -b daemon-root-linked \ + "$root_linked" origin/main && + git -C "$root_linked" branch --quiet \ + --set-upstream-to=origin/main && + git -C "$root_repo" config pull.ff only && + git -C "$root_repo" config core.untrackedCache true && + git -C "$root_repo" config core.fsmonitor true && + git -C "$root_repo" config core.preloadIndexBulk true && + git -C "$root_repo" config filter.lfs.clean \ + "git-lfs clean -- %f" && + git -C "$root_repo" config filter.lfs.smudge \ + "git-lfs smudge -- %f" && + git -C "$root_repo" config filter.lfs.process \ + "git-lfs filter-process" && + git -C "$root_repo" config filter.lfs.required true && + for worktree in "$root_repo" "$root_linked" + do + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + git -C "$worktree" fsmonitor--daemon start \ + --start-timeout=10 && + git -C "$worktree" update-index --fsmonitor && + GIT_INDEX_FILE="$gitdir/index" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/root-prime" && + test_must_be_empty "$gitdir/root-prime" && + test_fsmonitor_full_proof "$gitdir/index" paired || return 1 + done && + for role in main linked + do + if test "$role" = main + then + worktree="$root_repo" && + policy_dir=1 + else + worktree="$root_linked" && + policy_dir=2 + fi && + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + test_write_lines "*.ignored" \ + "# root ignore $role" >.gitignore && + test_write_lines "*.txt text" \ + "# root attributes $role" >.gitattributes && + test_write_lines "*.ignored" \ + "# nested ignore $role" \ + >"root-tree/$policy_dir/nested/.gitignore" && + test_write_lines "*.txt text" \ + "# nested attributes $role" \ + >"root-tree/$policy_dir/nested/.gitattributes" && + git add .gitignore .gitattributes \ + "root-tree/$policy_dir/nested/.gitignore" \ + "root-tree/$policy_dir/nested/.gitattributes" && + git commit -qm "root policy $role" && + git push --quiet origin main && + GIT_TRACE2_EVENT="$gitdir/root-pull.trace" \ + git -C "$worktree" pull --quiet && + test_trace2_data fsmonitor \ + checkout/untracked-policy-targeted 1 \ + <"$gitdir/root-pull.trace" && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/root-pull.trace" && + test_fsmonitor_full_proof "$gitdir/index" paired && + git --no-optional-locks -C "$worktree" \ + -c core.fsmonitor=false ls-files --debug -- \ + .gitattributes .gitignore \ + "root-tree/$policy_dir/nested/.gitattributes" \ + "root-tree/$policy_dir/nested/.gitignore" \ + >"$gitdir/root-policy-stat" && + test_grep ! "ctime: 0:0" "$gitdir/root-policy-stat" && + test_grep ! "mtime: 0:0" "$gitdir/root-policy-stat" && + test_grep ! "size: 0" "$gitdir/root-policy-stat" && + sleep 2 && + GIT_TRACE2_EVENT="$gitdir/root-delayed.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/root-delayed" && + test_must_be_empty "$gitdir/root-delayed" && + test_fsmonitor_full_proof "$gitdir/index" paired && + test_fsmonitor_clean_bitmap "$worktree" && + for pass in first second + do + cp "$gitdir/index" "$gitdir/root-$pass.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$gitdir/root-$pass.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/root-$pass" && + test_must_be_empty "$gitdir/root-$pass" && + test_cmp_bin "$gitdir/root-$pass.index" \ + "$gitdir/index" && + ! test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <"$gitdir/root-$pass.trace" && + ! test_trace2_data read_directory directories-visited \ + "[1-9][0-9]*" <"$gitdir/root-$pass.trace" && + ! test_trace2_data read_directory opendir \ + "[1-9][0-9]*" <"$gitdir/root-$pass.trace" && + ! test_trace2_data index preload/sum_lstat \ + "[1-9][0-9]*" <"$gitdir/root-$pass.trace" && + ! test_trace2_data index refresh/sum_lstat \ + "[1-9][0-9]*" <"$gitdir/root-$pass.trace" && + ! test_region index do_write_index \ + "$gitdir/root-$pass.trace" || return 1 + done && + git -C "$worktree" fsmonitor--daemon stop || return 1 + done && + git clone --quiet "$PWD/../daemon-pull-origin.git" \ + "$PWD/../daemon-pull-filter" && + filter="$PWD/../daemon-pull-filter" && + filter_gitdir=$(git -C "$filter" rev-parse --absolute-git-dir) && + git -C "$filter" config pull.ff only && + git -C "$filter" config core.untrackedCache true && + git -C "$filter" config core.fsmonitor true && + git -C "$filter" config core.preloadIndexBulk true && + git -C "$filter" config filter.daemonpull.clean false && + git -C "$filter" config filter.daemonpull.required true && + git -C "$filter" fsmonitor--daemon start --start-timeout=10 && + git -C "$filter" update-index --fsmonitor && + GIT_INDEX_FILE="$filter_gitdir/index" \ + git -C "$filter" status --porcelain=v2 \ + >"$filter_gitdir/prime" && + test_must_be_empty "$filter_gitdir/prime" && + git -C "$filter" fsmonitor--daemon stop && + git -C "$filter" fsmonitor--daemon start --start-timeout=10 && + test_write_lines "tracked filter=daemonpull" >.gitattributes && + git add .gitattributes && + git commit -qm "require unavailable filter" && + git push --quiet origin main && + GIT_TRACE2_EVENT="$filter_gitdir/pull.trace" \ + git -C "$filter" pull --quiet && + test_trace2_data fsmonitor semantic/manifest-invalidated 1 \ + <"$filter_gitdir/pull.trace" && + test_trace2_data status \ + semantic_verify/writer-repair-filtered 1 \ + <"$filter_gitdir/pull.trace" && + ! test_fsmonitor_full_proof "$filter_gitdir/index" paired \ + 2>"$filter_gitdir/proof.err" && + test_grep "missing FSUC extension" \ + "$filter_gitdir/proof.err" && + cp "$filter_gitdir/index" "$filter_gitdir/status.before" && + test_must_fail env GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$filter_gitdir/status.trace" \ + git -C "$filter" status --porcelain=v2 \ + --untracked-files=no -- tracked \ + >"$filter_gitdir/status" \ + 2>"$filter_gitdir/status.err" && + test_grep "clean filter .daemonpull. failed" \ + "$filter_gitdir/status.err" && + test_cmp_bin "$filter_gitdir/status.before" \ + "$filter_gitdir/index" && + git -C "$filter" fsmonitor--daemon stop + ) +' + +test_expect_success FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'index writers preserve authenticated policy and intent-to-add proofs' ' + test_when_finished "rm -rf writer-proof writer-proof-linked" && + test_when_finished \ + "git -C writer-proof fsmonitor--daemon stop 2>/dev/null || :" && + test_when_finished \ + "git -C writer-proof-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo writer-proof && + ( + cd writer-proof && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir -p policy/nested && + test_write_lines "*.txt text" "# root base" \ + >.gitattributes && + test_write_lines "*.ignored" "# root base" >.gitignore && + test_write_lines "*.txt text" "# nested base" \ + >policy/nested/.gitattributes && + test_write_lines "*.ignored" "# nested base" \ + >policy/nested/.gitignore && + test_write_lines tracked >tracked.txt && + git add . && + git commit -qm base && + git worktree add --quiet --detach ../writer-proof-linked HEAD && + git config core.untrackedCache true && + git config core.fsmonitor true && + git config core.preloadIndexBulk true && + git config filter.lfs.clean "git-lfs clean -- %f" && + git config filter.lfs.smudge "git-lfs smudge -- %f" && + git config filter.lfs.process "git-lfs filter-process" && + git config filter.lfs.required true && + for role in main linked + do + if test "$role" = main + then + worktree=$PWD + else + worktree=$PWD/../writer-proof-linked + fi && + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + commondir=$(git -C "$worktree" \ + rev-parse --path-format=absolute --git-common-dir) && + git -C "$worktree" fsmonitor--daemon start \ + --start-timeout=10 && + git -C "$worktree" update-index --fsmonitor && + GIT_INDEX_FILE="$gitdir/index" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/prime" && + test_must_be_empty "$gitdir/prime" && + test_fsmonitor_full_proof "$gitdir/index" paired && + test_fsmonitor_clean_bitmap "$worktree" && + + test_write_lines ordinary >"$worktree/ordinary-$role.txt" && + GIT_TRACE2_EVENT="$gitdir/ordinary-add.trace" \ + git -C "$worktree" add "ordinary-$role.txt" && + ! test_trace2_data fsmonitor history/writer-proof-repaired \ + <"$gitdir/ordinary-add.trace" && + test_fsmonitor_full_proof "$gitdir/index" paired && + test_fsmonitor_clean_bitmap "$worktree" && + git -C "$worktree" commit -qm ordinary && + test_fsmonitor_full_proof "$gitdir/index" paired && + + test_write_lines "*.txt text" "# root staged $role" \ + >"$worktree/.gitattributes" && + test_write_lines "*.ignored" "# nested staged $role" \ + >"$worktree/policy/nested/.gitignore" && + GIT_TRACE2_EVENT="$gitdir/policy-add.trace" \ + git -C "$worktree" add .gitattributes \ + policy/nested/.gitignore && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/policy-add.trace" && + test_fsmonitor_full_proof "$gitdir/index" paired && + test_fsmonitor_clean_bitmap "$worktree" && + git -C "$worktree" commit -qm "staged policy" && + test_fsmonitor_full_proof "$gitdir/index" paired && + + write_script "$commondir/hooks/pre-commit" <<-\EOF && + gitdir=$(git rev-parse --absolute-git-dir) || exit 1 + test -n "${GIT_INDEX_FILE-}" || exit 1 + printf "%s\n" "$GIT_INDEX_FILE" >"$gitdir/hook-index" + git status --porcelain=v2 >/dev/null || exit 1 + EOF + test_write_lines "*.ignored" "# root partial $role" \ + >"$worktree/.gitignore" && + test_write_lines "*.txt text" "# nested partial $role" \ + >"$worktree/policy/nested/.gitattributes" && + git -C "$worktree" add .gitignore \ + policy/nested/.gitattributes && + git -C "$worktree" commit --only \ + .gitignore policy/nested/.gitattributes \ + -m "partial policy" >/dev/null && + test_grep "next-index.*\\.lock$" "$gitdir/hook-index" && + test_fsmonitor_full_proof "$gitdir/index" paired && + test_fsmonitor_clean_bitmap "$worktree" && + + test_write_lines intent >"$worktree/intent-$role.txt" && + GIT_TRACE2_EVENT="$gitdir/intent-add.trace" \ + git -C "$worktree" add --intent-to-add \ + "intent-$role.txt" && + ! test_trace2_data fsmonitor history/writer-proof-repaired \ + <"$gitdir/intent-add.trace" && + test_fsmonitor_full_proof "$gitdir/index" paired && + test_fsmonitor_clean_bitmap "$worktree" && + cp "$gitdir/index" "$gitdir/intent.before" && + GIT_OPTIONAL_LOCKS=0 \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/intent" && + test_grep "intent-$role\\.txt$" "$gitdir/intent" && + test_cmp_bin "$gitdir/intent.before" "$gitdir/index" && + git -C "$worktree" rm --cached "intent-$role.txt" \ + >/dev/null && + test_fsmonitor_full_proof "$gitdir/index" paired && + test_fsmonitor_clean_bitmap "$worktree" && + cp "$gitdir/index" "$gitdir/removed.before" && + for pass in first second + do + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$gitdir/removed-$pass.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/removed-$pass" && + test_grep "^? intent-$role\\.txt$" \ + "$gitdir/removed-$pass" && + test_cmp_bin "$gitdir/removed.before" \ + "$gitdir/index" && + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ + <"$gitdir/removed-$pass.trace" && + ! test_region index do_write_index \ + "$gitdir/removed-$pass.trace" || return 1 + done && + test_fsmonitor_full_proof "$gitdir/index" paired && + git -C "$worktree" fsmonitor--daemon stop || return 1 done ) ' +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'no-ff merges repair proof after provider reset' ' + test_when_finished "rm -rf merge-proof-* merge-proof-linked-*" && + for mode in ort ort-resolve-undo multistrategy-decline \ + multistrategy-conflict \ + resolve-trivial resolve-content + do + case "$mode" in + ort) + set -- -s ort && + expect_refreshes=1 && + merge_queries=TCCCCCCCCCCCCCCCCCCCCCCCC + ;; + ort-resolve-undo) + set -- -s ort && + expect_refreshes=1 && + merge_queries=CCCCCCCCCCCCCCCCCCCCCCCC + ;; + multistrategy-decline) + set -- -s decline -s ort && + expect_refreshes=1 && + merge_queries=TCCCCCCCCCCCCCCCCCCCCCCCC + ;; + multistrategy-conflict) + set -- -s pollute -s ort && + expect_refreshes=1 && + merge_queries=TCCCCCCCCCCCCCCCCCCCCCCCC + ;; + resolve-trivial) + set -- -s resolve && + expect_refreshes=1 && + merge_queries=TCCCCCCCCCCCCCCCCCCCCCCCC + ;; + resolve-content) + set -- -s resolve && + expect_refreshes=4 && + merge_queries=TCCCCCCCCCCCCCCCCCCCCCCCC + ;; + esac && + test_create_repo "merge-proof-$mode" && + ( + cd "merge-proof-$mode" && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir -p .qualification && + test_write_lines one two three >tracked && + test_write_lines resolved >resolved && + test_write_lines ready >.qualification/workflow-state && + git -c core.fsmonitor=false add . && + git -c core.fsmonitor=false commit -qm base && + git branch review && + git -c core.fsmonitor=false worktree add --quiet \ + "../merge-proof-linked-$mode" review && + if test "$mode" = resolve-content + then + test_write_lines main two three >tracked && + git -c core.fsmonitor=false add tracked && + git -c core.fsmonitor=false commit -qm main && + test_write_lines one two review \ + >"../merge-proof-linked-$mode/tracked" + fi && + test_write_lines reviewed \ + >"../merge-proof-linked-$mode/.qualification/review-attestation" && + git -C "../merge-proof-linked-$mode" \ + -c core.fsmonitor=false add \ + .qualification/review-attestation tracked && + git -C "../merge-proof-linked-$mode" \ + -c core.fsmonitor=false \ + commit -qm review && + git config feature.manyFiles true && + git config index.version 4 && + git config core.untrackedCache true && + git config core.fsmonitor true && + if test "$mode" = ort-resolve-undo + then + base_oid=$(git rev-parse HEAD:resolved) && + ours_oid=$(printf "%s\n" ours | + git hash-object -w --stdin) && + theirs_oid=$(printf "%s\n" theirs | + git hash-object -w --stdin) && + git update-index --force-remove resolved && + { + printf "100644 %s 1\tresolved\n" "$base_oid" && + printf "100644 %s 2\tresolved\n" "$ours_oid" && + printf "100644 %s 3\tresolved\n" "$theirs_oid" + } | git update-index --index-info && + test_write_lines resolved >resolved && + git add resolved && + git ls-files --resolve-undo >resolve-undo && + test_file_not_empty resolve-undo && + rm resolve-undo + else + : + fi && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + gitdir=$(git rev-parse --absolute-git-dir) && + if test "$mode" = multistrategy-decline + then + mkdir -p "$gitdir/test-bin" && + write_script "$gitdir/test-bin/git-merge-decline" <<-\EOF + exit 2 + EOF + elif test "$mode" = multistrategy-conflict + then + mkdir -p "$gitdir/test-bin" && + write_script "$gitdir/test-bin/git-merge-pollute" <<-\EOF + base_oid=$(git rev-parse HEAD:tracked) + ours_oid=$(printf "%s\n" polluted-ours | git hash-object -w --stdin) + theirs_oid=$(printf "%s\n" polluted-theirs | git hash-object -w --stdin) + git update-index --force-remove tracked + { + printf "100644 %s 1\ttracked\n" "$base_oid" + printf "100644 %s 2\ttracked\n" "$ours_oid" + printf "100644 %s 3\ttracked\n" "$theirs_oid" + } | git update-index --index-info + printf "%s\n" "<<<<<<< ours" polluted-ours ======= \ + polluted-theirs ">>>>>>> theirs" >tracked + exit 1 + EOF + else + : + fi && + for pass in first second third + do + GIT_INDEX_FILE="$gitdir/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >"$gitdir/prime-$pass" && + test_must_be_empty "$gitdir/prime-$pass" || return 1 + done && + test_fsmonitor_full_proof "$gitdir/index" paired && + GIT_TEST_FSMONITOR_ALLOW_PROOF_REPAIR_SEQUENCE=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE="$merge_queries" \ + GIT_TRACE2_EVENT="$gitdir/merge.trace" \ + PATH="$gitdir/test-bin:$PATH" \ + git merge --no-ff "$@" -m merge review && + git ls-files --resolve-undo >"$gitdir/resolve-undo-after" && + test_must_be_empty "$gitdir/resolve-undo-after" && + if test "$mode" != ort-resolve-undo + then + test_trace2_data fsm_client query/trivial-response 1 \ + <"$gitdir/merge.trace" + else + : + fi && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/merge.trace" && + ! test_trace2_data fsmonitor history/writer-proof-repaired 0 \ + <"$gitdir/merge.trace" && + test_trace2_data fsmonitor history/writer-entry-refreshes \ + "$expect_refreshes" <"$gitdir/merge.trace" && + test_fsmonitor_full_proof "$gitdir/index" paired && + cp "$gitdir/index" "$gitdir/merge.before" && + for pass in first second + do + GIT_INDEX_FILE="$gitdir/index" \ + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$gitdir/status-$pass.trace" \ + git status --porcelain=v2 \ + >"$gitdir/status-$pass" && + test_must_be_empty "$gitdir/status-$pass" && + test_cmp_bin "$gitdir/merge.before" "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/status-$pass.trace" && + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ + <"$gitdir/status-$pass.trace" && + ! test_region index do_write_index \ + "$gitdir/status-$pass.trace" || return 1 + done + ) || return 1 + done +' + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'clean sequencer operations preserve authenticated worktree proofs' ' test_when_finished "rm -rf sequencer-proof sequencer-linked" && @@ -2900,6 +3881,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ git worktree add --detach ../sequencer-linked HEAD && git config core.untrackedCache true && git config core.fsmonitor true && + git config core.preloadIndexBulk true && for worktree in "$PWD" "$PWD/../sequencer-linked" do gitdir=$(git -C "$worktree" \ @@ -2930,37 +3912,300 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ GIT_TRACE2_EVENT="$gitdir/$operation-status.trace" \ git -C "$worktree" status --porcelain=v2 \ - >"$gitdir/$operation-status" && - test_must_be_empty "$gitdir/$operation-status" && - test_cmp_bin "$gitdir/$operation.index" \ - "$gitdir/index" && - test_trace2_data fsmonitor config/coherent 1 \ - <"$gitdir/$operation-status.trace" && - ! test_trace2_data fsmonitor \ - semantic/manifest-scan-count 1 \ - <"$gitdir/$operation-status.trace" || return 1 - done || return 1 + >"$gitdir/$operation-status" && + test_must_be_empty "$gitdir/$operation-status" && + test_cmp_bin "$gitdir/$operation.index" \ + "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/$operation-status.trace" && + ! test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <"$gitdir/$operation-status.trace" || return 1 + done || return 1 + done && + test_write_lines conflicting >tracked && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + git add tracked && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git commit -qm local-conflict && + test_must_fail env \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + git cherry-pick --no-edit "$picked" \ + >.git/conflict.out 2>.git/conflict.err && + if test_fsmonitor_full_proof .git/index paired \ + >/dev/null 2>&1 + then + return 1 + fi && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >.git/conflict && + test_grep "^u UU .* tracked$" .git/conflict + ) +' + +test_expect_success FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'routed push and owned writers preserve authenticated clean proofs' ' + test_when_finished "rm -rf daemon-writers daemon-writers-linked" && + test_when_finished \ + "git -C daemon-writers fsmonitor--daemon stop 2>/dev/null || :" && + test_when_finished \ + "git -C daemon-writers-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo daemon-writers && + ( + cd daemon-writers && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir stable && + for file in $(test_seq 1 16) + do + test_write_lines "stable-$file" >"stable/$file" || + return 1 + done && + test_write_lines base >stable/anchor && + git add stable && + git commit -qm base && + base=$(git rev-parse HEAD) && + git checkout -q -b upstream && + for file in $(test_seq 1 16) + do + mkdir -p "incoming/package-$file/nested" && + test_write_lines "incoming-$file" \ + >"incoming/package-$file/nested/tracked" || + return 1 + done && + git add incoming && + git commit -qm upstream && + git checkout -q -b owned-main "$base" && + test_write_lines base topic >stable/anchor && + git add stable/anchor && + git commit -qm topic && + git branch owned-linked && + git worktree add -q ../daemon-writers-linked owned-linked && + repo=$PWD && + linked=$PWD/../daemon-writers-linked && + git config core.untrackedCache true && + git config core.fsmonitor true && + git config filter.lfs.clean "git-lfs clean -- %f" && + git config filter.lfs.smudge "git-lfs smudge -- %f" && + git config filter.lfs.process "git-lfs filter-process" && + git config filter.lfs.required true && + + assert_no_full_worktree_scan () { + trace=$1 && + ! test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 <"$trace" && + ! test_trace2_data read_directory directories-visited \ + "[1-9][0-9]*" <"$trace" && + ! test_trace2_data read_directory opendir \ + "[1-9][0-9]*" <"$trace" && + ! test_trace2_data index preload/bulk_dirs \ + "[1-9][0-9]*" <"$trace" && + ! test_trace2_data index preload/bulk_entries \ + "[1-9][0-9]*" <"$trace" && + ! test_trace2_data index preload/sum_lstat \ + "[1-9][0-9]*" <"$trace" && + ! test_trace2_data index refresh/sum_lstat \ + "[1-9][0-9]*" <"$trace" && + ! test_region index do_write_index "$trace" + } && + + assert_clean_status_fast () { + trace=$1 && + { + test_trace2_data status clean-proof/hit 1 \ + <"$trace" || + test_trace2_data fsmonitor config/coherent 1 \ + <"$trace" + } + } && + + assert_owned_writer_clean () { + worktree=$1 && + label=$2 && + shift 2 && + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + test_fsmonitor_full_proof "$gitdir/index" paired && + git --no-optional-locks -C "$worktree" \ + -c core.fsmonitor=false ls-files --debug -- "$@" \ + >"$gitdir/$label-stat" && + test_grep ! "ctime: 0:0" "$gitdir/$label-stat" && + test_grep ! "mtime: 0:0" "$gitdir/$label-stat" && + test_grep ! "size: 0" "$gitdir/$label-stat" && + for bulk in false true + do + pass=bulk-$bulk && + cp "$gitdir/index" \ + "$gitdir/$label-$pass.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$gitdir/$label-$pass.trace" \ + git -C "$worktree" \ + -c core.preloadIndexBulk=$bulk \ + status --porcelain=v2 \ + >"$gitdir/$label-$pass" && + test_must_be_empty "$gitdir/$label-$pass" && + test_cmp_bin "$gitdir/$label-$pass.index" \ + "$gitdir/index" && + assert_clean_status_fast \ + "$gitdir/$label-$pass.trace" && + ! test_trace2_data fsmonitor untracked/proof-missing 1 \ + <"$gitdir/$label-$pass.trace" && + assert_no_full_worktree_scan \ + "$gitdir/$label-$pass.trace" || return 1 + done + } && + + assert_routed_push_fast () { + worktree=$1 && + label=$2 && + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + for route in https ssh + do + case "$route" in + https) + pushurl_key=remote.https.pushurl && + pushurl=https://example.invalid/repository.git + ;; + ssh) + pushurl_key=remote.origin.pushurl && + pushurl=ssh://git@example.invalid/repository.git + ;; + esac && + for pass in first second + do + prefix="$gitdir/$label-$route-$pass" && + cp "$gitdir/index" "$prefix.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$prefix.diff.trace" \ + git -C "$worktree" \ + -c push.negotiate=true \ + -c "$pushurl_key=$pushurl" \ + diff --quiet --no-ext-diff \ + --no-textconv --ignore-submodules && + test_cmp_bin "$prefix.index" "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$prefix.diff.trace" && + assert_no_full_worktree_scan \ + "$prefix.diff.trace" && + GIT_TRACE2_EVENT="$prefix.write-tree.trace" \ + git -C "$worktree" \ + -c push.negotiate=true \ + -c "$pushurl_key=$pushurl" \ + write-tree >"$prefix.tree" && + test_file_not_empty "$prefix.tree" && + test_cmp_bin "$prefix.index" "$gitdir/index" && + assert_no_full_worktree_scan \ + "$prefix.write-tree.trace" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$prefix.status.trace" \ + git -C "$worktree" \ + -c push.negotiate=true \ + -c "$pushurl_key=$pushurl" \ + status --porcelain=v2 \ + >"$prefix.status" && + test_must_be_empty "$prefix.status" && + test_cmp_bin "$prefix.index" "$gitdir/index" && + assert_clean_status_fast \ + "$prefix.status.trace" && + assert_no_full_worktree_scan \ + "$prefix.status.trace" || return 1 + done && + prefix="$gitdir/$label-$route-writable" && + cp "$gitdir/index" "$prefix.index" && + GIT_TRACE2_EVENT="$prefix.status.trace" \ + git -C "$worktree" \ + -c push.negotiate=true \ + -c "$pushurl_key=$pushurl" \ + status --porcelain=v2 >"$prefix.status" && + test_must_be_empty "$prefix.status" && + assert_clean_status_fast \ + "$prefix.status.trace" && + assert_no_full_worktree_scan \ + "$prefix.status.trace" && + GIT_TRACE2_EVENT="$prefix.tracked.trace" \ + git -C "$worktree" status --porcelain=v2 \ + --untracked-files=no >"$prefix.tracked" && + test_must_be_empty "$prefix.tracked" && + test_cmp_bin "$prefix.index" "$gitdir/index" && + assert_clean_status_fast \ + "$prefix.tracked.trace" && + assert_no_full_worktree_scan \ + "$prefix.tracked.trace" && + test_fsmonitor_full_proof "$gitdir/index" paired || + return 1 + done + } && + + for worktree in "$repo" "$linked" + do + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + test-tool chmtime =-60 "$worktree"/stable/* && + git -C "$worktree" -c core.fsmonitor=false \ + update-index --refresh && + git -C "$worktree" fsmonitor--daemon start \ + --start-timeout=10 && + git -C "$worktree" update-index --fsmonitor && + GIT_INDEX_FILE="$gitdir/index" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/prime" && + test_must_be_empty "$gitdir/prime" && + test_fsmonitor_full_proof "$gitdir/index" paired || + return 1 done && - test_write_lines conflicting >tracked && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCC \ - GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ - git add tracked && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ - git commit -qm local-conflict && - test_must_fail env \ - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ - GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ - git cherry-pick --no-edit "$picked" \ - >.git/conflict.out 2>.git/conflict.err && - if test_fsmonitor_full_proof .git/index paired \ - >/dev/null 2>&1 - then - return 1 - fi && - GIT_OPTIONAL_LOCKS=0 \ - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ - git status --porcelain=v2 >.git/conflict && - test_grep "^u UU .* tracked$" .git/conflict + assert_routed_push_fast "$repo" main && + assert_routed_push_fast "$linked" linked && + + for worktree in "$repo" "$linked" + do + gitdir=$(git -C "$worktree" \ + rev-parse --absolute-git-dir) && + GIT_TRACE2_EVENT="$gitdir/rebase.trace" \ + git -C "$worktree" rebase upstream && + test_trace2_data fsmonitor \ + history/writer-proof-repaired 1 \ + <"$gitdir/rebase.trace" && + ! test_trace2_data fsmonitor \ + history/writer-proof-repaired 0 \ + <"$gitdir/rebase.trace" && + assert_owned_writer_clean "$worktree" rebase incoming || + return 1 + done && + + rebased=$(git rev-parse HEAD) && + gitdir=$(git rev-parse --absolute-git-dir) && + GIT_TRACE2_EVENT="$gitdir/reset.trace" \ + git reset --hard -q upstream && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/reset.trace" && + assert_owned_writer_clean "$repo" reset incoming && + GIT_TRACE2_EVENT="$gitdir/reset-back.trace" \ + git reset --hard -q "$rebased" && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/reset-back.trace" && + assert_owned_writer_clean "$repo" reset-back incoming && + + test_write_lines base topic stashed >stable/anchor && + GIT_TRACE2_EVENT="$gitdir/stash.trace" \ + git stash push -qm writer-proof && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/stash.trace" && + assert_owned_writer_clean "$repo" stash incoming && + + GIT_TRACE2_EVENT="$gitdir/checkout-upstream.trace" \ + git checkout -q upstream && + assert_owned_writer_clean "$repo" checkout-upstream incoming && + GIT_TRACE2_EVENT="$gitdir/checkout-topic.trace" \ + git checkout -q owned-main && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <"$gitdir/checkout-topic.trace" && + assert_owned_writer_clean "$repo" checkout-topic incoming && + + git -C "$repo" fsmonitor--daemon stop && + git -C "$linked" fsmonitor--daemon stop ) ' @@ -3302,6 +4547,220 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'provider delta loads a previously unused tracked exclude' ' + test_when_finished "rm -rf builtin-delta-ignored" && + test_create_repo builtin-delta-ignored && + ( + cd builtin-delta-ignored && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir cached && + test_write_lines ignored >cached/.gitignore && + test_write_lines tracked >cached/tracked && + git add cached/.gitignore cached/tracked && + git commit -m base && + git config core.untrackedCache true && + git config core.fsmonitor true && + git config status.showUntrackedFiles all && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 --untracked-files=all \ + >.git/prime && + test_must_be_empty .git/prime && + test_write_lines generated >cached/ignored && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 --untracked-files=all \ + >.git/expect && + test_must_be_empty .git/expect && + for pass in first second + do + cp .git/index ".git/$pass.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=cached/ignored \ + GIT_TRACE2_EVENT="$PWD/.git/$pass.trace" \ + git status --porcelain=v2 --untracked-files=all \ + >".git/$pass.actual" && + test_cmp .git/expect ".git/$pass.actual" && + test_cmp_bin ".git/$pass.index" .git/index && + test_trace2_data fsmonitor untracked/targeted-refresh 1 \ + <".git/$pass.trace" && + # The provider path and its newly needed exclude source. \ + test_trace2_data read_directory paths-visited 2 \ + <".git/$pass.trace" && + test_trace2_data read_directory opendir 0 \ + <".git/$pass.trace" && + test_trace2_data read_directory gitignore-invalidation 0 \ + <".git/$pass.trace" && + ! test_region index do_write_index ".git/$pass.trace" || + return 1 + done + ) +' + +test_expect_success CASE_INSENSITIVE_FS,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'provider delta loads a case-folded tracked exclude' ' + test_when_finished "rm -rf builtin-delta-ignored-icase" && + test_create_repo builtin-delta-ignored-icase && + ( + cd builtin-delta-ignored-icase && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir cached && + test_write_lines ignored >cached/.GitIgnore && + test_write_lines tracked >cached/tracked && + git add cached/.GitIgnore cached/tracked && + git commit -m base && + git config core.ignoreCase true && + git config core.untrackedCache true && + git config core.fsmonitor true && + git config status.showUntrackedFiles all && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 --untracked-files=all \ + >.git/prime && + test_must_be_empty .git/prime && + test_write_lines generated >cached/ignored && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 --untracked-files=all \ + >.git/expect && + test_must_be_empty .git/expect && + for pass in first second + do + cp .git/index ".git/$pass.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=cached/ignored \ + GIT_TRACE2_EVENT="$PWD/.git/$pass.trace" \ + git status --porcelain=v2 --untracked-files=all \ + >".git/$pass.actual" && + test_cmp .git/expect ".git/$pass.actual" && + test_cmp_bin ".git/$pass.index" .git/index && + test_trace2_data fsmonitor untracked/targeted-refresh 1 \ + <".git/$pass.trace" && + test_trace2_data read_directory paths-visited 2 \ + <".git/$pass.trace" && + test_trace2_data read_directory opendir 0 \ + <".git/$pass.trace" && + test_trace2_data read_directory gitignore-invalidation 0 \ + <".git/$pass.trace" && + ! test_region index do_write_index ".git/$pass.trace" || + return 1 + done && + mtime=$(test-tool chmtime --get cached/.GitIgnore) && + test_write_lines visible >cached/.GitIgnore && + test-tool chmtime =$mtime cached/.GitIgnore && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 --untracked-files=all \ + >.git/changed.expect && + cp .git/index .git/changed.index && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=cached/ \ + GIT_TRACE2_EVENT="$PWD/.git/changed.trace" \ + git status --porcelain=v2 --untracked-files=all \ + >.git/changed.actual && + test_cmp .git/changed.expect .git/changed.actual && + test_cmp_bin .git/changed.index .git/index && + test_trace2_data read_directory gitignore-invalidation 1 \ + <.git/changed.trace && + ! test_region index do_write_index .git/changed.trace + ) +' + +test_expect_success CASE_INSENSITIVE_FS,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'case-folded excludes preserve sparse-index fallback boundaries' ' + test_when_finished "rm -rf sparse-casefold-source sparse-casefold" && + test_create_repo sparse-casefold-source && + ( + cd sparse-casefold-source && + mkdir -p in/cached out/cached && + test_write_lines ignored >in/cached/.GitIgnore && + test_write_lines inside >in/cached/tracked && + test_write_lines ignored >out/cached/.GitIgnore && + test_write_lines outside >out/cached/tracked && + git add . && + git commit -m base + ) && + git clone --quiet --sparse sparse-casefold-source sparse-casefold && + git -C sparse-casefold sparse-checkout init --cone --sparse-index && + git -C sparse-casefold sparse-checkout set in && + git -C sparse-casefold config core.ignoreCase true && + git -C sparse-casefold config core.untrackedCache true && + git -C sparse-casefold config core.fsmonitor true && + git -C sparse-casefold config status.showUntrackedFiles all && + git -C sparse-casefold config advice.sparseIndexExpanded false && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git -C sparse-casefold update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/sparse-casefold/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git -C sparse-casefold status --porcelain=v2 \ + --untracked-files=all >sparse-casefold/.git/prime && + test_must_be_empty sparse-casefold/.git/prime && + git -C sparse-casefold ls-files --sparse -t \ + >sparse-casefold/.git/sparse-index && + test_grep "^S out/$" sparse-casefold/.git/sparse-index && + test_write_lines generated >sparse-casefold/in/cached/ignored && + GIT_OPTIONAL_LOCKS=0 \ + git -C sparse-casefold -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + --untracked-files=all >sparse-casefold/.git/in.expect && + test_must_be_empty sparse-casefold/.git/in.expect && + cp sparse-casefold/.git/index sparse-casefold/.git/in.index && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=in/cached/ignored \ + GIT_TRACE2_EVENT="$PWD/sparse-casefold/.git/in.trace" \ + git -C sparse-casefold status --porcelain=v2 \ + --untracked-files=all >sparse-casefold/.git/in.actual && + test_cmp sparse-casefold/.git/in.expect \ + sparse-casefold/.git/in.actual && + test_cmp_bin sparse-casefold/.git/in.index \ + sparse-casefold/.git/index && + test_trace2_data fsmonitor untracked/targeted-refresh 1 \ + sparse-casefold/out/cached/.GitIgnore && + test_write_lines generated >sparse-casefold/out/cached/ignored && + GIT_OPTIONAL_LOCKS=0 \ + git -C sparse-casefold -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + --untracked-files=all >sparse-casefold/.git/out.expect && + test_must_be_empty sparse-casefold/.git/out.expect && + cp sparse-casefold/.git/index sparse-casefold/.git/out.index && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=out/cached/ignored \ + GIT_TRACE2_EVENT="$PWD/sparse-casefold/.git/out.trace" \ + git -C sparse-casefold status --porcelain=v2 \ + --untracked-files=all >sparse-casefold/.git/out.actual && + test_cmp sparse-casefold/.git/out.expect \ + sparse-casefold/.git/out.actual && + test_cmp_bin sparse-casefold/.git/out.index \ + sparse-casefold/.git/index && + ! test_trace2_data fsmonitor untracked/targeted-refresh 1 \ + tracked && + test_write_lines baseline >state && + git add tracked state && + git commit -m base && + git config feature.manyFiles true && + git config index.version 4 && + git config core.untrackedCache true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_fsmonitor_full_proof .git/index paired && + test_write_lines visible >visible && + test_write_lines temporary >state && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=state \ + git stash push -qm diagnostic -- state && + test_fsmonitor_full_proof .git/index paired && + GIT_TEST_FSMONITOR_ALLOW_PROOF_REPAIR_SEQUENCE=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CDCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=state \ + GIT_TRACE2_EVENT="$PWD/.git/stash-pop.trace" \ + git stash pop >.git/stash-pop && + test_grep "visible" .git/stash-pop && + test_write_lines temporary >.git/state.expect && + test_cmp .git/state.expect state && + test_trace2_data status \ + untracked/replaced-authenticated-snapshot 1 \ + <.git/stash-pop.trace + ) +' + test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ 'builtin initial trivial response anchors a closure' ' test_when_finished "rm -rf builtin-initial-trivial" && @@ -5164,9 +6665,12 @@ test_expect_success MACOS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHO if test "$branch" = alternate then test_trace2_data fsmonitor \ - history/untracked-paired-new-directory-deferred 1 \ + history/untracked-paired-new-directory-invalidated 2 \ <".git/switch-$branch.trace" && - test_grep ! FSUC .git/index && + test_trace2_data fsmonitor \ + history/untracked-paired-transfer 1 \ + <".git/switch-$branch.trace" && + test_grep FSUC .git/index && test_grep FSCF .git/index else test_trace2_data fsmonitor \ @@ -5200,11 +6704,9 @@ test_expect_success MACOS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHO test "$visited_dirs" -lt 12 elif test "$branch" = alternate then - test_trace2_data fsmonitor \ + ! test_trace2_data fsmonitor \ history/external-untracked-restored 1 \ <".git/status-$branch.trace" && - test_region index do_write_index \ - ".git/status-$branch.trace" && test_grep FSUC .git/index && visited_dirs=$(sed -n \ "s/.*directories-visited:\\([0-9][0-9]*\\).*/\\1/p" \ @@ -6662,6 +8164,271 @@ test_expect_success LINUX_SCOPED_HISTORY,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORE ) ' +test_expect_success MACOS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ + 'worktree add primes a read-only clean status proof' ' + test_when_finished "rm -rf worktree-add-proof worktree-add-proof-linked" && + test_when_finished \ + "git -C worktree-add-proof-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo worktree-add-proof && + ( + cd worktree-add-proof && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines tracked >tracked && + git add tracked && + git commit -qm base && + git config core.fsmonitor true && + git config core.untrackedCache true && + GIT_TRACE2_EVENT="$PWD/.git/worktree-add.trace" \ + git worktree add --detach \ + ../worktree-add-proof-linked HEAD && + worktree="$PWD/../worktree-add-proof-linked" && + gitdir=$(git -C "$worktree" rev-parse --absolute-git-dir) && + test_trace2_data worktree add/clean-status-primed 1 \ + <.git/worktree-add.trace && + test_fsmonitor_full_proof "$gitdir/index" paired && + cp "$gitdir/index" "$gitdir/before.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$gitdir/status.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/status.actual" && + test_must_be_empty "$gitdir/status.actual" && + test_cmp_bin "$gitdir/before.index" "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/status.trace" && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <"$gitdir/status.trace" + ) +' + +test_expect_success MACOS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ + 'worktree add primes after a post-checkout index write' ' + test_when_finished "rm -rf worktree-add-hook-proof worktree-add-hook-proof-linked" && + test_when_finished \ + "git -C worktree-add-hook-proof-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo worktree-add-hook-proof && + ( + cd worktree-add-hook-proof && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines tracked >tracked && + git add tracked && + git commit -qm base && + git config core.fsmonitor true && + git config core.untrackedCache true && + write_script .git/hooks/post-checkout <<-\EOF && + git update-index --no-fsmonitor + EOF + GIT_TRACE2_EVENT="$PWD/.git/worktree-add.trace" \ + git worktree add --detach \ + ../worktree-add-hook-proof-linked HEAD && + worktree="$PWD/../worktree-add-hook-proof-linked" && + gitdir=$(git -C "$worktree" rev-parse --absolute-git-dir) && + test_trace2_data worktree add/clean-status-primed 1 \ + <.git/worktree-add.trace && + cp "$gitdir/index" "$gitdir/before.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$gitdir/status.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/status.actual" && + test_must_be_empty "$gitdir/status.actual" && + test_cmp_bin "$gitdir/before.index" "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/status.trace" && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <"$gitdir/status.trace" + ) +' + +test_expect_success MACOS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ + 'worktree add primes from linked worktree config after post-checkout' ' + test_when_finished "rm -rf worktree-add-config-proof worktree-add-config-proof-linked" && + test_create_repo worktree-add-config-proof && + ( + cd worktree-add-config-proof && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines tracked >tracked && + git add tracked && + git commit -qm base && + git config extensions.worktreeConfig true && + git config --worktree core.fsmonitor false && + git config --worktree core.untrackedCache false && + write_script .git/hooks/post-checkout <<-\EOF && + git update-index --no-fsmonitor && + git config --worktree core.fsmonitor true && + git config --worktree core.untrackedCache true + EOF + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/worktree-add.trace" \ + git worktree add --detach \ + ../worktree-add-config-proof-linked HEAD && + worktree="$PWD/../worktree-add-config-proof-linked" && + gitdir=$(git -C "$worktree" rev-parse --absolute-git-dir) && + test_trace2_data worktree add/clean-status-primed 1 \ + <.git/worktree-add.trace && + test_fsmonitor_full_proof "$gitdir/index" paired && + cp "$gitdir/index" "$gitdir/before.index" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$gitdir/status.trace" \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/status.actual" && + test_must_be_empty "$gitdir/status.actual" && + test_cmp_bin "$gitdir/before.index" "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/status.trace" && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <"$gitdir/status.trace" + ) +' + +test_expect_success FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ + 'worktree add bootstraps a proof without optional locks' ' + test_when_finished "rm -rf worktree-add-no-locks worktree-add-no-locks-linked" && + test_when_finished \ + "git -C worktree-add-no-locks-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo worktree-add-no-locks && + ( + cd worktree-add-no-locks && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines tracked >tracked && + git add tracked && + git commit -qm base && + git config core.fsmonitor true && + git config core.untrackedCache true && + set -- -c attr.tree= -c core.attributesFile= \ + -c safe.bareRepository=explicit -c core.hooksPath=/dev/null \ + -c core.fsmonitor=true && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/.git/worktree-add.trace" \ + git "$@" worktree add --detach \ + ../worktree-add-no-locks-linked HEAD && + ! test_trace2_data worktree add/clean-status-primed 1 \ + <.git/worktree-add.trace && + worktree="$PWD/../worktree-add-no-locks-linked" && + gitdir=$(git -C "$worktree" rev-parse --absolute-git-dir) && + test_fsmonitor_full_proof "$gitdir/index" paired && + cp "$gitdir/index" "$gitdir/before.index" && + for attempt in first second + do + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$gitdir/$attempt.trace" \ + git "$@" -C "$worktree" status --porcelain \ + -z --untracked-files=no --no-renames \ + >"$gitdir/$attempt.actual" && + test_must_be_empty "$gitdir/$attempt.actual" && + test_cmp_bin "$gitdir/before.index" "$gitdir/index" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$gitdir/$attempt.trace" && + ! test_trace2_data index refresh/sum_lstat \ + "[1-9][0-9]*" <"$gitdir/$attempt.trace" && + ! test_trace2_data index preload/sum_lstat \ + "[1-9][0-9]*" <"$gitdir/$attempt.trace" && + ! test_trace2_data fsmonitor semantic/manifest-scan-count \ + "[1-9][0-9]*" <"$gitdir/$attempt.trace" && + test_region ! index do_write_index \ + "$gitdir/$attempt.trace" || return 1 + done + ) +' + +test_expect_success FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ + 'worktree add proof detects post-checkout changes with optional locks disabled' ' + test_when_finished "rm -rf worktree-add-dirty-hook worktree-add-dirty-hook-linked" && + test_when_finished \ + "git -C worktree-add-dirty-hook-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo worktree-add-dirty-hook && + ( + cd worktree-add-dirty-hook && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + git config core.fsmonitor true && + git config core.untrackedCache true && + write_script .git/hooks/post-checkout <<-\EOF && + echo modified >tracked && + echo untracked >visible + EOF + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/.git/worktree-add.trace" \ + git worktree add --detach \ + ../worktree-add-dirty-hook-linked HEAD && + test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <.git/worktree-add.trace && + worktree="$PWD/../worktree-add-dirty-hook-linked" && + gitdir=$(git -C "$worktree" rev-parse --absolute-git-dir) && + cp "$gitdir/index" "$gitdir/before.index" && + GIT_OPTIONAL_LOCKS=0 \ + git -C "$worktree" -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >"$gitdir/expected" && + test_grep "^1 \\.M .* tracked$" "$gitdir/expected" && + test_grep "^? visible$" "$gitdir/expected" && + for attempt in first second + do + GIT_OPTIONAL_LOCKS=0 \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/$attempt.actual" && + test_cmp "$gitdir/expected" "$gitdir/$attempt.actual" && + test_cmp_bin "$gitdir/before.index" "$gitdir/index" || + return 1 + done + ) +' + +for proof_blocker in filter error reset +do +test_expect_success FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ + "worktree add rejects an uncertified initial proof ($proof_blocker)" ' + test_when_finished "rm -rf worktree-add-no-proof worktree-add-no-proof-linked" && + test_when_finished \ + "git -C worktree-add-no-proof-linked fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo worktree-add-no-proof && + ( + cd worktree-add-no-proof && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines "tracked filter=demo" >.gitattributes && + test_write_lines raw >tracked && + if test "$proof_blocker" = filter + then + git config filter.demo.clean "sed s/raw/converted/" && + git config filter.demo.smudge "sed s/converted/raw/" && + git config filter.demo.required true + fi && + git add .gitattributes tracked && + git commit -qm base && + git config core.fsmonitor true && + git config core.untrackedCache true && + case "$proof_blocker" in + error) GIT_TEST_FSMONITOR_QUERY_SEQUENCE=EEEEEEEE ;; + reset) GIT_TEST_FSMONITOR_QUERY_SEQUENCE=TTTTTTTT ;; + filter) GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC ;; + esac && + export GIT_TEST_FSMONITOR_QUERY_SEQUENCE && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_ALLOW_PROOF_REPAIR_SEQUENCE=1 \ + GIT_TRACE2_EVENT="$PWD/.git/worktree-add.trace" \ + git worktree add --detach \ + ../worktree-add-no-proof-linked HEAD && + worktree="$PWD/../worktree-add-no-proof-linked" && + gitdir=$(git -C "$worktree" rev-parse --absolute-git-dir) && + ! test_trace2_data fsmonitor history/writer-proof-repaired 1 \ + <.git/worktree-add.trace && + ! test_fsmonitor_full_proof "$gitdir/index" paired \ + >"$gitdir/proof.out" 2>&1 && + test_write_lines modified >"$worktree/tracked" && + test_write_lines untracked >"$worktree/visible" && + cp "$gitdir/index" "$gitdir/before.index" && + GIT_OPTIONAL_LOCKS=0 \ + git -C "$worktree" -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >"$gitdir/expected" && + GIT_OPTIONAL_LOCKS=0 \ + git -C "$worktree" status --porcelain=v2 \ + >"$gitdir/actual" && + test_cmp "$gitdir/expected" "$gitdir/actual" && + test_cmp_bin "$gitdir/before.index" "$gitdir/index" + ) +' +done + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'repeated provider resets fall back before an unclosable rescan' ' test_when_finished "rm -rf builtin-closure-terminal-reset" && diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh index 04c188ac50132a..680b5535c6ee6e 100755 --- a/t/t7527-builtin-fsmonitor.sh +++ b/t/t7527-builtin-fsmonitor.sh @@ -59,6 +59,54 @@ test_lazy_prereq HARDLINKS ' ln hardlink-a hardlink-b ' +test_lazy_prereq UNTRACKED_CACHE ' + git update-index --test-untracked-cache +' + +test_lazy_prereq FSMONITOR_DIR_METADATA ' + test "$uname_s" = Darwin || test "$uname_s" = Linux +' + +test_lazy_prereq FSMONITOR_LINUX ' + test "$uname_s" = Linux +' + +test_lazy_prereq DARWIN_ASYNC_FENCE ' + test "$uname_s" = Darwin || return 1 + git init test_darwin_async_fence || return 1 + + GIT_TRACE2_EVENT="$PWD/darwin-async-fence.trace2" && + export GIT_TRACE2_EVENT && + maybe_timeout 30 \ + git -C test_darwin_async_fence fsmonitor--daemon start \ + --start-timeout=10 + start_ret=$? + if test $start_ret -eq 0 + then + maybe_timeout 10 \ + test-tool -C test_darwin_async_fence \ + fsmonitor-client query --token 0 \ + >/dev/null 2>&1 + query_ret=$? + maybe_timeout 5 \ + git -C test_darwin_async_fence \ + fsmonitor--daemon stop 2>/dev/null || : + else + query_ret=1 + fi + unset GIT_TRACE2_EVENT + + test $start_ret -eq 0 && + test $query_ret -eq 0 && + test_grep "region_enter.*darwin-flush-async" \ + darwin-async-fence.trace2 && + test_grep ! "region_enter.*darwin-flush-sync" \ + darwin-async-fence.trace2 + ret=$? + rm -rf test_darwin_async_fence darwin-async-fence.trace2 + return $ret +' + test_lazy_prereq FOREIGN_FSMONITOR_GIT ' test -x /opt/homebrew/bin/git && /opt/homebrew/bin/git version @@ -74,13 +122,21 @@ then test_done fi -if test_have_prereq MACOS -then +case "$uname_s" in +Darwin) fsmonitor_pre_cookie_token_prefix=dirmeta-v1.inode-v1. -else + fsmonitor_pre_fence_token_prefix=${fsmonitor_pre_cookie_token_prefix}cookie-v1. + fsmonitor_cookie_token_prefix=${fsmonitor_pre_fence_token_prefix}fence-v1. + ;; +Linux) + fsmonitor_pre_cookie_token_prefix=dirmeta-v1. + fsmonitor_cookie_token_prefix=cookie-v1.dirmeta-v1. + ;; +*) fsmonitor_pre_cookie_token_prefix= -fi -fsmonitor_cookie_token_prefix=${fsmonitor_pre_cookie_token_prefix}cookie-v1. + fsmonitor_cookie_token_prefix=cookie-v1. + ;; +esac stop_daemon_delete_repo () { r=$1 && @@ -88,6 +144,13 @@ stop_daemon_delete_repo () { rm -rf $1 } +stop_daemon_delete_linked_repo () { + r=$1 && + wt=$2 && + { maybe_timeout 30 git -C "$wt" fsmonitor--daemon stop 2>/dev/null || :; } && + rm -rf "$r" "$wt" +} + start_daemon () { r= tf= t2= tk= && @@ -144,6 +207,112 @@ start_daemon () { ) } +provider_rename_scope_run () { + repo=$1 + scope=$2 + fsm=$3 + uc=$4 + defer_path=$5 + + git init "$repo" || return 1 + ( + cd "$repo" && + mkdir dir1 dir2 && + printf "root\\n" >rename && + printf "one\\n" >dir1/rename && + printf "two\\n" >dir2/rename && + git add rename dir1/rename dir2/rename && + git commit -m base && + + if test "$uc" = true + then + git config core.untrackedCache true && + git update-index --untracked-cache + else + git config core.untrackedCache false && + git update-index --no-untracked-cache + fi && + if test "$fsm" = true + then + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_DEFER_PATH=$defer_path && + export GIT_TEST_FSMONITOR_DEFER_PATH && + start_daemon --tf "$PWD/../$repo.trace" + else + git config core.fsmonitor false + fi && + + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + printf "stale-root\\n" >new && + printf "stale-one\\n" >dir1/new && + printf "stale-two\\n" >dir2/new && + git status --porcelain=v2 >.git/stale-prime-1 && + git status --porcelain=v2 >.git/stale-prime-2 && + rm new dir1/new dir2/new && + + case "$scope" in + all) + mv rename renamed && + mv dir1/rename dir1/renamed && + mv dir2/rename dir2/renamed + ;; + root) + mv rename renamed + ;; + nested) + mv dir1/rename dir1/renamed && + mv dir2/rename dir2/renamed + ;; + *) + BUG "unknown provider rename scope: $scope" + ;; + esac && + + GIT_INDEX_FILE="$PWD/.git/cold.index" git read-tree HEAD && + GIT_INDEX_FILE="$PWD/.git/cold.index" \ + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + cp .git/index .git/index-before && + GIT_OPTIONAL_LOCKS=0 git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_cmp .git/index-before .git/index && + + if test "$fsm" = true + then + test_grep "test-defer-path: '$defer_path'" \ + "../$repo.trace" && + test_grep \ + "test-publish-deferred-path-at-provider-fence" \ + "../$repo.trace" && + test_grep "Darwin provider fence complete" \ + "../$repo.trace" + fi + ) + result=$? + maybe_timeout 30 git -C "$repo" fsmonitor--daemon stop \ + >/dev/null 2>&1 || : + rm -rf "$repo" + return $result +} + +cleanup_provider_rename_matrix () { + for scope in all root nested + do + for fsm in false true + do + for uc in false true + do + stop_daemon_delete_repo \ + "provider-rename-$scope-$fsm-$uc" + done + done + done +} + # Is a Trace2 data event present with the given catetory and key? # We do not care what the value is. # @@ -154,6 +323,63 @@ have_t2_data_event () { grep -e '"event":"data".*"category":"'"$c"'".*"key":"'"$k"'"' } +native_stash_full_proof () { + perl - "$1" <<-\EOF + open my $input, "<", $ARGV[0] or die "cannot read index: $!\n"; + binmode $input; + local $/; + my $index = <$input>; + my %tokens; + for my $name ("FSMN", "FSUC", "FSCF") { + my $offset = index($index, $name); + die "missing $name extension\n" if $offset < 0; + my $size = unpack("N", substr($index, $offset + 4, 4)); + my $payload = substr($index, $offset + 8, $size); + if ($name eq "FSCF") { + my $flags = unpack("N", substr($payload, 8, 4)); + die "unbound FSCF flags $flags\n" if $flags != 15; + my $length = unpack("N", substr($payload, 12, 4)); + $tokens{$name} = substr($payload, 20, $length); + } else { + my $end = index($payload, "\0", 4); + die "invalid $name token\n" if $end < 0; + $tokens{$name} = substr($payload, 4, $end - 4); + } + } + die "mismatched provider tokens\n" unless + $tokens{"FSMN"} eq $tokens{"FSUC"} && + $tokens{"FSMN"} eq $tokens{"FSCF"}; + EOF +} + +native_tracked_full_proof () { + perl - "$1" <<-\EOF + open my $input, "<", $ARGV[0] or die "cannot read index: $!\n"; + binmode $input; + local $/; + my $index = <$input>; + my %tokens; + for my $name ("FSMN", "FSCF") { + my $offset = index($index, $name); + die "missing $name extension\n" if $offset < 0; + my $size = unpack("N", substr($index, $offset + 4, 4)); + my $payload = substr($index, $offset + 8, $size); + if ($name eq "FSCF") { + my $flags = unpack("N", substr($payload, 8, 4)); + die "unbound FSCF flags $flags\n" if $flags != 15; + my $length = unpack("N", substr($payload, 12, 4)); + $tokens{$name} = substr($payload, 20, $length); + } else { + my $end = index($payload, "\0", 4); + die "invalid $name token\n" if $end < 0; + $tokens{$name} = substr($payload, 4, $end - 4); + } + } + die "mismatched provider tokens\n" unless + $tokens{"FSMN"} eq $tokens{"FSCF"}; + EOF +} + test_expect_success 'explicit daemon start and stop' ' test_when_finished "stop_daemon_delete_repo test_explicit" && @@ -196,6 +422,1013 @@ test_expect_success 'implicit daemon start' ' test_must_fail git -C test_implicit fsmonitor--daemon status ' +test_expect_success MACOS \ + 'provider-fence client replaces an unfenced Darwin daemon' ' + test_when_finished "stop_daemon_delete_repo test_pre_fence" && + + git init test_pre_fence && + ( + cd test_pre_fence && + test_commit base tracked && + git config core.fsmonitor true && + ipc_path=$(git rev-parse --path-format=absolute \ + --git-path fsmonitor--daemon.ipc) && + test-tool simple-ipc start-daemon \ + --name="$ipc_path" --threads=1 --max-wait=10 \ + --fsmonitor-pre-provider-fence && + test-tool simple-ipc send --name="$ipc_path" \ + --token=get-capabilities >.git/old-capabilities && + test_grep "^cookie-token-retirement-v1$" \ + .git/old-capabilities && + test_grep ! "^darwin-provider-fence-v1$" \ + .git/old-capabilities && + old_token="builtin:${fsmonitor_pre_fence_token_prefix}test-pre-fence:0" && + GIT_TRACE2_EVENT="$PWD/.git/upgrade.trace" \ + test-tool fsmonitor-client query --token "$old_token" \ + >.git/upgrade.raw && + nul_to_q <.git/upgrade.raw >.git/upgrade.response && + test_grep "^builtin:${fsmonitor_cookie_token_prefix}" \ + .git/upgrade.response && + test_trace2_data fsm_client query/incompatible-daemon 1 \ + <.git/upgrade.trace && + test_grep \ + "\"argv\":.*\"fsmonitor--daemon\",\"run\",\"--detach\"" \ + .git/upgrade.trace && + git fsmonitor--daemon status + ) +' + +test_expect_success MACOS \ + 'provider-fence daemon retains the token marker for older clients' ' + test_when_finished "stop_daemon_delete_repo test_new_fence" && + + git init test_new_fence && + ( + cd test_new_fence && + ipc_path=$(git rev-parse --path-format=absolute \ + --git-path fsmonitor--daemon.ipc) && + test-tool simple-ipc start-daemon \ + --name="$ipc_path" --threads=1 --max-wait=10 \ + --fsmonitor-capability-superset && + test-tool simple-ipc send --name="$ipc_path" \ + --token=get-capabilities >.git/new-capabilities && + test_grep "^query-v1$" .git/new-capabilities && + test_grep "^query-v2$" .git/new-capabilities && + test_grep "^cookie-token-retirement-v1$" \ + .git/new-capabilities && + test_grep "^dir-metadata-filter-v1$" \ + .git/new-capabilities && + test_grep "^hardlink-inode-v1$" .git/new-capabilities && + test_grep "^darwin-provider-fence-v1$" \ + .git/new-capabilities && + test-tool simple-ipc send --name="$ipc_path" \ + --token="query-v2 pre-fence-client" \ + >.git/new-response && + test_grep "^builtin:${fsmonitor_pre_fence_token_prefix}" \ + .git/new-response && + test_grep "^builtin:${fsmonitor_cookie_token_prefix}" \ + .git/new-response + ) +' + +test_expect_success MACOS 'complete a delayed Darwin provider fence within budget' ' + test_when_finished "stop_daemon_delete_repo test_delayed_cookie" && + + git init test_delayed_cookie && + ( + GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS=1200 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=3000 && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon -C test_delayed_cookie \ + --tf "$PWD/delayed-cookie.trace" --tk true + ) && + + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + test-tool -C test_delayed_cookie fsmonitor-client query \ + --token "$token" >actual 2>error && + nul_to_q actual-q && + response=$(sed -n "s/Q.*//p" actual-q) && + test "${response%:*}" = "${token%:*}" && + test_grep "^builtin:.*Q$" actual-q && + test_grep ! "Q/Q" actual-q && + test_grep ! "Q//Q" actual-q && + test_grep "Darwin provider fence requested" delayed-cookie.trace && + test_grep "Darwin provider fence begin" delayed-cookie.trace && + test_grep "Darwin provider fence complete" delayed-cookie.trace && + test_grep "cookie-seen:" delayed-cookie.trace && + test_grep ! "synchronous flush timed out" delayed-cookie.trace && + test_must_be_empty error +' + +test_expect_success MACOS,DARWIN_ASYNC_FENCE \ + 'real Darwin provider fence rescues a delayed cookie callback' ' + test_when_finished " + stop_daemon_delete_repo test_delayed_callback; + rm -f delayed-callback.trace + " && + + git init test_delayed_callback && + ( + GIT_TEST_FSMONITOR_COOKIE_DELAY_MS=1500 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=3000 && + export GIT_TEST_FSMONITOR_COOKIE_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon -C test_delayed_callback \ + --tf "$PWD/delayed-callback.trace" --tk true + ) && + + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + test-tool -C test_delayed_callback fsmonitor-client query \ + --token "$token" >actual 2>error && + nul_to_q actual-q && + response=$(sed -n "s/Q.*//p" actual-q) && + test "${response%:*}" = "${token%:*}" && + test_grep "^builtin:.*Q$" actual-q && + test_grep ! "Q/Q" actual-q && + test_grep "cookie_wait timed out" delayed-callback.trace && + test_grep "Darwin provider fence begin" delayed-callback.trace && + # FlushAsync may return zero after dequeuing the callback; the + # serial queue barrier must still wait for its publication. + test_grep "Darwin provider fence target=.* event-pending=[01]" \ + delayed-callback.trace && + test_grep "cookie-seen:" delayed-callback.trace && + test_grep "Darwin provider fence complete" \ + delayed-callback.trace && + test_grep ! "synchronous flush timed out" delayed-callback.trace && + perl -ne '\'' + $t ||= $. if /cookie_wait timed out/; + $b ||= $. if $t && /Darwin provider fence begin/; + $s ||= $. if $b && /cookie-seen:/; + $c ||= $. if $s && /Darwin provider fence complete/; + END { exit !($t && $b && $s && $c && + $t < $b && $b < $s && $s < $c) } + '\'' delayed-callback.trace && + test_must_be_empty error +' + +test_expect_success MACOS 'retire a daemon when its provider fence times out' ' + test_when_finished "stop_daemon_delete_repo test_lost_cookie" && + + git init test_lost_cookie && + ( + GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS=2500 && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS && + start_daemon -C test_lost_cookie \ + --tf "$PWD/lost-cookie.trace" --tk true + ) && + + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + test-tool -C test_lost_cookie fsmonitor-client query \ + --token "$token" >actual 2>error && + nul_to_q actual-q && + response=$(sed -n "s/Q.*//p" actual-q) && + test "${response%:*}" != "${token%:*}" && + test_grep "Q/Q$" actual-q && + test_grep "Darwin provider fence requested" lost-cookie.trace && + test_grep "cookie_wait: synchronous flush timed out" \ + lost-cookie.trace && + test_must_be_empty error && + test_must_fail git -C test_lost_cookie fsmonitor--daemon status && + test-tool -C test_lost_cookie fsmonitor-client query --token 0 \ + >restarted && + git -C test_lost_cookie fsmonitor--daemon status +' + +test_expect_success MACOS \ + 'retire a daemon when a callback blocks the real provider fence' ' + test_when_finished " + stop_daemon_delete_repo test_blocked_callback; + rm -f blocked-callback.trace + " && + + git init test_blocked_callback && + ( + GIT_TEST_FSMONITOR_COOKIE_DELAY_MS=2500 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=500 && + export GIT_TEST_FSMONITOR_COOKIE_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon -C test_blocked_callback \ + --tf "$PWD/blocked-callback.trace" --tk true + ) && + + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + test-tool -C test_blocked_callback fsmonitor-client query \ + --token "$token" >actual 2>error && + nul_to_q actual-q && + test_grep "Q/Q$" actual-q && + test_grep "cookie_wait timed out" blocked-callback.trace && + test_grep "Darwin provider fence begin" blocked-callback.trace && + test_grep "cookie_wait: synchronous flush timed out" \ + blocked-callback.trace && + test_grep ! "Darwin provider fence complete" \ + blocked-callback.trace && + test_must_be_empty error && + + # Start the replacement immediately. The timed-out daemon must not + # unlink a socket which the replacement has already claimed. + test-tool -C test_blocked_callback fsmonitor-client query --token 0 \ + >restarted && + git -C test_blocked_callback fsmonitor--daemon status +' + +test_expect_success MACOS \ + 'bypassing the Darwin provider fence reproduces stale status' ' + test_when_finished "stop_daemon_delete_repo test_provider_bypass" && + + git init test_provider_bypass && + ( + cd test_provider_bypass && + printf "base\\n" >tracked && + git add tracked && + git commit -m base && + git config core.fsmonitor true && + git config core.untrackedCache true && + GIT_TEST_FSMONITOR_DEFER_PATH=tracked && + GIT_TEST_FSMONITOR_FLUSH_SYNC_BYPASS=1 && + export GIT_TEST_FSMONITOR_DEFER_PATH && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_BYPASS && + start_daemon --tf "$PWD/../provider-bypass.trace" && + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + + printf "changed\\n" >>tracked && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + cp .git/index .git/index-before && + GIT_OPTIONAL_LOCKS=0 git status --porcelain=v2 \ + >.git/actual && + test_grep "^1 \\.M .* tracked$" .git/expect && + test_must_be_empty .git/actual && + test_cmp .git/index-before .git/index && + test_grep "test-defer-path: .*tracked" \ + ../provider-bypass.trace && + test_grep "test-bypass-darwin-flush-sync" \ + ../provider-bypass.trace && + test_grep ! "test-publish-deferred-path-at-provider-fence" \ + ../provider-bypass.trace + ) +' + +test_expect_success MACOS \ + 'Darwin provider fence publishes older paths before returning' ' + test_when_finished "stop_daemon_delete_repo test_provider_fence" && + + git init test_provider_fence && + ( + cd test_provider_fence && + printf "base\\n" >tracked && + git add tracked && + git commit -m base && + git config core.fsmonitor true && + git config core.untrackedCache true && + GIT_TEST_FSMONITOR_DEFER_PATH=tracked && + export GIT_TEST_FSMONITOR_DEFER_PATH && + start_daemon --tf "$PWD/../provider-fence.trace" && + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + + printf "changed\\n" >>tracked && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + GIT_OPTIONAL_LOCKS=0 git status --porcelain=v2 \ + >.git/actual && + test_cmp .git/expect .git/actual && + test_grep "^1 \\.M .* tracked$" .git/actual && + test_grep "test-defer-path: '\''tracked'\''" \ + ../provider-fence.trace && + test_grep "test-publish-deferred-path-at-provider-fence" \ + ../provider-fence.trace && + perl -ne '\'' + $d ||= $. if /test-defer-path: .tracked./; + $s ||= $. if $d && /cookie-seen:/; + $p ||= $. if $s && + /test-publish-deferred-path-at-provider-fence/; + $c ||= $. if $p && /Darwin provider fence complete/; + END { exit !($d && $s && $p && $c && + $d < $s && $s < $p && $p < $c) } + '\'' ../provider-fence.trace + ) +' + +test_expect_success MACOS \ + 'Darwin provider fence rejects a token reset while in flight' ' + test_when_finished "stop_daemon_delete_repo test_provider_reset" && + + git init test_provider_reset && + ( + cd test_provider_reset && + GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS=1500 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=5000 && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon --tf "$PWD/../provider-reset.trace" --tk true && + test-tool fsmonitor-client query --token 0 >.git/initial && + nul_to_q <.git/initial >.git/initial-q && + token=$(sed -n "s/Q.*//p" .git/initial-q) && + test -n "$token" && + : >../provider-reset.trace && + + { + test-tool fsmonitor-client query --token "$token" \ + >.git/in-flight 2>.git/in-flight.err & + query_pid=$! + } && + seen= && + for i in $(test_seq 1 100) + do + if grep "Darwin provider fence begin" \ + ../provider-reset.trace >/dev/null 2>&1 + then + seen=1 && + break + fi && + sleep 0.05 || return 1 + done && + test "$seen" = 1 && + test-tool fsmonitor-client flush >.git/reset && + wait "$query_pid" && + test_must_be_empty .git/in-flight.err && + nul_to_q <.git/in-flight >.git/in-flight-q && + test_grep "Q/Q$" .git/in-flight-q && + test_grep "provider fence crossed token generation" \ + ../provider-reset.trace && + test_grep ! "synchronous flush timed out" \ + ../provider-reset.trace && + git fsmonitor--daemon status + ) +' + +test_expect_success MACOS \ + 'Darwin provider fence discards a deferred path across token reset' ' + test_when_finished " + stop_daemon_delete_repo test_provider_deferred_reset; + rm -f provider-deferred-reset.trace + " && + + git init test_provider_deferred_reset && + ( + cd test_provider_deferred_reset && + printf "base\n" >tracked && + git add tracked && + git commit -m base && + git config core.fsmonitor true && + git config core.untrackedCache true && + GIT_TEST_FSMONITOR_DEFER_PATH=tracked && + GIT_TEST_FSMONITOR_DEFER_PATH_DELAY_MS=1500 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=5000 && + export GIT_TEST_FSMONITOR_DEFER_PATH && + export GIT_TEST_FSMONITOR_DEFER_PATH_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon \ + --tf "$PWD/../provider-deferred-reset.trace" \ + --tk true && + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + test-tool fsmonitor-client query --token 0 >.git/initial && + nul_to_q <.git/initial >.git/initial-q && + token=$(sed -n "s/Q.*//p" .git/initial-q) && + test -n "$token" && + + : >../provider-deferred-reset.trace && + printf "changed\n" >>tracked && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + + { + test-tool fsmonitor-client query --token "$token" \ + >.git/in-flight 2>.git/in-flight.err & + query_pid=$! + } && + seen= && + for i in $(test_seq 1 100) + do + if grep "Darwin provider fence begin" \ + ../provider-deferred-reset.trace >/dev/null 2>&1 && + grep "test-defer-path: '\''tracked'\''" \ + ../provider-deferred-reset.trace >/dev/null 2>&1 + then + seen=1 && + break + fi && + sleep 0.05 || return 1 + done && + test "$seen" = 1 && + test-tool fsmonitor-client flush >.git/reset && + wait "$query_pid" && + test_must_be_empty .git/in-flight.err && + nul_to_q <.git/in-flight >.git/in-flight-q && + test_grep "Q/Q$" .git/in-flight-q && + test_grep "test-publish-deferred-path-at-provider-fence" \ + ../provider-deferred-reset.trace && + test_grep "test-discard-deferred-path-after-token-reset" \ + ../provider-deferred-reset.trace && + test_grep "provider fence crossed token generation" \ + ../provider-deferred-reset.trace && + GIT_OPTIONAL_LOCKS=0 git status --porcelain=v2 \ + >.git/actual && + test_cmp .git/expect .git/actual && + git fsmonitor--daemon status + ) +' + +test_expect_success MACOS \ + 'watched-root shutdown retires an in-flight Darwin fence' ' + test_when_finished " + stop_daemon_delete_repo test_provider_root; + stop_daemon_delete_repo test_provider_root-away; + rm -f provider-root.trace + " && + + git init test_provider_root && + ( + cd test_provider_root && + GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS=1500 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=5000 && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon --tf "$PWD/../provider-root.trace" --tk true + ) && + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + : >provider-root.trace && + { + test-tool -C test_provider_root fsmonitor-client query \ + --token "$token" >test_provider_root/.git/in-flight \ + 2>test_provider_root/.git/in-flight.err & + query_pid=$! + } && + seen= && + for i in $(test_seq 1 100) + do + if grep "Darwin provider fence begin" provider-root.trace \ + >/dev/null 2>&1 + then + seen=1 && + break + fi && + sleep 0.05 || return 1 + done && + test "$seen" = 1 && + mv test_provider_root test_provider_root-away && + wait "$query_pid" && + test_must_be_empty test_provider_root-away/.git/in-flight.err && + nul_to_q test_provider_root-away/.git/in-flight-q && + test_grep "Q/Q$" test_provider_root-away/.git/in-flight-q && + test_grep "event: root changed" provider-root.trace && + test_must_fail git -C test_provider_root-away \ + fsmonitor--daemon status && + test-tool -C test_provider_root-away fsmonitor-client query \ + --token 0 >test_provider_root-away/.git/restarted && + git -C test_provider_root-away fsmonitor--daemon status +' + +test_expect_success MACOS,DARWIN_ASYNC_FENCE \ + 'Darwin provider fence detects a zero-ID watched-root ancestor ABA' ' + test_when_finished " + stop_daemon_delete_repo test_provider_root_identity/repo; + stop_daemon_delete_repo test_provider_root_identity-away/repo; + rm -rf test_provider_root_identity \ + test_provider_root_identity-away; + rm -f provider-root-identity.trace + " && + + mkdir test_provider_root_identity && + git init test_provider_root_identity/repo && + ( + cd test_provider_root_identity/repo && + printf "base\n" >tracked && + git add tracked && + git commit -m base && + GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS=1500 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=5000 && + GIT_TEST_FSMONITOR_IGNORE_ROOT_CHANGED=1 && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + export GIT_TEST_FSMONITOR_IGNORE_ROOT_CHANGED && + start_daemon --tf "$PWD/../../provider-root-identity.trace" \ + --tk true + ) && + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + : >provider-root-identity.trace && + { + test-tool -C test_provider_root_identity/repo \ + fsmonitor-client query \ + --token "$token" \ + >test_provider_root_identity/repo/.git/in-flight \ + 2>test_provider_root_identity/repo/.git/in-flight.err & + query_pid=$! + } && + seen= && + for i in $(test_seq 1 100) + do + if grep "Darwin provider fence begin" \ + provider-root-identity.trace >/dev/null 2>&1 + then + seen=1 && + break + fi && + sleep 0.05 || return 1 + done && + test "$seen" = 1 && + mv test_provider_root_identity test_provider_root_identity-away && + printf "changed\n" \ + >>test_provider_root_identity-away/repo/tracked && + mv test_provider_root_identity-away test_provider_root_identity && + wait "$query_pid" && + test_must_be_empty \ + test_provider_root_identity/repo/.git/in-flight.err && + nul_to_q test_provider_root_identity/repo/.git/in-flight-q && + test_grep "Q/Q$" \ + test_provider_root_identity/repo/.git/in-flight-q && + test_grep "provider fence observed watch-root edge" \ + provider-root-identity.trace && + test_grep "provider fence rejected changed watch root" \ + provider-root-identity.trace && + test_must_fail git -C test_provider_root_identity/repo \ + fsmonitor--daemon status +' + +test_expect_success MACOS \ + 'Darwin provider fence retains the synchronous fallback' ' + test_when_finished " + stop_daemon_delete_repo test_provider_sync_fallback; + rm -f provider-sync-fallback.trace \ + provider-sync-fallback.trace2 + " && + + git init test_provider_sync_fallback && + ( + cd test_provider_sync_fallback && + printf "base\n" >tracked && + git add tracked && + git commit -m base && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_FORCE_SYNC_FLUSH=1 && + export GIT_TEST_FSMONITOR_FORCE_SYNC_FLUSH && + start_daemon --tf "$PWD/../provider-sync-fallback.trace" \ + --t2 "$PWD/../provider-sync-fallback.trace2" && + git status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test_grep "region_enter.*darwin-flush-sync" \ + ../provider-sync-fallback.trace2 && + test_grep ! "region_enter.*darwin-flush-async" \ + ../provider-sync-fallback.trace2 + ) +' + +test_expect_success MACOS \ + 'explicit shutdown drains an in-flight Darwin fence' ' + test_when_finished "stop_daemon_delete_repo test_provider_stop" && + + git init test_provider_stop && + ( + cd test_provider_stop && + GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS=1200 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=3000 && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon --tf "$PWD/../provider-stop.trace" --tk true + ) && + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + : >provider-stop.trace && + { + test-tool -C test_provider_stop fsmonitor-client query \ + --token "$token" >test_provider_stop/.git/in-flight \ + 2>test_provider_stop/.git/in-flight.err & + query_pid=$! + } && + seen= && + for i in $(test_seq 1 100) + do + if grep "Darwin provider fence begin" provider-stop.trace \ + >/dev/null 2>&1 + then + seen=1 && + break + fi && + sleep 0.05 || return 1 + done && + test "$seen" = 1 && + { + git -C test_provider_stop fsmonitor--daemon stop \ + >test_provider_stop/.git/stop.out \ + 2>test_provider_stop/.git/stop.err & + stop_pid=$! + } && + wait "$query_pid" && + wait "$stop_pid" && + test_must_be_empty test_provider_stop/.git/in-flight.err && + test_must_be_empty test_provider_stop/.git/stop.err && + nul_to_q test_provider_stop/.git/in-flight-q && + response=$(sed -n "s/Q.*//p" \ + test_provider_stop/.git/in-flight-q) && + test "${response%:*}" = "${token%:*}" && + test_grep ! "Q/Q$" test_provider_stop/.git/in-flight-q && + test_grep "Darwin provider fence complete" provider-stop.trace && + test_grep ! "abandoned during shutdown" provider-stop.trace && + test_must_fail git -C test_provider_stop fsmonitor--daemon status +' + +test_expect_success MACOS \ + 'concurrent Darwin queries coalesce without hiding dirt' ' + test_when_finished "stop_daemon_delete_repo test_provider_coalesce" && + + git init test_provider_coalesce && + ( + cd test_provider_coalesce && + mkdir tracked-dir && + printf "base-a\\n" >tracked-a && + printf "base-b\\n" >tracked-dir/tracked-b && + git add tracked-a tracked-dir/tracked-b && + git commit -m base && + git config core.fsmonitor true && + git config core.untrackedCache true && + GIT_TEST_FSMONITOR_FLUSH_COALESCE_DELAY_MS=500 && + export GIT_TEST_FSMONITOR_FLUSH_COALESCE_DELAY_MS && + start_daemon --tf "$PWD/../provider-coalesce.trace" && + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + + printf "dirty\\n" >>tracked-a && + printf "untracked\\n" >tracked-dir/untracked && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + : >../provider-coalesce.trace && + + pids= && + { + for i in $(test_seq 1 8) + do + GIT_OPTIONAL_LOCKS=0 git status --porcelain=v2 \ + >.git/actual-$i 2>.git/error-$i & + pids="$pids $!" || return 1 + done && + for pid in $pids + do + wait "$pid" || return 1 + done + } && + for i in $(test_seq 1 8) + do + test_cmp .git/expect .git/actual-$i || return 1 + test_must_be_empty .git/error-$i || return 1 + done && + requests=$(grep -c "Darwin provider fence requested" \ + ../provider-coalesce.trace) && + fences=$(grep -c "Darwin provider fence begin" \ + ../provider-coalesce.trace) && + test "$requests" -ge 8 && + test "$fences" -lt "$requests" && + test_grep "coalesced=[2-9]" ../provider-coalesce.trace && + test_grep ! "synchronous flush timed out" \ + ../provider-coalesce.trace && + git fsmonitor--daemon status + ) +' + +test_expect_success MACOS \ + 'queries arriving during a Darwin fence require a second fence' ' + test_when_finished " + stop_daemon_delete_repo test_provider_two_wave; + rm -f provider-two-wave.trace + " && + + git init test_provider_two_wave && + ( + cd test_provider_two_wave && + GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS=2500 && + GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS=7000 && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_DELAY_MS && + export GIT_TEST_FSMONITOR_FLUSH_SYNC_TIMEOUT_MS && + start_daemon --tf "$PWD/../provider-two-wave.trace" \ + --tk true + ) && + token="builtin:${fsmonitor_cookie_token_prefix}test_00000001:0" && + : >provider-two-wave.trace && + { + test-tool -C test_provider_two_wave fsmonitor-client query \ + --token "$token" >test_provider_two_wave/.git/q1 \ + 2>test_provider_two_wave/.git/q1.err & + q1_pid=$! + } && + seen= && + for i in $(test_seq 1 100) + do + if grep "Darwin provider fence begin request=1" \ + provider-two-wave.trace >/dev/null 2>&1 + then + seen=1 && + break + fi && + sleep 0.05 || return 1 + done && + test "$seen" = 1 && + { + test-tool -C test_provider_two_wave fsmonitor-client query \ + --token "$token" >test_provider_two_wave/.git/q2 \ + 2>test_provider_two_wave/.git/q2.err & + q2_pid=$! + } && + seen= && + for i in $(test_seq 1 100) + do + if grep "Darwin provider fence requested request=2" \ + provider-two-wave.trace >/dev/null 2>&1 + then + seen=1 && + break + fi && + sleep 0.05 || return 1 + done && + test "$seen" = 1 && + wait "$q1_pid" && + wait "$q2_pid" && + test_must_be_empty test_provider_two_wave/.git/q1.err && + test_must_be_empty test_provider_two_wave/.git/q2.err && + nul_to_q test_provider_two_wave/.git/q1-q && + nul_to_q test_provider_two_wave/.git/q2-q && + test_grep ! "Q/Q$" test_provider_two_wave/.git/q1-q && + test_grep ! "Q/Q$" test_provider_two_wave/.git/q2-q && + test "$(grep -c "Darwin provider fence begin" \ + provider-two-wave.trace)" = 2 && + test "$(grep -c "Darwin provider fence complete" \ + provider-two-wave.trace)" = 2 && + perl -ne '\'' + $b1 ||= $. if /fence begin request=1/; + $c1 ||= $. if $b1 && /fence complete request=1/; + $b2 ||= $. if $c1 && /fence begin request=2/; + $c2 ||= $. if $b2 && /fence complete request=2/; + END { exit !($b1 && $c1 && $b2 && $c2 && + $b1 < $c1 && $c1 < $b2 && $b2 < $c2) } + '\'' provider-two-wave.trace && + git -C test_provider_two_wave fsmonitor--daemon status +' + +test_expect_success MACOS,UNTRACKED_CACHE \ + 'Darwin provider fence covers rename scopes and cache controls' ' + test_when_finished cleanup_provider_rename_matrix && + + for scope in all root nested + do + case "$scope" in + all|root) + defer_path=rename + ;; + nested) + defer_path=dir1/rename + ;; + esac && + for fsm in false true + do + for uc in false true + do + # The UC-only control is covered by the main matrix + # and is known to be timing-sensitive on macOS. + if test "$fsm" = false && test "$uc" = true + then + continue + fi && + provider_rename_scope_run \ + "provider-rename-$scope-$fsm-$uc" \ + "$scope" "$fsm" "$uc" "$defer_path" || + return 1 + done + done + done +' + +test_expect_success MACOS,UNTRACKED_CACHE,DARWIN_ASYNC_FENCE \ + 'repeated Darwin provider fences remain live' ' + test_when_finished " + stop_daemon_delete_repo test_provider_stress; + rm -f provider-stress.trace provider-stress.trace2 + " && + + git init test_provider_stress && + ( + cd test_provider_stress && + printf "base\\n" >tracked && + git add tracked && + git commit -m base && + git config core.fsmonitor true && + git config core.untrackedCache true && + start_daemon --tf "$PWD/../provider-stress.trace" \ + --t2 "$PWD/../provider-stress.trace2" && + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + + for i in $(test_seq 1 512) + do + GIT_OPTIONAL_LOCKS=0 git status --porcelain=v2 \ + >.git/actual || return 1 + test_must_be_empty .git/actual || return 1 + done && + requests=$(grep -c "Darwin provider fence requested" \ + ../provider-stress.trace) && + completed=$(grep -c "Darwin provider fence complete" \ + ../provider-stress.trace) && + test "$requests" -ge 512 && + test "$requests" = "$completed" && + test_grep ! "synchronous flush timed out" \ + ../provider-stress.trace && + test_grep "region_enter.*darwin-flush-async" \ + ../provider-stress.trace2 && + test_grep ! "region_enter.*darwin-flush-sync" \ + ../provider-stress.trace2 && + git fsmonitor--daemon status + ) +' + +test_expect_success MACOS 'fresh unpinned batches honor the retention grace' ' + test_when_finished "stop_daemon_delete_repo test_fresh_history" && + + git init test_fresh_history && + ( + cd test_fresh_history && + printf "target/\\n" >.gitignore && + printf "a\\n" >tracked-a && + printf "b\\n" >tracked-b && + git add .gitignore tracked-a tracked-b && + git commit -m base && + sane_unset GIT_TEST_FSMONITOR_TRUNCATE_DELAY_SECONDS && + start_daemon --tf "$PWD/../fresh-history.trace" && + git config core.fsmonitor true && + git config core.untrackedCache true && + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + mkdir target && + for i in $(test_seq 1 5250) + do + printf "x\\n" >"target/ignored-$i" || return 1 + done && + test-tool fsmonitor-client query >../fresh-history.response && + perl -0ne '\''$nr++; END { print "$nr\n" }'\'' \ + <../fresh-history.response >../fresh-history.count && + test "$(cat ../fresh-history.count)" -gt 1025 && + git status --porcelain=v2 >.git/after-burst && + printf "new\\n" >>tracked-a && + git status --porcelain=v2 >.git/after-event && + test_grep ! "Compact: batch" ../fresh-history.trace + ) +' + +test_expect_success MACOS 'private index cannot prune canonical index history' ' + test_when_finished "stop_daemon_delete_repo test_index_history" && + test_when_finished "rm -f private-index" && + + git init test_index_history && + ( + cd test_index_history && + test_commit base tracked && + test_commit other other && + git config core.untrackedCache true && + git config core.fsmonitor true && + ( + GIT_TEST_FSMONITOR_TRUNCATE_DELAY_SECONDS=0 && + export GIT_TEST_FSMONITOR_TRUNCATE_DELAY_SECONDS && + start_daemon --tf "$PWD/../index-history.trace" + ) && + + GIT_INDEX_FILE="$PWD/.git/index" \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + cp .git/index ../private-index && + cp .git/index .git/index.before && + + echo first >>tracked && + GIT_INDEX_FILE="$PWD/../private-index" git add -u && + echo second >>other && + GIT_INDEX_FILE="$PWD/../private-index" git add -u && + echo third >>tracked && + GIT_INDEX_FILE="$PWD/../private-index" git add -u && + echo fourth >>other && + GIT_INDEX_FILE="$PWD/../private-index" git add -u && + echo fifth >>tracked && + GIT_INDEX_FILE="$PWD/../private-index" git add -u && + test_cmp .git/index.before .git/index && + test_grep "Compact: batch" ../index-history.trace \ + >../index-history.compactions && + test_line_count = 3 ../index-history.compactions && + test_grep "covers 2 of 3 paths" ../index-history.compactions && + + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/.git/canonical.trace" \ + git status --porcelain=v2 >.git/canonical && + test_line_count = 2 .git/canonical && + test_grep "^1 \.M .* tracked$" .git/canonical && + test_grep "^1 \.M .* other$" .git/canonical && + test_cmp .git/index.before .git/index && + ! test_trace2_data fsm_client query/trivial-response 1 \ + <.git/canonical.trace + ) +' + +test_expect_success MACOS,HARDLINKS \ + 'compaction does not replay consumed hardlink events' ' + test_when_finished "stop_daemon_delete_repo test_hardlink_history" && + test_when_finished "rm -f hardlink-private-index" && + + git init test_hardlink_history && + ( + cd test_hardlink_history && + printf "target/\\n" >.gitignore && + printf "a\\n" >tracked-a && + printf "b\\n" >tracked-b && + git add .gitignore tracked-a tracked-b && + git commit -m base && + git config core.untrackedCache true && + git config core.trustctime false && + git config core.checkStat minimal && + ( + GIT_TEST_FSMONITOR_TRUNCATE_DELAY_SECONDS=0 && + export GIT_TEST_FSMONITOR_TRUNCATE_DELAY_SECONDS && + start_daemon --tf "$PWD/../hardlink-history.trace" + ) && + git config core.fsmonitor true && + git status --porcelain=v2 >.git/prime-1 && + git status --porcelain=v2 >.git/prime-2 && + test_must_be_empty .git/prime-1 && + test_must_be_empty .git/prime-2 && + mkdir target && + printf "AAAA\\n" >target/object && + ln target/object target/object-link && + printf "BBBB\\n" >target/object-link && + GIT_TRACE2_EVENT="$PWD/.git/consume.trace" \ + git status --porcelain=v2 >.git/consume && + test_must_be_empty .git/consume && + test_trace2_data fsmonitor apply/hardlink-index-scan 1 \ + <.git/consume.trace && + git update-index --refresh --force-write-index && + test-tool dump-fsmonitor >.git/checkpoint && + checkpoint=$(sed -n "s/^fsmonitor last update //p" \ + .git/checkpoint) && + test -n "$checkpoint" && + test "${checkpoint##*:}" -gt 0 && + cp .git/index .git/index.before && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/.git/control.trace" \ + git status --porcelain=v2 >.git/control && + test_must_be_empty .git/control && + ! test_trace2_data fsmonitor apply/hardlink-index-scan 1 \ + <.git/control.trace && + cp .git/index ../hardlink-private-index && + grep -c "event: //inode:" ../hardlink-history.trace \ + >.git/inodes.before && + for i in $(test_seq 1 8) + do + if test $((i % 2)) -eq 0 + then + printf "private-%s\\n" "$i" >>tracked-a + else + printf "private-%s\\n" "$i" >>tracked-b + fi && + GIT_INDEX_FILE="$PWD/../hardlink-private-index" \ + git add -u || return 1 + done && + test_cmp .git/index.before .git/index && + grep -c "event: //inode:" ../hardlink-history.trace \ + >.git/inodes.after && + test_cmp .git/inodes.before .git/inodes.after && + test_grep "Compact: batch" ../hardlink-history.trace && + for i in $(test_seq 1 5) + do + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/.git/repeat-$i.trace" \ + git status --porcelain=v2 \ + >.git/repeat-$i || return 1 + ! test_trace2_data fsmonitor apply/hardlink-index-scan 1 \ + <.git/repeat-$i.trace || return 1 + done && + test_cmp .git/index.before .git/index && + test-tool fsmonitor-client query-legacy \ + --token "$checkpoint" >.git/legacy && + nul_to_q <.git/legacy >.git/legacy-q && + test_grep ! "Q/Q" .git/legacy-q && + test_grep ! "Q//Q" .git/legacy-q + ) +' + # Verify that the daemon has shutdown. Spin a few seconds to # make the test a little more robust during CI testing. # @@ -721,10 +1954,6 @@ test_expect_success 'cleanup worktrees' ' # data) from fsmonitor doesn't cause incorrect results. And doesn't # cause incorrect results when the untracked-cache is enabled. -test_lazy_prereq UNTRACKED_CACHE ' - git update-index --test-untracked-cache -' - test_expect_success 'Matrix: setup for untracked-cache,fsmonitor matrix' ' test_unconfig core.fsmonitor && git update-index --no-fsmonitor && @@ -1374,14 +2603,15 @@ test_expect_success CASE_INSENSITIVE_FS 'fsmonitor file case wrong on disk' ' test_grep -q "fsmonitor_refresh_callback.*FILE-4-A.*pos 6" "$PWD/file_case_wrong-try1.log" && test_grep -q "fsmonitor_refresh_callback.*file-4-a.*pos -9" "$PWD/file_case_wrong-try1.log" && - # FSM refresh will have invalidated the FSM bit and cause a regular - # (real) scan of these tracked files, so they should have "H" status. - # (We will not see a "h" status until the next refresh (on the next - # command).) + # FSM refresh invalidates the FSM bit and causes a regular (real) scan + # of these tracked files. Authenticated external history may retain the + # refreshed bit for the following reader, so either marker is valid. git -C file_case_wrong ls-files -f >"$PWD/file_case_wrong-lsf1.out" && - test_grep -q "H dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf1.out" && - test_grep -q "H dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-lsf1.out" && + test_grep -E -q "^[Hh] dir1/dir2/dir3/file-3-a$" \ + "$PWD/file_case_wrong-lsf1.out" && + test_grep -E -q "^[Hh] dir1/dir2/dir4/FILE-4-A$" \ + "$PWD/file_case_wrong-lsf1.out" && # Try the status again. We assume that the above status command @@ -1719,6 +2949,62 @@ test_expect_success MACOS,UNTRACKED_CACHE \ ) ' +test_expect_success FSMONITOR_LINUX \ + 'nameless inotify events use the watched directory' ' + test_when_finished \ + "git -C inotify-nameless fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo inotify-nameless && + ( + cd inotify-nameless && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir -p existing/inner && + test_write_lines tracked >existing/inner/tracked && + git add existing/inner/tracked && + git commit -qm base && + git config core.fsmonitor true && + start_daemon --tf "$PWD/.git/daemon.trace" && + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + git update-index --force-write-index && + + chmod 750 existing/inner && + test-tool fsmonitor-client query >.git/chmod.raw && + nul_to_q <.git/chmod.raw >.git/chmod.response && + test_grep "Qexisting/inner/Q" .git/chmod.response && + test_grep ! "Qexisting/inner/[^Q][^Q]*Q" \ + .git/chmod.response && + chmod 755 existing/inner + ) +' + +test_expect_success FSMONITOR_LINUX \ + 'nested Git directory events remain valid' ' + test_when_finished \ + "git -C nested-dotgit fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo nested-dotgit && + ( + cd nested-dotgit && + test_write_lines tracked >tracked && + git add tracked && + git commit -qm base && + git config core.fsmonitor true && + start_daemon && + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + + mkdir -p scratch/.git && + test_write_lines nested >scratch/.git/file && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + status --porcelain=v2 >.git/expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + ! test_trace2_data fsm_client query/invalid-response 1 \ + <.git/status.trace + ) +' + test_expect_success MACOS 'implicit daemon reuses the invoking Git executable' ' test_create_repo same-executable-spawn && mkdir fake-exec-path && @@ -1829,7 +3115,121 @@ test_expect_success 'bound query replaces a legacy daemon' ' ) ' -test_expect_success MACOS 'bound query upgrades stale directory event daemon' ' +test_expect_success MACOS \ + 'read-only legacy upgrade waits for one writable exact repair' ' + test_when_finished \ + "stop_daemon_delete_repo legacy-read-only-upgrade" && + test_create_repo legacy-read-only-upgrade && + ( + cd legacy-read-only-upgrade && + sane_unset GIT_TEST_SPLIT_INDEX && + git config core.fsmonitor false && + for i in $(test_seq 1 64) + do + test_write_lines "$i" >"tracked-$i" || return 1 + done && + git add . && + git commit -qm base && + git config core.preloadIndex false && + git config core.untrackedCache true && + git config core.fsmonitor true && + ipc_path=$(git rev-parse --path-format=absolute \ + --git-path fsmonitor--daemon.ipc) && + test-tool simple-ipc start-daemon \ + --name="$ipc_path" --threads=1 \ + --fsmonitor-capability-superset && + git status --porcelain=v2 --untracked-files=normal \ + --no-ahead-behind >.git/prime && + test_must_be_empty .git/prime && + test_path_is_missing .git/index.csts && + test-tool simple-ipc stop-daemon --name="$ipc_path" && + test-tool simple-ipc start-daemon \ + --name="$ipc_path" --threads=1 --fsmonitor-legacy && + cp .git/index .git/index.before && + + for label in first repeat + do + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/.git/$label.trace" \ + git status --porcelain=v2 >.git/$label && + test_must_be_empty .git/$label && + test_cmp_bin .git/index.before .git/index && + test_trace2_data index refresh/sum_lstat 64 \ + <.git/$label.trace || return 1 + done && + test_trace2_data fsm_client query/incompatible-daemon 1 \ + <.git/first.trace && + test_path_is_missing .git/index.csts && + { git fsmonitor--daemon stop 2>/dev/null || :; } && + + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + GIT_TRACE2_EVENT="$PWD/.git/repair.trace" \ + git status --porcelain=v2 >.git/repair && + test_must_be_empty .git/repair && + ! test_cmp_bin .git/index.before .git/index && + test-tool dump-fsmonitor >.git/fsmonitor && + test_grep "fsmonitor last update builtin:test:1" \ + .git/fsmonitor + ) +' + +test_expect_success MACOS \ + 'linked worktree legacy upgrade uses its writable index repair' ' + test_when_finished \ + "stop_daemon_delete_linked_repo legacy-linked legacy-linked-wt" && + test_create_repo legacy-linked && + ( + cd legacy-linked && + git config core.fsmonitor false && + for i in $(test_seq 1 32) + do + test_write_lines "$i" >"tracked-$i" || return 1 + done && + git add . && + git commit -qm base && + git worktree add -q -b linked ../legacy-linked-wt + ) && + git -C legacy-linked config core.preloadIndex false && + git -C legacy-linked config core.untrackedCache true && + git -C legacy-linked config core.fsmonitor true && + gitdir=$(git -C legacy-linked-wt rev-parse --absolute-git-dir) && + ipc_path=$(git -C legacy-linked-wt rev-parse --path-format=absolute \ + --git-path fsmonitor--daemon.ipc) && + test-tool simple-ipc start-daemon \ + --name="$ipc_path" --threads=1 \ + --fsmonitor-capability-superset && + git -C legacy-linked-wt status --porcelain=v2 \ + --untracked-files=normal --no-ahead-behind >linked.prime && + test_must_be_empty linked.prime && + test-tool simple-ipc stop-daemon --name="$ipc_path" && + test-tool simple-ipc start-daemon \ + --name="$ipc_path" --threads=1 --fsmonitor-legacy && + cp "$gitdir/index" linked.index.before && + for label in first repeat + do + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/linked-$label.trace" \ + git -C legacy-linked-wt status --porcelain=v2 \ + >linked-$label && + test_must_be_empty linked-$label && + test_cmp_bin linked.index.before "$gitdir/index" && + test_trace2_data index refresh/sum_lstat 32 \ + /dev/null || :; } && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + GIT_TRACE2_EVENT="$PWD/linked-repair.trace" \ + git -C legacy-linked-wt status --porcelain=v2 >linked-repair && + test_must_be_empty linked-repair && + ! test_cmp_bin linked.index.before "$gitdir/index" && + test_path_is_missing "$gitdir/index.csts" && + test-tool -C legacy-linked-wt dump-fsmonitor >linked.fsmonitor && + test_grep "fsmonitor last update builtin:test:1" \ + linked.fsmonitor +' + +test_expect_success FSMONITOR_DIR_METADATA \ + 'bound query upgrades stale directory event daemon' ' test_when_finished \ "stop_daemon_delete_repo directory-daemon-upgrade" && test_create_repo directory-daemon-upgrade && @@ -1858,7 +3258,7 @@ test_expect_success MACOS 'bound query upgrades stale directory event daemon' ' "\"argv\":.*\"fsmonitor--daemon\",\"run\",\"--detach\"" \ .git/upgrade.trace && test-tool dump-fsmonitor >.git/fsmonitor && - test_grep "fsmonitor last update builtin:dirmeta-v1\\." \ + test_grep "fsmonitor last update builtin:${fsmonitor_cookie_token_prefix}" \ .git/fsmonitor && GIT_TRACE2_EVENT="$PWD/.git/repeat.trace" \ @@ -2091,7 +3491,7 @@ test_expect_success MACOS,UNTRACKED_CACHE \ ! test_trace2_data fsm_client query/worktree-mismatch 1 \ <.git/reconnect.trace && test-tool dump-fsmonitor >.git/fsmonitor && - test_grep "fsmonitor last update builtin:dirmeta-v1\\." \ + test_grep "fsmonitor last update builtin:${fsmonitor_cookie_token_prefix}" \ .git/fsmonitor && test_grep FSCF .git/index && test_grep FSUC .git/index && @@ -2109,7 +3509,7 @@ test_expect_success MACOS,UNTRACKED_CACHE \ ) ' -test_expect_success MACOS,UNTRACKED_CACHE \ +test_expect_success FSMONITOR_DIR_METADATA,UNTRACKED_CACHE \ 'concurrent clients share one stale directory daemon upgrade' ' test_when_finished \ "stop_daemon_delete_repo concurrent-directory-daemon-upgrade" && @@ -2165,7 +3565,7 @@ test_expect_success MACOS,UNTRACKED_CACHE \ .git/client-*.trace >.git/daemon-spawns && test_line_count = 1 .git/daemon-spawns && test-tool dump-fsmonitor >.git/fsmonitor && - test_grep "fsmonitor last update builtin:dirmeta-v1\\." \ + test_grep "fsmonitor last update builtin:${fsmonitor_cookie_token_prefix}" \ .git/fsmonitor && test_grep FSCF .git/index && test_grep FSUC .git/index && @@ -2201,6 +3601,12 @@ test_expect_success 'bound daemon also serves legacy token queries' ' token=$(sed -n "s/^fsmonitor last update //p" \ .git/fsmonitor) && test -n "$token" && + if test_have_prereq FSMONITOR_LINUX + then + test_grep \ + "^fsmonitor last update builtin:cookie-v1\\.dirmeta-v1\\." \ + .git/fsmonitor || return 1 + fi && ipc_path=$(git rev-parse --path-format=absolute \ --git-path fsmonitor--daemon.ipc) && test-tool simple-ipc send --name="$ipc_path" \ @@ -2900,6 +4306,68 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'large directory events authenticate distinct attribute sources once' ' + test_when_finished "rm -rf large-directory-event" && + test_create_repo large-directory-event && + ( + cd large-directory-event && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir -p bulk/nested siblings && + for file in $(test_seq 1 96) + do + test_write_lines "bulk-$file" >"bulk/nested/$file" || + return 1 + done && + for file in $(test_seq 1 128) + do + test_write_lines "sibling-$file" >"siblings/$file" || + return 1 + done && + git add bulk siblings && + git commit -qm base && + git config core.untrackedCache true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=bulk/ \ + GIT_TRACE2_EVENT="$PWD/.git/bulk.trace" \ + git status --porcelain=v2 >.git/bulk.actual && + test_must_be_empty .git/bulk.actual && + test_trace2_data fsmonitor \ + semantic/authenticated-restored-directory 1 \ + <.git/bulk.trace && + test_trace2_data index refresh/sum_lstat 96 \ + <.git/bulk.trace && + ! have_t2_data_event fsmonitor semantic/attributes-cone \ + <.git/bulk.trace && + ! have_t2_data_event fsmonitor semantic/manifest-scan-count \ + <.git/bulk.trace && + + test_write_lines "*.txt text" >bulk/nested/.gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/attributes.expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=bulk/ \ + GIT_TRACE2_EVENT="$PWD/.git/attributes.trace" \ + git status --porcelain=v2 >.git/attributes.actual && + test_cmp .git/attributes.expect .git/attributes.actual && + test_trace2_data fsmonitor semantic/attributes-cone 96 \ + <.git/attributes.trace && + test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/attributes.trace && + ! have_t2_data_event fsmonitor \ + semantic/authenticated-restored-directory \ + <.git/attributes.trace + ) +' + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'deleted staged directories never discard nested attribute sources' ' test_when_finished \ @@ -4074,6 +5542,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_must_be_empty .git/baseline && test_trace2_data fsmonitor history/external-stored 1 \ <"$TRASH_DIRECTORY/restored-racy-baseline.trace" && + test_remove_clean_status_sidecar .git/index.csts && cp .git/index .git/owned.before && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ GIT_TRACE2_EVENT="$TRASH_DIRECTORY/restored-racy-checkpoint.trace" \ @@ -4081,6 +5550,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_must_be_empty .git/checkpoint && test_trace2_data fsmonitor history/external-stored 1 \ <"$TRASH_DIRECTORY/restored-racy-checkpoint.trace" && + test_remove_clean_status_sidecar .git/index.csts && cp .git/owned.before .git/index && git -c core.fsmonitor=false update-index \ --no-fsmonitor --force-write-index && @@ -4507,6 +5977,336 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +native_stash_setup () { + repo=$1 && + mode=$2 && + test_create_repo "$repo" && + ( + cd "$repo" && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines base >tracked && + test_write_lines sibling >sibling && + test_write_lines "# baseline" >.gitattributes && + test_write_lines "# baseline" >.gitignore && + git add tracked sibling .gitattributes .gitignore && + git commit -qm base && + test-tool chmtime -120 tracked sibling \ + .gitattributes .gitignore && + git update-index --refresh && + git config core.autocrlf false && + git config core.trustctime true && + git config core.checkStat default && + git config core.untrackedCache true && + git config core.fsmonitor true && + if test "$mode" = lfs + then + git config filter.lfs.clean "git-lfs clean -- %f" && + git config filter.lfs.smudge "git-lfs smudge -- %f" && + git config filter.lfs.process "git-lfs filter-process" && + git config filter.lfs.required true + fi && + start_daemon && + git update-index --fsmonitor && + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + native_stash_full_proof .git/index + ) +} + +native_stash_reset () { + git reset --hard -q HEAD && + git clean -fdq && + git stash clear && + git status --porcelain=v2 >.git/stash.reset && + test_must_be_empty .git/stash.reset && + native_stash_full_proof .git/index +} + +native_stash_create_policy_file () { + source=$1 && + test_write_lines staged >tracked && + git add -- tracked && + native_stash_full_proof .git/index && + test_write_lines "# baseline" "# harmless" >"$source" && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/create.before && + git stash create >.git/create.oid && + test_file_not_empty .git/create.oid && + git cat-file -e "$(cat .git/create.oid)^{commit}" && + native_stash_full_proof .git/index && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/create.after && + test_cmp .git/create.before .git/create.after && + test_grep "^1 M\\. .* tracked$" .git/create.after && + test_grep "^1 \\.M .* $source$" .git/create.after && + test_must_fail git rev-parse --verify refs/stash +} + +native_stash_staged_existing () { + test_write_lines staged >tracked && + git add -- tracked && + native_stash_full_proof .git/index && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/staged.before && + git stash push --staged -q -m staged && + native_stash_full_proof .git/index && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/staged.pushed && + test_must_be_empty .git/staged.pushed && + git stash apply --index -q stash@{0} && + native_stash_full_proof .git/index && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/staged.after && + test_cmp .git/staged.before .git/staged.after +} + +native_stash_staged_new () { + test_write_lines staged >new-file && + git add -- new-file && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/indexed.expect && + git stash push --staged -q -m indexed && + native_stash_full_proof .git/index && + git status --porcelain=v2 >.git/indexed.clean && + test_must_be_empty .git/indexed.clean && + git stash apply --index -q stash@{0} && + native_stash_full_proof .git/index && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/indexed.actual && + test_cmp .git/indexed.expect .git/indexed.actual && + test_write_lines new-file >.git/indexed.expect-paths && + git diff --cached --name-only >.git/indexed.actual-paths && + test_cmp .git/indexed.expect-paths .git/indexed.actual-paths +} + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ + 'native stash updates retain a full proof' ' + for mode in plain lfs + do + repo="native-stash-$mode" && + test_when_finished "stop_daemon_delete_repo $repo" && + native_stash_setup "$repo" "$mode" && + ( + cd "$repo" && + sane_unset GIT_TEST_SPLIT_INDEX && + native_stash_create_policy_file .gitattributes && + native_stash_reset && + native_stash_create_policy_file .gitignore && + native_stash_reset && + native_stash_staged_existing && + native_stash_reset && + native_stash_staged_new + ) || return 1 + done +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS,!MINGW \ + 'completed native replay retains a full proof with skip-hash index' ' + for mode in plain lfs + do + for location in primary linked + do + repo="native-replay-$mode-$location" && + linked="$repo-linked" && + if test "$location" = linked + then + test_when_finished \ + "stop_daemon_delete_linked_repo $repo $linked" + else + test_when_finished "stop_daemon_delete_repo $repo" + fi && + test_create_repo "$repo" && + ( + cd "$repo" && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir unrelated && + for i in $(test_seq 1 96) + do + d=$(( (i - 1) % 32 + 1 )) && + mkdir -p "unrelated/d$d" && + test_write_lines "stable-$i" \ + >"unrelated/d$d/file-$i" || return 1 + done && + test_write_lines base >conflict && + test_write_lines "# baseline" >.gitattributes && + test_write_lines "# baseline" >.gitignore && + git add unrelated conflict .gitattributes .gitignore && + git commit -qm base && + base=$(git rev-parse HEAD) && + git switch -qc upstream && + test_write_lines upstream >conflict && + git add conflict && + git commit -qm upstream && + git switch -qc topic "$base" && + test_write_lines prefix >prefix && + git add prefix && + git commit -qm prefix && + test_write_lines topic >conflict && + git add conflict && + git commit -qm topic && + git config core.autocrlf false && + git config core.trustctime true && + git config core.checkStat default && + git config core.untrackedCache true && + git config core.fsmonitor true && + git config index.skipHash true && + git config core.preloadIndex true && + git config core.preloadIndexBulk true && + if test "$mode" = lfs + then + git config filter.lfs.clean \ + "git-lfs clean -- %f" && + git config filter.lfs.smudge \ + "git-lfs smudge -- %f" && + git config filter.lfs.process \ + "git-lfs filter-process" && + git config filter.lfs.required true + fi && + if test "$location" = linked + then + git switch -q upstream && + git worktree add -q "../$linked" topic + fi + ) && + if test "$location" = linked + then + worktree="$linked" + else + worktree="$repo" + fi && + ( + cd "$worktree" && + sane_unset GIT_TEST_SPLIT_INDEX && + git fsmonitor--daemon status >/dev/null 2>&1 || + start_daemon && + index=$(git rev-parse --git-path index) && + scratch=$(git rev-parse --path-format=absolute \ + --git-path replay-test) && + mkdir -p "$scratch" && + topic=$(git rev-parse topic) && + upstream=$(git rev-parse upstream) && + test-tool chmtime -120 $(git ls-files) && + git -c core.fsmonitor=false update-index --refresh && + git update-index --fsmonitor && + git status --porcelain=v2 >"$scratch/prime.actual" && + test_must_be_empty "$scratch/prime.actual" && + native_stash_full_proof "$index" && + for replay in rebase rebase-merges cherry-pick + do + artifact="$scratch/$replay" && + if test "$replay" = cherry-pick + then + git reset --hard -q "$upstream" + else + git reset --hard -q "$topic" + fi && + git status --porcelain=v2 \ + >"$artifact.prime" && + test_must_be_empty "$artifact.prime" && + native_stash_full_proof "$index" && + case "$replay" in + rebase) + test_must_fail git rebase upstream + ;; + rebase-merges) + test_must_fail git rebase \ + --rebase-merges upstream + ;; + cherry-pick) + test_must_fail git cherry-pick "$topic" + ;; + esac && + if test "$replay" != cherry-pick + then + test_path_is_file prefix && + test "$(git log -1 --format=%s)" = prefix + fi && + test -n "$(git ls-files -u)" && + test_grep ! FSUC "$index" && + test_write_lines resolved >conflict && + GIT_TRACE2_EVENT="$artifact.add.trace" \ + git add conflict && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-failed \ + <"$artifact.add.trace" && + ! test_trace2_data index refresh/sum_lstat \ + "[1-9][0-9]*" \ + <"$artifact.add.trace" && + ! test_trace2_data index preload/sum_lstat \ + "[1-9][0-9]*" \ + <"$artifact.add.trace" && + if test "$replay" = cherry-pick + then + GIT_TRACE2_EVENT="$artifact.continue.trace" \ + GIT_EDITOR=true git cherry-pick --continue + else + GIT_TRACE2_EVENT="$artifact.continue.trace" \ + GIT_EDITOR=true git rebase --continue + fi && + if test "$replay" = cherry-pick + then + native_tracked_full_proof "$index" && + test_grep ! FSUC "$index" + else + native_stash_full_proof "$index" && + test_trace2_data fsmonitor \ + history/writer-proof-repaired 1 \ + <"$artifact.continue.trace" && + ! test_trace2_data fsmonitor \ + history/writer-proof-repaired 0 \ + <"$artifact.continue.trace" + fi && + cp "$index" "$artifact.index.before" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$artifact.status.trace" \ + git status --porcelain=v2 \ + >"$artifact.status" && + test_must_be_empty "$artifact.status" && + test_cmp "$artifact.index.before" "$index" && + test-tool dump-fsmonitor | tail -n 1 \ + >"$artifact.bitmap" && + test_grep ! + "$artifact.bitmap" && + ! test_trace2_data index refresh/sum_lstat \ + "[1-9][0-9]*" \ + <"$artifact.status.trace" && + ! test_trace2_data index preload/sum_lstat \ + "[1-9][0-9]*" \ + <"$artifact.status.trace" && + if test "$replay" = cherry-pick + then + test_trace2_data read_directory \ + directories-visited \ + "[1-9][0-9]*" \ + <"$artifact.status.trace" + else + ! test_trace2_data read_directory \ + directories-visited "[2-9]" \ + <"$artifact.status.trace" && + ! test_trace2_data read_directory \ + directories-visited \ + "[1-9][0-9][0-9]*" \ + <"$artifact.status.trace" + fi || return 1 + done + ) || return 1 + done + done +' + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'clean stash push preserves closed semantic history' ' test_when_finished "rm -rf stash-clean-history" && @@ -4544,7 +6344,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ' test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'dirty stash push drops closed semantic history' ' + 'dirty stash push preserves closed semantic history' ' test_when_finished "rm -rf stash-dirty-history" && test_create_repo stash-dirty-history && ( @@ -4566,17 +6366,19 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ git stash push >.git/stash && test_grep "Saved working directory" .git/stash && + test_grep FSUC .git/index && + test_grep FSCF .git/index && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ git status >.git/actual && test_grep "nothing to commit, working tree clean" .git/actual && - test_trace2_data fsmonitor config/coherent 0 \ + test_trace2_data fsmonitor config/coherent 1 \ <.git/status.trace ) ' test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,!MINGW \ - 'dirty stash cannot resurrect an invalidated external checkpoint' ' + 'dirty stash preserves current proof without restoring a checkpoint' ' test_when_finished "rm -rf stash-checkpoint-history" && test_create_repo stash-checkpoint-history && ( @@ -4604,13 +6406,13 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,!MINGW \ GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ git stash push >.git/stash && test_grep "Saved working directory" .git/stash && + test_grep FSUC .git/index && + test_grep FSCF .git/index && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ git status >.git/actual && test_grep "nothing to commit, working tree clean" .git/actual && - test_trace2_data fsmonitor history/external-proof-invalidated 1 \ - <.git/status.trace && - test_trace2_data fsmonitor config/coherent 0 \ + test_trace2_data fsmonitor config/coherent 1 \ <.git/status.trace && ! test_trace2_data fsmonitor history/external-restored 1 \ <.git/status.trace @@ -6001,7 +7803,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ' test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'hard reset to a different tree drops closed semantic history' ' + 'hard reset to a different tree preserves closed semantic history' ' test_when_finished "rm -rf reset-hard-changed" && test_create_repo reset-hard-changed && ( @@ -6027,8 +7829,10 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ git status >.git/actual && test_grep "nothing to commit, working tree clean" .git/actual && - test_trace2_data fsmonitor config/coherent 0 \ - <.git/status.trace + test_trace2_data fsmonitor config/coherent 1 \ + <.git/status.trace && + test_grep FSCF .git/index && + test_grep FSUC .git/index ) ' @@ -6584,6 +8388,24 @@ test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ ! test_trace2_data fsmonitor semantic/manifest-scan-count \ "[1-9][0-9]*" <.git/disabled-filter.trace && + GIT_CONFIG_COUNT=3 \ + GIT_CONFIG_KEY_0=filter.demo.clean GIT_CONFIG_VALUE_0= \ + GIT_CONFIG_KEY_1=filter.demo.process GIT_CONFIG_VALUE_1= \ + GIT_CONFIG_KEY_2=filter.demo.required GIT_CONFIG_VALUE_2=false \ + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + GIT_TRACE2_EVENT="$PWD/.git/disabled-clean-filter.trace" \ + git -c core.fsmonitor=true -c core.hooksPath=/dev/null \ + diff --no-textconv --no-ext-diff --submodule=short \ + --ignore-submodules=dirty --color \ + >.git/disabled-clean-filter.out && + test_must_be_empty .git/disabled-clean-filter.out && + test_cmp_bin .git/disabled-filter.index .git/index && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/disabled-clean-filter.trace && + ! test_trace2_data index preload/sum_lstat \ + "[1-9][0-9]*" <.git/disabled-clean-filter.trace && + test_write_lines "tracked filter=demo" >.git/info/attributes && GIT_TEST_PRELOAD_INDEX=1 \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ @@ -6602,8 +8424,10 @@ test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +for disable_smudge in true false +do test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'disabled filters cannot publish a proof for active filtered paths' ' + "disabled filters cannot publish a proof for active filtered paths (smudge: $disable_smudge)" ' test_when_finished "rm -rf disabled-filter-active-path" && test_create_repo disabled-filter-active-path && ( @@ -6627,14 +8451,16 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_trace2_data fsmonitor filter-scope/valid 1 \ <.git/prime.trace && + set -- -c filter.demo.clean= -c filter.demo.process= \ + -c filter.demo.required=false && + if test "$disable_smudge" = true + then + set -- "$@" -c filter.demo.smudge= + fi && test_write_lines raw >active.filtered && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ GIT_TEST_FSMONITOR_QUERY_PATH=active.filtered \ - git -c filter.demo.clean= \ - -c filter.demo.smudge= \ - -c filter.demo.process= \ - -c filter.demo.required=false \ - add active.filtered && + git "$@" add active.filtered && GIT_OPTIONAL_LOCKS=0 \ git -c core.fsmonitor=false -c core.untrackedCache=false \ status --porcelain=v2 --untracked-files=no \ @@ -6648,6 +8474,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_cmp .git/expected .git/actual ) ' +done test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'partial required-filter overrides cannot hide a missing clean helper' ' @@ -6685,8 +8512,10 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +for disable_smudge in true false +do test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'disabled filters cannot prime a reusable proof for active clean filters' ' + "disabled filters cannot prime a reusable proof for active clean filters (smudge: $disable_smudge)" ' test_when_finished "rm -rf disabled-filter-prime" && test_create_repo disabled-filter-prime && ( @@ -6704,14 +8533,16 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ git config core.fsmonitor true && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ git update-index --fsmonitor && + set -- -c filter.demo.clean= -c filter.demo.process= \ + -c filter.demo.required=false && + if test "$disable_smudge" = true + then + set -- "$@" -c filter.demo.smudge= + fi && GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ GIT_TRACE2_EVENT="$PWD/.git/disabled-prime.trace" \ - git -c filter.demo.clean= \ - -c filter.demo.smudge= \ - -c filter.demo.process= \ - -c filter.demo.required=false \ - status --porcelain=v2 --untracked-files=no \ + git "$@" status --porcelain=v2 --untracked-files=no \ >.git/disabled && test_must_be_empty .git/disabled && GIT_OPTIONAL_LOCKS=0 \ @@ -6727,6 +8558,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_cmp .git/expected .git/actual ) ' +done test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'unused configured filters preserve staged and dry-run history' ' @@ -7217,7 +9049,7 @@ test_expect_success UNTRACKED_CACHE,!MINGW,!CYGWIN \ test-tool chmtime =$mtime cached/hook-tracked EOF GIT_EDITOR=: \ - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCDC \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCDC \ GIT_TEST_FSMONITOR_QUERY_PATH=cached/hook-tracked \ GIT_TRACE2_EVENT="$PWD/.git/commit.trace" \ git commit --allow-empty --edit -m adoption && diff --git a/t/t7530-status-clean-sidecar.sh b/t/t7530-status-clean-sidecar.sh index 03fbcdc68a2246..855ddad7c700f5 100755 --- a/t/t7530-status-clean-sidecar.sh +++ b/t/t7530-status-clean-sidecar.sh @@ -233,6 +233,50 @@ start_issue_raced_status () { wait_for_fast_ready } +start_postwrite_raced_stash () { + repo=$1 && + ready=$TRASH_DIRECTORY/$repo.postwrite-ready && + resume=$TRASH_DIRECTORY/$repo.postwrite-resume && + race_trace=$TRASH_DIRECTORY/$repo.postwrite-trace && + status_pid= && + rm -f "$ready" "$resume" "$race_trace" && + : >"$ready" && + mkfifo "$resume" && + exec 9<>"$resume" && + { + test_env \ + GIT_TEST_STATUS_CLEAN_SIDECAR_POSTWRITE_BARRIER_READY="$ready" \ + GIT_TEST_STATUS_CLEAN_SIDECAR_POSTWRITE_BARRIER_RESUME="$resume" \ + GIT_TRACE2_EVENT="$race_trace" \ + git -C "$repo" stash push -q --keep-index -- tracked \ + >raced.actual 9>&- & + status_pid=$! + } && + wait_for_fast_ready +} + +start_postwrite_raced_status () { + repo=$1 && + ready=$TRASH_DIRECTORY/$repo.postwrite-ready && + resume=$TRASH_DIRECTORY/$repo.postwrite-resume && + race_trace=$TRASH_DIRECTORY/$repo.postwrite-trace && + status_pid= && + rm -f "$ready" "$resume" "$race_trace" && + : >"$ready" && + mkfifo "$resume" && + exec 9<>"$resume" && + { + test_env \ + GIT_TEST_STATUS_CLEAN_SIDECAR_POSTWRITE_BARRIER_READY="$ready" \ + GIT_TEST_STATUS_CLEAN_SIDECAR_POSTWRITE_BARRIER_RESUME="$resume" \ + GIT_TRACE2_EVENT="$race_trace" \ + bulk_status -C "$repo" status --porcelain=v2 \ + >raced.actual 9>&- & + status_pid=$! + } && + wait_for_fast_ready +} + stop_after_fast_fallback () { for i in $(test_seq 1 1000) do @@ -1007,10 +1051,35 @@ test_expect_success DURABLE_FSMONITOR,PERL_TEST_HELPERS \ hardlink-sidecar-repair-reissue.trace && test_grep "\"label\":\"do_write_index\"" \ hardlink-sidecar-repair-reissue.trace && - test_trace2_data status clean-proof/sidecar 1 \ - "hardlink-sidecar-recovery-$recovery_attempt.actual" && + test_grep "nothing to commit, working tree clean" \ + "hardlink-sidecar-recovery-$recovery_attempt.actual" || + return 1 + if test_trace2_data status clean-proof/sidecar 1 \ + <"hardlink-sidecar-recovery-$recovery_attempt.trace" + then + hardlink_sidecar_recovered=1 && + break + fi + done && + test "$hardlink_sidecar_recovered" = 1 + fi && ! test_cmp sidecar-hardlink-stale-stat/.git/index.before-repair \ sidecar-hardlink-stale-stat/.git/index && assert_clean_sidecar_hit sidecar-hardlink-stale-stat \ @@ -1727,7 +1796,7 @@ test_expect_success DURABLE_FSMONITOR \ ' test_expect_success DURABLE_FSMONITOR \ - 'exact status persists stat repairs before a sidecar' ' + 'exact status installs a sidecar after stat repairs' ' test_when_finished "stop_daemon external-stat-exact" && setup_repo external-stat-exact && git -C external-stat-exact config core.autocrlf false && @@ -1740,21 +1809,32 @@ test_expect_success DURABLE_FSMONITOR \ test_must_be_empty actual && test_trace2_data fsmonitor history/external-stored 1 \ actual && test_must_be_empty actual && - test_trace2_data fsmonitor history/external-stored 1 \ + test_trace2_data status clean-proof/hit 1 \ actual && test_must_be_empty actual && test_path_is_missing sidecar-shape/.git/index.csts && + bulk_status -C sidecar-shape status --porcelain=v2 \ + --untracked-files=normal --no-ahead-behind >actual && + test_must_be_empty actual && + test_path_is_missing sidecar-shape/.git/index.csts && test_env GIT_TRACE2_EVENT="$PWD/shape-branch.trace" \ bulk_status -C sidecar-shape \ @@ -3547,4 +3631,246 @@ test_expect_success PERL_TEST_HELPERS \ ) ' +test_expect_success DURABLE_FSMONITOR \ + 'scoped stash authenticates its final skipHash index' ' + test_when_finished "stop_daemon sidecar-scoped-stash" && + setup_repo sidecar-scoped-stash && + git -C sidecar-scoped-stash config core.autocrlf false && + git -C sidecar-scoped-stash config feature.manyFiles true && + issue_sidecar sidecar-scoped-stash && + assert_clean_sidecar_hit sidecar-scoped-stash \ + sidecar-scoped-stash scoped-stash-before && + test_write_lines changed >sidecar-scoped-stash/tracked && + GIT_TRACE2_EVENT="$PWD/scoped-stash.trace" \ + git -C sidecar-scoped-stash stash push -q -- tracked && + test_trace2_data fsmonitor history/own-write-source-recorded 1 \ + sidecar-rebase-continue/tracked && + git -C sidecar-rebase-continue add tracked && + git -C sidecar-rebase-continue commit -m upstream && + git -C sidecar-rebase-continue checkout -q topic && + test_write_lines topic >sidecar-rebase-continue/tracked && + git -C sidecar-rebase-continue add tracked && + git -C sidecar-rebase-continue commit -m topic && + test_must_fail git -C sidecar-rebase-continue rebase upstream && + test_path_is_file sidecar-rebase-continue/.git/index.csts && + test_write_lines resolved >sidecar-rebase-continue/tracked && + git -C sidecar-rebase-continue add tracked && + GIT_EDITOR=true \ + GIT_TRACE2_EVENT="$PWD/rebase-continue.trace" \ + git -C sidecar-rebase-continue rebase --continue && + test_trace2_data status clean-proof/writer-sidecar 1 \ + .git/index.lock + EOF + test_write_lines changed >sidecar-scoped-stash-lock/tracked && + GIT_TRACE2_EVENT="$PWD/scoped-stash-lock.trace" \ + git -C sidecar-scoped-stash-lock stash push -q \ + --keep-index -- tracked 2>scoped-stash-lock.err && + test_trace2_data status clean-proof/writer-sidecar-skip index-lock \ + sidecar-rebase-lock/tracked && + git -C sidecar-rebase-lock add tracked && + git -C sidecar-rebase-lock commit -m upstream && + git -C sidecar-rebase-lock checkout -q topic && + test_write_lines topic >sidecar-rebase-lock/tracked && + git -C sidecar-rebase-lock add tracked && + git -C sidecar-rebase-lock commit -m topic && + test_must_fail git -C sidecar-rebase-lock rebase upstream && + test_write_lines resolved >rebase-lock.expect && + cp rebase-lock.expect sidecar-rebase-lock/tracked && + git -C sidecar-rebase-lock add tracked && + write_script sidecar-rebase-lock/.git/hooks/post-rewrite <<-\EOF && + : >.git/index.lock + EOF + GIT_EDITOR=true \ + GIT_TRACE2_EVENT="$PWD/rebase-lock.trace" \ + git -C sidecar-rebase-lock rebase --continue \ + 2>rebase-lock.err && + test_trace2_data status clean-proof/writer-sidecar-skip index-lock \ + sidecar-postwrite-race/tracked && + start_postwrite_raced_stash sidecar-postwrite-race && + test_write_lines concurrent >sidecar-postwrite-race/tracked && + git -C sidecar-postwrite-race add tracked && + GIT_OPTIONAL_LOCKS=1 git -C sidecar-postwrite-race \ + status --porcelain=v2 >postwrite-race.expect && + test_file_not_empty postwrite-race.expect && + finish_fast_raced_status && + test_must_be_empty raced.actual && + test_trace2_data status clean-proof/writer-sidecar 0 \ + <"$race_trace" && + assert_clean_sidecar_fallback sidecar-postwrite-race \ + sidecar-postwrite-race postwrite-race-after +' + +test_expect_success DURABLE_FSMONITOR \ + 'status cannot bind a clean scan to a replaced postwrite index' ' + test_when_finished "stop_daemon sidecar-status-postwrite-race" && + test_when_finished "cleanup_fast_race" && + setup_repo sidecar-status-postwrite-race && + git -C sidecar-status-postwrite-race config core.autocrlf false && + prime_semantic_history sidecar-status-postwrite-race && + test-tool chmtime -60 sidecar-status-postwrite-race/tracked && + test-tool -C sidecar-status-postwrite-race \ + fsmonitor-client flush >status-postwrite-race.flush && + start_postwrite_raced_status sidecar-status-postwrite-race && + test_write_lines concurrent >sidecar-status-postwrite-race/tracked && + git -C sidecar-status-postwrite-race add tracked && + GIT_OPTIONAL_LOCKS=1 git -C sidecar-status-postwrite-race \ + status --porcelain=v2 >status-postwrite-race.expect && + test_file_not_empty status-postwrite-race.expect && + finish_fast_raced_status && + test_must_be_empty raced.actual && + test_trace2_data status clean-proof/miss postwrite-index-raced \ + <"$race_trace" && + assert_clean_sidecar_fallback sidecar-status-postwrite-race \ + sidecar-status-postwrite-race status-postwrite-race-after \ + --porcelain=v2 && + test_cmp status-postwrite-race.expect \ + status-postwrite-race-after.actual +' + +test_expect_success DURABLE_FSMONITOR \ + 'a registered linked worktree can issue and consume a clean sidecar' ' + test_when_finished "stop_daemon sidecar-linked-review" && + test_when_finished "stop_daemon sidecar-linked-main" && + setup_repo sidecar-linked-main && + git -C sidecar-linked-main config core.autocrlf false && + git -C sidecar-linked-main config core.untrackedCache true && + git -C sidecar-linked-main worktree add -q \ + -b sidecar-linked-topic ../sidecar-linked-review && + linked_gitdir=$(git -C sidecar-linked-review \ + rev-parse --absolute-git-dir) && + linked_index=$linked_gitdir/index && + test_path_is_missing "$linked_index.csts" && + test_env GIT_TRACE2_EVENT="$PWD/linked-short.trace" \ + bulk_status -C sidecar-linked-review status --short \ + >linked-short.actual && + test_must_be_empty linked-short.actual && + test_trace2_data status clean-proof/sidecar 1 \ + linked.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/linked-hit.trace" \ + git -C sidecar-linked-review status --porcelain=v2 \ + >linked.actual && + test_cmp linked.expect linked.actual && + test_cmp_bin linked.index "$linked_index" && + test_trace2_data status clean-proof/hit 1 linked-impostor.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/linked-impostor.trace" \ + git --git-dir="$linked_gitdir" \ + --work-tree="$PWD/sidecar-linked-impostor" \ + status --porcelain=v2 >linked-impostor.actual && + test_cmp linked-impostor.expect linked-impostor.actual && + test_cmp_bin linked.index "$linked_index" && + test_trace2_data status clean-proof/miss fast-repository-shape \ + tracked && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCCCCCCCCCCC \ GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ @@ -485,7 +486,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELP ! test_trace2_data fsmonitor \ apply/untracked-replacement-preserved 1 \ <.git/whole.trace && - ! scoped_stash_full_proof .git/index && + scoped_stash_full_proof .git/index && GIT_OPTIONAL_LOCKS=0 \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCCCCCC \ git status --porcelain=v2 >.git/actual && diff --git a/t/t7537-fsmonitor-cookie-compat.sh b/t/t7537-fsmonitor-cookie-compat.sh index 29d1eca87ec641..2c96eda96e1f6d 100755 --- a/t/t7537-fsmonitor-cookie-compat.sh +++ b/t/t7537-fsmonitor-cookie-compat.sh @@ -13,10 +13,15 @@ fi if test_have_prereq MACOS then fsmonitor_pre_cookie_token_prefix=dirmeta-v1.inode-v1. + fsmonitor_cookie_token_prefix=${fsmonitor_pre_cookie_token_prefix}cookie-v1.fence-v1. +elif test "$uname_s" = Linux +then + fsmonitor_pre_cookie_token_prefix=dirmeta-v1. + fsmonitor_cookie_token_prefix=cookie-v1.dirmeta-v1. else fsmonitor_pre_cookie_token_prefix= + fsmonitor_cookie_token_prefix=cookie-v1. fi -fsmonitor_cookie_token_prefix=${fsmonitor_pre_cookie_token_prefix}cookie-v1. stop_cookie_compat_daemon () { cookie_compat_repo=$1 && diff --git a/t/unit-tests/u-attr-manifest.c b/t/unit-tests/u-attr-manifest.c index e8d62bad0b5d07..6ea8467c2983bd 100644 --- a/t/unit-tests/u-attr-manifest.c +++ b/t/unit-tests/u-attr-manifest.c @@ -383,6 +383,60 @@ void test_attr_manifest__falls_back_to_index_source(void) #endif } +void test_attr_manifest__ignores_unmerged_non_attribute_entries(void) +{ +#if !SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + cl_skip(); +#else + const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA1]; + char *worktree = create_worktree(); + char source[] = "*.dat text\n"; + struct repository repo = { .worktree = worktree, .hash_algo = algo }; + struct index_state istate = INDEX_STATE_INIT(&repo); + struct worktree_attr_manifest_stats stats; + struct attr_manifest_cursor cursor; + struct attr_manifest_entry entry; + struct cache_entry *attributes; + struct strbuf manifest = STRBUF_INIT; + unsigned char hash[GIT_MAX_RAWSZ]; + int have_repository, ret; + + init_object_store(&repo, worktree); + CALLOC_ARRAY(istate.cache, 5); + istate.cache_alloc = istate.cache_nr = 5; + attributes = add_index_path(&istate, 0, GITATTRIBUTES_FILE, 0); + add_index_path(&istate, 1, "conflict", 1); + add_index_path(&istate, 2, "conflict", 2); + add_index_path(&istate, 3, "conflict", 3); + add_index_path(&istate, 4, "nested/file", 0); + cl_must_pass(odb_pretend_object( + repo.objects, source, strlen(source), OBJ_BLOB, + &attributes->oid)); + + have_repository = startup_info->have_repository; + startup_info->have_repository = 1; + ret = worktree_attr_manifest_build( + &istate, &manifest, hash, &stats); + startup_info->have_repository = have_repository; + cl_assert_equal_i(ret, 0); + cl_assert_equal_i(stats.candidates, 2); + cl_assert_equal_i(stats.worktree_sources, 0); + cl_assert_equal_i(stats.index_sources, 1); + cl_assert_equal_i(attr_manifest_cursor_init( + &cursor, manifest.buf, manifest.len, algo), 0); + cl_assert_equal_i(attr_manifest_cursor_next(&cursor, &entry), 1); + cl_assert_equal_i(entry.source, ATTR_MANIFEST_INDEX); + cl_assert_equal_i(entry.path_len, strlen(GITATTRIBUTES_FILE)); + cl_assert(!memcmp(entry.path, GITATTRIBUTES_FILE, entry.path_len)); + cl_assert_equal_i(attr_manifest_cursor_next(&cursor, &entry), 0); + + strbuf_release(&manifest); + release_index(&istate); + odb_free(repo.objects); + remove_worktree(worktree); +#endif +} + void test_attr_manifest__rejects_hardlinked_source_over_index(void) { #if !SEMANTIC_VERIFY_HAS_ANCHORED_OPEN @@ -803,7 +857,7 @@ void test_attr_manifest__thread_failure_completes_remaining_ranges(void) #endif } -void test_attr_manifest__builder_rejects_structural_indexes(void) +void test_attr_manifest__builder_accepts_unmerged_but_rejects_sparse_indexes(void) { #if !SEMANTIC_VERIFY_HAS_ANCHORED_OPEN cl_skip(); @@ -820,8 +874,8 @@ void test_attr_manifest__builder_rejects_structural_indexes(void) istate.cache_alloc = istate.cache_nr = 1; add_index_path(&istate, 0, "file", 1); cl_assert_equal_i(worktree_attr_manifest_build( - &istate, &manifest, hash, &stats), -1); - cl_assert_equal_i(manifest.len, 0); + &istate, &manifest, hash, &stats), 0); + cl_assert(attr_manifest_valid(manifest.buf, manifest.len, algo)); istate.cache[0]->ce_flags = create_ce_flags(0); istate.sparse_index = INDEX_COLLAPSED; cl_assert_equal_i(worktree_attr_manifest_build( diff --git a/t/unit-tests/u-clean-status-config.c b/t/unit-tests/u-clean-status-config.c index 58270e6250c732..169ef693d274cb 100644 --- a/t/unit-tests/u-clean-status-config.c +++ b/t/unit-tests/u-clean-status-config.c @@ -70,11 +70,25 @@ void test_clean_status_config__origin_only_affects_full_hash(void) void test_clean_status_config__command_transport_config_does_not_change_proof(void) { static const char *const ignored_keys[] = { + "protocol.version", + "fetch.uriprotocols", + "push.negotiate", + "http.https://Example.Invalid.extraheader", + "http.https://Example.Invalid.proactiveauth", + "http.https://Example.Invalid.sslverify", "credential.helper", "credential.https://Example/Team.helper", + "remote.origin.pushurl", + "remote.MixedCase.pushurl", "url.https://Proxy.Example/Team/.insteadof", "url.https://Proxy.Example/Team/.pushinsteadof", }; + static const char *const retained_command_keys[] = { + "push.default", + "remote.origin.url", + "remote.pushurl", + "remote.origin.fetch", + }; static const enum config_scope persistent_scopes[] = { CONFIG_SCOPE_GLOBAL, CONFIG_SCOPE_LOCAL, @@ -107,6 +121,11 @@ void test_clean_status_config__command_transport_config_does_not_change_proof(vo digest_one(&digest, ignored_keys[i], "transport", NULL); cl_assert(!hashes_equal(digest.hash, baseline.hash)); } + + for (size_t i = 0; i < ARRAY_SIZE(retained_command_keys); i++) { + digest_one(&digest, retained_command_keys[i], "transport", &ctx); + cl_assert(!hashes_equal(digest.hash, baseline.hash)); + } } void test_clean_status_config__command_preload_config_does_not_change_proof(void) @@ -376,7 +395,7 @@ void test_clean_status_config__presentation_does_not_join_filter_parts(void) } } -void test_clean_status_config__only_complete_disabled_filters_are_normalized(void) +void test_clean_status_config__only_complete_disabled_clean_filters_are_normalized(void) { static const char *const keys[] = { "filter.demo.clean", "filter.demo.smudge", @@ -384,6 +403,7 @@ void test_clean_status_config__only_complete_disabled_filters_are_normalized(voi }; static const char *const values[] = { "", "", "", "false" }; static const int algorithms[] = { GIT_HASH_SHA1, GIT_HASH_SHA256 }; + const unsigned clean_parts = (1U << 0) | (1U << 2) | (1U << 3); struct key_value_info kvi = KVI_INIT; struct config_context ctx = { .kvi = &kvi }; @@ -399,6 +419,8 @@ void test_clean_status_config__only_complete_disabled_filters_are_normalized(voi cl_assert(!baseline.normalized_filter_disable); for (unsigned mask = 0; mask < (1U << ARRAY_SIZE(keys)); mask++) { + int disabled_clean = (mask & clean_parts) == clean_parts; + clean_status_config_init(&digest, algo); kvi.scope = CONFIG_SCOPE_LOCAL; clean_status_config_add(&digest, keys[0], "configured", &ctx); @@ -410,10 +432,10 @@ void test_clean_status_config__only_complete_disabled_filters_are_normalized(voi } clean_status_config_final(&digest); cl_assert_equal_i(hasheq(digest.hash, baseline.hash, algo), - !mask || mask == 15); + !mask || disabled_clean); cl_assert_equal_i(digest.normalized_filter_disable, - mask == 15); - if (mask == 15) { + disabled_clean); + if (disabled_clean) { cl_assert(hasheq(digest.semantic_hash, baseline.semantic_hash, algo)); cl_assert(hasheq(digest.tracked_policy_hash, diff --git a/t/unit-tests/u-clean-status-manifest.c b/t/unit-tests/u-clean-status-manifest.c index 971b4bea973c0c..06ea0d08f110e0 100644 --- a/t/unit-tests/u-clean-status-manifest.c +++ b/t/unit-tests/u-clean-status-manifest.c @@ -199,11 +199,13 @@ void test_clean_status_manifest__invalidates_only_changed_scopes(void) clean_status_manifest_invalidate(&state); cl_assert(state.current_invalidated); istate.cache[0]->ce_flags = create_ce_flags(1); + istate.sparse_index = INDEX_COLLAPSED; cl_assert_equal_i(clean_status_manifest_refresh(&istate, &state), -1); cl_assert(!state.current_valid); cl_assert(state.global_fallback); cl_assert_equal_i(strbuf_cmp(&state.current, &old), 0); + istate.sparse_index = INDEX_EXPANDED; write_file(path.buf, "*.txt text\n"); for (size_t i = 0; i < istate.cache_nr; i++) { istate.cache[i]->ce_flags = CE_FSMONITOR_VALID | CE_UPTODATE; diff --git a/t/unit-tests/u-fsmonitor-response.c b/t/unit-tests/u-fsmonitor-response.c index 1b024d72f6dea5..0f9016ebe0c1ee 100644 --- a/t/unit-tests/u-fsmonitor-response.c +++ b/t/unit-tests/u-fsmonitor-response.c @@ -103,6 +103,8 @@ void test_fsmonitor_response__accepts_valid_builtin_responses(void) static const char directory_path[] = "nested/\0"; static const char both_paths[] = "merged\0merged/\0"; static const char case_path[] = "Tracked\0"; + static const char nested_git[] = + "builtin:12\0scratch/.git/file\0scratch/.git/\0"; check_response(delta, sizeof(delta) - 1, FSMONITOR_QUERY_DELTA, "builtin:2", delta + sizeof("builtin:2"), @@ -112,6 +114,10 @@ void test_fsmonitor_response__accepts_valid_builtin_responses(void) sizeof(global) - 1 - sizeof("builtin:3")); check_response(trivial, sizeof(trivial) - 1, FSMONITOR_QUERY_TRIVIAL, "builtin:4", NULL, 0); + check_response(nested_git, sizeof(nested_git) - 1, + FSMONITOR_QUERY_DELTA, "builtin:12", + nested_git + sizeof("builtin:12"), + sizeof(nested_git) - 1 - sizeof("builtin:12")); check_worktree_event(stale_root, strlen("/repo"), 0, 1, global_path, sizeof(global_path) - 1); diff --git a/tempfile.c b/tempfile.c index dc9ca4e6459c64..302f00b69aa9bc 100644 --- a/tempfile.c +++ b/tempfile.c @@ -322,16 +322,26 @@ int close_tempfile_gently(struct tempfile *tempfile) return err ? -1 : 0; } -int reopen_tempfile(struct tempfile *tempfile) +static int reopen_tempfile_with_flags(struct tempfile *tempfile, int flags) { if (!is_tempfile_active(tempfile)) BUG("reopen_tempfile called for an inactive object"); if (0 <= tempfile->fd) BUG("reopen_tempfile called for an open object"); - tempfile->fd = open(tempfile->filename.buf, O_WRONLY|O_TRUNC); + tempfile->fd = open(tempfile->filename.buf, flags | O_TRUNC); return tempfile->fd; } +int reopen_tempfile(struct tempfile *tempfile) +{ + return reopen_tempfile_with_flags(tempfile, O_WRONLY); +} + +int reopen_tempfile_for_readwrite(struct tempfile *tempfile) +{ + return reopen_tempfile_with_flags(tempfile, O_RDWR); +} + int rename_tempfile(struct tempfile **tempfile_p, const char *path) { struct tempfile *tempfile = *tempfile_p; diff --git a/tempfile.h b/tempfile.h index f571f3c609c04a..0f79228fc47f81 100644 --- a/tempfile.h +++ b/tempfile.h @@ -267,6 +267,13 @@ int close_tempfile_gently(struct tempfile *tempfile); */ int reopen_tempfile(struct tempfile *tempfile); +/* + * Like `reopen_tempfile()`, but open the temporary file for both reading and + * writing. This requires read permission in addition to the write permission + * required by the ordinary reopen operation. + */ +int reopen_tempfile_for_readwrite(struct tempfile *tempfile); + /* * Close the file descriptor and/or file pointer and remove the * temporary file associated with `tempfile`. It is a NOOP to call diff --git a/unpack-trees.c b/unpack-trees.c index 5079079129af9c..9adee9f640ba72 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -1914,6 +1914,50 @@ static int checkout_introduces_new_indexed_directory( return 0; } +static unsigned int checkout_invalidate_new_index_entries( + struct index_state *source, struct index_state *result, int safe_path) +{ + unsigned int invalidated = 0, source_pos = 0; + + for (unsigned int result_pos = 0; + result_pos < result->cache_nr; result_pos++) { + const struct cache_entry *entry = result->cache[result_pos]; + int cmp; + + while (source_pos < source->cache_nr) { + const struct cache_entry *source_entry = + source->cache[source_pos]; + + cmp = strcmp(source_entry->name, entry->name); + if (!cmp) + cmp = ce_stage(source_entry) - ce_stage(entry); + if (cmp >= 0) + break; + source_pos++; + } + if (source_pos < source->cache_nr) { + const struct cache_entry *source_entry = + source->cache[source_pos]; + + cmp = strcmp(source_entry->name, entry->name); + if (!cmp) + cmp = ce_stage(source_entry) - ce_stage(entry); + } else { + cmp = 1; + } + if (!cmp) + continue; + /* + * The result receives the source untracked cache after its entries + * are built. Replay additions now so a newly tracked directory is + * represented by invalid cache nodes rather than dropping FSUC. + */ + untracked_cache_invalidate_path(result, entry->name, safe_path); + invalidated++; + } + return invalidated; +} + /* * N-way merge "len" trees. Returns 0 on success, -1 on failure to manipulate the * resulting index, -2 on failure to reflect the changes to the work tree. @@ -2121,7 +2165,11 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options ret = check_updates(o, &o->internal.result) ? (-2) : 0; if (o->dst_index) { int history_transferred = 0; + int manifest_refresh_required = 0; int new_indexed_directory = 0; + int source_untracked_fully_valid = o->src_index->untracked && + o->src_index->untracked->root && + o->src_index->untracked->root->valid_recursive; if (!ret) { history_transferred = @@ -2129,20 +2177,49 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options &o->internal.result, o->src_index); if (!history_transferred && o->preserve_semantic_history) history_transferred = - clean_status_transfer_current_proof_if_semantically_same_index( - &o->internal.result, o->src_index); + clean_status_transfer_current_proof_after_checkout( + &o->internal.result, o->src_index, + &manifest_refresh_required); if (history_transferred && o->preserve_semantic_history) new_indexed_directory = checkout_introduces_new_indexed_directory( o->src_index, &o->internal.result); } move_index_extensions(&o->internal.result, o->src_index); + if (!ret && history_transferred && manifest_refresh_required) { + int manifest_refreshed = 0; + + /* + * The checkout has installed the new attribute sources. Refresh + * them before allowing the old provider boundary to authenticate + * the resulting index and paired untracked cache. + */ + if (clean_status_refresh_worktree_manifest( + &o->internal.result) < 0 || + clean_status_manifest_global_fallback( + &o->internal.result)) { + clean_status_invalidate_current_proof( + &o->internal.result); + history_transferred = 0; + } else { + manifest_refreshed = 1; + clean_status_mark_fsmonitor_config_valid( + &o->internal.result, + o->internal.result.fsmonitor_last_update); + history_transferred = + clean_status_has_current_full_fsmonitor_proof( + &o->internal.result); + } + trace2_data_intmax( + "fsmonitor", repo, + "history/checkout-manifest-refreshed", + manifest_refreshed); + } if (!ret && o->internal.backoff_transfer) clean_status_transfer_backoff_history( o->internal.backoff_transfer, &o->internal.result, o->src_index); if (!ret && o->preserve_semantic_history && history_transferred && - !new_indexed_directory && !o->src_index->sparse_index && !o->internal.result.sparse_index && !o->src_index->split_index && @@ -2169,10 +2246,19 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options o->internal.result.untracked->use_fsmonitor = 1; trace2_data_intmax("fsmonitor", repo, "history/untracked-paired-transfer", 1); + if (new_indexed_directory) + trace2_data_intmax( + "fsmonitor", repo, + "history/untracked-paired-new-directory-invalidated", + checkout_invalidate_new_index_entries( + o->src_index, + &o->internal.result, + !source_untracked_fully_valid)); } else if (new_indexed_directory) { trace2_data_intmax( "fsmonitor", repo, - "history/untracked-paired-new-directory-deferred", 1); + "history/untracked-paired-new-directory-deferred", + 1); } if (!ret) { if (git_env_bool("GIT_TEST_CHECK_CACHE_TREE", 0) && @@ -2407,6 +2493,30 @@ static void invalidate_ce_path(const struct cache_entry *ce, untracked_cache_invalidate_path(o->src_index, ce->name, 1); } +static void invalidate_added_ce_path(const struct cache_entry *ce, + struct unpack_trees_options *o) +{ + const char *basename; + int policy_file; + + if (!ce) + return; + cache_tree_invalidate_path(o->src_index, ce->name); + basename = find_last_dir_sep(ce->name); + basename = basename ? basename + 1 : ce->name; + policy_file = !fspathcmp(basename, ".gitattributes") || + !fspathcmp(basename, ".gitignore"); + if (o->preserve_untracked_history && policy_file) { + untracked_cache_invalidate_path(o->src_index, ce->name, 0); + trace2_data_intmax("fsmonitor", o->src_index->repo, + "checkout/untracked-policy-targeted", 1); + return; + } + clean_status_release_backoff_transfer(o->internal.backoff_transfer); + o->internal.backoff_transfer = NULL; + untracked_cache_invalidate_path(o->src_index, ce->name, 1); +} + static void invalidate_replaced_ce_path(const struct cache_entry *old, const struct cache_entry *new, struct unpack_trees_options *o) @@ -2414,6 +2524,19 @@ static void invalidate_replaced_ce_path(const struct cache_entry *old, const unsigned int unsafe_flags = CE_SKIP_WORKTREE | CE_NEW_SKIP_WORKTREE | CE_INTENT_TO_ADD | CE_CONFLICTED; const char *basename; + int policy_file; + + basename = find_last_dir_sep(old->name); + basename = basename ? basename + 1 : old->name; + policy_file = !fspathcmp(basename, ".gitattributes") || + !fspathcmp(basename, ".gitignore"); + if (o->preserve_untracked_history && policy_file) { + cache_tree_invalidate_path(o->src_index, old->name); + untracked_cache_invalidate_path(o->src_index, old->name, 0); + trace2_data_intmax("fsmonitor", o->src_index->repo, + "checkout/untracked-policy-targeted", 1); + return; + } if (o->internal.backoff_transfer && clean_status_backoff_transfer_entry_is_safe( @@ -2437,10 +2560,7 @@ static void invalidate_replaced_ce_path(const struct cache_entry *old, ((old->ce_mode & S_IFMT) != (new->ce_mode & S_IFMT))) goto rooted; - basename = find_last_dir_sep(old->name); - basename = basename ? basename + 1 : old->name; - if (!fspathcmp(basename, ".gitattributes") || - !fspathcmp(basename, ".gitignore")) + if (policy_file) goto rooted; cache_tree_invalidate_path(o->src_index, old->name); @@ -2740,7 +2860,7 @@ static int merged_entry(const struct cache_entry *ce, discard_cache_entry(merge); return -1; } - invalidate_ce_path(merge, o); + invalidate_added_ce_path(merge, o); if (submodule_from_ce(ce) && file_exists(ce->name)) { int ret = check_submodule_move_head(ce, NULL, diff --git a/unpack-trees.h b/unpack-trees.h index 105896d0e4c853..e08ef55db9765f 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -73,6 +73,7 @@ struct unpack_trees_options { dry_run, skip_cache_tree_update, preserve_semantic_history, + preserve_untracked_history, preserve_backoff_history; enum unpack_trees_reset_type reset; const char *prefix; diff --git a/worktree-attr-manifest.c b/worktree-attr-manifest.c index f22efb927a02fc..29a713f98eb45f 100644 --- a/worktree-attr-manifest.c +++ b/worktree-attr-manifest.c @@ -65,7 +65,7 @@ static int collect_candidates(struct index_state *istate, const char *slash = ce->name; const char *basename = ce->name; - if (ce_stage(ce) || S_ISSPARSEDIR(ce->ce_mode)) + if (S_ISSPARSEDIR(ce->ce_mode)) goto done; while ((slash = strchr(slash, '/')) != NULL) { size_t len = slash - ce->name; @@ -87,7 +87,14 @@ static int collect_candidates(struct index_state *istate, } basename = ++slash; } - if (!fspathcmp(basename, GITATTRIBUTES_FILE)) { + /* + * Unmerged entries still identify every ancestor directory that + * can contain an attribute source. Do not use an unmerged + * .gitattributes entry as the index source; callers separately + * reject an unmerged index when deciding whether to issue a proof. + */ + if (!ce_stage(ce) && + !fspathcmp(basename, GITATTRIBUTES_FILE)) { strbuf_reset(&candidate); strbuf_add(&candidate, ce->name, basename - ce->name); strbuf_addstr(&candidate, GITATTRIBUTES_FILE); diff --git a/wt-status.c b/wt-status.c index 061c75d51f1c67..2b26228b518ee4 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1157,7 +1157,8 @@ void wt_status_start_untracked_cache_preload(struct wt_status *s) if (s->untracked_cache_preload) BUG("untracked-cache preload already started"); - if (!use_optional_locks()) + /* An index writer can certify under the lock it already owns. */ + if (!use_optional_locks() && !s->proof_index_path) s->certify_clean_status = 0; wt_status_begin_attr_snapshot(s); /* Record the provider token before either filesystem traversal. */ @@ -1707,6 +1708,7 @@ static struct semantic_verify_proof *wt_status_prepare_semantic_verify( options.require_proof_epoch = 1; options.validate_filter_scope = clean_status_filter_scope_needs_validation(istate); + options.index_path = s->proof_index_path; options.attr_snapshot = s->attr_source_snapshot; trace2_region_enter("status", "semantic_verify", s->repo); ret = semantic_verify_prepare(istate, &options, &proof); @@ -1730,6 +1732,18 @@ static int wt_status_collect_untracked(struct wt_status *s) s, &s->untracked, &s->ignored); } +static int wt_status_clear_authenticated_untracked(struct wt_status *s) +{ + if (!s->untracked_from_token_closure && !s->untracked_from_preload) + return 0; + + string_list_clear(&s->untracked, 0); + string_list_clear(&s->ignored, 0); + s->untracked_from_token_closure = 0; + s->untracked_from_preload = 0; + return 1; +} + #define FSMONITOR_TOKEN_MAX_QUERIES 3 struct wt_status_token_closure { @@ -1910,8 +1924,12 @@ static void wt_status_refresh_for_token( struct index_state *istate = s->repo->index; if (!*epoch) - *epoch = clean_status_capture_proof_epoch( - istate, s->attr_source_snapshot, 0); + *epoch = s->proof_index_path ? + clean_status_capture_proof_epoch_at_path( + istate, s->attr_source_snapshot, 0, + s->proof_index_path) : + clean_status_capture_proof_epoch( + istate, s->attr_source_snapshot, 0); if (*epoch && use_bulk_provider) istate->preload_bulk_proof_epoch = *epoch; if (*epoch) { @@ -1951,8 +1969,12 @@ static int wt_status_close_ordinary_fsmonitor_token( !clean_status_manifest_global_fallback(istate) && !clean_status_worktree_manifest_needs_refresh(istate) && clean_status_index_entries_are_certifiable(istate) && - (scan_epoch = clean_status_capture_proof_epoch( - istate, s->attr_source_snapshot, 0)) && + (scan_epoch = s->proof_index_path ? + clean_status_capture_proof_epoch_at_path( + istate, s->attr_source_snapshot, 0, + s->proof_index_path) : + clean_status_capture_proof_epoch( + istate, s->attr_source_snapshot, 0)) && wt_status_stage_untracked(closure) && closure->staged_untracked.nr && !clean_status_worktree_manifest_needs_refresh(istate)) { @@ -2127,7 +2149,8 @@ wt_status_close_semantic_fsmonitor_token( return WT_STATUS_TOKEN_CLOSURE_FALLBACK; } closure->refresh_result |= refresh_index( - istate, closure->refresh_flags, &s->pathspec, NULL, NULL); + istate, closure->refresh_flags | REFRESH_IN_PROOF_EPOCH, + &s->pathspec, NULL, NULL); trace2_data_intmax("status", s->repo, "fsmonitor_token/semantic-closed", 1); if (!semantic_verify_proof_is_current(istate, *proof)) { @@ -2138,8 +2161,6 @@ wt_status_close_semantic_fsmonitor_token( } if (defer_untracked) { - int directory_delta_reused; - closure->untracked_ready = wt_status_stage_untracked(closure); closure->untracked_proof_complete = @@ -2152,13 +2173,19 @@ wt_status_close_semantic_fsmonitor_token( closure->queries >= FSMONITOR_TOKEN_MAX_QUERIES) return WT_STATUS_TOKEN_CLOSURE_FALLBACK; - /* A second query closes the subsequent untracked scan. */ - closure->queries++; + } + + /* A second query closes the ordinary refresh tail and untracked scan. */ + closure->queries++; + if (defer_untracked) clean_status_manifest_begin_directory_delta(istate, *proof); - result = wt_status_query_pending_token( - closure, wt_status_untracked_cache_valid(closure)); - directory_delta_reused = + result = wt_status_query_pending_token( + closure, defer_untracked ? + wt_status_untracked_cache_valid(closure) : 0); + if (defer_untracked) { + int directory_delta_reused = clean_status_manifest_end_directory_delta(istate); + if (result != FSMONITOR_TOKEN_CLEAN) { /* Only directory reuse adds an unobserved exclude risk. */ int reuse_semantic_subtrees = @@ -2191,12 +2218,20 @@ wt_status_close_semantic_fsmonitor_token( return WT_STATUS_TOKEN_CLOSURE_RETRY; return WT_STATUS_TOKEN_CLOSURE_FALLBACK; } - if (!semantic_verify_proof_is_current(istate, *proof)) { - wt_status_reset_attr_snapshot_if_changed(s); - wt_status_discard_semantic_verify( - s, proof, "closure-drift"); - return WT_STATUS_TOKEN_CLOSURE_FALLBACK; - } + } else if (result != FSMONITOR_TOKEN_CLEAN) { + closure->untracked_ready = 0; + closure->untracked_proof_complete = !closure->require_untracked; + wt_status_discard_semantic_verify( + s, proof, "token-reset"); + if (fsmonitor_token_requires_rescan(result)) + return WT_STATUS_TOKEN_CLOSURE_RETRY; + return WT_STATUS_TOKEN_CLOSURE_FALLBACK; + } + if (!semantic_verify_proof_is_current(istate, *proof)) { + wt_status_reset_attr_snapshot_if_changed(s); + wt_status_discard_semantic_verify( + s, proof, "closure-drift"); + return WT_STATUS_TOKEN_CLOSURE_FALLBACK; } if (semantic_verify_accept_filter_scope(istate, *proof) < 0) { wt_status_discard_semantic_verify( @@ -2251,6 +2286,11 @@ static int wt_status_close_fsmonitor_token( int preserve_untracked, token_accepted = 0; refresh_fsmonitor(istate); + if (require_untracked && fsmonitor_has_pending_token(istate) && + wt_status_clear_authenticated_untracked(s)) + trace2_data_intmax( + "status", s->repo, + "untracked/replaced-authenticated-snapshot", 1); preserve_untracked = !require_untracked && s->show_untracked_files == SHOW_NO_UNTRACKED_FILES && !s->show_ignored_mode && !s->pathspec.nr && @@ -2399,6 +2439,22 @@ int wt_status_refresh_index(struct wt_status *s, wt_status_begin_attr_snapshot(s); refresh_fsmonitor(istate); proof = wt_status_prepare_semantic_verify(s, refresh_flags); + if (proof && s->proof_index_path) { + struct semantic_verify_stats stats; + + semantic_verify_get_stats(proof, &stats); + if (stats.active_filters) { + s->certify_active_filter_found = 1; + trace2_data_intmax( + "status", s->repo, + "semantic_verify/writer-repair-filtered", 1); + semantic_verify_proof_clear(proof); + git_attr_invalidate_all(); + if (fsmonitor_has_pending_token(istate)) + fsmonitor_reject_pending_token(istate); + return 0; + } + } ret = wt_status_close_fsmonitor_token( s, proof, refresh_flags, require_untracked, 0); istate->preload_bulk_recovery_requested = 0; @@ -2411,6 +2467,435 @@ int wt_status_refresh_index(struct wt_status *s, return ret; } +static int fsmonitor_proof_repair_is_eligible( + struct repository *repo, int allow_untracked_bootstrap) +{ + struct index_state *istate = repo->index; + const char *test_sequence = + getenv("GIT_TEST_FSMONITOR_QUERY_SEQUENCE"); + + if ((test_sequence && *test_sequence && + !getenv("GIT_TEST_FSMONITOR_ALLOW_PROOF_REPAIR_SEQUENCE")) || + !fstat_is_reliable() || + getenv(INDEX_ENVIRONMENT) || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || getenv(DB_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC || + (!allow_untracked_bootstrap && + (!istate->untracked || !istate->untracked->root)) || + (!istate->fsmonitor_token_valid && + !fsmonitor_pending_token_from_provider(istate))) + return 0; + return 1; +} + +static int locked_index_entries_are_certifiable( + const struct index_state *istate) +{ + const unsigned int allowed = CE_UPTODATE | CE_ADDED | CE_HASHED | + CE_FSMONITOR_VALID | CE_NEW_SKIP_WORKTREE | CE_UPDATE_IN_BASE; + const struct stat_data empty = { 0 }; + + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (S_ISGITLINK(ce->ce_mode) || ce_stage(ce) || + ce_intent_to_add(ce) || ce_skip_worktree(ce) || + (ce->ce_flags & CE_VALID) || + !(ce->ce_flags & CE_FSMONITOR_VALID) || + (ce->ce_flags & ~allowed) || + !memcmp(&ce->ce_stat_data, &empty, sizeof(empty))) + return 0; + } + return 1; +} + +static int locked_index_entries_have_stat_data( + const struct index_state *istate) +{ + const struct stat_data empty = { 0 }; + + for (size_t i = 0; i < istate->cache_nr; i++) + if (!memcmp(&istate->cache[i]->ce_stat_data, + &empty, sizeof(empty))) + return 0; + return 1; +} + +static void prepare_uncertified_index_entries_for_refresh( + struct index_state *istate) +{ + const struct stat_data empty = { 0 }; + int changed = 0; + size_t refreshes = 0; + + /* + * An owned worktree update can leave new entries process-locally + * up-to-date without a provider-valid bit, or transfer provider-valid + * bits to entries without stat data. A provider reset has no prior event + * interval from which to report those writes. Force the repair refresh + * to stat only those entries before certifying them. + */ + for (size_t i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + int zero_stat = + !memcmp(&ce->ce_stat_data, &empty, sizeof(empty)); + + if (!zero_stat && (ce->ce_flags & CE_FSMONITOR_VALID)) + continue; + refreshes++; + if (zero_stat && (ce->ce_flags & CE_FSMONITOR_VALID)) { + changed = 1; + ce->ce_flags &= ~CE_FSMONITOR_VALID; + } + ce->ce_flags &= ~CE_UPTODATE; + } + if (changed) + istate->cache_changed |= FSMONITOR_CHANGED; + trace2_data_intmax("fsmonitor", istate->repo, + "history/writer-entry-refreshes", refreshes); +} + +static int has_repairable_fsmonitor_proof_candidate( + struct repository *repo) +{ + struct index_state *istate = repo->index; + + if (clean_status_has_current_full_fsmonitor_proof(istate)) + return 1; + /* + * A provider reset can arrive while the index is being read. The live + * proof is then deliberately invalid, but a complete persistent proof + * and its paired untracked cache remain authenticated candidates for a + * full refresh. Preserve that pre-reset authority across an owned + * worktree update; the repair path still revalidates every component + * before publishing a new proof. + */ + return clean_status_has_persistent_fsmonitor_semantic_history(istate) && + clean_status_fsmonitor_semantic_baseline_pending(istate) && + !clean_status_fsmonitor_strong_mismatch(istate) && + !clean_status_filter_scope_needs_validation(istate) && + !clean_status_manifest_global_fallback(istate) && + istate->untracked && istate->untracked->root && + istate->untracked->fsmonitor_revalidation; +} + +int wt_status_fsmonitor_proof_needs_repair(struct repository *repo) +{ + struct index_state *istate = repo->index; + + if (!fsmonitor_proof_repair_is_eligible(repo, 0)) + return 0; + return fsmonitor_pending_token_from_provider(istate) || + !istate->fsmonitor_untracked_valid || + !istate->untracked->root->valid_recursive; +} + +static void release_repair_status(struct wt_status *status) +{ + wt_status_collect_free_buffers(status); + string_list_clear(&status->change, 1); + string_list_clear(&status->untracked, 0); + string_list_clear(&status->ignored, 0); + free(status->branch); +} + +static int repair_fsmonitor_proof( + struct repository *repo, const char *index_path, int force_refresh, + struct wt_status *repaired_status) +{ + struct index_state *istate = repo->index; + struct wt_status local_status; + struct wt_status *status = repaired_status ? + repaired_status : &local_status; + int no_pending, paired_untracked, valid_root, certifiable_index; + int full_proof, repaired = 0; + + if (!fsmonitor_proof_repair_is_eligible( + repo, !!repaired_status)) + return 0; + if (!force_refresh && !wt_status_fsmonitor_proof_needs_repair(repo)) + return 1; + + wt_status_prepare(repo, status); + status->proof_index_path = index_path; + status->allow_clean_status_shortcuts = 1; + status->certify_clean_status = 1; + wt_status_start_untracked_cache_preload(status); + /* There is no subsequent diff to consume deferred bulk results. */ + wt_status_refresh_index( + status, + REFRESH_QUIET | REFRESH_UNMERGED, + 1); + if (status->certify_active_filter_found) + goto done; + /* A policy-file update can invalidate the cache during token closure. */ + wt_status_collect_untracked(status); + /* Bind the rebuilt cache and refreshed entries to a fresh token. */ + if (fsmonitor_reopen_token(istate)) + wt_status_refresh_index( + status, + REFRESH_QUIET | REFRESH_UNMERGED, + 1); + if (status->certify_active_filter_found) + goto done; + untracked_cache_recompute_fsmonitor_valid_recursive(istate->untracked); + no_pending = !fsmonitor_has_pending_token(istate); + paired_untracked = istate->fsmonitor_untracked_valid && + istate->fsmonitor_last_update && + istate->fsmonitor_untracked_token && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token); + valid_root = istate->untracked && istate->untracked->root && + istate->untracked->root->valid_recursive; + certifiable_index = clean_status_index_entries_are_certifiable(istate) || + (index_path && locked_index_entries_are_certifiable(istate)); + full_proof = clean_status_has_current_full_fsmonitor_proof(istate); + repaired = !status->certify_untracked_scan_failed && no_pending && + paired_untracked && valid_root && certifiable_index && full_proof; + if (repaired_status && repaired) { + struct object_id oid; + + status->status_format = STATUS_FORMAT_PORCELAIN_V2; + status->show_branch = 0; + status->is_initial = + repo_get_oid(repo, status->reference, &oid) ? 1 : 0; + if (!status->is_initial) + oidcpy(&status->oid_commit, &oid); + wt_status_collect_changes_worktree(status); + if (status->is_initial) + wt_status_collect_changes_initial(status); + else + wt_status_collect_changes_index(status); + } + +done: + if (!repaired_status || !repaired) + release_repair_status(status); + trace2_data_intmax("fsmonitor", repo, + "history/writer-proof-repaired", repaired); + return repaired; +} + +int wt_status_repair_fsmonitor_proof(struct repository *repo) +{ + return repair_fsmonitor_proof(repo, NULL, 0, NULL); +} + +int wt_status_prepare_fsmonitor_proof_for_worktree_update( + struct repository *repo) +{ + if (!has_repairable_fsmonitor_proof_candidate(repo)) + return 0; + if (clean_status_has_current_full_fsmonitor_proof(repo->index)) + return 1; + if (!wt_status_fsmonitor_proof_needs_repair(repo) || + !wt_status_repair_fsmonitor_proof(repo)) + return 0; + return clean_status_has_current_full_fsmonitor_proof(repo->index); +} + +int wt_status_repair_fsmonitor_proof_at_path( + struct repository *repo, const char *index_path) +{ + if (!index_path || !*index_path) + return 0; + return repair_fsmonitor_proof(repo, index_path, 0, NULL); +} + +static int repair_fsmonitor_proof_after_update( + struct repository *repo, struct lock_file *lock, int had_full_proof, + int allow_manifest_refresh, struct wt_status *repaired_status) +{ + const char *proof_index_path; + int repaired; + + if (!had_full_proof) { + trace2_data_string("fsmonitor", repo, + "history/writer-repair-skip", "no-history"); + return 0; + } + if (repaired_status && !repo->index->untracked) + add_untracked_cache(repo->index); + if (!fsmonitor_proof_repair_is_eligible( + repo, !!repaired_status)) { + trace2_data_string("fsmonitor", repo, + "history/writer-repair-skip", "ineligible"); + return 0; + } + if (!allow_manifest_refresh && + clean_status_worktree_manifest_needs_refresh(repo->index)) { + trace2_data_string("fsmonitor", repo, + "history/writer-repair-skip", "manifest"); + return 0; + } + if (!repaired_status && + !wt_status_fsmonitor_proof_needs_repair(repo) && + clean_status_has_current_full_fsmonitor_proof(repo->index) && + locked_index_entries_have_stat_data(repo->index)) + return 1; + + /* Consume paths written by this process before publishing its index. */ + fsmonitor_refresh_after_worktree_update(repo->index); + prepare_uncertified_index_entries_for_refresh(repo->index); + if (write_locked_index(repo->index, lock, PROVISIONAL_LOCK)) + return -1; + proof_index_path = get_lock_file_path(lock); + repaired = repair_fsmonitor_proof( + repo, proof_index_path, 1, repaired_status); + /* Receipt preparation needs read access, but the write must not. */ +#ifdef __APPLE__ + /* Preserve the read-write reopen historically used on Apple platforms. */ + if (reopen_lock_file_for_readwrite(lock) < 0 && + reopen_lock_file(lock) < 0) + return -1; +#else + if (reopen_lock_file(lock) < 0) + return -1; +#endif + return repaired; +} + +int wt_status_repair_fsmonitor_proof_after_worktree_update( + struct repository *repo, struct lock_file *lock, int had_full_proof) +{ + return repair_fsmonitor_proof_after_update( + repo, lock, had_full_proof, 0, NULL); +} + +int wt_status_prime_fsmonitor_proof_after_worktree_update( + struct repository *repo, struct lock_file *lock) +{ + struct wt_status status = { 0 }; + int repaired; + + prepare_repo_settings(repo); + if (repo->settings.core_untracked_cache != UNTRACKED_CACHE_WRITE || + fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC) + return 0; + /* + * A newly created index has no history to repair. Certify its checkout + * under the writer's existing lock, including a complete untracked scan + * and a closing provider query, before the first index is committed. + */ + clean_status_attach_config(repo->index); + if (!repo->index->fsmonitor_token_valid) + repo->index->fsmonitor_has_run_once = 0; + refresh_fsmonitor(repo->index); + repaired = repair_fsmonitor_proof_after_update( + repo, lock, 1, 1, &status); + if (repaired > 0) + release_repair_status(&status); + return repaired; +} + +int wt_status_repair_fsmonitor_proof_after_update_with_sidecar( + struct repository *repo, struct lock_file *lock, int had_full_proof, + const struct clean_status_config_digest *config) +{ + struct wt_status status = { 0 }; + struct clean_status_index_snapshot scanned_index = { .fd = -1 }; + struct clean_status_index_write_receipt written_index = + CLEAN_STATUS_INDEX_WRITE_RECEIPT_INIT; + int installed = 0; + int repaired = repair_fsmonitor_proof_after_update( + repo, lock, had_full_proof, 1, &status); + + if (repaired <= 0) + return repaired; + if (clean_status_write_index_after_provisional( + repo->index, lock, COMMIT_LOCK | SKIP_IF_UNCHANGED, + &written_index)) { + clean_status_index_write_receipt_release(&written_index); + release_repair_status(&status); + return -1; + } + if (clean_status_sidecar_postwrite_test_barrier()) + goto done; + clean_status_index_adopt_write_receipt(repo->index, &written_index); + if (clean_status_index_snapshot_pin(&scanned_index, repo->index)) + goto done; + /* Rebind the repaired proof to the index inode just committed. */ + discard_index(repo->index); + if (repo_read_index(repo) < 0) + goto done; + if (repo_hold_locked_index(repo, lock, 0) < 0) + goto done; + if (clean_status_issue_sidecar( + &status, config, lock, &scanned_index, 1)) + installed = 1; + else + rollback_lock_file(lock); + +done: + clean_status_index_write_receipt_release(&written_index); + clean_status_index_snapshot_release(&scanned_index); + release_repair_status(&status); + trace2_data_intmax("status", repo, + "clean-proof/writer-sidecar", installed); + return repaired; +} + +int wt_status_clean_sidecar_present(struct repository *repo) +{ + struct stat st; + char *path = xstrfmt("%s.csts", repo_get_index_file(repo)); + int present = !lstat(path, &st) && S_ISREG(st.st_mode) && + st.st_nlink == 1; + + free(path); + return present; +} + +void wt_status_reissue_clean_sidecar_after_worktree_update( + struct repository *repo, int had_full_proof, + const struct clean_status_config_digest *config) +{ + struct lock_file lock = LOCK_INIT; + int repaired; + + /* A missing replacement sidecar safely falls back to normal status. */ + if (repo_hold_locked_index(repo, &lock, 0) < 0) { + trace2_data_string("status", repo, + "clean-proof/writer-sidecar-skip", + "index-lock"); + return; + } + /* A child command may have replaced the canonical index inode. */ + discard_index(repo->index); + if (repo_read_index(repo) < 0) { + rollback_lock_file(&lock); + trace2_data_string("status", repo, + "clean-proof/writer-sidecar-skip", + "index-read"); + return; + } + if (!repo->index->fsmonitor_token_valid) { + repo->index->fsmonitor_has_run_once = 0; + refresh_fsmonitor(repo->index); + } + repaired = wt_status_repair_fsmonitor_proof_after_update_with_sidecar( + repo, &lock, had_full_proof, config); + if (repaired < 0) { + rollback_lock_file(&lock); + trace2_data_string("status", repo, + "clean-proof/writer-sidecar-skip", + "proof-repair"); + return; + } + if (!repaired) + rollback_lock_file(&lock); +} + +int wt_status_repair_fsmonitor_proof_after_index_update( + struct repository *repo, struct lock_file *lock, int had_full_proof) +{ + return repair_fsmonitor_proof_after_update( + repo, lock, had_full_proof, 1, NULL); +} + static void wt_status_release_attr_snapshot(struct wt_status *s) { if (s->attr_source_snapshot) @@ -2425,11 +2910,7 @@ void wt_status_invalidate_refresh(struct wt_status *s) struct index_state *istate = s->repo->index; s->tracked_from_fsmonitor = 0; - if (s->untracked_from_token_closure) { - string_list_clear(&s->untracked, 0); - string_list_clear(&s->ignored, 0); - s->untracked_from_token_closure = 0; - } + wt_status_clear_authenticated_untracked(s); wt_status_release_exclude_proof(s); wt_status_release_attr_snapshot(s); if (!s->pathspec.nr && !istate->split_index && diff --git a/wt-status.h b/wt-status.h index 5106c02384dda4..98686afcb5b56c 100644 --- a/wt-status.h +++ b/wt-status.h @@ -8,6 +8,8 @@ struct repository; struct stat; +struct lock_file; +struct clean_status_config_digest; struct attr_source_snapshot; struct exclude_source_proof; struct wt_status_exclude_context; @@ -151,6 +153,7 @@ struct wt_status { unsigned untracked_from_preload : 1; unsigned bulk_update_index_stat : 1; const char *index_file; + const char *proof_index_path; FILE *fp; const char *prefix; struct string_list change; @@ -166,6 +169,7 @@ struct wt_status { unsigned attr_snapshot_failed : 1; unsigned certify_exclude_digest_valid : 1; unsigned certify_untracked_scan_failed : 1; + unsigned certify_active_filter_found : 1; }; size_t wt_status_locate_end(const char *s, size_t len); @@ -182,6 +186,30 @@ void wt_status_start_untracked_cache_preload(struct wt_status *s); int wt_status_refresh_index(struct wt_status *s, unsigned int refresh_flags, int require_untracked); +/* + * Re-establish a complete, writable fsmonitor proof after a provider reset or + * an owned worktree update invalidates part of an authenticated proof. + */ +int wt_status_repair_fsmonitor_proof(struct repository *repo); +int wt_status_repair_fsmonitor_proof_at_path( + struct repository *repo, const char *index_path); +int wt_status_prepare_fsmonitor_proof_for_worktree_update( + struct repository *repo); +int wt_status_fsmonitor_proof_needs_repair(struct repository *repo); +int wt_status_repair_fsmonitor_proof_after_worktree_update( + struct repository *repo, struct lock_file *lock, int had_full_proof); +/* The caller owns the mandatory index lock for the initial checkout. */ +int wt_status_prime_fsmonitor_proof_after_worktree_update( + struct repository *repo, struct lock_file *lock); +int wt_status_repair_fsmonitor_proof_after_update_with_sidecar( + struct repository *repo, struct lock_file *lock, int had_full_proof, + const struct clean_status_config_digest *config); +void wt_status_reissue_clean_sidecar_after_worktree_update( + struct repository *repo, int had_full_proof, + const struct clean_status_config_digest *config); +int wt_status_repair_fsmonitor_proof_after_index_update( + struct repository *repo, struct lock_file *lock, int had_full_proof); +int wt_status_clean_sidecar_present(struct repository *repo); void wt_status_invalidate_refresh(struct wt_status *s); int wt_status_certified_excludes_digest( struct wt_status *s, struct object_id *digest,