whizz waltz
whizz waltz

Reputation: 59

Some question about git rebase onto option

main branch:

enter image description here

lark branch:

enter image description here

after rebase enter image description here Strange!, where is the lark_file? Well, there should be three files (init, main_file, lark_file), right?

The script to create the environment.

#!/bin/env bash
git init
touch init
git add -- .
git commit -am init
echo a >> main_file
git add --all
git commit -am 'main a'
echo b >> main_file
git commit -am 'main b'
echo c >> main_file
git commit -am 'main c'
echo d >> main_file
git commit -am 'main d'
echo e >> main_file
git commit -am 'main e'

git checkout HEAD~5

git checkout -B lark
echo a >> lark_file
git add --all
git commit -am 'lark a'
echo b >> lark_file
git commit -am 'lark b'
echo c >> lark_file
git commit -am 'lark c'
echo d >> lark_file
git commit -am 'lark d'
echo e >> lark_file
git commit -am 'lark e'
echo f >> lark_file
git commit -am 'lark f'

Upvotes: 2

Views: 80

Answers (1)

torek
torek

Reputation: 490078

TL;DR: because of rebase's rename detection, Git is applying the change to the wrong file. (The --onto flag is not required to get this effect. You just have to have a diff in which Git produces a false/incorrect rename.)

Long

First, let me modify your reproducer a bit so that everyone can use it:

$ cat repro.sh
#! /bin/sh -e
mkdir t
cd t
git init
touch init
git add -- .
git commit -am init
echo a >> main_file
git add --all
git commit -am 'main a'
echo b >> main_file
git commit -am 'main b'
git tag tag-onto
echo c >> main_file
git commit -am 'main c'
echo d >> main_file
git commit -am 'main d'
echo e >> main_file
git commit -am 'main e'

git checkout HEAD~5

git checkout -B lark
echo a >> lark_file
git add --all
git commit -am 'lark a'
echo b >> lark_file
git commit -am 'lark b'
echo c >> lark_file
git commit -am 'lark c'
echo d >> lark_file
git commit -am 'lark d'
echo e >> lark_file
git commit -am 'lark e'
echo f >> lark_file
git commit -am 'lark f'

git rebase --onto tag-onto HEAD~3

This has a few simple changes: it makes a temporary directory t and cd-s into it, so that we can remove the temporary directory when we're done, and so that the reproducer script itself doesn't get stuck in to the repository. More usefully, it changes the last git rebase to:

git rebase --onto tag-onto HEAD~3

That is, we drop the final HEAD—which produces a detached HEAD pointlessly—and we use a tag, tag-onto, as the --onto target on which we're going to copy the lark d commit.

Running this script reproduces the issue:

$ ./repro.sh
[much output snipped]
CONFLICT (content): Merge conflict in main_file
error: could not apply 1a3193f... lark d
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 1a3193f... lark d

The setup here is that we're trying to cherry-pick commit lark~2 onto commit tag-onto (master~3 or main~3 depending on your initial branch).

What we need to realize is that git cherry-pick is a kind of git merge. It does a three-way merge, with the merge base being the parent of the commit being cherry-picked. That means it runs two git diffs, from parent lark~3 to current commit HEAD, and from parent lark~3 to commit lark~2. Let's look at the first of these two diffs:

$ git diff lark~3 HEAD
diff --git a/lark_file b/main_file
similarity index 66%
rename from lark_file
rename to main_file
index de98044..422c2b7 100644
--- a/lark_file
+++ b/main_file
@@ -1,3 +1,2 @@
 a
 b
-c

This says we should rename the file: it should be called main_file now, not lark_file.

The second diff of course shows what you added in commit lark d:

$ git diff lark~3 lark~2
diff --git a/lark_file b/lark_file
index de98044..d68dd40 100644
--- a/lark_file
+++ b/lark_file
@@ -1,3 +1,4 @@
 a
 b
 c
+d

So Git decides that we need to rename lark_file to main_file and add d to the end, at the same time as removing c from the end of main_file.

That is indeed what Git has done: we now have one file main_file instead of two separate files lark_file and main_file, and we see the conflict. Mine is in diff3 style rather than merge style, so it has more information:

$ cat main_file 
a
b
<<<<<<< HEAD:main_file
||||||| parent of 1a3193f... lark d:lark_file
c
=======
c
d
>>>>>>> 1a3193f... lark d:lark_file

Git is behaving correctly according to the Git rules.

The fact that the Git merge rules produce surprises now and then is why you must always inspect any merge result. This includes cherry-pick results, which are merge results.

Upvotes: 2

Related Questions