diff --git a/dvc/project.py b/dvc/project.py index a3398680a..c9242551f 100644 --- a/dvc/project.py +++ b/dvc/project.py @@ -261,49 +261,61 @@ class Project(object): return stage def move(self, from_path, to_path): + """ + Renames an output file and modifies the stage associated + to reflect the change on the pipeline. + + If the output has the same name as its stage, it would + also rename the corresponding stage file. + + E.g. + Having: (hello, hello.dvc) + + $ dvc move hello greetings + + Result: (greeting, greeting.dvc) + + It only works with outputs generated by `add` or `import`, + also known as data sources. + """ import dvc.output as Output from dvc.stage import Stage from_out = Output.loads_from(Stage(self, cwd=os.curdir), [from_path])[0] - found = False - self._files_to_git_add = [] + try: + stage, out = next((stage, out) + for stage in self.stages() + for out in stage.outs + if from_out.path == out.path) + except StopIteration: + raise DvcException("unable to find stage file with output '{path}'" + .format(path=from_path)) + + if not stage.is_data_source: + raise MoveNotDataSourceError(stage.relpath) + + to_out = Output.loads_from(stage, [to_path], out.cache, out.metric)[0] + with self.state: - for stage in self.stages(): - for out in stage.outs: - if out.path != from_out.path: - continue + out.move(to_out) - if not stage.is_data_source: - raise MoveNotDataSourceError(stage.relpath) + stage_name = os.path.splitext(os.path.basename(stage.path))[0] + from_name = os.path.basename(from_out.path) - found = True - to_out = Output.loads_from(out.stage, - [to_path], - out.cache, - out.metric)[0] - out.move(to_out) + if stage_name == from_name: + os.unlink(stage.path) - stage_base = os.path.basename(stage.path) - stage_base = stage_base.rstrip(Stage.STAGE_FILE_SUFFIX) + stage.path = os.path.join( + os.path.dirname(stage.path), + os.path.basename(to_path) + Stage.STAGE_FILE_SUFFIX + ) - stage_dir = os.path.dirname(stage.path) - from_base = os.path.basename(from_path) - to_base = os.path.basename(to_path) - if stage_base == from_base: - os.unlink(stage.path) - path = to_base + Stage.STAGE_FILE_SUFFIX - stage.path = os.path.join(stage_dir, path) - - stage.dump() + stage.dump() self._remind_to_git_add() - if not found: - msg = "unable to find dvcfile with output '{}'" - raise DvcException(msg.format(from_path)) - def _unprotect_file(self, path): import stat import uuid diff --git a/tests/test_move.py b/tests/test_move.py index 9e719691b..ab43f623e 100644 --- a/tests/test_move.py +++ b/tests/test_move.py @@ -57,3 +57,22 @@ class TestMoveNotDataSource(TestRepro): ret = main(['move', self.file1, 'dst']) self.assertNotEqual(ret, 0) + + +class TestMoveFileWithExtension(TestDvc): + def test(self): + with open(os.path.join(self.dvc.root_dir, 'file.csv'), 'w') as fd: + fd.write('1,2,3\n') + + self.dvc.add('file.csv') + + self.assertTrue(os.path.exists('file.csv')) + self.assertTrue(os.path.exists('file.csv.dvc')) + + ret = main(['move', 'file.csv', 'other_name.csv']) + self.assertEqual(ret, 0) + + self.assertFalse(os.path.exists('file.csv')) + self.assertFalse(os.path.exists('file.csv.dvc')) + self.assertTrue(os.path.exists('other_name.csv')) + self.assertTrue(os.path.exists('other_name.csv.dvc'))