diff --git a/dvc/repo/move.py b/dvc/repo/move.py index c6654554d..41d81aba5 100644 --- a/dvc/repo/move.py +++ b/dvc/repo/move.py @@ -53,18 +53,22 @@ def move(self, from_path, to_path): stage_name = os.path.splitext(os.path.basename(stage.path))[0] from_name = os.path.basename(from_out.fspath) if stage_name == from_name: - os.unlink(stage.path) - - stage.path = os.path.join( + new_fname = os.path.join( os.path.dirname(to_path), os.path.basename(to_path) + DVC_FILE_SUFFIX, ) - - stage.wdir = os.path.abspath( + new_wdir = os.path.abspath( os.path.join(os.curdir, os.path.dirname(to_path)) ) + to_path = os.path.relpath(to_path, new_wdir) + new_stage = self.stage.create( + single_stage=True, fname=new_fname, wdir=new_wdir, outs=[to_path], + ) - to_path = os.path.relpath(to_path, stage.wdir) + os.unlink(stage.path) + stage = new_stage + else: + to_path = os.path.relpath(to_path, stage.wdir) to_out = Output.loads_from(stage, [to_path], out.use_cache, out.metric)[0] diff --git a/dvc/stage/utils.py b/dvc/stage/utils.py index 2c327b1a4..fbed95f28 100644 --- a/dvc/stage/utils.py +++ b/dvc/stage/utils.py @@ -330,7 +330,7 @@ def validate_state( def validate_kwargs(single_stage: bool = False, fname: str = None, **kwargs): """Prepare, validate and process kwargs passed from cli""" cmd = kwargs.get("cmd") - if not cmd: + if not cmd and not single_stage: raise InvalidArgumentError("command is not specified") stage_name = kwargs.get("name") diff --git a/tests/func/test_move.py b/tests/func/test_move.py index 1059a3e05..08c5b7037 100644 --- a/tests/func/test_move.py +++ b/tests/func/test_move.py @@ -1,5 +1,7 @@ import os +import pytest + from dvc.dvcfile import DVC_FILE_SUFFIX from dvc.exceptions import DvcException, MoveNotDataSourceError from dvc.main import main @@ -211,3 +213,34 @@ def test_should_move_to_dir_on_non_default_stage_file(tmp_dir, dvc): dvc.move("file", "directory") assert os.path.exists(os.path.join("directory", "file")) + + +def test_move_gitignored(tmp_dir, scm, dvc): + from dvc.dvcfile import FileIsGitIgnored + + tmp_dir.dvc_gen({"foo": "foo"}) + + os.mkdir("dir") + (tmp_dir / "dir").gen(".gitignore", "*") + + with pytest.raises(FileIsGitIgnored): + dvc.move("foo", "dir") + + assert (tmp_dir / "foo").read_text() == "foo" + assert (tmp_dir / "foo.dvc").exists() + assert not (tmp_dir / "dir" / "foo").exists() + assert not (tmp_dir / "dir" / "foo.dvc").exists() + + +def test_move_output_overlap(tmp_dir, dvc): + from dvc.exceptions import OverlappingOutputPathsError + + tmp_dir.dvc_gen({"foo": "foo", "dir": {"bar": "bar"}}) + + with pytest.raises(OverlappingOutputPathsError): + dvc.move("foo", "dir") + + assert (tmp_dir / "foo").read_text() == "foo" + assert (tmp_dir / "foo.dvc").exists() + assert not (tmp_dir / "dir" / "foo").exists() + assert not (tmp_dir / "dir" / "foo.dvc").exists()