reflog: implement subcommand to drop reflogs

While 'git-reflog(1)' currently allows users to expire reflogs and delete individual entries, it lacks functionality to completely remove reflogs for specific references. This becomes problematic in repositories where reflogs are not needed but continue to accumulate entries despite setting 'core.logAllRefUpdates=false'. Add a new 'drop' subcommand to git-reflog that allows users to delete the entire reflog for a specified reference. Include an '--all' flag to enable dropping all reflogs from all worktrees and an addon flag '--single-worktree', to only drop all reflogs from the current worktree. While here, remove an extraneous newline in the file. Signed-off-by: Karthik Nayak <karthik.188@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karthik Nayak committed Mar 14, 2025 at 09:40 UTC d1270689a11e1e0dcf19d0257ce773a1d63d02d8
3 files changed +206 -5
Documentation/git-reflog.adoc
+19 -4
@@ -16,6 +16,7 @@ SYNOPSIS
16 [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]
17 'git reflog delete' [--rewrite] [--updateref]
18 [--dry-run | -n] [--verbose] <ref>@{<specifier>}...
19 +'git reflog drop' [--all [--single-worktree] | <refs>...]
20 'git reflog exists' <ref>
21
22 DESCRIPTION
@@ -48,10 +49,14 @@ and not reachable from the current tip, are removed from the reflog.
49 This is typically not used directly by end users -- instead, see
50 linkgit:git-gc[1].
51
51 -The "delete" subcommand deletes single entries from the reflog. Its
52 -argument must be an _exact_ entry (e.g. "`git reflog delete
53 -master@{2}`"). This subcommand is also typically not used directly by
54 -end users.
52 +The "delete" subcommand deletes single entries from the reflog, but
53 +not the reflog itself. Its argument must be an _exact_ entry (e.g. "`git
54 +reflog delete master@{2}`"). This subcommand is also typically not used
55 +directly by end users.
56 +
57 +The "drop" subcommand completely removes the reflog for the specified
58 +references. This is in contrast to "expire" and "delete", both of which
59 +can be used to delete reflog entries, but not the reflog itself.
60
61 The "exists" subcommand checks whether a ref has a reflog. It exits
62 with zero status if the reflog exists, and non-zero status if it does
@@ -132,6 +137,16 @@ Options for `delete`
137 `--dry-run`, and `--verbose`, with the same meanings as when they are
138 used with `expire`.
139
140 +Options for `drop`
141 +~~~~~~~~~~~~~~~~~~~~
142 +
143 +--all::
144 + Drop the reflogs of all references from all worktrees.
145 +
146 +--single-worktree::
147 + By default when `--all` is specified, reflogs from all working
148 + trees are dropped. This option limits the processing to reflogs
149 + from the current working tree only.
150
151 GIT
152 ---
builtin/reflog.c
+65 -1
@@ -29,6 +29,9 @@
29 #define BUILTIN_REFLOG_EXISTS_USAGE \
30 N_("git reflog exists <ref>")
31
32 +#define BUILTIN_REFLOG_DROP_USAGE \
33 + N_("git reflog drop [--all [--single-worktree] | <refs>...]")
34 +
35 static const char *const reflog_show_usage[] = {
36 BUILTIN_REFLOG_SHOW_USAGE,
37 NULL,
@@ -54,11 +57,17 @@ static const char *const reflog_exists_usage[] = {
57 NULL,
58 };
59
60 +static const char *const reflog_drop_usage[] = {
61 + BUILTIN_REFLOG_DROP_USAGE,
62 + NULL,
63 +};
64 +
65 static const char *const reflog_usage[] = {
66 BUILTIN_REFLOG_SHOW_USAGE,
67 BUILTIN_REFLOG_LIST_USAGE,
68 BUILTIN_REFLOG_EXPIRE_USAGE,
69 BUILTIN_REFLOG_DELETE_USAGE,
70 + BUILTIN_REFLOG_DROP_USAGE,
71 BUILTIN_REFLOG_EXISTS_USAGE,
72 NULL
73 };
@@ -449,10 +458,64 @@ static int cmd_reflog_exists(int argc, const char **argv, const char *prefix,
458 refname);
459 }
460
461 +static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,
462 + struct repository *repo)
463 +{
464 + int ret = 0, do_all = 0, single_worktree = 0;
465 + const struct option options[] = {
466 + OPT_BOOL(0, "all", &do_all, N_("drop the reflogs of all references")),
467 + OPT_BOOL(0, "single-worktree", &single_worktree,
468 + N_("drop reflogs from the current worktree only")),
469 + OPT_END()
470 + };
471 +
472 + argc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);
473 +
474 + if (argc && do_all)
475 + usage(_("references specified along with --all"));
476 +
477 + if (do_all) {
478 + struct worktree_reflogs collected = {
479 + .reflogs = STRING_LIST_INIT_DUP,
480 + };
481 + struct string_list_item *item;
482 + struct worktree **worktrees, **p;
483 +
484 + worktrees = get_worktrees();
485 + for (p = worktrees; *p; p++) {
486 + if (single_worktree && !(*p)->is_current)
487 + continue;
488 + collected.worktree = *p;
489 + refs_for_each_reflog(get_worktree_ref_store(*p),
490 + collect_reflog, &collected);
491 + }
492 + free_worktrees(worktrees);
493 +
494 + for_each_string_list_item(item, &collected.reflogs)
495 + ret |= refs_delete_reflog(get_main_ref_store(repo),
496 + item->string);
497 + string_list_clear(&collected.reflogs, 0);
498 +
499 + return ret;
500 + }
501 +
502 + for (int i = 0; i < argc; i++) {
503 + char *ref;
504 + if (!repo_dwim_log(repo, argv[i], strlen(argv[i]), NULL, &ref)) {
505 + ret |= error(_("reflog could not be found: '%s'"), argv[i]);
506 + continue;
507 + }
508 +
509 + ret |= refs_delete_reflog(get_main_ref_store(repo), ref);
510 + free(ref);
511 + }
512 +
513 + return ret;
514 +}
515 +
516 /*
517 * main "reflog"
518 */
455 -
519 int cmd_reflog(int argc,
520 const char **argv,
521 const char *prefix,
@@ -465,6 +528,7 @@ int cmd_reflog(int argc,
528 OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire),
529 OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete),
530 OPT_SUBCOMMAND("exists", &fn, cmd_reflog_exists),
531 + OPT_SUBCOMMAND("drop", &fn, cmd_reflog_drop),
532 OPT_END()
533 };
534
t/t1410-reflog.sh
+122
@@ -551,4 +551,126 @@ test_expect_success 'reflog with invalid object ID can be listed' '
551 )
552 '
553
554 +test_expect_success 'reflog drop non-existent ref' '
555 + test_when_finished "rm -rf repo" &&
556 + git init repo &&
557 + (
558 + cd repo &&
559 + test_must_fail git reflog exists refs/heads/non-existent &&
560 + test_must_fail git reflog drop refs/heads/non-existent 2>stderr &&
561 + test_grep "error: reflog could not be found: ${SQ}refs/heads/non-existent${SQ}" stderr
562 + )
563 +'
564 +
565 +test_expect_success 'reflog drop' '
566 + test_when_finished "rm -rf repo" &&
567 + git init repo &&
568 + (
569 + cd repo &&
570 + test_commit A &&
571 + test_commit_bulk --ref=refs/heads/branch 1 &&
572 + git reflog exists refs/heads/main &&
573 + git reflog exists refs/heads/branch &&
574 + git reflog drop refs/heads/main &&
575 + test_must_fail git reflog exists refs/heads/main &&
576 + git reflog exists refs/heads/branch
577 + )
578 +'
579 +
580 +test_expect_success 'reflog drop multiple references' '
581 + test_when_finished "rm -rf repo" &&
582 + git init repo &&
583 + (
584 + cd repo &&
585 + test_commit A &&
586 + test_commit_bulk --ref=refs/heads/branch 1 &&
587 + git reflog exists refs/heads/main &&
588 + git reflog exists refs/heads/branch &&
589 + git reflog drop refs/heads/main refs/heads/branch &&
590 + test_must_fail git reflog exists refs/heads/main &&
591 + test_must_fail git reflog exists refs/heads/branch
592 + )
593 +'
594 +
595 +test_expect_success 'reflog drop multiple references some non-existent' '
596 + test_when_finished "rm -rf repo" &&
597 + git init repo &&
598 + (
599 + cd repo &&
600 + test_commit A &&
601 + test_commit_bulk --ref=refs/heads/branch 1 &&
602 + git reflog exists refs/heads/main &&
603 + git reflog exists refs/heads/branch &&
604 + test_must_fail git reflog exists refs/heads/non-existent &&
605 + test_must_fail git reflog drop refs/heads/main refs/heads/non-existent refs/heads/branch 2>stderr &&
606 + test_must_fail git reflog exists refs/heads/main &&
607 + test_must_fail git reflog exists refs/heads/branch &&
608 + test_must_fail git reflog exists refs/heads/non-existent &&
609 + test_grep "error: reflog could not be found: ${SQ}refs/heads/non-existent${SQ}" stderr
610 + )
611 +'
612 +
613 +test_expect_success 'reflog drop --all' '
614 + test_when_finished "rm -rf repo" &&
615 + git init repo &&
616 + (
617 + cd repo &&
618 + test_commit A &&
619 + test_commit_bulk --ref=refs/heads/branch 1 &&
620 + git reflog exists refs/heads/main &&
621 + git reflog exists refs/heads/branch &&
622 + git reflog drop --all &&
623 + test_must_fail git reflog exists refs/heads/main &&
624 + test_must_fail git reflog exists refs/heads/branch
625 + )
626 +'
627 +
628 +test_expect_success 'reflog drop --all multiple worktrees' '
629 + test_when_finished "rm -rf repo" &&
630 + test_when_finished "rm -rf wt" &&
631 + git init repo &&
632 + (
633 + cd repo &&
634 + test_commit A &&
635 + git worktree add ../wt &&
636 + test_commit_bulk -C ../wt --ref=refs/heads/branch 1 &&
637 + git reflog exists refs/heads/main &&
638 + git reflog exists refs/heads/branch &&
639 + git reflog drop --all &&
640 + test_must_fail git reflog exists refs/heads/main &&
641 + test_must_fail git reflog exists refs/heads/branch
642 + )
643 +'
644 +
645 +test_expect_success 'reflog drop --all --single-worktree' '
646 + test_when_finished "rm -rf repo" &&
647 + test_when_finished "rm -rf wt" &&
648 + git init repo &&
649 + (
650 + cd repo &&
651 + test_commit A &&
652 + git worktree add ../wt &&
653 + test_commit -C ../wt foobar &&
654 + git reflog exists refs/heads/main &&
655 + git reflog exists refs/heads/wt &&
656 + test-tool ref-store worktree:wt reflog-exists HEAD &&
657 + git reflog drop --all --single-worktree &&
658 + test_must_fail git reflog exists refs/heads/main &&
659 + test_must_fail git reflog exists refs/heads/wt &&
660 + test_must_fail test-tool ref-store worktree:main reflog-exists HEAD &&
661 + test-tool ref-store worktree:wt reflog-exists HEAD
662 + )
663 +'
664 +
665 +test_expect_success 'reflog drop --all with reference' '
666 + test_when_finished "rm -rf repo" &&
667 + git init repo &&
668 + (
669 + cd repo &&
670 + test_commit A &&
671 + test_must_fail git reflog drop --all refs/heads/main 2>stderr &&
672 + test_grep "usage: references specified along with --all" stderr
673 + )
674 +'
675 +
676 test_done