builtin/remote.c: show progress when renaming remote references

When renaming a remote, Git needs to rename all remote tracking references to the remote's new name (e.g., renaming "refs/remotes/old/foo" to "refs/remotes/new/foo" when renaming a remote from "old" to "new"). This can be somewhat slow when there are many references to rename, since each rename is done in a separate call to rename_ref() as opposed to grouping all renames together into the same transaction. It would be nice to execute all renames as a single transaction, but there is a snag: the reference transaction backend doesn't support renames during a transaction (only individually, via rename_ref()). The reasons there are described in more detail in [1], but the main problem is that in order to preserve the existing reflog, it must be moved while holding both locks (i.e., on "oldname" and "newname"), and the ref transaction code doesn't support inserting arbitrary actions into the middle of a transaction like that. As an aside, adding support for this to the ref transaction code is less straightforward than inserting both a ref_update() and ref_delete() call into the same transaction. rename_ref()'s special handling to detect D/F conflicts would need to be rewritten for the transaction code if we wanted to proactively catch D/F conflicts when renaming a reference during a transaction. The reftable backend could support this much more readily because of its lack of D/F conflicts. Instead of a more complex modification to the ref transaction code, display a progress meter when running verbosely in order to convince the user that Git is doing work while renaming a remote. This is mostly done as-expected, with the minor caveat that we intentionally count symrefs renames twice, since renaming a symref takes place over two separate calls (one to delete the old one, and another to create the new one). [1]: https://lore.kernel.org/git/572367B4.4050207@alum.mit.edu/ Suggested-by: brian m. carlson <sandals@crustytoothpaste.net> Signed-off-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Taylor Blau committed Mar 3, 2022 at 17:25 UTC 56710a7ae0170404d7bc6411bd5c9a18124c0629
3 files changed +29 -7
Documentation/git-remote.txt
+1 -1
@@ -11,7 +11,7 @@ SYNOPSIS
11 [verse]
12 'git remote' [-v | --verbose]
13 'git remote add' [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
14 -'git remote rename' <old> <new>
14 +'git remote rename' [--[no-]progress] <old> <new>
15 'git remote remove' <name>
16 'git remote set-head' <name> (-a | --auto | -d | --delete | <branch>)
17 'git remote set-branches' [--add] <name> <branch>...
builtin/remote.c
+25 -5
@@ -12,11 +12,12 @@
12 #include "object-store.h"
13 #include "strvec.h"
14 #include "commit-reach.h"
15 +#include "progress.h"
16
17 static const char * const builtin_remote_usage[] = {
18 "git remote [-v | --verbose]",
19 N_("git remote add [-t <branch>] [-m <master>] [-f] [--tags | --no-tags] [--mirror=<fetch|push>] <name> <url>"),
19 - N_("git remote rename <old> <new>"),
20 + N_("git remote rename [--[no-]progress] <old> <new>"),
21 N_("git remote remove <name>"),
22 N_("git remote set-head <name> (-a | --auto | -d | --delete | <branch>)"),
23 N_("git remote [-v | --verbose] show [-n] <name>"),
@@ -36,7 +37,7 @@ static const char * const builtin_remote_add_usage[] = {
37 };
38
39 static const char * const builtin_remote_rename_usage[] = {
39 - N_("git remote rename <old> <new>"),
40 + N_("git remote rename [--[no-]progress] <old> <new>"),
41 NULL
42 };
43
@@ -571,6 +572,7 @@ struct rename_info {
572 const char *old_name;
573 const char *new_name;
574 struct string_list *remote_branches;
575 + uint32_t symrefs_nr;
576 };
577
578 static int read_remote_branches(const char *refname,
@@ -587,10 +589,12 @@ static int read_remote_branches(const char *refname,
589 item = string_list_append(rename->remote_branches, refname);
590 symref = resolve_ref_unsafe(refname, RESOLVE_REF_READING,
591 NULL, &flag);
590 - if (symref && (flag & REF_ISSYMREF))
592 + if (symref && (flag & REF_ISSYMREF)) {
593 item->util = xstrdup(symref);
592 - else
594 + rename->symrefs_nr++;
595 + } else {
596 item->util = NULL;
597 + }
598 }
599 strbuf_release(&buf);
600
@@ -674,7 +678,9 @@ static void handle_push_default(const char* old_name, const char* new_name)
678
679 static int mv(int argc, const char **argv)
680 {
681 + int show_progress = isatty(2);
682 struct option options[] = {
683 + OPT_BOOL(0, "progress", &show_progress, N_("force progress reporting")),
684 OPT_END()
685 };
686 struct remote *oldremote, *newremote;
@@ -682,7 +688,8 @@ static int mv(int argc, const char **argv)
688 old_remote_context = STRBUF_INIT;
689 struct string_list remote_branches = STRING_LIST_INIT_DUP;
690 struct rename_info rename;
685 - int i, refspec_updated = 0;
691 + int i, refs_renamed_nr = 0, refspec_updated = 0;
692 + struct progress *progress = NULL;
693
694 argc = parse_options(argc, argv, NULL, options,
695 builtin_remote_rename_usage, 0);
@@ -693,6 +700,7 @@ static int mv(int argc, const char **argv)
700 rename.old_name = argv[0];
701 rename.new_name = argv[1];
702 rename.remote_branches = &remote_branches;
703 + rename.symrefs_nr = 0;
704
705 oldremote = remote_get(rename.old_name);
706 if (!remote_is_configured(oldremote, 1)) {
@@ -767,6 +775,14 @@ static int mv(int argc, const char **argv)
775 * the new symrefs.
776 */
777 for_each_ref(read_remote_branches, &rename);
778 + if (show_progress) {
779 + /*
780 + * Count symrefs twice, since "renaming" them is done by
781 + * deleting and recreating them in two separate passes.
782 + */
783 + progress = start_progress(_("Renaming remote references"),
784 + rename.remote_branches->nr + rename.symrefs_nr);
785 + }
786 for (i = 0; i < remote_branches.nr; i++) {
787 struct string_list_item *item = remote_branches.items + i;
788 int flag = 0;
@@ -776,6 +792,7 @@ static int mv(int argc, const char **argv)
792 continue;
793 if (delete_ref(NULL, item->string, NULL, REF_NO_DEREF))
794 die(_("deleting '%s' failed"), item->string);
795 + display_progress(progress, ++refs_renamed_nr);
796 }
797 for (i = 0; i < remote_branches.nr; i++) {
798 struct string_list_item *item = remote_branches.items + i;
@@ -791,6 +808,7 @@ static int mv(int argc, const char **argv)
808 item->string, buf.buf);
809 if (rename_ref(item->string, buf.buf, buf2.buf))
810 die(_("renaming '%s' failed"), item->string);
811 + display_progress(progress, ++refs_renamed_nr);
812 }
813 for (i = 0; i < remote_branches.nr; i++) {
814 struct string_list_item *item = remote_branches.items + i;
@@ -810,7 +828,9 @@ static int mv(int argc, const char **argv)
828 item->string, buf.buf);
829 if (create_symref(buf.buf, buf2.buf, buf3.buf))
830 die(_("creating '%s' failed"), buf.buf);
831 + display_progress(progress, ++refs_renamed_nr);
832 }
833 + stop_progress(&progress);
834 string_list_clear(&remote_branches, 1);
835
836 handle_push_default(rename.old_name, rename.new_name);
t/t5505-remote.sh
+3 -1
@@ -753,7 +753,9 @@ test_expect_success 'rename a remote' '
753 (
754 cd four &&
755 git config branch.main.pushRemote origin &&
756 - git remote rename origin upstream &&
756 + GIT_TRACE2_EVENT=$(pwd)/trace \
757 + git remote rename --progress origin upstream &&
758 + test_region progress "Renaming remote references" trace &&
759 grep "pushRemote" .git/config &&
760 test -z "$(git for-each-ref refs/remotes/origin)" &&
761 test "$(git symbolic-ref refs/remotes/upstream/HEAD)" = "refs/remotes/upstream/main" &&