Submodule branching RFC: https://lore.kernel.org/git/kl6lv912uvjv.fsf@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/ Original Submodule UX RFC/Discussion: https://lore.kernel.org/git/YHofmWcIAidkvJiD@xxxxxxxxxx/ Contributor Summit submodules Notes: https://lore.kernel.org/git/nycvar.QRO.7.76.6.2110211148060.56@xxxxxxxxxxxxxxxxx/ 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@xxxxxxxxxx [2] https://lore.kernel.org/git/211123.86zgpvup6m.gmgdl@xxxxxxxxxxxxxxxxxxx [3] https://lore.kernel.org/git/211123.86r1b7uoil.gmgdl@xxxxxxxxxxxxxxxxxxx [4] https://lore.kernel.org/git/3ad3941c-de18-41bf-2e44-4238ae868d79@xxxxxxxxx [5] https://lore.kernel.org/git/211123.86v90juovj.gmgdl@xxxxxxxxxxxxxxxxxxx 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@xxxxxxxxxx> ## 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@xxxxxxxxxx> @@ 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@xxxxxxxxxx> @@ 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