Teach git to avoid unnecessary merge during trivial checkout. When running 'git checkout -b foo' git follows a common code path through the expensive merge_working_tree even when it is unnecessary. As a result, 95% of the time is spent in merge_working_tree doing the 2-way merge between the new and old commit trees that is unneeded. The time breakdown is as follows: merge_working_tree <-- 95% unpack_trees <-- 80% traverse_trees <-- 50% cache_tree_update <-- 17% mark_new_skip_worktree <-- 10% With a large repo, this cost is pronounced. Using "git checkout -b r" to create and switch to a new branch costs 166 seconds (all times worst case with a cold file system cache). git.c:406 trace: built-in: git 'checkout' '-b' 'r' read-cache.c:1667 performance: 17.442926555 s: read_index_from name-hash.c:128 performance: 2.912145231 s: lazy_init_name_hash read-cache.c:2208 performance: 4.387713335 s: write_locked_index trace.c:420 performance: 166.458921289 s: git command: 'c:\Users\benpeart\bin\git.exe' 'checkout' '-b' 'r' Switched to a new branch 'r' By adding a test to skip the unnecessary call to merge_working_tree in this case reduces the cost to 16 seconds. git.c:406 trace: built-in: git 'checkout' '-b' 's' read-cache.c:1667 performance: 16.100742476 s: read_index_from trace.c:420 performance: 16.461547867 s: git command: 'c:\Users\benpeart\bin\git.exe' 'checkout' '-b' 's' Switched to a new branch 's' Signed-off-by: Ben Peart <benpeart@xxxxxxxxxxxxx> --- builtin/checkout.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/builtin/checkout.c b/builtin/checkout.c index 8672d07..8b2f428 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -38,6 +38,10 @@ struct checkout_opts { int ignore_skipworktree; int ignore_other_worktrees; int show_progress; + /* + * If new checkout options are added, needs_working_tree_merge + * should be updated accordingly. + */ const char *new_branch; const char *new_branch_force; @@ -460,11 +464,99 @@ static void setup_branch_path(struct branch_info *branch) branch->path = strbuf_detach(&buf, NULL); } +static int needs_working_tree_merge(const struct checkout_opts *opts, + const struct branch_info *old, + const struct branch_info *new) +{ + /* + * We must do the merge if we are actually moving to a new + * commit tree. + */ + if (!old->commit || !new->commit || + oidcmp(&old->commit->tree->object.oid, &new->commit->tree->object.oid)) + return 1; + + /* + * 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 1; + + /* + * Checking out the requested commit may require updating the working + * directory and index, let the merge handle it. + */ + if (opts->force_detach) + return 1; + + /* + * 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 1; + + /* + * opts->show_progress only impacts output so doesn't require a merge + */ + + /* + * If we're not creating a new branch, by definition we're changing + * the existing one so need to do the merge + */ + if (!opts->new_branch) + return 1; + + /* + * 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 1; + + /* + * A new orphaned branch requrires the index and the working tree to be + * adjusted to <start_point> + */ + if (opts->new_orphan_branch) + return 1; + + /* + * Remaining variables are not checkout options but used to track state + * that doesn't trigger the need for a merge. + */ + + return 0; +} + static int merge_working_tree(const struct checkout_opts *opts, struct branch_info *old, struct branch_info *new, int *writeout_error) { + /* + * Optimize the performance of "git checkout -b foo" by avoiding + * the expensive merge, index and working directory updates if they + * are not needed. + */ + if (!needs_working_tree_merge(opts, old, new)) + return 0; + int ret; struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); -- 2.10.0.windows.1