Merge branch 'ps/pseudo-ref-terminology'

Terminology to call various ref-like things are getting
straightened out.

* ps/pseudo-ref-terminology:
  refs: refuse to write pseudorefs
  ref-filter: properly distinuish pseudo and root refs
  refs: pseudorefs are no refs
  refs: classify HEAD as a root ref
  refs: do not check ref existence in `is_root_ref()`
  refs: rename `is_special_ref()` to `is_pseudo_ref()`
  refs: rename `is_pseudoref()` to `is_root_ref()`
  Documentation/glossary: define root refs as refs
  Documentation/glossary: clarify limitations of pseudorefs
  Documentation/glossary: redefine pseudorefs as special refs
This commit is contained in:
Junio C Hamano 2024-05-28 11:17:06 -07:00
commit 16a592f132
10 changed files with 169 additions and 117 deletions

View File

@ -497,20 +497,18 @@ exclude;;
unusual refs. unusual refs.
[[def_pseudoref]]pseudoref:: [[def_pseudoref]]pseudoref::
Pseudorefs are a class of files under `$GIT_DIR` which behave A ref that has different semantics than normal refs. These refs can be
like refs for the purposes of rev-parse, but which are treated read via normal Git commands, but cannot be written to by commands like
specially by git. Pseudorefs both have names that are all-caps, linkgit:git-update-ref[1].
and always start with a line consisting of a +
<<def_SHA1,SHA-1>> followed by whitespace. So, HEAD is not a The following pseudorefs are known to Git:
pseudoref, because it is sometimes a symbolic ref. They might
optionally contain some additional data. `MERGE_HEAD` and - `FETCH_HEAD` is written by linkgit:git-fetch[1] or linkgit:git-pull[1]. It
`CHERRY_PICK_HEAD` are examples. Unlike may refer to multiple object IDs. Each object ID is annotated with metadata
<<def_per_worktree_ref,per-worktree refs>>, these files cannot indicating where it was fetched from and its fetch status.
be symbolic refs, and never have reflogs. They also cannot be
updated through the normal ref update machinery. Instead, - `MERGE_HEAD` is written by linkgit:git-merge[1] when resolving merge
they are updated by directly writing to the files. However, conflicts. It contains all commit IDs which are being merged.
they can be read as if they were refs, so `git rev-parse
MERGE_HEAD` will work.
[[def_pull]]pull:: [[def_pull]]pull::
Pulling a <<def_branch,branch>> means to <<def_fetch,fetch>> it and Pulling a <<def_branch,branch>> means to <<def_fetch,fetch>> it and
@ -552,20 +550,38 @@ exclude;;
to the result. to the result.
[[def_ref]]ref:: [[def_ref]]ref::
A name that begins with `refs/` (e.g. `refs/heads/master`) A name that that points to an <<def_object_name,object name>> or
that points to an <<def_object_name,object name>> or another another ref (the latter is called a <<def_symref,symbolic ref>>).
ref (the latter is called a <<def_symref,symbolic ref>>).
For convenience, a ref can sometimes be abbreviated when used For convenience, a ref can sometimes be abbreviated when used
as an argument to a Git command; see linkgit:gitrevisions[7] as an argument to a Git command; see linkgit:gitrevisions[7]
for details. for details.
Refs are stored in the <<def_repository,repository>>. Refs are stored in the <<def_repository,repository>>.
+ +
The ref namespace is hierarchical. The ref namespace is hierarchical.
Different subhierarchies are used for different purposes (e.g. the Ref names must either start with `refs/` or be located in the root of
`refs/heads/` hierarchy is used to represent local branches). the hierarchy. For the latter, their name must follow these rules:
+ +
There are a few special-purpose refs that do not begin with `refs/`. - The name consists of only upper-case characters or underscores.
The most notable example is `HEAD`.
- The name ends with "`_HEAD`" or is equal to "`HEAD`".
+
There are some irregular refs in the root of the hierarchy that do not
match these rules. The following list is exhaustive and shall not be
extended in the future:
+
- `AUTO_MERGE`
- `BISECT_EXPECTED_REV`
- `NOTES_MERGE_PARTIAL`
- `NOTES_MERGE_REF`
- `MERGE_AUTOSTASH`
+
Different subhierarchies are used for different purposes. For example,
the `refs/heads/` hierarchy is used to represent local branches whereas
the `refs/tags/` hierarchy is used to represent local tags..
[[def_reflog]]reflog:: [[def_reflog]]reflog::
A reflog shows the local "history" of a ref. In other words, A reflog shows the local "history" of a ref. In other words,
@ -639,20 +655,6 @@ The most notable example is `HEAD`.
An <<def_object,object>> used to temporarily store the contents of a An <<def_object,object>> used to temporarily store the contents of a
<<def_dirty,dirty>> working directory and the index for future reuse. <<def_dirty,dirty>> working directory and the index for future reuse.
[[def_special_ref]]special ref::
A ref that has different semantics than normal refs. These refs can be
accessed via normal Git commands but may not behave the same as a
normal ref in some cases.
+
The following special refs are known to Git:
- "`FETCH_HEAD`" is written by linkgit:git-fetch[1] or linkgit:git-pull[1]. It
may refer to multiple object IDs. Each object ID is annotated with metadata
indicating where it was fetched from and its fetch status.
- "`MERGE_HEAD`" is written by linkgit:git-merge[1] when resolving merge
conflicts. It contains all commit IDs which are being merged.
[[def_submodule]]submodule:: [[def_submodule]]submodule::
A <<def_repository,repository>> that holds the history of a A <<def_repository,repository>> that holds the history of a
separate project inside another repository (the latter of separate project inside another repository (the latter of

View File

@ -98,7 +98,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
} }
if (include_root_refs) if (include_root_refs)
flags |= FILTER_REFS_ROOT_REFS; flags |= FILTER_REFS_ROOT_REFS | FILTER_REFS_DETACHED_HEAD;
filter.match_as_path = 1; filter.match_as_path = 1;
filter_and_format_refs(&filter, flags, sorting, &format); filter_and_format_refs(&filter, flags, sorting, &format);

View File

@ -2634,7 +2634,7 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,
each_ref_fn cb, each_ref_fn cb,
void *cb_data) void *cb_data)
{ {
if (filter->kind == FILTER_REFS_KIND_MASK) { if (filter->kind & FILTER_REFS_ROOT_REFS) {
/* In this case, we want to print all refs including root refs. */ /* In this case, we want to print all refs including root refs. */
return refs_for_each_include_root_refs(get_main_ref_store(the_repository), return refs_for_each_include_root_refs(get_main_ref_store(the_repository),
cb, cb_data); cb, cb_data);
@ -2764,8 +2764,10 @@ static int ref_kind_from_refname(const char *refname)
return ref_kind[i].kind; return ref_kind[i].kind;
} }
if (is_pseudoref(get_main_ref_store(the_repository), refname)) if (is_pseudo_ref(refname))
return FILTER_REFS_PSEUDOREFS; return FILTER_REFS_PSEUDOREFS;
if (is_root_ref(refname))
return FILTER_REFS_ROOT_REFS;
return FILTER_REFS_OTHERS; return FILTER_REFS_OTHERS;
} }
@ -2802,11 +2804,11 @@ static struct ref_array_item *apply_ref_filter(const char *refname, const struct
/* /*
* Generally HEAD refs are printed with special description denoting a rebase, * Generally HEAD refs are printed with special description denoting a rebase,
* detached state and so forth. This is useful when only printing the HEAD ref * detached state and so forth. This is useful when only printing the HEAD ref
* But when it is being printed along with other pseudorefs, it makes sense to * But when it is being printed along with other root refs, it makes sense to
* keep the formatting consistent. So we mask the type to act like a pseudoref. * keep the formatting consistent. So we mask the type to act like a root ref.
*/ */
if (filter->kind == FILTER_REFS_KIND_MASK && kind == FILTER_REFS_DETACHED_HEAD) if (filter->kind & FILTER_REFS_ROOT_REFS && kind == FILTER_REFS_DETACHED_HEAD)
kind = FILTER_REFS_PSEUDOREFS; kind = FILTER_REFS_ROOT_REFS;
else if (!(kind & filter->kind)) else if (!(kind & filter->kind))
return NULL; return NULL;
@ -3086,7 +3088,7 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, each_ref
* When printing all ref types, HEAD is already included, * When printing all ref types, HEAD is already included,
* so we don't want to print HEAD again. * so we don't want to print HEAD again.
*/ */
if (!ret && (filter->kind != FILTER_REFS_KIND_MASK) && if (!ret && !(filter->kind & FILTER_REFS_ROOT_REFS) &&
(filter->kind & FILTER_REFS_DETACHED_HEAD)) (filter->kind & FILTER_REFS_DETACHED_HEAD))
refs_head_ref(get_main_ref_store(the_repository), fn, refs_head_ref(get_main_ref_store(the_repository), fn,
cb_data); cb_data);

View File

@ -23,9 +23,9 @@
FILTER_REFS_REMOTES | FILTER_REFS_OTHERS) FILTER_REFS_REMOTES | FILTER_REFS_OTHERS)
#define FILTER_REFS_DETACHED_HEAD 0x0020 #define FILTER_REFS_DETACHED_HEAD 0x0020
#define FILTER_REFS_PSEUDOREFS 0x0040 #define FILTER_REFS_PSEUDOREFS 0x0040
#define FILTER_REFS_ROOT_REFS (FILTER_REFS_DETACHED_HEAD | FILTER_REFS_PSEUDOREFS) #define FILTER_REFS_ROOT_REFS 0x0080
#define FILTER_REFS_KIND_MASK (FILTER_REFS_REGULAR | FILTER_REFS_DETACHED_HEAD | \ #define FILTER_REFS_KIND_MASK (FILTER_REFS_REGULAR | FILTER_REFS_DETACHED_HEAD | \
FILTER_REFS_PSEUDOREFS) FILTER_REFS_PSEUDOREFS | FILTER_REFS_ROOT_REFS)
struct atom_value; struct atom_value;
struct ref_sorting; struct ref_sorting;

98
refs.c
View File

@ -819,7 +819,22 @@ int is_per_worktree_ref(const char *refname)
starts_with(refname, "refs/rewritten/"); starts_with(refname, "refs/rewritten/");
} }
static int is_pseudoref_syntax(const char *refname) int is_pseudo_ref(const char *refname)
{
static const char * const pseudo_refs[] = {
"FETCH_HEAD",
"MERGE_HEAD",
};
size_t i;
for (i = 0; i < ARRAY_SIZE(pseudo_refs); i++)
if (!strcmp(refname, pseudo_refs[i]))
return 1;
return 0;
}
static int is_root_ref_syntax(const char *refname)
{ {
const char *c; const char *c;
@ -828,56 +843,37 @@ static int is_pseudoref_syntax(const char *refname)
return 0; return 0;
} }
/*
* HEAD is not a pseudoref, but it certainly uses the
* pseudoref syntax.
*/
return 1; return 1;
} }
int is_pseudoref(struct ref_store *refs, const char *refname) int is_root_ref(const char *refname)
{ {
static const char *const irregular_pseudorefs[] = { static const char *const irregular_root_refs[] = {
"HEAD",
"AUTO_MERGE", "AUTO_MERGE",
"BISECT_EXPECTED_REV", "BISECT_EXPECTED_REV",
"NOTES_MERGE_PARTIAL", "NOTES_MERGE_PARTIAL",
"NOTES_MERGE_REF", "NOTES_MERGE_REF",
"MERGE_AUTOSTASH", "MERGE_AUTOSTASH",
}; };
struct object_id oid;
size_t i; size_t i;
if (!is_pseudoref_syntax(refname)) if (!is_root_ref_syntax(refname) ||
is_pseudo_ref(refname))
return 0; return 0;
if (ends_with(refname, "_HEAD")) { if (ends_with(refname, "_HEAD"))
refs_resolve_ref_unsafe(refs, refname, return 1;
RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE,
&oid, NULL);
return !is_null_oid(&oid);
}
for (i = 0; i < ARRAY_SIZE(irregular_pseudorefs); i++) for (i = 0; i < ARRAY_SIZE(irregular_root_refs); i++)
if (!strcmp(refname, irregular_pseudorefs[i])) { if (!strcmp(refname, irregular_root_refs[i]))
refs_resolve_ref_unsafe(refs, refname, return 1;
RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE,
&oid, NULL);
return !is_null_oid(&oid);
}
return 0;
}
int is_headref(struct ref_store *refs, const char *refname)
{
if (!strcmp(refname, "HEAD"))
return refs_ref_exists(refs, refname);
return 0; return 0;
} }
static int is_current_worktree_ref(const char *ref) { static int is_current_worktree_ref(const char *ref) {
return is_pseudoref_syntax(ref) || is_per_worktree_ref(ref); return is_root_ref_syntax(ref) || is_per_worktree_ref(ref);
} }
enum ref_worktree_type parse_worktree_ref(const char *maybe_worktree_ref, enum ref_worktree_type parse_worktree_ref(const char *maybe_worktree_ref,
@ -1243,6 +1239,13 @@ int ref_transaction_update(struct ref_transaction *transaction,
return -1; return -1;
} }
if (!(flags & REF_SKIP_REFNAME_VERIFICATION) &&
is_pseudo_ref(refname)) {
strbuf_addf(err, _("refusing to update pseudoref '%s'"),
refname);
return -1;
}
if (flags & ~REF_TRANSACTION_UPDATE_ALLOWED_FLAGS) if (flags & ~REF_TRANSACTION_UPDATE_ALLOWED_FLAGS)
BUG("illegal flags 0x%x passed to ref_transaction_update()", flags); BUG("illegal flags 0x%x passed to ref_transaction_update()", flags);
@ -1816,43 +1819,12 @@ done:
return result; return result;
} }
static int is_special_ref(const char *refname)
{
/*
* Special references are refs that have different semantics compared
* to "normal" refs. These refs can thus not be stored in the ref
* backend, but must always be accessed via the filesystem. The
* following refs are special:
*
* - FETCH_HEAD may contain multiple object IDs, and each one of them
* carries additional metadata like where it came from.
*
* - MERGE_HEAD may contain multiple object IDs when merging multiple
* heads.
*
* Reading, writing or deleting references must consistently go either
* through the filesystem (special refs) or through the reference
* backend (normal ones).
*/
static const char * const special_refs[] = {
"FETCH_HEAD",
"MERGE_HEAD",
};
size_t i;
for (i = 0; i < ARRAY_SIZE(special_refs); i++)
if (!strcmp(refname, special_refs[i]))
return 1;
return 0;
}
int refs_read_raw_ref(struct ref_store *ref_store, const char *refname, int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,
struct object_id *oid, struct strbuf *referent, struct object_id *oid, struct strbuf *referent,
unsigned int *type, int *failure_errno) unsigned int *type, int *failure_errno)
{ {
assert(failure_errno); assert(failure_errno);
if (is_special_ref(refname)) if (is_pseudo_ref(refname))
return refs_read_special_head(ref_store, refname, oid, referent, return refs_read_special_head(ref_store, refname, oid, referent,
type, failure_errno); type, failure_errno);

48
refs.h
View File

@ -1013,8 +1013,52 @@ extern struct ref_namespace_info ref_namespace[NAMESPACE__COUNT];
*/ */
void update_ref_namespace(enum ref_namespace namespace, char *ref); void update_ref_namespace(enum ref_namespace namespace, char *ref);
int is_pseudoref(struct ref_store *refs, const char *refname); /*
int is_headref(struct ref_store *refs, const char *refname); * Check whether the provided name names a root reference. This function only
* performs a syntactic check.
*
* A root ref is a reference that lives in the root of the reference hierarchy.
* These references must conform to special syntax:
*
* - Their name must be all-uppercase or underscores ("_").
*
* - Their name must end with "_HEAD". As a special rule, "HEAD" is a root
* ref, as well.
*
* - Their name may not contain a slash.
*
* There is a special set of irregular root refs that exist due to historic
* reasons, only. This list shall not be expanded in the future:
*
* - AUTO_MERGE
*
* - BISECT_EXPECTED_REV
*
* - NOTES_MERGE_PARTIAL
*
* - NOTES_MERGE_REF
*
* - MERGE_AUTOSTASH
*/
int is_root_ref(const char *refname);
/*
* Pseudorefs are refs that have different semantics compared to
* "normal" refs. These refs can thus not be stored in the ref backend,
* but must always be accessed via the filesystem. The following refs
* are pseudorefs:
*
* - FETCH_HEAD may contain multiple object IDs, and each one of them
* carries additional metadata like where it came from.
*
* - MERGE_HEAD may contain multiple object IDs when merging multiple
* heads.
*
* Reading, writing or deleting references must consistently go either
* through the filesystem (pseudorefs) or through the reference
* backend (normal ones).
*/
int is_pseudo_ref(const char *refname);
/* /*
* The following functions have been removed in Git v2.45 in favor of functions * The following functions have been removed in Git v2.45 in favor of functions

View File

@ -351,8 +351,7 @@ static void add_pseudoref_and_head_entries(struct ref_store *ref_store,
strbuf_addstr(&refname, de->d_name); strbuf_addstr(&refname, de->d_name);
dtype = get_dtype(de, &path, 1); dtype = get_dtype(de, &path, 1);
if (dtype == DT_REG && (is_pseudoref(ref_store, de->d_name) || if (dtype == DT_REG && is_root_ref(de->d_name))
is_headref(ref_store, de->d_name)))
loose_fill_ref_dir_regular_file(refs, refname.buf, dir); loose_fill_ref_dir_regular_file(refs, refname.buf, dir);
strbuf_setlen(&refname, dirnamelen); strbuf_setlen(&refname, dirnamelen);

View File

@ -354,8 +354,7 @@ static int reftable_ref_iterator_advance(struct ref_iterator *ref_iterator)
*/ */
if (!starts_with(iter->ref.refname, "refs/") && if (!starts_with(iter->ref.refname, "refs/") &&
!(iter->flags & DO_FOR_EACH_INCLUDE_ROOT_REFS && !(iter->flags & DO_FOR_EACH_INCLUDE_ROOT_REFS &&
(is_pseudoref(&iter->refs->base, iter->ref.refname) || is_root_ref(iter->ref.refname))) {
is_headref(&iter->refs->base, iter->ref.refname)))) {
continue; continue;
} }

View File

@ -518,7 +518,7 @@ test_expect_success 'fetch with a non-applying branch.<name>.merge' '
test_expect_success 'fetch from GIT URL with a non-applying branch.<name>.merge [1]' ' test_expect_success 'fetch from GIT URL with a non-applying branch.<name>.merge [1]' '
one_head=$(cd one && git rev-parse HEAD) && one_head=$(cd one && git rev-parse HEAD) &&
this_head=$(git rev-parse HEAD) && this_head=$(git rev-parse HEAD) &&
git update-ref -d FETCH_HEAD && rm .git/FETCH_HEAD &&
git fetch one && git fetch one &&
test $one_head = "$(git rev-parse --verify FETCH_HEAD)" && test $one_head = "$(git rev-parse --verify FETCH_HEAD)" &&
test $this_head = "$(git rev-parse --verify HEAD)" test $this_head = "$(git rev-parse --verify HEAD)"
@ -530,7 +530,7 @@ test_expect_success 'fetch from GIT URL with a non-applying branch.<name>.merge
one_ref=$(cd one && git symbolic-ref HEAD) && one_ref=$(cd one && git symbolic-ref HEAD) &&
git config branch.main.remote blub && git config branch.main.remote blub &&
git config branch.main.merge "$one_ref" && git config branch.main.merge "$one_ref" &&
git update-ref -d FETCH_HEAD && rm .git/FETCH_HEAD &&
git fetch one && git fetch one &&
test $one_head = "$(git rev-parse --verify FETCH_HEAD)" && test $one_head = "$(git rev-parse --verify FETCH_HEAD)" &&
test $this_head = "$(git rev-parse --verify HEAD)" test $this_head = "$(git rev-parse --verify HEAD)"
@ -540,7 +540,7 @@ test_expect_success 'fetch from GIT URL with a non-applying branch.<name>.merge
# the merge spec does not match the branch the remote HEAD points to # the merge spec does not match the branch the remote HEAD points to
test_expect_success 'fetch from GIT URL with a non-applying branch.<name>.merge [3]' ' test_expect_success 'fetch from GIT URL with a non-applying branch.<name>.merge [3]' '
git config branch.main.merge "${one_ref}_not" && git config branch.main.merge "${one_ref}_not" &&
git update-ref -d FETCH_HEAD && rm .git/FETCH_HEAD &&
git fetch one && git fetch one &&
test $one_head = "$(git rev-parse --verify FETCH_HEAD)" && test $one_head = "$(git rev-parse --verify FETCH_HEAD)" &&
test $this_head = "$(git rev-parse --verify HEAD)" test $this_head = "$(git rev-parse --verify HEAD)"

View File

@ -52,6 +52,23 @@ test_expect_success '--include-root-refs pattern prints pseudorefs' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--include-root-refs pattern does not print special refs' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
git rev-parse HEAD >.git/MERGE_HEAD &&
git for-each-ref --format="%(refname)" --include-root-refs >actual &&
cat >expect <<-EOF &&
HEAD
$(git symbolic-ref HEAD)
refs/tags/initial
EOF
test_cmp expect actual
)
'
test_expect_success '--include-root-refs with other patterns' ' test_expect_success '--include-root-refs with other patterns' '
cat >expect <<-\EOF && cat >expect <<-\EOF &&
HEAD HEAD
@ -62,6 +79,23 @@ test_expect_success '--include-root-refs with other patterns' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--include-root-refs omits dangling symrefs' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
git symbolic-ref DANGLING_HEAD refs/heads/missing &&
cat >expect <<-EOF &&
HEAD
$(git symbolic-ref HEAD)
refs/tags/initial
EOF
git for-each-ref --format="%(refname)" --include-root-refs >actual &&
test_cmp expect actual
)
'
test_expect_success 'filtering with --points-at' ' test_expect_success 'filtering with --points-at' '
cat >expect <<-\EOF && cat >expect <<-\EOF &&
refs/heads/main refs/heads/main