diff --git a/attr-fingerprint.c b/attr-fingerprint.c index f614464f553331..3f6fbf18ba4007 100644 --- a/attr-fingerprint.c +++ b/attr-fingerprint.c @@ -215,6 +215,97 @@ int attr_fingerprint_repository(struct repository *repo, return ret; } +static int legacy_absent_path_is_stable(const char *path) +{ + struct path_namespace_snapshot *before = NULL, *after = NULL; + struct strbuf normalized = STRBUF_INIT; + char *absolute = NULL; + int stable = 0; + + if (!path) + return 0; + absolute = absolute_pathdup(path); + strbuf_addstr(&normalized, absolute); + if (!strbuf_normalize_path(&normalized) && + !path_namespace_capture(normalized.buf, &before) && + !path_namespace_target_present(before) && + !path_namespace_capture(normalized.buf, &after) && + !path_namespace_target_present(after) && + path_namespace_equal(before, after)) + stable = 1; + free(absolute); + path_namespace_clear(before); + path_namespace_clear(after); + strbuf_release(&normalized); + return stable; +} + +static void legacy_absent_source_hash( + const struct attr_fingerprint_source *sources, + const char *system_path, const struct git_hash_algo *algo, + unsigned char *hash) +{ + struct git_hash_ctx ctx; + uint32_t value; + + git_hash_init(&ctx, algo); + hash_optional_cstring(&ctx, "attribute-source-content-v1"); + put_be32(&value, ATTR_SOURCE_SNAPSHOT_NR); + hash_length_delimited(&ctx, &value, sizeof(value)); + for (size_t i = 0; i < ATTR_SOURCE_SNAPSHOT_NR; i++) { + const char *path = i == ATTR_SOURCE_SNAPSHOT_SYSTEM ? + system_path : sources[i].path; + + hash_optional_cstring(&ctx, path); + put_be32(&value, sources[i].enabled); + hash_length_delimited(&ctx, &value, sizeof(value)); + if (!sources[i].enabled || !path) + continue; + put_be32(&value, 0); + hash_length_delimited(&ctx, &value, sizeof(value)); + } + git_hash_final(hash, &ctx); +} + +int attr_fingerprint_matches_legacy_absent_sources( + struct repository *repo, const unsigned char *expected) +{ + static const char shipped_system_path[] = "//etc/gitattributes"; + struct attr_fingerprint_source sources[ATTR_SOURCE_SNAPSHOT_NR]; + struct attr_fingerprint before, after; + unsigned char legacy[GIT_MAX_RAWSZ]; + char *info_attributes = NULL; + int matches = 0; + + if (!expected || !fstat_is_reliable() || + repository_sources(repo, sources, &info_attributes) || + !sources[ATTR_SOURCE_SNAPSHOT_SYSTEM].enabled || + attr_fingerprint_repository(repo, &before) || + before.sources_present) + goto done; + legacy_absent_source_hash( + sources, sources[ATTR_SOURCE_SNAPSHOT_SYSTEM].path, + repo->hash_algo, legacy); + if (!memcmp(legacy, expected, repo->hash_algo->rawsz)) { + matches = 1; + } else if (legacy_absent_path_is_stable(shipped_system_path)) { + legacy_absent_source_hash( + sources, shipped_system_path, repo->hash_algo, legacy); + matches = !memcmp(legacy, expected, repo->hash_algo->rawsz); + } + if (!matches || attr_fingerprint_repository(repo, &after) || + after.sources_present || + memcmp(before.content_hash, after.content_hash, + repo->hash_algo->rawsz) || + memcmp(before.namespace_hash, after.namespace_hash, + repo->hash_algo->rawsz)) + matches = 0; + +done: + free(info_attributes); + return matches; +} + int attr_source_snapshot_repository(struct repository *repo, struct attr_source_snapshot **result) { diff --git a/attr-fingerprint.h b/attr-fingerprint.h index 69cd5bf79adc28..5d2bedf93d38a4 100644 --- a/attr-fingerprint.h +++ b/attr-fingerprint.h @@ -31,6 +31,8 @@ int attr_fingerprint_sources( const struct git_hash_algo *algo, struct attr_fingerprint *result); int attr_fingerprint_repository(struct repository *repo, struct attr_fingerprint *result); +int attr_fingerprint_matches_legacy_absent_sources( + struct repository *repo, const unsigned char *expected); int attr_source_snapshot_repository(struct repository *repo, struct attr_source_snapshot **result); int attr_source_snapshot_matches_repository( diff --git a/attr-manifest.c b/attr-manifest.c index 46aed49a430050..6c835f26cb410c 100644 --- a/attr-manifest.c +++ b/attr-manifest.c @@ -1,4 +1,5 @@ #include "git-compat-util.h" +#include "attr.h" #include "attr-manifest.h" #include "environment.h" #include "read-cache-ll.h" @@ -45,6 +46,86 @@ static int attr_manifest_entry_equal(const struct attr_manifest_entry *a, !memcmp(a->hash, b->hash, algo->rawsz); } +static void release_parsed_attr(struct match_attr *match) +{ + for (size_t i = 0; i < match->num_attr; i++) { + const char *value = match->state[i].setto; + + if (!ATTR_TRUE(value) && !ATTR_FALSE(value) && + !ATTR_UNSET(value)) + free((char *)value); + } + free(match); +} + +static int normalize_conversion_attributes( + const char *data, size_t len, struct strbuf *normalized) +{ + struct strbuf line = STRBUF_INIT; + size_t offset = 0; + int lineno = 0, ret = -1; + + if ((!data && len) || memchr(data, '\0', len)) + goto done; + while (offset < len) { + const char *start = data + offset; + const char *newline = memchr(start, '\n', len - offset); + const char *trimmed; + struct match_attr *match; + size_t line_len = newline ? + (size_t)(newline - start) + 1 : len - offset; + int display_only; + + strbuf_reset(&line); + strbuf_add(&line, start, line_len); + trimmed = line.buf + strspn(line.buf, " \t\r\n"); + lineno++; + if (!*trimmed || *trimmed == '#') { + strbuf_add(normalized, start, line_len); + offset += line_len; + continue; + } + if (starts_with(trimmed, ATTRIBUTE_MACRO_PREFIX)) + goto done; + match = parse_attr_line(line.buf, GITATTRIBUTES_FILE, lineno, 0); + if (!match || match->is_macro || !match->num_attr) { + if (match) + release_parsed_attr(match); + goto done; + } + display_only = 1; + for (size_t i = 0; i < match->num_attr; i++) + if (strcmp(git_attr_name(match->state[i].attr), + "linguist-generated")) + display_only = 0; + release_parsed_attr(match); + if (!display_only) + strbuf_add(normalized, start, line_len); + offset += line_len; + } + ret = 0; + +done: + strbuf_release(&line); + return ret; +} + +int attr_manifest_only_linguist_generated_changed( + const char *old_data, size_t old_len, + const char *new_data, size_t new_len) +{ + struct strbuf old = STRBUF_INIT, new = STRBUF_INIT; + int equal = 0; + + if (!normalize_conversion_attributes(old_data, old_len, &old) && + !normalize_conversion_attributes(new_data, new_len, &new)) + equal = old.len == new.len && + !memcmp(old.buf, new.buf, old.len); + strbuf_release(&old); + strbuf_release(&new); + return equal; +} + void attr_manifest_writer_init(struct attr_manifest_writer *writer, struct strbuf *buf, const struct git_hash_algo *algo) diff --git a/attr-manifest.h b/attr-manifest.h index a38acccc224832..6718afd5c897f9 100644 --- a/attr-manifest.h +++ b/attr-manifest.h @@ -55,5 +55,8 @@ int attr_manifest_for_each_changed(const void *old_data, size_t old_len, const void *new_data, size_t new_len, const struct git_hash_algo *algo, attr_manifest_change_fn fn, void *data); +int attr_manifest_only_linguist_generated_changed( + const char *old_data, size_t old_len, + const char *new_data, size_t new_len); #endif /* ATTR_MANIFEST_H */ diff --git a/builtin/add.c b/builtin/add.c index b13ba9580cf896..e7871643c6b97b 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -511,11 +511,13 @@ int cmd_add(int argc, * add/remove decision below. */ 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 && !chmod_arg && !include_sparse && !ignore_add_errors) { preserve_add_history = 1; flags |= ADD_CACHE_TRACK_CLEAN_HISTORY; + clean_status_enable_external_history(repo); clean_status_set_config_digest(repo, &clean_digest); } diff --git a/builtin/checkout.c b/builtin/checkout.c index a724d7a6018526..fbc324b8b7dee7 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -671,9 +671,11 @@ static int checkout_paths(const struct checkout_opts *opts, !opts->merge && !opts->writeout_stage; if ((opts->checkout_worktree && !opts->source_tree && !opts->merge && !opts->writeout_stage) || - preserve_source_tree_history) + preserve_source_tree_history) { + clean_status_enable_external_history(the_repository); clean_status_set_config_digest(the_repository, &opts->clean_digest); + } if (repo_read_index_preload(the_repository, &opts->pathspec, 0) < 0) return error(_("index file corrupt")); @@ -916,9 +918,11 @@ static int merge_working_tree(const struct checkout_opts *opts, * proof only after it proves that the rebuilt index is identical. */ if (opts->discard_changes || - (!opts->merge && !opts->new_orphan_branch)) + (!opts->merge && !opts->new_orphan_branch)) { + clean_status_enable_external_history(the_repository); clean_status_set_config_digest(the_repository, &opts->clean_digest); + } if (repo_read_index_preload(the_repository, NULL, 0) < 0) { rollback_lock_file(&lock_file); return error(_("index file corrupt")); diff --git a/builtin/commit.c b/builtin/commit.c index 4f8fcb5b8ad18d..41b9ea47fb4062 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -45,9 +45,11 @@ #include "sparse-index.h" #include "mailmap.h" #include "help.h" +#include "hook.h" #include "commit-reach.h" #include "commit-graph.h" #include "pretty.h" +#include "trace2.h" #include "trailer.h" static const char * const builtin_commit_usage[] = { @@ -1641,21 +1643,35 @@ static int print_clean_sidecar(struct wt_status *s, const char *prefix) return 1; } -static int clean_status_sidecar_has_stale_index(struct repository *repo) +static int clean_status_sidecar_needs_reissue(struct repository *repo) { struct clean_status_sidecar_record record = CLEAN_STATUS_SIDECAR_RECORD_INIT; struct clean_status_index_snapshot index = { .fd = -1 }; - int stale = 0; + char *path = xstrfmt("%s.csts", repo->index_file); + struct stat st; + int safe_existing = !lstat(path, &st) && + S_ISREG(st.st_mode) && st.st_nlink == 1 && + is_path_owned_by_current_user(path, NULL); + int reissue = 0; if (!clean_status_sidecar_load( repo->index_file, repo->hash_algo, &record)) - stale = !!clean_status_sidecar_pin_source( - repo->index_file, &record.sidecar, - repo->hash_algo, &index); + reissue = safe_existing && + (!!clean_status_sidecar_pin_source( + repo->index_file, &record.sidecar, + repo->hash_algo, &index) || + record.sidecar.hardlink_nr > 0); + else { + if (lstat(path, &st) < 0) + reissue = errno == ENOENT; + else + reissue = safe_existing; + } + free(path); clean_status_index_snapshot_release(&index); clean_status_sidecar_record_release(&record); - return stale; + return reissue; } int cmd_status(int argc, @@ -1676,7 +1692,9 @@ struct repository *repo UNUSED) int normal_clean_query; int reusable_clean_query; int normal_has_head; - int stale_clean_sidecar = 0; + int reissue_clean_sidecar = 0; + int reissue_after_write = 0; + int save_history_after_write = 0; struct object_id oid; static struct option builtin_status_options[] = { OPT__VERBOSE(&verbose, N_("be verbose")), @@ -1797,9 +1815,10 @@ struct repository *repo UNUSED) return 0; } } - if (normal_clean_query && use_optional_locks()) - stale_clean_sidecar = - clean_status_sidecar_has_stale_index(the_repository); + if (normal_clean_query && use_optional_locks() && + clean_status_identity_is_durable()) + reissue_clean_sidecar = + clean_status_sidecar_needs_reissue(the_repository); if (status_format != STATUS_FORMAT_PORCELAIN && status_format != STATUS_FORMAT_PORCELAIN_V2) { @@ -1812,7 +1831,8 @@ struct repository *repo UNUSED) clean_status_capture_external_history_source( the_repository->index); if (normal_clean_query && use_optional_locks() && - (stale_clean_sidecar || + clean_status_identity_is_durable() && + (reissue_clean_sidecar || clean_status_external_history_was_restored( the_repository->index))) s.certify_clean_status = 1; @@ -1854,10 +1874,18 @@ struct repository *repo UNUSED) clean_status_external_history_was_restored( the_repository->index); int external_saved = 0; + int persist_restored_boundary = 0; int preserve_entry_changes = (!external_restored && (the_repository->index->cache_changed & CE_ENTRY_CHANGED)) || the_repository->index->fsmonitor_untracked_must_persist; + int deferred_history = preserve_entry_changes && + !external_restored && + clean_status_has_recovered_tracked_stat( + the_repository->index); + int preserve_history_witness = external_restored && + clean_status_external_history_needs_witness_preservation( + the_repository->index); /* * Publish resumable history before the physical clean proof. @@ -1870,8 +1898,24 @@ struct repository *repo UNUSED) * entry repair durable. Restored checkpoints stay no-spill * for foreign index writers. */ - external_saved = clean_status_save_external_history( - the_repository->index); + if (!deferred_history && !preserve_history_witness) + external_saved = clean_status_save_external_history( + the_repository->index); + else if (deferred_history && + !hook_exists(the_repository, "post-index-change")) + save_history_after_write = 1; + if (external_restored && !external_saved && + clean_status_external_history_owns_index( + the_repository->index) && + has_racy_timestamp(the_repository->index)) { + persist_restored_boundary = 1; + trace2_data_intmax("fsmonitor", the_repository, + "history/external-racy-index-persisted", 1); + } + reissue_after_write = normal_clean_query && + reissue_clean_sidecar && preserve_entry_changes && + !external_restored && !persist_restored_boundary && + !hook_exists(the_repository, "post-index-change"); if (the_repository->index->fsmonitor_legacy_untracked_fallback && !preserve_entry_changes && !external_saved) { @@ -1883,25 +1927,46 @@ struct repository *repo UNUSED) &s, &clean_digest, &index_lock, 0)) fd = -1; else if (!preserve_entry_changes && + !persist_restored_boundary && (external_restored || external_saved)) { rollback_lock_file(&index_lock); fd = -1; } } else if (!preserve_entry_changes && + !persist_restored_boundary && normal_clean_query && - (external_restored || - (stale_clean_sidecar && external_saved)) && + (external_restored || reissue_clean_sidecar) && clean_status_issue_sidecar( &s, &clean_digest, &index_lock, 1)) { fd = -1; } else if (!preserve_entry_changes && + !persist_restored_boundary && (external_restored || external_saved)) { rollback_lock_file(&index_lock); fd = -1; } } - if (0 <= fd) + if (0 <= fd) { repo_update_index_if_able(the_repository, &index_lock); + if (save_history_after_write && + !hook_exists(the_repository, "post-index-change") && + repo_hold_locked_index(the_repository, &index_lock, 0) >= 0) { + if (clean_status_save_external_history( + the_repository->index)) + trace2_data_intmax("fsmonitor", the_repository, + "history/external-postwrite-stored", 1); + rollback_lock_file(&index_lock); + } + if (reissue_after_write && + repo_hold_locked_index(the_repository, &index_lock, 0) >= 0) { + if (clean_status_issue_sidecar( + &s, &clean_digest, &index_lock, 1)) + trace2_data_intmax("status", the_repository, + "clean-proof/postwrite-reissued", 1); + else + rollback_lock_file(&index_lock); + } + } if (s.relative_paths) s.prefix = prefix; diff --git a/builtin/diff.c b/builtin/diff.c index 2ebd04fa940780..d397463cde2b0c 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -406,31 +406,24 @@ static void symdiff_release(struct symdiff *sdiff) void prepare_diff_external_history(struct repository *repo) { - struct clean_status_sidecar_record sidecar = - CLEAN_STATUS_SIDECAR_RECORD_INIT; struct clean_status_config_digest digest; struct worktree *worktree = NULL; if (!fstat_is_reliable() || getenv(INDEX_ENVIRONMENT) || is_bare_repository(repo) || !repo_get_work_tree(repo) || fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC || - repo_config_values(repo)->apply_sparse_checkout || - clean_status_sidecar_load(repo_get_index_file(repo), - repo->hash_algo, &sidecar)) + repo_config_values(repo)->apply_sparse_checkout) goto done; worktree = get_current_worktree(repo); if (!worktree || !is_main_worktree(worktree) || clean_status_config_read_repository(repo, &digest) || - digest.filter_configured || - memcmp(digest.hash, sidecar.sidecar.proof.config_hash, - repo->hash_algo->rawsz)) + digest.filter_configured) goto done; clean_status_set_config_digest(repo, &digest); clean_status_enable_external_history(repo); done: free_worktree(worktree); - clean_status_sidecar_record_release(&sidecar); } int cmd_diff(int argc, diff --git a/builtin/read-tree.c b/builtin/read-tree.c index 8e3b023271723c..9a9f2c4a8b7e47 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -217,6 +217,7 @@ int cmd_read_tree(int argc, !opts.super_prefix && !index_output && !should_update_submodules()) { preserve_history = 1; + 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 3f18f921032fce..0c5eae4724ea10 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -510,9 +510,11 @@ int cmd_reset(int argc, !pathspec.nr && !intent_to_add && !unborn) { preserve_mixed_history = reset_type == MIXED; + clean_status_enable_external_history(the_repository); clean_status_set_config_digest(the_repository, &clean_digest); } else if (reset_type == MIXED && pathspec.nr && !intent_to_add && !unborn) { + clean_status_enable_external_history(the_repository); clean_status_set_config_digest(the_repository, &clean_digest); } diff --git a/builtin/stash.c b/builtin/stash.c index 6458ca7f9d91f8..898dc41007cfc1 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -1710,9 +1710,11 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q * If changes are found, invalidate it before stash machinery * mutates the index or worktree. */ - if (preserve_clean_history) + if (preserve_clean_history) { + 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); if (!include_untracked && ps->nr) { char *ps_matched = xcalloc(ps->nr, 1); @@ -1746,13 +1748,17 @@ 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) { + 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")); goto done; } - if (preserve_clean_history) - clean_status_invalidate_current_proof(the_repository->index); if (!refs_reflog_exists(get_main_ref_store(the_repository), ref_stash) && do_clear_stash()) { ret = -1; diff --git a/builtin/update-index.c b/builtin/update-index.c index b8b565f0d6f632..66746d11352e67 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -65,6 +65,19 @@ static int update_index_config(const char *key, const char *value, static int is_proof_preserving_rewrite(int argc, const char **argv) { + if (argc >= 4 && !strcmp(argv[1], "--refresh") && + !strcmp(argv[2], "--")) { + for (int i = 3; i < argc; i++) { + const char *base = strrchr(argv[i], '/'); + + base = base ? base + 1 : argv[i]; + if (!*base || !strcasecmp(base, ".gitattributes") || + !strcasecmp(base, ".gitignore")) + return 0; + } + return 1; + } + if (argc == 2) return !strcmp(argv[1], "--refresh") || !strcmp(argv[1], "--force-write-index"); @@ -78,6 +91,20 @@ static int is_proof_preserving_rewrite(int argc, const char **argv) !strcmp(argv[2], "--refresh")); } +static int is_fsmonitor_invalidation_rewrite(int argc, const char **argv) +{ + int first_path = 2; + + if (argc < 3 || strcmp(argv[1], "--no-fsmonitor-valid")) + return 0; + if (!strcmp(argv[first_path], "--")) + return argc > ++first_path; + for (int i = first_path; i < argc; i++) + if (argv[i][0] == '-') + return 0; + return 1; +} + /* Untracked cache mode */ enum uc_mode { UC_UNSPECIFIED = -1, @@ -273,6 +300,14 @@ static int mark_ce_flags(const char *path, int flag, int mark) the_repository->index->cache[pos]->ce_flags |= flag; else the_repository->index->cache[pos]->ce_flags &= ~flag; + if (flag == CE_FSMONITOR_VALID && !mark && + clean_status_external_history_enabled( + the_repository->index) && + !the_repository->index->split_index) { + /* The fsmonitor bitmap does not change the indexed tree. */ + the_repository->index->cache_changed |= FSMONITOR_CHANGED; + return 0; + } the_repository->index->cache[pos]->ce_flags |= CE_UPDATE_IN_BASE; cache_tree_invalidate_path(the_repository->index, path); the_repository->index->cache_changed |= CE_ENTRY_CHANGED; @@ -285,6 +320,8 @@ static int remove_one_path(const char *path) { if (!allow_remove) return error("%s: does not exist and --remove not passed", path); + if (clean_status_external_history_enabled(the_repository->index)) + clean_status_invalidate_current_proof(the_repository->index); if (remove_file_from_index(the_repository->index, path)) return error("%s: cannot remove from the index", path); return 0; @@ -327,6 +364,12 @@ static int add_one_path(const struct cache_entry *old, const char *path, int len } option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + if (clean_status_external_history_enabled(the_repository->index) && + (!old || old->ce_mode != ce->ce_mode || + !oideq(&old->oid, &ce->oid)) && + !clean_status_index_entry_is_semantically_safe( + the_repository->index, old, ce)) + clean_status_invalidate_current_proof(the_repository->index); if (add_index_entry(the_repository->index, ce, option)) { discard_cache_entry(ce); return error("%s: cannot add to the index - missing --add option?", path); @@ -362,6 +405,9 @@ static int process_directory(const char *path, int len, struct stat *st) struct object_id oid; int pos = index_name_pos(the_repository->index, path, len); + if (clean_status_external_history_enabled(the_repository->index)) + clean_status_invalidate_current_proof(the_repository->index); + /* Exact match: file or existing gitlink */ if (pos >= 0) { const struct cache_entry *ce = the_repository->index->cache[pos]; @@ -957,8 +1003,11 @@ int cmd_update_index(int argc, struct parse_opt_ctx_t ctx; strbuf_getline_fn getline_fn; int parseopt_state = PARSE_OPT_UNKNOWN; + int preserve_fsmonitor_history = + is_fsmonitor_invalidation_rewrite(argc, argv); int preserve_clean_history = - is_proof_preserving_rewrite(argc, argv); + is_proof_preserving_rewrite(argc, argv) || + preserve_fsmonitor_history; struct repository *r = the_repository; struct odb_transaction *transaction; struct option options[] = { @@ -1135,6 +1184,7 @@ int cmd_update_index(int argc, * but cannot change the logical contents of the index. */ clean_status_set_config_digest(the_repository, &clean_digest); + clean_status_enable_external_history(the_repository); } else { repo_config(the_repository, git_default_config, NULL); } @@ -1150,6 +1200,28 @@ int cmd_update_index(int argc, entries = repo_read_index(the_repository); if (entries < 0) die("cache corrupted"); + if (preserve_clean_history && argc >= 4 && + !strcmp(argv[1], "--refresh") && !strcmp(argv[2], "--")) { + if (the_repository->index->split_index || + the_repository->index->sparse_index) + clean_status_invalidate_current_proof( + the_repository->index); + for (int i = 3; i < argc; i++) { + char *path = prefix_path(the_repository, prefix, + prefix_length, argv[i]); + int pos = index_name_pos(the_repository->index, + path, strlen(path)); + const struct cache_entry *ce = pos < 0 ? NULL : + the_repository->index->cache[pos]; + + if (!ce || !S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + clean_status_invalidate_current_proof( + the_repository->index); + free(path); + } + } the_repository->index->updated_skipworktree = 1; diff --git a/clean-status-config.c b/clean-status-config.c index 0cbab0fe50acd4..1d32470e1e5586 100644 --- a/clean-status-config.c +++ b/clean-status-config.c @@ -1,9 +1,15 @@ #include "git-compat-util.h" +#include "abspath.h" #include "clean-status-config.h" +#include "clean-status-index.h" #include "config.h" +#include "environment.h" #include "hash-framing.h" +#include "path-namespace.h" +#include "read-cache-ll.h" #include "repository.h" #include "strbuf.h" +#include "wrapper.h" #define CLEAN_STATUS_FILTER_PROOF_DOMAIN \ "clean-status-configured-filter-scope-v1" @@ -16,6 +22,9 @@ void clean_status_config_init(struct clean_status_config_digest *digest, memset(digest, 0, sizeof(*digest)); git_hash_init(&digest->ctx, algo); git_hash_init(&digest->semantic_ctx, algo); + git_hash_init(&digest->tracked_policy_ctx, algo); + hash_optional_cstring(&digest->tracked_policy_ctx, + "clean-status-tracked-policy-v1"); /* Invalidate proofs written before multiply-linked files stayed dirty. */ hash_optional_cstring(&digest->ctx, "clean-status-config-hardlink-v1"); @@ -49,6 +58,40 @@ static void hash_effective_config_entry(struct git_hash_ctx *ctx, hash_optional_cstring(ctx, value); } +static int config_is_command_transport(const char *key, + const struct config_context *ctx) +{ + const char *subsection, *subkey; + size_t subsection_len; + + if (!ctx || !ctx->kvi || ctx->kvi->scope != CONFIG_SCOPE_COMMAND) + return 0; + if (starts_with(key, "credential.")) + return 1; + if (parse_config_key(key, "url", &subsection, &subsection_len, + &subkey) || !subsection || !subsection_len) + return 0; + return !strcmp(subkey, "insteadof") || + !strcmp(subkey, "pushinsteadof"); +} + +static int config_is_tracked_policy(const char *key) +{ + return !strcmp(key, "core.filemode") || + !strcmp(key, "core.trustctime") || + !strcmp(key, "core.checkstat") || + !strcmp(key, "core.symlinks") || + !strcmp(key, "core.ignorecase") || + !strcmp(key, "core.ignorestat") || + !strcmp(key, "core.sparsecheckout") || + !strcmp(key, "core.sparsecheckoutcone") || + !strcmp(key, "core.precomposeunicode") || + !strcmp(key, "core.protecthfs") || + !strcmp(key, "core.protectntfs") || + !strcmp(key, "core.excludesfile") || + !strcmp(key, "core.attributesfile"); +} + void clean_status_config_add(struct clean_status_config_digest *digest, const char *key, const char *value, const struct config_context *ctx) @@ -58,7 +101,13 @@ void clean_status_config_add(struct clean_status_config_digest *digest, if (!digest->initialized || digest->finalized) BUG("invalid clean-status config digest state"); + /* Process-local transport settings cannot change a worktree proof. */ + if (config_is_command_transport(key, ctx)) + return; hash_config_entry(&digest->ctx, key, value, ctx); + if (config_is_tracked_policy(key)) + hash_effective_config_entry(&digest->tracked_policy_ctx, + key, value); semantic = !strcmp(key, "core.autocrlf") || !strcmp(key, "core.eol") || !strcmp(key, "core.checkroundtripencoding"); @@ -91,6 +140,8 @@ void clean_status_config_final(struct clean_status_config_digest *digest) } git_hash_final(digest->hash, &digest->ctx); git_hash_final(digest->semantic_hash, &digest->semantic_ctx); + git_hash_final(digest->tracked_policy_hash, + &digest->tracked_policy_ctx); digest->finalized = 1; } @@ -118,3 +169,219 @@ int clean_status_config_read_repository( clean_status_config_final(digest); return 0; } + +#ifdef __APPLE__ +struct config_epoch_source { + char *path; + struct path_namespace_snapshot *namespace; + struct stat stat; + int fd; +}; + +struct config_epoch_proof { + struct config_epoch_source *sources; + char *system_path; + size_t nr; + size_t alloc; + struct stat index; + int failed; + int system_seen; +}; + +static int config_epoch_command_is_safe( + const char *key, const struct config_context *ctx) +{ + return starts_with(key, "advice.") || + !strcmp(key, "user.name") || !strcmp(key, "user.email") || + !strcmp(key, "core.preloadindexbulk") || + config_is_command_transport(key, ctx); +} + +static int config_epoch_source_precedes_index( + const struct stat *source, const struct stat *index) +{ + return source->st_ctimespec.tv_sec < index->st_birthtimespec.tv_sec || + (source->st_ctimespec.tv_sec == + index->st_birthtimespec.tv_sec && + source->st_ctimespec.tv_nsec < + index->st_birthtimespec.tv_nsec); +} + +static int config_epoch_capture_source( + const char *key, const char *value UNUSED, + const struct config_context *ctx, void *data) +{ + struct config_epoch_proof *proof = data; + struct config_epoch_source *source; + struct path_namespace_snapshot *after = NULL; + struct strbuf normalized = STRBUF_INIT; + struct stat named; + char *absolute = NULL; + int fd = -1; + int allocated = 0; + + if (proof->failed) + return 0; + if (!ctx || !ctx->kvi) + goto fail; + if (ctx->kvi->scope == CONFIG_SCOPE_COMMAND) { + if (!config_epoch_command_is_safe(key, ctx)) + goto fail; + return 0; + } + if (starts_with(key, "includeif.")) + goto fail; + if (ctx->kvi->origin_type != CONFIG_ORIGIN_FILE || + !ctx->kvi->filename || !*ctx->kvi->filename) + goto fail; + for (size_t i = 0; i < proof->nr; i++) + if (!strcmp(proof->sources[i].path, ctx->kvi->filename)) + return 0; + absolute = absolute_pathdup(ctx->kvi->filename); + strbuf_addstr(&normalized, absolute); + if (strbuf_normalize_path(&normalized)) + goto fail; + if (ctx->kvi->scope == CONFIG_SCOPE_SYSTEM && + proof->system_path && strcmp(normalized.buf, proof->system_path)) + goto fail; + fd = open_nofollow(normalized.buf, O_RDONLY | O_CLOEXEC); + if (fd < 0) + goto fail; + ALLOC_GROW(proof->sources, proof->nr + 1, proof->alloc); + source = &proof->sources[proof->nr]; + memset(source, 0, sizeof(*source)); + source->fd = -1; + allocated = 1; + if (fstat(fd, &source->stat) || + !S_ISREG(source->stat.st_mode) || + source->stat.st_nlink != 1 || + (!is_path_owned_by_current_user(normalized.buf, NULL) && + !(source->stat.st_uid == 0 && + ctx->kvi->scope == CONFIG_SCOPE_SYSTEM)) || + !config_epoch_source_precedes_index(&source->stat, &proof->index) || + lstat(normalized.buf, &named) || + !path_namespace_stat_equal(&source->stat, &named) || + path_namespace_capture(normalized.buf, &source->namespace) || + !path_namespace_target_present(source->namespace) || + path_namespace_capture(normalized.buf, &after) || + !path_namespace_equal(source->namespace, after)) + goto fail; + source->path = xstrdup(ctx->kvi->filename); + source->fd = fd; + proof->nr++; + if (ctx->kvi->scope == CONFIG_SCOPE_SYSTEM && proof->system_path) + proof->system_seen = 1; + fd = -1; + path_namespace_clear(after); + strbuf_release(&normalized); + free(absolute); + return 0; + +fail: + if (fd >= 0) + close(fd); + if (allocated) + path_namespace_clear(proof->sources[proof->nr].namespace); + path_namespace_clear(after); + strbuf_release(&normalized); + free(absolute); + proof->failed = 1; + return 0; +} + +static int config_epoch_sources_still_match( + const struct config_epoch_proof *proof) +{ + for (size_t i = 0; i < proof->nr; i++) { + const struct config_epoch_source *source = &proof->sources[i]; + struct path_namespace_snapshot *namespace = NULL; + struct strbuf normalized = STRBUF_INIT; + struct stat held, named; + char *absolute = absolute_pathdup(source->path); + int valid; + + strbuf_addstr(&normalized, absolute); + valid = !strbuf_normalize_path(&normalized) && + !fstat(source->fd, &held) && + !lstat(normalized.buf, &named) && + path_namespace_stat_equal(&source->stat, &held) && + path_namespace_stat_equal(&held, &named) && + config_epoch_source_precedes_index(&held, &proof->index) && + !path_namespace_capture(normalized.buf, &namespace) && + path_namespace_equal(source->namespace, namespace); + path_namespace_clear(namespace); + strbuf_release(&normalized); + free(absolute); + if (!valid) + return 0; + } + return 1; +} +#endif + +int clean_status_config_tracked_sources_predate_index( + struct index_state *istate) +{ +#ifdef __APPLE__ + struct clean_status_index_snapshot snapshot = { .fd = -1 }; + struct config_epoch_proof proof = { 0 }; + struct config_options opts = { 0 }; + const char *system_path = getenv("GIT_CONFIG_SYSTEM"); + int valid = 0; + + /* + * Version-one proofs did not record their tracked-stat policy. The + * shipped writer is trusted not to have used transient tracked-policy + * overrides; stable configuration sources older than its index then + * authenticate the one-time migration. Version-two proofs carry their + * complete policy instead and never use this compatibility exception. + */ + if (!istate || getenv("GIT_CONFIG_GLOBAL") || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || + getenv(INDEX_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || + clean_status_index_snapshot_pin(&snapshot, istate) || + fstat(snapshot.fd, &proof.index) || + proof.index.st_birthtimespec.tv_sec <= 0) + goto done; + if (system_path) { + struct strbuf normalized = STRBUF_INIT; + + if (!is_absolute_path(system_path)) + goto done; + strbuf_addstr(&normalized, system_path); + if (strbuf_normalize_path(&normalized)) { + strbuf_release(&normalized); + goto done; + } + proof.system_path = strbuf_detach(&normalized, NULL); + } + opts.respect_includes = 1; + opts.commondir = istate->repo->commondir; + opts.git_dir = istate->repo->gitdir; + if (config_with_options(config_epoch_capture_source, &proof, NULL, + istate->repo, &opts) < 0 || + proof.failed || !proof.nr || + (proof.system_path && !proof.system_seen) || + !config_epoch_sources_still_match(&proof) || + !clean_status_index_snapshot_still_matches_proof_epoch( + &snapshot, istate)) + goto done; + valid = 1; + +done: + free(proof.system_path); + for (size_t i = 0; i < proof.nr; i++) { + close(proof.sources[i].fd); + path_namespace_clear(proof.sources[i].namespace); + free(proof.sources[i].path); + } + free(proof.sources); + clean_status_index_snapshot_release(&snapshot); + return valid; +#else + (void)istate; + return 0; +#endif +} diff --git a/clean-status-config.h b/clean-status-config.h index 0a4275ea92242f..1f72325b9f0059 100644 --- a/clean-status-config.h +++ b/clean-status-config.h @@ -4,13 +4,16 @@ #include "hash.h" struct config_context; +struct index_state; struct repository; struct clean_status_config_digest { struct git_hash_ctx ctx; struct git_hash_ctx semantic_ctx; + struct git_hash_ctx tracked_policy_ctx; unsigned char hash[GIT_MAX_RAWSZ]; unsigned char semantic_hash[GIT_MAX_RAWSZ]; + unsigned char tracked_policy_hash[GIT_MAX_RAWSZ]; unsigned initialized : 1; unsigned finalized : 1; unsigned filter_configured : 1; @@ -26,5 +29,7 @@ void clean_status_config_final(struct clean_status_config_digest *digest); int clean_status_config_read_repository( struct repository *repo, struct clean_status_config_digest *digest); +int clean_status_config_tracked_sources_predate_index( + struct index_state *istate); #endif /* CLEAN_STATUS_CONFIG_H */ diff --git a/clean-status-fast.c b/clean-status-fast.c index f41951079f2094..512f6b4b67aba0 100644 --- a/clean-status-fast.c +++ b/clean-status-fast.c @@ -10,7 +10,9 @@ #include "fsmonitor.h" #include "fsmonitor-settings.h" #include "object-name.h" +#include "path-namespace.h" #include "repository.h" +#include "semantic-verify-internal.h" #include "trace2.h" #include "worktree.h" #include "wrapper.h" @@ -81,6 +83,84 @@ static int attr_snapshot_still_matches( repo->hash_algo->rawsz); } +static int hardlink_witnesses_still_match( + struct repository *repo, const struct clean_status_sidecar *sidecar) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN && !defined(NO_NSEC) + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + const unsigned char *cursor, *end; + unsigned int namespace_unstable = 0; + int ret = 0; + + if (!sidecar->hardlink_nr) + return 1; + cursor = sidecar->hardlinks; + end = cursor + sidecar->hardlinks_len; + if (!repo->config_values_private_.trust_ctime || + !repo->config_values_private_.check_stat || + semantic_verify_root_init(repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path) + goto done; + for (uint32_t i = 0; i < sidecar->hardlink_nr; i++) { + struct path_stat_identity expected, observed; + const unsigned char *raw_path; + const char *basename; + struct stat held, named; + size_t path_len; + char *name; + int parent_fd, fd; + + if (clean_status_sidecar_next_hardlink( + &cursor, end, &raw_path, &path_len, &expected) || + !path_len || memchr(raw_path, '\0', path_len)) + goto done; + name = xmemdupz(raw_path, path_len); + if (semantic_verify_resolve_parent( + path, name, i, &parent_fd, &basename)) { + free(name); + goto done; + } + fd = semantic_verify_openat( + parent_fd, basename, + O_RDONLY | O_NONBLOCK | O_NOFOLLOW); + if (fd < 0) { + free(name); + goto done; + } + if (fstat(fd, &held) || !S_ISREG(held.st_mode) || + held.st_nlink <= 1 || held.st_dev != root->stat.st_dev || + fstatat(parent_fd, basename, &named, + AT_SYMLINK_NOFOLLOW) || + !path_namespace_stat_equal(&held, &named)) { + close(fd); + free(name); + goto done; + } + path_stat_identity_init(&observed, &held); + close(fd); + free(name); + if (!path_stat_identity_equal(&expected, &observed)) + goto done; + } + if (cursor != end || !semantic_verify_root_stable(root)) + goto done; + ret = 1; + +done: + semantic_verify_path_free(path, &namespace_unstable, NULL); + if (namespace_unstable || (root && !semantic_verify_root_stable(root))) + ret = 0; + semantic_verify_root_clear(root); + return ret; +#else + (void)repo; + return !sidecar->hardlink_nr; +#endif +} + static int fast_path_test_barrier(void) { const char *ready = @@ -187,6 +267,10 @@ int clean_status_try_sidecar( trace_miss(repo, "fast-head-changed"); goto done; } + if (!hardlink_witnesses_still_match(repo, &record.sidecar)) { + trace_miss(repo, "fast-hardlink-changed"); + goto done; + } query_token = xmemdupz( record.sidecar.token, record.sidecar.token_len); @@ -233,7 +317,15 @@ int clean_status_try_sidecar( trace_miss(repo, "fast-index-raced"); goto done; } + if (!hardlink_witnesses_still_match(repo, &record.sidecar)) { + trace_miss(repo, "fast-hardlink-raced"); + goto done; + } + if (record.sidecar.hardlink_nr) + trace2_data_intmax("status", repo, + "clean-proof/hardlink-validated", + record.sidecar.hardlink_nr); trace2_data_intmax("status", repo, "clean-proof/hit", 1); ret = 1; diff --git a/clean-status-history.c b/clean-status-history.c index 347f6c6644e5a1..495566d74dbc6e 100644 --- a/clean-status-history.c +++ b/clean-status-history.c @@ -1,5 +1,6 @@ #include "git-compat-util.h" #include "abspath.h" +#include "attr-fingerprint.h" #include "clean-status.h" #include "clean-status-history-store.h" #include "clean-status-index.h" @@ -13,7 +14,9 @@ #include "hash-framing.h" #include "hex.h" #include "read-cache-ll.h" +#include "replace-object.h" #include "repository.h" +#include "semantic-verify-internal.h" #include "strbuf.h" #include "trace2.h" #include "ewah/ewok.h" @@ -26,6 +29,9 @@ static void invalidate_disk_history(struct clean_status_state *state) state->disk_config_invalid = 1; state->disk_config_valid = 0; state->disk_semantic_valid = 0; + state->disk_tracked_policy_valid = 0; + memset(state->disk_tracked_policy_hash, 0, + sizeof(state->disk_tracked_policy_hash)); state->disk_attr_valid = 0; FREE_AND_NULL(state->disk_config_token); strbuf_reset(&state->disk_config_raw); @@ -62,6 +68,16 @@ int clean_status_read_fsmonitor_config(struct index_state *istate, istate->repo->hash_algo->rawsz); memcpy(state->disk_attr_hash, proof.attr_hash, istate->repo->hash_algo->rawsz); + if (proof.tracked_policy_hash) { + memcpy(state->disk_tracked_policy_hash, + proof.tracked_policy_hash, + istate->repo->hash_algo->rawsz); + state->disk_tracked_policy_valid = 1; + } else { + state->disk_tracked_policy_valid = 0; + memset(state->disk_tracked_policy_hash, 0, + sizeof(state->disk_tracked_policy_hash)); + } strbuf_add(&state->disk_config_raw, data, size); state->disk_config_valid = 1; state->disk_semantic_valid = 1; @@ -73,7 +89,10 @@ static int prepare_fsmonitor_config(struct index_state *istate, int trace) { struct clean_status_state *state = istate->clean_status; const struct git_hash_algo *algo = istate->repo->hash_algo; - int token_coherent, config_coherent, semantic_changed, attr_changed; + int token_coherent, config_coherent, tracked_policy_coherent; + int semantic_changed, attr_changed; + int legacy_empty_attributes = 0; + int manifest_reusable; int coherent; if (!state || !state->current_config_valid) @@ -82,7 +101,12 @@ static int prepare_fsmonitor_config(struct index_state *istate, int trace) !state->disk_config_invalid && istate->fsmonitor_token_valid && istate->fsmonitor_last_update && state->disk_config_token && !strcmp(state->disk_config_token, istate->fsmonitor_last_update); + tracked_policy_coherent = !state->disk_tracked_policy_valid || + (state->current_tracked_policy_valid && + !memcmp(state->disk_tracked_policy_hash, + state->current_tracked_policy_hash, algo->rawsz)); config_coherent = state->disk_config_valid && + tracked_policy_coherent && !memcmp(state->disk_config_hash, state->current_config_hash, algo->rawsz); semantic_changed = state->disk_semantic_valid && @@ -93,6 +117,26 @@ static int prepare_fsmonitor_config(struct index_state *istate, int trace) (state->disk_attr_valid && state->current_attr_valid && memcmp(state->disk_attr_hash, state->current_attr_hash, algo->rawsz)); + if (attr_changed && token_coherent && state->disk_semantic_valid && + state->current_semantic_valid && !semantic_changed && + state->disk_attr_valid && state->current_attr_valid && + !state->current_attr_sources_present && !state->filter_configured && + state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL && + !getenv(INDEX_ENVIRONMENT) && + !getenv(GIT_WORK_TREE_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !getenv(ALTERNATE_DB_ENVIRONMENT) && + istate == istate->repo->index && !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC && + !repo_has_replace_refs_uncached(istate->repo) && + attr_fingerprint_matches_legacy_absent_sources( + istate->repo, state->disk_attr_hash)) { + attr_changed = 0; + legacy_empty_attributes = 1; + } coherent = token_coherent && config_coherent && state->disk_semantic_valid && state->current_semantic_valid && !semantic_changed && state->disk_attr_valid && @@ -100,6 +144,13 @@ static int prepare_fsmonitor_config(struct index_state *istate, int trace) state->manifest.disk_valid && (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == FSMONITOR_CLEAN_PROOF_ALL; + manifest_reusable = token_coherent && !config_coherent && + state->disk_semantic_valid && state->current_semantic_valid && + !semantic_changed && state->disk_attr_valid && + state->current_attr_valid && !attr_changed && + !state->filter_configured && state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL; state->filter_scope_valid = coherent && state->filter_configured; state->config_revalidated = coherent; state->initial_coherent = coherent; @@ -108,6 +159,11 @@ static int prepare_fsmonitor_config(struct index_state *istate, int trace) state->config_revalidated_token = xstrdup(istate->fsmonitor_last_update); clean_status_manifest_adopt_disk(&state->manifest); + } else if (manifest_reusable) { + clean_status_manifest_adopt_disk(&state->manifest); + if (trace) + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-reused", 1); } state->config_mismatch = state->config_enforced && !coherent; state->strong_mismatch = state->config_enforced && @@ -120,6 +176,9 @@ static int prepare_fsmonitor_config(struct index_state *istate, int trace) state->current_attr_sources_present) || clean_status_filter_scope_needs_validation(istate)); if (trace) { + if (legacy_empty_attributes) + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/legacy-empty-attributes", 1); trace2_data_intmax("fsmonitor", istate->repo, "config/coherent", coherent); trace2_data_intmax("fsmonitor", istate->repo, @@ -139,6 +198,60 @@ int clean_status_probe_fsmonitor_config(struct index_state *istate) return prepare_fsmonitor_config(istate, 0); } +int clean_status_try_preserve_tracked_config_epoch( + struct index_state *istate) +{ + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo; + int attr_matches; + + if (!state || !istate->repo || istate != istate->repo->index) + return 0; + algo = istate->repo->hash_algo; + attr_matches = state->disk_attr_valid && + state->current_attr_valid && + (!memcmp(state->disk_attr_hash, state->current_attr_hash, + algo->rawsz) || + attr_fingerprint_matches_legacy_absent_sources( + istate->repo, state->disk_attr_hash)); + if (!state->config_enforced || !state->config_mismatch || + state->strong_mismatch || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->current_config_valid || !state->disk_semantic_valid || + !state->current_semantic_valid || + memcmp(state->disk_semantic_hash, state->current_semantic_hash, + algo->rawsz) || !attr_matches || + (!state->disk_tracked_policy_valid && + state->current_attr_sources_present) || + state->filter_configured || + !state->manifest.disk_valid || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + state->manifest.global_fallback || + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || !state->disk_config_token || + strcmp(state->disk_config_token, istate->fsmonitor_last_update) || + istate->split_index || istate->sparse_index != INDEX_EXPANDED || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC || + repo_has_replace_refs_uncached(istate->repo) || + !state->current_tracked_policy_valid || + (state->disk_tracked_policy_valid ? + memcmp(state->disk_tracked_policy_hash, + state->current_tracked_policy_hash, algo->rawsz) : + !clean_status_config_tracked_sources_predate_index(istate))) + return 0; + clean_status_mark_fsmonitor_config_valid( + istate, istate->fsmonitor_last_update); + if (!clean_status_revalidated_token_matches(istate)) + return 0; + trace2_data_intmax("fsmonitor", istate->repo, + "config/tracked-epoch-valid", 1); + return 1; +} + int clean_status_has_persistent_fsmonitor_semantic_history( const struct index_state *istate) { @@ -279,6 +392,9 @@ void clean_status_write_fsmonitor_config(struct strbuf *out, .config_hash = state->current_config_hash, .semantic_hash = state->current_semantic_hash, .attr_hash = state->current_attr_hash, + .tracked_policy_hash = + state->current_tracked_policy_valid ? + state->current_tracked_policy_hash : NULL, .attr_manifest = (const unsigned char *)state->manifest.current.buf, .attr_manifest_len = state->manifest.current.len, @@ -575,6 +691,79 @@ static int external_token_is_replayable(const char *token) return replayable; } +static int missing_fsmonitor_token_is_replayable( + struct index_state *istate, struct index_state *parsed) +{ + struct fsmonitor_query_result result = + FSMONITOR_QUERY_RESULT_INIT; + const char *token = parsed->fsmonitor_last_update; + const char *path, *end; + int replayable = 0; + + if (!token || !starts_with(token, "builtin:") || + !strcmp(token, "builtin:fake") || + query_builtin_fsmonitor(token, &result) != + FSMONITOR_QUERY_DELTA) + goto done; + path = result.paths.buf; + end = result.paths.buf + result.paths.len; + while (path < end) { + size_t len = strlen(path); + const char *base = find_last_dir_sep(path); + + base = base ? base + 1 : path; + if (!strcmp(path, FSMONITOR_PATH_GLOBAL_INVALIDATE)) + goto done; + if (!fspathcmp(base, ".gitattributes")) { + struct index_state witness = *istate; + + witness.clean_status = parsed->clean_status; + witness.fsmonitor_last_update = + parsed->fsmonitor_last_update; + witness.fsmonitor_token_valid = + parsed->fsmonitor_token_valid; + if (!clean_status_manifest_reconcile_deleted_attribute( + &witness, path)) { + struct clean_status_state *state = + parsed->clean_status; + struct strbuf proof = STRBUF_INIT; + + if (!clean_status_manifest_reconcile_display_only_attribute( + &witness, path)) + goto done; + clean_status_write_fsmonitor_config( + &proof, parsed); + strbuf_reset(&state->disk_config_raw); + strbuf_addbuf(&state->disk_config_raw, &proof); + strbuf_reset(&state->manifest.disk); + strbuf_addbuf(&state->manifest.disk, + &state->manifest.current); + memcpy(state->manifest.disk_hash, + state->manifest.current_hash, + parsed->repo->hash_algo->rawsz); + state->manifest.disk_flags = + state->manifest.current_flags; + strbuf_release(&proof); + } + } + path += len + 1; + } + replayable = path == end; + +done: + fsmonitor_query_result_release(&result); + return replayable; +} + +static void invalidate_unwatched_recovered_entry(size_t pos, void *data) +{ + struct index_state *istate = data; + + if (pos >= istate->cache_nr) + BUG("recovered fsmonitor entry is outside the index"); + fsmonitor_invalidate_cache_entry(istate->cache[pos]); +} + #ifdef __APPLE__ static int external_semantic_delta_is_safe( const struct strbuf *paths, struct index_state *old_index, @@ -647,6 +836,7 @@ static void restore_external_tracked_history( const struct strbuf *paths, const struct fsmonitor_clean_proof *proof) { struct index_state parsed = INDEX_STATE_INIT(istate->repo); + const struct stat_data empty_stat = { 0 }; unsigned int old_pos = 0, new_pos = 0, restored = 0, i; const unsigned int unsafe_flags = CE_VALID | CE_SKIP_WORKTREE | CE_INTENT_TO_ADD | CE_CONTENT_CHECK_REQUIRED | CE_STAGEMASK; @@ -676,6 +866,7 @@ static void restore_external_tracked_history( const struct cache_entry *old_entry = witness->cache[old_pos]; struct cache_entry *new_entry = istate->cache[new_pos]; int cmp = strcmp(old_entry->name, new_entry->name); + int recover_stat; if (cmp < 0) { old_pos++; @@ -694,12 +885,30 @@ static void restore_external_tracked_history( !S_ISLNK(new_entry->ce_mode)) || old_entry->ce_mode != new_entry->ce_mode || !oideq(&old_entry->oid, &new_entry->oid) || - memcmp(&old_entry->ce_stat_data, &new_entry->ce_stat_data, - sizeof(old_entry->ce_stat_data)) || - is_racy_timestamp(istate, new_entry) || external_checkpoint_path_was_replayed( new_entry->name, paths)) continue; + recover_stat = !memcmp(&new_entry->ce_stat_data, &empty_stat, + sizeof(empty_stat)); + if (memcmp(&old_entry->ce_stat_data, &new_entry->ce_stat_data, + sizeof(old_entry->ce_stat_data)) && + (!recover_stat || + !memcmp(&old_entry->ce_stat_data, &empty_stat, + sizeof(empty_stat)) || + is_racy_timestamp(witness, old_entry))) + continue; + if (recover_stat) + new_entry->ce_stat_data = old_entry->ce_stat_data; + if (is_racy_timestamp(istate, new_entry)) { + if (recover_stat) + new_entry->ce_stat_data = empty_stat; + continue; + } + if (recover_stat) { + new_entry->ce_flags |= CE_UPDATE_IN_BASE; + istate->cache_changed |= CE_ENTRY_CHANGED; + istate->clean_status->recovered_tracked_stat = 1; + } new_entry->ce_flags |= CE_FSMONITOR_VALID; restored++; } @@ -962,6 +1171,171 @@ static int restore_external_semantic_history( #endif } +static int restore_external_bootstrap_manifest( + struct index_state *istate, + const struct clean_status_history_checkpoint *checkpoint, + const char *proof_namespace, + const struct clean_status_index_snapshot *snapshot) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + const unsigned int semantic_flags = + CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS; + const unsigned int transient_flags = + CE_UPDATE | CE_REMOVE | CE_ADDED | CE_WT_REMOVE | + CE_CONFLICTED | CE_UNPACKED | CE_NEW_SKIP_WORKTREE | + CE_MATCHED | CE_STRIP_NAME | CE_CONTENT_CHECK_REQUIRED; + struct clean_status_state *state = istate->clean_status; + struct index_state witness = INDEX_STATE_INIT(istate->repo); + struct clean_status_identity before_identity, after_identity; + struct fsmonitor_query_result changes = + FSMONITOR_QUERY_RESULT_INIT; + struct fsmonitor_clean_proof proof; + struct strbuf rewritten = STRBUF_INIT; + struct stat before, after; + unsigned char witness_hash[GIT_MAX_RAWSZ]; + char *path = NULL; + int fd = -1, attr_pos = -1, transferred = 0; + + if (!checkpoint->source_alias_valid || !state || + state->disk_config_invalid || state->filter_configured || + state->current_attr_sources_present || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC || + fsmonitor_clean_proof_parse(&proof, checkpoint->fsmonitor_config, + checkpoint->fsmonitor_config_len, + istate->repo->hash_algo) || + (proof.flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL) + goto done; + path = clean_status_history_store_witness_path( + istate->repo->index_file, proof_namespace, + istate->repo->hash_algo); + fd = open_nofollow(path, O_RDONLY | O_CLOEXEC); + if (fd < 0 || fstat(fd, &before) || !S_ISREG(before.st_mode) || + before.st_nlink != 1 || before.st_uid != geteuid() || + clean_status_identity_from_stat(&before_identity, &before) || + lstat(path, &after) || before.st_dev != after.st_dev || + before.st_ino != after.st_ino) + goto done; + do_read_index(&witness, path, 1); + if (fstat(fd, &after) || after.st_nlink != 1 || + after.st_uid != geteuid() || + clean_status_identity_from_stat(&after_identity, &after) || + !clean_status_identity_equal(&before_identity, &after_identity) || + before.st_size != after.st_size || + lstat(path, &after) || before.st_dev != after.st_dev || + before.st_ino != after.st_ino || + witness.version != checkpoint->source_version || + witness.cache_nr != checkpoint->source_cache_nr || + witness.cache_nr != istate->cache_nr || + !oideq(&witness.oid, &checkpoint->source_checksum) || + clean_status_index_logical_digest(&witness, witness_hash) || + memcmp(witness_hash, checkpoint->index_hash, + istate->repo->hash_algo->rawsz)) + goto done; + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *old = witness.cache[i]; + const struct cache_entry *current = istate->cache[i]; + + if (ce_namelen(old) != ce_namelen(current) || + memcmp(old->name, current->name, ce_namelen(old) + 1) || + old->ce_mode != current->ce_mode || + ((old->ce_flags ^ current->ce_flags) & semantic_flags) || + ((old->ce_flags | current->ce_flags) & transient_flags)) + goto done; + if (oideq(&old->oid, ¤t->oid)) + continue; + if (attr_pos >= 0 || strcmp(current->name, ".gitattributes") || + !S_ISREG(current->ce_mode)) + goto done; + attr_pos = i; + } + if (attr_pos < 0 || + !clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + clean_status_release(&witness); + clean_status_attach_config(&witness); + clean_status_read_fsmonitor_config( + &witness, checkpoint->fsmonitor_config, + checkpoint->fsmonitor_config_len); + free(witness.fsmonitor_last_update); + witness.fsmonitor_last_update = + xmemdupz(proof.token, proof.token_len); + witness.fsmonitor_token_valid = 1; + clean_status_prepare_fsmonitor_config(&witness); + if (!current_proof_is_writable(&witness)) + goto done; + { + struct index_state current = *istate; + + current.clean_status = witness.clean_status; + current.fsmonitor_last_update = + witness.fsmonitor_last_update; + current.fsmonitor_token_valid = 1; + if (!clean_status_manifest_reconcile_display_only_attribute( + ¤t, ".gitattributes")) + goto done; + } + if (!clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + if (query_builtin_fsmonitor(witness.fsmonitor_last_update, + &changes) != FSMONITOR_QUERY_DELTA) + goto done; + for (const char *changed = changes.paths.buf, + *end = changes.paths.buf + changes.paths.len; + changed < end; changed += strlen(changed) + 1) { + size_t len = strlen(changed); + const char *base = find_last_dir_sep(changed); + + base = base ? base + 1 : changed; + if (!len || + !strcmp(changed, FSMONITOR_PATH_GLOBAL_INVALIDATE) || + changed[len - 1] == '/' || + (!fspathcmp(base, ".gitattributes") && + strcmp(changed, ".gitattributes"))) + goto done; + } + if (!clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + clean_status_write_fsmonitor_config(&rewritten, &witness); + clean_status_release(istate); + clean_status_attach_config(istate); + clean_status_read_fsmonitor_config( + istate, rewritten.buf, rewritten.len); + state = istate->clean_status; + state->config_mismatch = 0; + state->strong_mismatch = 0; + state->initial_coherent = 0; + state->config_revalidated = 0; + clean_status_manifest_adopt_disk(&state->manifest); + state->manifest.checked = 1; + state->manifest.global_fallback = 0; + state->manifest.current_invalidated = 0; + state->authenticated_bootstrap_manifest = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-bootstrap-manifest", 1); + transferred = 1; + +done: + if (fd >= 0) + close(fd); + free(path); + fsmonitor_query_result_release(&changes); + strbuf_release(&rewritten); + release_index(&witness); + return transferred; +#else + (void)istate; + (void)checkpoint; + (void)proof_namespace; + (void)snapshot; + return 0; +#endif +} + int clean_status_restore_external_history(struct index_state *istate) { struct clean_status_history_store_record record = @@ -972,6 +1346,9 @@ int clean_status_restore_external_history(struct index_state *istate) unsigned char index_hash[GIT_MAX_RAWSZ]; char proof_namespace[GIT_MAX_HEXSZ + 1]; int record_loaded = 0; + int missing_fsmonitor_recovery = 0; + int owned_index = 0; + int preserve_witness = 0; int restored = 0; if (!clean_status_external_history_enabled(istate) || !state || @@ -992,9 +1369,19 @@ int clean_status_restore_external_history(struct index_state *istate) !memcmp(state->disk_config_hash, state->current_config_hash, istate->repo->hash_algo->rawsz) && !clean_status_has_persistent_fsmonitor_semantic_history(istate)) { - trace2_data_intmax("fsmonitor", istate->repo, - "history/external-proof-invalidated", 1); - goto done; + missing_fsmonitor_recovery = + !istate->fsmonitor_extension_seen && + !istate->fsmonitor_token_valid && + !istate->fsmonitor_last_update && + fsm_settings__get_mode(istate->repo) == + FSMONITOR_MODE_IPC && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat; + if (!missing_fsmonitor_recovery) { + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } } if (external_history_namespace(istate, proof_namespace)) goto done; @@ -1020,10 +1407,23 @@ int clean_status_restore_external_history(struct index_state *istate) memcpy(state->source_logical_hash, index_hash, istate->repo->hash_algo->rawsz); state->source_logical_hash_valid = 1; - if (!record_loaded) + if (!record_loaded) { + if (missing_fsmonitor_recovery) + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); goto done; + } if (memcmp(index_hash, record.checkpoint.index_hash, istate->repo->hash_algo->rawsz)) { + if (missing_fsmonitor_recovery) { + if (restore_external_bootstrap_manifest( + istate, &record.checkpoint, proof_namespace, + &snapshot)) + goto done; + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } restored = restore_external_semantic_history( istate, &record.checkpoint, proof_namespace, &snapshot); goto done; @@ -1058,6 +1458,13 @@ int clean_status_restore_external_history(struct index_state *istate) if (!current_proof_is_writable(&parsed) || (!!parsed.untracked && !parsed.fsmonitor_untracked_valid)) goto done; + if (missing_fsmonitor_recovery && + (!parsed.untracked || + !missing_fsmonitor_token_is_replayable(istate, &parsed))) { + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } /* * Provider tokens are opaque. A logical-index match says that the * checkpoint names the same staged entries; it does not say that its @@ -1081,6 +1488,37 @@ int clean_status_restore_external_history(struct index_state *istate) if (!clean_status_index_snapshot_still_matches_proof_epoch( &snapshot, istate)) goto done; + owned_index = !getenv(GIT_WORK_TREE_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !istate->split_index && + !state->current_attr_sources_present && + istate->sparse_index == INDEX_EXPANDED && + !state->disk_config_invalid && + ((!state->disk_config_seen && !state->disk_config_valid && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC && + has_usable_on_index_builtin_token(istate) && + has_usable_on_index_builtin_token(&parsed) && + !strcmp(istate->fsmonitor_last_update, + parsed.fsmonitor_last_update)) || + (state->disk_config_seen && state->disk_config_valid && + state->disk_semantic_valid && state->disk_attr_valid && + state->manifest.disk_valid && + (((state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL) || + (missing_fsmonitor_recovery && + clean_status_has_worktree_manifest_history(istate))) && + !memcmp(state->disk_config_hash, + state->current_config_hash, + istate->repo->hash_algo->rawsz) && + !memcmp(state->disk_semantic_hash, + state->current_semantic_hash, + istate->repo->hash_algo->rawsz) && + !memcmp(state->disk_attr_hash, + state->current_attr_hash, + istate->repo->hash_algo->rawsz))); + preserve_witness = !state->disk_config_seen && + !istate->fsmonitor_untracked_valid && + has_usable_on_index_builtin_token(istate); clean_status_invalidate_current_proof(istate); clean_status_copy_fsmonitor_history(istate, &parsed); FREE_AND_NULL(istate->fsmonitor_last_update); @@ -1092,6 +1530,9 @@ int clean_status_restore_external_history(struct index_state *istate) parsed.fsmonitor_last_update = NULL; istate->fsmonitor_dirty = parsed.fsmonitor_dirty; parsed.fsmonitor_dirty = NULL; + if (missing_fsmonitor_recovery) + ewah_each_bit(istate->fsmonitor_dirty, + invalidate_unwatched_recovered_entry, istate); istate->fsmonitor_token_valid = 1; istate->fsmonitor_extension_seen = 1; free_untracked_cache(istate->untracked); @@ -1108,7 +1549,12 @@ int clean_status_restore_external_history(struct index_state *istate) parsed.fsmonitor_untracked_valid; trace2_data_intmax("fsmonitor", istate->repo, "history/external-restored", 1); + if (missing_fsmonitor_recovery) + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-fsmn-recovered", 1); state->external_history_restored = 1; + state->external_history_owned_index = owned_index; + state->external_history_preserve_witness = preserve_witness; restored = 1; done: @@ -1130,6 +1576,56 @@ int clean_status_external_history_was_restored( return state && state->external_history_restored; } +int clean_status_external_history_needs_witness_preservation( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->external_history_restored && + state->external_history_preserve_witness && + !state->external_history_owned_index && + istate == istate->repo->index && + current_proof_is_writable(istate); +} + +int clean_status_has_recovered_tracked_stat( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->recovered_tracked_stat && + (istate->cache_changed & CE_ENTRY_CHANGED) && + istate == istate->repo->index && + current_proof_is_writable(istate); +} + +int clean_status_has_authenticated_bootstrap_manifest( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->authenticated_bootstrap_manifest && + state->manifest.current_valid && state->manifest.checked && + !state->manifest.current_invalidated && + (state->manifest.current_flags & + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX)) == + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX); +} + +int clean_status_external_history_owns_index( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->external_history_restored && + state->external_history_owned_index && + istate == istate->repo->index && + !getenv(INDEX_ENVIRONMENT) && !istate->split_index && + istate->sparse_index == INDEX_EXPANDED; +} + void clean_status_copy_fsmonitor_history(struct index_state *dst, const struct index_state *src) @@ -1151,6 +1647,9 @@ void clean_status_copy_fsmonitor_history(struct index_state *dst, dst->repo->hash_algo->rawsz); memcpy(dst_state->disk_semantic_hash, src_state->disk_semantic_hash, dst->repo->hash_algo->rawsz); + memcpy(dst_state->disk_tracked_policy_hash, + src_state->disk_tracked_policy_hash, + dst->repo->hash_algo->rawsz); memcpy(dst_state->disk_attr_hash, src_state->disk_attr_hash, dst->repo->hash_algo->rawsz); if (clean_status_manifest_load( @@ -1161,6 +1660,8 @@ void clean_status_copy_fsmonitor_history(struct index_state *dst, dst_state->disk_config_seen = 1; dst_state->disk_config_valid = 1; dst_state->disk_semantic_valid = src_state->disk_semantic_valid; + dst_state->disk_tracked_policy_valid = + src_state->disk_tracked_policy_valid; dst_state->disk_attr_valid = src_state->disk_attr_valid; dst_state->disk_config_invalid = 0; } diff --git a/clean-status-index.c b/clean-status-index.c index 4ea3c4bfc9d746..34dc23d6c0abfb 100644 --- a/clean-status-index.c +++ b/clean-status-index.c @@ -2,6 +2,7 @@ #include "clean-status.h" #include "clean-status-index.h" #include "clean-status-internal.h" +#include "clean-status-sidecar.h" #include "hash-framing.h" #include "object.h" #include "read-cache-ll.h" @@ -253,6 +254,44 @@ int clean_status_index_is_certifiable(const struct index_state *istate) clean_status_index_entries_are_certifiable(istate); } +int clean_status_index_is_certifiable_with_hardlinks( + const struct index_state *istate, uint32_t *hardlink_nr) +{ + const struct clean_status_state *state = istate->clean_status; + uint32_t nr = 0; + int checksum_is_bound = + !is_null_oid(&istate->oid) || + (clean_status_identity_is_durable() && state && + state->source_identity_valid); + + if (!hardlink_nr || !checksum_is_bound) + return 0; + *hardlink_nr = 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_UPTODATE | CE_HASHED | + CE_FSMONITOR_VALID | + CE_UPDATE_IN_BASE))) + return 0; + if (ce->ce_flags & CE_FSMONITOR_VALID) + continue; + if (!S_ISREG(ce->ce_mode) || + !(ce->ce_flags & CE_UPTODATE) || + nr == CLEAN_STATUS_HARDLINK_WITNESS_MAX) + return 0; + nr++; + } + if (nr && (!istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat)) + return 0; + *hardlink_nr = nr; + return 1; +} + static int index_entry_logical_state_is_supported( const struct cache_entry *ce, unsigned int extra_benign_flags) { diff --git a/clean-status-index.h b/clean-status-index.h index fc473d6c6f1330..1728b6021ddb3c 100644 --- a/clean-status-index.h +++ b/clean-status-index.h @@ -44,6 +44,8 @@ void clean_status_index_snapshot_release( int clean_status_index_entries_are_certifiable( const struct index_state *istate); int clean_status_index_is_certifiable(const struct index_state *istate); +int clean_status_index_is_certifiable_with_hardlinks( + const struct index_state *istate, uint32_t *hardlink_nr); int clean_status_index_logical_digest(const struct index_state *istate, unsigned char *out); int clean_status_index_logical_digest_after_status( diff --git a/clean-status-internal.h b/clean-status-internal.h index f19b03f89947b2..1bc3613920c9f1 100644 --- a/clean-status-internal.h +++ b/clean-status-internal.h @@ -20,6 +20,8 @@ struct clean_status_state { unsigned char disk_config_hash[GIT_MAX_RAWSZ]; unsigned char current_semantic_hash[GIT_MAX_RAWSZ]; unsigned char disk_semantic_hash[GIT_MAX_RAWSZ]; + unsigned char current_tracked_policy_hash[GIT_MAX_RAWSZ]; + unsigned char disk_tracked_policy_hash[GIT_MAX_RAWSZ]; unsigned char current_attr_hash[GIT_MAX_RAWSZ]; unsigned char current_attr_namespace_hash[GIT_MAX_RAWSZ]; unsigned char current_attr_portable_namespace_hash[GIT_MAX_RAWSZ]; @@ -27,6 +29,7 @@ struct clean_status_state { unsigned char source_logical_hash[GIT_MAX_RAWSZ]; unsigned current_config_valid : 1; unsigned current_semantic_valid : 1; + unsigned current_tracked_policy_valid : 1; unsigned current_attr_valid : 1; unsigned current_semantic_explicit : 1; unsigned current_attr_sources_present : 1; @@ -41,8 +44,13 @@ struct clean_status_state { unsigned source_index_identity_valid : 1; unsigned source_logical_hash_valid : 1; unsigned external_history_restored : 1; + unsigned external_history_owned_index : 1; + unsigned external_history_preserve_witness : 1; + unsigned recovered_tracked_stat : 1; + unsigned authenticated_bootstrap_manifest : 1; unsigned disk_config_valid : 1; unsigned disk_semantic_valid : 1; + unsigned disk_tracked_policy_valid : 1; unsigned disk_attr_valid : 1; unsigned disk_config_seen : 1; unsigned disk_config_invalid : 1; diff --git a/clean-status-manifest.c b/clean-status-manifest.c index 2750ddeb7280a9..4259385110f016 100644 --- a/clean-status-manifest.c +++ b/clean-status-manifest.c @@ -1,19 +1,40 @@ #include "git-compat-util.h" +#include "attr-fingerprint.h" +#include "attr.h" #include "attr-manifest.h" +#include "bloom.h" +#include "clean-status-config.h" #include "clean-status-index.h" +#include "clean-status-internal.h" #include "clean-status-manifest.h" +#include "commit.h" +#include "commit-graph.h" #include "dir.h" +#include "environment.h" +#include "fsmonitor.h" #include "fsmonitor-clean-proof.h" #include "fsmonitor-ll.h" #include "hash-framing.h" +#include "object.h" +#include "object-name.h" +#include "odb.h" +#include "path-namespace.h" #include "read-cache-ll.h" +#include "replace-object.h" #include "repository.h" +#include "semantic-verify-internal.h" #include "sparse-index.h" #include "trace2.h" +#include "tree.h" +#include "tree-walk.h" #include "worktree-attr-manifest.h" +#include "worktree-attr-source.h" +#include "wrapper.h" struct invalidate_manifest_data { struct index_state *istate; + const struct strbuf *baseline; + const struct strbuf *current; int invalidated; }; @@ -96,15 +117,653 @@ void clean_status_manifest_adopt_disk( state->current_invalidated = 0; } +static int find_manifest_entry( + const struct strbuf *manifest, const char *path, + const struct git_hash_algo *algo, struct attr_manifest_entry *found) +{ + struct attr_manifest_cursor cursor; + struct attr_manifest_entry entry; + size_t path_len = strlen(path); + int ret; + + if (attr_manifest_cursor_init(&cursor, manifest->buf, + manifest->len, algo)) + return -1; + while ((ret = attr_manifest_cursor_next(&cursor, &entry)) > 0) { + if (entry.path_len == path_len && + !memcmp(entry.path, path, path_len)) { + *found = entry; + return 0; + } + } + return -1; +} + +int clean_status_manifest_reconcile_deleted_attribute( + struct index_state *istate, const char *name) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + struct attr_fingerprint attrs; + struct attr_manifest_cursor cursor; + struct attr_manifest_writer writer; + struct attr_manifest_entry old, entry; + struct strbuf next = STRBUF_INIT; + const struct cache_entry *ce; + const char *base, *basename; + unsigned char hash[GIT_MAX_RAWSZ]; + unsigned char indexed_hash[GIT_MAX_RAWSZ]; + unsigned char worktree_hash[GIT_MAX_RAWSZ]; + unsigned char observed_hash[GIT_MAX_RAWSZ]; + unsigned int namespace_unstable = 0; + enum object_type type; + struct stat st; + void *content = NULL; + size_t size; + int pos, parent_fd, found = 0, next_entry, indexed, safe = 0; + int worktree_found, observed_found, changed; + + if (!name) + goto done; + base = find_last_dir_sep(name); + base = base ? base + 1 : name; + if (fspathcmp(base, GITATTRIBUTES_FILE) || + !state || !state->config_revalidated || + !state->current_attr_valid || state->filter_configured || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.current_invalidated || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !state->config_revalidated_token || + strcmp(state->config_revalidated_token, + istate->fsmonitor_last_update)) + goto done; + if (repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(&state->manifest.current, + name, algo, &old) || + (old.source != ATTR_MANIFEST_WORKTREE && + old.source != ATTR_MANIFEST_INDEX) || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present != state->current_attr_sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, + algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz)) + goto done; + pos = index_name_pos(istate, name, strlen(name)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + goto done; + indexed = old.source == ATTR_MANIFEST_INDEX; + if (indexed && memcmp(old.hash, ce->oid.hash, algo->rawsz)) + goto done; + content = odb_read_object(istate->repo->objects, + &ce->oid, &type, &size); + if (!content || type != OBJ_BLOB || size >= ATTR_MAX_FILE_SIZE) + goto done; + hash_buffer_digest(algo, content, size, indexed_hash); + if (!indexed && memcmp(old.hash, indexed_hash, algo->rawsz)) + goto done; + if (semantic_verify_root_init(istate->repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path || worktree_attr_source_read( + path, name, pos, algo, worktree_hash, &worktree_found) || + semantic_verify_resolve_parent( + path, name, pos, &parent_fd, &basename) || + (!worktree_found && + (!fstatat(parent_fd, basename, &st, AT_SYMLINK_NOFOLLOW) || + errno != ENOENT)) || + (worktree_found && + memcmp(worktree_hash, indexed_hash, algo->rawsz)) || + !semantic_verify_root_stable(root) || + !attr_manifest_valid(state->manifest.current.buf, + state->manifest.current.len, algo) || + attr_manifest_cursor_init(&cursor, + state->manifest.current.buf, + state->manifest.current.len, algo)) + goto done; + attr_manifest_writer_init(&writer, &next, algo); + while ((next_entry = attr_manifest_cursor_next(&cursor, &entry)) > 0) { + char *entry_name = xmemdupz(entry.path, entry.path_len); + int matches = !strcmp(entry_name, name); + int invalid = attr_manifest_writer_add( + &writer, entry_name, + matches ? + (worktree_found ? ATTR_MANIFEST_WORKTREE : + ATTR_MANIFEST_INDEX) : + entry.source, + matches ? + (worktree_found ? worktree_hash : ce->oid.hash) : + entry.hash); + + free(entry_name); + if (invalid) + goto done; + found += matches; + } + if (next_entry < 0 || found != 1 || + worktree_attr_source_read( + path, name, pos, algo, observed_hash, &observed_found) || + observed_found != worktree_found || + (!observed_found && + (!fstatat(parent_fd, basename, &st, AT_SYMLINK_NOFOLLOW) || + errno != ENOENT)) || + (observed_found && + memcmp(observed_hash, worktree_hash, algo->rawsz))) + goto done; + semantic_verify_path_free(path, &namespace_unstable, NULL); + path = NULL; + if (namespace_unstable || !semantic_verify_root_stable(root)) + goto done; + changed = indexed == worktree_found; + if (changed) { + hash_buffer_digest(algo, next.buf, next.len, hash); + strbuf_swap(&state->manifest.current, &next); + memcpy(state->manifest.current_hash, hash, algo->rawsz); + state->manifest.changed = 1; + state->manifest.global_fallback = 0; + } + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-reconciled", 1); + safe = 1; + +done: + if (path) + semantic_verify_path_free(path, NULL, NULL); + semantic_verify_root_clear(root); + strbuf_release(&next); + free(content); + return safe; +#else + (void)istate; + (void)name; + return 0; +#endif +} + +static int read_root_worktree_attributes( + struct repository *repo, struct strbuf *out) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct semantic_verify_root *root = NULL; + struct stat before, after, named; + size_t size; + int fd = -1, ret = -1; + char extra; + + if (semantic_verify_root_init(repo, &root)) + goto done; + fd = semantic_verify_openat(root->fd, GITATTRIBUTES_FILE, + O_RDONLY | O_NONBLOCK | O_NOFOLLOW); + if (fd < 0 || fstat(fd, &before) || !S_ISREG(before.st_mode) || + before.st_nlink != 1 || before.st_dev != root->stat.st_dev || + before.st_size < 0 || before.st_size >= ATTR_MAX_FILE_SIZE) + goto done; + size = xsize_t(before.st_size); + strbuf_grow(out, size); + strbuf_setlen(out, size); + if ((size_t)read_in_full(fd, out->buf, size) != size || + read(fd, &extra, 1) != 0 || fstat(fd, &after) || + fstatat(root->fd, GITATTRIBUTES_FILE, &named, + AT_SYMLINK_NOFOLLOW) || + !path_namespace_stat_equal(&before, &after) || + !path_namespace_stat_equal(&after, &named) || + !semantic_verify_root_stable(root)) + goto done; + ret = 0; + +done: + if (fd >= 0) + close(fd); + semantic_verify_root_clear(root); + if (ret) + strbuf_reset(out); + return ret; +#else + (void)repo; + (void)out; + return -1; +#endif +} + +static int find_previous_root_attributes( + struct repository *repo, struct object_id *oid) +{ + struct bloom_filter_settings *settings; + struct bloom_key key = { 0 }; + struct object_id head_oid; + struct commit *commit; + unsigned int visited = 0, bloom_hits = 0, tree_inspections = 0, limit; + int found = 0; + + if (repo_get_oid(repo, "HEAD", &head_oid) || + !(commit = lookup_commit_reference_gently( + repo, &head_oid, 1))) + return -1; + settings = get_bloom_filter_settings(repo); + limit = settings ? 8192 : 128; + if (settings) + bloom_key_fill(&key, GITATTRIBUTES_FILE, + strlen(GITATTRIBUTES_FILE), settings); + while (commit && visited < limit) { + struct bloom_filter *filter = NULL; + struct commit *parent; + struct tree *current_tree, *parent_tree; + struct object_id current_oid, parent_oid; + unsigned short current_mode, parent_mode; + + visited++; + if (repo_parse_commit_gently(repo, commit, 1) || + !commit->parents) + break; + parent = commit->parents->item; + if (settings) + filter = get_bloom_filter(repo, commit); + if (filter && filter->version >= 0 && + (uint32_t)filter->version == settings->hash_version && + bloom_filter_contains(filter, &key, settings) == 0) { + bloom_hits++; + commit = parent; + continue; + } + if (tree_inspections >= 512) + break; + tree_inspections++; + if (repo_parse_commit_gently(repo, parent, 1) || + !(current_tree = repo_get_commit_tree(repo, commit)) || + !(parent_tree = repo_get_commit_tree(repo, parent)) || + get_tree_entry(repo, ¤t_tree->object.oid, + GITATTRIBUTES_FILE, + ¤t_oid, ¤t_mode) || + get_tree_entry(repo, &parent_tree->object.oid, + GITATTRIBUTES_FILE, + &parent_oid, &parent_mode) || + !S_ISREG(current_mode) || !S_ISREG(parent_mode)) + break; + if (!oideq(¤t_oid, &parent_oid)) { + oidcpy(oid, &parent_oid); + found = 1; + break; + } + commit = parent; + } + if (settings) + bloom_key_clear(&key); + trace2_data_intmax("fsmonitor", repo, + "semantic/attribute-history-commits", visited); + trace2_data_intmax("fsmonitor", repo, + "semantic/attribute-history-bloom-skips", bloom_hits); + trace2_data_intmax("fsmonitor", repo, + "semantic/attribute-history-tree-inspections", + tree_inspections); + return found ? 0 : -1; +} + +static void *read_authenticated_attribute_blob( + struct index_state *istate, + const struct attr_manifest_entry *old, + const struct object_id *oid, size_t *size) +{ + const struct git_hash_algo *algo = istate->repo->hash_algo; + unsigned char hash[GIT_MAX_RAWSZ]; + enum object_type type; + void *content; + + if (old->source == ATTR_MANIFEST_INDEX && + memcmp(old->hash, oid->hash, algo->rawsz)) + return NULL; + content = odb_read_object(istate->repo->objects, + oid, &type, size); + if (!content || type != OBJ_BLOB || + *size >= ATTR_MAX_FILE_SIZE) { + free(content); + return NULL; + } + if (old->source == ATTR_MANIFEST_WORKTREE) { + hash_buffer_digest(algo, content, *size, hash); + if (memcmp(old->hash, hash, algo->rawsz)) { + free(content); + return NULL; + } + } else if (old->source != ATTR_MANIFEST_INDEX) { + free(content); + return NULL; + } + return content; +} + +static void *read_authenticated_old_attributes( + struct index_state *istate, + const struct attr_manifest_entry *old, + const struct cache_entry *current, size_t *size) +{ + struct object_id parent_oid, historical_oid; + const struct object_id *candidates[2]; + void *content; + size_t nr = 1; + + candidates[0] = ¤t->oid; + if (!repo_get_oid_blob(istate->repo, + "HEAD^:" GITATTRIBUTES_FILE, &parent_oid) && + !oideq(¤t->oid, &parent_oid)) + candidates[nr++] = &parent_oid; + for (size_t i = 0; i < nr; i++) { + content = read_authenticated_attribute_blob( + istate, old, candidates[i], size); + if (content) + return content; + } + if (find_previous_root_attributes(istate->repo, &historical_oid)) + return NULL; + return read_authenticated_attribute_blob( + istate, old, &historical_oid, size); +} + +int clean_status_manifest_reconcile_display_only_attribute( + struct index_state *istate, const char *path) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_cursor cursor; + struct attr_manifest_writer writer; + struct attr_manifest_entry old, entry; + struct strbuf worktree = STRBUF_INIT; + struct strbuf observed = STRBUF_INIT; + struct strbuf next = STRBUF_INIT; + const struct cache_entry *ce; + unsigned char worktree_hash[GIT_MAX_RAWSZ]; + unsigned char indexed_hash[GIT_MAX_RAWSZ]; + unsigned char manifest_hash[GIT_MAX_RAWSZ]; + enum object_type type; + void *previous = NULL, *indexed = NULL; + size_t previous_len, indexed_len; + int pos, found = 0, next_entry, safe = 0; + + if (!path || strcmp(path, GITATTRIBUTES_FILE) || !state || + !state->config_enforced || !state->config_revalidated || + !state->current_config_valid || !state->current_semantic_valid || + !state->current_attr_valid || state->current_attr_sources_present || + state->filter_configured || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->manifest.disk_valid || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !state->config_revalidated_token || + strcmp(state->config_revalidated_token, + istate->fsmonitor_last_update) || + repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(&state->manifest.current, + path, algo, &old) || + old.source != ATTR_MANIFEST_WORKTREE || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, + algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz) || + clean_status_config_read_repository(istate->repo, &config) || + !config.finalized || config.filter_configured || + memcmp(config.hash, state->current_config_hash, algo->rawsz) || + memcmp(config.semantic_hash, + state->current_semantic_hash, algo->rawsz)) + goto done; + pos = index_name_pos(istate, path, strlen(path)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + goto done; + previous = read_authenticated_old_attributes( + istate, &old, ce, &previous_len); + if (!previous || + read_root_worktree_attributes(istate->repo, &worktree)) + goto done; + indexed = odb_read_object(istate->repo->objects, + &ce->oid, &type, &indexed_len); + if (!indexed || type != OBJ_BLOB || + indexed_len >= ATTR_MAX_FILE_SIZE) + goto done; + hash_buffer_digest(algo, indexed, indexed_len, indexed_hash); + hash_buffer_digest(algo, worktree.buf, worktree.len, worktree_hash); + if ((memcmp(indexed_hash, worktree_hash, algo->rawsz) && + (indexed_len != previous_len || + memcmp(indexed, previous, indexed_len))) || + !attr_manifest_only_linguist_generated_changed( + previous, previous_len, worktree.buf, worktree.len) || + !attr_manifest_valid(state->manifest.current.buf, + state->manifest.current.len, algo) || + attr_manifest_cursor_init(&cursor, + state->manifest.current.buf, + state->manifest.current.len, algo)) + goto done; + attr_manifest_writer_init(&writer, &next, algo); + while ((next_entry = attr_manifest_cursor_next(&cursor, &entry)) > 0) { + char *entry_path = xmemdupz(entry.path, entry.path_len); + int matches = !strcmp(entry_path, path); + int invalid = attr_manifest_writer_add( + &writer, entry_path, + matches ? ATTR_MANIFEST_WORKTREE : entry.source, + matches ? worktree_hash : entry.hash); + + free(entry_path); + if (invalid) + goto done; + found += matches; + } + if (next_entry < 0 || found != 1 || + read_root_worktree_attributes(istate->repo, &observed) || + observed.len != worktree.len || + memcmp(observed.buf, worktree.buf, worktree.len)) + goto done; + hash_buffer_digest(algo, next.buf, next.len, manifest_hash); + strbuf_swap(&state->manifest.current, &next); + memcpy(state->manifest.current_hash, + manifest_hash, algo->rawsz); + state->manifest.changed = 1; + state->manifest.global_fallback = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/nonconversion-attributes", 1); + safe = 1; + +done: + free(previous); + free(indexed); + strbuf_release(&worktree); + strbuf_release(&observed); + strbuf_release(&next); + return safe; +#else + (void)istate; + (void)path; + return 0; +#endif +} + +int clean_status_manifest_accept_current_display_only_attribute( + struct index_state *istate, const char *path) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_entry current; + struct strbuf worktree = STRBUF_INIT; + struct strbuf observed = STRBUF_INIT; + const struct cache_entry *ce; + unsigned char worktree_hash[GIT_MAX_RAWSZ]; + enum object_type type; + void *indexed = NULL; + size_t indexed_len; + int pos, safe = 0; + + if (!path || strcmp(path, GITATTRIBUTES_FILE) || !state || + !state->config_enforced || !state->config_revalidated || + !state->current_config_valid || !state->current_semantic_valid || + !state->current_attr_valid || state->current_attr_sources_present || + state->filter_configured || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->manifest.disk_valid || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !state->config_revalidated_token || + strcmp(state->config_revalidated_token, + istate->fsmonitor_last_update) || + repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(&state->manifest.current, + path, algo, ¤t) || + current.source != ATTR_MANIFEST_WORKTREE || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, + algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz) || + clean_status_config_read_repository(istate->repo, &config) || + !config.finalized || config.filter_configured || + memcmp(config.hash, state->current_config_hash, algo->rawsz) || + memcmp(config.semantic_hash, + state->current_semantic_hash, algo->rawsz)) + goto done; + pos = index_name_pos(istate, path, strlen(path)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID) || + read_root_worktree_attributes(istate->repo, &worktree)) + goto done; + hash_buffer_digest(algo, worktree.buf, worktree.len, worktree_hash); + if (memcmp(current.hash, worktree_hash, algo->rawsz)) + goto done; + indexed = odb_read_object(istate->repo->objects, + &ce->oid, &type, &indexed_len); + if (!indexed || type != OBJ_BLOB || + indexed_len >= ATTR_MAX_FILE_SIZE || + !attr_manifest_only_linguist_generated_changed( + indexed, indexed_len, worktree.buf, worktree.len) || + read_root_worktree_attributes(istate->repo, &observed) || + observed.len != worktree.len || + memcmp(observed.buf, worktree.buf, worktree.len)) + goto done; + safe = 1; + +done: + free(indexed); + strbuf_release(&worktree); + strbuf_release(&observed); + return safe; +#else + (void)istate; + (void)path; + return 0; +#endif +} + +static int root_attributes_only_affect_display( + const struct invalidate_manifest_data *data, const char *path, + int *index_pos) +{ + struct index_state *istate = data->istate; + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_entry old, current; + struct strbuf worktree = STRBUF_INIT; + const struct cache_entry *ce; + unsigned char hash[GIT_MAX_RAWSZ]; + void *staged = NULL; + size_t staged_len; + int pos, safe = 0; + + if (strcmp(path, GITATTRIBUTES_FILE) || !data->baseline || + !data->current || !state || + (state->current_attr_valid && state->current_attr_sources_present) || + state->filter_configured || + repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(data->baseline, path, algo, &old) || + find_manifest_entry(data->current, path, algo, ¤t) || + current.source != ATTR_MANIFEST_WORKTREE || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present || + (state->current_attr_valid && + memcmp(attrs.content_hash, state->current_attr_hash, algo->rawsz)) || + clean_status_config_read_repository(istate->repo, &config) || + !config.finalized || config.filter_configured) + goto done; + pos = index_name_pos(istate, path, strlen(path)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + goto done; + staged = read_authenticated_old_attributes( + istate, &old, ce, &staged_len); + if (!staged) + goto done; + if (read_root_worktree_attributes(istate->repo, &worktree)) + goto done; + hash_buffer_digest(algo, worktree.buf, worktree.len, hash); + if (memcmp(current.hash, hash, algo->rawsz) || + !attr_manifest_only_linguist_generated_changed( + staged, staged_len, worktree.buf, worktree.len)) + goto done; + *index_pos = pos; + safe = 1; + +done: + free(staged); + strbuf_release(&worktree); + return safe; +} + static int invalidate_manifest_path(const struct attr_manifest_entry *entry, void *cb_data) { struct invalidate_manifest_data *data = cb_data; char *path = xmemdupz(entry->path, entry->path_len); + int pos; untracked_cache_invalidate_trimmed_path(data->istate, path, 0); - data->invalidated += - fsmonitor_invalidate_attributes_path(data->istate, path); + if (root_attributes_only_affect_display(data, path, &pos)) { + git_attr_invalidate_all(); + fsmonitor_invalidate_cache_entry(data->istate->cache[pos]); + data->istate->cache_changed |= FSMONITOR_CHANGED; + trace2_data_intmax("fsmonitor", data->istate->repo, + "semantic/nonconversion-attributes", 1); + } else { + data->invalidated += + fsmonitor_invalidate_attributes_path(data->istate, path); + } free(path); return 0; } @@ -139,6 +798,8 @@ int clean_status_manifest_refresh(struct index_state *istate, return -1; } if (baseline) { + invalidation.baseline = baseline; + invalidation.current = &next; if (attr_manifest_for_each_changed( baseline->buf, baseline->len, next.buf, next.len, algo, diff --git a/clean-status-manifest.h b/clean-status-manifest.h index 394fe25a888c0d..ba90498d56c247 100644 --- a/clean-status-manifest.h +++ b/clean-status-manifest.h @@ -31,6 +31,12 @@ void clean_status_manifest_adopt_disk( struct clean_status_manifest_state *state); int clean_status_manifest_refresh(struct index_state *istate, struct clean_status_manifest_state *state); +int clean_status_manifest_reconcile_deleted_attribute( + struct index_state *istate, const char *path); +int clean_status_manifest_reconcile_display_only_attribute( + struct index_state *istate, const char *path); +int clean_status_manifest_accept_current_display_only_attribute( + struct index_state *istate, const char *path); void clean_status_manifest_invalidate( struct clean_status_manifest_state *state); diff --git a/clean-status-sidecar-issue.c b/clean-status-sidecar-issue.c index 3c424417849f4e..d046307b2a7d34 100644 --- a/clean-status-sidecar-issue.c +++ b/clean-status-sidecar-issue.c @@ -9,10 +9,13 @@ #include "fsmonitor-ll.h" #include "fsmonitor-settings.h" #include "lockfile.h" +#include "object-file.h" #include "object-name.h" +#include "path-namespace.h" #include "preload-index.h" #include "read-cache-ll.h" #include "repository.h" +#include "semantic-verify-internal.h" #include "trace2.h" #include "wt-status.h" @@ -68,7 +71,8 @@ static int history_is_certifiable(const struct index_state *istate) } static int fsmonitor_state_is_certifiable( - struct repository *repo, const struct index_state *istate) + struct repository *repo, const struct index_state *istate, + uint32_t *hardlink_nr) { return !istate->split_index && istate->sparse_index == INDEX_EXPANDED && @@ -78,7 +82,101 @@ static int fsmonitor_state_is_certifiable( istate->fsmonitor_last_update && strlen(istate->fsmonitor_last_update) <= FSMONITOR_CLEAN_PROOF_TOKEN_MAX && - clean_status_index_is_certifiable(istate); + clean_status_index_is_certifiable_with_hardlinks( + istate, hardlink_nr); +} + +static int capture_hardlink_witnesses( + struct repository *repo, const struct index_state *istate, + uint32_t expected, struct strbuf *witnesses) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN && !defined(NO_NSEC) + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + unsigned int namespace_unstable = 0; + uint32_t captured = 0, verified = 0; + int ret = -1; + + if (!expected) + return 0; + if (!repo->config_values_private_.trust_ctime || + !repo->config_values_private_.check_stat || + semantic_verify_root_init(repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path) + goto done; + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + struct path_stat_identity identity; + struct stat held, named; + const char *basename; + int parent_fd, fd; + + if (ce->ce_flags & CE_FSMONITOR_VALID) + continue; + if (semantic_verify_resolve_parent( + path, ce->name, i, &parent_fd, &basename)) + goto done; + fd = semantic_verify_openat( + parent_fd, basename, + O_RDONLY | O_NONBLOCK | O_NOFOLLOW); + if (fd < 0) + goto done; + if (fstat(fd, &held) || !S_ISREG(held.st_mode) || + held.st_nlink <= 1 || held.st_dev != root->stat.st_dev || + match_stat_data(&ce->ce_stat_data, &held)) { + close(fd); + goto done; + } + if (ce->ce_stat_data.sd_ctime.nsec != ST_CTIME_NSEC(held) || + ce->ce_stat_data.sd_mtime.nsec != ST_MTIME_NSEC(held)) { + struct object_id observed; + struct stat after; + + if (index_fd(repo->index, &observed, xdup(fd), &held, + OBJ_BLOB, ce->name, 0) || + !oideq(&observed, &ce->oid) || fstat(fd, &after) || + !path_namespace_stat_equal(&held, &after)) { + close(fd); + goto done; + } + verified++; + } + if (fstatat(parent_fd, basename, &named, + AT_SYMLINK_NOFOLLOW) || + !path_namespace_stat_equal(&held, &named)) { + close(fd); + goto done; + } + path_stat_identity_init(&identity, &held); + close(fd); + if (clean_status_sidecar_append_hardlink( + witnesses, ce->name, &identity)) + goto done; + captured++; + } + if (captured != expected || !semantic_verify_root_stable(root)) + goto done; + if (verified) + trace2_data_intmax("status", repo, + "clean-proof/hardlink-content-verified", verified); + ret = 0; + +done: + semantic_verify_path_free(path, &namespace_unstable, NULL); + if (namespace_unstable || (root && !semantic_verify_root_stable(root))) + ret = -1; + semantic_verify_root_clear(root); + if (ret) + strbuf_reset(witnesses); + return ret; +#else + (void)repo; + (void)istate; + (void)witnesses; + return expected ? -1 : 0; +#endif } static int untracked_scan_is_certifiable( @@ -105,9 +203,11 @@ int clean_status_issue_sidecar( struct index_state *istate = repo->index; struct clean_status_index_snapshot index = { .fd = -1 }; struct clean_status_sidecar sidecar = { 0 }; + struct strbuf hardlinks = STRBUF_INIT; struct object_id exclude_digest, head_tree; struct stat scanned_worktree; unsigned char repo_hash[GIT_MAX_RAWSZ]; + uint32_t hardlink_nr = 0; int installed = 0; if (!is_lock_file_locked(index_lock) || @@ -121,7 +221,7 @@ int clean_status_issue_sidecar( goto done; } if (getenv(INDEX_ENVIRONMENT) || - !fsmonitor_state_is_certifiable(repo, istate) || + !fsmonitor_state_is_certifiable(repo, istate, &hardlink_nr) || !untracked_scan_is_certifiable( status, &exclude_digest, &scanned_worktree)) { trace_miss(repo, "issue-scan-or-index-shape"); @@ -159,6 +259,16 @@ int clean_status_issue_sidecar( oidcpy(&sidecar.proof.exclude_source_digest, &exclude_digest); sidecar.token = (const unsigned char *)istate->fsmonitor_last_update; sidecar.token_len = strlen(istate->fsmonitor_last_update); + if (capture_hardlink_witnesses( + repo, istate, hardlink_nr, &hardlinks)) { + trace_miss(repo, "issue-hardlink-witness"); + goto done; + } + if (hardlink_nr) { + sidecar.hardlinks = (const unsigned char *)hardlinks.buf; + sidecar.hardlinks_len = hardlinks.len; + sidecar.hardlink_nr = hardlink_nr; + } if (clean_status_sidecar_install( repo->index_file, &sidecar, &index, repo->hash_algo)) { @@ -166,10 +276,14 @@ int clean_status_issue_sidecar( goto done; } rollback_lock_file(index_lock); + if (hardlink_nr) + trace2_data_intmax("status", repo, + "clean-proof/hardlink-witnesses", hardlink_nr); trace2_data_intmax("status", repo, "clean-proof/sidecar", 1); installed = 1; done: + strbuf_release(&hardlinks); clean_status_index_snapshot_release(&index); return installed; } diff --git a/clean-status-sidecar.c b/clean-status-sidecar.c index f387881a79368a..b40959bcd301ee 100644 --- a/clean-status-sidecar.c +++ b/clean-status-sidecar.c @@ -12,6 +12,7 @@ #include "hash-framing.h" #include "lockfile.h" #include "path.h" +#include "read-cache-ll.h" #include "repository.h" #include "replace-object.h" #include "strbuf.h" @@ -19,7 +20,7 @@ #include "wrapper.h" #define CLEAN_STATUS_SIDECAR_MAGIC "CSTS" -#define CLEAN_STATUS_SIDECAR_MAX_SIZE 8192 +#define CLEAN_STATUS_HARDLINK_PATH_MAX 4096 #define CLEAN_STATUS_FILESYSTEM_ID_SIZE 16 struct clean_status_filesystem_id { @@ -62,6 +63,108 @@ static int proof_valid(const struct clean_status_proof *proof, proof->exclude_source_digest.algo == hash_algo_by_ptr(algo); } +static int hardlink_path_valid(const unsigned char *path, size_t len, + const struct path_stat_identity *identity) +{ + char *name; + int valid; + + if (!path || !len || len > CLEAN_STATUS_HARDLINK_PATH_MAX || + memchr(path, '\0', len) || + identity->fields[2] > UINT32_MAX || + !S_ISREG((mode_t)identity->fields[2]) || + identity->fields[3] <= 1) + return 0; + name = xmemdupz(path, len); + valid = verify_path(name, (unsigned)identity->fields[2]); + free(name); + return valid; +} + +int clean_status_sidecar_append_hardlink( + struct strbuf *out, const char *path, + const struct path_stat_identity *identity) +{ + uint32_t path_len; + uint64_t field; + size_t len; + + if (!out || !path || !identity) + return -1; + len = strlen(path); + if (!hardlink_path_valid((const unsigned char *)path, len, identity) || + out->len > CLEAN_STATUS_SIDECAR_MAX_SIZE - + (sizeof(path_len) + len + CLEAN_STATUS_IDENTITY_SIZE)) + return -1; + put_be32(&path_len, (uint32_t)len); + strbuf_add(out, &path_len, sizeof(path_len)); + strbuf_add(out, path, len); + for (size_t i = 0; i < PATH_STAT_IDENTITY_FIELDS; i++) { + put_be64(&field, identity->fields[i]); + strbuf_add(out, &field, sizeof(field)); + } + return 0; +} + +int clean_status_sidecar_next_hardlink( + const unsigned char **cursor, const unsigned char *end, + const unsigned char **path, size_t *path_len, + struct path_stat_identity *identity) +{ + const unsigned char *p; + size_t len; + + if (!cursor || !*cursor || !end || !path || !path_len || !identity || + *cursor > end || (size_t)(end - *cursor) < sizeof(uint32_t)) + return -1; + p = *cursor; + len = get_be32(p); + p += sizeof(uint32_t); + if (len > (size_t)(end - p) || + (size_t)(end - p) - len < CLEAN_STATUS_IDENTITY_SIZE) + return -1; + *path = p; + *path_len = len; + p += len; + for (size_t i = 0; i < PATH_STAT_IDENTITY_FIELDS; i++) { + identity->fields[i] = get_be64(p); + p += sizeof(uint64_t); + } + if (!hardlink_path_valid(*path, *path_len, identity)) + return -1; + *cursor = p; + return 0; +} + +static int hardlink_block_valid(const unsigned char *block, size_t len, + uint32_t nr) +{ + struct path_stat_identity identity; + const unsigned char *cursor = block, *previous = NULL; + const unsigned char *path; + size_t path_len, previous_len = 0; + + if (!nr || nr > CLEAN_STATUS_HARDLINK_WITNESS_MAX || !block || + len > CLEAN_STATUS_SIDECAR_MAX_SIZE) + return 0; + for (uint32_t i = 0; i < nr; i++) { + if (clean_status_sidecar_next_hardlink( + &cursor, block + len, &path, &path_len, &identity)) + return 0; + if (previous) { + size_t common = previous_len < path_len ? + previous_len : path_len; + int order = memcmp(previous, path, common); + + if (order > 0 || (!order && previous_len >= path_len)) + return 0; + } + previous = path; + previous_len = path_len; + } + return cursor == block + len; +} + int clean_status_sidecar_parse(struct clean_status_sidecar *sidecar, const void *data, size_t len, const struct git_hash_algo *algo) @@ -71,15 +174,18 @@ int clean_status_sidecar_parse(struct clean_status_sidecar *sidecar, size_t minimum = 4 + 2 * sizeof(uint32_t) + CLEAN_STATUS_IDENTITY_SIZE + 3 * sizeof(uint32_t) + 6 * algo->rawsz + 1; - uint32_t flags, token_len; + uint32_t flags, token_len, version; memset(sidecar, 0, sizeof(*sidecar)); - if (len < minimum || memcmp(p, CLEAN_STATUS_SIDECAR_MAGIC, 4) || + if (len < minimum || len > CLEAN_STATUS_SIDECAR_MAX_SIZE || + memcmp(p, CLEAN_STATUS_SIDECAR_MAGIC, 4) || !checksum_valid(data, len, algo)) return -1; end = p + len - algo->rawsz; p += 4; - if (get_be32(p) != CLEAN_STATUS_SIDECAR_VERSION) + version = get_be32(p); + if (version != CLEAN_STATUS_SIDECAR_VERSION && + version != CLEAN_STATUS_SIDECAR_HARDLINK_VERSION) return -1; p += sizeof(uint32_t); flags = get_be32(p); @@ -105,11 +211,24 @@ int clean_status_sidecar_parse(struct clean_status_sidecar *sidecar, token_len = get_be32(p); p += sizeof(uint32_t); if (!proof_valid(&sidecar->proof, algo) || - (size_t)(end - p) != token_len || + (size_t)(end - p) < token_len || !token_valid(p, token_len)) return -1; sidecar->token = p; sidecar->token_len = token_len; + p += token_len; + if (version == CLEAN_STATUS_SIDECAR_VERSION) + return p == end ? 0 : -1; + if ((size_t)(end - p) < sizeof(uint32_t)) + return -1; + sidecar->hardlink_nr = get_be32(p); + p += sizeof(uint32_t); + sidecar->hardlinks = p; + sidecar->hardlinks_len = end - p; + if (!hardlink_block_valid(sidecar->hardlinks, + sidecar->hardlinks_len, + sidecar->hardlink_nr)) + return -1; return 0; } @@ -122,11 +241,18 @@ int clean_status_sidecar_write(struct strbuf *out, strbuf_reset(out); if (!proof_valid(&sidecar->proof, algo) || sidecar->token_len > UINT32_MAX || - !token_valid(sidecar->token, sidecar->token_len)) + !token_valid(sidecar->token, sidecar->token_len) || + (sidecar->hardlink_nr ? + !hardlink_block_valid(sidecar->hardlinks, + sidecar->hardlinks_len, + sidecar->hardlink_nr) : + (sidecar->hardlinks || sidecar->hardlinks_len))) return -1; strbuf_add(out, CLEAN_STATUS_SIDECAR_MAGIC, 4); - put_be32(&value, CLEAN_STATUS_SIDECAR_VERSION); + put_be32(&value, sidecar->hardlink_nr ? + CLEAN_STATUS_SIDECAR_HARDLINK_VERSION : + CLEAN_STATUS_SIDECAR_VERSION); strbuf_add(out, &value, sizeof(value)); put_be32(&value, 0); strbuf_add(out, &value, sizeof(value)); @@ -144,6 +270,15 @@ int clean_status_sidecar_write(struct strbuf *out, put_be32(&value, sidecar->token_len); strbuf_add(out, &value, sizeof(value)); strbuf_add(out, sidecar->token, sidecar->token_len); + if (sidecar->hardlink_nr) { + put_be32(&value, sidecar->hardlink_nr); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, sidecar->hardlinks, sidecar->hardlinks_len); + } + if (out->len > CLEAN_STATUS_SIDECAR_MAX_SIZE - algo->rawsz) { + strbuf_reset(out); + return -1; + } hash_append_checksum(out, algo); return 0; } @@ -277,7 +412,7 @@ int clean_status_sidecar_install( index_path, sidecar, snapshot, algo) || clean_status_sidecar_write(&encoded, sidecar, algo)) goto done; - sidecar_fd = hold_lock_file_for_update(&lock, path, 0); + sidecar_fd = hold_lock_file_for_update(&lock, path, LOCK_NO_DEREF); if (sidecar_fd < 0 || (size_t)write_in_full(sidecar_fd, encoded.buf, encoded.len) != encoded.len || diff --git a/clean-status-sidecar.h b/clean-status-sidecar.h index 8149acbe5b86bb..a496246c28ddff 100644 --- a/clean-status-sidecar.h +++ b/clean-status-sidecar.h @@ -11,6 +11,9 @@ struct repository; struct stat; #define CLEAN_STATUS_SIDECAR_VERSION 1 +#define CLEAN_STATUS_SIDECAR_HARDLINK_VERSION 2 +#define CLEAN_STATUS_HARDLINK_WITNESS_MAX 4096 +#define CLEAN_STATUS_SIDECAR_MAX_SIZE (1024 * 1024) struct clean_status_proof { uint32_t index_version; @@ -27,6 +30,9 @@ struct clean_status_sidecar { struct clean_status_proof proof; const unsigned char *token; size_t token_len; + const unsigned char *hardlinks; + size_t hardlinks_len; + uint32_t hardlink_nr; }; struct clean_status_sidecar_record { @@ -44,6 +50,13 @@ int clean_status_sidecar_parse(struct clean_status_sidecar *sidecar, int clean_status_sidecar_write(struct strbuf *out, const struct clean_status_sidecar *sidecar, const struct git_hash_algo *algo); +int clean_status_sidecar_append_hardlink( + struct strbuf *out, const char *path, + const struct path_stat_identity *identity); +int clean_status_sidecar_next_hardlink( + const unsigned char **cursor, const unsigned char *end, + const unsigned char **path, size_t *path_len, + struct path_stat_identity *identity); int clean_status_sidecar_load( const char *index_path, const struct git_hash_algo *algo, struct clean_status_sidecar_record *record); diff --git a/clean-status.c b/clean-status.c index 47d49c2d78df1d..603edf66e524fb 100644 --- a/clean-status.c +++ b/clean-status.c @@ -16,6 +16,7 @@ static struct repository *configured_repo; static unsigned char configured_hash[GIT_MAX_RAWSZ]; static unsigned char configured_semantic_hash[GIT_MAX_RAWSZ]; +static unsigned char configured_tracked_policy_hash[GIT_MAX_RAWSZ]; static struct repository *external_history_repo; static struct repository *progress_repo; static int configured_hash_valid; @@ -105,6 +106,8 @@ void clean_status_set_config_digest( memcpy(configured_hash, digest->hash, repo->hash_algo->rawsz); memcpy(configured_semantic_hash, digest->semantic_hash, repo->hash_algo->rawsz); + memcpy(configured_tracked_policy_hash, + digest->tracked_policy_hash, repo->hash_algo->rawsz); } void clean_status_attach_config(struct index_state *istate) @@ -121,8 +124,12 @@ void clean_status_attach_config(struct index_state *istate) istate->repo->hash_algo->rawsz); memcpy(state->current_semantic_hash, configured_semantic_hash, istate->repo->hash_algo->rawsz); + memcpy(state->current_tracked_policy_hash, + configured_tracked_policy_hash, + istate->repo->hash_algo->rawsz); state->current_config_valid = 1; state->current_semantic_valid = 1; + state->current_tracked_policy_valid = 1; state->current_semantic_explicit = configured_semantic_explicit; state->config_enforced = 1; state->filter_configured = configured_filter_configured; @@ -515,6 +522,25 @@ int clean_status_manifest_global_fallback(const struct index_state *istate) istate->clean_status->manifest.global_fallback; } +int clean_status_has_authenticated_worktree_manifest( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->disk_config_valid && + !state->disk_config_invalid && istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && state->disk_config_token && + !strcmp(state->disk_config_token, + istate->fsmonitor_last_update) && + state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL && + state->manifest.current_valid && state->manifest.checked && + !state->manifest.current_invalidated && + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL; +} + int clean_status_worktree_manifest_needs_refresh( const struct index_state *istate) { diff --git a/clean-status.h b/clean-status.h index 47600588643244..b6a78bcf8b7b8c 100644 --- a/clean-status.h +++ b/clean-status.h @@ -59,6 +59,8 @@ void clean_status_release_proof_epoch( int clean_status_fsmonitor_config_mismatch(const struct index_state *istate); int clean_status_fsmonitor_strong_mismatch(const struct index_state *istate); +int clean_status_try_preserve_tracked_config_epoch( + struct index_state *istate); int clean_status_revalidated_token_matches( const struct index_state *istate); @@ -77,6 +79,10 @@ void clean_status_begin_fsmonitor_semantic_baseline( int clean_status_refresh_worktree_manifest(struct index_state *istate); int clean_status_manifest_global_fallback(const struct index_state *istate); +int clean_status_has_authenticated_worktree_manifest( + const struct index_state *istate); +int clean_status_has_authenticated_bootstrap_manifest( + const struct index_state *istate); int clean_status_worktree_manifest_needs_refresh( const struct index_state *istate); void clean_status_invalidate_current_manifest(struct index_state *istate); @@ -125,6 +131,12 @@ void clean_status_write_fsmonitor_config(struct strbuf *out, int clean_status_restore_external_history(struct index_state *istate); int clean_status_external_history_was_restored( const struct index_state *istate); +int clean_status_external_history_needs_witness_preservation( + const struct index_state *istate); +int clean_status_has_recovered_tracked_stat( + const struct index_state *istate); +int clean_status_external_history_owns_index( + const struct index_state *istate); void clean_status_capture_external_history_source( struct index_state *istate); int clean_status_save_external_history(struct index_state *istate); diff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c index e3dca14b620ee3..6181dcba51472d 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-ipc.h" #include "fsmonitor-ll.h" #include "fsm-listen.h" #include "fsmonitor--daemon.h" @@ -42,6 +43,7 @@ struct rename_entry { struct fsm_listen_data { int fd_inotify; + const char *worktree_identity; enum shutdown_reason shutdown; struct hashmap watches; struct hashmap renames; @@ -102,9 +104,12 @@ static int add_watch(const char *path, struct fsm_listen_data *data) return 0; /* directory was deleted or is not a directory */ if (errno == EEXIST) return 0; /* watch already exists, no action needed */ - if (errno == ENOSPC) + if (errno == ENOSPC) { + fsmonitor_ipc__record_watch_limit_failure( + data->worktree_identity); return error(_("inotify watch limit reached; " "increase fs.inotify.max_user_watches")); + } return error_errno(_("inotify_add_watch('%s') failed"), interned); } @@ -409,6 +414,7 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) state->listen_data = data; state->listen_error_code = -1; data->fd_inotify = -1; + data->worktree_identity = state->worktree_identity.buf; data->shutdown = SHUTDOWN_ERROR; fd = inotify_init1(O_NONBLOCK); @@ -435,6 +441,7 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) } if (!ret) { + fsmonitor_ipc__clear_watch_limit_failure(); state->listen_error_code = 0; data->shutdown = SHUTDOWN_CONTINUE; } @@ -445,11 +452,6 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) void fsm_listen__dtor(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; - struct hashmap_iter iter; - struct watch_entry *w; - struct watch_entry **to_remove; - size_t nr_to_remove = 0, alloc_to_remove = 0; - size_t i; int fd; if (!state || !state->listen_data) @@ -459,31 +461,17 @@ void fsm_listen__dtor(struct fsmonitor_daemon_state *state) fd = data->fd_inotify; /* - * Collect all entries first, then remove them. - * We can't modify the hashmap while iterating over it. + * Closing the inotify instance releases every kernel watch at once. + * The forward and reverse maps own separate watch_entry allocations. */ - to_remove = NULL; - hashmap_for_each_entry(&data->watches, &iter, w, ent) { - ALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove); - to_remove[nr_to_remove++] = w; - } - - for (i = 0; i < nr_to_remove; i++) { - to_remove[i]->cookie = 0; /* ignore any pending renames */ - remove_watch(to_remove[i], data); - } - free(to_remove); - - hashmap_clear(&data->watches); - - hashmap_clear(&data->revwatches); /* remove_watch freed the entries */ - + data->fd_inotify = -1; + if (fd >= 0 && close(fd) < 0) + error_errno(_("closing inotify file descriptor failed")); + hashmap_clear_and_free(&data->watches, struct watch_entry, ent); + hashmap_clear_and_free(&data->revwatches, struct watch_entry, ent); hashmap_clear_and_free(&data->renames, struct rename_entry, ent); FREE_AND_NULL(state->listen_data); - - if (fd >= 0 && (close(fd) < 0)) - error_errno(_("closing inotify file descriptor failed")); } void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) diff --git a/compat/simple-ipc/ipc-unix-socket.c b/compat/simple-ipc/ipc-unix-socket.c index 7db3b2a89755c6..d27747bc1d0b63 100644 --- a/compat/simple-ipc/ipc-unix-socket.c +++ b/compat/simple-ipc/ipc-unix-socket.c @@ -189,10 +189,10 @@ void ipc_client_close_connection(struct ipc_client_connection *connection) free(connection); } -int ipc_client_send_command_to_connection( +static int ipc_client_send_command_to_connection_1( struct ipc_client_connection *connection, const char *message, size_t message_len, - struct strbuf *answer) + struct strbuf *answer, int gentle) { int ret = 0; @@ -203,14 +203,14 @@ int ipc_client_send_command_to_connection( if (write_packetized_from_buf_no_flush(message, message_len, connection->fd) < 0 || packet_flush_gently(connection->fd) < 0) { - ret = error(_("could not send IPC command")); + ret = gentle ? -1 : error(_("could not send IPC command")); goto done; } if (read_packetized_to_strbuf( connection->fd, answer, PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) { - ret = error(_("could not read IPC response")); + ret = gentle ? -1 : error(_("could not read IPC response")); goto done; } @@ -219,6 +219,24 @@ int ipc_client_send_command_to_connection( return ret; } +int ipc_client_send_command_to_connection( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 0); +} + +int ipc_client_send_command_to_connection_gently( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 1); +} + int ipc_client_send_command(const char *path, const struct ipc_client_connect_options *options, const char *message, size_t message_len, diff --git a/compat/simple-ipc/ipc-win32.c b/compat/simple-ipc/ipc-win32.c index 4a3e7df9c739e1..f1b4124d3ae8df 100644 --- a/compat/simple-ipc/ipc-win32.c +++ b/compat/simple-ipc/ipc-win32.c @@ -235,10 +235,10 @@ void ipc_client_close_connection(struct ipc_client_connection *connection) free(connection); } -int ipc_client_send_command_to_connection( +static int ipc_client_send_command_to_connection_1( struct ipc_client_connection *connection, const char *message, size_t message_len, - struct strbuf *answer) + struct strbuf *answer, int gentle) { int ret = 0; @@ -249,7 +249,7 @@ int ipc_client_send_command_to_connection( if (write_packetized_from_buf_no_flush(message, message_len, connection->fd) < 0 || packet_flush_gently(connection->fd) < 0) { - ret = error(_("could not send IPC command")); + ret = gentle ? -1 : error(_("could not send IPC command")); goto done; } @@ -258,7 +258,7 @@ int ipc_client_send_command_to_connection( if (read_packetized_to_strbuf( connection->fd, answer, PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) { - ret = error(_("could not read IPC response")); + ret = gentle ? -1 : error(_("could not read IPC response")); goto done; } @@ -267,6 +267,24 @@ int ipc_client_send_command_to_connection( return ret; } +int ipc_client_send_command_to_connection( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 0); +} + +int ipc_client_send_command_to_connection_gently( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 1); +} + int ipc_client_send_command(const char *path, const struct ipc_client_connect_options *options, const char *message, size_t message_len, diff --git a/fsmonitor-clean-proof.c b/fsmonitor-clean-proof.c index 3c45c014469737..c150f86e208d71 100644 --- a/fsmonitor-clean-proof.c +++ b/fsmonitor-clean-proof.c @@ -22,8 +22,12 @@ int fsmonitor_clean_proof_parse(struct fsmonitor_clean_proof *proof, if (len < FSMONITOR_CLEAN_PROOF_HEADER_WORDS * sizeof(uint32_t) + hashes_len + 1) return -1; - if (get_be32(p) != FSMONITOR_CLEAN_PROOF_VERSION) + parsed.version = get_be32(p); + if (parsed.version != FSMONITOR_CLEAN_PROOF_VERSION_LEGACY && + parsed.version != FSMONITOR_CLEAN_PROOF_VERSION) return -1; + if (parsed.version == FSMONITOR_CLEAN_PROOF_VERSION) + hashes_len += algo->rawsz; p += sizeof(uint32_t); if (get_be32(p) != FSMONITOR_CLEAN_PROOF_MAGIC) return -1; @@ -51,6 +55,10 @@ int fsmonitor_clean_proof_parse(struct fsmonitor_clean_proof *proof, p += algo->rawsz; parsed.attr_hash = p; p += algo->rawsz; + if (parsed.version == FSMONITOR_CLEAN_PROOF_VERSION) { + parsed.tracked_policy_hash = p; + p += algo->rawsz; + } parsed.attr_manifest = p; parsed.attr_manifest_len = manifest_len; p += manifest_len; @@ -82,7 +90,9 @@ int fsmonitor_clean_proof_write(struct strbuf *out, proof->attr_manifest_len, algo)) return -1; - put_be32(&value, FSMONITOR_CLEAN_PROOF_VERSION); + put_be32(&value, proof->tracked_policy_hash ? + FSMONITOR_CLEAN_PROOF_VERSION : + FSMONITOR_CLEAN_PROOF_VERSION_LEGACY); strbuf_add(out, &value, sizeof(value)); put_be32(&value, FSMONITOR_CLEAN_PROOF_MAGIC); strbuf_add(out, &value, sizeof(value)); @@ -96,6 +106,8 @@ int fsmonitor_clean_proof_write(struct strbuf *out, strbuf_add(out, proof->config_hash, algo->rawsz); strbuf_add(out, proof->semantic_hash, algo->rawsz); strbuf_add(out, proof->attr_hash, algo->rawsz); + if (proof->tracked_policy_hash) + strbuf_add(out, proof->tracked_policy_hash, algo->rawsz); strbuf_add(out, proof->attr_manifest, proof->attr_manifest_len); hash_append_checksum(out, algo); return 0; diff --git a/fsmonitor-clean-proof.h b/fsmonitor-clean-proof.h index 0d4da4cd725803..2e2a3c2b252c39 100644 --- a/fsmonitor-clean-proof.h +++ b/fsmonitor-clean-proof.h @@ -5,7 +5,8 @@ struct strbuf; -#define FSMONITOR_CLEAN_PROOF_VERSION 1 +#define FSMONITOR_CLEAN_PROOF_VERSION_LEGACY 1 +#define FSMONITOR_CLEAN_PROOF_VERSION 2 #define FSMONITOR_CLEAN_PROOF_TOKEN_MAX 4096 #define FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE (1u << 0) @@ -19,12 +20,14 @@ struct strbuf; FSMONITOR_CLEAN_PROOF_FULL_INDEX) struct fsmonitor_clean_proof { + uint32_t version; uint32_t flags; const unsigned char *token; size_t token_len; const unsigned char *config_hash; const unsigned char *semantic_hash; const unsigned char *attr_hash; + const unsigned char *tracked_policy_hash; const unsigned char *attr_manifest; size_t attr_manifest_len; }; diff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c index 3a03658853308c..7c60d7b59193af 100644 --- a/fsmonitor-ipc.c +++ b/fsmonitor-ipc.c @@ -9,6 +9,7 @@ #include "hash.h" #include "lockfile.h" #include "parse.h" +#include "path.h" #include "simple-ipc.h" #include "fsmonitor-ipc.h" #include "repository.h" @@ -77,6 +78,20 @@ int fsmonitor_ipc__is_supported(void) return 0; } +void fsmonitor_ipc__record_watch_limit_failure( + const char *worktree_identity UNUSED) +{ +} + +void fsmonitor_ipc__clear_watch_limit_failure(void) +{ +} + +int fsmonitor_ipc__watch_limit_backoff(struct repository *r UNUSED) +{ + return 0; +} + const char *fsmonitor_ipc__get_path(struct repository *r UNUSED) { return NULL; @@ -127,6 +142,161 @@ enum ipc_active_state fsmonitor_ipc__get_state(void) #define FSMONITOR_START_TIMEOUT_DEFAULT 60 #define FSMONITOR_RESTART_ATTEMPTS 3 +#if defined(__linux__) || defined(__APPLE__) +#define FSMONITOR_WATCH_LIMIT_MARKER "fsmonitor--daemon.inotify-limit" +#define FSMONITOR_WATCH_LIMIT_MAGIC "inotify-limit-v1\n" +#define FSMONITOR_WATCH_LIMIT_BACKOFF_SECONDS 60 + +static int watch_limit_backoff_enabled(void) +{ +#ifdef __linux__ + return 1; +#else + return git_env_bool("GIT_TEST_FSMONITOR_INOTIFY_BACKOFF", 0); +#endif +} + +static int read_inotify_watch_limit(unsigned long *limit) +{ +#ifdef __linux__ + struct strbuf value = STRBUF_INIT; + int ret = -1; + + if (strbuf_read_file(&value, + "/proc/sys/fs/inotify/max_user_watches", 64) < 0) + goto done; + strbuf_trim(&value); + if (git_parse_ulong(value.buf, limit)) + ret = 0; +done: + strbuf_release(&value); + return ret; +#else + *limit = 0; + return 0; +#endif +} + +void fsmonitor_ipc__record_watch_limit_failure(const char *worktree_identity) +{ + struct lock_file lock = LOCK_INIT; + struct strbuf contents = STRBUF_INIT; + unsigned long limit; + char *path; + int fd; + + if (!watch_limit_backoff_enabled() || !worktree_identity || + strlen(worktree_identity) != FSMONITOR_IPC_WORKTREE_ID_HEX || + read_inotify_watch_limit(&limit)) + return; + path = repo_git_path(the_repository, FSMONITOR_WATCH_LIMIT_MARKER); + fd = hold_lock_file_for_update(&lock, path, LOCK_NO_DEREF); + if (fd < 0) + goto done; + strbuf_addf(&contents, "%s%s\n%lu\n", + FSMONITOR_WATCH_LIMIT_MAGIC, worktree_identity, limit); + if (fchmod(fd, 0600) || + write_in_full(fd, contents.buf, contents.len) != + (ssize_t)contents.len || + commit_lock_file(&lock)) + rollback_lock_file(&lock); +done: + strbuf_release(&contents); + free(path); +} + +void fsmonitor_ipc__clear_watch_limit_failure(void) +{ + char *path; + + if (!watch_limit_backoff_enabled()) + return; + path = repo_git_path(the_repository, FSMONITOR_WATCH_LIMIT_MARKER); + unlink(path); + free(path); +} + +int fsmonitor_ipc__watch_limit_backoff(struct repository *r) +{ + struct strbuf contents = STRBUF_INIT; + struct strbuf identity = STRBUF_INIT; + struct stat st; + const char *recorded_identity, *recorded_limit; + unsigned long limit, current_limit; + time_t now; + char *path, *identity_end, *limit_end; + int fd, ret = 0; + + if (!watch_limit_backoff_enabled()) + return 0; + path = repo_git_path(r, FSMONITOR_WATCH_LIMIT_MARKER); + fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) + goto done; + if (fstat(fd, &st) || !S_ISREG(st.st_mode) || + st.st_uid != geteuid() || st.st_nlink != 1 || + (st.st_mode & 077) || st.st_size < 0 || st.st_size > 256) + goto close_fd; + now = time(NULL); + if (now < st.st_mtime || + now - st.st_mtime > FSMONITOR_WATCH_LIMIT_BACKOFF_SECONDS) + goto clear_marker; + if (strbuf_read(&contents, fd, st.st_size) != st.st_size || + !skip_prefix(contents.buf, FSMONITOR_WATCH_LIMIT_MAGIC, + &recorded_identity) || + !(identity_end = strchr(contents.buf + + strlen(FSMONITOR_WATCH_LIMIT_MAGIC), '\n'))) + goto close_fd; + *identity_end = '\0'; + recorded_limit = identity_end + 1; + if (!(limit_end = strchr(identity_end + 1, '\n')) || limit_end[1]) + goto close_fd; + *limit_end = '\0'; + if (!git_parse_ulong(recorded_limit, &limit) || + read_inotify_watch_limit(¤t_limit)) + goto close_fd; + if (limit != current_limit) + goto clear_marker; + if (fsmonitor_ipc__get_worktree_identity(r, &identity)) + goto close_fd; + if (strcmp(recorded_identity, identity.buf)) { +#ifndef __linux__ + if (!git_env_bool("GIT_TEST_FSMONITOR_INOTIFY_BACKOFF", 0) || + strcmp(recorded_identity, "test-worktree")) +#endif + goto close_fd; + } + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + goto clear_marker; + ret = 1; + goto close_fd; + +clear_marker: + unlink(path); +close_fd: + close(fd); +done: + strbuf_release(&identity); + strbuf_release(&contents); + free(path); + return ret; +} +#else +void fsmonitor_ipc__record_watch_limit_failure( + const char *worktree_identity UNUSED) +{ +} + +void fsmonitor_ipc__clear_watch_limit_failure(void) +{ +} + +int fsmonitor_ipc__watch_limit_backoff(struct repository *r UNUSED) +{ + return 0; +} +#endif + static unsigned int get_start_timeout(void) { const char *value; @@ -194,7 +364,7 @@ static int spawn_daemon(void) } static int try_send_command(const char *command, struct strbuf *answer, - enum ipc_active_state *state_out) + enum ipc_active_state *state_out, int quietly) { struct ipc_client_connection *connection = NULL; struct ipc_client_connect_options options @@ -209,8 +379,12 @@ static int try_send_command(const char *command, struct strbuf *answer, state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository), &options, &connection); if (state == IPC_STATE__LISTENING) { - ret = ipc_client_send_command_to_connection( - connection, command, strlen(command), answer); + if (quietly) + ret = ipc_client_send_command_to_connection_gently( + connection, command, strlen(command), answer); + else + ret = ipc_client_send_command_to_connection( + connection, command, strlen(command), answer); ipc_client_close_connection(connection); } @@ -255,7 +429,7 @@ static int server_supports_bound_queries(void) int ret; ret = !try_send_command(FSMONITOR_IPC_CAPABILITY_COMMAND, - &answer, NULL) && + &answer, NULL, 1) && has_capability(&answer, FSMONITOR_IPC_QUERY_VERSION); strbuf_release(&answer); return ret; @@ -268,7 +442,7 @@ static int server_supports_required_capabilities(void) int ret; ret = !try_send_command(FSMONITOR_IPC_CAPABILITY_COMMAND, - &answer, NULL) && + &answer, NULL, 1) && has_capability(&answer, FSMONITOR_IPC_QUERY_VERSION) && has_capability(&answer, FSMONITOR_IPC_DIR_METADATA_CAPABILITY); @@ -550,7 +724,7 @@ static int try_send_attested_legacy_query( trace2_data_intmax("fsm_client", NULL, cached ? "query/legacy-peer-cached" : "query/legacy-peer-authenticated", 1); - ret = ipc_client_send_command_to_connection( + ret = ipc_client_send_command_to_connection_gently( connection, token, strlen(token), answer); done: ipc_client_close_connection(connection); @@ -632,7 +806,7 @@ static int restart_incompatible_daemon(void) if (!lstat(fsmonitor_ipc__get_path(the_repository), &socket_stat)) original_socket = &socket_stat; - if (try_send_command("quit", &answer, NULL)) { + if (try_send_command("quit", &answer, NULL, 1)) { /* * The failed connection may already have been replaced. * Re-read its state before abandoning the upgrade. @@ -744,10 +918,17 @@ int fsmonitor_ipc__send_query(const char *since_token, switch (state) { case IPC_STATE__LISTENING: - ret = ipc_client_send_command_to_connection( + ret = ipc_client_send_command_to_connection_gently( connection, command.buf, command.len, answer); ipc_client_close_connection(connection); connection = NULL; + if (ret && lifecycle_attempts++ < FSMONITOR_RESTART_ATTEMPTS) { + trace2_data_intmax("fsm_client", NULL, + "query/reconnect-after-failed-send", 1); + /* Let a missing daemon enter normal startup without polling. */ + options.wait_if_not_found = 0; + goto try_again; + } trace2_data_intmax("fsm_client", NULL, "query/response-length", answer->len); @@ -837,7 +1018,7 @@ int fsmonitor_ipc__send_command(const char *command, { enum ipc_active_state state; const char *c = command ? command : ""; - int ret = try_send_command(c, answer, &state); + int ret = try_send_command(c, answer, &state, 0); if (state != IPC_STATE__LISTENING) { die(_("fsmonitor--daemon is not running")); diff --git a/fsmonitor-ipc.h b/fsmonitor-ipc.h index eff0798464b362..d52fcf8cf96f2f 100644 --- a/fsmonitor-ipc.h +++ b/fsmonitor-ipc.h @@ -16,6 +16,11 @@ struct repository; int fsmonitor_ipc__get_worktree_identity(struct repository *r, struct strbuf *identity); +/* Remember a bounded, worktree-specific inotify watch-limit failure. */ +void fsmonitor_ipc__record_watch_limit_failure(const char *worktree_identity); +void fsmonitor_ipc__clear_watch_limit_failure(void); +int fsmonitor_ipc__watch_limit_backoff(struct repository *r); + /* * Returns true if built-in file system monitor daemon is defined * for this platform. diff --git a/fsmonitor-settings.c b/fsmonitor-settings.c index a6587a8972b184..a0c12533413013 100644 --- a/fsmonitor-settings.c +++ b/fsmonitor-settings.c @@ -5,6 +5,7 @@ #include "fsmonitor-ipc.h" #include "fsmonitor-settings.h" #include "fsmonitor-path-utils.h" +#include "trace2.h" /* * We keep this structure definition private and have getters @@ -119,7 +120,11 @@ static void lookup_fsmonitor_settings(struct repository *r) switch (repo_config_get_maybe_bool(r, "core.fsmonitor", &bool_value)) { case 0: /* config value was set to */ - if (bool_value) + if (bool_value && fsmonitor_ipc__watch_limit_backoff(r)) { + trace2_data_intmax("fsm_client", r, + "settings/inotify-watch-limit-backoff", 1); + fsm_settings__set_disabled(r); + } else if (bool_value) fsm_settings__set_ipc(r); else fsm_settings__set_disabled(r); diff --git a/fsmonitor.c b/fsmonitor.c index f08e15c3b6dcdc..ed4c7d6324ae78 100644 --- a/fsmonitor.c +++ b/fsmonitor.c @@ -4,6 +4,7 @@ #include "git-compat-util.h" #include "attr.h" #include "clean-status.h" +#include "clean-status-manifest.h" #include "config.h" #include "dir.h" #include "environment.h" @@ -687,8 +688,22 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name) return; } pos = index_name_pos(istate, name, len); - attributes_may_have_changed = - fsmonitor_invalidate_attributes_path(istate, name); + if (pos >= 0 && + clean_status_manifest_reconcile_deleted_attribute(istate, name)) { + attributes_may_have_changed = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/attribute-source-reused", 1); + } else if (pos >= 0 && + clean_status_manifest_accept_current_display_only_attribute( + istate, name)) { + git_attr_invalidate_all(); + attributes_may_have_changed = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/nonconversion-attribute-replayed", 1); + } else { + attributes_may_have_changed = + fsmonitor_invalidate_attributes_path(istate, name); + } directory_is_semantically_safe = name[len - 1] == '/' && clean_status_directory_event_is_semantically_safe(istate, name); @@ -1033,7 +1048,7 @@ void fsmonitor_invalidate_semantics(struct index_state *istate) static void invalidate_fsmonitor_for_bootstrap( struct index_state *istate, enum fsmonitor_mode mode, int semantic_adoption_needed, int semantic_baseline_needed, - int physical_history_unavailable) + int physical_history_unavailable, int provider_query_success) { int manifest_refresh_failed; @@ -1050,13 +1065,29 @@ static void invalidate_fsmonitor_for_bootstrap( "semantic/legacy-stat-fallback", 1); return; } - clean_status_refresh_worktree_manifest(istate); - fsmonitor_invalidate_semantics(istate); + manifest_refresh_failed = + !clean_status_has_authenticated_bootstrap_manifest(istate) && + clean_status_refresh_worktree_manifest(istate) < 0; + if (provider_query_success && !manifest_refresh_failed && + !clean_status_manifest_global_fallback(istate) && + !clean_status_fsmonitor_strong_mismatch(istate) && + !clean_status_filter_scope_needs_validation(istate) && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat) { + /* Strong stat identity survives a lost provider boundary. */ + clean_status_begin_fsmonitor_semantic_baseline(istate); + invalidate_all_fsmonitor_for_baseline(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/token-reset-stat-baseline", 1); + } else { + fsmonitor_invalidate_semantics(istate); + } untracked_cache_invalidate_all(istate); return; } manifest_refresh_failed = + !clean_status_has_authenticated_worktree_manifest(istate) && clean_status_refresh_worktree_manifest(istate) < 0; if (manifest_refresh_failed || clean_status_manifest_global_fallback(istate) || @@ -1280,8 +1311,15 @@ void refresh_fsmonitor(struct index_state *istate) */ if (fstat_is_reliable() && !istate->split_index && fsm_mode == FSMONITOR_MODE_IPC && - clean_status_fsmonitor_config_mismatch(istate)) - tracked_requires_bootstrap = 1; + clean_status_fsmonitor_config_mismatch(istate)) { + if (clean_status_try_preserve_tracked_config_epoch(istate)) { + tracked_requires_bootstrap = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "config/tracked-epoch-preserved", 1); + } else { + tracked_requires_bootstrap = 1; + } + } if (tracked_requires_bootstrap) { /* @@ -1302,7 +1340,7 @@ void refresh_fsmonitor(struct index_state *istate) invalidate_fsmonitor_for_bootstrap( istate, fsm_mode, semantic_adoption_needed, semantic_baseline_needed, - !istate->fsmonitor_token_valid); + !istate->fsmonitor_token_valid, query_success); } /* Now mark the untracked cache for fsmonitor usage */ @@ -1328,7 +1366,7 @@ void refresh_fsmonitor(struct index_state *istate) */ invalidate_fsmonitor_for_bootstrap( istate, fsm_mode, semantic_adoption_needed, - semantic_baseline_needed, 1); + semantic_baseline_needed, 1, query_success); } trace2_region_leave("fsmonitor", "apply_results", istate->repo); diff --git a/preload-index-bulk-index.c b/preload-index-bulk-index.c index d4691d07678bd4..3ff1b4879204a4 100644 --- a/preload-index-bulk-index.c +++ b/preload-index-bulk-index.c @@ -1,4 +1,6 @@ #include "git-compat-util.h" +#include "clean-status.h" +#include "fsmonitor.h" #include "name-hash.h" #include "object.h" #include "preload-index-bulk.h" @@ -176,6 +178,13 @@ void preload_bulk_record_tracked( if (!tracked_entry_is_eligible(ce)) return; + if (clean_status_fsmonitor_semantic_baseline_pending(scan->istate) && + !fsmonitor_stat_can_be_valid(st)) { + if (record_tracked_state(worker, pos, + PRELOAD_BULK_TRACKED_CONTENT_CHECK)) + fsmonitor_invalidate_cache_entry(ce); + return; + } changed = ie_match_stat( scan->istate, ce, (struct stat *)st, CE_MATCH_RACY_IS_DIRTY | CE_MATCH_IGNORE_FSMONITOR); diff --git a/preload-index.c b/preload-index.c index 1bea4a5d3f3b66..95ebfff1d46787 100644 --- a/preload-index.c +++ b/preload-index.c @@ -7,6 +7,7 @@ #include "git-compat-util.h" #include "pathspec.h" #include "dir.h" +#include "clean-status.h" #include "environment.h" #include "fsmonitor.h" #include "gettext.h" @@ -113,6 +114,12 @@ static void *preload_thread(void *_data) p->t2_nr_lstat++; if (lstat(ce->name, &st)) continue; + if (clean_status_fsmonitor_semantic_baseline_pending(index) && + !fsmonitor_stat_can_be_valid(&st)) { + /* An unwatched hard-link alias can evade coarse stat identity. */ + fsmonitor_invalidate_cache_entry(ce); + continue; + } if (ie_match_stat(index, ce, &st, CE_MATCH_RACY_IS_DIRTY|CE_MATCH_IGNORE_FSMONITOR)) continue; ce_mark_uptodate(ce); diff --git a/read-cache.c b/read-cache.c index 26425fea31837b..985cadedce6f25 100644 --- a/read-cache.c +++ b/read-cache.c @@ -1473,6 +1473,10 @@ static struct cache_entry *refresh_cache_ent(struct index_state *istate, *err = errno; return NULL; } + if (clean_status_fsmonitor_semantic_baseline_pending(istate) && + !fsmonitor_stat_can_be_valid(&st) && + !(ce->ce_flags & CE_CONTENT_CHECK_REQUIRED)) + fsmonitor_invalidate_cache_entry(ce); changed = ie_match_stat(istate, ce, &st, options); if (changed_ret) diff --git a/simple-ipc.h b/simple-ipc.h index 701e005cb8e5f3..cc7471c97624eb 100644 --- a/simple-ipc.h +++ b/simple-ipc.h @@ -106,6 +106,15 @@ int ipc_client_send_command_to_connection( const char *message, size_t message_len, struct strbuf *answer); +/* + * Like ipc_client_send_command_to_connection(), but suppress the generic IPC + * transport diagnostic so callers can recover from a disappearing server. + */ +int ipc_client_send_command_to_connection_gently( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer); + /* * Used by the client to synchronously connect and send and receive a * message to the server listening at the given path. diff --git a/t/helper/test-simple-ipc.c b/t/helper/test-simple-ipc.c index a37b7481f69e91..d06b94b01d41a4 100644 --- a/t/helper/test-simple-ipc.c +++ b/t/helper/test-simple-ipc.c @@ -162,6 +162,7 @@ static int my_app_data = 42; static int fsmonitor_legacy; static int fsmonitor_capability_superset; static int fsmonitor_pre_dir_metadata; +static int fsmonitor_disconnect_first; static ipc_server_application_cb test_app_cb; @@ -223,6 +224,10 @@ static int test_app_cb(void *application_data, if (application_data != (void*)&my_app_data) BUG("application_cb: application_data pointer wrong"); + /* Exit before the server can flush a response to this bound query. */ + if (fsmonitor_disconnect_first && starts_with(command, "query-v1 ")) + _exit(0); + if (command_len == 4 && !strncmp(command, "quit", 4)) { /* * The client sent a "quit" command. This is an async @@ -372,6 +377,8 @@ static int daemon__start_server(void) strvec_push(&cp.args, "--fsmonitor-capability-superset"); if (fsmonitor_pre_dir_metadata) strvec_push(&cp.args, "--fsmonitor-pre-dir-metadata"); + if (fsmonitor_disconnect_first) + strvec_push(&cp.args, "--fsmonitor-disconnect-first"); cp.no_stdin = 1; cp.no_stdout = 1; @@ -672,6 +679,9 @@ int cmd__simple_ipc(int argc, const char **argv) OPT_BOOL(0, "fsmonitor-pre-dir-metadata", &fsmonitor_pre_dir_metadata, N_("emulate a daemon without directory metadata filtering")), + OPT_BOOL(0, "fsmonitor-disconnect-first", + &fsmonitor_disconnect_first, + N_("disconnect while handling the first fsmonitor query")), /* * The "byte" string here is not marked for translation and diff --git a/t/t7519-status-fsmonitor.sh b/t/t7519-status-fsmonitor.sh index 483782ef174c7a..187c70e2ddd135 100755 --- a/t/t7519-status-fsmonitor.sh +++ b/t/t7519-status-fsmonitor.sh @@ -196,6 +196,7 @@ test_expect_success UNTRACKED_CACHE \ 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=CCCC \ git status --porcelain=v2 >.git/prime-fsmonitor && test_must_be_empty .git/prime-fsmonitor && @@ -246,6 +247,7 @@ test_expect_success UNTRACKED_CACHE,HARDLINKS \ 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=CCCC \ git status --porcelain=v2 >.git/prime-fsmonitor && test_must_be_empty .git/prime-fsmonitor && @@ -398,6 +400,7 @@ test_expect_success UNTRACKED_CACHE \ 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=CCCC \ git status --porcelain >.git/prime-fsmonitor && test_must_be_empty .git/prime-fsmonitor && @@ -407,6 +410,7 @@ test_expect_success UNTRACKED_CACHE \ .git/settle && GIT_OPTIONAL_LOCKS=0 \ + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ GIT_TRACE2_EVENT="$PWD/.git/clean.trace" \ git status >.git/clean && @@ -679,6 +683,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_grep UNTR .git/index && test_grep ! FSUC .git/index && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ git status --porcelain=v2 >.git/actual && @@ -748,6 +753,7 @@ test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ ( cd builtin-initial-trivial && sane_unset GIT_TEST_SPLIT_INDEX && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=TC \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ git status --porcelain=v2 \ @@ -818,14 +824,14 @@ test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ cd builtin-closure-error && sane_unset GIT_TEST_SPLIT_INDEX && test_write_lines visible >visible && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCE \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CE \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ git status --porcelain=v2 >.git/actual && test_grep "^? visible$" .git/actual && test_grep \ "\"event\":\"region_enter\".*\"category\":\"dir\",\"label\":\"read_directory\"" \ .git/status.trace >.git/read-directory && - test_line_count = 2 .git/read-directory && + test_line_count -ge 1 .git/read-directory && test_trace2_data fsmonitor token_closure/rejected 1 \ <.git/status.trace && ! test_trace2_data fsmonitor token_closure/accepted 1 \ @@ -1552,7 +1558,7 @@ test_expect_success HARDLINKS,!MINGW,!CYGWIN \ ' test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'second closing-query change reprimes untracked cache' ' + 'second closing-query change preserves verified sibling subtrees' ' test_when_finished "rm -rf second-query-changed" && test_create_repo second-query-changed && ( @@ -1563,14 +1569,24 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_write_lines "*.ignored" >cached/.gitignore && printf "aaaa\n" >cached/tracked && test_write_lines ignored >cached/junk.ignored && - git add .gitignore cached/.gitignore cached/tracked && + for sibling in $(test_seq 1 12) + do + mkdir "sibling-$sibling" && + test-tool genrandom "sibling-$sibling" 4096 \ + >"sibling-$sibling/tracked" && + test_write_lines ignored \ + >"sibling-$sibling/retained.ignored" || return 1 + done && + git add .gitignore cached/.gitignore cached/tracked sibling-* && git commit -m base && git config core.trustctime false && git config core.checkStat minimal && git config core.untrackedCache true && + test_write_lines visible >sibling-1/visible && git -c core.fsmonitor=false status --porcelain=v2 \ >.git/prime && - test_must_be_empty .git/prime && + test_line_count = 1 .git/prime && + test_grep "^? sibling-1/visible$" .git/prime && test-tool chmtime =-60 cached/tracked && git update-index --refresh && mtime=$(test-tool chmtime --get cached/tracked) && @@ -1582,13 +1598,20 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ git update-index --fsmonitor-valid cached/tracked && test_grep ! FSCF .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + -c core.trustctime=true -c core.checkStat=default \ + status --porcelain=v2 >.git/expect && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCDC \ GIT_TEST_FSMONITOR_QUERY_PATH=cached/tracked \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ + GIT_TRACE2_PERF="$PWD/.git/status.perf" \ git status --porcelain=v2 >.git/actual && - test_line_count = 1 .git/actual && + test_cmp .git/expect .git/actual && + test_line_count = 2 .git/actual && test_grep "^1 \.M .* cached/tracked$" .git/actual && + test_grep "^? sibling-1/visible$" .git/actual && test_trace2_data status fsmonitor_token/semantic-closed 1 \ <.git/status.trace && test_trace2_data status \ @@ -1596,6 +1619,37 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ <.git/status.trace && test_trace2_data fsmonitor token_closure/apply_count 1 \ <.git/status.trace && + test_trace2_data status \ + fsmonitor_token/reused-semantic-subtrees 1 \ + <.git/status.trace && + test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/status.trace >.git/strong-invalidations && + test_line_count = 1 .git/strong-invalidations && + sed -n \ + "s/.*directories-visited:\\([0-9][0-9]*\\).*/\\1/p" \ + .git/status.perf >.git/visited && + test_line_count = 2 .git/visited && + initial_visited=$(sed -n 1p .git/visited) && + retry_visited=$(sed -n 2p .git/visited) && + test "$initial_visited" -gt 8 && + test "$retry_visited" -lt 4 && + sed -n \ + "s/.*paths-visited:\\([0-9][0-9]*\\).*/\\1/p" \ + .git/status.perf >.git/visited-paths && + test_line_count = 2 .git/visited-paths && + initial_paths=$(sed -n 1p .git/visited-paths) && + retry_paths=$(sed -n 2p .git/visited-paths) && + test "$retry_paths" -lt "$initial_paths" && + sed -n "s/.*opendir:\\([0-9][0-9]*\\).*/\\1/p" \ + .git/status.perf >.git/opened && + test_line_count = 2 .git/opened && + initial_opened=$(sed -n 1p .git/opened) && + retry_opened=$(sed -n 2p .git/opened) && + test "$initial_opened" -gt 8 && + test $((retry_opened - initial_opened)) -gt 0 && + test $((retry_opened - initial_opened)) -le 2 && + test_trace2_data index refresh/sum_lstat "[0-2]" \ + <.git/status.trace && test_trace2_data status \ fsmonitor_token/untracked-after-retry 1 \ <.git/status.trace && @@ -1695,6 +1749,66 @@ test_expect_success MACOS,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'global second closing-query change rejects verified subtree reuse' ' + test_when_finished "rm -rf second-query-global" && + test_create_repo second-query-global && + ( + cd second-query-global && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir cached untouched && + test_write_lines "*.ignored" >.gitignore && + test_write_lines "*.ignored" >cached/.gitignore && + printf "aaaa\n" >cached/tracked && + printf "untouched\n" >untouched/tracked && + test_write_lines ignored >cached/junk.ignored && + git add .gitignore cached/.gitignore cached/tracked \ + untouched/tracked && + git commit -m base && + git config core.trustctime false && + git config core.checkStat minimal && + git config core.untrackedCache true && + git -c core.fsmonitor=false status --porcelain=v2 \ + >.git/prime && + test_must_be_empty .git/prime && + test-tool chmtime =-60 cached/tracked && + git update-index --refresh && + mtime=$(test-tool chmtime --get cached/tracked) && + printf "bbbb\n" >cached/tracked && + test-tool chmtime =$mtime cached/tracked && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor-valid cached/tracked && + test_grep ! FSCF .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + -c core.trustctime=true -c core.checkStat=default \ + status --porcelain=v2 >.git/expect && + + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCDC \ + GIT_TEST_FSMONITOR_QUERY_PATH=// \ + GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_line_count = 1 .git/actual && + test_grep "^1 \.M .* cached/tracked$" .git/actual && + test_trace2_data fsmonitor apply/global-invalidation 1 \ + <.git/status.trace && + test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/status.trace >.git/strong-invalidations && + test_line_count = 2 .git/strong-invalidations && + ! test_trace2_data status \ + fsmonitor_token/reused-semantic-subtrees 1 \ + <.git/status.trace && + test_trace2_data fsmonitor token_closure/accepted 1 \ + <.git/status.trace && + test_grep FSCF .git/index && + test_grep FSUC .git/index + ) +' + test_expect_success MACOS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS \ 'foreign index writers preserve unchanged worktree semantics' ' test_when_finished "rm -rf foreign-semantic-history" && diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh index e01015e043eb91..5a9359283867a4 100755 --- a/t/t7527-builtin-fsmonitor.sh +++ b/t/t7527-builtin-fsmonitor.sh @@ -59,6 +59,15 @@ test_lazy_prereq HARDLINKS ' ln hardlink-a hardlink-b ' +test_lazy_prereq FOREIGN_FSMONITOR_GIT ' + test -x /opt/homebrew/bin/git && + /opt/homebrew/bin/git version +' + +test_lazy_prereq LEGACY_PREVIEW_FSMONITOR_GIT ' + test -x /opt/homebrew/Cellar/og-preview/2026-08-11T2321Z/libexec/openai-git/bin/git +' + if ! test_have_prereq FSMONITOR_WORKS then skip_all="filesystem does not deliver fsmonitor events (container/overlayfs?)" @@ -1303,6 +1312,7 @@ test_expect_success CASE_INSENSITIVE_FS 'fsmonitor file case wrong on disk' ' # marked clean. git -C file_case_wrong config core.fsmonitor true && git -C file_case_wrong update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/file_case_wrong/.git/index" \ git -C file_case_wrong status && # Make some files dirty so that FSMonitor gets FSEvents for @@ -1335,6 +1345,7 @@ test_expect_success CASE_INSENSITIVE_FS 'fsmonitor file case wrong on disk' ' # token (so the next invocation will not see data for these # events). + GIT_INDEX_FILE="$PWD/file_case_wrong/.git/index" \ GIT_TRACE_FSMONITOR="$PWD/file_case_wrong-try1.log" \ git -C file_case_wrong status --short \ >"$PWD/file_case_wrong-try1.out" && @@ -1657,6 +1668,7 @@ test_expect_success MACOS 'bound query upgrades stale directory event daemon' ' --name="$ipc_path" --threads=1 \ --fsmonitor-pre-dir-metadata && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TRACE2_EVENT="$PWD/.git/upgrade.trace" \ git status --porcelain=v2 >.git/upgrade && test_must_be_empty .git/upgrade && @@ -1680,6 +1692,317 @@ test_expect_success MACOS 'bound query upgrades stale directory event daemon' ' ) ' +test_expect_success MACOS,UNTRACKED_CACHE \ + 'inotify watch-limit backoff preserves ordinary status without retries' ' + test_when_finished "stop_daemon_delete_repo inotify-watch-backoff" && + test_create_repo inotify-watch-backoff && + ( + cd inotify-watch-backoff && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + git config core.preloadIndex false && + git config core.untrackedCache true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + printf "inotify-limit-v1\\ntest-worktree\\n0\\n" \ + >.git/fsmonitor--daemon.inotify-limit && + chmod 600 .git/fsmonitor--daemon.inotify-limit && + test_write_lines changed >tracked && + test_write_lines visible >blep && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + + for attempt in first second + do + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + GIT_TRACE2_EVENT="$PWD/.git/$attempt.trace" \ + git status --porcelain=v2 >.git/$attempt.actual && + test_cmp .git/expect .git/$attempt.actual && + test_trace2_data fsm_client \ + settings/inotify-watch-limit-backoff 1 \ + <.git/$attempt.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/$attempt.trace && + test_grep ! \ + "\\\"event\\\":\\\"child_start\\\".*\\\"fsmonitor--daemon\\\"" \ + .git/$attempt.trace || return 1 + done && + test_grep "^1 \\.M .* tracked$" .git/first.actual && + test_grep "^? blep$" .git/first.actual + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE \ + 'expired watch-limit backoff allows an authenticated daemon to recover' ' + test_when_finished "stop_daemon_delete_repo inotify-watch-expired" && + test_create_repo inotify-watch-expired && + ( + cd inotify-watch-expired && + test_commit base tracked && + git config core.preloadIndex false && + git config core.untrackedCache true && + git config core.fsmonitor true && + printf "inotify-limit-v1\\ntest-worktree\\n0\\n" \ + >.git/fsmonitor--daemon.inotify-limit && + chmod 600 .git/fsmonitor--daemon.inotify-limit && + test-tool chmtime -120 .git/fsmonitor--daemon.inotify-limit && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + GIT_TRACE2_EVENT="$PWD/.git/expired.trace" \ + git status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + ! test_trace2_data fsm_client \ + settings/inotify-watch-limit-backoff 1 \ + <.git/expired.trace && + test_grep \ + "\\\"event\\\":\\\"child_start\\\".*\\\"fsmonitor--daemon\\\"" \ + .git/expired.trace && + test_path_is_missing .git/fsmonitor--daemon.inotify-limit && + git fsmonitor--daemon status + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE \ + 'a running or explicitly started daemon overrides watch-limit backoff' ' + test_when_finished "stop_daemon_delete_repo inotify-watch-live" && + test_create_repo inotify-watch-live && + ( + cd inotify-watch-live && + test_commit base tracked && + git config core.preloadIndex false && + git config core.untrackedCache true && + git config core.fsmonitor true && + git fsmonitor--daemon start && + printf "inotify-limit-v1\\ntest-worktree\\n0\\n" \ + >.git/fsmonitor--daemon.inotify-limit && + chmod 600 .git/fsmonitor--daemon.inotify-limit && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + GIT_TRACE2_EVENT="$PWD/.git/live.trace" \ + git status --porcelain=v2 >.git/live && + test_must_be_empty .git/live && + ! test_trace2_data fsm_client \ + settings/inotify-watch-limit-backoff 1 <.git/live.trace && + git fsmonitor--daemon stop && + printf "inotify-limit-v1\\ntest-worktree\\n0\\n" \ + >.git/fsmonitor--daemon.inotify-limit && + chmod 600 .git/fsmonitor--daemon.inotify-limit && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + git fsmonitor--daemon start && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + git status --porcelain=v2 >.git/recovered && + test_must_be_empty .git/recovered && + test_path_is_missing .git/fsmonitor--daemon.inotify-limit + ) +' + +test_expect_success MACOS,SYMLINKS,UNTRACKED_CACHE \ + 'foreign and symlinked watch-limit markers never disable a worktree' ' + test_when_finished "stop_daemon_delete_repo inotify-watch-foreign" && + test_create_repo inotify-watch-foreign && + ( + cd inotify-watch-foreign && + test_commit base tracked && + git config core.preloadIndex false && + git config core.untrackedCache true && + git config core.fsmonitor true && + printf "inotify-limit-v1\\nforeign-worktree\\n0\\n" \ + >.git/fsmonitor--daemon.inotify-limit && + chmod 600 .git/fsmonitor--daemon.inotify-limit && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + GIT_TRACE2_EVENT="$PWD/.git/foreign.trace" \ + git status --porcelain=v2 >.git/foreign && + test_must_be_empty .git/foreign && + ! test_trace2_data fsm_client \ + settings/inotify-watch-limit-backoff 1 \ + <.git/foreign.trace && + git fsmonitor--daemon stop && + rm -f .git/fsmonitor--daemon.inotify-limit && + printf "inotify-limit-v1\\ntest-worktree\\n0\\n" \ + >.git/inotify-marker-target && + chmod 600 .git/inotify-marker-target && + ln -s inotify-marker-target .git/fsmonitor--daemon.inotify-limit && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + GIT_TRACE2_EVENT="$PWD/.git/symlink.trace" \ + git status --porcelain=v2 >.git/symlink && + test_must_be_empty .git/symlink && + ! test_trace2_data fsm_client \ + settings/inotify-watch-limit-backoff 1 \ + <.git/symlink.trace && + test_path_is_file .git/inotify-marker-target && + test_grep test-worktree .git/inotify-marker-target + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE \ + 'watch-limit backoff does not leak into a linked worktree' ' + test_when_finished " + git -C inotify-watch-linked fsmonitor--daemon stop \ + 2>/dev/null || : + git -C inotify-watch-main fsmonitor--daemon stop \ + 2>/dev/null || : + git -C inotify-watch-main -c core.fsmonitor=false \ + worktree remove --force ../inotify-watch-linked \ + 2>/dev/null || : + " && + test_create_repo inotify-watch-main && + ( + cd inotify-watch-main && + test_commit base tracked && + git worktree add ../inotify-watch-linked && + git config core.preloadIndex false && + git config core.untrackedCache true && + git config core.fsmonitor true && + printf "inotify-limit-v1\\ntest-worktree\\n0\\n" \ + >.git/fsmonitor--daemon.inotify-limit && + chmod 600 .git/fsmonitor--daemon.inotify-limit && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + GIT_TRACE2_EVENT="$PWD/.git/main.trace" \ + git status --porcelain=v2 >.git/main && + test_must_be_empty .git/main && + test_trace2_data fsm_client \ + settings/inotify-watch-limit-backoff 1 <.git/main.trace && + GIT_TEST_FSMONITOR_INOTIFY_BACKOFF=1 \ + GIT_TRACE2_EVENT="$PWD/.git/linked.trace" \ + git -C ../inotify-watch-linked status --porcelain=v2 \ + >.git/linked && + test_must_be_empty .git/linked && + ! test_trace2_data fsm_client \ + settings/inotify-watch-limit-backoff 1 \ + <.git/linked.trace && + test_grep \ + "\\\"event\\\":\\\"child_start\\\".*\\\"fsmonitor--daemon\\\"" \ + .git/linked.trace + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE \ + 'failed bound query reconnects to an authenticated replacement daemon' ' + test_when_finished \ + "stop_daemon_delete_repo disconnected-directory-daemon" && + test_create_repo disconnected-directory-daemon && + ( + cd disconnected-directory-daemon && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + test_write_lines visible >blep && + git config core.preloadIndex false && + git config core.untrackedCache true && + git status --porcelain=v2 >.git/expect && + test_grep "^? blep$" .git/expect && + 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-disconnect-first && + + GIT_TRACE2_EVENT="$PWD/.git/reconnect.trace" \ + git status --porcelain=v2 \ + >.git/actual 2>.git/reconnect.error && + test_cmp .git/expect .git/actual && + test_must_be_empty .git/reconnect.error && + test_trace2_data fsm_client query/reconnect-after-failed-send 1 \ + <.git/reconnect.trace && + test_grep \ + "\"argv\":.*\"fsmonitor--daemon\",\"run\",\"--detach\"" \ + .git/reconnect.trace && + ! 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\\." \ + .git/fsmonitor && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + + GIT_TRACE2_EVENT="$PWD/.git/repeat.trace" \ + git status --porcelain=v2 >.git/repeat && + test_cmp .git/expect .git/repeat && + ! test_trace2_data fsm_client query/reconnect-after-failed-send 1 \ + <.git/repeat.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/repeat.trace && + test_grep ! \ + "\"argv\":.*\"fsmonitor--daemon\",\"run\",\"--detach\"" \ + .git/repeat.trace + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE \ + 'concurrent clients share one stale directory daemon upgrade' ' + test_when_finished \ + "stop_daemon_delete_repo concurrent-directory-daemon-upgrade" && + test_create_repo concurrent-directory-daemon-upgrade && + ( + cd concurrent-directory-daemon-upgrade && + sane_unset GIT_TEST_SPLIT_INDEX && + for sibling in $(test_seq 1 16) + do + mkdir "sibling-$sibling" && + test_write_lines "base-$sibling" \ + >"sibling-$sibling/tracked" || return 1 + done && + git add sibling-* && + git commit -m base && + test_write_lines visible >blep && + git config core.preloadIndex false && + git config core.untrackedCache true && + git status --porcelain=v2 >.git/expect && + test_grep "^? blep$" .git/expect && + 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=8 \ + --fsmonitor-pre-dir-metadata && + + pids= && + for client in $(test_seq 1 8) + do + GIT_TRACE2_EVENT="$PWD/.git/client-$client.trace" \ + git status --porcelain=v2 \ + >.git/client-$client.actual \ + 2>.git/client-$client.error & + pids="$pids $!" || return 1 + done && + failed= && + for pid in $pids + do + wait "$pid" || failed=1 || return 1 + done && + test -z "$failed" && + + for client in $(test_seq 1 8) + do + test_cmp .git/expect .git/client-$client.actual && + test_must_be_empty .git/client-$client.error && + ! test_trace2_data fsm_client query/worktree-mismatch 1 \ + <.git/client-$client.trace || return 1 + done && + grep -h \ + "\"event\":\"child_start\".*\"argv\":.*\"fsmonitor--daemon\",\"run\",\"--detach\"" \ + .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\\." \ + .git/fsmonitor && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + + GIT_TRACE2_EVENT="$PWD/.git/repeat.trace" \ + git status --porcelain=v2 >.git/repeat && + test_cmp .git/expect .git/repeat && + ! test_trace2_data fsm_client query/incompatible-daemon 1 \ + <.git/repeat.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/repeat.trace && + test_grep ! \ + "\"argv\":.*\"fsmonitor--daemon\",\"run\",\"--detach\"" \ + .git/repeat.trace + ) +' + test_expect_success 'bound daemon also serves legacy token queries' ' test_when_finished "stop_daemon_delete_repo legacy-client-query" && test_create_repo legacy-client-query && @@ -1690,6 +2013,7 @@ test_expect_success 'bound daemon also serves legacy token queries' ' git config core.preloadIndex false && git config core.untrackedCache true && git config core.fsmonitor true && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TRACE2_EVENT="$PWD/.git/daemon.trace" \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -1731,7 +2055,8 @@ test_expect_success MACOS 'daemon token reset closes a skipHash index' ' start_daemon && git update-index --force-write-index && - git status --porcelain=v2 >.git/prime.out && + GIT_INDEX_FILE="$PWD/.git/index" \ + git status --porcelain=v2 >.git/prime.out && test_must_be_empty .git/prime.out && test_grep FSMN .git/index && test_grep FSCF .git/index && @@ -1745,6 +2070,7 @@ test_expect_success MACOS 'daemon token reset closes a skipHash index' ' git fsmonitor--daemon stop && start_daemon && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TRACE2_EVENT="$PWD/.git/reset.trace" \ git status --porcelain=v2 --untracked-files=normal >.git/reset.out && test_must_be_empty .git/reset.out && @@ -1774,6 +2100,7 @@ test_expect_success MACOS 'daemon token reset closes a skipHash index' ' echo changed >>tracked && rm removed && start_daemon && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TRACE2_EVENT="$PWD/.git/dirty-reset.trace" \ git status --porcelain=v2 >.git/dirty-reset.out && test_line_count = 2 .git/dirty-reset.out && @@ -1875,6 +2202,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -1910,7 +2238,9 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ <.git/attributes.trace && test_trace2_data fsmonitor apply_count 1 \ <.git/attributes.trace && - ! test_trace2_data fsmonitor config/token-advanced 1 \ + test_trace2_data fsmonitor semantic/manifest-reconciled 1 \ + <.git/attributes.trace && + test_trace2_data fsmonitor config/token-advanced 1 \ <.git/attributes.trace ) ' @@ -1928,6 +2258,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -1965,6 +2296,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2005,6 +2337,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2033,6 +2366,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2065,6 +2399,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2097,6 +2432,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2129,6 +2465,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2161,6 +2498,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2296,120 +2634,969 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ' test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'ordinary staged paths reuse unchanged tracked ancestor attributes' ' - test_when_finished "rm -rf staged-tracked-ancestor-attributes" && - test_create_repo staged-tracked-ancestor-attributes && + 'command-scoped transport config preserves staged worktree proofs' ' + test_when_finished "rm -rf command-transport-history" && + test_create_repo command-transport-history && ( - cd staged-tracked-ancestor-attributes && + cd command-transport-history && sane_unset GIT_TEST_SPLIT_INDEX && - mkdir -p api/existing api/brand-new/deeper && - test_write_lines "*.txt text" >api/.gitattributes && - test_write_lines existing >api/existing/tracked && - git add api/.gitattributes api/existing/tracked && + for sibling in $(test_seq 1 32) + do + mkdir "sibling-$sibling" && + test_write_lines "base-$sibling" \ + >"sibling-$sibling/tracked" || return 1 + done && + git add sibling-* && git commit -m base && - initial_branch=$(git symbolic-ref --short HEAD) && - git switch -c changed-tree && - mkdir -p api/branch-only/deeper && - test_write_lines alternate >api/existing/alternate.txt && - test_write_lines alternate >api/branch-only/deeper/alternate.txt && - git add api/existing/alternate.txt \ - api/branch-only/deeper/alternate.txt && - git commit -m alternate && - git switch "$initial_branch" && - test-tool chmtime -120 api/.gitattributes api/existing/tracked && - git update-index --refresh && + git branch transport-alternate && git config core.autocrlf false && 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_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && + test_grep FSCF .git/index && + test_grep FSUC .git/index && - for location in api/existing/added.txt api/brand-new/deeper/added.txt + for cycle in first second do - test_write_lines added >"$location" && + test_write_lines "changed-$cycle" >sibling-1/tracked && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ - GIT_TEST_FSMONITOR_QUERY_PATH="$location" \ - git add "$location" && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ - GIT_TRACE2_EVENT="$PWD/.git/ancestor-add.trace" \ - git status --porcelain=v2 >.git/ancestor-add && - test_grep "^1 A\\..* $location$" .git/ancestor-add && - test_trace2_data fsmonitor config/coherent 1 \ - <.git/ancestor-add.trace && - ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ - <.git/ancestor-add.trace && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ - git restore --staged "$location" && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ - GIT_TRACE2_EVENT="$PWD/.git/ancestor-remove.trace" \ - git status --porcelain=v2 >.git/ancestor-remove && + GIT_TEST_FSMONITOR_QUERY_PATH=sibling-1/tracked \ + git add sibling-1/tracked && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + git status --porcelain=v2 >.git/staged && + test_grep "^1 M\\..* sibling-1/tracked$" .git/staged && + test_grep FSUC .git/index && + + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/$cycle.restore.trace" \ + git \ + -c "url.https://proxy.example/github/.insteadOf=https://github.com/" \ + -c "url.https://proxy.example/github/.insteadOf=https://proxy.example/github/" \ + -c "url.https://proxy.example/github/.insteadOf=https://proxy.example/" \ + -c "credential.https://github.com.helper=" \ + -c "credential.https://proxy.example.helper=" \ + -c "credential.https://proxy.example.helper=!og github-proxy credential-helper" \ + restore --staged sibling-1/tracked && test_trace2_data fsmonitor config/coherent 1 \ - <.git/ancestor-remove.trace && + <".git/$cycle.restore.trace" && + test_trace2_data fsmonitor apply_count 0 \ + <".git/$cycle.restore.trace" && ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ - <.git/ancestor-remove.trace && - rm "$location" .git/ancestor-add.trace \ - .git/ancestor-remove.trace && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCC \ - GIT_TEST_FSMONITOR_QUERY_PATH="$location" \ - git status --porcelain=v2 >.git/ancestor-deleted && - test_must_be_empty .git/ancestor-deleted || return 1 - done && - - rmdir api/brand-new/deeper api/brand-new && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCC \ - GIT_TEST_FSMONITOR_QUERY_PATH=api/brand-new/ \ - git status --porcelain=v2 >.git/before-switch && - test_must_be_empty .git/before-switch && + <".git/$cycle.restore.trace" && + test_grep FSCF .git/index && + test_grep FSUC .git/index && - for branch in changed-tree "$initial_branch" - do - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ - git switch "$branch" && - if test "$branch" = changed-tree - then - test_path_is_file api/branch-only/deeper/alternate.txt - else - test_path_is_missing api/branch-only - fi && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ - GIT_TRACE2_EVENT="$PWD/.git/ancestor-switch.trace" \ - git status --porcelain=v2 >.git/ancestor-switch && - GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ - -c core.untrackedCache=false status --porcelain=v2 \ - >.git/ancestor-switch.expect && - test_cmp .git/ancestor-switch.expect .git/ancestor-switch && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/$cycle.status.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_grep "^1 \\.M.* sibling-1/tracked$" .git/actual && test_trace2_data fsmonitor config/coherent 1 \ - <.git/ancestor-switch.trace && + <".git/$cycle.status.trace" && ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ - <.git/ancestor-switch.trace && - ! test_trace2_data index preload/bulk_useful \ - <.git/ancestor-switch.trace && - rm .git/ancestor-switch.trace || return 1 + <".git/$cycle.status.trace" && + test_trace2_data index refresh/sum_lstat 1 \ + <".git/$cycle.status.trace" && + test_trace2_data read_directory directories-visited 2 \ + <".git/$cycle.status.trace" && + test_grep FSCF .git/index && + test_grep FSUC .git/index || return 1 done && - cp api/.gitattributes .git/attributes.saved && - rm api/.gitattributes && - test_write_lines missing >api/existing/missing.txt && + test_write_lines "new root file" >blep && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ - GIT_TEST_FSMONITOR_QUERY_PATH=api/existing/missing.txt \ - git add api/existing/missing.txt && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ - GIT_TRACE2_EVENT="$PWD/.git/ancestor-missing.trace" \ - git status --porcelain=v2 >.git/ancestor-missing && - test_trace2_data fsmonitor config/coherent 0 \ - <.git/ancestor-missing.trace && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ - git restore --staged api/existing/missing.txt && - rm api/existing/missing.txt && - cp .git/attributes.saved api/.gitattributes && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCC \ - GIT_TEST_FSMONITOR_QUERY_PATH=api/.gitattributes \ - git status --porcelain=v2 >.git/repaired && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ - git status --porcelain=v2 >.git/repaired-repeat && + GIT_TEST_FSMONITOR_QUERY_PATH=blep \ + git add blep && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + git status --porcelain=v2 >.git/root-staged && + test_grep "^1 A\\..* blep$" .git/root-staged && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/root.restore.trace" \ + git \ + -c "url.https://proxy.example/github/.insteadOf=https://github.com/" \ + -c "url.https://proxy.example/github/.insteadOf=https://proxy.example/github/" \ + -c "url.https://proxy.example/github/.insteadOf=https://proxy.example/" \ + -c "credential.https://github.com.helper=" \ + -c "credential.https://proxy.example.helper=" \ + -c "credential.https://proxy.example.helper=!og github-proxy credential-helper" \ + restore --staged blep && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/root.restore.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/root.restore.trace && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/root.expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/root.status.trace" \ + git status --porcelain=v2 >.git/root.actual && + test_cmp .git/root.expect .git/root.actual && + test_grep "^? blep$" .git/root.actual && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/root.status.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/root.status.trace && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/switch.trace" \ + git \ + -c "url.https://proxy.example/github/.insteadOf=https://github.com/" \ + -c "url.https://proxy.example/github/.insteadOf=https://proxy.example/github/" \ + -c "url.https://proxy.example/github/.insteadOf=https://proxy.example/" \ + -c "credential.https://github.com.helper=" \ + -c "credential.https://proxy.example.helper=" \ + -c "credential.https://proxy.example.helper=!og github-proxy credential-helper" \ + switch transport-alternate && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/switch.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/switch.trace && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/switch.expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/switch-status.trace" \ + git status --porcelain=v2 >.git/switch.actual && + test_cmp .git/switch.expect .git/switch.actual && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/switch-status.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/switch-status.trace && + test_grep FSCF .git/index && + test_grep FSUC .git/index + ) +' + + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'nonsemantic configuration changes reuse the authenticated manifest' ' + test_when_finished "rm -rf command-nonsemantic-history" && + test_create_repo command-nonsemantic-history && + ( + cd command-nonsemantic-history && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir sibling && + test_commit base sibling/tracked && + git config core.autocrlf false && + 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=CCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 git -c user.name=Alternate \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ + git -c user.name=Alternate status --porcelain=v2 \ + >.git/actual && + test_cmp .git/expect .git/actual && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/status.trace && + test_trace2_data fsmonitor semantic/initial-mismatch 0 \ + <.git/status.trace && + test_trace2_data fsmonitor semantic/manifest-reused 1 \ + <.git/status.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/status.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/status.trace && + test_trace2_data fsmonitor config/revalidated 1 \ + <.git/status.trace && + + test_write_lines hidden >sibling/hidden && + test_write_lines sibling/hidden >.git/excludes && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.excludesFile="$PWD/.git/excludes" \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/excludes.expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=sibling/hidden \ + GIT_TRACE2_EVENT="$PWD/.git/excludes.trace" \ + git -c core.excludesFile="$PWD/.git/excludes" \ + status --porcelain=v2 >.git/excludes.actual && + test_cmp .git/excludes.expect .git/excludes.actual && + test_must_be_empty .git/excludes.actual && + test_trace2_data fsmonitor semantic/manifest-reused 1 \ + <.git/excludes.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/excludes.trace && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.excludesFile=/dev/null \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/visible.expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/visible.trace" \ + git -c core.excludesFile=/dev/null \ + status --porcelain=v2 >.git/visible.actual && + test_cmp .git/visible.expect .git/visible.actual && + test_grep "^? sibling/hidden$" .git/visible.actual && + test_trace2_data fsmonitor semantic/manifest-reused 1 \ + <.git/visible.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/visible.trace + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'harmless configuration drift preserves authenticated tracked state' ' + test_when_finished "rm -rf command-tracked-config-history" && + test_create_repo command-tracked-config-history && + ( + cd command-tracked-config-history && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir entries && + for tracked_entry in $(test_seq 1 257) + do + test_write_lines "$tracked_entry" \ + >"entries/tracked-$tracked_entry" || return 1 + done && + git add entries && + git commit -qm base && + test-tool chmtime -120 entries/tracked-* && + git -c core.fsmonitor=false 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.preloadIndexBulk 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=CCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 \ + git -c advice.statusHints=false \ + -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=entries/tracked-1 \ + GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ + git -c advice.statusHints=false \ + status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_must_be_empty .git/actual && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/status.trace && + test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/status.trace && + test_trace2_data fsmonitor apply_count 1 \ + <.git/status.trace && + ! test_trace2_data index preload/bulk_useful \ + "[2-9][0-9]*" <.git/status.trace && + ! test_trace2_data index preload/sum_lstat \ + "[2-9][0-9]*" <.git/status.trace && + ! test_trace2_data index refresh/sum_lstat \ + "[2-9][0-9]*" <.git/status.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/status.trace && + test_region index do_write_index .git/status.trace && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/issue.trace" \ + git -c advice.statusHints=false \ + status --porcelain=v2 >.git/issue && + test_cmp .git/expect .git/issue && + test_path_is_file .git/index.csts && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/repeat.trace" \ + git -c advice.statusHints=false \ + status --porcelain=v2 >.git/repeat && + test_cmp .git/expect .git/repeat && + test_trace2_data status clean-proof/hit 1 \ + <.git/repeat.trace + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE,PERL_TEST_HELPERS,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'executable policy changes invalidate prior tracked cleanliness' ' + test_when_finished "rm -rf command-tracked-filemode-history" && + test_create_repo command-tracked-filemode-history && + ( + cd command-tracked-filemode-history && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines tracked >tracked && + git add tracked && + git commit -qm base && + git config core.filemode false && + git config core.autocrlf false && + git config core.trustctime true && + git config core.checkStat default && + git config core.untrackedCache true && + git config core.preloadIndexBulk true && + git config core.fsmonitor true && + chmod +x tracked && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + cp .git/index .git/false.before && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.filemode=true \ + -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + test_grep "^1 .M .* tracked$" .git/expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/command.trace" \ + git -c core.filemode=true \ + status --porcelain=v2 >.git/command && + test_cmp .git/expect .git/command && + ! test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/command.trace && + cp .git/false.before .git/index && + rm -f .git/index.csts .git/index.csh1.* .git/index.cswi.* && + git config core.filemode true && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/persistent.expect && + test_grep "^1 .M .* tracked$" .git/persistent.expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/persistent.trace" \ + git status --porcelain=v2 >.git/persistent && + test_cmp .git/persistent.expect .git/persistent && + ! test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/persistent.trace && + cp .git/false.before .git/index && + rm -f .git/index.csts .git/index.csh1.* .git/index.cswi.* && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git -c core.filemode=false \ + status --porcelain=v2 >.git/old-command.prime && + test_must_be_empty .git/old-command.prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + rm -f .git/index.csts .git/index.csh1.* .git/index.cswi.* && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/old-command.trace" \ + git status --porcelain=v2 >.git/old-command && + test_cmp .git/persistent.expect .git/old-command && + ! test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/old-command.trace && + + cp .git/false.before .git/index && + rm -f .git/index.csts .git/index.csh1.* .git/index.cswi.* && + git config core.filemode false && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git update-index --index-version 2 && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >.git/tampered.prime && + test_must_be_empty .git/tampered.prime && + cat >.git/tamper-tracked-policy.pl <<-\EOF && + use Digest::SHA qw(sha1 sha256); + binmode STDIN; + binmode STDOUT; + local $/; + my $index = ; + my $algorithm = shift; + my $rawsz = $algorithm eq "sha256" ? 32 : 20; + my $digest = sub { + return $algorithm eq "sha256" ? + sha256($_[0]) : sha1($_[0]); + }; + my $payload = substr($index, 0, -$rawsz); + die "invalid index checksum\n" + unless substr($index, -$rawsz) eq $digest->($payload); + die "not a version 2 index\n" + unless substr($payload, 0, 4) eq "DIRC" && + unpack("N", substr($payload, 4, 4)) == 2; + my $entries = unpack("N", substr($payload, 8, 4)); + my $offset = 12; + for (1 .. $entries) { + my $name_offset = $offset + 40 + $rawsz + 2; + my $end = index($payload, "\0", $name_offset); + die "unterminated index entry\n" if $end < 0; + $offset += (($end + 1 - $offset + 7) & ~7); + } + my $found = 0; + while ($offset < length($payload)) { + die "truncated index extension\n" + if length($payload) - $offset < 8; + my $name = substr($payload, $offset, 4); + my $size = unpack("N", substr($payload, $offset + 4, 4)); + $offset += 8; + die "index extension exceeds payload\n" + if $size > length($payload) - $offset; + if ($name eq "FSCF") { + die "duplicate or truncated semantic proof\n" + if $found++ || $size < 20 + 5 * $rawsz; + my $extension = substr($payload, $offset, $size); + my ($version, $magic, $flags, $token, $manifest) = + unpack("NNNNN", substr($extension, 0, 20)); + die "incomplete version 2 semantic proof\n" + unless $version == 2 && + $magic == 0x46534331 && + $flags == 15 && $token && + $size == 20 + $token + + 5 * $rawsz + $manifest; + die "invalid semantic proof checksum\n" + unless substr($extension, -$rawsz) eq + $digest->(substr($extension, 0, -$rawsz)); + my $policy_offset = 20 + $token + 3 * $rawsz; + substr($extension, $policy_offset, 1, + chr(ord(substr($extension, + $policy_offset, 1)) ^ 1)); + substr($extension, -$rawsz, $rawsz, + $digest->(substr($extension, 0, -$rawsz))); + substr($payload, $offset, $size, $extension); + } + $offset += $size; + } + die "missing version 2 semantic proof\n" unless $found == 1; + print $payload, $digest->($payload); + EOF + perl .git/tamper-tracked-policy.pl "$(test_oid algo)" \ + <.git/index >.git/index.policy-tampered && + cp .git/index.policy-tampered .git/index && + rm -f .git/index.csts .git/index.csh1.* .git/index.cswi.* && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/policy-tampered.trace" \ + git status --porcelain=v2 >.git/policy-tampered && + test_must_be_empty .git/policy-tampered && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/policy-tampered.trace && + ! test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/policy-tampered.trace && + + cp .git/index.policy-tampered .git/index && + rm -f .git/index.csts .git/index.csh1.* .git/index.cswi.* && + git config core.filemode true && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/policy-tampered-dirty.trace" \ + git status --porcelain=v2 >.git/policy-tampered-dirty && + test_cmp .git/persistent.expect .git/policy-tampered-dirty && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/policy-tampered-dirty.trace && + ! test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/policy-tampered-dirty.trace + ) +' + +test_expect_success MACOS,UNTRACKED_CACHE,PERL_TEST_HELPERS,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'legacy empty attribute fingerprints retain authenticated manifests' ' + test_when_finished "rm -rf legacy-empty-attribute-history" && + test_create_repo legacy-empty-attribute-history && + ( + cd legacy-empty-attribute-history && + sane_unset GIT_TEST_SPLIT_INDEX && + sane_unset GIT_ATTR_NOSYSTEM && + sane_unset GIT_CONFIG_NOSYSTEM && + GIT_CONFIG_SYSTEM="$PWD/.git/published-system.gitconfig" && + export GIT_CONFIG_SYSTEM && + test_write_lines "[advice]" " statusHints = false" \ + >"$GIT_CONFIG_SYSTEM" && + git config --show-scope --get advice.statusHints \ + >.git/system-scope && + test_grep "^system[[:space:]]" .git/system-scope && + legacy_global=$(git var GIT_ATTR_GLOBAL) && + legacy_info=$(git rev-parse --git-path info/attributes) && + test_path_is_missing "$(git var GIT_ATTR_SYSTEM)" && + test_path_is_missing //etc/gitattributes && + test_path_is_missing "$legacy_global" && + test_path_is_missing "$legacy_info" && + test_write_lines "*.txt -text" >.gitattributes && + test_write_lines stable >tracked.txt && + for legacy_dir in $(test_seq 1 128) + do + mkdir "nested-$legacy_dir" && + test_write_lines "*.txt -text" \ + >"nested-$legacy_dir/.gitattributes" && + test_write_lines "$legacy_dir" \ + >"nested-$legacy_dir/tracked.txt" || return 1 + done && + git add .gitattributes tracked.txt nested-* && + git commit -qm base && + test-tool chmtime -120 .gitattributes tracked.txt \ + nested-*/*.txt nested-*/.gitattributes && + git -c core.fsmonitor=false 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.preloadIndexBulk true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git update-index --index-version 2 && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + test-tool dump-fsmonitor >.git/current-token && + cat >.git/legacy-empty-attributes.pl <<-\EOF && + use Digest::SHA qw(sha1 sha256); + binmode STDIN; + binmode STDOUT; + local $/; + my $index = ; + my ($algorithm, $system, $global, $info, $invalid) = @ARGV; + my $rawsz = $algorithm eq "sha256" ? 32 : 20; + my $digest = sub { + return $algorithm eq "sha256" ? + sha256($_[0]) : sha1($_[0]); + }; + my $frame = sub { + return pack("N", length($_[0])) . $_[0]; + }; + my $legacy = $frame->("attribute-source-content-v1") . + $frame->(pack("N", 3)); + for my $path ($system, $global, $info) { + $legacy .= $frame->($path) . + $frame->(pack("N", 1)) . + $frame->(pack("N", 0)); + } + my $legacy_hash = $digest->($legacy); + if ($invalid) { + substr($legacy_hash, 0, 1, + chr(ord(substr($legacy_hash, 0, 1)) ^ 1)); + } + my $payload = substr($index, 0, -$rawsz); + die "invalid index checksum\n" + unless substr($index, -$rawsz) eq $digest->($payload); + die "not a version 2 index\n" + unless substr($payload, 0, 4) eq "DIRC" && + unpack("N", substr($payload, 4, 4)) == 2; + my $entries = unpack("N", substr($payload, 8, 4)); + my $offset = 12; + for (1 .. $entries) { + my $name_offset = $offset + 40 + $rawsz + 2; + my $end = index($payload, "\0", $name_offset); + die "unterminated index entry\n" if $end < 0; + $offset += (($end + 1 - $offset + 7) & ~7); + } + my $rewritten = substr($payload, 0, $offset); + my $found_proof = 0; + my $found_token = 0; + my $removed_untracked = 0; + while ($offset < length($payload)) { + die "truncated index extension\n" + if length($payload) - $offset < 8; + my $name = substr($payload, $offset, 4); + my $size = unpack("N", substr($payload, $offset + 4, 4)); + $offset += 8; + die "index extension exceeds payload\n" + if $size > length($payload) - $offset; + my $extension = substr($payload, $offset, $size); + $offset += $size; + if ($name eq "FSUC") { + $removed_untracked++; + next; + } + $found_token++ if $name eq "FSMN"; + if ($name eq "FSCF") { + die "duplicate or truncated semantic proof\n" + if $found_proof++ || $size < 20 + 4 * $rawsz; + my ($version, $magic, $flags, $token, $manifest) = + unpack("NNNNN", substr($extension, 0, 20)); + my $hashes = $version == 2 ? 5 : 4; + die "incomplete semantic proof\n" + unless ($version == 1 || $version == 2) && + $magic == 0x46534331 && + $flags == 15 && $token && + $size == 20 + $token + + $hashes * $rawsz + $manifest; + die "invalid semantic proof checksum\n" + unless substr($extension, -$rawsz) eq + $digest->(substr($extension, 0, -$rawsz)); + my $attribute_offset = 20 + $token + 2 * $rawsz; + if ($version == 2) { + substr($extension, + $attribute_offset + $rawsz, $rawsz, ""); + substr($extension, 0, 4, pack("N", 1)); + } + substr($extension, $attribute_offset, $rawsz, + $legacy_hash); + substr($extension, -$rawsz, $rawsz, + $digest->(substr($extension, 0, -$rawsz))); + } + $rewritten .= $name . pack("N", length($extension)) . + $extension; + } + die "missing complete semantic proof, token, or untracked proof\n" + unless $found_proof == 1 && $found_token == 1 && + $removed_untracked == 1; + print $rewritten, $digest->($rewritten); + EOF + perl .git/legacy-empty-attributes.pl "$(test_oid algo)" \ + //etc/gitattributes "$legacy_global" "$legacy_info" 0 \ + <.git/index >.git/index.legacy && + perl .git/legacy-empty-attributes.pl "$(test_oid algo)" \ + //etc/gitattributes "$legacy_global" "$legacy_info" 1 \ + <.git/index >.git/index.invalid && + cp .git/index.legacy .git/index && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + test_grep ! FSUC .git/index && + test-tool dump-fsmonitor >.git/legacy-token && + test_cmp .git/current-token .git/legacy-token && + GIT_OPTIONAL_LOCKS=0 git -c user.name=Legacy \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 \ + >.git/legacy.expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked.txt \ + GIT_TRACE2_EVENT="$PWD/.git/legacy.trace" \ + git -c user.name=Legacy status --porcelain=v2 \ + >.git/legacy.actual && + test_cmp .git/legacy.expect .git/legacy.actual && + test_must_be_empty .git/legacy.actual && + test_trace2_data fsmonitor semantic/legacy-empty-attributes 1 \ + <.git/legacy.trace && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/legacy.trace && + test_trace2_data fsmonitor semantic/initial-mismatch 0 \ + <.git/legacy.trace && + test_trace2_data fsmonitor semantic/manifest-reused 1 \ + <.git/legacy.trace && + test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/legacy.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/legacy.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/legacy.trace && + ! test_trace2_data status semantic_verify/prepared 1 \ + <.git/legacy.trace && + ! test_trace2_data index preload/bulk_useful \ + "[1-9][0-9]*" <.git/legacy.trace && + ! test_trace2_data index preload/sum_lstat \ + "[2-9][0-9]*" <.git/legacy.trace && + ! test_trace2_data index refresh/sum_lstat \ + "[2-9][0-9]*" <.git/legacy.trace && + test_region index do_write_index .git/legacy.trace && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/issue.trace" \ + git -c user.name=Legacy status --porcelain=v2 \ + >.git/issue.actual && + test_cmp .git/legacy.expect .git/issue.actual && + test_path_is_file .git/index.csts && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/repeat.trace" \ + git -c user.name=Legacy status --porcelain=v2 \ + >.git/repeat.actual && + test_cmp .git/legacy.expect .git/repeat.actual && + test_trace2_data status clean-proof/hit 1 \ + <.git/repeat.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/repeat.trace && + + cp .git/index.legacy .git/index && + rm -f .git/index.csts .git/index.csh1.* .git/index.cswi.* && + GIT_OPTIONAL_LOCKS=0 git -c advice.statusHints=true \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/system-advice.expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked.txt \ + GIT_TRACE2_EVENT="$PWD/.git/system-advice.trace" \ + git -c advice.statusHints=true status --porcelain=v2 \ + >.git/system-advice.actual && + test_cmp .git/system-advice.expect .git/system-advice.actual && + test_trace2_data fsmonitor semantic/legacy-empty-attributes 1 \ + <.git/system-advice.trace && + test_trace2_data fsmonitor config/tracked-epoch-preserved 1 \ + <.git/system-advice.trace && + ! test_trace2_data index preload/bulk_useful \ + "[1-9][0-9]*" <.git/system-advice.trace && + ! test_trace2_data index preload/sum_lstat \ + "[2-9][0-9]*" <.git/system-advice.trace && + + for boundary in invalid external semantic filter + do + rm -f .git/index.csts .git/index.csh1.* \ + .git/index.cswi.* "$legacy_info" && + if test "$boundary" = invalid + then + cp .git/index.invalid .git/index + else + cp .git/index.legacy .git/index + fi && + case "$boundary" in + external) + test_write_lines "*.txt text eol=crlf" \ + >"$legacy_info" && + legacy_config= + ;; + semantic) + legacy_config="-c core.autocrlf=true" + ;; + filter) + legacy_config="-c filter.legacy.clean=cat" + ;; + *) + legacy_config= + ;; + esac && + GIT_OPTIONAL_LOCKS=0 git $legacy_config \ + -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >".git/$boundary.expect" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/$boundary.trace" \ + git $legacy_config status --porcelain=v2 \ + >".git/$boundary.actual" && + test_cmp ".git/$boundary.expect" \ + ".git/$boundary.actual" && + ! test_trace2_data fsmonitor \ + semantic/legacy-empty-attributes 1 \ + <".git/$boundary.trace" && + test_trace2_data fsmonitor semantic/initial-mismatch 1 \ + <".git/$boundary.trace" && + test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <".git/$boundary.trace" && + test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <".git/$boundary.trace" || return 1 + done && + + for system_boundary in newer symlink missing + do + rm -f .git/index.csts .git/index.csh1.* \ + .git/index.cswi.* "$legacy_info" && + cp .git/index.legacy .git/index && + case "$system_boundary" in + newer) + test_write_lines "[advice]" \ + " statusHints = true" \ + >"$GIT_CONFIG_SYSTEM" + ;; + symlink) + mv "$GIT_CONFIG_SYSTEM" .git/system-config.real && + ln -s system-config.real "$GIT_CONFIG_SYSTEM" + ;; + missing) + rm -f "$GIT_CONFIG_SYSTEM" + ;; + esac && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >".git/system-$system_boundary.expect" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/system-$system_boundary.trace" \ + git status --porcelain=v2 \ + >".git/system-$system_boundary.actual" && + test_cmp ".git/system-$system_boundary.expect" \ + ".git/system-$system_boundary.actual" && + ! test_trace2_data fsmonitor \ + config/tracked-epoch-preserved 1 \ + <".git/system-$system_boundary.trace" || return 1 + done + ) +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'command-scoped conversion config still invalidates worktree proofs' ' + test_when_finished "rm -rf command-semantic-history" && + test_create_repo command-semantic-history && + ( + cd command-semantic-history && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + git config core.autocrlf false && + 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=CCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + test_write_lines changed >tracked && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + git add tracked && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + git status --porcelain=v2 >.git/staged && + test_grep "^1 M\\..* tracked$" .git/staged && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/restore.trace" \ + git -c core.autocrlf=true restore --staged tracked && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/restore.trace && + test_trace2_data fsmonitor semantic/initial-mismatch 1 \ + <.git/restore.trace && + test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/restore.trace && + test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/restore.trace && + test_grep ! FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_grep "^1 \\.M.* tracked$" .git/actual && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/status.trace && + test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/status.trace && + test_grep FSCF .git/index && + test_grep FSUC .git/index + ) +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'ordinary staged paths reuse unchanged tracked ancestor attributes' ' + test_when_finished "rm -rf staged-tracked-ancestor-attributes" && + test_create_repo staged-tracked-ancestor-attributes && + ( + cd staged-tracked-ancestor-attributes && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir -p api/existing api/brand-new/deeper && + test_write_lines "*.txt text" >api/.gitattributes && + test_write_lines existing >api/existing/tracked && + git add api/.gitattributes api/existing/tracked && + git commit -m base && + initial_branch=$(git symbolic-ref --short HEAD) && + git switch -c changed-tree && + mkdir -p api/branch-only/deeper && + test_write_lines alternate >api/existing/alternate.txt && + test_write_lines alternate >api/branch-only/deeper/alternate.txt && + git add api/existing/alternate.txt \ + api/branch-only/deeper/alternate.txt && + git commit -m alternate && + git switch "$initial_branch" && + test-tool chmtime -120 api/.gitattributes api/existing/tracked && + git update-index --refresh && + git config core.autocrlf false && + 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 && + + for location in api/existing/added.txt api/brand-new/deeper/added.txt + do + test_write_lines added >"$location" && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ + GIT_TEST_FSMONITOR_QUERY_PATH="$location" \ + git add "$location" && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/ancestor-add.trace" \ + git status --porcelain=v2 >.git/ancestor-add && + test_grep "^1 A\\..* $location$" .git/ancestor-add && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/ancestor-add.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/ancestor-add.trace && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git restore --staged "$location" && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/ancestor-remove.trace" \ + git status --porcelain=v2 >.git/ancestor-remove && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/ancestor-remove.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/ancestor-remove.trace && + rm "$location" .git/ancestor-add.trace \ + .git/ancestor-remove.trace && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH="$location" \ + git status --porcelain=v2 >.git/ancestor-deleted && + test_must_be_empty .git/ancestor-deleted || return 1 + done && + + rmdir api/brand-new/deeper api/brand-new && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=api/brand-new/ \ + git status --porcelain=v2 >.git/before-switch && + test_must_be_empty .git/before-switch && + + for branch in changed-tree "$initial_branch" + do + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git switch "$branch" && + if test "$branch" = changed-tree + then + test_path_is_file api/branch-only/deeper/alternate.txt + else + test_path_is_missing api/branch-only + fi && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/ancestor-switch.trace" \ + git status --porcelain=v2 >.git/ancestor-switch && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/ancestor-switch.expect && + test_cmp .git/ancestor-switch.expect .git/ancestor-switch && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/ancestor-switch.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/ancestor-switch.trace && + ! test_trace2_data index preload/bulk_useful \ + <.git/ancestor-switch.trace && + rm .git/ancestor-switch.trace || return 1 + done && + + cp api/.gitattributes .git/attributes.saved && + rm api/.gitattributes && + test_write_lines missing >api/existing/missing.txt && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ + GIT_TEST_FSMONITOR_QUERY_PATH=api/existing/missing.txt \ + git add api/existing/missing.txt && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/ancestor-missing.trace" \ + git status --porcelain=v2 >.git/ancestor-missing && + test_trace2_data fsmonitor config/coherent 0 \ + <.git/ancestor-missing.trace && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git restore --staged api/existing/missing.txt && + rm api/existing/missing.txt && + cp .git/attributes.saved api/.gitattributes && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=api/.gitattributes \ + git status --porcelain=v2 >.git/repaired && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/repaired-repeat && test_write_lines "*.txt -text" >api/.gitattributes && test_write_lines changed >api/existing/changed.txt && @@ -2436,6 +3623,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2461,6 +3649,275 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ ) ' +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a racy restored checkpoint advances the named provider token' ' + test_when_finished "rm -rf restored-racy-token" && + test_create_repo restored-racy-token && + ( + cd restored-racy-token && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + test-tool chmtime -120 tracked && + 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 && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSCF .git/index && + test_grep FSMN .git/index && + test_grep FSUC .git/index && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$TRASH_DIRECTORY/restored-racy-baseline.trace" \ + git status --short >.git/baseline && + test_must_be_empty .git/baseline && + test_trace2_data fsmonitor history/external-stored 1 \ + <"$TRASH_DIRECTORY/restored-racy-baseline.trace" && + cp .git/index .git/owned.before && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$TRASH_DIRECTORY/restored-racy-checkpoint.trace" \ + git status --short >.git/checkpoint && + test_must_be_empty .git/checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <"$TRASH_DIRECTORY/restored-racy-checkpoint.trace" && + cp .git/owned.before .git/index && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + test-tool chmtime -180 .git/index && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --short \ + >.git/expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + GIT_TRACE2_EVENT="$TRASH_DIRECTORY/restored-racy-first.trace" \ + git status --short >.git/first && + test_cmp .git/expect .git/first && + test_trace2_data fsmonitor history/external-restored 1 \ + <"$TRASH_DIRECTORY/restored-racy-first.trace" && + test_trace2_data fsmonitor history/external-fsmn-recovered 1 \ + <"$TRASH_DIRECTORY/restored-racy-first.trace" && + test_trace2_data fsmonitor history/external-save-reject \ + racy-index <"$TRASH_DIRECTORY/restored-racy-first.trace" && + test_trace2_data fsmonitor \ + history/external-racy-index-persisted 1 \ + <"$TRASH_DIRECTORY/restored-racy-first.trace" && + test_grep "\"label\":\"do_write_index\"" \ + "$TRASH_DIRECTORY/restored-racy-first.trace" && + test-tool dump-fsmonitor >.git/first-token && + first_token=$(sed -n "s/^fsmonitor last update //p" \ + .git/first-token) && + test -n "$first_token" && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/repeat.trace" \ + git status --short >.git/repeat && + test_cmp .git/expect .git/repeat && + test_trace2_data index extension/fsmn/read/token "$first_token" \ + <.git/repeat.trace && + ! test_trace2_data fsmonitor apply_count 1 \ + <.git/repeat.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/repeat.trace + ) +' + +test_expect_success MACOS,LEGACY_PREVIEW_FSMONITOR_GIT,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a legacy writer preserves clean siblings when staging a new directory' ' + test_when_finished "stop_daemon_delete_repo foreign-staged-directory" && + test_create_repo foreign-staged-directory && + ( + cd foreign-staged-directory && + sane_unset GIT_TEST_SPLIT_INDEX && + for legacy_entry in $(test_seq 1 129) + do + legacy_dir="existing-$((legacy_entry % 24))" && + mkdir -p "$legacy_dir" && + test_write_lines "$legacy_entry" \ + >"$legacy_dir/tracked-$legacy_entry" || return 1 + done && + git add existing-* && + git commit -qm base && + test-tool chmtime -120 existing-*/* && + 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.preloadIndexBulk true && + test_write_lines "*.forced" >.git/info/exclude && + git config core.fsmonitor true && + git fsmonitor--daemon start --start-timeout=10 && + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_TRACE2_EVENT="$PWD/.git/checkpoint.trace" \ + git status --porcelain=v2 >.git/checkpoint && + test_must_be_empty .git/checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/checkpoint.trace && + + mkdir brand-new && + test_write_lines staged >brand-new/first && + /opt/homebrew/Cellar/og-preview/2026-08-11T2321Z/libexec/openai-git/bin/git \ + add brand-new/first && + cp .git/index .git/foreign-before.index && + test_grep FSMN .git/index && + test_grep ! FSCF .git/index && + test_grep ! FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + test_grep "^1 A\\..* brand-new/first$" .git/expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TRACE2_EVENT="$PWD/.git/foreign-stage.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_trace2_data fsmonitor history/external-semantic-restored 1 \ + <.git/foreign-stage.trace && + test_trace2_data fsmonitor history/external-untracked-restored 1 \ + <.git/foreign-stage.trace && + test_trace2_data fsmonitor history/external-tracked-restored \ + "[1-9][0-9]*" <.git/foreign-stage.trace && + ! test_trace2_data index preload/bulk_useful \ + "[1-9][0-9]*" <.git/foreign-stage.trace && + ! test_trace2_data index preload/bulk_dirs \ + "[1-9][0-9]*" <.git/foreign-stage.trace && + + /opt/homebrew/Cellar/og-preview/2026-08-11T2321Z/libexec/openai-git/bin/git \ + restore --staged brand-new/first && + cp .git/index .git/foreign-unstaged-before.index && + test_grep FSMN .git/index && + test_grep ! FSCF .git/index && + test_grep ! FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/unstaged.expect && + test_grep "^? brand-new/$" .git/unstaged.expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TRACE2_EVENT="$PWD/.git/foreign-unstaged.trace" \ + git status --porcelain=v2 >.git/unstaged.actual && + test_cmp .git/unstaged.expect .git/unstaged.actual && + test_trace2_data fsmonitor history/external-restored 1 \ + <.git/foreign-unstaged.trace && + ! test_trace2_data index preload/bulk_useful \ + "[1-9][0-9]*" <.git/foreign-unstaged.trace && + ! test_trace2_data index preload/bulk_dirs \ + "[1-9][0-9]*" <.git/foreign-unstaged.trace && + + test_write_lines ignored >existing-0/ignored.forced && + /opt/homebrew/Cellar/og-preview/2026-08-11T2321Z/libexec/openai-git/bin/git \ + add --force existing-0/ignored.forced && + cp .git/index .git/foreign-forced-before.index && + test_grep FSMN .git/index && + test_grep ! FSCF .git/index && + test_grep ! FSUC .git/index && + GIT_OPTIONAL_LOCKS=0 git \ + -c core.fsmonitor=false -c core.untrackedCache=false \ + status --porcelain=v2 >.git/forced.expect && + test_grep "^1 A\\..* existing-0/ignored.forced$" \ + .git/forced.expect && + test_grep "^? brand-new/$" .git/forced.expect && + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TRACE2_EVENT="$PWD/.git/foreign-forced.trace" \ + git status --porcelain=v2 >.git/forced.actual && + test_cmp .git/forced.expect .git/forced.actual && + test_trace2_data fsmonitor history/external-semantic-restored 1 \ + <.git/foreign-forced.trace && + test_trace2_data fsmonitor history/external-untracked-restored 1 \ + <.git/foreign-forced.trace && + test_trace2_data fsmonitor history/external-tracked-restored \ + "[1-9][0-9]*" <.git/foreign-forced.trace && + ! test_trace2_data index preload/bulk_useful \ + "[1-9][0-9]*" <.git/foreign-forced.trace && + ! test_trace2_data index preload/bulk_dirs \ + "[1-9][0-9]*" <.git/foreign-forced.trace + ) +' + +test_expect_success FOREIGN_FSMONITOR_GIT,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a foreign index writer does not strand a racy provider token' ' + test_when_finished "stop_daemon_delete_repo foreign-racy-token" && + test_create_repo foreign-racy-token && + ( + cd foreign-racy-token && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + test_commit peer racy-peer && + test-tool chmtime -120 tracked racy-peer && + 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 && + git fsmonitor--daemon start --start-timeout=10 && + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + GIT_TRACE2_EVENT="$PWD/.git/checkpoint.trace" \ + git status --short >.git/checkpoint && + test_must_be_empty .git/checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/checkpoint.trace && + + /opt/homebrew/bin/git update-index --force-write-index && + test_grep FSMN .git/index && + test_grep ! FSCF .git/index && + test-tool dump-fsmonitor >.git/homebrew-token && + homebrew_token=$(sed -n "s/^fsmonitor last update //p" \ + .git/homebrew-token) && + test -n "$homebrew_token" && + test-tool chmtime -120 tracked && + test-tool fsmonitor-client query \ + --token "$homebrew_token" >.git/observed && + nul_to_q <.git/observed >.git/observed.paths && + test_grep tracked .git/observed.paths && + test-tool chmtime -180 .git/index && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --short \ + >.git/expect && + GIT_TRACE2_EVENT="$PWD/.git/first.trace" \ + git status --short >.git/first && + test_cmp .git/expect .git/first && + test_trace2_data fsmonitor history/external-restored 1 \ + <.git/first.trace && + test_trace2_data fsmonitor history/external-save-reject \ + racy-index <.git/first.trace && + test_trace2_data fsmonitor \ + history/external-racy-index-persisted 1 \ + <.git/first.trace && + test_grep "\"label\":\"do_write_index\"" .git/first.trace && + test-tool dump-fsmonitor >.git/first-token && + first_token=$(sed -n "s/^fsmonitor last update //p" \ + .git/first-token) && + test -n "$first_token" && + test "$homebrew_token" != "$first_token" && + GIT_TRACE2_EVENT="$PWD/.git/repeat.trace" \ + git status --short >.git/repeat && + test_cmp .git/expect .git/repeat && + test_trace2_data index extension/fsmn/read/token "$first_token" \ + <.git/repeat.trace && + ! test_trace2_data fsmonitor apply_count 1 \ + <.git/repeat.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/repeat.trace + ) +' + test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 'describe --dirty preserves closed semantic history' ' test_when_finished "rm -rf describe-dirty-history" && @@ -2473,6 +3930,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2506,6 +3964,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -2541,6 +4000,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -3813,6 +5273,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -3845,6 +5306,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -3881,6 +5343,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -3920,6 +5383,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -3949,6 +5413,7 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -3970,44 +5435,284 @@ test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ test_trace2_data fsmonitor config/coherent 1 \ <.git/status.trace && ! test_trace2_data status semantic_verify/prepared 1 \ - <.git/status.trace + <.git/status.trace + ) +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'one-tree read-tree reset preserves closed semantic history' ' + test_when_finished "rm -rf read-tree-reset-history" && + test_create_repo read-tree-reset-history && + ( + cd read-tree-reset-history && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + 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=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSCF .git/index && + + test_write_lines changed >tracked && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + git status >.git/dirty && + test_grep "modified:.*tracked" .git/dirty && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ + GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ + git read-tree --reset -u HEAD >.git/read-tree && + + 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 1 \ + <.git/status.trace && + ! test_trace2_data status semantic_verify/prepared 1 \ + <.git/status.trace + ) +' + +test_expect_success UNTRACKED_CACHE,HARDLINKS,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a lost fsmonitor token reuses an authenticated external checkpoint' ' + test_when_finished "rm -rf missing-fsmonitor-token-checkpoint" && + test_create_repo missing-fsmonitor-token-checkpoint && + ( + cd missing-fsmonitor-token-checkpoint && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines "/ignored/" >.gitignore && + printf "aaaa\\n" >tracked && + git add .gitignore tracked && + git commit -m base && + mkdir ignored && + ln tracked ignored/alias && + test-tool chmtime -120 tracked .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 && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/materialized && + test_must_be_empty .git/materialized && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/checkpoint.trace" \ + git status --porcelain=v2 >.git/checkpoint && + test_must_be_empty .git/checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/checkpoint.trace && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + cp .git/index .git/missing.index && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/clean.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/clean.trace" \ + git status --porcelain=v2 >.git/clean.actual && + test_cmp .git/clean.expect .git/clean.actual && + test_trace2_data fsmonitor history/external-fsmn-recovered 1 \ + <.git/clean.trace && + test_trace2_data fsmonitor history/external-restored 1 \ + <.git/clean.trace && + test_trace2_data fsmonitor config/coherent 1 \ + <.git/clean.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/clean.trace && + ! test_trace2_data fsmonitor semantic/token-reset-stat-baseline 1 \ + <.git/clean.trace && + + mtime=$(test-tool chmtime --get tracked) && + printf "bbbb\\n" >ignored/alias && + test-tool chmtime =$mtime ignored/alias && + test "$(git hash-object tracked)" != \ + "$(git rev-parse HEAD:tracked)" && + cp .git/missing.index .git/index && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=ignored/alias \ + GIT_TRACE2_EVENT="$PWD/.git/dirty.trace" \ + git status --porcelain=v2 >.git/dirty.actual && + test_grep "^1 \\.M .* tracked$" .git/dirty.actual + ) +' + +test_expect_success UNTRACKED_CACHE,HARDLINKS,PERL,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a lost token never trusts an unwatched tracked hardlink' ' + test_when_finished \ + "rm -rf missing-fsmonitor-token-hardlink missing-fsmonitor-token.alias" && + test_create_repo missing-fsmonitor-token-hardlink && + ( + cd missing-fsmonitor-token-hardlink && + sane_unset GIT_TEST_SPLIT_INDEX && + printf "aaaa\\n" >tracked && + test_write_lines stable >sibling && + git add tracked sibling && + git commit -m base && + ln tracked ../missing-fsmonitor-token.alias && + test-tool chmtime -120 tracked sibling && + git update-index --refresh && + git config core.trustctime true && + git config core.checkStat default && + 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=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + rm -f .git/index.csh1.* .git/index.cswi.* .git/index.csts && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + mtime=$(test-tool chmtime --get tracked) && + for attempt in 1 2 3 4 5 + do + printf "aaaa\\n" \ + >../missing-fsmonitor-token.alias && + test-tool chmtime =$mtime \ + ../missing-fsmonitor-token.alias && + git -c core.fsmonitor=false update-index \ + --refresh --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + ctime=$(perl -e "print((stat(shift))[10])" tracked) && + printf "bbbb\\n" \ + >../missing-fsmonitor-token.alias && + test-tool chmtime =$mtime \ + ../missing-fsmonitor-token.alias && + if test "$(perl -e "print((stat(shift))[10])" tracked)" = \ + "$ctime" + then + break + fi || return 1 + done && + test "$(perl -e "print((stat(shift))[10])" tracked)" = \ + "$ctime" && + test "$(git hash-object tracked)" != \ + "$(git rev-parse HEAD:tracked)" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=TCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/recovery.trace" \ + git status --porcelain=v2 >.git/actual && + test_grep "^1 \\.M .* tracked$" .git/actual && + test_line_count = 1 .git/actual && + test_trace2_data fsm_client query/trivial-response 1 \ + <.git/recovery.trace && + test_trace2_data fsmonitor semantic/token-reset-stat-baseline 1 \ + <.git/recovery.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/recovery.trace + ) +' + +test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a missing fsmonitor token reuses strong tracked-file stat identities' ' + test_when_finished "rm -rf missing-fsmonitor-token-strong" && + test_create_repo missing-fsmonitor-token-strong && + ( + cd missing-fsmonitor-token-strong && + sane_unset GIT_TEST_SPLIT_INDEX && + test_commit base tracked && + test-tool chmtime -120 tracked && + git update-index --refresh && + git config core.trustctime true && + git config core.checkStat default && + 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=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSCF .git/index && + rm -f .git/index.csh1.* .git/index.cswi.* .git/index.csts && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/recovery.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_trace2_data fsmonitor semantic/token-reset-stat-baseline 1 \ + <.git/recovery.trace && + test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/recovery.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/recovery.trace && + ! test_trace2_data status semantic_verify/prepared 1 \ + <.git/recovery.trace && + test_trace2_data fsmonitor token_closure/accepted 1 \ + <.git/recovery.trace && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/recovery.trace ) ' -test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ - 'one-tree read-tree reset preserves closed semantic history' ' - test_when_finished "rm -rf read-tree-reset-history" && - test_create_repo read-tree-reset-history && +test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a missing fsmonitor token cannot trust weak tracked-file identities' ' + test_when_finished "rm -rf missing-fsmonitor-token-weak" && + test_create_repo missing-fsmonitor-token-weak && ( - cd read-tree-reset-history && + cd missing-fsmonitor-token-weak && sane_unset GIT_TEST_SPLIT_INDEX && - test_commit base tracked && + printf "aaaa\\n" >tracked && + git add tracked && + git commit -m base && + git config core.trustctime false && + git config core.checkStat minimal && + test-tool chmtime =-60 tracked && + git update-index --refresh && + mtime=$(test-tool chmtime --get tracked) && 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && test_grep FSCF .git/index && - - test_write_lines changed >tracked && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCC \ - GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ - git status >.git/dirty && - test_grep "modified:.*tracked" .git/dirty && - GIT_TEST_FSMONITOR_QUERY_SEQUENCE=D \ - GIT_TEST_FSMONITOR_QUERY_PATH=tracked \ - git read-tree --reset -u HEAD >.git/read-tree && - - 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 1 \ - <.git/status.trace && - ! test_trace2_data status semantic_verify/prepared 1 \ - <.git/status.trace + rm -f .git/index.csh1.* .git/index.cswi.* .git/index.csts && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + printf "bbbb\\n" >tracked && + test-tool chmtime =$mtime tracked && + test "$(git hash-object tracked)" != \ + "$(git rev-parse HEAD:tracked)" && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/recovery.trace" \ + git status --porcelain=v2 >.git/actual && + test_grep "^1 \\.M .* tracked$" .git/actual && + ! test_trace2_data fsmonitor semantic/token-reset-stat-baseline 1 \ + <.git/recovery.trace && + test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/recovery.trace ) ' @@ -4027,6 +5732,7 @@ test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ git update-index --fsmonitor-valid tracked && test_grep ! FSCF .git/index && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ git status --porcelain=v2 >.git/actual && @@ -4086,6 +5792,7 @@ test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ 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=CCCC \ git status --porcelain=v2 >.git/prime && test_must_be_empty .git/prime && @@ -4469,6 +6176,751 @@ test_expect_success UNTRACKED_CACHE,!MINGW,!CYGWIN \ ) ' +prepare_deleted_attribute_repo () { + test_create_repo "$1" && + ( + cd "$1" && + sane_unset GIT_TEST_SPLIT_INDEX && + mkdir scoped sibling && + test_write_lines "*.txt -text" >.gitattributes && + test_write_lines "*.txt -text" >scoped/.gitattributes && + test_write_lines root >tracked.txt && + test_write_lines scoped >scoped/tracked.txt && + test_write_lines sibling >sibling/tracked.txt && + git add .gitattributes scoped sibling tracked.txt && + git commit -qm base && + 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_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSCF .git/index + ) +} + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'deleting identical tracked attributes preserves root and nested proofs' ' + test_when_finished "rm -rf deleted-attributes-root deleted-attributes-nested" && + for scope in root nested + do + repo=deleted-attributes-$scope && + prepare_deleted_attribute_repo "$repo" && + ( + cd "$repo" && + if test "$scope" = root + then + path=.gitattributes + else + path=scoped/.gitattributes + fi && + rm "$path" && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH="$path" \ + GIT_TRACE2_EVENT="$PWD/.git/deleted.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_line_count = 1 .git/actual && + test_grep "^1 \\.D .* $path$" .git/actual && + test_trace2_data fsmonitor \ + semantic/manifest-reconciled 1 <.git/deleted.trace && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-count <.git/deleted.trace && + ! have_t2_data_event fsmonitor \ + semantic/attributes-scope <.git/deleted.trace && + ! have_t2_data_event fsmonitor \ + semantic/attributes-cone <.git/deleted.trace && + ! test_trace2_data fsmonitor \ + semantic/strong-invalidation 1 <.git/deleted.trace && + ! test_trace2_data index \ + preload/sum_lstat "[2-9][0-9]*" \ + <.git/deleted.trace && + ! test_trace2_data index \ + preload/sum_lstat "1[0-9][0-9]*" \ + <.git/deleted.trace && + ! test_trace2_data index \ + refresh/sum_lstat "[2-9][0-9]*" \ + <.git/deleted.trace && + ! test_trace2_data index \ + refresh/sum_lstat "1[0-9][0-9]*" \ + <.git/deleted.trace && + git show "HEAD:$path" >"$path" && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/restored.expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH="$path" \ + GIT_TRACE2_EVENT="$PWD/.git/restored.trace" \ + git status --porcelain=v2 >.git/restored.actual && + test_cmp .git/restored.expect .git/restored.actual && + test_must_be_empty .git/restored.actual && + test_trace2_data fsmonitor \ + semantic/manifest-reconciled 1 \ + <.git/restored.trace && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-count <.git/restored.trace && + ! have_t2_data_event fsmonitor \ + semantic/attributes-scope <.git/restored.trace && + ! test_trace2_data index \ + refresh/sum_lstat "[2-9][0-9]*" \ + <.git/restored.trace && + ! test_trace2_data index \ + refresh/sum_lstat "1[0-9][0-9]*" \ + <.git/restored.trace && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/repeated.trace" \ + git status --porcelain=v2 >.git/repeated.actual && + test_must_be_empty .git/repeated.actual && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-count <.git/repeated.trace && + ! have_t2_data_event fsmonitor \ + semantic/attributes-scope <.git/repeated.trace + ) || return 1 + done +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'attribute deletion reuses a checkpoint when the index lost its token' ' + test_when_finished \ + "rm -rf deleted-checkpoint-root deleted-checkpoint-nested" && + for scope in root nested + do + repo=deleted-checkpoint-$scope && + prepare_deleted_attribute_repo "$repo" && + ( + cd "$repo" && + sane_unset GIT_TEST_SPLIT_INDEX && + test-tool chmtime -120 .gitattributes \ + scoped/.gitattributes tracked.txt \ + scoped/tracked.txt sibling/tracked.txt && + git -c core.fsmonitor=false update-index --refresh && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/materialized && + test_must_be_empty .git/materialized && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/checkpoint.trace" \ + git status --porcelain=v2 >.git/checkpoint && + test_must_be_empty .git/checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/checkpoint.trace && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + if test "$scope" = root + then + path=.gitattributes + else + path=scoped/.gitattributes + fi && + rm "$path" && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH="$path" \ + GIT_TRACE2_EVENT="$PWD/.git/deleted.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_line_count = 1 .git/actual && + test_grep "^1 \\.D .* $path$" .git/actual && + test_trace2_data fsmonitor \ + history/external-fsmn-recovered 1 \ + <.git/deleted.trace && + test_trace2_data fsmonitor \ + semantic/manifest-reconciled 1 \ + <.git/deleted.trace && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-count <.git/deleted.trace && + ! have_t2_data_event fsmonitor \ + semantic/attributes-scope <.git/deleted.trace && + ! test_trace2_data fsmonitor \ + history/external-proof-invalidated 1 \ + <.git/deleted.trace && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + git show "HEAD:$path" >"$path" && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/restored.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH="$path" \ + GIT_TRACE2_EVENT="$PWD/.git/restored.trace" \ + git status --porcelain=v2 >.git/restored.actual && + test_cmp .git/restored.expect .git/restored.actual && + test_must_be_empty .git/restored.actual && + test_trace2_data fsmonitor \ + history/external-fsmn-recovered 1 \ + <.git/restored.trace && + test_trace2_data fsmonitor \ + semantic/manifest-reconciled 1 \ + <.git/restored.trace && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-count <.git/restored.trace && + ! have_t2_data_event fsmonitor \ + semantic/attributes-scope <.git/restored.trace && + ! test_trace2_data fsmonitor \ + history/external-proof-invalidated 1 \ + <.git/restored.trace + ) || return 1 + done +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'a changed attribute fallback cannot resurrect a lost-token checkpoint' ' + test_when_finished "rm -rf deleted-checkpoint-changed" && + prepare_deleted_attribute_repo deleted-checkpoint-changed && + ( + cd deleted-checkpoint-changed && + sane_unset GIT_TEST_SPLIT_INDEX && + test-tool chmtime -120 .gitattributes \ + scoped/.gitattributes tracked.txt scoped/tracked.txt \ + sibling/tracked.txt && + git -c core.fsmonitor=false update-index --refresh && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + test_write_lines "*.txt text eol=crlf" >.gitattributes && + test-tool chmtime -120 .gitattributes && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + git status --porcelain=v2 >.git/materialized && + test_grep "^1 \\.M .* .gitattributes$" .git/materialized && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/checkpoint.trace" \ + git status --porcelain=v2 >.git/checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/checkpoint.trace && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + rm .gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/deleted.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_grep "^1 \\.D .* .gitattributes$" .git/actual && + test_trace2_data fsmonitor history/external-proof-invalidated 1 \ + <.git/deleted.trace && + test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/deleted.trace && + ! test_trace2_data fsmonitor \ + history/external-fsmn-recovered 1 <.git/deleted.trace && + ! test_trace2_data fsmonitor semantic/manifest-reconciled 1 \ + <.git/deleted.trace + ) +' + +test_expect_success UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'deleting changed tracked attributes still invalidates their scope' ' + test_when_finished "rm -rf deleted-attributes-changed" && + prepare_deleted_attribute_repo deleted-attributes-changed && + ( + cd deleted-attributes-changed && + test_write_lines "*.txt text eol=crlf" >.gitattributes && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + git status --porcelain=v2 >.git/changed && + test_grep "^1 \\.M .* .gitattributes$" .git/changed && + test_grep FSCF .git/index && + rm .gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/deleted.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_grep "^1 \\.D .* .gitattributes$" .git/actual && + test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/deleted.trace && + ! test_trace2_data fsmonitor semantic/manifest-reconciled 1 \ + <.git/deleted.trace + ) +' + +test_expect_success SYMLINKS,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'an attribute symlink cannot impersonate an indexed fallback' ' + test_when_finished "rm -rf deleted-attributes-symlink" && + prepare_deleted_attribute_repo deleted-attributes-symlink && + ( + cd deleted-attributes-symlink && + rm .gitattributes && + ln -s tracked.txt .gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false status --porcelain=v2 \ + >.git/expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/symlink.trace" \ + git status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_grep "^1 \\.T .* .gitattributes$" .git/actual && + test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/symlink.trace && + ! test_trace2_data fsmonitor semantic/manifest-reconciled 1 \ + <.git/symlink.trace + ) +' + +test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'diff skips content verification for display-only root attributes' ' + test_when_finished "rm -rf display-only-root-attributes" && + test_create_repo display-only-root-attributes && + ( + cd display-only-root-attributes && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines "*.txt -text" >.gitattributes && + test_write_lines alpha >tracked.txt && + test_write_lines beta >sibling.txt && + for attribute_dir in $(test_seq 1 128) + do + mkdir "nested-$attribute_dir" && + test_write_lines "*.txt -text" \ + >"nested-$attribute_dir/.gitattributes" && + test_write_lines "$attribute_dir" \ + >"nested-$attribute_dir/tracked.txt" || return 1 + done && + git add .gitattributes tracked.txt sibling.txt nested-* && + git commit -m base && + 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_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSCF .git/index && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + test_write_lines "*.gen linguist-generated" \ + >>.gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >.git/display.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=TCCC \ + GIT_TRACE2_EVENT="$PWD/.git/display.trace" \ + git diff >.git/display.actual && + test_cmp .git/display.expect .git/display.actual && + test_grep "^+\\*.gen linguist-generated$" \ + .git/display.actual && + test_trace2_data fsmonitor semantic/nonconversion-attributes 1 \ + <.git/display.trace && + test_trace2_data fsmonitor semantic/manifest-changed 1 \ + <.git/display.trace && + ! test_trace2_data fsmonitor apply/global-invalidation 1 \ + <.git/display.trace && + ! test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/display.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/display.trace && + ! test_trace2_data status semantic_verify/prepared 1 \ + <.git/display.trace && + ! test_trace2_data index preload/bulk_useful \ + "[1-9][0-9]*" <.git/display.trace && + + # Preserve an authenticated checkpoint from the unchanged old + # index before an unstaged, presentation-only root change. + git show HEAD:.gitattributes >.gitattributes && + test-tool chmtime -120 .gitattributes tracked.txt sibling.txt \ + nested-*/*.txt nested-*/.gitattributes && + git -c core.fsmonitor=false update-index --refresh && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/old-display-materialized && + test_must_be_empty .git/old-display-materialized && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/old-display-checkpoint.trace" \ + git status --porcelain=v2 >.git/old-display-checkpoint && + test_must_be_empty .git/old-display-checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/old-display-checkpoint.trace && + old_display_attributes=$(git rev-parse :.gitattributes) && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + test_write_lines "*.gen linguist-generated" \ + >>.gitattributes && + test "$old_display_attributes" = \ + "$(git rev-parse :.gitattributes)" && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >.git/old-display.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDDCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/old-display.trace" \ + git diff >.git/old-display.actual && + test_cmp .git/old-display.expect .git/old-display.actual && + test_grep "^+\\*.gen linguist-generated$" \ + .git/old-display.actual && + test_trace2_data fsmonitor history/external-restored 1 \ + <.git/old-display.trace && + test_trace2_data fsmonitor \ + history/external-fsmn-recovered 1 \ + <.git/old-display.trace && + test_trace2_data fsmonitor semantic/nonconversion-attributes 1 \ + <.git/old-display.trace && + test_trace2_data fsmonitor \ + semantic/nonconversion-attribute-replayed 1 \ + <.git/old-display.trace && + ! test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <.git/old-display.trace && + ! test_trace2_data fsmonitor semantic/manifest-candidates 129 \ + <.git/old-display.trace && + ! test_trace2_data fsmonitor apply/global-invalidation 1 \ + <.git/old-display.trace && + ! test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/old-display.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/old-display.trace && + ! test_trace2_data status semantic_verify/prepared 1 \ + <.git/old-display.trace && + + for boundary in conversion macro + do + if test "$boundary" = conversion + then + test_write_lines "*.txt text eol=crlf" \ + >.gitattributes + else + test_write_lines \ + "[attr]linguist-generated filter=custom" \ + "*.gen linguist-generated" \ + >.gitattributes + fi && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >".git/$boundary.expect" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDDCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/$boundary.trace" \ + git diff >".git/$boundary.actual" && + test_cmp ".git/$boundary.expect" \ + ".git/$boundary.actual" && + test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <".git/$boundary.trace" && + test_trace2_data fsmonitor semantic/manifest-scan-count 1 \ + <".git/$boundary.trace" && + ! test_trace2_data fsmonitor history/external-restored 1 \ + <".git/$boundary.trace" && + ! test_trace2_data fsmonitor \ + semantic/nonconversion-attribute-replayed 1 \ + <".git/$boundary.trace" && + ! test_trace2_data fsmonitor \ + semantic/nonconversion-attributes 1 \ + <".git/$boundary.trace" || return 1 + done && + + test_write_lines "*.txt text eol=crlf" >.gitattributes && + test_must_fail env \ + GIT_TRACE2_EVENT="$PWD/.git/scoped-attributes.trace" \ + git update-index --refresh -- .gitattributes && + ! test_trace2_data fsmonitor history/external-restored 1 \ + <.git/scoped-attributes.trace && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >.git/scoped-attributes.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=TCCC \ + GIT_TRACE2_EVENT="$PWD/.git/scoped-attributes-diff.trace" \ + git diff >.git/scoped-attributes.actual && + test_cmp .git/scoped-attributes.expect \ + .git/scoped-attributes.actual && + test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/scoped-attributes-diff.trace && + ! test_trace2_data fsmonitor \ + semantic/nonconversion-attributes 1 \ + <.git/scoped-attributes-diff.trace && + + git show HEAD:.gitattributes >.gitattributes && + cp .git/index .git/old-index && + test_write_lines \ + "*.one linguist-generated" \ + "*.two linguist-generated" \ + "*.three linguist-generated" \ + "*.four linguist-generated" \ + >>.gitattributes && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git add .gitattributes && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git commit -qm generated && + new_attributes=$(git rev-parse HEAD:.gitattributes) && + history_base=$(git rev-parse HEAD) && + { + for unrelated in $(test_seq 1 1038) + do + printf "commit refs/heads/linguist-history\\n" && + printf "mark :%s\\n" "$unrelated" && + printf "committer Test 1112911993 +0000\\n" && + printf "data 9\\nunrelated\\n" && + if test "$unrelated" = 1 + then + printf "from %s\\n\\n" "$history_base" + else + previous=$((unrelated - 1)) && + printf "from :%s\\n\\n" "$previous" + fi || return 1 + done + } >.git/history.stream && + git fast-import --quiet <.git/history.stream && + git update-ref "$(git symbolic-ref HEAD)" \ + "$(git rev-parse refs/heads/linguist-history)" && + git commit-graph write --reachable --changed-paths && + test "$(git rev-parse HEAD:.gitattributes)" = \ + "$new_attributes" && + test "$(git rev-parse HEAD^:.gitattributes)" = \ + "$new_attributes" && + test "$(git rev-parse HEAD~1038:.gitattributes)" = \ + "$new_attributes" && + test "$(git rev-parse HEAD~1039:.gitattributes)" != \ + "$new_attributes" && + cp .git/old-index .git/index && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor \ + --cacheinfo "100644,$new_attributes,.gitattributes" \ + --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff --cached \ + >.git/committed-index && + test_must_be_empty .git/committed-index && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >.git/committed.expect && + test_must_be_empty .git/committed.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=TCCC \ + GIT_TRACE2_EVENT="$PWD/.git/committed.trace" \ + git diff >.git/committed.actual && + test_cmp .git/committed.expect .git/committed.actual && + test_trace2_data fsmonitor semantic/nonconversion-attributes 1 \ + <.git/committed.trace && + test_trace2_data fsmonitor \ + semantic/attribute-history-commits 1039 \ + <.git/committed.trace && + test_trace2_data fsmonitor \ + semantic/attribute-history-bloom-skips 1038 \ + <.git/committed.trace && + test_trace2_data fsmonitor semantic/manifest-changed 1 \ + <.git/committed.trace && + ! test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/committed.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/committed.trace && + + # Keep the same logical index but checkpoint the old worktree + # attributes, as an earlier dirty command would have done. + test-tool chmtime -120 .gitattributes tracked.txt sibling.txt \ + nested-*/*.txt nested-*/.gitattributes && + git -c core.fsmonitor=false update-index --refresh && + git show HEAD~1039:.gitattributes >.gitattributes && + test-tool chmtime -120 .gitattributes && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + git status --porcelain=v2 >.git/old-materialized && + test_grep "^1 \\.M .* .gitattributes$" \ + .git/old-materialized && + test_grep FSCF .git/index && + test_grep FSUC .git/index && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/old-checkpoint.trace" \ + git status --porcelain=v2 >.git/old-checkpoint && + test_cmp .git/old-materialized .git/old-checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/old-checkpoint.trace && + find .git -maxdepth 1 -type f -name "index.csh1.*" \ + >.git/old-checkpoints && + test_line_count = 1 .git/old-checkpoints && + rm -f .git/index.csts && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor --force-write-index && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + git show HEAD:.gitattributes >.gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >.git/checkpoint.expect && + test_must_be_empty .git/checkpoint.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDDCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/checkpoint.trace" \ + git diff >.git/checkpoint.actual && + test_cmp .git/checkpoint.expect .git/checkpoint.actual && + test_trace2_data fsmonitor history/external-restored 1 \ + <.git/checkpoint.trace && + test_trace2_data fsmonitor \ + history/external-fsmn-recovered 1 \ + <.git/checkpoint.trace && + test_trace2_data fsmonitor semantic/nonconversion-attributes 1 \ + <.git/checkpoint.trace && + test_trace2_data fsmonitor \ + semantic/attribute-history-commits 1039 \ + <.git/checkpoint.trace && + test_trace2_data fsmonitor \ + semantic/attribute-history-bloom-skips 1038 \ + <.git/checkpoint.trace && + ! test_trace2_data fsmonitor semantic/attributes-scope 0 \ + <.git/checkpoint.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/checkpoint.trace && + + # A writer can also advance the staged attribute blob after the + # checkpoint. Its token and tracked bitmap no longer authenticate + # the named index, but the old attribute manifest remains useful. + if test_have_prereq MACOS + then + test-tool chmtime -120 \ + .gitattributes tracked.txt sibling.txt \ + nested-*/*.txt nested-*/.gitattributes && + git -c core.fsmonitor=false update-index --refresh && + git show HEAD~1039:.gitattributes >.gitattributes && + old_attributes=$(git rev-parse HEAD~1039:.gitattributes) && + test-tool chmtime -120 .gitattributes && + git -c core.fsmonitor=false update-index \ + --cacheinfo "100644,$old_attributes,.gitattributes" \ + --force-write-index && + test "$(git rev-parse :.gitattributes)" = \ + "$old_attributes" && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DDCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + git status --porcelain=v2 \ + >.git/advanced-materialized && + test_grep "^1 M\\. .* .gitattributes$" \ + .git/advanced-materialized && + test_grep FSCF .git/index && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/advanced-checkpoint.trace" \ + git status --porcelain=v2 \ + >.git/advanced-checkpoint && + test_cmp .git/advanced-materialized \ + .git/advanced-checkpoint && + test_trace2_data fsmonitor history/external-stored 1 \ + <.git/advanced-checkpoint.trace && + rm -f .git/index.csts && + git -c core.fsmonitor=false update-index \ + --no-fsmonitor \ + --cacheinfo \ + "100644,$new_attributes,.gitattributes" \ + --force-write-index && + test "$(git rev-parse :.gitattributes)" = \ + "$new_attributes" && + test_grep ! FSMN .git/index && + test_grep FSCF .git/index && + test_grep UNTR .git/index && + test_grep ! FSUC .git/index && + git show HEAD:.gitattributes >.gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >.git/advanced.expect && + test_must_be_empty .git/advanced.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DTCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/advanced.trace" \ + git diff >.git/advanced.actual && + test_cmp .git/advanced.expect .git/advanced.actual && + test_trace2_data fsmonitor \ + history/external-bootstrap-manifest 1 \ + <.git/advanced.trace && + test_trace2_data fsmonitor \ + semantic/nonconversion-attributes 1 \ + <.git/advanced.trace && + test_trace2_data fsmonitor \ + semantic/attribute-history-commits 1039 \ + <.git/advanced.trace && + test_trace2_data fsmonitor \ + semantic/attribute-history-bloom-skips 1038 \ + <.git/advanced.trace && + test_trace2_data fsmonitor \ + semantic/token-reset-stat-baseline 1 \ + <.git/advanced.trace && + ! test_trace2_data fsmonitor history/external-restored 1 \ + <.git/advanced.trace && + ! test_trace2_data fsmonitor \ + history/external-fsmn-recovered 1 \ + <.git/advanced.trace && + ! have_t2_data_event fsmonitor \ + semantic/manifest-scan-count \ + <.git/advanced.trace && + ! test_trace2_data fsmonitor \ + semantic/strong-invalidation 1 \ + <.git/advanced.trace && + + test_write_lines "*.txt text eol=crlf" \ + >.gitattributes && + GIT_OPTIONAL_LOCKS=0 git -c core.fsmonitor=false \ + -c core.untrackedCache=false diff \ + >.git/advanced-conversion.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DTCCCCCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=.gitattributes \ + GIT_TRACE2_EVENT="$PWD/.git/advanced-conversion.trace" \ + git diff >.git/advanced-conversion.actual && + test_cmp .git/advanced-conversion.expect \ + .git/advanced-conversion.actual && + ! test_trace2_data fsmonitor \ + history/external-bootstrap-manifest 1 \ + <.git/advanced-conversion.trace && + ! test_trace2_data fsmonitor \ + semantic/nonconversion-attributes 1 \ + <.git/advanced-conversion.trace && + test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <.git/advanced-conversion.trace + fi + ) +' + test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ 'tracked attribute events reopen semantic history' ' test_when_finished "rm -rf tracked-attr-change" && @@ -4486,6 +6938,7 @@ test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ git config core.checkStat minimal && GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ git status --porcelain=v2 >.git/prime.actual && test_must_be_empty .git/prime.actual && diff --git a/t/t7530-status-clean-sidecar.sh b/t/t7530-status-clean-sidecar.sh index d3355ce258c5dc..a2747d84d2b2fe 100755 --- a/t/t7530-status-clean-sidecar.sh +++ b/t/t7530-status-clean-sidecar.sh @@ -55,7 +55,8 @@ prime_semantic_history () { repo=$1 && bulk_status -C "$repo" status --porcelain=2 >actual.1 && test_must_be_empty actual.1 && - bulk_status -C "$repo" status --porcelain=2 >actual.2 && + test_env GIT_INDEX_FILE="$PWD/$repo/.git/index" \ + bulk_status -C "$repo" status --porcelain=2 >actual.2 && test_must_be_empty actual.2 && test_grep FSCF "$repo/.git/index" && rm -f "$repo"/.git/index.csh1.* @@ -289,6 +290,675 @@ test_expect_success DURABLE_FSMONITOR \ test_grep ! "\"label\":\"do_read_index\"" hit.trace ' +test_expect_success DURABLE_FSMONITOR \ + 'ordinary clean status installs its first missing sidecar' ' + test_when_finished "stop_daemon sidecar-plain-first" && + setup_repo sidecar-plain-first && + git -C sidecar-plain-first config core.autocrlf false && + git -C sidecar-plain-first config core.untrackedCache true && + prime_semantic_history sidecar-plain-first && + test_path_is_missing sidecar-plain-first/.git/index.csts && + cp sidecar-plain-first/.git/index plain-first.index && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + -C sidecar-plain-first status >plain-first.expect && + test_env GIT_TRACE2_EVENT="$PWD/plain-first.trace" \ + git -C sidecar-plain-first status >plain-first.actual && + test_cmp plain-first.expect plain-first.actual && + test_cmp plain-first.index sidecar-plain-first/.git/index && + test_path_is_file sidecar-plain-first/.git/index.csts && + test_trace2_data fsmonitor config/coherent 1 \ + sidecar-plain-dirty/tracked && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + -C sidecar-plain-dirty status >plain-dirty-tracked.expect && + test_env GIT_TRACE2_EVENT="$PWD/plain-dirty-tracked.trace" \ + git -C sidecar-plain-dirty status \ + >plain-dirty-tracked.actual && + test_cmp plain-dirty-tracked.expect plain-dirty-tracked.actual && + test_trace2_data status count/changed 1 \ + sidecar-plain-dirty/untracked && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + -C sidecar-plain-dirty status >plain-dirty-untracked.expect && + test_env GIT_TRACE2_EVENT="$PWD/plain-dirty-untracked.trace" \ + git -C sidecar-plain-dirty status \ + >plain-dirty-untracked.actual && + test_cmp plain-dirty-untracked.expect \ + plain-dirty-untracked.actual && + test_trace2_data status count/untracked 1 \ + plain-dirty-recovered.actual && + test_grep "nothing to commit, working tree clean" \ + plain-dirty-recovered.actual && + test_trace2_data status clean-proof/sidecar 1 \ + sidecar-hardlink/.gitignore && + git -C sidecar-hardlink add .gitignore && + git -C sidecar-hardlink commit -qm ignores && + mkdir sidecar-hardlink/ignored && + ln sidecar-hardlink/tracked sidecar-hardlink/ignored/alias && + test-tool -C sidecar-hardlink chmtime -120 tracked .gitignore && + git -C sidecar-hardlink update-index --refresh && + git -C sidecar-hardlink config core.autocrlf false && + git -C sidecar-hardlink config core.untrackedCache true && + git -C sidecar-hardlink config core.trustctime true && + git -C sidecar-hardlink config core.checkStat default && + prime_semantic_history sidecar-hardlink && + test_path_is_missing sidecar-hardlink/.git/index.csts && + test_env GIT_TRACE2_EVENT="$PWD/hardlink-issue.trace" \ + git -C sidecar-hardlink status >hardlink-issue.actual && + test_grep "nothing to commit, working tree clean" \ + hardlink-issue.actual && + test_trace2_data status clean-proof/sidecar 1 \ + sidecar-hardlink/ignored/alias && + test-tool chmtime =$mtime sidecar-hardlink/ignored/alias && + test "$(git -C sidecar-hardlink hash-object tracked)" != \ + "$(git -C sidecar-hardlink rev-parse HEAD:tracked)" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/hardlink-dirty.trace" \ + git -C sidecar-hardlink status --porcelain=v2 \ + >hardlink-dirty.actual && + test_grep "^1 \\.M .* tracked$" hardlink-dirty.actual && + test_grep ! "\"key\":\"clean-proof/hit\"" hardlink-dirty.trace && + test_grep "fast-hardlink-changed" hardlink-dirty.trace +' + +test_expect_success DURABLE_FSMONITOR \ + 'explicitly invalid tracked hardlinks keep authenticated clean proofs' ' + test_when_finished "stop_daemon sidecar-hardlinks-invalid" && + setup_repo sidecar-hardlinks-invalid && + test_write_lines "/ignored/" \ + >sidecar-hardlinks-invalid/.gitignore && + printf "bbbb\\n" >sidecar-hardlinks-invalid/other && + git -C sidecar-hardlinks-invalid add .gitignore other && + git -C sidecar-hardlinks-invalid commit -qm hardlinks && + mkdir sidecar-hardlinks-invalid/ignored && + ln sidecar-hardlinks-invalid/tracked \ + sidecar-hardlinks-invalid/ignored/tracked && + ln sidecar-hardlinks-invalid/other \ + sidecar-hardlinks-invalid/ignored/other && + test-tool -C sidecar-hardlinks-invalid \ + chmtime -120 tracked other .gitignore && + git -C sidecar-hardlinks-invalid update-index --refresh && + git -C sidecar-hardlinks-invalid config core.autocrlf false && + git -C sidecar-hardlinks-invalid config core.untrackedCache true && + git -C sidecar-hardlinks-invalid config core.trustctime true && + git -C sidecar-hardlinks-invalid config core.checkStat default && + prime_semantic_history sidecar-hardlinks-invalid && + test_grep FSCF sidecar-hardlinks-invalid/.git/index && + test_grep FSUC sidecar-hardlinks-invalid/.git/index && + test_env GIT_TRACE2_EVENT="$PWD/hardlinks-invalid-checkpoint.trace" \ + git -C sidecar-hardlinks-invalid status \ + >hardlinks-invalid-checkpoint.actual && + test_grep "nothing to commit, working tree clean" \ + hardlinks-invalid-checkpoint.actual && + test_trace2_data fsmonitor history/external-stored 1 \ + hardlinks-invalid.checkpoints && + test_line_count = 1 hardlinks-invalid.checkpoints && + rm -f sidecar-hardlinks-invalid/.git/index.csts && + rm sidecar-hardlinks-invalid/.git/index && + git -c core.fsmonitor=false -c core.untrackedCache=false \ + -C sidecar-hardlinks-invalid read-tree HEAD && + test_grep ! FSCF sidecar-hardlinks-invalid/.git/index && + test_grep ! FSMN sidecar-hardlinks-invalid/.git/index && + GIT_TRACE2_EVENT="$PWD/hardlinks-invalid-refresh.trace" \ + git -C sidecar-hardlinks-invalid update-index \ + --refresh -- tracked && + test_trace2_data fsmonitor history/external-restored 1 \ + hardlinks-invalid.tree-before && + test_grep ! "^invalid " hardlinks-invalid.tree-before && + GIT_TRACE2_EVENT="$PWD/hardlinks-invalid-update.trace" \ + git -C sidecar-hardlinks-invalid update-index \ + --no-fsmonitor-valid tracked other && + test_grep FSCF sidecar-hardlinks-invalid/.git/index && + test_grep FSUC sidecar-hardlinks-invalid/.git/index && + test-tool -C sidecar-hardlinks-invalid dump-cache-tree \ + >hardlinks-invalid.tree-after && + test_grep ! "^invalid " hardlinks-invalid.tree-after && + test_cmp hardlinks-invalid.tree-before hardlinks-invalid.tree-after && + GIT_TRACE2_EVENT="$PWD/hardlinks-invalid-separated.trace" \ + git -C sidecar-hardlinks-invalid update-index \ + --no-fsmonitor-valid -- tracked other && + test_grep FSCF sidecar-hardlinks-invalid/.git/index && + test_grep FSUC sidecar-hardlinks-invalid/.git/index && + test-tool -C sidecar-hardlinks-invalid dump-cache-tree \ + >hardlinks-invalid.tree-separated && + test_cmp hardlinks-invalid.tree-before \ + hardlinks-invalid.tree-separated && + test_env GIT_TRACE2_EVENT="$PWD/hardlinks-invalid-issue.trace" \ + git -C sidecar-hardlinks-invalid status \ + >hardlinks-invalid-issue.actual && + test_grep "nothing to commit, working tree clean" \ + hardlinks-invalid-issue.actual && + test_trace2_data status clean-proof/sidecar 1 \ + sidecar-hardlinks-invalid/ignored/tracked && + test-tool chmtime =$mtime \ + sidecar-hardlinks-invalid/ignored/tracked && + test "$(git -C sidecar-hardlinks-invalid hash-object tracked)" \ + != "$(git -C sidecar-hardlinks-invalid \ + rev-parse HEAD:tracked)" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/hardlinks-invalid-dirty.trace" \ + git -C sidecar-hardlinks-invalid status --porcelain=v2 \ + >hardlinks-invalid-dirty.actual && + test_grep "^1 \\.M .* tracked$" hardlinks-invalid-dirty.actual && + test_grep "fast-hardlink-changed" hardlinks-invalid-dirty.trace +' + +test_expect_success DURABLE_FSMONITOR \ + 'a racy scoped hardlink refresh still installs a clean sidecar' ' + test_when_finished "stop_daemon sidecar-hardlink-racy" && + setup_repo sidecar-hardlink-racy && + test_write_lines "/ignored/" >sidecar-hardlink-racy/.gitignore && + git -C sidecar-hardlink-racy add .gitignore && + git -C sidecar-hardlink-racy commit -qm ignores && + git -C sidecar-hardlink-racy config core.autocrlf false && + git -C sidecar-hardlink-racy config core.untrackedCache true && + git -C sidecar-hardlink-racy config core.trustctime true && + git -C sidecar-hardlink-racy config core.checkStat default && + prime_semantic_history sidecar-hardlink-racy && + test_grep FSCF sidecar-hardlink-racy/.git/index && + test_grep FSUC sidecar-hardlink-racy/.git/index && + git -C sidecar-hardlink-racy update-index --refresh -- tracked && + test_grep FSCF sidecar-hardlink-racy/.git/index && + test_grep FSUC sidecar-hardlink-racy/.git/index && + test-tool -C sidecar-hardlink-racy dump-fsmonitor \ + >hardlink-racy.token && + hardlink_token=$(sed -n "s/^fsmonitor last update //p" \ + hardlink-racy.token) && + test -n "$hardlink_token" && + mkdir sidecar-hardlink-racy/ignored && + ln sidecar-hardlink-racy/tracked \ + sidecar-hardlink-racy/ignored/tracked && + test-tool -C sidecar-hardlink-racy fsmonitor-client query \ + --token "$hardlink_token" >hardlink-racy-event.out && + test_env GIT_INDEX_FILE="$PWD/sidecar-hardlink-racy/.git/index" \ + GIT_TRACE2_EVENT="$PWD/hardlink-racy-rebaseline.trace" \ + git -C sidecar-hardlink-racy status --porcelain=v2 \ + >hardlink-racy-rebaseline.actual && + test_must_be_empty hardlink-racy-rebaseline.actual && + test_trace2_data fsmonitor apply/global-invalidation 1 \ + hardlink-racy.tree && + test_grep ! "^invalid " hardlink-racy.tree && + test-tool -C sidecar-hardlink-racy chmtime -180 .git/index && + rm -f sidecar-hardlink-racy/.git/index.csts && + test_env GIT_TRACE2_EVENT="$PWD/hardlink-racy-issue.trace" \ + git -C sidecar-hardlink-racy status >hardlink-racy-issue.actual && + test_grep "nothing to commit, working tree clean" \ + hardlink-racy-issue.actual && + test_trace2_data fsmonitor history/external-save-reject racy-index \ + sidecar-hardlink-stale-stat/.gitignore && + printf "aaaa\\n" >sidecar-hardlink-stale-stat/.npmrc && + mkdir -p sidecar-hardlink-stale-stat/nested && + printf "bbbb\\n" \ + >sidecar-hardlink-stale-stat/nested/.node-version && + git -C sidecar-hardlink-stale-stat add \ + .gitignore .npmrc nested/.node-version && + git -C sidecar-hardlink-stale-stat commit -qm hardlinks && + git -C sidecar-hardlink-stale-stat config core.autocrlf false && + git -C sidecar-hardlink-stale-stat config core.untrackedCache true && + git -C sidecar-hardlink-stale-stat config core.trustctime true && + git -C sidecar-hardlink-stale-stat config core.checkStat default && + git -C sidecar-hardlink-stale-stat update-index \ + --index-version 2 && + prime_semantic_history sidecar-hardlink-stale-stat && + test_grep FSCF sidecar-hardlink-stale-stat/.git/index && + test_grep FSUC sidecar-hardlink-stale-stat/.git/index && + test-tool -C sidecar-hardlink-stale-stat dump-fsmonitor \ + >hardlink-stale-stat.token && + stale_token=$(sed -n "s/^fsmonitor last update //p" \ + hardlink-stale-stat.token) && + test -n "$stale_token" && + mkdir sidecar-hardlink-stale-stat/ignored && + stale_same_second= && + for stale_attempt in 1 2 3 4 5 + do + rm -f sidecar-hardlink-stale-stat/ignored/npmrc \ + sidecar-hardlink-stale-stat/ignored/node-version && + test-tool -C sidecar-hardlink-stale-stat \ + chmtime -1 .npmrc nested/.node-version && + git -C sidecar-hardlink-stale-stat update-index \ + --refresh -- .npmrc && + npmrc_second=$(/usr/bin/stat -f %c \ + sidecar-hardlink-stale-stat/.npmrc) && + node_second=$(/usr/bin/stat -f %c \ + sidecar-hardlink-stale-stat/nested/.node-version) && + ln sidecar-hardlink-stale-stat/.npmrc \ + sidecar-hardlink-stale-stat/ignored/npmrc && + ln sidecar-hardlink-stale-stat/nested/.node-version \ + sidecar-hardlink-stale-stat/ignored/node-version || + return 1 + if test "$npmrc_second" = "$(/usr/bin/stat -f %c \ + sidecar-hardlink-stale-stat/.npmrc)" && + test "$node_second" = "$(/usr/bin/stat -f %c \ + sidecar-hardlink-stale-stat/nested/.node-version)" + then + stale_same_second=1 && + break + fi + done && + test "$stale_same_second" = 1 && + test-tool -C sidecar-hardlink-stale-stat fsmonitor-client query \ + --token "$stale_token" >hardlink-stale-stat-event.out && + test_env GIT_INDEX_FILE="$PWD/sidecar-hardlink-stale-stat/.git/index" \ + GIT_TRACE2_EVENT="$PWD/hardlink-stale-stat-rebaseline.trace" \ + git -C sidecar-hardlink-stale-stat status --porcelain=v2 \ + >hardlink-stale-stat-rebaseline.actual && + test_must_be_empty hardlink-stale-stat-rebaseline.actual && + test_trace2_data fsmonitor apply/global-invalidation 1 \ + sidecar-hardlink-stale-stat/.git/stale-index-stat.pl <<-\EOF && + use Digest::SHA qw(sha1 sha256); + binmode STDIN; + binmode STDOUT; + local $/; + my $index = ; + my $algorithm = $ARGV[0]; + my $rawsz = $algorithm eq "sha256" ? 32 : 20; + my $payload = substr($index, 0, -$rawsz); + die "not a version 2 index\n" + unless substr($payload, 0, 4) eq "DIRC" && + unpack("N", substr($payload, 4, 4)) == 2; + my $entries = unpack("N", substr($payload, 8, 4)); + my $offset = 12; + my $changed = 0; + for (1 .. $entries) { + my $name_offset = $offset + 40 + $rawsz + 2; + my $end = index($payload, "\0", $name_offset); + die "unterminated index entry\n" if $end < 0; + my $name = substr($payload, $name_offset, $end - $name_offset); + if ($name eq ".npmrc" || $name eq "nested/.node-version") { + my $nsec = unpack("N", substr($payload, $offset + 4, 4)); + $nsec = ($nsec + 1) % 1000000000; + substr($payload, $offset + 4, 4, pack("N", $nsec)); + $changed++; + } + $offset += (($end + 1 - $offset + 7) & ~7); + } + die "did not rewrite both indexed hardlinks\n" unless $changed == 2; + print $payload, + $algorithm eq "sha256" ? sha256($payload) : sha1($payload); + EOF + perl sidecar-hardlink-stale-stat/.git/stale-index-stat.pl \ + "$(test_oid algo)" \ + sidecar-hardlink-stale-stat/.git/index.stale && + mv sidecar-hardlink-stale-stat/.git/index.stale \ + sidecar-hardlink-stale-stat/.git/index && + test_grep FSCF sidecar-hardlink-stale-stat/.git/index && + test_grep FSUC sidecar-hardlink-stale-stat/.git/index && + rm -f sidecar-hardlink-stale-stat/.git/index.csts && + test_env GIT_TRACE2_EVENT="$PWD/hardlink-stale-stat-issue.trace" \ + git -C sidecar-hardlink-stale-stat status \ + >hardlink-stale-stat-issue.actual && + test_grep "nothing to commit, working tree clean" \ + hardlink-stale-stat-issue.actual && + test_trace2_data status clean-proof/hardlink-content-verified 2 \ + >sidecar-hardlink-stale-stat/.git/index.csts + ;; + truncated) + printf "CSTS" \ + >sidecar-hardlink-stale-stat/.git/index.csts + ;; + oversized) + dd if=/dev/zero \ + of=sidecar-hardlink-stale-stat/.git/index.csts \ + bs=1048577 count=1 2>/dev/null + ;; + esac && + test_env \ + GIT_TRACE2_EVENT="$PWD/hardlink-sidecar-$malformed.trace" \ + git -C sidecar-hardlink-stale-stat status \ + >"hardlink-sidecar-$malformed.actual" && + test_grep "nothing to commit, working tree clean" \ + "hardlink-sidecar-$malformed.actual" && + test_grep "fast-sidecar-missing-or-corrupt" \ + "hardlink-sidecar-$malformed.trace" && + test_trace2_data status clean-proof/hardlink-content-verified 2 \ + <"hardlink-sidecar-$malformed.trace" && + test_trace2_data status clean-proof/hardlink-witnesses 2 \ + <"hardlink-sidecar-$malformed.trace" && + test_trace2_data status clean-proof/sidecar 1 \ + <"hardlink-sidecar-$malformed.trace" && + assert_clean_sidecar_hit sidecar-hardlink-stale-stat \ + sidecar-hardlink-stale-stat \ + "hardlink-sidecar-$malformed-hit" && + test_trace2_data status clean-proof/hardlink-validated 2 \ + <"hardlink-sidecar-$malformed-hit.trace" || return 1 + done && + cp sidecar-hardlink-stale-stat/.git/index.csts.valid \ + sidecar-hardlink-stale-stat/.git/index.csts.pristine && + for unsafe in fifo symlink directory multilink + do + rm -rf sidecar-hardlink-stale-stat/.git/index.csts && + case "$unsafe" in + fifo) + mkfifo sidecar-hardlink-stale-stat/.git/index.csts + ;; + symlink) + ln -s index.csts.valid \ + sidecar-hardlink-stale-stat/.git/index.csts + ;; + directory) + mkdir sidecar-hardlink-stale-stat/.git/index.csts + ;; + multilink) + ln sidecar-hardlink-stale-stat/.git/index.csts.valid \ + sidecar-hardlink-stale-stat/.git/index.csts + ;; + esac && + test_env \ + GIT_TRACE2_EVENT="$PWD/hardlink-sidecar-$unsafe.trace" \ + git -C sidecar-hardlink-stale-stat status \ + >"hardlink-sidecar-$unsafe.actual" && + test_grep "nothing to commit, working tree clean" \ + "hardlink-sidecar-$unsafe.actual" && + ! test_trace2_data status clean-proof/sidecar 1 \ + <"hardlink-sidecar-$unsafe.trace" && + test_cmp sidecar-hardlink-stale-stat/.git/index.csts.pristine \ + sidecar-hardlink-stale-stat/.git/index.csts.valid && + case "$unsafe" in + fifo) + test -p sidecar-hardlink-stale-stat/.git/index.csts + ;; + symlink) + test -h sidecar-hardlink-stale-stat/.git/index.csts + ;; + directory) + test -d sidecar-hardlink-stale-stat/.git/index.csts + ;; + multilink) + test "$(/usr/bin/stat -f %i \ + sidecar-hardlink-stale-stat/.git/index.csts)" = \ + "$(/usr/bin/stat -f %i \ + sidecar-hardlink-stale-stat/.git/index.csts.valid)" + ;; + esac || return 1 + done && + rm -f sidecar-hardlink-stale-stat/.git/index.csts && + cp sidecar-hardlink-stale-stat/.git/index.csts.valid \ + sidecar-hardlink-stale-stat/.git/index.csts && + git -C sidecar-hardlink-stale-stat status \ + >hardlink-sidecar-repair-prime.actual && + test_grep "nothing to commit, working tree clean" \ + hardlink-sidecar-repair-prime.actual && + assert_clean_sidecar_hit sidecar-hardlink-stale-stat \ + sidecar-hardlink-stale-stat hardlink-sidecar-repair-prime-hit && + cp sidecar-hardlink-stale-stat/.git/index \ + sidecar-hardlink-stale-stat/.git/index.before-repair && + test-tool -C sidecar-hardlink-stale-stat chmtime -60 tracked && + chmod 0600 sidecar-hardlink-stale-stat/ignored/npmrc \ + sidecar-hardlink-stale-stat/ignored/node-version && + chmod 0644 sidecar-hardlink-stale-stat/ignored/npmrc \ + sidecar-hardlink-stale-stat/ignored/node-version && + test_env \ + GIT_TRACE2_EVENT="$PWD/hardlink-sidecar-repair-reissue.trace" \ + git -C sidecar-hardlink-stale-stat status \ + >hardlink-sidecar-repair-reissue.actual && + test_grep "nothing to commit, working tree clean" \ + hardlink-sidecar-repair-reissue.actual && + test_grep "fast-hardlink-changed" \ + hardlink-sidecar-repair-reissue.trace && + test_grep "\"label\":\"do_write_index\"" \ + hardlink-sidecar-repair-reissue.trace && + test_trace2_data status clean-proof/sidecar 1 \ + hook-created + printf "ran\n" >.git/post-index-change-ran + EOF + test-tool -C sidecar-hardlink-stale-stat chmtime -120 tracked && + chmod 0600 sidecar-hardlink-stale-stat/ignored/npmrc \ + sidecar-hardlink-stale-stat/ignored/node-version && + chmod 0644 sidecar-hardlink-stale-stat/ignored/npmrc \ + sidecar-hardlink-stale-stat/ignored/node-version && + test_env \ + GIT_TRACE2_EVENT="$PWD/hardlink-sidecar-post-hook.trace" \ + git -C sidecar-hardlink-stale-stat status \ + >hardlink-sidecar-post-hook.actual && + test_path_is_file \ + sidecar-hardlink-stale-stat/.git/post-index-change-ran && + test_path_is_file sidecar-hardlink-stale-stat/hook-created && + test_grep "fast-hardlink-changed" \ + hardlink-sidecar-post-hook.trace && + test_grep "\"label\":\"do_write_index\"" \ + hardlink-sidecar-post-hook.trace && + ! test_trace2_data status clean-proof/postwrite-reissued 1 \ + hardlink-sidecar-post-hook.expect && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/hardlink-sidecar-post-hook-repeat.trace" \ + git -C sidecar-hardlink-stale-stat status --porcelain=v2 \ + >hardlink-sidecar-post-hook-repeat.actual && + test_cmp hardlink-sidecar-post-hook.expect \ + hardlink-sidecar-post-hook-repeat.actual && + test_grep "^? hook-created$" \ + hardlink-sidecar-post-hook-repeat.actual && + ! test_trace2_data status clean-proof/hit 1 \ + sidecar-hardlink-stale-stat/ignored/npmrc && + test-tool chmtime =$mtime \ + sidecar-hardlink-stale-stat/ignored/npmrc && + test "$(git -C sidecar-hardlink-stale-stat hash-object .npmrc)" \ + != "$(git -C sidecar-hardlink-stale-stat \ + rev-parse HEAD:.npmrc)" && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TRACE2_EVENT="$PWD/hardlink-stale-stat-dirty.trace" \ + git -C sidecar-hardlink-stale-stat status --porcelain=v2 \ + >hardlink-stale-stat-dirty.actual && + test_grep "^1 \\.M .* \\.npmrc$" \ + hardlink-stale-stat-dirty.actual && + test_path_is_missing sidecar-hardlink-stale-stat/.git/index.csts && + ! test_trace2_data status clean-proof/sidecar 1 \ + sidecar-hardlink-first-dirty/.gitignore && + git -C sidecar-hardlink-first-dirty add .gitignore && + git -C sidecar-hardlink-first-dirty commit -qm ignores && + mkdir sidecar-hardlink-first-dirty/ignored && + ln sidecar-hardlink-first-dirty/tracked \ + sidecar-hardlink-first-dirty/ignored/alias && + test-tool -C sidecar-hardlink-first-dirty \ + chmtime -120 tracked .gitignore && + git -C sidecar-hardlink-first-dirty update-index --refresh && + git -C sidecar-hardlink-first-dirty config core.autocrlf false && + git -C sidecar-hardlink-first-dirty config core.untrackedCache true && + git -C sidecar-hardlink-first-dirty config core.trustctime true && + git -C sidecar-hardlink-first-dirty config core.checkStat default && + prime_semantic_history sidecar-hardlink-first-dirty && + test_path_is_missing sidecar-hardlink-first-dirty/.git/index.csts && + mtime=$(test-tool chmtime --get \ + sidecar-hardlink-first-dirty/tracked) && + printf "xxxx\\n" \ + >sidecar-hardlink-first-dirty/ignored/alias && + test-tool chmtime =$mtime \ + sidecar-hardlink-first-dirty/ignored/alias && + test "$(git -C sidecar-hardlink-first-dirty hash-object tracked)" \ + != "$(git -C sidecar-hardlink-first-dirty \ + rev-parse HEAD:tracked)" && + test_env GIT_TRACE2_EVENT="$PWD/hardlink-first-dirty.trace" \ + git -C sidecar-hardlink-first-dirty status \ + >hardlink-first-dirty.actual && + test_path_is_missing sidecar-hardlink-first-dirty/.git/index.csts && + test_grep ! "\"key\":\"clean-proof/sidecar\"" \ + hardlink-first-dirty.trace +' + +test_expect_success DURABLE_FSMONITOR \ + 'weak stat identity never certifies multiply linked tracked files' ' + test_when_finished "stop_daemon sidecar-hardlink-weak" && + setup_repo sidecar-hardlink-weak && + test_write_lines "/ignored/" >sidecar-hardlink-weak/.gitignore && + git -C sidecar-hardlink-weak add .gitignore && + git -C sidecar-hardlink-weak commit -qm ignores && + mkdir sidecar-hardlink-weak/ignored && + ln sidecar-hardlink-weak/tracked \ + sidecar-hardlink-weak/ignored/alias && + test-tool chmtime -120 sidecar-hardlink-weak/tracked && + git -C sidecar-hardlink-weak update-index --refresh && + git -C sidecar-hardlink-weak config core.autocrlf false && + git -C sidecar-hardlink-weak config core.untrackedCache true && + git -C sidecar-hardlink-weak config core.trustctime false && + git -C sidecar-hardlink-weak config core.checkStat minimal && + prime_semantic_history sidecar-hardlink-weak && + test_env GIT_TRACE2_EVENT="$PWD/hardlink-weak.trace" \ + git -C sidecar-hardlink-weak status >hardlink-weak.actual && + test_grep "nothing to commit, working tree clean" \ + hardlink-weak.actual && + test_path_is_missing sidecar-hardlink-weak/.git/index.csts && + test_grep ! "\"key\":\"clean-proof/sidecar\"" hardlink-weak.trace +' + test_expect_success DURABLE_FSMONITOR \ 'a clean sidecar serves every index-independent status shape' ' shapes=sidecar-query-shapes && @@ -862,6 +1532,9 @@ test_expect_success DURABLE_FSMONITOR \ test_when_finished "stop_daemon external-stat-bootstrap" && setup_repo external-stat-bootstrap && git -C external-stat-bootstrap update-index --fsmonitor && + test-tool chmtime -60 external-stat-bootstrap/tracked && + test-tool -C external-stat-bootstrap \ + fsmonitor-client flush >bootstrap.flush && test_env GIT_TRACE2_EVENT="$PWD/external-stat-bootstrap.trace" \ git -C external-stat-bootstrap status >actual && test_trace2_data fsmonitor history/external-stored 1 \ @@ -884,7 +1557,7 @@ test_expect_success DURABLE_FSMONITOR \ bulk_status -C external-stat-exact status --porcelain=v2 \ >actual && test_must_be_empty actual && - ! test_trace2_data fsmonitor history/external-stored 1 \ + test_trace2_data fsmonitor history/external-stored 1 \ external-sidecars && diff --git a/t/unit-tests/u-attr-manifest.c b/t/unit-tests/u-attr-manifest.c index a41eb2a7975fac..33e1b048316be1 100644 --- a/t/unit-tests/u-attr-manifest.c +++ b/t/unit-tests/u-attr-manifest.c @@ -181,6 +181,32 @@ void test_attr_manifest__does_not_report_identical_entries(void) strbuf_release(&old); } +void test_attr_manifest__distinguishes_display_only_attribute_edits(void) +{ + static const char original[] = "*.txt text\n# keep me\n"; + static const char display[] = + "*.txt text\n# keep me\n*.gen linguist-generated\n"; + static const char removed[] = + "*.txt text\n*.old -linguist-generated\n# keep me\n"; + static const char converted[] = + "*.txt text\n# keep me\n*.gen linguist-generated text\n"; + static const char filtered[] = + "*.txt text\n# keep me\n*.gen filter=smudge\n"; + static const char macro[] = + "[attr]linguist-generated text\n*.gen linguist-generated\n"; + + cl_assert(attr_manifest_only_linguist_generated_changed( + original, strlen(original), display, strlen(display))); + cl_assert(attr_manifest_only_linguist_generated_changed( + removed, strlen(removed), original, strlen(original))); + cl_assert(!attr_manifest_only_linguist_generated_changed( + original, strlen(original), converted, strlen(converted))); + cl_assert(!attr_manifest_only_linguist_generated_changed( + original, strlen(original), filtered, strlen(filtered))); + cl_assert(!attr_manifest_only_linguist_generated_changed( + original, strlen(original), macro, strlen(macro))); +} + void test_attr_manifest__rejects_malformed_tail_before_callbacks(void) { const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA1]; diff --git a/t/unit-tests/u-clean-status-config.c b/t/unit-tests/u-clean-status-config.c index 582875fa2bf54d..7b96ae955b2c1a 100644 --- a/t/unit-tests/u-clean-status-config.c +++ b/t/unit-tests/u-clean-status-config.c @@ -67,6 +67,85 @@ void test_clean_status_config__origin_only_affects_full_hash(void) cl_assert(hashes_equal(global.semantic_hash, local.semantic_hash)); } +void test_clean_status_config__command_transport_config_does_not_change_proof(void) +{ + static const char *const ignored_keys[] = { + "credential.helper", + "credential.https://Example/Team.helper", + "url.https://Proxy.Example/Team/.insteadof", + "url.https://Proxy.Example/Team/.pushinsteadof", + }; + static const enum config_scope persistent_scopes[] = { + CONFIG_SCOPE_GLOBAL, + CONFIG_SCOPE_LOCAL, + CONFIG_SCOPE_WORKTREE, + CONFIG_SCOPE_UNKNOWN, + }; + struct key_value_info kvi = KVI_INIT; + struct config_context ctx = { .kvi = &kvi }; + struct clean_status_config_digest baseline, digest; + + clean_status_config_init(&baseline, &hash_algos[GIT_HASH_SHA1]); + clean_status_config_final(&baseline); + kvi.scope = CONFIG_SCOPE_COMMAND; + kvi.origin_type = CONFIG_ORIGIN_CMDLINE; + + for (size_t i = 0; i < ARRAY_SIZE(ignored_keys); i++) { + digest_one(&digest, ignored_keys[i], "transport", &ctx); + cl_assert(hashes_equal(digest.hash, baseline.hash)); + cl_assert(hashes_equal(digest.semantic_hash, + baseline.semantic_hash)); + cl_assert(!digest.filter_configured); + cl_assert(!digest.semantic_config_explicit); + + for (size_t j = 0; j < ARRAY_SIZE(persistent_scopes); j++) { + kvi.scope = persistent_scopes[j]; + digest_one(&digest, ignored_keys[i], "transport", &ctx); + cl_assert(!hashes_equal(digest.hash, baseline.hash)); + } + kvi.scope = CONFIG_SCOPE_COMMAND; + digest_one(&digest, ignored_keys[i], "transport", NULL); + cl_assert(!hashes_equal(digest.hash, baseline.hash)); + } +} + +void test_clean_status_config__command_worktree_config_still_changes_proof(void) +{ + static const char *const retained_keys[] = { + "url.insteadof", + "url.https://Proxy.Example/Team/.other", + "core.excludesfile", + "status.showuntrackedfiles", + }; + struct key_value_info kvi = KVI_INIT; + struct config_context ctx = { .kvi = &kvi }; + struct clean_status_config_digest baseline, digest; + + clean_status_config_init(&baseline, &hash_algos[GIT_HASH_SHA1]); + clean_status_config_final(&baseline); + kvi.scope = CONFIG_SCOPE_COMMAND; + kvi.origin_type = CONFIG_ORIGIN_CMDLINE; + + for (size_t i = 0; i < ARRAY_SIZE(retained_keys); i++) { + digest_one(&digest, retained_keys[i], "value", &ctx); + cl_assert(!hashes_equal(digest.hash, baseline.hash)); + cl_assert(hashes_equal(digest.semantic_hash, + baseline.semantic_hash)); + } + + digest_one(&digest, "core.autocrlf", "true", &ctx); + cl_assert(!hashes_equal(digest.hash, baseline.hash)); + cl_assert(!hashes_equal(digest.semantic_hash, baseline.semantic_hash)); + cl_assert(digest.semantic_config_explicit); + cl_assert(!digest.filter_configured); + + digest_one(&digest, "filter.demo.clean", "cat", &ctx); + cl_assert(!hashes_equal(digest.hash, baseline.hash)); + cl_assert(!hashes_equal(digest.semantic_hash, baseline.semantic_hash)); + cl_assert(digest.semantic_config_explicit); + cl_assert(digest.filter_configured); +} + static void digest_without_final_domain( const struct clean_status_config_digest *digest, unsigned char *full_hash, unsigned char *semantic_hash) diff --git a/t/unit-tests/u-clean-status-sidecar.c b/t/unit-tests/u-clean-status-sidecar.c index a4d819eb21290a..e5a974a11d4e2f 100644 --- a/t/unit-tests/u-clean-status-sidecar.c +++ b/t/unit-tests/u-clean-status-sidecar.c @@ -113,6 +113,8 @@ static void assert_round_trip(const struct git_hash_algo *algo) fixture_encode(&fixture, algo); cl_assert_equal_i(clean_status_sidecar_parse( &parsed, fixture.encoded.buf, fixture.encoded.len, algo), 0); + cl_assert_equal_i(get_be32(fixture.encoded.buf + 4), + CLEAN_STATUS_SIDECAR_VERSION); cl_assert(clean_status_identity_equal(&parsed.identity, &fixture.sidecar.identity)); cl_assert_equal_i(parsed.proof.index_version, @@ -132,6 +134,7 @@ static void assert_round_trip(const struct git_hash_algo *algo) cl_assert_equal_i(parsed.token_len, fixture.sidecar.token_len); cl_assert(!memcmp(parsed.token, fixture.sidecar.token, parsed.token_len)); + cl_assert_equal_i(parsed.hardlink_nr, 0); fixture_release(&fixture); } @@ -141,6 +144,124 @@ void test_clean_status_sidecar__round_trips_both_object_formats(void) assert_round_trip(&hash_algos[GIT_HASH_SHA256]); } +static void assert_hardlink_round_trip(const struct git_hash_algo *algo) +{ + struct sidecar_fixture fixture; + struct clean_status_sidecar parsed; + struct path_stat_identity expected = { 0 }, actual; + struct strbuf witnesses = STRBUF_INIT; + const unsigned char *cursor, *path; + size_t path_len; + + fixture_init(&fixture, algo); + expected.fields[0] = 11; + expected.fields[1] = 12; + expected.fields[2] = S_IFREG | 0644; + expected.fields[3] = 2; + expected.fields[10] = 123456789; + cl_assert_equal_i(clean_status_sidecar_append_hardlink( + &witnesses, "tracked/file", &expected), 0); + fixture.sidecar.hardlinks = (unsigned char *)witnesses.buf; + fixture.sidecar.hardlinks_len = witnesses.len; + fixture.sidecar.hardlink_nr = 1; + fixture_encode(&fixture, algo); + cl_assert_equal_i(get_be32(fixture.encoded.buf + 4), + CLEAN_STATUS_SIDECAR_HARDLINK_VERSION); + cl_assert_equal_i(clean_status_sidecar_parse( + &parsed, fixture.encoded.buf, fixture.encoded.len, algo), 0); + cl_assert_equal_i(parsed.hardlink_nr, 1); + cursor = parsed.hardlinks; + cl_assert_equal_i(clean_status_sidecar_next_hardlink( + &cursor, parsed.hardlinks + parsed.hardlinks_len, + &path, &path_len, &actual), 0); + cl_assert_equal_i(path_len, strlen("tracked/file")); + cl_assert(!memcmp(path, "tracked/file", path_len)); + cl_assert(path_stat_identity_equal(&expected, &actual)); + cl_assert(cursor == parsed.hardlinks + parsed.hardlinks_len); + strbuf_release(&witnesses); + fixture_release(&fixture); +} + +void test_clean_status_sidecar__round_trips_hardlinks_in_both_formats(void) +{ + assert_hardlink_round_trip(&hash_algos[GIT_HASH_SHA1]); + assert_hardlink_round_trip(&hash_algos[GIT_HASH_SHA256]); +} + +void test_clean_status_sidecar__accepts_hardlink_payloads_over_old_limit(void) +{ + const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA1]; + struct sidecar_fixture fixture; + struct clean_status_sidecar parsed; + struct path_stat_identity identity = { 0 }; + struct strbuf witnesses = STRBUF_INIT; + char path[32]; + + fixture_init(&fixture, algo); + identity.fields[2] = S_IFREG | 0644; + identity.fields[3] = 2; + for (uint32_t i = 0; i < 80; i++) { + xsnprintf(path, sizeof(path), "tracked/%04"PRIu32, i); + cl_assert_equal_i(clean_status_sidecar_append_hardlink( + &witnesses, path, &identity), 0); + } + fixture.sidecar.hardlinks = (unsigned char *)witnesses.buf; + fixture.sidecar.hardlinks_len = witnesses.len; + fixture.sidecar.hardlink_nr = 80; + fixture_encode(&fixture, algo); + cl_assert(fixture.encoded.len > 8192); + cl_assert_equal_i(clean_status_sidecar_parse( + &parsed, fixture.encoded.buf, fixture.encoded.len, algo), 0); + cl_assert_equal_i(parsed.hardlink_nr, 80); + strbuf_release(&witnesses); + fixture_release(&fixture); +} + +void test_clean_status_sidecar__rejects_invalid_hardlink_witnesses(void) +{ + const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA1]; + struct sidecar_fixture fixture; + struct path_stat_identity identity = { 0 }; + struct strbuf witnesses = STRBUF_INIT; + size_t count_offset, first_path, second_path; + + fixture_init(&fixture, algo); + identity.fields[2] = S_IFREG | 0644; + identity.fields[3] = 2; + cl_assert_equal_i(clean_status_sidecar_append_hardlink( + &witnesses, "a/file", &identity), 0); + cl_assert_equal_i(clean_status_sidecar_append_hardlink( + &witnesses, "z/file", &identity), 0); + fixture.sidecar.hardlinks = (unsigned char *)witnesses.buf; + fixture.sidecar.hardlinks_len = witnesses.len; + fixture.sidecar.hardlink_nr = 2; + fixture_encode(&fixture, algo); + count_offset = token_offset(algo) + fixture.sidecar.token_len; + first_path = count_offset + 2 * sizeof(uint32_t); + second_path = first_path + strlen("a/file") + + CLEAN_STATUS_IDENTITY_SIZE + sizeof(uint32_t); + + put_be32(fixture.encoded.buf + count_offset, 0); + assert_parse_fails(&fixture, algo); + put_be32(fixture.encoded.buf + count_offset, + CLEAN_STATUS_HARDLINK_WITNESS_MAX + 1); + assert_parse_fails(&fixture, algo); + put_be32(fixture.encoded.buf + count_offset, 2); + + fixture.encoded.buf[first_path] = '/'; + assert_parse_fails(&fixture, algo); + fixture.encoded.buf[first_path] = 'a'; + fixture.encoded.buf[second_path] = 'a'; + assert_parse_fails(&fixture, algo); + fixture.encoded.buf[second_path] = 'z'; + + put_be64(fixture.encoded.buf + first_path + strlen("a/file") + + 3 * sizeof(uint64_t), 1); + assert_parse_fails(&fixture, algo); + strbuf_release(&witnesses); + fixture_release(&fixture); +} + void test_clean_status_sidecar__rejects_bad_envelopes(void) { const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA1]; diff --git a/t/unit-tests/u-clean-status-store.c b/t/unit-tests/u-clean-status-store.c index 4c2e5739aaceea..9d2cbc2daf4b7a 100644 --- a/t/unit-tests/u-clean-status-store.c +++ b/t/unit-tests/u-clean-status-store.c @@ -187,7 +187,7 @@ void test_clean_status_store__rejects_oversized_sidecars(void) fixture_init(&fixture, algo); path = sidecar_path(&fixture); - strbuf_addchars(&oversized, 'x', 8193); + strbuf_addchars(&oversized, 'x', CLEAN_STATUS_SIDECAR_MAX_SIZE + 1); write_file_buf(path.buf, oversized.buf, oversized.len); cl_assert_equal_i(clean_status_sidecar_load( fixture.index_path.buf, algo, &record), -1); diff --git a/t/unit-tests/u-exclude-source-proof.c b/t/unit-tests/u-exclude-source-proof.c index be1f2d046599b1..4ac7690f2bf286 100644 --- a/t/unit-tests/u-exclude-source-proof.c +++ b/t/unit-tests/u-exclude-source-proof.c @@ -367,11 +367,16 @@ void test_exclude_source_proof__reresolves_absent_source_parent(void) void test_exclude_source_proof__accepts_dev_null(void) { - struct exclude_source_proof *proof = new_proof(); + int valid = 0; - record_file(proof, "/dev/null"); - cl_assert(exclude_source_proof_validate(proof)); - exclude_source_proof_release(proof); + for (int attempt = 0; attempt < 16 && !valid; attempt++) { + struct exclude_source_proof *proof = new_proof(); + + record_file(proof, "/dev/null"); + valid = exclude_source_proof_validate(proof); + exclude_source_proof_release(proof); + } + cl_assert(valid); } void test_exclude_source_proof__accepts_empty_fifo_replacement(void) diff --git a/t/unit-tests/u-fsmonitor-clean-proof.c b/t/unit-tests/u-fsmonitor-clean-proof.c index b4691221c75f49..037f8d7f57d1cb 100644 --- a/t/unit-tests/u-fsmonitor-clean-proof.c +++ b/t/unit-tests/u-fsmonitor-clean-proof.c @@ -9,6 +9,7 @@ struct proof_fixture { unsigned char config_hash[GIT_MAX_RAWSZ]; unsigned char semantic_hash[GIT_MAX_RAWSZ]; unsigned char attr_hash[GIT_MAX_RAWSZ]; + unsigned char tracked_policy_hash[GIT_MAX_RAWSZ]; struct fsmonitor_clean_proof proof; }; @@ -26,6 +27,7 @@ static void fixture_init(struct proof_fixture *fixture, memset(fixture->config_hash, 2, algo->rawsz); memset(fixture->semantic_hash, 3, algo->rawsz); memset(fixture->attr_hash, 4, algo->rawsz); + memset(fixture->tracked_policy_hash, 5, algo->rawsz); attr_manifest_writer_init(&writer, &fixture->manifest, algo); cl_assert_equal_i(attr_manifest_writer_add( &writer, ".gitattributes", ATTR_MANIFEST_INDEX, hash), 0); @@ -56,6 +58,9 @@ static void assert_round_trip(const struct git_hash_algo *algo) &fixture.encoded, &fixture.proof, algo), 0); cl_assert_equal_i(fsmonitor_clean_proof_parse( &parsed, fixture.encoded.buf, fixture.encoded.len, algo), 0); + cl_assert_equal_i(parsed.version, + FSMONITOR_CLEAN_PROOF_VERSION_LEGACY); + cl_assert_equal_p(parsed.tracked_policy_hash, NULL); cl_assert_equal_i(parsed.flags, fixture.proof.flags); cl_assert_equal_i(parsed.token_len, fixture.proof.token_len); cl_assert(!memcmp(parsed.token, fixture.proof.token, parsed.token_len)); @@ -78,6 +83,7 @@ static void assert_rejected(struct fsmonitor_clean_proof *parsed, cl_assert_equal_p(parsed->config_hash, NULL); cl_assert_equal_p(parsed->semantic_hash, NULL); cl_assert_equal_p(parsed->attr_hash, NULL); + cl_assert_equal_p(parsed->tracked_policy_hash, NULL); cl_assert_equal_p(parsed->attr_manifest, NULL); cl_assert_equal_i(parsed->attr_manifest_len, 0); } @@ -88,6 +94,52 @@ void test_fsmonitor_clean_proof__round_trips_both_object_formats(void) assert_round_trip(&hash_algos[GIT_HASH_SHA256]); } +static void assert_tracked_policy_round_trip( + const struct git_hash_algo *algo) +{ + struct proof_fixture fixture; + struct fsmonitor_clean_proof parsed; + struct strbuf unbound = STRBUF_INIT; + size_t policy_offset; + unsigned char saved; + + fixture_init(&fixture, algo); + fixture.proof.tracked_policy_hash = fixture.tracked_policy_hash; + cl_assert_equal_i(fsmonitor_clean_proof_write( + &fixture.encoded, &fixture.proof, algo), 0); + cl_assert_equal_i(fsmonitor_clean_proof_parse( + &parsed, fixture.encoded.buf, fixture.encoded.len, algo), 0); + cl_assert_equal_i(parsed.version, FSMONITOR_CLEAN_PROOF_VERSION); + cl_assert(!memcmp(parsed.tracked_policy_hash, + fixture.tracked_policy_hash, algo->rawsz)); + cl_assert_equal_i(fsmonitor_clean_proof_copy_without_bindings( + &unbound, fixture.encoded.buf, fixture.encoded.len, algo), 0); + cl_assert_equal_i(fsmonitor_clean_proof_parse( + &parsed, unbound.buf, unbound.len, algo), 0); + cl_assert_equal_i(parsed.version, FSMONITOR_CLEAN_PROOF_VERSION); + cl_assert(!memcmp(parsed.tracked_policy_hash, + fixture.tracked_policy_hash, algo->rawsz)); + cl_assert_equal_i(parsed.flags, + FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX); + policy_offset = 5 * sizeof(uint32_t) + fixture.proof.token_len + + 3 * algo->rawsz; + saved = fixture.encoded.buf[policy_offset]; + fixture.encoded.buf[policy_offset] ^= 1; + assert_rejected(&parsed, &fixture.encoded, algo); + fixture.encoded.buf[policy_offset] = saved; + fixture.encoded.len--; + assert_rejected(&parsed, &fixture.encoded, algo); + strbuf_release(&unbound); + fixture_release(&fixture); +} + +void test_fsmonitor_clean_proof__binds_tracked_policy_in_both_formats(void) +{ + assert_tracked_policy_round_trip(&hash_algos[GIT_HASH_SHA1]); + assert_tracked_policy_round_trip(&hash_algos[GIT_HASH_SHA256]); +} + void test_fsmonitor_clean_proof__rejects_corrupt_records(void) { const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA1]; diff --git a/wt-status.c b/wt-status.c index 9c6d060d9cf18b..66cb8a17cd2cca 100644 --- a/wt-status.c +++ b/wt-status.c @@ -559,9 +559,16 @@ static struct cache_entry **wt_status_collect_preload_changes( struct wt_status_change_data *d; unsigned char state = istate->preload_bulk_tracked_state[i]; + unsigned int worktree_mode = 0; int status; if (state == PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED) { + struct stat st; + + if (lstat(ce->name, &st)) + continue; + worktree_mode = ce_mode_from_stat( + s->repo, ce, st.st_mode); status = DIFF_STATUS_MODIFIED; modified++; } else if (state == PRELOAD_BULK_TRACKED_DEFINITIVE_DELETED) { @@ -575,8 +582,7 @@ static struct cache_entry **wt_status_collect_preload_changes( if (!d->worktree_status) d->worktree_status = status; d->mode_index = ce->ce_mode; - d->mode_worktree = status == DIFF_STATUS_MODIFIED ? - ce->ce_mode : 0; + d->mode_worktree = worktree_mode; oidcpy(&d->oid_index, &ce->oid); ce_mark_uptodate(ce); ALLOC_GROW(direct, *direct_nr + 1, direct_alloc); @@ -1102,6 +1108,7 @@ void wt_status_start_untracked_cache_preload(struct wt_status *s) unsigned int dir_flags; int has_fsmonitor = fsm_settings__get_mode(s->repo) > FSMONITOR_MODE_DISABLED; + int reopened_valid_token = 0; if (s->untracked_cache_preload) BUG("untracked-cache preload already started"); @@ -1112,9 +1119,15 @@ void wt_status_start_untracked_cache_preload(struct wt_status *s) refresh_fsmonitor(istate); if (s->certify_clean_status && !fsmonitor_has_pending_token(istate)) - fsmonitor_reopen_token(istate); + reopened_valid_token = + fsmonitor_reopen_token(istate) && + istate->fsmonitor_untracked_valid && + istate->untracked && istate->untracked->root && + istate->untracked->use_fsmonitor && + !clean_status_fsmonitor_semantic_adoption_needed(istate); if (has_fsmonitor && (!fsmonitor_has_pending_token(istate) || + reopened_valid_token || !fstat_is_reliable())) { s->untracked_cache_preload = untracked_cache_preload_start_fsmonitor_excludes( @@ -2057,9 +2070,24 @@ wt_status_close_semantic_fsmonitor_token( istate, wt_status_untracked_cache_valid(closure)); if (result != FSMONITOR_TOKEN_CLEAN) { + int reuse_semantic_subtrees = + result == FSMONITOR_TOKEN_CHANGED && + !clean_status_filter_scope_needs_validation(istate) && + !clean_status_worktree_manifest_needs_refresh(istate) && + semantic_verify_proof_is_current(istate, *proof); + wt_status_discard_staged_untracked(closure); - untracked_cache_invalidate_all(istate); - fsmonitor_invalidate_semantics(istate); + if (reuse_semantic_subtrees) { + /* Recompute scanned subtrees after the localized delta. */ + untracked_cache_recompute_fsmonitor_valid_recursive( + istate->untracked); + trace2_data_intmax( + "status", s->repo, + "fsmonitor_token/reused-semantic-subtrees", 1); + } else { + untracked_cache_invalidate_all(istate); + fsmonitor_invalidate_semantics(istate); + } closure->untracked_ready = 0; closure->untracked_proof_complete = 0; wt_status_discard_semantic_verify(