0

There is strange issue I am facing when I am trying to do the rebase into my development branch from master. There is some work in progress in the master as well as in dev branches.

In order to get the changes from master I do below

git checkout dev
git rebase master

This works fine. But -

  1. The dev branch commits are duplicating when I do git checkout dev git add . git commit -m 'some change' git push git rebase master the commitsome change is repeating in git project history for dev branch. It also creates the loop .
  2. If I do git rebase master before pushing the dev branch changes then it works well and I can see linear history in project.

I am not sure why it is like that. This seems to me the strange behaviour. Please help if anyone can explain this.

Project History commit b48659d is duplicating(original commit I did 37c07a4) when first did push and then rebase in dev branch.

*   fbadb86 (HEAD -> dev) Merge branch 'dev' of *******
|\  
| * b48659d (origin/dev) sample added in dev branch --> did rebase after push and got this commit again
* | 37c07a4 sample added in dev branch --> commit + push
* | 46c1f40 (origin/master, origin/HEAD, master) master file added in master branch --> commit + push
|/  
* 5baae80 first commit

And this happens for all commits after doing the push. That is causing lots of issue in rebasing as the commits are repeating and also creating lot off conflicts even with my own code changes from previous commits.

Ashvin777
  • 1,446
  • 13
  • 19
  • No matter rebase before pushing or after push, the commit history should be linear. Did you make changes on `master` branch after first time rebasing? And what's the commit history graph now (`gitk --all`)? – Marina Liu Sep 25 '17 at 06:38
  • @Marina-MSFT, I have added the project history, please check. – Ashvin777 Sep 25 '17 at 06:47
  • Which commit(s) is the rebased commit for the first time? the commit history can't find the manner for the first time rebasing. And what if you use `git push -f origin dev` to push dev branch? – Marina Liu Sep 25 '17 at 07:19
  • What do you mean by _There is some work in progress in the master as well as in dev branches._? – pishpish Sep 25 '17 at 12:51

1 Answers1

3

All commits are read-only, always. They are also permanent, with some exceptions (having to do with whether anyone can find them). This means that git rebase copies commits. That's it's job!

Let's take a look at your git log --graph --oneline ... output, but let's start a bit simpler:

  * b48659d (dev) ...
* | 46c1f40 (origin/master, origin/HEAD, master) ...
|/
* 5baae80 first commit

Note the shortened hash IDs, such as 5baae80 and b48659d. These are the "true names" of each commit, shortened to just 7 characters since that's usually sufficient.1 Each commit records the ID of a parent commit, and Git uses these parent commit hashes to follow each commit backwards through history, starting from a more-recent branch tip commit. The tip commit of branch dev is now 37c07a4...:

5baae80--46c1f40   <-- master, origin/master
       \
        b48659d   <-- dev

None of these commits can ever be changed!

You now git push this to origin. The git push command calls up a second Git, over on the machine acting as origin, and hands over the commits that you have that they don't, which everyone agrees to by their IDs. Then your Git asks their Git to set their dev, which will be your origin/dev, to the same value as your dev: b48659d... If they agree to this request, your Git remembers that they now have their dev—your origin/dev—pointing to b48659d, just as we have drawn here, so now we have this:

5baae80--46c1f40   <-- master, origin/master
       \
        b48659d   <-- dev, origin/dev

At this point, you run git rebase, and this is where things start to go wrong.


1Git now dynamically chooses the shortened length, rather than just always using 7, but it starts with 7 by default. You can always use more: the full names are currently 40 characters long.


Rebase copies commits, then moves a branch label

The git rebase command cannot change any existing commit, because nothing in Git can do that. Git is designed to make this impossible. So it does not even try. Instead, it copies commits.

When copying commits, Git needs two pieces of information:

  • What commits should it copy?
  • Where should it place the copies?

Git gets the first—the list of commits to copy—by listing commits starting from HEAD and working backwards, stopping at some point you choose. (This list is backwards so it has to reverse it.) Git gets the second—the target "copy after" point—from the argument you give it, such as master.

In this case, HEAD names dev (because you run git checkout dev before you start the rebase), so the commits to copy end with the one to which the name dev points:

5baae80--46c1f40   <-- master, origin/master
       \
        b48659d   <-- dev (HEAD), origin/dev

We will therefore copy some series of commits stopping at b48659d.

The place to copy them comes from your argument master. The name master identifies commit 46c1f40, so the copies will go after 46c1f40.

The tricky part is how Git computes which commits not to copy. It does this by starting from b48659d and working backwards. You can imagine Git coloring each of these commits green temporarily. This takes Git to 5baae80, which has no parent commit (it is the first commit in the repository, so it cannot have a parent). This stops the walk, so that these two commits are painted green. Then Git starts from the commit you specified by name—46c1f40—and painting the commits red temporarily. The parent of 46c1f40 is 5baae80, so Git paints this one red (don't copy), and tries to go on to its parent to paint those red as well. There is no parent, so we're done with the temporary painting, leaving one green commit, b48659d.

This is the list of commits to copy. (It's backwards, but it's only one entry long anyway, so reversing it does nothing.)

Now Git begins the copying process. Each commit to be copied is copied as if by git cherry-pick (if you use git rebase -i, it's literally copied by cherry-pick). This makes new commits.

The new commit made from the existing b48659d is 37c07a4. Let's draw it in:

                 37c07a4
                /
5baae80--46c1f40   <-- master, origin/master
       \
        b48659d   <-- dev, origin/dev

Now that the entire list of commits has been copied, git rebase does one last thing. It yanks the name dev off the original string of commits, and pastes it onto the new copy. The result is:

                 37c07a4   <-- dev (HEAD)
                /
5baae80--46c1f40   <-- master, origin/master
       \
        b48659d   <-- origin/dev

You can see this in the git log --graph output you quoted in your question.

Where did the last commit come from?

The one remaining question is, where did this come from:

*   fbadb86 (HEAD -> dev) Merge branch 'dev' of *******

That commit exists because you ran git merge.

You probably ran git merge by running git pull. What git pull does, by default, is run git fetch for you, and then run git merge for you. (I advise beginners in Git never to use git pull at all: what it does, and the way it does it, is excessively confusing. Split it up into git fetch followed, sooner or later, by a second Git command, sometimes git rebase, sometimes git merge.)

What git merge does, in general, is to try to combine two separate "lines of development". Given a graph like this:

                 37c07a4   <-- dev (HEAD)
                /
5baae80--46c1f40
       \
        b48659d   <-- origin/dev

Git thinks, more or less, that someone else wrote b48659d (origin/dev) while you wrote 37c07a4. In order to combine them, Git finds their "merge base", which is where these two lines join up again. Following the lines by eye makes it clear: that's 5baae80. So Git makes a new merge commit, which is a commit with two parents. Let's draw that in:

                 37c07a4--fbadb86   <-- dev (HEAD)
                /         /
5baae80--46c1f40         /
       \       __--------
        b48659d   <-- origin/dev

This is what you see now, in your git log --graph --oneline ... output (with the master and origin/master arrows also added in—I left them out because they're just too hard to draw this way).

The fundamental error here was in running git rebase on commits that someone else already had. In this case, the other Git over at origin already had commit b48659d. You cannot change that commit, so when you make your new copy, you stop using your b48659d, but they still have theirs. Eventually you have to incorporate "theirs" back with "your copy", by merging, giving this rather messy picture.

(There is an exception to this rule: if you can convince everyone else who has b48659d to give it up, and switch their devs to use your new copy instead, you can still rebase. But whether you can, and how, is another question.)

torek
  • 448,244
  • 59
  • 642
  • 775