diff mbox series

[v7,5/6] branch: add --recurse-submodules option for branch creation

Message ID 20220124204442.39353-6-chooglen@google.com (mailing list archive)
State Superseded
Headers show
Series implement branch --recurse-submodules | expand

Commit Message

Glen Choo Jan. 24, 2022, 8:44 p.m. UTC
To improve the submodules UX, we would like to teach Git to handle
branches in submodules. Start this process by teaching "git branch" the
--recurse-submodules option so that "git branch --recurse-submodules
topic" will create the `topic` branch in the superproject and its
submodules.

Although this commit does not introduce breaking changes, it is
incompatible with existing --recurse-submodules commands because "git
branch --recurse-submodules" writes to the submodule ref store, but most
commands only consider the superproject gitlink and ignore the submodule
ref store. For example, "git checkout --recurse-submodules" will check
out the commits in the superproject gitlinks (and put the submodules in
detached HEAD) instead of checking out the submodule branches.

Because of this, this commit introduces a new configuration value,
`submodule.propagateBranches`. The plan is for Git commands to
prioritize submodule ref store information over superproject gitlinks if
this value is true. Because "git branch --recurse-submodules" writes to
submodule ref stores, for the sake of clarity, it will not function
unless this configuration value is set.

This commit also includes changes that support working with submodules
from a superproject commit because "branch --recurse-submodules" (and
future commands) need to read .gitmodules and gitlinks from the
superproject commit, but submodules are typically read from the
filesystem's .gitmodules and the index's gitlinks. These changes are:

* add a submodules_of_tree() helper that gives the relevant
  information of an in-tree submodule (e.g. path and oid) and
  initializes the repository
* add is_tree_submodule_active() by adding a treeish_name parameter to
  is_submodule_active()
* add the "submoduleNotUpdated" advice to advise users to update the
  submodules in their trees

Incidentally, fix an incorrect usage string that combined the 'list'
usage of git branch (-l) with the 'create' usage; this string has been
incorrect since its inception, a8dfd5eac4 (Make builtin-branch.c use
parse_options., 2007-10-07).

Helped-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Glen Choo <chooglen@google.com>
---
To Jonathan: When describing the differences between "git branch" and "git
checkout", I opted to emphasize the fact that "git checkout" checks out the
gitlink in detached HEAD and not the branches themselves (vs your suggestion of
mentioning that checkout would check out the wrong version if the branch is
subsequently updated).

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  37 ++--
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 137 ++++++++++++++
 branch.h                           |  22 +++
 builtin/branch.c                   |  44 ++++-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  60 ++++++
 submodule-config.h                 |  34 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3207-branch-submodule.sh        | 282 +++++++++++++++++++++++++++++
 14 files changed, 664 insertions(+), 20 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

Comments

Jonathan Tan Jan. 27, 2022, 8:29 p.m. UTC | #1
Patches up to this one look good, and the implementation looks good too.
I just have minor comments on some of the text and tests.

Glen Choo <chooglen@google.com> writes:
> Although this commit does not introduce breaking changes, it is
> incompatible with existing --recurse-submodules commands because "git
> branch --recurse-submodules" writes to the submodule ref store, but most
> commands only consider the superproject gitlink and ignore the submodule
> ref store. For example, "git checkout --recurse-submodules" will check
> out the commits in the superproject gitlinks (and put the submodules in
> detached HEAD) instead of checking out the submodule branches.

The usual meaning of "incompatible" is "cannot be used together", I
think, which is not what's happening here - it's just that the user
would expect them to work together in a specific way but they don't do
that.

> To Jonathan: When describing the differences between "git branch" and "git
> checkout", I opted to emphasize the fact that "git checkout" checks out the
> gitlink in detached HEAD and not the branches themselves (vs your suggestion of
> mentioning that checkout would check out the wrong version if the branch is
> subsequently updated).

OK - makes sense.

> +/**
> + * Creates a branch in a submodule by calling
> + * create_branches_recursively() in a child process. The child process
> + * is necessary because install_branch_config_multiple_remotes() (which
> + * is called by setup_tracking()) does not support writing configs to
> + * submodules.

OK, good explanation for why a child process is needed.

> + */
> +static int submodule_create_branch(struct repository *r,
> +				   const struct submodule *submodule,
> +				   const char *name, const char *start_oid,
> +				   const char *start_name, int force,
> +				   int reflog, int quiet,
> +				   enum branch_track track, int dry_run)
> +{
> +	int ret = 0;
> +	struct child_process child = CHILD_PROCESS_INIT;
> +	struct strbuf child_err = STRBUF_INIT;
> +	struct strbuf out_buf = STRBUF_INIT;
> +	char *out_prefix = xstrfmt("submodule '%s': ", submodule->name);
> +	child.git_cmd = 1;
> +	child.err = -1;
> +	child.stdout_to_stderr = 1;
> +
> +	prepare_other_repo_env(&child.env_array, r->gitdir);
> +	/*
> +	 * submodule_create_branch() is indirectly invoked by "git
> +	 * branch", but we cannot invoke "git branch" in the child
> +	 * process because it does not let us set start_name and
> +	 * start_oid separately (see create_branches_recursively()).

Probably clearer to enumerate the 3 different pieces of information
needed: the name of the branch to be created, the OID, and the name of
the branch that would be used for tracking purposes.

An argument could be made that "git branch" should be extended to be
able to take in these 3 different pieces of information, but it also
makes sense to put this functionality in submodule--helper for now,
since the whole thing is marked as experimental.

> +/*
> + * Creates a new branch in repository and its submodules (and its
> + * submodules, recursively). Besides these exceptions, the parameters
> + * function identically to create_branch():
> + *
> + * - start_name is the name of the ref, in repository r, that the new
> + *   branch should start from. In submodules, branches will start from
> + *   the respective gitlink commit ids in start_name's tree.
> + *
> + * - tracking_name is the name used of the ref that will be used to set
> + *   up tracking, e.g. origin/main. This is propagated to submodules so
> + *   that tracking information will appear as if the branch branched off
> + *   tracking_name instead of start_name (which is a plain commit id for
> + *   submodules). If omitted, start_name is used for tracking (just like
> + *   create_branch()).
> + *
> + */
> +void create_branches_recursively(struct repository *r, const char *name,
> +				 const char *start_name,
> +				 const char *tracking_name, int force,
> +				 int reflog, int quiet, enum branch_track track,
> +				 int dry_run);

Instead of taking in "name", "start_name", and "tracking_name", could we
take in "name", "oid", and "tracking_name"? That way, it's clearer what
each parameter is used for.

> +test_expect_success 'should not create branches in inactive submodules' '
> +	test_when_finished "reset_test" &&
> +	test_config -C super submodule.sub.active false &&
> +	(
> +		cd super &&
> +		git branch --recurse-submodules branch-a &&
> +		git rev-parse branch-a &&
> +		test_must_fail git -C sub branch-a
> +	)
> +'

The "test_must_fail" line doesn't make sense - there is no such command
branch-a. To avoid errors like this, maybe make sure that all
test_must_fail invocations are accompanied by assertions on the error
message. (And for "test_must_fail git rev-parse", we could have a helper
function here that asserts the "object not found".)

> +test_expect_success 'should set up tracking of local branches with track=always' '
> +	test_when_finished "reset_test" &&
> +	(
> +		cd super &&
> +		git -c branch.autoSetupMerge=always branch --recurse-submodules branch-a main &&
> +		git -C sub rev-parse main &&
> +		test "$(git -C sub config branch.branch-a.remote)" = . &&
> +		test "$(git -C sub config branch.branch-a.merge)" = refs/heads/main
> +	)
> +'

As described in t/README line 671, this means that the inner command
could silently fail. You can do `REMOTE=$(...) &&` <newline> `test ...`.
Same comment throughout this file.
Glen Choo Jan. 27, 2022, 9:32 p.m. UTC | #2
Jonathan Tan <jonathantanmy@google.com> writes:

>> Although this commit does not introduce breaking changes, it is
>> incompatible with existing --recurse-submodules commands because "git
>> branch --recurse-submodules" writes to the submodule ref store, but most
>> commands only consider the superproject gitlink and ignore the submodule
>> ref store. For example, "git checkout --recurse-submodules" will check
>> out the commits in the superproject gitlinks (and put the submodules in
>> detached HEAD) instead of checking out the submodule branches.
>
> The usual meaning of "incompatible" is "cannot be used together", I
> think, which is not what's happening here - it's just that the user
> would expect them to work together in a specific way but they don't do
> that.

Ok, I'll replace "incompatible with" with "does not interact well with".

>> + */
>> +static int submodule_create_branch(struct repository *r,
>> +				   const struct submodule *submodule,
>> +				   const char *name, const char *start_oid,
>> +				   const char *start_name, int force,
>> +				   int reflog, int quiet,
>> +				   enum branch_track track, int dry_run)
>> +{
>> +	int ret = 0;
>> +	struct child_process child = CHILD_PROCESS_INIT;
>> +	struct strbuf child_err = STRBUF_INIT;
>> +	struct strbuf out_buf = STRBUF_INIT;
>> +	char *out_prefix = xstrfmt("submodule '%s': ", submodule->name);
>> +	child.git_cmd = 1;
>> +	child.err = -1;
>> +	child.stdout_to_stderr = 1;
>> +
>> +	prepare_other_repo_env(&child.env_array, r->gitdir);
>> +	/*
>> +	 * submodule_create_branch() is indirectly invoked by "git
>> +	 * branch", but we cannot invoke "git branch" in the child
>> +	 * process because it does not let us set start_name and
>> +	 * start_oid separately (see create_branches_recursively()).
>
> Probably clearer to enumerate the 3 different pieces of information
> needed: the name of the branch to be created, the OID, and the name of
> the branch that would be used for tracking purposes.

Makes sense. Also the comment on create_branches_recursively() doesn't
do a great job of explaining why we need the additional piece of
information (tracking name), so I'll tidy them up together.

> An argument could be made that "git branch" should be extended to be
> able to take in these 3 different pieces of information, but it also
> makes sense to put this functionality in submodule--helper for now,
> since the whole thing is marked as experimental.

Yes, I considered this, but I can't think of a good reason to expose
this functionality to users - users can set their upstream with the more
intuitive "git branch --set-upstream-to".

>> +/*
>> + * Creates a new branch in repository and its submodules (and its
>> + * submodules, recursively). Besides these exceptions, the parameters
>> + * function identically to create_branch():
>> + *
>> + * - start_name is the name of the ref, in repository r, that the new
>> + *   branch should start from. In submodules, branches will start from
>> + *   the respective gitlink commit ids in start_name's tree.
>> + *
>> + * - tracking_name is the name used of the ref that will be used to set
>> + *   up tracking, e.g. origin/main. This is propagated to submodules so
>> + *   that tracking information will appear as if the branch branched off
>> + *   tracking_name instead of start_name (which is a plain commit id for
>> + *   submodules). If omitted, start_name is used for tracking (just like
>> + *   create_branch()).
>> + *
>> + */
>> +void create_branches_recursively(struct repository *r, const char *name,
>> +				 const char *start_name,
>> +				 const char *tracking_name, int force,
>> +				 int reflog, int quiet, enum branch_track track,
>> +				 int dry_run);
>
> Instead of taking in "name", "start_name", and "tracking_name", could we
> take in "name", "oid", and "tracking_name"? That way, it's clearer what
> each parameter is used for.

I used start_name to mirror create_branches(), but start_name makes less
sense here because it's ambiguous.

Since the start_name is always an oid or NULL, this makes sense. I'll do
this.

>> +test_expect_success 'should not create branches in inactive submodules' '
>> +	test_when_finished "reset_test" &&
>> +	test_config -C super submodule.sub.active false &&
>> +	(
>> +		cd super &&
>> +		git branch --recurse-submodules branch-a &&
>> +		git rev-parse branch-a &&
>> +		test_must_fail git -C sub branch-a
>> +	)
>> +'
>
> The "test_must_fail" line doesn't make sense - there is no such command
> branch-a. To avoid errors like this, maybe make sure that all
> test_must_fail invocations are accompanied by assertions on the error
> message. (And for "test_must_fail git rev-parse", we could have a helper
> function here that asserts the "object not found".)

Ah good catch. I'll make the test helper.

>
>> +test_expect_success 'should set up tracking of local branches with track=always' '
>> +	test_when_finished "reset_test" &&
>> +	(
>> +		cd super &&
>> +		git -c branch.autoSetupMerge=always branch --recurse-submodules branch-a main &&
>> +		git -C sub rev-parse main &&
>> +		test "$(git -C sub config branch.branch-a.remote)" = . &&
>> +		test "$(git -C sub config branch.branch-a.merge)" = refs/heads/main
>> +	)
>> +'
>
> As described in t/README line 671, this means that the inner command
> could silently fail. You can do `REMOTE=$(...) &&` <newline> `test ...`.
> Same comment throughout this file.

Thanks for the catch and double thanks for the recommended fix.

Will reroll soon.
Glen Choo Jan. 27, 2022, 10:42 p.m. UTC | #3
Glen Choo <chooglen@google.com> writes:

> Jonathan Tan <jonathantanmy@google.com> writes:
>
>>> +/*
>>> + * Creates a new branch in repository and its submodules (and its
>>> + * submodules, recursively). Besides these exceptions, the parameters
>>> + * function identically to create_branch():
>>> + *
>>> + * - start_name is the name of the ref, in repository r, that the new
>>> + *   branch should start from. In submodules, branches will start from
>>> + *   the respective gitlink commit ids in start_name's tree.
>>> + *
>>> + * - tracking_name is the name used of the ref that will be used to set
>>> + *   up tracking, e.g. origin/main. This is propagated to submodules so
>>> + *   that tracking information will appear as if the branch branched off
>>> + *   tracking_name instead of start_name (which is a plain commit id for
>>> + *   submodules). If omitted, start_name is used for tracking (just like
>>> + *   create_branch()).
>>> + *
>>> + */
>>> +void create_branches_recursively(struct repository *r, const char *name,
>>> +				 const char *start_name,
>>> +				 const char *tracking_name, int force,
>>> +				 int reflog, int quiet, enum branch_track track,
>>> +				 int dry_run);
>>
>> Instead of taking in "name", "start_name", and "tracking_name", could we
>> take in "name", "oid", and "tracking_name"? That way, it's clearer what
>> each parameter is used for.
>
> I used start_name to mirror create_branches(), but start_name makes less
> sense here because it's ambiguous.
>
> Since the start_name is always an oid or NULL, this makes sense. I'll do
> this.

Rereading the code, I realise that start_name isn't guaranteed to be an
oid or NULL. When create_branches_recursively() is called at the root
level i.e. cmd_branch(), start_name is a user-given revision which
may or may not be an object id. In that scenario, tracking_name is NULL
to indicate that we would like to track start_name.

I can still pass only oids, but that would require parsing the revision
in cmd_branch() itself, and that seems like a violation of the layering
we have set up.

Instead, I'll try to find an accurate, but more descriptive name than
start_name.
diff mbox series

Patch

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index 063eec2511..adee26fbbb 100644
--- a/Documentation/config/advice.txt
+++ b/Documentation/config/advice.txt
@@ -116,6 +116,9 @@  advice.*::
 	submoduleAlternateErrorStrategyDie::
 		Advice shown when a submodule.alternateErrorStrategy option
 		configured to "die" causes a fatal error.
+	submodulesNotUpdated::
+		Advice shown when a user runs a submodule command that fails
+		because `git submodule update --init` was not run.
 	addIgnoredFile::
 		Advice shown if a user attempts to add an ignored file to
 		the index.
diff --git a/Documentation/config/submodule.txt b/Documentation/config/submodule.txt
index ee454f8126..6490527b45 100644
--- a/Documentation/config/submodule.txt
+++ b/Documentation/config/submodule.txt
@@ -59,18 +59,33 @@  submodule.active::
 
 submodule.recurse::
 	A boolean indicating if commands should enable the `--recurse-submodules`
-	option by default.
-	Applies to all commands that support this option
-	(`checkout`, `fetch`, `grep`, `pull`, `push`, `read-tree`, `reset`,
-	`restore` and `switch`) except `clone` and `ls-files`.
+	option by default. Defaults to false.
++
+When set to true, it can be deactivated via the
+`--no-recurse-submodules` option. Note that some Git commands
+lacking this option may call some of the above commands affected by
+`submodule.recurse`; for instance `git remote update` will call
+`git fetch` but does not have a `--no-recurse-submodules` option.
+For these commands a workaround is to temporarily change the
+configuration value by using `git -c submodule.recurse=0`.
++
+The following list shows the commands that accept
+`--recurse-submodules` and whether they are supported by this
+setting.
+
+* `checkout`, `fetch`, `grep`, `pull`, `push`, `read-tree`,
+`reset`, `restore` and `switch` are always supported.
+* `clone` and `ls-files` are not supported.
+* `branch` is supported only if `submodule.propagateBranches` is
+enabled
+
+submodule.propagateBranches::
+	[EXPERIMENTAL] A boolean that enables branching support when
+	using `--recurse-submodules` or `submodule.recurse=true`.
+	Enabling this will allow certain commands to accept
+	`--recurse-submodules` and certain commands that already accept
+	`--recurse-submodules` will now consider branches.
 	Defaults to false.
-	When set to true, it can be deactivated via the
-	`--no-recurse-submodules` option. Note that some Git commands
-	lacking this option may call some of the above commands affected by
-	`submodule.recurse`; for instance `git remote update` will call
-	`git fetch` but does not have a `--no-recurse-submodules` option.
-	For these commands a workaround is to temporarily change the
-	configuration value by using `git -c submodule.recurse=0`.
 
 submodule.fetchJobs::
 	Specifies how many submodules are fetched/cloned at the same time.
diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt
index 75beea7bac..871ca3c3eb 100644
--- a/Documentation/git-branch.txt
+++ b/Documentation/git-branch.txt
@@ -16,7 +16,8 @@  SYNOPSIS
 	[--points-at <object>] [--format=<format>]
 	[(-r | --remotes) | (-a | --all)]
 	[--list] [<pattern>...]
-'git branch' [--track [direct|inherit] | --no-track] [-f] <branchname> [<start-point>]
+'git branch' [--track [direct|inherit] | --no-track] [-f]
+	[--recurse-submodules] <branchname> [<start-point>]
 'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
 'git branch' --unset-upstream [<branchname>]
 'git branch' (-m | -M) [<oldbranch>] <newbranch>
@@ -235,6 +236,14 @@  how the `branch.<name>.remote` and `branch.<name>.merge` options are used.
 	Do not set up "upstream" configuration, even if the
 	branch.autoSetupMerge configuration variable is set.
 
+--recurse-submodules::
+	THIS OPTION IS EXPERIMENTAL! Causes the current command to
+	recurse into submodules if `submodule.propagateBranches` is
+	enabled. See `submodule.propagateBranches` in
+	linkgit:git-config[1].
+	+
+	Currently, only branch creation is supported.
+
 --set-upstream::
 	As this option had confusing syntax, it is no longer supported.
 	Please use `--track` or `--set-upstream-to` instead.
diff --git a/advice.c b/advice.c
index 1dfc91d176..e00d30254c 100644
--- a/advice.c
+++ b/advice.c
@@ -70,6 +70,7 @@  static struct {
 	[ADVICE_STATUS_HINTS]				= { "statusHints", 1 },
 	[ADVICE_STATUS_U_OPTION]			= { "statusUoption", 1 },
 	[ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE] = { "submoduleAlternateErrorStrategyDie", 1 },
+	[ADVICE_SUBMODULES_NOT_UPDATED] 		= { "submodulesNotUpdated", 1 },
 	[ADVICE_UPDATE_SPARSE_PATH]			= { "updateSparsePath", 1 },
 	[ADVICE_WAITING_FOR_EDITOR]			= { "waitingForEditor", 1 },
 };
diff --git a/advice.h b/advice.h
index 601265fd10..a7521d6087 100644
--- a/advice.h
+++ b/advice.h
@@ -44,6 +44,7 @@  struct string_list;
 	ADVICE_STATUS_HINTS,
 	ADVICE_STATUS_U_OPTION,
 	ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE,
+	ADVICE_SUBMODULES_NOT_UPDATED,
 	ADVICE_UPDATE_SPARSE_PATH,
 	ADVICE_WAITING_FOR_EDITOR,
 	ADVICE_SKIPPED_CHERRY_PICKS,
diff --git a/branch.c b/branch.c
index 02d46a69b8..be33fe09fa 100644
--- a/branch.c
+++ b/branch.c
@@ -8,6 +8,8 @@ 
 #include "sequencer.h"
 #include "commit.h"
 #include "worktree.h"
+#include "submodule-config.h"
+#include "run-command.h"
 
 struct tracking {
 	struct refspec_item spec;
@@ -483,6 +485,141 @@  void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
 	setup_tracking(new_ref, real_orig_ref, track, quiet);
 }
 
+/**
+ * Creates a branch in a submodule by calling
+ * create_branches_recursively() in a child process. The child process
+ * is necessary because install_branch_config_multiple_remotes() (which
+ * is called by setup_tracking()) does not support writing configs to
+ * submodules.
+ */
+static int submodule_create_branch(struct repository *r,
+				   const struct submodule *submodule,
+				   const char *name, const char *start_oid,
+				   const char *start_name, int force,
+				   int reflog, int quiet,
+				   enum branch_track track, int dry_run)
+{
+	int ret = 0;
+	struct child_process child = CHILD_PROCESS_INIT;
+	struct strbuf child_err = STRBUF_INIT;
+	struct strbuf out_buf = STRBUF_INIT;
+	char *out_prefix = xstrfmt("submodule '%s': ", submodule->name);
+	child.git_cmd = 1;
+	child.err = -1;
+	child.stdout_to_stderr = 1;
+
+	prepare_other_repo_env(&child.env_array, r->gitdir);
+	/*
+	 * submodule_create_branch() is indirectly invoked by "git
+	 * branch", but we cannot invoke "git branch" in the child
+	 * process because it does not let us set start_name and
+	 * start_oid separately (see create_branches_recursively()).
+	 */
+	strvec_pushl(&child.args, "submodule--helper", "create-branch", NULL);
+	if (dry_run)
+		strvec_push(&child.args, "--dry-run");
+	if (force)
+		strvec_push(&child.args, "--force");
+	if (quiet)
+		strvec_push(&child.args, "--quiet");
+	if (reflog)
+		strvec_push(&child.args, "--create-reflog");
+	if (track == BRANCH_TRACK_ALWAYS || track == BRANCH_TRACK_EXPLICIT)
+		strvec_push(&child.args, "--track");
+
+	strvec_pushl(&child.args, name, start_oid, start_name, NULL);
+
+	if ((ret = start_command(&child)))
+		return ret;
+	ret = finish_command(&child);
+	strbuf_read(&child_err, child.err, 0);
+	strbuf_add_lines(&out_buf, out_prefix, child_err.buf, child_err.len);
+
+	if (ret)
+		fprintf(stderr, "%s", out_buf.buf);
+	else
+		printf("%s", out_buf.buf);
+
+	strbuf_release(&child_err);
+	strbuf_release(&out_buf);
+	return ret;
+}
+
+void create_branches_recursively(struct repository *r, const char *name,
+				 const char *start_name,
+				 const char *tracking_name, int force,
+				 int reflog, int quiet, enum branch_track track,
+				 int dry_run)
+{
+	int i = 0;
+	char *branch_point = NULL;
+	struct object_id super_oid;
+	struct submodule_entry_list submodule_entry_list;
+
+	/* Perform dwim on start_name to get super_oid and branch_point. */
+	dwim_branch_start(r, start_name, BRANCH_TRACK_NEVER, &branch_point,
+			  &super_oid);
+
+	/*
+	 * If we were not given an explicit name to track, then assume we are at
+	 * the top level and, just like the non-recursive case, the tracking
+	 * name is the branch point.
+	 */
+	if (!tracking_name)
+		tracking_name = branch_point;
+
+	submodules_of_tree(r, &super_oid, &submodule_entry_list);
+	/*
+	 * Before creating any branches, first check that the branch can
+	 * be created in every submodule.
+	 */
+	for (i = 0; i < submodule_entry_list.entry_nr; i++) {
+		if (submodule_entry_list.entries[i].repo == NULL) {
+			if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED))
+				advise(_("You may try updating the submodules using 'git checkout %s && git submodule update --init'"),
+				       start_name);
+			die(_("submodule '%s': unable to find submodule"),
+			    submodule_entry_list.entries[i].submodule->name);
+		}
+
+		if (submodule_create_branch(
+			    submodule_entry_list.entries[i].repo,
+			    submodule_entry_list.entries[i].submodule, name,
+			    oid_to_hex(&submodule_entry_list.entries[i]
+						.name_entry->oid),
+			    tracking_name, force, reflog, quiet, track, 1))
+			die(_("submodule '%s': cannot create branch '%s'"),
+			    submodule_entry_list.entries[i].submodule->name,
+			    name);
+	}
+
+	create_branch(the_repository, name, start_name, force, 0, reflog, quiet,
+		      BRANCH_TRACK_NEVER, dry_run);
+	if (dry_run)
+		return;
+	/*
+	 * NEEDSWORK If tracking was set up in the superproject but not the
+	 * submodule, users might expect "git branch --recurse-submodules" to
+	 * fail or give a warning, but this is not yet implemented because it is
+	 * tedious to determine whether or not tracking was set up in the
+	 * superproject.
+	 */
+	setup_tracking(name, tracking_name, track, quiet);
+
+	for (i = 0; i < submodule_entry_list.entry_nr; i++) {
+		if (submodule_create_branch(
+			    submodule_entry_list.entries[i].repo,
+			    submodule_entry_list.entries[i].submodule, name,
+			    oid_to_hex(&submodule_entry_list.entries[i]
+						.name_entry->oid),
+			    tracking_name, force, reflog, quiet, track, 0))
+			die(_("submodule '%s': cannot create branch '%s'"),
+			    submodule_entry_list.entries[i].submodule->name,
+			    name);
+		repo_clear(submodule_entry_list.entries[i].repo);
+	}
+}
+
 void remove_merge_branch_state(struct repository *r)
 {
 	unlink(git_path_merge_head(r));
diff --git a/branch.h b/branch.h
index 509cfcc34e..a59905a211 100644
--- a/branch.h
+++ b/branch.h
@@ -71,6 +71,28 @@  void create_branch(struct repository *r,
 		   int reflog, int quiet, enum branch_track track,
 		   int dry_run);
 
+/*
+ * Creates a new branch in repository and its submodules (and its
+ * submodules, recursively). Besides these exceptions, the parameters
+ * function identically to create_branch():
+ *
+ * - start_name is the name of the ref, in repository r, that the new
+ *   branch should start from. In submodules, branches will start from
+ *   the respective gitlink commit ids in start_name's tree.
+ *
+ * - tracking_name is the name used of the ref that will be used to set
+ *   up tracking, e.g. origin/main. This is propagated to submodules so
+ *   that tracking information will appear as if the branch branched off
+ *   tracking_name instead of start_name (which is a plain commit id for
+ *   submodules). If omitted, start_name is used for tracking (just like
+ *   create_branch()).
+ *
+ */
+void create_branches_recursively(struct repository *r, const char *name,
+				 const char *start_name,
+				 const char *tracking_name, int force,
+				 int reflog, int quiet, enum branch_track track,
+				 int dry_run);
 /*
  * Check if 'name' can be a valid name for a branch; die otherwise.
  * Return 1 if the named branch already exists; return 0 otherwise.
diff --git a/builtin/branch.c b/builtin/branch.c
index c274fbdfcf..a720a6683d 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -27,7 +27,8 @@ 
 
 static const char * const builtin_branch_usage[] = {
 	N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
-	N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
+	N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"),
+	N_("git branch [<options>] [-l] [<pattern>...]"),
 	N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
 	N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
 	N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
@@ -38,6 +39,8 @@  static const char * const builtin_branch_usage[] = {
 
 static const char *head;
 static struct object_id head_oid;
+static int recurse_submodules = 0;
+static int submodule_propagate_branches = 0;
 
 static int branch_use_color = -1;
 static char branch_colors[][COLOR_MAXLEN] = {
@@ -100,6 +103,15 @@  static int git_branch_config(const char *var, const char *value, void *cb)
 			return config_error_nonbool(var);
 		return color_parse(value, branch_colors[slot]);
 	}
+	if (!strcmp(var, "submodule.recurse")) {
+		recurse_submodules = git_config_bool(var, value);
+		return 0;
+	}
+	if (!strcasecmp(var, "submodule.propagateBranches")) {
+		submodule_propagate_branches = git_config_bool(var, value);
+		return 0;
+	}
+
 	return git_color_default_config(var, value, cb);
 }
 
@@ -622,7 +634,8 @@  int cmd_branch(int argc, const char **argv, const char *prefix)
 	const char *new_upstream = NULL;
 	int noncreate_actions = 0;
 	/* possible options */
-	int reflog = 0, quiet = 0, icase = 0, force = 0;
+	int reflog = 0, quiet = 0, icase = 0, force = 0,
+	    recurse_submodules_explicit = 0;
 	enum branch_track track;
 	struct ref_filter filter;
 	static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
@@ -672,6 +685,7 @@  int cmd_branch(int argc, const char **argv, const char *prefix)
 		OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
 			N_("print only branches of the object"), parse_opt_object_name),
 		OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
+		OPT_BOOL(0, "recurse-submodules", &recurse_submodules_explicit, N_("recurse through submodules")),
 		OPT_STRING(  0 , "format", &format.format, N_("format"), N_("format to use for the output")),
 		OPT_END(),
 	};
@@ -714,6 +728,17 @@  int cmd_branch(int argc, const char **argv, const char *prefix)
 	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
+	if (recurse_submodules_explicit) {
+		if (!submodule_propagate_branches)
+			die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
+		if (noncreate_actions)
+			die(_("--recurse-submodules can only be used to create branches"));
+	}
+
+	recurse_submodules =
+		(recurse_submodules || recurse_submodules_explicit) &&
+		submodule_propagate_branches;
+
 	if (filter.abbrev == -1)
 		filter.abbrev = DEFAULT_ABBREV;
 	filter.ignore_case = icase;
@@ -852,6 +877,9 @@  int cmd_branch(int argc, const char **argv, const char *prefix)
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
 	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
+		const char *branch_name = argv[0];
+		const char *start_name = argc == 2 ? argv[1] : head;
+
 		if (filter.kind != FILTER_REFS_BRANCHES)
 			die(_("The -a, and -r, options to 'git branch' do not take a branch name.\n"
 				  "Did you mean to use: -a|-r --list <pattern>?"));
@@ -859,10 +887,14 @@  int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (track == BRANCH_TRACK_OVERRIDE)
 			die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead."));
 
-		create_branch(the_repository,
-			      argv[0], (argc == 2) ? argv[1] : head,
-			      force, 0, reflog, quiet, track, 0);
-
+		if (recurse_submodules) {
+			create_branches_recursively(the_repository, branch_name,
+						    start_name, NULL, force,
+						    reflog, quiet, track, 0);
+			return 0;
+		}
+		create_branch(the_repository, branch_name, start_name, force, 0,
+			      reflog, quiet, track, 0);
 	} else
 		usage_with_options(builtin_branch_usage, options);
 
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6298cbdd4e..eba2a6e9b0 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -20,6 +20,7 @@ 
 #include "diff.h"
 #include "object-store.h"
 #include "advice.h"
+#include "branch.h"
 
 #define OPT_QUIET (1 << 0)
 #define OPT_CACHED (1 << 1)
@@ -2983,6 +2984,42 @@  static int module_set_branch(int argc, const char **argv, const char *prefix)
 	return !!ret;
 }
 
+static int module_create_branch(int argc, const char **argv, const char *prefix)
+{
+	enum branch_track track;
+	int quiet = 0, force = 0, reflog = 0, dry_run = 0;
+
+	struct option options[] = {
+		OPT__QUIET(&quiet, N_("print only error messages")),
+		OPT__FORCE(&force, N_("force creation"), 0),
+		OPT_BOOL(0, "create-reflog", &reflog,
+			 N_("create the branch's reflog")),
+		OPT_SET_INT('t', "track", &track,
+			    N_("set up tracking mode (see git-pull(1))"),
+			    BRANCH_TRACK_EXPLICIT),
+		OPT__DRY_RUN(&dry_run,
+			     N_("show whether the branch would be created")),
+		OPT_END()
+	};
+	const char *const usage[] = {
+		N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] [-n|--dry-run] <name> <start_oid> <start_name>"),
+		NULL
+	};
+
+	git_config(git_default_config, NULL);
+	track = git_branch_track;
+	argc = parse_options(argc, argv, prefix, options, usage, 0);
+
+	if (argc != 3)
+		usage_with_options(usage, options);
+
+	if (!quiet && !dry_run)
+		printf_ln(_("creating branch '%s'"), argv[0]);
+
+	create_branches_recursively(the_repository, argv[0], argv[1], argv[2],
+				    force, reflog, quiet, track, dry_run);
+	return 0;
+}
 struct add_data {
 	const char *prefix;
 	const char *branch;
@@ -3379,6 +3416,7 @@  static struct cmd_struct commands[] = {
 	{"config", module_config, 0},
 	{"set-url", module_set_url, 0},
 	{"set-branch", module_set_branch, 0},
+	{"create-branch", module_create_branch, 0},
 };
 
 int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
diff --git a/submodule-config.c b/submodule-config.c
index f95344028b..24b8d1a700 100644
--- a/submodule-config.c
+++ b/submodule-config.c
@@ -7,6 +7,7 @@ 
 #include "strbuf.h"
 #include "object-store.h"
 #include "parse-options.h"
+#include "tree-walk.h"
 
 /*
  * submodule cache lookup structure
@@ -726,6 +727,65 @@  const struct submodule *submodule_from_path(struct repository *r,
 	return config_from(r->submodule_cache, treeish_name, path, lookup_path);
 }
 
+/**
+ * Used internally by submodules_of_tree(). Recurses into 'treeish_name'
+ * and appends submodule entries to 'out'. The submodule_cache expects
+ * a root-level treeish_name and paths, so keep track of these values
+ * with 'root_tree' and 'prefix'.
+ */
+static void traverse_tree_submodules(struct repository *r,
+				     const struct object_id *root_tree,
+				     char *prefix,
+				     const struct object_id *treeish_name,
+				     struct submodule_entry_list *out)
+{
+	struct tree_desc tree;
+	struct submodule_tree_entry *st_entry;
+	struct name_entry *name_entry;
+	char *tree_path = NULL;
+
+	name_entry = xmalloc(sizeof(*name_entry));
+
+	fill_tree_descriptor(r, &tree, treeish_name);
+	while (tree_entry(&tree, name_entry)) {
+		if (prefix)
+			tree_path =
+				mkpathdup("%s/%s", prefix, name_entry->path);
+		else
+			tree_path = xstrdup(name_entry->path);
+
+		if (S_ISGITLINK(name_entry->mode) &&
+		    is_tree_submodule_active(r, root_tree, tree_path)) {
+			st_entry = xmalloc(sizeof(*st_entry));
+			st_entry->name_entry = name_entry;
+			st_entry->submodule =
+				submodule_from_path(r, root_tree, tree_path);
+			st_entry->repo = xmalloc(sizeof(*st_entry->repo));
+			if (repo_submodule_init(st_entry->repo, r, tree_path,
+						root_tree))
+				FREE_AND_NULL(st_entry->repo);
+
+			ALLOC_GROW(out->entries, out->entry_nr + 1,
+				   out->entry_alloc);
+			out->entries[out->entry_nr++] = *st_entry;
+		} else if (S_ISDIR(name_entry->mode))
+			traverse_tree_submodules(r, root_tree, tree_path,
+						 &name_entry->oid, out);
+		free(tree_path);
+	}
+}
+
+void submodules_of_tree(struct repository *r,
+			const struct object_id *treeish_name,
+			struct submodule_entry_list *out)
+{
+	CALLOC_ARRAY(out->entries, 0);
+	out->entry_nr = 0;
+	out->entry_alloc = 0;
+
+	traverse_tree_submodules(r, treeish_name, NULL, treeish_name, out);
+}
+
 void submodule_free(struct repository *r)
 {
 	if (r->submodule_cache)
diff --git a/submodule-config.h b/submodule-config.h
index 65875b94ea..fa229a8b97 100644
--- a/submodule-config.h
+++ b/submodule-config.h
@@ -6,6 +6,7 @@ 
 #include "hashmap.h"
 #include "submodule.h"
 #include "strbuf.h"
+#include "tree-walk.h"
 
 /**
  * The submodule config cache API allows to read submodule
@@ -101,4 +102,37 @@  int check_submodule_name(const char *name);
 void fetch_config_from_gitmodules(int *max_children, int *recurse_submodules);
 void update_clone_config_from_gitmodules(int *max_jobs);
 
+/*
+ * Submodule entry that contains relevant information about a
+ * submodule in a tree.
+ */
+struct submodule_tree_entry {
+	/* The submodule's tree entry. */
+	struct name_entry *name_entry;
+	/*
+	 * A struct repository corresponding to the submodule. May be
+	 * NULL if the submodule has not been updated.
+	 */
+	struct repository *repo;
+	/*
+	 * A struct submodule containing the submodule config in the
+	 * tree's .gitmodules.
+	 */
+	const struct submodule *submodule;
+};
+
+struct submodule_entry_list {
+	struct submodule_tree_entry *entries;
+	int entry_nr;
+	int entry_alloc;
+};
+
+/**
+ * Given a treeish, return all submodules in the tree and its subtrees,
+ * but excluding nested submodules. Callers that require nested
+ * submodules are expected to recurse into the submodules themselves.
+ */
+void submodules_of_tree(struct repository *r,
+			const struct object_id *treeish_name,
+			struct submodule_entry_list *ret);
 #endif /* SUBMODULE_CONFIG_H */
diff --git a/submodule.c b/submodule.c
index f3c99634a9..07ae7c266e 100644
--- a/submodule.c
+++ b/submodule.c
@@ -265,7 +265,9 @@  int option_parse_recurse_submodules_worktree_updater(const struct option *opt,
  * ie, the config looks like: "[submodule] active\n".
  * Since that is an invalid pathspec, we should inform the user.
  */
-int is_submodule_active(struct repository *repo, const char *path)
+int is_tree_submodule_active(struct repository *repo,
+			     const struct object_id *treeish_name,
+			     const char *path)
 {
 	int ret = 0;
 	char *key = NULL;
@@ -273,7 +275,7 @@  int is_submodule_active(struct repository *repo, const char *path)
 	const struct string_list *sl;
 	const struct submodule *module;
 
-	module = submodule_from_path(repo, null_oid(), path);
+	module = submodule_from_path(repo, treeish_name, path);
 
 	/* early return if there isn't a path->module mapping */
 	if (!module)
@@ -315,6 +317,11 @@  int is_submodule_active(struct repository *repo, const char *path)
 	return ret;
 }
 
+int is_submodule_active(struct repository *repo, const char *path)
+{
+	return is_tree_submodule_active(repo, null_oid(), path);
+}
+
 int is_submodule_populated_gently(const char *path, int *return_error_code)
 {
 	int ret = 0;
diff --git a/submodule.h b/submodule.h
index 6bd2c99fd9..784ceffc0e 100644
--- a/submodule.h
+++ b/submodule.h
@@ -54,6 +54,9 @@  int git_default_submodule_config(const char *var, const char *value, void *cb);
 struct option;
 int option_parse_recurse_submodules_worktree_updater(const struct option *opt,
 						     const char *arg, int unset);
+int is_tree_submodule_active(struct repository *repo,
+			     const struct object_id *treeish_name,
+			     const char *path);
 int is_submodule_active(struct repository *repo, const char *path);
 /*
  * Determine if a submodule has been populated at a given 'path' by checking if
diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
new file mode 100755
index 0000000000..047b0cd174
--- /dev/null
+++ b/t/t3207-branch-submodule.sh
@@ -0,0 +1,282 @@ 
+#!/bin/sh
+
+test_description='git branch submodule tests'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
+
+pwd=$(pwd)
+
+# Creates a clean test environment in "pwd" by copying the repo setup
+# from test_dirs.
+reset_test () {
+	rm -fr super &&
+	rm -fr sub-sub-upstream &&
+	rm -fr sub-upstream &&
+	cp -r test_dirs/* .
+}
+
+test_expect_success 'setup superproject and submodule' '
+	mkdir test_dirs &&
+	(
+		cd test_dirs &&
+		git init super &&
+		test_commit -C super foo &&
+		git init sub-sub-upstream &&
+		test_commit -C sub-sub-upstream foo &&
+		git init sub-upstream &&
+		# Submodule in a submodule
+		git -C sub-upstream submodule add "${pwd}/test_dirs/sub-sub-upstream" sub-sub &&
+		git -C sub-upstream commit -m "add submodule" &&
+		# Regular submodule
+		git -C super submodule add "${pwd}/test_dirs/sub-upstream" sub &&
+		# Submodule in a subdirectory
+		git -C super submodule add "${pwd}/test_dirs/sub-sub-upstream" second/sub &&
+		git -C super commit -m "add submodule" &&
+		git -C super config submodule.propagateBranches true &&
+		git -C super/sub submodule update --init
+	) &&
+	reset_test
+'
+
+# Test the argument parsing
+test_expect_success '--recurse-submodules should create branches' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git rev-parse branch-a &&
+		git -C sub rev-parse branch-a &&
+		git -C sub/sub-sub rev-parse branch-a &&
+		git -C second/sub rev-parse branch-a
+	)
+'
+
+test_expect_success '--recurse-submodules should die if submodule.propagateBranches is false' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		echo "fatal: branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled" >expected &&
+		test_must_fail git -c submodule.propagateBranches=false branch --recurse-submodules branch-a 2>actual &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success '--recurse-submodules should fail when not creating branches' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		test_must_fail git branch --recurse-submodules -D branch-a &&
+		# Assert that the branches were not deleted
+		git rev-parse branch-a &&
+		git -C sub rev-parse branch-a
+	)
+'
+
+test_expect_success 'should respect submodule.recurse when creating branches' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git -c submodule.recurse=true branch branch-a &&
+		git rev-parse branch-a &&
+		git -C sub rev-parse branch-a
+	)
+'
+
+test_expect_success 'should ignore submodule.recurse when not creating branches' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git -c submodule.recurse=true branch -D branch-a &&
+		test_must_fail git rev-parse branch-a &&
+		git -C sub rev-parse branch-a
+	)
+'
+
+# Test branch creation behavior
+test_expect_success 'should create branches based off commit id in superproject' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git checkout --recurse-submodules branch-a &&
+		git -C sub rev-parse HEAD >expected &&
+		# Move the tip of sub:branch-a so that it no longer matches the commit in super:branch-a
+		git -C sub checkout branch-a &&
+		test_commit -C sub bar &&
+		# Create a new branch-b branch with start-point=branch-a
+		git branch --recurse-submodules branch-b branch-a &&
+		git rev-parse branch-b &&
+		git -C sub rev-parse branch-b >actual &&
+		# Assert that the commit id of sub:second-branch matches super:branch-a and not sub:branch-a
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should not create any branches if branch is not valid for all repos' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git -C sub branch branch-a &&
+		test_must_fail git branch --recurse-submodules branch-a 2>actual &&
+		test_must_fail git rev-parse branch-a &&
+		grep "submodule .sub.: fatal: A branch named .branch-a. already exists" actual
+	)
+'
+
+test_expect_success 'should create branches if branch exists and --force is given' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git -C sub rev-parse HEAD >expected &&
+		test_commit -C sub baz &&
+		# branch-a in sub now points to a newer commit.
+		git -C sub branch branch-a HEAD &&
+		git -C sub rev-parse branch-a >actual-old-branch-a &&
+		git branch --recurse-submodules --force branch-a &&
+		git rev-parse branch-a &&
+		git -C sub rev-parse branch-a >actual-new-branch-a &&
+		test_cmp expected actual-new-branch-a &&
+		# assert that branch --force actually moved the sub
+		# branch
+		! test_cmp expected actual-old-branch-a
+	)
+'
+
+test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git branch branch-a &&
+		git checkout -b branch-b &&
+		git submodule add ../sub-upstream sub2 &&
+		git -C sub2 submodule update --init &&
+		# branch-b now has a committed submodule not in branch-a
+		git commit -m "add second submodule" &&
+		git checkout branch-a &&
+		git branch --recurse-submodules branch-c branch-b &&
+		git checkout --recurse-submodules branch-c &&
+		git -C sub2 rev-parse branch-c &&
+		git -C sub2/sub-sub rev-parse branch-c
+	)
+'
+
+test_expect_success 'should not create branches in inactive submodules' '
+	test_when_finished "reset_test" &&
+	test_config -C super submodule.sub.active false &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git rev-parse branch-a &&
+		test_must_fail git -C sub branch-a
+	)
+'
+
+test_expect_success 'should set up tracking of local branches with track=always' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git -c branch.autoSetupMerge=always branch --recurse-submodules branch-a main &&
+		git -C sub rev-parse main &&
+		test "$(git -C sub config branch.branch-a.remote)" = . &&
+		test "$(git -C sub config branch.branch-a.merge)" = refs/heads/main
+	)
+'
+
+test_expect_success 'should set up tracking of local branches with explicit track' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git branch --track --recurse-submodules branch-a main &&
+		git -C sub rev-parse main &&
+		test "$(git -C sub config branch.branch-a.remote)" = . &&
+		test "$(git -C sub config branch.branch-a.merge)" = refs/heads/main
+	)
+'
+
+test_expect_success 'should not set up unnecessary tracking of local branches' '
+	test_when_finished "reset_test" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a main &&
+		git -C sub rev-parse main &&
+		test "$(git -C sub config branch.branch-a.remote)" = "" &&
+		test "$(git -C sub config branch.branch-a.merge)" = ""
+	)
+'
+
+reset_remote_test () {
+	rm -fr super-clone &&
+	reset_test
+}
+
+test_expect_success 'setup tests with remotes' '
+	(
+		cd test_dirs &&
+		(
+			cd super &&
+			git branch branch-a &&
+			git checkout -b branch-b &&
+			git submodule add ../sub-upstream sub2 &&
+			# branch-b now has a committed submodule not in branch-a
+			git commit -m "add second submodule"
+		) &&
+		git clone --branch main --recurse-submodules super super-clone &&
+		git -C super-clone config submodule.propagateBranches true
+	) &&
+	reset_remote_test
+'
+
+test_expect_success 'should get fatal error upon branch creation when submodule is not in .git/modules' '
+	test_when_finished "reset_remote_test" &&
+	(
+		cd super-clone &&
+		# This should succeed because super-clone has sub in .git/modules
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		# This should fail because super-clone does not have sub2 .git/modules
+		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
+		grep "fatal: submodule .sub2.: unable to find submodule" actual &&
+		test_must_fail git rev-parse branch-b &&
+		test_must_fail git -C sub rev-parse branch-b &&
+		# User can fix themselves by initializing the submodule
+		git checkout origin/branch-b &&
+		git submodule update --init --recursive &&
+		git branch --recurse-submodules branch-b origin/branch-b
+	)
+'
+
+test_expect_success 'should set up tracking of remote-tracking branches' '
+	test_when_finished "reset_remote_test" &&
+	(
+		cd super-clone &&
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		test "$(git config branch.branch-a.remote)" = origin &&
+		test "$(git config branch.branch-a.merge)" = refs/heads/branch-a &&
+		# "origin/branch-a" does not exist for "sub", but it matches the refspec
+		# so tracking should be set up
+		test "$(git -C sub config branch.branch-a.remote)" = origin &&
+		test "$(git -C sub config branch.branch-a.merge)" = refs/heads/branch-a &&
+		test "$(git -C sub/sub-sub config branch.branch-a.remote)" = origin &&
+		test "$(git -C sub/sub-sub config branch.branch-a.merge)" = refs/heads/branch-a
+	)
+'
+
+test_expect_success 'should not fail when unable to set up tracking in submodule' '
+	test_when_finished "reset_remote_test" &&
+	(
+		cd super-clone &&
+		git remote rename origin ex-origin &&
+		git branch --recurse-submodules branch-a ex-origin/branch-a &&
+		test "$(git config branch.branch-a.remote)" = ex-origin &&
+		test "$(git config branch.branch-a.merge)" = refs/heads/branch-a &&
+		test "$(git -C sub config branch.branch-a.remote)" = "" &&
+		test "$(git -C sub config branch.branch-a.merge)" = ""
+	)
+'
+
+test_done