git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations
@ 2021-01-04 16:21 Rafael Silva
  2021-01-04 16:21 ` [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c Rafael Silva
                   ` (8 more replies)
  0 siblings, 9 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

In c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) we taught `git worktree list` to annotate working tree that
is locked by appending "locked" text in order to signalize to the user
that a working tree is locked.  During the review, there was some
discussion about additional annotations and information that `list`
command could provide to the user that has long been envisioned and
mentioned in [2], [3] and [4].

This patch series address some of these changes by teaching
`worktree list` to show "prunable" annotation, adding verbose mode and
extending the --porcelain format with prunable and locked annotation as
follow up from [1]. Additionally, it address one shortcoming for porcelain
format to escape any newline characters (LF and CRLF) for the lock reason
to prevent breaking format mentioned in [4] and [1] during the review
cycle.

I have tried to organise in a way the series is easy to review, the
patches are organised in seven patches.

The first moves the should_prune_worktree() machinery to the top-level
worktree.c exposing the function as general API, that will be reference
by should_prune_worktree() wrapper implemented on the second patch. The
original idea was to not only move should_prune_worktree() but also
refactor to accept "struct worktree" and load the information directly,
which can simplify the `prune` command by reusing get_worktrees().
However this seems to also require refactoring get_worktrees() itself
to return "non-valid" working trees that can/should be pruned. This is
also mentioned in [5]. Having the wrapper function makes it easier to add
the prunable annotation without touching the get_worktrees() and the
other worktree sub commands.  The refactoring can be addressed in a
future patch, if this turns out to be good idea.  One possible approach
is to teach get_worktrees() to take additional flags that will tell
whether to return only valid or all worktrees in GIT_DIR/worktrees/
directory and address its own possible shortcoming, if any.

The third patch changes worktree_lock_reason() to be more gentle for the
main working tree to simply returning NULL instead of aborting the
program via assert() macro. This allow us to simplify the code that
checks if the working tree is locked for default and porcelain format.
This changes is also mentioned in [6].

The forth patch is the one that teaches `list` command to show prunable
annotation, for both default and porcelain format, and adds the verbose
option. These changes are also done for the "locked" annotation that was
not done on in [1]. Additionally, `list` learns to fail when both
--verbose and --porcelain format as used together.

The fifth patch adds worktree_escape_reason() that accepts a (char *)
text and returned the text with any LF or CRLF escaped. The caller is
responsible to freeing the escaped text. This is used by the locked
annotation in porcelain format. Currently, this is defined within
builtin/worktree.c as I was not sure whether libfying the function as
part of this series is a good idea. At this time it seems more sensible
to leave the code internally and libfying later once we are confident
about the implementation and whether it can be used in other part of the
code base but I'm open for suggestion.

The sixth and seventh patches adds tests and update the git-worktree.txt
documentation accordingly.

[1] https://lore.kernel.org/git/20200928154953.30396-1-rafaeloliveira.cs@gmail.com/
[2] https://lore.kernel.org/git/CAPig+cQF6V8HNdMX5AZbmz3_w2WhSfA4SFfNhQqxXBqPXTZL+w@mail.gmail.com/
[3] https://lore.kernel.org/git/CAPig+cSGXqJuaZPhUhOVX5X=LMrjVfv8ye_6ncMUbyKox1i7QA@mail.gmail.com/
[4] https://lore.kernel.org/git/CAPig+cTitWCs5vB=0iXuUyEY22c0gvjXvY1ZtTT90s74ydhE=A@mail.gmail.com/
[5] https://lore.kernel.org/git/CACsJy8ChM99n6skQCv-GmFiod19mnwwH4j-6R+cfZSiVFAxjgA@mail.gmail.com/
[6] https://lore.kernel.org/git/xmqq8sctlgzx.fsf@gitster.c.googlers.com/

Series is built on top of 71ca53e8125e36efbda17293c50027d31681a41f, currently
poiting to master and v2.30.0 tag.

Rafael Silva (7):
  worktree: move should_prune_worktree() to worktree.c
  worktree: implement worktree_prune_reason() wrapper
  worktree: teach worktree_lock_reason() to gently handle main worktree
  worktree: teach `list` prunable annotation and verbose
  worktree: `list` escape lock reason in --porcelain
  worktree: add tests for `list` verbose and annotations
  worktree: document `list` verbose and prunable annotations

 Documentation/git-worktree.txt |  59 ++++++++++++++-
 builtin/worktree.c             | 130 ++++++++++++++-------------------
 t/t2402-worktree-list.sh       |  94 +++++++++++++++++++++++-
 worktree.c                     |  96 +++++++++++++++++++++++-
 worktree.h                     |  17 +++++
 5 files changed, 313 insertions(+), 83 deletions(-)

-- 
2.30.0.391.g469bf2a980


^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
@ 2021-01-04 16:21 ` Rafael Silva
  2021-01-06  5:58   ` Eric Sunshine
  2021-01-06  6:55   ` Eric Sunshine
  2021-01-04 16:21 ` [PATCH 2/7] worktree: implement worktree_prune_reason() wrapper Rafael Silva
                   ` (7 subsequent siblings)
  8 siblings, 2 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

As part of teaching "git worktree list" to annotate worktree that is a
candidate for pruning, let's move should_prune_worktree() from
builtin/worktree.c to worktree.c in order to make part of the worktree
public API.

This function will be used by another API function, implemented in the
next patch that will accept a pointer to "worktree" structure directly
making it easier to implement the "prunable" annotations.

should_prune_worktree() knows how to select the given worktree for pruning
based on an expiration date, however the expiration value is stored on a
global variable and it is not local to the function. In order to move the
function, teach should_prune_worktree() to take the expiration date as an
argument.

Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 builtin/worktree.c | 75 +---------------------------------------------
 worktree.c         | 73 ++++++++++++++++++++++++++++++++++++++++++++
 worktree.h         | 10 +++++++
 3 files changed, 84 insertions(+), 74 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index 197fd24a55..eeb3ffaed0 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -67,79 +67,6 @@ static void delete_worktrees_dir_if_empty(void)
 	rmdir(git_path("worktrees")); /* ignore failed removal */
 }
 
-/*
- * Return true if worktree entry should be pruned, along with the reason for
- * pruning. Otherwise, return false and the worktree's path, or NULL if it
- * cannot be determined. Caller is responsible for freeing returned path.
- */
-static int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath)
-{
-	struct stat st;
-	char *path;
-	int fd;
-	size_t len;
-	ssize_t read_result;
-
-	*wtpath = NULL;
-	if (!is_directory(git_path("worktrees/%s", id))) {
-		strbuf_addstr(reason, _("not a valid directory"));
-		return 1;
-	}
-	if (file_exists(git_path("worktrees/%s/locked", id)))
-		return 0;
-	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
-		strbuf_addstr(reason, _("gitdir file does not exist"));
-		return 1;
-	}
-	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
-	if (fd < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		return 1;
-	}
-	len = xsize_t(st.st_size);
-	path = xmallocz(len);
-
-	read_result = read_in_full(fd, path, len);
-	if (read_result < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		close(fd);
-		free(path);
-		return 1;
-	}
-	close(fd);
-
-	if (read_result != len) {
-		strbuf_addf(reason,
-			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
-			    (uintmax_t)len, (uintmax_t)read_result);
-		free(path);
-		return 1;
-	}
-	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
-		len--;
-	if (!len) {
-		strbuf_addstr(reason, _("invalid gitdir file"));
-		free(path);
-		return 1;
-	}
-	path[len] = '\0';
-	if (!file_exists(path)) {
-		if (stat(git_path("worktrees/%s/index", id), &st) ||
-		    st.st_mtime <= expire) {
-			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
-			free(path);
-			return 1;
-		} else {
-			*wtpath = path;
-			return 0;
-		}
-	}
-	*wtpath = path;
-	return 0;
-}
-
 static void prune_worktree(const char *id, const char *reason)
 {
 	if (show_only || verbose)
@@ -195,7 +122,7 @@ static void prune_worktrees(void)
 		if (is_dot_or_dotdot(d->d_name))
 			continue;
 		strbuf_reset(&reason);
-		if (should_prune_worktree(d->d_name, &reason, &path))
+		if (should_prune_worktree(d->d_name, &reason, &path, expire))
 			prune_worktree(d->d_name, reason.buf);
 		else if (path)
 			string_list_append(&kept, path)->util = xstrdup(d->d_name);
diff --git a/worktree.c b/worktree.c
index f84ceae87d..5764b0dc7c 100644
--- a/worktree.c
+++ b/worktree.c
@@ -700,3 +700,76 @@ void repair_worktree_at_path(const char *path,
 	strbuf_release(&realdotgit);
 	strbuf_release(&dotgit);
 }
+
+/*
+ * Return true if worktree entry should be pruned, along with the reason for
+ * pruning. Otherwise, return false and the worktree's path, or NULL if it
+ * cannot be determined. Caller is responsible for freeing returned path.
+ */
+int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
+{
+	struct stat st;
+	char *path;
+	int fd;
+	size_t len;
+	ssize_t read_result;
+
+	*wtpath = NULL;
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addstr(reason, _("not a valid directory"));
+		return 1;
+	}
+	if (file_exists(git_path("worktrees/%s/locked", id)))
+		return 0;
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addstr(reason, _("gitdir file does not exist"));
+		return 1;
+	}
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		return 1;
+	}
+	len = xsize_t(st.st_size);
+	path = xmallocz(len);
+
+	read_result = read_in_full(fd, path, len);
+	if (read_result < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		close(fd);
+		free(path);
+		return 1;
+	}
+	close(fd);
+
+	if (read_result != len) {
+		strbuf_addf(reason,
+			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
+			    (uintmax_t)len, (uintmax_t)read_result);
+		free(path);
+		return 1;
+	}
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addstr(reason, _("invalid gitdir file"));
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		if (stat(git_path("worktrees/%s/index", id), &st) ||
+		    st.st_mtime <= expire) {
+			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
+			free(path);
+			return 1;
+		} else {
+			*wtpath = path;
+			return 0;
+		}
+	}
+	*wtpath = path;
+	return 0;
+}
diff --git a/worktree.h b/worktree.h
index f38e6fd5a2..e5f4320725 100644
--- a/worktree.h
+++ b/worktree.h
@@ -73,6 +73,16 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return true if worktree entry should be pruned, along with the reason for
+ * pruning. Otherwise, return false and the worktree's path, or NULL if it
+ * cannot be determined. Caller is responsible for freeing returned path.
+ */
+int should_prune_worktree(const char *id,
+			  struct strbuf *reason,
+			  char **wtpath,
+			  timestamp_t expire);
+
 #define WT_VALIDATE_WORKTREE_MISSING_OK (1 << 0)
 
 /*
-- 
2.30.0.391.g469bf2a980


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH 2/7] worktree: implement worktree_prune_reason() wrapper
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
  2021-01-04 16:21 ` [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c Rafael Silva
@ 2021-01-04 16:21 ` Rafael Silva
  2021-01-06  7:08   ` Eric Sunshine
  2021-01-04 16:21 ` [PATCH 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
                   ` (6 subsequent siblings)
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

The should_prune_worktree() machinery is used by the "prune" command to
identify whether a worktree is a candidate for pruning. This function
however, is not prepared to work directly with "struct worktree" and
refactoring is required not only on the function itself, but also also
changing get_worktrees() to return non-valid worktrees and address the
changes in all "worktree" sub commands.

Instead let's implement worktree_prune_reason() that accepts
"struct worktree" and uses should_prune_worktree() and returns whether
the given worktree is a candidate for pruning. As the "list" sub command
already uses a list of "struct worktree", this allow to simply check if
the working tree prunable by passing the structure directly without the
others parameters.

Also, let's add prune_reason field to the worktree structure that will
store the reason why the worktree can be pruned that is returned by
should_prune_worktree() when such reason is available.

Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 worktree.c | 19 +++++++++++++++++++
 worktree.h |  7 +++++++
 2 files changed, 26 insertions(+)

diff --git a/worktree.c b/worktree.c
index 5764b0dc7c..ee14db3ab5 100644
--- a/worktree.c
+++ b/worktree.c
@@ -15,6 +15,7 @@ void free_worktrees(struct worktree **worktrees)
 		free(worktrees[i]->id);
 		free(worktrees[i]->head_ref);
 		free(worktrees[i]->lock_reason);
+		free(worktrees[i]->prune_reason);
 		free(worktrees[i]);
 	}
 	free (worktrees);
@@ -245,6 +246,24 @@ const char *worktree_lock_reason(struct worktree *wt)
 	return wt->lock_reason;
 }
 
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
+{
+	if (!is_main_worktree(wt)) {
+		char *path;
+		struct strbuf reason = STRBUF_INIT;
+
+		if (should_prune_worktree(wt->id, &reason, &path, expire))
+			wt->prune_reason = strbuf_detach(&reason, NULL);
+		else
+			wt->prune_reason = NULL;
+
+		free(path);
+		strbuf_release(&reason);
+	}
+
+	return wt->prune_reason;
+}
+
 /* convenient wrapper to deal with NULL strbuf */
 static void strbuf_addf_gently(struct strbuf *buf, const char *fmt, ...)
 {
diff --git a/worktree.h b/worktree.h
index e5f4320725..24ded0f3c6 100644
--- a/worktree.h
+++ b/worktree.h
@@ -11,6 +11,7 @@ struct worktree {
 	char *id;
 	char *head_ref;		/* NULL if HEAD is broken or detached */
 	char *lock_reason;	/* private - use worktree_lock_reason */
+	char *prune_reason;     /* private - use worktree_prune_reason */
 	struct object_id head_oid;
 	int is_detached;
 	int is_bare;
@@ -73,6 +74,12 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return the reason string if the given worktree should be pruned
+ * or NULL otherwise.
+ */
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
+
 /*
  * Return true if worktree entry should be pruned, along with the reason for
  * pruning. Otherwise, return false and the worktree's path, or NULL if it
-- 
2.30.0.391.g469bf2a980


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
  2021-01-04 16:21 ` [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c Rafael Silva
  2021-01-04 16:21 ` [PATCH 2/7] worktree: implement worktree_prune_reason() wrapper Rafael Silva
@ 2021-01-04 16:21 ` Rafael Silva
  2021-01-06  7:29   ` Eric Sunshine
  2021-01-04 16:21 ` [PATCH 4/7] worktree: teach `list` prunable annotation and verbose Rafael Silva
                   ` (5 subsequent siblings)
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

The main worktree should not be locked and the worktree_lock_reason() API
is aware of this fact and avoids running the check code for the main
worktree. This checks is done via assert() macro, Therefore the caller
needs to ensure the function is never called, usually by additional code.

We can handle that case more gently by just returning false for the main
worktree and not bother checking if the "locked" file exists. This will
allowed further simplification from the caller as they will not need to
ensure the main worktree is never passed to the API.

Teach worktree_lock_reason() to be more gently and just return false for
the main working tree.

Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 worktree.c | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/worktree.c b/worktree.c
index ee14db3ab5..78be83ba16 100644
--- a/worktree.c
+++ b/worktree.c
@@ -225,9 +225,7 @@ int is_main_worktree(const struct worktree *wt)
 
 const char *worktree_lock_reason(struct worktree *wt)
 {
-	assert(!is_main_worktree(wt));
-
-	if (!wt->lock_reason_valid) {
+	if (!is_main_worktree(wt) && !wt->lock_reason_valid) {
 		struct strbuf path = STRBUF_INIT;
 
 		strbuf_addstr(&path, worktree_git_path(wt, "locked"));
-- 
2.30.0.391.g469bf2a980


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH 4/7] worktree: teach `list` prunable annotation and verbose
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
                   ` (2 preceding siblings ...)
  2021-01-04 16:21 ` [PATCH 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
@ 2021-01-04 16:21 ` Rafael Silva
  2021-01-06  8:31   ` Eric Sunshine
  2021-01-04 16:21 ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Rafael Silva
                   ` (4 subsequent siblings)
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

The "git worktree list" command shows the absolute path to the worktree,
the commit that is checked out, the name of the branch, and a "locked"
annotation if the worktree is locked. It is not clear whether a worktree,
if any, is prunable. The "prune" command will remove a worktree in case
is a prunable candidate unless --dry-run option is specified. This could
lead to a worktree being removed without the user realizing before is to
late, in case the user forgets to pass --dry-run for instance.

If the "list" command shows which worktree is prunable, the user could
verify before running "git worktree prune" and hopefully prevents the
working tree to be removed "accidently" on the worse case scenario.

Let's teach "git worktree list" to show when a worktree is prunable by
appending "prunable" text to its output by default and show a prunable
label for the porcelain format followed by the reason, if the avaiable.
While at it, let's do the same for the "locked" annotation.

Also, the worktree_prune_reason() stores the reason why git is selecting
the worktree to be pruned, so let's leverage that and also display this
information to the user. However, the reason is human readable text that
can take virtually any size which might make harder to extend the "list"
command with additional annotations and not fit nicely on the screen.

In order to address this shortcoming, let's teach "git worktree list" to
take a verbose option that will output the prune reason on the next line
indented, if the reason is available, otherwise the annotation is kept on
the same line. While at it, let's do the same for the "locked"
annotation.

The output of "git worktree list" with verbose becomes like so:

    $ git worktree list --verbose
    /path/to/main	      aba123 [main]
    /path/to/locked           acb124 [branch-a] locked
    /path/to/locked-reason    acc125 [branch-b]
        locked: worktree with locked reason
    /path/to/prunable         acd126 [branch-c] prunable
    /path/to/prunable-reason  ace127 [branch-d]
        prunable: gitdir file points to non-existent location

And the output of the "git worktree list --porcelain" becomes like so:

    $ git worktree list --porcelain
    worktree /path/to/main
    HEAD abc1234abc1234abc1234abc1234abc1234abc12
    branch refs/heads/main

    worktree /path/to/linked
    HEAD 123abc456def789abc1234def5678abc9123abce
    branch refs/heads/linked

    worktree /path/to/locked
    HEAD 123abcdea123abcd123acbd123acbda123abcd12
    detached
    locked lock reason

    worktree /path/to/prunable
    HEAD def1234def1234def1234def1234def1234def1a
    detached
    prunable prunable reason

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

diff --git a/builtin/worktree.c b/builtin/worktree.c
index eeb3ffaed0..dedd4089e5 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -578,6 +578,20 @@ static void show_worktree_porcelain(struct worktree *wt)
 			printf("detached\n");
 		else if (wt->head_ref)
 			printf("branch %s\n", wt->head_ref);
+
+		if (worktree_lock_reason(wt)) {
+			if (*wt->lock_reason)
+				printf("locked %s\n", wt->lock_reason);
+			else
+				printf("locked\n");
+		}
+
+		if (worktree_prune_reason(wt, expire)) {
+			if (*wt->prune_reason)
+				printf("prunable %s\n", wt->prune_reason);
+			else
+				printf("prunable\n");
+		}
 	}
 	printf("\n");
 }
@@ -604,8 +618,19 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 			strbuf_addstr(&sb, "(error)");
 	}
 
-	if (!is_main_worktree(wt) && worktree_lock_reason(wt))
-		strbuf_addstr(&sb, " locked");
+	if (worktree_lock_reason(wt)) {
+		if (verbose && *wt->lock_reason)
+			strbuf_addf(&sb, "\n\tlocked: %s", wt->lock_reason);
+		else
+			strbuf_addstr(&sb, " locked");
+	}
+
+	if (worktree_prune_reason(wt, expire)) {
+		if (verbose && *wt->prune_reason)
+			strbuf_addf(&sb, "\n\tprunable: %s", wt->prune_reason);
+		else
+			strbuf_addstr(&sb, " prunable");
+	}
 
 	printf("%s\n", sb.buf);
 	strbuf_release(&sb);
@@ -650,12 +675,18 @@ static int list(int ac, const char **av, const char *prefix)
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
+		OPT_EXPIRY_DATE(0, "expire", &expire,
+				N_("show working trees that is candidate to be pruned older than <time>")),
 		OPT_END()
 	};
 
+	expire = TIME_MAX;
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
+	else if (verbose && porcelain)
+		die(_("--verbose and --porcelain are mutually exclusive"));
 	else {
 		struct worktree **worktrees = get_worktrees();
 		int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
-- 
2.30.0.391.g469bf2a980


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH 5/7] worktree: `list` escape lock reason in --porcelain
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
                   ` (3 preceding siblings ...)
  2021-01-04 16:21 ` [PATCH 4/7] worktree: teach `list` prunable annotation and verbose Rafael Silva
@ 2021-01-04 16:21 ` Rafael Silva
  2021-01-05 10:29   ` Phillip Wood
  2021-01-06  8:59   ` Eric Sunshine
  2021-01-04 16:21 ` [PATCH 6/7] worktree: add tests for `list` verbose and annotations Rafael Silva
                   ` (3 subsequent siblings)
  8 siblings, 2 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

"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(-)

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");
 		}
 
-- 
2.30.0.391.g469bf2a980


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH 6/7] worktree: add tests for `list` verbose and annotations
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
                   ` (4 preceding siblings ...)
  2021-01-04 16:21 ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Rafael Silva
@ 2021-01-04 16:21 ` Rafael Silva
  2021-01-06  9:39   ` Eric Sunshine
  2021-01-04 16:21 ` [PATCH 7/7] worktree: document `list` verbose and prunable annotations Rafael Silva
                   ` (2 subsequent siblings)
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

Add tests for "git worktree list" verbose mode, prunable and locked
annotations for both default and porcelain format and ensure the
"prunable" annotation is consistent with what "git worktree prune"
command will eventually remove. Additionally, add one test case to
ensure any newline characters are escaped from locked reason for the
porcelain format to prevent breaking the format.

The c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) introduced a new test to ensure locked worktrees are listed
with "locked" annotation. However, the test does not remove the worktree
as the "git worktree prune" is not going to remove any locked worktrees.
Let's fix that by unlocking the worktree before the "prune" command.

Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 t/t2402-worktree-list.sh | 94 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 93 insertions(+), 1 deletion(-)

diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 795ddca2e4..6c4da438b3 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -62,7 +62,9 @@ test_expect_success '"list" all worktrees --porcelain' '
 '
 
 test_expect_success '"list" all worktrees with locked annotation' '
-	test_when_finished "rm -rf locked unlocked out && git worktree prune" &&
+	test_when_finished "rm -rf locked unlocked out &&
+		git worktree unlock locked &&
+		git worktree prune" &&
 	git worktree add --detach locked master &&
 	git worktree add --detach unlocked master &&
 	git worktree lock locked &&
@@ -71,6 +73,96 @@ test_expect_success '"list" all worktrees with locked annotation' '
 	! grep "/unlocked  *[0-9a-f].* locked$" out
 '
 
+test_expect_success '"list" all worktrees with prunable annotation' '
+	test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	git worktree add --detach unprunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	! grep "/unprunable  *[0-9a-f].* prunable$"
+'
+
+test_expect_success '"list" all worktrees with prunable consistent with "prune"' '
+	test_when_finished "rm -rf prunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	git worktree prune --verbose >out &&
+	test_i18ngrep "^Removing worktrees/prunable" out
+'
+
+test_expect_success '"list" all worktrees --porcelain with locked' '
+	test_when_finished "rm -rf locked1 locked2 out actual expect &&
+		git worktree unlock locked1 &&
+		git worktree unlock locked2 &&
+		git worktree prune" &&
+	echo "locked" >expect &&
+	echo "locked with reason" >>expect &&
+	git worktree add locked1 --detach &&
+	git worktree add locked2 --detach &&
+	git worktree lock locked1 &&
+	git worktree lock locked2 --reason "with reason" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '"list" all worktrees --porcelain with newline escaped locked reason' '
+	test_when_finished "rm -rf locked_lf locked_crlf reason_lf reason_crlf out actual expect reason &&
+		git worktree unlock locked_lf &&
+		git worktree unlock locked_crlf &&
+		git worktree prune" &&
+	printf "locked locked\\\\r\\\\nreason\n" >expect &&
+	printf "locked locked\\\\nreason\n" >>expect &&
+	git worktree add locked_lf --detach &&
+	git worktree add locked_crlf --detach &&
+	printf "locked\nreason\n\n" >reason_lf &&
+	printf "locked\r\nreason\n\n" >reason_crlf &&
+	git worktree lock locked_lf --reason "$(cat reason_lf)" &&
+	git worktree lock locked_crlf --reason "$(cat reason_crlf)" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '"list" all worktrees --porcelain with prunable' '
+	test_when_finished "rm -rf prunable list out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	rm -rf prunable &&
+	git worktree list --porcelain >out &&
+	test_i18ngrep "^prunable gitdir file points to non-existent location$" out
+'
+
+test_expect_success '"list" all worktrees --verbose and --porcelain mutually exclusive' '
+	test_must_fail git worktree list --verbose --porcelain
+'
+
+test_expect_success '"list" all worktrees --verbose with locked' '
+	test_when_finished "rm -rf locked out actual expect &&
+		git worktree unlock locked &&
+		git worktree prune" &&
+	git worktree add locked --detach &&
+	git worktree lock locked --reason "with reason" &&
+	echo "$(git -C locked rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tlocked: with reason\n" >>expect &&
+	git worktree list --verbose >out &&
+	sed -n "s/  */ /g;/\/locked  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
+	test_cmp actual expect
+'
+
+test_expect_success '"list" all worktrees --verbose with prunable' '
+	test_when_finished "rm -rf prunable out actual expect && git worktree prune" &&
+	git worktree add prunable --detach &&
+	echo "$(git -C prunable rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tprunable: gitdir file points to non-existent location\n" >>expect &&
+	rm -rf prunable &&
+	git worktree list --verbose >out &&
+	sed -n "s/  */ /g;/\/prunable  *[0-9a-f].*$/,/prunable: .*$/p" <out >actual &&
+	test_i18ncmp actual expect
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.391.g469bf2a980


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH 7/7] worktree: document `list` verbose and prunable annotations
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
                   ` (5 preceding siblings ...)
  2021-01-04 16:21 ` [PATCH 6/7] worktree: add tests for `list` verbose and annotations Rafael Silva
@ 2021-01-04 16:21 ` Rafael Silva
  2021-01-06  9:57   ` Eric Sunshine
  2021-01-06  5:36 ` [PATCH 0/7] teach `worktree list` verbose mode " Eric Sunshine
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-04 16:21 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Rafael Silva

Update the documentation with "git worktree list" verbose mode, prunable
and locked annotations for the default and porcelain format as part of
teaching the command to show prunable working trees and verbose mode.

Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 59 ++++++++++++++++++++++++++++++++--
 1 file changed, 56 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index af06128cc9..8ce579e2e8 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -97,8 +97,9 @@ list::
 List details of each working tree.  The main working tree is listed first,
 followed by each of the linked working trees.  The output details include
 whether the working tree is bare, the revision currently checked out, the
-branch currently checked out (or "detached HEAD" if none), and "locked" if
-the worktree is locked.
+branch currently checked out (or "detached HEAD" if none), "locked" if
+the worktree is locked, "prunable" if the worktree can be pruned by `prune`
+command.
 
 lock::
 
@@ -226,9 +227,12 @@ This can also be set up as the default behaviour by using the
 -v::
 --verbose::
 	With `prune`, report all removals.
+	With `list`, output additional information for working trees.
 
 --expire <time>::
 	With `prune`, only expire unused working trees older than `<time>`.
+	With `list`, annotate unused working trees older than `<time>` as prunable
+	candidates that will be remove by `prune` command if the same option is used.
 
 --reason <string>::
 	With `lock`, an explanation why the working tree is locked.
@@ -367,13 +371,48 @@ $ git worktree list
 /path/to/other-linked-worktree  1234abc  (detached HEAD)
 ------------
 
+The command also shows annotations for each working tree, according to its state.
+These annotations are:
+
+ * "locked", if any working tree is locked
+ * "prunable", if any working tree can be pruned via "git worktree prune".
+
+------------
+$ git worktree list
+/path/to/linked-worktree        abcd1234 [master]
+/path/to/locked-worktreee       acbd5678 (brancha) locked
+/path/to/prunable-worktree      5678abc  (detached HEAD) prunable
+------------
+
+For these annotations, a reason might also be available and this can be
+seen using the verbose mode. The annotation is then moved to the next line
+indented followed by the additional information.
+
+------------
+$ git worktree list --verbose
+/path/to/linked-worktree        abcd1234 [master]
+/path/to/locked-worktreee       acbd5678 (brancha)
+	locked: working tree path is mounted on a removable device
+/path/to/locked-no-reason       abcd578  (detached HEAD) locked
+/path/to/prunable-worktree      5678abc  (detached HEAD)
+	prunable: gitdir file points to non-existent location
+------------
+
+Note that, the annotation is only moved to the next line only if the
+additional text is available, otherwise the text is kept on the same.
+
 Porcelain Format
 ~~~~~~~~~~~~~~~~
 The porcelain format has a line per attribute.  Attributes are listed with a
 label and value separated by a single space.  Boolean attributes (like `bare`
 and `detached`) are listed as a label only, and are present only
 if the value is true.  The first attribute of a working tree is always
-`worktree`, an empty line indicates the end of the record.  For example:
+`worktree`, an empty line indicates the end of the record.
++
+In case any of the working trees are locked or is a candidate for pruning
+(See DESCRIPTION above) the labels "locked" and "prunable" is also shown
+followed by a reason, if available, otherwise only the labels are listed.
+For example:
 
 ------------
 $ git worktree list --porcelain
@@ -388,6 +427,20 @@ worktree /path/to/other-linked-worktree
 HEAD 1234abc1234abc1234abc1234abc1234abc1234a
 detached
 
+worktree /path/to/linked-worktree-locked
+HEAD 5678abc5678abc5678abc5678abc5678abc5678c
+branch refs/heads/locked
+locked
+
+worktree /path/to/linked-worktree-locked-with-reason
+HEAD 3456def3456def3456def3456def3456def3456b
+branch refs/heads/locked-with-reason
+locked reason why is locked
+
+worktree /path/to/linked-worktree-prunable
+HEAD 1233def1234def1234def1234def1234def1234b
+detached
+prunable gitdir file points to non-existent location
 ------------
 
 EXAMPLES
-- 
2.30.0.391.g469bf2a980


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* Re: [PATCH 5/7] worktree: `list` escape lock reason in --porcelain
  2021-01-04 16:21 ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Rafael Silva
@ 2021-01-05 10:29   ` Phillip Wood
  2021-01-05 11:02     ` [PATCH] worktree: add -z option for list subcommand Phillip Wood
                       ` (2 more replies)
  2021-01-06  8:59   ` Eric Sunshine
  1 sibling, 3 replies; 88+ messages in thread
From: Phillip Wood @ 2021-01-05 10:29 UTC (permalink / raw)
  To: Rafael Silva, git; +Cc: Eric Sunshine

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");
>   		}
>   
> 

^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH] worktree: add -z option for list subcommand
  2021-01-05 10:29   ` Phillip Wood
@ 2021-01-05 11:02     ` Phillip Wood
  2021-01-07  3:34       ` Eric Sunshine
  2021-01-06  9:07     ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Eric Sunshine
  2021-01-08  7:47     ` Rafael Silva
  2 siblings, 1 reply; 88+ messages in thread
From: Phillip Wood @ 2021-01-05 11:02 UTC (permalink / raw)
  To: Git Mailing List; +Cc: Eric Sunshine, Rafael Silva, Phillip Wood

From: Phillip Wood <phillip.wood@dunelm.org.uk>

Add a -z option to be used in conjunction with --porcelain that gives
NUL-terminated output. This enables 'worktree list --porcelain' to
handle worktree paths that contain newlines.

Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
---
I found an old patch that added NUL termination. I've rebased it
but the new test fails as there seems to be another worktree thats
been added since I wrote this, anyway I thought it might be a useful
start for adding a `-z` option.

 Documentation/git-worktree.txt | 13 +++++++++---
 builtin/worktree.c             | 36 ++++++++++++++++++++++------------
 t/t2402-worktree-list.sh       | 22 +++++++++++++++++++++
 3 files changed, 56 insertions(+), 15 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index af06128cc9..a07b593cc3 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [verse]
 'git worktree add' [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<commit-ish>]
-'git worktree list' [--porcelain]
+'git worktree list' [--porcelain [-z]]
 'git worktree lock' [--reason <string>] <worktree>
 'git worktree move' <worktree> <new-path>
 'git worktree prune' [-n] [-v] [--expire <expire>]
@@ -217,7 +217,13 @@ This can also be set up as the default behaviour by using the
 --porcelain::
 	With `list`, output in an easy-to-parse format for scripts.
 	This format will remain stable across Git versions and regardless of user
-	configuration.  See below for details.
+	configuration.  It is recommended to combine this with `-z`.
+	See below for details.
+
+-z::
+	When `--porcelain` is specified with `list` terminate each line with a
+	NUL rather than a newline. This makes it possible to parse the output
+	when a worktree path contains a newline character.
 
 -q::
 --quiet::
@@ -369,7 +375,8 @@ $ git worktree list
 
 Porcelain Format
 ~~~~~~~~~~~~~~~~
-The porcelain format has a line per attribute.  Attributes are listed with a
+The porcelain format has a line per attribute.  If `-z` is given then the lines
+are terminated with NUL rather than a newline.  Attributes are listed with a
 label and value separated by a single space.  Boolean attributes (like `bare`
 and `detached`) are listed as a label only, and are present only
 if the value is true.  The first attribute of a working tree is always
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 197fd24a55..0cd331873c 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -640,19 +640,25 @@ static int add(int ac, const char **av, const char *prefix)
 	return add_worktree(path, branch, &opts);
 }
 
-static void show_worktree_porcelain(struct worktree *wt)
+static void show_worktree_porcelain(struct worktree *wt, int line_terminator)
 {
-	printf("worktree %s\n", wt->path);
-	if (wt->is_bare)
-		printf("bare\n");
-	else {
-		printf("HEAD %s\n", oid_to_hex(&wt->head_oid));
-		if (wt->is_detached)
-			printf("detached\n");
-		else if (wt->head_ref)
-			printf("branch %s\n", wt->head_ref);
+	printf("worktree %s", wt->path);
+	fputc(line_terminator, stdout);
+	if (wt->is_bare) {
+		printf("bare");
+		fputc(line_terminator, stdout);
+	} else {
+		printf("HEAD %s", oid_to_hex(&wt->head_oid));
+		fputc(line_terminator, stdout);
+		if (wt->is_detached) {
+			printf("detached");
+			fputc(line_terminator, stdout);
+		} else if (wt->head_ref) {
+			printf("branch %s", wt->head_ref);
+			fputc(line_terminator, stdout);
+		}
 	}
-	printf("\n");
+	fputc(line_terminator, stdout);
 }
 
 static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
@@ -720,15 +726,20 @@ static void pathsort(struct worktree **wt)
 static int list(int ac, const char **av, const char *prefix)
 {
 	int porcelain = 0;
+	int line_terminator = '\n';
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT_SET_INT('z', NULL, &line_terminator,
+			    N_("fields are separated with NUL character"), '\0'),
 		OPT_END()
 	};
 
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
+	else if (!line_terminator && !porcelain)
+		die(_("'-z' requires '--porcelain'"));
 	else {
 		struct worktree **worktrees = get_worktrees();
 		int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
@@ -741,7 +752,8 @@ static int list(int ac, const char **av, const char *prefix)
 
 		for (i = 0; worktrees[i]; i++) {
 			if (porcelain)
-				show_worktree_porcelain(worktrees[i]);
+				show_worktree_porcelain(worktrees[i],
+							line_terminator);
 			else
 				show_worktree(worktrees[i], path_maxlen, abbrev);
 		}
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 795ddca2e4..acd9f8ab84 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -71,6 +71,28 @@ test_expect_success '"list" all worktrees with locked annotation' '
 	! grep "/unlocked  *[0-9a-f].* locked$" out
 '
 
+test_expect_success '"list" all worktrees --porcelain -z' '
+	test_when_finished "rm -rf here _actual actual expect &&
+				git worktree prune" &&
+	printf "worktree %sQHEAD %sQbranch %sQQ" \
+		"$(git rev-parse --show-toplevel)" \
+		"$(git rev-parse HEAD)" \
+		"$(git symbolic-ref HEAD)" >expect &&
+	git worktree add --detach here master &&
+	printf "worktree %sQHEAD %sQdetachedQQ" \
+		"$(git -C here rev-parse --show-toplevel)" \
+		"$(git rev-parse HEAD)" >>expect &&
+	git worktree list --porcelain -z >_actual &&
+	cat _actual | tr "\0" Q >actual	&&
+	test_cmp expect actual
+'
+
+test_expect_success '"list" -z fails without --porcelain' '
+	test_when_finished "rm -rf here && git worktree prune" &&
+	git worktree add --detach here master &&
+	test_must_fail git worktree list -z
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* Re: [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
                   ` (6 preceding siblings ...)
  2021-01-04 16:21 ` [PATCH 7/7] worktree: document `list` verbose and prunable annotations Rafael Silva
@ 2021-01-06  5:36 ` Eric Sunshine
  2021-01-08  7:38   ` Rafael Silva
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
  8 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  5:36 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> In c57b3367be (worktree: teach `list` to annotate locked worktree,
> 2020-10-11) we taught `git worktree list` to annotate working tree that
> is locked by appending "locked" text in order to signalize to the user
> that a working tree is locked.  During the review, there was some
> discussion about additional annotations and information that `list`
> command could provide to the user that has long been envisioned and
> mentioned in [2], [3] and [4].
>
> This patch series address some of these changes by teaching
> `worktree list` to show "prunable" annotation, adding verbose mode and
> extending the --porcelain format with prunable and locked annotation as
> follow up from [1]. Additionally, it address one shortcoming for porcelain
> format to escape any newline characters (LF and CRLF) for the lock reason
> to prevent breaking format mentioned in [4] and [1] during the review
> cycle.

Thank you for working on this. I'm happy to see these long-envisioned
enhancements finally taking shape. Before even reviewing the patches,
I decided to apply them and play with the new features, and I'm very
pleased to see that they behave exactly as I had envisioned all those
years ago.

Very nicely done.

I'll review the patches when I finish responding to this cover letter.

> The first moves the should_prune_worktree() machinery to the top-level
> worktree.c exposing the function as general API, that will be reference
> by should_prune_worktree() wrapper implemented on the second patch. The
> original idea was to not only move should_prune_worktree() but also
> refactor to accept "struct worktree" and load the information directly,
> which can simplify the `prune` command by reusing get_worktrees().
> However this seems to also require refactoring get_worktrees() itself
> to return "non-valid" working trees that can/should be pruned. This is
> also mentioned in [5]. Having the wrapper function makes it easier to add
> the prunable annotation without touching the get_worktrees() and the
> other worktree sub commands.  The refactoring can be addressed in a
> future patch, if this turns out to be good idea.  One possible approach
> is to teach get_worktrees() to take additional flags that will tell
> whether to return only valid or all worktrees in GIT_DIR/worktrees/
> directory and address its own possible shortcoming, if any.

I haven't looked at the patches yet, so I can't respond to this right now.

> The third patch changes worktree_lock_reason() to be more gentle for the
> main working tree to simply returning NULL instead of aborting the
> program via assert() macro. This allow us to simplify the code that
> checks if the working tree is locked for default and porcelain format.
> This changes is also mentioned in [6].

Sounds good.

> The forth patch is the one that teaches `list` command to show prunable
> annotation, for both default and porcelain format, and adds the verbose
> option. These changes are also done for the "locked" annotation that was
> not done on in [1]. Additionally, `list` learns to fail when both
> --verbose and --porcelain format as used together.

My knee-jerk response to disallowing --verbose and --porcelain
together was that it seemed unnecessarily harsh, but upon reflection,
I think it's the correct thing to do. After all, porcelain output,
even though extensible and flexible, should be predictable, so it
doesn't make sense for options such as --verbose to affect it.

> The fifth patch adds worktree_escape_reason() that accepts a (char *)
> text and returned the text with any LF or CRLF escaped. The caller is
> responsible to freeing the escaped text. This is used by the locked
> annotation in porcelain format. Currently, this is defined within
> builtin/worktree.c as I was not sure whether libfying the function as
> part of this series is a good idea. At this time it seems more sensible
> to leave the code internally and libfying later once we are confident
> about the implementation and whether it can be used in other part of the
> code base but I'm open for suggestion.

Perhaps I misunderstand, but I had envisioned employing one of the
codebase's existing quoting/escaping functions rather than crafting a
new one from scratch. However, I'll reserve judgment until I actually
read the patch.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c
  2021-01-04 16:21 ` [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c Rafael Silva
@ 2021-01-06  5:58   ` Eric Sunshine
  2021-01-08  7:40     ` Rafael Silva
  2021-01-06  6:55   ` Eric Sunshine
  1 sibling, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  5:58 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> worktree: move should_prune_worktree() to worktree.c

The subject is a bit confusing since this function already lives in
`worktree.c` (albeit within the `builtin` directory). An alternate way
to word this might be:

    worktree: libify and publish should_prune_worktree()

or just:

    worktree: libify should_prune_worktree()

> As part of teaching "git worktree list" to annotate worktree that is a
> candidate for pruning, let's move should_prune_worktree() from
> builtin/worktree.c to worktree.c in order to make part of the worktree
> public API.

Good.

> This function will be used by another API function, implemented in the
> next patch that will accept a pointer to "worktree" structure directly
> making it easier to implement the "prunable" annotations.

Okay, though I'm not sure this paragraph adds value since the reader
understands implicitly from the first paragraph that this is the
reason this patch is libifying the function. (I'd probably drop this
paragraph if I was composing the message.)

> should_prune_worktree() knows how to select the given worktree for pruning
> based on an expiration date, however the expiration value is stored on a
> global variable and it is not local to the function. In order to move the
> function, teach should_prune_worktree() to take the expiration date as an
> argument.

Rather than "on a global variable", it would be more accurate to say
"static file-scope variable".

> diff --git a/worktree.c b/worktree.c
> @@ -700,3 +700,76 @@ void repair_worktree_at_path(const char *path,
> +/*
> + * Return true if worktree entry should be pruned, along with the reason for
> + * pruning. Otherwise, return false and the worktree's path, or NULL if it
> + * cannot be determined. Caller is responsible for freeing returned path.
> + */
> +int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)

The function documentation which was moved here (worktree.c) from
builtin/worktree.c is duplicated in the header worktree.h. Having it
in the header is good. Having it here in the source file doesn't
necessarily hurt, but I worry about it becoming out-of-sync with the
copy in the header. For that reason, it might make sense to keep it
only in the header.

All of the above review comments are very minor and several of them
are subjective, thus not necessarily worth a re-roll.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c
  2021-01-04 16:21 ` [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c Rafael Silva
  2021-01-06  5:58   ` Eric Sunshine
@ 2021-01-06  6:55   ` Eric Sunshine
  2021-01-07  7:24     ` Eric Sunshine
  2021-01-08  7:41     ` Rafael Silva
  1 sibling, 2 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  6:55 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> diff --git a/worktree.h b/worktree.h
> @@ -73,6 +73,16 @@ int is_main_worktree(const struct worktree *wt);
> +/*
> + * Return true if worktree entry should be pruned, along with the reason for
> + * pruning. Otherwise, return false and the worktree's path, or NULL if it
> + * cannot be determined. Caller is responsible for freeing returned path.
> + */
> +int should_prune_worktree(const char *id,
> +                         struct strbuf *reason,
> +                         char **wtpath,
> +                         timestamp_t expire);

A few more comments...

It would be good to update the documentation to explain what `expire`
is since it's not necessarily obvious. The documentation could also be
tweaked to say that the worktree's path is returned in `wtpath` rather
than saying only that it is returned. If you choose to make these
changes, they should be probably done in a separate patch from the
patch which moves the code. This is a very minor issue, not
necessarily worth a re-roll.

Now that this is a public function, it might make sense for `wtpath`
to be optional; if the caller is not interested in it, then NULL would
be passed in for this argument. The implementation of
should_prune_worktree() would need to be updated to check that
`wtpath` is not NULL before assigning to it. This change, too, would
be done in a separate patch. However, this is just a "would be nice to
have" item, not at all required, and I don't think it should be added
to this series since it's not needed by anything in this series, thus
would just be noise (wasting your time and reviewer time). It's just
something we can keep in mind for the future.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 2/7] worktree: implement worktree_prune_reason() wrapper
  2021-01-04 16:21 ` [PATCH 2/7] worktree: implement worktree_prune_reason() wrapper Rafael Silva
@ 2021-01-06  7:08   ` Eric Sunshine
  2021-01-08  7:42     ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  7:08 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> worktree: implement worktree_prune_reason() wrapper

We might be able to give the reader more useful information in the
subject by explaining a bit more the goal of this patch. Perhaps
something like this:

    worktree: teach worktree to lazy-load "prunable" reason

> The should_prune_worktree() machinery is used by the "prune" command to
> identify whether a worktree is a candidate for pruning. This function
> however, is not prepared to work directly with "struct worktree" and
> refactoring is required not only on the function itself, but also also
> changing get_worktrees() to return non-valid worktrees and address the
> changes in all "worktree" sub commands.
>
> Instead let's implement worktree_prune_reason() that accepts
> "struct worktree" and uses should_prune_worktree() and returns whether
> the given worktree is a candidate for pruning. As the "list" sub command
> already uses a list of "struct worktree", this allow to simply check if
> the working tree prunable by passing the structure directly without the
> others parameters.

Everything through "not prepared to work directly with `struct
worktree`" makes sense when explaining why you are adding this wrapper
function, however, most of what follows is describing an aborted idea
about how you originally intended to implement this. The bit about
get_worktrees() not being able to return non-valid worktrees is
certainly an important limitation in the overall scheme of things, but
doesn't really help to sell the change made by this patch, and it
probably confuses the reader who didn't also read the cover-letter,
especially since this patch does nothing to help that situation.

It also isn't really necessary to talk about `git worktree list` at
this stage since this patch stands on its own by fleshing out the API
without having to cite a specific client.

> Also, let's add prune_reason field to the worktree structure that will
> store the reason why the worktree can be pruned that is returned by
> should_prune_worktree() when such reason is available.

In my opinion, this is the real reason this patch exists, thus should
be the focus of the commit message. All the description above this
paragraph can likely be dropped.

Taking the above comments into account, perhaps the entire commit
message could be collapsed to something like this:

    worktree: teach worktree to lazy-load "prunable" reason

    Add worktree_prune_reason() to allow a caller to discover whether
    a worktree is prunable and the reason that it is, much like
    worktree_lock_reason() indicates whether a worktree is locked and
    the reason for the lock. As with worktree_lock_reason(), retrieve
    the prunable reason lazily and cache it in the `worktree`
    structure.

> diff --git a/worktree.c b/worktree.c
> @@ -15,6 +15,7 @@ void free_worktrees(struct worktree **worktrees)
>                 free(worktrees[i]->lock_reason);
> +               free(worktrees[i]->prune_reason);
>                 free(worktrees[i]);

Remembering to free the prune-reason. Good.

> @@ -245,6 +246,24 @@ const char *worktree_lock_reason(struct worktree *wt)
> +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
> +{
> +       if (!is_main_worktree(wt)) {
> +               char *path;
> +               struct strbuf reason = STRBUF_INIT;
> +
> +               if (should_prune_worktree(wt->id, &reason, &path, expire))
> +                       wt->prune_reason = strbuf_detach(&reason, NULL);
> +               else
> +                       wt->prune_reason = NULL;
> +
> +               free(path);
> +               strbuf_release(&reason);
> +       }
> +
> +       return wt->prune_reason;
> +}

A couple observations...

I realize you patterned this after worktree_lock_reason(), however, it
is more common in this codebase to return early from the function for
conditions such as `is_main_worktree(wt)` which don't require any
additional processing. One reason is that doing so allows us to lose
an indentation level. Another is that it is easier to reason about the
rest of the function if we get the simple cases out of the way early,
such that we don't have to think about them again while reading the
remainder of the code.

If I'm not mistaken, the intention here was to cache `prune_reason`
for reuse, however, this function just overwrites it each time it's
called for a particular worktree, thus providing no caching and
leaking the previously-retrieved reason as well. To fix this, I think
you need to add a private `prune_reason_valid` member to `struct
worktree` (similar to `lock_reason_valid`) and check it before calling
should_prune_worktree().

Taking the above comments into account, perhaps it should be written like this:

    if (is_main_worktree(wt))
        return NULL;
    if (wt->prune_reason_valid)
        return wt->prune_reason;
    if (should_prune_worktree(wt->id, &reason, &path, expire))
        wt->prune_reason = strbuf_detach(&reason, NULL);
    wt_prune_reason_valid = 1;
    free(path);
    strbuf_release(&reason);

> diff --git a/worktree.h b/worktree.h
> @@ -11,6 +11,7 @@ struct worktree {
>         char *id;
>         char *head_ref;         /* NULL if HEAD is broken or detached */
>         char *lock_reason;      /* private - use worktree_lock_reason */
> +       char *prune_reason;     /* private - use worktree_prune_reason */

As noted above, we also probably need a new `prune_reason_valid`
member, similar to the existing `lock_reason_valid`.

> @@ -73,6 +74,12 @@ int is_main_worktree(const struct worktree *wt);
> +/*
> + * Return the reason string if the given worktree should be pruned
> + * or NULL otherwise.
> + */
> +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);

The documentation should also talk about `expire` since its purpose
and meaning is unclear.

Nit: I realize that you patterned this description after the one for
worktree_lock_reason(), but it's a bit unclear. Perhaps rephrasing it
like this would help:

    Return the reason the worktree should be pruned, otherwise
    NULL if it should not be pruned.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree
  2021-01-04 16:21 ` [PATCH 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
@ 2021-01-06  7:29   ` Eric Sunshine
  2021-01-08  7:43     ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  7:29 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> The main worktree should not be locked and the worktree_lock_reason() API
> is aware of this fact and avoids running the check code for the main
> worktree. This checks is done via assert() macro, Therefore the caller

s/Therefore/therefore/

> needs to ensure the function is never called, usually by additional code.
>
> We can handle that case more gently by just returning false for the main
> worktree and not bother checking if the "locked" file exists. This will
> allowed further simplification from the caller as they will not need to
> ensure the main worktree is never passed to the API.
>
> Teach worktree_lock_reason() to be more gently and just return false for
> the main working tree.

The situation is even a bit worse since the main worktree restriction
isn't even documented. Here's a possible rewrite of the commit message
which addresses that point too:

    worktree_lock_reason() aborts with an assertion failure when
    called on the main worktree since locking the main worktree is
    nonsensical. Not only is this behavior undocumented, thus callers
    might not even be aware that the call could potentially crash the
    program, but it also forces clients to be extra careful:

        if (!is_main_worktree(wt) && worktree_locked_reason(...))
           ...

    Since we know that locking makes no sense in the context of the
    main worktree, we can simply return false for the main worktree,
    thus making client code less complex by eliminating the need for
    callers to have inside knowledge about the implementation:

        if (worktree_locked_reason(...))
            ...

> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
> diff --git a/worktree.c b/worktree.c
> @@ -225,9 +225,7 @@ int is_main_worktree(const struct worktree *wt)
>  const char *worktree_lock_reason(struct worktree *wt)
>  {
> -       assert(!is_main_worktree(wt));
> -
> -       if (!wt->lock_reason_valid) {
> +       if (!is_main_worktree(wt) && !wt->lock_reason_valid) {
>                 struct strbuf path = STRBUF_INIT;

As mentioned in my review of patch [2/7], this would be more idiomatic
and easier to reason about if the function returns early for the main
worktree case, thus freeing the reader from having to think about that
case for the remainder of the code. So:

    if (is_main_worktree(wt))
        return NULL;
    if (!wt->lock_reason_valid) {
        ...

Subjective, and not necessarily worth a re-roll, though.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 4/7] worktree: teach `list` prunable annotation and verbose
  2021-01-04 16:21 ` [PATCH 4/7] worktree: teach `list` prunable annotation and verbose Rafael Silva
@ 2021-01-06  8:31   ` Eric Sunshine
  2021-01-08  7:45     ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  8:31 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> The "git worktree list" command shows the absolute path to the worktree,
> the commit that is checked out, the name of the branch, and a "locked"
> annotation if the worktree is locked. It is not clear whether a worktree,
> if any, is prunable.

Maybe this could just say...

    ... "locked" annotation if the worktree is locked,
    however, it does not indicate whether it is prunable.

> The "prune" command will remove a worktree in case
> is a prunable candidate unless --dry-run option is specified. This could

s/case is/case it is/

Or better:

    ... will remove a worktree if it is prunable unless...

> lead to a worktree being removed without the user realizing before is to
> late, in case the user forgets to pass --dry-run for instance.

s/before is/before it is/
s/to/too/

> If the "list" command shows which worktree is prunable, the user could
> verify before running "git worktree prune" and hopefully prevents the
> working tree to be removed "accidently" on the worse case scenario.
>
> Let's teach "git worktree list" to show when a worktree is prunable by
> appending "prunable" text to its output by default and show a prunable
> label for the porcelain format followed by the reason, if the avaiable.
> While at it, let's do the same for the "locked" annotation.

s/avaiable/available/

> Also, the worktree_prune_reason() stores the reason why git is selecting
> the worktree to be pruned, so let's leverage that and also display this
> information to the user. However, the reason is human readable text that
> can take virtually any size which might make harder to extend the "list"
> command with additional annotations and not fit nicely on the screen.
>
> In order to address this shortcoming, let's teach "git worktree list" to
> take a verbose option that will output the prune reason on the next line
> indented, if the reason is available, otherwise the annotation is kept on
> the same line. While at it, let's do the same for the "locked"
> annotation.

This is a lot of changes for one patch to be making, and it's hard for
a reader to digest all those changes from the commit message. I think
I counted four distinct changes being made here:

1. extend porcelain to include lock line (with optional reason)
2. add prunable annotation to normal list output
3. add prune line (with optional reason) to porcelain output
4. extend normal list output with --verbose to include reasons

The patch itself is not overly large or complicated, so perhaps
combining all these changes together is reasonable, although I'm quite
tempted to ask for them to be separated into at least four patches
(probably in the order shown above). A benefit of splitting them into
distinct patches is that you can add targeted documentation and test
updates to each individual patch rather than saving all the
documentation and test updates for a single (large) patch at the end
of the series. This helps reviewers reason about the changes more
easily since they get to see how each change impacts the documentation
and tests rather than having to wait for a patch late in the series to
make all the documentation and test updates, at which point the
reviewers may have forgotten details of the earlier patches.

(I've come back here after reading and reviewing the patch itself, and
I'm on the fence as to whether to suggest splitting this into multiple
patches. The changes in this patch are easy enough to understand and
digest, so I'm not convinced it makes sense to ask you to do all the
extra work of splitting it into smaller pieces. On the other hand, it
might be nice to have each documentation and test update done in the
patch which makes each particular change. So, I don't have a good
answer. Use your best judgment and do the amount of work you feel is
appropriate.)

> The output of "git worktree list" with verbose becomes like so:
>
>     $ git worktree list --verbose
>     /path/to/main             aba123 [main]
>     /path/to/locked           acb124 [branch-a] locked
>     /path/to/locked-reason    acc125 [branch-b]
>         locked: worktree with locked reason
>     /path/to/prunable         acd126 [branch-c] prunable
>     /path/to/prunable-reason  ace127 [branch-d]
>         prunable: gitdir file points to non-existent location

One "weird" aesthetic issue is that if the lock reason contains
newlines, the subsequent lines of the lock reason are not indented.
However, this is a very minor point and I don't think this patch
series should worry about it. We can think about how to address it
some time in the future if someone ever complains about it.

> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
>  builtin/worktree.c | 35 +++++++++++++++++++++++++++++++++--
>  1 file changed, 33 insertions(+), 2 deletions(-)
>
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index eeb3ffaed0..dedd4089e5 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -578,6 +578,20 @@ 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
> +                               printf("locked\n");
> +               }
> +
> +               if (worktree_prune_reason(wt, expire)) {
> +                       if (*wt->prune_reason)
> +                               printf("prunable %s\n", wt->prune_reason);
> +                       else
> +                               printf("prunable\n");
> +               }

A couple observations...

As a consumer of `struct worktree`, builtin/worktree.c should not be
poking at or accessing the structure's private fields `prune_reason`
and `lock_reason`; instead it should be retrieving those values via
worktree_prune_reason() and worktree_lock_reason() which are part of
the public API.

If a worktree is prunable, then worktree_prune_reason() will
unconditionally return a (non-empty) string; if it is not prunable,
then it will unconditionally return NULL. This means that the
`printf("prunable\n")` case is dead-code; it will never be reached.
This differs from worktree_lock_reason() which can return an empty
string to indicate a locked worktree for which no reason has been
specified.

Taking the above observations into account, a reasonable rewrite would be:

    const char *reason;

    reason = worktree_lock_reason(wt);
    if (reason && *reason)
        printf("locked %s\n", reason);
    else if (reason)
        printf("locked\n");

    reason = worktree_prune_reason(wt, expire);
    if (reason)
        printf("prunable %s\n", reason);

> @@ -604,8 +618,19 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
>                         strbuf_addstr(&sb, "(error)");
>         }
>
> -       if (!is_main_worktree(wt) && worktree_lock_reason(wt))
> -               strbuf_addstr(&sb, " locked");
> +       if (worktree_lock_reason(wt)) {
> +               if (verbose && *wt->lock_reason)
> +                       strbuf_addf(&sb, "\n\tlocked: %s", wt->lock_reason);
> +               else
> +                       strbuf_addstr(&sb, " locked");
> +       }
> +
> +       if (worktree_prune_reason(wt, expire)) {
> +               if (verbose && *wt->prune_reason)
> +                       strbuf_addf(&sb, "\n\tprunable: %s", wt->prune_reason);
> +               else
> +                       strbuf_addstr(&sb, " prunable");
> +       }

Same observations here about using public API rather than touching
private `struct worktree` fields, and about the final `else` case
being dead code.

> @@ -650,12 +675,18 @@ static int list(int ac, const char **av, const char *prefix)
>                 OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
> +               OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
> +               OPT_EXPIRY_DATE(0, "expire", &expire,
> +                               N_("show working trees that is candidate to be pruned older than <time>")),

Perhaps:

    "add 'prunable' annotation to worktrees older than <time>"

>                 OPT_END()
>         };
>
> +       expire = TIME_MAX;

Good, this mirrors how prune() initializes this variable.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 5/7] worktree: `list` escape lock reason in --porcelain
  2021-01-04 16:21 ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Rafael Silva
  2021-01-05 10:29   ` Phillip Wood
@ 2021-01-06  8:59   ` Eric Sunshine
  1 sibling, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  8:59 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

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.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 5/7] worktree: `list` escape lock reason in --porcelain
  2021-01-05 10:29   ` Phillip Wood
  2021-01-05 11:02     ` [PATCH] worktree: add -z option for list subcommand Phillip Wood
@ 2021-01-06  9:07     ` Eric Sunshine
  2021-01-08  7:47     ` Rafael Silva
  2 siblings, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  9:07 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Rafael Silva, Git List

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.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 6/7] worktree: add tests for `list` verbose and annotations
  2021-01-04 16:21 ` [PATCH 6/7] worktree: add tests for `list` verbose and annotations Rafael Silva
@ 2021-01-06  9:39   ` Eric Sunshine
  2021-01-07  4:09     ` Eric Sunshine
  2021-01-08  7:49     ` Rafael Silva
  0 siblings, 2 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  9:39 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Add tests for "git worktree list" verbose mode, prunable and locked
> annotations for both default and porcelain format and ensure the
> "prunable" annotation is consistent with what "git worktree prune"
> command will eventually remove. Additionally, add one test case to
> ensure any newline characters are escaped from locked reason for the
> porcelain format to prevent breaking the format.
>
> The c57b3367be (worktree: teach `list` to annotate locked worktree,
> 2020-10-11) introduced a new test to ensure locked worktrees are listed
> with "locked" annotation. However, the test does not remove the worktree
> as the "git worktree prune" is not going to remove any locked worktrees.
> Let's fix that by unlocking the worktree before the "prune" command.
>
> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> @@ -62,7 +62,9 @@ test_expect_success '"list" all worktrees --porcelain' '
>  test_expect_success '"list" all worktrees with locked annotation' '
> -       test_when_finished "rm -rf locked unlocked out && git worktree prune" &&
> +       test_when_finished "rm -rf locked unlocked out &&
> +               git worktree unlock locked &&
> +               git worktree prune" &&
>         git worktree add --detach locked master &&
>         git worktree add --detach unlocked master &&
>         git worktree lock locked &&

You might need to be a bit more careful here. If the test fails before
the worktree is locked, then the `git worktree unlock` in the cleanup
code will return an error, which will make the code executed by
test_when_finished() fail, as well, which makes it harder to debug
problems. Moving the `unlock` cleanup after you know the lock
succeeded will address this issue:

    test_when_finished "rm -rf locked unlocked out && git worktree prune" &&
    git worktree add --detach locked master &&
    git worktree add --detach unlocked master &&
    git worktree lock locked &&
    test_when_finished "git worktree unlock locked" &&
    ...

Same comment applies to other new tests added by this patch which lock
worktrees.

> @@ -71,6 +73,96 @@ test_expect_success '"list" all worktrees with locked annotation' '
> +test_expect_success '"list" all worktrees with prunable consistent with "prune"' '
> +       test_when_finished "rm -rf prunable out && git worktree prune" &&
> +       git worktree add --detach prunable &&
> +       rm -rf prunable &&
> +       git worktree list >out &&
> +       grep "/prunable  *[0-9a-f].* prunable$" out &&
> +       git worktree prune --verbose >out &&
> +       test_i18ngrep "^Removing worktrees/prunable" out
> +'

To be trustworthy, doesn't this test also need to have an unprunable
worktree, and test that `git worktree list` doesn't annotate it as
"prunable" _and_ that `git worktree prune` didn't prune it? Otherwise,
we really don't know that the two commands agree about what is and is
not prunable -- we only know they agree that this particular worktree
was prunable.

> +test_expect_success '"list" all worktrees --porcelain with newline escaped locked reason' '
> +       test_when_finished "rm -rf locked_lf locked_crlf reason_lf reason_crlf out actual expect reason &&
> +               git worktree unlock locked_lf &&
> +               git worktree unlock locked_crlf &&
> +               git worktree prune" &&

Nit: It's not a big deal, but we don't normally bother cleaning up
every junk file in tests, such as `out`, `actual`, `expect` if those
files aren't going to be a problem for subsequent tests. We are
explicitly cleaning up the worktrees in these tests because this
script is all about testing worktree behavior, and some random
leftover worktree could be a problem for other tests. I don't care
strongly one way or the other, but I worry a tiny bit that the list of
files being cleaned up could become outdated as changes are made to
the tests later...

> +test_expect_success '"list" all worktrees --porcelain with prunable' '
> +       test_when_finished "rm -rf prunable list out && git worktree prune" &&
> +       git worktree add --detach prunable &&
> +       rm -rf prunable &&
> +       git worktree list --porcelain >out &&
> +       test_i18ngrep "^prunable gitdir file points to non-existent location$" out
> +'

... for instance, the file `list` being cleaned up in this test is not
even created by this test.

> +
> +test_expect_success '"list" all worktrees --verbose and --porcelain mutually exclusive' '
> +       test_must_fail git worktree list --verbose --porcelain
> +'

Nit: This test title could probably be shortened to:

    '"list' --verbose and --porcelain mutually exclusive' '

Finally, I haven't tried it myself, but I was wondering if it is
possible to make a test which shows both "locked" and "prunable"
annotations for the same worktree. Would that be possible by
specifying a particular value for `--expire`? If it's too much work,
don't worry about it.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 7/7] worktree: document `list` verbose and prunable annotations
  2021-01-04 16:21 ` [PATCH 7/7] worktree: document `list` verbose and prunable annotations Rafael Silva
@ 2021-01-06  9:57   ` Eric Sunshine
  2021-01-08  7:49     ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-06  9:57 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Update the documentation with "git worktree list" verbose mode, prunable
> and locked annotations for the default and porcelain format as part of
> teaching the command to show prunable working trees and verbose mode.
>
> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
> @@ -226,9 +227,12 @@ This can also be set up as the default behaviour by using the
>  -v::
>  --verbose::
>         With `prune`, report all removals.
> +       With `list`, output additional information for working trees.

This leaves the reader wondering what additional information is output
for `list`. Perhaps a small tweak will help:

    With `list`, output additional information about worktrees (see below).

>  --expire <time>::
>         With `prune`, only expire unused working trees older than `<time>`.
> +       With `list`, annotate unused working trees older than `<time>` as prunable
> +       candidates that will be remove by `prune` command if the same option is used.

Perhaps just minimal:

    With `list`, annotate missing worktrees as prunable if they
    are older than `<time>`.

> @@ -367,13 +371,48 @@ $ git worktree list
> +The command also shows annotations for each working tree, according to its state.
> +These annotations are:
> +
> + * "locked", if any working tree is locked
> + * "prunable", if any working tree can be pruned via "git worktree prune".

s/any/the/g

We might want to use backticks around these annotations rather than
double quotes, and we certainly do want to use backticks around the
`git worktree prune` command to ensure it is styled consistently with
other commands in this document.

> +Note that, the annotation is only moved to the next line only if the
> +additional text is available, otherwise the text is kept on the same.

Drop the comma between "that, the". Also, too many "only"s in this
sentence. You can actually drop both of them and the sentence will
still read fine:

   Note that the annotation is moved to the next line if the
   additional information is available, otherwise it stays on
   the same line as the worktree itself.

or something.

>  Porcelain Format
>  ~~~~~~~~~~~~~~~~
>  The porcelain format has a line per attribute.  Attributes are listed with a
>  label and value separated by a single space.  Boolean attributes (like `bare`
>  and `detached`) are listed as a label only, and are present only
>  if the value is true.  The first attribute of a working tree is always
> -`worktree`, an empty line indicates the end of the record.  For example:
> +`worktree`, an empty line indicates the end of the record.
> ++
> +In case any of the working trees are locked or is a candidate for pruning
> +(See DESCRIPTION above) the labels "locked" and "prunable" is also shown
> +followed by a reason, if available, otherwise only the labels are listed.
> +For example:

s/(See/(see/
s/is also/are also/

Let's also use backticks rather than double quotes around `locked` and
`prunable` to ensure consistent formatting with the other porcelain
labels `bare` and `detached` which are already in backticks.

Also, the unnecessary `+` line (seen as `++` in the diff) makes this
render incorrectly. It renders as:

    +In case any...

To fix it, just leave the line blank between paragraphs.

(If possible, install `asciidoc` and `xmlto` and then run `make html`
to render the documentation yourself, and open
Documentation/git-worktree.html in your browser to check the output.)

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH] worktree: add -z option for list subcommand
  2021-01-05 11:02     ` [PATCH] worktree: add -z option for list subcommand Phillip Wood
@ 2021-01-07  3:34       ` Eric Sunshine
  2021-01-08 10:33         ` Phillip Wood
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-07  3:34 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Git Mailing List, Rafael Silva

On Tue, Jan 5, 2021 at 6:02 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> Add a -z option to be used in conjunction with --porcelain that gives
> NUL-terminated output. This enables 'worktree list --porcelain' to
> handle worktree paths that contain newlines.

Adding a -z mode makes a lot of sense. This, along with a fix to call
quote_c_style() on paths in normal (not `-z`) porcelain mode, can
easily be done after Rafael's series lands. Or they could be done
before his series, though that might make a lot of extra work for him.

> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
> ---
> I found an old patch that added NUL termination. I've rebased it
> but the new test fails as there seems to be another worktree thats
> been added since I wrote this, anyway I thought it might be a useful
> start for adding a `-z` option.

The test failure is fallout from a "bug" in a test added by Rafael's
earlier series[1] which was not caught by the reviewer[2]. In fact,
his current series has a drive-by fix[3] for this bug in patch [6/7].
Applying that fix (or the refinement[4] I suggested in my review)
makes your test work.

[1]: https://lore.kernel.org/git/20201011101152.5291-2-rafaeloliveira.cs@gmail.com/
[2]: https://lore.kernel.org/git/CAPig+cS8hvX4GCsnfLBnQ4Q_AkUad=bw7rjVcaOqSEqcLZvx8w@mail.gmail.com/
[3]: https://lore.kernel.org/git/20210104162128.95281-7-rafaeloliveira.cs@gmail.com/
[4]: https://lore.kernel.org/git/CAPig+cRysXpK0e1xXOuVd+EtkeyTk8FR6MUrL=Bg3W4ve8jdNA@mail.gmail.com/

> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
> @@ -217,7 +217,13 @@ This can also be set up as the default behaviour by using the
> +-z::
> +       When `--porcelain` is specified with `list` terminate each line with a
> +       NUL rather than a newline. This makes it possible to parse the output
> +       when a worktree path contains a newline character.

If we fix normal-mode porcelain to call quote_c_style(), then we can
drop the last sentence or refine it to say something along the lines
of it being easier to deal with paths with embedded newlines than in
normal porcelain mode.

> diff --git a/builtin/worktree.c b/builtin/worktree.c
> @@ -640,19 +640,25 @@ static int add(int ac, const char **av, const char *prefix)
> +static void show_worktree_porcelain(struct worktree *wt, int line_terminator)
>  {
> +       printf("worktree %s", wt->path);
> +       fputc(line_terminator, stdout);
> +       if (wt->is_bare) {
> +               printf("bare");
> +               fputc(line_terminator, stdout);
> +       } else {
> +               printf("HEAD %s", oid_to_hex(&wt->head_oid));
> +               fputc(line_terminator, stdout);
> +               if (wt->is_detached) {
> +                       printf("detached");
> +                       fputc(line_terminator, stdout);
> +               } else if (wt->head_ref) {
> +                       printf("branch %s", wt->head_ref);
> +                       fputc(line_terminator, stdout);
> +               }

Perhaps this could all be done a bit more concisely with printf()
alone rather than combining it with fputc(). For instance:

    printf("worktree %s%c", wt->path, line_terminator);

and so on.

> -       printf("\n");
> +       fputc(line_terminator, stdout);

This use of fputc() makes sense.

> @@ -720,15 +726,20 @@ static void pathsort(struct worktree **wt)
>  static int list(int ac, const char **av, const char *prefix)
>  {
> +               OPT_SET_INT('z', NULL, &line_terminator,
> +                           N_("fields are separated with NUL character"), '\0'),

"fields" sounds a little odd. "lines" might make more sense. "records"
might be even better and matches the wording of some other Git
commands accepting `-z`.

> +       else if (!line_terminator && !porcelain)
> +               die(_("'-z' requires '--porcelain'"));

Other error messages in this file don't quote command-line options, so:

    die(_("-z requires --porcelain"));

would be more consistent.

> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> @@ -71,6 +71,28 @@ test_expect_success '"list" all worktrees with locked annotation' '
> +test_expect_success '"list" all worktrees --porcelain -z' '
> +       test_when_finished "rm -rf here _actual actual expect &&
> +                               git worktree prune" &&
> +       printf "worktree %sQHEAD %sQbranch %sQQ" \
> +               "$(git rev-parse --show-toplevel)" \
> +               "$(git rev-parse HEAD)" \
> +               "$(git symbolic-ref HEAD)" >expect &&
> +       git worktree add --detach here master &&
> +       printf "worktree %sQHEAD %sQdetachedQQ" \
> +               "$(git -C here rev-parse --show-toplevel)" \
> +               "$(git rev-parse HEAD)" >>expect &&
> +       git worktree list --porcelain -z >_actual &&
> +       cat _actual | tr "\0" Q >actual &&

Or just use nul_to_q():

    nul_to_q <_actual >actual &&

(And there's no need to `cat` the file.)

> +       test_cmp expect actual
> +'
> +
> +test_expect_success '"list" -z fails without --porcelain' '
> +       test_when_finished "rm -rf here && git worktree prune" &&
> +       git worktree add --detach here master &&
> +       test_must_fail git worktree list -z
> +'

I don't think there's any need for this test to create (and cleanup) a
worktree. So, the entire test could collapse to:

    test_expect_success '"list" -z fails without --porcelain' '
        test_must_fail git worktree list -z
    '

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 6/7] worktree: add tests for `list` verbose and annotations
  2021-01-06  9:39   ` Eric Sunshine
@ 2021-01-07  4:09     ` Eric Sunshine
  2021-01-08  7:49     ` Rafael Silva
  1 sibling, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-07  4:09 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Wed, Jan 6, 2021 at 4:39 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
> Finally, I haven't tried it myself, but I was wondering if it is
> possible to make a test which shows both "locked" and "prunable"
> annotations for the same worktree. Would that be possible by
> specifying a particular value for `--expire`? If it's too much work,
> don't worry about it.

Ignore this stupid question/suggestion. A locked worktree will never
be considered prunable. Don't know what I was thinking when I asked
it.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c
  2021-01-06  6:55   ` Eric Sunshine
@ 2021-01-07  7:24     ` Eric Sunshine
  2021-01-08  7:41     ` Rafael Silva
  1 sibling, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-07  7:24 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Wed, Jan 6, 2021 at 1:55 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
> > +/*
> > + * Return true if worktree entry should be pruned, along with the reason for
> > + * pruning. Otherwise, return false and the worktree's path, or NULL if it
> > + * cannot be determined. Caller is responsible for freeing returned path.
> > + */
>
> It would be good to update the documentation to explain what `expire`
> is since it's not necessarily obvious. The documentation could also be
> tweaked to say that the worktree's path is returned in `wtpath` rather
> than saying only that it is returned. If you choose to make these
> changes, they should be probably done in a separate patch from the
> patch which moves the code. This is a very minor issue, not
> necessarily worth a re-roll.

On second thought, adding a patch just to make a minor update to the
function comment is probably overkill. It should be okay to do it in
this patch. Mention the change in the commit to alert reviewers.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-06  5:36 ` [PATCH 0/7] teach `worktree list` verbose mode " Eric Sunshine
@ 2021-01-08  7:38   ` Rafael Silva
  2021-01-08  8:19     ` Eric Sunshine
  0 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:38 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> In c57b3367be (worktree: teach `list` to annotate locked worktree,
>> 2020-10-11) we taught `git worktree list` to annotate working tree that
>> is locked by appending "locked" text in order to signalize to the user
>> that a working tree is locked.  During the review, there was some
>> discussion about additional annotations and information that `list`
>> command could provide to the user that has long been envisioned and
>> mentioned in [2], [3] and [4].
>>
>> This patch series address some of these changes by teaching
>> `worktree list` to show "prunable" annotation, adding verbose mode and
>> extending the --porcelain format with prunable and locked annotation as
>> follow up from [1]. Additionally, it address one shortcoming for porcelain
>> format to escape any newline characters (LF and CRLF) for the lock reason
>> to prevent breaking format mentioned in [4] and [1] during the review
>> cycle.
>
> Thank you for working on this. I'm happy to see these long-envisioned
> enhancements finally taking shape. Before even reviewing the patches,
> I decided to apply them and play with the new features, and I'm very
> pleased to see that they behave exactly as I had envisioned all those
> years ago.
>
> Very nicely done.

Thank you. I'm glad to hear the patches are aligned with what you
envisioned.

> I'll review the patches when I finish responding to this cover letter.
>

Thank you for reviewing and applying the patches, really appreciate it.

>> The fifth patch adds worktree_escape_reason() that accepts a (char *)
>> text and returned the text with any LF or CRLF escaped. The caller is
>> responsible to freeing the escaped text. This is used by the locked
>> annotation in porcelain format. Currently, this is defined within
>> builtin/worktree.c as I was not sure whether libfying the function as
>> part of this series is a good idea. At this time it seems more sensible
>> to leave the code internally and libfying later once we are confident
>> about the implementation and whether it can be used in other part of the
>> code base but I'm open for suggestion.
>
> Perhaps I misunderstand, but I had envisioned employing one of the
> codebase's existing quoting/escaping functions rather than crafting a
> new one from scratch. However, I'll reserve judgment until I actually
> read the patch.

Agreed. It make sense to reuse one of the already implemented functions
from the code base. for some reason I was not able to find it. I believe
this was cleared out in one of the patches replies by you and Phillip Wood.

I will remove this and reuse one of the existing function on the next
revision.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c
  2021-01-06  5:58   ` Eric Sunshine
@ 2021-01-08  7:40     ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:40 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Thank you for the detailed review.

Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> worktree: move should_prune_worktree() to worktree.c
>
> The subject is a bit confusing since this function already lives in
> `worktree.c` (albeit within the `builtin` directory). An alternate way
> to word this might be:
>
>     worktree: libify and publish should_prune_worktree()
>
> or just:
>
>     worktree: libify should_prune_worktree()
>

yes, I think these seems clearer. I will drop the current subject and
use one of these on the next revision.

>> This function will be used by another API function, implemented in the
>> next patch that will accept a pointer to "worktree" structure directly
>> making it easier to implement the "prunable" annotations.
>
> Okay, though I'm not sure this paragraph adds value since the reader
> understands implicitly from the first paragraph that this is the
> reason this patch is libifying the function. (I'd probably drop this
> paragraph if I was composing the message.)
>

Make sense.

>> should_prune_worktree() knows how to select the given worktree for pruning
>> based on an expiration date, however the expiration value is stored on a
>> global variable and it is not local to the function. In order to move the
>> function, teach should_prune_worktree() to take the expiration date as an
>> argument.
>
> Rather than "on a global variable", it would be more accurate to say
> "static file-scope variable".
>

Good point. I will definitely change that to be more accurate on the next
revision.   

>> diff --git a/worktree.c b/worktree.c
>> @@ -700,3 +700,76 @@ void repair_worktree_at_path(const char *path,
>> +/*
>> + * Return true if worktree entry should be pruned, along with the reason for
>> + * pruning. Otherwise, return false and the worktree's path, or NULL if it
>> + * cannot be determined. Caller is responsible for freeing returned path.
>> + */
>> +int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
>
> The function documentation which was moved here (worktree.c) from
> builtin/worktree.c is duplicated in the header worktree.h. Having it
> in the header is good. Having it here in the source file doesn't
> necessarily hurt, but I worry about it becoming out-of-sync with the
> copy in the header. For that reason, it might make sense to keep it
> only in the header.
>
> All of the above review comments are very minor and several of them
> are subjective, thus not necessarily worth a re-roll.

Make sense as well. 

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c
  2021-01-06  6:55   ` Eric Sunshine
  2021-01-07  7:24     ` Eric Sunshine
@ 2021-01-08  7:41     ` Rafael Silva
  1 sibling, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:41 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> diff --git a/worktree.h b/worktree.h
>> @@ -73,6 +73,16 @@ int is_main_worktree(const struct worktree *wt);
>> +/*
>> + * Return true if worktree entry should be pruned, along with the reason for
>> + * pruning. Otherwise, return false and the worktree's path, or NULL if it
>> + * cannot be determined. Caller is responsible for freeing returned path.
>> + */
>> +int should_prune_worktree(const char *id,
>> +                         struct strbuf *reason,
>> +                         char **wtpath,
>> +                         timestamp_t expire);
>
> A few more comments...
>
> It would be good to update the documentation to explain what `expire`
> is since it's not necessarily obvious. The documentation could also be
> tweaked to say that the worktree's path is returned in `wtpath` rather
> than saying only that it is returned. If you choose to make these
> changes, they should be probably done in a separate patch from the
> patch which moves the code. This is a very minor issue, not
> necessarily worth a re-roll.
>

Good idea. I will include these changes on the next revision. specially
the documentation about the `expire` field.

>
> Now that this is a public function, it might make sense for `wtpath`
> to be optional; if the caller is not interested in it, then NULL would
> be passed in for this argument. The implementation of
> should_prune_worktree() would need to be updated to check that
> `wtpath` is not NULL before assigning to it. This change, too, would
> be done in a separate patch. However, this is just a "would be nice to
> have" item, not at all required, and I don't think it should be added
> to this series since it's not needed by anything in this series, thus
> would just be noise (wasting your time and reviewer time). It's just
> something we can keep in mind for the future.

Agreed. These seems like reasonable changes and might not be good fit
for this patches but definitely something that we can follow up after it.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 2/7] worktree: implement worktree_prune_reason() wrapper
  2021-01-06  7:08   ` Eric Sunshine
@ 2021-01-08  7:42     ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:42 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> worktree: implement worktree_prune_reason() wrapper
>
> We might be able to give the reader more useful information in the
> subject by explaining a bit more the goal of this patch. Perhaps
> something like this:
>
>     worktree: teach worktree to lazy-load "prunable" reason
>

yeah, that's sounds better. will change it on the next revision.

>> The should_prune_worktree() machinery is used by the "prune" command to
>> identify whether a worktree is a candidate for pruning. This function
>> however, is not prepared to work directly with "struct worktree" and
>> refactoring is required not only on the function itself, but also also
>> changing get_worktrees() to return non-valid worktrees and address the
>> changes in all "worktree" sub commands.
>>
>> Instead let's implement worktree_prune_reason() that accepts
>> "struct worktree" and uses should_prune_worktree() and returns whether
>> the given worktree is a candidate for pruning. As the "list" sub command
>> already uses a list of "struct worktree", this allow to simply check if
>> the working tree prunable by passing the structure directly without the
>> others parameters.
>
> Everything through "not prepared to work directly with `struct
> worktree`" makes sense when explaining why you are adding this wrapper
> function, however, most of what follows is describing an aborted idea
> about how you originally intended to implement this. The bit about
> get_worktrees() not being able to return non-valid worktrees is
> certainly an important limitation in the overall scheme of things, but
> doesn't really help to sell the change made by this patch, and it
> probably confuses the reader who didn't also read the cover-letter,
> especially since this patch does nothing to help that situation.
>
> It also isn't really necessary to talk about `git worktree list` at
> this stage since this patch stands on its own by fleshing out the API
> without having to cite a specific client.
>

Make sense.

>> Also, let's add prune_reason field to the worktree structure that will
>> store the reason why the worktree can be pruned that is returned by
>> should_prune_worktree() when such reason is available.
>
> In my opinion, this is the real reason this patch exists, thus should
> be the focus of the commit message. All the description above this
> paragraph can likely be dropped.
>
> Taking the above comments into account, perhaps the entire commit
> message could be collapsed to something like this:
>
>     worktree: teach worktree to lazy-load "prunable" reason
>
>     Add worktree_prune_reason() to allow a caller to discover whether
>     a worktree is prunable and the reason that it is, much like
>     worktree_lock_reason() indicates whether a worktree is locked and
>     the reason for the lock. As with worktree_lock_reason(), retrieve
>     the prunable reason lazily and cache it in the `worktree`
>     structure.
>

Interesting point. Rewording the commit message like this seems better,
and as you mentioned this patch can stands on its own and can be more
clear about what the commit is introducing to the code instead of all
the previous message trying to explain why this exists.

Thank you for suggesting this commit message. I will revise and change on
the next revision.

>> diff --git a/worktree.c b/worktree.c
>> @@ -15,6 +15,7 @@ void free_worktrees(struct worktree **worktrees)
>>                 free(worktrees[i]->lock_reason);
>> +               free(worktrees[i]->prune_reason);
>>                 free(worktrees[i]);
>
> Remembering to free the prune-reason. Good.
>
>> @@ -245,6 +246,24 @@ const char *worktree_lock_reason(struct worktree *wt)
>> +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
>> +{
>> +       if (!is_main_worktree(wt)) {
>> +               char *path;
>> +               struct strbuf reason = STRBUF_INIT;
>> +
>> +               if (should_prune_worktree(wt->id, &reason, &path, expire))
>> +                       wt->prune_reason = strbuf_detach(&reason, NULL);
>> +               else
>> +                       wt->prune_reason = NULL;
>> +
>> +               free(path);
>> +               strbuf_release(&reason);
>> +       }
>> +
>> +       return wt->prune_reason;
>> +}
>
> A couple observations...
>
> I realize you patterned this after worktree_lock_reason(), however, it
> is more common in this codebase to return early from the function for
> conditions such as `is_main_worktree(wt)` which don't require any
> additional processing. One reason is that doing so allows us to lose
> an indentation level. Another is that it is easier to reason about the
> rest of the function if we get the simple cases out of the way early,
> such that we don't have to think about them again while reading the
> remainder of the code.
>
> If I'm not mistaken, the intention here was to cache `prune_reason`
> for reuse, however, this function just overwrites it each time it's
> called for a particular worktree, thus providing no caching and
> leaking the previously-retrieved reason as well. To fix this, I think
> you need to add a private `prune_reason_valid` member to `struct
> worktree` (similar to `lock_reason_valid`) and check it before calling
> should_prune_worktree().
>

Good point. I totally missed the caching aspect of the implementation
and I greed about getting the simple case out earlier in order to make it
simple to reason about it.

> Taking the above comments into account, perhaps it should be written like this:
>
>     if (is_main_worktree(wt))
>         return NULL;
>     if (wt->prune_reason_valid)
>         return wt->prune_reason;
>     if (should_prune_worktree(wt->id, &reason, &path, expire))
>         wt->prune_reason = strbuf_detach(&reason, NULL);
>     wt_prune_reason_valid = 1;
>     free(path);
>     strbuf_release(&reason);
>

Thanks. I will add this suggestion in the next revision. 

>> diff --git a/worktree.h b/worktree.h
>> @@ -11,6 +11,7 @@ struct worktree {
>>         char *id;
>>         char *head_ref;         /* NULL if HEAD is broken or detached */
>>         char *lock_reason;      /* private - use worktree_lock_reason */
>> +       char *prune_reason;     /* private - use worktree_prune_reason */
>
> As noted above, we also probably need a new `prune_reason_valid`
> member, similar to the existing `lock_reason_valid`.
>

Indeed. will add on the next revision.

>> @@ -73,6 +74,12 @@ int is_main_worktree(const struct worktree *wt);
>> +/*
>> + * Return the reason string if the given worktree should be pruned
>> + * or NULL otherwise.
>> + */
>> +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
>
> The documentation should also talk about `expire` since its purpose
> and meaning is unclear.
>

Good point. I missed adding the message here for the `expire` parameter,
will add it. 

> Nit: I realize that you patterned this description after the one for
> worktree_lock_reason(), but it's a bit unclear. Perhaps rephrasing it
> like this would help:
>
>     Return the reason the worktree should be pruned, otherwise
>     NULL if it should not be pruned.

Also make sense to rephrase like this.


Thank you for this review will address all this changes on the v2.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree
  2021-01-06  7:29   ` Eric Sunshine
@ 2021-01-08  7:43     ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:43 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> The main worktree should not be locked and the worktree_lock_reason() API
>> is aware of this fact and avoids running the check code for the main
>> worktree. This checks is done via assert() macro, Therefore the caller
>
> s/Therefore/therefore/
>

Nice catch. thanks.

>> needs to ensure the function is never called, usually by additional code.
>>
>> We can handle that case more gently by just returning false for the main
>> worktree and not bother checking if the "locked" file exists. This will
>> allowed further simplification from the caller as they will not need to
>> ensure the main worktree is never passed to the API.
>>
>> Teach worktree_lock_reason() to be more gently and just return false for
>> the main working tree.
>
> The situation is even a bit worse since the main worktree restriction
> isn't even documented. Here's a possible rewrite of the commit message
> which addresses that point too:
>
>     worktree_lock_reason() aborts with an assertion failure when
>     called on the main worktree since locking the main worktree is
>     nonsensical. Not only is this behavior undocumented, thus callers
>     might not even be aware that the call could potentially crash the
>     program, but it also forces clients to be extra careful:
>
>         if (!is_main_worktree(wt) && worktree_locked_reason(...))
>            ...
>
>     Since we know that locking makes no sense in the context of the
>     main worktree, we can simply return false for the main worktree,
>     thus making client code less complex by eliminating the need for
>     callers to have inside knowledge about the implementation:
>
>         if (worktree_locked_reason(...))
>             ...
>

Yes, this is a nicer commit message to explain the current situation and
having the code example in there makes even more clearer. Thanks for the
suggestion will definitely make this change on the next revision.

>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>> diff --git a/worktree.c b/worktree.c
>> @@ -225,9 +225,7 @@ int is_main_worktree(const struct worktree *wt)
>>  const char *worktree_lock_reason(struct worktree *wt)
>>  {
>> -       assert(!is_main_worktree(wt));
>> -
>> -       if (!wt->lock_reason_valid) {
>> +       if (!is_main_worktree(wt) && !wt->lock_reason_valid) {
>>                 struct strbuf path = STRBUF_INIT;
>
> As mentioned in my review of patch [2/7], this would be more idiomatic
> and easier to reason about if the function returns early for the main
> worktree case, thus freeing the reader from having to think about that
> case for the remainder of the code. So:
>
>     if (is_main_worktree(wt))
>         return NULL;
>     if (!wt->lock_reason_valid) {
>         ...
>
> Subjective, and not necessarily worth a re-roll, though.

I'm inclined to change here on the next revision as I will do the same
for the patch [2/7] with the worktree_prune_reason() and use the same
pattern for both functions.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 4/7] worktree: teach `list` prunable annotation and verbose
  2021-01-06  8:31   ` Eric Sunshine
@ 2021-01-08  7:45     ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:45 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> The "git worktree list" command shows the absolute path to the worktree,
>> the commit that is checked out, the name of the branch, and a "locked"
>> annotation if the worktree is locked. It is not clear whether a worktree,
>> if any, is prunable.
>
> Maybe this could just say...
>
>     ... "locked" annotation if the worktree is locked,
>     however, it does not indicate whether it is prunable.
>

Make sense.

>> The "prune" command will remove a worktree in case
>> is a prunable candidate unless --dry-run option is specified. This could
>
> s/case is/case it is/
>
> Or better:
>
>     ... will remove a worktree if it is prunable unless...
>

Yeah, it seems better.

>> lead to a worktree being removed without the user realizing before is to
>> late, in case the user forgets to pass --dry-run for instance.
>
> s/before is/before it is/
> s/to/too/
>

Nice catch, thanks.

>> If the "list" command shows which worktree is prunable, the user could
>> verify before running "git worktree prune" and hopefully prevents the
>> working tree to be removed "accidently" on the worse case scenario.
>>
>> Let's teach "git worktree list" to show when a worktree is prunable by
>> appending "prunable" text to its output by default and show a prunable
>> label for the porcelain format followed by the reason, if the avaiable.
>> While at it, let's do the same for the "locked" annotation.
>
> s/avaiable/available/
>

Another good catch, thanks. 

>> Also, the worktree_prune_reason() stores the reason why git is selecting
>> the worktree to be pruned, so let's leverage that and also display this
>> information to the user. However, the reason is human readable text that
>> can take virtually any size which might make harder to extend the "list"
>> command with additional annotations and not fit nicely on the screen.
>>
>> In order to address this shortcoming, let's teach "git worktree list" to
>> take a verbose option that will output the prune reason on the next line
>> indented, if the reason is available, otherwise the annotation is kept on
>> the same line. While at it, let's do the same for the "locked"
>> annotation.
>
> This is a lot of changes for one patch to be making, and it's hard for
> a reader to digest all those changes from the commit message. I think
> I counted four distinct changes being made here:
>
> 1. extend porcelain to include lock line (with optional reason)
> 2. add prunable annotation to normal list output
> 3. add prune line (with optional reason) to porcelain output
> 4. extend normal list output with --verbose to include reasons
>
> The patch itself is not overly large or complicated, so perhaps
> combining all these changes together is reasonable, although I'm quite
> tempted to ask for them to be separated into at least four patches
> (probably in the order shown above). A benefit of splitting them into
> distinct patches is that you can add targeted documentation and test
> updates to each individual patch rather than saving all the
> documentation and test updates for a single (large) patch at the end
> of the series. This helps reviewers reason about the changes more
> easily since they get to see how each change impacts the documentation
> and tests rather than having to wait for a patch late in the series to
> make all the documentation and test updates, at which point the
> reviewers may have forgotten details of the earlier patches.
>
> (I've come back here after reading and reviewing the patch itself, and
> I'm on the fence as to whether to suggest splitting this into multiple
> patches. The changes in this patch are easy enough to understand and
> digest, so I'm not convinced it makes sense to ask you to do all the
> extra work of splitting it into smaller pieces. On the other hand, it
> might be nice to have each documentation and test update done in the
> patch which makes each particular change. So, I don't have a good
> answer. Use your best judgment and do the amount of work you feel is
> appropriate.)
>

Interesting point. During the creation of this series it was not clear
to me how to properly split them in a way that was easier to review it.

I'm inclined to split the series in the way that you are suggesting,
specially about having the tests and documentation together. I believe
this also make sense to reason about in the future when looking over the
commit history of the project without needing to go back and forth
to understand the documentation vs implementation vs testing.

>> The output of "git worktree list" with verbose becomes like so:
>>
>>     $ git worktree list --verbose
>>     /path/to/main             aba123 [main]
>>     /path/to/locked           acb124 [branch-a] locked
>>     /path/to/locked-reason    acc125 [branch-b]
>>         locked: worktree with locked reason
>>     /path/to/prunable         acd126 [branch-c] prunable
>>     /path/to/prunable-reason  ace127 [branch-d]
>>         prunable: gitdir file points to non-existent location
>
> One "weird" aesthetic issue is that if the lock reason contains
> newlines, the subsequent lines of the lock reason are not indented.
> However, this is a very minor point and I don't think this patch
> series should worry about it. We can think about how to address it
> some time in the future if someone ever complains about it.
>

Yes, that is something that was scratching my head and it was not clear
whether something that we should roll out with this patch. But I agree,
that we can address this time in the future.

>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>>  builtin/worktree.c | 35 +++++++++++++++++++++++++++++++++--
>>  1 file changed, 33 insertions(+), 2 deletions(-)
>>
>> diff --git a/builtin/worktree.c b/builtin/worktree.c
>> index eeb3ffaed0..dedd4089e5 100644
>> --- a/builtin/worktree.c
>> +++ b/builtin/worktree.c
>> @@ -578,6 +578,20 @@ 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
>> +                               printf("locked\n");
>> +               }
>> +
>> +               if (worktree_prune_reason(wt, expire)) {
>> +                       if (*wt->prune_reason)
>> +                               printf("prunable %s\n", wt->prune_reason);
>> +                       else
>> +                               printf("prunable\n");
>> +               }
>
> A couple observations...
>
> As a consumer of `struct worktree`, builtin/worktree.c should not be
> poking at or accessing the structure's private fields `prune_reason`
> and `lock_reason`; instead it should be retrieving those values via
> worktree_prune_reason() and worktree_lock_reason() which are part of
> the public API.
>

Make sense.

> If a worktree is prunable, then worktree_prune_reason() will
> unconditionally return a (non-empty) string; if it is not prunable,
> then it will unconditionally return NULL. This means that the
> `printf("prunable\n")` case is dead-code; it will never be reached.
> This differs from worktree_lock_reason() which can return an empty
> string to indicate a locked worktree for which no reason has been
> specified.
>

Great point. I will remove the dead-code from the implementation as that
state is never going to be reached given the current implementation of
worktree_prune_reason().

Thanks.

> Taking the above observations into account, a reasonable rewrite would be:
>
>     const char *reason;
>
>     reason = worktree_lock_reason(wt);
>     if (reason && *reason)
>         printf("locked %s\n", reason);
>     else if (reason)
>         printf("locked\n");
>
>     reason = worktree_prune_reason(wt, expire);
>     if (reason)
>         printf("prunable %s\n", reason);
>

Yeah, this suggested snippet seems like sensible rewrite and much easier
to reason about.

>> @@ -604,8 +618,19 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
>>                         strbuf_addstr(&sb, "(error)");
>>         }
>>
>> -       if (!is_main_worktree(wt) && worktree_lock_reason(wt))
>> -               strbuf_addstr(&sb, " locked");
>> +       if (worktree_lock_reason(wt)) {
>> +               if (verbose && *wt->lock_reason)
>> +                       strbuf_addf(&sb, "\n\tlocked: %s", wt->lock_reason);
>> +               else
>> +                       strbuf_addstr(&sb, " locked");
>> +       }
>> +
>> +       if (worktree_prune_reason(wt, expire)) {
>> +               if (verbose && *wt->prune_reason)
>> +                       strbuf_addf(&sb, "\n\tprunable: %s", wt->prune_reason);
>> +               else
>> +                       strbuf_addstr(&sb, " prunable");
>> +       }
>
> Same observations here about using public API rather than touching
> private `struct worktree` fields, and about the final `else` case
> being dead code.
>

Make sense.

>> @@ -650,12 +675,18 @@ static int list(int ac, const char **av, const char *prefix)
>>                 OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
>> +               OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
>> +               OPT_EXPIRY_DATE(0, "expire", &expire,
>> +                               N_("show working trees that is candidate to be pruned older than <time>")),
>
> Perhaps:
>
>     "add 'prunable' annotation to worktrees older than <time>"
>

Make sense.

>>                 OPT_END()
>>         };
>>
>> +       expire = TIME_MAX;
>
> Good, this mirrors how prune() initializes this variable.


-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 5/7] worktree: `list` escape lock reason in --porcelain
  2021-01-05 10:29   ` Phillip Wood
  2021-01-05 11:02     ` [PATCH] worktree: add -z option for list subcommand Phillip Wood
  2021-01-06  9:07     ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Eric Sunshine
@ 2021-01-08  7:47     ` Rafael Silva
  2 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:47 UTC (permalink / raw)
  To: phillip.wood; +Cc: git, Eric Sunshine


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.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 6/7] worktree: add tests for `list` verbose and annotations
  2021-01-06  9:39   ` Eric Sunshine
  2021-01-07  4:09     ` Eric Sunshine
@ 2021-01-08  7:49     ` Rafael Silva
  1 sibling, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:49 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> Add tests for "git worktree list" verbose mode, prunable and locked
>> annotations for both default and porcelain format and ensure the
>> "prunable" annotation is consistent with what "git worktree prune"
>> command will eventually remove. Additionally, add one test case to
>> ensure any newline characters are escaped from locked reason for the
>> porcelain format to prevent breaking the format.
>>
>> The c57b3367be (worktree: teach `list` to annotate locked worktree,
>> 2020-10-11) introduced a new test to ensure locked worktrees are listed
>> with "locked" annotation. However, the test does not remove the worktree
>> as the "git worktree prune" is not going to remove any locked worktrees.
>> Let's fix that by unlocking the worktree before the "prune" command.
>>
>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
>> @@ -62,7 +62,9 @@ test_expect_success '"list" all worktrees --porcelain' '
>>  test_expect_success '"list" all worktrees with locked annotation' '
>> -       test_when_finished "rm -rf locked unlocked out && git worktree prune" &&
>> +       test_when_finished "rm -rf locked unlocked out &&
>> +               git worktree unlock locked &&
>> +               git worktree prune" &&
>>         git worktree add --detach locked master &&
>>         git worktree add --detach unlocked master &&
>>         git worktree lock locked &&
>
> You might need to be a bit more careful here. If the test fails before
> the worktree is locked, then the `git worktree unlock` in the cleanup
> code will return an error, which will make the code executed by
> test_when_finished() fail, as well, which makes it harder to debug
> problems. Moving the `unlock` cleanup after you know the lock
> succeeded will address this issue:
>
>     test_when_finished "rm -rf locked unlocked out && git worktree prune" &&
>     git worktree add --detach locked master &&
>     git worktree add --detach unlocked master &&
>     git worktree lock locked &&
>     test_when_finished "git worktree unlock locked" &&
>     ...
>
> Same comment applies to other new tests added by this patch which lock
> worktrees.
>

This is good point. will change on the next revision.

>> @@ -71,6 +73,96 @@ test_expect_success '"list" all worktrees with locked annotation' '
>> +test_expect_success '"list" all worktrees with prunable consistent with "prune"' '
>> +       test_when_finished "rm -rf prunable out && git worktree prune" &&
>> +       git worktree add --detach prunable &&
>> +       rm -rf prunable &&
>> +       git worktree list >out &&
>> +       grep "/prunable  *[0-9a-f].* prunable$" out &&
>> +       git worktree prune --verbose >out &&
>> +       test_i18ngrep "^Removing worktrees/prunable" out
>> +'
>
> To be trustworthy, doesn't this test also need to have an unprunable
> worktree, and test that `git worktree list` doesn't annotate it as
> "prunable" _and_ that `git worktree prune` didn't prune it? Otherwise,
> we really don't know that the two commands agree about what is and is
> not prunable -- we only know they agree that this particular worktree
> was prunable.
>

You're right this test is missing the case the "unprunable case". Will
add into the next revision.

>> +test_expect_success '"list" all worktrees --porcelain with newline escaped locked reason' '
>> +       test_when_finished "rm -rf locked_lf locked_crlf reason_lf reason_crlf out actual expect reason &&
>> +               git worktree unlock locked_lf &&
>> +               git worktree unlock locked_crlf &&
>> +               git worktree prune" &&
>
> Nit: It's not a big deal, but we don't normally bother cleaning up
> every junk file in tests, such as `out`, `actual`, `expect` if those
> files aren't going to be a problem for subsequent tests. We are
> explicitly cleaning up the worktrees in these tests because this
> script is all about testing worktree behavior, and some random
> leftover worktree could be a problem for other tests. I don't care
> strongly one way or the other, but I worry a tiny bit that the list of
> files being cleaned up could become outdated as changes are made to
> the tests later...
>

Make sense, and ...

>> +test_expect_success '"list" all worktrees --porcelain with prunable' '
>> +       test_when_finished "rm -rf prunable list out && git worktree prune" &&
>> +       git worktree add --detach prunable &&
>> +       rm -rf prunable &&
>> +       git worktree list --porcelain >out &&
>> +       test_i18ngrep "^prunable gitdir file points to non-existent location$" out
>> +'
>
> ... for instance, the file `list` being cleaned up in this test is not
> even created by this test.
>

This is exactly the case of updating the test and forgetting to
remove from the cleanup part because it was not used anymore. I'm also
inclined to make this changes on the next revision.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 7/7] worktree: document `list` verbose and prunable annotations
  2021-01-06  9:57   ` Eric Sunshine
@ 2021-01-08  7:49     ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-08  7:49 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List


Eric Sunshine writes:

> On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> Update the documentation with "git worktree list" verbose mode, prunable
>> and locked annotations for the default and porcelain format as part of
>> teaching the command to show prunable working trees and verbose mode.
>>
>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
>> @@ -226,9 +227,12 @@ This can also be set up as the default behaviour by using the
>>  -v::
>>  --verbose::
>>         With `prune`, report all removals.
>> +       With `list`, output additional information for working trees.
>
> This leaves the reader wondering what additional information is output
> for `list`. Perhaps a small tweak will help:
>
>     With `list`, output additional information about worktrees (see below).
>

Make sense.

>>  --expire <time>::
>>         With `prune`, only expire unused working trees older than `<time>`.
>> +       With `list`, annotate unused working trees older than `<time>` as prunable
>> +       candidates that will be remove by `prune` command if the same option is used.
>
> Perhaps just minimal:
>
>     With `list`, annotate missing worktrees as prunable if they
>     are older than `<time>`.
>

Sounds reasonable.

>> @@ -367,13 +371,48 @@ $ git worktree list
>> +The command also shows annotations for each working tree, according to its state.
>> +These annotations are:
>> +
>> + * "locked", if any working tree is locked
>> + * "prunable", if any working tree can be pruned via "git worktree prune".
>
> s/any/the/g
>
> We might want to use backticks around these annotations rather than
> double quotes, and we certainly do want to use backticks around the
> `git worktree prune` command to ensure it is styled consistently with
> other commands in this document.
>

Yes, good catch. It make sense to have backticks here.

>> +Note that, the annotation is only moved to the next line only if the
>> +additional text is available, otherwise the text is kept on the same.
>
> Drop the comma between "that, the". Also, too many "only"s in this
> sentence. You can actually drop both of them and the sentence will
> still read fine:
>
>    Note that the annotation is moved to the next line if the
>    additional information is available, otherwise it stays on
>    the same line as the worktree itself.
>
> or something.
>

Thanks for the alternative suggestion. It reads better like this will
add change the patch message to something more close to your suggestion.

>>  Porcelain Format
>>  ~~~~~~~~~~~~~~~~
>>  The porcelain format has a line per attribute.  Attributes are listed with a
>>  label and value separated by a single space.  Boolean attributes (like `bare`
>>  and `detached`) are listed as a label only, and are present only
>>  if the value is true.  The first attribute of a working tree is always
>> -`worktree`, an empty line indicates the end of the record.  For example:
>> +`worktree`, an empty line indicates the end of the record.
>> ++
>> +In case any of the working trees are locked or is a candidate for pruning
>> +(See DESCRIPTION above) the labels "locked" and "prunable" is also shown
>> +followed by a reason, if available, otherwise only the labels are listed.
>> +For example:
>
> s/(See/(see/
> s/is also/are also/
>
> Let's also use backticks rather than double quotes around `locked` and
> `prunable` to ensure consistent formatting with the other porcelain
> labels `bare` and `detached` which are already in backticks.
>

Nice catch. 

> Also, the unnecessary `+` line (seen as `++` in the diff) makes this
> render incorrectly. It renders as:
>
>     +In case any...
>
> To fix it, just leave the line blank between paragraphs.
>
> (If possible, install `asciidoc` and `xmlto` and then run `make html`
> to render the documentation yourself, and open
> Documentation/git-worktree.html in your browser to check the output.)

Sure thing. thanks for the suggestion. I should have rendered the
documentation before sending. Apologize for that.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-08  7:38   ` Rafael Silva
@ 2021-01-08  8:19     ` Eric Sunshine
  0 siblings, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-08  8:19 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List

On Fri, Jan 8, 2021 at 2:38 AM Rafael Silva <rafaeloliveira.cs@gmail.com> wrote:
> Eric Sunshine writes:
> > On Mon, Jan 4, 2021 at 11:22 AM Rafael Silva
> > <rafaeloliveira.cs@gmail.com> wrote:
> >> The fifth patch adds worktree_escape_reason() that accepts a (char *)
> >> text and returned the text with any LF or CRLF escaped. [...]
> >
> > Perhaps I misunderstand, but I had envisioned employing one of the
> > codebase's existing quoting/escaping functions rather than crafting a
> > new one from scratch. However, I'll reserve judgment until I actually
> > read the patch.
>
> Agreed. It make sense to reuse one of the already implemented functions
> from the code base. for some reason I was not able to find it. I believe
> this was cleared out in one of the patches replies by you and Phillip Wood.

No need to apologize. It's a big project and it can be difficult to
discover existing utility functions. Fortunately, reviewers can often
point out useful alternatives.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH] worktree: add -z option for list subcommand
  2021-01-07  3:34       ` Eric Sunshine
@ 2021-01-08 10:33         ` Phillip Wood
  2021-01-10  7:27           ` Eric Sunshine
  0 siblings, 1 reply; 88+ messages in thread
From: Phillip Wood @ 2021-01-08 10:33 UTC (permalink / raw)
  To: Eric Sunshine, Phillip Wood; +Cc: Git Mailing List, Rafael Silva

Hi Eric & Rafael

On 07/01/2021 03:34, Eric Sunshine wrote:
> On Tue, Jan 5, 2021 at 6:02 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>> Add a -z option to be used in conjunction with --porcelain that gives
>> NUL-terminated output. This enables 'worktree list --porcelain' to
>> handle worktree paths that contain newlines.
> 
> Adding a -z mode makes a lot of sense. This, along with a fix to call
> quote_c_style() on paths in normal (not `-z`) porcelain mode, 

I'm concerned that starting to quote paths will break backwards 
compatibility. Even if we restricted the quoting to just those paths 
that contain '\n' there is no way to distinguish between a quoted path 
and one that begins and ends with ". This is the reason I prefer to add 
`-z` instead of taking Rafael's patch to quote the lock reason as that 
patch still leaves the output of `git worktree list --porcelain` 
ambiguous and it cannot be fixed without breaking existing users. A 
counter argument to all this is that there are thousands of users on 
file systems that cannot have newlines in paths and Rafael's patch is 
definitely a net win for them.

> can
> easily be done after Rafael's series lands. Or they could be done
> before his series, though that might make a lot of extra work for him.

I would definitely be easiest to wait for Rafael's series to land if we 
want to add `-z` separately

>> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
>> ---
>> I found an old patch that added NUL termination. I've rebased it
>> but the new test fails as there seems to be another worktree thats
>> been added since I wrote this, anyway I thought it might be a useful
>> start for adding a `-z` option.
> 
> The test failure is fallout from a "bug" in a test added by Rafael's
> earlier series[1] which was not caught by the reviewer[2]. In fact,
> his current series has a drive-by fix[3] for this bug in patch [6/7].
> Applying that fix (or the refinement[4] I suggested in my review)
> makes your test work.

Thanks ever so much for taking the time to track down the cause of the 
test failure.

Best Wishes

Phillip

> [1]: https://lore.kernel.org/git/20201011101152.5291-2-rafaeloliveira.cs@gmail.com/
> [2]: https://lore.kernel.org/git/CAPig+cS8hvX4GCsnfLBnQ4Q_AkUad=bw7rjVcaOqSEqcLZvx8w@mail.gmail.com/
> [3]: https://lore.kernel.org/git/20210104162128.95281-7-rafaeloliveira.cs@gmail.com/
> [4]: https://lore.kernel.org/git/CAPig+cRysXpK0e1xXOuVd+EtkeyTk8FR6MUrL=Bg3W4ve8jdNA@mail.gmail.com/
> 
>> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
>> @@ -217,7 +217,13 @@ This can also be set up as the default behaviour by using the
>> +-z::
>> +       When `--porcelain` is specified with `list` terminate each line with a
>> +       NUL rather than a newline. This makes it possible to parse the output
>> +       when a worktree path contains a newline character.
> 
> If we fix normal-mode porcelain to call quote_c_style(), then we can
> drop the last sentence or refine it to say something along the lines
> of it being easier to deal with paths with embedded newlines than in
> normal porcelain mode.
> 
>> diff --git a/builtin/worktree.c b/builtin/worktree.c
>> @@ -640,19 +640,25 @@ static int add(int ac, const char **av, const char *prefix)
>> +static void show_worktree_porcelain(struct worktree *wt, int line_terminator)
>>   {
>> +       printf("worktree %s", wt->path);
>> +       fputc(line_terminator, stdout);
>> +       if (wt->is_bare) {
>> +               printf("bare");
>> +               fputc(line_terminator, stdout);
>> +       } else {
>> +               printf("HEAD %s", oid_to_hex(&wt->head_oid));
>> +               fputc(line_terminator, stdout);
>> +               if (wt->is_detached) {
>> +                       printf("detached");
>> +                       fputc(line_terminator, stdout);
>> +               } else if (wt->head_ref) {
>> +                       printf("branch %s", wt->head_ref);
>> +                       fputc(line_terminator, stdout);
>> +               }
> 
> Perhaps this could all be done a bit more concisely with printf()
> alone rather than combining it with fputc(). For instance:
> 
>      printf("worktree %s%c", wt->path, line_terminator);
> 
> and so on.
> 
>> -       printf("\n");
>> +       fputc(line_terminator, stdout);
> 
> This use of fputc() makes sense.
> 
>> @@ -720,15 +726,20 @@ static void pathsort(struct worktree **wt)
>>   static int list(int ac, const char **av, const char *prefix)
>>   {
>> +               OPT_SET_INT('z', NULL, &line_terminator,
>> +                           N_("fields are separated with NUL character"), '\0'),
> 
> "fields" sounds a little odd. "lines" might make more sense. "records"
> might be even better and matches the wording of some other Git
> commands accepting `-z`.
> 
>> +       else if (!line_terminator && !porcelain)
>> +               die(_("'-z' requires '--porcelain'"));
> 
> Other error messages in this file don't quote command-line options, so:
> 
>      die(_("-z requires --porcelain"));
> 
> would be more consistent.
> 
>> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
>> @@ -71,6 +71,28 @@ test_expect_success '"list" all worktrees with locked annotation' '
>> +test_expect_success '"list" all worktrees --porcelain -z' '
>> +       test_when_finished "rm -rf here _actual actual expect &&
>> +                               git worktree prune" &&
>> +       printf "worktree %sQHEAD %sQbranch %sQQ" \
>> +               "$(git rev-parse --show-toplevel)" \
>> +               "$(git rev-parse HEAD)" \
>> +               "$(git symbolic-ref HEAD)" >expect &&
>> +       git worktree add --detach here master &&
>> +       printf "worktree %sQHEAD %sQdetachedQQ" \
>> +               "$(git -C here rev-parse --show-toplevel)" \
>> +               "$(git rev-parse HEAD)" >>expect &&
>> +       git worktree list --porcelain -z >_actual &&
>> +       cat _actual | tr "\0" Q >actual &&
> 
> Or just use nul_to_q():
> 
>      nul_to_q <_actual >actual &&
> 
> (And there's no need to `cat` the file.)
> 
>> +       test_cmp expect actual
>> +'
>> +
>> +test_expect_success '"list" -z fails without --porcelain' '
>> +       test_when_finished "rm -rf here && git worktree prune" &&
>> +       git worktree add --detach here master &&
>> +       test_must_fail git worktree list -z
>> +'
> 
> I don't think there's any need for this test to create (and cleanup) a
> worktree. So, the entire test could collapse to:
> 
>      test_expect_success '"list" -z fails without --porcelain' '
>          test_must_fail git worktree list -z
>      '
> 

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH] worktree: add -z option for list subcommand
  2021-01-08 10:33         ` Phillip Wood
@ 2021-01-10  7:27           ` Eric Sunshine
  0 siblings, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-10  7:27 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Git Mailing List, Rafael Silva

On Fri, Jan 8, 2021 at 5:33 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> On 07/01/2021 03:34, Eric Sunshine wrote:
> > On Tue, Jan 5, 2021 at 6:02 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> >> Add a -z option to be used in conjunction with --porcelain that gives
> >> NUL-terminated output. This enables 'worktree list --porcelain' to
> >> handle worktree paths that contain newlines.
> >
> > Adding a -z mode makes a lot of sense. This, along with a fix to call
> > quote_c_style() on paths in normal (not `-z`) porcelain mode,
>
> I'm concerned that starting to quote paths will break backwards
> compatibility. Even if we restricted the quoting to just those paths
> that contain '\n' there is no way to distinguish between a quoted path
> and one that begins and ends with ".

Backward compatibility is a valid concern, though I haven't managed to
convince myself that it would matter much in this case. In one sense,
the failure of the porcelain format to properly quote/escape paths
when needed can be viewed as an outright bug and, although we value
backward compatibility, we also value correctness, and such bug fixes
have been accepted in the past. Especially in a case such as this, it
seems exceedingly unlikely that fixing the bug would be harmful or
break existing tooling (though, of course that possibility always
exists, even if remotely so).

I can imagine ways in which tooling might be engineered to work around
the shortcoming that `git worktree list --porcelain` doesn't properly
handle newlines embedded in paths, but such tooling would almost
certainly be so fragile anyhow that it would break as we add more keys
to the extensible porcelain format. Coupled with the fact that
newlines embedded in paths are so exceedingly unlikely, it's hard to
imagine that fixing this bug would have a big impact on existing
tooling.

The case you mention about a path which happens to have a double-quote
as its first character does concern me a bit more since existing
tooling wouldn't have had to jump through hoops, or indeed do anything
special, with such paths, unlike the embedded newline case. But then,
too, it's pretty hard to imagine this coming up much, if at all, in
practice. That's not to say that I can't imagine a path, in general,
beginning with a quote, but keeping in mind that we're talking only
about worktree paths, it seems exceedingly unlikely.

My gut feeling (for what it's worth) is that worktree paths containing
embedded newlines (or other special characters) or beginning with a
double-quote is so unlikely to come in in actual practice that viewing
this as a bug fix is probably a reasonable approach, whereas some
other approach -- such as introducing porcelain-v2 or creating a new
porcelain key, say "worktreepath" which supersedes "worktree" (without
retiring "worktree") -- may be overkill.

None of the above is an argument against a complementary `-z` mode,
which I think is a very good idea.

> This is the reason I prefer to add
> `-z` instead of taking Rafael's patch to quote the lock reason as that
> patch still leaves the output of `git worktree list --porcelain`
> ambiguous and it cannot be fixed without breaking existing users. A
> counter argument to all this is that there are thousands of users on
> file systems that cannot have newlines in paths and Rafael's patch is
> definitely a net win for them.

Rafael's patch is quoting only the lock-reason, not the worktree path,
so I think it's orthogonal to this discussion. Also, his patch is
introducing `lock` as a new attribute in porcelain output, not
modifying behavior of an existing `lock` attribute.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH v2 0/6] teach `worktree list` verbose mode and prunable annotations
  2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
                   ` (7 preceding siblings ...)
  2021-01-06  5:36 ` [PATCH 0/7] teach `worktree list` verbose mode " Eric Sunshine
@ 2021-01-17 23:42 ` Rafael Silva
  2021-01-17 23:42   ` [PATCH v2 1/6] worktree: libify should_prune_worktree() Rafael Silva
                     ` (7 more replies)
  8 siblings, 8 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-17 23:42 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

In c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) we taught `git worktree list` to annotate working tree that
is locked by appending "locked" text in order to signalize to the user
that a working tree is locked.  During the review, there was some
discussion about additional annotations and information that `list`
command could provide to the user that has long been envisioned and
mentioned in [2], [3] and [4].

This patch series addresses some of these changes by teaching
`worktree list` to show "prunable" annotation, adding verbose mode and
extending the --porcelain format with prunable and locked annotation as
follow up from [1]. Additionally, it addresses one shortcoming for porcelain
format to escape any newline characters (LF or CRLF) for the lock reason
to prevent breaking the format that is mentioned in [4] and [1] during the
review cycle.

The patch series is organizes as:

1. The first patch moves the should_prune_worktree() machinery to the top-level
   worktree.c exposing the function as general API, that will be reference
   by should_prune_worktree() wrapper implemented on the second patch.

   The original idea was to not only move should_prune_worktree() but also
   refactor to accept a "struct worktree" and load the information directly,
   which allows simplify the `prune` command by reusing get_worktrees().
   However this seems to also require refactoring get_worktrees() itself
   to return "non-valid" working trees that can/should be pruned. This is
   also mentioned in [5]. Having the wrapper function makes it easier to add
   the prunable annotation without touching the get_worktrees() and the
   other worktree sub commands. The refactoring can be addressed in a
   future patch, if this turns out to be good idea. One possible approach
   is to teach get_worktrees() to take additional flags that will tell
   whether to return only valid or all worktrees in GIT_DIR/worktrees/
   directory and address its own possible shortcoming.

2. Introduces the worktree_prune_reason() to discovery whether a worktree
   is prunable along with two new fields in the `worktree` structure. The
   function mimics the workree_lock_reason() API.

3. The third patch changes worktree_lock_reason() to be more gentle for
   the main working tree to simply returning NULL instead of aborting the
   program via assert() macro. This allow us to simplify the code that
   checks if the working tree is locked for default and porcelain format.
   This changes is also mentioned in [6].

4. The fourth patch adds the "locked" attribute for the porcelain format
   in order to make both default and --porcelain format consistent.

5. The fifth patch introduces "prunable" annotation for both default
   and --porcelain format.

6. The sixth patch introduces verbose mode to expand the `list` default
   format and show each annotation reason when its available.

Series is built on top of 66e871b664 (The third batch, 2021-01-15).

[1] https://lore.kernel.org/git/20200928154953.30396-1-rafaeloliveira.cs@gmail.com/
[2] https://lore.kernel.org/git/CAPig+cQF6V8HNdMX5AZbmz3_w2WhSfA4SFfNhQqxXBqPXTZL+w@mail.gmail.com/
[3] https://lore.kernel.org/git/CAPig+cSGXqJuaZPhUhOVX5X=LMrjVfv8ye_6ncMUbyKox1i7QA@mail.gmail.com/
[4] https://lore.kernel.org/git/CAPig+cTitWCs5vB=0iXuUyEY22c0gvjXvY1ZtTT90s74ydhE=A@mail.gmail.com/
[5] https://lore.kernel.org/git/CACsJy8ChM99n6skQCv-GmFiod19mnwwH4j-6R+cfZSiVFAxjgA@mail.gmail.com/
[6] https://lore.kernel.org/git/xmqq8sctlgzx.fsf@gitster.c.googlers.com/

Changes in v2
=============

v2 changes considerably from v1 taking into account several comments
suggested by Eric Sunshine and Phillip Wood (thanks guys for the
insightful comments and patience reviewing my patches :) ).

 * The biggest change are the way the series is organized. In v1 all the
   code was implemented in the fourth patch, all the tests and documentation
   updates was added in sixth and seventh patch respectfully, in v2 each
   patch introduces the annotations and verbose mode together with theirs
   respective test and documentation updates.

 * Several rewrite of the commit messages

 * In v1 the fifth patch was introducing a new function to escape newline
   characters for the "locked" attribute. However, we already have this
   feature from the quote.h API. So the new function is dropped and the
   changes are squashed into v2's fourth patch.

 * The new prunable annotation and locked annotation that was introduced
   by [1] was refactor to not poke around the worktree private fields.

 * Refactoring of the worktree_prune_reason() to cache the prune_reason
   and refactor to early return the use cases following the more common
   pattern used on Git's codebase.

 * Few documentation rewrites, most notably the `--verbose` and `--expire`
   doc sentences for the list command are moved to its own line to clearly
   separate the description from the others commands instead of continuing 
   on the same paragraph.

 * The `git unlock <worktree>` used in the test's cleanup is moved to after
   we know the `git worktree locked` is executed successfully. This ensures
   the unlock doesn't error in the cleanup stage which will make it harder
   to debug the tests.

Rafael Silva (6):
  worktree: libify should_prune_worktree()
  worktree: teach worktree to lazy-load "prunable" reason
  worktree: teach worktree_lock_reason() to gently handle main worktree
  worktree: teach `list --porcelain` to annotate locked worktree
  worktree: teach `list` to annotate prunable worktree
  worktree: teach `list` verbose mode

 Documentation/git-worktree.txt |  62 +++++++++++++++++--
 builtin/worktree.c             | 110 +++++++++++----------------------
 t/t2402-worktree-list.sh       |  91 +++++++++++++++++++++++++++
 worktree.c                     |  91 ++++++++++++++++++++++++++-
 worktree.h                     |  23 +++++++
 5 files changed, 297 insertions(+), 80 deletions(-)

Range-diff against v1:
1:  9d3fff52b4 ! 1:  e4df49d882 worktree: move should_prune_worktree() to worktree.c
    @@ Metadata
     Author: Rafael Silva <rafaeloliveira.cs@gmail.com>
     
      ## Commit message ##
    -    worktree: move should_prune_worktree() to worktree.c
    +    worktree: libify should_prune_worktree()
     
         As part of teaching "git worktree list" to annotate worktree that is a
         candidate for pruning, let's move should_prune_worktree() from
         builtin/worktree.c to worktree.c in order to make part of the worktree
         public API.
     
    -    This function will be used by another API function, implemented in the
    -    next patch that will accept a pointer to "worktree" structure directly
    -    making it easier to implement the "prunable" annotations.
    +    should_prune_worktree() knows how to select the given worktree for
    +    pruning based on an expiration date, however the expiration value is
    +    stored in a static file-scope variable and it is not local to the
    +    function. In order to move the function, teach should_prune_worktree()
    +    to take the expiration date as an argument and document the new
    +    parameter that is not immediately obvious.
     
    -    should_prune_worktree() knows how to select the given worktree for pruning
    -    based on an expiration date, however the expiration value is stored on a
    -    global variable and it is not local to the function. In order to move the
    -    function, teach should_prune_worktree() to take the expiration date as an
    -    argument.
    +    Also, change the function comment to clearly state that the worktree's
    +    path is returned in `wtpath` argument.
     
    +    Helped-by: Eric Sunshine <sunshine@sunshineco.com>
         Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
     
      ## builtin/worktree.c ##
    @@ worktree.c: void repair_worktree_at_path(const char *path,
      	strbuf_release(&dotgit);
      }
     +
    -+/*
    -+ * Return true if worktree entry should be pruned, along with the reason for
    -+ * pruning. Otherwise, return false and the worktree's path, or NULL if it
    -+ * cannot be determined. Caller is responsible for freeing returned path.
    -+ */
     +int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
     +{
     +	struct stat st;
    @@ worktree.h: int is_main_worktree(const struct worktree *wt);
      
     +/*
     + * Return true if worktree entry should be pruned, along with the reason for
    -+ * pruning. Otherwise, return false and the worktree's path, or NULL if it
    -+ * cannot be determined. Caller is responsible for freeing returned path.
    ++ * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
    ++ * NULL if it cannot be determined. Caller is responsible for freeing
    ++ * returned path.
    ++ *
    ++ * `expire` defines a grace period to prune the worktree when its path
    ++ * does not exist.
     + */
     +int should_prune_worktree(const char *id,
     +			  struct strbuf *reason,
2:  80044e0ba9 ! 2:  d8db43fec8 worktree: implement worktree_prune_reason() wrapper
    @@ Metadata
     Author: Rafael Silva <rafaeloliveira.cs@gmail.com>
     
      ## Commit message ##
    -    worktree: implement worktree_prune_reason() wrapper
    +    worktree: teach worktree to lazy-load "prunable" reason
     
    -    The should_prune_worktree() machinery is used by the "prune" command to
    -    identify whether a worktree is a candidate for pruning. This function
    -    however, is not prepared to work directly with "struct worktree" and
    -    refactoring is required not only on the function itself, but also also
    -    changing get_worktrees() to return non-valid worktrees and address the
    -    changes in all "worktree" sub commands.
    -
    -    Instead let's implement worktree_prune_reason() that accepts
    -    "struct worktree" and uses should_prune_worktree() and returns whether
    -    the given worktree is a candidate for pruning. As the "list" sub command
    -    already uses a list of "struct worktree", this allow to simply check if
    -    the working tree prunable by passing the structure directly without the
    -    others parameters.
    -
    -    Also, let's add prune_reason field to the worktree structure that will
    -    store the reason why the worktree can be pruned that is returned by
    -    should_prune_worktree() when such reason is available.
    +    Add worktree_prune_reason() to allow a caller to discover whether a
    +    worktree is prunable and the reason that it is, much like
    +    worktree_lock_reason() indicates whether a worktree is locked and the
    +    reason for the lock. As with worktree_lock_reason(), retrieve the
    +    prunable reason lazily and cache it in the `worktree` structure.
     
    +    Helped-by: Eric Sunshine <sunshine@sunshineco.com>
         Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
     
      ## worktree.c ##
    @@ worktree.c: const char *worktree_lock_reason(struct worktree *wt)
      
     +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
     +{
    -+	if (!is_main_worktree(wt)) {
    -+		char *path;
    -+		struct strbuf reason = STRBUF_INIT;
    ++	struct strbuf reason = STRBUF_INIT;
    ++	char *path;
     +
    -+		if (should_prune_worktree(wt->id, &reason, &path, expire))
    -+			wt->prune_reason = strbuf_detach(&reason, NULL);
    -+		else
    -+			wt->prune_reason = NULL;
    ++	if (is_main_worktree(wt))
    ++		return NULL;
    ++	if (wt->prune_reason_valid)
    ++		return wt->prune_reason;
     +
    -+		free(path);
    -+		strbuf_release(&reason);
    -+	}
    ++	if (should_prune_worktree(wt->id, &reason, &path, expire))
    ++		wt->prune_reason = strbuf_detach(&reason, NULL);
    ++	wt->prune_reason_valid = 1;
     +
    ++	strbuf_release(&reason);
    ++	free(path);
     +	return wt->prune_reason;
     +}
     +
    @@ worktree.h: struct worktree {
      	struct object_id head_oid;
      	int is_detached;
      	int is_bare;
    + 	int is_current;
    + 	int lock_reason_valid; /* private */
    ++	int prune_reason_valid; /* private */
    + };
    + 
    + /*
     @@ worktree.h: int is_main_worktree(const struct worktree *wt);
       */
      const char *worktree_lock_reason(struct worktree *wt);
      
     +/*
    -+ * Return the reason string if the given worktree should be pruned
    -+ * or NULL otherwise.
    ++ * Return the reason string if the given worktree should be pruned, otherwise
    ++ * NULL if it should not be pruned. `expire` defines a grace period to prune
    ++ * the worktree when its path does not exist.
     + */
     +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
     +
      /*
       * Return true if worktree entry should be pruned, along with the reason for
    -  * pruning. Otherwise, return false and the worktree's path, or NULL if it
    +  * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
3:  fa2297714b < -:  ---------- worktree: teach worktree_lock_reason() to gently handle main worktree
4:  bcc2e5d745 < -:  ---------- worktree: teach `list` prunable annotation and verbose
5:  588051ed5f < -:  ---------- worktree: `list` escape lock reason in --porcelain
6:  cbff8c993a < -:  ---------- worktree: add tests for `list` verbose and annotations
7:  9c5893d2b9 < -:  ---------- worktree: document `list` verbose and prunable annotations
-:  ---------- > 3:  5b8963292f worktree: teach worktree_lock_reason() to gently handle main worktree
-:  ---------- > 4:  6bdd46b9bc worktree: teach `list --porcelain` to annotate locked worktree
-:  ---------- > 5:  bbf6c53ecc worktree: teach `list` to annotate prunable worktree
-:  ---------- > 6:  acc0bf22cd worktree: teach `list` verbose mode
-- 
2.30.0.356.gd4bb5ad4ba


^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH v2 1/6] worktree: libify should_prune_worktree()
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
@ 2021-01-17 23:42   ` Rafael Silva
  2021-01-17 23:42   ` [PATCH v2 2/6] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
                     ` (6 subsequent siblings)
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-17 23:42 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

As part of teaching "git worktree list" to annotate worktree that is a
candidate for pruning, let's move should_prune_worktree() from
builtin/worktree.c to worktree.c in order to make part of the worktree
public API.

should_prune_worktree() knows how to select the given worktree for
pruning based on an expiration date, however the expiration value is
stored in a static file-scope variable and it is not local to the
function. In order to move the function, teach should_prune_worktree()
to take the expiration date as an argument and document the new
parameter that is not immediately obvious.

Also, change the function comment to clearly state that the worktree's
path is returned in `wtpath` argument.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 builtin/worktree.c | 75 +---------------------------------------------
 worktree.c         | 68 +++++++++++++++++++++++++++++++++++++++++
 worktree.h         | 14 +++++++++
 3 files changed, 83 insertions(+), 74 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index 71287b2da6..dd886d5029 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -67,79 +67,6 @@ static void delete_worktrees_dir_if_empty(void)
 	rmdir(git_path("worktrees")); /* ignore failed removal */
 }
 
-/*
- * Return true if worktree entry should be pruned, along with the reason for
- * pruning. Otherwise, return false and the worktree's path, or NULL if it
- * cannot be determined. Caller is responsible for freeing returned path.
- */
-static int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath)
-{
-	struct stat st;
-	char *path;
-	int fd;
-	size_t len;
-	ssize_t read_result;
-
-	*wtpath = NULL;
-	if (!is_directory(git_path("worktrees/%s", id))) {
-		strbuf_addstr(reason, _("not a valid directory"));
-		return 1;
-	}
-	if (file_exists(git_path("worktrees/%s/locked", id)))
-		return 0;
-	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
-		strbuf_addstr(reason, _("gitdir file does not exist"));
-		return 1;
-	}
-	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
-	if (fd < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		return 1;
-	}
-	len = xsize_t(st.st_size);
-	path = xmallocz(len);
-
-	read_result = read_in_full(fd, path, len);
-	if (read_result < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		close(fd);
-		free(path);
-		return 1;
-	}
-	close(fd);
-
-	if (read_result != len) {
-		strbuf_addf(reason,
-			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
-			    (uintmax_t)len, (uintmax_t)read_result);
-		free(path);
-		return 1;
-	}
-	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
-		len--;
-	if (!len) {
-		strbuf_addstr(reason, _("invalid gitdir file"));
-		free(path);
-		return 1;
-	}
-	path[len] = '\0';
-	if (!file_exists(path)) {
-		if (stat(git_path("worktrees/%s/index", id), &st) ||
-		    st.st_mtime <= expire) {
-			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
-			free(path);
-			return 1;
-		} else {
-			*wtpath = path;
-			return 0;
-		}
-	}
-	*wtpath = path;
-	return 0;
-}
-
 static void prune_worktree(const char *id, const char *reason)
 {
 	if (show_only || verbose)
@@ -195,7 +122,7 @@ static void prune_worktrees(void)
 		if (is_dot_or_dotdot(d->d_name))
 			continue;
 		strbuf_reset(&reason);
-		if (should_prune_worktree(d->d_name, &reason, &path))
+		if (should_prune_worktree(d->d_name, &reason, &path, expire))
 			prune_worktree(d->d_name, reason.buf);
 		else if (path)
 			string_list_append(&kept, path)->util = xstrdup(d->d_name);
diff --git a/worktree.c b/worktree.c
index 821b233479..8ae019af79 100644
--- a/worktree.c
+++ b/worktree.c
@@ -741,3 +741,71 @@ void repair_worktree_at_path(const char *path,
 	strbuf_release(&realdotgit);
 	strbuf_release(&dotgit);
 }
+
+int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
+{
+	struct stat st;
+	char *path;
+	int fd;
+	size_t len;
+	ssize_t read_result;
+
+	*wtpath = NULL;
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addstr(reason, _("not a valid directory"));
+		return 1;
+	}
+	if (file_exists(git_path("worktrees/%s/locked", id)))
+		return 0;
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addstr(reason, _("gitdir file does not exist"));
+		return 1;
+	}
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		return 1;
+	}
+	len = xsize_t(st.st_size);
+	path = xmallocz(len);
+
+	read_result = read_in_full(fd, path, len);
+	if (read_result < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		close(fd);
+		free(path);
+		return 1;
+	}
+	close(fd);
+
+	if (read_result != len) {
+		strbuf_addf(reason,
+			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
+			    (uintmax_t)len, (uintmax_t)read_result);
+		free(path);
+		return 1;
+	}
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addstr(reason, _("invalid gitdir file"));
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		if (stat(git_path("worktrees/%s/index", id), &st) ||
+		    st.st_mtime <= expire) {
+			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
+			free(path);
+			return 1;
+		} else {
+			*wtpath = path;
+			return 0;
+		}
+	}
+	*wtpath = path;
+	return 0;
+}
diff --git a/worktree.h b/worktree.h
index f38e6fd5a2..818e1491c7 100644
--- a/worktree.h
+++ b/worktree.h
@@ -73,6 +73,20 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return true if worktree entry should be pruned, along with the reason for
+ * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
+ * NULL if it cannot be determined. Caller is responsible for freeing
+ * returned path.
+ *
+ * `expire` defines a grace period to prune the worktree when its path
+ * does not exist.
+ */
+int should_prune_worktree(const char *id,
+			  struct strbuf *reason,
+			  char **wtpath,
+			  timestamp_t expire);
+
 #define WT_VALIDATE_WORKTREE_MISSING_OK (1 << 0)
 
 /*
-- 
2.30.0.372.gbc7e965391


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v2 2/6] worktree: teach worktree to lazy-load "prunable" reason
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
  2021-01-17 23:42   ` [PATCH v2 1/6] worktree: libify should_prune_worktree() Rafael Silva
@ 2021-01-17 23:42   ` Rafael Silva
  2021-01-18  2:57     ` Eric Sunshine
  2021-01-17 23:42   ` [PATCH v2 3/6] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
                     ` (5 subsequent siblings)
  7 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-17 23:42 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

Add worktree_prune_reason() to allow a caller to discover whether a
worktree is prunable and the reason that it is, much like
worktree_lock_reason() indicates whether a worktree is locked and the
reason for the lock. As with worktree_lock_reason(), retrieve the
prunable reason lazily and cache it in the `worktree` structure.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 worktree.c | 20 ++++++++++++++++++++
 worktree.h |  9 +++++++++
 2 files changed, 29 insertions(+)

diff --git a/worktree.c b/worktree.c
index 8ae019af79..474ed46562 100644
--- a/worktree.c
+++ b/worktree.c
@@ -15,6 +15,7 @@ void free_worktrees(struct worktree **worktrees)
 		free(worktrees[i]->id);
 		free(worktrees[i]->head_ref);
 		free(worktrees[i]->lock_reason);
+		free(worktrees[i]->prune_reason);
 		free(worktrees[i]);
 	}
 	free (worktrees);
@@ -245,6 +246,25 @@ const char *worktree_lock_reason(struct worktree *wt)
 	return wt->lock_reason;
 }
 
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
+{
+	struct strbuf reason = STRBUF_INIT;
+	char *path;
+
+	if (is_main_worktree(wt))
+		return NULL;
+	if (wt->prune_reason_valid)
+		return wt->prune_reason;
+
+	if (should_prune_worktree(wt->id, &reason, &path, expire))
+		wt->prune_reason = strbuf_detach(&reason, NULL);
+	wt->prune_reason_valid = 1;
+
+	strbuf_release(&reason);
+	free(path);
+	return wt->prune_reason;
+}
+
 /* convenient wrapper to deal with NULL strbuf */
 static void strbuf_addf_gently(struct strbuf *buf, const char *fmt, ...)
 {
diff --git a/worktree.h b/worktree.h
index 818e1491c7..8b7c408132 100644
--- a/worktree.h
+++ b/worktree.h
@@ -11,11 +11,13 @@ struct worktree {
 	char *id;
 	char *head_ref;		/* NULL if HEAD is broken or detached */
 	char *lock_reason;	/* private - use worktree_lock_reason */
+	char *prune_reason;     /* private - use worktree_prune_reason */
 	struct object_id head_oid;
 	int is_detached;
 	int is_bare;
 	int is_current;
 	int lock_reason_valid; /* private */
+	int prune_reason_valid; /* private */
 };
 
 /*
@@ -73,6 +75,13 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return the reason string if the given worktree should be pruned, otherwise
+ * NULL if it should not be pruned. `expire` defines a grace period to prune
+ * the worktree when its path does not exist.
+ */
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
+
 /*
  * Return true if worktree entry should be pruned, along with the reason for
  * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
-- 
2.30.0.372.gbc7e965391


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v2 3/6] worktree: teach worktree_lock_reason() to gently handle main worktree
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
  2021-01-17 23:42   ` [PATCH v2 1/6] worktree: libify should_prune_worktree() Rafael Silva
  2021-01-17 23:42   ` [PATCH v2 2/6] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
@ 2021-01-17 23:42   ` Rafael Silva
  2021-01-17 23:42   ` [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
                     ` (4 subsequent siblings)
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-17 23:42 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

worktree_lock_reason() aborts with an assertion failure when called on
the main worktree since locking the main worktree is nonsensical. Not
only is this behaviour undocumented, thus callers might not even be aware
that the call could potentially crash the program, but it also forces
clients to be extra careful:

    if (!is_main_worktree(wt) && worktree_locked_reason(...))
        ...

Since we know that locking makes no sense in the context of the main
worktree, we can simpliy return false for the main worktree, thus making
client code less complex by eliminating the need for the callers to have
inside knowledge about the implementation:

    if (worktree_lock_reason(...))
        ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 worktree.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/worktree.c b/worktree.c
index 474ed46562..39495b261b 100644
--- a/worktree.c
+++ b/worktree.c
@@ -225,7 +225,8 @@ int is_main_worktree(const struct worktree *wt)
 
 const char *worktree_lock_reason(struct worktree *wt)
 {
-	assert(!is_main_worktree(wt));
+	if (is_main_worktree(wt))
+		return NULL;
 
 	if (!wt->lock_reason_valid) {
 		struct strbuf path = STRBUF_INIT;
-- 
2.30.0.372.gbc7e965391


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
                     ` (2 preceding siblings ...)
  2021-01-17 23:42   ` [PATCH v2 3/6] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
@ 2021-01-17 23:42   ` Rafael Silva
  2021-01-18  3:55     ` Eric Sunshine
  2021-01-17 23:42   ` [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree Rafael Silva
                     ` (3 subsequent siblings)
  7 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-17 23:42 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

Commit c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) taught "git worktree list" to annotate locked worktrees by
appending "locked" text to its output, however, this is not listed in
the --porcelain format.

Teach "list --porcelain" to do the same and add a "locked" attribute
followed by its reason, thus making both default and porcelain format
consistent. If the locked reason is not available then only "locked"
is shown.

The output of the "git worktree list --porcelain" becomes like so:

    $ git worktree list --porcelain
    ...
    worktree /path/to/locked
    HEAD 123abcdea123abcd123acbd123acbda123abcd12
    detached
    locked

    worktree /path/to/locked-with-reason
    HEAD abc123abc123abc123abc123abc123abc123abc1
    detached
    locked reason why it is locked
    ...

The locked reason is quoted to prevent newline characters (i.e: LF or
CRLF) breaking the --porcelain format as each attribute is shown per
line. For example:

   $ git worktree list --porcelain
   ...
   locked worktree's path mounted in\nremovable device
   ...

Furthermore, let's update the documentation to state that some
attributes in the porcelain format might be listed alone or together
with its value depending whether the value is available or not. Thus
documenting the case of the new "locked" attribute.

Additionally, c57b3367be (worktree: teach `list` to annotate locked
worktree, 2020-10-11) introduced a new test to ensure locked worktrees
are listed with "locked" annotation. However, the test does not clean up
after itself as "git worktree prune" is not going to remove the locked
worktree in the first place. This not only leaves the test in an unclean
state it also potentially breaks following tests that relies on the
"git worktree list" output. Let's fix that by unlocking the worktree
before the "prune" command.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 16 ++++++++++++++--
 builtin/worktree.c             | 13 +++++++++++++
 t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
 3 files changed, 59 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 02a706c4c0..d352a002f2 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -377,8 +377,10 @@ Porcelain Format
 The porcelain format has a line per attribute.  Attributes are listed with a
 label and value separated by a single space.  Boolean attributes (like `bare`
 and `detached`) are listed as a label only, and are present only
-if the value is true.  The first attribute of a working tree is always
-`worktree`, an empty line indicates the end of the record.  For example:
+if the value is true.  Some attributes (like `locked`) can be listed as a label
+only or with a value depending whether a reason is available.  The first
+attribute of a working tree is always `worktree`, an empty line indicates the
+end of the record.  For example:
 
 ------------
 $ git worktree list --porcelain
@@ -393,6 +395,16 @@ worktree /path/to/other-linked-worktree
 HEAD 1234abc1234abc1234abc1234abc1234abc1234a
 detached
 
+worktree /path/to/linked-worktree-locked
+HEAD 5678abc5678abc5678abc5678abc5678abc5678c
+branch refs/heads/locked
+locked
+
+worktree /path/to/linked-worktree-locked-with-reason
+HEAD 3456def3456def3456def3456def3456def3456b
+branch refs/heads/locked-with-reason
+locked reason why is locked
+
 ------------
 
 EXAMPLES
diff --git a/builtin/worktree.c b/builtin/worktree.c
index dd886d5029..37ae277352 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -12,6 +12,7 @@
 #include "submodule.h"
 #include "utf8.h"
 #include "worktree.h"
+#include "quote.h"
 
 static const char * const worktree_usage[] = {
 	N_("git worktree add [<options>] <path> [<commit-ish>]"),
@@ -569,6 +570,8 @@ static int add(int ac, const char **av, const char *prefix)
 
 static void show_worktree_porcelain(struct worktree *wt)
 {
+	const char *reason;
+
 	printf("worktree %s\n", wt->path);
 	if (wt->is_bare)
 		printf("bare\n");
@@ -579,6 +582,16 @@ static void show_worktree_porcelain(struct worktree *wt)
 		else if (wt->head_ref)
 			printf("branch %s\n", wt->head_ref);
 	}
+
+	reason = worktree_lock_reason(wt);
+	if (reason && *reason) {
+		struct strbuf sb = STRBUF_INIT;
+		quote_c_style(reason, &sb, NULL, CQUOTE_NODQ);
+		printf("locked %s\n", sb.buf);
+		strbuf_release(&sb);
+	} else if (reason)
+		printf("locked\n");
+
 	printf("\n");
 }
 
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 795ddca2e4..c522190feb 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -66,11 +66,43 @@ test_expect_success '"list" all worktrees with locked annotation' '
 	git worktree add --detach locked master &&
 	git worktree add --detach unlocked master &&
 	git worktree lock locked &&
+	test_when_finished "git worktree unlock locked" &&
 	git worktree list >out &&
 	grep "/locked  *[0-9a-f].* locked$" out &&
 	! grep "/unlocked  *[0-9a-f].* locked$" out
 '
 
+test_expect_success '"list" all worktrees --porcelain with locked' '
+	test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
+	echo "locked" >expect &&
+	echo "locked with reason" >>expect &&
+	git worktree add --detach locked1 &&
+	git worktree add --detach locked2 &&
+	git worktree add --detach unlocked &&
+	git worktree lock locked1 &&
+	git worktree lock locked2 --reason "with reason" &&
+	test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
+	test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
+	printf "locked locked\\\\r\\\\nreason\n" >expect &&
+	printf "locked locked\\\\nreason\n" >>expect &&
+	git worktree add --detach locked_lf &&
+	git worktree add --detach locked_crlf &&
+	printf "locked\nreason\n\n" >reason_lf &&
+	printf "locked\r\nreason\n\n" >reason_crlf &&
+	git worktree lock locked_lf --reason "$(cat reason_lf)" &&
+	git worktree lock locked_crlf --reason "$(cat reason_crlf)" &&
+	test_when_finished "git worktree unlock locked_lf && git worktree unlock locked_crlf" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.372.gbc7e965391


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
                     ` (3 preceding siblings ...)
  2021-01-17 23:42   ` [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
@ 2021-01-17 23:42   ` Rafael Silva
  2021-01-18  4:45     ` Eric Sunshine
  2021-01-17 23:42   ` [PATCH v2 6/6] worktree: teach `list` verbose mode Rafael Silva
                     ` (2 subsequent siblings)
  7 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-17 23:42 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

The "git worktree list" command shows the absolute path to the worktree,
the commit that is checked out, the name of the branch, and a "locked"
annotation if the worktree is locked, however, it does not indicate
whether the worktree is prunable.

The "prune" command will remove a worktree if it is prunable unless
`--dry-run` option is specified. This could lead to a worktree being
removed without the user realizing before it is too late, in case the
user forgets to pass --dry-run for instance. If the "list" command shows
which worktree is prunable, the user could verify before running
"git worktree prune" and hopefully prevents the working tree to be
removed "accidentally" on the worse case scenario.

Let's teach "git worktree list" to show when a worktree is a prunable
candidate for both default and porcelain format.

In the default format a "prunable" text is appended:

    $ git worktree list
    /path/to/main      aba123 [main]
    /path/to/linked    123abc [branch-a]
    /path/to/prunable  ace127 (detached HEAD) prunable

In the --porcelain format a prunable label is added followed by
its reason:

    $ git worktree list --porcelain
    ...
    worktree /path/to/prunable
    HEAD abc1234abc1234abc1234abc1234abc1234abc12
    detached
    prunable gitdir file points to non-existent location
    ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 26 ++++++++++++++++++++++++--
 builtin/worktree.c             | 15 ++++++++++++++-
 t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
 3 files changed, 70 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index d352a002f2..3d8c14dbdf 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -97,8 +97,9 @@ list::
 List details of each working tree.  The main working tree is listed first,
 followed by each of the linked working trees.  The output details include
 whether the working tree is bare, the revision currently checked out, the
-branch currently checked out (or "detached HEAD" if none), and "locked" if
-the worktree is locked.
+branch currently checked out (or "detached HEAD" if none), "locked" if
+the worktree is locked, "prunable" if the worktree can be pruned by `prune`
+command.
 
 lock::
 
@@ -234,6 +235,9 @@ This can also be set up as the default behaviour by using the
 
 --expire <time>::
 	With `prune`, only expire unused working trees older than `<time>`.
++
+With `list`, annotate missing working trees as prunable if they are
+older than `<mtime>`.
 
 --reason <string>::
 	With `lock`, an explanation why the working tree is locked.
@@ -372,6 +376,19 @@ $ git worktree list
 /path/to/other-linked-worktree  1234abc  (detached HEAD)
 ------------
 
+The command also shows annotations for each working tree, according to its state.
+These annotations are:
+
+ * `locked`, if the working tree is locked.
+ * `prunable`, if the working tree can be pruned via `git worktree prune`.
+
+------------
+$ git worktree list
+/path/to/linked-worktree        abcd1234 [master]
+/path/to/locked-worktreee       acbd5678 (brancha) locked
+/path/to/prunable-worktree      5678abc  (detached HEAD) prunable
+------------
+
 Porcelain Format
 ~~~~~~~~~~~~~~~~
 The porcelain format has a line per attribute.  Attributes are listed with a
@@ -405,6 +422,11 @@ HEAD 3456def3456def3456def3456def3456def3456b
 branch refs/heads/locked-with-reason
 locked reason why is locked
 
+worktree /path/to/linked-worktree-prunable
+HEAD 1233def1234def1234def1234def1234def1234b
+detached
+prunable gitdir file points to non-existent location
+
 ------------
 
 EXAMPLES
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 37ae277352..fb82d7bb64 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -592,6 +592,10 @@ static void show_worktree_porcelain(struct worktree *wt)
 	} else if (reason)
 		printf("locked\n");
 
+	reason = worktree_prune_reason(wt, expire);
+	if (reason && *reason)
+		printf("prunable %s\n", reason);
+
 	printf("\n");
 }
 
@@ -600,6 +604,7 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 	struct strbuf sb = STRBUF_INIT;
 	int cur_path_len = strlen(wt->path);
 	int path_adj = cur_path_len - utf8_strwidth(wt->path);
+	const char *reason;
 
 	strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
 	if (wt->is_bare)
@@ -617,9 +622,14 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 			strbuf_addstr(&sb, "(error)");
 	}
 
-	if (!is_main_worktree(wt) && worktree_lock_reason(wt))
+	reason = worktree_lock_reason(wt);
+	if (reason)
 		strbuf_addstr(&sb, " locked");
 
+	reason = worktree_prune_reason(wt, expire);
+	if (reason)
+		strbuf_addstr(&sb, " prunable");
+
 	printf("%s\n", sb.buf);
 	strbuf_release(&sb);
 }
@@ -663,9 +673,12 @@ static int list(int ac, const char **av, const char *prefix)
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT_EXPIRY_DATE(0, "expire", &expire,
+				N_("add 'prunable' annotation to worktrees older than <time>")),
 		OPT_END()
 	};
 
+	expire = TIME_MAX;
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index c522190feb..e9b410b69e 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -103,6 +103,38 @@ test_expect_success '"list" all worktrees --porcelain with locked reason newline
 	test_cmp expect actual
 '
 
+test_expect_success '"list" all worktrees with prunable annotation' '
+	test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	git worktree add --detach unprunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	! grep "/unprunable  *[0-9a-f].* prunable$"
+'
+
+test_expect_success '"list" all worktrees --porcelain with prunable' '
+	test_when_finished "rm -rf prunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	rm -rf prunable &&
+	git worktree list --porcelain >out &&
+	sed -n "/^worktree .*\/prunable$/,/^$/p" <out >only_prunable &&
+	test_i18ngrep "^prunable gitdir file points to non-existent location$" only_prunable
+'
+
+test_expect_success '"list" all worktrees with prunable consistent with "prune"' '
+	test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	git worktree add --detach unprunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	! grep "/unprunable  *[0-9a-f].* unprunable$" out &&
+	git worktree prune --verbose >out &&
+	test_i18ngrep "^Removing worktrees/prunable" out &&
+	test_i18ngrep ! "^Removing worktrees/unprunable" out
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.372.gbc7e965391


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v2 6/6] worktree: teach `list` verbose mode
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
                     ` (4 preceding siblings ...)
  2021-01-17 23:42   ` [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree Rafael Silva
@ 2021-01-17 23:42   ` Rafael Silva
  2021-01-18  5:15     ` Eric Sunshine
  2021-01-18  5:33   ` [PATCH v2 0/6] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
  7 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-17 23:42 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

"git worktree list" annotates each worktree according to its state such
as "prunable" or "locked", however it is not immediately obvious why
these worktrees are being annotated. For prunable worktrees a reason
is available that is returned by should_prune_worktree() and for locked
worktrees a reason might be available provided by the user via `lock`
command.

Let's teach "git worktree list" to output the reason why the worktrees
are being annotated. The reason is a text that can take virtually any
size and appending the text on the default columned format will make it
difficult to extend the command with other annotations and not fit nicely
on the screen. In order to address this shortcoming the annotation is
then moved to the next line indented followed by the reason, if the
reason is not available the annotation stays on the same line as the
worktree itself.

The output of "git worktree list" with verbose becomes like so:

    $ git worktree list --verbose
    ...
    /path/to/locked                acb124 [branch-a] locked
    /path/to/locked-with-reason    acc125 [branch-b]
        locked: worktree with a locked reason
    /path/to/prunable-reason       ace127 [branch-d]
        prunable: gitdir file points to non-existent location
    ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 20 ++++++++++++++++++++
 builtin/worktree.c             | 11 +++++++++--
 t/t2402-worktree-list.sh       | 27 +++++++++++++++++++++++++++
 3 files changed, 56 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 3d8c14dbdf..d34bf121f3 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -232,6 +232,8 @@ This can also be set up as the default behaviour by using the
 -v::
 --verbose::
 	With `prune`, report all removals.
++
+With `list`, output additional information about worktrees (see below).
 
 --expire <time>::
 	With `prune`, only expire unused working trees older than `<time>`.
@@ -389,6 +391,24 @@ $ git worktree list
 /path/to/prunable-worktree      5678abc  (detached HEAD) prunable
 ------------
 
+For these annotations, a reason might also be available and this can be
+seen using the verbose mode. The annotation is then moved to the next line
+indented followed by the additional information.
+
+------------
+$ git worktree list --verbose
+/path/to/linked-worktree        abcd1234 [master]
+/path/to/locked-worktreee       acbd5678 (brancha)
+	locked: working tree path is mounted on a removable device
+/path/to/locked-no-reason       abcd578  (detached HEAD) locked
+/path/to/prunable-worktree      5678abc  (detached HEAD)
+	prunable: gitdir file points to non-existent location
+------------
+
+Note that the annotation is moved to the next line if the additional
+information is available, otherwise it stays on the same line as the
+working tree itself.
+
 Porcelain Format
 ~~~~~~~~~~~~~~~~
 The porcelain format has a line per attribute.  Attributes are listed with a
diff --git a/builtin/worktree.c b/builtin/worktree.c
index fb82d7bb64..a59cbfa0d2 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -623,11 +623,15 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 	}
 
 	reason = worktree_lock_reason(wt);
-	if (reason)
+	if (verbose && reason && *reason)
+		strbuf_addf(&sb, "\n\tlocked: %s", reason);
+	else if (reason)
 		strbuf_addstr(&sb, " locked");
 
 	reason = worktree_prune_reason(wt, expire);
-	if (reason)
+	if (verbose && reason && *reason)
+		strbuf_addf(&sb, "\n\tprunable: %s", reason);
+	else if (reason)
 		strbuf_addstr(&sb, " prunable");
 
 	printf("%s\n", sb.buf);
@@ -673,6 +677,7 @@ static int list(int ac, const char **av, const char *prefix)
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("add 'prunable' annotation to worktrees older than <time>")),
 		OPT_END()
@@ -682,6 +687,8 @@ static int list(int ac, const char **av, const char *prefix)
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
+	else if (verbose && porcelain)
+		die(_("--verbose and --porcelain are mutually exclusive"));
 	else {
 		struct worktree **worktrees = get_worktrees();
 		int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index e9b410b69e..4de37f896c 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -135,6 +135,33 @@ test_expect_success '"list" all worktrees with prunable consistent with "prune"'
 	test_i18ngrep ! "^Removing worktrees/unprunable" out
 '
 
+test_expect_success '"list" --verbose and --porcelain mutually exclusive' '
+	test_must_fail git worktree list --verbose --porcelain
+'
+
+test_expect_success '"list" all worktrees --verbose with locked' '
+	test_when_finished "rm -rf locked out actual expect && git worktree prune" &&
+	git worktree add locked --detach &&
+	git worktree lock locked --reason "with reason" &&
+	test_when_finished "git worktree unlock locked" &&
+	echo "$(git -C locked rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tlocked: with reason\n" >>expect &&
+	git worktree list --verbose >out &&
+	sed -n "s/  */ /g;/\/locked  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
+	test_cmp actual expect
+'
+
+test_expect_success '"list" all worktrees --verbose with prunable' '
+	test_when_finished "rm -rf prunable out actual expect && git worktree prune" &&
+	git worktree add prunable --detach &&
+	echo "$(git -C prunable rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tprunable: gitdir file points to non-existent location\n" >>expect &&
+	rm -rf prunable &&
+	git worktree list --verbose >out &&
+	sed -n "s/  */ /g;/\/prunable  *[0-9a-f].*$/,/prunable: .*$/p" <out >actual &&
+	test_i18ncmp actual expect
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.372.gbc7e965391


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 2/6] worktree: teach worktree to lazy-load "prunable" reason
  2021-01-17 23:42   ` [PATCH v2 2/6] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
@ 2021-01-18  2:57     ` Eric Sunshine
  2021-01-19  7:57       ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-18  2:57 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Add worktree_prune_reason() to allow a caller to discover whether a
> worktree is prunable and the reason that it is, much like
> worktree_lock_reason() indicates whether a worktree is locked and the
> reason for the lock. As with worktree_lock_reason(), retrieve the
> prunable reason lazily and cache it in the `worktree` structure.
>
> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
> diff --git a/worktree.c b/worktree.c
> @@ -245,6 +246,25 @@ const char *worktree_lock_reason(struct worktree *wt)
> +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
> +{
> +       struct strbuf reason = STRBUF_INIT;
> +       char *path;

The `path` variable is uninitialized...

> +       if (should_prune_worktree(wt->id, &reason, &path, expire))
> +               wt->prune_reason = strbuf_detach(&reason, NULL);

...but the very first thing should_prune_worktree() does is
unconditionally set it to NULL, so it's either NULL or an allocated
string regardless of the value returned by should_prune_worktree()...

> +       strbuf_release(&reason);
> +       free(path);

...which makes the behavior of `free(path)` deterministic. Good.

I'm on the fence about whether or not we should initialize `path` to NULL:

    char *path = NULL;

just to make it easier for the next person to reason about it without
having to dig into the implementation of should_prune_worktree(). This
is a really minor point, though, not worth a re-roll.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-17 23:42   ` [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
@ 2021-01-18  3:55     ` Eric Sunshine
  2021-01-19  8:20       ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-18  3:55 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Commit c57b3367be (worktree: teach `list` to annotate locked worktree,
> 2020-10-11) taught "git worktree list" to annotate locked worktrees by
> appending "locked" text to its output, however, this is not listed in
> the --porcelain format.
>
> Teach "list --porcelain" to do the same and add a "locked" attribute
> followed by its reason, thus making both default and porcelain format
> consistent. If the locked reason is not available then only "locked"
> is shown.
>
> The output of the "git worktree list --porcelain" becomes like so:
>
>     $ git worktree list --porcelain
>     ...
>     worktree /path/to/locked
>     HEAD 123abcdea123abcd123acbd123acbda123abcd12
>     detached
>     locked
>
>     worktree /path/to/locked-with-reason
>     HEAD abc123abc123abc123abc123abc123abc123abc1
>     detached
>     locked reason why it is locked
>     ...

All good.

> The locked reason is quoted to prevent newline characters (i.e: LF or
> CRLF) breaking the --porcelain format as each attribute is shown per
> line. For example:
>
>    $ git worktree list --porcelain
>    ...
>    locked worktree's path mounted in\nremovable device
>    ...

I have a bit to say about this below.

> Additionally, c57b3367be (worktree: teach `list` to annotate locked
> worktree, 2020-10-11) introduced a new test to ensure locked worktrees
> are listed with "locked" annotation. However, the test does not clean up
> after itself as "git worktree prune" is not going to remove the locked
> worktree in the first place. This not only leaves the test in an unclean
> state it also potentially breaks following tests that relies on the
> "git worktree list" output. Let's fix that by unlocking the worktree
> before the "prune" command.

The actual code change to fix this bug is about as minimal as it gets,
but the explanation you've written here is lengthy enough and nicely
self-contained that it suggests splitting it off to its own patch. And
since you can re-use this paragraph almost verbatim as the commit
message, it shouldn't require much work to do so. On the other hand,
it is itself not necessarily worth a re-roll, but if you do re-roll,
perhaps it's worth considering.

> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
> @@ -377,8 +377,10 @@ Porcelain Format
>  The porcelain format has a line per attribute.  Attributes are listed with a
>  label and value separated by a single space.  Boolean attributes (like `bare`
>  and `detached`) are listed as a label only, and are present only
> -if the value is true.  The first attribute of a working tree is always
> -`worktree`, an empty line indicates the end of the record.  For example:
> +if the value is true.  Some attributes (like `locked`) can be listed as a label
> +only or with a value depending whether a reason is available.  The first

Perhaps:
s/depending whether/depending upon whether/

> +attribute of a working tree is always `worktree`, an empty line indicates the
> +end of the record.  For example:
> @@ -393,6 +395,16 @@ worktree /path/to/other-linked-worktree
> +worktree /path/to/linked-worktree-locked
> +HEAD 5678abc5678abc5678abc5678abc5678abc5678c
> +branch refs/heads/locked
> +locked
> +
> +worktree /path/to/linked-worktree-locked-with-reason
> +HEAD 3456def3456def3456def3456def3456def3456b
> +branch refs/heads/locked-with-reason
> +locked reason why is locked

I was momentarily confused by the branch named `locked` with the
`locked` attribute in the first new stanza. Perhaps take a hint from
the second new stanza and call the first one `locked-no-reason`:

    worktree /path/to/linked-worktree-locked-no-reason
    HEAD 5678abc5678abc5678abc5678abc5678abc5678c
    branch refs/heads/locked-no-reason
    locked

Again, though, not worth a re-roll.

> diff --git a/builtin/worktree.c b/builtin/worktree.c
> @@ -579,6 +582,16 @@ static void show_worktree_porcelain(struct worktree *wt)
> +       reason = worktree_lock_reason(wt);
> +       if (reason && *reason) {
> +               struct strbuf sb = STRBUF_INIT;
> +               quote_c_style(reason, &sb, NULL, CQUOTE_NODQ);
> +               printf("locked %s\n", sb.buf);
> +               strbuf_release(&sb);
> +       } else if (reason)
> +               printf("locked\n");

This needs a change, and it's totally my fault that it does. In my
previous review, I mentioned that if the lock reason contains special
characters, we want those special characters escaped and the reason
quoted, but _only_ if it contains special characters. However, I then
incorrectly said to call quote_c_style() with CQUOTE_NODQ to achieve
that behavior. In fact, CQUOTE_NODQ gives us the wrong behavior since
it avoids quoting the string which, as Phillip pointed out, makes it
impossible to distinguish between a string which just happens to
contain the two-character sequence '\' and 'n', and an escaped newline
"\n". So, the above should really be:

    quote_c_style(reason, &sb, NULL, 0);

The example in the commit message should be adjusted to account for
this change, as well:

    In porcelain mode, if the lock reason contains special characters
    such as newlines, they are escaped with backslashes and the entire
    reason is enclosed in double quotes. For example:

    $ git worktree list --porcelain
    ...
    locked "worktree's path mounted in\nremovable device"
    ...

And, of course, the new test will need a slight adjustment.

> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> @@ -66,11 +66,43 @@ test_expect_success '"list" all worktrees with locked annotation' '
> +test_expect_success '"list" all worktrees --porcelain with locked' '
> +       test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
> +       echo "locked" >expect &&
> +       echo "locked with reason" >>expect &&
> +       git worktree add --detach locked1 &&
> +       git worktree add --detach locked2 &&
> +       git worktree add --detach unlocked &&
> +       git worktree lock locked1 &&
> +       git worktree lock locked2 --reason "with reason" &&
> +       test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
> +       git worktree list --porcelain >out &&
> +       grep "^locked" out >actual &&
> +       test_cmp expect actual
> +'

So, the purpose of the `unlocked` worktree in this test is to prove
that it didn't accidentally get annotated with `locked`? (Since, if it
did get annotated, then `actual` would contain too many lines and not
match `expect`.) Is that correct?

> +test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
> +       test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
> +       printf "locked locked\\\\r\\\\nreason\n" >expect &&
> +       printf "locked locked\\\\nreason\n" >>expect &&
> +       git worktree add --detach locked_lf &&
> +       git worktree add --detach locked_crlf &&
> +       printf "locked\nreason\n\n" >reason_lf &&
> +       printf "locked\r\nreason\n\n" >reason_crlf &&

The trailing "\n\n" is unneeded. Due to the way `$(...)` expansion
works (dropping trailing whitespace), you'll get the same successful
test result with:

    printf "locked\nreason\n" >reason_lf &&
    printf "locked\r\nreason\n" >reason_crlf &&

and even with:

    printf "locked\nreason" >reason_lf &&
    printf "locked\r\nreason" >reason_crlf &&

> +       git worktree lock locked_lf --reason "$(cat reason_lf)" &&
> +       git worktree lock locked_crlf --reason "$(cat reason_crlf)" &&

You could also just embed the `printf`'s here rather than using these
temporary files.

    git worktree lock --reason $(printf "...") <path> &&

Or, if we care only about testing LF, and not about CRLF, even this would work:

    git worktree lock --reason "reason with
    newline" <path> &&

but that gets a bit ugly.

Anyhow, all the line terminator commentary about this test is a matter
of personal taste, probably not worth a re-roll or even changing.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree
  2021-01-17 23:42   ` [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree Rafael Silva
@ 2021-01-18  4:45     ` Eric Sunshine
  2021-01-19 10:26       ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-18  4:45 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> The "git worktree list" command shows the absolute path to the worktree,
> the commit that is checked out, the name of the branch, and a "locked"
> annotation if the worktree is locked, however, it does not indicate
> whether the worktree is prunable.
> [...]
> Let's teach "git worktree list" to show when a worktree is a prunable
> candidate for both default and porcelain format.

Good.

> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
> @@ -234,6 +235,9 @@ This can also be set up as the default behaviour by using the
>  --expire <time>::
>         With `prune`, only expire unused working trees older than `<time>`.
> ++
> +With `list`, annotate missing working trees as prunable if they are
> +older than `<mtime>`.

s/mtime/time/

> diff --git a/builtin/worktree.c b/builtin/worktree.c
> @@ -592,6 +592,10 @@ static void show_worktree_porcelain(struct worktree *wt)
> +       reason = worktree_prune_reason(wt, expire);
> +       if (reason && *reason)
> +               printf("prunable %s\n", reason);

I lean toward not including `*reason` in the condition here. While one
may argue that `*reason` is future-proofing the condition, we know
today that worktree_prune_reason() will only ever return NULL or a
non-empty string. So, having `*reason` in the condition is misleading
and confuses readers into thinking that worktree_prune_reason() could
return an empty string or that perhaps it did in the past. And
studying the history of this file or even this commit won't help them
understand why the author included `*reason` in the condition.

> @@ -617,9 +622,14 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
> -       if (!is_main_worktree(wt) && worktree_lock_reason(wt))
> +       reason = worktree_lock_reason(wt);
> +       if (reason)
>                 strbuf_addstr(&sb, " locked");

Although I understand what happened here because I read the entire
series, for anyone reading this patch in the future or even someone
reading this patch in isolation, the disappearance of
is_main_worktree() from the condition is mysterious. They won't know
if its removal was an accident or intentional, or if the change
introduces a bug. Therefore, it would be better to drop
is_main_worktree() from this conditional in patch [3/6], which is the
patch which makes it safe to call worktree_lock_reason() even for the
main worktree. Thus, in [3/6], this would change from:

    if (!is_main_worktree(wt) && worktree_lock_reason(wt))

to:

    if (worktree_lock_reason(wt))

and then in this patch [5/6], it becomes:

    reason = worktree_lock_reason(wt);
    if (reason)

> +       reason = worktree_prune_reason(wt, expire);
> +       if (reason)
> +               strbuf_addstr(&sb, " prunable");

Looking at this also makes me wonder if patches [5/6] and [6/6] should
be swapped since it's not clear to the reader why you're adding the
`reason` variable in this patch when the same could be accomplished
more simply:

    if (worktree_lock_reason(wt))
        strbuf_addstr(&sb, " locked");
    if (worktree_prune_reason(wt, expire))
        strbuf_addstr(&sb, " prunable");

If you swap the patches and add --verbose mode first which requires
this new `reason` variable, then the mystery goes away, and the use of
`reason` is obvious when `prunable` annotation is added in the
subsequent patch.

Having said that, I'm not trying to make extra work for you by
expecting patch perfection. Sometimes it's fine to craft a patch in
such a way that it makes subsequent patches simpler, even if it looks
slightly odd in the current patch, and I haven't read [6/6] yet, so
whatever opinion I express here is based only upon what I've read up
to this point.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 6/6] worktree: teach `list` verbose mode
  2021-01-17 23:42   ` [PATCH v2 6/6] worktree: teach `list` verbose mode Rafael Silva
@ 2021-01-18  5:15     ` Eric Sunshine
  2021-01-18 19:40       ` Eric Sunshine
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-18  5:15 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> "git worktree list" annotates each worktree according to its state such
> as "prunable" or "locked", however it is not immediately obvious why
> these worktrees are being annotated. For prunable worktrees a reason
> is available that is returned by should_prune_worktree() and for locked
> worktrees a reason might be available provided by the user via `lock`
> command.
>
> Let's teach "git worktree list" to output the reason why the worktrees
> are being annotated. The reason is a text that can take virtually any
> size and appending the text on the default columned format will make it
> difficult to extend the command with other annotations and not fit nicely
> on the screen. In order to address this shortcoming the annotation is
> then moved to the next line indented followed by the reason, if the
> reason is not available the annotation stays on the same line as the
> worktree itself.

If you're re-rolling, let's mention the new `--verbose` option
somewhere in the commit message since that is the focus of this patch.
The second paragraph would be a good place:

    Let's teach "git worktree list" a --verbose mode which
    outputs the reason...

Also, the final sentence is a bit difficult to follow due to the comma
before "if the reason is not available". If you make the "if the
reason is not available" a separate sentence, it becomes simple to
understand.

> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> @@ -135,6 +135,33 @@ test_expect_success '"list" all worktrees with prunable consistent with "prune"'
> +test_expect_success '"list" all worktrees --verbose with locked' '
> +       test_when_finished "rm -rf locked out actual expect && git worktree prune" &&
> +       git worktree add locked --detach &&
> +       git worktree lock locked --reason "with reason" &&
> +       test_when_finished "git worktree unlock locked" &&
> +       echo "$(git -C locked rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
> +       printf "\tlocked: with reason\n" >>expect &&
> +       git worktree list --verbose >out &&
> +       sed -n "s/  */ /g;/\/locked  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
> +       test_cmp actual expect
> +'

At first, I wondered if we would also want this test to have a
locked-no-reason worktree to ensure that its `locked` annotation stays
on the same line as the worktree, but that's not needed because that
case is already covered by the existing test. Fine.

> +test_expect_success '"list" all worktrees --verbose with prunable' '
> +       test_when_finished "rm -rf prunable out actual expect && git worktree prune" &&
> +       git worktree add prunable --detach &&
> +       echo "$(git -C prunable rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
> +       printf "\tprunable: gitdir file points to non-existent location\n" >>expect &&
> +       rm -rf prunable &&
> +       git worktree list --verbose >out &&
> +       sed -n "s/  */ /g;/\/prunable  *[0-9a-f].*$/,/prunable: .*$/p" <out >actual &&
> +       test_i18ncmp actual expect
> +'

An alternative would be to have a single test of --verbose which
includes a locked-no-reason worktree, a locked-with-reason worktree,
and a prunable worktree. However, that's a very minor and subjective
point and certainly not worth a re-roll or changing unless you think
it's a nice simplification.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 0/6] teach `worktree list` verbose mode and prunable annotations
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
                     ` (5 preceding siblings ...)
  2021-01-17 23:42   ` [PATCH v2 6/6] worktree: teach `list` verbose mode Rafael Silva
@ 2021-01-18  5:33   ` Eric Sunshine
  2021-01-19 16:44     ` Rafael Silva
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
  7 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-18  5:33 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> This patch series addresses some of these changes by teaching
> `worktree list` to show "prunable" annotation, adding verbose mode and
> extending the --porcelain format with prunable and locked annotation as
> follow up from [1]. Additionally, it addresses one shortcoming for porcelain
> format to escape any newline characters (LF or CRLF) for the lock reason
> to prevent breaking the format that is mentioned in [4] and [1] during the
> review cycle.

Thanks for continuing to work on this. I'm pleased to see these
enhancements coming together so nicely.

> The patch series is organizes as:

The new organization is a nice improvement over v1.

As mentioned in my review of [5/6], there may be some value in
swapping [5/6] and [6/6] to make it easier for readers to digest the
changes, but it's not a hard requirement.

To avoid being mysterious, there's also a change in [5/6] which
probably belongs in [3/6], as explained in my review of [5/6].

My review of patch [4/6] suggests optionally splitting out a bug fix
into its own patch, but that's a very minor issue. Use your best
judgment whether or not to do so.

> 4. The fourth patch adds the "locked" attribute for the porcelain format
>    in order to make both default and --porcelain format consistent.

This patch does need a re-roll, and it's entirely my fault. In my
review of the previous round, I said that if the lock reason contained
special characters, we'd want to escape those characters and quote the
string, but then I gave you a code suggestion which did the opposite
of that. My [4/6] review goes into more detail.

Aside from the one important fix in [4/6], and the possible minor
re-organizations suggested above, all my other review comments were
very minor and probably subjective, thus nothing to sweat over.

Nicely done.

[1]: https://lore.kernel.org/git/CAPig+cSH6PKt8YvDJhMBvzvePYLqbf70uVV8TERoMj+kfxJRHQ@mail.gmail.com/

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 6/6] worktree: teach `list` verbose mode
  2021-01-18  5:15     ` Eric Sunshine
@ 2021-01-18 19:40       ` Eric Sunshine
  0 siblings, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-18 19:40 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Mon, Jan 18, 2021 at 12:15 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
> On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
> > +test_expect_success '"list" all worktrees --verbose with locked' '
> > +       test_when_finished "rm -rf locked out actual expect && git worktree prune" &&
> > +       git worktree add locked --detach &&
> > +       git worktree lock locked --reason "with reason" &&
> > +       test_when_finished "git worktree unlock locked" &&
> > +       echo "$(git -C locked rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
> > +       printf "\tlocked: with reason\n" >>expect &&
> > +       git worktree list --verbose >out &&
> > +       sed -n "s/  */ /g;/\/locked  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
> > +       test_cmp actual expect
> > +'
>
> At first, I wondered if we would also want this test to have a
> locked-no-reason worktree to ensure that its `locked` annotation stays
> on the same line as the worktree, but that's not needed because that
> case is already covered by the existing test. Fine.

Erm, I think what I said is wrong. There is no existing test to show
that `locked` without a reason stays on the same line as the worktree
in --verbose mode. So having a locked-no-reason worktree in this test
could be beneficial.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 2/6] worktree: teach worktree to lazy-load "prunable" reason
  2021-01-18  2:57     ` Eric Sunshine
@ 2021-01-19  7:57       ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-19  7:57 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Eric Sunshine writes:

> On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> Add worktree_prune_reason() to allow a caller to discover whether a
>> worktree is prunable and the reason that it is, much like
>> worktree_lock_reason() indicates whether a worktree is locked and the
>> reason for the lock. As with worktree_lock_reason(), retrieve the
>> prunable reason lazily and cache it in the `worktree` structure.
>>
>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>> diff --git a/worktree.c b/worktree.c
>> @@ -245,6 +246,25 @@ const char *worktree_lock_reason(struct worktree *wt)
>> +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
>> +{
>> +       struct strbuf reason = STRBUF_INIT;
>> +       char *path;
>
> The `path` variable is uninitialized...
>
>> +       if (should_prune_worktree(wt->id, &reason, &path, expire))
>> +               wt->prune_reason = strbuf_detach(&reason, NULL);
>
> ...but the very first thing should_prune_worktree() does is
> unconditionally set it to NULL, so it's either NULL or an allocated
> string regardless of the value returned by should_prune_worktree()...
>
>> +       strbuf_release(&reason);
>> +       free(path);
>
> ...which makes the behavior of `free(path)` deterministic. Good.
>
> I'm on the fence about whether or not we should initialize `path` to NULL:
>
>     char *path = NULL;
>
> just to make it easier for the next person to reason about it without
> having to dig into the implementation of should_prune_worktree(). This
> is a really minor point, though, not worth a re-roll.

This is a good point and totally agreed. I'm going to include this
change in the next revision.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-18  3:55     ` Eric Sunshine
@ 2021-01-19  8:20       ` Rafael Silva
  2021-01-19 17:16         ` Eric Sunshine
  0 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-19  8:20 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Thanks for the review.

Eric Sunshine writes:

> On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:

>> Additionally, c57b3367be (worktree: teach `list` to annotate locked
>> worktree, 2020-10-11) introduced a new test to ensure locked worktrees
>> are listed with "locked" annotation. However, the test does not clean up
>> after itself as "git worktree prune" is not going to remove the locked
>> worktree in the first place. This not only leaves the test in an unclean
>> state it also potentially breaks following tests that relies on the
>> "git worktree list" output. Let's fix that by unlocking the worktree
>> before the "prune" command.
>
> The actual code change to fix this bug is about as minimal as it gets,
> but the explanation you've written here is lengthy enough and nicely
> self-contained that it suggests splitting it off to its own patch. And
> since you can re-use this paragraph almost verbatim as the commit
> message, it shouldn't require much work to do so. On the other hand,
> it is itself not necessarily worth a re-roll, but if you do re-roll,
> perhaps it's worth considering.
>

make sense, I'll re-roll with this change on its own patch. I actually
thought about splitting it off during as well, but I wasn't sure whether
this was a good idea. Now that you mentioned here I guess it sounds a
like reasonable change for the next version.


>> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
>> @@ -377,8 +377,10 @@ Porcelain Format
>>  The porcelain format has a line per attribute.  Attributes are listed with a
>>  label and value separated by a single space.  Boolean attributes (like `bare`
>>  and `detached`) are listed as a label only, and are present only
>> -if the value is true.  The first attribute of a working tree is always
>> -`worktree`, an empty line indicates the end of the record.  For example:
>> +if the value is true.  Some attributes (like `locked`) can be listed as a label
>> +only or with a value depending whether a reason is available.  The first
>
> Perhaps:
> s/depending whether/depending upon whether/
>

Yeah, I think that's sounds bit better.

>> +attribute of a working tree is always `worktree`, an empty line indicates the
>> +end of the record.  For example:
>> @@ -393,6 +395,16 @@ worktree /path/to/other-linked-worktree
>> +worktree /path/to/linked-worktree-locked
>> +HEAD 5678abc5678abc5678abc5678abc5678abc5678c
>> +branch refs/heads/locked
>> +locked
>> +
>> +worktree /path/to/linked-worktree-locked-with-reason
>> +HEAD 3456def3456def3456def3456def3456def3456b
>> +branch refs/heads/locked-with-reason
>> +locked reason why is locked
>
> I was momentarily confused by the branch named `locked` with the
> `locked` attribute in the first new stanza. Perhaps take a hint from
> the second new stanza and call the first one `locked-no-reason`:
>
>     worktree /path/to/linked-worktree-locked-no-reason
>     HEAD 5678abc5678abc5678abc5678abc5678abc5678c
>     branch refs/heads/locked-no-reason
>     locked
>
> Again, though, not worth a re-roll.
>

This seems like a nice touch. will include this in the next revision.

>> diff --git a/builtin/worktree.c b/builtin/worktree.c
>> @@ -579,6 +582,16 @@ static void show_worktree_porcelain(struct worktree *wt)
>> +       reason = worktree_lock_reason(wt);
>> +       if (reason && *reason) {
>> +               struct strbuf sb = STRBUF_INIT;
>> +               quote_c_style(reason, &sb, NULL, CQUOTE_NODQ);
>> +               printf("locked %s\n", sb.buf);
>> +               strbuf_release(&sb);
>> +       } else if (reason)
>> +               printf("locked\n");
>
> This needs a change, and it's totally my fault that it does. In my
> previous review, I mentioned that if the lock reason contains special
> characters, we want those special characters escaped and the reason
> quoted, but _only_ if it contains special characters. However, I then
> incorrectly said to call quote_c_style() with CQUOTE_NODQ to achieve
> that behavior. In fact, CQUOTE_NODQ gives us the wrong behavior since
> it avoids quoting the string which, as Phillip pointed out, makes it
> impossible to distinguish between a string which just happens to
> contain the two-character sequence '\' and 'n', and an escaped newline
> "\n". So, the above should really be:
>
>     quote_c_style(reason, &sb, NULL, 0);
>
> The example in the commit message should be adjusted to account for
> this change, as well:
>
>     In porcelain mode, if the lock reason contains special characters
>     such as newlines, they are escaped with backslashes and the entire
>     reason is enclosed in double quotes. For example:
>
>     $ git worktree list --porcelain
>     ...
>     locked "worktree's path mounted in\nremovable device"
>     ...
>
> And, of course, the new test will need a slight adjustment.
>

Alright, I believe I've got the whole picture now and sorry for the
confusion. You and Phillip clearly stated in the review cycle that the
reason should be quoted because of the aforementioned reasons and I 
dropped when I was working on this version.

I will re-roll and change this in the next revision.

>> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
>> @@ -66,11 +66,43 @@ test_expect_success '"list" all worktrees with locked annotation' '
>> +test_expect_success '"list" all worktrees --porcelain with locked' '
>> +       test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
>> +       echo "locked" >expect &&
>> +       echo "locked with reason" >>expect &&
>> +       git worktree add --detach locked1 &&
>> +       git worktree add --detach locked2 &&
>> +       git worktree add --detach unlocked &&
>> +       git worktree lock locked1 &&
>> +       git worktree lock locked2 --reason "with reason" &&
>> +       test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
>> +       git worktree list --porcelain >out &&
>> +       grep "^locked" out >actual &&
>> +       test_cmp expect actual
>> +'
>
> So, the purpose of the `unlocked` worktree in this test is to prove
> that it didn't accidentally get annotated with `locked`? (Since, if it
> did get annotated, then `actual` would contain too many lines and not
> match `expect`.) Is that correct?
>

Yes, this is what I intended to check when adding the `unlocked`
worktree. I'm considering how to make this more explicit so it's clear
for readers why the `unlocked` worktree exists in this test.


>> +test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
>> +       test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
>> +       printf "locked locked\\\\r\\\\nreason\n" >expect &&
>> +       printf "locked locked\\\\nreason\n" >>expect &&
>> +       git worktree add --detach locked_lf &&
>> +       git worktree add --detach locked_crlf &&
>> +       printf "locked\nreason\n\n" >reason_lf &&
>> +       printf "locked\r\nreason\n\n" >reason_crlf &&
>
> The trailing "\n\n" is unneeded. Due to the way `$(...)` expansion
> works (dropping trailing whitespace), you'll get the same successful
> test result with:
>
>     printf "locked\nreason\n" >reason_lf &&
>     printf "locked\r\nreason\n" >reason_crlf &&
>
> and even with:
>
>     printf "locked\nreason" >reason_lf &&
>     printf "locked\r\nreason" >reason_crlf &&
>
>> +       git worktree lock locked_lf --reason "$(cat reason_lf)" &&
>> +       git worktree lock locked_crlf --reason "$(cat reason_crlf)" &&
>
> You could also just embed the `printf`'s here rather than using these
> temporary files.
>
>     git worktree lock --reason $(printf "...") <path> &&
>

Having the `printf` together with the $(...) expansion seems like the
good simplification for this test. will include in the next revision.

> Or, if we care only about testing LF, and not about CRLF, even this would work:
>
>     git worktree lock --reason "reason with
>     newline" <path> &&
>
> but that gets a bit ugly.
>
> Anyhow, all the line terminator commentary about this test is a matter
> of personal taste, probably not worth a re-roll or even changing.


-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree
  2021-01-18  4:45     ` Eric Sunshine
@ 2021-01-19 10:26       ` Rafael Silva
  2021-01-19 17:23         ` Eric Sunshine
  0 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 10:26 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Eric Sunshine writes:

> On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:

>> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
>> @@ -234,6 +235,9 @@ This can also be set up as the default behaviour by using the
>>  --expire <time>::
>>         With `prune`, only expire unused working trees older than `<time>`.
>> ++
>> +With `list`, annotate missing working trees as prunable if they are
>> +older than `<mtime>`.
>
> s/mtime/time/
>

Oops. thanks for catching that. Will fix it in the next revision.

>> diff --git a/builtin/worktree.c b/builtin/worktree.c
>> @@ -592,6 +592,10 @@ static void show_worktree_porcelain(struct worktree *wt)
>> +       reason = worktree_prune_reason(wt, expire);
>> +       if (reason && *reason)
>> +               printf("prunable %s\n", reason);
>
> I lean toward not including `*reason` in the condition here. While one
> may argue that `*reason` is future-proofing the condition, we know
> today that worktree_prune_reason() will only ever return NULL or a
> non-empty string. So, having `*reason` in the condition is misleading
> and confuses readers into thinking that worktree_prune_reason() could
> return an empty string or that perhaps it did in the past. And
> studying the history of this file or even this commit won't help them
> understand why the author included `*reason` in the condition.
>

Fair enough. The `*reason` condition was indeed added to be safe and
future-proofing and, as you pointed it out, we know that currently
the worktree_prune_reason() returns a non-empty string when its true
and when the code reaches the `*reason` condition in
"if (reason && *reason)" this will always evaluates to true.

Agreed that removing this part of the condition will make the code
clearer and easier to followed. So I will drop this in the next
revision.

>> @@ -617,9 +622,14 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
>> -       if (!is_main_worktree(wt) && worktree_lock_reason(wt))
>> +       reason = worktree_lock_reason(wt);
>> +       if (reason)
>>                 strbuf_addstr(&sb, " locked");
>
> Although I understand what happened here because I read the entire
> series, for anyone reading this patch in the future or even someone
> reading this patch in isolation, the disappearance of
> is_main_worktree() from the condition is mysterious. They won't know
> if its removal was an accident or intentional, or if the change
> introduces a bug. Therefore, it would be better to drop
> is_main_worktree() from this conditional in patch [3/6], which is the
> patch which makes it safe to call worktree_lock_reason() even for the
> main worktree. Thus, in [3/6], this would change from:
>
>     if (!is_main_worktree(wt) && worktree_lock_reason(wt))
>
> to:
>
>     if (worktree_lock_reason(wt))
>
> and then in this patch [5/6], it becomes:
>
>     reason = worktree_lock_reason(wt);
>     if (reason)
>

That's a good point, and even worse this is not mentioned in my commit
message at all. It make sense to move this change into [3/6] where the
API is changed and all the reason is explained in the commit message.

>> +       reason = worktree_prune_reason(wt, expire);
>> +       if (reason)
>> +               strbuf_addstr(&sb, " prunable");
>
> Looking at this also makes me wonder if patches [5/6] and [6/6] should
> be swapped since it's not clear to the reader why you're adding the
> `reason` variable in this patch when the same could be accomplished
> more simply:
>
>     if (worktree_lock_reason(wt))
>         strbuf_addstr(&sb, " locked");
>     if (worktree_prune_reason(wt, expire))
>         strbuf_addstr(&sb, " prunable");
>
> If you swap the patches and add --verbose mode first which requires
> this new `reason` variable, then the mystery goes away, and the use of
> `reason` is obvious when `prunable` annotation is added in the
> subsequent patch.
>
> Having said that, I'm not trying to make extra work for you by
> expecting patch perfection. Sometimes it's fine to craft a patch in
> such a way that it makes subsequent patches simpler, even if it looks
> slightly odd in the current patch, and I haven't read [6/6] yet, so
> whatever opinion I express here is based only upon what I've read up
> to this point.

That's a good point. I'm inclined to leave the [5/6] with the following:

    if (worktree_prune_reason(wt, expire))
        strbuf_addstr(&sb, " prunable");

And move up the changes that includes the `reason` into the [5/6]
patches that introduces the --verbose option. This line seems easier to
follow when the reader is looking on this patch alone and only care
about a reason when the --verbose comes into play in the next patch
[6/6].

Although your suggestion about changing the patch also sounds reasonable
and I'll take into consideration when I re-roll this series.

Btw, I don't mind spending extra work on and I'm all forward
with the changes so we it would be easier to understand not only now where
all the patches are being reviewed together but in the future when
someone is looking at the history of the project, specially for
debugging/bisecting reasons.

Thanks for the insightful comments.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 0/6] teach `worktree list` verbose mode and prunable annotations
  2021-01-18  5:33   ` [PATCH v2 0/6] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
@ 2021-01-19 16:44     ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 16:44 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Eric Sunshine writes:

> On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> This patch series addresses some of these changes by teaching
>> `worktree list` to show "prunable" annotation, adding verbose mode and
>> extending the --porcelain format with prunable and locked annotation as
>> follow up from [1]. Additionally, it addresses one shortcoming for porcelain
>> format to escape any newline characters (LF or CRLF) for the lock reason
>> to prevent breaking the format that is mentioned in [4] and [1] during the
>> review cycle.
>
> Thanks for continuing to work on this. I'm pleased to see these
> enhancements coming together so nicely.
>
>> The patch series is organizes as:
>
> The new organization is a nice improvement over v1.
>
> As mentioned in my review of [5/6], there may be some value in
> swapping [5/6] and [6/6] to make it easier for readers to digest the
> changes, but it's not a hard requirement.
>
> To avoid being mysterious, there's also a change in [5/6] which
> probably belongs in [3/6], as explained in my review of [5/6].
>
> My review of patch [4/6] suggests optionally splitting out a bug fix
> into its own patch, but that's a very minor issue. Use your best
> judgment whether or not to do so.
>
>> 4. The fourth patch adds the "locked" attribute for the porcelain format
>>    in order to make both default and --porcelain format consistent.
>
> This patch does need a re-roll, and it's entirely my fault. In my
> review of the previous round, I said that if the lock reason contained
> special characters, we'd want to escape those characters and quote the
> string, but then I gave you a code suggestion which did the opposite
> of that. My [4/6] review goes into more detail.
>

I kindly disagree and I actually think is my fault :).

> Aside from the one important fix in [4/6], and the possible minor
> re-organizations suggested above, all my other review comments were
> very minor and probably subjective, thus nothing to sweat over.
>
> Nicely done.
>
> [1]: https://lore.kernel.org/git/CAPig+cSH6PKt8YvDJhMBvzvePYLqbf70uVV8TERoMj+kfxJRHQ@mail.gmail.com/

Thank you for reviewing this series and all the helpful suggestions. I
will send another revision taking all of them into account.

--
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-19  8:20       ` Rafael Silva
@ 2021-01-19 17:16         ` Eric Sunshine
  0 siblings, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-19 17:16 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Tue, Jan 19, 2021 at 3:20 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Eric Sunshine writes:
> > On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
> > <rafaeloliveira.cs@gmail.com> wrote:
> >> +               quote_c_style(reason, &sb, NULL, CQUOTE_NODQ);
> >
> > This needs a change, and it's totally my fault that it does. In my
> > previous review, I mentioned that if the lock reason contains special
> > characters, we want those special characters escaped and the reason
> > quoted, but _only_ if it contains special characters. However, I then
> > incorrectly said to call quote_c_style() with CQUOTE_NODQ to achieve
> > that behavior. In fact, CQUOTE_NODQ gives us the wrong behavior since
> > it avoids quoting the string which, as Phillip pointed out, makes it
> > impossible to distinguish between a string which just happens to
> > contain the two-character sequence '\' and 'n', and an escaped newline
> > "\n". So, the above should really be:
> >
> Alright, I believe I've got the whole picture now and sorry for the
> confusion. You and Phillip clearly stated in the review cycle that the
> reason should be quoted because of the aforementioned reasons and I
> dropped when I was working on this version.

It was my fault by confusing you with the misleading mention of CQUOTE_NODQ.

> >> +       git worktree add --detach locked1 &&
> >> +       git worktree add --detach locked2 &&
> >> +       git worktree add --detach unlocked &&
> >
> > So, the purpose of the `unlocked` worktree in this test is to prove
> > that it didn't accidentally get annotated with `locked`? (Since, if it
> > did get annotated, then `actual` would contain too many lines and not
> > match `expect`.) Is that correct?
>
> Yes, this is what I intended to check when adding the `unlocked`
> worktree. I'm considering how to make this more explicit so it's clear
> for readers why the `unlocked` worktree exists in this test.

A simple in-code comment should suffice, I would think:

    git worktree add --detach locked1 &&
    git worktree add --detach locked2 &&
    # unlocked worktree should not be annotated "locked"
    git worktree add --detach unlocked &&

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree
  2021-01-19 10:26       ` Rafael Silva
@ 2021-01-19 17:23         ` Eric Sunshine
  0 siblings, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-19 17:23 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Tue, Jan 19, 2021 at 5:26 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Eric Sunshine writes:
> > On Sun, Jan 17, 2021 at 6:43 PM Rafael Silva
> > <rafaeloliveira.cs@gmail.com> wrote:
> >> +       reason = worktree_prune_reason(wt, expire);
> >> +       if (reason)
> >> +               strbuf_addstr(&sb, " prunable");
> >
> > Looking at this also makes me wonder if patches [5/6] and [6/6] should
> > be swapped since it's not clear to the reader why you're adding the
> > `reason` variable in this patch when the same could be accomplished
> > more simply:
> >
> >     if (worktree_prune_reason(wt, expire))
> >         strbuf_addstr(&sb, " prunable");
> >
> > If you swap the patches and add --verbose mode first which requires
> > this new `reason` variable, then the mystery goes away, and the use of
> > `reason` is obvious when `prunable` annotation is added in the
> > subsequent patch.
>
> That's a good point. I'm inclined to leave the [5/6] with the following:
>
>     if (worktree_prune_reason(wt, expire))
>         strbuf_addstr(&sb, " prunable");
>
> And move up the changes that includes the `reason` into the [5/6]
> patches that introduces the --verbose option. This line seems easier to
> follow when the reader is looking on this patch alone and only care
> about a reason when the --verbose comes into play in the next patch
> [6/6].

I considered this, as well, but figured that it would make patch [6/6]
even noiser, and that swapping the patches would keep the noise level
down. But, I haven't actually tried it myself, so I could be wrong
about the assumption, and maybe the noisiness wouldn't be a problem in
practice or would be so stylized that it wouldn't matter.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
                     ` (6 preceding siblings ...)
  2021-01-18  5:33   ` [PATCH v2 0/6] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
@ 2021-01-19 21:27   ` Rafael Silva
  2021-01-19 21:27     ` [PATCH v3 1/7] worktree: libify should_prune_worktree() Rafael Silva
                       ` (8 more replies)
  7 siblings, 9 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

In c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) we taught `git worktree list` to annotate working tree that
is locked by appending "locked" text in order to signalize to the user
that a working tree is locked.  During the review, there was some
discussion about additional annotations and information that `list`
command could provide to the user that has long been envisioned and
mentioned in [2], [3] and [4].

This patch series addresses some of these changes by teaching
`worktree list` to show "prunable" annotation, adding verbose mode and
extending the --porcelain format with prunable and locked annotation as
follow up from [1]. Additionally, it addresses one shortcoming for porcelain
format to escape any newline characters (LF or CRLF) for the lock reason
to prevent breaking the format that is mentioned in [4] and [1] during the
review cycle.

The patch series is organizes as:

1. The first patch moves the should_prune_worktree() machinery to the top-level
   worktree.c exposing the function as general API, that will be reference
   by should_prune_worktree() wrapper implemented on the second patch.

   The original idea was to not only move should_prune_worktree() but also
   refactor to accept a "struct worktree" and load the information directly,
   which allows simplify the `prune` command by reusing get_worktrees().
   However this seems to also require refactoring get_worktrees() itself
   to return "non-valid" working trees that can/should be pruned. This is
   also mentioned in [5]. Having the wrapper function makes it easier to add
   the prunable annotation without touching the get_worktrees() and the
   other worktree sub commands. The refactoring can be addressed in a
   future patch, if this turns out to be good idea. One possible approach
   is to teach get_worktrees() to take additional flags that will tell
   whether to return only valid or all worktrees in GIT_DIR/worktrees/
   directory and address its own possible shortcoming.

2. Introduces the worktree_prune_reason() to discovery whether a worktree
   is prunable along with two new fields in the `worktree` structure. The
   function mimics the workree_lock_reason() API.

3. The third patch changes worktree_lock_reason() to be more gentle for
   the main working tree to simply returning NULL instead of aborting the
   program via assert() macro. This allow us to simplify the code that
   checks if the working tree is locked for default and porcelain format.
   This changes is also mentioned in [6].

4. Fix t2402 added in [1] to ensure the locked worktree is properly cleaned up.

5. The fourth patch adds the "locked" attribute for the porcelain format
   in order to make both default and --porcelain format consistent.

6. The fifth patch introduces "prunable" annotation for both default
   and --porcelain format.

7. The sixth patch introduces verbose mode to expand the `list` default
   format and show each annotation reason when its available.

Series is built on top of 66e871b664 (The third batch, 2021-01-15).

[1] https://lore.kernel.org/git/20200928154953.30396-1-rafaeloliveira.cs@gmail.com/
[2] https://lore.kernel.org/git/CAPig+cQF6V8HNdMX5AZbmz3_w2WhSfA4SFfNhQqxXBqPXTZL+w@mail.gmail.com/
[3] https://lore.kernel.org/git/CAPig+cSGXqJuaZPhUhOVX5X=LMrjVfv8ye_6ncMUbyKox1i7QA@mail.gmail.com/
[4] https://lore.kernel.org/git/CAPig+cTitWCs5vB=0iXuUyEY22c0gvjXvY1ZtTT90s74ydhE=A@mail.gmail.com/
[5] https://lore.kernel.org/git/CACsJy8ChM99n6skQCv-GmFiod19mnwwH4j-6R+cfZSiVFAxjgA@mail.gmail.com/
[6] https://lore.kernel.org/git/xmqq8sctlgzx.fsf@gitster.c.googlers.com/

Changes in v3
=============

v3 is 1-patch bigger than v2 and it includes the following changes:

 * Dropped CQUOTE_NODQ flag in the the locked reason to return a string
   enclosed with double quotes if the text reason contains especial
   characters, like newlines.

 * fix in t2402 to ensure the locked worktree is properly cleaned up,
   is move to its own patch.

 * In worktree_prune_reason(), the `path` variable is initialize
   with NULL to make the code easier to follow.

 * The removal of `is_main_worktree()` before `worktree_lock_reason()`
   is moved to the patch that actually changes the API to be more gentle
   with the main worktree instead of refactoring the code in the next
   patches that adds the annotations.

 * Drop the `*reason` in `(reason && *reason)` as we know that
   worktree_prune_reason() is either going to return a non-empty string
   or NULL which makes the code easier to follow.

 * The --verbose test for the locked annotation now tests that "locked"
   annotation stays on the same line when there is no locked reason.

 * Small documentation updates and make the "git worktree --verbose"
   example a little consistent with the worktree path.

Changes in v2
=============

v2 changes considerably from v1 taking into account several comments
suggested by Eric Sunshine and Phillip Wood (thanks guys for the
insightful comments and patience reviewing my patches :) ).

 * The biggest change are the way the series is organized. In v1 all the
   code was implemented in the fourth patch, all the tests and documentation
   updates was added in sixth and seventh patch respectfully, in v2 each
   patch introduces the annotations and verbose mode together with theirs
   respective test and documentation updates.

 * Several rewrite of the commit messages

 * In v1 the fifth patch was introducing a new function to escape newline
   characters for the "locked" attribute. However, we already have this
   feature from the quote.h API. So the new function is dropped and the
   changes are squashed into v2's fourth patch.

 * The new prunable annotation and locked annotation that was introduced
   by [1] was refactor to not poke around the worktree private fields.

 * Refactoring of the worktree_prune_reason() to cache the prune_reason
   and refactor to early return the use cases following the more common
   pattern used on Git's codebase.

 * Few documentation rewrites, most notably the `--verbose` and `--expire`
   doc sentences for the list command are moved to its own line to clearly
   separate the description from the others commands instead of continuing 
   on the same paragraph.

 * The `git unlock <worktree>` used in the test's cleanup is moved to after
   we know the `git worktree locked` is executed successfully. This ensures
   the unlock doesn't error in the cleanup stage which will make it harder
   to debug the tests.

Rafael Silva (7):
  worktree: libify should_prune_worktree()
  worktree: teach worktree to lazy-load "prunable" reason
  worktree: teach worktree_lock_reason() to gently handle main worktree
  t2402: ensure locked worktree is properly cleaned up
  worktree: teach `list --porcelain` to annotate locked worktree
  worktree: teach `list` to annotate prunable worktree
  worktree: teach `list` verbose mode

 Documentation/git-worktree.txt |  62 +++++++++++++++++--
 builtin/worktree.c             | 110 +++++++++++----------------------
 builtin/worktree.cc            |   0
 t/t2402-worktree-list.sh       |  93 ++++++++++++++++++++++++++++
 worktree.c                     |  91 ++++++++++++++++++++++++++-
 worktree.h                     |  23 +++++++
 6 files changed, 299 insertions(+), 80 deletions(-)
 create mode 100644 builtin/worktree.cc

Range-diff against v2:
-:  ---------- > 1:  66cd83ba42 worktree: libify should_prune_worktree()
1:  9d47fcb4a4 ! 2:  81f4824028 worktree: teach worktree to lazy-load "prunable" reason
    @@ worktree.c: const char *worktree_lock_reason(struct worktree *wt)
     +const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
     +{
     +	struct strbuf reason = STRBUF_INIT;
    -+	char *path;
    ++	char *path = NULL;
     +
     +	if (is_main_worktree(wt))
     +		return NULL;
2:  ac7c375bac ! 3:  d0dbf0b709 worktree: teach worktree_lock_reason() to gently handle main worktree
    @@ Commit message
     
         worktree_lock_reason() aborts with an assertion failure when called on
         the main worktree since locking the main worktree is nonsensical. Not
    -    only is this behaviour undocumented, thus callers might not even be aware
    +    only is this behavior undocumented, thus callers might not even be aware
         that the call could potentially crash the program, but it also forces
         clients to be extra careful:
     
    @@ Commit message
                 ...
     
         Since we know that locking makes no sense in the context of the main
    -    worktree, we can simpliy return false for the main worktree, thus making
    +    worktree, we can simply return false for the main worktree, thus making
         client code less complex by eliminating the need for the callers to have
         inside knowledge about the implementation:
     
    @@ Commit message
         Helped-by: Eric Sunshine <sunshine@sunshineco.com>
         Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
     
    + ## builtin/worktree.c ##
    +@@ builtin/worktree.c: static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
    + 			strbuf_addstr(&sb, "(error)");
    + 	}
    + 
    +-	if (!is_main_worktree(wt) && worktree_lock_reason(wt))
    ++	if (worktree_lock_reason(wt))
    + 		strbuf_addstr(&sb, " locked");
    + 
    + 	printf("%s\n", sb.buf);
    +
      ## worktree.c ##
     @@ worktree.c: int is_main_worktree(const struct worktree *wt)
      
-:  ---------- > 4:  c6f8a3e9cd t2402: ensure locked worktree is properly cleaned up
3:  ec1eb5a9f8 ! 5:  86c5253288 worktree: teach `list --porcelain` to annotate locked worktree
    @@ Commit message
             locked reason why it is locked
             ...
     
    -    The locked reason is quoted to prevent newline characters (i.e: LF or
    -    CRLF) breaking the --porcelain format as each attribute is shown per
    -    line. For example:
    +    In porcelain mode, if the lock reason contains special characters
    +    such as newlines, they are escaped with backslashes and the entire
    +    reason is enclosed in double quotes. For example:
     
            $ git worktree list --porcelain
            ...
    -       locked worktree's path mounted in\nremovable device
    +       locked "worktree's path mounted in\nremovable device"
            ...
     
         Furthermore, let's update the documentation to state that some
    @@ Commit message
         with its value depending whether the value is available or not. Thus
         documenting the case of the new "locked" attribute.
     
    -    Additionally, c57b3367be (worktree: teach `list` to annotate locked
    -    worktree, 2020-10-11) introduced a new test to ensure locked worktrees
    -    are listed with "locked" annotation. However, the test does not clean up
    -    after itself as "git worktree prune" is not going to remove the locked
    -    worktree in the first place. This not only leaves the test in an unclean
    -    state it also potentially breaks following tests that relies on the
    -    "git worktree list" output. Let's fix that by unlocking the worktree
    -    before the "prune" command.
    -
         Helped-by: Eric Sunshine <sunshine@sunshineco.com>
         Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
     
    @@ Documentation/git-worktree.txt: Porcelain Format
     -if the value is true.  The first attribute of a working tree is always
     -`worktree`, an empty line indicates the end of the record.  For example:
     +if the value is true.  Some attributes (like `locked`) can be listed as a label
    -+only or with a value depending whether a reason is available.  The first
    ++only or with a value depending upon whether a reason is available.  The first
     +attribute of a working tree is always `worktree`, an empty line indicates the
     +end of the record.  For example:
      
    @@ Documentation/git-worktree.txt: worktree /path/to/other-linked-worktree
      HEAD 1234abc1234abc1234abc1234abc1234abc1234a
      detached
      
    -+worktree /path/to/linked-worktree-locked
    ++worktree /path/to/linked-worktree-locked-no-reason
     +HEAD 5678abc5678abc5678abc5678abc5678abc5678c
    -+branch refs/heads/locked
    ++branch refs/heads/locked-no-reason
     +locked
     +
     +worktree /path/to/linked-worktree-locked-with-reason
    @@ builtin/worktree.c: static void show_worktree_porcelain(struct worktree *wt)
     +	reason = worktree_lock_reason(wt);
     +	if (reason && *reason) {
     +		struct strbuf sb = STRBUF_INIT;
    -+		quote_c_style(reason, &sb, NULL, CQUOTE_NODQ);
    ++		quote_c_style(reason, &sb, NULL, 0);
     +		printf("locked %s\n", sb.buf);
     +		strbuf_release(&sb);
     +	} else if (reason)
    @@ builtin/worktree.c: static void show_worktree_porcelain(struct worktree *wt)
     
      ## t/t2402-worktree-list.sh ##
     @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees with locked annotation' '
    - 	git worktree add --detach locked master &&
    - 	git worktree add --detach unlocked master &&
    - 	git worktree lock locked &&
    -+	test_when_finished "git worktree unlock locked" &&
    - 	git worktree list >out &&
    - 	grep "/locked  *[0-9a-f].* locked$" out &&
      	! grep "/unlocked  *[0-9a-f].* locked$" out
      '
      
    @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees with locked
     +	echo "locked with reason" >>expect &&
     +	git worktree add --detach locked1 &&
     +	git worktree add --detach locked2 &&
    ++	# unlocked worktree should not be annotated with "locked"
     +	git worktree add --detach unlocked &&
     +	git worktree lock locked1 &&
     +	git worktree lock locked2 --reason "with reason" &&
    @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees with locked
     +
     +test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
     +	test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
    -+	printf "locked locked\\\\r\\\\nreason\n" >expect &&
    -+	printf "locked locked\\\\nreason\n" >>expect &&
    ++	printf "locked \"locked\\\\r\\\\nreason\"\n" >expect &&
    ++	printf "locked \"locked\\\\nreason\"\n" >>expect &&
     +	git worktree add --detach locked_lf &&
     +	git worktree add --detach locked_crlf &&
    -+	printf "locked\nreason\n\n" >reason_lf &&
    -+	printf "locked\r\nreason\n\n" >reason_crlf &&
    -+	git worktree lock locked_lf --reason "$(cat reason_lf)" &&
    -+	git worktree lock locked_crlf --reason "$(cat reason_crlf)" &&
    ++	git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
    ++	git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
     +	test_when_finished "git worktree unlock locked_lf && git worktree unlock locked_crlf" &&
     +	git worktree list --porcelain >out &&
     +	grep "^locked" out >actual &&
4:  f937df6460 ! 6:  be814326ee worktree: teach `list` to annotate prunable worktree
    @@ Documentation/git-worktree.txt: This can also be set up as the default behaviour
      	With `prune`, only expire unused working trees older than `<time>`.
     ++
     +With `list`, annotate missing working trees as prunable if they are
    -+older than `<mtime>`.
    ++older than `<time>`.
      
      --reason <string>::
      	With `lock`, an explanation why the working tree is locked.
    @@ Documentation/git-worktree.txt: $ git worktree list
     +
     +------------
     +$ git worktree list
    -+/path/to/linked-worktree        abcd1234 [master]
    -+/path/to/locked-worktreee       acbd5678 (brancha) locked
    -+/path/to/prunable-worktree      5678abc  (detached HEAD) prunable
    ++/path/to/linked-worktree    abcd1234 [master]
    ++/path/to/locked-worktreee   acbd5678 (brancha) locked
    ++/path/to/prunable-worktree  5678abc  (detached HEAD) prunable
     +------------
     +
      Porcelain Format
    @@ builtin/worktree.c: static void show_worktree_porcelain(struct worktree *wt)
      		printf("locked\n");
      
     +	reason = worktree_prune_reason(wt, expire);
    -+	if (reason && *reason)
    ++	if (reason)
     +		printf("prunable %s\n", reason);
     +
      	printf("\n");
      }
      
     @@ builtin/worktree.c: static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
    - 	struct strbuf sb = STRBUF_INIT;
    - 	int cur_path_len = strlen(wt->path);
    - 	int path_adj = cur_path_len - utf8_strwidth(wt->path);
    -+	const char *reason;
    - 
    - 	strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
    - 	if (wt->is_bare)
    -@@ builtin/worktree.c: static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
    - 			strbuf_addstr(&sb, "(error)");
    - 	}
    - 
    --	if (!is_main_worktree(wt) && worktree_lock_reason(wt))
    -+	reason = worktree_lock_reason(wt);
    -+	if (reason)
    + 	if (worktree_lock_reason(wt))
      		strbuf_addstr(&sb, " locked");
      
    -+	reason = worktree_prune_reason(wt, expire);
    -+	if (reason)
    ++	if (worktree_prune_reason(wt, expire))
     +		strbuf_addstr(&sb, " prunable");
     +
      	printf("%s\n", sb.buf);
    @@ builtin/worktree.c: static int list(int ac, const char **av, const char *prefix)
      	if (ac)
      		usage_with_options(worktree_usage, options);
     
    + ## builtin/worktree.cc (new) ##
    +
      ## t/t2402-worktree-list.sh ##
     @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees --porcelain with locked reason newline
      	test_cmp expect actual
5:  0b6ab6dddb ! 7:  b0688f142d worktree: teach `list` verbose mode
    @@ Commit message
         worktrees a reason might be available provided by the user via `lock`
         command.
     
    -    Let's teach "git worktree list" to output the reason why the worktrees
    -    are being annotated. The reason is a text that can take virtually any
    -    size and appending the text on the default columned format will make it
    -    difficult to extend the command with other annotations and not fit nicely
    -    on the screen. In order to address this shortcoming the annotation is
    -    then moved to the next line indented followed by the reason, if the
    -    reason is not available the annotation stays on the same line as the
    -    worktree itself.
    +    Let's teach "git worktree list" a --verbose mode that outputs the reason
    +    why the worktrees are being annotated. The reason is a text that can take
    +    virtually any size and appending the text on the default columned format
    +    will make it difficult to extend the command with other annotations and
    +    not fit nicely on the screen. In order to address this shortcoming the
    +    annotation is then moved to the next line indented followed by the reason
    +    If the reason is not available the annotation stays on the same line as
    +    the worktree itself.
     
         The output of "git worktree list" with verbose becomes like so:
     
             $ git worktree list --verbose
             ...
    -        /path/to/locked                acb124 [branch-a] locked
    -        /path/to/locked-with-reason    acc125 [branch-b]
    +        /path/to/locked-no-reason    acb124 [branch-a] locked
    +        /path/to/locked-with-reason  acc125 [branch-b]
                 locked: worktree with a locked reason
    -        /path/to/prunable-reason       ace127 [branch-d]
    +        /path/to/prunable-reason     ace127 [branch-d]
                 prunable: gitdir file points to non-existent location
             ...
     
    @@ Documentation/git-worktree.txt: This can also be set up as the default behaviour
      --expire <time>::
      	With `prune`, only expire unused working trees older than `<time>`.
     @@ Documentation/git-worktree.txt: $ git worktree list
    - /path/to/prunable-worktree      5678abc  (detached HEAD) prunable
    + /path/to/prunable-worktree  5678abc  (detached HEAD) prunable
      ------------
      
     +For these annotations, a reason might also be available and this can be
    @@ Documentation/git-worktree.txt: $ git worktree list
     +
     +------------
     +$ git worktree list --verbose
    -+/path/to/linked-worktree        abcd1234 [master]
    -+/path/to/locked-worktreee       acbd5678 (brancha)
    -+	locked: working tree path is mounted on a removable device
    -+/path/to/locked-no-reason       abcd578  (detached HEAD) locked
    -+/path/to/prunable-worktree      5678abc  (detached HEAD)
    ++/path/to/linked-worktree              abcd1234 [master]
    ++/path/to/locked-worktree-no-reason    abcd5678 (detached HEAD) locked
    ++/path/to/locked-worktree-with-reason  1234abcd (brancha)
    ++	locked: working tree path is mounted on a portable device
    ++/path/to/prunable-worktree            5678abc1 (detached HEAD)
     +	prunable: gitdir file points to non-existent location
     +------------
     +
    @@ Documentation/git-worktree.txt: $ git worktree list
     
      ## builtin/worktree.c ##
     @@ builtin/worktree.c: static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
    + 	struct strbuf sb = STRBUF_INIT;
    + 	int cur_path_len = strlen(wt->path);
    + 	int path_adj = cur_path_len - utf8_strwidth(wt->path);
    ++	const char *reason;
    + 
    + 	strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
    + 	if (wt->is_bare)
    +@@ builtin/worktree.c: static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
    + 			strbuf_addstr(&sb, "(error)");
      	}
      
    - 	reason = worktree_lock_reason(wt);
    --	if (reason)
    +-	if (worktree_lock_reason(wt))
    ++	reason = worktree_lock_reason(wt);
     +	if (verbose && reason && *reason)
     +		strbuf_addf(&sb, "\n\tlocked: %s", reason);
     +	else if (reason)
      		strbuf_addstr(&sb, " locked");
      
    - 	reason = worktree_prune_reason(wt, expire);
    --	if (reason)
    -+	if (verbose && reason && *reason)
    +-	if (worktree_prune_reason(wt, expire))
    ++	reason = worktree_prune_reason(wt, expire);
    ++	if (verbose && reason)
     +		strbuf_addf(&sb, "\n\tprunable: %s", reason);
     +	else if (reason)
      		strbuf_addstr(&sb, " prunable");
    @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees with prunabl
     +'
     +
     +test_expect_success '"list" all worktrees --verbose with locked' '
    -+	test_when_finished "rm -rf locked out actual expect && git worktree prune" &&
    -+	git worktree add locked --detach &&
    -+	git worktree lock locked --reason "with reason" &&
    -+	test_when_finished "git worktree unlock locked" &&
    -+	echo "$(git -C locked rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
    ++	test_when_finished "rm -rf locked1 locked2 out actual expect && git worktree prune" &&
    ++	git worktree add locked1 --detach &&
    ++	git worktree add locked2 --detach &&
    ++	git worktree lock locked1 &&
    ++	git worktree lock locked2 --reason "with reason" &&
    ++	test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
    ++	echo "$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
     +	printf "\tlocked: with reason\n" >>expect &&
     +	git worktree list --verbose >out &&
    -+	sed -n "s/  */ /g;/\/locked  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
    ++	grep "/locked1  *[0-9a-f].* locked$" out &&
    ++	sed -n "s/  */ /g;/\/locked2  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
     +	test_cmp actual expect
     +'
     +
-- 
2.30.0.421.g32f838e276


^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH v3 1/7] worktree: libify should_prune_worktree()
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
@ 2021-01-19 21:27     ` Rafael Silva
  2021-01-19 21:27     ` [PATCH v3 2/7] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
                       ` (7 subsequent siblings)
  8 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

As part of teaching "git worktree list" to annotate worktree that is a
candidate for pruning, let's move should_prune_worktree() from
builtin/worktree.c to worktree.c in order to make part of the worktree
public API.

should_prune_worktree() knows how to select the given worktree for
pruning based on an expiration date, however the expiration value is
stored in a static file-scope variable and it is not local to the
function. In order to move the function, teach should_prune_worktree()
to take the expiration date as an argument and document the new
parameter that is not immediately obvious.

Also, change the function comment to clearly state that the worktree's
path is returned in `wtpath` argument.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 builtin/worktree.c | 75 +---------------------------------------------
 worktree.c         | 68 +++++++++++++++++++++++++++++++++++++++++
 worktree.h         | 14 +++++++++
 3 files changed, 83 insertions(+), 74 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index 71287b2da6..dd886d5029 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -67,79 +67,6 @@ static void delete_worktrees_dir_if_empty(void)
 	rmdir(git_path("worktrees")); /* ignore failed removal */
 }
 
-/*
- * Return true if worktree entry should be pruned, along with the reason for
- * pruning. Otherwise, return false and the worktree's path, or NULL if it
- * cannot be determined. Caller is responsible for freeing returned path.
- */
-static int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath)
-{
-	struct stat st;
-	char *path;
-	int fd;
-	size_t len;
-	ssize_t read_result;
-
-	*wtpath = NULL;
-	if (!is_directory(git_path("worktrees/%s", id))) {
-		strbuf_addstr(reason, _("not a valid directory"));
-		return 1;
-	}
-	if (file_exists(git_path("worktrees/%s/locked", id)))
-		return 0;
-	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
-		strbuf_addstr(reason, _("gitdir file does not exist"));
-		return 1;
-	}
-	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
-	if (fd < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		return 1;
-	}
-	len = xsize_t(st.st_size);
-	path = xmallocz(len);
-
-	read_result = read_in_full(fd, path, len);
-	if (read_result < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		close(fd);
-		free(path);
-		return 1;
-	}
-	close(fd);
-
-	if (read_result != len) {
-		strbuf_addf(reason,
-			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
-			    (uintmax_t)len, (uintmax_t)read_result);
-		free(path);
-		return 1;
-	}
-	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
-		len--;
-	if (!len) {
-		strbuf_addstr(reason, _("invalid gitdir file"));
-		free(path);
-		return 1;
-	}
-	path[len] = '\0';
-	if (!file_exists(path)) {
-		if (stat(git_path("worktrees/%s/index", id), &st) ||
-		    st.st_mtime <= expire) {
-			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
-			free(path);
-			return 1;
-		} else {
-			*wtpath = path;
-			return 0;
-		}
-	}
-	*wtpath = path;
-	return 0;
-}
-
 static void prune_worktree(const char *id, const char *reason)
 {
 	if (show_only || verbose)
@@ -195,7 +122,7 @@ static void prune_worktrees(void)
 		if (is_dot_or_dotdot(d->d_name))
 			continue;
 		strbuf_reset(&reason);
-		if (should_prune_worktree(d->d_name, &reason, &path))
+		if (should_prune_worktree(d->d_name, &reason, &path, expire))
 			prune_worktree(d->d_name, reason.buf);
 		else if (path)
 			string_list_append(&kept, path)->util = xstrdup(d->d_name);
diff --git a/worktree.c b/worktree.c
index 821b233479..8ae019af79 100644
--- a/worktree.c
+++ b/worktree.c
@@ -741,3 +741,71 @@ void repair_worktree_at_path(const char *path,
 	strbuf_release(&realdotgit);
 	strbuf_release(&dotgit);
 }
+
+int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
+{
+	struct stat st;
+	char *path;
+	int fd;
+	size_t len;
+	ssize_t read_result;
+
+	*wtpath = NULL;
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addstr(reason, _("not a valid directory"));
+		return 1;
+	}
+	if (file_exists(git_path("worktrees/%s/locked", id)))
+		return 0;
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addstr(reason, _("gitdir file does not exist"));
+		return 1;
+	}
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		return 1;
+	}
+	len = xsize_t(st.st_size);
+	path = xmallocz(len);
+
+	read_result = read_in_full(fd, path, len);
+	if (read_result < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		close(fd);
+		free(path);
+		return 1;
+	}
+	close(fd);
+
+	if (read_result != len) {
+		strbuf_addf(reason,
+			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
+			    (uintmax_t)len, (uintmax_t)read_result);
+		free(path);
+		return 1;
+	}
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addstr(reason, _("invalid gitdir file"));
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		if (stat(git_path("worktrees/%s/index", id), &st) ||
+		    st.st_mtime <= expire) {
+			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
+			free(path);
+			return 1;
+		} else {
+			*wtpath = path;
+			return 0;
+		}
+	}
+	*wtpath = path;
+	return 0;
+}
diff --git a/worktree.h b/worktree.h
index f38e6fd5a2..818e1491c7 100644
--- a/worktree.h
+++ b/worktree.h
@@ -73,6 +73,20 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return true if worktree entry should be pruned, along with the reason for
+ * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
+ * NULL if it cannot be determined. Caller is responsible for freeing
+ * returned path.
+ *
+ * `expire` defines a grace period to prune the worktree when its path
+ * does not exist.
+ */
+int should_prune_worktree(const char *id,
+			  struct strbuf *reason,
+			  char **wtpath,
+			  timestamp_t expire);
+
 #define WT_VALIDATE_WORKTREE_MISSING_OK (1 << 0)
 
 /*
-- 
2.30.0.421.g32f838e276


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v3 2/7] worktree: teach worktree to lazy-load "prunable" reason
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
  2021-01-19 21:27     ` [PATCH v3 1/7] worktree: libify should_prune_worktree() Rafael Silva
@ 2021-01-19 21:27     ` Rafael Silva
  2021-01-19 21:27     ` [PATCH v3 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
                       ` (6 subsequent siblings)
  8 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

Add worktree_prune_reason() to allow a caller to discover whether a
worktree is prunable and the reason that it is, much like
worktree_lock_reason() indicates whether a worktree is locked and the
reason for the lock. As with worktree_lock_reason(), retrieve the
prunable reason lazily and cache it in the `worktree` structure.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 worktree.c | 20 ++++++++++++++++++++
 worktree.h |  9 +++++++++
 2 files changed, 29 insertions(+)

diff --git a/worktree.c b/worktree.c
index 8ae019af79..fb3e286996 100644
--- a/worktree.c
+++ b/worktree.c
@@ -15,6 +15,7 @@ void free_worktrees(struct worktree **worktrees)
 		free(worktrees[i]->id);
 		free(worktrees[i]->head_ref);
 		free(worktrees[i]->lock_reason);
+		free(worktrees[i]->prune_reason);
 		free(worktrees[i]);
 	}
 	free (worktrees);
@@ -245,6 +246,25 @@ const char *worktree_lock_reason(struct worktree *wt)
 	return wt->lock_reason;
 }
 
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
+{
+	struct strbuf reason = STRBUF_INIT;
+	char *path = NULL;
+
+	if (is_main_worktree(wt))
+		return NULL;
+	if (wt->prune_reason_valid)
+		return wt->prune_reason;
+
+	if (should_prune_worktree(wt->id, &reason, &path, expire))
+		wt->prune_reason = strbuf_detach(&reason, NULL);
+	wt->prune_reason_valid = 1;
+
+	strbuf_release(&reason);
+	free(path);
+	return wt->prune_reason;
+}
+
 /* convenient wrapper to deal with NULL strbuf */
 static void strbuf_addf_gently(struct strbuf *buf, const char *fmt, ...)
 {
diff --git a/worktree.h b/worktree.h
index 818e1491c7..8b7c408132 100644
--- a/worktree.h
+++ b/worktree.h
@@ -11,11 +11,13 @@ struct worktree {
 	char *id;
 	char *head_ref;		/* NULL if HEAD is broken or detached */
 	char *lock_reason;	/* private - use worktree_lock_reason */
+	char *prune_reason;     /* private - use worktree_prune_reason */
 	struct object_id head_oid;
 	int is_detached;
 	int is_bare;
 	int is_current;
 	int lock_reason_valid; /* private */
+	int prune_reason_valid; /* private */
 };
 
 /*
@@ -73,6 +75,13 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return the reason string if the given worktree should be pruned, otherwise
+ * NULL if it should not be pruned. `expire` defines a grace period to prune
+ * the worktree when its path does not exist.
+ */
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
+
 /*
  * Return true if worktree entry should be pruned, along with the reason for
  * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
-- 
2.30.0.421.g32f838e276


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v3 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
  2021-01-19 21:27     ` [PATCH v3 1/7] worktree: libify should_prune_worktree() Rafael Silva
  2021-01-19 21:27     ` [PATCH v3 2/7] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
@ 2021-01-19 21:27     ` Rafael Silva
  2021-01-19 21:27     ` [PATCH v3 4/7] t2402: ensure locked worktree is properly cleaned up Rafael Silva
                       ` (5 subsequent siblings)
  8 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

worktree_lock_reason() aborts with an assertion failure when called on
the main worktree since locking the main worktree is nonsensical. Not
only is this behavior undocumented, thus callers might not even be aware
that the call could potentially crash the program, but it also forces
clients to be extra careful:

    if (!is_main_worktree(wt) && worktree_locked_reason(...))
        ...

Since we know that locking makes no sense in the context of the main
worktree, we can simply return false for the main worktree, thus making
client code less complex by eliminating the need for the callers to have
inside knowledge about the implementation:

    if (worktree_lock_reason(...))
        ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 builtin/worktree.c | 2 +-
 worktree.c         | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index dd886d5029..df90a5acca 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -604,7 +604,7 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 			strbuf_addstr(&sb, "(error)");
 	}
 
-	if (!is_main_worktree(wt) && worktree_lock_reason(wt))
+	if (worktree_lock_reason(wt))
 		strbuf_addstr(&sb, " locked");
 
 	printf("%s\n", sb.buf);
diff --git a/worktree.c b/worktree.c
index fb3e286996..e00858540e 100644
--- a/worktree.c
+++ b/worktree.c
@@ -225,7 +225,8 @@ int is_main_worktree(const struct worktree *wt)
 
 const char *worktree_lock_reason(struct worktree *wt)
 {
-	assert(!is_main_worktree(wt));
+	if (is_main_worktree(wt))
+		return NULL;
 
 	if (!wt->lock_reason_valid) {
 		struct strbuf path = STRBUF_INIT;
-- 
2.30.0.421.g32f838e276


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v3 4/7] t2402: ensure locked worktree is properly cleaned up
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
                       ` (2 preceding siblings ...)
  2021-01-19 21:27     ` [PATCH v3 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
@ 2021-01-19 21:27     ` Rafael Silva
  2021-01-24  7:50       ` Eric Sunshine
  2021-01-19 21:27     ` [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
                       ` (4 subsequent siblings)
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

In c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) introduced a new test to ensure locked worktrees are listed
with "locked" annotation. However, the test does not clean up after
itself as "git worktree prune" is not going to remove the locked worktree
in the first place. This not only leaves the test in an unclean state it
also potentially breaks following tests that relies on the
"git worktree list" output.

Let's fix that by unlocking the worktree before the "prune" command.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 t/t2402-worktree-list.sh | 1 +
 1 file changed, 1 insertion(+)

diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 795ddca2e4..1866ea09f6 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -66,6 +66,7 @@ test_expect_success '"list" all worktrees with locked annotation' '
 	git worktree add --detach locked master &&
 	git worktree add --detach unlocked master &&
 	git worktree lock locked &&
+	test_when_finished "git worktree unlock locked" &&
 	git worktree list >out &&
 	grep "/locked  *[0-9a-f].* locked$" out &&
 	! grep "/unlocked  *[0-9a-f].* locked$" out
-- 
2.30.0.421.g32f838e276


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
                       ` (3 preceding siblings ...)
  2021-01-19 21:27     ` [PATCH v3 4/7] t2402: ensure locked worktree is properly cleaned up Rafael Silva
@ 2021-01-19 21:27     ` Rafael Silva
  2021-01-20 11:00       ` Phillip Wood
  2021-01-24  8:10       ` Eric Sunshine
  2021-01-19 21:27     ` [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree Rafael Silva
                       ` (3 subsequent siblings)
  8 siblings, 2 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

Commit c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) taught "git worktree list" to annotate locked worktrees by
appending "locked" text to its output, however, this is not listed in
the --porcelain format.

Teach "list --porcelain" to do the same and add a "locked" attribute
followed by its reason, thus making both default and porcelain format
consistent. If the locked reason is not available then only "locked"
is shown.

The output of the "git worktree list --porcelain" becomes like so:

    $ git worktree list --porcelain
    ...
    worktree /path/to/locked
    HEAD 123abcdea123abcd123acbd123acbda123abcd12
    detached
    locked

    worktree /path/to/locked-with-reason
    HEAD abc123abc123abc123abc123abc123abc123abc1
    detached
    locked reason why it is locked
    ...

In porcelain mode, if the lock reason contains special characters
such as newlines, they are escaped with backslashes and the entire
reason is enclosed in double quotes. For example:

   $ git worktree list --porcelain
   ...
   locked "worktree's path mounted in\nremovable device"
   ...

Furthermore, let's update the documentation to state that some
attributes in the porcelain format might be listed alone or together
with its value depending whether the value is available or not. Thus
documenting the case of the new "locked" attribute.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 16 ++++++++++++++--
 builtin/worktree.c             | 13 +++++++++++++
 t/t2402-worktree-list.sh       | 30 ++++++++++++++++++++++++++++++
 3 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 02a706c4c0..7cb8124f28 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -377,8 +377,10 @@ Porcelain Format
 The porcelain format has a line per attribute.  Attributes are listed with a
 label and value separated by a single space.  Boolean attributes (like `bare`
 and `detached`) are listed as a label only, and are present only
-if the value is true.  The first attribute of a working tree is always
-`worktree`, an empty line indicates the end of the record.  For example:
+if the value is true.  Some attributes (like `locked`) can be listed as a label
+only or with a value depending upon whether a reason is available.  The first
+attribute of a working tree is always `worktree`, an empty line indicates the
+end of the record.  For example:
 
 ------------
 $ git worktree list --porcelain
@@ -393,6 +395,16 @@ worktree /path/to/other-linked-worktree
 HEAD 1234abc1234abc1234abc1234abc1234abc1234a
 detached
 
+worktree /path/to/linked-worktree-locked-no-reason
+HEAD 5678abc5678abc5678abc5678abc5678abc5678c
+branch refs/heads/locked-no-reason
+locked
+
+worktree /path/to/linked-worktree-locked-with-reason
+HEAD 3456def3456def3456def3456def3456def3456b
+branch refs/heads/locked-with-reason
+locked reason why is locked
+
 ------------
 
 EXAMPLES
diff --git a/builtin/worktree.c b/builtin/worktree.c
index df90a5acca..98177f91d4 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -12,6 +12,7 @@
 #include "submodule.h"
 #include "utf8.h"
 #include "worktree.h"
+#include "quote.h"
 
 static const char * const worktree_usage[] = {
 	N_("git worktree add [<options>] <path> [<commit-ish>]"),
@@ -569,6 +570,8 @@ static int add(int ac, const char **av, const char *prefix)
 
 static void show_worktree_porcelain(struct worktree *wt)
 {
+	const char *reason;
+
 	printf("worktree %s\n", wt->path);
 	if (wt->is_bare)
 		printf("bare\n");
@@ -579,6 +582,16 @@ static void show_worktree_porcelain(struct worktree *wt)
 		else if (wt->head_ref)
 			printf("branch %s\n", wt->head_ref);
 	}
+
+	reason = worktree_lock_reason(wt);
+	if (reason && *reason) {
+		struct strbuf sb = STRBUF_INIT;
+		quote_c_style(reason, &sb, NULL, 0);
+		printf("locked %s\n", sb.buf);
+		strbuf_release(&sb);
+	} else if (reason)
+		printf("locked\n");
+
 	printf("\n");
 }
 
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 1866ea09f6..1fe53c3309 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -72,6 +72,36 @@ test_expect_success '"list" all worktrees with locked annotation' '
 	! grep "/unlocked  *[0-9a-f].* locked$" out
 '
 
+test_expect_success '"list" all worktrees --porcelain with locked' '
+	test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
+	echo "locked" >expect &&
+	echo "locked with reason" >>expect &&
+	git worktree add --detach locked1 &&
+	git worktree add --detach locked2 &&
+	# unlocked worktree should not be annotated with "locked"
+	git worktree add --detach unlocked &&
+	git worktree lock locked1 &&
+	git worktree lock locked2 --reason "with reason" &&
+	test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
+	test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
+	printf "locked \"locked\\\\r\\\\nreason\"\n" >expect &&
+	printf "locked \"locked\\\\nreason\"\n" >>expect &&
+	git worktree add --detach locked_lf &&
+	git worktree add --detach locked_crlf &&
+	git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
+	git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
+	test_when_finished "git worktree unlock locked_lf && git worktree unlock locked_crlf" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.421.g32f838e276


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
                       ` (4 preceding siblings ...)
  2021-01-19 21:27     ` [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
@ 2021-01-19 21:27     ` Rafael Silva
  2021-01-21  3:28       ` Junio C Hamano
  2021-01-19 21:27     ` [PATCH v3 7/7] worktree: teach `list` verbose mode Rafael Silva
                       ` (2 subsequent siblings)
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

The "git worktree list" command shows the absolute path to the worktree,
the commit that is checked out, the name of the branch, and a "locked"
annotation if the worktree is locked, however, it does not indicate
whether the worktree is prunable.

The "prune" command will remove a worktree if it is prunable unless
`--dry-run` option is specified. This could lead to a worktree being
removed without the user realizing before it is too late, in case the
user forgets to pass --dry-run for instance. If the "list" command shows
which worktree is prunable, the user could verify before running
"git worktree prune" and hopefully prevents the working tree to be
removed "accidentally" on the worse case scenario.

Let's teach "git worktree list" to show when a worktree is a prunable
candidate for both default and porcelain format.

In the default format a "prunable" text is appended:

    $ git worktree list
    /path/to/main      aba123 [main]
    /path/to/linked    123abc [branch-a]
    /path/to/prunable  ace127 (detached HEAD) prunable

In the --porcelain format a prunable label is added followed by
its reason:

    $ git worktree list --porcelain
    ...
    worktree /path/to/prunable
    HEAD abc1234abc1234abc1234abc1234abc1234abc12
    detached
    prunable gitdir file points to non-existent location
    ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 26 ++++++++++++++++++++++++--
 builtin/worktree.c             | 10 ++++++++++
 builtin/worktree.cc            |  0
 t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
 4 files changed, 66 insertions(+), 2 deletions(-)
 create mode 100644 builtin/worktree.cc

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 7cb8124f28..e0dd291e30 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -97,8 +97,9 @@ list::
 List details of each working tree.  The main working tree is listed first,
 followed by each of the linked working trees.  The output details include
 whether the working tree is bare, the revision currently checked out, the
-branch currently checked out (or "detached HEAD" if none), and "locked" if
-the worktree is locked.
+branch currently checked out (or "detached HEAD" if none), "locked" if
+the worktree is locked, "prunable" if the worktree can be pruned by `prune`
+command.
 
 lock::
 
@@ -234,6 +235,9 @@ This can also be set up as the default behaviour by using the
 
 --expire <time>::
 	With `prune`, only expire unused working trees older than `<time>`.
++
+With `list`, annotate missing working trees as prunable if they are
+older than `<time>`.
 
 --reason <string>::
 	With `lock`, an explanation why the working tree is locked.
@@ -372,6 +376,19 @@ $ git worktree list
 /path/to/other-linked-worktree  1234abc  (detached HEAD)
 ------------
 
+The command also shows annotations for each working tree, according to its state.
+These annotations are:
+
+ * `locked`, if the working tree is locked.
+ * `prunable`, if the working tree can be pruned via `git worktree prune`.
+
+------------
+$ git worktree list
+/path/to/linked-worktree    abcd1234 [master]
+/path/to/locked-worktreee   acbd5678 (brancha) locked
+/path/to/prunable-worktree  5678abc  (detached HEAD) prunable
+------------
+
 Porcelain Format
 ~~~~~~~~~~~~~~~~
 The porcelain format has a line per attribute.  Attributes are listed with a
@@ -405,6 +422,11 @@ HEAD 3456def3456def3456def3456def3456def3456b
 branch refs/heads/locked-with-reason
 locked reason why is locked
 
+worktree /path/to/linked-worktree-prunable
+HEAD 1233def1234def1234def1234def1234def1234b
+detached
+prunable gitdir file points to non-existent location
+
 ------------
 
 EXAMPLES
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 98177f91d4..20944c266e 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -592,6 +592,10 @@ static void show_worktree_porcelain(struct worktree *wt)
 	} else if (reason)
 		printf("locked\n");
 
+	reason = worktree_prune_reason(wt, expire);
+	if (reason)
+		printf("prunable %s\n", reason);
+
 	printf("\n");
 }
 
@@ -620,6 +624,9 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 	if (worktree_lock_reason(wt))
 		strbuf_addstr(&sb, " locked");
 
+	if (worktree_prune_reason(wt, expire))
+		strbuf_addstr(&sb, " prunable");
+
 	printf("%s\n", sb.buf);
 	strbuf_release(&sb);
 }
@@ -663,9 +670,12 @@ static int list(int ac, const char **av, const char *prefix)
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT_EXPIRY_DATE(0, "expire", &expire,
+				N_("add 'prunable' annotation to worktrees older than <time>")),
 		OPT_END()
 	};
 
+	expire = TIME_MAX;
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
diff --git a/builtin/worktree.cc b/builtin/worktree.cc
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 1fe53c3309..9e342d6c66 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -102,6 +102,38 @@ test_expect_success '"list" all worktrees --porcelain with locked reason newline
 	test_cmp expect actual
 '
 
+test_expect_success '"list" all worktrees with prunable annotation' '
+	test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	git worktree add --detach unprunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	! grep "/unprunable  *[0-9a-f].* prunable$"
+'
+
+test_expect_success '"list" all worktrees --porcelain with prunable' '
+	test_when_finished "rm -rf prunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	rm -rf prunable &&
+	git worktree list --porcelain >out &&
+	sed -n "/^worktree .*\/prunable$/,/^$/p" <out >only_prunable &&
+	test_i18ngrep "^prunable gitdir file points to non-existent location$" only_prunable
+'
+
+test_expect_success '"list" all worktrees with prunable consistent with "prune"' '
+	test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	git worktree add --detach unprunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	! grep "/unprunable  *[0-9a-f].* unprunable$" out &&
+	git worktree prune --verbose >out &&
+	test_i18ngrep "^Removing worktrees/prunable" out &&
+	test_i18ngrep ! "^Removing worktrees/unprunable" out
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.421.g32f838e276


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v3 7/7] worktree: teach `list` verbose mode
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
                       ` (5 preceding siblings ...)
  2021-01-19 21:27     ` [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree Rafael Silva
@ 2021-01-19 21:27     ` Rafael Silva
  2021-01-24  8:42       ` Eric Sunshine
  2021-01-24  8:51     ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
  2021-01-27  8:03     ` Rafael Silva
  8 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-19 21:27 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Rafael Silva

"git worktree list" annotates each worktree according to its state such
as "prunable" or "locked", however it is not immediately obvious why
these worktrees are being annotated. For prunable worktrees a reason
is available that is returned by should_prune_worktree() and for locked
worktrees a reason might be available provided by the user via `lock`
command.

Let's teach "git worktree list" a --verbose mode that outputs the reason
why the worktrees are being annotated. The reason is a text that can take
virtually any size and appending the text on the default columned format
will make it difficult to extend the command with other annotations and
not fit nicely on the screen. In order to address this shortcoming the
annotation is then moved to the next line indented followed by the reason
If the reason is not available the annotation stays on the same line as
the worktree itself.

The output of "git worktree list" with verbose becomes like so:

    $ git worktree list --verbose
    ...
    /path/to/locked-no-reason    acb124 [branch-a] locked
    /path/to/locked-with-reason  acc125 [branch-b]
        locked: worktree with a locked reason
    /path/to/prunable-reason     ace127 [branch-d]
        prunable: gitdir file points to non-existent location
    ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 20 ++++++++++++++++++++
 builtin/worktree.c             | 14 ++++++++++++--
 t/t2402-worktree-list.sh       | 30 ++++++++++++++++++++++++++++++
 3 files changed, 62 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index e0dd291e30..27f79ce638 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -232,6 +232,8 @@ This can also be set up as the default behaviour by using the
 -v::
 --verbose::
 	With `prune`, report all removals.
++
+With `list`, output additional information about worktrees (see below).
 
 --expire <time>::
 	With `prune`, only expire unused working trees older than `<time>`.
@@ -389,6 +391,24 @@ $ git worktree list
 /path/to/prunable-worktree  5678abc  (detached HEAD) prunable
 ------------
 
+For these annotations, a reason might also be available and this can be
+seen using the verbose mode. The annotation is then moved to the next line
+indented followed by the additional information.
+
+------------
+$ git worktree list --verbose
+/path/to/linked-worktree              abcd1234 [master]
+/path/to/locked-worktree-no-reason    abcd5678 (detached HEAD) locked
+/path/to/locked-worktree-with-reason  1234abcd (brancha)
+	locked: working tree path is mounted on a portable device
+/path/to/prunable-worktree            5678abc1 (detached HEAD)
+	prunable: gitdir file points to non-existent location
+------------
+
+Note that the annotation is moved to the next line if the additional
+information is available, otherwise it stays on the same line as the
+working tree itself.
+
 Porcelain Format
 ~~~~~~~~~~~~~~~~
 The porcelain format has a line per attribute.  Attributes are listed with a
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 20944c266e..1cd5c2016e 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -604,6 +604,7 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 	struct strbuf sb = STRBUF_INIT;
 	int cur_path_len = strlen(wt->path);
 	int path_adj = cur_path_len - utf8_strwidth(wt->path);
+	const char *reason;
 
 	strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
 	if (wt->is_bare)
@@ -621,10 +622,16 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 			strbuf_addstr(&sb, "(error)");
 	}
 
-	if (worktree_lock_reason(wt))
+	reason = worktree_lock_reason(wt);
+	if (verbose && reason && *reason)
+		strbuf_addf(&sb, "\n\tlocked: %s", reason);
+	else if (reason)
 		strbuf_addstr(&sb, " locked");
 
-	if (worktree_prune_reason(wt, expire))
+	reason = worktree_prune_reason(wt, expire);
+	if (verbose && reason)
+		strbuf_addf(&sb, "\n\tprunable: %s", reason);
+	else if (reason)
 		strbuf_addstr(&sb, " prunable");
 
 	printf("%s\n", sb.buf);
@@ -670,6 +677,7 @@ static int list(int ac, const char **av, const char *prefix)
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("add 'prunable' annotation to worktrees older than <time>")),
 		OPT_END()
@@ -679,6 +687,8 @@ static int list(int ac, const char **av, const char *prefix)
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
+	else if (verbose && porcelain)
+		die(_("--verbose and --porcelain are mutually exclusive"));
 	else {
 		struct worktree **worktrees = get_worktrees();
 		int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 9e342d6c66..6346cd38a8 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -134,6 +134,36 @@ test_expect_success '"list" all worktrees with prunable consistent with "prune"'
 	test_i18ngrep ! "^Removing worktrees/unprunable" out
 '
 
+test_expect_success '"list" --verbose and --porcelain mutually exclusive' '
+	test_must_fail git worktree list --verbose --porcelain
+'
+
+test_expect_success '"list" all worktrees --verbose with locked' '
+	test_when_finished "rm -rf locked1 locked2 out actual expect && git worktree prune" &&
+	git worktree add locked1 --detach &&
+	git worktree add locked2 --detach &&
+	git worktree lock locked1 &&
+	git worktree lock locked2 --reason "with reason" &&
+	test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
+	echo "$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tlocked: with reason\n" >>expect &&
+	git worktree list --verbose >out &&
+	grep "/locked1  *[0-9a-f].* locked$" out &&
+	sed -n "s/  */ /g;/\/locked2  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
+	test_cmp actual expect
+'
+
+test_expect_success '"list" all worktrees --verbose with prunable' '
+	test_when_finished "rm -rf prunable out actual expect && git worktree prune" &&
+	git worktree add prunable --detach &&
+	echo "$(git -C prunable rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tprunable: gitdir file points to non-existent location\n" >>expect &&
+	rm -rf prunable &&
+	git worktree list --verbose >out &&
+	sed -n "s/  */ /g;/\/prunable  *[0-9a-f].*$/,/prunable: .*$/p" <out >actual &&
+	test_i18ncmp actual expect
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.421.g32f838e276


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-19 21:27     ` [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
@ 2021-01-20 11:00       ` Phillip Wood
  2021-01-21  3:18         ` Junio C Hamano
                           ` (2 more replies)
  2021-01-24  8:10       ` Eric Sunshine
  1 sibling, 3 replies; 88+ messages in thread
From: Phillip Wood @ 2021-01-20 11:00 UTC (permalink / raw)
  To: Rafael Silva, git; +Cc: Eric Sunshine

Hi Rafael

Thanks for reworking this to use c_quote_path(). I have a couple of 
comments below.

On 19/01/2021 21:27, Rafael Silva wrote:
> Commit c57b3367be (worktree: teach `list` to annotate locked worktree,
> 2020-10-11) taught "git worktree list" to annotate locked worktrees by
> appending "locked" text to its output, however, this is not listed in
> the --porcelain format.
> 
> Teach "list --porcelain" to do the same and add a "locked" attribute
> followed by its reason, thus making both default and porcelain format
> consistent. If the locked reason is not available then only "locked"
> is shown.
> 
> The output of the "git worktree list --porcelain" becomes like so:
> 
>      $ git worktree list --porcelain
>      ...
>      worktree /path/to/locked
>      HEAD 123abcdea123abcd123acbd123acbda123abcd12
>      detached
>      locked
> 
>      worktree /path/to/locked-with-reason
>      HEAD abc123abc123abc123abc123abc123abc123abc1
>      detached
>      locked reason why it is locked
>      ...
> 
> In porcelain mode, if the lock reason contains special characters
> such as newlines, they are escaped with backslashes and the entire
> reason is enclosed in double quotes. For example:
> 
>     $ git worktree list --porcelain
>     ...
>     locked "worktree's path mounted in\nremovable device"
>     ...
> 
> Furthermore, let's update the documentation to state that some
> attributes in the porcelain format might be listed alone or together
> with its value depending whether the value is available or not. Thus
> documenting the case of the new "locked" attribute.
> 
> Helped-by: Eric Sunshine <sunshine@sunshineco.com>
> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
>   Documentation/git-worktree.txt | 16 ++++++++++++++--
>   builtin/worktree.c             | 13 +++++++++++++
>   t/t2402-worktree-list.sh       | 30 ++++++++++++++++++++++++++++++
>   3 files changed, 57 insertions(+), 2 deletions(-)
> 
> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
> index 02a706c4c0..7cb8124f28 100644
> --- a/Documentation/git-worktree.txt
> +++ b/Documentation/git-worktree.txt
> @@ -377,8 +377,10 @@ Porcelain Format
>   The porcelain format has a line per attribute.  Attributes are listed with a
>   label and value separated by a single space.  Boolean attributes (like `bare`
>   and `detached`) are listed as a label only, and are present only
> -if the value is true.  The first attribute of a working tree is always
> -`worktree`, an empty line indicates the end of the record.  For example:
> +if the value is true.  Some attributes (like `locked`) can be listed as a label
> +only or with a value depending upon whether a reason is available.  The first
> +attribute of a working tree is always `worktree`, an empty line indicates the
> +end of the record.  For example:

I think it would be helpful to document that the reasons are quoted 
according core.quotePath.

I'm not sure if it is worth changing this but I wonder if it would be 
easier to parse the output if the names of attributes with optional 
reasons were always followed by a space even when there is no reason, 
otherwise the code that parses the output has to check for the name 
followed by a space or newline. A script that only cares if the worktree 
is locked can parse the output with
l.starts_with("locked ")
rather than having to do
l.starts_with("locked ") || l == "locked\n"

Best Wishes

Phillip

>   ------------
>   $ git worktree list --porcelain
> @@ -393,6 +395,16 @@ worktree /path/to/other-linked-worktree
>   HEAD 1234abc1234abc1234abc1234abc1234abc1234a
>   detached
>   
> +worktree /path/to/linked-worktree-locked-no-reason
> +HEAD 5678abc5678abc5678abc5678abc5678abc5678c
> +branch refs/heads/locked-no-reason
> +locked
> +
> +worktree /path/to/linked-worktree-locked-with-reason
> +HEAD 3456def3456def3456def3456def3456def3456b
> +branch refs/heads/locked-with-reason
> +locked reason why is locked
> +
>   ------------
>   
>   EXAMPLES
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index df90a5acca..98177f91d4 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -12,6 +12,7 @@
>   #include "submodule.h"
>   #include "utf8.h"
>   #include "worktree.h"
> +#include "quote.h"
>   
>   static const char * const worktree_usage[] = {
>   	N_("git worktree add [<options>] <path> [<commit-ish>]"),
> @@ -569,6 +570,8 @@ static int add(int ac, const char **av, const char *prefix)
>   
>   static void show_worktree_porcelain(struct worktree *wt)
>   {
> +	const char *reason;
> +
>   	printf("worktree %s\n", wt->path);
>   	if (wt->is_bare)
>   		printf("bare\n");
> @@ -579,6 +582,16 @@ static void show_worktree_porcelain(struct worktree *wt)
>   		else if (wt->head_ref)
>   			printf("branch %s\n", wt->head_ref);
>   	}
> +
> +	reason = worktree_lock_reason(wt);
> +	if (reason && *reason) {
> +		struct strbuf sb = STRBUF_INIT;
> +		quote_c_style(reason, &sb, NULL, 0);
> +		printf("locked %s\n", sb.buf);
> +		strbuf_release(&sb);
> +	} else if (reason)
> +		printf("locked\n");
> +
>   	printf("\n");
>   }
>   
> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> index 1866ea09f6..1fe53c3309 100755
> --- a/t/t2402-worktree-list.sh
> +++ b/t/t2402-worktree-list.sh
> @@ -72,6 +72,36 @@ test_expect_success '"list" all worktrees with locked annotation' '
>   	! grep "/unlocked  *[0-9a-f].* locked$" out
>   '
>   
> +test_expect_success '"list" all worktrees --porcelain with locked' '
> +	test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
> +	echo "locked" >expect &&
> +	echo "locked with reason" >>expect &&
> +	git worktree add --detach locked1 &&
> +	git worktree add --detach locked2 &&
> +	# unlocked worktree should not be annotated with "locked"
> +	git worktree add --detach unlocked &&
> +	git worktree lock locked1 &&
> +	git worktree lock locked2 --reason "with reason" &&
> +	test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
> +	git worktree list --porcelain >out &&
> +	grep "^locked" out >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
> +	test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
> +	printf "locked \"locked\\\\r\\\\nreason\"\n" >expect &&
> +	printf "locked \"locked\\\\nreason\"\n" >>expect &&
> +	git worktree add --detach locked_lf &&
> +	git worktree add --detach locked_crlf &&
> +	git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
> +	git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
> +	test_when_finished "git worktree unlock locked_lf && git worktree unlock locked_crlf" &&
> +	git worktree list --porcelain >out &&
> +	grep "^locked" out >actual &&
> +	test_cmp expect actual
> +'
> +
>   test_expect_success 'bare repo setup' '
>   	git init --bare bare1 &&
>   	echo "data" >file1 &&
> 

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-20 11:00       ` Phillip Wood
@ 2021-01-21  3:18         ` Junio C Hamano
  2021-01-21 15:25         ` Rafael Silva
  2021-01-24  8:24         ` Eric Sunshine
  2 siblings, 0 replies; 88+ messages in thread
From: Junio C Hamano @ 2021-01-21  3:18 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Rafael Silva, git, Eric Sunshine

Phillip Wood <phillip.wood123@gmail.com> writes:

> l.starts_with("locked ")
> rather than having to do
> l.starts_with("locked ") || l == "locked\n"

l.regexp_matches("^locked[\s\n]")?

Jokes aside, isn't the "space separated list" meant to be used more
like:

    attrs = l.splitAtEach(" ")
    if "locked" in attrs:
	... yeah it is locked ...
    if "broken" in attrs:
	... ouch, it is broken ...

so I am not sure if having always a trailing whitespace is a good
idea to begin with (the last element may become an empty string if
the splitting is done naively).


^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree
  2021-01-19 21:27     ` [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree Rafael Silva
@ 2021-01-21  3:28       ` Junio C Hamano
  2021-01-21 15:09         ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Junio C Hamano @ 2021-01-21  3:28 UTC (permalink / raw)
  To: Rafael Silva; +Cc: git, Eric Sunshine, Phillip Wood

Rafael Silva <rafaeloliveira.cs@gmail.com> writes:

>  Documentation/git-worktree.txt | 26 ++++++++++++++++++++++++--
>  builtin/worktree.c             | 10 ++++++++++
>  builtin/worktree.cc            |  0
>  t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
>  4 files changed, 66 insertions(+), 2 deletions(-)
>  create mode 100644 builtin/worktree.cc

What's the h*ck is that .cc file doing ;-)

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree
  2021-01-21  3:28       ` Junio C Hamano
@ 2021-01-21 15:09         ` Rafael Silva
  2021-01-21 22:18           ` Junio C Hamano
  0 siblings, 1 reply; 88+ messages in thread
From: Rafael Silva @ 2021-01-21 15:09 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Eric Sunshine, Phillip Wood


Junio C Hamano writes:

> Rafael Silva <rafaeloliveira.cs@gmail.com> writes:
>
>>  Documentation/git-worktree.txt | 26 ++++++++++++++++++++++++--
>>  builtin/worktree.c             | 10 ++++++++++
>>  builtin/worktree.cc            |  0
>>  t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
>>  4 files changed, 66 insertions(+), 2 deletions(-)
>>  create mode 100644 builtin/worktree.cc
>
> What's the h*ck is that .cc file doing ;-)

Oops. I accidentally created and committed this file.

Re-rolling ...

Side note (joke): I wasn't trying to add C++ into Git codebase :)

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-20 11:00       ` Phillip Wood
  2021-01-21  3:18         ` Junio C Hamano
@ 2021-01-21 15:25         ` Rafael Silva
  2021-01-24  8:24         ` Eric Sunshine
  2 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-21 15:25 UTC (permalink / raw)
  To: phillip.wood; +Cc: git, Eric Sunshine


Hi Phillip,

Phillip Wood writes:

> Hi Rafael
>
> Thanks for reworking this to use c_quote_path(). I have a couple of
> comments below.
>

Thanks for reviewing this patch.

> On 19/01/2021 21:27, Rafael Silva wrote:
>> Commit c57b3367be (worktree: teach `list` to annotate locked worktree,
>> 2020-10-11) taught "git worktree list" to annotate locked worktrees by
>> appending "locked" text to its output, however, this is not listed in
>> the --porcelain format.
>> Teach "list --porcelain" to do the same and add a "locked" attribute
>> followed by its reason, thus making both default and porcelain format
>> consistent. If the locked reason is not available then only "locked"
>> is shown.
>> The output of the "git worktree list --porcelain" becomes like so:
>>      $ git worktree list --porcelain
>>      ...
>>      worktree /path/to/locked
>>      HEAD 123abcdea123abcd123acbd123acbda123abcd12
>>      detached
>>      locked
>>      worktree /path/to/locked-with-reason
>>      HEAD abc123abc123abc123abc123abc123abc123abc1
>>      detached
>>      locked reason why it is locked
>>      ...
>> In porcelain mode, if the lock reason contains special characters
>> such as newlines, they are escaped with backslashes and the entire
>> reason is enclosed in double quotes. For example:
>>     $ git worktree list --porcelain
>>     ...
>>     locked "worktree's path mounted in\nremovable device"
>>     ...
>> Furthermore, let's update the documentation to state that some
>> attributes in the porcelain format might be listed alone or together
>> with its value depending whether the value is available or not. Thus
>> documenting the case of the new "locked" attribute.
>> Helped-by: Eric Sunshine <sunshine@sunshineco.com>
>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>>   Documentation/git-worktree.txt | 16 ++++++++++++++--
>>   builtin/worktree.c             | 13 +++++++++++++
>>   t/t2402-worktree-list.sh       | 30 ++++++++++++++++++++++++++++++
>>   3 files changed, 57 insertions(+), 2 deletions(-)
>> diff --git a/Documentation/git-worktree.txt
>> b/Documentation/git-worktree.txt
>> index 02a706c4c0..7cb8124f28 100644
>> --- a/Documentation/git-worktree.txt
>> +++ b/Documentation/git-worktree.txt
>> @@ -377,8 +377,10 @@ Porcelain Format
>>   The porcelain format has a line per attribute.  Attributes are listed with a
>>   label and value separated by a single space.  Boolean attributes (like `bare`
>>   and `detached`) are listed as a label only, and are present only
>> -if the value is true.  The first attribute of a working tree is always
>> -`worktree`, an empty line indicates the end of the record.  For example:
>> +if the value is true.  Some attributes (like `locked`) can be listed as a label
>> +only or with a value depending upon whether a reason is available.  The first
>> +attribute of a working tree is always `worktree`, an empty line indicates the
>> +end of the record.  For example:
>
> I think it would be helpful to document that the reasons are quoted
> according core.quotePath.
>

Good point. I'll include this addition on the next revision.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree
  2021-01-21 15:09         ` Rafael Silva
@ 2021-01-21 22:18           ` Junio C Hamano
  0 siblings, 0 replies; 88+ messages in thread
From: Junio C Hamano @ 2021-01-21 22:18 UTC (permalink / raw)
  To: Rafael Silva; +Cc: git, Eric Sunshine, Phillip Wood

Rafael Silva <rafaeloliveira.cs@gmail.com> writes:

> Junio C Hamano writes:
>
>> Rafael Silva <rafaeloliveira.cs@gmail.com> writes:
>>
>>>  Documentation/git-worktree.txt | 26 ++++++++++++++++++++++++--
>>>  builtin/worktree.c             | 10 ++++++++++
>>>  builtin/worktree.cc            |  0
>>>  t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
>>>  4 files changed, 66 insertions(+), 2 deletions(-)
>>>  create mode 100644 builtin/worktree.cc
>>
>> What's the h*ck is that .cc file doing ;-)
>
> Oops. I accidentally created and committed this file.
>
> Re-rolling ...
>
> Side note (joke): I wasn't trying to add C++ into Git codebase :)

Yeah, I know ;-)  I've already removed the empty file in my copy,
but it seems you are getting other useful input, so it probably is a
good idea to remove it on your end, too.

Thanks.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 4/7] t2402: ensure locked worktree is properly cleaned up
  2021-01-19 21:27     ` [PATCH v3 4/7] t2402: ensure locked worktree is properly cleaned up Rafael Silva
@ 2021-01-24  7:50       ` Eric Sunshine
  2021-01-24 10:19         ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-24  7:50 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> In c57b3367be (worktree: teach `list` to annotate locked worktree,
> 2020-10-11) introduced a new test to ensure locked worktrees are listed
> with "locked" annotation. However, the test does not clean up after
> itself as "git worktree prune" is not going to remove the locked worktree
> in the first place. This not only leaves the test in an unclean state it
> also potentially breaks following tests that relies on the
> "git worktree list" output.

A couple grammos:

1) Drop "In" from the start of the first sentence.
2) s/relies/rely/

But please do not re-roll just for this. It's good enough as-is.

The patch itself is fine.

> Let's fix that by unlocking the worktree before the "prune" command.
>
> Helped-by: Eric Sunshine <sunshine@sunshineco.com>
> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-19 21:27     ` [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
  2021-01-20 11:00       ` Phillip Wood
@ 2021-01-24  8:10       ` Eric Sunshine
  2021-01-24 10:20         ` Rafael Silva
  1 sibling, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-24  8:10 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> [...]
> Teach "list --porcelain" to do the same and add a "locked" attribute
> followed by its reason, thus making both default and porcelain format
> consistent. If the locked reason is not available then only "locked"
> is shown.
> [...]
> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> @@ -72,6 +72,36 @@ test_expect_success '"list" all worktrees with locked annotation' '
> +test_expect_success '"list" all worktrees --porcelain with locked' '
> +       test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
> +       echo "locked" >expect &&
> +       echo "locked with reason" >>expect &&
> +       git worktree add --detach locked1 &&
> +       git worktree add --detach locked2 &&
> +       # unlocked worktree should not be annotated with "locked"
> +       git worktree add --detach unlocked &&
> +       git worktree lock locked1 &&
> +       git worktree lock locked2 --reason "with reason" &&
> +       test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&

There's a minor problem here. If the second `git worktree lock`
command fails, test_when_finished() will never be invoked, which means
that the first lock won't get cleaned up, thus the worktree won't get
pruned. To fix, you'd want:

    git worktree lock locked1 &&
    test_when_finished "git worktree unlock locked1" &&
    git worktree lock locked2 --reason "with reason" &&
    test_when_finished "git worktree unlock locked2" &&

> +       git worktree list --porcelain >out &&
> +       grep "^locked" out >actual &&
> +       test_cmp expect actual
> +'
> +
> +test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
> +       test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
> +       printf "locked \"locked\\\\r\\\\nreason\"\n" >expect &&
> +       printf "locked \"locked\\\\nreason\"\n" >>expect &&
> +       git worktree add --detach locked_lf &&
> +       git worktree add --detach locked_crlf &&
> +       git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
> +       git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
> +       test_when_finished "git worktree unlock locked_lf && git worktree unlock locked_crlf" &&

Same issue as above.

> +       git worktree list --porcelain >out &&
> +       grep "^locked" out >actual &&
> +       test_cmp expect actual
> +'

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-20 11:00       ` Phillip Wood
  2021-01-21  3:18         ` Junio C Hamano
  2021-01-21 15:25         ` Rafael Silva
@ 2021-01-24  8:24         ` Eric Sunshine
  2 siblings, 0 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-24  8:24 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Rafael Silva, Git List

On Wed, Jan 20, 2021 at 6:00 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> On 19/01/2021 21:27, Rafael Silva wrote:
> >   The porcelain format has a line per attribute.  Attributes are listed with a
> >   label and value separated by a single space.  Boolean attributes (like `bare`
> >   and `detached`) are listed as a label only, and are present only
> > +if the value is true.  Some attributes (like `locked`) can be listed as a label
> > +only or with a value depending upon whether a reason is available.  The first
> > +attribute of a working tree is always `worktree`, an empty line indicates the
> > +end of the record.  For example:
>
> I think it would be helpful to document that the reasons are quoted
> according core.quotePath.

Good idea.

> I'm not sure if it is worth changing this but I wonder if it would be
> easier to parse the output if the names of attributes with optional
> reasons were always followed by a space even when there is no reason,
> otherwise the code that parses the output has to check for the name
> followed by a space or newline. A script that only cares if the worktree
> is locked can parse the output with
> l.starts_with("locked ")
> rather than having to do
> l.starts_with("locked ") || l == "locked\n"

I see where you're coming from with this suggestion, though my
knee-jerk reaction is that it would be undesirable. Even after mulling
it over for a few days, I still haven't managed to convince myself
that it would be a good idea. There are a couple reasons (at least)
for my negative reaction. The primary reason is that the trailing
space is "invisible", and as such could end up being as confusing as
it is helpful for the simple parsing case (taking into consideration
that people often don't consult documentation). The second reason is
that we're already expecting clients to be able to parse C-style
quoting/escaping of the reason, so asking them to also distinguish
between a single token `locked` and a `locked reason-for-lock` seems
like very, very minor extra complexity. (It also just feels a bit
sloppy to have that trailing space, but that's a quite minor concern.)

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 7/7] worktree: teach `list` verbose mode
  2021-01-19 21:27     ` [PATCH v3 7/7] worktree: teach `list` verbose mode Rafael Silva
@ 2021-01-24  8:42       ` Eric Sunshine
  2021-01-24 10:21         ` Rafael Silva
  0 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-24  8:42 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> [...]
> Let's teach "git worktree list" a --verbose mode that outputs the reason
> why the worktrees are being annotated. The reason is a text that can take
> virtually any size and appending the text on the default columned format
> will make it difficult to extend the command with other annotations and
> not fit nicely on the screen. In order to address this shortcoming the
> annotation is then moved to the next line indented followed by the reason
> If the reason is not available the annotation stays on the same line as
> the worktree itself.
> [...]
> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
> ---
> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> @@ -134,6 +134,36 @@ test_expect_success '"list" all worktrees with prunable consistent with "prune"'
> +test_expect_success '"list" all worktrees --verbose with locked' '
> +       test_when_finished "rm -rf locked1 locked2 out actual expect && git worktree prune" &&
> +       git worktree add locked1 --detach &&
> +       git worktree add locked2 --detach &&
> +       git worktree lock locked1 &&
> +       git worktree lock locked2 --reason "with reason" &&
> +       test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&

Same minor problem here as mentioned in my review of [5/7]: If locking
of the second worktree fails then test_when_finished() won't get
invoked, so the first worktree won't get unlocked, thus won't be
pruned. To fix:

    git worktree lock locked1 &&
    test_when_finished "git worktree unlock locked1" &&
    git worktree lock locked2 --reason "with reason" &&
    test_when_finished "git worktree unlock locked2" &&

> +       echo "$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
> +       printf "\tlocked: with reason\n" >>expect &&
> +       git worktree list --verbose >out &&
> +       grep "/locked1  *[0-9a-f].* locked$" out &&
> +       sed -n "s/  */ /g;/\/locked2  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
> +       test_cmp actual expect
> +'

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
                       ` (6 preceding siblings ...)
  2021-01-19 21:27     ` [PATCH v3 7/7] worktree: teach `list` verbose mode Rafael Silva
@ 2021-01-24  8:51     ` Eric Sunshine
  2021-01-27  8:08       ` Rafael Silva
  2021-01-27  8:03     ` Rafael Silva
  8 siblings, 1 reply; 88+ messages in thread
From: Eric Sunshine @ 2021-01-24  8:51 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood

On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Changes in v3
> =============
> v3 is 1-patch bigger than v2 and it includes the following changes:

Thanks. I gave v3 a complete read-through, and it is just about perfect.

Aside from a very minor problem with the tests in [5/7] and [7/7] and
the documentation enhancement (`core.quotePath`) suggested by Phillip,
this is really, really close. (In fact, I would be happy to accept v3
as-is; those minor changes could easily be done on top.)

I look forward to giving v4 my Reviewed-by:.

> Range-diff against v2:
> -:  ---------- > 1:  66cd83ba42 worktree: libify should_prune_worktree()
> 1:  9d47fcb4a4 ! 2:  81f4824028 worktree: teach worktree to lazy-load "prunable" reason

The range-diff seems slightly off. The "libify
should_prune_worktree()" patch existed in v1 and v2, but the
range-diff suggests that it's new with v3. You probably just need a
minor adjustment to your `git format-patch --range-diff` invocation.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 4/7] t2402: ensure locked worktree is properly cleaned up
  2021-01-24  7:50       ` Eric Sunshine
@ 2021-01-24 10:19         ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-24 10:19 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Eric Sunshine writes:

> On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> In c57b3367be (worktree: teach `list` to annotate locked worktree,
>> 2020-10-11) introduced a new test to ensure locked worktrees are listed
>> with "locked" annotation. However, the test does not clean up after
>> itself as "git worktree prune" is not going to remove the locked worktree
>> in the first place. This not only leaves the test in an unclean state it
>> also potentially breaks following tests that relies on the
>> "git worktree list" output.
>
> A couple grammos:
>
> 1) Drop "In" from the start of the first sentence.
> 2) s/relies/rely/
>
> But please do not re-roll just for this. It's good enough as-is.
>
> The patch itself is fine.
>

Nice catch. I'll be re-rolling the series with the documentation addition
suggested by Phillip and the test fixes suggested by you. So, I'll
include these changes as well.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-24  8:10       ` Eric Sunshine
@ 2021-01-24 10:20         ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-24 10:20 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Eric Sunshine writes:

> On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> [...]
>> Teach "list --porcelain" to do the same and add a "locked" attribute
>> followed by its reason, thus making both default and porcelain format
>> consistent. If the locked reason is not available then only "locked"
>> is shown.
>> [...]
>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
>> @@ -72,6 +72,36 @@ test_expect_success '"list" all worktrees with locked annotation' '
>> +test_expect_success '"list" all worktrees --porcelain with locked' '
>> +       test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
>> +       echo "locked" >expect &&
>> +       echo "locked with reason" >>expect &&
>> +       git worktree add --detach locked1 &&
>> +       git worktree add --detach locked2 &&
>> +       # unlocked worktree should not be annotated with "locked"
>> +       git worktree add --detach unlocked &&
>> +       git worktree lock locked1 &&
>> +       git worktree lock locked2 --reason "with reason" &&
>> +       test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
>
> There's a minor problem here. If the second `git worktree lock`
> command fails, test_when_finished() will never be invoked, which means
> that the first lock won't get cleaned up, thus the worktree won't get
> pruned. To fix, you'd want:
>
>     git worktree lock locked1 &&
>     test_when_finished "git worktree unlock locked1" &&
>     git worktree lock locked2 --reason "with reason" &&
>     test_when_finished "git worktree unlock locked2" &&
>

Excellent point. This case didn't occur to me when I was working on v3, I
will make this change in the next revision.

>> +       git worktree list --porcelain >out &&
>> +       grep "^locked" out >actual &&
>> +       test_cmp expect actual
>> +'
>> +
>> +test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
>> +       test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
>> +       printf "locked \"locked\\\\r\\\\nreason\"\n" >expect &&
>> +       printf "locked \"locked\\\\nreason\"\n" >>expect &&
>> +       git worktree add --detach locked_lf &&
>> +       git worktree add --detach locked_crlf &&
>> +       git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
>> +       git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
>> +       test_when_finished "git worktree unlock locked_lf && git worktree unlock locked_crlf" &&
>
> Same issue as above.
>
>> +       git worktree list --porcelain >out &&
>> +       grep "^locked" out >actual &&
>> +       test_cmp expect actual
>> +'


-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 7/7] worktree: teach `list` verbose mode
  2021-01-24  8:42       ` Eric Sunshine
@ 2021-01-24 10:21         ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-24 10:21 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Eric Sunshine writes:

> On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> [...]
>> Let's teach "git worktree list" a --verbose mode that outputs the reason
>> why the worktrees are being annotated. The reason is a text that can take
>> virtually any size and appending the text on the default columned format
>> will make it difficult to extend the command with other annotations and
>> not fit nicely on the screen. In order to address this shortcoming the
>> annotation is then moved to the next line indented followed by the reason
>> If the reason is not available the annotation stays on the same line as
>> the worktree itself.
>> [...]
>> Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
>> ---
>> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
>> @@ -134,6 +134,36 @@ test_expect_success '"list" all worktrees with prunable consistent with "prune"'
>> +test_expect_success '"list" all worktrees --verbose with locked' '
>> +       test_when_finished "rm -rf locked1 locked2 out actual expect && git worktree prune" &&
>> +       git worktree add locked1 --detach &&
>> +       git worktree add locked2 --detach &&
>> +       git worktree lock locked1 &&
>> +       git worktree lock locked2 --reason "with reason" &&
>> +       test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
>
> Same minor problem here as mentioned in my review of [5/7]: If locking
> of the second worktree fails then test_when_finished() won't get
> invoked, so the first worktree won't get unlocked, thus won't be
> pruned. To fix:
>
>     git worktree lock locked1 &&
>     test_when_finished "git worktree unlock locked1" &&
>     git worktree lock locked2 --reason "with reason" &&
>     test_when_finished "git worktree unlock locked2" &&
>

Make sense. Thank you.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
                       ` (7 preceding siblings ...)
  2021-01-24  8:51     ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
@ 2021-01-27  8:03     ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 1/7] worktree: libify should_prune_worktree() Rafael Silva
                         ` (7 more replies)
  8 siblings, 8 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

In c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) we taught `git worktree list` to annotate working trees that
is locked by appending "locked" text in order to signalize to the user
that a working tree is locked.  During the review, there was some
discussion about additional annotations and information that `list`
command could provide to the user that has long been envisioned and
mentioned in [2], [3] and [4].

This patch series addresses some of these changes by teaching
`worktree list` to show "prunable" annotation, adding verbose mode and
extending the --porcelain format with prunable and locked annotation as
follow up from [1]. Additionally, it addresses one shortcoming for porcelain
format to escape any newline characters (LF or CRLF) and quote the lock
reason to prevent breaking the format that is mentioned in [4] and [1] during
the review cycle.

This patch series includes:

1. The first patch moves the should_prune_worktree() machinery to the top-level
   worktree.c exposing the function as general API, that will be reference
   by should_prune_worktree() wrapper implemented on the second patch.

   The original idea was to not only move should_prune_worktree() but also
   refactor to accept a "struct worktree" and load the information directly,
   which allows simplify the `prune` command by reusing get_worktrees().
   However this seems to also require refactoring get_worktrees() itself
   to return "non-valid" working trees that can/should be pruned. This is
   also mentioned in [5]. Having the wrapper function makes it easier to add
   the prunable annotation without touching the get_worktrees() and the
   other worktree sub commands. The refactoring can be addressed in a
   future patch, if this turns out to be good idea. One possible approach
   is to teach get_worktrees() to take additional flags that will tell
   whether to return only valid or all worktrees in GIT_DIR/worktrees/
   directory and address its own possible shortcoming.

2. Introduces the worktree_prune_reason() to discovery whether a worktree
   is prunable along with two new fields in the `worktree` structure. The
   function mimics the workree_lock_reason() API.

3. The third patch changes worktree_lock_reason() to be more gentle for
   the main working tree to simply returning NULL instead of aborting the
   program via assert() macro. This allow us to simplify the code that
   checks if the working tree is locked for default and porcelain format.
   This changes is also mentioned in [6].

4. Fix t2402 added in [1] to ensure the locked worktree is properly cleaned up.

5. The fourth patch adds the "locked" attribute for the porcelain format
   in order to make both default and --porcelain format consistent.

6. The fifth patch introduces "prunable" annotation for both default
   and --porcelain format.

7. The sixth patch introduces verbose mode to expand the `list` default
   format and show each annotation reason when its available.

Series is built on top of 66e871b664 (The third batch, 2021-01-15).

[1] https://lore.kernel.org/git/20200928154953.30396-1-rafaeloliveira.cs@gmail.com/
[2] https://lore.kernel.org/git/CAPig+cQF6V8HNdMX5AZbmz3_w2WhSfA4SFfNhQqxXBqPXTZL+w@mail.gmail.com/
[3] https://lore.kernel.org/git/CAPig+cSGXqJuaZPhUhOVX5X=LMrjVfv8ye_6ncMUbyKox1i7QA@mail.gmail.com/
[4] https://lore.kernel.org/git/CAPig+cTitWCs5vB=0iXuUyEY22c0gvjXvY1ZtTT90s74ydhE=A@mail.gmail.com/
[5] https://lore.kernel.org/git/CACsJy8ChM99n6skQCv-GmFiod19mnwwH4j-6R+cfZSiVFAxjgA@mail.gmail.com/
[6] https://lore.kernel.org/git/xmqq8sctlgzx.fsf@gitster.c.googlers.com/

Changes in v4
=============

Thanks Eric Sunshine and Phillip Wood for the review and suggestions.

 * Added documentation to explain that the lock reason is quoted with
   the same rules as described for `core.quotePath`.

 * The `worktree unlock` issued in the test cleanup is splitted and
   executed after each `worktree lock` to ensure the unlock is only
   done after we know each locked command was successful.  

 * Fix a couple of grammos in the [4/7] commit message.

Changes in v3
=============

v3 is 1-patch bigger than v2 and it includes the changes:

 * Dropped CQUOTE_NODQ flag in the the locked reason to return a string
   enclosed with double quotes if the text reason contains especial
   characters, like newlines.

 * fix in t2402 to ensure the locked worktree is properly cleaned up,
   is move to its own patch.

 * In worktree_prune_reason(), the `path` variable is initialize
   with NULL to make the code easier to follow.

 * The removal of `is_main_worktree()` before `worktree_lock_reason()`
   is moved to the patch that actually changes the API to be more gentle
   with the main worktree instead of refactoring the code in the next
   patches that adds the annotations.

 * Drop the `*reason` in `(reason && *reason)` as we know that
   worktree_prune_reason() is either going to return a non-empty string
   or NULL which makes the code easier to follow.

 * The --verbose test for the locked annotation now tests that "locked"
   annotation stays on the same line when there is no locked reason.

 * Small documentation updates and make the "git worktree --verbose"
   example a little consistent with the worktree path.

Changes in v2
=============

v2 changes considerably from v1 taking into account several comments
suggested by Eric Sunshine and Phillip Wood (thanks guys for the
insightful comments and patience reviewing my patches :) ).

 * The biggest change are the way the series is organized. In v1 all the
   code was implemented in the fourth patch, all the tests and documentation
   updates was added in sixth and seventh patch respectfully, in v2 each
   patch introduces the annotations and verbose mode together with theirs
   respective test and documentation updates.

 * Several rewrite of the commit messages

 * In v1 the fifth patch was introducing a new function to escape newline
   characters for the "locked" attribute. However, we already have this
   feature from the quote.h API. So the new function is dropped and the
   changes are squashed into v2's fourth patch.

 * The new prunable annotation and locked annotation that was introduced
   by [1] was refactor to not poke around the worktree private fields.

 * Refactoring of the worktree_prune_reason() to cache the prune_reason
   and refactor to early return the use cases following the more common
   pattern used on Git's codebase.

 * Few documentation rewrites, most notably the `--verbose` and `--expire`
   doc sentences for the list command are moved to its own line to clearly
   separate the description from the others commands instead of continuing 
   on the same paragraph.

 * The `git unlock <worktree>` used in the test's cleanup is moved to after
   we know the `git worktree locked` is executed successfully. This ensures
   the unlock doesn't error in the cleanup stage which will make it harder
   to debug the tests.

Rafael Silva (7):
  worktree: libify should_prune_worktree()
  worktree: teach worktree to lazy-load "prunable" reason
  worktree: teach worktree_lock_reason() to gently handle main worktree
  t2402: ensure locked worktree is properly cleaned up
  worktree: teach `list --porcelain` to annotate locked worktree
  worktree: teach `list` to annotate prunable worktree
  worktree: teach `list` verbose mode

 Documentation/git-worktree.txt |  74 ++++++++++++++++++++--
 builtin/worktree.c             | 110 +++++++++++----------------------
 t/t2402-worktree-list.sh       |  96 ++++++++++++++++++++++++++++
 worktree.c                     |  91 ++++++++++++++++++++++++++-
 worktree.h                     |  23 +++++++
 5 files changed, 314 insertions(+), 80 deletions(-)

Range-diff against v3:
1:  fc4252c129 = 1:  66cd83ba42 worktree: libify should_prune_worktree()
2:  664d84155b = 2:  81f4824028 worktree: teach worktree to lazy-load "prunable" reason
3:  cf334da2f0 = 3:  c62ecf60d6 worktree: teach worktree_lock_reason() to gently handle main worktree
4:  ecb11793d9 ! 4:  d2ea467927 t2402: ensure locked worktree is properly cleaned up
    @@ Metadata
      ## Commit message ##
         t2402: ensure locked worktree is properly cleaned up
     
    -    In c57b3367be (worktree: teach `list` to annotate locked worktree,
    +    c57b3367be (worktree: teach `list` to annotate locked worktree,
         2020-10-11) introduced a new test to ensure locked worktrees are listed
         with "locked" annotation. However, the test does not clean up after
         itself as "git worktree prune" is not going to remove the locked worktree
         in the first place. This not only leaves the test in an unclean state it
    -    also potentially breaks following tests that relies on the
    +    also potentially breaks following tests that rely on the
         "git worktree list" output.
     
         Let's fix that by unlocking the worktree before the "prune" command.
5:  1fb68562f6 ! 5:  1b7661a0b3 worktree: teach `list --porcelain` to annotate locked worktree
    @@ Commit message
         with its value depending whether the value is available or not. Thus
         documenting the case of the new "locked" attribute.
     
    +    Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
         Helped-by: Eric Sunshine <sunshine@sunshineco.com>
         Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
     
    @@ Documentation/git-worktree.txt: worktree /path/to/other-linked-worktree
     +branch refs/heads/locked-with-reason
     +locked reason why is locked
     +
    ++------------
    ++
    ++If the lock reason contains "unusual" characters such as newline, they
    ++are escaped and the entire reason is quoted as explained for the
    ++configuration variable `core.quotePath` (see linkgit:git-config[1]).
    ++For Example:
    ++
    ++------------
    ++$ git worktree list --porcelain
    ++...
    ++locked "reason\nwhy is locked"
    ++...
      ------------
      
      EXAMPLES
    @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees with locked
     +	# unlocked worktree should not be annotated with "locked"
     +	git worktree add --detach unlocked &&
     +	git worktree lock locked1 &&
    ++	test_when_finished "git worktree unlock locked1" &&
     +	git worktree lock locked2 --reason "with reason" &&
    -+	test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
    ++	test_when_finished "git worktree unlock locked2" &&
     +	git worktree list --porcelain >out &&
     +	grep "^locked" out >actual &&
     +	test_cmp expect actual
    @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees with locked
     +	git worktree add --detach locked_lf &&
     +	git worktree add --detach locked_crlf &&
     +	git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
    ++	test_when_finished "git worktree unlock locked_lf" &&
     +	git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
    -+	test_when_finished "git worktree unlock locked_lf && git worktree unlock locked_crlf" &&
    ++	test_when_finished "git worktree unlock locked_crlf" &&
     +	git worktree list --porcelain >out &&
     +	grep "^locked" out >actual &&
     +	test_cmp expect actual
6:  834584f826 = 6:  c29ad7ffb1 worktree: teach `list` to annotate prunable worktree
7:  35ad456fbb ! 7:  7938950d04 worktree: teach `list` verbose mode
    @@ t/t2402-worktree-list.sh: test_expect_success '"list" all worktrees with prunabl
     +	git worktree add locked1 --detach &&
     +	git worktree add locked2 --detach &&
     +	git worktree lock locked1 &&
    ++	test_when_finished "git worktree unlock locked1" &&
     +	git worktree lock locked2 --reason "with reason" &&
    -+	test_when_finished "git worktree unlock locked1 && git worktree unlock locked2" &&
    ++	test_when_finished "git worktree unlock locked2" &&
     +	echo "$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
     +	printf "\tlocked: with reason\n" >>expect &&
     +	git worktree list --verbose >out &&
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply	[flat|nested] 88+ messages in thread

* [PATCH v4 1/7] worktree: libify should_prune_worktree()
  2021-01-27  8:03     ` Rafael Silva
@ 2021-01-27  8:03       ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 2/7] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
                         ` (6 subsequent siblings)
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

As part of teaching "git worktree list" to annotate worktree that is a
candidate for pruning, let's move should_prune_worktree() from
builtin/worktree.c to worktree.c in order to make part of the worktree
public API.

should_prune_worktree() knows how to select the given worktree for
pruning based on an expiration date, however the expiration value is
stored in a static file-scope variable and it is not local to the
function. In order to move the function, teach should_prune_worktree()
to take the expiration date as an argument and document the new
parameter that is not immediately obvious.

Also, change the function comment to clearly state that the worktree's
path is returned in `wtpath` argument.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 builtin/worktree.c | 75 +---------------------------------------------
 worktree.c         | 68 +++++++++++++++++++++++++++++++++++++++++
 worktree.h         | 14 +++++++++
 3 files changed, 83 insertions(+), 74 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index 71287b2da6..dd886d5029 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -67,79 +67,6 @@ static void delete_worktrees_dir_if_empty(void)
 	rmdir(git_path("worktrees")); /* ignore failed removal */
 }
 
-/*
- * Return true if worktree entry should be pruned, along with the reason for
- * pruning. Otherwise, return false and the worktree's path, or NULL if it
- * cannot be determined. Caller is responsible for freeing returned path.
- */
-static int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath)
-{
-	struct stat st;
-	char *path;
-	int fd;
-	size_t len;
-	ssize_t read_result;
-
-	*wtpath = NULL;
-	if (!is_directory(git_path("worktrees/%s", id))) {
-		strbuf_addstr(reason, _("not a valid directory"));
-		return 1;
-	}
-	if (file_exists(git_path("worktrees/%s/locked", id)))
-		return 0;
-	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
-		strbuf_addstr(reason, _("gitdir file does not exist"));
-		return 1;
-	}
-	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
-	if (fd < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		return 1;
-	}
-	len = xsize_t(st.st_size);
-	path = xmallocz(len);
-
-	read_result = read_in_full(fd, path, len);
-	if (read_result < 0) {
-		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
-			    strerror(errno));
-		close(fd);
-		free(path);
-		return 1;
-	}
-	close(fd);
-
-	if (read_result != len) {
-		strbuf_addf(reason,
-			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
-			    (uintmax_t)len, (uintmax_t)read_result);
-		free(path);
-		return 1;
-	}
-	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
-		len--;
-	if (!len) {
-		strbuf_addstr(reason, _("invalid gitdir file"));
-		free(path);
-		return 1;
-	}
-	path[len] = '\0';
-	if (!file_exists(path)) {
-		if (stat(git_path("worktrees/%s/index", id), &st) ||
-		    st.st_mtime <= expire) {
-			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
-			free(path);
-			return 1;
-		} else {
-			*wtpath = path;
-			return 0;
-		}
-	}
-	*wtpath = path;
-	return 0;
-}
-
 static void prune_worktree(const char *id, const char *reason)
 {
 	if (show_only || verbose)
@@ -195,7 +122,7 @@ static void prune_worktrees(void)
 		if (is_dot_or_dotdot(d->d_name))
 			continue;
 		strbuf_reset(&reason);
-		if (should_prune_worktree(d->d_name, &reason, &path))
+		if (should_prune_worktree(d->d_name, &reason, &path, expire))
 			prune_worktree(d->d_name, reason.buf);
 		else if (path)
 			string_list_append(&kept, path)->util = xstrdup(d->d_name);
diff --git a/worktree.c b/worktree.c
index 821b233479..8ae019af79 100644
--- a/worktree.c
+++ b/worktree.c
@@ -741,3 +741,71 @@ void repair_worktree_at_path(const char *path,
 	strbuf_release(&realdotgit);
 	strbuf_release(&dotgit);
 }
+
+int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
+{
+	struct stat st;
+	char *path;
+	int fd;
+	size_t len;
+	ssize_t read_result;
+
+	*wtpath = NULL;
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addstr(reason, _("not a valid directory"));
+		return 1;
+	}
+	if (file_exists(git_path("worktrees/%s/locked", id)))
+		return 0;
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addstr(reason, _("gitdir file does not exist"));
+		return 1;
+	}
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		return 1;
+	}
+	len = xsize_t(st.st_size);
+	path = xmallocz(len);
+
+	read_result = read_in_full(fd, path, len);
+	if (read_result < 0) {
+		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
+			    strerror(errno));
+		close(fd);
+		free(path);
+		return 1;
+	}
+	close(fd);
+
+	if (read_result != len) {
+		strbuf_addf(reason,
+			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
+			    (uintmax_t)len, (uintmax_t)read_result);
+		free(path);
+		return 1;
+	}
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addstr(reason, _("invalid gitdir file"));
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		if (stat(git_path("worktrees/%s/index", id), &st) ||
+		    st.st_mtime <= expire) {
+			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
+			free(path);
+			return 1;
+		} else {
+			*wtpath = path;
+			return 0;
+		}
+	}
+	*wtpath = path;
+	return 0;
+}
diff --git a/worktree.h b/worktree.h
index f38e6fd5a2..818e1491c7 100644
--- a/worktree.h
+++ b/worktree.h
@@ -73,6 +73,20 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return true if worktree entry should be pruned, along with the reason for
+ * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
+ * NULL if it cannot be determined. Caller is responsible for freeing
+ * returned path.
+ *
+ * `expire` defines a grace period to prune the worktree when its path
+ * does not exist.
+ */
+int should_prune_worktree(const char *id,
+			  struct strbuf *reason,
+			  char **wtpath,
+			  timestamp_t expire);
+
 #define WT_VALIDATE_WORKTREE_MISSING_OK (1 << 0)
 
 /*
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v4 2/7] worktree: teach worktree to lazy-load "prunable" reason
  2021-01-27  8:03     ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 1/7] worktree: libify should_prune_worktree() Rafael Silva
@ 2021-01-27  8:03       ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
                         ` (5 subsequent siblings)
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

Add worktree_prune_reason() to allow a caller to discover whether a
worktree is prunable and the reason that it is, much like
worktree_lock_reason() indicates whether a worktree is locked and the
reason for the lock. As with worktree_lock_reason(), retrieve the
prunable reason lazily and cache it in the `worktree` structure.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 worktree.c | 20 ++++++++++++++++++++
 worktree.h |  9 +++++++++
 2 files changed, 29 insertions(+)

diff --git a/worktree.c b/worktree.c
index 8ae019af79..fb3e286996 100644
--- a/worktree.c
+++ b/worktree.c
@@ -15,6 +15,7 @@ void free_worktrees(struct worktree **worktrees)
 		free(worktrees[i]->id);
 		free(worktrees[i]->head_ref);
 		free(worktrees[i]->lock_reason);
+		free(worktrees[i]->prune_reason);
 		free(worktrees[i]);
 	}
 	free (worktrees);
@@ -245,6 +246,25 @@ const char *worktree_lock_reason(struct worktree *wt)
 	return wt->lock_reason;
 }
 
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
+{
+	struct strbuf reason = STRBUF_INIT;
+	char *path = NULL;
+
+	if (is_main_worktree(wt))
+		return NULL;
+	if (wt->prune_reason_valid)
+		return wt->prune_reason;
+
+	if (should_prune_worktree(wt->id, &reason, &path, expire))
+		wt->prune_reason = strbuf_detach(&reason, NULL);
+	wt->prune_reason_valid = 1;
+
+	strbuf_release(&reason);
+	free(path);
+	return wt->prune_reason;
+}
+
 /* convenient wrapper to deal with NULL strbuf */
 static void strbuf_addf_gently(struct strbuf *buf, const char *fmt, ...)
 {
diff --git a/worktree.h b/worktree.h
index 818e1491c7..8b7c408132 100644
--- a/worktree.h
+++ b/worktree.h
@@ -11,11 +11,13 @@ struct worktree {
 	char *id;
 	char *head_ref;		/* NULL if HEAD is broken or detached */
 	char *lock_reason;	/* private - use worktree_lock_reason */
+	char *prune_reason;     /* private - use worktree_prune_reason */
 	struct object_id head_oid;
 	int is_detached;
 	int is_bare;
 	int is_current;
 	int lock_reason_valid; /* private */
+	int prune_reason_valid; /* private */
 };
 
 /*
@@ -73,6 +75,13 @@ int is_main_worktree(const struct worktree *wt);
  */
 const char *worktree_lock_reason(struct worktree *wt);
 
+/*
+ * Return the reason string if the given worktree should be pruned, otherwise
+ * NULL if it should not be pruned. `expire` defines a grace period to prune
+ * the worktree when its path does not exist.
+ */
+const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
+
 /*
  * Return true if worktree entry should be pruned, along with the reason for
  * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v4 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree
  2021-01-27  8:03     ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 1/7] worktree: libify should_prune_worktree() Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 2/7] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
@ 2021-01-27  8:03       ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 4/7] t2402: ensure locked worktree is properly cleaned up Rafael Silva
                         ` (4 subsequent siblings)
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

worktree_lock_reason() aborts with an assertion failure when called on
the main worktree since locking the main worktree is nonsensical. Not
only is this behavior undocumented, thus callers might not even be aware
that the call could potentially crash the program, but it also forces
clients to be extra careful:

    if (!is_main_worktree(wt) && worktree_locked_reason(...))
        ...

Since we know that locking makes no sense in the context of the main
worktree, we can simply return false for the main worktree, thus making
client code less complex by eliminating the need for the callers to have
inside knowledge about the implementation:

    if (worktree_lock_reason(...))
        ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 builtin/worktree.c | 2 +-
 worktree.c         | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/builtin/worktree.c b/builtin/worktree.c
index dd886d5029..df90a5acca 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -604,7 +604,7 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 			strbuf_addstr(&sb, "(error)");
 	}
 
-	if (!is_main_worktree(wt) && worktree_lock_reason(wt))
+	if (worktree_lock_reason(wt))
 		strbuf_addstr(&sb, " locked");
 
 	printf("%s\n", sb.buf);
diff --git a/worktree.c b/worktree.c
index fb3e286996..e00858540e 100644
--- a/worktree.c
+++ b/worktree.c
@@ -225,7 +225,8 @@ int is_main_worktree(const struct worktree *wt)
 
 const char *worktree_lock_reason(struct worktree *wt)
 {
-	assert(!is_main_worktree(wt));
+	if (is_main_worktree(wt))
+		return NULL;
 
 	if (!wt->lock_reason_valid) {
 		struct strbuf path = STRBUF_INIT;
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v4 4/7] t2402: ensure locked worktree is properly cleaned up
  2021-01-27  8:03     ` Rafael Silva
                         ` (2 preceding siblings ...)
  2021-01-27  8:03       ` [PATCH v4 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
@ 2021-01-27  8:03       ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
                         ` (3 subsequent siblings)
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) introduced a new test to ensure locked worktrees are listed
with "locked" annotation. However, the test does not clean up after
itself as "git worktree prune" is not going to remove the locked worktree
in the first place. This not only leaves the test in an unclean state it
also potentially breaks following tests that rely on the
"git worktree list" output.

Let's fix that by unlocking the worktree before the "prune" command.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 t/t2402-worktree-list.sh | 1 +
 1 file changed, 1 insertion(+)

diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 795ddca2e4..1866ea09f6 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -66,6 +66,7 @@ test_expect_success '"list" all worktrees with locked annotation' '
 	git worktree add --detach locked master &&
 	git worktree add --detach unlocked master &&
 	git worktree lock locked &&
+	test_when_finished "git worktree unlock locked" &&
 	git worktree list >out &&
 	grep "/locked  *[0-9a-f].* locked$" out &&
 	! grep "/unlocked  *[0-9a-f].* locked$" out
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v4 5/7] worktree: teach `list --porcelain` to annotate locked worktree
  2021-01-27  8:03     ` Rafael Silva
                         ` (3 preceding siblings ...)
  2021-01-27  8:03       ` [PATCH v4 4/7] t2402: ensure locked worktree is properly cleaned up Rafael Silva
@ 2021-01-27  8:03       ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 6/7] worktree: teach `list` to annotate prunable worktree Rafael Silva
                         ` (2 subsequent siblings)
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

Commit c57b3367be (worktree: teach `list` to annotate locked worktree,
2020-10-11) taught "git worktree list" to annotate locked worktrees by
appending "locked" text to its output, however, this is not listed in
the --porcelain format.

Teach "list --porcelain" to do the same and add a "locked" attribute
followed by its reason, thus making both default and porcelain format
consistent. If the locked reason is not available then only "locked"
is shown.

The output of the "git worktree list --porcelain" becomes like so:

    $ git worktree list --porcelain
    ...
    worktree /path/to/locked
    HEAD 123abcdea123abcd123acbd123acbda123abcd12
    detached
    locked

    worktree /path/to/locked-with-reason
    HEAD abc123abc123abc123abc123abc123abc123abc1
    detached
    locked reason why it is locked
    ...

In porcelain mode, if the lock reason contains special characters
such as newlines, they are escaped with backslashes and the entire
reason is enclosed in double quotes. For example:

   $ git worktree list --porcelain
   ...
   locked "worktree's path mounted in\nremovable device"
   ...

Furthermore, let's update the documentation to state that some
attributes in the porcelain format might be listed alone or together
with its value depending whether the value is available or not. Thus
documenting the case of the new "locked" attribute.

Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 28 ++++++++++++++++++++++++++--
 builtin/worktree.c             | 13 +++++++++++++
 t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 02a706c4c0..426e9b4f76 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -377,8 +377,10 @@ Porcelain Format
 The porcelain format has a line per attribute.  Attributes are listed with a
 label and value separated by a single space.  Boolean attributes (like `bare`
 and `detached`) are listed as a label only, and are present only
-if the value is true.  The first attribute of a working tree is always
-`worktree`, an empty line indicates the end of the record.  For example:
+if the value is true.  Some attributes (like `locked`) can be listed as a label
+only or with a value depending upon whether a reason is available.  The first
+attribute of a working tree is always `worktree`, an empty line indicates the
+end of the record.  For example:
 
 ------------
 $ git worktree list --porcelain
@@ -393,6 +395,28 @@ worktree /path/to/other-linked-worktree
 HEAD 1234abc1234abc1234abc1234abc1234abc1234a
 detached
 
+worktree /path/to/linked-worktree-locked-no-reason
+HEAD 5678abc5678abc5678abc5678abc5678abc5678c
+branch refs/heads/locked-no-reason
+locked
+
+worktree /path/to/linked-worktree-locked-with-reason
+HEAD 3456def3456def3456def3456def3456def3456b
+branch refs/heads/locked-with-reason
+locked reason why is locked
+
+------------
+
+If the lock reason contains "unusual" characters such as newline, they
+are escaped and the entire reason is quoted as explained for the
+configuration variable `core.quotePath` (see linkgit:git-config[1]).
+For Example:
+
+------------
+$ git worktree list --porcelain
+...
+locked "reason\nwhy is locked"
+...
 ------------
 
 EXAMPLES
diff --git a/builtin/worktree.c b/builtin/worktree.c
index df90a5acca..98177f91d4 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -12,6 +12,7 @@
 #include "submodule.h"
 #include "utf8.h"
 #include "worktree.h"
+#include "quote.h"
 
 static const char * const worktree_usage[] = {
 	N_("git worktree add [<options>] <path> [<commit-ish>]"),
@@ -569,6 +570,8 @@ static int add(int ac, const char **av, const char *prefix)
 
 static void show_worktree_porcelain(struct worktree *wt)
 {
+	const char *reason;
+
 	printf("worktree %s\n", wt->path);
 	if (wt->is_bare)
 		printf("bare\n");
@@ -579,6 +582,16 @@ static void show_worktree_porcelain(struct worktree *wt)
 		else if (wt->head_ref)
 			printf("branch %s\n", wt->head_ref);
 	}
+
+	reason = worktree_lock_reason(wt);
+	if (reason && *reason) {
+		struct strbuf sb = STRBUF_INIT;
+		quote_c_style(reason, &sb, NULL, 0);
+		printf("locked %s\n", sb.buf);
+		strbuf_release(&sb);
+	} else if (reason)
+		printf("locked\n");
+
 	printf("\n");
 }
 
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 1866ea09f6..39596e4aa9 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -72,6 +72,38 @@ test_expect_success '"list" all worktrees with locked annotation' '
 	! grep "/unlocked  *[0-9a-f].* locked$" out
 '
 
+test_expect_success '"list" all worktrees --porcelain with locked' '
+	test_when_finished "rm -rf locked1 locked2 unlocked out actual expect && git worktree prune" &&
+	echo "locked" >expect &&
+	echo "locked with reason" >>expect &&
+	git worktree add --detach locked1 &&
+	git worktree add --detach locked2 &&
+	# unlocked worktree should not be annotated with "locked"
+	git worktree add --detach unlocked &&
+	git worktree lock locked1 &&
+	test_when_finished "git worktree unlock locked1" &&
+	git worktree lock locked2 --reason "with reason" &&
+	test_when_finished "git worktree unlock locked2" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '"list" all worktrees --porcelain with locked reason newline escaped' '
+	test_when_finished "rm -rf locked_lf locked_crlf out actual expect && git worktree prune" &&
+	printf "locked \"locked\\\\r\\\\nreason\"\n" >expect &&
+	printf "locked \"locked\\\\nreason\"\n" >>expect &&
+	git worktree add --detach locked_lf &&
+	git worktree add --detach locked_crlf &&
+	git worktree lock locked_lf --reason "$(printf "locked\nreason")" &&
+	test_when_finished "git worktree unlock locked_lf" &&
+	git worktree lock locked_crlf --reason "$(printf "locked\r\nreason")" &&
+	test_when_finished "git worktree unlock locked_crlf" &&
+	git worktree list --porcelain >out &&
+	grep "^locked" out >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v4 6/7] worktree: teach `list` to annotate prunable worktree
  2021-01-27  8:03     ` Rafael Silva
                         ` (4 preceding siblings ...)
  2021-01-27  8:03       ` [PATCH v4 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
@ 2021-01-27  8:03       ` Rafael Silva
  2021-01-27  8:03       ` [PATCH v4 7/7] worktree: teach `list` verbose mode Rafael Silva
  2021-01-30  7:04       ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

The "git worktree list" command shows the absolute path to the worktree,
the commit that is checked out, the name of the branch, and a "locked"
annotation if the worktree is locked, however, it does not indicate
whether the worktree is prunable.

The "prune" command will remove a worktree if it is prunable unless
`--dry-run` option is specified. This could lead to a worktree being
removed without the user realizing before it is too late, in case the
user forgets to pass --dry-run for instance. If the "list" command shows
which worktree is prunable, the user could verify before running
"git worktree prune" and hopefully prevents the working tree to be
removed "accidentally" on the worse case scenario.

Let's teach "git worktree list" to show when a worktree is a prunable
candidate for both default and porcelain format.

In the default format a "prunable" text is appended:

    $ git worktree list
    /path/to/main      aba123 [main]
    /path/to/linked    123abc [branch-a]
    /path/to/prunable  ace127 (detached HEAD) prunable

In the --porcelain format a prunable label is added followed by
its reason:

    $ git worktree list --porcelain
    ...
    worktree /path/to/prunable
    HEAD abc1234abc1234abc1234abc1234abc1234abc12
    detached
    prunable gitdir file points to non-existent location
    ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 26 ++++++++++++++++++++++++--
 builtin/worktree.c             | 10 ++++++++++
 t/t2402-worktree-list.sh       | 32 ++++++++++++++++++++++++++++++++
 3 files changed, 66 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 426e9b4f76..240c3fd76b 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -97,8 +97,9 @@ list::
 List details of each working tree.  The main working tree is listed first,
 followed by each of the linked working trees.  The output details include
 whether the working tree is bare, the revision currently checked out, the
-branch currently checked out (or "detached HEAD" if none), and "locked" if
-the worktree is locked.
+branch currently checked out (or "detached HEAD" if none), "locked" if
+the worktree is locked, "prunable" if the worktree can be pruned by `prune`
+command.
 
 lock::
 
@@ -234,6 +235,9 @@ This can also be set up as the default behaviour by using the
 
 --expire <time>::
 	With `prune`, only expire unused working trees older than `<time>`.
++
+With `list`, annotate missing working trees as prunable if they are
+older than `<time>`.
 
 --reason <string>::
 	With `lock`, an explanation why the working tree is locked.
@@ -372,6 +376,19 @@ $ git worktree list
 /path/to/other-linked-worktree  1234abc  (detached HEAD)
 ------------
 
+The command also shows annotations for each working tree, according to its state.
+These annotations are:
+
+ * `locked`, if the working tree is locked.
+ * `prunable`, if the working tree can be pruned via `git worktree prune`.
+
+------------
+$ git worktree list
+/path/to/linked-worktree    abcd1234 [master]
+/path/to/locked-worktreee   acbd5678 (brancha) locked
+/path/to/prunable-worktree  5678abc  (detached HEAD) prunable
+------------
+
 Porcelain Format
 ~~~~~~~~~~~~~~~~
 The porcelain format has a line per attribute.  Attributes are listed with a
@@ -405,6 +422,11 @@ HEAD 3456def3456def3456def3456def3456def3456b
 branch refs/heads/locked-with-reason
 locked reason why is locked
 
+worktree /path/to/linked-worktree-prunable
+HEAD 1233def1234def1234def1234def1234def1234b
+detached
+prunable gitdir file points to non-existent location
+
 ------------
 
 If the lock reason contains "unusual" characters such as newline, they
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 98177f91d4..20944c266e 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -592,6 +592,10 @@ static void show_worktree_porcelain(struct worktree *wt)
 	} else if (reason)
 		printf("locked\n");
 
+	reason = worktree_prune_reason(wt, expire);
+	if (reason)
+		printf("prunable %s\n", reason);
+
 	printf("\n");
 }
 
@@ -620,6 +624,9 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 	if (worktree_lock_reason(wt))
 		strbuf_addstr(&sb, " locked");
 
+	if (worktree_prune_reason(wt, expire))
+		strbuf_addstr(&sb, " prunable");
+
 	printf("%s\n", sb.buf);
 	strbuf_release(&sb);
 }
@@ -663,9 +670,12 @@ static int list(int ac, const char **av, const char *prefix)
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT_EXPIRY_DATE(0, "expire", &expire,
+				N_("add 'prunable' annotation to worktrees older than <time>")),
 		OPT_END()
 	};
 
+	expire = TIME_MAX;
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 39596e4aa9..69342b7280 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -104,6 +104,38 @@ test_expect_success '"list" all worktrees --porcelain with locked reason newline
 	test_cmp expect actual
 '
 
+test_expect_success '"list" all worktrees with prunable annotation' '
+	test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	git worktree add --detach unprunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	! grep "/unprunable  *[0-9a-f].* prunable$"
+'
+
+test_expect_success '"list" all worktrees --porcelain with prunable' '
+	test_when_finished "rm -rf prunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	rm -rf prunable &&
+	git worktree list --porcelain >out &&
+	sed -n "/^worktree .*\/prunable$/,/^$/p" <out >only_prunable &&
+	test_i18ngrep "^prunable gitdir file points to non-existent location$" only_prunable
+'
+
+test_expect_success '"list" all worktrees with prunable consistent with "prune"' '
+	test_when_finished "rm -rf prunable unprunable out && git worktree prune" &&
+	git worktree add --detach prunable &&
+	git worktree add --detach unprunable &&
+	rm -rf prunable &&
+	git worktree list >out &&
+	grep "/prunable  *[0-9a-f].* prunable$" out &&
+	! grep "/unprunable  *[0-9a-f].* unprunable$" out &&
+	git worktree prune --verbose >out &&
+	test_i18ngrep "^Removing worktrees/prunable" out &&
+	test_i18ngrep ! "^Removing worktrees/unprunable" out
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* [PATCH v4 7/7] worktree: teach `list` verbose mode
  2021-01-27  8:03     ` Rafael Silva
                         ` (5 preceding siblings ...)
  2021-01-27  8:03       ` [PATCH v4 6/7] worktree: teach `list` to annotate prunable worktree Rafael Silva
@ 2021-01-27  8:03       ` Rafael Silva
  2021-01-30  7:04       ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
  7 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:03 UTC (permalink / raw)
  To: git; +Cc: Eric Sunshine, Phillip Wood, Junio C Hamano, Rafael Silva

"git worktree list" annotates each worktree according to its state such
as "prunable" or "locked", however it is not immediately obvious why
these worktrees are being annotated. For prunable worktrees a reason
is available that is returned by should_prune_worktree() and for locked
worktrees a reason might be available provided by the user via `lock`
command.

Let's teach "git worktree list" a --verbose mode that outputs the reason
why the worktrees are being annotated. The reason is a text that can take
virtually any size and appending the text on the default columned format
will make it difficult to extend the command with other annotations and
not fit nicely on the screen. In order to address this shortcoming the
annotation is then moved to the next line indented followed by the reason
If the reason is not available the annotation stays on the same line as
the worktree itself.

The output of "git worktree list" with verbose becomes like so:

    $ git worktree list --verbose
    ...
    /path/to/locked-no-reason    acb124 [branch-a] locked
    /path/to/locked-with-reason  acc125 [branch-b]
        locked: worktree with a locked reason
    /path/to/prunable-reason     ace127 [branch-d]
        prunable: gitdir file points to non-existent location
    ...

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Rafael Silva <rafaeloliveira.cs@gmail.com>
---
 Documentation/git-worktree.txt | 20 ++++++++++++++++++++
 builtin/worktree.c             | 14 ++++++++++++--
 t/t2402-worktree-list.sh       | 31 +++++++++++++++++++++++++++++++
 3 files changed, 63 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 240c3fd76b..f1bb1fa5f5 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -232,6 +232,8 @@ This can also be set up as the default behaviour by using the
 -v::
 --verbose::
 	With `prune`, report all removals.
++
+With `list`, output additional information about worktrees (see below).
 
 --expire <time>::
 	With `prune`, only expire unused working trees older than `<time>`.
@@ -389,6 +391,24 @@ $ git worktree list
 /path/to/prunable-worktree  5678abc  (detached HEAD) prunable
 ------------
 
+For these annotations, a reason might also be available and this can be
+seen using the verbose mode. The annotation is then moved to the next line
+indented followed by the additional information.
+
+------------
+$ git worktree list --verbose
+/path/to/linked-worktree              abcd1234 [master]
+/path/to/locked-worktree-no-reason    abcd5678 (detached HEAD) locked
+/path/to/locked-worktree-with-reason  1234abcd (brancha)
+	locked: working tree path is mounted on a portable device
+/path/to/prunable-worktree            5678abc1 (detached HEAD)
+	prunable: gitdir file points to non-existent location
+------------
+
+Note that the annotation is moved to the next line if the additional
+information is available, otherwise it stays on the same line as the
+working tree itself.
+
 Porcelain Format
 ~~~~~~~~~~~~~~~~
 The porcelain format has a line per attribute.  Attributes are listed with a
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 20944c266e..1cd5c2016e 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -604,6 +604,7 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 	struct strbuf sb = STRBUF_INIT;
 	int cur_path_len = strlen(wt->path);
 	int path_adj = cur_path_len - utf8_strwidth(wt->path);
+	const char *reason;
 
 	strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
 	if (wt->is_bare)
@@ -621,10 +622,16 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
 			strbuf_addstr(&sb, "(error)");
 	}
 
-	if (worktree_lock_reason(wt))
+	reason = worktree_lock_reason(wt);
+	if (verbose && reason && *reason)
+		strbuf_addf(&sb, "\n\tlocked: %s", reason);
+	else if (reason)
 		strbuf_addstr(&sb, " locked");
 
-	if (worktree_prune_reason(wt, expire))
+	reason = worktree_prune_reason(wt, expire);
+	if (verbose && reason)
+		strbuf_addf(&sb, "\n\tprunable: %s", reason);
+	else if (reason)
 		strbuf_addstr(&sb, " prunable");
 
 	printf("%s\n", sb.buf);
@@ -670,6 +677,7 @@ static int list(int ac, const char **av, const char *prefix)
 
 	struct option options[] = {
 		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("add 'prunable' annotation to worktrees older than <time>")),
 		OPT_END()
@@ -679,6 +687,8 @@ static int list(int ac, const char **av, const char *prefix)
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac)
 		usage_with_options(worktree_usage, options);
+	else if (verbose && porcelain)
+		die(_("--verbose and --porcelain are mutually exclusive"));
 	else {
 		struct worktree **worktrees = get_worktrees();
 		int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 69342b7280..39a339e636 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -136,6 +136,37 @@ test_expect_success '"list" all worktrees with prunable consistent with "prune"'
 	test_i18ngrep ! "^Removing worktrees/unprunable" out
 '
 
+test_expect_success '"list" --verbose and --porcelain mutually exclusive' '
+	test_must_fail git worktree list --verbose --porcelain
+'
+
+test_expect_success '"list" all worktrees --verbose with locked' '
+	test_when_finished "rm -rf locked1 locked2 out actual expect && git worktree prune" &&
+	git worktree add locked1 --detach &&
+	git worktree add locked2 --detach &&
+	git worktree lock locked1 &&
+	test_when_finished "git worktree unlock locked1" &&
+	git worktree lock locked2 --reason "with reason" &&
+	test_when_finished "git worktree unlock locked2" &&
+	echo "$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tlocked: with reason\n" >>expect &&
+	git worktree list --verbose >out &&
+	grep "/locked1  *[0-9a-f].* locked$" out &&
+	sed -n "s/  */ /g;/\/locked2  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
+	test_cmp actual expect
+'
+
+test_expect_success '"list" all worktrees --verbose with prunable' '
+	test_when_finished "rm -rf prunable out actual expect && git worktree prune" &&
+	git worktree add prunable --detach &&
+	echo "$(git -C prunable rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
+	printf "\tprunable: gitdir file points to non-existent location\n" >>expect &&
+	rm -rf prunable &&
+	git worktree list --verbose >out &&
+	sed -n "s/  */ /g;/\/prunable  *[0-9a-f].*$/,/prunable: .*$/p" <out >actual &&
+	test_i18ncmp actual expect
+'
+
 test_expect_success 'bare repo setup' '
 	git init --bare bare1 &&
 	echo "data" >file1 &&
-- 
2.30.0.373.geade8fefe8


^ permalink raw reply related	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-24  8:51     ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
@ 2021-01-27  8:08       ` Rafael Silva
  0 siblings, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-27  8:08 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood


Eric Sunshine <sunshine@sunshineco.com> writes:

> On Tue, Jan 19, 2021 at 4:28 PM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> Changes in v3
>> =============
>> v3 is 1-patch bigger than v2 and it includes the following changes:
>
> Thanks. I gave v3 a complete read-through, and it is just about perfect.
>
> Aside from a very minor problem with the tests in [5/7] and [7/7] and
> the documentation enhancement (`core.quotePath`) suggested by Phillip,
> this is really, really close. (In fact, I would be happy to accept v3
> as-is; those minor changes could easily be done on top.)
>
> I look forward to giving v4 my Reviewed-by:.
>

Thanks again for reviewing the series. I've just sent the v4 and I'm
looking forward to your review.

>> Range-diff against v2:
>> -:  ---------- > 1:  66cd83ba42 worktree: libify should_prune_worktree()
>> 1:  9d47fcb4a4 ! 2:  81f4824028 worktree: teach worktree to lazy-load "prunable" reason
>
> The range-diff seems slightly off. The "libify
> should_prune_worktree()" patch existed in v1 and v2, but the
> range-diff suggests that it's new with v3. You probably just need a
> minor adjustment to your `git format-patch --range-diff` invocation.

Thanks for pointing this out, I didn't notice this from the v3 range-diff.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-27  8:03     ` Rafael Silva
                         ` (6 preceding siblings ...)
  2021-01-27  8:03       ` [PATCH v4 7/7] worktree: teach `list` verbose mode Rafael Silva
@ 2021-01-30  7:04       ` Eric Sunshine
  2021-01-30  9:42         ` Rafael Silva
  2021-01-30 17:50         ` Junio C Hamano
  7 siblings, 2 replies; 88+ messages in thread
From: Eric Sunshine @ 2021-01-30  7:04 UTC (permalink / raw)
  To: Rafael Silva; +Cc: Git List, Phillip Wood, Junio C Hamano

On Wed, Jan 27, 2021 at 3:03 AM Rafael Silva
<rafaeloliveira.cs@gmail.com> wrote:
> Changes in v4
>  * Added documentation to explain that the lock reason is quoted with
>    the same rules as described for `core.quotePath`.
>  * The `worktree unlock` issued in the test cleanup is splitted and
>    executed after each `worktree lock` to ensure the unlock is only
>    done after we know each locked command was successful.
>  * Fix a couple of grammos in the [4/7] commit message.

I just gave v4 a complete read-through and it looks great. All review
comments on previous rounds have been addressed, and I didn't find
anything important to comment on in v4. Very nicely done.

Please consider this entire series:

    Reviewed-by: Eric Sunshine <sunshine@sunshineco.com>

Junio: Perhaps the summary of this series in the merge message can be
refined a bit, as it only talks about verbose mode, but this series
does a bit more than that. Heres a proposal:

    `git worktree list` now annotates worktrees as prunable, shows
    locked and prunable attributes in --porcelain mode, and gained
    a --verbose option.

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-30  7:04       ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
@ 2021-01-30  9:42         ` Rafael Silva
  2021-01-30 17:50         ` Junio C Hamano
  1 sibling, 0 replies; 88+ messages in thread
From: Rafael Silva @ 2021-01-30  9:42 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Phillip Wood, Junio C Hamano


Eric Sunshine <sunshine@sunshineco.com> writes:

> On Wed, Jan 27, 2021 at 3:03 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> Changes in v4
>>  * Added documentation to explain that the lock reason is quoted with
>>    the same rules as described for `core.quotePath`.
>>  * The `worktree unlock` issued in the test cleanup is splitted and
>>    executed after each `worktree lock` to ensure the unlock is only
>>    done after we know each locked command was successful.
>>  * Fix a couple of grammos in the [4/7] commit message.
>
> I just gave v4 a complete read-through and it looks great. All review
> comments on previous rounds have been addressed, and I didn't find
> anything important to comment on in v4. Very nicely done.
>
> Please consider this entire series:
>
>     Reviewed-by: Eric Sunshine <sunshine@sunshineco.com>
>

Thanks for the review and for all the insightful comments.

> Junio: Perhaps the summary of this series in the merge message can be
> refined a bit, as it only talks about verbose mode, but this series
> does a bit more than that. Heres a proposal:
>
>     `git worktree list` now annotates worktrees as prunable, shows
>     locked and prunable attributes in --porcelain mode, and gained
>     a --verbose option.

I believe this message summarize nicely all the changes implemented by
this series.

-- 
Thanks
Rafael

^ permalink raw reply	[flat|nested] 88+ messages in thread

* Re: [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations
  2021-01-30  7:04       ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
  2021-01-30  9:42         ` Rafael Silva
@ 2021-01-30 17:50         ` Junio C Hamano
  1 sibling, 0 replies; 88+ messages in thread
From: Junio C Hamano @ 2021-01-30 17:50 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Rafael Silva, Git List, Phillip Wood

Eric Sunshine <sunshine@sunshineco.com> writes:

> On Wed, Jan 27, 2021 at 3:03 AM Rafael Silva
> <rafaeloliveira.cs@gmail.com> wrote:
>> Changes in v4
>>  * Added documentation to explain that the lock reason is quoted with
>>    the same rules as described for `core.quotePath`.
>>  * The `worktree unlock` issued in the test cleanup is splitted and
>>    executed after each `worktree lock` to ensure the unlock is only
>>    done after we know each locked command was successful.
>>  * Fix a couple of grammos in the [4/7] commit message.
>
> I just gave v4 a complete read-through and it looks great. All review
> comments on previous rounds have been addressed, and I didn't find
> anything important to comment on in v4. Very nicely done.
>
> Please consider this entire series:
>
>     Reviewed-by: Eric Sunshine <sunshine@sunshineco.com>
>
> Junio: Perhaps the summary of this series in the merge message can be
> refined a bit, as it only talks about verbose mode, but this series
> does a bit more than that. Heres a proposal:
>
>     `git worktree list` now annotates worktrees as prunable, shows
>     locked and prunable attributes in --porcelain mode, and gained
>     a --verbose option.

Nicely done.  Thanks for excellent contribution, both of you.


^ permalink raw reply	[flat|nested] 88+ messages in thread

end of thread, other threads:[~2021-01-30 17:51 UTC | newest]

Thread overview: 88+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-01-04 16:21 [PATCH 0/7] teach `worktree list` verbose mode and prunable annotations Rafael Silva
2021-01-04 16:21 ` [PATCH 1/7] worktree: move should_prune_worktree() to worktree.c Rafael Silva
2021-01-06  5:58   ` Eric Sunshine
2021-01-08  7:40     ` Rafael Silva
2021-01-06  6:55   ` Eric Sunshine
2021-01-07  7:24     ` Eric Sunshine
2021-01-08  7:41     ` Rafael Silva
2021-01-04 16:21 ` [PATCH 2/7] worktree: implement worktree_prune_reason() wrapper Rafael Silva
2021-01-06  7:08   ` Eric Sunshine
2021-01-08  7:42     ` Rafael Silva
2021-01-04 16:21 ` [PATCH 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
2021-01-06  7:29   ` Eric Sunshine
2021-01-08  7:43     ` Rafael Silva
2021-01-04 16:21 ` [PATCH 4/7] worktree: teach `list` prunable annotation and verbose Rafael Silva
2021-01-06  8:31   ` Eric Sunshine
2021-01-08  7:45     ` Rafael Silva
2021-01-04 16:21 ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Rafael Silva
2021-01-05 10:29   ` Phillip Wood
2021-01-05 11:02     ` [PATCH] worktree: add -z option for list subcommand Phillip Wood
2021-01-07  3:34       ` Eric Sunshine
2021-01-08 10:33         ` Phillip Wood
2021-01-10  7:27           ` Eric Sunshine
2021-01-06  9:07     ` [PATCH 5/7] worktree: `list` escape lock reason in --porcelain Eric Sunshine
2021-01-08  7:47     ` Rafael Silva
2021-01-06  8:59   ` Eric Sunshine
2021-01-04 16:21 ` [PATCH 6/7] worktree: add tests for `list` verbose and annotations Rafael Silva
2021-01-06  9:39   ` Eric Sunshine
2021-01-07  4:09     ` Eric Sunshine
2021-01-08  7:49     ` Rafael Silva
2021-01-04 16:21 ` [PATCH 7/7] worktree: document `list` verbose and prunable annotations Rafael Silva
2021-01-06  9:57   ` Eric Sunshine
2021-01-08  7:49     ` Rafael Silva
2021-01-06  5:36 ` [PATCH 0/7] teach `worktree list` verbose mode " Eric Sunshine
2021-01-08  7:38   ` Rafael Silva
2021-01-08  8:19     ` Eric Sunshine
2021-01-17 23:42 ` [PATCH v2 0/6] " Rafael Silva
2021-01-17 23:42   ` [PATCH v2 1/6] worktree: libify should_prune_worktree() Rafael Silva
2021-01-17 23:42   ` [PATCH v2 2/6] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
2021-01-18  2:57     ` Eric Sunshine
2021-01-19  7:57       ` Rafael Silva
2021-01-17 23:42   ` [PATCH v2 3/6] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
2021-01-17 23:42   ` [PATCH v2 4/6] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
2021-01-18  3:55     ` Eric Sunshine
2021-01-19  8:20       ` Rafael Silva
2021-01-19 17:16         ` Eric Sunshine
2021-01-17 23:42   ` [PATCH v2 5/6] worktree: teach `list` to annotate prunable worktree Rafael Silva
2021-01-18  4:45     ` Eric Sunshine
2021-01-19 10:26       ` Rafael Silva
2021-01-19 17:23         ` Eric Sunshine
2021-01-17 23:42   ` [PATCH v2 6/6] worktree: teach `list` verbose mode Rafael Silva
2021-01-18  5:15     ` Eric Sunshine
2021-01-18 19:40       ` Eric Sunshine
2021-01-18  5:33   ` [PATCH v2 0/6] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
2021-01-19 16:44     ` Rafael Silva
2021-01-19 21:27   ` [PATCH v3 0/7] " Rafael Silva
2021-01-19 21:27     ` [PATCH v3 1/7] worktree: libify should_prune_worktree() Rafael Silva
2021-01-19 21:27     ` [PATCH v3 2/7] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
2021-01-19 21:27     ` [PATCH v3 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
2021-01-19 21:27     ` [PATCH v3 4/7] t2402: ensure locked worktree is properly cleaned up Rafael Silva
2021-01-24  7:50       ` Eric Sunshine
2021-01-24 10:19         ` Rafael Silva
2021-01-19 21:27     ` [PATCH v3 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
2021-01-20 11:00       ` Phillip Wood
2021-01-21  3:18         ` Junio C Hamano
2021-01-21 15:25         ` Rafael Silva
2021-01-24  8:24         ` Eric Sunshine
2021-01-24  8:10       ` Eric Sunshine
2021-01-24 10:20         ` Rafael Silva
2021-01-19 21:27     ` [PATCH v3 6/7] worktree: teach `list` to annotate prunable worktree Rafael Silva
2021-01-21  3:28       ` Junio C Hamano
2021-01-21 15:09         ` Rafael Silva
2021-01-21 22:18           ` Junio C Hamano
2021-01-19 21:27     ` [PATCH v3 7/7] worktree: teach `list` verbose mode Rafael Silva
2021-01-24  8:42       ` Eric Sunshine
2021-01-24 10:21         ` Rafael Silva
2021-01-24  8:51     ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
2021-01-27  8:08       ` Rafael Silva
2021-01-27  8:03     ` Rafael Silva
2021-01-27  8:03       ` [PATCH v4 1/7] worktree: libify should_prune_worktree() Rafael Silva
2021-01-27  8:03       ` [PATCH v4 2/7] worktree: teach worktree to lazy-load "prunable" reason Rafael Silva
2021-01-27  8:03       ` [PATCH v4 3/7] worktree: teach worktree_lock_reason() to gently handle main worktree Rafael Silva
2021-01-27  8:03       ` [PATCH v4 4/7] t2402: ensure locked worktree is properly cleaned up Rafael Silva
2021-01-27  8:03       ` [PATCH v4 5/7] worktree: teach `list --porcelain` to annotate locked worktree Rafael Silva
2021-01-27  8:03       ` [PATCH v4 6/7] worktree: teach `list` to annotate prunable worktree Rafael Silva
2021-01-27  8:03       ` [PATCH v4 7/7] worktree: teach `list` verbose mode Rafael Silva
2021-01-30  7:04       ` [PATCH v3 0/7] teach `worktree list` verbose mode and prunable annotations Eric Sunshine
2021-01-30  9:42         ` Rafael Silva
2021-01-30 17:50         ` Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).