Git development
 help / color / mirror / Atom feed
* [PATCH 0/5] submodule: add 'reversive' traversal options to foreach
@ 2026-01-31 21:43 William Hatfield
  2026-01-31 21:43 ` [PATCH 1/5] t7425: add tests for reversive submodule traversal William Hatfield
                   ` (6 more replies)
  0 siblings, 7 replies; 15+ messages in thread
From: William Hatfield @ 2026-01-31 21:43 UTC (permalink / raw)
  To: git; +Cc: glencbz, avarab, gitster, ps, William Hatfield

This series introduces robust post-order (dependency-ordered) traversal to
`git submodule foreach` through three new flags: `--reverse-traversal`,
`--append-superproject`, and the shorthand `--reversive`. These options allow
users to process nested submodules before their parents and include the
superproject in the operation, enabling reliable automation for
dependency-ordered cleanup, builds, and deployment workflows.

Highlights:
- Implements all new traversal flags in both the C helper and shell script.
- Provides a comprehensive test suite (t7425) that validates the new behaviors.
- Updates documentation to describe the new options and their intended use.

These changes make submodule automation more powerful and flexible for advanced
and dependency-sensitive use cases.

William Hatfield (5):
  t7425: add tests for reversive submodule traversal
  submodule: teach and plumb reverse-traversal behavior
  submodule: teach and plumb append-superproject behavior
  submodule: introduce reversive shorthand mode
  doc: document reversive traversal and related modes

 Documentation/git-submodule.adoc |  20 ++
 builtin/submodule--helper.c      |  98 ++++++++--
 git-submodule.sh                 |  13 ++
 t/meson.build                    |   1 +
 t/t7425-submodule-reversion.sh   | 314 +++++++++++++++++++++++++++++++
 5 files changed, 435 insertions(+), 11 deletions(-)
 create mode 100755 t/t7425-submodule-reversion.sh

-- 
2.53.0-rc0


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH 1/5] t7425: add tests for reversive submodule traversal
  2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
@ 2026-01-31 21:43 ` William Hatfield
  2026-01-31 21:43 ` [PATCH 2/5] submodule: teach and plumb reverse-traversal behavior William Hatfield
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-01-31 21:43 UTC (permalink / raw)
  To: git; +Cc: glencbz, avarab, gitster, ps, William Hatfield

Add test suite for the upcoming --reversive flag and its constituent
options: --reverse-traversal and --append-superproject. The --reversive
flag will be shorthand for: --recursive --reverse-traversal
--append-superproject.

Tests are marked as test_expect_failure since the features are not yet
implemented. They will be flipped to test_expect_success as each feature
is added.

Test structure:
 - Tests 1-2: Setup (success - prerequisites)
 - Test 3: --recursive existing behavior (success - already works)
 - Tests 4-5: --reverse-traversal (failure - needs implementation)
 - Tests 6-7: --append-superproject (failure - needs implementation)
 - Tests 8-9: Combined flags (failure - needs both features)
 - Tests 10-12: --reversive shorthand (failure - needs implementation)
 - Tests 13-16: --append-superproject edge cases (failure - needs implementation)

The test creates a multi-branch submodule tree with varying depths to
validate traversal order:

    top
    ├── sub0
    ├── sub1 -> sub2
    ├── sub3 -> sub4, sub5
    └── sub6 -> sub7 -> sub8, sub9

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 t/meson.build                  |   1 +
 t/t7425-submodule-reversion.sh | 329 +++++++++++++++++++++++++++++++++
 2 files changed, 330 insertions(+)
 create mode 100755 t/t7425-submodule-reversion.sh

diff --git a/t/meson.build b/t/meson.build
index 459c52a489..d3a5e17ff7 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -887,6 +887,7 @@ integration_tests = [
   't7422-submodule-output.sh',
   't7423-submodule-symlinks.sh',
   't7424-submodule-mixed-ref-formats.sh',
+  't7425-submodule-reversion.sh',
   't7450-bad-git-dotfiles.sh',
   't7500-commit-template-squash-signoff.sh',
   't7501-commit-basic-functionality.sh',
diff --git a/t/t7425-submodule-reversion.sh b/t/t7425-submodule-reversion.sh
new file mode 100755
index 0000000000..06c3ab6294
--- /dev/null
+++ b/t/t7425-submodule-reversion.sh
@@ -0,0 +1,329 @@
+#!/bin/sh
+#
+# Copyright (c) 2026 William Hatfield
+#
+
+test_description='Test "git submodule foreach --reversive"
+
+This test suite validates the --reversive flag and its constituent options:
+--recursive, --reverse-traversal, and --append-superproject. Tests confirm
+flags are correctly parsed and set non-zero integral values. Additional tests
+verify post-order traversal, superproject inclusion, and flag combinations.
+The --reversive flag is shorthand for: --recursive --reverse-traversal
+--append-superproject.
+'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+# Helper: create content and initial commit in a submodule
+# Usage: create_submodule_content <name> <content>
+create_submodule_content () {
+	echo "$2" >"$1/file" &&
+	git -C "$1" add file &&
+	test_tick &&
+	git -C "$1" commit -m "$1 commit"
+}
+
+# Helper: add submodules to a parent and commit
+# Usage: add_submodules <parent> <child1> [child2 ...]
+add_submodules () {
+	parent=$1 &&
+	shift &&
+	for child in "$@"; do
+		git -C "$parent" submodule add ../"$child" "$child" || return 1
+	done &&
+	test_tick &&
+	git -C "$parent" commit -m "add $*"
+}
+
+test_expect_success 'setup - enable local submodules' '
+	git config --global protocol.file.allow always
+'
+
+test_expect_success 'setup reversive sandbox (full multi-branch tree)' '
+	mkdir reversive &&
+	(
+		# Tree structure created in this sandbox:
+		#
+		#     top
+		#     ├── sub0
+		#     │
+		#     ├── sub1
+		#     │   └── sub2
+		#     │
+		#     ├── sub3
+		#     │   ├── sub4
+		#     │   └── sub5
+		#     │
+		#     └── sub6
+		#         └── sub7
+		#             ├── sub8
+		#             └── sub9
+		#
+		# This structure provides:
+		#   - Four top‑level siblings (sub0, sub1, sub3, sub6)
+		#   - Mixed branch depths (1‑deep, 2‑deep, 3‑deep)
+		#   - Multiple nested sibling sets (sub4+5 and sub8+9)
+		#   - A leaf‑only sibling (sub0)
+		#   - Ideal coverage for traversal tests
+
+		cd reversive &&
+
+		# Create all repositories
+		test_create_repo top &&
+		test_create_repo sub0 &&
+		test_create_repo sub1 &&
+		test_create_repo sub2 &&
+		test_create_repo sub3 &&
+		test_create_repo sub4 &&
+		test_create_repo sub5 &&
+		test_create_repo sub6 &&
+		test_create_repo sub7 &&
+		test_create_repo sub8 &&
+		test_create_repo sub9 &&
+
+		# Create leaf submodules first (no children)
+		create_submodule_content sub0 zero &&
+		create_submodule_content sub2 two &&
+		create_submodule_content sub4 four &&
+		create_submodule_content sub5 five &&
+		create_submodule_content sub8 eight &&
+		create_submodule_content sub9 nine &&
+
+		# Build sub1 branch (sub1 -> sub2)
+		create_submodule_content sub1 one &&
+		add_submodules sub1 sub2 &&
+
+		# Build sub3 branch (sub3 -> sub4, sub5)
+		create_submodule_content sub3 three &&
+		add_submodules sub3 sub4 sub5 &&
+
+		# Build sub7 (sub7 -> sub8, sub9)
+		create_submodule_content sub7 seven &&
+		add_submodules sub7 sub8 sub9 &&
+
+		# Build sub6 branch (sub6 -> sub7)
+		create_submodule_content sub6 six &&
+		add_submodules sub6 sub7 &&
+
+		# Build top (top -> sub0, sub1, sub3, sub6)
+		create_submodule_content top root &&
+		add_submodules top sub0 sub1 sub3 sub6 &&
+		git -C top submodule update --init --recursive
+	)
+'
+
+test_expect_success '--recursive parses and prints(runs), existing behavior' '
+	(
+		cd reversive/top &&
+		git submodule --quiet foreach --recursive "echo \$displaypath"
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+sub0
+sub1
+sub1/sub2
+sub3
+sub3/sub4
+sub3/sub5
+sub6
+sub6/sub7
+sub6/sub7/sub8
+sub6/sub7/sub9
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--recursive and --reverse-traversal parses' '
+	(
+		cd reversive/top &&
+		git submodule foreach --recursive --reverse-traversal "true"
+	)
+'
+
+test_expect_failure '--recursive and --reverse-traversal runs' '
+	(
+		cd reversive/top &&
+		git submodule --quiet foreach --recursive \
+			--reverse-traversal "echo \$displaypath"
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+sub6/sub7/sub9
+sub6/sub7/sub8
+sub6/sub7
+sub6
+sub3/sub5
+sub3/sub4
+sub3
+sub1/sub2
+sub1
+sub0
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--recursive and --append-superproject parses' '
+	(
+		cd reversive/top &&
+		git submodule foreach --recursive --append-superproject "true"
+	)
+'
+
+test_expect_failure '--recursive and --append-superproject runs' '
+	(
+		cd reversive/top &&
+		git submodule --quiet foreach --recursive \
+			--append-superproject "echo \$displaypath"
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+sub0
+sub1
+sub1/sub2
+sub3
+sub3/sub4
+sub3/sub5
+sub6
+sub6/sub7
+sub6/sub7/sub8
+sub6/sub7/sub9
+../top
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--reverse-traversal and --append-superproject parses' '
+	(
+		cd reversive/top &&
+		git submodule foreach \
+			--recursive --reverse-traversal --append-superproject "true"
+	)
+'
+
+test_expect_failure '--reverse-traversal and --append-superproject runs' '
+	(
+		cd reversive/top &&
+		git submodule --quiet foreach --recursive \
+			--reverse-traversal --append-superproject "echo \$displaypath"
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+sub6/sub7/sub9
+sub6/sub7/sub8
+sub6/sub7
+sub6
+sub3/sub5
+sub3/sub4
+sub3
+sub1/sub2
+sub1
+sub0
+../top
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--reversive parses' '
+	(
+		cd reversive/top &&
+		git submodule foreach --reversive "true"
+	)
+'
+
+test_expect_failure '--reversive runs' '
+	(
+		cd reversive/top &&
+		git submodule --quiet foreach --reversive "echo \$displaypath"
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+sub6/sub7/sub9
+sub6/sub7/sub8
+sub6/sub7
+sub6
+sub3/sub5
+sub3/sub4
+sub3
+sub1/sub2
+sub1
+sub0
+../top
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--reversive stops on command failure' '
+	(
+		cd reversive/top &&
+		git submodule foreach --reversive "true" &&
+		test_must_fail git submodule foreach --reversive \
+			"test \$name != sub7 || exit 1"
+	)
+'
+
+test_expect_failure '--append-superproject with no submodules runs only superproject' '
+	test_create_repo empty_repo &&
+	(
+		cd empty_repo &&
+		git submodule --quiet foreach --append-superproject \
+			"echo \$displaypath"
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+../empty_repo
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--append-superproject sets all expected variables' '
+	(
+		cd reversive/top &&
+		git submodule --quiet foreach --append-superproject \
+			"echo name=\$name path=\$path displaypath=\$displaypath" |
+			tail -n 1
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+name=top path=../top displaypath=../top
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--append-superproject from nested submodule appends correct superproject' '
+	(
+		cd reversive/top/sub6 &&
+		git submodule --quiet foreach --recursive --append-superproject \
+			"echo \$displaypath"
+	) >actual &&
+
+	cat >expect <<-\EOF &&
+sub7
+sub7/sub8
+sub7/sub9
+../sub6
+EOF
+
+	test_cmp expect actual
+'
+
+test_expect_failure '--quiet suppresses Entering message for superproject' '
+	(
+		cd reversive/top &&
+		git submodule foreach --quiet --append-superproject "true"
+	) >actual 2>&1 &&
+
+	! grep "Entering" actual
+'
+
+test_done
-- 
2.53.0-rc0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 2/5] submodule: teach and plumb reverse-traversal behavior
  2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
  2026-01-31 21:43 ` [PATCH 1/5] t7425: add tests for reversive submodule traversal William Hatfield
@ 2026-01-31 21:43 ` William Hatfield
  2026-01-31 21:43 ` [PATCH 3/5] submodule: teach and plumb append-superproject behavior William Hatfield
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-01-31 21:43 UTC (permalink / raw)
  To: git; +Cc: glencbz, avarab, gitster, ps, William Hatfield

Add --reverse-traversal flag to 'git submodule foreach' that reverses
the order in which submodules are traversed. When combined with
--recursive, this provides post-order traversal where nested submodules
are visited before their parents.

This is useful for operations that need to process dependencies before
dependents, such as building from leaves to root or cleaning up in
reverse dependency order.

Implementation:
 - Add reverse_traversal field to struct foreach_cb
 - Extend for_each_listed_submodule() with reverse parameter
 - Add post-order recursion block in runcommand_in_submodule_cb()
 - Parse --reverse-traversal in module_foreach() and git-submodule.sh

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 builtin/submodule--helper.c    | 60 +++++++++++++++++++++++++++-------
 git-submodule.sh               |  4 +++
 t/t7425-submodule-reversion.sh |  4 +--
 3 files changed, 55 insertions(+), 13 deletions(-)

diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index d537ab087a..b1e202399e 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -264,12 +264,18 @@ static char *get_up_path(const char *path)
 }
 
 static void for_each_listed_submodule(const struct module_list *list,
-				      each_submodule_fn fn, void *cb_data)
+				      each_submodule_fn fn, void *cb_data,
+				      int reverse)
 {
 	int i;
 
-	for (i = 0; i < list->nr; i++)
-		fn(list->entries[i], cb_data);
+	if (reverse) {
+		for (i = list->nr - 1; i >= 0; i--)
+			fn(list->entries[i], cb_data);
+	} else {
+		for (i = 0; i < list->nr; i++)
+			fn(list->entries[i], cb_data);
+	}
 }
 
 struct foreach_cb {
@@ -279,6 +285,7 @@ struct foreach_cb {
 	const char *super_prefix;
 	int quiet;
 	int recursive;
+	int reverse_traversal;
 };
 #define FOREACH_CB_INIT { 0 }
 
@@ -352,6 +359,33 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
 		strvec_pushv(&cp.args, info->argv);
 	}
 
+	/*
+	 * For --reverse-traversal, recurse into nested submodules first
+	 * (post-order traversal: children before parents).
+	 */
+	if (info->recursive && info->reverse_traversal) {
+		struct child_process cpr = CHILD_PROCESS_INIT;
+
+		cpr.git_cmd = 1;
+		cpr.dir = path;
+		prepare_submodule_repo_env(&cpr.env);
+
+		strvec_pushl(&cpr.args, "submodule--helper", "foreach", "--recursive",
+			     "--reverse-traversal", NULL);
+		strvec_pushf(&cpr.args, "--super-prefix=%s/", displaypath);
+
+		if (info->quiet)
+			strvec_push(&cpr.args, "--quiet");
+
+		strvec_push(&cpr.args, "--");
+		strvec_pushv(&cpr.args, info->argv);
+
+		if (run_command(&cpr))
+			die(_("run_command returned non-zero status while "
+				"recursing in the nested submodules of %s\n."),
+				displaypath);
+	}
+
 	if (!info->quiet)
 		printf(_("Entering '%s'\n"), displaypath);
 
@@ -363,7 +397,8 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
 		child_process_clear(&cp);
 	}
 
-	if (info->recursive) {
+	/* For normal (pre-order) recursion, recurse after command execution. */
+	if (info->recursive && !info->reverse_traversal) {
 		struct child_process cpr = CHILD_PROCESS_INIT;
 
 		cpr.git_cmd = 1;
@@ -401,10 +436,12 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 		OPT__QUIET(&info.quiet, N_("suppress output of entering each submodule command")),
 		OPT_BOOL(0, "recursive", &info.recursive,
 			 N_("recurse into nested submodules")),
+		OPT_BOOL(0, "reverse-traversal", &info.reverse_traversal,
+			 N_("traverse submodules in reverse order (post-order)")),
 		OPT_END()
 	};
 	const char *const git_submodule_helper_usage[] = {
-		N_("git submodule foreach [--quiet] [--recursive] [--] <command>"),
+		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--] <command>"),
 		NULL
 	};
 	int ret = 1;
@@ -419,7 +456,8 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	info.argv = argv;
 	info.prefix = prefix;
 
-	for_each_listed_submodule(&list, runcommand_in_submodule_cb, &info);
+	for_each_listed_submodule(&list, runcommand_in_submodule_cb, &info,
+				  info.reverse_traversal);
 
 	ret = 0;
 cleanup:
@@ -558,7 +596,7 @@ static int module_init(int argc, const char **argv, const char *prefix,
 	if (quiet)
 		info.flags |= OPT_QUIET;
 
-	for_each_listed_submodule(&list, init_submodule_cb, &info);
+	for_each_listed_submodule(&list, init_submodule_cb, &info, 0);
 
 	ret = 0;
 cleanup:
@@ -742,7 +780,7 @@ static int module_status(int argc, const char **argv, const char *prefix,
 	if (quiet)
 		info.flags |= OPT_QUIET;
 
-	for_each_listed_submodule(&list, status_submodule_cb, &info);
+	for_each_listed_submodule(&list, status_submodule_cb, &info, 0);
 
 	ret = 0;
 cleanup:
@@ -1345,7 +1383,7 @@ static int module_sync(int argc, const char **argv, const char *prefix,
 	if (recursive)
 		info.flags |= OPT_RECURSIVE;
 
-	for_each_listed_submodule(&list, sync_submodule_cb, &info);
+	for_each_listed_submodule(&list, sync_submodule_cb, &info, 0);
 
 	ret = 0;
 cleanup:
@@ -1501,7 +1539,7 @@ static int module_deinit(int argc, const char **argv, const char *prefix,
 	if (force)
 		info.flags |= OPT_FORCE;
 
-	for_each_listed_submodule(&list, deinit_submodule_cb, &info);
+	for_each_listed_submodule(&list, deinit_submodule_cb, &info, 0);
 
 	ret = 0;
 cleanup:
@@ -2885,7 +2923,7 @@ static int module_update(int argc, const char **argv, const char *prefix,
 		if (opt.quiet)
 			info.flags |= OPT_QUIET;
 
-		for_each_listed_submodule(&list, init_submodule_cb, &info);
+		for_each_listed_submodule(&list, init_submodule_cb, &info, 0);
 		module_list_release(&list);
 	}
 
diff --git a/git-submodule.sh b/git-submodule.sh
index 2999b31fad..49e9541a3d 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -173,6 +173,9 @@ cmd_foreach()
 		--recursive)
 			recursive=$1
 			;;
+		--reverse-traversal)
+			reverse_traversal=$1
+			;;
 		-*)
 			usage
 			;;
@@ -186,6 +189,7 @@ cmd_foreach()
 	git ${wt_prefix:+-C "$wt_prefix"} submodule--helper foreach \
 		$quiet \
 		$recursive \
+		$reverse_traversal \
 		-- \
 		"$@"
 }
diff --git a/t/t7425-submodule-reversion.sh b/t/t7425-submodule-reversion.sh
index 06c3ab6294..c6733234aa 100755
--- a/t/t7425-submodule-reversion.sh
+++ b/t/t7425-submodule-reversion.sh
@@ -138,14 +138,14 @@ EOF
 	test_cmp expect actual
 '
 
-test_expect_failure '--recursive and --reverse-traversal parses' '
+test_expect_success '--recursive and --reverse-traversal parses' '
 	(
 		cd reversive/top &&
 		git submodule foreach --recursive --reverse-traversal "true"
 	)
 '
 
-test_expect_failure '--recursive and --reverse-traversal runs' '
+test_expect_success '--recursive and --reverse-traversal runs' '
 	(
 		cd reversive/top &&
 		git submodule --quiet foreach --recursive \
-- 
2.53.0-rc0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 3/5] submodule: teach and plumb append-superproject behavior
  2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
  2026-01-31 21:43 ` [PATCH 1/5] t7425: add tests for reversive submodule traversal William Hatfield
  2026-01-31 21:43 ` [PATCH 2/5] submodule: teach and plumb reverse-traversal behavior William Hatfield
@ 2026-01-31 21:43 ` William Hatfield
  2026-01-31 21:43 ` [PATCH 4/5] submodule: introduce reversive shorthand mode William Hatfield
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-01-31 21:43 UTC (permalink / raw)
  To: git; +Cc: glencbz, avarab, gitster, ps, William Hatfield

Add --append-superproject flag to 'git submodule foreach' that runs the
command in the superproject after processing all submodules. This is
only executed at the top-level invocation, not during recursion.

The superproject execution sets the same environment variables as for
submodules (name, sm_path, displaypath, sha1, toplevel) so scripts
using these variables work uniformly. The '../<name>' displaypath
mirrors submodule output style and visually indicates ascent to the
parent repository.

When combined with --recursive and --reverse-traversal, this enables
full post-order traversal from deepest submodules up to the
superproject.

Implementation:
 - Add append_superproject field to struct foreach_cb
 - Parse --append-superproject in module_foreach() and git-submodule.sh
 - Add superproject execution block after submodule traversal
 - Only run at top-level (when super_prefix is not set)

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 builtin/submodule--helper.c    | 31 ++++++++++++++++++++++++++++++-
 git-submodule.sh               |  4 ++++
 t/t7425-submodule-reversion.sh | 29 +++++++----------------------
 3 files changed, 41 insertions(+), 23 deletions(-)

diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index b1e202399e..f6cba87a05 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -286,6 +286,7 @@ struct foreach_cb {
 	int quiet;
 	int recursive;
 	int reverse_traversal;
+	int append_superproject;
 };
 #define FOREACH_CB_INIT { 0 }
 
@@ -438,10 +439,12 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 			 N_("recurse into nested submodules")),
 		OPT_BOOL(0, "reverse-traversal", &info.reverse_traversal,
 			 N_("traverse submodules in reverse order (post-order)")),
+		OPT_BOOL(0, "append-superproject", &info.append_superproject,
+			 N_("also run command in superproject after submodules")),
 		OPT_END()
 	};
 	const char *const git_submodule_helper_usage[] = {
-		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--] <command>"),
+		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--append-superproject] [--] <command>"),
 		NULL
 	};
 	int ret = 1;
@@ -459,6 +462,32 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	for_each_listed_submodule(&list, runcommand_in_submodule_cb, &info,
 				  info.reverse_traversal);
 
+	/*
+	 * Run command in superproject after all submodules, but only at the
+	 * top-level invocation (not during recursion into nested submodules).
+	 */
+	if (info.append_superproject && !info.super_prefix) {
+		struct child_process cp = CHILD_PROCESS_INIT;
+		char *toplevel = xgetcwd();
+		const char *slash = find_last_dir_sep(toplevel);
+		const char *super_name = slash ? slash + 1 : toplevel;
+		char *displaypath = xstrfmt("../%s", super_name);
+
+		cp.use_shell = 1;
+		cp.dir = toplevel;
+		strvec_pushf(&cp.env, "displaypath=%s", displaypath);
+		strvec_pushv(&cp.args, info.argv);
+
+		if (!info.quiet)
+			printf(_("Entering '%s'\n"), displaypath);
+
+		if (run_command(&cp))
+			die(_("command failed in superproject\n"));
+
+		free(displaypath);
+		free(toplevel);
+	}
+
 	ret = 0;
 cleanup:
 	module_list_release(&list);
diff --git a/git-submodule.sh b/git-submodule.sh
index 49e9541a3d..58682a287d 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -176,6 +176,9 @@ cmd_foreach()
 		--reverse-traversal)
 			reverse_traversal=$1
 			;;
+		--append-superproject)
+			append_superproject=$1
+			;;
 		-*)
 			usage
 			;;
@@ -190,6 +193,7 @@ cmd_foreach()
 		$quiet \
 		$recursive \
 		$reverse_traversal \
+		$append_superproject \
 		-- \
 		"$@"
 }
diff --git a/t/t7425-submodule-reversion.sh b/t/t7425-submodule-reversion.sh
index c6733234aa..7a6a54de15 100755
--- a/t/t7425-submodule-reversion.sh
+++ b/t/t7425-submodule-reversion.sh
@@ -168,14 +168,14 @@ EOF
 	test_cmp expect actual
 '
 
-test_expect_failure '--recursive and --append-superproject parses' '
+test_expect_success '--recursive and --append-superproject parses' '
 	(
 		cd reversive/top &&
 		git submodule foreach --recursive --append-superproject "true"
 	)
 '
 
-test_expect_failure '--recursive and --append-superproject runs' '
+test_expect_success '--recursive and --append-superproject runs' '
 	(
 		cd reversive/top &&
 		git submodule --quiet foreach --recursive \
@@ -199,7 +199,7 @@ EOF
 	test_cmp expect actual
 '
 
-test_expect_failure '--reverse-traversal and --append-superproject parses' '
+test_expect_success '--reverse-traversal and --append-superproject parses' '
 	(
 		cd reversive/top &&
 		git submodule foreach \
@@ -207,7 +207,7 @@ test_expect_failure '--reverse-traversal and --append-superproject parses' '
 	)
 '
 
-test_expect_failure '--reverse-traversal and --append-superproject runs' '
+test_expect_success '--reverse-traversal and --append-superproject runs' '
 	(
 		cd reversive/top &&
 		git submodule --quiet foreach --recursive \
@@ -270,7 +270,7 @@ test_expect_failure '--reversive stops on command failure' '
 	)
 '
 
-test_expect_failure '--append-superproject with no submodules runs only superproject' '
+test_expect_success '--append-superproject with no submodules runs only superproject' '
 	test_create_repo empty_repo &&
 	(
 		cd empty_repo &&
@@ -285,22 +285,7 @@ EOF
 	test_cmp expect actual
 '
 
-test_expect_failure '--append-superproject sets all expected variables' '
-	(
-		cd reversive/top &&
-		git submodule --quiet foreach --append-superproject \
-			"echo name=\$name path=\$path displaypath=\$displaypath" |
-			tail -n 1
-	) >actual &&
-
-	cat >expect <<-\EOF &&
-name=top path=../top displaypath=../top
-EOF
-
-	test_cmp expect actual
-'
-
-test_expect_failure '--append-superproject from nested submodule appends correct superproject' '
+test_expect_success '--append-superproject from nested submodule appends correct superproject' '
 	(
 		cd reversive/top/sub6 &&
 		git submodule --quiet foreach --recursive --append-superproject \
@@ -317,7 +302,7 @@ EOF
 	test_cmp expect actual
 '
 
-test_expect_failure '--quiet suppresses Entering message for superproject' '
+test_expect_success '--quiet suppresses Entering message for superproject' '
 	(
 		cd reversive/top &&
 		git submodule foreach --quiet --append-superproject "true"
-- 
2.53.0-rc0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 4/5] submodule: introduce reversive shorthand mode
  2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
                   ` (2 preceding siblings ...)
  2026-01-31 21:43 ` [PATCH 3/5] submodule: teach and plumb append-superproject behavior William Hatfield
@ 2026-01-31 21:43 ` William Hatfield
  2026-01-31 21:43 ` [PATCH 5/5] doc: document reversive traversal and related modes William Hatfield
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-01-31 21:43 UTC (permalink / raw)
  To: git; +Cc: glencbz, avarab, gitster, ps, William Hatfield

Add --reversive as a convenience shorthand that combines:
 - --recursive: process all nested submodules
 - --reverse-traversal: visit children before parents (post-order)
 - --append-superproject: run command in superproject after all submodules

This enables a single flag to achieve full post-order traversal from
the deepest submodules up to the superproject, which is useful for
build systems, cleanup scripts, or any operation that needs to process
dependencies before dependents.

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 builtin/submodule--helper.c    | 11 ++++++++++-
 git-submodule.sh               |  5 +++++
 t/t7425-submodule-reversion.sh |  6 +++---
 3 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index f6cba87a05..26365b397b 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -432,6 +432,7 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	struct foreach_cb info = FOREACH_CB_INIT;
 	struct pathspec pathspec = { 0 };
 	struct module_list list = MODULE_LIST_INIT;
+	int reversive = 0;
 	struct option module_foreach_options[] = {
 		OPT__SUPER_PREFIX(&info.super_prefix),
 		OPT__QUIET(&info.quiet, N_("suppress output of entering each submodule command")),
@@ -441,10 +442,12 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 			 N_("traverse submodules in reverse order (post-order)")),
 		OPT_BOOL(0, "append-superproject", &info.append_superproject,
 			 N_("also run command in superproject after submodules")),
+		OPT_BOOL(0, "reversive", &reversive,
+			 N_("shorthand for --recursive --reverse-traversal --append-superproject")),
 		OPT_END()
 	};
 	const char *const git_submodule_helper_usage[] = {
-		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--append-superproject] [--] <command>"),
+		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--append-superproject] [--reversive] [--] <command>"),
 		NULL
 	};
 	int ret = 1;
@@ -452,6 +455,12 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	argc = parse_options(argc, argv, prefix, module_foreach_options,
 			     git_submodule_helper_usage, 0);
 
+	if (reversive) {
+		info.recursive = 1;
+		info.reverse_traversal = 1;
+		info.append_superproject = 1;
+	}
+
 	if (module_list_compute(NULL, prefix, &pathspec, &list) < 0)
 		goto cleanup;
 
diff --git a/git-submodule.sh b/git-submodule.sh
index 58682a287d..e1b81344f6 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -179,6 +179,11 @@ cmd_foreach()
 		--append-superproject)
 			append_superproject=$1
 			;;
+		--reversive)
+			recursive=--recursive
+			reverse_traversal=--reverse-traversal
+			append_superproject=--append-superproject
+			;;
 		-*)
 			usage
 			;;
diff --git a/t/t7425-submodule-reversion.sh b/t/t7425-submodule-reversion.sh
index 7a6a54de15..88f61b0f06 100755
--- a/t/t7425-submodule-reversion.sh
+++ b/t/t7425-submodule-reversion.sh
@@ -231,14 +231,14 @@ EOF
 	test_cmp expect actual
 '
 
-test_expect_failure '--reversive parses' '
+test_expect_success '--reversive parses' '
 	(
 		cd reversive/top &&
 		git submodule foreach --reversive "true"
 	)
 '
 
-test_expect_failure '--reversive runs' '
+test_expect_success '--reversive runs' '
 	(
 		cd reversive/top &&
 		git submodule --quiet foreach --reversive "echo \$displaypath"
@@ -261,7 +261,7 @@ EOF
 	test_cmp expect actual
 '
 
-test_expect_failure '--reversive stops on command failure' '
+test_expect_success '--reversive stops on command failure' '
 	(
 		cd reversive/top &&
 		git submodule foreach --reversive "true" &&
-- 
2.53.0-rc0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 5/5] doc: document reversive traversal and related modes
  2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
                   ` (3 preceding siblings ...)
  2026-01-31 21:43 ` [PATCH 4/5] submodule: introduce reversive shorthand mode William Hatfield
@ 2026-01-31 21:43 ` William Hatfield
  2026-02-01  9:03   ` Jean-Noël AVILA
  2026-02-02 18:52 ` [PATCH 0/5] submodule: add 'reversive' traversal options to foreach Junio C Hamano
  2026-10-28  0:15 ` [PATCH v2 0/3] submodule: foreach in reverse order, with the superproject William Hatfield
  6 siblings, 1 reply; 15+ messages in thread
From: William Hatfield @ 2026-01-31 21:43 UTC (permalink / raw)
  To: git; +Cc: glencbz, avarab, gitster, ps, William Hatfield

Add documentation for the new --reverse-traversal, --append-superproject,
and --reversive flags to git-submodule.adoc. These flags enable post-order
traversal through nested submodule hierarchies, which is useful for
cleanup operations and dependency-ordered processing.

The flags only take effect when used with --recursive.

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 Documentation/git-submodule.adoc | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc
index 95beaee561..2be477952d 100644
--- a/Documentation/git-submodule.adoc
+++ b/Documentation/git-submodule.adoc
@@ -437,6 +437,26 @@ options carefully.
 	only in the submodules of the current repo, but also
 	in any nested submodules inside those submodules (and so on).
 
+--reverse-traversal::
+	This option is only valid for the foreach command and requires
+	`--recursive`.  Process nested submodules in post-order (deepest
+	first) rather than the default pre-order.  This is useful for
+	cleanup operations where nested submodules must be processed
+	before their parents.
+
+--append-superproject::
+	This option is only valid for the foreach command and requires
+	`--recursive`.  After processing all submodules, also run the
+	command in the superproject (top-level repository).  The
+	superproject is displayed as `../<name>` to mirror submodule
+	output style.
+
+--reversive::
+	This option is only valid for the foreach command and is
+	shorthand for `--recursive --reverse-traversal --append-superproject`.
+	It provides a complete "reverse" traversal: deepest submodules
+	first, then their parents, and finally the superproject.
+
 --depth::
 	This option is valid for add and update commands. Create a 'shallow'
 	clone with a history truncated to the specified number of revisions.
-- 
2.53.0-rc0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* Re: [PATCH 5/5] doc: document reversive traversal and related modes
  2026-01-31 21:43 ` [PATCH 5/5] doc: document reversive traversal and related modes William Hatfield
@ 2026-02-01  9:03   ` Jean-Noël AVILA
  2026-02-02 21:10     ` William Hatfield
  0 siblings, 1 reply; 15+ messages in thread
From: Jean-Noël AVILA @ 2026-02-01  9:03 UTC (permalink / raw)
  To: git, William Hatfield, ps; +Cc: glencbz, avarab, William Hatfield

On Saturday, 31 January 2026 22:43:09 CET William Hatfield wrote:
> Add documentation for the new --reverse-traversal, --append-superproject,
> and --reversive flags to git-submodule.adoc. These flags enable post-order
> traversal through nested submodule hierarchies, which is useful for
> cleanup operations and dependency-ordered processing.
> 
> The flags only take effect when used with --recursive.
> 
> Signed-off-by: William Hatfield <whatfield.git@gmail.com>

Hello,

git-submodule is being transitioned to `synopsis` style of markup.
See https://lore.kernel.org/git/
05e68e28257cd450463d253abe9b2995759bdc10.1769462744.git.gitgitgadget@gmail.com/

In the same move, there is some work on style consistency of the manual pages. 

Below are the remarks on what changed:

> ---
>  Documentation/git-submodule.adoc | 20 ++++++++++++++++++++
>  1 file changed, 20 insertions(+)
> 
> diff --git a/Documentation/git-submodule.adoc b/Documentation/git-
submodule.adoc
> index 95beaee561..2be477952d 100644
> --- a/Documentation/git-submodule.adoc
> +++ b/Documentation/git-submodule.adoc
> @@ -437,6 +437,26 @@ options carefully.
>  	only in the submodules of the current repo, but also
>  	in any nested submodules inside those submodules (and so on).
> 
> +--reverse-traversal::

Now, options in description lists are backticked:

`--reverse-traversal`::

> +	This option is only valid for the foreach command and requires
> +	`--recursive`.  Process nested submodules in post-order (deepest
> +	first) rather than the default pre-order.  This is useful for
> +	cleanup operations where nested submodules must be processed
> +	before their parents.

Please start the description with the actual action of the option. Then, you 
can add conditions, context, explanation:

Process nested [...]. This option is only valid [...]. This is useful [...]

Thanks



^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH 0/5] submodule: add 'reversive' traversal options to foreach
  2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
                   ` (4 preceding siblings ...)
  2026-01-31 21:43 ` [PATCH 5/5] doc: document reversive traversal and related modes William Hatfield
@ 2026-02-02 18:52 ` Junio C Hamano
  2026-02-02 21:02   ` William Hatfield
  2026-10-28  0:15 ` [PATCH v2 0/3] submodule: foreach in reverse order, with the superproject William Hatfield
  6 siblings, 1 reply; 15+ messages in thread
From: Junio C Hamano @ 2026-02-02 18:52 UTC (permalink / raw)
  To: William Hatfield; +Cc: git, glencbz, avarab, ps

William Hatfield <whatfield.git@gmail.com> writes:

> This series introduces robust post-order (dependency-ordered) traversal to
> `git submodule foreach` through three new flags: `--reverse-traversal`,
> `--append-superproject`, and the shorthand `--reversive`. These options allow
> users to process nested submodules before their parents and include the
> superproject in the operation, enabling reliable automation for
> dependency-ordered cleanup, builds, and deployment workflows.
>
> Highlights:
> - Implements all new traversal flags in both the C helper and shell script.
> - Provides a comprehensive test suite (t7425) that validates the new behaviors.
> - Updates documentation to describe the new options and their intended use.
>
> These changes make submodule automation more powerful and flexible for advanced
> and dependency-sensitive use cases.

A few comments on the overall structure and concepts.

 * We do not want to see tests in a commit separate from the commit
   that fixes.  The downside of such a layout of a series needs to
   be understood.  An earlier step of a series may introduce a line
   with "test_expect_failure" plus a short summary of what the piece
   fixes, followed by a large amount of code to show exactly what is
   being tested and expected outcome.  But when reading the step
   that comes later that fixes the issue, readers will only see
   changes from "test_expect_failure" to "test_expect success" with
   most of the test to remind them what the issue was hidden away
   from the view, in the post-context of patch hunk.  A commit that
   has both the fix and the test that describes the expectation is
   much easier to work with.

 * The name "--reverse-traversal" makes sense only to those who know
   what the normal traversal order is, but it is far from clear what
   the normal submodule traversal order is, because there is no
   "natural" order to traverse.  Any of the combination of "top
   down/bottom up" "width first/depth first" would make sense
   depending on the application.  If you are doing "bottom up", for
   example, please name it as such.

 * The name "--append-superproject" sounds strange.  It sounds as if
   you are appending the superproject to something else, but I
   suspect that is not what is happening; instead perhaps you are
   leaving the traversal of the superproject at the end, or
   something?

Thanks.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH 0/5] submodule: add 'reversive' traversal options to foreach
  2026-02-02 18:52 ` [PATCH 0/5] submodule: add 'reversive' traversal options to foreach Junio C Hamano
@ 2026-02-02 21:02   ` William Hatfield
  2026-02-02 21:25     ` Junio C Hamano
  0 siblings, 1 reply; 15+ messages in thread
From: William Hatfield @ 2026-02-02 21:02 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, glencbz, avarab, ps


> On Feb 2, 2026, at 1:52 PM, Junio C Hamano <gitster@pobox.com> wrote:
> 
> William Hatfield <whatfield.git@gmail.com> writes:
> 
>> This series introduces robust post-order (dependency-ordered) traversal to
>> `git submodule foreach` through three new flags: `--reverse-traversal`,
>> `--append-superproject`, and the shorthand `--reversive`. These options allow
>> users to process nested submodules before their parents and include the
>> superproject in the operation, enabling reliable automation for
>> dependency-ordered cleanup, builds, and deployment workflows.
>> 
>> Highlights:
>> - Implements all new traversal flags in both the C helper and shell script.
>> - Provides a comprehensive test suite (t7425) that validates the new behaviors.
>> - Updates documentation to describe the new options and their intended use.
>> 
>> These changes make submodule automation more powerful and flexible for advanced
>> and dependency-sensitive use cases.
> 
> A few comments on the overall structure and concepts.
> 
> * We do not want to see tests in a commit separate from the commit
>   that fixes.  The downside of such a layout of a series needs to
>   be understood.  An earlier step of a series may introduce a line
>   with "test_expect_failure" plus a short summary of what the piece
>   fixes, followed by a large amount of code to show exactly what is
>   being tested and expected outcome.  But when reading the step
>   that comes later that fixes the issue, readers will only see
>   changes from "test_expect_failure" to "test_expect success" with
>   most of the test to remind them what the issue was hidden away
>   from the view, in the post-context of patch hunk.  A commit that
>   has both the fix and the test that describes the expectation is
>   much easier to work with.
> 
> * The name "--reverse-traversal" makes sense only to those who know
>   what the normal traversal order is, but it is far from clear what
>   the normal submodule traversal order is, because there is no
>   "natural" order to traverse.  Any of the combination of "top
>   down/bottom up" "width first/depth first" would make sense
>   depending on the application.  If you are doing "bottom up", for
>   example, please name it as such.
> 
> * The name "--append-superproject" sounds strange.  It sounds as if
>   you are appending the superproject to something else, but I
>   suspect that is not what is happening; instead perhaps you are
>   leaving the traversal of the superproject at the end, or
>   something?
> 
> Thanks.

Thank you for the feedback, I will apply it in regard to test fixes in the same commit.

Do you have any recommendation regarding names of these flags?

Do you have a preference for: —postorder-traversal or —bottom-up-traversal?

What about: —include-superpoject or —execute-in-superproject-last?

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH 5/5] doc: document reversive traversal and related modes
  2026-02-01  9:03   ` Jean-Noël AVILA
@ 2026-02-02 21:10     ` William Hatfield
  0 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-02-02 21:10 UTC (permalink / raw)
  To: Jean-Noël AVILA; +Cc: git, ps, glencbz, avarab


> On Feb 1, 2026, at 4:03 AM, Jean-Noël AVILA <jn.avila@free.fr> wrote:
> 
> On Saturday, 31 January 2026 22:43:09 CET William Hatfield wrote:
>> Add documentation for the new --reverse-traversal, --append-superproject,
>> and --reversive flags to git-submodule.adoc. These flags enable post-order
>> traversal through nested submodule hierarchies, which is useful for
>> cleanup operations and dependency-ordered processing.
>> 
>> The flags only take effect when used with --recursive.
>> 
>> Signed-off-by: William Hatfield <whatfield.git@gmail.com>
> 
> Hello,
> 
> git-submodule is being transitioned to `synopsis` style of markup.
> See https://lore.kernel.org/git/
> 05e68e28257cd450463d253abe9b2995759bdc10.1769462744.git.gitgitgadget@gmail.com/
> 
> In the same move, there is some work on style consistency of the manual pages. 
> 
> Below are the remarks on what changed:
> 
>> ---
>> Documentation/git-submodule.adoc | 20 ++++++++++++++++++++
>> 1 file changed, 20 insertions(+)
>> 
>> diff --git a/Documentation/git-submodule.adoc b/Documentation/git-
> submodule.adoc
>> index 95beaee561..2be477952d 100644
>> --- a/Documentation/git-submodule.adoc
>> +++ b/Documentation/git-submodule.adoc
>> @@ -437,6 +437,26 @@ options carefully.
>> only in the submodules of the current repo, but also
>> in any nested submodules inside those submodules (and so on).
>> 
>> +--reverse-traversal::
> 
> Now, options in description lists are backticked:
> 
> `--reverse-traversal`::
> 
>> + This option is only valid for the foreach command and requires
>> + `--recursive`.  Process nested submodules in post-order (deepest
>> + first) rather than the default pre-order.  This is useful for
>> + cleanup operations where nested submodules must be processed
>> + before their parents.
> 
> Please start the description with the actual action of the option. Then, you 
> can add conditions, context, explanation:
> 
> Process nested [...]. This option is only valid [...]. This is useful [...]
> 
> Thanks

Thank you for the input. I will remedy the documentation as described.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH 0/5] submodule: add 'reversive' traversal options to foreach
  2026-02-02 21:02   ` William Hatfield
@ 2026-02-02 21:25     ` Junio C Hamano
  0 siblings, 0 replies; 15+ messages in thread
From: Junio C Hamano @ 2026-02-02 21:25 UTC (permalink / raw)
  To: William Hatfield; +Cc: git, glencbz, avarab, ps

William Hatfield <whatfield.git@gmail.com> writes:

> Do you have any recommendation regarding names of these flags?

Not really.  To be honest, I didn't even check to make sure what the
behaviour was that your "--reverse" was implementing ;-).

> Do you have a preference for: —postorder-traversal or —bottom-up-traversal?

If all three kinds of *order makes sense to the submodule traversal,
then --{pre,in,post}order would make sense.  In other words, does
there exist a good answer to this question: 

    What does "--inorder-traversal" would do and when would it be
    useful, as opposed to --preorder and --postorder?

Otherwise, the distinction between --bottom-up vs --top-down may be
sufficient to explain, and may explain the concept using more
familiar terms to us non-mathematics types.

> What about: —include-superpoject or —execute-in-superproject-last?

Depends on what you are trying to tell the command to do with these
options.

The former sounds as if you are saying "we usually do these things
only in submodules and never in superproject. But with this option I
am telling you to do the same in superproject after you are done
doing them in all the submoudles".

On the other hand, the latter sounds more like "do these things in
each and every repository (both submodules and the superproject),
but make sure the repository for superproject is handled after all
others."

Thanks.


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH v2 0/3] submodule: foreach in reverse order, with the superproject
  2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
                   ` (5 preceding siblings ...)
  2026-02-02 18:52 ` [PATCH 0/5] submodule: add 'reversive' traversal options to foreach Junio C Hamano
@ 2026-10-28  0:15 ` William Hatfield
  2026-10-28  0:15   ` [PATCH v2 1/3] submodule: add --reverse-traversal to foreach William Hatfield
                     ` (2 more replies)
  6 siblings, 3 replies; 15+ messages in thread
From: William Hatfield @ 2026-10-28  0:15 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Jean-Noël AVILA, Patrick Steinhardt,
	Glen Choo, Ævar Arnfjörð Bjarmason,
	William Hatfield

Some commands need to visit nested submodules before the repositories
that contain them.  For example, when committing changes throughout a
project, a superproject can record a submodule's new commit only after
that commit exists.  "git submodule foreach --recursive" visits each
submodule before descending into it, so it cannot do that in one pass.

This adds three options to "git submodule foreach":

 - --reverse-traversal visits submodules in the reverse of their usual
   order.  With --recursive, each submodule is visited after the
   submodules nested in it.

 - --include-superproject also runs the command in the superproject,
   after all the submodules.

 - --reversive is a shorthand for --recursive --reverse-traversal
   --include-superproject.

Take a project "top" with a submodule "mods/a" that has a nested
submodule "inner", and uncommitted changes in all three.  After

    git submodule foreach --reversive 'git commit -a -m update || :'

"git status" shows nothing modified in any of them.  With --recursive
instead of --reversive, "inner" is committed after "mods/a", which
still records its old "inner" and shows it as modified.  t7427 checks
both outcomes.  (The "|| :" is there because "git commit" fails where
there is nothing to commit.  What a failure means is left to the
command, as it is for "git submodule foreach" today: a non-zero exit
status stops the traversal, and the superproject is only visited if
every submodule succeeded.)

This is a new version of "[PATCH 0/5] submodule: add 'reversive'
traversal options to foreach" [1], reworked after the review by Junio
and Jean-Noel:

 - Tests are part of the commits that add the behavior they test,
   instead of a separate first commit with test_expect_failure.

 - --append-superproject is now --include-superproject, which says
   what it does: the command is run in the superproject too, last.  Of
   the variables a command can use in a submodule, Git sets only
   $displaypath there, to the name of the superproject's directory.
   The others are not cleared and may come from the environment.

 - The command run in the superproject keeps the environment that
   selects the repository, such as GIT_DIR and GIT_INDEX_FILE, because
   it is the repository "git submodule foreach" itself runs in, not
   another one being entered.  The tests cover a detached worktree and
   an alternate index, also from a subdirectory.

 - --reverse-traversal keeps its name, but the manual now says what the
   order it reverses is: the submodules by path, each one immediately
   followed by the submodules nested in it.  That was the missing piece
   in the objection that there is no "natural" order.  I do not think
   of it as a walk of a tree, for which "bottom-up" or "post-order"
   would be better names: those say nothing without --recursive, and
   reversing also reverses the order of siblings, which "bottom-up"
   does not promise.  Please tell me if it still reads as unclear.

 - With --recursive and --reverse-traversal, the submodules nested in
   a submodule are looked for before the command runs in it, so a
   command that checks out nested submodules itself does not get them
   visited, unlike in the usual order.  This is documented and tested.

 - The documentation follows the synopsis style that git-submodule.adoc
   has been converted to, describes each option by what it does first,
   and has an example of the resulting order.  The usage message, the
   synopsis and the completion are updated too.

Because the series is organised differently from v1 (three patches
instead of five, with the tests moved), a range-diff against v1 would
only show it as replaced, so there is none.

[1] https://lore.kernel.org/git/20260131214309.1899376-1-whatfield.git@gmail.com/

William Hatfield (3):
  submodule: add --reverse-traversal to foreach
  submodule: add --include-superproject to foreach
  submodule: add --reversive shorthand to foreach

 Documentation/git-submodule.adoc       |  41 ++-
 builtin/submodule--helper.c            | 103 ++++--
 contrib/completion/git-completion.bash |   5 +-
 git-submodule.sh                       |  17 +-
 t/meson.build                          |   1 +
 t/t7427-submodule-foreach-reversive.sh | 429 +++++++++++++++++++++++++
 6 files changed, 573 insertions(+), 23 deletions(-)
 create mode 100755 t/t7427-submodule-foreach-reversive.sh


base-commit: 37b3475fb530c14109211faca33716a173db61ed
-- 
2.56.0


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH v2 1/3] submodule: add --reverse-traversal to foreach
  2026-10-28  0:15 ` [PATCH v2 0/3] submodule: foreach in reverse order, with the superproject William Hatfield
@ 2026-10-28  0:15   ` William Hatfield
  2026-10-28  0:15   ` [PATCH v2 2/3] submodule: add --include-superproject " William Hatfield
  2026-10-28  0:15   ` [PATCH v2 3/3] submodule: add --reversive shorthand " William Hatfield
  2 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-10-28  0:15 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Jean-Noël AVILA, Patrick Steinhardt,
	Glen Choo, Ævar Arnfjörð Bjarmason,
	William Hatfield

When a project has submodules nested in other submodules, and there are
changes in several of them, committing everything with "git submodule
foreach --recursive" does not work in one pass: the command runs in each
submodule before it runs in the submodules nested in it. A nested
submodule thus gets its new commit after its superproject has already
been committed, and the superproject does not record it. To get a clean
working tree, each submodule has to be visited before its superproject,
which today needs a script that lists the submodules and walks the list
backwards.

Teach "git submodule foreach" an option, --reverse-traversal, that
visits the submodules in the reverse of the order it uses otherwise.
With --recursive this is the usual sequence read backwards, so each
submodule is visited after the submodules nested in it, and so before
its superproject. Without --recursive, the order of the submodules
directly in the superproject is reversed.

"Reverse" is meant literally. It does not describe a tree walk
(bottom-up, post-order) that would mean nothing without --recursive,
but the order that "foreach" and "foreach --recursive" currently use,
read backwards. As that order was not written down, document it: the
submodules in the order of their paths, each one followed by the
submodules nested in it.

The submodules of a repository are looked for when it is reached. With
--recursive and --reverse-traversal the nested submodules are therefore
looked for before the command runs in a submodule, not after it, and a
command that checks out nested submodules itself does not get them
visited, unlike in the usual order. This is documented and tested.

Implement it by reversing the list of submodules before walking it,
and by recursing into the nested submodules before, instead of after,
running the command in a submodule.

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 Documentation/git-submodule.adoc       |  25 ++-
 builtin/submodule--helper.c            |  66 +++++---
 contrib/completion/git-completion.bash |   5 +-
 git-submodule.sh                       |   7 +-
 t/meson.build                          |   1 +
 t/t7427-submodule-foreach-reversive.sh | 215 +++++++++++++++++++++++++
 6 files changed, 292 insertions(+), 27 deletions(-)
 create mode 100755 t/t7427-submodule-foreach-reversive.sh

diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc
index e9c81a7170..18658ded41 100644
--- a/Documentation/git-submodule.adoc
+++ b/Documentation/git-submodule.adoc
@@ -18,7 +18,7 @@ git submodule [--quiet] update [<options>] [--] [<path>...]
 git submodule [--quiet] set-branch [<options>] [--] <path>
 git submodule [--quiet] set-url [--] <path> <newurl>
 git submodule [--quiet] summary [<options>] [--] [<path>...]
-git submodule [--quiet] foreach [--recursive] <command>
+git submodule [--quiet] foreach [--recursive] [--reverse-traversal] <command>
 git submodule [--quiet] sync [--recursive] [--] [<path>...]
 git submodule [--quiet] absorbgitdirs [--] [<path>...]
 
@@ -222,7 +222,7 @@ specifications.
 Using the `--submodule=log` option with linkgit:git-diff[1] will provide that
 information too.
 
-`foreach [--recursive] <command>`::
+`foreach [--recursive] [--reverse-traversal] <command>`::
 	Evaluate an arbitrary shell _<command>_ in each checked out submodule.
 	The command has access to the variables `$name`, `$sm_path`, `$displaypath`,
 	`$sha1` and `$toplevel`:
@@ -244,6 +244,12 @@ ignored by this command. Unless given `--quiet`, foreach prints the name
 of each submodule before evaluating the command.
 If `--recursive` is given, submodules are traversed recursively (i.e.
 the given shell command is evaluated in nested submodules as well).
+Submodules are visited in the order of their paths, and with `--recursive`
+each one is immediately followed by the submodules nested in it.
+If `--reverse-traversal` is given, this order is reversed. With
+`--recursive`, each submodule is thus visited after the submodules nested
+in it. Those are looked for before the command runs in the submodule, so
+nested submodules that the command itself checks out are not visited.
 A non-zero return from the command in any submodule causes
 the processing to terminate. This can be overridden by adding ++||:++
 to the end of the command.
@@ -254,6 +260,13 @@ checked out commit for each submodule:
 --------------
 git submodule foreach 'echo $sm_path `git rev-parse HEAD`'
 --------------
++
+And this shows the path of each submodule, visiting the submodules nested
+in a submodule before the submodule itself:
++
+--------------
+git submodule --quiet foreach --recursive --reverse-traversal 'echo $displaypath'
+--------------
 
 `sync [--recursive] [--] [<path>...]`::
 	Synchronize submodules' remote URL configuration setting
@@ -442,6 +455,14 @@ NOTE: See the NOTE above for the `--reference` option.
 	only in the submodules of the current repo, but also
 	in any nested submodules inside those submodules (and so on).
 
+`--reverse-traversal`::
+	Visit the submodules in the reverse of the order they would otherwise
+	be visited in, which is described for `foreach` above. This option is
+	only valid for the `foreach` command.
+	With `--recursive`, nested submodules are visited before the
+	submodule they are nested in; without it, only the order of the
+	submodules directly in the superproject is reversed.
+
 `--depth=<depth>`::
 	Create a 'shallow' clone with a history truncated to the _<depth>_ revisions.
 	This option is valid for `add` and `update` commands. See linkgit:git-clone[1]
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 64412adf9f..8a16e1ceae 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -323,9 +323,38 @@ struct foreach_cb {
 	const char *super_prefix;
 	int quiet;
 	int recursive;
+	int reverse_traversal;
 };
 #define FOREACH_CB_INIT { 0 }
 
+static void recurse_into_nested_submodules(const struct foreach_cb *info,
+					   const char *path,
+					   const char *displaypath)
+{
+	struct child_process cpr = CHILD_PROCESS_INIT;
+
+	cpr.git_cmd = 1;
+	cpr.dir = path;
+	prepare_submodule_repo_env(&cpr.env);
+
+	strvec_pushl(&cpr.args, "submodule--helper", "foreach", "--recursive",
+		     NULL);
+	if (info->reverse_traversal)
+		strvec_push(&cpr.args, "--reverse-traversal");
+	strvec_pushf(&cpr.args, "--super-prefix=%s/", displaypath);
+
+	if (info->quiet)
+		strvec_push(&cpr.args, "--quiet");
+
+	strvec_push(&cpr.args, "--");
+	strvec_pushv(&cpr.args, info->argv);
+
+	if (run_command(&cpr))
+		die(_("run_command returned non-zero status while "
+			"recursing in the nested submodules of %s\n."),
+			displaypath);
+}
+
 static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
 				       void *cb_data)
 {
@@ -396,6 +425,10 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
 		strvec_pushv(&cp.args, info->argv);
 	}
 
+	/* With reverse traversal, visit nested submodules first. */
+	if (info->recursive && info->reverse_traversal)
+		recurse_into_nested_submodules(info, path, displaypath);
+
 	if (!info->quiet)
 		printf(_("Entering '%s'\n"), displaypath);
 
@@ -407,28 +440,8 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
 		child_process_clear(&cp);
 	}
 
-	if (info->recursive) {
-		struct child_process cpr = CHILD_PROCESS_INIT;
-
-		cpr.git_cmd = 1;
-		cpr.dir = path;
-		prepare_submodule_repo_env(&cpr.env);
-
-		strvec_pushl(&cpr.args, "submodule--helper", "foreach", "--recursive",
-			     NULL);
-		strvec_pushf(&cpr.args, "--super-prefix=%s/", displaypath);
-
-		if (info->quiet)
-			strvec_push(&cpr.args, "--quiet");
-
-		strvec_push(&cpr.args, "--");
-		strvec_pushv(&cpr.args, info->argv);
-
-		if (run_command(&cpr))
-			die(_("run_command returned non-zero status while "
-				"recursing in the nested submodules of %s\n."),
-				displaypath);
-	}
+	if (info->recursive && !info->reverse_traversal)
+		recurse_into_nested_submodules(info, path, displaypath);
 
 cleanup:
 	free(displaypath);
@@ -440,15 +453,18 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	struct foreach_cb info = FOREACH_CB_INIT;
 	struct pathspec pathspec = { 0 };
 	struct module_list list = MODULE_LIST_INIT;
+	int i, j;
 	struct option module_foreach_options[] = {
 		OPT__SUPER_PREFIX(&info.super_prefix),
 		OPT__QUIET(&info.quiet, N_("suppress output of entering each submodule command")),
 		OPT_BOOL(0, "recursive", &info.recursive,
 			 N_("recurse into nested submodules")),
+		OPT_BOOL(0, "reverse-traversal", &info.reverse_traversal,
+			 N_("visit submodules in the reverse of the usual order")),
 		OPT_END()
 	};
 	const char *const git_submodule_helper_usage[] = {
-		N_("git submodule foreach [--quiet] [--recursive] [--] <command>"),
+		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--] <command>"),
 		NULL
 	};
 	int ret = 1;
@@ -463,6 +479,10 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	info.argv = argv;
 	info.prefix = prefix;
 
+	if (info.reverse_traversal)
+		for (i = 0, j = list.nr - 1; i < j; i++, j--)
+			SWAP(list.entries[i], list.entries[j]);
+
 	for_each_listed_submodule(&list, runcommand_in_submodule_cb, &info);
 
 	ret = 0;
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index c3a49e2e22..be026abcf3 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -3644,7 +3644,10 @@ _git_submodule ()
 	summary,--*)
 		__gitcomp "--cached --files --summary-limit"
 		;;
-	foreach,--*|sync,--*)
+	foreach,--*)
+		__gitcomp "--recursive --reverse-traversal"
+		;;
+	sync,--*)
 		__gitcomp "--recursive"
 		;;
 	*)
diff --git a/git-submodule.sh b/git-submodule.sh
index 8632194138..efea4e5400 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -14,7 +14,7 @@ USAGE="[--quiet] [--cached]
    or: $dashless [--quiet] set-branch (--default|--branch <branch>) [--] <path>
    or: $dashless [--quiet] set-url [--] <path> <newurl>
    or: $dashless [--quiet] summary [--cached|--files] [--summary-limit <n>] [commit] [--] [<path>...]
-   or: $dashless [--quiet] foreach [--recursive] <command>
+   or: $dashless [--quiet] foreach [--recursive] [--reverse-traversal] <command>
    or: $dashless [--quiet] sync [--recursive] [--] [<path>...]
    or: $dashless [--quiet] absorbgitdirs [--] [<path>...]"
 OPTIONS_SPEC=
@@ -36,6 +36,7 @@ force=
 reference=
 cached=
 recursive=
+reverse_traversal=
 init=
 require_init=
 files=
@@ -173,6 +174,9 @@ cmd_foreach()
 		--recursive)
 			recursive=$1
 			;;
+		--reverse-traversal)
+			reverse_traversal=$1
+			;;
 		-*)
 			usage
 			;;
@@ -186,6 +190,7 @@ cmd_foreach()
 	git ${wt_prefix:+-C "$wt_prefix"} submodule--helper foreach \
 		$quiet \
 		$recursive \
+		$reverse_traversal \
 		-- \
 		"$@"
 }
diff --git a/t/meson.build b/t/meson.build
index 11b3752f00..31a57b85d3 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -926,6 +926,7 @@ integration_tests = [
   't7424-submodule-mixed-ref-formats.sh',
   't7425-submodule-gitdir-path-extension.sh',
   't7426-submodule-get-default-remote.sh',
+  't7427-submodule-foreach-reversive.sh',
   't7450-bad-git-dotfiles.sh',
   't7500-commit-template-squash-signoff.sh',
   't7501-commit-basic-functionality.sh',
diff --git a/t/t7427-submodule-foreach-reversive.sh b/t/t7427-submodule-foreach-reversive.sh
new file mode 100755
index 0000000000..06e341b075
--- /dev/null
+++ b/t/t7427-submodule-foreach-reversive.sh
@@ -0,0 +1,215 @@
+#!/bin/sh
+#
+# Copyright (c) 2026 William Hatfield
+#
+
+test_description='git submodule foreach --reverse-traversal
+
+Check that --reverse-traversal visits submodules in the reverse of their
+usual order, and that, together with --recursive, each submodule is
+visited after the submodules nested in it.
+'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+# Create a commit with some content in repository $1.
+create_content () {
+	echo "$2" >"$1/file" &&
+	git -C "$1" add file &&
+	test_tick &&
+	git -C "$1" commit -m "$1 commit"
+}
+
+# Add submodules $2... to repository $1 and commit.
+add_submodules () {
+	parent=$1 &&
+	shift &&
+	for child in "$@"
+	do
+		git -C "$parent" submodule add ../"$child" "$child" ||
+		return 1
+	done &&
+	test_tick &&
+	git -C "$parent" commit -m "add $*"
+}
+
+test_expect_success 'setup' '
+	git config --global protocol.file.allow always &&
+	#
+	# Resulting layout:
+	#
+	#   top
+	#   +-- sub0
+	#   +-- sub1
+	#   |   `-- sub2
+	#   +-- sub3
+	#   |   +-- sub4
+	#   |   `-- sub5
+	#   `-- sub6
+	#       `-- sub7
+	#           +-- sub8
+	#           `-- sub9
+	#
+	mkdir reversive &&
+	(
+		cd reversive &&
+		for r in top sub0 sub1 sub2 sub3 sub4 sub5 sub6 sub7 sub8 sub9
+		do
+			test_create_repo $r &&
+			create_content $r $r || return 1
+		done &&
+		add_submodules sub1 sub2 &&
+		add_submodules sub3 sub4 sub5 &&
+		add_submodules sub7 sub8 sub9 &&
+		add_submodules sub6 sub7 &&
+		add_submodules top sub0 sub1 sub3 sub6 &&
+		git -C top submodule update --init --recursive
+	)
+'
+
+test_expect_success 'default order is unchanged' '
+	git -C reversive/top submodule --quiet foreach --recursive \
+		"echo \$displaypath" >actual &&
+	cat >expect <<-\EOF &&
+	sub0
+	sub1
+	sub1/sub2
+	sub3
+	sub3/sub4
+	sub3/sub5
+	sub6
+	sub6/sub7
+	sub6/sub7/sub8
+	sub6/sub7/sub9
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--recursive --reverse-traversal visits each submodule before its superproject' '
+	git -C reversive/top submodule --quiet foreach --recursive \
+		--reverse-traversal "echo \$displaypath" >actual &&
+	cat >expect <<-\EOF &&
+	sub6/sub7/sub9
+	sub6/sub7/sub8
+	sub6/sub7
+	sub6
+	sub3/sub5
+	sub3/sub4
+	sub3
+	sub1/sub2
+	sub1
+	sub0
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--reverse-traversal without --recursive only reverses the top level' '
+	git -C reversive/top submodule --quiet foreach \
+		--reverse-traversal "echo \$displaypath" >actual &&
+	cat >expect <<-\EOF &&
+	sub6
+	sub3
+	sub1
+	sub0
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--reverse-traversal announces each submodule after the submodules nested in it' '
+	git -C reversive/top submodule foreach --recursive \
+		--reverse-traversal true >out &&
+	sed -n -e "s/^Entering .\(.*\).\$/\1/p" out >actual &&
+	cat >expect <<-\EOF &&
+	sub6/sub7/sub9
+	sub6/sub7/sub8
+	sub6/sub7
+	sub6
+	sub3/sub5
+	sub3/sub4
+	sub3
+	sub1/sub2
+	sub1
+	sub0
+	EOF
+	test_cmp expect actual &&
+	git -C reversive/top submodule foreach --quiet --recursive \
+		--reverse-traversal true >out 2>&1 &&
+	test_must_be_empty out
+'
+
+test_expect_success 'a failure stops the traversal' '
+	test_must_fail git -C reversive/top submodule --quiet foreach \
+		--recursive --reverse-traversal \
+		"echo \$displaypath; test \$name != sub8" >actual 2>err &&
+	cat >expect <<-\EOF &&
+	sub6/sub7/sub9
+	sub6/sub7/sub8
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'submodules that are not checked out are skipped' '
+	git clone reversive/top partial &&
+	git -C partial submodule update --init sub3 sub6 &&
+	git -C partial/sub6 submodule update --init sub7 &&
+	git -C partial submodule --quiet foreach --recursive \
+		--reverse-traversal "echo \$displaypath" >actual &&
+	cat >expect <<-\EOF &&
+	sub6/sub7
+	sub6
+	sub3
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'submodules checked out by the command are not visited' '
+	git clone reversive/top dynamic &&
+	git -C dynamic submodule update --init sub1 &&
+	git -C dynamic submodule --quiet foreach --recursive \
+		--reverse-traversal \
+		"echo \$displaypath; git submodule update --init --quiet" \
+		>actual &&
+	echo sub1 >expect &&
+	test_cmp expect actual &&
+	git -C dynamic/sub1/sub2 rev-parse --show-toplevel >actual &&
+	(cd dynamic/sub1/sub2 && pwd -P) >expect &&
+	test_cmp expect actual &&
+
+	git clone reversive/top dynamic-usual &&
+	git -C dynamic-usual submodule update --init sub1 &&
+	git -C dynamic-usual submodule --quiet foreach --recursive \
+		"echo \$displaypath; git submodule update --init --quiet" \
+		>actual &&
+	cat >expect <<-\EOF &&
+	sub1
+	sub1/sub2
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'committing in reverse order leaves nested submodules clean' '
+	git clone --recurse-submodules reversive/top rev &&
+	git clone --recurse-submodules reversive/top fwd &&
+	for d in rev fwd
+	do
+		echo changed >$d/sub6/sub7/sub9/file &&
+		echo changed >$d/sub6/sub7/file || return 1
+	done &&
+
+	git -C rev submodule foreach --quiet --recursive \
+		--reverse-traversal "git commit -q -a -m update || :" &&
+	git -C rev/sub6 status --porcelain >rev.sub6 &&
+	git -C rev/sub6/sub7 status --porcelain >rev.sub7 &&
+	test_must_be_empty rev.sub6 &&
+	test_must_be_empty rev.sub7 &&
+
+	git -C fwd submodule foreach --quiet --recursive \
+		"git commit -q -a -m update || :" &&
+	git -C fwd/sub6 status --porcelain >fwd.sub6 &&
+	test_file_not_empty fwd.sub6
+'
+
+test_done
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v2 2/3] submodule: add --include-superproject to foreach
  2026-10-28  0:15 ` [PATCH v2 0/3] submodule: foreach in reverse order, with the superproject William Hatfield
  2026-10-28  0:15   ` [PATCH v2 1/3] submodule: add --reverse-traversal to foreach William Hatfield
@ 2026-10-28  0:15   ` William Hatfield
  2026-10-28  0:15   ` [PATCH v2 3/3] submodule: add --reversive shorthand " William Hatfield
  2 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-10-28  0:15 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Jean-Noël AVILA, Patrick Steinhardt,
	Glen Choo, Ævar Arnfjörð Bjarmason,
	William Hatfield

Some commands, such as committing the changes made in the submodules
of a project, have to be run in the superproject afterwards: its
commit records the state the submodules ended up in. That means running
the same command a second time, outside of "git submodule foreach",
and keeping the two invocations in sync.

Teach "git submodule foreach" an option, --include-superproject, that
evaluates the command in the superproject as well, after all the
submodules. Together with --recursive and --reverse-traversal this
visits each submodule before its superproject, and the top-level
superproject last.

The command is run in the top level of the superproject, wherever
"git submodule foreach" was started in it. The output follows what is
done for submodules and announces the superproject by the name of its
directory, unless given --quiet.

The superproject is the repository "git submodule foreach" itself runs
in, not a different one that is being entered, so, unlike for a
submodule, the environment that selects the repository, such as
GIT_DIR and GIT_INDEX_FILE, is passed on as it is. The command thus
sees the same repository, worktree and index as the one given to
"git submodule foreach", including with a detached worktree or an
alternate index.

Git sets only $displaypath, to the name of the superproject's
directory. $name, $sm_path and $sha1 describe the submodule's entry in
its superproject, which the superproject does not have, and the command
already runs in the top level, which is what $toplevel would tell it.
The other variables are not cleared by this option, though: they are
whatever the environment provides, for instance when this
"git submodule foreach" is itself run by another one.

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 Documentation/git-submodule.adoc       |  20 ++-
 builtin/submodule--helper.c            |  38 ++++-
 contrib/completion/git-completion.bash |   2 +-
 git-submodule.sh                       |   7 +-
 t/t7427-submodule-foreach-reversive.sh | 208 ++++++++++++++++++++++++-
 5 files changed, 265 insertions(+), 10 deletions(-)

diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc
index 18658ded41..5d4b06cb96 100644
--- a/Documentation/git-submodule.adoc
+++ b/Documentation/git-submodule.adoc
@@ -18,7 +18,7 @@ git submodule [--quiet] update [<options>] [--] [<path>...]
 git submodule [--quiet] set-branch [<options>] [--] <path>
 git submodule [--quiet] set-url [--] <path> <newurl>
 git submodule [--quiet] summary [<options>] [--] [<path>...]
-git submodule [--quiet] foreach [--recursive] [--reverse-traversal] <command>
+git submodule [--quiet] foreach [--recursive] [--reverse-traversal] [--include-superproject] <command>
 git submodule [--quiet] sync [--recursive] [--] [<path>...]
 git submodule [--quiet] absorbgitdirs [--] [<path>...]
 
@@ -222,7 +222,7 @@ specifications.
 Using the `--submodule=log` option with linkgit:git-diff[1] will provide that
 information too.
 
-`foreach [--recursive] [--reverse-traversal] <command>`::
+`foreach [--recursive] [--reverse-traversal] [--include-superproject] <command>`::
 	Evaluate an arbitrary shell _<command>_ in each checked out submodule.
 	The command has access to the variables `$name`, `$sm_path`, `$displaypath`,
 	`$sha1` and `$toplevel`:
@@ -250,6 +250,11 @@ If `--reverse-traversal` is given, this order is reversed. With
 `--recursive`, each submodule is thus visited after the submodules nested
 in it. Those are looked for before the command runs in the submodule, so
 nested submodules that the command itself checks out are not visited.
+If `--include-superproject` is given, the command is also evaluated in the
+superproject, that is, at the top level of the repository `foreach` was
+started in, after all the submodules. There, Git sets `$displaypath` to
+the name of that directory and sets none of the other variables listed
+above, although they may come from the environment.
 A non-zero return from the command in any submodule causes
 the processing to terminate. This can be overridden by adding ++||:++
 to the end of the command.
@@ -261,11 +266,12 @@ checked out commit for each submodule:
 git submodule foreach 'echo $sm_path `git rev-parse HEAD`'
 --------------
 +
-And this shows the path of each submodule, visiting the submodules nested
-in a submodule before the submodule itself:
+And this shows the path of each submodule and then of the superproject,
+visiting the submodules nested in a submodule before the submodule itself:
 +
 --------------
-git submodule --quiet foreach --recursive --reverse-traversal 'echo $displaypath'
+git submodule --quiet foreach --recursive --reverse-traversal \
+	--include-superproject 'echo $displaypath'
 --------------
 
 `sync [--recursive] [--] [<path>...]`::
@@ -463,6 +469,10 @@ NOTE: See the NOTE above for the `--reference` option.
 	submodule they are nested in; without it, only the order of the
 	submodules directly in the superproject is reversed.
 
+`--include-superproject`::
+	Also evaluate the command in the superproject, after all the
+	submodules. This option is only valid for the `foreach` command.
+
 `--depth=<depth>`::
 	Create a 'shallow' clone with a history truncated to the _<depth>_ revisions.
 	This option is valid for `add` and `update` commands. See linkgit:git-clone[1]
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 8a16e1ceae..fff5142eda 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -324,6 +324,7 @@ struct foreach_cb {
 	int quiet;
 	int recursive;
 	int reverse_traversal;
+	int include_superproject;
 };
 #define FOREACH_CB_INIT { 0 }
 
@@ -447,6 +448,36 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
 	free(displaypath);
 }
 
+/*
+ * Run the command in the repository foreach was started in. The helper runs
+ * in its top level, so the command does too. Unlike for a submodule, the
+ * environment that selects this repository (GIT_DIR, GIT_INDEX_FILE, ...)
+ * is kept. Only $displaypath is set.
+ */
+static void run_command_in_superproject(const struct foreach_cb *info)
+{
+	struct child_process cp = CHILD_PROCESS_INIT;
+	char *toplevel = xgetcwd();
+	const char *slash = find_last_dir_sep(toplevel);
+	const char *name = slash ? slash + 1 : toplevel;
+
+	cp.use_shell = 1;
+	strvec_pushf(&cp.env, "displaypath=%s", name);
+	strvec_pushv(&cp.args, info->argv);
+
+	if (!info->quiet)
+		printf(_("Entering '%s'\n"), name);
+
+	if (info->argv[0]) {
+		if (run_command(&cp))
+			die(_("command failed in the superproject"));
+	} else {
+		child_process_clear(&cp);
+	}
+
+	free(toplevel);
+}
+
 static int module_foreach(int argc, const char **argv, const char *prefix,
 			  struct repository *repo UNUSED)
 {
@@ -461,10 +492,12 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 			 N_("recurse into nested submodules")),
 		OPT_BOOL(0, "reverse-traversal", &info.reverse_traversal,
 			 N_("visit submodules in the reverse of the usual order")),
+		OPT_BOOL(0, "include-superproject", &info.include_superproject,
+			 N_("also run the command in the superproject, last")),
 		OPT_END()
 	};
 	const char *const git_submodule_helper_usage[] = {
-		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--] <command>"),
+		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--include-superproject] [--] <command>"),
 		NULL
 	};
 	int ret = 1;
@@ -485,6 +518,9 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 
 	for_each_listed_submodule(&list, runcommand_in_submodule_cb, &info);
 
+	if (info.include_superproject)
+		run_command_in_superproject(&info);
+
 	ret = 0;
 cleanup:
 	module_list_release(&list);
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index be026abcf3..f47459d589 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -3645,7 +3645,7 @@ _git_submodule ()
 		__gitcomp "--cached --files --summary-limit"
 		;;
 	foreach,--*)
-		__gitcomp "--recursive --reverse-traversal"
+		__gitcomp "--recursive --reverse-traversal --include-superproject"
 		;;
 	sync,--*)
 		__gitcomp "--recursive"
diff --git a/git-submodule.sh b/git-submodule.sh
index efea4e5400..964f5a8bf0 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -14,7 +14,7 @@ USAGE="[--quiet] [--cached]
    or: $dashless [--quiet] set-branch (--default|--branch <branch>) [--] <path>
    or: $dashless [--quiet] set-url [--] <path> <newurl>
    or: $dashless [--quiet] summary [--cached|--files] [--summary-limit <n>] [commit] [--] [<path>...]
-   or: $dashless [--quiet] foreach [--recursive] [--reverse-traversal] <command>
+   or: $dashless [--quiet] foreach [--recursive] [--reverse-traversal] [--include-superproject] <command>
    or: $dashless [--quiet] sync [--recursive] [--] [<path>...]
    or: $dashless [--quiet] absorbgitdirs [--] [<path>...]"
 OPTIONS_SPEC=
@@ -37,6 +37,7 @@ reference=
 cached=
 recursive=
 reverse_traversal=
+include_superproject=
 init=
 require_init=
 files=
@@ -177,6 +178,9 @@ cmd_foreach()
 		--reverse-traversal)
 			reverse_traversal=$1
 			;;
+		--include-superproject)
+			include_superproject=$1
+			;;
 		-*)
 			usage
 			;;
@@ -191,6 +195,7 @@ cmd_foreach()
 		$quiet \
 		$recursive \
 		$reverse_traversal \
+		$include_superproject \
 		-- \
 		"$@"
 }
diff --git a/t/t7427-submodule-foreach-reversive.sh b/t/t7427-submodule-foreach-reversive.sh
index 06e341b075..a53ea0b851 100755
--- a/t/t7427-submodule-foreach-reversive.sh
+++ b/t/t7427-submodule-foreach-reversive.sh
@@ -3,11 +3,13 @@
 # Copyright (c) 2026 William Hatfield
 #
 
-test_description='git submodule foreach --reverse-traversal
+test_description='git submodule foreach --reverse-traversal and --include-superproject
 
 Check that --reverse-traversal visits submodules in the reverse of their
 usual order, and that, together with --recursive, each submodule is
-visited after the submodules nested in it.
+visited after the submodules nested in it. Check that
+--include-superproject also runs the command in the superproject, after
+all the submodules.
 '
 
 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
@@ -212,4 +214,206 @@ test_expect_success 'committing in reverse order leaves nested submodules clean'
 	test_file_not_empty fwd.sub6
 '
 
+test_expect_success '--include-superproject runs the command in the superproject last' '
+	git -C reversive/top submodule --quiet foreach --recursive \
+		--include-superproject "echo \$displaypath" >actual &&
+	cat >expect <<-\EOF &&
+	sub0
+	sub1
+	sub1/sub2
+	sub3
+	sub3/sub4
+	sub3/sub5
+	sub6
+	sub6/sub7
+	sub6/sub7/sub8
+	sub6/sub7/sub9
+	top
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject combines with --reverse-traversal' '
+	git -C reversive/top submodule --quiet foreach --recursive \
+		--reverse-traversal --include-superproject \
+		"echo \$displaypath" >actual &&
+	cat >expect <<-\EOF &&
+	sub6/sub7/sub9
+	sub6/sub7/sub8
+	sub6/sub7
+	sub6
+	sub3/sub5
+	sub3/sub4
+	sub3
+	sub1/sub2
+	sub1
+	sub0
+	top
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject announces the superproject' '
+	git -C reversive/top submodule foreach --include-superproject \
+		true >out &&
+	test_grep "^Entering .top.$" out &&
+	git -C reversive/top submodule foreach --quiet \
+		--include-superproject true >out &&
+	test_grep ! Entering out
+'
+
+test_expect_success '--include-superproject in a repository without submodules' '
+	test_create_repo "no subs" &&
+	git -C "no subs" submodule --quiet foreach --include-superproject \
+		"echo \"[\$displaypath]\"" >actual &&
+	echo "[no subs]" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject uses the repository it is run in' '
+	git -C reversive/top/sub6 submodule --quiet foreach --recursive \
+		--include-superproject "echo \$displaypath" >actual &&
+	cat >expect <<-\EOF &&
+	sub7
+	sub7/sub8
+	sub7/sub9
+	sub6
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject runs from a subdirectory at the top level' '
+	mkdir -p reversive/top/dir &&
+	git -C reversive/top/dir submodule --quiet foreach \
+		--include-superproject "pwd -P" >out &&
+	tail -n 1 out >actual &&
+	(cd reversive/top && pwd -P) >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject sets only $displaypath and keeps the rest of the environment' '
+	git -C "no subs" submodule --quiet foreach --include-superproject \
+		"echo \"[\$displaypath][\$name][\$sm_path][\$sha1][\$toplevel]\"" \
+		>actual &&
+	echo "[no subs][][][][]" >expect &&
+	test_cmp expect actual &&
+
+	write_script nested-foreach <<-\EOF &&
+	git submodule --quiet foreach --include-superproject \
+		'\''printf "%s|%s|%s|%s\n" "$displaypath" "$sm_path" "$sha1" "$toplevel"'\''
+	EOF
+	git -C reversive/top submodule --quiet foreach \
+		"if test \"\$name\" = sub0; then \"$TRASH_DIRECTORY/nested-foreach\"; fi" \
+		>actual &&
+	sub0_oid=$(git -C reversive/top rev-parse HEAD:sub0) &&
+	top=$(cd reversive/top && pwd -P) &&
+	echo "sub0|sub0|$sub0_oid|$top" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject preserves a detached worktree' '
+	git clone --bare reversive/top detached.git &&
+	mkdir detached &&
+	(
+		cd detached &&
+		GIT_DIR="$(pwd)/../detached.git" &&
+		GIT_WORK_TREE="$(pwd)" &&
+		export GIT_DIR GIT_WORK_TREE &&
+		git checkout --quiet &&
+		git submodule update --init &&
+		git submodule --quiet foreach --include-superproject \
+			"git rev-parse --show-toplevel" >../actual
+	) &&
+	(
+		cd detached &&
+		for sub in sub0 sub1 sub3 sub6
+		do
+			(cd "$sub" && pwd -P) || return 1
+		done &&
+		pwd -P
+	) >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject preserves the selected index' '
+	git clone --recurse-submodules reversive/top index-super &&
+	cp index-super/.git/index index-super/custom-index &&
+	echo content >index-super/alternate-file &&
+	GIT_INDEX_FILE=custom-index git -C index-super add alternate-file &&
+	mkdir index-super/dir &&
+	for dir in index-super index-super/dir
+	do
+		GIT_INDEX_FILE=custom-index git -C $dir submodule --quiet \
+			foreach --include-superproject \
+			"if test \"\$displaypath\" = index-super; then
+				git ls-files --error-unmatch alternate-file
+			else
+				test -z \"\$GIT_INDEX_FILE\"
+			fi" >actual &&
+		echo alternate-file >expect &&
+		test_cmp expect actual || return 1
+	done &&
+	test_must_fail git -C index-super ls-files --error-unmatch \
+		alternate-file >/dev/null 2>&1
+'
+
+test_expect_success '--include-superproject commits from the selected index' '
+	cp index-super/.git/index default-index-before &&
+	test_tick &&
+	GIT_INDEX_FILE=custom-index git -C index-super submodule --quiet \
+		foreach --include-superproject \
+		"if test \"\$displaypath\" = index-super; then
+			git commit -q -m alternate-index
+		else
+			test -z \"\$GIT_INDEX_FILE\"
+		fi" &&
+	git -C index-super show HEAD:alternate-file >actual &&
+	echo content >expect &&
+	test_cmp expect actual &&
+	test_cmp_bin default-index-before index-super/.git/index
+'
+
+test_expect_success '--include-superproject does not run after a failure' '
+	test_must_fail git -C reversive/top submodule --quiet foreach \
+		--recursive --reverse-traversal --include-superproject \
+		"echo \$displaypath; test \$name != sub8" >actual 2>err &&
+	cat >expect <<-\EOF &&
+	sub6/sub7/sub9
+	sub6/sub7/sub8
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--include-superproject reports a failing superproject' '
+	test_must_fail git -C reversive/top submodule --quiet foreach \
+		--include-superproject "test \$displaypath != top" 2>err &&
+	test_grep "command failed in the superproject" err
+'
+
+test_expect_success 'committing in the submodules and the superproject leaves nothing modified' '
+	git clone --recurse-submodules reversive/top full &&
+	echo changed >full/file &&
+	echo changed >full/sub0/file &&
+	echo changed >full/sub3/sub5/file &&
+	echo changed >full/sub6/sub7/sub9/file &&
+	git -C full submodule foreach --quiet --recursive \
+		--reverse-traversal --include-superproject \
+		"git commit -q -a -m \"update \$displaypath\" || :" &&
+	git -C full status --porcelain >actual &&
+	test_must_be_empty actual &&
+	for path in sub0 sub3/sub5 sub3 sub6/sub7/sub9 sub6/sub7 sub6
+	do
+		case "$path" in
+		*/*) parent=${path%/*} && child=${path##*/} ;;
+		*) parent=. && child=$path ;;
+		esac &&
+		current=$(git -C "full/$path" rev-parse HEAD) &&
+		original=$(git -C "reversive/top/$path" rev-parse HEAD) &&
+		test "$current" != "$original" &&
+		echo "$current" >expect &&
+		git -C "full/$parent" rev-parse "HEAD:$child" >actual &&
+		test_cmp expect actual || return 1
+	done
+'
+
 test_done
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH v2 3/3] submodule: add --reversive shorthand to foreach
  2026-10-28  0:15 ` [PATCH v2 0/3] submodule: foreach in reverse order, with the superproject William Hatfield
  2026-10-28  0:15   ` [PATCH v2 1/3] submodule: add --reverse-traversal to foreach William Hatfield
  2026-10-28  0:15   ` [PATCH v2 2/3] submodule: add --include-superproject " William Hatfield
@ 2026-10-28  0:15   ` William Hatfield
  2 siblings, 0 replies; 15+ messages in thread
From: William Hatfield @ 2026-10-28  0:15 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Jean-Noël AVILA, Patrick Steinhardt,
	Glen Choo, Ævar Arnfjörð Bjarmason,
	William Hatfield

Visiting each submodule before its superproject, ending with the
top-level superproject, takes all of --recursive, --reverse-traversal
and --include-superproject. Those are three options to remember and to
type for what is, for this purpose, a single thing.

Add --reversive as a shorthand for the three of them. The options keep
working on their own, for the cases that need only one or two of them.

Signed-off-by: William Hatfield <whatfield.git@gmail.com>
---
 Documentation/git-submodule.adoc       | 14 ++++++++++----
 builtin/submodule--helper.c            | 11 ++++++++++-
 contrib/completion/git-completion.bash |  2 +-
 git-submodule.sh                       |  7 ++++++-
 t/t7427-submodule-foreach-reversive.sh | 14 ++++++++++++--
 5 files changed, 39 insertions(+), 9 deletions(-)

diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc
index 5d4b06cb96..8f4bc002cc 100644
--- a/Documentation/git-submodule.adoc
+++ b/Documentation/git-submodule.adoc
@@ -18,7 +18,7 @@ git submodule [--quiet] update [<options>] [--] [<path>...]
 git submodule [--quiet] set-branch [<options>] [--] <path>
 git submodule [--quiet] set-url [--] <path> <newurl>
 git submodule [--quiet] summary [<options>] [--] [<path>...]
-git submodule [--quiet] foreach [--recursive] [--reverse-traversal] [--include-superproject] <command>
+git submodule [--quiet] foreach [--recursive] [--reverse-traversal] [--include-superproject] [--reversive] <command>
 git submodule [--quiet] sync [--recursive] [--] [<path>...]
 git submodule [--quiet] absorbgitdirs [--] [<path>...]
 
@@ -222,7 +222,7 @@ specifications.
 Using the `--submodule=log` option with linkgit:git-diff[1] will provide that
 information too.
 
-`foreach [--recursive] [--reverse-traversal] [--include-superproject] <command>`::
+`foreach [--recursive] [--reverse-traversal] [--include-superproject] [--reversive] <command>`::
 	Evaluate an arbitrary shell _<command>_ in each checked out submodule.
 	The command has access to the variables `$name`, `$sm_path`, `$displaypath`,
 	`$sha1` and `$toplevel`:
@@ -255,6 +255,8 @@ superproject, that is, at the top level of the repository `foreach` was
 started in, after all the submodules. There, Git sets `$displaypath` to
 the name of that directory and sets none of the other variables listed
 above, although they may come from the environment.
+`--reversive` is a shorthand for giving `--recursive`,
+`--reverse-traversal` and `--include-superproject` together.
 A non-zero return from the command in any submodule causes
 the processing to terminate. This can be overridden by adding ++||:++
 to the end of the command.
@@ -270,8 +272,7 @@ And this shows the path of each submodule and then of the superproject,
 visiting the submodules nested in a submodule before the submodule itself:
 +
 --------------
-git submodule --quiet foreach --recursive --reverse-traversal \
-	--include-superproject 'echo $displaypath'
+git submodule --quiet foreach --reversive 'echo $displaypath'
 --------------
 
 `sync [--recursive] [--] [<path>...]`::
@@ -473,6 +474,11 @@ NOTE: See the NOTE above for the `--reference` option.
 	Also evaluate the command in the superproject, after all the
 	submodules. This option is only valid for the `foreach` command.
 
+`--reversive`::
+	A shorthand for `--recursive --reverse-traversal
+	--include-superproject`. This option is only valid for the `foreach`
+	command.
+
 `--depth=<depth>`::
 	Create a 'shallow' clone with a history truncated to the _<depth>_ revisions.
 	This option is valid for `add` and `update` commands. See linkgit:git-clone[1]
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index fff5142eda..f923d2e926 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -485,6 +485,7 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	struct pathspec pathspec = { 0 };
 	struct module_list list = MODULE_LIST_INIT;
 	int i, j;
+	int reversive = 0;
 	struct option module_foreach_options[] = {
 		OPT__SUPER_PREFIX(&info.super_prefix),
 		OPT__QUIET(&info.quiet, N_("suppress output of entering each submodule command")),
@@ -494,10 +495,12 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 			 N_("visit submodules in the reverse of the usual order")),
 		OPT_BOOL(0, "include-superproject", &info.include_superproject,
 			 N_("also run the command in the superproject, last")),
+		OPT_BOOL(0, "reversive", &reversive,
+			 N_("shorthand for --recursive --reverse-traversal --include-superproject")),
 		OPT_END()
 	};
 	const char *const git_submodule_helper_usage[] = {
-		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--include-superproject] [--] <command>"),
+		N_("git submodule foreach [--quiet] [--recursive] [--reverse-traversal] [--include-superproject] [--reversive] [--] <command>"),
 		NULL
 	};
 	int ret = 1;
@@ -505,6 +508,12 @@ static int module_foreach(int argc, const char **argv, const char *prefix,
 	argc = parse_options(argc, argv, prefix, module_foreach_options,
 			     git_submodule_helper_usage, 0);
 
+	if (reversive) {
+		info.recursive = 1;
+		info.reverse_traversal = 1;
+		info.include_superproject = 1;
+	}
+
 	if (module_list_compute(NULL, prefix, &pathspec, &list) < 0)
 		goto cleanup;
 
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index f47459d589..69a31316ca 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -3645,7 +3645,7 @@ _git_submodule ()
 		__gitcomp "--cached --files --summary-limit"
 		;;
 	foreach,--*)
-		__gitcomp "--recursive --reverse-traversal --include-superproject"
+		__gitcomp "--recursive --reverse-traversal --include-superproject --reversive"
 		;;
 	sync,--*)
 		__gitcomp "--recursive"
diff --git a/git-submodule.sh b/git-submodule.sh
index 964f5a8bf0..c86cbe76d2 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -14,7 +14,7 @@ USAGE="[--quiet] [--cached]
    or: $dashless [--quiet] set-branch (--default|--branch <branch>) [--] <path>
    or: $dashless [--quiet] set-url [--] <path> <newurl>
    or: $dashless [--quiet] summary [--cached|--files] [--summary-limit <n>] [commit] [--] [<path>...]
-   or: $dashless [--quiet] foreach [--recursive] [--reverse-traversal] [--include-superproject] <command>
+   or: $dashless [--quiet] foreach [--recursive] [--reverse-traversal] [--include-superproject] [--reversive] <command>
    or: $dashless [--quiet] sync [--recursive] [--] [<path>...]
    or: $dashless [--quiet] absorbgitdirs [--] [<path>...]"
 OPTIONS_SPEC=
@@ -38,6 +38,7 @@ cached=
 recursive=
 reverse_traversal=
 include_superproject=
+reversive=
 init=
 require_init=
 files=
@@ -181,6 +182,9 @@ cmd_foreach()
 		--include-superproject)
 			include_superproject=$1
 			;;
+		--reversive)
+			reversive=$1
+			;;
 		-*)
 			usage
 			;;
@@ -196,6 +200,7 @@ cmd_foreach()
 		$recursive \
 		$reverse_traversal \
 		$include_superproject \
+		$reversive \
 		-- \
 		"$@"
 }
diff --git a/t/t7427-submodule-foreach-reversive.sh b/t/t7427-submodule-foreach-reversive.sh
index a53ea0b851..d5aa29f08e 100755
--- a/t/t7427-submodule-foreach-reversive.sh
+++ b/t/t7427-submodule-foreach-reversive.sh
@@ -3,13 +3,14 @@
 # Copyright (c) 2026 William Hatfield
 #
 
-test_description='git submodule foreach --reverse-traversal and --include-superproject
+test_description='git submodule foreach --reverse-traversal, --include-superproject and --reversive
 
 Check that --reverse-traversal visits submodules in the reverse of their
 usual order, and that, together with --recursive, each submodule is
 visited after the submodules nested in it. Check that
 --include-superproject also runs the command in the superproject, after
-all the submodules.
+all the submodules, and that --reversive is a shorthand for all three of
+these options.
 '
 
 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
@@ -416,4 +417,13 @@ test_expect_success 'committing in the submodules and the superproject leaves no
 	done
 '
 
+test_expect_success '--reversive is --recursive --reverse-traversal --include-superproject' '
+	git -C reversive/top submodule --quiet foreach --recursive \
+		--reverse-traversal --include-superproject \
+		"echo \$displaypath" >expect &&
+	git -C reversive/top submodule --quiet foreach --reversive \
+		"echo \$displaypath" >actual &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2026-10-28  0:15 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-31 21:43 [PATCH 0/5] submodule: add 'reversive' traversal options to foreach William Hatfield
2026-01-31 21:43 ` [PATCH 1/5] t7425: add tests for reversive submodule traversal William Hatfield
2026-01-31 21:43 ` [PATCH 2/5] submodule: teach and plumb reverse-traversal behavior William Hatfield
2026-01-31 21:43 ` [PATCH 3/5] submodule: teach and plumb append-superproject behavior William Hatfield
2026-01-31 21:43 ` [PATCH 4/5] submodule: introduce reversive shorthand mode William Hatfield
2026-01-31 21:43 ` [PATCH 5/5] doc: document reversive traversal and related modes William Hatfield
2026-02-01  9:03   ` Jean-Noël AVILA
2026-02-02 21:10     ` William Hatfield
2026-02-02 18:52 ` [PATCH 0/5] submodule: add 'reversive' traversal options to foreach Junio C Hamano
2026-02-02 21:02   ` William Hatfield
2026-02-02 21:25     ` Junio C Hamano
2026-10-28  0:15 ` [PATCH v2 0/3] submodule: foreach in reverse order, with the superproject William Hatfield
2026-10-28  0:15   ` [PATCH v2 1/3] submodule: add --reverse-traversal to foreach William Hatfield
2026-10-28  0:15   ` [PATCH v2 2/3] submodule: add --include-superproject " William Hatfield
2026-10-28  0:15   ` [PATCH v2 3/3] submodule: add --reversive shorthand " William Hatfield

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox