Merge branch 'rs/worktree-list-verbose'
`git worktree list` now annotates worktrees as prunable, shows locked and prunable attributes in --porcelain mode, and gained a --verbose option. * rs/worktree-list-verbose: worktree: teach `list` verbose mode worktree: teach `list` to annotate prunable worktree worktree: teach `list --porcelain` to annotate locked worktree t2402: ensure locked worktree is properly cleaned up worktree: teach worktree_lock_reason() to gently handle main worktree worktree: teach worktree to lazy-load "prunable" reason worktree: libify should_prune_worktree()
This commit is contained in:
@ -97,8 +97,9 @@ list::
|
|||||||
List details of each working tree. The main working tree is listed first,
|
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
|
followed by each of the linked working trees. The output details include
|
||||||
whether the working tree is bare, the revision currently checked out, the
|
whether the working tree is bare, the revision currently checked out, the
|
||||||
branch currently checked out (or "detached HEAD" if none), and "locked" if
|
branch currently checked out (or "detached HEAD" if none), "locked" if
|
||||||
the worktree is locked.
|
the worktree is locked, "prunable" if the worktree can be pruned by `prune`
|
||||||
|
command.
|
||||||
|
|
||||||
lock::
|
lock::
|
||||||
|
|
||||||
@ -231,9 +232,14 @@ This can also be set up as the default behaviour by using the
|
|||||||
-v::
|
-v::
|
||||||
--verbose::
|
--verbose::
|
||||||
With `prune`, report all removals.
|
With `prune`, report all removals.
|
||||||
|
+
|
||||||
|
With `list`, output additional information about worktrees (see below).
|
||||||
|
|
||||||
--expire <time>::
|
--expire <time>::
|
||||||
With `prune`, only expire unused working trees older than `<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>::
|
--reason <string>::
|
||||||
With `lock`, an explanation why the working tree is locked.
|
With `lock`, an explanation why the working tree is locked.
|
||||||
@ -372,13 +378,46 @@ $ git worktree list
|
|||||||
/path/to/other-linked-worktree 1234abc (detached HEAD)
|
/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
|
||||||
|
------------
|
||||||
|
|
||||||
|
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
|
Porcelain Format
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
The porcelain format has a line per attribute. Attributes are listed with a
|
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`
|
label and value separated by a single space. Boolean attributes (like `bare`
|
||||||
and `detached`) are listed as a label only, and are present only
|
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
|
if the value is true. Some attributes (like `locked`) can be listed as a label
|
||||||
`worktree`, an empty line indicates the end of the record. For example:
|
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
|
$ git worktree list --porcelain
|
||||||
@ -393,6 +432,33 @@ worktree /path/to/other-linked-worktree
|
|||||||
HEAD 1234abc1234abc1234abc1234abc1234abc1234a
|
HEAD 1234abc1234abc1234abc1234abc1234abc1234a
|
||||||
detached
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
EXAMPLES
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
#include "submodule.h"
|
#include "submodule.h"
|
||||||
#include "utf8.h"
|
#include "utf8.h"
|
||||||
#include "worktree.h"
|
#include "worktree.h"
|
||||||
|
#include "quote.h"
|
||||||
|
|
||||||
static const char * const worktree_usage[] = {
|
static const char * const worktree_usage[] = {
|
||||||
N_("git worktree add [<options>] <path> [<commit-ish>]"),
|
N_("git worktree add [<options>] <path> [<commit-ish>]"),
|
||||||
@ -67,79 +68,6 @@ static void delete_worktrees_dir_if_empty(void)
|
|||||||
rmdir(git_path("worktrees")); /* ignore failed removal */
|
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)
|
static void prune_worktree(const char *id, const char *reason)
|
||||||
{
|
{
|
||||||
if (show_only || verbose)
|
if (show_only || verbose)
|
||||||
@ -195,7 +123,7 @@ static void prune_worktrees(void)
|
|||||||
if (is_dot_or_dotdot(d->d_name))
|
if (is_dot_or_dotdot(d->d_name))
|
||||||
continue;
|
continue;
|
||||||
strbuf_reset(&reason);
|
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);
|
prune_worktree(d->d_name, reason.buf);
|
||||||
else if (path)
|
else if (path)
|
||||||
string_list_append(&kept, path)->util = xstrdup(d->d_name);
|
string_list_append(&kept, path)->util = xstrdup(d->d_name);
|
||||||
@ -642,6 +570,8 @@ static int add(int ac, const char **av, const char *prefix)
|
|||||||
|
|
||||||
static void show_worktree_porcelain(struct worktree *wt)
|
static void show_worktree_porcelain(struct worktree *wt)
|
||||||
{
|
{
|
||||||
|
const char *reason;
|
||||||
|
|
||||||
printf("worktree %s\n", wt->path);
|
printf("worktree %s\n", wt->path);
|
||||||
if (wt->is_bare)
|
if (wt->is_bare)
|
||||||
printf("bare\n");
|
printf("bare\n");
|
||||||
@ -652,6 +582,20 @@ static void show_worktree_porcelain(struct worktree *wt)
|
|||||||
else if (wt->head_ref)
|
else if (wt->head_ref)
|
||||||
printf("branch %s\n", 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");
|
||||||
|
|
||||||
|
reason = worktree_prune_reason(wt, expire);
|
||||||
|
if (reason)
|
||||||
|
printf("prunable %s\n", reason);
|
||||||
|
|
||||||
printf("\n");
|
printf("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,6 +604,7 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
|
|||||||
struct strbuf sb = STRBUF_INIT;
|
struct strbuf sb = STRBUF_INIT;
|
||||||
int cur_path_len = strlen(wt->path);
|
int cur_path_len = strlen(wt->path);
|
||||||
int path_adj = cur_path_len - utf8_strwidth(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);
|
strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path);
|
||||||
if (wt->is_bare)
|
if (wt->is_bare)
|
||||||
@ -677,9 +622,18 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len)
|
|||||||
strbuf_addstr(&sb, "(error)");
|
strbuf_addstr(&sb, "(error)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_main_worktree(wt) && 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");
|
strbuf_addstr(&sb, " locked");
|
||||||
|
|
||||||
|
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);
|
printf("%s\n", sb.buf);
|
||||||
strbuf_release(&sb);
|
strbuf_release(&sb);
|
||||||
}
|
}
|
||||||
@ -723,12 +677,18 @@ static int list(int ac, const char **av, const char *prefix)
|
|||||||
|
|
||||||
struct option options[] = {
|
struct option options[] = {
|
||||||
OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
|
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()
|
OPT_END()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
expire = TIME_MAX;
|
||||||
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
|
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
|
||||||
if (ac)
|
if (ac)
|
||||||
usage_with_options(worktree_usage, options);
|
usage_with_options(worktree_usage, options);
|
||||||
|
else if (verbose && porcelain)
|
||||||
|
die(_("--verbose and --porcelain are mutually exclusive"));
|
||||||
else {
|
else {
|
||||||
struct worktree **worktrees = get_worktrees();
|
struct worktree **worktrees = get_worktrees();
|
||||||
int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
|
int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
|
||||||
|
@ -69,11 +69,107 @@ test_expect_success '"list" all worktrees with locked annotation' '
|
|||||||
git worktree add --detach locked main &&
|
git worktree add --detach locked main &&
|
||||||
git worktree add --detach unlocked main &&
|
git worktree add --detach unlocked main &&
|
||||||
git worktree lock locked &&
|
git worktree lock locked &&
|
||||||
|
test_when_finished "git worktree unlock locked" &&
|
||||||
git worktree list >out &&
|
git worktree list >out &&
|
||||||
grep "/locked *[0-9a-f].* locked$" out &&
|
grep "/locked *[0-9a-f].* locked$" out &&
|
||||||
! grep "/unlocked *[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 &&
|
||||||
|
# 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 '"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 '"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' '
|
test_expect_success 'bare repo setup' '
|
||||||
git init --bare bare1 &&
|
git init --bare bare1 &&
|
||||||
echo "data" >file1 &&
|
echo "data" >file1 &&
|
||||||
|
91
worktree.c
91
worktree.c
@ -15,6 +15,7 @@ void free_worktrees(struct worktree **worktrees)
|
|||||||
free(worktrees[i]->id);
|
free(worktrees[i]->id);
|
||||||
free(worktrees[i]->head_ref);
|
free(worktrees[i]->head_ref);
|
||||||
free(worktrees[i]->lock_reason);
|
free(worktrees[i]->lock_reason);
|
||||||
|
free(worktrees[i]->prune_reason);
|
||||||
free(worktrees[i]);
|
free(worktrees[i]);
|
||||||
}
|
}
|
||||||
free (worktrees);
|
free (worktrees);
|
||||||
@ -224,7 +225,8 @@ int is_main_worktree(const struct worktree *wt)
|
|||||||
|
|
||||||
const char *worktree_lock_reason(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) {
|
if (!wt->lock_reason_valid) {
|
||||||
struct strbuf path = STRBUF_INIT;
|
struct strbuf path = STRBUF_INIT;
|
||||||
@ -245,6 +247,25 @@ const char *worktree_lock_reason(struct worktree *wt)
|
|||||||
return wt->lock_reason;
|
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 */
|
/* convenient wrapper to deal with NULL strbuf */
|
||||||
static void strbuf_addf_gently(struct strbuf *buf, const char *fmt, ...)
|
static void strbuf_addf_gently(struct strbuf *buf, const char *fmt, ...)
|
||||||
{
|
{
|
||||||
@ -741,3 +762,71 @@ done:
|
|||||||
strbuf_release(&realdotgit);
|
strbuf_release(&realdotgit);
|
||||||
strbuf_release(&dotgit);
|
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;
|
||||||
|
}
|
||||||
|
23
worktree.h
23
worktree.h
@ -11,11 +11,13 @@ struct worktree {
|
|||||||
char *id;
|
char *id;
|
||||||
char *head_ref; /* NULL if HEAD is broken or detached */
|
char *head_ref; /* NULL if HEAD is broken or detached */
|
||||||
char *lock_reason; /* private - use worktree_lock_reason */
|
char *lock_reason; /* private - use worktree_lock_reason */
|
||||||
|
char *prune_reason; /* private - use worktree_prune_reason */
|
||||||
struct object_id head_oid;
|
struct object_id head_oid;
|
||||||
int is_detached;
|
int is_detached;
|
||||||
int is_bare;
|
int is_bare;
|
||||||
int is_current;
|
int is_current;
|
||||||
int lock_reason_valid; /* private */
|
int lock_reason_valid; /* private */
|
||||||
|
int prune_reason_valid; /* private */
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -73,6 +75,27 @@ int is_main_worktree(const struct worktree *wt);
|
|||||||
*/
|
*/
|
||||||
const char *worktree_lock_reason(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
|
||||||
|
* 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)
|
#define WT_VALIDATE_WORKTREE_MISSING_OK (1 << 0)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
Reference in New Issue
Block a user