How to cherry-pick a range of commits and merge them into another branch? [duplicate]
Asked Answered
R

12

877

I have the following repository layout:

  • master branch (production)
  • integration
  • working

What I want to achieve is to cherry-pick a range of commits from the working branch and merge it into the integration branch. I'm pretty new to git and I can't figure out how to exactly do this (the cherry-picking of commit ranges in one operation, not the merging) without messing the repository up. Any pointers or thoughts on this? Thanks!

Rumple answered 3/1, 2010 at 9:50 Comment(0)
C
1053

When it comes to a range of commits, cherry-picking is was impractical.

As mentioned below by Keith Kim, Git 1.7.2+ introduced the ability to cherry-pick a range of commits (but you still need to be aware of the consequence of cherry-picking for future merge)

git cherry-pick" learned to pick a range of commits
(e.g. "cherry-pick A..B" and "cherry-pick --stdin"), so did "git revert"; these do not support the nicer sequencing control "rebase [-i]" has, though.

damian comments and warns us:

In the "cherry-pick A..B" form, A should be older than B.
If they're the wrong order, the command will silently fail.

If you want to pick the range B through D (including B) that would be B~..D (instead of B..D).
See "Git create branch from range of previous commits?" for an illustration.

As Jubobs mentions in the comments:

This assumes that B is not a root commit; you'll get an "unknown revision" error otherwise.

Note: as of Git 2.9.x/2.10 (Q3 2016), you can cherry-pick a range of commits directly on an orphan branch (empty head): see "How to make an existing branch an orphan in Git".


Original answer (January 2010)

A rebase --onto would be better, where you replay the given range of commits on top of your integration branch, as Charles Bailey described here.
(also, look for "Here is how you would transplant a topic branch based on one branch to another" in the git rebase man page, to see a practical example of git rebase --onto)

If your current branch is integration:

# Checkout a new temporary branch at the current location
git checkout -b tmp

# Move the integration branch to the head of the new patchset
git branch -f integration last_SHA-1_of_working_branch_range

# Rebase the patchset onto tmp, the old location of integration
git rebase --onto tmp first_SHA-1_of_working_branch_range~1 integration

That will replay everything between:

  • after the parent of first_SHA-1_of_working_branch_range (hence the ~1): the first commit you want to replay
  • up to "integration" (which points to the last commit you intend to replay, from the working branch)

to "tmp" (which points to where integration was pointing before)

If there is any conflict when one of those commits is replayed:

  • either solve it and run "git rebase --continue".
  • or skip this patch, and instead run "git rebase --skip"
  • or cancel the all thing with a "git rebase --abort" (and put back the integration branch on the tmp branch)

After that rebase --onto, integration will be back at the last commit of the integration branch (that is "tmp" branch + all the replayed commits)

With cherry-picking or rebase --onto, do not forget it has consequences on subsequent merges, as described here.


A pure "cherry-pick" solution is discussed here, and would involve something like:

If you want to use a patch approach then "git format-patch|git am" and "git cherry" are your options.
Currently, git cherry-pick accepts only a single commit, but if you want to pick the range B through D that would be B^..D (actually B~..D, see below) in Git lingo, so:

git rev-list --reverse --topo-order B~..D | while read rev 
do 
  git cherry-pick $rev || break 
done 

But anyway, when you need to "replay" a range of commits, the word "replay" should push you to use the "rebase" feature of Git.


pridmorej objects in the comments:

WARNING: don't be fooled by the above suggestion of using carat (^) to make the range inclusive!

This does not include CommitId1 if, for instance, the parent of CommitId1 is a merge commit:git cherry-pick CommitId1^..CommitId99.
In that case, cherry-pick still starts from CommitId2 - no idea why, but that's the behavior I've experienced.

I did, however, discover that using tilde (~) works as expected, even when the parent of CommitId1 is a merge commit: git cherry-pick CommitId1~..CommitId99.

True: that highlights an important nuance in using Git's cherry-picking command with commit ranges, particularly when dealing with merge commits.

In Git, ^ and ~ have specific meanings when used with commit references:

  • ^ refers to the parent of a commit, and when used in a range, it can lead to unexpected results, especially with merge commits.
  • ~, on the other hand, is used to denote the first parent of a commit in a linear history, which makes it more predictable in the context of cherry-picking a range.

When cherry-picking a range of commits, especially in scenarios involving merge commits, prefer using ~ to make sure the range includes the intended commits.

So regarding git cherry-pick CommitId1^..CommitId99: When specifying a range CommitId1^..CommitId99, Git interprets this as "start from the parent of CommitId1 and include commits up to CommitId99".

  • If CommitId1 is a regular commit, its only parent (say CommitId0) becomes the start of the range, effectively excluding CommitId1 itself.
  • If CommitId1 is a merge commit, CommitId1^ still points to its first parent. That can be particularly confusing because merge commits by their nature merge two lines of development, and the first parent might not be intuitively the "previous" commit in a linear sense.

In non-linear histories involving merge commits, the first parent of a merge commit might not be the direct predecessor in the same branch.

The tilde (~) notation, when used as in CommitId1~..CommitId99, effectively means "include CommitId1 and go back one commit from there", which in most cases will include CommitId1 in the range, as intended.

Consider the following commit history, where M represents a merge commit, and each letter represents a different commit:

A---B---C-------D---E--CommitId99   <- master
     \         /
      X---Y---M                     <- feature (CommitId1 is M)
  • A, B, C, D, E are commits on the master branch.
  • X, Y are commits on a feature branch.
  • M is a merge commit on the feature branch, merging changes from master into feature. Here, M is CommitId1.

When you run the command:

git cherry-pick CommitId1^..CommitId99
  • CommitId1 is M.
  • CommitId1^ refers to the first parent of M.
  • In this case, the first parent of M is D because merge commits list their parents in the order they were merged.

So, the range CommitId1^..CommitId99 translates to D..CommitId99. M (CommitId1) is excluded!

But if you use git cherry-pick CommitId1~..CommitId99, when used in the context of a range, like CommitId1~..CommitId99, the interpretation is "start from the commit right before CommitId1 and include up to CommitId99."

So CommitId1~ refers to the commit right before M in the feature branch, which is Y (since M was created on the feature branch, by a merge from master to feature).
The range CommitId1~..CommitId99 translates to Y..CommitId99.

Using ~ in the range with git cherry-pick effectively shifts the start of the range to the commit right before CommitId1, thereby including CommitId1 in the cherry-picked range. That behavior is particularly useful when you want to include merge commits in your cherry-picking operation.

Copolymer answered 3/1, 2010 at 10:8 Comment(16)
If you have commits that have parents that require the -m option, how do you handle those commits? Or is there a way to filter out these commits?Bestiary
@Bestiary -m is supposed to handle them for you, by selecting the mainline referenced by the -m parameter you have chosen for this cherry-picking.Copolymer
The thing is if you are cherry picking a range of commits, it will cherry pick the parent commits correctly but then when it hits a normal commit, it fails and says commit is not a merge. I guess my question is better phrased how to make it pass the -m option only when it hits a parent commit when cherry-picking range of commits? Right now if I pass -m like git cherry-pick a87afaaf..asfa789 -m 1 it applies to all commits within the range.Bestiary
@Bestiary Strange, I did not reproduce the issue. What is your git version and whayt is the exact error message you see?Copolymer
Ah I'm running git version 2.6.4 (Apple Git-63). The error I see would be something like error: Commit 8fcaf3b61823c14674c841ea88c6067dfda3af48 is a merge but no -m option was given. I actually realized you could just git cherry-pick --continue and it would be fine (but it wouldn't include the parent commit)Bestiary
i just cherry-picked a range and it's not the same as individually applying cherry-pick to each commit..ran into weird conflict errorsUrus
@Urus Strange: maybe you could ask a new question with more details (git version, OS, git config -l, ...), to see if that behavior is expected or not?Copolymer
The way I understand it is this. If you want to move commits, you need to rebase. If you want to copy commits (leaving the original branch alone), you should use cherry-pick. I find that a cherry-pick followed by an interactive rebase is great for cleaning up work. Does that follow with everyone else's thinking?Edin
@Edin I don't see often a cherry-pick + rebase -i, but yes, that should work.Copolymer
Don't remember from which version, but git cherry-pick -<NUMBER_OF_COMMITS> <HASH_OF_LAST_COMMIT> works like a charm!Horrify
Is git branch -f integration last_SHA-1_of_working_branch_range necessary? Can't you just use the SHA-1's directly in the rebase command? git rebase --onto tmp first_SHA-1_of_working_branch_range~1 last_SHA-1_of_working_branch_rangeUnderpart
Update: This isn't possible since you'll be on a detached HEAD after.Underpart
@ClementHoang True: a rebase starts by switching to (git switch, no more checkout since Git 2.23: https://mcmap.net/q/12203/-39-git-checkout-39-docs-claim-working-tree-will-change-why-are-edits-not-discarded) the new upstream branch. If said upstream branch is not a branch but a SHA1...: detached HEAD.Copolymer
May you never have to itch again and may your eyes only see bushy-tailed rabbits. Thank you!Insensate
WARNING: don't be fooled by the above suggestion of using carat (^) to make the range inclusive! This does not include CommitId1 if, for instance, the parent of CommitId1 is a merge commit. git cherry-pick CommitId1^..CommitId99 In that case cherry-pick still starts from CommitId2 - no idea why, but that's the behaviour I've experienced. I did, however, discover that using tilde (~) works as expected, even when the parent of CommitId1 is a merge commit: git cherry-pick CommitId1~..CommitId99Nemato
@Nemato Excellent point, thank you! I have edited the answer accordingly, and I have added a study to explain and illustrate why ~ is better than ^ when cherry-picking a range of commits. Please let me know if that study makes sense for you.Copolymer
S
170

As of git v1.7.2 cherry pick can accept a range of commits:

git cherry-pick learned to pick a range of commits (e.g. cherry-pick A..B and cherry-pick --stdin), so did git revert; these do not support the nicer sequencing control rebase [-i] has, though.

As Gabe Moothart notes, cherry-pick A..B will not get commit A (you would need A~1..B for that), and if there are any conflicts git will not automatically continue like rebase does (at least as of 1.7.3.1).

Shoa answered 8/9, 2010 at 3:57 Comment(0)
P
117

Assume that you have 2 branches,

"branchA" : includes commits you want to copy (from "commitA" to "commitB"

"branchB" : the branch you want the commits to be transferred from "branchA"

1)

 git checkout <branchA>

2) get the IDs of "commitA" and "commitB"

3)

git checkout <branchB>

4)

git cherry-pick <commitA>^..<commitB>

5) In case you have a conflict, solve it and type

git cherry-pick --continue

to continue the cherry-pick process.

Purse answered 1/8, 2017 at 14:29 Comment(7)
What does the "^" do in the "git cherry-pick <commitA>^..<commitB>" command at 4)?Goaltender
@Goaltender stackoverflow.com/questions/2221658/…Valorous
Someone please edit the post that the cherry-pick range is NOT inclusive.Mullah
@Goaltender when you use cherry-pick without the ^ in the range, the first commit will not be includedTipton
@Goaltender ^ indicates the previous commit. As @Tomas pointed, cherry-pick's behavior leaves the commitA. So to include commitA, you need to choose the respective previous commit using commitA^Golding
For anyone else that isn't aware, if you're using zsh, you need to surround your range in quotes: git cherry-pick "<commitA>^..<commitB>"Ciracirca
@DrewDaniels I wonder the side effect of quoting the hash range on a non zsh system.Hausmann
M
37

Are you sure you don't want to actually merge the branches? If the working branch has some recent commits you don't want, you can just create a new branch with a HEAD at the point you want.

Now, if you really do want to cherry-pick a range of commits, for whatever reason, an elegant way to do this is to just pull of a patchset and apply it to your new integration branch:

git format-patch A..B
git checkout integration
git am *.patch

This is essentially what git-rebase is doing anyway, but without the need to play games. You can add --3way to git-am if you need to merge. Make sure there are no other *.patch files already in the directory where you do this, if you follow the instructions verbatim...

Maisel answered 4/1, 2010 at 9:32 Comment(1)
Note that, same as with other revision ranges, it needs to be A^ to include A.Holleyholli
C
27

git cherry-pick start_commit_sha_id^..end_commit_sha_id

e.g. git cherry-pick 3a7322ac^..7d7c123c

Assuming you are on branchA where you want to pick commits (start & end commit SHA for the range is given and left commit SHA is older) from branchB. The entire range of commits (both inclusive) will be cherry picked in branchA.

The examples given in the official documentation are quite useful.

Crucifer answered 17/7, 2020 at 12:47 Comment(0)
O
9

I wrapped VonC's code into a short bash script, git-multi-cherry-pick, for easy running:

#!/bin/bash

if [ -z $1 ]; then
    echo "Equivalent to running git-cherry-pick on each of the commits in the range specified.";
    echo "";
    echo "Usage:  $0 start^..end";
    echo "";
    exit 1;
fi

git rev-list --reverse --topo-order $1 | while read rev 
do 
  git cherry-pick $rev || break 
done 

I'm currently using this as I rebuild the history of a project that had both 3rd-party code and customizations mixed together in the same svn trunk. I'm now splitting apart core 3rd party code, 3rd party modules, and customizations onto their own git branches for better understanding of customizations going forward. git-cherry-pick is helpful in this situation since I have two trees in the same repository, but without a shared ancestor.

Orv answered 31/3, 2010 at 14:20 Comment(0)
U
8

git cherry-pick FIRST^..LAST works only for simple scenarios.

To achieve a decent "merge it into the integration branch" while having the usal comfort with things like auto-skipping of already integrated picks, transplanting diamond-merges, interactive control ...) better use a rebase. One answer here pointed to that, however the protocol included a dicey git branch -f and a juggling with a temp branch. Here a straight robust method:

git rebase -i FIRST LAST~0 --onto integration
git rebase @ integration

The -i allows for interactive control. The ~0 ensures a detached rebase (not moving the / another branch) in case LAST is a branch name. It can be omitted otherwise. The second rebase command just moves the integration branch ref in safe manner forward to the intermediate detached head - it doesn't introduce new commits. To rebase a complex structure with merge diamonds etc. consider --rebase-merges or --rebase-merges=rebase-cousins in the first rebase.

Unattached answered 8/11, 2020 at 18:34 Comment(0)
I
6

I have tested that some days ago, after reading the very clear explanation of Vonc.

My steps

Start

  • Branch dev : A B C D E F G H I J
  • Branch target: A B C D
  • I don't want E nor H

Steps to copy features without the step E and H in the branch dev_feature_wo_E_H

  • git checkout dev
  • git checkout -b dev_feature_wo_E_H
  • git rebase --interactive --rebase-merges --no-ff D where I put drop front of E and H in the rebase editor
  • resolve conflicts, continue and commit

Steps to copy the branch dev_feature_wo_E_H on target.

  • git checkout target
  • git merge --no-ff --no-commit dev_feature_wo_E_H
  • resolve conflicts, continue and commit

Some remarks

  • I've done that because of too much cherry-pick in the days before

  • git cherry-pick is powerful and simple but

    • it creates duplicates commits
    • and when I want to merge I have to resolve conflicts of the initial commits and duplicates commits, so for one or two cherry-pick, it's OK to "cherry-picking" but for more it's too verbose and the branch will become too complex
Isochronism answered 15/2, 2020 at 9:36 Comment(3)
The moment you remove E F from the branch, you're rewritting (creating duplicates) of G H I J. It's no different than cherry picking them as individual commits.Haerr
For me cherry-pick should be an exception: merge is more in the original concept of git.Isochronism
My point is that rebase resembles more cherry-pick than merge. But I think I get your point now, you want to see the merge in the target branch (no-ff) rather than just plain cherry-picking.Haerr
L
3

All the above options will prompt you to resolve merge conflicts. If you are merging changes committed for a team, it is difficult to get resolved the merge conflicts from developers and proceed. However, "git merge" will do the merge in one shot but you can not pass a range of revisions as argument. we have to use "git diff" and "git apply" commands to do the merge range of revs. I have observed that "git apply" will fail if the patch file has diff for too many file, so we have to create a patch per file and then apply. Note that the script will not be able to delete the files that are deleted in source branch. This is a rare case, you can manually delete such files from target branch. The exit status of "git apply" is not zero if it is not able to apply the patch, however if you use -3way option it will fall back to 3 way merge and you don't have to worry about this failure.

Below is the script.

enter code here



  #!/bin/bash

    # This script will merge the diff between two git revisions to checked out branch
    # Make sure to cd to git source area and checkout the target branch
    # Make sure that checked out branch is clean run "git reset --hard HEAD"


    START=$1
    END=$2

    echo Start version: $START
    echo End version: $END

    mkdir -p ~/temp
    echo > /tmp/status
    #get files
    git --no-pager  diff  --name-only ${START}..${END} > ~/temp/files
    echo > ~/temp/error.log
    # merge every file
    for file in `cat  ~/temp/files`
    do
      git --no-pager diff --binary ${START}..${END} $file > ~/temp/git-diff
      if [ $? -ne 0 ]
      then
#      Diff usually fail if the file got deleted 
        echo Skipping the merge: git diff command failed for $file >> ~/temp/error.log
        echo Skipping the merge: git diff command failed for $file
        echo "STATUS: FAILED $file" >>  /tmp/status
        echo "STATUS: FAILED $file"
    # skip the merge for this file and continue the merge for others
        rm -f ~/temp/git-diff
        continue
      fi

      git apply  --ignore-space-change --ignore-whitespace  --3way --allow-binary-replacement ~/temp/git-diff

      if [ $? -ne 0 ]
       then
#  apply failed, but it will fall back to 3-way merge, you can ignore this failure
         echo "git apply command filed for $file"
       fi
       echo
       STATUS=`git status -s $file`


       if [ ! "$STATUS" ]
       then
#   status is null if the merged diffs are already present in the target file
         echo "STATUS:NOT_MERGED $file"
         echo "STATUS: NOT_MERGED $file$"  >>  /tmp/status
       else
#     3 way merge is successful
         echo STATUS: $STATUS
         echo "STATUS: $STATUS"  >>  /tmp/status
       fi
    done

    echo GIT merge failed for below listed files

    cat ~/temp/error.log

    echo "Git merge status per file is available in /tmp/status"
Lammastide answered 7/9, 2016 at 6:48 Comment(0)
T
2

If you've got only couple of commits and want to cherry-pick, you can simply do

git cherry-pick <commit> -n

on those commits and then make them into a new commit.

-n doesn't automatically create a commit rather just stages the changes hence you can continue to cherry-pick or make changes to the files in that commit.

Tulipwood answered 3/11, 2022 at 13:29 Comment(0)
H
1

Another option might be to merge with strategy ours to the commit before the range and then a 'normal' merge with the last commit of that range (or branch when it is the last one). So suppose only 2345 and 3456 commits of master to be merged into feature branch:

master:
1234
2345
3456
4567

in feature branch:

git merge -s ours 4567
git merge 2345
Haynie answered 16/8, 2015 at 12:47 Comment(0)
M
1

Merge can get difficult sometimes and it is easy to just create a patch and apply the changes manually.

  • Create a patch file from start_commit to end_commit.

    git diff <start_commit> <end_commit> > patch.diff

  • Checkout your branch and manually apply changes from the patch.

Most answered 7/2, 2023 at 13:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.