gitgit-mergegit-cherry-pick

How to cherry-pick a range of commits and merge them into another branch?


I have the following repository layout:

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!


Solution

  • 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:

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

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

    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:

    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".

    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)
    

    When you run the command:

    git cherry-pick CommitId1^..CommitId99
    

    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.