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 Bmaint
commit
1f73566af5
|
@ -696,7 +696,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
|
||||||
* case 3: git checkout <something> [<paths>]
|
* case 3: git checkout <something> [<paths>]
|
||||||
*
|
*
|
||||||
* With no paths, if <something> is a commit, that is to
|
* 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
|
* With no paths, if <something> is _not_ a commit, no -t nor -b
|
||||||
* was given, and there is a tracking branch whose name is
|
* 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, "-"))
|
if (!strcmp(arg, "-"))
|
||||||
arg = "@{-1}";
|
arg = "@{-1}";
|
||||||
|
|
||||||
if (get_sha1(arg, rev)) {
|
if (get_sha1_mb(arg, rev)) {
|
||||||
if (has_dash_dash) /* case (1) */
|
if (has_dash_dash) /* case (1) */
|
||||||
die("invalid reference: %s", arg);
|
die("invalid reference: %s", arg);
|
||||||
if (!patch_mode &&
|
if (!patch_mode &&
|
||||||
|
|
1
cache.h
1
cache.h
|
@ -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_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 dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
|
||||||
extern int interpret_branch_name(const char *str, struct strbuf *);
|
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 int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
|
||||||
extern const char *ref_rev_parse_rules[];
|
extern const char *ref_rev_parse_rules[];
|
||||||
|
|
|
@ -495,6 +495,25 @@ get_saved_options () {
|
||||||
test -f "$DOTEST"/rebase-root && REBASE_ROOT=t
|
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
|
while test $# != 0
|
||||||
do
|
do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
@ -602,7 +621,7 @@ first and then run 'git rebase --continue' again."
|
||||||
;;
|
;;
|
||||||
--onto)
|
--onto)
|
||||||
shift
|
shift
|
||||||
ONTO=$(git rev-parse --verify "$1") ||
|
ONTO=$(parse_onto "$1") ||
|
||||||
die "Does not point to a valid commit: $1"
|
die "Does not point to a valid commit: $1"
|
||||||
;;
|
;;
|
||||||
--)
|
--)
|
||||||
|
|
|
@ -34,6 +34,8 @@ set_reflog_action rebase
|
||||||
require_work_tree
|
require_work_tree
|
||||||
cd_to_toplevel
|
cd_to_toplevel
|
||||||
|
|
||||||
|
LF='
|
||||||
|
'
|
||||||
OK_TO_SKIP_PRE_REBASE=
|
OK_TO_SKIP_PRE_REBASE=
|
||||||
RESOLVEMSG="
|
RESOLVEMSG="
|
||||||
When you have resolved this problem run \"git rebase --continue\".
|
When you have resolved this problem run \"git rebase --continue\".
|
||||||
|
@ -417,7 +419,27 @@ fi
|
||||||
|
|
||||||
# Make sure the branch to rebase onto is valid.
|
# Make sure the branch to rebase onto is valid.
|
||||||
onto_name=${newbase-"$upstream_name"}
|
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
|
# If a hook exists, give it a chance to interrupt
|
||||||
run_pre_rebase_hook "$upstream_arg" "$@"
|
run_pre_rebase_hook "$upstream_arg" "$@"
|
||||||
|
|
42
sha1_name.c
42
sha1_name.c
|
@ -794,6 +794,48 @@ release_return:
|
||||||
return retval;
|
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",
|
* This is like "get_sha1_basic()", except it allows "sha1 expressions",
|
||||||
* notably "xyz^" for "parent of xyz"
|
* notably "xyz^" for "parent of xyz"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
test_description='checkout can switch to last branch'
|
test_description='checkout can switch to last branch and merge base'
|
||||||
|
|
||||||
. ./test-lib.sh
|
. ./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 "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
|
test_done
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue