Skip to content

Commit 5411572

Browse files
committed
Merge branch 'sk/maintenance-remote-prune' into seen
A new periodic maintenance task to run "git remote prune" has been introduced. * sk/maintenance-remote-prune: maintenance: add prune-remote-refs task
2 parents afa60be + 7559fd9 commit 5411572

File tree

3 files changed

+187
-7
lines changed

3 files changed

+187
-7
lines changed

Documentation/git-maintenance.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,26 @@ pack-refs::
158158
need to iterate across many references. See linkgit:git-pack-refs[1]
159159
for more information.
160160

161+
prune-remote-refs::
162+
The `prune-remote-refs` task runs `git remote prune` on each remote
163+
repository registered in the local repository. This task helps clean
164+
up deleted remote branches, improving the performance of operations
165+
that iterate through the refs. See linkgit:git-remote[1] for more
166+
information. This task is disabled by default.
167+
+
168+
NOTE: This task is opt-in to prevent unexpected removal of remote refs
169+
for users of linkgit:git-maintenance[1]. For most users, configuring `fetch.prune=true`
170+
is an acceptable solution, as it will automatically clean up stale remote-tracking
171+
branches during normal fetch operations. However, this task can be useful in
172+
specific scenarios:
173+
+
174+
--
175+
* When using selective fetching (e.g., `git fetch origin +foo:refs/remotes/origin/foo`)
176+
where `fetch.prune` would only affect refs that are explicitly fetched.
177+
* When third-party tools might perform unexpected full fetches, and you want
178+
periodic cleanup independently of fetch operations.
179+
--
180+
161181
OPTIONS
162182
-------
163183
--auto::

builtin/gc.c

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "lockfile.h"
2424
#include "parse-options.h"
2525
#include "run-command.h"
26+
#include "remote.h"
2627
#include "sigchain.h"
2728
#include "strvec.h"
2829
#include "commit.h"
@@ -916,6 +917,63 @@ static int maintenance_opt_schedule(const struct option *opt, const char *arg,
916917
return 0;
917918
}
918919

920+
struct remote_cb_data {
921+
struct maintenance_run_opts *maintenance_opts;
922+
struct string_list failed_remotes;
923+
};
924+
925+
static void report_failed_remotes(struct string_list *failed_remotes,
926+
const char *action_name)
927+
{
928+
if (failed_remotes->nr) {
929+
int i;
930+
struct strbuf msg = STRBUF_INIT;
931+
strbuf_addf(&msg, _("failed to %s the following remotes: "),
932+
action_name);
933+
for (i = 0; i < failed_remotes->nr; i++) {
934+
if (i)
935+
strbuf_addstr(&msg, ", ");
936+
strbuf_addstr(&msg, failed_remotes->items[i].string);
937+
}
938+
error("%s", msg.buf);
939+
strbuf_release(&msg);
940+
}
941+
}
942+
943+
static int prune_remote(struct remote *remote, void *cb_data)
944+
{
945+
struct child_process child = CHILD_PROCESS_INIT;
946+
struct remote_cb_data *data = cb_data;
947+
948+
if (!remote->url.nr)
949+
return 0;
950+
951+
child.git_cmd = 1;
952+
strvec_pushl(&child.args, "remote", "prune", remote->name, NULL);
953+
954+
if (run_command(&child))
955+
string_list_append(&data->failed_remotes, remote->name);
956+
957+
return 0;
958+
}
959+
960+
static int maintenance_task_prune_remote(struct maintenance_run_opts *opts,
961+
struct gc_config *cfg UNUSED)
962+
{
963+
struct remote_cb_data cbdata = { .maintenance_opts = opts,
964+
.failed_remotes = STRING_LIST_INIT_DUP };
965+
966+
int result;
967+
result = for_each_remote(prune_remote, &cbdata);
968+
969+
report_failed_remotes(&cbdata.failed_remotes, "prune");
970+
if (cbdata.failed_remotes.nr)
971+
result = 1;
972+
973+
string_list_clear(&cbdata.failed_remotes, 0);
974+
return result;
975+
}
976+
919977
/* Remember to update object flag allocation in object.h */
920978
#define SEEN (1u<<0)
921979

@@ -1036,8 +1094,8 @@ static int maintenance_task_commit_graph(struct maintenance_run_opts *opts,
10361094

10371095
static int fetch_remote(struct remote *remote, void *cbdata)
10381096
{
1039-
struct maintenance_run_opts *opts = cbdata;
10401097
struct child_process child = CHILD_PROCESS_INIT;
1098+
struct remote_cb_data *data = cbdata;
10411099

10421100
if (remote->skip_default_update)
10431101
return 0;
@@ -1048,21 +1106,34 @@ static int fetch_remote(struct remote *remote, void *cbdata)
10481106
"--no-write-fetch-head", "--recurse-submodules=no",
10491107
NULL);
10501108

1051-
if (opts->quiet)
1109+
if (data->maintenance_opts->quiet)
10521110
strvec_push(&child.args, "--quiet");
10531111

1054-
return !!run_command(&child);
1112+
if (run_command(&child))
1113+
string_list_append(&data->failed_remotes, remote->name);
1114+
1115+
return 0;
10551116
}
10561117

10571118
static int maintenance_task_prefetch(struct maintenance_run_opts *opts,
10581119
struct gc_config *cfg UNUSED)
10591120
{
1060-
if (for_each_remote(fetch_remote, opts)) {
1061-
error(_("failed to prefetch remotes"));
1062-
return 1;
1121+
struct remote_cb_data cbdata = { .maintenance_opts = opts,
1122+
.failed_remotes = STRING_LIST_INIT_DUP };
1123+
1124+
int result = 0;
1125+
1126+
if (for_each_remote(fetch_remote, &cbdata)) {
1127+
error(_("failed to prefetch some remotes"));
1128+
result = 1;
10631129
}
10641130

1065-
return 0;
1131+
report_failed_remotes(&cbdata.failed_remotes, "prefetch");
1132+
if (cbdata.failed_remotes.nr)
1133+
result = 1;
1134+
1135+
string_list_clear(&cbdata.failed_remotes, 0);
1136+
return result;
10661137
}
10671138

10681139
static int maintenance_task_gc(struct maintenance_run_opts *opts,
@@ -1378,6 +1449,7 @@ enum maintenance_task_label {
13781449
TASK_GC,
13791450
TASK_COMMIT_GRAPH,
13801451
TASK_PACK_REFS,
1452+
TASK_PRUNE_REMOTE_REFS,
13811453

13821454
/* Leave as final value */
13831455
TASK__COUNT
@@ -1414,6 +1486,10 @@ static struct maintenance_task tasks[] = {
14141486
maintenance_task_pack_refs,
14151487
pack_refs_condition,
14161488
},
1489+
[TASK_PRUNE_REMOTE_REFS] = {
1490+
"prune-remote-refs",
1491+
maintenance_task_prune_remote,
1492+
},
14171493
};
14181494

14191495
static int compare_tasks_by_selection(const void *a_, const void *b_)
@@ -1508,6 +1584,8 @@ static void initialize_maintenance_strategy(void)
15081584
tasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;
15091585
tasks[TASK_PACK_REFS].enabled = 1;
15101586
tasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;
1587+
tasks[TASK_PRUNE_REMOTE_REFS].enabled = 0;
1588+
tasks[TASK_PRUNE_REMOTE_REFS].schedule = SCHEDULE_DAILY;
15111589
}
15121590
}
15131591

t/t7900-maintenance.sh

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,88 @@ test_expect_success 'pack-refs task' '
447447
test_subcommand git pack-refs --all --prune <pack-refs.txt
448448
'
449449

450+
test_expect_success 'prune-remote-refs task not enabled by default' '
451+
git clone . prune-test &&
452+
(
453+
cd prune-test &&
454+
GIT_TRACE2_EVENT="$(pwd)/prune.txt" git maintenance run 2>err &&
455+
test_subcommand ! git remote prune origin <prune.txt
456+
)
457+
'
458+
459+
test_expect_success 'prune-remote-refs task cleans stale remote refs' '
460+
test_commit initial &&
461+
462+
# Create two separate remote repos
463+
git clone . remote1 &&
464+
git clone . remote2 &&
465+
466+
git clone . prune-test-clean &&
467+
(
468+
cd prune-test-clean &&
469+
git config maintenance.prune-remote-refs.enabled true &&
470+
471+
# Add both remotes
472+
git remote add remote1 "../remote1" &&
473+
git remote add remote2 "../remote2" &&
474+
475+
# Create and push branches to both remotes
476+
git branch -f side2 HEAD &&
477+
git push remote1 side2 &&
478+
git push remote2 side2 &&
479+
480+
# Rename branches in each remote to simulate a stale branch
481+
git -C ../remote1 branch -m side2 side3 &&
482+
git -C ../remote2 branch -m side2 side4 &&
483+
484+
GIT_TRACE2_EVENT="$(pwd)/prune.txt" git maintenance run --task=prune-remote-refs &&
485+
486+
# Verify pruning happened for both remotes
487+
test_subcommand git remote prune remote1 <prune.txt &&
488+
test_subcommand git remote prune remote2 <prune.txt &&
489+
test_must_fail git rev-parse refs/remotes/remote1/side2 &&
490+
test_must_fail git rev-parse refs/remotes/remote2/side2
491+
)
492+
'
493+
494+
test_expect_success 'prune-remote-refs task continues to prune remotes even if some fail' '
495+
test_commit initial-prune-remote-refs &&
496+
497+
git clone . remote-bad1 &&
498+
git clone . remote-bad2 &&
499+
git clone . remote-good &&
500+
501+
git clone . prune-test-partial &&
502+
(
503+
cd prune-test-partial &&
504+
git config maintenance.prune-remote-refs.enabled true &&
505+
506+
# Add remotes in alphabetical order to ensure processing order
507+
git remote add aaa-bad1 "../remote-bad1" &&
508+
git remote add bbb-bad2 "../remote-bad2" &&
509+
git remote add ccc-good "../remote-good" &&
510+
511+
# Create and push branches to all remotes
512+
git branch -f side2 HEAD &&
513+
git push aaa-bad1 side2 &&
514+
git push bbb-bad2 side2 &&
515+
git push ccc-good side2 &&
516+
517+
# Rename branch in good remote to simulate a stale branch
518+
git -C ../remote-good branch -m side2 side3 &&
519+
520+
# Break the bad remotes by removing their directories
521+
rm -rf ../remote-bad1 ../remote-bad2 &&
522+
523+
GIT_TRACE2_EVENT="$(pwd)/prune.txt" git maintenance run --task=prune-remote-refs 2>err || true &&
524+
525+
# Verify pruning happened for good remote despite bad remote failures
526+
test_subcommand git remote prune ccc-good <prune.txt &&
527+
test_must_fail git rev-parse refs/remotes/ccc-good/side2 &&
528+
test_grep "error: failed to prune the following remotes: aaa-bad1, bbb-bad2" err
529+
)
530+
'
531+
450532
test_expect_success '--auto and --schedule incompatible' '
451533
test_must_fail git maintenance run --auto --schedule=daily 2>err &&
452534
test_grep "at most one" err

0 commit comments

Comments
 (0)