Repository: iterative/dvc

Issue:
Error with "dvc add" on Windows, when local cache is not on the same drive as git repo
DVC version 0.35.7
installed with pip
Running on Windows 10

I have the local dvc cache on drive F: which is a HD, due to limited space on drive C: (SSD), where I have my git repo.

When trying to add some data to DVC control with "dvc add Path\To\Data", where the path to data is a relative path on C: drive, the command exits with the following error.

`ERROR: unexpected error - path is on mount 'F:', start on mount 'C:'                                                                                                                                               ------------------------------------------------------------                                                                                                                                                       Traceback (most recent call last):                                                                                                                                                                                   File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\main.py", line 37, in main                                                                                                  ret = cmd.run_cmd()                                                                                                                                                                                              File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\command\base.py", line 60, in run_cmd                                                                                       return self.run()                                                                                                                                                                                                File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\command\add.py", line 24, in run                                                                                            fname=self.args.file,                                                                                                                                                                                            File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\repo\scm_context.py", line 4, in run                                                                                        result = method(repo, *args, **kw)                                                                                                                                                                               File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\repo\add.py", line 16, in add                                                                                               stages = _create_stages(repo, targets, fname, no_commit)                                                                                                                                                         File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\repo\add.py", line 51, in _create_stages                                                                                    stage.commit()                                                                                                                                                                                                   File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\stage.py", line 698, in commit                                                                                              out.commit()                                                                                                                                                                                                     File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\output\base.py", line 180, in commit                                                                                        self.path_info, self.info                                                                                                                                                                                        File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\site-packages\dvc\remote\local.py", line 457, in save                                                                                         msg.format(os.path.relpath(path), os.path.relpath(self.cache_dir))                                                                                                                                               File "c:\users\srg\appdata\local\continuum\anaconda3\envs\tf-keras\lib\ntpath.py", line 584, in relpath                                                                                                              path_drive, start_drive))                                                                                                                                                                                      ValueError: path is on mount 'F:', start on mount 'C:'       
`

As pointed out by Ruslan in a response to my support email:

> the issue is that dvc is trying to find a relpath between C: and F: paths, which doesn't exist on windows. It is a known issue with `os.relpath` [1] . Luckily, that relpath is only needed for a debug message in our case, so we should be able to find a workaround for it. 
> 
> [1] https://bugs.python.org/issue7195
> 

I did try the same thing on an Ubuntu 18.04 machine, with the local cache on a different drive than the git repo and the "dvc add" command ran without any issues.


Discussion:

Resolving patch:
diff --git a/dvc/path/local.py b/dvc/path/local.py
--- a/dvc/path/local.py
+++ b/dvc/path/local.py
@@ -8,4 +8,10 @@ class PathLOCAL(PathBASE):
     scheme = Schemes.LOCAL
 
     def __str__(self):
+        if os.name == "nt" and not os.path.commonprefix(
+            [os.getcwd(), self.path]
+        ):
+            # In case of windows, when cache is on different drive than
+            # workspace, we will get ValueError when trying to get relpath
+            return self.path
         return os.path.relpath(self.path)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -97,3 +97,36 @@ def ssh(ssh_server):
         port=ssh_server.port,
         key_filename=key_path,
     )
+
+
+@pytest.fixture
+def temporary_windows_drive(repo_dir):
+    import string
+    import win32api
+    from ctypes import windll
+    from win32con import DDD_REMOVE_DEFINITION
+
+    drives = [
+        s[0].upper()
+        for s in win32api.GetLogicalDriveStrings().split("\000")
+        if len(s) > 0
+    ]
+
+    new_drive_name = [
+        letter for letter in string.ascii_uppercase if letter not in drives
+    ][0]
+    new_drive = "{}:".format(new_drive_name)
+
+    target_path = repo_dir.mkdtemp()
+
+    set_up_result = windll.kernel32.DefineDosDeviceW(0, new_drive, target_path)
+    if set_up_result == 0:
+        raise RuntimeError("Failed to mount windows drive!")
+
+    yield new_drive
+
+    tear_down_result = windll.kernel32.DefineDosDeviceW(
+        DDD_REMOVE_DEFINITION, new_drive, target_path
+    )
+    if tear_down_result == 0:
+        raise RuntimeError("Could not unmount windows drive!")
diff --git a/tests/func/test_add.py b/tests/func/test_add.py
--- a/tests/func/test_add.py
+++ b/tests/func/test_add.py
@@ -2,6 +2,8 @@
 
 import os
 
+import pytest
+
 import dvc
 import time
 import shutil
@@ -541,3 +543,14 @@ def test(self):
 
         self.assertFalse(os.access(self.FOO, os.W_OK))
         self.assertTrue(System.is_hardlink(self.FOO))
+
+
+@pytest.mark.skipif(os.name != "nt", reason="Windows specific")
+def test_windows_should_add_when_cache_on_different_drive(
+    dvc, repo_dir, temporary_windows_drive
+):
+    ret = main(["config", "cache.dir", temporary_windows_drive])
+    assert ret == 0
+
+    ret = main(["add", repo_dir.DATA])
+    assert ret == 0