Skip to content

Commit 961b130

Browse files
chooglengitster
authored andcommitted
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 submodules. Although this commit does not introduce breaking changes, it does not work well 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, a8dfd5e (Make builtin-branch.c use parse_options., 2007-10-07). Helped-by: Jonathan Tan <[email protected]> Signed-off-by: Glen Choo <[email protected]> Reviewed-by: Jonathan Tan <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 6e0a2ca commit 961b130

14 files changed

+694
-20
lines changed

Documentation/config/advice.txt

+3
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ advice.*::
116116
submoduleAlternateErrorStrategyDie::
117117
Advice shown when a submodule.alternateErrorStrategy option
118118
configured to "die" causes a fatal error.
119+
submodulesNotUpdated::
120+
Advice shown when a user runs a submodule command that fails
121+
because `git submodule update --init` was not run.
119122
addIgnoredFile::
120123
Advice shown if a user attempts to add an ignored file to
121124
the index.

Documentation/config/submodule.txt

+26-11
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,33 @@ submodule.active::
5959

6060
submodule.recurse::
6161
A boolean indicating if commands should enable the `--recurse-submodules`
62-
option by default.
63-
Applies to all commands that support this option
64-
(`checkout`, `fetch`, `grep`, `pull`, `push`, `read-tree`, `reset`,
65-
`restore` and `switch`) except `clone` and `ls-files`.
62+
option by default. Defaults to false.
63+
+
64+
When set to true, it can be deactivated via the
65+
`--no-recurse-submodules` option. Note that some Git commands
66+
lacking this option may call some of the above commands affected by
67+
`submodule.recurse`; for instance `git remote update` will call
68+
`git fetch` but does not have a `--no-recurse-submodules` option.
69+
For these commands a workaround is to temporarily change the
70+
configuration value by using `git -c submodule.recurse=0`.
71+
+
72+
The following list shows the commands that accept
73+
`--recurse-submodules` and whether they are supported by this
74+
setting.
75+
76+
* `checkout`, `fetch`, `grep`, `pull`, `push`, `read-tree`,
77+
`reset`, `restore` and `switch` are always supported.
78+
* `clone` and `ls-files` are not supported.
79+
* `branch` is supported only if `submodule.propagateBranches` is
80+
enabled
81+
82+
submodule.propagateBranches::
83+
[EXPERIMENTAL] A boolean that enables branching support when
84+
using `--recurse-submodules` or `submodule.recurse=true`.
85+
Enabling this will allow certain commands to accept
86+
`--recurse-submodules` and certain commands that already accept
87+
`--recurse-submodules` will now consider branches.
6688
Defaults to false.
67-
When set to true, it can be deactivated via the
68-
`--no-recurse-submodules` option. Note that some Git commands
69-
lacking this option may call some of the above commands affected by
70-
`submodule.recurse`; for instance `git remote update` will call
71-
`git fetch` but does not have a `--no-recurse-submodules` option.
72-
For these commands a workaround is to temporarily change the
73-
configuration value by using `git -c submodule.recurse=0`.
7489

7590
submodule.fetchJobs::
7691
Specifies how many submodules are fetched/cloned at the same time.

Documentation/git-branch.txt

+18-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ SYNOPSIS
1616
[--points-at <object>] [--format=<format>]
1717
[(-r | --remotes) | (-a | --all)]
1818
[--list] [<pattern>...]
19-
'git branch' [--track[=(direct|inherit)] | --no-track] [-f] <branchname> [<start-point>]
19+
'git branch' [--track[=(direct|inherit)] | --no-track] [-f]
20+
[--recurse-submodules] <branchname> [<start-point>]
2021
'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
2122
'git branch' --unset-upstream [<branchname>]
2223
'git branch' (-m | -M) [<oldbranch>] <newbranch>
@@ -235,6 +236,22 @@ how the `branch.<name>.remote` and `branch.<name>.merge` options are used.
235236
Do not set up "upstream" configuration, even if the
236237
branch.autoSetupMerge configuration variable is set.
237238

239+
--recurse-submodules::
240+
THIS OPTION IS EXPERIMENTAL! Causes the current command to
241+
recurse into submodules if `submodule.propagateBranches` is
242+
enabled. See `submodule.propagateBranches` in
243+
linkgit:git-config[1]. Currently, only branch creation is
244+
supported.
245+
+
246+
When used in branch creation, a new branch <branchname> will be created
247+
in the superproject and all of the submodules in the superproject's
248+
<start-point>. In submodules, the branch will point to the submodule
249+
commit in the superproject's <start-point> but the branch's tracking
250+
information will be set up based on the submodule's branches and remotes
251+
e.g. `git branch --recurse-submodules topic origin/main` will create the
252+
submodule branch "topic" that points to the submodule commit in the
253+
superproject's "origin/main", but tracks the submodule's "origin/main".
254+
238255
--set-upstream::
239256
As this option had confusing syntax, it is no longer supported.
240257
Please use `--track` or `--set-upstream-to` instead.

advice.c

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ static struct {
7070
[ADVICE_STATUS_HINTS] = { "statusHints", 1 },
7171
[ADVICE_STATUS_U_OPTION] = { "statusUoption", 1 },
7272
[ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE] = { "submoduleAlternateErrorStrategyDie", 1 },
73+
[ADVICE_SUBMODULES_NOT_UPDATED] = { "submodulesNotUpdated", 1 },
7374
[ADVICE_UPDATE_SPARSE_PATH] = { "updateSparsePath", 1 },
7475
[ADVICE_WAITING_FOR_EDITOR] = { "waitingForEditor", 1 },
7576
};

advice.h

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ struct string_list;
4444
ADVICE_STATUS_HINTS,
4545
ADVICE_STATUS_U_OPTION,
4646
ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE,
47+
ADVICE_SUBMODULES_NOT_UPDATED,
4748
ADVICE_UPDATE_SPARSE_PATH,
4849
ADVICE_WAITING_FOR_EDITOR,
4950
ADVICE_SKIPPED_CHERRY_PICKS,

branch.c

+141
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
#include "sequencer.h"
99
#include "commit.h"
1010
#include "worktree.h"
11+
#include "submodule-config.h"
12+
#include "run-command.h"
1113

1214
struct tracking {
1315
struct refspec_item spec;
@@ -483,6 +485,145 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
483485
setup_tracking(new_ref, real_orig_ref, track, quiet);
484486
}
485487

488+
/**
489+
* Creates a branch in a submodule by calling
490+
* create_branches_recursively() in a child process. The child process
491+
* is necessary because install_branch_config_multiple_remotes() (which
492+
* is called by setup_tracking()) does not support writing configs to
493+
* submodules.
494+
*/
495+
static int submodule_create_branch(struct repository *r,
496+
const struct submodule *submodule,
497+
const char *name, const char *start_oid,
498+
const char *tracking_name, int force,
499+
int reflog, int quiet,
500+
enum branch_track track, int dry_run)
501+
{
502+
int ret = 0;
503+
struct child_process child = CHILD_PROCESS_INIT;
504+
struct strbuf child_err = STRBUF_INIT;
505+
struct strbuf out_buf = STRBUF_INIT;
506+
char *out_prefix = xstrfmt("submodule '%s': ", submodule->name);
507+
child.git_cmd = 1;
508+
child.err = -1;
509+
child.stdout_to_stderr = 1;
510+
511+
prepare_other_repo_env(&child.env_array, r->gitdir);
512+
/*
513+
* submodule_create_branch() is indirectly invoked by "git
514+
* branch", but we cannot invoke "git branch" in the child
515+
* process. "git branch" accepts a branch name and start point,
516+
* where the start point is assumed to provide both the OID
517+
* (start_oid) and the branch to use for tracking
518+
* (tracking_name). But when recursing through submodules,
519+
* start_oid and tracking name need to be specified separately
520+
* (see create_branches_recursively()).
521+
*/
522+
strvec_pushl(&child.args, "submodule--helper", "create-branch", NULL);
523+
if (dry_run)
524+
strvec_push(&child.args, "--dry-run");
525+
if (force)
526+
strvec_push(&child.args, "--force");
527+
if (quiet)
528+
strvec_push(&child.args, "--quiet");
529+
if (reflog)
530+
strvec_push(&child.args, "--create-reflog");
531+
if (track == BRANCH_TRACK_ALWAYS || track == BRANCH_TRACK_EXPLICIT)
532+
strvec_push(&child.args, "--track");
533+
534+
strvec_pushl(&child.args, name, start_oid, tracking_name, NULL);
535+
536+
if ((ret = start_command(&child)))
537+
return ret;
538+
ret = finish_command(&child);
539+
strbuf_read(&child_err, child.err, 0);
540+
strbuf_add_lines(&out_buf, out_prefix, child_err.buf, child_err.len);
541+
542+
if (ret)
543+
fprintf(stderr, "%s", out_buf.buf);
544+
else
545+
printf("%s", out_buf.buf);
546+
547+
strbuf_release(&child_err);
548+
strbuf_release(&out_buf);
549+
return ret;
550+
}
551+
552+
void create_branches_recursively(struct repository *r, const char *name,
553+
const char *start_commitish,
554+
const char *tracking_name, int force,
555+
int reflog, int quiet, enum branch_track track,
556+
int dry_run)
557+
{
558+
int i = 0;
559+
char *branch_point = NULL;
560+
struct object_id super_oid;
561+
struct submodule_entry_list submodule_entry_list;
562+
563+
/* Perform dwim on start_commitish to get super_oid and branch_point. */
564+
dwim_branch_start(r, start_commitish, BRANCH_TRACK_NEVER,
565+
&branch_point, &super_oid);
566+
567+
/*
568+
* If we were not given an explicit name to track, then assume we are at
569+
* the top level and, just like the non-recursive case, the tracking
570+
* name is the branch point.
571+
*/
572+
if (!tracking_name)
573+
tracking_name = branch_point;
574+
575+
submodules_of_tree(r, &super_oid, &submodule_entry_list);
576+
/*
577+
* Before creating any branches, first check that the branch can
578+
* be created in every submodule.
579+
*/
580+
for (i = 0; i < submodule_entry_list.entry_nr; i++) {
581+
if (submodule_entry_list.entries[i].repo == NULL) {
582+
if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED))
583+
advise(_("You may try updating the submodules using 'git checkout %s && git submodule update --init'"),
584+
start_commitish);
585+
die(_("submodule '%s': unable to find submodule"),
586+
submodule_entry_list.entries[i].submodule->name);
587+
}
588+
589+
if (submodule_create_branch(
590+
submodule_entry_list.entries[i].repo,
591+
submodule_entry_list.entries[i].submodule, name,
592+
oid_to_hex(&submodule_entry_list.entries[i]
593+
.name_entry->oid),
594+
tracking_name, force, reflog, quiet, track, 1))
595+
die(_("submodule '%s': cannot create branch '%s'"),
596+
submodule_entry_list.entries[i].submodule->name,
597+
name);
598+
}
599+
600+
create_branch(the_repository, name, start_commitish, force, 0, reflog, quiet,
601+
BRANCH_TRACK_NEVER, dry_run);
602+
if (dry_run)
603+
return;
604+
/*
605+
* NEEDSWORK If tracking was set up in the superproject but not the
606+
* submodule, users might expect "git branch --recurse-submodules" to
607+
* fail or give a warning, but this is not yet implemented because it is
608+
* tedious to determine whether or not tracking was set up in the
609+
* superproject.
610+
*/
611+
setup_tracking(name, tracking_name, track, quiet);
612+
613+
for (i = 0; i < submodule_entry_list.entry_nr; i++) {
614+
if (submodule_create_branch(
615+
submodule_entry_list.entries[i].repo,
616+
submodule_entry_list.entries[i].submodule, name,
617+
oid_to_hex(&submodule_entry_list.entries[i]
618+
.name_entry->oid),
619+
tracking_name, force, reflog, quiet, track, 0))
620+
die(_("submodule '%s': cannot create branch '%s'"),
621+
submodule_entry_list.entries[i].submodule->name,
622+
name);
623+
repo_clear(submodule_entry_list.entries[i].repo);
624+
}
625+
}
626+
486627
void remove_merge_branch_state(struct repository *r)
487628
{
488629
unlink(git_path_merge_head(r));

branch.h

+29
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,35 @@ void create_branch(struct repository *r,
7171
int reflog, int quiet, enum branch_track track,
7272
int dry_run);
7373

74+
/*
75+
* Creates a new branch in a repository and its submodules (and its
76+
* submodules, recursively). The parameters are mostly analogous to
77+
* those of create_branch() except for start_name, which is represented
78+
* by two different parameters:
79+
*
80+
* - start_commitish is the commit-ish, in repository r, that determines
81+
* which commits the branches will point to. The superproject branch
82+
* will point to the commit of start_commitish and the submodule
83+
* branches will point to the gitlink commit oids in start_commitish's
84+
* tree.
85+
*
86+
* - tracking_name is the name of the ref, in repository r, that will be
87+
* used to set up tracking information. This value is propagated to
88+
* all submodules, which will evaluate the ref using their own ref
89+
* stores. If NULL, this defaults to start_commitish.
90+
*
91+
* When this function is called on the superproject, start_commitish
92+
* can be any user-provided ref and tracking_name can be NULL (similar
93+
* to create_branches()). But when recursing through submodules,
94+
* start_commitish is the plain gitlink commit oid. Since the oid cannot
95+
* be used for tracking information, tracking_name is propagated and
96+
* used for tracking instead.
97+
*/
98+
void create_branches_recursively(struct repository *r, const char *name,
99+
const char *start_commitish,
100+
const char *tracking_name, int force,
101+
int reflog, int quiet, enum branch_track track,
102+
int dry_run);
74103
/*
75104
* Check if 'name' can be a valid name for a branch; die otherwise.
76105
* Return 1 if the named branch already exists; return 0 otherwise.

builtin/branch.c

+38-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727

2828
static const char * const builtin_branch_usage[] = {
2929
N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
30-
N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
30+
N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"),
31+
N_("git branch [<options>] [-l] [<pattern>...]"),
3132
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
3233
N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
3334
N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
@@ -38,6 +39,8 @@ static const char * const builtin_branch_usage[] = {
3839

3940
static const char *head;
4041
static struct object_id head_oid;
42+
static int recurse_submodules = 0;
43+
static int submodule_propagate_branches = 0;
4144

4245
static int branch_use_color = -1;
4346
static char branch_colors[][COLOR_MAXLEN] = {
@@ -99,6 +102,15 @@ static int git_branch_config(const char *var, const char *value, void *cb)
99102
return config_error_nonbool(var);
100103
return color_parse(value, branch_colors[slot]);
101104
}
105+
if (!strcmp(var, "submodule.recurse")) {
106+
recurse_submodules = git_config_bool(var, value);
107+
return 0;
108+
}
109+
if (!strcasecmp(var, "submodule.propagateBranches")) {
110+
submodule_propagate_branches = git_config_bool(var, value);
111+
return 0;
112+
}
113+
102114
return git_color_default_config(var, value, cb);
103115
}
104116

@@ -622,7 +634,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
622634
const char *new_upstream = NULL;
623635
int noncreate_actions = 0;
624636
/* possible options */
625-
int reflog = 0, quiet = 0, icase = 0, force = 0;
637+
int reflog = 0, quiet = 0, icase = 0, force = 0,
638+
recurse_submodules_explicit = 0;
626639
enum branch_track track;
627640
struct ref_filter filter;
628641
static struct ref_sorting *sorting;
@@ -673,6 +686,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
673686
OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
674687
N_("print only branches of the object"), parse_opt_object_name),
675688
OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
689+
OPT_BOOL(0, "recurse-submodules", &recurse_submodules_explicit, N_("recurse through submodules")),
676690
OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")),
677691
OPT_END(),
678692
};
@@ -715,6 +729,17 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
715729
if (noncreate_actions > 1)
716730
usage_with_options(builtin_branch_usage, options);
717731

732+
if (recurse_submodules_explicit) {
733+
if (!submodule_propagate_branches)
734+
die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
735+
if (noncreate_actions)
736+
die(_("--recurse-submodules can only be used to create branches"));
737+
}
738+
739+
recurse_submodules =
740+
(recurse_submodules || recurse_submodules_explicit) &&
741+
submodule_propagate_branches;
742+
718743
if (filter.abbrev == -1)
719744
filter.abbrev = DEFAULT_ABBREV;
720745
filter.ignore_case = icase;
@@ -853,17 +878,24 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
853878
git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
854879
strbuf_release(&buf);
855880
} else if (!noncreate_actions && argc > 0 && argc <= 2) {
881+
const char *branch_name = argv[0];
882+
const char *start_name = argc == 2 ? argv[1] : head;
883+
856884
if (filter.kind != FILTER_REFS_BRANCHES)
857885
die(_("The -a, and -r, options to 'git branch' do not take a branch name.\n"
858886
"Did you mean to use: -a|-r --list <pattern>?"));
859887

860888
if (track == BRANCH_TRACK_OVERRIDE)
861889
die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead."));
862890

863-
create_branch(the_repository,
864-
argv[0], (argc == 2) ? argv[1] : head,
865-
force, 0, reflog, quiet, track, 0);
866-
891+
if (recurse_submodules) {
892+
create_branches_recursively(the_repository, branch_name,
893+
start_name, NULL, force,
894+
reflog, quiet, track, 0);
895+
return 0;
896+
}
897+
create_branch(the_repository, branch_name, start_name, force, 0,
898+
reflog, quiet, track, 0);
867899
} else
868900
usage_with_options(builtin_branch_usage, options);
869901

0 commit comments

Comments
 (0)