diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 9c816018fe83d0..214ab1e87b0371 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -153,6 +153,16 @@ UI, Workflows & Features 'CHERRY_PICK_HEAD' ref. A test has also been added to ensure this behavior holds even when the operation stops for conflicts. + * The 'git imap-send' command has been taught to take the '--draft' + option to mark uploaded messages as drafts, which helps some email + clients render them properly for editing and sending. + + * The git rev-list command has been augmented with a '--missing-only' + option that filters the output to only show missing objects, + stripping the leading '?' character and suppressing present objects, + which is useful when used in combination with '--missing=print' or + '--missing=print-info'. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -528,6 +538,14 @@ Performance, Internal Implementation, Development Support etc. the cumulative time spent downloading external packs and the number of advertised URIs without emitting a separate event per pack. + * CGI helper scripts used by HTTP-related test scripts have been updated + to use atomic filesystem operations, preventing race conditions when + Apache handles concurrent requests. + + * "git maintenance" triggered "rerere gc" in unappropriate times and + interfered with "git rebase" etc. too much. The conditions "rerere + gc" gets triggered have been tweaked. + Fixes since v2.55 ----------------- @@ -859,6 +877,12 @@ Fixes since v2.55 expression syntax that breaks on older Perl versions. (merge 8a631963a9 ta/lint-gitlink-older-perl-fix later to maint). + * The autostash fallback in 'git checkout -m' has been refined to only + retry when there are local changes. Additionally, a blank line now + visually separates autostash conflict advice from the subsequent + branch-switch message. + (merge 2ba77ea828 hn/checkout-m-autostash-refine later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 026636128f ss/submittingpatches-typofix later to maint). (merge d2af22cc21 jc/rerere-doc-typofix later to maint). diff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc index c778ab09ace9bc..165102518cede4 100644 --- a/Documentation/config/maintenance.adoc +++ b/Documentation/config/maintenance.adoc @@ -122,10 +122,10 @@ maintenance.rerere-gc.auto:: This integer config option controls how often the `rerere-gc` task should be run as part of `git maintenance run --auto`. If zero, then the `rerere-gc` task will not run with the `--auto` option. A negative - value will force the task to run every time. Otherwise, any positive - value implies the command will run when the "rr-cache" directory exists - and has at least one entry, regardless of whether it is stale or not. - This heuristic may be refined in the future. The default value is 1. + value will force the task to run every time. Otherwise, a positive + value implies the command should run when the estimated number of stale + entries that would be pruned is greater than or equal to the configured + value. The default value is 512. maintenance.worktree-prune.auto:: This integer config option controls how often the `worktree-prune` task diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc index 1814d94491f750..cf415df45ad7a6 100644 --- a/Documentation/git-imap-send.adoc +++ b/Documentation/git-imap-send.adoc @@ -9,7 +9,7 @@ git-imap-send - Send a collection of patches from stdin to an IMAP folder SYNOPSIS -------- [synopsis] -git imap-send [-v] [-q] [--[no-]curl] [(--folder|-f) ] +git imap-send [-v] [-q] [--[no-]curl] [--[no-]draft] [(--folder|-f) ] git imap-send --list @@ -55,6 +55,13 @@ OPTIONS using libcurl. Ignored if Git was built with the NO_OPENSSL option set. +`--draft`:: +`--no-draft`:: + Mark uploaded messages with the IMAP `\Draft` flag. The default is `--no-draft`. ++ +With libcurl, `--draft` requires version 8.13.0 or later. +Older libcurl still uploads the message but cannot set the flag. + `--list`:: Run the IMAP LIST command to output a list of all the folders present. diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc index 50bb89f48362a4..fc6a9a008cf207 100644 --- a/Documentation/git-stash.adoc +++ b/Documentation/git-stash.adoc @@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[] :git-stash: 1 include::config/stash.adoc[] +EXIT STATUS +----------- + +The `git stash` subcommands exit with status 0 on success. The +subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`, +exit with status 1 when applying the stash entry resulted in conflicts, +in which case the stash entry is left in place, and with a non-zero +status other than 1 when they fail for other reasons. + SEE ALSO -------- diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index fd831f0ec64744..bd9f3456902c8f 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -1083,6 +1083,19 @@ If some tips passed to the traversal are missing, they will be considered as missing too, and the traversal will ignore them. In case we cannot get their Object ID though, an error will be raised. +`--missing-only`:: + When used together with `--missing=print` or `--missing=print-info`, + suppress all output for present objects and print only the missing + ones. The selected `--missing=` format is preserved (so + `--missing=print-info` still emits `path=` / `type=` fields), but the + leading ``?'' prefix used by the non-`-z` forms is omitted. This is + useful for scripting, as a simpler and faster alternative to + post-processing the output of `--missing=print`. ++ +This option is incompatible with `--count` and `--disk-usage`. +It is an error to use `--missing-only` without `--missing=print` or +`--missing=print-info`. + `--exclude-promisor-objects`:: (For internal use only.) Prefilter object traversal at promisor boundary. This is used with partial clone. This is diff --git a/builtin/checkout.c b/builtin/checkout.c index 2bc21aa49be05c..bbfd41efa4a84c 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1166,6 +1166,7 @@ static int switch_branches(const struct checkout_opts *opts, int flag, writeout_error = 0; int do_merge = 1; int created_autostash = 0; + enum stash_apply_result autostash_res = STASH_APPLY_CLEAN; struct strbuf old_commit_shortname = STRBUF_INIT; struct strbuf autostash_msg = STRBUF_INIT; const char *stash_label_base = NULL; @@ -1237,12 +1238,12 @@ static int switch_branches(const struct checkout_opts *opts, git_config_push_parameter(cfg.buf); strbuf_release(&cfg); } - apply_autostash_ref(the_repository, - "CHECKOUT_AUTOSTASH_HEAD", - new_branch_info->name, - "local", - stash_label_base, - autostash_msg.buf); + autostash_res = apply_autostash_ref(the_repository, + "CHECKOUT_AUTOSTASH_HEAD", + new_branch_info->name, + "local", + stash_label_base, + autostash_msg.buf); } if (ret) { branch_info_release(&old_branch_info); @@ -1255,6 +1256,8 @@ static int switch_branches(const struct checkout_opts *opts, if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit) orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit); + if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet) + fputc('\n', stderr); update_refs_for_switch(opts, &old_branch_info, new_branch_info); if (created_autostash) { diff --git a/builtin/gc.c b/builtin/gc.c index de2f9e7fed5b3e..57a3520263d7be 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -396,31 +396,15 @@ static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED, static int rerere_gc_condition(struct gc_config *cfg UNUSED) { - struct strbuf path = STRBUF_INIT; - int should_gc = 0, limit = 1; - DIR *dir = NULL; + int limit = 512; repo_config_get_int(the_repository, "maintenance.rerere-gc.auto", &limit); - if (limit <= 0) { - should_gc = limit < 0; - goto out; - } - - /* - * We skip garbage collection in case we either have no "rr-cache" - * directory or when it doesn't contain at least one entry. - */ - repo_git_path_replace(the_repository, &path, "rr-cache"); - dir = opendir(path.buf); - if (!dir) - goto out; - should_gc = !!readdir_skip_dot_and_dotdot(dir); + if (!limit) + return 0; /* never prune */ + if (limit < 0) + return 1; /* always prune */ -out: - strbuf_release(&path); - if (dir) - closedir(dir); - return should_gc; + return rerere_gc_needed(the_repository, (size_t)limit); } #define OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive) \ diff --git a/builtin/rev-list.c b/builtin/rev-list.c index 6b596231ab1ab0..0faa833facfd4b 100644 --- a/builtin/rev-list.c +++ b/builtin/rev-list.c @@ -111,6 +111,13 @@ enum missing_action { MA_ALLOW_PROMISOR, /* silently allow all missing PROMISOR objects */ }; static enum missing_action arg_missing_action; +static int arg_missing_only; + +static inline int should_collect_missing(void) +{ + return arg_missing_action == MA_PRINT || + arg_missing_action == MA_PRINT_INFO; +} /* display only the oid of each object encountered */ static int arg_show_object_names = 1; @@ -156,7 +163,14 @@ static void print_missing_object(struct missing_objects_map_entry *entry, { struct strbuf sb = STRBUF_INIT; - if (line_term) + /* + * --missing-only filters present objects out of the walk output. + * It still uses the selected --missing= format for missing ones, + * except the human "?" prefix is omitted (script-friendly OIDs). + */ + if (arg_missing_only && line_term) + printf("%s", oid_to_hex(&entry->entry.oid)); + else if (line_term) printf("?%s", oid_to_hex(&entry->entry.oid)); else printf("%s%cmissing=yes", oid_to_hex(&entry->entry.oid), @@ -246,6 +260,11 @@ static void show_commit(struct commit *commit, void *data) return; } + if (arg_missing_only) { + finish_commit(commit); + return; + } + if (show_disk_usage) total_disk_usage += get_object_disk_usage(&commit->object); @@ -384,6 +403,8 @@ static void show_object(struct object *obj, const char *name, void *cb_data) if (finish_object(obj, name, cb_data)) return; display_progress(progress, ++progress_counter); + if (arg_missing_only) + return; if (show_disk_usage) total_disk_usage += get_object_disk_usage(obj); if (info->flags & REV_LIST_QUIET) @@ -750,12 +771,17 @@ int cmd_rev_list(int argc, revs.exclude_promisor_objects = 1; } else if (skip_prefix(arg, "--missing=", &arg)) { parse_missing_action_value(repo, arg); + } else if (!strcmp(arg, "--missing-only")) { + arg_missing_only = 1; } else if (!strcmp(arg, "-z")) { line_term = '\0'; info_term = '\0'; } } + if (arg_missing_only && !should_collect_missing()) + die(_("--missing-only requires --missing=print or --missing=print-info")); + die_for_incompatible_opt2(revs.exclude_promisor_objects, "--exclude_promisor_objects", arg_missing_action, "--missing"); @@ -865,6 +891,9 @@ int cmd_rev_list(int argc, continue; } + if (!strcmp(arg, "--missing-only")) + continue; + usage(rev_list_usage); } @@ -911,6 +940,11 @@ int cmd_rev_list(int argc, (revs.left_right || revs.cherry_mark)) die(_("marked counting and '%s' cannot be used together"), "--objects"); + die_for_incompatible_opt2(arg_missing_only, "--missing-only", + revs.count, "--count"); + die_for_incompatible_opt2(arg_missing_only, "--missing-only", + show_disk_usage, "--disk-usage"); + save_commit_buffer = (revs.verbose_header || revs.grep_filter.pattern_list || revs.grep_filter.header_list); @@ -968,8 +1002,7 @@ int cmd_rev_list(int argc, if (arg_print_omitted) oidset_init(&omitted_objects, DEFAULT_OIDSET_SIZE); - if (arg_missing_action == MA_PRINT || - arg_missing_action == MA_PRINT_INFO) { + if (should_collect_missing()) { struct oidset_iter iter; struct object_id *oid; @@ -995,8 +1028,7 @@ int cmd_rev_list(int argc, printf("~%s\n", oid_to_hex(oid)); oidset_clear(&omitted_objects); } - if (arg_missing_action == MA_PRINT || - arg_missing_action == MA_PRINT_INFO) { + if (should_collect_missing()) { struct missing_objects_map_entry *entry; struct oidmap_iter iter; diff --git a/builtin/stash.c b/builtin/stash.c index 72c52571f8c06c..7a9843413b11e2 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -10,6 +10,7 @@ #include "object-name.h" #include "parse-options.h" #include "refs.h" +#include "stash.h" #include "lockfile.h" #include "cache-tree.h" #include "unpack-trees.h" @@ -640,10 +641,12 @@ static void unstage_changes_unless_new(struct object_id *orig_tree) die(_("could not write index")); } -static int do_apply_stash(const char *prefix, struct stash_info *info, - int index, int quiet, - const char *label_ours, const char *label_theirs, - const char *label_base) +static enum stash_apply_result do_apply_stash(const char *prefix, + struct stash_info *info, + int index, int quiet, + const char *label_ours, + const char *label_theirs, + const char *label_base) { int clean, ret; int has_index = index; @@ -717,8 +720,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, /* * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the - * merge was clean, and nonzero if the merge was unclean or encountered - * an error. + * merge was clean, and 1 if the merge was unclean or a negative value + * if it encountered an error. */ ret = clean >= 0 ? !clean : clean; @@ -2492,10 +2495,22 @@ int cmd_stash(int argc, strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file, (uintmax_t)pid); - if (fn) - return !!fn(argc, argv, prefix, repo); - else if (!argc) + if (fn) { + ret = fn(argc, argv, prefix, repo); + + /* + * The subcommand implementations return 0 on success, a + * negative value on failure, and STASH_APPLY_CONFLICT + * when applying a stash entry resulted in conflicts. + * Map failures to 128, the status die() uses, so that + * exit status 1 unambiguously indicates conflicts. + */ + if (ret < 0) + return 128; + return ret; + } else if (!argc) { return !!push_stash_unassumed(0, NULL, prefix, repo); + } /* Assume 'stash push' */ strvec_push(&args, "push"); diff --git a/git-curl-compat.h b/git-curl-compat.h index dccdd4d6e54158..032aaf7126c977 100644 --- a/git-curl-compat.h +++ b/git-curl-compat.h @@ -67,4 +67,12 @@ #define GIT_CURL_HAVE_CURLOPT_TCP_KEEPCNT #endif +/** + * CURLOPT_UPLOAD_FLAGS and CURLULFLAG_* were added in 8.13.0, + * released in April 2025. + */ +#if LIBCURL_VERSION_NUM >= 0x080D00 +#define GIT_CURL_HAVE_CURLOPT_UPLOAD_FLAGS +#endif + #endif diff --git a/imap-send.c b/imap-send.c index 0d16d02029232b..bf1d2cf74d6d42 100644 --- a/imap-send.c +++ b/imap-send.c @@ -35,6 +35,7 @@ #include "setup.h" #include "strbuf.h" #ifdef USE_CURL_FOR_IMAP_SEND +#include "git-curl-compat.h" #include "http.h" #endif @@ -49,10 +50,11 @@ static int verbosity; static int list_folders; static int use_curl = USE_CURL_DEFAULT; +static int opt_draft; static char *opt_folder; static char const * const imap_send_usage[] = { - N_("git imap-send [-v] [-q] [--[no-]curl] [(--folder|-f) ] < "), + N_("git imap-send [-v] [-q] [--[no-]curl] [--[no-]draft] [(--folder|-f) ] < "), "git imap-send --list", NULL }; @@ -60,6 +62,7 @@ static char const * const imap_send_usage[] = { static struct option imap_send_options[] = { OPT__VERBOSITY(&verbosity), OPT_BOOL(0, "curl", &use_curl, "use libcurl to communicate with the IMAP server"), + OPT_BOOL(0, "draft", &opt_draft, "mark uploaded messages with the IMAP \\Draft flag"), OPT_STRING('f', "folder", &opt_folder, "folder", "specify the IMAP folder"), OPT_BOOL(0, "list", &list_folders, "list all folders on the IMAP server"), OPT_END() @@ -1416,7 +1419,8 @@ static int imap_store_msg(struct imap_store *ctx, struct strbuf *msg) box = ctx->name; prefix = !strcmp(box, "INBOX") ? "" : ctx->prefix; - ret = imap_exec_m(ctx, &cb, "APPEND \"%s%s\" ", prefix, box); + ret = imap_exec_m(ctx, &cb, "APPEND \"%s%s\" %s", prefix, box, + opt_draft ? "(\\Draft) " : ""); imap->caps = imap->rcaps; if (ret != DRV_OK) return ret; @@ -1718,6 +1722,13 @@ static int curl_append_msgs_to_imap(struct imap_server_conf *server, curl_easy_setopt(curl, CURLOPT_READDATA, &msgbuf); + if (opt_draft) { +#ifdef GIT_CURL_HAVE_CURLOPT_UPLOAD_FLAGS + curl_easy_setopt(curl, CURLOPT_UPLOAD_FLAGS, CURLULFLAG_DRAFT); +#else + warning("--draft requires libcurl 8.13.0 or later"); +#endif + } fprintf(stderr, "Sending %d message%s to %s folder...\n", total, (total != 1) ? "s" : "", server->folder); while (1) { diff --git a/rerere.c b/rerere.c index 3d3bd0db16a737..1c3745d9e3279a 100644 --- a/rerere.c +++ b/rerere.c @@ -1173,22 +1173,44 @@ static void unlink_rr_item(struct rerere_id *id) strbuf_release(&buf); } -static void prune_one(struct rerere_id *id, - timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve) +static void rerere_gc_cutoffs(struct repository *r, + timestamp_t *cutoff_resolve, + timestamp_t *cutoff_noresolve) +{ + timestamp_t now = time(NULL); + + if (repo_config_get_expiry_in_days(r, "gc.rerereresolved", + cutoff_resolve, now)) + *cutoff_resolve = now - 60 * 86400; + if (repo_config_get_expiry_in_days(r, "gc.rerereunresolved", + cutoff_noresolve, now)) + *cutoff_noresolve = now - 15 * 86400; +} + +static bool rerere_id_is_stale(struct rerere_id *id, + timestamp_t cutoff_resolve, + timestamp_t cutoff_noresolve) { timestamp_t then; timestamp_t cutoff; then = rerere_last_used_at(id); - if (then) + if (then) { cutoff = cutoff_resolve; - else { + } else { then = rerere_created_at(id); if (!then) - return; + return false; cutoff = cutoff_noresolve; } - if (then < cutoff) + + return then < cutoff; +} + +static void prune_one(struct rerere_id *id, + timestamp_t cutoff_resolve, timestamp_t cutoff_noresolve) +{ + if (rerere_id_is_stale(id, cutoff_resolve, cutoff_noresolve)) unlink_rr_item(id); } @@ -1200,24 +1222,70 @@ static int is_rr_cache_dirname(const char *path) return !parse_oid_hex(path, &oid, &end) && !*end; } +bool rerere_gc_needed(struct repository *r, size_t limit) +{ + timestamp_t cutoff_resolve, cutoff_noresolve; + struct strbuf buf = STRBUF_INIT; + bool needed = false; + struct dirent *e; + size_t count = 0; + DIR *dir; + + dir = opendir(repo_git_path_replace(r, &buf, "rr-cache")); + if (!dir) + goto out; + + rerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve); + + while ((e = readdir_skip_dot_and_dotdot(dir))) { + struct rerere_id id; + + /* + * We estimate the number of stale entries by only considering + * those starting with "17". This is the same strategy that we + * use for estimating the number of loose objects. + */ + if (!starts_with(e->d_name, "17") || + !is_rr_cache_dirname(e->d_name)) + continue; + + id.collection = find_rerere_dir(e->d_name); + for (id.variant = 0; + id.variant < id.collection->status_nr; + id.variant++) { + if (rerere_id_is_stale(&id, cutoff_resolve, + cutoff_noresolve)) { + count += 256; + if (count >= limit) { + needed = true; + goto out; + } + } + } + } + +out: + if (dir) + closedir(dir); + free_rerere_dirs(); + strbuf_release(&buf); + return needed; +} + void rerere_gc(struct repository *r, struct string_list *rr) { struct string_list to_remove = STRING_LIST_INIT_DUP; DIR *dir; struct dirent *e; int i; - timestamp_t now = time(NULL); - timestamp_t cutoff_noresolve = now - 15 * 86400; - timestamp_t cutoff_resolve = now - 60 * 86400; + timestamp_t cutoff_noresolve; + timestamp_t cutoff_resolve; struct strbuf buf = STRBUF_INIT; if (setup_rerere(r, rr, 0) < 0) return; - repo_config_get_expiry_in_days(the_repository, "gc.rerereresolved", - &cutoff_resolve, now); - repo_config_get_expiry_in_days(the_repository, "gc.rerereunresolved", - &cutoff_noresolve, now); + rerere_gc_cutoffs(r, &cutoff_resolve, &cutoff_noresolve); repo_config(the_repository, git_default_config, NULL); dir = opendir(repo_git_path_replace(the_repository, &buf, "rr-cache")); if (!dir) diff --git a/rerere.h b/rerere.h index d4b5f7c932006a..feeb0e2c9fe61b 100644 --- a/rerere.h +++ b/rerere.h @@ -39,6 +39,12 @@ int rerere_remaining(struct repository *, struct string_list *); void rerere_clear(struct repository *, struct string_list *); void rerere_gc(struct repository *, struct string_list *); +/* + * Check whether garbage collection for rerere entries is needed, which is + * the case when there's at least `limit` stale entries that would be pruned. + */ +bool rerere_gc_needed(struct repository *r, size_t limit); + #define OPT_RERERE_AUTOUPDATE(v) OPT_UYN(0, "rerere-autoupdate", (v), \ N_("update the index with reused conflict resolution if possible")) diff --git a/sequencer.c b/sequencer.c index 6a28e0dd9af97e..3aa6bb16e7a9e1 100644 --- a/sequencer.c +++ b/sequencer.c @@ -19,6 +19,7 @@ #include "commit.h" #include "sequencer.h" #include "run-command.h" +#include "stash.h" #include "hook.h" #include "utf8.h" #include "cache-tree.h" @@ -4801,31 +4802,50 @@ void create_autostash_ref(struct repository *r, const char *refname, create_autostash_internal(r, NULL, refname, message, silent); } -static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, - const char *label_ours, const char *label_theirs, - const char *label_base, - const char *stash_msg) +static enum stash_apply_result do_stash_apply(const char *stash_oid, + const char *label_ours, + const char *label_theirs, + const char *label_base) { struct child_process child = CHILD_PROCESS_INIT; - int ret = 0; - if (attempt_apply) { - child.git_cmd = 1; - child.no_stdout = 1; - child.no_stderr = 1; - strvec_push(&child.args, "stash"); - strvec_push(&child.args, "apply"); - if (label_ours) - strvec_pushf(&child.args, "--label-ours=%s", label_ours); - if (label_theirs) - strvec_pushf(&child.args, "--label-theirs=%s", label_theirs); - if (label_base) - strvec_pushf(&child.args, "--label-base=%s", label_base); - strvec_push(&child.args, stash_oid); - ret = run_command(&child); - } - - if (attempt_apply && !ret) + child.git_cmd = 1; + child.no_stdout = 1; + child.no_stderr = 1; + strvec_push(&child.args, "stash"); + strvec_push(&child.args, "apply"); + if (label_ours) + strvec_pushf(&child.args, "--label-ours=%s", label_ours); + if (label_theirs) + strvec_pushf(&child.args, "--label-theirs=%s", label_theirs); + if (label_base) + strvec_pushf(&child.args, "--label-base=%s", label_base); + strvec_push(&child.args, stash_oid); + + switch (run_command(&child)) { + case 0: + return STASH_APPLY_CLEAN; + case STASH_APPLY_CONFLICT: + return STASH_APPLY_CONFLICT; + default: + return STASH_APPLY_ERROR; + } +} + +static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid, + int attempt_apply, + const char *label_ours, + const char *label_theirs, + const char *label_base, + const char *stash_msg) +{ + enum stash_apply_result ret = STASH_APPLY_CLEAN; + + if (attempt_apply) + ret = do_stash_apply(stash_oid, label_ours, label_theirs, + label_base); + + if (attempt_apply && ret == STASH_APPLY_CLEAN) fprintf(stderr, _("Applied autostash.\n")); else { struct child_process store = CHILD_PROCESS_INIT; @@ -4839,13 +4859,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, strvec_push(&store.args, stash_oid); if (run_command(&store)) ret = error(_("cannot store %s"), stash_oid); - else if (attempt_apply) + else if (attempt_apply && ret == STASH_APPLY_CONFLICT) fprintf(stderr, _("Your local changes are stashed, however applying them\n" "resulted in conflicts. You can either resolve the conflicts\n" "and then discard the stash with \"git stash drop\", or, if you\n" "do not want to resolve them now, run \"git reset --hard\" and\n" "apply the local changes later by running \"git stash pop\".\n")); + else if (attempt_apply) + ret = error(_("could not apply autostash; " + "your changes are safe in the stash")); else fprintf(stderr, _("Autostash exists; creating a new stash entry.\n" @@ -4857,15 +4880,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, return ret; } -static int apply_save_autostash(const char *path, int attempt_apply) +static enum stash_apply_result apply_save_autostash(const char *path, + int attempt_apply) { struct strbuf stash_oid = STRBUF_INIT; - int ret = 0; + enum stash_apply_result ret = STASH_APPLY_CLEAN; if (!read_oneliner(&stash_oid, path, READ_ONELINER_SKIP_IF_EMPTY)) { strbuf_release(&stash_oid); - return 0; + return STASH_APPLY_CLEAN; } strbuf_trim(&stash_oid); @@ -4877,37 +4901,40 @@ static int apply_save_autostash(const char *path, int attempt_apply) return ret; } -int save_autostash(const char *path) +enum stash_apply_result save_autostash(const char *path) { return apply_save_autostash(path, 0); } -int apply_autostash(const char *path) +enum stash_apply_result apply_autostash(const char *path) { return apply_save_autostash(path, 1); } -int apply_autostash_oid(const char *stash_oid) +enum stash_apply_result apply_autostash_oid(const char *stash_oid) { return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL); } -static int apply_save_autostash_ref(struct repository *r, const char *refname, - int attempt_apply, - const char *label_ours, const char *label_theirs, - const char *label_base, - const char *stash_msg) +static enum stash_apply_result apply_save_autostash_ref(struct repository *r, + const char *refname, + int attempt_apply, + const char *label_ours, + const char *label_theirs, + const char *label_base, + const char *stash_msg) { struct object_id stash_oid; char stash_oid_hex[GIT_MAX_HEXSZ + 1]; - int flag, ret; + int flag; + enum stash_apply_result ret; if (!refs_ref_exists(get_main_ref_store(r), refname)) - return 0; + return STASH_APPLY_CLEAN; if (!refs_resolve_ref_unsafe(get_main_ref_store(r), refname, RESOLVE_REF_READING, &stash_oid, &flag)) - return -1; + return STASH_APPLY_ERROR; if (flag & REF_ISSYMREF) return error(_("autostash reference is a symref")); @@ -4922,15 +4949,19 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname, return ret; } -int save_autostash_ref(struct repository *r, const char *refname) +enum stash_apply_result save_autostash_ref(struct repository *r, + const char *refname) { return apply_save_autostash_ref(r, refname, 0, NULL, NULL, NULL, NULL); } -int apply_autostash_ref(struct repository *r, const char *refname, - const char *label_ours, const char *label_theirs, - const char *label_base, const char *stash_msg) +enum stash_apply_result apply_autostash_ref(struct repository *r, + const char *refname, + const char *label_ours, + const char *label_theirs, + const char *label_base, + const char *stash_msg) { return apply_save_autostash_ref(r, refname, 1, label_ours, label_theirs, label_base, diff --git a/sequencer.h b/sequencer.h index 61ebc2ca40e6d2..df9d0d189f921a 100644 --- a/sequencer.h +++ b/sequencer.h @@ -3,6 +3,7 @@ #include "strbuf.h" #include "strvec.h" +#include "stash.h" #include "wt-status.h" struct commit; @@ -231,13 +232,17 @@ void commit_post_rewrite(struct repository *r, void create_autostash(struct repository *r, const char *path); void create_autostash_ref(struct repository *r, const char *refname, const char *message, bool silent); -int save_autostash(const char *path); -int save_autostash_ref(struct repository *r, const char *refname); -int apply_autostash(const char *path); -int apply_autostash_oid(const char *stash_oid); -int apply_autostash_ref(struct repository *r, const char *refname, - const char *label_ours, const char *label_theirs, - const char *label_base, const char *stash_msg); +enum stash_apply_result save_autostash(const char *path); +enum stash_apply_result save_autostash_ref(struct repository *r, + const char *refname); +enum stash_apply_result apply_autostash(const char *path); +enum stash_apply_result apply_autostash_oid(const char *stash_oid); +enum stash_apply_result apply_autostash_ref(struct repository *r, + const char *refname, + const char *label_ours, + const char *label_theirs, + const char *label_base, + const char *stash_msg); #define SUMMARY_INITIAL_COMMIT (1 << 0) #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1) diff --git a/stash.h b/stash.h new file mode 100644 index 00000000000000..14ba4f946d9881 --- /dev/null +++ b/stash.h @@ -0,0 +1,21 @@ +#ifndef STASH_H +#define STASH_H + +enum stash_apply_result { + /* The stash was applied cleanly, or there was nothing to apply. */ + STASH_APPLY_CLEAN = 0, + + /* + * The stash could not be applied because it resulted in + * conflicts. The stash entry is left in place. The "git stash + * apply", "pop" and "branch" subcommands exit with this status + * in this case, mirroring the convention of "git merge-tree" and + * the merge strategies. + */ + STASH_APPLY_CONFLICT = 1, + + /* Something went wrong. */ + STASH_APPLY_ERROR = -1, +}; + +#endif /* STASH_H */ diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh index a216e5376fd7de..115455784c8fd6 100644 --- a/t/lib-httpd.sh +++ b/t/lib-httpd.sh @@ -159,6 +159,18 @@ prepare_httpd() { mkdir -p "$HTTPD_DOCUMENT_ROOT_PATH" cp "$TEST_PATH"/passwd "$HTTPD_ROOT_PATH" cp "$TEST_PATH"/proxy-passwd "$HTTPD_ROOT_PATH" + # Apache can run the following scripts concurrently per request. Make + # sure any state management logic is resilient to race conditions. + # + # For example: + # - use "mkdir dir" to ensure only one request "succeeds" under some + # condition (see http-429.sh). + # - chain (&&) atomic operations like "rm marker" (no -f) with the + # logic that is guarded by the marker instead of relying on a + # separate "test -f" and "rm marker" check + # (see apply-one-time-script.sh). + # - use scratch file names that include the process ID ($$), so + # concurrent requests do not overwrite each other's state. install_script incomplete-length-upload-pack-v2-http.sh install_script incomplete-body-upload-pack-v2-http.sh install_script error-no-report.sh diff --git a/t/lib-httpd/apply-one-time-script.sh b/t/lib-httpd/apply-one-time-script.sh index b1682944e280e2..eac21a3a8e739a 100644 --- a/t/lib-httpd/apply-one-time-script.sh +++ b/t/lib-httpd/apply-one-time-script.sh @@ -6,21 +6,31 @@ # # This can be used to simulate the effects of the repository changing in # between HTTP request-response pairs. -if test -f one-time-script -then - LC_ALL=C - export LC_ALL +test -f one-time-script || exec "$GIT_EXEC_PATH/git-http-backend" + +LC_ALL=C +export LC_ALL - "$GIT_EXEC_PATH/git-http-backend" >out - ./one-time-script out >out_modified +out=out.$$ +modified=out-modified.$$ +"$GIT_EXEC_PATH/git-http-backend" >"$out" - if cmp -s out out_modified - then - cat out - else - cat out_modified - rm one-time-script - fi +# Since Apache can execute this script for multiple requests +# concurrently, we chain "rm one-time-script" with the logic +# for generating a modified response. If the "rm" ran separately, +# a concurrent request could pass the "test -f" above and +# erroneously result in multiple modified responses or an empty +# body depending on the race state. +# +# We discard stderr for ./one-time-script since it is possible +# ./one-time-script has been removed already, which is expected +# sometimes. In this case, the unmodified response will be returned. +if ./one-time-script "$out" 2>/dev/null >"$modified" && + ! cmp -s "$out" "$modified" && + rm one-time-script 2>/dev/null +then + cat "$modified" else - "$GIT_EXEC_PATH/git-http-backend" + cat "$out" fi +rm -f "$out" "$modified" diff --git a/t/lib-httpd/http-429.sh b/t/lib-httpd/http-429.sh index c97b16145b7f92..1a5d7987db1fac 100644 --- a/t/lib-httpd/http-429.sh +++ b/t/lib-httpd/http-429.sh @@ -3,7 +3,7 @@ # Script to return HTTP 429 Too Many Requests responses for testing retry logic. # Usage: /http_429/// # -# The test-context is a unique identifier for each test to isolate state files. +# The test-context is a unique identifier for each test to isolate state directories. # The retry-after-value can be: # - A number (e.g., "1", "2", "100") - sets Retry-After header to that many seconds # - "none" - no Retry-After header @@ -26,14 +26,16 @@ repo_path="${remaining#*/}" # Get rest (repo path) # The repo name is the first component before any "/" repo_name="${repo_path%%/*}" -# Use current directory (HTTPD_ROOT_PATH) for state file -# Create a safe filename from test_context, retry_after and repo_name -# This ensures all requests for the same test context share the same state file +# Use current directory (HTTPD_ROOT_PATH) to hold state directory +# Create a safe directory name from test_context, retry_after and repo_name +# This ensures all requests for the same test context share the same state directory safe_name=$(echo "${test_context}-${retry_after}-${repo_name}" | tr '/' '_' | tr -cd 'a-zA-Z0-9_-') -state_file="http-429-state-${safe_name}" +state="http-429-state-${safe_name}" -# Check if this is the first call (no state file exists) -if test -f "$state_file" +# Check if this is the first call (no state directory exists), or if +# the retry-after-value is "permanent", which indicates a 429 must be +# returned for every request (even if the state directory exists). +if test "$retry_after" != permanent && ! mkdir "$state" 2>/dev/null then # Already returned 429 once, forward to git-http-backend # Set PATH_INFO to just the repo path (without retry-after value) @@ -52,9 +54,6 @@ then exec "$GIT_EXEC_PATH/git-http-backend" fi -# Mark that we've returned 429 -touch "$state_file" - # Output HTTP 429 response printf "Status: 429 Too Many Requests\r\n" @@ -67,8 +66,7 @@ case "$retry_after" in printf "Retry-After: invalid-format-123abc\r\n" ;; permanent) - # Always return 429, don't set state file for success - rm -f "$state_file" + # Always return 429 printf "Retry-After: 1\r\n" printf "Content-Type: text/plain\r\n" printf "\r\n" diff --git a/t/meson.build b/t/meson.build index 7f53cca7d1f891..3ca7b271040a62 100644 --- a/t/meson.build +++ b/t/meson.build @@ -717,6 +717,7 @@ integration_tests = [ 't5564-http-proxy.sh', 't5565-push-multiple.sh', 't5566-push-group.sh', + 't5567-one-time-script.sh', 't5570-git-daemon.sh', 't5571-pre-push-hook.sh', 't5572-pull-submodule.sh', diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 260c809f994bc6..721158606f25eb 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -1808,13 +1808,13 @@ test_expect_success 'stash.index=false overridden by --index' ' test_cmp expect file ' -test_expect_success 'apply with custom conflict labels' ' +test_expect_success 'apply exits 1 on conflicts' ' git reset --hard initial && test_commit label-base conflict-file base-content && echo stashed >conflict-file && git stash push -m "stashed" && test_commit label-upstream conflict-file upstream-content && - test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH && + test_expect_code 1 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH && test_grep "^<<<<<<< UP" conflict-file && test_grep "^||||||| Stash base" conflict-file && test_grep "^>>>>>>> STASH" conflict-file @@ -1826,11 +1826,30 @@ test_expect_success 'apply with empty conflict labels' ' echo stashed >conflict-file && git stash push -m "stashed" && test_commit empty-label-upstream conflict-file upstream-content && - test_must_fail git stash apply --label-ours= --label-theirs= && + test_expect_code 1 git stash apply --label-ours= --label-theirs= && test_grep "^<<<<<<<$" conflict-file && test_grep "^>>>>>>>$" conflict-file ' +test_expect_success 'pop exits 1 on conflicts and keeps the stash entry' ' + git reset --hard initial && + echo stashed >file && + git stash push -m pop-stashed && + test_commit pop-upstream file upstream-content && + test_expect_code 1 git stash pop && + git stash list >list && + test_grep pop-stashed list +' + +test_expect_success 'stash branch exits with a non-1 status on errors' ' + git reset --hard initial && + echo stashed >file && + git stash push -m branch-stashed && + test_expect_code 128 git stash branch conflicting-branch refs/heads/does-not-exist && + git stash list >list && + test_grep branch-stashed list +' + test_expect_success 'stash show --include-untracked includes untracked files' ' git reset --hard && diff --git a/t/t5567-one-time-script.sh b/t/t5567-one-time-script.sh new file mode 100755 index 00000000000000..a8429ef3c3d092 --- /dev/null +++ b/t/t5567-one-time-script.sh @@ -0,0 +1,96 @@ +#!/bin/sh + +test_description='apply-one-time-script CGI helper is safe under concurrent requests' + +. ./test-lib.sh + +HELPER="$TEST_DIRECTORY/lib-httpd/apply-one-time-script.sh" + +test_expect_success PIPE 'helper only serves one rewritten response for concurrent requests' ' + mkdir workdir fakebin && + ENTERED="$PWD/entered" && + GATE="$PWD/gate" && + export ENTERED GATE && + mkfifo "$ENTERED" "$GATE" && + + # A stub git-http-backend that returns a response based on + # $ROLE. For $ROLE = modify, return the response string + # "packfile", which ends up being modified by the example + # one-time-script below. + # + # Otherwise, run the branch returning a response that + # should be passed through, and block until released + # by "read -r $GATE". + write_script fakebin/git-http-backend <<-\EOF && + printf "Status: 200 OK\r\n" + printf "Content-Type: application/x-git-result\r\n" + printf "\r\n" + if test "$ROLE" = modify + then + printf "packfile\n" + else + echo entered >"$ENTERED" + read -r released <"$GATE" + printf "refs\n" + fi + EOF + + # An example one-time-script for apply-one-time-script + # to execute. Checks for "packfile" in the response + # that will be returned, and replaces it with a + # modified response. Passes through responses without + # "packfile" in them. + write_script workdir/one-time-script <<-\EOF && + if grep packfile "$1" >/dev/null + then + sed "/packfile/q" "$1" && + printf "REPLACED\n" + else + cat "$1" + fi + EOF + + GIT_EXEC_PATH="$PWD/fakebin" && + export GIT_EXEC_PATH && + + # Ensure $GATE has a reader so the test does not block indefinitely if + # the helper is buggy and "echo released >&9" below does not unblock + # the unmodified response gate. + exec 9<>"$GATE" && + + # Launch the passthrough request in the background. Record its pid + # so it can be killed when the test finishes if, for some reason, the + # request stays blocked and would stall a test runner. + { ( + cd workdir && + ROLE=passthrough sh "$HELPER" >../passthrough.out 2>../passthrough.err + ) & } && + passthrough_pid=$! && + test_when_finished "kill $passthrough_pid 2>/dev/null || :" && + + # Wait until the passthrough request is "in-flight" and paused + # mid-response. + read -r entered <"$ENTERED" && + + # Launch the request for a modified response while the passthrough + # request is concurrently "in-flight" and paused. + ( + cd workdir && + ROLE=modify sh "$HELPER" >../modify.out 2>../modify.err + ) && + + # Unblock the passthrough request, allowing git-http-backend to + # complete its response. + echo released >&9 && + { wait "$passthrough_pid" || :; } && + + test_must_be_empty passthrough.err && + test_must_be_empty modify.err && + test_grep "Status: 200 OK" passthrough.out && + test_grep "Status: 200 OK" modify.out && + test_grep REPLACED modify.out && + test_grep ! REPLACED passthrough.out && + test_grep refs passthrough.out +' + +test_done diff --git a/t/t6022-rev-list-missing.sh b/t/t6022-rev-list-missing.sh index 1e472a45afa21f..1bd2c3bc4f8075 100755 --- a/t/t6022-rev-list-missing.sh +++ b/t/t6022-rev-list-missing.sh @@ -198,6 +198,55 @@ do ' done +for obj in "HEAD~1" "HEAD~1^{tree}" "HEAD:1.t" +do + test_expect_success "rev-list --missing-only with missing $obj" ' + oid="$(git rev-parse $obj)" && + path=".git/objects/$(test_oid_to_path $oid)" && + + mv "$path" "$path.hidden" && + test_when_finished "mv $path.hidden $path" && + + git rev-list --missing=print --missing-only --objects \ + --no-object-names HEAD >actual && + + echo $oid >expect && + test_cmp expect actual + ' +done + +test_expect_success "--missing-only requires --missing=print or --missing=print-info" ' + test_must_fail git rev-list --missing-only --objects HEAD 2>err && + test_grep "requires --missing=print" err +' + +test_expect_success "--missing-only is incompatible with --count" ' + test_must_fail git rev-list --missing=print --missing-only \ + --count --objects HEAD 2>err && + test_grep "cannot be used together" err +' + +test_expect_success "--missing-only is incompatible with --disk-usage" ' + test_must_fail git rev-list --missing=print --missing-only \ + --disk-usage --objects HEAD 2>err && + test_grep "cannot be used together" err +' + +test_expect_success "--missing-only works with --missing=print-info" ' + oid="$(git rev-parse HEAD:1.t)" && + path=".git/objects/$(test_oid_to_path $oid)" && + + mv "$path" "$path.hidden" && + test_when_finished "mv $path.hidden $path" && + + git rev-list --missing=print-info --missing-only --objects \ + --no-object-names HEAD >actual && + + # Filter keeps print-info fields; only the "?" prefix is dropped. + echo "$oid path=1.t type=blob" >expect && + test_cmp expect actual +' + test_expect_success "-z nul-delimited --missing" ' test_when_finished rm -rf repo && diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 0ddd1ad7aab06a..9ea9462914f74b 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' ' test_must_fail git checkout side 2>stderr && test_grep "Your local changes" stderr && git checkout -m side >actual 2>&1 && - test_grep "resulted in conflicts" actual && - test_grep "git stash drop" actual && - test_grep "git stash pop" actual && - test_grep "The following paths have local changes" actual && + cat >expect <<-EOF && + Your local changes are stashed, however applying them + resulted in conflicts. You can either resolve the conflicts + and then discard the stash with "git stash drop", or, if you + do not want to resolve them now, run "git reset --hard" and + apply the local changes later by running "git stash pop". + + Switched to branch ${SQ}side${SQ} + The following paths have local changes: + M one + EOF + test_cmp expect actual && git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual && sed /^index/d actual >actual.trimmed && cat >expect <<-EOF && diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 5fbb16f0f0e59c..4f65fa9439c0b8 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -1016,37 +1016,70 @@ test_expect_success 'rerere-gc task without --auto always collects garbage' ' test_expect_rerere_gc git maintenance run --task=rerere-gc ' -test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' ' +test_expect_success 'rerere-gc task with --auto only prunes with stale entries' ' test_when_finished "rm -rf .git/rr-cache" && + entry_1=.git/rr-cache/171$(echo $ZERO_OID | cut -c4-) && + entry_2=.git/rr-cache/172$(echo $ZERO_OID | cut -c4-) && + entry_3=.git/rr-cache/173$(echo $ZERO_OID | cut -c4-) && + + # Without the "rr-cache" directory there is nothing to prune. ! git maintenance is-needed --auto --task=rerere-gc && test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc && - mkdir .git/rr-cache && + + # Fresh unresolved entries are not stale. + for e in $entry_1 $entry_2 $entry_3 + do + mkdir -p $e && + echo preimage >$e/preimage || return 1 + done && ! git maintenance is-needed --auto --task=rerere-gc && test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc && - : >.git/rr-cache/entry && + + # Entries are sampled using the "17" prefix, so we scale up the + # estimate by 256. A single entry is not sufficient to reach the + # default limit of 512. + test-tool chmtime =-$((16 * 86400)) $entry_1/preimage && + ! git maintenance is-needed --auto --task=rerere-gc && + + # A second prunable entry will reach the limit though and will thus get + # pruned. + test-tool chmtime =-$((16 * 86400)) $entry_2/preimage && git maintenance is-needed --auto --task=rerere-gc && - test_expect_rerere_gc git maintenance run --auto --task=rerere-gc + + # The prunable entries are gone, the other one remains. + test_expect_rerere_gc git maintenance run --auto --task=rerere-gc && + test_path_is_missing $entry_1 && + test_path_is_missing $entry_2 && + test_path_is_dir $entry_3 ' test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' ' test_when_finished "rm -rf .git/rr-cache" && + entry=.git/rr-cache/171$(echo $ZERO_OID | cut -c4-) && # A negative value should always prune. git -c maintenance.rerere-gc.auto=-1 maintenance is-needed --auto --task=rerere-gc && test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc && - # A positive value prunes when there is at least one entry. - ! git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc && - test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc && - mkdir .git/rr-cache && - ! git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc && - test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc && - : >.git/rr-cache/entry-1 && - git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc && - test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc && + # A positive value prunes only when the estimated number of stale + # entries is at least as big. A single sampled entry counts for 256 + # estimated entries. + mkdir -p $entry && + echo preimage >$entry/preimage && + test-tool chmtime =-$((16 * 86400)) $entry/preimage && + + ! git -c maintenance.rerere-gc.auto=257 maintenance is-needed --auto --task=rerere-gc && + test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=257 maintenance run --auto --task=rerere-gc && + test_path_is_dir $entry && + + git -c maintenance.rerere-gc.auto=256 maintenance is-needed --auto --task=rerere-gc && + test_expect_rerere_gc git -c maintenance.rerere-gc.auto=256 maintenance run --auto --task=rerere-gc && + test_path_is_missing $entry && # Zero should never prune. - : >.git/rr-cache/entry-1 && + mkdir -p $entry && + echo preimage >$entry/preimage && + test-tool chmtime =-$((16 * 86400)) $entry/preimage && ! git -c maintenance.rerere-gc.auto=0 maintenance is-needed --auto --task=rerere-gc && test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc '