mirror of
https://github.com/git/git.git
synced 2024-11-23 09:56:28 +08:00
Merge branch 'ps/reflog-list' into HEAD
"git reflog" learned a "list" subcommand that enumerates known reflogs. * ps/reflog-list: builtin/reflog: introduce subcommand to list reflogs refs: stop resolving ref corresponding to reflogs refs: drop unused params from the reflog iterator callback refs: always treat iterators as ordered refs/files: sort merged worktree and common reflogs refs/files: sort reflogs returned by the reflog iterator dir-iterator: support iteration in sorted order dir-iterator: pass name to `prepare_next_entry_data()` directly
This commit is contained in:
commit
510a27e9e4
@ -10,6 +10,7 @@ SYNOPSIS
|
||||
--------
|
||||
[verse]
|
||||
'git reflog' [show] [<log-options>] [<ref>]
|
||||
'git reflog list'
|
||||
'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]
|
||||
[--rewrite] [--updateref] [--stale-fix]
|
||||
[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]
|
||||
@ -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.
|
||||
|
@ -509,9 +509,7 @@ static int fsck_handle_reflog_ent(struct object_id *ooid, struct object_id *noid
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fsck_handle_reflog(const char *logname,
|
||||
const struct object_id *oid UNUSED,
|
||||
int flag UNUSED, void *cb_data)
|
||||
static int fsck_handle_reflog(const char *logname, void *cb_data)
|
||||
{
|
||||
struct strbuf refname = STRBUF_INIT;
|
||||
|
||||
|
@ -7,11 +7,15 @@
|
||||
#include "wildmatch.h"
|
||||
#include "worktree.h"
|
||||
#include "reflog.h"
|
||||
#include "refs.h"
|
||||
#include "parse-options.h"
|
||||
|
||||
#define BUILTIN_REFLOG_SHOW_USAGE \
|
||||
N_("git reflog [show] [<log-options>] [<ref>]")
|
||||
|
||||
#define BUILTIN_REFLOG_LIST_USAGE \
|
||||
N_("git reflog list")
|
||||
|
||||
#define BUILTIN_REFLOG_EXPIRE_USAGE \
|
||||
N_("git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n" \
|
||||
" [--rewrite] [--updateref] [--stale-fix]\n" \
|
||||
@ -29,6 +33,11 @@ static const char *const reflog_show_usage[] = {
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const char *const reflog_list_usage[] = {
|
||||
BUILTIN_REFLOG_LIST_USAGE,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const char *const reflog_expire_usage[] = {
|
||||
BUILTIN_REFLOG_EXPIRE_USAGE,
|
||||
NULL
|
||||
@ -46,6 +55,7 @@ static const char *const reflog_exists_usage[] = {
|
||||
|
||||
static const char *const reflog_usage[] = {
|
||||
BUILTIN_REFLOG_SHOW_USAGE,
|
||||
BUILTIN_REFLOG_LIST_USAGE,
|
||||
BUILTIN_REFLOG_EXPIRE_USAGE,
|
||||
BUILTIN_REFLOG_DELETE_USAGE,
|
||||
BUILTIN_REFLOG_EXISTS_USAGE,
|
||||
@ -60,8 +70,7 @@ struct worktree_reflogs {
|
||||
struct string_list reflogs;
|
||||
};
|
||||
|
||||
static int collect_reflog(const char *ref, const struct object_id *oid UNUSED,
|
||||
int flags UNUSED, void *cb_data)
|
||||
static int collect_reflog(const char *ref, void *cb_data)
|
||||
{
|
||||
struct worktree_reflogs *cb = cb_data;
|
||||
struct worktree *worktree = cb->worktree;
|
||||
@ -238,6 +247,29 @@ static int cmd_reflog_show(int argc, const char **argv, const char *prefix)
|
||||
return cmd_log_reflog(argc, argv, prefix);
|
||||
}
|
||||
|
||||
static int show_reflog(const char *refname, void *cb_data UNUSED)
|
||||
{
|
||||
printf("%s\n", refname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_reflog_list(int argc, const char **argv, const char *prefix)
|
||||
{
|
||||
struct option options[] = {
|
||||
OPT_END()
|
||||
};
|
||||
struct ref_store *ref_store;
|
||||
|
||||
argc = parse_options(argc, argv, prefix, options, reflog_list_usage, 0);
|
||||
if (argc)
|
||||
return error(_("%s does not accept arguments: '%s'"),
|
||||
"list", argv[0]);
|
||||
|
||||
ref_store = get_main_ref_store(the_repository);
|
||||
|
||||
return refs_for_each_reflog(ref_store, show_reflog, NULL);
|
||||
}
|
||||
|
||||
static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
|
||||
{
|
||||
struct cmd_reflog_expire_cb cmd = { 0 };
|
||||
@ -417,6 +449,7 @@ int cmd_reflog(int argc, const char **argv, const char *prefix)
|
||||
parse_opt_subcommand_fn *fn = NULL;
|
||||
struct option options[] = {
|
||||
OPT_SUBCOMMAND("show", &fn, cmd_reflog_show),
|
||||
OPT_SUBCOMMAND("list", &fn, cmd_reflog_list),
|
||||
OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire),
|
||||
OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete),
|
||||
OPT_SUBCOMMAND("exists", &fn, cmd_reflog_exists),
|
||||
|
105
dir-iterator.c
105
dir-iterator.c
@ -2,10 +2,19 @@
|
||||
#include "dir.h"
|
||||
#include "iterator.h"
|
||||
#include "dir-iterator.h"
|
||||
#include "string-list.h"
|
||||
|
||||
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 '/'):
|
||||
@ -43,6 +52,31 @@ 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
|
||||
@ -72,6 +106,35 @@ static int push_level(struct dir_iterator_int *iter)
|
||||
return -1;
|
||||
}
|
||||
|
||||
string_list_init_dup(&level->entries);
|
||||
level->entries_idx = 0;
|
||||
|
||||
/*
|
||||
* When the iterator is sorted we read and sort all directory entries
|
||||
* directly.
|
||||
*/
|
||||
if (iter->flags & DIR_ITERATOR_SORTED) {
|
||||
struct dirent *de;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
string_list_append(&level->entries, de->d_name);
|
||||
}
|
||||
string_list_sort(&level->entries);
|
||||
|
||||
closedir(level->dir);
|
||||
level->dir = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -88,21 +151,22 @@ static int pop_level(struct dir_iterator_int *iter)
|
||||
warning_errno("error closing directory '%s'",
|
||||
iter->base.path.buf);
|
||||
level->dir = NULL;
|
||||
string_list_clear(&level->entries, 0);
|
||||
|
||||
return --iter->levels_nr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Populate iter->base with the necessary information on the next iteration
|
||||
* entry, represented by the given dirent de. Return 0 on success and -1
|
||||
* entry, represented by the given name. Return 0 on success and -1
|
||||
* otherwise, setting errno accordingly.
|
||||
*/
|
||||
static int prepare_next_entry_data(struct dir_iterator_int *iter,
|
||||
struct dirent *de)
|
||||
const char *name)
|
||||
{
|
||||
int err, saved_errno;
|
||||
|
||||
strbuf_addstr(&iter->base.path, de->d_name);
|
||||
strbuf_addstr(&iter->base.path, name);
|
||||
/*
|
||||
* We have to reset these because the path strbuf might have
|
||||
* been realloc()ed at the previous strbuf_addstr().
|
||||
@ -139,27 +203,34 @@ int dir_iterator_advance(struct dir_iterator *dir_iterator)
|
||||
struct dirent *de;
|
||||
struct dir_iterator_level *level =
|
||||
&iter->levels[iter->levels_nr - 1];
|
||||
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 (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);
|
||||
continue;
|
||||
} else if (ret > 0) {
|
||||
if (pop_level(iter) == 0)
|
||||
return dir_iterator_abort(dir_iterator);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
|
||||
name = de->d_name;
|
||||
} else {
|
||||
if (level->entries_idx >= level->entries.nr) {
|
||||
if (pop_level(iter) == 0)
|
||||
return dir_iterator_abort(dir_iterator);
|
||||
continue;
|
||||
}
|
||||
|
||||
name = level->entries.items[level->entries_idx++].string;
|
||||
}
|
||||
|
||||
if (is_dot_or_dotdot(de->d_name))
|
||||
continue;
|
||||
|
||||
if (prepare_next_entry_data(iter, de)) {
|
||||
if (prepare_next_entry_data(iter, name)) {
|
||||
if (errno != ENOENT && iter->flags & DIR_ITERATOR_PEDANTIC)
|
||||
goto error_out;
|
||||
continue;
|
||||
@ -188,6 +259,8 @@ int dir_iterator_abort(struct dir_iterator *dir_iterator)
|
||||
warning_errno("error closing directory '%s'",
|
||||
iter->base.path.buf);
|
||||
}
|
||||
|
||||
string_list_clear(&level->entries, 0);
|
||||
}
|
||||
|
||||
free(iter->levels);
|
||||
|
@ -54,8 +54,11 @@
|
||||
* and ITER_ERROR is returned immediately. In both cases, a meaningful
|
||||
* warning is emitted. Note: ENOENT errors are always ignored so that
|
||||
* the API users may remove files during iteration.
|
||||
*
|
||||
* - DIR_ITERATOR_SORTED: sort directory entries alphabetically.
|
||||
*/
|
||||
#define DIR_ITERATOR_PEDANTIC (1 << 0)
|
||||
#define DIR_ITERATOR_SORTED (1 << 1)
|
||||
|
||||
struct dir_iterator {
|
||||
/* The current path: */
|
||||
|
27
refs.c
27
refs.c
@ -1594,10 +1594,6 @@ struct ref_iterator *refs_ref_iterator_begin(
|
||||
if (trim)
|
||||
iter = prefix_ref_iterator_begin(iter, "", trim);
|
||||
|
||||
/* Sanity check for subclasses: */
|
||||
if (!iter->ordered)
|
||||
BUG("reference iterator is not ordered");
|
||||
|
||||
return iter;
|
||||
}
|
||||
|
||||
@ -2516,18 +2512,33 @@ cleanup:
|
||||
return ret;
|
||||
}
|
||||
|
||||
int refs_for_each_reflog(struct ref_store *refs, each_ref_fn fn, void *cb_data)
|
||||
struct do_for_each_reflog_help {
|
||||
each_reflog_fn *fn;
|
||||
void *cb_data;
|
||||
};
|
||||
|
||||
static int do_for_each_reflog_helper(struct repository *r UNUSED,
|
||||
const char *refname,
|
||||
const struct object_id *oid UNUSED,
|
||||
int flags,
|
||||
void *cb_data)
|
||||
{
|
||||
struct do_for_each_reflog_help *hp = cb_data;
|
||||
return hp->fn(refname, hp->cb_data);
|
||||
}
|
||||
|
||||
int refs_for_each_reflog(struct ref_store *refs, each_reflog_fn fn, void *cb_data)
|
||||
{
|
||||
struct ref_iterator *iter;
|
||||
struct do_for_each_ref_help hp = { fn, cb_data };
|
||||
struct do_for_each_reflog_help hp = { fn, cb_data };
|
||||
|
||||
iter = refs->be->reflog_iterator_begin(refs);
|
||||
|
||||
return do_for_each_repo_ref_iterator(the_repository, iter,
|
||||
do_for_each_ref_helper, &hp);
|
||||
do_for_each_reflog_helper, &hp);
|
||||
}
|
||||
|
||||
int for_each_reflog(each_ref_fn fn, void *cb_data)
|
||||
int for_each_reflog(each_reflog_fn fn, void *cb_data)
|
||||
{
|
||||
return refs_for_each_reflog(get_main_ref_store(the_repository), fn, cb_data);
|
||||
}
|
||||
|
11
refs.h
11
refs.h
@ -534,12 +534,19 @@ int for_each_reflog_ent(const char *refname, each_reflog_ent_fn fn, void *cb_dat
|
||||
/* youngest entry first */
|
||||
int for_each_reflog_ent_reverse(const char *refname, each_reflog_ent_fn fn, void *cb_data);
|
||||
|
||||
/*
|
||||
* The signature for the callback function for the {refs_,}for_each_reflog()
|
||||
* functions below. The memory pointed to by the refname argument is only
|
||||
* guaranteed to be valid for the duration of a single callback invocation.
|
||||
*/
|
||||
typedef int each_reflog_fn(const char *refname, void *cb_data);
|
||||
|
||||
/*
|
||||
* Calls the specified function for each reflog file until it returns nonzero,
|
||||
* and returns the value. Reflog file order is unspecified.
|
||||
*/
|
||||
int refs_for_each_reflog(struct ref_store *refs, each_ref_fn fn, void *cb_data);
|
||||
int for_each_reflog(each_ref_fn fn, void *cb_data);
|
||||
int refs_for_each_reflog(struct ref_store *refs, each_reflog_fn fn, void *cb_data);
|
||||
int for_each_reflog(each_reflog_fn fn, void *cb_data);
|
||||
|
||||
#define REFNAME_ALLOW_ONELEVEL 1
|
||||
#define REFNAME_REFSPEC_PATTERN 2
|
||||
|
@ -181,7 +181,6 @@ static int debug_ref_iterator_advance(struct ref_iterator *ref_iterator)
|
||||
trace_printf_key(&trace_refs, "iterator_advance: %s (0)\n",
|
||||
diter->iter->refname);
|
||||
|
||||
diter->base.ordered = diter->iter->ordered;
|
||||
diter->base.refname = diter->iter->refname;
|
||||
diter->base.oid = diter->iter->oid;
|
||||
diter->base.flags = diter->iter->flags;
|
||||
@ -222,7 +221,7 @@ debug_ref_iterator_begin(struct ref_store *ref_store, const char *prefix,
|
||||
drefs->refs->be->iterator_begin(drefs->refs, prefix,
|
||||
exclude_patterns, flags);
|
||||
struct debug_ref_iterator *diter = xcalloc(1, sizeof(*diter));
|
||||
base_ref_iterator_init(&diter->base, &debug_ref_iterator_vtable, 1);
|
||||
base_ref_iterator_init(&diter->base, &debug_ref_iterator_vtable);
|
||||
diter->iter = res;
|
||||
trace_printf_key(&trace_refs, "ref_iterator_begin: \"%s\" (0x%x)\n",
|
||||
prefix, flags);
|
||||
|
@ -879,8 +879,7 @@ static struct ref_iterator *files_ref_iterator_begin(
|
||||
|
||||
CALLOC_ARRAY(iter, 1);
|
||||
ref_iterator = &iter->base;
|
||||
base_ref_iterator_init(ref_iterator, &files_ref_iterator_vtable,
|
||||
overlay_iter->ordered);
|
||||
base_ref_iterator_init(ref_iterator, &files_ref_iterator_vtable);
|
||||
iter->iter0 = overlay_iter;
|
||||
iter->repo = ref_store->repo;
|
||||
iter->flags = flags;
|
||||
@ -2116,10 +2115,8 @@ static int files_for_each_reflog_ent(struct ref_store *ref_store,
|
||||
|
||||
struct files_reflog_iterator {
|
||||
struct ref_iterator base;
|
||||
|
||||
struct ref_store *ref_store;
|
||||
struct dir_iterator *dir_iterator;
|
||||
struct object_id oid;
|
||||
};
|
||||
|
||||
static int files_reflog_iterator_advance(struct ref_iterator *ref_iterator)
|
||||
@ -2130,25 +2127,13 @@ static int files_reflog_iterator_advance(struct ref_iterator *ref_iterator)
|
||||
int ok;
|
||||
|
||||
while ((ok = dir_iterator_advance(diter)) == ITER_OK) {
|
||||
int flags;
|
||||
|
||||
if (!S_ISREG(diter->st.st_mode))
|
||||
continue;
|
||||
if (diter->basename[0] == '.')
|
||||
if (check_refname_format(diter->basename,
|
||||
REFNAME_ALLOW_ONELEVEL))
|
||||
continue;
|
||||
if (ends_with(diter->basename, ".lock"))
|
||||
continue;
|
||||
|
||||
if (!refs_resolve_ref_unsafe(iter->ref_store,
|
||||
diter->relative_path, 0,
|
||||
&iter->oid, &flags)) {
|
||||
error("bad ref for %s", diter->path.buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
iter->base.refname = diter->relative_path;
|
||||
iter->base.oid = &iter->oid;
|
||||
iter->base.flags = flags;
|
||||
return ITER_OK;
|
||||
}
|
||||
|
||||
@ -2193,7 +2178,7 @@ static struct ref_iterator *reflog_iterator_begin(struct ref_store *ref_store,
|
||||
|
||||
strbuf_addf(&sb, "%s/logs", gitdir);
|
||||
|
||||
diter = dir_iterator_begin(sb.buf, 0);
|
||||
diter = dir_iterator_begin(sb.buf, DIR_ITERATOR_SORTED);
|
||||
if (!diter) {
|
||||
strbuf_release(&sb);
|
||||
return empty_ref_iterator_begin();
|
||||
@ -2202,7 +2187,7 @@ static struct ref_iterator *reflog_iterator_begin(struct ref_store *ref_store,
|
||||
CALLOC_ARRAY(iter, 1);
|
||||
ref_iterator = &iter->base;
|
||||
|
||||
base_ref_iterator_init(ref_iterator, &files_reflog_iterator_vtable, 0);
|
||||
base_ref_iterator_init(ref_iterator, &files_reflog_iterator_vtable);
|
||||
iter->dir_iterator = diter;
|
||||
iter->ref_store = ref_store;
|
||||
strbuf_release(&sb);
|
||||
@ -2210,32 +2195,6 @@ static struct ref_iterator *reflog_iterator_begin(struct ref_store *ref_store,
|
||||
return ref_iterator;
|
||||
}
|
||||
|
||||
static enum iterator_selection reflog_iterator_select(
|
||||
struct ref_iterator *iter_worktree,
|
||||
struct ref_iterator *iter_common,
|
||||
void *cb_data UNUSED)
|
||||
{
|
||||
if (iter_worktree) {
|
||||
/*
|
||||
* We're a bit loose here. We probably should ignore
|
||||
* common refs if they are accidentally added as
|
||||
* per-worktree refs.
|
||||
*/
|
||||
return ITER_SELECT_0;
|
||||
} else if (iter_common) {
|
||||
if (parse_worktree_ref(iter_common->refname, NULL, NULL,
|
||||
NULL) == REF_WORKTREE_SHARED)
|
||||
return ITER_SELECT_1;
|
||||
|
||||
/*
|
||||
* The main ref store may contain main worktree's
|
||||
* per-worktree refs, which should be ignored
|
||||
*/
|
||||
return ITER_SKIP_1;
|
||||
} else
|
||||
return ITER_DONE;
|
||||
}
|
||||
|
||||
static struct ref_iterator *files_reflog_iterator_begin(struct ref_store *ref_store)
|
||||
{
|
||||
struct files_ref_store *refs =
|
||||
@ -2246,9 +2205,9 @@ 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),
|
||||
reflog_iterator_begin(ref_store, refs->base.gitdir),
|
||||
reflog_iterator_begin(ref_store, refs->gitcommondir),
|
||||
reflog_iterator_select, refs);
|
||||
ref_iterator_select, refs);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,9 @@ int ref_iterator_abort(struct ref_iterator *ref_iterator)
|
||||
}
|
||||
|
||||
void base_ref_iterator_init(struct ref_iterator *iter,
|
||||
struct ref_iterator_vtable *vtable,
|
||||
int ordered)
|
||||
struct ref_iterator_vtable *vtable)
|
||||
{
|
||||
iter->vtable = vtable;
|
||||
iter->ordered = !!ordered;
|
||||
iter->refname = NULL;
|
||||
iter->oid = NULL;
|
||||
iter->flags = 0;
|
||||
@ -74,7 +72,7 @@ struct ref_iterator *empty_ref_iterator_begin(void)
|
||||
struct empty_ref_iterator *iter = xcalloc(1, sizeof(*iter));
|
||||
struct ref_iterator *ref_iterator = &iter->base;
|
||||
|
||||
base_ref_iterator_init(ref_iterator, &empty_ref_iterator_vtable, 1);
|
||||
base_ref_iterator_init(ref_iterator, &empty_ref_iterator_vtable);
|
||||
return ref_iterator;
|
||||
}
|
||||
|
||||
@ -98,6 +96,49 @@ struct merge_ref_iterator {
|
||||
struct ref_iterator **current;
|
||||
};
|
||||
|
||||
enum iterator_selection ref_iterator_select(struct ref_iterator *iter_worktree,
|
||||
struct ref_iterator *iter_common,
|
||||
void *cb_data UNUSED)
|
||||
{
|
||||
if (iter_worktree && !iter_common) {
|
||||
/*
|
||||
* Return the worktree ref if there are no more common refs.
|
||||
*/
|
||||
return ITER_SELECT_0;
|
||||
} else if (iter_common) {
|
||||
/*
|
||||
* In case we have pending worktree and common refs we need to
|
||||
* yield them based on their lexicographical order. Worktree
|
||||
* refs that have the same name as common refs shadow the
|
||||
* latter.
|
||||
*/
|
||||
if (iter_worktree) {
|
||||
int cmp = strcmp(iter_worktree->refname,
|
||||
iter_common->refname);
|
||||
if (cmp < 0)
|
||||
return ITER_SELECT_0;
|
||||
else if (!cmp)
|
||||
return ITER_SELECT_0_SKIP_1;
|
||||
}
|
||||
|
||||
/*
|
||||
* We now know that the lexicographically-next ref is a common
|
||||
* ref. When the common ref is a shared one we return it.
|
||||
*/
|
||||
if (parse_worktree_ref(iter_common->refname, NULL, NULL,
|
||||
NULL) == REF_WORKTREE_SHARED)
|
||||
return ITER_SELECT_1;
|
||||
|
||||
/*
|
||||
* Otherwise, if the common ref is a per-worktree ref we skip
|
||||
* it because it would belong to the main worktree, not ours.
|
||||
*/
|
||||
return ITER_SKIP_1;
|
||||
} else {
|
||||
return ITER_DONE;
|
||||
}
|
||||
}
|
||||
|
||||
static int merge_ref_iterator_advance(struct ref_iterator *ref_iterator)
|
||||
{
|
||||
struct merge_ref_iterator *iter =
|
||||
@ -207,7 +248,6 @@ static struct ref_iterator_vtable merge_ref_iterator_vtable = {
|
||||
};
|
||||
|
||||
struct ref_iterator *merge_ref_iterator_begin(
|
||||
int ordered,
|
||||
struct ref_iterator *iter0, struct ref_iterator *iter1,
|
||||
ref_iterator_select_fn *select, void *cb_data)
|
||||
{
|
||||
@ -222,7 +262,7 @@ struct ref_iterator *merge_ref_iterator_begin(
|
||||
* references through only if they exist in both iterators.
|
||||
*/
|
||||
|
||||
base_ref_iterator_init(ref_iterator, &merge_ref_iterator_vtable, ordered);
|
||||
base_ref_iterator_init(ref_iterator, &merge_ref_iterator_vtable);
|
||||
iter->iter0 = iter0;
|
||||
iter->iter1 = iter1;
|
||||
iter->select = select;
|
||||
@ -271,12 +311,9 @@ struct ref_iterator *overlay_ref_iterator_begin(
|
||||
} else if (is_empty_ref_iterator(back)) {
|
||||
ref_iterator_abort(back);
|
||||
return front;
|
||||
} else if (!front->ordered || !back->ordered) {
|
||||
BUG("overlay_ref_iterator requires ordered inputs");
|
||||
}
|
||||
|
||||
return merge_ref_iterator_begin(1, front, back,
|
||||
overlay_iterator_select, NULL);
|
||||
return merge_ref_iterator_begin(front, back, overlay_iterator_select, NULL);
|
||||
}
|
||||
|
||||
struct prefix_ref_iterator {
|
||||
@ -315,16 +352,12 @@ static int prefix_ref_iterator_advance(struct ref_iterator *ref_iterator)
|
||||
|
||||
if (cmp > 0) {
|
||||
/*
|
||||
* If the source iterator is ordered, then we
|
||||
* As the source iterator is ordered, we
|
||||
* can stop the iteration as soon as we see a
|
||||
* refname that comes after the prefix:
|
||||
*/
|
||||
if (iter->iter0->ordered) {
|
||||
ok = ref_iterator_abort(iter->iter0);
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
ok = ref_iterator_abort(iter->iter0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (iter->trim) {
|
||||
@ -396,7 +429,7 @@ struct ref_iterator *prefix_ref_iterator_begin(struct ref_iterator *iter0,
|
||||
CALLOC_ARRAY(iter, 1);
|
||||
ref_iterator = &iter->base;
|
||||
|
||||
base_ref_iterator_init(ref_iterator, &prefix_ref_iterator_vtable, iter0->ordered);
|
||||
base_ref_iterator_init(ref_iterator, &prefix_ref_iterator_vtable);
|
||||
|
||||
iter->iter0 = iter0;
|
||||
iter->prefix = xstrdup(prefix);
|
||||
|
@ -1111,7 +1111,7 @@ static struct ref_iterator *packed_ref_iterator_begin(
|
||||
|
||||
CALLOC_ARRAY(iter, 1);
|
||||
ref_iterator = &iter->base;
|
||||
base_ref_iterator_init(ref_iterator, &packed_ref_iterator_vtable, 1);
|
||||
base_ref_iterator_init(ref_iterator, &packed_ref_iterator_vtable);
|
||||
|
||||
if (exclude_patterns)
|
||||
populate_excluded_jump_list(iter, snapshot, exclude_patterns);
|
||||
|
@ -486,7 +486,7 @@ struct ref_iterator *cache_ref_iterator_begin(struct ref_cache *cache,
|
||||
|
||||
CALLOC_ARRAY(iter, 1);
|
||||
ref_iterator = &iter->base;
|
||||
base_ref_iterator_init(ref_iterator, &cache_ref_iterator_vtable, 1);
|
||||
base_ref_iterator_init(ref_iterator, &cache_ref_iterator_vtable);
|
||||
ALLOC_GROW(iter->levels, 10, iter->levels_alloc);
|
||||
|
||||
iter->levels_nr = 1;
|
||||
|
@ -312,13 +312,6 @@ enum do_for_each_ref_flags {
|
||||
*/
|
||||
struct ref_iterator {
|
||||
struct ref_iterator_vtable *vtable;
|
||||
|
||||
/*
|
||||
* Does this `ref_iterator` iterate over references in order
|
||||
* by refname?
|
||||
*/
|
||||
unsigned int ordered : 1;
|
||||
|
||||
const char *refname;
|
||||
const struct object_id *oid;
|
||||
unsigned int flags;
|
||||
@ -386,15 +379,22 @@ typedef enum iterator_selection ref_iterator_select_fn(
|
||||
struct ref_iterator *iter0, struct ref_iterator *iter1,
|
||||
void *cb_data);
|
||||
|
||||
/*
|
||||
* An implementation of ref_iterator_select_fn that merges worktree and common
|
||||
* refs. Per-worktree refs from the common iterator are ignored, worktree refs
|
||||
* override common refs. Refs are selected lexicographically.
|
||||
*/
|
||||
enum iterator_selection ref_iterator_select(struct ref_iterator *iter_worktree,
|
||||
struct ref_iterator *iter_common,
|
||||
void *cb_data);
|
||||
|
||||
/*
|
||||
* Iterate over the entries from iter0 and iter1, with the values
|
||||
* interleaved as directed by the select function. The iterator takes
|
||||
* ownership of iter0 and iter1 and frees them when the iteration is
|
||||
* over. A derived class should set `ordered` to 1 or 0 based on
|
||||
* whether it generates its output in order by reference name.
|
||||
* over.
|
||||
*/
|
||||
struct ref_iterator *merge_ref_iterator_begin(
|
||||
int ordered,
|
||||
struct ref_iterator *iter0, struct ref_iterator *iter1,
|
||||
ref_iterator_select_fn *select, void *cb_data);
|
||||
|
||||
@ -423,8 +423,6 @@ struct ref_iterator *overlay_ref_iterator_begin(
|
||||
* As an convenience to callers, if prefix is the empty string and
|
||||
* trim is zero, this function returns iter0 directly, without
|
||||
* wrapping it.
|
||||
*
|
||||
* The resulting ref_iterator is ordered if iter0 is.
|
||||
*/
|
||||
struct ref_iterator *prefix_ref_iterator_begin(struct ref_iterator *iter0,
|
||||
const char *prefix,
|
||||
@ -435,14 +433,11 @@ struct ref_iterator *prefix_ref_iterator_begin(struct ref_iterator *iter0,
|
||||
/*
|
||||
* Base class constructor for ref_iterators. Initialize the
|
||||
* ref_iterator part of iter, setting its vtable pointer as specified.
|
||||
* `ordered` should be set to 1 if the iterator will iterate over
|
||||
* references in order by refname; otherwise it should be set to 0.
|
||||
* This is meant to be called only by the initializers of derived
|
||||
* classes.
|
||||
*/
|
||||
void base_ref_iterator_init(struct ref_iterator *iter,
|
||||
struct ref_iterator_vtable *vtable,
|
||||
int ordered);
|
||||
struct ref_iterator_vtable *vtable);
|
||||
|
||||
/*
|
||||
* Base class destructor for ref_iterators. Destroy the ref_iterator
|
||||
|
@ -479,7 +479,7 @@ static struct reftable_ref_iterator *ref_iterator_for_stack(struct reftable_ref_
|
||||
int ret;
|
||||
|
||||
iter = xcalloc(1, sizeof(*iter));
|
||||
base_ref_iterator_init(&iter->base, &reftable_ref_iterator_vtable, 1);
|
||||
base_ref_iterator_init(&iter->base, &reftable_ref_iterator_vtable);
|
||||
iter->prefix = prefix;
|
||||
iter->base.oid = &iter->oid;
|
||||
iter->flags = flags;
|
||||
@ -504,49 +504,6 @@ done:
|
||||
return iter;
|
||||
}
|
||||
|
||||
static enum iterator_selection iterator_select(struct ref_iterator *iter_worktree,
|
||||
struct ref_iterator *iter_common,
|
||||
void *cb_data UNUSED)
|
||||
{
|
||||
if (iter_worktree && !iter_common) {
|
||||
/*
|
||||
* Return the worktree ref if there are no more common refs.
|
||||
*/
|
||||
return ITER_SELECT_0;
|
||||
} else if (iter_common) {
|
||||
/*
|
||||
* In case we have pending worktree and common refs we need to
|
||||
* yield them based on their lexicographical order. Worktree
|
||||
* refs that have the same name as common refs shadow the
|
||||
* latter.
|
||||
*/
|
||||
if (iter_worktree) {
|
||||
int cmp = strcmp(iter_worktree->refname,
|
||||
iter_common->refname);
|
||||
if (cmp < 0)
|
||||
return ITER_SELECT_0;
|
||||
else if (!cmp)
|
||||
return ITER_SELECT_0_SKIP_1;
|
||||
}
|
||||
|
||||
/*
|
||||
* We now know that the lexicographically-next ref is a common
|
||||
* ref. When the common ref is a shared one we return it.
|
||||
*/
|
||||
if (parse_worktree_ref(iter_common->refname, NULL, NULL,
|
||||
NULL) == REF_WORKTREE_SHARED)
|
||||
return ITER_SELECT_1;
|
||||
|
||||
/*
|
||||
* Otherwise, if the common ref is a per-worktree ref we skip
|
||||
* it because it would belong to the main worktree, not ours.
|
||||
*/
|
||||
return ITER_SKIP_1;
|
||||
} else {
|
||||
return ITER_DONE;
|
||||
}
|
||||
}
|
||||
|
||||
static struct ref_iterator *reftable_be_iterator_begin(struct ref_store *ref_store,
|
||||
const char *prefix,
|
||||
const char **exclude_patterns,
|
||||
@ -575,8 +532,8 @@ static struct ref_iterator *reftable_be_iterator_begin(struct ref_store *ref_sto
|
||||
* single iterator.
|
||||
*/
|
||||
worktree_iter = ref_iterator_for_stack(refs, refs->worktree_stack, prefix, flags);
|
||||
return merge_ref_iterator_begin(1, &worktree_iter->base, &main_iter->base,
|
||||
iterator_select, NULL);
|
||||
return merge_ref_iterator_begin(&worktree_iter->base, &main_iter->base,
|
||||
ref_iterator_select, NULL);
|
||||
}
|
||||
|
||||
static int reftable_be_read_raw_ref(struct ref_store *ref_store,
|
||||
@ -1637,7 +1594,6 @@ struct reftable_reflog_iterator {
|
||||
struct reftable_ref_store *refs;
|
||||
struct reftable_iterator iter;
|
||||
struct reftable_log_record log;
|
||||
struct object_id oid;
|
||||
char *last_name;
|
||||
int err;
|
||||
};
|
||||
@ -1648,8 +1604,6 @@ static int reftable_reflog_iterator_advance(struct ref_iterator *ref_iterator)
|
||||
(struct reftable_reflog_iterator *)ref_iterator;
|
||||
|
||||
while (!iter->err) {
|
||||
int flags;
|
||||
|
||||
iter->err = reftable_iterator_next_log(&iter->iter, &iter->log);
|
||||
if (iter->err)
|
||||
break;
|
||||
@ -1662,17 +1616,13 @@ static int reftable_reflog_iterator_advance(struct ref_iterator *ref_iterator)
|
||||
if (iter->last_name && !strcmp(iter->log.refname, iter->last_name))
|
||||
continue;
|
||||
|
||||
if (!refs_resolve_ref_unsafe(&iter->refs->base, iter->log.refname,
|
||||
0, &iter->oid, &flags)) {
|
||||
error(_("bad ref for %s"), iter->log.refname);
|
||||
if (check_refname_format(iter->log.refname,
|
||||
REFNAME_ALLOW_ONELEVEL))
|
||||
continue;
|
||||
}
|
||||
|
||||
free(iter->last_name);
|
||||
iter->last_name = xstrdup(iter->log.refname);
|
||||
iter->base.refname = iter->log.refname;
|
||||
iter->base.oid = &iter->oid;
|
||||
iter->base.flags = flags;
|
||||
|
||||
break;
|
||||
}
|
||||
@ -1723,9 +1673,8 @@ static struct reftable_reflog_iterator *reflog_iterator_for_stack(struct reftabl
|
||||
int ret;
|
||||
|
||||
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;
|
||||
|
||||
ret = refs->err;
|
||||
if (ret)
|
||||
@ -1758,8 +1707,8 @@ static struct ref_iterator *reftable_be_reflog_iterator_begin(struct ref_store *
|
||||
|
||||
worktree_iter = reflog_iterator_for_stack(refs, refs->worktree_stack);
|
||||
|
||||
return merge_ref_iterator_begin(1, &worktree_iter->base, &main_iter->base,
|
||||
iterator_select, NULL);
|
||||
return merge_ref_iterator_begin(&worktree_iter->base, &main_iter->base,
|
||||
ref_iterator_select, NULL);
|
||||
}
|
||||
|
||||
static int yield_log_record(struct reftable_log_record *log,
|
||||
|
@ -1686,9 +1686,7 @@ static int handle_one_reflog_ent(struct object_id *ooid, struct object_id *noid,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_one_reflog(const char *refname_in_wt,
|
||||
const struct object_id *oid UNUSED,
|
||||
int flag UNUSED, void *cb_data)
|
||||
static int handle_one_reflog(const char *refname_in_wt, void *cb_data)
|
||||
{
|
||||
struct all_refs_cb *cb = cb_data;
|
||||
struct strbuf refname = STRBUF_INIT;
|
||||
|
@ -221,15 +221,21 @@ static int cmd_verify_ref(struct ref_store *refs, const char **argv)
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int each_reflog(const char *refname, void *cb_data UNUSED)
|
||||
{
|
||||
printf("%s\n", refname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_for_each_reflog(struct ref_store *refs,
|
||||
const char **argv UNUSED)
|
||||
{
|
||||
return refs_for_each_reflog(refs, each_ref, NULL);
|
||||
return refs_for_each_reflog(refs, each_reflog, NULL);
|
||||
}
|
||||
|
||||
static int each_reflog(struct object_id *old_oid, struct object_id *new_oid,
|
||||
const char *committer, timestamp_t timestamp,
|
||||
int tz, const char *msg, void *cb_data UNUSED)
|
||||
static int each_reflog_ent(struct object_id *old_oid, struct object_id *new_oid,
|
||||
const char *committer, timestamp_t timestamp,
|
||||
int tz, const char *msg, void *cb_data UNUSED)
|
||||
{
|
||||
printf("%s %s %s %" PRItime " %+05d%s%s", oid_to_hex(old_oid),
|
||||
oid_to_hex(new_oid), committer, timestamp, tz,
|
||||
@ -241,14 +247,14 @@ static int cmd_for_each_reflog_ent(struct ref_store *refs, const char **argv)
|
||||
{
|
||||
const char *refname = notnull(*argv++, "refname");
|
||||
|
||||
return refs_for_each_reflog_ent(refs, refname, each_reflog, refs);
|
||||
return refs_for_each_reflog_ent(refs, refname, each_reflog_ent, refs);
|
||||
}
|
||||
|
||||
static int cmd_for_each_reflog_ent_reverse(struct ref_store *refs, const char **argv)
|
||||
{
|
||||
const char *refname = notnull(*argv++, "refname");
|
||||
|
||||
return refs_for_each_reflog_ent_reverse(refs, refname, each_reflog, refs);
|
||||
return refs_for_each_reflog_ent_reverse(refs, refname, each_reflog_ent, refs);
|
||||
}
|
||||
|
||||
static int cmd_reflog_exists(struct ref_store *refs, const char **argv)
|
||||
|
@ -287,23 +287,23 @@ test_expect_success 'for_each_reflog()' '
|
||||
mkdir -p .git/worktrees/wt/logs/refs/bisect &&
|
||||
echo $ZERO_OID > .git/worktrees/wt/logs/refs/bisect/wt-random &&
|
||||
|
||||
$RWT for-each-reflog | cut -d" " -f 2- | sort >actual &&
|
||||
$RWT for-each-reflog >actual &&
|
||||
cat >expected <<-\EOF &&
|
||||
HEAD 0x1
|
||||
PSEUDO-WT 0x0
|
||||
refs/bisect/wt-random 0x0
|
||||
refs/heads/main 0x0
|
||||
refs/heads/wt-main 0x0
|
||||
HEAD
|
||||
PSEUDO-WT
|
||||
refs/bisect/wt-random
|
||||
refs/heads/main
|
||||
refs/heads/wt-main
|
||||
EOF
|
||||
test_cmp expected actual &&
|
||||
|
||||
$RMAIN for-each-reflog | cut -d" " -f 2- | sort >actual &&
|
||||
$RMAIN for-each-reflog >actual &&
|
||||
cat >expected <<-\EOF &&
|
||||
HEAD 0x1
|
||||
PSEUDO-MAIN 0x0
|
||||
refs/bisect/random 0x0
|
||||
refs/heads/main 0x0
|
||||
refs/heads/wt-main 0x0
|
||||
HEAD
|
||||
PSEUDO-MAIN
|
||||
refs/bisect/random
|
||||
refs/heads/main
|
||||
refs/heads/wt-main
|
||||
EOF
|
||||
test_cmp expected actual
|
||||
'
|
||||
|
@ -68,11 +68,11 @@ test_expect_success 'verify_ref(new-main)' '
|
||||
'
|
||||
|
||||
test_expect_success 'for_each_reflog()' '
|
||||
$RUN for-each-reflog | sort -k2 | cut -d" " -f 2- >actual &&
|
||||
$RUN for-each-reflog >actual &&
|
||||
cat >expected <<-\EOF &&
|
||||
HEAD 0x1
|
||||
refs/heads/main 0x0
|
||||
refs/heads/new-main 0x0
|
||||
HEAD
|
||||
refs/heads/main
|
||||
refs/heads/new-main
|
||||
EOF
|
||||
test_cmp expected actual
|
||||
'
|
||||
|
@ -63,11 +63,11 @@ test_expect_success 'verify_ref(new-main)' '
|
||||
'
|
||||
|
||||
test_expect_success 'for_each_reflog()' '
|
||||
$RUN for-each-reflog | sort | cut -d" " -f 2- >actual &&
|
||||
$RUN for-each-reflog >actual &&
|
||||
cat >expected <<-\EOF &&
|
||||
HEAD 0x1
|
||||
refs/heads/main 0x0
|
||||
refs/heads/new-main 0x0
|
||||
HEAD
|
||||
refs/heads/main
|
||||
refs/heads/new-main
|
||||
EOF
|
||||
test_cmp expected actual
|
||||
'
|
||||
|
@ -436,4 +436,112 @@ test_expect_success 'empty reflog' '
|
||||
test_must_be_empty err
|
||||
'
|
||||
|
||||
test_expect_success 'list reflogs' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
git reflog list >actual &&
|
||||
test_must_be_empty actual &&
|
||||
|
||||
test_commit A &&
|
||||
cat >expect <<-EOF &&
|
||||
HEAD
|
||||
refs/heads/main
|
||||
EOF
|
||||
git reflog list >actual &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
git branch b &&
|
||||
cat >expect <<-EOF &&
|
||||
HEAD
|
||||
refs/heads/b
|
||||
refs/heads/main
|
||||
EOF
|
||||
git reflog list >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'list reflogs with worktree' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_commit A &&
|
||||
git worktree add wt &&
|
||||
git -c core.logAllRefUpdates=always \
|
||||
update-ref refs/worktree/main HEAD &&
|
||||
git -c core.logAllRefUpdates=always \
|
||||
update-ref refs/worktree/per-worktree HEAD &&
|
||||
git -c core.logAllRefUpdates=always -C wt \
|
||||
update-ref refs/worktree/per-worktree HEAD &&
|
||||
git -c core.logAllRefUpdates=always -C wt \
|
||||
update-ref refs/worktree/worktree HEAD &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
HEAD
|
||||
refs/heads/main
|
||||
refs/heads/wt
|
||||
refs/worktree/main
|
||||
refs/worktree/per-worktree
|
||||
EOF
|
||||
git reflog list >actual &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
HEAD
|
||||
refs/heads/main
|
||||
refs/heads/wt
|
||||
refs/worktree/per-worktree
|
||||
refs/worktree/worktree
|
||||
EOF
|
||||
git -C wt reflog list >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'reflog list returns error with additional args' '
|
||||
cat >expect <<-EOF &&
|
||||
error: list does not accept arguments: ${SQ}bogus${SQ}
|
||||
EOF
|
||||
test_must_fail git reflog list bogus 2>err &&
|
||||
test_cmp expect err
|
||||
'
|
||||
|
||||
test_expect_success 'reflog for symref with unborn target can be listed' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit A &&
|
||||
git symbolic-ref HEAD refs/heads/unborn &&
|
||||
cat >expect <<-EOF &&
|
||||
HEAD
|
||||
refs/heads/main
|
||||
EOF
|
||||
git reflog list >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'reflog with invalid object ID can be listed' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit A &&
|
||||
test-tool ref-store main update-ref msg refs/heads/missing \
|
||||
$(test_oid deadbeef) "$ZERO_OID" REF_SKIP_OID_VERIFICATION &&
|
||||
cat >expect <<-EOF &&
|
||||
HEAD
|
||||
refs/heads/main
|
||||
refs/heads/missing
|
||||
EOF
|
||||
git reflog list >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_done
|
||||
|
Loading…
Reference in New Issue
Block a user