{"thread":{"id":"63007","subject":"[PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","startedAt":"2025-02-26T15:24:38Z","lastAt":"2025-04-08T06:22:28Z","messageCount":26,"participants":["Patrick Steinhardt","Ramsay Jones","Junio C Hamano","Justin Tobler"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"513097","messageId":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","threadId":"63007","inReplyTo":null,"subject":"[PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-26T15:24:25Z","receivedAt":"2025-02-26T15:24:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series introduces a new \"reflog-expire\" task to\ngit-maintenance(1). This task is designed to plug a gap when the \"gc\"\ntask is disabled, as there is no way to expire reflog entries in that\ncase.\n\nThis patch series has been inspired by the discussion at [1]. I consider\nit to be another step into the direction of replacing git-gc(1) and\nallowing for more flexible maintenance strategies overall. Next steps\ncould be:\n\n  1. Enable the \"reflog-expire\" task by default when using the\n     \"incremental\" strategy. and then we might eventually switch over\n     the\n\n  2. Use \"incremental\" strategy when \"features.experimental\" is enabled.\n\n  3. Switch over the default strategy to \"incremental\" after a couple of\n     releases.\n\nThanks!\n\nPatrick\n\n[1]: <e650f4e4-e267-4f1f-bb3a-c71b1fe0b276@uxp.de>\n\n---\nPatrick Steinhardt (6):\n      reflog: rename `cmd_reflog_expire_cb` to `reflog_expire_options`\n      builtin/reflog: stop storing default reflog expiry dates globally\n      builtin/reflog: stop storing per-reflog expiry dates globally\n      builtin/reflog: make functions regarding `reflog_expire_options` public\n      builtin/gc: split out function to expire reflog entries\n      builtin/maintenance: introduce \"reflog-expire\" task\n\n Documentation/config/maintenance.adoc |   9 ++\n Documentation/git-maintenance.adoc    |   4 +\n builtin/gc.c                          |  72 +++++++++++++---\n builtin/reflog.c                      | 153 ++++------------------------------\n reflog.c                              | 137 ++++++++++++++++++++++++++----\n reflog.h                              |  35 +++++++-\n t/t7900-maintenance.sh                |  18 ++++\n 7 files changed, 263 insertions(+), 165 deletions(-)\n\n\n---\nbase-commit: 5a526e5e18ddb9a7dfc5a2967d21d6154df64a4f\nchange-id: 20250226-pks-maintenance-reflog-expire-61c61410751a\n\n"},{"id":"513098","messageId":"20250226-pks-maintenance-reflog-expire-v1-1-a1204a814952@pks.im","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"[PATCH 1/6] reflog: rename `cmd_reflog_expire_cb` to `reflog_expire_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-26T15:24:26Z","receivedAt":"2025-02-26T15:24:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to expose `struct cmd_reflog_expire_cb` via \"reflog.h\" so\nthat we can also use this structure in \"builtin/gc.c\". Once we make it\naccessible to a wider scope though it becomes awkwardly named, as it\nisn't only useful in the context of a callback. Instead, the function is\ncontaining all kinds of options relevant to whether or not a reflog\nentry should be expired.\n\nRename the structure to `reflog_expire_options` to prepare for this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 38 +++++++++++++++++++-------------------\n reflog.c         | 30 +++++++++++++++---------------\n reflog.h         |  4 ++--\n 3 files changed, 36 insertions(+), 36 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 95f264989bb..dee49881d32 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -168,7 +168,7 @@ static int reflog_expire_config(const char *var, const char *value,\n \treturn 0;\n }\n \n-static void set_reflog_expiry_param(struct cmd_reflog_expire_cb *cb, const char *ref)\n+static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char *ref)\n {\n \tstruct reflog_expire_cfg *ent;\n \n@@ -207,15 +207,15 @@ static int expire_unreachable_callback(const struct option *opt,\n \t\t\t\t const char *arg,\n \t\t\t\t int unset)\n {\n-\tstruct cmd_reflog_expire_cb *cmd = opt->value;\n+\tstruct reflog_expire_options *opts = opt->value;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tif (parse_expiry_date(arg, &cmd->expire_unreachable))\n+\tif (parse_expiry_date(arg, &opts->expire_unreachable))\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\tcmd->explicit_expiry |= EXPIRE_UNREACH;\n+\topts->explicit_expiry |= EXPIRE_UNREACH;\n \treturn 0;\n }\n \n@@ -223,15 +223,15 @@ static int expire_total_callback(const struct option *opt,\n \t\t\t\t const char *arg,\n \t\t\t\t int unset)\n {\n-\tstruct cmd_reflog_expire_cb *cmd = opt->value;\n+\tstruct reflog_expire_options *opts = opt->value;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tif (parse_expiry_date(arg, &cmd->expire_total))\n+\tif (parse_expiry_date(arg, &opts->expire_total))\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\tcmd->explicit_expiry |= EXPIRE_TOTAL;\n+\topts->explicit_expiry |= EXPIRE_TOTAL;\n \treturn 0;\n }\n \n@@ -276,7 +276,7 @@ static int cmd_reflog_list(int argc, const char **argv, const char *prefix,\n static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\t     struct repository *repo UNUSED)\n {\n-\tstruct cmd_reflog_expire_cb cmd = { 0 };\n+\tstruct reflog_expire_options opts = { 0 };\n \ttimestamp_t now = time(NULL);\n \tint i, status, do_all, single_worktree = 0;\n \tunsigned int flags = 0;\n@@ -292,15 +292,15 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\tN_(\"update the reference to the value of the top reflog entry\"),\n \t\t\tEXPIRE_REFLOGS_UPDATE_REF),\n \t\tOPT_BOOL(0, \"verbose\", &verbose, N_(\"print extra information on screen\")),\n-\t\tOPT_CALLBACK_F(0, \"expire\", &cmd, N_(\"timestamp\"),\n+\t\tOPT_CALLBACK_F(0, \"expire\", &opts, N_(\"timestamp\"),\n \t\t\t       N_(\"prune entries older than the specified time\"),\n \t\t\t       PARSE_OPT_NONEG,\n \t\t\t       expire_total_callback),\n-\t\tOPT_CALLBACK_F(0, \"expire-unreachable\", &cmd, N_(\"timestamp\"),\n+\t\tOPT_CALLBACK_F(0, \"expire-unreachable\", &opts, N_(\"timestamp\"),\n \t\t\t       N_(\"prune entries older than <time> that are not reachable from the current tip of the branch\"),\n \t\t\t       PARSE_OPT_NONEG,\n \t\t\t       expire_unreachable_callback),\n-\t\tOPT_BOOL(0, \"stale-fix\", &cmd.stalefix,\n+\t\tOPT_BOOL(0, \"stale-fix\", &opts.stalefix,\n \t\t\t N_(\"prune any reflog entries that point to broken commits\")),\n \t\tOPT_BOOL(0, \"all\", &do_all, N_(\"process the reflogs of all references\")),\n \t\tOPT_BOOL(0, \"single-worktree\", &single_worktree,\n@@ -315,9 +315,9 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \tsave_commit_buffer = 0;\n \tdo_all = status = 0;\n \n-\tcmd.explicit_expiry = 0;\n-\tcmd.expire_total = default_reflog_expire;\n-\tcmd.expire_unreachable = default_reflog_expire_unreachable;\n+\topts.explicit_expiry = 0;\n+\topts.expire_total = default_reflog_expire;\n+\topts.expire_unreachable = default_reflog_expire_unreachable;\n \n \targc = parse_options(argc, argv, prefix, options, reflog_expire_usage, 0);\n \n@@ -329,7 +329,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t * even in older repository.  We cannot trust what's reachable\n \t * from reflog if the repository was pruned with older git.\n \t */\n-\tif (cmd.stalefix) {\n+\tif (opts.stalefix) {\n \t\tstruct rev_info revs;\n \n \t\trepo_init_revisions(the_repository, &revs, prefix);\n@@ -363,11 +363,11 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \n \t\tfor_each_string_list_item(item, &collected.reflogs) {\n \t\t\tstruct expire_reflog_policy_cb cb = {\n-\t\t\t\t.cmd = cmd,\n+\t\t\t\t.opts = opts,\n \t\t\t\t.dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN),\n \t\t\t};\n \n-\t\t\tset_reflog_expiry_param(&cb.cmd,  item->string);\n+\t\t\tset_reflog_expiry_param(&cb.opts,  item->string);\n \t\t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t\t     item->string, flags,\n \t\t\t\t\t\t     reflog_expiry_prepare,\n@@ -380,13 +380,13 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \n \tfor (i = 0; i < argc; i++) {\n \t\tchar *ref;\n-\t\tstruct expire_reflog_policy_cb cb = { .cmd = cmd };\n+\t\tstruct expire_reflog_policy_cb cb = { .opts = opts };\n \n \t\tif (!repo_dwim_log(the_repository, argv[i], strlen(argv[i]), NULL, &ref)) {\n \t\t\tstatus |= error(_(\"%s points nowhere!\"), argv[i]);\n \t\t\tcontinue;\n \t\t}\n-\t\tset_reflog_expiry_param(&cb.cmd, ref);\n+\t\tset_reflog_expiry_param(&cb.opts, ref);\n \t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t     ref, flags,\n \t\t\t\t\t     reflog_expiry_prepare,\ndiff --git a/reflog.c b/reflog.c\nindex 1b5f031f6d7..bcdb75514d0 100644\n--- a/reflog.c\n+++ b/reflog.c\n@@ -252,15 +252,15 @@ int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,\n \tstruct expire_reflog_policy_cb *cb = cb_data;\n \tstruct commit *old_commit, *new_commit;\n \n-\tif (timestamp < cb->cmd.expire_total)\n+\tif (timestamp < cb->opts.expire_total)\n \t\treturn 1;\n \n \told_commit = new_commit = NULL;\n-\tif (cb->cmd.stalefix &&\n+\tif (cb->opts.stalefix &&\n \t    (!keep_entry(&old_commit, ooid) || !keep_entry(&new_commit, noid)))\n \t\treturn 1;\n \n-\tif (timestamp < cb->cmd.expire_unreachable) {\n+\tif (timestamp < cb->opts.expire_unreachable) {\n \t\tswitch (cb->unreachable_expire_kind) {\n \t\tcase UE_ALWAYS:\n \t\t\treturn 1;\n@@ -272,7 +272,7 @@ int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,\n \t\t}\n \t}\n \n-\tif (cb->cmd.recno && --(cb->cmd.recno) == 0)\n+\tif (cb->opts.recno && --(cb->opts.recno) == 0)\n \t\treturn 1;\n \n \treturn 0;\n@@ -331,7 +331,7 @@ void reflog_expiry_prepare(const char *refname,\n \tstruct commit_list *elem;\n \tstruct commit *commit = NULL;\n \n-\tif (!cb->cmd.expire_unreachable || is_head(refname)) {\n+\tif (!cb->opts.expire_unreachable || is_head(refname)) {\n \t\tcb->unreachable_expire_kind = UE_HEAD;\n \t} else {\n \t\tcommit = lookup_commit_reference_gently(the_repository,\n@@ -341,7 +341,7 @@ void reflog_expiry_prepare(const char *refname,\n \t\tcb->unreachable_expire_kind = commit ? UE_NORMAL : UE_ALWAYS;\n \t}\n \n-\tif (cb->cmd.expire_unreachable <= cb->cmd.expire_total)\n+\tif (cb->opts.expire_unreachable <= cb->opts.expire_total)\n \t\tcb->unreachable_expire_kind = UE_ALWAYS;\n \n \tswitch (cb->unreachable_expire_kind) {\n@@ -358,7 +358,7 @@ void reflog_expiry_prepare(const char *refname,\n \t\t/* For reflog_expiry_cleanup() below */\n \t\tcb->tip_commit = commit;\n \t}\n-\tcb->mark_limit = cb->cmd.expire_total;\n+\tcb->mark_limit = cb->opts.expire_total;\n \tmark_reachable(cb);\n }\n \n@@ -390,7 +390,7 @@ int count_reflog_ent(struct object_id *ooid UNUSED,\n \t\t     timestamp_t timestamp, int tz UNUSED,\n \t\t     const char *message UNUSED, void *cb_data)\n {\n-\tstruct cmd_reflog_expire_cb *cb = cb_data;\n+\tstruct reflog_expire_options *cb = cb_data;\n \tif (!cb->expire_total || timestamp < cb->expire_total)\n \t\tcb->recno++;\n \treturn 0;\n@@ -398,7 +398,7 @@ int count_reflog_ent(struct object_id *ooid UNUSED,\n \n int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)\n {\n-\tstruct cmd_reflog_expire_cb cmd = { 0 };\n+\tstruct reflog_expire_options opts = { 0 };\n \tint status = 0;\n \treflog_expiry_should_prune_fn *should_prune_fn = should_expire_reflog_ent;\n \tconst char *spec = strstr(rev, \"@{\");\n@@ -421,17 +421,17 @@ int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)\n \n \trecno = strtoul(spec + 2, &ep, 10);\n \tif (*ep == '}') {\n-\t\tcmd.recno = -recno;\n+\t\topts.recno = -recno;\n \t\trefs_for_each_reflog_ent(get_main_ref_store(the_repository),\n-\t\t\t\t\t ref, count_reflog_ent, &cmd);\n+\t\t\t\t\t ref, count_reflog_ent, &opts);\n \t} else {\n-\t\tcmd.expire_total = approxidate(spec + 2);\n+\t\topts.expire_total = approxidate(spec + 2);\n \t\trefs_for_each_reflog_ent(get_main_ref_store(the_repository),\n-\t\t\t\t\t ref, count_reflog_ent, &cmd);\n-\t\tcmd.expire_total = 0;\n+\t\t\t\t\t ref, count_reflog_ent, &opts);\n+\t\topts.expire_total = 0;\n \t}\n \n-\tcb.cmd = cmd;\n+\tcb.opts = opts;\n \tstatus |= refs_reflog_expire(get_main_ref_store(the_repository), ref,\n \t\t\t\t     flags,\n \t\t\t\t     reflog_expiry_prepare,\ndiff --git a/reflog.h b/reflog.h\nindex d2906fb9f8d..eb948119e53 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -2,7 +2,7 @@\n #define REFLOG_H\n #include \"refs.h\"\n \n-struct cmd_reflog_expire_cb {\n+struct reflog_expire_options {\n \tint stalefix;\n \tint explicit_expiry;\n \ttimestamp_t expire_total;\n@@ -18,7 +18,7 @@ struct expire_reflog_policy_cb {\n \t} unreachable_expire_kind;\n \tstruct commit_list *mark_list;\n \tunsigned long mark_limit;\n-\tstruct cmd_reflog_expire_cb cmd;\n+\tstruct reflog_expire_options opts;\n \tstruct commit *tip_commit;\n \tstruct commit_list *tips;\n \tunsigned int dry_run:1;\n\n-- \n2.48.1.741.g8a9f3a5cdc.dirty\n\n"},{"id":"513099","messageId":"20250226-pks-maintenance-reflog-expire-v1-2-a1204a814952@pks.im","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"[PATCH 2/6] builtin/reflog: stop storing default reflog expiry dates globally","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-26T15:24:27Z","receivedAt":"2025-02-26T15:24:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When expiring reflog entries, it is possible to configure expiry dates\nthat depend on the name of the reflog. This requires us to store a\ncouple of different expiry dates:\n\n  - The default expiry date for reflog entries that aren't otherwise\n    specified.\n\n  - The per-reflog expiry date.\n\n  - The currently active set of expiry dates for a given reference.\n\nWhile the last item is stored in `struct reflog_expiry_options`, the\nother items aren't, which makes it hard to reuse the structure in other\nplaces.\n\nRefactor the code so that the default expiry date is stored as part of\nthe structure. The per-reflog expiry dates will be adapted accordingly\nin the subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 22 +++++++---------------\n reflog.h         |  6 ++++++\n 2 files changed, 13 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex dee49881d32..0910a4e25dc 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -63,9 +63,6 @@ static const char *const reflog_usage[] = {\n \tNULL\n };\n \n-static timestamp_t default_reflog_expire;\n-static timestamp_t default_reflog_expire_unreachable;\n-\n struct worktree_reflogs {\n \tstruct worktree *worktree;\n \tstruct string_list reflogs;\n@@ -122,6 +119,7 @@ static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len)\n static int reflog_expire_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *cb)\n {\n+\tstruct reflog_expire_options *opts = cb;\n \tconst char *pattern, *key;\n \tsize_t pattern_len;\n \ttimestamp_t expire;\n@@ -145,10 +143,10 @@ static int reflog_expire_config(const char *var, const char *value,\n \tif (!pattern) {\n \t\tswitch (slot) {\n \t\tcase EXPIRE_TOTAL:\n-\t\t\tdefault_reflog_expire = expire;\n+\t\t\topts->default_expire_total = expire;\n \t\t\tbreak;\n \t\tcase EXPIRE_UNREACH:\n-\t\t\tdefault_reflog_expire_unreachable = expire;\n+\t\t\topts->default_expire_unreachable = expire;\n \t\t\tbreak;\n \t\t}\n \t\treturn 0;\n@@ -198,9 +196,9 @@ static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char\n \n \t/* Nothing matched -- use the default value */\n \tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\tcb->expire_total = default_reflog_expire;\n+\t\tcb->expire_total = cb->default_expire_total;\n \tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\tcb->expire_unreachable = default_reflog_expire_unreachable;\n+\t\tcb->expire_unreachable = cb->default_expire_unreachable;\n }\n \n static int expire_unreachable_callback(const struct option *opt,\n@@ -276,8 +274,8 @@ static int cmd_reflog_list(int argc, const char **argv, const char *prefix,\n static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\t     struct repository *repo UNUSED)\n {\n-\tstruct reflog_expire_options opts = { 0 };\n \ttimestamp_t now = time(NULL);\n+\tstruct reflog_expire_options opts = REFLOG_EXPIRE_OPTIONS_INIT(now);\n \tint i, status, do_all, single_worktree = 0;\n \tunsigned int flags = 0;\n \tint verbose = 0;\n@@ -308,17 +306,11 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\tOPT_END()\n \t};\n \n-\tdefault_reflog_expire_unreachable = now - 30 * 24 * 3600;\n-\tdefault_reflog_expire = now - 90 * 24 * 3600;\n-\tgit_config(reflog_expire_config, NULL);\n+\tgit_config(reflog_expire_config, &opts);\n \n \tsave_commit_buffer = 0;\n \tdo_all = status = 0;\n \n-\topts.explicit_expiry = 0;\n-\topts.expire_total = default_reflog_expire;\n-\topts.expire_unreachable = default_reflog_expire_unreachable;\n-\n \targc = parse_options(argc, argv, prefix, options, reflog_expire_usage, 0);\n \n \tif (verbose)\ndiff --git a/reflog.h b/reflog.h\nindex eb948119e53..a9d464bbf8c 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -5,10 +5,16 @@\n struct reflog_expire_options {\n \tint stalefix;\n \tint explicit_expiry;\n+\ttimestamp_t default_expire_total;\n \ttimestamp_t expire_total;\n+\ttimestamp_t default_expire_unreachable;\n \ttimestamp_t expire_unreachable;\n \tint recno;\n };\n+#define REFLOG_EXPIRE_OPTIONS_INIT(now) { \\\n+\t.default_expire_total = now - 30 * 24 * 3600, \\\n+\t.default_expire_unreachable = now - 90 * 24 * 3600, \\\n+}\n \n struct expire_reflog_policy_cb {\n \tenum {\n\n-- \n2.48.1.741.g8a9f3a5cdc.dirty\n\n"},{"id":"513100","messageId":"20250226-pks-maintenance-reflog-expire-v1-3-a1204a814952@pks.im","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"[PATCH 3/6] builtin/reflog: stop storing per-reflog expiry dates globally","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-26T15:24:28Z","receivedAt":"2025-02-26T15:24:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"As described in the preceding commit, the per-reflog expiry dates are\nstored in a global pair of variables. Refactor the code so that they are\ncontained in `sturct reflog_expire_options` to make the structure useful\nin other contexts.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 30 ++++++++++++------------------\n reflog.h         |  8 ++++++++\n 2 files changed, 20 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 0910a4e25dc..a231cf4b857 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -88,27 +88,21 @@ static int collect_reflog(const char *ref, void *cb_data)\n \treturn 0;\n }\n \n-static struct reflog_expire_cfg {\n-\tstruct reflog_expire_cfg *next;\n-\ttimestamp_t expire_total;\n-\ttimestamp_t expire_unreachable;\n-\tchar pattern[FLEX_ARRAY];\n-} *reflog_expire_cfg, **reflog_expire_cfg_tail;\n-\n-static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len)\n+static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,\n+\t\t\t\t\t\t       const char *pattern, size_t len)\n {\n-\tstruct reflog_expire_cfg *ent;\n+\tstruct reflog_expire_entry_option *ent;\n \n-\tif (!reflog_expire_cfg_tail)\n-\t\treflog_expire_cfg_tail = &reflog_expire_cfg;\n+\tif (!opts->entries_tail)\n+\t\topts->entries_tail = &opts->entries;\n \n-\tfor (ent = reflog_expire_cfg; ent; ent = ent->next)\n+\tfor (ent = opts->entries; ent; ent = ent->next)\n \t\tif (!xstrncmpz(ent->pattern, pattern, len))\n \t\t\treturn ent;\n \n \tFLEX_ALLOC_MEM(ent, pattern, pattern, len);\n-\t*reflog_expire_cfg_tail = ent;\n-\treflog_expire_cfg_tail = &(ent->next);\n+\t*opts->entries_tail = ent;\n+\topts->entries_tail = &(ent->next);\n \treturn ent;\n }\n \n@@ -124,7 +118,7 @@ static int reflog_expire_config(const char *var, const char *value,\n \tsize_t pattern_len;\n \ttimestamp_t expire;\n \tint slot;\n-\tstruct reflog_expire_cfg *ent;\n+\tstruct reflog_expire_entry_option *ent;\n \n \tif (parse_config_key(var, \"gc\", &pattern, &pattern_len, &key) < 0)\n \t\treturn git_default_config(var, value, ctx, cb);\n@@ -152,7 +146,7 @@ static int reflog_expire_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tent = find_cfg_ent(pattern, pattern_len);\n+\tent = find_cfg_ent(opts, pattern, pattern_len);\n \tif (!ent)\n \t\treturn -1;\n \tswitch (slot) {\n@@ -168,12 +162,12 @@ static int reflog_expire_config(const char *var, const char *value,\n \n static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char *ref)\n {\n-\tstruct reflog_expire_cfg *ent;\n+\tstruct reflog_expire_entry_option *ent;\n \n \tif (cb->explicit_expiry == (EXPIRE_TOTAL|EXPIRE_UNREACH))\n \t\treturn; /* both given explicitly -- nothing to tweak */\n \n-\tfor (ent = reflog_expire_cfg; ent; ent = ent->next) {\n+\tfor (ent = cb->entries; ent; ent = ent->next) {\n \t\tif (!wildmatch(ent->pattern, ref, 0)) {\n \t\t\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n \t\t\t\tcb->expire_total = ent->expire_total;\ndiff --git a/reflog.h b/reflog.h\nindex a9d464bbf8c..b08780a30a7 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -2,7 +2,15 @@\n #define REFLOG_H\n #include \"refs.h\"\n \n+struct reflog_expire_entry_option {\n+\tstruct reflog_expire_entry_option *next;\n+\ttimestamp_t expire_total;\n+\ttimestamp_t expire_unreachable;\n+\tchar pattern[FLEX_ARRAY];\n+};\n+\n struct reflog_expire_options {\n+\tstruct reflog_expire_entry_option *entries, **entries_tail;\n \tint stalefix;\n \tint explicit_expiry;\n \ttimestamp_t default_expire_total;\n\n-- \n2.48.1.741.g8a9f3a5cdc.dirty\n\n"},{"id":"513101","messageId":"20250226-pks-maintenance-reflog-expire-v1-4-a1204a814952@pks.im","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"[PATCH 4/6] builtin/reflog: make functions regarding `reflog_expire_options` public","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-26T15:24:29Z","receivedAt":"2025-02-26T15:24:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Make functions that are required to manage `reflog_expire_options`\navailable elsewhere by moving them into \"reflog.c\" and exposing them in\nthe corresponding header. The functions will be used in a subsequent\ncommit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 115 ++-----------------------------------------------------\n reflog.c         | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++\n reflog.h         |  17 ++++++++\n 3 files changed, 128 insertions(+), 111 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex a231cf4b857..5fea31f9c3c 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -88,113 +88,6 @@ static int collect_reflog(const char *ref, void *cb_data)\n \treturn 0;\n }\n \n-static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,\n-\t\t\t\t\t\t       const char *pattern, size_t len)\n-{\n-\tstruct reflog_expire_entry_option *ent;\n-\n-\tif (!opts->entries_tail)\n-\t\topts->entries_tail = &opts->entries;\n-\n-\tfor (ent = opts->entries; ent; ent = ent->next)\n-\t\tif (!xstrncmpz(ent->pattern, pattern, len))\n-\t\t\treturn ent;\n-\n-\tFLEX_ALLOC_MEM(ent, pattern, pattern, len);\n-\t*opts->entries_tail = ent;\n-\topts->entries_tail = &(ent->next);\n-\treturn ent;\n-}\n-\n-/* expiry timer slot */\n-#define EXPIRE_TOTAL   01\n-#define EXPIRE_UNREACH 02\n-\n-static int reflog_expire_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *cb)\n-{\n-\tstruct reflog_expire_options *opts = cb;\n-\tconst char *pattern, *key;\n-\tsize_t pattern_len;\n-\ttimestamp_t expire;\n-\tint slot;\n-\tstruct reflog_expire_entry_option *ent;\n-\n-\tif (parse_config_key(var, \"gc\", &pattern, &pattern_len, &key) < 0)\n-\t\treturn git_default_config(var, value, ctx, cb);\n-\n-\tif (!strcmp(key, \"reflogexpire\")) {\n-\t\tslot = EXPIRE_TOTAL;\n-\t\tif (git_config_expiry_date(&expire, var, value))\n-\t\t\treturn -1;\n-\t} else if (!strcmp(key, \"reflogexpireunreachable\")) {\n-\t\tslot = EXPIRE_UNREACH;\n-\t\tif (git_config_expiry_date(&expire, var, value))\n-\t\t\treturn -1;\n-\t} else\n-\t\treturn git_default_config(var, value, ctx, cb);\n-\n-\tif (!pattern) {\n-\t\tswitch (slot) {\n-\t\tcase EXPIRE_TOTAL:\n-\t\t\topts->default_expire_total = expire;\n-\t\t\tbreak;\n-\t\tcase EXPIRE_UNREACH:\n-\t\t\topts->default_expire_unreachable = expire;\n-\t\t\tbreak;\n-\t\t}\n-\t\treturn 0;\n-\t}\n-\n-\tent = find_cfg_ent(opts, pattern, pattern_len);\n-\tif (!ent)\n-\t\treturn -1;\n-\tswitch (slot) {\n-\tcase EXPIRE_TOTAL:\n-\t\tent->expire_total = expire;\n-\t\tbreak;\n-\tcase EXPIRE_UNREACH:\n-\t\tent->expire_unreachable = expire;\n-\t\tbreak;\n-\t}\n-\treturn 0;\n-}\n-\n-static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char *ref)\n-{\n-\tstruct reflog_expire_entry_option *ent;\n-\n-\tif (cb->explicit_expiry == (EXPIRE_TOTAL|EXPIRE_UNREACH))\n-\t\treturn; /* both given explicitly -- nothing to tweak */\n-\n-\tfor (ent = cb->entries; ent; ent = ent->next) {\n-\t\tif (!wildmatch(ent->pattern, ref, 0)) {\n-\t\t\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\t\t\tcb->expire_total = ent->expire_total;\n-\t\t\tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\t\t\tcb->expire_unreachable = ent->expire_unreachable;\n-\t\t\treturn;\n-\t\t}\n-\t}\n-\n-\t/*\n-\t * If unconfigured, make stash never expire\n-\t */\n-\tif (!strcmp(ref, \"refs/stash\")) {\n-\t\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\t\tcb->expire_total = 0;\n-\t\tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\t\tcb->expire_unreachable = 0;\n-\t\treturn;\n-\t}\n-\n-\t/* Nothing matched -- use the default value */\n-\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\tcb->expire_total = cb->default_expire_total;\n-\tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\tcb->expire_unreachable = cb->default_expire_unreachable;\n-}\n-\n static int expire_unreachable_callback(const struct option *opt,\n \t\t\t\t const char *arg,\n \t\t\t\t int unset)\n@@ -207,7 +100,7 @@ static int expire_unreachable_callback(const struct option *opt,\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\topts->explicit_expiry |= EXPIRE_UNREACH;\n+\topts->explicit_expiry |= REFLOG_EXPIRE_UNREACH;\n \treturn 0;\n }\n \n@@ -223,7 +116,7 @@ static int expire_total_callback(const struct option *opt,\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\topts->explicit_expiry |= EXPIRE_TOTAL;\n+\topts->explicit_expiry |= REFLOG_EXPIRE_TOTAL;\n \treturn 0;\n }\n \n@@ -353,7 +246,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\t\t.dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN),\n \t\t\t};\n \n-\t\t\tset_reflog_expiry_param(&cb.opts,  item->string);\n+\t\t\treflog_expire_options_set_refname(&cb.opts,  item->string);\n \t\t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t\t     item->string, flags,\n \t\t\t\t\t\t     reflog_expiry_prepare,\n@@ -372,7 +265,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\tstatus |= error(_(\"%s points nowhere!\"), argv[i]);\n \t\t\tcontinue;\n \t\t}\n-\t\tset_reflog_expiry_param(&cb.opts, ref);\n+\t\treflog_expire_options_set_refname(&cb.opts, ref);\n \t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t     ref, flags,\n \t\t\t\t\t     reflog_expiry_prepare,\ndiff --git a/reflog.c b/reflog.c\nindex bcdb75514d0..642b162ef70 100644\n--- a/reflog.c\n+++ b/reflog.c\n@@ -2,13 +2,120 @@\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n+#include \"config.h\"\n #include \"gettext.h\"\n #include \"object-store-ll.h\"\n+#include \"parse-options.h\"\n #include \"reflog.h\"\n #include \"refs.h\"\n #include \"revision.h\"\n #include \"tree.h\"\n #include \"tree-walk.h\"\n+#include \"wildmatch.h\"\n+\n+static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,\n+\t\t\t\t\t\t       const char *pattern, size_t len)\n+{\n+\tstruct reflog_expire_entry_option *ent;\n+\n+\tif (!opts->entries_tail)\n+\t\topts->entries_tail = &opts->entries;\n+\n+\tfor (ent = opts->entries; ent; ent = ent->next)\n+\t\tif (!xstrncmpz(ent->pattern, pattern, len))\n+\t\t\treturn ent;\n+\n+\tFLEX_ALLOC_MEM(ent, pattern, pattern, len);\n+\t*opts->entries_tail = ent;\n+\topts->entries_tail = &(ent->next);\n+\treturn ent;\n+}\n+\n+int reflog_expire_config(const char *var, const char *value,\n+\t\t\t const struct config_context *ctx, void *cb)\n+{\n+\tstruct reflog_expire_options *opts = cb;\n+\tconst char *pattern, *key;\n+\tsize_t pattern_len;\n+\ttimestamp_t expire;\n+\tint slot;\n+\tstruct reflog_expire_entry_option *ent;\n+\n+\tif (parse_config_key(var, \"gc\", &pattern, &pattern_len, &key) < 0)\n+\t\treturn git_default_config(var, value, ctx, cb);\n+\n+\tif (!strcmp(key, \"reflogexpire\")) {\n+\t\tslot = REFLOG_EXPIRE_TOTAL;\n+\t\tif (git_config_expiry_date(&expire, var, value))\n+\t\t\treturn -1;\n+\t} else if (!strcmp(key, \"reflogexpireunreachable\")) {\n+\t\tslot = REFLOG_EXPIRE_UNREACH;\n+\t\tif (git_config_expiry_date(&expire, var, value))\n+\t\t\treturn -1;\n+\t} else\n+\t\treturn git_default_config(var, value, ctx, cb);\n+\n+\tif (!pattern) {\n+\t\tswitch (slot) {\n+\t\tcase REFLOG_EXPIRE_TOTAL:\n+\t\t\topts->default_expire_total = expire;\n+\t\t\tbreak;\n+\t\tcase REFLOG_EXPIRE_UNREACH:\n+\t\t\topts->default_expire_unreachable = expire;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn 0;\n+\t}\n+\n+\tent = find_cfg_ent(opts, pattern, pattern_len);\n+\tif (!ent)\n+\t\treturn -1;\n+\tswitch (slot) {\n+\tcase REFLOG_EXPIRE_TOTAL:\n+\t\tent->expire_total = expire;\n+\t\tbreak;\n+\tcase REFLOG_EXPIRE_UNREACH:\n+\t\tent->expire_unreachable = expire;\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+}\n+\n+void reflog_expire_options_set_refname(struct reflog_expire_options *cb,\n+\t\t\t\t       const char *ref)\n+{\n+\tstruct reflog_expire_entry_option *ent;\n+\n+\tif (cb->explicit_expiry == (REFLOG_EXPIRE_TOTAL|REFLOG_EXPIRE_UNREACH))\n+\t\treturn; /* both given explicitly -- nothing to tweak */\n+\n+\tfor (ent = cb->entries; ent; ent = ent->next) {\n+\t\tif (!wildmatch(ent->pattern, ref, 0)) {\n+\t\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))\n+\t\t\t\tcb->expire_total = ent->expire_total;\n+\t\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))\n+\t\t\t\tcb->expire_unreachable = ent->expire_unreachable;\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * If unconfigured, make stash never expire\n+\t */\n+\tif (!strcmp(ref, \"refs/stash\")) {\n+\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))\n+\t\t\tcb->expire_total = 0;\n+\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))\n+\t\t\tcb->expire_unreachable = 0;\n+\t\treturn;\n+\t}\n+\n+\t/* Nothing matched -- use the default value */\n+\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))\n+\t\tcb->expire_total = cb->default_expire_total;\n+\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))\n+\t\tcb->expire_unreachable = cb->default_expire_unreachable;\n+}\n \n /* Remember to update object flag allocation in object.h */\n #define INCOMPLETE\t(1u<<10)\ndiff --git a/reflog.h b/reflog.h\nindex b08780a30a7..63bb56280f4 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -2,6 +2,9 @@\n #define REFLOG_H\n #include \"refs.h\"\n \n+#define REFLOG_EXPIRE_TOTAL   (1 << 0)\n+#define REFLOG_EXPIRE_UNREACH (1 << 1)\n+\n struct reflog_expire_entry_option {\n \tstruct reflog_expire_entry_option *next;\n \ttimestamp_t expire_total;\n@@ -24,6 +27,20 @@ struct reflog_expire_options {\n \t.default_expire_unreachable = now - 90 * 24 * 3600, \\\n }\n \n+/*\n+ * Parse the reflog expire configuration. This should be used with\n+ * `repo_config()`.\n+ */\n+int reflog_expire_config(const char *var, const char *value,\n+\t\t\t const struct config_context *ctx, void *cb);\n+\n+/*\n+ * Adapt the options so that they apply to the given refname. This applies any\n+ * per-reference reflog expiry configuration that may exist to the options.\n+ */\n+void reflog_expire_options_set_refname(struct reflog_expire_options *cb,\n+\t\t\t\t       const char *refname);\n+\n struct expire_reflog_policy_cb {\n \tenum {\n \t\tUE_NORMAL,\n\n-- \n2.48.1.741.g8a9f3a5cdc.dirty\n\n"},{"id":"513102","messageId":"20250226-pks-maintenance-reflog-expire-v1-5-a1204a814952@pks.im","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"[PATCH 5/6] builtin/gc: split out function to expire reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-26T15:24:30Z","receivedAt":"2025-02-26T15:24:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to introduce a new task for git-maintenance(1) that knows to\nexpire reflog entries. The logic will be shared with git-gc(1), which\nalready knows how to do this.\n\nPull out the common logic into a separate function so that we can share\nthe implementation between both builtins.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 22 +++++++++++-----------\n 1 file changed, 11 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 409d454a4b7..e8f5705dc59 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -53,7 +53,6 @@ static const char * const builtin_gc_usage[] = {\n \n static timestamp_t gc_log_expire_time;\n \n-static struct strvec reflog = STRVEC_INIT;\n static struct strvec repack = STRVEC_INIT;\n static struct strvec prune = STRVEC_INIT;\n static struct strvec prune_worktrees = STRVEC_INIT;\n@@ -286,6 +285,15 @@ static int maintenance_task_pack_refs(struct maintenance_run_opts *opts,\n \treturn run_command(&cmd);\n }\n \n+static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t\t  struct gc_config *cfg UNUSED)\n+{\n+\tstruct child_process cmd = CHILD_PROCESS_INIT;\n+\tcmd.git_cmd = 1;\n+\tstrvec_pushl(&cmd.args, \"reflog\", \"expire\", \"--all\", NULL);\n+\treturn run_command(&cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -662,15 +670,8 @@ static void gc_before_repack(struct maintenance_run_opts *opts,\n \n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n \t\tdie(FAILED_RUN, \"pack-refs\");\n-\n-\tif (cfg->prune_reflogs) {\n-\t\tstruct child_process cmd = CHILD_PROCESS_INIT;\n-\n-\t\tcmd.git_cmd = 1;\n-\t\tstrvec_pushv(&cmd.args, reflog.v);\n-\t\tif (run_command(&cmd))\n-\t\t\tdie(FAILED_RUN, reflog.v[0]);\n-\t}\n+\tif (cfg->prune_reflogs && maintenance_task_reflog_expire(opts, cfg))\n+\t\tdie(FAILED_RUN, \"reflog\");\n }\n \n int cmd_gc(int argc,\n@@ -718,7 +719,6 @@ struct repository *repo UNUSED)\n \tshow_usage_with_options_if_asked(argc, argv,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n-\tstrvec_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n \tstrvec_pushl(&prune, \"prune\", \"--expire\", NULL);\n \tstrvec_pushl(&prune_worktrees, \"worktree\", \"prune\", \"--expire\", NULL);\n\n-- \n2.48.1.741.g8a9f3a5cdc.dirty\n\n"},{"id":"513103","messageId":"20250226-pks-maintenance-reflog-expire-v1-6-a1204a814952@pks.im","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"[PATCH 6/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-26T15:24:31Z","receivedAt":"2025-02-26T15:24:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"By default, git-maintenance(1) uses the \"gc\" task to ensure that the\nrepository is well-maintained. This can be changed, for example by\neither explicitly configuring which tasks should be enabled or by using\nthe \"incremental\" maintenance strategy. If so, git-maintenance(1) does\nnot know to expire reflog entries, which is a subtask that git-gc(1)\nknows to perform for the user. Consequently, the reflog will grow\nindefinitely unless the user manually trims it.\n\nIntroduce a new \"reflog-expire\" task that plugs this gap:\n\n  - When running the task directly, then we simply execute `git reflog\n    expire --all`, which is the same as git-gc(1).\n\n  - When running git-maintenance(1) with the `--auto` flag, then we only\n    run the task in case the \"HEAD\" reflog has at least N reflog entries\n    that would be discarded. By default, N is set to 100, but this can\n    be configured via \"maintenance.reflog-expire.auto\". When a negative\n    integer has been provided we always expire entries, zero causes us\n    to never expire entries, and a positive value specifies how many\n    entries need to exist before we consider pruning the entries.\n\nNote that the condition for the `--auto` flags is merely a heuristic and\noptimized for being fast. This is because `git maintenance run --auto`\nwill be executed quite regularly, so scanning through all reflogs would\nlikely be too expensive in many repositories.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++\n Documentation/git-maintenance.adoc    |  4 +++\n builtin/gc.c                          | 50 +++++++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 18 +++++++++++++\n 4 files changed, 81 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 72a9d6cf816..e57f346a067 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -69,3 +69,12 @@ maintenance.incremental-repack.auto::\n \tOtherwise, a positive value implies the command should run when the\n \tnumber of pack-files not in the multi-pack-index is at least the value\n \tof `maintenance.incremental-repack.auto`. The default value is 10.\n+\n+maintenance.reflog-expire.auto::\n+\tThis integer config option controls how often the `reflog-expire` task\n+\tshould be run as part of `git maintenance run --auto`. If zero, then\n+\tthe `reflog-expire` task will not run with the `--auto` option. A\n+\tnegative value will force the task to run every time. Otherwise, a\n+\tpositive value implies the command should run when the number of\n+\texpired reflog entries in the \"HEAD\" reflog is at least the value of\n+\t`maintenance.loose-objects.auto`. The default value is 100.\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 0450d74aff1..8bc94a6d4ff 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -158,6 +158,10 @@ pack-refs::\n \tneed to iterate across many references. See linkgit:git-pack-refs[1]\n \tfor more information.\n \n+reflog-expire::\n+\tThe `reflog-expire` task deletes any entries in the reflog older than the\n+\texpiry threshold. See linkgit:git-reflog[1] for more information.\n+\n OPTIONS\n -------\n --auto::\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e8f5705dc59..ce5bb2630f8 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -33,6 +33,7 @@\n #include \"pack.h\"\n #include \"pack-objects.h\"\n #include \"path.h\"\n+#include \"reflog.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n #include \"promisor-remote.h\"\n@@ -285,6 +286,49 @@ static int maintenance_task_pack_refs(struct maintenance_run_opts *opts,\n \treturn run_command(&cmd);\n }\n \n+struct count_reflog_entries_data {\n+\tstruct expire_reflog_policy_cb policy;\n+\tsize_t count;\n+\tsize_t limit;\n+};\n+\n+static int count_reflog_entries(struct object_id *old_oid, struct object_id *new_oid,\n+\t\t\t\tconst char *committer, timestamp_t timestamp,\n+\t\t\t\tint tz, const char *msg, void *cb_data)\n+{\n+\tstruct count_reflog_entries_data *data = cb_data;\n+\tif (should_expire_reflog_ent(old_oid, new_oid, committer, timestamp, tz, msg, &data->policy))\n+\t\tdata->count++;\n+\treturn data->count >= data->limit;\n+}\n+\n+static int reflog_expire_condition(struct gc_config *cfg UNUSED)\n+{\n+\ttimestamp_t now = time(NULL);\n+\tstruct count_reflog_entries_data data = {\n+\t\t.policy = {\n+\t\t\t.opts = REFLOG_EXPIRE_OPTIONS_INIT(now),\n+\t\t},\n+\t};\n+\tint limit = 100;\n+\n+\tgit_config_get_int(\"maintenance.reflog-expire.auto\", &limit);\n+\tif (!limit)\n+\t\treturn 0;\n+\tif (limit < 0)\n+\t\treturn 1;\n+\tdata.limit = limit;\n+\n+\trepo_config(the_repository, reflog_expire_config, &data.policy.opts);\n+\n+\treflog_expire_options_set_refname(&data.policy.opts, \"HEAD\");\n+\trefs_for_each_reflog_ent(get_main_ref_store(the_repository), \"HEAD\",\n+\t\t\t\t count_reflog_entries, &data);\n+\n+\treflog_expiry_cleanup(&data.policy);\n+\treturn data.count >= data.limit;\n+}\n+\n static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUSED,\n \t\t\t\t\t  struct gc_config *cfg UNUSED)\n {\n@@ -1383,6 +1427,7 @@ enum maintenance_task_label {\n \tTASK_GC,\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n+\tTASK_REFLOG_EXPIRE,\n \n \t/* Leave as final value */\n \tTASK__COUNT\n@@ -1419,6 +1464,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_pack_refs,\n \t\tpack_refs_condition,\n \t},\n+\t[TASK_REFLOG_EXPIRE] = {\n+\t\t\"reflog-expire\",\n+\t\tmaintenance_task_reflog_expire,\n+\t\treflog_expire_condition,\n+\t},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 1909aed95e0..ff98cde92c0 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -447,6 +447,24 @@ test_expect_success 'pack-refs task' '\n \ttest_subcommand git pack-refs --all --prune <pack-refs.txt\n '\n \n+test_expect_success 'reflog-expire task' '\n+\tGIT_TRACE2_EVENT=\"$(pwd)/reflog-expire.txt\" \\\n+\t\tgit maintenance run --task=reflog-expire &&\n+\ttest_subcommand git reflog expire --all <reflog-expire.txt\n+'\n+\n+test_expect_success 'reflog-expire task --auto only packs when exceeding limits' '\n+\tgit reflog expire --all --expire=now &&\n+\ttest_commit reflog-one &&\n+\ttest_commit reflog-two &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/reflog-expire-auto.txt\" \\\n+\t\tgit -c maintenance.reflog-expire.auto=3 maintenance run --auto --task=reflog-expire &&\n+\ttest_subcommand ! git reflog expire --all <reflog-expire-auto.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/reflog-expire-auto.txt\" \\\n+\t\tgit -c maintenance.reflog-expire.auto=2 maintenance run --auto --task=reflog-expire &&\n+\ttest_subcommand git reflog expire --all <reflog-expire-auto.txt\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"at most one\" err\n\n-- \n2.48.1.741.g8a9f3a5cdc.dirty\n\n"},{"id":"513111","messageId":"543e1914-d05c-4053-916d-e8286edecf50@ramsayjones.plus.com","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2025-02-26T17:50:04Z","receivedAt":"2025-02-26T17:53:15Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 26/02/2025 15:24, Patrick Steinhardt wrote:\n> Hi,\n> \n> this patch series introduces a new \"reflog-expire\" task to\n> git-maintenance(1). This task is designed to plug a gap when the \"gc\"\n> task is disabled, as there is no way to expire reflog entries in that\n> case.\n> \n> This patch series has been inspired by the discussion at [1]. I consider\n> it to be another step into the direction of replacing git-gc(1) and\n> allowing for more flexible maintenance strategies overall. Next steps\n\nHmm, I don't know what you have in mind, but just as a data-point, I have\nnever used, and have no inclination to use, git-maintenance. However, I do\nuse git-gc extensively: at least once (times the number of repos fetched\nwhich have changes) per day, pretty much every day! :)\n\nATB,\nRamsay Jones\n\n\n"},{"id":"513116","messageId":"xmqqtt8gbldf.fsf@gitster.g","threadId":"63007","inReplyTo":"543e1914-d05c-4053-916d-e8286edecf50@ramsayjones.plus.com","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-02-26T18:40:12Z","receivedAt":"2025-02-26T18:40:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ramsay Jones <ramsay@ramsayjones.plus.com> writes:\n\n> Hmm, I don't know what you have in mind, but just as a data-point, I have\n> never used, and have no inclination to use, git-maintenance. However, I do\n> use git-gc extensively: at least once (times the number of repos fetched\n> which have changes) per day, pretty much every day! :)\n\nThat makes two of us, but everybody knows that we are old fashioned ;-)\n"},{"id":"513117","messageId":"5ecea6cc-0702-47a7-91de-14aa06757d27@ramsayjones.plus.com","threadId":"63007","inReplyTo":"xmqqtt8gbldf.fsf@gitster.g","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2025-02-26T18:54:48Z","receivedAt":"2025-02-26T18:57:59Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 26/02/2025 18:40, Junio C Hamano wrote:\n> Ramsay Jones <ramsay@ramsayjones.plus.com> writes:\n> \n>> Hmm, I don't know what you have in mind, but just as a data-point, I have\n>> never used, and have no inclination to use, git-maintenance. However, I do\n>> use git-gc extensively: at least once (times the number of repos fetched\n>> which have changes) per day, pretty much every day! :)\n> \n> That makes two of us, but everybody knows that we are old fashioned ;-)\n\ntrue, very true. :)\n\nATB,\nRamsay Jones\n\n\n\n"},{"id":"513131","messageId":"xmqqikow9o5d.fsf@gitster.g","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-02-27T01:23:10Z","receivedAt":"2025-02-27T01:23:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> this patch series introduces a new \"reflog-expire\" task to\n> git-maintenance(1). This task is designed to plug a gap when the \"gc\"\n> task is disabled, as there is no way to expire reflog entries in that\n> case.\n\nI think in the longer run, \"maintenance\" users should be able to\ntreat the single ball of wax \"gc\" task as a mere short-hand to\ninvoke a set of often used maintenance tasks, and we would want to\nbreak down the component tasks grouped in it and make them\nindependently available.  This is a good step along that journey.\n\nAre there other things that the \"gc\" task covers that are not\navailable elsewhere?  \"git gc --help\" suggests there are things\nrelated to pruning (unused?) worktrees and stale rerere database\nentries.\n\nAnother thing, how much control do we want to cede to the end users\nthe choice of tasks and order of running them?  When you are\nexpiring stale reflog entries and repacking the object database to\ndiscard unreachable objects, it would only make sense to do them in\nthe order I just said.  We could leave it up to the end users, but\nthat may be doing disservice to them.\n\n"},{"id":"513136","messageId":"Z8AsGdPqDVXpqkhR@pks.im","threadId":"63007","inReplyTo":"5ecea6cc-0702-47a7-91de-14aa06757d27@ramsayjones.plus.com","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-27T09:10:49Z","receivedAt":"2025-02-27T09:10:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Feb 26, 2025 at 06:54:48PM +0000, Ramsay Jones wrote:\n> On 26/02/2025 18:40, Junio C Hamano wrote:\n> > Ramsay Jones <ramsay@ramsayjones.plus.com> writes:\n> > \n> >> Hmm, I don't know what you have in mind, but just as a data-point, I have\n> >> never used, and have no inclination to use, git-maintenance. However, I do\n> >> use git-gc extensively: at least once (times the number of repos fetched\n> >> which have changes) per day, pretty much every day! :)\n> > \n> > That makes two of us, but everybody knows that we are old fashioned ;-)\n> \n> true, very true. :)\n\nWell, it depends on what you mean by \"use\". In fact, both of you use it\nimplicitly assuming that you use a recent version of Git because that is\nwhat Git nowadays spawns automatically: we don't use `git gc --auto`\nanymore, but instead use `git maintenance run --auto`. It _does_ still\nuse git-gc(1) under the hood by default, but that is something we can\nchange going forward.\n\nThe opportunity here is to have a more fine-grained strategy to perform\nmaintenance, both when run explicitly but also when run automatically by\nGit. git-maintenance(1) is written in a way that makes it significantly\nmore flexible overall, so we can iterate on how exactly it performs the\nmaintenance for the user. Different strategies may make sense in some\ncontexts, but not in others, and that is something we can account for\nhere.\n\nIt also allows us to bring newer features to the masses that have a\nchance to improve performance or reduce the time spent maintaining\nrepositories for everyone: multi-pack indices, split commit graphs,\ngeometric repacking, incremental bitmaps.\n\nWhile we could move them into git-gc(1), I think that this tool is just\nnot well-suited for such changes as it simply doesn't provide a good\nfoundation for tweakable behaviour.\n\nPatrick\n"},{"id":"513137","messageId":"Z8Auzjw29t91tEuq@pks.im","threadId":"63007","inReplyTo":"xmqqikow9o5d.fsf@gitster.g","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-27T09:22:22Z","receivedAt":"2025-02-27T09:22:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Feb 26, 2025 at 05:23:10PM -0800, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > this patch series introduces a new \"reflog-expire\" task to\n> > git-maintenance(1). This task is designed to plug a gap when the \"gc\"\n> > task is disabled, as there is no way to expire reflog entries in that\n> > case.\n> \n> I think in the longer run, \"maintenance\" users should be able to\n> treat the single ball of wax \"gc\" task as a mere short-hand to\n> invoke a set of often used maintenance tasks, and we would want to\n> break down the component tasks grouped in it and make them\n> independently available.  This is a good step along that journey.\n> \n> Are there other things that the \"gc\" task covers that are not\n> available elsewhere?  \"git gc --help\" suggests there are things\n> related to pruning (unused?) worktrees and stale rerere database\n> entries.\n\nThese are more gaps indeed. I'm happy to work on them once this patch\nseries has landed. I don't know about any other gaps.\n\n> Another thing, how much control do we want to cede to the end users\n> the choice of tasks and order of running them?  When you are\n> expiring stale reflog entries and repacking the object database to\n> discard unreachable objects, it would only make sense to do them in\n> the order I just said.  We could leave it up to the end users, but\n> that may be doing disservice to them.\n\nThis is a good question. From my perspective, there are three classes of\nusers here:\n\n  - Those that don't care and don't have special needs. This class of\n    users is unlikely to tweak things anyway.\n\n  - Those that aren't deeply familiar with how Git works, but who do\n    have special needs e.g. because they have huge repositories. This\n    class of users may need to tweak configuration, but we should give\n    them an _easy_ way to do so. Configuring individual tasks ain't that\n    from my perspective.\n\n  - Power users that are deeply familiar with how Git works. This class\n    of users may even want to tweak the order in which specific tasks\n    run.\n\n\"maintenance.strategy\" exists to cater to the second class of users and\nallows them to configure the high-level strategy used to maintain repos.\nI don't know whether it's honored by `git maintenance run`, but I think\nit is (and if it's not it should be).\n\nThat to me means that the configuration for individual tasks for power\nusers can be as flexible as possible, including configuring the order in\nwhich tasks are run.\n\nPatrick\n"},{"id":"513167","messageId":"xmqq8qpr9v9e.fsf@gitster.g","threadId":"63007","inReplyTo":"Z8Auzjw29t91tEuq@pks.im","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-02-27T17:01:49Z","receivedAt":"2025-02-27T17:01:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Wed, Feb 26, 2025 at 05:23:10PM -0800, Junio C Hamano wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>> \n>> > this patch series introduces a new \"reflog-expire\" task to\n>> > git-maintenance(1). This task is designed to plug a gap when the \"gc\"\n>> > task is disabled, as there is no way to expire reflog entries in that\n>> > case.\n>> \n>> I think in the longer run, \"maintenance\" users should be able to\n>> treat the single ball of wax \"gc\" task as a mere short-hand to\n>> invoke a set of often used maintenance tasks, and we would want to\n>> break down the component tasks grouped in it and make them\n>> independently available.  This is a good step along that journey.\n>> \n>> Are there other things that the \"gc\" task covers that are not\n>> available elsewhere?  \"git gc --help\" suggests there are things\n>> related to pruning (unused?) worktrees and stale rerere database\n>> entries.\n>\n> These are more gaps indeed. I'm happy to work on them once this patch\n> series has landed. I don't know about any other gaps.\n\nOr maybe leave breadcrumbs and invite others to help advance the\ncause?  If we know we have achieved consensus that it is a good\ndirection to go in, that is (we already saw a mention that indicates\nthat there are populations of us who do not care too much about\nextending maintenance but are familiar with gc).\n\n"},{"id":"513241","messageId":"Z8F1NDvWgx1S3eze@pks.im","threadId":"63007","inReplyTo":"xmqq8qpr9v9e.fsf@gitster.g","subject":"Re: [PATCH 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-02-28T08:35:00Z","receivedAt":"2025-02-28T08:35:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Feb 27, 2025 at 09:01:49AM -0800, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > On Wed, Feb 26, 2025 at 05:23:10PM -0800, Junio C Hamano wrote:\n> >> Patrick Steinhardt <ps@pks.im> writes:\n> >> \n> >> > this patch series introduces a new \"reflog-expire\" task to\n> >> > git-maintenance(1). This task is designed to plug a gap when the \"gc\"\n> >> > task is disabled, as there is no way to expire reflog entries in that\n> >> > case.\n> >> \n> >> I think in the longer run, \"maintenance\" users should be able to\n> >> treat the single ball of wax \"gc\" task as a mere short-hand to\n> >> invoke a set of often used maintenance tasks, and we would want to\n> >> break down the component tasks grouped in it and make them\n> >> independently available.  This is a good step along that journey.\n> >> \n> >> Are there other things that the \"gc\" task covers that are not\n> >> available elsewhere?  \"git gc --help\" suggests there are things\n> >> related to pruning (unused?) worktrees and stale rerere database\n> >> entries.\n> >\n> > These are more gaps indeed. I'm happy to work on them once this patch\n> > series has landed. I don't know about any other gaps.\n> \n> Or maybe leave breadcrumbs and invite others to help advance the\n> cause?  If we know we have achieved consensus that it is a good\n> direction to go in, that is (we already saw a mention that indicates\n> that there are populations of us who do not care too much about\n> extending maintenance but are familiar with gc).\n\nOh, sure, I wouldn't mind at all if somebody else picked this up. The\nquestion to me is where to leave the breadcrumb, other than having it in\nthis thread.\n\nPatrick\n"},{"id":"513583","messageId":"ammsrvhyzo6f7hfcizlh42e3e5cznfb2j37tcicliqqpqaaqgs@4mniagc5ejzz","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-2-a1204a814952@pks.im","subject":"Re: [PATCH 2/6] builtin/reflog: stop storing default reflog expiry dates globally","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-03-04T23:23:53Z","receivedAt":"2025-03-04T23:27:16Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/02/26 04:24PM, Patrick Steinhardt wrote:\n> When expiring reflog entries, it is possible to configure expiry dates\n> that depend on the name of the reflog. This requires us to store a\n> couple of different expiry dates:\n> \n>   - The default expiry date for reflog entries that aren't otherwise\n>     specified.\n> \n>   - The per-reflog expiry date.\n> \n>   - The currently active set of expiry dates for a given reference.\n> \n> While the last item is stored in `struct reflog_expiry_options`, the\n\ns/reflog_expiry_options/reflog_expire_options/\n\n> other items aren't, which makes it hard to reuse the structure in other\n> places.\n> \n> Refactor the code so that the default expiry date is stored as part of\n> the structure. The per-reflog expiry dates will be adapted accordingly\n> in the subsequent commit.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/reflog.c | 22 +++++++---------------\n>  reflog.h         |  6 ++++++\n>  2 files changed, 13 insertions(+), 15 deletions(-)\n> \n> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> index dee49881d32..0910a4e25dc 100644\n> --- a/builtin/reflog.c\n> +++ b/builtin/reflog.c\n> @@ -63,9 +63,6 @@ static const char *const reflog_usage[] = {\n>  \tNULL\n>  };\n>  \n> -static timestamp_t default_reflog_expire;\n> -static timestamp_t default_reflog_expire_unreachable;\n> -\n>  struct worktree_reflogs {\n>  \tstruct worktree *worktree;\n>  \tstruct string_list reflogs;\n> @@ -122,6 +119,7 @@ static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len)\n>  static int reflog_expire_config(const char *var, const char *value,\n>  \t\t\t\tconst struct config_context *ctx, void *cb)\n>  {\n> +\tstruct reflog_expire_options *opts = cb;\n>  \tconst char *pattern, *key;\n>  \tsize_t pattern_len;\n>  \ttimestamp_t expire;\n> @@ -145,10 +143,10 @@ static int reflog_expire_config(const char *var, const char *value,\n>  \tif (!pattern) {\n>  \t\tswitch (slot) {\n>  \t\tcase EXPIRE_TOTAL:\n> -\t\t\tdefault_reflog_expire = expire;\n> +\t\t\topts->default_expire_total = expire;\n>  \t\t\tbreak;\n>  \t\tcase EXPIRE_UNREACH:\n> -\t\t\tdefault_reflog_expire_unreachable = expire;\n> +\t\t\topts->default_expire_unreachable = expire;\n\nOk, instead of setting the default fallback expiry values globably, it\nis set in the options type.\n\n>  \t\t\tbreak;\n>  \t\t}\n>  \t\treturn 0;\n> @@ -198,9 +196,9 @@ static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char\n>  \n>  \t/* Nothing matched -- use the default value */\n>  \tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n> -\t\tcb->expire_total = default_reflog_expire;\n> +\t\tcb->expire_total = cb->default_expire_total;\n>  \tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n> -\t\tcb->expire_unreachable = default_reflog_expire_unreachable;\n> +\t\tcb->expire_unreachable = cb->default_expire_unreachable;\n\nHere we update where the defaults are used.\n\n>  }\n>  \n>  static int expire_unreachable_callback(const struct option *opt,\n> @@ -276,8 +274,8 @@ static int cmd_reflog_list(int argc, const char **argv, const char *prefix,\n>  static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n>  \t\t\t     struct repository *repo UNUSED)\n>  {\n> -\tstruct reflog_expire_options opts = { 0 };\n>  \ttimestamp_t now = time(NULL);\n> +\tstruct reflog_expire_options opts = REFLOG_EXPIRE_OPTIONS_INIT(now);\n\nThe options type is initialized with the default defaults :)\n\n>  \tint i, status, do_all, single_worktree = 0;\n>  \tunsigned int flags = 0;\n>  \tint verbose = 0;\n> @@ -308,17 +306,11 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n>  \t\tOPT_END()\n>  \t};\n>  \n> -\tdefault_reflog_expire_unreachable = now - 30 * 24 * 3600;\n> -\tdefault_reflog_expire = now - 90 * 24 * 3600;\n> -\tgit_config(reflog_expire_config, NULL);\n> +\tgit_config(reflog_expire_config, &opts);\n>  \n>  \tsave_commit_buffer = 0;\n>  \tdo_all = status = 0;\n>  \n> -\topts.explicit_expiry = 0;\n> -\topts.expire_total = default_reflog_expire;\n> -\topts.expire_unreachable = default_reflog_expire_unreachable;\n> -\n>  \targc = parse_options(argc, argv, prefix, options, reflog_expire_usage, 0);\n>  \n>  \tif (verbose)\n> diff --git a/reflog.h b/reflog.h\n> index eb948119e53..a9d464bbf8c 100644\n> --- a/reflog.h\n> +++ b/reflog.h\n> @@ -5,10 +5,16 @@\n>  struct reflog_expire_options {\n>  \tint stalefix;\n>  \tint explicit_expiry;\n> +\ttimestamp_t default_expire_total;\n>  \ttimestamp_t expire_total;\n> +\ttimestamp_t default_expire_unreachable;\n>  \ttimestamp_t expire_unreachable;\n>  \tint recno;\n>  };\n> +#define REFLOG_EXPIRE_OPTIONS_INIT(now) { \\\n> +\t.default_expire_total = now - 30 * 24 * 3600, \\\n> +\t.default_expire_unreachable = now - 90 * 24 * 3600, \\\n> +}\n\nLooking good\n\n>  \n>  struct expire_reflog_policy_cb {\n>  \tenum {\n> \n> -- \n> 2.48.1.741.g8a9f3a5cdc.dirty\n> \n> \n"},{"id":"513584","messageId":"usf4pzrp322xvfrvsasgy22rnw7d2a6krzuctwpp23ppdgh6ji@ram73wv5dn54","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-3-a1204a814952@pks.im","subject":"Re: [PATCH 3/6] builtin/reflog: stop storing per-reflog expiry dates globally","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-03-04T23:41:08Z","receivedAt":"2025-03-04T23:44:30Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/02/26 04:24PM, Patrick Steinhardt wrote:\n> As described in the preceding commit, the per-reflog expiry dates are\n> stored in a global pair of variables. Refactor the code so that they are\n> contained in `sturct reflog_expire_options` to make the structure useful\n\ns/sturct/struct/\n\n> in other contexts.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/reflog.c | 30 ++++++++++++------------------\n>  reflog.h         |  8 ++++++++\n>  2 files changed, 20 insertions(+), 18 deletions(-)\n> \n> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> index 0910a4e25dc..a231cf4b857 100644\n> --- a/builtin/reflog.c\n> +++ b/builtin/reflog.c\n> @@ -88,27 +88,21 @@ static int collect_reflog(const char *ref, void *cb_data)\n>  \treturn 0;\n>  }\n>  \n> -static struct reflog_expire_cfg {\n> -\tstruct reflog_expire_cfg *next;\n> -\ttimestamp_t expire_total;\n> -\ttimestamp_t expire_unreachable;\n> -\tchar pattern[FLEX_ARRAY];\n> -} *reflog_expire_cfg, **reflog_expire_cfg_tail;\n> -\n> -static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len)\n> +static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,\n> +\t\t\t\t\t\t       const char *pattern, size_t len)\n>  {\n> -\tstruct reflog_expire_cfg *ent;\n> +\tstruct reflog_expire_entry_option *ent;\n>  \n> -\tif (!reflog_expire_cfg_tail)\n> -\t\treflog_expire_cfg_tail = &reflog_expire_cfg;\n> +\tif (!opts->entries_tail)\n> +\t\topts->entries_tail = &opts->entries;\n>  \n> -\tfor (ent = reflog_expire_cfg; ent; ent = ent->next)\n> +\tfor (ent = opts->entries; ent; ent = ent->next)\n>  \t\tif (!xstrncmpz(ent->pattern, pattern, len))\n>  \t\t\treturn ent;\n>  \n>  \tFLEX_ALLOC_MEM(ent, pattern, pattern, len);\n> -\t*reflog_expire_cfg_tail = ent;\n> -\treflog_expire_cfg_tail = &(ent->next);\n> +\t*opts->entries_tail = ent;\n> +\topts->entries_tail = &(ent->next);\n>  \treturn ent;\n>  }\n>  \n> @@ -124,7 +118,7 @@ static int reflog_expire_config(const char *var, const char *value,\n>  \tsize_t pattern_len;\n>  \ttimestamp_t expire;\n>  \tint slot;\n> -\tstruct reflog_expire_cfg *ent;\n> +\tstruct reflog_expire_entry_option *ent;\n>  \n>  \tif (parse_config_key(var, \"gc\", &pattern, &pattern_len, &key) < 0)\n>  \t\treturn git_default_config(var, value, ctx, cb);\n> @@ -152,7 +146,7 @@ static int reflog_expire_config(const char *var, const char *value,\n>  \t\treturn 0;\n>  \t}\n>  \n> -\tent = find_cfg_ent(pattern, pattern_len);\n> +\tent = find_cfg_ent(opts, pattern, pattern_len);\n>  \tif (!ent)\n>  \t\treturn -1;\n>  \tswitch (slot) {\n> @@ -168,12 +162,12 @@ static int reflog_expire_config(const char *var, const char *value,\n>  \n>  static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char *ref)\n>  {\n> -\tstruct reflog_expire_cfg *ent;\n> +\tstruct reflog_expire_entry_option *ent;\n>  \n>  \tif (cb->explicit_expiry == (EXPIRE_TOTAL|EXPIRE_UNREACH))\n>  \t\treturn; /* both given explicitly -- nothing to tweak */\n>  \n> -\tfor (ent = reflog_expire_cfg; ent; ent = ent->next) {\n> +\tfor (ent = cb->entries; ent; ent = ent->next) {\n>  \t\tif (!wildmatch(ent->pattern, ref, 0)) {\n>  \t\t\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n>  \t\t\t\tcb->expire_total = ent->expire_total;\n> diff --git a/reflog.h b/reflog.h\n> index a9d464bbf8c..b08780a30a7 100644\n> --- a/reflog.h\n> +++ b/reflog.h\n> @@ -2,7 +2,15 @@\n>  #define REFLOG_H\n>  #include \"refs.h\"\n>  \n> +struct reflog_expire_entry_option {\n> +\tstruct reflog_expire_entry_option *next;\n> +\ttimestamp_t expire_total;\n> +\ttimestamp_t expire_unreachable;\n> +\tchar pattern[FLEX_ARRAY];\n> +};\n> +\n>  struct reflog_expire_options {\n> +\tstruct reflog_expire_entry_option *entries, **entries_tail;\n\nNow we can also store the configured per-reflog-pattern expiry entries\nin the options type instead of relying on global state.\n\n>  \tint stalefix;\n>  \tint explicit_expiry;\n>  \ttimestamp_t default_expire_total;\n\nNow that all the reflog expiry configuration is contained within\nreflog_expire_options, I wonder if it really makes sense to also keep\nthe expire_total and expire_unreachable fields.\n\nFrom my understanding these fields are not really for configuration, but\nhold the reflog expiry configuration for the current active reference\nwhile iterating. This gets set by set_reflog_expiry_param() prior to\ncalling refs_reflog_expire(). It seems like this could be figured out\nduring refs_reflog_expire() now.\n\n-Justin\n"},{"id":"513645","messageId":"Z8l69bRy8PY3Ep9J@pks.im","threadId":"63007","inReplyTo":"usf4pzrp322xvfrvsasgy22rnw7d2a6krzuctwpp23ppdgh6ji@ram73wv5dn54","subject":"Re: [PATCH 3/6] builtin/reflog: stop storing per-reflog expiry dates globally","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-03-06T10:37:41Z","receivedAt":"2025-03-06T10:37:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Mar 04, 2025 at 05:41:08PM -0600, Justin Tobler wrote:\n> On 25/02/26 04:24PM, Patrick Steinhardt wrote:\n> > diff --git a/reflog.h b/reflog.h\n> > index a9d464bbf8c..b08780a30a7 100644\n> > --- a/reflog.h\n> > +++ b/reflog.h\n> > @@ -2,7 +2,15 @@\n> >  #define REFLOG_H\n> >  #include \"refs.h\"\n> >  \n> > +struct reflog_expire_entry_option {\n> > +\tstruct reflog_expire_entry_option *next;\n> > +\ttimestamp_t expire_total;\n> > +\ttimestamp_t expire_unreachable;\n> > +\tchar pattern[FLEX_ARRAY];\n> > +};\n> > +\n> >  struct reflog_expire_options {\n> > +\tstruct reflog_expire_entry_option *entries, **entries_tail;\n> \n> Now we can also store the configured per-reflog-pattern expiry entries\n> in the options type instead of relying on global state.\n> \n> >  \tint stalefix;\n> >  \tint explicit_expiry;\n> >  \ttimestamp_t default_expire_total;\n> \n> Now that all the reflog expiry configuration is contained within\n> reflog_expire_options, I wonder if it really makes sense to also keep\n> the expire_total and expire_unreachable fields.\n> \n> From my understanding these fields are not really for configuration, but\n> hold the reflog expiry configuration for the current active reference\n> while iterating. This gets set by set_reflog_expiry_param() prior to\n> calling refs_reflog_expire(). It seems like this could be figured out\n> during refs_reflog_expire() now.\n\nYes, these fields hold state indeed, namely the value for a given\nrefname. These fields thus need to be updated for every refname for\nwhich you want to check expiration, which is done by calling\n`reflog_expire_options_set_refname()`. This interfaces is extremely\nawkward from my perspective, and it would be very much preferable to\ninstead have an interface that, given the options and a refname as\nparameters, tells you whether the reflog contains entries that should be\npruned.\n\nIn fact, I did have a look at fixing this awkward interface, but it\nalways ended up with way more changes than I felt comfortable with. So I\ndecided to only go a couple steps into the direction of better\nencapsulation, but to not fix all of the design issues with the current\ninterface.\n\nPatrick\n"},{"id":"513732","messageId":"35jnncjitwoy7bb7w5zyk6nhjvab4tmdjhtuqltljypfigk2g7@swpulgkzooxl","threadId":"63007","inReplyTo":"Z8l69bRy8PY3Ep9J@pks.im","subject":"Re: [PATCH 3/6] builtin/reflog: stop storing per-reflog expiry dates globally","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-03-06T23:17:37Z","receivedAt":"2025-03-06T23:21:01Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/03/06 11:37AM, Patrick Steinhardt wrote:\n> > Now that all the reflog expiry configuration is contained within\n> > reflog_expire_options, I wonder if it really makes sense to also keep\n> > the expire_total and expire_unreachable fields.\n> > \n> > From my understanding these fields are not really for configuration, but\n> > hold the reflog expiry configuration for the current active reference\n> > while iterating. This gets set by set_reflog_expiry_param() prior to\n> > calling refs_reflog_expire(). It seems like this could be figured out\n> > during refs_reflog_expire() now.\n> \n> Yes, these fields hold state indeed, namely the value for a given\n> refname. These fields thus need to be updated for every refname for\n> which you want to check expiration, which is done by calling\n> `reflog_expire_options_set_refname()`. This interfaces is extremely\n> awkward from my perspective, and it would be very much preferable to\n> instead have an interface that, given the options and a refname as\n> parameters, tells you whether the reflog contains entries that should be\n> pruned.\n> \n> In fact, I did have a look at fixing this awkward interface, but it\n> always ended up with way more changes than I felt comfortable with. So I\n> decided to only go a couple steps into the direction of better\n> encapsulation, but to not fix all of the design issues with the current\n> interface.\n\nThat's fair. This interface certainly feels awkward and exposing it\nfurther doesn't seem ideal. At the same time, maybe its not too big of a\ndeal that it should be dealt with now.\n\n-Justin\n"},{"id":"515822","messageId":"20250408-pks-maintenance-reflog-expire-v2-0-1ad8634798b7@pks.im","threadId":"63007","inReplyTo":"20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im","subject":"[PATCH v2 0/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-08T06:22:11Z","receivedAt":"2025-04-08T06:22:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series introduces a new \"reflog-expire\" task to\ngit-maintenance(1). This task is designed to plug a gap when the \"gc\"\ntask is disabled, as there is no way to expire reflog entries in that\ncase.\n\nThis patch series has been inspired by the discussion at [1]. I consider\nit to be another step into the direction of replacing git-gc(1) and\nallowing for more flexible maintenance strategies overall. Next steps\ncould be:\n\n  1. Enable the \"reflog-expire\" task by default when using the\n     \"incremental\" strategy. and then we might eventually switch over\n     the\n\n  2. Use \"incremental\" strategy when \"features.experimental\" is enabled.\n\n  3. Switch over the default strategy to \"incremental\" after a couple of\n     releases.\n\nChanges in v2:\n  - Two commit message typos.\n  - Link to v1: https://lore.kernel.org/r/20250226-pks-maintenance-reflog-expire-v1-0-a1204a814952@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <e650f4e4-e267-4f1f-bb3a-c71b1fe0b276@uxp.de>\n\n---\nPatrick Steinhardt (6):\n      reflog: rename `cmd_reflog_expire_cb` to `reflog_expire_options`\n      builtin/reflog: stop storing default reflog expiry dates globally\n      builtin/reflog: stop storing per-reflog expiry dates globally\n      builtin/reflog: make functions regarding `reflog_expire_options` public\n      builtin/gc: split out function to expire reflog entries\n      builtin/maintenance: introduce \"reflog-expire\" task\n\n Documentation/config/maintenance.adoc |   9 ++\n Documentation/git-maintenance.adoc    |   4 +\n builtin/gc.c                          |  72 +++++++++++++---\n builtin/reflog.c                      | 153 ++++------------------------------\n reflog.c                              | 137 ++++++++++++++++++++++++++----\n reflog.h                              |  35 +++++++-\n t/t7900-maintenance.sh                |  18 ++++\n 7 files changed, 263 insertions(+), 165 deletions(-)\n\nRange-diff versus v1:\n\n1:  62ed4d1dcb6 = 1:  92d20e9403b reflog: rename `cmd_reflog_expire_cb` to `reflog_expire_options`\n2:  c9da014e85c ! 2:  5625b7472f8 builtin/reflog: stop storing default reflog expiry dates globally\n    @@ Commit message\n     \n           - The currently active set of expiry dates for a given reference.\n     \n    -    While the last item is stored in `struct reflog_expiry_options`, the\n    +    While the last item is stored in `struct reflog_expire_options`, the\n         other items aren't, which makes it hard to reuse the structure in other\n         places.\n     \n3:  bd06fee3d23 ! 3:  146080fe970 builtin/reflog: stop storing per-reflog expiry dates globally\n    @@ Commit message\n     \n         As described in the preceding commit, the per-reflog expiry dates are\n         stored in a global pair of variables. Refactor the code so that they are\n    -    contained in `sturct reflog_expire_options` to make the structure useful\n    +    contained in `struct reflog_expire_options` to make the structure useful\n         in other contexts.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n4:  90ec3a8ffc8 = 4:  0f40bf3f45a builtin/reflog: make functions regarding `reflog_expire_options` public\n5:  17d0e940266 = 5:  3072a421957 builtin/gc: split out function to expire reflog entries\n6:  5799eb30843 = 6:  ca81a7e5686 builtin/maintenance: introduce \"reflog-expire\" task\n\n---\nbase-commit: 5a526e5e18ddb9a7dfc5a2967d21d6154df64a4f\nchange-id: 20250226-pks-maintenance-reflog-expire-61c61410751a\n\n"},{"id":"515823","messageId":"20250408-pks-maintenance-reflog-expire-v2-2-1ad8634798b7@pks.im","threadId":"63007","inReplyTo":"20250408-pks-maintenance-reflog-expire-v2-0-1ad8634798b7@pks.im","subject":"[PATCH v2 2/6] builtin/reflog: stop storing default reflog expiry dates globally","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-08T06:22:13Z","receivedAt":"2025-04-08T06:22:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When expiring reflog entries, it is possible to configure expiry dates\nthat depend on the name of the reflog. This requires us to store a\ncouple of different expiry dates:\n\n  - The default expiry date for reflog entries that aren't otherwise\n    specified.\n\n  - The per-reflog expiry date.\n\n  - The currently active set of expiry dates for a given reference.\n\nWhile the last item is stored in `struct reflog_expire_options`, the\nother items aren't, which makes it hard to reuse the structure in other\nplaces.\n\nRefactor the code so that the default expiry date is stored as part of\nthe structure. The per-reflog expiry dates will be adapted accordingly\nin the subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 22 +++++++---------------\n reflog.h         |  6 ++++++\n 2 files changed, 13 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex dee49881d32..0910a4e25dc 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -63,9 +63,6 @@ static const char *const reflog_usage[] = {\n \tNULL\n };\n \n-static timestamp_t default_reflog_expire;\n-static timestamp_t default_reflog_expire_unreachable;\n-\n struct worktree_reflogs {\n \tstruct worktree *worktree;\n \tstruct string_list reflogs;\n@@ -122,6 +119,7 @@ static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len)\n static int reflog_expire_config(const char *var, const char *value,\n \t\t\t\tconst struct config_context *ctx, void *cb)\n {\n+\tstruct reflog_expire_options *opts = cb;\n \tconst char *pattern, *key;\n \tsize_t pattern_len;\n \ttimestamp_t expire;\n@@ -145,10 +143,10 @@ static int reflog_expire_config(const char *var, const char *value,\n \tif (!pattern) {\n \t\tswitch (slot) {\n \t\tcase EXPIRE_TOTAL:\n-\t\t\tdefault_reflog_expire = expire;\n+\t\t\topts->default_expire_total = expire;\n \t\t\tbreak;\n \t\tcase EXPIRE_UNREACH:\n-\t\t\tdefault_reflog_expire_unreachable = expire;\n+\t\t\topts->default_expire_unreachable = expire;\n \t\t\tbreak;\n \t\t}\n \t\treturn 0;\n@@ -198,9 +196,9 @@ static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char\n \n \t/* Nothing matched -- use the default value */\n \tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\tcb->expire_total = default_reflog_expire;\n+\t\tcb->expire_total = cb->default_expire_total;\n \tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\tcb->expire_unreachable = default_reflog_expire_unreachable;\n+\t\tcb->expire_unreachable = cb->default_expire_unreachable;\n }\n \n static int expire_unreachable_callback(const struct option *opt,\n@@ -276,8 +274,8 @@ static int cmd_reflog_list(int argc, const char **argv, const char *prefix,\n static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\t     struct repository *repo UNUSED)\n {\n-\tstruct reflog_expire_options opts = { 0 };\n \ttimestamp_t now = time(NULL);\n+\tstruct reflog_expire_options opts = REFLOG_EXPIRE_OPTIONS_INIT(now);\n \tint i, status, do_all, single_worktree = 0;\n \tunsigned int flags = 0;\n \tint verbose = 0;\n@@ -308,17 +306,11 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\tOPT_END()\n \t};\n \n-\tdefault_reflog_expire_unreachable = now - 30 * 24 * 3600;\n-\tdefault_reflog_expire = now - 90 * 24 * 3600;\n-\tgit_config(reflog_expire_config, NULL);\n+\tgit_config(reflog_expire_config, &opts);\n \n \tsave_commit_buffer = 0;\n \tdo_all = status = 0;\n \n-\topts.explicit_expiry = 0;\n-\topts.expire_total = default_reflog_expire;\n-\topts.expire_unreachable = default_reflog_expire_unreachable;\n-\n \targc = parse_options(argc, argv, prefix, options, reflog_expire_usage, 0);\n \n \tif (verbose)\ndiff --git a/reflog.h b/reflog.h\nindex eb948119e53..a9d464bbf8c 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -5,10 +5,16 @@\n struct reflog_expire_options {\n \tint stalefix;\n \tint explicit_expiry;\n+\ttimestamp_t default_expire_total;\n \ttimestamp_t expire_total;\n+\ttimestamp_t default_expire_unreachable;\n \ttimestamp_t expire_unreachable;\n \tint recno;\n };\n+#define REFLOG_EXPIRE_OPTIONS_INIT(now) { \\\n+\t.default_expire_total = now - 30 * 24 * 3600, \\\n+\t.default_expire_unreachable = now - 90 * 24 * 3600, \\\n+}\n \n struct expire_reflog_policy_cb {\n \tenum {\n\n-- \n2.49.0.682.gc9b6a7b2b0.dirty\n\n"},{"id":"515825","messageId":"20250408-pks-maintenance-reflog-expire-v2-1-1ad8634798b7@pks.im","threadId":"63007","inReplyTo":"20250408-pks-maintenance-reflog-expire-v2-0-1ad8634798b7@pks.im","subject":"[PATCH v2 1/6] reflog: rename `cmd_reflog_expire_cb` to `reflog_expire_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-08T06:22:12Z","receivedAt":"2025-04-08T06:22:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to expose `struct cmd_reflog_expire_cb` via \"reflog.h\" so\nthat we can also use this structure in \"builtin/gc.c\". Once we make it\naccessible to a wider scope though it becomes awkwardly named, as it\nisn't only useful in the context of a callback. Instead, the function is\ncontaining all kinds of options relevant to whether or not a reflog\nentry should be expired.\n\nRename the structure to `reflog_expire_options` to prepare for this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 38 +++++++++++++++++++-------------------\n reflog.c         | 30 +++++++++++++++---------------\n reflog.h         |  4 ++--\n 3 files changed, 36 insertions(+), 36 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 95f264989bb..dee49881d32 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -168,7 +168,7 @@ static int reflog_expire_config(const char *var, const char *value,\n \treturn 0;\n }\n \n-static void set_reflog_expiry_param(struct cmd_reflog_expire_cb *cb, const char *ref)\n+static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char *ref)\n {\n \tstruct reflog_expire_cfg *ent;\n \n@@ -207,15 +207,15 @@ static int expire_unreachable_callback(const struct option *opt,\n \t\t\t\t const char *arg,\n \t\t\t\t int unset)\n {\n-\tstruct cmd_reflog_expire_cb *cmd = opt->value;\n+\tstruct reflog_expire_options *opts = opt->value;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tif (parse_expiry_date(arg, &cmd->expire_unreachable))\n+\tif (parse_expiry_date(arg, &opts->expire_unreachable))\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\tcmd->explicit_expiry |= EXPIRE_UNREACH;\n+\topts->explicit_expiry |= EXPIRE_UNREACH;\n \treturn 0;\n }\n \n@@ -223,15 +223,15 @@ static int expire_total_callback(const struct option *opt,\n \t\t\t\t const char *arg,\n \t\t\t\t int unset)\n {\n-\tstruct cmd_reflog_expire_cb *cmd = opt->value;\n+\tstruct reflog_expire_options *opts = opt->value;\n \n \tBUG_ON_OPT_NEG(unset);\n \n-\tif (parse_expiry_date(arg, &cmd->expire_total))\n+\tif (parse_expiry_date(arg, &opts->expire_total))\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\tcmd->explicit_expiry |= EXPIRE_TOTAL;\n+\topts->explicit_expiry |= EXPIRE_TOTAL;\n \treturn 0;\n }\n \n@@ -276,7 +276,7 @@ static int cmd_reflog_list(int argc, const char **argv, const char *prefix,\n static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\t     struct repository *repo UNUSED)\n {\n-\tstruct cmd_reflog_expire_cb cmd = { 0 };\n+\tstruct reflog_expire_options opts = { 0 };\n \ttimestamp_t now = time(NULL);\n \tint i, status, do_all, single_worktree = 0;\n \tunsigned int flags = 0;\n@@ -292,15 +292,15 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\tN_(\"update the reference to the value of the top reflog entry\"),\n \t\t\tEXPIRE_REFLOGS_UPDATE_REF),\n \t\tOPT_BOOL(0, \"verbose\", &verbose, N_(\"print extra information on screen\")),\n-\t\tOPT_CALLBACK_F(0, \"expire\", &cmd, N_(\"timestamp\"),\n+\t\tOPT_CALLBACK_F(0, \"expire\", &opts, N_(\"timestamp\"),\n \t\t\t       N_(\"prune entries older than the specified time\"),\n \t\t\t       PARSE_OPT_NONEG,\n \t\t\t       expire_total_callback),\n-\t\tOPT_CALLBACK_F(0, \"expire-unreachable\", &cmd, N_(\"timestamp\"),\n+\t\tOPT_CALLBACK_F(0, \"expire-unreachable\", &opts, N_(\"timestamp\"),\n \t\t\t       N_(\"prune entries older than <time> that are not reachable from the current tip of the branch\"),\n \t\t\t       PARSE_OPT_NONEG,\n \t\t\t       expire_unreachable_callback),\n-\t\tOPT_BOOL(0, \"stale-fix\", &cmd.stalefix,\n+\t\tOPT_BOOL(0, \"stale-fix\", &opts.stalefix,\n \t\t\t N_(\"prune any reflog entries that point to broken commits\")),\n \t\tOPT_BOOL(0, \"all\", &do_all, N_(\"process the reflogs of all references\")),\n \t\tOPT_BOOL(0, \"single-worktree\", &single_worktree,\n@@ -315,9 +315,9 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \tsave_commit_buffer = 0;\n \tdo_all = status = 0;\n \n-\tcmd.explicit_expiry = 0;\n-\tcmd.expire_total = default_reflog_expire;\n-\tcmd.expire_unreachable = default_reflog_expire_unreachable;\n+\topts.explicit_expiry = 0;\n+\topts.expire_total = default_reflog_expire;\n+\topts.expire_unreachable = default_reflog_expire_unreachable;\n \n \targc = parse_options(argc, argv, prefix, options, reflog_expire_usage, 0);\n \n@@ -329,7 +329,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t * even in older repository.  We cannot trust what's reachable\n \t * from reflog if the repository was pruned with older git.\n \t */\n-\tif (cmd.stalefix) {\n+\tif (opts.stalefix) {\n \t\tstruct rev_info revs;\n \n \t\trepo_init_revisions(the_repository, &revs, prefix);\n@@ -363,11 +363,11 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \n \t\tfor_each_string_list_item(item, &collected.reflogs) {\n \t\t\tstruct expire_reflog_policy_cb cb = {\n-\t\t\t\t.cmd = cmd,\n+\t\t\t\t.opts = opts,\n \t\t\t\t.dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN),\n \t\t\t};\n \n-\t\t\tset_reflog_expiry_param(&cb.cmd,  item->string);\n+\t\t\tset_reflog_expiry_param(&cb.opts,  item->string);\n \t\t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t\t     item->string, flags,\n \t\t\t\t\t\t     reflog_expiry_prepare,\n@@ -380,13 +380,13 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \n \tfor (i = 0; i < argc; i++) {\n \t\tchar *ref;\n-\t\tstruct expire_reflog_policy_cb cb = { .cmd = cmd };\n+\t\tstruct expire_reflog_policy_cb cb = { .opts = opts };\n \n \t\tif (!repo_dwim_log(the_repository, argv[i], strlen(argv[i]), NULL, &ref)) {\n \t\t\tstatus |= error(_(\"%s points nowhere!\"), argv[i]);\n \t\t\tcontinue;\n \t\t}\n-\t\tset_reflog_expiry_param(&cb.cmd, ref);\n+\t\tset_reflog_expiry_param(&cb.opts, ref);\n \t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t     ref, flags,\n \t\t\t\t\t     reflog_expiry_prepare,\ndiff --git a/reflog.c b/reflog.c\nindex 1b5f031f6d7..bcdb75514d0 100644\n--- a/reflog.c\n+++ b/reflog.c\n@@ -252,15 +252,15 @@ int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,\n \tstruct expire_reflog_policy_cb *cb = cb_data;\n \tstruct commit *old_commit, *new_commit;\n \n-\tif (timestamp < cb->cmd.expire_total)\n+\tif (timestamp < cb->opts.expire_total)\n \t\treturn 1;\n \n \told_commit = new_commit = NULL;\n-\tif (cb->cmd.stalefix &&\n+\tif (cb->opts.stalefix &&\n \t    (!keep_entry(&old_commit, ooid) || !keep_entry(&new_commit, noid)))\n \t\treturn 1;\n \n-\tif (timestamp < cb->cmd.expire_unreachable) {\n+\tif (timestamp < cb->opts.expire_unreachable) {\n \t\tswitch (cb->unreachable_expire_kind) {\n \t\tcase UE_ALWAYS:\n \t\t\treturn 1;\n@@ -272,7 +272,7 @@ int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,\n \t\t}\n \t}\n \n-\tif (cb->cmd.recno && --(cb->cmd.recno) == 0)\n+\tif (cb->opts.recno && --(cb->opts.recno) == 0)\n \t\treturn 1;\n \n \treturn 0;\n@@ -331,7 +331,7 @@ void reflog_expiry_prepare(const char *refname,\n \tstruct commit_list *elem;\n \tstruct commit *commit = NULL;\n \n-\tif (!cb->cmd.expire_unreachable || is_head(refname)) {\n+\tif (!cb->opts.expire_unreachable || is_head(refname)) {\n \t\tcb->unreachable_expire_kind = UE_HEAD;\n \t} else {\n \t\tcommit = lookup_commit_reference_gently(the_repository,\n@@ -341,7 +341,7 @@ void reflog_expiry_prepare(const char *refname,\n \t\tcb->unreachable_expire_kind = commit ? UE_NORMAL : UE_ALWAYS;\n \t}\n \n-\tif (cb->cmd.expire_unreachable <= cb->cmd.expire_total)\n+\tif (cb->opts.expire_unreachable <= cb->opts.expire_total)\n \t\tcb->unreachable_expire_kind = UE_ALWAYS;\n \n \tswitch (cb->unreachable_expire_kind) {\n@@ -358,7 +358,7 @@ void reflog_expiry_prepare(const char *refname,\n \t\t/* For reflog_expiry_cleanup() below */\n \t\tcb->tip_commit = commit;\n \t}\n-\tcb->mark_limit = cb->cmd.expire_total;\n+\tcb->mark_limit = cb->opts.expire_total;\n \tmark_reachable(cb);\n }\n \n@@ -390,7 +390,7 @@ int count_reflog_ent(struct object_id *ooid UNUSED,\n \t\t     timestamp_t timestamp, int tz UNUSED,\n \t\t     const char *message UNUSED, void *cb_data)\n {\n-\tstruct cmd_reflog_expire_cb *cb = cb_data;\n+\tstruct reflog_expire_options *cb = cb_data;\n \tif (!cb->expire_total || timestamp < cb->expire_total)\n \t\tcb->recno++;\n \treturn 0;\n@@ -398,7 +398,7 @@ int count_reflog_ent(struct object_id *ooid UNUSED,\n \n int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)\n {\n-\tstruct cmd_reflog_expire_cb cmd = { 0 };\n+\tstruct reflog_expire_options opts = { 0 };\n \tint status = 0;\n \treflog_expiry_should_prune_fn *should_prune_fn = should_expire_reflog_ent;\n \tconst char *spec = strstr(rev, \"@{\");\n@@ -421,17 +421,17 @@ int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)\n \n \trecno = strtoul(spec + 2, &ep, 10);\n \tif (*ep == '}') {\n-\t\tcmd.recno = -recno;\n+\t\topts.recno = -recno;\n \t\trefs_for_each_reflog_ent(get_main_ref_store(the_repository),\n-\t\t\t\t\t ref, count_reflog_ent, &cmd);\n+\t\t\t\t\t ref, count_reflog_ent, &opts);\n \t} else {\n-\t\tcmd.expire_total = approxidate(spec + 2);\n+\t\topts.expire_total = approxidate(spec + 2);\n \t\trefs_for_each_reflog_ent(get_main_ref_store(the_repository),\n-\t\t\t\t\t ref, count_reflog_ent, &cmd);\n-\t\tcmd.expire_total = 0;\n+\t\t\t\t\t ref, count_reflog_ent, &opts);\n+\t\topts.expire_total = 0;\n \t}\n \n-\tcb.cmd = cmd;\n+\tcb.opts = opts;\n \tstatus |= refs_reflog_expire(get_main_ref_store(the_repository), ref,\n \t\t\t\t     flags,\n \t\t\t\t     reflog_expiry_prepare,\ndiff --git a/reflog.h b/reflog.h\nindex d2906fb9f8d..eb948119e53 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -2,7 +2,7 @@\n #define REFLOG_H\n #include \"refs.h\"\n \n-struct cmd_reflog_expire_cb {\n+struct reflog_expire_options {\n \tint stalefix;\n \tint explicit_expiry;\n \ttimestamp_t expire_total;\n@@ -18,7 +18,7 @@ struct expire_reflog_policy_cb {\n \t} unreachable_expire_kind;\n \tstruct commit_list *mark_list;\n \tunsigned long mark_limit;\n-\tstruct cmd_reflog_expire_cb cmd;\n+\tstruct reflog_expire_options opts;\n \tstruct commit *tip_commit;\n \tstruct commit_list *tips;\n \tunsigned int dry_run:1;\n\n-- \n2.49.0.682.gc9b6a7b2b0.dirty\n\n"},{"id":"515824","messageId":"20250408-pks-maintenance-reflog-expire-v2-3-1ad8634798b7@pks.im","threadId":"63007","inReplyTo":"20250408-pks-maintenance-reflog-expire-v2-0-1ad8634798b7@pks.im","subject":"[PATCH v2 3/6] builtin/reflog: stop storing per-reflog expiry dates globally","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-08T06:22:14Z","receivedAt":"2025-04-08T06:22:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"As described in the preceding commit, the per-reflog expiry dates are\nstored in a global pair of variables. Refactor the code so that they are\ncontained in `struct reflog_expire_options` to make the structure useful\nin other contexts.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 30 ++++++++++++------------------\n reflog.h         |  8 ++++++++\n 2 files changed, 20 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 0910a4e25dc..a231cf4b857 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -88,27 +88,21 @@ static int collect_reflog(const char *ref, void *cb_data)\n \treturn 0;\n }\n \n-static struct reflog_expire_cfg {\n-\tstruct reflog_expire_cfg *next;\n-\ttimestamp_t expire_total;\n-\ttimestamp_t expire_unreachable;\n-\tchar pattern[FLEX_ARRAY];\n-} *reflog_expire_cfg, **reflog_expire_cfg_tail;\n-\n-static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len)\n+static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,\n+\t\t\t\t\t\t       const char *pattern, size_t len)\n {\n-\tstruct reflog_expire_cfg *ent;\n+\tstruct reflog_expire_entry_option *ent;\n \n-\tif (!reflog_expire_cfg_tail)\n-\t\treflog_expire_cfg_tail = &reflog_expire_cfg;\n+\tif (!opts->entries_tail)\n+\t\topts->entries_tail = &opts->entries;\n \n-\tfor (ent = reflog_expire_cfg; ent; ent = ent->next)\n+\tfor (ent = opts->entries; ent; ent = ent->next)\n \t\tif (!xstrncmpz(ent->pattern, pattern, len))\n \t\t\treturn ent;\n \n \tFLEX_ALLOC_MEM(ent, pattern, pattern, len);\n-\t*reflog_expire_cfg_tail = ent;\n-\treflog_expire_cfg_tail = &(ent->next);\n+\t*opts->entries_tail = ent;\n+\topts->entries_tail = &(ent->next);\n \treturn ent;\n }\n \n@@ -124,7 +118,7 @@ static int reflog_expire_config(const char *var, const char *value,\n \tsize_t pattern_len;\n \ttimestamp_t expire;\n \tint slot;\n-\tstruct reflog_expire_cfg *ent;\n+\tstruct reflog_expire_entry_option *ent;\n \n \tif (parse_config_key(var, \"gc\", &pattern, &pattern_len, &key) < 0)\n \t\treturn git_default_config(var, value, ctx, cb);\n@@ -152,7 +146,7 @@ static int reflog_expire_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tent = find_cfg_ent(pattern, pattern_len);\n+\tent = find_cfg_ent(opts, pattern, pattern_len);\n \tif (!ent)\n \t\treturn -1;\n \tswitch (slot) {\n@@ -168,12 +162,12 @@ static int reflog_expire_config(const char *var, const char *value,\n \n static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char *ref)\n {\n-\tstruct reflog_expire_cfg *ent;\n+\tstruct reflog_expire_entry_option *ent;\n \n \tif (cb->explicit_expiry == (EXPIRE_TOTAL|EXPIRE_UNREACH))\n \t\treturn; /* both given explicitly -- nothing to tweak */\n \n-\tfor (ent = reflog_expire_cfg; ent; ent = ent->next) {\n+\tfor (ent = cb->entries; ent; ent = ent->next) {\n \t\tif (!wildmatch(ent->pattern, ref, 0)) {\n \t\t\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n \t\t\t\tcb->expire_total = ent->expire_total;\ndiff --git a/reflog.h b/reflog.h\nindex a9d464bbf8c..b08780a30a7 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -2,7 +2,15 @@\n #define REFLOG_H\n #include \"refs.h\"\n \n+struct reflog_expire_entry_option {\n+\tstruct reflog_expire_entry_option *next;\n+\ttimestamp_t expire_total;\n+\ttimestamp_t expire_unreachable;\n+\tchar pattern[FLEX_ARRAY];\n+};\n+\n struct reflog_expire_options {\n+\tstruct reflog_expire_entry_option *entries, **entries_tail;\n \tint stalefix;\n \tint explicit_expiry;\n \ttimestamp_t default_expire_total;\n\n-- \n2.49.0.682.gc9b6a7b2b0.dirty\n\n"},{"id":"515826","messageId":"20250408-pks-maintenance-reflog-expire-v2-4-1ad8634798b7@pks.im","threadId":"63007","inReplyTo":"20250408-pks-maintenance-reflog-expire-v2-0-1ad8634798b7@pks.im","subject":"[PATCH v2 4/6] builtin/reflog: make functions regarding `reflog_expire_options` public","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-08T06:22:15Z","receivedAt":"2025-04-08T06:22:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Make functions that are required to manage `reflog_expire_options`\navailable elsewhere by moving them into \"reflog.c\" and exposing them in\nthe corresponding header. The functions will be used in a subsequent\ncommit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/reflog.c | 115 ++-----------------------------------------------------\n reflog.c         | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++\n reflog.h         |  17 ++++++++\n 3 files changed, 128 insertions(+), 111 deletions(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex a231cf4b857..5fea31f9c3c 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -88,113 +88,6 @@ static int collect_reflog(const char *ref, void *cb_data)\n \treturn 0;\n }\n \n-static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,\n-\t\t\t\t\t\t       const char *pattern, size_t len)\n-{\n-\tstruct reflog_expire_entry_option *ent;\n-\n-\tif (!opts->entries_tail)\n-\t\topts->entries_tail = &opts->entries;\n-\n-\tfor (ent = opts->entries; ent; ent = ent->next)\n-\t\tif (!xstrncmpz(ent->pattern, pattern, len))\n-\t\t\treturn ent;\n-\n-\tFLEX_ALLOC_MEM(ent, pattern, pattern, len);\n-\t*opts->entries_tail = ent;\n-\topts->entries_tail = &(ent->next);\n-\treturn ent;\n-}\n-\n-/* expiry timer slot */\n-#define EXPIRE_TOTAL   01\n-#define EXPIRE_UNREACH 02\n-\n-static int reflog_expire_config(const char *var, const char *value,\n-\t\t\t\tconst struct config_context *ctx, void *cb)\n-{\n-\tstruct reflog_expire_options *opts = cb;\n-\tconst char *pattern, *key;\n-\tsize_t pattern_len;\n-\ttimestamp_t expire;\n-\tint slot;\n-\tstruct reflog_expire_entry_option *ent;\n-\n-\tif (parse_config_key(var, \"gc\", &pattern, &pattern_len, &key) < 0)\n-\t\treturn git_default_config(var, value, ctx, cb);\n-\n-\tif (!strcmp(key, \"reflogexpire\")) {\n-\t\tslot = EXPIRE_TOTAL;\n-\t\tif (git_config_expiry_date(&expire, var, value))\n-\t\t\treturn -1;\n-\t} else if (!strcmp(key, \"reflogexpireunreachable\")) {\n-\t\tslot = EXPIRE_UNREACH;\n-\t\tif (git_config_expiry_date(&expire, var, value))\n-\t\t\treturn -1;\n-\t} else\n-\t\treturn git_default_config(var, value, ctx, cb);\n-\n-\tif (!pattern) {\n-\t\tswitch (slot) {\n-\t\tcase EXPIRE_TOTAL:\n-\t\t\topts->default_expire_total = expire;\n-\t\t\tbreak;\n-\t\tcase EXPIRE_UNREACH:\n-\t\t\topts->default_expire_unreachable = expire;\n-\t\t\tbreak;\n-\t\t}\n-\t\treturn 0;\n-\t}\n-\n-\tent = find_cfg_ent(opts, pattern, pattern_len);\n-\tif (!ent)\n-\t\treturn -1;\n-\tswitch (slot) {\n-\tcase EXPIRE_TOTAL:\n-\t\tent->expire_total = expire;\n-\t\tbreak;\n-\tcase EXPIRE_UNREACH:\n-\t\tent->expire_unreachable = expire;\n-\t\tbreak;\n-\t}\n-\treturn 0;\n-}\n-\n-static void set_reflog_expiry_param(struct reflog_expire_options *cb, const char *ref)\n-{\n-\tstruct reflog_expire_entry_option *ent;\n-\n-\tif (cb->explicit_expiry == (EXPIRE_TOTAL|EXPIRE_UNREACH))\n-\t\treturn; /* both given explicitly -- nothing to tweak */\n-\n-\tfor (ent = cb->entries; ent; ent = ent->next) {\n-\t\tif (!wildmatch(ent->pattern, ref, 0)) {\n-\t\t\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\t\t\tcb->expire_total = ent->expire_total;\n-\t\t\tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\t\t\tcb->expire_unreachable = ent->expire_unreachable;\n-\t\t\treturn;\n-\t\t}\n-\t}\n-\n-\t/*\n-\t * If unconfigured, make stash never expire\n-\t */\n-\tif (!strcmp(ref, \"refs/stash\")) {\n-\t\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\t\tcb->expire_total = 0;\n-\t\tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\t\tcb->expire_unreachable = 0;\n-\t\treturn;\n-\t}\n-\n-\t/* Nothing matched -- use the default value */\n-\tif (!(cb->explicit_expiry & EXPIRE_TOTAL))\n-\t\tcb->expire_total = cb->default_expire_total;\n-\tif (!(cb->explicit_expiry & EXPIRE_UNREACH))\n-\t\tcb->expire_unreachable = cb->default_expire_unreachable;\n-}\n-\n static int expire_unreachable_callback(const struct option *opt,\n \t\t\t\t const char *arg,\n \t\t\t\t int unset)\n@@ -207,7 +100,7 @@ static int expire_unreachable_callback(const struct option *opt,\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\topts->explicit_expiry |= EXPIRE_UNREACH;\n+\topts->explicit_expiry |= REFLOG_EXPIRE_UNREACH;\n \treturn 0;\n }\n \n@@ -223,7 +116,7 @@ static int expire_total_callback(const struct option *opt,\n \t\tdie(_(\"invalid timestamp '%s' given to '--%s'\"),\n \t\t    arg, opt->long_name);\n \n-\topts->explicit_expiry |= EXPIRE_TOTAL;\n+\topts->explicit_expiry |= REFLOG_EXPIRE_TOTAL;\n \treturn 0;\n }\n \n@@ -353,7 +246,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\t\t.dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN),\n \t\t\t};\n \n-\t\t\tset_reflog_expiry_param(&cb.opts,  item->string);\n+\t\t\treflog_expire_options_set_refname(&cb.opts,  item->string);\n \t\t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t\t     item->string, flags,\n \t\t\t\t\t\t     reflog_expiry_prepare,\n@@ -372,7 +265,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,\n \t\t\tstatus |= error(_(\"%s points nowhere!\"), argv[i]);\n \t\t\tcontinue;\n \t\t}\n-\t\tset_reflog_expiry_param(&cb.opts, ref);\n+\t\treflog_expire_options_set_refname(&cb.opts, ref);\n \t\tstatus |= refs_reflog_expire(get_main_ref_store(the_repository),\n \t\t\t\t\t     ref, flags,\n \t\t\t\t\t     reflog_expiry_prepare,\ndiff --git a/reflog.c b/reflog.c\nindex bcdb75514d0..642b162ef70 100644\n--- a/reflog.c\n+++ b/reflog.c\n@@ -2,13 +2,120 @@\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n+#include \"config.h\"\n #include \"gettext.h\"\n #include \"object-store-ll.h\"\n+#include \"parse-options.h\"\n #include \"reflog.h\"\n #include \"refs.h\"\n #include \"revision.h\"\n #include \"tree.h\"\n #include \"tree-walk.h\"\n+#include \"wildmatch.h\"\n+\n+static struct reflog_expire_entry_option *find_cfg_ent(struct reflog_expire_options *opts,\n+\t\t\t\t\t\t       const char *pattern, size_t len)\n+{\n+\tstruct reflog_expire_entry_option *ent;\n+\n+\tif (!opts->entries_tail)\n+\t\topts->entries_tail = &opts->entries;\n+\n+\tfor (ent = opts->entries; ent; ent = ent->next)\n+\t\tif (!xstrncmpz(ent->pattern, pattern, len))\n+\t\t\treturn ent;\n+\n+\tFLEX_ALLOC_MEM(ent, pattern, pattern, len);\n+\t*opts->entries_tail = ent;\n+\topts->entries_tail = &(ent->next);\n+\treturn ent;\n+}\n+\n+int reflog_expire_config(const char *var, const char *value,\n+\t\t\t const struct config_context *ctx, void *cb)\n+{\n+\tstruct reflog_expire_options *opts = cb;\n+\tconst char *pattern, *key;\n+\tsize_t pattern_len;\n+\ttimestamp_t expire;\n+\tint slot;\n+\tstruct reflog_expire_entry_option *ent;\n+\n+\tif (parse_config_key(var, \"gc\", &pattern, &pattern_len, &key) < 0)\n+\t\treturn git_default_config(var, value, ctx, cb);\n+\n+\tif (!strcmp(key, \"reflogexpire\")) {\n+\t\tslot = REFLOG_EXPIRE_TOTAL;\n+\t\tif (git_config_expiry_date(&expire, var, value))\n+\t\t\treturn -1;\n+\t} else if (!strcmp(key, \"reflogexpireunreachable\")) {\n+\t\tslot = REFLOG_EXPIRE_UNREACH;\n+\t\tif (git_config_expiry_date(&expire, var, value))\n+\t\t\treturn -1;\n+\t} else\n+\t\treturn git_default_config(var, value, ctx, cb);\n+\n+\tif (!pattern) {\n+\t\tswitch (slot) {\n+\t\tcase REFLOG_EXPIRE_TOTAL:\n+\t\t\topts->default_expire_total = expire;\n+\t\t\tbreak;\n+\t\tcase REFLOG_EXPIRE_UNREACH:\n+\t\t\topts->default_expire_unreachable = expire;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn 0;\n+\t}\n+\n+\tent = find_cfg_ent(opts, pattern, pattern_len);\n+\tif (!ent)\n+\t\treturn -1;\n+\tswitch (slot) {\n+\tcase REFLOG_EXPIRE_TOTAL:\n+\t\tent->expire_total = expire;\n+\t\tbreak;\n+\tcase REFLOG_EXPIRE_UNREACH:\n+\t\tent->expire_unreachable = expire;\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+}\n+\n+void reflog_expire_options_set_refname(struct reflog_expire_options *cb,\n+\t\t\t\t       const char *ref)\n+{\n+\tstruct reflog_expire_entry_option *ent;\n+\n+\tif (cb->explicit_expiry == (REFLOG_EXPIRE_TOTAL|REFLOG_EXPIRE_UNREACH))\n+\t\treturn; /* both given explicitly -- nothing to tweak */\n+\n+\tfor (ent = cb->entries; ent; ent = ent->next) {\n+\t\tif (!wildmatch(ent->pattern, ref, 0)) {\n+\t\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))\n+\t\t\t\tcb->expire_total = ent->expire_total;\n+\t\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))\n+\t\t\t\tcb->expire_unreachable = ent->expire_unreachable;\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * If unconfigured, make stash never expire\n+\t */\n+\tif (!strcmp(ref, \"refs/stash\")) {\n+\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))\n+\t\t\tcb->expire_total = 0;\n+\t\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))\n+\t\t\tcb->expire_unreachable = 0;\n+\t\treturn;\n+\t}\n+\n+\t/* Nothing matched -- use the default value */\n+\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_TOTAL))\n+\t\tcb->expire_total = cb->default_expire_total;\n+\tif (!(cb->explicit_expiry & REFLOG_EXPIRE_UNREACH))\n+\t\tcb->expire_unreachable = cb->default_expire_unreachable;\n+}\n \n /* Remember to update object flag allocation in object.h */\n #define INCOMPLETE\t(1u<<10)\ndiff --git a/reflog.h b/reflog.h\nindex b08780a30a7..63bb56280f4 100644\n--- a/reflog.h\n+++ b/reflog.h\n@@ -2,6 +2,9 @@\n #define REFLOG_H\n #include \"refs.h\"\n \n+#define REFLOG_EXPIRE_TOTAL   (1 << 0)\n+#define REFLOG_EXPIRE_UNREACH (1 << 1)\n+\n struct reflog_expire_entry_option {\n \tstruct reflog_expire_entry_option *next;\n \ttimestamp_t expire_total;\n@@ -24,6 +27,20 @@ struct reflog_expire_options {\n \t.default_expire_unreachable = now - 90 * 24 * 3600, \\\n }\n \n+/*\n+ * Parse the reflog expire configuration. This should be used with\n+ * `repo_config()`.\n+ */\n+int reflog_expire_config(const char *var, const char *value,\n+\t\t\t const struct config_context *ctx, void *cb);\n+\n+/*\n+ * Adapt the options so that they apply to the given refname. This applies any\n+ * per-reference reflog expiry configuration that may exist to the options.\n+ */\n+void reflog_expire_options_set_refname(struct reflog_expire_options *cb,\n+\t\t\t\t       const char *refname);\n+\n struct expire_reflog_policy_cb {\n \tenum {\n \t\tUE_NORMAL,\n\n-- \n2.49.0.682.gc9b6a7b2b0.dirty\n\n"},{"id":"515827","messageId":"20250408-pks-maintenance-reflog-expire-v2-5-1ad8634798b7@pks.im","threadId":"63007","inReplyTo":"20250408-pks-maintenance-reflog-expire-v2-0-1ad8634798b7@pks.im","subject":"[PATCH v2 5/6] builtin/gc: split out function to expire reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-08T06:22:16Z","receivedAt":"2025-04-08T06:22:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to introduce a new task for git-maintenance(1) that knows to\nexpire reflog entries. The logic will be shared with git-gc(1), which\nalready knows how to do this.\n\nPull out the common logic into a separate function so that we can share\nthe implementation between both builtins.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c | 22 +++++++++++-----------\n 1 file changed, 11 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 409d454a4b7..e8f5705dc59 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -53,7 +53,6 @@ static const char * const builtin_gc_usage[] = {\n \n static timestamp_t gc_log_expire_time;\n \n-static struct strvec reflog = STRVEC_INIT;\n static struct strvec repack = STRVEC_INIT;\n static struct strvec prune = STRVEC_INIT;\n static struct strvec prune_worktrees = STRVEC_INIT;\n@@ -286,6 +285,15 @@ static int maintenance_task_pack_refs(struct maintenance_run_opts *opts,\n \treturn run_command(&cmd);\n }\n \n+static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUSED,\n+\t\t\t\t\t  struct gc_config *cfg UNUSED)\n+{\n+\tstruct child_process cmd = CHILD_PROCESS_INIT;\n+\tcmd.git_cmd = 1;\n+\tstrvec_pushl(&cmd.args, \"reflog\", \"expire\", \"--all\", NULL);\n+\treturn run_command(&cmd);\n+}\n+\n static int too_many_loose_objects(struct gc_config *cfg)\n {\n \t/*\n@@ -662,15 +670,8 @@ static void gc_before_repack(struct maintenance_run_opts *opts,\n \n \tif (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))\n \t\tdie(FAILED_RUN, \"pack-refs\");\n-\n-\tif (cfg->prune_reflogs) {\n-\t\tstruct child_process cmd = CHILD_PROCESS_INIT;\n-\n-\t\tcmd.git_cmd = 1;\n-\t\tstrvec_pushv(&cmd.args, reflog.v);\n-\t\tif (run_command(&cmd))\n-\t\t\tdie(FAILED_RUN, reflog.v[0]);\n-\t}\n+\tif (cfg->prune_reflogs && maintenance_task_reflog_expire(opts, cfg))\n+\t\tdie(FAILED_RUN, \"reflog\");\n }\n \n int cmd_gc(int argc,\n@@ -718,7 +719,6 @@ struct repository *repo UNUSED)\n \tshow_usage_with_options_if_asked(argc, argv,\n \t\t\t\t\t builtin_gc_usage, builtin_gc_options);\n \n-\tstrvec_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \tstrvec_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n \tstrvec_pushl(&prune, \"prune\", \"--expire\", NULL);\n \tstrvec_pushl(&prune_worktrees, \"worktree\", \"prune\", \"--expire\", NULL);\n\n-- \n2.49.0.682.gc9b6a7b2b0.dirty\n\n"},{"id":"515828","messageId":"20250408-pks-maintenance-reflog-expire-v2-6-1ad8634798b7@pks.im","threadId":"63007","inReplyTo":"20250408-pks-maintenance-reflog-expire-v2-0-1ad8634798b7@pks.im","subject":"[PATCH v2 6/6] builtin/maintenance: introduce \"reflog-expire\" task","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-08T06:22:17Z","receivedAt":"2025-04-08T06:22:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"By default, git-maintenance(1) uses the \"gc\" task to ensure that the\nrepository is well-maintained. This can be changed, for example by\neither explicitly configuring which tasks should be enabled or by using\nthe \"incremental\" maintenance strategy. If so, git-maintenance(1) does\nnot know to expire reflog entries, which is a subtask that git-gc(1)\nknows to perform for the user. Consequently, the reflog will grow\nindefinitely unless the user manually trims it.\n\nIntroduce a new \"reflog-expire\" task that plugs this gap:\n\n  - When running the task directly, then we simply execute `git reflog\n    expire --all`, which is the same as git-gc(1).\n\n  - When running git-maintenance(1) with the `--auto` flag, then we only\n    run the task in case the \"HEAD\" reflog has at least N reflog entries\n    that would be discarded. By default, N is set to 100, but this can\n    be configured via \"maintenance.reflog-expire.auto\". When a negative\n    integer has been provided we always expire entries, zero causes us\n    to never expire entries, and a positive value specifies how many\n    entries need to exist before we consider pruning the entries.\n\nNote that the condition for the `--auto` flags is merely a heuristic and\noptimized for being fast. This is because `git maintenance run --auto`\nwill be executed quite regularly, so scanning through all reflogs would\nlikely be too expensive in many repositories.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/config/maintenance.adoc |  9 +++++++\n Documentation/git-maintenance.adoc    |  4 +++\n builtin/gc.c                          | 50 +++++++++++++++++++++++++++++++++++\n t/t7900-maintenance.sh                | 18 +++++++++++++\n 4 files changed, 81 insertions(+)\n\ndiff --git a/Documentation/config/maintenance.adoc b/Documentation/config/maintenance.adoc\nindex 72a9d6cf816..e57f346a067 100644\n--- a/Documentation/config/maintenance.adoc\n+++ b/Documentation/config/maintenance.adoc\n@@ -69,3 +69,12 @@ maintenance.incremental-repack.auto::\n \tOtherwise, a positive value implies the command should run when the\n \tnumber of pack-files not in the multi-pack-index is at least the value\n \tof `maintenance.incremental-repack.auto`. The default value is 10.\n+\n+maintenance.reflog-expire.auto::\n+\tThis integer config option controls how often the `reflog-expire` task\n+\tshould be run as part of `git maintenance run --auto`. If zero, then\n+\tthe `reflog-expire` task will not run with the `--auto` option. A\n+\tnegative value will force the task to run every time. Otherwise, a\n+\tpositive value implies the command should run when the number of\n+\texpired reflog entries in the \"HEAD\" reflog is at least the value of\n+\t`maintenance.loose-objects.auto`. The default value is 100.\ndiff --git a/Documentation/git-maintenance.adoc b/Documentation/git-maintenance.adoc\nindex 0450d74aff1..8bc94a6d4ff 100644\n--- a/Documentation/git-maintenance.adoc\n+++ b/Documentation/git-maintenance.adoc\n@@ -158,6 +158,10 @@ pack-refs::\n \tneed to iterate across many references. See linkgit:git-pack-refs[1]\n \tfor more information.\n \n+reflog-expire::\n+\tThe `reflog-expire` task deletes any entries in the reflog older than the\n+\texpiry threshold. See linkgit:git-reflog[1] for more information.\n+\n OPTIONS\n -------\n --auto::\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e8f5705dc59..ce5bb2630f8 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -33,6 +33,7 @@\n #include \"pack.h\"\n #include \"pack-objects.h\"\n #include \"path.h\"\n+#include \"reflog.h\"\n #include \"blob.h\"\n #include \"tree.h\"\n #include \"promisor-remote.h\"\n@@ -285,6 +286,49 @@ static int maintenance_task_pack_refs(struct maintenance_run_opts *opts,\n \treturn run_command(&cmd);\n }\n \n+struct count_reflog_entries_data {\n+\tstruct expire_reflog_policy_cb policy;\n+\tsize_t count;\n+\tsize_t limit;\n+};\n+\n+static int count_reflog_entries(struct object_id *old_oid, struct object_id *new_oid,\n+\t\t\t\tconst char *committer, timestamp_t timestamp,\n+\t\t\t\tint tz, const char *msg, void *cb_data)\n+{\n+\tstruct count_reflog_entries_data *data = cb_data;\n+\tif (should_expire_reflog_ent(old_oid, new_oid, committer, timestamp, tz, msg, &data->policy))\n+\t\tdata->count++;\n+\treturn data->count >= data->limit;\n+}\n+\n+static int reflog_expire_condition(struct gc_config *cfg UNUSED)\n+{\n+\ttimestamp_t now = time(NULL);\n+\tstruct count_reflog_entries_data data = {\n+\t\t.policy = {\n+\t\t\t.opts = REFLOG_EXPIRE_OPTIONS_INIT(now),\n+\t\t},\n+\t};\n+\tint limit = 100;\n+\n+\tgit_config_get_int(\"maintenance.reflog-expire.auto\", &limit);\n+\tif (!limit)\n+\t\treturn 0;\n+\tif (limit < 0)\n+\t\treturn 1;\n+\tdata.limit = limit;\n+\n+\trepo_config(the_repository, reflog_expire_config, &data.policy.opts);\n+\n+\treflog_expire_options_set_refname(&data.policy.opts, \"HEAD\");\n+\trefs_for_each_reflog_ent(get_main_ref_store(the_repository), \"HEAD\",\n+\t\t\t\t count_reflog_entries, &data);\n+\n+\treflog_expiry_cleanup(&data.policy);\n+\treturn data.count >= data.limit;\n+}\n+\n static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUSED,\n \t\t\t\t\t  struct gc_config *cfg UNUSED)\n {\n@@ -1383,6 +1427,7 @@ enum maintenance_task_label {\n \tTASK_GC,\n \tTASK_COMMIT_GRAPH,\n \tTASK_PACK_REFS,\n+\tTASK_REFLOG_EXPIRE,\n \n \t/* Leave as final value */\n \tTASK__COUNT\n@@ -1419,6 +1464,11 @@ static struct maintenance_task tasks[] = {\n \t\tmaintenance_task_pack_refs,\n \t\tpack_refs_condition,\n \t},\n+\t[TASK_REFLOG_EXPIRE] = {\n+\t\t\"reflog-expire\",\n+\t\tmaintenance_task_reflog_expire,\n+\t\treflog_expire_condition,\n+\t},\n };\n \n static int compare_tasks_by_selection(const void *a_, const void *b_)\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 1909aed95e0..ff98cde92c0 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -447,6 +447,24 @@ test_expect_success 'pack-refs task' '\n \ttest_subcommand git pack-refs --all --prune <pack-refs.txt\n '\n \n+test_expect_success 'reflog-expire task' '\n+\tGIT_TRACE2_EVENT=\"$(pwd)/reflog-expire.txt\" \\\n+\t\tgit maintenance run --task=reflog-expire &&\n+\ttest_subcommand git reflog expire --all <reflog-expire.txt\n+'\n+\n+test_expect_success 'reflog-expire task --auto only packs when exceeding limits' '\n+\tgit reflog expire --all --expire=now &&\n+\ttest_commit reflog-one &&\n+\ttest_commit reflog-two &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/reflog-expire-auto.txt\" \\\n+\t\tgit -c maintenance.reflog-expire.auto=3 maintenance run --auto --task=reflog-expire &&\n+\ttest_subcommand ! git reflog expire --all <reflog-expire-auto.txt &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/reflog-expire-auto.txt\" \\\n+\t\tgit -c maintenance.reflog-expire.auto=2 maintenance run --auto --task=reflog-expire &&\n+\ttest_subcommand git reflog expire --all <reflog-expire-auto.txt\n+'\n+\n test_expect_success '--auto and --schedule incompatible' '\n \ttest_must_fail git maintenance run --auto --schedule=daily 2>err &&\n \ttest_grep \"at most one\" err\n\n-- \n2.49.0.682.gc9b6a7b2b0.dirty\n\n"}]}