Shawn Northrop
Shawn Northrop

Reputation: 6016

GIT - Rebase - How to deal with conflicts

I am working on a project that has two branches: master and feature

The feature branch was created some time ago and has numerous commits.

Since the feature branch was created there have been a couple of commits to master

At this point when I go to rebase off of master I get conflicts. I resolve them and then rebase --continue. Then I get conflicts again, and again resolve and rebase --continue. This happens over and over and many times it seems like they are the same conflicts that are appearing.

In my mind here is what is happening:

master(commits)->a->b feature(commits)->c->d->e->f->g

feature was branched from master->a and then all commits were created.

When I rebase it rewinds back to the begining of feature where it was branched from master applies master->b and then starts to apply feature->c at this point it has a conflict. I resolve (accepting the master changes) and continue. Now it tries to apply feature->d and finds the same conflict. Again I have to resolve and continue. This happens over and over.

For example here are the changes:

master->a
    <div id="foo">

master->b
    <div id="bar">

feature->c
    <div id="fubar">

feature->d
    //Nothing has changed, inherited from feature->c
    <div id="fubar">

I am assuming that when it reaches feature->c it says change foo to fubar and then it notices that foo is already changed to bar. I resolve to bar and then it does the same logic applying feature->d

My two questions:

1) Is my understanding of how git is working / dealing with commits/conflicts/rebasing correct?

2) How can I avoid having to resolve the same conflicts over and over again? I was thinking of squashing all of the commits on the feature branch so that there is only one to deal with. I was not sure if this was a good idea or exactly the best way to go about squashing in the scenario.

Note, this is a very simplified example. In reality I have a lot more commits and within each numerous files with numerous conflicts. Some of them appear to be the same throughout the rebase --continue process and some are new for each commit.

My end goal is to clean up this project as simply as possible (get the feature branch rebased off the current master). I do not care about the history of the commits.

Upvotes: 17

Views: 27688

Answers (4)

William Myers
William Myers

Reputation: 297

I had a similar issue and it turned out that I was resolving conflicts with --ours when I should have been using --theirs.

The reason is that --ours is your changes in a merge, but in a rebase it is --theirs because git effectively removes your commits and replays copies of them.

So when you resolve a conflict with your version with a rebase do:

git checkout --theirs some.file git add some.file

but when resolving a conflict with your version with a merge do:

git checkout --ours some.file git add some.file

Upvotes: 4

Asif Raza
Asif Raza

Reputation: 3695

When you perform a git rebase operation, you're typically moving commits around. Because of this, you might get into a situation where a merge conflict is introduced. That means that two of your commits modified the same line in the same file, and Git doesn't know which change to apply.

After you reorder and manipulate commits using git rebase, should a merge conflict occur, Git will tell you so with the following message printed to the terminal:

error: could not apply fa39187... something to add to patch A

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply fa39187f3c3dfd2ab5faa38ac01cf3de7ce2e841... Change fake file

Here, Git is telling you which commit is causing the conflict (fa39187). You're given three choices:

  • You can run git rebase --abort to completely undo the rebase. Git will return you to your branch's state as it was before git rebase was called.
  • You can run git rebase --skip to completely skip the commit. That means that none of the changes introduced by the problematic commit will be included. It is very rare that you would choose this option.
  • You can fix the conflict.

To fix the conflict, you can follow the standard procedures for resolving merge conflicts from the command line. When you're finished, you'll need to call git rebase --continue in order for Git to continue processing the rest of the rebase.

Upvotes: -1

dahlbyk
dahlbyk

Reputation: 77540

I do not care about the history of the commits.

If you really don't care about history, a git merge --squash is going to let you resolve all your conflicts at once and produce a single commit with all the changes.

To essentially do that --squash in-place, you could do something like this:

git branch -m feature feature-old
git checkout master -b feature
git merge --squash feature-old

After you resolve all conflicts (once), you will create a single commit on feature that has master as a parent.

That being said, I'm a fan of keeping history. Definitely try rerere first. You might also try an in-place rebase --interactive (e.g. git rebase -i $(git merge-base HEAD master)) to squash the fixup-type commits without completely eliminating all the discrete commits.

Upvotes: 10

torek
torek

Reputation: 488193

Your mental image is close, but let's make it exact:

...--o--*--A--B          <-- master
         \
          C--D--...--Z   <-- feature

This is what you have now: the name master points to tip commit B (with each single-letter or o or * representing a commit). Each commit points back (left-ish) to its previous commit. Two commits, A, and B, are only on master. A bunch of commits are only on feature, namely C through Z here. Commit * and all earlier (further-left) commits are on both branches.

What git rebase does is locate commit * by working backwards from commit Z and also from the tip of master. It then knows that it needs to copy commits C-Z. The copies are to start right after master. If we use C' to name the copy of C, D' for the copy of D, and so on, the final graph will look like this:

                C'-D'-...-Z'   <-- feature
               /
...--o--*--A--B                <-- master
         \
          C--D--...--Z         [abandoned]

As I think you have realized, each copy is made one commit at a time, by turning each commit (which is a complete snapshot) into a set-of-changes. To get what changed in C, Git does:

git diff <hash-of-*> <hash-of-C>

Git then tries to apply this change to the snapshot in B, but it doesn't apply easily, so Git tries doing a merge: it compares * to B and sees that it should change <div id="foo"> to <div id="bar">, according to that; but it should change <div id="foo"> <div id="fubar">, according to *-vs-C. That's the first merge conflict.

So, you resolve this and commit (well, git rebase --continue commits):

                C'       <-- HEAD (rebase in progress)
               /
...--o--*--A--B          <-- master
         \
          C--D--...--Z   <-- feature

and Git goes on to copy D, by diffing D vs C to get a patch.

This time, there should be no <div id="foo"> to <div id="fubar"> change, so you should not get a merge conflict here. To find out for sure, try git show-ing commit D, which likewise compares it to C, producing a diff.

You may, however, get bitten by white-space changes or similar. It's worth taking a close look at each git diff output, and checking on things like end-of-line attributes if you are using those (CRLF conversions). End of line stuff often affects every line in every file, but you can get cases of single-line problems, depending on who uses what editor, for instance.

I suspect a more likely problem: git diff is synchronizing on the wrong lines. It finds some trivial things that match up, such as lines that read }, and uses those to decide that two files are back in sync and then picks out "wrong changes". If this is the case, the cure—if there is one—is to instruct Git to use a smarter diff. In particular, the patience diff tries not to synchronize on trivial lines, but rather only on significant (i.e., non-recurring) lines. This may or may not actually help—running git diff --diff-algorithm=patience (or git show with the same argument) on your commits may tell you. Whether you can get rebase to use a different diff algorithm depends on your Git version.

Aside from the mystery of why Git seems to be repeating the change, when it should have only other changes in C-vs-D, one thing you can do to help out is to use git rerere to get Git to re-use a recorded resolution (hence the name). Basically, when Git hits a merge conflict, if rerere is enabled, Git writes the conflicting parts into its database, and then when you git add the resolved file, Git writes the resolution (paired with the original conflict). Then, the next time Git hits a merge conflict, it checks the saved rerere data: if the conflict is for the same patch, it pulls out the recorded resolution, and uses that without interacting with you.

You can indeed squash some or all feature commits together, by rebasing feature atop commit * (using interactive rebase). Since these patches won't conflict with themselves—at least as long as they are played back "in order"—that can let you reduce the number of commits that require resolving. Whether you should do this is up to you: in general, if you are rebasing, you get new replacement commits anyway, so you might as well make the new ones "as pretty as possible" for future debugging or other code exploration. Clearly, it would be better to have five "does something interesting, but pretty much stand-alone" commits instead of 50 "do fragment of one thing, do another fragment, fix previous fragment, do another fragment, OK that thing is done move to next thing, oops fix tiny error, ..." so those are very good candidates for squashing-together. But, it's usually better to have five "does one thing" commits than one "does five things" commits: it means that if there is a bug in one of the five, you can fix that without worrying about the other four.

Upvotes: 23

Related Questions