Skip to content

Commit 7aa619c

Browse files
pks-tgitster
authored andcommitted
builtin/reflog: implement subcommand to write new entries
While we provide a couple of subcommands in git-reflog(1) to remove reflog entries, we don't provide any to write new entries. Obviously this is not an operation that really would be needed for many use cases out there, or otherwise people would have complained that such a command does not exist yet. But the introduction of the "reftable" backend changes the picture a bit, as it is now basically impossible to manually append a reflog entry if one wanted to do so due to the binary format. Plug this gap by introducing a simple "write" subcommand. For now, all this command does is to append a single new reflog entry with the given object IDs and message to the reflog. More specifically, it is not yet possible to: - Write multiple reflog entries at once. - Insert reflog entries at arbitrary indices. - Specify the date of the reflog entry. - Insert reflog entries that refer to nonexistent objects. If required, those features can be added at a future point in time. For now though, the new command aims to fulfill the most basic use cases while being as strict as possible when it comes to verifying parameters. Signed-off-by: Patrick Steinhardt <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 1ffd2d4 commit 7aa619c

File tree

4 files changed

+201
-0
lines changed

4 files changed

+201
-0
lines changed

Documentation/git-reflog.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ SYNOPSIS
1212
git reflog [show] [<log-options>] [<ref>]
1313
git reflog list
1414
git reflog exists <ref>
15+
git reflog write <ref> <old-oid> <new-oid> <message>
1516
git reflog delete [--rewrite] [--updateref]
1617
[--dry-run | -n] [--verbose] <ref>@{<specifier>}...
1718
git reflog drop [--all [--single-worktree] | <refs>...]
@@ -47,6 +48,12 @@ The "exists" subcommand checks whether a ref has a reflog. It exits
4748
with zero status if the reflog exists, and non-zero status if it does
4849
not.
4950

51+
The "write" subcommand writes a single entry to the reflog of a given
52+
reference. This new entry is appended to the reflog and will thus become
53+
the most recent entry. The reference name must be fully qualified. Both the old
54+
and new object IDs must not be abbreviated and must point to existing objects.
55+
The reflog message gets normalized.
56+
5057
The "delete" subcommand deletes single entries from the reflog, but
5158
not the reflog itself. Its argument must be an _exact_ entry (e.g. "`git
5259
reflog delete master@{2}`"). This subcommand is also typically not used

builtin/reflog.c

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include "builtin.h"
44
#include "config.h"
55
#include "gettext.h"
6+
#include "hex.h"
7+
#include "object-store.h"
68
#include "revision.h"
79
#include "reachable.h"
810
#include "wildmatch.h"
@@ -20,6 +22,9 @@
2022
#define BUILTIN_REFLOG_EXISTS_USAGE \
2123
N_("git reflog exists <ref>")
2224

25+
#define BUILTIN_REFLOG_WRITE_USAGE \
26+
N_("git reflog write <ref> <old-oid> <new-oid> <message>")
27+
2328
#define BUILTIN_REFLOG_DELETE_USAGE \
2429
N_("git reflog delete [--rewrite] [--updateref]\n" \
2530
" [--dry-run | -n] [--verbose] <ref>@{<specifier>}...")
@@ -47,6 +52,11 @@ static const char *const reflog_exists_usage[] = {
4752
NULL,
4853
};
4954

55+
static const char *const reflog_write_usage[] = {
56+
BUILTIN_REFLOG_WRITE_USAGE,
57+
NULL,
58+
};
59+
5060
static const char *const reflog_delete_usage[] = {
5161
BUILTIN_REFLOG_DELETE_USAGE,
5262
NULL
@@ -66,6 +76,7 @@ static const char *const reflog_usage[] = {
6676
BUILTIN_REFLOG_SHOW_USAGE,
6777
BUILTIN_REFLOG_LIST_USAGE,
6878
BUILTIN_REFLOG_EXISTS_USAGE,
79+
BUILTIN_REFLOG_WRITE_USAGE,
6980
BUILTIN_REFLOG_DELETE_USAGE,
7081
BUILTIN_REFLOG_DROP_USAGE,
7182
BUILTIN_REFLOG_EXPIRE_USAGE,
@@ -392,6 +403,59 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,
392403
return ret;
393404
}
394405

406+
static int cmd_reflog_write(int argc, const char **argv, const char *prefix,
407+
struct repository *repo)
408+
{
409+
const struct option options[] = {
410+
OPT_END()
411+
};
412+
struct object_id old_oid, new_oid;
413+
struct strbuf err = STRBUF_INIT;
414+
struct ref_transaction *tx;
415+
const char *ref, *message;
416+
int ret;
417+
418+
argc = parse_options(argc, argv, prefix, options, reflog_write_usage, 0);
419+
if (argc != 4)
420+
usage_with_options(reflog_write_usage, options);
421+
422+
ref = argv[0];
423+
if (!is_root_ref(ref) && check_refname_format(ref, 0))
424+
die(_("invalid reference name: %s"), ref);
425+
426+
ret = get_oid_hex_algop(argv[1], &old_oid, repo->hash_algo);
427+
if (ret)
428+
die(_("invalid old object ID: '%s'"), argv[1]);
429+
if (!is_null_oid(&old_oid) && !has_object(the_repository, &old_oid, 0))
430+
die(_("old object '%s' does not exist"), argv[1]);
431+
432+
ret = get_oid_hex_algop(argv[2], &new_oid, repo->hash_algo);
433+
if (ret)
434+
die(_("invalid new object ID: '%s'"), argv[2]);
435+
if (!is_null_oid(&new_oid) && !has_object(the_repository, &new_oid, 0))
436+
die(_("new object '%s' does not exist"), argv[2]);
437+
438+
message = argv[3];
439+
440+
tx = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
441+
if (!tx)
442+
die(_("cannot start transaction: %s"), err.buf);
443+
444+
ret = ref_transaction_update_reflog(tx, ref, &new_oid, &old_oid,
445+
git_committer_info(0),
446+
message, 0, &err);
447+
if (ret)
448+
die(_("cannot queue reflog update: %s"), err.buf);
449+
450+
ret = ref_transaction_commit(tx, &err);
451+
if (ret)
452+
die(_("cannot commit reflog update: %s"), err.buf);
453+
454+
ref_transaction_free(tx);
455+
strbuf_release(&err);
456+
return 0;
457+
}
458+
395459
/*
396460
* main "reflog"
397461
*/
@@ -405,6 +469,7 @@ int cmd_reflog(int argc,
405469
OPT_SUBCOMMAND("show", &fn, cmd_reflog_show),
406470
OPT_SUBCOMMAND("list", &fn, cmd_reflog_list),
407471
OPT_SUBCOMMAND("exists", &fn, cmd_reflog_exists),
472+
OPT_SUBCOMMAND("write", &fn, cmd_reflog_write),
408473
OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete),
409474
OPT_SUBCOMMAND("drop", &fn, cmd_reflog_drop),
410475
OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire),

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ integration_tests = [
220220
't1418-reflog-exists.sh',
221221
't1419-exclude-refs.sh',
222222
't1420-lost-found.sh',
223+
't1421-reflog-write.sh',
223224
't1430-bad-ref-name.sh',
224225
't1450-fsck.sh',
225226
't1451-fsck-buffer.sh',

t/t1421-reflog-write.sh

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/bin/sh
2+
3+
test_description='Manually write reflog entries'
4+
5+
. ./test-lib.sh
6+
7+
SIGNATURE="C O Mitter <[email protected]> 1112911993 -0700"
8+
9+
test_reflog_matches () {
10+
repo="$1" &&
11+
refname="$2" &&
12+
cat >actual &&
13+
test-tool -C "$repo" ref-store main for-each-reflog-ent "$refname" >expected &&
14+
test_cmp expected actual
15+
}
16+
17+
test_expect_success 'invalid number of arguments' '
18+
test_when_finished "rm -rf repo" &&
19+
git init repo &&
20+
(
21+
cd repo &&
22+
for args in "" "1" "1 2" "1 2 3" "1 2 3 4 5"
23+
do
24+
test_must_fail git reflog write $args 2>err &&
25+
test_grep "usage: git reflog write" err || return 1
26+
done
27+
)
28+
'
29+
30+
test_expect_success 'invalid refname' '
31+
test_when_finished "rm -rf repo" &&
32+
git init repo &&
33+
(
34+
cd repo &&
35+
test_must_fail git reflog write "refs/heads/ invalid" $ZERO_OID $ZERO_OID first 2>err &&
36+
test_grep "invalid reference name: " err
37+
)
38+
'
39+
40+
test_expect_success 'unqualified refname is rejected' '
41+
test_when_finished "rm -rf repo" &&
42+
git init repo &&
43+
(
44+
cd repo &&
45+
test_must_fail git reflog write unqualified $ZERO_OID $ZERO_OID first 2>err &&
46+
test_grep "invalid reference name: " err
47+
)
48+
'
49+
50+
test_expect_success 'nonexistent object IDs' '
51+
test_when_finished "rm -rf repo" &&
52+
git init repo &&
53+
(
54+
cd repo &&
55+
test_must_fail git reflog write refs/heads/something $(test_oid deadbeef) $ZERO_OID old-object-id 2>err &&
56+
test_grep "old object .* does not exist" err &&
57+
test_must_fail git reflog write refs/heads/something $ZERO_OID $(test_oid deadbeef) new-object-id 2>err &&
58+
test_grep "new object .* does not exist" err
59+
)
60+
'
61+
62+
test_expect_success 'abbreviated object IDs' '
63+
test_when_finished "rm -rf repo" &&
64+
git init repo &&
65+
(
66+
cd repo &&
67+
test_commit initial &&
68+
abbreviated_oid=$(git rev-parse HEAD | test_copy_bytes 8) &&
69+
test_must_fail git reflog write refs/heads/something $abbreviated_oid $ZERO_OID old-object-id 2>err &&
70+
test_grep "invalid old object ID" err &&
71+
test_must_fail git reflog write refs/heads/something $ZERO_OID $abbreviated_oid new-object-id 2>err &&
72+
test_grep "invalid new object ID" err
73+
)
74+
'
75+
76+
test_expect_success 'reflog message gets normalized' '
77+
test_when_finished "rm -rf repo" &&
78+
git init repo &&
79+
(
80+
cd repo &&
81+
test_commit initial &&
82+
COMMIT_OID=$(git rev-parse HEAD) &&
83+
git reflog write HEAD $COMMIT_OID $COMMIT_OID "$(printf "message\nwith\nnewlines")" &&
84+
git reflog show -1 --format=%gs HEAD >actual &&
85+
echo "message with newlines" >expected &&
86+
test_cmp expected actual
87+
)
88+
'
89+
90+
test_expect_success 'simple writes' '
91+
test_when_finished "rm -rf repo" &&
92+
git init repo &&
93+
(
94+
cd repo &&
95+
test_commit initial &&
96+
COMMIT_OID=$(git rev-parse HEAD) &&
97+
98+
git reflog write refs/heads/something $ZERO_OID $COMMIT_OID first &&
99+
test_reflog_matches . refs/heads/something <<-EOF &&
100+
$ZERO_OID $COMMIT_OID $SIGNATURE first
101+
EOF
102+
103+
git reflog write refs/heads/something $COMMIT_OID $COMMIT_OID second &&
104+
# Note: the old object ID of the second reflog entry is broken.
105+
# This will be fixed in subsequent commits.
106+
test_reflog_matches . refs/heads/something <<-EOF
107+
$ZERO_OID $COMMIT_OID $SIGNATURE first
108+
$ZERO_OID $COMMIT_OID $SIGNATURE second
109+
EOF
110+
)
111+
'
112+
113+
test_expect_success 'can write to root ref' '
114+
test_when_finished "rm -rf repo" &&
115+
git init repo &&
116+
(
117+
cd repo &&
118+
test_commit initial &&
119+
COMMIT_OID=$(git rev-parse HEAD) &&
120+
121+
git reflog write ROOT_REF_HEAD $ZERO_OID $COMMIT_OID first &&
122+
test_reflog_matches . ROOT_REF_HEAD <<-EOF
123+
$ZERO_OID $COMMIT_OID $SIGNATURE first
124+
EOF
125+
)
126+
'
127+
128+
test_done

0 commit comments

Comments
 (0)