trailers: stop recognizing URLs as trailers

An HTTPS URL starts with an alphanumeric scheme followed by a colon. That means that they will be recognized as trailers in a trailer block. That turns out to be a problem in practice. Let’s stop recognizing these as trailers by failing the trailer parsing when we: 1. find the separator; 2. the separator and the next two characters form `://`; and 3. we haven’t parsed any whitespace yet. The simplest example of how this can be a problem is for people who do not use trailers but may leave URLs at the end of the commit message. Now, while these authors might not use trailers themselves, other authors may have used trailers and this metadata confusion can become a problem once someone tries to extract that metadata (and non-metadata). Let’s now look at some examples in the Linux Kernel[1] to see how this is a problem in practice. There are commits which contain intended non-trailer lines which start with URLs. These are comments. Example with just the trailers:[2] Signed-off-by: Shuai Xue <xueshuai@linux.alibaba.com> [bhelgaas: squash fixes: https://lore.kernel.org/r/20260108013956.14351-2-bagasdotme@gmail.com https://lore.kernel.org/r/20260108013956.14351-3-bagasdotme@gmail.com] Signed-off-by: Bjorn Helgaas <bhelgaas@google.com> Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com> Link: https://patch.msgid.link/20251210132907.58799-4-xueshuai@linux.alibaba.com Those `[]` pairs delimit the “squash fixes” comment. Now, any of these two commands: git log --format='%(trailers:only)' -1 <commit> git log -1 --format=%B <commit> | git interpret-trailers --only-trailers Will both wrongly (according to the surmised user intent) include these two URL lines as trailers and also mangle the URLs, e.g.: https: //lore.kernel.org/r/20260108013956.14351-2-bagasdotme@gmail.com Because the `--only-trailers` mode (or `only` for the git-log(1) format) normalizes the output to a colon and a space. Another example is linewrapping mistakes; a `Link` trailer with a URL where the URL ended up on the next line, presumably because the user’s editor linewrapped the “too long” line. Example with just the trailers:[3] Link: https://patch.msgid.link/20260216-work-xattr-socket-v1-4-c2efa4f74cb7@kernel.org Link: https://lore.kernel.org/3cnmtqmakpbb2uwhenrj7kdqu3uefykiykjllgfbtpkiwhaa4s@sghkevv7jned [1] Acked-by: Darrick J. Wong <djwong@kernel.org> Reviewed-by: Jan Kara <jack@suse.cz> Signed-off-by: Christian Brauner <brauner@kernel.org> Now, this intended trailer is already ruined, but interpreting the URL as a standalone trailer only compounds the mistake. Yet another example is the trailer machinery normalizing the trailer block before application, resulting in a `https` trailer key in the commit message itself. Example with just the trailers:[4] https: //sashiko.dev/#/patchset/20260429114208.941011-1-holger.brunck%40hitachienergy.com Fixes: c19b6d246a35 ("drivers/net: support hdlc function for QE-UCC") Signed-off-by: Holger Brunck <holger.brunck@hitachienergy.com> Link: https://patch.msgid.link/20260507155332.3452319-1-holger.brunck@hitachienergy.com Signed-off-by: Jakub Kicinski <kuba@kernel.org> We have a helpful `Link` that points to the original patch.[5] Following it we can see that that `https` trailer was indeed a URL originally (again just the trailer block here): https://sashiko.dev/#/patchset/20260429114208.941011-1-holger.brunck%40hitachienergy.com Fixes: c19b6d246a35 ("drivers/net: support hdlc function for QE-UCC") Signed-off-by: Holger Brunck <holger.brunck@hitachienergy.com> So how did it end up as a `https` trailer? My theory is that the trailer block was normalized on patch application, causing a URL comment to be wrongly normalized and cemented in the commit message as a trailer.[6] † 1: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ † 2: commit 8236fc613d44e59f6736d6c3e9efffaf26ab7f00 † 3: commit 5bd97f5c5f241a5610c4412d1b93995a26241f81 † 4: commit 496c0c4c53bbe1bad97e82cd12103df61a6e459d † 5: https://patch.msgid.link/20260507155332.3452319-1-holger.brunck@hitachienergy.com † 6: There are only four commits in the Linux Kernel of this kind, and three of them have the same recurring person in the signoff chain. Helped-by: Jeff King <peff@peff.net> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kristoffer Haugsbakk committed Aug 2, 2026 at 21:57 UTC 581183734cde74bfe78af69c36be9bc5ed1c453c
4 files changed +87 -4
Documentation/git-interpret-trailers.adoc
+10 -3
@@ -123,9 +123,16 @@ OTHER RULES
123 What was covered in the previous section are the rules that are relevant
124 for regular use. The following points are included for completeness.
125
126 -This command ignores comment lines (see `core.commentString` in
127 -linkgit:git-config[1]). This is for use with the `prepare-commit-msg`
128 -and `commit-msg` hooks.
126 +--
127 +* This command ignores comment lines (see `core.commentString` in
128 + linkgit:git-config[1]). This is for use with the `prepare-commit-msg`
129 + and `commit-msg` hooks.
130 +
131 +* Candidate trailer lines that have `:` as the separator, that have no
132 + whitespace before the value part, and that start with `//` are not
133 + recognized as trailers. This is to avoid accidentally interpreting
134 + URLs as trailers (e.g. lines that start with `https://`).
135 +--
136
137 OPTIONS
138 -------
t/t7513-interpret-trailers.sh
+19
@@ -1989,4 +1989,23 @@ test_expect_success 'handling of --- lines in conjunction with cut-lines' '
1989 test_cmp expected actual
1990 '
1991
1992 +test_expect_success 'URLs and lines that are not quite URLs' '
1993 + cat >expect <<-\EOF &&
1994 + https: //www.a-trailer.org
1995 + https: //www.another-trailer.org
1996 + Signed-off-by: somebody <somebody@somewhere>
1997 + EOF
1998 + git interpret-trailers --only-trailers >actual <<-\EOF &&
1999 + subject
2000 +
2001 + body
2002 +
2003 + https://www.not-a-trailer.org
2004 + https ://www.a-trailer.org
2005 + https: //www.another-trailer.org
2006 + Signed-off-by: somebody <somebody@somewhere>
2007 + EOF
2008 + test_cmp expect actual
2009 +'
2010 +
2011 test_done
t/unit-tests/u-trailer.c
+52
@@ -318,3 +318,55 @@ void test_trailer__one_non_trailer_no_git_trailers(void)
318 0,
319 expected_contents);
320 }
321 +
322 +void test_trailer__URL(void)
323 +{
324 + struct contents expected_contents[] = { 0 };
325 +
326 + t_trailer_iterator("Subject: foo bar\n"
327 + "\n"
328 + /*
329 + * We do not want to match URLs as trailers.
330 + */
331 + "https://www.example.org\n",
332 + 0,
333 + expected_contents);
334 +}
335 +
336 +void test_trailer__not_a_URL_space_after_separator(void)
337 +{
338 + struct contents expected_contents[] = {
339 + { .raw = "https: //www.example.org\n",
340 + .key = "https",
341 + .val = "//www.example.org" },
342 + { 0 },
343 + };
344 +
345 + t_trailer_iterator("Subject: foo bar\n"
346 + "\n"
347 + /*
348 + * This has a space after ':' so it's not a URL.
349 + */
350 + "https: //www.example.org\n",
351 + 1,
352 + expected_contents);
353 +}
354 +
355 +void test_trailer__not_a_URL_space_before_separator(void)
356 +{
357 + struct contents expected_contents[] = {
358 + { .raw = "https ://www.example.org\n",
359 + .key = "https",
360 + .val = "//www.example.org" },
361 + { 0 },
362 + };
363 +
364 + t_trailer_iterator("Subject: foo bar\n"
365 + "\n"
366 + /*
367 + * This has a space before ':' so it's not a URL.
368 + */
369 + "https ://www.example.org\n",
370 + 1,
371 + expected_contents);
372 +}
trailer.c
+6 -1
@@ -635,8 +635,13 @@ static ssize_t find_separator(const char *line, const char *separators)
635 int whitespace_found = 0;
636 const char *c;
637 for (c = line; *c; c++) {
638 - if (strchr(separators, *c))
638 + if (strchr(separators, *c)) {
639 + /* avoid accidental URL matches (://) */
640 + if (*c == ':' && c[1] == '/' && c[2] == '/' &&
641 + !whitespace_found)
642 + return -1;
643 return c - line;
644 + }
645 if (!whitespace_found && (isalnum(*c) || *c == '-'))
646 continue;
647 if (c != line && (*c == ' ' || *c == '\t')) {