It can be helpful when creating a new branch to use the existing tracking configuration from the branch point. However, there is currently not a method to automatically do so. Teach branch.autoSetupMerge a new "inherit" option. When this is set, creating a new branch will cause the tracking configuration to default to the configuration of the branch point, if set. For example, if branch.autoSetupMerge=inherit, branch "main" tracks "origin/main", and we run `git checkout -b feature main`, then branch "feature" will track "origin/main". Thus, `git status` will show us how far ahead/behind we are from origin, and `git pull` will pull from origin. This is particularly useful when creating branches across many submodules, such as with `git submodule foreach ...` (or if running with a patch such as [1], which we use at $job), as it avoids having to manually set tracking info for each submodule. [1]: https://lore.kernel.org/git/20180927221603.148025-1-sbeller@xxxxxxxxxx/ Signed-off-by: Josh Steadmon <steadmon@xxxxxxxxxx> --- After a bit of testing, I've verified that this still works as intended even without the extra patch [1] linked above. I've added documentation and tests. Range-diff against v1: 1: 9628d14588 ! 1: 0346f44754 branch: add "inherit" option for branch.autoSetupMerge @@ Commit message creating a new branch will cause the tracking configuration to default to the configuration of the branch point, if set. - NEEDS WORK: - * this breaks `git checkout -b new-branch --recurse-submodules` - * add documentation - * add tests - * check corner cases, including whether this plays well with related - cmd-line options (switch, checkout, branch) + For example, if branch.autoSetupMerge=inherit, branch "main" tracks + "origin/main", and we run `git checkout -b feature main`, then branch + "feature" will track "origin/main". Thus, `git status` will show us how + far ahead/behind we are from origin, and `git pull` will pull from + origin. + + This is particularly useful when creating branches across many + submodules, such as with `git submodule foreach ...` (or if running with + a patch such as [1], which we use at $job), as it avoids having to + manually set tracking info for each submodule. + + [1]: https://lore.kernel.org/git/20180927221603.148025-1-sbeller@xxxxxxxxxx/ + ## Documentation/config/branch.txt ## +@@ Documentation/config/branch.txt: branch.autoSetupMerge:: + automatic setup is done; `true` -- automatic setup is done when the + starting point is a remote-tracking branch; `always` -- + automatic setup is done when the starting point is either a +- local branch or remote-tracking ++ local branch or remote-tracking branch; `inherit` -- if the starting point ++ has a tracking configuration, it is copied to the new + branch. This option defaults to true. + + branch.autoSetupRebase:: + + ## Documentation/git-branch.txt ## +@@ Documentation/git-branch.txt: This behavior is the default when the start point is a remote-tracking branch. + Set the branch.autoSetupMerge configuration variable to `false` if you + want `git switch`, `git checkout` and `git branch` to always behave as if `--no-track` + were given. Set it to `always` if you want this behavior when the +-start-point is either a local or remote-tracking branch. ++start-point is either a local or remote-tracking branch. Set it to ++`inherit` if you want to copy the tracking configuration from the ++start point. + + --no-track:: + Do not set up "upstream" configuration, even if the + ## branch.c ## @@ branch.c: int install_branch_config(int flag, const char *local, const char *origin, const return -1; @@ config.c: static int git_default_branch_config(const char *var, const char *valu } git_branch_track = git_config_bool(var, value); return 0; + + ## t/t2017-checkout-orphan.sh ## +@@ t/t2017-checkout-orphan.sh: test_expect_success '--orphan ignores branch.autosetupmerge' ' + git checkout --orphan gamma && + test -z "$(git config branch.gamma.merge)" && + test refs/heads/gamma = "$(git symbolic-ref HEAD)" && ++ test_must_fail git rev-parse --verify HEAD^ && ++ git checkout main && ++ git config branch.autosetupmerge inherit && ++ git checkout --orphan eta && ++ test -z "$(git config branch.eta.merge)" && ++ test -z "$(git config branch.eta.remote)" && ++ test refs/heads/eta = "$(git symbolic-ref HEAD)" && + test_must_fail git rev-parse --verify HEAD^ + ' + + + ## t/t2027-checkout-track.sh ## +@@ t/t2027-checkout-track.sh: test_expect_success 'checkout --track -b rejects an extra path argument' ' + test_i18ngrep "cannot be used with updating paths" err + ' + ++test_expect_success 'checkout --track -b overrides autoSetupMerge=inherit' ' ++ # Set up tracking config on main ++ git config branch.main.remote origin && ++ git config branch.main.merge refs/heads/main && ++ test_config branch.autoSetupMerge inherit && ++ # With branch.autoSetupMerge=inherit, we copy the tracking config ++ git checkout -b b1 main && ++ test_cmp_config origin branch.b1.remote && ++ test_cmp_config refs/heads/main branch.b1.merge && ++ # But --track overrides this ++ git checkout --track -b b2 main && ++ test_cmp_config . branch.b2.remote && ++ test_cmp_config refs/heads/main branch.b2.merge ++' ++ + test_done + + ## t/t2060-switch.sh ## +@@ t/t2060-switch.sh: test_expect_success 'not switching when something is in progress' ' + test_must_fail git switch -d @^ + ' + ++test_expect_success 'tracking info copied with autoSetupMerge=inherit' ' ++ # default config does not copy tracking info ++ git switch -c foo-no-inherit foo && ++ test -z "$(git config branch.foo-no-inherit.remote)" && ++ test -z "$(git config branch.foo-no-inherit.merge)" && ++ # with autoSetupMerge=inherit, we copy tracking info from foo ++ test_config branch.autoSetupMerge inherit && ++ git switch -c foo2 foo && ++ test_cmp_config origin branch.foo2.remote && ++ test_cmp_config refs/heads/foo branch.foo2.merge && ++ # no tracking info to inherit from main ++ git switch -c main2 main && ++ test -z "$(git config branch.main2.remote)" && ++ test -z "$(git config branch.main2.merge)" ++' ++ + test_done + + ## t/t3200-branch.sh ## +@@ t/t3200-branch.sh: test_expect_success 'invalid sort parameter in configuration' ' + ) + ' + ++test_expect_success 'tracking info copied with autoSetupMerge=inherit' ' ++ test_unconfig branch.autoSetupMerge && ++ # default config does not copy tracking info ++ git branch foo-no-inherit my1 && ++ test -z "$(git config branch.foo-no-inherit.remote)" && ++ test -z "$(git config branch.foo-no-inherit.merge)" && ++ # with autoSetupMerge=inherit, we copy tracking info from my1 ++ test_config branch.autoSetupMerge inherit && ++ git branch foo2 my1 && ++ test_cmp_config local branch.foo2.remote && ++ test_cmp_config refs/heads/main branch.foo2.merge && ++ # no tracking info to inherit from main ++ git branch main2 main && ++ test -z "$(git config branch.main2.remote)" && ++ test -z "$(git config branch.main2.merge)" ++' ++ + test_done + + ## t/t7201-co.sh ## +@@ t/t7201-co.sh: test_expect_success 'custom merge driver with checkout -m' ' + test_cmp expect arm + ' + ++test_expect_success 'tracking info copied with autoSetupMerge=inherit' ' ++ git reset --hard main && ++ # default config does not copy tracking info ++ git checkout -b foo-no-inherit koala/bear && ++ test -z "$(git config branch.foo-no-inherit.remote)" && ++ test -z "$(git config branch.foo-no-inherit.merge)" && ++ # with autoSetupMerge=inherit, we copy tracking info from koala/bear ++ test_config branch.autoSetupMerge inherit && ++ git checkout -b foo koala/bear && ++ test_cmp_config origin branch.foo.remote && ++ test_cmp_config refs/heads/koala/bear branch.foo.merge && ++ # no tracking info to inherit from main ++ git checkout -b main2 main && ++ test -z "$(git config branch.main2.remote)" && ++ test -z "$(git config branch.main2.merge)" ++' ++ + test_done Documentation/config/branch.txt | 3 ++- Documentation/git-branch.txt | 4 +++- branch.c | 36 ++++++++++++++++++++++++++++++++- branch.h | 3 ++- config.c | 3 +++ t/t2017-checkout-orphan.sh | 7 +++++++ t/t2027-checkout-track.sh | 15 ++++++++++++++ t/t2060-switch.sh | 16 +++++++++++++++ t/t3200-branch.sh | 17 ++++++++++++++++ t/t7201-co.sh | 17 ++++++++++++++++ 10 files changed, 117 insertions(+), 4 deletions(-) diff --git a/Documentation/config/branch.txt b/Documentation/config/branch.txt index cc5f3249fc..55f7522e12 100644 --- a/Documentation/config/branch.txt +++ b/Documentation/config/branch.txt @@ -7,7 +7,8 @@ branch.autoSetupMerge:: automatic setup is done; `true` -- automatic setup is done when the starting point is a remote-tracking branch; `always` -- automatic setup is done when the starting point is either a - local branch or remote-tracking + local branch or remote-tracking branch; `inherit` -- if the starting point + has a tracking configuration, it is copied to the new branch. This option defaults to true. branch.autoSetupRebase:: diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index 94dc9a54f2..81e901b8e8 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -218,7 +218,9 @@ This behavior is the default when the start point is a remote-tracking branch. Set the branch.autoSetupMerge configuration variable to `false` if you want `git switch`, `git checkout` and `git branch` to always behave as if `--no-track` were given. Set it to `always` if you want this behavior when the -start-point is either a local or remote-tracking branch. +start-point is either a local or remote-tracking branch. Set it to +`inherit` if you want to copy the tracking configuration from the +start point. --no-track:: Do not set up "upstream" configuration, even if the diff --git a/branch.c b/branch.c index 7a88a4861e..17d4cc5128 100644 --- a/branch.c +++ b/branch.c @@ -126,6 +126,38 @@ int install_branch_config(int flag, const char *local, const char *origin, const return -1; } +static int inherit_tracking(struct tracking *tracking, const char *orig_ref) +{ + struct strbuf key = STRBUF_INIT; + char *remote; + const char *bare_ref; + + bare_ref = orig_ref; + skip_prefix(orig_ref, "refs/heads/", &bare_ref); + + strbuf_addf(&key, "branch.%s.remote", bare_ref); + if (git_config_get_string(key.buf, &remote)) { + warning("branch.autoSetupMerge=inherit, but could not find %s", + key.buf); + strbuf_release(&key); + return 1; + } + tracking->remote = remote; + + strbuf_reset(&key); + strbuf_addf(&key, "branch.%s.merge", bare_ref); + if (git_config_get_string(key.buf, &tracking->src)) { + warning("branch.autoSetupMerge=inherit, but could not find %s", + key.buf); + strbuf_release(&key); + return 1; + } + + tracking->matches = 1; + strbuf_release(&key); + return 0; +} + /* * 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 @@ -139,7 +171,9 @@ static void setup_tracking(const char *new_ref, const char *orig_ref, memset(&tracking, 0, sizeof(tracking)); tracking.spec.dst = (char *)orig_ref; - if (for_each_remote(find_tracked_branch, &tracking)) + if (track == BRANCH_TRACK_INHERIT && inherit_tracking(&tracking, orig_ref)) + return; + else if (for_each_remote(find_tracked_branch, &tracking)) return; if (!tracking.matches) diff --git a/branch.h b/branch.h index df0be61506..6484bda8a2 100644 --- a/branch.h +++ b/branch.h @@ -10,7 +10,8 @@ enum branch_track { BRANCH_TRACK_REMOTE, BRANCH_TRACK_ALWAYS, BRANCH_TRACK_EXPLICIT, - BRANCH_TRACK_OVERRIDE + BRANCH_TRACK_OVERRIDE, + BRANCH_TRACK_INHERIT }; extern enum branch_track git_branch_track; diff --git a/config.c b/config.c index cb4a8058bf..4bd5a18faf 100644 --- a/config.c +++ b/config.c @@ -1580,6 +1580,9 @@ static int git_default_branch_config(const char *var, const char *value) if (value && !strcasecmp(value, "always")) { git_branch_track = BRANCH_TRACK_ALWAYS; return 0; + } else if (value && !strcasecmp(value, "inherit")) { + git_branch_track = BRANCH_TRACK_INHERIT; + return 0; } git_branch_track = git_config_bool(var, value); return 0; diff --git a/t/t2017-checkout-orphan.sh b/t/t2017-checkout-orphan.sh index 88d6992a5e..31fb64c5be 100755 --- a/t/t2017-checkout-orphan.sh +++ b/t/t2017-checkout-orphan.sh @@ -64,6 +64,13 @@ test_expect_success '--orphan ignores branch.autosetupmerge' ' git checkout --orphan gamma && test -z "$(git config branch.gamma.merge)" && test refs/heads/gamma = "$(git symbolic-ref HEAD)" && + test_must_fail git rev-parse --verify HEAD^ && + git checkout main && + git config branch.autosetupmerge inherit && + git checkout --orphan eta && + test -z "$(git config branch.eta.merge)" && + test -z "$(git config branch.eta.remote)" && + test refs/heads/eta = "$(git symbolic-ref HEAD)" && test_must_fail git rev-parse --verify HEAD^ ' diff --git a/t/t2027-checkout-track.sh b/t/t2027-checkout-track.sh index 4453741b96..4805965872 100755 --- a/t/t2027-checkout-track.sh +++ b/t/t2027-checkout-track.sh @@ -24,4 +24,19 @@ test_expect_success 'checkout --track -b rejects an extra path argument' ' test_i18ngrep "cannot be used with updating paths" err ' +test_expect_success 'checkout --track -b overrides autoSetupMerge=inherit' ' + # Set up tracking config on main + git config branch.main.remote origin && + git config branch.main.merge refs/heads/main && + test_config branch.autoSetupMerge inherit && + # With branch.autoSetupMerge=inherit, we copy the tracking config + git checkout -b b1 main && + test_cmp_config origin branch.b1.remote && + test_cmp_config refs/heads/main branch.b1.merge && + # But --track overrides this + git checkout --track -b b2 main && + test_cmp_config . branch.b2.remote && + test_cmp_config refs/heads/main branch.b2.merge +' + test_done diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh index 9bc6a3aa5c..f9972e2841 100755 --- a/t/t2060-switch.sh +++ b/t/t2060-switch.sh @@ -107,4 +107,20 @@ test_expect_success 'not switching when something is in progress' ' test_must_fail git switch -d @^ ' +test_expect_success 'tracking info copied with autoSetupMerge=inherit' ' + # default config does not copy tracking info + git switch -c foo-no-inherit foo && + test -z "$(git config branch.foo-no-inherit.remote)" && + test -z "$(git config branch.foo-no-inherit.merge)" && + # with autoSetupMerge=inherit, we copy tracking info from foo + test_config branch.autoSetupMerge inherit && + git switch -c foo2 foo && + test_cmp_config origin branch.foo2.remote && + test_cmp_config refs/heads/foo branch.foo2.merge && + # no tracking info to inherit from main + git switch -c main2 main && + test -z "$(git config branch.main2.remote)" && + test -z "$(git config branch.main2.merge)" +' + test_done diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index cc4b10236e..8005a5ccc6 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -1409,4 +1409,21 @@ test_expect_success 'invalid sort parameter in configuration' ' ) ' +test_expect_success 'tracking info copied with autoSetupMerge=inherit' ' + test_unconfig branch.autoSetupMerge && + # default config does not copy tracking info + git branch foo-no-inherit my1 && + test -z "$(git config branch.foo-no-inherit.remote)" && + test -z "$(git config branch.foo-no-inherit.merge)" && + # with autoSetupMerge=inherit, we copy tracking info from my1 + test_config branch.autoSetupMerge inherit && + git branch foo2 my1 && + test_cmp_config local branch.foo2.remote && + test_cmp_config refs/heads/main branch.foo2.merge && + # no tracking info to inherit from main + git branch main2 main && + test -z "$(git config branch.main2.remote)" && + test -z "$(git config branch.main2.merge)" +' + test_done diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 7f6e23a4bb..ae9f8d02c2 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -657,4 +657,21 @@ test_expect_success 'custom merge driver with checkout -m' ' test_cmp expect arm ' +test_expect_success 'tracking info copied with autoSetupMerge=inherit' ' + git reset --hard main && + # default config does not copy tracking info + git checkout -b foo-no-inherit koala/bear && + test -z "$(git config branch.foo-no-inherit.remote)" && + test -z "$(git config branch.foo-no-inherit.merge)" && + # with autoSetupMerge=inherit, we copy tracking info from koala/bear + test_config branch.autoSetupMerge inherit && + git checkout -b foo koala/bear && + test_cmp_config origin branch.foo.remote && + test_cmp_config refs/heads/koala/bear branch.foo.merge && + # no tracking info to inherit from main + git checkout -b main2 main && + test -z "$(git config branch.main2.remote)" && + test -z "$(git config branch.main2.merge)" +' + test_done -- 2.33.0.309.g3052b89438-goog