diff options
author | Taylor Blau <me@ttaylorr.com> | 2022-11-18 20:04:57 -0500 |
---|---|---|
committer | Taylor Blau <me@ttaylorr.com> | 2022-11-18 20:04:57 -0500 |
commit | 7a93aaab006ae57b7e20b21fa981bf3e4fab2553 (patch) | |
tree | 3b95e1d7d833a71abf6771c3efe351cf69a743db | |
parent | e06bd84551261e45fad72b2ddff646c34ca29240 (diff) | |
parent | 52469059f10483401d194f781c083b700a3880d4 (diff) | |
download | git-7a93aaab006ae57b7e20b21fa981bf3e4fab2553.tar.gz |
Merge branch 'gc/submodule-clone-update-with-branches' into jch
"git clone --recurse-submodules" and "git submodule update" learns
to honor the "propagete branches" option.
* gc/submodule-clone-update-with-branches:
clone, submodule update: create and check out branches
submodule--helper: remove update_data.suboid
submodule update: refactor update targets
submodule: return target of submodule symref
t5617: drop references to remote-tracking branches
submodule--helper clone: create named branch
repo-settings: add submodule_propagate_branches
clone: teach --detach option
-rw-r--r-- | Documentation/git-clone.txt | 8 | ||||
-rw-r--r-- | builtin/branch.c | 11 | ||||
-rw-r--r-- | builtin/clone.c | 12 | ||||
-rw-r--r-- | builtin/submodule--helper.c | 102 | ||||
-rw-r--r-- | builtin/update-index.c | 4 | ||||
-rw-r--r-- | cache.h | 1 | ||||
-rw-r--r-- | combine-diff.c | 3 | ||||
-rw-r--r-- | diff-lib.c | 2 | ||||
-rw-r--r-- | dir.c | 2 | ||||
-rw-r--r-- | object-file.c | 2 | ||||
-rw-r--r-- | read-cache.c | 4 | ||||
-rw-r--r-- | refs.c | 10 | ||||
-rw-r--r-- | refs.h | 5 | ||||
-rw-r--r-- | repo-settings.c | 13 | ||||
-rw-r--r-- | repository.h | 1 | ||||
-rw-r--r-- | submodule.c | 2 | ||||
-rwxr-xr-x | t/t5601-clone.sh | 22 | ||||
-rwxr-xr-x | t/t5617-clone-submodules.sh (renamed from t/t5617-clone-submodules-remote.sh) | 40 | ||||
-rwxr-xr-x | t/t7406-submodule-update.sh | 156 | ||||
-rw-r--r-- | unpack-trees.c | 3 |
20 files changed, 357 insertions, 46 deletions
diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index d6434d262d..6a4e5d31b4 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -16,7 +16,7 @@ SYNOPSIS [--depth <depth>] [--[no-]single-branch] [--no-tags] [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules] [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow] - [--filter=<filter> [--also-filter-submodules]] [--] <repository> + [--filter=<filter> [--also-filter-submodules] [--detach]] [--] <repository> [<directory>] DESCRIPTION @@ -210,6 +210,12 @@ objects from the source repository into a pack in the cloned repository. `--branch` can also take tags and detaches the HEAD at that commit in the resulting repository. +--detach:: + If the cloned repository's HEAD points to a branch, point the newly + created HEAD to the branch's commit instead of the branch itself. + Additionally, in a non-bare repository, the corresponding local branch + will not be created. + -u <upload-pack>:: --upload-pack <upload-pack>:: When given, and the repository to clone from is accessed diff --git a/builtin/branch.c b/builtin/branch.c index 9470c980c1..0879cc655e 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -40,7 +40,6 @@ 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] = { @@ -106,10 +105,6 @@ static int git_branch_config(const char *var, const char *value, void *cb) 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); } @@ -713,7 +708,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, options, builtin_branch_usage, 0); - + prepare_repo_settings(the_repository); if (!delete && !rename && !copy && !edit_description && !new_upstream && !show_current && !unset_upstream && argc == 0) list = 1; @@ -729,7 +724,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) usage_with_options(builtin_branch_usage, options); if (recurse_submodules_explicit) { - if (!submodule_propagate_branches) + if (!the_repository->settings.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")); @@ -737,7 +732,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) recurse_submodules = (recurse_submodules || recurse_submodules_explicit) && - submodule_propagate_branches; + the_repository->settings.submodule_propagate_branches; if (filter.abbrev == -1) filter.abbrev = DEFAULT_ABBREV; diff --git a/builtin/clone.c b/builtin/clone.c index 0e4348686b..894be8eda4 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -78,6 +78,7 @@ static int option_filter_submodules = -1; /* unspecified */ static int config_filter_submodules = -1; /* unspecified */ static struct string_list server_options = STRING_LIST_INIT_NODUP; static int option_remote_submodules; +static int option_detach; static const char *bundle_uri; static int recurse_submodules_cb(const struct option *opt, @@ -162,6 +163,8 @@ static struct option builtin_clone_options[] = { N_("any cloned submodules will use their remote-tracking branch")), OPT_BOOL(0, "sparse", &option_sparse_checkout, N_("initialize sparse-checkout file to include only files at root")), + OPT_BOOL(0, "detach", &option_detach, + N_("detach HEAD and don't create a local branch")), OPT_STRING(0, "bundle-uri", &bundle_uri, N_("uri"), N_("a URI for downloading bundles before fetching from origin remote")), OPT_END() @@ -613,10 +616,12 @@ static void update_remote_refs(const struct ref *refs, } static void update_head(const struct ref *our, const struct ref *remote, - const char *unborn, const char *msg) + const char *unborn, int should_detach, + const char *msg) { const char *head; - if (our && skip_prefix(our->name, "refs/heads/", &head)) { + if (our && !should_detach && + skip_prefix(our->name, "refs/heads/", &head)) { /* Local default branch link */ if (create_symref("HEAD", our->name, NULL) < 0) die(_("unable to update HEAD")); @@ -1362,7 +1367,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix) branch_top.buf, reflog_msg.buf, transport, !is_local); - update_head(our_head_points_at, remote_head, unborn_head, reflog_msg.buf); + update_head(our_head_points_at, remote_head, unborn_head, + option_detach, reflog_msg.buf); /* * We want to show progress for recursive submodule clones iff diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index c75e9e86b0..28e66a3d70 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1503,6 +1503,8 @@ struct module_clone_data { const char *name; const char *url; const char *depth; + const char *branch; + const char *branch_oid; struct list_objects_filter_options *filter_options; unsigned int quiet: 1; unsigned int progress: 1; @@ -1692,6 +1694,8 @@ static int clone_submodule(const struct module_clone_data *clone_data, strvec_push(&cp.args, clone_data->single_branch ? "--single-branch" : "--no-single-branch"); + if (the_repository->settings.submodule_propagate_branches) + strvec_push(&cp.args, "--detach"); strvec_push(&cp.args, "--"); strvec_push(&cp.args, clone_data->url); @@ -1704,6 +1708,21 @@ static int clone_submodule(const struct module_clone_data *clone_data, if(run_command(&cp)) die(_("clone of '%s' into submodule path '%s' failed"), clone_data->url, clone_data_path); + + if (clone_data->branch) { + struct child_process branch_cp = CHILD_PROCESS_INIT; + + branch_cp.git_cmd = 1; + prepare_other_repo_env(&branch_cp.env, sm_gitdir); + + strvec_pushl(&branch_cp.args, "branch", + clone_data->branch, clone_data->branch_oid, + NULL); + + if (run_command(&branch_cp)) + die(_("could not create branch '%s' in submodule path '%s'"), + clone_data->branch, clone_data_path); + } } else { char *path; @@ -1778,6 +1797,12 @@ static int module_clone(int argc, const char **argv, const char *prefix) N_("disallow cloning into non-empty directory")), OPT_BOOL(0, "single-branch", &clone_data.single_branch, N_("clone only one branch, HEAD or --branch")), + OPT_STRING(0, "branch", &clone_data.branch, + N_("string"), + N_("name of branch to be created")), + OPT_STRING(0, "branch-oid", &clone_data.branch_oid, + N_("object-id"), + N_("commit id for new branch")), OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), OPT_END() }; @@ -1785,12 +1810,14 @@ static int module_clone(int argc, const char **argv, const char *prefix) N_("git submodule--helper clone [--prefix=<path>] [--quiet] " "[--reference <repository>] [--name <name>] [--depth <depth>] " "[--single-branch] [--filter <filter-spec>] " + "[--branch <branch> --branch-oid <oid>]" "--url <url> --path <path>"), NULL }; argc = parse_options(argc, argv, prefix, module_clone_options, git_submodule_helper_usage, 0); + prepare_repo_settings(the_repository); clone_data.dissociate = !!dissociate; clone_data.quiet = !!quiet; @@ -1802,6 +1829,12 @@ static int module_clone(int argc, const char **argv, const char *prefix) usage_with_options(git_submodule_helper_usage, module_clone_options); + if (!!clone_data.branch != !!clone_data.branch_oid) + BUG("--branch and --branch-oid must be set/unset together"); + if ((clone_data.branch && + !the_repository->settings.submodule_propagate_branches)) + BUG("--branch is only expected with submodule.propagateBranches"); + clone_submodule(&clone_data, &reference); list_objects_filter_release(&filter_options); string_list_clear(&reference, 1); @@ -1884,8 +1917,8 @@ static void submodule_update_clone_release(struct submodule_update_clone *suc) struct update_data { const char *prefix; char *displaypath; + const char *super_branch; enum submodule_update_type update_default; - struct object_id suboid; struct string_list references; struct submodule_update_strategy update_strategy; struct list_objects_filter_options *filter_options; @@ -2059,6 +2092,11 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce, strvec_push(&child->args, suc->update_data->single_branch ? "--single-branch" : "--no-single-branch"); + if (ud->super_branch) { + strvec_pushf(&child->args, "--branch=%s", ud->super_branch); + strvec_pushf(&child->args, "--branch-oid=%s", + oid_to_hex(&ce->oid)); + } cleanup: free(displaypath); @@ -2222,9 +2260,14 @@ static int fetch_in_submodule(const char *module_path, int depth, int quiet, static int run_update_command(const struct update_data *ud, int subforce) { struct child_process cp = CHILD_PROCESS_INIT; - char *oid = oid_to_hex(&ud->oid); + const char *update_target; int ret; + if (ud->update_strategy.type == SM_UPDATE_CHECKOUT && ud->super_branch) + update_target = ud->super_branch; + else + update_target = oid_to_hex(&ud->oid); + switch (ud->update_strategy.type) { case SM_UPDATE_CHECKOUT: cp.git_cmd = 1; @@ -2252,7 +2295,7 @@ static int run_update_command(const struct update_data *ud, int subforce) BUG("unexpected update strategy type: %d", ud->update_strategy.type); } - strvec_push(&cp.args, oid); + strvec_push(&cp.args, update_target); cp.dir = ud->sm_path; prepare_submodule_repo_env(&cp.env); @@ -2260,20 +2303,20 @@ static int run_update_command(const struct update_data *ud, int subforce) switch (ud->update_strategy.type) { case SM_UPDATE_CHECKOUT: die_message(_("Unable to checkout '%s' in submodule path '%s'"), - oid, ud->displaypath); + update_target, ud->displaypath); /* No "ret" assignment, use "git checkout"'s */ break; case SM_UPDATE_REBASE: ret = die_message(_("Unable to rebase '%s' in submodule path '%s'"), - oid, ud->displaypath); + update_target, ud->displaypath); break; case SM_UPDATE_MERGE: ret = die_message(_("Unable to merge '%s' in submodule path '%s'"), - oid, ud->displaypath); + update_target, ud->displaypath); break; case SM_UPDATE_COMMAND: ret = die_message(_("Execution of '%s %s' failed in submodule path '%s'"), - ud->update_strategy.command, oid, ud->displaypath); + ud->update_strategy.command, update_target, ud->displaypath); break; default: BUG("unexpected update strategy type: %d", @@ -2289,19 +2332,19 @@ static int run_update_command(const struct update_data *ud, int subforce) switch (ud->update_strategy.type) { case SM_UPDATE_CHECKOUT: printf(_("Submodule path '%s': checked out '%s'\n"), - ud->displaypath, oid); + ud->displaypath, update_target); break; case SM_UPDATE_REBASE: printf(_("Submodule path '%s': rebased into '%s'\n"), - ud->displaypath, oid); + ud->displaypath, update_target); break; case SM_UPDATE_MERGE: printf(_("Submodule path '%s': merged in '%s'\n"), - ud->displaypath, oid); + ud->displaypath, update_target); break; case SM_UPDATE_COMMAND: printf(_("Submodule path '%s': '%s %s'\n"), - ud->displaypath, ud->update_strategy.command, oid); + ud->displaypath, ud->update_strategy.command, update_target); break; default: BUG("unexpected update strategy type: %d", @@ -2313,7 +2356,7 @@ static int run_update_command(const struct update_data *ud, int subforce) static int run_update_procedure(const struct update_data *ud) { - int subforce = is_null_oid(&ud->suboid) || ud->force; + int subforce = ud->just_cloned || ud->force; if (!ud->nofetch) { /* @@ -2488,7 +2531,10 @@ static void update_data_to_args(const struct update_data *update_data, static int update_submodule(struct update_data *update_data) { + int submodule_up_to_date; int ret; + struct object_id suboid; + const char *submodule_head = NULL; ret = determine_submodule_update_strategy(the_repository, update_data->just_cloned, @@ -2498,9 +2544,9 @@ static int update_submodule(struct update_data *update_data) if (ret) return ret; - if (update_data->just_cloned) - oidcpy(&update_data->suboid, null_oid()); - else if (resolve_gitlink_ref(update_data->sm_path, "HEAD", &update_data->suboid)) + if (!update_data->just_cloned && + resolve_gitlink_ref(update_data->sm_path, "HEAD", &suboid, + &submodule_head)) return die_message(_("Unable to find current revision in submodule path '%s'"), update_data->displaypath); @@ -2527,14 +2573,26 @@ static int update_submodule(struct update_data *update_data) update_data->sm_path); } - if (resolve_gitlink_ref(update_data->sm_path, remote_ref, &update_data->oid)) + if (resolve_gitlink_ref(update_data->sm_path, remote_ref, + &update_data->oid, NULL)) return die_message(_("Unable to find %s revision in submodule path '%s'"), remote_ref, update_data->sm_path); free(remote_ref); } - if (!oideq(&update_data->oid, &update_data->suboid) || update_data->force) { + if (update_data->just_cloned) + submodule_up_to_date = 0; + else if (update_data->super_branch) + /* Check that the submodule's HEAD points to super_branch. */ + submodule_up_to_date = + skip_prefix(submodule_head, "refs/heads/", + &submodule_head) && + !strcmp(update_data->super_branch, submodule_head); + else + submodule_up_to_date = oideq(&update_data->oid, &suboid); + + if (!submodule_up_to_date || update_data->force) { ret = run_update_procedure(update_data); if (ret) return ret; @@ -2546,7 +2604,6 @@ static int update_submodule(struct update_data *update_data) next.prefix = NULL; oidcpy(&next.oid, null_oid()); - oidcpy(&next.suboid, null_oid()); cp.dir = update_data->sm_path; cp.git_cmd = 1; @@ -2579,6 +2636,12 @@ static int update_submodules(struct update_data *update_data) .data = &suc, }; + if (the_repository->settings.submodule_propagate_branches) { + struct branch *current_branch = branch_get(NULL); + if (current_branch) + update_data->super_branch = current_branch->name; + } + suc.update_data = update_data; run_processes_parallel(&opts); @@ -2688,6 +2751,7 @@ static int module_update(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, module_update_options, git_submodule_helper_usage, 0); + prepare_repo_settings(the_repository); if (opt.require_init) opt.init = 1; @@ -3227,7 +3291,7 @@ static void die_on_repo_without_commits(const char *path) strbuf_addstr(&sb, path); if (is_nonbare_repository_dir(&sb)) { struct object_id oid; - if (resolve_gitlink_ref(path, "HEAD", &oid) < 0) + if (resolve_gitlink_ref(path, "HEAD", &oid, NULL) < 0) die(_("'%s' does not have a commit checked out"), path); } strbuf_release(&sb); diff --git a/builtin/update-index.c b/builtin/update-index.c index 7b0c924d7d..e6305656c9 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -339,7 +339,7 @@ static int process_directory(const char *path, int len, struct stat *st) if (S_ISGITLINK(ce->ce_mode)) { /* Do nothing to the index if there is no HEAD! */ - if (resolve_gitlink_ref(path, "HEAD", &oid) < 0) + if (resolve_gitlink_ref(path, "HEAD", &oid, NULL) < 0) return 0; return add_one_path(ce, path, len, st); @@ -365,7 +365,7 @@ static int process_directory(const char *path, int len, struct stat *st) } /* No match - should we add it as a gitlink? */ - if (!resolve_gitlink_ref(path, "HEAD", &oid)) + if (!resolve_gitlink_ref(path, "HEAD", &oid, NULL)) return add_one_path(NULL, path, len, st); /* Error out. */ @@ -505,6 +505,7 @@ static inline enum object_type object_type(unsigned int mode) #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX" #define GIT_SUPER_PREFIX_ENVIRONMENT "GIT_INTERNAL_SUPER_PREFIX" +#define GIT_SUBMODULE_PROPAGATE_BRANCHES_ENVIRONMENT "GIT_INTERNAL_SUBMODULE_PROPAGATE_BRANCHES" #define DEFAULT_GIT_DIR_ENVIRONMENT ".git" #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" #define INDEX_ENVIRONMENT "GIT_INDEX_FILE" diff --git a/combine-diff.c b/combine-diff.c index b0ece95480..88efcaeefa 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -1060,7 +1060,8 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent, elem->mode = canon_mode(st.st_mode); } else if (S_ISDIR(st.st_mode)) { struct object_id oid; - if (resolve_gitlink_ref(elem->path, "HEAD", &oid) < 0) + if (resolve_gitlink_ref(elem->path, "HEAD", &oid, + NULL) < 0) result = grab_blob(opt->repo, &elem->oid, elem->mode, &result_size, NULL, NULL); diff --git a/diff-lib.c b/diff-lib.c index 30a3d9a2b5..b48f155736 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -54,7 +54,7 @@ static int check_removed(const struct index_state *istate, const struct cache_en * a directory --- the blob was removed! */ if (!S_ISGITLINK(ce->ce_mode) && - resolve_gitlink_ref(ce->name, "HEAD", &sub)) + resolve_gitlink_ref(ce->name, "HEAD", &sub, NULL)) return 1; } return 0; @@ -3251,7 +3251,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up) struct object_id submodule_head; if ((flag & REMOVE_DIR_KEEP_NESTED_GIT) && - !resolve_gitlink_ref(path->buf, "HEAD", &submodule_head)) { + !resolve_gitlink_ref(path->buf, "HEAD", &submodule_head, NULL)) { /* Do not descend and nuke a nested git work tree. */ if (kept_up) *kept_up = 1; diff --git a/object-file.c b/object-file.c index 957790098f..c118e62a55 100644 --- a/object-file.c +++ b/object-file.c @@ -2527,7 +2527,7 @@ int index_path(struct index_state *istate, struct object_id *oid, strbuf_release(&sb); break; case S_IFDIR: - return resolve_gitlink_ref(path, "HEAD", oid); + return resolve_gitlink_ref(path, "HEAD", oid, NULL); default: return error(_("%s: unsupported file type"), path); } diff --git a/read-cache.c b/read-cache.c index ac9f8122e5..57293825a8 100644 --- a/read-cache.c +++ b/read-cache.c @@ -285,7 +285,7 @@ static int ce_compare_gitlink(const struct cache_entry *ce) * * If so, we consider it always to match. */ - if (resolve_gitlink_ref(ce->name, "HEAD", &oid) < 0) + if (resolve_gitlink_ref(ce->name, "HEAD", &oid, NULL) < 0) return 0; return !oideq(&oid, &ce->oid); } @@ -781,7 +781,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, namelen = strlen(path); if (S_ISDIR(st_mode)) { - if (resolve_gitlink_ref(path, "HEAD", &oid) < 0) + if (resolve_gitlink_ref(path, "HEAD", &oid, NULL) < 0) return error(_("'%s' does not have a commit checked out"), path); while (namelen && path[namelen-1] == '/') namelen--; @@ -1898,19 +1898,21 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, } int resolve_gitlink_ref(const char *submodule, const char *refname, - struct object_id *oid) + struct object_id *oid, const char **target_out) { struct ref_store *refs; int flags; + const char *target; refs = get_submodule_ref_store(submodule); if (!refs) return -1; - - if (!refs_resolve_ref_unsafe(refs, refname, 0, oid, &flags) || - is_null_oid(oid)) + target = refs_resolve_ref_unsafe(refs, refname, 0, oid, &flags); + if (!target || is_null_oid(oid)) return -1; + if (target_out) + *target_out = target; return 0; } @@ -137,9 +137,12 @@ int peel_iterated_oid(const struct object_id *base, struct object_id *peeled); * submodule (which must be non-NULL). If the resolution is * successful, return 0 and set oid to the name of the object; * otherwise, return a non-zero value. + * + * FIXME: Return "target" just like refs_resolve_ref_unsafe(). This will be + * safe to do once we merge resolve_gitlink_ref() into master. */ int resolve_gitlink_ref(const char *submodule, const char *refname, - struct object_id *oid); + struct object_id *oid, const char **target); /* * Return true iff abbrev_name is a possible abbreviation for diff --git a/repo-settings.c b/repo-settings.c index 3021921c53..3ec0a53ea6 100644 --- a/repo-settings.c +++ b/repo-settings.c @@ -73,6 +73,19 @@ void prepare_repo_settings(struct repository *r) r->settings.core_multi_pack_index = 1; /* + * If the environment variable is set, assume that it was set by an + * invocation of git running in a superproject with + * submodule.propagateBranches set and that is recursing into this repo + * as a submodule. Therefore, we should ignore whatever is set in this + * repo's config. + */ + r->settings.submodule_propagate_branches = + git_env_bool(GIT_SUBMODULE_PROPAGATE_BRANCHES_ENVIRONMENT, -1); + if (r->settings.submodule_propagate_branches == -1) + repo_cfg_bool(r, "submodule.propagateBranches", + &r->settings.submodule_propagate_branches, 0); + + /* * Non-boolean config */ if (!repo_config_get_int(r, "index.version", &value)) diff --git a/repository.h b/repository.h index 7ce4b8a962..9f5ce21f70 100644 --- a/repository.h +++ b/repository.h @@ -38,6 +38,7 @@ struct repo_settings { int fetch_write_commit_graph; int command_requires_full_index; int sparse_index; + int submodule_propagate_branches; struct fsmonitor_settings *fsmonitor; /* lazily loaded */ diff --git a/submodule.c b/submodule.c index 142f5c7a75..f350932ef1 100644 --- a/submodule.c +++ b/submodule.c @@ -503,6 +503,8 @@ static void print_submodule_diff_summary(struct repository *r, struct rev_info * void prepare_submodule_repo_env(struct strvec *out) { + if (the_repository->settings.submodule_propagate_branches) + strvec_pushf(out, "%s=1", GIT_SUBMODULE_PROPAGATE_BRANCHES_ENVIRONMENT); prepare_other_repo_env(out, DEFAULT_GIT_DIR_ENVIRONMENT); } diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index b2524a24c2..23c8999bc5 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -310,6 +310,28 @@ test_expect_success 'clone checking out a tag' ' test_cmp fetch.expected fetch.actual ' +test_expect_success '--detach detaches and does not create branch' ' + test_when_finished "rm -fr dst" && + git clone --detach src dst && + ( + cd dst && + test_must_fail git rev-parse main && + test_must_fail git symbolic-ref HEAD && + test_cmp_rev HEAD refs/remotes/origin/HEAD + ) +' + +test_expect_success '--detach with --bare detaches but creates branch' ' + test_when_finished "rm -fr dst" && + git clone --bare --detach src dst && + ( + cd dst && + git rev-parse main && + test_must_fail git symbolic-ref HEAD && + test_cmp_rev HEAD refs/heads/main + ) +' + test_expect_success 'set up ssh wrapper' ' cp "$GIT_BUILD_DIR/t/helper/test-fake-ssh$X" \ "$TRASH_DIRECTORY/ssh$X" && diff --git a/t/t5617-clone-submodules-remote.sh b/t/t5617-clone-submodules.sh index 5a4d7936a7..5767c4d318 100755 --- a/t/t5617-clone-submodules-remote.sh +++ b/t/t5617-clone-submodules.sh @@ -1,6 +1,6 @@ #!/bin/sh -test_description='Test cloning repos with submodules using remote-tracking branches' +test_description='Test cloning repos with submodules' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME @@ -14,10 +14,17 @@ test_expect_success 'setup' ' git config --global protocol.file.allow always && git checkout -b main && test_commit commit1 && + mkdir subsub && + ( + cd subsub && + git init && + test_commit subsubcommit1 + ) && mkdir sub && ( cd sub && git init && + git submodule add "file://$pwd/subsub" subsub && test_commit subcommit1 && git tag sub_when_added_to_super && git branch other @@ -108,4 +115,35 @@ test_expect_success '--no-also-filter-submodules overrides clone.filterSubmodule test_cmp_config -C super_clone3/sub false --default false remote.origin.promisor ' +test_expect_success 'submodule.propagateBranches checks out branches at correct commits' ' + test_when_finished "git checkout main" && + + git checkout -b checked-out && + git -C sub checkout -b not-in-clone && + git -C subsub checkout -b not-in-clone && + git clone --recurse-submodules \ + --branch checked-out \ + -c submodule.propagateBranches=true \ + "file://$pwd/." super_clone4 && + + # Assert that each repo is pointing to "checked-out" + for REPO in "super_clone4" "super_clone4/sub" "super_clone4/sub/subsub" + do + HEAD_BRANCH=$(git -C $REPO symbolic-ref HEAD) && + test $HEAD_BRANCH = "refs/heads/checked-out" || return 1 + done && + + # Assert that the submodule branches are pointing to the right revs + EXPECT_SUB_OID="$(git -C super_clone4 rev-parse :sub)" && + ACTUAL_SUB_OID="$(git -C super_clone4/sub rev-parse refs/heads/checked-out)" && + test $EXPECT_SUB_OID = $ACTUAL_SUB_OID && + EXPECT_SUBSUB_OID="$(git -C super_clone4/sub rev-parse :subsub)" && + ACTUAL_SUBSUB_OID="$(git -C super_clone4/sub/subsub rev-parse refs/heads/checked-out)" && + test $EXPECT_SUBSUB_OID = $ACTUAL_SUBSUB_OID && + + # Assert that the submodules do not have branches from their upstream + test_must_fail git -C super_clone4/sub rev-parse not-in-clone && + test_must_fail git -C super_clone4/sub/subsub rev-parse not-in-clone +' + test_done diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index f094e3d7f3..b749d35f78 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -1179,4 +1179,160 @@ test_expect_success 'submodule update --recursive skip submodules with strategy= test_cmp expect.err actual.err ' +test_expect_success 'setup superproject with submodule.propagateBranches' ' + git init sub1 && + test_commit -C sub1 "sub1" && + git init branch-super && + git -C branch-super submodule add ../sub1 sub1 && + git -C branch-super commit -m "super" && + + # Clone into a clean repo that we can cp around + git clone --recurse-submodules \ + -c submodule.propagateBranches=true \ + branch-super branch-super-clean && + git -C branch-super-clean config submodule.propagateBranches true && + + # sub2 will not be in the clone. We will fetch the containing + # superproject commit and clone sub2 with "git submodule update". + git init sub2 && + test_commit -C sub2 "sub2" && + git -C branch-super submodule add ../sub2 sub2 && + git -C branch-super commit -m "add sub2" +' + +test_clean_submodule () +{ + local negate super_dir sub_dir expect_oid actual_oid && + if test "$1" = "!" + then + negate=t + shift + fi + super_dir="$1" && + sub_dir="$2" && + expect_oid="$(git -C "$super_dir" rev-parse ":$sub_dir")" && + actual_oid="$(git -C "$super_dir/$sub_dir" rev-parse HEAD)" && + if test -n "$negate" + then + ! test "$expect_oid" = "$actual_oid" + else + test "$expect_oid" = "$actual_oid" + fi +} + +# Test the behavior of a newly cloned submodule +test_expect_success 'branches - newly-cloned submodule, detached HEAD' ' + test_when_finished "rm -fr branch-super-cloned" && + cp -r branch-super-clean branch-super-cloned && + + git -C branch-super-cloned fetch origin main && + git -C branch-super-cloned checkout FETCH_HEAD && + git -C branch-super-cloned/sub1 checkout --detach && + git -C branch-super-cloned submodule update && + + # sub1 and sub2 should be in detached HEAD + git -C branch-super-cloned/sub1 rev-parse --verify HEAD && + test_must_fail git -C branch-super-cloned/sub1 symbolic-ref HEAD && + test_clean_submodule branch-super-cloned sub1 && + git -C branch-super-cloned/sub2 rev-parse --verify HEAD && + test_must_fail git -C branch-super-cloned/sub2 symbolic-ref HEAD && + test_clean_submodule branch-super-cloned sub2 +' + +test_expect_success 'branches - newly-cloned submodule, branch checked out' ' + test_when_finished "rm -fr branch-super-cloned" && + cp -r branch-super-clean branch-super-cloned && + + git -C branch-super-cloned fetch origin main && + git -C branch-super-cloned checkout FETCH_HEAD && + git -C branch-super-cloned branch new-branch && + git -C branch-super-cloned checkout new-branch && + git -C branch-super-cloned/sub1 branch new-branch && + git -C branch-super-cloned submodule update && + + # Ignore sub1, we will test it later. + # sub2 should check out the branch + HEAD_BRANCH2=$(git -C branch-super-cloned/sub2 symbolic-ref HEAD) && + test $HEAD_BRANCH2 = "refs/heads/new-branch" && + test_clean_submodule branch-super-cloned sub2 +' + +# Test the behavior of an already-cloned submodule. +# NEEDSWORK When updating with branches, we always use the branch instead of the +# gitlink's OID. This results in some imperfect behavior: +# +# - If the gitlink's OID disagrees with the branch OID, updating with branches +# may result in a dirty worktree +# - If the branch does not exist, the update fails. +# +# We will reevaluate when "git checkout --recurse-submodules" supports branches +# For now, just test for this imperfect behavior. +test_expect_success 'branches - correct branch checked out, OIDs agree' ' + test_when_finished "rm -fr branch-super-cloned" && + cp -r branch-super-clean branch-super-cloned && + + git -C branch-super-cloned branch --recurse-submodules new-branch && + git -C branch-super-cloned checkout new-branch && + git -C branch-super-cloned/sub1 checkout new-branch && + git -C branch-super-cloned submodule update && + + HEAD_BRANCH1=$(git -C branch-super-cloned/sub1 symbolic-ref HEAD) && + test $HEAD_BRANCH1 = "refs/heads/new-branch" && + test_clean_submodule branch-super-cloned sub1 +' + +test_expect_success 'branches - correct branch checked out, OIDs disagree' ' + test_when_finished "rm -fr branch-super-cloned" && + cp -r branch-super-clean branch-super-cloned && + + git -C branch-super-cloned branch --recurse-submodules new-branch && + git -C branch-super-cloned checkout new-branch && + git -C branch-super-cloned/sub1 checkout new-branch && + test_commit -C branch-super-cloned/sub1 new-commit && + git -C branch-super-cloned submodule update && + + HEAD_BRANCH1=$(git -C branch-super-cloned/sub1 symbolic-ref HEAD) && + test $HEAD_BRANCH1 = "refs/heads/new-branch" && + test_clean_submodule ! branch-super-cloned sub1 +' + +test_expect_success 'branches - other branch checked out, correct branch exists, OIDs agree' ' + test_when_finished "rm -fr branch-super-cloned" && + cp -r branch-super-clean branch-super-cloned && + + git -C branch-super-cloned branch --recurse-submodules new-branch && + git -C branch-super-cloned checkout new-branch && + git -C branch-super-cloned/sub1 checkout main && + git -C branch-super-cloned submodule update && + + HEAD_BRANCH1=$(git -C branch-super-cloned/sub1 symbolic-ref HEAD) && + test $HEAD_BRANCH1 = "refs/heads/new-branch" && + test_clean_submodule branch-super-cloned sub1 +' + +test_expect_success 'branches - other branch checked out, correct branch exists, OIDs disagree' ' + test_when_finished "rm -fr branch-super-cloned" && + cp -r branch-super-clean branch-super-cloned && + + git -C branch-super-cloned branch --recurse-submodules new-branch && + git -C branch-super-cloned checkout new-branch && + git -C branch-super-cloned/sub1 checkout new-branch && + test_commit -C branch-super-cloned/sub1 new-commit && + git -C branch-super-cloned/sub1 checkout main && + git -C branch-super-cloned submodule update && + + HEAD_BRANCH1=$(git -C branch-super-cloned/sub1 symbolic-ref HEAD) && + test $HEAD_BRANCH1 = "refs/heads/new-branch" && + test_clean_submodule ! branch-super-cloned sub1 +' + +test_expect_success 'branches - other branch checked out, correct branch does not exist' ' + test_when_finished "rm -fr branch-super-cloned" && + cp -r branch-super-clean branch-super-cloned && + + git -C branch-super-cloned branch new-branch && + git -C branch-super-cloned checkout new-branch && + test_must_fail git -C branch-super-cloned submodule update +' + test_done diff --git a/unpack-trees.c b/unpack-trees.c index 8a762aa077..56b71d37ce 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -2289,7 +2289,8 @@ static int verify_clean_subdirectory(const struct cache_entry *ce, if (S_ISGITLINK(ce->ce_mode)) { struct object_id oid; - int sub_head = resolve_gitlink_ref(ce->name, "HEAD", &oid); + int sub_head = + resolve_gitlink_ref(ce->name, "HEAD", &oid, NULL); /* * If we are not going to update the submodule, then * we don't care. |