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, 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> Reviewed-by: Jonathan Tan <jonathantanmy@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Glen Choo committed Jan 28, 2022 at 16:04 UTC 961b130d20c9aea322b94a639a63ec8cca9f14fc
14 files changed +694 -20
Documentation/config/advice.txt
+3
@@ -116,6 +116,9 @@ advice.*::
116 submoduleAlternateErrorStrategyDie::
117 Advice shown when a submodule.alternateErrorStrategy option
118 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.
122 addIgnoredFile::
123 Advice shown if a user attempts to add an ignored file to
124 the index.
Documentation/config/submodule.txt
+26 -11
@@ -59,18 +59,33 @@ submodule.active::
59
60 submodule.recurse::
61 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.
88 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`.
89
90 submodule.fetchJobs::
91 Specifies how many submodules are fetched/cloned at the same time.
Documentation/git-branch.txt
+18 -1
@@ -16,7 +16,8 @@ SYNOPSIS
16 [--points-at <object>] [--format=<format>]
17 [(-r | --remotes) | (-a | --all)]
18 [--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>]
21 'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
22 'git branch' --unset-upstream [<branchname>]
23 'git branch' (-m | -M) [<oldbranch>] <newbranch>
@@ -235,6 +236,22 @@ how the `branch.<name>.remote` and `branch.<name>.merge` options are used.
236 Do not set up "upstream" configuration, even if the
237 branch.autoSetupMerge configuration variable is set.
238
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 +
255 --set-upstream::
256 As this option had confusing syntax, it is no longer supported.
257 Please use `--track` or `--set-upstream-to` instead.
advice.c
+1
@@ -70,6 +70,7 @@ static struct {
70 [ADVICE_STATUS_HINTS] = { "statusHints", 1 },
71 [ADVICE_STATUS_U_OPTION] = { "statusUoption", 1 },
72 [ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE] = { "submoduleAlternateErrorStrategyDie", 1 },
73 + [ADVICE_SUBMODULES_NOT_UPDATED] = { "submodulesNotUpdated", 1 },
74 [ADVICE_UPDATE_SPARSE_PATH] = { "updateSparsePath", 1 },
75 [ADVICE_WAITING_FOR_EDITOR] = { "waitingForEditor", 1 },
76 };
advice.h
+1
@@ -44,6 +44,7 @@ struct string_list;
44 ADVICE_STATUS_HINTS,
45 ADVICE_STATUS_U_OPTION,
46 ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE,
47 + ADVICE_SUBMODULES_NOT_UPDATED,
48 ADVICE_UPDATE_SPARSE_PATH,
49 ADVICE_WAITING_FOR_EDITOR,
50 ADVICE_SKIPPED_CHERRY_PICKS,
branch.c
+141
@@ -8,6 +8,8 @@
8 #include "sequencer.h"
9 #include "commit.h"
10 #include "worktree.h"
11 +#include "submodule-config.h"
12 +#include "run-command.h"
13
14 struct tracking {
15 struct refspec_item spec;
@@ -483,6 +485,145 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,
485 setup_tracking(new_ref, real_orig_ref, track, quiet);
486 }
487
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 +
627 void remove_merge_branch_state(struct repository *r)
628 {
629 unlink(git_path_merge_head(r));
branch.h
+29
@@ -71,6 +71,35 @@ void create_branch(struct repository *r,
71 int reflog, int quiet, enum branch_track track,
72 int dry_run);
73
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);
103 /*
104 * Check if 'name' can be a valid name for a branch; die otherwise.
105 * Return 1 if the named branch already exists; return 0 otherwise.
builtin/branch.c
+38 -6
@@ -27,7 +27,8 @@
27
28 static const char * const builtin_branch_usage[] = {
29 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>...]"),
32 N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
33 N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
34 N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
@@ -38,6 +39,8 @@ static const char * const builtin_branch_usage[] = {
39
40 static const char *head;
41 static struct object_id head_oid;
42 +static int recurse_submodules = 0;
43 +static int submodule_propagate_branches = 0;
44
45 static int branch_use_color = -1;
46 static char branch_colors[][COLOR_MAXLEN] = {
@@ -99,6 +102,15 @@ static int git_branch_config(const char *var, const char *value, void *cb)
102 return config_error_nonbool(var);
103 return color_parse(value, branch_colors[slot]);
104 }
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 +
114 return git_color_default_config(var, value, cb);
115 }
116
@@ -622,7 +634,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
634 const char *new_upstream = NULL;
635 int noncreate_actions = 0;
636 /* 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;
639 enum branch_track track;
640 struct ref_filter filter;
641 static struct ref_sorting *sorting;
@@ -673,6 +686,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
686 OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
687 N_("print only branches of the object"), parse_opt_object_name),
688 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")),
690 OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")),
691 OPT_END(),
692 };
@@ -715,6 +729,17 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
729 if (noncreate_actions > 1)
730 usage_with_options(builtin_branch_usage, options);
731
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 +
743 if (filter.abbrev == -1)
744 filter.abbrev = DEFAULT_ABBREV;
745 filter.ignore_case = icase;
@@ -853,6 +878,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
878 git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
879 strbuf_release(&buf);
880 } 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 +
884 if (filter.kind != FILTER_REFS_BRANCHES)
885 die(_("The -a, and -r, options to 'git branch' do not take a branch name.\n"
886 "Did you mean to use: -a|-r --list <pattern>?"));
@@ -860,10 +888,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
888 if (track == BRANCH_TRACK_OVERRIDE)
889 die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead."));
890
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);
899 } else
900 usage_with_options(builtin_branch_usage, options);
901
builtin/submodule--helper.c
+38
@@ -20,6 +20,7 @@
20 #include "diff.h"
21 #include "object-store.h"
22 #include "advice.h"
23 +#include "branch.h"
24
25 #define OPT_QUIET (1 << 0)
26 #define OPT_CACHED (1 << 1)
@@ -2983,6 +2984,42 @@ static int module_set_branch(int argc, const char **argv, const char *prefix)
2984 return !!ret;
2985 }
2986
2987 +static int module_create_branch(int argc, const char **argv, const char *prefix)
2988 +{
2989 + enum branch_track track;
2990 + int quiet = 0, force = 0, reflog = 0, dry_run = 0;
2991 +
2992 + struct option options[] = {
2993 + OPT__QUIET(&quiet, N_("print only error messages")),
2994 + OPT__FORCE(&force, N_("force creation"), 0),
2995 + OPT_BOOL(0, "create-reflog", &reflog,
2996 + N_("create the branch's reflog")),
2997 + OPT_SET_INT('t', "track", &track,
2998 + N_("set up tracking mode (see git-pull(1))"),
2999 + BRANCH_TRACK_EXPLICIT),
3000 + OPT__DRY_RUN(&dry_run,
3001 + N_("show whether the branch would be created")),
3002 + OPT_END()
3003 + };
3004 + const char *const usage[] = {
3005 + N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] [-n|--dry-run] <name> <start_oid> <start_name>"),
3006 + NULL
3007 + };
3008 +
3009 + git_config(git_default_config, NULL);
3010 + track = git_branch_track;
3011 + argc = parse_options(argc, argv, prefix, options, usage, 0);
3012 +
3013 + if (argc != 3)
3014 + usage_with_options(usage, options);
3015 +
3016 + if (!quiet && !dry_run)
3017 + printf_ln(_("creating branch '%s'"), argv[0]);
3018 +
3019 + create_branches_recursively(the_repository, argv[0], argv[1], argv[2],
3020 + force, reflog, quiet, track, dry_run);
3021 + return 0;
3022 +}
3023 struct add_data {
3024 const char *prefix;
3025 const char *branch;
@@ -3389,6 +3426,7 @@ static struct cmd_struct commands[] = {
3426 {"config", module_config, 0},
3427 {"set-url", module_set_url, 0},
3428 {"set-branch", module_set_branch, 0},
3429 + {"create-branch", module_create_branch, 0},
3430 };
3431
3432 int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
submodule-config.c
+61
@@ -7,6 +7,7 @@
7 #include "strbuf.h"
8 #include "object-store.h"
9 #include "parse-options.h"
10 +#include "tree-walk.h"
11
12 /*
13 * submodule cache lookup structure
@@ -726,6 +727,66 @@ const struct submodule *submodule_from_path(struct repository *r,
727 return config_from(r->submodule_cache, treeish_name, path, lookup_path);
728 }
729
730 +/**
731 + * Used internally by submodules_of_tree(). Recurses into 'treeish_name'
732 + * and appends submodule entries to 'out'. The submodule_cache expects
733 + * a root-level treeish_name and paths, so keep track of these values
734 + * with 'root_tree' and 'prefix'.
735 + */
736 +static void traverse_tree_submodules(struct repository *r,
737 + const struct object_id *root_tree,
738 + char *prefix,
739 + const struct object_id *treeish_name,
740 + struct submodule_entry_list *out)
741 +{
742 + struct tree_desc tree;
743 + struct submodule_tree_entry *st_entry;
744 + struct name_entry *name_entry;
745 + char *tree_path = NULL;
746 +
747 + name_entry = xmalloc(sizeof(*name_entry));
748 +
749 + fill_tree_descriptor(r, &tree, treeish_name);
750 + while (tree_entry(&tree, name_entry)) {
751 + if (prefix)
752 + tree_path =
753 + mkpathdup("%s/%s", prefix, name_entry->path);
754 + else
755 + tree_path = xstrdup(name_entry->path);
756 +
757 + if (S_ISGITLINK(name_entry->mode) &&
758 + is_tree_submodule_active(r, root_tree, tree_path)) {
759 + st_entry = xmalloc(sizeof(*st_entry));
760 + st_entry->name_entry = xmalloc(sizeof(*st_entry->name_entry));
761 + *st_entry->name_entry = *name_entry;
762 + st_entry->submodule =
763 + submodule_from_path(r, root_tree, tree_path);
764 + st_entry->repo = xmalloc(sizeof(*st_entry->repo));
765 + if (repo_submodule_init(st_entry->repo, r, tree_path,
766 + root_tree))
767 + FREE_AND_NULL(st_entry->repo);
768 +
769 + ALLOC_GROW(out->entries, out->entry_nr + 1,
770 + out->entry_alloc);
771 + out->entries[out->entry_nr++] = *st_entry;
772 + } else if (S_ISDIR(name_entry->mode))
773 + traverse_tree_submodules(r, root_tree, tree_path,
774 + &name_entry->oid, out);
775 + free(tree_path);
776 + }
777 +}
778 +
779 +void submodules_of_tree(struct repository *r,
780 + const struct object_id *treeish_name,
781 + struct submodule_entry_list *out)
782 +{
783 + CALLOC_ARRAY(out->entries, 0);
784 + out->entry_nr = 0;
785 + out->entry_alloc = 0;
786 +
787 + traverse_tree_submodules(r, treeish_name, NULL, treeish_name, out);
788 +}
789 +
790 void submodule_free(struct repository *r)
791 {
792 if (r->submodule_cache)
submodule-config.h
+34
@@ -6,6 +6,7 @@
6 #include "hashmap.h"
7 #include "submodule.h"
8 #include "strbuf.h"
9 +#include "tree-walk.h"
10
11 /**
12 * The submodule config cache API allows to read submodule
@@ -101,4 +102,37 @@ int check_submodule_name(const char *name);
102 void fetch_config_from_gitmodules(int *max_children, int *recurse_submodules);
103 void update_clone_config_from_gitmodules(int *max_jobs);
104
105 +/*
106 + * Submodule entry that contains relevant information about a
107 + * submodule in a tree.
108 + */
109 +struct submodule_tree_entry {
110 + /* The submodule's tree entry. */
111 + struct name_entry *name_entry;
112 + /*
113 + * A struct repository corresponding to the submodule. May be
114 + * NULL if the submodule has not been updated.
115 + */
116 + struct repository *repo;
117 + /*
118 + * A struct submodule containing the submodule config in the
119 + * tree's .gitmodules.
120 + */
121 + const struct submodule *submodule;
122 +};
123 +
124 +struct submodule_entry_list {
125 + struct submodule_tree_entry *entries;
126 + int entry_nr;
127 + int entry_alloc;
128 +};
129 +
130 +/**
131 + * Given a treeish, return all submodules in the tree and its subtrees,
132 + * but excluding nested submodules. Callers that require nested
133 + * submodules are expected to recurse into the submodules themselves.
134 + */
135 +void submodules_of_tree(struct repository *r,
136 + const struct object_id *treeish_name,
137 + struct submodule_entry_list *ret);
138 #endif /* SUBMODULE_CONFIG_H */
submodule.c
+9 -2
@@ -267,7 +267,9 @@ int option_parse_recurse_submodules_worktree_updater(const struct option *opt,
267 * ie, the config looks like: "[submodule] active\n".
268 * Since that is an invalid pathspec, we should inform the user.
269 */
270 -int is_submodule_active(struct repository *repo, const char *path)
270 +int is_tree_submodule_active(struct repository *repo,
271 + const struct object_id *treeish_name,
272 + const char *path)
273 {
274 int ret = 0;
275 char *key = NULL;
@@ -275,7 +277,7 @@ int is_submodule_active(struct repository *repo, const char *path)
277 const struct string_list *sl;
278 const struct submodule *module;
279
278 - module = submodule_from_path(repo, null_oid(), path);
280 + module = submodule_from_path(repo, treeish_name, path);
281
282 /* early return if there isn't a path->module mapping */
283 if (!module)
@@ -317,6 +319,11 @@ int is_submodule_active(struct repository *repo, const char *path)
319 return ret;
320 }
321
322 +int is_submodule_active(struct repository *repo, const char *path)
323 +{
324 + return is_tree_submodule_active(repo, null_oid(), path);
325 +}
326 +
327 int is_submodule_populated_gently(const char *path, int *return_error_code)
328 {
329 int ret = 0;
submodule.h
+3
@@ -54,6 +54,9 @@ int git_default_submodule_config(const char *var, const char *value, void *cb);
54 struct option;
55 int option_parse_recurse_submodules_worktree_updater(const struct option *opt,
56 const char *arg, int unset);
57 +int is_tree_submodule_active(struct repository *repo,
58 + const struct object_id *treeish_name,
59 + const char *path);
60 int is_submodule_active(struct repository *repo, const char *path);
61 /*
62 * Determine if a submodule has been populated at a given 'path' by checking if
t/t3207-branch-submodule.sh new
+292
@@ -0,0 +1,292 @@
1 +#!/bin/sh
2 +
3 +test_description='git branch submodule tests'
4 +
5 +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
6 +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
7 +
8 +. ./test-lib.sh
9 +. "$TEST_DIRECTORY"/lib-rebase.sh
10 +
11 +pwd=$(pwd)
12 +
13 +# Creates a clean test environment in "pwd" by copying the repo setup
14 +# from test_dirs.
15 +reset_test () {
16 + rm -fr super &&
17 + rm -fr sub-sub-upstream &&
18 + rm -fr sub-upstream &&
19 + cp -r test_dirs/* .
20 +}
21 +
22 +# Tests that the expected branch does not exist
23 +test_no_branch () {
24 + DIR=$1 &&
25 + BRANCH_NAME=$2 &&
26 + test_must_fail git -C "$DIR" rev-parse "$BRANCH_NAME" 2>err &&
27 + grep "ambiguous argument .$BRANCH_NAME." err
28 +}
29 +
30 +test_expect_success 'setup superproject and submodule' '
31 + mkdir test_dirs &&
32 + (
33 + cd test_dirs &&
34 + git init super &&
35 + test_commit -C super foo &&
36 + git init sub-sub-upstream &&
37 + test_commit -C sub-sub-upstream foo &&
38 + git init sub-upstream &&
39 + # Submodule in a submodule
40 + git -C sub-upstream submodule add "${pwd}/test_dirs/sub-sub-upstream" sub-sub &&
41 + git -C sub-upstream commit -m "add submodule" &&
42 + # Regular submodule
43 + git -C super submodule add "${pwd}/test_dirs/sub-upstream" sub &&
44 + # Submodule in a subdirectory
45 + git -C super submodule add "${pwd}/test_dirs/sub-sub-upstream" second/sub &&
46 + git -C super commit -m "add submodule" &&
47 + git -C super config submodule.propagateBranches true &&
48 + git -C super/sub submodule update --init
49 + ) &&
50 + reset_test
51 +'
52 +
53 +# Test the argument parsing
54 +test_expect_success '--recurse-submodules should create branches' '
55 + test_when_finished "reset_test" &&
56 + (
57 + cd super &&
58 + git branch --recurse-submodules branch-a &&
59 + git rev-parse branch-a &&
60 + git -C sub rev-parse branch-a &&
61 + git -C sub/sub-sub rev-parse branch-a &&
62 + git -C second/sub rev-parse branch-a
63 + )
64 +'
65 +
66 +test_expect_success '--recurse-submodules should die if submodule.propagateBranches is false' '
67 + test_when_finished "reset_test" &&
68 + (
69 + cd super &&
70 + echo "fatal: branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled" >expected &&
71 + test_must_fail git -c submodule.propagateBranches=false branch --recurse-submodules branch-a 2>actual &&
72 + test_cmp expected actual
73 + )
74 +'
75 +
76 +test_expect_success '--recurse-submodules should fail when not creating branches' '
77 + test_when_finished "reset_test" &&
78 + (
79 + cd super &&
80 + git branch --recurse-submodules branch-a &&
81 + echo "fatal: --recurse-submodules can only be used to create branches" >expected &&
82 + test_must_fail git branch --recurse-submodules -D branch-a 2>actual &&
83 + test_cmp expected actual &&
84 + # Assert that the branches were not deleted
85 + git rev-parse branch-a &&
86 + git -C sub rev-parse branch-a
87 + )
88 +'
89 +
90 +test_expect_success 'should respect submodule.recurse when creating branches' '
91 + test_when_finished "reset_test" &&
92 + (
93 + cd super &&
94 + git -c submodule.recurse=true branch branch-a &&
95 + git rev-parse branch-a &&
96 + git -C sub rev-parse branch-a
97 + )
98 +'
99 +
100 +test_expect_success 'should ignore submodule.recurse when not creating branches' '
101 + test_when_finished "reset_test" &&
102 + (
103 + cd super &&
104 + git branch --recurse-submodules branch-a &&
105 + git -c submodule.recurse=true branch -D branch-a &&
106 + test_no_branch . branch-a &&
107 + git -C sub rev-parse branch-a
108 + )
109 +'
110 +
111 +# Test branch creation behavior
112 +test_expect_success 'should create branches based off commit id in superproject' '
113 + test_when_finished "reset_test" &&
114 + (
115 + cd super &&
116 + git branch --recurse-submodules branch-a &&
117 + git checkout --recurse-submodules branch-a &&
118 + git -C sub rev-parse HEAD >expected &&
119 + # Move the tip of sub:branch-a so that it no longer matches the commit in super:branch-a
120 + git -C sub checkout branch-a &&
121 + test_commit -C sub bar &&
122 + # Create a new branch-b branch with start-point=branch-a
123 + git branch --recurse-submodules branch-b branch-a &&
124 + git rev-parse branch-b &&
125 + git -C sub rev-parse branch-b >actual &&
126 + # Assert that the commit id of sub:second-branch matches super:branch-a and not sub:branch-a
127 + test_cmp expected actual
128 + )
129 +'
130 +
131 +test_expect_success 'should not create any branches if branch is not valid for all repos' '
132 + test_when_finished "reset_test" &&
133 + (
134 + cd super &&
135 + git -C sub branch branch-a &&
136 + test_must_fail git branch --recurse-submodules branch-a 2>actual &&
137 + test_no_branch . branch-a &&
138 + grep "submodule .sub.: fatal: A branch named .branch-a. already exists" actual
139 + )
140 +'
141 +
142 +test_expect_success 'should create branches if branch exists and --force is given' '
143 + test_when_finished "reset_test" &&
144 + (
145 + cd super &&
146 + git -C sub rev-parse HEAD >expected &&
147 + test_commit -C sub baz &&
148 + # branch-a in sub now points to a newer commit.
149 + git -C sub branch branch-a HEAD &&
150 + git -C sub rev-parse branch-a >actual-old-branch-a &&
151 + git branch --recurse-submodules --force branch-a &&
152 + git rev-parse branch-a &&
153 + git -C sub rev-parse branch-a >actual-new-branch-a &&
154 + test_cmp expected actual-new-branch-a &&
155 + # assert that branch --force actually moved the sub
156 + # branch
157 + ! test_cmp expected actual-old-branch-a
158 + )
159 +'
160 +
161 +test_expect_success 'should create branch when submodule is not in HEAD:.gitmodules' '
162 + test_when_finished "reset_test" &&
163 + (
164 + cd super &&
165 + git branch branch-a &&
166 + git checkout -b branch-b &&
167 + git submodule add ../sub-upstream sub2 &&
168 + git -C sub2 submodule update --init &&
169 + # branch-b now has a committed submodule not in branch-a
170 + git commit -m "add second submodule" &&
171 + git checkout branch-a &&
172 + git branch --recurse-submodules branch-c branch-b &&
173 + git checkout --recurse-submodules branch-c &&
174 + git -C sub2 rev-parse branch-c &&
175 + git -C sub2/sub-sub rev-parse branch-c
176 + )
177 +'
178 +
179 +test_expect_success 'should not create branches in inactive submodules' '
180 + test_when_finished "reset_test" &&
181 + test_config -C super submodule.sub.active false &&
182 + (
183 + cd super &&
184 + git branch --recurse-submodules branch-a &&
185 + git rev-parse branch-a &&
186 + test_no_branch sub branch-a
187 + )
188 +'
189 +
190 +test_expect_success 'should set up tracking of local branches with track=always' '
191 + test_when_finished "reset_test" &&
192 + (
193 + cd super &&
194 + git -c branch.autoSetupMerge=always branch --recurse-submodules branch-a main &&
195 + git -C sub rev-parse main &&
196 + test_cmp_config -C sub . branch.branch-a.remote &&
197 + test_cmp_config -C sub refs/heads/main branch.branch-a.merge
198 + )
199 +'
200 +
201 +test_expect_success 'should set up tracking of local branches with explicit track' '
202 + test_when_finished "reset_test" &&
203 + (
204 + cd super &&
205 + git branch --track --recurse-submodules branch-a main &&
206 + git -C sub rev-parse main &&
207 + test_cmp_config -C sub . branch.branch-a.remote &&
208 + test_cmp_config -C sub refs/heads/main branch.branch-a.merge
209 + )
210 +'
211 +
212 +test_expect_success 'should not set up unnecessary tracking of local branches' '
213 + test_when_finished "reset_test" &&
214 + (
215 + cd super &&
216 + git branch --recurse-submodules branch-a main &&
217 + git -C sub rev-parse main &&
218 + test_cmp_config -C sub "" --default "" branch.branch-a.remote &&
219 + test_cmp_config -C sub "" --default "" branch.branch-a.merge
220 + )
221 +'
222 +
223 +reset_remote_test () {
224 + rm -fr super-clone &&
225 + reset_test
226 +}
227 +
228 +test_expect_success 'setup tests with remotes' '
229 + (
230 + cd test_dirs &&
231 + (
232 + cd super &&
233 + git branch branch-a &&
234 + git checkout -b branch-b &&
235 + git submodule add ../sub-upstream sub2 &&
236 + # branch-b now has a committed submodule not in branch-a
237 + git commit -m "add second submodule"
238 + ) &&
239 + git clone --branch main --recurse-submodules super super-clone &&
240 + git -C super-clone config submodule.propagateBranches true
241 + ) &&
242 + reset_remote_test
243 +'
244 +
245 +test_expect_success 'should get fatal error upon branch creation when submodule is not in .git/modules' '
246 + test_when_finished "reset_remote_test" &&
247 + (
248 + cd super-clone &&
249 + # This should succeed because super-clone has sub in .git/modules
250 + git branch --recurse-submodules branch-a origin/branch-a &&
251 + # This should fail because super-clone does not have sub2 .git/modules
252 + test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
253 + grep "fatal: submodule .sub2.: unable to find submodule" actual &&
254 + test_no_branch . branch-b &&
255 + test_no_branch sub branch-b &&
256 + # User can fix themselves by initializing the submodule
257 + git checkout origin/branch-b &&
258 + git submodule update --init --recursive &&
259 + git branch --recurse-submodules branch-b origin/branch-b
260 + )
261 +'
262 +
263 +test_expect_success 'should set up tracking of remote-tracking branches' '
264 + test_when_finished "reset_remote_test" &&
265 + (
266 + cd super-clone &&
267 + git branch --recurse-submodules branch-a origin/branch-a &&
268 + test_cmp_config origin branch.branch-a.remote &&
269 + test_cmp_config refs/heads/branch-a branch.branch-a.merge &&
270 + # "origin/branch-a" does not exist for "sub", but it matches the refspec
271 + # so tracking should be set up
272 + test_cmp_config -C sub origin branch.branch-a.remote &&
273 + test_cmp_config -C sub refs/heads/branch-a branch.branch-a.merge &&
274 + test_cmp_config -C sub/sub-sub origin branch.branch-a.remote &&
275 + test_cmp_config -C sub/sub-sub refs/heads/branch-a branch.branch-a.merge
276 + )
277 +'
278 +
279 +test_expect_success 'should not fail when unable to set up tracking in submodule' '
280 + test_when_finished "reset_remote_test" &&
281 + (
282 + cd super-clone &&
283 + git remote rename origin ex-origin &&
284 + git branch --recurse-submodules branch-a ex-origin/branch-a &&
285 + test_cmp_config ex-origin branch.branch-a.remote &&
286 + test_cmp_config refs/heads/branch-a branch.branch-a.merge &&
287 + test_cmp_config -C sub "" --default "" branch.branch-a.remote &&
288 + test_cmp_config -C sub "" --default "" branch.branch-a.merge
289 + )
290 +'
291 +
292 +test_done