diff mbox series

[v2] reflog: implement subcommand to drop reflogs

Message ID 20250310-493-add-command-to-purge-reflog-entries-v2-1-05caa92e0bfa@gmail.com (mailing list archive)
State New
Headers show
Series [v2] reflog: implement subcommand to drop reflogs | expand

Commit Message

Karthik Nayak March 10, 2025, 12:36 p.m. UTC
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 a '--all' flag to
enable dropping all reflogs in a repository.

While here, remove an extraneous newline in the file.

Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
Changes in v2:
- Rephrase the commit message to be clearer and fix typo.
- Move the documentation to be next to 'git reflog delete' and also
  add missing documentation for the '--all' flag.
- Ensure '--all' is not used with references and add a test.
- Cleanup variable assignment.
- Check for error message in the test.
- Drop the cleanup commit.
- Rebased on top of master a36e024e98 (Merge branch 'js/win-2.49-build-fixes',
  2025-03-06), this was to include the adoc changes which were breaking
  tests on the CI. 
- Link to v1: https://lore.kernel.org/r/20250307-493-add-command-to-purge-reflog-entries-v1-0-84ab8529cf9e@gmail.com

 Documentation/git-reflog.adoc | 18 +++++++++---
 builtin/reflog.c              | 60 +++++++++++++++++++++++++++++++++++++-
 t/t1410-reflog.sh             | 67 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 140 insertions(+), 5 deletions(-)

Karthik Nayak (1):
      reflog: implement subcommand to drop reflogs

Range-diff versus v1:

1:  7c37f97eb0 < -:  ---------- reflog: drop usage of global variables
2:  d321bf14ae ! 1:  cac494d5cc reflog: implement subcommand to drop reflogs
    @@ Metadata
      ## Commit message ##
         reflog: implement subcommand to drop reflogs
     
    -    Add a new 'drop' subcommand to git-reflog that allows users to delete
    -    the entire reflog for a specified reference. Include a '--all' flag to
    -    enable dropping all reflogs in a repository.
    -
         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'.
     
    -    While here, remove an erranous newline in the file.
    +    Add a new 'drop' subcommand to git-reflog that allows users to delete
    +    the entire reflog for a specified reference. Include a '--all' flag to
    +    enable dropping all reflogs in a repository.
    +
    +    While here, remove an extraneous newline in the file.
     
         Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
     
      ## Documentation/git-reflog.adoc ##
     @@ Documentation/git-reflog.adoc: SYNOPSIS
    + 	[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]
      'git reflog delete' [--rewrite] [--updateref]
      	[--dry-run | -n] [--verbose] <ref>@{<specifier>}...
    - 'git reflog exists' <ref>
     +'git reflog drop' [--all | <refs>...]
    + 'git reflog exists' <ref>
      
      DESCRIPTION
    - -----------
    -@@ Documentation/git-reflog.adoc: The "exists" subcommand checks whether a ref has a reflog.  It exits
    +@@ Documentation/git-reflog.adoc: and not reachable from the current tip, are removed from the reflog.
    + This is typically not used directly by end users -- instead, see
    + linkgit:git-gc[1].
    + 
    +-The "delete" subcommand deletes single entries from the reflog. Its
    +-argument must be an _exact_ entry (e.g. "`git reflog delete
    +-master@{2}`"). This subcommand is also typically not used directly by
    +-end users.
    ++The "delete" subcommand deletes single entries from the reflog, but
    ++not the reflog itself. Its argument must be an _exact_ entry (e.g. "`git
    ++reflog delete master@{2}`"). This subcommand is also typically not used
    ++directly by end users.
    + 
    + The "exists" subcommand checks whether a ref has a reflog.  It exits
      with zero status if the reflog exists, and non-zero status if it does
      not.
      
    -+The "drop" subcommand removes the reflog for the specified references.
    -+In contrast, "expire" can be used to prune all entries from a reflog,
    -+but the reflog itself will still exist for that reference. To fully
    -+remove the reflog for specific references, use the "drop" subcommand.
    ++The "drop" subcommand completely removes the reflog for the specified
    ++references. This is in contrast to "expire" and "delete", both of which
    ++can be used to delete reflog entries, but not the reflog itself.
     +
      OPTIONS
      -------
      
    +@@ Documentation/git-reflog.adoc: Options for `delete`
    + `--dry-run`, and `--verbose`, with the same meanings as when they are
    + used with `expire`.
    + 
    ++Options for `drop`
    ++~~~~~~~~~~~~~~~~~~~~
    ++
    ++--all::
    ++	Drop the reflogs of all references from all worktrees.
    + 
    + GIT
    + ---
     
      ## builtin/reflog.c ##
     @@
    @@ builtin/reflog.c: static const char *const reflog_exists_usage[] = {
      	BUILTIN_REFLOG_LIST_USAGE,
      	BUILTIN_REFLOG_EXPIRE_USAGE,
      	BUILTIN_REFLOG_DELETE_USAGE,
    - 	BUILTIN_REFLOG_EXISTS_USAGE,
     +	BUILTIN_REFLOG_DROP_USAGE,
    + 	BUILTIN_REFLOG_EXISTS_USAGE,
      	NULL
      };
    - 
     @@ builtin/reflog.c: static int cmd_reflog_exists(int argc, const char **argv, const char *prefix,
      				   refname);
      }
    @@ builtin/reflog.c: static int cmd_reflog_exists(int argc, const char **argv, cons
     +static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,
     +			   struct repository *repo)
     +{
    -+	int i, ret, do_all;
    ++	int ret = 0, do_all = 0;
     +	const struct option options[] = {
     +		OPT_BOOL(0, "all", &do_all, N_("process the reflogs of all references")),
     +		OPT_END()
     +	};
     +
    -+	do_all = ret = 0;
     +	argc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);
     +
    ++	if (argc && do_all)
    ++		die(_("references specified along with --all"));
    ++
     +	if (do_all) {
     +		struct worktree_reflogs collected = {
     +			.reflogs = STRING_LIST_INIT_DUP,
    @@ builtin/reflog.c: static int cmd_reflog_exists(int argc, const char **argv, cons
     +		string_list_clear(&collected.reflogs, 0);
     +	}
     +
    -+	for (i = 0; i < argc; i++) {
    ++	for (int i = 0; i < argc; i++) {
     +		char *ref;
     +		if (!repo_dwim_log(repo, argv[i], strlen(argv[i]), NULL, &ref)) {
     +			ret |= error(_("%s points nowhere!"), argv[i]);
    @@ t/t1410-reflog.sh: test_expect_success 'reflog with invalid object ID can be lis
     +	(
     +		cd repo &&
     +		test_must_fail git reflog exists refs/heads/non-existent &&
    -+		test_must_fail git reflog drop refs/heads/non-existent
    ++		test_must_fail git reflog drop refs/heads/non-existent 2>stderr &&
    ++		test_grep "error: refs/heads/non-existent points nowhere!" stderr
     +	)
     +'
     +
    @@ t/t1410-reflog.sh: test_expect_success 'reflog with invalid object ID can be lis
     +		test_must_fail git reflog exists refs/heads/branch
     +	)
     +'
    ++
    ++test_expect_success 'reflog drop --all with reference' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++		test_commit A &&
    ++		test_must_fail git reflog drop --all refs/heads/main 2>stderr &&
    ++		test_grep "fatal: references specified along with --all" stderr
    ++	)
    ++'
     +
      test_done


base-commit: a36e024e989f4d35f35987a60e3af8022cac3420
change-id: 20250306-493-add-command-to-purge-reflog-entries-bd22547ad34a

Thanks
- Karthik
---

---
 Documentation/git-reflog.adoc | 18 +++++++++---
 builtin/reflog.c              | 60 +++++++++++++++++++++++++++++++++++++-
 t/t1410-reflog.sh             | 67 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 140 insertions(+), 5 deletions(-)
diff mbox series

Patch

diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc
index a929c52982..6ed98ddaef 100644
--- a/Documentation/git-reflog.adoc
+++ b/Documentation/git-reflog.adoc
@@ -16,6 +16,7 @@  SYNOPSIS
 	[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]
 'git reflog delete' [--rewrite] [--updateref]
 	[--dry-run | -n] [--verbose] <ref>@{<specifier>}...
+'git reflog drop' [--all | <refs>...]
 'git reflog exists' <ref>
 
 DESCRIPTION
@@ -48,15 +49,19 @@  and not reachable from the current tip, are removed from the reflog.
 This is typically not used directly by end users -- instead, see
 linkgit:git-gc[1].
 
-The "delete" subcommand deletes single entries from the reflog. Its
-argument must be an _exact_ entry (e.g. "`git reflog delete
-master@{2}`"). This subcommand is also typically not used directly by
-end users.
+The "delete" subcommand deletes single entries from the reflog, but
+not the reflog itself. Its argument must be an _exact_ entry (e.g. "`git
+reflog delete master@{2}`"). This subcommand is also typically not used
+directly by end users.
 
 The "exists" subcommand checks whether a ref has a reflog.  It exits
 with zero status if the reflog exists, and non-zero status if it does
 not.
 
+The "drop" subcommand completely removes the reflog for the specified
+references. This is in contrast to "expire" and "delete", both of which
+can be used to delete reflog entries, but not the reflog itself.
+
 OPTIONS
 -------
 
@@ -132,6 +137,11 @@  Options for `delete`
 `--dry-run`, and `--verbose`, with the same meanings as when they are
 used with `expire`.
 
+Options for `drop`
+~~~~~~~~~~~~~~~~~~~~
+
+--all::
+	Drop the reflogs of all references from all worktrees.
 
 GIT
 ---
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 95f264989b..cd93a0bef9 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -29,6 +29,9 @@ 
 #define BUILTIN_REFLOG_EXISTS_USAGE \
 	N_("git reflog exists <ref>")
 
+#define BUILTIN_REFLOG_DROP_USAGE \
+	N_("git reflog drop [--all | <refs>...]")
+
 static const char *const reflog_show_usage[] = {
 	BUILTIN_REFLOG_SHOW_USAGE,
 	NULL,
@@ -54,11 +57,17 @@  static const char *const reflog_exists_usage[] = {
 	NULL,
 };
 
+static const char *const reflog_drop_usage[] = {
+	BUILTIN_REFLOG_DROP_USAGE,
+	NULL,
+};
+
 static const char *const reflog_usage[] = {
 	BUILTIN_REFLOG_SHOW_USAGE,
 	BUILTIN_REFLOG_LIST_USAGE,
 	BUILTIN_REFLOG_EXPIRE_USAGE,
 	BUILTIN_REFLOG_DELETE_USAGE,
+	BUILTIN_REFLOG_DROP_USAGE,
 	BUILTIN_REFLOG_EXISTS_USAGE,
 	NULL
 };
@@ -449,10 +458,58 @@  static int cmd_reflog_exists(int argc, const char **argv, const char *prefix,
 				   refname);
 }
 
+static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,
+			   struct repository *repo)
+{
+	int ret = 0, do_all = 0;
+	const struct option options[] = {
+		OPT_BOOL(0, "all", &do_all, N_("process the reflogs of all references")),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0);
+
+	if (argc && do_all)
+		die(_("references specified along with --all"));
+
+	if (do_all) {
+		struct worktree_reflogs collected = {
+			.reflogs = STRING_LIST_INIT_DUP,
+		};
+		struct string_list_item *item;
+		struct worktree **worktrees, **p;
+
+		worktrees = get_worktrees();
+		for (p = worktrees; *p; p++) {
+			collected.worktree = *p;
+			refs_for_each_reflog(get_worktree_ref_store(*p),
+					     collect_reflog, &collected);
+		}
+		free_worktrees(worktrees);
+
+		for_each_string_list_item(item, &collected.reflogs)
+			ret |= refs_delete_reflog(get_main_ref_store(repo),
+						     item->string);
+		string_list_clear(&collected.reflogs, 0);
+	}
+
+	for (int i = 0; i < argc; i++) {
+		char *ref;
+		if (!repo_dwim_log(repo, argv[i], strlen(argv[i]), NULL, &ref)) {
+			ret |= error(_("%s points nowhere!"), argv[i]);
+			continue;
+		}
+
+		ret |= refs_delete_reflog(get_main_ref_store(repo), ref);
+		free(ref);
+	}
+
+	return ret;
+}
+
 /*
  * main "reflog"
  */
-
 int cmd_reflog(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -465,6 +522,7 @@  int cmd_reflog(int argc,
 		OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire),
 		OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete),
 		OPT_SUBCOMMAND("exists", &fn, cmd_reflog_exists),
+		OPT_SUBCOMMAND("drop", &fn, cmd_reflog_drop),
 		OPT_END()
 	};
 
diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh
index 388fdf9ae5..251caaf9a4 100755
--- a/t/t1410-reflog.sh
+++ b/t/t1410-reflog.sh
@@ -551,4 +551,71 @@  test_expect_success 'reflog with invalid object ID can be listed' '
 	)
 '
 
+test_expect_success 'reflog drop non-existent ref' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_must_fail git reflog exists refs/heads/non-existent &&
+		test_must_fail git reflog drop refs/heads/non-existent 2>stderr &&
+		test_grep "error: refs/heads/non-existent points nowhere!" stderr
+	)
+'
+
+test_expect_success 'reflog drop' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit A &&
+		test_commit_bulk --ref=refs/heads/branch 1 &&
+		git reflog exists refs/heads/main &&
+		git reflog exists refs/heads/branch &&
+		git reflog drop refs/heads/main &&
+		test_must_fail git reflog exists refs/heads/main &&
+		git reflog exists refs/heads/branch
+	)
+'
+
+test_expect_success 'reflog drop multiple references' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit A &&
+		test_commit_bulk --ref=refs/heads/branch 1 &&
+		git reflog exists refs/heads/main &&
+		git reflog exists refs/heads/branch &&
+		git reflog drop refs/heads/main refs/heads/branch &&
+		test_must_fail git reflog exists refs/heads/main &&
+		test_must_fail git reflog exists refs/heads/branch
+	)
+'
+
+test_expect_success 'reflog drop --all' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit A &&
+		test_commit_bulk --ref=refs/heads/branch 1 &&
+		git reflog exists refs/heads/main &&
+		git reflog exists refs/heads/branch &&
+		git reflog drop --all &&
+		test_must_fail git reflog exists refs/heads/main &&
+		test_must_fail git reflog exists refs/heads/branch
+	)
+'
+
+test_expect_success 'reflog drop --all with reference' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit A &&
+		test_must_fail git reflog drop --all refs/heads/main 2>stderr &&
+		test_grep "fatal: references specified along with --all" stderr
+	)
+'
+
 test_done