Merge branch 'nb/branch-show-other-worktrees-head'
authorJunio C Hamano <gitster@pobox.com>
Tue, 9 Jul 2019 22:25:33 +0000 (15:25 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 9 Jul 2019 22:25:33 +0000 (15:25 -0700)
"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

1  2 
Documentation/git-branch.txt
builtin/branch.c
ref-filter.c
t/t3200-branch.sh
t/t3203-branch-output.sh
index 6ebd512b4f3344c2f166f5bb09f0b5c6eb96ab03,d11d00583a6c398c06c7acce458b5cae30ad62f7..7c8fa3b64faff1c405927f0181fc079d8cb5d2e5
@@@ -26,8 -26,10 +26,10 @@@ DESCRIPTIO
  -----------
  
  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
@@@ -45,11 -47,7 +47,11 @@@ argument is missing it defaults to `HEA
  branch).
  
  The command's second form creates a new branch head named <branchname>
 -which points to the current `HEAD`, or <start-point> if given.
 +which points to the current `HEAD`, or <start-point> if given. As a
 +special case, for <start-point>, you may use `"A...B"` as a shortcut for
 +the merge base of `A` and `B` if there is exactly one merge base. You
 +can leave out at most one of `A` and `B`, in which case it defaults to
 +`HEAD`.
  
  Note that this will create the new branch, but it will not switch the
  working tree to it; use "git checkout <newbranch>" to switch to the
@@@ -174,8 -172,10 +176,10 @@@ This option is only applicable in non-v
        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::
diff --combined builtin/branch.c
index d4359b33ac0fb27b6b8be7109a2a862bc9cd2d77,3d1872babc826a611f4eeb71c36fb0dcc67bec85..2cb45e42e1967f365a6df14f42e2553290ffce85
@@@ -47,6 -47,7 +47,7 @@@ static char branch_colors[][COLOR_MAXLE
        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 +55,8 @@@
        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 +66,7 @@@
        [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 +345,10 @@@ static char *build_format(struct ref_fi
        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));
  
                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)");
  
@@@ -644,7 -652,8 +652,7 @@@ int cmd_branch(int argc, const char **a
                OPT_MERGED(&filter, N_("print only branches that are merged")),
                OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
                OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
 -              OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
 -                           N_("field name to sort on"), &parse_opt_ref_sorting),
 +              OPT_REF_SORT(sorting_tail),
                {
                        OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"),
                        N_("print only branches of the object"), 0, parse_opt_object_name
diff --combined ref-filter.c
index 8500671bc60957432568443de42b548612106f3c,79cfec914a276b635dceda8c81dd107e6d1c80d5..8fb25c1412b1324c10e2b0a518350488383aa669
@@@ -20,6 -20,8 +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 +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 +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 },
@@@ -1392,8 -1416,7 +1416,8 @@@ static void fill_remote_ref_details(str
                *s = show_ref(&atom->u.remote_ref.refname, refname);
        else if (atom->u.remote_ref.option == RR_TRACK) {
                if (stat_tracking_info(branch, &num_ours, &num_theirs,
 -                                     NULL, AHEAD_BEHIND_FULL) < 0) {
 +                                     NULL, atom->u.remote_ref.push,
 +                                     AHEAD_BEHIND_FULL) < 0) {
                        *s = xstrdup(msgs.gone);
                } else if (!num_ours && !num_theirs)
                        *s = xstrdup("");
                }
        } else if (atom->u.remote_ref.option == RR_TRACKSHORT) {
                if (stat_tracking_info(branch, &num_ours, &num_theirs,
 -                                     NULL, AHEAD_BEHIND_FULL) < 0) {
 +                                     NULL, atom->u.remote_ref.push,
 +                                     AHEAD_BEHIND_FULL) < 0) {
                        *s = xstrdup("");
                        return;
                }
@@@ -1531,6 -1553,48 +1555,48 @@@ static int get_object(struct ref_array_
        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 -1632,13 +1634,13 @@@ static int populate_value(struct ref_ar
  
                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 -2122,11 +2124,11 @@@ void ref_array_clear(struct ref_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)
@@@ -2339,13 -2415,8 +2417,13 @@@ void parse_ref_sorting(struct ref_sorti
  
  int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset)
  {
 -      if (!arg) /* should --no-sort void the list ? */
 -              return -1;
 +      /*
 +       * NEEDSWORK: We should probably clear the list in this case, but we've
 +       * already munged the global used_atoms list, which would need to be
 +       * undone.
 +       */
 +      BUG_ON_OPT_NEG(unset);
 +
        parse_ref_sorting(opt->value, arg);
        return 0;
  }
diff --combined t/t3200-branch.sh
index e9d7084d19c9d650f43f97d1c389c7fbc4cc51d2,88719cc02cb7e705c9aa0b46473ebbd213c42fe7..411a70b0ce966f196b516053f2b1ea35ad03bef2
@@@ -42,10 -42,6 +42,10 @@@ test_expect_success 'git branch a/b/c s
        git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
  '
  
 +test_expect_success 'git branch mb master... should create a branch' '
 +      git branch mb master... && test_path_is_file .git/refs/heads/mb
 +'
 +
  test_expect_success 'git branch HEAD should fail' '
        test_must_fail git branch HEAD
  '
@@@ -206,18 -202,22 +206,22 @@@ test_expect_success 'git branch -M baz 
        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' '
@@@ -268,30 -268,6 +272,30 @@@ test_expect_success 'git branch --list 
        test_must_fail git rev-parse refs/heads/t
  '
  
 +test_expect_success 'deleting checked-out branch from repo that is a submodule' '
 +      test_when_finished "rm -rf repo1 repo2" &&
 +
 +      git init repo1 &&
 +      git init repo1/sub &&
 +      test_commit -C repo1/sub x &&
 +      git -C repo1 submodule add ./sub &&
 +      git -C repo1 commit -m "adding sub" &&
 +
 +      git clone --recurse-submodules repo1 repo2 &&
 +      git -C repo2/sub checkout -b work &&
 +      test_must_fail git -C repo2/sub branch -D work
 +'
 +
 +test_expect_success 'bare main worktree has HEAD at branch deleted by secondary worktree' '
 +      test_when_finished "rm -rf nonbare base secondary" &&
 +
 +      git init nonbare &&
 +      test_commit -C nonbare x &&
 +      git clone --bare nonbare bare &&
 +      git -C bare worktree add --detach ../secondary master &&
 +      git -C secondary branch -D master
 +'
 +
  test_expect_success 'git branch --list -v with --abbrev' '
        test_when_finished "git branch -D t" &&
        git branch t &&
  test_expect_success 'git branch --column' '
        COLUMNS=81 git branch --column=column >actual &&
        cat >expected <<\EOF &&
 -  a/b/c     bam       foo       l       * master    n         o/p       r
 -  abc       bar       j/k       m/m       master2   o/o       q
 +  a/b/c     bam       foo       l       * master    mb        o/o       q
 +  abc       bar       j/k       m/m       master2   n         o/p       r
  EOF
        test_cmp expected actual
  '
@@@ -343,7 -319,6 +347,7 @@@ test_expect_success 'git branch --colum
    m/m
  * master
    master2
 +  mb
    n
    o/o
    o/p
@@@ -361,8 -336,8 +365,8 @@@ test_expect_success 'git branch with co
        git config --unset column.branch &&
        git config --unset column.ui &&
        cat >expected <<\EOF &&
 -  a/b/c   bam   foo   l   * master    n     o/p   r
 -  abc     bar   j/k   m/m   master2   o/o   q
 +  a/b/c   bam   foo   l   * master    mb   o/o   q
 +  abc     bar   j/k   m/m   master2   n    o/p   r
  EOF
        test_cmp expected actual
  '
@@@ -386,7 -361,6 +390,7 @@@ test_expect_success 'git branch -v wit
    m/m
  * master
    master2
 +  mb
    n
    o/o
    o/p
@@@ -804,7 -778,9 +808,9 @@@ test_expect_success 'test deleting bran
  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' '
diff --combined t/t3203-branch-output.sh
index be5514893030313bbe08b9041b9e3d85f7fbde2b,4bef8c75697d1725c6068091f045e71753951fda..71818b90f00d3727cb00e24da181fc9dec420f08
@@@ -136,11 -136,13 +136,14 @@@ test_expect_success 'git branch `--show
        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 &&
 -      rm -r worktree_dir &&
 -      git worktree prune &&
        test_cmp expect actual
  '
  
@@@ -284,6 -286,24 +287,24 @@@ test_expect_success 'git branch --forma
        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 -328,23 +329,23 @@@ test_expect_success '--color overrides 
        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