{"thread":{"id":"63818","subject":"[PATCH 0/8] refs: fix migration of reflog entries","startedAt":"2025-07-22T11:21:05Z","lastAt":"2025-08-06T05:54:53Z","messageCount":114,"participants":["Patrick Steinhardt","Junio C Hamano","Justin Tobler","Karthik Nayak","Toon Claes","SZEDER Gábor","Jeff King","Kristoffer Haugsbakk","Ben Knoble","Jean-Noël AVILA"],"isPatch":true,"patchVersion":1,"patchTotal":8},"messages":[{"id":"522423","messageId":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","threadId":"63818","inReplyTo":null,"subject":"[PATCH 0/8] refs: fix migration of reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:49Z","receivedAt":"2025-07-22T11:21:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nafter the announcement that \"reftable\" will become the default backend\nin Git 3.0 I've revived the efforts to implement this backend in\nlibgit2. I'm happy to report that this implementation is almost done by\nnow: out of 3000 tests only four are failing now.\n\nFor two of these tests I have been completely puzzled why those are\nfailing, as everything really looked perfectly fine in libgit2. As it\nturned out, the bug wasn't in libgit2 though, but in Git. Namely, the\nway we migrate reflog entries between storage formats is broken in two\nways:\n\n  - The identity we write into the reflog entries is wrong.\n\n  - The old commit ID of reflog entries is always set to all-zeroes.\n    This is what caused the libgit2 tests to fail, as I used `git refs\n    migrate` to convert test repositories to use reftables.\n\nThis patch series fixes both of these issues. Furthermore, it also adds\na new `git reflog write` subcommand to write new reflog entries for a\nspecific reference. This command was helpful to reproduce some test\nconstellations in libgit2.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (8):\n      Documentation/git-reflog: convert to use synopsis type\n      builtin/reflog: improve grouping of subcommands\n      refs: export `ref_transaction_update_reflog()`\n      builtin/reflog: implement subcommand to write new entries\n      ident: fix type of string length parameter\n      refs: fix identity for migrated reflogs\n      refs: stop unsetting REF_HAVE_OLD for log-only updates\n      refs: fix invalid old object IDs when migrating reflogs\n\n Documentation/git-reflog.adoc |  17 +++----\n builtin/reflog.c              | 103 ++++++++++++++++++++++++++++++++++--------\n ident.c                       |   2 +-\n ident.h                       |   2 +-\n refs.c                        |  58 +++++++++++++-----------\n refs.h                        |  24 +++++++++-\n refs/files-backend.c          |  25 ++++++++--\n refs/reftable-backend.c       |  26 +++++++----\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       |  81 +++++++++++++++++++++++++++++++++\n t/t1460-refs-migrate.sh       |  22 ++++++---\n 11 files changed, 283 insertions(+), 78 deletions(-)\n\n\n---\nbase-commit: 3f2a94875d2f41fe4758a439f68d8b73cfb19d0f\nchange-id: 20250722-pks-reflog-append-634172d8ab2c\n\n"},{"id":"522424","messageId":"20250722-pks-reflog-append-v1-1-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 1/8] Documentation/git-reflog: convert to use synopsis type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:50Z","receivedAt":"2025-07-22T11:21:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\nhave introduced a new synopsis type that simplifies the rules for\ntypesetting a command's synopsis. Convert the git-reflog(1)\ndocumentation to use it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 14 +++++++-------\n 1 file changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 412f06b8fec..707a9b39edb 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n \n SYNOPSIS\n --------\n-[verse]\n-'git reflog' [show] [<log-options>] [<ref>]\n-'git reflog list'\n-'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n+[synopsis]\n+git reflog [show] [<log-options>] [<ref>]\n+git reflog list\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n \t[--rewrite] [--updateref] [--stale-fix]\n \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n-'git reflog delete' [--rewrite] [--updateref]\n+git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n-'git reflog drop' [--all [--single-worktree] | <refs>...]\n-'git reflog exists' <ref>\n+git reflog drop [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n \n DESCRIPTION\n -----------\n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522425","messageId":"20250722-pks-reflog-append-v1-2-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 2/8] builtin/reflog: improve grouping of subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:51Z","receivedAt":"2025-07-22T11:21:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The way subcommands of git-reflog(1) are layed out does not make any\nimmediate sense. Reorder them such that read-only subcommands precede\nwriting commands for a bit more structure.\n\nFurthermore, move the \"expire\" subcommand last. This prepares for a\nsubsequent change where we are about to introduce a new \"write\" command\nto append reflog entries. Like this, the writing subcommands are ordered\nsuch that those affecting a single reflog come before those spanning\nacross all reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc |  8 ++++----\n builtin/reflog.c              | 38 +++++++++++++++++++-------------------\n 2 files changed, 23 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 707a9b39edb..6ae13e772b8 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -11,13 +11,13 @@ SYNOPSIS\n [synopsis]\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n-git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n-\t[--rewrite] [--updateref] [--stale-fix]\n-\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n-git reflog exists <ref>\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n+\t[--rewrite] [--updateref] [--stale-fix]\n+\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n \n DESCRIPTION\n -----------\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 3acaf3e32c2..b00b3f9edc9 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -17,21 +17,21 @@\n #define BUILTIN_REFLOG_LIST_USAGE \\\n \tN_(\"git reflog list\")\n \n-#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n-\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n-\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n-\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+#define BUILTIN_REFLOG_EXISTS_USAGE \\\n+\tN_(\"git reflog exists <ref>\")\n \n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n \n-#define BUILTIN_REFLOG_EXISTS_USAGE \\\n-\tN_(\"git reflog exists <ref>\")\n-\n #define BUILTIN_REFLOG_DROP_USAGE \\\n \tN_(\"git reflog drop [--all [--single-worktree] | <refs>...]\")\n \n+#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n+\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n+\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n+\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+\n static const char *const reflog_show_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tNULL,\n@@ -42,9 +42,9 @@ static const char *const reflog_list_usage[] = {\n \tNULL,\n };\n \n-static const char *const reflog_expire_usage[] = {\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n-\tNULL\n+static const char *const reflog_exists_usage[] = {\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tNULL,\n };\n \n static const char *const reflog_delete_usage[] = {\n@@ -52,23 +52,23 @@ static const char *const reflog_delete_usage[] = {\n \tNULL\n };\n \n-static const char *const reflog_exists_usage[] = {\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n-\tNULL,\n-};\n-\n static const char *const reflog_drop_usage[] = {\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tNULL,\n };\n \n+static const char *const reflog_expire_usage[] = {\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tNULL\n+};\n+\n static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n \tNULL\n };\n \n@@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n-\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n-\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522426","messageId":"20250722-pks-reflog-append-v1-3-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 3/8] refs: export `ref_transaction_update_reflog()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:52Z","receivedAt":"2025-07-22T11:21:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we'll add another user that wants to write reflog\nentries. This requires them to call `ref_transaction_update_reflog()`,\nbut that functino is local to \"refs.c\".\n\nExport the function to prepare for the change. While at it, drop the\n`flags` field, as all callers are for now expected to use the same flags\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 29 +++++++++++------------------\n refs.h | 15 +++++++++++++++\n 2 files changed, 26 insertions(+), 18 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 73913b6627b..188989e4113 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1362,27 +1362,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \treturn 0;\n }\n \n-/*\n- * Similar to`ref_transaction_update`, but this function is only for adding\n- * a reflog update. Supports providing custom committer information. The index\n- * field can be utiltized to order updates as desired. When not used, the\n- * updates default to being ordered by refname.\n- */\n-static int ref_transaction_update_reflog(struct ref_transaction *transaction,\n-\t\t\t\t\t const char *refname,\n-\t\t\t\t\t const struct object_id *new_oid,\n-\t\t\t\t\t const struct object_id *old_oid,\n-\t\t\t\t\t const char *committer_info,\n-\t\t\t\t\t unsigned int flags,\n-\t\t\t\t\t const char *msg,\n-\t\t\t\t\t uint64_t index,\n-\t\t\t\t\t struct strbuf *err)\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err)\n {\n \tstruct ref_update *update;\n+\tunsigned int flags;\n \n \tassert(err);\n \n-\tflags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\n@@ -3010,8 +3004,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n-\t\t\t\t\t    REF_HAVE_NEW | REF_HAVE_OLD, msg,\n-\t\t\t\t\t    data->index++, data->errbuf);\n+\t\t\t\t\t    msg, data->index++, data->errbuf);\n \treturn ret;\n }\n \ndiff --git a/refs.h b/refs.h\nindex efa182c6a14..0faf3bc0422 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -794,6 +794,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \n+/*\n+ * Similar to`ref_transaction_update`, but this function is only for adding\n+ * a reflog update. Supports providing custom committer information. The index\n+ * field can be utiltized to order updates as desired. When not used, the\n+ * updates default to being ordered by refname.\n+ */\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err);\n+\n /*\n  * Add a reference creation to transaction. new_oid is the value that\n  * the reference should have after the update; it must not be\n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522428","messageId":"20250722-pks-reflog-append-v1-4-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:53Z","receivedAt":"2025-07-22T11:21:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we provide a couple of subcommands in git-reflog(1) to remove\nreflog entries, we don't provide any to write new entries. Obviously\nthis is not an operation that really would be needed for many use cases\nout there, or otherwise people would have complained that such a command\ndoes not exist yet. But the introduction of the \"reftable\" backend\nchanges the picture a bit, as it is now basically impossible to manually\nappend a reflog entry if one wanted to do so due to the binary format.\n\nPlug this gap by introducing a simple \"write\" subcommand. For now, all\nthis command does is to append a single new reflog entry with the given\nobject IDs and message to the reflog. More specifically, it is not yet\npossible to:\n\n  - Write multiple reflog entries at once.\n\n  - Insert reflog entries at arbitrary indices.\n\n  - Specify the date of the reflog entry.\n\n  - Insert reflog entries that refer to nonexistent objects.\n\nIf required, those features can be added at a future point in time. For\nnow though, the new command aims to fulfill the most basic use cases\nwhile being as strict as possible when it comes to verifying parameters.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc |  1 +\n builtin/reflog.c              | 65 ++++++++++++++++++++++++++++++++++\n t/meson.build                 |  1 +\n t/t1421-reflog-write.sh       | 81 +++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 148 insertions(+)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 6ae13e772b8..798dbc0a00a 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n git reflog exists <ref>\n+git reflog write <ref> <old-oid> <new-oid> <message>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b00b3f9edc9..d0374295620 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -3,6 +3,8 @@\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"odb.h\"\n #include \"revision.h\"\n #include \"reachable.h\"\n #include \"wildmatch.h\"\n@@ -20,6 +22,9 @@\n #define BUILTIN_REFLOG_EXISTS_USAGE \\\n \tN_(\"git reflog exists <ref>\")\n \n+#define BUILTIN_REFLOG_WRITE_USAGE \\\n+\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n+\n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n@@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n \tNULL,\n };\n \n+static const char *const reflog_write_usage[] = {\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n+\tNULL,\n+};\n+\n static const char *const reflog_delete_usage[] = {\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tNULL\n@@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n \tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tBUILTIN_REFLOG_EXPIRE_USAGE,\n@@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n+\t\t\t    struct repository *repo)\n+{\n+\tconst struct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct object_id old_oid, new_oid;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct ref_transaction *tx;\n+\tconst char *ref, *message;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);\n+\tif (argc != 4)\n+\t\tusage_with_options(reflog_write_usage, options);\n+\n+\tref = argv[0];\n+\tif (check_refname_format(ref, REFNAME_ALLOW_ONELEVEL))\n+\t\tdie(_(\"invalid reference name: %s\"), ref);\n+\n+\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\tif (!is_null_oid(&old_oid) && !odb_has_object(repo->objects, &old_oid, 0))\n+\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n+\n+\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n+\tif (!is_null_oid(&new_oid) && !odb_has_object(repo->objects, &new_oid, 0))\n+\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n+\n+\tmessage = argv[3];\n+\n+\ttx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\tif (!tx)\n+\t\tdie(_(\"cannot start transaction: %s\"), err.buf);\n+\n+\tret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,\n+\t\t\t\t\t    git_committer_info(0),\n+\t\t\t\t\t    message, 0, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot queue reflog update: %s\"), err.buf);\n+\n+\tret = ref_transaction_commit(tx, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot commit reflog update: %s\"), err.buf);\n+\n+\tref_transaction_free(tx);\n+\tstrbuf_release(&err);\n+\treturn 0;\n+}\n+\n /*\n  * main \"reflog\"\n  */\n@@ -405,6 +469,7 @@ int cmd_reflog(int argc,\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_reflog_write),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n \t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\ndiff --git a/t/meson.build b/t/meson.build\nindex 1af289425d4..d68f5e24dbe 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -219,6 +219,7 @@ integration_tests = [\n   't1418-reflog-exists.sh',\n   't1419-exclude-refs.sh',\n   't1420-lost-found.sh',\n+  't1421-reflog-write.sh',\n   't1430-bad-ref-name.sh',\n   't1450-fsck.sh',\n   't1451-fsck-buffer.sh',\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nnew file mode 100755\nindex 00000000000..e284f42178f\n--- /dev/null\n+++ b/t/t1421-reflog-write.sh\n@@ -0,0 +1,81 @@\n+#!/bin/sh\n+\n+test_description='Manually write reflog entries'\n+\n+. ./test-lib.sh\n+\n+SIGNATURE=\"C O Mitter <committer@example.com> 1112911993 -0700\"\n+\n+test_reflog_matches () {\n+\trepo=\"$1\" &&\n+\trefname=\"$2\" &&\n+\tcat >actual &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$refname\" >expected &&\n+\ttest_cmp expected actual\n+}\n+\n+test_expect_success 'invalid number of arguments' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tfor args in \"\" \"1\" \"1 2\" \"1 2 3\" \"1 2 3 4 5\"\n+\t\tdo\n+\t\t\ttest_must_fail git reflog write $args 2>err &&\n+\t\t\ttest_grep \"usage: git reflog write\" err || return 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success 'invalid refname' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write \"refs/heads/ invalid\" $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'nonexistent old object ID' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID first 2>err &&\n+\t\ttest_grep \"old object .* does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'nonexistent new object ID' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) first 2>err &&\n+\t\ttest_grep \"new object .* does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'simple writes' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF &&\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522427","messageId":"20250722-pks-reflog-append-v1-5-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 5/8] ident: fix type of string length parameter","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:54Z","receivedAt":"2025-07-22T11:21:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The last parameter in `split_ident_line()` is the length of the line\npassed in by the caller. As such, most callers pass in either the result\nof `strlen()`, `struct strbuf::len` or a pointer diff, all of which\nare expected to be positive numbers. Regardless of that, the function\naccepts a signed integer, which is somewhat confusing.\n\nFix the function signature to instead accept a `size_t`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ident.c | 2 +-\n ident.h | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ident.c b/ident.c\nindex 281e830573b..0b7aacecd7d 100644\n--- a/ident.c\n+++ b/ident.c\n@@ -272,7 +272,7 @@ static void strbuf_addstr_without_crud(struct strbuf *sb, const char *src)\n  * can still be NULL if the input line only has the name/email part\n  * (e.g. reading from a reflog entry).\n  */\n-int split_ident_line(struct ident_split *split, const char *line, int len)\n+int split_ident_line(struct ident_split *split, const char *line, size_t len)\n {\n \tconst char *cp;\n \tsize_t span;\ndiff --git a/ident.h b/ident.h\nindex 6a79febba15..3c034038791 100644\n--- a/ident.h\n+++ b/ident.h\n@@ -35,7 +35,7 @@ void reset_ident_date(void);\n  * Signals an success with 0, but time part of the result may be NULL\n  * if the input lacks timestamp and zone\n  */\n-int split_ident_line(struct ident_split *, const char *, int);\n+int split_ident_line(struct ident_split *, const char *, size_t);\n \n /*\n  * Given a commit or tag object buffer and the commit or tag headers, replaces\n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522429","messageId":"20250722-pks-reflog-append-v1-6-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 6/8] refs: fix identity for migrated reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:55Z","receivedAt":"2025-07-22T11:21:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we must\nreconstruct the identity of reflog entries. This is done by passing the\ncommitter passed to the `migrate_one_reflog_entry()` callback function\nto `fmt_ident()`.\n\nThis results in an invalid identity though: `fmt_ident()` expects the\ncaller to provide both name and mail of the author, but we pass the full\nidentity as mail. This leads to an identity like:\n\n    pks <Patrick Steinhardt ps@pks.im>\n\nFix the bug by splitting the identity line first. This allows us to\nextract both the name and mail so that we can pass them to `fmt_ident()`\nseparately.\n\nThis commit does not yet add any tests as there is another bug in the\nreflog migration that will be fixed in a subsequent commit. Once that\nbug is fixed we'll make the reflog verification in t1450 stricter, and\nthat will catch both this bug here and the other bug.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 21 ++++++++++++++++++---\n 1 file changed, 18 insertions(+), 3 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 188989e4113..64544300dc3 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2945,7 +2945,7 @@ struct migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf sb;\n+\tstruct strbuf sb, name, mail;\n };\n \n static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n@@ -2984,7 +2984,7 @@ struct reflog_migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf *sb;\n+\tstruct strbuf *sb, *name, *mail;\n };\n \n static int migrate_one_reflog_entry(struct object_id *old_oid,\n@@ -2994,13 +2994,22 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \t\t\t\t    const char *msg, void *cb_data)\n {\n \tstruct reflog_migration_data *data = cb_data;\n+\tstruct ident_split ident;\n \tconst char *date;\n \tint ret;\n \n+\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n+\t\treturn -1;\n+\n+\tstrbuf_reset(data->name);\n+\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n+\tstrbuf_reset(data->mail);\n+\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n+\n \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n \tstrbuf_reset(data->sb);\n \t/* committer contains name and email */\n-\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n+\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n@@ -3017,6 +3026,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n \t\t.transaction = migration_data->transaction,\n \t\t.errbuf = migration_data->errbuf,\n \t\t.sb = &migration_data->sb,\n+\t\t.name = &migration_data->name,\n+\t\t.mail = &migration_data->mail,\n \t};\n \n \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n@@ -3115,6 +3126,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tstruct strbuf new_gitdir = STRBUF_INIT;\n \tstruct migration_data data = {\n \t\t.sb = STRBUF_INIT,\n+\t\t.name = STRBUF_INIT,\n+\t\t.mail = STRBUF_INIT,\n \t};\n \tint did_migrate_refs = 0;\n \tint ret;\n@@ -3290,6 +3303,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tref_transaction_free(transaction);\n \tstrbuf_release(&new_gitdir);\n \tstrbuf_release(&data.sb);\n+\tstrbuf_release(&data.name);\n+\tstrbuf_release(&data.mail);\n \treturn ret;\n }\n \n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522430","messageId":"20250722-pks-reflog-append-v1-7-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:56Z","receivedAt":"2025-07-22T11:21:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\nobject ID set. If so, the value of that field is used to verify whteher\nthe current state of the reference matches this expected state. It is\nthus an important part of mitigating races with a concurrent process\nthat updates the same set of references.\n\nWhen writing reflogs though we explicitly unset that flag. This is a\nsensible thing to do: the old state of reflog entry updates may not\nnecessarily match the current on-disk state of its accompanying ref, but\nit's only intended to signal what old object ID we want to write into\nthe new reflog entry. For example when migrating refs we end up writing\nmany reflog entries for a single reference, and most likely those reflog\nentries will have many different old object IDs.\n\nBut unsetting this flag also removes a useful signal, namely that the\ncaller _did_ provide an old object ID for a given reflog entry. This\nsignal is useful to determine whether we have to resolve the refname\nmanually to figure out the current state, or whether we should just go\nwith what the caller has provided.\n\nThis actually causes real issues when migrating reflogs, as we don't\nknow to actually use the caller-provided old object ID when writing\nthose entries. Instead, reflog entries simply end up with the all-zero\nobject ID.\n\nStop unsetting the flag so that we can use it as this described signal,\nwhich we'll do in a subsequent commit. Skip checking the old object ID\nfor log-only updates so that we don't expect it to match the current\non-disk state.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  7 +------\n refs/files-backend.c    |  9 +++++----\n refs/reftable-backend.c | 12 +++---------\n 3 files changed, 9 insertions(+), 19 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 64544300dc3..c78d5be6e20 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1384,11 +1384,6 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \tupdate = ref_transaction_add_update(transaction, refname, flags,\n \t\t\t\t\t    new_oid, old_oid, NULL, NULL,\n \t\t\t\t\t    committer_info, msg);\n-\t/*\n-\t * While we do set the old_oid value, we unset the flag to skip\n-\t * old_oid verification which only makes sense for refs.\n-\t */\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \tupdate->index = index;\n \n \t/*\n@@ -3310,7 +3305,7 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \n int ref_update_expects_existing_old_ref(struct ref_update *update)\n {\n-\treturn (update->flags & REF_HAVE_OLD) &&\n+\treturn (update->flags & (REF_HAVE_OLD | REF_LOG_ONLY)) == REF_HAVE_OLD &&\n \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n }\n \ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 89ae4517a97..d519bb615fa 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2493,7 +2493,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n \t * done when new_update is processed.\n \t */\n \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \n \treturn 0;\n }\n@@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n \t\t\t\t\t\tstruct object_id *oid,\n \t\t\t\t\t\tstruct strbuf *err)\n {\n-\tif (!(update->flags & REF_HAVE_OLD) ||\n-\t\t   oideq(oid, &update->old_oid))\n+\tif (update->flags & REF_LOG_ONLY ||\n+\t    !(update->flags & REF_HAVE_OLD) ||\n+\t    oideq(oid, &update->old_oid))\n \t\treturn 0;\n \n \tif (is_null_oid(&update->old_oid)) {\n@@ -3061,7 +3061,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n \n-\t\tif ((update->flags & REF_HAVE_OLD) &&\n+\t\tif (!(update->flags & REF_LOG_ONLY) &&\n+\t\t    (update->flags & REF_HAVE_OLD) &&\n \t\t    !is_null_oid(&update->old_oid))\n \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4c3817f4ec1..44af58ac50b 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1180,8 +1180,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret > 0) {\n \t\t/* The reference does not exist, but we expected it to. */\n \t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\n-\n \t\t\t\t   \"unable to resolve reference '%s'\"),\n \t\t\t    ref_update_original_update_refname(u), u->refname);\n \t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n@@ -1235,13 +1233,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \n \t\t\tnew_update->parent_update = u;\n \n-\t\t\t/*\n-\t\t\t * Change the symbolic ref update to log only. Also, it\n-\t\t\t * doesn't need to check its old OID value, as that will be\n-\t\t\t * done when new_update is processed.\n-\t\t\t */\n+\t\t\t/* Change the symbolic ref update to log only. */\n \t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\t\t\tu->flags &= ~REF_HAVE_OLD;\n \t\t}\n \t}\n \n@@ -1265,7 +1258,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\tret = ref_update_check_old_target(referent->buf, u, err);\n \t\tif (ret)\n \t\t\treturn ret;\n-\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n+\t} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&\n+\t\t   !oideq(&current_oid, &u->old_oid)) {\n \t\tif (is_null_oid(&u->old_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference already exists\"),\n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522431","messageId":"20250722-pks-reflog-append-v1-8-183e5949de16@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH 8/8] refs: fix invalid old object IDs when migrating reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-22T11:20:57Z","receivedAt":"2025-07-22T11:21:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we end\nup with invalid old object IDs for the migrated entries: instead of\nwriting the old object ID of the to-be-migrated entry, we end up with\nthe all-zeroes object ID.\n\nThe root cause of this issue is that we don't know to use the old object\nID provided by the caller. Instead, we manually resolve the old object\nID by resolving the current value of its matching reference. But as that\nreference does not yet exist in the target ref storage we always end up\nresolving it to all-zeroes.\n\nThis issue got unnoticed as there is no user-facing command that would\neven show the old object ID. While `git log -g` knows to show the new\nobject ID, we don't have any formatting directive to show the old object\nID.\n\nFix the bug by introducing a new flag `REF_LOG_USE_PROVIDED_OIDS`. If\nset, backends are instructed to use the old and new object IDs provided\nby the caller, without doing any manual resolving. Set this flag in\n`ref_transaction_update_reflog()`.\n\nAmend our tests in t1460-refs-migrate to use our test tool to read\nreflog entries. This test tool prints out both old and new object ID of\neach reflog entry, which fixes the test gap. Furthermore it also prints\nthe full identity used to write the reflog, which provides test coverage\nfor the previous commit in this patch series that fixed the identity for\nmigrated reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  3 ++-\n refs.h                  |  9 ++++++++-\n refs/files-backend.c    | 16 +++++++++++++++-\n refs/reftable-backend.c | 14 ++++++++++++++\n t/t1460-refs-migrate.sh | 22 +++++++++++++++-------\n 5 files changed, 54 insertions(+), 10 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex c78d5be6e20..3d07ead92cb 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1376,7 +1376,8 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \n \tassert(err);\n \n-\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF |\n+\t\tREF_LOG_USE_PROVIDED_OIDS;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\ndiff --git a/refs.h b/refs.h\nindex 0faf3bc0422..8e5416178ae 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -759,13 +759,20 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n  */\n #define REF_SKIP_CREATE_REFLOG (1 << 12)\n \n+/*\n+ * When writing a REF_LOG_ONLY record, use the old and new object IDs provided\n+ * in the update instead of resolving the old object ID. The caller must also\n+ * set both REF_HAVE_OLD and REF_HAVE_NEW.\n+ */\n+#define REF_LOG_USE_PROVIDED_OIDS (1 << 13)\n+\n /*\n  * Bitmask of all of the flags that are allowed to be passed in to\n  * ref_transaction_update() and friends:\n  */\n #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \\\n \t(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \\\n-\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)\n+\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | REF_LOG_USE_PROVIDED_OIDS)\n \n /*\n  * Add a reference update to transaction. `new_oid` is the value that\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex d519bb615fa..3ebe0323d4e 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2976,6 +2976,20 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t\t\t  struct ref_lock *lock,\n \t\t\t\t  struct strbuf *err)\n {\n+\tstruct object_id *old_oid = &lock->old_oid;\n+\n+\tif (update->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(update->flags & REF_HAVE_OLD) ||\n+\t\t    !(update->flags & REF_HAVE_NEW) ||\n+\t\t    !(update->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), update->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\told_oid = &update->old_oid;\n+\t}\n+\n \tif (update->new_target) {\n \t\t/*\n \t\t * We want to get the resolved OID for the target, to ensure\n@@ -2993,7 +3007,7 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t}\n \t}\n \n-\tif (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,\n+\tif (files_log_ref_write(refs, lock->ref_name, old_oid,\n \t\t\t\t&update->new_oid, update->committer_info,\n \t\t\t\tupdate->msg, update->flags, err)) {\n \t\tchar *old_msg = strbuf_detach(err, NULL);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 44af58ac50b..99fafd75ebe 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1096,6 +1096,20 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret)\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\tif (u->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(u->flags & REF_HAVE_OLD) ||\n+\t\t    !(u->flags & REF_HAVE_NEW) ||\n+\t\t    !(u->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), u->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\tif (queue_transaction_update(refs, tx_data, u, &u->old_oid, err))\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\treturn 0;\n+\t}\n+\n \t/* Verify that the new object ID is valid. */\n \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n \t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 2ab97e1b7df..8191b08a79c 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -7,6 +7,17 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+print_all_reflog_entries () {\n+\trepo=$1 &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n+\tcat reflogs | while read reflog\n+\tdo\n+\t\techo \"REFLOG: $reflog\" &&\n+\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n+\t\treturn 1\n+\tdone\n+}\n+\n # Migrate the provided repository from one format to the other and\n # verify that the references and logs are migrated over correctly.\n # Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]\n@@ -28,8 +39,7 @@ test_migration () {\n \t\t--format='%(refname) %(objectname) %(symref)' >expect &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t   git -C \"$repo\" reflog --all >expect_logs &&\n-\t   git -C \"$repo\" reflog list >expect_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >expect_logs\n \tfi &&\n \n \tgit -C \"$repo\" refs migrate --ref-format=\"$format\" \"$@\" &&\n@@ -39,10 +49,8 @@ test_migration () {\n \ttest_cmp expect actual &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t\tgit -C \"$repo\" reflog --all >actual_logs &&\n-\t\tgit -C \"$repo\" reflog list >actual_log_list &&\n-\t\ttest_cmp expect_logs actual_logs &&\n-\t\ttest_cmp expect_log_list actual_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >actual_logs &&\n+\t\ttest_cmp expect_logs actual_logs\n \tfi &&\n \n \tgit -C \"$repo\" rev-parse --show-ref-format >actual &&\n@@ -273,7 +281,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' '\n \ttest_commit -C repo second &&\n \tprintf \"update refs/heads/ref-%d HEAD\\n\" $(test_seq 3000) >stdin &&\n \tgit -C repo update-ref --stdin <stdin &&\n-\ttest_migration repo reftable\n+\ttest_migration repo reftable true\n '\n \n test_expect_success 'migrating from files format deletes backend files' '\n\n-- \n2.50.1.465.gcb3da1c9e6.dirty\n\n"},{"id":"522503","messageId":"xmqqms8vrif2.fsf@gitster.g","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-1-183e5949de16@pks.im","subject":"Re: [PATCH 1/8] Documentation/git-reflog: convert to use synopsis type","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-22T22:04:49Z","receivedAt":"2025-07-22T22:04:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\n> have introduced a new synopsis type that simplifies the rules for\n> typesetting a command's synopsis. Convert the git-reflog(1)\n> documentation to use it.\n\nGood.\n\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-reflog.adoc | 14 +++++++-------\n>  1 file changed, 7 insertions(+), 7 deletions(-)\n>\n> diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\n> index 412f06b8fec..707a9b39edb 100644\n> --- a/Documentation/git-reflog.adoc\n> +++ b/Documentation/git-reflog.adoc\n> @@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n>  \n>  SYNOPSIS\n>  --------\n> -[verse]\n> -'git reflog' [show] [<log-options>] [<ref>]\n> -'git reflog list'\n> -'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n> +[synopsis]\n> +git reflog [show] [<log-options>] [<ref>]\n> +git reflog list\n> +git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n>  \t[--rewrite] [--updateref] [--stale-fix]\n>  \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n> -'git reflog delete' [--rewrite] [--updateref]\n> +git reflog delete [--rewrite] [--updateref]\n>  \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n> -'git reflog drop' [--all [--single-worktree] | <refs>...]\n> -'git reflog exists' <ref>\n> +git reflog drop [--all [--single-worktree] | <refs>...]\n> +git reflog exists <ref>\n>  \n>  DESCRIPTION\n>  -----------\n"},{"id":"522505","messageId":"xmqqfrenri7m.fsf@gitster.g","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-8-183e5949de16@pks.im","subject":"Re: [PATCH 8/8] refs: fix invalid old object IDs when migrating reflogs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-22T22:09:17Z","receivedAt":"2025-07-22T22:09:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +print_all_reflog_entries () {\n> +\trepo=$1 &&\n> +\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n> +\tcat reflogs | while read reflog\n> +\tdo\n> +\t\techo \"REFLOG: $reflog\" &&\n> +\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n> +\t\treturn 1\n> +\tdone\n\nLet's not cat a single file into a pipe.  What is on the downstream\nside of such a pipe is always prepared to read from its standard\ninput.  I.e.\n\n\ttest-tool ... >reflogs &&\n\twhile read reflog\n\tdo\n\t\t...\n\tdone <reflogs\n\n"},{"id":"522517","messageId":"aIBfVgOqZNrcpmh_@pks.im","threadId":"63818","inReplyTo":"xmqqfrenri7m.fsf@gitster.g","subject":"Re: [PATCH 8/8] refs: fix invalid old object IDs when migrating reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-23T04:04:38Z","receivedAt":"2025-07-23T04:04:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Jul 22, 2025 at 03:09:17PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > +print_all_reflog_entries () {\n> > +\trepo=$1 &&\n> > +\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n> > +\tcat reflogs | while read reflog\n> > +\tdo\n> > +\t\techo \"REFLOG: $reflog\" &&\n> > +\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n> > +\t\treturn 1\n> > +\tdone\n> \n> Let's not cat a single file into a pipe.  What is on the downstream\n> side of such a pipe is always prepared to read from its standard\n> input.  I.e.\n> \n> \ttest-tool ... >reflogs &&\n> \twhile read reflog\n> \tdo\n> \t\t...\n> \tdone <reflogs\n\nAh, makes sense. Will queue the change locally and send it out with the\nnext version. Thanks!\n\nPatrick\n"},{"id":"522597","messageId":"32gvkjmjz6otqwpaxafmz5xuznowhn4iwcdyn52qhbvzohyih4@oeimg3xvqj5u","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-2-183e5949de16@pks.im","subject":"Re: [PATCH 2/8] builtin/reflog: improve grouping of subcommands","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-07-23T18:14:19Z","receivedAt":"2025-07-23T18:20:10Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> The way subcommands of git-reflog(1) are layed out does not make any\n\ns/layed/laid/\n\n> immediate sense. Reorder them such that read-only subcommands precede\n> writing commands for a bit more structure.\n> \n> Furthermore, move the \"expire\" subcommand last. This prepares for a\n> subsequent change where we are about to introduce a new \"write\" command\n> to append reflog entries. Like this, the writing subcommands are ordered\n> such that those affecting a single reflog come before those spanning\n> across all reflogs.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-reflog.adoc |  8 ++++----\n>  builtin/reflog.c              | 38 +++++++++++++++++++-------------------\n>  2 files changed, 23 insertions(+), 23 deletions(-)\n> \n> diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\n> index 707a9b39edb..6ae13e772b8 100644\n> --- a/Documentation/git-reflog.adoc\n> +++ b/Documentation/git-reflog.adoc\n> @@ -11,13 +11,13 @@ SYNOPSIS\n>  [synopsis]\n>  git reflog [show] [<log-options>] [<ref>]\n>  git reflog list\n> -git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n> -\t[--rewrite] [--updateref] [--stale-fix]\n> -\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n> +git reflog exists <ref>\n>  git reflog delete [--rewrite] [--updateref]\n>  \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n>  git reflog drop [--all [--single-worktree] | <refs>...]\n> -git reflog exists <ref>\n> +git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n> +\t[--rewrite] [--updateref] [--stale-fix]\n> +\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n>  \n>  DESCRIPTION\n>  -----------\n> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> index 3acaf3e32c2..b00b3f9edc9 100644\n> --- a/builtin/reflog.c\n> +++ b/builtin/reflog.c\n> @@ -17,21 +17,21 @@\n>  #define BUILTIN_REFLOG_LIST_USAGE \\\n>  \tN_(\"git reflog list\")\n>  \n> -#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n> -\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n> -\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n> -\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n> +#define BUILTIN_REFLOG_EXISTS_USAGE \\\n> +\tN_(\"git reflog exists <ref>\")\n>  \n>  #define BUILTIN_REFLOG_DELETE_USAGE \\\n>  \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n>  \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n>  \n> -#define BUILTIN_REFLOG_EXISTS_USAGE \\\n> -\tN_(\"git reflog exists <ref>\")\n> -\n>  #define BUILTIN_REFLOG_DROP_USAGE \\\n>  \tN_(\"git reflog drop [--all [--single-worktree] | <refs>...]\")\n>  \n> +#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n> +\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n> +\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n> +\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n> +\n>  static const char *const reflog_show_usage[] = {\n>  \tBUILTIN_REFLOG_SHOW_USAGE,\n>  \tNULL,\n> @@ -42,9 +42,9 @@ static const char *const reflog_list_usage[] = {\n>  \tNULL,\n>  };\n>  \n> -static const char *const reflog_expire_usage[] = {\n> -\tBUILTIN_REFLOG_EXPIRE_USAGE,\n> -\tNULL\n> +static const char *const reflog_exists_usage[] = {\n> +\tBUILTIN_REFLOG_EXISTS_USAGE,\n> +\tNULL,\n>  };\n>  \n>  static const char *const reflog_delete_usage[] = {\n> @@ -52,23 +52,23 @@ static const char *const reflog_delete_usage[] = {\n>  \tNULL\n>  };\n>  \n> -static const char *const reflog_exists_usage[] = {\n> -\tBUILTIN_REFLOG_EXISTS_USAGE,\n> -\tNULL,\n> -};\n> -\n>  static const char *const reflog_drop_usage[] = {\n>  \tBUILTIN_REFLOG_DROP_USAGE,\n>  \tNULL,\n>  };\n>  \n> +static const char *const reflog_expire_usage[] = {\n> +\tBUILTIN_REFLOG_EXPIRE_USAGE,\n> +\tNULL\n> +};\n> +\n>  static const char *const reflog_usage[] = {\n>  \tBUILTIN_REFLOG_SHOW_USAGE,\n>  \tBUILTIN_REFLOG_LIST_USAGE,\n> -\tBUILTIN_REFLOG_EXPIRE_USAGE,\n> +\tBUILTIN_REFLOG_EXISTS_USAGE,\n>  \tBUILTIN_REFLOG_DELETE_USAGE,\n>  \tBUILTIN_REFLOG_DROP_USAGE,\n> -\tBUILTIN_REFLOG_EXISTS_USAGE,\n> +\tBUILTIN_REFLOG_EXPIRE_USAGE,\n>  \tNULL\n>  };\n>  \n> @@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n>  \tstruct option options[] = {\n>  \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n>  \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n> -\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n> -\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n>  \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n> +\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n>  \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n> +\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n>  \t\tOPT_END()\n>  \t};\n\nStructing the subcommands order in such a manner seems sensible, but I'm\nnot sure the pattern will be recognized by others that may add\nsubcommands in the future. Maybe we could leave a comment that mentions\nthe order?\n\n-Justin\n"},{"id":"522599","messageId":"2xwgx55dxhltq3ohktarsfe5wsw4f4er2h5hylz5qov3esntt6@fexfi4mioaqi","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-3-183e5949de16@pks.im","subject":"Re: [PATCH 3/8] refs: export `ref_transaction_update_reflog()`","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-07-23T18:25:48Z","receivedAt":"2025-07-23T18:31:39Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> In a subsequent commit we'll add another user that wants to write reflog\n> entries. This requires them to call `ref_transaction_update_reflog()`,\n> but that functino is local to \"refs.c\".\n\ns/functino/function/\n\n> Export the function to prepare for the change. While at it, drop the\n> `flags` field, as all callers are for now expected to use the same flags\n> anyway.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs.c | 29 +++++++++++------------------\n>  refs.h | 15 +++++++++++++++\n>  2 files changed, 26 insertions(+), 18 deletions(-)\n> \n> diff --git a/refs.c b/refs.c\n> index 73913b6627b..188989e4113 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -1362,27 +1362,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>  \treturn 0;\n>  }\n>  \n> -/*\n> - * Similar to`ref_transaction_update`, but this function is only for adding\n> - * a reflog update. Supports providing custom committer information. The index\n> - * field can be utiltized to order updates as desired. When not used, the\n> - * updates default to being ordered by refname.\n> - */\n> -static int ref_transaction_update_reflog(struct ref_transaction *transaction,\n> -\t\t\t\t\t const char *refname,\n> -\t\t\t\t\t const struct object_id *new_oid,\n> -\t\t\t\t\t const struct object_id *old_oid,\n> -\t\t\t\t\t const char *committer_info,\n> -\t\t\t\t\t unsigned int flags,\n> -\t\t\t\t\t const char *msg,\n> -\t\t\t\t\t uint64_t index,\n> -\t\t\t\t\t struct strbuf *err)\n> +int ref_transaction_update_reflog(struct ref_transaction *transaction,\n> +\t\t\t\t  const char *refname,\n> +\t\t\t\t  const struct object_id *new_oid,\n> +\t\t\t\t  const struct object_id *old_oid,\n> +\t\t\t\t  const char *committer_info,\n> +\t\t\t\t  const char *msg,\n> +\t\t\t\t  uint64_t index,\n> +\t\t\t\t  struct strbuf *err)\n>  {\n>  \tstruct ref_update *update;\n> +\tunsigned int flags;\n>  \n>  \tassert(err);\n>  \n> -\tflags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n> +\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n>  \n>  \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n>  \t\treturn -1;\n> @@ -3010,8 +3004,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n>  \n>  \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n>  \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n> -\t\t\t\t\t    REF_HAVE_NEW | REF_HAVE_OLD, msg,\n> -\t\t\t\t\t    data->index++, data->errbuf);\n> +\t\t\t\t\t    msg, data->index++, data->errbuf);\n\nRight now this is only the single caller for\n`ref_transaction_update_reflog()`. Since it is intented for all callers\nto use the same set of flags, removing the field makes sense.\n\n>  \treturn ret;\n>  }\n>  \n> diff --git a/refs.h b/refs.h\n> index efa182c6a14..0faf3bc0422 100644\n> --- a/refs.h\n> +++ b/refs.h\n> @@ -794,6 +794,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>  \t\t\t   unsigned int flags, const char *msg,\n>  \t\t\t   struct strbuf *err);\n>  \n> +/*\n> + * Similar to`ref_transaction_update`, but this function is only for adding\n> + * a reflog update. Supports providing custom committer information. The index\n> + * field can be utiltized to order updates as desired. When not used, the\n> + * updates default to being ordered by refname.\n> + */\n> +int ref_transaction_update_reflog(struct ref_transaction *transaction,\n> +\t\t\t\t  const char *refname,\n> +\t\t\t\t  const struct object_id *new_oid,\n> +\t\t\t\t  const struct object_id *old_oid,\n> +\t\t\t\t  const char *committer_info,\n> +\t\t\t\t  const char *msg,\n> +\t\t\t\t  uint64_t index,\n> +\t\t\t\t  struct strbuf *err);\n> +\n>  /*\n>   * Add a reference creation to transaction. new_oid is the value that\n>   * the reference should have after the update; it must not be\n> \n> -- \n> 2.50.1.465.gcb3da1c9e6.dirty\n> \n> \n"},{"id":"522600","messageId":"mkqjim46zv4sfhzjjee2tcxwj23esfrupp5byienju5qlwvlht@r2gxnybioukw","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-4-183e5949de16@pks.im","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-07-23T19:00:10Z","receivedAt":"2025-07-23T19:06:01Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> While we provide a couple of subcommands in git-reflog(1) to remove\n> reflog entries, we don't provide any to write new entries. Obviously\n> this is not an operation that really would be needed for many use cases\n> out there, or otherwise people would have complained that such a command\n> does not exist yet. But the introduction of the \"reftable\" backend\n> changes the picture a bit, as it is now basically impossible to manually\n> append a reflog entry if one wanted to do so due to the binary format.\n> \n> Plug this gap by introducing a simple \"write\" subcommand. For now, all\n> this command does is to append a single new reflog entry with the given\n> object IDs and message to the reflog. More specifically, it is not yet\n> possible to:\n> \n>   - Write multiple reflog entries at once.\n> \n>   - Insert reflog entries at arbitrary indices.\n> \n>   - Specify the date of the reflog entry.\n> \n>   - Insert reflog entries that refer to nonexistent objects.\n> \n> If required, those features can be added at a future point in time. For\n> now though, the new command aims to fulfill the most basic use cases\n> while being as strict as possible when it comes to verifying parameters.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-reflog.adoc |  1 +\n>  builtin/reflog.c              | 65 ++++++++++++++++++++++++++++++++++\n>  t/meson.build                 |  1 +\n>  t/t1421-reflog-write.sh       | 81 +++++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 148 insertions(+)\n> \n> diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\n> index 6ae13e772b8..798dbc0a00a 100644\n> --- a/Documentation/git-reflog.adoc\n> +++ b/Documentation/git-reflog.adoc\n> @@ -12,6 +12,7 @@ SYNOPSIS\n>  git reflog [show] [<log-options>] [<ref>]\n>  git reflog list\n>  git reflog exists <ref>\n> +git reflog write <ref> <old-oid> <new-oid> <message>\n\nThe other subcommands each have an entry in the description. Do we want\nto also add something for the \"write\" subcommand?\n\nAlso, if we want to be consistent, I noticed the order of the\nsubcommands listed in the description was not changed either. \n\n>  git reflog delete [--rewrite] [--updateref]\n>  \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n>  git reflog drop [--all [--single-worktree] | <refs>...]\n> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> index b00b3f9edc9..d0374295620 100644\n> --- a/builtin/reflog.c\n> +++ b/builtin/reflog.c\n> @@ -3,6 +3,8 @@\n>  #include \"builtin.h\"\n>  #include \"config.h\"\n>  #include \"gettext.h\"\n> +#include \"hex.h\"\n> +#include \"odb.h\"\n>  #include \"revision.h\"\n>  #include \"reachable.h\"\n>  #include \"wildmatch.h\"\n> @@ -20,6 +22,9 @@\n>  #define BUILTIN_REFLOG_EXISTS_USAGE \\\n>  \tN_(\"git reflog exists <ref>\")\n>  \n> +#define BUILTIN_REFLOG_WRITE_USAGE \\\n> +\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n> +\n>  #define BUILTIN_REFLOG_DELETE_USAGE \\\n>  \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n>  \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n> @@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n>  \tNULL,\n>  };\n>  \n> +static const char *const reflog_write_usage[] = {\n> +\tBUILTIN_REFLOG_WRITE_USAGE,\n> +\tNULL,\n> +};\n> +\n>  static const char *const reflog_delete_usage[] = {\n>  \tBUILTIN_REFLOG_DELETE_USAGE,\n>  \tNULL\n> @@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n>  \tBUILTIN_REFLOG_SHOW_USAGE,\n>  \tBUILTIN_REFLOG_LIST_USAGE,\n>  \tBUILTIN_REFLOG_EXISTS_USAGE,\n> +\tBUILTIN_REFLOG_WRITE_USAGE,\n>  \tBUILTIN_REFLOG_DELETE_USAGE,\n>  \tBUILTIN_REFLOG_DROP_USAGE,\n>  \tBUILTIN_REFLOG_EXPIRE_USAGE,\n> @@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n>  \treturn ret;\n>  }\n>  \n> +static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n> +\t\t\t    struct repository *repo)\n> +{\n> +\tconst struct option options[] = {\n> +\t\tOPT_END()\n> +\t};\n> +\tstruct object_id old_oid, new_oid;\n> +\tstruct strbuf err = STRBUF_INIT;\n> +\tstruct ref_transaction *tx;\n> +\tconst char *ref, *message;\n> +\tint ret;\n> +\n> +\targc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);\n> +\tif (argc != 4)\n> +\t\tusage_with_options(reflog_write_usage, options);\n> +\n> +\tref = argv[0];\n> +\tif (check_refname_format(ref, REFNAME_ALLOW_ONELEVEL))\n> +\t\tdie(_(\"invalid reference name: %s\"), ref);\n> +\n> +\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n> +\tif (ret)\n> +\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n> +\tif (!is_null_oid(&old_oid) && !odb_has_object(repo->objects, &old_oid, 0))\n> +\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n> +\n> +\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n> +\tif (ret)\n> +\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n> +\tif (!is_null_oid(&new_oid) && !odb_has_object(repo->objects, &new_oid, 0))\n> +\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n\nOk so we validate the reference name and the old/new obects names to\nmake sure they are sane.\n\n> +\n> +\tmessage = argv[3];\n> +\n> +\ttx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n> +\tif (!tx)\n> +\t\tdie(_(\"cannot start transaction: %s\"), err.buf);\n> +\n> +\tret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,\n> +\t\t\t\t\t    git_committer_info(0),\n> +\t\t\t\t\t    message, 0, &err);\n> +\tif (ret)\n> +\t\tdie(_(\"cannot queue reflog update: %s\"), err.buf);\n> +\n> +\tret = ref_transaction_commit(tx, &err);\n> +\tif (ret)\n> +\t\tdie(_(\"cannot commit reflog update: %s\"), err.buf);\n\nAnd here we write the reflog entry. Looks good\n\n> +\n> +\tref_transaction_free(tx);\n> +\tstrbuf_release(&err);\n> +\treturn 0;\n> +}\n> +\n>  /*\n>   * main \"reflog\"\n>   */\n> @@ -405,6 +469,7 @@ int cmd_reflog(int argc,\n>  \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n>  \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n>  \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n> +\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_reflog_write),\n>  \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n>  \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n>  \t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n> diff --git a/t/meson.build b/t/meson.build\n> index 1af289425d4..d68f5e24dbe 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -219,6 +219,7 @@ integration_tests = [\n>    't1418-reflog-exists.sh',\n>    't1419-exclude-refs.sh',\n>    't1420-lost-found.sh',\n> +  't1421-reflog-write.sh',\n>    't1430-bad-ref-name.sh',\n>    't1450-fsck.sh',\n>    't1451-fsck-buffer.sh',\n> diff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\n> new file mode 100755\n> index 00000000000..e284f42178f\n> --- /dev/null\n> +++ b/t/t1421-reflog-write.sh\n> @@ -0,0 +1,81 @@\n> +#!/bin/sh\n> +\n> +test_description='Manually write reflog entries'\n> +\n> +. ./test-lib.sh\n> +\n> +SIGNATURE=\"C O Mitter <committer@example.com> 1112911993 -0700\"\n> +\n> +test_reflog_matches () {\n> +\trepo=\"$1\" &&\n> +\trefname=\"$2\" &&\n> +\tcat >actual &&\n> +\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$refname\" >expected &&\n> +\ttest_cmp expected actual\n> +}\n> +\n> +test_expect_success 'invalid number of arguments' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\tfor args in \"\" \"1\" \"1 2\" \"1 2 3\" \"1 2 3 4 5\"\n> +\t\tdo\n> +\t\t\ttest_must_fail git reflog write $args 2>err &&\n> +\t\t\ttest_grep \"usage: git reflog write\" err || return 1\n> +\t\tdone\n> +\t)\n> +'\n> +\n> +test_expect_success 'invalid refname' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_must_fail git reflog write \"refs/heads/ invalid\" $ZERO_OID $ZERO_OID first 2>err &&\n> +\t\ttest_grep \"invalid reference name: \" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'nonexistent old object ID' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID first 2>err &&\n> +\t\ttest_grep \"old object .* does not exist\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'nonexistent new object ID' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) first 2>err &&\n> +\t\ttest_grep \"new object .* does not exist\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'simple writes' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit initial &&\n> +\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n> +\n> +\t\tgit reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&\n> +\t\ttest_reflog_matches . refs/heads/something <<-EOF &&\n> +\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n> +\t\tEOF\n> +\n> +\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n> +\t\ttest_reflog_matches . refs/heads/something <<-EOF\n> +\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n> +\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n> +\t\tEOF\n> +\t)\n> +'\n> +\n> +test_done\n> \n> -- \n> 2.50.1.465.gcb3da1c9e6.dirty\n> \n> \n"},{"id":"522602","messageId":"tg72v5vgu56b6akawy7sfapi2qtrmy7q3uruhersy4dtzkpvju@wamlylndp3xv","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-6-183e5949de16@pks.im","subject":"Re: [PATCH 6/8] refs: fix identity for migrated reflogs","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-07-23T19:41:27Z","receivedAt":"2025-07-23T19:47:18Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> When migrating reflog entries between different storage formats we must\n> reconstruct the identity of reflog entries. This is done by passing the\n> committer passed to the `migrate_one_reflog_entry()` callback function\n> to `fmt_ident()`.\n> \n> This results in an invalid identity though: `fmt_ident()` expects the\n> caller to provide both name and mail of the author, but we pass the full\n> identity as mail. This leads to an identity like:\n> \n>     pks <Patrick Steinhardt ps@pks.im>\n> \n> Fix the bug by splitting the identity line first. This allows us to\n> extract both the name and mail so that we can pass them to `fmt_ident()`\n> separately.\n\nOk so IIUC, the bug is the result of passing the full committer info to\nthe mail field in `fmt_ident()` and leaving the name field unset. To\nproperly address we need to first deconstruct the committer info into\nseparate name and mail components and pass them separately to\n`fmt_ident()`.\n\n> This commit does not yet add any tests as there is another bug in the\n> reflog migration that will be fixed in a subsequent commit. Once that\n> bug is fixed we'll make the reflog verification in t1450 stricter, and\n> that will catch both this bug here and the other bug.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs.c | 21 ++++++++++++++++++---\n>  1 file changed, 18 insertions(+), 3 deletions(-)\n> \n> diff --git a/refs.c b/refs.c\n> index 188989e4113..64544300dc3 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -2945,7 +2945,7 @@ struct migration_data {\n>  \tstruct ref_store *old_refs;\n>  \tstruct ref_transaction *transaction;\n>  \tstruct strbuf *errbuf;\n> -\tstruct strbuf sb;\n> +\tstruct strbuf sb, name, mail;\n>  };\n>  \n>  static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n> @@ -2984,7 +2984,7 @@ struct reflog_migration_data {\n>  \tstruct ref_store *old_refs;\n>  \tstruct ref_transaction *transaction;\n>  \tstruct strbuf *errbuf;\n> -\tstruct strbuf *sb;\n> +\tstruct strbuf *sb, *name, *mail;\n>  };\n>  \n>  static int migrate_one_reflog_entry(struct object_id *old_oid,\n> @@ -2994,13 +2994,22 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n>  \t\t\t\t    const char *msg, void *cb_data)\n>  {\n>  \tstruct reflog_migration_data *data = cb_data;\n> +\tstruct ident_split ident;\n>  \tconst char *date;\n>  \tint ret;\n>  \n> +\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n> +\t\treturn -1;\n\nOk now we first deconstruct the committer info.\n\n> +\n> +\tstrbuf_reset(data->name);\n> +\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n> +\tstrbuf_reset(data->mail);\n> +\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n\nThe name and mail components get stored separately.\n\n> +\n>  \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n>  \tstrbuf_reset(data->sb);\n>  \t/* committer contains name and email */\n> -\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n> +\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n\n`fmt_ident()` now receives the expected information. Looks good\n\n>  \n>  \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n>  \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n> @@ -3017,6 +3026,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n>  \t\t.transaction = migration_data->transaction,\n>  \t\t.errbuf = migration_data->errbuf,\n>  \t\t.sb = &migration_data->sb,\n> +\t\t.name = &migration_data->name,\n> +\t\t.mail = &migration_data->mail,\n\nI was a bit confused at first why we cared to assign the name and mail\nfields here as it didn't look like we actually use them, but it looks\nlike we do this to release the the underlying strbuf as we don't free it\nfrom `reflog_migration_data`.\n\n>  \t};\n>  \n>  \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n> @@ -3115,6 +3126,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n>  \tstruct strbuf new_gitdir = STRBUF_INIT;\n>  \tstruct migration_data data = {\n>  \t\t.sb = STRBUF_INIT,\n> +\t\t.name = STRBUF_INIT,\n> +\t\t.mail = STRBUF_INIT,\n>  \t};\n>  \tint did_migrate_refs = 0;\n>  \tint ret;\n> @@ -3290,6 +3303,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n>  \tref_transaction_free(transaction);\n>  \tstrbuf_release(&new_gitdir);\n>  \tstrbuf_release(&data.sb);\n> +\tstrbuf_release(&data.name);\n> +\tstrbuf_release(&data.mail);\n>  \treturn ret;\n>  }\n>  \n> \n> -- \n> 2.50.1.465.gcb3da1c9e6.dirty\n> \n> \n"},{"id":"522604","messageId":"j52ugdtik25i6dqgqafchvy5an3o7qbdfeavtqg6bcr2ouxvyv@qwtqzf7xk4hi","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-7-183e5949de16@pks.im","subject":"Re: [PATCH 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2025-07-23T20:31:01Z","receivedAt":"2025-07-23T20:36:52Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\n> object ID set. If so, the value of that field is used to verify whteher\n\ns/whteher/whether/\n\n> the current state of the reference matches this expected state. It is\n> thus an important part of mitigating races with a concurrent process\n> that updates the same set of references.\n> \n> When writing reflogs though we explicitly unset that flag. This is a\n> sensible thing to do: the old state of reflog entry updates may not\n> necessarily match the current on-disk state of its accompanying ref, but\n> it's only intended to signal what old object ID we want to write into\n> the new reflog entry. For example when migrating refs we end up writing\n> many reflog entries for a single reference, and most likely those reflog\n> entries will have many different old object IDs.\n> \n> But unsetting this flag also removes a useful signal, namely that the\n> caller _did_ provide an old object ID for a given reflog entry. This\n> signal is useful to determine whether we have to resolve the refname\n> manually to figure out the current state, or whether we should just go\n> with what the caller has provided.\n> \n> This actually causes real issues when migrating reflogs, as we don't\n> know to actually use the caller-provided old object ID when writing\n> those entries. Instead, reflog entries simply end up with the all-zero\n> object ID.\n\nOk, if I'm understanding this correctly, the `REF_HAVE_OLD` flag is also\nrequired to actually record a provided old OID in the reflog entry. If it\nis not set, a NUL OID is recorded instead.\n\n> Stop unsetting the flag so that we can use it as this described signal,\n> which we'll do in a subsequent commit. Skip checking the old object ID\n> for log-only updates so that we don't expect it to match the current\n> on-disk state.\n\nJust to clarify, when migrating reflogs, are these operations always\nmarked with `REF_LOG_ONLY`? The comment for that flag states:\n\n  Used as a flag in ref_update::flags when we want to log a ref\n  update but not actually perform it.  This is used when a symbolic ref\n  update is split up.                                           \n\nI might be misunderstanding this though.\n\n-Justin\n"},{"id":"522632","messageId":"aIHj4NWr3M5lXLtD@pks.im","threadId":"63818","inReplyTo":"32gvkjmjz6otqwpaxafmz5xuznowhn4iwcdyn52qhbvzohyih4@oeimg3xvqj5u","subject":"Re: [PATCH 2/8] builtin/reflog: improve grouping of subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-24T07:42:24Z","receivedAt":"2025-07-24T07:42:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 23, 2025 at 01:14:19PM -0500, Justin Tobler wrote:\n> > @@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n> >  \tstruct option options[] = {\n> >  \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n> >  \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n> > -\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n> > -\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n> >  \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n> > +\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n> >  \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n> > +\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n> >  \t\tOPT_END()\n> >  \t};\n> \n> Structing the subcommands order in such a manner seems sensible, but I'm\n> not sure the pattern will be recognized by others that may add\n> subcommands in the future. Maybe we could leave a comment that mentions\n> the order?\n\nHm, dunno. I feel like it's subjective where to add a command anyway, so\nI'm not sure that a comment would be allt hat helpful.\n\nPatrick\n"},{"id":"522633","messageId":"aIHj7M5fzYSpqSKJ@pks.im","threadId":"63818","inReplyTo":"mkqjim46zv4sfhzjjee2tcxwj23esfrupp5byienju5qlwvlht@r2gxnybioukw","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-24T07:42:36Z","receivedAt":"2025-07-24T07:42:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 23, 2025 at 02:00:10PM -0500, Justin Tobler wrote:\n> On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> > diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\n> > index 6ae13e772b8..798dbc0a00a 100644\n> > --- a/Documentation/git-reflog.adoc\n> > +++ b/Documentation/git-reflog.adoc\n> > @@ -12,6 +12,7 @@ SYNOPSIS\n> >  git reflog [show] [<log-options>] [<ref>]\n> >  git reflog list\n> >  git reflog exists <ref>\n> > +git reflog write <ref> <old-oid> <new-oid> <message>\n> \n> The other subcommands each have an entry in the description. Do we want\n> to also add something for the \"write\" subcommand?\n\nYeah, let's.\n\n> Also, if we want to be consistent, I noticed the order of the\n> subcommands listed in the description was not changed either. \n\nTrue, I'll fix that.\n\nPatrick\n"},{"id":"522634","messageId":"aIHj8g08j8p_FY-d@pks.im","threadId":"63818","inReplyTo":"tg72v5vgu56b6akawy7sfapi2qtrmy7q3uruhersy4dtzkpvju@wamlylndp3xv","subject":"Re: [PATCH 6/8] refs: fix identity for migrated reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-24T07:42:42Z","receivedAt":"2025-07-24T07:42:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 23, 2025 at 02:41:27PM -0500, Justin Tobler wrote:\n> On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> > diff --git a/refs.c b/refs.c\n> > index 188989e4113..64544300dc3 100644\n> > --- a/refs.c\n> > +++ b/refs.c\n> > @@ -3017,6 +3026,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n> >  \t\t.transaction = migration_data->transaction,\n> >  \t\t.errbuf = migration_data->errbuf,\n> >  \t\t.sb = &migration_data->sb,\n> > +\t\t.name = &migration_data->name,\n> > +\t\t.mail = &migration_data->mail,\n> \n> I was a bit confused at first why we cared to assign the name and mail\n> fields here as it didn't look like we actually use them, but it looks\n> like we do this to release the the underlying strbuf as we don't free it\n> from `reflog_migration_data`.\n\nThis is an optimization: instead of reallocating a new buffer every time\nwe compute the name and mail we reuse a buffer. But because we're two\ncallbacks deep in the callchain we have to splice these buffers through\nvia multiple callback data structs.\n\nI'll add a note to the commit message.\n\nPatrick\n"},{"id":"522635","messageId":"aIHj-KBiD7hjOCNF@pks.im","threadId":"63818","inReplyTo":"j52ugdtik25i6dqgqafchvy5an3o7qbdfeavtqg6bcr2ouxvyv@qwtqzf7xk4hi","subject":"Re: [PATCH 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-24T07:42:48Z","receivedAt":"2025-07-24T07:42:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 23, 2025 at 03:31:01PM -0500, Justin Tobler wrote:\n> On 25/07/22 01:20PM, Patrick Steinhardt wrote:\n> > The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\n> > object ID set. If so, the value of that field is used to verify whteher\n> \n> s/whteher/whether/\n> \n> > the current state of the reference matches this expected state. It is\n> > thus an important part of mitigating races with a concurrent process\n> > that updates the same set of references.\n> > \n> > When writing reflogs though we explicitly unset that flag. This is a\n> > sensible thing to do: the old state of reflog entry updates may not\n> > necessarily match the current on-disk state of its accompanying ref, but\n> > it's only intended to signal what old object ID we want to write into\n> > the new reflog entry. For example when migrating refs we end up writing\n> > many reflog entries for a single reference, and most likely those reflog\n> > entries will have many different old object IDs.\n> > \n> > But unsetting this flag also removes a useful signal, namely that the\n> > caller _did_ provide an old object ID for a given reflog entry. This\n> > signal is useful to determine whether we have to resolve the refname\n> > manually to figure out the current state, or whether we should just go\n> > with what the caller has provided.\n> > \n> > This actually causes real issues when migrating reflogs, as we don't\n> > know to actually use the caller-provided old object ID when writing\n> > those entries. Instead, reflog entries simply end up with the all-zero\n> > object ID.\n> \n> Ok, if I'm understanding this correctly, the `REF_HAVE_OLD` flag is also\n> required to actually record a provided old OID in the reflog entry. If it\n> is not set, a NUL OID is recorded instead.\n\nThis fix is not sufficient to record the old object ID, so the commit\nmessage is indeed misleading as it comes from a previous iteration of\nthis patch seires.\n\nI want to have this signal so that in the next commit we can assert that\nif the new `REF_LOG_USE_PROVIDED_OIDS` is set, that the caller also sets\nboth `REF_HAVE_OLD` and `REF_HAVE_NEW`. So this commit here isn't really\nrequired anymore, but I think it's the right thing to do anyway, and the\nadditional safety check isn't too bad to have in my opinion.\n\nI'll rewrite the commit message a bit.\n\n> > Stop unsetting the flag so that we can use it as this described signal,\n> > which we'll do in a subsequent commit. Skip checking the old object ID\n> > for log-only updates so that we don't expect it to match the current\n> > on-disk state.\n> \n> Just to clarify, when migrating reflogs, are these operations always\n> marked with `REF_LOG_ONLY`? The comment for that flag states:\n> \n>   Used as a flag in ref_update::flags when we want to log a ref\n>   update but not actually perform it.  This is used when a symbolic ref\n>   update is split up.                                           \n> \n> I might be misunderstanding this though.\n\nThe comment is out of date indeed. When queueing reflog updates we use\n`ref_transaction_update_reflog()`, which sets the following flags:\n\n  - REF_HAVE_OLD and REF_HAVE_NEW to indicate the object IDs.\n\n  - REF_LOG_ONLY to skip writing the reference itself.\n\n  - REF_FORCE_CREATE_REFLOG to disable heuristics whether or not the\n    reflog entry should be written in the first place.\n\n  - REF_NO_DEREF to not write the reflog for the target of a potential\n    symref, but for the symref itself.\n\n  - REF_LOG_USE_PROVIDED_OIDS to actually use the provided object IDs\n    and not try to resolve state.\n\nIt's an awful lot of flags, and I tried to work on a solution where we\ndon't have to introduce the new flag. But the logic here is so intricate\nthat it always caused unintended side effects.\n\nThanks for your review!\n\nPatrick\n"},{"id":"522645","messageId":"CAOLa=ZSv0RkxOMAe-X=rsf6ht-AknFdcbmcQECyxzTe1ZKhmGg@mail.gmail.com","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-3-183e5949de16@pks.im","subject":"Re: [PATCH 3/8] refs: export `ref_transaction_update_reflog()`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-07-24T08:36:01Z","receivedAt":"2025-07-24T08:36:02Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n[snip]\n\n> diff --git a/refs.h b/refs.h\n> index efa182c6a14..0faf3bc0422 100644\n> --- a/refs.h\n> +++ b/refs.h\n> @@ -794,6 +794,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>  \t\t\t   unsigned int flags, const char *msg,\n>  \t\t\t   struct strbuf *err);\n>\n> +/*\n> + * Similar to`ref_transaction_update`, but this function is only for adding\n>\n\nNit: s/to`/to `/\n\n> + * a reflog update. Supports providing custom committer information. The index\n> + * field can be utiltized to order updates as desired. When not used, the\n> + * updates default to being ordered by refname.\n> + */\n> +int ref_transaction_update_reflog(struct ref_transaction *transaction,\n> +\t\t\t\t  const char *refname,\n> +\t\t\t\t  const struct object_id *new_oid,\n> +\t\t\t\t  const struct object_id *old_oid,\n> +\t\t\t\t  const char *committer_info,\n> +\t\t\t\t  const char *msg,\n> +\t\t\t\t  uint64_t index,\n> +\t\t\t\t  struct strbuf *err);\n> +\n>  /*\n>   * Add a reference creation to transaction. new_oid is the value that\n>   * the reference should have after the update; it must not be\n>\n> --\n> 2.50.1.465.gcb3da1c9e6.dirty\n"},{"id":"522648","messageId":"CAOLa=ZRQj+-WShOn8vQL_fnrPy12n3KB1jrarmJ8Mvz8DnQ-Fg@mail.gmail.com","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-6-183e5949de16@pks.im","subject":"Re: [PATCH 6/8] refs: fix identity for migrated reflogs","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-07-24T09:41:49Z","receivedAt":"2025-07-24T09:41:51Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When migrating reflog entries between different storage formats we must\n> reconstruct the identity of reflog entries. This is done by passing the\n> committer passed to the `migrate_one_reflog_entry()` callback function\n> to `fmt_ident()`.\n>\n> This results in an invalid identity though: `fmt_ident()` expects the\n> caller to provide both name and mail of the author, but we pass the full\n> identity as mail. This leads to an identity like:\n>\n>     pks <Patrick Steinhardt ps@pks.im>\n>\n> Fix the bug by splitting the identity line first. This allows us to\n> extract both the name and mail so that we can pass them to `fmt_ident()`\n> separately.\n>\n\nWell explained.\n\n> This commit does not yet add any tests as there is another bug in the\n> reflog migration that will be fixed in a subsequent commit. Once that\n> bug is fixed we'll make the reflog verification in t1450 stricter, and\n> that will catch both this bug here and the other bug.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs.c | 21 ++++++++++++++++++---\n>  1 file changed, 18 insertions(+), 3 deletions(-)\n>\n> diff --git a/refs.c b/refs.c\n> index 188989e4113..64544300dc3 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -2945,7 +2945,7 @@ struct migration_data {\n>  \tstruct ref_store *old_refs;\n>  \tstruct ref_transaction *transaction;\n>  \tstruct strbuf *errbuf;\n> -\tstruct strbuf sb;\n> +\tstruct strbuf sb, name, mail;\n>  };\n>\n>  static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n> @@ -2984,7 +2984,7 @@ struct reflog_migration_data {\n>  \tstruct ref_store *old_refs;\n>  \tstruct ref_transaction *transaction;\n>  \tstruct strbuf *errbuf;\n> -\tstruct strbuf *sb;\n> +\tstruct strbuf *sb, *name, *mail;\n>  };\n>\n>  static int migrate_one_reflog_entry(struct object_id *old_oid,\n> @@ -2994,13 +2994,22 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n>  \t\t\t\t    const char *msg, void *cb_data)\n>  {\n>  \tstruct reflog_migration_data *data = cb_data;\n> +\tstruct ident_split ident;\n>  \tconst char *date;\n>  \tint ret;\n>\n> +\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n> +\t\treturn -1;\n> +\n> +\tstrbuf_reset(data->name);\n> +\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n> +\tstrbuf_reset(data->mail);\n> +\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n> +\n>  \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n>  \tstrbuf_reset(data->sb);\n>  \t/* committer contains name and email */\n\nNit: This comment is now stale\n\n> -\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n> +\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n>\n\nI was a bit stuck on why we use `WANT_BLANK_IDENT`, since we explicitly\n(since we do a split_ident() and that would error out if there is no\nname/email) pass the 'name' and the 'email' here as non-null values. So\nthis seems to be the only option for the enum:\n\nenum want_ident {\n\tWANT_BLANK_IDENT,\n\tWANT_AUTHOR_IDENT,\n\tWANT_COMMITTER_IDENT\n};\n\nSince we don't want to extract author or committer information. However,\nin fmt_ident() we only use the 'want_ident' value, when either 'name' or\n'email' is not set. I found this a bit confusing, perhaps a simple\nchange of name from 'whose_ident' to 'fallback_ident' would be much more\neasier to read and understand. Anyways, this is not for your patch.\n\n>  \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n>  \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n> @@ -3017,6 +3026,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n>  \t\t.transaction = migration_data->transaction,\n>  \t\t.errbuf = migration_data->errbuf,\n>  \t\t.sb = &migration_data->sb,\n> +\t\t.name = &migration_data->name,\n> +\t\t.mail = &migration_data->mail,\n>  \t};\n>\n>  \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n> @@ -3115,6 +3126,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n>  \tstruct strbuf new_gitdir = STRBUF_INIT;\n>  \tstruct migration_data data = {\n>  \t\t.sb = STRBUF_INIT,\n> +\t\t.name = STRBUF_INIT,\n> +\t\t.mail = STRBUF_INIT,\n>  \t};\n>  \tint did_migrate_refs = 0;\n>  \tint ret;\n> @@ -3290,6 +3303,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n>  \tref_transaction_free(transaction);\n>  \tstrbuf_release(&new_gitdir);\n>  \tstrbuf_release(&data.sb);\n> +\tstrbuf_release(&data.name);\n> +\tstrbuf_release(&data.mail);\n>  \treturn ret;\n>  }\n>\n>\n> --\n> 2.50.1.465.gcb3da1c9e6.dirty\n\nThe patch look good. Thanks\n"},{"id":"522651","messageId":"CAOLa=ZTRGyypAMcevy2+CMbKQWe5PFqZrv9m5mOeaOAJSDV4kg@mail.gmail.com","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-7-183e5949de16@pks.im","subject":"Re: [PATCH 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-07-24T10:21:30Z","receivedAt":"2025-07-24T10:21:32Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> diff --git a/refs.c b/refs.c\n> index 64544300dc3..c78d5be6e20 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -1384,11 +1384,6 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n>  \tupdate = ref_transaction_add_update(transaction, refname, flags,\n>  \t\t\t\t\t    new_oid, old_oid, NULL, NULL,\n>  \t\t\t\t\t    committer_info, msg);\n> -\t/*\n> -\t * While we do set the old_oid value, we unset the flag to skip\n> -\t * old_oid verification which only makes sense for refs.\n> -\t */\n> -\tupdate->flags &= ~REF_HAVE_OLD;\n>  \tupdate->index = index;\n>\n>  \t/*\n\nSo we no longer unset the flag, this will ensure that the provided\nold_oid is propagated correctly.\n\n> @@ -3310,7 +3305,7 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n>\n>  int ref_update_expects_existing_old_ref(struct ref_update *update)\n\nNit: Wonder if we should update the comment for the function to reflect\nhow this works with reflog only entries.\n\n>  {\n> -\treturn (update->flags & REF_HAVE_OLD) &&\n> +\treturn (update->flags & (REF_HAVE_OLD | REF_LOG_ONLY)) == REF_HAVE_OLD &&\n>  \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n>  }\n>\n\nOkay this is check now says, if this is a reflog only entry, we don't\nexpect the reference to exist.\n\nNit: I wonder if can make this a bit more readable, perhaps:\n\n  int ref_update_expects_existing_old_ref(struct ref_update *update)\n  {\n      /* reflog only entries may not match on-disk status of a reference */\n      if (update->flags & REF_LOG_ONLY)\n          return 0;\n\n      return (update->flags & REF_HAVE_OLD &&\n          (!is_null_oid(&update->old_oid) || update->old_target);\n  }\n\nI'm okay with the current version too.\n\n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index 89ae4517a97..d519bb615fa 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -2493,7 +2493,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n>  \t * done when new_update is processed.\n>  \t */\n>  \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n> -\tupdate->flags &= ~REF_HAVE_OLD;\n>\n>  \treturn 0;\n>  }\n> @@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n>  \t\t\t\t\t\tstruct object_id *oid,\n>  \t\t\t\t\t\tstruct strbuf *err)\n>  {\n> -\tif (!(update->flags & REF_HAVE_OLD) ||\n> -\t\t   oideq(oid, &update->old_oid))\n> +\tif (update->flags & REF_LOG_ONLY ||\n> +\t    !(update->flags & REF_HAVE_OLD) ||\n> +\t    oideq(oid, &update->old_oid))\n>  \t\treturn 0;\n>\n>  \tif (is_null_oid(&update->old_oid)) {\n> @@ -3061,7 +3061,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n>  \tfor (i = 0; i < transaction->nr; i++) {\n>  \t\tstruct ref_update *update = transaction->updates[i];\n>\n> -\t\tif ((update->flags & REF_HAVE_OLD) &&\n> +\t\tif (!(update->flags & REF_LOG_ONLY) &&\n> +\t\t    (update->flags & REF_HAVE_OLD) &&\n>  \t\t    !is_null_oid(&update->old_oid))\n>  \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n>\n> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n> index 4c3817f4ec1..44af58ac50b 100644\n> --- a/refs/reftable-backend.c\n> +++ b/refs/reftable-backend.c\n> @@ -1180,8 +1180,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n>  \tif (ret > 0) {\n>  \t\t/* The reference does not exist, but we expected it to. */\n>  \t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n> -\n> -\n\nHuh. I'm the author of this misshap. Thanks for the cleanup :D\n\n>  \t\t\t\t   \"unable to resolve reference '%s'\"),\n>  \t\t\t    ref_update_original_update_refname(u), u->refname);\n>  \t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n> @@ -1235,13 +1233,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n>\n>  \t\t\tnew_update->parent_update = u;\n>\n> -\t\t\t/*\n> -\t\t\t * Change the symbolic ref update to log only. Also, it\n> -\t\t\t * doesn't need to check its old OID value, as that will be\n> -\t\t\t * done when new_update is processed.\n> -\t\t\t */\n> +\t\t\t/* Change the symbolic ref update to log only. */\n>  \t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n> -\t\t\tu->flags &= ~REF_HAVE_OLD;\n>  \t\t}\n>  \t}\n>\n> @@ -1265,7 +1258,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n>  \t\tret = ref_update_check_old_target(referent->buf, u, err);\n>  \t\tif (ret)\n>  \t\t\treturn ret;\n> -\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n> +\t} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&\n> +\t\t   !oideq(&current_oid, &u->old_oid)) {\n>  \t\tif (is_null_oid(&u->old_oid)) {\n>  \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n>  \t\t\t\t\t   \"reference already exists\"),\n>\n> --\n> 2.50.1.465.gcb3da1c9e6.dirty\n\nLooks good overall. Thanks\n"},{"id":"522654","messageId":"aIIahlqPJSn_ZVYl@pks.im","threadId":"63818","inReplyTo":"CAOLa=ZTRGyypAMcevy2+CMbKQWe5PFqZrv9m5mOeaOAJSDV4kg@mail.gmail.com","subject":"Re: [PATCH 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-24T11:35:34Z","receivedAt":"2025-07-24T11:35:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jul 24, 2025 at 03:21:30AM -0700, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > diff --git a/refs.c b/refs.c\n> > index 64544300dc3..c78d5be6e20 100644\n> > --- a/refs.c\n> > +++ b/refs.c\n> > @@ -3310,7 +3305,7 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n> >\n> >  int ref_update_expects_existing_old_ref(struct ref_update *update)\n> \n> Nit: Wonder if we should update the comment for the function to reflect\n> how this works with reflog only entries.\n\nYeah, let's.\n\n> >  {\n> > -\treturn (update->flags & REF_HAVE_OLD) &&\n> > +\treturn (update->flags & (REF_HAVE_OLD | REF_LOG_ONLY)) == REF_HAVE_OLD &&\n> >  \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n> >  }\n> >\n> \n> Okay this is check now says, if this is a reflog only entry, we don't\n> expect the reference to exist.\n> \n> Nit: I wonder if can make this a bit more readable, perhaps:\n> \n>   int ref_update_expects_existing_old_ref(struct ref_update *update)\n>   {\n>       /* reflog only entries may not match on-disk status of a reference */\n>       if (update->flags & REF_LOG_ONLY)\n>           return 0;\n> \n>       return (update->flags & REF_HAVE_OLD &&\n>           (!is_null_oid(&update->old_oid) || update->old_target);\n>   }\n> \n> I'm okay with the current version too.\n\nI think yours reads more straight-forward though.\n\nPatrick\n"},{"id":"522656","messageId":"87o6t9kaua.fsf@iotcl.com","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-4-183e5949de16@pks.im","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-07-24T12:54:53Z","receivedAt":"2025-07-24T12:55:10Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> While we provide a couple of subcommands in git-reflog(1) to remove\n> reflog entries, we don't provide any to write new entries. Obviously\n> this is not an operation that really would be needed for many use cases\n> out there, or otherwise people would have complained that such a command\n> does not exist yet. But the introduction of the \"reftable\" backend\n> changes the picture a bit, as it is now basically impossible to manually\n> append a reflog entry if one wanted to do so due to the binary format.\n>\n> Plug this gap by introducing a simple \"write\" subcommand. For now, all\n> this command does is to append a single new reflog entry with the given\n> object IDs and message to the reflog. More specifically, it is not yet\n> possible to:\n>\n>   - Write multiple reflog entries at once.\n>\n>   - Insert reflog entries at arbitrary indices.\n>\n>   - Specify the date of the reflog entry.\n>\n>   - Insert reflog entries that refer to nonexistent objects.\n>\n> If required, those features can be added at a future point in time. For\n> now though, the new command aims to fulfill the most basic use cases\n> while being as strict as possible when it comes to verifying parameters.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-reflog.adoc |  1 +\n>  builtin/reflog.c              | 65 ++++++++++++++++++++++++++++++++++\n>  t/meson.build                 |  1 +\n>  t/t1421-reflog-write.sh       | 81 +++++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 148 insertions(+)\n>\n> diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\n> index 6ae13e772b8..798dbc0a00a 100644\n> --- a/Documentation/git-reflog.adoc\n> +++ b/Documentation/git-reflog.adoc\n> @@ -12,6 +12,7 @@ SYNOPSIS\n>  git reflog [show] [<log-options>] [<ref>]\n>  git reflog list\n>  git reflog exists <ref>\n> +git reflog write <ref> <old-oid> <new-oid> <message>\n>  git reflog delete [--rewrite] [--updateref]\n>  \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n>  git reflog drop [--all [--single-worktree] | <refs>...]\n> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> index b00b3f9edc9..d0374295620 100644\n> --- a/builtin/reflog.c\n> +++ b/builtin/reflog.c\n> @@ -3,6 +3,8 @@\n>  #include \"builtin.h\"\n>  #include \"config.h\"\n>  #include \"gettext.h\"\n> +#include \"hex.h\"\n> +#include \"odb.h\"\n>  #include \"revision.h\"\n>  #include \"reachable.h\"\n>  #include \"wildmatch.h\"\n> @@ -20,6 +22,9 @@\n>  #define BUILTIN_REFLOG_EXISTS_USAGE \\\n>  \tN_(\"git reflog exists <ref>\")\n>  \n> +#define BUILTIN_REFLOG_WRITE_USAGE \\\n> +\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n> +\n>  #define BUILTIN_REFLOG_DELETE_USAGE \\\n>  \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n>  \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n> @@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n>  \tNULL,\n>  };\n>  \n> +static const char *const reflog_write_usage[] = {\n> +\tBUILTIN_REFLOG_WRITE_USAGE,\n> +\tNULL,\n> +};\n> +\n>  static const char *const reflog_delete_usage[] = {\n>  \tBUILTIN_REFLOG_DELETE_USAGE,\n>  \tNULL\n> @@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n>  \tBUILTIN_REFLOG_SHOW_USAGE,\n>  \tBUILTIN_REFLOG_LIST_USAGE,\n>  \tBUILTIN_REFLOG_EXISTS_USAGE,\n> +\tBUILTIN_REFLOG_WRITE_USAGE,\n>  \tBUILTIN_REFLOG_DELETE_USAGE,\n>  \tBUILTIN_REFLOG_DROP_USAGE,\n>  \tBUILTIN_REFLOG_EXPIRE_USAGE,\n> @@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n>  \treturn ret;\n>  }\n>  \n> +static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n> +\t\t\t    struct repository *repo)\n> +{\n> +\tconst struct option options[] = {\n> +\t\tOPT_END()\n> +\t};\n> +\tstruct object_id old_oid, new_oid;\n> +\tstruct strbuf err = STRBUF_INIT;\n> +\tstruct ref_transaction *tx;\n> +\tconst char *ref, *message;\n> +\tint ret;\n> +\n> +\targc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);\n\nWrong usage string here: s/reflog_drop_usage/reflog_write_usage/.\n\n-- \nCheers,\nToon\n"},{"id":"522657","messageId":"87ms8tkatf.fsf@iotcl.com","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-3-183e5949de16@pks.im","subject":"Re: [PATCH 3/8] refs: export `ref_transaction_update_reflog()`","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-07-24T12:55:24Z","receivedAt":"2025-07-24T12:55:35Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In a subsequent commit we'll add another user that wants to write reflog\n> entries. This requires them to call `ref_transaction_update_reflog()`,\n> but that functino is local to \"refs.c\".\n>\n> Export the function to prepare for the change. While at it, drop the\n> `flags` field, as all callers are for now expected to use the same flags\n> anyway.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs.c | 29 +++++++++++------------------\n>  refs.h | 15 +++++++++++++++\n>  2 files changed, 26 insertions(+), 18 deletions(-)\n>\n> diff --git a/refs.c b/refs.c\n> index 73913b6627b..188989e4113 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -1362,27 +1362,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>  \treturn 0;\n>  }\n>  \n> -/*\n> - * Similar to`ref_transaction_update`, but this function is only for adding\n> - * a reflog update. Supports providing custom committer information. The index\n> - * field can be utiltized to order updates as desired. When not used, the\n> - * updates default to being ordered by refname.\n\n\"not used\" is a little ambiguous for me. I had to dig a little and in\ntransaction_update_cmp() in refs/reftable-backend.c the index is only\nconsidered when the value is non-zero. What do you think about replacing\n\"not used\" with \"zero\"?\n\n-- \nCheers,\nToon\n"},{"id":"522658","messageId":"87ldodkas4.fsf@iotcl.com","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-6-183e5949de16@pks.im","subject":"Re: [PATCH 6/8] refs: fix identity for migrated reflogs","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-07-24T12:56:11Z","receivedAt":"2025-07-24T12:56:20Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When migrating reflog entries between different storage formats we must\n> reconstruct the identity of reflog entries. This is done by passing the\n> committer passed to the `migrate_one_reflog_entry()` callback function\n> to `fmt_ident()`.\n>\n> This results in an invalid identity though: `fmt_ident()` expects the\n> caller to provide both name and mail of the author, but we pass the full\n> identity as mail. This leads to an identity like:\n>\n>     pks <Patrick Steinhardt ps@pks.im>\n>\n> Fix the bug by splitting the identity line first. This allows us to\n> extract both the name and mail so that we can pass them to `fmt_ident()`\n> separately.\n>\n> This commit does not yet add any tests as there is another bug in the\n> reflog migration that will be fixed in a subsequent commit. Once that\n> bug is fixed we'll make the reflog verification in t1450 stricter, and\n> that will catch both this bug here and the other bug.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs.c | 21 ++++++++++++++++++---\n>  1 file changed, 18 insertions(+), 3 deletions(-)\n>\n> diff --git a/refs.c b/refs.c\n> index 188989e4113..64544300dc3 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -2945,7 +2945,7 @@ struct migration_data {\n>  \tstruct ref_store *old_refs;\n>  \tstruct ref_transaction *transaction;\n>  \tstruct strbuf *errbuf;\n> -\tstruct strbuf sb;\n> +\tstruct strbuf sb, name, mail;\n>  };\n>  \n>  static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n> @@ -2984,7 +2984,7 @@ struct reflog_migration_data {\n>  \tstruct ref_store *old_refs;\n>  \tstruct ref_transaction *transaction;\n>  \tstruct strbuf *errbuf;\n> -\tstruct strbuf *sb;\n> +\tstruct strbuf *sb, *name, *mail;\n>  };\n>  \n>  static int migrate_one_reflog_entry(struct object_id *old_oid,\n> @@ -2994,13 +2994,22 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n>  \t\t\t\t    const char *msg, void *cb_data)\n>  {\n>  \tstruct reflog_migration_data *data = cb_data;\n> +\tstruct ident_split ident;\n>  \tconst char *date;\n>  \tint ret;\n>  \n> +\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n> +\t\treturn -1;\n> +\n> +\tstrbuf_reset(data->name);\n> +\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n> +\tstrbuf_reset(data->mail);\n> +\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n> +\n>  \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n>  \tstrbuf_reset(data->sb);\n>  \t/* committer contains name and email */\n\nThis comment doesn't make sense here no more. Better leave it out.\n\n> -\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n> +\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n\nI was checking whether we could use IDENT_NO_NAME instead of parsing the\nident. But fmt_ident() calls strbuf_addstr_without_crud() which strips\nout the angle brackets from the email address, which builds an invalid\nidentity like: \"C O Mitter committer@example.com 1112911993 -0700\"\n\nWe could opt to add a flag IDENT_LEAVE_CRUD which calls strbuf_addstr()\ninstead, but I'm not sure we want to go there.\n\n-- \nCheers,\nToon\n"},{"id":"522686","messageId":"aIJdSCPEHoiWWxrP@szeder.dev","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-4-183e5949de16@pks.im","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2025-07-24T16:20:24Z","receivedAt":"2025-07-24T16:20:38Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Tue, Jul 22, 2025 at 01:20:53PM +0200, Patrick Steinhardt wrote:\n> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> index b00b3f9edc9..d0374295620 100644\n> --- a/builtin/reflog.c\n> +++ b/builtin/reflog.c\n> @@ -3,6 +3,8 @@\n>  #include \"builtin.h\"\n>  #include \"config.h\"\n>  #include \"gettext.h\"\n> +#include \"hex.h\"\n> +#include \"odb.h\"\n\nThis series is queued on top of v2.50.0, which doesn't have 'odb.h'\nyet.\n\n      CC builtin/reflog.o\n  builtin/reflog.c:7:10: fatal error: odb.h: No such file or directory\n      7 | #include \"odb.h\"\n        |          ^~~~~~~\n  compilation terminated.\n  make: *** [Makefile:2821: builtin/reflog.o] Error 1\n\n>  #include \"revision.h\"\n>  #include \"reachable.h\"\n>  #include \"wildmatch.h\"\n"},{"id":"522689","messageId":"xmqqikjhilm3.fsf@gitster.g","threadId":"63818","inReplyTo":"aIHj4NWr3M5lXLtD@pks.im","subject":"Re: [PATCH 2/8] builtin/reflog: improve grouping of subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-24T16:45:08Z","receivedAt":"2025-07-24T16:45:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Wed, Jul 23, 2025 at 01:14:19PM -0500, Justin Tobler wrote:\n>> > @@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n>> >  \tstruct option options[] = {\n>> >  \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n>> >  \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n>> > -\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n>> > -\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n>> >  \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n>> > +\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n>> >  \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n>> > +\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n>> >  \t\tOPT_END()\n>> >  \t};\n>> \n>> Structing the subcommands order in such a manner seems sensible, but I'm\n>> not sure the pattern will be recognized by others that may add\n>> subcommands in the future. Maybe we could leave a comment that mentions\n>> the order?\n>\n> Hm, dunno. I feel like it's subjective where to add a command anyway, so\n> I'm not sure that a comment would be allt hat helpful.\n\nI'd agree on both counts.  The only pattern I can see myself is to\nhave read-only operations first and then read-write operations, but\neven there, the choice of \"is it read-only?\" as an axis feels very\nmuch arbitrary (another obvious one is to list from the everyday \nuse to less often used to finally only administrative ones).\n\nIf the read-write operations are ordered by severity, I would place\nexpire (affects only stale entries) between delete (affects one\nentry in a reflog) and drop (deletes the whole thing).  But that\nagain is fairly arbitrary.\n"},{"id":"522702","messageId":"xmqqa54tz454.fsf@gitster.g","threadId":"63818","inReplyTo":"aIJdSCPEHoiWWxrP@szeder.dev","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-24T21:10:31Z","receivedAt":"2025-07-24T21:10:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> On Tue, Jul 22, 2025 at 01:20:53PM +0200, Patrick Steinhardt wrote:\n>> diff --git a/builtin/reflog.c b/builtin/reflog.c\n>> index b00b3f9edc9..d0374295620 100644\n>> --- a/builtin/reflog.c\n>> +++ b/builtin/reflog.c\n>> @@ -3,6 +3,8 @@\n>>  #include \"builtin.h\"\n>>  #include \"config.h\"\n>>  #include \"gettext.h\"\n>> +#include \"hex.h\"\n>> +#include \"odb.h\"\n>\n> This series is queued on top of v2.50.0, which doesn't have 'odb.h'\n> yet.\n\nThanks for checking.\n\nYet this is a topic to fix breakages that happened even before 2.50;\n\"git refs migrate\" started migrating reflogs in 2.48, which had one\nfix on top in 2.49.  For a non-security bugfix we typically do not\naddress anything older than the latest release's maintenance track,\nso a series that would fix on top of 2.50 would have been more\nappropriate.\n\n"},{"id":"522716","messageId":"aIMXzRESLwUVMNuD@pks.im","threadId":"63818","inReplyTo":"87o6t9kaua.fsf@iotcl.com","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T05:36:13Z","receivedAt":"2025-07-25T05:36:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jul 24, 2025 at 02:54:53PM +0200, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\n> > index 6ae13e772b8..798dbc0a00a 100644\n> > --- a/Documentation/git-reflog.adoc\n> > +++ b/Documentation/git-reflog.adoc\n> > @@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n> >  \treturn ret;\n> >  }\n> >  \n> > +static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n> > +\t\t\t    struct repository *repo)\n> > +{\n> > +\tconst struct option options[] = {\n> > +\t\tOPT_END()\n> > +\t};\n> > +\tstruct object_id old_oid, new_oid;\n> > +\tstruct strbuf err = STRBUF_INIT;\n> > +\tstruct ref_transaction *tx;\n> > +\tconst char *ref, *message;\n> > +\tint ret;\n> > +\n> > +\targc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);\n> \n> Wrong usage string here: s/reflog_drop_usage/reflog_write_usage/.\n\nGood catch, fixed now.\n\nPatrick\n"},{"id":"522717","messageId":"aIMX1nHa5E5Hn95g@pks.im","threadId":"63818","inReplyTo":"xmqqa54tz454.fsf@gitster.g","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T05:36:22Z","receivedAt":"2025-07-25T05:36:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jul 24, 2025 at 02:10:31PM -0700, Junio C Hamano wrote:\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n> \n> > On Tue, Jul 22, 2025 at 01:20:53PM +0200, Patrick Steinhardt wrote:\n> >> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> >> index b00b3f9edc9..d0374295620 100644\n> >> --- a/builtin/reflog.c\n> >> +++ b/builtin/reflog.c\n> >> @@ -3,6 +3,8 @@\n> >>  #include \"builtin.h\"\n> >>  #include \"config.h\"\n> >>  #include \"gettext.h\"\n> >> +#include \"hex.h\"\n> >> +#include \"odb.h\"\n> >\n> > This series is queued on top of v2.50.0, which doesn't have 'odb.h'\n> > yet.\n> \n> Thanks for checking.\n> \n> Yet this is a topic to fix breakages that happened even before 2.50;\n> \"git refs migrate\" started migrating reflogs in 2.48, which had one\n> fix on top in 2.49.  For a non-security bugfix we typically do not\n> address anything older than the latest release's maintenance track,\n> so a series that would fix on top of 2.50 would have been more\n> appropriate.\n\nSure, I can rebase this on top of v2.50.1. It would then of course\nrequire some smallish fixes when merged to `seen`. The below patch is\nwhat is required to make it work with the v2.50 track.\n\nPatrick\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex bc7e7f5e442..d3f0009cb0e 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -4,7 +4,7 @@\n #include \"config.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n-#include \"odb.h\"\n+#include \"object-store.h\"\n #include \"revision.h\"\n #include \"reachable.h\"\n #include \"wildmatch.h\"\n@@ -426,13 +426,13 @@ static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n \tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n \tif (ret)\n \t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n-\tif (!is_null_oid(&old_oid) && !odb_has_object(repo->objects, &old_oid, 0))\n+\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n \t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n \n \tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n \tif (ret)\n \t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n-\tif (!is_null_oid(&new_oid) && !odb_has_object(repo->objects, &new_oid, 0))\n+\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n \t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n \n \tmessage = argv[3];\n"},{"id":"522723","messageId":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH v2 0/8] refs: fix migration of reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:22Z","receivedAt":"2025-07-25T06:58:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nafter the announcement that \"reftable\" will become the default backend\nin Git 3.0 I've revived the efforts to implement this backend in\nlibgit2. I'm happy to report that this implementation is almost done by\nnow: out of 3000 tests only four are failing now.\n\nFor two of these tests I have been completely puzzled why those are\nfailing, as everything really looked perfectly fine in libgit2. As it\nturned out, the bug wasn't in libgit2 though, but in Git. Namely, the\nway we migrate reflog entries between storage formats is broken in two\nways:\n\n  - The identity we write into the reflog entries is wrong.\n\n  - The old commit ID of reflog entries is always set to all-zeroes.\n    This is what caused the libgit2 tests to fail, as I used `git refs\n    migrate` to convert test repositories to use reftables.\n\nThis patch series fixes both of these issues. Furthermore, it also adds\na new `git reflog write` subcommand to write new reflog entries for a\nspecific reference. This command was helpful to reproduce some test\nconstellations in libgit2.\n\nChanges in v2:\n  - !!! The base of this topic has changed so that it sits on top of\n    v2.50.1. This is done so that we can backport this change to older\n    release tracks.\n  - A couple of typo fixes and clarifications for commit messages.\n  - Reorder sections in git-reflog(1) manpage according to the\n    reordering we have in the synopsis.\n  - Add a section for the new `write` command.\n  - Improve test coverage for the `git reflog write` command.\n  - Avoid `cat`ing a file into a Bash loop.\n  - Remove a stale comment.\n  - Make `ref_update_expects_existing_old_ref()` a bit more straight\n    forward.\n  - Link to v1: https://lore.kernel.org/r/20250722-pks-reflog-append-v1-0-183e5949de16@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (8):\n      Documentation/git-reflog: convert to use synopsis type\n      builtin/reflog: improve grouping of subcommands\n      refs: export `ref_transaction_update_reflog()`\n      builtin/reflog: implement subcommand to write new entries\n      ident: fix type of string length parameter\n      refs: fix identity for migrated reflogs\n      refs: stop unsetting REF_HAVE_OLD for log-only updates\n      refs: fix invalid old object IDs when migrating reflogs\n\n Documentation/git-reflog.adoc |  76 +++++++++++++++++--------------\n builtin/reflog.c              | 103 ++++++++++++++++++++++++++++++++++--------\n ident.c                       |   2 +-\n ident.h                       |   2 +-\n refs.c                        |  60 +++++++++++++-----------\n refs.h                        |  24 +++++++++-\n refs/files-backend.c          |  25 ++++++++--\n refs/refs-internal.h          |   3 +-\n refs/reftable-backend.c       |  26 +++++++----\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       |  99 ++++++++++++++++++++++++++++++++++++++++\n t/t1460-refs-migrate.sh       |  22 ++++++---\n 12 files changed, 338 insertions(+), 105 deletions(-)\n\nRange-diff versus v1:\n\n1:  a5f91e51802 = 1:  19a1df63ee6 Documentation/git-reflog: convert to use synopsis type\n2:  3b39a73a942 ! 2:  807cc393337 builtin/reflog: improve grouping of subcommands\n    @@ Metadata\n      ## Commit message ##\n         builtin/reflog: improve grouping of subcommands\n     \n    -    The way subcommands of git-reflog(1) are layed out does not make any\n    +    The way subcommands of git-reflog(1) are laid out does not make any\n         immediate sense. Reorder them such that read-only subcommands precede\n         writing commands for a bit more structure.\n     \n    @@ Documentation/git-reflog.adoc: SYNOPSIS\n      \n      DESCRIPTION\n      -----------\n    +@@ Documentation/git-reflog.adoc: actions, and in addition the `HEAD` reflog records branch switching.\n    + \n    + The \"list\" subcommand lists all refs which have a corresponding reflog.\n    + \n    +-The \"expire\" subcommand prunes older reflog entries. Entries older\n    +-than `expire` time, or entries older than `expire-unreachable` time\n    +-and not reachable from the current tip, are removed from the reflog.\n    +-This is typically not used directly by end users -- instead, see\n    +-linkgit:git-gc[1].\n    ++The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n    ++with zero status if the reflog exists, and non-zero status if it does\n    ++not.\n    + \n    + The \"delete\" subcommand deletes single entries from the reflog, but\n    + not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n    +@@ Documentation/git-reflog.adoc: The \"drop\" subcommand completely removes the reflog for the specified\n    + references. This is in contrast to \"expire\" and \"delete\", both of which\n    + can be used to delete reflog entries, but not the reflog itself.\n    + \n    +-The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n    +-with zero status if the reflog exists, and non-zero status if it does\n    +-not.\n    ++The \"expire\" subcommand prunes older reflog entries. Entries older\n    ++than `expire` time, or entries older than `expire-unreachable` time\n    ++and not reachable from the current tip, are removed from the reflog.\n    ++This is typically not used directly by end users -- instead, see\n    ++linkgit:git-gc[1].\n    + \n    + OPTIONS\n    + -------\n    +@@ Documentation/git-reflog.adoc: Options for `show`\n    + `git reflog show` accepts any of the options accepted by `git log`.\n    + \n    + \n    ++Options for `delete`\n    ++~~~~~~~~~~~~~~~~~~~~\n    ++\n    ++`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n    ++`--dry-run`, and `--verbose`, with the same meanings as when they are\n    ++used with `expire`.\n    ++\n    ++Options for `drop`\n    ++~~~~~~~~~~~~~~~~~~\n    ++\n    ++--all::\n    ++\tDrop the reflogs of all references from all worktrees.\n    ++\n    ++--single-worktree::\n    ++\tBy default when `--all` is specified, reflogs from all working\n    ++\ttrees are dropped. This option limits the processing to reflogs\n    ++\tfrom the current working tree only.\n    ++\n    ++\n    + Options for `expire`\n    + ~~~~~~~~~~~~~~~~~~~~\n    + \n    +@@ Documentation/git-reflog.adoc: which didn't protect objects referred to by reflogs.\n    + \tPrint extra information on screen.\n    + \n    + \n    +-Options for `delete`\n    +-~~~~~~~~~~~~~~~~~~~~\n    +-\n    +-`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n    +-`--dry-run`, and `--verbose`, with the same meanings as when they are\n    +-used with `expire`.\n    +-\n    +-Options for `drop`\n    +-~~~~~~~~~~~~~~~~~~\n    +-\n    +---all::\n    +-\tDrop the reflogs of all references from all worktrees.\n    +-\n    +---single-worktree::\n    +-\tBy default when `--all` is specified, reflogs from all working\n    +-\ttrees are dropped. This option limits the processing to reflogs\n    +-\tfrom the current working tree only.\n    +-\n    + GIT\n    + ---\n    + Part of the linkgit:git[1] suite\n     \n      ## builtin/reflog.c ##\n     @@\n3:  ce7a5409eec ! 3:  1fe1c93db2f refs: export `ref_transaction_update_reflog()`\n    @@ Commit message\n     \n         In a subsequent commit we'll add another user that wants to write reflog\n         entries. This requires them to call `ref_transaction_update_reflog()`,\n    -    but that functino is local to \"refs.c\".\n    +    but that function is local to \"refs.c\".\n     \n         Export the function to prepare for the change. While at it, drop the\n         `flags` field, as all callers are for now expected to use the same flags\n    @@ refs.h: int ref_transaction_update(struct ref_transaction *transaction,\n      \t\t\t   struct strbuf *err);\n      \n     +/*\n    -+ * Similar to`ref_transaction_update`, but this function is only for adding\n    ++ * Similar to `ref_transaction_update`, but this function is only for adding\n     + * a reflog update. Supports providing custom committer information. The index\n    -+ * field can be utiltized to order updates as desired. When not used, the\n    ++ * field can be utiltized to order updates as desired. When set to zero, the\n     + * updates default to being ordered by refname.\n     + */\n     +int ref_transaction_update_reflog(struct ref_transaction *transaction,\n4:  c0a7e9031dd ! 4:  38835607a92 builtin/reflog: implement subcommand to write new entries\n    @@ Documentation/git-reflog.adoc: SYNOPSIS\n      git reflog delete [--rewrite] [--updateref]\n      \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n      git reflog drop [--all [--single-worktree] | <refs>...]\n    +@@ Documentation/git-reflog.adoc: The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n    + with zero status if the reflog exists, and non-zero status if it does\n    + not.\n    + \n    ++The \"write\" subcommand writes a single entry to the reflog of a given\n    ++reference. This new entry is appended to the reflog and will thus become\n    ++the most recent entry. Both the old and new object IDs must not be\n    ++abbreviated and must point to existing objects. The reflog message gets\n    ++normalized.\n    ++\n    + The \"delete\" subcommand deletes single entries from the reflog, but\n    + not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n    + reflog delete master@{2}`\"). This subcommand is also typically not used\n     \n      ## builtin/reflog.c ##\n     @@\n    @@ builtin/reflog.c\n      #include \"config.h\"\n      #include \"gettext.h\"\n     +#include \"hex.h\"\n    -+#include \"odb.h\"\n    ++#include \"object-store.h\"\n      #include \"revision.h\"\n      #include \"reachable.h\"\n      #include \"wildmatch.h\"\n    @@ builtin/reflog.c: static int cmd_reflog_drop(int argc, const char **argv, const\n     +\tconst char *ref, *message;\n     +\tint ret;\n     +\n    -+\targc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);\n    ++\targc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);\n     +\tif (argc != 4)\n     +\t\tusage_with_options(reflog_write_usage, options);\n     +\n    @@ builtin/reflog.c: static int cmd_reflog_drop(int argc, const char **argv, const\n     +\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n     +\tif (ret)\n     +\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n    -+\tif (!is_null_oid(&old_oid) && !odb_has_object(repo->objects, &old_oid, 0))\n    ++\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n     +\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n     +\n     +\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n     +\tif (ret)\n     +\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n    -+\tif (!is_null_oid(&new_oid) && !odb_has_object(repo->objects, &new_oid, 0))\n    ++\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n     +\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n     +\n     +\tmessage = argv[3];\n    @@ t/t1421-reflog-write.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'nonexistent old object ID' '\n    ++test_expect_success 'nonexistent object IDs' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n     +\t(\n     +\t\tcd repo &&\n    -+\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID first 2>err &&\n    -+\t\ttest_grep \"old object .* does not exist\" err\n    ++\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&\n    ++\t\ttest_grep \"old object .* does not exist\" err &&\n    ++\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&\n    ++\t\ttest_grep \"new object .* does not exist\" err\n     +\t)\n     +'\n     +\n    -+test_expect_success 'nonexistent new object ID' '\n    ++test_expect_success 'abbreviated object IDs' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n     +\t(\n     +\t\tcd repo &&\n    -+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) first 2>err &&\n    -+\t\ttest_grep \"new object .* does not exist\" err\n    ++\t\ttest_must_fail git reflog write refs/heads/something 12345 $ZERO_OID old-object-id 2>err &&\n    ++\t\ttest_grep \"invalid old object ID\" err &&\n    ++\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID 12345 new-object-id 2>err &&\n    ++\t\ttest_grep \"invalid new object ID\" err\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'reflog message gets normalized' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit initial &&\n    ++\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n    ++\t\tgit reflog write HEAD $COMMIT_OID $COMMIT_OID \"$(printf \"message\\nwith\\nnewlines\")\" &&\n    ++\t\tgit reflog show -1 --format=%gs HEAD >actual &&\n    ++\t\techo \"message with newlines\" >expected &&\n    ++\t\ttest_cmp expected actual\n     +\t)\n     +'\n     +\n    @@ t/t1421-reflog-write.sh (new)\n     +\t\tEOF\n     +\n     +\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n    ++\t\t# Note: the old object ID of the second reflog entry is broken.\n    ++\t\t# This will be fixed in subsequent commits.\n     +\t\ttest_reflog_matches . refs/heads/something <<-EOF\n     +\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n    -+\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n    ++\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n     +\t\tEOF\n     +\t)\n     +'\n5:  15fddba8b21 = 5:  7b6c0d00668 ident: fix type of string length parameter\n6:  588d7a397f9 ! 6:  8caa2899d25 refs: fix identity for migrated reflogs\n    @@ Commit message\n         bug is fixed we'll make the reflog verification in t1450 stricter, and\n         that will catch both this bug here and the other bug.\n     \n    +    Note that we also add two new `name` and `mail` string buffers to the\n    +    callback structures and splice them through to the callbacks. This is\n    +    done so that we can avoid allocating a new buffer every time we compute\n    +    the committer information.\n    +\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## refs.c ##\n    @@ refs.c: static int migrate_one_reflog_entry(struct object_id *old_oid,\n     +\n      \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n      \tstrbuf_reset(data->sb);\n    - \t/* committer contains name and email */\n    +-\t/* committer contains name and email */\n     -\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n     +\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n      \n7:  fea43d4a3f9 ! 7:  e2ed6164a7c refs: stop unsetting REF_HAVE_OLD for log-only updates\n    @@ Commit message\n         refs: stop unsetting REF_HAVE_OLD for log-only updates\n     \n         The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\n    -    object ID set. If so, the value of that field is used to verify whteher\n    +    object ID set. If so, the value of that field is used to verify whether\n         the current state of the reference matches this expected state. It is\n         thus an important part of mitigating races with a concurrent process\n         that updates the same set of references.\n    @@ Commit message\n     \n         But unsetting this flag also removes a useful signal, namely that the\n         caller _did_ provide an old object ID for a given reflog entry. This\n    -    signal is useful to determine whether we have to resolve the refname\n    -    manually to figure out the current state, or whether we should just go\n    -    with what the caller has provided.\n    +    signal will become useful in a subsequent commit, where we add a new\n    +    flag that tells the transaction to use the provided old and new object\n    +    IDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\n    +    signal to verify that the caller really did provide an old object ID.\n     \n    -    This actually causes real issues when migrating reflogs, as we don't\n    -    know to actually use the caller-provided old object ID when writing\n    -    those entries. Instead, reflog entries simply end up with the all-zero\n    -    object ID.\n    -\n    -    Stop unsetting the flag so that we can use it as this described signal,\n    -    which we'll do in a subsequent commit. Skip checking the old object ID\n    -    for log-only updates so that we don't expect it to match the current\n    -    on-disk state.\n    +    Stop unsetting the flag so that we can use it as this described signal\n    +    in a subsequent commit. Skip checking the old object ID for log-only\n    +    updates so that we don't expect it to match the current on-disk state.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ refs.c: int repo_migrate_ref_storage_format(struct repository *repo,\n      \n      int ref_update_expects_existing_old_ref(struct ref_update *update)\n      {\n    --\treturn (update->flags & REF_HAVE_OLD) &&\n    -+\treturn (update->flags & (REF_HAVE_OLD | REF_LOG_ONLY)) == REF_HAVE_OLD &&\n    ++\tif (update->flags & REF_LOG_ONLY)\n    ++\t\treturn 0;\n    ++\n    + \treturn (update->flags & REF_HAVE_OLD) &&\n      \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n      }\n    - \n     \n      ## refs/files-backend.c ##\n     @@ refs/files-backend.c: static enum ref_transaction_error split_symref_update(struct ref_update *update,\n    @@ refs/files-backend.c: static int files_transaction_finish_initial(struct files_r\n      \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n      \n     \n    + ## refs/refs-internal.h ##\n    +@@ refs/refs-internal.h: enum ref_transaction_error ref_update_check_old_target(const char *referent,\n    + \n    + /*\n    +  * Check if the ref must exist, this means that the old_oid or\n    +- * old_target is non NULL.\n    ++ * old_target is non NULL. Log-only updates never require the old state to\n    ++ * match.\n    +  */\n    + int ref_update_expects_existing_old_ref(struct ref_update *update);\n    + \n    +\n      ## refs/reftable-backend.c ##\n     @@ refs/reftable-backend.c: static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n      \tif (ret > 0) {\n8:  24b5261d882 ! 8:  2382633e2e3 refs: fix invalid old object IDs when migrating reflogs\n    @@ refs/reftable-backend.c: static enum ref_transaction_error prepare_single_update\n      \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n      \t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\n     \n    + ## t/t1421-reflog-write.sh ##\n    +@@ t/t1421-reflog-write.sh: test_expect_success 'simple writes' '\n    + \t\tEOF\n    + \n    + \t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n    +-\t\t# Note: the old object ID of the second reflog entry is broken.\n    +-\t\t# This will be fixed in subsequent commits.\n    + \t\ttest_reflog_matches . refs/heads/something <<-EOF\n    + \t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n    +-\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n    ++\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n    + \t\tEOF\n    + \t)\n    + '\n    +\n      ## t/t1460-refs-migrate.sh ##\n     @@ t/t1460-refs-migrate.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n      \n    @@ t/t1460-refs-migrate.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     +print_all_reflog_entries () {\n     +\trepo=$1 &&\n     +\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n    -+\tcat reflogs | while read reflog\n    ++\twhile read reflog\n     +\tdo\n     +\t\techo \"REFLOG: $reflog\" &&\n     +\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n     +\t\treturn 1\n    -+\tdone\n    ++\tdone <reflogs\n     +}\n     +\n      # Migrate the provided repository from one format to the other and\n\n---\nbase-commit: d82adb61ba2fd11d8f2587fca1b6bd7925ce4044\nchange-id: 20250722-pks-reflog-append-634172d8ab2c\n\n"},{"id":"522724","messageId":"20250725-pks-reflog-append-v2-1-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 1/8] Documentation/git-reflog: convert to use synopsis type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:23Z","receivedAt":"2025-07-25T06:58:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\nhave introduced a new synopsis type that simplifies the rules for\ntypesetting a command's synopsis. Convert the git-reflog(1)\ndocumentation to use it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 14 +++++++-------\n 1 file changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 412f06b8fec..707a9b39edb 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n \n SYNOPSIS\n --------\n-[verse]\n-'git reflog' [show] [<log-options>] [<ref>]\n-'git reflog list'\n-'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n+[synopsis]\n+git reflog [show] [<log-options>] [<ref>]\n+git reflog list\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n \t[--rewrite] [--updateref] [--stale-fix]\n \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n-'git reflog delete' [--rewrite] [--updateref]\n+git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n-'git reflog drop' [--all [--single-worktree] | <refs>...]\n-'git reflog exists' <ref>\n+git reflog drop [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n \n DESCRIPTION\n -----------\n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522725","messageId":"20250725-pks-reflog-append-v2-2-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 2/8] builtin/reflog: improve grouping of subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:24Z","receivedAt":"2025-07-25T06:58:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The way subcommands of git-reflog(1) are laid out does not make any\nimmediate sense. Reorder them such that read-only subcommands precede\nwriting commands for a bit more structure.\n\nFurthermore, move the \"expire\" subcommand last. This prepares for a\nsubsequent change where we are about to introduce a new \"write\" command\nto append reflog entries. Like this, the writing subcommands are ordered\nsuch that those affecting a single reflog come before those spanning\nacross all reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 61 ++++++++++++++++++++++---------------------\n builtin/reflog.c              | 38 +++++++++++++--------------\n 2 files changed, 50 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 707a9b39edb..c3801b82fb6 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -11,13 +11,13 @@ SYNOPSIS\n [synopsis]\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n-git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n-\t[--rewrite] [--updateref] [--stale-fix]\n-\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n-git reflog exists <ref>\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n+\t[--rewrite] [--updateref] [--stale-fix]\n+\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,9 @@ actions, and in addition the `HEAD` reflog records branch switching.\n \n The \"list\" subcommand lists all refs which have a corresponding reflog.\n \n-The \"expire\" subcommand prunes older reflog entries. Entries older\n-than `expire` time, or entries older than `expire-unreachable` time\n-and not reachable from the current tip, are removed from the reflog.\n-This is typically not used directly by end users -- instead, see\n-linkgit:git-gc[1].\n+The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n+with zero status if the reflog exists, and non-zero status if it does\n+not.\n \n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n@@ -58,9 +56,11 @@ The \"drop\" subcommand completely removes the reflog for the specified\n references. This is in contrast to \"expire\" and \"delete\", both of which\n can be used to delete reflog entries, but not the reflog itself.\n \n-The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n-with zero status if the reflog exists, and non-zero status if it does\n-not.\n+The \"expire\" subcommand prunes older reflog entries. Entries older\n+than `expire` time, or entries older than `expire-unreachable` time\n+and not reachable from the current tip, are removed from the reflog.\n+This is typically not used directly by end users -- instead, see\n+linkgit:git-gc[1].\n \n OPTIONS\n -------\n@@ -71,6 +71,25 @@ Options for `show`\n `git reflog show` accepts any of the options accepted by `git log`.\n \n \n+Options for `delete`\n+~~~~~~~~~~~~~~~~~~~~\n+\n+`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n+`--dry-run`, and `--verbose`, with the same meanings as when they are\n+used with `expire`.\n+\n+Options for `drop`\n+~~~~~~~~~~~~~~~~~~\n+\n+--all::\n+\tDrop the reflogs of all references from all worktrees.\n+\n+--single-worktree::\n+\tBy default when `--all` is specified, reflogs from all working\n+\ttrees are dropped. This option limits the processing to reflogs\n+\tfrom the current working tree only.\n+\n+\n Options for `expire`\n ~~~~~~~~~~~~~~~~~~~~\n \n@@ -130,24 +149,6 @@ which didn't protect objects referred to by reflogs.\n \tPrint extra information on screen.\n \n \n-Options for `delete`\n-~~~~~~~~~~~~~~~~~~~~\n-\n-`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n-`--dry-run`, and `--verbose`, with the same meanings as when they are\n-used with `expire`.\n-\n-Options for `drop`\n-~~~~~~~~~~~~~~~~~~\n-\n---all::\n-\tDrop the reflogs of all references from all worktrees.\n-\n---single-worktree::\n-\tBy default when `--all` is specified, reflogs from all working\n-\ttrees are dropped. This option limits the processing to reflogs\n-\tfrom the current working tree only.\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 3acaf3e32c2..b00b3f9edc9 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -17,21 +17,21 @@\n #define BUILTIN_REFLOG_LIST_USAGE \\\n \tN_(\"git reflog list\")\n \n-#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n-\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n-\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n-\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+#define BUILTIN_REFLOG_EXISTS_USAGE \\\n+\tN_(\"git reflog exists <ref>\")\n \n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n \n-#define BUILTIN_REFLOG_EXISTS_USAGE \\\n-\tN_(\"git reflog exists <ref>\")\n-\n #define BUILTIN_REFLOG_DROP_USAGE \\\n \tN_(\"git reflog drop [--all [--single-worktree] | <refs>...]\")\n \n+#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n+\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n+\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n+\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+\n static const char *const reflog_show_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tNULL,\n@@ -42,9 +42,9 @@ static const char *const reflog_list_usage[] = {\n \tNULL,\n };\n \n-static const char *const reflog_expire_usage[] = {\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n-\tNULL\n+static const char *const reflog_exists_usage[] = {\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tNULL,\n };\n \n static const char *const reflog_delete_usage[] = {\n@@ -52,23 +52,23 @@ static const char *const reflog_delete_usage[] = {\n \tNULL\n };\n \n-static const char *const reflog_exists_usage[] = {\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n-\tNULL,\n-};\n-\n static const char *const reflog_drop_usage[] = {\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tNULL,\n };\n \n+static const char *const reflog_expire_usage[] = {\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tNULL\n+};\n+\n static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n \tNULL\n };\n \n@@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n-\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n-\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522726","messageId":"20250725-pks-reflog-append-v2-3-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 3/8] refs: export `ref_transaction_update_reflog()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:25Z","receivedAt":"2025-07-25T06:58:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we'll add another user that wants to write reflog\nentries. This requires them to call `ref_transaction_update_reflog()`,\nbut that function is local to \"refs.c\".\n\nExport the function to prepare for the change. While at it, drop the\n`flags` field, as all callers are for now expected to use the same flags\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 29 +++++++++++------------------\n refs.h | 15 +++++++++++++++\n 2 files changed, 26 insertions(+), 18 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex dce5c49ca2b..8aa9f7236a3 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1371,27 +1371,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \treturn 0;\n }\n \n-/*\n- * Similar to`ref_transaction_update`, but this function is only for adding\n- * a reflog update. Supports providing custom committer information. The index\n- * field can be utiltized to order updates as desired. When not used, the\n- * updates default to being ordered by refname.\n- */\n-static int ref_transaction_update_reflog(struct ref_transaction *transaction,\n-\t\t\t\t\t const char *refname,\n-\t\t\t\t\t const struct object_id *new_oid,\n-\t\t\t\t\t const struct object_id *old_oid,\n-\t\t\t\t\t const char *committer_info,\n-\t\t\t\t\t unsigned int flags,\n-\t\t\t\t\t const char *msg,\n-\t\t\t\t\t uint64_t index,\n-\t\t\t\t\t struct strbuf *err)\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err)\n {\n \tstruct ref_update *update;\n+\tunsigned int flags;\n \n \tassert(err);\n \n-\tflags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\n@@ -3019,8 +3013,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n-\t\t\t\t\t    REF_HAVE_NEW | REF_HAVE_OLD, msg,\n-\t\t\t\t\t    data->index++, data->errbuf);\n+\t\t\t\t\t    msg, data->index++, data->errbuf);\n \treturn ret;\n }\n \ndiff --git a/refs.h b/refs.h\nindex 46a6008e07f..253dd8f4d5d 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -795,6 +795,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \n+/*\n+ * Similar to `ref_transaction_update`, but this function is only for adding\n+ * a reflog update. Supports providing custom committer information. The index\n+ * field can be utiltized to order updates as desired. When set to zero, the\n+ * updates default to being ordered by refname.\n+ */\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err);\n+\n /*\n  * Add a reference creation to transaction. new_oid is the value that\n  * the reference should have after the update; it must not be\n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522727","messageId":"20250725-pks-reflog-append-v2-4-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:26Z","receivedAt":"2025-07-25T06:58:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we provide a couple of subcommands in git-reflog(1) to remove\nreflog entries, we don't provide any to write new entries. Obviously\nthis is not an operation that really would be needed for many use cases\nout there, or otherwise people would have complained that such a command\ndoes not exist yet. But the introduction of the \"reftable\" backend\nchanges the picture a bit, as it is now basically impossible to manually\nappend a reflog entry if one wanted to do so due to the binary format.\n\nPlug this gap by introducing a simple \"write\" subcommand. For now, all\nthis command does is to append a single new reflog entry with the given\nobject IDs and message to the reflog. More specifically, it is not yet\npossible to:\n\n  - Write multiple reflog entries at once.\n\n  - Insert reflog entries at arbitrary indices.\n\n  - Specify the date of the reflog entry.\n\n  - Insert reflog entries that refer to nonexistent objects.\n\nIf required, those features can be added at a future point in time. For\nnow though, the new command aims to fulfill the most basic use cases\nwhile being as strict as possible when it comes to verifying parameters.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc |   7 +++\n builtin/reflog.c              |  65 +++++++++++++++++++++++++++\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 101 ++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 174 insertions(+)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex c3801b82fb6..c8389810273 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n git reflog exists <ref>\n+git reflog write <ref> <old-oid> <new-oid> <message>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n@@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n with zero status if the reflog exists, and non-zero status if it does\n not.\n \n+The \"write\" subcommand writes a single entry to the reflog of a given\n+reference. This new entry is appended to the reflog and will thus become\n+the most recent entry. Both the old and new object IDs must not be\n+abbreviated and must point to existing objects. The reflog message gets\n+normalized.\n+\n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n reflog delete master@{2}`\"). This subcommand is also typically not used\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b00b3f9edc9..d3f0009cb0e 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -3,6 +3,8 @@\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-store.h\"\n #include \"revision.h\"\n #include \"reachable.h\"\n #include \"wildmatch.h\"\n@@ -20,6 +22,9 @@\n #define BUILTIN_REFLOG_EXISTS_USAGE \\\n \tN_(\"git reflog exists <ref>\")\n \n+#define BUILTIN_REFLOG_WRITE_USAGE \\\n+\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n+\n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n@@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n \tNULL,\n };\n \n+static const char *const reflog_write_usage[] = {\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n+\tNULL,\n+};\n+\n static const char *const reflog_delete_usage[] = {\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tNULL\n@@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n \tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tBUILTIN_REFLOG_EXPIRE_USAGE,\n@@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n+\t\t\t    struct repository *repo)\n+{\n+\tconst struct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct object_id old_oid, new_oid;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct ref_transaction *tx;\n+\tconst char *ref, *message;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);\n+\tif (argc != 4)\n+\t\tusage_with_options(reflog_write_usage, options);\n+\n+\tref = argv[0];\n+\tif (check_refname_format(ref, REFNAME_ALLOW_ONELEVEL))\n+\t\tdie(_(\"invalid reference name: %s\"), ref);\n+\n+\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n+\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n+\n+\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n+\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n+\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n+\n+\tmessage = argv[3];\n+\n+\ttx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\tif (!tx)\n+\t\tdie(_(\"cannot start transaction: %s\"), err.buf);\n+\n+\tret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,\n+\t\t\t\t\t    git_committer_info(0),\n+\t\t\t\t\t    message, 0, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot queue reflog update: %s\"), err.buf);\n+\n+\tret = ref_transaction_commit(tx, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot commit reflog update: %s\"), err.buf);\n+\n+\tref_transaction_free(tx);\n+\tstrbuf_release(&err);\n+\treturn 0;\n+}\n+\n /*\n  * main \"reflog\"\n  */\n@@ -405,6 +469,7 @@ int cmd_reflog(int argc,\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_reflog_write),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n \t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\ndiff --git a/t/meson.build b/t/meson.build\nindex d052fc3e23d..adcdf09e740 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -220,6 +220,7 @@ integration_tests = [\n   't1418-reflog-exists.sh',\n   't1419-exclude-refs.sh',\n   't1420-lost-found.sh',\n+  't1421-reflog-write.sh',\n   't1430-bad-ref-name.sh',\n   't1450-fsck.sh',\n   't1451-fsck-buffer.sh',\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nnew file mode 100755\nindex 00000000000..0cd2c4f4f31\n--- /dev/null\n+++ b/t/t1421-reflog-write.sh\n@@ -0,0 +1,101 @@\n+#!/bin/sh\n+\n+test_description='Manually write reflog entries'\n+\n+. ./test-lib.sh\n+\n+SIGNATURE=\"C O Mitter <committer@example.com> 1112911993 -0700\"\n+\n+test_reflog_matches () {\n+\trepo=\"$1\" &&\n+\trefname=\"$2\" &&\n+\tcat >actual &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$refname\" >expected &&\n+\ttest_cmp expected actual\n+}\n+\n+test_expect_success 'invalid number of arguments' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tfor args in \"\" \"1\" \"1 2\" \"1 2 3\" \"1 2 3 4 5\"\n+\t\tdo\n+\t\t\ttest_must_fail git reflog write $args 2>err &&\n+\t\t\ttest_grep \"usage: git reflog write\" err || return 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success 'invalid refname' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write \"refs/heads/ invalid\" $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'nonexistent object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"old object .* does not exist\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&\n+\t\ttest_grep \"new object .* does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'abbreviated object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something 12345 $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID 12345 new-object-id 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err\n+\t)\n+'\n+\n+test_expect_success 'reflog message gets normalized' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\t\tgit reflog write HEAD $COMMIT_OID $COMMIT_OID \"$(printf \"message\\nwith\\nnewlines\")\" &&\n+\t\tgit reflog show -1 --format=%gs HEAD >actual &&\n+\t\techo \"message with newlines\" >expected &&\n+\t\ttest_cmp expected actual\n+\t)\n+'\n+\n+test_expect_success 'simple writes' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF &&\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n+\t\t# Note: the old object ID of the second reflog entry is broken.\n+\t\t# This will be fixed in subsequent commits.\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522728","messageId":"20250725-pks-reflog-append-v2-5-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 5/8] ident: fix type of string length parameter","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:27Z","receivedAt":"2025-07-25T06:58:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The last parameter in `split_ident_line()` is the length of the line\npassed in by the caller. As such, most callers pass in either the result\nof `strlen()`, `struct strbuf::len` or a pointer diff, all of which\nare expected to be positive numbers. Regardless of that, the function\naccepts a signed integer, which is somewhat confusing.\n\nFix the function signature to instead accept a `size_t`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ident.c | 2 +-\n ident.h | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ident.c b/ident.c\nindex 967895d8850..a7a2d132579 100644\n--- a/ident.c\n+++ b/ident.c\n@@ -272,7 +272,7 @@ static void strbuf_addstr_without_crud(struct strbuf *sb, const char *src)\n  * can still be NULL if the input line only has the name/email part\n  * (e.g. reading from a reflog entry).\n  */\n-int split_ident_line(struct ident_split *split, const char *line, int len)\n+int split_ident_line(struct ident_split *split, const char *line, size_t len)\n {\n \tconst char *cp;\n \tsize_t span;\ndiff --git a/ident.h b/ident.h\nindex 6a79febba15..3c034038791 100644\n--- a/ident.h\n+++ b/ident.h\n@@ -35,7 +35,7 @@ void reset_ident_date(void);\n  * Signals an success with 0, but time part of the result may be NULL\n  * if the input lacks timestamp and zone\n  */\n-int split_ident_line(struct ident_split *, const char *, int);\n+int split_ident_line(struct ident_split *, const char *, size_t);\n \n /*\n  * Given a commit or tag object buffer and the commit or tag headers, replaces\n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522729","messageId":"20250725-pks-reflog-append-v2-6-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 6/8] refs: fix identity for migrated reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:28Z","receivedAt":"2025-07-25T06:58:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we must\nreconstruct the identity of reflog entries. This is done by passing the\ncommitter passed to the `migrate_one_reflog_entry()` callback function\nto `fmt_ident()`.\n\nThis results in an invalid identity though: `fmt_ident()` expects the\ncaller to provide both name and mail of the author, but we pass the full\nidentity as mail. This leads to an identity like:\n\n    pks <Patrick Steinhardt ps@pks.im>\n\nFix the bug by splitting the identity line first. This allows us to\nextract both the name and mail so that we can pass them to `fmt_ident()`\nseparately.\n\nThis commit does not yet add any tests as there is another bug in the\nreflog migration that will be fixed in a subsequent commit. Once that\nbug is fixed we'll make the reflog verification in t1450 stricter, and\nthat will catch both this bug here and the other bug.\n\nNote that we also add two new `name` and `mail` string buffers to the\ncallback structures and splice them through to the callbacks. This is\ndone so that we can avoid allocating a new buffer every time we compute\nthe committer information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 22 ++++++++++++++++++----\n 1 file changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 8aa9f7236a3..a5f9ffaa45d 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2954,7 +2954,7 @@ struct migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf sb;\n+\tstruct strbuf sb, name, mail;\n };\n \n static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n@@ -2993,7 +2993,7 @@ struct reflog_migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf *sb;\n+\tstruct strbuf *sb, *name, *mail;\n };\n \n static int migrate_one_reflog_entry(struct object_id *old_oid,\n@@ -3003,13 +3003,21 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \t\t\t\t    const char *msg, void *cb_data)\n {\n \tstruct reflog_migration_data *data = cb_data;\n+\tstruct ident_split ident;\n \tconst char *date;\n \tint ret;\n \n+\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n+\t\treturn -1;\n+\n+\tstrbuf_reset(data->name);\n+\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n+\tstrbuf_reset(data->mail);\n+\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n+\n \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n \tstrbuf_reset(data->sb);\n-\t/* committer contains name and email */\n-\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n+\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n@@ -3026,6 +3034,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n \t\t.transaction = migration_data->transaction,\n \t\t.errbuf = migration_data->errbuf,\n \t\t.sb = &migration_data->sb,\n+\t\t.name = &migration_data->name,\n+\t\t.mail = &migration_data->mail,\n \t};\n \n \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n@@ -3124,6 +3134,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tstruct strbuf new_gitdir = STRBUF_INIT;\n \tstruct migration_data data = {\n \t\t.sb = STRBUF_INIT,\n+\t\t.name = STRBUF_INIT,\n+\t\t.mail = STRBUF_INIT,\n \t};\n \tint did_migrate_refs = 0;\n \tint ret;\n@@ -3299,6 +3311,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tref_transaction_free(transaction);\n \tstrbuf_release(&new_gitdir);\n \tstrbuf_release(&data.sb);\n+\tstrbuf_release(&data.name);\n+\tstrbuf_release(&data.mail);\n \treturn ret;\n }\n \n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522730","messageId":"20250725-pks-reflog-append-v2-7-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:29Z","receivedAt":"2025-07-25T06:58:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\nobject ID set. If so, the value of that field is used to verify whether\nthe current state of the reference matches this expected state. It is\nthus an important part of mitigating races with a concurrent process\nthat updates the same set of references.\n\nWhen writing reflogs though we explicitly unset that flag. This is a\nsensible thing to do: the old state of reflog entry updates may not\nnecessarily match the current on-disk state of its accompanying ref, but\nit's only intended to signal what old object ID we want to write into\nthe new reflog entry. For example when migrating refs we end up writing\nmany reflog entries for a single reference, and most likely those reflog\nentries will have many different old object IDs.\n\nBut unsetting this flag also removes a useful signal, namely that the\ncaller _did_ provide an old object ID for a given reflog entry. This\nsignal will become useful in a subsequent commit, where we add a new\nflag that tells the transaction to use the provided old and new object\nIDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\nsignal to verify that the caller really did provide an old object ID.\n\nStop unsetting the flag so that we can use it as this described signal\nin a subsequent commit. Skip checking the old object ID for log-only\nupdates so that we don't expect it to match the current on-disk state.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  8 +++-----\n refs/files-backend.c    |  9 +++++----\n refs/refs-internal.h    |  3 ++-\n refs/reftable-backend.c | 12 +++---------\n 4 files changed, 13 insertions(+), 19 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex a5f9ffaa45d..f88928de746 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1393,11 +1393,6 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \tupdate = ref_transaction_add_update(transaction, refname, flags,\n \t\t\t\t\t    new_oid, old_oid, NULL, NULL,\n \t\t\t\t\t    committer_info, msg);\n-\t/*\n-\t * While we do set the old_oid value, we unset the flag to skip\n-\t * old_oid verification which only makes sense for refs.\n-\t */\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \tupdate->index = index;\n \n \t/*\n@@ -3318,6 +3313,9 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \n int ref_update_expects_existing_old_ref(struct ref_update *update)\n {\n+\tif (update->flags & REF_LOG_ONLY)\n+\t\treturn 0;\n+\n \treturn (update->flags & REF_HAVE_OLD) &&\n \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex bf6f89b1d19..8b42fe18901 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2493,7 +2493,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n \t * done when new_update is processed.\n \t */\n \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \n \treturn 0;\n }\n@@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n \t\t\t\t\t\tstruct object_id *oid,\n \t\t\t\t\t\tstruct strbuf *err)\n {\n-\tif (!(update->flags & REF_HAVE_OLD) ||\n-\t\t   oideq(oid, &update->old_oid))\n+\tif (update->flags & REF_LOG_ONLY ||\n+\t    !(update->flags & REF_HAVE_OLD) ||\n+\t    oideq(oid, &update->old_oid))\n \t\treturn 0;\n \n \tif (is_null_oid(&update->old_oid)) {\n@@ -3059,7 +3059,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n \n-\t\tif ((update->flags & REF_HAVE_OLD) &&\n+\t\tif (!(update->flags & REF_LOG_ONLY) &&\n+\t\t    (update->flags & REF_HAVE_OLD) &&\n \t\t    !is_null_oid(&update->old_oid))\n \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex f8688708519..95a4dc3902f 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -802,7 +802,8 @@ enum ref_transaction_error ref_update_check_old_target(const char *referent,\n \n /*\n  * Check if the ref must exist, this means that the old_oid or\n- * old_target is non NULL.\n+ * old_target is non NULL. Log-only updates never require the old state to\n+ * match.\n  */\n int ref_update_expects_existing_old_ref(struct ref_update *update);\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4c3817f4ec1..44af58ac50b 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1180,8 +1180,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret > 0) {\n \t\t/* The reference does not exist, but we expected it to. */\n \t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\n-\n \t\t\t\t   \"unable to resolve reference '%s'\"),\n \t\t\t    ref_update_original_update_refname(u), u->refname);\n \t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n@@ -1235,13 +1233,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \n \t\t\tnew_update->parent_update = u;\n \n-\t\t\t/*\n-\t\t\t * Change the symbolic ref update to log only. Also, it\n-\t\t\t * doesn't need to check its old OID value, as that will be\n-\t\t\t * done when new_update is processed.\n-\t\t\t */\n+\t\t\t/* Change the symbolic ref update to log only. */\n \t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\t\t\tu->flags &= ~REF_HAVE_OLD;\n \t\t}\n \t}\n \n@@ -1265,7 +1258,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\tret = ref_update_check_old_target(referent->buf, u, err);\n \t\tif (ret)\n \t\t\treturn ret;\n-\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n+\t} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&\n+\t\t   !oideq(&current_oid, &u->old_oid)) {\n \t\tif (is_null_oid(&u->old_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference already exists\"),\n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522731","messageId":"20250725-pks-reflog-append-v2-8-e4e7cbe3f578@pks.im","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im","subject":"[PATCH v2 8/8] refs: fix invalid old object IDs when migrating reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-25T06:58:30Z","receivedAt":"2025-07-25T06:58:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we end\nup with invalid old object IDs for the migrated entries: instead of\nwriting the old object ID of the to-be-migrated entry, we end up with\nthe all-zeroes object ID.\n\nThe root cause of this issue is that we don't know to use the old object\nID provided by the caller. Instead, we manually resolve the old object\nID by resolving the current value of its matching reference. But as that\nreference does not yet exist in the target ref storage we always end up\nresolving it to all-zeroes.\n\nThis issue got unnoticed as there is no user-facing command that would\neven show the old object ID. While `git log -g` knows to show the new\nobject ID, we don't have any formatting directive to show the old object\nID.\n\nFix the bug by introducing a new flag `REF_LOG_USE_PROVIDED_OIDS`. If\nset, backends are instructed to use the old and new object IDs provided\nby the caller, without doing any manual resolving. Set this flag in\n`ref_transaction_update_reflog()`.\n\nAmend our tests in t1460-refs-migrate to use our test tool to read\nreflog entries. This test tool prints out both old and new object ID of\neach reflog entry, which fixes the test gap. Furthermore it also prints\nthe full identity used to write the reflog, which provides test coverage\nfor the previous commit in this patch series that fixed the identity for\nmigrated reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  3 ++-\n refs.h                  |  9 ++++++++-\n refs/files-backend.c    | 16 +++++++++++++++-\n refs/reftable-backend.c | 14 ++++++++++++++\n t/t1421-reflog-write.sh |  4 +---\n t/t1460-refs-migrate.sh | 22 +++++++++++++++-------\n 6 files changed, 55 insertions(+), 13 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex f88928de746..946eb48941b 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1385,7 +1385,8 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \n \tassert(err);\n \n-\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF |\n+\t\tREF_LOG_USE_PROVIDED_OIDS;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\ndiff --git a/refs.h b/refs.h\nindex 253dd8f4d5d..090b4fdff4f 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -760,13 +760,20 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n  */\n #define REF_SKIP_CREATE_REFLOG (1 << 12)\n \n+/*\n+ * When writing a REF_LOG_ONLY record, use the old and new object IDs provided\n+ * in the update instead of resolving the old object ID. The caller must also\n+ * set both REF_HAVE_OLD and REF_HAVE_NEW.\n+ */\n+#define REF_LOG_USE_PROVIDED_OIDS (1 << 13)\n+\n /*\n  * Bitmask of all of the flags that are allowed to be passed in to\n  * ref_transaction_update() and friends:\n  */\n #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \\\n \t(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \\\n-\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)\n+\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | REF_LOG_USE_PROVIDED_OIDS)\n \n /*\n  * Add a reference update to transaction. `new_oid` is the value that\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 8b42fe18901..b891a326a13 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2974,6 +2974,20 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t\t\t  struct ref_lock *lock,\n \t\t\t\t  struct strbuf *err)\n {\n+\tstruct object_id *old_oid = &lock->old_oid;\n+\n+\tif (update->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(update->flags & REF_HAVE_OLD) ||\n+\t\t    !(update->flags & REF_HAVE_NEW) ||\n+\t\t    !(update->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), update->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\told_oid = &update->old_oid;\n+\t}\n+\n \tif (update->new_target) {\n \t\t/*\n \t\t * We want to get the resolved OID for the target, to ensure\n@@ -2991,7 +3005,7 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t}\n \t}\n \n-\tif (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,\n+\tif (files_log_ref_write(refs, lock->ref_name, old_oid,\n \t\t\t\t&update->new_oid, update->committer_info,\n \t\t\t\tupdate->msg, update->flags, err)) {\n \t\tchar *old_msg = strbuf_detach(err, NULL);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 44af58ac50b..99fafd75ebe 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1096,6 +1096,20 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret)\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\tif (u->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(u->flags & REF_HAVE_OLD) ||\n+\t\t    !(u->flags & REF_HAVE_NEW) ||\n+\t\t    !(u->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), u->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\tif (queue_transaction_update(refs, tx_data, u, &u->old_oid, err))\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\treturn 0;\n+\t}\n+\n \t/* Verify that the new object ID is valid. */\n \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n \t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nindex 0cd2c4f4f31..a09b5cd44e9 100755\n--- a/t/t1421-reflog-write.sh\n+++ b/t/t1421-reflog-write.sh\n@@ -89,11 +89,9 @@ test_expect_success 'simple writes' '\n \t\tEOF\n \n \t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n-\t\t# Note: the old object ID of the second reflog entry is broken.\n-\t\t# This will be fixed in subsequent commits.\n \t\ttest_reflog_matches . refs/heads/something <<-EOF\n \t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n-\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n \t\tEOF\n \t)\n '\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 2ab97e1b7df..0e1116a319d 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -7,6 +7,17 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+print_all_reflog_entries () {\n+\trepo=$1 &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n+\twhile read reflog\n+\tdo\n+\t\techo \"REFLOG: $reflog\" &&\n+\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n+\t\treturn 1\n+\tdone <reflogs\n+}\n+\n # Migrate the provided repository from one format to the other and\n # verify that the references and logs are migrated over correctly.\n # Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]\n@@ -28,8 +39,7 @@ test_migration () {\n \t\t--format='%(refname) %(objectname) %(symref)' >expect &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t   git -C \"$repo\" reflog --all >expect_logs &&\n-\t   git -C \"$repo\" reflog list >expect_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >expect_logs\n \tfi &&\n \n \tgit -C \"$repo\" refs migrate --ref-format=\"$format\" \"$@\" &&\n@@ -39,10 +49,8 @@ test_migration () {\n \ttest_cmp expect actual &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t\tgit -C \"$repo\" reflog --all >actual_logs &&\n-\t\tgit -C \"$repo\" reflog list >actual_log_list &&\n-\t\ttest_cmp expect_logs actual_logs &&\n-\t\ttest_cmp expect_log_list actual_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >actual_logs &&\n+\t\ttest_cmp expect_logs actual_logs\n \tfi &&\n \n \tgit -C \"$repo\" rev-parse --show-ref-format >actual &&\n@@ -273,7 +281,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' '\n \ttest_commit -C repo second &&\n \tprintf \"update refs/heads/ref-%d HEAD\\n\" $(test_seq 3000) >stdin &&\n \tgit -C repo update-ref --stdin <stdin &&\n-\ttest_migration repo reftable\n+\ttest_migration repo reftable true\n '\n \n test_expect_success 'migrating from files format deletes backend files' '\n\n-- \n2.50.1.565.gc32cd1483b.dirty\n\n"},{"id":"522744","messageId":"20250725113610.GA3015361@coredump.intra.peff.net","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-7-e4e7cbe3f578@pks.im","subject":"Re: [PATCH v2 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-07-25T11:36:10Z","receivedAt":"2025-07-25T11:36:12Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Jul 25, 2025 at 08:58:29AM +0200, Patrick Steinhardt wrote:\n\n> The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\n> object ID set. If so, the value of that field is used to verify whether\n> the current state of the reference matches this expected state. It is\n> thus an important part of mitigating races with a concurrent process\n> that updates the same set of references.\n> \n> When writing reflogs though we explicitly unset that flag. This is a\n> sensible thing to do: the old state of reflog entry updates may not\n> necessarily match the current on-disk state of its accompanying ref, but\n> it's only intended to signal what old object ID we want to write into\n> the new reflog entry. For example when migrating refs we end up writing\n> many reflog entries for a single reference, and most likely those reflog\n> entries will have many different old object IDs.\n> \n> But unsetting this flag also removes a useful signal, namely that the\n> caller _did_ provide an old object ID for a given reflog entry. This\n> signal will become useful in a subsequent commit, where we add a new\n> flag that tells the transaction to use the provided old and new object\n> IDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\n> signal to verify that the caller really did provide an old object ID.\n> \n> Stop unsetting the flag so that we can use it as this described signal\n> in a subsequent commit. Skip checking the old object ID for log-only\n> updates so that we don't expect it to match the current on-disk state.\n\nI like this direction, but I happened to be working in this area\nyesterday[1] and noticed something interesting. You're effectively\nreplacing this removal of the HAVE_OLD flag when split a symref update:\n\n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index bf6f89b1d19..8b42fe18901 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -2493,7 +2493,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n>  \t * done when new_update is processed.\n>  \t */\n>  \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n> -\tupdate->flags &= ~REF_HAVE_OLD;\n>  \n>  \treturn 0;\n>  }\n\nand then later we get the same logic by checking for LOG_ONLY:\n\n> @@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n>  \t\t\t\t\t\tstruct object_id *oid,\n>  \t\t\t\t\t\tstruct strbuf *err)\n>  {\n> -\tif (!(update->flags & REF_HAVE_OLD) ||\n> -\t\t   oideq(oid, &update->old_oid))\n> +\tif (update->flags & REF_LOG_ONLY ||\n> +\t    !(update->flags & REF_HAVE_OLD) ||\n> +\t    oideq(oid, &update->old_oid))\n>  \t\treturn 0;\n>  \n>  \tif (is_null_oid(&update->old_oid)) {\n\nWhich make sense to me. But the weird thing I noticed is that when we do\nsomething similar for split_head_update(), we don't strip REF_HAVE_OLD!\n\n(For those not familiar with that function, it notices when we are\nupdating refs/heads/foo that is pointed-to by HEAD, and then adds an\nextra HEAD reflog update to the transaction).\n\nSo as I understand it, right now we are doing an extra check_old_oid()\non that log-only HEAD update, and after your patch we would stop doing\nso.\n\nWhich I _think_ is the right thing to do, but it made me wonder if the\ntransaction were ever non-atomic. That is, could we split off a log-only\nupdate that succeeds, even though the old-oid check for the actual\nref fails?\n\nHistorically, I'd guess the answer is mostly \"no\", because the point of\nref transactions is to be all-or-nothing, and to do the locking and\nold-oid checking before writing out any updates. But I also think I saw\nsome discussion of non-atomic transactions recently. I didn't really\nfollow it, but is this a potential problem?\n\n-Peff\n\n[1] If you are wondering what work: it is the fact that at least with\n    the files backend, we will happily overwrite a dangling symref even\n    when the caller asked us to make sure this is a creation event. That\n    is easy to fix, but I was surprised that some HEAD updates failed\n    after doing so. The problem is that the reflog update for HEAD did\n    not clear the HAVE_OLD flag, and my solution was to do so (just like\n    split_symref_update() does). But as your topic here shows, that will\n    probably result in broken reflogs. And we should be checking for\n    LOG_ONLY in check_old_oid, as you're doing here (which would also\n    fix my problem).\n\n    But that also makes me wonder: should ref_update_check_old_target()\n    also be checking LOG_ONLY now in your patch? I guess not, as it does\n    not use HAVE_OLD at all (that is just about the oid). We get the\n    equivalent behavior in the split-off log-only transaction item\n    because we just do not set \"old_target\" in the split-off item.\n"},{"id":"522747","messageId":"xmqqjz3w5ofa.fsf@gitster.g","threadId":"63818","inReplyTo":"aIMX1nHa5E5Hn95g@pks.im","subject":"Re: [PATCH 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-25T14:35:05Z","receivedAt":"2025-07-25T14:35:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Sure, I can rebase this on top of v2.50.1. It would then of course\n> require some smallish fixes when merged to `seen`. The below patch is\n> what is required to make it work with the v2.50 track.\n>\n> Patrick\n\nThanks.\n\n\n> diff --git a/builtin/reflog.c b/builtin/reflog.c\n> index bc7e7f5e442..d3f0009cb0e 100644\n> --- a/builtin/reflog.c\n> +++ b/builtin/reflog.c\n> @@ -4,7 +4,7 @@\n>  #include \"config.h\"\n>  #include \"gettext.h\"\n>  #include \"hex.h\"\n> -#include \"odb.h\"\n> +#include \"object-store.h\"\n>  #include \"revision.h\"\n>  #include \"reachable.h\"\n>  #include \"wildmatch.h\"\n> @@ -426,13 +426,13 @@ static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n>  \tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n>  \tif (ret)\n>  \t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n> -\tif (!is_null_oid(&old_oid) && !odb_has_object(repo->objects, &old_oid, 0))\n> +\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n>  \t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n>  \n>  \tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n>  \tif (ret)\n>  \t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n> -\tif (!is_null_oid(&new_oid) && !odb_has_object(repo->objects, &new_oid, 0))\n> +\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n>  \t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n>  \n>  \tmessage = argv[3];\n"},{"id":"522852","messageId":"aIeMgE-11UnAJINI@pks.im","threadId":"63818","inReplyTo":"20250725113610.GA3015361@coredump.intra.peff.net","subject":"Re: [PATCH v2 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-28T14:43:12Z","receivedAt":"2025-07-28T14:43:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jul 25, 2025 at 07:36:10AM -0400, Jeff King wrote:\n> On Fri, Jul 25, 2025 at 08:58:29AM +0200, Patrick Steinhardt wrote:\n> \n> > The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\n> > object ID set. If so, the value of that field is used to verify whether\n> > the current state of the reference matches this expected state. It is\n> > thus an important part of mitigating races with a concurrent process\n> > that updates the same set of references.\n> > \n> > When writing reflogs though we explicitly unset that flag. This is a\n> > sensible thing to do: the old state of reflog entry updates may not\n> > necessarily match the current on-disk state of its accompanying ref, but\n> > it's only intended to signal what old object ID we want to write into\n> > the new reflog entry. For example when migrating refs we end up writing\n> > many reflog entries for a single reference, and most likely those reflog\n> > entries will have many different old object IDs.\n> > \n> > But unsetting this flag also removes a useful signal, namely that the\n> > caller _did_ provide an old object ID for a given reflog entry. This\n> > signal will become useful in a subsequent commit, where we add a new\n> > flag that tells the transaction to use the provided old and new object\n> > IDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\n> > signal to verify that the caller really did provide an old object ID.\n> > \n> > Stop unsetting the flag so that we can use it as this described signal\n> > in a subsequent commit. Skip checking the old object ID for log-only\n> > updates so that we don't expect it to match the current on-disk state.\n> \n> I like this direction, but I happened to be working in this area\n> yesterday[1] and noticed something interesting. You're effectively\n> replacing this removal of the HAVE_OLD flag when split a symref update:\n> \n> > diff --git a/refs/files-backend.c b/refs/files-backend.c\n> > index bf6f89b1d19..8b42fe18901 100644\n> > --- a/refs/files-backend.c\n> > +++ b/refs/files-backend.c\n> > @@ -2493,7 +2493,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n> >  \t * done when new_update is processed.\n> >  \t */\n> >  \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n> > -\tupdate->flags &= ~REF_HAVE_OLD;\n> >  \n> >  \treturn 0;\n> >  }\n> \n> and then later we get the same logic by checking for LOG_ONLY:\n> \n> > @@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n> >  \t\t\t\t\t\tstruct object_id *oid,\n> >  \t\t\t\t\t\tstruct strbuf *err)\n> >  {\n> > -\tif (!(update->flags & REF_HAVE_OLD) ||\n> > -\t\t   oideq(oid, &update->old_oid))\n> > +\tif (update->flags & REF_LOG_ONLY ||\n> > +\t    !(update->flags & REF_HAVE_OLD) ||\n> > +\t    oideq(oid, &update->old_oid))\n> >  \t\treturn 0;\n> >  \n> >  \tif (is_null_oid(&update->old_oid)) {\n> \n> Which make sense to me. But the weird thing I noticed is that when we do\n> something similar for split_head_update(), we don't strip REF_HAVE_OLD!\n\nAnd we shouldn't do that, as in the next commit we actually build on\nalways having `REF_HAVE_OLD` set for reflog-only updates. So I'd argue\nthat the problem is actually the other way round: when splitting off the\nHEAD update we must resolve the old object ID if `REF_HAVE_OLD` is not\nset.\n\n> (For those not familiar with that\n"},{"id":"522858","messageId":"6414dbfd-6f34-48d1-aa3f-3fe7998f80af@app.fastmail.com","threadId":"63818","inReplyTo":"20250725-pks-reflog-append-v2-4-e4e7cbe3f578@pks.im","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-07-28T15:33:32Z","receivedAt":"2025-07-28T15:34:24Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Fri, Jul 25, 2025, at 08:58, Patrick Steinhardt wrote:\n> While we provide a couple of subcommands in git-reflog(1) to remove\n> reflog entries, we don't provide any to write new entries. Obviously\n> this is not an operation that really would be needed for many use cases\n> out there, or otherwise people would have complained that such a command\n> does not exist yet.\n\nThis command will allow you to write a simpler unique marker (without\nhaving to make a marker-ref for a ref) that can be used to find back to\na specific point.\n\nI’ve had some use for that.  I used git-update-ref(1) for that because\nof `-m` (as well as plumbing-for-scripting).\n\n> diff --git a/Documentation/git-reflog.adoc\n> b/Documentation/git-reflog.adoc\n> index c3801b82fb6..c8389810273 100644\n> --- a/Documentation/git-reflog.adoc\n> +++ b/Documentation/git-reflog.adoc\n> @@ -12,6 +12,7 @@ SYNOPSIS\n>  git reflog [show] [<log-options>] [<ref>]\n>  git reflog list\n>  git reflog exists <ref>\n> +git reflog write <ref> <old-oid> <new-oid> <message>\n>  git reflog delete [--rewrite] [--updateref]\n>  \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n>  git reflog drop [--all [--single-worktree] | <refs>...]\n> @@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a\n> reflog.  It exits\n>  with zero status if the reflog exists, and non-zero status if it does\n>  not.\n>\n> +The \"write\" subcommand writes a single entry to the reflog of a given\n> +reference. This new entry is appended to the reflog and will thus become\n> +the most recent entry. Both the old and new object IDs must not be\n> +abbreviated and must point to existing objects. The reflog message gets\n> +normalized.\n> +\n\nYou have to give the full refname to this subcommand.  `git reflog write\n... branch <msg>` will update the reflog for the one-level ref `branch`.\nBut I’m used to using git-reflog(1) with a name like `branch` and it\nusing `refs/heads/branch` if it exists.  At least that’s how the default\n`git reflog show` behaves.\n\nWhich means that\n\n    git reflog write ... refs/heads/branch <msg>\n    git reflog branch\n\nWill show that written reflog.\n\nWhereas this\n\n    git reflog write ... branch <msg>\n    git reflog branch\n\nWill show one entry since `branch` is the one-level ref `branch`, not\n`refs/heads/branch`.  Now it looks like `write` truncated the reflog and\nwrote a new reflog message (if you mistakenly think that `branch` is a\nbranch).\n\nIt isn’t clear to me how the current doc guides me in the correct\ndirection here.\n\nI tried `git reflog drop`[1] and it can deal with a branch like\n`branch`.  It doesn’t need to be told `refs/heads/branch`.\n\n>  The \"delete\" subcommand deletes single entries from the reflog, but\n>  not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n>  reflog delete master@{2}`\"). This subcommand is also typically not used\n"},{"id":"522872","messageId":"xmqqo6t4up5f.fsf@gitster.g","threadId":"63818","inReplyTo":"6414dbfd-6f34-48d1-aa3f-3fe7998f80af@app.fastmail.com","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-28T18:49:16Z","receivedAt":"2025-07-28T18:49:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> I tried `git reflog drop`[1] and it can deal with a branch like\n> `branch`.  It doesn’t need to be told `refs/heads/branch`.\n\nThat sounds like a bug to me.\n"},{"id":"522895","messageId":"CAOLa=ZS6e9dv-c+tajooZa5qFZT2T+jqaJahd3BzVuE7H=5PwA@mail.gmail.com","threadId":"63818","inReplyTo":"xmqqo6t4up5f.fsf@gitster.g","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-07-28T20:39:05Z","receivedAt":"2025-07-28T20:39:06Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n>\n>> I tried `git reflog drop`[1] and it can deal with a branch like\n>> `branch`.  It doesn’t need to be told `refs/heads/branch`.\n>\n> That sounds like a bug to me.\n\nSo `git reflog drop` `git reflog delete` and `git reflog expire` use\n`repo_dwim_log()` to resolve the provided reference.\n\nAnd `repo_dwim_log()` uses the following `ref_rev_parse_rules` to\nresolve the reference.\n\n  static const char *ref_rev_parse_rules[] = {\n  \t\"%.*s\",\n  \t\"refs/%.*s\",\n  \t\"refs/tags/%.*s\",\n  \t\"refs/heads/%.*s\",\n  \t\"refs/remotes/%.*s\",\n  \t\"refs/remotes/%.*s/HEAD\",\n  \tNULL\n  };\n\nWhich means we do a best case resolution of a given reference, but the\nfunction also checks for ambiguity and warns for it.\n"},{"id":"522901","messageId":"xmqqms8ot4jh.fsf@gitster.g","threadId":"63818","inReplyTo":"CAOLa=ZS6e9dv-c+tajooZa5qFZT2T+jqaJahd3BzVuE7H=5PwA@mail.gmail.com","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-28T20:59:46Z","receivedAt":"2025-07-28T20:59:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> \"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n>>\n>>> I tried `git reflog drop`[1] and it can deal with a branch like\n>>> `branch`.  It doesn’t need to be told `refs/heads/branch`.\n>>\n>> That sounds like a bug to me.\n>\n> So `git reflog drop` `git reflog delete` and `git reflog expire` use\n> `repo_dwim_log()` to resolve the provided reference.\n>\n> And `repo_dwim_log()` uses the following `ref_rev_parse_rules` to\n> resolve the reference.\n>\n>   static const char *ref_rev_parse_rules[] = {\n>   \t\"%.*s\",\n>   \t\"refs/%.*s\",\n>   \t\"refs/tags/%.*s\",\n>   \t\"refs/heads/%.*s\",\n>   \t\"refs/remotes/%.*s\",\n>   \t\"refs/remotes/%.*s/HEAD\",\n>   \tNULL\n>   };\n>\n> Which means we do a best case resolution of a given reference, but the\n> function also checks for ambiguity and warns for it.\n\nTrue.  But as I considered \"git reflog\" to be a lot closer to the\nplumbing than to Porcelain, using the dwim thing smelled like a bug.\n\nIt also is OK to update the commands that do not use dwim-log to\nalso use it.  That way, the result would be consistent across\nsubcommands of \"git reflog\".  As long as the users are aware of the\nfact that the command uses dwim-log, they can always spell their ref\nin full like \"refs/heads/branch\" to avoid ambiguity check getting in\nthe way.\n\nThanks.\n\n\n"},{"id":"522907","messageId":"6E84D571-CC02-45E6-9E58-DD918B45998E@gmail.com","threadId":"63818","inReplyTo":"6414dbfd-6f34-48d1-aa3f-3fe7998f80af@app.fastmail.com","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-07-29T00:25:43Z","receivedAt":"2025-07-29T00:25:55Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 28 juil. 2025 à 11:37, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> a écrit :\n> \n> ﻿On Fri, Jul 25, 2025, at 08:58, Patrick Steinhardt wrote:\n>> While we provide a couple of subcommands in git-reflog(1) to remove\n>> reflog entries, we don't provide any to write new entries. Obviously\n>> this is not an operation that really would be needed for many use cases\n>> out there, or otherwise people would have complained that such a command\n>> does not exist yet.\n> \n> This command will allow you to write a simpler unique marker (without\n> having to make a marker-ref for a ref) that can be used to find back to\n> a specific point.\n> \n> I’ve had some use for that.  I used git-update-ref(1) for that because\n> of `-m` (as well as plumbing-for-scripting).\n> \n>> diff --git a/Documentation/git-reflog.adoc\n>> b/Documentation/git-reflog.adoc\n>> index c3801b82fb6..c8389810273 100644\n>> --- a/Documentation/git-reflog.adoc\n>> +++ b/Documentation/git-reflog.adoc\n>> @@ -12,6 +12,7 @@ SYNOPSIS\n>> git reflog [show] [<log-options>] [<ref>]\n>> git reflog list\n>> git reflog exists <ref>\n>> +git reflog write <ref> <old-oid> <new-oid> <message>\n>> git reflog delete [--rewrite] [--updateref]\n>>   [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n>> git reflog drop [--all [--single-worktree] | <refs>...]\n>> @@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a\n>> reflog.  It exits\n>> with zero status if the reflog exists, and non-zero status if it does\n>> not.\n>> +The \"write\" subcommand writes a single entry to the reflog of a given\n>> +reference. This new entry is appended to the reflog and will thus become\n>> +the most recent entry. Both the old and new object IDs must not be\n>> +abbreviated and must point to existing objects. The reflog message gets\n>> +normalized.\n>> +\n> \n> You have to give the full refname to this subcommand.  `git reflog write\n> ... branch <msg>` will update the reflog for the one-level ref `branch`.\n> But I’m used to using git-reflog(1) with a name like `branch` and it\n> using `refs/heads/branch` if it exists.  At least that’s how the default\n> `git reflog show` behaves.\n> \n> Which means that\n> \n>   git reflog write ... refs/heads/branch <msg>\n>   git reflog branch\n> \n> Will show that written reflog.\n> \n> Whereas this\n> \n>   git reflog write ... branch <msg>\n>   git reflog branch\n> \n> Will show one entry since `branch` is the one-level ref `branch`, not\n> `refs/heads/branch`.  Now it looks like `write` truncated the reflog and\n> wrote a new reflog message (if you mistakenly think that `branch` is a\n> branch).\n\nThis quirk of update-ref bit me the first few times I used it, too. I think it’s at least documented there though.\n\n> \n> It isn’t clear to me how the current doc guides me in the correct\n> direction here.\n> \n> I tried `git reflog drop`[1] and it can deal with a branch like\n> `branch`.  It doesn’t need to be told `refs/heads/branch`.\n\n(Partly responding to comments about what to do with this) I think consistency would be best, and since “git reflog branch” is not abnormal we should continue to allow that. \n\n> \n>> The \"delete\" subcommand deletes single entries from the reflog, but\n>> not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n>> reflog delete master@{2}`\"). This subcommand is also typically not used\n"},{"id":"522917","messageId":"6cb4522b-43f0-4b0b-9dbf-4f10c8c94487@app.fastmail.com","threadId":"63818","inReplyTo":"6E84D571-CC02-45E6-9E58-DD918B45998E@gmail.com","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-07-29T06:14:59Z","receivedAt":"2025-07-29T06:15:21Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Jul 29, 2025, at 02:25, Ben Knoble wrote:\n>> ...\n>> Will show one entry since `branch` is the one-level ref `branch`, not\n>> `refs/heads/branch`.  Now it looks like `write` truncated the reflog and\n>> wrote a new reflog message (if you mistakenly think that `branch` is a\n>> branch).\n>\n> This quirk of update-ref bit me the first few times I used it, too. I\n> think it’s at least documented there though.\n\nI do that with update-ref more than I care to commit^W admit.  But\nthat’s consistent with the command and well-documented.\n"},{"id":"522919","messageId":"aIhvcSHrRLG6wcMD@pks.im","threadId":"63818","inReplyTo":"6E84D571-CC02-45E6-9E58-DD918B45998E@gmail.com","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T06:51:29Z","receivedAt":"2025-07-29T06:51:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Jul 28, 2025 at 08:25:43PM -0400, Ben Knoble wrote:\n> > Le 28 juil. 2025 à 11:37, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> a écrit :\n> > ﻿On Fri, Jul 25, 2025, at 08:58, Patrick Steinhardt wrote:\n> >> diff --git a/Documentation/git-reflog.adoc\n> >> b/Documentation/git-reflog.adoc\n> >> index c3801b82fb6..c8389810273 100644\n> >> --- a/Documentation/git-reflog.adoc\n> >> +++ b/Documentation/git-reflog.adoc\n> >> @@ -12,6 +12,7 @@ SYNOPSIS\n> >> git reflog [show] [<log-options>] [<ref>]\n> >> git reflog list\n> >> git reflog exists <ref>\n> >> +git reflog write <ref> <old-oid> <new-oid> <message>\n> >> git reflog delete [--rewrite] [--updateref]\n> >>   [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n> >> git reflog drop [--all [--single-worktree] | <refs>...]\n> >> @@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a\n> >> reflog.  It exits\n> >> with zero status if the reflog exists, and non-zero status if it does\n> >> not.\n> >> +The \"write\" subcommand writes a single entry to the reflog of a given\n> >> +reference. This new entry is appended to the reflog and will thus become\n> >> +the most recent entry. Both the old and new object IDs must not be\n> >> +abbreviated and must point to existing objects. The reflog message gets\n> >> +normalized.\n> >> +\n> > \n> > You have to give the full refname to this subcommand.  `git reflog write\n> > ... branch <msg>` will update the reflog for the one-level ref `branch`.\n> > But I’m used to using git-reflog(1) with a name like `branch` and it\n> > using `refs/heads/branch` if it exists.  At least that’s how the default\n> > `git reflog show` behaves.\n> > \n> > Which means that\n> > \n> >   git reflog write ... refs/heads/branch <msg>\n> >   git reflog branch\n> > \n> > Will show that written reflog.\n> > \n> > Whereas this\n> > \n> >   git reflog write ... branch <msg>\n> >   git reflog branch\n> > \n> > Will show one entry since `branch` is the one-level ref `branch`, not\n> > `refs/heads/branch`.  Now it looks like `write` truncated the reflog and\n> > wrote a new reflog message (if you mistakenly think that `branch` is a\n> > branch).\n> \n> This quirk of update-ref bit me the first few times I used it, too. I\n> think it’s at least documented there though.\n> \n> > \n> > It isn’t clear to me how the current doc guides me in the correct\n> > direction here.\n> > \n> > I tried `git reflog drop`[1] and it can deal with a branch like\n> > `branch`.  It doesn’t need to be told `refs/heads/branch`.\n> \n> (Partly responding to comments about what to do with this) I think\n> consistency would be best, and since “git reflog branch” is not\n> abnormal we should continue to allow that. \n\nThere's a big difference though: `git reflog drop` won't ever do\nanything for a reflog that doesn't exist. Consequently, we know that our\nDWIM mechanism can kick in and resolve the reference properly if such a\nreflog exists.\n\nBut for `git reflog write` that's not the case, as you can write a\nreflog message for a yet-nonexistent reflog. The DWIM mechanism cannot\nkick in here as there is no reflog. So what do we do in that case? We\ncould of course just pick the first DWIM rule, which would be that we\ndecide to write the reflog for \"refs/heads/$REFNAME\". But... I dunno,\nthat feels too magicky to m\n"},{"id":"522926","messageId":"20250729071455.GB1426859@coredump.intra.peff.net","threadId":"63818","inReplyTo":"aIeMgE-11UnAJINI@pks.im","subject":"Re: [PATCH v2 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-07-29T07:14:55Z","receivedAt":"2025-07-29T07:14:57Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Jul 28, 2025 at 04:43:12PM +0200, Patrick Steinhardt wrote:\n\n> > > @@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n> > >  \t\t\t\t\t\tstruct object_id *oid,\n> > >  \t\t\t\t\t\tstruct strbuf *err)\n> > >  {\n> > > -\tif (!(update->flags & REF_HAVE_OLD) ||\n> > > -\t\t   oideq(oid, &update->old_oid))\n> > > +\tif (update->flags & REF_LOG_ONLY ||\n> > > +\t    !(update->flags & REF_HAVE_OLD) ||\n> > > +\t    oideq(oid, &update->old_oid))\n> > >  \t\treturn 0;\n> > >  \n> > >  \tif (is_null_oid(&update->old_oid)) {\n> > \n> > Which make sense to me. But the weird thing I noticed is that when we do\n> > something similar for split_head_update(), we don't strip REF_HAVE_OLD!\n> \n> And we shouldn't do that, as in the next commit we actually build on\n> always having `REF_HAVE_OLD` set for reflog-only updates. So I'd argue\n> that the problem is actually the other way round: when splitting off the\n> HEAD update we must resolve the old object ID if `REF_HAVE_OLD` is not\n> set.\n\nYeah, I agree that after your patches, split_head_update() should\ndefinitely not be clearing that flag. What I more meant was: this patch\nis introducing a behavior change for those split HEAD updates, which\nused to do the extra old-oid check but now won't (whereas for other\nsymref log-only updates, you are preserving the behavior).\n\nI _think_ that's a reasonable thing, but I wanted to make sure.\n\nHowever...\n\n> > (For those not familiar with that\n\n...did you mean to write more? I know you've been running into weird\nemail truncation issues lately.\n\n-Peff\n"},{"id":"522930","messageId":"aIh-UWjs_092Dzdm@pks.im","threadId":"63818","inReplyTo":"20250729071455.GB1426859@coredump.intra.peff.net","subject":"Re: [PATCH v2 7/8] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T07:54:57Z","receivedAt":"2025-07-29T07:55:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Jul 29, 2025 at 03:14:55AM -0400, Jeff King wrote:\n> On Mon, Jul 28, 2025 at 04:43:12PM +0200, Patrick Steinhardt wrote:\n> > > > @@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n> > > >  \t\t\t\t\t\tstruct object_id *oid,\n> > > >  \t\t\t\t\t\tstruct strbuf *err)\n> > > >  {\n> > > > -\tif (!(update->flags & REF_HAVE_OLD) ||\n> > > > -\t\t   oideq(oid, &update->old_oid))\n> > > > +\tif (update->flags & REF_LOG_ONLY ||\n> > > > +\t    !(update->flags & REF_HAVE_OLD) ||\n> > > > +\t    oideq(oid, &update->old_oid))\n> > > >  \t\treturn 0;\n> > > >  \n> > > >  \tif (is_null_oid(&update->old_oid)) {\n> > > \n> > > Which make sense to me. But the weird thing I noticed is that when we do\n> > > something similar for split_head_update(), we don't strip REF_HAVE_OLD!\n> > \n> > And we shouldn't do that, as in the next commit we actually build on\n> > always having `REF_HAVE_OLD` set for reflog-only updates. So I'd argue\n> > that the problem is actually the other way round: when splitting off the\n> > HEAD update we must resolve the old object ID if `REF_HAVE_OLD` is not\n> > set.\n> \n> Yeah, I agree that after your patches, split_head_update() should\n> definitely not be clearing that flag. What I more meant was: this patch\n> is introducing a behavior change for those split HEAD updates, which\n> used to do the extra old-oid check but now won't (whereas for other\n> symref log-only updates, you are preserving the behavior).\n> \n> I _think_ that's a reasonable thing, but I wanted to make sure.\n> \n> However...\n> \n> > > (For those not familiar with that\n> \n> ...did you mean to write more? I know you've been running into weird\n> email truncation issues lately.\n\nSigh. Yes. I really need to figure this out, but I have no clue\nwhatsoever where to look.\n\nAnyway, here's the remainder of that mail:\n\nOn Fri, Jul 25, 2025 at 07:36:10AM -0400, Jeff King wrote:\n> On Fri, Jul 25, 2025 at 08:58:29AM +0200, Patrick Steinhardt wrote:\n> > @@ -2508,8 +2507,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n> (For those not familiar with that function, it notices when we are\n> updating refs/heads/foo that is pointed-to by HEAD, and then adds an\n> extra HEAD reflog update to the transaction).\n> \n> So as I understand it, right now we are doing an extra check_old_oid()\n> on that log-only HEAD update, and after your patch we would stop doing\n> so.\n> \n> Which I _think_ is the right thing to do, but it made me wonder if the\n> transaction were ever non-atomic. That is, could we split off a log-only\n> update that succeeds, even though the old-oid check for the actual\n> ref fails?\n\nI think this can only happen the other way round: the log update never\ngets persisted unless the parent ref is, as we'd otherwise abort. But\nwhat can happen is that we end up with a broken reflog entry. See below.\n\n> Historically, I'd guess the answer is mostly \"no\", because the point of\n> ref transactions is to be all-or-nothing, and to do the locking and\n> old-oid checking before writing out any updates. But I also think I saw\n> some discussion of non-atomic transactions recently. I didn't really\n> follow it, but is this a potential problem?\n\nI'd say that the whole logic has always been flawed: we resolve the\ntarget that \"HEAD\" points to without locking the reference. Consequently\nwe have a race in case \"HEAD\" got updated to point somewhere else, as\nwe'd still write a reflog entry to \"HEAD\".\n\nWhat used to save us a bit is that at least the old object ID would be\ncorrect in such a case because we used to verify it even for log-only\nupdates. But whether or not we write such a reflog message in the first\nplace is subject to a race. And when `REF_HAVE_OLD` wasn't set we might\neven end up writing a different old object ID than what we write to the\ntarget reference.\n\nTheoretically speaking we'd have to lock \"HEAD\" immediately after we\nhave resolved it to ensure that it doesn't change anymore. But that lock\nwould be quite restrictive.\n\nI guess the next-best thing that we can do is to lock \"HEAD\" as soon as\nwe find the ref that it's pointing to. If so, we can re-evaluate whether\n\"HEAD\" still points to the same ref -- and if so, we split off the\nupdate for the reflog. If it doesn't anymore then all bets are off, as\nit may be the case that \"HEAD\" has now been changed to point to a ref\nthat has already been processed by us.\n\nI guess the safest bet would be to just abort the whole transaction in\nthat case? After all it is a racy update, but it feels heavy-handed to\nreject the whole transaction only because we fail to write a reflog\nentry.\n\nBut even that doesn't solve this race completely: \"HEAD\" might have been\nunborn at the start of a transaction or refer to a target ref that isn't\nupdated as part of the ref. So we wouldn't ever get to re-resolving the\nref. We could double check at the end of the transaction whether \"HEAD\"\nhas changed, but that isn't really working either as its target ref may\nhave flip-flopped.\n\nIn any case, I think we can improve the situation at least a bit:\n\n  - We lock the parent update before calling `split_head_update()`. This\n    ensures the old object ID is resolved already and cannot change\n    anymore.\n\n  - In `split_head_update()` we take in the parent lock as a parameter.\n    If `REF_HAVE_OLD` is unset we take the old object ID from the parent\n    lock and set `REF_HAVE_OLD` for the reflog entry. This ensures that\n    we at least use the same old ID for both ref and reflog updates.\n\nBut the other race, that \"HEAD\" may have changed concurrently... I don't\nthink that one can be plugged without a bigger effort.\n\nPatrick\n"},{"id":"522936","messageId":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH v3 0/9] refs: fix migration of reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:18Z","receivedAt":"2025-07-29T08:55:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nafter the announcement that \"reftable\" will become the default backend\nin Git 3.0 I've revived the efforts to implement this backend in\nlibgit2. I'm happy to report that this implementation is almost done by\nnow: out of 3000 tests only four are failing now.\n\nFor two of these tests I have been completely puzzled why those are\nfailing, as everything really looked perfectly fine in libgit2. As it\nturned out, the bug wasn't in libgit2 though, but in Git. Namely, the\nway we migrate reflog entries between storage formats is broken in two\nways:\n\n  - The identity we write into the reflog entries is wrong.\n\n  - The old commit ID of reflog entries is always set to all-zeroes.\n    This is what caused the libgit2 tests to fail, as I used `git refs\n    migrate` to convert test repositories to use reftables.\n\nThis patch series fixes both of these issues. Furthermore, it also adds\na new `git reflog write` subcommand to write new reflog entries for a\nspecific reference. This command was helpful to reproduce some test\nconstellations in libgit2.\n\nChanges in v2:\n  - !!! The base of this topic has changed so that it sits on top of\n    v2.50.1. This is done so that we can backport this change to older\n    release tracks.\n  - A couple of typo fixes and clarifications for commit messages.\n  - Reorder sections in git-reflog(1) manpage according to the\n    reordering we have in the synopsis.\n  - Add a section for the new `write` command.\n  - Improve test coverage for the `git reflog write` command.\n  - Avoid `cat`ing a file into a Bash loop.\n  - Remove a stale comment.\n  - Make `ref_update_expects_existing_old_ref()` a bit more straight\n    forward.\n  - Link to v1: https://lore.kernel.org/r/20250722-pks-reflog-append-v1-0-183e5949de16@pks.im\n\nChanges in v3:\n  - `git reflog write` now requires fully-qualified refnames.\n  - A new commit that plugs one part of the race around splitting of\n    reflogs for HEAD in the \"files\" backend.\n  - Link to v2: https://lore.kernel.org/r/20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (9):\n      Documentation/git-reflog: convert to use synopsis type\n      builtin/reflog: improve grouping of subcommands\n      refs: export `ref_transaction_update_reflog()`\n      builtin/reflog: implement subcommand to write new entries\n      ident: fix type of string length parameter\n      refs: fix identity for migrated reflogs\n      refs/files: detect race when generating reflog entry for HEAD\n      refs: stop unsetting REF_HAVE_OLD for log-only updates\n      refs: fix invalid old object IDs when migrating reflogs\n\n Documentation/git-reflog.adoc |  76 ++++++++++++++------------\n builtin/reflog.c              | 103 ++++++++++++++++++++++++++++-------\n ident.c                       |   2 +-\n ident.h                       |   2 +-\n refs.c                        |  60 +++++++++++---------\n refs.h                        |  24 +++++++-\n refs/files-backend.c          |  65 +++++++++++++++++++---\n refs/refs-internal.h          |   3 +-\n refs/reftable-backend.c       |  26 ++++++---\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 124 ++++++++++++++++++++++++++++++++++++++++++\n t/t1460-refs-migrate.sh       |  22 +++++---\n 12 files changed, 401 insertions(+), 107 deletions(-)\n\nRange-diff versus v2:\n\n 1:  65f4647df02 =  1:  027ac6d12f3 Documentation/git-reflog: convert to use synopsis type\n 2:  e53a402a88d =  2:  1570cac0cb9 builtin/reflog: improve grouping of subcommands\n 3:  4d060861f50 =  3:  af43c907fa0 refs: export `ref_transaction_update_reflog()`\n 4:  ddd471f9891 !  4:  4322f98fcdd builtin/reflog: implement subcommand to write new entries\n    @@ Documentation/git-reflog.adoc: The \"exists\" subcommand checks whether a ref has\n      \n     +The \"write\" subcommand writes a single entry to the reflog of a given\n     +reference. This new entry is appended to the reflog and will thus become\n    -+the most recent entry. Both the old and new object IDs must not be\n    -+abbreviated and must point to existing objects. The reflog message gets\n    -+normalized.\n    ++the most recent entry. The reference name must be fully qualified. Both the old\n    ++and new object IDs must not be abbreviated and must point to existing objects.\n    ++The reflog message gets normalized.\n     +\n      The \"delete\" subcommand deletes single entries from the reflog, but\n      not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n    @@ builtin/reflog.c: static int cmd_reflog_drop(int argc, const char **argv, const\n     +\t\tusage_with_options(reflog_write_usage, options);\n     +\n     +\tref = argv[0];\n    -+\tif (check_refname_format(ref, REFNAME_ALLOW_ONELEVEL))\n    ++\tif (!is_root_ref(ref) && check_refname_format(ref, 0))\n     +\t\tdie(_(\"invalid reference name: %s\"), ref);\n     +\n     +\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n    @@ t/t1421-reflog-write.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'unqualified refname is rejected' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_must_fail git reflog write unqualified $ZERO_OID $ZERO_OID first 2>err &&\n    ++\t\ttest_grep \"invalid reference name: \" err\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'nonexistent object IDs' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n    @@ t/t1421-reflog-write.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'can write to root ref' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit initial &&\n    ++\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n    ++\n    ++\t\tgit reflog write ROOT_REF_HEAD $ZERO_OID $COMMIT_OID first &&\n    ++\t\ttest_reflog_matches . ROOT_REF_HEAD <<-EOF\n    ++\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n    ++\t\tEOF\n    ++\t)\n    ++'\n    ++\n     +test_done\n 5:  67028ef4439 =  5:  66de5312e83 ident: fix type of string length parameter\n 6:  a6bf88a4e89 =  6:  2b9fe08cf76 refs: fix identity for migrated reflogs\n -:  ----------- >  7:  7f87327e17c refs/files: detect race when generating reflog entry for HEAD\n 7:  71b0f753dd3 =  8:  792a1d7ce61 refs: stop unsetting REF_HAVE_OLD for log-only updates\n 8:  2d88a1e57b8 =  9:  121630b9d64 refs: fix invalid old object IDs when migrating reflogs\n\n---\nbase-commit: d82adb61ba2fd11d8f2587fca1b6bd7925ce4044\nchange-id: 20250722-pks-reflog-append-634172d8ab2c\n\n"},{"id":"522937","messageId":"20250729-pks-reflog-append-v3-1-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 1/9] Documentation/git-reflog: convert to use synopsis type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:19Z","receivedAt":"2025-07-29T08:55:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\nhave introduced a new synopsis type that simplifies the rules for\ntypesetting a command's synopsis. Convert the git-reflog(1)\ndocumentation to use it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 14 +++++++-------\n 1 file changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 412f06b8fec..707a9b39edb 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n \n SYNOPSIS\n --------\n-[verse]\n-'git reflog' [show] [<log-options>] [<ref>]\n-'git reflog list'\n-'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n+[synopsis]\n+git reflog [show] [<log-options>] [<ref>]\n+git reflog list\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n \t[--rewrite] [--updateref] [--stale-fix]\n \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n-'git reflog delete' [--rewrite] [--updateref]\n+git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n-'git reflog drop' [--all [--single-worktree] | <refs>...]\n-'git reflog exists' <ref>\n+git reflog drop [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n \n DESCRIPTION\n -----------\n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522938","messageId":"20250729-pks-reflog-append-v3-2-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 2/9] builtin/reflog: improve grouping of subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:20Z","receivedAt":"2025-07-29T08:55:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The way subcommands of git-reflog(1) are laid out does not make any\nimmediate sense. Reorder them such that read-only subcommands precede\nwriting commands for a bit more structure.\n\nFurthermore, move the \"expire\" subcommand last. This prepares for a\nsubsequent change where we are about to introduce a new \"write\" command\nto append reflog entries. Like this, the writing subcommands are ordered\nsuch that those affecting a single reflog come before those spanning\nacross all reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 61 ++++++++++++++++++++++---------------------\n builtin/reflog.c              | 38 +++++++++++++--------------\n 2 files changed, 50 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 707a9b39edb..c3801b82fb6 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -11,13 +11,13 @@ SYNOPSIS\n [synopsis]\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n-git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n-\t[--rewrite] [--updateref] [--stale-fix]\n-\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n-git reflog exists <ref>\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n+\t[--rewrite] [--updateref] [--stale-fix]\n+\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,9 @@ actions, and in addition the `HEAD` reflog records branch switching.\n \n The \"list\" subcommand lists all refs which have a corresponding reflog.\n \n-The \"expire\" subcommand prunes older reflog entries. Entries older\n-than `expire` time, or entries older than `expire-unreachable` time\n-and not reachable from the current tip, are removed from the reflog.\n-This is typically not used directly by end users -- instead, see\n-linkgit:git-gc[1].\n+The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n+with zero status if the reflog exists, and non-zero status if it does\n+not.\n \n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n@@ -58,9 +56,11 @@ The \"drop\" subcommand completely removes the reflog for the specified\n references. This is in contrast to \"expire\" and \"delete\", both of which\n can be used to delete reflog entries, but not the reflog itself.\n \n-The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n-with zero status if the reflog exists, and non-zero status if it does\n-not.\n+The \"expire\" subcommand prunes older reflog entries. Entries older\n+than `expire` time, or entries older than `expire-unreachable` time\n+and not reachable from the current tip, are removed from the reflog.\n+This is typically not used directly by end users -- instead, see\n+linkgit:git-gc[1].\n \n OPTIONS\n -------\n@@ -71,6 +71,25 @@ Options for `show`\n `git reflog show` accepts any of the options accepted by `git log`.\n \n \n+Options for `delete`\n+~~~~~~~~~~~~~~~~~~~~\n+\n+`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n+`--dry-run`, and `--verbose`, with the same meanings as when they are\n+used with `expire`.\n+\n+Options for `drop`\n+~~~~~~~~~~~~~~~~~~\n+\n+--all::\n+\tDrop the reflogs of all references from all worktrees.\n+\n+--single-worktree::\n+\tBy default when `--all` is specified, reflogs from all working\n+\ttrees are dropped. This option limits the processing to reflogs\n+\tfrom the current working tree only.\n+\n+\n Options for `expire`\n ~~~~~~~~~~~~~~~~~~~~\n \n@@ -130,24 +149,6 @@ which didn't protect objects referred to by reflogs.\n \tPrint extra information on screen.\n \n \n-Options for `delete`\n-~~~~~~~~~~~~~~~~~~~~\n-\n-`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n-`--dry-run`, and `--verbose`, with the same meanings as when they are\n-used with `expire`.\n-\n-Options for `drop`\n-~~~~~~~~~~~~~~~~~~\n-\n---all::\n-\tDrop the reflogs of all references from all worktrees.\n-\n---single-worktree::\n-\tBy default when `--all` is specified, reflogs from all working\n-\ttrees are dropped. This option limits the processing to reflogs\n-\tfrom the current working tree only.\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 3acaf3e32c2..b00b3f9edc9 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -17,21 +17,21 @@\n #define BUILTIN_REFLOG_LIST_USAGE \\\n \tN_(\"git reflog list\")\n \n-#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n-\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n-\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n-\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+#define BUILTIN_REFLOG_EXISTS_USAGE \\\n+\tN_(\"git reflog exists <ref>\")\n \n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n \n-#define BUILTIN_REFLOG_EXISTS_USAGE \\\n-\tN_(\"git reflog exists <ref>\")\n-\n #define BUILTIN_REFLOG_DROP_USAGE \\\n \tN_(\"git reflog drop [--all [--single-worktree] | <refs>...]\")\n \n+#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n+\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n+\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n+\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+\n static const char *const reflog_show_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tNULL,\n@@ -42,9 +42,9 @@ static const char *const reflog_list_usage[] = {\n \tNULL,\n };\n \n-static const char *const reflog_expire_usage[] = {\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n-\tNULL\n+static const char *const reflog_exists_usage[] = {\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tNULL,\n };\n \n static const char *const reflog_delete_usage[] = {\n@@ -52,23 +52,23 @@ static const char *const reflog_delete_usage[] = {\n \tNULL\n };\n \n-static const char *const reflog_exists_usage[] = {\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n-\tNULL,\n-};\n-\n static const char *const reflog_drop_usage[] = {\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tNULL,\n };\n \n+static const char *const reflog_expire_usage[] = {\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tNULL\n+};\n+\n static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n \tNULL\n };\n \n@@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n-\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n-\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522939","messageId":"20250729-pks-reflog-append-v3-3-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 3/9] refs: export `ref_transaction_update_reflog()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:21Z","receivedAt":"2025-07-29T08:55:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we'll add another user that wants to write reflog\nentries. This requires them to call `ref_transaction_update_reflog()`,\nbut that function is local to \"refs.c\".\n\nExport the function to prepare for the change. While at it, drop the\n`flags` field, as all callers are for now expected to use the same flags\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 29 +++++++++++------------------\n refs.h | 15 +++++++++++++++\n 2 files changed, 26 insertions(+), 18 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex dce5c49ca2b..8aa9f7236a3 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1371,27 +1371,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \treturn 0;\n }\n \n-/*\n- * Similar to`ref_transaction_update`, but this function is only for adding\n- * a reflog update. Supports providing custom committer information. The index\n- * field can be utiltized to order updates as desired. When not used, the\n- * updates default to being ordered by refname.\n- */\n-static int ref_transaction_update_reflog(struct ref_transaction *transaction,\n-\t\t\t\t\t const char *refname,\n-\t\t\t\t\t const struct object_id *new_oid,\n-\t\t\t\t\t const struct object_id *old_oid,\n-\t\t\t\t\t const char *committer_info,\n-\t\t\t\t\t unsigned int flags,\n-\t\t\t\t\t const char *msg,\n-\t\t\t\t\t uint64_t index,\n-\t\t\t\t\t struct strbuf *err)\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err)\n {\n \tstruct ref_update *update;\n+\tunsigned int flags;\n \n \tassert(err);\n \n-\tflags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\n@@ -3019,8 +3013,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n-\t\t\t\t\t    REF_HAVE_NEW | REF_HAVE_OLD, msg,\n-\t\t\t\t\t    data->index++, data->errbuf);\n+\t\t\t\t\t    msg, data->index++, data->errbuf);\n \treturn ret;\n }\n \ndiff --git a/refs.h b/refs.h\nindex 46a6008e07f..253dd8f4d5d 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -795,6 +795,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \n+/*\n+ * Similar to `ref_transaction_update`, but this function is only for adding\n+ * a reflog update. Supports providing custom committer information. The index\n+ * field can be utiltized to order updates as desired. When set to zero, the\n+ * updates default to being ordered by refname.\n+ */\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err);\n+\n /*\n  * Add a reference creation to transaction. new_oid is the value that\n  * the reference should have after the update; it must not be\n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522940","messageId":"20250729-pks-reflog-append-v3-4-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 4/9] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:22Z","receivedAt":"2025-07-29T08:55:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we provide a couple of subcommands in git-reflog(1) to remove\nreflog entries, we don't provide any to write new entries. Obviously\nthis is not an operation that really would be needed for many use cases\nout there, or otherwise people would have complained that such a command\ndoes not exist yet. But the introduction of the \"reftable\" backend\nchanges the picture a bit, as it is now basically impossible to manually\nappend a reflog entry if one wanted to do so due to the binary format.\n\nPlug this gap by introducing a simple \"write\" subcommand. For now, all\nthis command does is to append a single new reflog entry with the given\nobject IDs and message to the reflog. More specifically, it is not yet\npossible to:\n\n  - Write multiple reflog entries at once.\n\n  - Insert reflog entries at arbitrary indices.\n\n  - Specify the date of the reflog entry.\n\n  - Insert reflog entries that refer to nonexistent objects.\n\nIf required, those features can be added at a future point in time. For\nnow though, the new command aims to fulfill the most basic use cases\nwhile being as strict as possible when it comes to verifying parameters.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc |   7 +++\n builtin/reflog.c              |  65 ++++++++++++++++++++++\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 126 ++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 199 insertions(+)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex c3801b82fb6..34232a539a7 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n git reflog exists <ref>\n+git reflog write <ref> <old-oid> <new-oid> <message>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n@@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n with zero status if the reflog exists, and non-zero status if it does\n not.\n \n+The \"write\" subcommand writes a single entry to the reflog of a given\n+reference. This new entry is appended to the reflog and will thus become\n+the most recent entry. The reference name must be fully qualified. Both the old\n+and new object IDs must not be abbreviated and must point to existing objects.\n+The reflog message gets normalized.\n+\n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n reflog delete master@{2}`\"). This subcommand is also typically not used\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b00b3f9edc9..a1b4e022041 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -3,6 +3,8 @@\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-store.h\"\n #include \"revision.h\"\n #include \"reachable.h\"\n #include \"wildmatch.h\"\n@@ -20,6 +22,9 @@\n #define BUILTIN_REFLOG_EXISTS_USAGE \\\n \tN_(\"git reflog exists <ref>\")\n \n+#define BUILTIN_REFLOG_WRITE_USAGE \\\n+\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n+\n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n@@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n \tNULL,\n };\n \n+static const char *const reflog_write_usage[] = {\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n+\tNULL,\n+};\n+\n static const char *const reflog_delete_usage[] = {\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tNULL\n@@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n \tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tBUILTIN_REFLOG_EXPIRE_USAGE,\n@@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n+\t\t\t    struct repository *repo)\n+{\n+\tconst struct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct object_id old_oid, new_oid;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct ref_transaction *tx;\n+\tconst char *ref, *message;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);\n+\tif (argc != 4)\n+\t\tusage_with_options(reflog_write_usage, options);\n+\n+\tref = argv[0];\n+\tif (!is_root_ref(ref) && check_refname_format(ref, 0))\n+\t\tdie(_(\"invalid reference name: %s\"), ref);\n+\n+\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n+\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n+\n+\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n+\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n+\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n+\n+\tmessage = argv[3];\n+\n+\ttx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\tif (!tx)\n+\t\tdie(_(\"cannot start transaction: %s\"), err.buf);\n+\n+\tret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,\n+\t\t\t\t\t    git_committer_info(0),\n+\t\t\t\t\t    message, 0, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot queue reflog update: %s\"), err.buf);\n+\n+\tret = ref_transaction_commit(tx, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot commit reflog update: %s\"), err.buf);\n+\n+\tref_transaction_free(tx);\n+\tstrbuf_release(&err);\n+\treturn 0;\n+}\n+\n /*\n  * main \"reflog\"\n  */\n@@ -405,6 +469,7 @@ int cmd_reflog(int argc,\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_reflog_write),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n \t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\ndiff --git a/t/meson.build b/t/meson.build\nindex d052fc3e23d..adcdf09e740 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -220,6 +220,7 @@ integration_tests = [\n   't1418-reflog-exists.sh',\n   't1419-exclude-refs.sh',\n   't1420-lost-found.sh',\n+  't1421-reflog-write.sh',\n   't1430-bad-ref-name.sh',\n   't1450-fsck.sh',\n   't1451-fsck-buffer.sh',\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nnew file mode 100755\nindex 00000000000..6cad64f40ab\n--- /dev/null\n+++ b/t/t1421-reflog-write.sh\n@@ -0,0 +1,126 @@\n+#!/bin/sh\n+\n+test_description='Manually write reflog entries'\n+\n+. ./test-lib.sh\n+\n+SIGNATURE=\"C O Mitter <committer@example.com> 1112911993 -0700\"\n+\n+test_reflog_matches () {\n+\trepo=\"$1\" &&\n+\trefname=\"$2\" &&\n+\tcat >actual &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$refname\" >expected &&\n+\ttest_cmp expected actual\n+}\n+\n+test_expect_success 'invalid number of arguments' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tfor args in \"\" \"1\" \"1 2\" \"1 2 3\" \"1 2 3 4 5\"\n+\t\tdo\n+\t\t\ttest_must_fail git reflog write $args 2>err &&\n+\t\t\ttest_grep \"usage: git reflog write\" err || return 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success 'invalid refname' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write \"refs/heads/ invalid\" $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'unqualified refname is rejected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write unqualified $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'nonexistent object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"old object .* does not exist\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&\n+\t\ttest_grep \"new object .* does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'abbreviated object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something 12345 $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID 12345 new-object-id 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err\n+\t)\n+'\n+\n+test_expect_success 'reflog message gets normalized' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\t\tgit reflog write HEAD $COMMIT_OID $COMMIT_OID \"$(printf \"message\\nwith\\nnewlines\")\" &&\n+\t\tgit reflog show -1 --format=%gs HEAD >actual &&\n+\t\techo \"message with newlines\" >expected &&\n+\t\ttest_cmp expected actual\n+\t)\n+'\n+\n+test_expect_success 'simple writes' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF &&\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n+\t\t# Note: the old object ID of the second reflog entry is broken.\n+\t\t# This will be fixed in subsequent commits.\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can write to root ref' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write ROOT_REF_HEAD $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . ROOT_REF_HEAD <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522941","messageId":"20250729-pks-reflog-append-v3-5-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 5/9] ident: fix type of string length parameter","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:23Z","receivedAt":"2025-07-29T08:55:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The last parameter in `split_ident_line()` is the length of the line\npassed in by the caller. As such, most callers pass in either the result\nof `strlen()`, `struct strbuf::len` or a pointer diff, all of which\nare expected to be positive numbers. Regardless of that, the function\naccepts a signed integer, which is somewhat confusing.\n\nFix the function signature to instead accept a `size_t`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ident.c | 2 +-\n ident.h | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ident.c b/ident.c\nindex 967895d8850..a7a2d132579 100644\n--- a/ident.c\n+++ b/ident.c\n@@ -272,7 +272,7 @@ static void strbuf_addstr_without_crud(struct strbuf *sb, const char *src)\n  * can still be NULL if the input line only has the name/email part\n  * (e.g. reading from a reflog entry).\n  */\n-int split_ident_line(struct ident_split *split, const char *line, int len)\n+int split_ident_line(struct ident_split *split, const char *line, size_t len)\n {\n \tconst char *cp;\n \tsize_t span;\ndiff --git a/ident.h b/ident.h\nindex 6a79febba15..3c034038791 100644\n--- a/ident.h\n+++ b/ident.h\n@@ -35,7 +35,7 @@ void reset_ident_date(void);\n  * Signals an success with 0, but time part of the result may be NULL\n  * if the input lacks timestamp and zone\n  */\n-int split_ident_line(struct ident_split *, const char *, int);\n+int split_ident_line(struct ident_split *, const char *, size_t);\n \n /*\n  * Given a commit or tag object buffer and the commit or tag headers, replaces\n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522942","messageId":"20250729-pks-reflog-append-v3-6-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 6/9] refs: fix identity for migrated reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:24Z","receivedAt":"2025-07-29T08:55:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we must\nreconstruct the identity of reflog entries. This is done by passing the\ncommitter passed to the `migrate_one_reflog_entry()` callback function\nto `fmt_ident()`.\n\nThis results in an invalid identity though: `fmt_ident()` expects the\ncaller to provide both name and mail of the author, but we pass the full\nidentity as mail. This leads to an identity like:\n\n    pks <Patrick Steinhardt ps@pks.im>\n\nFix the bug by splitting the identity line first. This allows us to\nextract both the name and mail so that we can pass them to `fmt_ident()`\nseparately.\n\nThis commit does not yet add any tests as there is another bug in the\nreflog migration that will be fixed in a subsequent commit. Once that\nbug is fixed we'll make the reflog verification in t1450 stricter, and\nthat will catch both this bug here and the other bug.\n\nNote that we also add two new `name` and `mail` string buffers to the\ncallback structures and splice them through to the callbacks. This is\ndone so that we can avoid allocating a new buffer every time we compute\nthe committer information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 22 ++++++++++++++++++----\n 1 file changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 8aa9f7236a3..a5f9ffaa45d 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2954,7 +2954,7 @@ struct migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf sb;\n+\tstruct strbuf sb, name, mail;\n };\n \n static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n@@ -2993,7 +2993,7 @@ struct reflog_migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf *sb;\n+\tstruct strbuf *sb, *name, *mail;\n };\n \n static int migrate_one_reflog_entry(struct object_id *old_oid,\n@@ -3003,13 +3003,21 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \t\t\t\t    const char *msg, void *cb_data)\n {\n \tstruct reflog_migration_data *data = cb_data;\n+\tstruct ident_split ident;\n \tconst char *date;\n \tint ret;\n \n+\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n+\t\treturn -1;\n+\n+\tstrbuf_reset(data->name);\n+\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n+\tstrbuf_reset(data->mail);\n+\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n+\n \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n \tstrbuf_reset(data->sb);\n-\t/* committer contains name and email */\n-\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n+\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n@@ -3026,6 +3034,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n \t\t.transaction = migration_data->transaction,\n \t\t.errbuf = migration_data->errbuf,\n \t\t.sb = &migration_data->sb,\n+\t\t.name = &migration_data->name,\n+\t\t.mail = &migration_data->mail,\n \t};\n \n \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n@@ -3124,6 +3134,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tstruct strbuf new_gitdir = STRBUF_INIT;\n \tstruct migration_data data = {\n \t\t.sb = STRBUF_INIT,\n+\t\t.name = STRBUF_INIT,\n+\t\t.mail = STRBUF_INIT,\n \t};\n \tint did_migrate_refs = 0;\n \tint ret;\n@@ -3299,6 +3311,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tref_transaction_free(transaction);\n \tstrbuf_release(&new_gitdir);\n \tstrbuf_release(&data.sb);\n+\tstrbuf_release(&data.name);\n+\tstrbuf_release(&data.mail);\n \treturn ret;\n }\n \n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522943","messageId":"20250729-pks-reflog-append-v3-7-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:25Z","receivedAt":"2025-07-29T08:55:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When updating a reference that is being pointed to HEAD we don't only\nwrite a reflog message for that particular reference, but also generate\none for HEAD. This logic is handled by `split_head_update()`, where we:\n\n  1. Verify that the condition actually triggered. This is done by\n     reading HEAD at the start of the transaction so that we can then\n     check whether a given reference update refers to its target.\n\n  2. Queue a new log-only update for HEAD in case it did.\n\nBut the logic is unfortunately not free of races, as we do not lock the\nHEAD reference after we have read its target. This can lead to the\nfollowing two scenarios:\n\n  - HEAD gets concurrently updated to point to one of the references we\n    have already processed. This causes us not writing a reflog message\n    even though we should have done so.\n\n  - HEAD gets concurrently updated to point to not point to a reference\n    anymore that we have already processed. This causes us to write a\n    reflog message even though we should _not_ have done so.\n\nImprove the situation by introducing a new `REF_LOG_VIA_SPLIT` flag that\nis specific to the \"files\" backend. If set, we will double check that\nthe HEAD reference still points to the reference that we are creating\nthe reflog entry for after we have locked HEAD. Furthermore, instead of\nmanually resolving the old object ID of that entry, we now use the same\nold state as for the parent update.\n\nUnfortunately, this change only helps with the second race. We cannot\nreliably plug the first race without locking the HEAD reference at the\nstart of the transaction. Locking HEAD unconditionally would effectively\nserialize all writes though, and that doesn't seem like an option. Also,\ndouble checking its value at the end of the transaction is not an option\neither, as its target may have flip-flopped during the transaction.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c | 40 ++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 38 insertions(+), 2 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex bf6f89b1d19..ba018b0984a 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -68,6 +68,12 @@\n  */\n #define REF_DELETED_RMDIR (1 << 9)\n \n+/*\n+ * Used to indicate that the reflog-only update has been created via\n+ * `split_head_update()`.\n+ */\n+#define REF_LOG_VIA_SPLIT (1 << 14)\n+\n struct ref_lock {\n \tchar *ref_name;\n \tstruct lock_file lk;\n@@ -2420,9 +2426,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,\n \n \tnew_update = ref_transaction_add_update(\n \t\t\ttransaction, \"HEAD\",\n-\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF,\n+\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF | REF_LOG_VIA_SPLIT,\n \t\t\t&update->new_oid, &update->old_oid,\n \t\t\tNULL, NULL, update->committer_info, update->msg);\n+\tnew_update->parent_update = update;\n \n \t/*\n \t * Add \"HEAD\". This insertion is O(N) in the transaction\n@@ -2600,7 +2607,36 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \n \tupdate->backend_data = lock;\n \n-\tif (update->type & REF_ISSYMREF) {\n+\tif (update->flags & REF_LOG_VIA_SPLIT) {\n+\t\tstruct ref_lock *parent_lock;\n+\n+\t\tif (!update->parent_update)\n+\t\t\tBUG(\"split update without a parent\");\n+\n+\t\tparent_lock = update->parent_update->backend_data;\n+\n+\t\t/*\n+\t\t * Check that \"HEAD\" didn't racily change since we have looked\n+\t\t * it up. If it did we must refuse to write the reflog entry.\n+\t\t *\n+\t\t * Note that this does not catch all races: if \"HEAD\" was\n+\t\t * racily changed to point to one of the refs part of the\n+\t\t * transaction then we would miss writing the split reflog\n+\t\t * entry for \"HEAD\".\n+\t\t */\n+\t\tif (!(update->type & REF_ISSYMREF) ||\n+\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n+\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (update->flags & REF_HAVE_OLD) {\n+\t\t\toidcpy(&lock->old_oid, &update->old_oid);\n+\t\t} else {\n+\t\t\toidcpy(&lock->old_oid, &parent_lock->old_oid);\n+\t\t}\n+\t} else if (update->type & REF_ISSYMREF) {\n \t\tif (update->flags & REF_NO_DEREF) {\n \t\t\t/*\n \t\t\t * We won't be reading the referent as part of\n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522944","messageId":"20250729-pks-reflog-append-v3-8-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 8/9] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:26Z","receivedAt":"2025-07-29T08:55:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\nobject ID set. If so, the value of that field is used to verify whether\nthe current state of the reference matches this expected state. It is\nthus an important part of mitigating races with a concurrent process\nthat updates the same set of references.\n\nWhen writing reflogs though we explicitly unset that flag. This is a\nsensible thing to do: the old state of reflog entry updates may not\nnecessarily match the current on-disk state of its accompanying ref, but\nit's only intended to signal what old object ID we want to write into\nthe new reflog entry. For example when migrating refs we end up writing\nmany reflog entries for a single reference, and most likely those reflog\nentries will have many different old object IDs.\n\nBut unsetting this flag also removes a useful signal, namely that the\ncaller _did_ provide an old object ID for a given reflog entry. This\nsignal will become useful in a subsequent commit, where we add a new\nflag that tells the transaction to use the provided old and new object\nIDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\nsignal to verify that the caller really did provide an old object ID.\n\nStop unsetting the flag so that we can use it as this described signal\nin a subsequent commit. Skip checking the old object ID for log-only\nupdates so that we don't expect it to match the current on-disk state.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  8 +++-----\n refs/files-backend.c    |  9 +++++----\n refs/refs-internal.h    |  3 ++-\n refs/reftable-backend.c | 12 +++---------\n 4 files changed, 13 insertions(+), 19 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex a5f9ffaa45d..f88928de746 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1393,11 +1393,6 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \tupdate = ref_transaction_add_update(transaction, refname, flags,\n \t\t\t\t\t    new_oid, old_oid, NULL, NULL,\n \t\t\t\t\t    committer_info, msg);\n-\t/*\n-\t * While we do set the old_oid value, we unset the flag to skip\n-\t * old_oid verification which only makes sense for refs.\n-\t */\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \tupdate->index = index;\n \n \t/*\n@@ -3318,6 +3313,9 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \n int ref_update_expects_existing_old_ref(struct ref_update *update)\n {\n+\tif (update->flags & REF_LOG_ONLY)\n+\t\treturn 0;\n+\n \treturn (update->flags & REF_HAVE_OLD) &&\n \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex ba018b0984a..85ab2ef2b94 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2500,7 +2500,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n \t * done when new_update is processed.\n \t */\n \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \n \treturn 0;\n }\n@@ -2515,8 +2514,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n \t\t\t\t\t\tstruct object_id *oid,\n \t\t\t\t\t\tstruct strbuf *err)\n {\n-\tif (!(update->flags & REF_HAVE_OLD) ||\n-\t\t   oideq(oid, &update->old_oid))\n+\tif (update->flags & REF_LOG_ONLY ||\n+\t    !(update->flags & REF_HAVE_OLD) ||\n+\t    oideq(oid, &update->old_oid))\n \t\treturn 0;\n \n \tif (is_null_oid(&update->old_oid)) {\n@@ -3095,7 +3095,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n \n-\t\tif ((update->flags & REF_HAVE_OLD) &&\n+\t\tif (!(update->flags & REF_LOG_ONLY) &&\n+\t\t    (update->flags & REF_HAVE_OLD) &&\n \t\t    !is_null_oid(&update->old_oid))\n \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex f8688708519..95a4dc3902f 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -802,7 +802,8 @@ enum ref_transaction_error ref_update_check_old_target(const char *referent,\n \n /*\n  * Check if the ref must exist, this means that the old_oid or\n- * old_target is non NULL.\n+ * old_target is non NULL. Log-only updates never require the old state to\n+ * match.\n  */\n int ref_update_expects_existing_old_ref(struct ref_update *update);\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4c3817f4ec1..44af58ac50b 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1180,8 +1180,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret > 0) {\n \t\t/* The reference does not exist, but we expected it to. */\n \t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\n-\n \t\t\t\t   \"unable to resolve reference '%s'\"),\n \t\t\t    ref_update_original_update_refname(u), u->refname);\n \t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n@@ -1235,13 +1233,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \n \t\t\tnew_update->parent_update = u;\n \n-\t\t\t/*\n-\t\t\t * Change the symbolic ref update to log only. Also, it\n-\t\t\t * doesn't need to check its old OID value, as that will be\n-\t\t\t * done when new_update is processed.\n-\t\t\t */\n+\t\t\t/* Change the symbolic ref update to log only. */\n \t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\t\t\tu->flags &= ~REF_HAVE_OLD;\n \t\t}\n \t}\n \n@@ -1265,7 +1258,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\tret = ref_update_check_old_target(referent->buf, u, err);\n \t\tif (ret)\n \t\t\treturn ret;\n-\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n+\t} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&\n+\t\t   !oideq(&current_oid, &u->old_oid)) {\n \t\tif (is_null_oid(&u->old_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference already exists\"),\n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522945","messageId":"20250729-pks-reflog-append-v3-9-9614d310f073@pks.im","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-0-9614d310f073@pks.im","subject":"[PATCH v3 9/9] refs: fix invalid old object IDs when migrating reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-29T08:55:27Z","receivedAt":"2025-07-29T08:55:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we end\nup with invalid old object IDs for the migrated entries: instead of\nwriting the old object ID of the to-be-migrated entry, we end up with\nthe all-zeroes object ID.\n\nThe root cause of this issue is that we don't know to use the old object\nID provided by the caller. Instead, we manually resolve the old object\nID by resolving the current value of its matching reference. But as that\nreference does not yet exist in the target ref storage we always end up\nresolving it to all-zeroes.\n\nThis issue got unnoticed as there is no user-facing command that would\neven show the old object ID. While `git log -g` knows to show the new\nobject ID, we don't have any formatting directive to show the old object\nID.\n\nFix the bug by introducing a new flag `REF_LOG_USE_PROVIDED_OIDS`. If\nset, backends are instructed to use the old and new object IDs provided\nby the caller, without doing any manual resolving. Set this flag in\n`ref_transaction_update_reflog()`.\n\nAmend our tests in t1460-refs-migrate to use our test tool to read\nreflog entries. This test tool prints out both old and new object ID of\neach reflog entry, which fixes the test gap. Furthermore it also prints\nthe full identity used to write the reflog, which provides test coverage\nfor the previous commit in this patch series that fixed the identity for\nmigrated reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  3 ++-\n refs.h                  |  9 ++++++++-\n refs/files-backend.c    | 16 +++++++++++++++-\n refs/reftable-backend.c | 14 ++++++++++++++\n t/t1421-reflog-write.sh |  4 +---\n t/t1460-refs-migrate.sh | 22 +++++++++++++++-------\n 6 files changed, 55 insertions(+), 13 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex f88928de746..946eb48941b 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1385,7 +1385,8 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \n \tassert(err);\n \n-\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF |\n+\t\tREF_LOG_USE_PROVIDED_OIDS;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\ndiff --git a/refs.h b/refs.h\nindex 253dd8f4d5d..090b4fdff4f 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -760,13 +760,20 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n  */\n #define REF_SKIP_CREATE_REFLOG (1 << 12)\n \n+/*\n+ * When writing a REF_LOG_ONLY record, use the old and new object IDs provided\n+ * in the update instead of resolving the old object ID. The caller must also\n+ * set both REF_HAVE_OLD and REF_HAVE_NEW.\n+ */\n+#define REF_LOG_USE_PROVIDED_OIDS (1 << 13)\n+\n /*\n  * Bitmask of all of the flags that are allowed to be passed in to\n  * ref_transaction_update() and friends:\n  */\n #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \\\n \t(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \\\n-\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)\n+\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | REF_LOG_USE_PROVIDED_OIDS)\n \n /*\n  * Add a reference update to transaction. `new_oid` is the value that\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 85ab2ef2b94..905555365b8 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -3010,6 +3010,20 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t\t\t  struct ref_lock *lock,\n \t\t\t\t  struct strbuf *err)\n {\n+\tstruct object_id *old_oid = &lock->old_oid;\n+\n+\tif (update->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(update->flags & REF_HAVE_OLD) ||\n+\t\t    !(update->flags & REF_HAVE_NEW) ||\n+\t\t    !(update->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), update->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\told_oid = &update->old_oid;\n+\t}\n+\n \tif (update->new_target) {\n \t\t/*\n \t\t * We want to get the resolved OID for the target, to ensure\n@@ -3027,7 +3041,7 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t}\n \t}\n \n-\tif (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,\n+\tif (files_log_ref_write(refs, lock->ref_name, old_oid,\n \t\t\t\t&update->new_oid, update->committer_info,\n \t\t\t\tupdate->msg, update->flags, err)) {\n \t\tchar *old_msg = strbuf_detach(err, NULL);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 44af58ac50b..99fafd75ebe 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1096,6 +1096,20 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret)\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\tif (u->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(u->flags & REF_HAVE_OLD) ||\n+\t\t    !(u->flags & REF_HAVE_NEW) ||\n+\t\t    !(u->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), u->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\tif (queue_transaction_update(refs, tx_data, u, &u->old_oid, err))\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\treturn 0;\n+\t}\n+\n \t/* Verify that the new object ID is valid. */\n \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n \t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nindex 6cad64f40ab..d4e41838f8f 100755\n--- a/t/t1421-reflog-write.sh\n+++ b/t/t1421-reflog-write.sh\n@@ -99,11 +99,9 @@ test_expect_success 'simple writes' '\n \t\tEOF\n \n \t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n-\t\t# Note: the old object ID of the second reflog entry is broken.\n-\t\t# This will be fixed in subsequent commits.\n \t\ttest_reflog_matches . refs/heads/something <<-EOF\n \t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n-\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n \t\tEOF\n \t)\n '\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 2ab97e1b7df..0e1116a319d 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -7,6 +7,17 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+print_all_reflog_entries () {\n+\trepo=$1 &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n+\twhile read reflog\n+\tdo\n+\t\techo \"REFLOG: $reflog\" &&\n+\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n+\t\treturn 1\n+\tdone <reflogs\n+}\n+\n # Migrate the provided repository from one format to the other and\n # verify that the references and logs are migrated over correctly.\n # Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]\n@@ -28,8 +39,7 @@ test_migration () {\n \t\t--format='%(refname) %(objectname) %(symref)' >expect &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t   git -C \"$repo\" reflog --all >expect_logs &&\n-\t   git -C \"$repo\" reflog list >expect_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >expect_logs\n \tfi &&\n \n \tgit -C \"$repo\" refs migrate --ref-format=\"$format\" \"$@\" &&\n@@ -39,10 +49,8 @@ test_migration () {\n \ttest_cmp expect actual &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t\tgit -C \"$repo\" reflog --all >actual_logs &&\n-\t\tgit -C \"$repo\" reflog list >actual_log_list &&\n-\t\ttest_cmp expect_logs actual_logs &&\n-\t\ttest_cmp expect_log_list actual_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >actual_logs &&\n+\t\ttest_cmp expect_logs actual_logs\n \tfi &&\n \n \tgit -C \"$repo\" rev-parse --show-ref-format >actual &&\n@@ -273,7 +281,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' '\n \ttest_commit -C repo second &&\n \tprintf \"update refs/heads/ref-%d HEAD\\n\" $(test_seq 3000) >stdin &&\n \tgit -C repo update-ref --stdin <stdin &&\n-\ttest_migration repo reftable\n+\ttest_migration repo reftable true\n '\n \n test_expect_success 'migrating from files format deletes backend files' '\n\n-- \n2.50.1.619.g074bbf1d35.dirty\n\n"},{"id":"522962","messageId":"xmqqseifqbym.fsf@gitster.g","threadId":"63818","inReplyTo":"aIhvcSHrRLG6wcMD@pks.im","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-29T15:00:01Z","receivedAt":"2025-07-29T15:00:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> There's a big difference though: `git reflog drop` won't ever do\n> anything for a reflog that doesn't exist. Consequently, we know that our\n> DWIM mechanism can kick in and resolve the reference properly if such a\n> reflog exists.\n>\n> But for `git reflog write` that's not the case, as you can write a\n> reflog message for a yet-nonexistent reflog. The DWIM mechanism cannot\n> kick in here as there is no reflog. So what do we do in that case? We\n> could of course just pick the first DWIM rule, which would be that we\n> decide to write the reflog for \"refs/heads/$REFNAME\". But... I dunno,\n> that feels too magicky to m\n\nI concur.  Like update-ref, a command that would work on a name that\ndoes not yet exist, especially when it is a plumbing-ish low-level\ncommand, would be too confusing if it dwimmed based on what names\nexist already.\n\nI wonder if it is feasible to correct the UI mistake of \"git reflog\"\nusing the dwim-ref logic, and compensate it by teaching \"git branch\"\nand \"git tag\" options to drop reflog for the thing they act on.  At\nthat level, there is nothing to dwim---\"branch\" is about branches\nthat are either refs/heads/* or (when run with -r) refs/remotes/*.\nIt's sort of like \"for-each-ref\" requiring the refname from the top,\nwhile \"branch -l\" and \"branch -l -r\" always limit themselves to the\nrelevant hierarchy.\n\nBut it is probably not a good idea and is way too late.\n"},{"id":"522968","messageId":"xmqqtt2vou90.fsf@gitster.g","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-4-9614d310f073@pks.im","subject":"Re: [PATCH v3 4/9] builtin/reflog: implement subcommand to write new entries","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-29T16:07:55Z","receivedAt":"2025-07-29T16:07:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +\tref = argv[0];\n> +\tif (!is_root_ref(ref) && check_refname_format(ref, 0))\n> +\t\tdie(_(\"invalid reference name: %s\"), ref);\n\nThe \"root ref\" check is new in this iteration, and it makes perfect\nsense.\n\nWe are not passing REFNAME_ALLOW_ONELEVEL flag, so we explicitly\nallow things like HEAD (but exclude things like FETCH_HEAD).\n\n"},{"id":"522969","messageId":"xmqqpldjotu4.fsf@gitster.g","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-7-9614d310f073@pks.im","subject":"Re: [PATCH v3 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-29T16:16:51Z","receivedAt":"2025-07-29T16:16:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When updating a reference that is being pointed to HEAD we don't only\n> write a reflog message for that particular reference, but also generate\n> one for HEAD. This logic is handled by `split_head_update()`, where we:\n>\n>   1. Verify that the condition actually triggered. This is done by\n>      reading HEAD at the start of the transaction so that we can then\n>      check whether a given reference update refers to its target.\n>\n>   2. Queue a new log-only update for HEAD in case it did.\n>\n> But the logic is unfortunately not free of races, as we do not lock the\n> HEAD reference after we have read its target. This can lead to the\n> following two scenarios:\n>\n>   - HEAD gets concurrently updated to point to one of the references we\n>     have already processed. This causes us not writing a reflog message\n>     even though we should have done so.\n>\n>   - HEAD gets concurrently updated to point to not point to a reference\n>     anymore that we have already processed. This causes us to write a\n>     reflog message even though we should _not_ have done so.\n>\n> Improve the situation by introducing a new `REF_LOG_VIA_SPLIT` flag that\n> is specific to the \"files\" backend. If set, we will double check that\n> the HEAD reference still points to the reference that we are creating\n> the reflog entry for after we have locked HEAD. Furthermore, instead of\n> manually resolving the old object ID of that entry, we now use the same\n> old state as for the parent update.\n>\n> Unfortunately, this change only helps with the second race. We cannot\n> reliably plug the first race without locking the HEAD reference at the\n> start of the transaction. Locking HEAD unconditionally would effectively\n> serialize all writes though, and that doesn't seem like an option. Also,\n> double checking its value at the end of the transaction is not an option\n> either, as its target may have flip-flopped during the transaction.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs/files-backend.c | 40 ++++++++++++++++++++++++++++++++++++++--\n>  1 file changed, 38 insertions(+), 2 deletions(-)\n\nThis is a step new in this iteration.  I sometimes wonder if the\nworld were in a much better shape if I didn't record the updates to\nunderlying branch in the reflog of HEAD (and limited only to record\nswitching branches), as this is a fallout from the (mis)design.\n\nAnyway, I agree that the change in this patch matches the above\ndescription and takes us in a better place ;-)\n\nThanks.\n\n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index bf6f89b1d19..ba018b0984a 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -68,6 +68,12 @@\n>   */\n>  #define REF_DELETED_RMDIR (1 << 9)\n>  \n> +/*\n> + * Used to indicate that the reflog-only update has been created via\n> + * `split_head_update()`.\n> + */\n> +#define REF_LOG_VIA_SPLIT (1 << 14)\n> +\n>  struct ref_lock {\n>  \tchar *ref_name;\n>  \tstruct lock_file lk;\n> @@ -2420,9 +2426,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,\n>  \n>  \tnew_update = ref_transaction_add_update(\n>  \t\t\ttransaction, \"HEAD\",\n> -\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF,\n> +\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF | REF_LOG_VIA_SPLIT,\n>  \t\t\t&update->new_oid, &update->old_oid,\n>  \t\t\tNULL, NULL, update->committer_info, update->msg);\n> +\tnew_update->parent_update = update;\n>  \n>  \t/*\n>  \t * Add \"HEAD\". This insertion is O(N) in the transaction\n> @@ -2600,7 +2607,36 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n>  \n>  \tupdate->backend_data = lock;\n>  \n> -\tif (update->type & REF_ISSYMREF) {\n> +\tif (update->flags & REF_LOG_VIA_SPLIT) {\n> +\t\tstruct ref_lock *parent_lock;\n> +\n> +\t\tif (!update->parent_update)\n> +\t\t\tBUG(\"split update without a parent\");\n> +\n> +\t\tparent_lock = update->parent_update->backend_data;\n> +\n> +\t\t/*\n> +\t\t * Check that \"HEAD\" didn't racily change since we have looked\n> +\t\t * it up. If it did we must refuse to write the reflog entry.\n> +\t\t *\n> +\t\t * Note that this does not catch all races: if \"HEAD\" was\n> +\t\t * racily changed to point to one of the refs part of the\n> +\t\t * transaction then we would miss writing the split reflog\n> +\t\t * entry for \"HEAD\".\n> +\t\t */\n> +\t\tif (!(update->type & REF_ISSYMREF) ||\n> +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n> +\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n> +\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tif (update->flags & REF_HAVE_OLD) {\n> +\t\t\toidcpy(&lock->old_oid, &update->old_oid);\n> +\t\t} else {\n> +\t\t\toidcpy(&lock->old_oid, &parent_lock->old_oid);\n> +\t\t}\n> +\t} else if (update->type & REF_ISSYMREF) {\n>  \t\tif (update->flags & REF_NO_DEREF) {\n>  \t\t\t/*\n>  \t\t\t * We won't be reading the referent as part of\n"},{"id":"522992","messageId":"aImup_QlY0tdBtJq@pks.im","threadId":"63818","inReplyTo":"xmqqseifqbym.fsf@gitster.g","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-30T05:33:27Z","receivedAt":"2025-07-30T05:33:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Jul 29, 2025 at 08:00:01AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > There's a big difference though: `git reflog drop` won't ever do\n> > anything for a reflog that doesn't exist. Consequently, we know that our\n> > DWIM mechanism can kick in and resolve the reference properly if such a\n> > reflog exists.\n> >\n> > But for `git reflog write` that's not the case, as you can write a\n> > reflog message for a yet-nonexistent reflog. The DWIM mechanism cannot\n> > kick in here as there is no reflog. So what do we do in that case? We\n> > could of course just pick the first DWIM rule, which would be that we\n> > decide to write the reflog for \"refs/heads/$REFNAME\". But... I dunno,\n> > that feels too magicky to m\n\nGah, another cut-off mail. This is driving me crazy. I have an idea what\nthe root cause could be that I've implemented yesterday an hour after\nthis mail. So fingers crossed this that this will stop now.\n\nAnyway, remainder of this mail:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n> > But... I dunno, that feels too magicky to me given that we are in\n> > plumbing land.\n> > \n> > So I think the next-best thing for now is to make input verification\n> > stricter by dropping the `REFNAME_ALLOW_ONELEVEL` flag.\n\nOn Tue, Jul 29, 2025 at 08:00:01AM -0700, Junio C Hamano wrote:\n> I concur.  Like update-ref, a command that would work on a name that\n> does not yet exist, especially when it is a plumbing-ish low-level\n> command, would be too confusing if it dwimmed based on what names\n> exist already.\n> \n> I wonder if it is feasible to correct the UI mistake of \"git reflog\"\n> using the dwim-ref logic, and compensate it by teaching \"git branch\"\n> and \"git tag\" options to drop reflog for the thing they act on.  At\n> that level, there is nothing to dwim---\"branch\" is about branches\n> that are either refs/heads/* or (when run with -r) refs/remotes/*.\n> It's sort of like \"for-each-ref\" requiring the refname from the top,\n> while \"branch -l\" and \"branch -l -r\" always limit themselves to the\n> relevant hierarchy.\n> \n> But it is probably not a good idea and is way too late.\n\nYeah, I agree that it's probably too late now to change it. Whether we\nneed to teach git-branch(1) or git-tag(1) to do so I'm not sure. It\nfeels quite unlikely to me that anyone really does this operation\nfrequently enough to care.\n\nPatrick\n"},{"id":"523000","messageId":"CAOLa=ZQfkM2PydwPFKfEqiYiCeyqmd_Wpoarr6HkbVMJnrOd6A@mail.gmail.com","threadId":"63818","inReplyTo":"xmqqms8ot4jh.fsf@gitster.g","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-07-30T07:55:28Z","receivedAt":"2025-07-30T07:55:30Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>> Junio C Hamano <gitster@pobox.com> writes:\n>>\n>>> \"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n>>>\n>>>> I tried `git reflog drop`[1] and it can deal with a branch like\n>>>> `branch`.  It doesn’t need to be told `refs/heads/branch`.\n>>>\n>>> That sounds like a bug to me.\n>>\n>> So `git reflog drop` `git reflog delete` and `git reflog expire` use\n>> `repo_dwim_log()` to resolve the provided reference.\n>>\n>> And `repo_dwim_log()` uses the following `ref_rev_parse_rules` to\n>> resolve the reference.\n>>\n>>   static const char *ref_rev_parse_rules[] = {\n>>   \t\"%.*s\",\n>>   \t\"refs/%.*s\",\n>>   \t\"refs/tags/%.*s\",\n>>   \t\"refs/heads/%.*s\",\n>>   \t\"refs/remotes/%.*s\",\n>>   \t\"refs/remotes/%.*s/HEAD\",\n>>   \tNULL\n>>   };\n>>\n>> Which means we do a best case resolution of a given reference, but the\n>> function also checks for ambiguity and warns for it.\n>\n> True.  But as I considered \"git reflog\" to be a lot closer to the\n> plumbing than to Porcelain, using the dwim thing smelled like a bug.\n>\n\nI agree that 'git reflog' is more of a plumbing command. I'm trying to\nsee what subcommands of reflog act this way, so we can take a decision\non how to move forward.\n\n> It also is OK to update the commands that do not use dwim-log to\n> also use it.  That way, the result would be consistent across\n> subcommands of \"git reflog\".  As long as the users are aware of the\n> fact that the command uses dwim-log, they can always spell their ref\n> in full like \"refs/heads/branch\" to avoid ambiguity check getting in\n> the way.\n>\n> Thanks.\n\nYeah consistency and documentation around it would be great here.\n\n- Karthik\n"},{"id":"523005","messageId":"aIn1EqDARgpWMN5h@pks.im","threadId":"63818","inReplyTo":"aImup_QlY0tdBtJq@pks.im","subject":"Re: [PATCH v2 4/8] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-30T10:33:54Z","receivedAt":"2025-07-30T10:34:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 30, 2025 at 07:33:30AM +0200, Patrick Steinhardt wrote:\n> On Tue, Jul 29, 2025 at 08:00:01AM -0700, Junio C Hamano wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> > \n> > > There's a big difference though: `git reflog drop` won't ever do\n> > > anything for a reflog that doesn't exist. Consequently, we know that our\n> > > DWIM mechanism can kick in and resolve the reference properly if such a\n> > > reflog exists.\n> > >\n> > > But for `git reflog write` that's not the case, as you can write a\n> > > reflog message for a yet-nonexistent reflog. The DWIM mechanism cannot\n> > > kick in here as there is no reflog. So what do we do in that case? We\n> > > could of course just pick the first DWIM rule, which would be that we\n> > > decide to write the reflog for \"refs/heads/$REFNAME\". But... I dunno,\n> > > that feels too magicky to m\n> \n> Gah, another cut-off mail. This is driving me crazy. I have an idea what\n> the root cause could be that I've implemented yesterday an hour after\n> this mail. So fingers crossed this that this will stop now.\n\nYup, I was finally able to reproduce the issue. I'm using msmtp to send\nmail and have a `passwordeval` script that I use to yield the password.\nRecently I've switched to a different password manager though, so I had\nto adapt the script a bit. Basically, what the script does is to check\nwhether I'm already signed in -- if not, it spawns rofi to ask me for my\npassword.\n\nBut: rofi actually reads from stdin, and the `passwordeval` command in\nmsmtp explicitly must _not_ munge stdin, as stdin is where the mail gets\nread from. So this is what caused the mail to get truncated.\n\nSo why wasn't I able to reproduce the issue? Well, because it only\nhappens in case the password store in locked and I need to input my\npassword. But when reproducing it I already had the password store\nunlocked.\n\nThe fix is thus quite easy:\n\ndiff --git a/home-manager/profiles/graphical/workstation/gitlab/default.nix b/home-manager/profiles/graphical/workstation/gitlab/default.nix\nindex d47e95a83..3460a29fb 100644\n--- a/home-manager/profiles/graphical/workstation/gitlab/default.nix\n+++ b/home-manager/profiles/graphical/workstation/gitlab/default.nix\n@@ -42,6 +42,8 @@ in\n         pkgs.writeShellScript \"op-mail-password\" ''\n           set -eo pipefail\n \n+          exec 0<&-\n+\n           export OP_SESSION=$(systemctl --user show-environment | grep '^OP_SESSION=' | cut -d= -f2)\n           if test -z \"$OP_SESSION\" || ! op vault list &>/dev/null\n           then\n\nFinally, another mystery solved. This really has been stressing me out\nover the last two weeks.\n\nPatrick\n"},{"id":"523194","messageId":"87wm7nuvaz.fsf@iotcl.com","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-4-9614d310f073@pks.im","subject":"Re: [PATCH v3 4/9] builtin/reflog: implement subcommand to write new entries","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-08-01T11:37:40Z","receivedAt":"2025-08-01T11:37:58Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +test_expect_success 'abbreviated object IDs' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_must_fail git reflog write refs/heads/something 12345 $ZERO_OID old-object-id 2>err &&\n\nIs the object id rejected because it's short, or because there simply\ndoesn't exist an object that starts with `12345`? You're not really\ntesting the former, which you claim in the test name.\n\n-- \nCheers,\nToon\n"},{"id":"523195","messageId":"87tt2ruv9z.fsf@iotcl.com","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-3-9614d310f073@pks.im","subject":"Re: [PATCH v3 3/9] refs: export `ref_transaction_update_reflog()`","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-08-01T11:38:16Z","receivedAt":"2025-08-01T11:38:28Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In a subsequent commit we'll add another user that wants to write reflog\n> entries. This requires them to call `ref_transaction_update_reflog()`,\n> but that function is local to \"refs.c\".\n>\n> Export the function to prepare for the change. While at it, drop the\n> `flags` field, as all callers are for now expected to use the same flags\n> anyway.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs.c | 29 +++++++++++------------------\n>  refs.h | 15 +++++++++++++++\n>  2 files changed, 26 insertions(+), 18 deletions(-)\n>\n> diff --git a/refs.c b/refs.c\n> index dce5c49ca2b..8aa9f7236a3 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -1371,27 +1371,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n>  \treturn 0;\n>  }\n>  \n> -/*\n> - * Similar to`ref_transaction_update`, but this function is only for adding\n\nTiniest nit: for some reason the space after \"to\" fell away.\n\n-- \nCheers,\nToon\n"},{"id":"523196","messageId":"87seibuuh8.fsf@iotcl.com","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-7-9614d310f073@pks.im","subject":"Re: [PATCH v3 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2025-08-01T11:55:31Z","receivedAt":"2025-08-01T11:55:43Z","isPatch":true,"sender":{"key":"toon@iotcl.com","avatar":"https://avatars.githubusercontent.com/u/121621?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When updating a reference that is being pointed to HEAD we don't only\n> write a reflog message for that particular reference, but also generate\n> one for HEAD. This logic is handled by `split_head_update()`, where we:\n>\n>   1. Verify that the condition actually triggered. This is done by\n>      reading HEAD at the start of the transaction so that we can then\n>      check whether a given reference update refers to its target.\n>\n>   2. Queue a new log-only update for HEAD in case it did.\n>\n> But the logic is unfortunately not free of races, as we do not lock the\n> HEAD reference after we have read its target. This can lead to the\n> following two scenarios:\n>\n>   - HEAD gets concurrently updated to point to one of the references we\n>     have already processed. This causes us not writing a reflog message\n>     even though we should have done so.\n>\n>   - HEAD gets concurrently updated to point to not point to a reference\n\nThat's a little much of pointing, right? ;)\n\nMaybe change it to something like:\n\n    - HEAD gets concurrently updated to no longer point to a reference\n\n-- \nCheers,\nToon\n"},{"id":"523270","messageId":"20250802111128.GC1180347@coredump.intra.peff.net","threadId":"63818","inReplyTo":"20250729-pks-reflog-append-v3-7-9614d310f073@pks.im","subject":"Re: [PATCH v3 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-08-02T11:11:28Z","receivedAt":"2025-08-02T11:11:30Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Jul 29, 2025 at 10:55:25AM +0200, Patrick Steinhardt wrote:\n\n> Unfortunately, this change only helps with the second race. We cannot\n> reliably plug the first race without locking the HEAD reference at the\n> start of the transaction. Locking HEAD unconditionally would effectively\n> serialize all writes though, and that doesn't seem like an option. Also,\n> double checking its value at the end of the transaction is not an option\n> either, as its target may have flip-flopped during the transaction.\n\nI agree we should not always take a lock on HEAD, since most refs would\nnot need it. But I wonder if we could do better by examining HEAD, then\ntaking a lock when we think we'll need it, and then re-checking the\nvalue of HEAD. That is still racy, though (somebody could have pointed\nHEAD at us between the two checks). Fundamentally the files backend is\nnot atomic across the whole namespace, and we are trying to update two\nrefs. So I think there will always be some race.\n\nIt does make me wonder if this race-fix is even worth it, then. We are\ncatching the case where somebody moves HEAD away from the ref we are\nupdating while we are updating it. But without atomicity, do we even\nknow which happened first? That is, would it be incorrect to update\nHEAD anyway? I guess the outcome is observable because their movement of\nHEAD generated a reflog entry, and thus the entries would be out of\norder. So maybe that is worth it.\n\nAnyway, I had two questions about the code:\n\n> @@ -2600,7 +2607,36 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n>  \n>  \tupdate->backend_data = lock;\n>  \n> -\tif (update->type & REF_ISSYMREF) {\n> +\tif (update->flags & REF_LOG_VIA_SPLIT) {\n> +\t\tstruct ref_lock *parent_lock;\n> +\n> +\t\tif (!update->parent_update)\n> +\t\t\tBUG(\"split update without a parent\");\n> +\n> +\t\tparent_lock = update->parent_update->backend_data;\n> +\n> +\t\t/*\n> +\t\t * Check that \"HEAD\" didn't racily change since we have looked\n> +\t\t * it up. If it did we must refuse to write the reflog entry.\n> +\t\t *\n> +\t\t * Note that this does not catch all races: if \"HEAD\" was\n> +\t\t * racily changed to point to one of the refs part of the\n> +\t\t * transaction then we would miss writing the split reflog\n> +\t\t * entry for \"HEAD\".\n> +\t\t */\n> +\t\tif (!(update->type & REF_ISSYMREF) ||\n> +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n> +\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n> +\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n> +\t\t\tgoto out;\n> +\t\t}\n\nOne, what happens with a multi-level ref (e.g., HEAD points to\nrefs/heads/foo which points to refs/heads/bar)?\n\nWe've resolved HEAD to get referent.buf. Do we get \"foo\" or \"bar\" here?\nIf \"bar\", then a write through \"foo\" will complain. But if we get \"foo\",\nthen theoretically a write through \"bar\" will complain.\n\nI _think_ we are OK, though. Constructing it like this:\n\n  git init\n  git commit --allow-empty -m whatever\n\n  git symbolic-ref refs/heads/foo refs/heads/bar\n  git symbolic-ref HEAD refs/heads/foo\n  git update-ref refs/heads/foo main\n\ntriggers the check and shows that our referent from lock_raw_ref() is\nthe first level (i.e., \"foo\"). Which is good.\n\nIf we swap out \"foo\" for \"bar\" in the update-ref call, then we'd get a\nmismatch. But in that case we do not figure out that HEAD needs be\nwritten at all! That is, we only do a single level of look-back to\ndecide whether to write HEAD at all. So as long as we keep doing so, we\nare OK.\n\n> +\t\tif (!(update->type & REF_ISSYMREF) ||\n> +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n> +\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n> +\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n> +\t\t\tgoto out;\n> +\t\t}\n\nAnd two, is an error the right thing here? The user asked us to update\n\"foo\", and we saw that HEAD pointed to it. So we decided to update\nHEAD's reflog, too. And when it came time to do so under lock, we found\nthat HEAD did not point to \"foo\" any more.\n\nShouldn't we quietly drop the HEAD reflog update, rather than forcing\nthe whole transaction to fail? The user never asked us to update HEAD at\nall. It was something we opportunistically decided to do, and now we\nfind out that it is not appropriate to do so.\n\n-Peff\n"},{"id":"523384","messageId":"aJBjVwxjaPTLCOZR@pks.im","threadId":"63818","inReplyTo":"87tt2ruv9z.fsf@iotcl.com","subject":"Re: [PATCH v3 3/9] refs: export `ref_transaction_update_reflog()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T07:37:59Z","receivedAt":"2025-08-04T07:38:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Aug 01, 2025 at 01:38:16PM +0200, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > In a subsequent commit we'll add another user that wants to write reflog\n> > entries. This requires them to call `ref_transaction_update_reflog()`,\n> > but that function is local to \"refs.c\".\n> >\n> > Export the function to prepare for the change. While at it, drop the\n> > `flags` field, as all callers are for now expected to use the same flags\n> > anyway.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  refs.c | 29 +++++++++++------------------\n> >  refs.h | 15 +++++++++++++++\n> >  2 files changed, 26 insertions(+), 18 deletions(-)\n> >\n> > diff --git a/refs.c b/refs.c\n> > index dce5c49ca2b..8aa9f7236a3 100644\n> > --- a/refs.c\n> > +++ b/refs.c\n> > @@ -1371,27 +1371,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n> >  \treturn 0;\n> >  }\n> >  \n> > -/*\n> > - * Similar to`ref_transaction_update`, but this function is only for adding\n> \n> Tiniest nit: for some reason the space after \"to\" fell away.\n\nThis is the preimage though :) I've fixed it in the postimage already.\n\nPatrick\n"},{"id":"523385","messageId":"aJBjbZqW-GuVkFzo@pks.im","threadId":"63818","inReplyTo":"87wm7nuvaz.fsf@iotcl.com","subject":"Re: [PATCH v3 4/9] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T07:38:21Z","receivedAt":"2025-08-04T07:38:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Aug 01, 2025 at 01:37:40PM +0200, Toon Claes wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > +test_expect_success 'abbreviated object IDs' '\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tgit init repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\ttest_must_fail git reflog write refs/heads/something 12345 $ZERO_OID old-object-id 2>err &&\n> \n> Is the object id rejected because it's short, or because there simply\n> doesn't exist an object that starts with `12345`? You're not really\n> testing the former, which you claim in the test name.\n\nGood point, let me use an existing-but-abbreviated object instead.\n\nPatrick\n"},{"id":"523386","messageId":"aJBjdWv9qUeBL25-@pks.im","threadId":"63818","inReplyTo":"20250802111128.GC1180347@coredump.intra.peff.net","subject":"Re: [PATCH v3 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T07:38:29Z","receivedAt":"2025-08-04T07:38:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Aug 02, 2025 at 07:11:28AM -0400, Jeff King wrote:\n> On Tue, Jul 29, 2025 at 10:55:25AM +0200, Patrick Steinhardt wrote:\n> \n> > Unfortunately, this change only helps with the second race. We cannot\n> > reliably plug the first race without locking the HEAD reference at the\n> > start of the transaction. Locking HEAD unconditionally would effectively\n> > serialize all writes though, and that doesn't seem like an option. Also,\n> > double checking its value at the end of the transaction is not an option\n> > either, as its target may have flip-flopped during the transaction.\n> \n> I agree we should not always take a lock on HEAD, since most refs would\n> not need it. But I wonder if we could do better by examining HEAD, then\n> taking a lock when we think we'll need it, and then re-checking the\n> value of HEAD. That is still racy, though (somebody could have pointed\n> HEAD at us between the two checks). Fundamentally the files backend is\n> not atomic across the whole namespace, and we are trying to update two\n> refs. So I think there will always be some race.\n\nWe do that though. When queueing the log-only update for HEAD we don't\nlock immediately, but we lock once we process that log-only update. And\nthat's where we now do the check whether HEAD has changed meanwhile,\nwhich should be race-free given that it did change indeed.\n\n> It does make me wonder if this race-fix is even worth it, then. We are\n> catching the case where somebody moves HEAD away from the ref we are\n> updating while we are updating it. But without atomicity, do we even\n> know which happened first? That is, would it be incorrect to update\n> HEAD anyway? I guess the outcome is observable because their movement of\n> HEAD generated a reflog entry, and thus the entries would be out of\n> order. So maybe that is worth it.\n\nSo with the above I think that we are race-free for the case where HEAD\nhas been modified after we have decided to write a reflog entry for it.\nWe aren't though in the case where we _haven't_ decided to write a\nreflog entry, as HEAD might have been adjusted to point to one of the\nupdated refs meanwhile as you rightfully point out.\n\nBut I think that's an acceptable tradeoff. I'd rather write no reflog\nentry than an incorrect one.\n\n> Anyway, I had two questions about the code:\n> \n> > @@ -2600,7 +2607,36 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n> >  \n> >  \tupdate->backend_data = lock;\n> >  \n> > -\tif (update->type & REF_ISSYMREF) {\n> > +\tif (update->flags & REF_LOG_VIA_SPLIT) {\n> > +\t\tstruct ref_lock *parent_lock;\n> > +\n> > +\t\tif (!update->parent_update)\n> > +\t\t\tBUG(\"split update without a parent\");\n> > +\n> > +\t\tparent_lock = update->parent_update->backend_data;\n> > +\n> > +\t\t/*\n> > +\t\t * Check that \"HEAD\" didn't racily change since we have looked\n> > +\t\t * it up. If it did we must refuse to write the reflog entry.\n> > +\t\t *\n> > +\t\t * Note that this does not catch all races: if \"HEAD\" was\n> > +\t\t * racily changed to point to one of the refs part of the\n> > +\t\t * transaction then we would miss writing the split reflog\n> > +\t\t * entry for \"HEAD\".\n> > +\t\t */\n> > +\t\tif (!(update->type & REF_ISSYMREF) ||\n> > +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n> > +\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n> > +\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n> > +\t\t\tgoto out;\n> > +\t\t}\n> \n> One, what happens with a multi-level ref (e.g., HEAD points to\n> refs/heads/foo which points to refs/heads/bar)?\n> \n> We've resolved HEAD to get referent.buf. Do we get \"foo\" or \"bar\" here?\n> If \"bar\", then a write through \"foo\" will complain. But if we get \"foo\",\n> then theoretically a write through \"bar\" will complain.\n> \n> I _think_ we are OK, though. Constructing it like this:\n> \n>   git init\n>   git commit --allow-empty -m whatever\n> \n>   git symbolic-ref refs/heads/foo refs/heads/bar\n>   git symbolic-ref HEAD refs/heads/foo\n>   git update-ref refs/heads/foo main\n> \n> triggers the check and shows that our referent from lock_raw_ref() is\n> the first level (i.e., \"foo\"). Which is good.\n> \n> If we swap out \"foo\" for \"bar\" in the update-ref call, then we'd get a\n> mismatch. But in that case we do not figure out that HEAD needs be\n> written at all! That is, we only do a single level of look-back to\n> decide whether to write HEAD at all. So as long as we keep doing so, we\n> are OK.\n\nYeah, the whole code is best-effort only and doesn't work for all kinds\nof edge cases. The mere fact that we only handle this case for HEAD is\nalready a limitation -- in theory we should do it for all symbolic refs.\n\n> > +\t\tif (!(update->type & REF_ISSYMREF) ||\n> > +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n> > +\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n> > +\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n> > +\t\t\tgoto out;\n> > +\t\t}\n> \n> And two, is an error the right thing here? The user asked us to update\n> \"foo\", and we saw that HEAD pointed to it. So we decided to update\n> HEAD's reflog, too. And when it came time to do so under lock, we found\n> that HEAD did not point to \"foo\" any more.\n> \n> Shouldn't we quietly drop the HEAD reflog update, rather than forcing\n> the whole transaction to fail? The user never asked us to update HEAD at\n> all. It was something we opportunistically decided to do, and now we\n> find out that it is not appropriate to do so.\n\nThat's something I wasn't quite sure about, either. Honestly, the reason\nI shied away from it is that it needs a bit more munging for an edge\ncase that is hard to test reliably. But I guess we can do something like\nthe below patch to skip writing the reflog message instea.\n\nPatrick\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 905555365b..851b1b33f4 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -643,14 +643,16 @@ int parse_loose_ref_contents(const struct git_hash_algo *algop,\n \treturn 0;\n }\n \n-static void unlock_ref(struct ref_lock *lock)\n+static int unlock_ref(struct ref_lock *lock)\n {\n \tlock->count--;\n \tif (!lock->count) {\n \t\trollback_lock_file(&lock->lk);\n \t\tfree(lock->ref_name);\n \t\tfree(lock);\n+\t\treturn 1;\n \t}\n+\treturn 0;\n }\n \n /*\n@@ -2557,6 +2559,9 @@ struct files_transaction_backend_data {\n  *   the referent to transaction.\n  * - If it is an update of head_ref, add a corresponding REF_LOG_ONLY\n  *   update of HEAD.\n+ *\n+ * Returns 0 on success, 1 in case the update needs to be dropped or a negative\n+ * error code otherwise.\n  */\n static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t\t\t\t      struct ref_update *update,\n@@ -2617,7 +2622,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \n \t\t/*\n \t\t * Check that \"HEAD\" didn't racily change since we have looked\n-\t\t * it up. If it did we must refuse to write the reflog entry.\n+\t\t * it up. If it did we remove the reflog-only updateg from the\n+\t\t * transaction again.\n \t\t *\n \t\t * Note that this does not catch all races: if \"HEAD\" was\n \t\t * racily changed to point to one of the refs part of the\n@@ -2626,8 +2632,16 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \t\t */\n \t\tif (!(update->type & REF_ISSYMREF) ||\n \t\t    strcmp(update->parent_update->refname, referent.buf)) {\n-\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n-\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n+\t\t\tif (unlock_ref(lock))\n+\t\t\t\tstrmap_remove(&backend_data->ref_locks,\n+\t\t\t\t\t      update->refname, 0);\n+\n+\t\t\tmemmove(transaction->updates + update_idx,\n+\t\t\t\ttransaction->updates + update_idx + 1,\n+\t\t\t\t(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));\n+\t\t\ttransaction->nr--;\n+\n+\t\t\tret = 1;\n \t\t\tgoto out;\n \t\t}\n \n@@ -2896,6 +2910,8 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t  head_ref, &refnames_to_check,\n \t\t\t\t\t  err);\n \t\tif (ret) {\n+\t\t\tif (ret > 0)\n+\t\t\t\tcontinue;\n \t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n \t\t\t\tstrbuf_reset(err);\n \t\t\t\tret = 0;\n\n"},{"id":"523421","messageId":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH v4 0/9] refs: fix migration of reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:00Z","receivedAt":"2025-08-04T09:46:12Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nafter the announcement that \"reftable\" will become the default backend\nin Git 3.0 I've revived the efforts to implement this backend in\nlibgit2. I'm happy to report that this implementation is almost done by\nnow: out of 3000 tests only four are failing now.\n\nFor two of these tests I have been completely puzzled why those are\nfailing, as everything really looked perfectly fine in libgit2. As it\nturned out, the bug wasn't in libgit2 though, but in Git. Namely, the\nway we migrate reflog entries between storage formats is broken in two\nways:\n\n  - The identity we write into the reflog entries is wrong.\n\n  - The old commit ID of reflog entries is always set to all-zeroes.\n    This is what caused the libgit2 tests to fail, as I used `git refs\n    migrate` to convert test repositories to use reftables.\n\nThis patch series fixes both of these issues. Furthermore, it also adds\na new `git reflog write` subcommand to write new reflog entries for a\nspecific reference. This command was helpful to reproduce some test\nconstellations in libgit2.\n\nChanges in v2:\n  - !!! The base of this topic has changed so that it sits on top of\n    v2.50.1. This is done so that we can backport this change to older\n    release tracks.\n  - A couple of typo fixes and clarifications for commit messages.\n  - Reorder sections in git-reflog(1) manpage according to the\n    reordering we have in the synopsis.\n  - Add a section for the new `write` command.\n  - Improve test coverage for the `git reflog write` command.\n  - Avoid `cat`ing a file into a Bash loop.\n  - Remove a stale comment.\n  - Make `ref_update_expects_existing_old_ref()` a bit more straight\n    forward.\n  - Link to v1: https://lore.kernel.org/r/20250722-pks-reflog-append-v1-0-183e5949de16@pks.im\n\nChanges in v3:\n  - `git reflog write` now requires fully-qualified refnames.\n  - A new commit that plugs one part of the race around splitting of\n    reflogs for HEAD in the \"files\" backend.\n  - Link to v2: https://lore.kernel.org/r/20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im\n\nChanges in v4:\n  - Improve one of the tests to use an existing abbreviated object ID\n    instead of a non-existing one to make sure that we indeed fail due\n    to the abbreviation.\n  - Don't abort the transaction when HEAD has been racily updated, but\n    drop the log-only update instead.\n  - Link to v3: https://lore.kernel.org/r/20250729-pks-reflog-append-v3-0-9614d310f073@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (9):\n      Documentation/git-reflog: convert to use synopsis type\n      builtin/reflog: improve grouping of subcommands\n      refs: export `ref_transaction_update_reflog()`\n      builtin/reflog: implement subcommand to write new entries\n      ident: fix type of string length parameter\n      refs: fix identity for migrated reflogs\n      refs/files: detect race when generating reflog entry for HEAD\n      refs: stop unsetting REF_HAVE_OLD for log-only updates\n      refs: fix invalid old object IDs when migrating reflogs\n\n Documentation/git-reflog.adoc |  76 +++++++++++++------------\n builtin/reflog.c              | 103 +++++++++++++++++++++++++++-------\n ident.c                       |   2 +-\n ident.h                       |   2 +-\n refs.c                        |  60 +++++++++++---------\n refs.h                        |  24 +++++++-\n refs/files-backend.c          |  83 +++++++++++++++++++++++++---\n refs/refs-internal.h          |   3 +-\n refs/reftable-backend.c       |  26 ++++++---\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 126 ++++++++++++++++++++++++++++++++++++++++++\n t/t1460-refs-migrate.sh       |  22 +++++---\n 12 files changed, 420 insertions(+), 108 deletions(-)\n\nRange-diff versus v3:\n\n 1:  5ae2971a55 =  1:  d030117041 Documentation/git-reflog: convert to use synopsis type\n 2:  256589c289 =  2:  ad1dd8a226 builtin/reflog: improve grouping of subcommands\n 3:  3b9e0a1206 =  3:  d6d6d99421 refs: export `ref_transaction_update_reflog()`\n 4:  4fcf540ed6 !  4:  4e5433717e builtin/reflog: implement subcommand to write new entries\n    @@ t/t1421-reflog-write.sh (new)\n     +\tgit init repo &&\n     +\t(\n     +\t\tcd repo &&\n    -+\t\ttest_must_fail git reflog write refs/heads/something 12345 $ZERO_OID old-object-id 2>err &&\n    ++\t\ttest_commit initial &&\n    ++\t\tabbreviated_oid=$(git rev-parse HEAD | test_copy_bytes 8) &&\n    ++\t\ttest_must_fail git reflog write refs/heads/something $abbreviated_oid $ZERO_OID old-object-id 2>err &&\n     +\t\ttest_grep \"invalid old object ID\" err &&\n    -+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID 12345 new-object-id 2>err &&\n    ++\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $abbreviated_oid new-object-id 2>err &&\n     +\t\ttest_grep \"invalid new object ID\" err\n     +\t)\n     +'\n 5:  18b2f61366 =  5:  92e45f582c ident: fix type of string length parameter\n 6:  d140c53224 =  6:  e50c5aaae5 refs: fix identity for migrated reflogs\n 7:  91c6a7cbcb !  7:  9380dbfdab refs/files: detect race when generating reflog entry for HEAD\n    @@ Commit message\n             have already processed. This causes us not writing a reflog message\n             even though we should have done so.\n     \n    -      - HEAD gets concurrently updated to point to not point to a reference\n    +      - HEAD gets concurrently updated to no longer point to a reference\n             anymore that we have already processed. This causes us to write a\n             reflog message even though we should _not_ have done so.\n     \n    @@ refs/files-backend.c\n      struct ref_lock {\n      \tchar *ref_name;\n      \tstruct lock_file lk;\n    +@@ refs/files-backend.c: int parse_loose_ref_contents(const struct git_hash_algo *algop,\n    + \treturn 0;\n    + }\n    + \n    +-static void unlock_ref(struct ref_lock *lock)\n    ++static int unlock_ref(struct ref_lock *lock)\n    + {\n    + \tlock->count--;\n    + \tif (!lock->count) {\n    + \t\trollback_lock_file(&lock->lk);\n    + \t\tfree(lock->ref_name);\n    + \t\tfree(lock);\n    ++\t\treturn 1;\n    + \t}\n    ++\treturn 0;\n    + }\n    + \n    + /*\n     @@ refs/files-backend.c: static enum ref_transaction_error split_head_update(struct ref_update *update,\n      \n      \tnew_update = ref_transaction_add_update(\n    @@ refs/files-backend.c: static enum ref_transaction_error split_head_update(struct\n      \n      \t/*\n      \t * Add \"HEAD\". This insertion is O(N) in the transaction\n    +@@ refs/files-backend.c: struct files_transaction_backend_data {\n    +  *   the referent to transaction.\n    +  * - If it is an update of head_ref, add a corresponding REF_LOG_ONLY\n    +  *   update of HEAD.\n    ++ *\n    ++ * Returns 0 on success, 1 in case the update needs to be dropped or a negative\n    ++ * error code otherwise.\n    +  */\n    + static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,\n    + \t\t\t\t\t\t      struct ref_update *update,\n     @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n      \n      \tupdate->backend_data = lock;\n    @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(stru\n     +\n     +\t\t/*\n     +\t\t * Check that \"HEAD\" didn't racily change since we have looked\n    -+\t\t * it up. If it did we must refuse to write the reflog entry.\n    ++\t\t * it up. If it did we remove the reflog-only updateg from the\n    ++\t\t * transaction again.\n     +\t\t *\n     +\t\t * Note that this does not catch all races: if \"HEAD\" was\n     +\t\t * racily changed to point to one of the refs part of the\n    @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(stru\n     +\t\t */\n     +\t\tif (!(update->type & REF_ISSYMREF) ||\n     +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n    -+\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n    -+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n    ++\t\t\tif (unlock_ref(lock))\n    ++\t\t\t\tstrmap_remove(&backend_data->ref_locks,\n    ++\t\t\t\t\t      update->refname, 0);\n    ++\n    ++\t\t\tmemmove(transaction->updates + update_idx,\n    ++\t\t\t\ttransaction->updates + update_idx + 1,\n    ++\t\t\t\t(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));\n    ++\t\t\ttransaction->nr--;\n    ++\n    ++\t\t\tret = 1;\n     +\t\t\tgoto out;\n     +\t\t}\n     +\n    @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(stru\n      \t\tif (update->flags & REF_NO_DEREF) {\n      \t\t\t/*\n      \t\t\t * We won't be reading the referent as part of\n    +@@ refs/files-backend.c: static int files_transaction_prepare(struct ref_store *ref_store,\n    + \t\t\t\t\t  head_ref, &refnames_to_check,\n    + \t\t\t\t\t  err);\n    + \t\tif (ret) {\n    ++\t\t\tif (ret > 0)\n    ++\t\t\t\tcontinue;\n    + \t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n    + \t\t\t\tstrbuf_reset(err);\n    + \t\t\t\tret = 0;\n 8:  8468947824 =  8:  3c6182c96d refs: stop unsetting REF_HAVE_OLD for log-only updates\n 9:  78ca2d46f9 =  9:  eafd8f6d7d refs: fix invalid old object IDs when migrating reflogs\n\n---\nbase-commit: d82adb61ba2fd11d8f2587fca1b6bd7925ce4044\nchange-id: 20250722-pks-reflog-append-634172d8ab2c\n\n"},{"id":"523422","messageId":"20250804-pks-reflog-append-v4-1-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 1/9] Documentation/git-reflog: convert to use synopsis type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:01Z","receivedAt":"2025-08-04T09:46:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\nhave introduced a new synopsis type that simplifies the rules for\ntypesetting a command's synopsis. Convert the git-reflog(1)\ndocumentation to use it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 14 +++++++-------\n 1 file changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 412f06b8fe..707a9b39ed 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n \n SYNOPSIS\n --------\n-[verse]\n-'git reflog' [show] [<log-options>] [<ref>]\n-'git reflog list'\n-'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n+[synopsis]\n+git reflog [show] [<log-options>] [<ref>]\n+git reflog list\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n \t[--rewrite] [--updateref] [--stale-fix]\n \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n-'git reflog delete' [--rewrite] [--updateref]\n+git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n-'git reflog drop' [--all [--single-worktree] | <refs>...]\n-'git reflog exists' <ref>\n+git reflog drop [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n \n DESCRIPTION\n -----------\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523423","messageId":"20250804-pks-reflog-append-v4-2-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 2/9] builtin/reflog: improve grouping of subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:02Z","receivedAt":"2025-08-04T09:46:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The way subcommands of git-reflog(1) are laid out does not make any\nimmediate sense. Reorder them such that read-only subcommands precede\nwriting commands for a bit more structure.\n\nFurthermore, move the \"expire\" subcommand last. This prepares for a\nsubsequent change where we are about to introduce a new \"write\" command\nto append reflog entries. Like this, the writing subcommands are ordered\nsuch that those affecting a single reflog come before those spanning\nacross all reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 61 ++++++++++++++++++++++---------------------\n builtin/reflog.c              | 38 +++++++++++++--------------\n 2 files changed, 50 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 707a9b39ed..c3801b82fb 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -11,13 +11,13 @@ SYNOPSIS\n [synopsis]\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n-git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n-\t[--rewrite] [--updateref] [--stale-fix]\n-\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n-git reflog exists <ref>\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n+\t[--rewrite] [--updateref] [--stale-fix]\n+\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,9 @@ actions, and in addition the `HEAD` reflog records branch switching.\n \n The \"list\" subcommand lists all refs which have a corresponding reflog.\n \n-The \"expire\" subcommand prunes older reflog entries. Entries older\n-than `expire` time, or entries older than `expire-unreachable` time\n-and not reachable from the current tip, are removed from the reflog.\n-This is typically not used directly by end users -- instead, see\n-linkgit:git-gc[1].\n+The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n+with zero status if the reflog exists, and non-zero status if it does\n+not.\n \n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n@@ -58,9 +56,11 @@ The \"drop\" subcommand completely removes the reflog for the specified\n references. This is in contrast to \"expire\" and \"delete\", both of which\n can be used to delete reflog entries, but not the reflog itself.\n \n-The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n-with zero status if the reflog exists, and non-zero status if it does\n-not.\n+The \"expire\" subcommand prunes older reflog entries. Entries older\n+than `expire` time, or entries older than `expire-unreachable` time\n+and not reachable from the current tip, are removed from the reflog.\n+This is typically not used directly by end users -- instead, see\n+linkgit:git-gc[1].\n \n OPTIONS\n -------\n@@ -71,6 +71,25 @@ Options for `show`\n `git reflog show` accepts any of the options accepted by `git log`.\n \n \n+Options for `delete`\n+~~~~~~~~~~~~~~~~~~~~\n+\n+`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n+`--dry-run`, and `--verbose`, with the same meanings as when they are\n+used with `expire`.\n+\n+Options for `drop`\n+~~~~~~~~~~~~~~~~~~\n+\n+--all::\n+\tDrop the reflogs of all references from all worktrees.\n+\n+--single-worktree::\n+\tBy default when `--all` is specified, reflogs from all working\n+\ttrees are dropped. This option limits the processing to reflogs\n+\tfrom the current working tree only.\n+\n+\n Options for `expire`\n ~~~~~~~~~~~~~~~~~~~~\n \n@@ -130,24 +149,6 @@ which didn't protect objects referred to by reflogs.\n \tPrint extra information on screen.\n \n \n-Options for `delete`\n-~~~~~~~~~~~~~~~~~~~~\n-\n-`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n-`--dry-run`, and `--verbose`, with the same meanings as when they are\n-used with `expire`.\n-\n-Options for `drop`\n-~~~~~~~~~~~~~~~~~~\n-\n---all::\n-\tDrop the reflogs of all references from all worktrees.\n-\n---single-worktree::\n-\tBy default when `--all` is specified, reflogs from all working\n-\ttrees are dropped. This option limits the processing to reflogs\n-\tfrom the current working tree only.\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 3acaf3e32c..b00b3f9edc 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -17,21 +17,21 @@\n #define BUILTIN_REFLOG_LIST_USAGE \\\n \tN_(\"git reflog list\")\n \n-#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n-\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n-\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n-\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+#define BUILTIN_REFLOG_EXISTS_USAGE \\\n+\tN_(\"git reflog exists <ref>\")\n \n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n \n-#define BUILTIN_REFLOG_EXISTS_USAGE \\\n-\tN_(\"git reflog exists <ref>\")\n-\n #define BUILTIN_REFLOG_DROP_USAGE \\\n \tN_(\"git reflog drop [--all [--single-worktree] | <refs>...]\")\n \n+#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n+\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n+\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n+\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+\n static const char *const reflog_show_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tNULL,\n@@ -42,9 +42,9 @@ static const char *const reflog_list_usage[] = {\n \tNULL,\n };\n \n-static const char *const reflog_expire_usage[] = {\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n-\tNULL\n+static const char *const reflog_exists_usage[] = {\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tNULL,\n };\n \n static const char *const reflog_delete_usage[] = {\n@@ -52,23 +52,23 @@ static const char *const reflog_delete_usage[] = {\n \tNULL\n };\n \n-static const char *const reflog_exists_usage[] = {\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n-\tNULL,\n-};\n-\n static const char *const reflog_drop_usage[] = {\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tNULL,\n };\n \n+static const char *const reflog_expire_usage[] = {\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tNULL\n+};\n+\n static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n \tNULL\n };\n \n@@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n-\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n-\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523424","messageId":"20250804-pks-reflog-append-v4-3-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 3/9] refs: export `ref_transaction_update_reflog()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:03Z","receivedAt":"2025-08-04T09:46:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we'll add another user that wants to write reflog\nentries. This requires them to call `ref_transaction_update_reflog()`,\nbut that function is local to \"refs.c\".\n\nExport the function to prepare for the change. While at it, drop the\n`flags` field, as all callers are for now expected to use the same flags\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 29 +++++++++++------------------\n refs.h | 15 +++++++++++++++\n 2 files changed, 26 insertions(+), 18 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex dce5c49ca2..8aa9f7236a 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1371,27 +1371,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \treturn 0;\n }\n \n-/*\n- * Similar to`ref_transaction_update`, but this function is only for adding\n- * a reflog update. Supports providing custom committer information. The index\n- * field can be utiltized to order updates as desired. When not used, the\n- * updates default to being ordered by refname.\n- */\n-static int ref_transaction_update_reflog(struct ref_transaction *transaction,\n-\t\t\t\t\t const char *refname,\n-\t\t\t\t\t const struct object_id *new_oid,\n-\t\t\t\t\t const struct object_id *old_oid,\n-\t\t\t\t\t const char *committer_info,\n-\t\t\t\t\t unsigned int flags,\n-\t\t\t\t\t const char *msg,\n-\t\t\t\t\t uint64_t index,\n-\t\t\t\t\t struct strbuf *err)\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err)\n {\n \tstruct ref_update *update;\n+\tunsigned int flags;\n \n \tassert(err);\n \n-\tflags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\n@@ -3019,8 +3013,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n-\t\t\t\t\t    REF_HAVE_NEW | REF_HAVE_OLD, msg,\n-\t\t\t\t\t    data->index++, data->errbuf);\n+\t\t\t\t\t    msg, data->index++, data->errbuf);\n \treturn ret;\n }\n \ndiff --git a/refs.h b/refs.h\nindex 46a6008e07..253dd8f4d5 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -795,6 +795,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \n+/*\n+ * Similar to `ref_transaction_update`, but this function is only for adding\n+ * a reflog update. Supports providing custom committer information. The index\n+ * field can be utiltized to order updates as desired. When set to zero, the\n+ * updates default to being ordered by refname.\n+ */\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err);\n+\n /*\n  * Add a reference creation to transaction. new_oid is the value that\n  * the reference should have after the update; it must not be\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523425","messageId":"20250804-pks-reflog-append-v4-4-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 4/9] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:04Z","receivedAt":"2025-08-04T09:46:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we provide a couple of subcommands in git-reflog(1) to remove\nreflog entries, we don't provide any to write new entries. Obviously\nthis is not an operation that really would be needed for many use cases\nout there, or otherwise people would have complained that such a command\ndoes not exist yet. But the introduction of the \"reftable\" backend\nchanges the picture a bit, as it is now basically impossible to manually\nappend a reflog entry if one wanted to do so due to the binary format.\n\nPlug this gap by introducing a simple \"write\" subcommand. For now, all\nthis command does is to append a single new reflog entry with the given\nobject IDs and message to the reflog. More specifically, it is not yet\npossible to:\n\n  - Write multiple reflog entries at once.\n\n  - Insert reflog entries at arbitrary indices.\n\n  - Specify the date of the reflog entry.\n\n  - Insert reflog entries that refer to nonexistent objects.\n\nIf required, those features can be added at a future point in time. For\nnow though, the new command aims to fulfill the most basic use cases\nwhile being as strict as possible when it comes to verifying parameters.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc |   7 +++\n builtin/reflog.c              |  65 +++++++++++++++++++++\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 128 ++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 201 insertions(+)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex c3801b82fb..34232a539a 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n git reflog exists <ref>\n+git reflog write <ref> <old-oid> <new-oid> <message>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n@@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n with zero status if the reflog exists, and non-zero status if it does\n not.\n \n+The \"write\" subcommand writes a single entry to the reflog of a given\n+reference. This new entry is appended to the reflog and will thus become\n+the most recent entry. The reference name must be fully qualified. Both the old\n+and new object IDs must not be abbreviated and must point to existing objects.\n+The reflog message gets normalized.\n+\n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n reflog delete master@{2}`\"). This subcommand is also typically not used\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b00b3f9edc..a1b4e02204 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -3,6 +3,8 @@\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-store.h\"\n #include \"revision.h\"\n #include \"reachable.h\"\n #include \"wildmatch.h\"\n@@ -20,6 +22,9 @@\n #define BUILTIN_REFLOG_EXISTS_USAGE \\\n \tN_(\"git reflog exists <ref>\")\n \n+#define BUILTIN_REFLOG_WRITE_USAGE \\\n+\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n+\n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n@@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n \tNULL,\n };\n \n+static const char *const reflog_write_usage[] = {\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n+\tNULL,\n+};\n+\n static const char *const reflog_delete_usage[] = {\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tNULL\n@@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n \tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tBUILTIN_REFLOG_EXPIRE_USAGE,\n@@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n+\t\t\t    struct repository *repo)\n+{\n+\tconst struct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct object_id old_oid, new_oid;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct ref_transaction *tx;\n+\tconst char *ref, *message;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);\n+\tif (argc != 4)\n+\t\tusage_with_options(reflog_write_usage, options);\n+\n+\tref = argv[0];\n+\tif (!is_root_ref(ref) && check_refname_format(ref, 0))\n+\t\tdie(_(\"invalid reference name: %s\"), ref);\n+\n+\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n+\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n+\n+\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n+\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n+\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n+\n+\tmessage = argv[3];\n+\n+\ttx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\tif (!tx)\n+\t\tdie(_(\"cannot start transaction: %s\"), err.buf);\n+\n+\tret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,\n+\t\t\t\t\t    git_committer_info(0),\n+\t\t\t\t\t    message, 0, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot queue reflog update: %s\"), err.buf);\n+\n+\tret = ref_transaction_commit(tx, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot commit reflog update: %s\"), err.buf);\n+\n+\tref_transaction_free(tx);\n+\tstrbuf_release(&err);\n+\treturn 0;\n+}\n+\n /*\n  * main \"reflog\"\n  */\n@@ -405,6 +469,7 @@ int cmd_reflog(int argc,\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_reflog_write),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n \t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\ndiff --git a/t/meson.build b/t/meson.build\nindex d052fc3e23..adcdf09e74 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -220,6 +220,7 @@ integration_tests = [\n   't1418-reflog-exists.sh',\n   't1419-exclude-refs.sh',\n   't1420-lost-found.sh',\n+  't1421-reflog-write.sh',\n   't1430-bad-ref-name.sh',\n   't1450-fsck.sh',\n   't1451-fsck-buffer.sh',\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nnew file mode 100755\nindex 0000000000..dd7ffa5241\n--- /dev/null\n+++ b/t/t1421-reflog-write.sh\n@@ -0,0 +1,128 @@\n+#!/bin/sh\n+\n+test_description='Manually write reflog entries'\n+\n+. ./test-lib.sh\n+\n+SIGNATURE=\"C O Mitter <committer@example.com> 1112911993 -0700\"\n+\n+test_reflog_matches () {\n+\trepo=\"$1\" &&\n+\trefname=\"$2\" &&\n+\tcat >actual &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$refname\" >expected &&\n+\ttest_cmp expected actual\n+}\n+\n+test_expect_success 'invalid number of arguments' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tfor args in \"\" \"1\" \"1 2\" \"1 2 3\" \"1 2 3 4 5\"\n+\t\tdo\n+\t\t\ttest_must_fail git reflog write $args 2>err &&\n+\t\t\ttest_grep \"usage: git reflog write\" err || return 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success 'invalid refname' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write \"refs/heads/ invalid\" $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'unqualified refname is rejected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write unqualified $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'nonexistent object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"old object .* does not exist\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&\n+\t\ttest_grep \"new object .* does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'abbreviated object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tabbreviated_oid=$(git rev-parse HEAD | test_copy_bytes 8) &&\n+\t\ttest_must_fail git reflog write refs/heads/something $abbreviated_oid $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $abbreviated_oid new-object-id 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err\n+\t)\n+'\n+\n+test_expect_success 'reflog message gets normalized' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\t\tgit reflog write HEAD $COMMIT_OID $COMMIT_OID \"$(printf \"message\\nwith\\nnewlines\")\" &&\n+\t\tgit reflog show -1 --format=%gs HEAD >actual &&\n+\t\techo \"message with newlines\" >expected &&\n+\t\ttest_cmp expected actual\n+\t)\n+'\n+\n+test_expect_success 'simple writes' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF &&\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n+\t\t# Note: the old object ID of the second reflog entry is broken.\n+\t\t# This will be fixed in subsequent commits.\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can write to root ref' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write ROOT_REF_HEAD $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . ROOT_REF_HEAD <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523426","messageId":"20250804-pks-reflog-append-v4-5-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 5/9] ident: fix type of string length parameter","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:05Z","receivedAt":"2025-08-04T09:46:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The last parameter in `split_ident_line()` is the length of the line\npassed in by the caller. As such, most callers pass in either the result\nof `strlen()`, `struct strbuf::len` or a pointer diff, all of which\nare expected to be positive numbers. Regardless of that, the function\naccepts a signed integer, which is somewhat confusing.\n\nFix the function signature to instead accept a `size_t`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ident.c | 2 +-\n ident.h | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ident.c b/ident.c\nindex 967895d8850..a7a2d132579 100644\n--- a/ident.c\n+++ b/ident.c\n@@ -272,7 +272,7 @@ static void strbuf_addstr_without_crud(struct strbuf *sb, const char *src)\n  * can still be NULL if the input line only has the name/email part\n  * (e.g. reading from a reflog entry).\n  */\n-int split_ident_line(struct ident_split *split, const char *line, int len)\n+int split_ident_line(struct ident_split *split, const char *line, size_t len)\n {\n \tconst char *cp;\n \tsize_t span;\ndiff --git a/ident.h b/ident.h\nindex 6a79febba15..3c034038791 100644\n--- a/ident.h\n+++ b/ident.h\n@@ -35,7 +35,7 @@ void reset_ident_date(void);\n  * Signals an success with 0, but time part of the result may be NULL\n  * if the input lacks timestamp and zone\n  */\n-int split_ident_line(struct ident_split *, const char *, int);\n+int split_ident_line(struct ident_split *, const char *, size_t);\n \n /*\n  * Given a commit or tag object buffer and the commit or tag headers, replaces\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523427","messageId":"20250804-pks-reflog-append-v4-6-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 6/9] refs: fix identity for migrated reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:06Z","receivedAt":"2025-08-04T09:46:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we must\nreconstruct the identity of reflog entries. This is done by passing the\ncommitter passed to the `migrate_one_reflog_entry()` callback function\nto `fmt_ident()`.\n\nThis results in an invalid identity though: `fmt_ident()` expects the\ncaller to provide both name and mail of the author, but we pass the full\nidentity as mail. This leads to an identity like:\n\n    pks <Patrick Steinhardt ps@pks.im>\n\nFix the bug by splitting the identity line first. This allows us to\nextract both the name and mail so that we can pass them to `fmt_ident()`\nseparately.\n\nThis commit does not yet add any tests as there is another bug in the\nreflog migration that will be fixed in a subsequent commit. Once that\nbug is fixed we'll make the reflog verification in t1450 stricter, and\nthat will catch both this bug here and the other bug.\n\nNote that we also add two new `name` and `mail` string buffers to the\ncallback structures and splice them through to the callbacks. This is\ndone so that we can avoid allocating a new buffer every time we compute\nthe committer information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 22 ++++++++++++++++++----\n 1 file changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 8aa9f7236a..a5f9ffaa45 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2954,7 +2954,7 @@ struct migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf sb;\n+\tstruct strbuf sb, name, mail;\n };\n \n static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n@@ -2993,7 +2993,7 @@ struct reflog_migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf *sb;\n+\tstruct strbuf *sb, *name, *mail;\n };\n \n static int migrate_one_reflog_entry(struct object_id *old_oid,\n@@ -3003,13 +3003,21 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \t\t\t\t    const char *msg, void *cb_data)\n {\n \tstruct reflog_migration_data *data = cb_data;\n+\tstruct ident_split ident;\n \tconst char *date;\n \tint ret;\n \n+\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n+\t\treturn -1;\n+\n+\tstrbuf_reset(data->name);\n+\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n+\tstrbuf_reset(data->mail);\n+\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n+\n \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n \tstrbuf_reset(data->sb);\n-\t/* committer contains name and email */\n-\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n+\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n@@ -3026,6 +3034,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n \t\t.transaction = migration_data->transaction,\n \t\t.errbuf = migration_data->errbuf,\n \t\t.sb = &migration_data->sb,\n+\t\t.name = &migration_data->name,\n+\t\t.mail = &migration_data->mail,\n \t};\n \n \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n@@ -3124,6 +3134,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tstruct strbuf new_gitdir = STRBUF_INIT;\n \tstruct migration_data data = {\n \t\t.sb = STRBUF_INIT,\n+\t\t.name = STRBUF_INIT,\n+\t\t.mail = STRBUF_INIT,\n \t};\n \tint did_migrate_refs = 0;\n \tint ret;\n@@ -3299,6 +3311,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tref_transaction_free(transaction);\n \tstrbuf_release(&new_gitdir);\n \tstrbuf_release(&data.sb);\n+\tstrbuf_release(&data.name);\n+\tstrbuf_release(&data.mail);\n \treturn ret;\n }\n \n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523428","messageId":"20250804-pks-reflog-append-v4-7-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:07Z","receivedAt":"2025-08-04T09:46:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When updating a reference that is being pointed to HEAD we don't only\nwrite a reflog message for that particular reference, but also generate\none for HEAD. This logic is handled by `split_head_update()`, where we:\n\n  1. Verify that the condition actually triggered. This is done by\n     reading HEAD at the start of the transaction so that we can then\n     check whether a given reference update refers to its target.\n\n  2. Queue a new log-only update for HEAD in case it did.\n\nBut the logic is unfortunately not free of races, as we do not lock the\nHEAD reference after we have read its target. This can lead to the\nfollowing two scenarios:\n\n  - HEAD gets concurrently updated to point to one of the references we\n    have already processed. This causes us not writing a reflog message\n    even though we should have done so.\n\n  - HEAD gets concurrently updated to no longer point to a reference\n    anymore that we have already processed. This causes us to write a\n    reflog message even though we should _not_ have done so.\n\nImprove the situation by introducing a new `REF_LOG_VIA_SPLIT` flag that\nis specific to the \"files\" backend. If set, we will double check that\nthe HEAD reference still points to the reference that we are creating\nthe reflog entry for after we have locked HEAD. Furthermore, instead of\nmanually resolving the old object ID of that entry, we now use the same\nold state as for the parent update.\n\nUnfortunately, this change only helps with the second race. We cannot\nreliably plug the first race without locking the HEAD reference at the\nstart of the transaction. Locking HEAD unconditionally would effectively\nserialize all writes though, and that doesn't seem like an option. Also,\ndouble checking its value at the end of the transaction is not an option\neither, as its target may have flip-flopped during the transaction.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++++---\n 1 file changed, 55 insertions(+), 3 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex bf6f89b1d1..d0baa4e01c 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -68,6 +68,12 @@\n  */\n #define REF_DELETED_RMDIR (1 << 9)\n \n+/*\n+ * Used to indicate that the reflog-only update has been created via\n+ * `split_head_update()`.\n+ */\n+#define REF_LOG_VIA_SPLIT (1 << 14)\n+\n struct ref_lock {\n \tchar *ref_name;\n \tstruct lock_file lk;\n@@ -637,14 +643,16 @@ int parse_loose_ref_contents(const struct git_hash_algo *algop,\n \treturn 0;\n }\n \n-static void unlock_ref(struct ref_lock *lock)\n+static int unlock_ref(struct ref_lock *lock)\n {\n \tlock->count--;\n \tif (!lock->count) {\n \t\trollback_lock_file(&lock->lk);\n \t\tfree(lock->ref_name);\n \t\tfree(lock);\n+\t\treturn 1;\n \t}\n+\treturn 0;\n }\n \n /*\n@@ -2420,9 +2428,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,\n \n \tnew_update = ref_transaction_add_update(\n \t\t\ttransaction, \"HEAD\",\n-\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF,\n+\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF | REF_LOG_VIA_SPLIT,\n \t\t\t&update->new_oid, &update->old_oid,\n \t\t\tNULL, NULL, update->committer_info, update->msg);\n+\tnew_update->parent_update = update;\n \n \t/*\n \t * Add \"HEAD\". This insertion is O(N) in the transaction\n@@ -2550,6 +2559,9 @@ struct files_transaction_backend_data {\n  *   the referent to transaction.\n  * - If it is an update of head_ref, add a corresponding REF_LOG_ONLY\n  *   update of HEAD.\n+ *\n+ * Returns 0 on success, 1 in case the update needs to be dropped or a negative\n+ * error code otherwise.\n  */\n static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,\n \t\t\t\t\t\t      struct ref_update *update,\n@@ -2600,7 +2612,45 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \n \tupdate->backend_data = lock;\n \n-\tif (update->type & REF_ISSYMREF) {\n+\tif (update->flags & REF_LOG_VIA_SPLIT) {\n+\t\tstruct ref_lock *parent_lock;\n+\n+\t\tif (!update->parent_update)\n+\t\t\tBUG(\"split update without a parent\");\n+\n+\t\tparent_lock = update->parent_update->backend_data;\n+\n+\t\t/*\n+\t\t * Check that \"HEAD\" didn't racily change since we have looked\n+\t\t * it up. If it did we remove the reflog-only updateg from the\n+\t\t * transaction again.\n+\t\t *\n+\t\t * Note that this does not catch all races: if \"HEAD\" was\n+\t\t * racily changed to point to one of the refs part of the\n+\t\t * transaction then we would miss writing the split reflog\n+\t\t * entry for \"HEAD\".\n+\t\t */\n+\t\tif (!(update->type & REF_ISSYMREF) ||\n+\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n+\t\t\tif (unlock_ref(lock))\n+\t\t\t\tstrmap_remove(&backend_data->ref_locks,\n+\t\t\t\t\t      update->refname, 0);\n+\n+\t\t\tmemmove(transaction->updates + update_idx,\n+\t\t\t\ttransaction->updates + update_idx + 1,\n+\t\t\t\t(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));\n+\t\t\ttransaction->nr--;\n+\n+\t\t\tret = 1;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (update->flags & REF_HAVE_OLD) {\n+\t\t\toidcpy(&lock->old_oid, &update->old_oid);\n+\t\t} else {\n+\t\t\toidcpy(&lock->old_oid, &parent_lock->old_oid);\n+\t\t}\n+\t} else if (update->type & REF_ISSYMREF) {\n \t\tif (update->flags & REF_NO_DEREF) {\n \t\t\t/*\n \t\t\t * We won't be reading the referent as part of\n@@ -2860,6 +2910,8 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t  head_ref, &refnames_to_check,\n \t\t\t\t\t  err);\n \t\tif (ret) {\n+\t\t\tif (ret > 0)\n+\t\t\t\tcontinue;\n \t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n \t\t\t\tstrbuf_reset(err);\n \t\t\t\tret = 0;\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523429","messageId":"20250804-pks-reflog-append-v4-8-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 8/9] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:08Z","receivedAt":"2025-08-04T09:46:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\nobject ID set. If so, the value of that field is used to verify whether\nthe current state of the reference matches this expected state. It is\nthus an important part of mitigating races with a concurrent process\nthat updates the same set of references.\n\nWhen writing reflogs though we explicitly unset that flag. This is a\nsensible thing to do: the old state of reflog entry updates may not\nnecessarily match the current on-disk state of its accompanying ref, but\nit's only intended to signal what old object ID we want to write into\nthe new reflog entry. For example when migrating refs we end up writing\nmany reflog entries for a single reference, and most likely those reflog\nentries will have many different old object IDs.\n\nBut unsetting this flag also removes a useful signal, namely that the\ncaller _did_ provide an old object ID for a given reflog entry. This\nsignal will become useful in a subsequent commit, where we add a new\nflag that tells the transaction to use the provided old and new object\nIDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\nsignal to verify that the caller really did provide an old object ID.\n\nStop unsetting the flag so that we can use it as this described signal\nin a subsequent commit. Skip checking the old object ID for log-only\nupdates so that we don't expect it to match the current on-disk state.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  8 +++-----\n refs/files-backend.c    |  9 +++++----\n refs/refs-internal.h    |  3 ++-\n refs/reftable-backend.c | 12 +++---------\n 4 files changed, 13 insertions(+), 19 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex a5f9ffaa45..f88928de74 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1393,11 +1393,6 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \tupdate = ref_transaction_add_update(transaction, refname, flags,\n \t\t\t\t\t    new_oid, old_oid, NULL, NULL,\n \t\t\t\t\t    committer_info, msg);\n-\t/*\n-\t * While we do set the old_oid value, we unset the flag to skip\n-\t * old_oid verification which only makes sense for refs.\n-\t */\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \tupdate->index = index;\n \n \t/*\n@@ -3318,6 +3313,9 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \n int ref_update_expects_existing_old_ref(struct ref_update *update)\n {\n+\tif (update->flags & REF_LOG_ONLY)\n+\t\treturn 0;\n+\n \treturn (update->flags & REF_HAVE_OLD) &&\n \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex d0baa4e01c..c37fbfd138 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2502,7 +2502,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n \t * done when new_update is processed.\n \t */\n \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \n \treturn 0;\n }\n@@ -2517,8 +2516,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n \t\t\t\t\t\tstruct object_id *oid,\n \t\t\t\t\t\tstruct strbuf *err)\n {\n-\tif (!(update->flags & REF_HAVE_OLD) ||\n-\t\t   oideq(oid, &update->old_oid))\n+\tif (update->flags & REF_LOG_ONLY ||\n+\t    !(update->flags & REF_HAVE_OLD) ||\n+\t    oideq(oid, &update->old_oid))\n \t\treturn 0;\n \n \tif (is_null_oid(&update->old_oid)) {\n@@ -3111,7 +3111,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n \n-\t\tif ((update->flags & REF_HAVE_OLD) &&\n+\t\tif (!(update->flags & REF_LOG_ONLY) &&\n+\t\t    (update->flags & REF_HAVE_OLD) &&\n \t\t    !is_null_oid(&update->old_oid))\n \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex f868870851..95a4dc3902 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -802,7 +802,8 @@ enum ref_transaction_error ref_update_check_old_target(const char *referent,\n \n /*\n  * Check if the ref must exist, this means that the old_oid or\n- * old_target is non NULL.\n+ * old_target is non NULL. Log-only updates never require the old state to\n+ * match.\n  */\n int ref_update_expects_existing_old_ref(struct ref_update *update);\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4c3817f4ec..44af58ac50 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1180,8 +1180,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret > 0) {\n \t\t/* The reference does not exist, but we expected it to. */\n \t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\n-\n \t\t\t\t   \"unable to resolve reference '%s'\"),\n \t\t\t    ref_update_original_update_refname(u), u->refname);\n \t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n@@ -1235,13 +1233,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \n \t\t\tnew_update->parent_update = u;\n \n-\t\t\t/*\n-\t\t\t * Change the symbolic ref update to log only. Also, it\n-\t\t\t * doesn't need to check its old OID value, as that will be\n-\t\t\t * done when new_update is processed.\n-\t\t\t */\n+\t\t\t/* Change the symbolic ref update to log only. */\n \t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\t\t\tu->flags &= ~REF_HAVE_OLD;\n \t\t}\n \t}\n \n@@ -1265,7 +1258,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\tret = ref_update_check_old_target(referent->buf, u, err);\n \t\tif (ret)\n \t\t\treturn ret;\n-\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n+\t} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&\n+\t\t   !oideq(&current_oid, &u->old_oid)) {\n \t\tif (is_null_oid(&u->old_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference already exists\"),\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523430","messageId":"20250804-pks-reflog-append-v4-9-13213fef7200@pks.im","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-0-13213fef7200@pks.im","subject":"[PATCH v4 9/9] refs: fix invalid old object IDs when migrating reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-04T09:46:09Z","receivedAt":"2025-08-04T09:46:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we end\nup with invalid old object IDs for the migrated entries: instead of\nwriting the old object ID of the to-be-migrated entry, we end up with\nthe all-zeroes object ID.\n\nThe root cause of this issue is that we don't know to use the old object\nID provided by the caller. Instead, we manually resolve the old object\nID by resolving the current value of its matching reference. But as that\nreference does not yet exist in the target ref storage we always end up\nresolving it to all-zeroes.\n\nThis issue got unnoticed as there is no user-facing command that would\neven show the old object ID. While `git log -g` knows to show the new\nobject ID, we don't have any formatting directive to show the old object\nID.\n\nFix the bug by introducing a new flag `REF_LOG_USE_PROVIDED_OIDS`. If\nset, backends are instructed to use the old and new object IDs provided\nby the caller, without doing any manual resolving. Set this flag in\n`ref_transaction_update_reflog()`.\n\nAmend our tests in t1460-refs-migrate to use our test tool to read\nreflog entries. This test tool prints out both old and new object ID of\neach reflog entry, which fixes the test gap. Furthermore it also prints\nthe full identity used to write the reflog, which provides test coverage\nfor the previous commit in this patch series that fixed the identity for\nmigrated reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  3 ++-\n refs.h                  |  9 ++++++++-\n refs/files-backend.c    | 16 +++++++++++++++-\n refs/reftable-backend.c | 14 ++++++++++++++\n t/t1421-reflog-write.sh |  4 +---\n t/t1460-refs-migrate.sh | 22 +++++++++++++++-------\n 6 files changed, 55 insertions(+), 13 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex f88928de74..946eb48941 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1385,7 +1385,8 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \n \tassert(err);\n \n-\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF |\n+\t\tREF_LOG_USE_PROVIDED_OIDS;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\ndiff --git a/refs.h b/refs.h\nindex 253dd8f4d5..090b4fdff4 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -760,13 +760,20 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n  */\n #define REF_SKIP_CREATE_REFLOG (1 << 12)\n \n+/*\n+ * When writing a REF_LOG_ONLY record, use the old and new object IDs provided\n+ * in the update instead of resolving the old object ID. The caller must also\n+ * set both REF_HAVE_OLD and REF_HAVE_NEW.\n+ */\n+#define REF_LOG_USE_PROVIDED_OIDS (1 << 13)\n+\n /*\n  * Bitmask of all of the flags that are allowed to be passed in to\n  * ref_transaction_update() and friends:\n  */\n #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \\\n \t(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \\\n-\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)\n+\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | REF_LOG_USE_PROVIDED_OIDS)\n \n /*\n  * Add a reference update to transaction. `new_oid` is the value that\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex c37fbfd138..851b1b33f4 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -3026,6 +3026,20 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t\t\t  struct ref_lock *lock,\n \t\t\t\t  struct strbuf *err)\n {\n+\tstruct object_id *old_oid = &lock->old_oid;\n+\n+\tif (update->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(update->flags & REF_HAVE_OLD) ||\n+\t\t    !(update->flags & REF_HAVE_NEW) ||\n+\t\t    !(update->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), update->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\told_oid = &update->old_oid;\n+\t}\n+\n \tif (update->new_target) {\n \t\t/*\n \t\t * We want to get the resolved OID for the target, to ensure\n@@ -3043,7 +3057,7 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t}\n \t}\n \n-\tif (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,\n+\tif (files_log_ref_write(refs, lock->ref_name, old_oid,\n \t\t\t\t&update->new_oid, update->committer_info,\n \t\t\t\tupdate->msg, update->flags, err)) {\n \t\tchar *old_msg = strbuf_detach(err, NULL);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 44af58ac50..99fafd75eb 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1096,6 +1096,20 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret)\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\tif (u->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(u->flags & REF_HAVE_OLD) ||\n+\t\t    !(u->flags & REF_HAVE_NEW) ||\n+\t\t    !(u->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), u->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\tif (queue_transaction_update(refs, tx_data, u, &u->old_oid, err))\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\treturn 0;\n+\t}\n+\n \t/* Verify that the new object ID is valid. */\n \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n \t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nindex dd7ffa5241..46df64c176 100755\n--- a/t/t1421-reflog-write.sh\n+++ b/t/t1421-reflog-write.sh\n@@ -101,11 +101,9 @@ test_expect_success 'simple writes' '\n \t\tEOF\n \n \t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n-\t\t# Note: the old object ID of the second reflog entry is broken.\n-\t\t# This will be fixed in subsequent commits.\n \t\ttest_reflog_matches . refs/heads/something <<-EOF\n \t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n-\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n \t\tEOF\n \t)\n '\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 2ab97e1b7d..0e1116a319 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -7,6 +7,17 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+print_all_reflog_entries () {\n+\trepo=$1 &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n+\twhile read reflog\n+\tdo\n+\t\techo \"REFLOG: $reflog\" &&\n+\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n+\t\treturn 1\n+\tdone <reflogs\n+}\n+\n # Migrate the provided repository from one format to the other and\n # verify that the references and logs are migrated over correctly.\n # Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]\n@@ -28,8 +39,7 @@ test_migration () {\n \t\t--format='%(refname) %(objectname) %(symref)' >expect &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t   git -C \"$repo\" reflog --all >expect_logs &&\n-\t   git -C \"$repo\" reflog list >expect_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >expect_logs\n \tfi &&\n \n \tgit -C \"$repo\" refs migrate --ref-format=\"$format\" \"$@\" &&\n@@ -39,10 +49,8 @@ test_migration () {\n \ttest_cmp expect actual &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t\tgit -C \"$repo\" reflog --all >actual_logs &&\n-\t\tgit -C \"$repo\" reflog list >actual_log_list &&\n-\t\ttest_cmp expect_logs actual_logs &&\n-\t\ttest_cmp expect_log_list actual_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >actual_logs &&\n+\t\ttest_cmp expect_logs actual_logs\n \tfi &&\n \n \tgit -C \"$repo\" rev-parse --show-ref-format >actual &&\n@@ -273,7 +281,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' '\n \ttest_commit -C repo second &&\n \tprintf \"update refs/heads/ref-%d HEAD\\n\" $(test_seq 3000) >stdin &&\n \tgit -C repo update-ref --stdin <stdin &&\n-\ttest_migration repo reftable\n+\ttest_migration repo reftable true\n '\n \n test_expect_success 'migrating from files format deletes backend files' '\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523450","messageId":"20250804144725.GA109984@coredump.intra.peff.net","threadId":"63818","inReplyTo":"aJBjdWv9qUeBL25-@pks.im","subject":"Re: [PATCH v3 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-08-04T14:47:25Z","receivedAt":"2025-08-04T14:47:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Aug 04, 2025 at 09:38:29AM +0200, Patrick Steinhardt wrote:\n\n> > Shouldn't we quietly drop the HEAD reflog update, rather than forcing\n> > the whole transaction to fail? The user never asked us to update HEAD at\n> > all. It was something we opportunistically decided to do, and now we\n> > find out that it is not appropriate to do so.\n> \n> That's something I wasn't quite sure about, either. Honestly, the reason\n> I shied away from it is that it needs a bit more munging for an edge\n> case that is hard to test reliably. But I guess we can do something like\n> the below patch to skip writing the reflog message instea.\n\nYeah, I wondered what it would look like to drop a single update from a\ntransaction, since that's not something we currently allow. And indeed,\nthis is a bit scary:\n\n> @@ -2626,8 +2632,16 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n>  \t\t */\n>  \t\tif (!(update->type & REF_ISSYMREF) ||\n>  \t\t    strcmp(update->parent_update->refname, referent.buf)) {\n> -\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n> -\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n> +\t\t\tif (unlock_ref(lock))\n> +\t\t\t\tstrmap_remove(&backend_data->ref_locks,\n> +\t\t\t\t\t      update->refname, 0);\n> +\n> +\t\t\tmemmove(transaction->updates + update_idx,\n> +\t\t\t\ttransaction->updates + update_idx + 1,\n> +\t\t\t\t(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));\n> +\t\t\ttransaction->nr--;\n> +\n> +\t\t\tret = 1;\n>  \t\t\tgoto out;\n>  \t\t}\n\nNot because it's necessarily wrong, but it feels like a maintainability\nproblem, when the transaction code learns some new struct field similar\nto \"ref_locks\", and we have to update it here, too. I dunno. Pulling it\nout into a \"transaction_drop_update()\" helper would make that a bit more\nobvious, but you're right that the fundamental issue is that we're not\ngoing to be testing this very well.\n\nMaybe erroring out, as your original patch did, is the least-bad thing,\nthen? I think that _might_ even be what happens in the current code as\nan emergent behavior. We leave HAVE_OLD_OID set, so we'd expect HEAD to\nresolve to the same thing it originally did. If you've pointed it\nelsewhere, then it probably would fail to resolve to that same oid\n(unless you pointed to a different branch with the same tip commit).\n\nI really wish there was an easy way to test this. I guess something like\nthis:\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 89ae4517a9..6aeec2e8e0 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2393,6 +2393,7 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,\n \t\t\t\t\t\t    const char *head_ref,\n \t\t\t\t\t\t    struct strbuf *err)\n {\n+\tstatic int force_split = -1;\n \tstruct ref_update *new_update;\n \n \tif ((update->flags & REF_LOG_ONLY) ||\n@@ -2401,7 +2402,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,\n \t    (update->flags & REF_UPDATE_VIA_HEAD))\n \t\treturn 0;\n \n-\tif (strcmp(update->refname, head_ref))\n+\tif (force_split < 0)\n+\t\tforce_split = git_env_bool(\"GIT_TEST_FORCE_SPLIT_HEAD_UPDATE\", 0);\n+\n+\tif (!force_split && strcmp(update->refname, head_ref))\n \t\treturn 0;\n \n \t/*\n\nalong with:\n\n  git commit --allow-empty one\n  git commit --allow-empty two\n  GIT_TEST_FORCE_SPLIT_HEAD_UPDATE=1 git branch foo HEAD^\n\ncreates roughly the situation (HEAD was never pointed at \"foo\", but\nwe'll create the reflog update for it anyway). It does fail with:\n\n  fatal: cannot lock ref 'HEAD': reference already exists\n\neven before your patch. And after, we get:\n\n  fatal: HEAD has been racily updated\n\nSo it probably is just not something that happens very often, as I don't\nrecall ever seeing any discussion of it.\n\nI dunno. Looks like you posted a new version of the series that loosens\nthis, so I'll take a peek at that (I also wondered whether what you\nposted above leaks entries in the update struct, so maybe you've dealt\nwith that).\n\n-Peff\n"},{"id":"523454","messageId":"20250804153803.GB109984@coredump.intra.peff.net","threadId":"63818","inReplyTo":"20250804-pks-reflog-append-v4-7-13213fef7200@pks.im","subject":"Re: [PATCH v4 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-08-04T15:38:03Z","receivedAt":"2025-08-04T15:38:05Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Aug 04, 2025 at 11:46:07AM +0200, Patrick Steinhardt wrote:\n\n> +\t\t/*\n> +\t\t * Check that \"HEAD\" didn't racily change since we have looked\n> +\t\t * it up. If it did we remove the reflog-only updateg from the\n> +\t\t * transaction again.\n> +\t\t *\n> +\t\t * Note that this does not catch all races: if \"HEAD\" was\n> +\t\t * racily changed to point to one of the refs part of the\n> +\t\t * transaction then we would miss writing the split reflog\n> +\t\t * entry for \"HEAD\".\n> +\t\t */\n> +\t\tif (!(update->type & REF_ISSYMREF) ||\n> +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n> +\t\t\tif (unlock_ref(lock))\n> +\t\t\t\tstrmap_remove(&backend_data->ref_locks,\n> +\t\t\t\t\t      update->refname, 0);\n> +\n> +\t\t\tmemmove(transaction->updates + update_idx,\n> +\t\t\t\ttransaction->updates + update_idx + 1,\n> +\t\t\t\t(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));\n> +\t\t\ttransaction->nr--;\n> +\n> +\t\t\tret = 1;\n> +\t\t\tgoto out;\n> +\t\t}\n\nOK, so this is basically the same as the patch you posted earlier. Let's\nsee how it fares with my hacky GIT_TEST_FORCE_SPLIT_HEAD_UPDATE patch:\n\n  $ GIT_TEST_FORCE_SPLIT_HEAD_UPDATE=1 git branch foo HEAD^\n  fatal:\n\nYikes. I'm not sure if there's a bug here, or if my hacky patch is\nviolating some other assumption. It looks like we get to the die() call\nin branch.c:create_branch() because the transaction reports failure, but\nwith an empty err strbuf.\n\nAh, I think I see it. When we return from lock_ref_for_update(), we've\nset \"ret\" to \"1\", indicating we are skipping the update. But then we do\nthis:\n\n  if (ret > 0)\n\tcontinue;\n\nI think there are two problems there:\n\n  1. That \"ret\" is also used as our return from\n     files_transaction_prepare(). So if this is the last update in the\n     transaction, then we return \"1\", rather than \"0\" for success, and\n     the caller thinks there was an error.\n\n  2. If it's not the last transaction, then we go to the next element in\n     the loop. But because it's a for-loop, we still increment \"i\",\n     which is wrong (because we shrunk the transaction list). We need to\n     check that \"i\" again.\n\nSo maybe:\n\n@@ -2910,8 +2914,11 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\t\t\t\t  head_ref, &refnames_to_check,\n \t\t\t\t\t  err);\n \t\tif (ret) {\n-\t\t\tif (ret > 0)\n+\t\t\tif (ret > 0) {\n+\t\t\t\tret = 0; /* not an error; we skipped it */\n+\t\t\t\ti--; /* we shrunk the list */\n \t\t\t\tcontinue;\n+\t\t\t}\n \t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n \t\t\t\tstrbuf_reset(err);\n \t\t\t\tret = 0;\n\nI confirmed that fixes case (1). I guess I could test case (2) with a\nbigger transaction involving multiple refs, but it's awkward because my\n\"force split update\" patch would try to create multiple HEAD updates. :-/\n\nI guess maybe it should be \"pretend HEAD is this\", like so:\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 851b1b33f4..564b77d0da 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2895,6 +2895,14 @@ static int files_transaction_prepare(struct ref_store *ref_store,\n \t\tFREE_AND_NULL(head_ref);\n \t}\n \n+\t{\n+\t\tconst char *v = getenv(\"GIT_TEST_PRETEND_SPLIT_HEAD\");\n+\t\tif (v) {\n+\t\t\tfree(head_ref);\n+\t\t\thead_ref = xstrdup(v);\n+\t\t}\n+\t}\n+\n \t/*\n \t * Acquire all locks, verify old values if provided, check\n \t * that new values are valid, and write new values to the\n\nWe have to be a bit tricky here. The split head update is always added\nat the end during the transaction preparation. So we need a situation\nwhere another update is added _after_ that. I guess it would be another\nsymref split (but done by updating the symref).\n\nSo:\n\n  git symbolic-ref refs/heads/SYMREF refs/heads/dest\n  (\n    echo \"create refs/heads/foo HEAD\"\n    echo \"create refs/heads/SYMREF HEAD\"\n  ) |\n  GIT_TEST_PRETEND_SPLIT_HEAD=refs/heads/foo git update-ref --stdin\n\nends up with four updates:\n\n  - the original create foo\n  - the original create SYMREF\n  - the reflog update of HEAD from split_head_update()\n  - the update of refs/heads/dest from split_symref_update()\n\nAnd indeed, running that through the debugger shows that we'd otherwise\nskip the final update with your patch (but the extra \"i--\" fixes it).\n\nI also tried this with SANITIZE=leak, and I think you'd need something\nlike this, as well:\n\ndiff --git a/refs.c b/refs.c\nindex 946eb48941..27c182e107 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1184,6 +1184,15 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n \treturn tr;\n }\n \n+void ref_update_free(struct ref_update *u)\n+{\n+\tfree(u->msg);\n+\tfree(u->committer_info);\n+\tfree((char *)u->new_target);\n+\tfree((char *)u->old_target);\n+\tfree(u);\n+}\n+\n void ref_transaction_free(struct ref_transaction *transaction)\n {\n \tsize_t i;\n@@ -1204,13 +1213,8 @@ void ref_transaction_free(struct ref_transaction *transaction)\n \t\tbreak;\n \t}\n \n-\tfor (i = 0; i < transaction->nr; i++) {\n-\t\tfree(transaction->updates[i]->msg);\n-\t\tfree(transaction->updates[i]->committer_info);\n-\t\tfree((char *)transaction->updates[i]->new_target);\n-\t\tfree((char *)transaction->updates[i]->old_target);\n-\t\tfree(transaction->updates[i]);\n-\t}\n+\tfor (i = 0; i < transaction->nr; i++)\n+\t\tref_update_free(transaction->updates[i]);\n \n \tif (transaction->rejections)\n \t\tfree(transaction->rejections->update_indices);\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 851b1b33f4..0246715383 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2640,6 +2640,7 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \t\t\t\ttransaction->updates + update_idx + 1,\n \t\t\t\t(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));\n \t\t\ttransaction->nr--;\n+\t\t\tref_update_free(update);\n \n \t\t\tret = 1;\n \t\t\tgoto out;\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex 95a4dc3902..6b5895a3b3 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -144,6 +144,8 @@ struct ref_update {\n \tconst char refname[FLEX_ARRAY];\n };\n \n+void ref_update_free(struct ref_update *);\n+\n int refs_read_raw_ref(struct ref_store *ref_store, const char *refname,\n \t\t      struct object_id *oid, struct strbuf *referent,\n \t\t      unsigned int *type, int *failure_errno);\n\nIt's a little hard to see that freeing update inside\nlock_ref_for_update() is safe (but we \"goto out\" after and don't look at\nit again). I think it would all be a bit more obvious if\nlock_ref_for_update() just returned 1 for \"skip this\", and then the\ncaller actually shrunk the transaction list.\n\n-Peff\n"},{"id":"523552","messageId":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH v5 0/9] refs: fix migration of reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:30Z","receivedAt":"2025-08-05T15:11:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nafter the announcement that \"reftable\" will become the default backend\nin Git 3.0 I've revived the efforts to implement this backend in\nlibgit2. I'm happy to report that this implementation is almost done by\nnow: out of 3000 tests only four are failing now.\n\nFor two of these tests I have been completely puzzled why those are\nfailing, as everything really looked perfectly fine in libgit2. As it\nturned out, the bug wasn't in libgit2 though, but in Git. Namely, the\nway we migrate reflog entries between storage formats is broken in two\nways:\n\n  - The identity we write into the reflog entries is wrong.\n\n  - The old commit ID of reflog entries is always set to all-zeroes.\n    This is what caused the libgit2 tests to fail, as I used `git refs\n    migrate` to convert test repositories to use reftables.\n\nThis patch series fixes both of these issues. Furthermore, it also adds\na new `git reflog write` subcommand to write new reflog entries for a\nspecific reference. This command was helpful to reproduce some test\nconstellations in libgit2.\n\nChanges in v2:\n  - !!! The base of this topic has changed so that it sits on top of\n    v2.50.1. This is done so that we can backport this change to older\n    release tracks.\n  - A couple of typo fixes and clarifications for commit messages.\n  - Reorder sections in git-reflog(1) manpage according to the\n    reordering we have in the synopsis.\n  - Add a section for the new `write` command.\n  - Improve test coverage for the `git reflog write` command.\n  - Avoid `cat`ing a file into a Bash loop.\n  - Remove a stale comment.\n  - Make `ref_update_expects_existing_old_ref()` a bit more straight\n    forward.\n  - Link to v1: https://lore.kernel.org/r/20250722-pks-reflog-append-v1-0-183e5949de16@pks.im\n\nChanges in v3:\n  - `git reflog write` now requires fully-qualified refnames.\n  - A new commit that plugs one part of the race around splitting of\n    reflogs for HEAD in the \"files\" backend.\n  - Link to v2: https://lore.kernel.org/r/20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im\n\nChanges in v4:\n  - Improve one of the tests to use an existing abbreviated object ID\n    instead of a non-existing one to make sure that we indeed fail due\n    to the abbreviation.\n  - Don't abort the transaction when HEAD has been racily updated, but\n    drop the log-only update instead.\n  - Link to v3: https://lore.kernel.org/r/20250729-pks-reflog-append-v3-0-9614d310f073@pks.im\n\nChanges in v5:\n  - Revert back to the logic that aborts the transaction if we see a\n    racy HEAD update. It's the pragmatic thing to do for an edge case\n    that is very unlikely to ever happen.\n  - Link to v4: https://lore.kernel.org/r/20250804-pks-reflog-append-v4-0-13213fef7200@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (9):\n      Documentation/git-reflog: convert to use synopsis type\n      builtin/reflog: improve grouping of subcommands\n      refs: export `ref_transaction_update_reflog()`\n      builtin/reflog: implement subcommand to write new entries\n      ident: fix type of string length parameter\n      refs: fix identity for migrated reflogs\n      refs/files: detect race when generating reflog entry for HEAD\n      refs: stop unsetting REF_HAVE_OLD for log-only updates\n      refs: fix invalid old object IDs when migrating reflogs\n\n Documentation/git-reflog.adoc |  76 +++++++++++++------------\n builtin/reflog.c              | 103 +++++++++++++++++++++++++++-------\n ident.c                       |   2 +-\n ident.h                       |   2 +-\n refs.c                        |  60 +++++++++++---------\n refs.h                        |  24 +++++++-\n refs/files-backend.c          |  65 +++++++++++++++++++---\n refs/refs-internal.h          |   3 +-\n refs/reftable-backend.c       |  26 ++++++---\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 126 ++++++++++++++++++++++++++++++++++++++++++\n t/t1460-refs-migrate.sh       |  22 +++++---\n 12 files changed, 403 insertions(+), 107 deletions(-)\n\nRange-diff versus v4:\n\n 1:  dd5a8065de =  1:  56319ac4ea Documentation/git-reflog: convert to use synopsis type\n 2:  6cb5f94795 =  2:  d294ddc23f builtin/reflog: improve grouping of subcommands\n 3:  6ba62d6b33 =  3:  9b28228df6 refs: export `ref_transaction_update_reflog()`\n 4:  e5630532ba =  4:  936223d8c8 builtin/reflog: implement subcommand to write new entries\n 5:  3a5f13aeb2 =  5:  78a3d896c7 ident: fix type of string length parameter\n 6:  1d3ac825e1 =  6:  b164e5ef4a refs: fix identity for migrated reflogs\n 7:  35cbf4a06f !  7:  e5817d65bf refs/files: detect race when generating reflog entry for HEAD\n    @@ Commit message\n         manually resolving the old object ID of that entry, we now use the same\n         old state as for the parent update.\n     \n    +    If we detect such a racy update we abort the transaction. This is a bit\n    +    heavy-handed: the user didn't even ask us to write a reflog update for\n    +    \"HEAD\", so it might be surprising if we abort the transaction. That\n    +    being said:\n    +\n    +      - Normal users wouldn't typically hit this case as we only hit the\n    +        relevant code when committing to a branch that is being pointed to\n    +        by \"HEAD\" directly. Commands like git-commit(1) typically commit to\n    +        \"HEAD\" itself though.\n    +\n    +      - Scripted users that use git-update-ref(1) and related plumbing\n    +        commands are unlikely to hit this case either, as they would have to\n    +        update the pointed-to-branch at the same as \"HEAD\" is being updated,\n    +        which is an exceedingly rare event.\n    +\n    +    The alternative would be to instead drop the log-only update completely,\n    +    but that would require more logic that is hard to verify without adding\n    +    infrastructure specific for such a test. So we rather do the pragmatic\n    +    thing and don't worry too much about an edge case that is very unlikely\n    +    to happen.\n    +\n         Unfortunately, this change only helps with the second race. We cannot\n         reliably plug the first race without locking the HEAD reference at the\n         start of the transaction. Locking HEAD unconditionally would effectively\n    @@ refs/files-backend.c\n      struct ref_lock {\n      \tchar *ref_name;\n      \tstruct lock_file lk;\n    -@@ refs/files-backend.c: int parse_loose_ref_contents(const struct git_hash_algo *algop,\n    - \treturn 0;\n    - }\n    - \n    --static void unlock_ref(struct ref_lock *lock)\n    -+static int unlock_ref(struct ref_lock *lock)\n    - {\n    - \tlock->count--;\n    - \tif (!lock->count) {\n    - \t\trollback_lock_file(&lock->lk);\n    - \t\tfree(lock->ref_name);\n    - \t\tfree(lock);\n    -+\t\treturn 1;\n    - \t}\n    -+\treturn 0;\n    - }\n    - \n    - /*\n     @@ refs/files-backend.c: static enum ref_transaction_error split_head_update(struct ref_update *update,\n      \n      \tnew_update = ref_transaction_add_update(\n    @@ refs/files-backend.c: static enum ref_transaction_error split_head_update(struct\n      \n      \t/*\n      \t * Add \"HEAD\". This insertion is O(N) in the transaction\n    -@@ refs/files-backend.c: struct files_transaction_backend_data {\n    -  *   the referent to transaction.\n    -  * - If it is an update of head_ref, add a corresponding REF_LOG_ONLY\n    -  *   update of HEAD.\n    -+ *\n    -+ * Returns 0 on success, 1 in case the update needs to be dropped or a negative\n    -+ * error code otherwise.\n    -  */\n    - static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,\n    - \t\t\t\t\t\t      struct ref_update *update,\n     @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n      \n      \tupdate->backend_data = lock;\n    @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(stru\n     +\n     +\t\t/*\n     +\t\t * Check that \"HEAD\" didn't racily change since we have looked\n    -+\t\t * it up. If it did we remove the reflog-only updateg from the\n    -+\t\t * transaction again.\n    ++\t\t * it up. If it did we must refuse to write the reflog entry.\n     +\t\t *\n     +\t\t * Note that this does not catch all races: if \"HEAD\" was\n     +\t\t * racily changed to point to one of the refs part of the\n    @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(stru\n     +\t\t */\n     +\t\tif (!(update->type & REF_ISSYMREF) ||\n     +\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n    -+\t\t\tif (unlock_ref(lock))\n    -+\t\t\t\tstrmap_remove(&backend_data->ref_locks,\n    -+\t\t\t\t\t      update->refname, 0);\n    -+\n    -+\t\t\tmemmove(transaction->updates + update_idx,\n    -+\t\t\t\ttransaction->updates + update_idx + 1,\n    -+\t\t\t\t(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));\n    -+\t\t\ttransaction->nr--;\n    -+\n    -+\t\t\tret = 1;\n    ++\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n    ++\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n     +\t\t\tgoto out;\n     +\t\t}\n     +\n    @@ refs/files-backend.c: static enum ref_transaction_error lock_ref_for_update(stru\n      \t\tif (update->flags & REF_NO_DEREF) {\n      \t\t\t/*\n      \t\t\t * We won't be reading the referent as part of\n    -@@ refs/files-backend.c: static int files_transaction_prepare(struct ref_store *ref_store,\n    - \t\t\t\t\t  head_ref, &refnames_to_check,\n    - \t\t\t\t\t  err);\n    - \t\tif (ret) {\n    -+\t\t\tif (ret > 0)\n    -+\t\t\t\tcontinue;\n    - \t\t\tif (ref_transaction_maybe_set_rejected(transaction, i, ret)) {\n    - \t\t\t\tstrbuf_reset(err);\n    - \t\t\t\tret = 0;\n 8:  00caacba94 =  8:  06c98ff66f refs: stop unsetting REF_HAVE_OLD for log-only updates\n 9:  2271b80a90 =  9:  a3f8e9bee3 refs: fix invalid old object IDs when migrating reflogs\n\n---\nbase-commit: d82adb61ba2fd11d8f2587fca1b6bd7925ce4044\nchange-id: 20250722-pks-reflog-append-634172d8ab2c\n\n"},{"id":"523553","messageId":"20250805-pks-reflog-append-v5-1-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 1/9] Documentation/git-reflog: convert to use synopsis type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:31Z","receivedAt":"2025-08-05T15:11:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\nhave introduced a new synopsis type that simplifies the rules for\ntypesetting a command's synopsis. Convert the git-reflog(1)\ndocumentation to use it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 14 +++++++-------\n 1 file changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 412f06b8fe..707a9b39ed 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n \n SYNOPSIS\n --------\n-[verse]\n-'git reflog' [show] [<log-options>] [<ref>]\n-'git reflog list'\n-'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n+[synopsis]\n+git reflog [show] [<log-options>] [<ref>]\n+git reflog list\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n \t[--rewrite] [--updateref] [--stale-fix]\n \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n-'git reflog delete' [--rewrite] [--updateref]\n+git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n-'git reflog drop' [--all [--single-worktree] | <refs>...]\n-'git reflog exists' <ref>\n+git reflog drop [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n \n DESCRIPTION\n -----------\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523554","messageId":"20250805-pks-reflog-append-v5-2-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 2/9] builtin/reflog: improve grouping of subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:32Z","receivedAt":"2025-08-05T15:11:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The way subcommands of git-reflog(1) are laid out does not make any\nimmediate sense. Reorder them such that read-only subcommands precede\nwriting commands for a bit more structure.\n\nFurthermore, move the \"expire\" subcommand last. This prepares for a\nsubsequent change where we are about to introduce a new \"write\" command\nto append reflog entries. Like this, the writing subcommands are ordered\nsuch that those affecting a single reflog come before those spanning\nacross all reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 61 ++++++++++++++++++++++---------------------\n builtin/reflog.c              | 38 +++++++++++++--------------\n 2 files changed, 50 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 707a9b39ed..c3801b82fb 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -11,13 +11,13 @@ SYNOPSIS\n [synopsis]\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n-git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n-\t[--rewrite] [--updateref] [--stale-fix]\n-\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n-git reflog exists <ref>\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n+\t[--rewrite] [--updateref] [--stale-fix]\n+\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,9 @@ actions, and in addition the `HEAD` reflog records branch switching.\n \n The \"list\" subcommand lists all refs which have a corresponding reflog.\n \n-The \"expire\" subcommand prunes older reflog entries. Entries older\n-than `expire` time, or entries older than `expire-unreachable` time\n-and not reachable from the current tip, are removed from the reflog.\n-This is typically not used directly by end users -- instead, see\n-linkgit:git-gc[1].\n+The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n+with zero status if the reflog exists, and non-zero status if it does\n+not.\n \n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n@@ -58,9 +56,11 @@ The \"drop\" subcommand completely removes the reflog for the specified\n references. This is in contrast to \"expire\" and \"delete\", both of which\n can be used to delete reflog entries, but not the reflog itself.\n \n-The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n-with zero status if the reflog exists, and non-zero status if it does\n-not.\n+The \"expire\" subcommand prunes older reflog entries. Entries older\n+than `expire` time, or entries older than `expire-unreachable` time\n+and not reachable from the current tip, are removed from the reflog.\n+This is typically not used directly by end users -- instead, see\n+linkgit:git-gc[1].\n \n OPTIONS\n -------\n@@ -71,6 +71,25 @@ Options for `show`\n `git reflog show` accepts any of the options accepted by `git log`.\n \n \n+Options for `delete`\n+~~~~~~~~~~~~~~~~~~~~\n+\n+`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n+`--dry-run`, and `--verbose`, with the same meanings as when they are\n+used with `expire`.\n+\n+Options for `drop`\n+~~~~~~~~~~~~~~~~~~\n+\n+--all::\n+\tDrop the reflogs of all references from all worktrees.\n+\n+--single-worktree::\n+\tBy default when `--all` is specified, reflogs from all working\n+\ttrees are dropped. This option limits the processing to reflogs\n+\tfrom the current working tree only.\n+\n+\n Options for `expire`\n ~~~~~~~~~~~~~~~~~~~~\n \n@@ -130,24 +149,6 @@ which didn't protect objects referred to by reflogs.\n \tPrint extra information on screen.\n \n \n-Options for `delete`\n-~~~~~~~~~~~~~~~~~~~~\n-\n-`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n-`--dry-run`, and `--verbose`, with the same meanings as when they are\n-used with `expire`.\n-\n-Options for `drop`\n-~~~~~~~~~~~~~~~~~~\n-\n---all::\n-\tDrop the reflogs of all references from all worktrees.\n-\n---single-worktree::\n-\tBy default when `--all` is specified, reflogs from all working\n-\ttrees are dropped. This option limits the processing to reflogs\n-\tfrom the current working tree only.\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 3acaf3e32c..b00b3f9edc 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -17,21 +17,21 @@\n #define BUILTIN_REFLOG_LIST_USAGE \\\n \tN_(\"git reflog list\")\n \n-#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n-\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n-\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n-\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+#define BUILTIN_REFLOG_EXISTS_USAGE \\\n+\tN_(\"git reflog exists <ref>\")\n \n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n \n-#define BUILTIN_REFLOG_EXISTS_USAGE \\\n-\tN_(\"git reflog exists <ref>\")\n-\n #define BUILTIN_REFLOG_DROP_USAGE \\\n \tN_(\"git reflog drop [--all [--single-worktree] | <refs>...]\")\n \n+#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n+\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n+\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n+\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+\n static const char *const reflog_show_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tNULL,\n@@ -42,9 +42,9 @@ static const char *const reflog_list_usage[] = {\n \tNULL,\n };\n \n-static const char *const reflog_expire_usage[] = {\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n-\tNULL\n+static const char *const reflog_exists_usage[] = {\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tNULL,\n };\n \n static const char *const reflog_delete_usage[] = {\n@@ -52,23 +52,23 @@ static const char *const reflog_delete_usage[] = {\n \tNULL\n };\n \n-static const char *const reflog_exists_usage[] = {\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n-\tNULL,\n-};\n-\n static const char *const reflog_drop_usage[] = {\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tNULL,\n };\n \n+static const char *const reflog_expire_usage[] = {\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tNULL\n+};\n+\n static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n \tNULL\n };\n \n@@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n-\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n-\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523555","messageId":"20250805-pks-reflog-append-v5-3-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 3/9] refs: export `ref_transaction_update_reflog()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:33Z","receivedAt":"2025-08-05T15:11:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we'll add another user that wants to write reflog\nentries. This requires them to call `ref_transaction_update_reflog()`,\nbut that function is local to \"refs.c\".\n\nExport the function to prepare for the change. While at it, drop the\n`flags` field, as all callers are for now expected to use the same flags\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 29 +++++++++++------------------\n refs.h | 15 +++++++++++++++\n 2 files changed, 26 insertions(+), 18 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex dce5c49ca2..8aa9f7236a 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1371,27 +1371,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \treturn 0;\n }\n \n-/*\n- * Similar to`ref_transaction_update`, but this function is only for adding\n- * a reflog update. Supports providing custom committer information. The index\n- * field can be utiltized to order updates as desired. When not used, the\n- * updates default to being ordered by refname.\n- */\n-static int ref_transaction_update_reflog(struct ref_transaction *transaction,\n-\t\t\t\t\t const char *refname,\n-\t\t\t\t\t const struct object_id *new_oid,\n-\t\t\t\t\t const struct object_id *old_oid,\n-\t\t\t\t\t const char *committer_info,\n-\t\t\t\t\t unsigned int flags,\n-\t\t\t\t\t const char *msg,\n-\t\t\t\t\t uint64_t index,\n-\t\t\t\t\t struct strbuf *err)\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err)\n {\n \tstruct ref_update *update;\n+\tunsigned int flags;\n \n \tassert(err);\n \n-\tflags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\n@@ -3019,8 +3013,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n-\t\t\t\t\t    REF_HAVE_NEW | REF_HAVE_OLD, msg,\n-\t\t\t\t\t    data->index++, data->errbuf);\n+\t\t\t\t\t    msg, data->index++, data->errbuf);\n \treturn ret;\n }\n \ndiff --git a/refs.h b/refs.h\nindex 46a6008e07..253dd8f4d5 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -795,6 +795,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \n+/*\n+ * Similar to `ref_transaction_update`, but this function is only for adding\n+ * a reflog update. Supports providing custom committer information. The index\n+ * field can be utiltized to order updates as desired. When set to zero, the\n+ * updates default to being ordered by refname.\n+ */\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err);\n+\n /*\n  * Add a reference creation to transaction. new_oid is the value that\n  * the reference should have after the update; it must not be\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523556","messageId":"20250805-pks-reflog-append-v5-4-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 4/9] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:34Z","receivedAt":"2025-08-05T15:11:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we provide a couple of subcommands in git-reflog(1) to remove\nreflog entries, we don't provide any to write new entries. Obviously\nthis is not an operation that really would be needed for many use cases\nout there, or otherwise people would have complained that such a command\ndoes not exist yet. But the introduction of the \"reftable\" backend\nchanges the picture a bit, as it is now basically impossible to manually\nappend a reflog entry if one wanted to do so due to the binary format.\n\nPlug this gap by introducing a simple \"write\" subcommand. For now, all\nthis command does is to append a single new reflog entry with the given\nobject IDs and message to the reflog. More specifically, it is not yet\npossible to:\n\n  - Write multiple reflog entries at once.\n\n  - Insert reflog entries at arbitrary indices.\n\n  - Specify the date of the reflog entry.\n\n  - Insert reflog entries that refer to nonexistent objects.\n\nIf required, those features can be added at a future point in time. For\nnow though, the new command aims to fulfill the most basic use cases\nwhile being as strict as possible when it comes to verifying parameters.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc |   7 +++\n builtin/reflog.c              |  65 +++++++++++++++++++++\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 128 ++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 201 insertions(+)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex c3801b82fb..34232a539a 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n git reflog exists <ref>\n+git reflog write <ref> <old-oid> <new-oid> <message>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n@@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n with zero status if the reflog exists, and non-zero status if it does\n not.\n \n+The \"write\" subcommand writes a single entry to the reflog of a given\n+reference. This new entry is appended to the reflog and will thus become\n+the most recent entry. The reference name must be fully qualified. Both the old\n+and new object IDs must not be abbreviated and must point to existing objects.\n+The reflog message gets normalized.\n+\n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n reflog delete master@{2}`\"). This subcommand is also typically not used\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b00b3f9edc..a1b4e02204 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -3,6 +3,8 @@\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-store.h\"\n #include \"revision.h\"\n #include \"reachable.h\"\n #include \"wildmatch.h\"\n@@ -20,6 +22,9 @@\n #define BUILTIN_REFLOG_EXISTS_USAGE \\\n \tN_(\"git reflog exists <ref>\")\n \n+#define BUILTIN_REFLOG_WRITE_USAGE \\\n+\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n+\n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n@@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n \tNULL,\n };\n \n+static const char *const reflog_write_usage[] = {\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n+\tNULL,\n+};\n+\n static const char *const reflog_delete_usage[] = {\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tNULL\n@@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n \tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tBUILTIN_REFLOG_EXPIRE_USAGE,\n@@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n+\t\t\t    struct repository *repo)\n+{\n+\tconst struct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct object_id old_oid, new_oid;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct ref_transaction *tx;\n+\tconst char *ref, *message;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);\n+\tif (argc != 4)\n+\t\tusage_with_options(reflog_write_usage, options);\n+\n+\tref = argv[0];\n+\tif (!is_root_ref(ref) && check_refname_format(ref, 0))\n+\t\tdie(_(\"invalid reference name: %s\"), ref);\n+\n+\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n+\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n+\n+\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n+\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n+\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n+\n+\tmessage = argv[3];\n+\n+\ttx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\tif (!tx)\n+\t\tdie(_(\"cannot start transaction: %s\"), err.buf);\n+\n+\tret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,\n+\t\t\t\t\t    git_committer_info(0),\n+\t\t\t\t\t    message, 0, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot queue reflog update: %s\"), err.buf);\n+\n+\tret = ref_transaction_commit(tx, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot commit reflog update: %s\"), err.buf);\n+\n+\tref_transaction_free(tx);\n+\tstrbuf_release(&err);\n+\treturn 0;\n+}\n+\n /*\n  * main \"reflog\"\n  */\n@@ -405,6 +469,7 @@ int cmd_reflog(int argc,\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_reflog_write),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n \t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\ndiff --git a/t/meson.build b/t/meson.build\nindex d052fc3e23..adcdf09e74 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -220,6 +220,7 @@ integration_tests = [\n   't1418-reflog-exists.sh',\n   't1419-exclude-refs.sh',\n   't1420-lost-found.sh',\n+  't1421-reflog-write.sh',\n   't1430-bad-ref-name.sh',\n   't1450-fsck.sh',\n   't1451-fsck-buffer.sh',\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nnew file mode 100755\nindex 0000000000..dd7ffa5241\n--- /dev/null\n+++ b/t/t1421-reflog-write.sh\n@@ -0,0 +1,128 @@\n+#!/bin/sh\n+\n+test_description='Manually write reflog entries'\n+\n+. ./test-lib.sh\n+\n+SIGNATURE=\"C O Mitter <committer@example.com> 1112911993 -0700\"\n+\n+test_reflog_matches () {\n+\trepo=\"$1\" &&\n+\trefname=\"$2\" &&\n+\tcat >actual &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$refname\" >expected &&\n+\ttest_cmp expected actual\n+}\n+\n+test_expect_success 'invalid number of arguments' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tfor args in \"\" \"1\" \"1 2\" \"1 2 3\" \"1 2 3 4 5\"\n+\t\tdo\n+\t\t\ttest_must_fail git reflog write $args 2>err &&\n+\t\t\ttest_grep \"usage: git reflog write\" err || return 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success 'invalid refname' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write \"refs/heads/ invalid\" $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'unqualified refname is rejected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write unqualified $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'nonexistent object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"old object .* does not exist\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&\n+\t\ttest_grep \"new object .* does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'abbreviated object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tabbreviated_oid=$(git rev-parse HEAD | test_copy_bytes 8) &&\n+\t\ttest_must_fail git reflog write refs/heads/something $abbreviated_oid $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $abbreviated_oid new-object-id 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err\n+\t)\n+'\n+\n+test_expect_success 'reflog message gets normalized' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\t\tgit reflog write HEAD $COMMIT_OID $COMMIT_OID \"$(printf \"message\\nwith\\nnewlines\")\" &&\n+\t\tgit reflog show -1 --format=%gs HEAD >actual &&\n+\t\techo \"message with newlines\" >expected &&\n+\t\ttest_cmp expected actual\n+\t)\n+'\n+\n+test_expect_success 'simple writes' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF &&\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n+\t\t# Note: the old object ID of the second reflog entry is broken.\n+\t\t# This will be fixed in subsequent commits.\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can write to root ref' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write ROOT_REF_HEAD $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . ROOT_REF_HEAD <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523557","messageId":"20250805-pks-reflog-append-v5-5-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 5/9] ident: fix type of string length parameter","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:35Z","receivedAt":"2025-08-05T15:12:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The last parameter in `split_ident_line()` is the length of the line\npassed in by the caller. As such, most callers pass in either the result\nof `strlen()`, `struct strbuf::len` or a pointer diff, all of which\nare expected to be positive numbers. Regardless of that, the function\naccepts a signed integer, which is somewhat confusing.\n\nFix the function signature to instead accept a `size_t`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ident.c | 2 +-\n ident.h | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ident.c b/ident.c\nindex 967895d8850..a7a2d132579 100644\n--- a/ident.c\n+++ b/ident.c\n@@ -272,7 +272,7 @@ static void strbuf_addstr_without_crud(struct strbuf *sb, const char *src)\n  * can still be NULL if the input line only has the name/email part\n  * (e.g. reading from a reflog entry).\n  */\n-int split_ident_line(struct ident_split *split, const char *line, int len)\n+int split_ident_line(struct ident_split *split, const char *line, size_t len)\n {\n \tconst char *cp;\n \tsize_t span;\ndiff --git a/ident.h b/ident.h\nindex 6a79febba15..3c034038791 100644\n--- a/ident.h\n+++ b/ident.h\n@@ -35,7 +35,7 @@ void reset_ident_date(void);\n  * Signals an success with 0, but time part of the result may be NULL\n  * if the input lacks timestamp and zone\n  */\n-int split_ident_line(struct ident_split *, const char *, int);\n+int split_ident_line(struct ident_split *, const char *, size_t);\n \n /*\n  * Given a commit or tag object buffer and the commit or tag headers, replaces\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523558","messageId":"20250805-pks-reflog-append-v5-6-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 6/9] refs: fix identity for migrated reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:36Z","receivedAt":"2025-08-05T15:12:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we must\nreconstruct the identity of reflog entries. This is done by passing the\ncommitter passed to the `migrate_one_reflog_entry()` callback function\nto `fmt_ident()`.\n\nThis results in an invalid identity though: `fmt_ident()` expects the\ncaller to provide both name and mail of the author, but we pass the full\nidentity as mail. This leads to an identity like:\n\n    pks <Patrick Steinhardt ps@pks.im>\n\nFix the bug by splitting the identity line first. This allows us to\nextract both the name and mail so that we can pass them to `fmt_ident()`\nseparately.\n\nThis commit does not yet add any tests as there is another bug in the\nreflog migration that will be fixed in a subsequent commit. Once that\nbug is fixed we'll make the reflog verification in t1450 stricter, and\nthat will catch both this bug here and the other bug.\n\nNote that we also add two new `name` and `mail` string buffers to the\ncallback structures and splice them through to the callbacks. This is\ndone so that we can avoid allocating a new buffer every time we compute\nthe committer information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 22 ++++++++++++++++++----\n 1 file changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 8aa9f7236a..a5f9ffaa45 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2954,7 +2954,7 @@ struct migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf sb;\n+\tstruct strbuf sb, name, mail;\n };\n \n static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n@@ -2993,7 +2993,7 @@ struct reflog_migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf *sb;\n+\tstruct strbuf *sb, *name, *mail;\n };\n \n static int migrate_one_reflog_entry(struct object_id *old_oid,\n@@ -3003,13 +3003,21 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \t\t\t\t    const char *msg, void *cb_data)\n {\n \tstruct reflog_migration_data *data = cb_data;\n+\tstruct ident_split ident;\n \tconst char *date;\n \tint ret;\n \n+\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n+\t\treturn -1;\n+\n+\tstrbuf_reset(data->name);\n+\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n+\tstrbuf_reset(data->mail);\n+\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n+\n \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n \tstrbuf_reset(data->sb);\n-\t/* committer contains name and email */\n-\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n+\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n@@ -3026,6 +3034,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n \t\t.transaction = migration_data->transaction,\n \t\t.errbuf = migration_data->errbuf,\n \t\t.sb = &migration_data->sb,\n+\t\t.name = &migration_data->name,\n+\t\t.mail = &migration_data->mail,\n \t};\n \n \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n@@ -3124,6 +3134,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tstruct strbuf new_gitdir = STRBUF_INIT;\n \tstruct migration_data data = {\n \t\t.sb = STRBUF_INIT,\n+\t\t.name = STRBUF_INIT,\n+\t\t.mail = STRBUF_INIT,\n \t};\n \tint did_migrate_refs = 0;\n \tint ret;\n@@ -3299,6 +3311,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tref_transaction_free(transaction);\n \tstrbuf_release(&new_gitdir);\n \tstrbuf_release(&data.sb);\n+\tstrbuf_release(&data.name);\n+\tstrbuf_release(&data.mail);\n \treturn ret;\n }\n \n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523559","messageId":"20250805-pks-reflog-append-v5-7-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:37Z","receivedAt":"2025-08-05T15:12:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When updating a reference that is being pointed to HEAD we don't only\nwrite a reflog message for that particular reference, but also generate\none for HEAD. This logic is handled by `split_head_update()`, where we:\n\n  1. Verify that the condition actually triggered. This is done by\n     reading HEAD at the start of the transaction so that we can then\n     check whether a given reference update refers to its target.\n\n  2. Queue a new log-only update for HEAD in case it did.\n\nBut the logic is unfortunately not free of races, as we do not lock the\nHEAD reference after we have read its target. This can lead to the\nfollowing two scenarios:\n\n  - HEAD gets concurrently updated to point to one of the references we\n    have already processed. This causes us not writing a reflog message\n    even though we should have done so.\n\n  - HEAD gets concurrently updated to no longer point to a reference\n    anymore that we have already processed. This causes us to write a\n    reflog message even though we should _not_ have done so.\n\nImprove the situation by introducing a new `REF_LOG_VIA_SPLIT` flag that\nis specific to the \"files\" backend. If set, we will double check that\nthe HEAD reference still points to the reference that we are creating\nthe reflog entry for after we have locked HEAD. Furthermore, instead of\nmanually resolving the old object ID of that entry, we now use the same\nold state as for the parent update.\n\nIf we detect such a racy update we abort the transaction. This is a bit\nheavy-handed: the user didn't even ask us to write a reflog update for\n\"HEAD\", so it might be surprising if we abort the transaction. That\nbeing said:\n\n  - Normal users wouldn't typically hit this case as we only hit the\n    relevant code when committing to a branch that is being pointed to\n    by \"HEAD\" directly. Commands like git-commit(1) typically commit to\n    \"HEAD\" itself though.\n\n  - Scripted users that use git-update-ref(1) and related plumbing\n    commands are unlikely to hit this case either, as they would have to\n    update the pointed-to-branch at the same as \"HEAD\" is being updated,\n    which is an exceedingly rare event.\n\nThe alternative would be to instead drop the log-only update completely,\nbut that would require more logic that is hard to verify without adding\ninfrastructure specific for such a test. So we rather do the pragmatic\nthing and don't worry too much about an edge case that is very unlikely\nto happen.\n\nUnfortunately, this change only helps with the second race. We cannot\nreliably plug the first race without locking the HEAD reference at the\nstart of the transaction. Locking HEAD unconditionally would effectively\nserialize all writes though, and that doesn't seem like an option. Also,\ndouble checking its value at the end of the transaction is not an option\neither, as its target may have flip-flopped during the transaction.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c | 40 ++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 38 insertions(+), 2 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex bf6f89b1d1..ba018b0984 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -68,6 +68,12 @@\n  */\n #define REF_DELETED_RMDIR (1 << 9)\n \n+/*\n+ * Used to indicate that the reflog-only update has been created via\n+ * `split_head_update()`.\n+ */\n+#define REF_LOG_VIA_SPLIT (1 << 14)\n+\n struct ref_lock {\n \tchar *ref_name;\n \tstruct lock_file lk;\n@@ -2420,9 +2426,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,\n \n \tnew_update = ref_transaction_add_update(\n \t\t\ttransaction, \"HEAD\",\n-\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF,\n+\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF | REF_LOG_VIA_SPLIT,\n \t\t\t&update->new_oid, &update->old_oid,\n \t\t\tNULL, NULL, update->committer_info, update->msg);\n+\tnew_update->parent_update = update;\n \n \t/*\n \t * Add \"HEAD\". This insertion is O(N) in the transaction\n@@ -2600,7 +2607,36 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \n \tupdate->backend_data = lock;\n \n-\tif (update->type & REF_ISSYMREF) {\n+\tif (update->flags & REF_LOG_VIA_SPLIT) {\n+\t\tstruct ref_lock *parent_lock;\n+\n+\t\tif (!update->parent_update)\n+\t\t\tBUG(\"split update without a parent\");\n+\n+\t\tparent_lock = update->parent_update->backend_data;\n+\n+\t\t/*\n+\t\t * Check that \"HEAD\" didn't racily change since we have looked\n+\t\t * it up. If it did we must refuse to write the reflog entry.\n+\t\t *\n+\t\t * Note that this does not catch all races: if \"HEAD\" was\n+\t\t * racily changed to point to one of the refs part of the\n+\t\t * transaction then we would miss writing the split reflog\n+\t\t * entry for \"HEAD\".\n+\t\t */\n+\t\tif (!(update->type & REF_ISSYMREF) ||\n+\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n+\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (update->flags & REF_HAVE_OLD) {\n+\t\t\toidcpy(&lock->old_oid, &update->old_oid);\n+\t\t} else {\n+\t\t\toidcpy(&lock->old_oid, &parent_lock->old_oid);\n+\t\t}\n+\t} else if (update->type & REF_ISSYMREF) {\n \t\tif (update->flags & REF_NO_DEREF) {\n \t\t\t/*\n \t\t\t * We won't be reading the referent as part of\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523560","messageId":"20250805-pks-reflog-append-v5-8-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 8/9] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:38Z","receivedAt":"2025-08-05T15:12:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\nobject ID set. If so, the value of that field is used to verify whether\nthe current state of the reference matches this expected state. It is\nthus an important part of mitigating races with a concurrent process\nthat updates the same set of references.\n\nWhen writing reflogs though we explicitly unset that flag. This is a\nsensible thing to do: the old state of reflog entry updates may not\nnecessarily match the current on-disk state of its accompanying ref, but\nit's only intended to signal what old object ID we want to write into\nthe new reflog entry. For example when migrating refs we end up writing\nmany reflog entries for a single reference, and most likely those reflog\nentries will have many different old object IDs.\n\nBut unsetting this flag also removes a useful signal, namely that the\ncaller _did_ provide an old object ID for a given reflog entry. This\nsignal will become useful in a subsequent commit, where we add a new\nflag that tells the transaction to use the provided old and new object\nIDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\nsignal to verify that the caller really did provide an old object ID.\n\nStop unsetting the flag so that we can use it as this described signal\nin a subsequent commit. Skip checking the old object ID for log-only\nupdates so that we don't expect it to match the current on-disk state.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  8 +++-----\n refs/files-backend.c    |  9 +++++----\n refs/refs-internal.h    |  3 ++-\n refs/reftable-backend.c | 12 +++---------\n 4 files changed, 13 insertions(+), 19 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex a5f9ffaa45..f88928de74 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1393,11 +1393,6 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \tupdate = ref_transaction_add_update(transaction, refname, flags,\n \t\t\t\t\t    new_oid, old_oid, NULL, NULL,\n \t\t\t\t\t    committer_info, msg);\n-\t/*\n-\t * While we do set the old_oid value, we unset the flag to skip\n-\t * old_oid verification which only makes sense for refs.\n-\t */\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \tupdate->index = index;\n \n \t/*\n@@ -3318,6 +3313,9 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \n int ref_update_expects_existing_old_ref(struct ref_update *update)\n {\n+\tif (update->flags & REF_LOG_ONLY)\n+\t\treturn 0;\n+\n \treturn (update->flags & REF_HAVE_OLD) &&\n \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex ba018b0984..85ab2ef2b9 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2500,7 +2500,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n \t * done when new_update is processed.\n \t */\n \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \n \treturn 0;\n }\n@@ -2515,8 +2514,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n \t\t\t\t\t\tstruct object_id *oid,\n \t\t\t\t\t\tstruct strbuf *err)\n {\n-\tif (!(update->flags & REF_HAVE_OLD) ||\n-\t\t   oideq(oid, &update->old_oid))\n+\tif (update->flags & REF_LOG_ONLY ||\n+\t    !(update->flags & REF_HAVE_OLD) ||\n+\t    oideq(oid, &update->old_oid))\n \t\treturn 0;\n \n \tif (is_null_oid(&update->old_oid)) {\n@@ -3095,7 +3095,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n \n-\t\tif ((update->flags & REF_HAVE_OLD) &&\n+\t\tif (!(update->flags & REF_LOG_ONLY) &&\n+\t\t    (update->flags & REF_HAVE_OLD) &&\n \t\t    !is_null_oid(&update->old_oid))\n \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex f868870851..95a4dc3902 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -802,7 +802,8 @@ enum ref_transaction_error ref_update_check_old_target(const char *referent,\n \n /*\n  * Check if the ref must exist, this means that the old_oid or\n- * old_target is non NULL.\n+ * old_target is non NULL. Log-only updates never require the old state to\n+ * match.\n  */\n int ref_update_expects_existing_old_ref(struct ref_update *update);\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4c3817f4ec..44af58ac50 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1180,8 +1180,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret > 0) {\n \t\t/* The reference does not exist, but we expected it to. */\n \t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\n-\n \t\t\t\t   \"unable to resolve reference '%s'\"),\n \t\t\t    ref_update_original_update_refname(u), u->refname);\n \t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n@@ -1235,13 +1233,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \n \t\t\tnew_update->parent_update = u;\n \n-\t\t\t/*\n-\t\t\t * Change the symbolic ref update to log only. Also, it\n-\t\t\t * doesn't need to check its old OID value, as that will be\n-\t\t\t * done when new_update is processed.\n-\t\t\t */\n+\t\t\t/* Change the symbolic ref update to log only. */\n \t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\t\t\tu->flags &= ~REF_HAVE_OLD;\n \t\t}\n \t}\n \n@@ -1265,7 +1258,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\tret = ref_update_check_old_target(referent->buf, u, err);\n \t\tif (ret)\n \t\t\treturn ret;\n-\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n+\t} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&\n+\t\t   !oideq(&current_oid, &u->old_oid)) {\n \t\tif (is_null_oid(&u->old_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference already exists\"),\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523561","messageId":"20250805-pks-reflog-append-v5-9-050997db09d5@pks.im","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"[PATCH v5 9/9] refs: fix invalid old object IDs when migrating reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-05T15:11:39Z","receivedAt":"2025-08-05T15:12:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we end\nup with invalid old object IDs for the migrated entries: instead of\nwriting the old object ID of the to-be-migrated entry, we end up with\nthe all-zeroes object ID.\n\nThe root cause of this issue is that we don't know to use the old object\nID provided by the caller. Instead, we manually resolve the old object\nID by resolving the current value of its matching reference. But as that\nreference does not yet exist in the target ref storage we always end up\nresolving it to all-zeroes.\n\nThis issue got unnoticed as there is no user-facing command that would\neven show the old object ID. While `git log -g` knows to show the new\nobject ID, we don't have any formatting directive to show the old object\nID.\n\nFix the bug by introducing a new flag `REF_LOG_USE_PROVIDED_OIDS`. If\nset, backends are instructed to use the old and new object IDs provided\nby the caller, without doing any manual resolving. Set this flag in\n`ref_transaction_update_reflog()`.\n\nAmend our tests in t1460-refs-migrate to use our test tool to read\nreflog entries. This test tool prints out both old and new object ID of\neach reflog entry, which fixes the test gap. Furthermore it also prints\nthe full identity used to write the reflog, which provides test coverage\nfor the previous commit in this patch series that fixed the identity for\nmigrated reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  3 ++-\n refs.h                  |  9 ++++++++-\n refs/files-backend.c    | 16 +++++++++++++++-\n refs/reftable-backend.c | 14 ++++++++++++++\n t/t1421-reflog-write.sh |  4 +---\n t/t1460-refs-migrate.sh | 22 +++++++++++++++-------\n 6 files changed, 55 insertions(+), 13 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex f88928de74..946eb48941 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1385,7 +1385,8 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \n \tassert(err);\n \n-\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF |\n+\t\tREF_LOG_USE_PROVIDED_OIDS;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\ndiff --git a/refs.h b/refs.h\nindex 253dd8f4d5..090b4fdff4 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -760,13 +760,20 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n  */\n #define REF_SKIP_CREATE_REFLOG (1 << 12)\n \n+/*\n+ * When writing a REF_LOG_ONLY record, use the old and new object IDs provided\n+ * in the update instead of resolving the old object ID. The caller must also\n+ * set both REF_HAVE_OLD and REF_HAVE_NEW.\n+ */\n+#define REF_LOG_USE_PROVIDED_OIDS (1 << 13)\n+\n /*\n  * Bitmask of all of the flags that are allowed to be passed in to\n  * ref_transaction_update() and friends:\n  */\n #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \\\n \t(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \\\n-\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)\n+\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | REF_LOG_USE_PROVIDED_OIDS)\n \n /*\n  * Add a reference update to transaction. `new_oid` is the value that\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 85ab2ef2b9..905555365b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -3010,6 +3010,20 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t\t\t  struct ref_lock *lock,\n \t\t\t\t  struct strbuf *err)\n {\n+\tstruct object_id *old_oid = &lock->old_oid;\n+\n+\tif (update->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(update->flags & REF_HAVE_OLD) ||\n+\t\t    !(update->flags & REF_HAVE_NEW) ||\n+\t\t    !(update->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), update->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\told_oid = &update->old_oid;\n+\t}\n+\n \tif (update->new_target) {\n \t\t/*\n \t\t * We want to get the resolved OID for the target, to ensure\n@@ -3027,7 +3041,7 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t}\n \t}\n \n-\tif (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,\n+\tif (files_log_ref_write(refs, lock->ref_name, old_oid,\n \t\t\t\t&update->new_oid, update->committer_info,\n \t\t\t\tupdate->msg, update->flags, err)) {\n \t\tchar *old_msg = strbuf_detach(err, NULL);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 44af58ac50..99fafd75eb 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1096,6 +1096,20 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret)\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\tif (u->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(u->flags & REF_HAVE_OLD) ||\n+\t\t    !(u->flags & REF_HAVE_NEW) ||\n+\t\t    !(u->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), u->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\tif (queue_transaction_update(refs, tx_data, u, &u->old_oid, err))\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\treturn 0;\n+\t}\n+\n \t/* Verify that the new object ID is valid. */\n \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n \t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nindex dd7ffa5241..46df64c176 100755\n--- a/t/t1421-reflog-write.sh\n+++ b/t/t1421-reflog-write.sh\n@@ -101,11 +101,9 @@ test_expect_success 'simple writes' '\n \t\tEOF\n \n \t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n-\t\t# Note: the old object ID of the second reflog entry is broken.\n-\t\t# This will be fixed in subsequent commits.\n \t\ttest_reflog_matches . refs/heads/something <<-EOF\n \t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n-\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n \t\tEOF\n \t)\n '\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 2ab97e1b7d..0e1116a319 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -7,6 +7,17 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+print_all_reflog_entries () {\n+\trepo=$1 &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n+\twhile read reflog\n+\tdo\n+\t\techo \"REFLOG: $reflog\" &&\n+\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n+\t\treturn 1\n+\tdone <reflogs\n+}\n+\n # Migrate the provided repository from one format to the other and\n # verify that the references and logs are migrated over correctly.\n # Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]\n@@ -28,8 +39,7 @@ test_migration () {\n \t\t--format='%(refname) %(objectname) %(symref)' >expect &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t   git -C \"$repo\" reflog --all >expect_logs &&\n-\t   git -C \"$repo\" reflog list >expect_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >expect_logs\n \tfi &&\n \n \tgit -C \"$repo\" refs migrate --ref-format=\"$format\" \"$@\" &&\n@@ -39,10 +49,8 @@ test_migration () {\n \ttest_cmp expect actual &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t\tgit -C \"$repo\" reflog --all >actual_logs &&\n-\t\tgit -C \"$repo\" reflog list >actual_log_list &&\n-\t\ttest_cmp expect_logs actual_logs &&\n-\t\ttest_cmp expect_log_list actual_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >actual_logs &&\n+\t\ttest_cmp expect_logs actual_logs\n \tfi &&\n \n \tgit -C \"$repo\" rev-parse --show-ref-format >actual &&\n@@ -273,7 +281,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' '\n \ttest_commit -C repo second &&\n \tprintf \"update refs/heads/ref-%d HEAD\\n\" $(test_seq 3000) >stdin &&\n \tgit -C repo update-ref --stdin <stdin &&\n-\ttest_migration repo reftable\n+\ttest_migration repo reftable true\n '\n \n test_expect_success 'migrating from files format deletes backend files' '\n\n-- \n2.50.1.723.g3e08bea96f.dirty\n\n"},{"id":"523573","messageId":"5910515.DvuYhMxLoT@cayenne","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-1-050997db09d5@pks.im","subject":"Re: [PATCH v5 1/9] Documentation/git-reflog: convert to use synopsis type","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2025-08-05T17:04:23Z","receivedAt":"2025-08-05T17:04:41Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"On Tuesday, 5 August 2025 17:11:31 CEST Patrick Steinhardt wrote:\n> With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\n> have introduced a new synopsis type that simplifies the rules for\n> typesetting a command's synopsis. Convert the git-reflog(1)\n> documentation to use it.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-reflog.adoc | 14 +++++++-------\n>  1 file changed, 7 insertions(+), 7 deletions(-)\n> \n> diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\n> index 412f06b8fe..707a9b39ed 100644\n> --- a/Documentation/git-reflog.adoc\n> +++ b/Documentation/git-reflog.adoc\n> @@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n> \n>  SYNOPSIS\n>  --------\n> -[verse]\n> -'git reflog' [show] [<log-options>] [<ref>]\n> -'git reflog list'\n> -'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n> +[synopsis]\n> +git reflog [show] [<log-options>] [<ref>]\n> +git reflog list\n> +git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n>  \t[--rewrite] [--updateref] [--stale-fix]\n>  \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n> -'git reflog delete' [--rewrite] [--updateref]\n> +git reflog delete [--rewrite] [--updateref]\n>  \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n> -'git reflog drop' [--all [--single-worktree] | <refs>...]\n> -'git reflog exists' <ref>\n> +git reflog drop [--all [--single-worktree] | <refs>...]\n> +git reflog exists <ref>\n> \n>  DESCRIPTION\n>  -----------\n\n\nHello,\n\nBe careful that with the doc lint series I'm proposing, this change will raise \na failure: one of the tests checks that switching the main synopsis to \n[synopsis] is linked to switching the definitions lists to inline synopsis, \nusing `backticks`. This check may be too restrictive though.\n\nJean-Noël\n\n\n"},{"id":"523583","messageId":"20250805184712.GA1345110@coredump.intra.peff.net","threadId":"63818","inReplyTo":"20250805-pks-reflog-append-v5-0-050997db09d5@pks.im","subject":"Re: [PATCH v5 0/9] refs: fix migration of reflog entries","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-08-05T18:47:12Z","receivedAt":"2025-08-05T18:47:17Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Aug 05, 2025 at 05:11:30PM +0200, Patrick Steinhardt wrote:\n\n> Changes in v5:\n>   - Revert back to the logic that aborts the transaction if we see a\n>     racy HEAD update. It's the pragmatic thing to do for an edge case\n>     that is very unlikely to ever happen.\n>   - Link to v4: https://lore.kernel.org/r/20250804-pks-reflog-append-v4-0-13213fef7200@pks.im\n\nThanks, this makes sense to me. I hadn't reviewed the whole series very\nthoroughly, but I think others did. And certainly this version addresses\nall of the discussion I did participate in.\n\n-Peff\n"},{"id":"523610","messageId":"xmqqqzxpxwxu.fsf@gitster.g","threadId":"63818","inReplyTo":"5910515.DvuYhMxLoT@cayenne","subject":"Re: [PATCH v5 1/9] Documentation/git-reflog: convert to use synopsis type","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-05T21:47:41Z","receivedAt":"2025-08-05T21:47:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jean-Noël AVILA <jn.avila@free.fr> writes:\n\n> Be careful that with the doc lint series I'm proposing, this change will raise \n> a failure: one of the tests checks that switching the main synopsis to \n> [synopsis] is linked to switching the definitions lists to inline synopsis, \n> using `backticks`. This check may be too restrictive though.\n\nThis is what I've queued on top of your topic to prepare for today's\nintegration.\n\n--- >8 ---\nSubject: [PATCH] fixup! Documentation/git-reflog: convert to use synopsis type\n\n---\n Documentation/git-reflog.adoc | 24 ++++++++++++------------\n 1 file changed, 12 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 34232a539a..38af0c977a 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -88,10 +88,10 @@ used with `expire`.\n Options for `drop`\n ~~~~~~~~~~~~~~~~~~\n \n---all::\n+`--all`::\n \tDrop the reflogs of all references from all worktrees.\n \n---single-worktree::\n+`--single-worktree`::\n \tBy default when `--all` is specified, reflogs from all working\n \ttrees are dropped. This option limits the processing to reflogs\n \tfrom the current working tree only.\n@@ -100,15 +100,15 @@ Options for `drop`\n Options for `expire`\n ~~~~~~~~~~~~~~~~~~~~\n \n---all::\n+`--all`::\n \tProcess the reflogs of all references.\n \n---single-worktree::\n+`--single-worktree`::\n \tBy default when `--all` is specified, reflogs from all working\n \ttrees are processed. This option limits the processing to reflogs\n \tfrom the current working tree only.\n \n---expire=<time>::\n+`--expire=<time>`::\n \tPrune entries older than the specified time. If this option is\n \tnot specified, the expiration time is taken from the\n \tconfiguration setting `gc.reflogExpire`, which in turn\n@@ -116,7 +116,7 @@ Options for `expire`\n \tof their age; `--expire=never` turns off pruning of reachable\n \tentries (but see `--expire-unreachable`).\n \n---expire-unreachable=<time>::\n+`--expire-unreachable=<time>`::\n \tPrune entries older than `<time>` that are not reachable from\n \tthe current tip of the branch. If this option is not\n \tspecified, the expiration time is taken from the configuration\n@@ -126,17 +126,17 @@ Options for `expire`\n \tturns off early pruning of unreachable entries (but see\n \t`--expire`).\n \n---updateref::\n+`--updateref`::\n \tUpdate the reference to the value of the top reflog entry (i.e.\n \t<ref>@\\{0\\}) if the previous top entry was pruned.  (This\n \toption is ignored for symbolic references.)\n \n---rewrite::\n+`--rewrite`::\n \tIf a reflog entry's predecessor is pruned, adjust its \"old\"\n \tSHA-1 to be equal to the \"new\" SHA-1 field of the entry that\n \tnow precedes it.\n \n---stale-fix::\n+`--stale-fix`::\n \tPrune any reflog entries that point to \"broken commits\". A\n \tbroken commit is a commit that is not reachable from any of\n \tthe reference tips and that refers, directly or indirectly, to\n@@ -147,12 +147,12 @@ has the same cost as 'git prune'.  It is primarily intended to fix\n corruption caused by garbage collecting using older versions of Git,\n which didn't protect objects referred to by reflogs.\n \n--n::\n---dry-run::\n+`-n`::\n+`--dry-run`::\n \tDo not actually prune any entries; just show what would have\n \tbeen pruned.\n \n---verbose::\n+`--verbose`::\n \tPrint extra information on screen.\n \n \n-- \n2.51.0-rc0-162-g220549999b\n\n"},{"id":"523621","messageId":"aJLt3pkBpIuPYA99@pks.im","threadId":"63818","inReplyTo":"xmqqqzxpxwxu.fsf@gitster.g","subject":"Re: [PATCH v5 1/9] Documentation/git-reflog: convert to use synopsis type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:53:34Z","receivedAt":"2025-08-06T05:53:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 05, 2025 at 02:47:41PM -0700, Junio C Hamano wrote:\n> Jean-Noël AVILA <jn.avila@free.fr> writes:\n> \n> > Be careful that with the doc lint series I'm proposing, this change will raise \n> > a failure: one of the tests checks that switching the main synopsis to \n> > [synopsis] is linked to switching the definitions lists to inline synopsis, \n> > using `backticks`. This check may be too restrictive though.\n> \n> This is what I've queued on top of your topic to prepare for today's\n> integration.\n\nThanks. I see that the fixup commit cannot trivially be applied to the\nfirst commit due to a merge conflict. So let me send a new version that\nabsorbs the fix.\n\nPatrick\n"},{"id":"523622","messageId":"aJLt7zK8pZffHAOc@pks.im","threadId":"63818","inReplyTo":"20250805184712.GA1345110@coredump.intra.peff.net","subject":"Re: [PATCH v5 0/9] refs: fix migration of reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:53:51Z","receivedAt":"2025-08-06T05:53:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 05, 2025 at 02:47:12PM -0400, Jeff King wrote:\n> On Tue, Aug 05, 2025 at 05:11:30PM +0200, Patrick Steinhardt wrote:\n> \n> > Changes in v5:\n> >   - Revert back to the logic that aborts the transaction if we see a\n> >     racy HEAD update. It's the pragmatic thing to do for an edge case\n> >     that is very unlikely to ever happen.\n> >   - Link to v4: https://lore.kernel.org/r/20250804-pks-reflog-append-v4-0-13213fef7200@pks.im\n> \n> Thanks, this makes sense to me. I hadn't reviewed the whole series very\n> thoroughly, but I think others did. And certainly this version addresses\n> all of the discussion I did participate in.\n\nThanks for the discussion!\n\nPatrick\n"},{"id":"523623","messageId":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250722-pks-reflog-append-v1-0-183e5949de16@pks.im","subject":"[PATCH v6 0/9] refs: fix migration of reflog entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:11Z","receivedAt":"2025-08-06T05:54:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nafter the announcement that \"reftable\" will become the default backend\nin Git 3.0 I've revived the efforts to implement this backend in\nlibgit2. I'm happy to report that this implementation is almost done by\nnow: out of 3000 tests only four are failing now.\n\nFor two of these tests I have been completely puzzled why those are\nfailing, as everything really looked perfectly fine in libgit2. As it\nturned out, the bug wasn't in libgit2 though, but in Git. Namely, the\nway we migrate reflog entries between storage formats is broken in two\nways:\n\n  - The identity we write into the reflog entries is wrong.\n\n  - The old commit ID of reflog entries is always set to all-zeroes.\n    This is what caused the libgit2 tests to fail, as I used `git refs\n    migrate` to convert test repositories to use reftables.\n\nThis patch series fixes both of these issues. Furthermore, it also adds\na new `git reflog write` subcommand to write new reflog entries for a\nspecific reference. This command was helpful to reproduce some test\nconstellations in libgit2.\n\nChanges in v2:\n  - !!! The base of this topic has changed so that it sits on top of\n    v2.50.1. This is done so that we can backport this change to older\n    release tracks.\n  - A couple of typo fixes and clarifications for commit messages.\n  - Reorder sections in git-reflog(1) manpage according to the\n    reordering we have in the synopsis.\n  - Add a section for the new `write` command.\n  - Improve test coverage for the `git reflog write` command.\n  - Avoid `cat`ing a file into a Bash loop.\n  - Remove a stale comment.\n  - Make `ref_update_expects_existing_old_ref()` a bit more straight\n    forward.\n  - Link to v1: https://lore.kernel.org/r/20250722-pks-reflog-append-v1-0-183e5949de16@pks.im\n\nChanges in v3:\n  - `git reflog write` now requires fully-qualified refnames.\n  - A new commit that plugs one part of the race around splitting of\n    reflogs for HEAD in the \"files\" backend.\n  - Link to v2: https://lore.kernel.org/r/20250725-pks-reflog-append-v2-0-e4e7cbe3f578@pks.im\n\nChanges in v4:\n  - Improve one of the tests to use an existing abbreviated object ID\n    instead of a non-existing one to make sure that we indeed fail due\n    to the abbreviation.\n  - Don't abort the transaction when HEAD has been racily updated, but\n    drop the log-only update instead.\n  - Link to v3: https://lore.kernel.org/r/20250729-pks-reflog-append-v3-0-9614d310f073@pks.im\n\nChanges in v5:\n  - Revert back to the logic that aborts the transaction if we see a\n    racy HEAD update. It's the pragmatic thing to do for an edge case\n    that is very unlikely to ever happen.\n  - Link to v4: https://lore.kernel.org/r/20250804-pks-reflog-append-v4-0-13213fef7200@pks.im\n\nChanges in v6:\n  - Convert options to use backticks in git-reflog(1) to appease the\n    upcoming new manpage linter.\n  - Link to v5: https://lore.kernel.org/r/20250805-pks-reflog-append-v5-0-050997db09d5@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (9):\n      Documentation/git-reflog: convert to use synopsis type\n      builtin/reflog: improve grouping of subcommands\n      refs: export `ref_transaction_update_reflog()`\n      builtin/reflog: implement subcommand to write new entries\n      ident: fix type of string length parameter\n      refs: fix identity for migrated reflogs\n      refs/files: detect race when generating reflog entry for HEAD\n      refs: stop unsetting REF_HAVE_OLD for log-only updates\n      refs: fix invalid old object IDs when migrating reflogs\n\n Documentation/git-reflog.adoc |  96 +++++++++++++++++---------------\n builtin/reflog.c              | 103 +++++++++++++++++++++++++++-------\n ident.c                       |   2 +-\n ident.h                       |   2 +-\n refs.c                        |  60 +++++++++++---------\n refs.h                        |  24 +++++++-\n refs/files-backend.c          |  65 +++++++++++++++++++---\n refs/refs-internal.h          |   3 +-\n refs/reftable-backend.c       |  26 ++++++---\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 126 ++++++++++++++++++++++++++++++++++++++++++\n t/t1460-refs-migrate.sh       |  22 +++++---\n 12 files changed, 413 insertions(+), 117 deletions(-)\n\nRange-diff versus v5:\n\n 1:  22b0ea5e69 <  -:  ---------- Documentation/git-reflog: convert to use synopsis type\n -:  ---------- >  1:  289dcbe595 Documentation/git-reflog: convert to use synopsis type\n 2:  bf5d901269 !  2:  0363c102e6 builtin/reflog: improve grouping of subcommands\n    @@ Documentation/git-reflog.adoc: Options for `show`\n     +Options for `drop`\n     +~~~~~~~~~~~~~~~~~~\n     +\n    -+--all::\n    ++`--all`::\n     +\tDrop the reflogs of all references from all worktrees.\n     +\n    -+--single-worktree::\n    ++`--single-worktree`::\n     +\tBy default when `--all` is specified, reflogs from all working\n     +\ttrees are dropped. This option limits the processing to reflogs\n     +\tfrom the current working tree only.\n    @@ Documentation/git-reflog.adoc: which didn't protect objects referred to by reflo\n     -Options for `drop`\n     -~~~~~~~~~~~~~~~~~~\n     -\n    ----all::\n    +-`--all`::\n     -\tDrop the reflogs of all references from all worktrees.\n     -\n    ----single-worktree::\n    +-`--single-worktree`::\n     -\tBy default when `--all` is specified, reflogs from all working\n     -\ttrees are dropped. This option limits the processing to reflogs\n     -\tfrom the current working tree only.\n 3:  709148c2a2 =  3:  ff885b29f4 refs: export `ref_transaction_update_reflog()`\n 4:  3ad459bf6e =  4:  8ad1992cc9 builtin/reflog: implement subcommand to write new entries\n 5:  207546401a =  5:  bb5dddf606 ident: fix type of string length parameter\n 6:  0791e82645 =  6:  e15eba8029 refs: fix identity for migrated reflogs\n 7:  e5c0280a36 =  7:  c83037d9a4 refs/files: detect race when generating reflog entry for HEAD\n 8:  c31e03ebcb =  8:  8be3d5c126 refs: stop unsetting REF_HAVE_OLD for log-only updates\n 9:  61acc7af7a =  9:  11a21b71f6 refs: fix invalid old object IDs when migrating reflogs\n\n---\nbase-commit: d82adb61ba2fd11d8f2587fca1b6bd7925ce4044\nchange-id: 20250722-pks-reflog-append-634172d8ab2c\n\n"},{"id":"523624","messageId":"20250806-pks-reflog-append-v6-1-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 1/9] Documentation/git-reflog: convert to use synopsis type","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:12Z","receivedAt":"2025-08-06T05:54:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we\nhave introduced a new synopsis type that simplifies the rules for\ntypesetting a command's synopsis. Convert the git-reflog(1)\ndocumentation to use it.\n\nWhile at it, convert the list of options to use backticks. This is done\nto appease an upcoming new linter that mandates the use of backticks\nwhen using the synopsis type.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 38 +++++++++++++++++++-------------------\n 1 file changed, 19 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 412f06b8fe..0d6601fdea 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -8,16 +8,16 @@ git-reflog - Manage reflog information\n \n SYNOPSIS\n --------\n-[verse]\n-'git reflog' [show] [<log-options>] [<ref>]\n-'git reflog list'\n-'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]\n+[synopsis]\n+git reflog [show] [<log-options>] [<ref>]\n+git reflog list\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n \t[--rewrite] [--updateref] [--stale-fix]\n \t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n-'git reflog delete' [--rewrite] [--updateref]\n+git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n-'git reflog drop' [--all [--single-worktree] | <refs>...]\n-'git reflog exists' <ref>\n+git reflog drop [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n \n DESCRIPTION\n -----------\n@@ -74,15 +74,15 @@ Options for `show`\n Options for `expire`\n ~~~~~~~~~~~~~~~~~~~~\n \n---all::\n+`--all`::\n \tProcess the reflogs of all references.\n \n---single-worktree::\n+`--single-worktree`::\n \tBy default when `--all` is specified, reflogs from all working\n \ttrees are processed. This option limits the processing to reflogs\n \tfrom the current working tree only.\n \n---expire=<time>::\n+`--expire=<time>`::\n \tPrune entries older than the specified time. If this option is\n \tnot specified, the expiration time is taken from the\n \tconfiguration setting `gc.reflogExpire`, which in turn\n@@ -90,7 +90,7 @@ Options for `expire`\n \tof their age; `--expire=never` turns off pruning of reachable\n \tentries (but see `--expire-unreachable`).\n \n---expire-unreachable=<time>::\n+`--expire-unreachable=<time>`::\n \tPrune entries older than `<time>` that are not reachable from\n \tthe current tip of the branch. If this option is not\n \tspecified, the expiration time is taken from the configuration\n@@ -100,17 +100,17 @@ Options for `expire`\n \tturns off early pruning of unreachable entries (but see\n \t`--expire`).\n \n---updateref::\n+`--updateref`::\n \tUpdate the reference to the value of the top reflog entry (i.e.\n \t<ref>@\\{0\\}) if the previous top entry was pruned.  (This\n \toption is ignored for symbolic references.)\n \n---rewrite::\n+`--rewrite`::\n \tIf a reflog entry's predecessor is pruned, adjust its \"old\"\n \tSHA-1 to be equal to the \"new\" SHA-1 field of the entry that\n \tnow precedes it.\n \n---stale-fix::\n+`--stale-fix`::\n \tPrune any reflog entries that point to \"broken commits\". A\n \tbroken commit is a commit that is not reachable from any of\n \tthe reference tips and that refers, directly or indirectly, to\n@@ -121,12 +121,12 @@ has the same cost as 'git prune'.  It is primarily intended to fix\n corruption caused by garbage collecting using older versions of Git,\n which didn't protect objects referred to by reflogs.\n \n--n::\n---dry-run::\n+`-n`::\n+`--dry-run`::\n \tDo not actually prune any entries; just show what would have\n \tbeen pruned.\n \n---verbose::\n+`--verbose`::\n \tPrint extra information on screen.\n \n \n@@ -140,10 +140,10 @@ used with `expire`.\n Options for `drop`\n ~~~~~~~~~~~~~~~~~~\n \n---all::\n+`--all`::\n \tDrop the reflogs of all references from all worktrees.\n \n---single-worktree::\n+`--single-worktree`::\n \tBy default when `--all` is specified, reflogs from all working\n \ttrees are dropped. This option limits the processing to reflogs\n \tfrom the current working tree only.\n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523625","messageId":"20250806-pks-reflog-append-v6-2-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 2/9] builtin/reflog: improve grouping of subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:13Z","receivedAt":"2025-08-06T05:54:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The way subcommands of git-reflog(1) are laid out does not make any\nimmediate sense. Reorder them such that read-only subcommands precede\nwriting commands for a bit more structure.\n\nFurthermore, move the \"expire\" subcommand last. This prepares for a\nsubsequent change where we are about to introduce a new \"write\" command\nto append reflog entries. Like this, the writing subcommands are ordered\nsuch that those affecting a single reflog come before those spanning\nacross all reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc | 61 ++++++++++++++++++++++---------------------\n builtin/reflog.c              | 38 +++++++++++++--------------\n 2 files changed, 50 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 0d6601fdea..4eb6c25607 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -11,13 +11,13 @@ SYNOPSIS\n [synopsis]\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n-git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n-\t[--rewrite] [--updateref] [--stale-fix]\n-\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n+git reflog exists <ref>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n-git reflog exists <ref>\n+git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n+\t[--rewrite] [--updateref] [--stale-fix]\n+\t[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,9 @@ actions, and in addition the `HEAD` reflog records branch switching.\n \n The \"list\" subcommand lists all refs which have a corresponding reflog.\n \n-The \"expire\" subcommand prunes older reflog entries. Entries older\n-than `expire` time, or entries older than `expire-unreachable` time\n-and not reachable from the current tip, are removed from the reflog.\n-This is typically not used directly by end users -- instead, see\n-linkgit:git-gc[1].\n+The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n+with zero status if the reflog exists, and non-zero status if it does\n+not.\n \n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n@@ -58,9 +56,11 @@ The \"drop\" subcommand completely removes the reflog for the specified\n references. This is in contrast to \"expire\" and \"delete\", both of which\n can be used to delete reflog entries, but not the reflog itself.\n \n-The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n-with zero status if the reflog exists, and non-zero status if it does\n-not.\n+The \"expire\" subcommand prunes older reflog entries. Entries older\n+than `expire` time, or entries older than `expire-unreachable` time\n+and not reachable from the current tip, are removed from the reflog.\n+This is typically not used directly by end users -- instead, see\n+linkgit:git-gc[1].\n \n OPTIONS\n -------\n@@ -71,6 +71,25 @@ Options for `show`\n `git reflog show` accepts any of the options accepted by `git log`.\n \n \n+Options for `delete`\n+~~~~~~~~~~~~~~~~~~~~\n+\n+`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n+`--dry-run`, and `--verbose`, with the same meanings as when they are\n+used with `expire`.\n+\n+Options for `drop`\n+~~~~~~~~~~~~~~~~~~\n+\n+`--all`::\n+\tDrop the reflogs of all references from all worktrees.\n+\n+`--single-worktree`::\n+\tBy default when `--all` is specified, reflogs from all working\n+\ttrees are dropped. This option limits the processing to reflogs\n+\tfrom the current working tree only.\n+\n+\n Options for `expire`\n ~~~~~~~~~~~~~~~~~~~~\n \n@@ -130,24 +149,6 @@ which didn't protect objects referred to by reflogs.\n \tPrint extra information on screen.\n \n \n-Options for `delete`\n-~~~~~~~~~~~~~~~~~~~~\n-\n-`git reflog delete` accepts options `--updateref`, `--rewrite`, `-n`,\n-`--dry-run`, and `--verbose`, with the same meanings as when they are\n-used with `expire`.\n-\n-Options for `drop`\n-~~~~~~~~~~~~~~~~~~\n-\n-`--all`::\n-\tDrop the reflogs of all references from all worktrees.\n-\n-`--single-worktree`::\n-\tBy default when `--all` is specified, reflogs from all working\n-\ttrees are dropped. This option limits the processing to reflogs\n-\tfrom the current working tree only.\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex 3acaf3e32c..b00b3f9edc 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -17,21 +17,21 @@\n #define BUILTIN_REFLOG_LIST_USAGE \\\n \tN_(\"git reflog list\")\n \n-#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n-\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n-\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n-\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+#define BUILTIN_REFLOG_EXISTS_USAGE \\\n+\tN_(\"git reflog exists <ref>\")\n \n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n \n-#define BUILTIN_REFLOG_EXISTS_USAGE \\\n-\tN_(\"git reflog exists <ref>\")\n-\n #define BUILTIN_REFLOG_DROP_USAGE \\\n \tN_(\"git reflog drop [--all [--single-worktree] | <refs>...]\")\n \n+#define BUILTIN_REFLOG_EXPIRE_USAGE \\\n+\tN_(\"git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\\n\" \\\n+\t   \"                  [--rewrite] [--updateref] [--stale-fix]\\n\" \\\n+\t   \"                  [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]\")\n+\n static const char *const reflog_show_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tNULL,\n@@ -42,9 +42,9 @@ static const char *const reflog_list_usage[] = {\n \tNULL,\n };\n \n-static const char *const reflog_expire_usage[] = {\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n-\tNULL\n+static const char *const reflog_exists_usage[] = {\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tNULL,\n };\n \n static const char *const reflog_delete_usage[] = {\n@@ -52,23 +52,23 @@ static const char *const reflog_delete_usage[] = {\n \tNULL\n };\n \n-static const char *const reflog_exists_usage[] = {\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n-\tNULL,\n-};\n-\n static const char *const reflog_drop_usage[] = {\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tNULL,\n };\n \n+static const char *const reflog_expire_usage[] = {\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tNULL\n+};\n+\n static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n-\tBUILTIN_REFLOG_EXPIRE_USAGE,\n+\tBUILTIN_REFLOG_EXISTS_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n-\tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_EXPIRE_USAGE,\n \tNULL\n };\n \n@@ -404,10 +404,10 @@ int cmd_reflog(int argc,\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n-\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n-\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n+\t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523626","messageId":"20250806-pks-reflog-append-v6-3-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 3/9] refs: export `ref_transaction_update_reflog()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:14Z","receivedAt":"2025-08-06T05:54:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we'll add another user that wants to write reflog\nentries. This requires them to call `ref_transaction_update_reflog()`,\nbut that function is local to \"refs.c\".\n\nExport the function to prepare for the change. While at it, drop the\n`flags` field, as all callers are for now expected to use the same flags\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 29 +++++++++++------------------\n refs.h | 15 +++++++++++++++\n 2 files changed, 26 insertions(+), 18 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex dce5c49ca2..8aa9f7236a 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1371,27 +1371,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \treturn 0;\n }\n \n-/*\n- * Similar to`ref_transaction_update`, but this function is only for adding\n- * a reflog update. Supports providing custom committer information. The index\n- * field can be utiltized to order updates as desired. When not used, the\n- * updates default to being ordered by refname.\n- */\n-static int ref_transaction_update_reflog(struct ref_transaction *transaction,\n-\t\t\t\t\t const char *refname,\n-\t\t\t\t\t const struct object_id *new_oid,\n-\t\t\t\t\t const struct object_id *old_oid,\n-\t\t\t\t\t const char *committer_info,\n-\t\t\t\t\t unsigned int flags,\n-\t\t\t\t\t const char *msg,\n-\t\t\t\t\t uint64_t index,\n-\t\t\t\t\t struct strbuf *err)\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err)\n {\n \tstruct ref_update *update;\n+\tunsigned int flags;\n \n \tassert(err);\n \n-\tflags |= REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\n@@ -3019,8 +3013,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n-\t\t\t\t\t    REF_HAVE_NEW | REF_HAVE_OLD, msg,\n-\t\t\t\t\t    data->index++, data->errbuf);\n+\t\t\t\t\t    msg, data->index++, data->errbuf);\n \treturn ret;\n }\n \ndiff --git a/refs.h b/refs.h\nindex 46a6008e07..253dd8f4d5 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -795,6 +795,21 @@ int ref_transaction_update(struct ref_transaction *transaction,\n \t\t\t   unsigned int flags, const char *msg,\n \t\t\t   struct strbuf *err);\n \n+/*\n+ * Similar to `ref_transaction_update`, but this function is only for adding\n+ * a reflog update. Supports providing custom committer information. The index\n+ * field can be utiltized to order updates as desired. When set to zero, the\n+ * updates default to being ordered by refname.\n+ */\n+int ref_transaction_update_reflog(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const char *committer_info,\n+\t\t\t\t  const char *msg,\n+\t\t\t\t  uint64_t index,\n+\t\t\t\t  struct strbuf *err);\n+\n /*\n  * Add a reference creation to transaction. new_oid is the value that\n  * the reference should have after the update; it must not be\n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523627","messageId":"20250806-pks-reflog-append-v6-4-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 4/9] builtin/reflog: implement subcommand to write new entries","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:15Z","receivedAt":"2025-08-06T05:54:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we provide a couple of subcommands in git-reflog(1) to remove\nreflog entries, we don't provide any to write new entries. Obviously\nthis is not an operation that really would be needed for many use cases\nout there, or otherwise people would have complained that such a command\ndoes not exist yet. But the introduction of the \"reftable\" backend\nchanges the picture a bit, as it is now basically impossible to manually\nappend a reflog entry if one wanted to do so due to the binary format.\n\nPlug this gap by introducing a simple \"write\" subcommand. For now, all\nthis command does is to append a single new reflog entry with the given\nobject IDs and message to the reflog. More specifically, it is not yet\npossible to:\n\n  - Write multiple reflog entries at once.\n\n  - Insert reflog entries at arbitrary indices.\n\n  - Specify the date of the reflog entry.\n\n  - Insert reflog entries that refer to nonexistent objects.\n\nIf required, those features can be added at a future point in time. For\nnow though, the new command aims to fulfill the most basic use cases\nwhile being as strict as possible when it comes to verifying parameters.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-reflog.adoc |   7 +++\n builtin/reflog.c              |  65 +++++++++++++++++++++\n t/meson.build                 |   1 +\n t/t1421-reflog-write.sh       | 128 ++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 201 insertions(+)\n\ndiff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc\nindex 4eb6c25607..38af0c977a 100644\n--- a/Documentation/git-reflog.adoc\n+++ b/Documentation/git-reflog.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n git reflog [show] [<log-options>] [<ref>]\n git reflog list\n git reflog exists <ref>\n+git reflog write <ref> <old-oid> <new-oid> <message>\n git reflog delete [--rewrite] [--updateref]\n \t[--dry-run | -n] [--verbose] <ref>@{<specifier>}...\n git reflog drop [--all [--single-worktree] | <refs>...]\n@@ -47,6 +48,12 @@ The \"exists\" subcommand checks whether a ref has a reflog.  It exits\n with zero status if the reflog exists, and non-zero status if it does\n not.\n \n+The \"write\" subcommand writes a single entry to the reflog of a given\n+reference. This new entry is appended to the reflog and will thus become\n+the most recent entry. The reference name must be fully qualified. Both the old\n+and new object IDs must not be abbreviated and must point to existing objects.\n+The reflog message gets normalized.\n+\n The \"delete\" subcommand deletes single entries from the reflog, but\n not the reflog itself. Its argument must be an _exact_ entry (e.g. \"`git\n reflog delete master@{2}`\"). This subcommand is also typically not used\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex b00b3f9edc..a1b4e02204 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -3,6 +3,8 @@\n #include \"builtin.h\"\n #include \"config.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-store.h\"\n #include \"revision.h\"\n #include \"reachable.h\"\n #include \"wildmatch.h\"\n@@ -20,6 +22,9 @@\n #define BUILTIN_REFLOG_EXISTS_USAGE \\\n \tN_(\"git reflog exists <ref>\")\n \n+#define BUILTIN_REFLOG_WRITE_USAGE \\\n+\tN_(\"git reflog write <ref> <old-oid> <new-oid> <message>\")\n+\n #define BUILTIN_REFLOG_DELETE_USAGE \\\n \tN_(\"git reflog delete [--rewrite] [--updateref]\\n\" \\\n \t   \"                  [--dry-run | -n] [--verbose] <ref>@{<specifier>}...\")\n@@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {\n \tNULL,\n };\n \n+static const char *const reflog_write_usage[] = {\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n+\tNULL,\n+};\n+\n static const char *const reflog_delete_usage[] = {\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tNULL\n@@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {\n \tBUILTIN_REFLOG_SHOW_USAGE,\n \tBUILTIN_REFLOG_LIST_USAGE,\n \tBUILTIN_REFLOG_EXISTS_USAGE,\n+\tBUILTIN_REFLOG_WRITE_USAGE,\n \tBUILTIN_REFLOG_DELETE_USAGE,\n \tBUILTIN_REFLOG_DROP_USAGE,\n \tBUILTIN_REFLOG_EXPIRE_USAGE,\n@@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int cmd_reflog_write(int argc, const char **argv, const char *prefix,\n+\t\t\t    struct repository *repo)\n+{\n+\tconst struct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct object_id old_oid, new_oid;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct ref_transaction *tx;\n+\tconst char *ref, *message;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);\n+\tif (argc != 4)\n+\t\tusage_with_options(reflog_write_usage, options);\n+\n+\tref = argv[0];\n+\tif (!is_root_ref(ref) && check_refname_format(ref, 0))\n+\t\tdie(_(\"invalid reference name: %s\"), ref);\n+\n+\tret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid old object ID: '%s'\"), argv[1]);\n+\tif (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))\n+\t\tdie(_(\"old object '%s' does not exist\"), argv[1]);\n+\n+\tret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);\n+\tif (ret)\n+\t\tdie(_(\"invalid new object ID: '%s'\"), argv[2]);\n+\tif (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))\n+\t\tdie(_(\"new object '%s' does not exist\"), argv[2]);\n+\n+\tmessage = argv[3];\n+\n+\ttx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\tif (!tx)\n+\t\tdie(_(\"cannot start transaction: %s\"), err.buf);\n+\n+\tret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,\n+\t\t\t\t\t    git_committer_info(0),\n+\t\t\t\t\t    message, 0, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot queue reflog update: %s\"), err.buf);\n+\n+\tret = ref_transaction_commit(tx, &err);\n+\tif (ret)\n+\t\tdie(_(\"cannot commit reflog update: %s\"), err.buf);\n+\n+\tref_transaction_free(tx);\n+\tstrbuf_release(&err);\n+\treturn 0;\n+}\n+\n /*\n  * main \"reflog\"\n  */\n@@ -405,6 +469,7 @@ int cmd_reflog(int argc,\n \t\tOPT_SUBCOMMAND(\"show\", &fn, cmd_reflog_show),\n \t\tOPT_SUBCOMMAND(\"list\", &fn, cmd_reflog_list),\n \t\tOPT_SUBCOMMAND(\"exists\", &fn, cmd_reflog_exists),\n+\t\tOPT_SUBCOMMAND(\"write\", &fn, cmd_reflog_write),\n \t\tOPT_SUBCOMMAND(\"delete\", &fn, cmd_reflog_delete),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_reflog_drop),\n \t\tOPT_SUBCOMMAND(\"expire\", &fn, cmd_reflog_expire),\ndiff --git a/t/meson.build b/t/meson.build\nindex d052fc3e23..adcdf09e74 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -220,6 +220,7 @@ integration_tests = [\n   't1418-reflog-exists.sh',\n   't1419-exclude-refs.sh',\n   't1420-lost-found.sh',\n+  't1421-reflog-write.sh',\n   't1430-bad-ref-name.sh',\n   't1450-fsck.sh',\n   't1451-fsck-buffer.sh',\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nnew file mode 100755\nindex 0000000000..dd7ffa5241\n--- /dev/null\n+++ b/t/t1421-reflog-write.sh\n@@ -0,0 +1,128 @@\n+#!/bin/sh\n+\n+test_description='Manually write reflog entries'\n+\n+. ./test-lib.sh\n+\n+SIGNATURE=\"C O Mitter <committer@example.com> 1112911993 -0700\"\n+\n+test_reflog_matches () {\n+\trepo=\"$1\" &&\n+\trefname=\"$2\" &&\n+\tcat >actual &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$refname\" >expected &&\n+\ttest_cmp expected actual\n+}\n+\n+test_expect_success 'invalid number of arguments' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tfor args in \"\" \"1\" \"1 2\" \"1 2 3\" \"1 2 3 4 5\"\n+\t\tdo\n+\t\t\ttest_must_fail git reflog write $args 2>err &&\n+\t\t\ttest_grep \"usage: git reflog write\" err || return 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success 'invalid refname' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write \"refs/heads/ invalid\" $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'unqualified refname is rejected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write unqualified $ZERO_OID $ZERO_OID first 2>err &&\n+\t\ttest_grep \"invalid reference name: \" err\n+\t)\n+'\n+\n+test_expect_success 'nonexistent object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"old object .* does not exist\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&\n+\t\ttest_grep \"new object .* does not exist\" err\n+\t)\n+'\n+\n+test_expect_success 'abbreviated object IDs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tabbreviated_oid=$(git rev-parse HEAD | test_copy_bytes 8) &&\n+\t\ttest_must_fail git reflog write refs/heads/something $abbreviated_oid $ZERO_OID old-object-id 2>err &&\n+\t\ttest_grep \"invalid old object ID\" err &&\n+\t\ttest_must_fail git reflog write refs/heads/something $ZERO_OID $abbreviated_oid new-object-id 2>err &&\n+\t\ttest_grep \"invalid new object ID\" err\n+\t)\n+'\n+\n+test_expect_success 'reflog message gets normalized' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\t\tgit reflog write HEAD $COMMIT_OID $COMMIT_OID \"$(printf \"message\\nwith\\nnewlines\")\" &&\n+\t\tgit reflog show -1 --format=%gs HEAD >actual &&\n+\t\techo \"message with newlines\" >expected &&\n+\t\ttest_cmp expected actual\n+\t)\n+'\n+\n+test_expect_success 'simple writes' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF &&\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n+\t\t# Note: the old object ID of the second reflog entry is broken.\n+\t\t# This will be fixed in subsequent commits.\n+\t\ttest_reflog_matches . refs/heads/something <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can write to root ref' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\tCOMMIT_OID=$(git rev-parse HEAD) &&\n+\n+\t\tgit reflog write ROOT_REF_HEAD $ZERO_OID $COMMIT_OID first &&\n+\t\ttest_reflog_matches . ROOT_REF_HEAD <<-EOF\n+\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523628","messageId":"20250806-pks-reflog-append-v6-5-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 5/9] ident: fix type of string length parameter","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:16Z","receivedAt":"2025-08-06T05:54:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The last parameter in `split_ident_line()` is the length of the line\npassed in by the caller. As such, most callers pass in either the result\nof `strlen()`, `struct strbuf::len` or a pointer diff, all of which\nare expected to be positive numbers. Regardless of that, the function\naccepts a signed integer, which is somewhat confusing.\n\nFix the function signature to instead accept a `size_t`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ident.c | 2 +-\n ident.h | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/ident.c b/ident.c\nindex 967895d8850..a7a2d132579 100644\n--- a/ident.c\n+++ b/ident.c\n@@ -272,7 +272,7 @@ static void strbuf_addstr_without_crud(struct strbuf *sb, const char *src)\n  * can still be NULL if the input line only has the name/email part\n  * (e.g. reading from a reflog entry).\n  */\n-int split_ident_line(struct ident_split *split, const char *line, int len)\n+int split_ident_line(struct ident_split *split, const char *line, size_t len)\n {\n \tconst char *cp;\n \tsize_t span;\ndiff --git a/ident.h b/ident.h\nindex 6a79febba15..3c034038791 100644\n--- a/ident.h\n+++ b/ident.h\n@@ -35,7 +35,7 @@ void reset_ident_date(void);\n  * Signals an success with 0, but time part of the result may be NULL\n  * if the input lacks timestamp and zone\n  */\n-int split_ident_line(struct ident_split *, const char *, int);\n+int split_ident_line(struct ident_split *, const char *, size_t);\n \n /*\n  * Given a commit or tag object buffer and the commit or tag headers, replaces\n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523629","messageId":"20250806-pks-reflog-append-v6-6-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 6/9] refs: fix identity for migrated reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:17Z","receivedAt":"2025-08-06T05:54:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we must\nreconstruct the identity of reflog entries. This is done by passing the\ncommitter passed to the `migrate_one_reflog_entry()` callback function\nto `fmt_ident()`.\n\nThis results in an invalid identity though: `fmt_ident()` expects the\ncaller to provide both name and mail of the author, but we pass the full\nidentity as mail. This leads to an identity like:\n\n    pks <Patrick Steinhardt ps@pks.im>\n\nFix the bug by splitting the identity line first. This allows us to\nextract both the name and mail so that we can pass them to `fmt_ident()`\nseparately.\n\nThis commit does not yet add any tests as there is another bug in the\nreflog migration that will be fixed in a subsequent commit. Once that\nbug is fixed we'll make the reflog verification in t1450 stricter, and\nthat will catch both this bug here and the other bug.\n\nNote that we also add two new `name` and `mail` string buffers to the\ncallback structures and splice them through to the callbacks. This is\ndone so that we can avoid allocating a new buffer every time we compute\nthe committer information.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 22 ++++++++++++++++++----\n 1 file changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 8aa9f7236a..a5f9ffaa45 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2954,7 +2954,7 @@ struct migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf sb;\n+\tstruct strbuf sb, name, mail;\n };\n \n static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,\n@@ -2993,7 +2993,7 @@ struct reflog_migration_data {\n \tstruct ref_store *old_refs;\n \tstruct ref_transaction *transaction;\n \tstruct strbuf *errbuf;\n-\tstruct strbuf *sb;\n+\tstruct strbuf *sb, *name, *mail;\n };\n \n static int migrate_one_reflog_entry(struct object_id *old_oid,\n@@ -3003,13 +3003,21 @@ static int migrate_one_reflog_entry(struct object_id *old_oid,\n \t\t\t\t    const char *msg, void *cb_data)\n {\n \tstruct reflog_migration_data *data = cb_data;\n+\tstruct ident_split ident;\n \tconst char *date;\n \tint ret;\n \n+\tif (split_ident_line(&ident, committer, strlen(committer)) < 0)\n+\t\treturn -1;\n+\n+\tstrbuf_reset(data->name);\n+\tstrbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin);\n+\tstrbuf_reset(data->mail);\n+\tstrbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin);\n+\n \tdate = show_date(timestamp, tz, DATE_MODE(NORMAL));\n \tstrbuf_reset(data->sb);\n-\t/* committer contains name and email */\n-\tstrbuf_addstr(data->sb, fmt_ident(\"\", committer, WANT_BLANK_IDENT, date, 0));\n+\tstrbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0));\n \n \tret = ref_transaction_update_reflog(data->transaction, data->refname,\n \t\t\t\t\t    new_oid, old_oid, data->sb->buf,\n@@ -3026,6 +3034,8 @@ static int migrate_one_reflog(const char *refname, void *cb_data)\n \t\t.transaction = migration_data->transaction,\n \t\t.errbuf = migration_data->errbuf,\n \t\t.sb = &migration_data->sb,\n+\t\t.name = &migration_data->name,\n+\t\t.mail = &migration_data->mail,\n \t};\n \n \treturn refs_for_each_reflog_ent(migration_data->old_refs, refname,\n@@ -3124,6 +3134,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tstruct strbuf new_gitdir = STRBUF_INIT;\n \tstruct migration_data data = {\n \t\t.sb = STRBUF_INIT,\n+\t\t.name = STRBUF_INIT,\n+\t\t.mail = STRBUF_INIT,\n \t};\n \tint did_migrate_refs = 0;\n \tint ret;\n@@ -3299,6 +3311,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \tref_transaction_free(transaction);\n \tstrbuf_release(&new_gitdir);\n \tstrbuf_release(&data.sb);\n+\tstrbuf_release(&data.name);\n+\tstrbuf_release(&data.mail);\n \treturn ret;\n }\n \n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523630","messageId":"20250806-pks-reflog-append-v6-7-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 7/9] refs/files: detect race when generating reflog entry for HEAD","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:18Z","receivedAt":"2025-08-06T05:54:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When updating a reference that is being pointed to HEAD we don't only\nwrite a reflog message for that particular reference, but also generate\none for HEAD. This logic is handled by `split_head_update()`, where we:\n\n  1. Verify that the condition actually triggered. This is done by\n     reading HEAD at the start of the transaction so that we can then\n     check whether a given reference update refers to its target.\n\n  2. Queue a new log-only update for HEAD in case it did.\n\nBut the logic is unfortunately not free of races, as we do not lock the\nHEAD reference after we have read its target. This can lead to the\nfollowing two scenarios:\n\n  - HEAD gets concurrently updated to point to one of the references we\n    have already processed. This causes us not writing a reflog message\n    even though we should have done so.\n\n  - HEAD gets concurrently updated to no longer point to a reference\n    anymore that we have already processed. This causes us to write a\n    reflog message even though we should _not_ have done so.\n\nImprove the situation by introducing a new `REF_LOG_VIA_SPLIT` flag that\nis specific to the \"files\" backend. If set, we will double check that\nthe HEAD reference still points to the reference that we are creating\nthe reflog entry for after we have locked HEAD. Furthermore, instead of\nmanually resolving the old object ID of that entry, we now use the same\nold state as for the parent update.\n\nIf we detect such a racy update we abort the transaction. This is a bit\nheavy-handed: the user didn't even ask us to write a reflog update for\n\"HEAD\", so it might be surprising if we abort the transaction. That\nbeing said:\n\n  - Normal users wouldn't typically hit this case as we only hit the\n    relevant code when committing to a branch that is being pointed to\n    by \"HEAD\" directly. Commands like git-commit(1) typically commit to\n    \"HEAD\" itself though.\n\n  - Scripted users that use git-update-ref(1) and related plumbing\n    commands are unlikely to hit this case either, as they would have to\n    update the pointed-to-branch at the same as \"HEAD\" is being updated,\n    which is an exceedingly rare event.\n\nThe alternative would be to instead drop the log-only update completely,\nbut that would require more logic that is hard to verify without adding\ninfrastructure specific for such a test. So we rather do the pragmatic\nthing and don't worry too much about an edge case that is very unlikely\nto happen.\n\nUnfortunately, this change only helps with the second race. We cannot\nreliably plug the first race without locking the HEAD reference at the\nstart of the transaction. Locking HEAD unconditionally would effectively\nserialize all writes though, and that doesn't seem like an option. Also,\ndouble checking its value at the end of the transaction is not an option\neither, as its target may have flip-flopped during the transaction.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c | 40 ++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 38 insertions(+), 2 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex bf6f89b1d1..ba018b0984 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -68,6 +68,12 @@\n  */\n #define REF_DELETED_RMDIR (1 << 9)\n \n+/*\n+ * Used to indicate that the reflog-only update has been created via\n+ * `split_head_update()`.\n+ */\n+#define REF_LOG_VIA_SPLIT (1 << 14)\n+\n struct ref_lock {\n \tchar *ref_name;\n \tstruct lock_file lk;\n@@ -2420,9 +2426,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,\n \n \tnew_update = ref_transaction_add_update(\n \t\t\ttransaction, \"HEAD\",\n-\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF,\n+\t\t\tupdate->flags | REF_LOG_ONLY | REF_NO_DEREF | REF_LOG_VIA_SPLIT,\n \t\t\t&update->new_oid, &update->old_oid,\n \t\t\tNULL, NULL, update->committer_info, update->msg);\n+\tnew_update->parent_update = update;\n \n \t/*\n \t * Add \"HEAD\". This insertion is O(N) in the transaction\n@@ -2600,7 +2607,36 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re\n \n \tupdate->backend_data = lock;\n \n-\tif (update->type & REF_ISSYMREF) {\n+\tif (update->flags & REF_LOG_VIA_SPLIT) {\n+\t\tstruct ref_lock *parent_lock;\n+\n+\t\tif (!update->parent_update)\n+\t\t\tBUG(\"split update without a parent\");\n+\n+\t\tparent_lock = update->parent_update->backend_data;\n+\n+\t\t/*\n+\t\t * Check that \"HEAD\" didn't racily change since we have looked\n+\t\t * it up. If it did we must refuse to write the reflog entry.\n+\t\t *\n+\t\t * Note that this does not catch all races: if \"HEAD\" was\n+\t\t * racily changed to point to one of the refs part of the\n+\t\t * transaction then we would miss writing the split reflog\n+\t\t * entry for \"HEAD\".\n+\t\t */\n+\t\tif (!(update->type & REF_ISSYMREF) ||\n+\t\t    strcmp(update->parent_update->refname, referent.buf)) {\n+\t\t\tstrbuf_addstr(err, \"HEAD has been racily updated\");\n+\t\t\tret = REF_TRANSACTION_ERROR_GENERIC;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (update->flags & REF_HAVE_OLD) {\n+\t\t\toidcpy(&lock->old_oid, &update->old_oid);\n+\t\t} else {\n+\t\t\toidcpy(&lock->old_oid, &parent_lock->old_oid);\n+\t\t}\n+\t} else if (update->type & REF_ISSYMREF) {\n \t\tif (update->flags & REF_NO_DEREF) {\n \t\t\t/*\n \t\t\t * We won't be reading the referent as part of\n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523631","messageId":"20250806-pks-reflog-append-v6-8-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 8/9] refs: stop unsetting REF_HAVE_OLD for log-only updates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:19Z","receivedAt":"2025-08-06T05:54:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `REF_HAVE_OLD` flag indicates whether a given ref update has its old\nobject ID set. If so, the value of that field is used to verify whether\nthe current state of the reference matches this expected state. It is\nthus an important part of mitigating races with a concurrent process\nthat updates the same set of references.\n\nWhen writing reflogs though we explicitly unset that flag. This is a\nsensible thing to do: the old state of reflog entry updates may not\nnecessarily match the current on-disk state of its accompanying ref, but\nit's only intended to signal what old object ID we want to write into\nthe new reflog entry. For example when migrating refs we end up writing\nmany reflog entries for a single reference, and most likely those reflog\nentries will have many different old object IDs.\n\nBut unsetting this flag also removes a useful signal, namely that the\ncaller _did_ provide an old object ID for a given reflog entry. This\nsignal will become useful in a subsequent commit, where we add a new\nflag that tells the transaction to use the provided old and new object\nIDs to write a reflog entry. The `REF_HAVE_OLD` flag is then used as a\nsignal to verify that the caller really did provide an old object ID.\n\nStop unsetting the flag so that we can use it as this described signal\nin a subsequent commit. Skip checking the old object ID for log-only\nupdates so that we don't expect it to match the current on-disk state.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  8 +++-----\n refs/files-backend.c    |  9 +++++----\n refs/refs-internal.h    |  3 ++-\n refs/reftable-backend.c | 12 +++---------\n 4 files changed, 13 insertions(+), 19 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex a5f9ffaa45..f88928de74 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1393,11 +1393,6 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \tupdate = ref_transaction_add_update(transaction, refname, flags,\n \t\t\t\t\t    new_oid, old_oid, NULL, NULL,\n \t\t\t\t\t    committer_info, msg);\n-\t/*\n-\t * While we do set the old_oid value, we unset the flag to skip\n-\t * old_oid verification which only makes sense for refs.\n-\t */\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \tupdate->index = index;\n \n \t/*\n@@ -3318,6 +3313,9 @@ int repo_migrate_ref_storage_format(struct repository *repo,\n \n int ref_update_expects_existing_old_ref(struct ref_update *update)\n {\n+\tif (update->flags & REF_LOG_ONLY)\n+\t\treturn 0;\n+\n \treturn (update->flags & REF_HAVE_OLD) &&\n \t\t(!is_null_oid(&update->old_oid) || update->old_target);\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex ba018b0984..85ab2ef2b9 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -2500,7 +2500,6 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,\n \t * done when new_update is processed.\n \t */\n \tupdate->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\tupdate->flags &= ~REF_HAVE_OLD;\n \n \treturn 0;\n }\n@@ -2515,8 +2514,9 @@ static enum ref_transaction_error check_old_oid(struct ref_update *update,\n \t\t\t\t\t\tstruct object_id *oid,\n \t\t\t\t\t\tstruct strbuf *err)\n {\n-\tif (!(update->flags & REF_HAVE_OLD) ||\n-\t\t   oideq(oid, &update->old_oid))\n+\tif (update->flags & REF_LOG_ONLY ||\n+\t    !(update->flags & REF_HAVE_OLD) ||\n+\t    oideq(oid, &update->old_oid))\n \t\treturn 0;\n \n \tif (is_null_oid(&update->old_oid)) {\n@@ -3095,7 +3095,8 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,\n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n \n-\t\tif ((update->flags & REF_HAVE_OLD) &&\n+\t\tif (!(update->flags & REF_LOG_ONLY) &&\n+\t\t    (update->flags & REF_HAVE_OLD) &&\n \t\t    !is_null_oid(&update->old_oid))\n \t\t\tBUG(\"initial ref transaction with old_sha1 set\");\n \ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex f868870851..95a4dc3902 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -802,7 +802,8 @@ enum ref_transaction_error ref_update_check_old_target(const char *referent,\n \n /*\n  * Check if the ref must exist, this means that the old_oid or\n- * old_target is non NULL.\n+ * old_target is non NULL. Log-only updates never require the old state to\n+ * match.\n  */\n int ref_update_expects_existing_old_ref(struct ref_update *update);\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4c3817f4ec..44af58ac50 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1180,8 +1180,6 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret > 0) {\n \t\t/* The reference does not exist, but we expected it to. */\n \t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n-\n-\n \t\t\t\t   \"unable to resolve reference '%s'\"),\n \t\t\t    ref_update_original_update_refname(u), u->refname);\n \t\treturn REF_TRANSACTION_ERROR_NONEXISTENT_REF;\n@@ -1235,13 +1233,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \n \t\t\tnew_update->parent_update = u;\n \n-\t\t\t/*\n-\t\t\t * Change the symbolic ref update to log only. Also, it\n-\t\t\t * doesn't need to check its old OID value, as that will be\n-\t\t\t * done when new_update is processed.\n-\t\t\t */\n+\t\t\t/* Change the symbolic ref update to log only. */\n \t\t\tu->flags |= REF_LOG_ONLY | REF_NO_DEREF;\n-\t\t\tu->flags &= ~REF_HAVE_OLD;\n \t\t}\n \t}\n \n@@ -1265,7 +1258,8 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \t\tret = ref_update_check_old_target(referent->buf, u, err);\n \t\tif (ret)\n \t\t\treturn ret;\n-\t} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {\n+\t} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&\n+\t\t   !oideq(&current_oid, &u->old_oid)) {\n \t\tif (is_null_oid(&u->old_oid)) {\n \t\t\tstrbuf_addf(err, _(\"cannot lock ref '%s': \"\n \t\t\t\t\t   \"reference already exists\"),\n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"},{"id":"523632","messageId":"20250806-pks-reflog-append-v6-9-a50839653766@pks.im","threadId":"63818","inReplyTo":"20250806-pks-reflog-append-v6-0-a50839653766@pks.im","subject":"[PATCH v6 9/9] refs: fix invalid old object IDs when migrating reflogs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-06T05:54:20Z","receivedAt":"2025-08-06T05:54:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When migrating reflog entries between different storage formats we end\nup with invalid old object IDs for the migrated entries: instead of\nwriting the old object ID of the to-be-migrated entry, we end up with\nthe all-zeroes object ID.\n\nThe root cause of this issue is that we don't know to use the old object\nID provided by the caller. Instead, we manually resolve the old object\nID by resolving the current value of its matching reference. But as that\nreference does not yet exist in the target ref storage we always end up\nresolving it to all-zeroes.\n\nThis issue got unnoticed as there is no user-facing command that would\neven show the old object ID. While `git log -g` knows to show the new\nobject ID, we don't have any formatting directive to show the old object\nID.\n\nFix the bug by introducing a new flag `REF_LOG_USE_PROVIDED_OIDS`. If\nset, backends are instructed to use the old and new object IDs provided\nby the caller, without doing any manual resolving. Set this flag in\n`ref_transaction_update_reflog()`.\n\nAmend our tests in t1460-refs-migrate to use our test tool to read\nreflog entries. This test tool prints out both old and new object ID of\neach reflog entry, which fixes the test gap. Furthermore it also prints\nthe full identity used to write the reflog, which provides test coverage\nfor the previous commit in this patch series that fixed the identity for\nmigrated reflogs.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                  |  3 ++-\n refs.h                  |  9 ++++++++-\n refs/files-backend.c    | 16 +++++++++++++++-\n refs/reftable-backend.c | 14 ++++++++++++++\n t/t1421-reflog-write.sh |  4 +---\n t/t1460-refs-migrate.sh | 22 +++++++++++++++-------\n 6 files changed, 55 insertions(+), 13 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex f88928de74..946eb48941 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1385,7 +1385,8 @@ int ref_transaction_update_reflog(struct ref_transaction *transaction,\n \n \tassert(err);\n \n-\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF;\n+\tflags = REF_HAVE_OLD | REF_HAVE_NEW | REF_LOG_ONLY | REF_FORCE_CREATE_REFLOG | REF_NO_DEREF |\n+\t\tREF_LOG_USE_PROVIDED_OIDS;\n \n \tif (!transaction_refname_valid(refname, new_oid, flags, err))\n \t\treturn -1;\ndiff --git a/refs.h b/refs.h\nindex 253dd8f4d5..090b4fdff4 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -760,13 +760,20 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,\n  */\n #define REF_SKIP_CREATE_REFLOG (1 << 12)\n \n+/*\n+ * When writing a REF_LOG_ONLY record, use the old and new object IDs provided\n+ * in the update instead of resolving the old object ID. The caller must also\n+ * set both REF_HAVE_OLD and REF_HAVE_NEW.\n+ */\n+#define REF_LOG_USE_PROVIDED_OIDS (1 << 13)\n+\n /*\n  * Bitmask of all of the flags that are allowed to be passed in to\n  * ref_transaction_update() and friends:\n  */\n #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \\\n \t(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \\\n-\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)\n+\t REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | REF_LOG_USE_PROVIDED_OIDS)\n \n /*\n  * Add a reference update to transaction. `new_oid` is the value that\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 85ab2ef2b9..905555365b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -3010,6 +3010,20 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t\t\t  struct ref_lock *lock,\n \t\t\t\t  struct strbuf *err)\n {\n+\tstruct object_id *old_oid = &lock->old_oid;\n+\n+\tif (update->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(update->flags & REF_HAVE_OLD) ||\n+\t\t    !(update->flags & REF_HAVE_NEW) ||\n+\t\t    !(update->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), update->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\told_oid = &update->old_oid;\n+\t}\n+\n \tif (update->new_target) {\n \t\t/*\n \t\t * We want to get the resolved OID for the target, to ensure\n@@ -3027,7 +3041,7 @@ static int parse_and_write_reflog(struct files_ref_store *refs,\n \t\t}\n \t}\n \n-\tif (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,\n+\tif (files_log_ref_write(refs, lock->ref_name, old_oid,\n \t\t\t\t&update->new_oid, update->committer_info,\n \t\t\t\tupdate->msg, update->flags, err)) {\n \t\tchar *old_msg = strbuf_detach(err, NULL);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 44af58ac50..99fafd75eb 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -1096,6 +1096,20 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor\n \tif (ret)\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\tif (u->flags & REF_LOG_USE_PROVIDED_OIDS) {\n+\t\tif (!(u->flags & REF_HAVE_OLD) ||\n+\t\t    !(u->flags & REF_HAVE_NEW) ||\n+\t\t    !(u->flags & REF_LOG_ONLY)) {\n+\t\t\tstrbuf_addf(err, _(\"trying to write reflog for '%s'\"\n+\t\t\t\t\t   \"with incomplete values\"), u->refname);\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\t}\n+\n+\t\tif (queue_transaction_update(refs, tx_data, u, &u->old_oid, err))\n+\t\t\treturn REF_TRANSACTION_ERROR_GENERIC;\n+\t\treturn 0;\n+\t}\n+\n \t/* Verify that the new object ID is valid. */\n \tif ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) &&\n \t    !(u->flags & REF_SKIP_OID_VERIFICATION) &&\ndiff --git a/t/t1421-reflog-write.sh b/t/t1421-reflog-write.sh\nindex dd7ffa5241..46df64c176 100755\n--- a/t/t1421-reflog-write.sh\n+++ b/t/t1421-reflog-write.sh\n@@ -101,11 +101,9 @@ test_expect_success 'simple writes' '\n \t\tEOF\n \n \t\tgit reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&\n-\t\t# Note: the old object ID of the second reflog entry is broken.\n-\t\t# This will be fixed in subsequent commits.\n \t\ttest_reflog_matches . refs/heads/something <<-EOF\n \t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tfirst\n-\t\t$ZERO_OID $COMMIT_OID $SIGNATURE\tsecond\n+\t\t$COMMIT_OID $COMMIT_OID $SIGNATURE\tsecond\n \t\tEOF\n \t)\n '\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 2ab97e1b7d..0e1116a319 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -7,6 +7,17 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n+print_all_reflog_entries () {\n+\trepo=$1 &&\n+\ttest-tool -C \"$repo\" ref-store main for-each-reflog >reflogs &&\n+\twhile read reflog\n+\tdo\n+\t\techo \"REFLOG: $reflog\" &&\n+\t\ttest-tool -C \"$repo\" ref-store main for-each-reflog-ent \"$reflog\" ||\n+\t\treturn 1\n+\tdone <reflogs\n+}\n+\n # Migrate the provided repository from one format to the other and\n # verify that the references and logs are migrated over correctly.\n # Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]\n@@ -28,8 +39,7 @@ test_migration () {\n \t\t--format='%(refname) %(objectname) %(symref)' >expect &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t   git -C \"$repo\" reflog --all >expect_logs &&\n-\t   git -C \"$repo\" reflog list >expect_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >expect_logs\n \tfi &&\n \n \tgit -C \"$repo\" refs migrate --ref-format=\"$format\" \"$@\" &&\n@@ -39,10 +49,8 @@ test_migration () {\n \ttest_cmp expect actual &&\n \tif ! $skip_reflog_verify\n \tthen\n-\t\tgit -C \"$repo\" reflog --all >actual_logs &&\n-\t\tgit -C \"$repo\" reflog list >actual_log_list &&\n-\t\ttest_cmp expect_logs actual_logs &&\n-\t\ttest_cmp expect_log_list actual_log_list\n+\t\tprint_all_reflog_entries \"$repo\" >actual_logs &&\n+\t\ttest_cmp expect_logs actual_logs\n \tfi &&\n \n \tgit -C \"$repo\" rev-parse --show-ref-format >actual &&\n@@ -273,7 +281,7 @@ test_expect_success 'multiple reftable blocks with multiple entries' '\n \ttest_commit -C repo second &&\n \tprintf \"update refs/heads/ref-%d HEAD\\n\" $(test_seq 3000) >stdin &&\n \tgit -C repo update-ref --stdin <stdin &&\n-\ttest_migration repo reftable\n+\ttest_migration repo reftable true\n '\n \n test_expect_success 'migrating from files format deletes backend files' '\n\n-- \n2.51.0.rc0.215.g125493bb4a.dirty\n\n"}]}