Skip to contentSkip to conversation

Make orphaned commit checks configurable

PATCH In review Wyatt Holden via GitGitGadget 3 participants 9 messages Nov 10 Documentation

lore
Share & tools
lore
#0 lore raw

cover Make orphaned commit checks configurable

Hi there - my name's Wyatt Holden and I'm a Software Engineer at Microsoft :) I've been working with Derrick Stolee over the past few weeks on optimizing git in the Office mono-repo. We found some issues around slow checkouts on mono-repos in an automated environment:

When leaving a detached HEAD, git checkout and git switch
call orphaned_commit_warning() to warn about commits that are not reachable from any existing ref or the destination HEAD. This requires enumerating refs and walking commit history, which can be expensive in large repositories.

In an internal mono-repo, Trace2 showed tree unpacking finishing approximately 12 seconds into checkout, but the process continuing for roughly another 193 seconds. A stack dump captured the main thread inside orphaned_commit_warning(), enumerating loose refs and opening ref files. Running checkout with --quiet, which bypasses this routine, reduced the observed runtime to approximately 14 seconds. However, --quiet also suppresses unrelated output.

This three-commit series adds a targeted opt-out, advertises it when the check is slow, and enables it in Scalar:

1. checkout: make orphaned commit checks configurable Add checkout.orphanedCommitWarning , enabled by default, for both checkout and switch. Setting it to  false  skips the reachability check while retaining the previous HEAD description and normal checkout output. Add a Trace2 region to expose the check’s duration and tests verifying that disabling the setting bypasses the routine.
2. checkout: advise when orphaned commit checks are slow When the check takes more than two seconds, suggest the opt-out and explain that disabling it also removes the warning about commits left behind. Control this advice independently through advice.checkoutOrphanedCommitWarning. Test advice and suppression without relying on wall-clock delays.
3. scalar: disable orphaned commit checks by default Add checkout.orphanedCommitWarning=false to Scalar’s recommended configuration using its existing set-if-missing mechanism, preserving explicit user settings during clone, register, and reconfigure. Document the tradeoff and reflog recovery; rely on existing Scalar tests for configuration-preservation behavior.

Ordinary Git retains its existing default behavior. Users who disable the check lose the immediate warning about commits left behind; those commits can still be recovered through the HEAD reflog while the relevant entries and objects remain available.

Wyatt Holden (3):
checkout: make orphaned commit checks configurable
checkout: advise when orphaned commit checks are slow
scalar: disable orphaned commit checks by default

 Documentation/config/advice.adoc   |  6 +++
 Documentation/config/checkout.adoc | 11 +++++
 Documentation/scalar.adoc          |  7 +++
 advice.c                           |  1 +
 advice.h                           |  1 +
 builtin/checkout.c                 | 31 +++++++++++++-
 scalar.c                           |  1 +
 t/README                           |  5 +++
 t/t2020-checkout-detach.sh         | 69 ++++++++++++++++++++++++++++++
 9 files changed, 130 insertions(+), 2 deletions(-)

base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2264%2Fwyatt-holden%2Fwyattholden%2Fcheckout-orphaned-commit-warning-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2264/wyatt-holden/wyattholden/checkout-orphaned-commit-warning-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2264

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/3 checkout: make orphaned commit checks configurable

From: Wyatt Holden <[email protected]>

When leaving a detached HEAD, checkout enumerates refs and walks history to warn about commits left behind. This work can dominate checkout time even after the working tree has been updated.

In an automated checkout of an internal monorepo, unpack_trees ended about 12 seconds into a roughly 204-second run. Stack dumps captured the main thread in orphaned_commit_warning(), enumerating loose refs and opening ref files. Using --quiet reduced the observed checkout time to about 14 seconds, but also suppressed unrelated output. These observations motivate an opt-out; they do not establish how much of the delay came from enumeration versus history traversal.

Add checkout.orphanedCommitWarning, enabled by default, so checkout and switch can skip the check while retaining normal checkout output. Automated callers can opt out when they do not need a warning about leaving detached commits behind.

Follow the configuration precedent of 06b324c1d71 (status: add status.aheadbehind setting, 2019-06-18). Unlike status, introduce no dedicated command-line option; callers can use git -c for a one-off override.

Add a Trace2 region around the check to make its cost visible in future investigations and to verify that disabling the setting skips the routine rather than merely hiding its warning. Document the setting and test both checkout and switch.

---
 Documentation/config/checkout.adoc | 11 +++++++++++
 builtin/checkout.c                 | 18 ++++++++++++++++--
 t/t2020-checkout-detach.sh         | 29 +++++++++++++++++++++++++++++
 3 files changed, 56 insertions(+), 2 deletions(-)
Documentation/config/checkout.adoc +11 -0
index e35d212969..cacd10449a 100644
@@ -22,6 +22,17 @@ commands or functionality in the future.
 	option in `git checkout` and `git switch`. See
 	linkgit:git-switch[1] and linkgit:git-checkout[1].
 
+`checkout.orphanedCommitWarning`::
+	When leaving a detached `HEAD`, linkgit:git-checkout[1] and
+	linkgit:git-switch[1] check whether any commits are being left
+	behind that are not reachable from any ref or the new `HEAD`,
+	and warn if so. This check can be expensive in large repositories.
+	Set this variable to `false` to skip the check and its warning
+	without suppressing other checkout output. Defaults to `true`.
++
+When disabled, commits left behind can still be found in the `HEAD`
+reflog until its entries expire. See linkgit:git-reflog[1].
+
 `checkout.workers`::
 	The number of parallel workers to use when updating the working tree.
 	The default is one, i.e. sequential execution. If set to a value less
builtin/checkout.c +16 -2
index c0f0d2c700..7cc9e97612 100644
@@ -51,6 +51,7 @@ struct checkout_opts {
 	int patch_interhunk_context;
 	int auto_advance;
 	int quiet;
+	int orphaned_commit_warning;
 	int merge;
 	int force;
 	int force_detach;
@@ -99,6 +100,7 @@ struct checkout_opts {
 	.patch_context = -1, \
 	.patch_interhunk_context = -1, \
 	.auto_advance = 1, \
+	.orphaned_commit_warning = 1, \
 }
 
 #define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
@@ -1132,6 +1134,7 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
 	struct rev_info revs;
 	struct object *object = &old_commit->object;
 
+	trace2_region_enter("checkout", "orphaned_commit_warning", the_repository);
 	repo_init_revisions(the_repository, &revs, NULL);
 	setup_revisions(0, NULL, &revs, NULL);
 
@@ -1155,6 +1158,7 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
 	/* Clean up objects used, as they will be reused. */
 	repo_clear_commit_marks(the_repository, ALL_REV_FLAGS);
 	release_revisions(&revs);
+	trace2_region_leave("checkout", "orphaned_commit_warning", the_repository);
 }
 
 static int switch_branches(const struct checkout_opts *opts,
@@ -1253,8 +1257,14 @@ static int switch_branches(const struct checkout_opts *opts,
 		}
 	}
 
-	if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)
-		orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);
+	if (!opts->quiet && !old_branch_info.path && old_branch_info.commit &&
+	    new_branch_info->commit != old_branch_info.commit) {
+		if (opts->orphaned_commit_warning)
+			orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);
+		else
+			describe_detached_head(_("Previous HEAD position was"),
+					       old_branch_info.commit);
+	}
 
 	if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)
 		fputc('\n', stderr);
@@ -1295,6 +1305,10 @@ static int git_checkout_config(const char *var, const char *value,
 		opts->dwim_new_local_branch = git_config_bool(var, value);
 		return 0;
 	}
+	if (!strcmp(var, "checkout.orphanedcommitwarning")) {
+		opts->orphaned_commit_warning = git_config_bool(var, value);
+		return 0;
+	}
 
 	if (starts_with(var, "submodule."))
 		return git_default_submodule_config(var, value, NULL);
t/t2020-checkout-detach.sh +29 -0
index 28bbbe6c05..d21dbd5b72 100755
@@ -164,6 +164,35 @@ test_expect_success 'checkout does not warn leaving reachable commit' '
 	check_no_orphan_warning stderr
 '
 
+for command in checkout switch
+do
+	test_expect_success "$command honors checkout.orphanedCommitWarning" '
+		git checkout --detach "$orphan2" &&
+		GIT_TRACE2_EVENT="$(pwd)/trace-enabled-$command" \
+			git -c checkout.orphanedCommitWarning=true \
+			$command main 2>stderr &&
+		check_orphan_warning stderr "2 commits" &&
+		test_grep "\"label\":\"orphaned_commit_warning\"" \
+			trace-enabled-$command &&
+
+		git checkout --detach "$orphan2" &&
+		GIT_TRACE2_EVENT="$(pwd)/trace-disabled-$command" \
+			git -c checkout.orphanedCommitWarning=false \
+			$command main 2>stderr &&
+		check_no_orphan_warning stderr &&
+		test_grep "Switched to branch" stderr &&
+		test_grep ! "\"label\":\"orphaned_commit_warning\"" \
+			trace-disabled-$command &&
+		check_not_detached
+	'
+
+	test_expect_success "$command rejects invalid checkout.orphanedCommitWarning" '
+		test_must_fail git -c checkout.orphanedCommitWarning=invalid \
+			$command main 2>stderr &&
+		test_grep "bad boolean config value.*checkout.orphanedcommitwarning" stderr
+	'
+done
+
 cat >expect <<'EOF'
 Your branch is behind 'main' by 1 commit, and can be fast-forwarded.
   (use "git pull" to update your local branch)
Signature · 1 line
gitgitgadget
#3 lore raw

PATCH 2/3 checkout: advise when orphaned commit checks are slow

In reply to Wyatt Holden via GitGitGadget

From: Wyatt Holden <[email protected]>

The orphaned-commit check can take much longer than updating the working tree. Users who encounter this cost need a way to discover the checkout.orphanedCommitWarning setting.

Measure the complete check and suggest disabling it when it takes more than two seconds. Explain that opting out also removes the warning about commits left behind, and allow the advice itself to be disabled with advice.checkoutOrphanedCommitWarning.

Follow the slow-operation advice introduced by:

0a53561a622 (status: warn when a/b calculation takes too long, 2019-06-18)

Unlike that status advice, recommend a configuration setting rather than a dedicated command-line option.

Keep a test hook to exercise advice and suppression without relying on wall-clock delays. Force the advice condition without replacing the measured duration, and check the formatted duration rather than a fabricated fixed value.

---
 Documentation/config/advice.adoc |  6 +++++
 advice.c                         |  1 +
 advice.h                         |  1 +
 builtin/checkout.c               | 13 +++++++++++
 t/README                         |  5 ++++
 t/t2020-checkout-detach.sh       | 40 ++++++++++++++++++++++++++++++++
 6 files changed, 66 insertions(+)
Documentation/config/advice.adoc +6 -0
index 3d91c90eda..eecf72d8d1 100644
@@ -38,6 +38,12 @@ all advice messages.
 		configuration variable for how to set a given remote
 		to be used by default in some situations where this
 		advice would be printed.
+	checkoutOrphanedCommitWarning::
+		Shown when linkgit:git-checkout[1] or linkgit:git-switch[1]
+		takes more than 2 seconds to check for commits left behind
+		when leaving a detached `HEAD`. Suggests setting
+		`checkout.orphanedCommitWarning` to `false` to skip the check.
+		Disabling this advice does not disable the check itself.
 	commitBeforeMerge::
 		Shown when linkgit:git-merge[1] refuses to
 		merge to avoid overwriting local changes.
advice.c +1 -0
index 401d047391..8142d73b5e 100644
@@ -50,6 +50,7 @@ static struct {
 	[ADVICE_AMBIGUOUS_FETCH_REFSPEC]		= { "ambiguousFetchRefspec" },
 	[ADVICE_AM_WORK_DIR] 				= { "amWorkDir" },
 	[ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME] 	= { "checkoutAmbiguousRemoteBranchName" },
+	[ADVICE_CHECKOUT_ORPHANED_COMMIT_WARNING]		= { "checkoutOrphanedCommitWarning" },
 	[ADVICE_COMMIT_BEFORE_MERGE]			= { "commitBeforeMerge" },
 	[ADVICE_DEFAULT_BRANCH_NAME]			= { "defaultBranchName" },
 	[ADVICE_DETACHED_HEAD]				= { "detachedHead" },
advice.h +1 -0
index 3f0b4f0485..7dd1fe109c 100644
@@ -17,6 +17,7 @@ enum advice_type {
 	ADVICE_AMBIGUOUS_FETCH_REFSPEC,
 	ADVICE_AM_WORK_DIR,
 	ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME,
+	ADVICE_CHECKOUT_ORPHANED_COMMIT_WARNING,
 	ADVICE_COMMIT_BEFORE_MERGE,
 	ADVICE_DEFAULT_BRANCH_NAME, /* To be retired sometime after Git 3.0 */
 	ADVICE_DETACHED_HEAD,
builtin/checkout.c +13 -0
index 7cc9e97612..1e94280d35 100644
@@ -35,6 +35,7 @@
 #include "strvec.h"
 #include "submodule.h"
 #include "symlinks.h"
+#include "trace.h"
 #include "trace2.h"
 #include "tree.h"
 #include "tree-walk.h"
@@ -1133,6 +1134,8 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
 {
 	struct rev_info revs;
 	struct object *object = &old_commit->object;
+	uint64_t t_begin = getnanotime();
+	uint64_t t_delta_in_ms;
 
 	trace2_region_enter("checkout", "orphaned_commit_warning", the_repository);
 	repo_init_revisions(the_repository, &revs, NULL);
@@ -1159,6 +1162,16 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
 	repo_clear_commit_marks(the_repository, ALL_REV_FLAGS);
 	release_revisions(&revs);
 	trace2_region_leave("checkout", "orphaned_commit_warning", the_repository);
+
+	t_delta_in_ms = (getnanotime() - t_begin) / 1000000;
+	if (t_delta_in_ms > 2000 ||
+	    git_env_bool("GIT_TEST_CHECKOUT_DELAY_WARNING", 0))
+		advise_if_enabled(ADVICE_CHECKOUT_ORPHANED_COMMIT_WARNING,
+				  _("Checking for orphaned commits took %.2f seconds.\n"
+				    "You can use 'git config set checkout.orphanedCommitWarning false'\n"
+				    "to skip this check, but you will no longer be warned when leaving\n"
+				    "commits behind that are not connected to any of your branches."),
+				  t_delta_in_ms / 1000.0);
 }
 
 static int switch_branches(const struct checkout_opts *opts,
t/README +5 -0
index fd7e8504b5..577f02732e 100644
@@ -371,6 +371,11 @@ test suite. Accept any boolean values that are accepted by git-config.
 GIT_TEST_PROTOCOL_VERSION=<n>, when set, makes 'protocol.version'
 default to n.
 
+GIT_TEST_CHECKOUT_DELAY_WARNING=<boolean>, when true, forces the
+slow orphaned-commit check advice to be considered regardless of the
+elapsed time. It does not change the measured duration or override
+advice settings.
+
 GIT_TEST_FULL_IN_PACK_ARRAY=<boolean> exercises the uncommon
 pack-objects code path where there are more than 1024 packs even if
 the actual number of packs in repository is below this limit. Accept
t/t2020-checkout-detach.sh +40 -0
index d21dbd5b72..7dfaaab1b8 100755
@@ -186,6 +186,46 @@ do
 		check_not_detached
 	'
 
+	test_expect_success "$command advises when the orphan check is slow" '
+		git checkout --detach "$orphan2" &&
+		GIT_TEST_CHECKOUT_DELAY_WARNING=true \
+			git $command main 2>stderr &&
+		check_orphan_warning stderr "2 commits" &&
+		test_grep -E "^hint: Checking for orphaned commits took [0-9]+[.][0-9]{2} seconds[.]$" stderr &&
+		test_grep "git config set checkout.orphanedCommitWarning false" stderr &&
+		test_grep "git config set advice.checkoutOrphanedCommitWarning false" stderr
+	'
+
+	test_expect_success "$command can suppress slow orphan check advice" '
+		git checkout --detach "$orphan2" &&
+		GIT_TEST_CHECKOUT_DELAY_WARNING=true \
+			git -c advice.checkoutOrphanedCommitWarning=false \
+			$command main 2>stderr &&
+		check_orphan_warning stderr "2 commits" &&
+		test_grep ! "^hint:" stderr &&
+
+		git checkout --detach "$orphan2" &&
+		GIT_TEST_CHECKOUT_DELAY_WARNING=true GIT_ADVICE=0 \
+			git $command main 2>stderr &&
+		check_orphan_warning stderr "2 commits" &&
+		test_grep ! "^hint:" stderr
+	'
+
+	test_expect_success "$command does not advise when the orphan check is skipped" '
+		git checkout --detach "$orphan2" &&
+		GIT_TEST_CHECKOUT_DELAY_WARNING=true \
+			git -c checkout.orphanedCommitWarning=false \
+			$command main 2>stderr &&
+		check_no_orphan_warning stderr &&
+		test_grep ! "^hint:" stderr &&
+
+		git checkout --detach "$orphan2" &&
+		GIT_TEST_CHECKOUT_DELAY_WARNING=true \
+			git $command --quiet main 2>stderr &&
+		test_must_be_empty stderr &&
+		check_not_detached
+	'
+
 	test_expect_success "$command rejects invalid checkout.orphanedCommitWarning" '
 		test_must_fail git -c checkout.orphanedCommitWarning=invalid \
 			$command main 2>stderr &&
Signature · 1 line
gitgitgadget
#6 lore raw

PATCH 3/3 scalar: disable orphaned commit checks by default

In reply to Wyatt Holden via GitGitGadget

From: Wyatt Holden <[email protected]>

Checking for commits left behind when leaving a detached HEAD can be expensive in large repositories. Add checkout.orphanedCommitWarning=false to Scalar's recommended settings to avoid this foreground work while preserving normal checkout output.

Use the existing set-if-missing behavior so explicit user choices are preserved during clone, register, and reconfigure. Document the loss of the immediate orphan warning and the availability of reflog recovery. Existing Scalar tests cover configuration preservation, so no additional inventory of recommended settings is needed.

---
 Documentation/scalar.adoc | 7 +++++++
 scalar.c                  | 1 +
 2 files changed, 8 insertions(+)
Documentation/scalar.adoc +7 -0
index 5252fb134a..d2b8c9a6ca 100644
@@ -214,6 +214,13 @@ am.keepCR=true::
 	and non-Windows platforms and keeping carriage return (`\r`) characters
 	in certain workflows.
 
+checkout.orphanedCommitWarning=false::
+	This disables the reachability check that warns about commits left
+	behind when `git checkout` or `git switch` leaves a detached `HEAD`.
+	The check can be expensive in large repositories. Other checkout
+	output is unchanged, and commits left behind can still be found
+	in the `HEAD` reflog until its entries expire.
+
 commitGraph.changedPaths=true::
 	This setting helps the background maintenance steps that compute the
 	serialized commit-graph to also store changed-path Bloom filters. This
scalar.c +1 -0
index a80d8ee3ff..7f851f5610 100644
@@ -138,6 +138,7 @@ static int set_recommended_config(int reconfigure)
 	 */
 	struct scalar_config config[] = {
 		{ "am.keepCR", "true" },
+		{ "checkout.orphanedCommitWarning", "false" },
 		{ "commitGraph.changedPaths", "true" },
 		{ "commitGraph.generationVersion", "1" },
 		{ "core.autoCRLF", "false" },
Signature · 1 line
gitgitgadget
#4 lore raw

Re: [PATCH 2/3] checkout: advise when orphaned commit checks are slow

In reply to Wyatt Holden via GitGitGadget

advice.c static struct { · line 54

Quoted 3 lines · Wyatt Holden via GitGitGadget
advice.c +1 -0
 	[ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME] 	= { "checkoutAmbiguousRemoteBranchName" },
+	[ADVICE_CHECKOUT_ORPHANED_COMMIT_WARNING]		= { "checkoutOrphanedCommitWarning" },
 	[ADVICE_COMMIT_BEFORE_MERGE]			= { "commitBeforeMerge" },

It looks like we have a slight whitespace alignment problem here.

Thanks,
-Stolee

#7 lore raw
In reply to Wyatt Holden via GitGitGadget
Quoted 4 lines · Wyatt Holden via GitGitGadget

Hi there - my name's Wyatt Holden and I'm a Software Engineer at Microsoft :) I've been working with Derrick Stolee over the past few weeks on optimizing git in the Office mono-repo. We found some issues around slow checkouts on mono-repos in an automated environment:

Welcome, Wyatt! (I was prepared for your series.)

Quoted 6 lines

When leaving a detached HEAD, git checkout and git switch
call orphaned_commit_warning() to warn about commits that are not reachable from any existing ref or the destination HEAD. This requires enumerating refs and walking commit history, which can be expensive in large repositories.

Quoted 7 lines

In an internal mono-repo, Trace2 showed tree unpacking finishing approximately 12 seconds into checkout, but the process continuing for roughly another 193 seconds. A stack dump captured the main thread inside orphaned_commit_warning(), enumerating loose refs and opening ref files. Running checkout with --quiet, which bypasses this routine, reduced the observed runtime to approximately 14 seconds. However, --quiet also suppresses unrelated output.

This example shows a significant pain caused by this check, but we also want to be able to see progress indicators over time in our logs, so using --quiet isn't preferred, long term.

Quoted 2 lines

This three-commit series adds a targeted opt-out, advertises it when the check is slow, and enables it in Scalar:

I did some pre-review with Wyatt (but found one nitpick in my on-list re-review) so I'm biased. Further bias: I recommended that he follow the model of the status.aheadBehind config [1] and advice.statusAheadBehind
[2] patches as prior art.

[1] https://lore.kernel.org/git/2f0f2a2df9410947b985d83a924d2549d62bb65f.1560889284.git.gitgitgadget@gmail.com/

[2] https://lore.kernel.org/git/c1056545b617b89e881f5991beb2fe0d860de740.1560889284.git.gitgitgadget@gmail.com/

And also I added status.aheadBehind to Scalar's recommended config in its very first version [3]. Wyatt has updated the Scalar config docs that justify the recommendation, which was introduced only recently [4].

[3] https://lore.kernel.org/git/4439ab4de0bc3f48a6bdcf4b5165b16fad792ebd.1638538470.git.gitgitgadget@gmail.com/

[4] https://lore.kernel.org/git/f062b0e0773b9d168c031bb1f35538665394a077.1765552528.git.gitgitgadget@gmail.com/

So I give my support publicly for Wyatt's updates.

Thanks,
-Stolee

#2 lore raw

Re: [PATCH 1/3] checkout: make orphaned commit checks configurable

In reply to Wyatt Holden via GitGitGadget
Quoted 5 lines · Wyatt Holden via GitGitGadget

From: Wyatt Holden <[email protected]>

When leaving a detached HEAD, checkout enumerates refs and walks history to warn about commits left behind. This work can dominate checkout time even after the working tree has been updated.

Interesting.

I often am on a detached HEAD but never realized this particular check was expensive. The size of changes between the HEAD and the next branch would affect the time it needs to update the working tree (the larger the difference, the more expensive to complete the branch switching). What factors are involved in making this "orphaned commits" check more expensive? The total number of branches? The length of commit chain we are leaving behind? I am trying to see if this is something we can aim to optimize or if it is a lost cause and a cop-out to introduce an option to disable the check altogether is the best we can do.

Quoted 7 lines

In an automated checkout of an internal monorepo, unpack_trees ended about 12 seconds into a roughly 204-second run. Stack dumps captured the main thread in orphaned_commit_warning(), enumerating loose refs and opening ref files. Using --quiet reduced the observed checkout time to about 14 seconds, but also suppressed unrelated output. These observations motivate an opt-out; they do not establish how much of the delay came from enumeration versus history traversal.

So, in short, the answer to my earlier question is "we don't know".

Quoted 4 lines

Add checkout.orphanedCommitWarning, enabled by default, so checkout and switch can skip the check while retaining normal checkout output. Automated callers can opt out when they do not need a warning about leaving detached commits behind.

Sounds sensible.

Quoted 4 lines

Follow the configuration precedent of 06b324c1d71 (status: add status.aheadbehind setting, 2019-06-18). Unlike status, introduce no dedicated command-line option; callers can use git -c for a one-off override.

I am not sure how justifiable the design decision to leave the command line option out is.

The automation can run with "-q" to omit the overhead altogether, as well as squelching any other output the automation would not read anyway, so I would not worry too much about them.

But for human users, what are they losing by passing "-q" when they do not want to pay the cost of this check because we are not giving them an easy to use command line option? What other output are they declining by "--quiet"? "The following paths have local changes" list? Anything else?

Quoted 4 lines

Add a Trace2 region around the check to make its cost visible in future investigations and to verify that disabling the setting skips the routine rather than merely hiding its warning. Document the setting and test both checkout and switch.

Sounds sensible.

Quoted 9 lines
builtin/checkout.c +1 -0
@@ -1132,6 +1134,7 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
 	struct rev_info revs;
 	struct object *object = &old_commit->object;
 
+	trace2_region_enter("checkout", "orphaned_commit_warning", the_repository);
 	repo_init_revisions(the_repository, &revs, NULL);
 	setup_revisions(0, NULL, &revs, NULL);
 

builtin/checkout.c static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne · line 1162

Quoted 5 lines
builtin/checkout.c +1 -0
@@ -1155,6 +1158,7 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
 	/* Clean up objects used, as they will be reused. */
 	repo_clear_commit_marks(the_repository, ALL_REV_FLAGS);
 	release_revisions(&revs);
+	trace2_region_leave("checkout", "orphaned_commit_warning", the_repository);
 }

Quite straightforward. There is no early return in the middle, so entering at the beginning with leaving at the end is the only change necessary.

Quoted 4 lines
builtin/checkout.c +0 -0
@@ -1253,8 +1257,14 @@ static int switch_branches(const struct checkout_opts *opts,
 		}
 	}
 

builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1267

Quoted 10 lines
builtin/checkout.c +8 -2
-	if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)
-		orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);
+	if (!opts->quiet && !old_branch_info.path && old_branch_info.commit &&
+	    new_branch_info->commit != old_branch_info.commit) {
+		if (opts->orphaned_commit_warning)
+			orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);
+		else
+			describe_detached_head(_("Previous HEAD position was"),
+					       old_branch_info.commit);
+	}

OK. The call into suggest_reattach() via orphaned_commit_warning() is the more expensive part, and it also is where "Previous HEAD was" comes, which we do not want to lose. It makes sense.

Another thing I noticed is that the loop in suggest_reattach() stops showing each commit after showing ORPHAN_CUTOFF but keeps counting. I wonder if it becomes palatable to introduce a configuration to stop it early, instead of disabling the orphaned_commit_warning() altogether?

#5 lore raw

Re: [PATCH 2/3] checkout: advise when orphaned commit checks are slow

In reply to Derrick Stolee
Quoted 10 lines · Derrick Stolee

advice.c static struct { · line 54

Quoted 3 lines · Wyatt Holden via GitGitGadget
advice.c +1 -0
 	[ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME] 	= { "checkoutAmbiguousRemoteBranchName" },
+	[ADVICE_CHECKOUT_ORPHANED_COMMIT_WARNING]		= { "checkoutOrphanedCommitWarning" },
 	[ADVICE_COMMIT_BEFORE_MERGE]			= { "commitBeforeMerge" },

It looks like we have a slight whitespace alignment problem here.

Thanks,
-Stolee

Good eyes. What the patch aims does sound sensible to me, though.

#8 lore raw
In reply to Wyatt Holden via GitGitGadget
Quoted 5 lines · Wyatt Holden via GitGitGadget

When leaving a detached HEAD, git checkout and git switch
call orphaned_commit_warning() to warn about commits that are not reachable from any existing ref or the destination HEAD. This requires enumerating refs and walking commit history, which can be expensive in large repositories.

I found some design choices made in 1/3 questionable but overall the series is coherently done and very easy to understand _why_ it wants to do what it does. Nice work.