Reputation: 11556
Okay here's my situation. I have the following branches
My boss would like my changes from development-kirby merged into development but he doesn't want the whole thing in one huge merge. How can spoon feed the commits (say 50 at a time, in order) in several smaller chunks? Do I just checkout several points along the way in the development-kirby branch and merge them in that way or is there something better?
I'm sure this question has been asked in a better way and answered but searching SO hasn't turned up much.
Upvotes: 1
Views: 187
Reputation: 4572
To answer directly, you can branch upon a selected commit (syntax: git branch <branchname> <start-point>), merge the trunk into this new branch, pull (push) merge result into the trunk, drop temporary branch and go to the next commit portion.
OTOH I guess your boss is wrong: there is none benefit of such slow feeding unless commits are reorganized into logical groups which can be applied separately without breaking the whole application logic; but in that case commits shall not be simply counted. You can reorganize them (reorder, restruct) using interactive rebase. [Update: this is not the case, see below]
Upvotes: 2
Reputation: 488243
Yes, you can do exactly what you asked (but, don't; see below). For instance (this is a pretty fragile since I'm assuming the 270 number is correct, and that the first merge will work; it's also completely untested...):
b=development-kirby
m=development
git checkout $m
# 270 = 50 + 50 + 50 + 50 + 50 + 20
# hence, to be done in groups of 50, the commit-points are:
# $b~219, $b~169, $b~119, $b~69, $b~19, and $b (aka $b~0)
for dist in 219 169 119 69 19 0; do
# want to merge $b~$dist into $m, and we're (still) on $m
git merge --no-ff $b~$dist -m "mechanical merge at $b~$dist"
done
This should give you something like:
---- K1 --...-- K50 ---- K51..K100 --- ... --- K201...K250 --- K251..K270 development-kirby
/ \ \ \ \ \
* --- D1 --- D2 --- D3 --- M1 --------- M2 --- ... --- M4 ----------- M5 --- M6 development
where K1..K270 are your 270 commits, and D1 through D3 are three example commits that were put on development
since you started your work.
(The --no-ff
makes sure that all six actual Merge Commits, M1 through M6, wind up in the repo, otherwise you'd get fast-forwards and you'd just have M1 in the above, with K51 through K270 on top, and the branch development
now pointing to the same place as the branch development-kirby
:
---- K1 --...-- K50 --- --- K51..K270 development-kirby, development
/ \ /
* --- D1 --- D2 --- D3 --- M1
[It might help to note that that in between, during each "intermediate fast-forward", development
will point to some commit in the K51..K269 span, while development-kirby
will continue to point to commit K270. Only after the very last fast-forward will the two branch-names point to the same commit.])
The problem is, this is a weird and un-smart thing to do. Those intermediate merge commits, if they're chosen purely mechanically ("every 50" or whatever), are useless. They give you absolutely nothing you could not get by just doing one big 270-commit merge. It would make a whole lot more sense to pick points along your private development branch that "make sense" (e.g., "completely implemented feature X"). Let's say you've gone and picked out several such points, such as by doing:
git checkout $b~215
... poke around to make sure it's a good merge point,
and maybe move to $b~213 instead ...
git tag mp1 # merge point 1
git checkout $b~170
... poke around more, move to $b~172 because that's better ...
git tag mp2 # merge point 2
git checkout ...
# add more temporary tag labels as needed
# let's say you wind up with 4 total merge points
# which I've simply numbered mp1, mp2, mp3, mp4
git checkout development # get on target merge branch
git merge --no-ff mp1
<put in a sensible commit message this time, and resolve any merge conflicts>
git merge --no-ff mp2
<put in a sensible commit message for merge point 2>
git merge --no-ff mp3
<etc>
git merge --no-ff mp4
<etc>
git tag -d mp1 mp2 mp3 mp4 # get rid of temporary tags
Here's the main thing to know about "git merge", in this particular case where there's always an actual merge commit going in (i.e., not a fast-forward): what you are doing when you run git merge
is adding—to your current branch—one new commit, whose parents are:
development
in this case), andgit merge
command (which can be any commit-ID anywhere).(The contents of your new commit are, of course, the result of merging "contents of HEAD commit" with "contents of named commit", which implies finding what has changed between those two commits, etc., and sometimes getting help resolving merge conflicts.) After your new commit goes in, the current branch tip is re-pointed to the new merge commit. In this case, development
moves forward to the newly-added merge.
Hence, all you need to do is run git merge --no-ff
(to force actual merge commits, rather than fast-forwards) repeatedly, each time specifying one appropriate commit-ID within your development-kirby
branch—in "topological order", i.e., mp1 < mp2 < mp3 < mp4 (otherwise you'll wind up with a disaster of a merge graph; and see footnote). There are a lot of ways to specify commit-IDs, such as using the ~
notation above, but the "best" (for some value of "best") way is to go find individual "good ones" (git checkout
), then label them (git tag
) so that you don't have to remember long digit-strings.
git rev-list --no-walk --topo-order
to see whether some list of commit IDs is in the right order, but it's kind of a pain. I don't know of a better way and a quick google search did not turn anything up, so I wrote a short script:
#! /bin/sh
#
# check a list of IDs to see if they're in "topo order"
usage()
{
echo "usage: $0 id [...]"
}
case $# in
0) usage 1>&2; exit 1;;
esac
TF1=$(mktemp)
TF2=$(mktemp)
trap "rm -f $TF1 $TF2; exit" 0 1 2 3 15
# parse the arguments into one file
git rev-parse $@ > $TF1 || exit 1
# and topo-sort the arguments into another
git rev-list --topo-order --no-walk --reverse $@ > $TF2 || exit 1
# If the list is in the correct order the files will be the same
cmp -s $TF1 $TF2 || {
# If the files differ, it's possible that some argument(s) name
# the same rev...
[ $(wc -l < $TF1) -eq $(wc -l < $TF2) ] || {
echo "ERROR: there are repeats in $@"
# finding them is a pain, we don't bother trying
exit 1
}
echo "ERROR: $@ NOT in topo order"
echo "use instead:"
# read the topo-ordered raw IDs
while read sha1; do
# and find the (single) arg in $@ that names this one
for i; do
if [ $(git rev-parse $i) = $sha1 ]; then
printf ' %s' $i
break
fi
done
done < $TF2
echo
exit 1
}
echo "$@ in topo order"
exit 0
After making a set of merge-point tags you'd run, e.g.:
check-topo-order mp1 mp2 mp3 mp4
and it would tell you whether mp1 < mp2 < mp3 < mp4.
For instance, you might wind up with 3 commits that "add feature F"—perhaps "clean up to prepare for F" followed by "add infrastructure for F" followed by "enable feature F"—then just 1 for G, and 2 or 3 commits to add feature H. This would give you a 6-commit-long branch (perhaps call it devel-kirby-squashed
for instance). At that point, you have a pretty clean development chain that can be spliced directly into, or onto, the branch named development
.
Of course, sometimes there is reason to preserve all that intermediate work, in which case a series of --no-ff
merges is reasonable.
Upvotes: 1