* [PATCH] merge-ll: Cleanup merge driver temporaries after interrupt
@ 2026-09-10 15:06 Michal Koutný
2026-09-10 16:22 ` Jeff King
0 siblings, 1 reply; 11+ messages in thread
From: Michal Koutný @ 2026-09-10 15:06 UTC (permalink / raw)
To: git
Cc: Michal Koutný, Jean Delvare, Elijah Newren, Usman Akinyemi,
Taylor Blau, Junio C Hamano, René Scharfe
When there's a long(er) running merge driver helper, the user may just
decide to terminate it with Ctrl+C. That sends a signal to the driver
prog and to the whole process group as well, including the git merge
command proper. Hence the cleanup code would not run and .merge_file_*
files are left behind.
Transfer the idiom [1] from editor.c where the (process group) signal
delivery is approximated from the return code of the child process and
do the cleanup before going for good.
[1] Note: when the helper SIGINTs alone, it'd tear down the git-merge too.
Reported-by: Jean Delvare <jdelvare@suse.de>
Signed-off-by: Michal Koutný <mkoutny@suse.com>
---
merge-ll.c | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/merge-ll.c b/merge-ll.c
index ef5287dee8..bee30fb5dd 100644
--- a/merge-ll.c
+++ b/merge-ll.c
@@ -17,6 +17,7 @@
#include "quote.h"
#include "strbuf.h"
#include "gettext.h"
+#include "sigchain.h"
struct ll_merge_driver;
@@ -201,7 +202,7 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
struct strbuf cmd = STRBUF_INIT;
const char *format = fn->cmdline;
struct child_process child = CHILD_PROCESS_INIT;
- int status, fd, i;
+ int status, fd, i, sig;
struct stat st;
enum ll_merge_result ret;
assert(opts);
@@ -240,7 +241,13 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
child.use_shell = 1;
strvec_push(&child.args, cmd.buf);
- status = run_command(&child);
+ status = -1;
+ if (start_command(&child) < 0)
+ goto bad;
+ sigchain_push(SIGINT, SIG_IGN);
+ sigchain_push(SIGQUIT, SIG_IGN);
+ status = finish_command(&child);
+
fd = open(temp[1], O_RDONLY);
if (fd < 0)
goto bad;
@@ -262,9 +269,15 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
ret = LL_MERGE_OK;
else if (status <= 128)
ret = LL_MERGE_CONFLICT;
- else
+ else {
/* died due to a signal: WTERMSIG(status) + 128 */
+ sig = status - 128;
+ sigchain_pop(SIGINT);
+ sigchain_pop(SIGQUIT);
+ if (sig == SIGINT || sig == SIGQUIT)
+ raise(sig);
ret = LL_MERGE_ERROR;
+ }
return ret;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH] merge-ll: Cleanup merge driver temporaries after interrupt
2026-09-10 15:06 [PATCH] merge-ll: Cleanup merge driver temporaries after interrupt Michal Koutný
@ 2026-09-10 16:22 ` Jeff King
2026-09-11 14:43 ` Michal Koutný
0 siblings, 1 reply; 11+ messages in thread
From: Jeff King @ 2026-09-10 16:22 UTC (permalink / raw)
To: Michal Koutný
Cc: git, Jean Delvare, Elijah Newren, Usman Akinyemi, Taylor Blau,
Junio C Hamano, René Scharfe
On Thu, Sep 10, 2026 at 05:06:07PM +0200, Michal Koutný wrote:
> When there's a long(er) running merge driver helper, the user may just
> decide to terminate it with Ctrl+C. That sends a signal to the driver
> prog and to the whole process group as well, including the git merge
> command proper. Hence the cleanup code would not run and .merge_file_*
> files are left behind.
>
> Transfer the idiom [1] from editor.c where the (process group) signal
> delivery is approximated from the return code of the child process and
> do the cleanup before going for good.
We have a temporary-file cleanup handler that we install already, which
handles signal propagation, atomicity, etc. It seems like it would be
simpler to just use that.
In the worst case we can just call register_tempfile() on each path, but
I think this code could be taught to use the actual creation. Something
like the patch below (only lightly tested).
---
diff --git a/merge-ll.c b/merge-ll.c
index ef5287dee8..d53f0fe4a6 100644
--- a/merge-ll.c
+++ b/merge-ll.c
@@ -17,6 +17,7 @@
#include "quote.h"
#include "strbuf.h"
#include "gettext.h"
+#include "tempfile.h"
struct ll_merge_driver;
@@ -174,15 +175,30 @@ static struct ll_merge_driver ll_merge_drv[] = {
{ "union", "built-in union merge", ll_union_merge },
};
-static void create_temp(mmfile_t *src, char *path, size_t len)
+static struct tempfile *create_temp(mmfile_t *src)
{
- int fd;
-
- xsnprintf(path, len, ".merge_file_XXXXXX");
- fd = xmkstemp(path);
- if (write_in_full(fd, src->ptr, src->size) < 0)
+ struct tempfile *t = xmks_tempfile(".merge_file_XXXXXX");
+ if (write_in_full(t->fd, src->ptr, src->size) < 0)
die_errno("unable to write temp-file");
- close(fd);
+ close(t->fd);
+ return t;
+}
+
+static const char *get_temp_path(struct tempfile *t)
+{
+ /*
+ * Tempfiles store the absolute path of the file, but
+ * we don't do any quoting against the shell, which
+ * can lead to problems if your path has spaces, etc, in it.
+ * Historically this was OK since we only provided relative
+ * paths which were fairly vanilla.
+ *
+ * We can work around it by going back to the relative path (since we
+ * know we created a tempfile in the cwd via create_temp() above).
+ * In the long run I think we ought to consider providing
+ * the absolute paths but correctly shell-quoting them.
+ */
+ return basename(get_tempfile_path(t));
}
/*
@@ -197,11 +213,11 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
const struct ll_merge_options *opts,
int marker_size)
{
- char temp[3][50];
+ struct tempfile *tmp_o, *tmp_a, *tmp_b;
struct strbuf cmd = STRBUF_INIT;
const char *format = fn->cmdline;
struct child_process child = CHILD_PROCESS_INIT;
- int status, fd, i;
+ int status, fd;
struct stat st;
enum ll_merge_result ret;
assert(opts);
@@ -211,19 +227,19 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
result->ptr = NULL;
result->size = 0;
- create_temp(orig, temp[0], sizeof(temp[0]));
- create_temp(src1, temp[1], sizeof(temp[1]));
- create_temp(src2, temp[2], sizeof(temp[2]));
+ tmp_o = create_temp(orig);
+ tmp_a = create_temp(src1);
+ tmp_b = create_temp(src2);
while (strbuf_expand_step(&cmd, &format)) {
if (skip_prefix(format, "%", &format))
strbuf_addch(&cmd, '%');
else if (skip_prefix(format, "O", &format))
- strbuf_addstr(&cmd, temp[0]);
+ strbuf_addstr(&cmd, get_temp_path(tmp_o));
else if (skip_prefix(format, "A", &format))
- strbuf_addstr(&cmd, temp[1]);
+ strbuf_addstr(&cmd, get_temp_path(tmp_a));
else if (skip_prefix(format, "B", &format))
- strbuf_addstr(&cmd, temp[2]);
+ strbuf_addstr(&cmd, get_temp_path(tmp_b));
else if (skip_prefix(format, "L", &format))
strbuf_addf(&cmd, "%d", marker_size);
else if (skip_prefix(format, "P", &format))
@@ -241,7 +257,8 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
child.use_shell = 1;
strvec_push(&child.args, cmd.buf);
status = run_command(&child);
- fd = open(temp[1], O_RDONLY);
+ /* really feels like we could just use strbuf_read_file() here? */
+ fd = open(get_tempfile_path(tmp_a), O_RDONLY);
if (fd < 0)
goto bad;
if (fstat(fd, &st))
@@ -255,8 +272,9 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
close_bad:
close(fd);
bad:
- for (i = 0; i < 3; i++)
- unlink_or_warn(temp[i]);
+ delete_tempfile(&tmp_o);
+ delete_tempfile(&tmp_a);
+ delete_tempfile(&tmp_b);
strbuf_release(&cmd);
if (!status)
ret = LL_MERGE_OK;
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH] merge-ll: Cleanup merge driver temporaries after interrupt
2026-09-10 16:22 ` Jeff King
@ 2026-09-11 14:43 ` Michal Koutný
2026-09-11 17:10 ` [PATCH v2 0/3] merge-ll: Cleanup merge driver temporaries after Jeff King
0 siblings, 1 reply; 11+ messages in thread
From: Michal Koutný @ 2026-09-11 14:43 UTC (permalink / raw)
To: Jeff King
Cc: git, Jean Delvare, Elijah Newren, Usman Akinyemi, Taylor Blau,
Junio C Hamano, René Scharfe
[-- Attachment #1: Type: text/plain, Size: 905 bytes --]
Hi.
On Thu, Sep 10, 2026 at 12:22:42PM -0400, Jeff King <peff@peff.net> wrote:
> We have a temporary-file cleanup handler that we install already, which
> handles signal propagation, atomicity, etc. It seems like it would be
> simpler to just use that.
That sounds like even a better idiom to achieve the goal.
>
> In the worst case we can just call register_tempfile() on each path, but
> I think this code could be taught to use the actual creation. Something
> like the patch below (only lightly tested).
I've tested it and it works (cleans up both after SIGINT and regular
termination).
(There's only a warning about constness, one should not change the
tempfile's path buffer. But here the ovewrite happens only if there were
trialing dirseps, which they aren't as the filename is under control.)
Do you want me to send your variant as v2 or will you?
Thanks,
Michal
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 265 bytes --]
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 0/3] merge-ll: Cleanup merge driver temporaries after
2026-09-11 14:43 ` Michal Koutný
@ 2026-09-11 17:10 ` Jeff King
2026-09-11 17:11 ` [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result Jeff King
` (2 more replies)
0 siblings, 3 replies; 11+ messages in thread
From: Jeff King @ 2026-09-11 17:10 UTC (permalink / raw)
To: Michal Koutný
Cc: git, Jean Delvare, Elijah Newren, Usman Akinyemi, Taylor Blau,
Junio C Hamano, René Scharfe
On Fri, Sep 11, 2026 at 04:43:08PM +0200, Michal Koutný wrote:
> > In the worst case we can just call register_tempfile() on each path, but
> > I think this code could be taught to use the actual creation. Something
> > like the patch below (only lightly tested).
>
> I've tested it and it works (cleans up both after SIGINT and regular
> termination).
Thanks for testing. I considered putting something in the test suite,
but it gets ugly (we'd have the external driver pause, signal a fifo,
then kill git-merge and it with SIGINT). I guess an alternative would be
setting GIT_ALLOC_LIMIT to something low, and then generating a
too-large output, which would cause xmalloc() to fail, which I believe
would also fail. But then we're not really testing the signal handling.
Hmm. I wonder if leaving the files could actually be a _feature_. If you
completed the merge with the external tool but we barfed reading it back
in, would it be useful to leave the file in place? It's possible, I
suppose, but I think it is more likely to be a nuisance (and we already
delete it for things like read() errors, just not anything that would
cause us to die()).
> (There's only a warning about constness, one should not change the
> tempfile's path buffer. But here the ovewrite happens only if there were
> trialing dirseps, which they aren't as the filename is under control.)
Yeah, I've fixed it in this iteration, plus a few tweaks:
- I did the strbuf cleanup I mentioned (patch 1)
- we should be using close_tempfile_gently() instead of close() on the
tempfiles so that they don't get double-closed when deleting
- that made me notice a small error-checking bug in the original code,
fixed in patch 2
> Do you want me to send your variant as v2 or will you?
Here it is. I've labeled it v2, and I stole your commit message for the
third patch.
[1/3]: merge-ll: use strbuf to read back external merge result
[2/3]: merge-ll: catch close() errors when writing external tempfiles
[3/3]: merge-ll: use tempfile API for external driver files
merge-ll.c | 68 +++++++++++++++++++++++++++++-------------------------
1 file changed, 37 insertions(+), 31 deletions(-)
-Peff
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result
2026-09-11 17:10 ` [PATCH v2 0/3] merge-ll: Cleanup merge driver temporaries after Jeff King
@ 2026-09-11 17:11 ` Jeff King
2026-09-11 18:06 ` Elijah Newren
2026-09-11 17:11 ` [PATCH v2 2/3] merge-ll: catch close() errors when writing external tempfiles Jeff King
2026-09-11 17:13 ` [PATCH v2 3/3] merge-ll: use tempfile API for external driver files Jeff King
2 siblings, 1 reply; 11+ messages in thread
From: Jeff King @ 2026-09-11 17:11 UTC (permalink / raw)
To: Michal Koutný
Cc: git, Jean Delvare, Elijah Newren, Usman Akinyemi, Taylor Blau,
Junio C Hamano, René Scharfe
After the external merge runs, we read the file back into a heap buffer.
This ancient code does it by hand, but these days we can make the code
shorter and less error prone by using strbuf_read_file().
It's not quite a one-liner replacement, because we have to copy the
pointer and size into an mmbuffer_t. Two things to note there:
1. We can't just pass result->size to strbuf_detach(), since the
former uses long instead of size_t (something that we'd ideally fix
in the long run, but is way out of scope here).
2. We can leave result untouched on error; we zero it at the top of
the function (confusingly we may still return LL_MERGE_OK and a
NULL result if we hit an I/O error, but that is how the function
has always behaved, and callers know to check for NULL).
Signed-off-by: Jeff King <peff@peff.net>
---
Not strictly needed for the rest of the series, but it felt like a
cleanup worth doing, and it conflicts textually.
merge-ll.c | 22 +++++++---------------
1 file changed, 7 insertions(+), 15 deletions(-)
diff --git a/merge-ll.c b/merge-ll.c
index ef5287dee8..5b6af15e23 100644
--- a/merge-ll.c
+++ b/merge-ll.c
@@ -201,8 +201,8 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
struct strbuf cmd = STRBUF_INIT;
const char *format = fn->cmdline;
struct child_process child = CHILD_PROCESS_INIT;
- int status, fd, i;
- struct stat st;
+ int status, i;
+ struct strbuf result_buf = STRBUF_INIT;
enum ll_merge_result ret;
assert(opts);
@@ -241,20 +241,12 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
child.use_shell = 1;
strvec_push(&child.args, cmd.buf);
status = run_command(&child);
- fd = open(temp[1], O_RDONLY);
- if (fd < 0)
- goto bad;
- if (fstat(fd, &st))
- goto close_bad;
- result->size = st.st_size;
- result->ptr = xmallocz(result->size);
- if (read_in_full(fd, result->ptr, result->size) != result->size) {
- FREE_AND_NULL(result->ptr);
- result->size = 0;
+
+ if (strbuf_read_file(&result_buf, temp[1], 0) >= 0) {
+ result->size = result_buf.len;
+ result->ptr = strbuf_detach(&result_buf, NULL);
}
- close_bad:
- close(fd);
- bad:
+
for (i = 0; i < 3; i++)
unlink_or_warn(temp[i]);
strbuf_release(&cmd);
--
2.56.0.rc0.314.g7a874b6915
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 2/3] merge-ll: catch close() errors when writing external tempfiles
2026-09-11 17:10 ` [PATCH v2 0/3] merge-ll: Cleanup merge driver temporaries after Jeff King
2026-09-11 17:11 ` [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result Jeff King
@ 2026-09-11 17:11 ` Jeff King
2026-09-11 18:06 ` Elijah Newren
2026-09-11 17:13 ` [PATCH v2 3/3] merge-ll: use tempfile API for external driver files Jeff King
2 siblings, 1 reply; 11+ messages in thread
From: Jeff King @ 2026-09-11 17:11 UTC (permalink / raw)
To: Michal Koutný
Cc: git, Jean Delvare, Elijah Newren, Usman Akinyemi, Taylor Blau,
Junio C Hamano, René Scharfe
When writing out tempfiles for an external merge driver, we catch the
case that write() fails, but not the follow-up close(). This close()
would usually succeed, but the system could report a delayed write error
(e.g., on a network file system).
Signed-off-by: Jeff King <peff@peff.net>
---
merge-ll.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/merge-ll.c b/merge-ll.c
index 5b6af15e23..5a11a9613b 100644
--- a/merge-ll.c
+++ b/merge-ll.c
@@ -180,9 +180,9 @@ static void create_temp(mmfile_t *src, char *path, size_t len)
xsnprintf(path, len, ".merge_file_XXXXXX");
fd = xmkstemp(path);
- if (write_in_full(fd, src->ptr, src->size) < 0)
+ if (write_in_full(fd, src->ptr, src->size) < 0 ||
+ close(fd) < 0)
die_errno("unable to write temp-file");
- close(fd);
}
/*
--
2.56.0.rc0.314.g7a874b6915
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 3/3] merge-ll: use tempfile API for external driver files
2026-09-11 17:10 ` [PATCH v2 0/3] merge-ll: Cleanup merge driver temporaries after Jeff King
2026-09-11 17:11 ` [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result Jeff King
2026-09-11 17:11 ` [PATCH v2 2/3] merge-ll: catch close() errors when writing external tempfiles Jeff King
@ 2026-09-11 17:13 ` Jeff King
2026-09-11 18:10 ` Elijah Newren
2 siblings, 1 reply; 11+ messages in thread
From: Jeff King @ 2026-09-11 17:13 UTC (permalink / raw)
To: Michal Koutný
Cc: git, Jean Delvare, Elijah Newren, Usman Akinyemi, Taylor Blau,
Junio C Hamano, René Scharfe
When there's a long(er) running merge driver helper, the user may just
decide to terminate it with Ctrl+C. That sends a signal to the driver
prog and to the whole process group as well, including the git merge
command proper. Hence the cleanup code would not run and .merge_file_*
files are left behind.
We can fix this by using the tempfile API, which auto-cleans files on
signal or other error. That covers the Ctrl+C case above, as well as any
other incidental death (e.g., allocation error due to a gigantic
output).
Note that there is one gotcha here. The current code uses short,
relative filenames for the tempfiles (like ".merge_file_abc123"). But
the tempfile API stores and returns absolute paths. Because we run the
merge driver as a shell command, this can result in problems if the
leading directories contain shell metacharacters (like our tests, which
put a space in the trash directory name for exactly this purpose).
If we were starting from scratch, I'd say the correct solution here is
to shell-quote the filenames we put in the command. But doing so isn't
strictly backwards compatible, because users might have their own shell
characters. For example, if I configure a driver like this:
[merge "foo"]
driver = "my-driver '%O' '%A' '%B'"
then adding extra quoting will screw things up! Strictly speaking, this
kind of quoting is wrong (it would fail if %A expanded to something with
a single-quote in it), but it is entirely harmless with the current
vanilla relative paths. It doesn't seem worth breaking it.
So let's take the most conservative route, and just continue reporting
the relative paths.
Commit-message-stolen-from: Michal Koutný <mkoutny@suse.com>
Reported-by: Jean Delvare <jdelvare@suse.de>
Signed-off-by: Jeff King <peff@peff.net>
---
merge-ll.c | 50 ++++++++++++++++++++++++++++++++------------------
1 file changed, 32 insertions(+), 18 deletions(-)
diff --git a/merge-ll.c b/merge-ll.c
index 5a11a9613b..ec0f012b4f 100644
--- a/merge-ll.c
+++ b/merge-ll.c
@@ -17,6 +17,7 @@
#include "quote.h"
#include "strbuf.h"
#include "gettext.h"
+#include "tempfile.h"
struct ll_merge_driver;
@@ -174,15 +175,27 @@ static struct ll_merge_driver ll_merge_drv[] = {
{ "union", "built-in union merge", ll_union_merge },
};
-static void create_temp(mmfile_t *src, char *path, size_t len)
+static struct tempfile *create_temp(mmfile_t *src)
{
- int fd;
-
- xsnprintf(path, len, ".merge_file_XXXXXX");
- fd = xmkstemp(path);
- if (write_in_full(fd, src->ptr, src->size) < 0 ||
- close(fd) < 0)
+ struct tempfile *t = xmks_tempfile(".merge_file_XXXXXX");
+ if (write_in_full(t->fd, src->ptr, src->size) < 0 ||
+ close_tempfile_gently(t) < 0)
die_errno("unable to write temp-file");
+ return t;
+}
+
+static const char *temp_path_basename(struct tempfile *t)
+{
+ /*
+ * basename() takes a non-const pointer because it can
+ * modify the input string to remove trailing directory
+ * separators. We know that we don't have any because
+ * this is a clean path generated from our vanilla
+ * tempfile template.
+ *
+ * So casting away the const here is safe, albeit gross.
+ */
+ return basename((char *)get_tempfile_path(t));
}
/*
@@ -197,11 +210,11 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
const struct ll_merge_options *opts,
int marker_size)
{
- char temp[3][50];
+ struct tempfile *tmp_o, *tmp_a, *tmp_b;
struct strbuf cmd = STRBUF_INIT;
const char *format = fn->cmdline;
struct child_process child = CHILD_PROCESS_INIT;
- int status, i;
+ int status;
struct strbuf result_buf = STRBUF_INIT;
enum ll_merge_result ret;
assert(opts);
@@ -211,19 +224,19 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
result->ptr = NULL;
result->size = 0;
- create_temp(orig, temp[0], sizeof(temp[0]));
- create_temp(src1, temp[1], sizeof(temp[1]));
- create_temp(src2, temp[2], sizeof(temp[2]));
+ tmp_o = create_temp(orig);
+ tmp_a = create_temp(src1);
+ tmp_b = create_temp(src2);
while (strbuf_expand_step(&cmd, &format)) {
if (skip_prefix(format, "%", &format))
strbuf_addch(&cmd, '%');
else if (skip_prefix(format, "O", &format))
- strbuf_addstr(&cmd, temp[0]);
+ strbuf_addstr(&cmd, temp_path_basename(tmp_o));
else if (skip_prefix(format, "A", &format))
- strbuf_addstr(&cmd, temp[1]);
+ strbuf_addstr(&cmd, temp_path_basename(tmp_a));
else if (skip_prefix(format, "B", &format))
- strbuf_addstr(&cmd, temp[2]);
+ strbuf_addstr(&cmd, temp_path_basename(tmp_b));
else if (skip_prefix(format, "L", &format))
strbuf_addf(&cmd, "%d", marker_size);
else if (skip_prefix(format, "P", &format))
@@ -242,13 +255,14 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
strvec_push(&child.args, cmd.buf);
status = run_command(&child);
- if (strbuf_read_file(&result_buf, temp[1], 0) >= 0) {
+ if (strbuf_read_file(&result_buf, get_tempfile_path(tmp_a), 0) >= 0) {
result->size = result_buf.len;
result->ptr = strbuf_detach(&result_buf, NULL);
}
- for (i = 0; i < 3; i++)
- unlink_or_warn(temp[i]);
+ delete_tempfile(&tmp_o);
+ delete_tempfile(&tmp_a);
+ delete_tempfile(&tmp_b);
strbuf_release(&cmd);
if (!status)
ret = LL_MERGE_OK;
--
2.56.0.rc0.314.g7a874b6915
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result
2026-09-11 17:11 ` [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result Jeff King
@ 2026-09-11 18:06 ` Elijah Newren
2026-09-11 18:32 ` Junio C Hamano
0 siblings, 1 reply; 11+ messages in thread
From: Elijah Newren @ 2026-09-11 18:06 UTC (permalink / raw)
To: Jeff King
Cc: Michal Koutný, git, Jean Delvare, Usman Akinyemi,
Taylor Blau, Junio C Hamano, René Scharfe
On Fri, Sep 11, 2026 at 10:11 AM Jeff King <peff@peff.net> wrote:
>
> After the external merge runs, we read the file back into a heap buffer.
> This ancient code does it by hand, but these days we can make the code
> shorter and less error prone by using strbuf_read_file().
>
> It's not quite a one-liner replacement, because we have to copy the
> pointer and size into an mmbuffer_t. Two things to note there:
>
> 1. We can't just pass result->size to strbuf_detach(), since the
> former uses long instead of size_t (something that we'd ideally fix
> in the long run, but is way out of scope here).
>
> 2. We can leave result untouched on error; we zero it at the top of
> the function (confusingly we may still return LL_MERGE_OK and a
> NULL result if we hit an I/O error, but that is how the function
> has always behaved, and callers know to check for NULL).
>
> Signed-off-by: Jeff King <peff@peff.net>
> ---
> Not strictly needed for the rest of the series, but it felt like a
> cleanup worth doing, and it conflicts textually.
>
> merge-ll.c | 22 +++++++---------------
> 1 file changed, 7 insertions(+), 15 deletions(-)
>
> diff --git a/merge-ll.c b/merge-ll.c
> index ef5287dee8..5b6af15e23 100644
> --- a/merge-ll.c
> +++ b/merge-ll.c
> @@ -201,8 +201,8 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
> struct strbuf cmd = STRBUF_INIT;
> const char *format = fn->cmdline;
> struct child_process child = CHILD_PROCESS_INIT;
> - int status, fd, i;
> - struct stat st;
> + int status, i;
> + struct strbuf result_buf = STRBUF_INIT;
> enum ll_merge_result ret;
> assert(opts);
>
> @@ -241,20 +241,12 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
> child.use_shell = 1;
> strvec_push(&child.args, cmd.buf);
> status = run_command(&child);
> - fd = open(temp[1], O_RDONLY);
> - if (fd < 0)
> - goto bad;
> - if (fstat(fd, &st))
> - goto close_bad;
> - result->size = st.st_size;
> - result->ptr = xmallocz(result->size);
> - if (read_in_full(fd, result->ptr, result->size) != result->size) {
> - FREE_AND_NULL(result->ptr);
> - result->size = 0;
> +
> + if (strbuf_read_file(&result_buf, temp[1], 0) >= 0) {
> + result->size = result_buf.len;
> + result->ptr = strbuf_detach(&result_buf, NULL);
I know the type mismatch is pre-existing, but the order makes the new
behavior different. On LLP64, assuming the usual wraparound, a result
of LONG_MAX + 101 narrows to the negative value LONG_MIN + 100 .
The old code narrows before xmallocz() , so it requests an impossibly
large allocation and dies. The new code allocates the actual buffer
first, then records a negative size; callers converting that size back
to size_t could read past the allocation.
Would a simple fail-fast make sense?
if (result_buf.len > LONG_MAX)
die(_("external merge result is too large"));
> }
> - close_bad:
> - close(fd);
> - bad:
> +
> for (i = 0; i < 3; i++)
> unlink_or_warn(temp[i]);
> strbuf_release(&cmd);
> --
> 2.56.0.rc0.314.g7a874b6915
Otherwise, looks nice.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 2/3] merge-ll: catch close() errors when writing external tempfiles
2026-09-11 17:11 ` [PATCH v2 2/3] merge-ll: catch close() errors when writing external tempfiles Jeff King
@ 2026-09-11 18:06 ` Elijah Newren
0 siblings, 0 replies; 11+ messages in thread
From: Elijah Newren @ 2026-09-11 18:06 UTC (permalink / raw)
To: Jeff King
Cc: Michal Koutný, git, Jean Delvare, Usman Akinyemi,
Taylor Blau, Junio C Hamano, René Scharfe
On Fri, Sep 11, 2026 at 10:11 AM Jeff King <peff@peff.net> wrote:
>
> When writing out tempfiles for an external merge driver, we catch the
> case that write() fails, but not the follow-up close(). This close()
> would usually succeed, but the system could report a delayed write error
> (e.g., on a network file system).
>
> Signed-off-by: Jeff King <peff@peff.net>
> ---
> merge-ll.c | 4 ++--
> 1 file changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/merge-ll.c b/merge-ll.c
> index 5b6af15e23..5a11a9613b 100644
> --- a/merge-ll.c
> +++ b/merge-ll.c
> @@ -180,9 +180,9 @@ static void create_temp(mmfile_t *src, char *path, size_t len)
>
> xsnprintf(path, len, ".merge_file_XXXXXX");
> fd = xmkstemp(path);
> - if (write_in_full(fd, src->ptr, src->size) < 0)
> + if (write_in_full(fd, src->ptr, src->size) < 0 ||
> + close(fd) < 0)
> die_errno("unable to write temp-file");
> - close(fd);
> }
>
> /*
> --
> 2.56.0.rc0.314.g7a874b6915
I got tripped up at first on this patch; if write_in_full() < 0, then
we won't explicitly close(), but since die will result in an implicit
close, that's not a problem.
Instead, the only thing that changes is we also die if close() fails.
Looks good.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 3/3] merge-ll: use tempfile API for external driver files
2026-09-11 17:13 ` [PATCH v2 3/3] merge-ll: use tempfile API for external driver files Jeff King
@ 2026-09-11 18:10 ` Elijah Newren
0 siblings, 0 replies; 11+ messages in thread
From: Elijah Newren @ 2026-09-11 18:10 UTC (permalink / raw)
To: Jeff King
Cc: Michal Koutný, git, Jean Delvare, Usman Akinyemi,
Taylor Blau, Junio C Hamano, René Scharfe
On Fri, Sep 11, 2026 at 10:13 AM Jeff King <peff@peff.net> wrote:
>
> When there's a long(er) running merge driver helper, the user may just
> decide to terminate it with Ctrl+C. That sends a signal to the driver
> prog and to the whole process group as well, including the git merge
Minor nit:
prog -> program ? or -> process ?
Or maybe tweak whole sentence? : That sends a signal to the whole
foreground process group, including both the driver and the git merge
process.
> command proper. Hence the cleanup code would not run and .merge_file_*
> files are left behind.
>
> We can fix this by using the tempfile API, which auto-cleans files on
> signal or other error. That covers the Ctrl+C case above, as well as any
> other incidental death (e.g., allocation error due to a gigantic
> output).
>
> Note that there is one gotcha here. The current code uses short,
> relative filenames for the tempfiles (like ".merge_file_abc123"). But
> the tempfile API stores and returns absolute paths. Because we run the
> merge driver as a shell command, this can result in problems if the
> leading directories contain shell metacharacters (like our tests, which
> put a space in the trash directory name for exactly this purpose).
>
> If we were starting from scratch, I'd say the correct solution here is
> to shell-quote the filenames we put in the command. But doing so isn't
> strictly backwards compatible, because users might have their own shell
> characters. For example, if I configure a driver like this:
>
> [merge "foo"]
> driver = "my-driver '%O' '%A' '%B'"
>
> then adding extra quoting will screw things up! Strictly speaking, this
> kind of quoting is wrong (it would fail if %A expanded to something with
> a single-quote in it), but it is entirely harmless with the current
> vanilla relative paths. It doesn't seem worth breaking it.
>
> So let's take the most conservative route, and just continue reporting
> the relative paths.
>
> Commit-message-stolen-from: Michal Koutný <mkoutny@suse.com>
:-)
But maybe Commit-message-mostly-stolen-from? Much of your commit
message is understandably about tempfile specifics, which the original
didn't have.
(Yeah, probably not important enough to bother changing; I'm just
"thinking out loud" as I read...)
> Reported-by: Jean Delvare <jdelvare@suse.de>
> Signed-off-by: Jeff King <peff@peff.net>
> ---
> merge-ll.c | 50 ++++++++++++++++++++++++++++++++------------------
> 1 file changed, 32 insertions(+), 18 deletions(-)
>
> diff --git a/merge-ll.c b/merge-ll.c
> index 5a11a9613b..ec0f012b4f 100644
> --- a/merge-ll.c
> +++ b/merge-ll.c
> @@ -17,6 +17,7 @@
> #include "quote.h"
> #include "strbuf.h"
> #include "gettext.h"
> +#include "tempfile.h"
>
> struct ll_merge_driver;
>
> @@ -174,15 +175,27 @@ static struct ll_merge_driver ll_merge_drv[] = {
> { "union", "built-in union merge", ll_union_merge },
> };
>
> -static void create_temp(mmfile_t *src, char *path, size_t len)
> +static struct tempfile *create_temp(mmfile_t *src)
> {
> - int fd;
> -
> - xsnprintf(path, len, ".merge_file_XXXXXX");
> - fd = xmkstemp(path);
> - if (write_in_full(fd, src->ptr, src->size) < 0 ||
> - close(fd) < 0)
> + struct tempfile *t = xmks_tempfile(".merge_file_XXXXXX");
> + if (write_in_full(t->fd, src->ptr, src->size) < 0 ||
> + close_tempfile_gently(t) < 0)
> die_errno("unable to write temp-file");
> + return t;
> +}
> +
> +static const char *temp_path_basename(struct tempfile *t)
> +{
> + /*
> + * basename() takes a non-const pointer because it can
> + * modify the input string to remove trailing directory
> + * separators. We know that we don't have any because
> + * this is a clean path generated from our vanilla
> + * tempfile template.
> + *
> + * So casting away the const here is safe, albeit gross.
> + */
> + return basename((char *)get_tempfile_path(t));
Thanks for the comment.
> }
>
> /*
> @@ -197,11 +210,11 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
> const struct ll_merge_options *opts,
> int marker_size)
> {
> - char temp[3][50];
> + struct tempfile *tmp_o, *tmp_a, *tmp_b;
> struct strbuf cmd = STRBUF_INIT;
> const char *format = fn->cmdline;
> struct child_process child = CHILD_PROCESS_INIT;
> - int status, i;
> + int status;
> struct strbuf result_buf = STRBUF_INIT;
> enum ll_merge_result ret;
> assert(opts);
> @@ -211,19 +224,19 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
>
> result->ptr = NULL;
> result->size = 0;
> - create_temp(orig, temp[0], sizeof(temp[0]));
> - create_temp(src1, temp[1], sizeof(temp[1]));
> - create_temp(src2, temp[2], sizeof(temp[2]));
> + tmp_o = create_temp(orig);
> + tmp_a = create_temp(src1);
> + tmp_b = create_temp(src2);
>
> while (strbuf_expand_step(&cmd, &format)) {
> if (skip_prefix(format, "%", &format))
> strbuf_addch(&cmd, '%');
> else if (skip_prefix(format, "O", &format))
> - strbuf_addstr(&cmd, temp[0]);
> + strbuf_addstr(&cmd, temp_path_basename(tmp_o));
> else if (skip_prefix(format, "A", &format))
> - strbuf_addstr(&cmd, temp[1]);
> + strbuf_addstr(&cmd, temp_path_basename(tmp_a));
> else if (skip_prefix(format, "B", &format))
> - strbuf_addstr(&cmd, temp[2]);
> + strbuf_addstr(&cmd, temp_path_basename(tmp_b));
> else if (skip_prefix(format, "L", &format))
> strbuf_addf(&cmd, "%d", marker_size);
> else if (skip_prefix(format, "P", &format))
> @@ -242,13 +255,14 @@ static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn,
> strvec_push(&child.args, cmd.buf);
> status = run_command(&child);
>
> - if (strbuf_read_file(&result_buf, temp[1], 0) >= 0) {
> + if (strbuf_read_file(&result_buf, get_tempfile_path(tmp_a), 0) >= 0) {
> result->size = result_buf.len;
> result->ptr = strbuf_detach(&result_buf, NULL);
> }
>
> - for (i = 0; i < 3; i++)
> - unlink_or_warn(temp[i]);
> + delete_tempfile(&tmp_o);
> + delete_tempfile(&tmp_a);
> + delete_tempfile(&tmp_b);
> strbuf_release(&cmd);
> if (!status)
> ret = LL_MERGE_OK;
> --
> 2.56.0.rc0.314.g7a874b6915
Looks good to me.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result
2026-09-11 18:06 ` Elijah Newren
@ 2026-09-11 18:32 ` Junio C Hamano
0 siblings, 0 replies; 11+ messages in thread
From: Junio C Hamano @ 2026-09-11 18:32 UTC (permalink / raw)
To: Elijah Newren
Cc: Jeff King, Michal Koutný, git, Jean Delvare, Usman Akinyemi,
Taylor Blau, René Scharfe
Elijah Newren <newren@gmail.com> writes:
> The old code narrows before xmallocz() , so it requests an impossibly
> large allocation and dies. The new code allocates the actual buffer
> first, then records a negative size; callers converting that size back
> to size_t could read past the allocation.
>
> Would a simple fail-fast make sense?
>
> if (result_buf.len > LONG_MAX)
> die(_("external merge result is too large"));
Intereting find. That does sound sensible.
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-09-11 18:32 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-10 15:06 [PATCH] merge-ll: Cleanup merge driver temporaries after interrupt Michal Koutný
2026-09-10 16:22 ` Jeff King
2026-09-11 14:43 ` Michal Koutný
2026-09-11 17:10 ` [PATCH v2 0/3] merge-ll: Cleanup merge driver temporaries after Jeff King
2026-09-11 17:11 ` [PATCH v2 1/3] merge-ll: use strbuf to read back external merge result Jeff King
2026-09-11 18:06 ` Elijah Newren
2026-09-11 18:32 ` Junio C Hamano
2026-09-11 17:11 ` [PATCH v2 2/3] merge-ll: catch close() errors when writing external tempfiles Jeff King
2026-09-11 18:06 ` Elijah Newren
2026-09-11 17:13 ` [PATCH v2 3/3] merge-ll: use tempfile API for external driver files Jeff King
2026-09-11 18:10 ` Elijah Newren
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox