Subject: [PATCH v2 2/3] ref-filter: add new "describe" atom

From: Kousik Sanagavarapu <[email-masked]>
Date: Sat, 15 Jul 2023 00:50:27 +0530

Duplicate the logic of %(describe) and friends from pretty to
ref-filter. In the future, this change helps in unifying both the
formats as ref-filter will be able to do everything that pretty is doing
and we can have a single interface.

The new atom "describe" and its friends are equivalent to the existing
pretty formats with the same name.

Mentored-by: Christian Couder <[email-masked]>
Mentored-by: Hariom Verma <[email-masked]>
Signed-off-by: Kousik Sanagavarapu <[email-masked]>
---
 Documentation/git-for-each-ref.txt |  23 +++++
 ref-filter.c                       | 147 +++++++++++++++++++++++++++++
 t/t6300-for-each-ref.sh            |  85 +++++++++++++++++
 3 files changed, 255 insertions(+)

diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt
index 1e215d4e73..2a44119f38 100644
--- a/Documentation/git-for-each-ref.txt
+++ b/Documentation/git-for-each-ref.txt
@@ -231,6 +231,29 @@ ahead-behind:<committish>::
 	commits ahead and behind, respectively, when comparing the output
 	ref to the `<committish>` specified in the format.
 
+describe[:options]:: Human-readable name, like
+		     link-git:git-describe[1]; empty string for
+		     undescribable commits. The `describe` string may be
+		     followed by a colon and zero or more comma-separated
+		     options. Descriptions can be inconsistent when tags
+		     are added or removed at the same time.
++
+--
+tags=<bool-value>;; Instead of only considering annotated tags, consider
+		    lightweight tags as well; see the corresponding option
+		    in linkgit:git-describe[1] for details.
+abbrev=<number>;; Use at least <number> hexadecimal digits; see
+		  the corresponding option in linkgit:git-describe[1]
+		  for details.
+match=<pattern>;; Only consider tags matching the given `glob(7)` pattern,
+		  excluding the "refs/tags/" prefix; see the corresponding
+		  option in linkgit:git-describe[1] for details.
+exclude=<pattern>;; Do not consider tags matching the given `glob(7)`
+		    pattern, excluding the "refs/tags/" prefix; see the
+		    corresponding option in linkgit:git-describe[1] for
+		    details.
+--
+
 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 b170994d9d..fe4830dbea 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -2,9 +2,11 @@
 #include "alloc.h"
 #include "environment.h"
 #include "gettext.h"
+#include "config.h"
 #include "gpg-interface.h"
 #include "hex.h"
 #include "parse-options.h"
+#include "run-command.h"
 #include "refs.h"
 #include "wildmatch.h"
 #include "object-name.h"
@@ -146,6 +148,7 @@ enum atom_type {
 	ATOM_TAGGERDATE,
 	ATOM_CREATOR,
 	ATOM_CREATORDATE,
+	ATOM_DESCRIBE,
 	ATOM_SUBJECT,
 	ATOM_BODY,
 	ATOM_TRAILERS,
@@ -215,6 +218,11 @@ static struct used_atom {
 		struct email_option {
 			enum { EO_RAW, EO_TRIM, EO_LOCALPART } option;
 		} email_option;
+		struct {
+			enum { D_BARE, D_TAGS, D_ABBREV,
+			       D_EXCLUDE, D_MATCH } option;
+			const char **args;
+		} describe;
 		struct refname_atom refname;
 		char *head;
 	} u;
@@ -521,6 +529,94 @@ static int contents_atom_parser(struct ref_format *format, struct used_atom *ato
 	return 0;
 }
 
+static int describe_atom_parser(struct ref_format *format UNUSED,
+				struct used_atom *atom,
+				const char *arg, struct strbuf *err)
+{
+	const char *describe_opts[] = {
+		"",
+		"tags",
+		"abbrev",
+		"match",
+		"exclude",
+		NULL
+	};
+
+	struct strvec args = STRVEC_INIT;
+	for (;;) {
+		int found = 0;
+		const char *argval;
+		size_t arglen = 0;
+		int optval = 0;
+		int opt;
+
+		if (!arg)
+			break;
+
+		for (opt = D_BARE; !found && describe_opts[opt]; opt++) {
+			switch(opt) {
+			case D_BARE:
+				/*
+				 * Do nothing. This is the bare describe
+				 * atom and we already handle this above.
+				 */
+				break;
+			case D_TAGS:
+				if (match_atom_bool_arg(arg, describe_opts[opt],
+							&arg, &optval)) {
+					if (!optval)
+						strvec_pushf(&args, "--no-%s",
+							     describe_opts[opt]);
+					else
+						strvec_pushf(&args, "--%s",
+							     describe_opts[opt]);
+					found = 1;
+				}
+				break;
+			case D_ABBREV:
+				if (match_atom_arg_value(arg, describe_opts[opt],
+							 &arg, &argval, &arglen)) {
+					char *endptr;
+					int ret = 0;
+
+					if (!arglen)
+						ret = -1;
+					if (strtol(argval, &endptr, 10) < 0)
+						ret = -1;
+					if (endptr - argval != arglen)
+						ret = -1;
+
+					if (ret)
+						return strbuf_addf_ret(err, ret,
+								_("positive value expected describe:abbrev=%s"), argval);
+					strvec_pushf(&args, "--%s=%.*s",
+						     describe_opts[opt],
+						     (int)arglen, argval);
+					found = 1;
+				}
+				break;
+			case D_MATCH:
+			case D_EXCLUDE:
+				if (match_atom_arg_value(arg, describe_opts[opt],
+							 &arg, &argval, &arglen)) {
+					if (!arglen)
+						return strbuf_addf_ret(err, -1,
+								_("value expected describe:%s="), describe_opts[opt]);
+					strvec_pushf(&args, "--%s=%.*s",
+						     describe_opts[opt],
+						     (int)arglen, argval);
+					found = 1;
+				}
+				break;
+			}
+		}
+		if (!found)
+			break;
+	}
+	atom->u.describe.args = strvec_detach(&args);
+	return 0;
+}
+
 static int raw_atom_parser(struct ref_format *format UNUSED,
 			   struct used_atom *atom,
 			   const char *arg, struct strbuf *err)
@@ -723,6 +819,7 @@ static struct {
 	[ATOM_TAGGERDATE] = { "taggerdate", SOURCE_OBJ, FIELD_TIME },
 	[ATOM_CREATOR] = { "creator", SOURCE_OBJ },
 	[ATOM_CREATORDATE] = { "creatordate", SOURCE_OBJ, FIELD_TIME },
+	[ATOM_DESCRIBE] = { "describe", SOURCE_OBJ, FIELD_STR, describe_atom_parser },
 	[ATOM_SUBJECT] = { "subject", SOURCE_OBJ, FIELD_STR, subject_atom_parser },
 	[ATOM_BODY] = { "body", SOURCE_OBJ, FIELD_STR, body_atom_parser },
 	[ATOM_TRAILERS] = { "trailers", SOURCE_OBJ, FIELD_STR, trailers_atom_parser },
@@ -1542,6 +1639,54 @@ static void append_lines(struct strbuf *out, const char *buf, unsigned long size
 	}
 }
 
+static void grab_describe_values(struct atom_value *val, int deref,
+				 struct object *obj)
+{
+	struct commit *commit = (struct commit *)obj;
+	int i;
+
+	for (i = 0; i < used_atom_cnt; i++) {
+		struct used_atom *atom = &used_atom[i];
+		enum atom_type type = atom->atom_type;
+		const char *name = atom->name;
+		struct atom_value *v = &val[i];
+
+		struct child_process cmd = CHILD_PROCESS_INIT;
+		struct strbuf out = STRBUF_INIT;
+		struct strbuf err = STRBUF_INIT;
+
+		if (type != ATOM_DESCRIBE)
+			continue;
+
+		if (!!deref != (*name == '*'))
+			continue;
+		if (deref)
+			name++;
+
+		if (!skip_prefix(name, "describe", &name) ||
+		    (*name && *name != ':'))
+			    continue;
+		if (!*name)
+			name = NULL;
+		else
+			name++;
+
+		cmd.git_cmd = 1;
+		strvec_push(&cmd.args, "describe");
+		strvec_pushv(&cmd.args, atom->u.describe.args);
+		strvec_push(&cmd.args, oid_to_hex(&commit->object.oid));
+		if (pipe_command(&cmd, NULL, 0, &out, 0, &err, 0) < 0) {
+			error(_("failed to run 'describe'"));
+			v->s = xstrdup("");
+			continue;
+		}
+		strbuf_rtrim(&out);
+		v->s = strbuf_detach(&out, NULL);
+
+		strbuf_release(&err);
+	}
+}
+
 /* See grab_values */
 static void grab_sub_body_contents(struct atom_value *val, int deref, struct expand_data *data)
 {
@@ -1651,12 +1796,14 @@ static void grab_values(struct atom_value *val, int deref, struct object *obj, s
 		grab_tag_values(val, deref, obj);
 		grab_sub_body_contents(val, deref, data);
 		grab_person("tagger", val, deref, buf);
+		grab_describe_values(val, deref, obj);
 		break;
 	case OBJ_COMMIT:
 		grab_commit_values(val, deref, obj);
 		grab_sub_body_contents(val, deref, data);
 		grab_person("author", val, deref, buf);
 		grab_person("committer", val, deref, buf);
+		grab_describe_values(val, deref, obj);
 		break;
 	case OBJ_TREE:
 		/* grab_tree_values(val, deref, obj, buf, sz); */
diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh
index 5c00607608..98ea37d336 100755
--- a/t/t6300-for-each-ref.sh
+++ b/t/t6300-for-each-ref.sh
@@ -561,6 +561,91 @@ test_expect_success 'color.ui=always does not override tty check' '
 	test_cmp expected.bare actual
 '
 
+test_expect_success 'describe atom vs git describe' '
+	test_when_finished "rm -rf describe-repo" &&
+
+	git init describe-repo &&
+	(
+		cd describe-repo &&
+
+		test_commit --no-tag one &&
+		git tag tagone &&
+
+		test_commit --no-tag two &&
+		git tag -a -m "tag two" tagtwo &&
+
+		git for-each-ref refs/tags/ --format="%(objectname)" >obj &&
+		while read hash
+		do
+			if desc=$(git describe $hash)
+			then
+				: >expect-contains-good
+			else
+				: >expect-contains-bad
+			fi &&
+			echo "$hash $desc" || return 1
+		done <obj >expect &&
+		test_path_exists expect-contains-good &&
+		test_path_exists expect-contains-bad &&
+
+		git for-each-ref --format="%(objectname) %(describe)" \
+			refs/tags/ >actual 2>err &&
+		test_cmp expect actual &&
+		test_must_be_empty err
+	)
+'
+
+test_expect_success 'describe:tags vs describe --tags' '
+	test_when_finished "git tag -d tagname" &&
+	git tag tagname &&
+	git describe --tags >expect &&
+	git for-each-ref --format="%(describe:tags)" refs/heads/ >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'describe:abbrev=... vs describe --abbrev=...' '
+	test_when_finished "git tag -d tagname" &&
+
+	# Case 1: We have commits between HEAD and the most
+	#         recent tag reachable from it
+	test_commit --no-tag file &&
+	git describe --abbrev=14 >expect &&
+	git for-each-ref --format="%(describe:abbrev=14)" \
+		refs/heads/ >actual &&
+	test_cmp expect actual &&
+
+	# Make sure the hash used is atleast 14 digits long
+	sed -e "s/^.*-g\([0-9a-f]*\)$/\1/" <actual >hexpart &&
+	test 15 -le $(wc -c <hexpart) &&
+
+	# Case 2: We have a tag at HEAD, describe directly gives
+	#         the name of the tag
+	git tag -a -m tagged tagname &&
+	git describe --abbrev=14 >expect &&
+	git for-each-ref --format="%(describe:abbrev=14)" \
+		refs/heads/ >actual &&
+	test_cmp expect actual &&
+	test tagname = $(cat actual)
+'
+
+test_expect_success 'describe:match=... vs describe --match ...' '
+	test_when_finished "git tag -d tag-match" &&
+	git tag -a -m "tag match" tag-match &&
+	git describe --match "*-match" >expect &&
+	git for-each-ref --format="%(describe:match="*-match")" \
+		refs/heads/ >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'describe:exclude:... vs describe --exclude ...' '
+	test_when_finished "git tag -d tag-exclude" &&
+	git tag -a -m "tag exclude" tag-exclude &&
+	git describe --exclude "*-exclude" >expect &&
+	git for-each-ref --format="%(describe:exclude="*-exclude")" \
+		refs/heads/ >actual &&
+	test_cmp expect actual
+'
+
 cat >expected <<\EOF
 heads/main
 tags/main
-- 
2.41.0.321.g26b82700c0.dirty