diff --git a/dvc/exceptions.py b/dvc/exceptions.py index aedd15a75..f4ac40ae4 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -31,6 +31,18 @@ class OutputDuplicationError(DvcException): super(OutputDuplicationError, self).__init__(msg.format(output, s)) +class WorkingDirectoryAsOutputError(DvcException): + def __init__(self, cwd, fname): + assert isinstance(cwd, str) + assert isinstance(fname, str) + msg = ( + "Current working directory '{cwd}' is specified as an output in" + " '{fname}'. Use another CWD to prevent any data removal." + .format(cwd=cwd, fname=fname) + ) + super(WorkingDirectoryAsOutputError, self).__init__(msg) + + class CircularDependencyError(DvcException): def __init__(self, dependency): assert isinstance(dependency, str) diff --git a/dvc/project.py b/dvc/project.py index c0b3a00f4..2d965f656 100644 --- a/dvc/project.py +++ b/dvc/project.py @@ -165,10 +165,20 @@ class Project(object): def to_dvc_path(self, path): return os.path.relpath(path, self.root_dir) - def _check_output_duplication(self, outs): + def _check_cwd_specified_as_output(self, cwd, stages): + from dvc.exceptions import WorkingDirectoryAsOutputError + + cwd_path = os.path.abspath(os.path.normpath(cwd)) + + for stage in stages: + for output in stage.outs: + if os.path.isdir(output.path) and output.path == cwd_path: + raise WorkingDirectoryAsOutputError(cwd, stage.relpath) + + def _check_output_duplication(self, outs, stages): from dvc.exceptions import OutputDuplicationError - for stage in self.stages(): + for stage in stages: for o in stage.outs: for out in outs: if o.path == out.path and o.stage.path != out.stage.path: @@ -203,6 +213,7 @@ class Project(object): else: fnames = [fname] + all_stages = self.stages() stages = [] self._files_to_git_add = [] with self.state: @@ -211,7 +222,7 @@ class Project(object): outs=[f], add=True) - self._check_output_duplication(stage.outs) + self._check_output_duplication(stage.outs, all_stages) stage.save() stage.dump() @@ -332,7 +343,10 @@ class Project(object): deps=deps, overwrite=overwrite) - self._check_output_duplication(stage.outs) + all_stages = self.stages() + + self._check_cwd_specified_as_output(cwd, all_stages) + self._check_output_duplication(stage.outs, all_stages) self._check_circular_dependency(stage.deps, stage.outs) self._files_to_git_add = [] @@ -352,7 +366,8 @@ class Project(object): deps=[url], outs=[out]) - self._check_output_duplication(stage.outs) + self._check_output_duplication(stage.outs, self.stages()) + self._check_circular_dependency(stage.deps, stage.outs) self._files_to_git_add = [] with self.state: @@ -1116,6 +1131,14 @@ class Project(object): return pipelines def stages(self): + """ + Walks down the root directory looking for Dvcfiles, + skipping the directories that are related with + any SCM (e.g. `.git`) or DVC itself (`.dvc`). + + NOTE: For large projects, this could be an expensive + operation. Consider using some memorization. + """ stages = [] outs = [] for root, dirs, files in os.walk(self.root_dir): diff --git a/tests/test_run.py b/tests/test_run.py index ac1eed326..32ac4c33d 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -6,7 +6,9 @@ from dvc.utils import file_md5 from dvc.stage import Stage, StageFileBadNameError, MissingDep from dvc.stage import StageBadCwdError from dvc.command.run import CmdRun -from dvc.exceptions import OutputDuplicationError, CircularDependencyError +from dvc.exceptions import (OutputDuplicationError, + CircularDependencyError, + WorkingDirectoryAsOutputError) from tests.basic_env import TestDvc @@ -117,6 +119,19 @@ class TestRunCircularDependency(TestDvc): fname='circular-dependency.dvc') +class TestRunWorkingDirectoryAsOutput(TestDvc): + def test(self): + self.dvc.run(cmd='', + deps=[], + outs=[self.DATA_DIR]) + + with self.assertRaises(WorkingDirectoryAsOutputError): + self.dvc.run(cmd='', + cwd=self.DATA_DIR, + outs=[self.FOO], + fname='inside-cwd.dvc') + + class TestRunBadCwd(TestDvc): def test(self): with self.assertRaises(StageBadCwdError):