Repository: iterative/dvc

Issue:
add: fails for repo on some windows mapped network shares for python >= 3.8
# Bug Report

<!--
## Issue name

Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug. 

Example: `repro: doesn't detect input changes`
-->

## Description

dvc add fails for repositories located on a windows network share for python >= 3.8

### Reproduce

For a network shared drive mapped to m:

```console
M:\>mkdir repo-on-network-share
M:\>cd repo-on-network-share
M:\repo-on-network-share>echo this is a test file  1>data.txt
M:\repo-on-network-share>git init
Initialized empty Git repository in //mcealrpdevfilestorage.file.core.windows.net/mainfilesharedev/repo-on-network-share/.git/
M:\repo-on-network-share>dvc init
Initialized DVC repository.

You can now commit the changes to git.

+---------------------------------------------------------------------+
|                                                                     |
|        DVC has enabled anonymous aggregate usage analytics.         |
|     Read the analytics documentation (and how to opt-out) here:     |
|             <https://dvc.org/doc/user-guide/analytics>              |
|                                                                     |
+---------------------------------------------------------------------+

What's next?
------------
- Check out the documentation: <https://dvc.org/doc>
- Get help and share ideas: <https://dvc.org/chat>
- Star us on GitHub: <https://github.com/iterative/dvc>

M:\repo-on-network-share>dvc add -v data.txt
2021-04-16 10:49:40,314 DEBUG: Check for update is enabled.
2021-04-16 10:49:40,689 DEBUG: Trying to spawn '['daemon', '-q', 'updater']'
2021-04-16 10:49:49,927 DEBUG: Spawned '['daemon', '-q', 'updater']'
Adding...
2021-04-16 10:49:50,630 ERROR: unexpected error - M:\repo-on-network-share\data.txt is an absolute path
------------------------------------------------------------
Traceback (most recent call last):
  File "dvc\main.py", line 55, in main
  File "dvc\command\add.py", line 21, in run
  File "dvc\repo\__init__.py", line 49, in wrapper
  File "dvc\repo\scm_context.py", line 14, in run
  File "dvc\repo\add.py", line 123, in add
  File "dvc\repo\add.py", line 195, in _process_stages
  File "dvc\stage\__init__.py", line 437, in save
  File "dvc\stage\__init__.py", line 451, in save_outs
  File "dvc\output\base.py", line 289, in save
  File "dvc\output\base.py", line 269, in ignore
  File "dvc\scm\git\__init__.py", line 155, in ignore
  File "dvc\scm\git\__init__.py", line 335, in _backend_func
  File "dvc\scm\git\backend\dulwich.py", line 285, in is_ignored
  File "dulwich\ignore.py", line 366, in is_ignored
  File "dulwich\ignore.py", line 337, in find_matching
ValueError: M:\repo-on-network-share\data.txt is an absolute path
------------------------------------------------------------
2021-04-16 10:49:52,709 DEBUG: Version info for developers:
DVC version: 2.0.17 (exe)
---------------------------------
Platform: Python 3.8.8 on Windows-10-10.0.14393-SP0
Supports: All remotes
Cache types: <https://error.dvc.org/no-dvc-cache>
Caches: local
Remotes: None
Workspace directory: ('unknown', 'none')
Repo: dvc, git

Having any troubles? Hit us up at https://dvc.org/support, we are always happy to help!
2021-04-16 10:49:52,740 DEBUG: Analytics is enabled.
2021-04-16 10:49:54,099 DEBUG: Trying to spawn '['daemon', '-q', 'analytics', 'C:\\Users\\TOM.RUTTER\\AppData\\Local\\Temp\\3\\tmp58lvv709']'
2021-04-16 10:49:56,724 DEBUG: Spawned '['daemon', '-q', 'analytics', 'C:\\Users\\TOM.RUTTER\\AppData\\Local\\Temp\\3\\tmp58lvv709']'
```

### Expected

The file is added to the repo without error.

### Environment information

Windows 10
M: drive mapped to a network UNC path
DVC 2.0.17 installed using windows installer

**Output of `dvc doctor`:**

```console
$ dvc doctor
DVC version: 2.0.17 (exe)
---------------------------------
Platform: Python 3.8.8 on Windows-10-10.0.14393-SP0
Supports: All remotes
Cache types: <https://error.dvc.org/no-dvc-cache>
Caches: local
Remotes: None
Workspace directory: ('unknown', 'none')
Repo: dvc, git
```

**Additional Information (if any):**



Discussion:

Resolving patch:
diff --git a/dvc/utils/__init__.py b/dvc/utils/__init__.py
--- a/dvc/utils/__init__.py
+++ b/dvc/utils/__init__.py
@@ -319,10 +319,19 @@ def relpath(path, start=os.curdir):
     start = os.path.abspath(os.fspath(start))
 
     # Windows path on different drive than curdir doesn't have relpath
-    if os.name == "nt" and not os.path.commonprefix(
-        [start, os.path.abspath(path)]
-    ):
-        return path
+    if os.name == "nt":
+        # Since python 3.8 os.realpath resolves network shares to their UNC
+        # path. So, to be certain that relative paths correctly captured,
+        # we need to resolve to UNC path first. We resolve only the drive
+        # name so that we don't follow any 'real' symlinks on the path
+        def resolve_network_drive_windows(path_to_resolve):
+            drive, tail = os.path.splitdrive(path_to_resolve)
+            return os.path.join(os.path.realpath(drive), tail)
+
+        path = resolve_network_drive_windows(os.path.abspath(path))
+        start = resolve_network_drive_windows(start)
+        if not os.path.commonprefix([start, path]):
+            return path
     return os.path.relpath(path, start)
 
 


Test patch:
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -113,6 +113,27 @@ def test_relpath():
     assert relpath(path) == relpath(path_info)
 
 
+@pytest.mark.skipif(os.name != "nt", reason="Windows specific")
+def test_relpath_windows(monkeypatch):
+    """test that relpath correctly generated when run on a
+    windows network share. The drive mapped path is mapped
+    to a UNC path by os.path.realpath"""
+
+    def dummy_realpath(path):
+        return path.replace("x:", "\\\\server\\share")
+
+    monkeypatch.setattr(os.path, "realpath", dummy_realpath)
+    assert (
+        relpath("x:\\dir1\\dir2\\file.txt", "\\\\server\\share\\dir1")
+        == "dir2\\file.txt"
+    )
+
+    assert (
+        relpath("y:\\dir1\\dir2\\file.txt", "\\\\server\\share\\dir1")
+        == "y:\\dir1\\dir2\\file.txt"
+    )
+
+
 @pytest.mark.parametrize(
     "inp,out,is_dir,expected",
     [