Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries
- From
Justin Tobler <jltobler@gmail.com>
- Date
- Jul 23, 2025, 19:00 UTC
- Message-ID
- <mkqjim46zv4sfhzjjee2tcxwj23esfrupp5byienju5qlwvlht@r2gxnybioukw>
- In-Reply-To
- <20250722-pks-reflog-append-v1-4-183e5949de16@pks.im>
On 25/07/22 01:20PM, Patrick Steinhardt wrote:
Show 42 quoted lines
> While we provide a couple of subcommands in git-reflog(1) to remove > reflog entries, we don't provide any to write new entries. Obviously > this is not an operation that really would be needed for many use cases > out there, or otherwise people would have complained that such a command > does not exist yet. But the introduction of the "reftable" backend > changes the picture a bit, as it is now basically impossible to manually > append a reflog entry if one wanted to do so due to the binary format. > > Plug this gap by introducing a simple "write" subcommand. For now, all > this command does is to append a single new reflog entry with the given > object IDs and message to the reflog. More specifically, it is not yet > possible to: > > - Write multiple reflog entries at once. > > - Insert reflog entries at arbitrary indices. > > - Specify the date of the reflog entry. > > - Insert reflog entries that refer to nonexistent objects. > > If required, those features can be added at a future point in time. For > now though, the new command aims to fulfill the most basic use cases > while being as strict as possible when it comes to verifying parameters. > > Signed-off-by: Patrick Steinhardt <ps@pks.im> > --- > Documentation/git-reflog.adoc | 1 + > builtin/reflog.c | 65 ++++++++++++++++++++++++++++++++++ > t/meson.build | 1 + > t/t1421-reflog-write.sh | 81 +++++++++++++++++++++++++++++++++++++++++++ > 4 files changed, 148 insertions(+) > > diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc > index 6ae13e772b8..798dbc0a00a 100644 > --- a/Documentation/git-reflog.adoc > +++ b/Documentation/git-reflog.adoc > @@ -12,6 +12,7 @@ SYNOPSIS > git reflog [show] [<log-options>] [<ref>] > git reflog list > git reflog exists <ref> > +git reflog write <ref> <old-oid> <new-oid> <message>
The other subcommands each have an entry in the description. Do we want to also add something for the "write" subcommand?
Also, if we want to be consistent, I noticed the order of the subcommands listed in the description was not changed either.
Show 81 quoted lines
> git reflog delete [--rewrite] [--updateref]
> [--dry-run | -n] [--verbose] <ref>@{<specifier>}...
> git reflog drop [--all [--single-worktree] | <refs>...]
> diff --git a/builtin/reflog.c b/builtin/reflog.c
> index b00b3f9edc9..d0374295620 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"
> @@ -20,6 +22,9 @@
> #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>}...")
> @@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {
> NULL,
> };
>
> +static const char *const reflog_write_usage[] = {
> + BUILTIN_REFLOG_WRITE_USAGE,
> + NULL,
> +};
> +
> static const char *const reflog_delete_usage[] = {
> BUILTIN_REFLOG_DELETE_USAGE,
> NULL
> @@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {
> BUILTIN_REFLOG_SHOW_USAGE,
> BUILTIN_REFLOG_LIST_USAGE,
> BUILTIN_REFLOG_EXISTS_USAGE,
> + BUILTIN_REFLOG_WRITE_USAGE,
> BUILTIN_REFLOG_DELETE_USAGE,
> BUILTIN_REFLOG_DROP_USAGE,
> BUILTIN_REFLOG_EXPIRE_USAGE,
> @@ -392,6 +403,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_drop_usage, 0);
> + if (argc != 4)
> + usage_with_options(reflog_write_usage, options);
> +
> + ref = argv[0];
> + if (check_refname_format(ref, REFNAME_ALLOW_ONELEVEL))
> + 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]);Ok so we validate the reference name and the old/new obects names to make sure they are sane.
Show 16 quoted lines
> +
> + 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);And here we write the reflog entry. Looks good
Show 121 quoted lines
> +
> + ref_transaction_free(tx);
> + strbuf_release(&err);
> + return 0;
> +}
> +
> /*
> * main "reflog"
> */
> @@ -405,6 +469,7 @@ int cmd_reflog(int argc,
> OPT_SUBCOMMAND("show", &fn, cmd_reflog_show),
> OPT_SUBCOMMAND("list", &fn, cmd_reflog_list),
> 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),
> diff --git a/t/meson.build b/t/meson.build
> index 1af289425d4..d68f5e24dbe 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -219,6 +219,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 00000000000..e284f42178f
> --- /dev/null
> +++ b/t/t1421-reflog-write.sh
> @@ -0,0 +1,81 @@
> +#!/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 'nonexistent old object ID' '
> + 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 first 2>err &&
> + test_grep "old object .* does not exist" err
> + )
> +'
> +
> +test_expect_success 'nonexistent new object ID' '
> + test_when_finished "rm -rf repo" &&
> + git init repo &&
> + (
> + cd repo &&
> + test_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) first 2>err &&
> + test_grep "new object .* does not exist" err
> + )
> +'
> +
> +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_done
>
> --
> 2.50.1.465.gcb3da1c9e6.dirty
>
>