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/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..97a9fec1a89476 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1906,6 +1906,8 @@ struct repository *repo UNUSED) int repository_inputs_changed = 0; int sidecar_provider_reset = 0; int reissue_after_write = 0; + int issue_exact_after_write = 0; + int exact_after_write_candidate = 0; int save_history_after_write = 0; int deferred_scoped_history = 0; int guarded_scoped_history_source = 0; @@ -2204,6 +2206,17 @@ struct repository *repo UNUSED) 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) { @@ -2251,17 +2264,23 @@ struct repository *repo UNUSED) !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; + } rollback_lock_file(&index_lock); } - if (reissue_after_write && + if ((reissue_after_write || issue_exact_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, + reissue_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); } diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c index 1c53a5af4dd6df..fe61dfb700b88a 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. */ @@ -238,6 +253,37 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie( &state->main_lock, &ts); if (err == ETIMEDOUT && cookie->result == FCIR_INIT) { +#ifdef __APPLE__ + struct timeval rescue_now; + + /* + * FSEvents may be healthy but late enough that its normal + * delivery misses our bounded wait. Flush only after that + * wait expires, so successful queries pay no extra cost. + * The asynchronous flush cannot block on the listener callback, + * which needs main_lock to publish the cookie. + */ + trace_printf_key(&trace_fsmonitor, + "cookie_wait: requesting FSEvents flush after initial timeout"); + fsm_listen__flush_async(state); + + /* + * Give the listener one more bounded interval to deliver and + * publish the cookie rather than falling back to a full index + * scan. A broken provider still reaches the existing error + * path instead of hanging a client indefinitely. + */ + gettimeofday(&rescue_now, NULL); + ts.tv_sec = rescue_now.tv_sec + 1; + ts.tv_nsec = rescue_now.tv_usec * 1000; + err = 0; + while (cookie->result == FCIR_INIT && !err) + err = pthread_cond_timedwait(&state->cookies_cond, + &state->main_lock, + &ts); +#endif + } + if (err == ETIMEDOUT && cookie->result == FCIR_INIT) { trace_printf_key(&trace_fsmonitor, "cookie_wait timed out"); cookie->result = FCIR_ERROR; @@ -390,6 +436,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 +457,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 +532,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 +565,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 +679,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 +689,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 +698,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 +714,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; @@ -692,6 +835,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, @@ -760,7 +915,11 @@ 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" +#endif +#if FSMONITOR_IPC_HAS_DIR_METADATA FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" +#endif +#ifdef __APPLE__ FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n" #endif ; @@ -909,13 +1068,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 +1125,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 +1161,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); @@ -1056,6 +1225,60 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, 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; static int handle_client(void *data, @@ -1379,6 +1602,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); diff --git a/builtin/merge.c b/builtin/merge.c index a7fcf6d8080e57..82a276074329d6 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1472,7 +1472,7 @@ 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); diff --git a/builtin/reset.c b/builtin/reset.c index 0d8660fa3b9f46..0f28886d02fa6a 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,9 @@ 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); repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); @@ -580,6 +589,10 @@ int cmd_reset(int argc, !the_repository->index->cache_changed && !hook_exists(the_repository, "post-index-change")) write_flags |= SKIP_IF_UNCHANGED; + 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..87006050bcc409 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -6,6 +6,7 @@ #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" @@ -29,6 +30,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 +399,44 @@ 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) +{ + struct lock_file lock = LOCK_INIT; + int repaired; + + if (!had_full_proof) + return 0; + if (repo_hold_locked_index(the_repository, &lock, + LOCK_REPORT_ON_ERROR) < 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); + 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 ? + 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); + return error(_("could not repair index")); + } + 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 +718,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 +729,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 +837,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)) + ret = -1; if (!quiet) { struct child_process cp = CHILD_PROCESS_INIT; @@ -1706,6 +1752,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 +1763,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)) + ret = -1; if (!ret) printf_ln("%s", oid_to_hex(&info.w_commit)); @@ -1736,6 +1789,7 @@ 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_full_proof; struct lock_file index_lock = LOCK_INIT; struct stash_info info = STASH_INFO_INIT; struct strbuf patch = STRBUF_INIT; @@ -1772,6 +1826,8 @@ 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); if (!include_untracked && ps->nr) { char *ps_matched = xcalloc(ps->nr, 1); @@ -1804,12 +1860,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")); @@ -2002,6 +2052,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)) { + ret = -1; + goto done; + } goto done; } diff --git a/clean-status-config.c b/clean-status-config.c index e6523e193ccaaa..5c52bbca1c240c 100644 --- a/clean-status-config.c +++ b/clean-status-config.c @@ -90,8 +90,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; 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-history.c b/clean-status-history.c index 97a501424c0f58..434ff3f7b9c8f2 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,13 +2019,172 @@ 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; if (!current_proof_is_writable(src) || src->repo != dst->repo || src->split_index || dst->split_index || @@ -2034,6 +2194,8 @@ int clean_status_transfer_current_proof_if_semantically_same_index( !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 +2212,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 +2262,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 +2274,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.c b/clean-status-index.c index 50cb6d870d8ecc..858fd90160cfa7 100644 --- a/clean-status-index.c +++ b/clean-status-index.c @@ -215,11 +215,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 +234,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 +247,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 +283,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..4cb60df0dedd40 100644 --- a/clean-status-index.h +++ b/clean-status-index.h @@ -71,12 +71,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.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..dacaaf28debfc3 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); @@ -127,6 +131,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 +170,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..b749012c959ca8 100644 --- a/compat/fsmonitor/fsm-darwin-gcc.h +++ b/compat/fsmonitor/fsm-darwin-gcc.h @@ -97,6 +97,7 @@ CFRunLoopRef CFRunLoopGetCurrent(void); extern CFStringRef kCFRunLoopDefaultMode; void FSEventStreamSetDispatchQueue(FSEventStreamRef stream, dispatch_queue_t q); unsigned char FSEventStreamStart(FSEventStreamRef stream); +FSEventStreamEventId FSEventStreamFlushAsync(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..5ebc70902553c5 100644 --- a/compat/fsmonitor/fsm-listen-darwin.c +++ b/compat/fsmonitor/fsm-listen-darwin.c @@ -29,6 +29,7 @@ #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" @@ -57,6 +58,8 @@ struct fsm_listen_data unsigned int stream_scheduled:1; unsigned int stream_started:1; + unsigned int test_cookie_delayed:1; + unsigned long test_cookie_delay_ms; }; static void log_flags_set(const char *path, const FSEventStreamEventFlags flag) @@ -453,6 +456,11 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, } 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); string_list_clear(&cookie_list, 0); strbuf_release(&tmp); @@ -511,6 +519,8 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) CALLOC_ARRAY(data, 1); state->listen_data = data; + data->test_cookie_delay_ms = git_env_ulong( + "GIT_TEST_FSMONITOR_COOKIE_DELAY_MS", 0); data->cfsr_event_path_key = CFStringCreateWithCString( NULL, "path", kCFStringEncodingUTF8); @@ -586,6 +596,11 @@ void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) pthread_mutex_unlock(&data->dq_lock); } +void fsm_listen__flush_async(struct fsmonitor_daemon_state *state) +{ + FSEventStreamFlushAsync(state->listen_data->stream); +} + void fsm_listen__loop(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; 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..d58e01243b9ad9 100644 --- a/compat/fsmonitor/fsm-listen.h +++ b/compat/fsmonitor/fsm-listen.h @@ -38,6 +38,11 @@ void fsm_listen__dtor(struct fsmonitor_daemon_state *state); */ void fsm_listen__loop(struct fsmonitor_daemon_state *state); +#ifdef __APPLE__ +/* Request delivery of all FSEvents that occurred before this call. */ +void fsm_listen__flush_async(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..400187f2f7de43 100644 --- a/compat/simple-ipc/ipc-unix-socket.c +++ b/compat/simple-ipc/ipc-unix-socket.c @@ -194,8 +194,13 @@ 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; 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); @@ -208,8 +213,7 @@ static int ipc_client_send_command_to_connection_1( } 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/compat/simple-ipc/ipc-win32.c b/compat/simple-ipc/ipc-win32.c index f1b4124d3ae8df..8edc138fd2f99d 100644 --- a/compat/simple-ipc/ipc-win32.c +++ b/compat/simple-ipc/ipc-win32.c @@ -240,8 +240,13 @@ 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; 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); @@ -256,8 +261,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/fsmonitor-ipc.c b/fsmonitor-ipc.c index 5ddd7503ca60f4..49ff20be9d0563 100644 --- a/fsmonitor-ipc.c +++ b/fsmonitor-ipc.c @@ -449,10 +449,13 @@ static int server_supports_required_capabilities(void) ret = ret && has_capability(&answer, FSMONITOR_IPC_HARDLINK_QUERY_VERSION) && - has_capability(&answer, - FSMONITOR_IPC_DIR_METADATA_CAPABILITY) && has_capability(&answer, FSMONITOR_IPC_HARDLINK_INODE_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 +466,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 +853,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; @@ -991,7 +991,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..3800b51c897d9c 100644 --- a/fsmonitor-ipc.h +++ b/fsmonitor-ipc.h @@ -21,6 +21,27 @@ struct repository; #define FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX "cookie-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 +#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..1725b06d34363f 100644 --- a/fsmonitor.c +++ b/fsmonitor.c @@ -1795,6 +1795,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/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/pkt-line.c b/pkt-line.c index 3fc3e9ea7059be..c2323e920fd496 100644 --- a/pkt-line.c +++ b/pkt-line.c @@ -353,6 +353,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..c7130f28bb0f87 100644 --- a/pkt-line.h +++ b/pkt-line.h @@ -78,6 +78,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 +89,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..62616b02be58d3 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,6 +360,8 @@ 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); diff --git a/read-cache.c b/read-cache.c index 6c1270306438ba..551823805f5b1b 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) @@ -3931,11 +3934,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; } 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..295df2eb383a23 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" @@ -752,7 +754,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 +762,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 +799,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 +5358,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 +5408,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 +5580,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)); 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-simple-ipc.c b/t/helper/test-simple-ipc.c index a7e4750fc9be2a..43a18d0f2213b4 100644 --- a/t/helper/test-simple-ipc.c +++ b/t/helper/test-simple-ipc.c @@ -179,8 +179,10 @@ 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" #endif ; @@ -188,7 +190,11 @@ static int app__fsmonitor_capability_superset( 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,15 +202,18 @@ 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 -#endif + FSMONITOR_IPC_PLATFORM_TOKEN_PREFIX "test-pre-cookie:0"; const char *token; const char *query; @@ -227,9 +236,12 @@ static int app__fsmonitor_capability_superset( 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_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; diff --git a/t/t7519-status-fsmonitor.sh b/t/t7519-status-fsmonitor.sh index 9454c11695077f..6b04326cc81cc2 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 ) ' @@ -2518,6 +2534,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" && @@ -2744,7 +2891,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 +2948,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 +2978,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 +3009,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,33 +3028,663 @@ 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 ) ' @@ -2900,6 +3705,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" \ @@ -2964,6 +3770,269 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +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" && + test_trace2_data fsmonitor config/coherent 1 \ + <"$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 && + 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 + ) +' + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'expired add preserves untracked candidates until revalidation' ' test_when_finished "rm -rf pending-untracked-revalidation pending-untracked-hostile" && @@ -5164,9 +6233,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 +6272,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" \ diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh index 04c188ac50132a..b1dc6d4bb517b1 100755 --- a/t/t7527-builtin-fsmonitor.sh +++ b/t/t7527-builtin-fsmonitor.sh @@ -59,6 +59,14 @@ test_lazy_prereq HARDLINKS ' ln hardlink-a hardlink-b ' +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 FOREIGN_FSMONITOR_GIT ' test -x /opt/homebrew/bin/git && /opt/homebrew/bin/git version @@ -74,13 +82,20 @@ 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_cookie_token_prefix=${fsmonitor_pre_cookie_token_prefix}cookie-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 +103,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= && @@ -154,6 +176,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 +275,228 @@ test_expect_success 'implicit daemon start' ' test_must_fail git -C test_implicit fsmonitor--daemon status ' +test_expect_success MACOS 'rescue a delayed FSEvents cookie after timeout' ' + test_when_finished "stop_daemon_delete_repo test_delayed_cookie" && + + git init test_delayed_cookie && + ( + GIT_TEST_FSMONITOR_COOKIE_DELAY_MS=1200 && + export GIT_TEST_FSMONITOR_COOKIE_DELAY_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 "cookie_wait: requesting FSEvents flush after initial timeout" \ + delayed-cookie.trace && + test_grep "cookie-seen:" delayed-cookie.trace && + test_grep ! "cookie_wait timed out$" delayed-cookie.trace && + test_must_be_empty error +' + +test_expect_success MACOS 'fall back when a delayed FSEvents cookie stays late' ' + test_when_finished "stop_daemon_delete_repo test_lost_cookie" && + + git init test_lost_cookie && + ( + GIT_TEST_FSMONITOR_COOKIE_DELAY_MS=2500 && + export GIT_TEST_FSMONITOR_COOKIE_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 "cookie_wait: requesting FSEvents flush after initial timeout" \ + lost-cookie.trace && + test_grep "cookie_wait timed out$" lost-cookie.trace && + test_must_be_empty error +' + +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. # @@ -1719,6 +2020,32 @@ 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 && + 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 && + + 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 MACOS 'implicit daemon reuses the invoking Git executable' ' test_create_repo same-executable-spawn && mkdir fake-exec-path && @@ -1829,7 +2156,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 +2299,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 +2532,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 +2550,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 +2606,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 +2642,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 +3347,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 \ @@ -4507,6 +5016,317 @@ 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" && + 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' ' + 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 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 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" && + 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 && + 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_EDITOR=true git cherry-pick --continue + else + 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" + 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 +5364,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 +5386,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 +5426,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 +6823,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 +6849,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 ) ' @@ -7217,7 +8041,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..c67531a7a5fcd0 100755 --- a/t/t7530-status-clean-sidecar.sh +++ b/t/t7530-status-clean-sidecar.sh @@ -1727,7 +1727,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 +1740,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 \ diff --git a/t/t7533-status-scoped-stash.sh b/t/t7533-status-scoped-stash.sh index 67bf189c6e7d31..5201d798d46fd1 100755 --- a/t/t7533-status-scoped-stash.sh +++ b/t/t7533-status-scoped-stash.sh @@ -472,7 +472,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELP ' test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ - 'whole-worktree stash retains its deliberate proof invalidation' ' + 'whole-worktree stash preserves its authenticated proof' ' test_when_finished "rm -rf scoped-stash-whole" && scoped_stash_setup scoped-stash-whole && ( @@ -485,7 +485,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..e3ae98de78ea3d 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. +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..ae3984cf084419 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) 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/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..83497130d6548f 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1707,6 +1707,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); @@ -1910,8 +1911,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 +1956,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 +2136,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 +2148,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 +2160,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 +2205,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( @@ -2399,6 +2421,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 +2449,184 @@ int wt_status_refresh_index(struct wt_status *s, return ret; } +static int fsmonitor_proof_repair_is_eligible(struct repository *repo) +{ + struct index_state *istate = repo->index; + const char *test_sequence = + getenv("GIT_TEST_FSMONITOR_QUERY_SEQUENCE"); + + if ((test_sequence && *test_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 || + !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; +} + +int wt_status_fsmonitor_proof_needs_repair(struct repository *repo) +{ + struct index_state *istate = repo->index; + + if (!fsmonitor_proof_repair_is_eligible(repo)) + return 0; + return fsmonitor_pending_token_from_provider(istate) || + !istate->fsmonitor_untracked_valid || + !istate->untracked->root->valid_recursive; +} + +static int repair_fsmonitor_proof( + struct repository *repo, const char *index_path, int force_refresh) +{ + struct index_state *istate = repo->index; + struct wt_status status; + int no_pending, paired_untracked, valid_root, certifiable_index; + int full_proof, repaired = 0; + + if (!fsmonitor_proof_repair_is_eligible(repo)) + 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->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; + +done: + 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); + 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); +} + +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); +} + +static int repair_fsmonitor_proof_after_update( + struct repository *repo, struct lock_file *lock, int had_full_proof, + int allow_manifest_refresh) +{ + const char *proof_index_path; + int repaired; + + if (!had_full_proof || !fsmonitor_proof_repair_is_eligible(repo) || + (!allow_manifest_refresh && + clean_status_worktree_manifest_needs_refresh(repo->index))) + return 0; + if (!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); + 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); + if (reopen_lock_file(lock) < 0) + return -1; + 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); +} + +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); +} + static void wt_status_release_attr_snapshot(struct wt_status *s) { if (s->attr_source_snapshot) diff --git a/wt-status.h b/wt-status.h index 5106c02384dda4..3d44db8c24c6ff 100644 --- a/wt-status.h +++ b/wt-status.h @@ -8,6 +8,7 @@ struct repository; struct stat; +struct lock_file; struct attr_source_snapshot; struct exclude_source_proof; struct wt_status_exclude_context; @@ -151,6 +152,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 +168,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 +185,18 @@ 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_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); +int wt_status_repair_fsmonitor_proof_after_index_update( + struct repository *repo, struct lock_file *lock, int had_full_proof); void wt_status_invalidate_refresh(struct wt_status *s); int wt_status_certified_excludes_digest( struct wt_status *s, struct object_id *digest,