builtin/maintenance: introduce "worktree-prune" task

While git-gc(1) knows to prune stale worktrees, git-maintenance(1) does
not yet have a task for this cleanup. Introduce a new "worktree-prune"
task to plug this gap.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Patrick Steinhardt 2025-04-30 12:25:09 +02:00 committed by Junio C Hamano
parent 4167c6153b
commit eae6763649
4 changed files with 129 additions and 0 deletions

View File

@ -83,3 +83,11 @@ 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.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,10 @@ 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.


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

@ -43,6 +43,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"


@ -345,6 +346,45 @@ static int maintenance_task_worktree_prune(struct maintenance_run_opts *opts UNU
return run_command(&prune_worktrees_cmd); return run_command(&prune_worktrees_cmd);
} }


static int worktree_prune_condition(struct gc_config *cfg)
{
struct strvec worktrees = STRVEC_INIT;
struct strbuf reason = STRBUF_INIT;
timestamp_t expiry_date;
int should_prune = 0;
int limit = 1;

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) ||
get_worktree_names(the_repository, &worktrees) < 0)
goto out;

for (size_t i = 0; i < worktrees.nr; i++) {
char *wtpath;

strbuf_reset(&reason);
if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {
limit--;

if (!limit) {
should_prune = 1;
goto out;
}
}
free(wtpath);
}

out:
strvec_clear(&worktrees);
strbuf_release(&reason);
return should_prune;
}

static int too_many_loose_objects(struct gc_config *cfg) static int too_many_loose_objects(struct gc_config *cfg)
{ {
/* /*
@ -1465,6 +1505,7 @@ 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,


/* Leave as final value */ /* Leave as final value */
TASK__COUNT TASK__COUNT
@ -1506,6 +1547,11 @@ 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,
},
}; };


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,77 @@ 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 man 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_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