Repository: iterative/dvc

Issue:
Deduplicate links on checkout
In #2016 
We touched the subject of deduplicating links when re-adding dependencies.

Currently, checkout, due to [this](https://github.com/iterative/dvc/blob/41351a2df2f36ed04846d5e3d976d6d99b71a151/dvc/remote/base.py#L749) check we will not recreate link, even if particular `path_info` link type is different than `cache.type` one. `checkout` should `re-checkout` with proper link type and protection level.


Discussion:
@pared , is this related to this comment? https://github.com/iterative/dvc/pull/2358#pullrequestreview-278735035
@mroutis Yes
@pared could you please clarify - is this a bug or we just talking about different link-types (which might lead to some bugs)?
Need to not forget to check performance after this is implemented.
@dmpetrov 
This is the same situation that we have been working on #2016. When doing checkout, we only check whether file checksum did not change. 

What we should do is to resotore file "state". 
What I mean by that could be explained with example:
- we have repo with hardlink cache type
- we unprotect some file, its copy is created
- upon checkout this file, we should "restore" this file as hardlink, so that it reflects current repository configuration

I guess you could say it is a bug.

Resolving patch:
diff --git a/dvc/remote/base.py b/dvc/remote/base.py
--- a/dvc/remote/base.py
+++ b/dvc/remote/base.py
@@ -851,11 +851,6 @@ def checkout(
             self.safe_remove(path_info, force=force)
             skip = True
 
-        elif not self.changed(path_info, checksum_info):
-            msg = "Data '{}' didn't change."
-            logger.debug(msg.format(str(path_info)))
-            skip = True
-
         elif self.changed_cache(checksum):
             msg = "Cache '{}' not found. File '{}' won't be created."
             logger.warning(msg.format(checksum, str(path_info)))


Test patch:
diff --git a/tests/func/test_checkout.py b/tests/func/test_checkout.py
--- a/tests/func/test_checkout.py
+++ b/tests/func/test_checkout.py
@@ -4,6 +4,8 @@
 import collections
 import logging
 
+import pytest
+
 from dvc.main import main
 from dvc.repo import Repo as DvcRepo
 from dvc.system import System
@@ -23,6 +25,7 @@
 
 from mock import patch
 
+from tests.utils import spy
 
 logger = logging.getLogger("dvc")
 
@@ -475,3 +478,56 @@ def test_checkout_no_checksum(repo_dir, dvc_repo):
     stage = dvc_repo.run(outs=[repo_dir.FOO], no_exec=True, cmd="somecmd")
     dvc_repo.checkout(stage.path, force=True)
     assert not os.path.exists(repo_dir.FOO)
+
+
+@pytest.mark.parametrize(
+    "link, link_test_func",
+    [("hardlink", System.is_hardlink), ("symlink", System.is_symlink)],
+)
+def test_should_relink_on_checkout(link, link_test_func, repo_dir, dvc_repo):
+    dvc_repo.cache.local.cache_types = [link]
+
+    dvc_repo.add(repo_dir.DATA_DIR)
+    dvc_repo.unprotect(repo_dir.DATA_SUB)
+
+    dvc_repo.checkout(repo_dir.DATA_DIR + Stage.STAGE_FILE_SUFFIX)
+
+    assert link_test_func(repo_dir.DATA_SUB)
+
+
+@pytest.mark.parametrize("link", ["hardlink", "symlink", "copy"])
+def test_should_protect_on_checkout(link, dvc_repo, repo_dir):
+    dvc_repo.cache.local.cache_types = [link]
+    dvc_repo.cache.local.protected = True
+
+    dvc_repo.add(repo_dir.FOO)
+    dvc_repo.unprotect(repo_dir.FOO)
+
+    dvc_repo.checkout(repo_dir.FOO + Stage.STAGE_FILE_SUFFIX)
+
+    assert not os.access(repo_dir.FOO, os.W_OK)
+
+
+def test_should_relink_only_one_file_in_dir(dvc_repo, repo_dir):
+    dvc_repo.cache.local.cache_types = ["symlink"]
+
+    dvc_repo.add(repo_dir.DATA_DIR)
+    dvc_repo.unprotect(repo_dir.DATA_SUB)
+
+    link_spy = spy(System.symlink)
+    with patch.object(dvc_repo.cache.local, "symlink", link_spy):
+        dvc_repo.checkout(repo_dir.DATA_DIR + Stage.STAGE_FILE_SUFFIX)
+
+    assert link_spy.mock.call_count == 1
+
+
+@pytest.mark.parametrize("link", ["hardlink", "symlink", "copy"])
+def test_should_not_relink_on_unchanged_dependency(link, dvc_repo, repo_dir):
+    dvc_repo.cache.local.cache_types = [link]
+
+    dvc_repo.add(repo_dir.DATA_DIR)
+
+    with patch.object(dvc_repo.cache.local, "link") as mock_link:
+        dvc_repo.checkout(repo_dir.DATA_DIR + Stage.STAGE_FILE_SUFFIX)
+
+    assert not mock_link.called