git@vger.kernel.org list mirror (unofficial, one of many)
 help / color / mirror / code / Atom feed
* [PATCH 0/4] implement branch --recurse-submodules
@ 2021-11-22 22:32 Glen Choo
  2021-11-22 22:32 ` [PATCH 1/4] submodule-config: add submodules_of_tree() helper Glen Choo
                   ` (4 more replies)
  0 siblings, 5 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-22 22:32 UTC (permalink / raw)
  To: git; +Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer, Glen Choo

Submodule branching RFC:
https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/

Original Submodule UX RFC/Discussion:
https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/

Contributor Summit submodules Notes:
https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/

Submodule UX overhaul updates:
https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update

This series implements branch --recurse-submodules as laid out in the
Submodule branching RFC (linked above). If there are concerns about the
UX/behavior, I would appreciate feedback on the RFC thread as well :)

This series uses child processes to support submodules. I initially
hoped to do this in-core and [1] and [2] were meant to prepare for that.
But even though in-core is tantalizingly close, [1] showed that there
is more work to be done on config.c before this is possible, and I would
like to get more feedback on the UX before converting this to in-core.

[1] https://lore.kernel.org/git/20211111171643.13805-1-chooglen@google.com/
[2] https://lore.kernel.org/git/20211118005325.64971-1-chooglen@google.com/

Glen Choo (4):
  submodule-config: add submodules_of_tree() helper
  branch: refactor out branch validation from create_branch()
  branch: add --dry-run option to branch
  branch: add --recurse-submodules option for branch creation

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |   9 +
 Documentation/git-branch.txt       |   8 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 300 +++++++++++++++++++++--------
 branch.h                           |  41 +++-
 builtin/branch.c                   |  77 ++++++--
 builtin/submodule--helper.c        |  33 ++++
 submodule-config.c                 |  19 ++
 submodule-config.h                 |  13 ++
 t/t3200-branch.sh                  |  30 +++
 t/t3207-branch-submodule.sh        | 249 ++++++++++++++++++++++++
 13 files changed, 678 insertions(+), 106 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH 1/4] submodule-config: add submodules_of_tree() helper
  2021-11-22 22:32 [PATCH 0/4] implement branch --recurse-submodules Glen Choo
@ 2021-11-22 22:32 ` Glen Choo
  2021-11-23  2:12   ` Jonathan Tan
                     ` (2 more replies)
  2021-11-22 22:32 ` [PATCH 2/4] branch: refactor out branch validation from create_branch() Glen Choo
                   ` (3 subsequent siblings)
  4 siblings, 3 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-22 22:32 UTC (permalink / raw)
  To: git; +Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer, Glen Choo

As we introduce a submodule UX with branches, we would like to be able
to get the submodule commit ids in a superproject tree because those ids
are the source of truth e.g. "git branch --recurse-submodules topic
start-point" should create branches based off the commit ids recorded in
the superproject's 'start-point' tree.

To make this easy, introduce a submodules_of_tree() helper function that
iterates through a tree and returns the tree's gitlink entries as a
list.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 submodule-config.c | 19 +++++++++++++++++++
 submodule-config.h | 13 +++++++++++++
 2 files changed, 32 insertions(+)

diff --git a/submodule-config.c b/submodule-config.c
index f95344028b..97da373301 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,24 @@ const struct submodule *submodule_from_path(struct repository *r,
 	return config_from(r->submodule_cache, treeish_name, path, lookup_path);
 }
 
+struct submodule_entry_list *
+submodules_of_tree(struct repository *r, const struct object_id *treeish_name)
+{
+	struct tree_desc tree;
+	struct name_entry entry;
+	struct submodule_entry_list *ret;
+
+	CALLOC_ARRAY(ret, 1);
+	fill_tree_descriptor(r, &tree, treeish_name);
+	while (tree_entry(&tree, &entry)) {
+		if (!S_ISGITLINK(entry.mode))
+			continue;
+		ALLOC_GROW(ret->name_entries, ret->entry_nr + 1, ret->entry_alloc);
+		ret->name_entries[ret->entry_nr++] = entry;
+	}
+	return ret;
+}
+
 void submodule_free(struct repository *r)
 {
 	if (r->submodule_cache)
diff --git a/submodule-config.h b/submodule-config.h
index 65875b94ea..4379ec77e3 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
@@ -67,6 +68,18 @@ const struct submodule *submodule_from_name(struct repository *r,
 					    const struct object_id *commit_or_tree,
 					    const char *name);
 
+struct submodule_entry_list {
+	struct name_entry *name_entries;
+	int entry_nr;
+	int entry_alloc;
+};
+
+/**
+ * Given a tree-ish, return all submodules in the tree.
+ */
+struct submodule_entry_list *
+submodules_of_tree(struct repository *r, const struct object_id *treeish_name);
+
 /**
  * Given a tree-ish in the superproject and a path, return the submodule that
  * is bound at the path in the named tree.
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH 2/4] branch: refactor out branch validation from create_branch()
  2021-11-22 22:32 [PATCH 0/4] implement branch --recurse-submodules Glen Choo
  2021-11-22 22:32 ` [PATCH 1/4] submodule-config: add submodules_of_tree() helper Glen Choo
@ 2021-11-22 22:32 ` Glen Choo
  2021-11-22 22:32 ` [PATCH 3/4] branch: add --dry-run option to branch Glen Choo
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-22 22:32 UTC (permalink / raw)
  To: git; +Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer, Glen Choo

In a subsequent commit, we would like to be able to validate whether or
not a branch name is valid before we create it (--dry-run). This is
useful for `git branch --recurse-submodules topic` because it allows Git
to determine if the branch 'topic' can be created in all submodules
without creating the branch 'topic'.

A good starting point would be to refactor out the start point
validation and dwim logic in create_branch() in a
validate_branch_start() helper function. Once we do so, it becomes
clear that create_branch() is more complex than it needs to be -
create_branch() is also used to set tracking information when performing
`git branch --set-upstream-to`. This made more sense when
(the now unsupported) --set-upstream was first introduced in
4fc5006676 (Add branch --set-upstream, 2010-01-18), because
it would sometimes create a branch and sometimes update tracking
information without creating a branch.

Refactor out the branch validation and dwim logic from create_branch()
into validate_branch_start(), make it so that create_branch() always
tries to create a branch, and replace the now-incorrect create_branch()
call with setup_tracking(). Since there were none, add tests for
creating a branch with `--force`.

Signed-off-by: Glen Choo <chooglen@google.com>
---
In this refactor, I preserved the existing behavior by making
setup_tracking() call validate_branch_start(). setup_tracking() needs
the dwim behavior e.g. to expand 'origin/main' into
'refs/remotes/origin/main' but I'm doubtful that it needs the exact same
set of validation behavior as creating a new branch e.g. validating that
the object_id is a commit.

 branch.c          | 177 ++++++++++++++++++++++++----------------------
 branch.h          |  13 +++-
 builtin/branch.c  |   7 +-
 t/t3200-branch.sh |  17 +++++
 4 files changed, 121 insertions(+), 93 deletions(-)

diff --git a/branch.c b/branch.c
index 07a46430b3..f8b755513f 100644
--- a/branch.c
+++ b/branch.c
@@ -126,43 +126,6 @@ int install_branch_config(int flag, const char *local, const char *origin, const
 	return -1;
 }
 
-/*
- * 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 void setup_tracking(const char *new_ref, const char *orig_ref,
-			   enum branch_track track, int quiet)
-{
-	struct tracking tracking;
-	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
-
-	memset(&tracking, 0, sizeof(tracking));
-	tracking.spec.dst = (char *)orig_ref;
-	if (for_each_remote(find_tracked_branch, &tracking))
-		return;
-
-	if (!tracking.matches)
-		switch (track) {
-		case BRANCH_TRACK_ALWAYS:
-		case BRANCH_TRACK_EXPLICIT:
-		case BRANCH_TRACK_OVERRIDE:
-			break;
-		default:
-			return;
-		}
-
-	if (tracking.matches > 1)
-		die(_("Not tracking: ambiguous information for ref %s"),
-		    orig_ref);
-
-	if (install_branch_config(config_flags, new_ref, tracking.remote,
-			      tracking.src ? tracking.src : orig_ref) < 0)
-		exit(-1);
-
-	free(tracking.src);
-}
-
 int read_branch_desc(struct strbuf *buf, const char *branch_name)
 {
 	char *v = NULL;
@@ -243,33 +206,17 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+static void validate_branch_start(struct repository *r, const char *start_name,
+				  enum branch_track track,
+				  struct object_id *oid, char **full_ref)
 {
 	struct commit *commit;
-	struct object_id oid;
-	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
 	int explicit_tracking = 0;
 
 	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
 		explicit_tracking = 1;
 
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
-
-	real_ref = NULL;
-	if (get_oid_mb(start_name, &oid)) {
+	if (repo_get_oid_mb(r, start_name, oid)) {
 		if (explicit_tracking) {
 			if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) {
 				error(_(upstream_missing), start_name);
@@ -281,7 +228,8 @@ void create_branch(struct repository *r,
 		die(_("Not a valid object name: '%s'."), start_name);
 	}
 
-	switch (dwim_ref(start_name, strlen(start_name), &oid, &real_ref, 0)) {
+	switch (repo_dwim_ref(r, start_name, strlen(start_name), oid, full_ref,
+			      0)) {
 	case 0:
 		/* Not branching from any existing branch */
 		if (explicit_tracking)
@@ -289,12 +237,12 @@ void create_branch(struct repository *r,
 		break;
 	case 1:
 		/* Unique completion -- good, only if it is a real branch */
-		if (!starts_with(real_ref, "refs/heads/") &&
-		    validate_remote_tracking_branch(real_ref)) {
+		if (!starts_with(*full_ref, "refs/heads/") &&
+		    validate_remote_tracking_branch(*full_ref)) {
 			if (explicit_tracking)
 				die(_(upstream_not_branch), start_name);
 			else
-				FREE_AND_NULL(real_ref);
+				FREE_AND_NULL(*full_ref);
 		}
 		break;
 	default:
@@ -302,37 +250,96 @@ void create_branch(struct repository *r,
 		break;
 	}
 
-	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
+	if ((commit = lookup_commit_reference(r, oid)) == NULL)
 		die(_("Not a valid branch point: '%s'."), start_name);
-	oidcpy(&oid, &commit->object.oid);
+	oidcpy(oid, &commit->object.oid);
+}
+
+void setup_tracking(const char *new_ref, const char *orig_ref,
+			   enum branch_track track, int quiet, int expand_orig)
+{
+	struct tracking tracking;
+	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
+	char *full_orig_ref;
+	struct object_id unused_oid;
+
+	memset(&tracking, 0, sizeof(tracking));
+	if (expand_orig)
+		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
+	else
+		full_orig_ref = xstrdup(orig_ref);
+
+	tracking.spec.dst = full_orig_ref;
+	if (for_each_remote(find_tracked_branch, &tracking))
+		goto cleanup;
+
+	if (!tracking.matches)
+		switch (track) {
+		case BRANCH_TRACK_ALWAYS:
+		case BRANCH_TRACK_EXPLICIT:
+		case BRANCH_TRACK_OVERRIDE:
+			break;
+		default:
+			goto cleanup;
+		}
+
+	if (tracking.matches > 1)
+		die(_("Not tracking: ambiguous information for ref %s"),
+		    full_orig_ref);
+
+	if (install_branch_config(config_flags, new_ref, tracking.remote,
+			      tracking.src ? tracking.src : full_orig_ref) < 0)
+		exit(-1);
+
+cleanup:
+	free(tracking.src);
+	free(full_orig_ref);
+}
+
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track)
+{
+	struct object_id oid;
+	char *real_ref;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	struct ref_transaction *transaction;
+	struct strbuf err = STRBUF_INIT;
+	char *msg;
+
+	if (clobber_head_ok && !force)
+		BUG("'clobber_head_ok' can only be used with 'force'");
+
+	if (clobber_head_ok ?
+			  validate_branchname(name, &ref) :
+			  validate_new_branchname(name, &ref, force)) {
+		forcing = 1;
+	}
+
+	validate_branch_start(r, start_name, track, &oid, &real_ref);
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
-	if (!dont_change_ref) {
-		struct ref_transaction *transaction;
-		struct strbuf err = STRBUF_INIT;
-		char *msg;
-
-		if (forcing)
-			msg = xstrfmt("branch: Reset to %s", start_name);
-		else
-			msg = xstrfmt("branch: Created from %s", start_name);
-
-		transaction = ref_transaction_begin(&err);
-		if (!transaction ||
-		    ref_transaction_update(transaction, ref.buf,
-					   &oid, forcing ? NULL : null_oid(),
-					   0, msg, &err) ||
-		    ref_transaction_commit(transaction, &err))
-			die("%s", err.buf);
-		ref_transaction_free(transaction);
-		strbuf_release(&err);
-		free(msg);
-	}
+	if (forcing)
+		msg = xstrfmt("branch: Reset to %s", start_name);
+	else
+		msg = xstrfmt("branch: Created from %s", start_name);
+
+	transaction = ref_transaction_begin(&err);
+	if (!transaction ||
+		ref_transaction_update(transaction, ref.buf,
+					&oid, forcing ? NULL : null_oid(),
+					0, msg, &err) ||
+		ref_transaction_commit(transaction, &err))
+		die("%s", err.buf);
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	free(msg);
 
 	if (real_ref && track)
-		setup_tracking(ref.buf + 11, real_ref, track, quiet);
+		setup_tracking(ref.buf + 11, real_ref, track, quiet, 0);
 
 	strbuf_release(&ref);
 	free(real_ref);
diff --git a/branch.h b/branch.h
index df0be61506..75cefcdcbd 100644
--- a/branch.h
+++ b/branch.h
@@ -17,6 +17,15 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/*
+ * This sets the branch.<new_ref>.{remote,merge} config settings so that
+ * branch 'new_ref' tracks 'orig_ref'. This is called when branches are
+ * created, or when branch configs are updated (e.g. with
+ * git branch --set-upstream-to).
+ */
+void setup_tracking(const char *new_ref, const char *orig_ref,
+		    enum branch_track track, int quiet, int expand_orig);
+
 /*
  * Creates a new branch, where:
  *
@@ -29,8 +38,8 @@ extern enum branch_track git_branch_track;
  *
  *   - force enables overwriting an existing (non-head) branch
  *
- *   - clobber_head_ok allows the currently checked out (hence existing)
- *     branch to be overwritten; without 'force', it has no effect.
+ *   - clobber_head_ok, when enabled with 'force', allows the currently
+ *     checked out (head) branch to be overwritten
  *
  *   - reflog creates a reflog for the branch
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index 0b7ed82654..eb5c117a6e 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -820,12 +820,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		setup_tracking(branch->name, new_upstream, BRANCH_TRACK_OVERRIDE, quiet, 1);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index e575ffb4ff..6bf95a1707 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
 	git branch abc && test_path_is_file .git/refs/heads/abc
 '
 
+test_expect_success 'git branch abc should fail when abc exists' '
+	test_must_fail git branch abc
+'
+
+test_expect_success 'git branch --force abc should fail when abc is checked out' '
+	test_when_finished git switch main &&
+	git switch abc &&
+	test_must_fail git branch --force abc HEAD~1
+'
+
+test_expect_success 'git branch --force abc should succeed when abc exists' '
+	git rev-parse HEAD~1 >expect &&
+	git branch --force abc HEAD~1 &&
+	git rev-parse abc >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH 3/4] branch: add --dry-run option to branch
  2021-11-22 22:32 [PATCH 0/4] implement branch --recurse-submodules Glen Choo
  2021-11-22 22:32 ` [PATCH 1/4] submodule-config: add submodules_of_tree() helper Glen Choo
  2021-11-22 22:32 ` [PATCH 2/4] branch: refactor out branch validation from create_branch() Glen Choo
@ 2021-11-22 22:32 ` Glen Choo
  2021-11-23 10:42   ` Ævar Arnfjörð Bjarmason
  2021-11-23 23:10   ` Jonathan Tan
  2021-11-22 22:32 ` [PATCH 4/4] branch: add --recurse-submodules option for branch creation Glen Choo
  2021-12-06 21:55 ` [PATCH v2 0/3] implement branch --recurse-submodules Glen Choo
  4 siblings, 2 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-22 22:32 UTC (permalink / raw)
  To: git; +Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer, Glen Choo

When running "git branch --recurse-submodules topic", it would be useful
to know whether or not 'topic' is a valid branch for all repositories.
Currently there is no way to test this without actually creating the
branch.

Add a --dry-run option to branch creation that can check whether or not
a branch name and start point would be valid for a repository without
creating a branch. Refactor cmd_branch() to make the chosen action more
obvious.

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).

Signed-off-by: Glen Choo <chooglen@google.com>
---
The --dry-run option is motivated mainly by --recurse-submodules. To my
knowledge, there isn't a strong existing demand, but this might be
mildly useful to some users.

 Documentation/git-branch.txt |  8 ++++++-
 branch.c                     |  6 ++---
 branch.h                     | 22 ++++++++++++++++++
 builtin/branch.c             | 44 ++++++++++++++++++++++++++----------
 t/t3200-branch.sh            | 13 +++++++++++
 5 files changed, 77 insertions(+), 16 deletions(-)

diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt
index 5449767121..8cdc33c097 100644
--- a/Documentation/git-branch.txt
+++ b/Documentation/git-branch.txt
@@ -16,7 +16,7 @@ SYNOPSIS
 	[--points-at <object>] [--format=<format>]
 	[(-r | --remotes) | (-a | --all)]
 	[--list] [<pattern>...]
-'git branch' [--track | --no-track] [-f] <branchname> [<start-point>]
+'git branch' [--track | --no-track] [-f] [--dry-run | -n] <branchname> [<start-point>]
 'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
 'git branch' --unset-upstream [<branchname>]
 'git branch' (-m | -M) [<oldbranch>] <newbranch>
@@ -205,6 +205,12 @@ This option is only applicable in non-verbose mode.
 --no-abbrev::
 	Display the full sha1s in the output listing rather than abbreviating them.
 
+-n::
+--dry-run::
+	Can only be used when creating a branch. If the branch creation
+	would fail, show the relevant error message. If the branch
+	creation would succeed, show nothing.
+
 -t::
 --track::
 	When creating a new branch, set up `branch.<name>.remote` and
diff --git a/branch.c b/branch.c
index f8b755513f..528cb2d639 100644
--- a/branch.c
+++ b/branch.c
@@ -206,9 +206,9 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-static void validate_branch_start(struct repository *r, const char *start_name,
-				  enum branch_track track,
-				  struct object_id *oid, char **full_ref)
+void validate_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, struct object_id *oid,
+			   char **full_ref)
 {
 	struct commit *commit;
 	int explicit_tracking = 0;
diff --git a/branch.h b/branch.h
index 75cefcdcbd..d8e5ff4e28 100644
--- a/branch.h
+++ b/branch.h
@@ -3,6 +3,7 @@
 
 struct repository;
 struct strbuf;
+struct object_id;
 
 enum branch_track {
 	BRANCH_TRACK_UNSPECIFIED = -1,
@@ -17,6 +18,27 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/*
+ * Validates whether a ref is a valid starting point for a branch, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - oid is an out parameter containing the object_id of start_name
+ *
+ *   - full_ref is an out parameter containing the 'full' form of
+ *     start_name e.g. refs/heads/main instead of main
+ *
+ */
+void validate_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, struct object_id *oid,
+			   char **full_ref);
+
 /*
  * This sets the branch.<new_ref>.{remote,merge} config settings so that
  * branch 'new_ref' tracks 'orig_ref'. This is called when branches are
diff --git a/builtin/branch.c b/builtin/branch.c
index eb5c117a6e..5d4b9c82b4 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>] [-l] [<pattern>...]"),
+	N_("git branch [<options>] [-f] [--dry-run | -n] <branch-name> [<start-point>]"),
 	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>"),
@@ -616,14 +617,14 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, force = 0, list = 0, create = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, dry_run = 0, icase = 0;
 	const char *new_upstream = NULL;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
 	struct ref_format format = REF_FORMAT_INIT;
 
@@ -670,6 +671,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 			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_STRING(  0 , "format", &format.format, N_("format"), N_("format to use for the output")),
+		OPT__DRY_RUN(&dry_run, N_("show whether the branch would be created")),
 		OPT_END(),
 	};
 
@@ -705,10 +707,15 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	create = 1 - (!!delete + !!rename + !!copy + !!new_upstream +
+		      !!show_current + !!list + !!edit_description +
+		      !!unset_upstream);
+	if (create < 0)
 		usage_with_options(builtin_branch_usage, options);
 
+	if (dry_run && !create)
+		die(_("--dry-run can only be used when creating branches"));
+
 	if (filter.abbrev == -1)
 		filter.abbrev = DEFAULT_ABBREV;
 	filter.ignore_case = icase;
@@ -844,7 +851,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (create && 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>?"));
@@ -852,10 +862,20 @@ 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);
+		if (dry_run) {
+			struct strbuf buf = STRBUF_INIT;
+			char *unused_full_ref;
+			struct object_id unused_oid;
 
+			validate_new_branchname(branch_name, &buf, force);
+			validate_branch_start(the_repository, start_name, track,
+					      &unused_oid, &unused_full_ref);
+			strbuf_release(&buf);
+			FREE_AND_NULL(unused_full_ref);
+			return 0;
+		}
+		create_branch(the_repository, branch_name, start_name, force, 0,
+			      reflog, quiet, track);
 	} else
 		usage_with_options(builtin_branch_usage, options);
 
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 6bf95a1707..653891736a 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -59,6 +59,19 @@ test_expect_success 'git branch --force abc should succeed when abc exists' '
 	test_cmp expect actual
 '
 
+test_expect_success 'git branch --dry-run abc should fail when abc exists' '
+	test_must_fail git branch --dry-run abc
+'
+
+test_expect_success 'git branch --dry-run --force abc should succeed when abc exists' '
+	git branch --dry-run --force abc
+'
+
+test_expect_success 'git branch --dry-run def should not create a branch' '
+	git branch --dry-run def &&
+	test_must_fail git rev-parse def
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-22 22:32 [PATCH 0/4] implement branch --recurse-submodules Glen Choo
                   ` (2 preceding siblings ...)
  2021-11-22 22:32 ` [PATCH 3/4] branch: add --dry-run option to branch Glen Choo
@ 2021-11-22 22:32 ` Glen Choo
  2021-11-23 10:45   ` Ævar Arnfjörð Bjarmason
                     ` (2 more replies)
  2021-12-06 21:55 ` [PATCH v2 0/3] implement branch --recurse-submodules Glen Choo
  4 siblings, 3 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-22 22:32 UTC (permalink / raw)
  To: git; +Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer, Glen Choo

Add a --recurse-submodules option when creating branches so that `git
branch --recurse-submodules topic` will create the "topic" branch in the
superproject and all submodules. Guard this (and future submodule
branching) behavior behind a new configuration value
'submodule.propagateBranches'.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |   9 ++
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 123 ++++++++++++++
 branch.h                           |   6 +
 builtin/branch.c                   |  28 +++-
 builtin/submodule--helper.c        |  33 ++++
 t/t3207-branch-submodule.sh        | 249 +++++++++++++++++++++++++++++
 9 files changed, 452 insertions(+), 1 deletion(-)
 create mode 100755 t/t3207-branch-submodule.sh

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index 063eec2511..e52262dc69 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` 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..c318b849aa 100644
--- a/Documentation/config/submodule.txt
+++ b/Documentation/config/submodule.txt
@@ -72,6 +72,15 @@ submodule.recurse::
 	For these commands a workaround is to temporarily change the
 	configuration value by using `git -c submodule.recurse=0`.
 
+submodule.propagateBranches::
+	[EXPERIMENTAL] A boolean that enables branching support with
+	submodules. This allows certain commands to accept
+	`--recurse-submodules` (`git branch --recurse-submodules` will
+	create branches recursively), and certain commands that already
+	accept `--recurse-submodules` will now consider branches (`git
+	switch --recurse-submodules` will switch to the correct branch
+	in all submodules).
+
 submodule.fetchJobs::
 	Specifies how many submodules are fetched/cloned at the same time.
 	A positive integer allows up to that number of submodules fetched
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 528cb2d639..404766d01d 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;
@@ -345,6 +347,127 @@ void create_branch(struct repository *r, const char *name,
 	free(real_ref);
 }
 
+static int submodule_validate_branchname(struct repository *r, const char *name,
+					 const char *start_name, int force,
+					 int quiet, char **err_msg)
+{
+	int ret = 0;
+	struct child_process child = CHILD_PROCESS_INIT;
+	struct strbuf child_err = STRBUF_INIT;
+	child.git_cmd = 1;
+	child.err = -1;
+
+	prepare_other_repo_env(&child.env_array, r->gitdir);
+	strvec_pushl(&child.args, "branch", "--dry-run", NULL);
+	if (force)
+		strvec_push(&child.args, "--force");
+	if (quiet)
+		strvec_push(&child.args, "--quiet");
+	strvec_pushl(&child.args, name, start_name, NULL);
+
+	if ((ret = start_command(&child)))
+		return ret;
+	ret = finish_command(&child);
+	strbuf_read(&child_err, child.err, 0);
+	*err_msg = strbuf_detach(&child_err, NULL);
+	return ret;
+}
+
+static int submodule_create_branch(struct repository *r, const char *name,
+				   const char *start_oid,
+				   const char *start_name, int force,
+				   int reflog, int quiet,
+				   enum branch_track track, char **err_msg)
+{
+	int ret = 0;
+	struct child_process child = CHILD_PROCESS_INIT;
+	struct strbuf child_err = STRBUF_INIT;
+	child.git_cmd = 1;
+	child.err = -1;
+
+	prepare_other_repo_env(&child.env_array, r->gitdir);
+	strvec_pushl(&child.args, "submodule--helper", "create-branch", NULL);
+	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);
+	*err_msg = strbuf_detach(&child_err, NULL);
+	return ret;
+}
+
+void create_submodule_branches(struct repository *r, const char *name,
+			       const char *start_name, int force, int reflog,
+			       int quiet, enum branch_track track)
+{
+	int i = 0;
+	char *branch_point = NULL;
+	struct repository *subrepos;
+	struct submodule *submodules;
+	struct object_id super_oid;
+	struct submodule_entry_list *submodule_entry_list;
+	char *err_msg = NULL;
+
+	validate_branch_start(r, start_name, track, &super_oid, &branch_point);
+
+	submodule_entry_list = submodules_of_tree(r, &super_oid);
+	CALLOC_ARRAY(subrepos, submodule_entry_list->entry_nr);
+	CALLOC_ARRAY(submodules, submodule_entry_list->entry_nr);
+
+	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
+		submodules[i] = *submodule_from_path(
+			r, &super_oid,
+			submodule_entry_list->name_entries[i].path);
+
+		if (repo_submodule_init(
+			    &subrepos[i], r,
+			    submodule_entry_list->name_entries[i].path,
+			    &super_oid)) {
+			die(_("submodule %s: unable to find submodule"),
+			    submodules[i].name);
+			if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED))
+				advise(_("You may try initializing the submodules using 'git checkout %s && git submodule update'"),
+				       start_name);
+		}
+
+		if (submodule_validate_branchname(
+			    &subrepos[i], name,
+			    oid_to_hex(
+				    &submodule_entry_list->name_entries[i].oid),
+			    force, quiet, &err_msg))
+			die(_("submodule %s: could not create branch '%s'\n\t%s"),
+			    submodules[i].name, name, err_msg);
+	}
+
+	create_branch(the_repository, name, start_name, force, 0, reflog, quiet,
+		      track);
+
+	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
+		printf_ln(_("submodule %s: creating branch '%s'"),
+			  submodules[i].name, name);
+		if (submodule_create_branch(
+			    &subrepos[i], name,
+			    oid_to_hex(
+				    &submodule_entry_list->name_entries[i].oid),
+			    branch_point, force, reflog, quiet, track,
+			    &err_msg))
+			die(_("submodule %s: could not create branch '%s'\n\t%s"),
+			    submodules[i].name, name, err_msg);
+
+		repo_clear(&subrepos[i]);
+	}
+}
+
 void remove_merge_branch_state(struct repository *r)
 {
 	unlink(git_path_merge_head(r));
diff --git a/branch.h b/branch.h
index d8e5ff4e28..1b4a635a2f 100644
--- a/branch.h
+++ b/branch.h
@@ -76,6 +76,12 @@ void create_branch(struct repository *r,
 		   int force, int clobber_head_ok,
 		   int reflog, int quiet, enum branch_track track);
 
+/*
+ * Creates a new branch in repository and its submodules.
+ */
+void create_submodule_branches(struct repository *r, const char *name,
+			       const char *start_name, int force, int reflog,
+			       int quiet, enum branch_track track);
 /*
  * 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 5d4b9c82b4..6a16bdb1a3 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -39,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] = {
@@ -101,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);
 }
 
@@ -621,7 +632,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	int delete = 0, rename = 0, copy = 0, force = 0, list = 0, create = 0,
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	/* possible options */
-	int reflog = 0, quiet = 0, dry_run = 0, icase = 0;
+	int reflog = 0, quiet = 0, dry_run = 0, icase = 0,
+	    recurse_submodules_explicit = 0;
 	const char *new_upstream = NULL;
 	enum branch_track track;
 	struct ref_filter filter;
@@ -670,6 +682,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__DRY_RUN(&dry_run, N_("show whether the branch would be created")),
 		OPT_END(),
@@ -713,9 +726,16 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	if (create < 0)
 		usage_with_options(builtin_branch_usage, options);
 
+	if (recurse_submodules_explicit && submodule_propagate_branches &&
+	    !create)
+		die(_("--recurse-submodules can only be used to create branches"));
 	if (dry_run && !create)
 		die(_("--dry-run can only be used when creating branches"));
 
+	recurse_submodules =
+		(recurse_submodules || recurse_submodules_explicit) &&
+		submodule_propagate_branches;
+
 	if (filter.abbrev == -1)
 		filter.abbrev = DEFAULT_ABBREV;
 	filter.ignore_case = icase;
@@ -874,6 +894,12 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 			FREE_AND_NULL(unused_full_ref);
 			return 0;
 		}
+		if (recurse_submodules) {
+			create_submodule_branches(the_repository, branch_name,
+						  start_name, force, reflog,
+						  quiet, track);
+			return 0;
+		}
 		create_branch(the_repository, branch_name, start_name, force, 0,
 			      reflog, quiet, track);
 	} else
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6298cbdd4e..3ea1e8cc96 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,37 @@ 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;
+
+	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_END()
+	};
+	const char *const usage[] = {
+		N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] <name> <start_oid> <start_name>"),
+		NULL
+	};
+
+	argc = parse_options(argc, argv, prefix, options, usage, 0);
+
+	if (argc != 3)
+		usage_with_options(usage, options);
+
+	create_branch(the_repository, argv[0], argv[1], force, 0, reflog, quiet,
+		      BRANCH_TRACK_NEVER);
+	setup_tracking(argv[0], argv[2], track, quiet, 0);
+
+	return 0;
+}
 struct add_data {
 	const char *prefix;
 	const char *branch;
@@ -3379,6 +3411,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/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
new file mode 100755
index 0000000000..14ff066e91
--- /dev/null
+++ b/t/t3207-branch-submodule.sh
@@ -0,0 +1,249 @@
+#!/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
+
+test_expect_success 'setup superproject and submodule' '
+	git init super &&
+	test_commit foo &&
+	git init sub-upstream &&
+	test_commit -C sub-upstream foo &&
+	git -C super submodule add ../sub-upstream sub &&
+	git -C super commit -m "add submodule" &&
+	git -C super config submodule.propagateBranches true
+'
+
+cleanup_branches() {
+	super_dir="$1"
+	shift
+	(
+		cd "$super_dir" &&
+		git checkout main &&
+		for branch_name in "$@"; do
+			git branch -D "$branch_name"
+			git submodule foreach "(git checkout main && git branch -D $branch_name) || true"
+		done
+	)
+} >/dev/null 2>/dev/null
+
+# Test the argument parsing
+test_expect_success '--recurse-submodules should create branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success '--recurse-submodules should be ignored if submodule.propagateBranches is false' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -c submodule.propagateBranches=false branch --recurse-submodules branch-a &&
+		git rev-parse branch-a &&
+		test_must_fail git -C sub rev-parse branch-a
+	)
+'
+
+test_expect_success '--recurse-submodules should fail when not creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should respect submodule.recurse when creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -c submodule.recurse=true branch branch-a &&
+		git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should ignore submodule.recurse when not creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git -c submodule.recurse=true branch -D branch-a &&
+		test_must_fail git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+# Test branch creation behavior
+test_expect_success 'should create branches based off commit id in superproject' '
+	test_when_finished "cleanup_branches super branch-a branch-b" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 &&
+
+		cat >expected <<EOF &&
+fatal: submodule sub: could not create branch ${SQ}branch-a${SQ}
+	fatal: A branch named ${SQ}branch-a${SQ} already exists.
+
+EOF
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branches if branch exists and --force is given' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -C sub rev-parse HEAD >expected &&
+		test_commit -C sub baz &&
+		git -C sub branch branch-a HEAD~1 &&
+		git branch --recurse-submodules --force branch-a &&
+		git rev-parse branch-a &&
+		# assert that sub:branch-a was moved
+		git -C sub rev-parse branch-a >actual &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branch when submodule is in .git/modules but not .gitmodules' '
+	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
+	(
+		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 checkout branch-a &&
+		git branch --recurse-submodules branch-c branch-b &&
+		git rev-parse branch-c &&
+		git -C sub rev-parse branch-c &&
+		git checkout --recurse-submodules branch-c &&
+		git -C sub2 rev-parse branch-c
+	)
+'
+
+test_expect_success 'should set up tracking of local branches with track=always' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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)" = ""
+	)
+'
+
+test_expect_success 'setup remote-tracking tests' '
+	(
+		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"
+	) &&
+	(
+		cd sub-upstream &&
+		git branch branch-a
+	) &&
+	git clone --branch main --recurse-submodules super super-clone &&
+	git -C super-clone config submodule.propagateBranches true
+'
+
+test_expect_success 'should not create branch when submodule is not in .git/modules' '
+	# The cleanup needs to delete sub2:branch-b in particular because main does not have sub2
+	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
+		cleanup_branches super-clone branch-a branch-b" &&
+	(
+		cd super-clone &&
+		# This should succeed because super-clone has sub.
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		# This should fail because super-clone does not have sub2.
+		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
+		cat >expected <<-EOF &&
+		fatal: submodule sub: unable to find submodule
+		You may reinitialize the submodules using ${SQ}git checkout origin/branch-b && git submodule update${SQ}
+		EOF
+		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 &&
+		git branch --recurse-submodules branch-b origin/branch-b
+	)
+'
+
+test_expect_success 'should set up tracking of remote-tracking branches' '
+	test_when_finished "cleanup_branches super-clone branch-a" &&
+	(
+		cd super-clone &&
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		test "$(git -C sub config branch.branch-a.remote)" = origin &&
+		test "$(git -C 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 "cleanup_branches super-clone branch-b" &&
+	(
+		cd super-clone &&
+		git branch --recurse-submodules branch-b origin/branch-b
+	)
+'
+
+test_done
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 1/4] submodule-config: add submodules_of_tree() helper
  2021-11-22 22:32 ` [PATCH 1/4] submodule-config: add submodules_of_tree() helper Glen Choo
@ 2021-11-23  2:12   ` Jonathan Tan
  2021-11-23 19:48     ` Glen Choo
  2021-11-23 10:53   ` Ævar Arnfjörð Bjarmason
  2021-11-23 22:46   ` Junio C Hamano
  2 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2021-11-23  2:12 UTC (permalink / raw)
  To: chooglen; +Cc: git, jonathantanmy, steadmon, emilyshaffer

Glen Choo <chooglen@google.com> writes:
> +struct submodule_entry_list *
> +submodules_of_tree(struct repository *r, const struct object_id *treeish_name)
> +{
> +	struct tree_desc tree;
> +	struct name_entry entry;
> +	struct submodule_entry_list *ret;
> +
> +	CALLOC_ARRAY(ret, 1);
> +	fill_tree_descriptor(r, &tree, treeish_name);
> +	while (tree_entry(&tree, &entry)) {

I think that tree_entry() doesn't recurse into subtrees, but in any case we
should test this. (I looked at patch 4 and I think that the submodules are
always in the root tree.)

This reminded me of a similar thing when fetching submodules recursively and we
needed the "before" and "after" of submodule gitlinks. You can look at the code
(collect_changed_submodules_cb() and the functions that use it in submodule.c)
but it may not be useful - in particular, that uses diff since we need to see
differences there, but we don't need that here.

I'll review the other patches tomorrow.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 3/4] branch: add --dry-run option to branch
  2021-11-22 22:32 ` [PATCH 3/4] branch: add --dry-run option to branch Glen Choo
@ 2021-11-23 10:42   ` Ævar Arnfjörð Bjarmason
  2021-11-23 18:42     ` Glen Choo
  2021-11-23 23:10   ` Jonathan Tan
  1 sibling, 1 reply; 110+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-11-23 10:42 UTC (permalink / raw)
  To: Glen Choo; +Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer


On Mon, Nov 22 2021, Glen Choo wrote:

> Add a --dry-run option to branch creation that can check whether or not
> a branch name and start point would be valid for a repository without
> creating a branch. Refactor cmd_branch() to make the chosen action more
> obvious.
> [...]
> -'git branch' [--track | --no-track] [-f] <branchname> [<start-point>]
> +'git branch' [--track | --no-track] [-f] [--dry-run | -n] <branchname> [<start-point>]
>  'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
>  'git branch' --unset-upstream [<branchname>]
>  'git branch' (-m | -M) [<oldbranch>] <newbranch>
> @@ -205,6 +205,12 @@ This option is only applicable in non-verbose mode.
>  --no-abbrev::
>  	Display the full sha1s in the output listing rather than abbreviating them.
>  
> +-n::
> +--dry-run::
> +	Can only be used when creating a branch. If the branch creation
> +	would fail, show the relevant error message. If the branch
> +	creation would succeed, show nothing.
> +

The usage & test show that we've got --dry-run for branch creation, but
not the "creation" we do on --copy or --move.

The former is just a "create from source", but "move" maybe not.

In any case, any reason to leave those out?

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-22 22:32 ` [PATCH 4/4] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-11-23 10:45   ` Ævar Arnfjörð Bjarmason
  2021-11-23 18:56     ` Glen Choo
  2021-11-23 19:41   ` Philippe Blain
  2021-11-24  1:31   ` Jonathan Tan
  2 siblings, 1 reply; 110+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-11-23 10:45 UTC (permalink / raw)
  To: Glen Choo; +Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer


On Mon, Nov 22 2021, Glen Choo wrote:

>  	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` was not run.
>  	addIgnoredFile::
>  		Advice shown if a user attempts to add an ignored file to
>  		the index.

Does it need to be submodule*s*NotUpdated? I.e. the existing error is
submodule.. (non-plural), and we surely error on this per-submodule? The
plural would make senes if the advice aggregates them to the end, let's
look at the implementation...

> [...]
> +	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
> +		submodules[i] = *submodule_from_path(
> +			r, &super_oid,
> +			submodule_entry_list->name_entries[i].path);
> +
> +		if (repo_submodule_init(
> +			    &subrepos[i], r,
> +			    submodule_entry_list->name_entries[i].path,
> +			    &super_oid)) {
> +			die(_("submodule %s: unable to find submodule"),
> +			    submodules[i].name);
> +			if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED))
> +				advise(_("You may try initializing the submodules using 'git checkout %s && git submodule update'"),
> +				       start_name);
> +		}

Uh, a call to advise() after die()? :) That code isn't reachable.

It would be good to add test for what the output is in the next
iteration, which would be a forcing function for making sure this code
works.

One thing I find quite annoying about submodules currently is the
verbosity of the output when we do N operations. E.g. I've got a repo
with 15-20 small submodules, cloning it prints out the usual "git clone"
verbosity, which isn't so much when cloning one repo, but with 15-20 it
fills up your screen.

Operations like these should also behave more like "git fetch --all",
surely? I.e. let's try to run the operation on all, but if some failed
along the way let's have our exit code reflect that, and ideally print
out an error summary, not N number of error() calls.

That would also justify the plural "submodulesNotUpdated", if such an
advise() printed out a summary of the N that failed at the end.


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 1/4] submodule-config: add submodules_of_tree() helper
  2021-11-22 22:32 ` [PATCH 1/4] submodule-config: add submodules_of_tree() helper Glen Choo
  2021-11-23  2:12   ` Jonathan Tan
@ 2021-11-23 10:53   ` Ævar Arnfjörð Bjarmason
  2021-11-23 18:35     ` Glen Choo
  2021-11-23 22:46   ` Junio C Hamano
  2 siblings, 1 reply; 110+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-11-23 10:53 UTC (permalink / raw)
  To: Glen Choo; +Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer


On Mon, Nov 22 2021, Glen Choo wrote:

> As we introduce a submodule UX with branches, we would like to be able
> to get the submodule commit ids in a superproject tree because those ids
> are the source of truth e.g. "git branch --recurse-submodules topic
> start-point" should create branches based off the commit ids recorded in
> the superproject's 'start-point' tree.
>
> To make this easy, introduce a submodules_of_tree() helper function that
> iterates through a tree and returns the tree's gitlink entries as a
> list.
>
> Signed-off-by: Glen Choo <chooglen@google.com>
> ---
>  submodule-config.c | 19 +++++++++++++++++++
>  submodule-config.h | 13 +++++++++++++
>  2 files changed, 32 insertions(+)
>
> diff --git a/submodule-config.c b/submodule-config.c
> index f95344028b..97da373301 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,24 @@ const struct submodule *submodule_from_path(struct repository *r,
>  	return config_from(r->submodule_cache, treeish_name, path, lookup_path);
>  }
>  
> +struct submodule_entry_list *
> +submodules_of_tree(struct repository *r, const struct object_id *treeish_name)
> +{
> +	struct tree_desc tree;
> +	struct name_entry entry;
> +	struct submodule_entry_list *ret;
> +
> +	CALLOC_ARRAY(ret, 1);
> +	fill_tree_descriptor(r, &tree, treeish_name);
> +	while (tree_entry(&tree, &entry)) {
> +		if (!S_ISGITLINK(entry.mode))
> +			continue;
> +		ALLOC_GROW(ret->name_entries, ret->entry_nr + 1, ret->entry_alloc);
> +		ret->name_entries[ret->entry_nr++] = entry;
> +	}
> +	return ret;
> +}
> +
>  void submodule_free(struct repository *r)
>  {
>  	if (r->submodule_cache)
> diff --git a/submodule-config.h b/submodule-config.h
> index 65875b94ea..4379ec77e3 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
> @@ -67,6 +68,18 @@ const struct submodule *submodule_from_name(struct repository *r,
>  					    const struct object_id *commit_or_tree,
>  					    const char *name);
>  
> +struct submodule_entry_list {
> +	struct name_entry *name_entries;
> +	int entry_nr;
> +	int entry_alloc;
> +};
> +
> +/**
> + * Given a tree-ish, return all submodules in the tree.
> + */
> +struct submodule_entry_list *
> +submodules_of_tree(struct repository *r, const struct object_id *treeish_name);
> +
>  /**
>   * Given a tree-ish in the superproject and a path, return the submodule that
>   * is bound at the path in the named tree.

Having skimmed through this topic isn't this in 4/4 the only resulting caller:
	
	+void create_submodule_branches(struct repository *r, const char *name,
	+			       const char *start_name, int force, int reflog,
	+			       int quiet, enum branch_track track)
	+{
	+	int i = 0;
	+	char *branch_point = NULL;
	+	struct repository *subrepos;
	+	struct submodule *submodules;
	+	struct object_id super_oid;
	+	struct submodule_entry_list *submodule_entry_list;
	+	char *err_msg = NULL;
	+
	+	validate_branch_start(r, start_name, track, &super_oid, &branch_point);
	+
	+	submodule_entry_list = submodules_of_tree(r, &super_oid);
	+	CALLOC_ARRAY(subrepos, submodule_entry_list->entry_nr);
	+	CALLOC_ARRAY(submodules, submodule_entry_list->entry_nr);
	+
	+	for (i = 0; i < submodule_entry_list->entry_nr; i++) {

I think it would be better to just intorduce this function at the same
time as its (only?) user, which also makes it clear how it's used.

In this case this seems like quite a bit of over-allocation. I.e. we
return a malloc'd pointer, and iterate with tree_entry(), the caller
then needs to loop over that and do its own allocations of "struct
repository *" and "struct submodule *".

Wouldn't it be better just to have this new submodule_entry_list contain
a list of not "struct name_entry", but:

    struct new_thingy {
        struct name_entry *entry;
        struct repository *repo;
        struct submodule *submodule;
    }

Then have the caller allocate the container on the stack, pass it to
this function.

Maybe not, just musings while doing some light reading. I was surprised
at what are effectively two loops over the same data, first allocating
1/3, then the other doing the other 2/3...

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 1/4] submodule-config: add submodules_of_tree() helper
  2021-11-23 10:53   ` Ævar Arnfjörð Bjarmason
@ 2021-11-23 18:35     ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-23 18:35 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> Having skimmed through this topic isn't this in 4/4 the only resulting caller:
> 	
> 	+void create_submodule_branches(struct repository *r, const char *name,
> 	+			       const char *start_name, int force, int reflog,
> 	+			       int quiet, enum branch_track track)
> 	+{
> 	+	int i = 0;
> 	+	char *branch_point = NULL;
> 	+	struct repository *subrepos;
> 	+	struct submodule *submodules;
> 	+	struct object_id super_oid;
> 	+	struct submodule_entry_list *submodule_entry_list;
> 	+	char *err_msg = NULL;
> 	+
> 	+	validate_branch_start(r, start_name, track, &super_oid, &branch_point);
> 	+
> 	+	submodule_entry_list = submodules_of_tree(r, &super_oid);
> 	+	CALLOC_ARRAY(subrepos, submodule_entry_list->entry_nr);
> 	+	CALLOC_ARRAY(submodules, submodule_entry_list->entry_nr);
> 	+
> 	+	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
>
> I think it would be better to just intorduce this function at the same
> time as its (only?) user, which also makes it clear how it's used.

Yes that makes sense. That is the only user (for now). 

> In this case this seems like quite a bit of over-allocation. I.e. we
> return a malloc'd pointer, and iterate with tree_entry(), the caller
> then needs to loop over that and do its own allocations of "struct
> repository *" and "struct submodule *".
>
> Wouldn't it be better just to have this new submodule_entry_list contain
> a list of not "struct name_entry", but:
>
>     struct new_thingy {
>         struct name_entry *entry;
>         struct repository *repo;
>         struct submodule *submodule;
>     }
>
> Then have the caller allocate the container on the stack, pass it to
> this function.

I thought about it as well. "struct new_thingy" is obviously the right
struct for create_submodule_branches(), but I'm not sure if it is the
right thing for other future callers e.g. "struct submodule" is only
used to give a submodule name to users in help messages.

But chances are, any caller that needs 'submodules of a tree' will need
very similar pieces of information, so it seems reasonable to do what
you said instead of over-allocating in all of the callers.

> Maybe not, just musings while doing some light reading. I was surprised
> at what are effectively two loops over the same data, first allocating
> 1/3, then the other doing the other 2/3...

The first loop validates all submodules before creating any branches
(and also happens to allocate). If we didn't have the validation step,
allocation + creating branches could just be one loop :)

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 3/4] branch: add --dry-run option to branch
  2021-11-23 10:42   ` Ævar Arnfjörð Bjarmason
@ 2021-11-23 18:42     ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-23 18:42 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Mon, Nov 22 2021, Glen Choo wrote:
>
>> Add a --dry-run option to branch creation that can check whether or not
>> a branch name and start point would be valid for a repository without
>> creating a branch. Refactor cmd_branch() to make the chosen action more
>> obvious.
>> [...]
>> -'git branch' [--track | --no-track] [-f] <branchname> [<start-point>]
>> +'git branch' [--track | --no-track] [-f] [--dry-run | -n] <branchname> [<start-point>]
>>  'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
>>  'git branch' --unset-upstream [<branchname>]
>>  'git branch' (-m | -M) [<oldbranch>] <newbranch>
>> @@ -205,6 +205,12 @@ This option is only applicable in non-verbose mode.
>>  --no-abbrev::
>>  	Display the full sha1s in the output listing rather than abbreviating them.
>>  
>> +-n::
>> +--dry-run::
>> +	Can only be used when creating a branch. If the branch creation
>> +	would fail, show the relevant error message. If the branch
>> +	creation would succeed, show nothing.
>> +
>
> The usage & test show that we've got --dry-run for branch creation, but
> not the "creation" we do on --copy or --move.

Perhaps this is more of a wording issue i.e. 'creating a branch' is too
unspecific. Maybe 

	Can only be used when creating a new branch (without copying or moving
	an existing branch). If the branch creation would fail, show the
	relevant error message. If the branch creation would succeed, show
	nothing.

> In any case, any reason to leave those out?

No long term reason. I left those out because "create brand new branch
with --dry-run" was needed right now, but the others are not. For
consistency, we'd want --dry-run for all other actions, including copy,
move, delete, etc.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-23 10:45   ` Ævar Arnfjörð Bjarmason
@ 2021-11-23 18:56     ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-23 18:56 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Mon, Nov 22 2021, Glen Choo wrote:
>
>>  	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` was not run.
>>  	addIgnoredFile::
>>  		Advice shown if a user attempts to add an ignored file to
>>  		the index.
>
> Does it need to be submodule*s*NotUpdated? I.e. the existing error is
> submodule.. (non-plural), and we surely error on this per-submodule?

From the user's perspective, failing on a per-submodule basis looks like
an implementation detail. As a user, I am looking for advice on errors
that could be avoided by running "git submodule update"; I'd want the
advice option to describe what "git submodule update" does, which is
update submodule*s*.

>> [...]
>> +	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
>> +		submodules[i] = *submodule_from_path(
>> +			r, &super_oid,
>> +			submodule_entry_list->name_entries[i].path);
>> +
>> +		if (repo_submodule_init(
>> +			    &subrepos[i], r,
>> +			    submodule_entry_list->name_entries[i].path,
>> +			    &super_oid)) {
>> +			die(_("submodule %s: unable to find submodule"),
>> +			    submodules[i].name);
>> +			if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED))
>> +				advise(_("You may try initializing the submodules using 'git checkout %s && git submodule update'"),
>> +				       start_name);
>> +		}
>
> Uh, a call to advise() after die()? :) That code isn't reachable.
>
> It would be good to add test for what the output is in the next
> iteration, which would be a forcing function for making sure this code
> works.

Whoops. Good catch. Yes I should test that.

> One thing I find quite annoying about submodules currently is the
> verbosity of the output when we do N operations. E.g. I've got a repo
> with 15-20 small submodules, cloning it prints out the usual "git clone"
> verbosity, which isn't so much when cloning one repo, but with 15-20 it
> fills up your screen.
>
> Operations like these should also behave more like "git fetch --all",
> surely? I.e. let's try to run the operation on all, but if some failed
> along the way let's have our exit code reflect that, and ideally print
> out an error summary, not N number of error() calls.

That's a valid criticism, and one we're concerned about as well. I'm
not 100% satisfied with how I've structured the output either, but as a
practical matter, figuring out an *ideal* output format and
standardizing it takes up too much valuable iteration time that could be
spent on improving the UX instead.

So the approach here is to have output that is 'good enough' for now,
and to create better and standardized output when we've nailed down more
of the UX.

> That would also justify the plural "submodulesNotUpdated", if such an
> advise() printed out a summary of the N that failed at the end.

Fair. We could treat an uninitialized submodule the same as any other
failure and summarize all failures.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-22 22:32 ` [PATCH 4/4] branch: add --recurse-submodules option for branch creation Glen Choo
  2021-11-23 10:45   ` Ævar Arnfjörð Bjarmason
@ 2021-11-23 19:41   ` Philippe Blain
  2021-11-23 23:43     ` Glen Choo
  2021-11-24  1:31   ` Jonathan Tan
  2 siblings, 1 reply; 110+ messages in thread
From: Philippe Blain @ 2021-11-23 19:41 UTC (permalink / raw)
  To: Glen Choo, git
  Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason

Hi Glen,

Le 2021-11-22 à 17:32, Glen Choo a écrit :
> Add a --recurse-submodules option when creating branches so that `git
> branch --recurse-submodules topic` will create the "topic" branch in the
> superproject and all submodules. Guard this (and future submodule
> branching) behavior behind a new configuration value
> 'submodule.propagateBranches'.
> 
> Signed-off-by: Glen Choo <chooglen@google.com>
> ---
>   Documentation/config/advice.txt    |   3 +
>   Documentation/config/submodule.txt |   9 ++

We would need to add the new flag to Documentation/git-branch.txt,
and also probably update the documentation of 'submodule.recurse'
in 'Documentation/config/submodule.txt'.

>   advice.c                           |   1 +
>   advice.h                           |   1 +
>   branch.c                           | 123 ++++++++++++++
>   branch.h                           |   6 +
>   builtin/branch.c                   |  28 +++-
>   builtin/submodule--helper.c        |  33 ++++
>   t/t3207-branch-submodule.sh        | 249 +++++++++++++++++++++++++++++
>   9 files changed, 452 insertions(+), 1 deletion(-)
>   create mode 100755 t/t3207-branch-submodule.sh
> 
> diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
> index 063eec2511..e52262dc69 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` 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..c318b849aa 100644
> --- a/Documentation/config/submodule.txt
> +++ b/Documentation/config/submodule.txt
> @@ -72,6 +72,15 @@ submodule.recurse::
>   	For these commands a workaround is to temporarily change the
>   	configuration value by using `git -c submodule.recurse=0`.
>   
> +submodule.propagateBranches::
> +	[EXPERIMENTAL] A boolean that enables branching support with
> +	submodules. This allows certain commands to accept
> +	`--recurse-submodules` (`git branch --recurse-submodules` will
> +	create branches recursively), and certain commands that already
> +	accept `--recurse-submodules` will now consider branches (`git
> +	switch --recurse-submodules` will switch to the correct branch
> +	in all submodules).

Looking at the rest of the patch, this just implements 'branch --recurse-submodules', right ?
i.e. 'git switch' and 'git checkout' are left alone for
now, so I think this addition to the doc should only mention 'git branch'.

> +
>   submodule.fetchJobs::
>   	Specifies how many submodules are fetched/cloned at the same time.
>   	A positive integer allows up to that number of submodules fetched
> 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 528cb2d639..404766d01d 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;
> @@ -345,6 +347,127 @@ void create_branch(struct repository *r, const char *name,
>   	free(real_ref);
>   }
>   
> +static int submodule_validate_branchname(struct repository *r, const char *name,
> +					 const char *start_name, int force,
> +					 int quiet, char **err_msg)
> +{
> +	int ret = 0;
> +	struct child_process child = CHILD_PROCESS_INIT;
> +	struct strbuf child_err = STRBUF_INIT;
> +	child.git_cmd = 1;
> +	child.err = -1;
> +
> +	prepare_other_repo_env(&child.env_array, r->gitdir);
> +	strvec_pushl(&child.args, "branch", "--dry-run", NULL);
> +	if (force)
> +		strvec_push(&child.args, "--force");
> +	if (quiet)
> +		strvec_push(&child.args, "--quiet");
> +	strvec_pushl(&child.args, name, start_name, NULL);
> +
> +	if ((ret = start_command(&child)))
> +		return ret;
> +	ret = finish_command(&child);
> +	strbuf_read(&child_err, child.err, 0);
> +	*err_msg = strbuf_detach(&child_err, NULL);
> +	return ret;
> +}
> +
> +static int submodule_create_branch(struct repository *r, const char *name,
> +				   const char *start_oid,
> +				   const char *start_name, int force,
> +				   int reflog, int quiet,
> +				   enum branch_track track, char **err_msg)
> +{
> +	int ret = 0;
> +	struct child_process child = CHILD_PROCESS_INIT;
> +	struct strbuf child_err = STRBUF_INIT;
> +	child.git_cmd = 1;
> +	child.err = -1;
> +
> +	prepare_other_repo_env(&child.env_array, r->gitdir);
> +	strvec_pushl(&child.args, "submodule--helper", "create-branch", NULL);
> +	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);
> +	*err_msg = strbuf_detach(&child_err, NULL);
> +	return ret;
> +}
> +
> +void create_submodule_branches(struct repository *r, const char *name,
> +			       const char *start_name, int force, int reflog,
> +			       int quiet, enum branch_track track)
> +{
> +	int i = 0;
> +	char *branch_point = NULL;
> +	struct repository *subrepos;
> +	struct submodule *submodules;
> +	struct object_id super_oid;
> +	struct submodule_entry_list *submodule_entry_list;
> +	char *err_msg = NULL;
> +
> +	validate_branch_start(r, start_name, track, &super_oid, &branch_point);
> +
> +	submodule_entry_list = submodules_of_tree(r, &super_oid);
> +	CALLOC_ARRAY(subrepos, submodule_entry_list->entry_nr);
> +	CALLOC_ARRAY(submodules, submodule_entry_list->entry_nr);
> +
> +	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
> +		submodules[i] = *submodule_from_path(
> +			r, &super_oid,
> +			submodule_entry_list->name_entries[i].path);
> +
> +		if (repo_submodule_init(
> +			    &subrepos[i], r,
> +			    submodule_entry_list->name_entries[i].path,
> +			    &super_oid)) {
> +			die(_("submodule %s: unable to find submodule"),
> +			    submodules[i].name);
> +			if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED))
> +				advise(_("You may try initializing the submodules using 'git checkout %s && git submodule update'"),
> +				       start_name);

Apart from what Ævar pointed out about advise() being called after die(),
I'm not sure this is the right advice, because if repo_submodule_init fails
it means there is no .git/modules/<name> directory corresponding to the submodule's
Git repository, i.e. the submodule was never cloned. So it's not guaranteed
that 'git checkout $start_name && git submodule update' would initialize (and clone) it,
not without '--init'.

> +		}
> +
> +		if (submodule_validate_branchname(
> +			    &subrepos[i], name,
> +			    oid_to_hex(
> +				    &submodule_entry_list->name_entries[i].oid),
> +			    force, quiet, &err_msg))
> +			die(_("submodule %s: could not create branch '%s'\n\t%s"),
> +			    submodules[i].name, name, err_msg);

minor nit abour wording: we did not try to create the branch here, we just checked if
creating is possible. So it *might* be confusing (maybe not). Maybe just "can not"
instead of "could not" ?

> +	}
> +
> +	create_branch(the_repository, name, start_name, force, 0, reflog, quiet,
> +		      track);

OK, here we create the superproject branch. This might not be expected when
just reading the name of the function...

> +
> +	for (i = 0; i < submodule_entry_list->entry_nr; i++) {

Here we loop over all submodules, so branches are created even in
inactive submodules... this might not be wanted.

> +		printf_ln(_("submodule %s: creating branch '%s'"),
> +			  submodules[i].name, name);
> +		if (submodule_create_branch(
> +			    &subrepos[i], name,
> +			    oid_to_hex(
> +				    &submodule_entry_list->name_entries[i].oid),
> +			    branch_point, force, reflog, quiet, track,
> +			    &err_msg))
> +			die(_("submodule %s: could not create branch '%s'\n\t%s"),
> +			    submodules[i].name, name, err_msg);
> +
> +		repo_clear(&subrepos[i]);
> +	}
> +}
> +
>   void remove_merge_branch_state(struct repository *r)
>   {
>   	unlink(git_path_merge_head(r));
> diff --git a/branch.h b/branch.h
> index d8e5ff4e28..1b4a635a2f 100644
> --- a/branch.h
> +++ b/branch.h
> @@ -76,6 +76,12 @@ void create_branch(struct repository *r,
>   		   int force, int clobber_head_ok,
>   		   int reflog, int quiet, enum branch_track track);
>   
> +/*
> + * Creates a new branch in repository and its submodules.
> + */
> +void create_submodule_branches(struct repository *r, const char *name,
> +			       const char *start_name, int force, int reflog,
> +			       int quiet, enum branch_track track);
>   /*
>    * 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 5d4b9c82b4..6a16bdb1a3 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -39,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] = {
> @@ -101,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);
>   }
>   
> @@ -621,7 +632,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>   	int delete = 0, rename = 0, copy = 0, force = 0, list = 0, create = 0,
>   	    unset_upstream = 0, show_current = 0, edit_description = 0;
>   	/* possible options */
> -	int reflog = 0, quiet = 0, dry_run = 0, icase = 0;
> +	int reflog = 0, quiet = 0, dry_run = 0, icase = 0,
> +	    recurse_submodules_explicit = 0;
>   	const char *new_upstream = NULL;
>   	enum branch_track track;
>   	struct ref_filter filter;
> @@ -670,6 +682,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__DRY_RUN(&dry_run, N_("show whether the branch would be created")),
>   		OPT_END(),
> @@ -713,9 +726,16 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>   	if (create < 0)
>   		usage_with_options(builtin_branch_usage, options);
>   
> +	if (recurse_submodules_explicit && submodule_propagate_branches &&
> +	    !create)
> +		die(_("--recurse-submodules can only be used to create branches"));
>   	if (dry_run && !create)
>   		die(_("--dry-run can only be used when creating branches"));
>   
> +	recurse_submodules =
> +		(recurse_submodules || recurse_submodules_explicit) &&
> +		submodule_propagate_branches;
> +

OK, so we get the new behaviour if either --recurse-submodules was used, or 'submodule.recurse' is true,
and in both case we also need the new submodule.propagateBranches config set.

Why not adding 'branch.recurseSubmodules' instead, with a higher priority than submodule.recurse ?
Is it because then it would be mildly confusing for 'git checkout / git switch' to also honor
a setting named 'branch.*' when they learn the new behaviour ? (I don't think this would be the
first time that 'git foo' honors 'bar.*', so it might be worth mentioning).

Also, why do we quietly ignore '--recurse-submodules' if submodule.propagateBranches is unset ?
Wouldn't it be better to warn the user "hey, if you want this new behaviour you need to
set that config !" ?

I don't have a strong opinion about the fact that you need to set the config in the first
place, but I think it should be mentioned in the commit message why you chose to implement
it that way (meaning, why do we need a config set, instead of adding the config but defaulting it
to true, so that you get the new behaviour by default, but you still can disable it if you do not
want it)...

>   	if (filter.abbrev == -1)
>   		filter.abbrev = DEFAULT_ABBREV;
>   	filter.ignore_case = icase;
> @@ -874,6 +894,12 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>   			FREE_AND_NULL(unused_full_ref);
>   			return 0;
>   		}
> +		if (recurse_submodules) {
> +			create_submodule_branches(the_repository, branch_name,
> +						  start_name, force, reflog,
> +						  quiet, track);
> +			return 0;
> +		}
>   		create_branch(the_repository, branch_name, start_name, force, 0,
>   			      reflog, quiet, track);
>   	} else
> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
> index 6298cbdd4e..3ea1e8cc96 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,37 @@ 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;
> +
> +	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_END()
> +	};
> +	const char *const usage[] = {
> +		N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] <name> <start_oid> <start_name>"),
> +		NULL
> +	};
> +
> +	argc = parse_options(argc, argv, prefix, options, usage, 0);
> +
> +	if (argc != 3)
> +		usage_with_options(usage, options);
> +
> +	create_branch(the_repository, argv[0], argv[1], force, 0, reflog, quiet,
> +		      BRANCH_TRACK_NEVER);
> +	setup_tracking(argv[0], argv[2], track, quiet, 0);
> +
> +	return 0;
> +}
>   struct add_data {
>   	const char *prefix;
>   	const char *branch;
> @@ -3379,6 +3411,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/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
> new file mode 100755
> index 0000000000..14ff066e91
> --- /dev/null
> +++ b/t/t3207-branch-submodule.sh
> @@ -0,0 +1,249 @@
> +#!/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
> +
> +test_expect_success 'setup superproject and submodule' '
> +	git init super &&
> +	test_commit foo &&
> +	git init sub-upstream &&
> +	test_commit -C sub-upstream foo &&
> +	git -C super submodule add ../sub-upstream sub &&
> +	git -C super commit -m "add submodule" &&
> +	git -C super config submodule.propagateBranches true
> +'
> +
> +cleanup_branches() {
> +	super_dir="$1"
> +	shift
> +	(
> +		cd "$super_dir" &&
> +		git checkout main &&
> +		for branch_name in "$@"; do
> +			git branch -D "$branch_name"
> +			git submodule foreach "(git checkout main && git branch -D $branch_name) || true"
> +		done
> +	)
> +} >/dev/null 2>/dev/null
> +
> +# Test the argument parsing
> +test_expect_success '--recurse-submodules should create branches' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		cd super &&
> +		git branch --recurse-submodules branch-a &&
> +		git rev-parse --abbrev-ref branch-a &&
> +		git -C sub rev-parse --abbrev-ref branch-a
> +	)
> +'
> +
> +test_expect_success '--recurse-submodules should be ignored if submodule.propagateBranches is false' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		cd super &&
> +		git -c submodule.propagateBranches=false branch --recurse-submodules branch-a &&
> +		git rev-parse branch-a &&
> +		test_must_fail git -C sub rev-parse branch-a
> +	)
> +'
> +
> +test_expect_success '--recurse-submodules should fail when not creating branches' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		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 --abbrev-ref branch-a &&
> +		git -C sub rev-parse --abbrev-ref branch-a
> +	)
> +'
> +
> +test_expect_success 'should respect submodule.recurse when creating branches' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		cd super &&
> +		git -c submodule.recurse=true branch branch-a &&
> +		git rev-parse --abbrev-ref branch-a &&
> +		git -C sub rev-parse --abbrev-ref branch-a
> +	)
> +'
> +
> +test_expect_success 'should ignore submodule.recurse when not creating branches' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		cd super &&
> +		git branch --recurse-submodules branch-a &&
> +		git -c submodule.recurse=true branch -D branch-a &&
> +		test_must_fail git rev-parse --abbrev-ref branch-a &&
> +		git -C sub rev-parse --abbrev-ref branch-a
> +	)
> +'
> +
> +# Test branch creation behavior
> +test_expect_success 'should create branches based off commit id in superproject' '
> +	test_when_finished "cleanup_branches super branch-a branch-b" &&
> +	(
> +		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 "cleanup_branches super branch-a" &&
> +	(
> +		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 &&
> +
> +		cat >expected <<EOF &&
> +fatal: submodule sub: could not create branch ${SQ}branch-a${SQ}
> +	fatal: A branch named ${SQ}branch-a${SQ} already exists.
> +
> +EOF
> +		test_cmp expected actual
> +	)
> +'
> +
> +test_expect_success 'should create branches if branch exists and --force is given' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		cd super &&
> +		git -C sub rev-parse HEAD >expected &&
> +		test_commit -C sub baz &&
> +		git -C sub branch branch-a HEAD~1 &&
> +		git branch --recurse-submodules --force branch-a &&
> +		git rev-parse branch-a &&
> +		# assert that sub:branch-a was moved
> +		git -C sub rev-parse branch-a >actual &&
> +		test_cmp expected actual
> +	)
> +'
> +
> +test_expect_success 'should create branch when submodule is in .git/modules but not .gitmodules' '
> +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
> +	(
> +		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 checkout branch-a &&
> +		git branch --recurse-submodules branch-c branch-b &&
> +		git rev-parse branch-c &&
> +		git -C sub rev-parse branch-c &&
> +		git checkout --recurse-submodules branch-c &&
> +		git -C sub2 rev-parse branch-c
> +	)
> +'
> +
> +test_expect_success 'should set up tracking of local branches with track=always' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		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 "cleanup_branches super branch-a" &&
> +	(
> +		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 "cleanup_branches super branch-a" &&
> +	(
> +		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)" = ""
> +	)

don't we have a "config is empty" test helper or something similar ?

> +'
> +
> +test_expect_success 'setup remote-tracking tests' '
> +	(
> +		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"
> +	) &&
> +	(
> +		cd sub-upstream &&
> +		git branch branch-a
> +	) &&
> +	git clone --branch main --recurse-submodules super super-clone &&
> +	git -C super-clone config submodule.propagateBranches true
> +'
> +
> +test_expect_success 'should not create branch when submodule is not in .git/modules' '
> +	# The cleanup needs to delete sub2:branch-b in particular because main does not have sub2
> +	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
> +		cleanup_branches super-clone branch-a branch-b" &&
> +	(
> +		cd super-clone &&
> +		# This should succeed because super-clone has sub.
> +		git branch --recurse-submodules branch-a origin/branch-a &&
> +		# This should fail because super-clone does not have sub2.
> +		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
> +		cat >expected <<-EOF &&
> +		fatal: submodule sub: unable to find submodule
> +		You may reinitialize the submodules using ${SQ}git checkout origin/branch-b && git submodule update${SQ}
> +		EOF
> +		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 &&
> +		git branch --recurse-submodules branch-b origin/branch-b
> +	)

Considering what has been pointed out above, I'm not sure why this test passes...
Unless I'm missing something.

> +'
> +
> +test_expect_success 'should set up tracking of remote-tracking branches' '
> +	test_when_finished "cleanup_branches super-clone branch-a" &&
> +	(
> +		cd super-clone &&
> +		git branch --recurse-submodules branch-a origin/branch-a &&
> +		test "$(git -C sub config branch.branch-a.remote)" = origin &&
> +		test "$(git -C 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 "cleanup_branches super-clone branch-b" &&
> +	(
> +		cd super-clone &&
> +		git branch --recurse-submodules branch-b origin/branch-b
> +	)
> +'
> +
> +test_done
> 

Cheers,

Philippe.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 1/4] submodule-config: add submodules_of_tree() helper
  2021-11-23  2:12   ` Jonathan Tan
@ 2021-11-23 19:48     ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-23 19:48 UTC (permalink / raw)
  To: Jonathan Tan; +Cc: git, jonathantanmy, steadmon, emilyshaffer

Jonathan Tan <jonathantanmy@google.com> writes:

> I think that tree_entry() doesn't recurse into subtrees, but in any case we
> should test this. (I looked at patch 4 and I think that the submodules are
> always in the root tree.)

I've tested this and indeed it doesn't work. I've attached my test case
below.

> This reminded me of a similar thing when fetching submodules recursively and we
> needed the "before" and "after" of submodule gitlinks. You can look at the code
> (collect_changed_submodules_cb() and the functions that use it in submodule.c)
> but it may not be useful - in particular, that uses diff since we need to see
> differences there, but we don't need that here.

Thanks for the hint. If that fails, I could also implement it via the
helper methods in submodule--helper.

---- >8 ----

diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
index 14ff066e91..0e086f716d 100755
--- a/t/t3207-branch-submodule.sh
+++ b/t/t3207-branch-submodule.sh
@@ -11,11 +11,15 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 test_expect_success 'setup superproject and submodule' '
 	git init super &&
 	test_commit foo &&
+	git init sub-sub-upstream &&
+	test_commit -C sub-sub-upstream foo &&
 	git init sub-upstream &&
-	test_commit -C sub-upstream foo &&
-	git -C super submodule add ../sub-upstream sub &&
+	git -C sub-upstream submodule add "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
+	git -C sub-upstream commit -m "add submodule" &&
+	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
 	git -C super commit -m "add submodule" &&
-	git -C super config submodule.propagateBranches true
+	git -C super config submodule.propagateBranches true &&
+	git -C super/sub submodule update --init
 '
 
 cleanup_branches() {
@@ -26,7 +30,7 @@ cleanup_branches() {
 		git checkout main &&
 		for branch_name in "$@"; do
 			git branch -D "$branch_name"
-			git submodule foreach "(git checkout main && git branch -D $branch_name) || true"
+			git submodule foreach "cleanup_branches . $branch_name || true"
 		done
 	)
 } >/dev/null 2>/dev/null
@@ -37,8 +41,9 @@ test_expect_success '--recurse-submodules should create branches' '
 	(
 		cd super &&
 		git branch --recurse-submodules branch-a &&
-		git rev-parse --abbrev-ref branch-a &&
-		git -C sub rev-parse --abbrev-ref branch-a
+		git rev-parse branch-a &&
+		git -C sub rev-parse branch-a &&
+		git -C sub/sub-sub rev-parse branch-a
 	)
 '
 

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 1/4] submodule-config: add submodules_of_tree() helper
  2021-11-22 22:32 ` [PATCH 1/4] submodule-config: add submodules_of_tree() helper Glen Choo
  2021-11-23  2:12   ` Jonathan Tan
  2021-11-23 10:53   ` Ævar Arnfjörð Bjarmason
@ 2021-11-23 22:46   ` Junio C Hamano
  2 siblings, 0 replies; 110+ messages in thread
From: Junio C Hamano @ 2021-11-23 22:46 UTC (permalink / raw)
  To: Glen Choo; +Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer

Glen Choo <chooglen@google.com> writes:

> +struct submodule_entry_list *
> +submodules_of_tree(struct repository *r, const struct object_id *treeish_name)
> +{
> +	struct tree_desc tree;
> +	struct name_entry entry;
> +	struct submodule_entry_list *ret;
> +
> +	CALLOC_ARRAY(ret, 1);
> +	fill_tree_descriptor(r, &tree, treeish_name);
> +	while (tree_entry(&tree, &entry)) {
> +		if (!S_ISGITLINK(entry.mode))
> +			continue;
> +		ALLOC_GROW(ret->name_entries, ret->entry_nr + 1, ret->entry_alloc);
> +		ret->name_entries[ret->entry_nr++] = entry;
> +	}
> +	return ret;
> +}

This only looks at the root level of the tree, doesn't it?  Without
any caller in the same step, it is impossible to tell if that is an
outright bug, or merely an incomplete code that will gain recursion
in a later step.

If it is the latter, I do not think that is a patch series
organization that is very friendly to reviewers.



^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 3/4] branch: add --dry-run option to branch
  2021-11-22 22:32 ` [PATCH 3/4] branch: add --dry-run option to branch Glen Choo
  2021-11-23 10:42   ` Ævar Arnfjörð Bjarmason
@ 2021-11-23 23:10   ` Jonathan Tan
  2021-11-24  0:52     ` Glen Choo
  1 sibling, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2021-11-23 23:10 UTC (permalink / raw)
  To: chooglen; +Cc: git, jonathantanmy, steadmon, emilyshaffer

Glen Choo <chooglen@google.com> writes:
> When running "git branch --recurse-submodules topic"

At this point, this argument has not been introduced yet, so better to
just say "A future patch will introduce branch creation that recurses
into submodules, so..."

> +-n::
> +--dry-run::
> +	Can only be used when creating a branch. If the branch creation
> +	would fail, show the relevant error message. If the branch
> +	creation would succeed, show nothing.

Right now we only plan to use this internally so it's not worth using a
single character argument for this right now, I think. We can always add
it later if we find it useful.

> -	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
> -	    list + edit_description + unset_upstream > 1)
> +	create = 1 - (!!delete + !!rename + !!copy + !!new_upstream +
> +		      !!show_current + !!list + !!edit_description +
> +		      !!unset_upstream);
> +	if (create < 0)
>  		usage_with_options(builtin_branch_usage, options);

Hmm...I think it would be clearer just to call it noncreate_options and
print usage if it is greater than 1. Then whenever you want to check if
it's create, check `!noncreate_options` instead.

> @@ -852,10 +862,20 @@ 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);
> +		if (dry_run) {
> +			struct strbuf buf = STRBUF_INIT;
> +			char *unused_full_ref;
> +			struct object_id unused_oid;
>  
> +			validate_new_branchname(branch_name, &buf, force);
> +			validate_branch_start(the_repository, start_name, track,
> +					      &unused_oid, &unused_full_ref);
> +			strbuf_release(&buf);
> +			FREE_AND_NULL(unused_full_ref);
> +			return 0;
> +		}
> +		create_branch(the_repository, branch_name, start_name, force, 0,
> +			      reflog, quiet, track);
>  	} else
>  		usage_with_options(builtin_branch_usage, options);
>  

I don't think we should use separate code paths for the dry run and the
regular run - could create_branch() take a dry_run parameter instead?
(If there are too many boolean parameters, it might be time to convert
some or all to a struct.)

This suggestion would require a reworking of patch 2, which is why I
didn't comment there. But if we are not going to use this suggestion and
are going to stick with patch 2, then my comment on it is that it seems
to be doing too much: I ran "git show --color-moved" on it and saw that
quite a few lines were newly introduced (not just moved around).

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-23 19:41   ` Philippe Blain
@ 2021-11-23 23:43     ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-23 23:43 UTC (permalink / raw)
  To: Philippe Blain, git
  Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason


Thanks for the thorough review! I really appreciate it, Philippe :)

Philippe Blain <levraiphilippeblain@gmail.com> writes:

> Hi Glen,
>
> Le 2021-11-22 à 17:32, Glen Choo a écrit :
>> Add a --recurse-submodules option when creating branches so that `git
>> branch --recurse-submodules topic` will create the "topic" branch in the
>> superproject and all submodules. Guard this (and future submodule
>> branching) behavior behind a new configuration value
>> 'submodule.propagateBranches'.
>> 
>> Signed-off-by: Glen Choo <chooglen@google.com>
>> ---
>>   Documentation/config/advice.txt    |   3 +
>>   Documentation/config/submodule.txt |   9 ++
>
> We would need to add the new flag to Documentation/git-branch.txt,
> and also probably update the documentation of 'submodule.recurse'
> in 'Documentation/config/submodule.txt'.

Yes, thanks for the catch.

>> diff --git a/Documentation/config/submodule.txt b/Documentation/config/submodule.txt
>> index ee454f8126..c318b849aa 100644
>> --- a/Documentation/config/submodule.txt
>> +++ b/Documentation/config/submodule.txt
>> @@ -72,6 +72,15 @@ submodule.recurse::
>>   	For these commands a workaround is to temporarily change the
>>   	configuration value by using `git -c submodule.recurse=0`.
>>   
>> +submodule.propagateBranches::
>> +	[EXPERIMENTAL] A boolean that enables branching support with
>> +	submodules. This allows certain commands to accept
>> +	`--recurse-submodules` (`git branch --recurse-submodules` will
>> +	create branches recursively), and certain commands that already
>> +	accept `--recurse-submodules` will now consider branches (`git
>> +	switch --recurse-submodules` will switch to the correct branch
>> +	in all submodules).
>
> Looking at the rest of the patch, this just implements 'branch --recurse-submodules', right ?
> i.e. 'git switch' and 'git checkout' are left alone for
> now, so I think this addition to the doc should only mention 'git
> branch'.

That sounds reasonable. I can move this description into the commit
message instead.

>> diff --git a/advice.h b/advice.h
>> index 601265fd10..a7521d6087 100644
>> --- a/advice.h
>> +
>> +		if (repo_submodule_init(
>> +			    &subrepos[i], r,
>> +			    submodule_entry_list->name_entries[i].path,
>> +			    &super_oid)) {
>> +			die(_("submodule %s: unable to find submodule"),
>> +			    submodules[i].name);
>> +			if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED))
>> +				advise(_("You may try initializing the submodules using 'git checkout %s && git submodule update'"),
>> +				       start_name);
>
> Apart from what Ævar pointed out about advise() being called after die(),
> I'm not sure this is the right advice, because if repo_submodule_init fails
> it means there is no .git/modules/<name> directory corresponding to the submodule's
> Git repository, i.e. the submodule was never cloned. So it's not guaranteed
> that 'git checkout $start_name && git submodule update' would initialize (and clone) it,
> not without '--init'.

After further testing, it seems that --init might be required for
recursive submodules, but as you note later on, it's not needed for the
test case I have created. Using --init is still good advice though, so I
will add that.

>> +	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
>
> Here we loop over all submodules, so branches are created even in
> inactive submodules... this might not be wanted.

Yes, we should ignore inactive submodules. This is a bug.

>> @@ -713,9 +726,16 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>>   	if (create < 0)
>>   		usage_with_options(builtin_branch_usage, options);
>>   
>> +	if (recurse_submodules_explicit && submodule_propagate_branches &&
>> +	    !create)
>> +		die(_("--recurse-submodules can only be used to create branches"));
>>   	if (dry_run && !create)
>>   		die(_("--dry-run can only be used when creating branches"));
>>   
>> +	recurse_submodules =
>> +		(recurse_submodules || recurse_submodules_explicit) &&
>> +		submodule_propagate_branches;
>> +
>
> OK, so we get the new behaviour if either --recurse-submodules was used, or 'submodule.recurse' is true,
> and in both case we also need the new submodule.propagateBranches config set.
>
> Why not adding 'branch.recurseSubmodules' instead, with a higher priority than submodule.recurse ?
> Is it because then it would be mildly confusing for 'git checkout / git switch' to also honor
> a setting named 'branch.*' when they learn the new behaviour ? (I don't think this would be the
> first time that 'git foo' honors 'bar.*', so it might be worth mentioning).

I am avoiding the prefix 'branch.' because that might suggest that the
functionality is centered around the 'git branch' command. I chose the
'submodule.' prefix because what we are feature flagging is an entirely
redesigned UX for _submodules_ that uses branches; we also have work
planned for other commands like push/merge/rebase/reset.

> Also, why do we quietly ignore '--recurse-submodules' if submodule.propagateBranches is unset ?
> Wouldn't it be better to warn the user "hey, if you want this new behaviour you need to
> set that config !" ?

Ah, yes this is an oversight on my part.

> I don't have a strong opinion about the fact that you need to set the config in the first
> place, but I think it should be mentioned in the commit message why you chose to implement
> it that way (meaning, why do we need a config set, instead of adding the config but defaulting it
> to true, so that you get the new behaviour by default, but you still can disable it if you do not
> want it)...

It seems self-evident to me that experimental features should not be
shipped to users by default.

>> +test_expect_success 'should not set up unnecessary tracking of local branches' '
>> +	test_when_finished "cleanup_branches super branch-a" &&
>> +	(
>> +		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)" = ""
>> +	)
>
> don't we have a "config is empty" test helper or something similar ?

Hm, I couldn't find one, but there is test_cmp_config(). That's probably
better than calling test() directly.

>> +test_expect_success 'should not create branch when submodule is not in .git/modules' '
>> +	# The cleanup needs to delete sub2:branch-b in particular because main does not have sub2
>> +	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
>> +		cleanup_branches super-clone branch-a branch-b" &&
>> +	(
>> +		cd super-clone &&
>> +		# This should succeed because super-clone has sub.
>> +		git branch --recurse-submodules branch-a origin/branch-a &&
>> +		# This should fail because super-clone does not have sub2.
>> +		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
>> +		cat >expected <<-EOF &&
>> +		fatal: submodule sub: unable to find submodule
>> +		You may reinitialize the submodules using ${SQ}git checkout origin/branch-b && git submodule update${SQ}
>> +		EOF
>> +		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 &&
>> +		git branch --recurse-submodules branch-b origin/branch-b
>> +	)
>
> Considering what has been pointed out above, I'm not sure why this test passes...
> Unless I'm missing something.

As I understand it, --init is used to set values in .git/config. My best
guess is that 'git submodule update' doesn't use .git/config at all - it
looks for submodules in the index and .gitmodules and clones the
submodules as expected.

I still think that we should promote --init, but I still find this
situation very strange and inconsistent.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 3/4] branch: add --dry-run option to branch
  2021-11-23 23:10   ` Jonathan Tan
@ 2021-11-24  0:52     ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-11-24  0:52 UTC (permalink / raw)
  To: Jonathan Tan; +Cc: git, jonathantanmy, steadmon, emilyshaffer

Jonathan Tan <jonathantanmy@google.com> writes:

> Glen Choo <chooglen@google.com> writes:
>> When running "git branch --recurse-submodules topic"
>
> At this point, this argument has not been introduced yet, so better to
> just say "A future patch will introduce branch creation that recurses
> into submodules, so..."
>
>> +-n::
>> +--dry-run::
>> +	Can only be used when creating a branch. If the branch creation
>> +	would fail, show the relevant error message. If the branch
>> +	creation would succeed, show nothing.
>
> Right now we only plan to use this internally so it's not worth using a
> single character argument for this right now, I think. We can always add
> it later if we find it useful.

For the reasons you specified, I didn't intend to add -n. However, -n is
automatically added by OPT__DRY_RUN, so I thought it was appropriate to
document it.

>> -	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
>> -	    list + edit_description + unset_upstream > 1)
>> +	create = 1 - (!!delete + !!rename + !!copy + !!new_upstream +
>> +		      !!show_current + !!list + !!edit_description +
>> +		      !!unset_upstream);
>> +	if (create < 0)
>>  		usage_with_options(builtin_branch_usage, options);
>
> Hmm...I think it would be clearer just to call it noncreate_options and
> print usage if it is greater than 1. Then whenever you want to check if
> it's create, check `!noncreate_options` instead.

Sounds good.

>> @@ -852,10 +862,20 @@ 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);
>> +		if (dry_run) {
>> +			struct strbuf buf = STRBUF_INIT;
>> +			char *unused_full_ref;
>> +			struct object_id unused_oid;
>>  
>> +			validate_new_branchname(branch_name, &buf, force);
>> +			validate_branch_start(the_repository, start_name, track,
>> +					      &unused_oid, &unused_full_ref);
>> +			strbuf_release(&buf);
>> +			FREE_AND_NULL(unused_full_ref);
>> +			return 0;
>> +		}
>> +		create_branch(the_repository, branch_name, start_name, force, 0,
>> +			      reflog, quiet, track);
>>  	} else
>>  		usage_with_options(builtin_branch_usage, options);
>>  
>
> I don't think we should use separate code paths for the dry run and the
> regular run - could create_branch() take a dry_run parameter instead?
> (If there are too many boolean parameters, it might be time to convert
> some or all to a struct.)

That sounds reasonable, it would be good not to have duplicate code
paths.

> This suggestion would require a reworking of patch 2, which is why I
> didn't comment there. But if we are not going to use this suggestion and
> are going to stick with patch 2, then my comment on it is that it seems
> to be doing too much: I ran "git show --color-moved" on it and saw that
> quite a few lines were newly introduced (not just moved around).

I will do the reworking, but the final result will probably look very
similar to the one in patch 2. Does it look more acceptable with
--color-moved-ws=ignore-all-space? Some text had to be reindented
(because I removed one conditional), but I also replaced some functions
with repo_* versions.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-22 22:32 ` [PATCH 4/4] branch: add --recurse-submodules option for branch creation Glen Choo
  2021-11-23 10:45   ` Ævar Arnfjörð Bjarmason
  2021-11-23 19:41   ` Philippe Blain
@ 2021-11-24  1:31   ` Jonathan Tan
  2021-11-24 18:18     ` Glen Choo
  2 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2021-11-24  1:31 UTC (permalink / raw)
  To: chooglen; +Cc: git, jonathantanmy, steadmon, emilyshaffer

Glen Choo <chooglen@google.com> writes:
> +submodule.propagateBranches::
> +	[EXPERIMENTAL] A boolean that enables branching support with
> +	submodules. This allows certain commands to accept
> +	`--recurse-submodules` (`git branch --recurse-submodules` will
> +	create branches recursively), and certain commands that already
> +	accept `--recurse-submodules` will now consider branches (`git
> +	switch --recurse-submodules` will switch to the correct branch
> +	in all submodules).

After some thought, I think that the way in this patch is the best way
to do it, even if it's unfortunate that a natural-looking "git branch
--recurse-submodules" wouldn't work without
"submodule.propagateBranches" (or whatever we decide to call the
variable).

Right now, as far as I know, "--recurse-submodules" in all Git commands
(and equivalently, the configuration variable "submodule.recurse") does
the same thing but in submodules too. For example, "fetch" fetches in
submodules, "switch" switches in submodules, and so on. It would be
plausible for "branch --recurse-submodules" to also create a branch in
submodules, but if a user just used it as-is, it would be surprising if
subsequent commands like "switch --recurse-submodules" or "-c
submodule.recurse=true switch" didn't use the branches that were just
created.

So OK, this makes sense.

> +static int submodule_create_branch(struct repository *r, const char *name,
> +				   const char *start_oid,
> +				   const char *start_name, int force,
> +				   int reflog, int quiet,
> +				   enum branch_track track, char **err_msg)
> +{
> +	int ret = 0;
> +	struct child_process child = CHILD_PROCESS_INIT;
> +	struct strbuf child_err = STRBUF_INIT;
> +	child.git_cmd = 1;
> +	child.err = -1;
> +
> +	prepare_other_repo_env(&child.env_array, r->gitdir);
> +	strvec_pushl(&child.args, "submodule--helper", "create-branch", NULL);

Before this function is a function that calls "git branch" directly -
couldn't this function do the same? (And then you can make both of them
into one function.) The functionality should be exactly the same except
that one has "--dry-run" and the other doesn't.

> @@ -874,6 +894,12 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>  			FREE_AND_NULL(unused_full_ref);
>  			return 0;
>  		}
> +		if (recurse_submodules) {
> +			create_submodule_branches(the_repository, branch_name,
> +						  start_name, force, reflog,
> +						  quiet, track);
> +			return 0;
> +		}
>  		create_branch(the_repository, branch_name, start_name, force, 0,
>  			      reflog, quiet, track);
>  	} else

create_submodule_branches() here is a bit misleading since it also
creates branches in the_repository. Might be better to write explicitly
check in submodules -> create in main repository -> create in
submodules. Or, if you want to combine all the submodule code in one
function, (check in submodules -> create in submodules) -> create in
main repository.

> +test_expect_success 'setup superproject and submodule' '
> +	git init super &&
> +	test_commit foo &&
> +	git init sub-upstream &&
> +	test_commit -C sub-upstream foo &&
> +	git -C super submodule add ../sub-upstream sub &&
> +	git -C super commit -m "add submodule" &&
> +	git -C super config submodule.propagateBranches true
> +'

If making each test independent is important (which seems like a good
goal to me, although I know that the Git tests are inconsistent on
that), we could make this into a bash function (with test_when_finished)
that gets called in every test. It doesn't violate the t/README request
to put all test code inside test_expect_success assertions (since the
function is still being run inside an assertion).

In the general case, it will make test code slower to run, but if you're
going to have to cleanup branches, I think it's better to just recreate
the repo. In any case, for the general case, I can start a separate
email thread for this discussion.

> +test_expect_success '--recurse-submodules should be ignored if submodule.propagateBranches is false' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		cd super &&
> +		git -c submodule.propagateBranches=false branch --recurse-submodules branch-a &&
> +		git rev-parse branch-a &&
> +		test_must_fail git -C sub rev-parse branch-a
> +	)
> +'

This doesn't sound like the right behavior to me - I think it's fine if
it was the config "submodule.recurse" instead of "--recurse-submodules",
but if the argument is given on CLI, it should be a fatal error.

> +test_expect_success 'should create branch when submodule is in .git/modules but not .gitmodules' '
> +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
> +	(
> +		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 checkout branch-a &&
> +		git branch --recurse-submodules branch-c branch-b &&
> +		git rev-parse branch-c &&
> +		git -C sub rev-parse branch-c &&
> +		git checkout --recurse-submodules branch-c &&
> +		git -C sub2 rev-parse branch-c
> +	)
> +'

Hmm...how is this submodule in .git/modules but not .gitmodules? It
looks like a normal submodule to me.

> +test_expect_success 'should not create branch when submodule is not in .git/modules' '

The title of this test contradicts the title of the test that I quoted
previously.

> +test_expect_success 'should not fail when unable to set up tracking in submodule' '
> +	test_when_finished "cleanup_branches super-clone branch-b" &&
> +	(
> +		cd super-clone &&
> +		git branch --recurse-submodules branch-b origin/branch-b
> +	)
> +'

Is there a warning printed that we can check?

Also, this patch set doesn't discuss the case in which the branch in a
submodule already exists, but it points to the exact commit that we
want. What is the functionality in that case? I would say that the user
should be able to recursively create the branch in this case, but am
open to other opinions. In any case, that case should be tested.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-24  1:31   ` Jonathan Tan
@ 2021-11-24 18:18     ` Glen Choo
  2021-11-29 21:01       ` Jonathan Tan
  0 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-11-24 18:18 UTC (permalink / raw)
  To: Jonathan Tan; +Cc: git, jonathantanmy, steadmon, emilyshaffer


Thanks! The feedback is really useful.

Jonathan Tan <jonathantanmy@google.com> writes:

>> +static int submodule_create_branch(struct repository *r, const char *name,
>> +				   const char *start_oid,
>> +				   const char *start_name, int force,
>> +				   int reflog, int quiet,
>> +				   enum branch_track track, char **err_msg)
>> +{
>> +	int ret = 0;
>> +	struct child_process child = CHILD_PROCESS_INIT;
>> +	struct strbuf child_err = STRBUF_INIT;
>> +	child.git_cmd = 1;
>> +	child.err = -1;
>> +
>> +	prepare_other_repo_env(&child.env_array, r->gitdir);
>> +	strvec_pushl(&child.args, "submodule--helper", "create-branch", NULL);
>
> Before this function is a function that calls "git branch" directly -
> couldn't this function do the same? (And then you can make both of them
> into one function.) The functionality should be exactly the same except
> that one has "--dry-run" and the other doesn't.

I see two somewhat valid interpretations, so I will address both.

If you are suggesting that I should call "git branch" instead of a new
"git submodule--helper create-branch", that unfortunately does not work.
Because of how we've defined the semantics, we want the submodule to
branch off the commit in the superproject tree (which is a bare object
id), but we want to set up tracking based off the ref that the user gave
(evaluating it in the context of the submodule). This is why
submodule--helper.c:module_create_branch() makes two calls like so:

	create_branch(the_repository, "<branch name>", "<object id>", force, 0, reflog, quiet,
		      BRANCH_TRACK_NEVER);
	setup_tracking("<branch name>", "<tracking name>", track, quiet, 0);

On the other hand, you might be suggesting that I should just add
--dry-run to "git submodule--helper create-branch". That way, the
dry-run form of the command is validating the non dry-run form (instead
of using "git branch --dry-run" to validate "git submodule--helper
create-branch"). That's a reasonable suggestion that avoids
bikeshedding around "git branch --dry-run".

>> @@ -874,6 +894,12 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>>  			FREE_AND_NULL(unused_full_ref);
>>  			return 0;
>>  		}
>> +		if (recurse_submodules) {
>> +			create_submodule_branches(the_repository, branch_name,
>> +						  start_name, force, reflog,
>> +						  quiet, track);
>> +			return 0;
>> +		}
>>  		create_branch(the_repository, branch_name, start_name, force, 0,
>>  			      reflog, quiet, track);
>>  	} else
>
> create_submodule_branches() here is a bit misleading since it also
> creates branches in the_repository. Might be better to write explicitly
> check in submodules -> create in main repository -> create in
> submodules. Or, if you want to combine all the submodule code in one
> function, (check in submodules -> create in submodules) -> create in
> main repository.

Philippe had a similar comment, I will rename it.

>> +test_expect_success 'setup superproject and submodule' '
>> +	git init super &&
>> +	test_commit foo &&
>> +	git init sub-upstream &&
>> +	test_commit -C sub-upstream foo &&
>> +	git -C super submodule add ../sub-upstream sub &&
>> +	git -C super commit -m "add submodule" &&
>> +	git -C super config submodule.propagateBranches true
>> +'
>
> If making each test independent is important (which seems like a good
> goal to me, although I know that the Git tests are inconsistent on
> that), we could make this into a bash function (with test_when_finished)
> that gets called in every test. It doesn't violate the t/README request
> to put all test code inside test_expect_success assertions (since the
> function is still being run inside an assertion).

That's an interesting idea and it's more likely to be correct than my
approach. I think it lines up better with testing best practices.
However...

> In the general case, it will make test code slower to run, but if you're
> going to have to cleanup branches, I think it's better to just recreate
> the repo. In any case, for the general case, I can start a separate
> email thread for this discussion.

I'm concerned about the same thing and I suspect that recreating the
repo won't be kindly received by some reviewers, even if they might be
able to stomach cleanup_branches(). I think your suggestion is a better
long-term direction, but I'd like to see discussion on the general case
before changing the tests. A separate thread sounds good.

>> +test_expect_success '--recurse-submodules should be ignored if submodule.propagateBranches is false' '
>> +	test_when_finished "cleanup_branches super branch-a" &&
>> +	(
>> +		cd super &&
>> +		git -c submodule.propagateBranches=false branch --recurse-submodules branch-a &&
>> +		git rev-parse branch-a &&
>> +		test_must_fail git -C sub rev-parse branch-a
>> +	)
>> +'
>
> This doesn't sound like the right behavior to me - I think it's fine if
> it was the config "submodule.recurse" instead of "--recurse-submodules",
> but if the argument is given on CLI, it should be a fatal error.

Philippe mentioned the same thing, which sounds right to me.

>> +test_expect_success 'should create branch when submodule is in .git/modules but not .gitmodules' '
>> +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
>> +	(
>> +		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 checkout branch-a &&
>> +		git branch --recurse-submodules branch-c branch-b &&
>> +		git rev-parse branch-c &&
>> +		git -C sub rev-parse branch-c &&
>> +		git checkout --recurse-submodules branch-c &&
>> +		git -C sub2 rev-parse branch-c
>> +	)
>> +'
>
> Hmm...how is this submodule in .git/modules but not .gitmodules? It
> looks like a normal submodule to me.

The test title is probably too terse - the submodule is not in the
working tree's .gitmodules, but it is in branch-b's .gitmodules. I'll
reword the title.

>> +test_expect_success 'should not create branch when submodule is not in .git/modules' '
>
> The title of this test contradicts the title of the test that I quoted
> previously.

I'm not sure how this is a contradiction, from before..

  should create branch when submodule is in .git/modules but not
  [the working tree's] .gitmodules

meaning "we should create the branch if we can find the submodule in
.git/modules", i.e. the implication is: presence in .git/modules => can
create branch.

Whereas

  should not create branch when submodule is not in .git/modules

meaning "we should not create the branch if we cannot find the submodule
in .git/modules", i.e. the implication is: absence in .git/modules =>
cannot create branch.

Taken together, they assert that presence in .git/modules is a necessary
condition for the subodule branch to be created.

>> +test_expect_success 'should not fail when unable to set up tracking in submodule' '
>> +	test_when_finished "cleanup_branches super-clone branch-b" &&
>> +	(
>> +		cd super-clone &&
>> +		git branch --recurse-submodules branch-b origin/branch-b
>> +	)
>> +'
>
> Is there a warning printed that we can check?

"git branch" does not warn if tracking is not set up when it is not
explicitly required, so this does not warn. However, I can imagine that
if the superproject branch has tracking set up, a user might expect that
all submodules would also have tracking set up, and thus a warning might
be useful. I don't think it will be _that_ useful for most users, but at
least some users would probably appreciate it.

For slightly unrelated reasons, I tried to get the tracking info of a
newly created branch and it is tedious. For this reason and the fact
that I'm not sure if the benefit is that great, I'm tempted *not* to add
the warning, but perhaps you feel more strongly than I do?

> Also, this patch set doesn't discuss the case in which the branch in a
> submodule already exists, but it points to the exact commit that we
> want. What is the functionality in that case?

The behavior is identical to the general case where the branch already
exists - branch validation (git branch --dry-run) fails.

> I would say that the user should be able to recursively create the
> branch in this case, but am open to other opinions.

I'm inclined to disagree. To illustrate this in the real world, say a
user wants to create a 'new-feature' branch recursively, but there is
already a 'new-feature in a submodule. Here are two possible sets of
events that could lead to this situation:

1) the 'new-feature' branch was intended for the same work as the new
   branch but the user just happened to create the 'new-feature'
   subomdule branch first
2) the existing 'new-feature' branch doesn't actually contain the same
   work (maybe it's an overloaded term, or maybe the user used a branch
   to bookmark a commit before starting work on it)

What would happen if we allowed the branch to be created? In case 1,
the user would be more than happy (because we read their mind! woohoo!)
But in case 2, the user might not realize that their bookmark is getting
clobbered by the new recursive branch - they might try revisiting their
bookmark only to realize that they've accidentally committed on top of
it.

I think the incidence of case 2 is far lower than case 1's, but I think
it's reasonable to be defensive by default. In any case, we can change
the default after more user testing.

> In any case, that case should be tested.

Testing this case would make the intended behavior clearer, so I will
add this test.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH 4/4] branch: add --recurse-submodules option for branch creation
  2021-11-24 18:18     ` Glen Choo
@ 2021-11-29 21:01       ` Jonathan Tan
  0 siblings, 0 replies; 110+ messages in thread
From: Jonathan Tan @ 2021-11-29 21:01 UTC (permalink / raw)
  To: chooglen; +Cc: jonathantanmy, git, steadmon, emilyshaffer

Glen Choo <chooglen@google.com> writes:
> > Before this function is a function that calls "git branch" directly -
> > couldn't this function do the same? (And then you can make both of them
> > into one function.) The functionality should be exactly the same except
> > that one has "--dry-run" and the other doesn't.
> 
> I see two somewhat valid interpretations, so I will address both.
> 
> If you are suggesting that I should call "git branch" instead of a new
> "git submodule--helper create-branch", that unfortunately does not work.
> Because of how we've defined the semantics, we want the submodule to
> branch off the commit in the superproject tree (which is a bare object
> id), but we want to set up tracking based off the ref that the user gave
> (evaluating it in the context of the submodule). This is why
> submodule--helper.c:module_create_branch() makes two calls like so:
> 
> 	create_branch(the_repository, "<branch name>", "<object id>", force, 0, reflog, quiet,
> 		      BRANCH_TRACK_NEVER);
> 	setup_tracking("<branch name>", "<tracking name>", track, quiet, 0);

Ah, I meant this interpretation. In that case, could we do it like this:

 - Add a dry_run parameter to submodule_create_branch(). It should stay
   exactly the same (except passing dry_run), so that the same code path
   is executed with and without dry_run. Delete
   submodule_validate_branchname(). (This makes sense conceptually
   because we're validating that we can later create the branches with
   the exact same parameters.)

 - When actually creating the branches, call submodule_create_branch(),
   then another command to set up tracking. I think it makes more sense
   if this is done directly through a Git command. If you want to use
   setup_tracking() through submodule--helper, I think that needs an
   explanation as to why a Git command wouldn't work.

> On the other hand, you might be suggesting that I should just add
> --dry-run to "git submodule--helper create-branch". That way, the
> dry-run form of the command is validating the non dry-run form (instead
> of using "git branch --dry-run" to validate "git submodule--helper
> create-branch"). That's a reasonable suggestion that avoids
> bikeshedding around "git branch --dry-run".

I didn't mean this interpretation, but this idea is reasonable if we
needed such functionality.

> >> +test_expect_success 'should create branch when submodule is in .git/modules but not .gitmodules' '
> >> +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
> >> +	(
> >> +		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 checkout branch-a &&
> >> +		git branch --recurse-submodules branch-c branch-b &&
> >> +		git rev-parse branch-c &&
> >> +		git -C sub rev-parse branch-c &&
> >> +		git checkout --recurse-submodules branch-c &&
> >> +		git -C sub2 rev-parse branch-c
> >> +	)
> >> +'
> >
> > Hmm...how is this submodule in .git/modules but not .gitmodules? It
> > looks like a normal submodule to me.
> 
> The test title is probably too terse - the submodule is not in the
> working tree's .gitmodules, but it is in branch-b's .gitmodules. I'll
> reword the title.

Ah, I see. I think just delete the "is in .git/modules" part, so
something like "should create branch when submodule is not in HEAD's
.gitmodules".

> >> +test_expect_success 'should not create branch when submodule is not in .git/modules' '
> >
> > The title of this test contradicts the title of the test that I quoted
> > previously.
> 
> I'm not sure how this is a contradiction, from before..

[snip]

Ah...yes you're right.

> >> +test_expect_success 'should not fail when unable to set up tracking in submodule' '
> >> +	test_when_finished "cleanup_branches super-clone branch-b" &&
> >> +	(
> >> +		cd super-clone &&
> >> +		git branch --recurse-submodules branch-b origin/branch-b
> >> +	)
> >> +'
> >
> > Is there a warning printed that we can check?
> 
> "git branch" does not warn if tracking is not set up when it is not
> explicitly required, so this does not warn. However, I can imagine that
> if the superproject branch has tracking set up, a user might expect that
> all submodules would also have tracking set up, and thus a warning might
> be useful. I don't think it will be _that_ useful for most users, but at
> least some users would probably appreciate it.
> 
> For slightly unrelated reasons, I tried to get the tracking info of a
> newly created branch and it is tedious. For this reason and the fact
> that I'm not sure if the benefit is that great, I'm tempted *not* to add
> the warning, but perhaps you feel more strongly than I do?

In that case, maybe explain why tracking cannot be set up and verify in
the test that no tracking was set up.

> > Also, this patch set doesn't discuss the case in which the branch in a
> > submodule already exists, but it points to the exact commit that we
> > want. What is the functionality in that case?
> 
> The behavior is identical to the general case where the branch already
> exists - branch validation (git branch --dry-run) fails.
> 
> > I would say that the user should be able to recursively create the
> > branch in this case, but am open to other opinions.
> 
> I'm inclined to disagree. To illustrate this in the real world, say a
> user wants to create a 'new-feature' branch recursively, but there is
> already a 'new-feature in a submodule. Here are two possible sets of
> events that could lead to this situation:
> 
> 1) the 'new-feature' branch was intended for the same work as the new
>    branch but the user just happened to create the 'new-feature'
>    subomdule branch first
> 2) the existing 'new-feature' branch doesn't actually contain the same
>    work (maybe it's an overloaded term, or maybe the user used a branch
>    to bookmark a commit before starting work on it)
> 
> What would happen if we allowed the branch to be created? In case 1,
> the user would be more than happy (because we read their mind! woohoo!)
> But in case 2, the user might not realize that their bookmark is getting
> clobbered by the new recursive branch - they might try revisiting their
> bookmark only to realize that they've accidentally committed on top of
> it.
> 
> I think the incidence of case 2 is far lower than case 1's, but I think
> it's reasonable to be defensive by default. In any case, we can change
> the default after more user testing.

OK, this makes sense.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v2 0/3] implement branch --recurse-submodules
  2021-11-22 22:32 [PATCH 0/4] implement branch --recurse-submodules Glen Choo
                   ` (3 preceding siblings ...)
  2021-11-22 22:32 ` [PATCH 4/4] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-06 21:55 ` Glen Choo
  2021-12-06 21:55   ` [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
                     ` (3 more replies)
  4 siblings, 4 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-06 21:55 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Submodule branching RFC:
https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/

Original Submodule UX RFC/Discussion:
https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/

Contributor Summit submodules Notes:
https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/

Submodule UX overhaul updates:
https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update

This series implements branch --recurse-submodules as laid out in the
Submodule branching RFC (linked above). If there are concerns about the
UX/behavior, I would appreciate feedback on the RFC thread as well :)

Thanks for the feedback, everyone. I really really appreciate it.

The biggest difference in V2 is that I took Jonathan's advice to remove
"git branch --dry-run" in favor of adding "--dry-run" to "git
submodule--helper create-branch" instead [1]. The benefit of having "git
branch --dry-run" is pretty small, and we'd have to explain to users why
"--dry-run" doesn't work in more situations [2].

Unfortunately patch 3 (formerly patch 4) is now bigger than I would
prefer. This is due to the combined effect of removing "--dry-run" and
squashing the former patch 1. I'd appreciate any feedback on how I can
structure things differently :)

Changes since v1:
* Move the functionality of "git branch --dry-run" into "git
  submodule-helper create-branch --dry-run"
* Add more fields to the submodules_of_tree() struct to reduce the
  number of allocations made by the caller [3]. Move this functionality
  to patch 3 (formerly patch 4) and drop patch 1.
* Make submodules_of_tree() ignore inactive submodules [4]
* Structure the output of the submodules a bit better by adding prefixes
  to the child process' output (instead of inconsistently indenting the
  output).
** I wasn't able to find a good way to interleave stdout/stderr
   correctly, so a less-than-desirable workaround was to route the child
   process output to stdout/stderr depending on the exit code.
** Eventually, I would like to structure the output of submodules in a
   report, as Ævar suggested [5]. But at this stage, I think that it's
   better to spend time getting user feedback on the submodules
   branching UX and it'll be easier to standardize the output when we've
   implemented more of the UX :)

[1] https://lore.kernel.org/git/20211129210140.937875-1-jonathantanmy@google.com
[2] https://lore.kernel.org/git/211123.86zgpvup6m.gmgdl@evledraar.gmail.com
[3] https://lore.kernel.org/git/211123.86r1b7uoil.gmgdl@evledraar.gmail.com
[4] https://lore.kernel.org/git/3ad3941c-de18-41bf-2e44-4238ae868d79@gmail.com
[5] https://lore.kernel.org/git/211123.86v90juovj.gmgdl@evledraar.gmail.com

Glen Choo (3):
  branch: move --set-upstream-to behavior to setup_tracking()
  builtin/branch: clean up action-picking logic in cmd_branch()
  branch: add --recurse-submodules option for branch creation

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |   8 +
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 322 +++++++++++++++++++++--------
 branch.h                           |  44 +++-
 builtin/branch.c                   |  66 ++++--
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  35 ++++
 submodule-config.h                 |  35 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3200-branch.sh                  |  17 ++
 t/t3207-branch-submodule.sh        | 284 +++++++++++++++++++++++++
 15 files changed, 758 insertions(+), 113 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

Range-diff against v1:
1:  1551dd683f < -:  ---------- submodule-config: add submodules_of_tree() helper
2:  a4984f6eef ! 1:  cc212dcd39 branch: refactor out branch validation from create_branch()
    @@ Metadata
     Author: Glen Choo <chooglen@google.com>
     
      ## Commit message ##
    -    branch: refactor out branch validation from create_branch()
    +    branch: move --set-upstream-to behavior to setup_tracking()
     
    -    In a subsequent commit, we would like to be able to validate whether or
    -    not a branch name is valid before we create it (--dry-run). This is
    -    useful for `git branch --recurse-submodules topic` because it allows Git
    -    to determine if the branch 'topic' can be created in all submodules
    -    without creating the branch 'topic'.
    +    This refactor is motivated by a desire to add a "dry_run" parameter to
    +    create_branch() that will validate whether or not a branch can be
    +    created without actually creating it - this behavior be used in a
    +    subsequent commit that adds `git branch --recurse-submodules topic`.
     
    -    A good starting point would be to refactor out the start point
    -    validation and dwim logic in create_branch() in a
    -    validate_branch_start() helper function. Once we do so, it becomes
    -    clear that create_branch() is more complex than it needs to be -
    -    create_branch() is also used to set tracking information when performing
    -    `git branch --set-upstream-to`. This made more sense when
    -    (the now unsupported) --set-upstream was first introduced in
    -    4fc5006676 (Add branch --set-upstream, 2010-01-18), because
    -    it would sometimes create a branch and sometimes update tracking
    -    information without creating a branch.
    +    Adding "dry_run" is not obvious because create_branch() is also used to
    +    set tracking information without creating a branch, i.e. when using
    +    --set-upstream-to. This appears to be a leftover from 4fc5006676 (Add
    +    branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
    +    create a branch and sometimes update tracking information without
    +    creating a branch. However, we no longer support --set-upstream, so it
    +    makes more sense to set tracking information with another function, like
    +    setup_tracking(), and use create_branch() only to create branches. When
    +    this is done, it will be trivial to add "dry_run".
     
    -    Refactor out the branch validation and dwim logic from create_branch()
    -    into validate_branch_start(), make it so that create_branch() always
    -    tries to create a branch, and replace the now-incorrect create_branch()
    -    call with setup_tracking(). Since there were none, add tests for
    -    creating a branch with `--force`.
    +    Do this refactor by moving the branch validation and dwim logic from
    +    create_branch() into a new function, validate_branch_start(), and call
    +    it from setup_tracking(). Now that setup_tracking() can perform dwim and
    +    tracking setup without creating a branch, use it in `git branch
    +    --set-upstream-to` and remove unnecessary behavior from create_branch().
    +
    +    Since there were none, add tests for creating a branch with `--force`.
     
         Signed-off-by: Glen Choo <chooglen@google.com>
     
    @@ branch.c: N_("\n"
     -		   const char *name, const char *start_name,
     -		   int force, int clobber_head_ok, int reflog,
     -		   int quiet, enum branch_track track)
    ++/**
    ++ * Validates whether a ref is a valid starting point for a branch, where:
    ++ *
    ++ *   - r is the repository to validate the branch for
    ++ *
    ++ *   - start_name is the ref that we would like to test. This is
    ++ *     expanded with DWIM and assigned to real_ref.
    ++ *
    ++ *   - track is the tracking mode of the new branch. If tracking is
    ++ *     explicitly requested, start_name must be a branch (because
    ++ *     otherwise start_name cannot be tracked)
    ++ *
    ++ *   - oid is an out parameter containing the object_id of start_name
    ++ *
    ++ *   - real_ref is an out parameter containing the full, 'real' form of
    ++ *     start_name e.g. refs/heads/main instead of main
    ++ *
    ++ */
     +static void validate_branch_start(struct repository *r, const char *start_name,
     +				  enum branch_track track,
    -+				  struct object_id *oid, char **full_ref)
    ++				  struct object_id *oid, char **real_ref)
      {
      	struct commit *commit;
     -	struct object_id oid;
    @@ branch.c: void create_branch(struct repository *r,
      	}
      
     -	switch (dwim_ref(start_name, strlen(start_name), &oid, &real_ref, 0)) {
    -+	switch (repo_dwim_ref(r, start_name, strlen(start_name), oid, full_ref,
    ++	switch (repo_dwim_ref(r, start_name, strlen(start_name), oid, real_ref,
     +			      0)) {
      	case 0:
      		/* Not branching from any existing branch */
    @@ branch.c: void create_branch(struct repository *r,
      		/* Unique completion -- good, only if it is a real branch */
     -		if (!starts_with(real_ref, "refs/heads/") &&
     -		    validate_remote_tracking_branch(real_ref)) {
    -+		if (!starts_with(*full_ref, "refs/heads/") &&
    -+		    validate_remote_tracking_branch(*full_ref)) {
    ++		if (!starts_with(*real_ref, "refs/heads/") &&
    ++		    validate_remote_tracking_branch(*real_ref)) {
      			if (explicit_tracking)
      				die(_(upstream_not_branch), start_name);
      			else
     -				FREE_AND_NULL(real_ref);
    -+				FREE_AND_NULL(*full_ref);
    ++				FREE_AND_NULL(*real_ref);
      		}
      		break;
      	default:
3:  cbcbc4f49e < -:  ---------- branch: add --dry-run option to branch
-:  ---------- > 2:  320749cc82 builtin/branch: clean up action-picking logic in cmd_branch()
4:  416a114fa9 ! 3:  c0441c6691 branch: add --recurse-submodules option for branch creation
    @@ Metadata
      ## Commit message ##
         branch: add --recurse-submodules option for branch creation
     
    -    Teach cmd_branch to accept the --recurse-submodules option when creating
    -    branches so that `git branch --recurse-submodules topic` will create the
    -    "topic" branch in the superproject and all submodules. Guard this (and
    -    future submodule branching) behavior behind a new configuration value
    -    'submodule.propagateBranches'.
    +    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 semantics e.g. `git
    +    checkout` does not recursively checkout the expected branches created by
    +    `git branch` yet. To ensure that the correct set of semantics is used,
    +    this commit introduces a new configuration value,
    +    `submodule.propagateBranches`, which enables submodule branching when
    +    true (defaults to false).
    +
    +    This commit includes changes that allow Git to work with submodules
    +    that are in trees (and not just the index):
    +
    +    * 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
    +
    +    Other changes
    +
    +    * add a "dry_run" parameter to create_branch() in order to support
    +      `git submodule--helper create-branch --dry-run`
     
         Signed-off-by: Glen Choo <chooglen@google.com>
     
    @@ Documentation/config/submodule.txt: submodule.recurse::
      	configuration value by using `git -c submodule.recurse=0`.
      
     +submodule.propagateBranches::
    -+	[EXPERIMENTAL] A boolean that enables branching support with
    -+	submodules. This allows certain commands to accept
    -+	`--recurse-submodules` (`git branch --recurse-submodules` will
    -+	create branches recursively), and certain commands that already
    -+	accept `--recurse-submodules` will now consider branches (`git
    -+	switch --recurse-submodules` will switch to the correct branch
    -+	in all submodules).
    ++	[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.
     +
      submodule.fetchJobs::
      	Specifies how many submodules are fetched/cloned at the same time.
    @@ branch.c
      
      struct tracking {
      	struct refspec_item spec;
    +@@ branch.c: void setup_tracking(const char *new_ref, const char *orig_ref,
    + 
    + void create_branch(struct repository *r, const char *name,
    + 		   const char *start_name, int force, int clobber_head_ok,
    +-		   int reflog, int quiet, enum branch_track track)
    ++		   int reflog, int quiet, enum branch_track track, int dry_run)
    + {
    + 	struct object_id oid;
    + 	char *real_ref;
     @@ branch.c: void create_branch(struct repository *r, const char *name,
    + 	}
    + 
    + 	validate_branch_start(r, start_name, track, &oid, &real_ref);
    ++	if (dry_run)
    ++		goto cleanup;
    + 
    + 	if (reflog)
    + 		log_all_ref_updates = LOG_REFS_NORMAL;
    +@@ branch.c: void create_branch(struct repository *r, const char *name,
    + 	if (real_ref && track)
    + 		setup_tracking(ref.buf + 11, real_ref, track, quiet, 0);
    + 
    ++cleanup:
    + 	strbuf_release(&ref);
      	free(real_ref);
      }
      
    -+static int submodule_validate_branchname(struct repository *r, const char *name,
    -+					 const char *start_name, int force,
    -+					 int quiet, char **err_msg)
    -+{
    -+	int ret = 0;
    -+	struct child_process child = CHILD_PROCESS_INIT;
    -+	struct strbuf child_err = STRBUF_INIT;
    -+	child.git_cmd = 1;
    -+	child.err = -1;
    -+
    -+	prepare_other_repo_env(&child.env_array, r->gitdir);
    -+	strvec_pushl(&child.args, "branch", "--dry-run", NULL);
    -+	if (force)
    -+		strvec_push(&child.args, "--force");
    -+	if (quiet)
    -+		strvec_push(&child.args, "--quiet");
    -+	strvec_pushl(&child.args, name, start_name, NULL);
    -+
    -+	if ((ret = start_command(&child)))
    -+		return ret;
    -+	ret = finish_command(&child);
    -+	strbuf_read(&child_err, child.err, 0);
    -+	*err_msg = strbuf_detach(&child_err, NULL);
    -+	return ret;
    -+}
    -+
    -+static int submodule_create_branch(struct repository *r, const char *name,
    -+				   const char *start_oid,
    ++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, char **err_msg)
    ++				   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);
     +	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)
    @@ branch.c: void create_branch(struct repository *r, const char *name,
     +		return ret;
     +	ret = finish_command(&child);
     +	strbuf_read(&child_err, child.err, 0);
    -+	*err_msg = strbuf_detach(&child_err, NULL);
    ++	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_submodule_branches(struct repository *r, const char *name,
    -+			       const char *start_name, int force, int reflog,
    -+			       int quiet, enum branch_track track)
    ++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 repository *subrepos;
    -+	struct submodule *submodules;
     +	struct object_id super_oid;
    -+	struct submodule_entry_list *submodule_entry_list;
    -+	char *err_msg = NULL;
    -+
    -+	validate_branch_start(r, start_name, track, &super_oid, &branch_point);
    -+
    -+	submodule_entry_list = submodules_of_tree(r, &super_oid);
    -+	CALLOC_ARRAY(subrepos, submodule_entry_list->entry_nr);
    -+	CALLOC_ARRAY(submodules, submodule_entry_list->entry_nr);
    -+
    -+	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
    -+		submodules[i] = *submodule_from_path(
    -+			r, &super_oid,
    -+			submodule_entry_list->name_entries[i].path);
    -+
    -+		if (repo_submodule_init(
    -+			    &subrepos[i], r,
    -+			    submodule_entry_list->name_entries[i].path,
    -+			    &super_oid)) {
    -+			die(_("submodule %s: unable to find submodule"),
    -+			    submodules[i].name);
    ++	struct submodule_entry_list submodule_entry_list;
    ++
    ++	/* Perform dwim on start_name to get super_oid and branch_point. */
    ++	validate_branch_start(r, start_name, BRANCH_TRACK_NEVER, &super_oid,
    ++			      &branch_point);
    ++
    ++	/*
    ++	 * 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 initializing the submodules using 'git checkout %s && git submodule update'"),
    ++				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_validate_branchname(
    -+			    &subrepos[i], name,
    -+			    oid_to_hex(
    -+				    &submodule_entry_list->name_entries[i].oid),
    -+			    force, quiet, &err_msg))
    -+			die(_("submodule %s: could not create branch '%s'\n\t%s"),
    -+			    submodules[i].name, name, err_msg);
    ++		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,
    -+		      track);
    -+
    -+	for (i = 0; i < submodule_entry_list->entry_nr; i++) {
    -+		printf_ln(_("submodule %s: creating branch '%s'"),
    -+			  submodules[i].name, name);
    ++		      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, 0);
    ++
    ++	for (i = 0; i < submodule_entry_list.entry_nr; i++) {
     +		if (submodule_create_branch(
    -+			    &subrepos[i], name,
    -+			    oid_to_hex(
    -+				    &submodule_entry_list->name_entries[i].oid),
    -+			    branch_point, force, reflog, quiet, track,
    -+			    &err_msg))
    -+			die(_("submodule %s: could not create branch '%s'\n\t%s"),
    -+			    submodules[i].name, name, err_msg);
    -+
    -+		repo_clear(&subrepos[i]);
    ++			    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);
     +	}
     +}
     +
    @@ branch.c: void create_branch(struct repository *r, const char *name,
      	unlink(git_path_merge_head(r));
     
      ## branch.h ##
    -@@ branch.h: void create_branch(struct repository *r,
    - 		   int force, int clobber_head_ok,
    - 		   int reflog, int quiet, enum branch_track track);
    +@@ branch.h: void setup_tracking(const char *new_ref, const char *orig_ref,
    +  *   - track causes the new branch to be configured to merge the remote branch
    +  *     that start_name is a tracking branch for (if any).
    +  *
    ++ *   - dry_run causes the branch to be validated but not created.
    ++ *
    +  */
    +-void create_branch(struct repository *r,
    +-		   const char *name, const char *start_name,
    +-		   int force, int clobber_head_ok,
    +-		   int reflog, int quiet, enum branch_track track);
    ++void create_branch(struct repository *r, const char *name,
    ++		   const char *start_name, int force, int clobber_head_ok,
    ++		   int reflog, int quiet, enum branch_track track, int dry_run);
      
     +/*
    -+ * Creates a new branch in repository and its submodules.
    ++ * 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_submodule_branches(struct repository *r, const char *name,
    -+			       const char *start_name, int force, int reflog,
    -+			       int quiet, enum branch_track track);
    ++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.
    @@ builtin/branch.c: static int git_branch_config(const char *var, const char *valu
      }
      
     @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix)
    - 	int delete = 0, rename = 0, copy = 0, force = 0, list = 0, create = 0,
      	    unset_upstream = 0, show_current = 0, edit_description = 0;
    + 	int noncreate_actions = 0;
      	/* possible options */
    --	int reflog = 0, quiet = 0, dry_run = 0, icase = 0;
    -+	int reflog = 0, quiet = 0, dry_run = 0, icase = 0,
    -+	    recurse_submodules_explicit = 0;
    +-	int reflog = 0, quiet = 0, icase = 0;
    ++	int reflog = 0, quiet = 0, icase = 0, recurse_submodules_explicit = 0;
      	const char *new_upstream = NULL;
      	enum branch_track track;
      	struct ref_filter filter;
    @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix
      		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__DRY_RUN(&dry_run, N_("show whether the branch would be created")),
      		OPT_END(),
    + 	};
     @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix)
    - 	if (create < 0)
    + 	if (noncreate_actions > 1)
      		usage_with_options(builtin_branch_usage, options);
      
    -+	if (recurse_submodules_explicit && submodule_propagate_branches &&
    -+	    !create)
    -+		die(_("--recurse-submodules can only be used to create branches"));
    - 	if (dry_run && !create)
    - 		die(_("--dry-run can only be used when creating branches"));
    - 
    ++	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;
    @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix
      		filter.abbrev = DEFAULT_ABBREV;
      	filter.ignore_case = icase;
     @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix)
    - 			FREE_AND_NULL(unused_full_ref);
    - 			return 0;
    - 		}
    + 		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>?"));
    +@@ builtin/branch.c: 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);
    +-
     +		if (recurse_submodules) {
    -+			create_submodule_branches(the_repository, branch_name,
    -+						  start_name, force, reflog,
    -+						  quiet, track);
    ++			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);
    ++		create_branch(the_repository, branch_name, start_name, force, 0,
    ++			      reflog, quiet, track, 0);
      	} else
    + 		usage_with_options(builtin_branch_usage, options);
    + 
    +
    + ## builtin/checkout.c ##
    +@@ builtin/checkout.c: static void update_refs_for_switch(const struct checkout_opts *opts,
    + 				      opts->new_branch_force ? 1 : 0,
    + 				      opts->new_branch_log,
    + 				      opts->quiet,
    +-				      opts->track);
    ++				      opts->track,
    ++				      0);
    + 		new_branch_info->name = opts->new_branch;
    + 		setup_branch_path(new_branch_info);
    + 	}
     
      ## builtin/submodule--helper.c ##
     @@
    @@ builtin/submodule--helper.c: static int module_set_branch(int argc, const char *
     +static int module_create_branch(int argc, const char **argv, const char *prefix)
     +{
     +	enum branch_track track;
    -+	int quiet = 0, force = 0, reflog = 0;
    ++	int quiet = 0, force = 0, reflog = 0, dry_run = 0;
     +
     +	struct option options[] = {
     +		OPT__QUIET(&quiet, N_("print only error messages")),
    @@ builtin/submodule--helper.c: static int module_set_branch(int argc, const char *
     +		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] <name> <start_oid> <start_name>"),
    ++		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);
     +
    -+	create_branch(the_repository, argv[0], argv[1], force, 0, reflog, quiet,
    -+		      BRANCH_TRACK_NEVER);
    -+	setup_tracking(argv[0], argv[2], track, quiet, 0);
    ++	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 {
    @@ builtin/submodule--helper.c: static struct cmd_struct commands[] = {
      
      int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
     
    + ## submodule-config.c ##
    +@@
    + #include "strbuf.h"
    + #include "object-store.h"
    + #include "parse-options.h"
    ++#include "tree-walk.h"
    + 
    + /*
    +  * submodule cache lookup structure
    +@@ submodule-config.c: const struct submodule *submodule_from_path(struct repository *r,
    + 	return config_from(r->submodule_cache, treeish_name, path, lookup_path);
    + }
    + 
    ++void submodules_of_tree(struct repository *r,
    ++			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;
    ++
    ++	name_entry = xmalloc(sizeof(*name_entry));
    ++
    ++	CALLOC_ARRAY(out->entries, 0);
    ++	out->entry_nr = 0;
    ++	out->entry_alloc = 0;
    ++
    ++	fill_tree_descriptor(r, &tree, treeish_name);
    ++	while (tree_entry(&tree, name_entry)) {
    ++		if (!S_ISGITLINK(name_entry->mode) || !is_tree_submodule_active(r, treeish_name, name_entry->path)) {
    ++			continue;
    ++		}
    ++
    ++		st_entry = xmalloc(sizeof(*st_entry));
    ++		st_entry->name_entry = name_entry;
    ++		st_entry->submodule =
    ++			submodule_from_path(r, treeish_name, name_entry->path);
    ++		st_entry->repo = xmalloc(sizeof(*st_entry->repo));
    ++		if (repo_submodule_init(st_entry->repo, r, name_entry->path,
    ++					treeish_name))
    ++			FREE_AND_NULL(st_entry->repo);
    ++
    ++		ALLOC_GROW(out->entries, out->entry_nr + 1, out->entry_alloc);
    ++		out->entries[out->entry_nr++] = *st_entry;
    ++	}
    ++}
    ++
    + void submodule_free(struct repository *r)
    + {
    + 	if (r->submodule_cache)
    +
    + ## submodule-config.h ##
    +@@
    + #include "hashmap.h"
    + #include "submodule.h"
    + #include "strbuf.h"
    ++#include "tree-walk.h"
    + 
    + /**
    +  * The submodule config cache API allows to read submodule
    +@@ submodule-config.h: 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. This only reads
    ++ * one level of the tree, so it will not return nested submodules;
    ++ * callers that require nested submodules are expected to handle the
    ++ * recursion themselves.
    ++ */
    ++void submodules_of_tree(struct repository *r,
    ++			const struct object_id *treeish_name,
    ++			struct submodule_entry_list *ret);
    + #endif /* SUBMODULE_CONFIG_H */
    +
    + ## submodule.c ##
    +@@ submodule.c: 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;
    +@@ submodule.c: 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)
    +@@ submodule.c: 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;
    +
    + ## submodule.h ##
    +@@ submodule.h: 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
    +
      ## t/t3207-branch-submodule.sh (new) ##
     @@
     +#!/bin/sh
    @@ t/t3207-branch-submodule.sh (new)
     +test_expect_success 'setup superproject and submodule' '
     +	git init super &&
     +	test_commit foo &&
    ++	git init sub-sub-upstream &&
    ++	test_commit -C sub-sub-upstream foo &&
     +	git init sub-upstream &&
    -+	test_commit -C sub-upstream foo &&
    -+	git -C super submodule add ../sub-upstream sub &&
    ++	git -C sub-upstream submodule add "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
    ++	git -C sub-upstream commit -m "add submodule" &&
    ++	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
     +	git -C super commit -m "add submodule" &&
    -+	git -C super config submodule.propagateBranches true
    ++	git -C super config submodule.propagateBranches true &&
    ++	git -C super/sub submodule update --init
     +'
     +
    -+cleanup_branches() {
    ++CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
    ++
    ++cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
    ++	#!/bin/sh
    ++
     +	super_dir="$1"
     +	shift
     +	(
    @@ t/t3207-branch-submodule.sh (new)
     +		git checkout main &&
     +		for branch_name in "$@"; do
     +			git branch -D "$branch_name"
    -+			git submodule foreach "(git checkout main && git branch -D $branch_name) || true"
    ++			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
     +		done
     +	)
    ++EOF
    ++chmod +x "$CLEANUP_SCRIPT_PATH"
    ++
    ++cleanup_branches() {
    ++	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
     +} >/dev/null 2>/dev/null
     +
     +# Test the argument parsing
    @@ t/t3207-branch-submodule.sh (new)
     +	(
     +		cd super &&
     +		git branch --recurse-submodules branch-a &&
    -+		git rev-parse --abbrev-ref branch-a &&
    -+		git -C sub rev-parse --abbrev-ref branch-a
    ++		git rev-parse branch-a &&
    ++		git -C sub rev-parse branch-a &&
    ++		git -C sub/sub-sub rev-parse branch-a
     +	)
     +'
     +
    -+test_expect_success '--recurse-submodules should be ignored if submodule.propagateBranches is false' '
    ++test_expect_success '--recurse-submodules should die if submodule.propagateBranches is false' '
     +	test_when_finished "cleanup_branches super branch-a" &&
     +	(
     +		cd super &&
    -+		git -c submodule.propagateBranches=false branch --recurse-submodules branch-a &&
    -+		git rev-parse branch-a &&
    -+		test_must_fail git -C sub rev-parse branch-a
    ++		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
     +	)
     +'
     +
    @@ t/t3207-branch-submodule.sh (new)
     +		test_must_fail git branch --recurse-submodules branch-a 2>actual &&
     +		test_must_fail git rev-parse branch-a &&
     +
    -+		cat >expected <<EOF &&
    -+fatal: submodule sub: could not create branch ${SQ}branch-a${SQ}
    -+	fatal: A branch named ${SQ}branch-a${SQ} already exists.
    -+
    -+EOF
    ++		cat >expected <<-EOF &&
    ++		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
    ++		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
    ++		EOF
     +		test_cmp expected actual
     +	)
     +'
    @@ t/t3207-branch-submodule.sh (new)
     +	)
     +'
     +
    -+test_expect_success 'should create branch when submodule is in .git/modules but not .gitmodules' '
    ++test_expect_success 'should create branch when submodule is not in HEAD .gitmodules' '
     +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
     +	(
     +		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 &&
    @@ t/t3207-branch-submodule.sh (new)
     +	)
     +'
     +
    ++test_expect_success 'should not create branches in inactive submodules' '
    ++	test_when_finished "cleanup_branches super branch-a" &&
    ++	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 'setup remote-tracking tests' '
     +	(
     +		cd super &&
    @@ t/t3207-branch-submodule.sh (new)
     +		# branch-b now has a committed submodule not in branch-a
     +		git commit -m "add second submodule"
     +	) &&
    -+	(
    -+		cd sub-upstream &&
    -+		git branch branch-a
    -+	) &&
     +	git clone --branch main --recurse-submodules super super-clone &&
     +	git -C super-clone config submodule.propagateBranches true
     +'
     +
     +test_expect_success 'should not create branch when submodule is not in .git/modules' '
    -+	# The cleanup needs to delete sub2:branch-b in particular because main does not have sub2
    ++	# The cleanup needs to delete sub2 separately because main does not have sub2
     +	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
    ++		git -C super-clone/sub2/sub-sub branch -D branch-b && \
     +		cleanup_branches super-clone branch-a branch-b" &&
     +	(
     +		cd super-clone &&
    @@ t/t3207-branch-submodule.sh (new)
     +		# This should fail because super-clone does not have sub2.
     +		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
     +		cat >expected <<-EOF &&
    -+		fatal: submodule sub: unable to find submodule
    -+		You may reinitialize the submodules using ${SQ}git checkout origin/branch-b && git submodule update${SQ}
    ++		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
    ++		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
     +		EOF
    ++		test_cmp expected 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 &&
    ++		git submodule update --init --recursive &&
     +		git branch --recurse-submodules branch-b origin/branch-b
     +	)
     +'
    @@ t/t3207-branch-submodule.sh (new)
     +	(
     +		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 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 "cleanup_branches super-clone branch-b" &&
    ++	test_when_finished "cleanup_branches super-clone branch-a && \
    ++		git -C super-clone remote rename ex-origin origin" &&
     +	(
     +		cd super-clone &&
    -+		git branch --recurse-submodules branch-b origin/branch-b
    ++		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)" = ""
     +	)
     +'
     +
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-06 21:55 ` [PATCH v2 0/3] implement branch --recurse-submodules Glen Choo
@ 2021-12-06 21:55   ` Glen Choo
  2021-12-06 22:48     ` Junio C Hamano
  2021-12-06 23:28     ` Junio C Hamano
  2021-12-06 21:55   ` [PATCH v2 2/3] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
                     ` (2 subsequent siblings)
  3 siblings, 2 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-06 21:55 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

This refactor is motivated by a desire to add a "dry_run" parameter to
create_branch() that will validate whether or not a branch can be
created without actually creating it - this behavior be used in a
subsequent commit that adds `git branch --recurse-submodules topic`.

Adding "dry_run" is not obvious because create_branch() is also used to
set tracking information without creating a branch, i.e. when using
--set-upstream-to. This appears to be a leftover from 4fc5006676 (Add
branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
create a branch and sometimes update tracking information without
creating a branch. However, we no longer support --set-upstream, so it
makes more sense to set tracking information with another function, like
setup_tracking(), and use create_branch() only to create branches. When
this is done, it will be trivial to add "dry_run".

Do this refactor by moving the branch validation and dwim logic from
create_branch() into a new function, validate_branch_start(), and call
it from setup_tracking(). Now that setup_tracking() can perform dwim and
tracking setup without creating a branch, use it in `git branch
--set-upstream-to` and remove unnecessary behavior from create_branch().

Since there were none, add tests for creating a branch with `--force`.

Signed-off-by: Glen Choo <chooglen@google.com>
---
As Jonathan noted in v1, the diff is quite large. I could shrink this
by forward-declaring setup_tracking() so that the function definitions
are in the same order; let me know if that would be preferred.

 branch.c          | 195 ++++++++++++++++++++++++++--------------------
 branch.h          |  13 +++-
 builtin/branch.c  |   7 +-
 t/t3200-branch.sh |  17 ++++
 4 files changed, 139 insertions(+), 93 deletions(-)

diff --git a/branch.c b/branch.c
index 07a46430b3..a635a60f8b 100644
--- a/branch.c
+++ b/branch.c
@@ -126,43 +126,6 @@ int install_branch_config(int flag, const char *local, const char *origin, const
 	return -1;
 }
 
-/*
- * 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 void setup_tracking(const char *new_ref, const char *orig_ref,
-			   enum branch_track track, int quiet)
-{
-	struct tracking tracking;
-	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
-
-	memset(&tracking, 0, sizeof(tracking));
-	tracking.spec.dst = (char *)orig_ref;
-	if (for_each_remote(find_tracked_branch, &tracking))
-		return;
-
-	if (!tracking.matches)
-		switch (track) {
-		case BRANCH_TRACK_ALWAYS:
-		case BRANCH_TRACK_EXPLICIT:
-		case BRANCH_TRACK_OVERRIDE:
-			break;
-		default:
-			return;
-		}
-
-	if (tracking.matches > 1)
-		die(_("Not tracking: ambiguous information for ref %s"),
-		    orig_ref);
-
-	if (install_branch_config(config_flags, new_ref, tracking.remote,
-			      tracking.src ? tracking.src : orig_ref) < 0)
-		exit(-1);
-
-	free(tracking.src);
-}
-
 int read_branch_desc(struct strbuf *buf, const char *branch_name)
 {
 	char *v = NULL;
@@ -243,33 +206,35 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+/**
+ * Validates whether a ref is a valid starting point for a branch, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test. This is
+ *     expanded with DWIM and assigned to real_ref.
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - oid is an out parameter containing the object_id of start_name
+ *
+ *   - real_ref is an out parameter containing the full, 'real' form of
+ *     start_name e.g. refs/heads/main instead of main
+ *
+ */
+static void validate_branch_start(struct repository *r, const char *start_name,
+				  enum branch_track track,
+				  struct object_id *oid, char **real_ref)
 {
 	struct commit *commit;
-	struct object_id oid;
-	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
 	int explicit_tracking = 0;
 
 	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
 		explicit_tracking = 1;
 
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
-
-	real_ref = NULL;
-	if (get_oid_mb(start_name, &oid)) {
+	if (repo_get_oid_mb(r, start_name, oid)) {
 		if (explicit_tracking) {
 			if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) {
 				error(_(upstream_missing), start_name);
@@ -281,7 +246,8 @@ void create_branch(struct repository *r,
 		die(_("Not a valid object name: '%s'."), start_name);
 	}
 
-	switch (dwim_ref(start_name, strlen(start_name), &oid, &real_ref, 0)) {
+	switch (repo_dwim_ref(r, start_name, strlen(start_name), oid, real_ref,
+			      0)) {
 	case 0:
 		/* Not branching from any existing branch */
 		if (explicit_tracking)
@@ -289,12 +255,12 @@ void create_branch(struct repository *r,
 		break;
 	case 1:
 		/* Unique completion -- good, only if it is a real branch */
-		if (!starts_with(real_ref, "refs/heads/") &&
-		    validate_remote_tracking_branch(real_ref)) {
+		if (!starts_with(*real_ref, "refs/heads/") &&
+		    validate_remote_tracking_branch(*real_ref)) {
 			if (explicit_tracking)
 				die(_(upstream_not_branch), start_name);
 			else
-				FREE_AND_NULL(real_ref);
+				FREE_AND_NULL(*real_ref);
 		}
 		break;
 	default:
@@ -302,37 +268,96 @@ void create_branch(struct repository *r,
 		break;
 	}
 
-	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
+	if ((commit = lookup_commit_reference(r, oid)) == NULL)
 		die(_("Not a valid branch point: '%s'."), start_name);
-	oidcpy(&oid, &commit->object.oid);
+	oidcpy(oid, &commit->object.oid);
+}
+
+void setup_tracking(const char *new_ref, const char *orig_ref,
+			   enum branch_track track, int quiet, int expand_orig)
+{
+	struct tracking tracking;
+	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
+	char *full_orig_ref;
+	struct object_id unused_oid;
+
+	memset(&tracking, 0, sizeof(tracking));
+	if (expand_orig)
+		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
+	else
+		full_orig_ref = xstrdup(orig_ref);
+
+	tracking.spec.dst = full_orig_ref;
+	if (for_each_remote(find_tracked_branch, &tracking))
+		goto cleanup;
+
+	if (!tracking.matches)
+		switch (track) {
+		case BRANCH_TRACK_ALWAYS:
+		case BRANCH_TRACK_EXPLICIT:
+		case BRANCH_TRACK_OVERRIDE:
+			break;
+		default:
+			goto cleanup;
+		}
+
+	if (tracking.matches > 1)
+		die(_("Not tracking: ambiguous information for ref %s"),
+		    full_orig_ref);
+
+	if (install_branch_config(config_flags, new_ref, tracking.remote,
+			      tracking.src ? tracking.src : full_orig_ref) < 0)
+		exit(-1);
+
+cleanup:
+	free(tracking.src);
+	free(full_orig_ref);
+}
+
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track)
+{
+	struct object_id oid;
+	char *real_ref;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	struct ref_transaction *transaction;
+	struct strbuf err = STRBUF_INIT;
+	char *msg;
+
+	if (clobber_head_ok && !force)
+		BUG("'clobber_head_ok' can only be used with 'force'");
+
+	if (clobber_head_ok ?
+			  validate_branchname(name, &ref) :
+			  validate_new_branchname(name, &ref, force)) {
+		forcing = 1;
+	}
+
+	validate_branch_start(r, start_name, track, &oid, &real_ref);
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
-	if (!dont_change_ref) {
-		struct ref_transaction *transaction;
-		struct strbuf err = STRBUF_INIT;
-		char *msg;
-
-		if (forcing)
-			msg = xstrfmt("branch: Reset to %s", start_name);
-		else
-			msg = xstrfmt("branch: Created from %s", start_name);
-
-		transaction = ref_transaction_begin(&err);
-		if (!transaction ||
-		    ref_transaction_update(transaction, ref.buf,
-					   &oid, forcing ? NULL : null_oid(),
-					   0, msg, &err) ||
-		    ref_transaction_commit(transaction, &err))
-			die("%s", err.buf);
-		ref_transaction_free(transaction);
-		strbuf_release(&err);
-		free(msg);
-	}
+	if (forcing)
+		msg = xstrfmt("branch: Reset to %s", start_name);
+	else
+		msg = xstrfmt("branch: Created from %s", start_name);
+
+	transaction = ref_transaction_begin(&err);
+	if (!transaction ||
+		ref_transaction_update(transaction, ref.buf,
+					&oid, forcing ? NULL : null_oid(),
+					0, msg, &err) ||
+		ref_transaction_commit(transaction, &err))
+		die("%s", err.buf);
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	free(msg);
 
 	if (real_ref && track)
-		setup_tracking(ref.buf + 11, real_ref, track, quiet);
+		setup_tracking(ref.buf + 11, real_ref, track, quiet, 0);
 
 	strbuf_release(&ref);
 	free(real_ref);
diff --git a/branch.h b/branch.h
index df0be61506..75cefcdcbd 100644
--- a/branch.h
+++ b/branch.h
@@ -17,6 +17,15 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/*
+ * This sets the branch.<new_ref>.{remote,merge} config settings so that
+ * branch 'new_ref' tracks 'orig_ref'. This is called when branches are
+ * created, or when branch configs are updated (e.g. with
+ * git branch --set-upstream-to).
+ */
+void setup_tracking(const char *new_ref, const char *orig_ref,
+		    enum branch_track track, int quiet, int expand_orig);
+
 /*
  * Creates a new branch, where:
  *
@@ -29,8 +38,8 @@ extern enum branch_track git_branch_track;
  *
  *   - force enables overwriting an existing (non-head) branch
  *
- *   - clobber_head_ok allows the currently checked out (hence existing)
- *     branch to be overwritten; without 'force', it has no effect.
+ *   - clobber_head_ok, when enabled with 'force', allows the currently
+ *     checked out (head) branch to be overwritten
  *
  *   - reflog creates a reflog for the branch
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index 81b5c111cb..19f2845e7a 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -821,12 +821,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		setup_tracking(branch->name, new_upstream, BRANCH_TRACK_OVERRIDE, quiet, 1);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 8c5c1ccf33..f97cf495ab 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
 	git branch abc && test_path_is_file .git/refs/heads/abc
 '
 
+test_expect_success 'git branch abc should fail when abc exists' '
+	test_must_fail git branch abc
+'
+
+test_expect_success 'git branch --force abc should fail when abc is checked out' '
+	test_when_finished git switch main &&
+	git switch abc &&
+	test_must_fail git branch --force abc HEAD~1
+'
+
+test_expect_success 'git branch --force abc should succeed when abc exists' '
+	git rev-parse HEAD~1 >expect &&
+	git branch --force abc HEAD~1 &&
+	git rev-parse abc >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v2 2/3] builtin/branch: clean up action-picking logic in cmd_branch()
  2021-12-06 21:55 ` [PATCH v2 0/3] implement branch --recurse-submodules Glen Choo
  2021-12-06 21:55   ` [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
@ 2021-12-06 21:55   ` Glen Choo
  2021-12-06 21:55   ` [PATCH v2 3/3] branch: add --recurse-submodules option for branch creation Glen Choo
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
  3 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-06 21:55 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Add a variable, noncreate_actions, to cmd_branch() that will tell us
whether or not cmd_branch() will default to creating a branch (instead
of performing another action). Besides making the function more
explicit, this allows us to validate options that can only be used when
creating a branch. Such an option does not exist yet, but one will be
introduced in a subsequent commit.

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).

Signed-off-by: Glen Choo <chooglen@google.com>
---
 builtin/branch.c | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 19f2845e7a..83b8016b38 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -27,7 +27,7 @@
 
 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>] [-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>"),
@@ -616,14 +616,15 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, force = 0, list = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
+	int noncreate_actions = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, icase = 0;
 	const char *new_upstream = NULL;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting;
 	struct string_list sorting_options = STRING_LIST_INIT_DUP;
 	struct ref_format format = REF_FORMAT_INIT;
@@ -706,8 +707,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
+			    !!show_current + !!list + !!edit_description +
+			    !!unset_upstream;
+	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
 	if (filter.abbrev == -1)
@@ -845,7 +848,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
 		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>?"));
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v2 3/3] branch: add --recurse-submodules option for branch creation
  2021-12-06 21:55 ` [PATCH v2 0/3] implement branch --recurse-submodules Glen Choo
  2021-12-06 21:55   ` [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
  2021-12-06 21:55   ` [PATCH v2 2/3] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
@ 2021-12-06 21:55   ` Glen Choo
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
  3 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-06 21:55 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

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 semantics e.g. `git
checkout` does not recursively checkout the expected branches created by
`git branch` yet. To ensure that the correct set of semantics is used,
this commit introduces a new configuration value,
`submodule.propagateBranches`, which enables submodule branching when
true (defaults to false).

This commit includes changes that allow Git to work with submodules
that are in trees (and not just the index):

* 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

Other changes

* add a "dry_run" parameter to create_branch() in order to support
  `git submodule--helper create-branch --dry-run`

Signed-off-by: Glen Choo <chooglen@google.com>
---
 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |   8 +
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 129 ++++++++++++-
 branch.h                           |  31 +++-
 builtin/branch.c                   |  40 +++-
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  35 ++++
 submodule-config.h                 |  35 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3207-branch-submodule.sh        | 284 +++++++++++++++++++++++++++++
 14 files changed, 609 insertions(+), 13 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index 063eec2511..e52262dc69 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` 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..52b35964c0 100644
--- a/Documentation/config/submodule.txt
+++ b/Documentation/config/submodule.txt
@@ -72,6 +72,14 @@ submodule.recurse::
 	For these commands a workaround is to temporarily change the
 	configuration value by using `git -c submodule.recurse=0`.
 
+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.
+
 submodule.fetchJobs::
 	Specifies how many submodules are fetched/cloned at the same time.
 	A positive integer allows up to that number of submodules fetched
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 a635a60f8b..55b44f2417 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;
@@ -316,7 +318,7 @@ void setup_tracking(const char *new_ref, const char *orig_ref,
 
 void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track)
+		   int reflog, int quiet, enum branch_track track, int dry_run)
 {
 	struct object_id oid;
 	char *real_ref;
@@ -336,6 +338,8 @@ void create_branch(struct repository *r, const char *name,
 	}
 
 	validate_branch_start(r, start_name, track, &oid, &real_ref);
+	if (dry_run)
+		goto cleanup;
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -359,10 +363,133 @@ void create_branch(struct repository *r, const char *name,
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet, 0);
 
+cleanup:
 	strbuf_release(&ref);
 	free(real_ref);
 }
 
+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);
+	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. */
+	validate_branch_start(r, start_name, BRANCH_TRACK_NEVER, &super_oid,
+			      &branch_point);
+
+	/*
+	 * 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, 0);
+
+	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 75cefcdcbd..19ebc9253a 100644
--- a/branch.h
+++ b/branch.h
@@ -48,12 +48,35 @@ void setup_tracking(const char *new_ref, const char *orig_ref,
  *   - track causes the new branch to be configured to merge the remote branch
  *     that start_name is a tracking branch for (if any).
  *
+ *   - dry_run causes the branch to be validated but not created.
+ *
  */
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track);
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   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 83b8016b38..bd3a7e566a 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -38,6 +38,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] = {
@@ -99,6 +101,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);
 }
 
@@ -621,7 +632,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	int noncreate_actions = 0;
 	/* possible options */
-	int reflog = 0, quiet = 0, icase = 0;
+	int reflog = 0, quiet = 0, icase = 0, recurse_submodules_explicit = 0;
 	const char *new_upstream = NULL;
 	enum branch_track track;
 	struct ref_filter filter;
@@ -671,6 +682,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(),
 	};
@@ -713,6 +725,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;
@@ -849,6 +872,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>?"));
@@ -856,10 +882,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);
-
+		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/checkout.c b/builtin/checkout.c
index cbf73b8c9f..c927a175a3 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -893,7 +893,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				      opts->new_branch_force ? 1 : 0,
 				      opts->new_branch_log,
 				      opts->quiet,
-				      opts->track);
+				      opts->track,
+				      0);
 		new_branch_info->name = opts->new_branch;
 		setup_branch_path(new_branch_info);
 	}
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index e630f0c730..44b6283c08 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;
@@ -3389,6 +3426,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..f246359d63 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,40 @@ const struct submodule *submodule_from_path(struct repository *r,
 	return config_from(r->submodule_cache, treeish_name, path, lookup_path);
 }
 
+void submodules_of_tree(struct repository *r,
+			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;
+
+	name_entry = xmalloc(sizeof(*name_entry));
+
+	CALLOC_ARRAY(out->entries, 0);
+	out->entry_nr = 0;
+	out->entry_alloc = 0;
+
+	fill_tree_descriptor(r, &tree, treeish_name);
+	while (tree_entry(&tree, name_entry)) {
+		if (!S_ISGITLINK(name_entry->mode) || !is_tree_submodule_active(r, treeish_name, name_entry->path)) {
+			continue;
+		}
+
+		st_entry = xmalloc(sizeof(*st_entry));
+		st_entry->name_entry = name_entry;
+		st_entry->submodule =
+			submodule_from_path(r, treeish_name, name_entry->path);
+		st_entry->repo = xmalloc(sizeof(*st_entry->repo));
+		if (repo_submodule_init(st_entry->repo, r, name_entry->path,
+					treeish_name))
+			FREE_AND_NULL(st_entry->repo);
+
+		ALLOC_GROW(out->entries, out->entry_nr + 1, out->entry_alloc);
+		out->entries[out->entry_nr++] = *st_entry;
+	}
+}
+
 void submodule_free(struct repository *r)
 {
 	if (r->submodule_cache)
diff --git a/submodule-config.h b/submodule-config.h
index 65875b94ea..56c12af03f 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,38 @@ 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. This only reads
+ * one level of the tree, so it will not return nested submodules;
+ * callers that require nested submodules are expected to handle the
+ * recursion 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 c689070524..5ace18a7d9 100644
--- a/submodule.c
+++ b/submodule.c
@@ -267,7 +267,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;
@@ -275,7 +277,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)
@@ -317,6 +319,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..2dd0e2b01f
--- /dev/null
+++ b/t/t3207-branch-submodule.sh
@@ -0,0 +1,284 @@
+#!/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
+
+test_expect_success 'setup superproject and submodule' '
+	git init super &&
+	test_commit foo &&
+	git init sub-sub-upstream &&
+	test_commit -C sub-sub-upstream foo &&
+	git init sub-upstream &&
+	git -C sub-upstream submodule add "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
+	git -C sub-upstream commit -m "add submodule" &&
+	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
+	git -C super commit -m "add submodule" &&
+	git -C super config submodule.propagateBranches true &&
+	git -C super/sub submodule update --init
+'
+
+CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
+
+cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
+	#!/bin/sh
+
+	super_dir="$1"
+	shift
+	(
+		cd "$super_dir" &&
+		git checkout main &&
+		for branch_name in "$@"; do
+			git branch -D "$branch_name"
+			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
+		done
+	)
+EOF
+chmod +x "$CLEANUP_SCRIPT_PATH"
+
+cleanup_branches() {
+	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
+} >/dev/null 2>/dev/null
+
+# Test the argument parsing
+test_expect_success '--recurse-submodules should create branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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
+	)
+'
+
+test_expect_success '--recurse-submodules should die if submodule.propagateBranches is false' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should respect submodule.recurse when creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -c submodule.recurse=true branch branch-a &&
+		git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should ignore submodule.recurse when not creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git -c submodule.recurse=true branch -D branch-a &&
+		test_must_fail git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+# Test branch creation behavior
+test_expect_success 'should create branches based off commit id in superproject' '
+	test_when_finished "cleanup_branches super branch-a branch-b" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 &&
+
+		cat >expected <<-EOF &&
+		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
+		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
+		EOF
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branches if branch exists and --force is given' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -C sub rev-parse HEAD >expected &&
+		test_commit -C sub baz &&
+		git -C sub branch branch-a HEAD~1 &&
+		git branch --recurse-submodules --force branch-a &&
+		git rev-parse branch-a &&
+		# assert that sub:branch-a was moved
+		git -C sub rev-parse branch-a >actual &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branch when submodule is not in HEAD .gitmodules' '
+	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
+	(
+		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 rev-parse branch-c &&
+		git -C sub rev-parse branch-c &&
+		git checkout --recurse-submodules branch-c &&
+		git -C sub2 rev-parse branch-c
+	)
+'
+
+test_expect_success 'should set up tracking of local branches with track=always' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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)" = ""
+	)
+'
+
+test_expect_success 'should not create branches in inactive submodules' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	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 'setup remote-tracking tests' '
+	(
+		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
+'
+
+test_expect_success 'should not create branch when submodule is not in .git/modules' '
+	# The cleanup needs to delete sub2 separately because main does not have sub2
+	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
+		git -C super-clone/sub2/sub-sub branch -D branch-b && \
+		cleanup_branches super-clone branch-a branch-b" &&
+	(
+		cd super-clone &&
+		# This should succeed because super-clone has sub.
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		# This should fail because super-clone does not have sub2.
+		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
+		cat >expected <<-EOF &&
+		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
+		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
+		EOF
+		test_cmp expected 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 "cleanup_branches super-clone branch-a" &&
+	(
+		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 "cleanup_branches super-clone branch-a && \
+		git -C super-clone remote rename ex-origin origin" &&
+	(
+		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
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-06 21:55   ` [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
@ 2021-12-06 22:48     ` Junio C Hamano
  2021-12-08 18:48       ` Glen Choo
  2021-12-06 23:28     ` Junio C Hamano
  1 sibling, 1 reply; 110+ messages in thread
From: Junio C Hamano @ 2021-12-06 22:48 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Glen Choo <chooglen@google.com> writes:

> As Jonathan noted in v1, the diff is quite large. I could shrink this
> by forward-declaring setup_tracking() so that the function definitions
> are in the same order; let me know if that would be preferred.

If you are not making any changes to setup_tracking() and just
moving, then the patch size inflated by the move is OK.

If you are moving and changing at the same time, well, that would
make it harder to read what is going on in the patch, so you want to
find a way to avoid it.  Splitting it the pure move into a separate
patch or use of forward-declaration may be good ways to do so.




>  branch.c          | 195 ++++++++++++++++++++++++++--------------------
>  branch.h          |  13 +++-
>  builtin/branch.c  |   7 +-
>  t/t3200-branch.sh |  17 ++++
>  4 files changed, 139 insertions(+), 93 deletions(-)
>
> diff --git a/branch.c b/branch.c
> index 07a46430b3..a635a60f8b 100644
> --- a/branch.c
> +++ b/branch.c
> @@ -126,43 +126,6 @@ int install_branch_config(int flag, const char *local, const char *origin, const
>  	return -1;
>  }
>  
> -/*
> - * 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 void setup_tracking(const char *new_ref, const char *orig_ref,
> -			   enum branch_track track, int quiet)
> -{
> -	struct tracking tracking;
> -	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
> -
> -	memset(&tracking, 0, sizeof(tracking));
> -	tracking.spec.dst = (char *)orig_ref;
> -	if (for_each_remote(find_tracked_branch, &tracking))
> -		return;
> -
> -	if (!tracking.matches)
> -		switch (track) {
> -		case BRANCH_TRACK_ALWAYS:
> -		case BRANCH_TRACK_EXPLICIT:
> -		case BRANCH_TRACK_OVERRIDE:
> -			break;
> -		default:
> -			return;
> -		}
> -
> -	if (tracking.matches > 1)
> -		die(_("Not tracking: ambiguous information for ref %s"),
> -		    orig_ref);
> -
> -	if (install_branch_config(config_flags, new_ref, tracking.remote,
> -			      tracking.src ? tracking.src : orig_ref) < 0)
> -		exit(-1);
> -
> -	free(tracking.src);
> -}
> -
>  int read_branch_desc(struct strbuf *buf, const char *branch_name)
>  {
>  	char *v = NULL;
> @@ -243,33 +206,35 @@ N_("\n"
>  "will track its remote counterpart, you may want to use\n"
>  "\"git push -u\" to set the upstream config as you push.");
>  
> -void create_branch(struct repository *r,
> -		   const char *name, const char *start_name,
> -		   int force, int clobber_head_ok, int reflog,
> -		   int quiet, enum branch_track track)
> +/**
> + * Validates whether a ref is a valid starting point for a branch, where:
> + *
> + *   - r is the repository to validate the branch for
> + *
> + *   - start_name is the ref that we would like to test. This is
> + *     expanded with DWIM and assigned to real_ref.
> + *
> + *   - track is the tracking mode of the new branch. If tracking is
> + *     explicitly requested, start_name must be a branch (because
> + *     otherwise start_name cannot be tracked)
> + *
> + *   - oid is an out parameter containing the object_id of start_name
> + *
> + *   - real_ref is an out parameter containing the full, 'real' form of
> + *     start_name e.g. refs/heads/main instead of main
> + *
> + */

Good description that will help reviewers and future developers.
Very much appreciated.

> +static void validate_branch_start(struct repository *r, const char *start_name,
> +				  enum branch_track track,
> +				  struct object_id *oid, char **real_ref)
>  {
>  	struct commit *commit;
> -	struct object_id oid;
> -	char *real_ref;
> -	struct strbuf ref = STRBUF_INIT;
> -	int forcing = 0;
> -	int dont_change_ref = 0;
>  	int explicit_tracking = 0;
>  
>  	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
>  		explicit_tracking = 1;
>  
> -	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
> -	    ? validate_branchname(name, &ref)
> -	    : validate_new_branchname(name, &ref, force)) {
> -		if (!force)
> -			dont_change_ref = 1;
> -		else
> -			forcing = 1;
> -	}
> -
> -	real_ref = NULL;
> -	if (get_oid_mb(start_name, &oid)) {
> +	if (repo_get_oid_mb(r, start_name, oid)) {
>  		if (explicit_tracking) {
>  			if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) {
>  				error(_(upstream_missing), start_name);

The post context continues with:

				advise(_(upstream_advice));
				exit(1);
			}
			die(_(upstream_missing), start_name);

This is not a problem with this patch, and it should not be fixed as
part of this series, but since I noticed it, I'll mention it as a
leftover low-hanging fruit to be fixed after the dust settles.  The
exit(1) looks wrong.  We should exit with 128 just like die() does.
Issuing of an advice message should not affect the exit code.

> +void setup_tracking(const char *new_ref, const char *orig_ref,
> +			   enum branch_track track, int quiet, int expand_orig)

It is unclear what expand_orig option is supposed to do and how it
would help the caller.  Perhaps a comment before the function is in
order (the comment in branch.h before the declaration of this
function does not make it clear, either).

> +{
> +	struct tracking tracking;
> +	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
> +	char *full_orig_ref;
> +	struct object_id unused_oid;
> +
> +	memset(&tracking, 0, sizeof(tracking));
> +	if (expand_orig)
> +		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);

So, the idea is, because we are setting up a new_ref to "track"
orig_ref, we may be better off pretending that we are "creating a
new branch from the orig_ref and tracking it", so that orig_ref that
is not something we can track will be caught with the same logic?

This will cause full_orig_ref to start with "refs/heads/" or
"refs/remotes/" if 'track' is something that requires tracking.

> +	else
> +		full_orig_ref = xstrdup(orig_ref);

Even though the variable claims to be FULL orig_ref, when this side
of if/else is taken, nobody guarantees that full_orig_ref is in fact
a full ref, or merely the name of the branch, no?  Would that cause
problems later?

> +	tracking.spec.dst = full_orig_ref;
> +	if (for_each_remote(find_tracked_branch, &tracking))
> +		goto cleanup;
> +
> +	if (!tracking.matches)
> +		switch (track) {
> +		case BRANCH_TRACK_ALWAYS:
> +		case BRANCH_TRACK_EXPLICIT:
> +		case BRANCH_TRACK_OVERRIDE:
> +			break;
> +		default:
> +			goto cleanup;
> +		}
> +
> +	if (tracking.matches > 1)
> +		die(_("Not tracking: ambiguous information for ref %s"),
> +		    full_orig_ref);

What's the next step for the user to take, after seeing this message?
Do we have the necessary info readily available to help them at this
point in tracking.* structure (e.g. "it could be following X or Y and
we cannot decide between the two for you"), or have we discarded the
information already?

If tracking.matches == 0, because we broke out of the switch() for
some values of track, we will make this install_branch_config()
using members of the tracking structure, which is a bit unnerving.

> +	if (install_branch_config(config_flags, new_ref, tracking.remote,
> +			      tracking.src ? tracking.src : full_orig_ref) < 0)

But tracking.src==NULL is substituted with full_orig_ref, so as long
as the value in that variable is sensible, we would probably be ok
on the 4th parameter.  I am not sure who set tracking.remote or if
it is always set to a sensible value.  Especially when tracking.matches
is zero.

> +		exit(-1);

Don't exit with a negative value.

> +cleanup:
> +	free(tracking.src);
> +	free(full_orig_ref);
> +}

I'll stop here for now.

Thanks.


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-06 21:55   ` [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
  2021-12-06 22:48     ` Junio C Hamano
@ 2021-12-06 23:28     ` Junio C Hamano
  2021-12-08 17:09       ` Glen Choo
  1 sibling, 1 reply; 110+ messages in thread
From: Junio C Hamano @ 2021-12-06 23:28 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Glen Choo <chooglen@google.com> writes:

> +void setup_tracking(const char *new_ref, const char *orig_ref,
> +			   enum branch_track track, int quiet, int expand_orig)
> +{
> +	struct tracking tracking;
> +	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
> +	char *full_orig_ref;
> +	struct object_id unused_oid;
> +
> +	memset(&tracking, 0, sizeof(tracking));
> +	if (expand_orig)
> +		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
> +	else
> +		full_orig_ref = xstrdup(orig_ref);
> +
> +	tracking.spec.dst = full_orig_ref;
> +	if (for_each_remote(find_tracked_branch, &tracking))
> +		goto cleanup;
> +
> +	if (!tracking.matches)
> +		switch (track) {
> +		case BRANCH_TRACK_ALWAYS:
> +		case BRANCH_TRACK_EXPLICIT:
> +		case BRANCH_TRACK_OVERRIDE:
> +			break;

This heavily conflicts with what another topic "inherit tracking
info from the other branch" wants to do to this function.  What's 
the status of that topic, by the way?  Should we block this one
waiting for the other, or the other way around?

Thanks.




^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-06 23:28     ` Junio C Hamano
@ 2021-12-08 17:09       ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-08 17:09 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Junio C Hamano <gitster@pobox.com> writes:

>> +void setup_tracking(const char *new_ref, const char *orig_ref,
>> +			   enum branch_track track, int quiet, int expand_orig)
>> +{
>> +	struct tracking tracking;
>> +	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
>> +	char *full_orig_ref;
>> +	struct object_id unused_oid;
>> +
>> +	memset(&tracking, 0, sizeof(tracking));
>> +	if (expand_orig)
>> +		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
>> +	else
>> +		full_orig_ref = xstrdup(orig_ref);
>> +
>> +	tracking.spec.dst = full_orig_ref;
>> +	if (for_each_remote(find_tracked_branch, &tracking))
>> +		goto cleanup;
>> +
>> +	if (!tracking.matches)
>> +		switch (track) {
>> +		case BRANCH_TRACK_ALWAYS:
>> +		case BRANCH_TRACK_EXPLICIT:
>> +		case BRANCH_TRACK_OVERRIDE:
>> +			break;
>
> This heavily conflicts with what another topic "inherit tracking
> info from the other branch" wants to do to this function.  What's 
> the status of that topic, by the way?  Should we block this one
> waiting for the other, or the other way around?
>
> Thanks.

As mentioned in [1], I plan to rebase this series on top of the "inherit
tracking info from the other branch" series.

I'll send a re-roll soon, thanks!

[1] https://lore.kernel.org/git/kl6lbl1rauw3.fsf@chooglen-macbookpro.roam.corp.google.com/

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v2 1/3] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-06 22:48     ` Junio C Hamano
@ 2021-12-08 18:48       ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-08 18:48 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Junio C Hamano <gitster@pobox.com> writes:

>> As Jonathan noted in v1, the diff is quite large. I could shrink this
>> by forward-declaring setup_tracking() so that the function definitions
>> are in the same order; let me know if that would be preferred.
>
> If you are not making any changes to setup_tracking() and just
> moving, then the patch size inflated by the move is OK.
>
> If you are moving and changing at the same time, well, that would
> make it harder to read what is going on in the patch, so you want to
> find a way to avoid it.  Splitting it the pure move into a separate
> patch or use of forward-declaration may be good ways to do so.

Thanks for the advice! I'll use one of these approaches.

> The post context continues with:
>
> 				advise(_(upstream_advice));
> 				exit(1);
> 			}
> 			die(_(upstream_missing), start_name);
>
> This is not a problem with this patch, and it should not be fixed as
> part of this series, but since I noticed it, I'll mention it as a
> leftover low-hanging fruit to be fixed after the dust settles.  The
> exit(1) looks wrong.  We should exit with 128 just like die() does.
> Issuing of an advice message should not affect the exit code.

I'll include an optional cleanup patch to address this and the exit(-1).

>> +void setup_tracking(const char *new_ref, const char *orig_ref,
>> +			   enum branch_track track, int quiet, int expand_orig)
>
> It is unclear what expand_orig option is supposed to do and how it
> would help the caller.  Perhaps a comment before the function is in
> order (the comment in branch.h before the declaration of this
> function does not make it clear, either).

Ah, I will add extra clarification.

This boolean parameter controls whether or not we should validate + DWIM
orig_ref. This is a performance optimization to avoid expanding an
orig_ref that has already been expanded (e.g. because create_branch()
has already expanded it).

But, as you point out later on, this may not be a good idea.

>> +{
>> +	struct tracking tracking;
>> +	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
>> +	char *full_orig_ref;
>> +	struct object_id unused_oid;
>> +
>> +	memset(&tracking, 0, sizeof(tracking));
>> +	if (expand_orig)
>> +		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
>
> So, the idea is, because we are setting up a new_ref to "track"
> orig_ref, we may be better off pretending that we are "creating a
> new branch from the orig_ref and tracking it", so that orig_ref that
> is not something we can track will be caught with the same logic?
>
> This will cause full_orig_ref to start with "refs/heads/" or
> "refs/remotes/" if 'track' is something that requires tracking.

Yes.

>> +	else
>> +		full_orig_ref = xstrdup(orig_ref);
>
> Even though the variable claims to be FULL orig_ref, when this side
> of if/else is taken, nobody guarantees that full_orig_ref is in fact
> a full ref, or merely the name of the branch, no?  Would that cause
> problems later?

Yes, the assumption is that the caller has already done the work of
making sure that orig_ref has been validated and expanded into its full
form. As mentioned earlier, this is purely a performance optimzation,
but it is not a very safe one because it requires the caller to pick the
correct value for expand_orig.

I chose this approach because I anticipate that the only callers of
setup_tracking() will be create_branch() (which always wants expand_orig
= 0) and cmd_branch() (which always wants expand_orig = 1), so the right
course of action is clear for now. I don't think setup_tracking() will
be useful to anyone else, but expand_orig is a potential sharp edge for
new callers.

My next-preferred option would be to remove "expand_orig" and always
call validate_branch_start(). We might waste a few cycles sometimes, but
the function becomes impossible to misuse.

>> +	tracking.spec.dst = full_orig_ref;
>> +	if (for_each_remote(find_tracked_branch, &tracking))
>> +		goto cleanup;
>> +
>> +	if (!tracking.matches)
>> +		switch (track) {
>> +		case BRANCH_TRACK_ALWAYS:
>> +		case BRANCH_TRACK_EXPLICIT:
>> +		case BRANCH_TRACK_OVERRIDE:
>> +			break;
>> +		default:
>> +			goto cleanup;
>> +		}
>> +
>> +	if (tracking.matches > 1)
>> +		die(_("Not tracking: ambiguous information for ref %s"),
>> +		    full_orig_ref);
>
> What's the next step for the user to take, after seeing this message?
> Do we have the necessary info readily available to help them at this
> point in tracking.* structure (e.g. "it could be following X or Y and
> we cannot decide between the two for you"), or have we discarded the
> information already?

This information is discarded in expand_ref(). From the function
signature:

  int expand_ref(struct repository *repo, const char *str, int len,
	       struct object_id *oid, char **ref)

The return value is the number of matched refs and "ref" is an out
parameter containing the first matched ref.

> If tracking.matches == 0, because we broke out of the switch() for
> some values of track, we will make this install_branch_config()
> using members of the tracking structure, which is a bit unnerving.
>
>> +	if (install_branch_config(config_flags, new_ref, tracking.remote,
>> +			      tracking.src ? tracking.src : full_orig_ref) < 0)
>
> But tracking.src==NULL is substituted with full_orig_ref, so as long
> as the value in that variable is sensible, we would probably be ok
> on the 4th parameter.

When tracking.matches is zero, the assumption is that
install_branch_config() should set up tracking based off a local branch.
By default, we assume that users only want tracking for remote-tracking
branches, so only track local branches if we are confident that the user
wants this, aka track = BRANCH_TRACK_{EXPLICIT,ALWAYS,OVERRIDE} (and
soon, BRANCH_TRACK_INHERIT).

> I am not sure who set tracking.remote or if it is always set to a
> sensible value. Especially when tracking.matches is zero.

tracking.matches == 0 should imply that tracking.remote == NULL, which
gives us the expected behavior of tracking a local branch in
install_branch_config().

This is also a bit too implicit for my tastes, but I don't think this is
the time for such a refactor :)

^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v3 0/5] implement branch --recurse-submodules
  2021-12-06 21:55 ` [PATCH v2 0/3] implement branch --recurse-submodules Glen Choo
                     ` (2 preceding siblings ...)
  2021-12-06 21:55   ` [PATCH v2 3/3] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-09 18:49   ` Glen Choo
  2021-12-09 18:49     ` [PATCH v3 1/5] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
                       ` (6 more replies)
  3 siblings, 7 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-09 18:49 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Submodule branching RFC:
https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/

Original Submodule UX RFC/Discussion:
https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/

Contributor Summit submodules Notes:
https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/

Submodule UX overhaul updates:
https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update

This series implements branch --recurse-submodules as laid out in the
Submodule branching RFC (linked above). If there are concerns about the
UX/behavior, I would appreciate feedback on the RFC thread as well :)

This series is based off js/branch-track-inherit.

This version is functionally identical to v2. I've only addressed
feedback around code organization, i.e. the the merge conflict with
js/branch-track-inherit and making patch 1 easier to review. Thus, some
discussions on [1] are still unaddressed.

Patch 5 is an optional cleanup of the questionable exit codes that Junio
found [1]. I wasn't able to figure out the intent of the original
authors, so it is mostly a best-guess at the right exit code. It doesn't
cause any tests to fail, but this wasn't covered by tests to begin with.

Changes since v2
* Rebase onto js/branch-track-inherit. This series should continue to be
  the case going forward.
* Patch 1 has a smaller diff because the introduction of
  validate_branch_start() no longer changes the function order thanks to a
  forward declaration. This artificial forward declaration is removed in a
  patch 2 (which can just be squashed into patch 1).
* Optional cleanup: fix questionable exit codes in patch 5.

Changes since v1:
* Move the functionality of "git branch --dry-run" into "git submodule-helper create-branch --dry-run"
* Add more fields to the submodules_of_tree() struct to reduce the
  number of allocations made by the caller [2]. Move this functionality
  to patch 3 (formerly patch 4) and drop patch 1.
* Make submodules_of_tree() ignore inactive submodules [3]
* Structure the output of the submodules a bit better by adding prefixes
  to the child process' output (instead of inconsistently indenting the
  output).
** I wasn't able to find a good way to interleave stdout/stderr
   correctly, so a less-than-desirable workaround was to route the child
   process output to stdout/stderr depending on the exit code.
** Eventually, I would like to structure the output of submodules in a
   report, as Ævar suggested [4]. But at this stage, I think that it's
   better to spend time getting user feedback on the submodules
   branching UX and it'll be easier to standardize the output when we've
   implemented more of the UX :)

[1] https://lore.kernel.org/git/xmqqbl1tcptq.fsf@gitster.g
[2] https://lore.kernel.org/git/211123.86r1b7uoil.gmgdl@evledraar.gmail.com
[3] https://lore.kernel.org/git/3ad3941c-de18-41bf-2e44-4238ae868d79@gmail.com
[4] https://lore.kernel.org/git/211123.86v90juovj.gmgdl@evledraar.gmail.com

Glen Choo (5):
  branch: move --set-upstream-to behavior to setup_tracking()
  branch: remove forward declaration of validate_branch_start()
  builtin/branch: clean up action-picking logic in cmd_branch()
  branch: add --recurse-submodules option for branch creation
  branch.c: replace questionable exit() codes

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |   8 +
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 336 +++++++++++++++++++++--------
 branch.h                           |  44 +++-
 builtin/branch.c                   |  66 ++++--
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  35 +++
 submodule-config.h                 |  35 +++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3200-branch.sh                  |  17 ++
 t/t3207-branch-submodule.sh        | 284 ++++++++++++++++++++++++
 15 files changed, 765 insertions(+), 120 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

Range-diff against v2:
1:  cc212dcd39 ! 1:  8241c0b51a branch: move --set-upstream-to behavior to setup_tracking()
    @@ Commit message
         Signed-off-by: Glen Choo <chooglen@google.com>
     
      ## branch.c ##
    -@@ branch.c: int install_branch_config(int flag, const char *local, const char *origin, const
    - 	return -1;
    +@@ branch.c: static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
    + 	return 0;
      }
      
     -/*
    @@ branch.c: int install_branch_config(int flag, const char *local, const char *ori
     - */
     -static void setup_tracking(const char *new_ref, const char *orig_ref,
     -			   enum branch_track track, int quiet)
    --{
    --	struct tracking tracking;
    --	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
    --
    --	memset(&tracking, 0, sizeof(tracking));
    ++static void validate_branch_start(struct repository *r, const char *start_name,
    ++				  enum branch_track track,
    ++				  struct object_id *oid, char **real_ref);
    ++
    ++void setup_tracking(const char *new_ref, const char *orig_ref,
    ++			   enum branch_track track, int quiet, int expand_orig)
    + {
    + 	struct tracking tracking;
    + 	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
    + 	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
    ++	char *full_orig_ref;
    ++	struct object_id unused_oid;
    + 
    + 	memset(&tracking, 0, sizeof(tracking));
     -	tracking.spec.dst = (char *)orig_ref;
    --	if (for_each_remote(find_tracked_branch, &tracking))
    --		return;
    --
    --	if (!tracking.matches)
    --		switch (track) {
    --		case BRANCH_TRACK_ALWAYS:
    --		case BRANCH_TRACK_EXPLICIT:
    --		case BRANCH_TRACK_OVERRIDE:
    --			break;
    --		default:
    ++	if (expand_orig)
    ++		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
    ++	else
    ++		full_orig_ref = xstrdup(orig_ref);
    ++
    ++	tracking.spec.dst = full_orig_ref;
    + 	tracking.srcs = &tracking_srcs;
    + 	if (track != BRANCH_TRACK_INHERIT)
    + 		for_each_remote(find_tracked_branch, &tracking);
    +@@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
    + 		case BRANCH_TRACK_OVERRIDE:
    + 			break;
    + 		default:
     -			return;
    --		}
    --
    --	if (tracking.matches > 1)
    --		die(_("Not tracking: ambiguous information for ref %s"),
    ++			goto cleanup;
    + 		}
    + 
    + 	if (tracking.matches > 1)
    + 		die(_("Not tracking: ambiguous information for ref %s"),
     -		    orig_ref);
    --
    --	if (install_branch_config(config_flags, new_ref, tracking.remote,
    --			      tracking.src ? tracking.src : orig_ref) < 0)
    --		exit(-1);
    --
    --	free(tracking.src);
    --}
    --
    ++		    full_orig_ref);
    + 
    + 	if (tracking.srcs->nr < 1)
    +-		string_list_append(tracking.srcs, orig_ref);
    ++		string_list_append(tracking.srcs, full_orig_ref);
    + 	if (install_branch_config_multiple_remotes(config_flags, new_ref, tracking.remote,
    + 			      tracking.srcs) < 0)
    + 		exit(-1);
    + 
    ++cleanup:
    + 	string_list_clear(tracking.srcs, 0);
    ++	free(full_orig_ref);
    + }
    + 
      int read_branch_desc(struct strbuf *buf, const char *branch_name)
    - {
    - 	char *v = NULL;
     @@ branch.c: N_("\n"
      "will track its remote counterpart, you may want to use\n"
      "\"git push -u\" to set the upstream config as you push.");
    @@ branch.c: void create_branch(struct repository *r,
     +	oidcpy(oid, &commit->object.oid);
     +}
     +
    -+void setup_tracking(const char *new_ref, const char *orig_ref,
    -+			   enum branch_track track, int quiet, int expand_orig)
    -+{
    -+	struct tracking tracking;
    -+	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
    -+	char *full_orig_ref;
    -+	struct object_id unused_oid;
    -+
    -+	memset(&tracking, 0, sizeof(tracking));
    -+	if (expand_orig)
    -+		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
    -+	else
    -+		full_orig_ref = xstrdup(orig_ref);
    -+
    -+	tracking.spec.dst = full_orig_ref;
    -+	if (for_each_remote(find_tracked_branch, &tracking))
    -+		goto cleanup;
    -+
    -+	if (!tracking.matches)
    -+		switch (track) {
    -+		case BRANCH_TRACK_ALWAYS:
    -+		case BRANCH_TRACK_EXPLICIT:
    -+		case BRANCH_TRACK_OVERRIDE:
    -+			break;
    -+		default:
    -+			goto cleanup;
    -+		}
    -+
    -+	if (tracking.matches > 1)
    -+		die(_("Not tracking: ambiguous information for ref %s"),
    -+		    full_orig_ref);
    -+
    -+	if (install_branch_config(config_flags, new_ref, tracking.remote,
    -+			      tracking.src ? tracking.src : full_orig_ref) < 0)
    -+		exit(-1);
    -+
    -+cleanup:
    -+	free(tracking.src);
    -+	free(full_orig_ref);
    -+}
    -+
     +void create_branch(struct repository *r, const char *name,
     +		   const char *start_name, int force, int clobber_head_ok,
     +		   int reflog, int quiet, enum branch_track track)
-:  ---------- > 2:  b74bcbaade branch: remove forward declaration of validate_branch_start()
2:  320749cc82 = 3:  235173efc9 builtin/branch: clean up action-picking logic in cmd_branch()
3:  c0441c6691 = 4:  3dabb8e2fa branch: add --recurse-submodules option for branch creation
-:  ---------- > 5:  70fb03f882 branch.c: replace questionable exit() codes
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v3 1/5] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
@ 2021-12-09 18:49     ` Glen Choo
  2021-12-09 21:19       ` Jonathan Tan
  2021-12-09 18:49     ` [PATCH v3 2/5] branch: remove forward declaration of validate_branch_start() Glen Choo
                       ` (5 subsequent siblings)
  6 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-09 18:49 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

This refactor is motivated by a desire to add a "dry_run" parameter to
create_branch() that will validate whether or not a branch can be
created without actually creating it - this behavior be used in a
subsequent commit that adds `git branch --recurse-submodules topic`.

Adding "dry_run" is not obvious because create_branch() is also used to
set tracking information without creating a branch, i.e. when using
--set-upstream-to. This appears to be a leftover from 4fc5006676 (Add
branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
create a branch and sometimes update tracking information without
creating a branch. However, we no longer support --set-upstream, so it
makes more sense to set tracking information with another function, like
setup_tracking(), and use create_branch() only to create branches. When
this is done, it will be trivial to add "dry_run".

Do this refactor by moving the branch validation and dwim logic from
create_branch() into a new function, validate_branch_start(), and call
it from setup_tracking(). Now that setup_tracking() can perform dwim and
tracking setup without creating a branch, use it in `git branch
--set-upstream-to` and remove unnecessary behavior from create_branch().

Since there were none, add tests for creating a branch with `--force`.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c          | 147 +++++++++++++++++++++++++++-------------------
 branch.h          |  13 +++-
 builtin/branch.c  |   7 +--
 t/t3200-branch.sh |  17 ++++++
 4 files changed, 117 insertions(+), 67 deletions(-)

diff --git a/branch.c b/branch.c
index 29e835d367..9429936734 100644
--- a/branch.c
+++ b/branch.c
@@ -209,20 +209,26 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
 	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
- * config.
- */
-static void setup_tracking(const char *new_ref, const char *orig_ref,
-			   enum branch_track track, int quiet)
+static void validate_branch_start(struct repository *r, const char *start_name,
+				  enum branch_track track,
+				  struct object_id *oid, char **real_ref);
+
+void setup_tracking(const char *new_ref, const char *orig_ref,
+			   enum branch_track track, int quiet, int expand_orig)
 {
 	struct tracking tracking;
 	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
 	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
+	char *full_orig_ref;
+	struct object_id unused_oid;
 
 	memset(&tracking, 0, sizeof(tracking));
-	tracking.spec.dst = (char *)orig_ref;
+	if (expand_orig)
+		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
+	else
+		full_orig_ref = xstrdup(orig_ref);
+
+	tracking.spec.dst = full_orig_ref;
 	tracking.srcs = &tracking_srcs;
 	if (track != BRANCH_TRACK_INHERIT)
 		for_each_remote(find_tracked_branch, &tracking);
@@ -236,20 +242,22 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 		case BRANCH_TRACK_OVERRIDE:
 			break;
 		default:
-			return;
+			goto cleanup;
 		}
 
 	if (tracking.matches > 1)
 		die(_("Not tracking: ambiguous information for ref %s"),
-		    orig_ref);
+		    full_orig_ref);
 
 	if (tracking.srcs->nr < 1)
-		string_list_append(tracking.srcs, orig_ref);
+		string_list_append(tracking.srcs, full_orig_ref);
 	if (install_branch_config_multiple_remotes(config_flags, new_ref, tracking.remote,
 			      tracking.srcs) < 0)
 		exit(-1);
 
+cleanup:
 	string_list_clear(tracking.srcs, 0);
+	free(full_orig_ref);
 }
 
 int read_branch_desc(struct strbuf *buf, const char *branch_name)
@@ -332,33 +340,35 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+/**
+ * Validates whether a ref is a valid starting point for a branch, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test. This is
+ *     expanded with DWIM and assigned to real_ref.
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - oid is an out parameter containing the object_id of start_name
+ *
+ *   - real_ref is an out parameter containing the full, 'real' form of
+ *     start_name e.g. refs/heads/main instead of main
+ *
+ */
+static void validate_branch_start(struct repository *r, const char *start_name,
+				  enum branch_track track,
+				  struct object_id *oid, char **real_ref)
 {
 	struct commit *commit;
-	struct object_id oid;
-	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
 	int explicit_tracking = 0;
 
 	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
 		explicit_tracking = 1;
 
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
-
-	real_ref = NULL;
-	if (get_oid_mb(start_name, &oid)) {
+	if (repo_get_oid_mb(r, start_name, oid)) {
 		if (explicit_tracking) {
 			if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) {
 				error(_(upstream_missing), start_name);
@@ -370,7 +380,8 @@ void create_branch(struct repository *r,
 		die(_("Not a valid object name: '%s'."), start_name);
 	}
 
-	switch (dwim_ref(start_name, strlen(start_name), &oid, &real_ref, 0)) {
+	switch (repo_dwim_ref(r, start_name, strlen(start_name), oid, real_ref,
+			      0)) {
 	case 0:
 		/* Not branching from any existing branch */
 		if (explicit_tracking)
@@ -378,12 +389,12 @@ void create_branch(struct repository *r,
 		break;
 	case 1:
 		/* Unique completion -- good, only if it is a real branch */
-		if (!starts_with(real_ref, "refs/heads/") &&
-		    validate_remote_tracking_branch(real_ref)) {
+		if (!starts_with(*real_ref, "refs/heads/") &&
+		    validate_remote_tracking_branch(*real_ref)) {
 			if (explicit_tracking)
 				die(_(upstream_not_branch), start_name);
 			else
-				FREE_AND_NULL(real_ref);
+				FREE_AND_NULL(*real_ref);
 		}
 		break;
 	default:
@@ -391,37 +402,55 @@ void create_branch(struct repository *r,
 		break;
 	}
 
-	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
+	if ((commit = lookup_commit_reference(r, oid)) == NULL)
 		die(_("Not a valid branch point: '%s'."), start_name);
-	oidcpy(&oid, &commit->object.oid);
+	oidcpy(oid, &commit->object.oid);
+}
+
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track)
+{
+	struct object_id oid;
+	char *real_ref;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	struct ref_transaction *transaction;
+	struct strbuf err = STRBUF_INIT;
+	char *msg;
+
+	if (clobber_head_ok && !force)
+		BUG("'clobber_head_ok' can only be used with 'force'");
+
+	if (clobber_head_ok ?
+			  validate_branchname(name, &ref) :
+			  validate_new_branchname(name, &ref, force)) {
+		forcing = 1;
+	}
+
+	validate_branch_start(r, start_name, track, &oid, &real_ref);
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
-	if (!dont_change_ref) {
-		struct ref_transaction *transaction;
-		struct strbuf err = STRBUF_INIT;
-		char *msg;
-
-		if (forcing)
-			msg = xstrfmt("branch: Reset to %s", start_name);
-		else
-			msg = xstrfmt("branch: Created from %s", start_name);
-
-		transaction = ref_transaction_begin(&err);
-		if (!transaction ||
-		    ref_transaction_update(transaction, ref.buf,
-					   &oid, forcing ? NULL : null_oid(),
-					   0, msg, &err) ||
-		    ref_transaction_commit(transaction, &err))
-			die("%s", err.buf);
-		ref_transaction_free(transaction);
-		strbuf_release(&err);
-		free(msg);
-	}
+	if (forcing)
+		msg = xstrfmt("branch: Reset to %s", start_name);
+	else
+		msg = xstrfmt("branch: Created from %s", start_name);
+
+	transaction = ref_transaction_begin(&err);
+	if (!transaction ||
+		ref_transaction_update(transaction, ref.buf,
+					&oid, forcing ? NULL : null_oid(),
+					0, msg, &err) ||
+		ref_transaction_commit(transaction, &err))
+		die("%s", err.buf);
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	free(msg);
 
 	if (real_ref && track)
-		setup_tracking(ref.buf + 11, real_ref, track, quiet);
+		setup_tracking(ref.buf + 11, real_ref, track, quiet, 0);
 
 	strbuf_release(&ref);
 	free(real_ref);
diff --git a/branch.h b/branch.h
index 6484bda8a2..46951c446b 100644
--- a/branch.h
+++ b/branch.h
@@ -18,6 +18,15 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/*
+ * This sets the branch.<new_ref>.{remote,merge} config settings so that
+ * branch 'new_ref' tracks 'orig_ref'. This is called when branches are
+ * created, or when branch configs are updated (e.g. with
+ * git branch --set-upstream-to).
+ */
+void setup_tracking(const char *new_ref, const char *orig_ref,
+		    enum branch_track track, int quiet, int expand_orig);
+
 /*
  * Creates a new branch, where:
  *
@@ -30,8 +39,8 @@ extern enum branch_track git_branch_track;
  *
  *   - force enables overwriting an existing (non-head) branch
  *
- *   - clobber_head_ok allows the currently checked out (hence existing)
- *     branch to be overwritten; without 'force', it has no effect.
+ *   - clobber_head_ok, when enabled with 'force', allows the currently
+ *     checked out (head) branch to be overwritten
  *
  *   - reflog creates a reflog for the branch
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index 81a29edb4a..e19aab5356 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -823,12 +823,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		setup_tracking(branch->name, new_upstream, BRANCH_TRACK_OVERRIDE, quiet, 1);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 267a624671..18e285a876 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
 	git branch abc && test_path_is_file .git/refs/heads/abc
 '
 
+test_expect_success 'git branch abc should fail when abc exists' '
+	test_must_fail git branch abc
+'
+
+test_expect_success 'git branch --force abc should fail when abc is checked out' '
+	test_when_finished git switch main &&
+	git switch abc &&
+	test_must_fail git branch --force abc HEAD~1
+'
+
+test_expect_success 'git branch --force abc should succeed when abc exists' '
+	git rev-parse HEAD~1 >expect &&
+	git branch --force abc HEAD~1 &&
+	git rev-parse abc >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v3 2/5] branch: remove forward declaration of validate_branch_start()
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
  2021-12-09 18:49     ` [PATCH v3 1/5] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
@ 2021-12-09 18:49     ` Glen Choo
  2021-12-09 18:49     ` [PATCH v3 3/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
                       ` (4 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-09 18:49 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

In the previous commit, validate_branch_start() was forward declared in
order to preserve the function order and minimize the diff. Since the
forward declaration is no longer needed, remove it by moving
setup_tracking() to the appropriate position.

Signed-off-by: Glen Choo <chooglen@google.com>
---
This patch is logically part of the previous patch because it just
cleans up the artificial forward declaration that exists only to shrink
the diff for reviewers.

As such, if/when this series is merged, I would prefer for this patch to
be squashed with the previous one.

 branch.c | 98 +++++++++++++++++++++++++++-----------------------------
 1 file changed, 47 insertions(+), 51 deletions(-)

diff --git a/branch.c b/branch.c
index 9429936734..6b9d64cdf9 100644
--- a/branch.c
+++ b/branch.c
@@ -209,57 +209,6 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
 	return 0;
 }
 
-static void validate_branch_start(struct repository *r, const char *start_name,
-				  enum branch_track track,
-				  struct object_id *oid, char **real_ref);
-
-void setup_tracking(const char *new_ref, const char *orig_ref,
-			   enum branch_track track, int quiet, int expand_orig)
-{
-	struct tracking tracking;
-	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
-	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
-	char *full_orig_ref;
-	struct object_id unused_oid;
-
-	memset(&tracking, 0, sizeof(tracking));
-	if (expand_orig)
-		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
-	else
-		full_orig_ref = xstrdup(orig_ref);
-
-	tracking.spec.dst = full_orig_ref;
-	tracking.srcs = &tracking_srcs;
-	if (track != BRANCH_TRACK_INHERIT)
-		for_each_remote(find_tracked_branch, &tracking);
-	else if (inherit_tracking(&tracking, orig_ref))
-		return;
-
-	if (!tracking.matches)
-		switch (track) {
-		case BRANCH_TRACK_ALWAYS:
-		case BRANCH_TRACK_EXPLICIT:
-		case BRANCH_TRACK_OVERRIDE:
-			break;
-		default:
-			goto cleanup;
-		}
-
-	if (tracking.matches > 1)
-		die(_("Not tracking: ambiguous information for ref %s"),
-		    full_orig_ref);
-
-	if (tracking.srcs->nr < 1)
-		string_list_append(tracking.srcs, full_orig_ref);
-	if (install_branch_config_multiple_remotes(config_flags, new_ref, tracking.remote,
-			      tracking.srcs) < 0)
-		exit(-1);
-
-cleanup:
-	string_list_clear(tracking.srcs, 0);
-	free(full_orig_ref);
-}
-
 int read_branch_desc(struct strbuf *buf, const char *branch_name)
 {
 	char *v = NULL;
@@ -407,6 +356,53 @@ static void validate_branch_start(struct repository *r, const char *start_name,
 	oidcpy(oid, &commit->object.oid);
 }
 
+void setup_tracking(const char *new_ref, const char *orig_ref,
+			   enum branch_track track, int quiet, int expand_orig)
+{
+	struct tracking tracking;
+	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
+	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
+	char *full_orig_ref;
+	struct object_id unused_oid;
+
+	memset(&tracking, 0, sizeof(tracking));
+	if (expand_orig)
+		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
+	else
+		full_orig_ref = xstrdup(orig_ref);
+
+	tracking.spec.dst = full_orig_ref;
+	tracking.srcs = &tracking_srcs;
+	if (track != BRANCH_TRACK_INHERIT)
+		for_each_remote(find_tracked_branch, &tracking);
+	else if (inherit_tracking(&tracking, orig_ref))
+		return;
+
+	if (!tracking.matches)
+		switch (track) {
+		case BRANCH_TRACK_ALWAYS:
+		case BRANCH_TRACK_EXPLICIT:
+		case BRANCH_TRACK_OVERRIDE:
+			break;
+		default:
+			goto cleanup;
+		}
+
+	if (tracking.matches > 1)
+		die(_("Not tracking: ambiguous information for ref %s"),
+		    full_orig_ref);
+
+	if (tracking.srcs->nr < 1)
+		string_list_append(tracking.srcs, full_orig_ref);
+	if (install_branch_config_multiple_remotes(config_flags, new_ref, tracking.remote,
+			      tracking.srcs) < 0)
+		exit(-1);
+
+cleanup:
+	string_list_clear(tracking.srcs, 0);
+	free(full_orig_ref);
+}
+
 void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
 		   int reflog, int quiet, enum branch_track track)
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v3 3/5] builtin/branch: clean up action-picking logic in cmd_branch()
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
  2021-12-09 18:49     ` [PATCH v3 1/5] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
  2021-12-09 18:49     ` [PATCH v3 2/5] branch: remove forward declaration of validate_branch_start() Glen Choo
@ 2021-12-09 18:49     ` Glen Choo
  2021-12-09 21:23       ` Jonathan Tan
  2021-12-09 18:49     ` [PATCH v3 4/5] branch: add --recurse-submodules option for branch creation Glen Choo
                       ` (3 subsequent siblings)
  6 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-09 18:49 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a variable to cmd_branch() that will tell us whether or not
cmd_branch() will default to creating a branch (instead of performing
another action). Besides making the function more explicit, this allows
us to validate options that can only be used when creating a branch.
Such an option does not exist yet, but one will be introduced in a
subsequent commit.

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).

Signed-off-by: Glen Choo <chooglen@google.com>
---
 builtin/branch.c | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index e19aab5356..14aff33a50 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -27,7 +27,7 @@
 
 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>] [-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>"),
@@ -616,14 +616,15 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, force = 0, list = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
+	int noncreate_actions = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, icase = 0;
 	const char *new_upstream = NULL;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting;
 	struct string_list sorting_options = STRING_LIST_INIT_DUP;
 	struct ref_format format = REF_FORMAT_INIT;
@@ -708,8 +709,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
+			    !!show_current + !!list + !!edit_description +
+			    !!unset_upstream;
+	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
 	if (filter.abbrev == -1)
@@ -847,7 +850,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
 		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>?"));
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v3 4/5] branch: add --recurse-submodules option for branch creation
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
                       ` (2 preceding siblings ...)
  2021-12-09 18:49     ` [PATCH v3 3/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
@ 2021-12-09 18:49     ` Glen Choo
  2021-12-11 18:08       ` Philippe Blain
  2021-12-09 18:49     ` [PATCH v3 5/5] branch.c: replace questionable exit() codes Glen Choo
                       ` (2 subsequent siblings)
  6 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-09 18:49 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

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 semantics e.g. `git
checkout` does not recursively checkout the expected branches created by
`git branch` yet. To ensure that the correct set of semantics is used,
this commit introduces a new configuration value,
`submodule.propagateBranches`, which enables submodule branching when
true (defaults to false).

This commit includes changes that allow Git to work with submodules
that are in trees (and not just the index):

* 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

Other changes

* add a "dry_run" parameter to create_branch() in order to support
  `git submodule--helper create-branch --dry-run`

Signed-off-by: Glen Choo <chooglen@google.com>
---
 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |   8 +
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 129 ++++++++++++-
 branch.h                           |  31 +++-
 builtin/branch.c                   |  40 +++-
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  35 ++++
 submodule-config.h                 |  35 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3207-branch-submodule.sh        | 284 +++++++++++++++++++++++++++++
 14 files changed, 609 insertions(+), 13 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index 063eec2511..e52262dc69 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` 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..52b35964c0 100644
--- a/Documentation/config/submodule.txt
+++ b/Documentation/config/submodule.txt
@@ -72,6 +72,14 @@ submodule.recurse::
 	For these commands a workaround is to temporarily change the
 	configuration value by using `git -c submodule.recurse=0`.
 
+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.
+
 submodule.fetchJobs::
 	Specifies how many submodules are fetched/cloned at the same time.
 	A positive integer allows up to that number of submodules fetched
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 6b9d64cdf9..305154de0b 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;
@@ -405,7 +407,7 @@ void setup_tracking(const char *new_ref, const char *orig_ref,
 
 void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track)
+		   int reflog, int quiet, enum branch_track track, int dry_run)
 {
 	struct object_id oid;
 	char *real_ref;
@@ -425,6 +427,8 @@ void create_branch(struct repository *r, const char *name,
 	}
 
 	validate_branch_start(r, start_name, track, &oid, &real_ref);
+	if (dry_run)
+		goto cleanup;
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -448,10 +452,133 @@ void create_branch(struct repository *r, const char *name,
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet, 0);
 
+cleanup:
 	strbuf_release(&ref);
 	free(real_ref);
 }
 
+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);
+	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. */
+	validate_branch_start(r, start_name, BRANCH_TRACK_NEVER, &super_oid,
+			      &branch_point);
+
+	/*
+	 * 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, 0);
+
+	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 46951c446b..8e28a9507f 100644
--- a/branch.h
+++ b/branch.h
@@ -49,12 +49,35 @@ void setup_tracking(const char *new_ref, const char *orig_ref,
  *   - track causes the new branch to be configured to merge the remote branch
  *     that start_name is a tracking branch for (if any).
  *
+ *   - dry_run causes the branch to be validated but not created.
+ *
  */
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track);
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   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 14aff33a50..9adbf70aa7 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -38,6 +38,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] = {
@@ -99,6 +101,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);
 }
 
@@ -621,7 +632,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	int noncreate_actions = 0;
 	/* possible options */
-	int reflog = 0, quiet = 0, icase = 0;
+	int reflog = 0, quiet = 0, icase = 0, recurse_submodules_explicit = 0;
 	const char *new_upstream = NULL;
 	enum branch_track track;
 	struct ref_filter filter;
@@ -673,6 +684,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(),
 	};
@@ -715,6 +727,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;
@@ -851,6 +874,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>?"));
@@ -858,10 +884,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);
-
+		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/checkout.c b/builtin/checkout.c
index 8d511aa6b7..fb4323dfca 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -893,7 +893,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				      opts->new_branch_force ? 1 : 0,
 				      opts->new_branch_log,
 				      opts->quiet,
-				      opts->track);
+				      opts->track,
+				      0);
 		new_branch_info->name = opts->new_branch;
 		setup_branch_path(new_branch_info);
 	}
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index e630f0c730..44b6283c08 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;
@@ -3389,6 +3426,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..f246359d63 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,40 @@ const struct submodule *submodule_from_path(struct repository *r,
 	return config_from(r->submodule_cache, treeish_name, path, lookup_path);
 }
 
+void submodules_of_tree(struct repository *r,
+			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;
+
+	name_entry = xmalloc(sizeof(*name_entry));
+
+	CALLOC_ARRAY(out->entries, 0);
+	out->entry_nr = 0;
+	out->entry_alloc = 0;
+
+	fill_tree_descriptor(r, &tree, treeish_name);
+	while (tree_entry(&tree, name_entry)) {
+		if (!S_ISGITLINK(name_entry->mode) || !is_tree_submodule_active(r, treeish_name, name_entry->path)) {
+			continue;
+		}
+
+		st_entry = xmalloc(sizeof(*st_entry));
+		st_entry->name_entry = name_entry;
+		st_entry->submodule =
+			submodule_from_path(r, treeish_name, name_entry->path);
+		st_entry->repo = xmalloc(sizeof(*st_entry->repo));
+		if (repo_submodule_init(st_entry->repo, r, name_entry->path,
+					treeish_name))
+			FREE_AND_NULL(st_entry->repo);
+
+		ALLOC_GROW(out->entries, out->entry_nr + 1, out->entry_alloc);
+		out->entries[out->entry_nr++] = *st_entry;
+	}
+}
+
 void submodule_free(struct repository *r)
 {
 	if (r->submodule_cache)
diff --git a/submodule-config.h b/submodule-config.h
index 65875b94ea..56c12af03f 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,38 @@ 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. This only reads
+ * one level of the tree, so it will not return nested submodules;
+ * callers that require nested submodules are expected to handle the
+ * recursion 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 c689070524..5ace18a7d9 100644
--- a/submodule.c
+++ b/submodule.c
@@ -267,7 +267,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;
@@ -275,7 +277,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)
@@ -317,6 +319,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..2dd0e2b01f
--- /dev/null
+++ b/t/t3207-branch-submodule.sh
@@ -0,0 +1,284 @@
+#!/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
+
+test_expect_success 'setup superproject and submodule' '
+	git init super &&
+	test_commit foo &&
+	git init sub-sub-upstream &&
+	test_commit -C sub-sub-upstream foo &&
+	git init sub-upstream &&
+	git -C sub-upstream submodule add "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
+	git -C sub-upstream commit -m "add submodule" &&
+	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
+	git -C super commit -m "add submodule" &&
+	git -C super config submodule.propagateBranches true &&
+	git -C super/sub submodule update --init
+'
+
+CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
+
+cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
+	#!/bin/sh
+
+	super_dir="$1"
+	shift
+	(
+		cd "$super_dir" &&
+		git checkout main &&
+		for branch_name in "$@"; do
+			git branch -D "$branch_name"
+			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
+		done
+	)
+EOF
+chmod +x "$CLEANUP_SCRIPT_PATH"
+
+cleanup_branches() {
+	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
+} >/dev/null 2>/dev/null
+
+# Test the argument parsing
+test_expect_success '--recurse-submodules should create branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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
+	)
+'
+
+test_expect_success '--recurse-submodules should die if submodule.propagateBranches is false' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should respect submodule.recurse when creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -c submodule.recurse=true branch branch-a &&
+		git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should ignore submodule.recurse when not creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git -c submodule.recurse=true branch -D branch-a &&
+		test_must_fail git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+# Test branch creation behavior
+test_expect_success 'should create branches based off commit id in superproject' '
+	test_when_finished "cleanup_branches super branch-a branch-b" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 &&
+
+		cat >expected <<-EOF &&
+		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
+		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
+		EOF
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branches if branch exists and --force is given' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -C sub rev-parse HEAD >expected &&
+		test_commit -C sub baz &&
+		git -C sub branch branch-a HEAD~1 &&
+		git branch --recurse-submodules --force branch-a &&
+		git rev-parse branch-a &&
+		# assert that sub:branch-a was moved
+		git -C sub rev-parse branch-a >actual &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branch when submodule is not in HEAD .gitmodules' '
+	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
+	(
+		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 rev-parse branch-c &&
+		git -C sub rev-parse branch-c &&
+		git checkout --recurse-submodules branch-c &&
+		git -C sub2 rev-parse branch-c
+	)
+'
+
+test_expect_success 'should set up tracking of local branches with track=always' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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)" = ""
+	)
+'
+
+test_expect_success 'should not create branches in inactive submodules' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	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 'setup remote-tracking tests' '
+	(
+		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
+'
+
+test_expect_success 'should not create branch when submodule is not in .git/modules' '
+	# The cleanup needs to delete sub2 separately because main does not have sub2
+	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
+		git -C super-clone/sub2/sub-sub branch -D branch-b && \
+		cleanup_branches super-clone branch-a branch-b" &&
+	(
+		cd super-clone &&
+		# This should succeed because super-clone has sub.
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		# This should fail because super-clone does not have sub2.
+		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
+		cat >expected <<-EOF &&
+		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
+		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
+		EOF
+		test_cmp expected 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 "cleanup_branches super-clone branch-a" &&
+	(
+		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 "cleanup_branches super-clone branch-a && \
+		git -C super-clone remote rename ex-origin origin" &&
+	(
+		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
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v3 5/5] branch.c: replace questionable exit() codes
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
                       ` (3 preceding siblings ...)
  2021-12-09 18:49     ` [PATCH v3 4/5] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-09 18:49     ` Glen Choo
  2021-12-10  2:21       ` Ævar Arnfjörð Bjarmason
  2021-12-09 21:59     ` [PATCH v3 0/5] implement branch --recurse-submodules Jonathan Tan
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
  6 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-09 18:49 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Replace exit() calls in branch.c that have questionable exit codes:

* in setup_tracking(), exit(-1) was introduced in 27852b2c53 (branch:
  report errors in tracking branch setup, 2016-02-22). This may have
  been a mechanical typo because the same commit changes the return type
  of setup_tracking() from int to void.

* in validate_branch_start(), the exit code changes depending on whether
  or not advice is enabled. This behavior was not discussed
  upstream (see caa2036b3b (branch: give advice when tracking
  start-point is missing, 2013-04-02)).

Signed-off-by: Glen Choo <chooglen@google.com>
---
I don't know what the 'correct' exit codes should be, only that Junio
makes a good case that the existing exit codes are wrong. My best,
non-prescriptive, choice is 128, to be consistent with the surrounding
code and Documentation/technical/api-error-handling.txt.

 branch.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/branch.c b/branch.c
index 305154de0b..ad70ddd120 100644
--- a/branch.c
+++ b/branch.c
@@ -324,7 +324,7 @@ static void validate_branch_start(struct repository *r, const char *start_name,
 			if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) {
 				error(_(upstream_missing), start_name);
 				advise(_(upstream_advice));
-				exit(1);
+				exit(128);
 			}
 			die(_(upstream_missing), start_name);
 		}
@@ -398,7 +398,7 @@ void setup_tracking(const char *new_ref, const char *orig_ref,
 		string_list_append(tracking.srcs, full_orig_ref);
 	if (install_branch_config_multiple_remotes(config_flags, new_ref, tracking.remote,
 			      tracking.srcs) < 0)
-		exit(-1);
+		exit(128);
 
 cleanup:
 	string_list_clear(tracking.srcs, 0);
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 1/5] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-09 18:49     ` [PATCH v3 1/5] branch: move --set-upstream-to behavior to setup_tracking() Glen Choo
@ 2021-12-09 21:19       ` Jonathan Tan
  2021-12-09 22:16         ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2021-12-09 21:19 UTC (permalink / raw)
  To: chooglen
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> This refactor is motivated by a desire to add a "dry_run" parameter to
> create_branch() that will validate whether or not a branch can be
> created without actually creating it - this behavior be used in a
> subsequent commit that adds `git branch --recurse-submodules topic`.

When I said that the patch was doing too much [1], I meant that for a
patch that is supposed to be about refactoring, there were many lines
that didn't seem to be a straightforward move. I've attached a version
of the patch that I expected - notice that a reviewer can see that the
lines are straightforwardly moved.

[1] https://lore.kernel.org/git/20211123231035.3607109-1-jonathantanmy@google.com/

> +void setup_tracking(const char *new_ref, const char *orig_ref,
> +			   enum branch_track track, int quiet, int expand_orig)
>  {
>  	struct tracking tracking;
>  	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
>  	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
> +	char *full_orig_ref;
> +	struct object_id unused_oid;
>  
>  	memset(&tracking, 0, sizeof(tracking));
> -	tracking.spec.dst = (char *)orig_ref;
> +	if (expand_orig)
> +		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
> +	else
> +		full_orig_ref = xstrdup(orig_ref);

Having two meanings for a parameter (and which meaning used depending on
another parameter) is quite confusing - I think it's better to call
another function to expand the ref if necessary. See my patch below for
an example.

> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
> index 267a624671..18e285a876 100755
> --- a/t/t3200-branch.sh
> +++ b/t/t3200-branch.sh
> @@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
>  	git branch abc && test_path_is_file .git/refs/heads/abc
>  '
>  
> +test_expect_success 'git branch abc should fail when abc exists' '
> +	test_must_fail git branch abc
> +'
> +
> +test_expect_success 'git branch --force abc should fail when abc is checked out' '
> +	test_when_finished git switch main &&
> +	git switch abc &&
> +	test_must_fail git branch --force abc HEAD~1
> +'
> +
> +test_expect_success 'git branch --force abc should succeed when abc exists' '
> +	git rev-parse HEAD~1 >expect &&
> +	git branch --force abc HEAD~1 &&
> +	git rev-parse abc >actual &&
> +	test_cmp expect actual
> +'

This seems like an unrelated test for a refactoring that doesn't seem to
touch "force" (in builtin/branch.c, the line removed hardcodes force to
0).

Below is my diff, as mentioned.

---
diff --git a/branch.c b/branch.c
index 07a46430b3..a6803e9900 100644
--- a/branch.c
+++ b/branch.c
@@ -131,8 +131,8 @@ int install_branch_config(int flag, const char *local, const char *origin, const
  * to infer the settings for branch.<new_ref>.{remote,merge} from the
  * config.
  */
-static void setup_tracking(const char *new_ref, const char *orig_ref,
-			   enum branch_track track, int quiet)
+void setup_tracking(const char *new_ref, const char *orig_ref,
+		    enum branch_track track, int quiet)
 {
 	struct tracking tracking;
 	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
@@ -243,30 +243,12 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+void TODO_name(struct repository *r, const char *start_name, int explicit_tracking,
+	       char **out_real_ref, struct commit **out_commit)
 {
 	struct commit *commit;
 	struct object_id oid;
 	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
-	int explicit_tracking = 0;
-
-	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
-		explicit_tracking = 1;
-
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
 
 	real_ref = NULL;
 	if (get_oid_mb(start_name, &oid)) {
@@ -304,6 +286,38 @@ void create_branch(struct repository *r,
 
 	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
 		die(_("Not a valid branch point: '%s'."), start_name);
+	if (out_real_ref)
+		*out_real_ref = real_ref;
+	if (out_commit)
+		*out_commit = commit;
+}
+
+void create_branch(struct repository *r,
+		   const char *name, const char *start_name,
+		   int force, int clobber_head_ok, int reflog,
+		   int quiet, enum branch_track track)
+{
+	struct commit *commit;
+	struct object_id oid;
+	char *real_ref = NULL;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	int dont_change_ref = 0;
+	int explicit_tracking = 0;
+
+	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
+		explicit_tracking = 1;
+
+	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
+	    ? validate_branchname(name, &ref)
+	    : validate_new_branchname(name, &ref, force)) {
+		if (!force)
+			dont_change_ref = 1;
+		else
+			forcing = 1;
+	}
+
+	TODO_name(r, start_name, explicit_tracking, &real_ref, &commit);
 	oidcpy(&oid, &commit->object.oid);
 
 	if (reflog)
diff --git a/branch.h b/branch.h
index df0be61506..577483012b 100644
--- a/branch.h
+++ b/branch.h
@@ -1,6 +1,7 @@
 #ifndef BRANCH_H
 #define BRANCH_H
 
+struct commit;
 struct repository;
 struct strbuf;
 
@@ -17,6 +18,11 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+void setup_tracking(const char *new_ref, const char *orig_ref,
+		    enum branch_track track, int quiet);
+void TODO_name(struct repository *r, const char *start_name, int explicit_tracking,
+	       char **out_real_ref, struct commit **out_commit);
+
 /*
  * Creates a new branch, where:
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index 81b5c111cb..78f5c1d17e 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -806,6 +806,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 			die(_("too many arguments for a rename operation"));
 	} else if (new_upstream) {
 		struct branch *branch = branch_get(argv[0]);
+		char *real_ref;
 
 		if (argc > 1)
 			die(_("too many arguments to set new upstream"));
@@ -821,12 +822,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		TODO_name(the_repository, new_upstream, 1, &real_ref, NULL);
+		setup_tracking(branch->name, real_ref, BRANCH_TRACK_OVERRIDE, quiet);
+		free(real_ref);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 3/5] builtin/branch: clean up action-picking logic in cmd_branch()
  2021-12-09 18:49     ` [PATCH v3 3/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
@ 2021-12-09 21:23       ` Jonathan Tan
  2021-12-09 21:57         ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2021-12-09 21:23 UTC (permalink / raw)
  To: chooglen
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> 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).

I think that we implement such incidental fixes only when we're touching
the relevant lines, but this change looks correct.

> -	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
> -	int show_current = 0;
> -	int reflog = 0, edit_description = 0;
> -	int quiet = 0, unset_upstream = 0;
> +	/* possible actions */
> +	int delete = 0, rename = 0, copy = 0, force = 0, list = 0,
> +	    unset_upstream = 0, show_current = 0, edit_description = 0;
> +	int noncreate_actions = 0;
> +	/* possible options */
> +	int reflog = 0, quiet = 0, icase = 0;

[snip]

> -	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
> -	    list + edit_description + unset_upstream > 1)
> +	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
> +			    !!show_current + !!list + !!edit_description +
> +			    !!unset_upstream;
> +	if (noncreate_actions > 1)
>  		usage_with_options(builtin_branch_usage, options);

Overall this change looks good, although if you're going to rearrange
the variable declarations (e.g. the positions of show_current,
edit_description, and unset_upstream have moved), you might as well make
them consistent with the noncreate_actions statement, I guess. Also
maybe move new_upstream closer.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 3/5] builtin/branch: clean up action-picking logic in cmd_branch()
  2021-12-09 21:23       ` Jonathan Tan
@ 2021-12-09 21:57         ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-09 21:57 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Jonathan Tan <jonathantanmy@google.com> writes:

>> 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).
>
> I think that we implement such incidental fixes only when we're touching
> the relevant lines, but this change looks correct.

That's fair. This fix is such low-hanging fruit that I don't think it
deserves its patch, but if others agree, I'll separate it.

>
>> -	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
>> -	int show_current = 0;
>> -	int reflog = 0, edit_description = 0;
>> -	int quiet = 0, unset_upstream = 0;
>> +	/* possible actions */
>> +	int delete = 0, rename = 0, copy = 0, force = 0, list = 0,
>> +	    unset_upstream = 0, show_current = 0, edit_description = 0;
>> +	int noncreate_actions = 0;
>> +	/* possible options */
>> +	int reflog = 0, quiet = 0, icase = 0;
>
> [snip]
>
>> -	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
>> -	    list + edit_description + unset_upstream > 1)
>> +	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
>> +			    !!show_current + !!list + !!edit_description +
>> +			    !!unset_upstream;
>> +	if (noncreate_actions > 1)
>>  		usage_with_options(builtin_branch_usage, options);
>
> Overall this change looks good, although if you're going to rearrange
> the variable declarations (e.g. the positions of show_current,
> edit_description, and unset_upstream have moved), you might as well make
> them consistent with the noncreate_actions statement, I guess. Also
> maybe move new_upstream closer.

Yeah this is obviously inconsistent, thanks for the catch.

* force isn't an action
* new_upstream is an action
* for QoL, all of the actions should be listed in the same order at both
  sites

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 0/5] implement branch --recurse-submodules
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
                       ` (4 preceding siblings ...)
  2021-12-09 18:49     ` [PATCH v3 5/5] branch.c: replace questionable exit() codes Glen Choo
@ 2021-12-09 21:59     ` Jonathan Tan
  2021-12-09 22:21       ` Glen Choo
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
  6 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2021-12-09 21:59 UTC (permalink / raw)
  To: chooglen
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> This version is functionally identical to v2. I've only addressed
> feedback around code organization, i.e. the the merge conflict with
> js/branch-track-inherit and making patch 1 easier to review. Thus, some
> discussions on [1] are still unaddressed.

I do notice that some of my comments on "add --recurse-submodules option
for branch creation" are still unaddressed so I'll hold off review until
they are.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 1/5] branch: move --set-upstream-to behavior to setup_tracking()
  2021-12-09 21:19       ` Jonathan Tan
@ 2021-12-09 22:16         ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-09 22:16 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Jonathan Tan <jonathantanmy@google.com> writes:

>> This refactor is motivated by a desire to add a "dry_run" parameter to
>> create_branch() that will validate whether or not a branch can be
>> created without actually creating it - this behavior be used in a
>> subsequent commit that adds `git branch --recurse-submodules topic`.
>
> When I said that the patch was doing too much [1], I meant that for a
> patch that is supposed to be about refactoring, there were many lines
> that didn't seem to be a straightforward move. I've attached a version
> of the patch that I expected - notice that a reviewer can see that the
> lines are straightforwardly moved.
>
> [1] https://lore.kernel.org/git/20211123231035.3607109-1-jonathantanmy@google.com/

Thanks, the patch gave me a lot of think about.

Comparing yours and mine side-by-side makes it obvious that what I
thought was a 'simple' refactor is not so obvious to readers. I think I
can incorporate most of your patch.

>> +void setup_tracking(const char *new_ref, const char *orig_ref,
>> +			   enum branch_track track, int quiet, int expand_orig)
>>  {
>>  	struct tracking tracking;
>>  	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
>>  	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
>> +	char *full_orig_ref;
>> +	struct object_id unused_oid;
>>  
>>  	memset(&tracking, 0, sizeof(tracking));
>> -	tracking.spec.dst = (char *)orig_ref;
>> +	if (expand_orig)
>> +		validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref);
>> +	else
>> +		full_orig_ref = xstrdup(orig_ref);
>
> Having two meanings for a parameter (and which meaning used depending on
> another parameter) is quite confusing - I think it's better to call
> another function to expand the ref if necessary. See my patch below for
> an example.

Since you and Junio have both commented on this inconsistency, I'll drop
the extra parameter.

I wonder if you've considered another alternative [1], which is to
_always_ expand the ref, instead of _never_ expanding the ref. Always
expanding the ref wastes cycles, but it avoids creating an implicit
contract between setup_tracking() and
validate_branch_start_or_TODO_name().

>> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
>> index 267a624671..18e285a876 100755
>> --- a/t/t3200-branch.sh
>> +++ b/t/t3200-branch.sh
>> @@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
>>  	git branch abc && test_path_is_file .git/refs/heads/abc
>>  '
>>  
>> +test_expect_success 'git branch abc should fail when abc exists' '
>> +	test_must_fail git branch abc
>> +'
>> +
>> +test_expect_success 'git branch --force abc should fail when abc is checked out' '
>> +	test_when_finished git switch main &&
>> +	git switch abc &&
>> +	test_must_fail git branch --force abc HEAD~1
>> +'
>> +
>> +test_expect_success 'git branch --force abc should succeed when abc exists' '
>> +	git rev-parse HEAD~1 >expect &&
>> +	git branch --force abc HEAD~1 &&
>> +	git rev-parse abc >actual &&
>> +	test_cmp expect actual
>> +'
>
> This seems like an unrelated test for a refactoring that doesn't seem to
> touch "force" (in builtin/branch.c, the line removed hardcodes force to
> 0).

I added this test because I was trying to simplify create_branch() by
moving around/deleting forcing and clobber_head_ok. It turns out that I
had actually broken the behavior of --force, but the test suite didn't
catch it.

I think this test is a step in the right direction, but it probably
falls into the category of 'incidental fix'. I am not sure where it
belongs if not here.

> Below is my diff, as mentioned.
>
> ---
> diff --git a/branch.c b/branch.c
> index 07a46430b3..a6803e9900 100644
> --- a/branch.c
> +++ b/branch.c
> @@ -131,8 +131,8 @@ int install_branch_config(int flag, const char *local, const char *origin, const
>   * to infer the settings for branch.<new_ref>.{remote,merge} from the
>   * config.
>   */
> -static void setup_tracking(const char *new_ref, const char *orig_ref,
> -			   enum branch_track track, int quiet)
> +void setup_tracking(const char *new_ref, const char *orig_ref,
> +		    enum branch_track track, int quiet)
>  {
>  	struct tracking tracking;
>  	int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE;
> @@ -243,30 +243,12 @@ N_("\n"
>  "will track its remote counterpart, you may want to use\n"
>  "\"git push -u\" to set the upstream config as you push.");
>  
> -void create_branch(struct repository *r,
> -		   const char *name, const char *start_name,
> -		   int force, int clobber_head_ok, int reflog,
> -		   int quiet, enum branch_track track)
> +void TODO_name(struct repository *r, const char *start_name, int explicit_tracking,
> +	       char **out_real_ref, struct commit **out_commit)
>  {
>  	struct commit *commit;
>  	struct object_id oid;
>  	char *real_ref;
> -	struct strbuf ref = STRBUF_INIT;
> -	int forcing = 0;
> -	int dont_change_ref = 0;
> -	int explicit_tracking = 0;
> -
> -	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
> -		explicit_tracking = 1;
> -
> -	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
> -	    ? validate_branchname(name, &ref)
> -	    : validate_new_branchname(name, &ref, force)) {
> -		if (!force)
> -			dont_change_ref = 1;
> -		else
> -			forcing = 1;
> -	}
>  
>  	real_ref = NULL;
>  	if (get_oid_mb(start_name, &oid)) {
> @@ -304,6 +286,38 @@ void create_branch(struct repository *r,
>  
>  	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
>  		die(_("Not a valid branch point: '%s'."), start_name);
> +	if (out_real_ref)
> +		*out_real_ref = real_ref;
> +	if (out_commit)
> +		*out_commit = commit;
> +}

Using a separate out_real_ref makes a lot more sense than reusing
real_ref. I'll borrow this :)

> +
> +void create_branch(struct repository *r,
> +		   const char *name, const char *start_name,
> +		   int force, int clobber_head_ok, int reflog,
> +		   int quiet, enum branch_track track)
> +{
> +	struct commit *commit;
> +	struct object_id oid;
> +	char *real_ref = NULL;
> +	struct strbuf ref = STRBUF_INIT;
> +	int forcing = 0;
> +	int dont_change_ref = 0;
> +	int explicit_tracking = 0;
> +
> +	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
> +		explicit_tracking = 1;
> +
> +	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
> +	    ? validate_branchname(name, &ref)
> +	    : validate_new_branchname(name, &ref, force)) {
> +		if (!force)
> +			dont_change_ref = 1;
> +		else
> +			forcing = 1;
> +	}

This patch just moves things around without performing internal
simplification (like removing dont_change_ref). The simplification would
probably be done in a later patch.

Makes sense, I see how this is easier to review.

> @@ -821,12 +822,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>  		if (!ref_exists(branch->refname))
>  			die(_("branch '%s' does not exist"), branch->name);
>  
> -		/*
> -		 * create_branch takes care of setting up the tracking
> -		 * info and making sure new_upstream is correct
> -		 */
> -		create_branch(the_repository, branch->name, new_upstream,
> -			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
> +		TODO_name(the_repository, new_upstream, 1, &real_ref, NULL);
> +		setup_tracking(branch->name, real_ref, BRANCH_TRACK_OVERRIDE, quiet);
> +		free(real_ref);
>  	} else if (unset_upstream) {
>  		struct branch *branch = branch_get(argv[0]);
>  		struct strbuf buf = STRBUF_INIT;

See my previous comment and [1] about avoiding the implicit contract of
TODO_name() + setup_tracking(), but of course, this is still more
consistent than what what I proposed.

[1] https://lore.kernel.org/git/kl6l5yrzaq5z.fsf@chooglen-macbookpro.roam.corp.google.com

Thanks!

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 0/5] implement branch --recurse-submodules
  2021-12-09 21:59     ` [PATCH v3 0/5] implement branch --recurse-submodules Jonathan Tan
@ 2021-12-09 22:21       ` Glen Choo
  2021-12-13 23:20         ` Jonathan Tan
  0 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-09 22:21 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Jonathan Tan <jonathantanmy@google.com> writes:

> Glen Choo <chooglen@google.com> writes:
>> This version is functionally identical to v2. I've only addressed
>> feedback around code organization, i.e. the the merge conflict with
>> js/branch-track-inherit and making patch 1 easier to review. Thus, some
>> discussions on [1] are still unaddressed.
>
> I do notice that some of my comments on "add --recurse-submodules option
> for branch creation" are still unaddressed so I'll hold off review until
> they are.

Are you referring to your comments on v1 [1]? If so, I believe I had
addressed them all in v2 (and v3 is mostly a reorganization of v2).

Let me know what you think is unaddressed :)

[1] https://lore.kernel.org/git/20211124013153.3619998-1-jonathantanmy@google.com

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 5/5] branch.c: replace questionable exit() codes
  2021-12-09 18:49     ` [PATCH v3 5/5] branch.c: replace questionable exit() codes Glen Choo
@ 2021-12-10  2:21       ` Ævar Arnfjörð Bjarmason
  2021-12-10 17:43         ` Glen Choo
  2021-12-13  9:02         ` Junio C Hamano
  0 siblings, 2 replies; 110+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-12-10  2:21 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer, Philippe Blain,
	Junio C Hamano


On Thu, Dec 09 2021, Glen Choo wrote:

> Replace exit() calls in branch.c that have questionable exit codes:
>
> * in setup_tracking(), exit(-1) was introduced in 27852b2c53 (branch:
>   report errors in tracking branch setup, 2016-02-22). This may have
>   been a mechanical typo because the same commit changes the return type
>   of setup_tracking() from int to void.
>
> * in validate_branch_start(), the exit code changes depending on whether
>   or not advice is enabled. This behavior was not discussed
>   upstream (see caa2036b3b (branch: give advice when tracking
>   start-point is missing, 2013-04-02)).
>
> Signed-off-by: Glen Choo <chooglen@google.com>
> ---
> I don't know what the 'correct' exit codes should be, only that Junio
> makes a good case that the existing exit codes are wrong. My best,
> non-prescriptive, choice is 128, to be consistent with the surrounding
> code and Documentation/technical/api-error-handling.txt.
>
>  branch.c | 4 ++--
>  1 file changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/branch.c b/branch.c
> index 305154de0b..ad70ddd120 100644
> --- a/branch.c
> +++ b/branch.c
> @@ -324,7 +324,7 @@ static void validate_branch_start(struct repository *r, const char *start_name,
>  			if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) {
>  				error(_(upstream_missing), start_name);
>  				advise(_(upstream_advice));
> -				exit(1);
> +				exit(128);
>  			}
>  			die(_(upstream_missing), start_name);
>  		}
> @@ -398,7 +398,7 @@ void setup_tracking(const char *new_ref, const char *orig_ref,
>  		string_list_append(tracking.srcs, full_orig_ref);
>  	if (install_branch_config_multiple_remotes(config_flags, new_ref, tracking.remote,
>  			      tracking.srcs) < 0)
> -		exit(-1);
> +		exit(128);
>  
>  cleanup:
>  	string_list_clear(tracking.srcs, 0);

Junio noted in <xmqqbl1tcptq.fsf@gitster.g>:
    
    This is not a problem with this patch, and it should not be fixed as
    part of this series, but since I noticed it, I'll mention it as a
    leftover low-hanging fruit to be fixed after the dust settles.  The
    exit(1) looks wrong.  We should exit with 128 just like die() does.
    Issuing of an advice message should not affect the exit code.

I think it's good to fix these inconsistencies, but also that we
shouldn't be doing it as part of this series, or does it conflict in
some way that's hard to untangle?

FWIW the former hunk is a perfect candidate for the new die_message()
function[1]. I.e. we should be doing:

    int code = die_message(_(upsream_missing), start_name);
    if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE))
        advise(_(upstream_advice));
    exit(code);

That we print an "error" when giving the advice but "fatal" when not is
really UX wart, and also that the exit code differs.

The latter should really be "exit(1)", not 128. We should reserve that
for die(). FWIW I had some local hacks to detect all these cases of exit
-1 via the test suite, they're almost all cases where we want to exit
with 1, but just conflated an error() return value with a return from
main() (or exit).

1. https://lore.kernel.org/git/cover-v2-0.6-00000000000-20211207T182419Z-avarab@gmail.com/#t

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 5/5] branch.c: replace questionable exit() codes
  2021-12-10  2:21       ` Ævar Arnfjörð Bjarmason
@ 2021-12-10 17:43         ` Glen Choo
  2021-12-13  9:02         ` Junio C Hamano
  1 sibling, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-10 17:43 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer, Philippe Blain,
	Junio C Hamano

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Thu, Dec 09 2021, Glen Choo wrote:
>
>> Replace exit() calls in branch.c that have questionable exit codes:
>>
>> * in setup_tracking(), exit(-1) was introduced in 27852b2c53 (branch:
>>   report errors in tracking branch setup, 2016-02-22). This may have
>>   been a mechanical typo because the same commit changes the return type
>>   of setup_tracking() from int to void.
>>
>> * in validate_branch_start(), the exit code changes depending on whether
>>   or not advice is enabled. This behavior was not discussed
>>   upstream (see caa2036b3b (branch: give advice when tracking
>>   start-point is missing, 2013-04-02)).
>>
>> Signed-off-by: Glen Choo <chooglen@google.com>
>> ---
>> I don't know what the 'correct' exit codes should be, only that Junio
>> makes a good case that the existing exit codes are wrong. My best,
>> non-prescriptive, choice is 128, to be consistent with the surrounding
>> code and Documentation/technical/api-error-handling.txt.
>>
>>  branch.c | 4 ++--
>>  1 file changed, 2 insertions(+), 2 deletions(-)
>>
>> diff --git a/branch.c b/branch.c
>> index 305154de0b..ad70ddd120 100644
>> --- a/branch.c
>> +++ b/branch.c
>> @@ -324,7 +324,7 @@ static void validate_branch_start(struct repository *r, const char *start_name,
>>  			if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) {
>>  				error(_(upstream_missing), start_name);
>>  				advise(_(upstream_advice));
>> -				exit(1);
>> +				exit(128);
>>  			}
>>  			die(_(upstream_missing), start_name);
>>  		}
>> @@ -398,7 +398,7 @@ void setup_tracking(const char *new_ref, const char *orig_ref,
>>  		string_list_append(tracking.srcs, full_orig_ref);
>>  	if (install_branch_config_multiple_remotes(config_flags, new_ref, tracking.remote,
>>  			      tracking.srcs) < 0)
>> -		exit(-1);
>> +		exit(128);
>>  
>>  cleanup:
>>  	string_list_clear(tracking.srcs, 0);
>
> Junio noted in <xmqqbl1tcptq.fsf@gitster.g>:
>     
>     This is not a problem with this patch, and it should not be fixed as
>     part of this series, but since I noticed it, I'll mention it as a
>     leftover low-hanging fruit to be fixed after the dust settles.  The
>     exit(1) looks wrong.  We should exit with 128 just like die() does.
>     Issuing of an advice message should not affect the exit code.
>
> I think it's good to fix these inconsistencies, but also that we
> shouldn't be doing it as part of this series, or does it conflict in
> some way that's hard to untangle?

There isn't any conflict. Probably a leftover habit from previous
projects, but I thought that this would be right time to clean up. Looks
like we think it'll be better to clean this up in a separate series, so
I'll do that instead.

> FWIW the former hunk is a perfect candidate for the new die_message()
> function[1]. I.e. we should be doing:
>
>     int code = die_message(_(upsream_missing), start_name);
>     if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE))
>         advise(_(upstream_advice));
>     exit(code);
>
> That we print an "error" when giving the advice but "fatal" when not is
> really UX wart, and also that the exit code differs.

Ah, thanks!

> The latter should really be "exit(1)", not 128. We should reserve that
> for die().

Thanks, this is exactly what I was looking for guidance on.
Documentation/technical/api-error-handling.txt is silent on what exit
code to use when a command does 90% of what the caller wants (so it's
not really an application error) but fails on the 10% that the user
doesn't care so much about - in this case, creating a branch but failing
to setup tracking.

> FWIW I had some local hacks to detect all these cases of exit -1 via
> the test suite, they're almost all cases where we want to exit with 1,
> but just conflated an error() return value with a return from main()
> (or exit).

Yes, this sounds like what happened here.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 4/5] branch: add --recurse-submodules option for branch creation
  2021-12-09 18:49     ` [PATCH v3 4/5] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-11 18:08       ` Philippe Blain
  2021-12-14 20:08         ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Philippe Blain @ 2021-12-11 18:08 UTC (permalink / raw)
  To: Glen Choo, git
  Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Junio C Hamano

Hi Glen,

Le 2021-12-09 à 13:49, Glen Choo a écrit :
> 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 semantics e.g. `git
> checkout` does not recursively checkout the expected branches created by
> `git branch` yet. To ensure that the correct set of semantics is used,
> this commit introduces a new configuration value,
> `submodule.propagateBranches`, which enables submodule branching when
> true (defaults to false).
> 
> This commit includes changes that allow Git to work with submodules
> that are in trees (and not just the index):
> 
> * 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
> 
> Other changes
> 
> * add a "dry_run" parameter to create_branch() in order to support
>    `git submodule--helper create-branch --dry-run`
> 
> Signed-off-by: Glen Choo <chooglen@google.com>
> ---
>   Documentation/config/advice.txt    |   3 +
>   Documentation/config/submodule.txt |   8 +

Same comment as I remarked in v1 [1]:

We would need to add the new flag to Documentation/git-branch.txt,
and also probably update the documentation of 'submodule.recurse'
in 'Documentation/config/submodule.txt'.

[1] https://lore.kernel.org/git/3ad3941c-de18-41bf-2e44-4238ae868d79@gmail.com/

>   advice.c                           |   1 +
>   advice.h                           |   1 +
>   branch.c                           | 129 ++++++++++++-
>   branch.h                           |  31 +++-
>   builtin/branch.c                   |  40 +++-
>   builtin/checkout.c                 |   3 +-
>   builtin/submodule--helper.c        |  38 ++++
>   submodule-config.c                 |  35 ++++
>   submodule-config.h                 |  35 ++++
>   submodule.c                        |  11 +-
>   submodule.h                        |   3 +
>   t/t3207-branch-submodule.sh        | 284 +++++++++++++++++++++++++++++
>   14 files changed, 609 insertions(+), 13 deletions(-)
>   create mode 100755 t/t3207-branch-submodule.sh
> 
> diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
> index 063eec2511..e52262dc69 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` was not run.

I see you added '--init' in the actual message below, but maybe it would be more accurate
to also add it here ?

>   	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..52b35964c0 100644
> --- a/Documentation/config/submodule.txt
> +++ b/Documentation/config/submodule.txt
> @@ -72,6 +72,14 @@ submodule.recurse::
>   	For these commands a workaround is to temporarily change the
>   	configuration value by using `git -c submodule.recurse=0`.
>   
> +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.
> +

Thanks, that updated description seems simpler and more accurate with what
this series is doing.

>   submodule.fetchJobs::
>   	Specifies how many submodules are fetched/cloned at the same time.
>   	A positive integer allows up to that number of submodules fetched

> diff --git a/branch.c b/branch.c
> index 6b9d64cdf9..305154de0b 100644
> --- a/branch.c
> +++ b/branch.c
> @@ -8,6 +8,8 @@


> +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. */
> +	validate_branch_start(r, start_name, BRANCH_TRACK_NEVER, &super_oid,
> +			      &branch_point);
> +
> +	/*
> +	 * 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"),

small nit, maybe write: "unable to find submodule repository" ?

> +			    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))

Here, we do not actually use the dry_run argument, since we *always* want to
do a dry run for the submodules...

> +			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);

... whereas for the superproject branch, we use the given dry_run argument...

> +	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, 0);
> +
> +	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))

... and if !dry_run, then we do create the branches in the submodules.

That is a little bit hard to follow if you are not careful. Maybe it's just me,
but as I was first reading it I wondered why '0' and '1' were hard-coded as the dry-run
arguments to submodule_create_branch... It would maybe be clearer to use a named
variable ?


> @@ -851,6 +874,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>?"));
> @@ -858,10 +884,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);
> -
> +		if (recurse_submodules) {
> +			create_branches_recursively(the_repository, branch_name,
> +						    start_name, NULL, force,
> +						    reflog, quiet, track, 0);

Here again, maybe it would be clearer to use a named variable instead of hard-coding '0' ?

> +			return 0;
> +		}
> +		create_branch(the_repository, branch_name, start_name, force, 0,
> +			      reflog, quiet, track, 0);

Same here.

>   	} else
>   		usage_with_options(builtin_branch_usage, options);
>   

> diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
> new file mode 100755
> index 0000000000..2dd0e2b01f
> --- /dev/null
> +++ b/t/t3207-branch-submodule.sh

The tests look pretty thourough.


> +
> +test_expect_success 'should create branch when submodule is not in HEAD .gitmodules' '

micro-nit: maybe write: HEAD:.gitmodules as this is revision synatx.


Cheers,

Philippe.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 5/5] branch.c: replace questionable exit() codes
  2021-12-10  2:21       ` Ævar Arnfjörð Bjarmason
  2021-12-10 17:43         ` Glen Choo
@ 2021-12-13  9:02         ` Junio C Hamano
  2021-12-13  9:19           ` Ævar Arnfjörð Bjarmason
  1 sibling, 1 reply; 110+ messages in thread
From: Junio C Hamano @ 2021-12-13  9:02 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: Glen Choo, git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Philippe Blain

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> The latter should really be "exit(1)", not 128. We should reserve that
> for die().

Is it because install_branch_config_multiple_remotes() gives enough
information to the user that the caller exits without its own
message?  In other words, are messages given by the callee to the
users are morally equivalent to what the caller would call die()
with, if the callee were silent?  If so, 128 is perfectly fine.  If
not, 1 or anything positive that is not 128 may be more appropriate.

Either case, -1 is a definite no-no.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 5/5] branch.c: replace questionable exit() codes
  2021-12-13  9:02         ` Junio C Hamano
@ 2021-12-13  9:19           ` Ævar Arnfjörð Bjarmason
  2021-12-13 19:26             ` Junio C Hamano
  0 siblings, 1 reply; 110+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-12-13  9:19 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Glen Choo, git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Philippe Blain


On Mon, Dec 13 2021, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>> The latter should really be "exit(1)", not 128. We should reserve that
>> for die().
>
> Is it because install_branch_config_multiple_remotes() gives enough
> information to the user that the caller exits without its own
> message?  In other words, are messages given by the callee to the
> users are morally equivalent to what the caller would call die()
> with, if the callee were silent?  If so, 128 is perfectly fine.  If
> not, 1 or anything positive that is not 128 may be more appropriate.

We don't really document this outside of this tidbit:
    
    Documentation/technical/api-error-handling.txt-- `die` is for fatal application errors.  It prints a message to
    Documentation/technical/api-error-handling.txt:  the user and exits with status 128.
    Documentation/technical/api-error-handling.txt-
    Documentation/technical/api-error-handling.txt-- `usage` is for errors in command line usage.  After printing its
    Documentation/technical/api-error-handling.txt-  message, it exits with status 129.  (See also `usage_with_options`
    Documentation/technical/api-error-handling.txt-  in the link:api-parse-options.html[parse-options API].)

But while that doesn't say that you can *only* use 128 for die, and I
wouldn't consider the existing code that calls exit(128) in dire need of
a change, most of the builtins simply return with 1 for generic errors,
and reserve 128 for die()..

So for any new code it makes sense to follow that convention.

> Either case, -1 is a definite no-no.

I've got a local WIP patch to fix those that I can polish up, if you're
interested. It's the result of adding the below & running the test suite
against it:

diff --git a/git.c b/git.c
index 60c2784be45..d6bdb3571df 100644
--- a/git.c
+++ b/git.c
@@ -419,6 +419,7 @@ static int handle_alias(int *argcp, const char ***argv)
 static int run_builtin(struct cmd_struct *p, int argc, const char **argv)
 {
 	int status, help;
+	int posix_status;
 	struct stat st;
 	const char *prefix;
 
@@ -459,6 +460,9 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)
 
 	validate_cache_entries(the_repository->index);
 	status = p->fn(argc, argv, prefix);
+	posix_status = status & 0xFF;
+	if (status != posix_status)
+		BUG("got status %d which will be cast to %d, returning error() perhaps?", status, posix_status);
 	validate_cache_entries(the_repository->index);
 
 	if (status)

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 5/5] branch.c: replace questionable exit() codes
  2021-12-13  9:19           ` Ævar Arnfjörð Bjarmason
@ 2021-12-13 19:26             ` Junio C Hamano
  0 siblings, 0 replies; 110+ messages in thread
From: Junio C Hamano @ 2021-12-13 19:26 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: Glen Choo, git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Philippe Blain

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Mon, Dec 13 2021, Junio C Hamano wrote:
>
>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>
>>> The latter should really be "exit(1)", not 128. We should reserve that
>>> for die().
>>
>> Is it because install_branch_config_multiple_remotes() gives enough
>> information to the user that the caller exits without its own
>> message?  In other words, are messages given by the callee to the
>> users are morally equivalent to what the caller would call die()
>> with, if the callee were silent?  If so, 128 is perfectly fine.  If
>> not, 1 or anything positive that is not 128 may be more appropriate.
>
> We don't really document this outside of this tidbit:
>     
>     Documentation/technical/api-error-handling.txt-- `die` is for fatal application errors.  It prints a message to
>     Documentation/technical/api-error-handling.txt:  the user and exits with status 128.
>     Documentation/technical/api-error-handling.txt-
>     Documentation/technical/api-error-handling.txt-- `usage` is for errors in command line usage.  After printing its
>     Documentation/technical/api-error-handling.txt-  message, it exits with status 129.  (See also `usage_with_options`
>     Documentation/technical/api-error-handling.txt-  in the link:api-parse-options.html[parse-options API].)
>
> But while that doesn't say that you can *only* use 128 for die, and I
> wouldn't consider the existing code that calls exit(128) in dire need of
> a change, most of the builtins simply return with 1 for generic errors,
> and reserve 128 for die()..
>
> So for any new code it makes sense to follow that convention.

Only when they are not calling die() for some technical reasons,
though.  IOW, if you would have called die() if you could, that is a
good indication that you would want to consistently use 128.

Capturing return value from die_message(), giving more message and
then dying with the 128 squarely falls into that pattern.  I am not
sure if the install_branch_config_multiple_remotes() case falls into
it, though.

And more importantly in the context of this topic, I am not
convinced install_branch_config_multiple_remotes() helper function
itself is a good idea to begin with.  It is to handle a case where
branch.$name.remote is set multiple times right?

This is because I do not think I saw anybody defined the right
semantics during the discussion (or written in the documentation)
and explained why being able to do so makes sense in the first
place, and it is not known if it makes sense to copy such a
configuration blindly to a new branch.  If it punts without doing
anything with a warning(), or calls a die(), it would be a more
sensible first step for this topic.  Users with real need for such a
configuration will then come to us with real use case, and what they
need may turn out to be something different from a blind copying of
the original.

Thanks.




^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 0/5] implement branch --recurse-submodules
  2021-12-09 22:21       ` Glen Choo
@ 2021-12-13 23:20         ` Jonathan Tan
  2021-12-14 18:47           ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2021-12-13 23:20 UTC (permalink / raw)
  To: chooglen
  Cc: jonathantanmy, git, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> Jonathan Tan <jonathantanmy@google.com> writes:
> 
> > Glen Choo <chooglen@google.com> writes:
> >> This version is functionally identical to v2. I've only addressed
> >> feedback around code organization, i.e. the the merge conflict with
> >> js/branch-track-inherit and making patch 1 easier to review. Thus, some
> >> discussions on [1] are still unaddressed.
> >
> > I do notice that some of my comments on "add --recurse-submodules option
> > for branch creation" are still unaddressed so I'll hold off review until
> > they are.
> 
> Are you referring to your comments on v1 [1]? If so, I believe I had
> addressed them all in v2 (and v3 is mostly a reorganization of v2).
> 
> Let me know what you think is unaddressed :)
> 
> [1] https://lore.kernel.org/git/20211124013153.3619998-1-jonathantanmy@google.com

Ah...I just saw that you are creating branches through a remote helper
[1] and are still using tree_entry() non-recursively [2] (to be
specific, I think we need a test where the submodule is at
$ROOT/foo/bar, not just $ROOT/foo), and saw your cover letter that said
that some comments were unaddressed, and jumped to a conclusion. Looking
back, I think that these are my only unaddressed comments.

[1] I said "If you want to use setup_tracking() through
submodule--helper, I think that needs an explanation as to why a Git
command wouldn't work." in
https://lore.kernel.org/git/20211129210140.937875-1-jonathantanmy@google.com/

[2] Discussed in
https://lore.kernel.org/git/20211123021223.3472472-1-jonathantanmy@google.com/

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 0/5] implement branch --recurse-submodules
  2021-12-13 23:20         ` Jonathan Tan
@ 2021-12-14 18:47           ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-14 18:47 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: jonathantanmy, git, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Ah, I should make my cover letter more explicit then. I believe some of
the comments are addressed in the body of the patches, but that's not
the most reviewer-friendly way to do it.

I'll address the comments out-of-order because it makes a bit more sense
this way.

All the patch snippets come from
https://lore.kernel.org/git/20211209184928.71413-5-chooglen@google.com

Jonathan Tan <jonathantanmy@google.com> writes:

> I think we need a test where the submodule is at
> $ROOT/foo/bar, not just $ROOT/foo

This scenario is tested by changing how the test repo is set up:

  +test_expect_success 'setup superproject and submodule' '
  +	git init super &&
  +	test_commit foo &&
  +	git init sub-sub-upstream &&
  +	test_commit -C sub-sub-upstream foo &&
  +	git init sub-upstream &&
  +	git -C sub-upstream submodule add "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
  +	git -C sub-upstream commit -m "add submodule" &&
  +	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
  +	git -C super commit -m "add submodule" &&
  +	git -C super config submodule.propagateBranches true &&
  +	git -C super/sub submodule update --init
  +'

so the tests are now all written against a superproject with a 2-level
submodule.

> I just saw that you [...] are still using tree_entry() non-recursively
> [2]

> [2] Discussed in https://lore.kernel.org/git/20211123021223.3472472-1-jonathantanmy@google.com/

Yes, but this limitation is overcome by having
create_branches_recursively() call itself. I documented this caveat on
submodules_of_tree(), which is the function that calls tree_entry().

  +/**
  + * Given a treeish, return all submodules in the tree. This only reads
  + * one level of the tree, so it will not return nested submodules;
  + * callers that require nested submodules are expected to handle the
  + * recursion themselves.
  + */
  +void submodules_of_tree(struct repository *r,
  +			const struct object_id *treeish_name,
  +			struct submodule_entry_list *ret);

> [1] I said "If you want to use setup_tracking() through
> submodule--helper, I think that needs an explanation as to why a Git
> command wouldn't work." in
> https://lore.kernel.org/git/20211129210140.937875-1-jonathantanmy@google.com/

Now that create_branches_recursively() calls itself, I moved
setup_tracking() into create_branches_recursively() instead of having a
submodule helper that only calls create_branch() and setup_tracking().
As a result, I think this comment isn't as relevant as before.

That said, it might still be unobvious that create_branch() does not do
the right thing without setup_tracking(), i.e.

  # Why doesn't create_branch() just do the right thing?
  +	create_branch(the_repository, name, start_name, force, 0, reflog, quiet,
  +		      BRANCH_TRACK_NEVER, dry_run);
  +	setup_tracking(name, tracking_name, track, quiet, 0);

I was hoping that readers who were unclear would find the comment on
create_branches_recursively() adequate:

  + * - 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()).


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v3 4/5] branch: add --recurse-submodules option for branch creation
  2021-12-11 18:08       ` Philippe Blain
@ 2021-12-14 20:08         ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-14 20:08 UTC (permalink / raw)
  To: Philippe Blain, git
  Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Junio C Hamano

Philippe Blain <levraiphilippeblain@gmail.com> writes:

> Hi Glen,
>
> Le 2021-12-09 à 13:49, Glen Choo a écrit :
>>   Documentation/config/advice.txt    |   3 +
>>   Documentation/config/submodule.txt |   8 +
>
> Same comment as I remarked in v1 [1]:
>
> We would need to add the new flag to Documentation/git-branch.txt,
> and also probably update the documentation of 'submodule.recurse'
> in 'Documentation/config/submodule.txt'.
>
> [1] https://lore.kernel.org/git/3ad3941c-de18-41bf-2e44-4238ae868d79@gmail.com/

Ah, I missed Documentation/git-branch.txt. Thanks.

I avoided updating 'submodule.recurse' because it seemed a bit redundant
when 'submodule.propagateBranches' is in the next paragraph. But I can
imagine that this helps users with tunnel vision (like me...), so I'll
do the update.

>> diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
>> index 063eec2511..e52262dc69 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` was not run.
>
> I see you added '--init' in the actual message below, but maybe it would be more accurate
> to also add it here ?

Sounds good.

>> +	/*
>> +	 * 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"),
>
> small nit, maybe write: "unable to find submodule repository" ?

I don't think that adding the word 'repository' makes the problem
clearer to the user. It might even be misleading e.g. a hypotethical
user might think "Unable to find the repository? The remote repository
is giving a 404 Not Found?".

>> +			    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))
>
> Here, we do not actually use the dry_run argument, since we *always* want to
> do a dry run for the submodules...
>
>> +			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);
>
> ... whereas for the superproject branch, we use the given dry_run argument...
>
>> +	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, 0);
>> +
>> +	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))
>
> ... and if !dry_run, then we do create the branches in the submodules.
>
> That is a little bit hard to follow if you are not careful. Maybe it's just me,
> but as I was first reading it I wondered why '0' and '1' were hard-coded as the dry-run
> arguments to submodule_create_branch... It would maybe be clearer to use a named
> variable ?

This is valid feedback, but I'm not sure how to make this clearer
(besides adding comments). In every case, we really do want the
hard-coded value, i.e. we'd always want dry_run = 1 (validation) at the
beginning, and always dry_run = 0 (actually create branches) at the end.

As such, a named variable like the following:

  int dry_run_off = 0;
  setup_tracking(name, tracking_name, track, quiet, dry_run_off);

seems rather artificial. Perhaps you had something like an enum in mind,
like:

  enum dry_run {
    DRY_RUN_OFF,
    DRY_RUN_ON
  };
  setup_tracking(name, tracking_name, track, quiet, DRY_RUN_OFF);

which is better, but still feels artificial for a single function with
an boolean parameter.

Passing literal 0s and 1s don't seem to contradict
Documentation/CodingGuidelines or the style of code I have touched, e.g.

>> -		create_branch(the_repository,
>> -			      argv[0], (argc == 2) ? argv[1] : head,
>> -			      force, 0, reflog, quiet, track);

hard-codes 0 to clobber_head_ok.

Perhaps you had something else in mind?

>> @@ -851,6 +874,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>?"));
>> @@ -858,10 +884,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);
>> -
>> +		if (recurse_submodules) {
>> +			create_branches_recursively(the_repository, branch_name,
>> +						    start_name, NULL, force,
>> +						    reflog, quiet, track, 0);
>
> Here again, maybe it would be clearer to use a named variable instead of hard-coding '0' ?
>
>> +			return 0;
>> +		}
>> +		create_branch(the_repository, branch_name, start_name, force, 0,
>> +			      reflog, quiet, track, 0);
>
> Same here.

I agree that the previous example of create_branches_recursively() is
hard to follow, but for cmd_branch() I think there is very little reason
to name a variable for dry_run when cmd_branch() only uses 0.

>>   	} else
>>   		usage_with_options(builtin_branch_usage, options);
>>   
>
>> diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
>> new file mode 100755
>> index 0000000000..2dd0e2b01f
>> --- /dev/null
>> +++ b/t/t3207-branch-submodule.sh
>
> The tests look pretty thourough.

Thanks!

>> +
>> +test_expect_success 'should create branch when submodule is not in HEAD .gitmodules' '
>
> micro-nit: maybe write: HEAD:.gitmodules as this is revision synatx.

Sounds good.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v4 0/5] implement branch --recurse-submodules
  2021-12-09 18:49   ` [PATCH v3 0/5] implement branch --recurse-submodules Glen Choo
                       ` (5 preceding siblings ...)
  2021-12-09 21:59     ` [PATCH v3 0/5] implement branch --recurse-submodules Jonathan Tan
@ 2021-12-16  0:32     ` Glen Choo
  2021-12-16  0:32       ` [PATCH v4 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
                         ` (5 more replies)
  6 siblings, 6 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16  0:32 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Submodule branching RFC:
https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/

Original Submodule UX RFC/Discussion:
https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/

Contributor Summit submodules Notes:
https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/

Submodule UX overhaul updates:
https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update

This series implements branch --recurse-submodules as laid out in the
Submodule branching RFC (linked above). If there are concerns about the
UX/behavior, I would appreciate feedback on the RFC thread as well :)

This series is based off js/branch-track-inherit.

Future work:
* `git branch -d --recurse-submodules` so that users can clean up
  extraneous branches.
* `git [checkout | switch] --recurse-submodules` +
  submodule.propagateBranches so that users can actually checkout the
  branches.
* After [1], it seems clear that --recurse-submodules parsing could
  really benefit from some standardization. It's not obvious which
  RECURSE_SUBMODULE_* enums are applicable to which commands, and there
  is no way to distinguish between an explicit --recurse-submodules from
  argv vs submodule.recurse from the config.

  I chose not to use them in this series because their usage is already
  inconsistent (grep.c doesn't use them either), and it would be _more_
  confusing to use the enum (handling RECURSE_SUBMODULES_DEFAULT = 1 is
  trickier than boolean 0 and 1).

  At this point, I think it would be too noisy to introduce the enum,
  but this would be a nice cleanup to do later.
* As documented in branch.c, we create branches using a child process
  only because install_branch_config() does not support submodules.
  It should be possible to remove the child process once we make the
  appropriate changes to config.c. I attempted this in [2] but chose to
  punt it because it was too time-consuming at the time.

Changes since v3:
* Split up the old patch 1. Patch 1 had a big diff because it used to
  move lines, remove dead code and introduce repo_* functions (thanks
  Jonathan!)
** repo_* functions have been dropped; they added noise and are not
   necessary for correctness.
* Use a new, harder-to-misuse function in --set-upstream-to,
  dwim_and_setup_tracking(). Now, setup_tracking() never does DWIM and
  dwim_and_setup_tracking() always does DWIM.
* Move create_branch() dry_run to its own patch.
* Fix an oversight where submodules in subtrees were ignored. This was
  because submodules_of_tree() and tree_entry() didn't recurse into
  subtrees. Test this accordingly (thanks Jonathan!).
* cmd_branch() possible actions are more consistently ordered.
* Documentation fixes (thanks Philippe!).
* Additional comments and explanation.
* Drop patch 5 (optional cleanup).
* Rebase onto js/branch-track-inherit v6.

Changes since v2:
* Rebase onto js/branch-track-inherit. This series should continue to be
  the case going forward.
* Patch 1 has a smaller diff because the introduction of
  validate_branch_start() no longer changes the function order thanks to a
  forward declaration. This artificial forward declaration is removed in a
  patch 2 (which can just be squashed into patch 1).
* Optional cleanup: fix questionable exit codes in patch 5.

Changes since v1:
* Move the functionality of "git branch --dry-run" into "git submodule-helper create-branch --dry-run"
* Add more fields to the submodules_of_tree() struct to reduce the
  number of allocations made by the caller. Move this functionality
  to patch 3 (formerly patch 4) and drop patch 1.
* Make submodules_of_tree() ignore inactive submodules
* Structure the output of the submodules a bit better by adding prefixes
  to the child process' output (instead of inconsistently indenting the
  output).
** I wasn't able to find a good way to interleave stdout/stderr
   correctly, so a less-than-desirable workaround was to route the child
   process output to stdout/stderr depending on the exit code.
** Eventually, I would like to structure the output of submodules in a
   report, as Ævar suggested. But at this stage, I think that it's
   better to spend time getting user feedback on the submodules
   branching UX and it'll be easier to standardize the output when we've
   implemented more of the UX :)

[1] https://lore.kernel.org/git/kl6lbl1p9zjf.fsf@chooglen-macbookpro.roam.corp.google.com/
[2] https://lore.kernel.org/git/kl6lv90ytd4v.fsf@chooglen-macbookpro.roam.corp.google.com/

Glen Choo (5):
  branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  branch: make create_branch() always create a branch
  branch: add a dry_run parameter to create_branch()
  builtin/branch: clean up action-picking logic in cmd_branch()
  branch: add --recurse-submodules option for branch creation

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  24 ++-
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 257 ++++++++++++++++++++-----
 branch.h                           |  57 +++++-
 builtin/branch.c                   |  70 +++++--
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  60 ++++++
 submodule-config.h                 |  34 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3200-branch.sh                  |  17 ++
 t/t3207-branch-submodule.sh        | 291 +++++++++++++++++++++++++++++
 16 files changed, 805 insertions(+), 76 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

Range-diff against v3:
1:  8241c0b51a < -:  ---------- branch: move --set-upstream-to behavior to setup_tracking()
2:  b74bcbaade < -:  ---------- branch: remove forward declaration of validate_branch_start()
-:  ---------- > 1:  dfdbbaaca5 branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
-:  ---------- > 2:  e22a177cb7 branch: make create_branch() always create a branch
-:  ---------- > 3:  8a895aa401 branch: add a dry_run parameter to create_branch()
3:  235173efc9 ! 4:  971c53ec85 builtin/branch: clean up action-picking logic in cmd_branch()
    @@ Commit message
         Such an option does not exist yet, but one will be introduced in a
         subsequent commit.
     
    -    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).
    -
         Signed-off-by: Glen Choo <chooglen@google.com>
     
      ## builtin/branch.c ##
    -@@
    - 
    - 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>] [-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>"),
     @@ builtin/branch.c: static int edit_branch_description(const char *branch_name)
      
      int cmd_branch(int argc, const char **argv, const char *prefix)
    @@ builtin/branch.c: static int edit_branch_description(const char *branch_name)
     -	int reflog = 0, edit_description = 0;
     -	int quiet = 0, unset_upstream = 0;
     +	/* possible actions */
    -+	int delete = 0, rename = 0, copy = 0, force = 0, list = 0,
    ++	int delete = 0, rename = 0, copy = 0, list = 0,
     +	    unset_upstream = 0, show_current = 0, edit_description = 0;
    + 	const char *new_upstream = NULL;
     +	int noncreate_actions = 0;
     +	/* possible options */
    -+	int reflog = 0, quiet = 0, icase = 0;
    - 	const char *new_upstream = NULL;
    ++	int reflog = 0, quiet = 0, icase = 0, force = 0;
      	enum branch_track track;
      	struct ref_filter filter;
     -	int icase = 0;
4:  3dabb8e2fa ! 5:  cd88f3ad92 branch: add --recurse-submodules option for branch creation
    @@ Commit message
         * add the "submoduleNotUpdated" advice to advise users to update the
           submodules in their trees
     
    -    Other changes
    -
    -    * add a "dry_run" parameter to create_branch() in order to support
    -      `git submodule--helper create-branch --dry-run`
    +    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).
     
         Signed-off-by: Glen Choo <chooglen@google.com>
     
    @@ Documentation/config/advice.txt: advice.*::
      		configured to "die" causes a fatal error.
     +	submodulesNotUpdated::
     +		Advice shown when a user runs a submodule command that fails
    -+		because `git submodule update` was not run.
    ++		because `git submodule update --init` was not run.
      	addIgnoredFile::
      		Advice shown if a user attempts to add an ignored file to
      		the index.
     
      ## Documentation/config/submodule.txt ##
    +@@ Documentation/config/submodule.txt: 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`.
    +-	Defaults to false.
    ++	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
     @@ Documentation/config/submodule.txt: submodule.recurse::
    + 	`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`.
    @@ Documentation/config/submodule.txt: submodule.recurse::
     +	`--recurse-submodules` and certain commands that already accept
     +	`--recurse-submodules` will now consider branches.
     +	Defaults to false.
    -+
    + 
      submodule.fetchJobs::
      	Specifies how many submodules are fetched/cloned at the same time.
    - 	A positive integer allows up to that number of submodules fetched
    +
    + ## Documentation/git-branch.txt ##
    +@@ Documentation/git-branch.txt: 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>
    +@@ Documentation/git-branch.txt: 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.
     
      ## advice.c ##
     @@ advice.c: static struct {
    @@ branch.c
      
      struct tracking {
      	struct refspec_item spec;
    -@@ branch.c: void setup_tracking(const char *new_ref, const char *orig_ref,
    - 
    - void create_branch(struct repository *r, const char *name,
    - 		   const char *start_name, int force, int clobber_head_ok,
    --		   int reflog, int quiet, enum branch_track track)
    -+		   int reflog, int quiet, enum branch_track track, int dry_run)
    - {
    - 	struct object_id oid;
    - 	char *real_ref;
    -@@ branch.c: void create_branch(struct repository *r, const char *name,
    - 	}
    - 
    - 	validate_branch_start(r, start_name, track, &oid, &real_ref);
    -+	if (dry_run)
    -+		goto cleanup;
    - 
    - 	if (reflog)
    - 		log_all_ref_updates = LOG_REFS_NORMAL;
    -@@ branch.c: void create_branch(struct repository *r, const char *name,
    - 	if (real_ref && track)
    - 		setup_tracking(ref.buf + 11, real_ref, track, quiet, 0);
    - 
    -+cleanup:
    - 	strbuf_release(&ref);
    - 	free(real_ref);
    +@@ branch.c: 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() (and its variants) do
    ++ * 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,
    @@ branch.c: void create_branch(struct repository *r, const char *name,
     +	struct submodule_entry_list submodule_entry_list;
     +
     +	/* Perform dwim on start_name to get super_oid and branch_point. */
    -+	validate_branch_start(r, start_name, BRANCH_TRACK_NEVER, &super_oid,
    -+			      &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
    @@ branch.c: void create_branch(struct repository *r, const char *name,
     +	 * tedious to determine whether or not tracking was set up in the
     +	 * superproject.
     +	 */
    -+	setup_tracking(name, tracking_name, track, quiet, 0);
    ++	setup_tracking(name, tracking_name, track, quiet);
     +
     +	for (i = 0; i < submodule_entry_list.entry_nr; i++) {
     +		if (submodule_create_branch(
    @@ branch.c: void create_branch(struct repository *r, const char *name,
      	unlink(git_path_merge_head(r));
     
      ## branch.h ##
    -@@ branch.h: void setup_tracking(const char *new_ref, const char *orig_ref,
    -  *   - track causes the new branch to be configured to merge the remote branch
    -  *     that start_name is a tracking branch for (if any).
    -  *
    -+ *   - dry_run causes the branch to be validated but not created.
    -+ *
    -  */
    --void create_branch(struct repository *r,
    --		   const char *name, const char *start_name,
    --		   int force, int clobber_head_ok,
    --		   int reflog, int quiet, enum branch_track track);
    -+void create_branch(struct repository *r, const char *name,
    -+		   const char *start_name, int force, int clobber_head_ok,
    -+		   int reflog, int quiet, enum branch_track track, int dry_run);
    +@@ branch.h: void create_branch(struct repository *r, const char *name,
    + 		   const char *start_name, int force, int clobber_head_ok,
    + 		   int reflog, int quiet, enum branch_track track, int dry_run);
      
     +/*
     + * Creates a new branch in repository and its submodules (and its
    @@ branch.h: void setup_tracking(const char *new_ref, const char *orig_ref,
       * Return 1 if the named branch already exists; return 0 otherwise.
     
      ## builtin/branch.c ##
    +@@
    + 
    + 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>"),
     @@ builtin/branch.c: static const char * const builtin_branch_usage[] = {
      
      static const char *head;
    @@ builtin/branch.c: static int git_branch_config(const char *var, const char *valu
      }
      
     @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix)
    - 	    unset_upstream = 0, show_current = 0, edit_description = 0;
    + 	const char *new_upstream = NULL;
      	int noncreate_actions = 0;
      	/* possible options */
    --	int reflog = 0, quiet = 0, icase = 0;
    -+	int reflog = 0, quiet = 0, icase = 0, recurse_submodules_explicit = 0;
    - 	const char *new_upstream = NULL;
    +-	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;
     @@ builtin/branch.c: 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),
    @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix
      
     -		create_branch(the_repository,
     -			      argv[0], (argc == 2) ? argv[1] : head,
    --			      force, 0, reflog, quiet, track);
    +-			      force, 0, reflog, quiet, track, 0);
     -
     +		if (recurse_submodules) {
     +			create_branches_recursively(the_repository, branch_name,
    @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix
      		usage_with_options(builtin_branch_usage, options);
      
     
    - ## builtin/checkout.c ##
    -@@ builtin/checkout.c: static void update_refs_for_switch(const struct checkout_opts *opts,
    - 				      opts->new_branch_force ? 1 : 0,
    - 				      opts->new_branch_log,
    - 				      opts->quiet,
    --				      opts->track);
    -+				      opts->track,
    -+				      0);
    - 		new_branch_info->name = opts->new_branch;
    - 		setup_branch_path(new_branch_info);
    - 	}
    -
      ## builtin/submodule--helper.c ##
     @@
      #include "diff.h"
    @@ submodule-config.c: const struct submodule *submodule_from_path(struct repositor
      	return config_from(r->submodule_cache, treeish_name, path, lookup_path);
      }
      
    -+void submodules_of_tree(struct repository *r,
    -+			const struct object_id *treeish_name,
    -+			struct submodule_entry_list *out)
    ++/**
    ++ * 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;
     +
    -+	fill_tree_descriptor(r, &tree, treeish_name);
    -+	while (tree_entry(&tree, name_entry)) {
    -+		if (!S_ISGITLINK(name_entry->mode) || !is_tree_submodule_active(r, treeish_name, name_entry->path)) {
    -+			continue;
    -+		}
    -+
    -+		st_entry = xmalloc(sizeof(*st_entry));
    -+		st_entry->name_entry = name_entry;
    -+		st_entry->submodule =
    -+			submodule_from_path(r, treeish_name, name_entry->path);
    -+		st_entry->repo = xmalloc(sizeof(*st_entry->repo));
    -+		if (repo_submodule_init(st_entry->repo, r, name_entry->path,
    -+					treeish_name))
    -+			FREE_AND_NULL(st_entry->repo);
    -+
    -+		ALLOC_GROW(out->entries, out->entry_nr + 1, out->entry_alloc);
    -+		out->entries[out->entry_nr++] = *st_entry;
    -+	}
    ++	traverse_tree_submodules(r, treeish_name, NULL, treeish_name, out);
     +}
     +
      void submodule_free(struct repository *r)
    @@ submodule-config.h: int check_submodule_name(const char *name);
     +};
     +
     +/**
    -+ * Given a treeish, return all submodules in the tree. This only reads
    -+ * one level of the tree, so it will not return nested submodules;
    -+ * callers that require nested submodules are expected to handle the
    -+ * recursion themselves.
    ++ * 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,
    @@ t/t3207-branch-submodule.sh (new)
     +	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 "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
     +	git -C sub-upstream commit -m "add submodule" &&
    ++	# Regular submodule
     +	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
    ++	# Submodule in a subdirectory
    ++	git -C super submodule add "$TRASH_DIRECTORY/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
    @@ t/t3207-branch-submodule.sh (new)
     +		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 sub/sub-sub rev-parse branch-a &&
    ++		git -C second/sub rev-parse branch-a
     +	)
     +'
     +
    @@ t/t3207-branch-submodule.sh (new)
     +	)
     +'
     +
    -+test_expect_success 'should create branch when submodule is not in HEAD .gitmodules' '
    ++test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
     +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
     +	(
     +		cd super &&
    @@ t/t3207-branch-submodule.sh (new)
     +		git branch --recurse-submodules branch-c branch-b &&
     +		git rev-parse branch-c &&
     +		git -C sub rev-parse branch-c &&
    ++		git -C second/sub rev-parse branch-c &&
     +		git checkout --recurse-submodules branch-c &&
    -+		git -C sub2 rev-parse branch-c
    ++		git -C sub2 rev-parse branch-c &&
    ++		git -C sub2/sub-sub rev-parse branch-c
     +	)
     +'
     +
5:  70fb03f882 < -:  ---------- branch.c: replace questionable exit() codes
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v4 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
@ 2021-12-16  0:32       ` Glen Choo
  2021-12-16  0:32       ` [PATCH v4 2/5] branch: make create_branch() always create a branch Glen Choo
                         ` (4 subsequent siblings)
  5 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16  0:32 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

This refactor is motivated by a desire to add a "dry_run" parameter to
create_branch() that will validate whether or not a branch can be
created without actually creating it - this behavior will be used in a
subsequent commit that adds `git branch --recurse-submodules topic`.

Adding "dry_run" is not obvious because create_branch() is also used to
set tracking information without creating a branch, i.e. when using
--set-upstream-to. This appears to be a leftover from 4fc5006676 (Add
branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
create a branch and sometimes update tracking information without
creating a branch. However, we no longer support --set-upstream, so it
makes more sense to set tracking information with another function and
use create_branch() only to create branches. In a later commit, we will
remove the now-unnecessary logic from create_branch() so that "dry_run"
becomes trivial to implement.

Introduce dwim_and_setup_tracking(), which replaces create_branch()
in `git branch --set-upstream-to`. Ensure correctness by moving the DWIM
and branch validation logic from create_branch() into a helper function,
dwim_branch_start(), so that the logic is shared by both functions.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c         | 87 ++++++++++++++++++++++++++++++++++++------------
 branch.h         | 22 ++++++++++++
 builtin/branch.c |  9 ++---
 3 files changed, 91 insertions(+), 27 deletions(-)

diff --git a/branch.c b/branch.c
index 6e7e2af5cb..fc3304e51d 100644
--- a/branch.c
+++ b/branch.c
@@ -212,9 +212,11 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
 }
 
 /*
- * 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.
+ * Used internally to set the branch.<new_ref>.{remote,merge} config
+ * settings so that branch 'new_ref' tracks 'orig_ref'. Unlike
+ * dwim_and_setup_tracking(), this does not do DWIM, i.e. "origin/main"
+ * will not be expanded to "refs/remotes/origin/main", so it is not safe
+ * for 'orig_ref' to be raw user input.
  */
 static void setup_tracking(const char *new_ref, const char *orig_ref,
 			   enum branch_track track, int quiet)
@@ -239,7 +241,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 		case BRANCH_TRACK_INHERIT:
 			break;
 		default:
-			return;
+			goto cleanup;
 		}
 
 	if (tracking.matches > 1)
@@ -252,6 +254,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 			      tracking.srcs) < 0)
 		exit(-1);
 
+cleanup:
 	string_list_clear(tracking.srcs, 0);
 }
 
@@ -335,31 +338,37 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+/**
+ * DWIMs a user-provided ref to determine the starting point for a
+ * branch and validates it, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test. This is
+ *     expanded with DWIM and assigned to out_real_ref.
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - out_oid is an out parameter containing the object_id of start_name
+ *
+ *   - out_real_ref is an out parameter containing the full, 'real' form
+ *     of start_name e.g. refs/heads/main instead of main
+ *
+ */
+static void dwim_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, char **out_real_ref,
+			   struct object_id *out_oid)
 {
 	struct commit *commit;
 	struct object_id oid;
 	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
 	int explicit_tracking = 0;
 
 	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
 		explicit_tracking = 1;
 
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
-
 	real_ref = NULL;
 	if (get_oid_mb(start_name, &oid)) {
 		if (explicit_tracking) {
@@ -396,7 +405,34 @@ void create_branch(struct repository *r,
 
 	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
 		die(_("Not a valid branch point: '%s'."), start_name);
-	oidcpy(&oid, &commit->object.oid);
+	if (out_real_ref)
+		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
+	if (out_oid)
+		oidcpy(out_oid, &commit->object.oid);
+
+	FREE_AND_NULL(real_ref);
+}
+
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track)
+{
+	struct object_id oid;
+	char *real_ref;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	int dont_change_ref = 0;
+
+	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
+	    ? validate_branchname(name, &ref)
+	    : validate_new_branchname(name, &ref, force)) {
+		if (!force)
+			dont_change_ref = 1;
+		else
+			forcing = 1;
+	}
+
+	dwim_branch_start(r, start_name, track, &real_ref, &oid);
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -430,6 +466,15 @@ void create_branch(struct repository *r,
 	free(real_ref);
 }
 
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet)
+{
+	char *real_orig_ref;
+	dwim_branch_start(r, orig_ref, track, &real_orig_ref, NULL);
+	setup_tracking(new_ref, real_orig_ref, track, quiet);
+}
+
 void remove_merge_branch_state(struct repository *r)
 {
 	unlink(git_path_merge_head(r));
diff --git a/branch.h b/branch.h
index 815dcd40c0..ab2315c611 100644
--- a/branch.h
+++ b/branch.h
@@ -18,6 +18,28 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/**
+ * Sets branch.<new_ref>.{remote,merge} config settings such that
+ * new_ref tracks orig_ref according to the specified tracking mode.
+ *
+ *   - new_ref is the name of the branch that we are setting tracking
+ *     for.
+ *
+ *   - orig_ref is the name of the ref that is 'upstream' of new_ref.
+ *     orig_ref will be expanded with DWIM so that the config settings
+ *     are in the correct format e.g. "refs/remotes/origin/main" instead
+ *     of "origin/main".
+ *
+ *   - track is the tracking mode e.g. BRANCH_TRACK_REMOTE causes
+ *     new_ref to track orig_ref directly, whereas BRANCH_TRACK_INHERIT
+ *     causes new_ref to track whatever orig_ref tracks.
+ *
+ *   - quiet suppresses tracking information.
+ */
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet);
+
 /*
  * Creates a new branch, where:
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index 81a29edb4a..16a7e80df5 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -823,12 +823,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		dwim_and_setup_tracking(the_repository, branch->name,
+					new_upstream, BRANCH_TRACK_OVERRIDE,
+					quiet);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v4 2/5] branch: make create_branch() always create a branch
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
  2021-12-16  0:32       ` [PATCH v4 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
@ 2021-12-16  0:32       ` Glen Choo
  2021-12-16  0:32       ` [PATCH v4 3/5] branch: add a dry_run parameter to create_branch() Glen Choo
                         ` (3 subsequent siblings)
  5 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16  0:32 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

create_branch() was formerly used to set tracking without creating a
branch. Since the previous commit replaces this use case with
setup_tracking(), we can simplify create_branch() so that it always
creates a branch.

Do this simplification, in particular:

* remove the special handling of BRANCH_TRACK_OVERRIDE because it is no
  longer used
* assert that clobber_head_ok can only be provided with force
* check that we're handling clobber_head_ok and force correctly by
  introducing tests for `git branch --force`

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c          | 55 +++++++++++++++++++++--------------------------
 branch.h          |  4 ++--
 t/t3200-branch.sh | 17 +++++++++++++++
 3 files changed, 44 insertions(+), 32 deletions(-)

diff --git a/branch.c b/branch.c
index fc3304e51d..f8944d3117 100644
--- a/branch.c
+++ b/branch.c
@@ -421,15 +421,17 @@ void create_branch(struct repository *r, const char *name,
 	char *real_ref;
 	struct strbuf ref = STRBUF_INIT;
 	int forcing = 0;
-	int dont_change_ref = 0;
-
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
+	struct ref_transaction *transaction;
+	struct strbuf err = STRBUF_INIT;
+	char *msg;
+
+	if (clobber_head_ok && !force)
+		BUG("'clobber_head_ok' can only be used with 'force'");
+
+	if (clobber_head_ok ?
+			  validate_branchname(name, &ref) :
+			  validate_new_branchname(name, &ref, force)) {
+		forcing = 1;
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
@@ -437,27 +439,20 @@ void create_branch(struct repository *r, const char *name,
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
-	if (!dont_change_ref) {
-		struct ref_transaction *transaction;
-		struct strbuf err = STRBUF_INIT;
-		char *msg;
-
-		if (forcing)
-			msg = xstrfmt("branch: Reset to %s", start_name);
-		else
-			msg = xstrfmt("branch: Created from %s", start_name);
-
-		transaction = ref_transaction_begin(&err);
-		if (!transaction ||
-		    ref_transaction_update(transaction, ref.buf,
-					   &oid, forcing ? NULL : null_oid(),
-					   0, msg, &err) ||
-		    ref_transaction_commit(transaction, &err))
-			die("%s", err.buf);
-		ref_transaction_free(transaction);
-		strbuf_release(&err);
-		free(msg);
-	}
+	if (forcing)
+		msg = xstrfmt("branch: Reset to %s", start_name);
+	else
+		msg = xstrfmt("branch: Created from %s", start_name);
+	transaction = ref_transaction_begin(&err);
+	if (!transaction ||
+		ref_transaction_update(transaction, ref.buf,
+					&oid, forcing ? NULL : null_oid(),
+					0, msg, &err) ||
+		ref_transaction_commit(transaction, &err))
+		die("%s", err.buf);
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	free(msg);
 
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
diff --git a/branch.h b/branch.h
index ab2315c611..cf3a4d3ff3 100644
--- a/branch.h
+++ b/branch.h
@@ -52,8 +52,8 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *
  *   - force enables overwriting an existing (non-head) branch
  *
- *   - clobber_head_ok allows the currently checked out (hence existing)
- *     branch to be overwritten; without 'force', it has no effect.
+ *   - clobber_head_ok, when enabled with 'force', allows the currently
+ *     checked out (head) branch to be overwritten
  *
  *   - reflog creates a reflog for the branch
  *
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 09ab132377..71a72efcb2 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
 	git branch abc && test_path_is_file .git/refs/heads/abc
 '
 
+test_expect_success 'git branch abc should fail when abc exists' '
+	test_must_fail git branch abc
+'
+
+test_expect_success 'git branch --force abc should fail when abc is checked out' '
+	test_when_finished git switch main &&
+	git switch abc &&
+	test_must_fail git branch --force abc HEAD~1
+'
+
+test_expect_success 'git branch --force abc should succeed when abc exists' '
+	git rev-parse HEAD~1 >expect &&
+	git branch --force abc HEAD~1 &&
+	git rev-parse abc >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v4 3/5] branch: add a dry_run parameter to create_branch()
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
  2021-12-16  0:32       ` [PATCH v4 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
  2021-12-16  0:32       ` [PATCH v4 2/5] branch: make create_branch() always create a branch Glen Choo
@ 2021-12-16  0:32       ` Glen Choo
  2021-12-16  0:32       ` [PATCH v4 4/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
                         ` (2 subsequent siblings)
  5 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16  0:32 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a dry_run parameter to create_branch() such that dry_run = 1 will
validate a new branch without trying to create it. This will be used in
`git branch --recurse-submodules` to ensure that the new branch can be
created in all submodules.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c           | 5 ++++-
 branch.h           | 9 +++++----
 builtin/branch.c   | 2 +-
 builtin/checkout.c | 3 ++-
 4 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/branch.c b/branch.c
index f8944d3117..bcaac0ac5e 100644
--- a/branch.c
+++ b/branch.c
@@ -415,7 +415,7 @@ static void dwim_branch_start(struct repository *r, const char *start_name,
 
 void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track)
+		   int reflog, int quiet, enum branch_track track, int dry_run)
 {
 	struct object_id oid;
 	char *real_ref;
@@ -435,6 +435,8 @@ void create_branch(struct repository *r, const char *name,
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
+	if (dry_run)
+		goto cleanup;
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -457,6 +459,7 @@ void create_branch(struct repository *r, const char *name,
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
 
+cleanup:
 	strbuf_release(&ref);
 	free(real_ref);
 }
diff --git a/branch.h b/branch.h
index cf3a4d3ff3..8009266343 100644
--- a/branch.h
+++ b/branch.h
@@ -62,11 +62,12 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *   - track causes the new branch to be configured to merge the remote branch
  *     that start_name is a tracking branch for (if any).
  *
+ *   - dry_run causes the branch to be validated but not created.
+ *
  */
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track);
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track, int dry_run);
 
 /*
  * Check if 'name' can be a valid name for a branch; die otherwise.
diff --git a/builtin/branch.c b/builtin/branch.c
index 16a7e80df5..9b300cf42f 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -859,7 +859,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 
 		create_branch(the_repository,
 			      argv[0], (argc == 2) ? argv[1] : head,
-			      force, 0, reflog, quiet, track);
+			      force, 0, reflog, quiet, track, 0);
 
 	} else
 		usage_with_options(builtin_branch_usage, options);
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 8d511aa6b7..fb4323dfca 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -893,7 +893,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				      opts->new_branch_force ? 1 : 0,
 				      opts->new_branch_log,
 				      opts->quiet,
-				      opts->track);
+				      opts->track,
+				      0);
 		new_branch_info->name = opts->new_branch;
 		setup_branch_path(new_branch_info);
 	}
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v4 4/5] builtin/branch: clean up action-picking logic in cmd_branch()
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
                         ` (2 preceding siblings ...)
  2021-12-16  0:32       ` [PATCH v4 3/5] branch: add a dry_run parameter to create_branch() Glen Choo
@ 2021-12-16  0:32       ` Glen Choo
  2021-12-16  0:32       ` [PATCH v4 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
  5 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16  0:32 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a variable to cmd_branch() that will tell us whether or not
cmd_branch() will default to creating a branch (instead of performing
another action). Besides making the function more explicit, this allows
us to validate options that can only be used when creating a branch.
Such an option does not exist yet, but one will be introduced in a
subsequent commit.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 builtin/branch.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 9b300cf42f..3b010c1d2c 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -616,14 +616,15 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, list = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	const char *new_upstream = NULL;
+	int noncreate_actions = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, icase = 0, force = 0;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting;
 	struct string_list sorting_options = STRING_LIST_INIT_DUP;
 	struct ref_format format = REF_FORMAT_INIT;
@@ -708,8 +709,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
+			    !!show_current + !!list + !!edit_description +
+			    !!unset_upstream;
+	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
 	if (filter.abbrev == -1)
@@ -849,7 +852,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
 		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>?"));
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v4 5/5] branch: add --recurse-submodules option for branch creation
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
                         ` (3 preceding siblings ...)
  2021-12-16  0:32       ` [PATCH v4 4/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
@ 2021-12-16  0:32       ` Glen Choo
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
  5 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16  0:32 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

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 semantics e.g. `git
checkout` does not recursively checkout the expected branches created by
`git branch` yet. To ensure that the correct set of semantics is used,
this commit introduces a new configuration value,
`submodule.propagateBranches`, which enables submodule branching when
true (defaults to false).

This commit includes changes that allow Git to work with submodules
that are in trees (and not just the index):

* 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).

Signed-off-by: Glen Choo <chooglen@google.com>
---
 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  24 ++-
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 130 +++++++++++++
 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        | 291 +++++++++++++++++++++++++++++
 14 files changed, 659 insertions(+), 14 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

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..0aa5946364 100644
--- a/Documentation/config/submodule.txt
+++ b/Documentation/config/submodule.txt
@@ -59,11 +59,8 @@ 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`.
-	Defaults to false.
+	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
@@ -71,6 +68,23 @@ submodule.recurse::
 	`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.
 
 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 2d52ae396b..435dc2b20e 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 bcaac0ac5e..90609c2ea9 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;
@@ -473,6 +475,134 @@ 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() (and its variants) do
+ * 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);
+	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 8009266343..63448e4278 100644
--- a/branch.h
+++ b/branch.h
@@ -69,6 +69,28 @@ void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
 		   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 3b010c1d2c..c06de58873 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] = {
@@ -99,6 +102,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;
@@ -673,6 +686,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(),
 	};
@@ -715,6 +729,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;
@@ -853,6 +878,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>?"));
@@ -860,10 +888,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 e630f0c730..44b6283c08 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;
@@ -3389,6 +3426,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 c689070524..5ace18a7d9 100644
--- a/submodule.c
+++ b/submodule.c
@@ -267,7 +267,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;
@@ -275,7 +277,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)
@@ -317,6 +319,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..a2dfb5ad7f
--- /dev/null
+++ b/t/t3207-branch-submodule.sh
@@ -0,0 +1,291 @@
+#!/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
+
+test_expect_success 'setup superproject and submodule' '
+	git init super &&
+	test_commit 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 "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
+	git -C sub-upstream commit -m "add submodule" &&
+	# Regular submodule
+	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
+	# Submodule in a subdirectory
+	git -C super submodule add "$TRASH_DIRECTORY/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
+'
+
+CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
+
+cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
+	#!/bin/sh
+
+	super_dir="$1"
+	shift
+	(
+		cd "$super_dir" &&
+		git checkout main &&
+		for branch_name in "$@"; do
+			git branch -D "$branch_name"
+			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
+		done
+	)
+EOF
+chmod +x "$CLEANUP_SCRIPT_PATH"
+
+cleanup_branches() {
+	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
+} >/dev/null 2>/dev/null
+
+# Test the argument parsing
+test_expect_success '--recurse-submodules should create branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should respect submodule.recurse when creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -c submodule.recurse=true branch branch-a &&
+		git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should ignore submodule.recurse when not creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git -c submodule.recurse=true branch -D branch-a &&
+		test_must_fail git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+# Test branch creation behavior
+test_expect_success 'should create branches based off commit id in superproject' '
+	test_when_finished "cleanup_branches super branch-a branch-b" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 &&
+
+		cat >expected <<-EOF &&
+		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
+		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
+		EOF
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branches if branch exists and --force is given' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -C sub rev-parse HEAD >expected &&
+		test_commit -C sub baz &&
+		git -C sub branch branch-a HEAD~1 &&
+		git branch --recurse-submodules --force branch-a &&
+		git rev-parse branch-a &&
+		# assert that sub:branch-a was moved
+		git -C sub rev-parse branch-a >actual &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
+	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
+	(
+		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 rev-parse branch-c &&
+		git -C sub rev-parse branch-c &&
+		git -C second/sub rev-parse branch-c &&
+		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 set up tracking of local branches with track=always' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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)" = ""
+	)
+'
+
+test_expect_success 'should not create branches in inactive submodules' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	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 'setup remote-tracking tests' '
+	(
+		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
+'
+
+test_expect_success 'should not create branch when submodule is not in .git/modules' '
+	# The cleanup needs to delete sub2 separately because main does not have sub2
+	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
+		git -C super-clone/sub2/sub-sub branch -D branch-b && \
+		cleanup_branches super-clone branch-a branch-b" &&
+	(
+		cd super-clone &&
+		# This should succeed because super-clone has sub.
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		# This should fail because super-clone does not have sub2.
+		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
+		cat >expected <<-EOF &&
+		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
+		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
+		EOF
+		test_cmp expected 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 "cleanup_branches super-clone branch-a" &&
+	(
+		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 "cleanup_branches super-clone branch-a && \
+		git -C super-clone remote rename ex-origin origin" &&
+	(
+		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
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v5 0/5] implement branch --recurse-submodules
  2021-12-16  0:32     ` [PATCH v4 " Glen Choo
                         ` (4 preceding siblings ...)
  2021-12-16  0:32       ` [PATCH v4 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-16 23:33       ` Glen Choo
  2021-12-16 23:33         ` [PATCH v5 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
                           ` (6 more replies)
  5 siblings, 7 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16 23:33 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Submodule branching RFC:
https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/

Original Submodule UX RFC/Discussion:
https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/

Contributor Summit submodules Notes:
https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/

Submodule UX overhaul updates:
https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update

This series implements branch --recurse-submodules as laid out in the
Submodule branching RFC (linked above). If there are concerns about the
UX/behavior, I would appreciate feedback on the RFC thread as well :)

This series is based off js/branch-track-inherit.

Future work:
* `git branch -d --recurse-submodules` so that users can clean up
  extraneous branches.
* `git [checkout | switch] --recurse-submodules` +
  submodule.propagateBranches so that users can actually checkout the
  branches.
* After [1], it seems clear that --recurse-submodules parsing could
  really benefit from some standardization. It's not obvious which
  RECURSE_SUBMODULE_* enums are applicable to which commands, and there
  is no way to distinguish between an explicit --recurse-submodules from
  argv vs submodule.recurse from the config.

  I chose not to use them in this series because their usage is already
  inconsistent (grep.c doesn't use them either), and it would be _more_
  confusing to use the enum (handling RECURSE_SUBMODULES_DEFAULT = 1 is
  trickier than boolean 0 and 1).

  At this point, I think it would be too noisy to introduce the enum,
  but this would be a nice cleanup to do later.
* As documented in branch.c, we create branches using a child process
  only because install_branch_config() does not support submodules.
  It should be possible to remove the child process once we make the
  appropriate changes to config.c. I attempted this in [2] but chose to
  punt it because it was too time-consuming at the time.

Changes since v4:
* Rebase correctly onto 'gitster/seen^{/^Merge branch .js/branch-track-inherit.}'
  (see base-commit) as suggested in [3] (thanks Junio!)
* These patches were also verified on top of 'next'.

Changes since v3:
* Split up the old patch 1. Patch 1 had a big diff because it used to
  move lines, remove dead code and introduce repo_* functions (thanks
  Jonathan!)
** repo_* functions have been dropped; they added noise and are not
   necessary for correctness.
* Use a new, harder-to-misuse function in --set-upstream-to,
  dwim_and_setup_tracking(). Now, setup_tracking() never does DWIM and
  dwim_and_setup_tracking() always does DWIM.
* Move create_branch() dry_run to its own patch.
* Fix an oversight where submodules in subtrees were ignored. This was
  because submodules_of_tree() and tree_entry() didn't recurse into
  subtrees. Test this accordingly (thanks Jonathan!).
* cmd_branch() possible actions are more consistently ordered.
* Documentation fixes (thanks Philippe!).
* Additional comments and explanation.
* Drop patch 5 (optional cleanup).
* Rebase onto js/branch-track-inherit v6.

Changes since v2:
* Rebase onto js/branch-track-inherit. This series should continue to be
  the case going forward.
* Patch 1 has a smaller diff because the introduction of
  validate_branch_start() no longer changes the function order thanks to a
  forward declaration. This artificial forward declaration is removed in a
  patch 2 (which can just be squashed into patch 1).
* Optional cleanup: fix questionable exit codes in patch 5.

Changes since v1:
* Move the functionality of "git branch --dry-run" into "git submodule-helper create-branch --dry-run"
* Add more fields to the submodules_of_tree() struct to reduce the
  number of allocations made by the caller. Move this functionality
  to patch 3 (formerly patch 4) and drop patch 1.
* Make submodules_of_tree() ignore inactive submodules
* Structure the output of the submodules a bit better by adding prefixes
  to the child process' output (instead of inconsistently indenting the
  output).
** I wasn't able to find a good way to interleave stdout/stderr
   correctly, so a less-than-desirable workaround was to route the child
   process output to stdout/stderr depending on the exit code.
** Eventually, I would like to structure the output of submodules in a
   report, as Ævar suggested. But at this stage, I think that it's
   better to spend time getting user feedback on the submodules
   branching UX and it'll be easier to standardize the output when we've
   implemented more of the UX :)

[1] https://lore.kernel.org/git/kl6lbl1p9zjf.fsf@chooglen-macbookpro.roam.corp.google.com/
[2] https://lore.kernel.org/git/kl6lv90ytd4v.fsf@chooglen-macbookpro.roam.corp.google.com/
[3] https://lore.kernel.org/git/xmqqlf0lz6os.fsf@gitster.g 

Glen Choo (5):
  branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  branch: make create_branch() always create a branch
  branch: add a dry_run parameter to create_branch()
  builtin/branch: clean up action-picking logic in cmd_branch()
  branch: add --recurse-submodules option for branch creation

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  24 ++-
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 257 ++++++++++++++++++++-----
 branch.h                           |  57 +++++-
 builtin/branch.c                   |  70 +++++--
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  60 ++++++
 submodule-config.h                 |  34 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3200-branch.sh                  |  17 ++
 t/t3207-branch-submodule.sh        | 291 +++++++++++++++++++++++++++++
 16 files changed, 805 insertions(+), 76 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

Range-diff against v4:
1:  751e8ae566 < -:  ---------- branch: accept multiple upstream branches for tracking
2:  5d1ebe1495 < -:  ---------- branch: add flags and config to inherit tracking
3:  0080a1fb35 < -:  ---------- config: require lowercase for branch.autosetupmerge
4:  dfdbbaaca5 ! 1:  a9d1108b3e branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
    @@ Commit message
     
         This refactor is motivated by a desire to add a "dry_run" parameter to
         create_branch() that will validate whether or not a branch can be
    -    created without actually creating it - this behavior be used in a
    +    created without actually creating it - this behavior will be used in a
         subsequent commit that adds `git branch --recurse-submodules topic`.
     
         Adding "dry_run" is not obvious because create_branch() is also used to
    @@ branch.c: N_("\n"
     @@ branch.c: void create_branch(struct repository *r,
      
      	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
    - 		die(_("Not a valid branch point: '%s'."), start_name);
    + 		die(_("not a valid branch point: '%s'"), start_name);
     -	oidcpy(&oid, &commit->object.oid);
     +	if (out_real_ref)
     +		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
5:  e22a177cb7 ! 2:  c543c1412a branch: make create_branch() always create a branch
    @@ Commit message
     
         create_branch() was formerly used to set tracking without creating a
         branch. Since the previous commit replaces this use case with
    -    setup_tracking(), we can simplify create_branch() so that it always
    -    creates a branch.
    +    dwim_and_setup_tracking(), we can simplify create_branch() so that it
    +    always creates a branch.
     
         Do this simplification, in particular:
     
6:  8a895aa401 ! 3:  dddd434d7a branch: add a dry_run parameter to create_branch()
    @@ builtin/checkout.c: static void update_refs_for_switch(const struct checkout_opt
     -				      opts->track);
     +				      opts->track,
     +				      0);
    - 		new_branch_info->name = opts->new_branch;
    - 		setup_branch_path(new_branch_info);
    - 	}
    + 		free(new_branch_info->name);
    + 		free(new_branch_info->refname);
    + 		new_branch_info->name = xstrdup(opts->new_branch);
7:  971c53ec85 = 4:  41cca3bd52 builtin/branch: clean up action-picking logic in cmd_branch()
8:  cd88f3ad92 ! 5:  540eeab183 branch: add --recurse-submodules option for branch creation
    @@ t/t3207-branch-submodule.sh (new)
     +		test_must_fail git rev-parse branch-a &&
     +
     +		cat >expected <<-EOF &&
    -+		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
    ++		submodule ${SQ}sub${SQ}: fatal: a branch named ${SQ}branch-a${SQ} already exists
     +		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
     +		EOF
     +		test_cmp expected actual

base-commit: a1eb3ee8288c96c95d18fef027fc276b5cb3b17a
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v5 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
@ 2021-12-16 23:33         ` Glen Choo
  2021-12-16 23:33         ` [PATCH v5 2/5] branch: make create_branch() always create a branch Glen Choo
                           ` (5 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16 23:33 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

This refactor is motivated by a desire to add a "dry_run" parameter to
create_branch() that will validate whether or not a branch can be
created without actually creating it - this behavior will be used in a
subsequent commit that adds `git branch --recurse-submodules topic`.

Adding "dry_run" is not obvious because create_branch() is also used to
set tracking information without creating a branch, i.e. when using
--set-upstream-to. This appears to be a leftover from 4fc5006676 (Add
branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
create a branch and sometimes update tracking information without
creating a branch. However, we no longer support --set-upstream, so it
makes more sense to set tracking information with another function and
use create_branch() only to create branches. In a later commit, we will
remove the now-unnecessary logic from create_branch() so that "dry_run"
becomes trivial to implement.

Introduce dwim_and_setup_tracking(), which replaces create_branch()
in `git branch --set-upstream-to`. Ensure correctness by moving the DWIM
and branch validation logic from create_branch() into a helper function,
dwim_branch_start(), so that the logic is shared by both functions.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c         | 87 ++++++++++++++++++++++++++++++++++++------------
 branch.h         | 22 ++++++++++++
 builtin/branch.c |  9 ++---
 3 files changed, 91 insertions(+), 27 deletions(-)

diff --git a/branch.c b/branch.c
index eed8665b76..54e989f3c8 100644
--- a/branch.c
+++ b/branch.c
@@ -212,9 +212,11 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
 }
 
 /*
- * 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.
+ * Used internally to set the branch.<new_ref>.{remote,merge} config
+ * settings so that branch 'new_ref' tracks 'orig_ref'. Unlike
+ * dwim_and_setup_tracking(), this does not do DWIM, i.e. "origin/main"
+ * will not be expanded to "refs/remotes/origin/main", so it is not safe
+ * for 'orig_ref' to be raw user input.
  */
 static void setup_tracking(const char *new_ref, const char *orig_ref,
 			   enum branch_track track, int quiet)
@@ -239,7 +241,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 		case BRANCH_TRACK_INHERIT:
 			break;
 		default:
-			return;
+			goto cleanup;
 		}
 
 	if (tracking.matches > 1)
@@ -252,6 +254,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 			      tracking.srcs) < 0)
 		exit(-1);
 
+cleanup:
 	string_list_clear(tracking.srcs, 0);
 }
 
@@ -340,31 +343,37 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+/**
+ * DWIMs a user-provided ref to determine the starting point for a
+ * branch and validates it, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test. This is
+ *     expanded with DWIM and assigned to out_real_ref.
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - out_oid is an out parameter containing the object_id of start_name
+ *
+ *   - out_real_ref is an out parameter containing the full, 'real' form
+ *     of start_name e.g. refs/heads/main instead of main
+ *
+ */
+static void dwim_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, char **out_real_ref,
+			   struct object_id *out_oid)
 {
 	struct commit *commit;
 	struct object_id oid;
 	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
 	int explicit_tracking = 0;
 
 	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
 		explicit_tracking = 1;
 
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
-
 	real_ref = NULL;
 	if (get_oid_mb(start_name, &oid)) {
 		if (explicit_tracking) {
@@ -401,7 +410,34 @@ void create_branch(struct repository *r,
 
 	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
 		die(_("not a valid branch point: '%s'"), start_name);
-	oidcpy(&oid, &commit->object.oid);
+	if (out_real_ref)
+		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
+	if (out_oid)
+		oidcpy(out_oid, &commit->object.oid);
+
+	FREE_AND_NULL(real_ref);
+}
+
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track)
+{
+	struct object_id oid;
+	char *real_ref;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	int dont_change_ref = 0;
+
+	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
+	    ? validate_branchname(name, &ref)
+	    : validate_new_branchname(name, &ref, force)) {
+		if (!force)
+			dont_change_ref = 1;
+		else
+			forcing = 1;
+	}
+
+	dwim_branch_start(r, start_name, track, &real_ref, &oid);
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -435,6 +471,15 @@ void create_branch(struct repository *r,
 	free(real_ref);
 }
 
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet)
+{
+	char *real_orig_ref;
+	dwim_branch_start(r, orig_ref, track, &real_orig_ref, NULL);
+	setup_tracking(new_ref, real_orig_ref, track, quiet);
+}
+
 void remove_merge_branch_state(struct repository *r)
 {
 	unlink(git_path_merge_head(r));
diff --git a/branch.h b/branch.h
index 815dcd40c0..ab2315c611 100644
--- a/branch.h
+++ b/branch.h
@@ -18,6 +18,28 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/**
+ * Sets branch.<new_ref>.{remote,merge} config settings such that
+ * new_ref tracks orig_ref according to the specified tracking mode.
+ *
+ *   - new_ref is the name of the branch that we are setting tracking
+ *     for.
+ *
+ *   - orig_ref is the name of the ref that is 'upstream' of new_ref.
+ *     orig_ref will be expanded with DWIM so that the config settings
+ *     are in the correct format e.g. "refs/remotes/origin/main" instead
+ *     of "origin/main".
+ *
+ *   - track is the tracking mode e.g. BRANCH_TRACK_REMOTE causes
+ *     new_ref to track orig_ref directly, whereas BRANCH_TRACK_INHERIT
+ *     causes new_ref to track whatever orig_ref tracks.
+ *
+ *   - quiet suppresses tracking information.
+ */
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet);
+
 /*
  * Creates a new branch, where:
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index e99bdae897..4a9e0f5727 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -828,12 +828,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		dwim_and_setup_tracking(the_repository, branch->name,
+					new_upstream, BRANCH_TRACK_OVERRIDE,
+					quiet);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v5 2/5] branch: make create_branch() always create a branch
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
  2021-12-16 23:33         ` [PATCH v5 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
@ 2021-12-16 23:33         ` Glen Choo
  2021-12-16 23:33         ` [PATCH v5 3/5] branch: add a dry_run parameter to create_branch() Glen Choo
                           ` (4 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16 23:33 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

create_branch() was formerly used to set tracking without creating a
branch. Since the previous commit replaces this use case with
dwim_and_setup_tracking(), we can simplify create_branch() so that it
always creates a branch.

Do this simplification, in particular:

* remove the special handling of BRANCH_TRACK_OVERRIDE because it is no
  longer used
* assert that clobber_head_ok can only be provided with force
* check that we're handling clobber_head_ok and force correctly by
  introducing tests for `git branch --force`

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c          | 55 +++++++++++++++++++++--------------------------
 branch.h          |  4 ++--
 t/t3200-branch.sh | 17 +++++++++++++++
 3 files changed, 44 insertions(+), 32 deletions(-)

diff --git a/branch.c b/branch.c
index 54e989f3c8..6f474a5505 100644
--- a/branch.c
+++ b/branch.c
@@ -426,15 +426,17 @@ void create_branch(struct repository *r, const char *name,
 	char *real_ref;
 	struct strbuf ref = STRBUF_INIT;
 	int forcing = 0;
-	int dont_change_ref = 0;
-
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
+	struct ref_transaction *transaction;
+	struct strbuf err = STRBUF_INIT;
+	char *msg;
+
+	if (clobber_head_ok && !force)
+		BUG("'clobber_head_ok' can only be used with 'force'");
+
+	if (clobber_head_ok ?
+			  validate_branchname(name, &ref) :
+			  validate_new_branchname(name, &ref, force)) {
+		forcing = 1;
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
@@ -442,27 +444,20 @@ void create_branch(struct repository *r, const char *name,
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
-	if (!dont_change_ref) {
-		struct ref_transaction *transaction;
-		struct strbuf err = STRBUF_INIT;
-		char *msg;
-
-		if (forcing)
-			msg = xstrfmt("branch: Reset to %s", start_name);
-		else
-			msg = xstrfmt("branch: Created from %s", start_name);
-
-		transaction = ref_transaction_begin(&err);
-		if (!transaction ||
-		    ref_transaction_update(transaction, ref.buf,
-					   &oid, forcing ? NULL : null_oid(),
-					   0, msg, &err) ||
-		    ref_transaction_commit(transaction, &err))
-			die("%s", err.buf);
-		ref_transaction_free(transaction);
-		strbuf_release(&err);
-		free(msg);
-	}
+	if (forcing)
+		msg = xstrfmt("branch: Reset to %s", start_name);
+	else
+		msg = xstrfmt("branch: Created from %s", start_name);
+	transaction = ref_transaction_begin(&err);
+	if (!transaction ||
+		ref_transaction_update(transaction, ref.buf,
+					&oid, forcing ? NULL : null_oid(),
+					0, msg, &err) ||
+		ref_transaction_commit(transaction, &err))
+		die("%s", err.buf);
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	free(msg);
 
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
diff --git a/branch.h b/branch.h
index ab2315c611..cf3a4d3ff3 100644
--- a/branch.h
+++ b/branch.h
@@ -52,8 +52,8 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *
  *   - force enables overwriting an existing (non-head) branch
  *
- *   - clobber_head_ok allows the currently checked out (hence existing)
- *     branch to be overwritten; without 'force', it has no effect.
+ *   - clobber_head_ok, when enabled with 'force', allows the currently
+ *     checked out (head) branch to be overwritten
  *
  *   - reflog creates a reflog for the branch
  *
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 1bc3795847..7a0ff75ba8 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
 	git branch abc && test_path_is_file .git/refs/heads/abc
 '
 
+test_expect_success 'git branch abc should fail when abc exists' '
+	test_must_fail git branch abc
+'
+
+test_expect_success 'git branch --force abc should fail when abc is checked out' '
+	test_when_finished git switch main &&
+	git switch abc &&
+	test_must_fail git branch --force abc HEAD~1
+'
+
+test_expect_success 'git branch --force abc should succeed when abc exists' '
+	git rev-parse HEAD~1 >expect &&
+	git branch --force abc HEAD~1 &&
+	git rev-parse abc >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v5 3/5] branch: add a dry_run parameter to create_branch()
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
  2021-12-16 23:33         ` [PATCH v5 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
  2021-12-16 23:33         ` [PATCH v5 2/5] branch: make create_branch() always create a branch Glen Choo
@ 2021-12-16 23:33         ` Glen Choo
  2021-12-16 23:33         ` [PATCH v5 4/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
                           ` (3 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16 23:33 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a dry_run parameter to create_branch() such that dry_run = 1 will
validate a new branch without trying to create it. This will be used in
`git branch --recurse-submodules` to ensure that the new branch can be
created in all submodules.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c           | 5 ++++-
 branch.h           | 9 +++++----
 builtin/branch.c   | 2 +-
 builtin/checkout.c | 3 ++-
 4 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/branch.c b/branch.c
index 6f474a5505..047d9958fc 100644
--- a/branch.c
+++ b/branch.c
@@ -420,7 +420,7 @@ static void dwim_branch_start(struct repository *r, const char *start_name,
 
 void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track)
+		   int reflog, int quiet, enum branch_track track, int dry_run)
 {
 	struct object_id oid;
 	char *real_ref;
@@ -440,6 +440,8 @@ void create_branch(struct repository *r, const char *name,
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
+	if (dry_run)
+		goto cleanup;
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -462,6 +464,7 @@ void create_branch(struct repository *r, const char *name,
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
 
+cleanup:
 	strbuf_release(&ref);
 	free(real_ref);
 }
diff --git a/branch.h b/branch.h
index cf3a4d3ff3..8009266343 100644
--- a/branch.h
+++ b/branch.h
@@ -62,11 +62,12 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *   - track causes the new branch to be configured to merge the remote branch
  *     that start_name is a tracking branch for (if any).
  *
+ *   - dry_run causes the branch to be validated but not created.
+ *
  */
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track);
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track, int dry_run);
 
 /*
  * Check if 'name' can be a valid name for a branch; die otherwise.
diff --git a/builtin/branch.c b/builtin/branch.c
index 4a9e0f5727..d785ebdba0 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -864,7 +864,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 
 		create_branch(the_repository,
 			      argv[0], (argc == 2) ? argv[1] : head,
-			      force, 0, reflog, quiet, track);
+			      force, 0, reflog, quiet, track, 0);
 
 	} else
 		usage_with_options(builtin_branch_usage, options);
diff --git a/builtin/checkout.c b/builtin/checkout.c
index a1ba3e2b56..da183724d3 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -905,7 +905,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				      opts->new_branch_force ? 1 : 0,
 				      opts->new_branch_log,
 				      opts->quiet,
-				      opts->track);
+				      opts->track,
+				      0);
 		free(new_branch_info->name);
 		free(new_branch_info->refname);
 		new_branch_info->name = xstrdup(opts->new_branch);
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v5 4/5] builtin/branch: clean up action-picking logic in cmd_branch()
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
                           ` (2 preceding siblings ...)
  2021-12-16 23:33         ` [PATCH v5 3/5] branch: add a dry_run parameter to create_branch() Glen Choo
@ 2021-12-16 23:33         ` Glen Choo
  2021-12-16 23:33         ` [PATCH v5 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
                           ` (2 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16 23:33 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a variable to cmd_branch() that will tell us whether or not
cmd_branch() will default to creating a branch (instead of performing
another action). Besides making the function more explicit, this allows
us to validate options that can only be used when creating a branch.
Such an option does not exist yet, but one will be introduced in a
subsequent commit.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 builtin/branch.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index d785ebdba0..6c0069bf00 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -621,14 +621,15 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, list = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	const char *new_upstream = NULL;
+	int noncreate_actions = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, icase = 0, force = 0;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting;
 	struct string_list sorting_options = STRING_LIST_INIT_DUP;
 	struct ref_format format = REF_FORMAT_INIT;
@@ -713,8 +714,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
+			    !!show_current + !!list + !!edit_description +
+			    !!unset_upstream;
+	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
 	if (filter.abbrev == -1)
@@ -854,7 +857,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
 		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>?"));
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v5 5/5] branch: add --recurse-submodules option for branch creation
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
                           ` (3 preceding siblings ...)
  2021-12-16 23:33         ` [PATCH v5 4/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
@ 2021-12-16 23:33         ` Glen Choo
  2021-12-17  0:34         ` [PATCH v5 0/5] implement branch --recurse-submodules Junio C Hamano
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-16 23:33 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

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 semantics e.g. `git
checkout` does not recursively checkout the expected branches created by
`git branch` yet. To ensure that the correct set of semantics is used,
this commit introduces a new configuration value,
`submodule.propagateBranches`, which enables submodule branching when
true (defaults to false).

This commit includes changes that allow Git to work with submodules
that are in trees (and not just the index):

* 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).

Signed-off-by: Glen Choo <chooglen@google.com>
---
 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  24 ++-
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 130 +++++++++++++
 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        | 291 +++++++++++++++++++++++++++++
 14 files changed, 659 insertions(+), 14 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

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 61d975745e..61492d09a8 100644
--- a/Documentation/config/submodule.txt
+++ b/Documentation/config/submodule.txt
@@ -59,11 +59,8 @@ 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`.
-	Defaults to false.
+	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
@@ -71,6 +68,23 @@ submodule.recurse::
 	`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.
 
 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 2d52ae396b..435dc2b20e 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 047d9958fc..78cf5af8e2 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;
@@ -478,6 +480,134 @@ 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() (and its variants) do
+ * 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);
+	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 8009266343..63448e4278 100644
--- a/branch.h
+++ b/branch.h
@@ -69,6 +69,28 @@ void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
 		   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 6c0069bf00..47155e126f 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] = {
@@ -99,6 +102,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);
 }
 
@@ -627,7 +639,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;
@@ -678,6 +691,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(),
 	};
@@ -720,6 +734,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;
@@ -858,6 +883,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>?"));
@@ -865,10 +893,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 6a66c8363c..cc9a2bdbaf 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)
@@ -2995,6 +2996,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;
@@ -3401,6 +3438,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 ad95cdda07..95af84347d 100644
--- a/submodule.c
+++ b/submodule.c
@@ -267,7 +267,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;
@@ -275,7 +277,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)
@@ -317,6 +319,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..0407220f5c
--- /dev/null
+++ b/t/t3207-branch-submodule.sh
@@ -0,0 +1,291 @@
+#!/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
+
+test_expect_success 'setup superproject and submodule' '
+	git init super &&
+	test_commit 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 "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
+	git -C sub-upstream commit -m "add submodule" &&
+	# Regular submodule
+	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
+	# Submodule in a subdirectory
+	git -C super submodule add "$TRASH_DIRECTORY/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
+'
+
+CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
+
+cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
+	#!/bin/sh
+
+	super_dir="$1"
+	shift
+	(
+		cd "$super_dir" &&
+		git checkout main &&
+		for branch_name in "$@"; do
+			git branch -D "$branch_name"
+			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
+		done
+	)
+EOF
+chmod +x "$CLEANUP_SCRIPT_PATH"
+
+cleanup_branches() {
+	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
+} >/dev/null 2>/dev/null
+
+# Test the argument parsing
+test_expect_success '--recurse-submodules should create branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should respect submodule.recurse when creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -c submodule.recurse=true branch branch-a &&
+		git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should ignore submodule.recurse when not creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git -c submodule.recurse=true branch -D branch-a &&
+		test_must_fail git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+# Test branch creation behavior
+test_expect_success 'should create branches based off commit id in superproject' '
+	test_when_finished "cleanup_branches super branch-a branch-b" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 &&
+
+		cat >expected <<-EOF &&
+		submodule ${SQ}sub${SQ}: fatal: a branch named ${SQ}branch-a${SQ} already exists
+		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
+		EOF
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branches if branch exists and --force is given' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -C sub rev-parse HEAD >expected &&
+		test_commit -C sub baz &&
+		git -C sub branch branch-a HEAD~1 &&
+		git branch --recurse-submodules --force branch-a &&
+		git rev-parse branch-a &&
+		# assert that sub:branch-a was moved
+		git -C sub rev-parse branch-a >actual &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
+	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
+	(
+		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 rev-parse branch-c &&
+		git -C sub rev-parse branch-c &&
+		git -C second/sub rev-parse branch-c &&
+		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 set up tracking of local branches with track=always' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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)" = ""
+	)
+'
+
+test_expect_success 'should not create branches in inactive submodules' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	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 'setup remote-tracking tests' '
+	(
+		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
+'
+
+test_expect_success 'should not create branch when submodule is not in .git/modules' '
+	# The cleanup needs to delete sub2 separately because main does not have sub2
+	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
+		git -C super-clone/sub2/sub-sub branch -D branch-b && \
+		cleanup_branches super-clone branch-a branch-b" &&
+	(
+		cd super-clone &&
+		# This should succeed because super-clone has sub.
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		# This should fail because super-clone does not have sub2.
+		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
+		cat >expected <<-EOF &&
+		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
+		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
+		EOF
+		test_cmp expected 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 "cleanup_branches super-clone branch-a" &&
+	(
+		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 "cleanup_branches super-clone branch-a && \
+		git -C super-clone remote rename ex-origin origin" &&
+	(
+		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
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v5 0/5] implement branch --recurse-submodules
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
                           ` (4 preceding siblings ...)
  2021-12-16 23:33         ` [PATCH v5 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-17  0:34         ` Junio C Hamano
  2021-12-17  0:45           ` Junio C Hamano
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
  6 siblings, 1 reply; 110+ messages in thread
From: Junio C Hamano @ 2021-12-17  0:34 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Glen Choo <chooglen@google.com> writes:

> This series implements branch --recurse-submodules as laid out in the
> Submodule branching RFC (linked above). If there are concerns about the
> UX/behavior, I would appreciate feedback on the RFC thread as well :)
>
> This series is based off js/branch-track-inherit.

Sigh.

When a series is labelled as "based off of X", I expect that the
series either apply on the tip of branch X I have, or it applies on
top of the merge result of branch X into 'master'.  It shouldn't be
forked at a random point on the 'seen' or 'next' branch, as you'd
end up depending on not just X but all the other topics that are
merged before X is merged to these integration branches.

This seems not apply on either c99fa303 (config: require lowercase
for branch.autosetupmerge, 2021-12-14), which is the tip of the
js/branch-track-inherit topic, or 47e85bee (Merge branch
'js/branch-track-inherit' into gc/branch-recurse-submodules,
2021-12-15), which is a merge of that topic into 'master' I prepared
to queue the previous round of this topic the other day.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v5 0/5] implement branch --recurse-submodules
  2021-12-17  0:34         ` [PATCH v5 0/5] implement branch --recurse-submodules Junio C Hamano
@ 2021-12-17  0:45           ` Junio C Hamano
  2021-12-20 19:09             ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Junio C Hamano @ 2021-12-17  0:45 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Junio C Hamano <gitster@pobox.com> writes:

> Glen Choo <chooglen@google.com> writes:
>
>> This series implements branch --recurse-submodules as laid out in the
>> Submodule branching RFC (linked above). If there are concerns about the
>> UX/behavior, I would appreciate feedback on the RFC thread as well :)
>>
>> This series is based off js/branch-track-inherit.
>
> Sigh.
>
> When a series is labelled as "based off of X", I expect that the
> series either apply on the tip of branch X I have, or it applies on
> top of the merge result of branch X into 'master'.  It shouldn't be
> forked at a random point on the 'seen' or 'next' branch, as you'd
> end up depending on not just X but all the other topics that are
> merged before X is merged to these integration branches.
>
> This seems not apply on either c99fa303 (config: require lowercase
> for branch.autosetupmerge, 2021-12-14), which is the tip of the
> js/branch-track-inherit topic, or 47e85bee (Merge branch
> 'js/branch-track-inherit' into gc/branch-recurse-submodules,
> 2021-12-15), which is a merge of that topic into 'master' I prepared
> to queue the previous round of this topic the other day.

Ah, I figured it out.

These are based on the merge of the other branch into 'seen'.  I
have (deliberately) merged js/branch-track-inherit and the previous
round of this tipc in 'seen' next to each other.

And when these five are applied on top of that merge of the other
topic into 'seen', we get an identical tree as the merge of the
previous round of this topic into 'seen'.

So unless you updated some commit log message, nothing is lost if I
ignore this round.  Just to save time for both of us the next time,
plesae fetch from any of the public tree, find on the first parent
chain leading to 'origin/seen' a commit labelled as "Merge branch
'gc/branch-recurse-submodules'", and check out its second parent,
and what we have there.

    $ git checkout "origin/seen^{/^Merge branch .gc/branch-rec}^2"
    $ git log --first-parent --oneline origin/main..
    35bb9f67f9 branch: add --recurse-submodules option for branch creation
    ce3a710d42 builtin/branch: clean up action-picking logic in cmd_branch()
    f368230ca9 branch: add a dry_run parameter to create_branch()
    d77f8a125b branch: make create_branch() always create a branch
    f8a88a03b9 branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
    47e85beee9 Merge branch 'js/branch-track-inherit' into gc/branch-recurse-submodules

If you "rebase -i 47e85beee9" (the exact object name might differ,
as that commit needs to be recreated when 'js/branch-track-inherit'
is updated) these five commits, and format-patch everything on the
topic with --base=47e85beee9, it is guaranteed that I'll be able to
cleanly apply what you meant to send out on top of 47e85beee9.

Thanks.







^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v5 0/5] implement branch --recurse-submodules
  2021-12-17  0:45           ` Junio C Hamano
@ 2021-12-20 19:09             ` Glen Choo
  2021-12-20 19:50               ` Junio C Hamano
  0 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-20 19:09 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Junio C Hamano <gitster@pobox.com> writes:

> Junio C Hamano <gitster@pobox.com> writes:
>
>> Glen Choo <chooglen@google.com> writes:
>>
>>> This series implements branch --recurse-submodules as laid out in the
>>> Submodule branching RFC (linked above). If there are concerns about the
>>> UX/behavior, I would appreciate feedback on the RFC thread as well :)
>>>
>>> This series is based off js/branch-track-inherit.
>>
>> Sigh.
>>
>> When a series is labelled as "based off of X", I expect that the
>> series either apply on the tip of branch X I have, or it applies on
>> top of the merge result of branch X into 'master'.  It shouldn't be
>> forked at a random point on the 'seen' or 'next' branch, as you'd
>> end up depending on not just X but all the other topics that are
>> merged before X is merged to these integration branches.
>>
>> This seems not apply on either c99fa303 (config: require lowercase
>> for branch.autosetupmerge, 2021-12-14), which is the tip of the
>> js/branch-track-inherit topic, or 47e85bee (Merge branch
>> 'js/branch-track-inherit' into gc/branch-recurse-submodules,
>> 2021-12-15), which is a merge of that topic into 'master' I prepared
>> to queue the previous round of this topic the other day.
>
> Ah, I figured it out.
>
> These are based on the merge of the other branch into 'seen'.  I
> have (deliberately) merged js/branch-track-inherit and the previous
> round of this tipc in 'seen' next to each other.

Oh my goodness.. I'm sorry, I didn't mean to complicate matters like
this.

If it's alright with you, I'd like to check my understanding so that I
can avoid this mistake in the future.

What happened was that I got confused by [1], where it reads:

  [...]
  find the tip of js/branch-track-inherit from 'seen' [*]
  [...]

  [Footnote]

  * One way to do so would be:

    $ git fetch
    $ git show 'remote/origin/seen^{/^Merge branch .js/branch-track-inherit.}'

The commit that I got was the "merge of js/branch-track-inherit into
'seen'", but what you intended was the "merge of js/branch-track-inherit
into gc/branch-recurse-submodules"; I didn't realize that there might
have been more than commit matching that regex.

This makes much more sense in the context of your comment:

  That's OK; please do not ever rebase anything on top of 'seen' or
  'next'.

> And when these five are applied on top of that merge of the other
> topic into 'seen', we get an identical tree as the merge of the
> previous round of this topic into 'seen'.
>
> So unless you updated some commit log message, nothing is lost if I
> ignore this round.

I made some commit message changes. Unless you think it's a good idea, I
won't re-roll this to fix the issue.

> Just to save time for both of us the next time,
> plesae fetch from any of the public tree, find on the first parent
> chain leading to 'origin/seen' a commit labelled as "Merge branch
> 'gc/branch-recurse-submodules'", and check out its second parent,
> and what we have there.
>
>     $ git checkout "origin/seen^{/^Merge branch .gc/branch-rec}^2"
>     $ git log --first-parent --oneline origin/main..
>     35bb9f67f9 branch: add --recurse-submodules option for branch creation
>     ce3a710d42 builtin/branch: clean up action-picking logic in cmd_branch()
>     f368230ca9 branch: add a dry_run parameter to create_branch()
>     d77f8a125b branch: make create_branch() always create a branch
>     f8a88a03b9 branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
>     47e85beee9 Merge branch 'js/branch-track-inherit' into gc/branch-recurse-submodules
>
> If you "rebase -i 47e85beee9" (the exact object name might differ,
> as that commit needs to be recreated when 'js/branch-track-inherit'
> is updated) these five commits, and format-patch everything on the
> topic with --base=47e85beee9, it is guaranteed that I'll be able to
> cleanly apply what you meant to send out on top of 47e85beee9.

So if my branch were not in 'seen', I should have based my changes on
'origin/js/branch-track-inherit'. If my branch is in 'seen', I should
base it off the merge of js/branch-track-inherit' into my my branch?

I'll continue to use format-patch --base because I see how that can be
useful for you.

[1] https://lore.kernel.org/git/xmqqlf0lz6os.fsf@gitster.g/

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v5 0/5] implement branch --recurse-submodules
  2021-12-20 19:09             ` Glen Choo
@ 2021-12-20 19:50               ` Junio C Hamano
  2021-12-20 20:25                 ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Junio C Hamano @ 2021-12-20 19:50 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Glen Choo <chooglen@google.com> writes:

> What happened was that I got confused by [1], where it reads:
>
>   [...]
>   find the tip of js/branch-track-inherit from 'seen' [*]
>   [...]
>
>   [Footnote]
>
>   * One way to do so would be:
>
>     $ git fetch
>     $ git show 'remote/origin/seen^{/^Merge branch .js/branch-track-inherit.}'
>
> The commit that I got was the "merge of js/branch-track-inherit into
> 'seen'", but what you intended was the "merge of js/branch-track-inherit
> into gc/branch-recurse-submodules"; I didn't realize that there might
> have been more than commit matching that regex.

Yeah, that was not quite clearly written.  The way it was showing
was to find the tip of the other branch.  The instruction was to
prepare you (and others reading from the sidelines) for a case where
your branch depends on somebody else's work that is *not* even in
'seen' (e.g. I may have an older version of 'seen' but there is a
newer and clearly improved version on the list that is likely to
replace).  In such a case, you'd 

 (1) "find" the tip of the other branch, either by traversing from
     the tip of 'seen' to find the merge and taking its second
     parent, or applying the latest from the list to a locally
     created topic branch forked off of 'main',

 (2) create your topic branch, forked off of 'main', and merge (1)
     into it, and

 (3) build your series on it.

If I have your previous round, and if the other topic you depend on
hasn't changed, you can omit (2) and instead find the equivalent of
(2) I created for your topic the last time I queued it.

> I made some commit message changes. Unless you think it's a good idea, I
> won't re-roll this to fix the issue.

Let's not waste your message changes to clarify the patches.

> So if my branch were not in 'seen', I should have based my changes on
> 'origin/js/branch-track-inherit'. If my branch is in 'seen', I should
> base it off the merge of js/branch-track-inherit' into my my branch?

Hopefully the above is clear now?  Sorry for the trouble.

Thanks.


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v5 0/5] implement branch --recurse-submodules
  2021-12-20 19:50               ` Junio C Hamano
@ 2021-12-20 20:25                 ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-20 20:25 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Junio C Hamano <gitster@pobox.com> writes:

> Glen Choo <chooglen@google.com> writes:
>
>> What happened was that I got confused by [1], where it reads:
>>
>>   [...]
>>   find the tip of js/branch-track-inherit from 'seen' [*]
>>   [...]
>>
>>   [Footnote]
>>
>>   * One way to do so would be:
>>
>>     $ git fetch
>>     $ git show 'remote/origin/seen^{/^Merge branch .js/branch-track-inherit.}'
>>
>> The commit that I got was the "merge of js/branch-track-inherit into
>> 'seen'", but what you intended was the "merge of js/branch-track-inherit
>> into gc/branch-recurse-submodules"; I didn't realize that there might
>> have been more than commit matching that regex.
>
> Yeah, that was not quite clearly written.  The way it was showing
> was to find the tip of the other branch.  The instruction was to
> prepare you (and others reading from the sidelines) for a case where
> your branch depends on somebody else's work that is *not* even in
> 'seen' (e.g. I may have an older version of 'seen' but there is a
> newer and clearly improved version on the list that is likely to
> replace).  In such a case, you'd 
>
>  (1) "find" the tip of the other branch, either by traversing from
>      the tip of 'seen' to find the merge and taking its second
>      parent, or applying the latest from the list to a locally
>      created topic branch forked off of 'main',
>
>  (2) create your topic branch, forked off of 'main', and merge (1)
>      into it, and
>
>  (3) build your series on it.
>
> If I have your previous round, and if the other topic you depend on
> hasn't changed, you can omit (2) and instead find the equivalent of
> (2) I created for your topic the last time I queued it.
>
>> I made some commit message changes. Unless you think it's a good idea, I
>> won't re-roll this to fix the issue.
>
> Let's not waste your message changes to clarify the patches.
>
>> So if my branch were not in 'seen', I should have based my changes on
>> 'origin/js/branch-track-inherit'. If my branch is in 'seen', I should
>> base it off the merge of js/branch-track-inherit' into my my branch?
>
> Hopefully the above is clear now?  Sorry for the trouble.
>
> Thanks.

It's no trouble for me. I should be thanking you for taking the time to
make it clear :) I really appreciate it.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v6 0/5] implement branch --recurse-submodules
  2021-12-16 23:33       ` [PATCH v5 0/5] implement branch --recurse-submodules Glen Choo
                           ` (5 preceding siblings ...)
  2021-12-17  0:34         ` [PATCH v5 0/5] implement branch --recurse-submodules Junio C Hamano
@ 2021-12-20 23:34         ` Glen Choo
  2021-12-20 23:34           ` [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
                             ` (7 more replies)
  6 siblings, 8 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-20 23:34 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Submodule branching RFC:
https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/

Original Submodule UX RFC/Discussion:
https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/

Contributor Summit submodules Notes:
https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/

Submodule UX overhaul updates:
https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update

This series implements branch --recurse-submodules as laid out in the
Submodule branching RFC (linked above). If there are concerns about the
UX/behavior, I would appreciate feedback on the RFC thread as well :)

v6 fixes v5's bad rebase; it was based off a merge commit in 'seen'
instead of js/branch-track-inherit. Since v5 is mostly a no-op, I will
also include a range-diff against v4, which is the last version that
anyone except Junio would care about.

This version is based off Josh's js/branch-track-inherit v7. Because that
is not yet in 'seen', I applied those patches onto abe6bb3905 (The first
batch to start the current cycle, 2021-11-29). The base commit can be
found at https://github.com/chooglen/git/tree/js/branch-track-inherit-v7.
I really hope I got right this time, but if not, hopefully the paper
trail is obvious.

Future work:
* `git branch -d --recurse-submodules` so that users can clean up
  extraneous branches.
* `git [checkout | switch] --recurse-submodules` +
  submodule.propagateBranches so that users can actually checkout the
  branches.
* After [1], it seems clear that --recurse-submodules parsing could
  really benefit from some standardization. It's not obvious which
  RECURSE_SUBMODULE_* enums are applicable to which commands, and there
  is no way to distinguish between an explicit --recurse-submodules from
  argv vs submodule.recurse from the config.

  I chose not to use them in this series because their usage is already
  inconsistent (grep.c doesn't use them either), and it would be _more_
  confusing to use the enum (handling RECURSE_SUBMODULES_DEFAULT = 1 is
  trickier than boolean 0 and 1).

  At this point, I think it would be too noisy to introduce the enum,
  but this would be a nice cleanup to do later.
* As documented in branch.c, we create branches using a child process
  only because install_branch_config() does not support submodules.
  It should be possible to remove the child process once we make the
  appropriate changes to config.c. I attempted this in [2] but chose to
  punt it because it was too time-consuming at the time.

Changes since v5:
* Rebase onto v7 of js/branch-track-inherit
  (https://lore.kernel.org/git/cover.1639717481.git.steadmon@google.com)

Changes since v4:
* Rebase correctly onto 'gitster/seen^{/^Merge branch .js/branch-track-inherit.}'
  (see base-commit) as suggested in [3] (thanks Junio!)
* These patches were also verified on top of 'next'.

Changes since v3:
* Split up the old patch 1. Patch 1 had a big diff because it used to
  move lines, remove dead code and introduce repo_* functions (thanks
  Jonathan!)
** repo_* functions have been dropped; they added noise and are not
   necessary for correctness.
* Use a new, harder-to-misuse function in --set-upstream-to,
  dwim_and_setup_tracking(). Now, setup_tracking() never does DWIM and
  dwim_and_setup_tracking() always does DWIM.
* Move create_branch() dry_run to its own patch.
* Fix an oversight where submodules in subtrees were ignored. This was
  because submodules_of_tree() and tree_entry() didn't recurse into
  subtrees. Test this accordingly (thanks Jonathan!).
* cmd_branch() possible actions are more consistently ordered.
* Documentation fixes (thanks Philippe!).
* Additional comments and explanation.
* Drop patch 5 (optional cleanup).
* Rebase onto js/branch-track-inherit v6.

Changes since v2:
* Rebase onto js/branch-track-inherit. This series should continue to be
  the case going forward.
* Patch 1 has a smaller diff because the introduction of
  validate_branch_start() no longer changes the function order thanks to a
  forward declaration. This artificial forward declaration is removed in a
  patch 2 (which can just be squashed into patch 1).
* Optional cleanup: fix questionable exit codes in patch 5.

Changes since v1:
* Move the functionality of "git branch --dry-run" into "git submodule-helper create-branch --dry-run"
* Add more fields to the submodules_of_tree() struct to reduce the
  number of allocations made by the caller. Move this functionality
  to patch 3 (formerly patch 4) and drop patch 1.
* Make submodules_of_tree() ignore inactive submodules
* Structure the output of the submodules a bit better by adding prefixes
  to the child process' output (instead of inconsistently indenting the
  output).
** I wasn't able to find a good way to interleave stdout/stderr
   correctly, so a less-than-desirable workaround was to route the child
   process output to stdout/stderr depending on the exit code.
** Eventually, I would like to structure the output of submodules in a
   report, as Ævar suggested. But at this stage, I think that it's
   better to spend time getting user feedback on the submodules
   branching UX and it'll be easier to standardize the output when we've
   implemented more of the UX :)

[1] https://lore.kernel.org/git/kl6lbl1p9zjf.fsf@chooglen-macbookpro.roam.corp.google.com/
[2] https://lore.kernel.org/git/kl6lv90ytd4v.fsf@chooglen-macbookpro.roam.corp.google.com/
[3] https://lore.kernel.org/git/xmqqlf0lz6os.fsf@gitster.g 

Glen Choo (5):
  branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  branch: make create_branch() always create a branch
  branch: add a dry_run parameter to create_branch()
  builtin/branch: clean up action-picking logic in cmd_branch()
  branch: add --recurse-submodules option for branch creation

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  24 ++-
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 257 ++++++++++++++++++++-----
 branch.h                           |  57 +++++-
 builtin/branch.c                   |  70 +++++--
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  60 ++++++
 submodule-config.h                 |  34 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3200-branch.sh                  |  17 ++
 t/t3207-branch-submodule.sh        | 291 +++++++++++++++++++++++++++++
 16 files changed, 805 insertions(+), 76 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

Range-diff against v5:
1:  a9d1108b3e ! 1:  29669c57b4 branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
    @@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
      
      	if (tracking.matches > 1)
     @@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
    - 			      tracking.srcs) < 0)
    + 				tracking.remote, tracking.srcs) < 0)
      		exit(-1);
      
     +cleanup:
    @@ branch.c: N_("\n"
     @@ branch.c: void create_branch(struct repository *r,
      
      	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
    - 		die(_("not a valid branch point: '%s'"), start_name);
    + 		die(_("Not a valid branch point: '%s'."), start_name);
     -	oidcpy(&oid, &commit->object.oid);
     +	if (out_real_ref)
     +		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
2:  c543c1412a = 2:  ac2532a953 branch: make create_branch() always create a branch
3:  dddd434d7a = 3:  a0ed3fa438 branch: add a dry_run parameter to create_branch()
4:  41cca3bd52 = 4:  ebded31c96 builtin/branch: clean up action-picking logic in cmd_branch()
5:  540eeab183 ! 5:  0a7ec6ee75 branch: add --recurse-submodules option for branch creation
    @@ t/t3207-branch-submodule.sh (new)
     +		test_must_fail git rev-parse branch-a &&
     +
     +		cat >expected <<-EOF &&
    -+		submodule ${SQ}sub${SQ}: fatal: a branch named ${SQ}branch-a${SQ} already exists
    ++		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
     +		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
     +		EOF
     +		test_cmp expected actual

base-commit: e1c6879cf24f90693ca108542ec1db970764e1dc
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
@ 2021-12-20 23:34           ` Glen Choo
  2022-01-11  2:09             ` Jonathan Tan
  2021-12-20 23:34           ` [PATCH v6 2/5] branch: make create_branch() always create a branch Glen Choo
                             ` (6 subsequent siblings)
  7 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-20 23:34 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

This refactor is motivated by a desire to add a "dry_run" parameter to
create_branch() that will validate whether or not a branch can be
created without actually creating it - this behavior will be used in a
subsequent commit that adds `git branch --recurse-submodules topic`.

Adding "dry_run" is not obvious because create_branch() is also used to
set tracking information without creating a branch, i.e. when using
--set-upstream-to. This appears to be a leftover from 4fc5006676 (Add
branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
create a branch and sometimes update tracking information without
creating a branch. However, we no longer support --set-upstream, so it
makes more sense to set tracking information with another function and
use create_branch() only to create branches. In a later commit, we will
remove the now-unnecessary logic from create_branch() so that "dry_run"
becomes trivial to implement.

Introduce dwim_and_setup_tracking(), which replaces create_branch()
in `git branch --set-upstream-to`. Ensure correctness by moving the DWIM
and branch validation logic from create_branch() into a helper function,
dwim_branch_start(), so that the logic is shared by both functions.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c         | 87 ++++++++++++++++++++++++++++++++++++------------
 branch.h         | 22 ++++++++++++
 builtin/branch.c |  9 ++---
 3 files changed, 91 insertions(+), 27 deletions(-)

diff --git a/branch.c b/branch.c
index fa339b64e2..e271a4e0a7 100644
--- a/branch.c
+++ b/branch.c
@@ -217,9 +217,11 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
 }
 
 /*
- * 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.
+ * Used internally to set the branch.<new_ref>.{remote,merge} config
+ * settings so that branch 'new_ref' tracks 'orig_ref'. Unlike
+ * dwim_and_setup_tracking(), this does not do DWIM, i.e. "origin/main"
+ * will not be expanded to "refs/remotes/origin/main", so it is not safe
+ * for 'orig_ref' to be raw user input.
  */
 static void setup_tracking(const char *new_ref, const char *orig_ref,
 			   enum branch_track track, int quiet)
@@ -244,7 +246,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 		case BRANCH_TRACK_INHERIT:
 			break;
 		default:
-			return;
+			goto cleanup;
 		}
 
 	if (tracking.matches > 1)
@@ -257,6 +259,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
 				tracking.remote, tracking.srcs) < 0)
 		exit(-1);
 
+cleanup:
 	string_list_clear(tracking.srcs, 0);
 }
 
@@ -340,31 +343,37 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+/**
+ * DWIMs a user-provided ref to determine the starting point for a
+ * branch and validates it, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test. This is
+ *     expanded with DWIM and assigned to out_real_ref.
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - out_oid is an out parameter containing the object_id of start_name
+ *
+ *   - out_real_ref is an out parameter containing the full, 'real' form
+ *     of start_name e.g. refs/heads/main instead of main
+ *
+ */
+static void dwim_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, char **out_real_ref,
+			   struct object_id *out_oid)
 {
 	struct commit *commit;
 	struct object_id oid;
 	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
 	int explicit_tracking = 0;
 
 	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
 		explicit_tracking = 1;
 
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
-
 	real_ref = NULL;
 	if (get_oid_mb(start_name, &oid)) {
 		if (explicit_tracking) {
@@ -401,7 +410,34 @@ void create_branch(struct repository *r,
 
 	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
 		die(_("Not a valid branch point: '%s'."), start_name);
-	oidcpy(&oid, &commit->object.oid);
+	if (out_real_ref)
+		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
+	if (out_oid)
+		oidcpy(out_oid, &commit->object.oid);
+
+	FREE_AND_NULL(real_ref);
+}
+
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track)
+{
+	struct object_id oid;
+	char *real_ref;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	int dont_change_ref = 0;
+
+	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
+	    ? validate_branchname(name, &ref)
+	    : validate_new_branchname(name, &ref, force)) {
+		if (!force)
+			dont_change_ref = 1;
+		else
+			forcing = 1;
+	}
+
+	dwim_branch_start(r, start_name, track, &real_ref, &oid);
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -435,6 +471,15 @@ void create_branch(struct repository *r,
 	free(real_ref);
 }
 
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet)
+{
+	char *real_orig_ref;
+	dwim_branch_start(r, orig_ref, track, &real_orig_ref, NULL);
+	setup_tracking(new_ref, real_orig_ref, track, quiet);
+}
+
 void remove_merge_branch_state(struct repository *r)
 {
 	unlink(git_path_merge_head(r));
diff --git a/branch.h b/branch.h
index 815dcd40c0..ab2315c611 100644
--- a/branch.h
+++ b/branch.h
@@ -18,6 +18,28 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/**
+ * Sets branch.<new_ref>.{remote,merge} config settings such that
+ * new_ref tracks orig_ref according to the specified tracking mode.
+ *
+ *   - new_ref is the name of the branch that we are setting tracking
+ *     for.
+ *
+ *   - orig_ref is the name of the ref that is 'upstream' of new_ref.
+ *     orig_ref will be expanded with DWIM so that the config settings
+ *     are in the correct format e.g. "refs/remotes/origin/main" instead
+ *     of "origin/main".
+ *
+ *   - track is the tracking mode e.g. BRANCH_TRACK_REMOTE causes
+ *     new_ref to track orig_ref directly, whereas BRANCH_TRACK_INHERIT
+ *     causes new_ref to track whatever orig_ref tracks.
+ *
+ *   - quiet suppresses tracking information.
+ */
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet);
+
 /*
  * Creates a new branch, where:
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index 81a29edb4a..16a7e80df5 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -823,12 +823,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		dwim_and_setup_tracking(the_repository, branch->name,
+					new_upstream, BRANCH_TRACK_OVERRIDE,
+					quiet);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v6 2/5] branch: make create_branch() always create a branch
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
  2021-12-20 23:34           ` [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
@ 2021-12-20 23:34           ` Glen Choo
  2022-01-11  2:19             ` Jonathan Tan
  2021-12-20 23:34           ` [PATCH v6 3/5] branch: add a dry_run parameter to create_branch() Glen Choo
                             ` (5 subsequent siblings)
  7 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2021-12-20 23:34 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

create_branch() was formerly used to set tracking without creating a
branch. Since the previous commit replaces this use case with
dwim_and_setup_tracking(), we can simplify create_branch() so that it
always creates a branch.

Do this simplification, in particular:

* remove the special handling of BRANCH_TRACK_OVERRIDE because it is no
  longer used
* assert that clobber_head_ok can only be provided with force
* check that we're handling clobber_head_ok and force correctly by
  introducing tests for `git branch --force`

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c          | 55 +++++++++++++++++++++--------------------------
 branch.h          |  4 ++--
 t/t3200-branch.sh | 17 +++++++++++++++
 3 files changed, 44 insertions(+), 32 deletions(-)

diff --git a/branch.c b/branch.c
index e271a4e0a7..de680f311d 100644
--- a/branch.c
+++ b/branch.c
@@ -426,15 +426,17 @@ void create_branch(struct repository *r, const char *name,
 	char *real_ref;
 	struct strbuf ref = STRBUF_INIT;
 	int forcing = 0;
-	int dont_change_ref = 0;
-
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
+	struct ref_transaction *transaction;
+	struct strbuf err = STRBUF_INIT;
+	char *msg;
+
+	if (clobber_head_ok && !force)
+		BUG("'clobber_head_ok' can only be used with 'force'");
+
+	if (clobber_head_ok ?
+			  validate_branchname(name, &ref) :
+			  validate_new_branchname(name, &ref, force)) {
+		forcing = 1;
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
@@ -442,27 +444,20 @@ void create_branch(struct repository *r, const char *name,
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
-	if (!dont_change_ref) {
-		struct ref_transaction *transaction;
-		struct strbuf err = STRBUF_INIT;
-		char *msg;
-
-		if (forcing)
-			msg = xstrfmt("branch: Reset to %s", start_name);
-		else
-			msg = xstrfmt("branch: Created from %s", start_name);
-
-		transaction = ref_transaction_begin(&err);
-		if (!transaction ||
-		    ref_transaction_update(transaction, ref.buf,
-					   &oid, forcing ? NULL : null_oid(),
-					   0, msg, &err) ||
-		    ref_transaction_commit(transaction, &err))
-			die("%s", err.buf);
-		ref_transaction_free(transaction);
-		strbuf_release(&err);
-		free(msg);
-	}
+	if (forcing)
+		msg = xstrfmt("branch: Reset to %s", start_name);
+	else
+		msg = xstrfmt("branch: Created from %s", start_name);
+	transaction = ref_transaction_begin(&err);
+	if (!transaction ||
+		ref_transaction_update(transaction, ref.buf,
+					&oid, forcing ? NULL : null_oid(),
+					0, msg, &err) ||
+		ref_transaction_commit(transaction, &err))
+		die("%s", err.buf);
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	free(msg);
 
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
diff --git a/branch.h b/branch.h
index ab2315c611..cf3a4d3ff3 100644
--- a/branch.h
+++ b/branch.h
@@ -52,8 +52,8 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *
  *   - force enables overwriting an existing (non-head) branch
  *
- *   - clobber_head_ok allows the currently checked out (hence existing)
- *     branch to be overwritten; without 'force', it has no effect.
+ *   - clobber_head_ok, when enabled with 'force', allows the currently
+ *     checked out (head) branch to be overwritten
  *
  *   - reflog creates a reflog for the branch
  *
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 09ab132377..71a72efcb2 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
 	git branch abc && test_path_is_file .git/refs/heads/abc
 '
 
+test_expect_success 'git branch abc should fail when abc exists' '
+	test_must_fail git branch abc
+'
+
+test_expect_success 'git branch --force abc should fail when abc is checked out' '
+	test_when_finished git switch main &&
+	git switch abc &&
+	test_must_fail git branch --force abc HEAD~1
+'
+
+test_expect_success 'git branch --force abc should succeed when abc exists' '
+	git rev-parse HEAD~1 >expect &&
+	git branch --force abc HEAD~1 &&
+	git rev-parse abc >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v6 3/5] branch: add a dry_run parameter to create_branch()
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
  2021-12-20 23:34           ` [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
  2021-12-20 23:34           ` [PATCH v6 2/5] branch: make create_branch() always create a branch Glen Choo
@ 2021-12-20 23:34           ` Glen Choo
  2021-12-20 23:34           ` [PATCH v6 4/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
                             ` (4 subsequent siblings)
  7 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-20 23:34 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a dry_run parameter to create_branch() such that dry_run = 1 will
validate a new branch without trying to create it. This will be used in
`git branch --recurse-submodules` to ensure that the new branch can be
created in all submodules.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c           | 5 ++++-
 branch.h           | 9 +++++----
 builtin/branch.c   | 2 +-
 builtin/checkout.c | 3 ++-
 4 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/branch.c b/branch.c
index de680f311d..55c7ba4a25 100644
--- a/branch.c
+++ b/branch.c
@@ -420,7 +420,7 @@ static void dwim_branch_start(struct repository *r, const char *start_name,
 
 void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track)
+		   int reflog, int quiet, enum branch_track track, int dry_run)
 {
 	struct object_id oid;
 	char *real_ref;
@@ -440,6 +440,8 @@ void create_branch(struct repository *r, const char *name,
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
+	if (dry_run)
+		goto cleanup;
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -462,6 +464,7 @@ void create_branch(struct repository *r, const char *name,
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
 
+cleanup:
 	strbuf_release(&ref);
 	free(real_ref);
 }
diff --git a/branch.h b/branch.h
index cf3a4d3ff3..8009266343 100644
--- a/branch.h
+++ b/branch.h
@@ -62,11 +62,12 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *   - track causes the new branch to be configured to merge the remote branch
  *     that start_name is a tracking branch for (if any).
  *
+ *   - dry_run causes the branch to be validated but not created.
+ *
  */
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track);
+void create_branch(struct repository *r, const char *name,
+		   const char *start_name, int force, int clobber_head_ok,
+		   int reflog, int quiet, enum branch_track track, int dry_run);
 
 /*
  * Check if 'name' can be a valid name for a branch; die otherwise.
diff --git a/builtin/branch.c b/builtin/branch.c
index 16a7e80df5..9b300cf42f 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -859,7 +859,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 
 		create_branch(the_repository,
 			      argv[0], (argc == 2) ? argv[1] : head,
-			      force, 0, reflog, quiet, track);
+			      force, 0, reflog, quiet, track, 0);
 
 	} else
 		usage_with_options(builtin_branch_usage, options);
diff --git a/builtin/checkout.c b/builtin/checkout.c
index bbbfabb871..629a288dd8 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -904,7 +904,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				      opts->new_branch_force ? 1 : 0,
 				      opts->new_branch_log,
 				      opts->quiet,
-				      opts->track);
+				      opts->track,
+				      0);
 		free(new_branch_info->name);
 		free(new_branch_info->refname);
 		new_branch_info->name = xstrdup(opts->new_branch);
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v6 4/5] builtin/branch: clean up action-picking logic in cmd_branch()
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
                             ` (2 preceding siblings ...)
  2021-12-20 23:34           ` [PATCH v6 3/5] branch: add a dry_run parameter to create_branch() Glen Choo
@ 2021-12-20 23:34           ` Glen Choo
  2021-12-20 23:34           ` [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
                             ` (3 subsequent siblings)
  7 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-20 23:34 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a variable to cmd_branch() that will tell us whether or not
cmd_branch() will default to creating a branch (instead of performing
another action). Besides making the function more explicit, this allows
us to validate options that can only be used when creating a branch.
Such an option does not exist yet, but one will be introduced in a
subsequent commit.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 builtin/branch.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 9b300cf42f..3b010c1d2c 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -616,14 +616,15 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, list = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	const char *new_upstream = NULL;
+	int noncreate_actions = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, icase = 0, force = 0;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting;
 	struct string_list sorting_options = STRING_LIST_INIT_DUP;
 	struct ref_format format = REF_FORMAT_INIT;
@@ -708,8 +709,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
+			    !!show_current + !!list + !!edit_description +
+			    !!unset_upstream;
+	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
 	if (filter.abbrev == -1)
@@ -849,7 +852,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
 		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>?"));
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
                             ` (3 preceding siblings ...)
  2021-12-20 23:34           ` [PATCH v6 4/5] builtin/branch: clean up action-picking logic in cmd_branch() Glen Choo
@ 2021-12-20 23:34           ` Glen Choo
  2021-12-26  4:09             ` Junio C Hamano
  2022-01-11  3:28             ` Jonathan Tan
  2021-12-20 23:36           ` [PATCH v6 0/5] implement branch --recurse-submodules Glen Choo
                             ` (2 subsequent siblings)
  7 siblings, 2 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-20 23:34 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

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 semantics e.g. `git
checkout` does not recursively checkout the expected branches created by
`git branch` yet. To ensure that the correct set of semantics is used,
this commit introduces a new configuration value,
`submodule.propagateBranches`, which enables submodule branching when
true (defaults to false).

This commit includes changes that allow Git to work with submodules
that are in trees (and not just the index):

* 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).

Signed-off-by: Glen Choo <chooglen@google.com>
---
 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  24 ++-
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 130 +++++++++++++
 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        | 291 +++++++++++++++++++++++++++++
 14 files changed, 659 insertions(+), 14 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

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..0aa5946364 100644
--- a/Documentation/config/submodule.txt
+++ b/Documentation/config/submodule.txt
@@ -59,11 +59,8 @@ 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`.
-	Defaults to false.
+	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
@@ -71,6 +68,23 @@ submodule.recurse::
 	`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.
 
 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 2d52ae396b..435dc2b20e 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 55c7ba4a25..6d0d9a8e1b 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;
@@ -478,6 +480,134 @@ 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() (and its variants) do
+ * 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);
+	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 8009266343..63448e4278 100644
--- a/branch.h
+++ b/branch.h
@@ -69,6 +69,28 @@ void create_branch(struct repository *r, const char *name,
 		   const char *start_name, int force, int clobber_head_ok,
 		   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 3b010c1d2c..c06de58873 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] = {
@@ -99,6 +102,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;
@@ -673,6 +686,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(),
 	};
@@ -715,6 +729,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;
@@ -853,6 +878,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>?"));
@@ -860,10 +888,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 9b25a508e6..d7e50af72f 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)
@@ -2984,6 +2985,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;
@@ -3390,6 +3427,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 c689070524..5ace18a7d9 100644
--- a/submodule.c
+++ b/submodule.c
@@ -267,7 +267,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;
@@ -275,7 +277,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)
@@ -317,6 +319,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..a2dfb5ad7f
--- /dev/null
+++ b/t/t3207-branch-submodule.sh
@@ -0,0 +1,291 @@
+#!/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
+
+test_expect_success 'setup superproject and submodule' '
+	git init super &&
+	test_commit 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 "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
+	git -C sub-upstream commit -m "add submodule" &&
+	# Regular submodule
+	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
+	# Submodule in a subdirectory
+	git -C super submodule add "$TRASH_DIRECTORY/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
+'
+
+CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
+
+cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
+	#!/bin/sh
+
+	super_dir="$1"
+	shift
+	(
+		cd "$super_dir" &&
+		git checkout main &&
+		for branch_name in "$@"; do
+			git branch -D "$branch_name"
+			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
+		done
+	)
+EOF
+chmod +x "$CLEANUP_SCRIPT_PATH"
+
+cleanup_branches() {
+	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
+} >/dev/null 2>/dev/null
+
+# Test the argument parsing
+test_expect_success '--recurse-submodules should create branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should respect submodule.recurse when creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -c submodule.recurse=true branch branch-a &&
+		git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+test_expect_success 'should ignore submodule.recurse when not creating branches' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git branch --recurse-submodules branch-a &&
+		git -c submodule.recurse=true branch -D branch-a &&
+		test_must_fail git rev-parse --abbrev-ref branch-a &&
+		git -C sub rev-parse --abbrev-ref branch-a
+	)
+'
+
+# Test branch creation behavior
+test_expect_success 'should create branches based off commit id in superproject' '
+	test_when_finished "cleanup_branches super branch-a branch-b" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 &&
+
+		cat >expected <<-EOF &&
+		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
+		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
+		EOF
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branches if branch exists and --force is given' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		cd super &&
+		git -C sub rev-parse HEAD >expected &&
+		test_commit -C sub baz &&
+		git -C sub branch branch-a HEAD~1 &&
+		git branch --recurse-submodules --force branch-a &&
+		git rev-parse branch-a &&
+		# assert that sub:branch-a was moved
+		git -C sub rev-parse branch-a >actual &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
+	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
+	(
+		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 rev-parse branch-c &&
+		git -C sub rev-parse branch-c &&
+		git -C second/sub rev-parse branch-c &&
+		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 set up tracking of local branches with track=always' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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 "cleanup_branches super branch-a" &&
+	(
+		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)" = ""
+	)
+'
+
+test_expect_success 'should not create branches in inactive submodules' '
+	test_when_finished "cleanup_branches super branch-a" &&
+	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 'setup remote-tracking tests' '
+	(
+		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
+'
+
+test_expect_success 'should not create branch when submodule is not in .git/modules' '
+	# The cleanup needs to delete sub2 separately because main does not have sub2
+	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
+		git -C super-clone/sub2/sub-sub branch -D branch-b && \
+		cleanup_branches super-clone branch-a branch-b" &&
+	(
+		cd super-clone &&
+		# This should succeed because super-clone has sub.
+		git branch --recurse-submodules branch-a origin/branch-a &&
+		# This should fail because super-clone does not have sub2.
+		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
+		cat >expected <<-EOF &&
+		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
+		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
+		EOF
+		test_cmp expected 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 "cleanup_branches super-clone branch-a" &&
+	(
+		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 "cleanup_branches super-clone branch-a && \
+		git -C super-clone remote rename ex-origin origin" &&
+	(
+		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
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 0/5] implement branch --recurse-submodules
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
                             ` (4 preceding siblings ...)
  2021-12-20 23:34           ` [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-20 23:36           ` Glen Choo
  2021-12-21  1:07           ` Junio C Hamano
  2022-01-24 20:44           ` [PATCH v7 0/6] " Glen Choo
  7 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-20 23:36 UTC (permalink / raw)
  To: git
  Cc: Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Glen Choo <chooglen@google.com> writes:

> Submodule branching RFC:
> https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/
>
> Original Submodule UX RFC/Discussion:
> https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/
>
> Contributor Summit submodules Notes:
> https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/
>
> Submodule UX overhaul updates:
> https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update
>
> This series implements branch --recurse-submodules as laid out in the
> Submodule branching RFC (linked above). If there are concerns about the
> UX/behavior, I would appreciate feedback on the RFC thread as well :)
>
> v6 fixes v5's bad rebase; it was based off a merge commit in 'seen'
> instead of js/branch-track-inherit. Since v5 is mostly a no-op, I will
> also include a range-diff against v4, which is the last version that
> anyone except Junio would care about.

As mentioned, here is the range-diff against v4

Range-diff against v4:
1:  751e8ae566 < -:  ---------- branch: accept multiple upstream branches for tracking
2:  5d1ebe1495 < -:  ---------- branch: add flags and config to inherit tracking
3:  0080a1fb35 < -:  ---------- config: require lowercase for branch.autosetupmerge
4:  dfdbbaaca5 ! 1:  29669c57b4 branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
    @@ Commit message
     
         This refactor is motivated by a desire to add a "dry_run" parameter to
         create_branch() that will validate whether or not a branch can be
    -    created without actually creating it - this behavior be used in a
    +    created without actually creating it - this behavior will be used in a
         subsequent commit that adds `git branch --recurse-submodules topic`.
     
         Adding "dry_run" is not obvious because create_branch() is also used to
    @@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
      
      	if (tracking.matches > 1)
     @@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
    - 			      tracking.srcs) < 0)
    + 				tracking.remote, tracking.srcs) < 0)
      		exit(-1);
      
     +cleanup:
5:  e22a177cb7 ! 2:  ac2532a953 branch: make create_branch() always create a branch
    @@ Commit message
     
         create_branch() was formerly used to set tracking without creating a
         branch. Since the previous commit replaces this use case with
    -    setup_tracking(), we can simplify create_branch() so that it always
    -    creates a branch.
    +    dwim_and_setup_tracking(), we can simplify create_branch() so that it
    +    always creates a branch.
     
         Do this simplification, in particular:
     
6:  8a895aa401 ! 3:  a0ed3fa438 branch: add a dry_run parameter to create_branch()
    @@ builtin/checkout.c: static void update_refs_for_switch(const struct checkout_opt
     -				      opts->track);
     +				      opts->track,
     +				      0);
    - 		new_branch_info->name = opts->new_branch;
    - 		setup_branch_path(new_branch_info);
    - 	}
    + 		free(new_branch_info->name);
    + 		free(new_branch_info->refname);
    + 		new_branch_info->name = xstrdup(opts->new_branch);
7:  971c53ec85 = 4:  ebded31c96 builtin/branch: clean up action-picking logic in cmd_branch()
8:  cd88f3ad92 = 5:  0a7ec6ee75 branch: add --recurse-submodules option for branch creation

> This version is based off Josh's js/branch-track-inherit v7. Because that
> is not yet in 'seen', I applied those patches onto abe6bb3905 (The first
> batch to start the current cycle, 2021-11-29). The base commit can be
> found at https://github.com/chooglen/git/tree/js/branch-track-inherit-v7.
> I really hope I got right this time, but if not, hopefully the paper
> trail is obvious.
>
> Future work:
> * `git branch -d --recurse-submodules` so that users can clean up
>   extraneous branches.
> * `git [checkout | switch] --recurse-submodules` +
>   submodule.propagateBranches so that users can actually checkout the
>   branches.
> * After [1], it seems clear that --recurse-submodules parsing could
>   really benefit from some standardization. It's not obvious which
>   RECURSE_SUBMODULE_* enums are applicable to which commands, and there
>   is no way to distinguish between an explicit --recurse-submodules from
>   argv vs submodule.recurse from the config.
>
>   I chose not to use them in this series because their usage is already
>   inconsistent (grep.c doesn't use them either), and it would be _more_
>   confusing to use the enum (handling RECURSE_SUBMODULES_DEFAULT = 1 is
>   trickier than boolean 0 and 1).
>
>   At this point, I think it would be too noisy to introduce the enum,
>   but this would be a nice cleanup to do later.
> * As documented in branch.c, we create branches using a child process
>   only because install_branch_config() does not support submodules.
>   It should be possible to remove the child process once we make the
>   appropriate changes to config.c. I attempted this in [2] but chose to
>   punt it because it was too time-consuming at the time.
>
> Changes since v5:
> * Rebase onto v7 of js/branch-track-inherit
>   (https://lore.kernel.org/git/cover.1639717481.git.steadmon@google.com)
>
> Changes since v4:
> * Rebase correctly onto 'gitster/seen^{/^Merge branch .js/branch-track-inherit.}'
>   (see base-commit) as suggested in [3] (thanks Junio!)
> * These patches were also verified on top of 'next'.
>
> Changes since v3:
> * Split up the old patch 1. Patch 1 had a big diff because it used to
>   move lines, remove dead code and introduce repo_* functions (thanks
>   Jonathan!)
> ** repo_* functions have been dropped; they added noise and are not
>    necessary for correctness.
> * Use a new, harder-to-misuse function in --set-upstream-to,
>   dwim_and_setup_tracking(). Now, setup_tracking() never does DWIM and
>   dwim_and_setup_tracking() always does DWIM.
> * Move create_branch() dry_run to its own patch.
> * Fix an oversight where submodules in subtrees were ignored. This was
>   because submodules_of_tree() and tree_entry() didn't recurse into
>   subtrees. Test this accordingly (thanks Jonathan!).
> * cmd_branch() possible actions are more consistently ordered.
> * Documentation fixes (thanks Philippe!).
> * Additional comments and explanation.
> * Drop patch 5 (optional cleanup).
> * Rebase onto js/branch-track-inherit v6.
>
> Changes since v2:
> * Rebase onto js/branch-track-inherit. This series should continue to be
>   the case going forward.
> * Patch 1 has a smaller diff because the introduction of
>   validate_branch_start() no longer changes the function order thanks to a
>   forward declaration. This artificial forward declaration is removed in a
>   patch 2 (which can just be squashed into patch 1).
> * Optional cleanup: fix questionable exit codes in patch 5.
>
> Changes since v1:
> * Move the functionality of "git branch --dry-run" into "git submodule-helper create-branch --dry-run"
> * Add more fields to the submodules_of_tree() struct to reduce the
>   number of allocations made by the caller. Move this functionality
>   to patch 3 (formerly patch 4) and drop patch 1.
> * Make submodules_of_tree() ignore inactive submodules
> * Structure the output of the submodules a bit better by adding prefixes
>   to the child process' output (instead of inconsistently indenting the
>   output).
> ** I wasn't able to find a good way to interleave stdout/stderr
>    correctly, so a less-than-desirable workaround was to route the child
>    process output to stdout/stderr depending on the exit code.
> ** Eventually, I would like to structure the output of submodules in a
>    report, as Ævar suggested. But at this stage, I think that it's
>    better to spend time getting user feedback on the submodules
>    branching UX and it'll be easier to standardize the output when we've
>    implemented more of the UX :)
>
> [1] https://lore.kernel.org/git/kl6lbl1p9zjf.fsf@chooglen-macbookpro.roam.corp.google.com/
> [2] https://lore.kernel.org/git/kl6lv90ytd4v.fsf@chooglen-macbookpro.roam.corp.google.com/
> [3] https://lore.kernel.org/git/xmqqlf0lz6os.fsf@gitster.g 
>
> Glen Choo (5):
>   branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
>   branch: make create_branch() always create a branch
>   branch: add a dry_run parameter to create_branch()
>   builtin/branch: clean up action-picking logic in cmd_branch()
>   branch: add --recurse-submodules option for branch creation
>
>  Documentation/config/advice.txt    |   3 +
>  Documentation/config/submodule.txt |  24 ++-
>  Documentation/git-branch.txt       |  11 +-
>  advice.c                           |   1 +
>  advice.h                           |   1 +
>  branch.c                           | 257 ++++++++++++++++++++-----
>  branch.h                           |  57 +++++-
>  builtin/branch.c                   |  70 +++++--
>  builtin/checkout.c                 |   3 +-
>  builtin/submodule--helper.c        |  38 ++++
>  submodule-config.c                 |  60 ++++++
>  submodule-config.h                 |  34 ++++
>  submodule.c                        |  11 +-
>  submodule.h                        |   3 +
>  t/t3200-branch.sh                  |  17 ++
>  t/t3207-branch-submodule.sh        | 291 +++++++++++++++++++++++++++++
>  16 files changed, 805 insertions(+), 76 deletions(-)
>  create mode 100755 t/t3207-branch-submodule.sh
>
> Range-diff against v5:
> 1:  a9d1108b3e ! 1:  29669c57b4 branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
>     @@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
>       
>       	if (tracking.matches > 1)
>      @@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
>     - 			      tracking.srcs) < 0)
>     + 				tracking.remote, tracking.srcs) < 0)
>       		exit(-1);
>       
>      +cleanup:
>     @@ branch.c: N_("\n"
>      @@ branch.c: void create_branch(struct repository *r,
>       
>       	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
>     - 		die(_("not a valid branch point: '%s'"), start_name);
>     + 		die(_("Not a valid branch point: '%s'."), start_name);
>      -	oidcpy(&oid, &commit->object.oid);
>      +	if (out_real_ref)
>      +		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
> 2:  c543c1412a = 2:  ac2532a953 branch: make create_branch() always create a branch
> 3:  dddd434d7a = 3:  a0ed3fa438 branch: add a dry_run parameter to create_branch()
> 4:  41cca3bd52 = 4:  ebded31c96 builtin/branch: clean up action-picking logic in cmd_branch()
> 5:  540eeab183 ! 5:  0a7ec6ee75 branch: add --recurse-submodules option for branch creation
>     @@ t/t3207-branch-submodule.sh (new)
>      +		test_must_fail git rev-parse branch-a &&
>      +
>      +		cat >expected <<-EOF &&
>     -+		submodule ${SQ}sub${SQ}: fatal: a branch named ${SQ}branch-a${SQ} already exists
>     ++		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
>      +		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
>      +		EOF
>      +		test_cmp expected actual
>
> base-commit: e1c6879cf24f90693ca108542ec1db970764e1dc
> -- 
> 2.33.GIT

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 0/5] implement branch --recurse-submodules
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
                             ` (5 preceding siblings ...)
  2021-12-20 23:36           ` [PATCH v6 0/5] implement branch --recurse-submodules Glen Choo
@ 2021-12-21  1:07           ` Junio C Hamano
  2021-12-21 17:51             ` Glen Choo
  2022-01-24 20:44           ` [PATCH v7 0/6] " Glen Choo
  7 siblings, 1 reply; 110+ messages in thread
From: Junio C Hamano @ 2021-12-21  1:07 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Glen Choo <chooglen@google.com> writes:

> v6 fixes v5's bad rebase; it was based off a merge commit in 'seen'
> instead of js/branch-track-inherit. Since v5 is mostly a no-op, I will
> also include a range-diff against v4, which is the last version that
> anyone except Junio would care about.
>
> This version is based off Josh's js/branch-track-inherit v7. Because that
> is not yet in 'seen',...

Thanks.  js/branch/track-inherit has been using v7 since Fri Dec 17
15:47:47 2021 -0800, but unfortunately that was after the week's
integration work and the 'seen' shown to the public probably did not
have it.

FYI at least since [*1*] was used to create 751363af (branch: add
flags and config to inherit tracking, 2021-10-16), the topic
js/branch-track-inherit has always been queued on top of f443b226
(Thirteenth batch, 2021-10-14).  I try to keep the same base to keep
things stable, unless there is a strong enough reason why we should
depend on newer base commit.

I've wiggled the patches to queue your v6 but haven't pushed the
result out yet.

Thanks.


[Reference]

*1* <b9356d9837479914bcf9a265f52afe170be7e2e2.1634445482.git.steadmon@google.com>

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 0/5] implement branch --recurse-submodules
  2021-12-21  1:07           ` Junio C Hamano
@ 2021-12-21 17:51             ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2021-12-21 17:51 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

Junio C Hamano <gitster@pobox.com> writes:

> Glen Choo <chooglen@google.com> writes:
>
>> v6 fixes v5's bad rebase; it was based off a merge commit in 'seen'
>> instead of js/branch-track-inherit. Since v5 is mostly a no-op, I will
>> also include a range-diff against v4, which is the last version that
>> anyone except Junio would care about.
>>
>> This version is based off Josh's js/branch-track-inherit v7. Because that
>> is not yet in 'seen',...
>
> Thanks.  js/branch/track-inherit has been using v7 since Fri Dec 17
> 15:47:47 2021 -0800, but unfortunately that was after the week's
> integration work and the 'seen' shown to the public probably did not
> have it.
>
> FYI at least since [*1*] was used to create 751363af (branch: add
> flags and config to inherit tracking, 2021-10-16), the topic
> js/branch-track-inherit has always been queued on top of f443b226
> (Thirteenth batch, 2021-10-14).  I try to keep the same base to keep
> things stable, unless there is a strong enough reason why we should
> depend on newer base commit.

Ah, I see, so it would make the most sense for me to apply the patches
on the the same base as you. I'll keep that in mind in the future.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation
  2021-12-20 23:34           ` [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
@ 2021-12-26  4:09             ` Junio C Hamano
  2022-01-11  3:28             ` Jonathan Tan
  1 sibling, 0 replies; 110+ messages in thread
From: Junio C Hamano @ 2021-12-26  4:09 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain

To be squashed if we were to take the series queued on 'seen'
otherwise as-is (or if the series will be rerolled, please squash it
into the new round on your end).

----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----
Subject: [PATCH] fixup! branch: add --recurse-submodules option for branch
 creation

Style fix to have SP on both sides of () in

    shell_function () {

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 t/t3207-branch-submodule.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
index a2dfb5ad7f..4256a95f4e 100755
--- a/t/t3207-branch-submodule.sh
+++ b/t/t3207-branch-submodule.sh
@@ -44,7 +44,7 @@ cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
 EOF
 chmod +x "$CLEANUP_SCRIPT_PATH"
 
-cleanup_branches() {
+cleanup_branches () {
 	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
 } >/dev/null 2>/dev/null
 
-- 
2.34.1-568-g69e9fd72b5


^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  2021-12-20 23:34           ` [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
@ 2022-01-11  2:09             ` Jonathan Tan
  2022-01-11 17:29               ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2022-01-11  2:09 UTC (permalink / raw)
  To: chooglen
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> This refactor is motivated by a desire to add a "dry_run" parameter to
> create_branch() that will validate whether or not a branch can be
> created without actually creating it - this behavior will be used in a
> subsequent commit that adds `git branch --recurse-submodules topic`.

Makes sense.

> Adding "dry_run" is not obvious because create_branch() is also used to
> set tracking information without creating a branch, i.e. when using
> --set-upstream-to. 

create_branch() is complicated, OK.

> This appears to be a leftover from 4fc5006676 (Add
> branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
> create a branch and sometimes update tracking information without
> creating a branch. However, we no longer support --set-upstream, so it
> makes more sense to set tracking information with another function and
> use create_branch() only to create branches. In a later commit, we will
> remove the now-unnecessary logic from create_branch() so that "dry_run"
> becomes trivial to implement.

What do you mean by "leftover"?

Aside from that, the pertinent information is that the mentioned commit
changed create_branch() to no longer always create a branch, instead
sometimes creating a branch and sometimes updating tracking information
(and sometimes both). I don't think whether we support "--set-upstream"
is material here.

Also, what is the now-unnecessary logic to be removed in a later commit?

> Introduce dwim_and_setup_tracking(), which replaces create_branch()
> in `git branch --set-upstream-to`. Ensure correctness by moving the DWIM
> and branch validation logic from create_branch() into a helper function,
> dwim_branch_start(), so that the logic is shared by both functions.

I think it's clearer to just say what we're refactoring instead of
saying that we're introducing a function and making sure that it is
correct, not by testing (as one would expect), but by moving logic.

I would write the commit message like this:

  This commit is in preparation for a future commit that adds a dry_run
  parameter to create_branch() (that is needed for supporting "git
  branch --recurse-submodules", to be introduced in another future
  commit).

  create_branch() used to always create a branch, but this was changed
  in 4fc5006676 (Add branch --set-upstream, 2010-01-18), when it was
  changed to be also able to set tracking information.

  Refactor the code that sets tracking information into its own
  functions dwim_branch_start() and dwim_and_setup_tracking(). Also
  change an invocation of create_branch() in cmd_branch() in
  builtin/branch.c to use dwim_and_setup_tracking(), since that
  invocation is only for setting tracking information.

And if this is true:

  As of this commit, create_branch() still sometimes does not create
  branches, but this will be fixed in a subsequent commit.

> @@ -217,9 +217,11 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
>  }
>  
>  /*
> - * 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.
> + * Used internally to set the branch.<new_ref>.{remote,merge} config
> + * settings so that branch 'new_ref' tracks 'orig_ref'. Unlike
> + * dwim_and_setup_tracking(), this does not do DWIM, i.e. "origin/main"
> + * will not be expanded to "refs/remotes/origin/main", so it is not safe
> + * for 'orig_ref' to be raw user input.
>   */
>  static void setup_tracking(const char *new_ref, const char *orig_ref,
>  			   enum branch_track track, int quiet)

The comment makes sense.

> @@ -244,7 +246,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
>  		case BRANCH_TRACK_INHERIT:
>  			break;
>  		default:
> -			return;
> +			goto cleanup;
>  		}
>  
>  	if (tracking.matches > 1)
> @@ -257,6 +259,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
>  				tracking.remote, tracking.srcs) < 0)
>  		exit(-1);
>  
> +cleanup:
>  	string_list_clear(tracking.srcs, 0);
>  }
>  

This seems like it's just for avoiding a memory leak, and is unrelated
to this commit, so it should go into its own commit.

> @@ -340,31 +343,37 @@ N_("\n"
>  "will track its remote counterpart, you may want to use\n"
>  "\"git push -u\" to set the upstream config as you push.");
>  
> -void create_branch(struct repository *r,
> -		   const char *name, const char *start_name,
> -		   int force, int clobber_head_ok, int reflog,
> -		   int quiet, enum branch_track track)

This seems to have the same parameters as the "+" version, but wrapped
differently - don't rewrap unless you're also changing it.

> +/**
> + * DWIMs a user-provided ref to determine the starting point for a
> + * branch and validates it, where:
> + *
> + *   - r is the repository to validate the branch for
> + *
> + *   - start_name is the ref that we would like to test. This is
> + *     expanded with DWIM and assigned to out_real_ref.
> + *
> + *   - track is the tracking mode of the new branch. If tracking is
> + *     explicitly requested, start_name must be a branch (because
> + *     otherwise start_name cannot be tracked)
> + *
> + *   - out_oid is an out parameter containing the object_id of start_name
> + *
> + *   - out_real_ref is an out parameter containing the full, 'real' form
> + *     of start_name e.g. refs/heads/main instead of main
> + *
> + */
> +static void dwim_branch_start(struct repository *r, const char *start_name,
> +			   enum branch_track track, char **out_real_ref,
> +			   struct object_id *out_oid)

[snip]

> @@ -401,7 +410,34 @@ void create_branch(struct repository *r,
>  
>  	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
>  		die(_("Not a valid branch point: '%s'."), start_name);
> -	oidcpy(&oid, &commit->object.oid);
> +	if (out_real_ref)
> +		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;

I think you can just write "*out_real_ref = real_ref; real_ref = NULL;"
here, and then not need to xstrdup.

> +	if (out_oid)
> +		oidcpy(out_oid, &commit->object.oid);
> +
> +	FREE_AND_NULL(real_ref);
> +}

Comparing dwim_branch_start()...

> +void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
> +			     const char *orig_ref, enum branch_track track,
> +			     int quiet)
> +{
> +	char *real_orig_ref;
> +	dwim_branch_start(r, orig_ref, track, &real_orig_ref, NULL);
> +	setup_tracking(new_ref, real_orig_ref, track, quiet);
> +}

...and this...

> @@ -823,12 +823,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
>  		if (!ref_exists(branch->refname))
>  			die(_("branch '%s' does not exist"), branch->name);
>  
> -		/*
> -		 * create_branch takes care of setting up the tracking
> -		 * info and making sure new_upstream is correct
> -		 */
> -		create_branch(the_repository, branch->name, new_upstream,
> -			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
> +		dwim_and_setup_tracking(the_repository, branch->name,
> +					new_upstream, BRANCH_TRACK_OVERRIDE,
> +					quiet);
>  	} else if (unset_upstream) {
>  		struct branch *branch = branch_get(argv[0]);
>  		struct strbuf buf = STRBUF_INIT;

...looking at this, I can see that dwim_and_setup_tracking() indeed does
everything that this create_branch() invocation would do, so overall the
commit makes sense.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 2/5] branch: make create_branch() always create a branch
  2021-12-20 23:34           ` [PATCH v6 2/5] branch: make create_branch() always create a branch Glen Choo
@ 2022-01-11  2:19             ` Jonathan Tan
  2022-01-11 17:51               ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2022-01-11  2:19 UTC (permalink / raw)
  To: chooglen
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> create_branch() was formerly used to set tracking without creating a
> branch. Since the previous commit replaces this use case with
> dwim_and_setup_tracking(), we can simplify create_branch() so that it
> always creates a branch.
> 
> Do this simplification, in particular:
> 
> * remove the special handling of BRANCH_TRACK_OVERRIDE because it is no
>   longer used
> * assert that clobber_head_ok can only be provided with force
> * check that we're handling clobber_head_ok and force correctly by
>   introducing tests for `git branch --force`

This might have been more simply explained as:

  With the previous commit, these are true of create_branch():
   * BRANCH_TRACK_OVERRIDE is no longer passed
   * if clobber_head_ok is true, force is also true

  Assert these situations, delete dead code, and ensure that we're
  handling clobber_head_ok and force correctly by introducing tests for
  `git branch --force`. This also means that create_branch() now always
  creates a branch.

> @@ -426,15 +426,17 @@ void create_branch(struct repository *r, const char *name,
>  	char *real_ref;
>  	struct strbuf ref = STRBUF_INIT;
>  	int forcing = 0;
> -	int dont_change_ref = 0;
> -
> -	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
> -	    ? validate_branchname(name, &ref)
> -	    : validate_new_branchname(name, &ref, force)) {
> -		if (!force)
> -			dont_change_ref = 1;
> -		else
> -			forcing = 1;
> +	struct ref_transaction *transaction;
> +	struct strbuf err = STRBUF_INIT;
> +	char *msg;
> +
> +	if (clobber_head_ok && !force)
> +		BUG("'clobber_head_ok' can only be used with 'force'");
> +
> +	if (clobber_head_ok ?
> +			  validate_branchname(name, &ref) :
> +			  validate_new_branchname(name, &ref, force)) {
> +		forcing = 1;
>  	}

Also assert that track is not BRANCH_TRACK_OVERRIDE.

> @@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
>  	git branch abc && test_path_is_file .git/refs/heads/abc
>  '
>  
> +test_expect_success 'git branch abc should fail when abc exists' '
> +	test_must_fail git branch abc
> +'
> +
> +test_expect_success 'git branch --force abc should fail when abc is checked out' '
> +	test_when_finished git switch main &&
> +	git switch abc &&
> +	test_must_fail git branch --force abc HEAD~1
> +'
> +
> +test_expect_success 'git branch --force abc should succeed when abc exists' '
> +	git rev-parse HEAD~1 >expect &&
> +	git branch --force abc HEAD~1 &&
> +	git rev-parse abc >actual &&
> +	test_cmp expect actual
> +'

These tests make sense.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation
  2021-12-20 23:34           ` [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation Glen Choo
  2021-12-26  4:09             ` Junio C Hamano
@ 2022-01-11  3:28             ` Jonathan Tan
  2022-01-11 18:11               ` Glen Choo
  1 sibling, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2022-01-11  3:28 UTC (permalink / raw)
  To: chooglen
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

I am not very familiar with branch tracking, so I'll review everything
except that part.

Glen Choo <chooglen@google.com> writes:
> Although this commit does not introduce breaking changes, it is
> incompatible with existing --recurse-submodules semantics e.g. `git
> checkout` does not recursively checkout the expected branches created by
> `git branch` yet. 

Probably worth explaining that it will not recursively checkout the
expected branches *if* any of them are subsequently updated. Maybe say:

  If a user were to create branches in this way, create a commit on the
  branch in a submodule, and run "git checkout --recurse-submodules" in
  the superproject, the commits to be checked out (which are based on
  the gitlink in the superproject, not on the ref store of the
  submodule) probably wouldn't match the user's expectation.

> To ensure that the correct set of semantics is used,
> this commit introduces a new configuration value,
> `submodule.propagateBranches`, which enables submodule branching when
> true (defaults to false).

And then this could be reworded to:

  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 gitlink
  if this 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.

> @@ -71,6 +68,23 @@ submodule.recurse::
>  	`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

One oddity is that paragraphs after the "+" cannot be indented - see
other documentation files for examples.

> diff --git a/branch.c b/branch.c
> index 55c7ba4a25..6d0d9a8e1b 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;
> @@ -478,6 +480,134 @@ 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() (and its variants) do
> + * not support writing configs to submodules.
> + */

Makes sense that we need a child process, but could the child process be
"branch" instead of "submodule--helper"? If not, also mention why.

As for the reason, probably better to explicitly mention
install_branch_config_multiple_remotes() and say "(which is called by
setup_tracking())".

> diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
> new file mode 100755
> index 0000000000..a2dfb5ad7f
> --- /dev/null
> +++ b/t/t3207-branch-submodule.sh
> @@ -0,0 +1,291 @@
> +#!/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
> +
> +test_expect_success 'setup superproject and submodule' '
> +	git init super &&
> +	test_commit 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 "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
> +	git -C sub-upstream commit -m "add submodule" &&
> +	# Regular submodule
> +	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
> +	# Submodule in a subdirectory
> +	git -C super submodule add "$TRASH_DIRECTORY/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
> +'
> +
> +CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
> +
> +cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
> +	#!/bin/sh
> +
> +	super_dir="$1"
> +	shift
> +	(
> +		cd "$super_dir" &&
> +		git checkout main &&
> +		for branch_name in "$@"; do
> +			git branch -D "$branch_name"
> +			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
> +		done
> +	)
> +EOF
> +chmod +x "$CLEANUP_SCRIPT_PATH"
> +
> +cleanup_branches() {
> +	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
> +} >/dev/null 2>/dev/null

I don't think that the cleanup is saving much in process invocation
anymore - could we just delete the whole thing and start anew in each
test?

The rest of the tests are assuming that the cleanup works as expected -
I didn't take a close look.

Also, you should probably use "$(pwd)" instead of $TRASH_DIRECTORY.

> +test_expect_success '--recurse-submodules should fail when not creating branches' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		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 --abbrev-ref branch-a &&
> +		git -C sub rev-parse --abbrev-ref branch-a
> +	)
> +'

If we're just checking that the ref exists, no need for "--abbrev-ref".
Same comment throughout the file.

> +test_expect_success 'should not create any branches if branch is not valid for all repos' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		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 &&
> +
> +		cat >expected <<-EOF &&
> +		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
> +		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
> +		EOF
> +		test_cmp expected actual
> +	)
> +'

The error message seems too specific - probably enough to grep for the
information about the branch already existing.

> +test_expect_success 'should create branches if branch exists and --force is given' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	(
> +		cd super &&
> +		git -C sub rev-parse HEAD >expected &&
> +		test_commit -C sub baz &&
> +		git -C sub branch branch-a HEAD~1 &&
> +		git branch --recurse-submodules --force branch-a &&
> +		git rev-parse branch-a &&
> +		# assert that sub:branch-a was moved
> +		git -C sub rev-parse branch-a >actual &&
> +		test_cmp expected actual
> +	)
> +'

Should we create branch-a at HEAD instead of HEAD~1?

> +test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
> +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
> +	(
> +		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 rev-parse branch-c &&
> +		git -C sub rev-parse branch-c &&
> +		git -C second/sub rev-parse branch-c &&
> +		git checkout --recurse-submodules branch-c &&
> +		git -C sub2 rev-parse branch-c &&
> +		git -C sub2/sub-sub rev-parse branch-c
> +	)
> +'

No need to check so many repos, I think - just sub2 will do.

[skip tracking tests]

> +test_expect_success 'should not create branches in inactive submodules' '
> +	test_when_finished "cleanup_branches super branch-a" &&
> +	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
> +	)
> +'

Makes sense, but could all the tracking tests be together in the file?
Or is this order for a reason?

[skip tracking tests]

> +test_expect_success 'setup remote-tracking tests' '

This setup is not just for remote-tracking tests.

> +	(
> +		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
> +'
> +
> +test_expect_success 'should not create branch when submodule is not in .git/modules' '

I understand that no branch is created, but the title is ambiguous, to
me, whether it is a fatal error or not. Maybe the title should be "fatal
error upon branch creation when submodule is not in .git/modules".

> +	# The cleanup needs to delete sub2 separately because main does not have sub2
> +	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
> +		git -C super-clone/sub2/sub-sub branch -D branch-b && \
> +		cleanup_branches super-clone branch-a branch-b" &&
> +	(
> +		cd super-clone &&
> +		# This should succeed because super-clone has sub.

"has sub in .git/modules", I think.

> +		git branch --recurse-submodules branch-a origin/branch-a &&
> +		# This should fail because super-clone does not have sub2.

Likewise.

> +		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
> +		cat >expected <<-EOF &&
> +		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
> +		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
> +		EOF
> +		test_cmp expected 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
> +	)
> +'

Apart from the branch tracking part (which I didn't review), overall,
this patch set looks good.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  2022-01-11  2:09             ` Jonathan Tan
@ 2022-01-11 17:29               ` Glen Choo
  2022-01-11 20:03                 ` Jonathan Tan
  0 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2022-01-11 17:29 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Jonathan Tan <jonathantanmy@google.com> writes:

> Glen Choo <chooglen@google.com> writes:
>> This appears to be a leftover from 4fc5006676 (Add
>> branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
>> create a branch and sometimes update tracking information without
>> creating a branch. However, we no longer support --set-upstream, so it
>> makes more sense to set tracking information with another function and
>> use create_branch() only to create branches. In a later commit, we will
>> remove the now-unnecessary logic from create_branch() so that "dry_run"
>> becomes trivial to implement.
>
> What do you mean by "leftover"?
>
> Aside from that, the pertinent information is that the mentioned commit
> changed create_branch() to no longer always create a branch, instead
> sometimes creating a branch and sometimes updating tracking information
> (and sometimes both). I don't think whether we support "--set-upstream"
> is material here.

I was hoping to explain why we made the decision to reuse
create_branch() to set tracking information (it made sense with
--set-upstream) and why it now makes sense to use another function
(because we no longer have --set-upstream).

But maybe this justification is unnecessary, and the desire to add a
dry_run parameter is enough.

>
> Also, what is the now-unnecessary logic to be removed in a later commit?

This would be the logic that makes create_branch() not create branches,
which is addressed in your proposed commit message.

>> Introduce dwim_and_setup_tracking(), which replaces create_branch()
>> in `git branch --set-upstream-to`. Ensure correctness by moving the DWIM
>> and branch validation logic from create_branch() into a helper function,
>> dwim_branch_start(), so that the logic is shared by both functions.
>
> I think it's clearer to just say what we're refactoring instead of
> saying that we're introducing a function and making sure that it is
> correct, not by testing (as one would expect), but by moving logic.
>
> I would write the commit message like this:
>
>   This commit is in preparation for a future commit that adds a dry_run
>   parameter to create_branch() (that is needed for supporting "git
>   branch --recurse-submodules", to be introduced in another future
>   commit).
>
>   create_branch() used to always create a branch, but this was changed
>   in 4fc5006676 (Add branch --set-upstream, 2010-01-18), when it was
>   changed to be also able to set tracking information.
>
>   Refactor the code that sets tracking information into its own
>   functions dwim_branch_start() and dwim_and_setup_tracking(). Also
>   change an invocation of create_branch() in cmd_branch() in
>   builtin/branch.c to use dwim_and_setup_tracking(), since that
>   invocation is only for setting tracking information.
>
> And if this is true:
>
>   As of this commit, create_branch() still sometimes does not create
>   branches, but this will be fixed in a subsequent commit.

Hm, I see. This makes sense, I'll incorporate some of your suggestions :)

>> @@ -244,7 +246,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
>>  		case BRANCH_TRACK_INHERIT:
>>  			break;
>>  		default:
>> -			return;
>> +			goto cleanup;
>>  		}
>>  
>>  	if (tracking.matches > 1)
>> @@ -257,6 +259,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
>>  				tracking.remote, tracking.srcs) < 0)
>>  		exit(-1);
>>  
>> +cleanup:
>>  	string_list_clear(tracking.srcs, 0);
>>  }
>>  
>
> This seems like it's just for avoiding a memory leak, and is unrelated
> to this commit, so it should go into its own commit.

Thanks for the catch.

>
>> @@ -340,31 +343,37 @@ N_("\n"
>>  "will track its remote counterpart, you may want to use\n"
>>  "\"git push -u\" to set the upstream config as you push.");
>>  
>> -void create_branch(struct repository *r,
>> -		   const char *name, const char *start_name,
>> -		   int force, int clobber_head_ok, int reflog,
>> -		   int quiet, enum branch_track track)
>
> This seems to have the same parameters as the "+" version, but wrapped
> differently - don't rewrap unless you're also changing it.

Ah, I didn't realize it had rewrapped. Thanks for the catch.

>> +/**
>> + * DWIMs a user-provided ref to determine the starting point for a
>> + * branch and validates it, where:
>> + *
>> + *   - r is the repository to validate the branch for
>> + *
>> + *   - start_name is the ref that we would like to test. This is
>> + *     expanded with DWIM and assigned to out_real_ref.
>> + *
>> + *   - track is the tracking mode of the new branch. If tracking is
>> + *     explicitly requested, start_name must be a branch (because
>> + *     otherwise start_name cannot be tracked)
>> + *
>> + *   - out_oid is an out parameter containing the object_id of start_name
>> + *
>> + *   - out_real_ref is an out parameter containing the full, 'real' form
>> + *     of start_name e.g. refs/heads/main instead of main
>> + *
>> + */
>> +static void dwim_branch_start(struct repository *r, const char *start_name,
>> +			   enum branch_track track, char **out_real_ref,
>> +			   struct object_id *out_oid)
>
> [snip]
>
>> @@ -401,7 +410,34 @@ void create_branch(struct repository *r,
>>  
>>  	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
>>  		die(_("Not a valid branch point: '%s'."), start_name);
>> -	oidcpy(&oid, &commit->object.oid);
>> +	if (out_real_ref)
>> +		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
>
> I think you can just write "*out_real_ref = real_ref; real_ref = NULL;"
> here, and then not need to xstrdup.

Hm, you are right. The xstrdup was added because the original function
calls FREE_AND_NULL(real_ref) and then checks if real_ref is NULL. But
after the refactor, real_ref is not referenced after the
FREE_AND_NULL(real_ref), so that call can be removed.

I intend to remove the xstrdup, though it will introduce a bit of noise
because that block will no longer be moved wholesale.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 2/5] branch: make create_branch() always create a branch
  2022-01-11  2:19             ` Jonathan Tan
@ 2022-01-11 17:51               ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2022-01-11 17:51 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Jonathan Tan <jonathantanmy@google.com> writes:

> Glen Choo <chooglen@google.com> writes:
>> create_branch() was formerly used to set tracking without creating a
>> branch. Since the previous commit replaces this use case with
>> dwim_and_setup_tracking(), we can simplify create_branch() so that it
>> always creates a branch.
>> 
>> Do this simplification, in particular:
>> 
>> * remove the special handling of BRANCH_TRACK_OVERRIDE because it is no
>>   longer used
>> * assert that clobber_head_ok can only be provided with force
>> * check that we're handling clobber_head_ok and force correctly by
>>   introducing tests for `git branch --force`
>
> This might have been more simply explained as:
>
>   With the previous commit, these are true of create_branch():
>    * BRANCH_TRACK_OVERRIDE is no longer passed
>    * if clobber_head_ok is true, force is also true
>
>   Assert these situations, delete dead code, and ensure that we're
>   handling clobber_head_ok and force correctly by introducing tests for
>   `git branch --force`. This also means that create_branch() now always
>   creates a branch.

This is a great suggestion, I'll incorporate some of this.

As an aside, it has always been the case that clobber_head_ok is only true
when force is true, so it might be misleading to include it under the
umbrella of 'With the previous commit'. I'll move things around
accordingly.

>> @@ -426,15 +426,17 @@ void create_branch(struct repository *r, const char *name,
>>  	char *real_ref;
>>  	struct strbuf ref = STRBUF_INIT;
>>  	int forcing = 0;
>> -	int dont_change_ref = 0;
>> -
>> -	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
>> -	    ? validate_branchname(name, &ref)
>> -	    : validate_new_branchname(name, &ref, force)) {
>> -		if (!force)
>> -			dont_change_ref = 1;
>> -		else
>> -			forcing = 1;
>> +	struct ref_transaction *transaction;
>> +	struct strbuf err = STRBUF_INIT;
>> +	char *msg;
>> +
>> +	if (clobber_head_ok && !force)
>> +		BUG("'clobber_head_ok' can only be used with 'force'");
>> +
>> +	if (clobber_head_ok ?
>> +			  validate_branchname(name, &ref) :
>> +			  validate_new_branchname(name, &ref, force)) {
>> +		forcing = 1;
>>  	}
>
> Also assert that track is not BRANCH_TRACK_OVERRIDE.

Makes sense, I'll do that.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation
  2022-01-11  3:28             ` Jonathan Tan
@ 2022-01-11 18:11               ` Glen Choo
  2022-01-11 20:15                 ` Jonathan Tan
  0 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2022-01-11 18:11 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: git, jonathantanmy, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Jonathan Tan <jonathantanmy@google.com> writes:

> I am not very familiar with branch tracking, so I'll review everything
> except that part.
>
> Glen Choo <chooglen@google.com> writes:
>> Although this commit does not introduce breaking changes, it is
>> incompatible with existing --recurse-submodules semantics e.g. `git
>> checkout` does not recursively checkout the expected branches created by
>> `git branch` yet. 
>
> Probably worth explaining that it will not recursively checkout the
> expected branches *if* any of them are subsequently updated. Maybe say:
>
>   If a user were to create branches in this way, create a commit on the
>   branch in a submodule, and run "git checkout --recurse-submodules" in
>   the superproject, the commits to be checked out (which are based on
>   the gitlink in the superproject, not on the ref store of the
>   submodule) probably wouldn't match the user's expectation.
>
>> To ensure that the correct set of semantics is used,
>> this commit introduces a new configuration value,
>> `submodule.propagateBranches`, which enables submodule branching when
>> true (defaults to false).
>
> And then this could be reworded to:
>
>   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 gitlink
>   if this 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.

Thanks! I'll use this wording - otherwise it might not be clear to
readers what the difference in 'semantics' refers to.

>
>> @@ -71,6 +68,23 @@ submodule.recurse::
>>  	`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
>
> One oddity is that paragraphs after the "+" cannot be indented - see
> other documentation files for examples.

Ah, thanks for the catch. Interestingly, it still rendered as expected
when I ran `make` - perhaps a deviation from the asciidoc spec.

>> diff --git a/branch.c b/branch.c
>> index 55c7ba4a25..6d0d9a8e1b 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;
>> @@ -478,6 +480,134 @@ 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() (and its variants) do
>> + * not support writing configs to submodules.
>> + */
>
> Makes sense that we need a child process, but could the child process be
> "branch" instead of "submodule--helper"? If not, also mention why.

I'll mention why it has to be "submodule--helper"; I can see why a
reader might wonder this.

> As for the reason, probably better to explicitly mention
> install_branch_config_multiple_remotes() and say "(which is called by
> setup_tracking())".

Sounds good.

>> diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
>> new file mode 100755
>> index 0000000000..a2dfb5ad7f
>> --- /dev/null
>> +++ b/t/t3207-branch-submodule.sh
>> @@ -0,0 +1,291 @@
>> +#!/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
>> +
>> +test_expect_success 'setup superproject and submodule' '
>> +	git init super &&
>> +	test_commit 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 "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
>> +	git -C sub-upstream commit -m "add submodule" &&
>> +	# Regular submodule
>> +	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
>> +	# Submodule in a subdirectory
>> +	git -C super submodule add "$TRASH_DIRECTORY/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
>> +'
>> +
>> +CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
>> +
>> +cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
>> +	#!/bin/sh
>> +
>> +	super_dir="$1"
>> +	shift
>> +	(
>> +		cd "$super_dir" &&
>> +		git checkout main &&
>> +		for branch_name in "$@"; do
>> +			git branch -D "$branch_name"
>> +			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
>> +		done
>> +	)
>> +EOF
>> +chmod +x "$CLEANUP_SCRIPT_PATH"
>> +
>> +cleanup_branches() {
>> +	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
>> +} >/dev/null 2>/dev/null
>
> I don't think that the cleanup is saving much in process invocation
> anymore - could we just delete the whole thing and start anew in each
> test?
>
> The rest of the tests are assuming that the cleanup works as expected -
> I didn't take a close look.

Hm, you're right - deleting the branches is already quite slow, starting
anew would be easier and might not be much slower. I'll test the
'starting anew' approach to make sure it's not too slow.

> Also, you should probably use "$(pwd)" instead of $TRASH_DIRECTORY.

Thanks!

>> +test_expect_success '--recurse-submodules should fail when not creating branches' '
>> +	test_when_finished "cleanup_branches super branch-a" &&
>> +	(
>> +		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 --abbrev-ref branch-a &&
>> +		git -C sub rev-parse --abbrev-ref branch-a
>> +	)
>> +'
>
> If we're just checking that the ref exists, no need for "--abbrev-ref".
> Same comment throughout the file.

Ah yes, thanks.

>> +test_expect_success 'should not create any branches if branch is not valid for all repos' '
>> +	test_when_finished "cleanup_branches super branch-a" &&
>> +	(
>> +		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 &&
>> +
>> +		cat >expected <<-EOF &&
>> +		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
>> +		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
>> +		EOF
>> +		test_cmp expected actual
>> +	)
>> +'
>
> The error message seems too specific - probably enough to grep for the
> information about the branch already existing.

Makes sense.

>
>> +test_expect_success 'should create branches if branch exists and --force is given' '
>> +	test_when_finished "cleanup_branches super branch-a" &&
>> +	(
>> +		cd super &&
>> +		git -C sub rev-parse HEAD >expected &&
>> +		test_commit -C sub baz &&
>> +		git -C sub branch branch-a HEAD~1 &&
>> +		git branch --recurse-submodules --force branch-a &&
>> +		git rev-parse branch-a &&
>> +		# assert that sub:branch-a was moved
>> +		git -C sub rev-parse branch-a >actual &&
>> +		test_cmp expected actual
>> +	)
>> +'
>
> Should we create branch-a at HEAD instead of HEAD~1?

If we create branch-a at HEAD, we won't be testing that --force moves
the branch head. This means that the test might pass if we simply ignore
any existing branch-a - which is not the intended behavior of --force,
but this is behavior that we might want in the future (probably using
another option).

>> +test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
>> +	test_when_finished "cleanup_branches super branch-a branch-b branch-c" &&
>> +	(
>> +		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 rev-parse branch-c &&
>> +		git -C sub rev-parse branch-c &&
>> +		git -C second/sub rev-parse branch-c &&
>> +		git checkout --recurse-submodules branch-c &&
>> +		git -C sub2 rev-parse branch-c &&
>> +		git -C sub2/sub-sub rev-parse branch-c
>> +	)
>> +'
>
> No need to check so many repos, I think - just sub2 will do.

Hm yes, there isn't a reason to think that the branch wouldn't be
created in the other repos.

>
> [skip tracking tests]
>
>> +test_expect_success 'should not create branches in inactive submodules' '
>> +	test_when_finished "cleanup_branches super branch-a" &&
>> +	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
>> +	)
>> +'
>
> Makes sense, but could all the tracking tests be together in the file?
> Or is this order for a reason?

No, you're right, the order doesn't make sense. I'll move this to before
the tracking tests.

>> +test_expect_success 'setup remote-tracking tests' '
>
> This setup is not just for remote-tracking tests.

Yes, this is misleading, thanks.

>
>> +	(
>> +		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
>> +'
>> +
>> +test_expect_success 'should not create branch when submodule is not in .git/modules' '
>
> I understand that no branch is created, but the title is ambiguous, to
> me, whether it is a fatal error or not. Maybe the title should be "fatal
> error upon branch creation when submodule is not in .git/modules".

Ok, makes sense.

>
>> +	# The cleanup needs to delete sub2 separately because main does not have sub2
>> +	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
>> +		git -C super-clone/sub2/sub-sub branch -D branch-b && \
>> +		cleanup_branches super-clone branch-a branch-b" &&
>> +	(
>> +		cd super-clone &&
>> +		# This should succeed because super-clone has sub.
>
> "has sub in .git/modules", I think.
>
>> +		git branch --recurse-submodules branch-a origin/branch-a &&
>> +		# This should fail because super-clone does not have sub2.
>
> Likewise.

Ah, yes, it might be confusing otherwise.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 1/5] branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  2022-01-11 17:29               ` Glen Choo
@ 2022-01-11 20:03                 ` Jonathan Tan
  0 siblings, 0 replies; 110+ messages in thread
From: Jonathan Tan @ 2022-01-11 20:03 UTC (permalink / raw)
  To: chooglen
  Cc: jonathantanmy, git, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> >> @@ -401,7 +410,34 @@ void create_branch(struct repository *r,
> >>  
> >>  	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
> >>  		die(_("Not a valid branch point: '%s'."), start_name);
> >> -	oidcpy(&oid, &commit->object.oid);
> >> +	if (out_real_ref)
> >> +		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
> >
> > I think you can just write "*out_real_ref = real_ref; real_ref = NULL;"
> > here, and then not need to xstrdup.
> 
> Hm, you are right. The xstrdup was added because the original function
> calls FREE_AND_NULL(real_ref) and then checks if real_ref is NULL. But
> after the refactor, real_ref is not referenced after the
> FREE_AND_NULL(real_ref), so that call can be removed.
> 
> I intend to remove the xstrdup, though it will introduce a bit of noise
> because that block will no longer be moved wholesale.

Ah, thanks for watching out for the diff. I think in this case, we'll be
fine, since the xstrdup line is a new line ("xstrdup" only appears once
in the diff, here).

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation
  2022-01-11 18:11               ` Glen Choo
@ 2022-01-11 20:15                 ` Jonathan Tan
  2022-01-11 23:22                   ` Glen Choo
  0 siblings, 1 reply; 110+ messages in thread
From: Jonathan Tan @ 2022-01-11 20:15 UTC (permalink / raw)
  To: chooglen
  Cc: jonathantanmy, git, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Glen Choo <chooglen@google.com> writes:
> >> +test_expect_success 'should create branches if branch exists and --force is given' '
> >> +	test_when_finished "cleanup_branches super branch-a" &&
> >> +	(
> >> +		cd super &&
> >> +		git -C sub rev-parse HEAD >expected &&
> >> +		test_commit -C sub baz &&
> >> +		git -C sub branch branch-a HEAD~1 &&
> >> +		git branch --recurse-submodules --force branch-a &&
> >> +		git rev-parse branch-a &&
> >> +		# assert that sub:branch-a was moved
> >> +		git -C sub rev-parse branch-a >actual &&
> >> +		test_cmp expected actual
> >> +	)
> >> +'
> >
> > Should we create branch-a at HEAD instead of HEAD~1?
> 
> If we create branch-a at HEAD, we won't be testing that --force moves
> the branch head. 

Walking through the lines of the test:

> >> +		git -C sub rev-parse HEAD >expected &&

So "expected" is sub's HEAD at the start of the test. Let's call this
old-head.

> >> +		test_commit -C sub baz &&

We create a new commit on top, whose parent is old-head. Let's call this
new-head.

> >> +		git -C sub branch branch-a HEAD~1 &&

We create a new branch at HEAD~1, which is new-head's parent, which is
old-head. So this branch points to the same thing as "expected".

> >> +		git branch --recurse-submodules --force branch-a &&

When creating new branches with "--force", any branch information in the
submodule is ignored. So we would expect "branch-a" in sub to be
overridden from "old-head" to "old-head".

> >> +		git rev-parse branch-a &&

Verifying that branch-a exists, although upon second look, this would
work whether or not we recursed, so maybe this line can be deleted.

> >> +		# assert that sub:branch-a was moved
> >> +		git -C sub rev-parse branch-a >actual &&
> >> +		test_cmp expected actual

A check, as expected.

Unless I missed something, branch-a was never moved - it was created at
"old-head" and then the "branch --force" is supposed to create it at
"old-head" anyway. It would make more sense to me if the branch was
created at "new-head", and then "branch --force" moved it to "old-head".

> This means that the test might pass if we simply ignore
> any existing branch-a - which is not the intended behavior of --force,
> but this is behavior that we might want in the future (probably using
> another option).

By "ignore", supposing that there is an existing branch-a, do you mean
overwrite branch-a, or not create any branch at all?

^ permalink raw reply	[flat|nested] 110+ messages in thread

* Re: [PATCH v6 5/5] branch: add --recurse-submodules option for branch creation
  2022-01-11 20:15                 ` Jonathan Tan
@ 2022-01-11 23:22                   ` Glen Choo
  0 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2022-01-11 23:22 UTC (permalink / raw)
  To: Jonathan Tan
  Cc: jonathantanmy, git, steadmon, emilyshaffer, avarab,
	levraiphilippeblain, gitster

Jonathan Tan <jonathantanmy@google.com> writes:

> Glen Choo <chooglen@google.com> writes:
>> >> +test_expect_success 'should create branches if branch exists and --force is given' '
>> >> +	test_when_finished "cleanup_branches super branch-a" &&
>> >> +	(
>> >> +		cd super &&
>> >> +		git -C sub rev-parse HEAD >expected &&
>> >> +		test_commit -C sub baz &&
>> >> +		git -C sub branch branch-a HEAD~1 &&
>> >> +		git branch --recurse-submodules --force branch-a &&
>> >> +		git rev-parse branch-a &&
>> >> +		# assert that sub:branch-a was moved
>> >> +		git -C sub rev-parse branch-a >actual &&
>> >> +		test_cmp expected actual
>> >> +	)
>> >> +'
>> >
>> > Should we create branch-a at HEAD instead of HEAD~1?
>> 
>> If we create branch-a at HEAD, we won't be testing that --force moves
>> the branch head. 
>
> Walking through the lines of the test:
>
>> >> +		git -C sub rev-parse HEAD >expected &&
>
> So "expected" is sub's HEAD at the start of the test. Let's call this
> old-head.
>
>> >> +		test_commit -C sub baz &&
>
> We create a new commit on top, whose parent is old-head. Let's call this
> new-head.
>
>> >> +		git -C sub branch branch-a HEAD~1 &&
>
> We create a new branch at HEAD~1, which is new-head's parent, which is
> old-head. So this branch points to the same thing as "expected".
>
>> >> +		git branch --recurse-submodules --force branch-a &&
>
> When creating new branches with "--force", any branch information in the
> submodule is ignored. So we would expect "branch-a" in sub to be
> overridden from "old-head" to "old-head".
>
>> >> +		git rev-parse branch-a &&
>
> Verifying that branch-a exists, although upon second look, this would
> work whether or not we recursed, so maybe this line can be deleted.
>
>> >> +		# assert that sub:branch-a was moved
>> >> +		git -C sub rev-parse branch-a >actual &&
>> >> +		test_cmp expected actual
>
> A check, as expected.
>
> Unless I missed something, branch-a was never moved - it was created at
> "old-head" and then the "branch --force" is supposed to create it at
> "old-head" anyway. It would make more sense to me if the branch was
> created at "new-head", and then "branch --force" moved it to "old-head".

Oh, yes, I tested this and you're right. That's a really good catch - it
would have been hard to spot amongst all of the tests. Yes, I should
have set it up the way you described. I'll modify the test to assert
that "branch --force" moved the branch and instead of creating it where
we expected.

>> This means that the test might pass if we simply ignore
>> any existing branch-a - which is not the intended behavior of --force,
>> but this is behavior that we might want in the future (probably using
>> another option).
>
> By "ignore", supposing that there is an existing branch-a, do you mean
> overwrite branch-a, or not create any branch at all?

What I meant was that if branch-a was not moved, the test would pass
if we did not create branch-a at all (and that we might want an option
that will not create a branch if it already exists).

But.. that comment was made under wrong assumptions - I had assumed that
branch-a was moved, and that your proposed fix would not move branch-a
(which is the complete opposite of reality). We agree that the test
makes more sense if branch-a is moved.

^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v7 0/6] implement branch --recurse-submodules
  2021-12-20 23:34         ` [PATCH v6 " Glen Choo
                             ` (6 preceding siblings ...)
  2021-12-21  1:07           ` Junio C Hamano
@ 2022-01-24 20:44           ` Glen Choo
  2022-01-24 20:44             ` [PATCH v7 1/6] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
                               ` (6 more replies)
  7 siblings, 7 replies; 110+ messages in thread
From: Glen Choo @ 2022-01-24 20:44 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Thanks for the feedback, Jonathan! I believe I've incorporated all of your
suggested fixes in this version - the most notable of which is to recreate the
test repo setup in each test instead of deleting branches. To save CPU cycles in
setting up the test, the test suite writes the clean test setup to a
directory, then copies the setup to the right location for each test. In my
informal testing, this is even faster than deleting the unused branches.

The range-diff for patches 3-4 have some irrelevant noise because this version
is based off 'js/branch-track-inherit', but (due to a mistake when applying the
'js/branch-track-inherit' patches), v6 is based off a different 'master' commit.

= Description

This series implements "git branch --recurse-submodules", which is part of the
submodule branching RFC (see "See also"). "git branch" (and some other features
in that RFC) are incompatible with existing "--recurse-submodules" commands; 
patch 5 describes this incompatibility in more detail and how we plan to
introduce this new functionality in a manner.

This series is based off js/branch-track-inherit.

Patches 1-4 are preparation for the implementation of "branch
--recurse-submodules" in patch 5. Patch 6 cleans up a memory leak that I
encountered on js/branch-track-inherit as I was moving code around; it isn't
neeeded by "branch --recurse-submodules".

See the previous cover letter
(https://lore.kernel.org/git/20211220233459.45739-1-chooglen@google.com)
for discussions of future work.

= See also

Submodule branching RFC:
https://lore.kernel.org/git/kl6lv912uvjv.fsf@chooglen-macbookpro.roam.corp.google.com/

Original Submodule UX RFC/Discussion:
https://lore.kernel.org/git/YHofmWcIAidkvJiD@google.com/

Contributor Summit submodules Notes:
https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@tvgsbejvaqbjf.bet/

Submodule UX overhaul updates:
https://lore.kernel.org/git/?q=Submodule+UX+overhaul+update

= Changes

Changes since v6:
* Move memory leak fix into its own patch (patch 6)
* Eliminate unnecessary rewrapping and small style fixes
* In branch --recurse-submodules tests, setup test repo anew instead of
  deleting the unused branches
* Numerous commit message wording changes

Changes since v5:
* Rebase onto v7 of js/branch-track-inherit
  (https://lore.kernel.org/git/cover.1639717481.git.steadmon@google.com)

Changes since v4:
* Rebase correctly onto 'gitster/seen^{/^Merge branch .js/branch-track-inherit.}'
  (see base-commit) as suggested in [1] (thanks Junio!)
* These patches were also verified on top of 'next'.

Changes since v3:
* Split up the old patch 1. Patch 1 had a big diff because it used to
  move lines, remove dead code and introduce repo_* functions (thanks
  Jonathan!)
** repo_* functions have been dropped; they added noise and are not
   necessary for correctness.
* Use a new, harder-to-misuse function in --set-upstream-to,
  dwim_and_setup_tracking(). Now, setup_tracking() never does DWIM and
  dwim_and_setup_tracking() always does DWIM.
* Move create_branch() dry_run to its own patch.
* Fix an oversight where submodules in subtrees were ignored. This was
  because submodules_of_tree() and tree_entry() didn't recurse into
  subtrees. Test this accordingly (thanks Jonathan!).
* cmd_branch() possible actions are more consistently ordered.
* Documentation fixes (thanks Philippe!).
* Additional comments and explanation.
* Drop patch 5 (optional cleanup).
* Rebase onto js/branch-track-inherit v6.

Changes since v2:
* Rebase onto js/branch-track-inherit. This series should continue to be
  the case going forward.
* Patch 1 has a smaller diff because the introduction of
  validate_branch_start() no longer changes the function order thanks to a
  forward declaration. This artificial forward declaration is removed in a
  patch 2 (which can just be squashed into patch 1).
* Optional cleanup: fix questionable exit codes in patch 5.

Changes since v1:
* Move the functionality of "git branch --dry-run" into "git submodule-helper create-branch --dry-run"
* Add more fields to the submodules_of_tree() struct to reduce the
  number of allocations made by the caller. Move this functionality
  to patch 3 (formerly patch 4) and drop patch 1.
* Make submodules_of_tree() ignore inactive submodules
* Structure the output of the submodules a bit better by adding prefixes
  to the child process' output (instead of inconsistently indenting the
  output).
** I wasn't able to find a good way to interleave stdout/stderr
   correctly, so a less-than-desirable workaround was to route the child
   process output to stdout/stderr depending on the exit code.
** Eventually, I would like to structure the output of submodules in a
   report, as Ævar suggested. But at this stage, I think that it's
   better to spend time getting user feedback on the submodules
   branching UX and it'll be easier to standardize the output when we've
   implemented more of the UX :)

[1] https://lore.kernel.org/git/xmqqlf0lz6os.fsf@gitster.g 

Glen Choo (6):
  branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  branch: make create_branch() always create a branch
  branch: add a dry_run parameter to create_branch()
  builtin/branch: consolidate action-picking logic in cmd_branch()
  branch: add --recurse-submodules option for branch creation
  branch.c: use 'goto cleanup' in setup_tracking() to fix memory leaks

 Documentation/config/advice.txt    |   3 +
 Documentation/config/submodule.txt |  37 ++--
 Documentation/git-branch.txt       |  11 +-
 advice.c                           |   1 +
 advice.h                           |   1 +
 branch.c                           | 271 ++++++++++++++++++++++-----
 branch.h                           |  53 +++++-
 builtin/branch.c                   |  70 +++++--
 builtin/checkout.c                 |   3 +-
 builtin/submodule--helper.c        |  38 ++++
 submodule-config.c                 |  60 ++++++
 submodule-config.h                 |  34 ++++
 submodule.c                        |  11 +-
 submodule.h                        |   3 +
 t/t3200-branch.sh                  |  17 ++
 t/t3207-branch-submodule.sh        | 282 +++++++++++++++++++++++++++++
 16 files changed, 815 insertions(+), 80 deletions(-)
 create mode 100755 t/t3207-branch-submodule.sh

Range-diff against v6:
1:  29669c57b4 ! 1:  206175cfb3 branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
    @@ Metadata
      ## Commit message ##
         branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
     
    -    This refactor is motivated by a desire to add a "dry_run" parameter to
    -    create_branch() that will validate whether or not a branch can be
    -    created without actually creating it - this behavior will be used in a
    -    subsequent commit that adds `git branch --recurse-submodules topic`.
    +    This commit is preparation for a future commit that will simplify
    +    create_branch() so that it always creates a branch. This will allow
    +    create_branch() to accept a dry_run parameter (which is needed for "git
    +    branch --recurse-submodules").
     
    -    Adding "dry_run" is not obvious because create_branch() is also used to
    -    set tracking information without creating a branch, i.e. when using
    -    --set-upstream-to. This appears to be a leftover from 4fc5006676 (Add
    -    branch --set-upstream, 2010-01-18), when --set-upstream would sometimes
    -    create a branch and sometimes update tracking information without
    -    creating a branch. However, we no longer support --set-upstream, so it
    -    makes more sense to set tracking information with another function and
    -    use create_branch() only to create branches. In a later commit, we will
    -    remove the now-unnecessary logic from create_branch() so that "dry_run"
    -    becomes trivial to implement.
    +    create_branch() used to always create a branch, but 4fc5006676 (Add
    +    branch --set-upstream, 2010-01-18) changed it to also be able to set
    +    tracking information without creating a branch.
     
    -    Introduce dwim_and_setup_tracking(), which replaces create_branch()
    -    in `git branch --set-upstream-to`. Ensure correctness by moving the DWIM
    -    and branch validation logic from create_branch() into a helper function,
    -    dwim_branch_start(), so that the logic is shared by both functions.
    +    Refactor the code that sets tracking information into its own functions
    +    dwim_branch_start() and dwim_and_setup_tracking(). Also change an
    +    invocation of create_branch() in cmd_branch() in builtin/branch.c to use
    +    dwim_and_setup_tracking(), since that invocation is only for setting
    +    tracking information (in "git branch --set-upstream-to").
     
    +    As of this commit, create_branch() is no longer invoked in a way that
    +    does not create branches.
    +
    +    Helped-by: Jonathan Tan <jonathantanmy@google.com>
         Signed-off-by: Glen Choo <chooglen@google.com>
     
      ## branch.c ##
    @@ branch.c: static int inherit_tracking(struct tracking *tracking, const char *ori
       */
      static void setup_tracking(const char *new_ref, const char *orig_ref,
      			   enum branch_track track, int quiet)
    -@@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
    - 		case BRANCH_TRACK_INHERIT:
    - 			break;
    - 		default:
    --			return;
    -+			goto cleanup;
    - 		}
    - 
    - 	if (tracking.matches > 1)
    -@@ branch.c: static void setup_tracking(const char *new_ref, const char *orig_ref,
    - 				tracking.remote, tracking.srcs) < 0)
    - 		exit(-1);
    - 
    -+cleanup:
    - 	string_list_clear(tracking.srcs, 0);
    - }
    - 
     @@ branch.c: N_("\n"
      "will track its remote counterpart, you may want to use\n"
      "\"git push -u\" to set the upstream config as you push.");
    @@ branch.c: void create_branch(struct repository *r,
      	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
      		die(_("Not a valid branch point: '%s'."), start_name);
     -	oidcpy(&oid, &commit->object.oid);
    -+	if (out_real_ref)
    -+		*out_real_ref = real_ref ? xstrdup(real_ref) : NULL;
    ++	if (out_real_ref) {
    ++		*out_real_ref = real_ref;
    ++		real_ref = NULL;
    ++	}
     +	if (out_oid)
     +		oidcpy(out_oid, &commit->object.oid);
     +
     +	FREE_AND_NULL(real_ref);
     +}
     +
    -+void create_branch(struct repository *r, const char *name,
    -+		   const char *start_name, int force, int clobber_head_ok,
    -+		   int reflog, int quiet, enum branch_track track)
    ++void create_branch(struct repository *r,
    ++		   const char *name, const char *start_name,
    ++		   int force, int clobber_head_ok, int reflog,
    ++		   int quiet, enum branch_track track)
     +{
     +	struct object_id oid;
     +	char *real_ref;
2:  ac2532a953 ! 2:  2e02885eb3 branch: make create_branch() always create a branch
    @@ Metadata
      ## Commit message ##
         branch: make create_branch() always create a branch
     
    -    create_branch() was formerly used to set tracking without creating a
    -    branch. Since the previous commit replaces this use case with
    -    dwim_and_setup_tracking(), we can simplify create_branch() so that it
    -    always creates a branch.
    +    With the previous commit, there are no more invocations of
    +    create_branch() that do not create a branch because:
     
    -    Do this simplification, in particular:
    +    * BRANCH_TRACK_OVERRIDE is no longer passed
    +    * clobber_head_ok = true and force = false is never passed
     
    -    * remove the special handling of BRANCH_TRACK_OVERRIDE because it is no
    -      longer used
    -    * assert that clobber_head_ok can only be provided with force
    -    * check that we're handling clobber_head_ok and force correctly by
    -      introducing tests for `git branch --force`
    +    Assert these situations, delete dead code and ensure that we're handling
    +    clobber_head_ok and force correctly by introducing tests for `git branch
    +    --force`. As a result, create_branch() now always creates a branch.
     
    +    Helped-by: Jonathan Tan <jonathantanmy@google.com>
         Signed-off-by: Glen Choo <chooglen@google.com>
     
      ## branch.c ##
    -@@ branch.c: void create_branch(struct repository *r, const char *name,
    +@@ branch.c: void create_branch(struct repository *r,
      	char *real_ref;
      	struct strbuf ref = STRBUF_INIT;
      	int forcing = 0;
    @@ branch.c: void create_branch(struct repository *r, const char *name,
     +	struct strbuf err = STRBUF_INIT;
     +	char *msg;
     +
    ++	if (track == BRANCH_TRACK_OVERRIDE)
    ++		BUG("'track' cannot be BRANCH_TRACK_OVERRIDE. Did you mean to call dwim_and_setup_tracking()?");
     +	if (clobber_head_ok && !force)
     +		BUG("'clobber_head_ok' can only be used with 'force'");
     +
    @@ branch.c: void create_branch(struct repository *r, const char *name,
      	}
      
      	dwim_branch_start(r, start_name, track, &real_ref, &oid);
    -@@ branch.c: void create_branch(struct repository *r, const char *name,
    +@@ branch.c: void create_branch(struct repository *r,
      	if (reflog)
      		log_all_ref_updates = LOG_REFS_NORMAL;
      
3:  a0ed3fa438 ! 3:  cd43a9aaaa branch: add a dry_run parameter to create_branch()
    @@ Commit message
     
      ## branch.c ##
     @@ branch.c: static void dwim_branch_start(struct repository *r, const char *start_name,
    - 
    - void create_branch(struct repository *r, const char *name,
    - 		   const char *start_name, int force, int clobber_head_ok,
    --		   int reflog, int quiet, enum branch_track track)
    -+		   int reflog, int quiet, enum branch_track track, int dry_run)
    + void create_branch(struct repository *r,
    + 		   const char *name, const char *start_name,
    + 		   int force, int clobber_head_ok, int reflog,
    +-		   int quiet, enum branch_track track)
    ++		   int quiet, enum branch_track track, int dry_run)
      {
      	struct object_id oid;
      	char *real_ref;
    -@@ branch.c: void create_branch(struct repository *r, const char *name,
    +@@ branch.c: void create_branch(struct repository *r,
      	}
      
      	dwim_branch_start(r, start_name, track, &real_ref, &oid);
    @@ branch.c: void create_branch(struct repository *r, const char *name,
      
      	if (reflog)
      		log_all_ref_updates = LOG_REFS_NORMAL;
    -@@ branch.c: void create_branch(struct repository *r, const char *name,
    +@@ branch.c: void create_branch(struct repository *r,
      	if (real_ref && track)
      		setup_tracking(ref.buf + 11, real_ref, track, quiet);
      
    @@ branch.h: void dwim_and_setup_tracking(struct repository *r, const char *new_ref
     + *   - dry_run causes the branch to be validated but not created.
     + *
       */
    --void create_branch(struct repository *r,
    --		   const char *name, const char *start_name,
    --		   int force, int clobber_head_ok,
    + void create_branch(struct repository *r,
    + 		   const char *name, const char *start_name,
    + 		   int force, int clobber_head_ok,
     -		   int reflog, int quiet, enum branch_track track);
    -+void create_branch(struct repository *r, const char *name,
    -+		   const char *start_name, int force, int clobber_head_ok,
    -+		   int reflog, int quiet, enum branch_track track, int dry_run);
    ++		   int reflog, int quiet, enum branch_track track,
    ++		   int dry_run);
      
      /*
       * Check if 'name' can be a valid name for a branch; die otherwise.
    @@ builtin/checkout.c: static void update_refs_for_switch(const struct checkout_opt
     -				      opts->track);
     +				      opts->track,
     +				      0);
    - 		free(new_branch_info->name);
    - 		free(new_branch_info->refname);
    - 		new_branch_info->name = xstrdup(opts->new_branch);
    + 		new_branch_info->name = opts->new_branch;
    + 		setup_branch_path(new_branch_info);
    + 	}
4:  ebded31c96 ! 4:  8e5f750162 builtin/branch: clean up action-picking logic in cmd_branch()
    @@ Metadata
     Author: Glen Choo <chooglen@google.com>
     
      ## Commit message ##
    -    builtin/branch: clean up action-picking logic in cmd_branch()
    +    builtin/branch: consolidate action-picking logic in cmd_branch()
     
    -    Add a variable to cmd_branch() that will tell us whether or not
    -    cmd_branch() will default to creating a branch (instead of performing
    -    another action). Besides making the function more explicit, this allows
    -    us to validate options that can only be used when creating a branch.
    -    Such an option does not exist yet, but one will be introduced in a
    -    subsequent commit.
    +    Consolidate the logic for deciding when to create a new branch in
    +    cmd_branch(), and save the result for reuse. Besides making the function
    +    more explicit, this allows us to validate options that can only be used
    +    when creating a branch. Such an option does not exist yet, but one will
    +    be introduced in a subsequent commit.
     
    +    Helped-by: Jonathan Tan <jonathantanmy@google.com>
         Signed-off-by: Glen Choo <chooglen@google.com>
     
      ## builtin/branch.c ##
    @@ builtin/branch.c: static int edit_branch_description(const char *branch_name)
      	enum branch_track track;
      	struct ref_filter filter;
     -	int icase = 0;
    - 	static struct ref_sorting *sorting;
    - 	struct string_list sorting_options = STRING_LIST_INIT_DUP;
    + 	static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
      	struct ref_format format = REF_FORMAT_INIT;
    + 
     @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix)
      	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
      		list = 1;
5:  0a7ec6ee75 ! 5:  c59de1fd9c branch: add --recurse-submodules option for branch creation
    @@ Commit message
         branch: add --recurse-submodules option for branch creation
     
         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
    +    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 semantics e.g. `git
    -    checkout` does not recursively checkout the expected branches created by
    -    `git branch` yet. To ensure that the correct set of semantics is used,
    -    this commit introduces a new configuration value,
    -    `submodule.propagateBranches`, which enables submodule branching when
    -    true (defaults to false).
    +    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.
     
    -    This commit includes changes that allow Git to work with submodules
    -    that are in trees (and not just the index):
    +    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
    @@ Commit message
         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>
     
      ## Documentation/config/advice.txt ##
    @@ Documentation/config/submodule.txt: submodule.active::
     -	Applies to all commands that support this option
     -	(`checkout`, `fetch`, `grep`, `pull`, `push`, `read-tree`, `reset`,
     -	`restore` and `switch`) except `clone` and `ls-files`.
    --	Defaults to false.
     +	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
    -@@ Documentation/config/submodule.txt: submodule.recurse::
    - 	`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
    +++
    ++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
    @@ Documentation/config/submodule.txt: submodule.recurse::
     +	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.
    + 	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.
    @@ branch.c: void dwim_and_setup_tracking(struct repository *r, const char *new_ref
     +/**
     + * Creates a branch in a submodule by calling
     + * create_branches_recursively() in a child process. The child process
    -+ * is necessary because install_branch_config() (and its variants) do
    -+ * not support writing configs to submodules.
    ++ * 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,
    @@ branch.c: void dwim_and_setup_tracking(struct repository *r, const char *new_ref
     +	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");
    @@ branch.c: void dwim_and_setup_tracking(struct repository *r, const char *new_ref
      	unlink(git_path_merge_head(r));
     
      ## branch.h ##
    -@@ branch.h: void create_branch(struct repository *r, const char *name,
    - 		   const char *start_name, int force, int clobber_head_ok,
    - 		   int reflog, int quiet, enum branch_track track, int dry_run);
    +@@ branch.h: 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
    @@ builtin/branch.c: int cmd_branch(int argc, const char **argv, const char *prefix
     +	    recurse_submodules_explicit = 0;
      	enum branch_track track;
      	struct ref_filter filter;
    - 	static struct ref_sorting *sorting;
    + 	static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
     @@ builtin/branch.c: 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),
    @@ t/t3207-branch-submodule.sh (new)
     +. ./test-lib.sh
     +. "$TEST_DIRECTORY"/lib-rebase.sh
     +
    -+test_expect_success 'setup superproject and submodule' '
    -+	git init super &&
    -+	test_commit 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 "$TRASH_DIRECTORY/sub-sub-upstream" sub-sub &&
    -+	git -C sub-upstream commit -m "add submodule" &&
    -+	# Regular submodule
    -+	git -C super submodule add "$TRASH_DIRECTORY/sub-upstream" sub &&
    -+	# Submodule in a subdirectory
    -+	git -C super submodule add "$TRASH_DIRECTORY/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
    -+'
    -+
    -+CLEANUP_SCRIPT_PATH="$TRASH_DIRECTORY/cleanup_branches.sh"
    ++pwd=$(pwd)
     +
    -+cat >"$CLEANUP_SCRIPT_PATH" <<'EOF'
    -+	#!/bin/sh
    ++# 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/* .
    ++}
     +
    -+	super_dir="$1"
    -+	shift
    ++test_expect_success 'setup superproject and submodule' '
    ++	mkdir test_dirs &&
     +	(
    -+		cd "$super_dir" &&
    -+		git checkout main &&
    -+		for branch_name in "$@"; do
    -+			git branch -D "$branch_name"
    -+			git submodule foreach "$TRASH_DIRECTORY/cleanup_branches.sh . $branch_name || true"
    -+		done
    -+	)
    -+EOF
    -+chmod +x "$CLEANUP_SCRIPT_PATH"
    -+
    -+cleanup_branches() {
    -+	TRASH_DIRECTORY="\"$TRASH_DIRECTORY\"" "$CLEANUP_SCRIPT_PATH" "$@"
    -+} >/dev/null 2>/dev/null
    ++		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 "cleanup_branches super branch-a" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git branch --recurse-submodules branch-a &&
    @@ t/t3207-branch-submodule.sh (new)
     +'
     +
     +test_expect_success '--recurse-submodules should die if submodule.propagateBranches is false' '
    -+	test_when_finished "cleanup_branches super branch-a" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		echo "fatal: branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled" >expected &&
    @@ t/t3207-branch-submodule.sh (new)
     +'
     +
     +test_expect_success '--recurse-submodules should fail when not creating branches' '
    -+	test_when_finished "cleanup_branches super branch-a" &&
    ++	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 --abbrev-ref branch-a &&
    -+		git -C sub rev-parse --abbrev-ref branch-a
    ++		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 "cleanup_branches super branch-a" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git -c submodule.recurse=true branch branch-a &&
    -+		git rev-parse --abbrev-ref branch-a &&
    -+		git -C sub rev-parse --abbrev-ref 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 "cleanup_branches super branch-a" &&
    ++	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 --abbrev-ref branch-a &&
    -+		git -C sub rev-parse --abbrev-ref 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 "cleanup_branches super branch-a branch-b" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git branch --recurse-submodules branch-a &&
    @@ t/t3207-branch-submodule.sh (new)
     +'
     +
     +test_expect_success 'should not create any branches if branch is not valid for all repos' '
    -+	test_when_finished "cleanup_branches super branch-a" &&
    ++	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 &&
    -+
    -+		cat >expected <<-EOF &&
    -+		submodule ${SQ}sub${SQ}: fatal: A branch named ${SQ}branch-a${SQ} already exists.
    -+		fatal: submodule ${SQ}sub${SQ}: cannot create branch ${SQ}branch-a${SQ}
    -+		EOF
    -+		test_cmp expected actual
    ++		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 "cleanup_branches super branch-a" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git -C sub rev-parse HEAD >expected &&
     +		test_commit -C sub baz &&
    -+		git -C sub branch branch-a HEAD~1 &&
    ++		# 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 &&
    -+		# assert that sub:branch-a was moved
    -+		git -C sub rev-parse branch-a >actual &&
    -+		test_cmp expected actual
    ++		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 "cleanup_branches super branch-a branch-b branch-c" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git branch branch-a &&
    @@ t/t3207-branch-submodule.sh (new)
     +		git commit -m "add second submodule" &&
     +		git checkout branch-a &&
     +		git branch --recurse-submodules branch-c branch-b &&
    -+		git rev-parse branch-c &&
    -+		git -C sub rev-parse branch-c &&
    -+		git -C second/sub rev-parse branch-c &&
     +		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 "cleanup_branches super branch-a" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git -c branch.autoSetupMerge=always branch --recurse-submodules branch-a main &&
    @@ t/t3207-branch-submodule.sh (new)
     +'
     +
     +test_expect_success 'should set up tracking of local branches with explicit track' '
    -+	test_when_finished "cleanup_branches super branch-a" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git branch --track --recurse-submodules branch-a main &&
    @@ t/t3207-branch-submodule.sh (new)
     +'
     +
     +test_expect_success 'should not set up unnecessary tracking of local branches' '
    -+	test_when_finished "cleanup_branches super branch-a" &&
    ++	test_when_finished "reset_test" &&
     +	(
     +		cd super &&
     +		git branch --recurse-submodules branch-a main &&
    @@ t/t3207-branch-submodule.sh (new)
     +	)
     +'
     +
    -+test_expect_success 'should not create branches in inactive submodules' '
    -+	test_when_finished "cleanup_branches super branch-a" &&
    -+	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
    -+	)
    -+'
    ++reset_remote_test () {
    ++	rm -fr super-clone &&
    ++	reset_test
    ++}
     +
    -+test_expect_success 'setup remote-tracking tests' '
    ++test_expect_success 'setup tests with remotes' '
     +	(
    -+		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"
    ++		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
     +	) &&
    -+	git clone --branch main --recurse-submodules super super-clone &&
    -+	git -C super-clone config submodule.propagateBranches true
    ++	reset_remote_test
     +'
     +
    -+test_expect_success 'should not create branch when submodule is not in .git/modules' '
    -+	# The cleanup needs to delete sub2 separately because main does not have sub2
    -+	test_when_finished "git -C super-clone/sub2 branch -D branch-b && \
    -+		git -C super-clone/sub2/sub-sub branch -D branch-b && \
    -+		cleanup_branches super-clone branch-a branch-b" &&
    ++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.
    ++		# 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.
    ++		# 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 &&
    -+		cat >expected <<-EOF &&
    -+		hint: You may try updating the submodules using ${SQ}git checkout origin/branch-b && git submodule update --init${SQ}
    -+		fatal: submodule ${SQ}sub2${SQ}: unable to find submodule
    -+		EOF
    -+		test_cmp expected 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
    @@ t/t3207-branch-submodule.sh (new)
     +'
     +
     +test_expect_success 'should set up tracking of remote-tracking branches' '
    -+	test_when_finished "cleanup_branches super-clone branch-a" &&
    ++	test_when_finished "reset_remote_test" &&
     +	(
     +		cd super-clone &&
     +		git branch --recurse-submodules branch-a origin/branch-a &&
    @@ t/t3207-branch-submodule.sh (new)
     +'
     +
     +test_expect_success 'should not fail when unable to set up tracking in submodule' '
    -+	test_when_finished "cleanup_branches super-clone branch-a && \
    -+		git -C super-clone remote rename ex-origin origin" &&
    ++	test_when_finished "reset_remote_test" &&
     +	(
     +		cd super-clone &&
     +		git remote rename origin ex-origin &&
-:  ---------- > 6:  a4634f0493 branch.c: use 'goto cleanup' in setup_tracking() to fix memory leaks

base-commit: 44f14a9d24cd9f04a0a789e58968d52d44d7f332
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v7 1/6] branch: move --set-upstream-to behavior to dwim_and_setup_tracking()
  2022-01-24 20:44           ` [PATCH v7 0/6] " Glen Choo
@ 2022-01-24 20:44             ` Glen Choo
  2022-01-24 20:44             ` [PATCH v7 2/6] branch: make create_branch() always create a branch Glen Choo
                               ` (5 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2022-01-24 20:44 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

This commit is preparation for a future commit that will simplify
create_branch() so that it always creates a branch. This will allow
create_branch() to accept a dry_run parameter (which is needed for "git
branch --recurse-submodules").

create_branch() used to always create a branch, but 4fc5006676 (Add
branch --set-upstream, 2010-01-18) changed it to also be able to set
tracking information without creating a branch.

Refactor the code that sets tracking information into its own functions
dwim_branch_start() and dwim_and_setup_tracking(). Also change an
invocation of create_branch() in cmd_branch() in builtin/branch.c to use
dwim_and_setup_tracking(), since that invocation is only for setting
tracking information (in "git branch --set-upstream-to").

As of this commit, create_branch() is no longer invoked in a way that
does not create branches.

Helped-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c         | 87 +++++++++++++++++++++++++++++++++++++-----------
 branch.h         | 22 ++++++++++++
 builtin/branch.c |  9 ++---
 3 files changed, 92 insertions(+), 26 deletions(-)

diff --git a/branch.c b/branch.c
index a4e4631ef1..f3a31930fb 100644
--- a/branch.c
+++ b/branch.c
@@ -218,9 +218,11 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)
 }
 
 /*
- * 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.
+ * Used internally to set the branch.<new_ref>.{remote,merge} config
+ * settings so that branch 'new_ref' tracks 'orig_ref'. Unlike
+ * dwim_and_setup_tracking(), this does not do DWIM, i.e. "origin/main"
+ * will not be expanded to "refs/remotes/origin/main", so it is not safe
+ * for 'orig_ref' to be raw user input.
  */
 static void setup_tracking(const char *new_ref, const char *orig_ref,
 			   enum branch_track track, int quiet)
@@ -341,31 +343,37 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-void create_branch(struct repository *r,
-		   const char *name, const char *start_name,
-		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+/**
+ * DWIMs a user-provided ref to determine the starting point for a
+ * branch and validates it, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test. This is
+ *     expanded with DWIM and assigned to out_real_ref.
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - out_oid is an out parameter containing the object_id of start_name
+ *
+ *   - out_real_ref is an out parameter containing the full, 'real' form
+ *     of start_name e.g. refs/heads/main instead of main
+ *
+ */
+static void dwim_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, char **out_real_ref,
+			   struct object_id *out_oid)
 {
 	struct commit *commit;
 	struct object_id oid;
 	char *real_ref;
-	struct strbuf ref = STRBUF_INIT;
-	int forcing = 0;
-	int dont_change_ref = 0;
 	int explicit_tracking = 0;
 
 	if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE)
 		explicit_tracking = 1;
 
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
-	}
-
 	real_ref = NULL;
 	if (get_oid_mb(start_name, &oid)) {
 		if (explicit_tracking) {
@@ -402,7 +410,37 @@ void create_branch(struct repository *r,
 
 	if ((commit = lookup_commit_reference(r, &oid)) == NULL)
 		die(_("Not a valid branch point: '%s'."), start_name);
-	oidcpy(&oid, &commit->object.oid);
+	if (out_real_ref) {
+		*out_real_ref = real_ref;
+		real_ref = NULL;
+	}
+	if (out_oid)
+		oidcpy(out_oid, &commit->object.oid);
+
+	FREE_AND_NULL(real_ref);
+}
+
+void create_branch(struct repository *r,
+		   const char *name, const char *start_name,
+		   int force, int clobber_head_ok, int reflog,
+		   int quiet, enum branch_track track)
+{
+	struct object_id oid;
+	char *real_ref;
+	struct strbuf ref = STRBUF_INIT;
+	int forcing = 0;
+	int dont_change_ref = 0;
+
+	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
+	    ? validate_branchname(name, &ref)
+	    : validate_new_branchname(name, &ref, force)) {
+		if (!force)
+			dont_change_ref = 1;
+		else
+			forcing = 1;
+	}
+
+	dwim_branch_start(r, start_name, track, &real_ref, &oid);
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -436,6 +474,15 @@ void create_branch(struct repository *r,
 	free(real_ref);
 }
 
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet)
+{
+	char *real_orig_ref;
+	dwim_branch_start(r, orig_ref, track, &real_orig_ref, NULL);
+	setup_tracking(new_ref, real_orig_ref, track, quiet);
+}
+
 void remove_merge_branch_state(struct repository *r)
 {
 	unlink(git_path_merge_head(r));
diff --git a/branch.h b/branch.h
index 815dcd40c0..ab2315c611 100644
--- a/branch.h
+++ b/branch.h
@@ -18,6 +18,28 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/**
+ * Sets branch.<new_ref>.{remote,merge} config settings such that
+ * new_ref tracks orig_ref according to the specified tracking mode.
+ *
+ *   - new_ref is the name of the branch that we are setting tracking
+ *     for.
+ *
+ *   - orig_ref is the name of the ref that is 'upstream' of new_ref.
+ *     orig_ref will be expanded with DWIM so that the config settings
+ *     are in the correct format e.g. "refs/remotes/origin/main" instead
+ *     of "origin/main".
+ *
+ *   - track is the tracking mode e.g. BRANCH_TRACK_REMOTE causes
+ *     new_ref to track orig_ref directly, whereas BRANCH_TRACK_INHERIT
+ *     causes new_ref to track whatever orig_ref tracks.
+ *
+ *   - quiet suppresses tracking information.
+ */
+void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
+			     const char *orig_ref, enum branch_track track,
+			     int quiet);
+
 /*
  * Creates a new branch, where:
  *
diff --git a/builtin/branch.c b/builtin/branch.c
index 1890afd4e5..59768a3f67 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -822,12 +822,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (!ref_exists(branch->refname))
 			die(_("branch '%s' does not exist"), branch->name);
 
-		/*
-		 * create_branch takes care of setting up the tracking
-		 * info and making sure new_upstream is correct
-		 */
-		create_branch(the_repository, branch->name, new_upstream,
-			      0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE);
+		dwim_and_setup_tracking(the_repository, branch->name,
+					new_upstream, BRANCH_TRACK_OVERRIDE,
+					quiet);
 	} else if (unset_upstream) {
 		struct branch *branch = branch_get(argv[0]);
 		struct strbuf buf = STRBUF_INIT;
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v7 2/6] branch: make create_branch() always create a branch
  2022-01-24 20:44           ` [PATCH v7 0/6] " Glen Choo
  2022-01-24 20:44             ` [PATCH v7 1/6] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
@ 2022-01-24 20:44             ` Glen Choo
  2022-01-24 20:44             ` [PATCH v7 3/6] branch: add a dry_run parameter to create_branch() Glen Choo
                               ` (4 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2022-01-24 20:44 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

With the previous commit, there are no more invocations of
create_branch() that do not create a branch because:

* BRANCH_TRACK_OVERRIDE is no longer passed
* clobber_head_ok = true and force = false is never passed

Assert these situations, delete dead code and ensure that we're handling
clobber_head_ok and force correctly by introducing tests for `git branch
--force`. As a result, create_branch() now always creates a branch.

Helped-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c          | 57 ++++++++++++++++++++++-------------------------
 branch.h          |  4 ++--
 t/t3200-branch.sh | 17 ++++++++++++++
 3 files changed, 46 insertions(+), 32 deletions(-)

diff --git a/branch.c b/branch.c
index f3a31930fb..df24021f27 100644
--- a/branch.c
+++ b/branch.c
@@ -429,15 +429,19 @@ void create_branch(struct repository *r,
 	char *real_ref;
 	struct strbuf ref = STRBUF_INIT;
 	int forcing = 0;
-	int dont_change_ref = 0;
-
-	if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok)
-	    ? validate_branchname(name, &ref)
-	    : validate_new_branchname(name, &ref, force)) {
-		if (!force)
-			dont_change_ref = 1;
-		else
-			forcing = 1;
+	struct ref_transaction *transaction;
+	struct strbuf err = STRBUF_INIT;
+	char *msg;
+
+	if (track == BRANCH_TRACK_OVERRIDE)
+		BUG("'track' cannot be BRANCH_TRACK_OVERRIDE. Did you mean to call dwim_and_setup_tracking()?");
+	if (clobber_head_ok && !force)
+		BUG("'clobber_head_ok' can only be used with 'force'");
+
+	if (clobber_head_ok ?
+			  validate_branchname(name, &ref) :
+			  validate_new_branchname(name, &ref, force)) {
+		forcing = 1;
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
@@ -445,27 +449,20 @@ void create_branch(struct repository *r,
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
-	if (!dont_change_ref) {
-		struct ref_transaction *transaction;
-		struct strbuf err = STRBUF_INIT;
-		char *msg;
-
-		if (forcing)
-			msg = xstrfmt("branch: Reset to %s", start_name);
-		else
-			msg = xstrfmt("branch: Created from %s", start_name);
-
-		transaction = ref_transaction_begin(&err);
-		if (!transaction ||
-		    ref_transaction_update(transaction, ref.buf,
-					   &oid, forcing ? NULL : null_oid(),
-					   0, msg, &err) ||
-		    ref_transaction_commit(transaction, &err))
-			die("%s", err.buf);
-		ref_transaction_free(transaction);
-		strbuf_release(&err);
-		free(msg);
-	}
+	if (forcing)
+		msg = xstrfmt("branch: Reset to %s", start_name);
+	else
+		msg = xstrfmt("branch: Created from %s", start_name);
+	transaction = ref_transaction_begin(&err);
+	if (!transaction ||
+		ref_transaction_update(transaction, ref.buf,
+					&oid, forcing ? NULL : null_oid(),
+					0, msg, &err) ||
+		ref_transaction_commit(transaction, &err))
+		die("%s", err.buf);
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	free(msg);
 
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
diff --git a/branch.h b/branch.h
index ab2315c611..cf3a4d3ff3 100644
--- a/branch.h
+++ b/branch.h
@@ -52,8 +52,8 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *
  *   - force enables overwriting an existing (non-head) branch
  *
- *   - clobber_head_ok allows the currently checked out (hence existing)
- *     branch to be overwritten; without 'force', it has no effect.
+ *   - clobber_head_ok, when enabled with 'force', allows the currently
+ *     checked out (head) branch to be overwritten
  *
  *   - reflog creates a reflog for the branch
  *
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index a74b2e06a1..9157382ad0 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' '
 	git branch abc && test_path_is_file .git/refs/heads/abc
 '
 
+test_expect_success 'git branch abc should fail when abc exists' '
+	test_must_fail git branch abc
+'
+
+test_expect_success 'git branch --force abc should fail when abc is checked out' '
+	test_when_finished git switch main &&
+	git switch abc &&
+	test_must_fail git branch --force abc HEAD~1
+'
+
+test_expect_success 'git branch --force abc should succeed when abc exists' '
+	git rev-parse HEAD~1 >expect &&
+	git branch --force abc HEAD~1 &&
+	git rev-parse abc >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v7 3/6] branch: add a dry_run parameter to create_branch()
  2022-01-24 20:44           ` [PATCH v7 0/6] " Glen Choo
  2022-01-24 20:44             ` [PATCH v7 1/6] branch: move --set-upstream-to behavior to dwim_and_setup_tracking() Glen Choo
  2022-01-24 20:44             ` [PATCH v7 2/6] branch: make create_branch() always create a branch Glen Choo
@ 2022-01-24 20:44             ` Glen Choo
  2022-01-24 20:44             ` [PATCH v7 4/6] builtin/branch: consolidate action-picking logic in cmd_branch() Glen Choo
                               ` (3 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2022-01-24 20:44 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Add a dry_run parameter to create_branch() such that dry_run = 1 will
validate a new branch without trying to create it. This will be used in
`git branch --recurse-submodules` to ensure that the new branch can be
created in all submodules.

Signed-off-by: Glen Choo <chooglen@google.com>
---
 branch.c           | 5 ++++-
 branch.h           | 5 ++++-
 builtin/branch.c   | 2 +-
 builtin/checkout.c | 3 ++-
 4 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/branch.c b/branch.c
index df24021f27..02d46a69b8 100644
--- a/branch.c
+++ b/branch.c
@@ -423,7 +423,7 @@ static void dwim_branch_start(struct repository *r, const char *start_name,
 void create_branch(struct repository *r,
 		   const char *name, const char *start_name,
 		   int force, int clobber_head_ok, int reflog,
-		   int quiet, enum branch_track track)
+		   int quiet, enum branch_track track, int dry_run)
 {
 	struct object_id oid;
 	char *real_ref;
@@ -445,6 +445,8 @@ void create_branch(struct repository *r,
 	}
 
 	dwim_branch_start(r, start_name, track, &real_ref, &oid);
+	if (dry_run)
+		goto cleanup;
 
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
@@ -467,6 +469,7 @@ void create_branch(struct repository *r,
 	if (real_ref && track)
 		setup_tracking(ref.buf + 11, real_ref, track, quiet);
 
+cleanup:
 	strbuf_release(&ref);
 	free(real_ref);
 }
diff --git a/branch.h b/branch.h
index cf3a4d3ff3..509cfcc34e 100644
--- a/branch.h
+++ b/branch.h
@@ -62,11 +62,14 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
  *   - track causes the new branch to be configured to merge the remote branch
  *     that start_name is a tracking branch for (if any).
  *
+ *   - dry_run causes the branch to be validated but not created.
+ *
  */
 void create_branch(struct repository *r,
 		   const char *name, const char *start_name,
 		   int force, int clobber_head_ok,
-		   int reflog, int quiet, enum branch_track track);
+		   int reflog, int quiet, enum branch_track track,
+		   int dry_run);
 
 /*
  * Check if 'name' can be a valid name for a branch; die otherwise.
diff --git a/builtin/branch.c b/builtin/branch.c
index 59768a3f67..d1e3ad6ebf 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -858,7 +858,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 
 		create_branch(the_repository,
 			      argv[0], (argc == 2) ? argv[1] : head,
-			      force, 0, reflog, quiet, track);
+			      force, 0, reflog, quiet, track, 0);
 
 	} else
 		usage_with_options(builtin_branch_usage, options);
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 8d511aa6b7..fb4323dfca 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -893,7 +893,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 				      opts->new_branch_force ? 1 : 0,
 				      opts->new_branch_log,
 				      opts->quiet,
-				      opts->track);
+				      opts->track,
+				      0);
 		new_branch_info->name = opts->new_branch;
 		setup_branch_path(new_branch_info);
 	}
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v7 4/6] builtin/branch: consolidate action-picking logic in cmd_branch()
  2022-01-24 20:44           ` [PATCH v7 0/6] " Glen Choo
                               ` (2 preceding siblings ...)
  2022-01-24 20:44             ` [PATCH v7 3/6] branch: add a dry_run parameter to create_branch() Glen Choo
@ 2022-01-24 20:44             ` Glen Choo
  2022-01-24 20:44             ` [PATCH v7 5/6] branch: add --recurse-submodules option for branch creation Glen Choo
                               ` (2 subsequent siblings)
  6 siblings, 0 replies; 110+ messages in thread
From: Glen Choo @ 2022-01-24 20:44 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

Consolidate the logic for deciding when to create a new branch in
cmd_branch(), and save the result for reuse. Besides making the function
more explicit, this allows us to validate options that can only be used
when creating a branch. Such an option does not exist yet, but one will
be introduced in a subsequent commit.

Helped-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Glen Choo <chooglen@google.com>
---
 builtin/branch.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index d1e3ad6ebf..c274fbdfcf 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -616,14 +616,15 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, list = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	const char *new_upstream = NULL;
+	int noncreate_actions = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, icase = 0, force = 0;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
 	struct ref_format format = REF_FORMAT_INIT;
 
@@ -707,8 +708,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
+			    !!show_current + !!list + !!edit_description +
+			    !!unset_upstream;
+	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
 	if (filter.abbrev == -1)
@@ -848,7 +851,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
 		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>?"));
-- 
2.33.GIT


^ permalink raw reply	[flat|nested] 110+ messages in thread

* [PATCH v7 5/6] branch: add --recurse-submodules option for branch creation
  2022-01-24 20:44           ` [PATCH v7 0/6] " Glen Choo
                               ` (3 preceding siblings ...)
  2022-01-24 20:44             ` [PATCH v7 4/6] builtin/branch: consolidate action-picking logic in cmd_branch() Glen Choo
@ 2022-01-24 20:44             ` Glen Choo
  2022-01-27 20:29               ` Jonathan Tan
  2022-01-24 20:44             ` [PATCH v7 6/6] branch.c: use 'goto cleanup' in setup_tracking() to fix memory leaks Glen Choo
  2022-01-29  0:04             ` [PATCH v8 0/6] implement branch --recurse-submodules Glen Choo
  6 siblings, 1 reply; 110+ messages in thread
From: Glen Choo @ 2022-01-24 20:44 UTC (permalink / raw)
  To: git
  Cc: Glen Choo, Jonathan Tan, Josh Steadmon, Emily Shaffer,
	Ævar Arnfjörð Bjarmason, Philippe Blain,
	Junio C Hamano

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

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,