From: Patrick Steinhardt <hidden> Date: 2024-02-19 14:35:19
Hi,
this patch series introduces a new `git reflog list` subcommand that
lists all reflogs of the current repository. This addresses an issue
with discoverability as there is no way for a user to learn about which
reflogs exist. While this isn't all that bad with the "files" backend as
a user could in the worst case figure out which reflogs exist by walking
the ".git/logs" directory, with the "reftable" backend it's basically
impossible to achieve this.
While I think this is sufficient motivation to have such a subcommand
nowadays already, I think the need for such a thing will grow in the
future. It was noted in multiple threads that we may eventually want to
lift the artificial limitations in the "reftable" backend where reflogs
are deleted together with their refs. This limitation is inherited from
the "files" backend, which may otherwise hit issues with directory/file
conflicts if it didn't delete reflogs.
Once that limitation is lifted for the "reftable" backend though, it
will become even more important to give users the tools to discover
reflogs that do not have a corresponding ref.
The series is structured as follows:
- Patches 1-3 extend the dir iterator so that it can sort directory
entries lexicographically. This is required such that we can list
reflogs with deterministic ordering.
- Patch 4 refactors the reflog iterator interface to demonstrate that
the object ID and flags aren't needed nowadays, and patch 5 builds
on top of that and stops resolving the refs altogether. This allows
us to also surface reflogs of broken refs.
- Patch 6 introduces the new subcommand.
The series depends on Junio's ps/reftable-backend at 8a0bebdeae
(refs/reftable: fix leak when copying reflog fails, 2024-02-08): the
change in behaviour in patches 4 and 5 apply to both backends.
Patrick
Patrick Steinhardt (6):
dir-iterator: pass name to `prepare_next_entry_data()` directly
dir-iterator: support iteration in sorted order
refs/files: sort reflogs returned by the reflog iterator
refs: drop unused params from the reflog iterator callback
refs: stop resolving ref corresponding to reflogs
builtin/reflog: introduce subcommand to list reflogs
Documentation/git-reflog.txt | 3 ++
builtin/fsck.c | 4 +-
builtin/reflog.c | 37 +++++++++++++-
dir-iterator.c | 93 ++++++++++++++++++++++++++--------
dir-iterator.h | 3 ++
refs.c | 23 +++++++--
refs.h | 11 +++-
refs/files-backend.c | 22 ++------
refs/reftable-backend.c | 12 +----
revision.c | 4 +-
t/helper/test-ref-store.c | 18 ++++---
t/t0600-reffiles-backend.sh | 24 ++++-----
t/t1405-main-ref-store.sh | 8 +--
t/t1406-submodule-ref-store.sh | 8 +--
t/t1410-reflog.sh | 69 +++++++++++++++++++++++++
15 files changed, 251 insertions(+), 88 deletions(-)
--
2.44.0-rc1
From: Patrick Steinhardt <hidden> Date: 2024-02-19 14:35:21
When adding the next directory entry for `struct dir_iterator` we pass
the complete `struct dirent *` to `prepare_next_entry_data()` even
though we only need the entry's name.
Refactor the code to pass in the name, only. This prepares for a
subsequent commit where we introduce the ability to iterate through
dir entries in an ordered manner.
Signed-off-by: Patrick Steinhardt <redacted>
---
dir-iterator.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-19 14:35:25
The `struct dir_iterator` is a helper that allows us to iterate through
directory entries. This iterator returns entries in the exact same order
as readdir(3P) does -- or in other words, it guarantees no specific
order at all.
This is about to become problematic as we are introducing a new reflog
subcommand to list reflogs. As the "files" backend uses the directory
iterator to enumerate reflogs, returning reflog names and exposing them
to the user would inherit the indeterministic ordering. Naturally, it
would make for a terrible user interface to show a list with no
discernible order. While this could be handled at a higher level by the
new subcommand itself by collecting and ordering the reflogs, this would
be inefficient and introduce latency when there are many reflogs.
Instead, introduce a new option into the directory iterator that asks
for its entries to be yielded in lexicographical order. If set, the
iterator will read all directory entries greedily end sort them before
we start to iterate over them.
While this will of course also incur overhead as we cannot yield the
directory entries immediately, it should at least be more efficient than
having to sort the complete list of reflogs as we only need to sort one
directory at a time.
This functionality will be used in a follow-up commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
dir-iterator.c | 87 ++++++++++++++++++++++++++++++++++++++++----------
dir-iterator.h | 3 ++
2 files changed, 73 insertions(+), 17 deletions(-)
@@ -136,30 +174,43 @@ int dir_iterator_advance(struct dir_iterator *dir_iterator)/* Loop until we find an entry that we can give back to the caller. */while(1){-structdirent*de;structdir_iterator_level*level=&iter->levels[iter->levels_nr-1];+structdirent*de;+constchar*name;strbuf_setlen(&iter->base.path,level->prefix_len);-errno=0;-de=readdir(level->dir);--if(!de){-if(errno){-warning_errno("error reading directory '%s'",-iter->base.path.buf);-if(iter->flags&DIR_ITERATOR_PEDANTIC)-gotoerror_out;-}elseif(pop_level(iter)==0){-returndir_iterator_abort(dir_iterator);++if(level->dir){+errno=0;+de=readdir(level->dir);+if(!de){+if(errno){+warning_errno("error reading directory '%s'",+iter->base.path.buf);+if(iter->flags&DIR_ITERATOR_PEDANTIC)+gotoerror_out;+}elseif(pop_level(iter)==0){+returndir_iterator_abort(dir_iterator);+}+continue;}-continue;-}-if(is_dot_or_dotdot(de->d_name))-continue;+if(is_dot_or_dotdot(de->d_name))+continue;-if(prepare_next_entry_data(iter,de->d_name)){+name=de->d_name;+}else{+if(level->entries_idx>=level->entries.nr){+if(pop_level(iter)==0)+returndir_iterator_abort(dir_iterator);+continue;+}++name=level->entries.items[level->entries_idx++].string;+}++if(prepare_next_entry_data(iter,name)){if(errno!=ENOENT&&iter->flags&DIR_ITERATOR_PEDANTIC)gotoerror_out;continue;
From: Patrick Steinhardt <hidden> Date: 2024-02-19 14:35:30
We use a directory iterator to return reflogs via the reflog iterator.
This iterator returns entries in the same order as readdir(3P) would and
will thus yield reflogs with no discernible order.
Set the new `DIR_ITERATOR_SORTED` flag that was introduced in the
preceding commit so that the order is deterministic. While the effect of
this can only been observed in a test tool, a subsequent commit will
start to expose this functionality to users via a new `git reflog list`
subcommand.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs/files-backend.c | 4 ++--
t/t0600-reffiles-backend.sh | 4 ++--
t/t1405-main-ref-store.sh | 2 +-
t/t1406-submodule-ref-store.sh | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-19 14:35:34
The ref and reflog iterators share much of the same underlying code to
iterate over the corresponding entries. This results in some weird code
because the reflog iterator also exposes an object ID as well as a flag
to the callback function. Neither of these fields do refer to the reflog
though -- they refer to the corresponding ref with the same name. This
is quite misleading. In practice at least the object ID cannot really be
implemented in any other way as a reflog does not have a specific object
ID in the first place. This is further stressed by the fact that none of
the callbacks except for our test helper make use of these fields.
Split up the infrastucture so that ref and reflog iterators use separate
callback signatures. This allows us to drop the nonsensical fields from
the reflog iterator.
Note that internally, the backends still use the same shared infra to
iterate over both types. As the backends should never end up being
called directly anyway, this is not much of a problem and thus kept
as-is for simplicity's sake.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fsck.c | 4 +---
builtin/reflog.c | 3 +--
refs.c | 23 +++++++++++++++++++----
refs.h | 11 +++++++++--
refs/files-backend.c | 8 +-------
refs/reftable-backend.c | 8 +-------
revision.c | 4 +---
t/helper/test-ref-store.c | 18 ++++++++++++------
t/t0600-reffiles-backend.sh | 24 ++++++++++++------------
t/t1405-main-ref-store.sh | 8 ++++----
t/t1406-submodule-ref-store.sh | 8 ++++----
11 files changed, 65 insertions(+), 54 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-19 14:35:39
The reflog iterator tries to resolve the corresponding ref for every
reflog that it is about to yield. Historically, this was done due to
multiple reasons:
- It ensures that the refname is safe because we end up calling
`check_refname_format()`. Also, non-conformant refnames are skipped
altogether.
- The iterator used to yield the resolved object ID as well as its
flags to the callback. This info was never used though, and the
corresponding parameters were dropped in the preceding commit.
- When a ref is corrupt then the reflog is not emitted at all.
We're about to introduce a new `git reflog list` subcommand that will
print all reflogs that the refdb knows about. Skipping over reflogs
whose refs are corrupted would be quite counterproductive in this case
as the user would have no way to learn about reflogs which may still
exist in their repository to help and rescue such a corrupted ref. Thus,
the only remaining reason for why we'd want to resolve the ref is to
verify its refname.
Refactor the code to call `check_refname_format()` directly instead of
trying to resolve the ref. This is significantly more efficient given
that we don't have to hit the object database anymore to list reflogs.
And second, it ensures that we end up showing reflogs of broken refs,
which will help to make the reflog more useful.
Note that this really only impacts the case where the corresponding ref
is corrupt. Reflogs for nonexistent refs would have been returned to the
caller beforehand already as we did not pass `RESOLVE_REF_READING` to
the function, and thus `refs_resolve_ref_unsafe()` would have returned
successfully in that case.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs/files-backend.c | 12 ++----------
refs/reftable-backend.c | 6 ++----
2 files changed, 4 insertions(+), 14 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-19 14:35:43
While the git-reflog(1) command has subcommands to show reflog entries
or check for reflog existence, it does not have any subcommands that
would allow the user to enumerate all existing reflogs. This makes it
quite hard to discover which reflogs a repository has. While this can
be worked around with the "files" backend by enumerating files in the
".git/logs" directory, users of the "reftable" backend don't enjoy such
a luxury.
Introduce a new subcommand `git reflog list` that lists all reflogs the
repository knows of to fill this gap.
Signed-off-by: Patrick Steinhardt <redacted>
---
Documentation/git-reflog.txt | 3 ++
builtin/reflog.c | 34 ++++++++++++++++++
t/t1410-reflog.sh | 69 ++++++++++++++++++++++++++++++++++++
3 files changed, 106 insertions(+)
@@ -39,6 +40,8 @@ actions, and in addition the `HEAD` reflog records branch switching. `git reflog show` is an alias for `git log -g --abbrev-commit --pretty=oneline`; see linkgit:git-log[1] for more information.+The "list" subcommand lists all refs which have a corresponding reflog.+ The "expire" subcommand prunes older reflog entries. Entries older than `expire` time, or entries older than `expire-unreachable` time and not reachable from the current tip, are removed from the reflog.
@@ -436,4 +436,73 @@ test_expect_success 'empty reflog' 'test_must_be_emptyerr'+test_expect_success'list reflogs''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+gitrefloglist>actual&&+test_must_be_emptyactual&&++test_commitA&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual&&++gitbranchb&&+cat>expect<<-EOF&&+HEAD+refs/heads/b+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'++test_expect_success'reflog list returns error with additional args''+cat>expect<<-EOF&&+error:listdoesnotacceptarguments:${SQ}bogus${SQ}+EOF+test_must_failgitrefloglistbogus2>err&&+test_cmpexpecterr+'++test_expect_success'reflog for symref with unborn target can be listed''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+test_commitA&&+gitsymbolic-refHEADrefs/heads/unborn&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'++test_expect_success'reflog with invalid object ID can be listed''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+test_commitA&&+test-toolref-storemainupdate-refmsgrefs/heads/missing\+$(test_oiddeadbeef)"$ZERO_OID"REF_SKIP_OID_VERIFICATION&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+refs/heads/missing+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'+ test_done
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:22
Hi,
this is the second version of my patch series that introduces a new `git
reflog list` subcommand to list available reflogs in a repository.
Changes compared to v1:
- Patch 2: Clarified the commit message to hopefully explain better
why a higher level implementation of reflog sorting would have
increased latency.
- Patch 2: Introduced a helper function that unifies the logic to
yield the next directory entry.
- Patch 3: Mark the merged reflog iterator as sorted, which I missed
in my previous round.
- Patch 4: This patch is new and simplifies the code to require all
ref iterators to be sorted.
Junio, I noticed that you already merged v1 of this patch series to
`next`. I was a bit surprised to see it merged down this fast, so I
assume that this is only done due to the pending Git v2.44 release and
that you plan to reroll `next` anyway. I thus didn't send follow-up
patches but resent the whole patch series as v2. If I misinterpreted
your intent I'm happy to send the changes as follow-up patches instead.
The patch series continues to depend on ps/reftable-backend at
8a0bebdeae (refs/reftable: fix leak when copying reflog fails,
2024-02-08).
Thanks!
Patrick
Patrick Steinhardt (7):
dir-iterator: pass name to `prepare_next_entry_data()` directly
dir-iterator: support iteration in sorted order
refs/files: sort reflogs returned by the reflog iterator
refs: always treat iterators as ordered
refs: drop unused params from the reflog iterator callback
refs: stop resolving ref corresponding to reflogs
builtin/reflog: introduce subcommand to list reflogs
Documentation/git-reflog.txt | 3 +
builtin/fsck.c | 4 +-
builtin/reflog.c | 37 +++++++++++-
dir-iterator.c | 105 ++++++++++++++++++++++++++++-----
dir-iterator.h | 3 +
refs.c | 27 ++++++---
refs.h | 11 +++-
refs/debug.c | 3 +-
refs/files-backend.c | 27 ++-------
refs/iterator.c | 26 +++-----
refs/packed-backend.c | 2 +-
refs/ref-cache.c | 2 +-
refs/refs-internal.h | 18 +-----
refs/reftable-backend.c | 20 ++-----
revision.c | 4 +-
t/helper/test-ref-store.c | 18 ++++--
t/t0600-reffiles-backend.sh | 24 ++++----
t/t1405-main-ref-store.sh | 8 +--
t/t1406-submodule-ref-store.sh | 8 +--
t/t1410-reflog.sh | 69 ++++++++++++++++++++++
20 files changed, 286 insertions(+), 133 deletions(-)
Range-diff against v1:
1: 12de25dfe2 = 1: 12de25dfe2 dir-iterator: pass name to `prepare_next_entry_data()` directly
2: 8a588175db ! 2: 788afce189 dir-iterator: support iteration in sorted order
@@ Commit message
iterator to enumerate reflogs, returning reflog names and exposing them
to the user would inherit the indeterministic ordering. Naturally, it
would make for a terrible user interface to show a list with no
- discernible order. While this could be handled at a higher level by the
- new subcommand itself by collecting and ordering the reflogs, this would
- be inefficient and introduce latency when there are many reflogs.
+ discernible order.
+
+ While this could be handled at a higher level by the new subcommand
+ itself by collecting and ordering the reflogs, this would be inefficient
+ because we would first have to collect all reflogs before we can sort
+ them, which would introduce additional latency when there are many
+ reflogs.
Instead, introduce a new option into the directory iterator that asks
for its entries to be yielded in lexicographical order. If set, the
- iterator will read all directory entries greedily end sort them before
+ iterator will read all directory entries greedily and sort them before
we start to iterate over them.
While this will of course also incur overhead as we cannot yield the
@@ dir-iterator.c
struct dir_iterator_level {
DIR *dir;
+
++ /*
++ * The directory entries of the current level. This list will only be
++ * populated when the iterator is ordered. In that case, `dir` will be
++ * set to `NULL`.
++ */
+ struct string_list entries;
+ size_t entries_idx;
-
++
/*
* The length of the directory part of path at this level
+ * (including a trailing '/'):
+@@ dir-iterator.c: struct dir_iterator_int {
+ unsigned int flags;
+ };
+
++static int next_directory_entry(DIR *dir, const char *path,
++ struct dirent **out)
++{
++ struct dirent *de;
++
++repeat:
++ errno = 0;
++ de = readdir(dir);
++ if (!de) {
++ if (errno) {
++ warning_errno("error reading directory '%s'",
++ path);
++ return -1;
++ }
++
++ return 1;
++ }
++
++ if (is_dot_or_dotdot(de->d_name))
++ goto repeat;
++
++ *out = de;
++ return 0;
++}
++
+ /*
+ * Push a level in the iter stack and initialize it with information from
+ * the directory pointed by iter->base->path. It is assumed that this
@@ dir-iterator.c: static int push_level(struct dir_iterator_int *iter)
return -1;
}
@@ dir-iterator.c: static int push_level(struct dir_iterator_int *iter)
+ * directly.
+ */
+ if (iter->flags & DIR_ITERATOR_SORTED) {
-+ while (1) {
-+ struct dirent *de;
++ struct dirent *de;
+
-+ errno = 0;
-+ de = readdir(level->dir);
-+ if (!de) {
-+ if (errno && errno != ENOENT) {
-+ warning_errno("error reading directory '%s'",
-+ iter->base.path.buf);
++ while (1) {
++ int ret = next_directory_entry(level->dir, iter->base.path.buf, &de);
++ if (ret < 0) {
++ if (errno != ENOENT &&
++ iter->flags & DIR_ITERATOR_PEDANTIC)
+ return -1;
-+ }
-+
++ continue;
++ } else if (ret > 0) {
+ break;
+ }
+
-+ if (is_dot_or_dotdot(de->d_name))
-+ continue;
-+
+ string_list_append(&level->entries, de->d_name);
+ }
+ string_list_sort(&level->entries);
@@ dir-iterator.c: static int pop_level(struct dir_iterator_int *iter)
return --iter->levels_nr;
}
@@ dir-iterator.c: int dir_iterator_advance(struct dir_iterator *dir_iterator)
-
- /* Loop until we find an entry that we can give back to the caller. */
- while (1) {
-- struct dirent *de;
+ struct dirent *de;
struct dir_iterator_level *level =
&iter->levels[iter->levels_nr - 1];
-+ struct dirent *de;
+ const char *name;
strbuf_setlen(&iter->base.path, level->prefix_len);
- errno = 0;
- de = readdir(level->dir);
--
+
- if (!de) {
- if (errno) {
- warning_errno("error reading directory '%s'",
- iter->base.path.buf);
-- if (iter->flags & DIR_ITERATOR_PEDANTIC)
-- goto error_out;
++ if (level->dir) {
++ int ret = next_directory_entry(level->dir, iter->base.path.buf, &de);
++ if (ret < 0) {
+ if (iter->flags & DIR_ITERATOR_PEDANTIC)
+ goto error_out;
- } else if (pop_level(iter) == 0) {
- return dir_iterator_abort(dir_iterator);
-+
-+ if (level->dir) {
-+ errno = 0;
-+ de = readdir(level->dir);
-+ if (!de) {
-+ if (errno) {
-+ warning_errno("error reading directory '%s'",
-+ iter->base.path.buf);
-+ if (iter->flags & DIR_ITERATOR_PEDANTIC)
-+ goto error_out;
-+ } else if (pop_level(iter) == 0) {
++ continue;
++ } else if (ret > 0) {
++ if (pop_level(iter) == 0)
+ return dir_iterator_abort(dir_iterator);
-+ }
+ continue;
}
- continue;
@@ dir-iterator.c: int dir_iterator_advance(struct dir_iterator *dir_iterator)
- if (is_dot_or_dotdot(de->d_name))
- continue;
-+ if (is_dot_or_dotdot(de->d_name))
-+ continue;
-
-- if (prepare_next_entry_data(iter, de->d_name)) {
+ name = de->d_name;
+ } else {
+ if (level->entries_idx >= level->entries.nr) {
@@ dir-iterator.c: int dir_iterator_advance(struct dir_iterator *dir_iterator)
+ return dir_iterator_abort(dir_iterator);
+ continue;
+ }
-+
+
+- if (prepare_next_entry_data(iter, de->d_name)) {
+ name = level->entries.items[level->entries_idx++].string;
+ }
+
3: e4e4fac05c ! 3: 32b24a3d4b refs/files: sort reflogs returned by the reflog iterator
@@ refs/files-backend.c: static struct ref_iterator *reflog_iterator_begin(struct r
iter->dir_iterator = diter;
iter->ref_store = ref_store;
strbuf_release(&sb);
+@@ refs/files-backend.c: static struct ref_iterator *files_reflog_iterator_begin(struct ref_store *ref_st
+ return reflog_iterator_begin(ref_store, refs->gitcommondir);
+ } else {
+ return merge_ref_iterator_begin(
+- 0, reflog_iterator_begin(ref_store, refs->base.gitdir),
++ 1, reflog_iterator_begin(ref_store, refs->base.gitdir),
+ reflog_iterator_begin(ref_store, refs->gitcommondir),
+ reflog_iterator_select, refs);
+ }
## t/t0600-reffiles-backend.sh ##
@@ t/t0600-reffiles-backend.sh: test_expect_success 'for_each_reflog()' '
-: ---------- > 4: 4254f23fd4 refs: always treat iterators as ordered
4: be512ef268 ! 5: 240334df6c refs: drop unused params from the reflog iterator callback
@@ refs/reftable-backend.c: static int reftable_reflog_iterator_advance(struct ref_
}
@@ refs/reftable-backend.c: static struct reftable_reflog_iterator *reflog_iterator_for_stack(struct reftabl
iter = xcalloc(1, sizeof(*iter));
- base_ref_iterator_init(&iter->base, &reftable_reflog_iterator_vtable, 1);
+ base_ref_iterator_init(&iter->base, &reftable_reflog_iterator_vtable);
iter->refs = refs;
- iter->base.oid = &iter->oid;
5: a7459b9483 = 6: 7928661318 refs: stop resolving ref corresponding to reflogs
6: cddb2de939 = 7: d7b9cff4c3 builtin/reflog: introduce subcommand to list reflogs
--
2.44.0-rc1
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:26
When adding the next directory entry for `struct dir_iterator` we pass
the complete `struct dirent *` to `prepare_next_entry_data()` even
though we only need the entry's name.
Refactor the code to pass in the name, only. This prepares for a
subsequent commit where we introduce the ability to iterate through
dir entries in an ordered manner.
Signed-off-by: Patrick Steinhardt <redacted>
---
dir-iterator.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:30
The `struct dir_iterator` is a helper that allows us to iterate through
directory entries. This iterator returns entries in the exact same order
as readdir(3P) does -- or in other words, it guarantees no specific
order at all.
This is about to become problematic as we are introducing a new reflog
subcommand to list reflogs. As the "files" backend uses the directory
iterator to enumerate reflogs, returning reflog names and exposing them
to the user would inherit the indeterministic ordering. Naturally, it
would make for a terrible user interface to show a list with no
discernible order.
While this could be handled at a higher level by the new subcommand
itself by collecting and ordering the reflogs, this would be inefficient
because we would first have to collect all reflogs before we can sort
them, which would introduce additional latency when there are many
reflogs.
Instead, introduce a new option into the directory iterator that asks
for its entries to be yielded in lexicographical order. If set, the
iterator will read all directory entries greedily and sort them before
we start to iterate over them.
While this will of course also incur overhead as we cannot yield the
directory entries immediately, it should at least be more efficient than
having to sort the complete list of reflogs as we only need to sort one
directory at a time.
This functionality will be used in a follow-up commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
dir-iterator.c | 99 +++++++++++++++++++++++++++++++++++++++++++-------
dir-iterator.h | 3 ++
2 files changed, 89 insertions(+), 13 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:33
We use a directory iterator to return reflogs via the reflog iterator.
This iterator returns entries in the same order as readdir(3P) would and
will thus yield reflogs with no discernible order.
Set the new `DIR_ITERATOR_SORTED` flag that was introduced in the
preceding commit so that the order is deterministic. While the effect of
this can only been observed in a test tool, a subsequent commit will
start to expose this functionality to users via a new `git reflog list`
subcommand.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs/files-backend.c | 6 +++---
t/t0600-reffiles-backend.sh | 4 ++--
t/t1405-main-ref-store.sh | 2 +-
t/t1406-submodule-ref-store.sh | 2 +-
4 files changed, 7 insertions(+), 7 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:38
In the preceding commit we have converted the reflog iterator of the
"files" backend to be ordered, which was the only remaining ref iterator
that wasn't ordered. Refactor the ref iterator infrastructure so that we
always assume iterators to be ordered, thus simplifying the code.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs.c | 4 ----
refs/debug.c | 3 +--
refs/files-backend.c | 7 +++----
refs/iterator.c | 26 ++++++++------------------
refs/packed-backend.c | 2 +-
refs/ref-cache.c | 2 +-
refs/refs-internal.h | 18 ++----------------
refs/reftable-backend.c | 8 ++++----
8 files changed, 20 insertions(+), 50 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:42
The ref and reflog iterators share much of the same underlying code to
iterate over the corresponding entries. This results in some weird code
because the reflog iterator also exposes an object ID as well as a flag
to the callback function. Neither of these fields do refer to the reflog
though -- they refer to the corresponding ref with the same name. This
is quite misleading. In practice at least the object ID cannot really be
implemented in any other way as a reflog does not have a specific object
ID in the first place. This is further stressed by the fact that none of
the callbacks except for our test helper make use of these fields.
Split up the infrastucture so that ref and reflog iterators use separate
callback signatures. This allows us to drop the nonsensical fields from
the reflog iterator.
Note that internally, the backends still use the same shared infra to
iterate over both types. As the backends should never end up being
called directly anyway, this is not much of a problem and thus kept
as-is for simplicity's sake.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fsck.c | 4 +---
builtin/reflog.c | 3 +--
refs.c | 23 +++++++++++++++++++----
refs.h | 11 +++++++++--
refs/files-backend.c | 8 +-------
refs/reftable-backend.c | 8 +-------
revision.c | 4 +---
t/helper/test-ref-store.c | 18 ++++++++++++------
t/t0600-reffiles-backend.sh | 24 ++++++++++++------------
t/t1405-main-ref-store.sh | 8 ++++----
t/t1406-submodule-ref-store.sh | 8 ++++----
11 files changed, 65 insertions(+), 54 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:47
The reflog iterator tries to resolve the corresponding ref for every
reflog that it is about to yield. Historically, this was done due to
multiple reasons:
- It ensures that the refname is safe because we end up calling
`check_refname_format()`. Also, non-conformant refnames are skipped
altogether.
- The iterator used to yield the resolved object ID as well as its
flags to the callback. This info was never used though, and the
corresponding parameters were dropped in the preceding commit.
- When a ref is corrupt then the reflog is not emitted at all.
We're about to introduce a new `git reflog list` subcommand that will
print all reflogs that the refdb knows about. Skipping over reflogs
whose refs are corrupted would be quite counterproductive in this case
as the user would have no way to learn about reflogs which may still
exist in their repository to help and rescue such a corrupted ref. Thus,
the only remaining reason for why we'd want to resolve the ref is to
verify its refname.
Refactor the code to call `check_refname_format()` directly instead of
trying to resolve the ref. This is significantly more efficient given
that we don't have to hit the object database anymore to list reflogs.
And second, it ensures that we end up showing reflogs of broken refs,
which will help to make the reflog more useful.
Note that this really only impacts the case where the corresponding ref
is corrupt. Reflogs for nonexistent refs would have been returned to the
caller beforehand already as we did not pass `RESOLVE_REF_READING` to
the function, and thus `refs_resolve_ref_unsafe()` would have returned
successfully in that case.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs/files-backend.c | 12 ++----------
refs/reftable-backend.c | 6 ++----
2 files changed, 4 insertions(+), 14 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-20 09:06:51
While the git-reflog(1) command has subcommands to show reflog entries
or check for reflog existence, it does not have any subcommands that
would allow the user to enumerate all existing reflogs. This makes it
quite hard to discover which reflogs a repository has. While this can
be worked around with the "files" backend by enumerating files in the
".git/logs" directory, users of the "reftable" backend don't enjoy such
a luxury.
Introduce a new subcommand `git reflog list` that lists all reflogs the
repository knows of to fill this gap.
Signed-off-by: Patrick Steinhardt <redacted>
---
Documentation/git-reflog.txt | 3 ++
builtin/reflog.c | 34 ++++++++++++++++++
t/t1410-reflog.sh | 69 ++++++++++++++++++++++++++++++++++++
3 files changed, 106 insertions(+)
@@ -39,6 +40,8 @@ actions, and in addition the `HEAD` reflog records branch switching. `git reflog show` is an alias for `git log -g --abbrev-commit --pretty=oneline`; see linkgit:git-log[1] for more information.+The "list" subcommand lists all refs which have a corresponding reflog.+ The "expire" subcommand prunes older reflog entries. Entries older than `expire` time, or entries older than `expire-unreachable` time and not reachable from the current tip, are removed from the reflog.
@@ -436,4 +436,73 @@ test_expect_success 'empty reflog' 'test_must_be_emptyerr'+test_expect_success'list reflogs''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+gitrefloglist>actual&&+test_must_be_emptyactual&&++test_commitA&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual&&++gitbranchb&&+cat>expect<<-EOF&&+HEAD+refs/heads/b+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'++test_expect_success'reflog list returns error with additional args''+cat>expect<<-EOF&&+error:listdoesnotacceptarguments:${SQ}bogus${SQ}+EOF+test_must_failgitrefloglistbogus2>err&&+test_cmpexpecterr+'++test_expect_success'reflog for symref with unborn target can be listed''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+test_commitA&&+gitsymbolic-refHEADrefs/heads/unborn&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'++test_expect_success'reflog with invalid object ID can be listed''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+test_commitA&&+test-toolref-storemainupdate-refmsgrefs/heads/missing\+$(test_oiddeadbeef)"$ZERO_OID"REF_SKIP_OID_VERIFICATION&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+refs/heads/missing+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'+ test_done
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:23
When adding the next directory entry for `struct dir_iterator` we pass
the complete `struct dirent *` to `prepare_next_entry_data()` even
though we only need the entry's name.
Refactor the code to pass in the name, only. This prepares for a
subsequent commit where we introduce the ability to iterate through
dir entries in an ordered manner.
Signed-off-by: Patrick Steinhardt <redacted>
---
dir-iterator.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:27
The `struct dir_iterator` is a helper that allows us to iterate through
directory entries. This iterator returns entries in the exact same order
as readdir(3P) does -- or in other words, it guarantees no specific
order at all.
This is about to become problematic as we are introducing a new reflog
subcommand to list reflogs. As the "files" backend uses the directory
iterator to enumerate reflogs, returning reflog names and exposing them
to the user would inherit the indeterministic ordering. Naturally, it
would make for a terrible user interface to show a list with no
discernible order.
While this could be handled at a higher level by the new subcommand
itself by collecting and ordering the reflogs, this would be inefficient
because we would first have to collect all reflogs before we can sort
them, which would introduce additional latency when there are many
reflogs.
Instead, introduce a new option into the directory iterator that asks
for its entries to be yielded in lexicographical order. If set, the
iterator will read all directory entries greedily and sort them before
we start to iterate over them.
While this will of course also incur overhead as we cannot yield the
directory entries immediately, it should at least be more efficient than
having to sort the complete list of reflogs as we only need to sort one
directory at a time.
This functionality will be used in a follow-up commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
dir-iterator.c | 99 +++++++++++++++++++++++++++++++++++++++++++-------
dir-iterator.h | 3 ++
2 files changed, 89 insertions(+), 13 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:31
We use a directory iterator to return reflogs via the reflog iterator.
This iterator returns entries in the same order as readdir(3P) would and
will thus yield reflogs with no discernible order.
Set the new `DIR_ITERATOR_SORTED` flag that was introduced in the
preceding commit so that the order is deterministic. While the effect of
this can only been observed in a test tool, a subsequent commit will
start to expose this functionality to users via a new `git reflog list`
subcommand.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs/files-backend.c | 4 ++--
t/t0600-reffiles-backend.sh | 4 ++--
t/t1405-main-ref-store.sh | 2 +-
t/t1406-submodule-ref-store.sh | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:35
When iterating through reflogs in a worktree we create a merged iterator
that merges reflogs from both refdbs. The resulting refs are ordered so
that instead we first return all worktree reflogs before we return all
common refs.
This is the only remaining case where a ref iterator returns entries in
a non-lexicographic order. The result would look something like the
following (listed with a command we introduce in a subsequent commit):
So we first print the per-worktree reflogs in lexicographic order, then
the common reflogs in lexicographic order. This is confusing and not
consistent with how we print per-worktree refs, which are exclusively
sorted lexicographically.
Sort reflogs lexicographically in the same way as we sort normal refs.
As this is already implemented properly by the "reftable" backend via a
separate selection function, we simply pull out that logic and reuse it
for the "files" backend. As logs are properly sorted now, mark the
merged reflog iterator as sorted.
Tests will be added in a subsequent commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs/files-backend.c | 30 ++------------------------
refs/iterator.c | 43 +++++++++++++++++++++++++++++++++++++
refs/refs-internal.h | 9 ++++++++
refs/reftable-backend.c | 47 ++---------------------------------------
4 files changed, 56 insertions(+), 73 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:39
In the preceding commit we have converted the reflog iterator of the
"files" backend to be ordered, which was the only remaining ref iterator
that wasn't ordered. Refactor the ref iterator infrastructure so that we
always assume iterators to be ordered, thus simplifying the code.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs.c | 4 ----
refs/debug.c | 3 +--
refs/files-backend.c | 7 +++----
refs/iterator.c | 26 ++++++++------------------
refs/packed-backend.c | 2 +-
refs/ref-cache.c | 2 +-
refs/refs-internal.h | 18 ++----------------
refs/reftable-backend.c | 8 ++++----
8 files changed, 20 insertions(+), 50 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:43
The ref and reflog iterators share much of the same underlying code to
iterate over the corresponding entries. This results in some weird code
because the reflog iterator also exposes an object ID as well as a flag
to the callback function. Neither of these fields do refer to the reflog
though -- they refer to the corresponding ref with the same name. This
is quite misleading. In practice at least the object ID cannot really be
implemented in any other way as a reflog does not have a specific object
ID in the first place. This is further stressed by the fact that none of
the callbacks except for our test helper make use of these fields.
Split up the infrastucture so that ref and reflog iterators use separate
callback signatures. This allows us to drop the nonsensical fields from
the reflog iterator.
Note that internally, the backends still use the same shared infra to
iterate over both types. As the backends should never end up being
called directly anyway, this is not much of a problem and thus kept
as-is for simplicity's sake.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fsck.c | 4 +---
builtin/reflog.c | 3 +--
refs.c | 23 +++++++++++++++++++----
refs.h | 11 +++++++++--
refs/files-backend.c | 8 +-------
refs/reftable-backend.c | 8 +-------
revision.c | 4 +---
t/helper/test-ref-store.c | 18 ++++++++++++------
t/t0600-reffiles-backend.sh | 24 ++++++++++++------------
t/t1405-main-ref-store.sh | 8 ++++----
t/t1406-submodule-ref-store.sh | 8 ++++----
11 files changed, 65 insertions(+), 54 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:47
The reflog iterator tries to resolve the corresponding ref for every
reflog that it is about to yield. Historically, this was done due to
multiple reasons:
- It ensures that the refname is safe because we end up calling
`check_refname_format()`. Also, non-conformant refnames are skipped
altogether.
- The iterator used to yield the resolved object ID as well as its
flags to the callback. This info was never used though, and the
corresponding parameters were dropped in the preceding commit.
- When a ref is corrupt then the reflog is not emitted at all.
We're about to introduce a new `git reflog list` subcommand that will
print all reflogs that the refdb knows about. Skipping over reflogs
whose refs are corrupted would be quite counterproductive in this case
as the user would have no way to learn about reflogs which may still
exist in their repository to help and rescue such a corrupted ref. Thus,
the only remaining reason for why we'd want to resolve the ref is to
verify its refname.
Refactor the code to call `check_refname_format()` directly instead of
trying to resolve the ref. This is significantly more efficient given
that we don't have to hit the object database anymore to list reflogs.
And second, it ensures that we end up showing reflogs of broken refs,
which will help to make the reflog more useful.
Note that this really only impacts the case where the corresponding ref
is corrupt. Reflogs for nonexistent refs would have been returned to the
caller beforehand already as we did not pass `RESOLVE_REF_READING` to
the function, and thus `refs_resolve_ref_unsafe()` would have returned
successfully in that case.
Signed-off-by: Patrick Steinhardt <redacted>
---
refs/files-backend.c | 12 ++----------
refs/reftable-backend.c | 6 ++----
2 files changed, 4 insertions(+), 14 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2024-02-21 12:37:51
While the git-reflog(1) command has subcommands to show reflog entries
or check for reflog existence, it does not have any subcommands that
would allow the user to enumerate all existing reflogs. This makes it
quite hard to discover which reflogs a repository has. While this can
be worked around with the "files" backend by enumerating files in the
".git/logs" directory, users of the "reftable" backend don't enjoy such
a luxury.
Introduce a new subcommand `git reflog list` that lists all reflogs the
repository knows of to fill this gap.
Signed-off-by: Patrick Steinhardt <redacted>
---
Documentation/git-reflog.txt | 3 +
builtin/reflog.c | 34 +++++++++++
t/t1410-reflog.sh | 108 +++++++++++++++++++++++++++++++++++
3 files changed, 145 insertions(+)
@@ -39,6 +40,8 @@ actions, and in addition the `HEAD` reflog records branch switching. `git reflog show` is an alias for `git log -g --abbrev-commit --pretty=oneline`; see linkgit:git-log[1] for more information.+The "list" subcommand lists all refs which have a corresponding reflog.+ The "expire" subcommand prunes older reflog entries. Entries older than `expire` time, or entries older than `expire-unreachable` time and not reachable from the current tip, are removed from the reflog.
@@ -436,4 +436,112 @@ test_expect_success 'empty reflog' 'test_must_be_emptyerr'+test_expect_success'list reflogs''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+gitrefloglist>actual&&+test_must_be_emptyactual&&++test_commitA&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual&&++gitbranchb&&+cat>expect<<-EOF&&+HEAD+refs/heads/b+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'++test_expect_success'list reflogs with worktree''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&++test_commitA&&+gitworktreeaddwt&&+git-ccore.logAllRefUpdates=always\+update-refrefs/worktree/mainHEAD&&+git-ccore.logAllRefUpdates=always\+update-refrefs/worktree/per-worktreeHEAD&&+git-ccore.logAllRefUpdates=always-Cwt\+update-refrefs/worktree/per-worktreeHEAD&&+git-ccore.logAllRefUpdates=always-Cwt\+update-refrefs/worktree/worktreeHEAD&&++cat>expect<<-EOF&&+HEAD+refs/heads/main+refs/heads/wt+refs/worktree/main+refs/worktree/per-worktree+EOF+gitrefloglist>actual&&+test_cmpexpectactual&&++cat>expect<<-EOF&&+HEAD+refs/heads/main+refs/heads/wt+refs/worktree/per-worktree+refs/worktree/worktree+EOF+git-Cwtrefloglist>actual&&+test_cmpexpectactual+)+'++test_expect_success'reflog list returns error with additional args''+cat>expect<<-EOF&&+error:listdoesnotacceptarguments:${SQ}bogus${SQ}+EOF+test_must_failgitrefloglistbogus2>err&&+test_cmpexpecterr+'++test_expect_success'reflog for symref with unborn target can be listed''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+test_commitA&&+gitsymbolic-refHEADrefs/heads/unborn&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'++test_expect_success'reflog with invalid object ID can be listed''+test_when_finished"rm -rf repo"&&+gitinitrepo&&+(+cdrepo&&+test_commitA&&+test-toolref-storemainupdate-refmsgrefs/heads/missing\+$(test_oiddeadbeef)"$ZERO_OID"REF_SKIP_OID_VERIFICATION&&+cat>expect<<-EOF&&+HEAD+refs/heads/main+refs/heads/missing+EOF+gitrefloglist>actual&&+test_cmpexpectactual+)+'+ test_done
From: Patrick Steinhardt <hidden> Date: 2024-04-24 08:02:02
On Wed, Apr 24, 2024 at 03:30:47PM +0800, Teng Long wrote:
Patrick Steinhardt [off-list ref] wrote:
+#define BUILTIN_REFLOG_LIST_USAGE \
+ N_("git reflog list")
Doesn't seem to need a translation here?
I was following the precedent of the other subcommands, which all mark
their usage as needing translation. Whether that is ultimately warranted
I can't really tell. In any case, if we decide that it's not we should
also drop the marker for all the other usages.
Patrick