diff mbox series

[5/7] worktree: `list` escape lock reason in --porcelain

Message ID 20210104162128.95281-6-rafaeloliveira.cs@gmail.com (mailing list archive)
State Superseded
Headers show
Series teach `worktree list` verbose mode and prunable annotations | expand

Commit Message

Rafael Silva Jan. 4, 2021, 4:21 p.m. UTC
"git worktree list" porcelain format shows one attribute per line, each
attribute is listed with a label and value separated by a single space.
If any worktree is locked, a "locked" attribute is listed followed by the
reason, if available, otherwise only "locked" is shown.

In case the lock reason contains newline characters (i.e: LF or CRLF)
this will cause the format to break as each line should correspond to
one attribute. This leads to unwanted behavior, specially as the
porcelain is intended to be machine-readable. To address this shortcoming
let's teach "git worktree list" to escape any newline character before
outputting the locked reason for porcelain format.

Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 builtin/worktree.c | 26 +++++++++++++++++++++++---
 1 file changed, 23 insertions(+), 3 deletions(-)

Comments

Phillip Wood Jan. 5, 2021, 10:29 a.m. UTC | #1
Hi Rafael

Thanks for working on this

On 04/01/2021 16:21, Rafael Silva wrote:
> "git worktree list" porcelain format shows one attribute per line, each
> attribute is listed with a label and value separated by a single space.
> If any worktree is locked, a "locked" attribute is listed followed by the
> reason, if available, otherwise only "locked" is shown.
> 
> In case the lock reason contains newline characters (i.e: LF or CRLF)
> this will cause the format to break as each line should correspond to
> one attribute. This leads to unwanted behavior, specially as the
> porcelain is intended to be machine-readable. To address this shortcoming
> let's teach "git worktree list" to escape any newline character before
> outputting the locked reason for porcelain format.

As the porcelain output format cannot handle worktree paths that contain 
newlines either I think it would be better to add a `-z` flag which uses 
NUL termination as we have for many other commands (diff, ls-files etc). 
This would fix the problem forever without having to worry about quoting 
each time a new field is added.

If you really need to quote the lock reason then please use one of the 
path quoting routines (probably quote_c_style() or write_name_quoted()) 
in quote.c rather than rolling your own - the code below gives the same 
output for a string that contains the two characters `\` followed by `n` 
as it does for the single character `\n`. People are (hopefully) used to 
dequoting our path quoting and there are routines out there to do it (we 
have one git Git.pm) using a function in quote.c will allow them to use 
those routines here.

Best Wishes

Phillip

> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
>   builtin/worktree.c | 26 +++++++++++++++++++++++---
>   1 file changed, 23 insertions(+), 3 deletions(-)
> 
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index dedd4089e5..9156ccd67e 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -567,6 +567,24 @@ static int add(int ac, const char **av, const char *prefix)
>   	return add_worktree(path, branch, &opts);
>   }
>   
> +static char *worktree_escape_reason(char *reason)
> +{
> +	struct strbuf escaped = STRBUF_INIT;
> +	char *r;
> +
> +	for (r = reason; *r; r++) {
> +		if (*r == '\r' && *(r + 1) && *(r + 1) == '\n') {
> +			strbuf_addstr(&escaped, "\\r\\n");
> +			r++;
> +		} else if (*r == '\n')
> +			strbuf_addstr(&escaped, "\\n");
> +		else
> +			strbuf_addch(&escaped, *r);
> +	}
> +
> +	return strbuf_detach(&escaped, NULL);
> +}
> +
>   static void show_worktree_porcelain(struct worktree *wt)
>   {
>   	printf("worktree %s\n", wt->path);
> @@ -580,9 +598,11 @@ static void show_worktree_porcelain(struct worktree *wt)
>   			printf("branch %s\n", wt->head_ref);
>   
>   		if (worktree_lock_reason(wt)) {
> -			if (*wt->lock_reason)
> -				printf("locked %s\n", wt->lock_reason);
> -			else
> +			if (*wt->lock_reason) {
> +				char *reason = worktree_escape_reason(wt->lock_reason);
> +				printf("locked %s\n", reason);
> +				free(reason);
> +			} else
>   				printf("locked\n");
>   		}
>   
>
Eric Sunshine Jan. 6, 2021, 8:59 a.m. UTC | #2
On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> "git worktree list" porcelain format shows one attribute per line, each
> attribute is listed with a label and value separated by a single space.
> If any worktree is locked, a "locked" attribute is listed followed by the
> reason, if available, otherwise only "locked" is shown.
>
> In case the lock reason contains newline characters (i.e: LF or CRLF)
> this will cause the format to break as each line should correspond to
> one attribute. This leads to unwanted behavior, specially as the
> porcelain is intended to be machine-readable. To address this shortcoming
> let's teach "git worktree list" to escape any newline character before
> outputting the locked reason for porcelain format.

s/specially/especially/

> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> @@ -567,6 +567,24 @@ static int add(int ac, const char **av, const char *prefix)
> +static char *worktree_escape_reason(char *reason)
> +{
> +       struct strbuf escaped = STRBUF_INIT;
> +       char *r;
> +
> +       for (r = reason; *r; r++) {
> +               if (*r == '\r' && *(r + 1) && *(r + 1) == '\n') {

The `*(r +1)` in the middle of the condition is redundant. The same
case is already handled by the `*(r + 1) == '\n'` which follows it
since EOL ('\0') won't match '\n'.

> +                       strbuf_addstr(&escaped, "\\r\\n");
> +                       r++;
> +               } else if (*r == '\n')
> +                       strbuf_addstr(&escaped, "\\n");
> +               else
> +                       strbuf_addch(&escaped, *r);
> +       }
> +
> +       return strbuf_detach(&escaped, NULL);
> +}

As Phillip already mentioned upstream, we can use one of the functions
from quote.c rather than rolling out own here. quote_c_style(), as
Phillip suggested, is almost certainly the best choice for a couple
reasons. First, when called with CQUOTE_NODQ, it adds quotes around
the string if there are any characters which need to be escaped, but
doesn't quote the string if it contains no special characters. This
makes the output more nicely readable in the typical case when there
are no special characters, and makes it possible to distinguish the
case Phillip pointed out between literal two characters "\n" in a
string and '\n' representing a newline. Second, consumers of our
porcelain are already used to consuming strings produced by
quote_c_style().

> @@ -580,9 +598,11 @@ static void show_worktree_porcelain(struct worktree *wt)
>                 if (worktree_lock_reason(wt)) {
> -                       if (*wt->lock_reason)
> -                               printf("locked %s\n", wt->lock_reason);
> -                       else
> +                       if (*wt->lock_reason) {
> +                               char *reason = worktree_escape_reason(wt->lock_reason);
> +                               printf("locked %s\n", reason);
> +                               free(reason);
> +                       } else

It would be preferable to fold this change directly into the preceding
patch, thus eliminating this patch altogether. That way this patch
series doesn't introduce a state in which the behavior is knowingly
"broken", but instead is correct from the start. Also, since you can
use the existing quote_c_style(), the change made by this patch
becomes tiny, thus is easily folded into the earlier patch.
Eric Sunshine Jan. 6, 2021, 9:07 a.m. UTC | #3
On Tue, Jan 5, 2021 at 5:29 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> On 04/01/2021 16:21, Rafael Silva wrote:
> > In case the lock reason contains newline characters (i.e: LF or CRLF)
> > this will cause the format to break as each line should correspond to
> > one attribute. This leads to unwanted behavior, specially as the
> > porcelain is intended to be machine-readable. To address this shortcoming
> > let's teach "git worktree list" to escape any newline character before
> > outputting the locked reason for porcelain format.
>
> As the porcelain output format cannot handle worktree paths that contain
> newlines either I think it would be better to add a `-z` flag which uses
> NUL termination as we have for many other commands (diff, ls-files etc).
> This would fix the problem forever without having to worry about quoting
> each time a new field is added.

Adding a `-z` flag makes sense, but that doesn't mean the existing
`--porcelain` is meaningless without it. I see `-z` as a good
complement to `--porcelain`.

Yes, we should fix the problem that the existing porcelain output
doesn't escape the path, but that can be done as a separate bug fix;
it doesn't need to be rolled into this series.

> If you really need to quote the lock reason then please use one of the
> path quoting routines (probably quote_c_style() or write_name_quoted())
> in quote.c rather than rolling your own - the code below gives the same
> output for a string that contains the two characters `\` followed by `n`
> as it does for the single character `\n`. People are (hopefully) used to
> dequoting our path quoting and there are routines out there to do it (we
> have one git Git.pm) using a function in quote.c will allow them to use
> those routines here.

I was going to suggest the same. Thanks.
Rafael Silva Jan. 8, 2021, 7:47 a.m. UTC | #4
Hi Phillip,

Thank you for reviewing the patch and the suggestions.

Phillip Wood writes:

> On 04/01/2021 16:21, Rafael Silva wrote:
>> "git worktree list" porcelain format shows one attribute per line, each
>> attribute is listed with a label and value separated by a single space.
>> If any worktree is locked, a "locked" attribute is listed followed by the
>> reason, if available, otherwise only "locked" is shown.
>> In case the lock reason contains newline characters (i.e: LF or
>> CRLF)
>> this will cause the format to break as each line should correspond to
>> one attribute. This leads to unwanted behavior, specially as the
>> porcelain is intended to be machine-readable. To address this shortcoming
>> let's teach "git worktree list" to escape any newline character before
>> outputting the locked reason for porcelain format.
>
> As the porcelain output format cannot handle worktree paths that
> contain newlines either I think it would be better to add a `-z` flag
> which uses NUL termination as we have for many other commands (diff,
> ls-files etc). This would fix the problem forever without having to
> worry about quoting each time a new field is added.
>

This is a good point and it seems to be good addition for the porcelain format.

Perhaps it make more sense to apply as separate patch though, something
that Eric Sunshine also mentioned on this message reply.

> If you really need to quote the lock reason then please use one of the
> path quoting routines (probably quote_c_style() or
> write_name_quoted()) in quote.c rather than rolling your own - the
> code below gives the same output for a string that contains the two
> characters `\` followed by `n` as it does for the single character
> `\n`. People are (hopefully) used to dequoting our path quoting and
> there are routines out there to do it (we have one git Git.pm) using a
> function in quote.c will allow them to use those routines here.
>
> Best Wishes
>
> Phillip
>

That's great, thank you for pointing this out to me. I will definitely
change this into the next revision by dropping the function written from
scratch on this patch and reuse one of the existing functions.

For what is worth, I just added the function because I didn't know about
the existing ones. So, there is no reason to add the new function.
diff mbox series

Patch

diff --git a/builtin/worktree.c b/builtin/worktree.c
index dedd4089e5..9156ccd67e 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -567,6 +567,24 @@  static int add(int ac, const char **av, const char *prefix)
 	return add_worktree(path, branch, &opts);
 }
 
+static char *worktree_escape_reason(char *reason)
+{
+	struct strbuf escaped = STRBUF_INIT;
+	char *r;
+
+	for (r = reason; *r; r++) {
+		if (*r == '\r' && *(r + 1) && *(r + 1) == '\n') {
+			strbuf_addstr(&escaped, "\\r\\n");
+			r++;
+		} else if (*r == '\n')
+			strbuf_addstr(&escaped, "\\n");
+		else
+			strbuf_addch(&escaped, *r);
+	}
+
+	return strbuf_detach(&escaped, NULL);
+}
+
 static void show_worktree_porcelain(struct worktree *wt)
 {
 	printf("worktree %s\n", wt->path);
@@ -580,9 +598,11 @@  static void show_worktree_porcelain(struct worktree *wt)
 			printf("branch %s\n", wt->head_ref);
 
 		if (worktree_lock_reason(wt)) {
-			if (*wt->lock_reason)
-				printf("locked %s\n", wt->lock_reason);
-			else
+			if (*wt->lock_reason) {
+				char *reason = worktree_escape_reason(wt->lock_reason);
+				printf("locked %s\n", reason);
+				free(reason);
+			} else
 				printf("locked\n");
 		}