Merge branch 'ps/maintenance-reflog-expire'

"git maintenance" learns a new task to expire reflog entries.

* ps/maintenance-reflog-expire:
  builtin/maintenance: introduce "reflog-expire" task
  builtin/gc: split out function to expire reflog entries
  builtin/reflog: make functions regarding `reflog_expire_options` public
  builtin/reflog: stop storing per-reflog expiry dates globally
  builtin/reflog: stop storing default reflog expiry dates globally
  reflog: rename `cmd_reflog_expire_cb` to `reflog_expire_options`
main
Junio C Hamano 2025-04-16 13:54:19 -07:00
commit 01a6e244f9
7 changed files with 263 additions and 165 deletions

View File

@ -74,3 +74,12 @@ maintenance.incremental-repack.auto::
Otherwise, a positive value implies the command should run when the
number of pack-files not in the multi-pack-index is at least the value
of `maintenance.incremental-repack.auto`. The default value is 10.

maintenance.reflog-expire.auto::
This integer config option controls how often the `reflog-expire` task
should be run as part of `git maintenance run --auto`. If zero, then
the `reflog-expire` task will not run with the `--auto` option. A
negative value will force the task to run every time. Otherwise, a
positive value implies the command should run when the number of
expired reflog entries in the "HEAD" reflog is at least the value of
`maintenance.loose-objects.auto`. The default value is 100.

View File

@ -162,6 +162,10 @@ pack-refs::
need to iterate across many references. See linkgit:git-pack-refs[1]
for more information.

reflog-expire::
The `reflog-expire` task deletes any entries in the reflog older than the
expiry threshold. See linkgit:git-reflog[1] for more information.

OPTIONS
-------
--auto::

View File

@ -33,6 +33,7 @@
#include "pack.h"
#include "pack-objects.h"
#include "path.h"
#include "reflog.h"
#include "blob.h"
#include "tree.h"
#include "promisor-remote.h"
@ -53,7 +54,6 @@ static const char * const builtin_gc_usage[] = {

static timestamp_t gc_log_expire_time;

static struct strvec reflog = STRVEC_INIT;
static struct strvec repack = STRVEC_INIT;
static struct strvec prune = STRVEC_INIT;
static struct strvec prune_worktrees = STRVEC_INIT;
@ -288,6 +288,58 @@ static int maintenance_task_pack_refs(struct maintenance_run_opts *opts,
return run_command(&cmd);
}

struct count_reflog_entries_data {
struct expire_reflog_policy_cb policy;
size_t count;
size_t limit;
};

static int count_reflog_entries(struct object_id *old_oid, struct object_id *new_oid,
const char *committer, timestamp_t timestamp,
int tz, const char *msg, void *cb_data)
{
struct count_reflog_entries_data *data = cb_data;
if (should_expire_reflog_ent(old_oid, new_oid, committer, timestamp, tz, msg, &data->policy))
data->count++;
return data->count >= data->limit;
}

static int reflog_expire_condition(struct gc_config *cfg UNUSED)
{
timestamp_t now = time(NULL);
struct count_reflog_entries_data data = {
.policy = {
.opts = REFLOG_EXPIRE_OPTIONS_INIT(now),
},
};
int limit = 100;

git_config_get_int("maintenance.reflog-expire.auto", &limit);
if (!limit)
return 0;
if (limit < 0)
return 1;
data.limit = limit;

repo_config(the_repository, reflog_expire_config, &data.policy.opts);

reflog_expire_options_set_refname(&data.policy.opts, "HEAD");
refs_for_each_reflog_ent(get_main_ref_store(the_repository), "HEAD",
count_reflog_entries, &data);

reflog_expiry_cleanup(&data.policy);
return data.count >= data.limit;
}

static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUSED,
struct gc_config *cfg UNUSED)
{
struct child_process cmd = CHILD_PROCESS_INIT;
cmd.git_cmd = 1;
strvec_pushl(&cmd.args, "reflog", "expire", "--all", NULL);
return run_command(&cmd);
}

static int too_many_loose_objects(struct gc_config *cfg)
{
/*
@ -667,15 +719,8 @@ static void gc_before_repack(struct maintenance_run_opts *opts,

if (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))
die(FAILED_RUN, "pack-refs");

if (cfg->prune_reflogs) {
struct child_process cmd = CHILD_PROCESS_INIT;

cmd.git_cmd = 1;
strvec_pushv(&cmd.args, reflog.v);
if (run_command(&cmd))
die(FAILED_RUN, reflog.v[0]);
}
if (cfg->prune_reflogs && maintenance_task_reflog_expire(opts, cfg))
die(FAILED_RUN, "reflog");
}

int cmd_gc(int argc,
@ -723,7 +768,6 @@ struct repository *repo UNUSED)
show_usage_with_options_if_asked(argc, argv,
builtin_gc_usage, builtin_gc_options);

strvec_pushl(&reflog, "reflog", "expire", "--all", NULL);
strvec_pushl(&repack, "repack", "-d", "-l", NULL);
strvec_pushl(&prune, "prune", "--expire", NULL);
strvec_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL);
@ -1412,6 +1456,7 @@ enum maintenance_task_label {
TASK_GC,
TASK_COMMIT_GRAPH,
TASK_PACK_REFS,
TASK_REFLOG_EXPIRE,

/* Leave as final value */
TASK__COUNT
@ -1448,6 +1493,11 @@ static struct maintenance_task tasks[] = {
maintenance_task_pack_refs,
pack_refs_condition,
},
[TASK_REFLOG_EXPIRE] = {
"reflog-expire",
maintenance_task_reflog_expire,
reflog_expire_condition,
},
};

static int compare_tasks_by_selection(const void *a_, const void *b_)

View File

@ -72,9 +72,6 @@ static const char *const reflog_usage[] = {
NULL
};

static timestamp_t default_reflog_expire;
static timestamp_t default_reflog_expire_unreachable;

struct worktree_reflogs {
struct worktree *worktree;
struct string_list reflogs;
@ -100,131 +97,19 @@ static int collect_reflog(const char *ref, void *cb_data)
return 0;
}

static struct reflog_expire_cfg {
struct reflog_expire_cfg *next;
timestamp_t expire_total;
timestamp_t expire_unreachable;
char pattern[FLEX_ARRAY];
} *reflog_expire_cfg, **reflog_expire_cfg_tail;

static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len)
{
struct reflog_expire_cfg *ent;

if (!reflog_expire_cfg_tail)
reflog_expire_cfg_tail = &reflog_expire_cfg;

for (ent = reflog_expire_cfg; ent; ent = ent->next)
if (!xstrncmpz(ent->pattern, pattern, len))
return ent;

FLEX_ALLOC_MEM(ent, pattern, pattern, len);
*reflog_expire_cfg_tail = ent;
reflog_expire_cfg_tail = &(ent->next);
return ent;
}

/* expiry timer slot */
#define EXPIRE_TOTAL 01
#define EXPIRE_UNREACH 02

static int reflog_expire_config(const char *var, const char *value,
const struct config_context *ctx, void *cb)
{
const char *pattern, *key;
size_t pattern_len;
timestamp_t expire;
int slot;
struct reflog_expire_cfg *ent;

if (parse_config_key(var, "gc", &pattern, &pattern_len, &key) < 0)
return git_default_config(var, value, ctx, cb);

if (!strcmp(key, "reflogexpire")) {
slot = EXPIRE_TOTAL;
if (git_config_expiry_date(&expire, var, value))
return -1;
} else if (!strcmp(key, "reflogexpireunreachable")) {
slot = EXPIRE_UNREACH;
if (git_config_expiry_date(&expire, var, value))
return -1;
} else
return git_default_config(var, value, ctx, cb);

if (!pattern) {
switch (slot) {
case EXPIRE_TOTAL:
default_reflog_expire = expire;
break;
case EXPIRE_UNREACH:
default_reflog_expire_unreachable = expire;
break;
}
return 0;
}

ent = find_cfg_ent(pattern, pattern_len);
if (!ent)
return -1;
switch (slot) {
case EXPIRE_TOTAL:
ent->expire_total = expire;
break;
case EXPIRE_UNREACH:
ent->expire_unreachable = expire;
break;
}
return 0;
}

static void set_reflog_expiry_param(struct cmd_reflog_expire_cb *cb, const char *ref)
{
struct reflog_expire_cfg *ent;

if (cb->explicit_expiry == (EXPIRE_TOTAL|EXPIRE_UNREACH))
return; /* both given explicitly -- nothing to tweak */

for (ent = reflog_expire_cfg; ent; ent = ent->next) {
if (!wildmatch(ent->pattern, ref, 0)) {
if (!(cb->explicit_expiry & EXPIRE_TOTAL))
cb->expire_total = ent->expire_total;
if (!(cb->explicit_expiry & EXPIRE_UNREACH))
cb->expire_unreachable = ent->expire_unreachable;
return;
}
}

/*
* If unconfigured, make stash never expire
*/
if (!strcmp(ref, "refs/stash")) {
if (!(cb->explicit_expiry & EXPIRE_TOTAL))
cb->expire_total = 0;
if (!(cb->explicit_expiry & EXPIRE_UNREACH))
cb->expire_unreachable = 0;
return;
}

/* Nothing matched -- use the default value */
if (!(cb->explicit_expiry & EXPIRE_TOTAL))
cb->expire_total = default_reflog_expire;
if (!(cb->explicit_expiry & EXPIRE_UNREACH))
cb->expire_unreachable = default_reflog_expire_unreachable;
}

static int expire_unreachable_callback(const struct option *opt,
const char *arg,
int unset)
{
struct cmd_reflog_expire_cb *cmd = opt->value;
struct reflog_expire_options *opts = opt->value;

BUG_ON_OPT_NEG(unset);

if (parse_expiry_date(arg, &cmd->expire_unreachable))
if (parse_expiry_date(arg, &opts->expire_unreachable))
die(_("invalid timestamp '%s' given to '--%s'"),
arg, opt->long_name);

cmd->explicit_expiry |= EXPIRE_UNREACH;
opts->explicit_expiry |= REFLOG_EXPIRE_UNREACH;
return 0;
}

@ -232,15 +117,15 @@ static int expire_total_callback(const struct option *opt,
const char *arg,
int unset)
{
struct cmd_reflog_expire_cb *cmd = opt->value;
struct reflog_expire_options *opts = opt->value;

BUG_ON_OPT_NEG(unset);

if (parse_expiry_date(arg, &cmd->expire_total))
if (parse_expiry_date(arg, &opts->expire_total))
die(_("invalid timestamp '%s' given to '--%s'"),
arg, opt->long_name);

cmd->explicit_expiry |= EXPIRE_TOTAL;
opts->explicit_expiry |= REFLOG_EXPIRE_TOTAL;
return 0;
}

@ -285,8 +170,8 @@ static int cmd_reflog_list(int argc, const char **argv, const char *prefix,
static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,
struct repository *repo UNUSED)
{
struct cmd_reflog_expire_cb cmd = { 0 };
timestamp_t now = time(NULL);
struct reflog_expire_options opts = REFLOG_EXPIRE_OPTIONS_INIT(now);
int i, status, do_all, single_worktree = 0;
unsigned int flags = 0;
int verbose = 0;
@ -301,15 +186,15 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,
N_("update the reference to the value of the top reflog entry"),
EXPIRE_REFLOGS_UPDATE_REF),
OPT_BOOL(0, "verbose", &verbose, N_("print extra information on screen")),
OPT_CALLBACK_F(0, "expire", &cmd, N_("timestamp"),
OPT_CALLBACK_F(0, "expire", &opts, N_("timestamp"),
N_("prune entries older than the specified time"),
PARSE_OPT_NONEG,
expire_total_callback),
OPT_CALLBACK_F(0, "expire-unreachable", &cmd, N_("timestamp"),
OPT_CALLBACK_F(0, "expire-unreachable", &opts, N_("timestamp"),
N_("prune entries older than <time> that are not reachable from the current tip of the branch"),
PARSE_OPT_NONEG,
expire_unreachable_callback),
OPT_BOOL(0, "stale-fix", &cmd.stalefix,
OPT_BOOL(0, "stale-fix", &opts.stalefix,
N_("prune any reflog entries that point to broken commits")),
OPT_BOOL(0, "all", &do_all, N_("process the reflogs of all references")),
OPT_BOOL(0, "single-worktree", &single_worktree,
@ -317,17 +202,11 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,
OPT_END()
};

default_reflog_expire_unreachable = now - 30 * 24 * 3600;
default_reflog_expire = now - 90 * 24 * 3600;
git_config(reflog_expire_config, NULL);
git_config(reflog_expire_config, &opts);

save_commit_buffer = 0;
do_all = status = 0;

cmd.explicit_expiry = 0;
cmd.expire_total = default_reflog_expire;
cmd.expire_unreachable = default_reflog_expire_unreachable;

argc = parse_options(argc, argv, prefix, options, reflog_expire_usage, 0);

if (verbose)
@ -338,7 +217,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,
* even in older repository. We cannot trust what's reachable
* from reflog if the repository was pruned with older git.
*/
if (cmd.stalefix) {
if (opts.stalefix) {
struct rev_info revs;

repo_init_revisions(the_repository, &revs, prefix);
@ -372,11 +251,11 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,

for_each_string_list_item(item, &collected.reflogs) {
struct expire_reflog_policy_cb cb = {
.cmd = cmd,
.opts = opts,
.dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN),
};

set_reflog_expiry_param(&cb.cmd, item->string);
reflog_expire_options_set_refname(&cb.opts, item->string);
status |= refs_reflog_expire(get_main_ref_store(the_repository),
item->string, flags,
reflog_expiry_prepare,
@ -389,13 +268,13 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,

for (i = 0; i < argc; i++) {
char *ref;
struct expire_reflog_policy_cb cb = { .cmd = cmd };
struct expire_reflog_policy_cb cb = { .opts = opts };

if (!repo_dwim_log(the_repository, argv[i], strlen(argv[i]), NULL, &ref)) {
status |= error(_("reflog could not be found: '%s'"), argv[i]);
continue;
}
set_reflog_expiry_param(&cb.cmd, ref);
reflog_expire_options_set_refname(&cb.opts, ref);
status |= refs_reflog_expire(get_main_ref_store(the_repository),
ref, flags,
reflog_expiry_prepare,

137
reflog.c
View File

@ -2,13 +2,120 @@
#define DISABLE_SIGN_COMPARE_WARNINGS

#include "git-compat-util.h"
#include "config.h"
#include "gettext.h"
#include "object-store-ll.h"
#include "parse-options.h"
#include "reflog.h"
#include "refs.h"
#include "revision.h"
#include "tree.h"
#include "tree-walk.h"
#include "wildmatch.h"

static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,
const char *pattern, size_t len)
{
struct reflog_expire_entry_option *ent;

if (!opts->entries_tail)
opts->entries_tail = &opts->entries;

for (ent = opts->entries; ent; ent = ent->next)
if (!xstrncmpz(ent->pattern, pattern, len))
return ent;

FLEX_ALLOC_MEM(ent, pattern, pattern, len);
*opts->entries_tail = ent;
opts->entries_tail = &(ent->next);
return ent;
}

int reflog_expire_config(const char *var, const char *value,
const struct config_context *ctx, void *cb)
{
struct reflog_expire_options *opts = cb;
const char *pattern, *key;
size_t pattern_len;
timestamp_t expire;
int slot;
struct reflog_expire_entry_option *ent;

if (parse_config_key(var, "gc", &pattern, &pattern_len, &key) < 0)
return git_default_config(var, value, ctx, cb);

if (!strcmp(key, "reflogexpire")) {
slot = REFLOG_EXPIRE_TOTAL;
if (git_config_expiry_date(&expire, var, value))
return -1;
} else if (!strcmp(key, "reflogexpireunreachable")) {
slot = REFLOG_EXPIRE_UNREACH;
if (git_config_expiry_date(&expire, var, value))
return -1;
} else
return git_default_config(var, value, ctx, cb);

if (!pattern) {
switch (slot) {
case REFLOG_EXPIRE_TOTAL:
opts->default_expire_total = expire;
break;
case REFLOG_EXPIRE_UNREACH:
opts->default_expire_unreachable = expire;
break;
}
return 0;
}

ent = find_cfg_ent(opts, pattern, pattern_len);
if (!ent)
return -1;
switch (slot) {
case REFLOG_EXPIRE_TOTAL:
ent->expire_total = expire;
break;
case REFLOG_EXPIRE_UNREACH:
ent->expire_unreachable = expire;
break;
}
return 0;
}

void reflog_expire_options_set_refname(struct reflog_expire_options *cb,
const char *ref)
{
struct reflog_expire_entry_option *ent;

if (cb->explicit_expiry == (REFLOG_EXPIRE_TOTAL|REFLOG_EXPIRE_UNREACH))
return; /* both given explicitly -- nothing to tweak */

for (ent = cb->entries; ent; ent = ent->next) {
if (!wildmatch(ent->pattern, ref, 0)) {
if (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))
cb->expire_total = ent->expire_total;
if (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))
cb->expire_unreachable = ent->expire_unreachable;
return;
}
}

/*
* If unconfigured, make stash never expire
*/
if (!strcmp(ref, "refs/stash")) {
if (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))
cb->expire_total = 0;
if (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))
cb->expire_unreachable = 0;
return;
}

/* Nothing matched -- use the default value */
if (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))
cb->expire_total = cb->default_expire_total;
if (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))
cb->expire_unreachable = cb->default_expire_unreachable;
}

/* Remember to update object flag allocation in object.h */
#define INCOMPLETE (1u<<10)
@ -252,15 +359,15 @@ int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,
struct expire_reflog_policy_cb *cb = cb_data;
struct commit *old_commit, *new_commit;

if (timestamp < cb->cmd.expire_total)
if (timestamp < cb->opts.expire_total)
return 1;

old_commit = new_commit = NULL;
if (cb->cmd.stalefix &&
if (cb->opts.stalefix &&
(!keep_entry(&old_commit, ooid) || !keep_entry(&new_commit, noid)))
return 1;

if (timestamp < cb->cmd.expire_unreachable) {
if (timestamp < cb->opts.expire_unreachable) {
switch (cb->unreachable_expire_kind) {
case UE_ALWAYS:
return 1;
@ -272,7 +379,7 @@ int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,
}
}

if (cb->cmd.recno && --(cb->cmd.recno) == 0)
if (cb->opts.recno && --(cb->opts.recno) == 0)
return 1;

return 0;
@ -331,7 +438,7 @@ void reflog_expiry_prepare(const char *refname,
struct commit_list *elem;
struct commit *commit = NULL;

if (!cb->cmd.expire_unreachable || is_head(refname)) {
if (!cb->opts.expire_unreachable || is_head(refname)) {
cb->unreachable_expire_kind = UE_HEAD;
} else {
commit = lookup_commit_reference_gently(the_repository,
@ -341,7 +448,7 @@ void reflog_expiry_prepare(const char *refname,
cb->unreachable_expire_kind = commit ? UE_NORMAL : UE_ALWAYS;
}

if (cb->cmd.expire_unreachable <= cb->cmd.expire_total)
if (cb->opts.expire_unreachable <= cb->opts.expire_total)
cb->unreachable_expire_kind = UE_ALWAYS;

switch (cb->unreachable_expire_kind) {
@ -358,7 +465,7 @@ void reflog_expiry_prepare(const char *refname,
/* For reflog_expiry_cleanup() below */
cb->tip_commit = commit;
}
cb->mark_limit = cb->cmd.expire_total;
cb->mark_limit = cb->opts.expire_total;
mark_reachable(cb);
}

@ -390,7 +497,7 @@ int count_reflog_ent(struct object_id *ooid UNUSED,
timestamp_t timestamp, int tz UNUSED,
const char *message UNUSED, void *cb_data)
{
struct cmd_reflog_expire_cb *cb = cb_data;
struct reflog_expire_options *cb = cb_data;
if (!cb->expire_total || timestamp < cb->expire_total)
cb->recno++;
return 0;
@ -398,7 +505,7 @@ int count_reflog_ent(struct object_id *ooid UNUSED,

int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)
{
struct cmd_reflog_expire_cb cmd = { 0 };
struct reflog_expire_options opts = { 0 };
int status = 0;
reflog_expiry_should_prune_fn *should_prune_fn = should_expire_reflog_ent;
const char *spec = strstr(rev, "@{");
@ -421,17 +528,17 @@ int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)

recno = strtoul(spec + 2, &ep, 10);
if (*ep == '}') {
cmd.recno = -recno;
opts.recno = -recno;
refs_for_each_reflog_ent(get_main_ref_store(the_repository),
ref, count_reflog_ent, &cmd);
ref, count_reflog_ent, &opts);
} else {
cmd.expire_total = approxidate(spec + 2);
opts.expire_total = approxidate(spec + 2);
refs_for_each_reflog_ent(get_main_ref_store(the_repository),
ref, count_reflog_ent, &cmd);
cmd.expire_total = 0;
ref, count_reflog_ent, &opts);
opts.expire_total = 0;
}

cb.cmd = cmd;
cb.opts = opts;
status |= refs_reflog_expire(get_main_ref_store(the_repository), ref,
flags,
reflog_expiry_prepare,

View File

@ -2,13 +2,44 @@
#define REFLOG_H
#include "refs.h"

struct cmd_reflog_expire_cb {
#define REFLOG_EXPIRE_TOTAL (1 << 0)
#define REFLOG_EXPIRE_UNREACH (1 << 1)

struct reflog_expire_entry_option {
struct reflog_expire_entry_option *next;
timestamp_t expire_total;
timestamp_t expire_unreachable;
char pattern[FLEX_ARRAY];
};

struct reflog_expire_options {
struct reflog_expire_entry_option *entries, **entries_tail;
int stalefix;
int explicit_expiry;
timestamp_t default_expire_total;
timestamp_t expire_total;
timestamp_t default_expire_unreachable;
timestamp_t expire_unreachable;
int recno;
};
#define REFLOG_EXPIRE_OPTIONS_INIT(now) { \
.default_expire_total = now - 30 * 24 * 3600, \
.default_expire_unreachable = now - 90 * 24 * 3600, \
}

/*
* Parse the reflog expire configuration. This should be used with
* `repo_config()`.
*/
int reflog_expire_config(const char *var, const char *value,
const struct config_context *ctx, void *cb);

/*
* Adapt the options so that they apply to the given refname. This applies any
* per-reference reflog expiry configuration that may exist to the options.
*/
void reflog_expire_options_set_refname(struct reflog_expire_options *cb,
const char *refname);

struct expire_reflog_policy_cb {
enum {
@ -18,7 +49,7 @@ struct expire_reflog_policy_cb {
} unreachable_expire_kind;
struct commit_list *mark_list;
unsigned long mark_limit;
struct cmd_reflog_expire_cb cmd;
struct reflog_expire_options opts;
struct commit *tip_commit;
struct commit_list *tips;
unsigned int dry_run:1;

View File

@ -475,6 +475,24 @@ test_expect_success 'pack-refs task' '
test_subcommand git pack-refs --all --prune <pack-refs.txt
'

test_expect_success 'reflog-expire task' '
GIT_TRACE2_EVENT="$(pwd)/reflog-expire.txt" \
git maintenance run --task=reflog-expire &&
test_subcommand git reflog expire --all <reflog-expire.txt
'

test_expect_success 'reflog-expire task --auto only packs when exceeding limits' '
git reflog expire --all --expire=now &&
test_commit reflog-one &&
test_commit reflog-two &&
GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \
git -c maintenance.reflog-expire.auto=3 maintenance run --auto --task=reflog-expire &&
test_subcommand ! git reflog expire --all <reflog-expire-auto.txt &&
GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \
git -c maintenance.reflog-expire.auto=2 maintenance run --auto --task=reflog-expire &&
test_subcommand git reflog expire --all <reflog-expire-auto.txt
'

test_expect_success '--auto and --schedule incompatible' '
test_must_fail git maintenance run --auto --schedule=daily 2>err &&
test_grep "at most one" err