diff mbox series

[v3,2/2] builtin/remote.c: show progress when renaming remote references

Message ID d5b0a4b71027619123b7284611692d3a9c128518.1646346287.git.me@ttaylorr.com (mailing list archive)
State Accepted
Commit 56710a7ae0170404d7bc6411bd5c9a18124c0629
Headers show
Series remote: show progress display when renaming | expand

Commit Message

Taylor Blau March 3, 2022, 10:25 p.m. UTC
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>
---
 Documentation/git-remote.txt |  2 +-
 builtin/remote.c             | 30 +++++++++++++++++++++++++-----
 t/t5505-remote.sh            |  4 +++-
 3 files changed, 29 insertions(+), 7 deletions(-)

Comments

Junio C Hamano March 3, 2022, 11:20 p.m. UTC | #1
Taylor Blau <me@ttaylorr.com> writes:

> 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.

Is it still "when running verbosely"?

I thought that tying this to --[no-]progress was the whole point of
iterating another round.

	... when the standard error output is connected to a
	terminal, so that user knows Git is not completely stuck.

> 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).

That's a nice note to leave here, as it is a bit tricky to reason about.

> [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>
> ---



>  Documentation/git-remote.txt |  2 +-
>  builtin/remote.c             | 30 +++++++++++++++++++++++++-----
>  t/t5505-remote.sh            |  4 +++-
>  3 files changed, 29 insertions(+), 7 deletions(-)
>
> diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt
> index 2bebc32566..cde9614e36 100644
> --- a/Documentation/git-remote.txt
> +++ b/Documentation/git-remote.txt
> @@ -11,7 +11,7 @@ SYNOPSIS
>  [verse]
>  'git remote' [-v | --verbose]
>  'git remote add' [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
> -'git remote rename' <old> <new>
> +'git remote rename' [--[no-]progress] <old> <new>
>  'git remote remove' <name>
>  'git remote set-head' <name> (-a | --auto | -d | --delete | <branch>)
>  'git remote set-branches' [--add] <name> <branch>...

Do we already have an entry for the --progress option in the
description part of the documentation?  I think the way progress
works in the context of this command is pretty much bog-standard
one that we may not have to, once the user has seen how progress
options work elsewhere.

If not, then we'd want something like this squashed in, perhaps?

 Documentation/git-remote.txt | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git c/Documentation/git-remote.txt w/Documentation/git-remote.txt
index cde9614e36..e04810ee70 100644
--- c/Documentation/git-remote.txt
+++ w/Documentation/git-remote.txt
@@ -83,7 +83,10 @@ will always behave as if `--mirror` was passed.
 'rename'::
 
 Rename the remote named <old> to <new>. All remote-tracking branches and
-configuration settings for the remote are updated.
+configuration settings for the remote are updated.  When used interactively
+(i.e. the standard error stream is connected to a terminal),
+a progress bar may be shown while remote-tracking branches are renamed,
+which can be silenced by passing the `--no-progress` option.
 +
 In case <old> and <new> are the same, and <old> is a file under
 `$GIT_DIR/remotes` or `$GIT_DIR/branches`, the remote is converted to
Taylor Blau March 3, 2022, 11:30 p.m. UTC | #2
On Thu, Mar 03, 2022 at 03:20:48PM -0800, Junio C Hamano wrote:
> Taylor Blau <me@ttaylorr.com> writes:
>
> > 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.
>
> Is it still "when running verbosely"?
>
> I thought that tying this to --[no-]progress was the whole point of
> iterating another round.
>
> 	... when the standard error output is connected to a
> 	terminal, so that user knows Git is not completely stuck.

Ah, I glossed over this (stale) reference to the verbose option. I'm
almost willing to let it go, since it doesn't mention `--verbose`
directly, but happy to change it, too.

> > 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).
>
> That's a nice note to leave here, as it is a bit tricky to reason about.

Thanks, it's the sort of thing that I'd hope to find in a commit message
if I were confused about something.

> Do we already have an entry for the --progress option in the
> description part of the documentation?  I think the way progress
> works in the context of this command is pretty much bog-standard
> one that we may not have to, once the user has seen how progress
> options work elsewhere.
>
> If not, then we'd want something like this squashed in, perhaps?

I like the suggestion, but I agree it's probably not necessary since
this usage is standard. I thought I had written something explaining the
option explicitly in this section, but apparently dropped it when
preparing this patch. Ugh.

Thanks,
Taylor
Ævar Arnfjörð Bjarmason March 5, 2022, 2:31 p.m. UTC | #3
On Thu, Mar 03 2022, Taylor Blau wrote:

> diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt
> index 2bebc32566..cde9614e36 100644
> --- a/Documentation/git-remote.txt
> +++ b/Documentation/git-remote.txt
> @@ -11,7 +11,7 @@ SYNOPSIS
>  [verse]
>  'git remote' [-v | --verbose]
>  'git remote add' [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
> -'git remote rename' <old> <new>
> +'git remote rename' [--[no-]progress] <old> <new>
>  'git remote remove' <name>
>  'git remote set-head' <name> (-a | --auto | -d | --delete | <branch>)
>  'git remote set-branches' [--add] <name> <branch>...

Thanks, this looks much better.

But now that we don't piggy-back on --verbose (which as noted, would
have needed to be reworded if we still did) we should add a
--no-progress, --progress to the OPTIONS section, see e.g.:

    git grep '^--.*progress::' -- Documentation/

> @@ -571,6 +572,7 @@ struct rename_info {
>  	const char *old_name;
>  	const char *new_name;
>  	struct string_list *remote_branches;
> +	uint32_t symrefs_nr;
>  };

I didn't notice this in previous iterations, but why the uint32_t over
say a size_t?

The string_list is "unsigned int" (but we should really use size_t
there), but there's some code in refs.c that thinks about number of refs
as size_t already...

>  
>  static int read_remote_branches(const char *refname,
> @@ -587,10 +589,12 @@ static int read_remote_branches(const char *refname,
>  		item = string_list_append(rename->remote_branches, refname);
>  		symref = resolve_ref_unsafe(refname, RESOLVE_REF_READING,
>  					    NULL, &flag);
> -		if (symref && (flag & REF_ISSYMREF))
> +		if (symref && (flag & REF_ISSYMREF)) {
>  			item->util = xstrdup(symref);
> -		else
> +			rename->symrefs_nr++;
> +		} else {
>  			item->util = NULL;
> +		}
>  	}
>  	strbuf_release(&buf);

Just FWIW this could also be, if you prefer to skip the brace additions:
	
	@@ -588,9 +590,10 @@ static int read_remote_branches(const char *refname,
	 		symref = resolve_ref_unsafe(refname, RESOLVE_REF_READING,
	 					    NULL, &flag);
	 		if (symref && (flag & REF_ISSYMREF))
	-			item->util = xstrdup(symref);
	+			rename->symrefs_nr++;
	 		else
	-			item->util = NULL;
	+			symref = NULL;
	+		item->util = xstrdup_or_null(symref);
	 	}
	 	strbuf_release(&buf);

> @@ -682,7 +688,8 @@ static int mv(int argc, const char **argv)
>  		old_remote_context = STRBUF_INIT;
>  	struct string_list remote_branches = STRING_LIST_INIT_DUP;
>  	struct rename_info rename;
> -	int i, refspec_updated = 0;
> +	int i, refs_renamed_nr = 0, refspec_updated = 0;

Another type mixup nit, refs_renamed_nr should be "size_t" (as above,
it's looping over the "unsigned int" string_list, but we can just use
size_t for future-proofing...)

> +	struct progress *progress = NULL;
>  
>  	argc = parse_options(argc, argv, NULL, options,
>  			     builtin_remote_rename_usage, 0);
> @@ -693,6 +700,7 @@ static int mv(int argc, const char **argv)
>  	rename.old_name = argv[0];
>  	rename.new_name = argv[1];
>  	rename.remote_branches = &remote_branches;
> +	rename.symrefs_nr = 0;
>  
>  	oldremote = remote_get(rename.old_name);
>  	if (!remote_is_configured(oldremote, 1)) {
> @@ -767,6 +775,14 @@ static int mv(int argc, const char **argv)
>  	 * the new symrefs.
>  	 */
>  	for_each_ref(read_remote_branches, &rename);
> +	if (show_progress) {
> +		/*
> +		 * Count symrefs twice, since "renaming" them is done by
> +		 * deleting and recreating them in two separate passes.
> +		 */

I didn't look this over all that carefully before, but is the count that
we'll get in rename.symrefs_nr ever different than in
resolve_ref_unsafe() in read_remote_branches()? If not that's an issue
that pre-exists here, i.e. why do we need to find out twice for each ref
it's a symref?

And in any case the "total" fed to start_progress() will be wrong since
in the two later loops we "continue" if "item->util" is true....

> +		progress = start_progress(_("Renaming remote references"),
> +					  rename.remote_branches->nr + rename.symrefs_nr);
> +	}
>  	for (i = 0; i < remote_branches.nr; i++) {
>  		struct string_list_item *item = remote_branches.items + i;
>  		int flag = 0;
> @@ -776,6 +792,7 @@ static int mv(int argc, const char **argv)
>  			continue;
>  		if (delete_ref(NULL, item->string, NULL, REF_NO_DEREF))
>  			die(_("deleting '%s' failed"), item->string);
> +		display_progress(progress, ++refs_renamed_nr);

...I think it makes sense to display_progress() at the start of the
loop, otherwise we expose ourselves to the progress bar stalling if
we're just looping over a bunch of stuff where we "continue", and it's
easier to reason about.
diff mbox series

Patch

diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt
index 2bebc32566..cde9614e36 100644
--- a/Documentation/git-remote.txt
+++ b/Documentation/git-remote.txt
@@ -11,7 +11,7 @@  SYNOPSIS
 [verse]
 'git remote' [-v | --verbose]
 'git remote add' [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
-'git remote rename' <old> <new>
+'git remote rename' [--[no-]progress] <old> <new>
 'git remote remove' <name>
 'git remote set-head' <name> (-a | --auto | -d | --delete | <branch>)
 'git remote set-branches' [--add] <name> <branch>...
diff --git a/builtin/remote.c b/builtin/remote.c
index 824fb8099c..950e7958d5 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -12,11 +12,12 @@ 
 #include "object-store.h"
 #include "strvec.h"
 #include "commit-reach.h"
+#include "progress.h"
 
 static const char * const builtin_remote_usage[] = {
 	"git remote [-v | --verbose]",
 	N_("git remote add [-t <branch>] [-m <master>] [-f] [--tags | --no-tags] [--mirror=<fetch|push>] <name> <url>"),
-	N_("git remote rename <old> <new>"),
+	N_("git remote rename [--[no-]progress] <old> <new>"),
 	N_("git remote remove <name>"),
 	N_("git remote set-head <name> (-a | --auto | -d | --delete | <branch>)"),
 	N_("git remote [-v | --verbose] show [-n] <name>"),
@@ -36,7 +37,7 @@  static const char * const builtin_remote_add_usage[] = {
 };
 
 static const char * const builtin_remote_rename_usage[] = {
-	N_("git remote rename <old> <new>"),
+	N_("git remote rename [--[no-]progress] <old> <new>"),
 	NULL
 };
 
@@ -571,6 +572,7 @@  struct rename_info {
 	const char *old_name;
 	const char *new_name;
 	struct string_list *remote_branches;
+	uint32_t symrefs_nr;
 };
 
 static int read_remote_branches(const char *refname,
@@ -587,10 +589,12 @@  static int read_remote_branches(const char *refname,
 		item = string_list_append(rename->remote_branches, refname);
 		symref = resolve_ref_unsafe(refname, RESOLVE_REF_READING,
 					    NULL, &flag);
-		if (symref && (flag & REF_ISSYMREF))
+		if (symref && (flag & REF_ISSYMREF)) {
 			item->util = xstrdup(symref);
-		else
+			rename->symrefs_nr++;
+		} else {
 			item->util = NULL;
+		}
 	}
 	strbuf_release(&buf);
 
@@ -674,7 +678,9 @@  static void handle_push_default(const char* old_name, const char* new_name)
 
 static int mv(int argc, const char **argv)
 {
+	int show_progress = isatty(2);
 	struct option options[] = {
+		OPT_BOOL(0, "progress", &show_progress, N_("force progress reporting")),
 		OPT_END()
 	};
 	struct remote *oldremote, *newremote;
@@ -682,7 +688,8 @@  static int mv(int argc, const char **argv)
 		old_remote_context = STRBUF_INIT;
 	struct string_list remote_branches = STRING_LIST_INIT_DUP;
 	struct rename_info rename;
-	int i, refspec_updated = 0;
+	int i, refs_renamed_nr = 0, refspec_updated = 0;
+	struct progress *progress = NULL;
 
 	argc = parse_options(argc, argv, NULL, options,
 			     builtin_remote_rename_usage, 0);
@@ -693,6 +700,7 @@  static int mv(int argc, const char **argv)
 	rename.old_name = argv[0];
 	rename.new_name = argv[1];
 	rename.remote_branches = &remote_branches;
+	rename.symrefs_nr = 0;
 
 	oldremote = remote_get(rename.old_name);
 	if (!remote_is_configured(oldremote, 1)) {
@@ -767,6 +775,14 @@  static int mv(int argc, const char **argv)
 	 * the new symrefs.
 	 */
 	for_each_ref(read_remote_branches, &rename);
+	if (show_progress) {
+		/*
+		 * Count symrefs twice, since "renaming" them is done by
+		 * deleting and recreating them in two separate passes.
+		 */
+		progress = start_progress(_("Renaming remote references"),
+					  rename.remote_branches->nr + rename.symrefs_nr);
+	}
 	for (i = 0; i < remote_branches.nr; i++) {
 		struct string_list_item *item = remote_branches.items + i;
 		int flag = 0;
@@ -776,6 +792,7 @@  static int mv(int argc, const char **argv)
 			continue;
 		if (delete_ref(NULL, item->string, NULL, REF_NO_DEREF))
 			die(_("deleting '%s' failed"), item->string);
+		display_progress(progress, ++refs_renamed_nr);
 	}
 	for (i = 0; i < remote_branches.nr; i++) {
 		struct string_list_item *item = remote_branches.items + i;
@@ -791,6 +808,7 @@  static int mv(int argc, const char **argv)
 				item->string, buf.buf);
 		if (rename_ref(item->string, buf.buf, buf2.buf))
 			die(_("renaming '%s' failed"), item->string);
+		display_progress(progress, ++refs_renamed_nr);
 	}
 	for (i = 0; i < remote_branches.nr; i++) {
 		struct string_list_item *item = remote_branches.items + i;
@@ -810,7 +828,9 @@  static int mv(int argc, const char **argv)
 				item->string, buf.buf);
 		if (create_symref(buf.buf, buf2.buf, buf3.buf))
 			die(_("creating '%s' failed"), buf.buf);
+		display_progress(progress, ++refs_renamed_nr);
 	}
+	stop_progress(&progress);
 	string_list_clear(&remote_branches, 1);
 
 	handle_push_default(rename.old_name, rename.new_name);
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 9ab315424c..c90cf47acd 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -753,7 +753,9 @@  test_expect_success 'rename a remote' '
 	(
 		cd four &&
 		git config branch.main.pushRemote origin &&
-		git remote rename origin upstream &&
+		GIT_TRACE2_EVENT=$(pwd)/trace \
+			git remote rename --progress origin upstream &&
+		test_region progress "Renaming remote references" trace &&
 		grep "pushRemote" .git/config &&
 		test -z "$(git for-each-ref refs/remotes/origin)" &&
 		test "$(git symbolic-ref refs/remotes/upstream/HEAD)" = "refs/remotes/upstream/main" &&