diff mbox series

[v4,1/3] ref-filter: add worktreepath atom

Message ID 20181224084756.49952-2-nbelakovski@gmail.com (mailing list archive)
State New, archived
Headers show
Series [v4,1/3] ref-filter: add worktreepath atom | expand

Commit Message

Nickolai Belakovski Dec. 24, 2018, 8:47 a.m. UTC
From: Nickolai Belakovski <nbelakovski@gmail.com>

Add an atom providing the path of the linked worktree where this ref is
checked out, if it is checked out in any linked worktrees, and empty
string otherwise.
---
 Documentation/git-for-each-ref.txt |  5 +++
 ref-filter.c                       | 72 +++++++++++++++++++++++++++++++++++++-
 t/t6302-for-each-ref-filter.sh     | 15 ++++++++
 3 files changed, 91 insertions(+), 1 deletion(-)

Comments

Jeff King Jan. 3, 2019, 5:40 a.m. UTC | #1
On Mon, Dec 24, 2018 at 12:47:54AM -0800, nbelakovski@gmail.com wrote:

> [...]

Thanks for keeping with this.  I think we're getting quite close, though
I did find a few small-ish issues.

> @@ -34,6 +36,8 @@ static struct ref_msg {
>  	"ahead %d, behind %d"
>  };
>  
> +static struct worktree **worktrees;
> +

Maybe define this near "struct hashmap ref_to_worktree_map" so it's
more obvious that the two are related?

> @@ -75,6 +79,11 @@ 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 */
> +};

Indent with spaces?

> -static int used_atom_cnt, need_tagged, need_symref;
> +static int used_atom_cnt, need_tagged, need_symref, has_worktree;
> +static struct hashmap ref_to_worktree_map;

Makes sense. I thought at first has_worktree was a flag that we might
care about between parsing and formatting, but it's really just a flag
to say "we lazy-loaded the worktree list".

> +static int worktree_hashmap_cmpfnc(const void *unused_lookupdata, const void *existing_hashmap_entry_to_test,
> +				   const void *unused_key, const void *keydata_aka_refname)
> +{
> +	const struct ref_to_worktree_entry *e = existing_hashmap_entry_to_test;
> +	return strcmp(e->wt->head_ref, keydata_aka_refname);
> +}

So from the discussion in the cover letter, this needs to be more like:

  static int worktree_hashmap_cmpfnc(const void *unused_lookupdata,
                                     const void *ve1, const void *ve2,
				     const void *keydata_aka_refname)
  {
	const struct ref_to_worktree_entry *e1 = ve1, *e2 = ve2;
	return strcmp(e1->wt->head_ref, keydata_aka_refname ?
		                        keydata_aka_refname :
					e2->wt->head_ref);
  }

> +static int worktree_atom_parser(const struct ref_format *format,
> +				struct used_atom *atom,
> +				const char *arg,
> +				struct strbuf *unused_err)
> +{
> +	int i;
> +	if (has_worktree)
> +		return 0;

Minor style nit, but please put a space between the declarations and the
start of the code (not strictly necessary for a short function which has
no other linebreaks, like the cmpfunc above, but here I think it's
confusing not to).

> +	worktrees = get_worktrees(0);
> +
> +	hashmap_init(&ref_to_worktree_map, worktree_hashmap_cmpfnc, NULL, 0);
> +
> +	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(&ref_to_worktree_map, entry);
> +		}
> +	}

Makes sense to load the map.

> +static const char *get_worktree_path(const struct used_atom *atom, const struct ref_array_item *ref)
> +{
> +	struct strbuf val = STRBUF_INIT;
> +	struct hashmap_entry entry;
> +	struct ref_to_worktree_entry *lookup_result;
> +
> +	hashmap_entry_init(&entry, strhash(ref->refname));
> +	lookup_result = hashmap_get(&ref_to_worktree_map, &entry, ref->refname);
> +
> +	strbuf_addstr(&val, lookup_result ? lookup_result->wt->path : "");
> +
> +	return strbuf_detach(&val, NULL);
> +}

And that makes sense to look up an item in it. Good.

Adding an empty string to a strbuf is a noop, so that part might more
clearly be written as just:

  if (lookup_result)
	strbuf_addstr(&val, lookup_result->wt->path);

We return a "const char *" here, but the result is always allocated. Do
we leak the result? Or should this return a "char *"?

I think there are a lot of other atoms that leak currently, but that is
being fixed in another topic that is currently in pu.

> @@ -2020,6 +2085,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 (has_worktree)
> +	{
> +		hashmap_free(&ref_to_worktree_map, 1);
> +		free_worktrees(worktrees);
> +	}

Here we free everything, but we don't unset has_worktree. So anybody
trying to format more refs afterward would see our freed worktree list.

We probably want:

  has_worktree = 0;

here. Or simpler still, I think get_worktrees() will always return a
non-NULL list (even if it is empty). So you could just drop has_worktree
entirely, and use:

  if (worktrees)
	return; /* already loaded */;

in the loading function, and:

  free_worktrees(worktrees);
  worktrees = NULL;

here.

> +test_expect_success '"add" a worktree' '
> +	mkdir worktree_dir &&
> +	git worktree add -b master_worktree worktree_dir master
> +'
> +
> +test_expect_success 'validate worktree atom' '
> +	{
> +	echo master: $PWD &&
> +	echo master_worktree: $PWD/worktree_dir &&
> +	echo side: not checked out
> +	} > expect &&

Minor style nit: use "} >expect" without the extra space.

This checks the actual directories. Good. I can never remember the rules
for when to use $PWD versus $(pwd) on Windows. We may run afoul of the
distinction here.

-Peff
Eric Sunshine Jan. 3, 2019, 9:31 a.m. UTC | #2
On Thu, Jan 3, 2019 at 12:40 AM Jeff King <peff@peff.net> wrote:
> On Mon, Dec 24, 2018 at 12:47:54AM -0800, nbelakovski@gmail.com wrote:
> > +test_expect_success 'validate worktree atom' '
> > +     {
> > +     echo master: $PWD &&
> > +     echo master_worktree: $PWD/worktree_dir &&
> > +     echo side: not checked out
> > +     } > expect &&
>
> Minor style nit: use "} >expect" without the extra space.

An interpolating here-doc would be even more natural:

    cat >expect <-EOF &&
    master: $(pwd)
    master_worktree: $(pwd)/worktree_dir
    side: not checked out
    EOF

> This checks the actual directories. Good. I can never remember the rules
> for when to use $PWD versus $(pwd) on Windows. We may run afoul of the
> distinction here.

As I understand it, this is exactly a case in which you would need to
use $(pwd); namely, when coming up with an "expect" value. t/README
talks about it.
diff mbox series

Patch

diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt
index 901faef1bf..caba1c23b8 100644
--- a/Documentation/git-for-each-ref.txt
+++ b/Documentation/git-for-each-ref.txt
@@ -209,6 +209,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.
diff --git a/ref-filter.c b/ref-filter.c
index 5de616befe..240e7b80f8 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -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;
@@ -34,6 +36,8 @@  static struct ref_msg {
 	"ahead %d, behind %d"
 };
 
+static struct worktree **worktrees;
+
 void setup_ref_filter_porcelain_msg(void)
 {
 	msgs.gone = _("gone");
@@ -75,6 +79,11 @@  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 */
+};
+
 /*
  * An atom is a valid field atom listed below, possibly prefixed with
  * a "*" to denote deref_tag().
@@ -116,7 +125,8 @@  static struct used_atom {
 		char *head;
 	} u;
 } *used_atom;
-static int used_atom_cnt, need_tagged, need_symref;
+static int used_atom_cnt, need_tagged, need_symref, has_worktree;
+static struct hashmap ref_to_worktree_map;
 
 /*
  * Expand string, append it to strbuf *sb, then return error code ret.
@@ -420,6 +430,42 @@  static int head_atom_parser(const struct ref_format *format, struct used_atom *a
 	return 0;
 }
 
+static int worktree_hashmap_cmpfnc(const void *unused_lookupdata, const void *existing_hashmap_entry_to_test,
+				   const void *unused_key, const void *keydata_aka_refname)
+{
+	const struct ref_to_worktree_entry *e = existing_hashmap_entry_to_test;
+	return strcmp(e->wt->head_ref, keydata_aka_refname);
+}
+
+static int worktree_atom_parser(const struct ref_format *format,
+				struct used_atom *atom,
+				const char *arg,
+				struct strbuf *unused_err)
+{
+	int i;
+	if (has_worktree)
+		return 0;
+
+	worktrees = get_worktrees(0);
+
+	hashmap_init(&ref_to_worktree_map, worktree_hashmap_cmpfnc, NULL, 0);
+
+	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(&ref_to_worktree_map, entry);
+		}
+	}
+
+	has_worktree = 1;
+
+	return 0;
+}
+
 static struct {
 	const char *name;
 	info_source source;
@@ -461,6 +507,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, FIELD_STR, worktree_atom_parser },
 	{ "align", SOURCE_NONE, FIELD_STR, align_atom_parser },
 	{ "end", SOURCE_NONE },
 	{ "if", SOURCE_NONE, FIELD_STR, if_atom_parser },
@@ -1500,6 +1547,20 @@  static int get_object(struct ref_array_item *ref, int deref, struct object **obj
 	return 0;
 }
 
+static const char *get_worktree_path(const struct used_atom *atom, const struct ref_array_item *ref)
+{
+	struct strbuf val = STRBUF_INIT;
+	struct hashmap_entry entry;
+	struct ref_to_worktree_entry *lookup_result;
+
+	hashmap_entry_init(&entry, strhash(ref->refname));
+	lookup_result = hashmap_get(&ref_to_worktree_map, &entry, ref->refname);
+
+	strbuf_addstr(&val, lookup_result ? lookup_result->wt->path : "");
+
+	return strbuf_detach(&val, NULL);
+}
+
 /*
  * Parse the object referred by ref, and grab needed value.
  */
@@ -1537,6 +1598,10 @@  static int populate_value(struct ref_array_item *ref, struct strbuf *err)
 
 		if (starts_with(name, "refname"))
 			refname = get_refname(atom, ref);
+		else if (starts_with(name, "worktreepath")) {
+			v->s = get_worktree_path(atom, ref);
+			continue;
+		}
 		else if (starts_with(name, "symref"))
 			refname = get_symref(atom, ref);
 		else if (starts_with(name, "upstream")) {
@@ -2020,6 +2085,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 (has_worktree)
+	{
+		hashmap_free(&ref_to_worktree_map, 1);
+		free_worktrees(worktrees);
+	}
 }
 
 static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata)
diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
index fc067ed672..d70517a6ae 100755
--- a/t/t6302-for-each-ref-filter.sh
+++ b/t/t6302-for-each-ref-filter.sh
@@ -441,4 +441,19 @@  test_expect_success '--merged is incompatible with --no-merged' '
 	test_must_fail git for-each-ref --merged HEAD --no-merged HEAD
 '
 
+test_expect_success '"add" a worktree' '
+	mkdir worktree_dir &&
+	git worktree add -b master_worktree worktree_dir master
+'
+
+test_expect_success 'validate worktree atom' '
+	{
+	echo master: $PWD &&
+	echo master_worktree: $PWD/worktree_dir &&
+	echo side: not checked out
+	} > expect &&
+	git for-each-ref --format="%(refname:short): %(if)%(worktreepath)%(then)%(worktreepath)%(else)not checked out%(end)" refs/heads/ >actual &&
+	test_cmp expect actual
+'
+
 test_done