From 619a644d6daef56d70aeca85514e2d281eb483a5 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 18 Oct 2009 12:34:56 -0700 Subject: [PATCH 1/4] "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 --- builtin-checkout.c | 7 +++++-- cache.h | 1 + sha1_name.c | 42 ++++++++++++++++++++++++++++++++++++++++ t/t2012-checkout-last.sh | 27 +++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/builtin-checkout.c b/builtin-checkout.c index da04eed391..fe7c8584ca 100644 --- a/builtin-checkout.c +++ b/builtin-checkout.c @@ -689,7 +689,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 @@ -715,7 +718,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 71a731dbc9..f8df15a060 100644 --- a/cache.h +++ b/cache.h @@ -703,6 +703,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/sha1_name.c b/sha1_name.c index 44bb62d270..d5a240cf07 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 From 61dfa1bb6710690e53fa20bc3ddd1f5fbe8c1d22 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 20 Nov 2009 03:02:44 -0800 Subject: [PATCH 2/4] "rebase --onto A...B" replays history on the merge base between A and B This is in spirit similar to "checkout A...B". To re-queue a new set of patches for a series that the original author prepared to apply on 'next' on the same base as before, you would do something like this: $ git checkout next^0 $ git am -s rerolled-series.mbox $ git rebase --onto next...jh/notes next The first two commands recreates commits to be rebased as the original author intended (i.e. applies directly on top of 'next'), and the rebase command replays that history on top of the same commit the series being replaced was built on (which is typically much older than the tip of 'next'). Signed-off-by: Junio C Hamano --- git-rebase.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/git-rebase.sh b/git-rebase.sh index 6ec155cf03..6503113a84 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,22 @@ fi # Make sure the branch to rebase onto is valid. onto_name=${newbase-"$upstream_name"} -onto=$(git rev-parse --verify "${onto_name}^0") || exit +if left=$(expr "$onto_name" : '\(.*\)\.\.\.') && + right=$(expr "$onto_name" : '\.\.\.\(.*\)$') && + : ${left:=HEAD} ${right:=HEAD} && + onto=$(git merge-base "$left" "$right") +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 + onto=$(git rev-parse --verify "${onto_name}^0") || exit +fi # If a hook exists, give it a chance to interrupt run_pre_rebase_hook "$upstream_arg" "$@" From 9f21e97ddccf114a6919cf4b8cf57c2838328f36 Mon Sep 17 00:00:00 2001 From: Nanako Shiraishi Date: Thu, 7 Jan 2010 20:05:02 +0900 Subject: [PATCH 3/4] rebase: fix --onto A...B parsing and add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous patch didn't parse "rebase --onto A...B" correctly when A isn't an empty string. It also tried to be careful to notice a case in which there are more than one merge bases, but forgot to give --all option to merge-base, making the test pointless. Fix these problems and add a test script to verify. Improvements to the script to parse A...B syntax was taken from review comments by Johannes Schindelin. Signed-off-by: しらいし ななこ Signed-off-by: Junio C Hamano --- git-rebase.sh | 33 ++++++++------ t/t3415-rebase-onto-threedots.sh | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 14 deletions(-) create mode 100755 t/t3415-rebase-onto-threedots.sh diff --git a/git-rebase.sh b/git-rebase.sh index 6503113a84..9bd89746ab 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -419,22 +419,27 @@ fi # Make sure the branch to rebase onto is valid. onto_name=${newbase-"$upstream_name"} -if left=$(expr "$onto_name" : '\(.*\)\.\.\.') && - right=$(expr "$onto_name" : '\.\.\.\(.*\)$') && - : ${left:=HEAD} ${right:=HEAD} && - onto=$(git merge-base "$left" "$right") -then - case "$onto" in - ?*"$LF"?*) - die "$onto_name: there are more than one merge bases" - ;; - '') +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" - ;; - esac -else + fi + ;; +*) onto=$(git rev-parse --verify "${onto_name}^0") || exit -fi + ;; +esac # If a hook exists, give it a chance to interrupt run_pre_rebase_hook "$upstream_arg" "$@" diff --git a/t/t3415-rebase-onto-threedots.sh b/t/t3415-rebase-onto-threedots.sh new file mode 100755 index 0000000000..b09e907c35 --- /dev/null +++ b/t/t3415-rebase-onto-threedots.sh @@ -0,0 +1,75 @@ +#!/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_done From 230a4566382860fc26a3f8d578a41c6504cf865f Mon Sep 17 00:00:00 2001 From: Nanako Shiraishi Date: Thu, 7 Jan 2010 20:05:09 +0900 Subject: [PATCH 4/4] rebase -i: teach --onto A...B syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When rewriting commits on a topic branch, sometimes it is easier to compare the version of commits before and after the rewrite if they are based on the same commit that forked from the upstream. An earlier commit by Junio (fixed up by the previous commit) gives "--onto A...B" syntax to rebase command, and rebases on top of the merge base between A and B; teach the same to the interactive version, too. Signed-off-by: しらいし ななこ Signed-off-by: Junio C Hamano --- git-rebase--interactive.sh | 21 ++++++++++++++++++++- t/t3415-rebase-onto-threedots.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 23ded48322..f7ae02ccb5 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -482,6 +482,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 @@ -589,7 +608,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/t/t3415-rebase-onto-threedots.sh b/t/t3415-rebase-onto-threedots.sh index b09e907c35..ddf2f64853 100755 --- a/t/t3415-rebase-onto-threedots.sh +++ b/t/t3415-rebase-onto-threedots.sh @@ -72,4 +72,34 @@ test_expect_success 'rebase --onto master...side' ' 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