From 28fb84382b0eb728534dbe2972bbfec3f3d83dd9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 10 Sep 2009 17:25:57 +0200 Subject: Introduce @{upstream} notation A new notation '@{upstream}' refers to the branch is set to build on top of. Missing (i.e. '@{upstream}') defaults to the current branch. This allows you to run, for example, for l in list of local branches do git log --oneline --left-right $l...$l@{upstream} done to inspect each of the local branches you are interested in for the divergence from its upstream. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt index 82045a2..923b56a 100644 --- a/Documentation/git-rev-parse.txt +++ b/Documentation/git-rev-parse.txt @@ -231,6 +231,10 @@ when you run 'git-merge'. * The special construct '@\{-\}' means the th branch checked out before the current one. +* The suffix '@{upstream}' to a ref (short form 'ref@{u}') refers to + the branch the ref is set to build on top of. Missing ref defaults + to the current branch. + * A suffix '{caret}' to a revision parameter means the first parent of that commit object. '{caret}' means the th parent (i.e. 'rev{caret}' diff --git a/sha1_name.c b/sha1_name.c index 44bb62d..fb4e214 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -5,6 +5,7 @@ #include "blob.h" #include "tree-walk.h" #include "refs.h" +#include "remote.h" static int find_short_object_filename(int len, const char *name, unsigned char *sha1) { @@ -238,9 +239,24 @@ static int ambiguous_path(const char *path, int len) return slash; } +static inline int tracked_suffix(const char *string, int len) +{ + const char *suffix[] = { "@{upstream}", "@{u}" }; + int i; + + for (i = 0; i < ARRAY_SIZE(suffix); i++) { + int suffix_len = strlen(suffix[i]); + if (len >= suffix_len && !memcmp(string + len - suffix_len, + suffix[i], suffix_len)) + return suffix_len; + } + return 0; +} + /* * *string and *len will only be substituted, and *string returned (for - * later free()ing) if the string passed in is of the form @{-}. + * later free()ing) if the string passed in is of the form @{-} or + * of the form @{upstream}. */ static char *substitute_branch_name(const char **string, int *len) { @@ -254,6 +270,21 @@ static char *substitute_branch_name(const char **string, int *len) return (char *)*string; } + ret = tracked_suffix(*string, *len); + if (ret) { + char *ref = xstrndup(*string, *len - ret); + struct branch *tracking = branch_get(*ref ? ref : NULL); + + if (!tracking) + die ("No tracking branch found for '%s'", ref); + free(ref); + if (tracking->merge && tracking->merge[0]->dst) { + *string = xstrdup(tracking->merge[0]->dst); + *len = strlen(*string); + return (char *)*string; + } + } + return NULL; } @@ -340,8 +371,10 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1) if (len && str[len-1] == '}') { for (at = len-2; at >= 0; at--) { if (str[at] == '@' && str[at+1] == '{') { - reflog_len = (len-1) - (at+2); - len = at; + if (!tracked_suffix(str + at, len - at)) { + reflog_len = (len-1) - (at+2); + len = at; + } break; } } diff --git a/t/t1506-rev-parse-upstream.sh b/t/t1506-rev-parse-upstream.sh new file mode 100755 index 0000000..5abdc13 --- /dev/null +++ b/t/t1506-rev-parse-upstream.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +test_description='test @{upstream} syntax' + +. ./test-lib.sh + + +test_expect_success 'setup' ' + + test_commit 1 && + git checkout -b side && + test_commit 2 && + git checkout master && + git clone . clone && + test_commit 3 && + (cd clone && + test_commit 4 && + git branch --track my-side origin/side) + +' + +full_name () { + (cd clone && + git rev-parse --symbolic-full-name "$@") +} + +commit_subject () { + (cd clone && + git show -s --pretty=format:%s "$@") +} + +test_expect_success '@{upstream} resolves to correct full name' ' + test refs/remotes/origin/master = "$(full_name @{upstream})" +' + +test_expect_success '@{u} resolves to correct full name' ' + test refs/remotes/origin/master = "$(full_name @{u})" +' + +test_expect_success 'my-side@{upstream} resolves to correct full name' ' + test refs/remotes/origin/side = "$(full_name my-side@{u})" +' + +test_expect_success 'my-side@{u} resolves to correct commit' ' + git checkout side && + test_commit 5 && + (cd clone && git fetch) && + test 2 = "$(commit_subject my-side)" && + test 5 = "$(commit_subject my-side@{u})" +' + +test_expect_success 'not-tracking@{u} fails' ' + test_must_fail full_name non-tracking@{u} && + (cd clone && git checkout --no-track -b non-tracking) && + test_must_fail full_name non-tracking@{u} +' + +test_expect_success '@{u}@{1} resolves correctly' ' + test_commit 6 && + (cd clone && git fetch) && + test 5 = $(commit_subject my-side@{u}@{1}) +' + +test_expect_success '@{u} without specifying branch fails on a detached HEAD' ' + git checkout HEAD^0 && + test_must_fail git rev-parse @{u} +' + +test_done -- cgit v0.10.2-6-g49f6 From 69add8e6d20989babdb128cf63decc10c418fcfa Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 20 Jan 2010 01:08:48 -0800 Subject: t1506: more test for @{upstream} syntax This adds a few more tests that exercises @{upstream} syntax by commands that operate differently when they are given branch name as opposed to a refname (i.e. where "master" and "refs/heads/master" makes a difference). Signed-off-by: Junio C Hamano diff --git a/t/t1506-rev-parse-upstream.sh b/t/t1506-rev-parse-upstream.sh index 5abdc13..a2c7f92 100755 --- a/t/t1506-rev-parse-upstream.sh +++ b/t/t1506-rev-parse-upstream.sh @@ -66,4 +66,45 @@ test_expect_success '@{u} without specifying branch fails on a detached HEAD' ' test_must_fail git rev-parse @{u} ' +test_expect_success 'checkout -b new my-side@{u} forks from the same' ' +( + cd clone && + git checkout -b new my-side@{u} && + git rev-parse --symbolic-full-name my-side@{u} >expect && + git rev-parse --symbolic-full-name new@{u} >actual && + test_cmp expect actual +) +' + +test_expect_failure 'merge my-side@{u} records the correct name' ' +( + sq="'\''" && + cd clone || exit + git checkout master || exit + git branch -D new ;# can fail but is ok + git branch -t new my-side@{u} && + git merge -s ours new@{u} && + git show -s --pretty=format:%s >actual && + echo "Merge remote branch ${sq}origin/side${sq}" >expect && + test_cmp expect actual +) +' + +test_expect_failure 'branch -d other@{u}' ' + git checkout -t -b other master && + git branch -d @{u} && + git for-each-ref refs/heads/master >actual && + >expect && + test_cmp expect actual +' + +test_expect_failure 'checkout other@{u}' ' + git branch -f master HEAD && + git checkout -t -b another master && + git checkout @{u} && + git symbolic-ref HEAD >actual && + echo refs/heads/master >expect && + test_cmp expect actual +' + test_done -- cgit v0.10.2-6-g49f6 From ae0ba8e20a2fb57299381ba2daa7c6d1863320d0 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 19 Jan 2010 23:17:11 -0800 Subject: Teach @{upstream} syntax to strbuf_branchanme() This teaches @{upstream} syntax to interpret_branch_name(), instead of dwim_ref() machinery. There are places in git UI that behaves differently when you give a local branch name and when you give an extended SHA-1 expression that evaluates to the commit object name at the tip of the branch. The intent is that the special syntax such as @{-1} can stand in as if the user spelled the name of the branch in such places. The name of the branch "frotz" to switch to ("git checkout frotz"), and the name of the branch "nitfol" to fork a new branch "frotz" from ("git checkout -b frotz nitfol"), are examples of such places. These places take only the name of the branch (e.g. "frotz"), and they are supposed to act differently to an equivalent refname (e.g. "refs/heads/frotz"), so hooking the @{upstream} and @{-N} syntax to dwim_ref() is insufficient when we want to deal with cases a local branch is forked from another local branch and use "forked@{upstream}" to name the forkee branch. The "upstream" syntax "forked@{u}" is to specify the ref that "forked" is configured to merge with, and most often the forkee is a remote tracking branch, not a local branch. We cannot simply return a local branch name, but that does not necessarily mean we have to returns the full refname (e.g. refs/remotes/origin/frotz, when returning origin/frotz is enough). This update calls shorten_unambiguous_ref() to do so. Signed-off-by: Junio C Hamano diff --git a/sha1_name.c b/sha1_name.c index fb4e214..2376c6d 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -239,24 +239,10 @@ static int ambiguous_path(const char *path, int len) return slash; } -static inline int tracked_suffix(const char *string, int len) -{ - const char *suffix[] = { "@{upstream}", "@{u}" }; - int i; - - for (i = 0; i < ARRAY_SIZE(suffix); i++) { - int suffix_len = strlen(suffix[i]); - if (len >= suffix_len && !memcmp(string + len - suffix_len, - suffix[i], suffix_len)) - return suffix_len; - } - return 0; -} - /* * *string and *len will only be substituted, and *string returned (for - * later free()ing) if the string passed in is of the form @{-} or - * of the form @{upstream}. + * later free()ing) if the string passed in is a magic short-hand form + * to name a branch. */ static char *substitute_branch_name(const char **string, int *len) { @@ -270,21 +256,6 @@ static char *substitute_branch_name(const char **string, int *len) return (char *)*string; } - ret = tracked_suffix(*string, *len); - if (ret) { - char *ref = xstrndup(*string, *len - ret); - struct branch *tracking = branch_get(*ref ? ref : NULL); - - if (!tracking) - die ("No tracking branch found for '%s'", ref); - free(ref); - if (tracking->merge && tracking->merge[0]->dst) { - *string = xstrdup(tracking->merge[0]->dst); - *len = strlen(*string); - return (char *)*string; - } - } - return NULL; } @@ -354,6 +325,20 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log) return logs_found; } +static inline int upstream_mark(const char *string, int len) +{ + const char *suffix[] = { "@{upstream}", "@{u}" }; + int i; + + for (i = 0; i < ARRAY_SIZE(suffix); i++) { + int suffix_len = strlen(suffix[i]); + if (suffix_len <= len + && !memcmp(string, suffix[i], suffix_len)) + return suffix_len; + } + return 0; +} + static int get_sha1_1(const char *name, int len, unsigned char *sha1); static int get_sha1_basic(const char *str, int len, unsigned char *sha1) @@ -371,7 +356,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1) if (len && str[len-1] == '}') { for (at = len-2; at >= 0; at--) { if (str[at] == '@' && str[at+1] == '{') { - if (!tracked_suffix(str + at, len - at)) { + if (!upstream_mark(str + at, len - at)) { reflog_len = (len-1) - (at+2); len = at; } @@ -773,17 +758,10 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1, } /* - * This reads "@{-N}" syntax, finds the name of the Nth previous - * branch we were on, and places the name of the branch in the given - * buf and returns the number of characters parsed if successful. - * - * If the input is not of the accepted format, it returns a negative - * number to signal an error. - * - * If the input was ok but there are not N branch switches in the - * reflog, it returns 0. + * Parse @{-N} syntax, return the number of characters parsed + * if successful; otherwise signal an error with negative value. */ -int interpret_branch_name(const char *name, struct strbuf *buf) +static int interpret_nth_prior_checkout(const char *name, struct strbuf *buf) { long nth; int i, retval; @@ -828,6 +806,60 @@ release_return: } /* + * This reads short-hand syntax that not only evaluates to a commit + * object name, but also can act as if the end user spelled the name + * of the branch from the command line. + * + * - "@{-N}" finds the name of the Nth previous branch we were on, and + * places the name of the branch in the given buf and returns the + * number of characters parsed if successful. + * + * - "@{upstream}" finds the name of the other ref that + * is configured to merge with (missing defaults + * to the current branch), and places the name of the branch in the + * given buf and returns the number of characters parsed if + * successful. + * + * If the input is not of the accepted format, it returns a negative + * number to signal an error. + * + * If the input was ok but there are not N branch switches in the + * reflog, it returns 0. + */ +int interpret_branch_name(const char *name, struct strbuf *buf) +{ + char *cp; + struct branch *upstream; + int namelen = strlen(name); + int len = interpret_nth_prior_checkout(name, buf); + int tmp_len; + + if (!len) + return len; /* syntax Ok, not enough switches */ + if (0 < len) + return len; /* consumed from the front */ + cp = strchr(name, '@'); + if (!cp) + return -1; + tmp_len = upstream_mark(cp, namelen - (cp - name)); + if (!tmp_len) + return -1; + len = cp + tmp_len - name; + cp = xstrndup(name, cp - name); + upstream = branch_get(*cp ? cp : NULL); + if (!upstream + || !upstream->merge + || !upstream->merge[0]->dst) + return error("No upstream branch found for '%s'", cp); + free(cp); + cp = shorten_unambiguous_ref(upstream->merge[0]->dst, 0); + strbuf_reset(buf); + strbuf_addstr(buf, cp); + free(cp); + return len; +} + +/* * This is like "get_sha1_basic()", except it allows "sha1 expressions", * notably "xyz^" for "parent of xyz" */ diff --git a/t/t1506-rev-parse-upstream.sh b/t/t1506-rev-parse-upstream.sh index a2c7f92..95c9b09 100755 --- a/t/t1506-rev-parse-upstream.sh +++ b/t/t1506-rev-parse-upstream.sh @@ -76,7 +76,7 @@ test_expect_success 'checkout -b new my-side@{u} forks from the same' ' ) ' -test_expect_failure 'merge my-side@{u} records the correct name' ' +test_expect_success 'merge my-side@{u} records the correct name' ' ( sq="'\''" && cd clone || exit @@ -90,7 +90,7 @@ test_expect_failure 'merge my-side@{u} records the correct name' ' ) ' -test_expect_failure 'branch -d other@{u}' ' +test_expect_success 'branch -d other@{u}' ' git checkout -t -b other master && git branch -d @{u} && git for-each-ref refs/heads/master >actual && @@ -98,7 +98,7 @@ test_expect_failure 'branch -d other@{u}' ' test_cmp expect actual ' -test_expect_failure 'checkout other@{u}' ' +test_expect_success 'checkout other@{u}' ' git branch -f master HEAD && git checkout -t -b another master && git checkout @{u} && -- cgit v0.10.2-6-g49f6