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>...]
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
#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"
#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>}...")
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
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,
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"
*/
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),
--- /dev/null
+#!/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 &&
+ # 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