I created an interesting problem for myself last night while working on some code for personal use. I’ll state upfront that the code is not public so I didn’t have to worry about screwing things up for other people. Just myself, though the goal was not to. Following is a description of the circumstances and how I used git to rectify the situation.
First, I have a repository called origin/dev
with the following commit history:
...-> A -> B -> C (origin/dev)
Obviously, there’s previous history, but that’s not germane here. In this case, A
will be the first common ancestor that everything starts from.
Next, the dev
branch in my working repository was as follows:
...-> A -> B -> C -> D -> E -> F (dev)
While doing some testing, I came across a bug that was a result of “sins of omission.” Basically, I made changes back in commit B
and missed a line of code that should also have been modified for that commit. And here, at last, is where I created my problem because of how my mind works on these things.
I could just fix the bug and create commit G
off it, but I really didn’t want to do that. It just didn’t sit well with me. What I wanted to do is fix commit B
and then bring that forward. I knew that, code wise, this should be completely doable because the fix involved one line of code in one file that would not interfere with any of the commits following B
. So what I wanted to to was end up with a commit history like so:
...-> A -> B' -> C' -> D' -> E' -> F' (dev)
The solution for this ended up being to use git’s rebase functionality. Or, at least, that’s how I was able to tackle the problem. I proceeded as follows:
git checkout -b bugfix B
This creates and checks out a new branch called bugfix
based on commit B
of my current branch dev
. Following this, I made my correction and then performed an amend
commit:
git commit --amend -a
I updated the commit message, leaving me with the following history:
...-> A -> B -> C -> D -> E -> F (dev)
\
B' (bugfix)
Here’s where the rebase comes in, though admittedly I wasn’t entirely sure it would do what I wanted at first. In fact, I created test branches to prove this whole process out prior to performing it on the real thing (One of the nice things I’ve learned about git is that branches are cheap and easy.) The rebase command was as follows:
git rebase bugfix dev
This version of the command causes git to checkout the dev
branch prior to performing the rebase. The result was exactly what I wanted, a modified history with that one change added to the B
commit which propagated through all subsequent commits to the current one:
...-> A -> B' -> C' -> D' -> E' -> F' (dev)
At this point, I’m only half way there. Recall, this is a local repository. My origin/dev
repository that I push to still needed to be dealt with. Namely, the unmodified B
and C
commits. I suspected (and confirmed by attempting) I could not just push my changes up to my main repository- fast-forwarding was not possible. I didn’t want to muck with the main repository by removing the offending commits because that would cause problems with other copies of the repository, like the one I have on my laptop. Plus, that’s just bad practice. If this were a public code base, other users might get hosed by my mucking with the main repository.
I’ll also note quickly that I like the fact that there’s a force
option for the push. Or, more precisely, that it’s named force
. For myself, it’s an immediate flag that 99% of the time it’s a bad idea and should be avoided if possible. Other ways might be messier or more involved, but they’ll have other benefits. So no, I didn’t take that route.
Instead, thanks to the suggestions here, I performed a pull
and then a push
. The key is that when pulling from the main repo, I took advantage of the strategy
option like so:
git pull --strategy=ours
The result of this was a new commit history as follows:
...-> A -> B -> C -> B' -> C' -> D' -> E' -> F' (dev)
See the git documentation for an explanation of the “ours” strategy. The gist is that it fixes the merge game so that the current branch ends up as the main history.
I was then able to immediately push
back up to the main repository, origin/dev
. I wasn’t initially crazy about the history, but then I realized I really didn’t have a choice here. Anything else was going to cause problems when I updated the repository on my laptop. Or at least, to the best of my understanding. That’s the thing about the git tool that I’m understanding better now- how much power it gives you over the project metadata. There probably are other approaches to solve the problem, but in the end I was satisfied with these results.