Skip to content

Commit b66f827

Browse files
committed
maintenance: add prune-remote-refs task
Remote-tracking refs can accumulate in local repositories even as branches are deleted on remotes, impacting git performance negatively. Existing alternatives to keep refs pruned have a few issues —  1. The `fetch.prune` config automatically cleans up remote ref on fetch, but also pulls in new ref from remote which is an undesirable side-effect. 2.`git remote prune` cleans up refs without adding to the existing list but requires periodic user intervention. This adds a new maintenance task 'prune-remote-refs' that runs 'git remote prune' for each configured remote daily. This provides an automated way to clean up stale remote-tracking refs — especially when a users may not do a full fetch. This task is disabled by default. Signed-off-by: Shubham Kanodia <[email protected]>
1 parent 063bceb commit b66f827

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

Documentation/git-maintenance.txt

+20
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 git-maintenance. For most users, configuring `fetch.prune=true`
170+
is a 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 not affect refs outside the fetched hierarchy
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

+42
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "lockfile.h"
2121
#include "parse-options.h"
2222
#include "run-command.h"
23+
#include "remote.h"
2324
#include "sigchain.h"
2425
#include "strvec.h"
2526
#include "commit.h"
@@ -913,6 +914,40 @@ static int maintenance_opt_schedule(const struct option *opt, const char *arg,
913914
return 0;
914915
}
915916

917+
static int collect_remote(struct remote *remote, void *cb_data)
918+
{
919+
struct string_list *list = cb_data;
920+
921+
if (!remote->url.nr)
922+
return 0;
923+
924+
string_list_append(list, remote->name);
925+
return 0;
926+
}
927+
928+
static int maintenance_task_prune_remote(struct maintenance_run_opts *opts UNUSED,
929+
struct gc_config *cfg UNUSED)
930+
{
931+
struct string_list_item *item;
932+
struct string_list remotes_list = STRING_LIST_INIT_NODUP;
933+
struct child_process child = CHILD_PROCESS_INIT;
934+
int result = 0;
935+
936+
for_each_remote(collect_remote, &remotes_list);
937+
938+
for_each_string_list_item (item, &remotes_list) {
939+
const char *remote_name = item->string;
940+
child.git_cmd = 1;
941+
strvec_pushl(&child.args, "remote", "prune", remote_name, NULL);
942+
943+
if (run_command(&child))
944+
result = error(_("failed to prune '%s'"), remote_name);
945+
}
946+
947+
string_list_clear(&remotes_list, 0);
948+
return result;
949+
}
950+
916951
/* Remember to update object flag allocation in object.h */
917952
#define SEEN (1u<<0)
918953

@@ -1375,6 +1410,7 @@ enum maintenance_task_label {
13751410
TASK_GC,
13761411
TASK_COMMIT_GRAPH,
13771412
TASK_PACK_REFS,
1413+
TASK_PRUNE_REMOTE_REFS,
13781414

13791415
/* Leave as final value */
13801416
TASK__COUNT
@@ -1411,6 +1447,10 @@ static struct maintenance_task tasks[] = {
14111447
maintenance_task_pack_refs,
14121448
pack_refs_condition,
14131449
},
1450+
[TASK_PRUNE_REMOTE_REFS] = {
1451+
"prune-remote-refs",
1452+
maintenance_task_prune_remote,
1453+
},
14141454
};
14151455

14161456
static int compare_tasks_by_selection(const void *a_, const void *b_)
@@ -1505,6 +1545,8 @@ static void initialize_maintenance_strategy(void)
15051545
tasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;
15061546
tasks[TASK_PACK_REFS].enabled = 1;
15071547
tasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;
1548+
tasks[TASK_PRUNE_REMOTE_REFS].enabled = 0;
1549+
tasks[TASK_PRUNE_REMOTE_REFS].schedule = SCHEDULE_DAILY;
15081550
}
15091551
}
15101552

t/t7900-maintenance.sh

+44
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,50 @@ test_expect_success 'pack-refs task' '
446446
test_subcommand git pack-refs --all --prune <pack-refs.txt
447447
'
448448

449+
test_expect_success 'prune-remote-refs task not enabled by default' '
450+
git clone . prune-test &&
451+
(
452+
cd prune-test &&
453+
GIT_TRACE2_EVENT="$(pwd)/prune.txt" git maintenance run 2>err &&
454+
test_subcommand ! git remote prune origin <prune.txt
455+
)
456+
'
457+
458+
test_expect_success 'prune-remote-refs task cleans stale remote refs' '
459+
test_commit initial &&
460+
461+
# Create two separate remote repos
462+
git clone . remote1 &&
463+
git clone . remote2 &&
464+
465+
git clone . prune-test-clean &&
466+
(
467+
cd prune-test-clean &&
468+
git config maintenance.prune-remote-refs.enabled true &&
469+
470+
# Add both remotes
471+
git remote add remote1 "../remote1" &&
472+
git remote add remote2 "../remote2" &&
473+
474+
# Create and push branches to both remotes
475+
git branch -f side2 HEAD &&
476+
git push remote1 side2 &&
477+
git push remote2 side2 &&
478+
479+
# Rename branches in each remote to simulate a stale branch
480+
git -C ../remote1 branch -m side2 side3 &&
481+
git -C ../remote2 branch -m side2 side4 &&
482+
483+
GIT_TRACE2_EVENT="$(pwd)/prune.txt" git maintenance run --task=prune-remote-refs &&
484+
485+
# Verify pruning happened for both remotes
486+
test_subcommand git remote prune remote1 <prune.txt &&
487+
test_subcommand git remote prune remote2 <prune.txt &&
488+
test_must_fail git rev-parse refs/remotes/remote1/side2 &&
489+
test_must_fail git rev-parse refs/remotes/remote2/side2
490+
)
491+
'
492+
449493
test_expect_success '--auto and --schedule incompatible' '
450494
test_must_fail git maintenance run --auto --schedule=daily 2>err &&
451495
test_grep "at most one" err

0 commit comments

Comments
 (0)