Merge branch 'ps/maintenance-missing-tasks'

Make repository clean-up tasks "gc" can do available to "git
maintenance" front-end.

* ps/maintenance-missing-tasks:
  builtin/maintenance: introduce "rerere-gc" task
  builtin/gc: move rerere garbage collection into separate function
  builtin/maintenance: introduce "worktree-prune" task
  builtin/gc: move pruning of worktrees into a separate function
  builtin/gc: remove global variables where it is trivial to do
  builtin/gc: fix indentation of `cmd_gc()` parameters
This commit is contained in:
Junio C Hamano
2025-05-15 17:24:55 -07:00
4 changed files with 257 additions and 31 deletions

View File

@@ -83,3 +83,20 @@ maintenance.reflog-expire.auto::
positive value implies the command should run when the number of positive value implies the command should run when the number of
expired reflog entries in the "HEAD" reflog is at least the value of expired reflog entries in the "HEAD" reflog is at least the value of
`maintenance.loose-objects.auto`. The default value is 100. `maintenance.loose-objects.auto`. The default value is 100.
maintenance.rerere-gc.auto::
This integer config option controls how often the `rerere-gc` task
should be run as part of `git maintenance run --auto`. If zero, then
the `rerere-gc` task will not run with the `--auto` option. A negative
value will force the task to run every time. Otherwise, any positive
value implies the command will run when the "rr-cache" directory exists
and has at least one entry, regardless of whether it is stale or not.
This heuristic may be refined in the future. The default value is 1.
maintenance.worktree-prune.auto::
This integer config option controls how often the `worktree-prune` task
should be run as part of `git maintenance run --auto`. If zero, then
the `worktree-prune` task will not run with the `--auto` option. A
negative value will force the task to run every time. Otherwise, a
positive value implies the command should run when the number of
prunable worktrees exceeds the value. The default value is 1.

View File

@@ -166,6 +166,14 @@ reflog-expire::
The `reflog-expire` task deletes any entries in the reflog older than the The `reflog-expire` task deletes any entries in the reflog older than the
expiry threshold. See linkgit:git-reflog[1] for more information. expiry threshold. See linkgit:git-reflog[1] for more information.
rerere-gc::
The `rerere-gc` task invokes garbage collection for stale entries in
the rerere cache. See linkgit:git-rerere[1] for more information.
worktree-prune::
The `worktree-prune` task deletes stale or broken worktrees. See
linkit:git-worktree[1] for more information.
OPTIONS OPTIONS
------- -------
--auto:: --auto::

View File

@@ -16,6 +16,7 @@
#include "builtin.h" #include "builtin.h"
#include "abspath.h" #include "abspath.h"
#include "date.h" #include "date.h"
#include "dir.h"
#include "environment.h" #include "environment.h"
#include "hex.h" #include "hex.h"
#include "config.h" #include "config.h"
@@ -33,6 +34,7 @@
#include "pack-objects.h" #include "pack-objects.h"
#include "path.h" #include "path.h"
#include "reflog.h" #include "reflog.h"
#include "rerere.h"
#include "blob.h" #include "blob.h"
#include "tree.h" #include "tree.h"
#include "promisor-remote.h" #include "promisor-remote.h"
@@ -43,6 +45,7 @@
#include "hook.h" #include "hook.h"
#include "setup.h" #include "setup.h"
#include "trace2.h" #include "trace2.h"
#include "worktree.h"
#define FAILED_RUN "failed to run %s" #define FAILED_RUN "failed to run %s"
@@ -52,15 +55,9 @@ static const char * const builtin_gc_usage[] = {
}; };
static timestamp_t gc_log_expire_time; static timestamp_t gc_log_expire_time;
static struct strvec repack = STRVEC_INIT; static struct strvec repack = STRVEC_INIT;
static struct strvec prune = STRVEC_INIT;
static struct strvec prune_worktrees = STRVEC_INIT;
static struct strvec rerere = STRVEC_INIT;
static struct tempfile *pidfile; static struct tempfile *pidfile;
static struct lock_file log_lock; static struct lock_file log_lock;
static struct string_list pack_garbage = STRING_LIST_INIT_DUP; static struct string_list pack_garbage = STRING_LIST_INIT_DUP;
static void clean_pack_garbage(void) static void clean_pack_garbage(void)
@@ -339,6 +336,94 @@ static int maintenance_task_reflog_expire(struct maintenance_run_opts *opts UNUS
return run_command(&cmd); return run_command(&cmd);
} }
static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNUSED,
struct gc_config *cfg)
{
struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;
prune_worktrees_cmd.git_cmd = 1;
strvec_pushl(&prune_worktrees_cmd.args, "worktree", "prune", "--expire", NULL);
strvec_push(&prune_worktrees_cmd.args, cfg->prune_worktrees_expire);
return run_command(&prune_worktrees_cmd);
}
static int worktree_prune_condition(struct gc_config *cfg)
{
struct strbuf buf = STRBUF_INIT;
int should_prune = 0, limit = 1;
timestamp_t expiry_date;
struct dirent *d;
DIR *dir = NULL;
git_config_get_int("maintenance.worktree-prune.auto", &limit);
if (limit <= 0) {
should_prune = limit < 0;
goto out;
}
if (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date))
goto out;
dir = opendir(repo_git_path_replace(the_repository, &buf, "worktrees"));
if (!dir)
goto out;
while (limit && (d = readdir_skip_dot_and_dotdot(dir))) {
char *wtpath;
strbuf_reset(&buf);
if (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date))
limit--;
free(wtpath);
}
should_prune = !limit;
out:
if (dir)
closedir(dir);
strbuf_release(&buf);
return should_prune;
}
static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,
struct gc_config *cfg UNUSED)
{
struct child_process rerere_cmd = CHILD_PROCESS_INIT;
rerere_cmd.git_cmd = 1;
strvec_pushl(&rerere_cmd.args, "rerere", "gc", NULL);
return run_command(&rerere_cmd);
}
static int rerere_gc_condition(struct gc_config *cfg UNUSED)
{
struct strbuf path = STRBUF_INIT;
int should_gc = 0, limit = 1;
DIR *dir = NULL;
git_config_get_int("maintenance.rerere-gc.auto", &limit);
if (limit <= 0) {
should_gc = limit < 0;
goto out;
}
/*
* We skip garbage collection in case we either have no "rr-cache"
* directory or when it doesn't contain at least one entry.
*/
repo_git_path_replace(the_repository, &path, "rr-cache");
dir = opendir(path.buf);
if (!dir)
goto out;
should_gc = !!readdir_skip_dot_and_dotdot(dir);
out:
strbuf_release(&path);
if (dir)
closedir(dir);
return should_gc;
}
static int too_many_loose_objects(struct gc_config *cfg) static int too_many_loose_objects(struct gc_config *cfg)
{ {
/* /*
@@ -728,9 +813,9 @@ static void gc_before_repack(struct maintenance_run_opts *opts,
} }
int cmd_gc(int argc, int cmd_gc(int argc,
const char **argv, const char **argv,
const char *prefix, const char *prefix,
struct repository *repo UNUSED) struct repository *repo UNUSED)
{ {
int aggressive = 0; int aggressive = 0;
int quiet = 0; int quiet = 0;
@@ -740,7 +825,6 @@ struct repository *repo UNUSED)
int daemonized = 0; int daemonized = 0;
int keep_largest_pack = -1; int keep_largest_pack = -1;
timestamp_t dummy; timestamp_t dummy;
struct child_process rerere_cmd = CHILD_PROCESS_INIT;
struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT; struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;
struct gc_config cfg = GC_CONFIG_INIT; struct gc_config cfg = GC_CONFIG_INIT;
const char *prune_expire_sentinel = "sentinel"; const char *prune_expire_sentinel = "sentinel";
@@ -779,9 +863,6 @@ struct repository *repo UNUSED)
builtin_gc_usage, builtin_gc_options); builtin_gc_usage, builtin_gc_options);
strvec_pushl(&repack, "repack", "-d", "-l", NULL); strvec_pushl(&repack, "repack", "-d", "-l", NULL);
strvec_pushl(&prune, "prune", "--expire", NULL);
strvec_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL);
strvec_pushl(&rerere, "rerere", "gc", NULL);
gc_config(&cfg); gc_config(&cfg);
@@ -907,34 +988,27 @@ struct repository *repo UNUSED)
if (cfg.prune_expire) { if (cfg.prune_expire) {
struct child_process prune_cmd = CHILD_PROCESS_INIT; struct child_process prune_cmd = CHILD_PROCESS_INIT;
strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL);
/* run `git prune` even if using cruft packs */ /* run `git prune` even if using cruft packs */
strvec_push(&prune, cfg.prune_expire); strvec_push(&prune_cmd.args, cfg.prune_expire);
if (quiet) if (quiet)
strvec_push(&prune, "--no-progress"); strvec_push(&prune_cmd.args, "--no-progress");
if (repo_has_promisor_remote(the_repository)) if (repo_has_promisor_remote(the_repository))
strvec_push(&prune, strvec_push(&prune_cmd.args,
"--exclude-promisor-objects"); "--exclude-promisor-objects");
prune_cmd.git_cmd = 1; prune_cmd.git_cmd = 1;
strvec_pushv(&prune_cmd.args, prune.v);
if (run_command(&prune_cmd)) if (run_command(&prune_cmd))
die(FAILED_RUN, prune.v[0]); die(FAILED_RUN, prune_cmd.args.v[0]);
} }
} }
if (cfg.prune_worktrees_expire) { if (cfg.prune_worktrees_expire &&
struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT; maintenance_task_worktree_prune(&opts, &cfg))
die(FAILED_RUN, "worktree");
strvec_push(&prune_worktrees, cfg.prune_worktrees_expire); if (maintenance_task_rerere_gc(&opts, &cfg))
prune_worktrees_cmd.git_cmd = 1; die(FAILED_RUN, "rerere");
strvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v);
if (run_command(&prune_worktrees_cmd))
die(FAILED_RUN, prune_worktrees.v[0]);
}
rerere_cmd.git_cmd = 1;
strvec_pushv(&rerere_cmd.args, rerere.v);
if (run_command(&rerere_cmd))
die(FAILED_RUN, rerere.v[0]);
report_garbage = report_pack_garbage; report_garbage = report_pack_garbage;
reprepare_packed_git(the_repository); reprepare_packed_git(the_repository);
@@ -1467,6 +1541,8 @@ enum maintenance_task_label {
TASK_COMMIT_GRAPH, TASK_COMMIT_GRAPH,
TASK_PACK_REFS, TASK_PACK_REFS,
TASK_REFLOG_EXPIRE, TASK_REFLOG_EXPIRE,
TASK_WORKTREE_PRUNE,
TASK_RERERE_GC,
/* Leave as final value */ /* Leave as final value */
TASK__COUNT TASK__COUNT
@@ -1508,6 +1584,16 @@ static struct maintenance_task tasks[] = {
maintenance_task_reflog_expire, maintenance_task_reflog_expire,
reflog_expire_condition, reflog_expire_condition,
}, },
[TASK_WORKTREE_PRUNE] = {
"worktree-prune",
maintenance_task_worktree_prune,
worktree_prune_condition,
},
[TASK_RERERE_GC] = {
"rerere-gc",
maintenance_task_rerere_gc,
rerere_gc_condition,
},
}; };
static int compare_tasks_by_selection(const void *a_, const void *b_) static int compare_tasks_by_selection(const void *a_, const void *b_)

View File

@@ -493,6 +493,121 @@ test_expect_success 'reflog-expire task --auto only packs when exceeding limits'
test_subcommand git reflog expire --all <reflog-expire-auto.txt test_subcommand git reflog expire --all <reflog-expire-auto.txt
' '
test_expect_worktree_prune () {
negate=
if test "$1" = "!"
then
negate="!"
shift
fi
rm -f "worktree-prune.txt" &&
GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" "$@" &&
test_subcommand $negate git worktree prune --expire 3.months.ago <worktree-prune.txt
}
test_expect_success 'worktree-prune task without --auto always prunes' '
test_expect_worktree_prune git maintenance run --task=worktree-prune
'
test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' '
test_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&
mkdir .git/worktrees &&
: >.git/worktrees/abc &&
test_expect_worktree_prune git maintenance run --auto --task=worktree-prune
'
test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '
# A negative value should always prune.
test_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&
mkdir .git/worktrees &&
: >.git/worktrees/first &&
: >.git/worktrees/second &&
: >.git/worktrees/third &&
# Zero should never prune.
test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&
# A positive value should require at least this many prunable worktrees.
test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&
test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune
'
test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '
# A negative value should always prune.
test_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&
mkdir .git/worktrees &&
: >.git/worktrees/first &&
: >.git/worktrees/second &&
: >.git/worktrees/third &&
# Zero should never prune.
test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&
# A positive value should require at least this many prunable worktrees.
test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&
test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune
'
test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '
git worktree add worktree &&
rm -rf worktree &&
rm -f worktree-prune.txt &&
GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=1.week.ago maintenance run --auto --task=worktree-prune &&
test_subcommand ! git worktree prune --expire 1.week.ago <worktree-prune.txt &&
test_path_is_dir .git/worktrees/worktree &&
rm -f worktree-prune.txt &&
GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=now maintenance run --auto --task=worktree-prune &&
test_subcommand git worktree prune --expire now <worktree-prune.txt &&
test_path_is_missing .git/worktrees/worktree
'
test_expect_rerere_gc () {
negate=
if test "$1" = "!"
then
negate="!"
shift
fi
rm -f "rerere-gc.txt" &&
GIT_TRACE2_EVENT="$(pwd)/rerere-gc.txt" "$@" &&
test_subcommand $negate git rerere gc <rerere-gc.txt
}
test_expect_success 'rerere-gc task without --auto always collects garbage' '
test_expect_rerere_gc git maintenance run --task=rerere-gc
'
test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '
test_when_finished "rm -rf .git/rr-cache" &&
test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
mkdir .git/rr-cache &&
test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
: >.git/rr-cache/entry &&
test_expect_rerere_gc git maintenance run --auto --task=rerere-gc
'
test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' '
test_when_finished "rm -rf .git/rr-cache" &&
# A negative value should always prune.
test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&
# A positive value prunes when there is at least one entry.
test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
mkdir .git/rr-cache &&
test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
: >.git/rr-cache/entry-1 &&
test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
# Zero should never prune.
: >.git/rr-cache/entry-1 &&
test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc
'
test_expect_success '--auto and --schedule incompatible' ' test_expect_success '--auto and --schedule incompatible' '
test_must_fail git maintenance run --auto --schedule=daily 2>err && test_must_fail git maintenance run --auto --schedule=daily 2>err &&
test_grep "at most one" err test_grep "at most one" err