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>
This commit is contained in:
parent
ffc6fa0e39
commit
fa655d8411
@ -1101,6 +1101,14 @@ browser.<tool>.path::
|
|||||||
browse HTML help (see `-w` option in linkgit:git-help[1]) or a
|
browse HTML help (see `-w` option in linkgit:git-help[1]) or a
|
||||||
working repository in gitweb (see linkgit:git-instaweb[1]).
|
working repository in gitweb (see linkgit:git-instaweb[1]).
|
||||||
|
|
||||||
|
checkout.optimizeNewBranch
|
||||||
|
Optimizes the performance of "git checkout -b <new_branch>" 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.
|
||||||
|
|
||||||
clean.requireForce::
|
clean.requireForce::
|
||||||
A boolean to make git-clean do nothing unless given -f,
|
A boolean to make git-clean do nothing unless given -f,
|
||||||
-i or -n. Defaults to true.
|
-i or -n. Defaults to true.
|
||||||
|
@ -24,6 +24,8 @@
|
|||||||
#include "submodule-config.h"
|
#include "submodule-config.h"
|
||||||
#include "submodule.h"
|
#include "submodule.h"
|
||||||
|
|
||||||
|
static int checkout_optimize_new_branch;
|
||||||
|
|
||||||
static const char * const checkout_usage[] = {
|
static const char * const checkout_usage[] = {
|
||||||
N_("git checkout [<options>] <branch>"),
|
N_("git checkout [<options>] <branch>"),
|
||||||
N_("git checkout [<options>] [<branch>] -- <file>..."),
|
N_("git checkout [<options>] [<branch>] -- <file>..."),
|
||||||
@ -41,6 +43,10 @@ struct checkout_opts {
|
|||||||
int ignore_skipworktree;
|
int ignore_skipworktree;
|
||||||
int ignore_other_worktrees;
|
int ignore_other_worktrees;
|
||||||
int show_progress;
|
int show_progress;
|
||||||
|
/*
|
||||||
|
* If new checkout options are added, skip_merge_working_tree
|
||||||
|
* should be updated accordingly.
|
||||||
|
*/
|
||||||
|
|
||||||
const char *new_branch;
|
const char *new_branch;
|
||||||
const char *new_branch_force;
|
const char *new_branch_force;
|
||||||
@ -471,6 +477,98 @@ static void setup_branch_path(struct branch_info *branch)
|
|||||||
branch->path = strbuf_detach(&buf, NULL);
|
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 <new_branch>."
|
||||||
|
*/
|
||||||
|
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 ||
|
||||||
|
oidcmp(&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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 <start_point>
|
||||||
|
*/
|
||||||
|
if (opts->new_orphan_branch)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remaining variables are not checkout options but used to track state
|
||||||
|
*/
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static int merge_working_tree(const struct checkout_opts *opts,
|
static int merge_working_tree(const struct checkout_opts *opts,
|
||||||
struct branch_info *old_branch_info,
|
struct branch_info *old_branch_info,
|
||||||
struct branch_info *new_branch_info,
|
struct branch_info *new_branch_info,
|
||||||
@ -845,10 +943,19 @@ static int switch_branches(const struct checkout_opts *opts,
|
|||||||
parse_commit_or_die(new_branch_info->commit);
|
parse_commit_or_die(new_branch_info->commit);
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
|
/* optimize the "checkout -b <new_branch> path */
|
||||||
if (ret) {
|
if (skip_merge_working_tree(opts, &old_branch_info, new_branch_info)) {
|
||||||
free(path_to_free);
|
if (!checkout_optimize_new_branch && !opts->quiet) {
|
||||||
return ret;
|
if (read_cache_preload(NULL) < 0)
|
||||||
|
return error(_("index file corrupt"));
|
||||||
|
show_local_changes(&new_branch_info->commit->object, &opts->diff_options);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
|
||||||
|
if (ret) {
|
||||||
|
free(path_to_free);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)
|
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,
|
|||||||
|
|
||||||
static int git_checkout_config(const char *var, const char *value, void *cb)
|
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")) {
|
if (!strcmp(var, "diff.ignoresubmodules")) {
|
||||||
struct checkout_opts *opts = cb;
|
struct checkout_opts *opts = cb;
|
||||||
handle_ignore_submodules_arg(&opts->diff_options, value);
|
handle_ignore_submodules_arg(&opts->diff_options, value);
|
||||||
|
@ -31,6 +31,20 @@ test_expect_success 'perform sparse checkout of master' '
|
|||||||
test_path_is_file c
|
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' '
|
test_expect_success 'merge feature branch into sparse checkout of master' '
|
||||||
git merge feature &&
|
git merge feature &&
|
||||||
test_path_is_file a &&
|
test_path_is_file a &&
|
||||||
|
Loading…
x
Reference in New Issue
Block a user