diff --git a/dvc/utils/__init__.py b/dvc/utils/__init__.py index b91a01144..522037f26 100644 --- 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) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index b86c9044d..43d9c5ba3 100644 --- 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", [