Merge branch 'nb/branch-show-other-worktrees-head'
"git branch --list" learned to show branches that are checked out in other worktrees connected to the same repository prefixed with '+', similar to the way the currently checked out branch is shown with '*' in front. * nb/branch-show-other-worktrees-head: branch: add worktree info on verbose output branch: update output to include worktree info ref-filter: add worktreepath atom
This commit is contained in:
@ -26,8 +26,10 @@ DESCRIPTION
|
|||||||
-----------
|
-----------
|
||||||
|
|
||||||
If `--list` is given, or if there are no non-option arguments, existing
|
If `--list` is given, or if there are no non-option arguments, existing
|
||||||
branches are listed; the current branch will be highlighted with an
|
branches are listed; the current branch will be highlighted in green and
|
||||||
asterisk. Option `-r` causes the remote-tracking branches to be listed,
|
marked with an asterisk. Any branches checked out in linked worktrees will
|
||||||
|
be highlighted in cyan and marked with a plus sign. Option `-r` causes the
|
||||||
|
remote-tracking branches to be listed,
|
||||||
and option `-a` shows both local and remote branches. If a `<pattern>`
|
and option `-a` shows both local and remote branches. If a `<pattern>`
|
||||||
is given, it is used as a shell wildcard to restrict the output to
|
is given, it is used as a shell wildcard to restrict the output to
|
||||||
matching branches. If multiple patterns are given, a branch is shown if
|
matching branches. If multiple patterns are given, a branch is shown if
|
||||||
@ -174,8 +176,10 @@ This option is only applicable in non-verbose mode.
|
|||||||
When in list mode,
|
When in list mode,
|
||||||
show sha1 and commit subject line for each head, along with
|
show sha1 and commit subject line for each head, along with
|
||||||
relationship to upstream branch (if any). If given twice, print
|
relationship to upstream branch (if any). If given twice, print
|
||||||
the name of the upstream branch, as well (see also `git remote
|
the path of the linked worktree (if any) and the name of the upstream
|
||||||
show <remote>`).
|
branch, as well (see also `git remote show <remote>`). Note that the
|
||||||
|
current worktree's HEAD will not have its path printed (it will always
|
||||||
|
be your current directory).
|
||||||
|
|
||||||
-q::
|
-q::
|
||||||
--quiet::
|
--quiet::
|
||||||
|
@ -214,6 +214,11 @@ symref::
|
|||||||
`:lstrip` and `:rstrip` options in the same way as `refname`
|
`:lstrip` and `:rstrip` options in the same way as `refname`
|
||||||
above.
|
above.
|
||||||
|
|
||||||
|
worktreepath::
|
||||||
|
The absolute path to the worktree in which the ref is checked
|
||||||
|
out, if it is checked out in any linked worktree. Empty string
|
||||||
|
otherwise.
|
||||||
|
|
||||||
In addition to the above, for commit and tag objects, the header
|
In addition to the above, for commit and tag objects, the header
|
||||||
field names (`tree`, `parent`, `object`, `type`, and `tag`) can
|
field names (`tree`, `parent`, `object`, `type`, and `tag`) can
|
||||||
be used to specify the value in the header field.
|
be used to specify the value in the header field.
|
||||||
|
@ -47,6 +47,7 @@ static char branch_colors[][COLOR_MAXLEN] = {
|
|||||||
GIT_COLOR_NORMAL, /* LOCAL */
|
GIT_COLOR_NORMAL, /* LOCAL */
|
||||||
GIT_COLOR_GREEN, /* CURRENT */
|
GIT_COLOR_GREEN, /* CURRENT */
|
||||||
GIT_COLOR_BLUE, /* UPSTREAM */
|
GIT_COLOR_BLUE, /* UPSTREAM */
|
||||||
|
GIT_COLOR_CYAN, /* WORKTREE */
|
||||||
};
|
};
|
||||||
enum color_branch {
|
enum color_branch {
|
||||||
BRANCH_COLOR_RESET = 0,
|
BRANCH_COLOR_RESET = 0,
|
||||||
@ -54,7 +55,8 @@ enum color_branch {
|
|||||||
BRANCH_COLOR_REMOTE = 2,
|
BRANCH_COLOR_REMOTE = 2,
|
||||||
BRANCH_COLOR_LOCAL = 3,
|
BRANCH_COLOR_LOCAL = 3,
|
||||||
BRANCH_COLOR_CURRENT = 4,
|
BRANCH_COLOR_CURRENT = 4,
|
||||||
BRANCH_COLOR_UPSTREAM = 5
|
BRANCH_COLOR_UPSTREAM = 5,
|
||||||
|
BRANCH_COLOR_WORKTREE = 6
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *color_branch_slots[] = {
|
static const char *color_branch_slots[] = {
|
||||||
@ -64,6 +66,7 @@ static const char *color_branch_slots[] = {
|
|||||||
[BRANCH_COLOR_LOCAL] = "local",
|
[BRANCH_COLOR_LOCAL] = "local",
|
||||||
[BRANCH_COLOR_CURRENT] = "current",
|
[BRANCH_COLOR_CURRENT] = "current",
|
||||||
[BRANCH_COLOR_UPSTREAM] = "upstream",
|
[BRANCH_COLOR_UPSTREAM] = "upstream",
|
||||||
|
[BRANCH_COLOR_WORKTREE] = "worktree",
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct string_list output = STRING_LIST_INIT_DUP;
|
static struct string_list output = STRING_LIST_INIT_DUP;
|
||||||
@ -342,9 +345,10 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
|
|||||||
struct strbuf local = STRBUF_INIT;
|
struct strbuf local = STRBUF_INIT;
|
||||||
struct strbuf remote = STRBUF_INIT;
|
struct strbuf remote = STRBUF_INIT;
|
||||||
|
|
||||||
strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else) %s%%(end)",
|
strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
|
||||||
branch_get_color(BRANCH_COLOR_CURRENT),
|
branch_get_color(BRANCH_COLOR_CURRENT),
|
||||||
branch_get_color(BRANCH_COLOR_LOCAL));
|
branch_get_color(BRANCH_COLOR_WORKTREE),
|
||||||
|
branch_get_color(BRANCH_COLOR_LOCAL));
|
||||||
strbuf_addf(&remote, " %s",
|
strbuf_addf(&remote, " %s",
|
||||||
branch_get_color(BRANCH_COLOR_REMOTE));
|
branch_get_color(BRANCH_COLOR_REMOTE));
|
||||||
|
|
||||||
@ -363,9 +367,13 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
|
|||||||
strbuf_addf(&local, " %s ", obname.buf);
|
strbuf_addf(&local, " %s ", obname.buf);
|
||||||
|
|
||||||
if (filter->verbose > 1)
|
if (filter->verbose > 1)
|
||||||
|
{
|
||||||
|
strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
|
||||||
|
branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
|
||||||
strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
|
strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
|
||||||
"%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
|
"%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
|
||||||
branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));
|
branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
strbuf_addf(&local, "%%(if)%%(upstream:track)%%(then)%%(upstream:track) %%(end)%%(contents:subject)");
|
strbuf_addf(&local, "%%(if)%%(upstream:track)%%(then)%%(upstream:track) %%(end)%%(contents:subject)");
|
||||||
|
|
||||||
|
78
ref-filter.c
78
ref-filter.c
@ -20,6 +20,8 @@
|
|||||||
#include "commit-slab.h"
|
#include "commit-slab.h"
|
||||||
#include "commit-graph.h"
|
#include "commit-graph.h"
|
||||||
#include "commit-reach.h"
|
#include "commit-reach.h"
|
||||||
|
#include "worktree.h"
|
||||||
|
#include "hashmap.h"
|
||||||
|
|
||||||
static struct ref_msg {
|
static struct ref_msg {
|
||||||
const char *gone;
|
const char *gone;
|
||||||
@ -75,6 +77,27 @@ static struct expand_data {
|
|||||||
struct object_info info;
|
struct object_info info;
|
||||||
} oi, oi_deref;
|
} oi, oi_deref;
|
||||||
|
|
||||||
|
struct ref_to_worktree_entry {
|
||||||
|
struct hashmap_entry ent; /* must be the first member! */
|
||||||
|
struct worktree *wt; /* key is wt->head_ref */
|
||||||
|
};
|
||||||
|
|
||||||
|
static int ref_to_worktree_map_cmpfnc(const void *unused_lookupdata,
|
||||||
|
const void *existing_hashmap_entry_to_test,
|
||||||
|
const void *key,
|
||||||
|
const void *keydata_aka_refname)
|
||||||
|
{
|
||||||
|
const struct ref_to_worktree_entry *e = existing_hashmap_entry_to_test;
|
||||||
|
const struct ref_to_worktree_entry *k = key;
|
||||||
|
return strcmp(e->wt->head_ref,
|
||||||
|
keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct ref_to_worktree_map {
|
||||||
|
struct hashmap map;
|
||||||
|
struct worktree **worktrees;
|
||||||
|
} ref_to_worktree_map;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* An atom is a valid field atom listed below, possibly prefixed with
|
* An atom is a valid field atom listed below, possibly prefixed with
|
||||||
* a "*" to denote deref_tag().
|
* a "*" to denote deref_tag().
|
||||||
@ -480,6 +503,7 @@ static struct {
|
|||||||
{ "flag", SOURCE_NONE },
|
{ "flag", SOURCE_NONE },
|
||||||
{ "HEAD", SOURCE_NONE, FIELD_STR, head_atom_parser },
|
{ "HEAD", SOURCE_NONE, FIELD_STR, head_atom_parser },
|
||||||
{ "color", SOURCE_NONE, FIELD_STR, color_atom_parser },
|
{ "color", SOURCE_NONE, FIELD_STR, color_atom_parser },
|
||||||
|
{ "worktreepath", SOURCE_NONE },
|
||||||
{ "align", SOURCE_NONE, FIELD_STR, align_atom_parser },
|
{ "align", SOURCE_NONE, FIELD_STR, align_atom_parser },
|
||||||
{ "end", SOURCE_NONE },
|
{ "end", SOURCE_NONE },
|
||||||
{ "if", SOURCE_NONE, FIELD_STR, if_atom_parser },
|
{ "if", SOURCE_NONE, FIELD_STR, if_atom_parser },
|
||||||
@ -1531,6 +1555,48 @@ static int get_object(struct ref_array_item *ref, int deref, struct object **obj
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; worktrees[i]; i++) {
|
||||||
|
if (worktrees[i]->head_ref) {
|
||||||
|
struct ref_to_worktree_entry *entry;
|
||||||
|
entry = xmalloc(sizeof(*entry));
|
||||||
|
entry->wt = worktrees[i];
|
||||||
|
hashmap_entry_init(entry, strhash(worktrees[i]->head_ref));
|
||||||
|
|
||||||
|
hashmap_add(map, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void lazy_init_worktree_map(void)
|
||||||
|
{
|
||||||
|
if (ref_to_worktree_map.worktrees)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ref_to_worktree_map.worktrees = get_worktrees(0);
|
||||||
|
hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
|
||||||
|
populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *get_worktree_path(const struct used_atom *atom, const struct ref_array_item *ref)
|
||||||
|
{
|
||||||
|
struct hashmap_entry entry;
|
||||||
|
struct ref_to_worktree_entry *lookup_result;
|
||||||
|
|
||||||
|
lazy_init_worktree_map();
|
||||||
|
|
||||||
|
hashmap_entry_init(&entry, strhash(ref->refname));
|
||||||
|
lookup_result = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);
|
||||||
|
|
||||||
|
if (lookup_result)
|
||||||
|
return xstrdup(lookup_result->wt->path);
|
||||||
|
else
|
||||||
|
return xstrdup("");
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Parse the object referred by ref, and grab needed value.
|
* Parse the object referred by ref, and grab needed value.
|
||||||
*/
|
*/
|
||||||
@ -1568,6 +1634,13 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
|
|||||||
|
|
||||||
if (starts_with(name, "refname"))
|
if (starts_with(name, "refname"))
|
||||||
refname = get_refname(atom, ref);
|
refname = get_refname(atom, ref);
|
||||||
|
else if (!strcmp(name, "worktreepath")) {
|
||||||
|
if (ref->kind == FILTER_REFS_BRANCHES)
|
||||||
|
v->s = get_worktree_path(atom, ref);
|
||||||
|
else
|
||||||
|
v->s = xstrdup("");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
else if (starts_with(name, "symref"))
|
else if (starts_with(name, "symref"))
|
||||||
refname = get_symref(atom, ref);
|
refname = get_symref(atom, ref);
|
||||||
else if (starts_with(name, "upstream")) {
|
else if (starts_with(name, "upstream")) {
|
||||||
@ -2051,6 +2124,11 @@ void ref_array_clear(struct ref_array *array)
|
|||||||
free_array_item(array->items[i]);
|
free_array_item(array->items[i]);
|
||||||
FREE_AND_NULL(array->items);
|
FREE_AND_NULL(array->items);
|
||||||
array->nr = array->alloc = 0;
|
array->nr = array->alloc = 0;
|
||||||
|
if (ref_to_worktree_map.worktrees) {
|
||||||
|
hashmap_free(&(ref_to_worktree_map.map), 1);
|
||||||
|
free_worktrees(ref_to_worktree_map.worktrees);
|
||||||
|
ref_to_worktree_map.worktrees = NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata)
|
static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata)
|
||||||
|
@ -206,18 +206,22 @@ test_expect_success 'git branch -M baz bam should succeed when baz is checked ou
|
|||||||
git worktree add -f bazdir2 baz &&
|
git worktree add -f bazdir2 baz &&
|
||||||
git branch -M baz bam &&
|
git branch -M baz bam &&
|
||||||
test $(git -C bazdir rev-parse --abbrev-ref HEAD) = bam &&
|
test $(git -C bazdir rev-parse --abbrev-ref HEAD) = bam &&
|
||||||
test $(git -C bazdir2 rev-parse --abbrev-ref HEAD) = bam
|
test $(git -C bazdir2 rev-parse --abbrev-ref HEAD) = bam &&
|
||||||
|
rm -r bazdir bazdir2 &&
|
||||||
|
git worktree prune
|
||||||
'
|
'
|
||||||
|
|
||||||
test_expect_success 'git branch -M baz bam should succeed within a worktree in which baz is checked out' '
|
test_expect_success 'git branch -M baz bam should succeed within a worktree in which baz is checked out' '
|
||||||
git checkout -b baz &&
|
git checkout -b baz &&
|
||||||
git worktree add -f bazdir3 baz &&
|
git worktree add -f bazdir baz &&
|
||||||
(
|
(
|
||||||
cd bazdir3 &&
|
cd bazdir &&
|
||||||
git branch -M baz bam &&
|
git branch -M baz bam &&
|
||||||
test $(git rev-parse --abbrev-ref HEAD) = bam
|
test $(git rev-parse --abbrev-ref HEAD) = bam
|
||||||
) &&
|
) &&
|
||||||
test $(git rev-parse --abbrev-ref HEAD) = bam
|
test $(git rev-parse --abbrev-ref HEAD) = bam &&
|
||||||
|
rm -r bazdir &&
|
||||||
|
git worktree prune
|
||||||
'
|
'
|
||||||
|
|
||||||
test_expect_success 'git branch -M master should work when master is checked out' '
|
test_expect_success 'git branch -M master should work when master is checked out' '
|
||||||
@ -804,7 +808,9 @@ test_expect_success 'test deleting branch without config' '
|
|||||||
test_expect_success 'deleting currently checked out branch fails' '
|
test_expect_success 'deleting currently checked out branch fails' '
|
||||||
git worktree add -b my7 my7 &&
|
git worktree add -b my7 my7 &&
|
||||||
test_must_fail git -C my7 branch -d my7 &&
|
test_must_fail git -C my7 branch -d my7 &&
|
||||||
test_must_fail git branch -d my7
|
test_must_fail git branch -d my7 &&
|
||||||
|
rm -r my7 &&
|
||||||
|
git worktree prune
|
||||||
'
|
'
|
||||||
|
|
||||||
test_expect_success 'test --track without .fetch entries' '
|
test_expect_success 'test --track without .fetch entries' '
|
||||||
|
@ -136,10 +136,13 @@ test_expect_success 'git branch `--show-current` works properly with worktrees'
|
|||||||
branch-two
|
branch-two
|
||||||
EOF
|
EOF
|
||||||
git checkout branch-one &&
|
git checkout branch-one &&
|
||||||
git worktree add worktree branch-two &&
|
test_when_finished "
|
||||||
|
git worktree remove worktree_dir
|
||||||
|
" &&
|
||||||
|
git worktree add worktree_dir branch-two &&
|
||||||
{
|
{
|
||||||
git branch --show-current &&
|
git branch --show-current &&
|
||||||
git -C worktree branch --show-current
|
git -C worktree_dir branch --show-current
|
||||||
} >actual &&
|
} >actual &&
|
||||||
test_cmp expect actual
|
test_cmp expect actual
|
||||||
'
|
'
|
||||||
@ -284,6 +287,24 @@ test_expect_success 'git branch --format option' '
|
|||||||
test_i18ncmp expect actual
|
test_i18ncmp expect actual
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'worktree colors correct' '
|
||||||
|
cat >expect <<-EOF &&
|
||||||
|
* <GREEN>(HEAD detached from fromtag)<RESET>
|
||||||
|
ambiguous<RESET>
|
||||||
|
branch-one<RESET>
|
||||||
|
+ <CYAN>branch-two<RESET>
|
||||||
|
master<RESET>
|
||||||
|
ref-to-branch<RESET> -> branch-one
|
||||||
|
ref-to-remote<RESET> -> origin/branch-one
|
||||||
|
EOF
|
||||||
|
git worktree add worktree_dir branch-two &&
|
||||||
|
git branch --color >actual.raw &&
|
||||||
|
rm -r worktree_dir &&
|
||||||
|
git worktree prune &&
|
||||||
|
test_decode_color <actual.raw >actual &&
|
||||||
|
test_i18ncmp expect actual
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success "set up color tests" '
|
test_expect_success "set up color tests" '
|
||||||
echo "<RED>master<RESET>" >expect.color &&
|
echo "<RED>master<RESET>" >expect.color &&
|
||||||
echo "master" >expect.bare &&
|
echo "master" >expect.bare &&
|
||||||
@ -308,4 +329,23 @@ test_expect_success '--color overrides auto-color' '
|
|||||||
test_cmp expect.color actual
|
test_cmp expect.color actual
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'verbose output lists worktree path' '
|
||||||
|
one=$(git rev-parse --short HEAD) &&
|
||||||
|
two=$(git rev-parse --short master) &&
|
||||||
|
cat >expect <<-EOF &&
|
||||||
|
* (HEAD detached from fromtag) $one one
|
||||||
|
ambiguous $one one
|
||||||
|
branch-one $two two
|
||||||
|
+ branch-two $one ($(pwd)/worktree_dir) one
|
||||||
|
master $two two
|
||||||
|
ref-to-branch $two two
|
||||||
|
ref-to-remote $two two
|
||||||
|
EOF
|
||||||
|
git worktree add worktree_dir branch-two &&
|
||||||
|
git branch -vv >actual &&
|
||||||
|
rm -r worktree_dir &&
|
||||||
|
git worktree prune &&
|
||||||
|
test_i18ncmp expect actual
|
||||||
|
'
|
||||||
|
|
||||||
test_done
|
test_done
|
||||||
|
@ -441,4 +441,17 @@ test_expect_success '--merged is incompatible with --no-merged' '
|
|||||||
test_must_fail git for-each-ref --merged HEAD --no-merged HEAD
|
test_must_fail git for-each-ref --merged HEAD --no-merged HEAD
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'validate worktree atom' '
|
||||||
|
cat >expect <<-EOF &&
|
||||||
|
master: $(pwd)
|
||||||
|
master_worktree: $(pwd)/worktree_dir
|
||||||
|
side: not checked out
|
||||||
|
EOF
|
||||||
|
git worktree add -b master_worktree worktree_dir master &&
|
||||||
|
git for-each-ref --format="%(refname:short): %(if)%(worktreepath)%(then)%(worktreepath)%(else)not checked out%(end)" refs/heads/ >actual &&
|
||||||
|
rm -r worktree_dir &&
|
||||||
|
git worktree prune &&
|
||||||
|
test_cmp expect actual
|
||||||
|
'
|
||||||
|
|
||||||
test_done
|
test_done
|
||||||
|
Reference in New Issue
Block a user