diff --git a/dvc/commands/gc.py b/dvc/commands/gc.py index 46250c2e2..1350b9362 100644 --- a/dvc/commands/gc.py +++ b/dvc/commands/gc.py @@ -81,6 +81,7 @@ class CmdGC(CmdBase): rev=self.args.rev, num=self.args.num, not_in_remote=self.args.not_in_remote, + dry=self.args.dry, ) return 0 @@ -213,4 +214,10 @@ def add_parser(subparsers, parent_parser): ), metavar="", ) + gc_parser.add_argument( + "--dry", + action="store_true", + default=False, + help=("Only print what would get removed without actually removing."), + ) gc_parser.set_defaults(func=CmdGC) diff --git a/dvc/repo/gc.py b/dvc/repo/gc.py index 1527af579..63286737e 100644 --- a/dvc/repo/gc.py +++ b/dvc/repo/gc.py @@ -67,6 +67,7 @@ def gc( # noqa: PLR0913, C901 rev: Optional[str] = None, num: Optional[int] = None, not_in_remote: bool = False, + dry: bool = False, ): # require `workspace` to be true to come into effect. # assume `workspace` to be enabled if any of `all_tags`, `all_commits`, @@ -128,8 +129,10 @@ def gc( # noqa: PLR0913, C901 for scheme, odb in self.cache.by_scheme(): if not odb: continue - removed = ogc(odb, used_obj_ids, jobs=jobs) - if not removed: + num_removed = ogc(odb, used_obj_ids, jobs=jobs, dry=dry) + if num_removed: + logger.info("Removed %d objects from %s cache.", num_removed, scheme) + else: logger.info("No unused '%s' cache to remove.", scheme) if not cloud: @@ -137,9 +140,10 @@ def gc( # noqa: PLR0913, C901 for remote_odb, obj_ids in odb_to_obj_ids.items(): assert remote_odb is not None - removed = ogc(remote_odb, obj_ids, jobs=jobs) - if removed: + num_removed = ogc(remote_odb, obj_ids, jobs=jobs, dry=dry) + if num_removed: get_index(remote_odb).clear() + logger.info("Removed %d objects from remote.", num_removed) else: logger.info("No unused cache to remove from remote.") diff --git a/tests/func/test_gc.py b/tests/func/test_gc.py index a7e96b91d..f1e28465a 100644 --- a/tests/func/test_gc.py +++ b/tests/func/test_gc.py @@ -36,14 +36,14 @@ def test_gc_api(dvc, good_and_bad_cache): dvc.gc(workspace=True) odb = dvc.cache.local good_cache, bad_cache = good_and_bad_cache - assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache + assert set(odb.all()) == good_cache def test_gc_cli(dvc, good_and_bad_cache): assert main(["gc", "-wf"]) == 0 odb = dvc.cache.local good_cache, bad_cache = good_and_bad_cache - assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache + assert set(odb.all()) == good_cache def test_gc_branches_tags(tmp_dir, dvc, scm): @@ -423,3 +423,19 @@ def test_gc_cloud_remote_field(tmp_dir, scm, dvc, mocker, make_remote): mocked_remove = mocker.spy(LocalFileSystem, "remove") dvc.gc(workspace=True, cloud=True) assert len(mocked_remove.mock_calls) == 2 # local and other_remote + + +def test_gc_dry(dvc, good_and_bad_cache): + dvc.gc(workspace=True, dry=True) + odb = dvc.cache.local + good_cache, bad_cache = good_and_bad_cache + assert set(odb.all()) != good_cache + + +def test_gc_logging(caplog, dvc, good_and_bad_cache): + with caplog.at_level(logging.INFO, logger="dvc"): + dvc.gc(workspace=True) + + assert "Removed 3 objects from repo cache." in caplog.text + assert "No unused 'local' cache to remove." in caplog.text + assert "No unused 'legacy' cache to remove." in caplog.text diff --git a/tests/unit/command/test_gc.py b/tests/unit/command/test_gc.py index 34c75acf7..5df832767 100644 --- a/tests/unit/command/test_gc.py +++ b/tests/unit/command/test_gc.py @@ -22,6 +22,7 @@ def test_(dvc, scm, mocker): "--force", "--jobs", "3", + "--dry", "--projects", "project1", "project2", @@ -49,6 +50,7 @@ def test_(dvc, scm, mocker): rev=None, num=None, not_in_remote=False, + dry=True, ) cli_args = parse_args(["gc"])