checkout: optimize "git checkout -b <new_branch>"

Skip merging the commit, updating the index and working directory if and only if we are creating a new branch via "git checkout -b <new_branch>." Any other checkout options will still go through the former code path. If sparse_checkout is on, require the user to manually opt in to this optimzed behavior by setting the config setting checkout.optimizeNewBranch to true as we will no longer update the skip-worktree bit in the index, nor add/remove files in the working directory to reflect the current sparse checkout settings. For comparison, running "git checkout -b <new_branch>" on a large repo takes: 14.6 seconds - without this patch 0.3 seconds - with this patch Signed-off-by: Ben Peart <Ben.Peart@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ben Peart committed Aug 16, 2018 at 18:27 UTC fa655d8411cc2d7ffcf898e53a1493c737d7de68
3 files changed +138 -4
Documentation/config.txt
+8
@@ -1101,6 +1101,14 @@ browser.<tool>.path::
1101 browse HTML help (see `-w` option in linkgit:git-help[1]) or a
1102 working repository in gitweb (see linkgit:git-instaweb[1]).
1103
1104 +checkout.optimizeNewBranch
1105 + Optimizes the performance of "git checkout -b <new_branch>" when
1106 + using sparse-checkout. When set to true, git will not update the
1107 + repo based on the current sparse-checkout settings. This means it
1108 + will not update the skip-worktree bit in the index nor add/remove
1109 + files in the working directory to reflect the current sparse checkout
1110 + settings nor will it show the local changes.
1111 +
1112 clean.requireForce::
1113 A boolean to make git-clean do nothing unless given -f,
1114 -i or -n. Defaults to true.
builtin/checkout.c
+116 -4
@@ -24,6 +24,8 @@
24 #include "submodule-config.h"
25 #include "submodule.h"
26
27 +static int checkout_optimize_new_branch;
28 +
29 static const char * const checkout_usage[] = {
30 N_("git checkout [<options>] <branch>"),
31 N_("git checkout [<options>] [<branch>] -- <file>..."),
@@ -41,6 +43,10 @@ struct checkout_opts {
43 int ignore_skipworktree;
44 int ignore_other_worktrees;
45 int show_progress;
46 + /*
47 + * If new checkout options are added, skip_merge_working_tree
48 + * should be updated accordingly.
49 + */
50
51 const char *new_branch;
52 const char *new_branch_force;
@@ -471,6 +477,98 @@ static void setup_branch_path(struct branch_info *branch)
477 branch->path = strbuf_detach(&buf, NULL);
478 }
479
480 +/*
481 + * Skip merging the trees, updating the index and working directory if and
482 + * only if we are creating a new branch via "git checkout -b <new_branch>."
483 + */
484 +static int skip_merge_working_tree(const struct checkout_opts *opts,
485 + const struct branch_info *old_branch_info,
486 + const struct branch_info *new_branch_info)
487 +{
488 + /*
489 + * Do the merge if sparse checkout is on and the user has not opted in
490 + * to the optimized behavior
491 + */
492 + if (core_apply_sparse_checkout && !checkout_optimize_new_branch)
493 + return 0;
494 +
495 + /*
496 + * We must do the merge if we are actually moving to a new commit.
497 + */
498 + if (!old_branch_info->commit || !new_branch_info->commit ||
499 + oidcmp(&old_branch_info->commit->object.oid, &new_branch_info->commit->object.oid))
500 + return 0;
501 +
502 + /*
503 + * opts->patch_mode cannot be used with switching branches so is
504 + * not tested here
505 + */
506 +
507 + /*
508 + * opts->quiet only impacts output so doesn't require a merge
509 + */
510 +
511 + /*
512 + * Honor the explicit request for a three-way merge or to throw away
513 + * local changes
514 + */
515 + if (opts->merge || opts->force)
516 + return 0;
517 +
518 + /*
519 + * --detach is documented as "updating the index and the files in the
520 + * working tree" but this optimization skips those steps so fall through
521 + * to the regular code path.
522 + */
523 + if (opts->force_detach)
524 + return 0;
525 +
526 + /*
527 + * opts->writeout_stage cannot be used with switching branches so is
528 + * not tested here
529 + */
530 +
531 + /*
532 + * Honor the explicit ignore requests
533 + */
534 + if (!opts->overwrite_ignore || opts->ignore_skipworktree ||
535 + opts->ignore_other_worktrees)
536 + return 0;
537 +
538 + /*
539 + * opts->show_progress only impacts output so doesn't require a merge
540 + */
541 +
542 + /*
543 + * If we aren't creating a new branch any changes or updates will
544 + * happen in the existing branch. Since that could only be updating
545 + * the index and working directory, we don't want to skip those steps
546 + * or we've defeated any purpose in running the command.
547 + */
548 + if (!opts->new_branch)
549 + return 0;
550 +
551 + /*
552 + * new_branch_force is defined to "create/reset and checkout a branch"
553 + * so needs to go through the merge to do the reset
554 + */
555 + if (opts->new_branch_force)
556 + return 0;
557 +
558 + /*
559 + * A new orphaned branch requrires the index and the working tree to be
560 + * adjusted to <start_point>
561 + */
562 + if (opts->new_orphan_branch)
563 + return 0;
564 +
565 + /*
566 + * Remaining variables are not checkout options but used to track state
567 + */
568 +
569 + return 1;
570 +}
571 +
572 static int merge_working_tree(const struct checkout_opts *opts,
573 struct branch_info *old_branch_info,
574 struct branch_info *new_branch_info,
@@ -845,10 +943,19 @@ static int switch_branches(const struct checkout_opts *opts,
943 parse_commit_or_die(new_branch_info->commit);
944 }
945
848 - ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
849 - if (ret) {
850 - free(path_to_free);
851 - return ret;
946 + /* optimize the "checkout -b <new_branch> path */
947 + if (skip_merge_working_tree(opts, &old_branch_info, new_branch_info)) {
948 + if (!checkout_optimize_new_branch && !opts->quiet) {
949 + if (read_cache_preload(NULL) < 0)
950 + return error(_("index file corrupt"));
951 + show_local_changes(&new_branch_info->commit->object, &opts->diff_options);
952 + }
953 + } else {
954 + ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
955 + if (ret) {
956 + free(path_to_free);
957 + return ret;
958 + }
959 }
960
961 if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)
@@ -863,6 +970,11 @@ static int switch_branches(const struct checkout_opts *opts,
970
971 static int git_checkout_config(const char *var, const char *value, void *cb)
972 {
973 + if (!strcmp(var, "checkout.optimizenewbranch")) {
974 + checkout_optimize_new_branch = git_config_bool(var, value);
975 + return 0;
976 + }
977 +
978 if (!strcmp(var, "diff.ignoresubmodules")) {
979 struct checkout_opts *opts = cb;
980 handle_ignore_submodules_arg(&opts->diff_options, value);
t/t1090-sparse-checkout-scope.sh
+14
@@ -31,6 +31,20 @@ test_expect_success 'perform sparse checkout of master' '
31 test_path_is_file c
32 '
33
34 +test_expect_success 'checkout -b checkout.optimizeNewBranch interaction' '
35 + cp .git/info/sparse-checkout .git/info/sparse-checkout.bak &&
36 + test_when_finished "
37 + mv -f .git/info/sparse-checkout.bak .git/info/sparse-checkout
38 + git checkout master
39 + " &&
40 + echo "/b" >>.git/info/sparse-checkout &&
41 + test "$(git ls-files -t b)" = "S b" &&
42 + git -c checkout.optimizeNewBranch=true checkout -b fast &&
43 + test "$(git ls-files -t b)" = "S b" &&
44 + git checkout -b slow &&
45 + test "$(git ls-files -t b)" = "H b"
46 +'
47 +
48 test_expect_success 'merge feature branch into sparse checkout of master' '
49 git merge feature &&
50 test_path_is_file a &&