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>

Karthik Nayak committed Nov 8, 2025 at 22:51 UTC 28b83e6f08ae022d54d79e518e72933ae0930091
3 files changed +113 -17
Documentation/git-maintenance.adoc
+13
@@ -12,6 +12,7 @@ SYNOPSIS
12 'git maintenance' run [<options>]
13 'git maintenance' start [--scheduler=<scheduler>]
14 'git maintenance' (stop|register|unregister) [<options>]
15 +'git maintenance' is-needed [<options>]
16
17
18 DESCRIPTION
@@ -84,6 +85,16 @@ The `unregister` subcommand will report an error if the current repository
85 is not already registered. Use the `--force` option to return success even
86 when the current repository is not registered.
87
88 +is-needed::
89 + Check whether maintenance needs to be run without actually running it.
90 + Exits with a 0 status code if maintenance needs to be run, 1 otherwise.
91 + Ideally used with the '--auto' flag.
92 ++
93 +If one or more `--task` options are specified, then those tasks are checked
94 +in that order. Otherwise, the tasks are determined by which
95 +`maintenance.<task>.enabled` config options are true. By default, only
96 +`maintenance.gc.enabled` is true.
97 +
98 TASKS
99 -----
100
@@ -183,6 +194,8 @@ OPTIONS
194 in the `gc.auto` config setting, or when the number of pack-files
195 exceeds the `gc.autoPackLimit` config setting. Not compatible with
196 the `--schedule` option.
197 + When combined with the `is-needed` subcommand, check if the required
198 + thresholds are met without actually running maintenance.
199
200 --schedule::
201 When combined with the `run` subcommand, run maintenance tasks
builtin/gc.c
+62 -1
@@ -3253,7 +3253,67 @@ static int maintenance_stop(int argc, const char **argv, const char *prefix,
3253 return update_background_schedule(NULL, 0);
3254 }
3255
3256 -static const char * const builtin_maintenance_usage[] = {
3256 +static const char *const builtin_maintenance_is_needed_usage[] = {
3257 + "git maintenance is-needed [--task=<task>] [--schedule]",
3258 + NULL
3259 +};
3260 +
3261 +static int maintenance_is_needed(int argc, const char **argv, const char *prefix,
3262 + struct repository *repo UNUSED)
3263 +{
3264 + struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;
3265 + struct string_list selected_tasks = STRING_LIST_INIT_DUP;
3266 + struct gc_config cfg = GC_CONFIG_INIT;
3267 + struct option options[] = {
3268 + OPT_BOOL(0, "auto", &opts.auto_flag,
3269 + N_("run tasks based on the state of the repository")),
3270 + OPT_CALLBACK_F(0, "task", &selected_tasks, N_("task"),
3271 + N_("check a specific task"),
3272 + PARSE_OPT_NONEG, task_option_parse),
3273 + OPT_END()
3274 + };
3275 + bool is_needed = false;
3276 +
3277 + argc = parse_options(argc, argv, prefix, options,
3278 + builtin_maintenance_is_needed_usage,
3279 + PARSE_OPT_STOP_AT_NON_OPTION);
3280 + if (argc)
3281 + usage_with_options(builtin_maintenance_is_needed_usage, options);
3282 +
3283 + gc_config(&cfg);
3284 + initialize_task_config(&opts, &selected_tasks);
3285 +
3286 + if (opts.auto_flag) {
3287 + for (size_t i = 0; i < opts.tasks_nr; i++) {
3288 + if (tasks[opts.tasks[i]].auto_condition &&
3289 + tasks[opts.tasks[i]].auto_condition(&cfg)) {
3290 + is_needed = true;
3291 + break;
3292 + }
3293 + }
3294 + } else {
3295 + /*
3296 + * When not using --auto we always require maintenance right now.
3297 + *
3298 + * TODO: this certainly is too eager, as some maintenance tasks may
3299 + * decide to not do anything because the data structures are already
3300 + * fully optimized. We may eventually want to extend the auto
3301 + * condition to also cover non-auto runs so that we can detect such
3302 + * cases.
3303 + */
3304 + is_needed = true;
3305 + }
3306 +
3307 + string_list_clear(&selected_tasks, 0);
3308 + maintenance_run_opts_release(&opts);
3309 + gc_config_release(&cfg);
3310 +
3311 + if (is_needed)
3312 + return 0;
3313 + return 1;
3314 +}
3315 +
3316 +static const char *const builtin_maintenance_usage[] = {
3317 N_("git maintenance <subcommand> [<options>]"),
3318 NULL,
3319 };
@@ -3270,6 +3330,7 @@ int cmd_maintenance(int argc,
3330 OPT_SUBCOMMAND("stop", &fn, maintenance_stop),
3331 OPT_SUBCOMMAND("register", &fn, maintenance_register),
3332 OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister),
3333 + OPT_SUBCOMMAND("is-needed", &fn, maintenance_is_needed),
3334 OPT_END(),
3335 };
3336
t/t7900-maintenance.sh
+38 -16
@@ -49,7 +49,9 @@ test_expect_success 'run [--auto|--quiet]' '
49 git maintenance run --auto 2>/dev/null &&
50 GIT_TRACE2_EVENT="$(pwd)/run-no-quiet.txt" \
51 git maintenance run --no-quiet 2>/dev/null &&
52 + git maintenance is-needed &&
53 test_subcommand git gc --quiet --no-detach --skip-foreground-tasks <run-no-auto.txt &&
54 + ! git maintenance is-needed --auto &&
55 test_subcommand ! git gc --auto --quiet --no-detach --skip-foreground-tasks <run-auto.txt &&
56 test_subcommand git gc --no-quiet --no-detach --skip-foreground-tasks <run-no-quiet.txt
57 '
@@ -180,6 +182,11 @@ test_expect_success 'commit-graph auto condition' '
182
183 test_commit first &&
184
185 + ! git -c maintenance.commit-graph.auto=0 \
186 + maintenance is-needed --auto --task=commit-graph &&
187 + git -c maintenance.commit-graph.auto=1 \
188 + maintenance is-needed --auto --task=commit-graph &&
189 +
190 GIT_TRACE2_EVENT="$(pwd)/cg-zero-means-no.txt" \
191 git -c maintenance.commit-graph.auto=0 $COMMAND &&
192 GIT_TRACE2_EVENT="$(pwd)/cg-one-satisfied.txt" \
@@ -290,16 +297,23 @@ test_expect_success 'maintenance.loose-objects.auto' '
297 git -c maintenance.loose-objects.auto=1 maintenance \
298 run --auto --task=loose-objects 2>/dev/null &&
299 test_subcommand ! git prune-packed --quiet <trace-lo1.txt &&
300 +
301 printf data-A | git hash-object -t blob --stdin -w &&
302 + ! git -c maintenance.loose-objects.auto=2 \
303 + maintenance is-needed --auto --task=loose-objects &&
304 GIT_TRACE2_EVENT="$(pwd)/trace-loA" \
305 git -c maintenance.loose-objects.auto=2 \
306 maintenance run --auto --task=loose-objects 2>/dev/null &&
307 test_subcommand ! git prune-packed --quiet <trace-loA &&
308 +
309 printf data-B | git hash-object -t blob --stdin -w &&
310 + git -c maintenance.loose-objects.auto=2 \
311 + maintenance is-needed --auto --task=loose-objects &&
312 GIT_TRACE2_EVENT="$(pwd)/trace-loB" \
313 git -c maintenance.loose-objects.auto=2 \
314 maintenance run --auto --task=loose-objects 2>/dev/null &&
315 test_subcommand git prune-packed --quiet <trace-loB &&
316 +
317 GIT_TRACE2_EVENT="$(pwd)/trace-loC" \
318 git -c maintenance.loose-objects.auto=2 \
319 maintenance run --auto --task=loose-objects 2>/dev/null &&
@@ -421,10 +435,13 @@ run_incremental_repack_and_verify () {
435 test_commit A &&
436 git repack -adk &&
437 git multi-pack-index write &&
438 + ! git -c maintenance.incremental-repack.auto=1 \
439 + maintenance is-needed --auto --task=incremental-repack &&
440 GIT_TRACE2_EVENT="$(pwd)/midx-init.txt" git \
441 -c maintenance.incremental-repack.auto=1 \
442 maintenance run --auto --task=incremental-repack 2>/dev/null &&
443 test_subcommand ! git multi-pack-index write --no-progress <midx-init.txt &&
444 +
445 test_commit B &&
446 git pack-objects --revs .git/objects/pack/pack <<-\EOF &&
447 HEAD
@@ -434,11 +451,14 @@ run_incremental_repack_and_verify () {
451 -c maintenance.incremental-repack.auto=2 \
452 maintenance run --auto --task=incremental-repack 2>/dev/null &&
453 test_subcommand ! git multi-pack-index write --no-progress <trace-A &&
454 +
455 test_commit C &&
456 git pack-objects --revs .git/objects/pack/pack <<-\EOF &&
457 HEAD
458 ^HEAD~1
459 EOF
460 + git -c maintenance.incremental-repack.auto=2 \
461 + maintenance is-needed --auto --task=incremental-repack &&
462 GIT_TRACE2_EVENT=$(pwd)/trace-B git \
463 -c maintenance.incremental-repack.auto=2 \
464 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'
505 git reflog expire --all --expire=now &&
506 test_commit reflog-one &&
507 test_commit reflog-two &&
508 +
509 + ! git -c maintenance.reflog-expire.auto=3 \
510 + maintenance is-needed --auto --task=reflog-expire &&
511 GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \
512 git -c maintenance.reflog-expire.auto=3 maintenance run --auto --task=reflog-expire &&
513 test_subcommand ! git reflog expire --all <reflog-expire-auto.txt &&
514 +
515 + git -c maintenance.reflog-expire.auto=2 \
516 + maintenance is-needed --auto --task=reflog-expire &&
517 GIT_TRACE2_EVENT="$(pwd)/reflog-expire-auto.txt" \
518 git -c maintenance.reflog-expire.auto=2 maintenance run --auto --task=reflog-expire &&
519 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
540 test_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&
541 mkdir .git/worktrees &&
542 : >.git/worktrees/abc &&
543 + git maintenance is-needed --auto --task=worktree-prune &&
544 test_expect_worktree_prune git maintenance run --auto --task=worktree-prune
545 '
546
@@ -530,22 +557,7 @@ test_expect_success 'worktree-prune task with --auto honors maintenance.worktree
557 test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&
558 # A positive value should require at least this many prunable worktrees.
559 test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&
533 - test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune
534 -'
535 -
536 -test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '
537 - # A negative value should always prune.
538 - test_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&
539 -
540 - mkdir .git/worktrees &&
541 - : >.git/worktrees/first &&
542 - : >.git/worktrees/second &&
543 - : >.git/worktrees/third &&
544 -
545 - # Zero should never prune.
546 - test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&
547 - # A positive value should require at least this many prunable worktrees.
548 - test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&
560 + git -c maintenance.worktree-prune.auto=3 maintenance is-needed --auto --task=worktree-prune &&
561 test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune
562 '
563
@@ -554,11 +566,13 @@ test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '
566 rm -rf worktree &&
567
568 rm -f worktree-prune.txt &&
569 + ! git -c gc.worktreePruneExpire=1.week.ago maintenance is-needed --auto --task=worktree-prune &&
570 GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=1.week.ago maintenance run --auto --task=worktree-prune &&
571 test_subcommand ! git worktree prune --expire 1.week.ago <worktree-prune.txt &&
572 test_path_is_dir .git/worktrees/worktree &&
573
574 rm -f worktree-prune.txt &&
575 + git -c gc.worktreePruneExpire=now maintenance is-needed --auto --task=worktree-prune &&
576 GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=now maintenance run --auto --task=worktree-prune &&
577 test_subcommand git worktree prune --expire now <worktree-prune.txt &&
578 test_path_is_missing .git/worktrees/worktree
@@ -583,10 +597,13 @@ test_expect_success 'rerere-gc task without --auto always collects garbage' '
597
598 test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '
599 test_when_finished "rm -rf .git/rr-cache" &&
600 + ! git maintenance is-needed --auto --task=rerere-gc &&
601 test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
602 mkdir .git/rr-cache &&
603 + ! git maintenance is-needed --auto --task=rerere-gc &&
604 test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
605 : >.git/rr-cache/entry &&
606 + git maintenance is-needed --auto --task=rerere-gc &&
607 test_expect_rerere_gc git maintenance run --auto --task=rerere-gc
608 '
609
@@ -594,17 +611,22 @@ test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.aut
611 test_when_finished "rm -rf .git/rr-cache" &&
612
613 # A negative value should always prune.
614 + git -c maintenance.rerere-gc.auto=-1 maintenance is-needed --auto --task=rerere-gc &&
615 test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&
616
617 # A positive value prunes when there is at least one entry.
618 + ! git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc &&
619 test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
620 mkdir .git/rr-cache &&
621 + ! git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc &&
622 test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
623 : >.git/rr-cache/entry-1 &&
624 + git -c maintenance.rerere-gc.auto=9000 maintenance is-needed --auto --task=rerere-gc &&
625 test_expect_rerere_gc git -c maintenance.rerere-gc.auto=9000 maintenance run --auto --task=rerere-gc &&
626
627 # Zero should never prune.
628 : >.git/rr-cache/entry-1 &&
629 + ! git -c maintenance.rerere-gc.auto=0 maintenance is-needed --auto --task=rerere-gc &&
630 test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc
631 '
632