On 25/07/22 01:20PM, Patrick Steinhardt wrote: > 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@xxxxxx> > --- > 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. > 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. > + > + 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 > + > + 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@xxxxxxxxxxx> 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 > >