Subject: [GSoC][PATCH] builtin/refs: add 'get' subcommand

From: Meet Soni <[email-masked]>
Date: Tue, 23 Sep 2025 16:15:33 +0530

While `git-rev-parse(1)` and `git-show-ref(1)` can be used to read
reference values, they have drawbacks for scripting and discoverability.
`rev-parse` performs DWIM expansion which is unpredictable for scripts,
and `show-ref --verify` is difficult to discover and cannot read the
direct target of a symbolic reference.

To address this, introduce a new plumbing command, `git refs get <ref>`.
This new command provides three key advantages:

  - It requires an exact refname and does not perform expansion, making
    it safer and more predictable for scripting.

  - Its name clearly states its purpose and it lives in the logical `git
    refs` namespace, unlike the `--verify` flag which lives in
    `git-show-ref`.

  - It provides a clean, dedicated way to read the direct target of a
    symbolic reference (e.g., `HEAD`) without recursively dereferencing
    it to an object ID.

Add documentation for the new subcommand to the `git-refs(1)` man page
and a comprehensive test suite to verify its behavior.

Mentored-by: Patrick Steinhardt <[email-masked]>
Mentored-by: shejialuo <[email-masked]>
Signed-off-by: Meet Soni <[email-masked]>
---
 Documentation/git-refs.adoc |  7 ++++
 builtin/refs.c              | 43 ++++++++++++++++++++++++
 t/meson.build               |  1 +
 t/t1464-refs-get.sh         | 66 +++++++++++++++++++++++++++++++++++++
 4 files changed, 117 insertions(+)
 create mode 100755 t/t1464-refs-get.sh

diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index bfa9b3ea2d..f07fe8c864 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -19,6 +19,7 @@ git refs list [--count=<count>] [--shell|--perl|--python|--tcl]
 		   [(--exclude=<pattern>)...] [--start-after=<marker>]
 		   [ --stdin | (<pattern>...)]
 git refs exists <ref>
+git refs get <ref>
 
 DESCRIPTION
 -----------
@@ -45,6 +46,12 @@ exists::
 	failed with an error other than the reference being missing. This does
 	not verify whether the reference resolves to an actual object.
 
+get::
+	Reads the raw value of a single, exact reference. Instead of
+	recursively dereferencing symbolic references, this command prints the
+	direct target of the symref (e.g., ref: refs/heads/main). For regular
+	references, it prints the object ID (SHA-1) they point to.
+
 OPTIONS
 -------
 
diff --git a/builtin/refs.c b/builtin/refs.c
index 91548783b7..b473a78e18 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -2,6 +2,7 @@
 #include "builtin.h"
 #include "config.h"
 #include "fsck.h"
+#include "hex.h"
 #include "parse-options.h"
 #include "refs.h"
 #include "strbuf.h"
@@ -18,6 +19,9 @@
 #define REFS_EXISTS_USAGE \
 	N_("git refs exists <ref>")
 
+#define REFS_GET_USAGE \
+	N_("git refs get <ref>")
+
 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
 			    struct repository *repo UNUSED)
 {
@@ -159,6 +163,43 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
 	return ret;
 }
 
+static int cmd_refs_get(int argc, const char **argv, const char *prefix,
+			struct repository *repo UNUSED)
+{
+	const char *refname;
+	struct object_id oid;
+	unsigned int type;
+	int failure_errno = 0;
+	struct strbuf referent = STRBUF_INIT;
+
+	const char * const exists_usage[] = {
+		REFS_EXISTS_USAGE,
+		NULL,
+	};
+	struct option options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, options, exists_usage, 0);
+	if (argc != 1)
+		die("refs get requires exactly one reference");
+
+	refname = *argv++;
+	if (refs_read_raw_ref(get_main_ref_store(the_repository), refname,
+			      &oid, &referent, &type, &failure_errno)) {
+		die("'%s' - not a valid ref", refname);
+	}
+
+	if (type & REF_ISSYMREF) {
+		printf("ref: %s\n", referent.buf);
+	} else {
+		printf("%s\n", oid_to_hex(&oid));
+	}
+
+	strbuf_release(&referent);
+	return 0;
+}
+
 int cmd_refs(int argc,
 	     const char **argv,
 	     const char *prefix,
@@ -169,6 +210,7 @@ int cmd_refs(int argc,
 		REFS_VERIFY_USAGE,
 		"git refs list " COMMON_USAGE_FOR_EACH_REF,
 		REFS_EXISTS_USAGE,
+		REFS_GET_USAGE,
 		NULL,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
@@ -177,6 +219,7 @@ int cmd_refs(int argc,
 		OPT_SUBCOMMAND("verify", &fn, cmd_refs_verify),
 		OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
 		OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
+		OPT_SUBCOMMAND("get", &fn, cmd_refs_get),
 		OPT_END(),
 	};
 
diff --git a/t/meson.build b/t/meson.build
index 7974795fe4..0c8067c69d 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -213,6 +213,7 @@ integration_tests = [
   't1460-refs-migrate.sh',
   't1461-refs-list.sh',
   't1462-refs-exists.sh',
+  't1464-refs-get.sh',
   't1500-rev-parse.sh',
   't1501-work-tree.sh',
   't1502-rev-parse-parseopt.sh',
diff --git a/t/t1464-refs-get.sh b/t/t1464-refs-get.sh
new file mode 100755
index 0000000000..166176c881
--- /dev/null
+++ b/t/t1464-refs-get.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+test_description='git refs get'
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+test_expect_success 'setup repository' '
+	test_commit one &&
+	git tag -a -m "tagging one" my-tag one &&
+	git symbolic-ref refs/my-symref refs/heads/main &&
+	git symbolic-ref refs/dangling-symref refs/heads/no-such-branch
+'
+
+test_expect_success 'fails with no arguments' '
+	test_must_fail git refs get >out 2>err &&
+	test_grep "refs get requires exactly one reference" err
+'
+
+test_expect_success 'fails with too many arguments' '
+	test_must_fail git refs get HEAD HEAD >out 2>err &&
+	test_grep "refs get requires exactly one reference" err
+'
+
+test_expect_success 'get a branch head' '
+	git rev-parse main >expect &&
+	git refs get refs/heads/main >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get an annotated tag' '
+	git rev-parse my-tag >expect &&
+	git refs get refs/tags/my-tag >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get HEAD (a symbolic ref)' '
+	echo "ref: refs/heads/main" >expect &&
+	git refs get HEAD >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get a custom symbolic ref' '
+	echo "ref: refs/heads/main" >expect &&
+	git refs get refs/my-symref >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get a dangling symbolic ref' '
+	echo "ref: refs/heads/no-such-branch" >expect &&
+	git refs get refs/dangling-symref >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get a non-existent ref' '
+	test_must_fail git refs get refs/heads/no-such-branch 2>err &&
+	test_grep "not a valid ref" err
+'
+
+test_expect_success 'get does not perform DWIM' '
+	test_must_fail git refs get main 2>err &&
+	test_grep "not a valid ref" err
+'
+
+test_done

base-commit: ca2559c1d630eb4f04cdee2328aaf1c768907a9e
-- 
2.34.1