Skip to content

Commit

Permalink
branch: add --recurse-submodules option for branch creation
Browse files Browse the repository at this point in the history
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 <jonathantanmy@google.com>
Signed-off-by: Glen Choo <chooglen@google.com>
Reviewed-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
chooglen authored and gitster committed Feb 4, 2022
1 parent 6e0a2ca commit 961b130
Show file tree
Hide file tree
Showing 14 changed files with 694 additions and 20 deletions.
3 changes: 3 additions & 0 deletions Documentation/config/advice.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 26 additions & 11 deletions Documentation/config/submodule.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 18 additions & 1 deletion Documentation/git-branch.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -235,6 +236,22 @@ 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.
+
When used in branch creation, a new branch <branchname> will be created
in the superproject and all of the submodules in the superproject's
<start-point>. In submodules, the branch will point to the submodule
commit in the superproject's <start-point> but the branch's tracking
information will be set up based on the submodule's branches and remotes
e.g. `git branch --recurse-submodules topic origin/main` will create the
submodule branch "topic" that points to the submodule commit in the
superproject's "origin/main", but tracks the submodule's "origin/main".

--set-upstream::
As this option had confusing syntax, it is no longer supported.
Please use `--track` or `--set-upstream-to` instead.
Expand Down
1 change: 1 addition & 0 deletions advice.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
};
Expand Down
1 change: 1 addition & 0 deletions advice.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
141 changes: 141 additions & 0 deletions branch.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -483,6 +485,145 @@ 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 *tracking_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. "git branch" accepts a branch name and start point,
* where the start point is assumed to provide both the OID
* (start_oid) and the branch to use for tracking
* (tracking_name). But when recursing through submodules,
* start_oid and tracking name need to be specified 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, tracking_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_commitish,
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_commitish to get super_oid and branch_point. */
dwim_branch_start(r, start_commitish, 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_commitish);
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_commitish, 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));
Expand Down
29 changes: 29 additions & 0 deletions branch.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,35 @@ void create_branch(struct repository *r,
int reflog, int quiet, enum branch_track track,
int dry_run);

/*
* Creates a new branch in a repository and its submodules (and its
* submodules, recursively). The parameters are mostly analogous to
* those of create_branch() except for start_name, which is represented
* by two different parameters:
*
* - start_commitish is the commit-ish, in repository r, that determines
* which commits the branches will point to. The superproject branch
* will point to the commit of start_commitish and the submodule
* branches will point to the gitlink commit oids in start_commitish's
* tree.
*
* - tracking_name is the name of the ref, in repository r, that will be
* used to set up tracking information. This value is propagated to
* all submodules, which will evaluate the ref using their own ref
* stores. If NULL, this defaults to start_commitish.
*
* When this function is called on the superproject, start_commitish
* can be any user-provided ref and tracking_name can be NULL (similar
* to create_branches()). But when recursing through submodules,
* start_commitish is the plain gitlink commit oid. Since the oid cannot
* be used for tracking information, tracking_name is propagated and
* used for tracking instead.
*/
void create_branches_recursively(struct repository *r, const char *name,
const char *start_commitish,
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.
Expand Down
44 changes: 38 additions & 6 deletions builtin/branch.c
Original file line number Diff line number Diff line change
Expand Up @@ -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>"),
Expand All @@ -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] = {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -853,17 +878,24 @@ 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>?"));

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

Expand Down

0 comments on commit 961b130

Please sign in to comment.