diff --git a/Makefile b/Makefile
index a2a6e1f20f6..c07cfb75532 100644
--- a/Makefile
+++ b/Makefile
@@ -757,6 +757,7 @@ TEST_BUILTINS_OBJS += test-string-list.o
 TEST_BUILTINS_OBJS += test-submodule-config.o
 TEST_BUILTINS_OBJS += test-submodule-nested-repo-config.o
 TEST_BUILTINS_OBJS += test-subprocess.o
+TEST_BUILTINS_OBJS += test-touch.o
 TEST_BUILTINS_OBJS += test-trace2.o
 TEST_BUILTINS_OBJS += test-urlmatch-normalization.o
 TEST_BUILTINS_OBJS += test-userdiff.o
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index af879e4a5d7..1ad8d5fbd82 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -73,6 +73,7 @@ static struct test_cmd cmds[] = {
 	{ "submodule-config", cmd__submodule_config },
 	{ "submodule-nested-repo-config", cmd__submodule_nested_repo_config },
 	{ "subprocess", cmd__subprocess },
+	{ "touch", cmd__touch },
 	{ "trace2", cmd__trace2 },
 	{ "userdiff", cmd__userdiff },
 	{ "urlmatch-normalization", cmd__urlmatch_normalization },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 6c5134b46d9..58fde0a62e5 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -63,6 +63,7 @@ int cmd__string_list(int argc, const char **argv);
 int cmd__submodule_config(int argc, const char **argv);
 int cmd__submodule_nested_repo_config(int argc, const char **argv);
 int cmd__subprocess(int argc, const char **argv);
+int cmd__touch(int argc, const char **argv);
 int cmd__trace2(int argc, const char **argv);
 int cmd__userdiff(int argc, const char **argv);
 int cmd__urlmatch_normalization(int argc, const char **argv);
diff --git a/t/helper/test-touch.c b/t/helper/test-touch.c
new file mode 100644
index 00000000000..e9b3b754f1f
--- /dev/null
+++ b/t/helper/test-touch.c
@@ -0,0 +1,126 @@
+/*
+ * test-touch.c: variation on /usr/bin/touch to speed up tests
+ * with a large number of files (primarily on Windows where child
+ * process are very, very expensive).
+ */
+
+#include "test-tool.h"
+#include "cache.h"
+#include "parse-options.h"
+
+char *seq_pattern;
+int seq_start = 1;
+int seq_count = 1;
+
+static int do_touch_one(const char *path)
+{
+	int fd;
+
+	if (!utime(path, NULL))
+		return 0;
+
+	if (errno != ENOENT) {
+		warning_errno("could not touch '%s'", path);
+		return 0;
+	}
+
+	fd = open(path, O_RDWR | O_CREAT, 0644);
+	if (fd == -1) {
+		warning_errno("could not create '%s'", path);
+		return 0;
+	}
+	close(fd);
+
+	return 0;
+}
+
+/*
+ * Touch a series of files.  We assume that any required subdirs
+ * already exist.  This function allows us to replace the following
+ * test script fragment:
+ *
+ *    for i in $(test_seq 1 10000); do touch 10000_files/$i; done &&
+ *
+ * with a single process:
+ *
+ *    test-tool touch sequence --pattern="10000_files/%d" --start=1 --count=10000
+ *
+ * which is much faster on Windows.
+ */
+static int do_sequence(void)
+{
+	struct strbuf buf = STRBUF_INIT;
+	int k;
+
+	for (k = seq_start; k < seq_start + seq_count; k++) {
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, seq_pattern, k);
+
+		if (do_touch_one(buf.buf))
+			return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Read a list of pathnames from stdin and touch them.  We assume that
+ * any required subdirs already exist.
+ */
+static int do_stdin(void)
+{
+	struct strbuf buf = STRBUF_INIT;
+
+	while (strbuf_getline(&buf, stdin) != EOF && buf.len)
+		if (do_touch_one(buf.buf))
+			return 1;
+
+	return 0;
+}
+
+int cmd__touch(int argc, const char **argv)
+{
+	const char *touch_usage[] = {
+		N_("test-tool touch sequence <pattern> <start> <count>"),
+		N_("test-tool touch stdin"),
+		NULL,
+	};
+
+	struct option touch_options[] = {
+		OPT_GROUP(N_("sequence")),
+		OPT_STRING(0, "pattern", &seq_pattern, N_("format"),
+			   N_("sequence pathname pattern")),
+		OPT_INTEGER(0, "start", &seq_start,
+			    N_("sequence starting value")),
+		OPT_INTEGER(0, "count", &seq_count,
+			    N_("sequence count")),
+		OPT_END()
+	};
+
+	const char *subcmd;
+
+	if (argc < 2)
+		usage_with_options(touch_usage, touch_options);
+	if (argc == 2 && !strcmp(argv[1], "-h"))
+		usage_with_options(touch_usage, touch_options);
+
+	subcmd = argv[1];
+	argv--;
+	argc++;
+
+	argc = parse_options(argc, argv, NULL, touch_options, touch_usage, 0);
+
+	if (!strcmp(subcmd, "sequence")) {
+		if (!seq_pattern || !strstr(seq_pattern, "%d"))
+			die("invalid sequence pattern");
+		if (seq_count < 1)
+			die("invalid sequence count: %d", seq_count);
+		return !!do_sequence();
+	}
+
+	if (!strcmp(subcmd, "stdin")) {
+		return !!do_stdin();
+	}
+
+	die("Unhandled subcommand: '%s'", subcmd);
+}
