-
Notifications
You must be signed in to change notification settings - Fork 165
git-add: Skip submodules with ignore=all unless --force and explicit path used #1987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
69e0065
504edcb
d5ea8eb
df5c85d
8bfaa14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,9 @@ | |
| #include "csum-file.h" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Kristoffer Haugsbakk" wrote on the Git mailing list (how to reply to this email): On Wed, Jan 14, 2026, at 08:47, Claus Schneider(Eficode) via GitGitGadget wrote:
> From: "Claus Schneider(Eficode)" <[email protected]>
>
> Submodules configured with ignore=all are now skipped during add operations
> unless overridden by --force and the submodule path is explicitly specified.
>
> A message is printed (like ignored files) guiding the user to use the
> --force flag if the user has explicitely want to update the submodule
> reference.
>
> The reason for the change is support submodule branch tracking or
> similar and git status states nothing and git add should not add either
> as a default behaviour. The workflow is more logic and similar to regular
> ignored files even the submodule is already tracked.
>
> The change opens up a lot of possibilities for submodules to be used
> more freely and simular to the repo tool. A submodule can be added for many
s/simular/similar/ ? Although the sentence doesn’t quite make sense to
me. Well I’m unfamiliar with the domain anyway. ;)
> more reason and loosely coupled dependencies to the super repo which often
s/more reason/more reasons/ ?
> gives the friction of handle the explicit commits and updates without
s/handle/handling/
> the need for tracking the submodule sha1 by sha1.
>
> Signed-off-by: Claus Schneider(Eficode) <[email protected]>
> ---
>[snip]There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claus Schneider wrote on the Git mailing list (how to reply to this email): Thanks for feedback. Well noted and it will be reworded like this:
....
A message is printed (like ignored files) guiding the user to use the
--force flag if the user explicitly wants to update the submodule reference.
The reason for the change is to support branch tracking in submodules
with configuration `submdule.<name>.branch` or similar workflows where the
user is not interested in tracking each update of the sha1 in the submdule.
You can additionally set `submodule.<name>.ignore=all` and the
`git status`will state nothing and, with this patch, the `git add`
does not either - as
the default behaviour. This patch changes the workflow to a more logical
behaviour and similar to workflow for ignored files.
The patch gives more scenarios for submodules to be used effectively with less
friction similar to the "repo" tool. A submodule can be added for many
different reasons than a hard dependency. It can be added as loosely coupled
dependencies whereas the user wants the latest based on the configuration
`submoule.<name>.branch`, but are not interested to track each commit in the
`super-repo`. Currently it gives friction of handling conflicts between
branches even the sha1's are fast-forward and the user just wants the latest
in any way. A developer can still add a sha1 explicitly to track updates.
...
// Claus
On Thu, Jan 15, 2026 at 4:43 PM Kristoffer Haugsbakk
<[email protected]> wrote:
>
> On Wed, Jan 14, 2026, at 08:47, Claus Schneider(Eficode) via GitGitGadget wrote:
> > From: "Claus Schneider(Eficode)" <[email protected]>
> >
> > Submodules configured with ignore=all are now skipped during add operations
> > unless overridden by --force and the submodule path is explicitly specified.
> >
> > A message is printed (like ignored files) guiding the user to use the
> > --force flag if the user has explicitely want to update the submodule
> > reference.
> >
> > The reason for the change is support submodule branch tracking or
> > similar and git status states nothing and git add should not add either
> > as a default behaviour. The workflow is more logic and similar to regular
> > ignored files even the submodule is already tracked.
> >
> > The change opens up a lot of possibilities for submodules to be used
> > more freely and simular to the repo tool. A submodule can be added for many
>
> s/simular/similar/ ? Although the sentence doesn’t quite make sense to
> me. Well I’m unfamiliar with the domain anyway. ;)
>
> > more reason and loosely coupled dependencies to the super repo which often
>
> s/more reason/more reasons/ ?
>
> > gives the friction of handle the explicit commits and updates without
>
> s/handle/handling/
>
> > the need for tracking the submodule sha1 by sha1.
> >
> > Signed-off-by: Claus Schneider(Eficode) <[email protected]>
> > ---
> >[snip] |
||
| #include "promisor-remote.h" | ||
| #include "hook.h" | ||
| #include "submodule.h" | ||
| #include "submodule-config.h" | ||
| #include "advice.h" | ||
|
|
||
| /* Mask for the name length in ce_flags in the on-disk index */ | ||
|
|
||
|
|
@@ -3881,9 +3884,12 @@ void overlay_tree_on_index(struct index_state *istate, | |
|
|
||
| struct update_callback_data { | ||
| struct index_state *index; | ||
| struct repository *repo; | ||
| struct pathspec *pathspec; | ||
| int include_sparse; | ||
| int flags; | ||
| int add_errors; | ||
| int ignored_too; | ||
| }; | ||
|
|
||
| static int fix_unmerged_status(struct diff_filepair *p, | ||
|
|
@@ -3907,8 +3913,68 @@ static int fix_unmerged_status(struct diff_filepair *p, | |
| return DIFF_STATUS_MODIFIED; | ||
| } | ||
|
|
||
| static int skip_submodule(const char *path, | ||
| struct repository *repo, | ||
| struct pathspec *pathspec, | ||
| int ignored_too) | ||
| { | ||
| struct stat st; | ||
| const struct submodule *sub; | ||
| int pathspec_matches = 0; | ||
| int ps_i; | ||
| char *norm_pathspec = NULL; | ||
|
|
||
| /* Only consider if path is a directory */ | ||
| if (lstat(path, &st) || !S_ISDIR(st.st_mode)) | ||
| return 0; | ||
|
|
||
| /* Check if it's a submodule with ignore=all */ | ||
| sub = submodule_from_path(repo, null_oid(the_hash_algo), path); | ||
| if (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, "all")) | ||
| return 0; | ||
|
|
||
| trace_printf("ignore=all: %s\n", path); | ||
| trace_printf("pathspec %s\n", (pathspec && pathspec->nr) | ||
| ? "has pathspec" | ||
| : "no pathspec"); | ||
|
|
||
| /* Check if submodule path is explicitly mentioned in pathspec */ | ||
| if (pathspec) { | ||
| for (ps_i = 0; ps_i < pathspec->nr; ps_i++) { | ||
| const char *m = pathspec->items[ps_i].match; | ||
| if (!m) | ||
| continue; | ||
| norm_pathspec = xstrdup(m); | ||
| strip_dir_trailing_slashes(norm_pathspec); | ||
| if (!strcmp(path, norm_pathspec)) { | ||
| pathspec_matches = 1; | ||
| FREE_AND_NULL(norm_pathspec); | ||
| break; | ||
| } | ||
| FREE_AND_NULL(norm_pathspec); | ||
| } | ||
| } | ||
|
|
||
| /* If explicitly matched and forced, allow adding */ | ||
| if (pathspec_matches) { | ||
| if (ignored_too && ignored_too > 0) { | ||
| trace_printf("Add submodule due to --force: %s\n", path); | ||
| return 0; | ||
| } else { | ||
| advise_if_enabled(ADVICE_ADD_IGNORED_FILE, | ||
| _("Skipping submodule due to ignore=all: %s\n" | ||
| "Use --force if you really want to add the submodule."), path); | ||
| return 1; | ||
| } | ||
| } | ||
|
|
||
| /* No explicit pathspec match -> skip silently */ | ||
| trace_printf("Pathspec to submodule does not match explicitly: %s\n", path); | ||
| return 1; | ||
| } | ||
|
|
||
| static void update_callback(struct diff_queue_struct *q, | ||
| struct diff_options *opt UNUSED, void *cbdata) | ||
| struct diff_options *opt UNUSED, void *cbdata) | ||
| { | ||
| int i; | ||
| struct update_callback_data *data = cbdata; | ||
|
|
@@ -3918,14 +3984,19 @@ static void update_callback(struct diff_queue_struct *q, | |
| const char *path = p->one->path; | ||
|
|
||
| if (!data->include_sparse && | ||
| !path_in_sparse_checkout(path, data->index)) | ||
| !path_in_sparse_checkout(path, data->index)) | ||
| continue; | ||
|
|
||
| switch (fix_unmerged_status(p, data)) { | ||
| default: | ||
| die(_("unexpected diff status %c"), p->status); | ||
| case DIFF_STATUS_MODIFIED: | ||
| case DIFF_STATUS_TYPE_CHANGED: | ||
| if (skip_submodule(path, data->repo, | ||
| data->pathspec, | ||
| data->ignored_too)) | ||
| continue; | ||
|
|
||
| if (add_file_to_index(data->index, path, data->flags)) { | ||
| if (!(data->flags & ADD_CACHE_IGNORE_ERRORS)) | ||
| die(_("updating files failed")); | ||
|
|
@@ -3946,7 +4017,7 @@ static void update_callback(struct diff_queue_struct *q, | |
|
|
||
| int add_files_to_cache(struct repository *repo, const char *prefix, | ||
| const struct pathspec *pathspec, char *ps_matched, | ||
| int include_sparse, int flags) | ||
| int include_sparse, int flags, int ignored_too ) | ||
| { | ||
| struct odb_transaction *transaction; | ||
| struct update_callback_data data; | ||
|
|
@@ -3956,6 +4027,9 @@ int add_files_to_cache(struct repository *repo, const char *prefix, | |
| data.index = repo->index; | ||
| data.include_sparse = include_sparse; | ||
| data.flags = flags; | ||
| data.repo = repo; | ||
| data.ignored_too = ignored_too; | ||
| data.pathspec = (struct pathspec *)pathspec; | ||
|
|
||
| repo_init_revisions(repo, &rev, prefix); | ||
| setup_revisions(0, NULL, &rev, NULL); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| #!/bin/sh | ||
| # shellcheck disable=SC2016 | ||
|
|
||
| # shellcheck disable=SC2034 | ||
| test_description='git add respects submodule ignore=all and explicit pathspec' | ||
|
|
||
| # This test covers the behavior of "git add", "git status" and "git log" when | ||
| # dealing with submodules that have the ignore=all setting in | ||
| # .gitmodules. It ensures that changes in such submodules are | ||
| # ignored by default, but can be staged with "git add --force". | ||
|
|
||
| # shellcheck disable=SC1091 | ||
| . ./test-lib.sh | ||
|
|
||
| GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main | ||
| export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME | ||
|
|
||
| base_path=$(pwd -P) | ||
|
|
||
| #1 | ||
| test_expect_success 'setup: create origin repos' ' | ||
| cd "${base_path}" && | ||
| git config --global protocol.file.allow always && | ||
| git init sub && | ||
| pwd && | ||
| cd sub && | ||
| test_commit sub_file1 && | ||
| git tag v1.0 && | ||
| test_commit sub_file2 && | ||
| git tag v2.0 && | ||
| test_commit sub_file3 && | ||
| git tag v3.0 && | ||
| cd "${base_path}" && | ||
| git init main && | ||
| cd main && | ||
| test_commit first && | ||
| cd "${base_path}" | ||
| ' | ||
| #2 | ||
| # add submodule with default config (ignore=none) and | ||
| # check log that is contains a path entry for the submodule 'sub' | ||
| # change the commit in the submodule and check that 'git status' shows it as modified | ||
| test_expect_success 'main: add submodule with default config' ' | ||
| cd "${base_path}" && | ||
| cd main && | ||
| git submodule add ../sub && | ||
| git commit -m "add submodule" && | ||
| git log --oneline --name-only | grep "^sub$" && | ||
| git -C sub reset --hard v2.0 && | ||
| git status --porcelain | grep "^ M sub$" && | ||
| echo | ||
| ' | ||
| #3 | ||
| # change the submodule config to ignore=all and check that status and log do not show changes | ||
| test_expect_success 'main: submodule config ignore=all' ' | ||
| cd "${base_path}" && | ||
| cd main && | ||
| git config -f .gitmodules submodule.sub.ignore all && | ||
| GIT_TRACE=1 git add . && | ||
| git commit -m "update submodule config sub.ignore all" && | ||
| ! git status --porcelain | grep "^.*$" && | ||
| ! git log --oneline --name-only | grep "^sub$" && | ||
| echo | ||
| ' | ||
| #4 | ||
| # change the commit in the submodule and check that 'git status' does not show it as modified | ||
| # but 'git status --ignore-submodules=none' does show it as modified | ||
| test_expect_success 'sub: change to different sha1 and check status in main' ' | ||
| cd "${base_path}" && | ||
| cd main && | ||
| git -C sub reset --hard v1.0 && | ||
| ! git status --porcelain | grep "^ M sub$" && | ||
| git status --ignore-submodules=none --porcelain | grep "^ M sub$" && | ||
| echo | ||
| ' | ||
|
|
||
| #5 | ||
| # check that normal 'git add' does not stage the change in the submodule | ||
| test_expect_success 'main: check normal add and status' ' | ||
| cd "${base_path}" && | ||
| cd main && | ||
| GIT_TRACE=1 git add . && | ||
| ! git status --porcelain | grep "^ M sub$" && | ||
| echo | ||
| ' | ||
|
|
||
| #6 | ||
| # check that 'git add --force .' does not stage the change in the submodule | ||
| # and that 'git status' does not show it as modified | ||
| test_expect_success 'main: check --force add . and status' ' | ||
| cd "${base_path}" && | ||
| cd main && | ||
| GIT_TRACE=1 git add --force . && | ||
| ! git status --porcelain | grep "^M sub$" && | ||
| echo | ||
| ' | ||
|
|
||
| #7 | ||
| # check that 'git add .' does not stage the change in the submodule | ||
| # and that 'git status' does not show it as modified | ||
| test_expect_success 'main: check _add sub_ and status' ' | ||
| cd "${base_path}" && | ||
| cd main && | ||
| GIT_TRACE=1 git add sub 2>&1 | grep "Skipping submodule due to ignore=all: sub" && | ||
| ! git status --porcelain | grep "^M sub$" && | ||
| echo | ||
| ' | ||
|
|
||
| #8 | ||
| # check that 'git add --force sub' does stage the change in the submodule | ||
| # check that 'git add --force ./sub/' does stage the change in the submodule | ||
| # and that 'git status --porcelain' does show it as modified | ||
| # commit it.. | ||
| # check that 'git log --ignore-submodules=none' shows the submodule change | ||
| # in the log | ||
| test_expect_success 'main: check force add sub and ./sub/ and status' ' | ||
| cd "${base_path}" && | ||
| cd main && | ||
| echo "Adding with --force should work: git add --force sub" && | ||
| GIT_TRACE=1 git add --force sub && | ||
| git status --porcelain | grep "^M sub$" && | ||
| git restore --staged sub && | ||
| ! git status --porcelain | grep "^M sub$" && | ||
| echo "Adding with --force should work: git add --force ./sub/" && | ||
| GIT_TRACE=1 git add --force ./sub/ && | ||
| git status --porcelain | grep "^M sub$" && | ||
| git commit -m "update submodule pointer" && | ||
| ! git status --porcelain | grep "^ M sub$" && | ||
| git log --ignore-submodules=none --name-only --oneline | grep "^sub$" && | ||
| echo | ||
| ' | ||
|
|
||
| test_done | ||
| exit 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ben Knoble wrote on the Git mailing list (how to reply to this email):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claus Schneider wrote on the Git mailing list (how to reply to this email):