Skip to content

Commit 619a644

Browse files
committed
"checkout A...B" switches to the merge base between A and B
When flipping commits around on topic branches, I often end up doing this sequence: * Run "log --oneline next..jc/frotz" to find out the first commit on 'jc/frotz' branch not yet merged to 'next'; * Run "checkout $that_commit^" to detach HEAD to the parent of it; * Rebuild the series on top of that commit; and * "show-branch jc/frotz HEAD" and "diff jc/frotz HEAD" to verify. Introduce a new syntax to "git checkout" to name the commit to switch to, to make the first two steps easier. When the branch to switch to is specified as A...B (you can omit either A or B but not both, and HEAD is used instead of the omitted side), the merge base between these two commits are computed, and if there is one unique one, we detach the HEAD at that commit. With this, I can say "checkout next...jc/frotz". Signed-off-by: Junio C Hamano <[email protected]>
1 parent 46148dd commit 619a644

File tree

4 files changed

+74
-3
lines changed

4 files changed

+74
-3
lines changed

builtin-checkout.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
689689
* case 3: git checkout <something> [<paths>]
690690
*
691691
* With no paths, if <something> is a commit, that is to
692-
* switch to the branch or detach HEAD at it.
692+
* switch to the branch or detach HEAD at it. As a special case,
693+
* if <something> is A...B (missing A or B means HEAD but you can
694+
* omit at most one side), and if there is a unique merge base
695+
* between A and B, A...B names that merge base.
693696
*
694697
* With no paths, if <something> is _not_ a commit, no -t nor -b
695698
* was given, and there is a tracking branch whose name is
@@ -715,7 +718,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
715718
if (!strcmp(arg, "-"))
716719
arg = "@{-1}";
717720

718-
if (get_sha1(arg, rev)) {
721+
if (get_sha1_mb(arg, rev)) {
719722
if (has_dash_dash) /* case (1) */
720723
die("invalid reference: %s", arg);
721724
if (!patch_mode &&

cache.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@ extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *
703703
extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
704704
extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
705705
extern int interpret_branch_name(const char *str, struct strbuf *);
706+
extern int get_sha1_mb(const char *str, unsigned char *sha1);
706707

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

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

0 commit comments

Comments
 (0)