diff --git a/Documentation/config/checkout.txt b/Documentation/config/checkout.txt index 6b646813abadc9..d6872ffa83ea5a 100644 --- a/Documentation/config/checkout.txt +++ b/Documentation/config/checkout.txt @@ -16,3 +16,11 @@ will checkout the '' branch on another remote, and by linkgit:git-worktree[1] when 'git worktree add' refers to a remote branch. This setting might be used for other checkout-like commands or functionality in the future. + +checkout.optimizeNewBranch:: + Optimizes the performance of "git checkout -b " when + using sparse-checkout. When set to true, git will not update the + repo based on the current sparse-checkout settings. This means it + will not update the skip-worktree bit in the index nor add/remove + files in the working directory to reflect the current sparse checkout + settings nor will it show the local changes. diff --git a/builtin/checkout.c b/builtin/checkout.c index 6123f732a2c84f..32c8edccaf986f 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -27,6 +27,8 @@ #include "wt-status.h" #include "xdiff-interface.h" +static int checkout_optimize_new_branch; + static const char * const checkout_usage[] = { N_("git checkout [] "), N_("git checkout [] [] -- ..."), @@ -71,6 +73,11 @@ struct checkout_opts { const char *ignore_unmerged_opt; int ignore_unmerged; + /* + * If new checkout options are added, skip_merge_working_tree + * should be updated accordingly. + */ + const char *new_branch; const char *new_branch_force; const char *new_orphan_branch; @@ -637,6 +644,112 @@ static void setup_branch_path(struct branch_info *branch) branch->path = strbuf_detach(&buf, NULL); } +/* + * Skip merging the trees, updating the index and working directory if and + * only if we are creating a new branch via "git checkout -b ." + */ +static int skip_merge_working_tree(const struct checkout_opts *opts, + const struct branch_info *old_branch_info, + const struct branch_info *new_branch_info) +{ + /* + * Do the merge if sparse checkout is on and the user has not opted in + * to the optimized behavior + */ + if (core_apply_sparse_checkout && !checkout_optimize_new_branch) + return 0; + + /* + * We must do the merge if we are actually moving to a new commit. + */ + if (!old_branch_info->commit || !new_branch_info->commit || + !oideq(&old_branch_info->commit->object.oid, + &new_branch_info->commit->object.oid)) + return 0; + + /* + * opts->patch_mode cannot be used with switching branches so is + * not tested here + */ + + /* + * opts->quiet only impacts output so doesn't require a merge + */ + + /* + * Honor the explicit request for a three-way merge or to throw away + * local changes + */ + if (opts->merge || opts->force) + return 0; + + /* + * --detach is documented as "updating the index and the files in the + * working tree" but this optimization skips those steps so fall through + * to the regular code path. + */ + if (opts->force_detach) + return 0; + + /* + * opts->writeout_stage cannot be used with switching branches so is + * not tested here + */ + + /* + * Honor the explicit ignore requests + */ + if (!opts->overwrite_ignore || opts->ignore_skipworktree || + opts->ignore_other_worktrees) + return 0; + + /* + * opts->show_progress only impacts output so doesn't require a merge + */ + + /* + * opts->overlay_mode cannot be used with switching branches so is + * not tested here + */ + + /* + * If we aren't creating a new branch any changes or updates will + * happen in the existing branch. Since that could only be updating + * the index and working directory, we don't want to skip those steps + * or we've defeated any purpose in running the command. + */ + if (!opts->new_branch) + return 0; + + /* + * new_branch_force is defined to "create/reset and checkout a branch" + * so needs to go through the merge to do the reset + */ + if (opts->new_branch_force) + return 0; + + /* + * A new orphaned branch requrires the index and the working tree to be + * adjusted to + */ + if (opts->new_orphan_branch) + return 0; + + /* + * Remaining variables are not checkout options but used to track state + */ + + /* + * Do the merge if this is the initial checkout. We cannot use + * is_cache_unborn() here because the index hasn't been loaded yet + * so cache_nr and timestamp.sec are always zero. + */ + if (!file_exists(get_index_file())) + return 0; + + return 1; +} + static int merge_working_tree(const struct checkout_opts *opts, struct branch_info *old_branch_info, struct branch_info *new_branch_info, @@ -1039,7 +1152,6 @@ static int switch_branches(const struct checkout_opts *opts, BUG("'switch --orphan' should never accept a commit as starting point"); new_branch_info->commit = NULL; new_branch_info->name = "(empty)"; - do_merge = 1; } if (!new_branch_info->name) { @@ -1053,7 +1165,16 @@ static int switch_branches(const struct checkout_opts *opts, do_merge = 0; } - if (do_merge) { + /* optimize the "checkout -b path */ + if (!do_merge || skip_merge_working_tree(opts, &old_branch_info, new_branch_info)) { + if (!checkout_optimize_new_branch && !opts->quiet) { + if (read_cache_preload(NULL) < 0) + return error(_("index file corrupt")); + show_local_changes(&new_branch_info->commit->object, &opts->diff_options); + } + if (!opts->only_merge_on_switching_branches) + warning(_("'git checkout -b ' is being deprecated in favor of 'git switch -c '. Please adjust your workflow accordingly.")); + } else { ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error); if (ret) { free(path_to_free); @@ -1073,6 +1194,11 @@ static int switch_branches(const struct checkout_opts *opts, static int git_checkout_config(const char *var, const char *value, void *cb) { + if (!strcmp(var, "checkout.optimizenewbranch")) { + checkout_optimize_new_branch = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "diff.ignoresubmodules")) { struct checkout_opts *opts = cb; handle_ignore_submodules_arg(&opts->diff_options, value); diff --git a/t/t1090-sparse-checkout-scope.sh b/t/t1090-sparse-checkout-scope.sh index 40cc004326e2f0..090b7fc3d35d1a 100755 --- a/t/t1090-sparse-checkout-scope.sh +++ b/t/t1090-sparse-checkout-scope.sh @@ -31,6 +31,20 @@ test_expect_success 'perform sparse checkout of master' ' test_path_is_file c ' +test_expect_success 'checkout -b checkout.optimizeNewBranch interaction' ' + cp .git/info/sparse-checkout .git/info/sparse-checkout.bak && + test_when_finished " + mv -f .git/info/sparse-checkout.bak .git/info/sparse-checkout + git checkout master + " && + echo "/b" >>.git/info/sparse-checkout && + test "$(git ls-files -t b)" = "S b" && + git -c checkout.optimizeNewBranch=true checkout -b fast && + test "$(git ls-files -t b)" = "S b" && + git checkout -b slow && + test "$(git ls-files -t b)" = "H b" +' + test_expect_success 'merge feature branch into sparse checkout of master' ' git merge feature && test_path_is_file a && diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index c81ca360ac4ac9..35100cf9c47796 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1253,7 +1253,7 @@ do git cat-file commit $SHA1_2 && test_must_fail env GIT_TEST_PROTOCOL_VERSION= \ git fetch ../testrepo/.git $SHA1_3 2>err && - test_i18ngrep "remote error:.*not our ref.*$SHA1_3\$" err + test_i18ngrep "not our ref.*$SHA1_3" err ) ' done