From 32fb87b9986d35d1a9a9843d105cffda59e5453d Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 6 Mar 2025 15:54:35 +0100 Subject: [PATCH 1/3] 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 a '--all' flag to enable dropping all reflogs from all worktrees and an addon flag '--single-worktree', to drop all reflogs from the current worktree. The first patch is a small cleanup in 'git refs expire' which improves the error message used when there is no reflog present for a given reference. Changes in v3: - Add a preparatory commit to fix the error message in 'git reflog expire' when a non-existent ref is provided. - Add support for '--single-worktree' to provide feature parity with 'git reflog expire'. - Improved error message and small code fixes. - Added some additional tests. - Link to v2: https://lore.kernel.org/r/20250310-493-add-command-to-purge-reflog-entries-v2-1-05caa92e0bfa@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 --- b4-submit-tracking --- { "series": { "revision": 3, "change-id": "20250306-493-add-command-to-purge-reflog-entries-bd22547ad34a", "prefixes": [], "history": { "v1": [ "20250307-493-add-command-to-purge-reflog-entries-v1-0-84ab8529cf9e@gmail.com" ], "v2": [ "20250310-493-add-command-to-purge-reflog-entries-v2-1-05caa92e0bfa@gmail.com" ] } } } -- GitLab From d4c4bf8c99aa42e321f90860afbfc2a93c7cfec4 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 13 Mar 2025 15:26:16 +0100 Subject: [PATCH 2/3] reflog: improve error for when reflog is not found The 'git reflog expire' prints the error message ' points nowhere!' when used with a non-existent ref. This message is a bit confusing and vague. Modify the message to be more clear and direct. Signed-off-by: Karthik Nayak --- builtin/reflog.c | 2 +- t/t1410-reflog.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/reflog.c b/builtin/reflog.c index 95f264989bb..762719315e9 100644 --- a/builtin/reflog.c +++ b/builtin/reflog.c @@ -383,7 +383,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix, struct expire_reflog_policy_cb cb = { .cmd = cmd }; if (!repo_dwim_log(the_repository, argv[i], strlen(argv[i]), NULL, &ref)) { - status |= error(_("%s points nowhere!"), argv[i]); + status |= error(_("reflog could not be found: '%s'"), argv[i]); continue; } set_reflog_expiry_param(&cb.cmd, ref); diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh index 388fdf9ae57..1f7249be764 100755 --- a/t/t1410-reflog.sh +++ b/t/t1410-reflog.sh @@ -315,9 +315,9 @@ test_expect_success 'git reflog expire unknown reference' ' test_config gc.reflogexpireunreachable never && test_must_fail git reflog expire main@{123} 2>stderr && - test_grep "points nowhere" stderr && + test_grep "error: reflog could not be found: ${SQ}main@{123}${SQ}" stderr && test_must_fail git reflog expire does-not-exist 2>stderr && - test_grep "points nowhere" stderr + test_grep "error: reflog could not be found: ${SQ}does-not-exist${SQ}" stderr ' test_expect_success 'checkout should not delete log for packed ref' ' -- GitLab From 237ee7f96ea39ca7a413b7fd7acaba8433599aef Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Thu, 6 Mar 2025 17:39:37 +0100 Subject: [PATCH 3/3] 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 a '--all' flag to enable dropping all reflogs from all worktrees and an addon flag '--single-worktree', to drop all reflogs from the current worktree. While here, remove an extraneous newline in the file. Signed-off-by: Karthik Nayak --- Documentation/git-reflog.adoc | 23 +++++-- builtin/reflog.c | 66 +++++++++++++++++- t/t1410-reflog.sh | 122 ++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 5 deletions(-) diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc index a929c52982f..b55c0605697 100644 --- a/Documentation/git-reflog.adoc +++ b/Documentation/git-reflog.adoc @@ -16,6 +16,7 @@ SYNOPSIS [--dry-run | -n] [--verbose] [--all [--single-worktree] | ...] 'git reflog delete' [--rewrite] [--updateref] [--dry-run | -n] [--verbose] @{}... +'git reflog drop' [--all [--single-worktree] | ...] 'git reflog exists' DESCRIPTION @@ -48,10 +49,14 @@ 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 "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. 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 @@ -132,6 +137,16 @@ 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. + +--single-worktree:: + By default when `--all` is specified, reflogs from all working + trees are dropped. This option limits the processing to reflogs + from the current working tree only. GIT --- diff --git a/builtin/reflog.c b/builtin/reflog.c index 762719315e9..a3652e69f1b 100644 --- a/builtin/reflog.c +++ b/builtin/reflog.c @@ -29,6 +29,9 @@ #define BUILTIN_REFLOG_EXISTS_USAGE \ N_("git reflog exists ") +#define BUILTIN_REFLOG_DROP_USAGE \ + N_("git reflog drop [--all [--single-worktree] | ...]") + 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,64 @@ 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, single_worktree = 0; + const struct option options[] = { + OPT_BOOL(0, "all", &do_all, N_("drop the reflogs of all references")), + OPT_BOOL(0, "single-worktree", &single_worktree, + N_("drop reflogs from the current worktree only")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, reflog_drop_usage, 0); + + if (argc && do_all) + usage(_("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++) { + if (single_worktree && !(*p)->is_current) + continue; + 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); + + return ret; + } + + for (int i = 0; i < argc; i++) { + char *ref; + if (!repo_dwim_log(repo, argv[i], strlen(argv[i]), NULL, &ref)) { + ret |= error(_("reflog could not be found: '%s'"), 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 +528,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 1f7249be764..42b501f163f 100755 --- a/t/t1410-reflog.sh +++ b/t/t1410-reflog.sh @@ -551,4 +551,126 @@ 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: reflog could not be found: ${SQ}refs/heads/non-existent${SQ}" 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 multiple references some non-existent' ' + 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 && + test_must_fail git reflog exists refs/heads/non-existent && + test_must_fail git reflog drop refs/heads/main refs/heads/non-existent refs/heads/branch 2>stderr && + test_must_fail git reflog exists refs/heads/main && + test_must_fail git reflog exists refs/heads/branch && + test_must_fail git reflog exists refs/heads/non-existent && + test_grep "error: reflog could not be found: ${SQ}refs/heads/non-existent${SQ}" stderr + ) +' + +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 multiple worktrees' ' + test_when_finished "rm -rf repo" && + test_when_finished "rm -rf wt" && + git init repo && + ( + cd repo && + test_commit A && + git worktree add ../wt && + test_commit_bulk -C ../wt --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 --single-worktree' ' + test_when_finished "rm -rf repo" && + test_when_finished "rm -rf wt" && + git init repo && + ( + cd repo && + test_commit A && + git worktree add ../wt && + test_commit -C ../wt foobar && + git reflog exists refs/heads/main && + git reflog exists refs/heads/wt && + test-tool ref-store worktree:wt reflog-exists HEAD && + git reflog drop --all --single-worktree && + test_must_fail git reflog exists refs/heads/main && + test_must_fail git reflog exists refs/heads/wt && + test_must_fail test-tool ref-store worktree:main reflog-exists HEAD && + test-tool ref-store worktree:wt reflog-exists HEAD + ) +' + +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 "usage: references specified along with --all" stderr + ) +' + test_done -- GitLab