branch --track: code cleanup and saner handling of local branches

This patch cleans up some complicated code, and replaces it with a
cleaner version, using code from remote.[ch], which got extended a
little in the process.  This also enables us to fix two cases:

The earlier "fix" to setup tracking only when the original ref started
with "refs/remotes" is wrong.  You are absolutely allowed to use a
separate layout for your tracking branches.  The correct fix, of course,
is to set up tracking information only when there is a matching
remote.<nick>.fetch line containing a colon.

Another corner case was not handled properly.  If two remotes write to
the original ref, just warn the user and do not set up tracking.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Johannes Schindelin 2007-07-10 18:50:44 +01:00 committed by Junio C Hamano
parent b42f69273b
commit 6f084a56fc
4 changed files with 68 additions and 131 deletions

View File

@ -309,10 +309,7 @@ branch.autosetupmerge::
so that gitlink:git-pull[1] will appropriately merge from that so that gitlink:git-pull[1] will appropriately merge from that
remote branch. Note that even if this option is not set, remote branch. Note that even if this option is not set,
this behavior can be chosen per-branch using the `--track` this behavior can be chosen per-branch using the `--track`
and `--no-track` options. This option can have values and `--no-track` options. This option defaults to false.
'false' (never touch the configuration), 'all' (do this
for all branches), or 'true' (do this only when
branching from a remote tracking branch), and defaults to 'true'.
branch.<name>.remote:: branch.<name>.remote::
When in branch <name>, it tells `git fetch` which remote to fetch. When in branch <name>, it tells `git fetch` which remote to fetch.

View File

@ -52,9 +52,8 @@ OPTIONS
set up configuration so that git-pull will automatically set up configuration so that git-pull will automatically
retrieve data from the remote branch. Set the retrieve data from the remote branch. Set the
branch.autosetupmerge configuration variable to true if you branch.autosetupmerge configuration variable to true if you
want git-checkout and git-branch to behave as if want git-checkout and git-branch to always behave as if
'--track' were given when you branch from a remote '--track' were given.
tracking branch.
--no-track:: --no-track::
When -b is given and a branch is created off a remote branch, When -b is given and a branch is created off a remote branch,

View File

@ -10,6 +10,7 @@
#include "refs.h" #include "refs.h"
#include "commit.h" #include "commit.h"
#include "builtin.h" #include "builtin.h"
#include "remote.h"
static const char builtin_branch_usage[] = static const char builtin_branch_usage[] =
"git-branch [-r] (-d | -D) <branchname> | [--track | --no-track] [-l] [-f] <branchname> [<start-point>] | (-m | -M) [<oldbranch>] <newbranch> | [--color | --no-color] [-r | -a] [-v [--abbrev=<length> | --no-abbrev]]"; "git-branch [-r] (-d | -D) <branchname> | [--track | --no-track] [-l] [-f] <branchname> [<start-point>] | (-m | -M) [<oldbranch>] <newbranch> | [--color | --no-color] [-r | -a] [-v [--abbrev=<length> | --no-abbrev]]";
@ -22,7 +23,7 @@ static const char builtin_branch_usage[] =
static const char *head; static const char *head;
static unsigned char head_sha1[20]; static unsigned char head_sha1[20];
static int branch_track = 1; /* 0 = none, 1 = remotes, 2 = all */ static int branch_track = 1;
static int branch_use_color; static int branch_use_color;
static char branch_colors[][COLOR_MAXLEN] = { static char branch_colors[][COLOR_MAXLEN] = {
@ -66,12 +67,8 @@ static int git_branch_config(const char *var, const char *value)
color_parse(value, var, branch_colors[slot]); color_parse(value, var, branch_colors[slot]);
return 0; return 0;
} }
if (!strcmp(var, "branch.autosetupmerge")) { if (!strcmp(var, "branch.autosetupmerge"))
if (!strcmp(value, "all"))
branch_track = 2;
else
branch_track = git_config_bool(var, value); branch_track = git_config_bool(var, value);
}
return git_default_config(var, value); return git_default_config(var, value);
} }
@ -328,125 +325,70 @@ static void print_ref_list(int kinds, int detached, int verbose, int abbrev)
free_ref_list(&ref_list); free_ref_list(&ref_list);
} }
static char *config_repo; struct tracking {
static char *config_remote; struct refspec spec;
static const char *start_ref; char *src;
const char *remote;
int matches;
};
static int get_remote_branch_name(const char *value) static int find_tracked_branch(struct remote *remote, void *priv)
{ {
const char *colon; struct tracking *tracking = priv;
const char *end;
if (*value == '+') if (!remote_find_tracking(remote, &tracking->spec)) {
value++; if (++tracking->matches == 1) {
tracking->src = tracking->spec.src;
colon = strchr(value, ':'); tracking->remote = remote->name;
if (!colon) } else {
return 0; free(tracking->spec.src);
if (tracking->src) {
end = value + strlen(value); free(tracking->src);
tracking->src = NULL;
/* }
* Try an exact match first. I.e. handle the case where the }
* value is "$anything:refs/foo/bar/baz" and start_ref is exactly tracking->spec.src = NULL;
* "refs/foo/bar/baz". Then the name at the remote is $anything.
*/
if (!strcmp(colon + 1, start_ref)) {
/* Truncate the value before the colon. */
nfasprintf(&config_repo, "%.*s", colon - value, value);
return 1;
} }
/*
* Is this a wildcard match?
*/
if ((end - 2 <= value) || end[-2] != '/' || end[-1] != '*' ||
(colon - 2 <= value) || colon[-2] != '/' || colon[-1] != '*')
return 0;
/*
* Value is "refs/foo/bar/<asterisk>:refs/baz/boa/<asterisk>"
* and start_ref begins with "refs/baz/boa/"; the name at the
* remote is refs/foo/bar/ with the remaining part of the
* start_ref. The length of the prefix on the RHS is (end -
* colon - 2), including the slash immediately before the
* asterisk.
*/
if ((strlen(start_ref) < end - colon - 2) ||
memcmp(start_ref, colon + 1, end - colon - 2))
return 0; /* does not match prefix */
/* Replace the asterisk with the remote branch name. */
nfasprintf(&config_repo, "%.*s%s",
(colon - 1) - value, value,
start_ref + (end - colon - 2));
return 1;
}
static int get_remote_config(const char *key, const char *value)
{
const char *var;
if (prefixcmp(key, "remote."))
return 0;
var = strrchr(key, '.');
if (var == key + 6 || strcmp(var, ".fetch"))
return 0;
/*
* Ok, we are looking at key == "remote.$foo.fetch";
*/
if (get_remote_branch_name(value))
nfasprintf(&config_remote, "%.*s", var - (key + 7), key + 7);
return 0; return 0;
} }
static void set_branch_merge(const char *name, const char *config_remote,
const char *config_repo) /*
* This is called when new_ref is branched off of orig_ref, and tries
* to infer the settings for branch.<new_ref>.{remote,merge} from the
* config.
*/
static int setup_tracking(const char *new_ref, const char *orig_ref)
{ {
char key[1024]; char key[1024];
if (sizeof(key) <= struct tracking tracking;
snprintf(key, sizeof(key), "branch.%s.remote", name))
die("what a long branch name you have!");
git_config_set(key, config_remote);
/* if (strlen(new_ref) > 1024 - 7 - 7 - 1)
* We do not have to check if we have enough space for return error("Tracking not set up: name too long: %s",
* the 'merge' key, since it's shorter than the new_ref);
* previous 'remote' key, which we already checked.
*/
snprintf(key, sizeof(key), "branch.%s.merge", name);
git_config_set(key, config_repo);
}
static void set_branch_defaults(const char *name, const char *real_ref) memset(&tracking, 0, sizeof(tracking));
{ tracking.spec.dst = (char *)orig_ref;
/* if (for_each_remote(find_tracked_branch, &tracking) ||
* name is the name of new branch under refs/heads; !tracking.matches)
* real_ref is typically refs/remotes/$foo/$bar, where return 1;
* $foo is the remote name (there typically are no slashes)
* and $bar is the branch name we map from the remote
* (it could have slashes).
*/
start_ref = real_ref;
git_config(get_remote_config);
if (!config_repo && !config_remote &&
!prefixcmp(real_ref, "refs/heads/")) {
set_branch_merge(name, ".", real_ref);
printf("Branch %s set up to track local branch %s.\n",
name, real_ref);
}
if (config_repo && config_remote) { if (tracking.matches > 1)
set_branch_merge(name, config_remote, config_repo); return error("Not tracking: ambiguous information for ref %s",
orig_ref);
if (tracking.matches == 1) {
sprintf(key, "branch.%s.remote", new_ref);
git_config_set(key, tracking.remote ? tracking.remote : ".");
sprintf(key, "branch.%s.merge", new_ref);
git_config_set(key, tracking.src);
free(tracking.src);
printf("Branch %s set up to track remote branch %s.\n", printf("Branch %s set up to track remote branch %s.\n",
name, real_ref); new_ref, orig_ref);
} }
if (config_repo) return 0;
free(config_repo);
if (config_remote)
free(config_remote);
} }
static void create_branch(const char *name, const char *start_name, static void create_branch(const char *name, const char *start_name,
@ -508,10 +450,8 @@ static void create_branch(const char *name, const char *start_name,
/* When branching off a remote branch, set up so that git-pull /* When branching off a remote branch, set up so that git-pull
automatically merges from there. So far, this is only done for automatically merges from there. So far, this is only done for
remotes registered via .git/config. */ remotes registered via .git/config. */
if (real_ref && (track == 2 || if (real_ref && track)
(track == 1 && setup_tracking(name, real_ref);
!prefixcmp(real_ref, "refs/remotes/"))))
set_branch_defaults(name, real_ref);
if (write_ref_sha1(lock, sha1, msg) < 0) if (write_ref_sha1(lock, sha1, msg) < 0)
die("Failed to write ref: %s.", strerror(errno)); die("Failed to write ref: %s.", strerror(errno));
@ -582,7 +522,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
break; break;
} }
if (!strcmp(arg, "--track")) { if (!strcmp(arg, "--track")) {
track = 2; track = 1;
continue; continue;
} }
if (!strcmp(arg, "--no-track")) { if (!strcmp(arg, "--no-track")) {

View File

@ -148,13 +148,14 @@ test_expect_success 'test tracking setup via config' \
test $(git config branch.my3.remote) = local && test $(git config branch.my3.remote) = local &&
test $(git config branch.my3.merge) = refs/heads/master' test $(git config branch.my3.merge) = refs/heads/master'
test_expect_success 'autosetupmerge = all' ' test_expect_success 'avoid ambiguous track' '
git config branch.autosetupmerge true && git config branch.autosetupmerge true &&
git config remote.ambi1.url = lalala &&
git config remote.ambi1.fetch = refs/heads/lalala:refs/heads/master &&
git config remote.ambi2.url = lilili &&
git config remote.ambi2.fetch = refs/heads/lilili:refs/heads/master &&
git branch all1 master && git branch all1 master &&
test -z "$(git config branch.all1.merge)" && test -z "$(git config branch.all1.merge)"
git config branch.autosetupmerge all &&
git branch all2 master &&
test $(git config branch.all2.merge) = refs/heads/master
' '
test_expect_success 'test overriding tracking setup via --no-track' \ test_expect_success 'test overriding tracking setup via --no-track' \
@ -167,10 +168,10 @@ test_expect_success 'test overriding tracking setup via --no-track' \
! test "$(git config branch.my2.remote)" = local && ! test "$(git config branch.my2.remote)" = local &&
! test "$(git config branch.my2.merge)" = refs/heads/master' ! test "$(git config branch.my2.merge)" = refs/heads/master'
test_expect_success 'test local tracking setup' \ test_expect_success 'no tracking without .fetch entries' \
'git branch --track my6 s && 'git branch --track my6 s &&
test $(git config branch.my6.remote) = . && test -z "$(git config branch.my6.remote)" &&
test $(git config branch.my6.merge) = refs/heads/s' test -z "$(git config branch.my6.merge)"'
test_expect_success 'test tracking setup via --track but deeper' \ test_expect_success 'test tracking setup via --track but deeper' \
'git config remote.local.url . && 'git config remote.local.url . &&
@ -182,8 +183,8 @@ test_expect_success 'test tracking setup via --track but deeper' \
test_expect_success 'test deleting branch deletes branch config' \ test_expect_success 'test deleting branch deletes branch config' \
'git branch -d my7 && 'git branch -d my7 &&
test "$(git config branch.my7.remote)" = "" && test -z "$(git config branch.my7.remote)" &&
test "$(git config branch.my7.merge)" = ""' test -z "$(git config branch.my7.merge)"'
test_expect_success 'test deleting branch without config' \ test_expect_success 'test deleting branch without config' \
'git branch my7 s && 'git branch my7 s &&