Skip to content

Commit 1a7f6be

Browse files
committed
Merge branch 'ab/hooks-regression-fix'
In Git 2.36 we revamped the way how hooks are invoked. One change that is end-user visible is that the output of a hook is no longer directly connected to the standard output of "git" that spawns the hook, which was noticed post release. This is getting corrected. * ab/hooks-regression-fix: hook API: fix v2.36.0 regression: hooks should be connected to a TTY run-command: add an "ungroup" option to run_process_parallel()
2 parents 66c2948 + a082345 commit 1a7f6be

File tree

6 files changed

+155
-29
lines changed

6 files changed

+155
-29
lines changed

hook.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
144144
cb_data.hook_path = abs_path.buf;
145145
}
146146

147+
run_processes_parallel_ungroup = 1;
147148
run_processes_parallel_tr2(jobs,
148149
pick_next_hook,
149150
notify_start_failure,

run-command.c

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,7 @@ enum child_state {
14721472
GIT_CP_WAIT_CLEANUP,
14731473
};
14741474

1475+
int run_processes_parallel_ungroup;
14751476
struct parallel_processes {
14761477
void *data;
14771478

@@ -1495,6 +1496,7 @@ struct parallel_processes {
14951496
struct pollfd *pfd;
14961497

14971498
unsigned shutdown : 1;
1499+
unsigned ungroup : 1;
14981500

14991501
int output_owner;
15001502
struct strbuf buffered_output; /* of finished children */
@@ -1538,7 +1540,7 @@ static void pp_init(struct parallel_processes *pp,
15381540
get_next_task_fn get_next_task,
15391541
start_failure_fn start_failure,
15401542
task_finished_fn task_finished,
1541-
void *data)
1543+
void *data, int ungroup)
15421544
{
15431545
int i;
15441546

@@ -1560,15 +1562,21 @@ static void pp_init(struct parallel_processes *pp,
15601562
pp->nr_processes = 0;
15611563
pp->output_owner = 0;
15621564
pp->shutdown = 0;
1565+
pp->ungroup = ungroup;
15631566
CALLOC_ARRAY(pp->children, n);
1564-
CALLOC_ARRAY(pp->pfd, n);
1567+
if (pp->ungroup)
1568+
pp->pfd = NULL;
1569+
else
1570+
CALLOC_ARRAY(pp->pfd, n);
15651571
strbuf_init(&pp->buffered_output, 0);
15661572

15671573
for (i = 0; i < n; i++) {
15681574
strbuf_init(&pp->children[i].err, 0);
15691575
child_process_init(&pp->children[i].process);
1570-
pp->pfd[i].events = POLLIN | POLLHUP;
1571-
pp->pfd[i].fd = -1;
1576+
if (pp->pfd) {
1577+
pp->pfd[i].events = POLLIN | POLLHUP;
1578+
pp->pfd[i].fd = -1;
1579+
}
15721580
}
15731581

15741582
pp_for_signal = pp;
@@ -1616,32 +1624,40 @@ static int pp_start_one(struct parallel_processes *pp)
16161624
BUG("bookkeeping is hard");
16171625

16181626
code = pp->get_next_task(&pp->children[i].process,
1619-
&pp->children[i].err,
1627+
pp->ungroup ? NULL : &pp->children[i].err,
16201628
pp->data,
16211629
&pp->children[i].data);
16221630
if (!code) {
1623-
strbuf_addbuf(&pp->buffered_output, &pp->children[i].err);
1624-
strbuf_reset(&pp->children[i].err);
1631+
if (!pp->ungroup) {
1632+
strbuf_addbuf(&pp->buffered_output, &pp->children[i].err);
1633+
strbuf_reset(&pp->children[i].err);
1634+
}
16251635
return 1;
16261636
}
1627-
pp->children[i].process.err = -1;
1628-
pp->children[i].process.stdout_to_stderr = 1;
1637+
if (!pp->ungroup) {
1638+
pp->children[i].process.err = -1;
1639+
pp->children[i].process.stdout_to_stderr = 1;
1640+
}
16291641
pp->children[i].process.no_stdin = 1;
16301642

16311643
if (start_command(&pp->children[i].process)) {
1632-
code = pp->start_failure(&pp->children[i].err,
1644+
code = pp->start_failure(pp->ungroup ? NULL :
1645+
&pp->children[i].err,
16331646
pp->data,
16341647
pp->children[i].data);
1635-
strbuf_addbuf(&pp->buffered_output, &pp->children[i].err);
1636-
strbuf_reset(&pp->children[i].err);
1648+
if (!pp->ungroup) {
1649+
strbuf_addbuf(&pp->buffered_output, &pp->children[i].err);
1650+
strbuf_reset(&pp->children[i].err);
1651+
}
16371652
if (code)
16381653
pp->shutdown = 1;
16391654
return code;
16401655
}
16411656

16421657
pp->nr_processes++;
16431658
pp->children[i].state = GIT_CP_WORKING;
1644-
pp->pfd[i].fd = pp->children[i].process.err;
1659+
if (pp->pfd)
1660+
pp->pfd[i].fd = pp->children[i].process.err;
16451661
return 0;
16461662
}
16471663

@@ -1675,6 +1691,7 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)
16751691
static void pp_output(struct parallel_processes *pp)
16761692
{
16771693
int i = pp->output_owner;
1694+
16781695
if (pp->children[i].state == GIT_CP_WORKING &&
16791696
pp->children[i].err.len) {
16801697
strbuf_write(&pp->children[i].err, stderr);
@@ -1697,7 +1714,7 @@ static int pp_collect_finished(struct parallel_processes *pp)
16971714

16981715
code = finish_command(&pp->children[i].process);
16991716

1700-
code = pp->task_finished(code,
1717+
code = pp->task_finished(code, pp->ungroup ? NULL :
17011718
&pp->children[i].err, pp->data,
17021719
pp->children[i].data);
17031720

@@ -1708,10 +1725,13 @@ static int pp_collect_finished(struct parallel_processes *pp)
17081725

17091726
pp->nr_processes--;
17101727
pp->children[i].state = GIT_CP_FREE;
1711-
pp->pfd[i].fd = -1;
1728+
if (pp->pfd)
1729+
pp->pfd[i].fd = -1;
17121730
child_process_init(&pp->children[i].process);
17131731

1714-
if (i != pp->output_owner) {
1732+
if (pp->ungroup) {
1733+
; /* no strbuf_*() work to do here */
1734+
} else if (i != pp->output_owner) {
17151735
strbuf_addbuf(&pp->buffered_output, &pp->children[i].err);
17161736
strbuf_reset(&pp->children[i].err);
17171737
} else {
@@ -1748,9 +1768,14 @@ int run_processes_parallel(int n,
17481768
int i, code;
17491769
int output_timeout = 100;
17501770
int spawn_cap = 4;
1771+
int ungroup = run_processes_parallel_ungroup;
17511772
struct parallel_processes pp;
17521773

1753-
pp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);
1774+
/* unset for the next API user */
1775+
run_processes_parallel_ungroup = 0;
1776+
1777+
pp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb,
1778+
ungroup);
17541779
while (1) {
17551780
for (i = 0;
17561781
i < spawn_cap && !pp.shutdown &&
@@ -1767,8 +1792,15 @@ int run_processes_parallel(int n,
17671792
}
17681793
if (!pp.nr_processes)
17691794
break;
1770-
pp_buffer_stderr(&pp, output_timeout);
1771-
pp_output(&pp);
1795+
if (ungroup) {
1796+
int i;
1797+
1798+
for (i = 0; i < pp.max_processes; i++)
1799+
pp.children[i].state = GIT_CP_WAIT_CLEANUP;
1800+
} else {
1801+
pp_buffer_stderr(&pp, output_timeout);
1802+
pp_output(&pp);
1803+
}
17721804
code = pp_collect_finished(&pp);
17731805
if (code) {
17741806
pp.shutdown = 1;

run-command.h

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,9 @@ void check_pipe(int err);
405405
* pp_cb is the callback cookie as passed to run_processes_parallel.
406406
* You can store a child process specific callback cookie in pp_task_cb.
407407
*
408+
* See run_processes_parallel() below for a discussion of the "struct
409+
* strbuf *out" parameter.
410+
*
408411
* Even after returning 0 to indicate that there are no more processes,
409412
* this function will be called again until there are no more running
410413
* child processes.
@@ -423,9 +426,8 @@ typedef int (*get_next_task_fn)(struct child_process *cp,
423426
* This callback is called whenever there are problems starting
424427
* a new process.
425428
*
426-
* You must not write to stdout or stderr in this function. Add your
427-
* message to the strbuf out instead, which will be printed without
428-
* messing up the output of the other parallel processes.
429+
* See run_processes_parallel() below for a discussion of the "struct
430+
* strbuf *out" parameter.
429431
*
430432
* pp_cb is the callback cookie as passed into run_processes_parallel,
431433
* pp_task_cb is the callback cookie as passed into get_next_task_fn.
@@ -441,9 +443,8 @@ typedef int (*start_failure_fn)(struct strbuf *out,
441443
/**
442444
* This callback is called on every child process that finished processing.
443445
*
444-
* You must not write to stdout or stderr in this function. Add your
445-
* message to the strbuf out instead, which will be printed without
446-
* messing up the output of the other parallel processes.
446+
* See run_processes_parallel() below for a discussion of the "struct
447+
* strbuf *out" parameter.
447448
*
448449
* pp_cb is the callback cookie as passed into run_processes_parallel,
449450
* pp_task_cb is the callback cookie as passed into get_next_task_fn.
@@ -464,11 +465,26 @@ typedef int (*task_finished_fn)(int result,
464465
*
465466
* The children started via this function run in parallel. Their output
466467
* (both stdout and stderr) is routed to stderr in a manner that output
467-
* from different tasks does not interleave.
468+
* from different tasks does not interleave (but see "ungroup" below).
468469
*
469470
* start_failure_fn and task_finished_fn can be NULL to omit any
470471
* special handling.
472+
*
473+
* If the "ungroup" option isn't specified, the API will set the
474+
* "stdout_to_stderr" parameter in "struct child_process" and provide
475+
* the callbacks with a "struct strbuf *out" parameter to write output
476+
* to. In this case the callbacks must not write to stdout or
477+
* stderr as such output will mess up the output of the other parallel
478+
* processes. If "ungroup" option is specified callbacks will get a
479+
* NULL "struct strbuf *out" parameter, and are responsible for
480+
* emitting their own output, including dealing with any race
481+
* conditions due to writing in parallel to stdout and stderr.
482+
* The "ungroup" option can be enabled by setting the global
483+
* "run_processes_parallel_ungroup" to "1" before invoking
484+
* run_processes_parallel(), it will be set back to "0" as soon as the
485+
* API reads that setting.
471486
*/
487+
extern int run_processes_parallel_ungroup;
472488
int run_processes_parallel(int n,
473489
get_next_task_fn,
474490
start_failure_fn,

t/helper/test-run-command.c

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ static int parallel_next(struct child_process *cp,
3131
return 0;
3232

3333
strvec_pushv(&cp->args, d->args.v);
34-
strbuf_addstr(err, "preloaded output of a child\n");
34+
if (err)
35+
strbuf_addstr(err, "preloaded output of a child\n");
36+
else
37+
fprintf(stderr, "preloaded output of a child\n");
38+
3539
number_callbacks++;
3640
return 1;
3741
}
@@ -41,7 +45,10 @@ static int no_job(struct child_process *cp,
4145
void *cb,
4246
void **task_cb)
4347
{
44-
strbuf_addstr(err, "no further jobs available\n");
48+
if (err)
49+
strbuf_addstr(err, "no further jobs available\n");
50+
else
51+
fprintf(stderr, "no further jobs available\n");
4552
return 0;
4653
}
4754

@@ -50,7 +57,10 @@ static int task_finished(int result,
5057
void *pp_cb,
5158
void *pp_task_cb)
5259
{
53-
strbuf_addstr(err, "asking for a quick stop\n");
60+
if (err)
61+
strbuf_addstr(err, "asking for a quick stop\n");
62+
else
63+
fprintf(stderr, "asking for a quick stop\n");
5464
return 1;
5565
}
5666

@@ -407,6 +417,12 @@ int cmd__run_command(int argc, const char **argv)
407417
if (!strcmp(argv[1], "run-command"))
408418
exit(run_command(&proc));
409419

420+
if (!strcmp(argv[1], "--ungroup")) {
421+
argv += 1;
422+
argc -= 1;
423+
run_processes_parallel_ungroup = 1;
424+
}
425+
410426
jobs = atoi(argv[2]);
411427
strvec_clear(&proc.args);
412428
strvec_pushv(&proc.args, (const char **)argv + 3);

t/t0061-run-command.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,34 @@ test_expect_success 'run_command runs in parallel with more jobs available than
134134
test_cmp expect actual
135135
'
136136

137+
test_expect_success 'run_command runs ungrouped in parallel with more jobs available than tasks' '
138+
test-tool run-command --ungroup run-command-parallel 5 sh -c "printf \"%s\n%s\n\" Hello World" >out 2>err &&
139+
test_line_count = 8 out &&
140+
test_line_count = 4 err
141+
'
142+
137143
test_expect_success 'run_command runs in parallel with as many jobs as tasks' '
138144
test-tool run-command run-command-parallel 4 sh -c "printf \"%s\n%s\n\" Hello World" 2>actual &&
139145
test_cmp expect actual
140146
'
141147

148+
test_expect_success 'run_command runs ungrouped in parallel with as many jobs as tasks' '
149+
test-tool run-command --ungroup run-command-parallel 4 sh -c "printf \"%s\n%s\n\" Hello World" >out 2>err &&
150+
test_line_count = 8 out &&
151+
test_line_count = 4 err
152+
'
153+
142154
test_expect_success 'run_command runs in parallel with more tasks than jobs available' '
143155
test-tool run-command run-command-parallel 3 sh -c "printf \"%s\n%s\n\" Hello World" 2>actual &&
144156
test_cmp expect actual
145157
'
146158

159+
test_expect_success 'run_command runs ungrouped in parallel with more tasks than jobs available' '
160+
test-tool run-command --ungroup run-command-parallel 3 sh -c "printf \"%s\n%s\n\" Hello World" >out 2>err &&
161+
test_line_count = 8 out &&
162+
test_line_count = 4 err
163+
'
164+
147165
cat >expect <<-EOF
148166
preloaded output of a child
149167
asking for a quick stop
@@ -158,6 +176,12 @@ test_expect_success 'run_command is asked to abort gracefully' '
158176
test_cmp expect actual
159177
'
160178

179+
test_expect_success 'run_command is asked to abort gracefully (ungroup)' '
180+
test-tool run-command --ungroup run-command-abort 3 false >out 2>err &&
181+
test_must_be_empty out &&
182+
test_line_count = 6 err
183+
'
184+
161185
cat >expect <<-EOF
162186
no further jobs available
163187
EOF
@@ -167,6 +191,12 @@ test_expect_success 'run_command outputs ' '
167191
test_cmp expect actual
168192
'
169193

194+
test_expect_success 'run_command outputs (ungroup) ' '
195+
test-tool run-command --ungroup run-command-no-jobs 3 sh -c "printf \"%s\n%s\n\" Hello World" >out 2>err &&
196+
test_must_be_empty out &&
197+
test_cmp expect err
198+
'
199+
170200
test_trace () {
171201
expect="$1"
172202
shift

t/t1800-hook.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ test_description='git-hook command'
44

55
TEST_PASSES_SANITIZE_LEAK=true
66
. ./test-lib.sh
7+
. "$TEST_DIRECTORY"/lib-terminal.sh
78

89
test_expect_success 'git hook usage' '
910
test_expect_code 129 git hook &&
@@ -120,4 +121,34 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
120121
test_cmp expect actual
121122
'
122123

124+
test_hook_tty () {
125+
cat >expect <<-\EOF
126+
STDOUT TTY
127+
STDERR TTY
128+
EOF
129+
130+
test_when_finished "rm -rf repo" &&
131+
git init repo &&
132+
133+
test_commit -C repo A &&
134+
test_commit -C repo B &&
135+
git -C repo reset --soft HEAD^ &&
136+
137+
test_hook -C repo pre-commit <<-EOF &&
138+
test -t 1 && echo STDOUT TTY >>actual || echo STDOUT NO TTY >>actual &&
139+
test -t 2 && echo STDERR TTY >>actual || echo STDERR NO TTY >>actual
140+
EOF
141+
142+
test_terminal git -C repo "$@" &&
143+
test_cmp expect repo/actual
144+
}
145+
146+
test_expect_success TTY 'git hook run: stdout and stderr are connected to a TTY' '
147+
test_hook_tty hook run pre-commit
148+
'
149+
150+
test_expect_success TTY 'git commit: stdout and stderr are connected to a TTY' '
151+
test_hook_tty commit -m"B.new"
152+
'
153+
123154
test_done

0 commit comments

Comments
 (0)