Skip to content

Commit

Permalink
Merge pull request #404: Make 'ort' the default merge strategy
Browse files Browse the repository at this point in the history
The 'ort' strategy is a new algorithm to replace the 'recursive' merge strategy. I've been reviewing some of the performance patches upstream, many of which are already in Git 2.32.0 (more coming in 2.33.0) and even with the ones already included, it is a clear performance win for our large repos.

I tested on the Office monorepo and consistently saw merge times in the 5-6 second range. With the 'recursive' strategy, these would range from 7-20 seconds. My tests reproduced merges found within the commit history, and the ones that succeeded without conflicts matched the committed changes. There were even a few where the 'recursive' strategy did not resolve to the committed change, but the 'ort' version did (probably because of better rename detection).

Not only is this a beneficial performance change for our users across `microsoft/git`, it will be a critical step to allowing `git merge` to work quickly with sparse index. In my testing of a prototype, I was able to get `git merge` commands with sparse index and the 'ort' strategy down to 0.5-1.5 seconds in most cases. (Cases with a merge conflict outside of the sparse-checkout definition jumped back up to the 6-7 second range, which is expected, and should be rare.)

cc: @newren for awareness. Thanks for the patches! These were applied from those sent to the list via git#1055.
  • Loading branch information
derrickstolee committed Aug 12, 2021
2 parents 71f6630 + 806be39 commit 9e4057a
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 72 deletions.
29 changes: 15 additions & 14 deletions Documentation/git-rebase.txt
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,7 @@ See also INCOMPATIBLE OPTIONS below.

-m::
--merge::
Use merging strategies to rebase. When the recursive (default) merge
strategy is used, this allows rebase to be aware of renames on the
upstream side. This is the default.
Using merging strategies to rebase (default).
+
Note that a rebase merge works by replaying each commit from the working
branch on top of the <upstream> branch. Because of this, when a merge
Expand All @@ -354,9 +352,8 @@ See also INCOMPATIBLE OPTIONS below.

-s <strategy>::
--strategy=<strategy>::
Use the given merge strategy.
If there is no `-s` option 'git merge-recursive' is used
instead. This implies --merge.
Use the given merge strategy, instead of the default `ort`.
This implies `--merge`.
+
Because 'git rebase' replays each commit from the working branch
on top of the <upstream> branch using the given strategy, using
Expand All @@ -369,7 +366,7 @@ See also INCOMPATIBLE OPTIONS below.
--strategy-option=<strategy-option>::
Pass the <strategy-option> through to the merge strategy.
This implies `--merge` and, if no strategy has been
specified, `-s recursive`. Note the reversal of 'ours' and
specified, `-s ort`. Note the reversal of 'ours' and
'theirs' as noted above for the `-m` option.
+
See also INCOMPATIBLE OPTIONS below.
Expand Down Expand Up @@ -530,7 +527,7 @@ The `--rebase-merges` mode is similar in spirit to the deprecated
where commits can be reordered, inserted and dropped at will.
+
It is currently only possible to recreate the merge commits using the
`recursive` merge strategy; Different merge strategies can be used only via
`ort` merge strategy; different merge strategies can be used only via
explicit `exec git merge -s <strategy> [...]` commands.
+
See also REBASING MERGES and INCOMPATIBLE OPTIONS below.
Expand Down Expand Up @@ -1219,12 +1216,16 @@ successful merge so that the user can edit the message.
If a `merge` command fails for any reason other than merge conflicts (i.e.
when the merge operation did not even start), it is rescheduled immediately.

At this time, the `merge` command will *always* use the `recursive`
merge strategy for regular merges, and `octopus` for octopus merges,
with no way to choose a different one. To work around
this, an `exec` command can be used to call `git merge` explicitly,
using the fact that the labels are worktree-local refs (the ref
`refs/rewritten/onto` would correspond to the label `onto`, for example).
By default, the `merge` command will use the `ort` merge strategy for
regular merges, and `octopus` for octopus merges. One can specify a
default strategy for all merges using the `--strategy` argument when
invoking rebase, or can override specific merges in the interactive
list of commands by using an `exec` command to call `git merge`
explicitly with a `--strategy` argument. Note that when calling `git
merge` explicitly like this, you can make use of the fact that the
labels are worktree-local refs (the ref `refs/rewritten/onto` would
correspond to the label `onto`, for example) in order to refer to the
branches you want to merge.

Note: the first command (`label onto`) labels the revision onto which
the commits are rebased; The name `onto` is just a convention, as a nod
Expand Down
2 changes: 1 addition & 1 deletion Documentation/gitfaq.txt
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ best to always use a regular merge commit.

[[merge-two-revert-one]]
If I make a change on two branches but revert it on one, why does the merge of those branches include the change?::
By default, when Git does a merge, it uses a strategy called the recursive
By default, when Git does a merge, it uses a strategy called the `ort`
strategy, which does a fancy three-way merge. In such a case, when Git
performs the merge, it considers exactly three points: the two heads and a
third point, called the _merge base_, which is usually the common ancestor of
Expand Down
4 changes: 2 additions & 2 deletions Documentation/merge-options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ With --squash, --commit is not allowed, and will fail.
Use the given merge strategy; can be supplied more than
once to specify them in the order they should be tried.
If there is no `-s` option, a built-in list of strategies
is used instead ('git merge-recursive' when merging a single
head, 'git merge-octopus' otherwise).
is used instead (`ort` when merging a single head,
`octopus` otherwise).

-X <option>::
--strategy-option=<option>::
Expand Down
99 changes: 58 additions & 41 deletions Documentation/merge-strategies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,23 @@ backend 'merge strategies' to be chosen with `-s` option. Some strategies
can also take their own options, which can be passed by giving `-X<option>`
arguments to `git merge` and/or `git pull`.

resolve::
This can only resolve two heads (i.e. the current branch
and another branch you pulled from) using a 3-way merge
algorithm. It tries to carefully detect criss-cross
merge ambiguities and is considered generally safe and
fast.

recursive::
This can only resolve two heads using a 3-way merge
algorithm. When there is more than one common
ancestor that can be used for 3-way merge, it creates a
merged tree of the common ancestors and uses that as
the reference tree for the 3-way merge. This has been
reported to result in fewer merge conflicts without
causing mismerges by tests done on actual merge commits
taken from Linux 2.6 kernel development history.
Additionally this can detect and handle merges involving
renames, but currently cannot make use of detected
copies. This is the default merge strategy when pulling
or merging one branch.
ort::
This is the default merge strategy when pulling or merging one
branch. This strategy can only resolve two heads using a
3-way merge algorithm. When there is more than one common
ancestor that can be used for 3-way merge, it creates a merged
tree of the common ancestors and uses that as the reference
tree for the 3-way merge. This has been reported to result in
fewer merge conflicts without causing mismerges by tests done
on actual merge commits taken from Linux 2.6 kernel
development history. Additionally this strategy can detect
and handle merges involving renames. It does not make use of
detected copies. The name for this algorithm is an acronym
("Ostensibly Recursive's Twin") and came from the fact that it
was written as a replacement for the previous default
algorithm, `recursive`.
+
The 'recursive' strategy can take the following options:
The 'ort' strategy can take the following options:

ours;;
This option forces conflicting hunks to be auto-resolved cleanly by
Expand All @@ -43,19 +38,6 @@ theirs;;
This is the opposite of 'ours'; note that, unlike 'ours', there is
no 'theirs' merge strategy to confuse this merge option with.

patience;;
With this option, 'merge-recursive' spends a little extra time
to avoid mismerges that sometimes occur due to unimportant
matching lines (e.g., braces from distinct functions). Use
this when the branches to be merged have diverged wildly.
See also linkgit:git-diff[1] `--patience`.

diff-algorithm=[patience|minimal|histogram|myers];;
Tells 'merge-recursive' to use a different diff algorithm, which
can help avoid mismerges that occur due to unimportant matching
lines (such as braces from distinct functions). See also
linkgit:git-diff[1] `--diff-algorithm`.

ignore-space-change;;
ignore-all-space;;
ignore-space-at-eol;;
Expand Down Expand Up @@ -84,11 +66,6 @@ no-renormalize;;
Disables the `renormalize` option. This overrides the
`merge.renormalize` configuration variable.

no-renames;;
Turn off rename detection. This overrides the `merge.renames`
configuration variable.
See also linkgit:git-diff[1] `--no-renames`.

find-renames[=<n>];;
Turn on rename detection, optionally setting the similarity
threshold. This is the default. This overrides the
Expand All @@ -105,6 +82,46 @@ subtree[=<path>];;
is prefixed (or stripped from the beginning) to make the shape of
two trees to match.

recursive::
This can only resolve two heads using a 3-way merge
algorithm. When there is more than one common
ancestor that can be used for 3-way merge, it creates a
merged tree of the common ancestors and uses that as
the reference tree for the 3-way merge. This has been
reported to result in fewer merge conflicts without
causing mismerges by tests done on actual merge commits
taken from Linux 2.6 kernel development history.
Additionally this can detect and handle merges involving
renames. It does not make use of detected copies. This was
the default strategy for resolving two heads from Git v0.99.9k
until v2.33.0.
+
The 'recursive' strategy takes the same options as 'ort'. However,
there are three additional options that 'ort' ignores (not documented
above) that are potentially useful with the 'recursive' strategy:

patience;;
Deprecated synonym for `diff-algorithm=patience`.

diff-algorithm=[patience|minimal|histogram|myers];;
Use a different diff algorithm while merging, which can help
avoid mismerges that occur due to unimportant matching lines
(such as braces from distinct functions). See also
linkgit:git-diff[1] `--diff-algorithm`. Note that `ort`
specifically uses `diff-algorithm=histogram`, while `recursive`
defaults to the `diff.algorithm` config setting.

no-renames;;
Turn off rename detection. This overrides the `merge.renames`
configuration variable.
See also linkgit:git-diff[1] `--no-renames`.

resolve::
This can only resolve two heads (i.e. the current branch
and another branch you pulled from) using a 3-way merge
algorithm. It tries to carefully detect criss-cross
merge ambiguities. It does not handle renames.

octopus::
This resolves cases with more than two heads, but refuses to do
a complex merge that needs manual resolution. It is
Expand All @@ -121,13 +138,13 @@ ours::
the 'recursive' merge strategy.

subtree::
This is a modified recursive strategy. When merging trees A and
This is a modified ort strategy. When merging trees A and
B, if B corresponds to a subtree of A, B is first adjusted to
match the tree structure of A, instead of reading the trees at
the same level. This adjustment is also done to the common
ancestor tree.

With the strategies that use 3-way merge (including the default, 'recursive'),
With the strategies that use 3-way merge (including the default, 'ort'),
if a change is made on both branches, but later reverted on one of the
branches, that change will be present in the merged result; some people find
this behavior confusing. It occurs because only the heads and the merge base
Expand Down
14 changes: 8 additions & 6 deletions Documentation/technical/directory-rename-detection.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ Directory rename detection
==========================

Rename detection logic in diffcore-rename that checks for renames of
individual files is aggregated and analyzed in merge-recursive for cases
where combinations of renames indicate that a full directory has been
renamed.
individual files is also aggregated there and then analyzed in either
merge-ort or merge-recursive for cases where combinations of renames
indicate that a full directory has been renamed.

Scope of abilities
------------------
Expand Down Expand Up @@ -88,9 +88,11 @@ directory rename detection support in:
Folks have requested in the past that `git diff` detect directory
renames and somehow simplify its output. It is not clear whether this
would be desirable or how the output should be simplified, so this was
simply not implemented. Further, to implement this, directory rename
detection logic would need to move from merge-recursive to
diffcore-rename.
simply not implemented. Also, while diffcore-rename has most of the
logic for detecting directory renames, some of the logic is still found
within merge-ort and merge-recursive. Fully supporting directory
rename detection in diffs would require copying or moving the remaining
bits of logic to the diff machinery.

* am

Expand Down
2 changes: 1 addition & 1 deletion Documentation/user-manual.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3190,7 +3190,7 @@ that *updated* thing--the old state that you added originally ends up
not being pointed to by any commit or tree, so it's now a dangling blob
object.

Similarly, when the "recursive" merge strategy runs, and finds that
Similarly, when the "ort" merge strategy runs, and finds that
there are criss-cross merges and thus more than one merge base (which is
fairly unusual, but it does happen), it will generate one temporary
midway tree (or possibly even more, if you had lots of criss-crossing
Expand Down
12 changes: 9 additions & 3 deletions builtin/merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ static int autostash;
static int no_verify;

static struct strategy all_strategy[] = {
{ "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL },
{ "recursive", NO_TRIVIAL },
{ "octopus", DEFAULT_OCTOPUS },
{ "ort", NO_TRIVIAL },
{ "ort", DEFAULT_TWOHEAD | NO_TRIVIAL },
{ "resolve", 0 },
{ "ours", NO_FAST_FORWARD | NO_TRIVIAL },
{ "subtree", NO_FAST_FORWARD | NO_TRIVIAL },
Expand Down Expand Up @@ -739,7 +739,7 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,

for (x = 0; x < xopts_nr; x++)
if (parse_merge_opt(&o, xopts[x]))
die(_("Unknown option for merge-recursive: -X%s"), xopts[x]);
die(_("unknown strategy option: -X%s"), xopts[x]);

o.branch1 = head_arg;
o.branch2 = merge_remote_util(remoteheads->item)->name;
Expand Down Expand Up @@ -1485,6 +1485,12 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
fast_forward = FF_NO;
}

if (!use_strategies && !pull_twohead &&
remoteheads && !remoteheads->next) {
char *default_strategy = getenv("GIT_TEST_MERGE_ALGORITHM");
if (default_strategy)
append_strategy(get_strategy(default_strategy));
}
if (!use_strategies) {
if (!remoteheads)
; /* already up-to-date */
Expand Down
2 changes: 1 addition & 1 deletion builtin/rebase.c
Original file line number Diff line number Diff line change
Expand Up @@ -1713,7 +1713,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
int i;

if (!options.strategy)
options.strategy = "recursive";
options.strategy = "ort";

strbuf_reset(&buf);
for (i = 0; i < strategy_options.nr; i++)
Expand Down
6 changes: 3 additions & 3 deletions sequencer.c
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ static int do_recursive_merge(struct repository *r,
for (i = 0; i < opts->xopts_nr; i++)
parse_merge_opt(&o, opts->xopts[i]);

if (opts->strategy && !strcmp(opts->strategy, "ort")) {
if (!opts->strategy || !strcmp(opts->strategy, "ort")) {
memset(&result, 0, sizeof(result));
merge_incore_nonrecursive(&o, base_tree, head_tree, next_tree,
&result);
Expand Down Expand Up @@ -2065,7 +2065,7 @@ static int do_pick_commit(struct repository *r,
/*
* We do not intend to commit immediately. We just want to
* merge the differences in, so let's compute the tree
* that represents the "current" state for merge-recursive
* that represents the "current" state for the merge machinery
* to work on.
*/
if (write_index_as_tree(&head, r->index, r->index_file, 0, NULL))
Expand Down Expand Up @@ -3988,7 +3988,7 @@ static int do_merge(struct repository *r,
o.branch2 = ref_name.buf;
o.buffer_output = 2;

if (opts->strategy && !strcmp(opts->strategy, "ort")) {
if (!opts->strategy || !strcmp(opts->strategy, "ort")) {
/*
* TODO: Should use merge_incore_recursive() and
* merge_switch_to_result(), skipping the call to
Expand Down

0 comments on commit 9e4057a

Please sign in to comment.