Repository: iterative/dvc

Issue:
dvc push: when not all files are in local cache, still pushes the hash.dir file, breaking remote repository.
EDIT: Solved. The issue was pushing incomplete datasets from my gpu server to the new storage remotes. It pushed all the files present in the local cache (which is what I wanted) but then it pushed the hash.dir listing, blocking my local machine from uploading the rest of the files that were not present on the cloud server. This is a pretty serious bug IMO (even though it was accidental and poor practice!)

## Bug Report

### Please provide information about your setup

**Output of `dvc version`:**

```console
$ dvc version
DVC version: 1.3.1 (pip)
---------------------------------
Platform: Python 3.7.7 on Linux-5.3.0-1032-aws-x86_64-with-debian-buster-sid
Supports: http, https, s3
Cache types: hardlink, symlink
Repo: dvc, git
```

**Additional Information (if any):**

On this VPS I get the following errors repeatedly when trying to pull from my s3 storage:

```console
2020-08-05 18:32:52,114 ERROR: failed to download 's3://[xxx]/repo.dvc/69/763f0cecd801483a1490a0b2a0b84d' to '.dvc/cache/69/763f0cecd801483a1490a0b2a0b84d' - An error occurred (404) when calling the HeadObject operation: Not Found
------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ubuntu/.venv-dvc/lib/python3.7/site-packages/dvc/cache/local.py", line 30, in wrapper
    func(from_info, to_info, *args, **kwargs)
  File "/home/ubuntu/.venv-dvc/lib/python3.7/site-packages/dvc/tree/base.py", line 420, in download
    from_info, to_info, name, no_progress_bar, file_mode, dir_mode
  File "/home/ubuntu/.venv-dvc/lib/python3.7/site-packages/dvc/tree/base.py", line 478, in _download_file
    from_info, tmp_file, name=name, no_progress_bar=no_progress_bar
  File "/home/ubuntu/.venv-dvc/lib/python3.7/site-packages/dvc/tree/s3.py", line 341, in _download
    Bucket=from_info.bucket, Key=from_info.path
  File "/home/ubuntu/.venv-dvc/lib/python3.7/site-packages/botocore/client.py", line 316, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/ubuntu/.venv-dvc/lib/python3.7/site-packages/botocore/client.py", line 635, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (404) when calling the HeadObject operation: Not Found

```
This had happened on a ec2-linux VPS. I tried again on an Ubuntu Deep Learning AMI, then again with a fresh python3 virtualenv with only dvc installed. I have not been able to replicated this on any of my local workstations. They are able to clone the dvc directories just fine. Even pushing from one and pulling on another.

Also, on any machine, `aws s3 ls ...` does not return any thing for the hashes it is searching for on s3. But, I am able to clone the .dvc on my other machines... I am stumped....

For the record, one local dvc version:

```console
DVC version: 1.3.1 (pip)
---------------------------------
Platform: Python 3.6.9 on Linux-5.4.0-42-generic-x86_64-with-Ubuntu-18.04-bionic
Supports: http, https, s3, ssh
Cache types: hardlink, symlink
Cache directory: ext4 on /dev/sda2
Workspace directory: ext4 on /dev/sda2
Repo: dvc, git
```

If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.


Discussion:

Patch:
diff --git a/dvc/cache/local.py b/dvc/cache/local.py
index 1db76fb23..ec4d1d267 100644
--- a/dvc/cache/local.py
+++ b/dvc/cache/local.py
@@ -255,6 +255,7 @@ class LocalCache(CloudCache):
         path_infos = []
         names = []
         hashes = []
+        missing = []
         for md5, info in Tqdm(
             status_info.items(), desc="Analysing status", unit="file"
         ):
@@ -263,6 +264,8 @@ class LocalCache(CloudCache):
                 path_infos.append(remote.tree.hash_to_path_info(md5))
                 names.append(info["name"])
                 hashes.append(md5)
+            elif info["status"] == STATUS_MISSING:
+                missing.append(md5)
 
         if download:
             to_infos = cache
@@ -271,7 +274,7 @@ class LocalCache(CloudCache):
             to_infos = path_infos
             from_infos = cache
 
-        return from_infos, to_infos, names, hashes
+        return (from_infos, to_infos, names, hashes), missing
 
     def _process(
         self,
@@ -312,8 +315,10 @@ class LocalCache(CloudCache):
             download=download,
         )
 
-        dir_plans = self._get_plans(download, remote, dir_status, status)
-        file_plans = self._get_plans(download, remote, file_status, status)
+        dir_plans, _ = self._get_plans(download, remote, dir_status, status)
+        file_plans, missing_files = self._get_plans(
+            download, remote, file_status, status
+        )
 
         total = len(dir_plans[0]) + len(file_plans[0])
         if total == 0:
@@ -339,19 +344,32 @@ class LocalCache(CloudCache):
                         )
                     dir_futures = {}
                     for from_info, to_info, name, dir_hash in zip(*dir_plans):
-                        wait_futures = {
-                            future
-                            for file_hash, future in file_futures.items()
-                            if file_hash in dir_contents[dir_hash]
-                        }
-                        dir_futures[dir_hash] = executor.submit(
-                            self._dir_upload,
-                            func,
-                            wait_futures,
-                            from_info,
-                            to_info,
-                            name,
-                        )
+                        # if for some reason a file contained in this dir is
+                        # missing both locally and in the remote, we want to
+                        # push whatever file content we have, but should not
+                        # push .dir file
+                        for file_hash in missing_files:
+                            if file_hash in dir_contents[dir_hash]:
+                                logger.debug(
+                                    "directory '%s' contains missing files,"
+                                    "skipping .dir file upload",
+                                    name,
+                                )
+                                break
+                        else:
+                            wait_futures = {
+                                future
+                                for file_hash, future in file_futures.items()
+                                if file_hash in dir_contents[dir_hash]
+                            }
+                            dir_futures[dir_hash] = executor.submit(
+                                self._dir_upload,
+                                func,
+                                wait_futures,
+                                from_info,
+                                to_info,
+                                name,
+                            )
                     fails = sum(
                         future.result()
                         for future in concat(
diff --git a/dvc/command/check_ignore.py b/dvc/command/check_ignore.py
index e28f4f19b..97da662a3 100644
--- a/dvc/command/check_ignore.py
+++ b/dvc/command/check_ignore.py
@@ -38,12 +38,11 @@ class CmdCheckIgnore(CmdBase):
     def _interactive_mode(self):
         ret = 1
         while True:
-            target = ask("")
+            try:
+                target = ask("")
+            except (KeyboardInterrupt, EOFError):
+                break
             if target == "":
-                logger.info(
-                    "Empty string is not a valid pathspec. Please use . "
-                    "instead if you meant to match all paths."
-                )
                 break
             if not self._check_one_file(target):
                 ret = 0
diff --git a/dvc/config.py b/dvc/config.py
index 6aa5908e5..e370489d6 100644
--- a/dvc/config.py
+++ b/dvc/config.py
@@ -138,6 +138,7 @@ SCHEMA = {
         Optional("hardlink_lock", default=False): Bool,
         Optional("no_scm", default=False): Bool,
         Optional("experiments", default=False): Bool,
+        Optional("check_update", default=True): Bool,
     },
     "cache": {
         "local": str,
diff --git a/dvc/ignore.py b/dvc/ignore.py
index 0e80a0210..71569b38b 100644
--- a/dvc/ignore.py
+++ b/dvc/ignore.py
@@ -6,12 +6,12 @@ from itertools import groupby, takewhile
 
 from pathspec.patterns import GitWildMatchPattern
 from pathspec.util import normalize_file
-from pygtrie import StringTrie
 
 from dvc.path_info import PathInfo
 from dvc.pathspec_math import PatternInfo, merge_patterns
 from dvc.system import System
 from dvc.utils import relpath
+from dvc.utils.collections import PathStringTrie
 
 logger = logging.getLogger(__name__)
 
@@ -156,7 +156,7 @@ class DvcIgnoreFilterNoop:
     def __init__(self, tree, root_dir):
         pass
 
-    def __call__(self, root, dirs, files):
+    def __call__(self, root, dirs, files, **kwargs):
         return dirs, files
 
     def is_ignored_dir(self, _):
@@ -183,10 +183,11 @@ class DvcIgnoreFilter:
 
         self.tree = tree
         self.root_dir = root_dir
-        self.ignores_trie_tree = StringTrie(separator=os.sep)
+        self.ignores_trie_tree = PathStringTrie()
         self.ignores_trie_tree[root_dir] = DvcIgnorePatterns(
             default_ignore_patterns, root_dir
         )
+        self._ignored_subrepos = PathStringTrie()
         self._update(self.root_dir)
 
     def _update(self, dirname):
@@ -222,7 +223,10 @@ class DvcIgnoreFilter:
     def _update_sub_repo(self, root, dirs):
         for d in dirs:
             if self._is_dvc_repo(root, d):
-                new_pattern = DvcIgnorePatterns(["/{}/".format(d)], root)
+                self._ignored_subrepos[root] = self._ignored_subrepos.get(
+                    root, set()
+                ) | {d}
+                new_pattern = DvcIgnorePatterns([f"/{d}/"], root)
                 old_pattern = self.ignores_trie_tree.longest_prefix(root).value
                 if old_pattern:
                     self.ignores_trie_tree[root] = DvcIgnorePatterns(
@@ -236,12 +240,13 @@ class DvcIgnoreFilter:
                 else:
                     self.ignores_trie_tree[root] = new_pattern
 
-    def __call__(self, root, dirs, files):
+    def __call__(self, root, dirs, files, ignore_subrepos=True):
         ignore_pattern = self._get_trie_pattern(root)
         if ignore_pattern:
-            return ignore_pattern(root, dirs, files)
-        else:
-            return dirs, files
+            dirs, files = ignore_pattern(root, dirs, files)
+            if not ignore_subrepos:
+                dirs.extend(self._ignored_subrepos.get(root, []))
+        return dirs, files
 
     def _get_trie_pattern(self, dirname):
         ignore_pattern = self.ignores_trie_tree.get(dirname)
@@ -277,8 +282,14 @@ class DvcIgnoreFilter:
         else:
             return False
 
-    def is_ignored_dir(self, path):
+    def _is_subrepo(self, path):
+        dirname, basename = os.path.split(os.path.normpath(path))
+        return basename in self._ignored_subrepos.get(dirname, set())
+
+    def is_ignored_dir(self, path, ignore_subrepos=True):
         path = os.path.abspath(path)
+        if not ignore_subrepos:
+            return not self._is_subrepo(path)
         if path == self.root_dir:
             return False
 
diff --git a/dvc/main.py b/dvc/main.py
index fc1450e53..07e081253 100644
--- a/dvc/main.py
+++ b/dvc/main.py
@@ -84,7 +84,9 @@ def main(argv=None):  # noqa: C901
         ret = 255
 
     try:
-        if ret != 0:
+        if ret != 0 and (
+            ret != 1 or getattr(args, "cmd", "") != "check-ignore"
+        ):
             logger.info(FOOTER)
 
         if analytics.is_enabled():
diff --git a/dvc/tree/git.py b/dvc/tree/git.py
index edf49964a..e4f254898 100644
--- a/dvc/tree/git.py
+++ b/dvc/tree/git.py
@@ -84,13 +84,15 @@ class GitTree(BaseTree):  # pylint:disable=abstract-method
             path
         ) and not self.dvcignore.is_ignored_dir(path)
 
-    def isdir(self, path):  # pylint: disable=arguments-differ
+    def isdir(
+        self, path, use_dvcignore=True
+    ):  # pylint: disable=arguments-differ
         obj = self._git_object_by_path(path)
         if obj is None:
             return False
         if obj.mode != GIT_MODE_DIR:
             return False
-        return not self.dvcignore.is_ignored_dir(path)
+        return not (use_dvcignore and self.dvcignore.is_ignored_dir(path))
 
     def isfile(self, path):  # pylint: disable=arguments-differ
         obj = self._git_object_by_path(path)
@@ -156,7 +158,14 @@ class GitTree(BaseTree):  # pylint:disable=abstract-method
         if not topdown:
             yield os.path.normpath(tree.abspath), dirs, nondirs
 
-    def walk(self, top, topdown=True, onerror=None, use_dvcignore=True):
+    def walk(
+        self,
+        top,
+        topdown=True,
+        onerror=None,
+        use_dvcignore=True,
+        ignore_subrepos=True,
+    ):
         """Directory tree generator.
 
         See `os.walk` for the docs. Differences:
@@ -176,7 +185,10 @@ class GitTree(BaseTree):  # pylint:disable=abstract-method
         for root, dirs, files in self._walk(tree, topdown=topdown):
             if use_dvcignore:
                 dirs[:], files[:] = self.dvcignore(
-                    os.path.abspath(root), dirs, files
+                    os.path.abspath(root),
+                    dirs,
+                    files,
+                    ignore_subrepos=ignore_subrepos,
                 )
             yield root, dirs, files
 
diff --git a/dvc/tree/local.py b/dvc/tree/local.py
index bfce3345f..ca3b8cfbf 100644
--- a/dvc/tree/local.py
+++ b/dvc/tree/local.py
@@ -86,18 +86,26 @@ class LocalTree(BaseTree):
 
         return not self.dvcignore.is_ignored_file(path_info)
 
-    def isdir(self, path_info):
+    def isdir(
+        self, path_info, use_dvcignore=True
+    ):  # pylint: disable=arguments-differ
         if not os.path.isdir(path_info):
             return False
-
-        return not self.dvcignore.is_ignored_dir(path_info)
+        return not (use_dvcignore and self.dvcignore.is_ignored_dir(path_info))
 
     def iscopy(self, path_info):
         return not (
             System.is_symlink(path_info) or System.is_hardlink(path_info)
         )
 
-    def walk(self, top, topdown=True, onerror=None, use_dvcignore=True):
+    def walk(
+        self,
+        top,
+        topdown=True,
+        onerror=None,
+        use_dvcignore=True,
+        ignore_subrepos=True,
+    ):
         """Directory tree generator.
 
         See `os.walk` for the docs. Differences:
@@ -108,7 +116,10 @@ class LocalTree(BaseTree):
         ):
             if use_dvcignore:
                 dirs[:], files[:] = self.dvcignore(
-                    os.path.abspath(root), dirs, files
+                    os.path.abspath(root),
+                    dirs,
+                    files,
+                    ignore_subrepos=ignore_subrepos,
                 )
 
             yield os.path.normpath(root), dirs, files
diff --git a/dvc/updater.py b/dvc/updater.py
index b2fb79b6a..92ef1bb88 100644
--- a/dvc/updater.py
+++ b/dvc/updater.py
@@ -7,6 +7,7 @@ import colorama
 from packaging import version
 
 from dvc import __version__
+from dvc.config import Config, to_bool
 from dvc.lock import LockError, make_lock
 from dvc.utils import boxify, env2bool
 from dvc.utils.pkg import PKG
@@ -46,7 +47,12 @@ class Updater:  # pragma: no cover
             logger.debug(msg.format(self.lock.lockfile, action))
 
     def check(self):
-        if os.getenv("CI") or env2bool("DVC_TEST") or PKG == "snap":
+        if (
+            os.getenv("CI")
+            or env2bool("DVC_TEST")
+            or PKG == "snap"
+            or not self.is_enabled()
+        ):
             return
 
         self._with_lock(self._check, "checking")
@@ -144,3 +150,12 @@ class Updater:  # pragma: no cover
             package_manager = "binary"
 
         return instructions[package_manager]
+
+    def is_enabled(self):
+        enabled = to_bool(
+            Config(validate=False).get("core", {}).get("check_update", "true")
+        )
+        logger.debug(
+            "Check for update is {}abled.".format("en" if enabled else "dis")
+        )
+        return enabled
diff --git a/dvc/utils/collections.py b/dvc/utils/collections.py
index 1ff72a347..d59869b61 100644
--- a/dvc/utils/collections.py
+++ b/dvc/utils/collections.py
@@ -1,5 +1,16 @@
+import os
 from collections.abc import Mapping
 
+from pygtrie import StringTrie as _StringTrie
+
+
+class PathStringTrie(_StringTrie):
+    """Trie based on platform-dependent separator for pathname components."""
+
+    def __init__(self, *args, **kwargs):
+        kwargs["separator"] = os.sep
+        super().__init__(*args, **kwargs)
+
 
 def apply_diff(src, dest):
     """Recursively apply changes from src to dest.
diff --git a/dvc/utils/pager.py b/dvc/utils/pager.py
index 17431bdf9..72344b139 100644
--- a/dvc/utils/pager.py
+++ b/dvc/utils/pager.py
@@ -12,8 +12,8 @@ logger = logging.getLogger(__name__)
 
 
 DEFAULT_PAGER = "less"
-DEFAULT_PAGER_FORMATTED = "{} --chop-long-lines --clear-screen".format(
-    DEFAULT_PAGER
+DEFAULT_PAGER_FORMATTED = (
+    f"{DEFAULT_PAGER} --chop-long-lines --clear-screen --RAW-CONTROL-CHARS"
 )
 
 


Test patch:
diff --git a/tests/dir_helpers.py b/tests/dir_helpers.py
index 02ae154fd..6d1abf813 100644
--- a/tests/dir_helpers.py
+++ b/tests/dir_helpers.py
@@ -74,6 +74,8 @@ disable_other_loggers()
 
 
 class TmpDir(pathlib.Path):
+    scheme = "local"
+
     def __new__(cls, *args, **kwargs):
         if cls is TmpDir:
             cls = (  # pylint: disable=self-cls-assignment
@@ -87,7 +89,7 @@ class TmpDir(pathlib.Path):
         self._init()
         return self
 
-    def init(self, *, scm=False, dvc=False):
+    def init(self, *, scm=False, dvc=False, subdir=False):
         from dvc.repo import Repo
         from dvc.scm.git import Git
 
@@ -100,7 +102,9 @@ class TmpDir(pathlib.Path):
             git_init(str_path)
         if dvc:
             self.dvc = Repo.init(
-                str_path, no_scm=not scm and not hasattr(self, "scm")
+                str_path,
+                no_scm=not scm and not hasattr(self, "scm"),
+                subdir=subdir,
             )
         if scm:
             self.scm = self.dvc.scm if hasattr(self, "dvc") else Git(str_path)
@@ -123,10 +127,10 @@ class TmpDir(pathlib.Path):
         if isinstance(struct, (str, bytes, pathlib.PurePath)):
             struct = {struct: text}
 
-        self._gen(struct)
-        return struct.keys()
+        return self._gen(struct)
 
     def _gen(self, struct, prefix=None):
+        paths = []
         for name, contents in struct.items():
             path = (prefix or self) / name
 
@@ -141,6 +145,8 @@ class TmpDir(pathlib.Path):
                     path.write_bytes(contents)
                 else:
                     path.write_text(contents, encoding="utf-8")
+            paths.append(path)
+        return paths
 
     def dvc_gen(self, struct, text="", commit=None):
         paths = self.gen(struct, text)
@@ -249,10 +255,10 @@ class PosixTmpDir(TmpDir, pathlib.PurePosixPath):
 
 @pytest.fixture(scope="session")
 def make_tmp_dir(tmp_path_factory, request):
-    def make(name, *, scm=False, dvc=False):
+    def make(name, *, scm=False, dvc=False, **kwargs):
         path = tmp_path_factory.mktemp(name) if isinstance(name, str) else name
         new_dir = TmpDir(path)
-        new_dir.init(scm=scm, dvc=dvc)
+        new_dir.init(scm=scm, dvc=dvc, **kwargs)
         request.addfinalizer(new_dir.close)
         return new_dir
 
diff --git a/tests/func/test_check_ignore.py b/tests/func/test_check_ignore.py
index eba86bc98..905b6245d 100644
--- a/tests/func/test_check_ignore.py
+++ b/tests/func/test_check_ignore.py
@@ -14,6 +14,7 @@ def test_check_ignore(tmp_dir, dvc, file, ret, output, caplog):
 
     assert main(["check-ignore", file]) == ret
     assert (file in caplog.text) is output
+    assert "Having any troubles?" not in caplog.text
 
 
 @pytest.mark.parametrize(
@@ -60,8 +61,9 @@ def test_check_ignore_non_matching(tmp_dir, dvc, non_matching, caplog):
         [],
     ],
 )
-def test_check_ignore_error_args_cases(tmp_dir, dvc, args):
+def test_check_ignore_error_args_cases(tmp_dir, dvc, args, caplog):
     assert main(["check-ignore"] + args) == 255
+    assert ("Having any troubles?" in caplog.text) == ("-q" not in args)
 
 
 @pytest.mark.parametrize("path,ret", [({"dir": {}}, 0), ({"dir": "files"}, 1)])
diff --git a/tests/func/test_ignore.py b/tests/func/test_ignore.py
index 67e8f71a5..042329f79 100644
--- a/tests/func/test_ignore.py
+++ b/tests/func/test_ignore.py
@@ -182,6 +182,28 @@ def test_ignore_subrepo(tmp_dir, scm, dvc):
         assert subrepo.tree.exists(PathInfo(subrepo_dir / "foo"))
 
 
+def test_ignore_resurface_subrepo(tmp_dir, scm, dvc):
+    tmp_dir.dvc_gen({"foo": "foo"}, commit="add foo")
+    subrepo_dir = tmp_dir / "subdir"
+    subrepo_dir.mkdir()
+    with subrepo_dir.chdir():
+        Repo.init(subdir=True)
+
+    dvc.tree.__dict__.pop("dvcignore", None)
+
+    dirs = ["subdir"]
+    files = ["foo"]
+    assert dvc.tree.dvcignore(os.fspath(tmp_dir), dirs, files) == ([], files)
+    assert dvc.tree.dvcignore(
+        os.fspath(tmp_dir), dirs, files, ignore_subrepos=False
+    ) == (dirs, files)
+
+    assert dvc.tree.dvcignore.is_ignored_dir(os.fspath(subrepo_dir))
+    assert not dvc.tree.dvcignore.is_ignored_dir(
+        os.fspath(subrepo_dir), ignore_subrepos=False
+    )
+
+
 def test_ignore_blank_line(tmp_dir, dvc):
     tmp_dir.gen({"dir": {"ignored": "text", "other": "text2"}})
     tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "foo\n\ndir/ignored")
diff --git a/tests/func/test_remote.py b/tests/func/test_remote.py
index 38196ffbd..008b80421 100644
--- a/tests/func/test_remote.py
+++ b/tests/func/test_remote.py
@@ -412,3 +412,23 @@ def test_protect_local_remote(tmp_dir, dvc, local_remote):
 
     assert os.path.exists(remote_cache_file)
     assert stat.S_IMODE(os.stat(remote_cache_file).st_mode) == 0o444
+
+
+def test_push_incomplete_dir(tmp_dir, dvc, mocker, local_remote):
+    (stage,) = tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
+    remote = dvc.cloud.get_remote("upstream")
+
+    cache = dvc.cache.local
+    dir_hash = stage.outs[0].checksum
+    used = stage.get_used_cache(remote=remote)
+
+    # remove one of the cache files for directory
+    file_hashes = list(used.child_keys(cache.tree.scheme, dir_hash))
+    remove(cache.tree.hash_to_path_info(file_hashes[0]))
+
+    dvc.push()
+    assert not remote.tree.exists(remote.tree.hash_to_path_info(dir_hash))
+    assert not remote.tree.exists(
+        remote.tree.hash_to_path_info(file_hashes[0])
+    )
+    assert remote.tree.exists(remote.tree.hash_to_path_info(file_hashes[1]))
diff --git a/tests/func/test_tree.py b/tests/func/test_tree.py
index 3ab93efc9..ad09e5312 100644
--- a/tests/func/test_tree.py
+++ b/tests/func/test_tree.py
@@ -1,4 +1,5 @@
 import os
+from operator import itemgetter
 from os.path import join
 
 from dvc.path_info import PathInfo
@@ -245,3 +246,25 @@ def test_cleantree_subrepo(tmp_dir, dvc, scm, monkeypatch):
     assert subrepo.tree.isfile(path / "foo")
     assert subrepo.tree.exists(path / "dir")
     assert subrepo.tree.isdir(path / "dir")
+
+
+def test_walk_dont_ignore_subrepos(tmp_dir, scm, dvc):
+    tmp_dir.dvc_gen({"foo": "foo"}, commit="add foo")
+    subrepo_dir = tmp_dir / "subdir"
+    subrepo_dir.mkdir()
+    with subrepo_dir.chdir():
+        Repo.init(subdir=True)
+    scm.add(["subdir"])
+    scm.commit("Add subrepo")
+
+    dvc_tree = dvc.tree
+    scm_tree = scm.get_tree("HEAD", use_dvcignore=True)
+    path = os.fspath(tmp_dir)
+    get_dirs = itemgetter(1)
+
+    assert get_dirs(next(dvc_tree.walk(path))) == []
+    assert get_dirs(next(scm_tree.walk(path))) == []
+
+    kw = dict(ignore_subrepos=False)
+    assert get_dirs(next(dvc_tree.walk(path, **kw))) == ["subdir"]
+    assert get_dirs(next(scm_tree.walk(path, **kw))) == ["subdir"]
diff --git a/tests/unit/test_updater.py b/tests/unit/test_updater.py
index 76257af66..076229e29 100644
--- a/tests/unit/test_updater.py
+++ b/tests/unit/test_updater.py
@@ -44,6 +44,28 @@ def test_fetch(mock_get, updater):
     assert info["version"] == __version__
 
 
+@pytest.mark.parametrize(
+    "core, result",
+    [
+        ({}, True),
+        ({"check_update": "true"}, True),
+        ({"check_update": "false"}, False),
+    ],
+)
+def test_is_enabled(dvc, updater, core, result):
+    with dvc.config.edit("local") as conf:
+        conf["core"] = core
+    assert result == updater.is_enabled()
+
+
+@pytest.mark.parametrize("result", [True, False])
+@mock.patch("dvc.updater.Updater._check")
+def test_check_update_respect_config(mock_check, result, updater, mocker):
+    mocker.patch.object(updater, "is_enabled", return_value=result)
+    updater.check()
+    assert result == mock_check.called
+
+
 @pytest.mark.parametrize(
     "current,latest,notify",
     [