builtin/show-ref: add new mode to check for reference existence

While we have multiple ways to show the value of a given reference, we do not have any way to check whether a reference exists at all. While commands like git-rev-parse(1) or git-show-ref(1) can be used to check for reference existence in case the reference resolves to something sane, neither of them can be used to check for existence in some other scenarios where the reference does not resolve cleanly: - References which have an invalid name cannot be resolved. - References to nonexistent objects cannot be resolved. - Dangling symrefs can be resolved via git-symbolic-ref(1), but this requires the caller to special case existence checks depending on whether or not a reference is symbolic or direct. Furthermore, git-rev-list(1) and other commands do not let the caller distinguish easily between an actually missing reference and a generic error. Taken together, this seems like sufficient motivation to introduce a separate plumbing command to explicitly check for the existence of a reference without trying to resolve its contents. This new command comes in the form of `git show-ref --exists`. This new mode will exit successfully when the reference exists, with a specific exit code of 2 when it does not exist, or with 1 when there has been a generic error. Note that the only way to properly implement this command is by using the internal `refs_read_raw_ref()` function. While the public function `refs_resolve_ref_unsafe()` can be made to behave in the same way by passing various flags, it does not provide any way to obtain the errno with which the reference backend failed when reading the reference. As such, it becomes impossible for us to distinguish generic errors from the explicit case where the reference wasn't found. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Oct 31, 2023 at 09:16 UTC 9080a7f1782b55ee146dd7f9b4f464fe5ea8747a
3 files changed +117 -6
Documentation/git-show-ref.txt
+11
@@ -15,6 +15,7 @@ SYNOPSIS
15 [-s | --hash[=<n>]] [--abbrev[=<n>]]
16 [--] [<ref>...]
17 'git show-ref' --exclude-existing[=<pattern>]
18 +'git show-ref' --exists <ref>
19
20 DESCRIPTION
21 -----------
@@ -30,6 +31,10 @@ The `--exclude-existing` form is a filter that does the inverse. It reads
31 refs from stdin, one ref per line, and shows those that don't exist in
32 the local repository.
33
34 +The `--exists` form can be used to check for the existence of a single
35 +references. This form does not verify whether the reference resolves to an
36 +actual object.
37 +
38 Use of this utility is encouraged in favor of directly accessing files under
39 the `.git` directory.
40
@@ -65,6 +70,12 @@ OPTIONS
70 Aside from returning an error code of 1, it will also print an error
71 message if `--quiet` was not specified.
72
73 +--exists::
74 +
75 + Check whether the given reference exists. Returns an exit code of 0 if
76 + it does, 2 if it is missing, and 1 in case looking up the reference
77 + failed with an error other than the reference being missing.
78 +
79 --abbrev[=<n>]::
80
81 Abbreviate the object name. When using `--hash`, you do
builtin/show-ref.c
+44 -5
@@ -2,7 +2,7 @@
2 #include "config.h"
3 #include "gettext.h"
4 #include "hex.h"
5 -#include "refs.h"
5 +#include "refs/refs-internal.h"
6 #include "object-name.h"
7 #include "object-store-ll.h"
8 #include "object.h"
@@ -18,6 +18,7 @@ static const char * const show_ref_usage[] = {
18 " [-s | --hash[=<n>]] [--abbrev[=<n>]]\n"
19 " [--] [<ref>...]"),
20 N_("git show-ref --exclude-existing[=<pattern>]"),
21 + N_("git show-ref --exists <ref>"),
22 NULL
23 };
24
@@ -220,6 +221,41 @@ static int cmd_show_ref__patterns(const struct patterns_options *opts,
221 return 0;
222 }
223
224 +static int cmd_show_ref__exists(const char **refs)
225 +{
226 + struct strbuf unused_referent = STRBUF_INIT;
227 + struct object_id unused_oid;
228 + unsigned int unused_type;
229 + int failure_errno = 0;
230 + const char *ref;
231 + int ret = 0;
232 +
233 + if (!refs || !*refs)
234 + die("--exists requires a reference");
235 + ref = *refs++;
236 + if (*refs)
237 + die("--exists requires exactly one reference");
238 +
239 + if (refs_read_raw_ref(get_main_ref_store(the_repository), ref,
240 + &unused_oid, &unused_referent, &unused_type,
241 + &failure_errno)) {
242 + if (failure_errno == ENOENT) {
243 + error(_("reference does not exist"));
244 + ret = 2;
245 + } else {
246 + errno = failure_errno;
247 + error_errno(_("failed to look up reference"));
248 + ret = 1;
249 + }
250 +
251 + goto out;
252 + }
253 +
254 +out:
255 + strbuf_release(&unused_referent);
256 + return ret;
257 +}
258 +
259 static int hash_callback(const struct option *opt, const char *arg, int unset)
260 {
261 struct show_one_options *opts = opt->value;
@@ -249,10 +285,11 @@ int cmd_show_ref(int argc, const char **argv, const char *prefix)
285 struct exclude_existing_options exclude_existing_opts = {0};
286 struct patterns_options patterns_opts = {0};
287 struct show_one_options show_one_opts = {0};
252 - int verify = 0;
288 + int verify = 0, exists = 0;
289 const struct option show_ref_options[] = {
290 OPT_BOOL(0, "tags", &patterns_opts.tags_only, N_("only show tags (can be combined with heads)")),
291 OPT_BOOL(0, "heads", &patterns_opts.heads_only, N_("only show heads (can be combined with tags)")),
292 + OPT_BOOL(0, "exists", &exists, N_("check for reference existence without resolving")),
293 OPT_BOOL(0, "verify", &verify, N_("stricter reference checking, "
294 "requires exact ref path")),
295 OPT_HIDDEN_BOOL('h', NULL, &patterns_opts.show_head,
@@ -278,14 +315,16 @@ int cmd_show_ref(int argc, const char **argv, const char *prefix)
315 argc = parse_options(argc, argv, prefix, show_ref_options,
316 show_ref_usage, 0);
317
281 - if ((!!exclude_existing_opts.enabled + !!verify) > 1)
282 - die(_("only one of '%s' or '%s' can be given"),
283 - "--exclude-existing", "--verify");
318 + if ((!!exclude_existing_opts.enabled + !!verify + !!exists) > 1)
319 + die(_("only one of '%s', '%s' or '%s' can be given"),
320 + "--exclude-existing", "--verify", "--exists");
321
322 if (exclude_existing_opts.enabled)
323 return cmd_show_ref__exclude_existing(&exclude_existing_opts);
324 else if (verify)
325 return cmd_show_ref__verify(&show_one_opts, argv);
326 + else if (exists)
327 + return cmd_show_ref__exists(argv);
328 else
329 return cmd_show_ref__patterns(&patterns_opts, &show_one_opts, argv);
330 }
t/t1403-show-ref.sh
+62 -1
@@ -198,10 +198,71 @@ test_expect_success 'show-ref --verify with dangling ref' '
198
199 test_expect_success 'show-ref sub-modes are mutually exclusive' '
200 cat >expect <<-EOF &&
201 - fatal: only one of ${SQ}--exclude-existing${SQ} or ${SQ}--verify${SQ} can be given
201 + fatal: only one of ${SQ}--exclude-existing${SQ}, ${SQ}--verify${SQ} or ${SQ}--exists${SQ} can be given
202 EOF
203
204 test_must_fail git show-ref --verify --exclude-existing 2>err &&
205 + test_cmp expect err &&
206 +
207 + test_must_fail git show-ref --verify --exists 2>err &&
208 + test_cmp expect err &&
209 +
210 + test_must_fail git show-ref --exclude-existing --exists 2>err &&
211 + test_cmp expect err
212 +'
213 +
214 +test_expect_success '--exists with existing reference' '
215 + git show-ref --exists refs/heads/$GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
216 +'
217 +
218 +test_expect_success '--exists with missing reference' '
219 + test_expect_code 2 git show-ref --exists refs/heads/does-not-exist
220 +'
221 +
222 +test_expect_success '--exists does not use DWIM' '
223 + test_expect_code 2 git show-ref --exists $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME 2>err &&
224 + grep "reference does not exist" err
225 +'
226 +
227 +test_expect_success '--exists with HEAD' '
228 + git show-ref --exists HEAD
229 +'
230 +
231 +test_expect_success '--exists with bad reference name' '
232 + test_when_finished "git update-ref -d refs/heads/bad...name" &&
233 + new_oid=$(git rev-parse HEAD) &&
234 + test-tool ref-store main update-ref msg refs/heads/bad...name $new_oid $ZERO_OID REF_SKIP_REFNAME_VERIFICATION &&
235 + git show-ref --exists refs/heads/bad...name
236 +'
237 +
238 +test_expect_success '--exists with arbitrary symref' '
239 + test_when_finished "git symbolic-ref -d refs/symref" &&
240 + git symbolic-ref refs/symref refs/heads/$GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME &&
241 + git show-ref --exists refs/symref
242 +'
243 +
244 +test_expect_success '--exists with dangling symref' '
245 + test_when_finished "git symbolic-ref -d refs/heads/dangling" &&
246 + git symbolic-ref refs/heads/dangling refs/heads/does-not-exist &&
247 + git show-ref --exists refs/heads/dangling
248 +'
249 +
250 +test_expect_success '--exists with nonexistent object ID' '
251 + test-tool ref-store main update-ref msg refs/heads/missing-oid $(test_oid 001) $ZERO_OID REF_SKIP_OID_VERIFICATION &&
252 + git show-ref --exists refs/heads/missing-oid
253 +'
254 +
255 +test_expect_success '--exists with non-commit object' '
256 + tree_oid=$(git rev-parse HEAD^{tree}) &&
257 + test-tool ref-store main update-ref msg refs/heads/tree ${tree_oid} $ZERO_OID REF_SKIP_OID_VERIFICATION &&
258 + git show-ref --exists refs/heads/tree
259 +'
260 +
261 +test_expect_success '--exists with directory fails with generic error' '
262 + cat >expect <<-EOF &&
263 + error: failed to look up reference: Is a directory
264 + EOF
265 + test_expect_code 1 git show-ref --exists refs/heads 2>err &&
266 test_cmp expect err
267 '
268