Merge branch 'mr/worktree-list'

Add the "list" subcommand to "git worktree".

* mr/worktree-list:
  worktree: add 'list' command
  worktree: add details to the worktree struct
  worktree: add a function to get worktree details
  worktree: refactor find_linked_symref function
  worktree: add top-level worktree.c
This commit is contained in:
Junio C Hamano 2015-10-26 15:55:16 -07:00
commit a46dcfb840
9 changed files with 485 additions and 88 deletions

View File

@ -11,6 +11,7 @@ SYNOPSIS
[verse] [verse]
'git worktree add' [-f] [--detach] [-b <new-branch>] <path> [<branch>] 'git worktree add' [-f] [--detach] [-b <new-branch>] <path> [<branch>]
'git worktree prune' [-n] [-v] [--expire <expire>] 'git worktree prune' [-n] [-v] [--expire <expire>]
'git worktree list' [--porcelain]
DESCRIPTION DESCRIPTION
----------- -----------
@ -59,6 +60,13 @@ prune::
Prune working tree information in $GIT_DIR/worktrees. Prune working tree information in $GIT_DIR/worktrees.
list::
List details of each worktree. The main worktree is listed first, followed by
each of the linked worktrees. The output details include if the worktree is
bare, the revision currently checked out, and the branch currently checked out
(or 'detached HEAD' if none).
OPTIONS OPTIONS
------- -------
@ -86,6 +94,11 @@ OPTIONS
With `prune`, do not remove anything; just report what it would With `prune`, do not remove anything; just report what it would
remove. remove.
--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.
-v:: -v::
--verbose:: --verbose::
With `prune`, report all removals. With `prune`, report all removals.
@ -134,6 +147,41 @@ to `/path/main/.git/worktrees/test-next` then a file named
`test-next` entry from being pruned. See `test-next` entry from being pruned. See
linkgit:gitrepository-layout[5] for details. linkgit:gitrepository-layout[5] for details.
LIST OUTPUT FORMAT
------------------
The worktree list command has two output formats. The default format shows the
details on a single line with columns. For example:
------------
S git worktree list
/path/to/bare-source (bare)
/path/to/linked-worktree abcd1234 [master]
/path/to/other-linked-worktree 1234abc (detached HEAD)
------------
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 only present if and only
if the value is true. An empty line indicates the end of a worktree. For
example:
------------
S git worktree list --porcelain
worktree /path/to/bare-source
bare
worktree /path/to/linked-worktree
HEAD abcd1234abcd1234abcd1234abcd1234abcd1234
branch refs/heads/master
worktree /path/to/other-linked-worktree
HEAD 1234abc1234abc1234abc1234abc1234abc1234a
detached
------------
EXAMPLES EXAMPLES
-------- --------
You are in the middle of a refactoring session and your boss comes in and You are in the middle of a refactoring session and your boss comes in and
@ -167,7 +215,6 @@ performed manually, such as:
- `remove` to remove a linked working tree and its administrative files (and - `remove` to remove a linked working tree and its administrative files (and
warn if the working tree is dirty) warn if the working tree is dirty)
- `mv` to move or rename a working tree and update its administrative files - `mv` to move or rename a working tree and update its administrative files
- `list` to list linked working trees
- `lock` to prevent automatic pruning of administrative files (for instance, - `lock` to prevent automatic pruning of administrative files (for instance,
for a working tree on a portable device) for a working tree on a portable device)

View File

@ -808,6 +808,7 @@ LIB_OBJS += version.o
LIB_OBJS += versioncmp.o LIB_OBJS += versioncmp.o
LIB_OBJS += walker.o LIB_OBJS += walker.o
LIB_OBJS += wildmatch.o LIB_OBJS += wildmatch.o
LIB_OBJS += worktree.o
LIB_OBJS += wrapper.o LIB_OBJS += wrapper.o
LIB_OBJS += write_or_die.o LIB_OBJS += write_or_die.o
LIB_OBJS += ws.o LIB_OBJS += ws.o

View File

@ -4,6 +4,7 @@
#include "refs.h" #include "refs.h"
#include "remote.h" #include "remote.h"
#include "commit.h" #include "commit.h"
#include "worktree.h"
struct tracking { struct tracking {
struct refspec spec; struct refspec spec;
@ -311,84 +312,6 @@ void remove_branch_state(void)
unlink(git_path_squash_msg()); unlink(git_path_squash_msg());
} }
static char *find_linked_symref(const char *symref, const char *branch,
const char *id)
{
struct strbuf sb = STRBUF_INIT;
struct strbuf path = STRBUF_INIT;
struct strbuf gitdir = STRBUF_INIT;
char *existing = NULL;
/*
* $GIT_COMMON_DIR/$symref (e.g. HEAD) is practically outside
* $GIT_DIR so resolve_ref_unsafe() won't work (it uses
* git_path). Parse the ref ourselves.
*/
if (id)
strbuf_addf(&path, "%s/worktrees/%s/%s", get_git_common_dir(), id, symref);
else
strbuf_addf(&path, "%s/%s", get_git_common_dir(), symref);
if (!strbuf_readlink(&sb, path.buf, 0)) {
if (!starts_with(sb.buf, "refs/") ||
check_refname_format(sb.buf, 0))
goto done;
} else if (strbuf_read_file(&sb, path.buf, 0) >= 0 &&
starts_with(sb.buf, "ref:")) {
strbuf_remove(&sb, 0, strlen("ref:"));
strbuf_trim(&sb);
} else
goto done;
if (strcmp(sb.buf, branch))
goto done;
if (id) {
strbuf_reset(&path);
strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id);
if (strbuf_read_file(&gitdir, path.buf, 0) <= 0)
goto done;
strbuf_rtrim(&gitdir);
} else
strbuf_addstr(&gitdir, get_git_common_dir());
strbuf_strip_suffix(&gitdir, ".git");
existing = strbuf_detach(&gitdir, NULL);
done:
strbuf_release(&path);
strbuf_release(&sb);
strbuf_release(&gitdir);
return existing;
}
char *find_shared_symref(const char *symref, const char *target)
{
struct strbuf path = STRBUF_INIT;
DIR *dir;
struct dirent *d;
char *existing;
if ((existing = find_linked_symref(symref, target, NULL)))
return existing;
strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
dir = opendir(path.buf);
strbuf_release(&path);
if (!dir)
return NULL;
while ((d = readdir(dir)) != NULL) {
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
continue;
existing = find_linked_symref(symref, target, d->d_name);
if (existing)
goto done;
}
done:
closedir(dir);
return existing;
}
void die_if_checked_out(const char *branch) void die_if_checked_out(const char *branch)
{ {
char *existing; char *existing;

View File

@ -59,12 +59,4 @@ extern int read_branch_desc(struct strbuf *, const char *branch_name);
*/ */
extern void die_if_checked_out(const char *branch); extern void die_if_checked_out(const char *branch);
/*
* Check if a per-worktree symref points to a ref in the main worktree
* or any linked worktree, and return the path to the exising worktree
* if it is. Returns NULL if there is no existing ref. The caller is
* responsible for freeing the returned path.
*/
extern char *find_shared_symref(const char *symref, const char *target);
#endif #endif

View File

@ -19,7 +19,7 @@
#include "string-list.h" #include "string-list.h"
#include "notes-merge.h" #include "notes-merge.h"
#include "notes-utils.h" #include "notes-utils.h"
#include "branch.h" #include "worktree.h"
static const char * const git_notes_usage[] = { static const char * const git_notes_usage[] = {
N_("git notes [--ref <notes-ref>] [list [<object>]]"), N_("git notes [--ref <notes-ref>] [list [<object>]]"),

View File

@ -8,10 +8,13 @@
#include "run-command.h" #include "run-command.h"
#include "sigchain.h" #include "sigchain.h"
#include "refs.h" #include "refs.h"
#include "utf8.h"
#include "worktree.h"
static const char * const worktree_usage[] = { static const char * const worktree_usage[] = {
N_("git worktree add [<options>] <path> <branch>"), N_("git worktree add [<options>] <path> <branch>"),
N_("git worktree prune [<options>]"), N_("git worktree prune [<options>]"),
N_("git worktree list [<options>]"),
NULL NULL
}; };
@ -359,6 +362,89 @@ static int add(int ac, const char **av, const char *prefix)
return add_worktree(path, branch, &opts); return add_worktree(path, branch, &opts);
} }
static void show_worktree_porcelain(struct worktree *wt)
{
printf("worktree %s\n", wt->path);
if (wt->is_bare)
printf("bare\n");
else {
printf("HEAD %s\n", sha1_to_hex(wt->head_sha1));
if (wt->is_detached)
printf("detached\n");
else
printf("branch %s\n", wt->head_ref);
}
printf("\n");
}
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);
strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
if (wt->is_bare)
strbuf_addstr(&sb, "(bare)");
else {
strbuf_addf(&sb, "%-*s ", abbrev_len,
find_unique_abbrev(wt->head_sha1, DEFAULT_ABBREV));
if (!wt->is_detached)
strbuf_addf(&sb, "[%s]", shorten_unambiguous_ref(wt->head_ref, 0));
else
strbuf_addstr(&sb, "(detached HEAD)");
}
printf("%s\n", sb.buf);
strbuf_release(&sb);
}
static void measure_widths(struct worktree **wt, int *abbrev, int *maxlen)
{
int i;
for (i = 0; wt[i]; i++) {
int sha1_len;
int path_len = strlen(wt[i]->path);
if (path_len > *maxlen)
*maxlen = path_len;
sha1_len = strlen(find_unique_abbrev(wt[i]->head_sha1, *abbrev));
if (sha1_len > *abbrev)
*abbrev = sha1_len;
}
}
static int list(int ac, const char **av, const char *prefix)
{
int porcelain = 0;
struct option options[] = {
OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
OPT_END()
};
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
if (ac)
usage_with_options(worktree_usage, options);
else {
struct worktree **worktrees = get_worktrees();
int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
if (!porcelain)
measure_widths(worktrees, &abbrev, &path_maxlen);
for (i = 0; worktrees[i]; i++) {
if (porcelain)
show_worktree_porcelain(worktrees[i]);
else
show_worktree(worktrees[i], path_maxlen, abbrev);
}
free_worktrees(worktrees);
}
return 0;
}
int cmd_worktree(int ac, const char **av, const char *prefix) int cmd_worktree(int ac, const char **av, const char *prefix)
{ {
struct option options[] = { struct option options[] = {
@ -371,5 +457,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
return add(ac - 1, av + 1, prefix); return add(ac - 1, av + 1, prefix);
if (!strcmp(av[1], "prune")) if (!strcmp(av[1], "prune"))
return prune(ac - 1, av + 1, prefix); return prune(ac - 1, av + 1, prefix);
if (!strcmp(av[1], "list"))
return list(ac - 1, av + 1, prefix);
usage_with_options(worktree_usage, options); usage_with_options(worktree_usage, options);
} }

89
t/t2027-worktree-list.sh Executable file
View File

@ -0,0 +1,89 @@
#!/bin/sh
test_description='test git worktree list'
. ./test-lib.sh
test_expect_success 'setup' '
test_commit init
'
test_expect_success '"list" all worktrees from main' '
echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect &&
test_when_finished "rm -rf here && git worktree prune" &&
git worktree add --detach here master &&
echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect &&
git worktree list | sed "s/ */ /g" >actual &&
test_cmp expect actual
'
test_expect_success '"list" all worktrees from linked' '
echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect &&
test_when_finished "rm -rf here && git worktree prune" &&
git worktree add --detach here master &&
echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect &&
git -C here worktree list | sed "s/ */ /g" >actual &&
test_cmp expect actual
'
test_expect_success '"list" all worktrees --porcelain' '
echo "worktree $(git rev-parse --show-toplevel)" >expect &&
echo "HEAD $(git rev-parse HEAD)" >>expect &&
echo "branch $(git symbolic-ref HEAD)" >>expect &&
echo >>expect &&
test_when_finished "rm -rf here && git worktree prune" &&
git worktree add --detach here master &&
echo "worktree $(git -C here rev-parse --show-toplevel)" >>expect &&
echo "HEAD $(git rev-parse HEAD)" >>expect &&
echo "detached" >>expect &&
echo >>expect &&
git worktree list --porcelain >actual &&
test_cmp expect actual
'
test_expect_success 'bare repo setup' '
git init --bare bare1 &&
echo "data" >file1 &&
git add file1 &&
git commit -m"File1: add data" &&
git push bare1 master &&
git reset --hard HEAD^
'
test_expect_success '"list" all worktrees from bare main' '
test_when_finished "rm -rf there && git -C bare1 worktree prune" &&
git -C bare1 worktree add --detach ../there master &&
echo "$(pwd)/bare1 (bare)" >expect &&
echo "$(git -C there rev-parse --show-toplevel) $(git -C there rev-parse --short HEAD) (detached HEAD)" >>expect &&
git -C bare1 worktree list | sed "s/ */ /g" >actual &&
test_cmp expect actual
'
test_expect_success '"list" all worktrees --porcelain from bare main' '
test_when_finished "rm -rf there && git -C bare1 worktree prune" &&
git -C bare1 worktree add --detach ../there master &&
echo "worktree $(pwd)/bare1" >expect &&
echo "bare" >>expect &&
echo >>expect &&
echo "worktree $(git -C there rev-parse --show-toplevel)" >>expect &&
echo "HEAD $(git -C there rev-parse HEAD)" >>expect &&
echo "detached" >>expect &&
echo >>expect &&
git -C bare1 worktree list --porcelain >actual &&
test_cmp expect actual
'
test_expect_success '"list" all worktrees from linked with a bare main' '
test_when_finished "rm -rf there && git -C bare1 worktree prune" &&
git -C bare1 worktree add --detach ../there master &&
echo "$(pwd)/bare1 (bare)" >expect &&
echo "$(git -C there rev-parse --show-toplevel) $(git -C there rev-parse --short HEAD) (detached HEAD)" >>expect &&
git -C there worktree list | sed "s/ */ /g" >actual &&
test_cmp expect actual
'
test_expect_success 'bare repo cleanup' '
rm -rf bare1
'
test_done

219
worktree.c Normal file
View File

@ -0,0 +1,219 @@
#include "cache.h"
#include "refs.h"
#include "strbuf.h"
#include "worktree.h"
void free_worktrees(struct worktree **worktrees)
{
int i = 0;
for (i = 0; worktrees[i]; i++) {
free(worktrees[i]->path);
free(worktrees[i]->git_dir);
free(worktrees[i]->head_ref);
free(worktrees[i]);
}
free (worktrees);
}
/*
* read 'path_to_ref' into 'ref'. Also if is_detached is not NULL,
* set is_detached to 1 (0) if the ref is detatched (is not detached).
*
* $GIT_COMMON_DIR/$symref (e.g. HEAD) is practically outside $GIT_DIR so
* for linked worktrees, `resolve_ref_unsafe()` won't work (it uses
* git_path). Parse the ref ourselves.
*
* return -1 if the ref is not a proper ref, 0 otherwise (success)
*/
static int parse_ref(char *path_to_ref, struct strbuf *ref, int *is_detached)
{
if (is_detached)
*is_detached = 0;
if (!strbuf_readlink(ref, path_to_ref, 0)) {
/* HEAD is symbolic link */
if (!starts_with(ref->buf, "refs/") ||
check_refname_format(ref->buf, 0))
return -1;
} else if (strbuf_read_file(ref, path_to_ref, 0) >= 0) {
/* textual symref or detached */
if (!starts_with(ref->buf, "ref:")) {
if (is_detached)
*is_detached = 1;
} else {
strbuf_remove(ref, 0, strlen("ref:"));
strbuf_trim(ref);
if (check_refname_format(ref->buf, 0))
return -1;
}
} else
return -1;
return 0;
}
/**
* Add the head_sha1 and head_ref (if not detached) to the given worktree
*/
static void add_head_info(struct strbuf *head_ref, struct worktree *worktree)
{
if (head_ref->len) {
if (worktree->is_detached) {
get_sha1_hex(head_ref->buf, worktree->head_sha1);
} else {
resolve_ref_unsafe(head_ref->buf, 0, worktree->head_sha1, NULL);
worktree->head_ref = strbuf_detach(head_ref, NULL);
}
}
}
/**
* get the main worktree
*/
static struct worktree *get_main_worktree(void)
{
struct worktree *worktree = NULL;
struct strbuf path = STRBUF_INIT;
struct strbuf worktree_path = STRBUF_INIT;
struct strbuf gitdir = STRBUF_INIT;
struct strbuf head_ref = STRBUF_INIT;
int is_bare = 0;
int is_detached = 0;
strbuf_addf(&gitdir, "%s", absolute_path(get_git_common_dir()));
strbuf_addbuf(&worktree_path, &gitdir);
is_bare = !strbuf_strip_suffix(&worktree_path, "/.git");
if (is_bare)
strbuf_strip_suffix(&worktree_path, "/.");
strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
if (parse_ref(path.buf, &head_ref, &is_detached) < 0)
goto done;
worktree = xmalloc(sizeof(struct worktree));
worktree->path = strbuf_detach(&worktree_path, NULL);
worktree->git_dir = strbuf_detach(&gitdir, NULL);
worktree->is_bare = is_bare;
worktree->head_ref = NULL;
worktree->is_detached = is_detached;
add_head_info(&head_ref, worktree);
done:
strbuf_release(&path);
strbuf_release(&gitdir);
strbuf_release(&worktree_path);
strbuf_release(&head_ref);
return worktree;
}
static struct worktree *get_linked_worktree(const char *id)
{
struct worktree *worktree = NULL;
struct strbuf path = STRBUF_INIT;
struct strbuf worktree_path = STRBUF_INIT;
struct strbuf gitdir = STRBUF_INIT;
struct strbuf head_ref = STRBUF_INIT;
int is_detached = 0;
if (!id)
die("Missing linked worktree name");
strbuf_addf(&gitdir, "%s/worktrees/%s",
absolute_path(get_git_common_dir()), id);
strbuf_addf(&path, "%s/gitdir", gitdir.buf);
if (strbuf_read_file(&worktree_path, path.buf, 0) <= 0)
/* invalid gitdir file */
goto done;
strbuf_rtrim(&worktree_path);
if (!strbuf_strip_suffix(&worktree_path, "/.git")) {
strbuf_reset(&worktree_path);
strbuf_addstr(&worktree_path, absolute_path("."));
strbuf_strip_suffix(&worktree_path, "/.");
}
strbuf_reset(&path);
strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
if (parse_ref(path.buf, &head_ref, &is_detached) < 0)
goto done;
worktree = xmalloc(sizeof(struct worktree));
worktree->path = strbuf_detach(&worktree_path, NULL);
worktree->git_dir = strbuf_detach(&gitdir, NULL);
worktree->is_bare = 0;
worktree->head_ref = NULL;
worktree->is_detached = is_detached;
add_head_info(&head_ref, worktree);
done:
strbuf_release(&path);
strbuf_release(&gitdir);
strbuf_release(&worktree_path);
strbuf_release(&head_ref);
return worktree;
}
struct worktree **get_worktrees(void)
{
struct worktree **list = NULL;
struct strbuf path = STRBUF_INIT;
DIR *dir;
struct dirent *d;
int counter = 0, alloc = 2;
list = xmalloc(alloc * sizeof(struct worktree *));
if ((list[counter] = get_main_worktree()))
counter++;
strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
dir = opendir(path.buf);
strbuf_release(&path);
if (dir) {
while ((d = readdir(dir)) != NULL) {
struct worktree *linked = NULL;
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
continue;
if ((linked = get_linked_worktree(d->d_name))) {
ALLOC_GROW(list, counter + 1, alloc);
list[counter++] = linked;
}
}
closedir(dir);
}
ALLOC_GROW(list, counter + 1, alloc);
list[counter] = NULL;
return list;
}
char *find_shared_symref(const char *symref, const char *target)
{
char *existing = NULL;
struct strbuf path = STRBUF_INIT;
struct strbuf sb = STRBUF_INIT;
struct worktree **worktrees = get_worktrees();
int i = 0;
for (i = 0; worktrees[i]; i++) {
strbuf_reset(&path);
strbuf_reset(&sb);
strbuf_addf(&path, "%s/%s", worktrees[i]->git_dir, symref);
if (parse_ref(path.buf, &sb, NULL)) {
continue;
}
if (!strcmp(sb.buf, target)) {
existing = xstrdup(worktrees[i]->path);
break;
}
}
strbuf_release(&path);
strbuf_release(&sb);
free_worktrees(worktrees);
return existing;
}

38
worktree.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef WORKTREE_H
#define WORKTREE_H
struct worktree {
char *path;
char *git_dir;
char *head_ref;
unsigned char head_sha1[20];
int is_detached;
int is_bare;
};
/* Functions for acting on the information about worktrees. */
/*
* Get the worktrees. The primary worktree will always be the first returned,
* and linked worktrees will be pointed to by 'next' in each subsequent
* worktree. No specific ordering is done on the linked worktrees.
*
* The caller is responsible for freeing the memory from the returned
* worktree(s).
*/
extern struct worktree **get_worktrees(void);
/*
* Free up the memory for worktree(s)
*/
extern void free_worktrees(struct worktree **);
/*
* Check if a per-worktree symref points to a ref in the main worktree
* or any linked worktree, and return the path to the exising worktree
* if it is. Returns NULL if there is no existing ref. The caller is
* responsible for freeing the returned path.
*/
extern char *find_shared_symref(const char *symref, const char *target);
#endif