diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc index 3d91c90eda72f9..eecf72d8d13080 100644 --- a/Documentation/config/advice.adoc +++ b/Documentation/config/advice.adoc @@ -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. diff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc index e35d21296978fe..cacd10449a20b0 100644 --- a/Documentation/config/checkout.adoc +++ b/Documentation/config/checkout.adoc @@ -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 diff --git a/Documentation/scalar.adoc b/Documentation/scalar.adoc index 5252fb134a47ab..d2b8c9a6ca66ea 100644 --- a/Documentation/scalar.adoc +++ b/Documentation/scalar.adoc @@ -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 diff --git a/advice.c b/advice.c index 401d0473917cd9..8142d73b5e9c70 100644 --- a/advice.c +++ b/advice.c @@ -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" }, diff --git a/advice.h b/advice.h index 3f0b4f0485c7fe..7dd1fe109c247b 100644 --- a/advice.h +++ b/advice.h @@ -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, diff --git a/builtin/checkout.c b/builtin/checkout.c index c0f0d2c700c447..1e94280d35264f 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -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" @@ -51,6 +52,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 +101,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) @@ -1131,7 +1134,10 @@ 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); setup_revisions(0, NULL, &revs, NULL); @@ -1155,6 +1161,17 @@ 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); + + 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, @@ -1253,8 +1270,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 +1318,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); diff --git a/scalar.c b/scalar.c index a80d8ee3ff54a7..7f851f56109fc3 100644 --- a/scalar.c +++ b/scalar.c @@ -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" }, diff --git a/t/README b/t/README index fd7e8504b511e2..577f02732e97e5 100644 --- a/t/README +++ b/t/README @@ -371,6 +371,11 @@ test suite. Accept any boolean values that are accepted by git-config. GIT_TEST_PROTOCOL_VERSION=, when set, makes 'protocol.version' default to n. +GIT_TEST_CHECKOUT_DELAY_WARNING=, 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= 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 diff --git a/t/t2020-checkout-detach.sh b/t/t2020-checkout-detach.sh index 28bbbe6c05547a..7dfaaab1b8b603 100755 --- a/t/t2020-checkout-detach.sh +++ b/t/t2020-checkout-detach.sh @@ -164,6 +164,75 @@ 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 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 && + 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)