read-cache: submodule add need --force given ignore=all configuration

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 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. The user can still add a sha1 explicitly to track updates. Signed-off-by: Claus Schneider(Eficode) <claus.schneider@eficode.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Claus Schneider(Eficode) committed Feb 6, 2026 at 13:22 UTC a16c4a245acb2420bafcbd572ba9fb94b1ba5146
1 file changed +71 -3
read-cache.c
+71 -3
@@ -47,6 +47,9 @@
47 #include "csum-file.h"
48 #include "promisor-remote.h"
49 #include "hook.h"
50 +#include "submodule.h"
51 +#include "submodule-config.h"
52 +#include "advice.h"
53
54 /* Mask for the name length in ce_flags in the on-disk index */
55
@@ -3907,8 +3910,68 @@ static int fix_unmerged_status(struct diff_filepair *p,
3910 return DIFF_STATUS_MODIFIED;
3911 }
3912
3913 +static int skip_submodule(const char *path,
3914 + struct repository *repo,
3915 + struct pathspec *pathspec,
3916 + int ignored_too)
3917 +{
3918 + struct stat st;
3919 + const struct submodule *sub;
3920 + int pathspec_matches = 0;
3921 + int ps_i;
3922 + char *norm_pathspec = NULL;
3923 +
3924 + /* Only consider if path is a directory */
3925 + if (lstat(path, &st) || !S_ISDIR(st.st_mode))
3926 + return 0;
3927 +
3928 + /* Check if it's a submodule with ignore=all */
3929 + sub = submodule_from_path(repo, null_oid(the_hash_algo), path);
3930 + if (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, "all"))
3931 + return 0;
3932 +
3933 + trace_printf("ignore=all: %s\n", path);
3934 + trace_printf("pathspec %s\n", (pathspec && pathspec->nr)
3935 + ? "has pathspec"
3936 + : "no pathspec");
3937 +
3938 + /* Check if submodule path is explicitly mentioned in pathspec */
3939 + if (pathspec) {
3940 + for (ps_i = 0; ps_i < pathspec->nr; ps_i++) {
3941 + const char *m = pathspec->items[ps_i].match;
3942 + if (!m)
3943 + continue;
3944 + norm_pathspec = xstrdup(m);
3945 + strip_dir_trailing_slashes(norm_pathspec);
3946 + if (!strcmp(path, norm_pathspec)) {
3947 + pathspec_matches = 1;
3948 + FREE_AND_NULL(norm_pathspec);
3949 + break;
3950 + }
3951 + FREE_AND_NULL(norm_pathspec);
3952 + }
3953 + }
3954 +
3955 + /* If explicitly matched and forced, allow adding */
3956 + if (pathspec_matches) {
3957 + if (ignored_too && ignored_too > 0) {
3958 + trace_printf("Add submodule due to --force: %s\n", path);
3959 + return 0;
3960 + } else {
3961 + advise_if_enabled(ADVICE_ADD_IGNORED_FILE,
3962 + _("Skipping submodule due to ignore=all: %s\n"
3963 + "Use --force if you really want to add the submodule."), path);
3964 + return 1;
3965 + }
3966 + }
3967 +
3968 + /* No explicit pathspec match -> skip silently */
3969 + trace_printf("Pathspec to submodule does not match explicitly: %s\n", path);
3970 + return 1;
3971 +}
3972 +
3973 static void update_callback(struct diff_queue_struct *q,
3911 - struct diff_options *opt UNUSED, void *cbdata)
3974 + struct diff_options *opt UNUSED, void *cbdata)
3975 {
3976 int i;
3977 struct update_callback_data *data = cbdata;
@@ -3918,14 +3981,19 @@ static void update_callback(struct diff_queue_struct *q,
3981 const char *path = p->one->path;
3982
3983 if (!data->include_sparse &&
3921 - !path_in_sparse_checkout(path, data->index))
3984 + !path_in_sparse_checkout(path, data->index))
3985 continue;
3986
3987 switch (fix_unmerged_status(p, data)) {
3988 default:
3989 die(_("unexpected diff status %c"), p->status);
3990 case DIFF_STATUS_MODIFIED:
3928 - case DIFF_STATUS_TYPE_CHANGED: {
3991 + case DIFF_STATUS_TYPE_CHANGED:
3992 + if (skip_submodule(path, data->repo,
3993 + data->pathspec,
3994 + data->ignored_too))
3995 + continue;
3996 +
3997 if (add_file_to_index(data->index, path, data->flags)) {
3998 if (!(data->flags & ADD_CACHE_IGNORE_ERRORS))
3999 die(_("updating files failed"));