hook: introduce extensions.hookStdoutToStderr

All hooks already redirect stdout to stderr with the exception of
pre-push which has a known user who depends on the separate stdout
versus stderr outputs (the git-lfs project).

The pre-push behavior was a surprise which we found out about after
causing a regression for git-lfs. Notably, it might not be the only
exception (it's the one we know about). There might be more.

This presents a challenge because stdout_to_stderr is required for
hook parallelization, so run-command can buffer and de-interleave
the hook outputs using ungroup=0, when hook.jobs > 1.

Introduce an extension to enforce consistency: all hooks merge stdout
into stderr and can be safely parallelized. This provides a clean
separation and avoids breaking existing stdout vs stderr behavior.

When this extension is disabled, the `hook.jobs` config has no
effect for pre-push, to prevent garbled (interleaved) parallel
output, so it runs sequentially like before.

Alternatives I've considered to this extension include:
1. Allowing pre-push to run in parallel with interleaved output.
2. Always running pre-push sequentially (no parallel jobs for it).
3. Making users (only git-lfs? maybe more?) fix their hooks to read
   stderr not stdout.

Out of all these alternatives, I think this extension is the most
reasonable compromise, to not break existing users, allow pre-push
parallel jobs for those who need it (with correct outputs) and also
future-proofing in case there are any more exceptions to be added.

Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Adrian Ratiu
2026-03-09 15:37:38 +02:00
committed by Junio C Hamano
parent 24c7af4d22
commit 8a101e3d2c
8 changed files with 64 additions and 5 deletions

View File

@@ -710,6 +710,9 @@ static enum extension_result handle_extension(const char *var,
} else if (!strcmp(ext, "submodulepathconfig")) {
data->submodule_path_cfg = git_config_bool(var, value);
return EXTENSION_OK;
} else if (!strcmp(ext, "hookstdouttostderr")) {
data->hook_stdout_to_stderr = git_config_bool(var, value);
return EXTENSION_OK;
}
return EXTENSION_UNKNOWN;
}
@@ -1976,6 +1979,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
repo_fmt.relative_worktrees;
the_repository->repository_format_submodule_path_cfg =
repo_fmt.submodule_path_cfg;
the_repository->repository_format_hook_stdout_to_stderr =
repo_fmt.hook_stdout_to_stderr;
/* take ownership of repo_fmt.partial_clone */
the_repository->repository_format_partial_clone =
repo_fmt.partial_clone;
@@ -2098,6 +2103,8 @@ void check_repository_format(struct repository_format *fmt)
fmt->submodule_path_cfg;
the_repository->repository_format_relative_worktrees =
fmt->relative_worktrees;
the_repository->repository_format_hook_stdout_to_stderr =
fmt->hook_stdout_to_stderr;
the_repository->repository_format_partial_clone =
xstrdup_or_null(fmt->partial_clone);
clear_repository_format(&repo_fmt);