{"thread":{"id":"61052","subject":"Clarify the meaning of \"character\" in the documentation","startedAt":"2024-03-05T08:43:56Z","lastAt":"2024-03-28T09:47:47Z","messageCount":82,"participants":["Manlio Perillo","Kristoffer Haugsbakk","Junio C Hamano","Dragan Simic","brian m. carlson","Jeff King","René Scharfe","Phillip Wood","phillip.wood123@gmail.com","Chris Torek"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"489944","messageId":"CAAToxAEE-2B_8Wqrquk1peOnqTbOpV_8KZmsJ2dgk-mfCZXHiA@mail.gmail.com","threadId":"61052","inReplyTo":null,"subject":"Clarify the meaning of \"character\" in the documentation","fromName":"Manlio Perillo","fromEmail":"manlio.perillo@gmail.com","sentAt":"2024-03-05T08:43:42Z","receivedAt":"2024-03-05T08:43:56Z","isPatch":false,"sender":{"key":"manlio.perillo@gmail.com","avatar":"https://avatars.githubusercontent.com/u/6217088?v=4"},"body":"The term \"character\" is confusing: does it mean 7bit/ASCII character\nor Unicode Code Point?\n\nAs an example, with\ngit config --add core.commentChar •  // Bullet (U+2022)\ngit does not complain, but it is rejected later.\n\nA counter example is using UTF-8 with \"user.name\", where it is handled\ncorrectly.\n\nI sent this email after reading the documentation of \"git diff\n--color-moved=blocks, where the text says:\n> Blocks of moved text of at least 20 alphanumeric characters are detected greedily.\n\nIn this case it is not clear if the number of characters are counted\nas UTF-8 or normal 8bit bytes.\n\nThanks\nManlio Perillo\n"},{"id":"489945","messageId":"5e10f1e5-b87f-43cd-ac1e-d7c01b7dad21@app.fastmail.com","threadId":"61052","inReplyTo":"CAAToxAEE-2B_8Wqrquk1peOnqTbOpV_8KZmsJ2dgk-mfCZXHiA@mail.gmail.com","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-05T09:00:06Z","receivedAt":"2024-03-05T09:01:53Z","isPatch":false,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"\nOn Tue, Mar 5, 2024, at 09:43, Manlio Perillo wrote:\n> The term \"character\" is confusing: does it mean 7bit/ASCII character\n> or Unicode Code Point?\n\nIMO it should say “ASCII” in contexts where it is restricted to\nthat. Otherwise UTF-8 can be assumed since git(1) handles that well.\n\n> As an example, with\n> git config --add core.commentChar •  // Bullet (U+2022)\n> git does not complain, but it is rejected later.\n\nI think this is more about `git config --add` not doing any\nvalidation. It just sets things. You can do `git config --add\ncore.commentChar 'ffd'` and get the same effect.\n\n> A counter example is using UTF-8 with \"user.name\", where it is handled\n> correctly.\n\nYep.\n\nIt will also handle UTF-8 in cross-systems setting, in my experience: if\nyou generate patches with git-format-patch(1) it will handle UTF-8 that\nends up in email headers correctly (it needs its own encoding).\n\nIt’s quite UTF-8 friendly.\n\n> I sent this email after reading the documentation of \"git diff\n> --color-moved=blocks, where the text says:\n>> Blocks of moved text of at least 20 alphanumeric characters are detected greedily.\n>\n> In this case it is not clear if the number of characters are counted\n> as UTF-8 or normal 8bit bytes.\n\nAlphanumeric characters (a-z and A-Z and 0-9) are ASCII. And one ASCII\nchar is represented using one byte in UTF-8. This already looks precise\nto me.\n\nI’ve never run into a case where git-diff(1) does not handle UTF-8. I\ndon’t even know if it really needs to “handle” it per se as opposed to\njust treating it as opaque bytes. Maybe it matters for things like\nwhitespace and word-boundaries, I don’t know.\n\n-- \nKristoffer Haugsbakk\n\n"},{"id":"489961","messageId":"xmqqsf1466eh.fsf@gitster.g","threadId":"61052","inReplyTo":"5e10f1e5-b87f-43cd-ac1e-d7c01b7dad21@app.fastmail.com","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-05T15:32:38Z","receivedAt":"2024-03-05T15:32:50Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Kristoffer Haugsbakk\" <code@khaugsbakk.name> writes:\n\n>> As an example, with\n>> git config --add core.commentChar •  // Bullet (U+2022)\n>> git does not complain, but it is rejected later.\n>\n> I think this is more about `git config --add` not doing any\n> validation. It just sets things. You can do `git config --add\n> core.commentChar 'ffd'` and get the same effect.\n\nThis is not wrong per-se, but it merely explains why \"config\" takes\nit without complaining (the command just does not know anything\nabout what each variable means and what the valid range of values\nare).  core.commentChar is limited to \"a byte\" so in the context of\neverything else (like commit log message in the editor) being UTF-8,\nit means ASCII would only work there.\n\nAs you said, we should document core.commentChar as limited to an\nASCII character, at least as a short term solution.\n\nI personally do not see a reason, however, why we need to be limited\nto a single byte, though.  If a patch cleanly implements to allow us\nto use any one-or-more-byte sequence as core.commentChar, I do not\noffhand see a good reason to reject it---it would be fully backward\ncompatible and allows you to use a UTF-8 charcter outside ASCII, as\nwell as \"//\" and the like.\n\n> Alphanumeric characters (a-z and A-Z and 0-9) are ASCII. And one ASCII\n> char is represented using one byte in UTF-8. This already looks precise\n> to me.\n\nCorrect.\n\n> I’ve never run into a case where git-diff(1) does not handle UTF-8. I\n> don’t even know if it really needs to “handle” it per se as opposed to\n> just treating it as opaque bytes. Maybe it matters for things like\n> whitespace and word-boundaries, I don’t know.\n\nThe core part of \"diff\" is very much line oriented, and after\nchopping your random sequence of bytes at each LF that appears in\nit, the code is pretty oblivious to the character boundary, except\nfor a few cases.  \"-w\" needs to know what the whitespace characters\nare (it knows only the limited basic set like SP HT and probably\nVT), \"-i\" needs to know that \"A\" and \"a\" are equivalent (I think it\nonly knows the ASCII, but I may be misremembering).  Outside the\ncore part of \"diff\", there are frills that need to know about\ncharacter boundaries, like chopping the function header comment\nplaced on a hunk header \"@@ -1682,7 +1682,7 @@\" to a reasonable\nlength, --color-words/--word-diff that first separates lines into\nmulti-character tokens and align matching sequences in them, etc.\n"},{"id":"489962","messageId":"52d6850914982ffaf15dda937d611ffb@manjaro.org","threadId":"61052","inReplyTo":"xmqqsf1466eh.fsf@gitster.g","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Dragan Simic","fromEmail":"dsimic@manjaro.org","sentAt":"2024-03-05T15:42:07Z","receivedAt":"2024-03-05T15:42:15Z","isPatch":false,"sender":{"key":"dsimic@manjaro.org","avatar":null},"body":"On 2024-03-05 16:32, Junio C Hamano wrote:\n> \"Kristoffer Haugsbakk\" <code@khaugsbakk.name> writes:\n>> I think this is more about `git config --add` not doing any\n>> validation. It just sets things. You can do `git config --add\n>> core.commentChar 'ffd'` and get the same effect.\n> \n> As you said, we should document core.commentChar as limited to an\n> ASCII character, at least as a short term solution.\n> \n> I personally do not see a reason, however, why we need to be limited\n> to a single byte, though.  If a patch cleanly implements to allow us\n> to use any one-or-more-byte sequence as core.commentChar, I do not\n> offhand see a good reason to reject it---it would be fully backward\n> compatible and allows you to use a UTF-8 charcter outside ASCII, as\n> well as \"//\" and the like.\n\nMay I ask why would we want the comment character to possibly be\na multibyte character?  I mean, I support localization, to make it all\neasier for the users who opt not to use English, but wouldn't allowing\nmultibyte characters for the comment character simply be a bit unneeded?\n\nMaybe I'm missing something?\n"},{"id":"489966","messageId":"9633f9be5ddd9ab3df4b79ee934e1ed47e90bd1d.1709656683.git.code@khaugsbakk.name","threadId":"61052","inReplyTo":"xmqqsf1466eh.fsf@gitster.g","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-05T16:51:08Z","receivedAt":"2024-03-05T16:52:01Z","isPatch":false,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Tue, Mar 5, 2024, at 16:32, Junio C Hamano wrote:\n> \"Kristoffer Haugsbakk\" <code@khaugsbakk.name> writes:\n>\n>>> As an example, with\n>>> git config --add core.commentChar •  // Bullet (U+2022)\n>>> git does not complain, but it is rejected later.\n>>\n>> I think this is more about `git config --add` not doing any\n>> validation. It just sets things. You can do `git config --add\n>> core.commentChar 'ffd'` and get the same effect.\n>\n> This is not wrong per-se, but it merely explains why \"config\" takes\n> it without complaining (the command just does not know anything\n> about what each variable means and what the valid range of values\n> are).  core.commentChar is limited to \"a byte\" so in the context of\n> everything else (like commit log message in the editor) being UTF-8,\n> it means ASCII would only work there.\n\nYep, I neglected to mention that part.\n\n> I personally do not see a reason, however, why we need to be limited\n> to a single byte, though.  If a patch cleanly implements to allow us\n> to use any one-or-more-byte sequence as core.commentChar, I do not\n> offhand see a good reason to reject it---it would be fully backward\n> compatible and allows you to use a UTF-8 charcter outside ASCII, as\n> well as \"//\" and the like.\n\nAllow one codepoint or a string? Since a Unicode “character” can be\ncomposed of multiple codepoints. And at that point it might be more work\nto validate that it is a “character” compared to allowing any kind of\nstring.\n\nMaybe introduce `core.commentString` and make it a synonym for\n`core.commentChar`?\n\n> The core part of \"diff\" is very much line oriented, and after\n> chopping your random sequence of bytes at each LF that appears in\n> it, the code is pretty oblivious to the character boundary, except\n> for a few cases.  \"-w\" needs to know what the whitespace characters\n> are (it knows only the limited basic set like SP HT and probably\n> VT), \"-i\" needs to know that \"A\" and \"a\" are equivalent (I think it\n> only knows the ASCII, but I may be misremembering).  Outside the\n> core part of \"diff\", there are frills that need to know about\n> character boundaries, like chopping the function header comment\n> placed on a hunk header \"@@ -1682,7 +1682,7 @@\" to a reasonable\n> length, --color-words/--word-diff that first separates lines into\n> multi-character tokens and align matching sequences in them, etc.\n\nAh, interesting. Thanks :)\n\n> As you said, we should document core.commentChar as limited to an\n> ASCII character, at least as a short term solution.\n\nAha, I see now that the config documentation doesn’t make that clear.\n\n-- >8 --\nSubject: [PATCH] config: document `core.commentChar` as ASCII-only\n\nd3b3419f8f2 (config: tell the user that we expect an ASCII character,\n2023-03-27) updated an error message to make clear that this option\nspecifically wants an ASCII character but neglected to consider the\nconfig documentation.\n\nReported-by: Manlio Perillo <manlio.perillo@gmail.com>\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    I didn’t find any other relevant occurences with\n    \n        git grep 'commentChar' -- ':(exclude)po'\n    \n    `Documentation/git-commit.txt` mentions it but it doesn’t seem like a\n    clarification is needed in that context.\n\n Documentation/config/core.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex 0e8c2832bf9..2d4bbdb25fa 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -521,7 +521,7 @@ core.editor::\n \n core.commentChar::\n \tCommands such as `commit` and `tag` that let you edit\n-\tmessages consider a line that begins with this character\n+\tmessages consider a line that begins with this ASCII character\n \tcommented, and removes them after the editor returns\n \t(default '#').\n +\n-- \n2.44.0.64.g52b67adbeb2\n\n"},{"id":"489967","messageId":"3bef4fef-0a00-4ed8-878e-372c4d8f552d@app.fastmail.com","threadId":"61052","inReplyTo":"52d6850914982ffaf15dda937d611ffb@manjaro.org","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-05T16:58:59Z","receivedAt":"2024-03-05T16:59:22Z","isPatch":false,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Tue, Mar 5, 2024, at 16:42, Dragan Simic wrote:\n>\n> May I ask why would we want the comment character to possibly be\n> a multibyte character?  I mean, I support localization, to make it all\n> easier for the users who opt not to use English, but wouldn't allowing\n> multibyte characters for the comment character simply be a bit unneeded?\n>\n> Maybe I'm missing something?\n\nPersonally I think it’s okay. `%` for example is a good candidate since\nyou seldom use that as a leading character in prose (after a\nwhitespace), and it seems that `%` is often recommended as an\nalternative.\n\nBut if it doesn’t make the code more complex: why not? (I just\npersonally don’t have a use-case.)\n"},{"id":"489970","messageId":"xmqqmsrc4osm.fsf@gitster.g","threadId":"61052","inReplyTo":"52d6850914982ffaf15dda937d611ffb@manjaro.org","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-05T16:38:17Z","receivedAt":"2024-03-05T17:13:54Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Dragan Simic <dsimic@manjaro.org> writes:\n\n> On 2024-03-05 16:32, Junio C Hamano wrote:\n>> \"Kristoffer Haugsbakk\" <code@khaugsbakk.name> writes:\n>>> I think this is more about `git config --add` not doing any\n>>> validation. It just sets things. You can do `git config --add\n>>> core.commentChar 'ffd'` and get the same effect.\n>> As you said, we should document core.commentChar as limited to an\n>> ASCII character, at least as a short term solution.\n>> I personally do not see a reason, however, why we need to be limited\n>> to a single byte, though.  If a patch cleanly implements to allow us\n>> to use any one-or-more-byte sequence as core.commentChar, I do not\n>> offhand see a good reason to reject it---it would be fully backward\n>> compatible and allows you to use a UTF-8 charcter outside ASCII, as\n>> well as \"//\" and the like.\n>\n> May I ask why would we want the comment character to possibly be\n> a multibyte character?  I mean, I support localization, to make it all\n> easier for the users who opt not to use English, but wouldn't allowing\n> multibyte characters for the comment character simply be a bit unneeded?\n>\n> Maybe I'm missing something?\n\nThat's not a question for me ;-).\n\nIt is not my personal itch, so I haven't done anything to make the\ncommentChar take more than one byte.  But if it is somebody else's\nitch, I do not see a reason why we should forbid them from\nscratching.  If the setting seeps through across repository\nboundaries, that may create a compatibility issue and that by itself\nmight be such a reason.  If it greatly makes the code more complex,\nthat may be another reason you can use to argue against adding such\na \"feature\".  If it makes the semantics of what \"a comment string\"\nis and how they are added and stripped at various stages of\nprocessing commit log messages fuzzy and harder to document and\nunderstand, that might be another reason.  I however do not think\nany of these to be true.  Maybe I am overly optimistic.  I haven't\nlooked deeply into the code around commentChar for quite some time.\n\n\n"},{"id":"489971","messageId":"1e71ce757c3d773fd7354cd12473b851@manjaro.org","threadId":"61052","inReplyTo":"3bef4fef-0a00-4ed8-878e-372c4d8f552d@app.fastmail.com","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Dragan Simic","fromEmail":"dsimic@manjaro.org","sentAt":"2024-03-05T17:20:27Z","receivedAt":"2024-03-05T17:20:29Z","isPatch":false,"sender":{"key":"dsimic@manjaro.org","avatar":null},"body":"On 2024-03-05 17:58, Kristoffer Haugsbakk wrote:\n> On Tue, Mar 5, 2024, at 16:42, Dragan Simic wrote:\n>> May I ask why would we want the comment character to possibly be\n>> a multibyte character?  I mean, I support localization, to make it all\n>> easier for the users who opt not to use English, but wouldn't allowing\n>> multibyte characters for the comment character simply be a bit \n>> unneeded?\n>> \n>> Maybe I'm missing something?\n> \n> Personally I think it’s okay. `%` for example is a good candidate since\n> you seldom use that as a leading character in prose (after a\n> whitespace), and it seems that `%` is often recommended as an\n> alternative.\n\nIsn't '%' actually an ASCII character?\n\n> But if it doesn’t make the code more complex: why not? (I just\n> personally don’t have a use-case.)\n\nFrankly, allowing multibyte characters as comment characters looks to me\nmore like a programming exercise than a really needed feature.\n"},{"id":"489972","messageId":"1a4a04fd6b99e1de1696563cecbe14d1@manjaro.org","threadId":"61052","inReplyTo":"xmqqmsrc4osm.fsf@gitster.g","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Dragan Simic","fromEmail":"dsimic@manjaro.org","sentAt":"2024-03-05T17:28:46Z","receivedAt":"2024-03-05T17:28:48Z","isPatch":false,"sender":{"key":"dsimic@manjaro.org","avatar":null},"body":"On 2024-03-05 17:38, Junio C Hamano wrote:\n> Dragan Simic <dsimic@manjaro.org> writes:\n>> On 2024-03-05 16:32, Junio C Hamano wrote:\n>>> \"Kristoffer Haugsbakk\" <code@khaugsbakk.name> writes:\n>>>> I think this is more about `git config --add` not doing any\n>>>> validation. It just sets things. You can do `git config --add\n>>>> core.commentChar 'ffd'` and get the same effect.\n>>> As you said, we should document core.commentChar as limited to an\n>>> ASCII character, at least as a short term solution.\n>>> I personally do not see a reason, however, why we need to be limited\n>>> to a single byte, though.  If a patch cleanly implements to allow us\n>>> to use any one-or-more-byte sequence as core.commentChar, I do not\n>>> offhand see a good reason to reject it---it would be fully backward\n>>> compatible and allows you to use a UTF-8 charcter outside ASCII, as\n>>> well as \"//\" and the like.\n>> \n>> May I ask why would we want the comment character to possibly be\n>> a multibyte character?  I mean, I support localization, to make it all\n>> easier for the users who opt not to use English, but wouldn't allowing\n>> multibyte characters for the comment character simply be a bit \n>> unneeded?\n>> \n>> Maybe I'm missing something?\n> \n> That's not a question for me ;-).\n> \n> It is not my personal itch, so I haven't done anything to make the\n> commentChar take more than one byte.  But if it is somebody else's\n> itch, I do not see a reason why we should forbid them from\n> scratching.  If the setting seeps through across repository\n> boundaries, that may create a compatibility issue and that by itself\n> might be such a reason.  If it greatly makes the code more complex,\n> that may be another reason you can use to argue against adding such\n> a \"feature\".  If it makes the semantics of what \"a comment string\"\n> is and how they are added and stripped at various stages of\n> processing commit log messages fuzzy and harder to document and\n> understand, that might be another reason.  I however do not think\n> any of these to be true.  Maybe I am overly optimistic.  I haven't\n> looked deeply into the code around commentChar for quite some time.\n\nYes, there are quite a few possible obstacles.  As I replied to \nKristoffer\na bit earlier, I see this more as a programming exercise.  Of course,\nunless someone really needs it as a new feature, in which case they will\nprobably need to overcome all those obstacles. :)\n"},{"id":"489973","messageId":"6de8764e-9764-4c2b-ac2e-ca5e8480908a@app.fastmail.com","threadId":"61052","inReplyTo":"1e71ce757c3d773fd7354cd12473b851@manjaro.org","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-05T17:37:01Z","receivedAt":"2024-03-05T17:37:23Z","isPatch":false,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Tue, Mar 5, 2024, at 18:20, Dragan Simic wrote:\n> On 2024-03-05 17:58, Kristoffer Haugsbakk wrote:\n>> Personally I think it’s okay. `%` for example is a good candidate since\n>> you seldom use that as a leading character in prose (after a\n>> whitespace), and it seems that `%` is often recommended as an\n>> alternative.\n>\n> Isn't '%' actually an ASCII character?\n\nI wasn’t clear: personally I think the status quo of only allowing ASCII\ncharacters seems fine given that you can use something like `%` as an\nalternative comment char.\n\n-- \nKristoffer Haugsbakk\n\n"},{"id":"489974","messageId":"xmqqedco37hh.fsf@gitster.g","threadId":"61052","inReplyTo":"9633f9be5ddd9ab3df4b79ee934e1ed47e90bd1d.1709656683.git.code@khaugsbakk.name","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-05T17:37:30Z","receivedAt":"2024-03-05T17:37:36Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Kristoffer Haugsbakk <code@khaugsbakk.name> writes:\n\n>> I personally do not see a reason, however, why we need to be limited\n>> to a single byte, though.  If a patch cleanly implements to allow us\n>> to use any one-or-more-byte sequence as core.commentChar, I do not\n>> offhand see a good reason to reject it---it would be fully backward\n>> compatible and allows you to use a UTF-8 charcter outside ASCII, as\n>> well as \"//\" and the like.\n>\n> Allow one codepoint or a string?\n\nI said \"any one-or-more-byte sequence\" and I meant it.  It does not\neven have to be a full and complete UTF-8 character.  As long as we\ncorrectly prefix the sequence and strip it from the front, I do not\ncare if the user chooses to use a broken half-character ;-).\n\n> Maybe introduce `core.commentString` and make it a synonym for\n> `core.commentChar`?\n\nYes, if we were to do so.  As I already said, this is not my itch,\nbut such a synonym would be part of the migration plan if somebody\nseriously designs this as a new feature.\n\n> diff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\n> index 0e8c2832bf9..2d4bbdb25fa 100644\n> --- a/Documentation/config/core.txt\n> +++ b/Documentation/config/core.txt\n> @@ -521,7 +521,7 @@ core.editor::\n>  \n>  core.commentChar::\n>  \tCommands such as `commit` and `tag` that let you edit\n> -\tmessages consider a line that begins with this character\n> +\tmessages consider a line that begins with this ASCII character\n>  \tcommented, and removes them after the editor returns\n>  \t(default '#').\n>  +\n\nLooks sensible.  Thanks.  Will queue.\n\n"},{"id":"489975","messageId":"33966e29-ca9e-4835-933a-9316b3c6c24b@app.fastmail.com","threadId":"61052","inReplyTo":"xmqqedco37hh.fsf@gitster.g","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-05T17:49:29Z","receivedAt":"2024-03-05T17:49:51Z","isPatch":false,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Tue, Mar 5, 2024, at 18:37, Junio C Hamano wrote:\n>> Maybe introduce `core.commentString` and make it a synonym for\n>> `core.commentChar`?\n>\n> Yes, if we were to do so.  As I already said, this is not my itch,\n> but such a synonym would be part of the migration plan if somebody\n> seriously designs this as a new feature.\n\nMaybe someone will discover an itch:\n\nhttps://github.com/gitgitgadget/git/issues/1685\n\n-- \nKristoffer Haugsbakk\n\n"},{"id":"489993","messageId":"1c2ee0c45053e1b60e8ea64c19b6c292@manjaro.org","threadId":"61052","inReplyTo":"6de8764e-9764-4c2b-ac2e-ca5e8480908a@app.fastmail.com","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"Dragan Simic","fromEmail":"dsimic@manjaro.org","sentAt":"2024-03-05T21:19:51Z","receivedAt":"2024-03-05T21:19:53Z","isPatch":false,"sender":{"key":"dsimic@manjaro.org","avatar":null},"body":"On 2024-03-05 18:37, Kristoffer Haugsbakk wrote:\n> On Tue, Mar 5, 2024, at 18:20, Dragan Simic wrote:\n>> On 2024-03-05 17:58, Kristoffer Haugsbakk wrote:\n>>> Personally I think it’s okay. `%` for example is a good candidate \n>>> since\n>>> you seldom use that as a leading character in prose (after a\n>>> whitespace), and it seems that `%` is often recommended as an\n>>> alternative.\n>> \n>> Isn't '%' actually an ASCII character?\n> \n> I wasn’t clear: personally I think the status quo of only allowing \n> ASCII\n> characters seems fine given that you can use something like `%` as an\n> alternative comment char.\n\nAh, I see.  Thanks for the clarification.\n"},{"id":"490032","messageId":"ZeehSl-u3qAXKjF_@tapette.crustytoothpaste.net","threadId":"61052","inReplyTo":"5e10f1e5-b87f-43cd-ac1e-d7c01b7dad21@app.fastmail.com","subject":"Re: Clarify the meaning of \"character\" in the documentation","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2024-03-05T22:48:42Z","receivedAt":"2024-03-05T22:48:50Z","isPatch":false,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2024-03-05 at 09:00:06, Kristoffer Haugsbakk wrote:\n> \n> On Tue, Mar 5, 2024, at 09:43, Manlio Perillo wrote:\n> > I sent this email after reading the documentation of \"git diff\n> > --color-moved=blocks, where the text says:\n> >> Blocks of moved text of at least 20 alphanumeric characters are detected greedily.\n> >\n> > In this case it is not clear if the number of characters are counted\n> > as UTF-8 or normal 8bit bytes.\n> \n> Alphanumeric characters (a-z and A-Z and 0-9) are ASCII. And one ASCII\n> char is represented using one byte in UTF-8. This already looks precise\n> to me.\n\nI don't believe that's an appropriate definition. é is an alphanumeric\ncharacter, as is ç.  ½ is numeric.  I would argue an alphanumeric\ncharacter comprises at least Unicode classes Ll, Lm, Lo, Lt, Lu, and Nd.\nUnicode TR#18 agrees with my assessment.\n\nIf we wanted to restrict it ASCII, we need to state that explicitly.\nAlternately, if the constraint is 20 UTF-8 octets or something else, we\nshould state that instead.\n-- \nbrian m. carlson (he/him or they/them)\nToronto, Ontario, CA\n"},{"id":"490062","messageId":"20240306080804.GB4099518@coredump.intra.peff.net","threadId":"61052","inReplyTo":"xmqqmsrc4osm.fsf@gitster.g","subject":"[messy PATCH] multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-06T08:08:04Z","receivedAt":"2024-03-06T08:08:05Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Mar 05, 2024 at 08:38:17AM -0800, Junio C Hamano wrote:\n\n> It is not my personal itch, so I haven't done anything to make the\n> commentChar take more than one byte.  But if it is somebody else's\n> itch, I do not see a reason why we should forbid them from\n> scratching.  If the setting seeps through across repository\n> boundaries, that may create a compatibility issue and that by itself\n> might be such a reason.  If it greatly makes the code more complex,\n> that may be another reason you can use to argue against adding such\n> a \"feature\".  If it makes the semantics of what \"a comment string\"\n> is and how they are added and stripped at various stages of\n> processing commit log messages fuzzy and harder to document and\n> understand, that might be another reason.  I however do not think\n> any of these to be true.  Maybe I am overly optimistic.  I haven't\n> looked deeply into the code around commentChar for quite some time.\n\nHere's a messy version of what it would look like, in case anybody is\ninterested. It passes the tests (using the string \"#\" for the most\npart), but there may be corner cases lurking. The %c/%s conversions are\nnoisy but obvious. The trickier parts are matching, which goes from\nsingle-character to a string match. I used starts_with() but had to\nintroduce a \"_mem\" variant for buffers that aren't NUL-terminated.\nThere's a bit of mild refactoring/cleanup to avoid awkwardness in a few\nspots, as well.\n\nI also did a few manual tests with \"foo>\" and \"•\" as comment chars,\nwhich seemed to work.\n\nI can't imagine using this myself (I don't even set core.commentChar at\nall), so it was mostly that I nerd-sniped myself by thinking \"how hard\ncould it be?\". Not too bad, but not trivial. But maybe it spurs somebody\ninterested in working on it. I am on the fence whether supporting UTF-8\nlike the bullet-point above is maybe something we should just do on\nprinciple.\n\nFor a more readable series, I'd guess it would make sense to introduce\ncomment_line_str as a separate variable (but continue to enforce the\nsingle-char rule), convert the easy cases en masse, the tricky cases one\nby one, and then finally drop comment_line_char entirely. At which point\nthe config rules can be lifted to allow multi-byte strings.\n\n---\n add-patch.c       |  4 ++--\n builtin/branch.c  |  2 +-\n builtin/commit.c  | 19 ++++++++++------\n builtin/merge.c   |  2 +-\n builtin/tag.c     |  4 ++--\n commit.c          |  3 ++-\n config.c          |  7 +++---\n environment.c     |  2 +-\n environment.h     |  2 +-\n fmt-merge-msg.c   |  2 +-\n sequencer.c       | 28 ++++++++++++-----------\n strbuf.c          | 43 +++++++++++++++++++----------------\n strbuf.h          |  7 +++---\n t/t7508-status.sh |  4 +++-\n trailer.c         |  6 ++---\n wt-status.c       | 23 +++++++------------\n 16 files changed, 82 insertions(+), 76 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 68f525b35c..4b4db0f253 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1114,7 +1114,7 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \t\t\t\t\"To remove '%c' lines, make them ' ' lines \"\n \t\t\t\t\"(context).\\n\"\n \t\t\t\t\"To remove '%c' lines, delete them.\\n\"\n-\t\t\t\t\"Lines starting with %c will be removed.\\n\"),\n+\t\t\t\t\"Lines starting with %s will be removed.\\n\"),\n \t\t\t      s->mode->is_reverse ? '+' : '-',\n \t\t\t      s->mode->is_reverse ? '-' : '+',\n \t\t\t      comment_line_char);\n@@ -1139,7 +1139,7 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \tfor (i = 0; i < s->buf.len; ) {\n \t\tsize_t next = find_next_line(&s->buf, i);\n \n-\t\tif (s->buf.buf[i] != comment_line_char)\n+\t\tif (!starts_with(s->buf.buf + i, comment_line_char))\n \t\t\tstrbuf_add(&s->plain, s->buf.buf + i, next - i);\n \t\ti = next;\n \t}\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex cfb63cce5f..0b6e1d1adb 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -670,7 +670,7 @@ static int edit_branch_description(const char *branch_name)\n \tstrbuf_commented_addf(&buf, comment_line_char,\n \t\t    _(\"Please edit the description for the branch\\n\"\n \t\t      \"  %s\\n\"\n-\t\t      \"Lines starting with '%c' will be stripped.\\n\"),\n+\t\t      \"Lines starting with '%s' will be stripped.\\n\"),\n \t\t    branch_name, comment_line_char);\n \twrite_file_buf(edit_description(), buf.buf, buf.len);\n \tstrbuf_reset(&buf);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 6d1fa71676..898c8aadc7 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -678,15 +678,20 @@ static int author_date_is_interesting(void)\n \treturn author_message || force_date;\n }\n \n+/*\n+ * This only supports single-byte comment chars, but that's OK;\n+ * our candidate list is fixed.\n+ */\n static void adjust_comment_line_char(const struct strbuf *sb)\n {\n \tchar candidates[] = \"#;@!$%^&|:\";\n \tchar *candidate;\n \tconst char *p;\n \n-\tcomment_line_char = candidates[0];\n-\tif (!memchr(sb->buf, comment_line_char, sb->len))\n+\tif (!memchr(sb->buf, candidates[0], sb->len)) {\n+\t\tcomment_line_char = xstrfmt(\"%c\", candidates[0]);\n \t\treturn;\n+\t}\n \n \tp = sb->buf;\n \tcandidate = strchr(candidates, *p);\n@@ -705,7 +710,7 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n \tif (!*p)\n \t\tdie(_(\"unable to select a comment character that is not used\\n\"\n \t\t      \"in the current commit message\"));\n-\tcomment_line_char = *p;\n+\tcomment_line_char = xstrfmt(\"%c\", *p);\n }\n \n static void prepare_amend_commit(struct commit *commit, struct strbuf *sb,\n@@ -909,18 +914,18 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tstruct ident_split ci, ai;\n \t\tconst char *hint_cleanup_all = allow_empty_message ?\n \t\t\t_(\"Please enter the commit message for your changes.\"\n-\t\t\t  \" Lines starting\\nwith '%c' will be ignored.\\n\") :\n+\t\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\") :\n \t\t\t_(\"Please enter the commit message for your changes.\"\n-\t\t\t  \" Lines starting\\nwith '%c' will be ignored, and an empty\"\n+\t\t\t  \" Lines starting\\nwith '%s' will be ignored, and an empty\"\n \t\t\t  \" message aborts the commit.\\n\");\n \t\tconst char *hint_cleanup_space = allow_empty_message ?\n \t\t\t_(\"Please enter the commit message for your changes.\"\n \t\t\t  \" Lines starting\\n\"\n-\t\t\t  \"with '%c' will be kept; you may remove them\"\n+\t\t\t  \"with '%s' will be kept; you may remove them\"\n \t\t\t  \" yourself if you want to.\\n\") :\n \t\t\t_(\"Please enter the commit message for your changes.\"\n \t\t\t  \" Lines starting\\n\"\n-\t\t\t  \"with '%c' will be kept; you may remove them\"\n+\t\t\t  \"with '%s' will be kept; you may remove them\"\n \t\t\t  \" yourself if you want to.\\n\"\n \t\t\t  \"An empty message aborts the commit.\\n\");\n \t\tif (whence != FROM_COMMIT) {\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 935c8a57dd..81b1cf5b90 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -821,7 +821,7 @@ static const char scissors_editor_comment[] =\n N_(\"An empty message aborts the commit.\\n\");\n \n static const char no_scissors_editor_comment[] =\n-N_(\"Lines starting with '%c' will be ignored, and an empty message aborts\\n\"\n+N_(\"Lines starting with '%s' will be ignored, and an empty message aborts\\n\"\n    \"the commit.\\n\");\n \n static void write_merge_heads(struct commit_list *);\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 19a7e06bf4..8b17705cf6 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -158,11 +158,11 @@ static int do_sign(struct strbuf *buffer)\n \n static const char tag_template[] =\n \tN_(\"\\nWrite a message for tag:\\n  %s\\n\"\n-\t\"Lines starting with '%c' will be ignored.\\n\");\n+\t\"Lines starting with '%s' will be ignored.\\n\");\n \n static const char tag_template_nocleanup[] =\n \tN_(\"\\nWrite a message for tag:\\n  %s\\n\"\n-\t\"Lines starting with '%c' will be kept; you may remove them\"\n+\t\"Lines starting with '%s' will be kept; you may remove them\"\n \t\" yourself if you want to.\\n\");\n \n static int git_tag_config(const char *var, const char *value,\ndiff --git a/commit.c b/commit.c\nindex ef679a0b93..ff9d49a141 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1796,7 +1796,8 @@ size_t ignored_log_message_bytes(const char *buf, size_t len)\n \t\telse\n \t\t\tnext_line++;\n \n-\t\tif (buf[bol] == comment_line_char || buf[bol] == '\\n') {\n+\t\tif (starts_with_mem(buf + bol, cutoff - bol, comment_line_char) ||\n+\t\t    buf[bol] == '\\n') {\n \t\t\t/* is this the first of the run of comments? */\n \t\t\tif (!boc)\n \t\t\t\tboc = bol;\ndiff --git a/config.c b/config.c\nindex 3cfeb3d8bd..9280dc9844 100644\n--- a/config.c\n+++ b/config.c\n@@ -1565,11 +1565,10 @@ static int git_default_core_config(const char *var, const char *value,\n \t\t\treturn config_error_nonbool(var);\n \t\telse if (!strcasecmp(value, \"auto\"))\n \t\t\tauto_comment_line_char = 1;\n-\t\telse if (value[0] && !value[1]) {\n-\t\t\tcomment_line_char = value[0];\n+\t\telse {\n+\t\t\tcomment_line_char = xstrdup(value);\n \t\t\tauto_comment_line_char = 0;\n-\t\t} else\n-\t\t\treturn error(_(\"core.commentChar should only be one ASCII character\"));\n+\t\t}\n \t\treturn 0;\n \t}\n \ndiff --git a/environment.c b/environment.c\nindex 90632a39bc..4435866d4e 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -110,7 +110,7 @@ int protect_ntfs = PROTECT_NTFS_DEFAULT;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-char comment_line_char = '#';\n+char *comment_line_char = \"#\";\n int auto_comment_line_char;\n \n /* Parallel index stat data preload? */\ndiff --git a/environment.h b/environment.h\nindex e5351c9dd9..821c6079af 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -8,7 +8,7 @@ struct strvec;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-extern char comment_line_char;\n+extern char *comment_line_char;\n extern int auto_comment_line_char;\n \n /*\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 66e47449a0..daf57917fa 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -321,7 +321,7 @@ static void credit_people(struct strbuf *out,\n \t     skip_prefix(me, them->items->string, &me) &&\n \t     starts_with(me, \" <\")))\n \t\treturn;\n-\tstrbuf_addf(out, \"\\n%c %s \", comment_line_char, label);\n+\tstrbuf_addf(out, \"\\n%s %s \", comment_line_char, label);\n \tadd_people_count(out, them);\n }\n \ndiff --git a/sequencer.c b/sequencer.c\nindex f49a871ac0..2370abc379 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -663,7 +663,7 @@ void append_conflicts_hint(struct index_state *istate,\n \tif (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {\n \t\tstrbuf_addch(msgbuf, '\\n');\n \t\twt_status_append_cut_line(msgbuf);\n-\t\tstrbuf_addch(msgbuf, comment_line_char);\n+\t\tstrbuf_addstr(msgbuf, comment_line_char);\n \t}\n \n \tstrbuf_addch(msgbuf, '\\n');\n@@ -1779,14 +1779,16 @@ static const char *command_to_string(const enum todo_command command)\n {\n \tif (command < TODO_COMMENT)\n \t\treturn todo_command_info[command].str;\n+\tif (command == TODO_COMMENT)\n+\t\treturn comment_line_char;\n \tdie(_(\"unknown command: %d\"), command);\n }\n \n static char command_to_char(const enum todo_command command)\n {\n \tif (command < TODO_COMMENT)\n \t\treturn todo_command_info[command].c;\n-\treturn comment_line_char;\n+\treturn 0;\n }\n \n static int is_noop(const enum todo_command command)\n@@ -1840,7 +1842,7 @@ static int is_fixup_flag(enum todo_command command, unsigned flag)\n static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)\n {\n \tconst char *s = str;\n-\twhile (len > 0 && s[0] == comment_line_char) {\n+\twhile (len > 0 && starts_with_mem(s, len, comment_line_char)) {\n \t\tsize_t count;\n \t\tconst char *n = memchr(s, '\\n', len);\n \t\tif (!n)\n@@ -1946,7 +1948,7 @@ static int append_squash_message(struct strbuf *buf, const char *body,\n \t     (starts_with(body, \"squash!\") || starts_with(body, \"fixup!\"))))\n \t\tcommented_len = commit_subject_length(body);\n \n-\tstrbuf_addf(buf, \"\\n%c \", comment_line_char);\n+\tstrbuf_addf(buf, \"\\n%s \", comment_line_char);\n \tstrbuf_addf(buf, _(nth_commit_msg_fmt),\n \t\t    ++opts->current_fixup_count + 1);\n \tstrbuf_addstr(buf, \"\\n\\n\");\n@@ -2003,10 +2005,10 @@ static int update_squash_messages(struct repository *r,\n \t\t\treturn error(_(\"could not read '%s'\"),\n \t\t\t\trebase_path_squash_msg());\n \n-\t\teol = buf.buf[0] != comment_line_char ?\n+\t\teol = !starts_with(buf.buf, comment_line_char) ?\n \t\t\tbuf.buf : strchrnul(buf.buf, '\\n');\n \n-\t\tstrbuf_addf(&header, \"%c \", comment_line_char);\n+\t\tstrbuf_addf(&header, \"%s \", comment_line_char);\n \t\tstrbuf_addf(&header, _(combined_commit_msg_fmt),\n \t\t\t    opts->current_fixup_count + 2);\n \t\tstrbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);\n@@ -2032,9 +2034,9 @@ static int update_squash_messages(struct repository *r,\n \t\t\trepo_unuse_commit_buffer(r, head_commit, head_message);\n \t\t\treturn error(_(\"cannot write '%s'\"), rebase_path_fixup_msg());\n \t\t}\n-\t\tstrbuf_addf(&buf, \"%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"%s \", comment_line_char);\n \t\tstrbuf_addf(&buf, _(combined_commit_msg_fmt), 2);\n-\t\tstrbuf_addf(&buf, \"\\n%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"\\n%s \", comment_line_char);\n \t\tstrbuf_addstr(&buf, is_fixup_flag(command, flag) ?\n \t\t\t      _(skip_first_commit_msg_str) :\n \t\t\t      _(first_commit_msg_str));\n@@ -2056,7 +2058,7 @@ static int update_squash_messages(struct repository *r,\n \tif (command == TODO_SQUASH || is_fixup_flag(command, flag)) {\n \t\tres = append_squash_message(&buf, body, command, opts, flag);\n \t} else if (command == TODO_FIXUP) {\n-\t\tstrbuf_addf(&buf, \"\\n%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"\\n%s \", comment_line_char);\n \t\tstrbuf_addf(&buf, _(skip_nth_commit_msg_fmt),\n \t\t\t    ++opts->current_fixup_count + 1);\n \t\tstrbuf_addstr(&buf, \"\\n\\n\");\n@@ -2562,7 +2564,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,\n \t/* left-trim */\n \tbol += strspn(bol, \" \\t\");\n \n-\tif (bol == eol || *bol == '\\r' || *bol == comment_line_char) {\n+\tif (bol == eol || *bol == '\\r' || starts_with_mem(bol, eol - bol, comment_line_char)) {\n \t\titem->command = TODO_COMMENT;\n \t\titem->commit = NULL;\n \t\titem->arg_offset = bol - buf;\n@@ -5659,7 +5661,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\t\t\t    oid_to_hex(&commit->object.oid),\n \t\t\t\t    oneline.buf);\n \t\t\tif (is_empty)\n-\t\t\t\tstrbuf_addf(&buf, \" %c empty\",\n+\t\t\t\tstrbuf_addf(&buf, \" %s empty\",\n \t\t\t\t\t    comment_line_char);\n \n \t\t\tFLEX_ALLOC_STR(entry, string, buf.buf);\n@@ -5750,7 +5752,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\tentry = oidmap_get(&state.commit2label, &commit->object.oid);\n \n \t\tif (entry)\n-\t\t\tstrbuf_addf(out, \"\\n%c Branch %s\\n\", comment_line_char, entry->string);\n+\t\t\tstrbuf_addf(out, \"\\n%s Branch %s\\n\", comment_line_char, entry->string);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \n@@ -5887,7 +5889,7 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,\n \t\t\t    oid_to_hex(&commit->object.oid));\n \t\tpretty_print_commit(&pp, commit, out);\n \t\tif (is_empty)\n-\t\t\tstrbuf_addf(out, \" %c empty\", comment_line_char);\n+\t\t\tstrbuf_addf(out, \" %s empty\", comment_line_char);\n \t\tstrbuf_addch(out, '\\n');\n \t}\n \tif (skipped_commit)\ndiff --git a/strbuf.c b/strbuf.c\nindex 7827178d8e..5d2a32d8f0 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -24,6 +24,17 @@ int istarts_with(const char *str, const char *prefix)\n \t\t\treturn 0;\n }\n \n+int starts_with_mem(const char *str, size_t len, const char *prefix)\n+{\n+\tconst char *end = str + len;\n+\tfor (; ; str++, prefix++) {\n+\t\tif (!*prefix)\n+\t\t\treturn 1;\n+\t\telse if (str == end || *str != *prefix)\n+\t\t\treturn 0;\n+\t}\n+}\n+\n int skip_to_optional_arg_default(const char *str, const char *prefix,\n \t\t\t\t const char **arg, const char *def)\n {\n@@ -340,18 +351,17 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...)\n }\n \n static void add_lines(struct strbuf *out,\n-\t\t\tconst char *prefix1,\n-\t\t\tconst char *prefix2,\n-\t\t\tconst char *buf, size_t size)\n+\t\t\tconst char *prefix,\n+\t\t\tconst char *buf, size_t size,\n+\t\t\tint space_after_prefix)\n {\n \twhile (size) {\n-\t\tconst char *prefix;\n \t\tconst char *next = memchr(buf, '\\n', size);\n \t\tnext = next ? (next + 1) : (buf + size);\n \n-\t\tprefix = ((prefix2 && (buf[0] == '\\n' || buf[0] == '\\t'))\n-\t\t\t  ? prefix2 : prefix1);\n \t\tstrbuf_addstr(out, prefix);\n+\t\tif (space_after_prefix && buf[0] != '\\n' && buf[0] != '\\t')\n+\t\t\tstrbuf_addch(out, ' ');\n \t\tstrbuf_add(out, buf, next - buf);\n \t\tsize -= next - buf;\n \t\tbuf = next;\n@@ -360,19 +370,12 @@ static void add_lines(struct strbuf *out,\n }\n \n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n-\t\t\t\tsize_t size, char comment_line_char)\n+\t\t\t\tsize_t size, const char *comment_line_char)\n {\n-\tstatic char prefix1[3];\n-\tstatic char prefix2[2];\n-\n-\tif (prefix1[0] != comment_line_char) {\n-\t\txsnprintf(prefix1, sizeof(prefix1), \"%c \", comment_line_char);\n-\t\txsnprintf(prefix2, sizeof(prefix2), \"%c\", comment_line_char);\n-\t}\n-\tadd_lines(out, prefix1, prefix2, buf, size);\n+\tadd_lines(out, comment_line_char, buf, size, 1);\n }\n \n-void strbuf_commented_addf(struct strbuf *sb, char comment_line_char,\n+void strbuf_commented_addf(struct strbuf *sb, const char *comment_line_char,\n \t\t\t   const char *fmt, ...)\n {\n \tva_list params;\n@@ -750,7 +753,7 @@ ssize_t strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)\n void strbuf_add_lines(struct strbuf *out, const char *prefix,\n \t\t      const char *buf, size_t size)\n {\n-\tadd_lines(out, prefix, NULL, buf, size);\n+\tadd_lines(out, prefix, buf, size, 0);\n }\n \n void strbuf_addstr_xml_quoted(struct strbuf *buf, const char *s)\n@@ -1005,10 +1008,10 @@ static size_t cleanup(char *line, size_t len)\n  *\n  * If last line does not have a newline at the end, one is added.\n  *\n- * Pass a non-NUL comment_line_char to skip every line starting\n+ * Pass a non-NULL comment_line_char to skip every line starting\n  * with it.\n  */\n-void strbuf_stripspace(struct strbuf *sb, char comment_line_char)\n+void strbuf_stripspace(struct strbuf *sb, const char *comment_line_char)\n {\n \tsize_t empties = 0;\n \tsize_t i, j, len, newlen;\n@@ -1022,7 +1025,7 @@ void strbuf_stripspace(struct strbuf *sb, char comment_line_char)\n \t\tlen = eol ? eol - (sb->buf + i) + 1 : sb->len - i;\n \n \t\tif (comment_line_char && len &&\n-\t\t    sb->buf[i] == comment_line_char) {\n+\t\t    starts_with(sb->buf + i, comment_line_char)) {\n \t\t\tnewlen = 0;\n \t\t\tcontinue;\n \t\t}\ndiff --git a/strbuf.h b/strbuf.h\nindex e959caca87..b310a55095 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -288,7 +288,7 @@ void strbuf_splice(struct strbuf *sb, size_t pos, size_t len,\n  */\n void strbuf_add_commented_lines(struct strbuf *out,\n \t\t\t\tconst char *buf, size_t size,\n-\t\t\t\tchar comment_line_char);\n+\t\t\t\tconst char *comment_line_char);\n \n \n /**\n@@ -379,7 +379,7 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...);\n  * blank to the buffer.\n  */\n __attribute__((format (printf, 3, 4)))\n-void strbuf_commented_addf(struct strbuf *sb, char comment_line_char, const char *fmt, ...);\n+void strbuf_commented_addf(struct strbuf *sb, const char *comment_line_char, const char *fmt, ...);\n \n __attribute__((format (printf,2,0)))\n void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap);\n@@ -517,7 +517,7 @@ int strbuf_normalize_path(struct strbuf *sb);\n  * then lines beginning with that character are considered comments,\n  * thus removed.\n  */\n-void strbuf_stripspace(struct strbuf *buf, char comment_line_char);\n+void strbuf_stripspace(struct strbuf *buf, const char *comment_line_char);\n \n static inline int strbuf_strip_suffix(struct strbuf *sb, const char *suffix)\n {\n@@ -673,6 +673,7 @@ char *xstrfmt(const char *fmt, ...);\n \n int starts_with(const char *str, const char *prefix);\n int istarts_with(const char *str, const char *prefix);\n+int starts_with_mem(const char *str, size_t len, const char *prefix);\n \n /*\n  * If the string \"str\" is the same as the string in \"prefix\", then the \"arg\"\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex a3c18a4fc2..10ed8b32bc 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -1403,7 +1403,9 @@ test_expect_success \"status (core.commentchar with submodule summary)\" '\n \n test_expect_success \"status (core.commentchar with two chars with submodule summary)\" '\n \ttest_config core.commentchar \";;\" &&\n-\ttest_must_fail git -c status.displayCommentPrefix=true status\n+\tsed \"s/^/;/\" <expect >expect.double &&\n+\tgit -c status.displayCommentPrefix=true status >output &&\n+\ttest_cmp expect.double output\n '\n \n test_expect_success \"--ignore-submodules=all suppresses submodule summary\" '\ndiff --git a/trailer.c b/trailer.c\nindex ef9df4af55..b3edcfe695 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n \n \t/* The first paragraph is the title and cannot be trailers */\n \tfor (s = buf; s < buf + len; s = next_line(s)) {\n-\t\tif (s[0] == comment_line_char)\n+\t\tif (starts_with_mem(s, buf + len - s, comment_line_char))\n \t\t\tcontinue;\n \t\tif (is_blank_line(s))\n \t\t\tbreak;\n@@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n \t\tconst char **p;\n \t\tssize_t separator_pos;\n \n-\t\tif (bol[0] == comment_line_char) {\n+\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_char)) {\n \t\t\tnon_trailer_lines += possible_continuation_lines;\n \t\t\tpossible_continuation_lines = 0;\n \t\t\tcontinue;\n@@ -1013,7 +1013,7 @@ static void parse_trailers(struct trailer_info *info,\n \tfor (i = 0; i < info->trailer_nr; i++) {\n \t\tint separator_pos;\n \t\tchar *trailer = info->trailers[i];\n-\t\tif (trailer[0] == comment_line_char)\n+\t\tif (starts_with(trailer, comment_line_char))\n \t\t\tcontinue;\n \t\tseparator_pos = find_separator(trailer, separators);\n \t\tif (separator_pos >= 1) {\ndiff --git a/wt-status.c b/wt-status.c\nindex b5a29083df..dfe120a559 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -70,7 +70,7 @@ static void status_vprintf(struct wt_status *s, int at_bol, const char *color,\n \tstrbuf_vaddf(&sb, fmt, ap);\n \tif (!sb.len) {\n \t\tif (s->display_comment_prefix) {\n-\t\t\tstrbuf_addch(&sb, comment_line_char);\n+\t\t\tstrbuf_addstr(&sb, comment_line_char);\n \t\t\tif (!trail)\n \t\t\t\tstrbuf_addch(&sb, ' ');\n \t\t}\n@@ -85,7 +85,7 @@ static void status_vprintf(struct wt_status *s, int at_bol, const char *color,\n \n \t\tstrbuf_reset(&linebuf);\n \t\tif (at_bol && s->display_comment_prefix) {\n-\t\t\tstrbuf_addch(&linebuf, comment_line_char);\n+\t\t\tstrbuf_addstr(&linebuf, comment_line_char);\n \t\t\tif (*line != '\\n' && *line != '\\t')\n \t\t\t\tstrbuf_addch(&linebuf, ' ');\n \t\t}\n@@ -1090,7 +1090,7 @@ size_t wt_status_locate_end(const char *s, size_t len)\n \tconst char *p;\n \tstruct strbuf pattern = STRBUF_INIT;\n \n-\tstrbuf_addf(&pattern, \"\\n%c %s\", comment_line_char, cut_line);\n+\tstrbuf_addf(&pattern, \"\\n%s %s\", comment_line_char, cut_line);\n \tif (starts_with(s, pattern.buf + 1))\n \t\tlen = 0;\n \telse if ((p = strstr(s, pattern.buf)))\n@@ -1176,8 +1176,6 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \tstruct strbuf sb = STRBUF_INIT;\n \tconst char *cp, *ep, *branch_name;\n \tstruct branch *branch;\n-\tchar comment_line_string[3];\n-\tint i;\n \tuint64_t t_begin = 0;\n \n \tassert(s->branch && !s->is_initial);\n@@ -1202,19 +1200,14 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \t\t}\n \t}\n \n-\ti = 0;\n-\tif (s->display_comment_prefix) {\n-\t\tcomment_line_string[i++] = comment_line_char;\n-\t\tcomment_line_string[i++] = ' ';\n-\t}\n-\tcomment_line_string[i] = '\\0';\n-\n \tfor (cp = sb.buf; (ep = strchr(cp, '\\n')) != NULL; cp = ep + 1)\n \t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s),\n-\t\t\t\t \"%s%.*s\", comment_line_string,\n+\t\t\t\t \"%s%s%.*s\",\n+\t\t\t\t s->display_comment_prefix ? comment_line_char : \"\",\n+\t\t\t\t s->display_comment_prefix ? \" \" : \"\",\n \t\t\t\t (int)(ep - cp), cp);\n \tif (s->display_comment_prefix)\n-\t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%c\",\n+\t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%s\",\n \t\t\t\t comment_line_char);\n \telse\n \t\tfputs(\"\\n\", s->fp);\n@@ -1382,7 +1375,7 @@ static int read_rebase_todolist(const char *fname, struct string_list *lines)\n \t\t\t  git_path(\"%s\", fname));\n \t}\n \twhile (!strbuf_getline_lf(&line, f)) {\n-\t\tif (line.len && line.buf[0] == comment_line_char)\n+\t\tif (starts_with(line.buf, comment_line_char))\n \t\t\tcontinue;\n \t\tstrbuf_trim(&line);\n \t\tif (!line.len)\n"},{"id":"490149","messageId":"20240307091407.GA2072522@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240306080804.GB4099518@coredump.intra.peff.net","subject":"[PATCH 0/15] allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:14:07Z","receivedAt":"2024-03-07T09:14:09Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Mar 06, 2024 at 03:08:04AM -0500, Jeff King wrote:\n\n> For a more readable series, I'd guess it would make sense to introduce\n> comment_line_str as a separate variable (but continue to enforce the\n> single-char rule), convert the easy cases en masse, the tricky cases one\n> by one, and then finally drop comment_line_char entirely. At which point\n> the config rules can be lifted to allow multi-byte strings.\n\nI ended up cleaning this up. Like I said, this isn't something I'm\npersonally that interested in. But it just seemed like a wart that this\none spot could not handle multi-byte characters that all the cool kids\nare using in their prompts etc these days.\n\nPlus it was kind of an interesting puzzle for how to lay out the\nrefactoring to make each step self-consistent. At the very least, I\nthink the first couple of cleanups are worth it even if we do not see\nthe whole thing through. ;)\n\nIt obviously nullifies kh/doc-commentchar-is-a-byte, which is in 'next'.\nSadly \"git merge\" does not find a conflict with the documentation update\nin patch 15, so we'll have to remember to pick up one topic or the\nother.\n\nI'm using U+00BB as my commentChar for now to see if any bugs show up,\nbut I expect I'll get sick of it after a few days.\n\n  [01/15]: strbuf: simplify comment-handling in add_lines() helper\n  [02/15]: strbuf: avoid static variables in strbuf_add_commented_lines()\n  [03/15]: commit: refactor base-case of adjust_comment_line_char()\n  [04/15]: strbuf: avoid shadowing global comment_line_char name\n\n    These four are cleanups that could be taken independently.\n\n  [05/15]: environment: store comment_line_char as a string\n\n    This one preps us for incrementally moving code over to the new\n    system.\n\n  [06/15]: strbuf: accept a comment string for strbuf_stripspace()\n  [07/15]: strbuf: accept a comment string for strbuf_commented_addf()\n  [08/15]: strbuf: accept a comment string for strbuf_add_commented_lines()\n  [09/15]: prefer comment_line_str to comment_line_char for printing\n  [10/15]: find multi-byte comment chars in NUL-terminated strings\n  [11/15]: find multi-byte comment chars in unterminated buffers\n  [12/15]: sequencer: handle multi-byte comment characters when writing todo list\n  [13/15]: wt-status: drop custom comment-char stringification\n\n    These ones are the actual transition.\n\n  [14/15]: environment: drop comment_line_char compatibility macro\n  [15/15]: config: allow multi-byte core.commentChar\n\n    And then we tie it off by dropping the now-unused bits and loosening\n    the config logic.\n\n Documentation/config/core.txt |  4 ++-\n add-patch.c                   | 14 +++++-----\n builtin/branch.c              |  8 +++---\n builtin/commit.c              | 19 +++++++-------\n builtin/merge.c               | 12 ++++-----\n builtin/notes.c               | 10 ++++----\n builtin/rebase.c              |  2 +-\n builtin/stripspace.c          |  4 +--\n builtin/tag.c                 | 14 +++++-----\n commit.c                      |  3 ++-\n config.c                      |  6 ++---\n environment.c                 |  2 +-\n environment.h                 |  2 +-\n fmt-merge-msg.c               |  8 +++---\n rebase-interactive.c          | 10 ++++----\n sequencer.c                   | 48 ++++++++++++++++++-----------------\n strbuf.c                      | 47 ++++++++++++++++++----------------\n strbuf.h                      |  9 ++++---\n t/t0030-stripspace.sh         |  5 ++++\n t/t7507-commit-verbose.sh     | 10 ++++++++\n t/t7508-status.sh             |  4 ++-\n trailer.c                     |  6 ++---\n wt-status.c                   | 31 +++++++++-------------\n 23 files changed, 149 insertions(+), 129 deletions(-)\n"},{"id":"490150","messageId":"20240307091547.GA2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 01/15] strbuf: simplify comment-handling in add_lines() helper","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:15:47Z","receivedAt":"2024-03-07T09:15:48Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"In strbuf_add_commented_lines(), we prepare two strings with potential\nprefixes: one with just the comment char, and one with an additional\nspace. In the add_lines() helper, we use the one without the extra space\nfor blank lines or lines starting with a tab.\n\nWhile passing in two separate prefixes to the helper is very flexible,\nit's more flexibility than we actually use (or are likely to use, since\nthe rules inside add_lines() only make sense if \"prefix2\" is a variant\nof \"prefix1\" without the extra space). And setting up the two strings\nmakes refactoring in strbuf_add_commented_lines() awkward.\n\nInstead, let's pass in a single string, and just let add_lines() add the\nextra space to the result as appropriate.\n\nWe do still need to pass in a flag to trigger this behavior. The helper\nis shared by strbuf_add_lines(), which passes in a NULL \"prefix2\" to\ninhibit this extra handling.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n strbuf.c | 24 ++++++++++--------------\n 1 file changed, 10 insertions(+), 14 deletions(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex 7827178d8e..689d8acd5e 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -340,18 +340,17 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...)\n }\n \n static void add_lines(struct strbuf *out,\n-\t\t\tconst char *prefix1,\n-\t\t\tconst char *prefix2,\n-\t\t\tconst char *buf, size_t size)\n+\t\t\tconst char *prefix,\n+\t\t\tconst char *buf, size_t size,\n+\t\t\tint space_after_prefix)\n {\n \twhile (size) {\n-\t\tconst char *prefix;\n \t\tconst char *next = memchr(buf, '\\n', size);\n \t\tnext = next ? (next + 1) : (buf + size);\n \n-\t\tprefix = ((prefix2 && (buf[0] == '\\n' || buf[0] == '\\t'))\n-\t\t\t  ? prefix2 : prefix1);\n \t\tstrbuf_addstr(out, prefix);\n+\t\tif (space_after_prefix && buf[0] != '\\n' && buf[0] != '\\t')\n+\t\t\tstrbuf_addch(out, ' ');\n \t\tstrbuf_add(out, buf, next - buf);\n \t\tsize -= next - buf;\n \t\tbuf = next;\n@@ -362,14 +361,11 @@ static void add_lines(struct strbuf *out,\n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n \t\t\t\tsize_t size, char comment_line_char)\n {\n-\tstatic char prefix1[3];\n-\tstatic char prefix2[2];\n+\tstatic char prefix[2];\n \n-\tif (prefix1[0] != comment_line_char) {\n-\t\txsnprintf(prefix1, sizeof(prefix1), \"%c \", comment_line_char);\n-\t\txsnprintf(prefix2, sizeof(prefix2), \"%c\", comment_line_char);\n-\t}\n-\tadd_lines(out, prefix1, prefix2, buf, size);\n+\tif (prefix[0] != comment_line_char)\n+\t\txsnprintf(prefix, sizeof(prefix), \"%c\", comment_line_char);\n+\tadd_lines(out, prefix, buf, size, 1);\n }\n \n void strbuf_commented_addf(struct strbuf *sb, char comment_line_char,\n@@ -750,7 +746,7 @@ ssize_t strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)\n void strbuf_add_lines(struct strbuf *out, const char *prefix,\n \t\t      const char *buf, size_t size)\n {\n-\tadd_lines(out, prefix, NULL, buf, size);\n+\tadd_lines(out, prefix, buf, size, 0);\n }\n \n void strbuf_addstr_xml_quoted(struct strbuf *buf, const char *s)\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490151","messageId":"20240307091652.GB2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 02/15] strbuf: avoid static variables in strbuf_add_commented_lines()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:16:52Z","receivedAt":"2024-03-07T09:16:54Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"In strbuf_add_commented_lines(), we have to convert the single-byte\ncomment_line_char into a string to pass to add_lines(). We cache the\ncreated string using a static-local variable. But this makes the\nfunction non-reentrant, and it's doubtful that this provides any real\nperformance benefit given that we know the string always contains a\nsingle character.\n\nSo let's just create it from scratch each time, and to give the compiler\nthe maximal opportunity to make it fast we'll ditch the over-complicated\nxsnprintf() and just assign directly into the array.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nIn the long run we'll end up just passing in the comment-string that the\ncaller gives us, so this patch could arguably be dropped until that\npoint.\n\n strbuf.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex 689d8acd5e..ca80a2c77e 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -361,10 +361,10 @@ static void add_lines(struct strbuf *out,\n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n \t\t\t\tsize_t size, char comment_line_char)\n {\n-\tstatic char prefix[2];\n+\tchar prefix[2];\n \n-\tif (prefix[0] != comment_line_char)\n-\t\txsnprintf(prefix, sizeof(prefix), \"%c\", comment_line_char);\n+\tprefix[0] = comment_line_char;\n+\tprefix[1] = '\\0';\n \tadd_lines(out, prefix, buf, size, 1);\n }\n \n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490152","messageId":"20240307091829.GC2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 03/15] commit: refactor base-case of adjust_comment_line_char()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:18:29Z","receivedAt":"2024-03-07T09:18:30Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"When core.commentChar is set to \"auto\", we check a set of candidate\ncharacters against the proposed buffer to see which if any can be used\nwithout ambiguity. But before we do that, we optimize for the common\ncase that the default \"#\" is fine by just seeing if it is present in the\nbuffer at all.\n\nThe way we do this is a bit subtle, though: we assign the candidate\ncharacter to comment_line_char preemptively, then check if it works, and\nreturn if it does. The subtle part is that sometimes setting\ncomment_line_char is important (after we return, the important outcome\nis the fact that we have set the variable) and sometimes it is useless\n(if our optimization fails, we go on to do the more careful checks and\neventually assign something else instead).\n\nTo make it more clear what is happening (and to make further refactoring\nof comment_line_char easier), let's check our candidate character\ndirectly, and then assign as part of returning if it worked out.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/commit.c | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 6d1fa71676..d496980421 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -684,9 +684,10 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n \tchar *candidate;\n \tconst char *p;\n \n-\tcomment_line_char = candidates[0];\n-\tif (!memchr(sb->buf, comment_line_char, sb->len))\n+\tif (!memchr(sb->buf, candidates[0], sb->len)) {\n+\t\tcomment_line_char = candidates[0];\n \t\treturn;\n+\t}\n \n \tp = sb->buf;\n \tcandidate = strchr(candidates, *p);\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490153","messageId":"20240307091922.GD2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 04/15] strbuf: avoid shadowing global comment_line_char name","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:19:22Z","receivedAt":"2024-03-07T09:19:23Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Several comment-related strbuf functions take a comment_line_char\nparameter. There's also a global comment_line_char variable, which is\nclosely related (most callers pass it in as this parameter). Let's avoid\nshadowing the global name. This makes it more obvious that we're not\nusing the global value, and it will be especially helpful as we refactor\nthe global in future patches (in particular, any macro trickery wouldn't\nwork because the preprocessor doesn't respect scope).\n\nWe'll use \"comment_prefix\". That should be descriptive enough, and as a\nbonus is more neutral with respect to the \"char\" type (since we'll\neventually swap it out for a string).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n strbuf.c | 16 ++++++++--------\n strbuf.h |  8 ++++----\n 2 files changed, 12 insertions(+), 12 deletions(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex ca80a2c77e..a33aed6c07 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -359,16 +359,16 @@ static void add_lines(struct strbuf *out,\n }\n \n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n-\t\t\t\tsize_t size, char comment_line_char)\n+\t\t\t\tsize_t size, char comment_prefix)\n {\n \tchar prefix[2];\n \n-\tprefix[0] = comment_line_char;\n+\tprefix[0] = comment_prefix;\n \tprefix[1] = '\\0';\n \tadd_lines(out, prefix, buf, size, 1);\n }\n \n-void strbuf_commented_addf(struct strbuf *sb, char comment_line_char,\n+void strbuf_commented_addf(struct strbuf *sb, char comment_prefix,\n \t\t\t   const char *fmt, ...)\n {\n \tva_list params;\n@@ -379,7 +379,7 @@ void strbuf_commented_addf(struct strbuf *sb, char comment_line_char,\n \tstrbuf_vaddf(&buf, fmt, params);\n \tva_end(params);\n \n-\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_line_char);\n+\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix);\n \tif (incomplete_line)\n \t\tsb->buf[--sb->len] = '\\0';\n \n@@ -1001,10 +1001,10 @@ static size_t cleanup(char *line, size_t len)\n  *\n  * If last line does not have a newline at the end, one is added.\n  *\n- * Pass a non-NUL comment_line_char to skip every line starting\n+ * Pass a non-NUL comment_prefix to skip every line starting\n  * with it.\n  */\n-void strbuf_stripspace(struct strbuf *sb, char comment_line_char)\n+void strbuf_stripspace(struct strbuf *sb, char comment_prefix)\n {\n \tsize_t empties = 0;\n \tsize_t i, j, len, newlen;\n@@ -1017,8 +1017,8 @@ void strbuf_stripspace(struct strbuf *sb, char comment_line_char)\n \t\teol = memchr(sb->buf + i, '\\n', sb->len - i);\n \t\tlen = eol ? eol - (sb->buf + i) + 1 : sb->len - i;\n \n-\t\tif (comment_line_char && len &&\n-\t\t    sb->buf[i] == comment_line_char) {\n+\t\tif (comment_prefix && len &&\n+\t\t    sb->buf[i] == comment_prefix) {\n \t\t\tnewlen = 0;\n \t\t\tcontinue;\n \t\t}\ndiff --git a/strbuf.h b/strbuf.h\nindex e959caca87..860fcec5fb 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -288,7 +288,7 @@ void strbuf_splice(struct strbuf *sb, size_t pos, size_t len,\n  */\n void strbuf_add_commented_lines(struct strbuf *out,\n \t\t\t\tconst char *buf, size_t size,\n-\t\t\t\tchar comment_line_char);\n+\t\t\t\tchar comment_prefix);\n \n \n /**\n@@ -379,7 +379,7 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...);\n  * blank to the buffer.\n  */\n __attribute__((format (printf, 3, 4)))\n-void strbuf_commented_addf(struct strbuf *sb, char comment_line_char, const char *fmt, ...);\n+void strbuf_commented_addf(struct strbuf *sb, char comment_prefix, const char *fmt, ...);\n \n __attribute__((format (printf,2,0)))\n void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap);\n@@ -513,11 +513,11 @@ int strbuf_getcwd(struct strbuf *sb);\n int strbuf_normalize_path(struct strbuf *sb);\n \n /**\n- * Strip whitespace from a buffer. If comment_line_char is non-NUL,\n+ * Strip whitespace from a buffer. If comment_prefix is non-NUL,\n  * then lines beginning with that character are considered comments,\n  * thus removed.\n  */\n-void strbuf_stripspace(struct strbuf *buf, char comment_line_char);\n+void strbuf_stripspace(struct strbuf *buf, char comment_prefix);\n \n static inline int strbuf_strip_suffix(struct strbuf *sb, const char *suffix)\n {\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490154","messageId":"20240307092024.GE2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 05/15] environment: store comment_line_char as a string","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:20:24Z","receivedAt":"2024-03-07T09:20:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"We'd like to eventually support multi-byte comment prefixes, but the\ncomment_line_char variable is referenced in many spots, making the\ntransition difficult.\n\nLet's start by storing the character in a NUL-terminated string. That\nwill let us switch code over incrementally to the string format, and we\ncan easily support the existing code with a macro wrapper (since we'll\ncontinue to allow only a single-byte prefix, this will behave\nidentically).\n\nOnce all references to the \"char\" variable have been converted, we can\ndrop it and enable longer strings.\n\nWe'll still have to touch all of the spots that create or set the\nvariable in this patch, but there are only a few (reading the config,\nand the \"auto\" character selector).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/commit.c | 4 ++--\n config.c         | 2 +-\n environment.c    | 2 +-\n environment.h    | 3 ++-\n 4 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex d496980421..d8abbe48b1 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -685,7 +685,7 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n \tconst char *p;\n \n \tif (!memchr(sb->buf, candidates[0], sb->len)) {\n-\t\tcomment_line_char = candidates[0];\n+\t\tcomment_line_str = xstrfmt(\"%c\", candidates[0]);\n \t\treturn;\n \t}\n \n@@ -706,7 +706,7 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n \tif (!*p)\n \t\tdie(_(\"unable to select a comment character that is not used\\n\"\n \t\t      \"in the current commit message\"));\n-\tcomment_line_char = *p;\n+\tcomment_line_str = xstrfmt(\"%c\", *p);\n }\n \n static void prepare_amend_commit(struct commit *commit, struct strbuf *sb,\ndiff --git a/config.c b/config.c\nindex 3cfeb3d8bd..e12ea68f24 100644\n--- a/config.c\n+++ b/config.c\n@@ -1566,7 +1566,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\telse if (!strcasecmp(value, \"auto\"))\n \t\t\tauto_comment_line_char = 1;\n \t\telse if (value[0] && !value[1]) {\n-\t\t\tcomment_line_char = value[0];\n+\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n \t\t\tauto_comment_line_char = 0;\n \t\t} else\n \t\t\treturn error(_(\"core.commentChar should only be one ASCII character\"));\ndiff --git a/environment.c b/environment.c\nindex 90632a39bc..0a9f5db407 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -110,7 +110,7 @@ int protect_ntfs = PROTECT_NTFS_DEFAULT;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-char comment_line_char = '#';\n+const char *comment_line_str = \"#\";\n int auto_comment_line_char;\n \n /* Parallel index stat data preload? */\ndiff --git a/environment.h b/environment.h\nindex e5351c9dd9..3496474cce 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -8,7 +8,8 @@ struct strvec;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-extern char comment_line_char;\n+#define comment_line_char (comment_line_str[0])\n+extern const char *comment_line_str;\n extern int auto_comment_line_char;\n \n /*\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490155","messageId":"20240307092126.GF2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 06/15] strbuf: accept a comment string for strbuf_stripspace()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:21:26Z","receivedAt":"2024-03-07T09:21:27Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, let's take a\nNUL-terminated string pointer for strbuf_stripspace(), rather than a\nsingle character. We can continue to support its feature of ignoring\ncomments by accepting a NULL pointer (as opposed to the current behavior\nof a NUL byte).\n\nAll of the callers have to be adjusted, but they can all just pass\ncomment_line_str (or NULL).\n\nInside the function we detect comments by comparing the first byte of a\nline to the comment character. We'll adjust that to use starts_with(),\nwhich will match multiple bytes (though for now, of course, we still\nonly allow a single byte, so it's academic).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/branch.c     | 2 +-\n builtin/notes.c      | 2 +-\n builtin/rebase.c     | 2 +-\n builtin/stripspace.c | 2 +-\n builtin/tag.c        | 2 +-\n rebase-interactive.c | 2 +-\n sequencer.c          | 6 +++---\n strbuf.c             | 6 +++---\n strbuf.h             | 4 ++--\n 9 files changed, 14 insertions(+), 14 deletions(-)\n\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex cfb63cce5f..c03c0407d1 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -678,7 +678,7 @@ static int edit_branch_description(const char *branch_name)\n \t\tstrbuf_release(&buf);\n \t\treturn -1;\n \t}\n-\tstrbuf_stripspace(&buf, comment_line_char);\n+\tstrbuf_stripspace(&buf, comment_line_str);\n \n \tstrbuf_addf(&name, \"branch.%s.description\", branch_name);\n \tif (buf.len || exists)\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex caf20fd5bd..5223a3f350 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -223,7 +223,7 @@ static void prepare_note_data(const struct object_id *object, struct note_data *\n \t\t\tdie(_(\"please supply the note contents using either -m or -F option\"));\n \t\t}\n \t\tif (d->stripspace)\n-\t\t\tstrbuf_stripspace(&d->buf, comment_line_char);\n+\t\t\tstrbuf_stripspace(&d->buf, comment_line_str);\n \t}\n }\n \ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 6ead9465a4..bf78402129 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -204,7 +204,7 @@ static int edit_todo_file(unsigned flags)\n \tif (strbuf_read_file(&todo_list.buf, todo_file, 0) < 0)\n \t\treturn error_errno(_(\"could not read '%s'.\"), todo_file);\n \n-\tstrbuf_stripspace(&todo_list.buf, comment_line_char);\n+\tstrbuf_stripspace(&todo_list.buf, comment_line_str);\n \tres = edit_todo_list(the_repository, &todo_list, &new_todo, NULL, NULL, flags);\n \tif (!res && todo_list_write_to_file(the_repository, &new_todo, todo_file,\n \t\t\t\t\t    NULL, NULL, -1, flags & ~(TODO_LIST_SHORTEN_IDS)))\ndiff --git a/builtin/stripspace.c b/builtin/stripspace.c\nindex 7b700a9fb1..434ac490cb 100644\n--- a/builtin/stripspace.c\n+++ b/builtin/stripspace.c\n@@ -59,7 +59,7 @@ int cmd_stripspace(int argc, const char **argv, const char *prefix)\n \n \tif (mode == STRIP_DEFAULT || mode == STRIP_COMMENTS)\n \t\tstrbuf_stripspace(&buf,\n-\t\t\t  mode == STRIP_COMMENTS ? comment_line_char : '\\0');\n+\t\t\t  mode == STRIP_COMMENTS ? comment_line_str : NULL);\n \telse\n \t\tcomment_lines(&buf);\n \ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 19a7e06bf4..07327d3c04 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -310,7 +310,7 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n \n \tif (opt->cleanup_mode != CLEANUP_NONE)\n \t\tstrbuf_stripspace(buf,\n-\t\t  opt->cleanup_mode == CLEANUP_ALL ? comment_line_char : '\\0');\n+\t\t  opt->cleanup_mode == CLEANUP_ALL ? comment_line_str : NULL);\n \n \tif (!opt->message_given && !buf->len)\n \t\tdie(_(\"no tag message?\"));\ndiff --git a/rebase-interactive.c b/rebase-interactive.c\nindex d9718409b3..6dfc33e4e3 100644\n--- a/rebase-interactive.c\n+++ b/rebase-interactive.c\n@@ -130,7 +130,7 @@ int edit_todo_list(struct repository *r, struct todo_list *todo_list,\n \tif (launch_sequence_editor(todo_file, &new_todo->buf, NULL))\n \t\treturn -2;\n \n-\tstrbuf_stripspace(&new_todo->buf, comment_line_char);\n+\tstrbuf_stripspace(&new_todo->buf, comment_line_str);\n \tif (initial && new_todo->buf.len == 0)\n \t\treturn -3;\n \ndiff --git a/sequencer.c b/sequencer.c\nindex f49a871ac0..6a1b7b200e 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1152,7 +1152,7 @@ void cleanup_message(struct strbuf *msgbuf,\n \t\tstrbuf_setlen(msgbuf, wt_status_locate_end(msgbuf->buf, msgbuf->len));\n \tif (cleanup_mode != COMMIT_MSG_CLEANUP_NONE)\n \t\tstrbuf_stripspace(msgbuf,\n-\t\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_char : '\\0');\n+\t\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_str : NULL);\n }\n \n /*\n@@ -1184,7 +1184,7 @@ int template_untouched(const struct strbuf *sb, const char *template_file,\n \t\treturn 0;\n \n \tstrbuf_stripspace(&tmpl,\n-\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_char : '\\0');\n+\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_str : NULL);\n \tif (!skip_prefix(sb->buf, tmpl.buf, &start))\n \t\tstart = sb->buf;\n \tstrbuf_release(&tmpl);\n@@ -1557,7 +1557,7 @@ static int try_to_commit(struct repository *r,\n \n \tif (cleanup != COMMIT_MSG_CLEANUP_NONE)\n \t\tstrbuf_stripspace(msg,\n-\t\t  cleanup == COMMIT_MSG_CLEANUP_ALL ? comment_line_char : '\\0');\n+\t\t  cleanup == COMMIT_MSG_CLEANUP_ALL ? comment_line_str : NULL);\n \tif ((flags & EDIT_MSG) && message_is_empty(msg, cleanup)) {\n \t\tres = 1; /* run 'git commit' to display error message */\n \t\tgoto out;\ndiff --git a/strbuf.c b/strbuf.c\nindex a33aed6c07..e9b6127e76 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -1001,10 +1001,10 @@ static size_t cleanup(char *line, size_t len)\n  *\n  * If last line does not have a newline at the end, one is added.\n  *\n- * Pass a non-NUL comment_prefix to skip every line starting\n+ * Pass a non-NULL comment_prefix to skip every line starting\n  * with it.\n  */\n-void strbuf_stripspace(struct strbuf *sb, char comment_prefix)\n+void strbuf_stripspace(struct strbuf *sb, const char *comment_prefix)\n {\n \tsize_t empties = 0;\n \tsize_t i, j, len, newlen;\n@@ -1018,7 +1018,7 @@ void strbuf_stripspace(struct strbuf *sb, char comment_prefix)\n \t\tlen = eol ? eol - (sb->buf + i) + 1 : sb->len - i;\n \n \t\tif (comment_prefix && len &&\n-\t\t    sb->buf[i] == comment_prefix) {\n+\t\t    starts_with(sb->buf + i, comment_prefix)) {\n \t\t\tnewlen = 0;\n \t\t\tcontinue;\n \t\t}\ndiff --git a/strbuf.h b/strbuf.h\nindex 860fcec5fb..dc4710adbb 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -513,11 +513,11 @@ int strbuf_getcwd(struct strbuf *sb);\n int strbuf_normalize_path(struct strbuf *sb);\n \n /**\n- * Strip whitespace from a buffer. If comment_prefix is non-NUL,\n+ * Strip whitespace from a buffer. If comment_prefix is non-NULL,\n  * then lines beginning with that character are considered comments,\n  * thus removed.\n  */\n-void strbuf_stripspace(struct strbuf *buf, char comment_prefix);\n+void strbuf_stripspace(struct strbuf *buf, const char *comment_prefix);\n \n static inline int strbuf_strip_suffix(struct strbuf *sb, const char *suffix)\n {\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490156","messageId":"20240307092209.GG2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 07/15] strbuf: accept a comment string for strbuf_commented_addf()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:22:09Z","receivedAt":"2024-03-07T09:22:10Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, let's take a\nNUL-terminated string pointer for strbuf_commented_addf() rather than a\nsingle character.\n\nAll of the callers have to be adjusted, but they can just pass\ncomment_line_str rather than comment_line_char.\n\nNote that we rely on strbuf_add_commented_lines() under the hood, so\nwe'll cheat a bit to squeeze our string into a single character (for now\nthe two are equivalent, and we'll address this TODO in the next patch).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n add-patch.c          |  8 ++++----\n builtin/branch.c     |  2 +-\n builtin/merge.c      |  8 ++++----\n builtin/tag.c        |  4 ++--\n rebase-interactive.c |  2 +-\n sequencer.c          |  4 ++--\n strbuf.c             | 10 ++++++++--\n strbuf.h             |  2 +-\n wt-status.c          |  2 +-\n 9 files changed, 24 insertions(+), 18 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 68f525b35c..7390677795 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1105,11 +1105,11 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \tsize_t i;\n \n \tstrbuf_reset(&s->buf);\n-\tstrbuf_commented_addf(&s->buf, comment_line_char,\n+\tstrbuf_commented_addf(&s->buf, comment_line_str,\n \t\t\t      _(\"Manual hunk edit mode -- see bottom for \"\n \t\t\t\t\"a quick guide.\\n\"));\n \trender_hunk(s, hunk, 0, 0, &s->buf);\n-\tstrbuf_commented_addf(&s->buf, comment_line_char,\n+\tstrbuf_commented_addf(&s->buf, comment_line_str,\n \t\t\t      _(\"---\\n\"\n \t\t\t\t\"To remove '%c' lines, make them ' ' lines \"\n \t\t\t\t\"(context).\\n\"\n@@ -1118,13 +1118,13 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \t\t\t      s->mode->is_reverse ? '+' : '-',\n \t\t\t      s->mode->is_reverse ? '-' : '+',\n \t\t\t      comment_line_char);\n-\tstrbuf_commented_addf(&s->buf, comment_line_char, \"%s\",\n+\tstrbuf_commented_addf(&s->buf, comment_line_str, \"%s\",\n \t\t\t      _(s->mode->edit_hunk_hint));\n \t/*\n \t * TRANSLATORS: 'it' refers to the patch mentioned in the previous\n \t * messages.\n \t */\n-\tstrbuf_commented_addf(&s->buf, comment_line_char,\n+\tstrbuf_commented_addf(&s->buf, comment_line_str,\n \t\t\t      _(\"If it does not apply cleanly, you will be \"\n \t\t\t\t\"given an opportunity to\\n\"\n \t\t\t\t\"edit again.  If all lines of the hunk are \"\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex c03c0407d1..8904a1e5d9 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -667,7 +667,7 @@ static int edit_branch_description(const char *branch_name)\n \texists = !read_branch_desc(&buf, branch_name);\n \tif (!buf.len || buf.buf[buf.len-1] != '\\n')\n \t\tstrbuf_addch(&buf, '\\n');\n-\tstrbuf_commented_addf(&buf, comment_line_char,\n+\tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t    _(\"Please edit the description for the branch\\n\"\n \t\t      \"  %s\\n\"\n \t\t      \"Lines starting with '%c' will be stripped.\\n\"),\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 935c8a57dd..6d048fb628 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -852,15 +852,15 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tstrbuf_addch(&msg, '\\n');\n \t\tif (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {\n \t\t\twt_status_append_cut_line(&msg);\n-\t\t\tstrbuf_commented_addf(&msg, comment_line_char, \"\\n\");\n+\t\t\tstrbuf_commented_addf(&msg, comment_line_str, \"\\n\");\n \t\t}\n-\t\tstrbuf_commented_addf(&msg, comment_line_char,\n+\t\tstrbuf_commented_addf(&msg, comment_line_str,\n \t\t\t\t      _(merge_editor_comment));\n \t\tif (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS)\n-\t\t\tstrbuf_commented_addf(&msg, comment_line_char,\n+\t\t\tstrbuf_commented_addf(&msg, comment_line_str,\n \t\t\t\t\t      _(scissors_editor_comment));\n \t\telse\n-\t\t\tstrbuf_commented_addf(&msg, comment_line_char,\n+\t\t\tstrbuf_commented_addf(&msg, comment_line_str,\n \t\t\t\t_(no_scissors_editor_comment), comment_line_char);\n \t}\n \tif (signoff)\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 07327d3c04..1c708785bf 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -291,10 +291,10 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n \t\t\tstruct strbuf buf = STRBUF_INIT;\n \t\t\tstrbuf_addch(&buf, '\\n');\n \t\t\tif (opt->cleanup_mode == CLEANUP_ALL)\n-\t\t\t\tstrbuf_commented_addf(&buf, comment_line_char,\n+\t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t\t\t      _(tag_template), tag, comment_line_char);\n \t\t\telse\n-\t\t\t\tstrbuf_commented_addf(&buf, comment_line_char,\n+\t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t\t\t      _(tag_template_nocleanup), tag, comment_line_char);\n \t\t\twrite_or_die(fd, buf.buf, buf.len);\n \t\t\tstrbuf_release(&buf);\ndiff --git a/rebase-interactive.c b/rebase-interactive.c\nindex 6dfc33e4e3..affc93a8e4 100644\n--- a/rebase-interactive.c\n+++ b/rebase-interactive.c\n@@ -71,7 +71,7 @@ void append_todo_help(int command_count,\n \n \tif (!edit_todo) {\n \t\tstrbuf_addch(buf, '\\n');\n-\t\tstrbuf_commented_addf(buf, comment_line_char,\n+\t\tstrbuf_commented_addf(buf, comment_line_str,\n \t\t\t\t      Q_(\"Rebase %s onto %s (%d command)\",\n \t\t\t\t\t \"Rebase %s onto %s (%d commands)\",\n \t\t\t\t\t command_count),\ndiff --git a/sequencer.c b/sequencer.c\nindex 6a1b7b200e..852c3f9f4e 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -667,11 +667,11 @@ void append_conflicts_hint(struct index_state *istate,\n \t}\n \n \tstrbuf_addch(msgbuf, '\\n');\n-\tstrbuf_commented_addf(msgbuf, comment_line_char, \"Conflicts:\\n\");\n+\tstrbuf_commented_addf(msgbuf, comment_line_str, \"Conflicts:\\n\");\n \tfor (i = 0; i < istate->cache_nr;) {\n \t\tconst struct cache_entry *ce = istate->cache[i++];\n \t\tif (ce_stage(ce)) {\n-\t\t\tstrbuf_commented_addf(msgbuf, comment_line_char,\n+\t\t\tstrbuf_commented_addf(msgbuf, comment_line_str,\n \t\t\t\t\t      \"\\t%s\\n\", ce->name);\n \t\t\twhile (i < istate->cache_nr &&\n \t\t\t       !strcmp(ce->name, istate->cache[i]->name))\ndiff --git a/strbuf.c b/strbuf.c\nindex e9b6127e76..76d02e0920 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -368,7 +368,7 @@ void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n \tadd_lines(out, prefix, buf, size, 1);\n }\n \n-void strbuf_commented_addf(struct strbuf *sb, char comment_prefix,\n+void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix,\n \t\t\t   const char *fmt, ...)\n {\n \tva_list params;\n@@ -379,7 +379,13 @@ void strbuf_commented_addf(struct strbuf *sb, char comment_prefix,\n \tstrbuf_vaddf(&buf, fmt, params);\n \tva_end(params);\n \n-\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix);\n+\t/*\n+\t * TODO Our commented_lines helper does not yet understand\n+\t * comment strings. But since we know that the strings are\n+\t * always single-char, we can cheat for the moment, and\n+\t * fix this later.\n+\t */\n+\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix[0]);\n \tif (incomplete_line)\n \t\tsb->buf[--sb->len] = '\\0';\n \ndiff --git a/strbuf.h b/strbuf.h\nindex dc4710adbb..b128ca539a 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -379,7 +379,7 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...);\n  * blank to the buffer.\n  */\n __attribute__((format (printf, 3, 4)))\n-void strbuf_commented_addf(struct strbuf *sb, char comment_prefix, const char *fmt, ...);\n+void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix, const char *fmt, ...);\n \n __attribute__((format (printf,2,0)))\n void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap);\ndiff --git a/wt-status.c b/wt-status.c\nindex b5a29083df..2be2eb094c 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1103,7 +1103,7 @@ void wt_status_append_cut_line(struct strbuf *buf)\n {\n \tconst char *explanation = _(\"Do not modify or remove the line above.\\nEverything below it will be ignored.\");\n \n-\tstrbuf_commented_addf(buf, comment_line_char, \"%s\", cut_line);\n+\tstrbuf_commented_addf(buf, comment_line_str, \"%s\", cut_line);\n \tstrbuf_add_commented_lines(buf, explanation, strlen(explanation), comment_line_char);\n }\n \n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490157","messageId":"20240307092312.GH2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 08/15] strbuf: accept a comment string for strbuf_add_commented_lines()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:23:12Z","receivedAt":"2024-03-07T09:23:13Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, let's take a\nNUL-terminated string pointer for strbuf_add_commented_lines() rather\nthan a single character.\n\nAll of the callers have to be adjusted; most can just pass\ncomment_line_str rather than comment_line_char.\n\nAnd now our \"cheat\" in strbuf_commented_addf() can go away, as we can\ntake the full string from it.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nThis could also be squashed into the previous patch. I wasn't sure if it\nwould be more overwhelming to have so many changes intermingled, or if\nthe \"cheat\" / \"uncheat\" back-and-forth would be too confusing. Pick your\npoison.\n\n builtin/notes.c      |  8 ++++----\n builtin/stripspace.c |  2 +-\n fmt-merge-msg.c      |  6 +++---\n rebase-interactive.c |  6 +++---\n sequencer.c          |  8 ++++----\n strbuf.c             | 16 +++-------------\n strbuf.h             |  2 +-\n wt-status.c          |  4 ++--\n 8 files changed, 21 insertions(+), 31 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 5223a3f350..1a67f01d00 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -179,7 +179,7 @@ static void write_commented_object(int fd, const struct object_id *object)\n \n \tif (strbuf_read(&buf, show.out, 0) < 0)\n \t\tdie_errno(_(\"could not read 'show' output\"));\n-\tstrbuf_add_commented_lines(&cbuf, buf.buf, buf.len, comment_line_char);\n+\tstrbuf_add_commented_lines(&cbuf, buf.buf, buf.len, comment_line_str);\n \twrite_or_die(fd, cbuf.buf, cbuf.len);\n \n \tstrbuf_release(&cbuf);\n@@ -207,10 +207,10 @@ static void prepare_note_data(const struct object_id *object, struct note_data *\n \t\t\tcopy_obj_to_fd(fd, old_note);\n \n \t\tstrbuf_addch(&buf, '\\n');\n-\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_char);\n+\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_str);\n \t\tstrbuf_add_commented_lines(&buf, _(note_template), strlen(_(note_template)),\n-\t\t\t\t\t   comment_line_char);\n-\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_char);\n+\t\t\t\t\t   comment_line_str);\n+\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_str);\n \t\twrite_or_die(fd, buf.buf, buf.len);\n \n \t\twrite_commented_object(fd, object);\ndiff --git a/builtin/stripspace.c b/builtin/stripspace.c\nindex 434ac490cb..e5626e5126 100644\n--- a/builtin/stripspace.c\n+++ b/builtin/stripspace.c\n@@ -13,7 +13,7 @@ static void comment_lines(struct strbuf *buf)\n \tsize_t len;\n \n \tmsg = strbuf_detach(buf, &len);\n-\tstrbuf_add_commented_lines(buf, msg, len, comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, len, comment_line_str);\n \tfree(msg);\n }\n \ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 66e47449a0..79e8aad086 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -510,7 +510,7 @@ static void fmt_tag_signature(struct strbuf *tagbuf,\n \tif (sig->len) {\n \t\tstrbuf_addch(tagbuf, '\\n');\n \t\tstrbuf_add_commented_lines(tagbuf, sig->buf, sig->len,\n-\t\t\t\t\t   comment_line_char);\n+\t\t\t\t\t   comment_line_str);\n \t}\n }\n \n@@ -557,7 +557,7 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\t\tstrbuf_add_commented_lines(&tagline,\n \t\t\t\t\t\torigins.items[first_tag].string,\n \t\t\t\t\t\tstrlen(origins.items[first_tag].string),\n-\t\t\t\t\t\tcomment_line_char);\n+\t\t\t\t\t\tcomment_line_str);\n \t\t\t\tstrbuf_insert(&tagbuf, 0, tagline.buf,\n \t\t\t\t\t      tagline.len);\n \t\t\t\tstrbuf_release(&tagline);\n@@ -566,7 +566,7 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tstrbuf_add_commented_lines(&tagbuf,\n \t\t\t\t\torigins.items[i].string,\n \t\t\t\t\tstrlen(origins.items[i].string),\n-\t\t\t\t\tcomment_line_char);\n+\t\t\t\t\tcomment_line_str);\n \t\t\tfmt_tag_signature(&tagbuf, &sig, buf, len);\n \t\t}\n \t\tstrbuf_release(&payload);\ndiff --git a/rebase-interactive.c b/rebase-interactive.c\nindex affc93a8e4..c343e16fcd 100644\n--- a/rebase-interactive.c\n+++ b/rebase-interactive.c\n@@ -78,7 +78,7 @@ void append_todo_help(int command_count,\n \t\t\t\t      shortrevisions, shortonto, command_count);\n \t}\n \n-\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str);\n \n \tif (get_missing_commit_check_level() == MISSING_COMMIT_CHECK_ERROR)\n \t\tmsg = _(\"\\nDo not remove any line. Use 'drop' \"\n@@ -87,7 +87,7 @@ void append_todo_help(int command_count,\n \t\tmsg = _(\"\\nIf you remove a line here \"\n \t\t\t \"THAT COMMIT WILL BE LOST.\\n\");\n \n-\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str);\n \n \tif (edit_todo)\n \t\tmsg = _(\"\\nYou are editing the todo file \"\n@@ -98,7 +98,7 @@ void append_todo_help(int command_count,\n \t\tmsg = _(\"\\nHowever, if you remove everything, \"\n \t\t\t\"the rebase will be aborted.\\n\\n\");\n \n-\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str);\n }\n \n int edit_todo_list(struct repository *r, struct todo_list *todo_list,\ndiff --git a/sequencer.c b/sequencer.c\nindex 852c3f9f4e..032e213a3f 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1851,7 +1851,7 @@ static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)\n \t\ts += count;\n \t\tlen -= count;\n \t}\n-\tstrbuf_add_commented_lines(buf, s, len, comment_line_char);\n+\tstrbuf_add_commented_lines(buf, s, len, comment_line_str);\n }\n \n /* Does the current fixup chain contain a squash command? */\n@@ -1950,7 +1950,7 @@ static int append_squash_message(struct strbuf *buf, const char *body,\n \tstrbuf_addf(buf, _(nth_commit_msg_fmt),\n \t\t    ++opts->current_fixup_count + 1);\n \tstrbuf_addstr(buf, \"\\n\\n\");\n-\tstrbuf_add_commented_lines(buf, body, commented_len, comment_line_char);\n+\tstrbuf_add_commented_lines(buf, body, commented_len, comment_line_str);\n \t/* buf->buf may be reallocated so store an offset into the buffer */\n \tfixup_off = buf->len;\n \tstrbuf_addstr(buf, body + commented_len);\n@@ -2041,7 +2041,7 @@ static int update_squash_messages(struct repository *r,\n \t\tstrbuf_addstr(&buf, \"\\n\\n\");\n \t\tif (is_fixup_flag(command, flag))\n \t\t\tstrbuf_add_commented_lines(&buf, body, strlen(body),\n-\t\t\t\t\t\t   comment_line_char);\n+\t\t\t\t\t\t   comment_line_str);\n \t\telse\n \t\t\tstrbuf_addstr(&buf, body);\n \n@@ -2061,7 +2061,7 @@ static int update_squash_messages(struct repository *r,\n \t\t\t    ++opts->current_fixup_count + 1);\n \t\tstrbuf_addstr(&buf, \"\\n\\n\");\n \t\tstrbuf_add_commented_lines(&buf, body, strlen(body),\n-\t\t\t\t\t   comment_line_char);\n+\t\t\t\t\t   comment_line_str);\n \t} else\n \t\treturn error(_(\"unknown command: %d\"), command);\n \trepo_unuse_commit_buffer(r, commit, message);\ndiff --git a/strbuf.c b/strbuf.c\nindex 76d02e0920..7c8f582127 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -359,13 +359,9 @@ static void add_lines(struct strbuf *out,\n }\n \n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n-\t\t\t\tsize_t size, char comment_prefix)\n+\t\t\t\tsize_t size, const char *comment_prefix)\n {\n-\tchar prefix[2];\n-\n-\tprefix[0] = comment_prefix;\n-\tprefix[1] = '\\0';\n-\tadd_lines(out, prefix, buf, size, 1);\n+\tadd_lines(out, comment_prefix, buf, size, 1);\n }\n \n void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix,\n@@ -379,13 +375,7 @@ void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix,\n \tstrbuf_vaddf(&buf, fmt, params);\n \tva_end(params);\n \n-\t/*\n-\t * TODO Our commented_lines helper does not yet understand\n-\t * comment strings. But since we know that the strings are\n-\t * always single-char, we can cheat for the moment, and\n-\t * fix this later.\n-\t */\n-\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix[0]);\n+\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix);\n \tif (incomplete_line)\n \t\tsb->buf[--sb->len] = '\\0';\n \ndiff --git a/strbuf.h b/strbuf.h\nindex b128ca539a..58dddf2777 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -288,7 +288,7 @@ void strbuf_splice(struct strbuf *sb, size_t pos, size_t len,\n  */\n void strbuf_add_commented_lines(struct strbuf *out,\n \t\t\t\tconst char *buf, size_t size,\n-\t\t\t\tchar comment_prefix);\n+\t\t\t\tconst char *comment_prefix);\n \n \n /**\ndiff --git a/wt-status.c b/wt-status.c\nindex 2be2eb094c..6b81f5349c 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1028,7 +1028,7 @@ static void wt_longstatus_print_submodule_summary(struct wt_status *s, int uncom\n \tif (s->display_comment_prefix) {\n \t\tsize_t len;\n \t\tsummary_content = strbuf_detach(&summary, &len);\n-\t\tstrbuf_add_commented_lines(&summary, summary_content, len, comment_line_char);\n+\t\tstrbuf_add_commented_lines(&summary, summary_content, len, comment_line_str);\n \t\tfree(summary_content);\n \t}\n \n@@ -1104,7 +1104,7 @@ void wt_status_append_cut_line(struct strbuf *buf)\n \tconst char *explanation = _(\"Do not modify or remove the line above.\\nEverything below it will be ignored.\");\n \n \tstrbuf_commented_addf(buf, comment_line_str, \"%s\", cut_line);\n-\tstrbuf_add_commented_lines(buf, explanation, strlen(explanation), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, explanation, strlen(explanation), comment_line_str);\n }\n \n void wt_status_add_cut_line(FILE *fp)\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490158","messageId":"20240307092326.GI2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 09/15] prefer comment_line_str to comment_line_char for printing","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:23:26Z","receivedAt":"2024-03-07T09:23:28Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, we should\nuse the string variable rather than the historical character variable.\nAll of the sites adjusted here are just swapping out \"%c\" for \"%s\" in\nformat strings, or strbuf_addch() for strbuf_addstr(). The type system\nand printf-attribute give the compiler enough information to make sure\nour formats and variable changes all match (especially important for\ncases where the format string is defined far away from its use, like\nprepare_to_commit() in commit.c).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n add-patch.c      |  4 ++--\n builtin/branch.c |  4 ++--\n builtin/commit.c | 12 ++++++------\n builtin/merge.c  |  4 ++--\n builtin/tag.c    |  8 ++++----\n fmt-merge-msg.c  |  2 +-\n sequencer.c      | 20 ++++++++++----------\n wt-status.c      | 10 +++++-----\n 8 files changed, 32 insertions(+), 32 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 7390677795..4a10237d50 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1114,10 +1114,10 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \t\t\t\t\"To remove '%c' lines, make them ' ' lines \"\n \t\t\t\t\"(context).\\n\"\n \t\t\t\t\"To remove '%c' lines, delete them.\\n\"\n-\t\t\t\t\"Lines starting with %c will be removed.\\n\"),\n+\t\t\t\t\"Lines starting with %s will be removed.\\n\"),\n \t\t\t      s->mode->is_reverse ? '+' : '-',\n \t\t\t      s->mode->is_reverse ? '-' : '+',\n-\t\t\t      comment_line_char);\n+\t\t\t      comment_line_str);\n \tstrbuf_commented_addf(&s->buf, comment_line_str, \"%s\",\n \t\t\t      _(s->mode->edit_hunk_hint));\n \t/*\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex 8904a1e5d9..1cdcae8454 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -670,8 +670,8 @@ static int edit_branch_description(const char *branch_name)\n \tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t    _(\"Please edit the description for the branch\\n\"\n \t\t      \"  %s\\n\"\n-\t\t      \"Lines starting with '%c' will be stripped.\\n\"),\n-\t\t    branch_name, comment_line_char);\n+\t\t      \"Lines starting with '%s' will be stripped.\\n\"),\n+\t\t    branch_name, comment_line_str);\n \twrite_file_buf(edit_description(), buf.buf, buf.len);\n \tstrbuf_reset(&buf);\n \tif (launch_editor(edit_description(), &buf, NULL)) {\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex d8abbe48b1..8519a004d0 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -910,18 +910,18 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tstruct ident_split ci, ai;\n \t\tconst char *hint_cleanup_all = allow_empty_message ?\n \t\t\t_(\"Please enter the commit message for your changes.\"\n-\t\t\t  \" Lines starting\\nwith '%c' will be ignored.\\n\") :\n+\t\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\") :\n \t\t\t_(\"Please enter the commit message for your changes.\"\n-\t\t\t  \" Lines starting\\nwith '%c' will be ignored, and an empty\"\n+\t\t\t  \" Lines starting\\nwith '%s' will be ignored, and an empty\"\n \t\t\t  \" message aborts the commit.\\n\");\n \t\tconst char *hint_cleanup_space = allow_empty_message ?\n \t\t\t_(\"Please enter the commit message for your changes.\"\n \t\t\t  \" Lines starting\\n\"\n-\t\t\t  \"with '%c' will be kept; you may remove them\"\n+\t\t\t  \"with '%s' will be kept; you may remove them\"\n \t\t\t  \" yourself if you want to.\\n\") :\n \t\t\t_(\"Please enter the commit message for your changes.\"\n \t\t\t  \" Lines starting\\n\"\n-\t\t\t  \"with '%c' will be kept; you may remove them\"\n+\t\t\t  \"with '%s' will be kept; you may remove them\"\n \t\t\t  \" yourself if you want to.\\n\"\n \t\t\t  \"An empty message aborts the commit.\\n\");\n \t\tif (whence != FROM_COMMIT) {\n@@ -945,12 +945,12 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \n \t\tfprintf(s->fp, \"\\n\");\n \t\tif (cleanup_mode == COMMIT_MSG_CLEANUP_ALL)\n-\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_all, comment_line_char);\n+\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_all, comment_line_str);\n \t\telse if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {\n \t\t\tif (whence == FROM_COMMIT && !merge_contains_scissors)\n \t\t\t\twt_status_add_cut_line(s->fp);\n \t\t} else /* COMMIT_MSG_CLEANUP_SPACE, that is. */\n-\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_space, comment_line_char);\n+\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_space, comment_line_str);\n \n \t\t/*\n \t\t * These should never fail because they come from our own\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 6d048fb628..ba4308883f 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -821,7 +821,7 @@ static const char scissors_editor_comment[] =\n N_(\"An empty message aborts the commit.\\n\");\n \n static const char no_scissors_editor_comment[] =\n-N_(\"Lines starting with '%c' will be ignored, and an empty message aborts\\n\"\n+N_(\"Lines starting with '%s' will be ignored, and an empty message aborts\\n\"\n    \"the commit.\\n\");\n \n static void write_merge_heads(struct commit_list *);\n@@ -861,7 +861,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\t\t\t\t      _(scissors_editor_comment));\n \t\telse\n \t\t\tstrbuf_commented_addf(&msg, comment_line_str,\n-\t\t\t\t_(no_scissors_editor_comment), comment_line_char);\n+\t\t\t\t_(no_scissors_editor_comment), comment_line_str);\n \t}\n \tif (signoff)\n \t\tappend_signoff(&msg, ignored_log_message_bytes(msg.buf, msg.len), 0);\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 1c708785bf..721d07a589 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -158,11 +158,11 @@ static int do_sign(struct strbuf *buffer)\n \n static const char tag_template[] =\n \tN_(\"\\nWrite a message for tag:\\n  %s\\n\"\n-\t\"Lines starting with '%c' will be ignored.\\n\");\n+\t\"Lines starting with '%s' will be ignored.\\n\");\n \n static const char tag_template_nocleanup[] =\n \tN_(\"\\nWrite a message for tag:\\n  %s\\n\"\n-\t\"Lines starting with '%c' will be kept; you may remove them\"\n+\t\"Lines starting with '%s' will be kept; you may remove them\"\n \t\" yourself if you want to.\\n\");\n \n static int git_tag_config(const char *var, const char *value,\n@@ -292,10 +292,10 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n \t\t\tstrbuf_addch(&buf, '\\n');\n \t\t\tif (opt->cleanup_mode == CLEANUP_ALL)\n \t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n-\t\t\t\t      _(tag_template), tag, comment_line_char);\n+\t\t\t\t      _(tag_template), tag, comment_line_str);\n \t\t\telse\n \t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n-\t\t\t\t      _(tag_template_nocleanup), tag, comment_line_char);\n+\t\t\t\t      _(tag_template_nocleanup), tag, comment_line_str);\n \t\t\twrite_or_die(fd, buf.buf, buf.len);\n \t\t\tstrbuf_release(&buf);\n \t\t}\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 79e8aad086..ae201e21db 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -321,7 +321,7 @@ static void credit_people(struct strbuf *out,\n \t     skip_prefix(me, them->items->string, &me) &&\n \t     starts_with(me, \" <\")))\n \t\treturn;\n-\tstrbuf_addf(out, \"\\n%c %s \", comment_line_char, label);\n+\tstrbuf_addf(out, \"\\n%s %s \", comment_line_str, label);\n \tadd_people_count(out, them);\n }\n \ndiff --git a/sequencer.c b/sequencer.c\nindex 032e213a3f..241e185f87 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -663,7 +663,7 @@ void append_conflicts_hint(struct index_state *istate,\n \tif (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {\n \t\tstrbuf_addch(msgbuf, '\\n');\n \t\twt_status_append_cut_line(msgbuf);\n-\t\tstrbuf_addch(msgbuf, comment_line_char);\n+\t\tstrbuf_addstr(msgbuf, comment_line_str);\n \t}\n \n \tstrbuf_addch(msgbuf, '\\n');\n@@ -1946,7 +1946,7 @@ static int append_squash_message(struct strbuf *buf, const char *body,\n \t     (starts_with(body, \"squash!\") || starts_with(body, \"fixup!\"))))\n \t\tcommented_len = commit_subject_length(body);\n \n-\tstrbuf_addf(buf, \"\\n%c \", comment_line_char);\n+\tstrbuf_addf(buf, \"\\n%s \", comment_line_str);\n \tstrbuf_addf(buf, _(nth_commit_msg_fmt),\n \t\t    ++opts->current_fixup_count + 1);\n \tstrbuf_addstr(buf, \"\\n\\n\");\n@@ -2006,7 +2006,7 @@ static int update_squash_messages(struct repository *r,\n \t\teol = buf.buf[0] != comment_line_char ?\n \t\t\tbuf.buf : strchrnul(buf.buf, '\\n');\n \n-\t\tstrbuf_addf(&header, \"%c \", comment_line_char);\n+\t\tstrbuf_addf(&header, \"%s \", comment_line_str);\n \t\tstrbuf_addf(&header, _(combined_commit_msg_fmt),\n \t\t\t    opts->current_fixup_count + 2);\n \t\tstrbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);\n@@ -2032,9 +2032,9 @@ static int update_squash_messages(struct repository *r,\n \t\t\trepo_unuse_commit_buffer(r, head_commit, head_message);\n \t\t\treturn error(_(\"cannot write '%s'\"), rebase_path_fixup_msg());\n \t\t}\n-\t\tstrbuf_addf(&buf, \"%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"%s \", comment_line_str);\n \t\tstrbuf_addf(&buf, _(combined_commit_msg_fmt), 2);\n-\t\tstrbuf_addf(&buf, \"\\n%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"\\n%s \", comment_line_str);\n \t\tstrbuf_addstr(&buf, is_fixup_flag(command, flag) ?\n \t\t\t      _(skip_first_commit_msg_str) :\n \t\t\t      _(first_commit_msg_str));\n@@ -2056,7 +2056,7 @@ static int update_squash_messages(struct repository *r,\n \tif (command == TODO_SQUASH || is_fixup_flag(command, flag)) {\n \t\tres = append_squash_message(&buf, body, command, opts, flag);\n \t} else if (command == TODO_FIXUP) {\n-\t\tstrbuf_addf(&buf, \"\\n%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"\\n%s \", comment_line_str);\n \t\tstrbuf_addf(&buf, _(skip_nth_commit_msg_fmt),\n \t\t\t    ++opts->current_fixup_count + 1);\n \t\tstrbuf_addstr(&buf, \"\\n\\n\");\n@@ -5659,8 +5659,8 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\t\t\t    oid_to_hex(&commit->object.oid),\n \t\t\t\t    oneline.buf);\n \t\t\tif (is_empty)\n-\t\t\t\tstrbuf_addf(&buf, \" %c empty\",\n-\t\t\t\t\t    comment_line_char);\n+\t\t\t\tstrbuf_addf(&buf, \" %s empty\",\n+\t\t\t\t\t    comment_line_str);\n \n \t\t\tFLEX_ALLOC_STR(entry, string, buf.buf);\n \t\t\toidcpy(&entry->entry.oid, &commit->object.oid);\n@@ -5750,7 +5750,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\tentry = oidmap_get(&state.commit2label, &commit->object.oid);\n \n \t\tif (entry)\n-\t\t\tstrbuf_addf(out, \"\\n%c Branch %s\\n\", comment_line_char, entry->string);\n+\t\t\tstrbuf_addf(out, \"\\n%s Branch %s\\n\", comment_line_str, entry->string);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \n@@ -5887,7 +5887,7 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,\n \t\t\t    oid_to_hex(&commit->object.oid));\n \t\tpretty_print_commit(&pp, commit, out);\n \t\tif (is_empty)\n-\t\t\tstrbuf_addf(out, \" %c empty\", comment_line_char);\n+\t\t\tstrbuf_addf(out, \" %s empty\", comment_line_str);\n \t\tstrbuf_addch(out, '\\n');\n \t}\n \tif (skipped_commit)\ndiff --git a/wt-status.c b/wt-status.c\nindex 6b81f5349c..b66c30775b 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -70,7 +70,7 @@ static void status_vprintf(struct wt_status *s, int at_bol, const char *color,\n \tstrbuf_vaddf(&sb, fmt, ap);\n \tif (!sb.len) {\n \t\tif (s->display_comment_prefix) {\n-\t\t\tstrbuf_addch(&sb, comment_line_char);\n+\t\t\tstrbuf_addstr(&sb, comment_line_str);\n \t\t\tif (!trail)\n \t\t\t\tstrbuf_addch(&sb, ' ');\n \t\t}\n@@ -85,7 +85,7 @@ static void status_vprintf(struct wt_status *s, int at_bol, const char *color,\n \n \t\tstrbuf_reset(&linebuf);\n \t\tif (at_bol && s->display_comment_prefix) {\n-\t\t\tstrbuf_addch(&linebuf, comment_line_char);\n+\t\t\tstrbuf_addstr(&linebuf, comment_line_str);\n \t\t\tif (*line != '\\n' && *line != '\\t')\n \t\t\t\tstrbuf_addch(&linebuf, ' ');\n \t\t}\n@@ -1090,7 +1090,7 @@ size_t wt_status_locate_end(const char *s, size_t len)\n \tconst char *p;\n \tstruct strbuf pattern = STRBUF_INIT;\n \n-\tstrbuf_addf(&pattern, \"\\n%c %s\", comment_line_char, cut_line);\n+\tstrbuf_addf(&pattern, \"\\n%s %s\", comment_line_str, cut_line);\n \tif (starts_with(s, pattern.buf + 1))\n \t\tlen = 0;\n \telse if ((p = strstr(s, pattern.buf)))\n@@ -1214,8 +1214,8 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \t\t\t\t \"%s%.*s\", comment_line_string,\n \t\t\t\t (int)(ep - cp), cp);\n \tif (s->display_comment_prefix)\n-\t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%c\",\n-\t\t\t\t comment_line_char);\n+\t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%s\",\n+\t\t\t\t comment_line_str);\n \telse\n \t\tfputs(\"\\n\", s->fp);\n \tstrbuf_release(&sb);\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490159","messageId":"20240307092416.GJ2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 10/15] find multi-byte comment chars in NUL-terminated strings","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:24:16Z","receivedAt":"2024-03-07T09:24:18Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Several parts of the code need to identify lines that begin with the\ncomment character, and do so with a simple byte equality check. As part\nof the transition to handling multi-byte characters, we need to match\nall of the bytes. For cases where we are looking in a NUL-terminated\nstring, we can just use starts_with(), which checks all of the\ncharacters in comment_line_str.\n\nNote that we can drop the \"line.len\" check in wt-status.c's\nread_rebase_todolist(). The starts_with() function handles the case of\nan empty haystack buffer (it will always return false for a non-empty\nprefix).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nI think the main way these hunks could be wrong is if the buffer is not\nin fact NUL-terminated. In most cases we're working with a strbuf,\nthough.\n\n add-patch.c | 2 +-\n sequencer.c | 2 +-\n trailer.c   | 2 +-\n wt-status.c | 2 +-\n 4 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 4a10237d50..d599ca53e1 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1139,7 +1139,7 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \tfor (i = 0; i < s->buf.len; ) {\n \t\tsize_t next = find_next_line(&s->buf, i);\n \n-\t\tif (s->buf.buf[i] != comment_line_char)\n+\t\tif (!starts_with(s->buf.buf + i, comment_line_str))\n \t\t\tstrbuf_add(&s->plain, s->buf.buf + i, next - i);\n \t\ti = next;\n \t}\ndiff --git a/sequencer.c b/sequencer.c\nindex 241e185f87..991a2dbe96 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2003,7 +2003,7 @@ static int update_squash_messages(struct repository *r,\n \t\t\treturn error(_(\"could not read '%s'\"),\n \t\t\t\trebase_path_squash_msg());\n \n-\t\teol = buf.buf[0] != comment_line_char ?\n+\t\teol = !starts_with(buf.buf, comment_line_str) ?\n \t\t\tbuf.buf : strchrnul(buf.buf, '\\n');\n \n \t\tstrbuf_addf(&header, \"%s \", comment_line_str);\ndiff --git a/trailer.c b/trailer.c\nindex ef9df4af55..fe18faf6c5 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -1013,7 +1013,7 @@ static void parse_trailers(struct trailer_info *info,\n \tfor (i = 0; i < info->trailer_nr; i++) {\n \t\tint separator_pos;\n \t\tchar *trailer = info->trailers[i];\n-\t\tif (trailer[0] == comment_line_char)\n+\t\tif (starts_with(trailer, comment_line_str))\n \t\t\tcontinue;\n \t\tseparator_pos = find_separator(trailer, separators);\n \t\tif (separator_pos >= 1) {\ndiff --git a/wt-status.c b/wt-status.c\nindex b66c30775b..084bfc584f 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1382,7 +1382,7 @@ static int read_rebase_todolist(const char *fname, struct string_list *lines)\n \t\t\t  git_path(\"%s\", fname));\n \t}\n \twhile (!strbuf_getline_lf(&line, f)) {\n-\t\tif (line.len && line.buf[0] == comment_line_char)\n+\t\tif (starts_with(line.buf, comment_line_str))\n \t\t\tcontinue;\n \t\tstrbuf_trim(&line);\n \t\tif (!line.len)\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490160","messageId":"20240307092638.GK2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:26:38Z","receivedAt":"2024-03-07T09:26:39Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As with the previous patch, we need to swap out single-byte matching for\nsomething like starts_with() to match all bytes of a multi-byte comment\ncharacter. But for cases where the buffer is not NUL-terminated (and we\ninstead have an explicit size or end pointer), it's not safe to use\nstarts_with(), as it might walk off the end of the buffer.\n\nLet's introduce a new starts_with_mem() that does the same thing but\nalso accepts the length of the \"haystack\" str and makes sure not to walk\npast it.\n\nNote that in most cases the existing code did not need a length check at\nall, since it was written in a way that knew we had at least one byte\navailable (and that was all we checked). So I had to read each one to\nfind the appropriate bounds. The one exception is sequencer.c's\nadd_commented_lines(), where we can actually get rid of the length\ncheck. Just like starts_with(), our starts_with_mem() handles an empty\nhaystack variable by not matching (assuming a non-empty prefix).\n\nA few notes on the implementation of starts_with_mem():\n\n  - it would be equally correct to take an \"end\" pointer (and indeed,\n    many of the callers have this and have to subtract to come up with\n    the length). I think taking a ptr/size combo is a more usual\n    interface for our codebase, though, and has the added benefit that\n    the function signature makes it harder to mix up the three\n    parameters.\n\n  - we could obviously build starts_with() on top of this by passing\n    strlen(str) as the length. But it's possible that starts_with() is a\n    relatively hot code path, and it should not pay that penalty (it can\n    generally return an answer proportional to the size of the prefix,\n    not the whole string).\n\n  - it naively feels like xstrncmpz() should be able to do the same\n    thing, but that's not quite true. If you pass the length of the\n    haystack buffer, then strncmp() finds that a shorter prefix string\n    is \"less than\" than the haystack, even if the haystack starts with\n    the prefix. If you pass the length of the prefix, then you risk\n    reading past the end of the haystack if it is shorter than the\n    prefix. So I think we really do need a new function.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nArguably starts_with() and this new function should both be inlined,\nlike we do for skip_prefix(), but I think that's out of scope for this\nseries.\n\nAnd it's possible I was simply too dumb to figure out xstrncmpz() here.\nI'm waiting for René to show up and tell me how to do it. ;)\n\nIMHO this is the trickiest commit of the whole series, as it would be\neasy to get the length computations subtly wrong.\n\n commit.c    |  3 ++-\n sequencer.c |  4 ++--\n strbuf.c    | 11 +++++++++++\n strbuf.h    |  1 +\n trailer.c   |  4 ++--\n 5 files changed, 18 insertions(+), 5 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex ef679a0b93..531a666cba 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1796,7 +1796,8 @@ size_t ignored_log_message_bytes(const char *buf, size_t len)\n \t\telse\n \t\t\tnext_line++;\n \n-\t\tif (buf[bol] == comment_line_char || buf[bol] == '\\n') {\n+\t\tif (starts_with_mem(buf + bol, cutoff - bol, comment_line_str) ||\n+\t\t    buf[bol] == '\\n') {\n \t\t\t/* is this the first of the run of comments? */\n \t\t\tif (!boc)\n \t\t\t\tboc = bol;\ndiff --git a/sequencer.c b/sequencer.c\nindex 991a2dbe96..664986e3b2 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1840,7 +1840,7 @@ static int is_fixup_flag(enum todo_command command, unsigned flag)\n static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)\n {\n \tconst char *s = str;\n-\twhile (len > 0 && s[0] == comment_line_char) {\n+\twhile (starts_with_mem(s, len, comment_line_str)) {\n \t\tsize_t count;\n \t\tconst char *n = memchr(s, '\\n', len);\n \t\tif (!n)\n@@ -2562,7 +2562,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,\n \t/* left-trim */\n \tbol += strspn(bol, \" \\t\");\n \n-\tif (bol == eol || *bol == '\\r' || *bol == comment_line_char) {\n+\tif (bol == eol || *bol == '\\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {\n \t\titem->command = TODO_COMMENT;\n \t\titem->commit = NULL;\n \t\titem->arg_offset = bol - buf;\ndiff --git a/strbuf.c b/strbuf.c\nindex 7c8f582127..291bdc2a65 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -24,6 +24,17 @@ int istarts_with(const char *str, const char *prefix)\n \t\t\treturn 0;\n }\n \n+int starts_with_mem(const char *str, size_t len, const char *prefix)\n+{\n+\tconst char *end = str + len;\n+\tfor (; ; str++, prefix++) {\n+\t\tif (!*prefix)\n+\t\t\treturn 1;\n+\t\telse if (str == end || *str != *prefix)\n+\t\t\treturn 0;\n+\t}\n+}\n+\n int skip_to_optional_arg_default(const char *str, const char *prefix,\n \t\t\t\t const char **arg, const char *def)\n {\ndiff --git a/strbuf.h b/strbuf.h\nindex 58dddf2777..3156d6ea8c 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -673,6 +673,7 @@ char *xstrfmt(const char *fmt, ...);\n \n int starts_with(const char *str, const char *prefix);\n int istarts_with(const char *str, const char *prefix);\n+int starts_with_mem(const char *str, size_t len, const char *prefix);\n \n /*\n  * If the string \"str\" is the same as the string in \"prefix\", then the \"arg\"\ndiff --git a/trailer.c b/trailer.c\nindex fe18faf6c5..f59c90b4b5 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n \n \t/* The first paragraph is the title and cannot be trailers */\n \tfor (s = buf; s < buf + len; s = next_line(s)) {\n-\t\tif (s[0] == comment_line_char)\n+\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n \t\t\tcontinue;\n \t\tif (is_blank_line(s))\n \t\t\tbreak;\n@@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n \t\tconst char **p;\n \t\tssize_t separator_pos;\n \n-\t\tif (bol[0] == comment_line_char) {\n+\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n \t\t\tnon_trailer_lines += possible_continuation_lines;\n \t\t\tpossible_continuation_lines = 0;\n \t\t\tcontinue;\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490161","messageId":"20240307092747.GL2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 12/15] sequencer: handle multi-byte comment characters when writing todo list","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:27:47Z","receivedAt":"2024-03-07T09:27:49Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"We already match multi-byte comment characters in parse_insn_line(),\nthanks to the previous commit, yielding a TODO_COMMENT entry. But in\ntodo_list_to_strbuf(), we may call command_to_char() to convert that\nback into something we can output.\n\nWe can't just return comment_line_char anymore, since it may require\nmultiple bytes. Instead, we'll return \"0\" for this case, which is the\nsame thing we'd return for a command which does not have a single-letter\nabbreviation (e.g., \"revert\" or \"noop\"). In that case the caller then\nfalls back to outputting the full name via command_to_string(). So we\ncan handle TODO_COMMENT there, returning the full string.\n\nNote that there are many other callers of command_to_string(), which\nwill now behave differently if they pass TODO_COMMENT. But we would not\nexpect that to happen; prior to this commit, the function just calls\ndie() in this case. And looking at those callers, that makes sense;\ne.g., do_pick_commit() will only be called when servicing a pick\ncommand, and should never be called for a comment in the first place.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n sequencer.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 664986e3b2..9e2851428b 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1779,14 +1779,16 @@ static const char *command_to_string(const enum todo_command command)\n {\n \tif (command < TODO_COMMENT)\n \t\treturn todo_command_info[command].str;\n+\tif (command == TODO_COMMENT)\n+\t\treturn comment_line_str;\n \tdie(_(\"unknown command: %d\"), command);\n }\n \n static char command_to_char(const enum todo_command command)\n {\n \tif (command < TODO_COMMENT)\n \t\treturn todo_command_info[command].c;\n-\treturn comment_line_char;\n+\treturn 0;\n }\n \n static int is_noop(const enum todo_command command)\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490162","messageId":"20240307092832.GM2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 13/15] wt-status: drop custom comment-char stringification","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:28:32Z","receivedAt":"2024-03-07T09:28:33Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"In wt_longstatus_print_tracking() we may conditionally show a comment\nprefix based on the wt_status->display_comment_prefix flag. We handle\nthat by creating a local \"comment_line_string\" that is either the empty\nstring or the comment character followed by a space.\n\nFor a single-byte comment, the maximum length of this string is 2 (plus\na NUL byte). But to handle multi-byte comment characters, it can be\narbitrarily large. One way to handle this is to just call\nxstrfmt(\"%s \", comment_line_str), and then free it when we're done.\n\nBut we can simplify things further by just conditionally switching\nbetween our prefix string and an empty string when formatting. We\ncouldn't just do that with the previous code, because the comment\ncharacter was a single byte. There's no way to have a \"%c\" format switch\nbetween some character and \"no character at all\". Whereas with \"%s\" you\ncan switch between some string and the empty string. So now that we have\na comment string and not a comment char, we can just use it directly\nwhen formatting. Do note that we have to also conditionally add the\ntrailing space at the same time.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nI had hoped to clean this up as a preparatory commit, but it really is\nawkward until we can make use of comment_line_str, for the reasons given\nabove.\n\n wt-status.c | 13 +++----------\n 1 file changed, 3 insertions(+), 10 deletions(-)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex 084bfc584f..823e8e81b0 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1176,8 +1176,6 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \tstruct strbuf sb = STRBUF_INIT;\n \tconst char *cp, *ep, *branch_name;\n \tstruct branch *branch;\n-\tchar comment_line_string[3];\n-\tint i;\n \tuint64_t t_begin = 0;\n \n \tassert(s->branch && !s->is_initial);\n@@ -1202,16 +1200,11 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \t\t}\n \t}\n \n-\ti = 0;\n-\tif (s->display_comment_prefix) {\n-\t\tcomment_line_string[i++] = comment_line_char;\n-\t\tcomment_line_string[i++] = ' ';\n-\t}\n-\tcomment_line_string[i] = '\\0';\n-\n \tfor (cp = sb.buf; (ep = strchr(cp, '\\n')) != NULL; cp = ep + 1)\n \t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s),\n-\t\t\t\t \"%s%.*s\", comment_line_string,\n+\t\t\t\t \"%s%s%.*s\",\n+\t\t\t\t s->display_comment_prefix ? comment_line_str : \"\",\n+\t\t\t\t s->display_comment_prefix ? \" \" : \"\",\n \t\t\t\t (int)(ep - cp), cp);\n \tif (s->display_comment_prefix)\n \t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%s\",\n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490163","messageId":"20240307093005.GN2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 14/15] environment: drop comment_line_char compatibility macro","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:30:05Z","receivedAt":"2024-03-07T09:30:07Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"There is no longer any code which references the single-byte\ncomment_line_char. Let's drop it, clearing the way for true multi-byte\nentries in comment_line_str.\n\nIt's possible there are topics in flight that have added new references\nto comment_line_char. But we would prefer to fail compilation (and then\nfix it) upon merging with this, rather than have them quietly ignore the\nbytes after the first.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nI did merge against 'next' and there are no such topics. And likewise\n\"log -Scomment_line_char next..seen\" shows nothing. But as somebody who\nmaintained a long-running fork for many years, who knows what people are\ncarrying in their private trees. ;)\n\n environment.h | 1 -\n 1 file changed, 1 deletion(-)\n\ndiff --git a/environment.h b/environment.h\nindex 3496474cce..a8b06674eb 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -8,7 +8,6 @@ struct strvec;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-#define comment_line_char (comment_line_str[0])\n extern const char *comment_line_str;\n extern int auto_comment_line_char;\n \n-- \n2.44.0.463.g71abcb3a9f\n\n"},{"id":"490164","messageId":"20240307093431.GO2080210@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH 15/15] config: allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:34:31Z","receivedAt":"2024-03-07T09:34:32Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Now that all of the code handles multi-byte comment characters, it's\nsafe to allow users to set them.\n\nThere is one special case I kept: we still will not allow an empty\nstring for the commentChar. While it might make sense in some contexts\n(e.g., output where you don't want any comment prefix), there are plenty\nwhere it will behave badly (e.g., all of our starts_with() checks will\nindicate that every line is a comment!). It might be reasonable to\nassign some meaningful semantics, but it would probably involve checking\nhow each site behaves. In the interim let's forbid it and we can loosen\nthings later.\n\nSince comment_line_str is used in many parts of the code, it's hard to\ncover all possibilities with tests. We can convert the existing\ndouble-semicolon prefix test to show that \"git status\" works. And we'll\ngive it a more challenging case in t7507, where we confirm that\ngit-commit strips out the commit template along with any --verbose text\nwhen reading the edited commit message back in. That covers the basics,\nthough it's possible there could be issues in more exotic spots (e.g.,\nthe sequencer todo list uses its own code).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nObviously everything works using the \"str\" variant with a single\ncharacter, and many tests are already covering that. You can swap out\nthe default to \"foo>\" or something and run the test suite, but there are\nmany spots that hard-code \"#\" in their expectations.\n\nI do think it's an acceptable risk, though; for the most part you'd only\nfind new bugs if you set a multi-byte core.commentChar, which was simply\nnot allowed before. So we're more likely to see bugs in the new feature\nthan regression of existing cases.\n\n Documentation/config/core.txt |  4 +++-\n config.c                      |  6 +++---\n t/t0030-stripspace.sh         |  5 +++++\n t/t7507-commit-verbose.sh     | 10 ++++++++++\n t/t7508-status.sh             |  4 +++-\n 5 files changed, 24 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex 0e8c2832bf..c86b8c8408 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -523,7 +523,9 @@ core.commentChar::\n \tCommands such as `commit` and `tag` that let you edit\n \tmessages consider a line that begins with this character\n \tcommented, and removes them after the editor returns\n-\t(default '#').\n+\t(default '#'). Note that this option can take values larger than\n+\ta byte (whether a single multi-byte character, or you\n+\tcould even go wild with a multi-character sequence).\n +\n If set to \"auto\", `git-commit` would select a character that is not\n the beginning character of any line in existing commit messages.\ndiff --git a/config.c b/config.c\nindex e12ea68f24..4dea34936c 100644\n--- a/config.c\n+++ b/config.c\n@@ -1565,11 +1565,11 @@ static int git_default_core_config(const char *var, const char *value,\n \t\t\treturn config_error_nonbool(var);\n \t\telse if (!strcasecmp(value, \"auto\"))\n \t\t\tauto_comment_line_char = 1;\n-\t\telse if (value[0] && !value[1]) {\n-\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n+\t\telse if (value[0]) {\n+\t\t\tcomment_line_str = xstrdup(value);\n \t\t\tauto_comment_line_char = 0;\n \t\t} else\n-\t\t\treturn error(_(\"core.commentChar should only be one ASCII character\"));\n+\t\t\treturn error(_(\"core.commentChar must have at least one character\"));\n \t\treturn 0;\n \t}\n \ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex d1b3be8725..9cdf2bddbd 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -401,6 +401,11 @@ test_expect_success 'strip comments with changed comment char' '\n \ttest -z \"$(echo \"; comment\" | git -c core.commentchar=\";\" stripspace -s)\"\n '\n \n+test_expect_success 'empty commentchar is forbidden' '\n+\ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n+\tgrep \"core.commentChar must have at least one character\" err\n+'\n+\n test_expect_success '-c with single line' '\n \tprintf \"# foo\\n\" >expect &&\n \tprintf \"foo\" | git stripspace -c >actual &&\ndiff --git a/t/t7507-commit-verbose.sh b/t/t7507-commit-verbose.sh\nindex c3281b192e..4c7db19ce7 100755\n--- a/t/t7507-commit-verbose.sh\n+++ b/t/t7507-commit-verbose.sh\n@@ -101,6 +101,16 @@ test_expect_success 'verbose diff is stripped out with set core.commentChar' '\n \ttest_grep \"Aborting commit due to empty commit message.\" err\n '\n \n+test_expect_success 'verbose diff is stripped with multi-byte comment char' '\n+\t(\n+\t\tGIT_EDITOR=cat &&\n+\t\texport GIT_EDITOR &&\n+\t\ttest_must_fail git -c core.commentchar=\"foo>\" commit -a -v >out 2>err\n+\t) &&\n+\tgrep \"^foo> \" out &&\n+\ttest_grep \"Aborting commit due to empty commit message.\" err\n+'\n+\n test_expect_success 'status does not verbose without --verbose' '\n \tgit status >actual &&\n \t! grep \"^diff --git\" actual\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex a3c18a4fc2..10ed8b32bc 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -1403,7 +1403,9 @@ test_expect_success \"status (core.commentchar with submodule summary)\" '\n \n test_expect_success \"status (core.commentchar with two chars with submodule summary)\" '\n \ttest_config core.commentchar \";;\" &&\n-\ttest_must_fail git -c status.displayCommentPrefix=true status\n+\tsed \"s/^/;/\" <expect >expect.double &&\n+\tgit -c status.displayCommentPrefix=true status >output &&\n+\ttest_cmp expect.double output\n '\n \n test_expect_success \"--ignore-submodules=all suppresses submodule summary\" '\n-- \n2.44.0.463.g71abcb3a9f\n"},{"id":"490166","messageId":"20240307095332.GB2650063@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307092126.GF2080210@coredump.intra.peff.net","subject":"Re: [PATCH 06/15] strbuf: accept a comment string for strbuf_stripspace()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T09:53:32Z","receivedAt":"2024-03-07T09:53:33Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Mar 07, 2024 at 04:21:26AM -0500, Jeff King wrote:\n\n> As part of our transition to multi-byte comment characters, let's take a\n> NUL-terminated string pointer for strbuf_stripspace(), rather than a\n> single character. We can continue to support its feature of ignoring\n> comments by accepting a NULL pointer (as opposed to the current behavior\n> of a NUL byte).\n> \n> All of the callers have to be adjusted, but they can all just pass\n> comment_line_str (or NULL).\n\nBah. I relied on the compiler to tell me the call-sites that needed to\nbe adjusted. But interestingly gcc is quite happy to allow '\\0' to be\npassed in place of a pointer, but clang complains:\n\n  gpg-interface.c:589:37: error: expression which evaluates to zero treated as a null pointer constant of type 'const char *' [-Werror,-Wnon-literal-null-conversion]\n          strbuf_stripspace(&ssh_keygen_out, '\\0');\n                                             ^~~~\n\nLikewise there are a few bare \"0\"'s which do not cause a warning, but\nwhich violate our style standards. So I think we'd want to squash the\npatch below in to this step. The other functions don't need the same\ntreatment because they never treated NUL specially.\n\n---\ndiff --git a/builtin/am.c b/builtin/am.c\nindex d1990d7edc..5bc72d7822 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1286,7 +1286,7 @@ static int parse_mail(struct am_state *state, const char *mail)\n \n \tstrbuf_addstr(&msg, \"\\n\\n\");\n \tstrbuf_addbuf(&msg, &mi.log_message);\n-\tstrbuf_stripspace(&msg, '\\0');\n+\tstrbuf_stripspace(&msg, NULL);\n \n \tassert(!state->author_name);\n \tstate->author_name = strbuf_detach(&author_name, NULL);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 8519a004d0..e04f1236e8 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -890,7 +890,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \ts->hints = 0;\n \n \tif (clean_message_contents)\n-\t\tstrbuf_stripspace(&sb, '\\0');\n+\t\tstrbuf_stripspace(&sb, NULL);\n \n \tif (signoff)\n \t\tappend_signoff(&sb, ignored_log_message_bytes(sb.buf, sb.len), 0);\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 1a67f01d00..cb011303e6 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -264,7 +264,7 @@ static void concat_messages(struct note_data *d)\n \t\tif ((d->stripspace == UNSPECIFIED &&\n \t\t     d->messages[i]->stripspace == STRIPSPACE) ||\n \t\t    d->stripspace == STRIPSPACE)\n-\t\t\tstrbuf_stripspace(&d->buf, 0);\n+\t\t\tstrbuf_stripspace(&d->buf, NULL);\n \t\tstrbuf_reset(&msg);\n \t}\n \tstrbuf_release(&msg);\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 9c76b62b02..f0aa962cf8 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -657,7 +657,7 @@ static int can_use_local_refs(const struct add_opts *opts)\n \t\t\tstrbuf_add_real_path(&path, get_worktree_git_dir(NULL));\n \t\t\tstrbuf_addstr(&path, \"/HEAD\");\n \t\t\tstrbuf_read_file(&contents, path.buf, 64);\n-\t\t\tstrbuf_stripspace(&contents, 0);\n+\t\t\tstrbuf_stripspace(&contents, NULL);\n \t\t\tstrbuf_strip_suffix(&contents, \"\\n\");\n \n \t\t\twarning(_(\"HEAD points to an invalid (or orphaned) reference.\\n\"\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 95e764acb1..b5993385ff 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -586,8 +586,8 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,\n \t\t}\n \t}\n \n-\tstrbuf_stripspace(&ssh_keygen_out, '\\0');\n-\tstrbuf_stripspace(&ssh_keygen_err, '\\0');\n+\tstrbuf_stripspace(&ssh_keygen_out, NULL);\n+\tstrbuf_stripspace(&ssh_keygen_err, NULL);\n \t/* Add stderr outputs to show the user actual ssh-keygen errors */\n \tstrbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);\n \tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n"},{"id":"490170","messageId":"20240307110809.GA3668372@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307092638.GK2080210@coredump.intra.peff.net","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-07T11:08:09Z","receivedAt":"2024-03-07T11:08:11Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Mar 07, 2024 at 04:26:38AM -0500, Jeff King wrote:\n\n> IMHO this is the trickiest commit of the whole series, as it would be\n> easy to get the length computations subtly wrong.\n\nAnd sure enough...\n\n> diff --git a/trailer.c b/trailer.c\n> index fe18faf6c5..f59c90b4b5 100644\n> --- a/trailer.c\n> +++ b/trailer.c\n> @@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>  \n>  \t/* The first paragraph is the title and cannot be trailers */\n>  \tfor (s = buf; s < buf + len; s = next_line(s)) {\n> -\t\tif (s[0] == comment_line_char)\n> +\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n>  \t\t\tcontinue;\n>  \t\tif (is_blank_line(s))\n>  \t\t\tbreak;\n> @@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>  \t\tconst char **p;\n>  \t\tssize_t separator_pos;\n>  \n> -\t\tif (bol[0] == comment_line_char) {\n> +\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n>  \t\t\tnon_trailer_lines += possible_continuation_lines;\n>  \t\t\tpossible_continuation_lines = 0;\n>  \t\t\tcontinue;\n\nThis second hunk needs:\n\ndiff --git a/trailer.c b/trailer.c\nindex f59c90b4b5..fdb0b8137e 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n \t\tconst char **p;\n \t\tssize_t separator_pos;\n \n-\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n+\t\tif (starts_with_mem(bol, buf + len - bol, comment_line_str)) {\n \t\t\tnon_trailer_lines += possible_continuation_lines;\n \t\t\tpossible_continuation_lines = 0;\n \t\t\tcontinue;\n\nI was trying to bound the size based on the loop, which is:\n\n          for (l = last_line(buf, len);\n               l >= end_of_title;\n               l = last_line(buf, l)) {\n                  const char *bol = buf + l;\n\nbut I misread \"end_of_title\" as an upper bound, not a lower one. Which\nmakes sense because we're iterating backwards over the lines. So I\nsuppose we could bound it by the previous \"bol\" value. But in practice,\nyour prefix won't cross such a boundary anyway, as it won't have a\nnewline in it (maybe that's something we should enforce? I guess you\ncould set core.commentChar to '\\n' even before my series, which would be\nslightly insane).\n\nSo just bounding ourselves to \"buf + len\" seems reasonable, as that\nmakes sure we don't step outside the buffer passed into the function.\n\nCuriously, this was found by the sanitizer job in CI, where UBSan\ncomplains of integer overflow in the pointer computation. I had run with\nboth ASan/UBSan locally, but just using gcc, which doesn't seem to find\nit (the CI job uses clang). So I'll that to my mental tally of \"clang\nseems to be better with sanitizers\".\n\n-Peff\n"},{"id":"490203","messageId":"742ed8e6-d68b-47a0-8d57-731716c5365d@web.de","threadId":"61052","inReplyTo":"20240307110809.GA3668372@coredump.intra.peff.net","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2024-03-07T19:41:59Z","receivedAt":"2024-03-07T19:42:42Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 07.03.24 um 12:08 schrieb Jeff King:\n> On Thu, Mar 07, 2024 at 04:26:38AM -0500, Jeff King wrote:\n>\n>> IMHO this is the trickiest commit of the whole series, as it would be\n>> easy to get the length computations subtly wrong.\n>\n> And sure enough...\n>\n>> diff --git a/trailer.c b/trailer.c\n>> index fe18faf6c5..f59c90b4b5 100644\n>> --- a/trailer.c\n>> +++ b/trailer.c\n>> @@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>>\n>>  \t/* The first paragraph is the title and cannot be trailers */\n>>  \tfor (s = buf; s < buf + len; s = next_line(s)) {\n>> -\t\tif (s[0] == comment_line_char)\n>> +\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n>>  \t\t\tcontinue;\n>>  \t\tif (is_blank_line(s))\n>>  \t\t\tbreak;\n>> @@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>>  \t\tconst char **p;\n>>  \t\tssize_t separator_pos;\n>>\n>> -\t\tif (bol[0] == comment_line_char) {\n>> +\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n>>  \t\t\tnon_trailer_lines += possible_continuation_lines;\n>>  \t\t\tpossible_continuation_lines = 0;\n>>  \t\t\tcontinue;\n>\n> This second hunk needs:\n>\n> diff --git a/trailer.c b/trailer.c\n> index f59c90b4b5..fdb0b8137e 100644\n> --- a/trailer.c\n> +++ b/trailer.c\n> @@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>  \t\tconst char **p;\n>  \t\tssize_t separator_pos;\n>\n> -\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n> +\t\tif (starts_with_mem(bol, buf + len - bol, comment_line_str)) {\n>  \t\t\tnon_trailer_lines += possible_continuation_lines;\n>  \t\t\tpossible_continuation_lines = 0;\n>  \t\t\tcontinue;\n>\n> I was trying to bound the size based on the loop, which is:\n>\n>           for (l = last_line(buf, len);\n>                l >= end_of_title;\n>                l = last_line(buf, l)) {\n>                   const char *bol = buf + l;\n>\n> but I misread \"end_of_title\" as an upper bound, not a lower one. Which\n> makes sense because we're iterating backwards over the lines. So I\n> suppose we could bound it by the previous \"bol\" value. But in practice,\n> your prefix won't cross such a boundary anyway, as it won't have a\n> newline in it (maybe that's something we should enforce? I guess you\n> could set core.commentChar to '\\n' even before my series, which would be\n> slightly insane).\n>\n> So just bounding ourselves to \"buf + len\" seems reasonable, as that\n> makes sure we don't step outside the buffer passed into the function.\n>\n> Curiously, this was found by the sanitizer job in CI, where UBSan\n> complains of integer overflow in the pointer computation. I had run with\n> both ASan/UBSan locally, but just using gcc, which doesn't seem to find\n> it (the CI job uses clang). So I'll that to my mental tally of \"clang\n> seems to be better with sanitizers\".\n>\n> -Peff\n\n"},{"id":"490204","messageId":"3f823e48-572c-4e19-ab76-e6d7cab9461f@web.de","threadId":"61052","inReplyTo":"20240307092638.GK2080210@coredump.intra.peff.net","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2024-03-07T19:42:22Z","receivedAt":"2024-03-07T19:42:46Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 07.03.24 um 10:26 schrieb Jeff King:\n> As with the previous patch, we need to swap out single-byte matching for\n> something like starts_with() to match all bytes of a multi-byte comment\n> character. But for cases where the buffer is not NUL-terminated (and we\n> instead have an explicit size or end pointer), it's not safe to use\n> starts_with(), as it might walk off the end of the buffer.\n>\n> Let's introduce a new starts_with_mem() that does the same thing but\n> also accepts the length of the \"haystack\" str and makes sure not to walk\n> past it.\n>\n> Note that in most cases the existing code did not need a length check at\n> all, since it was written in a way that knew we had at least one byte\n> available (and that was all we checked). So I had to read each one to\n> find the appropriate bounds. The one exception is sequencer.c's\n> add_commented_lines(), where we can actually get rid of the length\n> check. Just like starts_with(), our starts_with_mem() handles an empty\n> haystack variable by not matching (assuming a non-empty prefix).\n>\n> A few notes on the implementation of starts_with_mem():\n>\n>   - it would be equally correct to take an \"end\" pointer (and indeed,\n>     many of the callers have this and have to subtract to come up with\n>     the length). I think taking a ptr/size combo is a more usual\n>     interface for our codebase, though, and has the added benefit that\n>     the function signature makes it harder to mix up the three\n>     parameters.\n>\n>   - we could obviously build starts_with() on top of this by passing\n>     strlen(str) as the length. But it's possible that starts_with() is a\n>     relatively hot code path, and it should not pay that penalty (it can\n>     generally return an answer proportional to the size of the prefix,\n>     not the whole string).\n>\n>   - it naively feels like xstrncmpz() should be able to do the same\n>     thing, but that's not quite true. If you pass the length of the\n>     haystack buffer, then strncmp() finds that a shorter prefix string\n>     is \"less than\" than the haystack, even if the haystack starts with\n>     the prefix. If you pass the length of the prefix, then you risk\n>     reading past the end of the haystack if it is shorter than the\n>     prefix. So I think we really do need a new function.\n\nYes.  xstrncmpz() compares a NUL-terminated string and a length-limited\nstring.  If you want to check whether the former is a prefix of the\nlatter then you need to stop comparing when reaching its NUL, and also\nafter exhausting the latter.  So you need to take both lengths into\naccount:\n\nint starts_with_mem(const char *str, size_t len, const char *prefix)\n{\n\tsize_t prefixlen = strlen(prefix);\n\treturn prefixlen <= len && !xstrncmpz(prefix, str, prefixlen);\n}\n\nUsing memcmp() here is equivalent and simpler:\n\nint starts_with_mem(const char *str, size_t len, const char *prefix)\n{\n\tsize_t prefixlen = strlen(prefix);\n\treturn prefixlen <= len && !memcmp(str, prefix, prefixlen);\n}\n\nAnd your version below avoids function calls and avoids traversing the\nstrings beyond their common prefix, of course.\n\n>\n> Signed-off-by: Jeff King <peff@peff.net>\n> ---\n> Arguably starts_with() and this new function should both be inlined,\n> like we do for skip_prefix(), but I think that's out of scope for this\n> series.\n\nInlining would allow the compiler to unroll the loop for string\nconstants.  I doubt it would do that for variables, as in the code\nbelow.\n\nInlining the strlen()+memcmp() version above might allow the compiler\nto push the strlen() call out of a loop.\n\nWould any of that improve performance noticeably?  For the call sites\nbelow I doubt it.  But it would probably increase the object text size.\n\n> And it's possible I was simply too dumb to figure out xstrncmpz() here.\n> I'm waiting for René to show up and tell me how to do it. ;)\n\nNah, it's not a good fit, as it requires the two strings to have the\nsame length.\n\n>\n> IMHO this is the trickiest commit of the whole series, as it would be\n> easy to get the length computations subtly wrong.\n>\n>  commit.c    |  3 ++-\n>  sequencer.c |  4 ++--\n>  strbuf.c    | 11 +++++++++++\n>  strbuf.h    |  1 +\n>  trailer.c   |  4 ++--\n>  5 files changed, 18 insertions(+), 5 deletions(-)\n>\n> diff --git a/commit.c b/commit.c\n> index ef679a0b93..531a666cba 100644\n> --- a/commit.c\n> +++ b/commit.c\n> @@ -1796,7 +1796,8 @@ size_t ignored_log_message_bytes(const char *buf, size_t len)\n>  \t\telse\n>  \t\t\tnext_line++;\n>\n> -\t\tif (buf[bol] == comment_line_char || buf[bol] == '\\n') {\n> +\t\tif (starts_with_mem(buf + bol, cutoff - bol, comment_line_str) ||\n> +\t\t    buf[bol] == '\\n') {\n>  \t\t\t/* is this the first of the run of comments? */\n>  \t\t\tif (!boc)\n>  \t\t\t\tboc = bol;\n> diff --git a/sequencer.c b/sequencer.c\n> index 991a2dbe96..664986e3b2 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -1840,7 +1840,7 @@ static int is_fixup_flag(enum todo_command command, unsigned flag)\n>  static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)\n>  {\n>  \tconst char *s = str;\n> -\twhile (len > 0 && s[0] == comment_line_char) {\n> +\twhile (starts_with_mem(s, len, comment_line_str)) {\n>  \t\tsize_t count;\n>  \t\tconst char *n = memchr(s, '\\n', len);\n>  \t\tif (!n)\n> @@ -2562,7 +2562,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,\n>  \t/* left-trim */\n>  \tbol += strspn(bol, \" \\t\");\n>\n> -\tif (bol == eol || *bol == '\\r' || *bol == comment_line_char) {\n> +\tif (bol == eol || *bol == '\\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {\n\nIf the strspn() call is safe (which it is, as the caller expects the\nstring to be NUL-terminated) then you could use starts_with() here and\navoid the length calculation.  But that would also match\ncomment_line_str values that contain LF, which the _mem version does not\nand that's better.\n\nNot sure why lines that start with CR are considered comment lines,\nthough.\n\n>  \t\titem->command = TODO_COMMENT;\n>  \t\titem->commit = NULL;\n>  \t\titem->arg_offset = bol - buf;\n> diff --git a/strbuf.c b/strbuf.c\n> index 7c8f582127..291bdc2a65 100644\n> --- a/strbuf.c\n> +++ b/strbuf.c\n> @@ -24,6 +24,17 @@ int istarts_with(const char *str, const char *prefix)\n>  \t\t\treturn 0;\n>  }\n>\n> +int starts_with_mem(const char *str, size_t len, const char *prefix)\n> +{\n> +\tconst char *end = str + len;\n> +\tfor (; ; str++, prefix++) {\n> +\t\tif (!*prefix)\n> +\t\t\treturn 1;\n> +\t\telse if (str == end || *str != *prefix)\n> +\t\t\treturn 0;\n> +\t}\n> +}\n\nSo this checks whether a length-limited string has a prefix given as a\nNUL-terminated string.  I'd have called it mem_starts_with() and have\nexpected starts_with_mem() to check a NUL-terminated string for a\nlength-limited prefix (think !strncmp(str, prefix, prefixlen)).\n\n> +\n>  int skip_to_optional_arg_default(const char *str, const char *prefix,\n>  \t\t\t\t const char **arg, const char *def)\n>  {\n> diff --git a/strbuf.h b/strbuf.h\n> index 58dddf2777..3156d6ea8c 100644\n> --- a/strbuf.h\n> +++ b/strbuf.h\n> @@ -673,6 +673,7 @@ char *xstrfmt(const char *fmt, ...);\n>\n>  int starts_with(const char *str, const char *prefix);\n>  int istarts_with(const char *str, const char *prefix);\n> +int starts_with_mem(const char *str, size_t len, const char *prefix);\n>\n>  /*\n>   * If the string \"str\" is the same as the string in \"prefix\", then the \"arg\"\n> diff --git a/trailer.c b/trailer.c\n> index fe18faf6c5..f59c90b4b5 100644\n> --- a/trailer.c\n> +++ b/trailer.c\n> @@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>\n>  \t/* The first paragraph is the title and cannot be trailers */\n>  \tfor (s = buf; s < buf + len; s = next_line(s)) {\n> -\t\tif (s[0] == comment_line_char)\n> +\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n>  \t\t\tcontinue;\n>  \t\tif (is_blank_line(s))\n\nAnother case where starts_with() would be safe to use, as\nis_blank_line() expects (and gets) a NUL-terminated string, but it would\nallow matching comment_line_str values that contain LF.\n\n>  \t\t\tbreak;\n> @@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>  \t\tconst char **p;\n>  \t\tssize_t separator_pos;\n>\n> -\t\tif (bol[0] == comment_line_char) {\n> +\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n\nWe're in the same buffer, so the above comment applies here as well.\n\n>  \t\t\tnon_trailer_lines += possible_continuation_lines;\n>  \t\t\tpossible_continuation_lines = 0;\n>  \t\t\tcontinue;\n"},{"id":"490205","messageId":"50c7f2b9-2f40-49f8-af13-1521323825cd@web.de","threadId":"61052","inReplyTo":"742ed8e6-d68b-47a0-8d57-731716c5365d@web.de","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2024-03-07T19:47:57Z","receivedAt":"2024-03-07T19:48:15Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 07.03.24 um 20:41 schrieb René Scharfe:\n\nSorry, sent too early.\n\n> Am 07.03.24 um 12:08 schrieb Jeff King:\n>> On Thu, Mar 07, 2024 at 04:26:38AM -0500, Jeff King wrote:\n>>\n>>> IMHO this is the trickiest commit of the whole series, as it would be\n>>> easy to get the length computations subtly wrong.\n>>\n>> And sure enough...\n>>\n>>> diff --git a/trailer.c b/trailer.c\n>>> index fe18faf6c5..f59c90b4b5 100644\n>>> --- a/trailer.c\n>>> +++ b/trailer.c\n>>> @@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>>>\n>>>  \t/* The first paragraph is the title and cannot be trailers */\n>>>  \tfor (s = buf; s < buf + len; s = next_line(s)) {\n>>> -\t\tif (s[0] == comment_line_char)\n>>> +\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n>>>  \t\t\tcontinue;\n>>>  \t\tif (is_blank_line(s))\n>>>  \t\t\tbreak;\n>>> @@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>>>  \t\tconst char **p;\n>>>  \t\tssize_t separator_pos;\n>>>\n>>> -\t\tif (bol[0] == comment_line_char) {\n>>> +\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n>>>  \t\t\tnon_trailer_lines += possible_continuation_lines;\n>>>  \t\t\tpossible_continuation_lines = 0;\n>>>  \t\t\tcontinue;\n>>\n>> This second hunk needs:\n>>\n>> diff --git a/trailer.c b/trailer.c\n>> index f59c90b4b5..fdb0b8137e 100644\n>> --- a/trailer.c\n>> +++ b/trailer.c\n>> @@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>>  \t\tconst char **p;\n>>  \t\tssize_t separator_pos;\n>>\n>> -\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n>> +\t\tif (starts_with_mem(bol, buf + len - bol, comment_line_str)) {\n>>  \t\t\tnon_trailer_lines += possible_continuation_lines;\n>>  \t\t\tpossible_continuation_lines = 0;\n>>  \t\t\tcontinue;\n>>\n>> I was trying to bound the size based on the loop, which is:\n>>\n>>           for (l = last_line(buf, len);\n>>                l >= end_of_title;\n>>                l = last_line(buf, l)) {\n>>                   const char *bol = buf + l;\n>>\n>> but I misread \"end_of_title\" as an upper bound, not a lower one. Which\n>> makes sense because we're iterating backwards over the lines. So I\n>> suppose we could bound it by the previous \"bol\" value. But in practice,\n>> your prefix won't cross such a boundary anyway, as it won't have a\n>> newline in it (maybe that's something we should enforce? I guess you\n>> could set core.commentChar to '\\n' even before my series, which would be\n>> slightly insane).\n>>\n>> So just bounding ourselves to \"buf + len\" seems reasonable, as that\n>> makes sure we don't step outside the buffer passed into the function.\n\nIf you don't want or expect LF in comment_line_str, better check it.\nAnd if you do that, most callers of starts_with_mem() -- including this\none -- can use starts_with() instead, as mentioned in my reply to your\npatch.  Less calculations, less errors..\n\nRené\n\n\n\n"},{"id":"490237","messageId":"d82e11f2-15c1-4ecb-8c7c-19667d2c5fb4@gmail.com","threadId":"61052","inReplyTo":"3f823e48-572c-4e19-ab76-e6d7cab9461f@web.de","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-03-08T10:17:37Z","receivedAt":"2024-03-08T10:17:40Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Peff and René\n\nOn 07/03/2024 19:42, René Scharfe wrote:\n> Am 07.03.24 um 10:26 schrieb Jeff King:\n>> diff --git a/sequencer.c b/sequencer.c\n>> index 991a2dbe96..664986e3b2 100644\n>> --- a/sequencer.c\n>> +++ b/sequencer.c\n>> @@ -1840,7 +1840,7 @@ static int is_fixup_flag(enum todo_command command, unsigned flag)\n>>   static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)\n>>   {\n>>   \tconst char *s = str;\n>> -\twhile (len > 0 && s[0] == comment_line_char) {\n>> +\twhile (starts_with_mem(s, len, comment_line_str)) {\n>>   \t\tsize_t count;\n>>   \t\tconst char *n = memchr(s, '\\n', len);\n>>   \t\tif (!n)\n>> @@ -2562,7 +2562,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,\n>>   \t/* left-trim */\n>>   \tbol += strspn(bol, \" \\t\");\n>>\n>> -\tif (bol == eol || *bol == '\\r' || *bol == comment_line_char) {\n>> +\tif (bol == eol || *bol == '\\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {\n> \n> If the strspn() call is safe (which it is, as the caller expects the\n> string to be NUL-terminated) then you could use starts_with() here and\n> avoid the length calculation.  But that would also match\n> comment_line_str values that contain LF, which the _mem version does not > and that's better.\n\nI agree with your analysis. I do wonder though if we should reject \nwhitespace and control characters when parsing core.commentChar, it \nfeels like accepting them is a bug waiting to happen. If \ncomment_line_char starts with ' ' or '\\t' that part will be eaten by the \nstrspn() above and so starts_with_mem() wont match. Also we will never \nmatch a comment if comment_line_str contains '\\n'.\n\n> Not sure why lines that start with CR are considered comment lines,\n> though.\n\nI think it is a lazy way of looking for an empty line ending in CR LF, \nit should really be\n\n\t|| (bol[0] == '\\r' && bol[1] == '\\n') ||\n\nBest Wishes\n\nPhillip\n"},{"id":"490238","messageId":"43b7b21f-8d11-4c67-96a6-ff2c8f037263@gmail.com","threadId":"61052","inReplyTo":"20240307092747.GL2080210@coredump.intra.peff.net","subject":"Re: [PATCH 12/15] sequencer: handle multi-byte comment characters when writing todo list","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-03-08T10:20:45Z","receivedAt":"2024-03-08T10:20:48Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Peff\n\nOn 07/03/2024 09:27, Jeff King wrote:\n> We already match multi-byte comment characters in parse_insn_line(),\n> thanks to the previous commit, yielding a TODO_COMMENT entry. But in\n> todo_list_to_strbuf(), we may call command_to_char() to convert that\n> back into something we can output.\n> \n> We can't just return comment_line_char anymore, since it may require\n> multiple bytes. Instead, we'll return \"0\" for this case, which is the\n> same thing we'd return for a command which does not have a single-letter\n> abbreviation (e.g., \"revert\" or \"noop\"). In that case the caller then\n> falls back to outputting the full name via command_to_string(). So we\n> can handle TODO_COMMENT there, returning the full string.\n\nIf you do re-roll it might be helpful to emphasize that there is only \none caller.\n\n> Note that there are many other callers of command_to_string(), which\n> will now behave differently if they pass TODO_COMMENT. But we would not\n> expect that to happen; prior to this commit, the function just calls\n> die() in this case. And looking at those callers, that makes sense;\n> e.g., do_pick_commit() will only be called when servicing a pick\n> command, and should never be called for a comment in the first place.\n\nI've checked the other callers and agree with your analysis. The fact \nthat it used to die() also makes it pretty clear that this should be safe.\n\nBest Wishes\n\nPhillip\n\n> Signed-off-by: Jeff King <peff@peff.net>\n> ---\n>   sequencer.c | 4 +++-\n>   1 file changed, 3 insertions(+), 1 deletion(-)\n> \n> diff --git a/sequencer.c b/sequencer.c\n> index 664986e3b2..9e2851428b 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -1779,14 +1779,16 @@ static const char *command_to_string(const enum todo_command command)\n>   {\n>   \tif (command < TODO_COMMENT)\n>   \t\treturn todo_command_info[command].str;\n> +\tif (command == TODO_COMMENT)\n> +\t\treturn comment_line_str;\n>   \tdie(_(\"unknown command: %d\"), command);\n>   }\n>   \n>   static char command_to_char(const enum todo_command command)\n>   {\n>   \tif (command < TODO_COMMENT)\n>   \t\treturn todo_command_info[command].c;\n> -\treturn comment_line_char;\n> +\treturn 0;\n>   }\n>   \n>   static int is_noop(const enum todo_command command)\n"},{"id":"490240","messageId":"b98671d5-e166-4b5b-9cb2-b0c85bca7fd6@gmail.com","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"Re: [PATCH 0/15] allow multi-byte core.commentChar","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-03-08T11:07:48Z","receivedAt":"2024-03-08T11:07:51Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Peff\n\nOn 07/03/2024 09:14, Jeff King wrote:\n> On Wed, Mar 06, 2024 at 03:08:04AM -0500, Jeff King wrote:\n> \n>> For a more readable series, I'd guess it would make sense to introduce\n>> comment_line_str as a separate variable (but continue to enforce the\n>> single-char rule), convert the easy cases en masse, the tricky cases one\n>> by one, and then finally drop comment_line_char entirely. At which point\n>> the config rules can be lifted to allow multi-byte strings.\n> \n> I ended up cleaning this up. Like I said, this isn't something I'm\n> personally that interested in. But it just seemed like a wart that this\n> one spot could not handle multi-byte characters that all the cool kids\n> are using in their prompts etc these days.\n\nI agree it would be nice to support multibyte comment characters on \nprinciple even if I don't think I'd use that feature myself. I've looked \nthrough the changes to the sequencer and they all look sensible to me. \nAs I mentioned when looking at patch 11 I do wonder if we want to reject \nascii whitespace and control characters when parsing core.commentChar. \nAt a minimum leading whitespace and LF anywhere in the comment string \nfeel like they are asking for trouble.\n\nBest Wishes\n\nPhillip\n\n> Plus it was kind of an interesting puzzle for how to lay out the\n> refactoring to make each step self-consistent. At the very least, I\n> think the first couple of cleanups are worth it even if we do not see\n> the whole thing through. ;)\n> \n> It obviously nullifies kh/doc-commentchar-is-a-byte, which is in 'next'.\n> Sadly \"git merge\" does not find a conflict with the documentation update\n> in patch 15, so we'll have to remember to pick up one topic or the\n> other.\n> \n> I'm using U+00BB as my commentChar for now to see if any bugs show up,\n> but I expect I'll get sick of it after a few days.\n> \n>    [01/15]: strbuf: simplify comment-handling in add_lines() helper\n>    [02/15]: strbuf: avoid static variables in strbuf_add_commented_lines()\n>    [03/15]: commit: refactor base-case of adjust_comment_line_char()\n>    [04/15]: strbuf: avoid shadowing global comment_line_char name\n> \n>      These four are cleanups that could be taken independently.\n> \n>    [05/15]: environment: store comment_line_char as a string\n> \n>      This one preps us for incrementally moving code over to the new\n>      system.\n> \n>    [06/15]: strbuf: accept a comment string for strbuf_stripspace()\n>    [07/15]: strbuf: accept a comment string for strbuf_commented_addf()\n>    [08/15]: strbuf: accept a comment string for strbuf_add_commented_lines()\n>    [09/15]: prefer comment_line_str to comment_line_char for printing\n>    [10/15]: find multi-byte comment chars in NUL-terminated strings\n>    [11/15]: find multi-byte comment chars in unterminated buffers\n>    [12/15]: sequencer: handle multi-byte comment characters when writing todo list\n>    [13/15]: wt-status: drop custom comment-char stringification\n> \n>      These ones are the actual transition.\n> \n>    [14/15]: environment: drop comment_line_char compatibility macro\n>    [15/15]: config: allow multi-byte core.commentChar\n> \n>      And then we tie it off by dropping the now-unused bits and loosening\n>      the config logic.\n> \n>   Documentation/config/core.txt |  4 ++-\n>   add-patch.c                   | 14 +++++-----\n>   builtin/branch.c              |  8 +++---\n>   builtin/commit.c              | 19 +++++++-------\n>   builtin/merge.c               | 12 ++++-----\n>   builtin/notes.c               | 10 ++++----\n>   builtin/rebase.c              |  2 +-\n>   builtin/stripspace.c          |  4 +--\n>   builtin/tag.c                 | 14 +++++-----\n>   commit.c                      |  3 ++-\n>   config.c                      |  6 ++---\n>   environment.c                 |  2 +-\n>   environment.h                 |  2 +-\n>   fmt-merge-msg.c               |  8 +++---\n>   rebase-interactive.c          | 10 ++++----\n>   sequencer.c                   | 48 ++++++++++++++++++-----------------\n>   strbuf.c                      | 47 ++++++++++++++++++----------------\n>   strbuf.h                      |  9 ++++---\n>   t/t0030-stripspace.sh         |  5 ++++\n>   t/t7507-commit-verbose.sh     | 10 ++++++++\n>   t/t7508-status.sh             |  4 ++-\n>   trailer.c                     |  6 ++---\n>   wt-status.c                   | 31 +++++++++-------------\n>   23 files changed, 149 insertions(+), 129 deletions(-)\n> \n\n"},{"id":"490255","messageId":"xmqqfrx0hg01.fsf@gitster.g","threadId":"61052","inReplyTo":"d82e11f2-15c1-4ecb-8c7c-19667d2c5fb4@gmail.com","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-08T15:58:54Z","receivedAt":"2024-03-08T15:59:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> I agree with your analysis. I do wonder though if we should reject\n> whitespace and control characters when parsing core.commentChar, it\n> feels like accepting them is a bug waiting to happen. If\n> comment_line_char starts with ' ' or '\\t' that part will be eaten by\n> the strspn() above and so starts_with_mem() wont match. Also we will\n> never match a comment if comment_line_str contains '\\n'.\n\nAnother thing I was wondering is what we want to do a random\nbyte-sequence that may match from the middle of a multi-byte UTF-8\ncharacter.\n\nThe reason I haven't mentioned these \"nonsense input\" is because\nthey will at worst only lead to self-denial-of-service to those who\nare too curious, and will fall into \"don't do it then\" category.\n\nAlso, what exactly is the definition of \"nonsense\" will become can\nof worms.  I can sympathise if somebody wants to use \"#\\t\" to give\nthemselves a bit more room than usual on the left for visibility,\nfor example, so there might be a case to want whitespace characters.\n\n>> Not sure why lines that start with CR are considered comment lines,\n>> though.\n>\n> I think it is a lazy way of looking for an empty line ending in CR LF,\n> it should really be\n>\n> \t|| (bol[0] == '\\r' && bol[1] == '\\n') ||\n\nMy recollection matches your speculation. \n\nIIRC the lazy persono was probably me but I didn't run \"git blame\".\n"},{"id":"490259","messageId":"66a70346-98e8-44dd-a680-36a21df5f32a@gmail.com","threadId":"61052","inReplyTo":"xmqqfrx0hg01.fsf@gitster.g","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-03-08T16:20:12Z","receivedAt":"2024-03-08T16:20:14Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 08/03/2024 15:58, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n>> I agree with your analysis. I do wonder though if we should reject\n>> whitespace and control characters when parsing core.commentChar, it\n>> feels like accepting them is a bug waiting to happen. If\n>> comment_line_char starts with ' ' or '\\t' that part will be eaten by\n>> the strspn() above and so starts_with_mem() wont match. Also we will\n>> never match a comment if comment_line_str contains '\\n'.\n> \n> Another thing I was wondering is what we want to do a random\n> byte-sequence that may match from the middle of a multi-byte UTF-8\n> character.\n> \n> The reason I haven't mentioned these \"nonsense input\" is because\n> they will at worst only lead to self-denial-of-service to those who\n> are too curious, and will fall into \"don't do it then\" category.\n\nWe could certainly leave it as-is and tell users they are only hurting \nthemselves if they complain when it does not work.\n\n> Also, what exactly is the definition of \"nonsense\" will become can\n> of worms.  I can sympathise if somebody wants to use \"#\\t\" to give\n> themselves a bit more room than usual on the left for visibility,\n> for example, so there might be a case to want whitespace characters.\n\nThat's fair, maybe we could just ban leading whitespace if we do decide \nto restrict core.commentChar\n\nBest Wishes\n\nPhillip\n\n>>> Not sure why lines that start with CR are considered comment lines,\n>>> though.\n>>\n>> I think it is a lazy way of looking for an empty line ending in CR LF,\n>> it should really be\n>>\n>> \t|| (bol[0] == '\\r' && bol[1] == '\\n') ||\n> \n> My recollection matches your speculation.\n> \n> IIRC the lazy persono was probably me but I didn't run \"git blame\".\n"},{"id":"490451","messageId":"20240312080525.GB47852@coredump.intra.peff.net","threadId":"61052","inReplyTo":"3f823e48-572c-4e19-ab76-e6d7cab9461f@web.de","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T08:05:25Z","receivedAt":"2024-03-12T08:05:27Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Mar 07, 2024 at 08:42:22PM +0100, René Scharfe wrote:\n\n> > Arguably starts_with() and this new function should both be inlined,\n> > like we do for skip_prefix(), but I think that's out of scope for this\n> > series.\n> \n> Inlining would allow the compiler to unroll the loop for string\n> constants.  I doubt it would do that for variables, as in the code\n> below.\n> \n> Inlining the strlen()+memcmp() version above might allow the compiler\n> to push the strlen() call out of a loop.\n> \n> Would any of that improve performance noticeably?  For the call sites\n> below I doubt it.  But it would probably increase the object text size.\n\nGood point. With non-constant prefixes in these cases, it probably\nwouldn't buy much. There are a lot of other cases with actual string\nconstants. A compiler in theory could turn starts_with(str, \"foo\") into\na few instructions. But it's not even clear that it's in very many hot\npaths. It would definitely be something we'd have to measure.\n\n> > And it's possible I was simply too dumb to figure out xstrncmpz() here.\n> > I'm waiting for René to show up and tell me how to do it. ;)\n> \n> Nah, it's not a good fit, as it requires the two strings to have the\n> same length.\n\nThanks for confirming I wasn't missing anything. :)\n\n> > @@ -2562,7 +2562,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,\n> >  \t/* left-trim */\n> >  \tbol += strspn(bol, \" \\t\");\n> >\n> > -\tif (bol == eol || *bol == '\\r' || *bol == comment_line_char) {\n> > +\tif (bol == eol || *bol == '\\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {\n> \n> If the strspn() call is safe (which it is, as the caller expects the\n> string to be NUL-terminated) then you could use starts_with() here and\n> avoid the length calculation.  But that would also match\n> comment_line_str values that contain LF, which the _mem version does not\n> and that's better.\n\nI try not to read too much into the use of string functions on what\notherwise appears to be an unterminated buffer. While in Git it is quite\noften terminated at allocation time (coming from a strbuf, etc) I feel\nlike I've fixed a number of out-of-bounds reads simply due to sloppy\npractices. And even if something is correct today, it is easy for it to\nchange, since the assumption is made far away from allocation.\n\nSo I dunno. Like you said, fewer computations is fewer opportunity to\nmess things up. I don't like the idea of introducing a new hand-grenade\nthat might blow up later, but maybe if it's right next to a strspn()\ncall that's already a problem, it's not materially making anything\nworse.\n\n> > +int starts_with_mem(const char *str, size_t len, const char *prefix)\n> > +{\n> > +\tconst char *end = str + len;\n> > +\tfor (; ; str++, prefix++) {\n> > +\t\tif (!*prefix)\n> > +\t\t\treturn 1;\n> > +\t\telse if (str == end || *str != *prefix)\n> > +\t\t\treturn 0;\n> > +\t}\n> > +}\n> \n> So this checks whether a length-limited string has a prefix given as a\n> NUL-terminated string.  I'd have called it mem_starts_with() and have\n> expected starts_with_mem() to check a NUL-terminated string for a\n> length-limited prefix (think !strncmp(str, prefix, prefixlen)).\n\nI was going for consistency with skip_prefix_mem() and strip_suffix_mem().\nTo be fair, I probably also named those ones, but I think it's pretty\nestablished. We've never needed the length-limited prefix variant yet,\nso I don't know that we're squatting on anything too valuable.\n\n> > @@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n> >\n> >  \t/* The first paragraph is the title and cannot be trailers */\n> >  \tfor (s = buf; s < buf + len; s = next_line(s)) {\n> > -\t\tif (s[0] == comment_line_char)\n> > +\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n> >  \t\t\tcontinue;\n> >  \t\tif (is_blank_line(s))\n> \n> Another case where starts_with() would be safe to use, as\n> is_blank_line() expects (and gets) a NUL-terminated string, but it would\n> allow matching comment_line_str values that contain LF.\n\nHmm. Yes, it is a NUL-terminated string always, but the caller has told\nus not to look past end_of_log_message(). I suspect that if there is no\nnewline in comment_line_str() it's probably impossible to go past \"len\"\n(just because the end of the log surely ends with either a NUL or a\nnewline). But it feels iffy to me. I dunno.\n\n-Peff\n"},{"id":"490453","messageId":"20240312081931.GC47852@coredump.intra.peff.net","threadId":"61052","inReplyTo":"66a70346-98e8-44dd-a680-36a21df5f32a@gmail.com","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T08:19:31Z","receivedAt":"2024-03-12T08:19:32Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 08, 2024 at 04:20:12PM +0000, Phillip Wood wrote:\n\n> On 08/03/2024 15:58, Junio C Hamano wrote:\n> > Phillip Wood <phillip.wood123@gmail.com> writes:\n> > \n> > > I agree with your analysis. I do wonder though if we should reject\n> > > whitespace and control characters when parsing core.commentChar, it\n> > > feels like accepting them is a bug waiting to happen. If\n> > > comment_line_char starts with ' ' or '\\t' that part will be eaten by\n> > > the strspn() above and so starts_with_mem() wont match. Also we will\n> > > never match a comment if comment_line_str contains '\\n'.\n> > \n> > Another thing I was wondering is what we want to do a random\n> > byte-sequence that may match from the middle of a multi-byte UTF-8\n> > character.\n> > \n> > The reason I haven't mentioned these \"nonsense input\" is because\n> > they will at worst only lead to self-denial-of-service to those who\n> > are too curious, and will fall into \"don't do it then\" category.\n> \n> We could certainly leave it as-is and tell users they are only hurting\n> themselves if they complain when it does not work.\n\nThat was mostly my plan. To some degree I think this is orthogonal to my\nseries. You can already set core.commentChar to space or newline, and\nI'm sure the results are not very good. Actually, I guess it is easy to\ntry:\n\n  git -c core.commentChar=$'\\n' commit --allow-empty\n\ntreats everything as not-a-comment.\n\nMaybe it's worth forbidding this at the start of the series, and then\ncarrying it through. I really do think newline is the most special\ncharacter here, just because it's obviously going to be meaningful to\nall of our line-oriented parsing. So you'll get weird results, as\nopposed to broken multibyte characters, where things would still work if\nyou choose to consistently use them (and arguably we cannot even define\n\"broken\" as the user can use a different encoding).\n\nLikewise, I guess people might complain that their core.commentChar is\nNFD and their editor writes out NFC characters or something, and we\ndon't match. I was hoping we could just punt on that and nobody would\never notice (certainly I think it is OK to punt for now and somebody who\ntruly cares can make a utf8_starts_with() or similar).\n\n> > Also, what exactly is the definition of \"nonsense\" will become can\n> > of worms.  I can sympathise if somebody wants to use \"#\\t\" to give\n> > themselves a bit more room than usual on the left for visibility,\n> > for example, so there might be a case to want whitespace characters.\n> \n> That's fair, maybe we could just ban leading whitespace if we do decide to\n> restrict core.commentChar\n\nLeading whitespace actually does work, though I think you'd be slightly\ninsane to use it.\n\nI'm currently using \"! COMMENT !\" (after using a unicode char for a few\ndays). It's horribly ugly, but I wanted to see if any bugs cropped up\n(and vim's built-in git syntax highlighting colors it correctly ;) ).\n\n-Peff\n"},{"id":"490454","messageId":"20240312082143.GD47852@coredump.intra.peff.net","threadId":"61052","inReplyTo":"43b7b21f-8d11-4c67-96a6-ff2c8f037263@gmail.com","subject":"Re: [PATCH 12/15] sequencer: handle multi-byte comment characters when writing todo list","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T08:21:43Z","receivedAt":"2024-03-12T08:21:44Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 08, 2024 at 10:20:45AM +0000, Phillip Wood wrote:\n\n> Hi Peff\n> \n> On 07/03/2024 09:27, Jeff King wrote:\n> > We already match multi-byte comment characters in parse_insn_line(),\n> > thanks to the previous commit, yielding a TODO_COMMENT entry. But in\n> > todo_list_to_strbuf(), we may call command_to_char() to convert that\n> > back into something we can output.\n> > \n> > We can't just return comment_line_char anymore, since it may require\n> > multiple bytes. Instead, we'll return \"0\" for this case, which is the\n> > same thing we'd return for a command which does not have a single-letter\n> > abbreviation (e.g., \"revert\" or \"noop\"). In that case the caller then\n> > falls back to outputting the full name via command_to_string(). So we\n> > can handle TODO_COMMENT there, returning the full string.\n> \n> If you do re-roll it might be helpful to emphasize that there is only one\n> caller.\n\nThanks, will do.\n\n-Peff\n"},{"id":"490457","messageId":"20240312091013.GA95442@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240307091407.GA2072522@coredump.intra.peff.net","subject":"[PATCH v2 0/16] allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:10:13Z","receivedAt":"2024-03-12T09:10:14Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Here's a revised version of my series. It incorporates the fixups I sent\n(which I think Junio had applied already), and incorporates a new patch\nat the beginning to forbid newlines.\n\nI _didn't_ convert any of the starts_with_mem() call to starts_with().\nI'm on the fence on whether that is simplifying things or creating\npotential confusion/bugs later.\n\nIf we don't like the new patch 1 (or if we prefer to do it on top; there\nis really not much reason to prefer one or the other), then this should\notherwise be the same as what Junio has already queued as\njk/core-comment-char.\n\nRange diff (from v1, without my fixups) is below.\n\n -:  ---------- >  1:  86efec435d config: forbid newline as core.commentChar\n 1:  be18aa04e3 =  2:  7c016e5dc3 strbuf: simplify comment-handling in add_lines() helper\n 2:  0f8ea2a86d =  3:  2b4170b5f0 strbuf: avoid static variables in strbuf_add_commented_lines()\n 3:  9b56d9f4f0 =  4:  24ca214986 commit: refactor base-case of adjust_comment_line_char()\n 4:  0a191e5588 =  5:  9f6433dbe6 strbuf: avoid shadowing global comment_line_char name\n 5:  f41e196138 !  6:  d0f32f10f9 environment: store comment_line_char as a string\n    @@ builtin/commit.c: static void adjust_comment_line_char(const struct strbuf *sb)\n     \n      ## config.c ##\n     @@ config.c: static int git_default_core_config(const char *var, const char *value,\n    - \t\telse if (!strcasecmp(value, \"auto\"))\n    - \t\t\tauto_comment_line_char = 1;\n      \t\telse if (value[0] && !value[1]) {\n    + \t\t\tif (value[0] == '\\n')\n    + \t\t\t\treturn error(_(\"core.commentChar cannot be newline\"));\n     -\t\t\tcomment_line_char = value[0];\n     +\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n      \t\t\tauto_comment_line_char = 0;\n 6:  84261af2ed !  7:  2c91628564 strbuf: accept a comment string for strbuf_stripspace()\n    @@ Commit message\n     \n         Signed-off-by: Jeff King <peff@peff.net>\n     \n    + ## builtin/am.c ##\n    +@@ builtin/am.c: static int parse_mail(struct am_state *state, const char *mail)\n    + \n    + \tstrbuf_addstr(&msg, \"\\n\\n\");\n    + \tstrbuf_addbuf(&msg, &mi.log_message);\n    +-\tstrbuf_stripspace(&msg, '\\0');\n    ++\tstrbuf_stripspace(&msg, NULL);\n    + \n    + \tassert(!state->author_name);\n    + \tstate->author_name = strbuf_detach(&author_name, NULL);\n    +\n      ## builtin/branch.c ##\n     @@ builtin/branch.c: static int edit_branch_description(const char *branch_name)\n      \t\tstrbuf_release(&buf);\n    @@ builtin/branch.c: static int edit_branch_description(const char *branch_name)\n      \tstrbuf_addf(&name, \"branch.%s.description\", branch_name);\n      \tif (buf.len || exists)\n     \n    + ## builtin/commit.c ##\n    +@@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,\n    + \ts->hints = 0;\n    + \n    + \tif (clean_message_contents)\n    +-\t\tstrbuf_stripspace(&sb, '\\0');\n    ++\t\tstrbuf_stripspace(&sb, NULL);\n    + \n    + \tif (signoff)\n    + \t\tappend_signoff(&sb, ignored_log_message_bytes(sb.buf, sb.len), 0);\n    +\n      ## builtin/notes.c ##\n     @@ builtin/notes.c: static void prepare_note_data(const struct object_id *object, struct note_data *\n      \t\t\tdie(_(\"please supply the note contents using either -m or -F option\"));\n    @@ builtin/notes.c: static void prepare_note_data(const struct object_id *object, s\n      \t}\n      }\n      \n    +@@ builtin/notes.c: static void concat_messages(struct note_data *d)\n    + \t\tif ((d->stripspace == UNSPECIFIED &&\n    + \t\t     d->messages[i]->stripspace == STRIPSPACE) ||\n    + \t\t    d->stripspace == STRIPSPACE)\n    +-\t\t\tstrbuf_stripspace(&d->buf, 0);\n    ++\t\t\tstrbuf_stripspace(&d->buf, NULL);\n    + \t\tstrbuf_reset(&msg);\n    + \t}\n    + \tstrbuf_release(&msg);\n     \n      ## builtin/rebase.c ##\n     @@ builtin/rebase.c: static int edit_todo_file(unsigned flags)\n    @@ builtin/tag.c: static void create_tag(const struct object_id *object, const char\n      \tif (!opt->message_given && !buf->len)\n      \t\tdie(_(\"no tag message?\"));\n     \n    + ## builtin/worktree.c ##\n    +@@ builtin/worktree.c: static int can_use_local_refs(const struct add_opts *opts)\n    + \t\t\tstrbuf_add_real_path(&path, get_worktree_git_dir(NULL));\n    + \t\t\tstrbuf_addstr(&path, \"/HEAD\");\n    + \t\t\tstrbuf_read_file(&contents, path.buf, 64);\n    +-\t\t\tstrbuf_stripspace(&contents, 0);\n    ++\t\t\tstrbuf_stripspace(&contents, NULL);\n    + \t\t\tstrbuf_strip_suffix(&contents, \"\\n\");\n    + \n    + \t\t\twarning(_(\"HEAD points to an invalid (or orphaned) reference.\\n\"\n    +\n    + ## gpg-interface.c ##\n    +@@ gpg-interface.c: static int verify_ssh_signed_buffer(struct signature_check *sigc,\n    + \t\t}\n    + \t}\n    + \n    +-\tstrbuf_stripspace(&ssh_keygen_out, '\\0');\n    +-\tstrbuf_stripspace(&ssh_keygen_err, '\\0');\n    ++\tstrbuf_stripspace(&ssh_keygen_out, NULL);\n    ++\tstrbuf_stripspace(&ssh_keygen_err, NULL);\n    + \t/* Add stderr outputs to show the user actual ssh-keygen errors */\n    + \tstrbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);\n    + \tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n    +\n      ## rebase-interactive.c ##\n     @@ rebase-interactive.c: int edit_todo_list(struct repository *r, struct todo_list *todo_list,\n      \tif (launch_sequence_editor(todo_file, &new_todo->buf, NULL))\n 7:  bb22f9c9c5 =  8:  a271207e48 strbuf: accept a comment string for strbuf_commented_addf()\n 8:  8d20688e87 =  9:  c1831453d8 strbuf: accept a comment string for strbuf_add_commented_lines()\n 9:  4b22efb941 = 10:  523eb9e534 prefer comment_line_str to comment_line_char for printing\n10:  cd03310902 = 11:  85428eadaa find multi-byte comment chars in NUL-terminated strings\n11:  13a346480e ! 12:  b9e2e2302d find multi-byte comment chars in unterminated buffers\n    @@ trailer.c: static size_t find_trailer_block_start(const char *buf, size_t len)\n      \t\tssize_t separator_pos;\n      \n     -\t\tif (bol[0] == comment_line_char) {\n    -+\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n    ++\t\tif (starts_with_mem(bol, buf + len - bol, comment_line_str)) {\n      \t\t\tnon_trailer_lines += possible_continuation_lines;\n      \t\t\tpossible_continuation_lines = 0;\n      \t\t\tcontinue;\n12:  fb3c6659fc ! 13:  7661ca6306 sequencer: handle multi-byte comment characters when writing todo list\n    @@ Commit message\n         We can't just return comment_line_char anymore, since it may require\n         multiple bytes. Instead, we'll return \"0\" for this case, which is the\n         same thing we'd return for a command which does not have a single-letter\n    -    abbreviation (e.g., \"revert\" or \"noop\"). In that case the caller then\n    -    falls back to outputting the full name via command_to_string(). So we\n    -    can handle TODO_COMMENT there, returning the full string.\n    +    abbreviation (e.g., \"revert\" or \"noop\"). There is only a single caller\n    +    of command_to_char(), and upon seeing \"0\" it falls back to outputting\n    +    the full name via command_to_string(). So we can handle TODO_COMMENT\n    +    there, returning the full string.\n     \n         Note that there are many other callers of command_to_string(), which\n         will now behave differently if they pass TODO_COMMENT. But we would not\n13:  94524b8817 = 14:  8ddab67432 wt-status: drop custom comment-char stringification\n14:  d754e86f7b = 15:  16d65f9179 environment: drop comment_line_char compatibility macro\n15:  a6ffe08469 ! 16:  461cc720a0 config: allow multi-byte core.commentChar\n    @@ Commit message\n         how each site behaves. In the interim let's forbid it and we can loosen\n         things later.\n     \n    +    Likewise, the \"commentChar cannot be a newline\" rule is now extended to\n    +    \"it cannot contain a newline\" (for the same reason: it can confuse our\n    +    parsing loops).\n    +\n         Since comment_line_str is used in many parts of the code, it's hard to\n         cover all possibilities with tests. We can convert the existing\n         double-semicolon prefix test to show that \"git status\" works. And we'll\n    @@ config.c: static int git_default_core_config(const char *var, const char *value,\n      \t\telse if (!strcasecmp(value, \"auto\"))\n      \t\t\tauto_comment_line_char = 1;\n     -\t\telse if (value[0] && !value[1]) {\n    +-\t\t\tif (value[0] == '\\n')\n    +-\t\t\t\treturn error(_(\"core.commentChar cannot be newline\"));\n     -\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n     +\t\telse if (value[0]) {\n    ++\t\t\tif (strchr(value, '\\n'))\n    ++\t\t\t\treturn error(_(\"core.commentChar cannot contain newline\"));\n     +\t\t\tcomment_line_str = xstrdup(value);\n      \t\t\tauto_comment_line_char = 0;\n      \t\t} else\n    @@ config.c: static int git_default_core_config(const char *var, const char *value,\n     \n      ## t/t0030-stripspace.sh ##\n     @@ t/t0030-stripspace.sh: test_expect_success 'strip comments with changed comment char' '\n    - \ttest -z \"$(echo \"; comment\" | git -c core.commentchar=\";\" stripspace -s)\"\n    - '\n      \n    + test_expect_success 'newline as commentchar is forbidden' '\n    + \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n    +-\tgrep \"core.commentChar cannot be newline\" err\n    ++\tgrep \"core.commentChar cannot contain newline\" err\n    ++'\n    ++\n     +test_expect_success 'empty commentchar is forbidden' '\n     +\ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n     +\tgrep \"core.commentChar must have at least one character\" err\n    -+'\n    -+\n    + '\n    + \n      test_expect_success '-c with single line' '\n    - \tprintf \"# foo\\n\" >expect &&\n    - \tprintf \"foo\" | git stripspace -c >actual &&\n     \n      ## t/t7507-commit-verbose.sh ##\n     @@ t/t7507-commit-verbose.sh: test_expect_success 'verbose diff is stripped out with set core.commentChar' '\n\n  [01/16]: config: forbid newline as core.commentChar\n  [02/16]: strbuf: simplify comment-handling in add_lines() helper\n  [03/16]: strbuf: avoid static variables in strbuf_add_commented_lines()\n  [04/16]: commit: refactor base-case of adjust_comment_line_char()\n  [05/16]: strbuf: avoid shadowing global comment_line_char name\n  [06/16]: environment: store comment_line_char as a string\n  [07/16]: strbuf: accept a comment string for strbuf_stripspace()\n  [08/16]: strbuf: accept a comment string for strbuf_commented_addf()\n  [09/16]: strbuf: accept a comment string for strbuf_add_commented_lines()\n  [10/16]: prefer comment_line_str to comment_line_char for printing\n  [11/16]: find multi-byte comment chars in NUL-terminated strings\n  [12/16]: find multi-byte comment chars in unterminated buffers\n  [13/16]: sequencer: handle multi-byte comment characters when writing todo list\n  [14/16]: wt-status: drop custom comment-char stringification\n  [15/16]: environment: drop comment_line_char compatibility macro\n  [16/16]: config: allow multi-byte core.commentChar\n\n Documentation/config/core.txt |  4 ++-\n add-patch.c                   | 14 +++++-----\n builtin/am.c                  |  2 +-\n builtin/branch.c              |  8 +++---\n builtin/commit.c              | 21 +++++++--------\n builtin/merge.c               | 12 ++++-----\n builtin/notes.c               | 12 ++++-----\n builtin/rebase.c              |  2 +-\n builtin/stripspace.c          |  4 +--\n builtin/tag.c                 | 14 +++++-----\n builtin/worktree.c            |  2 +-\n commit.c                      |  3 ++-\n config.c                      |  8 +++---\n environment.c                 |  2 +-\n environment.h                 |  2 +-\n fmt-merge-msg.c               |  8 +++---\n gpg-interface.c               |  4 +--\n rebase-interactive.c          | 10 ++++----\n sequencer.c                   | 48 ++++++++++++++++++-----------------\n strbuf.c                      | 47 ++++++++++++++++++----------------\n strbuf.h                      |  9 ++++---\n t/t0030-stripspace.sh         | 10 ++++++++\n t/t7507-commit-verbose.sh     | 10 ++++++++\n t/t7508-status.sh             |  4 ++-\n trailer.c                     |  6 ++---\n wt-status.c                   | 31 +++++++++-------------\n 26 files changed, 162 insertions(+), 135 deletions(-)\n\n-Peff\n"},{"id":"490458","messageId":"20240312091706.GA95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 01/16] config: forbid newline as core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:06Z","receivedAt":"2024-03-12T09:17:07Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Since we usually look for a comment char while parsing line-oriented\nfiles, setting core.commentChar to a single newline can confuse our code\nquite a bit. For example, using it with \"git commit\" causes us to fail\nto recognize any of the template as comments, including it in the config\nmessage. Which kind of makes sense, since the template content is on its\nown line (so no line can \"start\" with a newline). In other spots I would\nnot be surprised if you can create more mischief (e.g., violating loop\nassumptions) but I didn't dig into it.\n\nSince comment characters are a local preference, to some degree this is\na case of \"if it hurts, don't do it\". But given that this would be a\nsilly and pointless thing to do, and that it makes it harder to reason\nabout code parsing comment lines, let's just forbid it.\n\nThere are other cases that are perhaps questionable (e.g., setting the\ncomment char to a single space), but they seem to behave reasonably (at\nleast a simple \"git commit\" will correctly identify and strip the\ntemplate lines). So I haven't worried about going on a hunt for every\nstupid thing a user might do to themselves, and just focused on the most\nconfusing case.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nIn the string version I suppose you could set it to \"\\nexec rm -rf /\" if\nyou really wanted to treat yourself to a fun \"git rebase\". Again, this\nis all local, but it's perhaps nice to know that core.commentChar is not\na vector for arbitrary code execution.\n\n(That of course made me wonder if setting it to just \"exec rm -rf / \"\nwould work, as the rest of the template line would be ignored by \"rm\";\nbut that is self-defeating as we'd recognize the line as a comment and\nremove it).\n\n config.c              | 2 ++\n t/t0030-stripspace.sh | 5 +++++\n 2 files changed, 7 insertions(+)\n\ndiff --git a/config.c b/config.c\nindex 3cfeb3d8bd..f561631374 100644\n--- a/config.c\n+++ b/config.c\n@@ -1566,6 +1566,8 @@ static int git_default_core_config(const char *var, const char *value,\n \t\telse if (!strcasecmp(value, \"auto\"))\n \t\t\tauto_comment_line_char = 1;\n \t\telse if (value[0] && !value[1]) {\n+\t\t\tif (value[0] == '\\n')\n+\t\t\t\treturn error(_(\"core.commentChar cannot be newline\"));\n \t\t\tcomment_line_char = value[0];\n \t\t\tauto_comment_line_char = 0;\n \t\t} else\ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex d1b3be8725..e399dd9189 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -401,6 +401,11 @@ test_expect_success 'strip comments with changed comment char' '\n \ttest -z \"$(echo \"; comment\" | git -c core.commentchar=\";\" stripspace -s)\"\n '\n \n+test_expect_success 'newline as commentchar is forbidden' '\n+\ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n+\tgrep \"core.commentChar cannot be newline\" err\n+'\n+\n test_expect_success '-c with single line' '\n \tprintf \"# foo\\n\" >expect &&\n \tprintf \"foo\" | git stripspace -c >actual &&\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490459","messageId":"20240312091710.GB95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 02/16] strbuf: simplify comment-handling in add_lines() helper","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:10Z","receivedAt":"2024-03-12T09:17:11Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"In strbuf_add_commented_lines(), we prepare two strings with potential\nprefixes: one with just the comment char, and one with an additional\nspace. In the add_lines() helper, we use the one without the extra space\nfor blank lines or lines starting with a tab.\n\nWhile passing in two separate prefixes to the helper is very flexible,\nit's more flexibility than we actually use (or are likely to use, since\nthe rules inside add_lines() only make sense if \"prefix2\" is a variant\nof \"prefix1\" without the extra space). And setting up the two strings\nmakes refactoring in strbuf_add_commented_lines() awkward.\n\nInstead, let's pass in a single string, and just let add_lines() add the\nextra space to the result as appropriate.\n\nWe do still need to pass in a flag to trigger this behavior. The helper\nis shared by strbuf_add_lines(), which passes in a NULL \"prefix2\" to\ninhibit this extra handling.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n strbuf.c | 24 ++++++++++--------------\n 1 file changed, 10 insertions(+), 14 deletions(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex 7827178d8e..689d8acd5e 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -340,18 +340,17 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...)\n }\n \n static void add_lines(struct strbuf *out,\n-\t\t\tconst char *prefix1,\n-\t\t\tconst char *prefix2,\n-\t\t\tconst char *buf, size_t size)\n+\t\t\tconst char *prefix,\n+\t\t\tconst char *buf, size_t size,\n+\t\t\tint space_after_prefix)\n {\n \twhile (size) {\n-\t\tconst char *prefix;\n \t\tconst char *next = memchr(buf, '\\n', size);\n \t\tnext = next ? (next + 1) : (buf + size);\n \n-\t\tprefix = ((prefix2 && (buf[0] == '\\n' || buf[0] == '\\t'))\n-\t\t\t  ? prefix2 : prefix1);\n \t\tstrbuf_addstr(out, prefix);\n+\t\tif (space_after_prefix && buf[0] != '\\n' && buf[0] != '\\t')\n+\t\t\tstrbuf_addch(out, ' ');\n \t\tstrbuf_add(out, buf, next - buf);\n \t\tsize -= next - buf;\n \t\tbuf = next;\n@@ -362,14 +361,11 @@ static void add_lines(struct strbuf *out,\n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n \t\t\t\tsize_t size, char comment_line_char)\n {\n-\tstatic char prefix1[3];\n-\tstatic char prefix2[2];\n+\tstatic char prefix[2];\n \n-\tif (prefix1[0] != comment_line_char) {\n-\t\txsnprintf(prefix1, sizeof(prefix1), \"%c \", comment_line_char);\n-\t\txsnprintf(prefix2, sizeof(prefix2), \"%c\", comment_line_char);\n-\t}\n-\tadd_lines(out, prefix1, prefix2, buf, size);\n+\tif (prefix[0] != comment_line_char)\n+\t\txsnprintf(prefix, sizeof(prefix), \"%c\", comment_line_char);\n+\tadd_lines(out, prefix, buf, size, 1);\n }\n \n void strbuf_commented_addf(struct strbuf *sb, char comment_line_char,\n@@ -750,7 +746,7 @@ ssize_t strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)\n void strbuf_add_lines(struct strbuf *out, const char *prefix,\n \t\t      const char *buf, size_t size)\n {\n-\tadd_lines(out, prefix, NULL, buf, size);\n+\tadd_lines(out, prefix, buf, size, 0);\n }\n \n void strbuf_addstr_xml_quoted(struct strbuf *buf, const char *s)\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490460","messageId":"20240312091715.GC95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 03/16] strbuf: avoid static variables in strbuf_add_commented_lines()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:15Z","receivedAt":"2024-03-12T09:17:16Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"In strbuf_add_commented_lines(), we have to convert the single-byte\ncomment_line_char into a string to pass to add_lines(). We cache the\ncreated string using a static-local variable. But this makes the\nfunction non-reentrant, and it's doubtful that this provides any real\nperformance benefit given that we know the string always contains a\nsingle character.\n\nSo let's just create it from scratch each time, and to give the compiler\nthe maximal opportunity to make it fast we'll ditch the over-complicated\nxsnprintf() and just assign directly into the array.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n strbuf.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex 689d8acd5e..ca80a2c77e 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -361,10 +361,10 @@ static void add_lines(struct strbuf *out,\n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n \t\t\t\tsize_t size, char comment_line_char)\n {\n-\tstatic char prefix[2];\n+\tchar prefix[2];\n \n-\tif (prefix[0] != comment_line_char)\n-\t\txsnprintf(prefix, sizeof(prefix), \"%c\", comment_line_char);\n+\tprefix[0] = comment_line_char;\n+\tprefix[1] = '\\0';\n \tadd_lines(out, prefix, buf, size, 1);\n }\n \n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490461","messageId":"20240312091719.GD95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 04/16] commit: refactor base-case of adjust_comment_line_char()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:19Z","receivedAt":"2024-03-12T09:17:20Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"When core.commentChar is set to \"auto\", we check a set of candidate\ncharacters against the proposed buffer to see which if any can be used\nwithout ambiguity. But before we do that, we optimize for the common\ncase that the default \"#\" is fine by just seeing if it is present in the\nbuffer at all.\n\nThe way we do this is a bit subtle, though: we assign the candidate\ncharacter to comment_line_char preemptively, then check if it works, and\nreturn if it does. The subtle part is that sometimes setting\ncomment_line_char is important (after we return, the important outcome\nis the fact that we have set the variable) and sometimes it is useless\n(if our optimization fails, we go on to do the more careful checks and\neventually assign something else instead).\n\nTo make it more clear what is happening (and to make further refactoring\nof comment_line_char easier), let's check our candidate character\ndirectly, and then assign as part of returning if it worked out.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/commit.c | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex a91197245f..b2d05c0cc9 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -685,9 +685,10 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n \tchar *candidate;\n \tconst char *p;\n \n-\tcomment_line_char = candidates[0];\n-\tif (!memchr(sb->buf, comment_line_char, sb->len))\n+\tif (!memchr(sb->buf, candidates[0], sb->len)) {\n+\t\tcomment_line_char = candidates[0];\n \t\treturn;\n+\t}\n \n \tp = sb->buf;\n \tcandidate = strchr(candidates, *p);\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490462","messageId":"20240312091722.GE95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 05/16] strbuf: avoid shadowing global comment_line_char name","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:22Z","receivedAt":"2024-03-12T09:17:23Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Several comment-related strbuf functions take a comment_line_char\nparameter. There's also a global comment_line_char variable, which is\nclosely related (most callers pass it in as this parameter). Let's avoid\nshadowing the global name. This makes it more obvious that we're not\nusing the global value, and it will be especially helpful as we refactor\nthe global in future patches (in particular, any macro trickery wouldn't\nwork because the preprocessor doesn't respect scope).\n\nWe'll use \"comment_prefix\". That should be descriptive enough, and as a\nbonus is more neutral with respect to the \"char\" type (since we'll\neventually swap it out for a string).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n strbuf.c | 16 ++++++++--------\n strbuf.h |  8 ++++----\n 2 files changed, 12 insertions(+), 12 deletions(-)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex ca80a2c77e..a33aed6c07 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -359,16 +359,16 @@ static void add_lines(struct strbuf *out,\n }\n \n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n-\t\t\t\tsize_t size, char comment_line_char)\n+\t\t\t\tsize_t size, char comment_prefix)\n {\n \tchar prefix[2];\n \n-\tprefix[0] = comment_line_char;\n+\tprefix[0] = comment_prefix;\n \tprefix[1] = '\\0';\n \tadd_lines(out, prefix, buf, size, 1);\n }\n \n-void strbuf_commented_addf(struct strbuf *sb, char comment_line_char,\n+void strbuf_commented_addf(struct strbuf *sb, char comment_prefix,\n \t\t\t   const char *fmt, ...)\n {\n \tva_list params;\n@@ -379,7 +379,7 @@ void strbuf_commented_addf(struct strbuf *sb, char comment_line_char,\n \tstrbuf_vaddf(&buf, fmt, params);\n \tva_end(params);\n \n-\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_line_char);\n+\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix);\n \tif (incomplete_line)\n \t\tsb->buf[--sb->len] = '\\0';\n \n@@ -1001,10 +1001,10 @@ static size_t cleanup(char *line, size_t len)\n  *\n  * If last line does not have a newline at the end, one is added.\n  *\n- * Pass a non-NUL comment_line_char to skip every line starting\n+ * Pass a non-NUL comment_prefix to skip every line starting\n  * with it.\n  */\n-void strbuf_stripspace(struct strbuf *sb, char comment_line_char)\n+void strbuf_stripspace(struct strbuf *sb, char comment_prefix)\n {\n \tsize_t empties = 0;\n \tsize_t i, j, len, newlen;\n@@ -1017,8 +1017,8 @@ void strbuf_stripspace(struct strbuf *sb, char comment_line_char)\n \t\teol = memchr(sb->buf + i, '\\n', sb->len - i);\n \t\tlen = eol ? eol - (sb->buf + i) + 1 : sb->len - i;\n \n-\t\tif (comment_line_char && len &&\n-\t\t    sb->buf[i] == comment_line_char) {\n+\t\tif (comment_prefix && len &&\n+\t\t    sb->buf[i] == comment_prefix) {\n \t\t\tnewlen = 0;\n \t\t\tcontinue;\n \t\t}\ndiff --git a/strbuf.h b/strbuf.h\nindex e959caca87..860fcec5fb 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -288,7 +288,7 @@ void strbuf_splice(struct strbuf *sb, size_t pos, size_t len,\n  */\n void strbuf_add_commented_lines(struct strbuf *out,\n \t\t\t\tconst char *buf, size_t size,\n-\t\t\t\tchar comment_line_char);\n+\t\t\t\tchar comment_prefix);\n \n \n /**\n@@ -379,7 +379,7 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...);\n  * blank to the buffer.\n  */\n __attribute__((format (printf, 3, 4)))\n-void strbuf_commented_addf(struct strbuf *sb, char comment_line_char, const char *fmt, ...);\n+void strbuf_commented_addf(struct strbuf *sb, char comment_prefix, const char *fmt, ...);\n \n __attribute__((format (printf,2,0)))\n void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap);\n@@ -513,11 +513,11 @@ int strbuf_getcwd(struct strbuf *sb);\n int strbuf_normalize_path(struct strbuf *sb);\n \n /**\n- * Strip whitespace from a buffer. If comment_line_char is non-NUL,\n+ * Strip whitespace from a buffer. If comment_prefix is non-NUL,\n  * then lines beginning with that character are considered comments,\n  * thus removed.\n  */\n-void strbuf_stripspace(struct strbuf *buf, char comment_line_char);\n+void strbuf_stripspace(struct strbuf *buf, char comment_prefix);\n \n static inline int strbuf_strip_suffix(struct strbuf *sb, const char *suffix)\n {\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490463","messageId":"20240312091724.GF95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 06/16] environment: store comment_line_char as a string","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:24Z","receivedAt":"2024-03-12T09:17:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"We'd like to eventually support multi-byte comment prefixes, but the\ncomment_line_char variable is referenced in many spots, making the\ntransition difficult.\n\nLet's start by storing the character in a NUL-terminated string. That\nwill let us switch code over incrementally to the string format, and we\ncan easily support the existing code with a macro wrapper (since we'll\ncontinue to allow only a single-byte prefix, this will behave\nidentically).\n\nOnce all references to the \"char\" variable have been converted, we can\ndrop it and enable longer strings.\n\nWe'll still have to touch all of the spots that create or set the\nvariable in this patch, but there are only a few (reading the config,\nand the \"auto\" character selector).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/commit.c | 4 ++--\n config.c         | 2 +-\n environment.c    | 2 +-\n environment.h    | 3 ++-\n 4 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex b2d05c0cc9..82229c3100 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -686,7 +686,7 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n \tconst char *p;\n \n \tif (!memchr(sb->buf, candidates[0], sb->len)) {\n-\t\tcomment_line_char = candidates[0];\n+\t\tcomment_line_str = xstrfmt(\"%c\", candidates[0]);\n \t\treturn;\n \t}\n \n@@ -707,7 +707,7 @@ static void adjust_comment_line_char(const struct strbuf *sb)\n \tif (!*p)\n \t\tdie(_(\"unable to select a comment character that is not used\\n\"\n \t\t      \"in the current commit message\"));\n-\tcomment_line_char = *p;\n+\tcomment_line_str = xstrfmt(\"%c\", *p);\n }\n \n static void prepare_amend_commit(struct commit *commit, struct strbuf *sb,\ndiff --git a/config.c b/config.c\nindex f561631374..7e5dbca4bd 100644\n--- a/config.c\n+++ b/config.c\n@@ -1568,7 +1568,7 @@ static int git_default_core_config(const char *var, const char *value,\n \t\telse if (value[0] && !value[1]) {\n \t\t\tif (value[0] == '\\n')\n \t\t\t\treturn error(_(\"core.commentChar cannot be newline\"));\n-\t\t\tcomment_line_char = value[0];\n+\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n \t\t\tauto_comment_line_char = 0;\n \t\t} else\n \t\t\treturn error(_(\"core.commentChar should only be one ASCII character\"));\ndiff --git a/environment.c b/environment.c\nindex 60706ea398..a73ba9c12c 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -110,7 +110,7 @@ int protect_ntfs = PROTECT_NTFS_DEFAULT;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-char comment_line_char = '#';\n+const char *comment_line_str = \"#\";\n int auto_comment_line_char;\n \n /* Parallel index stat data preload? */\ndiff --git a/environment.h b/environment.h\nindex 5cec19cecc..1c7d0c2f74 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -8,7 +8,8 @@ struct strvec;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-extern char comment_line_char;\n+#define comment_line_char (comment_line_str[0])\n+extern const char *comment_line_str;\n extern int auto_comment_line_char;\n \n /*\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490464","messageId":"20240312091727.GG95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 07/16] strbuf: accept a comment string for strbuf_stripspace()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:27Z","receivedAt":"2024-03-12T09:17:28Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, let's take a\nNUL-terminated string pointer for strbuf_stripspace(), rather than a\nsingle character. We can continue to support its feature of ignoring\ncomments by accepting a NULL pointer (as opposed to the current behavior\nof a NUL byte).\n\nAll of the callers have to be adjusted, but they can all just pass\ncomment_line_str (or NULL).\n\nInside the function we detect comments by comparing the first byte of a\nline to the comment character. We'll adjust that to use starts_with(),\nwhich will match multiple bytes (though for now, of course, we still\nonly allow a single byte, so it's academic).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/am.c         | 2 +-\n builtin/branch.c     | 2 +-\n builtin/commit.c     | 2 +-\n builtin/notes.c      | 4 ++--\n builtin/rebase.c     | 2 +-\n builtin/stripspace.c | 2 +-\n builtin/tag.c        | 2 +-\n builtin/worktree.c   | 2 +-\n gpg-interface.c      | 4 ++--\n rebase-interactive.c | 2 +-\n sequencer.c          | 6 +++---\n strbuf.c             | 6 +++---\n strbuf.h             | 4 ++--\n 13 files changed, 20 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex d1990d7edc..5bc72d7822 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -1286,7 +1286,7 @@ static int parse_mail(struct am_state *state, const char *mail)\n \n \tstrbuf_addstr(&msg, \"\\n\\n\");\n \tstrbuf_addbuf(&msg, &mi.log_message);\n-\tstrbuf_stripspace(&msg, '\\0');\n+\tstrbuf_stripspace(&msg, NULL);\n \n \tassert(!state->author_name);\n \tstate->author_name = strbuf_detach(&author_name, NULL);\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex b3cbb7fd44..f6091f3438 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -684,7 +684,7 @@ static int edit_branch_description(const char *branch_name)\n \t\tstrbuf_release(&buf);\n \t\treturn -1;\n \t}\n-\tstrbuf_stripspace(&buf, comment_line_char);\n+\tstrbuf_stripspace(&buf, comment_line_str);\n \n \tstrbuf_addf(&name, \"branch.%s.description\", branch_name);\n \tif (buf.len || exists)\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 82229c3100..9b139fc795 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -890,7 +890,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \ts->hints = 0;\n \n \tif (clean_message_contents)\n-\t\tstrbuf_stripspace(&sb, '\\0');\n+\t\tstrbuf_stripspace(&sb, NULL);\n \n \tif (signoff)\n \t\tappend_signoff(&sb, ignored_log_message_bytes(sb.buf, sb.len), 0);\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex caf20fd5bd..ae981085ea 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -223,7 +223,7 @@ static void prepare_note_data(const struct object_id *object, struct note_data *\n \t\t\tdie(_(\"please supply the note contents using either -m or -F option\"));\n \t\t}\n \t\tif (d->stripspace)\n-\t\t\tstrbuf_stripspace(&d->buf, comment_line_char);\n+\t\t\tstrbuf_stripspace(&d->buf, comment_line_str);\n \t}\n }\n \n@@ -264,7 +264,7 @@ static void concat_messages(struct note_data *d)\n \t\tif ((d->stripspace == UNSPECIFIED &&\n \t\t     d->messages[i]->stripspace == STRIPSPACE) ||\n \t\t    d->stripspace == STRIPSPACE)\n-\t\t\tstrbuf_stripspace(&d->buf, 0);\n+\t\t\tstrbuf_stripspace(&d->buf, NULL);\n \t\tstrbuf_reset(&msg);\n \t}\n \tstrbuf_release(&msg);\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex be787690bd..dc17c4727f 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -204,7 +204,7 @@ static int edit_todo_file(unsigned flags)\n \tif (strbuf_read_file(&todo_list.buf, todo_file, 0) < 0)\n \t\treturn error_errno(_(\"could not read '%s'.\"), todo_file);\n \n-\tstrbuf_stripspace(&todo_list.buf, comment_line_char);\n+\tstrbuf_stripspace(&todo_list.buf, comment_line_str);\n \tres = edit_todo_list(the_repository, &todo_list, &new_todo, NULL, NULL, flags);\n \tif (!res && todo_list_write_to_file(the_repository, &new_todo, todo_file,\n \t\t\t\t\t    NULL, NULL, -1, flags & ~(TODO_LIST_SHORTEN_IDS)))\ndiff --git a/builtin/stripspace.c b/builtin/stripspace.c\nindex 7b700a9fb1..434ac490cb 100644\n--- a/builtin/stripspace.c\n+++ b/builtin/stripspace.c\n@@ -59,7 +59,7 @@ int cmd_stripspace(int argc, const char **argv, const char *prefix)\n \n \tif (mode == STRIP_DEFAULT || mode == STRIP_COMMENTS)\n \t\tstrbuf_stripspace(&buf,\n-\t\t\t  mode == STRIP_COMMENTS ? comment_line_char : '\\0');\n+\t\t\t  mode == STRIP_COMMENTS ? comment_line_str : NULL);\n \telse\n \t\tcomment_lines(&buf);\n \ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 19a7e06bf4..07327d3c04 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -310,7 +310,7 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n \n \tif (opt->cleanup_mode != CLEANUP_NONE)\n \t\tstrbuf_stripspace(buf,\n-\t\t  opt->cleanup_mode == CLEANUP_ALL ? comment_line_char : '\\0');\n+\t\t  opt->cleanup_mode == CLEANUP_ALL ? comment_line_str : NULL);\n \n \tif (!opt->message_given && !buf->len)\n \t\tdie(_(\"no tag message?\"));\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 9c76b62b02..f0aa962cf8 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -657,7 +657,7 @@ static int can_use_local_refs(const struct add_opts *opts)\n \t\t\tstrbuf_add_real_path(&path, get_worktree_git_dir(NULL));\n \t\t\tstrbuf_addstr(&path, \"/HEAD\");\n \t\t\tstrbuf_read_file(&contents, path.buf, 64);\n-\t\t\tstrbuf_stripspace(&contents, 0);\n+\t\t\tstrbuf_stripspace(&contents, NULL);\n \t\t\tstrbuf_strip_suffix(&contents, \"\\n\");\n \n \t\t\twarning(_(\"HEAD points to an invalid (or orphaned) reference.\\n\"\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 95e764acb1..b5993385ff 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -586,8 +586,8 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,\n \t\t}\n \t}\n \n-\tstrbuf_stripspace(&ssh_keygen_out, '\\0');\n-\tstrbuf_stripspace(&ssh_keygen_err, '\\0');\n+\tstrbuf_stripspace(&ssh_keygen_out, NULL);\n+\tstrbuf_stripspace(&ssh_keygen_err, NULL);\n \t/* Add stderr outputs to show the user actual ssh-keygen errors */\n \tstrbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);\n \tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\ndiff --git a/rebase-interactive.c b/rebase-interactive.c\nindex d9718409b3..6dfc33e4e3 100644\n--- a/rebase-interactive.c\n+++ b/rebase-interactive.c\n@@ -130,7 +130,7 @@ int edit_todo_list(struct repository *r, struct todo_list *todo_list,\n \tif (launch_sequence_editor(todo_file, &new_todo->buf, NULL))\n \t\treturn -2;\n \n-\tstrbuf_stripspace(&new_todo->buf, comment_line_char);\n+\tstrbuf_stripspace(&new_todo->buf, comment_line_str);\n \tif (initial && new_todo->buf.len == 0)\n \t\treturn -3;\n \ndiff --git a/sequencer.c b/sequencer.c\nindex 5c6f541126..4819265bf1 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1154,7 +1154,7 @@ void cleanup_message(struct strbuf *msgbuf,\n \t\tstrbuf_setlen(msgbuf, wt_status_locate_end(msgbuf->buf, msgbuf->len));\n \tif (cleanup_mode != COMMIT_MSG_CLEANUP_NONE)\n \t\tstrbuf_stripspace(msgbuf,\n-\t\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_char : '\\0');\n+\t\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_str : NULL);\n }\n \n /*\n@@ -1186,7 +1186,7 @@ int template_untouched(const struct strbuf *sb, const char *template_file,\n \t\treturn 0;\n \n \tstrbuf_stripspace(&tmpl,\n-\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_char : '\\0');\n+\t  cleanup_mode == COMMIT_MSG_CLEANUP_ALL ? comment_line_str : NULL);\n \tif (!skip_prefix(sb->buf, tmpl.buf, &start))\n \t\tstart = sb->buf;\n \tstrbuf_release(&tmpl);\n@@ -1559,7 +1559,7 @@ static int try_to_commit(struct repository *r,\n \n \tif (cleanup != COMMIT_MSG_CLEANUP_NONE)\n \t\tstrbuf_stripspace(msg,\n-\t\t  cleanup == COMMIT_MSG_CLEANUP_ALL ? comment_line_char : '\\0');\n+\t\t  cleanup == COMMIT_MSG_CLEANUP_ALL ? comment_line_str : NULL);\n \tif ((flags & EDIT_MSG) && message_is_empty(msg, cleanup)) {\n \t\tres = 1; /* run 'git commit' to display error message */\n \t\tgoto out;\ndiff --git a/strbuf.c b/strbuf.c\nindex a33aed6c07..e9b6127e76 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -1001,10 +1001,10 @@ static size_t cleanup(char *line, size_t len)\n  *\n  * If last line does not have a newline at the end, one is added.\n  *\n- * Pass a non-NUL comment_prefix to skip every line starting\n+ * Pass a non-NULL comment_prefix to skip every line starting\n  * with it.\n  */\n-void strbuf_stripspace(struct strbuf *sb, char comment_prefix)\n+void strbuf_stripspace(struct strbuf *sb, const char *comment_prefix)\n {\n \tsize_t empties = 0;\n \tsize_t i, j, len, newlen;\n@@ -1018,7 +1018,7 @@ void strbuf_stripspace(struct strbuf *sb, char comment_prefix)\n \t\tlen = eol ? eol - (sb->buf + i) + 1 : sb->len - i;\n \n \t\tif (comment_prefix && len &&\n-\t\t    sb->buf[i] == comment_prefix) {\n+\t\t    starts_with(sb->buf + i, comment_prefix)) {\n \t\t\tnewlen = 0;\n \t\t\tcontinue;\n \t\t}\ndiff --git a/strbuf.h b/strbuf.h\nindex 860fcec5fb..dc4710adbb 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -513,11 +513,11 @@ int strbuf_getcwd(struct strbuf *sb);\n int strbuf_normalize_path(struct strbuf *sb);\n \n /**\n- * Strip whitespace from a buffer. If comment_prefix is non-NUL,\n+ * Strip whitespace from a buffer. If comment_prefix is non-NULL,\n  * then lines beginning with that character are considered comments,\n  * thus removed.\n  */\n-void strbuf_stripspace(struct strbuf *buf, char comment_prefix);\n+void strbuf_stripspace(struct strbuf *buf, const char *comment_prefix);\n \n static inline int strbuf_strip_suffix(struct strbuf *sb, const char *suffix)\n {\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490465","messageId":"20240312091729.GH95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 08/16] strbuf: accept a comment string for strbuf_commented_addf()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:29Z","receivedAt":"2024-03-12T09:17:31Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, let's take a\nNUL-terminated string pointer for strbuf_commented_addf() rather than a\nsingle character.\n\nAll of the callers have to be adjusted, but they can just pass\ncomment_line_str rather than comment_line_char.\n\nNote that we rely on strbuf_add_commented_lines() under the hood, so\nwe'll cheat a bit to squeeze our string into a single character (for now\nthe two are equivalent, and we'll address this TODO in the next patch).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n add-patch.c          |  8 ++++----\n builtin/branch.c     |  2 +-\n builtin/merge.c      |  8 ++++----\n builtin/tag.c        |  4 ++--\n rebase-interactive.c |  2 +-\n sequencer.c          |  4 ++--\n strbuf.c             | 10 ++++++++--\n strbuf.h             |  2 +-\n wt-status.c          |  2 +-\n 9 files changed, 24 insertions(+), 18 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 68f525b35c..7390677795 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1105,11 +1105,11 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \tsize_t i;\n \n \tstrbuf_reset(&s->buf);\n-\tstrbuf_commented_addf(&s->buf, comment_line_char,\n+\tstrbuf_commented_addf(&s->buf, comment_line_str,\n \t\t\t      _(\"Manual hunk edit mode -- see bottom for \"\n \t\t\t\t\"a quick guide.\\n\"));\n \trender_hunk(s, hunk, 0, 0, &s->buf);\n-\tstrbuf_commented_addf(&s->buf, comment_line_char,\n+\tstrbuf_commented_addf(&s->buf, comment_line_str,\n \t\t\t      _(\"---\\n\"\n \t\t\t\t\"To remove '%c' lines, make them ' ' lines \"\n \t\t\t\t\"(context).\\n\"\n@@ -1118,13 +1118,13 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \t\t\t      s->mode->is_reverse ? '+' : '-',\n \t\t\t      s->mode->is_reverse ? '-' : '+',\n \t\t\t      comment_line_char);\n-\tstrbuf_commented_addf(&s->buf, comment_line_char, \"%s\",\n+\tstrbuf_commented_addf(&s->buf, comment_line_str, \"%s\",\n \t\t\t      _(s->mode->edit_hunk_hint));\n \t/*\n \t * TRANSLATORS: 'it' refers to the patch mentioned in the previous\n \t * messages.\n \t */\n-\tstrbuf_commented_addf(&s->buf, comment_line_char,\n+\tstrbuf_commented_addf(&s->buf, comment_line_str,\n \t\t\t      _(\"If it does not apply cleanly, you will be \"\n \t\t\t\t\"given an opportunity to\\n\"\n \t\t\t\t\"edit again.  If all lines of the hunk are \"\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex f6091f3438..2d8c89e9ac 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -673,7 +673,7 @@ static int edit_branch_description(const char *branch_name)\n \texists = !read_branch_desc(&buf, branch_name);\n \tif (!buf.len || buf.buf[buf.len-1] != '\\n')\n \t\tstrbuf_addch(&buf, '\\n');\n-\tstrbuf_commented_addf(&buf, comment_line_char,\n+\tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t    _(\"Please edit the description for the branch\\n\"\n \t\t      \"  %s\\n\"\n \t\t      \"Lines starting with '%c' will be stripped.\\n\"),\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex a0ba1f9815..4e47434708 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -852,15 +852,15 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tstrbuf_addch(&msg, '\\n');\n \t\tif (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {\n \t\t\twt_status_append_cut_line(&msg);\n-\t\t\tstrbuf_commented_addf(&msg, comment_line_char, \"\\n\");\n+\t\t\tstrbuf_commented_addf(&msg, comment_line_str, \"\\n\");\n \t\t}\n-\t\tstrbuf_commented_addf(&msg, comment_line_char,\n+\t\tstrbuf_commented_addf(&msg, comment_line_str,\n \t\t\t\t      _(merge_editor_comment));\n \t\tif (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS)\n-\t\t\tstrbuf_commented_addf(&msg, comment_line_char,\n+\t\t\tstrbuf_commented_addf(&msg, comment_line_str,\n \t\t\t\t\t      _(scissors_editor_comment));\n \t\telse\n-\t\t\tstrbuf_commented_addf(&msg, comment_line_char,\n+\t\t\tstrbuf_commented_addf(&msg, comment_line_str,\n \t\t\t\t_(no_scissors_editor_comment), comment_line_char);\n \t}\n \tif (signoff)\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 07327d3c04..1c708785bf 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -291,10 +291,10 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n \t\t\tstruct strbuf buf = STRBUF_INIT;\n \t\t\tstrbuf_addch(&buf, '\\n');\n \t\t\tif (opt->cleanup_mode == CLEANUP_ALL)\n-\t\t\t\tstrbuf_commented_addf(&buf, comment_line_char,\n+\t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t\t\t      _(tag_template), tag, comment_line_char);\n \t\t\telse\n-\t\t\t\tstrbuf_commented_addf(&buf, comment_line_char,\n+\t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t\t\t      _(tag_template_nocleanup), tag, comment_line_char);\n \t\t\twrite_or_die(fd, buf.buf, buf.len);\n \t\t\tstrbuf_release(&buf);\ndiff --git a/rebase-interactive.c b/rebase-interactive.c\nindex 6dfc33e4e3..affc93a8e4 100644\n--- a/rebase-interactive.c\n+++ b/rebase-interactive.c\n@@ -71,7 +71,7 @@ void append_todo_help(int command_count,\n \n \tif (!edit_todo) {\n \t\tstrbuf_addch(buf, '\\n');\n-\t\tstrbuf_commented_addf(buf, comment_line_char,\n+\t\tstrbuf_commented_addf(buf, comment_line_str,\n \t\t\t\t      Q_(\"Rebase %s onto %s (%d command)\",\n \t\t\t\t\t \"Rebase %s onto %s (%d commands)\",\n \t\t\t\t\t command_count),\ndiff --git a/sequencer.c b/sequencer.c\nindex 4819265bf1..051929c9f1 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -667,11 +667,11 @@ void append_conflicts_hint(struct index_state *istate,\n \t}\n \n \tstrbuf_addch(msgbuf, '\\n');\n-\tstrbuf_commented_addf(msgbuf, comment_line_char, \"Conflicts:\\n\");\n+\tstrbuf_commented_addf(msgbuf, comment_line_str, \"Conflicts:\\n\");\n \tfor (i = 0; i < istate->cache_nr;) {\n \t\tconst struct cache_entry *ce = istate->cache[i++];\n \t\tif (ce_stage(ce)) {\n-\t\t\tstrbuf_commented_addf(msgbuf, comment_line_char,\n+\t\t\tstrbuf_commented_addf(msgbuf, comment_line_str,\n \t\t\t\t\t      \"\\t%s\\n\", ce->name);\n \t\t\twhile (i < istate->cache_nr &&\n \t\t\t       !strcmp(ce->name, istate->cache[i]->name))\ndiff --git a/strbuf.c b/strbuf.c\nindex e9b6127e76..76d02e0920 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -368,7 +368,7 @@ void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n \tadd_lines(out, prefix, buf, size, 1);\n }\n \n-void strbuf_commented_addf(struct strbuf *sb, char comment_prefix,\n+void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix,\n \t\t\t   const char *fmt, ...)\n {\n \tva_list params;\n@@ -379,7 +379,13 @@ void strbuf_commented_addf(struct strbuf *sb, char comment_prefix,\n \tstrbuf_vaddf(&buf, fmt, params);\n \tva_end(params);\n \n-\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix);\n+\t/*\n+\t * TODO Our commented_lines helper does not yet understand\n+\t * comment strings. But since we know that the strings are\n+\t * always single-char, we can cheat for the moment, and\n+\t * fix this later.\n+\t */\n+\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix[0]);\n \tif (incomplete_line)\n \t\tsb->buf[--sb->len] = '\\0';\n \ndiff --git a/strbuf.h b/strbuf.h\nindex dc4710adbb..b128ca539a 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -379,7 +379,7 @@ void strbuf_addf(struct strbuf *sb, const char *fmt, ...);\n  * blank to the buffer.\n  */\n __attribute__((format (printf, 3, 4)))\n-void strbuf_commented_addf(struct strbuf *sb, char comment_prefix, const char *fmt, ...);\n+void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix, const char *fmt, ...);\n \n __attribute__((format (printf,2,0)))\n void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap);\ndiff --git a/wt-status.c b/wt-status.c\nindex 7108a92b52..3845e1d383 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1103,7 +1103,7 @@ void wt_status_append_cut_line(struct strbuf *buf)\n {\n \tconst char *explanation = _(\"Do not modify or remove the line above.\\nEverything below it will be ignored.\");\n \n-\tstrbuf_commented_addf(buf, comment_line_char, \"%s\", cut_line);\n+\tstrbuf_commented_addf(buf, comment_line_str, \"%s\", cut_line);\n \tstrbuf_add_commented_lines(buf, explanation, strlen(explanation), comment_line_char);\n }\n \n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490466","messageId":"20240312091732.GI95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 09/16] strbuf: accept a comment string for strbuf_add_commented_lines()","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:32Z","receivedAt":"2024-03-12T09:17:33Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, let's take a\nNUL-terminated string pointer for strbuf_add_commented_lines() rather\nthan a single character.\n\nAll of the callers have to be adjusted; most can just pass\ncomment_line_str rather than comment_line_char.\n\nAnd now our \"cheat\" in strbuf_commented_addf() can go away, as we can\ntake the full string from it.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n builtin/notes.c      |  8 ++++----\n builtin/stripspace.c |  2 +-\n fmt-merge-msg.c      |  6 +++---\n rebase-interactive.c |  6 +++---\n sequencer.c          |  8 ++++----\n strbuf.c             | 16 +++-------------\n strbuf.h             |  2 +-\n wt-status.c          |  4 ++--\n 8 files changed, 21 insertions(+), 31 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex ae981085ea..cb011303e6 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -179,7 +179,7 @@ static void write_commented_object(int fd, const struct object_id *object)\n \n \tif (strbuf_read(&buf, show.out, 0) < 0)\n \t\tdie_errno(_(\"could not read 'show' output\"));\n-\tstrbuf_add_commented_lines(&cbuf, buf.buf, buf.len, comment_line_char);\n+\tstrbuf_add_commented_lines(&cbuf, buf.buf, buf.len, comment_line_str);\n \twrite_or_die(fd, cbuf.buf, cbuf.len);\n \n \tstrbuf_release(&cbuf);\n@@ -207,10 +207,10 @@ static void prepare_note_data(const struct object_id *object, struct note_data *\n \t\t\tcopy_obj_to_fd(fd, old_note);\n \n \t\tstrbuf_addch(&buf, '\\n');\n-\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_char);\n+\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_str);\n \t\tstrbuf_add_commented_lines(&buf, _(note_template), strlen(_(note_template)),\n-\t\t\t\t\t   comment_line_char);\n-\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_char);\n+\t\t\t\t\t   comment_line_str);\n+\t\tstrbuf_add_commented_lines(&buf, \"\\n\", strlen(\"\\n\"), comment_line_str);\n \t\twrite_or_die(fd, buf.buf, buf.len);\n \n \t\twrite_commented_object(fd, object);\ndiff --git a/builtin/stripspace.c b/builtin/stripspace.c\nindex 434ac490cb..e5626e5126 100644\n--- a/builtin/stripspace.c\n+++ b/builtin/stripspace.c\n@@ -13,7 +13,7 @@ static void comment_lines(struct strbuf *buf)\n \tsize_t len;\n \n \tmsg = strbuf_detach(buf, &len);\n-\tstrbuf_add_commented_lines(buf, msg, len, comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, len, comment_line_str);\n \tfree(msg);\n }\n \ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 66e47449a0..79e8aad086 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -510,7 +510,7 @@ static void fmt_tag_signature(struct strbuf *tagbuf,\n \tif (sig->len) {\n \t\tstrbuf_addch(tagbuf, '\\n');\n \t\tstrbuf_add_commented_lines(tagbuf, sig->buf, sig->len,\n-\t\t\t\t\t   comment_line_char);\n+\t\t\t\t\t   comment_line_str);\n \t}\n }\n \n@@ -557,7 +557,7 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\t\tstrbuf_add_commented_lines(&tagline,\n \t\t\t\t\t\torigins.items[first_tag].string,\n \t\t\t\t\t\tstrlen(origins.items[first_tag].string),\n-\t\t\t\t\t\tcomment_line_char);\n+\t\t\t\t\t\tcomment_line_str);\n \t\t\t\tstrbuf_insert(&tagbuf, 0, tagline.buf,\n \t\t\t\t\t      tagline.len);\n \t\t\t\tstrbuf_release(&tagline);\n@@ -566,7 +566,7 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tstrbuf_add_commented_lines(&tagbuf,\n \t\t\t\t\torigins.items[i].string,\n \t\t\t\t\tstrlen(origins.items[i].string),\n-\t\t\t\t\tcomment_line_char);\n+\t\t\t\t\tcomment_line_str);\n \t\t\tfmt_tag_signature(&tagbuf, &sig, buf, len);\n \t\t}\n \t\tstrbuf_release(&payload);\ndiff --git a/rebase-interactive.c b/rebase-interactive.c\nindex affc93a8e4..c343e16fcd 100644\n--- a/rebase-interactive.c\n+++ b/rebase-interactive.c\n@@ -78,7 +78,7 @@ void append_todo_help(int command_count,\n \t\t\t\t      shortrevisions, shortonto, command_count);\n \t}\n \n-\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str);\n \n \tif (get_missing_commit_check_level() == MISSING_COMMIT_CHECK_ERROR)\n \t\tmsg = _(\"\\nDo not remove any line. Use 'drop' \"\n@@ -87,7 +87,7 @@ void append_todo_help(int command_count,\n \t\tmsg = _(\"\\nIf you remove a line here \"\n \t\t\t \"THAT COMMIT WILL BE LOST.\\n\");\n \n-\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str);\n \n \tif (edit_todo)\n \t\tmsg = _(\"\\nYou are editing the todo file \"\n@@ -98,7 +98,7 @@ void append_todo_help(int command_count,\n \t\tmsg = _(\"\\nHowever, if you remove everything, \"\n \t\t\t\"the rebase will be aborted.\\n\\n\");\n \n-\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, msg, strlen(msg), comment_line_str);\n }\n \n int edit_todo_list(struct repository *r, struct todo_list *todo_list,\ndiff --git a/sequencer.c b/sequencer.c\nindex 051929c9f1..d12c5a8a03 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1853,7 +1853,7 @@ static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)\n \t\ts += count;\n \t\tlen -= count;\n \t}\n-\tstrbuf_add_commented_lines(buf, s, len, comment_line_char);\n+\tstrbuf_add_commented_lines(buf, s, len, comment_line_str);\n }\n \n /* Does the current fixup chain contain a squash command? */\n@@ -1952,7 +1952,7 @@ static int append_squash_message(struct strbuf *buf, const char *body,\n \tstrbuf_addf(buf, _(nth_commit_msg_fmt),\n \t\t    ++opts->current_fixup_count + 1);\n \tstrbuf_addstr(buf, \"\\n\\n\");\n-\tstrbuf_add_commented_lines(buf, body, commented_len, comment_line_char);\n+\tstrbuf_add_commented_lines(buf, body, commented_len, comment_line_str);\n \t/* buf->buf may be reallocated so store an offset into the buffer */\n \tfixup_off = buf->len;\n \tstrbuf_addstr(buf, body + commented_len);\n@@ -2043,7 +2043,7 @@ static int update_squash_messages(struct repository *r,\n \t\tstrbuf_addstr(&buf, \"\\n\\n\");\n \t\tif (is_fixup_flag(command, flag))\n \t\t\tstrbuf_add_commented_lines(&buf, body, strlen(body),\n-\t\t\t\t\t\t   comment_line_char);\n+\t\t\t\t\t\t   comment_line_str);\n \t\telse\n \t\t\tstrbuf_addstr(&buf, body);\n \n@@ -2063,7 +2063,7 @@ static int update_squash_messages(struct repository *r,\n \t\t\t    ++opts->current_fixup_count + 1);\n \t\tstrbuf_addstr(&buf, \"\\n\\n\");\n \t\tstrbuf_add_commented_lines(&buf, body, strlen(body),\n-\t\t\t\t\t   comment_line_char);\n+\t\t\t\t\t   comment_line_str);\n \t} else\n \t\treturn error(_(\"unknown command: %d\"), command);\n \trepo_unuse_commit_buffer(r, commit, message);\ndiff --git a/strbuf.c b/strbuf.c\nindex 76d02e0920..7c8f582127 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -359,13 +359,9 @@ static void add_lines(struct strbuf *out,\n }\n \n void strbuf_add_commented_lines(struct strbuf *out, const char *buf,\n-\t\t\t\tsize_t size, char comment_prefix)\n+\t\t\t\tsize_t size, const char *comment_prefix)\n {\n-\tchar prefix[2];\n-\n-\tprefix[0] = comment_prefix;\n-\tprefix[1] = '\\0';\n-\tadd_lines(out, prefix, buf, size, 1);\n+\tadd_lines(out, comment_prefix, buf, size, 1);\n }\n \n void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix,\n@@ -379,13 +375,7 @@ void strbuf_commented_addf(struct strbuf *sb, const char *comment_prefix,\n \tstrbuf_vaddf(&buf, fmt, params);\n \tva_end(params);\n \n-\t/*\n-\t * TODO Our commented_lines helper does not yet understand\n-\t * comment strings. But since we know that the strings are\n-\t * always single-char, we can cheat for the moment, and\n-\t * fix this later.\n-\t */\n-\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix[0]);\n+\tstrbuf_add_commented_lines(sb, buf.buf, buf.len, comment_prefix);\n \tif (incomplete_line)\n \t\tsb->buf[--sb->len] = '\\0';\n \ndiff --git a/strbuf.h b/strbuf.h\nindex b128ca539a..58dddf2777 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -288,7 +288,7 @@ void strbuf_splice(struct strbuf *sb, size_t pos, size_t len,\n  */\n void strbuf_add_commented_lines(struct strbuf *out,\n \t\t\t\tconst char *buf, size_t size,\n-\t\t\t\tchar comment_prefix);\n+\t\t\t\tconst char *comment_prefix);\n \n \n /**\ndiff --git a/wt-status.c b/wt-status.c\nindex 3845e1d383..ae623e760e 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1028,7 +1028,7 @@ static void wt_longstatus_print_submodule_summary(struct wt_status *s, int uncom\n \tif (s->display_comment_prefix) {\n \t\tsize_t len;\n \t\tsummary_content = strbuf_detach(&summary, &len);\n-\t\tstrbuf_add_commented_lines(&summary, summary_content, len, comment_line_char);\n+\t\tstrbuf_add_commented_lines(&summary, summary_content, len, comment_line_str);\n \t\tfree(summary_content);\n \t}\n \n@@ -1104,7 +1104,7 @@ void wt_status_append_cut_line(struct strbuf *buf)\n \tconst char *explanation = _(\"Do not modify or remove the line above.\\nEverything below it will be ignored.\");\n \n \tstrbuf_commented_addf(buf, comment_line_str, \"%s\", cut_line);\n-\tstrbuf_add_commented_lines(buf, explanation, strlen(explanation), comment_line_char);\n+\tstrbuf_add_commented_lines(buf, explanation, strlen(explanation), comment_line_str);\n }\n \n void wt_status_add_cut_line(struct wt_status *s)\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490467","messageId":"20240312091734.GJ95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 10/16] prefer comment_line_str to comment_line_char for printing","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:34Z","receivedAt":"2024-03-12T09:17:36Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As part of our transition to multi-byte comment characters, we should\nuse the string variable rather than the historical character variable.\nAll of the sites adjusted here are just swapping out \"%c\" for \"%s\" in\nformat strings, or strbuf_addch() for strbuf_addstr(). The type system\nand printf-attribute give the compiler enough information to make sure\nour formats and variable changes all match (especially important for\ncases where the format string is defined far away from its use, like\nprepare_to_commit() in commit.c).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n add-patch.c      |  4 ++--\n builtin/branch.c |  4 ++--\n builtin/commit.c | 12 ++++++------\n builtin/merge.c  |  4 ++--\n builtin/tag.c    |  8 ++++----\n fmt-merge-msg.c  |  2 +-\n sequencer.c      | 20 ++++++++++----------\n wt-status.c      | 10 +++++-----\n 8 files changed, 32 insertions(+), 32 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 7390677795..4a10237d50 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1114,10 +1114,10 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \t\t\t\t\"To remove '%c' lines, make them ' ' lines \"\n \t\t\t\t\"(context).\\n\"\n \t\t\t\t\"To remove '%c' lines, delete them.\\n\"\n-\t\t\t\t\"Lines starting with %c will be removed.\\n\"),\n+\t\t\t\t\"Lines starting with %s will be removed.\\n\"),\n \t\t\t      s->mode->is_reverse ? '+' : '-',\n \t\t\t      s->mode->is_reverse ? '-' : '+',\n-\t\t\t      comment_line_char);\n+\t\t\t      comment_line_str);\n \tstrbuf_commented_addf(&s->buf, comment_line_str, \"%s\",\n \t\t\t      _(s->mode->edit_hunk_hint));\n \t/*\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex 2d8c89e9ac..faf6ea1b7b 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -676,8 +676,8 @@ static int edit_branch_description(const char *branch_name)\n \tstrbuf_commented_addf(&buf, comment_line_str,\n \t\t    _(\"Please edit the description for the branch\\n\"\n \t\t      \"  %s\\n\"\n-\t\t      \"Lines starting with '%c' will be stripped.\\n\"),\n-\t\t    branch_name, comment_line_char);\n+\t\t      \"Lines starting with '%s' will be stripped.\\n\"),\n+\t\t    branch_name, comment_line_str);\n \twrite_file_buf(edit_description(), buf.buf, buf.len);\n \tstrbuf_reset(&buf);\n \tif (launch_editor(edit_description(), &buf, NULL)) {\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 9b139fc795..066dc42a3d 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -910,18 +910,18 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tstruct ident_split ci, ai;\n \t\tconst char *hint_cleanup_all = allow_empty_message ?\n \t\t\t_(\"Please enter the commit message for your changes.\"\n-\t\t\t  \" Lines starting\\nwith '%c' will be ignored.\\n\") :\n+\t\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\") :\n \t\t\t_(\"Please enter the commit message for your changes.\"\n-\t\t\t  \" Lines starting\\nwith '%c' will be ignored, and an empty\"\n+\t\t\t  \" Lines starting\\nwith '%s' will be ignored, and an empty\"\n \t\t\t  \" message aborts the commit.\\n\");\n \t\tconst char *hint_cleanup_space = allow_empty_message ?\n \t\t\t_(\"Please enter the commit message for your changes.\"\n \t\t\t  \" Lines starting\\n\"\n-\t\t\t  \"with '%c' will be kept; you may remove them\"\n+\t\t\t  \"with '%s' will be kept; you may remove them\"\n \t\t\t  \" yourself if you want to.\\n\") :\n \t\t\t_(\"Please enter the commit message for your changes.\"\n \t\t\t  \" Lines starting\\n\"\n-\t\t\t  \"with '%c' will be kept; you may remove them\"\n+\t\t\t  \"with '%s' will be kept; you may remove them\"\n \t\t\t  \" yourself if you want to.\\n\"\n \t\t\t  \"An empty message aborts the commit.\\n\");\n \t\tif (whence != FROM_COMMIT) {\n@@ -944,12 +944,12 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \n \t\tfprintf(s->fp, \"\\n\");\n \t\tif (cleanup_mode == COMMIT_MSG_CLEANUP_ALL)\n-\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_all, comment_line_char);\n+\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_all, comment_line_str);\n \t\telse if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {\n \t\t\tif (whence == FROM_COMMIT)\n \t\t\t\twt_status_add_cut_line(s);\n \t\t} else /* COMMIT_MSG_CLEANUP_SPACE, that is. */\n-\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_space, comment_line_char);\n+\t\t\tstatus_printf(s, GIT_COLOR_NORMAL, hint_cleanup_space, comment_line_str);\n \n \t\t/*\n \t\t * These should never fail because they come from our own\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 4e47434708..1e33aa49a0 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -821,7 +821,7 @@ static const char scissors_editor_comment[] =\n N_(\"An empty message aborts the commit.\\n\");\n \n static const char no_scissors_editor_comment[] =\n-N_(\"Lines starting with '%c' will be ignored, and an empty message aborts\\n\"\n+N_(\"Lines starting with '%s' will be ignored, and an empty message aborts\\n\"\n    \"the commit.\\n\");\n \n static void write_merge_heads(struct commit_list *);\n@@ -861,7 +861,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\t\t\t\t      _(scissors_editor_comment));\n \t\telse\n \t\t\tstrbuf_commented_addf(&msg, comment_line_str,\n-\t\t\t\t_(no_scissors_editor_comment), comment_line_char);\n+\t\t\t\t_(no_scissors_editor_comment), comment_line_str);\n \t}\n \tif (signoff)\n \t\tappend_signoff(&msg, ignored_log_message_bytes(msg.buf, msg.len), 0);\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 1c708785bf..721d07a589 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -158,11 +158,11 @@ static int do_sign(struct strbuf *buffer)\n \n static const char tag_template[] =\n \tN_(\"\\nWrite a message for tag:\\n  %s\\n\"\n-\t\"Lines starting with '%c' will be ignored.\\n\");\n+\t\"Lines starting with '%s' will be ignored.\\n\");\n \n static const char tag_template_nocleanup[] =\n \tN_(\"\\nWrite a message for tag:\\n  %s\\n\"\n-\t\"Lines starting with '%c' will be kept; you may remove them\"\n+\t\"Lines starting with '%s' will be kept; you may remove them\"\n \t\" yourself if you want to.\\n\");\n \n static int git_tag_config(const char *var, const char *value,\n@@ -292,10 +292,10 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n \t\t\tstrbuf_addch(&buf, '\\n');\n \t\t\tif (opt->cleanup_mode == CLEANUP_ALL)\n \t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n-\t\t\t\t      _(tag_template), tag, comment_line_char);\n+\t\t\t\t      _(tag_template), tag, comment_line_str);\n \t\t\telse\n \t\t\t\tstrbuf_commented_addf(&buf, comment_line_str,\n-\t\t\t\t      _(tag_template_nocleanup), tag, comment_line_char);\n+\t\t\t\t      _(tag_template_nocleanup), tag, comment_line_str);\n \t\t\twrite_or_die(fd, buf.buf, buf.len);\n \t\t\tstrbuf_release(&buf);\n \t\t}\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 79e8aad086..ae201e21db 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -321,7 +321,7 @@ static void credit_people(struct strbuf *out,\n \t     skip_prefix(me, them->items->string, &me) &&\n \t     starts_with(me, \" <\")))\n \t\treturn;\n-\tstrbuf_addf(out, \"\\n%c %s \", comment_line_char, label);\n+\tstrbuf_addf(out, \"\\n%s %s \", comment_line_str, label);\n \tadd_people_count(out, them);\n }\n \ndiff --git a/sequencer.c b/sequencer.c\nindex d12c5a8a03..b75d0c098d 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -663,7 +663,7 @@ void append_conflicts_hint(struct index_state *istate,\n \tif (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {\n \t\tstrbuf_addch(msgbuf, '\\n');\n \t\twt_status_append_cut_line(msgbuf);\n-\t\tstrbuf_addch(msgbuf, comment_line_char);\n+\t\tstrbuf_addstr(msgbuf, comment_line_str);\n \t}\n \n \tstrbuf_addch(msgbuf, '\\n');\n@@ -1948,7 +1948,7 @@ static int append_squash_message(struct strbuf *buf, const char *body,\n \t     (starts_with(body, \"squash!\") || starts_with(body, \"fixup!\"))))\n \t\tcommented_len = commit_subject_length(body);\n \n-\tstrbuf_addf(buf, \"\\n%c \", comment_line_char);\n+\tstrbuf_addf(buf, \"\\n%s \", comment_line_str);\n \tstrbuf_addf(buf, _(nth_commit_msg_fmt),\n \t\t    ++opts->current_fixup_count + 1);\n \tstrbuf_addstr(buf, \"\\n\\n\");\n@@ -2008,7 +2008,7 @@ static int update_squash_messages(struct repository *r,\n \t\teol = buf.buf[0] != comment_line_char ?\n \t\t\tbuf.buf : strchrnul(buf.buf, '\\n');\n \n-\t\tstrbuf_addf(&header, \"%c \", comment_line_char);\n+\t\tstrbuf_addf(&header, \"%s \", comment_line_str);\n \t\tstrbuf_addf(&header, _(combined_commit_msg_fmt),\n \t\t\t    opts->current_fixup_count + 2);\n \t\tstrbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);\n@@ -2034,9 +2034,9 @@ static int update_squash_messages(struct repository *r,\n \t\t\trepo_unuse_commit_buffer(r, head_commit, head_message);\n \t\t\treturn error(_(\"cannot write '%s'\"), rebase_path_fixup_msg());\n \t\t}\n-\t\tstrbuf_addf(&buf, \"%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"%s \", comment_line_str);\n \t\tstrbuf_addf(&buf, _(combined_commit_msg_fmt), 2);\n-\t\tstrbuf_addf(&buf, \"\\n%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"\\n%s \", comment_line_str);\n \t\tstrbuf_addstr(&buf, is_fixup_flag(command, flag) ?\n \t\t\t      _(skip_first_commit_msg_str) :\n \t\t\t      _(first_commit_msg_str));\n@@ -2058,7 +2058,7 @@ static int update_squash_messages(struct repository *r,\n \tif (command == TODO_SQUASH || is_fixup_flag(command, flag)) {\n \t\tres = append_squash_message(&buf, body, command, opts, flag);\n \t} else if (command == TODO_FIXUP) {\n-\t\tstrbuf_addf(&buf, \"\\n%c \", comment_line_char);\n+\t\tstrbuf_addf(&buf, \"\\n%s \", comment_line_str);\n \t\tstrbuf_addf(&buf, _(skip_nth_commit_msg_fmt),\n \t\t\t    ++opts->current_fixup_count + 1);\n \t\tstrbuf_addstr(&buf, \"\\n\\n\");\n@@ -5667,8 +5667,8 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\t\t\t    oid_to_hex(&commit->object.oid),\n \t\t\t\t    oneline.buf);\n \t\t\tif (is_empty)\n-\t\t\t\tstrbuf_addf(&buf, \" %c empty\",\n-\t\t\t\t\t    comment_line_char);\n+\t\t\t\tstrbuf_addf(&buf, \" %s empty\",\n+\t\t\t\t\t    comment_line_str);\n \n \t\t\tFLEX_ALLOC_STR(entry, string, buf.buf);\n \t\t\toidcpy(&entry->entry.oid, &commit->object.oid);\n@@ -5758,7 +5758,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\tentry = oidmap_get(&state.commit2label, &commit->object.oid);\n \n \t\tif (entry)\n-\t\t\tstrbuf_addf(out, \"\\n%c Branch %s\\n\", comment_line_char, entry->string);\n+\t\t\tstrbuf_addf(out, \"\\n%s Branch %s\\n\", comment_line_str, entry->string);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \n@@ -5895,7 +5895,7 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,\n \t\t\t    oid_to_hex(&commit->object.oid));\n \t\tpretty_print_commit(&pp, commit, out);\n \t\tif (is_empty)\n-\t\t\tstrbuf_addf(out, \" %c empty\", comment_line_char);\n+\t\t\tstrbuf_addf(out, \" %s empty\", comment_line_str);\n \t\tstrbuf_addch(out, '\\n');\n \t}\n \tif (skipped_commit)\ndiff --git a/wt-status.c b/wt-status.c\nindex ae623e760e..6201a97de0 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -70,7 +70,7 @@ static void status_vprintf(struct wt_status *s, int at_bol, const char *color,\n \tstrbuf_vaddf(&sb, fmt, ap);\n \tif (!sb.len) {\n \t\tif (s->display_comment_prefix) {\n-\t\t\tstrbuf_addch(&sb, comment_line_char);\n+\t\t\tstrbuf_addstr(&sb, comment_line_str);\n \t\t\tif (!trail)\n \t\t\t\tstrbuf_addch(&sb, ' ');\n \t\t}\n@@ -85,7 +85,7 @@ static void status_vprintf(struct wt_status *s, int at_bol, const char *color,\n \n \t\tstrbuf_reset(&linebuf);\n \t\tif (at_bol && s->display_comment_prefix) {\n-\t\t\tstrbuf_addch(&linebuf, comment_line_char);\n+\t\t\tstrbuf_addstr(&linebuf, comment_line_str);\n \t\t\tif (*line != '\\n' && *line != '\\t')\n \t\t\t\tstrbuf_addch(&linebuf, ' ');\n \t\t}\n@@ -1090,7 +1090,7 @@ size_t wt_status_locate_end(const char *s, size_t len)\n \tconst char *p;\n \tstruct strbuf pattern = STRBUF_INIT;\n \n-\tstrbuf_addf(&pattern, \"\\n%c %s\", comment_line_char, cut_line);\n+\tstrbuf_addf(&pattern, \"\\n%s %s\", comment_line_str, cut_line);\n \tif (starts_with(s, pattern.buf + 1))\n \t\tlen = 0;\n \telse if ((p = strstr(s, pattern.buf)))\n@@ -1218,8 +1218,8 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \t\t\t\t \"%s%.*s\", comment_line_string,\n \t\t\t\t (int)(ep - cp), cp);\n \tif (s->display_comment_prefix)\n-\t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%c\",\n-\t\t\t\t comment_line_char);\n+\t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%s\",\n+\t\t\t\t comment_line_str);\n \telse\n \t\tfputs(\"\\n\", s->fp);\n \tstrbuf_release(&sb);\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490468","messageId":"20240312091737.GK95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 11/16] find multi-byte comment chars in NUL-terminated strings","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:37Z","receivedAt":"2024-03-12T09:17:38Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Several parts of the code need to identify lines that begin with the\ncomment character, and do so with a simple byte equality check. As part\nof the transition to handling multi-byte characters, we need to match\nall of the bytes. For cases where we are looking in a NUL-terminated\nstring, we can just use starts_with(), which checks all of the\ncharacters in comment_line_str.\n\nNote that we can drop the \"line.len\" check in wt-status.c's\nread_rebase_todolist(). The starts_with() function handles the case of\nan empty haystack buffer (it will always return false for a non-empty\nprefix).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n add-patch.c | 2 +-\n sequencer.c | 2 +-\n trailer.c   | 2 +-\n wt-status.c | 2 +-\n 4 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 4a10237d50..d599ca53e1 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1139,7 +1139,7 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n \tfor (i = 0; i < s->buf.len; ) {\n \t\tsize_t next = find_next_line(&s->buf, i);\n \n-\t\tif (s->buf.buf[i] != comment_line_char)\n+\t\tif (!starts_with(s->buf.buf + i, comment_line_str))\n \t\t\tstrbuf_add(&s->plain, s->buf.buf + i, next - i);\n \t\ti = next;\n \t}\ndiff --git a/sequencer.c b/sequencer.c\nindex b75d0c098d..42125e57a4 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2005,7 +2005,7 @@ static int update_squash_messages(struct repository *r,\n \t\t\treturn error(_(\"could not read '%s'\"),\n \t\t\t\trebase_path_squash_msg());\n \n-\t\teol = buf.buf[0] != comment_line_char ?\n+\t\teol = !starts_with(buf.buf, comment_line_str) ?\n \t\t\tbuf.buf : strchrnul(buf.buf, '\\n');\n \n \t\tstrbuf_addf(&header, \"%s \", comment_line_str);\ndiff --git a/trailer.c b/trailer.c\nindex ef9df4af55..fe18faf6c5 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -1013,7 +1013,7 @@ static void parse_trailers(struct trailer_info *info,\n \tfor (i = 0; i < info->trailer_nr; i++) {\n \t\tint separator_pos;\n \t\tchar *trailer = info->trailers[i];\n-\t\tif (trailer[0] == comment_line_char)\n+\t\tif (starts_with(trailer, comment_line_str))\n \t\t\tcontinue;\n \t\tseparator_pos = find_separator(trailer, separators);\n \t\tif (separator_pos >= 1) {\ndiff --git a/wt-status.c b/wt-status.c\nindex 6201a97de0..8753d59f90 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1386,7 +1386,7 @@ static int read_rebase_todolist(const char *fname, struct string_list *lines)\n \t\t\t  git_path(\"%s\", fname));\n \t}\n \twhile (!strbuf_getline_lf(&line, f)) {\n-\t\tif (line.len && line.buf[0] == comment_line_char)\n+\t\tif (starts_with(line.buf, comment_line_str))\n \t\t\tcontinue;\n \t\tstrbuf_trim(&line);\n \t\tif (!line.len)\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490469","messageId":"20240312091739.GL95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 12/16] find multi-byte comment chars in unterminated buffers","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:39Z","receivedAt":"2024-03-12T09:17:41Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"As with the previous patch, we need to swap out single-byte matching for\nsomething like starts_with() to match all bytes of a multi-byte comment\ncharacter. But for cases where the buffer is not NUL-terminated (and we\ninstead have an explicit size or end pointer), it's not safe to use\nstarts_with(), as it might walk off the end of the buffer.\n\nLet's introduce a new starts_with_mem() that does the same thing but\nalso accepts the length of the \"haystack\" str and makes sure not to walk\npast it.\n\nNote that in most cases the existing code did not need a length check at\nall, since it was written in a way that knew we had at least one byte\navailable (and that was all we checked). So I had to read each one to\nfind the appropriate bounds. The one exception is sequencer.c's\nadd_commented_lines(), where we can actually get rid of the length\ncheck. Just like starts_with(), our starts_with_mem() handles an empty\nhaystack variable by not matching (assuming a non-empty prefix).\n\nA few notes on the implementation of starts_with_mem():\n\n  - it would be equally correct to take an \"end\" pointer (and indeed,\n    many of the callers have this and have to subtract to come up with\n    the length). I think taking a ptr/size combo is a more usual\n    interface for our codebase, though, and has the added benefit that\n    the function signature makes it harder to mix up the three\n    parameters.\n\n  - we could obviously build starts_with() on top of this by passing\n    strlen(str) as the length. But it's possible that starts_with() is a\n    relatively hot code path, and it should not pay that penalty (it can\n    generally return an answer proportional to the size of the prefix,\n    not the whole string).\n\n  - it naively feels like xstrncmpz() should be able to do the same\n    thing, but that's not quite true. If you pass the length of the\n    haystack buffer, then strncmp() finds that a shorter prefix string\n    is \"less than\" than the haystack, even if the haystack starts with\n    the prefix. If you pass the length of the prefix, then you risk\n    reading past the end of the haystack if it is shorter than the\n    prefix. So I think we really do need a new function.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n commit.c    |  3 ++-\n sequencer.c |  4 ++--\n strbuf.c    | 11 +++++++++++\n strbuf.h    |  1 +\n trailer.c   |  4 ++--\n 5 files changed, 18 insertions(+), 5 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex 467be9f7f9..9cfbe9d657 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1797,7 +1797,8 @@ size_t ignored_log_message_bytes(const char *buf, size_t len)\n \t\telse\n \t\t\tnext_line++;\n \n-\t\tif (buf[bol] == comment_line_char || buf[bol] == '\\n') {\n+\t\tif (starts_with_mem(buf + bol, cutoff - bol, comment_line_str) ||\n+\t\t    buf[bol] == '\\n') {\n \t\t\t/* is this the first of the run of comments? */\n \t\t\tif (!boc)\n \t\t\t\tboc = bol;\ndiff --git a/sequencer.c b/sequencer.c\nindex 42125e57a4..ef84832855 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1842,7 +1842,7 @@ static int is_fixup_flag(enum todo_command command, unsigned flag)\n static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)\n {\n \tconst char *s = str;\n-\twhile (len > 0 && s[0] == comment_line_char) {\n+\twhile (starts_with_mem(s, len, comment_line_str)) {\n \t\tsize_t count;\n \t\tconst char *n = memchr(s, '\\n', len);\n \t\tif (!n)\n@@ -2564,7 +2564,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,\n \t/* left-trim */\n \tbol += strspn(bol, \" \\t\");\n \n-\tif (bol == eol || *bol == '\\r' || *bol == comment_line_char) {\n+\tif (bol == eol || *bol == '\\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {\n \t\titem->command = TODO_COMMENT;\n \t\titem->commit = NULL;\n \t\titem->arg_offset = bol - buf;\ndiff --git a/strbuf.c b/strbuf.c\nindex 7c8f582127..291bdc2a65 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -24,6 +24,17 @@ int istarts_with(const char *str, const char *prefix)\n \t\t\treturn 0;\n }\n \n+int starts_with_mem(const char *str, size_t len, const char *prefix)\n+{\n+\tconst char *end = str + len;\n+\tfor (; ; str++, prefix++) {\n+\t\tif (!*prefix)\n+\t\t\treturn 1;\n+\t\telse if (str == end || *str != *prefix)\n+\t\t\treturn 0;\n+\t}\n+}\n+\n int skip_to_optional_arg_default(const char *str, const char *prefix,\n \t\t\t\t const char **arg, const char *def)\n {\ndiff --git a/strbuf.h b/strbuf.h\nindex 58dddf2777..3156d6ea8c 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -673,6 +673,7 @@ char *xstrfmt(const char *fmt, ...);\n \n int starts_with(const char *str, const char *prefix);\n int istarts_with(const char *str, const char *prefix);\n+int starts_with_mem(const char *str, size_t len, const char *prefix);\n \n /*\n  * If the string \"str\" is the same as the string in \"prefix\", then the \"arg\"\ndiff --git a/trailer.c b/trailer.c\nindex fe18faf6c5..fdb0b8137e 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n \n \t/* The first paragraph is the title and cannot be trailers */\n \tfor (s = buf; s < buf + len; s = next_line(s)) {\n-\t\tif (s[0] == comment_line_char)\n+\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n \t\t\tcontinue;\n \t\tif (is_blank_line(s))\n \t\t\tbreak;\n@@ -902,7 +902,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n \t\tconst char **p;\n \t\tssize_t separator_pos;\n \n-\t\tif (bol[0] == comment_line_char) {\n+\t\tif (starts_with_mem(bol, buf + len - bol, comment_line_str)) {\n \t\t\tnon_trailer_lines += possible_continuation_lines;\n \t\t\tpossible_continuation_lines = 0;\n \t\t\tcontinue;\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490470","messageId":"20240312091742.GM95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 13/16] sequencer: handle multi-byte comment characters when writing todo list","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:42Z","receivedAt":"2024-03-12T09:17:44Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"We already match multi-byte comment characters in parse_insn_line(),\nthanks to the previous commit, yielding a TODO_COMMENT entry. But in\ntodo_list_to_strbuf(), we may call command_to_char() to convert that\nback into something we can output.\n\nWe can't just return comment_line_char anymore, since it may require\nmultiple bytes. Instead, we'll return \"0\" for this case, which is the\nsame thing we'd return for a command which does not have a single-letter\nabbreviation (e.g., \"revert\" or \"noop\"). There is only a single caller\nof command_to_char(), and upon seeing \"0\" it falls back to outputting\nthe full name via command_to_string(). So we can handle TODO_COMMENT\nthere, returning the full string.\n\nNote that there are many other callers of command_to_string(), which\nwill now behave differently if they pass TODO_COMMENT. But we would not\nexpect that to happen; prior to this commit, the function just calls\ndie() in this case. And looking at those callers, that makes sense;\ne.g., do_pick_commit() will only be called when servicing a pick\ncommand, and should never be called for a comment in the first place.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n sequencer.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex ef84832855..a8fdf00e89 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1781,14 +1781,16 @@ static const char *command_to_string(const enum todo_command command)\n {\n \tif (command < TODO_COMMENT)\n \t\treturn todo_command_info[command].str;\n+\tif (command == TODO_COMMENT)\n+\t\treturn comment_line_str;\n \tdie(_(\"unknown command: %d\"), command);\n }\n \n static char command_to_char(const enum todo_command command)\n {\n \tif (command < TODO_COMMENT)\n \t\treturn todo_command_info[command].c;\n-\treturn comment_line_char;\n+\treturn 0;\n }\n \n static int is_noop(const enum todo_command command)\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490471","messageId":"20240312091745.GN95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 14/16] wt-status: drop custom comment-char stringification","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:45Z","receivedAt":"2024-03-12T09:17:46Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"In wt_longstatus_print_tracking() we may conditionally show a comment\nprefix based on the wt_status->display_comment_prefix flag. We handle\nthat by creating a local \"comment_line_string\" that is either the empty\nstring or the comment character followed by a space.\n\nFor a single-byte comment, the maximum length of this string is 2 (plus\na NUL byte). But to handle multi-byte comment characters, it can be\narbitrarily large. One way to handle this is to just call\nxstrfmt(\"%s \", comment_line_str), and then free it when we're done.\n\nBut we can simplify things further by just conditionally switching\nbetween our prefix string and an empty string when formatting. We\ncouldn't just do that with the previous code, because the comment\ncharacter was a single byte. There's no way to have a \"%c\" format switch\nbetween some character and \"no character at all\". Whereas with \"%s\" you\ncan switch between some string and the empty string. So now that we have\na comment string and not a comment char, we can just use it directly\nwhen formatting. Do note that we have to also conditionally add the\ntrailing space at the same time.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n wt-status.c | 13 +++----------\n 1 file changed, 3 insertions(+), 10 deletions(-)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex 8753d59f90..7217ff30c5 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -1180,8 +1180,6 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \tstruct strbuf sb = STRBUF_INIT;\n \tconst char *cp, *ep, *branch_name;\n \tstruct branch *branch;\n-\tchar comment_line_string[3];\n-\tint i;\n \tuint64_t t_begin = 0;\n \n \tassert(s->branch && !s->is_initial);\n@@ -1206,16 +1204,11 @@ static void wt_longstatus_print_tracking(struct wt_status *s)\n \t\t}\n \t}\n \n-\ti = 0;\n-\tif (s->display_comment_prefix) {\n-\t\tcomment_line_string[i++] = comment_line_char;\n-\t\tcomment_line_string[i++] = ' ';\n-\t}\n-\tcomment_line_string[i] = '\\0';\n-\n \tfor (cp = sb.buf; (ep = strchr(cp, '\\n')) != NULL; cp = ep + 1)\n \t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s),\n-\t\t\t\t \"%s%.*s\", comment_line_string,\n+\t\t\t\t \"%s%s%.*s\",\n+\t\t\t\t s->display_comment_prefix ? comment_line_str : \"\",\n+\t\t\t\t s->display_comment_prefix ? \" \" : \"\",\n \t\t\t\t (int)(ep - cp), cp);\n \tif (s->display_comment_prefix)\n \t\tcolor_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), \"%s\",\n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490472","messageId":"20240312091747.GO95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 15/16] environment: drop comment_line_char compatibility macro","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:47Z","receivedAt":"2024-03-12T09:17:49Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"There is no longer any code which references the single-byte\ncomment_line_char. Let's drop it, clearing the way for true multi-byte\nentries in comment_line_str.\n\nIt's possible there are topics in flight that have added new references\nto comment_line_char. But we would prefer to fail compilation (and then\nfix it) upon merging with this, rather than have them quietly ignore the\nbytes after the first.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n environment.h | 1 -\n 1 file changed, 1 deletion(-)\n\ndiff --git a/environment.h b/environment.h\nindex 1c7d0c2f74..05fd94d7be 100644\n--- a/environment.h\n+++ b/environment.h\n@@ -8,7 +8,6 @@ struct strvec;\n  * The character that begins a commented line in user-editable file\n  * that is subject to stripspace.\n  */\n-#define comment_line_char (comment_line_str[0])\n extern const char *comment_line_str;\n extern int auto_comment_line_char;\n \n-- \n2.44.0.481.gf1a6d20963\n\n"},{"id":"490473","messageId":"20240312091750.GP95609@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"[PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-12T09:17:50Z","receivedAt":"2024-03-12T09:17:51Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"Now that all of the code handles multi-byte comment characters, it's\nsafe to allow users to set them.\n\nThere is one special case I kept: we still will not allow an empty\nstring for the commentChar. While it might make sense in some contexts\n(e.g., output where you don't want any comment prefix), there are plenty\nwhere it will behave badly (e.g., all of our starts_with() checks will\nindicate that every line is a comment!). It might be reasonable to\nassign some meaningful semantics, but it would probably involve checking\nhow each site behaves. In the interim let's forbid it and we can loosen\nthings later.\n\nLikewise, the \"commentChar cannot be a newline\" rule is now extended to\n\"it cannot contain a newline\" (for the same reason: it can confuse our\nparsing loops).\n\nSince comment_line_str is used in many parts of the code, it's hard to\ncover all possibilities with tests. We can convert the existing\ndouble-semicolon prefix test to show that \"git status\" works. And we'll\ngive it a more challenging case in t7507, where we confirm that\ngit-commit strips out the commit template along with any --verbose text\nwhen reading the edited commit message back in. That covers the basics,\nthough it's possible there could be issues in more exotic spots (e.g.,\nthe sequencer todo list uses its own code).\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n Documentation/config/core.txt |  4 +++-\n config.c                      | 10 +++++-----\n t/t0030-stripspace.sh         |  7 ++++++-\n t/t7507-commit-verbose.sh     | 10 ++++++++++\n t/t7508-status.sh             |  4 +++-\n 5 files changed, 27 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex 0e8c2832bf..c86b8c8408 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -523,7 +523,9 @@ core.commentChar::\n \tCommands such as `commit` and `tag` that let you edit\n \tmessages consider a line that begins with this character\n \tcommented, and removes them after the editor returns\n-\t(default '#').\n+\t(default '#'). Note that this option can take values larger than\n+\ta byte (whether a single multi-byte character, or you\n+\tcould even go wild with a multi-character sequence).\n +\n If set to \"auto\", `git-commit` would select a character that is not\n the beginning character of any line in existing commit messages.\ndiff --git a/config.c b/config.c\nindex 7e5dbca4bd..92c752ed9f 100644\n--- a/config.c\n+++ b/config.c\n@@ -1565,13 +1565,13 @@ static int git_default_core_config(const char *var, const char *value,\n \t\t\treturn config_error_nonbool(var);\n \t\telse if (!strcasecmp(value, \"auto\"))\n \t\t\tauto_comment_line_char = 1;\n-\t\telse if (value[0] && !value[1]) {\n-\t\t\tif (value[0] == '\\n')\n-\t\t\t\treturn error(_(\"core.commentChar cannot be newline\"));\n-\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n+\t\telse if (value[0]) {\n+\t\t\tif (strchr(value, '\\n'))\n+\t\t\t\treturn error(_(\"core.commentChar cannot contain newline\"));\n+\t\t\tcomment_line_str = xstrdup(value);\n \t\t\tauto_comment_line_char = 0;\n \t\t} else\n-\t\t\treturn error(_(\"core.commentChar should only be one ASCII character\"));\n+\t\t\treturn error(_(\"core.commentChar must have at least one character\"));\n \t\treturn 0;\n \t}\n \ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex e399dd9189..a161faf702 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -403,7 +403,12 @@ test_expect_success 'strip comments with changed comment char' '\n \n test_expect_success 'newline as commentchar is forbidden' '\n \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n-\tgrep \"core.commentChar cannot be newline\" err\n+\tgrep \"core.commentChar cannot contain newline\" err\n+'\n+\n+test_expect_success 'empty commentchar is forbidden' '\n+\ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n+\tgrep \"core.commentChar must have at least one character\" err\n '\n \n test_expect_success '-c with single line' '\ndiff --git a/t/t7507-commit-verbose.sh b/t/t7507-commit-verbose.sh\nindex c3281b192e..4c7db19ce7 100755\n--- a/t/t7507-commit-verbose.sh\n+++ b/t/t7507-commit-verbose.sh\n@@ -101,6 +101,16 @@ test_expect_success 'verbose diff is stripped out with set core.commentChar' '\n \ttest_grep \"Aborting commit due to empty commit message.\" err\n '\n \n+test_expect_success 'verbose diff is stripped with multi-byte comment char' '\n+\t(\n+\t\tGIT_EDITOR=cat &&\n+\t\texport GIT_EDITOR &&\n+\t\ttest_must_fail git -c core.commentchar=\"foo>\" commit -a -v >out 2>err\n+\t) &&\n+\tgrep \"^foo> \" out &&\n+\ttest_grep \"Aborting commit due to empty commit message.\" err\n+'\n+\n test_expect_success 'status does not verbose without --verbose' '\n \tgit status >actual &&\n \t! grep \"^diff --git\" actual\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex a3c18a4fc2..10ed8b32bc 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -1403,7 +1403,9 @@ test_expect_success \"status (core.commentchar with submodule summary)\" '\n \n test_expect_success \"status (core.commentchar with two chars with submodule summary)\" '\n \ttest_config core.commentchar \";;\" &&\n-\ttest_must_fail git -c status.displayCommentPrefix=true status\n+\tsed \"s/^/;/\" <expect >expect.double &&\n+\tgit -c status.displayCommentPrefix=true status >output &&\n+\ttest_cmp expect.double output\n '\n \n test_expect_success \"--ignore-submodules=all suppresses submodule summary\" '\n-- \n2.44.0.481.gf1a6d20963\n"},{"id":"490485","messageId":"9b63eab9-7b6d-4024-9ec4-b58c5ca3b084@gmail.com","threadId":"61052","inReplyTo":"20240312081931.GC47852@coredump.intra.peff.net","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-03-12T14:36:36Z","receivedAt":"2024-03-12T14:36:40Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Peff\n\nOn 12/03/2024 08:19, Jeff King wrote:\n> On Fri, Mar 08, 2024 at 04:20:12PM +0000, Phillip Wood wrote:\n>> We could certainly leave it as-is and tell users they are only hurting\n>> themselves if they complain when it does not work.\n> \n> That was mostly my plan. To some degree I think this is orthogonal to my\n> series. You can already set core.commentChar to space or newline, and\n> I'm sure the results are not very good. Actually, I guess it is easy to\n> try:\n> \n>    git -c core.commentChar=$'\\n' commit --allow-empty\n> \n> treats everything as not-a-comment.\n> \n> Maybe it's worth forbidding this at the start of the series, and then\n> carrying it through. I really do think newline is the most special\n> character here, just because it's obviously going to be meaningful to\n> all of our line-oriented parsing. So you'll get weird results, as\n> opposed to broken multibyte characters, where things would still work if\n> you choose to consistently use them (and arguably we cannot even define\n> \"broken\" as the user can use a different encoding).\n\nI agree newline is a special case compared to broken multibyte \ncharacters, I see you've disallowed it in v2 which seems like a good idea.\n\n> Likewise, I guess people might complain that their core.commentChar is\n> NFD and their editor writes out NFC characters or something, and we\n> don't match. I was hoping we could just punt on that and nobody would\n> ever notice (certainly I think it is OK to punt for now and somebody who\n> truly cares can make a utf8_starts_with() or similar).\n> \n>>> Also, what exactly is the definition of \"nonsense\" will become can\n>>> of worms.  I can sympathise if somebody wants to use \"#\\t\" to give\n>>> themselves a bit more room than usual on the left for visibility,\n>>> for example, so there might be a case to want whitespace characters.\n>>\n>> That's fair, maybe we could just ban leading whitespace if we do decide to\n>> restrict core.commentChar\n> \n> Leading whitespace actually does work, though I think you'd be slightly\n> insane to use it.\n\nFor \"git rebase\" in only works if you edit the todo list with \"git \nrebase --edit-todo\" which calls strbuf_stripspace() and therefore \nparse_insn_line() never sees the comments. If you edit the todo list \ndirectly then it will error out. You can see this with\n\n     git -c core.commentChar=' ' rebase -x 'echo \" this is a comment\" \n >>\"$(git rev-parse --git-path rebase-merge/git-rebase-todo)\"' HEAD^\n\nwhich successfully picks HEAD but then gives\n\n     error: invalid command 'this'\n\nwhen it tries to parse the todo list after the exec command is run. \nGiven it is broken already I'm not sure we should worry about it here. \nIn any case it is not clear how much we should worry about problems \ncaused by users editing the todo list without using \"git rebase \n--edit-todo\". There is code in parse_insn_line() which is clearly there \nto handle direct editing of the file but I don't think it is tested and \ndirectly editing the file probably bypasses the \nrebase.missingCommitsCheck checks as well.\n\nBest Wishes\n\nPhillip\n\n> I'm currently using \"! COMMENT !\" (after using a unicode char for a few\n> days). It's horribly ugly, but I wanted to see if any bugs cropped up\n> (and vim's built-in git syntax highlighting colors it correctly ;) ).\n> \n> -Peff\n"},{"id":"490486","messageId":"7cc7d539-7f31-4cd8-bcc8-a98a67a5079f@gmail.com","threadId":"61052","inReplyTo":"20240312091013.GA95442@coredump.intra.peff.net","subject":"Re: [PATCH v2 0/16] allow multi-byte core.commentChar","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-03-12T14:40:18Z","receivedAt":"2024-03-12T14:40:23Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Peff\n\nOn 12/03/2024 09:10, Jeff King wrote:\n> Here's a revised version of my series. It incorporates the fixups I sent\n> (which I think Junio had applied already), and incorporates a new patch\n> at the beginning to forbid newlines.\n> \n> I _didn't_ convert any of the starts_with_mem() call to starts_with().\n> I'm on the fence on whether that is simplifying things or creating\n> potential confusion/bugs later.\n> \n> If we don't like the new patch 1 (or if we prefer to do it on top; there\n> is really not much reason to prefer one or the other), then this should\n> otherwise be the same as what Junio has already queued as\n> jk/core-comment-char.\n\nLooking through the range-diff it addresses all of my (sequencer \nfocused) comments on v1.\n\nBest Wishes\n\nPhillip\n\n> Range diff (from v1, without my fixups) is below.\n> \n>   -:  ---------- >  1:  86efec435d config: forbid newline as core.commentChar\n>   1:  be18aa04e3 =  2:  7c016e5dc3 strbuf: simplify comment-handling in add_lines() helper\n>   2:  0f8ea2a86d =  3:  2b4170b5f0 strbuf: avoid static variables in strbuf_add_commented_lines()\n>   3:  9b56d9f4f0 =  4:  24ca214986 commit: refactor base-case of adjust_comment_line_char()\n>   4:  0a191e5588 =  5:  9f6433dbe6 strbuf: avoid shadowing global comment_line_char name\n>   5:  f41e196138 !  6:  d0f32f10f9 environment: store comment_line_char as a string\n>      @@ builtin/commit.c: static void adjust_comment_line_char(const struct strbuf *sb)\n>       \n>        ## config.c ##\n>       @@ config.c: static int git_default_core_config(const char *var, const char *value,\n>      - \t\telse if (!strcasecmp(value, \"auto\"))\n>      - \t\t\tauto_comment_line_char = 1;\n>        \t\telse if (value[0] && !value[1]) {\n>      + \t\t\tif (value[0] == '\\n')\n>      + \t\t\t\treturn error(_(\"core.commentChar cannot be newline\"));\n>       -\t\t\tcomment_line_char = value[0];\n>       +\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n>        \t\t\tauto_comment_line_char = 0;\n>   6:  84261af2ed !  7:  2c91628564 strbuf: accept a comment string for strbuf_stripspace()\n>      @@ Commit message\n>       \n>           Signed-off-by: Jeff King <peff@peff.net>\n>       \n>      + ## builtin/am.c ##\n>      +@@ builtin/am.c: static int parse_mail(struct am_state *state, const char *mail)\n>      +\n>      + \tstrbuf_addstr(&msg, \"\\n\\n\");\n>      + \tstrbuf_addbuf(&msg, &mi.log_message);\n>      +-\tstrbuf_stripspace(&msg, '\\0');\n>      ++\tstrbuf_stripspace(&msg, NULL);\n>      +\n>      + \tassert(!state->author_name);\n>      + \tstate->author_name = strbuf_detach(&author_name, NULL);\n>      +\n>        ## builtin/branch.c ##\n>       @@ builtin/branch.c: static int edit_branch_description(const char *branch_name)\n>        \t\tstrbuf_release(&buf);\n>      @@ builtin/branch.c: static int edit_branch_description(const char *branch_name)\n>        \tstrbuf_addf(&name, \"branch.%s.description\", branch_name);\n>        \tif (buf.len || exists)\n>       \n>      + ## builtin/commit.c ##\n>      +@@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,\n>      + \ts->hints = 0;\n>      +\n>      + \tif (clean_message_contents)\n>      +-\t\tstrbuf_stripspace(&sb, '\\0');\n>      ++\t\tstrbuf_stripspace(&sb, NULL);\n>      +\n>      + \tif (signoff)\n>      + \t\tappend_signoff(&sb, ignored_log_message_bytes(sb.buf, sb.len), 0);\n>      +\n>        ## builtin/notes.c ##\n>       @@ builtin/notes.c: static void prepare_note_data(const struct object_id *object, struct note_data *\n>        \t\t\tdie(_(\"please supply the note contents using either -m or -F option\"));\n>      @@ builtin/notes.c: static void prepare_note_data(const struct object_id *object, s\n>        \t}\n>        }\n>        \n>      +@@ builtin/notes.c: static void concat_messages(struct note_data *d)\n>      + \t\tif ((d->stripspace == UNSPECIFIED &&\n>      + \t\t     d->messages[i]->stripspace == STRIPSPACE) ||\n>      + \t\t    d->stripspace == STRIPSPACE)\n>      +-\t\t\tstrbuf_stripspace(&d->buf, 0);\n>      ++\t\t\tstrbuf_stripspace(&d->buf, NULL);\n>      + \t\tstrbuf_reset(&msg);\n>      + \t}\n>      + \tstrbuf_release(&msg);\n>       \n>        ## builtin/rebase.c ##\n>       @@ builtin/rebase.c: static int edit_todo_file(unsigned flags)\n>      @@ builtin/tag.c: static void create_tag(const struct object_id *object, const char\n>        \tif (!opt->message_given && !buf->len)\n>        \t\tdie(_(\"no tag message?\"));\n>       \n>      + ## builtin/worktree.c ##\n>      +@@ builtin/worktree.c: static int can_use_local_refs(const struct add_opts *opts)\n>      + \t\t\tstrbuf_add_real_path(&path, get_worktree_git_dir(NULL));\n>      + \t\t\tstrbuf_addstr(&path, \"/HEAD\");\n>      + \t\t\tstrbuf_read_file(&contents, path.buf, 64);\n>      +-\t\t\tstrbuf_stripspace(&contents, 0);\n>      ++\t\t\tstrbuf_stripspace(&contents, NULL);\n>      + \t\t\tstrbuf_strip_suffix(&contents, \"\\n\");\n>      +\n>      + \t\t\twarning(_(\"HEAD points to an invalid (or orphaned) reference.\\n\"\n>      +\n>      + ## gpg-interface.c ##\n>      +@@ gpg-interface.c: static int verify_ssh_signed_buffer(struct signature_check *sigc,\n>      + \t\t}\n>      + \t}\n>      +\n>      +-\tstrbuf_stripspace(&ssh_keygen_out, '\\0');\n>      +-\tstrbuf_stripspace(&ssh_keygen_err, '\\0');\n>      ++\tstrbuf_stripspace(&ssh_keygen_out, NULL);\n>      ++\tstrbuf_stripspace(&ssh_keygen_err, NULL);\n>      + \t/* Add stderr outputs to show the user actual ssh-keygen errors */\n>      + \tstrbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);\n>      + \tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n>      +\n>        ## rebase-interactive.c ##\n>       @@ rebase-interactive.c: int edit_todo_list(struct repository *r, struct todo_list *todo_list,\n>        \tif (launch_sequence_editor(todo_file, &new_todo->buf, NULL))\n>   7:  bb22f9c9c5 =  8:  a271207e48 strbuf: accept a comment string for strbuf_commented_addf()\n>   8:  8d20688e87 =  9:  c1831453d8 strbuf: accept a comment string for strbuf_add_commented_lines()\n>   9:  4b22efb941 = 10:  523eb9e534 prefer comment_line_str to comment_line_char for printing\n> 10:  cd03310902 = 11:  85428eadaa find multi-byte comment chars in NUL-terminated strings\n> 11:  13a346480e ! 12:  b9e2e2302d find multi-byte comment chars in unterminated buffers\n>      @@ trailer.c: static size_t find_trailer_block_start(const char *buf, size_t len)\n>        \t\tssize_t separator_pos;\n>        \n>       -\t\tif (bol[0] == comment_line_char) {\n>      -+\t\tif (starts_with_mem(bol, buf + end_of_title - bol, comment_line_str)) {\n>      ++\t\tif (starts_with_mem(bol, buf + len - bol, comment_line_str)) {\n>        \t\t\tnon_trailer_lines += possible_continuation_lines;\n>        \t\t\tpossible_continuation_lines = 0;\n>        \t\t\tcontinue;\n> 12:  fb3c6659fc ! 13:  7661ca6306 sequencer: handle multi-byte comment characters when writing todo list\n>      @@ Commit message\n>           We can't just return comment_line_char anymore, since it may require\n>           multiple bytes. Instead, we'll return \"0\" for this case, which is the\n>           same thing we'd return for a command which does not have a single-letter\n>      -    abbreviation (e.g., \"revert\" or \"noop\"). In that case the caller then\n>      -    falls back to outputting the full name via command_to_string(). So we\n>      -    can handle TODO_COMMENT there, returning the full string.\n>      +    abbreviation (e.g., \"revert\" or \"noop\"). There is only a single caller\n>      +    of command_to_char(), and upon seeing \"0\" it falls back to outputting\n>      +    the full name via command_to_string(). So we can handle TODO_COMMENT\n>      +    there, returning the full string.\n>       \n>           Note that there are many other callers of command_to_string(), which\n>           will now behave differently if they pass TODO_COMMENT. But we would not\n> 13:  94524b8817 = 14:  8ddab67432 wt-status: drop custom comment-char stringification\n> 14:  d754e86f7b = 15:  16d65f9179 environment: drop comment_line_char compatibility macro\n> 15:  a6ffe08469 ! 16:  461cc720a0 config: allow multi-byte core.commentChar\n>      @@ Commit message\n>           how each site behaves. In the interim let's forbid it and we can loosen\n>           things later.\n>       \n>      +    Likewise, the \"commentChar cannot be a newline\" rule is now extended to\n>      +    \"it cannot contain a newline\" (for the same reason: it can confuse our\n>      +    parsing loops).\n>      +\n>           Since comment_line_str is used in many parts of the code, it's hard to\n>           cover all possibilities with tests. We can convert the existing\n>           double-semicolon prefix test to show that \"git status\" works. And we'll\n>      @@ config.c: static int git_default_core_config(const char *var, const char *value,\n>        \t\telse if (!strcasecmp(value, \"auto\"))\n>        \t\t\tauto_comment_line_char = 1;\n>       -\t\telse if (value[0] && !value[1]) {\n>      +-\t\t\tif (value[0] == '\\n')\n>      +-\t\t\t\treturn error(_(\"core.commentChar cannot be newline\"));\n>       -\t\t\tcomment_line_str = xstrfmt(\"%c\", value[0]);\n>       +\t\telse if (value[0]) {\n>      ++\t\t\tif (strchr(value, '\\n'))\n>      ++\t\t\t\treturn error(_(\"core.commentChar cannot contain newline\"));\n>       +\t\t\tcomment_line_str = xstrdup(value);\n>        \t\t\tauto_comment_line_char = 0;\n>        \t\t} else\n>      @@ config.c: static int git_default_core_config(const char *var, const char *value,\n>       \n>        ## t/t0030-stripspace.sh ##\n>       @@ t/t0030-stripspace.sh: test_expect_success 'strip comments with changed comment char' '\n>      - \ttest -z \"$(echo \"; comment\" | git -c core.commentchar=\";\" stripspace -s)\"\n>      - '\n>        \n>      + test_expect_success 'newline as commentchar is forbidden' '\n>      + \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n>      +-\tgrep \"core.commentChar cannot be newline\" err\n>      ++\tgrep \"core.commentChar cannot contain newline\" err\n>      ++'\n>      ++\n>       +test_expect_success 'empty commentchar is forbidden' '\n>       +\ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n>       +\tgrep \"core.commentChar must have at least one character\" err\n>      -+'\n>      -+\n>      + '\n>      +\n>        test_expect_success '-c with single line' '\n>      - \tprintf \"# foo\\n\" >expect &&\n>      - \tprintf \"foo\" | git stripspace -c >actual &&\n>       \n>        ## t/t7507-commit-verbose.sh ##\n>       @@ t/t7507-commit-verbose.sh: test_expect_success 'verbose diff is stripped out with set core.commentChar' '\n> \n>    [01/16]: config: forbid newline as core.commentChar\n>    [02/16]: strbuf: simplify comment-handling in add_lines() helper\n>    [03/16]: strbuf: avoid static variables in strbuf_add_commented_lines()\n>    [04/16]: commit: refactor base-case of adjust_comment_line_char()\n>    [05/16]: strbuf: avoid shadowing global comment_line_char name\n>    [06/16]: environment: store comment_line_char as a string\n>    [07/16]: strbuf: accept a comment string for strbuf_stripspace()\n>    [08/16]: strbuf: accept a comment string for strbuf_commented_addf()\n>    [09/16]: strbuf: accept a comment string for strbuf_add_commented_lines()\n>    [10/16]: prefer comment_line_str to comment_line_char for printing\n>    [11/16]: find multi-byte comment chars in NUL-terminated strings\n>    [12/16]: find multi-byte comment chars in unterminated buffers\n>    [13/16]: sequencer: handle multi-byte comment characters when writing todo list\n>    [14/16]: wt-status: drop custom comment-char stringification\n>    [15/16]: environment: drop comment_line_char compatibility macro\n>    [16/16]: config: allow multi-byte core.commentChar\n> \n>   Documentation/config/core.txt |  4 ++-\n>   add-patch.c                   | 14 +++++-----\n>   builtin/am.c                  |  2 +-\n>   builtin/branch.c              |  8 +++---\n>   builtin/commit.c              | 21 +++++++--------\n>   builtin/merge.c               | 12 ++++-----\n>   builtin/notes.c               | 12 ++++-----\n>   builtin/rebase.c              |  2 +-\n>   builtin/stripspace.c          |  4 +--\n>   builtin/tag.c                 | 14 +++++-----\n>   builtin/worktree.c            |  2 +-\n>   commit.c                      |  3 ++-\n>   config.c                      |  8 +++---\n>   environment.c                 |  2 +-\n>   environment.h                 |  2 +-\n>   fmt-merge-msg.c               |  8 +++---\n>   gpg-interface.c               |  4 +--\n>   rebase-interactive.c          | 10 ++++----\n>   sequencer.c                   | 48 ++++++++++++++++++-----------------\n>   strbuf.c                      | 47 ++++++++++++++++++----------------\n>   strbuf.h                      |  9 ++++---\n>   t/t0030-stripspace.sh         | 10 ++++++++\n>   t/t7507-commit-verbose.sh     | 10 ++++++++\n>   t/t7508-status.sh             |  4 ++-\n>   trailer.c                     |  6 ++---\n>   wt-status.c                   | 31 +++++++++-------------\n>   26 files changed, 162 insertions(+), 135 deletions(-)\n> \n> -Peff\n"},{"id":"490501","messageId":"xmqqsf0vjiqk.fsf@gitster.g","threadId":"61052","inReplyTo":"7cc7d539-7f31-4cd8-bcc8-a98a67a5079f@gmail.com","subject":"Re: [PATCH v2 0/16] allow multi-byte core.commentChar","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-12T20:30:27Z","receivedAt":"2024-03-12T20:30:33Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"phillip.wood123@gmail.com writes:\n\n> Looking through the range-diff it addresses all of my (sequencer\n> focused) comments on v1.\n\nThanks, both.\n"},{"id":"490529","messageId":"20240313062321.GA125150@coredump.intra.peff.net","threadId":"61052","inReplyTo":"9b63eab9-7b6d-4024-9ec4-b58c5ca3b084@gmail.com","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-13T06:23:21Z","receivedAt":"2024-03-13T06:23:23Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Mar 12, 2024 at 02:36:36PM +0000, phillip.wood123@gmail.com wrote:\n\n> > Leading whitespace actually does work, though I think you'd be slightly\n> > insane to use it.\n> \n> For \"git rebase\" in only works if you edit the todo list with \"git rebase\n> --edit-todo\" which calls strbuf_stripspace() and therefore parse_insn_line()\n> never sees the comments. If you edit the todo list directly then it will\n> error out. You can see this with\n> \n>     git -c core.commentChar=' ' rebase -x 'echo \" this is a comment\"\n> >>\"$(git rev-parse --git-path rebase-merge/git-rebase-todo)\"' HEAD^\n> \n> which successfully picks HEAD but then gives\n> \n>     error: invalid command 'this'\n> \n> when it tries to parse the todo list after the exec command is run. Given it\n> is broken already I'm not sure we should worry about it here. In any case it\n> is not clear how much we should worry about problems caused by users editing\n> the todo list without using \"git rebase --edit-todo\". There is code in\n> parse_insn_line() which is clearly there to handle direct editing of the\n> file but I don't think it is tested and directly editing the file probably\n> bypasses the rebase.missingCommitsCheck checks as well.\n\nAh, thanks for the example. I guess it's not too surprising that it can\ncause confusion. Given that it's an existing issue, I think my\npreference would be to leave it out of the series under discussion\n(given how long and complicated it is already), but I'd have no\nobjection to tightening things further on top as a separate series.\n\n-Peff\n"},{"id":"490568","messageId":"0426f7bf-6032-4fc7-886c-c4278c6e105b@app.fastmail.com","threadId":"61052","inReplyTo":"20240312091750.GP95609@coredump.intra.peff.net","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-13T18:23:25Z","receivedAt":"2024-03-13T18:24:37Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"Thanks for your work on this. Now I can use dingbats as my comment char.\n\nOn Tue, Mar 12, 2024, at 10:17, Jeff King wrote:\n> diff --git a/Documentation/config/core.txt\n> b/Documentation/config/core.txt\n> index 0e8c2832bf..c86b8c8408 100644\n> --- a/Documentation/config/core.txt\n> +++ b/Documentation/config/core.txt\n> @@ -523,7 +523,9 @@ core.commentChar::\n>  \tCommands such as `commit` and `tag` that let you edit\n>  \tmessages consider a line that begins with this character\n>  \tcommented, and removes them after the editor returns\n> -\t(default '#').\n> +\t(default '#'). Note that this option can take values larger than\n> +\ta byte (whether a single multi-byte character, or you\n> +\tcould even go wild with a multi-character sequence).\n\nI don’t know if this expanded description focuses a bit much on the\nhistory of the change[1] or if it is intentionally indirect about this\nchar-is-really-a-string behavior as a sort of easter egg.[2]\n\nMaybe it could be more directly stated like:\n\n  “ Note that this variable can in fact be a string like `foo`; it\n    doesn’t have to be a single character.\n\n(Hopefully UTF-8 is implied by “foo”. Or else “føø”.)\n\nTerms like “a byte” and “multi-byte characters” seem a bit too technical\nin this context when you can just say “string”.\n\n† 1: (1) What’s a “char”, is it ASCII? (2) It’s ASCII but could in\n    principle be made multi-byte (3) And also a multi-byte *string*,\n    right? (4) …\n† 2: In five years: (1) How come this Git tutorial’s commit message\n    template has `(commit)` as the ignore-these-lines marker? How did he\n    abuse “comment char” to make a long string? (2) Actually…\n\n❦ Please enter the email reply. Lines starting with '❦' will be ignored,\n❦ and an empty message aborts the sendout.\n\n-- \nKristoffer Haugsbakk\n"},{"id":"490571","messageId":"xmqqle6mascv.fsf@gitster.g","threadId":"61052","inReplyTo":"0426f7bf-6032-4fc7-886c-c4278c6e105b@app.fastmail.com","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-13T18:39:44Z","receivedAt":"2024-03-13T18:39:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Kristoffer Haugsbakk\" <code@khaugsbakk.name> writes:\n\n>> +\t(default '#'). Note that this option can take values larger than\n>> +\ta byte (whether a single multi-byte character, or you\n>> +\tcould even go wild with a multi-character sequence).\n>\n> I don’t know if this expanded description focuses a bit much on the\n> history of the change[1] or if it is intentionally indirect about this\n> char-is-really-a-string behavior as a sort of easter egg.[2]\n\n> Maybe it could be more directly stated like:\n>\n>   “ Note that this variable can in fact be a string like `foo`; it\n>     doesn’t have to be a single character.\n>\n> (Hopefully UTF-8 is implied by “foo”. Or else “føø”.)\n\nThat's definitely an improvement, but I would say that using a\ndingbat instad of \"foo\", and \"single character\" -> \"single ASCII\ncharacter\" (or \"single byte\") would make it even clearer.\n\nThanks.\n"},{"id":"490643","messageId":"89e2369a-9d9b-4621-8311-77e2deb3e5f7@web.de","threadId":"61052","inReplyTo":"20240312080525.GB47852@coredump.intra.peff.net","subject":"Re: [PATCH 11/15] find multi-byte comment chars in unterminated buffers","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2024-03-14T19:37:45Z","receivedAt":"2024-03-14T19:38:08Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 12.03.24 um 09:05 schrieb Jeff King:\n> On Thu, Mar 07, 2024 at 08:42:22PM +0100, René Scharfe wrote:\n>\n>>> @@ -2562,7 +2562,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,\n>>>  \t/* left-trim */\n>>>  \tbol += strspn(bol, \" \\t\");\n>>>\n>>> -\tif (bol == eol || *bol == '\\r' || *bol == comment_line_char) {\n>>> +\tif (bol == eol || *bol == '\\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {\n>>\n>> If the strspn() call is safe (which it is, as the caller expects the\n>> string to be NUL-terminated) then you could use starts_with() here and\n>> avoid the length calculation.  But that would also match\n>> comment_line_str values that contain LF, which the _mem version does not\n>> and that's better.\n>\n> I try not to read too much into the use of string functions on what\n> otherwise appears to be an unterminated buffer. While in Git it is quite\n> often terminated at allocation time (coming from a strbuf, etc) I feel\n> like I've fixed a number of out-of-bounds reads simply due to sloppy\n> practices. And even if something is correct today, it is easy for it to\n> change, since the assumption is made far away from allocation.\n>\n> So I dunno. Like you said, fewer computations is fewer opportunity to\n> mess things up. I don't like the idea of introducing a new hand-grenade\n> that might blow up later, but maybe if it's right next to a strspn()\n> call that's already a problem, it's not materially making anything\n> worse.\n\nYeah, and my logic was flawed: If the caller somehow guarantees that a\nspace or tab occurs before eol then the strspn() call is safe.  Its\npresence doesn't guarantee NUL termination.  parse_insn_line() would\nnot be safe to use without that prerequisite, but that's a different\nmatter..\n\n>>> @@ -882,7 +882,7 @@ static size_t find_trailer_block_start(const char *buf, size_t len)\n>>>\n>>>  \t/* The first paragraph is the title and cannot be trailers */\n>>>  \tfor (s = buf; s < buf + len; s = next_line(s)) {\n>>> -\t\tif (s[0] == comment_line_char)\n>>> +\t\tif (starts_with_mem(s, buf + len - s, comment_line_str))\n>>>  \t\t\tcontinue;\n>>>  \t\tif (is_blank_line(s))\n>>\n>> Another case where starts_with() would be safe to use, as\n>> is_blank_line() expects (and gets) a NUL-terminated string, but it would\n>> allow matching comment_line_str values that contain LF.\n>\n> Hmm. Yes, it is a NUL-terminated string always, but the caller has told\n> us not to look past end_of_log_message(). I suspect that if there is no\n> newline in comment_line_str() it's probably impossible to go past \"len\"\n> (just because the end of the log surely ends with either a NUL or a\n> newline). But it feels iffy to me. I dunno.\n\nSame flawed thinking on my part: As long as we're guaranteed a blank\nline in the buffer we won't walk past its end.  That doesn't mean we can\nassume a NUL is present.  But that's fragile.  The code should use\nmemchr() instead of strchrnul().\n\nThat's not the problem you set out to solve in your series, though, and\nyou avoid making it worse by respecting the length limit in the code\nyou change.  #leftoverbits\n\nKeeping track of the remaining length increases code size and adds\nopportunities for mistakes.  Not sure how to avoid it, however.  Using\neol instead of len at least avoids subtractions.\n\ntl;dr: Good patch (in v2).\n\nRené\n"},{"id":"490666","messageId":"20240315055944.GB1741107@coredump.intra.peff.net","threadId":"61052","inReplyTo":"0426f7bf-6032-4fc7-886c-c4278c6e105b@app.fastmail.com","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-15T05:59:44Z","receivedAt":"2024-03-15T05:59:45Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Mar 13, 2024 at 07:23:25PM +0100, Kristoffer Haugsbakk wrote:\n\n> Thanks for your work on this. Now I can use dingbats as my comment char.\n\nTruly we have entered a golden age of technology. ;)\n\n> > @@ -523,7 +523,9 @@ core.commentChar::\n> >  \tCommands such as `commit` and `tag` that let you edit\n> >  \tmessages consider a line that begins with this character\n> >  \tcommented, and removes them after the editor returns\n> > -\t(default '#').\n> > +\t(default '#'). Note that this option can take values larger than\n> > +\ta byte (whether a single multi-byte character, or you\n> > +\tcould even go wild with a multi-character sequence).\n> \n> I don’t know if this expanded description focuses a bit much on the\n> history of the change[1] or if it is intentionally indirect about this\n> char-is-really-a-string behavior as a sort of easter egg.[2]\n\nMostly I was worried that people would take \"char\" in the name to assume\nit could only be a single byte (I had originally even started the new\nsentence with \"Despite the word 'char' in the name, this option\ncan...\"). And that is not just history, but a name we are stuck with\nforever[1].\n\nCertainly \"char\" is an ambiguous term, though. I didn't mean to leave\nchar-is-a-string as an easter egg; that's what I meant by\n\"multi-character sequence\". Certainly \"string\" is a shorter way of\nsaying that. ;) But I wasn't sure its meaning would be obvious without\nthe word \"multi-character\". Giving an example as you suggested does\nhelp that.\n\nThat said...\n\n> Maybe it could be more directly stated like:\n> \n>   “ Note that this variable can in fact be a string like `foo`; it\n>     doesn’t have to be a single character.\n\nI actually do think the \"string\" nature is mostly uninteresting, and I'd\nbe OK leaving it as an easter egg. What your suggestion doesn't say is\nthat multi-byte characters are OK. But if we think people will just\nassume that in a modern UTF-8 world, then maybe we don't need to say\nanything at all?\n\n> (Hopefully UTF-8 is implied by “foo”. Or else “føø”.)\n\nIt actually does not have to be UTF-8. If you use an alternate encoding\nin your editor (and probably set i18n.commitEncoding to match), I think\neverything might just work. (Though to be clear, I think anybody using\nnon-UTF8 in 2024 deserves our pity either for being crazy or for being\nstuck working on an antiquated system).\n\n-Peff\n"},{"id":"490676","messageId":"6be335ed-8598-406c-b535-2e58554b00e9@app.fastmail.com","threadId":"61052","inReplyTo":"20240315055944.GB1741107@coredump.intra.peff.net","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-15T07:16:53Z","receivedAt":"2024-03-15T07:17:15Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Fri, Mar 15, 2024, at 06:59, Jeff King wrote:\n> On Wed, Mar 13, 2024 at 07:23:25PM +0100, Kristoffer Haugsbakk wrote:\n>\n>> Thanks for your work on this. Now I can use dingbats as my comment char.\n>\n> Truly we have entered a golden age of technology. ;)\n\nQoL features can in aggregate have a surprising impact :)\n\n>\n>> > @@ -523,7 +523,9 @@ core.commentChar::\n>> >  \tCommands such as `commit` and `tag` that let you edit\n>> >  \tmessages consider a line that begins with this character\n>> >  \tcommented, and removes them after the editor returns\n>> > -\t(default '#').\n>> > +\t(default '#'). Note that this option can take values larger than\n>> > +\ta byte (whether a single multi-byte character, or you\n>> > +\tcould even go wild with a multi-character sequence).\n>>\n>> I don’t know if this expanded description focuses a bit much on the\n>> history of the change[1] or if it is intentionally indirect about this\n>> char-is-really-a-string behavior as a sort of easter egg.[2]\n>\n> Mostly I was worried that people would take \"char\" in the name to assume\n> it could only be a single byte (I had originally even started the new\n> sentence with \"Despite the word 'char' in the name, this option\n> can...\"). And that is not just history, but a name we are stuck with\n> forever[1].\n\nMissing footnote or referring to my footnote?\n\nMy suggestion was to use a `core.commentString` alias. Which might\nmatter for new answers to questions about its use. It might not matter\nif in practice most people get their config tips from 1500 point\nStackOverflow question about how git-commit(1) keeps swallowing their\nGitHub issue numbers (due to automatic linewrap) from 2011.\n\n> Certainly \"char\" is an ambiguous term, though. I didn't mean to leave\n> char-is-a-string as an easter egg; that's what I meant by\n> \"multi-character sequence\". Certainly \"string\" is a shorter way of\n> saying that. ;) But I wasn't sure its meaning would be obvious without\n> the word \"multi-character\". Giving an example as you suggested does\n> help that.\n>\n> That said...\n>\n>> Maybe it could be more directly stated like:\n>>\n>>   “ Note that this variable can in fact be a string like `foo`; it\n>>     doesn’t have to be a single character.\n>\n> I actually do think the \"string\" nature is mostly uninteresting, and I'd\n> be OK leaving it as an easter egg.\n\nTo my mind a string subsumes a char (multi- or not). Like in programming\nlanguages: some might be used to single-char `#`, but I don’t think they\ndo a double take when they see languages with `//` or `--`.\n\n> What your suggestion doesn't say is that multi-byte characters are\n> OK. But if we think people will just assume that in a modern UTF-8\n> world, then maybe we don't need to say anything at all?\n\nGiven that we’re mostly in the context of a commit message, an\nASCII-only restriction would feel archaic.\n\nI guess it depends on what the *normal* is in the documentation at\nlarge. As a user I’m used to Git handling the text that I give it.\n\n> It actually does not have to be UTF-8.\n\nGood point. Unicode is more appropriate.\n\n> (Though to be clear, I think anybody using non-UTF8 in 2024 deserves\n> our pity either for being crazy or for being stuck working on an\n> antiquated system).\n\nI honestly feel blessed that I have to worry so little about text\nencoding.\n\n-- \nKristoffer Haugsbakk\n\n"},{"id":"490678","messageId":"20240315081041.GA1753560@coredump.intra.peff.net","threadId":"61052","inReplyTo":"6be335ed-8598-406c-b535-2e58554b00e9@app.fastmail.com","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-15T08:10:41Z","receivedAt":"2024-03-15T08:10:42Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 15, 2024 at 08:16:53AM +0100, Kristoffer Haugsbakk wrote:\n\n> > Mostly I was worried that people would take \"char\" in the name to assume\n> > it could only be a single byte (I had originally even started the new\n> > sentence with \"Despite the word 'char' in the name, this option\n> > can...\"). And that is not just history, but a name we are stuck with\n> > forever[1].\n> \n> Missing footnote or referring to my footnote?\n> \n> My suggestion was to use a `core.commentString` alias. Which might\n> matter for new answers to questions about its use. It might not matter\n> if in practice most people get their config tips from 1500 point\n> StackOverflow question about how git-commit(1) keeps swallowing their\n> GitHub issue numbers (due to automatic linewrap) from 2011.\n\nHeh, missing footnote. I was going to say \"we could introduce\ncore.commentStr or similar\", but after your comment I searched in the\narchive and see that you did indeed already suggest it.\n\nI'm not sure if it would make things more or less confusing to have two\nrelated values. One nice side effect is that the new variable would be\nignored by older versions of Git (whereas by extending core.commentChar,\nyou end up with config that causes older versions to barf). That\nprobably doesn't matter that much for most users, but as somebody who\nworks on Git I frequently run old versions for bug testing, bisection,\nand so forth.\n\n> > I actually do think the \"string\" nature is mostly uninteresting, and I'd\n> > be OK leaving it as an easter egg.\n> \n> To my mind a string subsumes a char (multi- or not). Like in programming\n> languages: some might be used to single-char `#`, but I don’t think they\n> do a double take when they see languages with `//` or `--`.\n\nHmm, good point. I was mostly focused on UTF-8 characters, but \"//\" is\nquite a reasonable thing for people to try. It is probably a better\nexample than \"foo\".\n\n> > What your suggestion doesn't say is that multi-byte characters are\n> > OK. But if we think people will just assume that in a modern UTF-8\n> > world, then maybe we don't need to say anything at all?\n> \n> Given that we’re mostly in the context of a commit message, an\n> ASCII-only restriction would feel archaic.\n>\n> I guess it depends on what the *normal* is in the documentation at\n> large. As a user I’m used to Git handling the text that I give it.\n\nRight, that's what I was asking. To me \"character\" means an ASCII byte,\nbut I think I might be archaic myself. ;) If most of our readers would\njust assume that multi-byte characters work, perhaps it is confusing\nthings to even mention it.\n\n> > It actually does not have to be UTF-8.\n> \n> Good point. Unicode is more appropriate.\n\nI think other Unicode encodings are likely to have problems (because\nthey embed NULs). Specifically I was thinking that you could probably\nget away with latin1 or other 8-bit encodings. But again, I really hope\nnobody is doing that anymore.\n\nSo anyway, adapting your original suggestion based on discussion in the\nthread, maybe squash in (to the final patch):\n\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex c86b8c8408..c5a8033df9 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -523,9 +523,8 @@ core.commentChar::\n \tCommands such as `commit` and `tag` that let you edit\n \tmessages consider a line that begins with this character\n \tcommented, and removes them after the editor returns\n-\t(default '#'). Note that this option can take values larger than\n-\ta byte (whether a single multi-byte character, or you\n-\tcould even go wild with a multi-character sequence).\n+\t(default '#'). Note that this variable can be a string like\n+\t`//` or `⁑⁕⁑`; it doesn't have to be a single ASCII character.\n +\n If set to \"auto\", `git-commit` would select a character that is not\n the beginning character of any line in existing commit messages.\n\n\nThat's assuming we don't want to go the commentString route, which would\nrequire a bit more re-working of the patch. I'm also open to a more\nclever or pretty multi-byte example if we have one. ;)\n\n-Peff\n"},{"id":"490689","messageId":"c3128984-3426-4d2a-bbd4-4d94ad43682e@app.fastmail.com","threadId":"61052","inReplyTo":"20240315081041.GA1753560@coredump.intra.peff.net","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-15T13:30:18Z","receivedAt":"2024-03-15T13:30:41Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"> diff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\n> index c86b8c8408..c5a8033df9 100644\n> --- a/Documentation/config/core.txt\n> +++ b/Documentation/config/core.txt\n> @@ -523,9 +523,8 @@ core.commentChar::\n>  \tCommands such as `commit` and `tag` that let you edit\n>  \tmessages consider a line that begins with this character\n>  \tcommented, and removes them after the editor returns\n> -\t(default '#'). Note that this option can take values larger than\n> -\ta byte (whether a single multi-byte character, or you\n> -\tcould even go wild with a multi-character sequence).\n> +\t(default '#'). Note that this variable can be a string like\n> +\t`//` or `⁑⁕⁑`; it doesn't have to be a single ASCII character.\n\nThis is perfect :)\n\n-- \nKristoffer Haugsbakk\n\n"},{"id":"490691","messageId":"xmqqr0gbtsdz.fsf@gitster.g","threadId":"61052","inReplyTo":"20240315081041.GA1753560@coredump.intra.peff.net","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-15T15:40:56Z","receivedAt":"2024-03-15T15:41:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> +\t(default '#'). Note that this variable can be a string like\n> +\t`//` or `⁑⁕⁑`; it doesn't have to be a single ASCII character.\n\nLooking good.\n\n> That's assuming we don't want to go the commentString route, which would\n> require a bit more re-working of the patch. I'm also open to a more\n> clever or pretty multi-byte example if we have one. ;)\n\nAdding core.commentString can be done long after the dust settles\nand I would expect that most of the changes in the patch would not\nhave to be updated.  The parts that use comment_line_str variable do\nnot have to change, the documentation needs \"core.commentString is a\nsynonym for core.commentChar, the latter of which is understood by\nolder versions of Git (but they may use only the first byte of the\nstring)\" or something, but other than that, the existing text after\nthis patch does not have to be updated.  If we add a proper synonym\nsupport to the config machinery, that would be a sizable project,\nbut otherwise it would be just another \"if (!strcmp()) var = val\".\n\nStepping back a bit, one thing that we do need to mention in this\nround is what happens when you use multi-byte sequence and have it\naccessed by existing versions of Git.  \"use only the first byte\" I\nwrote above came out of thin air without experimenting or reading\nthe code, but something like that ought to be part of the \"Note\nthat\" paragraph above.\n\n\t(default '#'). Note that this variable can be a string like\n\t`//` or `⁑⁕⁑`; it doesn't have to be a single ASCII character.\n\tAlso note that older versions of Git used only the first byte\n\t(not necessarily a character) of the value of this variable,\n\tso you may want to be careful if you plan to use versions of\n\tGit older than 2.45.\n\nor something like that, perhaps.\n\n\n\n\n"},{"id":"490761","messageId":"20240316055013.GA32145@coredump.intra.peff.net","threadId":"61052","inReplyTo":"xmqqr0gbtsdz.fsf@gitster.g","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-16T05:50:13Z","receivedAt":"2024-03-16T05:56:56Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Mar 15, 2024 at 08:40:56AM -0700, Junio C Hamano wrote:\n\n> > That's assuming we don't want to go the commentString route, which would\n> > require a bit more re-working of the patch. I'm also open to a more\n> > clever or pretty multi-byte example if we have one. ;)\n> \n> Adding core.commentString can be done long after the dust settles\n> and I would expect that most of the changes in the patch would not\n> have to be updated.  The parts that use comment_line_str variable do\n> not have to change, the documentation needs \"core.commentString is a\n> synonym for core.commentChar, the latter of which is understood by\n> older versions of Git (but they may use only the first byte of the\n> string)\" or something, but other than that, the existing text after\n> this patch does not have to be updated.  If we add a proper synonym\n> support to the config machinery, that would be a sizable project,\n> but otherwise it would be just another \"if (!strcmp()) var = val\".\n\nYeah, I agree we could add core.commentString on top of what's here, as\nlong as we're OK with core.commentChar starting to accept strings in the\nmeantime. Which is probably reasonable, and in which case the code\nportion of the patch really is just:\n\ndiff --git a/config.c b/config.c\nindex 92c752ed9f..13fb922bf5 100644\n--- a/config.c\n+++ b/config.c\n@@ -1560,7 +1560,8 @@ static int git_default_core_config(const char *var, const char *value,\n \tif (!strcmp(var, \"core.editor\"))\n \t\treturn git_config_string(&editor_program, var, value);\n \n-\tif (!strcmp(var, \"core.commentchar\")) {\n+\tif (!strcmp(var, \"core.commentchar\") ||\n+\t    !strcmp(var, \"core.commentstring\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n \t\telse if (!strcasecmp(value, \"auto\"))\n\n(the real work of course being in docs and tests).\n\nIf we wanted to distinguish them more (say, core.commentChar remains\nas-is but core.commentString allows strings and takes precedence), then\nwe'd need to do it now to avoid flip-flopping between versions. I don't\nsee a huge benefit in restricting commentChar though.\n\n> Stepping back a bit, one thing that we do need to mention in this\n> round is what happens when you use multi-byte sequence and have it\n> accessed by existing versions of Git.  \"use only the first byte\" I\n> wrote above came out of thin air without experimenting or reading\n> the code, but something like that ought to be part of the \"Note\n> that\" paragraph above.\n> \n> \t(default '#'). Note that this variable can be a string like\n> \t`//` or `⁑⁕⁑`; it doesn't have to be a single ASCII character.\n> \tAlso note that older versions of Git used only the first byte\n> \t(not necessarily a character) of the value of this variable,\n> \tso you may want to be careful if you plan to use versions of\n> \tGit older than 2.45.\n\nThe current code barfs for anything larger than a byte:\n\n  $ git.v2.44.0 -c core.commentchar=foo stripspace -s\n  error: core.commentChar should only be one ASCII character\n  fatal: unable to parse 'core.commentchar' from command-line config\n\nI'm mixed on these sorts of version-specific notes in the documentation.\nFor people who aren't mixing versions, the history is useless noise\n(whose value decreases as time goes on and 2.45 becomes \"old\" itself).\nFor people who do use older versions, they'd quickly get an error like\nthe one above.\n\nSo I dunno. I'm not strictly opposed, but if this is something we think\nis worth warning about, then that implies to me that it is worth\nproviding a more ergonomic solution like core.commentString.\n\n-Peff\n"},{"id":"491616","messageId":"xmqq8r247igg.fsf@gitster.g","threadId":"61052","inReplyTo":"20240315081041.GA1753560@coredump.intra.peff.net","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-26T22:10:23Z","receivedAt":"2024-03-26T22:10:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> So anyway, adapting your original suggestion based on discussion in the\n> thread, maybe squash in (to the final patch):\n>\n> diff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\n> index c86b8c8408..c5a8033df9 100644\n> --- a/Documentation/config/core.txt\n> +++ b/Documentation/config/core.txt\n> @@ -523,9 +523,8 @@ core.commentChar::\n>  \tCommands such as `commit` and `tag` that let you edit\n>  \tmessages consider a line that begins with this character\n>  \tcommented, and removes them after the editor returns\n> -\t(default '#'). Note that this option can take values larger than\n> -\ta byte (whether a single multi-byte character, or you\n> -\tcould even go wild with a multi-character sequence).\n> +\t(default '#'). Note that this variable can be a string like\n> +\t`//` or `⁑⁕⁑`; it doesn't have to be a single ASCII character.\n>  +\n>  If set to \"auto\", `git-commit` would select a character that is not\n>  the beginning character of any line in existing commit messages.\n>\n>\n> That's assuming we don't want to go the commentString route, which would\n> require a bit more re-working of the patch. I'm also open to a more\n> clever or pretty multi-byte example if we have one. ;)\n\nIt has been 10 days since this discussion petered out.\n\nMy preference is to introduce core.commentString to avoid confusion\ncoming from an older Git using the first-byte of a multi-byte\nstring, or dying upon reading a configuration file meant for a newer\nGit, and then let core.commentString override core.commentChar, but\nI would prefer to see the discussion participants to raise their\nopinions and reach a conclusion.\n\nThanks.\n"},{"id":"491617","messageId":"aef18cfa-e5fb-4575-9d9b-095ada1d03ef@app.fastmail.com","threadId":"61052","inReplyTo":"xmqq8r247igg.fsf@gitster.g","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-26T22:12:36Z","receivedAt":"2024-03-26T22:12:59Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Tue, Mar 26, 2024, at 23:10, Junio C Hamano wrote:\n> Jeff King <peff@peff.net> writes:\n>\n>> So anyway, adapting your original suggestion based on discussion in the\n>> thread, maybe squash in (to the final patch):\n>>\n>> diff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\n>> index c86b8c8408..c5a8033df9 100644\n>> --- a/Documentation/config/core.txt\n>> +++ b/Documentation/config/core.txt\n>> @@ -523,9 +523,8 @@ core.commentChar::\n>>  \tCommands such as `commit` and `tag` that let you edit\n>>  \tmessages consider a line that begins with this character\n>>  \tcommented, and removes them after the editor returns\n>> -\t(default '#'). Note that this option can take values larger than\n>> -\ta byte (whether a single multi-byte character, or you\n>> -\tcould even go wild with a multi-character sequence).\n>> +\t(default '#'). Note that this variable can be a string like\n>> +\t`//` or `⁑⁕⁑`; it doesn't have to be a single ASCII character.\n>>  +\n>>  If set to \"auto\", `git-commit` would select a character that is not\n>>  the beginning character of any line in existing commit messages.\n>>\n>>\n>> That's assuming we don't want to go the commentString route, which would\n>> require a bit more re-working of the patch. I'm also open to a more\n>> clever or pretty multi-byte example if we have one. ;)\n>\n> It has been 10 days since this discussion petered out.\n>\n> My preference is to introduce core.commentString to avoid confusion\n> coming from an older Git using the first-byte of a multi-byte\n> string, or dying upon reading a configuration file meant for a newer\n> Git, and then let core.commentString override core.commentChar, but\n> I would prefer to see the discussion participants to raise their\n> opinions and reach a conclusion.\n>\n> Thanks.\n\nSounds good to me.\n\n-- \nKristoffer Haugsbakk\n\n"},{"id":"491645","messageId":"20240327074655.GA831122@coredump.intra.peff.net","threadId":"61052","inReplyTo":"xmqq8r247igg.fsf@gitster.g","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-27T07:46:55Z","receivedAt":"2024-03-27T07:47:02Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Mar 26, 2024 at 03:10:23PM -0700, Junio C Hamano wrote:\n\n> It has been 10 days since this discussion petered out.\n\nI wrote the last message, so I was waiting for you to respond. ;)\n\n  https://lore.kernel.org/git/20240316055013.GA32145@coredump.intra.peff.net/\n\n> My preference is to introduce core.commentString to avoid confusion\n> coming from an older Git using the first-byte of a multi-byte\n> string, or dying upon reading a configuration file meant for a newer\n> Git, and then let core.commentString override core.commentChar, but\n> I would prefer to see the discussion participants to raise their\n> opinions and reach a conclusion.\n\nOK. I don't have a strong opinion. Are you OK with core.commentString as\na strict synonym (so last-one-wins and either name overwrites previous)?\nOr do you want an override (i.e., commentString always overrides\ncommentChar, regardless of order). I think it's mostly academic, and the\nstrict synonym version is much easier to implement.\n\n-Peff\n"},{"id":"491650","messageId":"20240327081922.GA830163@coredump.intra.peff.net","threadId":"61052","inReplyTo":"20240327074655.GA831122@coredump.intra.peff.net","subject":"[PATCH 17/16] config: add core.commentString","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-27T08:19:22Z","receivedAt":"2024-03-27T08:19:24Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Mar 27, 2024 at 03:46:55AM -0400, Jeff King wrote:\n\n> > My preference is to introduce core.commentString to avoid confusion\n> > coming from an older Git using the first-byte of a multi-byte\n> > string, or dying upon reading a configuration file meant for a newer\n> > Git, and then let core.commentString override core.commentChar, but\n> > I would prefer to see the discussion participants to raise their\n> > opinions and reach a conclusion.\n> \n> OK. I don't have a strong opinion. Are you OK with core.commentString as\n> a strict synonym (so last-one-wins and either name overwrites previous)?\n> Or do you want an override (i.e., commentString always overrides\n> commentChar, regardless of order). I think it's mostly academic, and the\n> strict synonym version is much easier to implement.\n\nLike this, on top of what you have queued in jk/core-comment-string.\n\nNote that you graduated kh/doc-commentchar-is-a-byte, which says \"this\nASCII character\" early in the description, which will be incorrect if my\nseries is merged. This would need to be fixed (possibly as part of\nmerging my topic, though I don't think it actually triggers a conflict,\nso you'll have to remember to do so manually). Or mine could be rebased\non top of master and then remove it as part of the series.\n\n-- >8 --\nSubject: [PATCH] config: add core.commentString\n\nThe core.commentChar code recently learned to accept more than a\nsingle ASCII character. But using it is annoying with multiple versions\nof Git, since older ones will reject it outright:\n\n    $ git.v2.44.0 -c core.commentchar=foo stripspace -s\n    error: core.commentChar should only be one ASCII character\n    fatal: unable to parse 'core.commentchar' from command-line config\n\nLet's add an alias core.commentString. That's arguably a better name\nanyway, since we now can handle strings, and it makes it possible to\nhave a config that works reasonably with both old and new versions of\nGit (see the example in the documentation).\n\nThis is strictly an alias, so there's not much point in adding duplicate\ntests; I added a single one to t0030 that exercises the alias code.\n\nNote also that the error messages for invalid values will now show the\nvariable the config parser handed us, and thus will be normalized to\nlowercase (rather than camelcase). A few tests in t0030 are adjusted to\nmatch.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\nAn alternative to using \"$var cannot ...\" in the error messages (if we\ndon't like the all-lowercase variable name) is to just say \"comment\nstrings cannot ...\". That vaguely covers both cases, and the message\nprinted by the config code itself does mention the actual variable name\nthat triggered the error.\n\n Documentation/config/core.txt | 19 ++++++++++++++++---\n config.c                      |  7 ++++---\n t/t0030-stripspace.sh         |  9 +++++++--\n 3 files changed, 27 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex c86b8c8408..bbe869c497 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -520,15 +520,28 @@ core.editor::\n \t`GIT_EDITOR` is not set.  See linkgit:git-var[1].\n \n core.commentChar::\n+core.commentString::\n \tCommands such as `commit` and `tag` that let you edit\n \tmessages consider a line that begins with this character\n \tcommented, and removes them after the editor returns\n-\t(default '#'). Note that this option can take values larger than\n-\ta byte (whether a single multi-byte character, or you\n-\tcould even go wild with a multi-character sequence).\n+\t(default '#').\n +\n If set to \"auto\", `git-commit` would select a character that is not\n the beginning character of any line in existing commit messages.\n++\n+Note that these two variables are aliases of each other, and in modern\n+versions of Git you are free to use a string (e.g., `//` or `⁑⁕⁑`) with\n+`commentChar`. Versions of Git prior to v2.45.0 will ignore\n+`commentString` but will reject a value of `commentChar` that consists\n+of more than a single ASCII byte. If you plan to use your config with\n+older and newer versions of Git, you may want to specify both:\n++\n+    [core]\n+    # single character for older versions\n+    commentChar = \"#\"\n+    # string for newer versions (which will override commentChar\n+    # because it comes later in the file)\n+    commentString = \"//\"\n \n core.filesRefLockTimeout::\n \tThe length of time, in milliseconds, to retry when trying to\ndiff --git a/config.c b/config.c\nindex 92c752ed9f..d12e0f34f1 100644\n--- a/config.c\n+++ b/config.c\n@@ -1560,18 +1560,19 @@ static int git_default_core_config(const char *var, const char *value,\n \tif (!strcmp(var, \"core.editor\"))\n \t\treturn git_config_string(&editor_program, var, value);\n \n-\tif (!strcmp(var, \"core.commentchar\")) {\n+\tif (!strcmp(var, \"core.commentchar\") ||\n+\t    !strcmp(var, \"core.commentstring\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n \t\telse if (!strcasecmp(value, \"auto\"))\n \t\t\tauto_comment_line_char = 1;\n \t\telse if (value[0]) {\n \t\t\tif (strchr(value, '\\n'))\n-\t\t\t\treturn error(_(\"core.commentChar cannot contain newline\"));\n+\t\t\t\treturn error(_(\"%s cannot contain newline\"), var);\n \t\t\tcomment_line_str = xstrdup(value);\n \t\t\tauto_comment_line_char = 0;\n \t\t} else\n-\t\t\treturn error(_(\"core.commentChar must have at least one character\"));\n+\t\t\treturn error(_(\"%s must have at least one character\"), var);\n \t\treturn 0;\n \t}\n \ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex a161faf702..f10f42ff1e 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -401,14 +401,19 @@ test_expect_success 'strip comments with changed comment char' '\n \ttest -z \"$(echo \"; comment\" | git -c core.commentchar=\";\" stripspace -s)\"\n '\n \n+test_expect_success 'strip comments with changed comment string' '\n+\ttest ! -z \"$(echo \"// comment\" | git -c core.commentchar=// stripspace)\" &&\n+\ttest -z \"$(echo \"// comment\" | git -c core.commentchar=\"//\" stripspace -s)\"\n+'\n+\n test_expect_success 'newline as commentchar is forbidden' '\n \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n-\tgrep \"core.commentChar cannot contain newline\" err\n+\tgrep \"core.commentchar cannot contain newline\" err\n '\n \n test_expect_success 'empty commentchar is forbidden' '\n \ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n-\tgrep \"core.commentChar must have at least one character\" err\n+\tgrep \"core.commentchar must have at least one character\" err\n '\n \n test_expect_success '-c with single line' '\n-- \n2.44.0.727.g4d9414de3a\n\n"},{"id":"491691","messageId":"CAPx1Gvcms2Ssik5RNPp90kq1L1QmTcDxO4-j-L98hrhquDP6zA@mail.gmail.com","threadId":"61052","inReplyTo":"20240327081922.GA830163@coredump.intra.peff.net","subject":"Re: [PATCH 17/16] config: add core.commentString","fromName":"Chris Torek","fromEmail":"chris.torek@gmail.com","sentAt":"2024-03-27T12:45:51Z","receivedAt":"2024-03-27T12:46:04Z","isPatch":true,"sender":{"key":"chris.torek@gmail.com","avatar":"https://avatars.githubusercontent.com/u/16826774?v=4"},"body":"Assuming the implementation continues as suggested, I'll mention\nhere that I really like this note:\n\nOn Wed, Mar 27, 2024 at 1:19 AM Jeff King <peff@peff.net> wrote:\n> +Note that these two variables are aliases of each other, and in modern\n> +versions of Git you are free to use a string (e.g., `//` or `⁑⁕⁑`) with\n> +`commentChar`. Versions of Git prior to v2.45.0 will ignore\n> +`commentString` but will reject a value of `commentChar` that consists\n> +of more than a single ASCII byte. If you plan to use your config with\n> +older and newer versions of Git, you may want to specify both:\n\nOne of the big things I think is missing from existing Git documentation\n(and would, alas, be a huge effort to provide) is backwards-compatibility\nnotes. People are often stuck with old versions of software, at least\nduring initial bringup, for a variety of reasons, and such notes can\nbe quite helpful.\n\nExamples of modern systems that have extensive notes include\nPython, where the documentation often says \"new in 3.7\" or\nwhatever, and Go, where the automatically-built documentation\nnotes which version of Go introduced some new function.\n\nI'm not exactly volunteering here for the heavy lifting though. :-)\n\nChris\n"},{"id":"491697","messageId":"xmqqzfuj4tgc.fsf@gitster.g","threadId":"61052","inReplyTo":"20240327074655.GA831122@coredump.intra.peff.net","subject":"Re: [PATCH v2 16/16] config: allow multi-byte core.commentChar","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-27T14:53:23Z","receivedAt":"2024-03-27T14:53:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> OK. I don't have a strong opinion. Are you OK with core.commentString as\n> a strict synonym (so last-one-wins and either name overwrites previous)?\n> Or do you want an override (i.e., commentString always overrides\n> commentChar, regardless of order). I think it's mostly academic, and the\n> strict synonym version is much easier to implement.\n\nWhen I wrote it, I meant \"String is a successor of Char, if both\nexists that is used regardless of the order\", but either is OK.\nOlder versions of Git would not understand the \"String\" version, so\nit matters only to those who uses mixed versions of Git and they can\ncontrol the last-one-wins in their configuration file, I would\nguess.\n\nThanks.\n\n"},{"id":"491701","messageId":"xmqqa5mj3b6c.fsf@gitster.g","threadId":"61052","inReplyTo":"20240327081922.GA830163@coredump.intra.peff.net","subject":"Re: [PATCH 17/16] config: add core.commentString","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-27T16:13:31Z","receivedAt":"2024-03-27T16:13:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> Note that you graduated kh/doc-commentchar-is-a-byte, which says \"this\n> ASCII character\" early in the description, which will be incorrect if my\n> series is merged.\n\nTrue.  I could tweak this patch to force a conflict\n\n core.commentChar::\n core.commentString::\n \tCommands such as `commit` and `tag` that let you edit\n-\tmessages consider a line that begins with this character\n+\tmessages consider a line that begins with this string\n \tcommented, and removes them after the editor returns\n \t(default '#').\n\nand let the rerere database to remember the resolution (which will\ntweak \"string\" back to \"character\").  But I'll prepare a merge-fix\nbefore I forget, which is a cleaner approach.\n\n> An alternative to using \"$var cannot ...\" in the error messages (if we\n> don't like the all-lowercase variable name) is to just say \"comment\n> strings cannot ...\". That vaguely covers both cases, and the message\n> printed by the config code itself does mention the actual variable name\n> that triggered the error.\n\nOK, because the error() return from this function will trigger\nanother die() in the caller, e.g.\n\n    error: core.commentchar must have at least one character\n    fatal: bad config variable 'core.commentchar' in file '.git/config' at line 6\n\nso we can afford to make the \"error\" side vague, except that the\n\"fatal\" one is also downcased already, so we are not really solving\nanything by making the message vague, I would think.  The posted\npatch as-is is prefectly fine.\n\nSide note:\n    I wonder if we would later want to somehow _merge_ these two\n    error messages, i.e. the lower-level will notice and record the\n    nature of the problem instead of calling error(), and the caller\n    will use the recorded information while composing the \"fatal\"\n    message to die with.  I actually do not know if it is a good\n    idea to begin with.  If we want to do it right, the \"record\"\n    part probably cannot be a simple \"stringify into strbuf\" that\n    will result in lego message that is harder for i18n folks.\n\n\n$ git diff refs/merge-fix/jk/core-comment-string^!\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex bd033ab100..bbe869c497 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -522,7 +522,7 @@ core.editor::\n core.commentChar::\n core.commentString::\n \tCommands such as `commit` and `tag` that let you edit\n-\tmessages consider a line that begins with this ASCII character\n+\tmessages consider a line that begins with this character\n \tcommented, and removes them after the editor returns\n \t(default '#').\n +\n"},{"id":"491734","messageId":"20240328094740.GA871147@coredump.intra.peff.net","threadId":"61052","inReplyTo":"xmqqa5mj3b6c.fsf@gitster.g","subject":"Re: [PATCH 17/16] config: add core.commentString","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-03-28T09:47:40Z","receivedAt":"2024-03-28T09:47:47Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Mar 27, 2024 at 09:13:31AM -0700, Junio C Hamano wrote:\n\n> > An alternative to using \"$var cannot ...\" in the error messages (if we\n> > don't like the all-lowercase variable name) is to just say \"comment\n> > strings cannot ...\". That vaguely covers both cases, and the message\n> > printed by the config code itself does mention the actual variable name\n> > that triggered the error.\n> \n> OK, because the error() return from this function will trigger\n> another die() in the caller, e.g.\n> \n>     error: core.commentchar must have at least one character\n>     fatal: bad config variable 'core.commentchar' in file '.git/config' at line 6\n> \n> so we can afford to make the \"error\" side vague, except that the\n> \"fatal\" one is also downcased already, so we are not really solving\n> anything by making the message vague, I would think.  The posted\n> patch as-is is prefectly fine.\n\nOh, right.  For some reason I thought the die() message would have the\nvariable as written by the user, but that obviously is not true. So I\nagree it would not even be an improvement (and the normalizing in my new\nerror() message is something we've been living with all along anyway for\nother messages).\n\n> Side note:\n>     I wonder if we would later want to somehow _merge_ these two\n>     error messages, i.e. the lower-level will notice and record the\n>     nature of the problem instead of calling error(), and the caller\n>     will use the recorded information while composing the \"fatal\"\n>     message to die with.  I actually do not know if it is a good\n>     idea to begin with.  If we want to do it right, the \"record\"\n>     part probably cannot be a simple \"stringify into strbuf\" that\n>     will result in lego message that is harder for i18n folks.\n\nYeah, this is a general problem of accumulating errors. I had always\nassumed in cases like this that we could have some language-independent\nsyntax like:\n\n  die(\"%s:%d: error parsing '%s': %s\",\n      file, line_nr, var, err_from_callback);\n\nIt's certainly lego-like, but it avoids the worst lego cases where\nwe're literally composing sentences. But as somebody who does not do\ntranslations, it's possible I'm just being optimistic. ;)\n\n-Peff\n"}]}