{"thread":{"id":"40082","subject":"[PATCH 0/2 v4] worktree: for_each_worktree and list","startedAt":"2015-08-13T18:32:17Z","lastAt":"2015-08-13T23:32:09Z","messageCount":6,"participants":["Michael Rappazzo","David Turner","Mike Rappazzo"],"isPatch":true,"patchVersion":4,"patchTotal":2},"messages":[{"id":"268006","messageId":"1439490739-9361-1-git-send-email-rappazzo@gmail.com","threadId":"40082","inReplyTo":null,"subject":"[PATCH 0/2 v4] worktree: for_each_worktree and list","fromName":"Michael Rappazzo","fromEmail":"rappazzo@gmail.com","sentAt":"2015-08-13T18:32:17Z","receivedAt":"2015-08-13T18:32:17Z","isPatch":true,"sender":{"key":"rappazzo@gmail.com","avatar":"https://avatars.githubusercontent.com/u/525287?v=4"},"body":"This patch series adds a for_each_worktree function and then uses it to implement \nthe `git worktree list` command.\n\nDifferences from [v3](http://www.mail-archive.com/git@vger.kernel.org/msg75599.html)\n  - create the for_each_worktree function\n  - uses the function in the list\n  - a bare repo is NOT included in the iterated worktrees\n\n\nMichael Rappazzo (2):\n  worktree: add 'for_each_worktree' function\n  worktree: add 'list' command\n\n Documentation/git-worktree.txt |  11 +++-\n builtin/worktree.c             | 139 +++++++++++++++++++++++++++++++++++++++++\n t/t2027-worktree-list.sh       |  51 +++++++++++++++\n 3 files changed, 200 insertions(+), 1 deletion(-)\n create mode 100755 t/t2027-worktree-list.sh\n\n-- \n2.5.0\n"},{"id":"268007","messageId":"1439490739-9361-2-git-send-email-rappazzo@gmail.com","threadId":"40082","inReplyTo":"1439490739-9361-1-git-send-email-rappazzo@gmail.com","subject":"[PATCH 1/2 v4] worktree: add 'for_each_worktree' function","fromName":"Michael Rappazzo","fromEmail":"rappazzo@gmail.com","sentAt":"2015-08-13T18:32:18Z","receivedAt":"2015-08-13T18:32:18Z","isPatch":true,"sender":{"key":"rappazzo@gmail.com","avatar":"https://avatars.githubusercontent.com/u/525287?v=4"},"body":"for_each_worktree iterates through each worktree and invokes a callback\nfunction.  The main worktree (if not bare) is always encountered first,\nfollowed by worktrees created by `git worktree add`.\n\nIf the callback function returns a non-zero value, iteration stops, and\nthe callback's return value is returned from the for_each_worktree\nfunction.\n\nSigned-off-by: Michael Rappazzo <rappazzo@gmail.com>\n---\n builtin/worktree.c | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 84 insertions(+)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 430b51e..a43e360 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -26,6 +26,14 @@ static int show_only;\n static int verbose;\n static unsigned long expire;\n \n+/*\n+ * The signature for the callback function for the for_each_worktree()\n+ * function below.  The memory pointed to by the callback arguments\n+ * is only guaranteed to be valid for the duration of a single\n+ * callback invocation.\n+ */\n+typedef int each_worktree_fn(const char *path, const char *git_dir, void *cb_data);\n+\n static int prune_worktree(const char *id, struct strbuf *reason)\n {\n \tstruct stat st;\n@@ -358,6 +366,82 @@ static int add(int ac, const char **av, const char *prefix)\n \n \treturn add_worktree(path, branch, &opts);\n }\n+/*\n+ * Iterate through each worktree and invoke the callback function.  If\n+ * the callback function returns non-zero, the iteration stops, and\n+ * this function returns that return value\n+ */\n+static int for_each_worktree(each_worktree_fn fn, void *cb_data)\n+{\n+\tstruct strbuf main_path = STRBUF_INIT;\n+\tstruct dirent *d;\n+\tstruct stat st;\n+\tstruct strbuf worktree_path = STRBUF_INIT;\n+\tstruct strbuf worktree_git = STRBUF_INIT;\n+\tconst char *common_dir;\n+\tDIR *dir;\n+\tint main_is_bare = 0;\n+\tint ret = 0;\n+\n+\tcommon_dir = get_git_common_dir();\n+\tif (!strcmp(common_dir, get_git_dir())) {\n+\t\t/* simple case - this is the main repo */\n+\t\tmain_is_bare = is_bare_repository();\n+\t\tif (!main_is_bare) {\n+\t\t\tstrbuf_addstr(&main_path, get_git_work_tree());\n+\t\t} else {\n+\t\t\tstrbuf_addstr(&main_path, common_dir);\n+\t\t}\n+\t} else {\n+\t\tstrbuf_addstr(&main_path, common_dir);\n+\t\tstrbuf_strip_suffix(&main_path, \"/.git\");\n+\t\t/* If the common_dir DOES NOT end with '/.git', then it is bare */\n+\t\tmain_is_bare = !strcmp(common_dir, main_path.buf);\n+\t}\n+\n+\tif (!main_is_bare) {\n+\t\tstrbuf_addstr(&worktree_path, main_path.buf);\n+\n+\t\tstrbuf_addstr(&main_path, \"/.git\");\n+\t\tstrbuf_addstr(&worktree_git, main_path.buf);\n+\n+\t\tret = fn(worktree_path.buf, worktree_git.buf, cb_data);\n+\t\tif (ret)\n+\t\t\tgoto done;\n+\t}\n+\tstrbuf_addstr(&main_path, \"/worktrees\");\n+\n+\tif (is_directory(main_path.buf)) {\n+\t\tdir = opendir(main_path.buf);\n+\t\tif (dir) {\n+\t\t\twhile ((d = readdir(dir)) != NULL) {\n+\t\t\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\t\t\tcontinue;\n+\n+\t\t\t\tstrbuf_reset(&worktree_git);\n+\t\t\t\tstrbuf_addf(&worktree_git, \"%s/%s/gitdir\", main_path.buf, d->d_name);\n+\n+\t\t\t\tif (stat(worktree_git.buf, &st))\n+\t\t\t\t\tcontinue;\n+\n+\t\t\t\tstrbuf_reset(&worktree_path);\n+\t\t\t\tstrbuf_read_file(&worktree_path, worktree_git.buf, st.st_size);\n+\t\t\t\tstrbuf_strip_suffix(&worktree_path, \"/.git\\n\");\n+\n+\t\t\t\tstrbuf_strip_suffix(&worktree_git, \"/gitdir\");\n+\t\t\t\tret = fn(worktree_path.buf, worktree_git.buf, cb_data);\n+\t\t\t\tif (ret)\n+\t\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t\tclosedir(dir);\n+\t}\n+done:\n+\tstrbuf_release(&worktree_git);\n+\tstrbuf_release(&worktree_path);\n+\tstrbuf_release(&main_path);\n+\treturn ret;\n+}\n \n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n-- \n2.5.0\n"},{"id":"268008","messageId":"1439490739-9361-3-git-send-email-rappazzo@gmail.com","threadId":"40082","inReplyTo":"1439490739-9361-1-git-send-email-rappazzo@gmail.com","subject":"[PATCH 2/2 v4] worktree: add 'list' command","fromName":"Michael Rappazzo","fromEmail":"rappazzo@gmail.com","sentAt":"2015-08-13T18:32:19Z","receivedAt":"2015-08-13T18:32:19Z","isPatch":true,"sender":{"key":"rappazzo@gmail.com","avatar":"https://avatars.githubusercontent.com/u/525287?v=4"},"body":"'git worktree list' uses the for_each_worktree function to iterate,\nand outputs in the format: '<worktree>  (<short-ref>)'\n\nSigned-off-by: Michael Rappazzo <rappazzo@gmail.com>\n---\n Documentation/git-worktree.txt | 11 ++++++++-\n builtin/worktree.c             | 55 ++++++++++++++++++++++++++++++++++++++++++\n t/t2027-worktree-list.sh       | 51 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 116 insertions(+), 1 deletion(-)\n create mode 100755 t/t2027-worktree-list.sh\n\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex fb68156..e953b4e 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -11,6 +11,7 @@ SYNOPSIS\n [verse]\n 'git worktree add' [-f] [--detach] [-b <new-branch>] <path> [<branch>]\n 'git worktree prune' [-n] [-v] [--expire <expire>]\n+'git worktree list' [--path-only]\n \n DESCRIPTION\n -----------\n@@ -59,6 +60,12 @@ prune::\n \n Prune working tree information in $GIT_DIR/worktrees.\n \n+list::\n+\n+List the main worktree followed by all of the linked worktrees.  The default\n+format of the list includes the full path to the worktree and the branch or\n+revision that the head of that worktree is currently pointing to.\n+\n OPTIONS\n -------\n \n@@ -93,6 +100,9 @@ OPTIONS\n --expire <time>::\n \tWith `prune`, only expire unused working trees older than <time>.\n \n+--path-only\n+\tWith `list`, only show the worktree path\n+\n DETAILS\n -------\n Each linked working tree has a private sub-directory in the repository's\n@@ -167,7 +177,6 @@ performed manually, such as:\n - `remove` to remove a linked working tree and its administrative files (and\n   warn if the working tree is dirty)\n - `mv` to move or rename a working tree and update its administrative files\n-- `list` to list linked working trees\n - `lock` to prevent automatic pruning of administrative files (for instance,\n   for a working tree on a portable device)\n \ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex a43e360..b39ecbd 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -12,6 +12,7 @@\n static const char * const worktree_usage[] = {\n \tN_(\"git worktree add [<options>] <path> <branch>\"),\n \tN_(\"git worktree prune [<options>]\"),\n+\tN_(\"git worktree list [<options>]\"),\n \tNULL\n };\n \n@@ -443,6 +444,58 @@ done:\n \treturn ret;\n }\n \n+/* list callback data */\n+struct list_opts {\n+\tint path_only;\n+};\n+\n+static int print_worktree_details(const char *path, const char *git_dir, void *cb_data)\n+{\n+\tstruct strbuf head_file = STRBUF_INIT;\n+\tstruct strbuf head_ref = STRBUF_INIT;\n+\tstruct stat st;\n+\tstruct list_opts *opts = cb_data;\n+\tconst char *ref_prefix = \"ref: refs/heads/\";\n+\n+\tstrbuf_addf(&head_file, \"%s/HEAD\", git_dir);\n+\tif (!opts->path_only && !stat(head_file.buf, &st)) {\n+\t\tstrbuf_read_file(&head_ref, head_file.buf, st.st_size);\n+\t\tstrbuf_strip_suffix(&head_ref, \"\\n\");\n+\n+\t\tif (starts_with(head_ref.buf, ref_prefix)) {\n+\t\t\t/* branch checked out */\n+\t\t\tstrbuf_remove(&head_ref, 0, strlen(ref_prefix));\n+\t\t/* } else {\n+\t\t *  headless -- no-op\n+\t\t */\n+\t\t}\n+\t\tprintf(\"%s  (%s)\\n\", path, head_ref.buf);\n+\t} else {\n+\t\tprintf(\"%s\\n\", path);\n+\t}\n+\n+\tstrbuf_release(&head_ref);\n+\tstrbuf_release(&head_file);\n+\treturn 0;\n+}\n+\n+static int list(int ac, const char **av, const char *prefix)\n+{\n+\tstruct list_opts opts;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"path-only\", &opts.path_only, N_(\"only show the path of the worktree\")),\n+\t\tOPT_END()\n+\t};\n+\n+\topts.path_only = 0;\n+\n+\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n+\tif (ac)\n+\t\tusage_with_options(worktree_usage, options);\n+\n+\treturn for_each_worktree(&print_worktree_details, &opts);\n+}\n+\n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n \tstruct option options[] = {\n@@ -455,5 +508,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)\n \t\treturn add(ac - 1, av + 1, prefix);\n \tif (!strcmp(av[1], \"prune\"))\n \t\treturn prune(ac - 1, av + 1, prefix);\n+\tif (!strcmp(av[1], \"list\"))\n+\t\treturn list(ac - 1, av + 1, prefix);\n \tusage_with_options(worktree_usage, options);\n }\ndiff --git a/t/t2027-worktree-list.sh b/t/t2027-worktree-list.sh\nnew file mode 100755\nindex 0000000..220f98e\n--- /dev/null\n+++ b/t/t2027-worktree-list.sh\n@@ -0,0 +1,51 @@\n+#!/bin/sh\n+\n+test_description='test git worktree list'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit init\n+'\n+\n+test_expect_success '\"list\" all worktrees from main' '\n+\techo \"$(git rev-parse --show-toplevel)  ($(git symbolic-ref --short HEAD))\" >expect &&\n+\tgit worktree add --detach here master &&\n+\techo \"$(git -C here rev-parse --show-toplevel)  ($(git -C here rev-parse HEAD))\" >>expect &&\n+\tgit worktree list >actual &&\n+\ttest_cmp expect actual &&\n+\trm -rf here &&\n+\tgit worktree prune\n+'\n+\n+test_expect_success '\"list\" all worktrees from linked' '\n+\techo \"$(git rev-parse --show-toplevel)  ($(git symbolic-ref --short HEAD))\" >expect &&\n+\tgit worktree add --detach here master &&\n+\techo \"$(git -C here rev-parse --show-toplevel)  ($(git -C here rev-parse HEAD))\" >>expect &&\n+\tgit -C here worktree list >actual &&\n+\ttest_cmp expect actual &&\n+\trm -rf here &&\n+\tgit worktree prune\n+'\n+\n+test_expect_success '\"list\" all worktrees from main --path-only' '\n+\tgit rev-parse --show-toplevel >expect &&\n+\tgit worktree add --detach here master &&\n+\tgit -C here rev-parse --show-toplevel >>expect &&\n+\tgit worktree list --path-only >actual &&\n+\ttest_cmp expect actual &&\n+\trm -rf here &&\n+\tgit worktree prune\n+'\n+\n+test_expect_success '\"list\" all worktrees from linked --path-only' '\n+\tgit rev-parse --show-toplevel >expect &&\n+\tgit worktree add --detach here master &&\n+\tgit -C here rev-parse --show-toplevel >>expect &&\n+\tgit -C here worktree list --path-only >actual &&\n+\ttest_cmp expect actual &&\n+\trm -rf here &&\n+\tgit worktree prune\n+'\n+\n+test_done\n-- \n2.5.0\n"},{"id":"268023","messageId":"1439493795.8855.16.camel@twopensource.com","threadId":"40082","inReplyTo":"1439490739-9361-2-git-send-email-rappazzo@gmail.com","subject":"Re: [PATCH 1/2 v4] worktree: add 'for_each_worktree' function","fromName":"David Turner","fromEmail":"dturner@twopensource.com","sentAt":"2015-08-13T19:23:15Z","receivedAt":"2015-08-13T19:23:15Z","isPatch":true,"sender":{"key":"novalis@novalis.org","avatar":"https://avatars.githubusercontent.com/u/77003?v=4"},"body":"On Thu, 2015-08-13 at 14:32 -0400, Michael Rappazzo wrote:\n> for_each_worktree iterates through each worktree and invokes a callback\n> function.  The main worktree (if not bare) is always encountered first,\n> followed by worktrees created by `git worktree add`.\n\nThanks!  This will be super-useful!  I particularly appreciate the\ndetailed function documentation.\n\nMore comments below.\n\n> If the callback function returns a non-zero value, iteration stops, and\n> the callback's return value is returned from the for_each_worktree\n> function.\n> \n> Signed-off-by: Michael Rappazzo <rappazzo@gmail.com>\n> ---\n>  builtin/worktree.c | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n>  1 file changed, 84 insertions(+)\n> \n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> index 430b51e..a43e360 100644\n> --- a/builtin/worktree.c\n> +++ b/builtin/worktree.c\n> @@ -26,6 +26,14 @@ static int show_only;\n>  static int verbose;\n>  static unsigned long expire;\n>  \n> +/*\n> + * The signature for the callback function for the for_each_worktree()\n> + * function below.  The memory pointed to by the callback arguments\n> + * is only guaranteed to be valid for the duration of a single\n> + * callback invocation.\n> + */\n> +typedef int each_worktree_fn(const char *path, const char *git_dir, void *cb_data);\n> +\n>  static int prune_worktree(const char *id, struct strbuf *reason)\n>  {\n>  \tstruct stat st;\n> @@ -358,6 +366,82 @@ static int add(int ac, const char **av, const char *prefix)\n>  \n>  \treturn add_worktree(path, branch, &opts);\n>  }\n> +/*\n\nnit: newline above this line\n\n> + * Iterate through each worktree and invoke the callback function.  If\n> + * the callback function returns non-zero, the iteration stops, and\n> + * this function returns that return value\n> + */\n> +static int for_each_worktree(each_worktree_fn fn, void *cb_data)\n> +{\n> +\tstruct strbuf main_path = STRBUF_INIT;\n> +\tstruct dirent *d;\n\n\nThe scope of d can be smaller; move it down to the place I've marked\nbelow\n\n> +\tstruct stat st;\n> +\tstruct strbuf worktree_path = STRBUF_INIT;\n> +\tstruct strbuf worktree_git = STRBUF_INIT;\n> +\tconst char *common_dir;\n> +\tDIR *dir;\n\nditto\n\n> +\tint main_is_bare = 0;\n> +\tint ret = 0;\n> +\n> +\tcommon_dir = get_git_common_dir();\n> +\tif (!strcmp(common_dir, get_git_dir())) {\n> +\t\t/* simple case - this is the main repo */\n> +\t\tmain_is_bare = is_bare_repository();\n> +\t\tif (!main_is_bare) {\n> +\t\t\tstrbuf_addstr(&main_path, get_git_work_tree());\n> +\t\t} else {\n> +\t\t\tstrbuf_addstr(&main_path, common_dir);\n> +\t\t}\n> +\t} else {\n> +\t\tstrbuf_addstr(&main_path, common_dir);\n> +\t\tstrbuf_strip_suffix(&main_path, \"/.git\");\n> +\t\t/* If the common_dir DOES NOT end with '/.git', then it is bare */\n> +\t\tmain_is_bare = !strcmp(common_dir, main_path.buf);\n\nstrbuf_strip_suffix returns 1 if the suffix was stripped and 0\notherwise, so there is no need for this strcmp.\n\nI'm a little worried about this path manipulation; it looks like the\nconfig setting core.bare is the actual thing to check?  (Along with\nmaybe the GIT_WORK_TREE environment var; not sure how that interacts\nwith worktrees).\n\n> +\t}\n> +\n> +\tif (!main_is_bare) {\n> +\t\tstrbuf_addstr(&worktree_path, main_path.buf);\n> +\n> +\t\tstrbuf_addstr(&main_path, \"/.git\");\n> +\t\tstrbuf_addstr(&worktree_git, main_path.buf);\n> +\n> +\t\tret = fn(worktree_path.buf, worktree_git.buf, cb_data);\n> +\t\tif (ret)\n> +\t\t\tgoto done;\n> +\t}\n> +\tstrbuf_addstr(&main_path, \"/worktrees\");\n\nEarlier, you said:\n\t\tstrbuf_addstr(&main_path, common_dir);\n\t\tstrbuf_strip_suffix(&main_path, \"/.git\");\n\nNow you are adding /worktrees.  Doesn't this mean, in the non-bare case,\nthat you'll be looking in $common_dir/worktrees instead of\n$common_dir/.git/worktrees ?\n\n> +\tif (is_directory(main_path.buf)) {\n\ndeclare dir here...\n\n> +\t\tdir = opendir(main_path.buf);\n> +\t\tif (dir) {\n\n... and d here.\n\n> +\t\t\twhile ((d = readdir(dir)) != NULL) {\n> +\t\t\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n> +\t\t\t\t\tcontinue;\n> +\n> +\t\t\t\tstrbuf_reset(&worktree_git);\n> +\t\t\t\tstrbuf_addf(&worktree_git, \"%s/%s/gitdir\", main_path.buf, d->d_name);\n\ntioli: main_path never changes, so no need to keep chopping it off and\nadding it back on; just truncate worktree_git to main_path.len + 1 here\nand then add d->name.\n\n> +\t\t\t\tif (stat(worktree_git.buf, &st))\n> +\t\t\t\t\tcontinue;\n\nDo we want to report errors other than ENOENT? (genuine question)\n"},{"id":"268024","messageId":"1439493989.8855.18.camel@twopensource.com","threadId":"40082","inReplyTo":"1439490739-9361-3-git-send-email-rappazzo@gmail.com","subject":"Re: [PATCH 2/2 v4] worktree: add 'list' command","fromName":"David Turner","fromEmail":"dturner@twopensource.com","sentAt":"2015-08-13T19:26:29Z","receivedAt":"2015-08-13T19:26:29Z","isPatch":true,"sender":{"key":"novalis@novalis.org","avatar":"https://avatars.githubusercontent.com/u/77003?v=4"},"body":"On Thu, 2015-08-13 at 14:32 -0400, Michael Rappazzo wrote:\n> 'git worktree list' uses the for_each_worktree function to iterate,\n> and outputs in the format: '<worktree>  (<short-ref>)'\n\nI'm not sure I'm going to have time to review the whole thing, but I\nthink we ought to have tests with both bare and non-bare main repos.\n\n> Signed-off-by: Michael Rappazzo <rappazzo@gmail.com>\n> ---\n>  Documentation/git-worktree.txt | 11 ++++++++-\n>  builtin/worktree.c             | 55 ++++++++++++++++++++++++++++++++++++++++++\n>  t/t2027-worktree-list.sh       | 51 +++++++++++++++++++++++++++++++++++++++\n>  3 files changed, 116 insertions(+), 1 deletion(-)\n>  create mode 100755 t/t2027-worktree-list.sh\n> \n> diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\n> index fb68156..e953b4e 100644\n> --- a/Documentation/git-worktree.txt\n> +++ b/Documentation/git-worktree.txt\n> @@ -11,6 +11,7 @@ SYNOPSIS\n>  [verse]\n>  'git worktree add' [-f] [--detach] [-b <new-branch>] <path> [<branch>]\n>  'git worktree prune' [-n] [-v] [--expire <expire>]\n> +'git worktree list' [--path-only]\n>  \n>  DESCRIPTION\n>  -----------\n> @@ -59,6 +60,12 @@ prune::\n>  \n>  Prune working tree information in $GIT_DIR/worktrees.\n>  \n> +list::\n> +\n> +List the main worktree followed by all of the linked worktrees.  The default\n> +format of the list includes the full path to the worktree and the branch or\n> +revision that the head of that worktree is currently pointing to.\n> +\n>  OPTIONS\n>  -------\n>  \n> @@ -93,6 +100,9 @@ OPTIONS\n>  --expire <time>::\n>  \tWith `prune`, only expire unused working trees older than <time>.\n>  \n> +--path-only\n> +\tWith `list`, only show the worktree path\n> +\n>  DETAILS\n>  -------\n>  Each linked working tree has a private sub-directory in the repository's\n> @@ -167,7 +177,6 @@ performed manually, such as:\n>  - `remove` to remove a linked working tree and its administrative files (and\n>    warn if the working tree is dirty)\n>  - `mv` to move or rename a working tree and update its administrative files\n> -- `list` to list linked working trees\n>  - `lock` to prevent automatic pruning of administrative files (for instance,\n>    for a working tree on a portable device)\n>  \n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> index a43e360..b39ecbd 100644\n> --- a/builtin/worktree.c\n> +++ b/builtin/worktree.c\n> @@ -12,6 +12,7 @@\n>  static const char * const worktree_usage[] = {\n>  \tN_(\"git worktree add [<options>] <path> <branch>\"),\n>  \tN_(\"git worktree prune [<options>]\"),\n> +\tN_(\"git worktree list [<options>]\"),\n>  \tNULL\n>  };\n>  \n> @@ -443,6 +444,58 @@ done:\n>  \treturn ret;\n>  }\n>  \n> +/* list callback data */\n> +struct list_opts {\n> +\tint path_only;\n> +};\n> +\n> +static int print_worktree_details(const char *path, const char *git_dir, void *cb_data)\n> +{\n> +\tstruct strbuf head_file = STRBUF_INIT;\n> +\tstruct strbuf head_ref = STRBUF_INIT;\n> +\tstruct stat st;\n> +\tstruct list_opts *opts = cb_data;\n> +\tconst char *ref_prefix = \"ref: refs/heads/\";\n> +\n> +\tstrbuf_addf(&head_file, \"%s/HEAD\", git_dir);\n> +\tif (!opts->path_only && !stat(head_file.buf, &st)) {\n> +\t\tstrbuf_read_file(&head_ref, head_file.buf, st.st_size);\n> +\t\tstrbuf_strip_suffix(&head_ref, \"\\n\");\n> +\n> +\t\tif (starts_with(head_ref.buf, ref_prefix)) {\n> +\t\t\t/* branch checked out */\n> +\t\t\tstrbuf_remove(&head_ref, 0, strlen(ref_prefix));\n> +\t\t/* } else {\n> +\t\t *  headless -- no-op\n> +\t\t */\n> +\t\t}\n> +\t\tprintf(\"%s  (%s)\\n\", path, head_ref.buf);\n> +\t} else {\n> +\t\tprintf(\"%s\\n\", path);\n> +\t}\n> +\n> +\tstrbuf_release(&head_ref);\n> +\tstrbuf_release(&head_file);\n> +\treturn 0;\n> +}\n> +\n> +static int list(int ac, const char **av, const char *prefix)\n> +{\n> +\tstruct list_opts opts;\n> +\tstruct option options[] = {\n> +\t\tOPT_BOOL(0, \"path-only\", &opts.path_only, N_(\"only show the path of the worktree\")),\n> +\t\tOPT_END()\n> +\t};\n> +\n> +\topts.path_only = 0;\n> +\n> +\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n> +\tif (ac)\n> +\t\tusage_with_options(worktree_usage, options);\n> +\n> +\treturn for_each_worktree(&print_worktree_details, &opts);\n> +}\n> +\n>  int cmd_worktree(int ac, const char **av, const char *prefix)\n>  {\n>  \tstruct option options[] = {\n> @@ -455,5 +508,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)\n>  \t\treturn add(ac - 1, av + 1, prefix);\n>  \tif (!strcmp(av[1], \"prune\"))\n>  \t\treturn prune(ac - 1, av + 1, prefix);\n> +\tif (!strcmp(av[1], \"list\"))\n> +\t\treturn list(ac - 1, av + 1, prefix);\n>  \tusage_with_options(worktree_usage, options);\n>  }\n> diff --git a/t/t2027-worktree-list.sh b/t/t2027-worktree-list.sh\n> new file mode 100755\n> index 0000000..220f98e\n> --- /dev/null\n> +++ b/t/t2027-worktree-list.sh\n> @@ -0,0 +1,51 @@\n> +#!/bin/sh\n> +\n> +test_description='test git worktree list'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup' '\n> +\ttest_commit init\n> +'\n> +\n> +test_expect_success '\"list\" all worktrees from main' '\n> +\techo \"$(git rev-parse --show-toplevel)  ($(git symbolic-ref --short HEAD))\" >expect &&\n> +\tgit worktree add --detach here master &&\n> +\techo \"$(git -C here rev-parse --show-toplevel)  ($(git -C here rev-parse HEAD))\" >>expect &&\n> +\tgit worktree list >actual &&\n> +\ttest_cmp expect actual &&\n> +\trm -rf here &&\n> +\tgit worktree prune\n> +'\n> +\n> +test_expect_success '\"list\" all worktrees from linked' '\n> +\techo \"$(git rev-parse --show-toplevel)  ($(git symbolic-ref --short HEAD))\" >expect &&\n> +\tgit worktree add --detach here master &&\n> +\techo \"$(git -C here rev-parse --show-toplevel)  ($(git -C here rev-parse HEAD))\" >>expect &&\n> +\tgit -C here worktree list >actual &&\n> +\ttest_cmp expect actual &&\n> +\trm -rf here &&\n> +\tgit worktree prune\n> +'\n> +\n> +test_expect_success '\"list\" all worktrees from main --path-only' '\n> +\tgit rev-parse --show-toplevel >expect &&\n> +\tgit worktree add --detach here master &&\n> +\tgit -C here rev-parse --show-toplevel >>expect &&\n> +\tgit worktree list --path-only >actual &&\n> +\ttest_cmp expect actual &&\n> +\trm -rf here &&\n> +\tgit worktree prune\n> +'\n> +\n> +test_expect_success '\"list\" all worktrees from linked --path-only' '\n> +\tgit rev-parse --show-toplevel >expect &&\n> +\tgit worktree add --detach here master &&\n> +\tgit -C here rev-parse --show-toplevel >>expect &&\n> +\tgit -C here worktree list --path-only >actual &&\n> +\ttest_cmp expect actual &&\n> +\trm -rf here &&\n> +\tgit worktree prune\n> +'\n> +\n> +test_done\n"},{"id":"268030","messageId":"CANoM8SUGHzH-YjB9yK-juoX0S9zyUk=QhCK5JUqiQiqwOwwqJg@mail.gmail.com","threadId":"40082","inReplyTo":"1439493795.8855.16.camel@twopensource.com","subject":"Re: [PATCH 1/2 v4] worktree: add 'for_each_worktree' function","fromName":"Mike Rappazzo","fromEmail":"rappazzo@gmail.com","sentAt":"2015-08-13T23:32:09Z","receivedAt":"2015-08-13T23:32:09Z","isPatch":true,"sender":{"key":"rappazzo@gmail.com","avatar":"https://avatars.githubusercontent.com/u/525287?v=4"},"body":"I will reroll this series with adjustments as you suggested, and I\nwill add the extra tests for bare repos.\n\nOn Thu, Aug 13, 2015 at 3:23 PM, David Turner <dturner@twopensource.com> wrote:\n> The scope of d can be smaller; move it down to the place I've marked\n> below\n\nI have adjusted the scoping.  I misunderstood the meaning of some\ncomments from the v3 patch, but your statements have helped me\nunderstand.\n\n\n>\n> strbuf_strip_suffix returns 1 if the suffix was stripped and 0\n> otherwise, so there is no need for this strcmp.\n\nDone.\n\n>\n> I'm a little worried about this path manipulation; it looks like the\n> config setting core.bare is the actual thing to check?  (Along with\n> maybe the GIT_WORK_TREE environment var; not sure how that interacts\n> with worktrees).\n\nAs Junio pointed out in a previous version of this patch, the\ncore.bare will always be 'true' since they share the config.  He also\nsuggested that this could be the cause for concern.  Therefore, I can\nuse core.bare to check if the main is a bare repo.  I guess that with\nthe inclusion of tests using a bare repo, that will catch it if things\nchange in the future.\n\n>\n>> +     }\n>> +\n>> +     if (!main_is_bare) {\n>> +             strbuf_addstr(&worktree_path, main_path.buf);\n>> +\n>> +             strbuf_addstr(&main_path, \"/.git\");\n>> +             strbuf_addstr(&worktree_git, main_path.buf);\n>> +\n>> +             ret = fn(worktree_path.buf, worktree_git.buf, cb_data);\n>> +             if (ret)\n>> +                     goto done;\n>> +     }\n>> +     strbuf_addstr(&main_path, \"/worktrees\");\n>\n> Earlier, you said:\n>                 strbuf_addstr(&main_path, common_dir);\n>                 strbuf_strip_suffix(&main_path, \"/.git\");\n>\n> Now you are adding /worktrees.  Doesn't this mean, in the non-bare case,\n> that you'll be looking in $common_dir/worktrees instead of\n> $common_dir/.git/worktrees ?\n\nI read the gitdir file from the common dir.\n\n>> +                     while ((d = readdir(dir)) != NULL) {\n>> +                             if (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n>> +                                     continue;\n>> +\n>> +                             strbuf_reset(&worktree_git);\n>> +                             strbuf_addf(&worktree_git, \"%s/%s/gitdir\", main_path.buf, d->d_name);\n>\n> tioli: main_path never changes, so no need to keep chopping it off and\n> adding it back on; just truncate worktree_git to main_path.len + 1 here\n> and then add d->name.\n\nI will try to clean it up a bit.  I am not very experienced with C,\nbut I will do my best.\n"}]}