Repository: iterative/dvc

Issue:
move: stage file doesn't change when data has a suffix in the filename
```
echo foo > foo
dvc add foo
dvc move foo bar
# removes foo.dvc and creates bar.dvc

echo foo > foo.csv
dvc add foo.csv
dvc move foo.csv bar.csv
# doesn't create bar.csv.dvc and leaves foo.csv.dvc
```

Discussion:

Patch:
diff --git a/dvc/project.py b/dvc/project.py
index a3398680a..c9242551f 100644
--- a/dvc/project.py
+++ b/dvc/project.py
@@ -261,48 +261,60 @@ 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 = []
-        with self.state:
-            for stage in self.stages():
-                for out in stage.outs:
-                    if out.path != from_out.path:
-                        continue
+        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)
+        if not stage.is_data_source:
+            raise MoveNotDataSourceError(stage.relpath)
 
-                    found = True
-                    to_out = Output.loads_from(out.stage,
-                                               [to_path],
-                                               out.cache,
-                                               out.metric)[0]
-                    out.move(to_out)
+        to_out = Output.loads_from(stage, [to_path], out.cache, out.metric)[0]
 
-                    stage_base = os.path.basename(stage.path)
-                    stage_base = stage_base.rstrip(Stage.STAGE_FILE_SUFFIX)
+        with self.state:
+            out.move(to_out)
 
-                    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_name = os.path.splitext(os.path.basename(stage.path))[0]
+        from_name = os.path.basename(from_out.path)
 
-                stage.dump()
+        if stage_name == from_name:
+            os.unlink(stage.path)
 
-        self._remind_to_git_add()
+            stage.path = os.path.join(
+                os.path.dirname(stage.path),
+                os.path.basename(to_path) + Stage.STAGE_FILE_SUFFIX
+            )
+
+        stage.dump()
 
-        if not found:
-            msg = "unable to find dvcfile with output '{}'"
-            raise DvcException(msg.format(from_path))
+        self._remind_to_git_add()
 
     def _unprotect_file(self, path):
         import stat


Test patch:
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'))