I have a git issue I've never encountered before.
I have 2 branches that have 2 files that differ.
If I merge one into the other (in either direction) it says 'Already up-to-date.'
Why does it think they are up to date, and how can I get it to merge?
I read this post git merge does not merge (and others) and they all say "If it says up to date then you have merged correctly."
I realize git thinks they are merged, but they are not - I have files in the 2 branches that differ.
Even if I go in and make changes and I merge it still says 'Already up-to-date.'
Can I get git to "forget" I have merged them and it to merge again?
I don't know what happened to get in this state, but it's been a nightmare. There must be some way to fix this.
Update with what I did:
I am the only one working on this. I was asked to add some new features so I made a branch off master, new_features, and started work on them. Then I was told to put that aside and work on higher priority new features, so I made another branch off master, new_features2, and I started working on those. Then I was told we needed some bug_fixes, and I made another branch off master, bug_fixes. I fixed the bugs and merged bug_fixes into master. Then I went back to working new_features2, got that stuff working, merged master into new_features2, tested, then merged new_features2 into master. All good so far.
Now I'm back to working in new_features. I get that stuff done, and merge master into new_features. It does merge some stuff. I test, and I find that neither the new_features2 stuff nor the bug_fixes are working quite right. I look at the merged code and it seems some of the changes from new_features2 and bug_fixes are in new_features, but not all of them. I try to merge again - it says "Already up-to-date."
I'm putting the rebase section at the top, but you may want to read the rest first.
One of the most useful things in git is to cherry-pick an entire "branchlet" (a string of commits ending in a branch tip) to, in effect, move them. That is, assuming you are now comfortable with cherry-picking (see below if needed), let's say you have something like this in your commit graph:
o--o <-- fix-bug-1234
/ \
...--o--o--o---o--o--o <-- develop
\
o--o--o--o <-- feature
You started work on feature
a while ago, then had to make some emergency changes on develop
and an emergency bug fix that is merged back in now; and now it is time to get back to working on feature
.
It sure would be nice, though, to work on a version of feature
that starts from the latest good stuff in develop
.
So let's do just that. We start by checking out a new feature branch based on the tip of develop
:
git checkout -b new-feature develop
giving us this:
o--o <-- fix-bug-1234
/ \
...--o--o--o---o--o--o <-- develop, HEAD->new-feature
\
o--o--o--o <-- feature
Now we simply cherry-pick in all four feature
commits. We could write feature~4..feature
but that requires that we count up the commits, so since we know all about git set notation,1 we use develop..feature
to find that very same set of commits. This allows us to use as our second git command:
git cherry-pick develop..feature
which copies those four commits:
o--o <-- fix-bug-1234
/ \
...--o--o--o---o--o--o <-- develop
\ \
\ o--o--o--o <-- HEAD->new-feature
\
o--o--o--o <-- feature
Now we rename feature
to old-feature
and then rename new-feature
to feature
:
git branch -m feature old-feature
git branch -m new-feature feature
Or we do all of this the easy way, using git rebase
. We don't have to start with a git checkout -b
, then a git cherry-pick
and two rename steps, we just do one git checkout
followed by one git rebase
:
git checkout feature
git rebase develop
The rebase
command uses our one argument, develop
, to:
develop..feature
develop
There are fancier forms of rebase
that let us separate the list-of-commits-to-cherry-pick from the place-they-go-after, but for the typical case, we do not need these.
Note that when we do a rebase, the rebase command normally omits merge commits from the copying, but copies commits "behind" the merge commits unless they are excluded by the set notation (the develop..feature
stuff). When there are merge commits in the sequence this can produce surprising results.
In addition, since rebasing copies commits—it really is a series of cherry pick operations—you are only 100% safe to do this if you are the only person with the originals of those commits. If someone sharing your work (via fetch or push) has the originals as well, they may be using them and they may re-introduce those originals alongside your rebased copies. If you are the only one with the originals, this is not a problem. If those others who do have copies know all about your rebased copies, they can deal with the issue themselves (although they may not want to).
1If you don't know about this set notation, you are missing something that makes life much easier in git.
You are missing the point of a merge—or perhaps it would be more accurate to say that the point of a merge is missing what you would like done.
Let's take a look at the point of merge in the first place.
Suppose you and Alice are working on two different things. You are supposed to get widgets to slice both left and right, rather than slicing rightwards only. Alice is supposed to make crossbars hold up better.
Both of you start from a common base, where widgets only slice rightwards, and crossbars break.
This week you managed to make the widgets slice leftwards, and Alice fixed crossbars. So you (or Alice) now want to combine your work and her work:
$ git checkout larry-widget-fixes
$ git merge alice-crossbar-fixes
If this made all of your files match Alice's files exactly, that would undo all your work from this week.
Therefore, git merge
does not make things match up. Instead, it goes back to the common base to see (1) what you changed, and (2) what Alice changed. It then tries to combine these two sets of changes into one combined (merged) change.
In this case, it does so by adding her changes to yours (because we merged her work into yours, rather than your work into hers).
Note that the merge base—the common starting point—is one of the key items here.
Let's say you don't want to combine some changes. You want instead to make some file(s) match.
There are a lot of ways to do this, depending on just what results you want. Let's look at three of these ways.
Just make one file match, by extracting the other version.
Let's say that while fixing widgets, you accidentally broke shims. Alice's branch has a good copy of shims.data
(as does the merge base, for that matter, but let's just use Alice's here):
git checkout alice-crossbar-fixes -- shims.data
This extracts the version of file shims.data
from the tip of Alice's branch, and now you have the one you want.
Just un-do one commit, by commit ID.
Let's say that you realize you broke the shims in commit badf00d
, and all the other changes you made there should be undone too, or it's OK to undo them. You can then run:
git revert badf00d
The git revert
command turns the given commit into a patch, then reverse-applies the patch so that the effect of that commit is un-done, and makes a new commit out of the result.
You want to give up on everything entirely. You realize that everything you did this week, while it makes widgets slice leftwards as well as rightwards, is a mistake because widgets should not slice rightwards at all. Here you can use git reset
.
I won't give an example since this (a) this is probably not what you want; (b) git reset
is overly complicated (the git folks stuffed about five to eight logically-different operations into one command); and (c) there are plenty of good examples on StackOverflow already. (This is true for git revert
in step 2 as well, but git revert
is a lot simpler so I left it in.)
This is more complicated.
Remember that when we did the merge in the first example, we had a common starting point, at the beginning of the week before you and Alice started different tasks:
...--o--*--o--o--o <-- larry-widget-fixes
\
o-o-o-o-o <-- alice-crossbar-fixes
The common merge base is commit *
.
Once you finish your merge, though, we have this (I labeled the merge commit M1
to make it easy to see):
...--o--o--o--o--o--M1 <-- larry-widget-fixes
\ /
o-o-o-o-o <-- alice-crossbar-fixes
Now suppose Alice makes some more fixes, and let's have you make one more change too:
...--o--o--o--o--o--M1-o <-- larry-widget-fixes
\ /
o-o-o-o-*--o--o <-- alice-crossbar-fixes
Now you go to pick these up by merging again:
git merge alice-crossbar-fixes
and git finds the common merge base. This is the nearest commit that is on both branches, which I marked with *
again. Git will find out what Alice changed since *
, and what you changed since *
, and try to combine those changes. You already picked up all of Alice's earlier changes—they are in your branch already, in M1
—so it is a good thing that git does not look at those again. Git will just try to add Alice's new changes since *
to your changes since *
(your version of Alice's changes).
But what if you got them wrong? What if your version of Alice's changes, in M1
, are a bunch of wrong files?
At this point you must choose whether to retry the merge, or to use git's many other tools to fix things up.
To retry the merge, you must back up to a point before the merge happened. For instance, suppose we draw a new arrow to the commit just before M1
, like so:
..........<-- retry
.
...--o--o--o--o--o--M1-o <-- larry-widget-fixes
\ /
o-o-o-o-o--o--o <-- alice-crossbar-fixes
(imagine it's an arrow rather than just some crappy ASCII dots, anyway). We can leave the existing branch labels in there, pointing where they do, we just add this new retry
, by doing:
git branch retry <commit-id> && git checkout retry
or:
git checkout -b retry <commit-id>
(these both essentially do the same thing, giving us retry
pointing to the desired commit). Now if we do:
git merge alice-crossbar-fixes
git will try to merge the commit-before-M1
that retry
points to, with the (new, current) tip of alice-crossbar-fixes
. Git will once again find a merge base, which is the nearest commit on both of these branches, which is our original merge base again.
In other words, we will re-do the merge, ignoring the bad merge M1
, but also ignoring new work done since then.
We can git checkout -b <commit>
on any particular commit to get back to that state, and then start doing things like git cherry-pick
to re-add specific commits.
Let's say we like the commit-before-M1
a lot (it looks really close), so we make our new branch—let's call it something other than retry
—that points here:
git checkout -b rework <commit-ID>
Now we can pick and choose from other commits, extracting just the changes they made, using git cherry-pick
. We give it more commit IDs, more of those 197a3c2...
strings, and it compares the state just before that commit to the state at that commit. The result is a patch, but in fact, it's somewhat better than a regular diff-only patch, for two reasons:
Leaving out the second advantage and just thinking about it as a patch, what git cherry-pick
does is to apply the patch to the current commit, then make a new commit, re-using the original commit message.
If you would like to re-apply many commits this way, you can give git cherry-pick
multiple commits and it will do them all:
git cherry-pick xbranch~3..xbranch
This applies every commit that is on xbranch
except for commits starting at xbranch~3
and earlier, i.e., we count back three steps along xbranch
(assuming no merges...) and apply the remaining three commits:
N N Y Y Y [do we take it?]
| | | | |
v v v v v
...-o-o--o-o-o <-- xbranch
...-o--o <-- larry-widget-fixes (before cherry pick)
(and see rebasing, at the top)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With