In my codebase we have two branches (relevant to this question) we have main
and TestFlight
.
When we want to make a release, we do the following
$ git checkout main
$ git pull
$ git checkout TestFlight
$ git pull
$ git merge main --ff-only
$ git push
Before I do this, I make a pull request on GitHub for the changes between main
and TestFlight
and just keep it open. After running those commands, GitHub figures out that it has been merged and closed the PR as merged. I am not sure if this matters.
Now in our latest release I wanted to do something a bit different. I had two commits that were on main that I wanted to have included in TestFlight. I did not, however, want any of the other commits from main.
What I did was cherry-picked those two commits onto TestFlight and pushed. This kicked off the build in Xcode Cloud that I wanted.
The problem was afterwards. I tried following the normal procedure of opening a pull request, then running those steps. However, when I tried to do the merge git complained saying fatal: Not possible to fast-forward, aborting.
I am assuming because there was no linear path to get between the two branches.
So I inspected the branches in a visual git client. What I noticed is that this procedure resulted in the TestFlight branch having all the commits look like they "belonged" to the TestFlight branch, rather than what it normally looks like when you merge in a branch. So I cherry-picked every single one of the remaining commits in main
into TestFlight
one by one and pushed.
At this point, the branches match. And the two branches have the commits, although they are in a different order.
The problem is now that I have clearly ended up in some sort of different state because when I try to look at changes between main and TestFlight in GitHub to open that pull request, GitHub lists all the commits since before I started doing any cherry-picking.
I am confused why this is the case. I realize the commits are in a different order, but to me in my visual git client the branches look exactly like I would expect minus the fact that they are in a different order.
How come my attempt at a fix (cherry-picking each commit manually one by one) did not work? And how can I get back into a steady state so that the next time we want to deploy we can follow the normal procedure?
When you cherry-pick a commit, you're not copying the commit from a branch to another, but you're rather rewriting it as a new one with the same set of changes but different SHA. This means that if you have a commit A
on branch main
with id xyz
, and then you cherry-pick it into branch TestFlight
, you basically end up with a commit A'
, that brings the same set of changes of A
, but that is identified with a different id abc
.
... -- C -- B -- A main
\
-- A' TestFlight
A
and A'
are different commits, and even though they represent the same set of modifications, this is not enough for Git to perform a fast-forward merge. A fast-forward merge can occur only if one branch is a subset of the other, meaning that one branch contains the same history of commits of the other one (commits with same id) plus other commits. That's why the merge is called fast-forward, because the ref of the target branch is simply forwarded to the commit of the other branch (the one ahead).
In this scenario, TestFlight
is at commit C
while main is at commit A
.
... -- C -- B -- A
| |
TestFlight main
After a fast-forward merge, the ref of the branch TestFlight
is simply moved ahead, pointing to the same commit of main
.
... -- C -- B -- A
|
main
TestFlight
In your case, you could bring yourself to a situation where you're able to fast-forward again by:
Resetting TestFlight
to the second cherry-picked commit that you were actually interested in.
Merge main
into TestFlight
with the ours
strategy to retain only TestFlight
's version, while marking the two branches as merged.
Finally, force push TestFlight
to the remote.
So, assuming that C1'
and C2'
are the ids of the two commits originally cherry-picked, you could perform:
# make sure to be on TestFlight
git checkout TestFlight
# hard reset TestFlight to the second commit originally cherry-picked
git reset --hard C2
# merge main into TestFlight, but completely disregard main's changes,
# while maintaining only TestFlight's version.
# This not only will leave you with only TestFlight's changes, but will also mark
# the two branches as merged, bringing you to a state where if main advances
# with new commits, TestFlight can be fast-forwarded to main.
# This is because the current merge commit will be used as a new merge-base
# for future merges between the two branches.
git merge -s ours main
# force push TestFlight to its corresponding remote branch
git push --force origin TestFlight