git rebase has two primary backends: apply and merge. (The apply
backend used to be known as the am backend, but the name led to
confusion as it looks like a verb instead of a noun. Also, the
merge backend used to be known as the interactive backend, but it
is now used for non-interactive cases as well. Both were renamed
based on lower-level functionality that underpinned each.) There
are some subtle differences in how these two backends behave:
Empty commits
The apply backend unfortunately drops intentionally empty
commits, i.e. commits that started empty, though these are rare
in practice. It also drops commits that become empty and has no
option for controlling this behavior.
The merge backend keeps intentionally empty commits by default
(though with -i they are marked as empty in the todo list editor,
or they can be dropped automatically with --no-keep-empty).
Similar to the apply backend, by default the merge backend drops
commits that become empty unless -i/--interactive is specified
(in which case it stops and asks the user what to do). The merge
backend also has an --empty={drop,keep,ask} option for changing
the behavior of handling commits that become empty.
Directory rename detection
Due to the lack of accurate tree information (arising from
constructing fake ancestors with the limited information
available in patches), directory rename detection is disabled in
the apply backend. Disabled directory rename detection means that
if one side of history renames a directory and the other adds new
files to the old directory, then the new files will be left
behind in the old directory without any warning at the time of
rebasing that you may want to move these files into the new
directory.
Directory rename detection works with the merge backend to
provide you warnings in such cases.
Context
The apply backend works by creating a sequence of patches (by
calling format-patch
internally), and then applying the patches
in sequence (calling am
internally). Patches are composed of
multiple hunks, each with line numbers, a context region, and the
actual changes. The line numbers have to be taken with some fuzz,
since the other side will likely have inserted or deleted lines
earlier in the file. The context region is meant to help find how
to adjust the line numbers in order to apply the changes to the
right lines. However, if multiple areas of the code have the same
surrounding lines of context, the wrong one can be picked. There
are real-world cases where this has caused commits to be
reapplied incorrectly with no conflicts reported. Setting
diff.context to a larger value may prevent such types of
problems, but increases the chance of spurious conflicts (since
it will require more lines of matching context to apply).
The merge backend works with a full copy of each relevant file,
insulating it from these types of problems.
Labelling of conflicts markers
When there are content conflicts, the merge machinery tries to
annotate each side's conflict markers with the commits where the
content came from. Since the apply backend drops the original
information about the rebased commits and their parents (and
instead generates new fake commits based off limited information
in the generated patches), those commits cannot be identified;
instead it has to fall back to a commit summary. Also, when
merge.conflictStyle is set to diff3, the apply backend will use
"constructed merge base" to label the content from the merge
base, and thus provide no information about the merge base commit
whatsoever.
The merge backend works with the full commits on both sides of
history and thus has no such limitations.
Hooks
The apply backend has not traditionally called the post-commit
hook, while the merge backend has. Both have called the
post-checkout hook, though the merge backend has squelched its
output. Further, both backends only call the post-checkout hook
with the starting point commit of the rebase, not the
intermediate commits nor the final commit. In each case, the
calling of these hooks was by accident of implementation rather
than by design (both backends were originally implemented as
shell scripts and happened to invoke other commands like git
checkout or git commit that would call the hooks). Both backends
should have the same behavior, though it is not entirely clear
which, if any, is correct. We will likely make rebase stop
calling either of these hooks in the future.
Interruptability
The apply backend has safety problems with an ill-timed
interrupt; if the user presses Ctrl-C at the wrong time to try to
abort the rebase, the rebase can enter a state where it cannot be
aborted with a subsequent git rebase --abort
. The merge backend
does not appear to suffer from the same shortcoming. (See
https://lore.kernel.org/git/20200207132152.GC2868@szeder.dev/
for
details.)
Commit Rewording
When a conflict occurs while rebasing, rebase stops and asks the
user to resolve. Since the user may need to make notable changes
while resolving conflicts, after conflicts are resolved and the
user has run git rebase --continue
, the rebase should open an
editor and ask the user to update the commit message. The merge
backend does this, while the apply backend blindly applies the
original commit message.
Miscellaneous differences
There are a few more behavioral differences that most folks would
probably consider inconsequential but which are mentioned for
completeness:
• Reflog: The two backends will use different wording when
describing the changes made in the reflog, though both will
make use of the word "rebase".
• Progress, informational, and error messages: The two backends
provide slightly different progress and informational
messages. Also, the apply backend writes error messages (such
as "Your files would be overwritten...") to stdout, while the
merge backend writes them to stderr.
• State directories: The two backends keep their state in
different directories under .git/