Skip to content

Commit 1f73566

Browse files
committed
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
2 parents 5b9c0a6 + 230a456 commit 1f73566

File tree

7 files changed

+222
-5
lines changed

7 files changed

+222
-5
lines changed

builtin-checkout.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
696696
* case 3: git checkout <something> [<paths>]
697697
*
698698
* With no paths, if <something> is a commit, that is to
699-
* switch to the branch or detach HEAD at it.
699+
* switch to the branch or detach HEAD at it. As a special case,
700+
* if <something> is A...B (missing A or B means HEAD but you can
701+
* omit at most one side), and if there is a unique merge base
702+
* between A and B, A...B names that merge base.
700703
*
701704
* With no paths, if <something> is _not_ a commit, no -t nor -b
702705
* 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)
722725
if (!strcmp(arg, "-"))
723726
arg = "@{-1}";
724727

725-
if (get_sha1(arg, rev)) {
728+
if (get_sha1_mb(arg, rev)) {
726729
if (has_dash_dash) /* case (1) */
727730
die("invalid reference: %s", arg);
728731
if (!patch_mode &&

cache.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *
723723
extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
724724
extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
725725
extern int interpret_branch_name(const char *str, struct strbuf *);
726+
extern int get_sha1_mb(const char *str, unsigned char *sha1);
726727

727728
extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
728729
extern const char *ref_rev_parse_rules[];

git-rebase--interactive.sh

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,25 @@ get_saved_options () {
495495
test -f "$DOTEST"/rebase-root && REBASE_ROOT=t
496496
}
497497

498+
LF='
499+
'
500+
parse_onto () {
501+
case "$1" in
502+
*...*)
503+
if left=${1%...*} right=${1#*...} &&
504+
onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
505+
then
506+
case "$onto" in
507+
?*"$LF"?* | '')
508+
exit 1 ;;
509+
esac
510+
echo "$onto"
511+
exit 0
512+
fi
513+
esac
514+
git rev-parse --verify "$1^0"
515+
}
516+
498517
while test $# != 0
499518
do
500519
case "$1" in
@@ -602,7 +621,7 @@ first and then run 'git rebase --continue' again."
602621
;;
603622
--onto)
604623
shift
605-
ONTO=$(git rev-parse --verify "$1") ||
624+
ONTO=$(parse_onto "$1") ||
606625
die "Does not point to a valid commit: $1"
607626
;;
608627
--)

git-rebase.sh

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ set_reflog_action rebase
3434
require_work_tree
3535
cd_to_toplevel
3636

37+
LF='
38+
'
3739
OK_TO_SKIP_PRE_REBASE=
3840
RESOLVEMSG="
3941
When you have resolved this problem run \"git rebase --continue\".
@@ -417,7 +419,27 @@ fi
417419

418420
# Make sure the branch to rebase onto is valid.
419421
onto_name=${newbase-"$upstream_name"}
420-
onto=$(git rev-parse --verify "${onto_name}^0") || exit
422+
case "$onto_name" in
423+
*...*)
424+
if left=${onto_name%...*} right=${onto_name#*...} &&
425+
onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
426+
then
427+
case "$onto" in
428+
?*"$LF"?*)
429+
die "$onto_name: there are more than one merge bases"
430+
;;
431+
'')
432+
die "$onto_name: there is no merge base"
433+
;;
434+
esac
435+
else
436+
die "$onto_name: there is no merge base"
437+
fi
438+
;;
439+
*)
440+
onto=$(git rev-parse --verify "${onto_name}^0") || exit
441+
;;
442+
esac
421443

422444
# If a hook exists, give it a chance to interrupt
423445
run_pre_rebase_hook "$upstream_arg" "$@"

sha1_name.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,48 @@ int interpret_branch_name(const char *name, struct strbuf *buf)
794794
return retval;
795795
}
796796

797+
int get_sha1_mb(const char *name, unsigned char *sha1)
798+
{
799+
struct commit *one, *two;
800+
struct commit_list *mbs;
801+
unsigned char sha1_tmp[20];
802+
const char *dots;
803+
int st;
804+
805+
dots = strstr(name, "...");
806+
if (!dots)
807+
return get_sha1(name, sha1);
808+
if (dots == name)
809+
st = get_sha1("HEAD", sha1_tmp);
810+
else {
811+
struct strbuf sb;
812+
strbuf_init(&sb, dots - name);
813+
strbuf_add(&sb, name, dots - name);
814+
st = get_sha1(sb.buf, sha1_tmp);
815+
strbuf_release(&sb);
816+
}
817+
if (st)
818+
return st;
819+
one = lookup_commit_reference_gently(sha1_tmp, 0);
820+
if (!one)
821+
return -1;
822+
823+
if (get_sha1(dots[3] ? (dots + 3) : "HEAD", sha1_tmp))
824+
return -1;
825+
two = lookup_commit_reference_gently(sha1_tmp, 0);
826+
if (!two)
827+
return -1;
828+
mbs = get_merge_bases(one, two, 1);
829+
if (!mbs || mbs->next)
830+
st = -1;
831+
else {
832+
st = 0;
833+
hashcpy(sha1, mbs->item->object.sha1);
834+
}
835+
free_commit_list(mbs);
836+
return st;
837+
}
838+
797839
/*
798840
* This is like "get_sha1_basic()", except it allows "sha1 expressions",
799841
* notably "xyz^" for "parent of xyz"

t/t2012-checkout-last.sh

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/sh
22

3-
test_description='checkout can switch to last branch'
3+
test_description='checkout can switch to last branch and merge base'
44

55
. ./test-lib.sh
66

@@ -91,4 +91,29 @@ test_expect_success 'switch to twelfth from the last' '
9191
test "z$(git symbolic-ref HEAD)" = "zrefs/heads/branch13"
9292
'
9393

94+
test_expect_success 'merge base test setup' '
95+
git checkout -b another other &&
96+
echo "hello again" >>world &&
97+
git add world &&
98+
git commit -m third
99+
'
100+
101+
test_expect_success 'another...master' '
102+
git checkout another &&
103+
git checkout another...master &&
104+
test "z$(git rev-parse --verify HEAD)" = "z$(git rev-parse --verify master^)"
105+
'
106+
107+
test_expect_success '...master' '
108+
git checkout another &&
109+
git checkout ...master &&
110+
test "z$(git rev-parse --verify HEAD)" = "z$(git rev-parse --verify master^)"
111+
'
112+
113+
test_expect_success 'master...' '
114+
git checkout another &&
115+
git checkout master... &&
116+
test "z$(git rev-parse --verify HEAD)" = "z$(git rev-parse --verify master^)"
117+
'
118+
94119
test_done

t/t3415-rebase-onto-threedots.sh

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/bin/sh
2+
3+
test_description='git rebase --onto A...B'
4+
5+
. ./test-lib.sh
6+
. "$TEST_DIRECTORY/lib-rebase.sh"
7+
8+
# Rebase only the tip commit of "topic" on merge base between "master"
9+
# and "topic". Cannot do this for "side" with "master" because there
10+
# is no single merge base.
11+
#
12+
#
13+
# F---G topic G'
14+
# / /
15+
# A---B---C---D---E master --> A---B---C---D---E
16+
# \ \ /
17+
# \ x
18+
# \ / \
19+
# H---I---J---K side
20+
21+
test_expect_success setup '
22+
test_commit A &&
23+
test_commit B &&
24+
git branch side &&
25+
test_commit C &&
26+
git branch topic &&
27+
git checkout side &&
28+
test_commit H &&
29+
git checkout master &&
30+
test_tick &&
31+
git merge H &&
32+
git tag D &&
33+
test_commit E &&
34+
git checkout topic &&
35+
test_commit F &&
36+
test_commit G &&
37+
git checkout side &&
38+
test_tick &&
39+
git merge C &&
40+
git tag I &&
41+
test_commit J &&
42+
test_commit K
43+
'
44+
45+
test_expect_success 'rebase --onto master...topic' '
46+
git reset --hard &&
47+
git checkout topic &&
48+
git reset --hard G &&
49+
50+
git rebase --onto master...topic F &&
51+
git rev-parse HEAD^1 >actual &&
52+
git rev-parse C^0 >expect &&
53+
test_cmp expect actual
54+
'
55+
56+
test_expect_success 'rebase --onto master...' '
57+
git reset --hard &&
58+
git checkout topic &&
59+
git reset --hard G &&
60+
61+
git rebase --onto master... F &&
62+
git rev-parse HEAD^1 >actual &&
63+
git rev-parse C^0 >expect &&
64+
test_cmp expect actual
65+
'
66+
67+
test_expect_success 'rebase --onto master...side' '
68+
git reset --hard &&
69+
git checkout side &&
70+
git reset --hard K &&
71+
72+
test_must_fail git rebase --onto master...side J
73+
'
74+
75+
test_expect_success 'rebase -i --onto master...topic' '
76+
git reset --hard &&
77+
git checkout topic &&
78+
git reset --hard G &&
79+
set_fake_editor &&
80+
EXPECT_COUNT=1 git rebase -i --onto master...topic F &&
81+
git rev-parse HEAD^1 >actual &&
82+
git rev-parse C^0 >expect &&
83+
test_cmp expect actual
84+
'
85+
86+
test_expect_success 'rebase -i --onto master...' '
87+
git reset --hard &&
88+
git checkout topic &&
89+
git reset --hard G &&
90+
set_fake_editor &&
91+
EXPECT_COUNT=1 git rebase -i --onto master... F &&
92+
git rev-parse HEAD^1 >actual &&
93+
git rev-parse C^0 >expect &&
94+
test_cmp expect actual
95+
'
96+
97+
test_expect_success 'rebase -i --onto master...side' '
98+
git reset --hard &&
99+
git checkout side &&
100+
git reset --hard K &&
101+
102+
test_must_fail git rebase -i --onto master...side J
103+
'
104+
105+
test_done

0 commit comments

Comments
 (0)