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:
Junio C Hamano 2019-07-09 15:25:33 -07:00
commit 99eea64583
7 changed files with 169 additions and 15 deletions

View File

@ -26,8 +26,10 @@ DESCRIPTION
-----------
If `--list` is given, or if there are no non-option arguments, existing
branches are listed; the current branch will be highlighted with an
asterisk. Option `-r` causes the remote-tracking branches to be listed,
branches are listed; the current branch will be highlighted in green and
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>`
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
@ -174,8 +176,10 @@ This option is only applicable in non-verbose mode.
When in list mode,
show sha1 and commit subject line for each head, along with
relationship to upstream branch (if any). If given twice, print
the name of the upstream branch, as well (see also `git remote
show <remote>`).
the path of the linked worktree (if any) and the name of the upstream
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::
--quiet::

View File

@ -214,6 +214,11 @@ symref::
`:lstrip` and `:rstrip` options in the same way as `refname`
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
field names (`tree`, `parent`, `object`, `type`, and `tag`) can
be used to specify the value in the header field.

View File

@ -47,6 +47,7 @@ static char branch_colors[][COLOR_MAXLEN] = {
GIT_COLOR_NORMAL, /* LOCAL */
GIT_COLOR_GREEN, /* CURRENT */
GIT_COLOR_BLUE, /* UPSTREAM */
GIT_COLOR_CYAN, /* WORKTREE */
};
enum color_branch {
BRANCH_COLOR_RESET = 0,
@ -54,7 +55,8 @@ enum color_branch {
BRANCH_COLOR_REMOTE = 2,
BRANCH_COLOR_LOCAL = 3,
BRANCH_COLOR_CURRENT = 4,
BRANCH_COLOR_UPSTREAM = 5
BRANCH_COLOR_UPSTREAM = 5,
BRANCH_COLOR_WORKTREE = 6
};
static const char *color_branch_slots[] = {
@ -64,6 +66,7 @@ static const char *color_branch_slots[] = {
[BRANCH_COLOR_LOCAL] = "local",
[BRANCH_COLOR_CURRENT] = "current",
[BRANCH_COLOR_UPSTREAM] = "upstream",
[BRANCH_COLOR_WORKTREE] = "worktree",
};
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 remote = STRBUF_INIT;
strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else) %s%%(end)",
branch_get_color(BRANCH_COLOR_CURRENT),
branch_get_color(BRANCH_COLOR_LOCAL));
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_WORKTREE),
branch_get_color(BRANCH_COLOR_LOCAL));
strbuf_addf(&remote, " %s",
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);
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)"
"%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));
}
else
strbuf_addf(&local, "%%(if)%%(upstream:track)%%(then)%%(upstream:track) %%(end)%%(contents:subject)");

View File

@ -20,6 +20,8 @@
#include "commit-slab.h"
#include "commit-graph.h"
#include "commit-reach.h"
#include "worktree.h"
#include "hashmap.h"
static struct ref_msg {
const char *gone;
@ -75,6 +77,27 @@ static struct expand_data {
struct object_info info;
} 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
* a "*" to denote deref_tag().
@ -480,6 +503,7 @@ static struct {
{ "flag", SOURCE_NONE },
{ "HEAD", SOURCE_NONE, FIELD_STR, head_atom_parser },
{ "color", SOURCE_NONE, FIELD_STR, color_atom_parser },
{ "worktreepath", SOURCE_NONE },
{ "align", SOURCE_NONE, FIELD_STR, align_atom_parser },
{ "end", SOURCE_NONE },
{ "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;
}
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.
*/
@ -1568,6 +1634,13 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
if (starts_with(name, "refname"))
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"))
refname = get_symref(atom, ref);
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_AND_NULL(array->items);
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)

View File

@ -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 branch -M baz 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' '
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 &&
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' '
@ -804,7 +808,9 @@ test_expect_success 'test deleting branch without config' '
test_expect_success 'deleting currently checked out branch fails' '
git worktree add -b my7 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' '

View File

@ -136,10 +136,13 @@ test_expect_success 'git branch `--show-current` works properly with worktrees'
branch-two
EOF
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 -C worktree branch --show-current
git -C worktree_dir branch --show-current
} >actual &&
test_cmp expect actual
'
@ -284,6 +287,24 @@ test_expect_success 'git branch --format option' '
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" '
echo "<RED>master<RESET>" >expect.color &&
echo "master" >expect.bare &&
@ -308,4 +329,23 @@ test_expect_success '--color overrides auto-color' '
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

View File

@ -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_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