]> git.ipfire.org Git - thirdparty/git.git/commitdiff
submodule: prevent overwriting .gitmodules on path reuse
authorK Jayatheerth <jayatheerthkulkarni2005@gmail.com>
Thu, 24 Jul 2025 15:24:17 +0000 (20:54 +0530)
committerJunio C Hamano <gitster@pobox.com>
Thu, 24 Jul 2025 20:35:07 +0000 (13:35 -0700)
Adding a submodule at a path that previously hosted
another submodule (e.g., 'child') reuses the submodule
name derived from the path. If the original submodule
was only moved (e.g., to 'child_old') and not renamed,
this silently overwrites its configuration in .gitmodules.

This behavior loses user configuration and causes
confusion when the original submodule is expected
to remain intact. It assumes that the path-derived
name is always safe to reuse, even though the name
might still be in use elsewhere in the repository.

Teach module_add() to check if the computed submodule
name already exists in the repository's submodule config,
and if so, refuse the operation unless the user explicitly
renames the submodule or uses the --force option,
which will automatically generate a unique name by
appending a number (e.g., child1).

Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-submodule.adoc
builtin/submodule--helper.c
t/t7400-submodule-basic.sh

index 87d8e0f0c563b74acc880c2b7bee3c2c94086d05..503c84a200c929fe623f00567821abaccd4d78da 100644 (file)
@@ -307,6 +307,13 @@ OPTIONS
 --force::
        This option is only valid for add, deinit and update commands.
        When running add, allow adding an otherwise ignored submodule path.
+       This option is also used to bypass a check that the submodule's name
+       is not already in use. By default, 'git submodule add' will fail if
+       the proposed name (which is derived from the path) is already registered
+       for another submodule in the repository. Using '--force' allows the command
+       to proceed by automatically generating a unique name by appending a number
+       to the conflicting name (e.g., if a submodule named 'child' exists, it will
+       try 'child1', and so on).
        When running deinit the submodule working trees will be removed even
        if they contain local changes.
        When running update (only effective with the checkout procedure),
index 53da2116ddf576bc565b29f043e8b703b8b1563b..5f85b169c539ac8dbe588fd84cfc8d6ba8ce64a8 100644 (file)
@@ -3444,6 +3444,10 @@ static int module_add(int argc, const char **argv, const char *prefix,
        struct add_data add_data = ADD_DATA_INIT;
        const char *ref_storage_format = NULL;
        char *to_free = NULL;
+       const struct submodule *existing;
+       struct strbuf buf = STRBUF_INIT;
+       int i;
+       char *sm_name_to_free = NULL;
        struct option options[] = {
                OPT_STRING('b', "branch", &add_data.branch, N_("branch"),
                           N_("branch of repository to add as submodule")),
@@ -3546,6 +3550,28 @@ static int module_add(int argc, const char **argv, const char *prefix,
        if(!add_data.sm_name)
                add_data.sm_name = add_data.sm_path;
 
+       existing = submodule_from_name(the_repository,
+                                       null_oid(the_hash_algo),
+                                       add_data.sm_name);
+
+       if (existing && strcmp(existing->path, add_data.sm_path)) {
+               if (!force) {
+                       die(_("submodule name '%s' already used for path '%s'"),
+                       add_data.sm_name, existing->path);
+               }
+               /* --force: build <name><n> until unique */
+               for (i = 1; ; i++) {
+                       strbuf_reset(&buf);
+                       strbuf_addf(&buf, "%s%d", add_data.sm_name, i);
+                       if (!submodule_from_name(the_repository,
+                                               null_oid(the_hash_algo),
+                                               buf.buf)) {
+                               break;
+                       }
+               }
+               add_data.sm_name = sm_name_to_free = strbuf_detach(&buf, NULL);
+       }
+
        if (check_submodule_name(add_data.sm_name))
                die(_("'%s' is not a valid submodule name"), add_data.sm_name);
 
@@ -3561,6 +3587,7 @@ static int module_add(int argc, const char **argv, const char *prefix,
 
        ret = 0;
 cleanup:
+       free(sm_name_to_free);
        free(add_data.sm_path);
        free(to_free);
        strbuf_release(&sb);
index d6a501d4535417c91723f223a662c8678f507b3f..6812df308107cb92423f40ea90b8ace80e2adf5e 100755 (executable)
@@ -1482,4 +1482,26 @@ test_expect_success '`submodule init` and `init.templateDir`' '
        )
 '
 
+test_expect_success 'submodule add fails when name is reused' '
+       git init test-submodule &&
+       (
+               cd test-submodule &&
+               git commit --allow-empty -m init &&
+
+               git init ../child-origin &&
+               git -C ../child-origin commit --allow-empty -m init &&
+
+               git submodule add ../child-origin child &&
+               git commit -m "Add submodule child" &&
+
+               git mv child child_old &&
+               git commit -m "Move child to child_old" &&
+
+               # Now adding a *new* repo at the old name must fail
+               git init ../child2-origin &&
+               git -C ../child2-origin commit --allow-empty -m init &&
+               test_must_fail git submodule add ../child2-origin child
+       )
+'
+
 test_done