0

We have been working on a work branch for some time now, with the intention of bringing it back to my main branch at some point.

In the mean time, that work branch has been kept up-to-date by frequent merges from the main branch.

Now it turns out we only want to bring some of the changes from that work branch, but not yet all, to the main branch.

What is difficult here, is that those changes are not at all nicely separated by commit or by file, because it was never expected to have to do this. Instead, individual commits bring changes we want on my main, as well as changes we don't want on my main branch. It possible to see instances of both kinds of changes within any given single file.

Now, there is still a possibility that in the end, we will want to merge that work branch definitively back in the main branch, as originally planned. So I need to keep this possibility open, and it needs to offer a good guarantee that everything has indeed been merged as planned - or at least a good way to check.

Because of that, it feels that git cherry-pick (assuming it can go down to the level of individual hunks within files) is not the option I want, because then all the original commits from the work branch on the one hand, and the commits generated by cherry-picking on the main branch on the other hand will all be dumb conflicts where it will look like both branches bring the same change. Also the log will look totally ugly with duplicate log comments.

I would much rather have a single commit saying "Bring all changes related to X and Y but not Z from work branch.". And then sometime later, if we so choose, another commit saying "Bring remaining changes from work branch, mainly about Z.". I guess ideally that last one would have the two parents, but I don't know what would be ideal for the first one : should it have some commit in the work branch as its parent ? So merge-like rather than cherry-pick-like.

At the same time, if I decide to go for an actual merge to create the first commit, I will have to manually remove stuff in the code on the main branch to ignore changes about Z that would have come from the work branch via this merge. However, since that first commit would have the tip of the work branch as one of its parents (this being an actual merge), it would appear to git that that first commit contains all the changes from the work branch - but that wouldn't be true.

It may be that I put myself in a situation that git cannot solve cleanly or natively, and if that is the case, I would like to know, and I would be fine with it. I would still be interested in other degraded solutions which would break some of my requirements.

Charles
  • 988
  • 1
  • 11
  • 28
  • You can only automatically merge (and cherry-pick) _commits_ with Git. If you need anything more fine-grained (be it directories, files, or hunks), you need to do it manually. – knittl Apr 24 '23 at 10:19
  • It’s important in this context to think of what you are merging in as a single snapshot, not as a bunch of changes that are built on top of each other (since commits are snapshots and not patches). That’s why I think [this](https://stackoverflow.com/a/76090934/1725151) answer is correct. – Guildenstern Apr 24 '23 at 11:37

4 Answers4

4

Your best option IMO is:

  • Make one commit on the feature branch that undoes the unwanted changes.
  • Merge the result to main.
  • Revert the earlier commit on the feature branch to bring back the unwanted changes.

The reverted changes can now live on the feature branch until decision is made to merge them as well, or they can be abandoned if they remain unwanted.

j6t
  • 9,150
  • 1
  • 15
  • 35
  • This will result in likely merge conflicts if a second attempt is made to merge the feature branch into main. – matt Apr 24 '23 at 11:18
  • You mean a third attempt ? Since already in this solution we have : 1. One merge with the changes that we want today 2. One possible future merge with the changes that we do not want yet – Charles Apr 24 '23 at 11:44
  • 2
    @matt There will only be merge conflicts that would also occur without the undo- and revert-commits, but no additional conflicts. – j6t Apr 24 '23 at 11:50
  • Agreed, I'm just warning against merging the same branch twice, in general. – matt Apr 24 '23 at 11:52
  • 2
    @matt There is nothing to be warned about merging a branch twice. It is totally normal. In fact, OP has done just that many times: main was merged into feature multiple times. There is nothing wrong in merging multiple times in the opposite direction. – j6t Apr 24 '23 at 11:55
  • My experience and understanding of Git aligns with @j6t's statements here. – Charles Apr 24 '23 at 13:14
  • 1
    Merging the same branch twice will be a problem if the branch was rebased in the meantime or if its commits were amended. But then it technically isn't *really* the same branch, but an alternative commit history put in place of the original branch. Which would then conflict with the already merged history if merged as well. However in this specific answer no amending/rebasing was suggested, so there shouldn't be (such) conflicts :) – Jay Apr 25 '23 at 09:28
2

Approach 1

  • Merge current main into feature
  • Create and checkout a new branch partially-merge-feature-into-main based on current main
  • On partially-merge-feature-into-main use git checkout -p feature -- . to selectively choose which file change chunks to keep/edit and commit the changes.
  • Merge partially-merge-feature-into-main into feature and in case of conflicts always take "--ours" i.e. the version from feature.
    As a result the merge should not change anything at all.
    (But will it will allow you to later merge feature into branch without those conflicts re-occuring)
  • Merge partially-merge-feature-into-main (preferably with --no-ff so that people know what's going on there)

Approach 2

  • Create a commit on feature that removes everything that you don't want on main
    (Either completely manually or first merge main, then use git checkout -p main -- . to selectively discard unwanted changes/chunks, before committing)
  • Merge feature into main
  • On feature, revert the commit from first step to re-include the removed changes.
Kit
  • 20,354
  • 4
  • 60
  • 103
Jay
  • 3,640
  • 12
  • 17
1

Here's a completely different approach. Since your commit history is a mess already, abandon it and clean it up:

  • Rebase the branch onto main.
  • Reset (mixed) the branch back to main.
  • Use git add, possibly with patch, interactive, etc., to form the basis in the index of a single "good" commit consisting of the stuff you want to merge now. Make the commit.
  • Stash, so that git status reports clean.
  • Switch to main, merge the feature branch, and throw away this feature branch.

You could stop at this point, with the stash preserving the stuff you might or might not merge later. But if you intend further development of that material, then, still on main, make a new branch and switch to it, and pop the stash and add everything and commit. You now have a feature branch in waiting, consisting only of the stuff you might or might not merge to main later.

matt
  • 515,959
  • 87
  • 875
  • 1,141
0

Some alternatives:

Rebase

Interactively rebase the feature branch onto main, reviewing and modifying each commit.

Set every commit in your interactive rebase to "edit", except for the ones you know you're gonna ditch altogether (set these to "skip"). On each commit, the rebase pauses, giving you the opportunity to review the change (git show), and possible modify it (git reset HEAD~ might be handy to make all changes visible in the diff), before committing it (git commit --amend), then going on with the rebase --continue.

But if your branch history is a mess, you may want to leave it behind you anyway. Same if you think that there are more changes to discard, than changes to bring in.

Discard history and use a patch file

Save the whole of what happens in the feature branch with git diff main...feature > feature-changes.patch (NOTE! 3 dots!).

Review the content of this file. Make sure all changes are there. If you've merged main back to feature, you might need to manually find the commit of main, on which feature was originally based, and run git diff <that commit> feature > feature-changes.patch.

You now have all the changes of the feature branch in a patch file.

Modify the patch file so that only the hunks you want are left, then apply it to main (git checkout main then git apply feature-changes.patch).

Get changes through the file system

Checkout the branch, reset it to main, make a new commit.

git checkout feature updates the file system.

git checkout -b migrate-to-main lets you work on a new branch without risking losing feature.

git reset main moves your currently checked out branch (remember that branches are just labels pointing to a commit) to main, but keeps the content on the file system unmodified.

You are now on your new branch migrate-to-main, which points at the same commit as main, and your file system has the same content as in feature.

You can now review the diff, decide what to include and not, and can resolve possible conflicts there as well. You can do this manually, or use tools such as git-gui to select what you want to include or not.

When you're done, all that is left is committing.

Gauthier
  • 40,309
  • 11
  • 63
  • 97
  • Whatever we end up doing - yes, if we had had the faintest idea that we would ultimately want to extract some part of the changes, and not the rest - then we would have done it on two separate branches from the get go. – Charles Apr 26 '23 at 14:10
  • @Charles, sorry if that top comment sounded arrogant. We all deal with our own $hit all the time :) – Gauthier Apr 26 '23 at 16:45
  • No such thing as arrogance was felt on my end. I fully agree about the lessons learned here, which is what I commented myself. – Charles May 04 '23 at 09:08