How can I selectively merge or pick changes from another branch in Git?

Viewed 975521

I'm using Git on a new project that has two parallel -- but currently experimental -- development branches:

  • master: import of existing codebase plus a few modifications that I'm generally sure of
  • exp1: experimental branch #1
  • exp2: experimental branch #2

exp1 and exp2 represent two very different architectural approaches. Until I get further along I have no way of knowing which one (if either) will work. As I make progress in one branch I sometimes have edits that would be useful in the other branch and would like to merge just those.

What is the best way to merge selective changes from one development branch to another while leaving behind everything else?

Approaches I've considered:

  1. git merge --no-commit followed by manual unstaging of a large number of edits that I don't want to make common between the branches.

  2. Manual copying of common files into a temporary directory followed by git checkout to move to the other branch and then more manual copying out of the temporary directory into the working tree.

  3. A variation on the above. Abandon the exp branches for now and use two additional local repositories for experimentation. This makes the manual copying of files much more straightforward.

All three of these approaches seem tedious and error-prone. I'm hoping there is a better approach; something akin to a filter path parameter that would make git-merge more selective.

28 Answers

You use the cherry-pick command to get individual commits from one branch.

If the change(s) you want are not in individual commits, then use the method shown here to split the commit into individual commits. Roughly speaking, you use git rebase -i to get the original commit to edit, then git reset HEAD^ to selectively revert changes, then git commit to commit that bit as a new commit in the history.

There is another nice method here in Red Hat Magazine, where they use git add --patch or possibly git add --interactive which allows you to add just parts of a hunk, if you want to split different changes to an individual file (search in that page for "split").

Having split the changes, you can now cherry-pick just the ones you want.

I don't like the above approaches. Using cherry-pick is great for picking a single change, but it is a pain if you want to bring in all the changes except for some bad ones. Here is my approach.

There is no --interactive argument you can pass to git merge.

Here is the alternative:

You have some changes in branch 'feature' and you want to bring some but not all of them over to 'master' in a not sloppy way (i.e. you don't want to cherry pick and commit each one)

git checkout feature
git checkout -b temp
git rebase -i master

# Above will drop you in an editor and pick the changes you want ala:
pick 7266df7 First change
pick 1b3f7df Another change
pick 5bbf56f Last change

# Rebase b44c147..5bbf56f onto b44c147
#
# Commands:
# pick = use commit
# edit = use commit, but stop for amending
# squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

git checkout master
git pull . temp
git branch -d temp

So just wrap that in a shell script, change master into $to and change feature into $from and you are good to go:

#!/bin/bash
# git-interactive-merge
from=$1
to=$2
git checkout $from
git checkout -b ${from}_tmp
git rebase -i $to
# Above will drop you in an editor and pick the changes you want
git checkout $to
git pull . ${from}_tmp
git branch -d ${from}_tmp

1800 INFORMATION's answer is completely correct. As someone new to Git, though, "use git cherry-pick" wasn't enough for me to figure this out without a bit more digging on the Internet, so I thought I'd post a more detailed guide in case anyone else is in a similar boat.

My use case was wanting to selectively pull changes from someone else's GitHub branch into my own. If you already have a local branch with the changes, you only need to do steps 2 and 5-7.

  1. Create (if not created) a local branch with the changes you want to bring in.

    $ git branch mybranch <base branch>

  2. Switch into it.

    $ git checkout mybranch

  3. Pull down the changes you want from the other person's account. If you haven't already, you'll want to add them as a remote.

    $ git remote add repos-w-changes <git url>

  4. Pull down everything from their branch.

    $ git pull repos-w-changes branch-i-want

  5. View the commit logs to see which changes you want:

    $ git log

  6. Switch back to the branch you want to pull the changes into.

    $ git checkout originalbranch

  7. Cherry pick your commits, one by one, with the hashes.

    $ git cherry-pick -x hash-of-commit

This is my workflow for merging selective files.

# Make a new branch (this will be temporary)
git checkout -b newbranch

# Grab the changes
git merge --no-commit  featurebranch

# Unstage those changes
git reset HEAD
(You can now see the files from the merge are unstaged)

# Now you can chose which files are to be merged.
git add -p

# Remember to "git add" any new files you wish to keep
git commit

For me, git reset --soft branch is the easiest way to selectively pick the changes from another branch, since, this command puts in my working tree, all the diff changes, and I can easily pick or revert which one I need.

In this way, I have full control over the committed files.

A simple approach for selective merging/committing by file:

git checkout dstBranch
git merge srcBranch

// Make changes, including resolving conflicts to single files
git add singleFile1 singleFile2
git commit -m "message specific to a few files"
git reset --hard # Blow away uncommitted changes

If you don't have too many files that have changed, this will leave you with no extra commits.

1. Duplicate branch temporarily
$ git checkout -b temp_branch

2. Reset to last wanted commit
$ git reset --hard HEAD~n, where n is the number of commits you need to go back

3. Checkout each file from original branch
$ git checkout origin/original_branch filename.ext

Now you can commit and force push (to overwrite remote), if needed.

If you only need to merge a particular directory and leave everything else intact and yet preserve history, you could possibly try this... create a new target-branch off of the master before you experiment.

The steps below assume you have two branches target-branch and source-branch, and the directory dir-to-merge that you want to merge is in the source-branch. Also assume you have other directories like dir-to-retain in the target that you don't want to change and retain history. Also, assumes there are merge conflicts in the dir-to-merge.

git checkout target-branch
git merge --no-ff --no-commit -X theirs source-branch
# the option "-X theirs", will pick theirs when there is a conflict. 
# the options "--no--ff --no-commit" prevent a commit after a merge, and give you an opportunity to fix other directories you want to retain, before you commit this merge.

# the above, would have messed up the other directories that you want to retain.
# so you need to reset them for every directory that you want to retain.
git reset HEAD dir-to-retain
# verify everything and commit.

I'm going to concentrate on the subset of this problem that was of interest to me: I have two branches and I want to pseudo-merge one file from one into the other.

(I say "pseudo-merge" because I don't need or want a merge commit; I just want to combine the contributions of both versions of the file, in the way that I see fit.)

My approach is based on the approach taken in https://stackoverflow.com/a/39916536/341994. Unfortunately that question is closed as a duplicate (wrongly, in my opinion: it's not a duplicate of this question, and it is wrong to answer-and-close-as-duplicate which is what the answerer did there). But there are some things wrong with that answer, so I've modernized and cleaned up the approach. Instead of checkout and reset, I use restore, and I don't bother to commit anything I don't have to.

Okay, so imagine I have three files:

$ ls
a   b   f

But I only want to pseudo-merge one of them, a, from otherbranch. Let's peek at them to see what the situation will look like. Here's my version:

$ cat a
line one
line two
line three
line four
line five

Here's otherbranch's version:

$ git show otherbranch:a
line one
line two edited
line three
line four
line five
line six

Now the trick here is that we're going to use the index as a scratch pad (which is, after all, what it is for). So we start (STEP 1) by making sure that our version is copied into the index:

$ git add a

Now (STEP 2) we can use restore to fetch the version from otherbranch (nowadays, restore is better than checkout as it lets us talk more clearly):

$ git restore --source otherbranch a

At first blush, this looks bad. We have now completely overwritten our a with the version from otherbranch, as you can see:

$ cat a
line one
line two edited
line three
line four
line five
line six

But not to worry! The previous version of a is still in the index, as you can see:

$ git diff a
diff --git a/a b/a
index abf51fa..333614b 100644
--- a/a
+++ b/a
@@ -1,6 +1,7 @@
 line one
-line two
+line two edited
 line three
 line four
 line five
+line six

Very well, now we're ready for the key move (STEP 3). We do an interactive patch add of our file from the working tree to the index.

We could say git add -p a to start the interactive patch process, in which case we are fed hunks one at a time. But in this case there is just one hunk, and I want to edit it anyway, so I say:

$ git add --e a

The result is that we open a patch file of diffs in our editor! It looks like this:

 line one
-line two
+line two edited
 line three
 line four
 line five
+line six

By careful editing we can now decide what parts we want to accept and what parts we don't. Let's accept "line six" but not "line two edited". So we edit to look like this:

 line one
 line two
 line three
 line four
 line five
+line six

We close the editor and the patch is applied to the index version of a. But we are not quite finished! The otherbranch version of a is still sitting in the working tree:

$ cat a
line one
line two edited
line three
line four
line five
line six

The version we like is in the index, remember? To get it, (STEP 4) we just call git restore plain and simple (again, this the modern way; restore is nicer than reset and can apply to a single file):

$ git restore a

Now our a is correct, and we're all finished:

$ cat a
line one
line two
line three
line four
line five
line six

We could at this point commit, but we don't have to; we've accomplished what we set out to accomplish.

What I want: Interactively pick hunks from a branch (which had several messy commits) into a clean commit in a new branch.

git diff + git apply won't work if you had any binary files in that diff.

My approach:

# New branch from a clean starting point, e.g. master
git checkout new-clean-branch origin/master

# Get all changes from the messy branch
# (quote the star so your shell doesn't expand it)
git checkout messy-branch -- '*'

# Unstage ("un-add") everything
git restore --staged .

# Interactively add hunks to be staged for commit
git add -p

If you are a Gitkraken User here you have a small guide

In summary:

  1. move to the branch which you want to bring the changes. (e.g. develop)
  2. right click on the branch that has the new changes and choose the "cherrypick commit" option (e.g. feature-ABC).
  3. Finally Accept and check for conflicts if any.
Related