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!
When it comes to a range of commits, cherry-picking is was not practical.
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 thanB
.
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?" as 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 commit directly on an orphan branch (empty head): see "How to make existing branch an orphan in git".
Original answer (January 2010)
A rebase --onto
would be better, where you replay the given range of commit 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 want to replay, from theworking
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 theintegration
branch on thetmp
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 rangeB
throughD
that would beB^..D
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.
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
andcherry-pick --stdin
), so didgit revert
; these do not support the nicer sequencing controlrebase [-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).
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.
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...
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.
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.
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.
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
norH
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 putdrop
front ofE
andH
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 beforegit 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 twocherry-pick
, it's OK to "cherry-picking" but for more it's too verbose and the branch will become too complex
In my mind the steps I've done are more clear than
git rebase --onto
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"
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
If you've got only couple of commits and want to cherry-pick, you can 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.
精彩评论