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
maint
Junio C Hamano 2025-05-15 17:24:55 -07:00
commit 4dda60c9df
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
expired reflog entries in the "HEAD" reflog is at least the value of
`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
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
-------
--auto::

View File

@ -16,6 +16,7 @@
#include "builtin.h"
#include "abspath.h"
#include "date.h"
#include "dir.h"
#include "environment.h"
#include "hex.h"
#include "config.h"
@ -33,6 +34,7 @@
#include "pack-objects.h"
#include "path.h"
#include "reflog.h"
#include "rerere.h"
#include "blob.h"
#include "tree.h"
#include "promisor-remote.h"
@ -43,6 +45,7 @@
#include "hook.h"
#include "setup.h"
#include "trace2.h"
#include "worktree.h"

#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 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 lock_file log_lock;

static struct string_list pack_garbage = STRING_LIST_INIT_DUP;

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);
}

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)
{
/*
@ -728,9 +813,9 @@ static void gc_before_repack(struct maintenance_run_opts *opts,
}

int cmd_gc(int argc,
const char **argv,
const char *prefix,
struct repository *repo UNUSED)
const char **argv,
const char *prefix,
struct repository *repo UNUSED)
{
int aggressive = 0;
int quiet = 0;
@ -740,7 +825,6 @@ struct repository *repo UNUSED)
int daemonized = 0;
int keep_largest_pack = -1;
timestamp_t dummy;
struct child_process rerere_cmd = CHILD_PROCESS_INIT;
struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;
struct gc_config cfg = GC_CONFIG_INIT;
const char *prune_expire_sentinel = "sentinel";
@ -779,9 +863,6 @@ struct repository *repo UNUSED)
builtin_gc_usage, builtin_gc_options);

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);

@ -907,34 +988,27 @@ struct repository *repo UNUSED)
if (cfg.prune_expire) {
struct child_process prune_cmd = CHILD_PROCESS_INIT;

strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL);
/* run `git prune` even if using cruft packs */
strvec_push(&prune, cfg.prune_expire);
strvec_push(&prune_cmd.args, cfg.prune_expire);
if (quiet)
strvec_push(&prune, "--no-progress");
strvec_push(&prune_cmd.args, "--no-progress");
if (repo_has_promisor_remote(the_repository))
strvec_push(&prune,
strvec_push(&prune_cmd.args,
"--exclude-promisor-objects");
prune_cmd.git_cmd = 1;
strvec_pushv(&prune_cmd.args, prune.v);

if (run_command(&prune_cmd))
die(FAILED_RUN, prune.v[0]);
die(FAILED_RUN, prune_cmd.args.v[0]);
}
}

if (cfg.prune_worktrees_expire) {
struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;
if (cfg.prune_worktrees_expire &&
maintenance_task_worktree_prune(&opts, &cfg))
die(FAILED_RUN, "worktree");

strvec_push(&prune_worktrees, cfg.prune_worktrees_expire);
prune_worktrees_cmd.git_cmd = 1;
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]);
if (maintenance_task_rerere_gc(&opts, &cfg))
die(FAILED_RUN, "rerere");

report_garbage = report_pack_garbage;
reprepare_packed_git(the_repository);
@ -1467,6 +1541,8 @@ enum maintenance_task_label {
TASK_COMMIT_GRAPH,
TASK_PACK_REFS,
TASK_REFLOG_EXPIRE,
TASK_WORKTREE_PRUNE,
TASK_RERERE_GC,

/* Leave as final value */
TASK__COUNT
@ -1508,6 +1584,16 @@ static struct maintenance_task tasks[] = {
maintenance_task_reflog_expire,
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_)

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_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_must_fail git maintenance run --auto --schedule=daily 2>err &&
test_grep "at most one" err