Home > Enterprise >  How to merge/cherry-pick a single commit
How to merge/cherry-pick a single commit

Time:07-01

I've got a project (GitHub-based) with a branch structure like so

master:   A - B - C - D
            \
release:      W - X - M
                    /
bugfix:       Y - Z

Thus once bugfix is merged, release will have A - W - X - Y - Z - M, where M is the merge commit and Y & Z are the original commits from the bugfix branch. All pretty normal so far.

Now, what I want to do is get M along with Y and Z into master. But here's the caveat - I do not want W or X in master. Why? W and X are commits like "Bump to our hotfix release version" that have no place in master. So in the end, I want master to be A - B - C - D - Y - Z - M.

If I git cherry-pick on M, I just get M and I lose the history of Y and Z. This is what I was doing, but then I lose functionality in git blame in the future, and authors end up getting hidden (since GitHub commits a merge as authored by the person who merged it, not the original submitter).

If I git merge on M, I get the entire branch including W and X that I don't want.

How can this be accomplished?


Let me clarify the X to my Y a bit more:

Our project is entirely GitHub based using a fork-and-PR model (as is normal there).

What's before A doesn't matter; our release branch is based on A and then we added W and X to it to make our release (the tag is on X).

So Random Contributor Alice makes a bugfix coming from and targeting our Release branch.

release:      W - X
                    \
bugfix:               Y - Z

Our team member Bob merges that pull request into our Release branch.

release:      W - X   - - -  M
                    \       /
bugfix:               Y - Z

And importantly, I, or anyone on the project, can't touch bugfix once it's merged. It's on Alice's fork of the project, and we have this in our Release branch:

A - W - X - Y - Z - M

And since we made our Release branch, new commits (B, C, D) have been added to our Master branch.

Now, and this is the crux of my question: I want to get the entire history of that Pull Request - the original commits from Alice (Y and Z) and the merge details from Bob (via merge commit M) - into our master branch.

I was quite happy to just cherry-pick the merge, but because of the idosyncracies of how GitHub handles PR merges, that means that all trace of Alice (and her commits) are lost if I do so. This is what we want to avoid.

CodePudding user response:

Your diagram is missing the history (i.e., the commits) that come before A and Y, but usually the thing to do is merge the bug fixes directly into all affected branches. So here you would git switch master then git merge bugfix.

(I also recommend putting the branch names in as entities with arrows that point to the last commit that is currently on the branch, since that's how the names actually work in Git. Updating a branch by adding more commits simply moves the arrow along to the new last commit. If you "reset a commit away", that moves the arrow backwards—the way the commit-to-commit arrows go, in Git—to the older commit, dropping the newer commit(s) off the end of the branch.)

CodePudding user response:

Ok, it's not really an answer, but a workaround to the original cherry-pick idea.

hub pr gives details about who submitted the PR, so at the very least, I can alter the cherry-pick commit to show who wrote the PR. I guess that will have to be enough because it is indeed clear this can't ever work the way I had hoped.

For anyone curious, this is for Jellyfin, and here's the script we use to backport the PRs from a release branch back to master, with the tweak to show the author of the PR too. Hopefully then another project won't have to go blind into this.

https://github.com/jellyfin/jellyfin-build/blob/master/backport-prs#L82

CodePudding user response:

First a clarification about this statement:

I do not want W or X in master. Why? W and X are commits like "Bump to our hotfix release version" that have no place in master.

Typically, it's not that you don't want those commits in master, but instead it's that you don't want the changes from those commits in master. Given this, if it were me (and I do this), I would merge release back into master! There are 2 ways to do it so that you can accomplish what you want and still achieve all the other benefits of merging the entire release branch back into master, which include:

  • Suppose it was a really bad day/week and you have 10 bug fixes on the release branch. Do you really want to merge them all back individually? (Probably not.)
  • It's nice to not have to keep old release branches around if you don't need them. If you are able to merge your release branches into master you can delete them. All of the information you need about any prior release is contained in master.
  • The commit IDs that were actually released are reachable by master, along with any associated tags. (IMHO this is the most important reason, and is why I want all commits leading up to a release to end up in master.)

So, how do you do this without pulling in the version-only changes in your commits labeled W and X? Here are 2 ways:

  1. You can do exactly two merges. First you merge in X but you discard the changes using the -s ours option. Then you merge in release. The end result would have everything on release except for the changes in W and X. For example:
git switch master
git merge -s ours X # or the tag name you used here
git merge release

Note that -s ours should not be confused with -X ours (or theirs) which is something entirely different and is about automatic conflict resolution. I would consider -s ours merges unusual since it completely ignores all changes getting merged in, and such I recommend with the first merge you put a comment in the commit message saying something to the effect of "Merge and ignore release version changes" so it's clear what that merge is for.

  1. The other way to go is to just do one merge of the release branch, but stop before committing to undo the version changes. This can be easily automated in a script. For example:
git switch master
git merge release --no-commit
# now the version file changes will be staged, so unstage them:
git reset some-path/some-version-file
# now restore the version file to the version on the master branch:
git restore some-path/some-version-file
# repeat the reset and restore commands for all version files modified in W and X
...
# and finally complete the merge
git commit # or git merge --continue

I personally do this second option. (My script undoes 8 version files that are auto-updated by a build.) The reason I prefer option 2 is sometimes my release branch has multiple builds on it, so I would need to do the double merge multiple times, alternating between -s ours merges and actual merges to get the entire contents of the release branch back into master. But if I always had just 1 version up-rev at the beginning like you described, I probably wouldn't have taken the time to automate undoing the version files like I did.

  • Related