Merge branch 'jc/checkout-merge-base'

* jc/checkout-merge-base:
  rebase -i: teach --onto A...B syntax
  rebase: fix --onto A...B parsing and add tests
  "rebase --onto A...B" replays history on the merge base between A and B
  "checkout A...B" switches to the merge base between A and B
This commit is contained in:
Junio C Hamano 2010-01-13 12:31:13 -08:00
commit 1f73566af5
7 changed files with 222 additions and 5 deletions

View File

@ -696,7 +696,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
* case 3: git checkout <something> [<paths>]
*
* With no paths, if <something> is a commit, that is to
* switch to the branch or detach HEAD at it.
* switch to the branch or detach HEAD at it. As a special case,
* if <something> is A...B (missing A or B means HEAD but you can
* omit at most one side), and if there is a unique merge base
* between A and B, A...B names that merge base.
*
* With no paths, if <something> is _not_ a commit, no -t nor -b
* was given, and there is a tracking branch whose name is
@ -722,7 +725,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
if (!strcmp(arg, "-"))
arg = "@{-1}";
if (get_sha1(arg, rev)) {
if (get_sha1_mb(arg, rev)) {
if (has_dash_dash) /* case (1) */
die("invalid reference: %s", arg);
if (!patch_mode &&

View File

@ -723,6 +723,7 @@ extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *
extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
extern int interpret_branch_name(const char *str, struct strbuf *);
extern int get_sha1_mb(const char *str, unsigned char *sha1);
extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
extern const char *ref_rev_parse_rules[];

View File

@ -495,6 +495,25 @@ get_saved_options () {
test -f "$DOTEST"/rebase-root && REBASE_ROOT=t
}
LF='
'
parse_onto () {
case "$1" in
*...*)
if left=${1%...*} right=${1#*...} &&
onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
then
case "$onto" in
?*"$LF"?* | '')
exit 1 ;;
esac
echo "$onto"
exit 0
fi
esac
git rev-parse --verify "$1^0"
}
while test $# != 0
do
case "$1" in
@ -602,7 +621,7 @@ first and then run 'git rebase --continue' again."
;;
--onto)
shift
ONTO=$(git rev-parse --verify "$1") ||
ONTO=$(parse_onto "$1") ||
die "Does not point to a valid commit: $1"
;;
--)

View File

@ -34,6 +34,8 @@ set_reflog_action rebase
require_work_tree
cd_to_toplevel
LF='
'
OK_TO_SKIP_PRE_REBASE=
RESOLVEMSG="
When you have resolved this problem run \"git rebase --continue\".
@ -417,7 +419,27 @@ fi
# Make sure the branch to rebase onto is valid.
onto_name=${newbase-"$upstream_name"}
onto=$(git rev-parse --verify "${onto_name}^0") || exit
case "$onto_name" in
*...*)
if left=${onto_name%...*} right=${onto_name#*...} &&
onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
then
case "$onto" in
?*"$LF"?*)
die "$onto_name: there are more than one merge bases"
;;
'')
die "$onto_name: there is no merge base"
;;
esac
else
die "$onto_name: there is no merge base"
fi
;;
*)
onto=$(git rev-parse --verify "${onto_name}^0") || exit
;;
esac
# If a hook exists, give it a chance to interrupt
run_pre_rebase_hook "$upstream_arg" "$@"

View File

@ -794,6 +794,48 @@ release_return:
return retval;
}
int get_sha1_mb(const char *name, unsigned char *sha1)
{
struct commit *one, *two;
struct commit_list *mbs;
unsigned char sha1_tmp[20];
const char *dots;
int st;
dots = strstr(name, "...");
if (!dots)
return get_sha1(name, sha1);
if (dots == name)
st = get_sha1("HEAD", sha1_tmp);
else {
struct strbuf sb;
strbuf_init(&sb, dots - name);
strbuf_add(&sb, name, dots - name);
st = get_sha1(sb.buf, sha1_tmp);
strbuf_release(&sb);
}
if (st)
return st;
one = lookup_commit_reference_gently(sha1_tmp, 0);
if (!one)
return -1;
if (get_sha1(dots[3] ? (dots + 3) : "HEAD", sha1_tmp))
return -1;
two = lookup_commit_reference_gently(sha1_tmp, 0);
if (!two)
return -1;
mbs = get_merge_bases(one, two, 1);
if (!mbs || mbs->next)
st = -1;
else {
st = 0;
hashcpy(sha1, mbs->item->object.sha1);
}
free_commit_list(mbs);
return st;
}
/*
* This is like "get_sha1_basic()", except it allows "sha1 expressions",
* notably "xyz^" for "parent of xyz"

View File

@ -1,6 +1,6 @@
#!/bin/sh
test_description='checkout can switch to last branch'
test_description='checkout can switch to last branch and merge base'
. ./test-lib.sh
@ -91,4 +91,29 @@ test_expect_success 'switch to twelfth from the last' '
test "z$(git symbolic-ref HEAD)" = "zrefs/heads/branch13"
'
test_expect_success 'merge base test setup' '
git checkout -b another other &&
echo "hello again" >>world &&
git add world &&
git commit -m third
'
test_expect_success 'another...master' '
git checkout another &&
git checkout another...master &&
test "z$(git rev-parse --verify HEAD)" = "z$(git rev-parse --verify master^)"
'
test_expect_success '...master' '
git checkout another &&
git checkout ...master &&
test "z$(git rev-parse --verify HEAD)" = "z$(git rev-parse --verify master^)"
'
test_expect_success 'master...' '
git checkout another &&
git checkout master... &&
test "z$(git rev-parse --verify HEAD)" = "z$(git rev-parse --verify master^)"
'
test_done

105
t/t3415-rebase-onto-threedots.sh Executable file
View File

@ -0,0 +1,105 @@
#!/bin/sh
test_description='git rebase --onto A...B'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-rebase.sh"
# Rebase only the tip commit of "topic" on merge base between "master"
# and "topic". Cannot do this for "side" with "master" because there
# is no single merge base.
#
#
# F---G topic G'
# / /
# A---B---C---D---E master --> A---B---C---D---E
# \ \ /
# \ x
# \ / \
# H---I---J---K side
test_expect_success setup '
test_commit A &&
test_commit B &&
git branch side &&
test_commit C &&
git branch topic &&
git checkout side &&
test_commit H &&
git checkout master &&
test_tick &&
git merge H &&
git tag D &&
test_commit E &&
git checkout topic &&
test_commit F &&
test_commit G &&
git checkout side &&
test_tick &&
git merge C &&
git tag I &&
test_commit J &&
test_commit K
'
test_expect_success 'rebase --onto master...topic' '
git reset --hard &&
git checkout topic &&
git reset --hard G &&
git rebase --onto master...topic F &&
git rev-parse HEAD^1 >actual &&
git rev-parse C^0 >expect &&
test_cmp expect actual
'
test_expect_success 'rebase --onto master...' '
git reset --hard &&
git checkout topic &&
git reset --hard G &&
git rebase --onto master... F &&
git rev-parse HEAD^1 >actual &&
git rev-parse C^0 >expect &&
test_cmp expect actual
'
test_expect_success 'rebase --onto master...side' '
git reset --hard &&
git checkout side &&
git reset --hard K &&
test_must_fail git rebase --onto master...side J
'
test_expect_success 'rebase -i --onto master...topic' '
git reset --hard &&
git checkout topic &&
git reset --hard G &&
set_fake_editor &&
EXPECT_COUNT=1 git rebase -i --onto master...topic F &&
git rev-parse HEAD^1 >actual &&
git rev-parse C^0 >expect &&
test_cmp expect actual
'
test_expect_success 'rebase -i --onto master...' '
git reset --hard &&
git checkout topic &&
git reset --hard G &&
set_fake_editor &&
EXPECT_COUNT=1 git rebase -i --onto master... F &&
git rev-parse HEAD^1 >actual &&
git rev-parse C^0 >expect &&
test_cmp expect actual
'
test_expect_success 'rebase -i --onto master...side' '
git reset --hard &&
git checkout side &&
git reset --hard K &&
test_must_fail git rebase -i --onto master...side J
'
test_done