Merge branch 'ps/reflog-migrate-fixes' into maint-2.51

"git refs migrate" to migrate the reflog entries from a refs
backend to another had a handful of bugs squashed.

* ps/reflog-migrate-fixes:
  refs: fix invalid old object IDs when migrating reflogs
  refs: stop unsetting REF_HAVE_OLD for log-only updates
  refs/files: detect race when generating reflog entry for HEAD
  refs: fix identity for migrated reflogs
  ident: fix type of string length parameter
  builtin/reflog: implement subcommand to write new entries
  refs: export `ref_transaction_update_reflog()`
  builtin/reflog: improve grouping of subcommands
  Documentation/git-reflog: convert to use synopsis type
maint
Junio C Hamano 2025-10-15 10:29:27 -07:00
commit e04c0aded3
12 changed files with 446 additions and 150 deletions

View File

@ -8,16 +8,17 @@ git-reflog - Manage reflog information

SYNOPSIS
--------
[verse]
'git reflog' [show] [<log-options>] [<ref>]
'git reflog list'
'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]
[synopsis]
git reflog [show] [<log-options>] [<ref>]
git reflog list
git reflog exists <ref>
git reflog write <ref> <old-oid> <new-oid> <message>
git reflog delete [--rewrite] [--updateref]
[--dry-run | -n] [--verbose] <ref>@{<specifier>}...
git reflog drop [--all [--single-worktree] | <refs>...]
git reflog expire [--expire=<time>] [--expire-unreachable=<time>]
[--rewrite] [--updateref] [--stale-fix]
[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]
'git reflog delete' [--rewrite] [--updateref]
[--dry-run | -n] [--verbose] <ref>@{<specifier>}...
'git reflog drop' [--all [--single-worktree] | <refs>...]
'git reflog exists' <ref>

DESCRIPTION
-----------
@ -43,11 +44,15 @@ actions, and in addition the `HEAD` reflog records branch switching.

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.
This is typically not used directly by end users -- instead, see
linkgit:git-gc[1].
The "exists" subcommand checks whether a ref has a reflog. It exits
with zero status if the reflog exists, and non-zero status if it does
not.

The "write" subcommand writes a single entry to the reflog of a given
reference. This new entry is appended to the reflog and will thus become
the most recent entry. The reference name must be fully qualified. Both the old
and new object IDs must not be abbreviated and must point to existing objects.
The reflog message gets normalized.

The "delete" subcommand deletes single entries from the reflog, but
not the reflog itself. Its argument must be an _exact_ entry (e.g. "`git
@ -58,9 +63,11 @@ The "drop" subcommand completely removes the reflog for the specified
references. This is in contrast to "expire" and "delete", both of which
can be used to delete reflog entries, but not the reflog itself.

The "exists" subcommand checks whether a ref has a reflog. It exits
with zero status if the reflog exists, and non-zero status if it does
not.
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.
This is typically not used directly by end users -- instead, see
linkgit:git-gc[1].

OPTIONS
-------
@ -71,65 +78,6 @@ Options for `show`
`git reflog show` accepts any of the options accepted by `git log`.


Options for `expire`
~~~~~~~~~~~~~~~~~~~~

--all::
Process the reflogs of all references.

--single-worktree::
By default when `--all` is specified, reflogs from all working
trees are processed. This option limits the processing to reflogs
from the current working tree only.

--expire=<time>::
Prune entries older than the specified time. If this option is
not specified, the expiration time is taken from the
configuration setting `gc.reflogExpire`, which in turn
defaults to 90 days. `--expire=all` prunes entries regardless
of their age; `--expire=never` turns off pruning of reachable
entries (but see `--expire-unreachable`).

--expire-unreachable=<time>::
Prune entries older than `<time>` that are not reachable from
the current tip of the branch. If this option is not
specified, the expiration time is taken from the configuration
setting `gc.reflogExpireUnreachable`, which in turn defaults
to 30 days. `--expire-unreachable=all` prunes unreachable
entries regardless of their age; `--expire-unreachable=never`
turns off early pruning of unreachable entries (but see
`--expire`).

--updateref::
Update the reference to the value of the top reflog entry (i.e.
<ref>@\{0\}) if the previous top entry was pruned. (This
option is ignored for symbolic references.)

--rewrite::
If a reflog entry's predecessor is pruned, adjust its "old"
SHA-1 to be equal to the "new" SHA-1 field of the entry that
now precedes it.

--stale-fix::
Prune any reflog entries that point to "broken commits". A
broken commit is a commit that is not reachable from any of
the reference tips and that refers, directly or indirectly, to
a missing commit, tree, or blob object.
+
This computation involves traversing all the reachable objects, i.e. it
has the same cost as 'git prune'. It is primarily intended to fix
corruption caused by garbage collecting using older versions of Git,
which didn't protect objects referred to by reflogs.

-n::
--dry-run::
Do not actually prune any entries; just show what would have
been pruned.

--verbose::
Print extra information on screen.


Options for `delete`
~~~~~~~~~~~~~~~~~~~~

@ -140,14 +88,74 @@ used with `expire`.
Options for `drop`
~~~~~~~~~~~~~~~~~~

--all::
`--all`::
Drop the reflogs of all references from all worktrees.

--single-worktree::
`--single-worktree`::
By default when `--all` is specified, reflogs from all working
trees are dropped. This option limits the processing to reflogs
from the current working tree only.


Options for `expire`
~~~~~~~~~~~~~~~~~~~~

`--all`::
Process the reflogs of all references.

`--single-worktree`::
By default when `--all` is specified, reflogs from all working
trees are processed. This option limits the processing to reflogs
from the current working tree only.

`--expire=<time>`::
Prune entries older than the specified time. If this option is
not specified, the expiration time is taken from the
configuration setting `gc.reflogExpire`, which in turn
defaults to 90 days. `--expire=all` prunes entries regardless
of their age; `--expire=never` turns off pruning of reachable
entries (but see `--expire-unreachable`).

`--expire-unreachable=<time>`::
Prune entries older than `<time>` that are not reachable from
the current tip of the branch. If this option is not
specified, the expiration time is taken from the configuration
setting `gc.reflogExpireUnreachable`, which in turn defaults
to 30 days. `--expire-unreachable=all` prunes unreachable
entries regardless of their age; `--expire-unreachable=never`
turns off early pruning of unreachable entries (but see
`--expire`).

`--updateref`::
Update the reference to the value of the top reflog entry (i.e.
<ref>@\{0\}) if the previous top entry was pruned. (This
option is ignored for symbolic references.)

`--rewrite`::
If a reflog entry's predecessor is pruned, adjust its "old"
SHA-1 to be equal to the "new" SHA-1 field of the entry that
now precedes it.

`--stale-fix`::
Prune any reflog entries that point to "broken commits". A
broken commit is a commit that is not reachable from any of
the reference tips and that refers, directly or indirectly, to
a missing commit, tree, or blob object.
+
This computation involves traversing all the reachable objects, i.e. it
has the same cost as 'git prune'. It is primarily intended to fix
corruption caused by garbage collecting using older versions of Git,
which didn't protect objects referred to by reflogs.

`-n`::
`--dry-run`::
Do not actually prune any entries; just show what would have
been pruned.

`--verbose`::
Print extra information on screen.


GIT
---
Part of the linkgit:git[1] suite

View File

@ -3,6 +3,8 @@
#include "builtin.h"
#include "config.h"
#include "gettext.h"
#include "hex.h"
#include "odb.h"
#include "revision.h"
#include "reachable.h"
#include "wildmatch.h"
@ -17,21 +19,24 @@
#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" \
" [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]")
#define BUILTIN_REFLOG_EXISTS_USAGE \
N_("git reflog exists <ref>")

#define BUILTIN_REFLOG_WRITE_USAGE \
N_("git reflog write <ref> <old-oid> <new-oid> <message>")

#define BUILTIN_REFLOG_DELETE_USAGE \
N_("git reflog delete [--rewrite] [--updateref]\n" \
" [--dry-run | -n] [--verbose] <ref>@{<specifier>}...")

#define BUILTIN_REFLOG_EXISTS_USAGE \
N_("git reflog exists <ref>")

#define BUILTIN_REFLOG_DROP_USAGE \
N_("git reflog drop [--all [--single-worktree] | <refs>...]")

#define BUILTIN_REFLOG_EXPIRE_USAGE \
N_("git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n" \
" [--rewrite] [--updateref] [--stale-fix]\n" \
" [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]")

static const char *const reflog_show_usage[] = {
BUILTIN_REFLOG_SHOW_USAGE,
NULL,
@ -42,9 +47,14 @@ static const char *const reflog_list_usage[] = {
NULL,
};

static const char *const reflog_expire_usage[] = {
BUILTIN_REFLOG_EXPIRE_USAGE,
NULL
static const char *const reflog_exists_usage[] = {
BUILTIN_REFLOG_EXISTS_USAGE,
NULL,
};

static const char *const reflog_write_usage[] = {
BUILTIN_REFLOG_WRITE_USAGE,
NULL,
};

static const char *const reflog_delete_usage[] = {
@ -52,23 +62,24 @@ static const char *const reflog_delete_usage[] = {
NULL
};

static const char *const reflog_exists_usage[] = {
BUILTIN_REFLOG_EXISTS_USAGE,
NULL,
};

static const char *const reflog_drop_usage[] = {
BUILTIN_REFLOG_DROP_USAGE,
NULL,
};

static const char *const reflog_expire_usage[] = {
BUILTIN_REFLOG_EXPIRE_USAGE,
NULL
};

static const char *const reflog_usage[] = {
BUILTIN_REFLOG_SHOW_USAGE,
BUILTIN_REFLOG_LIST_USAGE,
BUILTIN_REFLOG_EXPIRE_USAGE,
BUILTIN_REFLOG_EXISTS_USAGE,
BUILTIN_REFLOG_WRITE_USAGE,
BUILTIN_REFLOG_DELETE_USAGE,
BUILTIN_REFLOG_DROP_USAGE,
BUILTIN_REFLOG_EXISTS_USAGE,
BUILTIN_REFLOG_EXPIRE_USAGE,
NULL
};

@ -395,6 +406,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,
return ret;
}

static int cmd_reflog_write(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
const struct option options[] = {
OPT_END()
};
struct object_id old_oid, new_oid;
struct strbuf err = STRBUF_INIT;
struct ref_transaction *tx;
const char *ref, *message;
int ret;

argc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);
if (argc != 4)
usage_with_options(reflog_write_usage, options);

ref = argv[0];
if (!is_root_ref(ref) && check_refname_format(ref, 0))
die(_("invalid reference name: %s"), ref);

ret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);
if (ret)
die(_("invalid old object ID: '%s'"), argv[1]);
if (!is_null_oid(&old_oid) && !odb_has_object(repo->objects, &old_oid, 0))
die(_("old object '%s' does not exist"), argv[1]);

ret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);
if (ret)
die(_("invalid new object ID: '%s'"), argv[2]);
if (!is_null_oid(&new_oid) && !odb_has_object(repo->objects, &new_oid, 0))
die(_("new object '%s' does not exist"), argv[2]);

message = argv[3];

tx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
if (!tx)
die(_("cannot start transaction: %s"), err.buf);

ret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,
git_committer_info(0),
message, 0, &err);
if (ret)
die(_("cannot queue reflog update: %s"), err.buf);

ret = ref_transaction_commit(tx, &err);
if (ret)
die(_("cannot commit reflog update: %s"), err.buf);

ref_transaction_free(tx);
strbuf_release(&err);
return 0;
}

/*
* main "reflog"
*/
@ -407,10 +471,11 @@ int cmd_reflog(int argc,
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),
OPT_SUBCOMMAND("write", &fn, cmd_reflog_write),
OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete),
OPT_SUBCOMMAND("drop", &fn, cmd_reflog_drop),
OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire),
OPT_END()
};


View File

@ -272,7 +272,7 @@ static void strbuf_addstr_without_crud(struct strbuf *sb, const char *src)
* can still be NULL if the input line only has the name/email part
* (e.g. reading from a reflog entry).
*/
int split_ident_line(struct ident_split *split, const char *line, int len)
int split_ident_line(struct ident_split *split, const char *line, size_t len)
{
const char *cp;
size_t span;

View File

@ -35,7 +35,7 @@ void reset_ident_date(void);
* Signals an success with 0, but time part of the result may be NULL
* if the input lacks timestamp and zone
*/
int split_ident_line(struct ident_split *, const char *, int);
int split_ident_line(struct ident_split *, const char *, size_t);

/*
* Given a commit or tag object buffer and the commit or tag headers, replaces

60
refs.c
View File

@ -1362,27 +1362,22 @@ int ref_transaction_update(struct ref_transaction *transaction,
return 0;
}

/*
* Similar to`ref_transaction_update`, but this function is only for adding
* a reflog update. Supports providing custom committer information. The index
* field can be utiltized to order updates as desired. When not used, the
* updates default to being ordered by refname.
*/
static int ref_transaction_update_reflog(struct ref_transaction *transaction,
const char *refname,
const struct object_id *new_oid,
const struct object_id *old_oid,
const char *committer_info,
unsigned int flags,
const char *msg,
uint64_t index,
struct strbuf *err)
int ref_transaction_update_reflog(struct ref_transaction *transaction,
const char *refname,
const struct object_id *new_oid,
const struct object_id *old_oid,
const char *committer_info,
const char *msg,
uint64_t index,
struct strbuf *err)
{
struct ref_update *update;
unsigned int flags;

assert(err);

flags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;
flags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF |
REF_LOG_USE_PROVIDED_OIDS;

if (!transaction_refname_valid(refname, new_oid, flags, err))
return -1;
@ -1390,11 +1385,6 @@ static int ref_transaction_update_reflog(struct ref_transaction *transaction,
update = ref_transaction_add_update(transaction, refname, flags,
new_oid, old_oid, NULL, NULL,
committer_info, msg);
/*
* While we do set the old_oid value, we unset the flag to skip
* old_oid verification which only makes sense for refs.
*/
update->flags &= ~REF_HAVE_OLD;
update->index = index;

/*
@ -2951,7 +2941,7 @@ struct migration_data {
struct ref_store *old_refs;
struct ref_transaction *transaction;
struct strbuf *errbuf;
struct strbuf sb;
struct strbuf sb, name, mail;
};

static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,
@ -2990,7 +2980,7 @@ struct reflog_migration_data {
struct ref_store *old_refs;
struct ref_transaction *transaction;
struct strbuf *errbuf;
struct strbuf *sb;
struct strbuf *sb, *name, *mail;
};

static int migrate_one_reflog_entry(struct object_id *old_oid,
@ -3000,18 +2990,25 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,
const char *msg, void *cb_data)
{
struct reflog_migration_data *data = cb_data;
struct ident_split ident;
const char *date;
int ret;

if (split_ident_line(&ident, committer, strlen(committer)) < 0)
return -1;

strbuf_reset(data->name);
strbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);
strbuf_reset(data->mail);
strbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);

date = show_date(timestamp, tz, DATE_MODE(NORMAL));
strbuf_reset(data->sb);
/* committer contains name and email */
strbuf_addstr(data->sb, fmt_ident("", committer, WANT_BLANK_IDENT, date, 0));
strbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));

ret = ref_transaction_update_reflog(data->transaction, data->refname,
new_oid, old_oid, data->sb->buf,
REF_HAVE_NEW | REF_HAVE_OLD, msg,
data->index++, data->errbuf);
msg, data->index++, data->errbuf);
return ret;
}

@ -3024,6 +3021,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)
.transaction = migration_data->transaction,
.errbuf = migration_data->errbuf,
.sb = &migration_data->sb,
.name = &migration_data->name,
.mail = &migration_data->mail,
};

return refs_for_each_reflog_ent(migration_data->old_refs, refname,
@ -3122,6 +3121,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,
struct strbuf new_gitdir = STRBUF_INIT;
struct migration_data data = {
.sb = STRBUF_INIT,
.name = STRBUF_INIT,
.mail = STRBUF_INIT,
};
int did_migrate_refs = 0;
int ret;
@ -3297,11 +3298,16 @@ done:
ref_transaction_free(transaction);
strbuf_release(&new_gitdir);
strbuf_release(&data.sb);
strbuf_release(&data.name);
strbuf_release(&data.mail);
return ret;
}

int ref_update_expects_existing_old_ref(struct ref_update *update)
{
if (update->flags & REF_LOG_ONLY)
return 0;

return (update->flags & REF_HAVE_OLD) &&
(!is_null_oid(&update->old_oid) || update->old_target);
}

24
refs.h
View File

@ -759,13 +759,20 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,
*/
#define REF_SKIP_CREATE_REFLOG (1 << 12)

/*
* When writing a REF_LOG_ONLY record, use the old and new object IDs provided
* in the update instead of resolving the old object ID. The caller must also
* set both REF_HAVE_OLD and REF_HAVE_NEW.
*/
#define REF_LOG_USE_PROVIDED_OIDS (1 << 13)

/*
* Bitmask of all of the flags that are allowed to be passed in to
* ref_transaction_update() and friends:
*/
#define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS \
(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \
REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)
REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | REF_LOG_USE_PROVIDED_OIDS)

/*
* Add a reference update to transaction. `new_oid` is the value that
@ -794,6 +801,21 @@ int ref_transaction_update(struct ref_transaction *transaction,
unsigned int flags, const char *msg,
struct strbuf *err);

/*
* Similar to `ref_transaction_update`, but this function is only for adding
* a reflog update. Supports providing custom committer information. The index
* field can be utiltized to order updates as desired. When set to zero, the
* updates default to being ordered by refname.
*/
int ref_transaction_update_reflog(struct ref_transaction *transaction,
const char *refname,
const struct object_id *new_oid,
const struct object_id *old_oid,
const char *committer_info,
const char *msg,
uint64_t index,
struct strbuf *err);

/*
* Add a reference creation to transaction. new_oid is the value that
* the reference should have after the update; it must not be

View File

@ -68,6 +68,12 @@
*/
#define REF_DELETED_RMDIR (1 << 9)

/*
* Used to indicate that the reflog-only update has been created via
* `split_head_update()`.
*/
#define REF_LOG_VIA_SPLIT (1 << 14)

struct ref_lock {
char *ref_name;
struct lock_file lk;
@ -2421,9 +2427,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,

new_update = ref_transaction_add_update(
transaction, "HEAD",
update->flags | REF_LOG_ONLY | REF_NO_DEREF,
update->flags | REF_LOG_ONLY | REF_NO_DEREF | REF_LOG_VIA_SPLIT,
&update->new_oid, &update->old_oid,
NULL, NULL, update->committer_info, update->msg);
new_update->parent_update = update;

/*
* Add "HEAD". This insertion is O(N) in the transaction
@ -2494,7 +2501,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,
* done when new_update is processed.
*/
update->flags |= REF_LOG_ONLY | REF_NO_DEREF;
update->flags &= ~REF_HAVE_OLD;

return 0;
}
@ -2509,8 +2515,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,
struct object_id *oid,
struct strbuf *err)
{
if (!(update->flags & REF_HAVE_OLD) ||
oideq(oid, &update->old_oid))
if (update->flags & REF_LOG_ONLY ||
!(update->flags & REF_HAVE_OLD) ||
oideq(oid, &update->old_oid))
return 0;

if (is_null_oid(&update->old_oid)) {
@ -2601,7 +2608,36 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re

update->backend_data = lock;

if (update->type & REF_ISSYMREF) {
if (update->flags & REF_LOG_VIA_SPLIT) {
struct ref_lock *parent_lock;

if (!update->parent_update)
BUG("split update without a parent");

parent_lock = update->parent_update->backend_data;

/*
* Check that "HEAD" didn't racily change since we have looked
* it up. If it did we must refuse to write the reflog entry.
*
* Note that this does not catch all races: if "HEAD" was
* racily changed to point to one of the refs part of the
* transaction then we would miss writing the split reflog
* entry for "HEAD".
*/
if (!(update->type & REF_ISSYMREF) ||
strcmp(update->parent_update->refname, referent.buf)) {
strbuf_addstr(err, "HEAD has been racily updated");
ret = REF_TRANSACTION_ERROR_GENERIC;
goto out;
}

if (update->flags & REF_HAVE_OLD) {
oidcpy(&lock->old_oid, &update->old_oid);
} else {
oidcpy(&lock->old_oid, &parent_lock->old_oid);
}
} else if (update->type & REF_ISSYMREF) {
if (update->flags & REF_NO_DEREF) {
/*
* We won't be reading the referent as part of
@ -2977,6 +3013,20 @@ static int parse_and_write_reflog(struct files_ref_store *refs,
struct ref_lock *lock,
struct strbuf *err)
{
struct object_id *old_oid = &lock->old_oid;

if (update->flags & REF_LOG_USE_PROVIDED_OIDS) {
if (!(update->flags & REF_HAVE_OLD) ||
!(update->flags & REF_HAVE_NEW) ||
!(update->flags & REF_LOG_ONLY)) {
strbuf_addf(err, _("trying to write reflog for '%s'"
"with incomplete values"), update->refname);
return REF_TRANSACTION_ERROR_GENERIC;
}

old_oid = &update->old_oid;
}

if (update->new_target) {
/*
* We want to get the resolved OID for the target, to ensure
@ -2994,7 +3044,7 @@ static int parse_and_write_reflog(struct files_ref_store *refs,
}
}

if (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,
if (files_log_ref_write(refs, lock->ref_name, old_oid,
&update->new_oid, update->committer_info,
update->msg, update->flags, err)) {
char *old_msg = strbuf_detach(err, NULL);
@ -3062,7 +3112,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,
for (i = 0; i < transaction->nr; i++) {
struct ref_update *update = transaction->updates[i];

if ((update->flags & REF_HAVE_OLD) &&
if (!(update->flags & REF_LOG_ONLY) &&
(update->flags & REF_HAVE_OLD) &&
!is_null_oid(&update->old_oid))
BUG("initial ref transaction with old_sha1 set");


View File

@ -662,7 +662,8 @@ enum ref_transaction_error ref_update_check_old_target(const char *referent,

/*
* Check if the ref must exist, this means that the old_oid or
* old_target is non NULL.
* old_target is non NULL. Log-only updates never require the old state to
* match.
*/
int ref_update_expects_existing_old_ref(struct ref_update *update);


View File

@ -1102,6 +1102,20 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor
if (ret)
return REF_TRANSACTION_ERROR_GENERIC;

if (u->flags & REF_LOG_USE_PROVIDED_OIDS) {
if (!(u->flags & REF_HAVE_OLD) ||
!(u->flags & REF_HAVE_NEW) ||
!(u->flags & REF_LOG_ONLY)) {
strbuf_addf(err, _("trying to write reflog for '%s'"
"with incomplete values"), u->refname);
return REF_TRANSACTION_ERROR_GENERIC;
}

if (queue_transaction_update(refs, tx_data, u, &u->old_oid, err))
return REF_TRANSACTION_ERROR_GENERIC;
return 0;
}

/* Verify that the new object ID is valid. */
if ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&
!(u->flags & REF_SKIP_OID_VERIFICATION) &&
@ -1186,8 +1200,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor
if (ret > 0) {
/* The reference does not exist, but we expected it to. */
strbuf_addf(err, _("cannot lock ref '%s': "


"unable to resolve reference '%s'"),
ref_update_original_update_refname(u), u->refname);
return REF_TRANSACTION_ERROR_NONEXISTENT_REF;
@ -1241,13 +1253,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor

new_update->parent_update = u;

/*
* Change the symbolic ref update to log only. Also, it
* doesn't need to check its old OID value, as that will be
* done when new_update is processed.
*/
/* Change the symbolic ref update to log only. */
u->flags |= REF_LOG_ONLY | REF_NO_DEREF;
u->flags &= ~REF_HAVE_OLD;
}
}

@ -1271,7 +1278,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor
ret = ref_update_check_old_target(referent->buf, u, err);
if (ret)
return ret;
} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&
!oideq(&current_oid, &u->old_oid)) {
if (is_null_oid(&u->old_oid)) {
strbuf_addf(err, _("cannot lock ref '%s': "
"reference already exists"),

View File

@ -204,6 +204,7 @@ integration_tests = [
't1418-reflog-exists.sh',
't1419-exclude-refs.sh',
't1420-lost-found.sh',
't1421-reflog-write.sh',
't1430-bad-ref-name.sh',
't1450-fsck.sh',
't1451-fsck-buffer.sh',

126
t/t1421-reflog-write.sh Executable file
View File

@ -0,0 +1,126 @@
#!/bin/sh

test_description='Manually write reflog entries'

. ./test-lib.sh

SIGNATURE="C O Mitter <committer@example.com> 1112911993 -0700"

test_reflog_matches () {
repo="$1" &&
refname="$2" &&
cat >actual &&
test-tool -C "$repo" ref-store main for-each-reflog-ent "$refname" >expected &&
test_cmp expected actual
}

test_expect_success 'invalid number of arguments' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
for args in "" "1" "1 2" "1 2 3" "1 2 3 4 5"
do
test_must_fail git reflog write $args 2>err &&
test_grep "usage: git reflog write" err || return 1
done
)
'

test_expect_success 'invalid refname' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_must_fail git reflog write "refs/heads/ invalid" $ZERO_OID $ZERO_OID first 2>err &&
test_grep "invalid reference name: " err
)
'

test_expect_success 'unqualified refname is rejected' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_must_fail git reflog write unqualified $ZERO_OID $ZERO_OID first 2>err &&
test_grep "invalid reference name: " err
)
'

test_expect_success 'nonexistent object IDs' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&
test_grep "old object .* does not exist" err &&
test_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&
test_grep "new object .* does not exist" err
)
'

test_expect_success 'abbreviated object IDs' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
abbreviated_oid=$(git rev-parse HEAD | test_copy_bytes 8) &&
test_must_fail git reflog write refs/heads/something $abbreviated_oid $ZERO_OID old-object-id 2>err &&
test_grep "invalid old object ID" err &&
test_must_fail git reflog write refs/heads/something $ZERO_OID $abbreviated_oid new-object-id 2>err &&
test_grep "invalid new object ID" err
)
'

test_expect_success 'reflog message gets normalized' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
COMMIT_OID=$(git rev-parse HEAD) &&
git reflog write HEAD $COMMIT_OID $COMMIT_OID "$(printf "message\nwith\nnewlines")" &&
git reflog show -1 --format=%gs HEAD >actual &&
echo "message with newlines" >expected &&
test_cmp expected actual
)
'

test_expect_success 'simple writes' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
COMMIT_OID=$(git rev-parse HEAD) &&

git reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&
test_reflog_matches . refs/heads/something <<-EOF &&
$ZERO_OID $COMMIT_OID $SIGNATURE first
EOF

git reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&
test_reflog_matches . refs/heads/something <<-EOF
$ZERO_OID $COMMIT_OID $SIGNATURE first
$COMMIT_OID $COMMIT_OID $SIGNATURE second
EOF
)
'

test_expect_success 'can write to root ref' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
COMMIT_OID=$(git rev-parse HEAD) &&

git reflog write ROOT_REF_HEAD $ZERO_OID $COMMIT_OID first &&
test_reflog_matches . ROOT_REF_HEAD <<-EOF
$ZERO_OID $COMMIT_OID $SIGNATURE first
EOF
)
'

test_done

View File

@ -7,6 +7,17 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME

. ./test-lib.sh

print_all_reflog_entries () {
repo=$1 &&
test-tool -C "$repo" ref-store main for-each-reflog >reflogs &&
while read reflog
do
echo "REFLOG: $reflog" &&
test-tool -C "$repo" ref-store main for-each-reflog-ent "$reflog" ||
return 1
done <reflogs
}

# Migrate the provided repository from one format to the other and
# verify that the references and logs are migrated over correctly.
# Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]
@ -28,8 +39,7 @@ test_migration () {
--format='%(refname) %(objectname) %(symref)' >expect &&
if ! $skip_reflog_verify
then
git -C "$repo" reflog --all >expect_logs &&
git -C "$repo" reflog list >expect_log_list
print_all_reflog_entries "$repo" >expect_logs
fi &&

git -C "$repo" refs migrate --ref-format="$format" "$@" &&
@ -39,10 +49,8 @@ test_migration () {
test_cmp expect actual &&
if ! $skip_reflog_verify
then
git -C "$repo" reflog --all >actual_logs &&
git -C "$repo" reflog list >actual_log_list &&
test_cmp expect_logs actual_logs &&
test_cmp expect_log_list actual_log_list
print_all_reflog_entries "$repo" >actual_logs &&
test_cmp expect_logs actual_logs
fi &&

git -C "$repo" rev-parse --show-ref-format >actual &&
@ -273,7 +281,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' '
test_commit -C repo second &&
printf "update refs/heads/ref-%d HEAD\n" $(test_seq 3000) >stdin &&
git -C repo update-ref --stdin <stdin &&
test_migration repo reftable
test_migration repo reftable true
'

test_expect_success 'migrating from files format deletes backend files' '