diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 85cfedf5b0e0a4..205325eb33b064 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -460,8 +460,8 @@ jobs: if: needs.ci-config.outputs.enabled == 'yes' env: jobname: StaticAnalysis - CI_JOB_IMAGE: ubuntu-22.04 - runs-on: ubuntu-22.04 + CI_JOB_IMAGE: ubuntu-latest + runs-on: ubuntu-latest concurrency: group: static-analysis-${{ github.ref }} cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1c4d04da9dcd4c..0242283c3c5571 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -227,7 +227,7 @@ test:fuzz-smoke-tests: - ./ci/run-build-and-minimal-fuzzers.sh static-analysis: - image: ubuntu:22.04 + image: ubuntu:latest stage: analyze needs: [ ] variables: diff --git a/.mailmap b/.mailmap index f8ede075ea172f..48c34797b94a94 100644 --- a/.mailmap +++ b/.mailmap @@ -39,12 +39,13 @@ Chris Shoemaker Chris Wright Christian Ludwig Cord Seele -Christian Couder +Christian Couder Christian Stimming Christopher Díaz Riveros Christopher Diaz Riveros Clemens Buchacher Clemens Buchacher Csaba Henk +D. Ben Knoble Dan Johnson Dana L. How Dana L. How Dana How diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index a57ca5426fb4ca..87ac9067f194eb 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -76,6 +76,9 @@ UI, Workflows & Features filtered on the client based on server-advertised capabilities, returning empty strings for inapplicable or unsupported fields. + * 'git branch -d' has been taught to report when a branch cannot be + deleted because it is being used in an active bisect run. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -280,6 +283,21 @@ Performance, Internal Implementation, Development Support etc. first refactoring force_object_loose() to use generic ODB write interfaces instead of loose-backend internals. + * Object database housekeeping in 'git gc' and 'git maintenance' has + been refactored to be pluggable. The files-backend-specific logic, + including incremental and geometric repacking as well as object + pruning, has been moved out of the command implementation and into the + files object database source, enabling future alternative object + database backends to implement their own housekeeping services. + + * The image version used by the static-analysis CI job has been bumped + to ubuntu-latest (Ubuntu 24.04), which brings in a newer Coccinelle + version that resolves a severe performance regression. A false + positive warning from the 'CHECK_ASSERTION_SIDE_EFFECTS' build with + GCC 15 in the Bloom filter code has also been silenced to facilitate + the image upgrade. + (merge 1a1579c42d jk/ci-static-analysis-image-bump later to maint). + Fixes since v2.55 ----------------- @@ -466,3 +484,26 @@ Fixes since v2.55 * The remote-matching logic for submodules has been corrected to resolve 'url.*.insteadOf' aliases before comparing the inventoried URL from '.gitmodules' with the URLs of configured remotes. + + * 'git diff --relative' running with '--cached' has been corrected to + avoid a segfault when encountering unmerged paths outside the + prefix. + (merge 447126ed7d jk/diff-relative-cached-unmerged later to maint). + + * Two bugs in how 'git rebase' handles skipped 'fixup' and 'squash' + commands have been fixed. One bug caused an incorrect commit count to + be shown in the template message when multiple commands were skipped, + and another prevented the editor from opening when the final command + in a chain containing 'fixup -c' was skipped. + + * The alias tests in 't/t0014-alias.sh' have been updated to dynamically + query the list of deprecated commands using 'git + --list-cmds=deprecated' to avoid test failures when running with + 'WITH_BREAKING_CHANGES' in a build directory that contains stale + executables of formerly deprecated commands. + (merge bc57ecb915 jk/t0014-dynamic-deprecated-cmds later to maint). + + * Git for Windows has been updated to avoid auto-detecting the symlink + type if the target path starts with a slash, preventing NTLM + credential leaks when checking out repositories with crafted + symbolic links pointing to network shares. diff --git a/bloom.c b/bloom.c index c98d1672adb71a..caf22f9831bb93 100644 --- a/bloom.c +++ b/bloom.c @@ -610,10 +610,10 @@ int bloom_filter_contains_vec(const struct bloom_filter *filter, uint32_t test_bloom_murmur3_seeded(uint32_t seed, const char *data, size_t len, int version) { - assert(version == 1 || version == 2); - if (version == 2) return murmur3_seeded_v2(seed, data, len); - else + else if (version == 1) return murmur3_seeded_v1(seed, data, len); + else + BUG("unexpected bloom version: %d", version); } diff --git a/branch.c b/branch.c index 3a12e2a9ebeeb6..22f4f46b96ed3a 100644 --- a/branch.c +++ b/branch.c @@ -385,6 +385,39 @@ int validate_branchname(const char *name, struct strbuf *ref) static int initialized_checked_out_branches; static struct strmap current_checked_out_branches = STRMAP_INIT; +enum branch_checkout_kind { + BRANCH_CHECKOUT_KIND_CHECKOUT, + BRANCH_CHECKOUT_KIND_REBASE, + BRANCH_CHECKOUT_KIND_BISECT, + BRANCH_CHECKOUT_KIND_UPDATE_REF, +}; + +struct checked_out_branch { + char *refname; + char *path; + enum branch_checkout_kind kind; +}; + +static struct checked_out_branch *checked_out_branches; +static size_t checked_out_branches_alloc, checked_out_branches_nr; + +static void register_checked_out_branch(const char *prefix, const char *name, + const char *path, + enum branch_checkout_kind kind) +{ + char *refname = xstrfmt("%s%s", prefix, name); + char *path_copy = xstrdup(path); + + ALLOC_GROW(checked_out_branches, checked_out_branches_nr + 1, + checked_out_branches_alloc); + checked_out_branches[checked_out_branches_nr].refname = refname; + checked_out_branches[checked_out_branches_nr].path = path_copy; + checked_out_branches[checked_out_branches_nr].kind = kind; + checked_out_branches_nr++; + + strmap_put(¤t_checked_out_branches, refname, path_copy); +} + static void prepare_checked_out_branches(void) { int i = 0; @@ -397,7 +430,7 @@ static void prepare_checked_out_branches(void) worktrees = get_worktrees(the_repository); while (worktrees[i]) { - char *old, *wt_gitdir; + char *wt_gitdir; struct wt_status_state state = { 0 }; struct worktree *wt = worktrees[i++]; struct string_list update_refs = STRING_LIST_INIT_DUP; @@ -406,34 +439,25 @@ static void prepare_checked_out_branches(void) continue; if (wt->head_ref) { - old = strmap_put(¤t_checked_out_branches, - wt->head_ref, - xstrdup(wt->path)); - free(old); + register_checked_out_branch("", wt->head_ref, wt->path, + BRANCH_CHECKOUT_KIND_CHECKOUT); } if (wt_status_check_rebase(wt, &state) && (state.rebase_in_progress || state.rebase_interactive_in_progress) && state.branch) { - struct strbuf ref = STRBUF_INIT; - strbuf_addf(&ref, "refs/heads/%s", state.branch); - old = strmap_put(¤t_checked_out_branches, - ref.buf, - xstrdup(wt->path)); - free(old); - strbuf_release(&ref); + register_checked_out_branch("refs/heads/", state.branch, + wt->path, + BRANCH_CHECKOUT_KIND_REBASE); } wt_status_state_free_buffers(&state); if (wt_status_check_bisect(wt, &state) && state.bisecting_from) { - struct strbuf ref = STRBUF_INIT; - strbuf_addf(&ref, "refs/heads/%s", state.bisecting_from); - old = strmap_put(¤t_checked_out_branches, - ref.buf, - xstrdup(wt->path)); - free(old); - strbuf_release(&ref); + register_checked_out_branch("refs/heads/", + state.bisecting_from, + wt->path, + BRANCH_CHECKOUT_KIND_BISECT); } wt_status_state_free_buffers(&state); @@ -442,10 +466,9 @@ static void prepare_checked_out_branches(void) &update_refs)) { struct string_list_item *item; for_each_string_list_item(item, &update_refs) { - old = strmap_put(¤t_checked_out_branches, - item->string, - xstrdup(wt->path)); - free(old); + register_checked_out_branch("", item->string, + wt->path, + BRANCH_CHECKOUT_KIND_UPDATE_REF); } string_list_clear(&update_refs, 1); } @@ -462,6 +485,17 @@ const char *branch_checked_out(const char *refname) return strmap_get(¤t_checked_out_branches, refname); } +const char *branch_bisecting(const char *refname) +{ + prepare_checked_out_branches(); + for (size_t i = 0; i < checked_out_branches_nr; i++) { + if (!strcmp(refname, checked_out_branches[i].refname) && + checked_out_branches[i].kind == BRANCH_CHECKOUT_KIND_BISECT) + return checked_out_branches[i].path; + } + return NULL; +} + /* * Check if a branch 'name' can be created as a new branch; die otherwise. * 'force' can be used when it is OK for the named branch already exists. diff --git a/branch.h b/branch.h index 3dc6e2a0ffe635..e9b1f7b37df06f 100644 --- a/branch.h +++ b/branch.h @@ -106,6 +106,12 @@ void create_branches_recursively(struct repository *r, const char *name, */ const char *branch_checked_out(const char *refname); +/* + * If the branch at 'refname' is currently used for bisecting in a + * worktree, then return the path to that worktree. + */ +const char *branch_bisecting(const char *refname); + /* * Check if 'name' can be a valid name for a branch; die otherwise. * Return 1 if the named branch already exists; return 0 otherwise. diff --git a/builtin/branch.c b/builtin/branch.c index 031a4a9d055558..357209748d4fe4 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -266,6 +266,13 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, if (kinds == FILTER_REFS_BRANCHES) { const char *path; + if ((path = branch_bisecting(name))) { + error(_("cannot delete branch '%s' " + "used by worktree at '%s' for bisect"), + bname.buf, path); + ret = 1; + continue; + } if ((path = branch_checked_out(name))) { error(_("cannot delete branch '%s' " "used by worktree at '%s'"), diff --git a/builtin/gc.c b/builtin/gc.c index 49c8474fade8ed..de2f9e7fed5b3e 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -30,16 +30,11 @@ #include "commit-graph.h" #include "packfile.h" #include "object-file.h" -#include "pack.h" -#include "pack-objects.h" +#include "odb.h" #include "path.h" #include "reflog.h" -#include "repack.h" #include "rerere.h" #include "revision.h" -#include "blob.h" -#include "tree.h" -#include "promisor-remote.h" #include "refs.h" #include "remote.h" #include "exec-cmd.h" @@ -130,22 +125,11 @@ struct gc_config { unsigned long max_cruft_size; int aggressive_depth; int aggressive_window; - int gc_auto_threshold; - int gc_auto_pack_limit; int detach_auto; char *gc_log_expire; char *prune_expire; char *prune_worktrees_expire; - char *repack_filter; - char *repack_filter_to; char *repack_expire_to; - unsigned long big_pack_threshold; - unsigned long max_delta_cache_size; - /* - * Remove this member from gc_config once repo_settings is passed - * through the callchain. - */ - size_t delta_base_cache_limit; }; #define GC_CONFIG_INIT { \ @@ -154,14 +138,10 @@ struct gc_config { .cruft_packs = 1, \ .aggressive_depth = 50, \ .aggressive_window = 250, \ - .gc_auto_threshold = 6700, \ - .gc_auto_pack_limit = 50, \ .detach_auto = 1, \ .gc_log_expire = xstrdup("1.day.ago"), \ .prune_expire = xstrdup("2.weeks.ago"), \ .prune_worktrees_expire = xstrdup("3.months.ago"), \ - .max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE, \ - .delta_base_cache_limit = DEFAULT_DELTA_BASE_CACHE_LIMIT, \ } static void gc_config_release(struct gc_config *cfg) @@ -169,15 +149,12 @@ static void gc_config_release(struct gc_config *cfg) free(cfg->gc_log_expire); free(cfg->prune_expire); free(cfg->prune_worktrees_expire); - free(cfg->repack_filter); - free(cfg->repack_filter_to); } static void gc_config(struct gc_config *cfg) { const char *value; char *owned = NULL; - unsigned long ulongval; if (!repo_config_get_value(the_repository, "gc.packrefs", &value)) { if (value && !strcmp(value, "notbare")) @@ -192,8 +169,6 @@ static void gc_config(struct gc_config *cfg) repo_config_get_int(the_repository, "gc.aggressivewindow", &cfg->aggressive_window); repo_config_get_int(the_repository, "gc.aggressivedepth", &cfg->aggressive_depth); - repo_config_get_int(the_repository, "gc.auto", &cfg->gc_auto_threshold); - repo_config_get_int(the_repository, "gc.autopacklimit", &cfg->gc_auto_pack_limit); repo_config_get_bool(the_repository, "gc.autodetach", &cfg->detach_auto); repo_config_get_bool(the_repository, "gc.cruftpacks", &cfg->cruft_packs); repo_config_get_ulong(the_repository, "gc.maxcruftsize", &cfg->max_cruft_size); @@ -213,22 +188,6 @@ static void gc_config(struct gc_config *cfg) cfg->gc_log_expire = owned; } - repo_config_get_ulong(the_repository, "gc.bigpackthreshold", &cfg->big_pack_threshold); - repo_config_get_ulong(the_repository, "pack.deltacachesize", &cfg->max_delta_cache_size); - - if (!repo_config_get_ulong(the_repository, "core.deltabasecachelimit", &ulongval)) - cfg->delta_base_cache_limit = ulongval; - - if (!repo_config_get_string(the_repository, "gc.repackfilter", &owned)) { - free(cfg->repack_filter); - cfg->repack_filter = owned; - } - - if (!repo_config_get_string(the_repository, "gc.repackfilterto", &owned)) { - free(cfg->repack_filter_to); - cfg->repack_filter_to = owned; - } - repo_config(the_repository, git_default_config, NULL); } @@ -464,255 +423,13 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED) return should_gc; } -static int too_many_loose_objects(int limit) -{ - struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources); - /* - * This is weird, but stems from legacy behaviour: the GC auto - * threshold was always essentially interpreted as if it was rounded up - * to the next multiple 256 of, so we retain this behaviour for now. - */ - int auto_threshold = DIV_ROUND_UP(limit, 256) * 256; - unsigned long loose_count; - - if (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE, - &loose_count) < 0) - return 0; - - return loose_count > auto_threshold; -} - -static struct packed_git *find_base_packs(struct string_list *packs, - unsigned long limit) -{ - struct packed_git *p, *base = NULL; - - repo_for_each_pack(the_repository, p) { - if (!p->pack_local || p->is_cruft) - continue; - if (limit) { - if (p->pack_size >= limit) - string_list_append(packs, p->pack_name); - } else if (!base || base->pack_size < p->pack_size) { - base = p; - } - } - - if (base) - string_list_append(packs, base->pack_name); - - return base; -} - -static int too_many_packs(struct gc_config *cfg) -{ - struct packed_git *p; - int cnt = 0; - - if (cfg->gc_auto_pack_limit <= 0) - return 0; - - repo_for_each_pack(the_repository, p) { - if (!p->pack_local) - continue; - if (p->pack_keep) - continue; - /* - * Perhaps check the size of the pack and count only - * very small ones here? - */ - cnt++; - } - return cfg->gc_auto_pack_limit < cnt; -} - -static uint64_t total_ram(void) -{ -#if defined(HAVE_SYSINFO) - struct sysinfo si; - - if (!sysinfo(&si)) { - uint64_t total = si.totalram; - - if (si.mem_unit > 1) - total *= (uint64_t)si.mem_unit; - return total; - } -#elif defined(HAVE_BSD_SYSCTL) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM) || defined(HW_PHYSMEM64)) - uint64_t physical_memory; - int mib[2]; - size_t length; - - mib[0] = CTL_HW; -# if defined(HW_MEMSIZE) - mib[1] = HW_MEMSIZE; -# elif defined(HW_PHYSMEM64) - mib[1] = HW_PHYSMEM64; -# else - mib[1] = HW_PHYSMEM; -# endif - length = sizeof(physical_memory); - if (!sysctl(mib, 2, &physical_memory, &length, NULL, 0)) { - if (length == 4) { - uint32_t mem; - - if (!sysctl(mib, 2, &mem, &length, NULL, 0)) - physical_memory = mem; - } - return physical_memory; - } -#elif defined(GIT_WINDOWS_NATIVE) - MEMORYSTATUSEX memInfo; - - memInfo.dwLength = sizeof(MEMORYSTATUSEX); - if (GlobalMemoryStatusEx(&memInfo)) - return memInfo.ullTotalPhys; -#endif - return 0; -} - -static uint64_t estimate_repack_memory(struct gc_config *cfg, - struct packed_git *pack) -{ - unsigned long nr_objects; - size_t os_cache, heap; - - if (odb_count_objects(the_repository->objects, - ODB_COUNT_OBJECTS_APPROXIMATE, &nr_objects) < 0) - return 0; - - if (!pack || !nr_objects) - return 0; - - /* - * First we have to scan through at least one pack. - * Assume enough room in OS file cache to keep the entire pack - * or we may accidentally evict data of other processes from - * the cache. - */ - os_cache = pack->pack_size + pack->index_size; - /* then pack-objects needs lots more for book keeping */ - heap = sizeof(struct object_entry) * nr_objects; - /* - * internal rev-list --all --objects takes up some memory too, - * let's say half of it is for blobs - */ - heap += sizeof(struct blob) * nr_objects / 2; - /* - * and the other half is for trees (commits and tags are - * usually insignificant) - */ - heap += sizeof(struct tree) * nr_objects / 2; - /* and then obj_hash[], underestimated in fact */ - heap += sizeof(struct object *) * nr_objects; - /* revindex is used also */ - heap += (sizeof(off_t) + sizeof(uint32_t)) * nr_objects; - /* - * read_sha1_file() (either at delta calculation phase, or - * writing phase) also fills up the delta base cache - */ - heap += cfg->delta_base_cache_limit; - /* and of course pack-objects has its own delta cache */ - heap += cfg->max_delta_cache_size; - - return os_cache + heap; -} - -static int keep_one_pack(struct string_list_item *item, void *data) -{ - struct strvec *args = data; - strvec_pushf(args, "--keep-pack=%s", basename(item->string)); - return 0; -} - -static void add_repack_all_option(struct gc_config *cfg, - struct string_list *keep_pack, - struct strvec *args) -{ - if (cfg->prune_expire && !strcmp(cfg->prune_expire, "now") - && !(cfg->cruft_packs && cfg->repack_expire_to)) - strvec_push(args, "-a"); - else if (cfg->cruft_packs) { - strvec_push(args, "--cruft"); - if (cfg->prune_expire) - strvec_pushf(args, "--cruft-expiration=%s", cfg->prune_expire); - if (cfg->max_cruft_size) - strvec_pushf(args, "--max-cruft-size=%lu", - cfg->max_cruft_size); - if (cfg->repack_expire_to) - strvec_pushf(args, "--expire-to=%s", cfg->repack_expire_to); - } else { - strvec_push(args, "-A"); - if (cfg->prune_expire) - strvec_pushf(args, "--unpack-unreachable=%s", cfg->prune_expire); - } - - if (keep_pack) - for_each_string_list(keep_pack, keep_one_pack, args); - - if (cfg->repack_filter && *cfg->repack_filter) - strvec_pushf(args, "--filter=%s", cfg->repack_filter); - if (cfg->repack_filter_to && *cfg->repack_filter_to) - strvec_pushf(args, "--filter-to=%s", cfg->repack_filter_to); -} - -static void add_repack_incremental_option(struct strvec *args) -{ - strvec_push(args, "--no-write-bitmap-index"); -} - -static int need_to_gc(struct gc_config *cfg, struct strvec *repack_args) -{ - /* - * Setting gc.auto to 0 or negative can disable the - * automatic gc. - */ - if (cfg->gc_auto_threshold <= 0) - return 0; - - /* - * If there are too many loose objects, but not too many - * packs, we run "repack -d -l". If there are too many packs, - * we run "repack -A -d -l". Otherwise we tell the caller - * there is no need. - */ - if (too_many_packs(cfg)) { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (cfg->big_pack_threshold) { - find_base_packs(&keep_pack, cfg->big_pack_threshold); - if (keep_pack.nr >= cfg->gc_auto_pack_limit) { - cfg->big_pack_threshold = 0; - string_list_clear(&keep_pack, 0); - find_base_packs(&keep_pack, 0); - } - } else { - struct packed_git *p = find_base_packs(&keep_pack, 0); - uint64_t mem_have, mem_want; - - mem_have = total_ram(); - mem_want = estimate_repack_memory(cfg, p); - - /* - * Only allow 1/2 of memory for pack-objects, leave - * the rest for the OS and other processes in the - * system. - */ - if (!mem_have || mem_want < mem_have / 2) - string_list_clear(&keep_pack, 0); - } - - add_repack_all_option(cfg, &keep_pack, repack_args); - string_list_clear(&keep_pack, 0); - } else if (too_many_loose_objects(cfg->gc_auto_threshold)) - add_repack_incremental_option(repack_args); - else - return 0; - - if (run_hooks(the_repository, "pre-auto-gc")) - return 0; - return 1; -} +#define OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive) \ + .prune_expire = (cfg)->prune_expire, \ + .expire_to = (cfg)->repack_expire_to, \ + .cruft_packs = (cfg)->cruft_packs, \ + .max_cruft_size = (cfg)->max_cruft_size, \ + .window = (aggressive) ? (cfg)->aggressive_window : 0, \ + .depth = (aggressive) ? (cfg)->aggressive_depth : 0 /* return NULL on success, else hostname running the gc */ static const char *lock_repo_for_gc(int force, pid_t* ret_pid) @@ -841,6 +558,27 @@ static int gc_foreground_tasks(struct maintenance_run_opts *opts, return 0; } +static int maintenance_task_odb(struct maintenance_run_opts *opts, + struct gc_config *cfg, + int keep_largest_pack, + int aggressive) +{ + struct odb_optimize_options odb_opts = { + .strategy = ODB_OPTIMIZE_INCREMENTAL, + .keep_largest_pack = keep_largest_pack, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive), + }; + + if (opts->auto_flag) + odb_opts.flags |= ODB_OPTIMIZE_AUTO; + if (!opts->quiet) + odb_opts.flags |= ODB_OPTIMIZE_VERBOSE; + if (aggressive) + odb_opts.flags |= ODB_OPTIMIZE_NO_REUSE_DELTAS; + + return odb_optimize(the_repository->objects, &odb_opts); +} + int cmd_gc(int argc, const char **argv, const char *prefix, @@ -854,7 +592,6 @@ int cmd_gc(int argc, int keep_largest_pack = -1; int skip_foreground_tasks = 0; timestamp_t dummy; - struct strvec repack_args = STRVEC_INIT; struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT; struct gc_config cfg = GC_CONFIG_INIT; const char *prune_expire_sentinel = "sentinel"; @@ -894,8 +631,6 @@ int cmd_gc(int argc, show_usage_with_options_if_asked(argc, argv, builtin_gc_usage, builtin_gc_options); - strvec_pushl(&repack_args, "repack", "-d", "-l", NULL); - gc_config(&cfg); if (parse_expiry_date(cfg.gc_log_expire, &gc_log_expire_time)) @@ -916,24 +651,20 @@ int cmd_gc(int argc, if (cfg.prune_expire && parse_expiry_date(cfg.prune_expire, &dummy)) die(_("failed to parse prune expiry value %s"), cfg.prune_expire); - if (aggressive) { - strvec_push(&repack_args, "-f"); - if (cfg.aggressive_depth > 0) - strvec_pushf(&repack_args, "--depth=%d", cfg.aggressive_depth); - if (cfg.aggressive_window > 0) - strvec_pushf(&repack_args, "--window=%d", cfg.aggressive_window); - } - if (opts.quiet) - strvec_push(&repack_args, "-q"); - if (opts.auto_flag) { + struct odb_optimize_options optimize_opts = { + .strategy = ODB_OPTIMIZE_INCREMENTAL, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(&cfg, 0), + }; + if (cfg.detach_auto && opts.detach < 0) opts.detach = 1; /* * Auto-gc should be least intrusive as possible. */ - if (!need_to_gc(&cfg, &repack_args)) { + if (!odb_optimize_required(the_repository->objects, &optimize_opts) || + run_hooks(the_repository, "pre-auto-gc")) { ret = 0; goto out; } @@ -945,18 +676,6 @@ int cmd_gc(int argc, fprintf(stderr, _("Auto packing the repository for optimum performance.\n")); fprintf(stderr, _("See \"git help gc\" for manual housekeeping.\n")); } - } else { - struct string_list keep_pack = STRING_LIST_INIT_NODUP; - - if (keep_largest_pack != -1) { - if (keep_largest_pack) - find_base_packs(&keep_pack, 0); - } else if (cfg.big_pack_threshold) { - find_base_packs(&keep_pack, cfg.big_pack_threshold); - } - - add_repack_all_option(&cfg, &keep_pack, &repack_args); - string_list_clear(&keep_pack, 0); } if (opts.detach > 0) { @@ -1012,33 +731,6 @@ int cmd_gc(int argc, if (opts.detach <= 0 && !skip_foreground_tasks) gc_foreground_tasks(&opts, &cfg); - if (!the_repository->repository_format_precious_objects) { - struct child_process repack_cmd = CHILD_PROCESS_INIT; - - repack_cmd.git_cmd = 1; - repack_cmd.odb_to_close = the_repository->objects; - strvec_pushv(&repack_cmd.args, repack_args.v); - if (run_command(&repack_cmd)) - die(FAILED_RUN, repack_args.v[0]); - - if (cfg.prune_expire) { - struct child_process prune_cmd = CHILD_PROCESS_INIT; - - strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); - /* run `git prune` even if using cruft packs */ - strvec_push(&prune_cmd.args, cfg.prune_expire); - if (opts.quiet) - strvec_push(&prune_cmd.args, "--no-progress"); - if (repo_has_promisor_remote(the_repository)) - strvec_push(&prune_cmd.args, - "--exclude-promisor-objects"); - prune_cmd.git_cmd = 1; - - if (run_command(&prune_cmd)) - die(FAILED_RUN, prune_cmd.args.v[0]); - } - } - if (cfg.prune_worktrees_expire && maintenance_task_worktree_prune(&opts, &cfg)) die(FAILED_RUN, "worktree"); @@ -1046,6 +738,9 @@ int cmd_gc(int argc, if (maintenance_task_rerere_gc(&opts, &cfg)) die(FAILED_RUN, "rerere"); + if (maintenance_task_odb(&opts, &cfg, keep_largest_pack, aggressive)) + die(NULL); + report_garbage = report_pack_garbage; odb_reprepare(the_repository->objects); if (pack_garbage.nr > 0) { @@ -1058,10 +753,6 @@ int cmd_gc(int argc, !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0, NULL); - if (opts.auto_flag && too_many_loose_objects(cfg.gc_auto_threshold)) - warning(_("There are too many unreachable loose objects; " - "run 'git prune' to remove them.")); - if (!daemonized) { char *path = repo_git_path(the_repository, "gc.log"); unlink(path); @@ -1070,7 +761,6 @@ int cmd_gc(int argc, out: maintenance_run_opts_release(&opts); - strvec_clear(&repack_args); gc_config_release(&cfg); return 0; } @@ -1273,15 +963,11 @@ static int maintenance_task_gc_background(struct maintenance_run_opts *opts, static int gc_condition(struct gc_config *cfg) { - /* - * Note that it's fine to drop the repack arguments here, as we execute - * git-gc(1) as a separate child process anyway. So it knows to compute - * these arguments again. - */ - struct strvec repack_args = STRVEC_INIT; - int ret = need_to_gc(cfg, &repack_args); - strvec_clear(&repack_args); - return ret; + struct odb_optimize_options opts = { + .strategy = ODB_OPTIMIZE_INCREMENTAL, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), + }; + return odb_optimize_required(the_repository->objects, &opts); } static int prune_packed(struct maintenance_run_opts *opts) @@ -1570,104 +1256,24 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts, struct gc_config *cfg) { - struct pack_geometry geometry = { - .split_factor = 2, + struct odb_optimize_options odb_opts = { + .strategy = ODB_OPTIMIZE_GEOMETRIC, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), }; - struct pack_objects_args po_args = { - .local = 1, - }; - struct existing_packs existing_packs = EXISTING_PACKS_INIT; - struct string_list kept_packs = STRING_LIST_INIT_DUP; - struct child_process child = CHILD_PROCESS_INIT; - int ret; - repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor", - &geometry.split_factor); + if (!opts->quiet) + odb_opts.flags |= ODB_OPTIMIZE_VERBOSE; - existing_packs.repo = the_repository; - existing_packs_collect(&existing_packs, &kept_packs); - pack_geometry_init(&geometry, &existing_packs, &po_args); - pack_geometry_split(&geometry); - - child.git_cmd = 1; - child.odb_to_close = the_repository->objects; - - strvec_pushl(&child.args, "repack", "-d", "-l", NULL); - if (geometry.split < geometry.pack_nr) - strvec_pushf(&child.args, "--geometric=%d", - geometry.split_factor); - else - add_repack_all_option(cfg, NULL, &child.args); - if (opts->quiet) - strvec_push(&child.args, "--quiet"); - if (the_repository->settings.core_multi_pack_index) - strvec_push(&child.args, "--write-midx"); - - if (run_command(&child)) { - ret = error(_("failed to perform geometric repack")); - goto out; - } - - ret = 0; - -out: - existing_packs_release(&existing_packs); - pack_geometry_release(&geometry); - return ret; + return odb_optimize(the_repository->objects, &odb_opts); } -static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED) +static int geometric_repack_auto_condition(struct gc_config *cfg) { - struct pack_geometry geometry = { - .split_factor = 2, - }; - struct pack_objects_args po_args = { - .local = 1, + struct odb_optimize_options opts = { + .strategy = ODB_OPTIMIZE_GEOMETRIC, + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0), }; - struct existing_packs existing_packs = EXISTING_PACKS_INIT; - struct string_list kept_packs = STRING_LIST_INIT_DUP; - int auto_value = 100; - int ret; - - repo_config_get_int(the_repository, "maintenance.geometric-repack.auto", - &auto_value); - if (!auto_value) - return 0; - if (auto_value < 0) - return 1; - - repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor", - &geometry.split_factor); - - existing_packs.repo = the_repository; - existing_packs_collect(&existing_packs, &kept_packs); - pack_geometry_init(&geometry, &existing_packs, &po_args); - pack_geometry_split(&geometry); - - /* - * When we'd merge at least two packs with one another we always - * perform the repack. - */ - if (geometry.split) { - ret = 1; - goto out; - } - - /* - * Otherwise, we estimate the number of loose objects to determine - * whether we want to create a new packfile or not. - */ - if (too_many_loose_objects(auto_value)) { - ret = 1; - goto out; - } - - ret = 0; - -out: - existing_packs_release(&existing_packs); - pack_geometry_release(&geometry); - return ret; + return odb_optimize_required(the_repository->objects, &opts); } typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts, @@ -1755,11 +1361,18 @@ enum task_phase { TASK_PHASE_BACKGROUND, }; +enum auto_gc_hook_result { + AUTO_GC_HOOK_UNDECIDED = 0, + AUTO_GC_HOOK_RUN = 1, + AUTO_GC_HOOK_SKIP = 2, +}; + static int maybe_run_task(const struct maintenance_task *task, struct repository *repo, struct maintenance_run_opts *opts, struct gc_config *cfg, - enum task_phase phase) + enum task_phase phase, + enum auto_gc_hook_result *auto_gc_hook_result) { int foreground = (phase == TASK_PHASE_FOREGROUND); maintenance_task_fn fn = foreground ? task->foreground : task->background; @@ -1768,9 +1381,19 @@ static int maybe_run_task(const struct maintenance_task *task, if (!fn) return 0; - if (opts->auto_flag && - (!task->auto_condition || !task->auto_condition(cfg))) - return 0; + if (opts->auto_flag) { + if (*auto_gc_hook_result == AUTO_GC_HOOK_SKIP) + return 0; + + if (!task->auto_condition || !task->auto_condition(cfg)) + return 0; + + if (*auto_gc_hook_result == AUTO_GC_HOOK_UNDECIDED) + *auto_gc_hook_result = run_hooks(repo, "pre-auto-gc") ? + AUTO_GC_HOOK_SKIP : AUTO_GC_HOOK_RUN; + if (*auto_gc_hook_result == AUTO_GC_HOOK_SKIP) + return 0; + } trace2_region_enter(region, task->name, repo); if (fn(opts, cfg)) { @@ -1789,6 +1412,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts, struct lock_file lk; struct repository *r = the_repository; char *lock_path = xstrfmt("%s/maintenance", r->objects->sources->path); + enum auto_gc_hook_result auto_gc_hook_result = AUTO_GC_HOOK_UNDECIDED; if (repo_hold_lock_file_for_update(r, &lk, lock_path, LOCK_NO_DEREF) < 0) { /* @@ -1808,7 +1432,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts, for (size_t i = 0; i < opts->tasks_nr; i++) if (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, - TASK_PHASE_FOREGROUND)) + TASK_PHASE_FOREGROUND, &auto_gc_hook_result)) result = 1; /* Failure to daemonize is ok, we'll continue in foreground. */ @@ -1820,7 +1444,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts, for (size_t i = 0; i < opts->tasks_nr; i++) if (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg, - TASK_PHASE_BACKGROUND)) + TASK_PHASE_BACKGROUND, &auto_gc_hook_result)) result = 1; rollback_lock_file(&lk); diff --git a/compat/mingw.c b/compat/mingw.c index e0fbd2c66de23d..4c2f26d4548147 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -353,6 +353,29 @@ process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink) wchar_t relative[MAX_PATH]; const wchar_t *rel; + /* + * Do not follow symlinks to network shares, to avoid NTLM credential + * leak from crafted repositories (e.g. \\attacker-server\share). + * Since paths come in all kind of enterprising shapes and forms (in + * addition to the canonical `\\host\share` form, there's also + * `\??\UNC\host\share`, `\GLOBAL??\UNC\host\share` and also + * `\Device\Mup\host\share`, just to name a few), we simply avoid + * following every symlink target that starts with a slash. + * + * This also catches drive-less absolute paths, of course. These are + * uncommon in practice (and also fragile because they are relative to + * the current working directory's drive). The only "harm" this does + * is that it now requires users to specify via the Git attributes if + * they have such an uncommon symbolic link and need it to be a + * directory type link. + */ + if (is_wdir_sep(wtarget[0])) { + warning("created file symlink '%ls' pointing to '%ls';\n" + "set the `symlink` gitattribute to `dir` if a " + "directory symlink is required", wlink, wtarget); + return PHANTOM_SYMLINK_DONE; + } + /* check that wlink is still a file symlink */ if ((GetFileAttributesW(wlink) & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) diff --git a/diff-lib.c b/diff-lib.c index 46cae637ecda83..ac4e310438e710 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -467,7 +467,7 @@ static void do_oneway_diff(struct unpack_trees_options *o, if (cached && idx && ce_stage(idx)) { struct diff_filepair *pair; pair = diff_unmerge(&revs->diffopt, idx->name); - if (tree) + if (pair && tree) fill_filespec(pair->one, &tree->oid, 1, tree->ce_mode); return; @@ -530,6 +530,16 @@ static int oneway_diff(const struct cache_entry * const *src, if (tree == o->df_conflict_entry) tree = NULL; + /* + * We should only see a NULL idx when the entry was present in the tree + * but deleted in the idx. In which case it should be impossible + * that a NULL tree was passed in (there would have been no entry at + * all) or that we got a df conflict above (you need a directory and a + * file to get such a conflict, which implies both sides are present). + */ + if (!idx && !tree) + BUG("oneway_diff with neither idx nor tree"); + if (ce_path_match(revs->diffopt.repo->index, idx ? idx : tree, &revs->prune_data, NULL)) { diff --git a/odb.c b/odb.c index dabd481f57dbc4..caf1d0f5424d29 100644 --- a/odb.c +++ b/odb.c @@ -1034,6 +1034,18 @@ int odb_write_object_stream(struct object_database *odb, return odb_source_write_object_stream(odb->sources, stream, len, oid); } +int odb_optimize(struct object_database *odb, + const struct odb_optimize_options *opts) +{ + return odb_source_optimize(odb->sources, opts); +} + +bool odb_optimize_required(struct object_database *odb, + const struct odb_optimize_options *opts) +{ + return odb_source_optimize_required(odb->sources, opts); +} + struct object_database *odb_new(struct repository *repo, const char *primary_source, const char *secondary_sources) diff --git a/odb.h b/odb.h index cbc2f9ced42338..fca67e8253e7ad 100644 --- a/odb.h +++ b/odb.h @@ -118,6 +118,51 @@ struct object_database *odb_new(struct repository *repo, /* Free the object database and release all resources. */ void odb_free(struct object_database *o); +enum odb_optimize_strategy { + ODB_OPTIMIZE_INCREMENTAL, + ODB_OPTIMIZE_GEOMETRIC, +}; + +enum odb_optimize_flags { + /* Enable verbose logging and progress reporting. */ + ODB_OPTIMIZE_VERBOSE = (1 << 0), + + /* Perform auto-maintenance, only optimizing objects as required. */ + ODB_OPTIMIZE_AUTO = (1 << 1), + + /* Recompute existing deltas. */ + ODB_OPTIMIZE_NO_REUSE_DELTAS = (1 << 2), +}; + +struct odb_optimize_options { + enum odb_optimize_strategy strategy; + enum odb_optimize_flags flags; + const char *prune_expire; + const char *expire_to; + int depth; + int window; + + /* Backend-specific options. */ + int keep_largest_pack; + int cruft_packs; + unsigned long max_cruft_size; +}; + +/* + * Optimize the object database. Returns 0 on success, a negative error code + * otherwise. + */ +int odb_optimize(struct object_database *odb, + const struct odb_optimize_options *opts); + +/* + * Check whether optimization of the object database is required given the + * provided options. Returns true if optimization should be performed, false + * otherwise. + */ +bool odb_optimize_required(struct object_database *odb, + const struct odb_optimize_options *opts); + /* * Close the object database and all of its sources so that any held resources * will be released. The database can still be used after closing it, in which diff --git a/odb/source-files.c b/odb/source-files.c index 5e086d266fac4f..5a68af7d84c250 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -1,6 +1,8 @@ #include "git-compat-util.h" #include "abspath.h" +#include "blob.h" #include "chdir-notify.h" +#include "config.h" #include "gettext.h" #include "lockfile.h" #include "object-file.h" @@ -8,8 +10,16 @@ #include "odb/source.h" #include "odb/source-files.h" #include "odb/source-loose.h" +#include "pack-objects.h" #include "packfile.h" +#include "path.h" +#include "promisor-remote.h" +#include "repack.h" +#include "run-command.h" #include "strbuf.h" +#include "string-list.h" +#include "strvec.h" +#include "tree.h" #include "write-or-die.h" static void odb_source_files_reparent(const char *name UNUSED, @@ -261,6 +271,464 @@ static int odb_source_files_write_alternate(struct odb_source *source, return ret; } +static int too_many_loose_objects(struct odb_source_files *files, int limit) +{ + unsigned long loose_count; + + if (limit <= 0) + return 0; + + if (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE, + &loose_count) < 0) + return 0; + + /* + * This is weird, but stems from legacy behaviour: the GC auto + * threshold was always essentially interpreted as if it was rounded up + * to the next multiple 256 of, so we retain this behaviour for now. + */ + return loose_count > (DIV_ROUND_UP(((unsigned long) limit), 256) * 256); +} + +static struct packed_git *find_base_packs(struct odb_source_files *files, + struct string_list *packs, + unsigned long limit) +{ + struct packfile_list_entry *e; + struct packed_git *base = NULL; + + for (e = packfile_store_get_packs(files->packed); e; e = e->next) { + if (e->pack->is_cruft) + continue; + if (limit) { + if ((uintmax_t) e->pack->pack_size >= limit) + string_list_append(packs, e->pack->pack_name); + } else if (!base || base->pack_size < e->pack->pack_size) { + base = e->pack; + } + } + + if (base) + string_list_append(packs, base->pack_name); + + return base; +} + +static int too_many_packs(struct odb_source_files *files, int gc_auto_pack_limit) +{ + struct packfile_list_entry *e; + int cnt = 0; + + if (gc_auto_pack_limit <= 0) + return 0; + + for (e = packfile_store_get_packs(files->packed); e; e = e->next) { + if (e->pack->pack_keep) + continue; + /* + * Perhaps check the size of the pack and count only + * very small ones here? + */ + cnt++; + } + return gc_auto_pack_limit < cnt; +} + +static uint64_t total_ram(void) +{ +#if defined(HAVE_SYSINFO) + struct sysinfo si; + + if (!sysinfo(&si)) { + uint64_t total = si.totalram; + + if (si.mem_unit > 1) + total *= (uint64_t)si.mem_unit; + return total; + } +#elif defined(HAVE_BSD_SYSCTL) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM) || defined(HW_PHYSMEM64)) + uint64_t physical_memory; + int mib[2]; + size_t length; + + mib[0] = CTL_HW; +# if defined(HW_MEMSIZE) + mib[1] = HW_MEMSIZE; +# elif defined(HW_PHYSMEM64) + mib[1] = HW_PHYSMEM64; +# else + mib[1] = HW_PHYSMEM; +# endif + length = sizeof(physical_memory); + if (!sysctl(mib, 2, &physical_memory, &length, NULL, 0)) { + if (length == 4) { + uint32_t mem; + + if (!sysctl(mib, 2, &mem, &length, NULL, 0)) + physical_memory = mem; + } + return physical_memory; + } +#elif defined(GIT_WINDOWS_NATIVE) + MEMORYSTATUSEX memInfo; + + memInfo.dwLength = sizeof(MEMORYSTATUSEX); + if (GlobalMemoryStatusEx(&memInfo)) + return memInfo.ullTotalPhys; +#endif + return 0; +} + +static uint64_t estimate_repack_memory(struct odb_source_files *files, + struct packed_git *pack) +{ + unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; + unsigned long delta_base_cache_limit = DEFAULT_DELTA_BASE_CACHE_LIMIT; + unsigned long nr_objects; + size_t os_cache, heap; + + if (odb_source_count_objects(&files->base, ODB_COUNT_OBJECTS_APPROXIMATE, + &nr_objects) < 0) + return 0; + + if (!pack || !nr_objects) + return 0; + + repo_config_get_ulong(files->base.odb->repo, "pack.deltacachesize", + &max_delta_cache_size); + repo_config_get_ulong(files->base.odb->repo, "core.deltabasecachelimit", + &delta_base_cache_limit); + + /* + * First we have to scan through at least one pack. + * Assume enough room in OS file cache to keep the entire pack + * or we may accidentally evict data of other processes from + * the cache. + */ + os_cache = pack->pack_size + pack->index_size; + /* then pack-objects needs lots more for book keeping */ + heap = sizeof(struct object_entry) * nr_objects; + /* + * internal rev-list --all --objects takes up some memory too, + * let's say half of it is for blobs + */ + heap += sizeof(struct blob) * nr_objects / 2; + /* + * and the other half is for trees (commits and tags are + * usually insignificant) + */ + heap += sizeof(struct tree) * nr_objects / 2; + /* and then obj_hash[], underestimated in fact */ + heap += sizeof(struct object *) * nr_objects; + /* revindex is used also */ + heap += (sizeof(off_t) + sizeof(uint32_t)) * nr_objects; + /* + * read_sha1_file() (either at delta calculation phase, or + * writing phase) also fills up the delta base cache + */ + heap += delta_base_cache_limit; + /* and of course pack-objects has its own delta cache */ + heap += max_delta_cache_size; + + return os_cache + heap; +} + +static int keep_one_pack(struct string_list_item *item, void *data) +{ + struct strvec *args = data; + strvec_pushf(args, "--keep-pack=%s", basename(item->string)); + return 0; +} + +static void add_repack_all_option(struct repository *repo, + const struct odb_optimize_options *opts, + struct string_list *keep_pack, + struct strvec *args) +{ + char *repack_filter = NULL; + char *repack_filter_to = NULL; + + repo_config_get_string(repo, "gc.repackfilter", &repack_filter); + repo_config_get_string(repo, "gc.repackfilterto", &repack_filter_to); + + if (opts->prune_expire && !strcmp(opts->prune_expire, "now") && + !(opts->cruft_packs && opts->expire_to)) + strvec_push(args, "-a"); + else if (opts->cruft_packs) { + strvec_push(args, "--cruft"); + if (opts->prune_expire) + strvec_pushf(args, "--cruft-expiration=%s", opts->prune_expire); + if (opts->max_cruft_size) + strvec_pushf(args, "--max-cruft-size=%lu", + opts->max_cruft_size); + if (opts->expire_to) + strvec_pushf(args, "--expire-to=%s", opts->expire_to); + } else { + strvec_push(args, "-A"); + if (opts->prune_expire) + strvec_pushf(args, "--unpack-unreachable=%s", opts->prune_expire); + } + + if (keep_pack) + for_each_string_list(keep_pack, keep_one_pack, args); + + if (repack_filter && *repack_filter) + strvec_pushf(args, "--filter=%s", repack_filter); + if (repack_filter_to && *repack_filter_to) + strvec_pushf(args, "--filter-to=%s", repack_filter_to); + + free(repack_filter); + free(repack_filter_to); +} + +static void add_repack_incremental_option(struct strvec *args) +{ + strvec_push(args, "--no-write-bitmap-index"); +} + +bool odb_source_files_optimize_required(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + struct odb_source_files *files = odb_source_files_downcast(source); + struct repository *repo = source->odb->repo; + + switch (opts->strategy) { + case ODB_OPTIMIZE_INCREMENTAL: { + int gc_auto_threshold = 6700; + int gc_auto_pack_limit = 50; + + repo_config_get_int(repo, "gc.auto", &gc_auto_threshold); + repo_config_get_int(repo, "gc.autopacklimit", &gc_auto_pack_limit); + + /* + * Setting gc.auto to 0 or negative can disable the + * automatic gc. + */ + if (gc_auto_threshold <= 0) + return false; + if (!too_many_packs(files, gc_auto_pack_limit) && + !too_many_loose_objects(files, gc_auto_threshold)) + return false; + + return true; + } + case ODB_OPTIMIZE_GEOMETRIC: { + struct pack_geometry geometry = { + .split_factor = 2, + }; + struct pack_objects_args po_args = { + .local = 1, + }; + struct existing_packs existing_packs = EXISTING_PACKS_INIT; + struct string_list kept_packs = STRING_LIST_INIT_DUP; + int auto_value = 100; + bool ret; + + repo_config_get_int(repo, "maintenance.geometric-repack.auto", + &auto_value); + if (!auto_value) + return false; + if (auto_value < 0) + return true; + + repo_config_get_int(repo, "maintenance.geometric-repack.splitFactor", + &geometry.split_factor); + + existing_packs.repo = repo; + existing_packs_collect(&existing_packs, &kept_packs); + pack_geometry_init(&geometry, &existing_packs, &po_args); + pack_geometry_split(&geometry); + + /* + * When we'd merge at least two packs with one another we always + * perform the repack. + */ + if (geometry.split) { + ret = true; + goto out; + } + + /* + * Otherwise, we estimate the number of loose objects to determine + * whether we want to create a new packfile or not. + */ + if (too_many_loose_objects(files, auto_value)) { + ret = true; + goto out; + } + + ret = false; + + out: + existing_packs_release(&existing_packs); + pack_geometry_release(&geometry); + return ret; + } + default: + BUG("unknown maintenance strategy '%d'", opts->strategy); + } +} + +int odb_source_files_optimize(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + struct odb_source_files *files = odb_source_files_downcast(source); + struct repository *repo = source->odb->repo; + struct child_process repack_cmd = CHILD_PROCESS_INIT; + unsigned long big_pack_threshold = 0; + int gc_auto_threshold = 6700; + int gc_auto_pack_limit = 50; + int ret; + + repo_config_get_int(repo, "gc.auto", &gc_auto_threshold); + repo_config_get_int(repo, "gc.autopacklimit", &gc_auto_pack_limit); + repo_config_get_ulong(repo, "gc.bigpackthreshold", &big_pack_threshold); + + if (repo->repository_format_precious_objects) + return 0; + + repack_cmd.git_cmd = 1; + repack_cmd.odb_to_close = repo->objects; + + strvec_pushl(&repack_cmd.args, "repack", "-d", "-l", NULL); + if (opts->flags & ODB_OPTIMIZE_NO_REUSE_DELTAS) + strvec_push(&repack_cmd.args, "-f"); + if (opts->depth > 0) + strvec_pushf(&repack_cmd.args, "--depth=%d", opts->depth); + if (opts->window > 0) + strvec_pushf(&repack_cmd.args, "--window=%d", opts->window); + if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) + strvec_push(&repack_cmd.args, "-q"); + + /* + * There's three cases we need to consider: + * + * - If we're invoked without `--auto` we'll need to perform a full + * repack. + * + * - If we're invoked with `--auto` and there's too many packs, then + * we perform a full repack, as well. + * + * - Otherwise we perform an incremental repack. + */ + switch (opts->strategy) { + case ODB_OPTIMIZE_INCREMENTAL: + if (!(opts->flags & ODB_OPTIMIZE_AUTO)) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (opts->keep_largest_pack != -1) { + if (opts->keep_largest_pack) + find_base_packs(files, &keep_pack, 0); + } else if (big_pack_threshold) { + find_base_packs(files, &keep_pack, big_pack_threshold); + } + + add_repack_all_option(repo, opts, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else { + if (too_many_packs(files, gc_auto_pack_limit)) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (big_pack_threshold) { + find_base_packs(files, &keep_pack, big_pack_threshold); + if (keep_pack.nr >= (unsigned long) gc_auto_pack_limit) { + string_list_clear(&keep_pack, 0); + find_base_packs(files, &keep_pack, 0); + } + } else { + struct packed_git *p = find_base_packs(files, &keep_pack, 0); + uint64_t mem_have, mem_want; + + mem_have = total_ram(); + mem_want = estimate_repack_memory(files, p); + + /* + * Only allow 1/2 of memory for pack-objects, leave + * the rest for the OS and other processes in the + * system. + */ + if (!mem_have || mem_want < mem_have / 2) + string_list_clear(&keep_pack, 0); + } + + add_repack_all_option(repo, opts, &keep_pack, &repack_cmd.args); + string_list_clear(&keep_pack, 0); + } else { + add_repack_incremental_option(&repack_cmd.args); + } + } + + break; + case ODB_OPTIMIZE_GEOMETRIC: { + struct pack_geometry geometry = { + .split_factor = 2, + }; + struct pack_objects_args po_args = { + .local = 1, + }; + struct existing_packs existing_packs = EXISTING_PACKS_INIT; + struct string_list kept_packs = STRING_LIST_INIT_DUP; + + repo_config_get_int(repo, "maintenance.geometric-repack.splitFactor", + &geometry.split_factor); + + existing_packs.repo = repo; + existing_packs_collect(&existing_packs, &kept_packs); + pack_geometry_init(&geometry, &existing_packs, &po_args); + pack_geometry_split(&geometry); + + if (geometry.split < geometry.pack_nr) { + strvec_pushf(&repack_cmd.args, "--geometric=%d", + geometry.split_factor); + } else { + add_repack_all_option(repo, opts, NULL, &repack_cmd.args); + } + if (repo->settings.core_multi_pack_index) + strvec_push(&repack_cmd.args, "--write-midx"); + + existing_packs_release(&existing_packs); + pack_geometry_release(&geometry); + break; + } + default: + die("unknown maintenance strategy '%d'", opts->strategy); + } + + if (run_command(&repack_cmd)) { + ret = error("failed to run %s", repack_cmd.args.v[0]); + goto out; + } + + /* Geometric repacking uses cruft packs, so we don't have to prune separately. */ + if (opts->strategy != ODB_OPTIMIZE_GEOMETRIC && opts->prune_expire) { + struct child_process prune_cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL); + /* run `git prune` even if using cruft packs */ + strvec_push(&prune_cmd.args, opts->prune_expire); + if (!(opts->flags & ODB_OPTIMIZE_VERBOSE)) + strvec_push(&prune_cmd.args, "--no-progress"); + if (repo_has_promisor_remote(repo)) + strvec_push(&prune_cmd.args, + "--exclude-promisor-objects"); + prune_cmd.git_cmd = 1; + + if (run_command(&prune_cmd)) { + ret = error("failed to run %s", prune_cmd.args.v[0]); + goto out; + } + } + + if (opts->flags & ODB_OPTIMIZE_AUTO && too_many_loose_objects(files, gc_auto_threshold)) + warning(_("There are too many unreachable loose objects; " + "run 'git prune' to remove them.")); + + ret = 0; + +out: + return ret; +} + struct odb_source_files *odb_source_files_new(struct object_database *odb, const char *path, bool local) @@ -286,6 +754,8 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, files->base.begin_transaction = odb_source_files_begin_transaction; files->base.read_alternates = odb_source_files_read_alternates; files->base.write_alternate = odb_source_files_write_alternate; + files->base.optimize = odb_source_files_optimize; + files->base.optimize_required = odb_source_files_optimize_required; /* * Ideally, we would only ever store absolute paths in the source. This diff --git a/odb/source-files.h b/odb/source-files.h index d7ac3c1c81d892..044242bc36e4a7 100644 --- a/odb/source-files.h +++ b/odb/source-files.h @@ -21,6 +21,21 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, const char *path, bool local); +/* + * Optimize the files object database source by repacking loose objects and + * packfiles as needed. Returns 0 on success, a negative error code otherwise. + */ +int odb_source_files_optimize(struct odb_source *source, + const struct odb_optimize_options *opts); + +/* + * Check whether optimization of the files object database source is required + * given the provided options. Returns true if optimization should be + * performed, false otherwise. + */ +bool odb_source_files_optimize_required(struct odb_source *source, + const struct odb_optimize_options *opts); + /* * Cast the given object database source to the files backend. This will cause * a BUG in case the source doesn't use this backend. diff --git a/odb/source.h b/odb/source.h index fc04dd5cda8800..d69f8e2d1cf626 100644 --- a/odb/source.h +++ b/odb/source.h @@ -263,6 +263,21 @@ struct odb_source { */ int (*write_alternate)(struct odb_source *source, const char *alternate); + + /* + * This callback is expected to optimize the object database source. + * Returns 0 on success, a negative error code otherwise. + */ + int (*optimize)(struct odb_source *source, + const struct odb_optimize_options *opts); + + /* + * This callback is expected to check whether optimization of the + * object database source is required given the provided options. + * Returns true if optimization should be performed, false otherwise. + */ + bool (*optimize_required)(struct odb_source *source, + const struct odb_optimize_options *opts); }; /* @@ -484,4 +499,25 @@ static inline int odb_source_begin_transaction(struct odb_source *source, return source->begin_transaction(source, out, flags); } +/* + * Optimize the object database source. Returns 0 on success, a negative error + * code otherwise. + */ +static inline int odb_source_optimize(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + return source->optimize(source, opts); +} + +/* + * Check whether optimization of the object database source is required given + * the provided options. Returns true if optimization should be performed, + * false otherwise. + */ +static inline bool odb_source_optimize_required(struct odb_source *source, + const struct odb_optimize_options *opts) +{ + return source->optimize_required(source, opts); +} + #endif diff --git a/sequencer.c b/sequencer.c index 82ab3c536f94a2..83c38492052b46 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1926,6 +1926,13 @@ static int seen_squash(struct replay_ctx *ctx) strstr(ctx->current_fixups.buf, "\nsquash"); } +/* Does the current fixup chain contain a "fixup -c" command? */ +static int seen_fixup_edit_msg(struct replay_ctx *ctx) +{ + return starts_with(ctx->current_fixups.buf, "fixup -c") || + strstr(ctx->current_fixups.buf, "\nfixup -c"); +} + static void update_comment_bufs(struct strbuf *buf1, struct strbuf *buf2, int n) { strbuf_setlen(buf1, strlen(comment_line_str) + 1); @@ -2148,9 +2155,14 @@ static int update_squash_messages(struct repository *r, strbuf_release(&buf); if (!res) { - strbuf_addf(&ctx->current_fixups, "%s%s %s", + const char *fixup_flag = ""; + + if (is_fixup_flag(command, flag) && (flag & TODO_EDIT_FIXUP_MSG)) + fixup_flag = " -c"; + + strbuf_addf(&ctx->current_fixups, "%s%s%s %s", ctx->current_fixups.len ? "\n" : "", - command_to_string(command), + command_to_string(command), fixup_flag, oid_to_hex(&commit->object.oid)); res = write_message(ctx->current_fixups.buf, ctx->current_fixups.len, @@ -3311,7 +3323,13 @@ static int read_populate_opts(struct replay_opts *opts) const char *p = ctx->current_fixups.buf; ctx->current_fixup_count = 1; while ((p = strchr(p, '\n'))) { - ctx->current_fixup_count++; + /* + * Older versions of git accidentally + * inserted blank lines when a fixup + * was skipped. + */ + if (p[1] && p[1] != '\n') + ctx->current_fixup_count++; p++; } } @@ -5406,6 +5424,9 @@ static int commit_staged_changes(struct repository *r, BUG("Incorrect current_fixups:\n%s", p); while (len && p[len - 1] != '\n') len--; + /* Remove trailing newline */ + if (len) + len--; strbuf_setlen(&ctx->current_fixups, len); if (write_message(p, len, rebase_path_current_fixups(), 0) < 0) { @@ -5434,8 +5455,8 @@ static int commit_staged_changes(struct repository *r, * message, no need to bother the user with * opening the commit message in the editor. */ - if (!starts_with(p, "squash ") && - !strstr(p, "\nsquash ")) + if (!seen_squash(ctx) && + !seen_fixup_edit_msg(ctx)) flags = (flags & ~EDIT_MSG) | CLEANUP_MSG; } else if (is_fixup(peek_command(todo_list, 0))) { /* diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh index 5144b0effd78aa..cbc447b4814e78 100755 --- a/t/t0014-alias.sh +++ b/t/t0014-alias.sh @@ -27,17 +27,25 @@ test_expect_success 'looping aliases - internal execution' ' test_grep "^fatal: alias loop detected: expansion of" output ' -test_expect_success 'looping aliases - deprecated builtins' ' - test_config alias.whatchanged pack-redundant && - test_config alias.pack-redundant whatchanged && +test_expect_success 'detect deprecated commands' ' + git --list-cmds=deprecated >deprecated && + if read deprecated1 && read deprecated2 + then + test_set_prereq HAVE_DEPRECATED + fi expect <<-EOF && - ${SQ}whatchanged${SQ} is aliased to ${SQ}pack-redundant${SQ} - ${SQ}pack-redundant${SQ} is aliased to ${SQ}whatchanged${SQ} - fatal: alias loop detected: expansion of ${SQ}whatchanged${SQ} does not terminate: - whatchanged <== - pack-redundant ==> + ${SQ}$deprecated1${SQ} is aliased to ${SQ}$deprecated2${SQ} + ${SQ}$deprecated2${SQ} is aliased to ${SQ}$deprecated1${SQ} + fatal: alias loop detected: expansion of ${SQ}$deprecated1${SQ} does not terminate: + $deprecated1 <== + $deprecated2 ==> EOF - test_must_fail git whatchanged -h 2>actual && + test_must_fail git $deprecated1 -h 2>actual && test_cmp expect actual ' @@ -86,12 +94,12 @@ test_expect_success 'can alias-shadow deprecated builtins' ' done ' -test_expect_success 'can alias-shadow via two deprecated builtins' ' +test_expect_success HAVE_DEPRECATED 'can alias-shadow via two deprecated builtins' ' # some git(1) commands will fail... (see above) test_might_fail git status -h >expect && test_file_not_empty expect && - test_might_fail git -c alias.whatchanged=pack-redundant \ - -c alias.pack-redundant=status whatchanged -h >actual && + test_might_fail git -c alias.$deprecated1=$deprecated2 \ + -c alias.$deprecated2=status $deprecated1 -h >actual && test_cmp expect actual ' diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 7f7807eb88e928..b5a56ff25d871c 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -930,7 +930,7 @@ test_expect_success 'deleting currently checked out branch fails' ' git worktree add -b my7 my7 && test_must_fail git -C my7 branch -d my7 && test_must_fail git branch -d my7 2>actual && - test_grep "^error: cannot delete branch .my7. used by worktree at " actual && + test_grep "^error: cannot delete branch '"'"'my7'"'"' used by worktree at '"'.*'\$"'" actual && rm -r my7 && git worktree prune ' @@ -941,7 +941,7 @@ test_expect_success 'deleting in-use branch fails' ' git -C my7 bisect start HEAD HEAD~2 && test_must_fail git -C my7 branch -d my7 && test_must_fail git branch -d my7 2>actual && - test_grep "^error: cannot delete branch .my7. used by worktree at " actual && + test_grep "^error: cannot delete branch '"'"'my7'"'"' used by worktree at '"'.*' for bisect\$"'" actual && rm -r my7 && git worktree prune ' diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh index 03e0714864c5fd..cb5c3a1cb5bc6f 100755 --- a/t/t3418-rebase-continue.sh +++ b/t/t3418-rebase-continue.sh @@ -134,6 +134,7 @@ test_expect_success '--skip after failed fixup cleans commit message' ' EOF : skip and continue && + test_config commit.status false && echo "cp \"\$1\" .git/copy.txt" | write_script copy-editor.sh && (test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) && @@ -145,7 +146,8 @@ test_expect_success '--skip after failed fixup cleans commit message' ' : now, let us ensure that "squash" is handled correctly && git reset --hard wants-fixup-3 && - test_must_fail env FAKE_LINES="1 squash 2 squash 1 squash 3 squash 1" \ + test_must_fail env \ + FAKE_LINES="1 squash 2 squash 1 squash 3 squash 1 squash 4 squash 1" \ git rebase -i HEAD~4 && : the second squash failed, but there are two more in the chain && @@ -171,6 +173,32 @@ test_expect_success '--skip after failed fixup cleans commit message' ' fixup 2 EOF + (test_set_editor "$PWD/copy-editor.sh" && + test_must_fail git rebase --skip) && + : not the final squash, no need to edit the commit message && + test_path_is_missing .git/copy.txt && + + : The first, third and fifth squashes succeeded, therefore: && + cat >expect <<-\EOF && + # This is a combination of 4 commits. + # This is the 1st commit message: + + wants-fixup + + # This is the commit message #2: + + fixup 1 + + # This is the commit message #3: + + fixup 2 + + # This is the commit message #4: + + fixup 3 + EOF + test_commit_message HEAD expect && + (test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) && test_commit_message HEAD <<-\EOF && wants-fixup @@ -178,12 +206,12 @@ test_expect_success '--skip after failed fixup cleans commit message' ' fixup 1 fixup 2 + + fixup 3 EOF : Final squash failed, but there was still a squash && - head -n1 .git/copy.txt >first-line && - test_grep "# This is a combination of 3 commits" first-line && - test_grep "# This is the commit message #3:" .git/copy.txt + test_cmp expect .git/copy.txt ' test_expect_success 'setup rerere database' ' diff --git a/t/t3437-rebase-fixup-options.sh b/t/t3437-rebase-fixup-options.sh index 5d306a476928b1..a4b2a631654f1c 100755 --- a/t/t3437-rebase-fixup-options.sh +++ b/t/t3437-rebase-fixup-options.sh @@ -186,6 +186,53 @@ test_expect_success 'multiple fixup -c opens editor once' ' test_commit_message HEAD expected-message ' +test_expect_success 'fixup -c is remembered after skipping final fixup' ' + test_when_finished "test_might_fail git rebase --abort" && + cat >todo <<-\EOF && + pick B + fixup -c A1 + fixup A3 + EOF + ( + set_fake_editor && + set_replace_editor todo && + test_must_fail git rebase -i A A && + git show && cat .git/rebase-merge/message-squash && + FAKE_COMMIT_AMEND=edited git rebase --skip + ) && + test_commit_message HEAD <<-\EOF + new subject + + new + body + + edited + EOF +' +test_expect_success 'fixup -c is remembered after skipping later fixup' ' + test_when_finished "test_might_fail git rebase --abort" && + cat >todo <<-\EOF && + pick B + fixup -c A1 + fixup A3 + fixup A2 + EOF + ( + set_fake_editor && + set_replace_editor todo && + test_must_fail git rebase -i A A && + FAKE_COMMIT_AMEND=edited git rebase --skip + ) && + test_commit_message HEAD <<-\EOF + new subject + + new + body + + edited + EOF +' + test_expect_success 'sequence squash, fixup & fixup -c gives combined message' ' test_when_finished "test_might_fail git rebase --abort" && git checkout --detach A3 && diff --git a/t/t4045-diff-relative.sh b/t/t4045-diff-relative.sh index 2c8493fe66c441..167be0bdcce586 100755 --- a/t/t4045-diff-relative.sh +++ b/t/t4045-diff-relative.sh @@ -245,4 +245,13 @@ test_expect_failure 'diff --relative with change in subdir' ' test_cmp expected out ' +test_expect_success 'diff --relative --cached with change in subdir' ' + git switch br3 && + test_when_finished "git merge --abort" && + test_must_fail git merge sub1 && + echo file0 >expected && + git -C subdir diff --relative --name-only --cached >out && + test_cmp expected out +' + test_done diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index a8d691719da062..4238569b688a4c 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -23,6 +23,12 @@ test_xmllint () { fi } +test_maintenance_tasks () { + cat >expect && + sed -ne "s/.*\"region_enter\".*\"category\":\"maintenance\([^\"]*\)\".*\"label\":\"\([^\"][^\"]*\)\".*/\2\1/p" "$1" >actual && + test_cmp expect actual +} + test_lazy_prereq SYSTEMD_ANALYZE ' systemd-analyze verify /lib/systemd/system/basic.target ' @@ -180,8 +186,9 @@ test_expect_success 'maintenance..enabled' ' git config maintenance.gc.enabled false && git config maintenance.commit-graph.enabled true && GIT_TRACE2_EVENT="$(pwd)/run-config.txt" git maintenance run 2>err && - test_subcommand ! git gc --quiet ' ' @@ -189,16 +196,20 @@ test_expect_success 'run --task=' ' git maintenance run --task=commit-graph 2>/dev/null && GIT_TRACE2_EVENT="$(pwd)/run-gc.txt" \ git maintenance run --task=gc 2>/dev/null && - GIT_TRACE2_EVENT="$(pwd)/run-commit-graph.txt" \ - git maintenance run --task=commit-graph 2>/dev/null && GIT_TRACE2_EVENT="$(pwd)/run-both.txt" \ git maintenance run --task=commit-graph --task=gc 2>/dev/null && - test_subcommand ! git gc --quiet --no-detach --skip-foreground-tasks /dev/null && - test_subcommand git repack -d -l --geometric=2 \ - --quiet --write-midx packfiles && @@ -594,8 +612,8 @@ test_expect_success 'geometric repacking task' ' # The initial repack causes an all-into-one repack. GIT_TRACE2_EVENT="$(pwd)/initial-repack.txt" \ git maintenance run --task=geometric-repack 2>/dev/null && - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \ - --quiet --write-midx /dev/null && - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \ - --quiet --write-midx /dev/null && - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \ - --quiet --write-midx packs && test_line_count = 2 packs && ls .git/objects/pack/*.mtimes >cruft && @@ -742,7 +760,133 @@ test_expect_success 'geometric repacking honors configured split factor' ' test_geometric_repack_needed false splitFactor=2 && test_geometric_repack_needed true splitFactor=3 && - test_subcommand git repack -d -l --geometric=3 --quiet --write-midx >hook.log + EOF + + # Satisfy the auto condition for multiple tasks, both in the + # foreground and in the background phase. + git config set maintenance.reflog-expire.auto -1 && + git config set maintenance.geometric-repack.auto -1 && + git config set maintenance.rerere-gc.auto -1 && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run --auto 2>/dev/null && + + # The successful hook does not inhibit any of the tasks... + test_maintenance_tasks trace2.txt <<-\EOF && + reflog-expire foreground + geometric-repack + rerere-gc + EOF + # ... but it must only have been executed a single time. + test_line_count = 1 hook.log + ) +' + +test_expect_success 'pre-auto-gc hook can inhibit geometric strategy' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + write_script .git/hooks/pre-auto-gc <<-\EOF && + echo hook >>hook.log + exit 1 + EOF + + git config set maintenance.reflog-expire.auto -1 && + git config set maintenance.geometric-repack.auto -1 && + git config set maintenance.rerere-gc.auto -1 && + + # Maintenance would be required... + git maintenance is-needed --auto && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run --auto 2>/dev/null && + + # ... but the failing hook inhibits all tasks. The hook itself + # is expected to be the only child process being spawned, and + # it must only run a single time. + test_grep "child_start.*pre-auto-gc" trace2.txt && + test_maintenance_tasks trace2.txt <<-\EOF && + EOF + test_line_count = 1 hook.log + ) +' + +test_expect_success 'pre-auto-gc hook can inhibit gc strategy' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + write_script .git/hooks/pre-auto-gc <<-\EOF && + echo hook >>hook.log + exit 1 + EOF + + git config set maintenance.strategy gc && + git config set maintenance.auto false && + git config set gc.auto 3 && + + test_oid_init && + + # We need to create two objects whose hashes start with 17 + # since this is what the gc task counts. + test_commit "$(test_oid blob17_1)" && + test_commit "$(test_oid blob17_2)" && + + # Maintenance would be required... + git maintenance is-needed --auto && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run --auto 2>/dev/null && + + # ... but the failing hook inhibits all tasks. The hook itself + # is expected to be the only child process being spawned, and + # it must only run a single time. + test_grep "child_start.*pre-auto-gc" trace2.txt && + test_maintenance_tasks trace2.txt <<-\EOF && + EOF + test_subcommand_flex ! git trace2 && + test_line_count = 1 hook.log + ) +' + +test_expect_success 'pre-auto-gc hook does not run when no maintenance is needed' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + write_script .git/hooks/pre-auto-gc <<-\EOF && + echo hook >>hook.log + EOF + test_must_fail git maintenance is-needed --auto && + git maintenance run --auto 2>/dev/null && + test_path_is_missing hook.log + ) +' + +test_expect_success 'pre-auto-gc hook does not run without --auto' ' + test_when_finished "rm -rf repo" && + git init repo && + test_hook -C repo pre-auto-gc <<-\EOF && + echo hook >>hook.log + EOF + ( + cd repo && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git maintenance run 2>/dev/null && + test_grep "\[\"git\",\"repack\"," trace2.txt && + test_path_is_missing hook.log ) ' @@ -916,24 +1060,28 @@ test_expect_success '--schedule inheritance weekly -> daily -> hourly' ' GIT_TRACE2_EVENT="$(pwd)/hourly.txt" \ git maintenance run --schedule=hourly 2>/dev/null && - test_subcommand git prune-packed --quiet /dev/null && - test_subcommand git prune-packed --quiet /dev/null && - test_subcommand git prune-packed --quiet expect && rm -f trace2.txt && GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ git -c maintenance.strategy=$STRATEGY maintenance run --quiet "$@" && - sed -n 's/{"event":"child_start","sid":"[^/"]*",.*,"argv":\["\(.*\)\"]}/\1/p' actual - test_cmp expect actual + test_maintenance_tasks trace2.txt } test_expect_success 'maintenance.strategy is respected' ' @@ -1023,48 +1163,44 @@ test_expect_success 'maintenance.strategy is respected' ' test_grep "unknown maintenance strategy: .unknown." err && test_strategy incremental <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git gc --quiet --no-detach --skip-foreground-tasks + gc foreground + gc EOF test_strategy incremental --schedule=weekly <<-\EOF && - git pack-refs --all --prune - git prune-packed --quiet - git multi-pack-index write --no-progress - git multi-pack-index expire --no-progress - git multi-pack-index repack --no-progress --batch-size=1 - git commit-graph write --split --reachable --no-progress + pack-refs foreground + prefetch + loose-objects + incremental-repack + commit-graph EOF test_strategy gc <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git gc --quiet --no-detach --skip-foreground-tasks + gc foreground + gc EOF test_strategy gc --schedule=weekly <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git gc --quiet --no-detach --skip-foreground-tasks + gc foreground + gc EOF test_strategy geometric <<-\EOF && - git pack-refs --all --prune - git reflog expire --all - git repack -d -l --geometric=2 --quiet --write-midx - git commit-graph write --split --reachable --no-progress - git worktree prune --expire 3.months.ago - git rerere gc + pack-refs foreground + reflog-expire foreground + geometric-repack + commit-graph + worktree-prune + rerere-gc EOF test_strategy geometric --schedule=weekly <<-\EOF - git pack-refs --all --prune - git reflog expire --all - git repack -d -l --geometric=2 --quiet --write-midx - git commit-graph write --split --reachable --no-progress - git worktree prune --expire 3.months.ago - git rerere gc + pack-refs foreground + reflog-expire foreground + geometric-repack + commit-graph + worktree-prune + rerere-gc EOF ) '