Repository: iterative/dvc

Issue:
move: does not work with symlinks
**Please provide information about your setup**
DVC version(i.e. `dvc --version`), Platform and method of installation (pip, homebrew, pkg Mac, exe (Windows), DEB(Linux), RPM(Linux))
dvc version 0.57.0
Ubuntu 16.04 pip.

@Ruslan at discord said that this link should be enough:
https://discordapp.com/channels/485586884165107732/485596304961962003/620590523144470557



Discussion:

Patch:
diff --git a/dvc/scm/base.py b/dvc/scm/base.py
index a1b2bd4fa..d7ad9d2a5 100644
--- a/dvc/scm/base.py
+++ b/dvc/scm/base.py
@@ -23,11 +23,6 @@ class FileNotInCommitError(SCMError):
     """
 
 
-class FileNotInTargetSubdirError(SCMError):
-    """Thrown when trying to place .gitignore for a file that not in
-    the file subdirectory."""
-
-
 class CloneError(SCMError):
     def __init__(self, url, path, cause):
         super(CloneError, self).__init__(
diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py
index 6afe2973b..2b9a67938 100644
--- a/dvc/scm/git/__init__.py
+++ b/dvc/scm/git/__init__.py
@@ -11,7 +11,6 @@ from dvc.scm.base import (
     Base,
     SCMError,
     FileNotInRepoError,
-    FileNotInTargetSubdirError,
     CloneError,
     RevError,
 )
@@ -104,22 +103,12 @@ class Git(Base):
     def ignore_file(self):
         return self.GITIGNORE
 
-    def _get_gitignore(self, path, ignore_file_dir=None):
-        if not ignore_file_dir:
-            ignore_file_dir = os.path.dirname(os.path.realpath(path))
+    def _get_gitignore(self, path):
+        ignore_file_dir = os.path.dirname(path)
 
         assert os.path.isabs(path)
         assert os.path.isabs(ignore_file_dir)
 
-        if not path.startswith(ignore_file_dir):
-            msg = (
-                "{} file has to be located in one of '{}' subdirectories"
-                ", not outside '{}'"
-            )
-            raise FileNotInTargetSubdirError(
-                msg.format(self.GITIGNORE, path, ignore_file_dir)
-            )
-
         entry = relpath(path, ignore_file_dir).replace(os.sep, "/")
         # NOTE: using '/' prefix to make path unambiguous
         if len(entry) > 0 and entry[0] != "/":
@@ -143,8 +132,7 @@ class Git(Base):
         return False
 
     def ignore(self, path):
-        base_dir = os.path.dirname(path)
-        entry, gitignore = self._get_gitignore(path, base_dir)
+        entry, gitignore = self._get_gitignore(path)
 
         if self._ignored(entry, gitignore):
             return


Test patch:
diff --git a/tests/func/test_scm.py b/tests/func/test_scm.py
index ada5b4759..6c8d60276 100644
--- a/tests/func/test_scm.py
+++ b/tests/func/test_scm.py
@@ -2,12 +2,11 @@ from __future__ import unicode_literals
 
 import os
 
-import pytest
 from git import Repo
 
+from dvc.system import System
 from dvc.utils.compat import str  # noqa: F401
 from dvc.scm import SCM, NoSCM, Git
-from dvc.scm.base import FileNotInTargetSubdirError
 
 from tests.basic_env import TestDir, TestGit, TestGitSubmodule
 from tests.utils import get_gitignore_content
@@ -97,6 +96,14 @@ class TestIgnore(object):
         assert entry == "/dir"
         assert gitignore == os.path.join(repo_dir._root_dir, Git.GITIGNORE)
 
+    def test_get_gitignore_symlink(self, git, repo_dir):
+        link = os.path.join(repo_dir.root_dir, "link")
+        target = os.path.join(repo_dir.root_dir, repo_dir.DATA_SUB)
+        System.symlink(target, link)
+        entry, gitignore = Git(repo_dir._root_dir)._get_gitignore(link)
+        assert entry == "/link"
+        assert gitignore == os.path.join(repo_dir.root_dir, Git.GITIGNORE)
+
     def test_get_gitignore_subdir(self, git, repo_dir):
         data_dir = os.path.join(
             repo_dir._root_dir, os.path.join("dir1", "file1")
@@ -116,35 +123,6 @@ class TestIgnore(object):
             repo_dir._root_dir, "dir1", Git.GITIGNORE
         )
 
-    def test_get_gitignore_ignorefile_dir(self, git, repo_dir):
-        git = Git(repo_dir._root_dir)
-
-        file_double_dir = os.path.join("dir1", "dir2", "file1")
-        data_dir1 = os.path.join(repo_dir._root_dir, file_double_dir)
-        dir1_real1 = os.path.realpath("dir1")
-        entry, gitignore = git._get_gitignore(data_dir1, dir1_real1)
-        assert entry == "/dir2/file1"
-        gitignore1 = os.path.join(repo_dir._root_dir, "dir1", Git.GITIGNORE)
-        assert gitignore == gitignore1
-
-        triple_dir = os.path.join("dir1", "dir2", "dir3")
-        data_dir2 = os.path.join(repo_dir._root_dir, triple_dir)
-        dir1_real2 = os.path.realpath("dir1")
-        entry, gitignore = git._get_gitignore(data_dir2, dir1_real2)
-        assert entry == "/dir2/dir3"
-        gitignore2 = os.path.join(repo_dir._root_dir, "dir1", Git.GITIGNORE)
-        assert gitignore == gitignore2
-
-    def test_get_gitignore_ignorefile_dir_upper_level(self, git, repo_dir):
-        git = Git(repo_dir._root_dir)
-
-        file_double_dir = os.path.join("dir1", "dir2", "file1")
-        data_dir1 = os.path.join(repo_dir._root_dir, file_double_dir)
-        ignore_file_dir = os.path.realpath(os.path.join("aa", "bb"))
-
-        with pytest.raises(FileNotInTargetSubdirError):
-            git._get_gitignore(data_dir1, ignore_file_dir)
-
     def test_gitignore_should_end_with_newline(self, git, repo_dir):
         git = Git(repo_dir._root_dir)