diff --git a/builtin-checkout.c b/builtin-checkout.c index 2708669383..793542eb06 100644 --- a/builtin-checkout.c +++ b/builtin-checkout.c @@ -696,7 +696,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) * case 3: git checkout [] * * With no paths, if 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 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 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 && diff --git a/cache.h b/cache.h index 3e52c4efb1..cf36c8160e 100644 --- a/cache.h +++ b/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_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[]; diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 6ed57e2664..1560e84bd5 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -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" ;; --) diff --git a/git-rebase.sh b/git-rebase.sh index b121f4537c..3de0942c0f 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -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" "$@" diff --git a/sha1_name.c b/sha1_name.c index ca8f9dba94..1739e9e612 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -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" diff --git a/t/t2012-checkout-last.sh b/t/t2012-checkout-last.sh index 87b30a268c..b44de9dc62 100755 --- a/t/t2012-checkout-last.sh +++ b/t/t2012-checkout-last.sh @@ -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 diff --git a/t/t3415-rebase-onto-threedots.sh b/t/t3415-rebase-onto-threedots.sh new file mode 100755 index 0000000000..ddf2f64853 --- /dev/null +++ b/t/t3415-rebase-onto-threedots.sh @@ -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