diff --git a/dvc/repo/checkout.py b/dvc/repo/checkout.py index 0676f14bd..b8e8dc0aa 100644 --- a/dvc/repo/checkout.py +++ b/dvc/repo/checkout.py @@ -1,12 +1,21 @@ import logging import os -from typing import Dict, List +from typing import TYPE_CHECKING, Dict, List -from dvc.exceptions import CheckoutError, CheckoutErrorSuggestGit, NoOutputOrStageError +from dvc.exceptions import ( + CheckoutError, + CheckoutErrorSuggestGit, + DvcException, + NoOutputOrStageError, +) from dvc.utils import relpath from . import locked +if TYPE_CHECKING: + from dvc_data.index import BaseDataIndex, DataIndexEntry + from dvc_objects.fs.base import FileSystem + logger = logging.getLogger(__name__) @@ -59,6 +68,33 @@ def _build_out_changes(index, changes): return out_changes +def _check_can_delete( + entries: List["DataIndexEntry"], + index: "BaseDataIndex", + path: str, + fs: "FileSystem", +): + entry_paths = [] + for entry in entries: + try: + cache_fs, cache_path = index.storage_map.get_cache(entry) + except ValueError: + continue + + if cache_fs.exists(cache_path): + continue + + entry_paths.append(fs.path.join(path, *entry.key)) + + if not entry_paths: + return + + raise DvcException( + "Can't remove the following unsaved files without confirmation. " + "Use `--force` to force.\n" + "\n".join(entry_paths) + ) + + @locked def checkout( # noqa: C901 self, @@ -70,7 +106,6 @@ def checkout( # noqa: C901 allow_missing=False, **kwargs, ): - from dvc import prompt from dvc.fs.callbacks import Callback from dvc.repo.index import build_data_index from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError @@ -123,6 +158,9 @@ def checkout( # noqa: C901 ) as cb: diff = compare(old, new, relink=relink, delete=True, callback=cb) + if not force: + _check_can_delete(diff.files_delete, new, self.root_dir, self.fs) + failed = [] def checkout_onerror(src_path, dest_path, _exc): @@ -140,9 +178,7 @@ def checkout( # noqa: C901 self.root_dir, self.fs, callback=cb, - prompt=prompt.confirm, update_meta=False, - force=force, onerror=checkout_onerror, state=self.state, **kwargs, diff --git a/pyproject.toml b/pyproject.toml index 89bc945ec..3fda13f51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "configobj>=5.0.6", "distro>=1.3", "dpath<3,>=2.1.0", - "dvc-data>=2.1.1,<2.2.0", + "dvc-data>=2.2.0,<2.3.0", "dvc-http>=2.29.0", "dvc-render>=0.3.1,<1", "dvc-studio-client>=0.9.2,<1", diff --git a/tests/func/test_checkout.py b/tests/func/test_checkout.py index eda509ba6..5f7d202d8 100644 --- a/tests/func/test_checkout.py +++ b/tests/func/test_checkout.py @@ -84,23 +84,19 @@ def test_remove_files_when_checkout(tmp_dir, dvc, scm): class TestCheckoutCleanWorkingDir: def test(self, mocker, tmp_dir, dvc): - mock_prompt = mocker.patch("dvc.prompt.confirm", return_value=True) (stage,) = tmp_dir.dvc_gen("data", {"foo": "foo"}) # change working directory (tmp_dir / "data").gen("not_cached.txt", "not_cached") - assert main(["checkout", stage.relpath]) == 0 - mock_prompt.assert_called() + assert main(["checkout", stage.relpath, "--force"]) == 0 assert not (tmp_dir / "data" / "not_cached.txt").exists() def test_force(self, mocker, tmp_dir, dvc): - mock_prompt = mocker.patch("dvc.prompt.confirm", return_value=False) (stage,) = tmp_dir.dvc_gen("data", {"foo": "foo"}) # change working directory (tmp_dir / "data").gen("not_cached.txt", "not_cached") assert main(["checkout", stage.relpath]) != 0 - mock_prompt.assert_called() assert (tmp_dir / "data" / "not_cached.txt").exists()