summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/git-reflog.adoc96
-rw-r--r--builtin/reflog.c103
-rw-r--r--ident.c2
-rw-r--r--ident.h2
-rw-r--r--refs.c60
-rw-r--r--refs.h24
-rw-r--r--refs/files-backend.c65
-rw-r--r--refs/refs-internal.h3
-rw-r--r--refs/reftable-backend.c26
-rw-r--r--t/meson.build1
-rwxr-xr-xt/t1421-reflog-write.sh126
-rwxr-xr-xt/t1460-refs-migrate.sh22
12 files changed, 413 insertions, 117 deletions
diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc
index 412f06b8fe..38af0c977a 100644
--- a/Documentation/git-reflog.adoc
+++ b/Documentation/git-reflog.adoc
@@ -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,18 +78,37 @@ Options for `show`
`git reflog show` accepts any of the options accepted by `git log`.
+Options for `delete`
+~~~~~~~~~~~~~~~~~~~~
+
+`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,
+`--dry-run`, and `--verbose`, with the same meanings as when they are
+used with `expire`.
+
+Options for `drop`
+~~~~~~~~~~~~~~~~~~
+
+`--all`::
+ Drop the reflogs of all references from all worktrees.
+
+`--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::
+`--all`::
Process the reflogs of all references.
---single-worktree::
+`--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>::
+`--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
@@ -90,7 +116,7 @@ Options for `expire`
of their age; `--expire=never` turns off pruning of reachable
entries (but see `--expire-unreachable`).
---expire-unreachable=<time>::
+`--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
@@ -100,17 +126,17 @@ Options for `expire`
turns off early pruning of unreachable entries (but see
`--expire`).
---updateref::
+`--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::
+`--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::
+`--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
@@ -121,33 +147,15 @@ 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::
+`-n`::
+`--dry-run`::
Do not actually prune any entries; just show what would have
been pruned.
---verbose::
+`--verbose`::
Print extra information on screen.
-Options for `delete`
-~~~~~~~~~~~~~~~~~~~~
-
-`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,
-`--dry-run`, and `--verbose`, with the same meanings as when they are
-used with `expire`.
-
-Options for `drop`
-~~~~~~~~~~~~~~~~~~
-
---all::
- Drop the reflogs of all references from all worktrees.
-
---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.
-
GIT
---
Part of the linkgit:git[1] suite
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 1db26aa65f..c8f6b93d60 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -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()
};
diff --git a/ident.c b/ident.c
index 281e830573..0b7aacecd7 100644
--- a/ident.c
+++ b/ident.c
@@ -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;
diff --git a/ident.h b/ident.h
index 6a79febba1..3c03403879 100644
--- a/ident.h
+++ b/ident.h
@@ -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
diff --git a/refs.c b/refs.c
index bfdbe718b7..f852010457 100644
--- a/refs.c
+++ b/refs.c
@@ -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);
}
diff --git a/refs.h b/refs.h
index eedbb599c5..a540aa5e1b 100644
--- a/refs.h
+++ b/refs.h
@@ -760,12 +760,19 @@ 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
@@ -795,6 +802,21 @@ int ref_transaction_update(struct ref_transaction *transaction,
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
* null_oid. It is verified that the reference does not exist
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 088b52c740..dea46d07ae 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -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");
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 40c1c0f93d..54c2079c12 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -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);
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 8dae1e1112..c0440b4bd0 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -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"),
diff --git a/t/meson.build b/t/meson.build
index bbeba1a8d5..983245501c 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -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',
diff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh
new file mode 100755
index 0000000000..46df64c176
--- /dev/null
+++ b/t/t1421-reflog-write.sh
@@ -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
diff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh
index 2ab97e1b7d..0e1116a319 100755
--- a/t/t1460-refs-migrate.sh
+++ b/t/t1460-refs-migrate.sh
@@ -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' '