Show changes to 12 files +490 −59
Documentation/fetch-options.txt, builtin/fetch.c, builtin/pull.c, run-command.c, run-command.h, strbuf.c, strbuf.h, submodule.c, submodule.h, t/t0061-run-command.sh, t/t5526-fetch-submodules.sh, test-run-command.c
diff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt
index 45583d8..6b109f6 100644
--- a/Documentation/fetch-options.txt
+++ b/Documentation/fetch-options.txt
@@ -100,6 +100,13 @@ ifndef::git-pull[]
reference to a commit that isn't already in the local submodule
clone.
+-j::
+--jobs=<n>::
+ Number of parallel children to be used for fetching submodules.
+ Each will fetch from different submodules, such that fetching many
+ submodules will be faster. By default submodules will be fetched
+ one at a time.
+
--no-recurse-submodules::
Disable recursive fetching of submodules (this has the same effect as
using the '--recurse-submodules=no' option).
diff --git a/builtin/fetch.c b/builtin/fetch.c
index ee1f1a9..f28eac6 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -37,6 +37,7 @@ static int prune = -1; /* unspecified */
static int all, append, dry_run, force, keep, multiple, update_head_ok, verbosity;
static int progress = -1, recurse_submodules = RECURSE_SUBMODULES_DEFAULT;
static int tags = TAGS_DEFAULT, unshallow, update_shallow;
+static int max_children = 1;
static const char *depth;
static const char *upload_pack;
static struct strbuf default_rla = STRBUF_INIT;
@@ -99,6 +100,8 @@ static struct option builtin_fetch_options[] = {
N_("fetch all tags and associated objects"), TAGS_SET),
OPT_SET_INT('n', NULL, &tags,
N_("do not fetch all tags (--no-tags)"), TAGS_UNSET),
+ OPT_INTEGER('j', "jobs", &max_children,
+ N_("number of submodules fetched in parallel")),
OPT_BOOL('p', "prune", &prune,
N_("prune remote-tracking branches no longer on remote")),
{ OPTION_CALLBACK, 0, "recurse-submodules", NULL, N_("on-demand"),
@@ -1217,7 +1220,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
result = fetch_populated_submodules(&options,
submodule_prefix,
recurse_submodules,
- verbosity < 0);
+ verbosity < 0,
+ max_children);
argv_array_clear(&options);
}
diff --git a/builtin/pull.c b/builtin/pull.c
index 722a83c..f0af196 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -94,6 +94,7 @@ static int opt_force;
static char *opt_tags;
static char *opt_prune;
static char *opt_recurse_submodules;
+static char *max_children;
static int opt_dry_run;
static char *opt_keep;
static char *opt_depth;
@@ -177,6 +178,9 @@ static struct option pull_options[] = {
N_("on-demand"),
N_("control recursive fetching of submodules"),
PARSE_OPT_OPTARG),
+ OPT_PASSTHRU('j', "jobs", &max_children, N_("n"),
+ N_("number of submodules pulled in parallel"),
+ PARSE_OPT_OPTARG),
OPT_BOOL(0, "dry-run", &opt_dry_run,
N_("dry run")),
OPT_PASSTHRU('k', "keep", &opt_keep, NULL,
@@ -524,6 +528,8 @@ static int run_fetch(const char *repo, const char **refspecs)
argv_array_push(&args, opt_prune);
if (opt_recurse_submodules)
argv_array_push(&args, opt_recurse_submodules);
+ if (max_children)
+ argv_array_push(&args, max_children);
if (opt_dry_run)
argv_array_push(&args, "--dry-run");
if (opt_keep)
diff --git a/run-command.c b/run-command.c
index 28e1d55..6f6f9fb 100644
--- a/run-command.c
+++ b/run-command.c
@@ -232,6 +232,35 @@ static inline void set_cloexec(int fd)
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
+static int determine_return_value(int wait_status,
+ int *result,
+ int *error_code,
+ const char *argv0)
+{
+ if (WIFSIGNALED(wait_status)) {
+ *result = WTERMSIG(wait_status);
+ if (*result != SIGINT && *result != SIGQUIT)
+ error("%s died of signal %d", argv0, *result);
+ /*
+ * This return value is chosen so that code & 0xff
+ * mimics the exit code that a POSIX shell would report for
+ * a program that died from this signal.
+ */
+ *result += 128;
+ } else if (WIFEXITED(wait_status)) {
+ *result = WEXITSTATUS(wait_status);
+ /*
+ * Convert special exit code when execvp failed.
+ */
+ if (*result == 127) {
+ *result = -1;
+ *error_code = ENOENT;
+ }
+ } else
+ return 1;
+ return 0;
+}
+
static int wait_or_whine(pid_t pid, const char *argv0)
{
int status, code = -1;
@@ -244,29 +273,10 @@ static int wait_or_whine(pid_t pid, const char *argv0)
if (waiting < 0) {
failed_errno = errno;
error("waitpid for %s failed: %s", argv0, strerror(errno));
- } else if (waiting != pid) {
- error("waitpid is confused (%s)", argv0);
- } else if (WIFSIGNALED(status)) {
- code = WTERMSIG(status);
- if (code != SIGINT && code != SIGQUIT)
- error("%s died of signal %d", argv0, code);
- /*
- * This return value is chosen so that code & 0xff
- * mimics the exit code that a POSIX shell would report for
- * a program that died from this signal.
- */
- code += 128;
- } else if (WIFEXITED(status)) {
- code = WEXITSTATUS(status);
- /*
- * Convert special exit code when execvp failed.
- */
- if (code == 127) {
- code = -1;
- failed_errno = ENOENT;
- }
} else {
- error("waitpid is confused (%s)", argv0);
+ if (waiting != pid
+ || (determine_return_value(status, &code, &failed_errno, argv0) < 0))
+ error("waitpid is confused (%s)", argv0);
}
clear_child_for_cleanup(pid);
@@ -852,3 +862,227 @@ int capture_command(struct child_process *cmd, struct strbuf *buf, size_t hint)
close(cmd->out);
return finish_command(cmd);
}
+
+static void unblock_fd(int fd)
+{
+ int flags = fcntl(fd, F_GETFL);
+ if (flags < 0) {
+ warning("Could not get file status flags, "
+ "output will be degraded");
+ return;
+ }
+ if (fcntl(fd, F_SETFL, flags | O_NONBLOCK)) {
+ warning("Could not set file status flags, "
+ "output will be degraded");
+ return;
+ }
+}
+
+struct parallel_processes {
+ int max_number_processes;
+ void *data;
+ get_next_task fn;
+ handle_child_starting_failure fn_err;
+ handle_child_return_value fn_exit;
+
+ int nr_processes;
+ int all_tasks_started;
+ int foreground_child;
+ char *slots;
+ struct child_process *children;
+ struct pollfd *pfd;
+ struct strbuf *err;
+ struct strbuf finished_children;
+};
+
+static void run_processes_parallel_init(struct parallel_processes *pp,
+ int n, void *data,
+ get_next_task fn,
+ handle_child_starting_failure fn_err,
+ handle_child_return_value fn_exit)
+{
+ int i;
+
+ pp->max_number_processes = n;
+ pp->data = data;
+ pp->fn = fn;
+ pp->fn_err = fn_err;
+ pp->fn_exit = fn_exit;
+
+ pp->nr_processes = 0;
+ pp->all_tasks_started = 0;
+ pp->foreground_child = 0;
+ pp->slots = xcalloc(n, sizeof(*pp->slots));
+ pp->children = xcalloc(n, sizeof(*pp->children));
+ pp->pfd = xcalloc(n, sizeof(*pp->pfd));
+ pp->err = xcalloc(n, sizeof(*pp->err));
+ strbuf_init(&pp->finished_children, 0);
+
+ for (i = 0; i < n; i++) {
+ strbuf_init(&pp->err[i], 0);
+ pp->pfd[i].events = POLLIN;
+ pp->pfd[i].fd = -1;
+ }
+}
+
+static void run_processes_parallel_cleanup(struct parallel_processes *pp)
+{
+ int i;
+ for (i = 0; i < pp->max_number_processes; i++)
+ strbuf_release(&pp->err[i]);
+
+ free(pp->children);
+ free(pp->slots);
+ free(pp->pfd);
+ free(pp->err);
+ strbuf_release(&pp->finished_children);
+}
+
+static void run_processes_parallel_start_new(struct parallel_processes *pp)
+{
+ int i;
+ /* Start new processes. */
+ while (!pp->all_tasks_started
+ && pp->nr_processes < pp->max_number_processes) {
+ for (i = 0; i < pp->max_number_processes; i++)
+ if (!pp->slots[i])
+ break; /* found an empty slot */
+ if (i == pp->max_number_processes)
+ die("BUG: bookkeeping is hard");
+
+ if (pp->fn(pp->data, &pp->children[i], &pp->err[i])) {
+ pp->all_tasks_started = 1;
+ break;
+ }
+ if (start_command(&pp->children[i]))
+ pp->fn_err(pp->data, &pp->children[i], &pp->err[i]);
+
+ unblock_fd(pp->children[i].err);
+
+ pp->nr_processes++;
+ pp->slots[i] = 1;
+ pp->pfd[i].fd = pp->children[i].err;
+ }
+}
+
+static int run_processes_parallel_buffer_stderr(struct parallel_processes *pp)
+{
+ int i;
+ i = poll(pp->pfd, pp->max_number_processes, 100);
+ if (i < 0) {
+ if (errno == EINTR)
+ /* A signal was caught; try again */
+ return -1;
+ else {
+ run_processes_parallel_cleanup(pp);
+ die_errno("poll");
+ }
+ }
+
+ /* Buffer output from all pipes. */
+ for (i = 0; i < pp->max_number_processes; i++) {
+ if (!pp->slots[i])
+ continue;
+ if (pp->pfd[i].revents & POLLIN)
+ strbuf_read_noblock(&pp->err[i], pp->children[i].err, 0);
+ if (pp->foreground_child == i) {
+ fputs(pp->err[i].buf, stderr);
+ strbuf_reset(&pp->err[i]);
+ }
+ }
+ return 0;
+}
+
+
+static void run_processes_parallel_collect_finished(struct parallel_processes *pp)
+{
+ int i = 0;
+ pid_t pid;
+ int wait_status, code;
+ int n = pp->max_number_processes;
+ /* Collect finished child processes. */
+ while (pp->nr_processes > 0) {
+ pid = waitpid(-1, &wait_status, WNOHANG);
+ if (pid == 0)
+ return; /* no child finished */
+
+ if (pid < 0) {
+ if (errno == EINTR)
+ return; /* just try again next time */
+ if (errno == EINVAL || errno == ECHILD)
+ die_errno("wait");
+ } else {
+ /* Find the finished child. */
+ for (i = 0; i < pp->max_number_processes; i++)
+ if (pp->slots[i] && pid == pp->children[i].pid)
+ break;
+ if (i == pp->max_number_processes)
+ /*
+ * waitpid returned another process id
+ * which we are not waiting for.
+ */
+ return;
+ }
+ strbuf_read_noblock(&pp->err[i], pp->children[i].err, 0);
+
+ if (determine_return_value(wait_status, &code, &errno,
+ pp->children[i].argv[0]) < 0)
+ error("waitpid is confused (%s)",
+ pp->children[i].argv[0]);
+
+ pp->fn_exit(pp->data, &pp->children[i], code);
+
+ argv_array_clear(&pp->children[i].args);
+ argv_array_clear(&pp->children[i].env_array);
+
+ pp->nr_processes--;
+ pp->slots[i] = 0;
+ pp->pfd[i].fd = -1;
+
+ if (i != pp->foreground_child) {
+ strbuf_addbuf(&pp->finished_children, &pp->err[i]);
+ strbuf_reset(&pp->err[i]);
+ } else {
+ fputs(pp->err[i].buf, stderr);
+ strbuf_reset(&pp->err[i]);
+
+ /* Output all other finished child processes */
+ fputs(pp->finished_children.buf, stderr);
+ strbuf_reset(&pp->finished_children);
+
+ /*
+ * Pick next process to output live.
+ * NEEDSWORK:
+ * For now we pick it randomly by doing a round
+ * robin. Later we may want to pick the one with
+ * the most output or the longest or shortest
+ * running process time.
+ */
+ for (i = 0; i < n; i++)
+ if (pp->slots[(pp->foreground_child + i) % n])
+ break;
+ pp->foreground_child = (pp->foreground_child + i) % n;
+ fputs(pp->err[pp->foreground_child].buf, stderr);
+ strbuf_reset(&pp->err[pp->foreground_child]);
+ }
+ }
+}
+
+int run_processes_parallel(int n, void *data,
+ get_next_task fn,
+ handle_child_starting_failure fn_err,
+ handle_child_return_value fn_exit)
+{
+ struct parallel_processes pp;
+ run_processes_parallel_init(&pp, n, data, fn, fn_err, fn_exit);
+
+ while (!pp.all_tasks_started || pp.nr_processes > 0) {
+ run_processes_parallel_start_new(&pp);
+ if (run_processes_parallel_buffer_stderr(&pp))
+ continue;
+ run_processes_parallel_collect_finished(&pp);
+ }
+ run_processes_parallel_cleanup(&pp);
+
+ return 0;
+}
diff --git a/run-command.h b/run-command.h
index 5b4425a..0487f71 100644
--- a/run-command.h
+++ b/run-command.h
@@ -119,4 +119,40 @@ struct async {
int start_async(struct async *async);
int finish_async(struct async *async);
+/**
+ * This callback should initialize the child process and preload the
+ * error channel. The preloading of is useful if you want to have a message
+ * printed directly before the output of the child process.
+ * You MUST set stdout_to_stderr.
+ *
+ * Return 0 if the next child is ready to run.
+ * Return != 0 if there are no more tasks to be processed.
+ */
+typedef int (*get_next_task)(void *data,
+ struct child_process *cp,
+ struct strbuf *err);
+
+typedef void (*handle_child_starting_failure)(void *data,
+ struct child_process *cp,
+ struct strbuf *err);
+
+typedef void (*handle_child_return_value)(void *data,
+ struct child_process *cp,
+ int result);
+
+/**
+ * Runs up to n processes at the same time. Whenever a process can be
+ * started, the callback `get_next_task` is called to obtain the data
+ * fed to the child process.
+ *
+ * The children started via this function run in parallel and their output
+ * to stderr is buffered, while one of the children will directly output
+ * to stderr.
+ */
+
+int run_processes_parallel(int n, void *data,
+ get_next_task fn,
+ handle_child_starting_failure,
+ handle_child_return_value);
+
#endif
diff --git a/strbuf.c b/strbuf.c
index cce5eed..7f866c3 100644
--- a/strbuf.c
+++ b/strbuf.c
@@ -384,6 +384,37 @@ ssize_t strbuf_read(struct strbuf *sb, int fd, size_t hint)
return sb->len - oldlen;
}
+ssize_t strbuf_read_noblock(struct strbuf *sb, int fd, size_t hint)
+{
+ size_t oldlen = sb->len;
+ size_t oldalloc = sb->alloc;
+
+ strbuf_grow(sb, hint ? hint : 8192);
+ for (;;) {
+ ssize_t cnt;
+
+ cnt = read(fd, sb->buf + sb->len, sb->alloc - sb->len - 1);
+ if (cnt < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno == EAGAIN)
+ break;
+ if (oldalloc == 0)
+ strbuf_release(sb);
+ else
+ strbuf_setlen(sb, oldlen);
+ return -1;
+ }
+ if (!cnt)
+ break;
+ sb->len += cnt;
+ strbuf_grow(sb, 8192);
+ }
+
+ sb->buf[sb->len] = '\0';
+ return sb->len - oldlen;
+}
+
#define STRBUF_MAXLINK (2*PATH_MAX)
int strbuf_readlink(struct strbuf *sb, const char *path, size_t hint)
diff --git a/strbuf.h b/strbuf.h
index aef2794..7ea462b 100644
--- a/strbuf.h
+++ b/strbuf.h
@@ -365,6 +365,7 @@ extern size_t strbuf_fread(struct strbuf *, size_t, FILE *);
* any partial read is undone.
*/
extern ssize_t strbuf_read(struct strbuf *, int fd, size_t hint);
+extern ssize_t strbuf_read_noblock(struct strbuf *, int fd, size_t hint);
/**
* Read the contents of a file, specified by its path. The third argument
diff --git a/submodule.c b/submodule.c
index 1d64e57..a0e06e8 100644
--- a/submodule.c
+++ b/submodule.c
@@ -12,6 +12,7 @@
#include "sha1-array.h"
#include "argv-array.h"
#include "blob.h"
+#include "thread-utils.h"
static int config_fetch_recurse_submodules = RECURSE_SUBMODULES_ON_DEMAND;
static struct string_list changed_submodule_paths;
@@ -615,37 +616,79 @@ static void calculate_changed_submodule_paths(void)
initialized_fetch_ref_tips = 0;
}
+struct submodule_parallel_fetch {
+ int count;
+ struct argv_array args;
+ const char *work_tree;
+ const char *prefix;
+ int command_line_option;
+ int quiet;
+ int result;
+};
+#define SPF_INIT {0, ARGV_ARRAY_INIT, NULL, NULL, 0, 0, 0}
+
+int get_next_submodule(void *data, struct child_process *cp,
+ struct strbuf *err);
+
+void handle_submodule_fetch_start_err(void *data, struct child_process *cp, struct strbuf *err)
+{
+ struct submodule_parallel_fetch *spf = data;
+ spf->result = 1;
+}
+
+void handle_submodule_fetch_finish( void *data, struct child_process *cp, int retvalue)
+{
+ struct submodule_parallel_fetch *spf = data;
+
+ if (retvalue)
+ spf->result = 1;
+}
+
int fetch_populated_submodules(const struct argv_array *options,
const char *prefix, int command_line_option,
- int quiet)
+ int quiet, int max_parallel_jobs)
{
- int i, result = 0;
- struct child_process cp = CHILD_PROCESS_INIT;
- struct argv_array argv = ARGV_ARRAY_INIT;
- const char *work_tree = get_git_work_tree();
- if (!work_tree)
+ int i;
+ struct submodule_parallel_fetch spf = SPF_INIT;
+ spf.work_tree = get_git_work_tree();
+ spf.command_line_option = command_line_option;
+ spf.quiet = quiet;
+ spf.prefix = prefix;
+ if (!spf.work_tree)
goto out;
if (read_cache() < 0)
die("index file corrupt");
- argv_array_push(&argv, "fetch");
+ argv_array_push(&spf.args, "fetch");
for (i = 0; i < options->argc; i++)
- argv_array_push(&argv, options->argv[i]);
- argv_array_push(&argv, "--recurse-submodules-default");
+ argv_array_push(&spf.args, options->argv[i]);
+ argv_array_push(&spf.args, "--recurse-submodules-default");
/* default value, "--submodule-prefix" and its value are added later */
- cp.env = local_repo_env;
- cp.git_cmd = 1;
- cp.no_stdin = 1;
-
calculate_changed_submodule_paths();
+ run_processes_parallel(max_parallel_jobs, &spf,
+ get_next_submodule,
+ handle_submodule_fetch_start_err,
+ handle_submodule_fetch_finish);
+
+ argv_array_clear(&spf.args);
+out:
+ string_list_clear(&changed_submodule_paths, 1);
+ return spf.result;
+}
+
+int get_next_submodule(void *data, struct child_process *cp,
+ struct strbuf *err)
+{
+ int ret = 0;
+ struct submodule_parallel_fetch *spf = data;
- for (i = 0; i < active_nr; i++) {
+ for ( ; spf->count < active_nr; spf->count++) {
struct strbuf submodule_path = STRBUF_INIT;
struct strbuf submodule_git_dir = STRBUF_INIT;
struct strbuf submodule_prefix = STRBUF_INIT;
- const struct cache_entry *ce = active_cache[i];
+ const struct cache_entry *ce = active_cache[spf->count];
const char *git_dir, *default_argv;
const struct submodule *submodule;
@@ -657,7 +700,7 @@ int fetch_populated_submodules(const struct argv_array *options,
submodule = submodule_from_name(null_sha1, ce->name);
default_argv = "yes";
- if (command_line_option == RECURSE_SUBMODULES_DEFAULT) {
+ if (spf->command_line_option == RECURSE_SUBMODULES_DEFAULT) {
if (submodule &&
submodule->fetch_recurse !=
RECURSE_SUBMODULES_NONE) {
@@ -680,40 +723,46 @@ int fetch_populated_submodules(const struct argv_array *options,
default_argv = "on-demand";
}
}
- } else if (command_line_option == RECURSE_SUBMODULES_ON_DEMAND) {
+ } else if (spf->command_line_option == RECURSE_SUBMODULES_ON_DEMAND) {
if (!unsorted_string_list_lookup(&changed_submodule_paths, ce->name))
continue;
default_argv = "on-demand";
}
- strbuf_addf(&submodule_path, "%s/%s", work_tree, ce->name);
+ strbuf_addf(&submodule_path, "%s/%s", spf->work_tree, ce->name);
strbuf_addf(&submodule_git_dir, "%s/.git", submodule_path.buf);
- strbuf_addf(&submodule_prefix, "%s%s/", prefix, ce->name);
+ strbuf_addf(&submodule_prefix, "%s%s/", spf->prefix, ce->name);
git_dir = read_gitfile(submodule_git_dir.buf);
if (!git_dir)
git_dir = submodule_git_dir.buf;
if (is_directory(git_dir)) {
- if (!quiet)
- fprintf(stderr, "Fetching submodule %s%s\n", prefix, ce->name);
- cp.dir = submodule_path.buf;
- argv_array_push(&argv, default_argv);
- argv_array_push(&argv, "--submodule-prefix");
- argv_array_push(&argv, submodule_prefix.buf);
- cp.argv = argv.argv;
- if (run_command(&cp))
- result = 1;
- argv_array_pop(&argv);
- argv_array_pop(&argv);
- argv_array_pop(&argv);
+ child_process_init(cp);
+ cp->dir = strbuf_detach(&submodule_path, NULL);
+ cp->git_cmd = 1;
+ cp->no_stdout = 1;
+ cp->no_stdin = 1;
+ cp->stdout_to_stderr = 1;
+ cp->err = -1;
+ cp->env = local_repo_env;
+ if (!spf->quiet)
+ strbuf_addf(err, "Fetching submodule %s%s\n",
+ spf->prefix, ce->name);
+ argv_array_init(&cp->args);
+ argv_array_pushv(&cp->args, spf->args.argv);
+ argv_array_push(&cp->args, default_argv);
+ argv_array_push(&cp->args, "--submodule-prefix");
+ argv_array_push(&cp->args, submodule_prefix.buf);
+ ret = 1;
}
strbuf_release(&submodule_path);
strbuf_release(&submodule_git_dir);
strbuf_release(&submodule_prefix);
+ if (ret) {
+ spf->count++;
+ return 0;
+ }
}
- argv_array_clear(&argv);
-out:
- string_list_clear(&changed_submodule_paths, 1);
- return result;
+ return 1;
}
unsigned is_submodule_modified(const char *path, int ignore_untracked)
diff --git a/submodule.h b/submodule.h
index 5507c3d..cbc0003 100644
--- a/submodule.h
+++ b/submodule.h
@@ -31,7 +31,7 @@ void set_config_fetch_recurse_submodules(int value);
void check_for_new_submodule_commits(unsigned char new_sha1[20]);
int fetch_populated_submodules(const struct argv_array *options,
const char *prefix, int command_line_option,
- int quiet);
+ int quiet, int max_parallel_jobs);
unsigned is_submodule_modified(const char *path, int ignore_untracked);
int submodule_uses_gitfile(const char *path);
int ok_to_remove_submodule(const char *path);
diff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh
index 9acf628..37c89b9 100755
--- a/t/t0061-run-command.sh
+++ b/t/t0061-run-command.sh
@@ -47,4 +47,24 @@ test_expect_success POSIXPERM,SANITY 'unreadable directory in PATH' '
test_cmp expect actual
'
+cat >expect <<-EOF
+preloaded output of a child
+Hello
+World
+preloaded output of a child
+Hello
+World
+preloaded output of a child
+Hello
+World
+preloaded output of a child
+Hello
+World
+EOF
+
+test_expect_success 'run_command runs in parallel' '
+ test-run-command run-command-async sh -c "printf \"%s\n%s\n\" Hello World" 2>actual &&
+ test_cmp expect actual
+'
+
test_done
diff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh
index 17759b1..1b4ce69 100755
--- a/t/t5526-fetch-submodules.sh
+++ b/t/t5526-fetch-submodules.sh
@@ -71,6 +71,16 @@ test_expect_success "fetch --recurse-submodules recurses into submodules" '
test_i18ncmp expect.err actual.err
'
+test_expect_success "fetch --recurse-submodules -j2 has the same output behaviour" '
+ add_upstream_commit &&
+ (
+ cd downstream &&
+ git fetch --recurse-submodules -j2 2>../actual.err
+ ) &&
+ test_must_be_empty actual.out &&
+ test_i18ncmp expect.err actual.err
+'
+
test_expect_success "fetch alone only fetches superproject" '
add_upstream_commit &&
(
@@ -140,6 +150,15 @@ test_expect_success "--quiet propagates to submodules" '
! test -s actual.err
'
+test_expect_success "--quiet propagates to parallel submodules" '
+ (
+ cd downstream &&
+ git fetch --recurse-submodules -j 2 --quiet >../actual.out 2>../actual.err
+ ) &&
+ ! test -s actual.out &&
+ ! test -s actual.err
+'
+
test_expect_success "--dry-run propagates to submodules" '
add_upstream_commit &&
(
diff --git a/test-run-command.c b/test-run-command.c
index 89c7de2..71fd3ca 100644
--- a/test-run-command.c
+++ b/test-run-command.c
@@ -10,9 +10,29 @@
#include "git-compat-util.h"
#include "run-command.h"
+#include "argv-array.h"
+#include "strbuf.h"
#include <string.h>
#include <errno.h>
+static int number_callbacks;
+int parallel_next(void *data,
+ struct child_process *cp,
+ struct strbuf *err)
+{
+ struct child_process *d = data;
+ if (number_callbacks >= 4)
+ return 1;
+
+ argv_array_pushv(&cp->args, d->argv);
+ cp->stdout_to_stderr = 1;
+ cp->no_stdin = 1;
+ cp->err = -1;
+ strbuf_addf(err, "preloaded output of a child\n");
+ number_callbacks++;
+ return 0;
+}
+
int main(int argc, char **argv)
{
struct child_process proc = CHILD_PROCESS_INIT;
@@ -30,6 +50,10 @@ int main(int argc, char **argv)
if (!strcmp(argv[1], "run-command"))
exit(run_command(&proc));
+ if (!strcmp(argv[1], "run-command-async"))
+ exit(run_processes_parallel(4, &proc, parallel_next,
+ NULL, NULL));
+
fprintf(stderr, "check usage\n");
return 1;
}