checkout: friendlier UI when tryint to remove uncached files

Related #9618
This commit is contained in:
Ruslan Kuprieiev
2023-06-20 21:09:00 +03:00
parent 8323923ce9
commit 621fe07a3b
3 changed files with 43 additions and 11 deletions
+41 -5
View File
@@ -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,
+1 -1
View File
@@ -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",
+1 -5
View File
@@ -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()