Subject: [GSoC][RFC PATCH 1/2] builtin/refs: add list subcommand

From: Meet Soni <[email-masked]>
Date: Fri, 27 Jun 2025 13:19:33 +0530

Git's reference management is distributed across multiple commands. As
part of an ongoing effort to consolidate and modernize reference
handling, introduce a `list` subcommand under the `git refs` umbrella as
a replacement for `git for-each-ref`.

For now, cmd_refs_list is effectively a copy of cmd_for_each_ref,
allowing us to reuse the existing filtering and formatting logic while
avoiding any backward compatibility issues. The core implementation
continues to reside in shared modules.

This duplication is temporary and intentional: future enhancements will
be added exclusively to `git refs list`, providing a gradual path
forward without disrupting existing workflows.

Mentored-by: Patrick Steinhardt <[email-masked]>
Mentored-by: shejialuo <[email-masked]>
Mentored-by: Karthik Nayak <[email-masked]>
Signed-off-by: Meet Soni <[email-masked]>
---
 Documentation/git-refs.adoc |  95 +++++++++++++++++++++++++++++++
 builtin/refs.c              | 110 ++++++++++++++++++++++++++++++++++++
 2 files changed, 205 insertions(+)

diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index 4d6dc994f9..d8f81eaabd 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -11,6 +11,13 @@ SYNOPSIS
 [synopsis]
 git refs migrate --ref-format=<format> [--no-reflog] [--dry-run]
 git refs verify [--strict] [--verbose]
+git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
+	      [(--sort=<key>)...] [--format=<format>]
+	      [--include-root-refs] [ --stdin | <pattern>... ]
+	      [--points-at=<object>]
+	      [--merged[=<object>]] [--no-merged[=<object>]]
+	      [--contains[=<object>]] [--no-contains[=<object>]]
+	      [--exclude=<pattern> ...]
 
 DESCRIPTION
 -----------
@@ -26,6 +33,11 @@ migrate::
 verify::
 	Verify reference database consistency.
 
+list::
+	List references in the repository with support for filtering, formatting,
+	and sorting. This subcommand uses the same core logic as
+	linkgit:git-for-each-ref[1] and offers equivalent functionality.
+
 OPTIONS
 -------
 
@@ -57,6 +69,89 @@ The following options are specific to 'git refs verify':
 --verbose::
 	When verifying the reference database consistency, be chatty.
 
+The following options are specific to 'git refs list':
+
+<pattern>...::
+	If one or more patterns are given, only refs are shown that
+	match against at least one pattern, either using fnmatch(3) or
+	literally, in the latter case matching completely or from the
+	beginning up to a slash.
+
+--stdin::
+	If `--stdin` is supplied, then the list of patterns is read from
+	standard input instead of from the argument list.
+
+--count=<count>::
+	By default the command shows all refs that match
+	`<pattern>`.  This option makes it stop after showing
+	that many refs.
+
+--sort=<key>::
+	A field name to sort on.  Prefix `-` to sort in
+	descending order of the value.  When unspecified,
+	`refname` is used.  You may use the --sort=<key> option
+	multiple times, in which case the last key becomes the primary
+	key.
+
+--format=<format>::
+	A string that interpolates `%(fieldname)` from a ref being shown and
+	the object it points at. In addition, the string literal `%%`
+	renders as `%` and `%xx` - where `xx` are hex digits - renders as
+	the character with hex code `xx`. For example, `%00` interpolates to
+	`\0` (NUL), `%09` to `\t` (TAB), and `%0a` to `\n` (LF).
++
+When unspecified, `<format>` defaults to `%(objectname) SPC %(objecttype)
+TAB %(refname)`.
+
+--color[=<when>]::
+	Respect any colors specified in the `--format` option. The
+	`<when>` field must be one of `always`, `never`, or `auto` (if
+	`<when>` is absent, behave as if `always` was given).
+
+--shell::
+--perl::
+--python::
+--tcl::
+	If given, strings that substitute `%(fieldname)`
+	placeholders are quoted as string literals suitable for
+	the specified host language.  This is meant to produce
+	a scriptlet that can directly be `eval`ed.
+
+--points-at=<object>::
+	Only list refs which points at the given object.
+
+--merged[=<object>]::
+	Only list refs whose tips are reachable from the
+	specified commit (HEAD if not specified).
+
+--no-merged[=<object>]::
+	Only list refs whose tips are not reachable from the
+	specified commit (HEAD if not specified).
+
+--contains[=<object>]::
+	Only list refs which contain the specified commit (HEAD if not
+	specified).
+
+--no-contains[=<object>]::
+	Only list refs which don't contain the specified commit (HEAD
+	if not specified).
+
+--ignore-case::
+	Sorting and filtering refs are case insensitive.
+
+--omit-empty::
+	Do not print a newline after formatted refs where the format expands
+	to the empty string.
+
+--exclude=<pattern>::
+	If one or more patterns are given, only refs which do not match
+	any excluded pattern(s) are shown. Matching is done using the
+	same rules as `<pattern>` above.
+
+--include-root-refs::
+	List root refs (HEAD and pseudorefs) apart from regular refs.
+
+
 KNOWN LIMITATIONS
 -----------------
 
diff --git a/builtin/refs.c b/builtin/refs.c
index 998d2a2c1c..70e1757791 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -3,6 +3,7 @@
 #include "config.h"
 #include "fsck.h"
 #include "parse-options.h"
+#include "ref-filter.h"
 #include "refs.h"
 #include "strbuf.h"
 #include "worktree.h"
@@ -13,6 +14,15 @@
 #define REFS_VERIFY_USAGE \
 	N_("git refs verify [--strict] [--verbose]")
 
+#define REFS_LIST_USAGE \
+	N_("git refs list [--count=<count>] [--shell|--perl|--python|--tcl]\n" \
+	   "              [(--sort=<key>)...] [--format=<format>]\n" \
+	   "              [--include-root-refs] [ --stdin | <pattern>... ]\n" \
+	   "              [--points-at=<object>]\n" \
+	   "              [--merged[=<object>]] [--no-merged[=<object>]]\n" \
+	   "              [--contains[=<object>]] [--no-contains[=<object>]]\n" \
+	   "              [--exclude=<pattern> ...]")
+
 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
 			    struct repository *repo UNUSED)
 {
@@ -101,6 +111,104 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,
 	return ret;
 }
 
+static int cmd_refs_list(int argc, const char **argv, const char *prefix,
+			 struct repository *repo)
+{
+	struct ref_sorting *sorting;
+	struct string_list sorting_options = STRING_LIST_INIT_DUP;
+	int icase = 0, include_root_refs = 0, from_stdin = 0;
+	struct ref_filter filter = REF_FILTER_INIT;
+	struct ref_format format = REF_FORMAT_INIT;
+	unsigned int flags = FILTER_REFS_REGULAR;
+	struct strvec vec = STRVEC_INIT;
+	const char * const list_usage[] = {
+		REFS_LIST_USAGE,
+		NULL
+	};
+
+	struct option opts[] = {
+		OPT_BIT('s', "shell", &format.quote_style,
+			N_("quote placeholders suitably for shells"), QUOTE_SHELL),
+		OPT_BIT('p', "perl",  &format.quote_style,
+			N_("quote placeholders suitably for perl"), QUOTE_PERL),
+		OPT_BIT(0 , "python", &format.quote_style,
+			N_("quote placeholders suitably for python"), QUOTE_PYTHON),
+		OPT_BIT(0 , "tcl",  &format.quote_style,
+			N_("quote placeholders suitably for Tcl"), QUOTE_TCL),
+		OPT_BOOL(0, "omit-empty",  &format.array_opts.omit_empty,
+			N_("do not output a newline after empty formatted refs")),
+
+		OPT_GROUP(""),
+		OPT_INTEGER(0, "count", &format.array_opts.max_count, N_("show only <n> matched refs")),
+		OPT_STRING(0, "format", &format.format, N_("format"), N_("format to use for the output")),
+		OPT__COLOR(&format.use_color, N_("respect format colors")),
+		OPT_REF_FILTER_EXCLUDE(&filter),
+		OPT_REF_SORT(&sorting_options),
+		OPT_CALLBACK(0, "points-at", &filter.points_at,
+			     N_("object"), N_("print only refs which points at the given object"),
+			     parse_opt_object_name),
+		OPT_MERGED(&filter, N_("print only refs that are merged")),
+		OPT_NO_MERGED(&filter, N_("print only refs that are not merged")),
+		OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
+		OPT_NO_CONTAINS(&filter.no_commit, N_("print only refs which don't contain the commit")),
+		OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
+		OPT_BOOL(0, "stdin", &from_stdin, N_("read reference patterns from stdin")),
+		OPT_BOOL(0, "include-root-refs", &include_root_refs, N_("also include HEAD ref and pseudorefs")),
+		OPT_END(),
+	};
+
+	format.format = "%(objectname) %(objecttype)\t%(refname)";
+
+	repo_config(repo, git_default_config, NULL);
+
+	/* Default to sorting by refname unless overridden by --sort= */
+	string_list_append(&sorting_options, "refname");
+
+	parse_options(argc, argv, prefix, opts, list_usage, 0);
+	if (format.array_opts.max_count < 0) {
+		error("invalid --count value: `%d'", format.array_opts.max_count);
+		usage_with_options(list_usage, opts);
+	}
+	if (HAS_MULTI_BITS(format.quote_style)) {
+		error("more than one quoting style?");
+		usage_with_options(list_usage, opts);
+	}
+	if (verify_ref_format(&format))
+		usage_with_options(list_usage, opts);
+
+	sorting = ref_sorting_options(&sorting_options);
+	ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
+	filter.ignore_case = icase;
+
+	if (from_stdin) {
+		struct strbuf line = STRBUF_INIT;
+
+		if (argv[0])
+			die(_("unknown arguments supplied with --stdin"));
+
+		while (strbuf_getline(&line, stdin) != EOF)
+			strvec_push(&vec, line.buf);
+
+		strbuf_release(&line);
+
+		/* vec.v is NULL-terminated, just like 'argv'. */
+		filter.name_patterns = vec.v;
+	} else {
+		filter.name_patterns = argv;
+	}
+
+	if (include_root_refs)
+		flags |= FILTER_REFS_ROOT_REFS | FILTER_REFS_DETACHED_HEAD;
+
+	filter.match_as_path = 1;
+	filter_and_format_refs(&filter, flags, sorting, &format);
+
+	ref_filter_clear(&filter);
+	ref_sorting_release(sorting);
+	strvec_clear(&vec);
+	return 0;
+}
+
 int cmd_refs(int argc,
 	     const char **argv,
 	     const char *prefix,
@@ -109,12 +217,14 @@ int cmd_refs(int argc,
 	const char * const refs_usage[] = {
 		REFS_MIGRATE_USAGE,
 		REFS_VERIFY_USAGE,
+		REFS_LIST_USAGE,
 		NULL,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
 	struct option opts[] = {
 		OPT_SUBCOMMAND("migrate", &fn, cmd_refs_migrate),
 		OPT_SUBCOMMAND("verify", &fn, cmd_refs_verify),
+		OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
 		OPT_END(),
 	};
 
-- 
2.34.1