Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How rebase result may differ from result of a merge?

In one of GitHub's articles I read the following:

You aren't able to automatically rebase and merge on GitHub when: Rebasing the commits is considered "unsafe", such as when a rebase is possible without merge conflicts but would produce a different result than a merge would.

It isn't clear for me how a rebase may produce a different result than a merge.

Can anyone explain how is it possible?


Link to the original article: https://help.github.com/articles/about-pull-request-merges/

like image 363
Victor Dombrovsky Avatar asked Jun 14 '17 13:06

Victor Dombrovsky


People also ask

How is rebase different from merge?

Merging is a safe option that preserves the entire history of your repository, while rebasing creates a linear history by moving your feature branch onto the tip of main .

What is rebase instead of merge?

Git rebase and merge both integrate changes from one branch into another. Where they differ is how it's done. Git rebase moves a feature branch into a master. Git merge adds a new commit, preserving the history.

Can a rebase result in merge conflict?

If the change that you submitted has a merge conflict, you need to manually resolve it using git rebase. Rebasing is used to integrate changes from one branch into another to resolve conflicts when multiple commits happen on the same file. Never do a rebase on public (master) branches.

What is the difference between git merge and git rebase when would you use one over the other?

Reading the official Git manual it states that “rebase reapplies commits on top of another base branch”, whereas “merge joins two or more development histories together”. In other words, the key difference between merge and rebase is that while merge preserves history as it happened, rebase rewrites it.


1 Answers

Here's a construction proof of a case where rebase and merge produce different results. I assume this is the case they are talking about. Edit: There is another case that can occur, when merging branches where the side branch to be rebased has-or-merged contains one commit that will be skipped (due to patch-ID matching) during a rebase, followed by a reversion of that commit (that will not be skipped). See Changes to a file are not retained by merge, why? If I have time later I will try to add a construction proof for that example as well.

The trick is that since rebase copies commits but omits merges, we need to drop a merge whose resolution is not simple composition of its predecessors. For this merge to have had no conflicts, I think it must be an "evil merge", so this is what I put into the script.

The graph we build looks like this:

  B   <-- master
 /
A--C--E   <-- branch
 \   /
  \ /
   D   <-- br2

If you are on master (your tip commit is B) and you git merge branch, this combines the changes from diffing A-vs-B with those from diffing A-vs-E. The resulting graph is:

  B-----F   <-- master
 /     /
A--C--E   <-- branch
 \   /
  \ /
   D   <-- br2

and the contents of commit F are determined by those of A, B, and E.

If you are on branch (your tip commit is E) and you git rebase master, this copies commits C and D, in some order (it's not clear which). It completely omits commit E. The resulting graph is:

  B   <-- master
 / \
A   C'-D'   <-- branch
 \
  D   <-- br2

(the original C and E are only available through reflogs and ORIG_HEAD). Moving master in a fast-forward fashion, the tip of master becomes commit D'. The contents of commit D' are determined by adding the changes extracted from C and D to B.

Since we used an "evil merge" to make changes in E that appear in neither C nor D, those changes vanish.

Here is the script that creates the problem (note, it makes a temporary directory tt that it leaves in the current directory).

#! /bin/sh

fatal() {
    echo fatal: "$@" 1>&2; exit 1
}

[ -e tt ] && fatal tt already exists

mkdir tt && cd tt && git init -q || fatal failed to create tt repo

echo README > README && git add README && git commit -q -m A || fatal A
git branch branch || fatal unable to make branch
echo for master > bfile && git add bfile && git commit -q -m B || fatal B

git checkout -q -b br2 branch || fatal checkout -b br2 branch
echo file for C > cfile && git add cfile && git commit -q -m C || fatal C
git checkout -q branch || fatal checkout branch
echo file for D > dfile && git add dfile && git commit -q -m D || fatal D
git merge -q --no-commit br2 && git rm -q -f cfile && git commit -q -m E ||
    fatal E
git branch -D br2
git checkout -q master || fatal checkout master

echo merging branch
git merge --no-edit branch || fatal merge failed
echo result is: *

echo removing merge, replacing with rebase of branch onto master
git reset -q --hard HEAD^ || fatal reset failed
git checkout -q branch || fatal switch back to master failed
git rebase master || fatal rebase failed
echo result is: *

echo removing rebase as well so you can poke around
git reset --hard ORIG_HEAD
like image 113
torek Avatar answered Oct 21 '22 16:10

torek