diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc index 4eb6c25607..38af0c977a 100644 --- a/Documentation/git-reflog.adoc +++ b/Documentation/git-reflog.adoc @@ -12,6 +12,7 @@ SYNOPSIS git reflog [show] [] [] git reflog list git reflog exists +git reflog write git reflog delete [--rewrite] [--updateref] [--dry-run | -n] [--verbose] @{}... git reflog drop [--all [--single-worktree] | ...] @@ -47,6 +48,12 @@ 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 reflog delete master@{2}`"). This subcommand is also typically not used diff --git a/builtin/reflog.c b/builtin/reflog.c index b00b3f9edc..a1b4e02204 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 "object-store.h" #include "revision.h" #include "reachable.h" #include "wildmatch.h" @@ -20,6 +22,9 @@ #define BUILTIN_REFLOG_EXISTS_USAGE \ N_("git reflog exists ") +#define BUILTIN_REFLOG_WRITE_USAGE \ + N_("git reflog write ") + #define BUILTIN_REFLOG_DELETE_USAGE \ N_("git reflog delete [--rewrite] [--updateref]\n" \ " [--dry-run | -n] [--verbose] @{}...") @@ -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_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) && !has_object(the_repository, &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) && !has_object(the_repository, &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" */ @@ -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 d052fc3e23..adcdf09e74 100644 --- a/t/meson.build +++ b/t/meson.build @@ -220,6 +220,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..dd7ffa5241 --- /dev/null +++ b/t/t1421-reflog-write.sh @@ -0,0 +1,128 @@ +#!/bin/sh + +test_description='Manually write reflog entries' + +. ./test-lib.sh + +SIGNATURE="C O Mitter 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 && + # Note: the old object ID of the second reflog entry is broken. + # This will be fixed in subsequent commits. + test_reflog_matches . refs/heads/something <<-EOF + $ZERO_OID $COMMIT_OID $SIGNATURE first + $ZERO_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