maintenance: add 'is-needed' subcommand

The 'git-maintenance(1)' command provides tooling to run maintenance
tasks over Git repositories. The 'run' subcommand, as the name suggests,
runs the maintenance tasks. When used with the '--auto' flag, it uses
heuristics to determine if the required thresholds are met for running
said maintenance tasks.

There is however a lack of insight into these heuristics. Meaning, the
checks are linked to the execution.

Add a new 'is-needed' subcommand to 'git-maintenance(1)' which allows
users to simply check if it is needed to run maintenance without
performing it.

This subcommand can check if it is needed to run maintenance without
actually running it. Ideally it should be used with the '--auto' flag,
which would allow users to check if the thresholds required are met. The
subcommand also supports the '--task' flag which can be used to check
specific maintenance tasks.

While adding the respective tests in 't/t7900-maintenance.sh', remove a
duplicate of the test: 'worktree-prune task with --auto honors
maintenance.worktree-prune.auto'.

Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
Acked-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Karthik Nayak
2025-11-08 22:51:57 +01:00
committed by Junio C Hamano
parent 8c1ce2204c
commit 28b83e6f08
3 changed files with 113 additions and 17 deletions

View File

@@ -12,6 +12,7 @@ SYNOPSIS
'git maintenance' run [<options>] 'git maintenance' run [<options>]
'git maintenance' start [--scheduler=<scheduler>] 'git maintenance' start [--scheduler=<scheduler>]
'git maintenance' (stop|register|unregister) [<options>] 'git maintenance' (stop|register|unregister) [<options>]
'git maintenance' is-needed [<options>]
DESCRIPTION DESCRIPTION
@@ -84,6 +85,16 @@ The `unregister` subcommand will report an error if the current repository
is not already registered. Use the `--force` option to return success even is not already registered. Use the `--force` option to return success even
when the current repository is not registered. when the current repository is not registered.
is-needed::
Check whether maintenance needs to be run without actually running it.
Exits with a 0 status code if maintenance needs to be run, 1 otherwise.
Ideally used with the '--auto' flag.
+
If one or more `--task` options are specified, then those tasks are checked
in that order. Otherwise, the tasks are determined by which
`maintenance.<task>.enabled` config options are true. By default, only
`maintenance.gc.enabled` is true.
TASKS TASKS
----- -----
@@ -183,6 +194,8 @@ OPTIONS
in the `gc.auto` config setting, or when the number of pack-files in the `gc.auto` config setting, or when the number of pack-files
exceeds the `gc.autoPackLimit` config setting. Not compatible with exceeds the `gc.autoPackLimit` config setting. Not compatible with
the `--schedule` option. the `--schedule` option.
When combined with the `is-needed` subcommand, check if the required
thresholds are met without actually running maintenance.
--schedule:: --schedule::
When combined with the `run` subcommand, run maintenance tasks When combined with the `run` subcommand, run maintenance tasks

View File

@@ -3253,7 +3253,67 @@ static int maintenance_stop(int argc, const char **argv, const char *prefix,
return update_background_schedule(NULL, 0); return update_background_schedule(NULL, 0);
} }
static const char * const builtin_maintenance_usage[] = { static const char *const builtin_maintenance_is_needed_usage[] = {
"git maintenance is-needed [--task=<task>] [--schedule]",
NULL
};
static int maintenance_is_needed(int argc, const char **argv, const char *prefix,
struct repository *repo UNUSED)
{
struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;
struct string_list selected_tasks = STRING_LIST_INIT_DUP;
struct gc_config cfg = GC_CONFIG_INIT;
struct option options[] = {
OPT_BOOL(0, "auto", &opts.auto_flag,
N_("run tasks based on the state of the repository")),
OPT_CALLBACK_F(0, "task", &selected_tasks, N_("task"),
N_("check a specific task"),
PARSE_OPT_NONEG, task_option_parse),
OPT_END()
};
bool is_needed = false;
argc = parse_options(argc, argv, prefix, options,
builtin_maintenance_is_needed_usage,
PARSE_OPT_STOP_AT_NON_OPTION);
if (argc)
usage_with_options(builtin_maintenance_is_needed_usage, options);
gc_config(&cfg);
initialize_task_config(&opts, &selected_tasks);
if (opts.auto_flag) {
for (size_t i = 0; i < opts.tasks_nr; i++) {
if (tasks[opts.tasks[i]].auto_condition &&
tasks[opts.tasks[i]].auto_condition(&cfg)) {
is_needed = true;
break;
}
}
} else {
/*
* When not using --auto we always require maintenance right now.
*
* TODO: this certainly is too eager, as some maintenance tasks may
* decide to not do anything because the data structures are already
* fully optimized. We may eventually want to extend the auto
* condition to also cover non-auto runs so that we can detect such
* cases.
*/
is_needed = true;
}
string_list_clear(&selected_tasks, 0);
maintenance_run_opts_release(&opts);
gc_config_release(&cfg);
if (is_needed)
return 0;
return 1;
}
static const char *const builtin_maintenance_usage[] = {
N_("git maintenance <subcommand> [<options>]"), N_("git maintenance <subcommand> [<options>]"),
NULL, NULL,
}; };
@@ -3270,6 +3330,7 @@ int cmd_maintenance(int argc,
OPT_SUBCOMMAND("stop", &fn, maintenance_stop), OPT_SUBCOMMAND("stop", &fn, maintenance_stop),
OPT_SUBCOMMAND("register", &fn, maintenance_register), OPT_SUBCOMMAND("register", &fn, maintenance_register),
OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister), OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister),
OPT_SUBCOMMAND("is-needed", &fn, maintenance_is_needed),
OPT_END(), OPT_END(),
}; };

View File

@@ -49,7 +49,9 @@ test_expect_success 'run [--auto|--quiet]' '
git maintenance run --auto 2>/dev/null && git maintenance run --auto 2>/dev/null &&
GIT_TRACE2_EVENT="$(pwd)/run-no-quiet.txt" \ GIT_TRACE2_EVENT="$(pwd)/run-no-quiet.txt" \
git maintenance run --no-quiet 2>/dev/null && git maintenance run --no-quiet 2>/dev/null &&
git maintenance is-needed &&
test_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-no-auto.txt && test_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-no-auto.txt &&
! git maintenance is-needed --auto &&
test_subcommand ! git gc --auto --quiet --no-detach --skip-foreground-tasks <run-auto.txt && test_subcommand ! git gc --auto --quiet --no-detach --skip-foreground-tasks <run-auto.txt &&
test_subcommand git gc --no-quiet --no-detach --skip-foreground-tasks <run-no-quiet.txt test_subcommand git gc --no-quiet --no-detach --skip-foreground-tasks <run-no-quiet.txt
' '
@@ -180,6 +182,11 @@ test_expect_success 'commit-graph auto condition' '
test_commit first && test_commit first &&
! git -c maintenance.commit-graph.auto=0 \
maintenance is-needed --auto --task=commit-graph &&
git -c maintenance.commit-graph.auto=1 \
maintenance is-needed --auto --task=commit-graph &&
GIT_TRACE2_EVENT="$(pwd)/cg-zero-means-no.txt" \ GIT_TRACE2_EVENT="$(pwd)/cg-zero-means-no.txt" \
git -c maintenance.commit-graph.auto=0 $COMMAND && git -c maintenance.commit-graph.auto=0 $COMMAND &&
GIT_TRACE2_EVENT="$(pwd)/cg-one-satisfied.txt" \ GIT_TRACE2_EVENT="$(pwd)/cg-one-satisfied.txt" \
@@ -290,16 +297,23 @@ test_expect_success 'maintenance.loose-objects.auto' '
git -c maintenance.loose-objects.auto=1 maintenance \ git -c maintenance.loose-objects.auto=1 maintenance \
run --auto --task=loose-objects 2>/dev/null && run --auto --task=loose-objects 2>/dev/null &&
test_subcommand ! git prune-packed --quiet <trace-lo1.txt && test_subcommand ! git prune-packed --quiet <trace-lo1.txt &&
printf data-A | git hash-object -t blob --stdin -w && printf data-A | git hash-object -t blob --stdin -w &&
! git -c maintenance.loose-objects.auto=2 \
maintenance is-needed --auto --task=loose-objects &&
GIT_TRACE2_EVENT="$(pwd)/trace-loA" \ GIT_TRACE2_EVENT="$(pwd)/trace-loA" \
git -c maintenance.loose-objects.auto=2 \ git -c maintenance.loose-objects.auto=2 \
maintenance run --auto --task=loose-objects 2>/dev/null && maintenance run --auto --task=loose-objects 2>/dev/null &&
test_subcommand ! git prune-packed --quiet <trace-loA && test_subcommand ! git prune-packed --quiet <trace-loA &&
printf data-B | git hash-object -t blob --stdin -w && printf data-B | git hash-object -t blob --stdin -w &&
git -c maintenance.loose-objects.auto=2 \
maintenance is-needed --auto --task=loose-objects &&
GIT_TRACE2_EVENT="$(pwd)/trace-loB" \ GIT_TRACE2_EVENT="$(pwd)/trace-loB" \
git -c maintenance.loose-objects.auto=2 \ git -c maintenance.loose-objects.auto=2 \
maintenance run --auto --task=loose-objects 2>/dev/null && maintenance run --auto --task=loose-objects 2>/dev/null &&
test_subcommand git prune-packed --quiet <trace-loB && test_subcommand git prune-packed --quiet <trace-loB &&
GIT_TRACE2_EVENT="$(pwd)/trace-loC" \ GIT_TRACE2_EVENT="$(pwd)/trace-loC" \
git -c maintenance.loose-objects.auto=2 \ git -c maintenance.loose-objects.auto=2 \
maintenance run --auto --task=loose-objects 2>/dev/null && maintenance run --auto --task=loose-objects 2>/dev/null &&
@@ -421,10 +435,13 @@ run_incremental_repack_and_verify () {
test_commit A && test_commit A &&
git repack -adk && git repack -adk &&
git multi-pack-index write && git multi-pack-index write &&
! git -c maintenance.incremental-repack.auto=1 \
maintenance is-needed --auto --task=incremental-repack &&
GIT_TRACE2_EVENT="$(pwd)/midx-init.txt" git \ GIT_TRACE2_EVENT="$(pwd)/midx-init.txt" git \
-c maintenance.incremental-repack.auto=1 \ -c maintenance.incremental-repack.auto=1 \
maintenance run --auto --task=incremental-repack 2>/dev/null && maintenance run --auto --task=incremental-repack 2>/dev/null &&
test_subcommand ! git multi-pack-index write --no-progress <midx-init.txt && test_subcommand ! git multi-pack-index write --no-progress <midx-init.txt &&
test_commit B && test_commit B &&
git pack-objects --revs .git/objects/pack/pack <<-\EOF && git pack-objects --revs .git/objects/pack/pack <<-\EOF &&
HEAD HEAD
@@ -434,11 +451,14 @@ run_incremental_repack_and_verify () {
-c maintenance.incremental-repack.auto=2 \ -c maintenance.incremental-repack.auto=2 \
maintenance run --auto --task=incremental-repack 2>/dev/null && maintenance run --auto --task=incremental-repack 2>/dev/null &&
test_subcommand ! git multi-pack-index write --no-progress <trace-A && test_subcommand ! git multi-pack-index write --no-progress <trace-A &&
test_commit C && test_commit C &&
git pack-objects --revs .git/objects/pack/pack <<-\EOF && git pack-objects --revs .git/objects/pack/pack <<-\EOF &&
HEAD HEAD
^HEAD~1 ^HEAD~1
EOF EOF
git -c maintenance.incremental-repack.auto=2 \
maintenance is-needed --auto --task=incremental-repack &&
GIT_TRACE2_EVENT=$(pwd)/trace-B git \ GIT_TRACE2_EVENT=$(pwd)/trace-B git \
-c maintenance.incremental-repack.auto=2 \ -c maintenance.incremental-repack.auto=2 \
maintenance run --auto --task=incremental-repack 2>/dev/null && maintenance run --auto --task=incremental-repack 2>/dev/null &&
@@ -485,9 +505,15 @@ test_expect_success 'reflog-expire task --auto only packs when exceeding limits'
git reflog expire --all --expire=now && git reflog expire --all --expire=now &&
test_commit reflog-one && test_commit reflog-one &&
test_commit reflog-two && test_commit reflog-two &&
! git -c maintenance.reflog-expire.auto=3 \
maintenance is-needed --auto --task=reflog-expire &&
GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \ GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \
git -c maintenance.reflog-expire.auto=3 maintenance run --auto --task=reflog-expire && git -c maintenance.reflog-expire.auto=3 maintenance run --auto --task=reflog-expire &&
test_subcommand ! git reflog expire --all <reflog-expire-auto.txt && test_subcommand ! git reflog expire --all <reflog-expire-auto.txt &&
git -c maintenance.reflog-expire.auto=2 \
maintenance is-needed --auto --task=reflog-expire &&
GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \ GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \
git -c maintenance.reflog-expire.auto=2 maintenance run --auto --task=reflog-expire && git -c maintenance.reflog-expire.auto=2 maintenance run --auto --task=reflog-expire &&
test_subcommand git reflog expire --all <reflog-expire-auto.txt test_subcommand git reflog expire --all <reflog-expire-auto.txt
@@ -514,6 +540,7 @@ test_expect_success 'worktree-prune task --auto only prunes with prunable worktr
test_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune && test_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&
mkdir .git/worktrees && mkdir .git/worktrees &&
: >.git/worktrees/abc && : >.git/worktrees/abc &&
git maintenance is-needed --auto --task=worktree-prune &&
test_expect_worktree_prune git maintenance run --auto --task=worktree-prune test_expect_worktree_prune git maintenance run --auto --task=worktree-prune
' '
@@ -530,22 +557,7 @@ test_expect_success 'worktree-prune task with --auto honors maintenance.worktree
test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-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. # 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=4 maintenance run --auto --task=worktree-prune &&
test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune git -c maintenance.worktree-prune.auto=3 maintenance is-needed --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_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune
' '
@@ -554,11 +566,13 @@ test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '
rm -rf worktree && rm -rf worktree &&
rm -f worktree-prune.txt && rm -f worktree-prune.txt &&
! git -c gc.worktreePruneExpire=1.week.ago maintenance is-needed --auto --task=worktree-prune &&
GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=1.week.ago maintenance run --auto --task=worktree-prune && 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_subcommand ! git worktree prune --expire 1.week.ago <worktree-prune.txt &&
test_path_is_dir .git/worktrees/worktree && test_path_is_dir .git/worktrees/worktree &&
rm -f worktree-prune.txt && rm -f worktree-prune.txt &&
git -c gc.worktreePruneExpire=now maintenance is-needed --auto --task=worktree-prune &&
GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=now maintenance run --auto --task=worktree-prune && 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_subcommand git worktree prune --expire now <worktree-prune.txt &&
test_path_is_missing .git/worktrees/worktree test_path_is_missing .git/worktrees/worktree
@@ -583,10 +597,13 @@ test_expect_success 'rerere-gc task without --auto always collects garbage' '
test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' ' test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '
test_when_finished "rm -rf .git/rr-cache" && test_when_finished "rm -rf .git/rr-cache" &&
! git maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc && test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
mkdir .git/rr-cache && mkdir .git/rr-cache &&
! git maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc && test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
: >.git/rr-cache/entry && : >.git/rr-cache/entry &&
git maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc git maintenance run --auto --task=rerere-gc test_expect_rerere_gc git maintenance run --auto --task=rerere-gc
' '
@@ -594,17 +611,22 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut
test_when_finished "rm -rf .git/rr-cache" && test_when_finished "rm -rf .git/rr-cache" &&
# A negative value should always prune. # A negative value should always prune.
git -c maintenance.rerere-gc.auto=-1 maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc && 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. # A positive value prunes when there is at least one entry.
! git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc && test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
mkdir .git/rr-cache && mkdir .git/rr-cache &&
! git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc && test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
: >.git/rr-cache/entry-1 && : >.git/rr-cache/entry-1 &&
git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc && test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
# Zero should never prune. # Zero should never prune.
: >.git/rr-cache/entry-1 && : >.git/rr-cache/entry-1 &&
! git -c maintenance.rerere-gc.auto=0 maintenance is-needed --auto --task=rerere-gc &&
test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc
' '