diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 5582aacbe..b50dcdb9e 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,7 +1,7 @@
-* [ ] ❗ Have you followed the guidelines in the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) list?
+* [ ] ❗ I have followed the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) checklist.
 
-* [ ] 📖 Check this box if this PR **does not** require [documentation](https://dvc.org/doc) updates, or if it does **and** you have created a separate PR in [dvc.org](https://github.com/iterative/dvc.org) with such updates (or at least opened an issue about it in that repo). Please link below to your PR (or issue) in the [dvc.org](https://github.com/iterative/dvc.org) repo.
+* [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/iterative/dvc.org) and linked it here. If the CLI API is changed, I have updated [tab completion scripts](https://github.com/iterative/dvc/tree/master/scripts/completion).
 
-* [ ] ❌ Have you checked DeepSource, CodeClimate, and other sanity checks below? We consider their findings recommendatory and don't expect everything to be addressed. Please review them carefully and fix those that actually improve code or fix bugs.
+* [ ] ❌ I will check DeepSource, CodeClimate, and other sanity checks below. (We consider them recommendatory and don't expect everything to be addressed. Please fix things that actually improve code or fix bugs.)
 
 Thank you for the contribution - we'll try to review it as soon as possible. 🙏
diff --git a/dvc/command/gc.py b/dvc/command/gc.py
index b4b710ae7..64e634d3a 100644
--- a/dvc/command/gc.py
+++ b/dvc/command/gc.py
@@ -19,7 +19,6 @@ class CmdGC(CmdBase):
             all_tags=self.args.all_tags,
             all_commits=self.args.all_commits,
             workspace=self.args.workspace,
-            cloud=self.args.cloud,
         )
 
         msg = "This will remove all cache except items used in "
diff --git a/dvc/command/unprotect.py b/dvc/command/unprotect.py
index eb6dc8876..e9c5df81a 100644
--- a/dvc/command/unprotect.py
+++ b/dvc/command/unprotect.py
@@ -22,7 +22,10 @@ class CmdUnprotect(CmdBase):
 
 
 def add_parser(subparsers, parent_parser):
-    UNPROTECT_HELP = "Unprotect data files or directories."
+    UNPROTECT_HELP = (
+        "Unprotect tracked files or directories (when hardlinks or symlinks "
+        "have been enabled with `dvc config cache.type`)"
+    )
     unprotect_parser = subparsers.add_parser(
         "unprotect",
         parents=[parent_parser],
diff --git a/dvc/config.py b/dvc/config.py
index 0907c66f3..34addc400 100644
--- a/dvc/config.py
+++ b/dvc/config.py
@@ -97,7 +97,7 @@ REMOTE_COMMON = {
 }
 LOCAL_COMMON = {
     "type": supported_cache_type,
-    Optional("protected", default=False): Bool,
+    Optional("protected", default=False): Bool,  # obsoleted
     "shared": All(Lower, Choices("group")),
     Optional("slow_link_warning", default=True): Bool,
 }
diff --git a/dvc/remote/base.py b/dvc/remote/base.py
index c12f5df91..8ff3046cb 100644
--- a/dvc/remote/base.py
+++ b/dvc/remote/base.py
@@ -83,6 +83,9 @@ class RemoteBASE(object):
     DEFAULT_NO_TRAVERSE = True
     DEFAULT_VERIFY = False
 
+    CACHE_MODE = None
+    SHARED_MODE_MAP = {None: (None, None), "group": (None, None)}
+
     state = StateNoop()
 
     def __init__(self, repo, config):
@@ -90,12 +93,14 @@ class RemoteBASE(object):
 
         self._check_requires(config)
 
+        shared = config.get("shared")
+        self._file_mode, self._dir_mode = self.SHARED_MODE_MAP[shared]
+
         self.checksum_jobs = (
             config.get("checksum_jobs")
             or (self.repo and self.repo.config["core"].get("checksum_jobs"))
             or self.CHECKSUM_JOBS
         )
-        self.protected = False
         self.no_traverse = config.get("no_traverse", self.DEFAULT_NO_TRAVERSE)
         self.verify = config.get("verify", self.DEFAULT_VERIFY)
         self._dir_info = {}
@@ -221,7 +226,7 @@ class RemoteBASE(object):
         new_info = self.cache.checksum_to_path_info(checksum)
         if self.cache.changed_cache_file(checksum):
             self.cache.makedirs(new_info.parent)
-            self.cache.move(tmp_info, new_info)
+            self.cache.move(tmp_info, new_info, mode=self.CACHE_MODE)
 
         self.state.save(path_info, checksum)
         self.state.save(new_info, checksum)
@@ -409,15 +414,8 @@ class RemoteBASE(object):
 
         link_method(from_info, to_info)
 
-        if self.protected:
-            self.protect(to_info)
-
         logger.debug(
-            "Created %s'%s': %s -> %s",
-            "protected " if self.protected else "",
-            self.cache_types[0],
-            from_info,
-            to_info,
+            "Created '%s': %s -> %s", self.cache_types[0], from_info, to_info,
         )
 
     def _save_file(self, path_info, checksum, save_link=True):
@@ -425,14 +423,11 @@ class RemoteBASE(object):
 
         cache_info = self.checksum_to_path_info(checksum)
         if self.changed_cache(checksum):
-            self.move(path_info, cache_info)
+            self.move(path_info, cache_info, mode=self.CACHE_MODE)
             self.link(cache_info, path_info)
         elif self.iscopy(path_info) and self._cache_is_copy(path_info):
             # Default relink procedure involves unneeded copy
-            if self.protected:
-                self.protect(path_info)
-            else:
-                self.unprotect(path_info)
+            self.unprotect(path_info)
         else:
             self.remove(path_info)
             self.link(cache_info, path_info)
@@ -656,7 +651,8 @@ class RemoteBASE(object):
     def remove(self, path_info):
         raise RemoteActionNotImplemented("remove", self.scheme)
 
-    def move(self, from_info, to_info):
+    def move(self, from_info, to_info, mode=None):
+        assert mode is None
         self.copy(from_info, to_info)
         self.remove(from_info)
 
@@ -718,6 +714,9 @@ class RemoteBASE(object):
             removed = True
         return removed
 
+    def is_protected(self, path_info):
+        return False
+
     def changed_cache_file(self, checksum):
         """Compare the given checksum with the (corresponding) actual one.
 
@@ -730,7 +729,14 @@ class RemoteBASE(object):
 
         - Remove the file from cache if it doesn't match the actual checksum
         """
+
         cache_info = self.checksum_to_path_info(checksum)
+        if self.is_protected(cache_info):
+            logger.debug(
+                "Assuming '%s' is unchanged since it is read-only", cache_info
+            )
+            return False
+
         actual = self.get_checksum(cache_info)
 
         logger.debug(
@@ -744,6 +750,9 @@ class RemoteBASE(object):
             return True
 
         if actual.split(".")[0] == checksum.split(".")[0]:
+            # making cache file read-only so we don't need to check it
+            # next time
+            self.protect(cache_info)
             return False
 
         if self.exists(cache_info):
diff --git a/dvc/remote/local.py b/dvc/remote/local.py
index 3e62e0628..d1470620e 100644
--- a/dvc/remote/local.py
+++ b/dvc/remote/local.py
@@ -32,19 +32,11 @@ class RemoteLOCAL(RemoteBASE):
 
     DEFAULT_CACHE_TYPES = ["reflink", "copy"]
 
+    CACHE_MODE = 0o444
     SHARED_MODE_MAP = {None: (0o644, 0o755), "group": (0o664, 0o775)}
 
     def __init__(self, repo, config):
         super().__init__(repo, config)
-        self.protected = config.get("protected", False)
-
-        shared = config.get("shared")
-        self._file_mode, self._dir_mode = self.SHARED_MODE_MAP[shared]
-
-        if self.protected:
-            # cache files are set to be read-only for everyone
-            self._file_mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
-
         self.cache_dir = config.get("url")
         self._dir_info = {}
 
@@ -142,16 +134,17 @@ class RemoteLOCAL(RemoteBASE):
         if self.exists(path_info):
             remove(path_info.fspath)
 
-    def move(self, from_info, to_info):
+    def move(self, from_info, to_info, mode=None):
         if from_info.scheme != "local" or to_info.scheme != "local":
             raise NotImplementedError
 
         self.makedirs(to_info.parent)
 
-        if self.isfile(from_info):
-            mode = self._file_mode
-        else:
-            mode = self._dir_mode
+        if mode is None:
+            if self.isfile(from_info):
+                mode = self._file_mode
+            else:
+                mode = self._dir_mode
 
         move(from_info, to_info, mode=mode)
 
@@ -159,6 +152,7 @@ class RemoteLOCAL(RemoteBASE):
         tmp_info = to_info.parent / tmp_fname(to_info.name)
         try:
             System.copy(from_info, tmp_info)
+            os.chmod(fspath_py35(tmp_info), self._file_mode)
             os.rename(fspath_py35(tmp_info), fspath_py35(to_info))
         except Exception:
             self.remove(tmp_info)
@@ -202,9 +196,13 @@ class RemoteLOCAL(RemoteBASE):
     def is_hardlink(path_info):
         return System.is_hardlink(path_info)
 
-    @staticmethod
-    def reflink(from_info, to_info):
-        System.reflink(from_info, to_info)
+    def reflink(self, from_info, to_info):
+        tmp_info = to_info.parent / tmp_fname(to_info.name)
+        System.reflink(from_info, tmp_info)
+        # NOTE: reflink has its own separate inode, so you can set permissions
+        # that are different from the source.
+        os.chmod(fspath_py35(tmp_info), self._file_mode)
+        os.rename(fspath_py35(tmp_info), fspath_py35(to_info))
 
     def cache_exists(self, checksums, jobs=None, name=None):
         return [
@@ -402,8 +400,7 @@ class RemoteLOCAL(RemoteBASE):
             )
             logger.warning(msg)
 
-    @staticmethod
-    def _unprotect_file(path):
+    def _unprotect_file(self, path):
         if System.is_symlink(path) or System.is_hardlink(path):
             logger.debug("Unprotecting '{}'".format(path))
             tmp = os.path.join(os.path.dirname(path), "." + uuid())
@@ -423,13 +420,13 @@ class RemoteLOCAL(RemoteBASE):
                 "a symlink or a hardlink.".format(path)
             )
 
-        os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
+        os.chmod(path, self._file_mode)
 
     def _unprotect_dir(self, path):
         assert is_working_tree(self.repo.tree)
 
         for fname in self.repo.tree.walk_files(path):
-            RemoteLOCAL._unprotect_file(fname)
+            self._unprotect_file(fname)
 
     def unprotect(self, path_info):
         path = path_info.fspath
@@ -441,12 +438,11 @@ class RemoteLOCAL(RemoteBASE):
         if os.path.isdir(path):
             self._unprotect_dir(path)
         else:
-            RemoteLOCAL._unprotect_file(path)
+            self._unprotect_file(path)
 
-    @staticmethod
-    def protect(path_info):
+    def protect(self, path_info):
         path = fspath_py35(path_info)
-        mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
+        mode = self.CACHE_MODE
 
         try:
             os.chmod(path, mode)
@@ -519,3 +515,11 @@ class RemoteLOCAL(RemoteBASE):
             if self.is_dir_checksum(c):
                 unpacked.add(c + self.UNPACKED_DIR_SUFFIX)
         return unpacked
+
+    def is_protected(self, path_info):
+        if not self.exists(path_info):
+            return False
+
+        mode = os.stat(fspath_py35(path_info)).st_mode
+
+        return stat.S_IMODE(mode) == self.CACHE_MODE
diff --git a/dvc/remote/ssh/__init__.py b/dvc/remote/ssh/__init__.py
index a3773871f..09c23bc6b 100644
--- a/dvc/remote/ssh/__init__.py
+++ b/dvc/remote/ssh/__init__.py
@@ -209,7 +209,8 @@ class RemoteSSH(RemoteBASE):
         with self.ssh(path_info) as ssh:
             ssh.remove(path_info.path)
 
-    def move(self, from_info, to_info):
+    def move(self, from_info, to_info, mode=None):
+        assert mode is None
         if from_info.scheme != self.scheme or to_info.scheme != self.scheme:
             raise NotImplementedError
 
diff --git a/dvc/repo/gc.py b/dvc/repo/gc.py
index a19c3932b..5f988244d 100644
--- a/dvc/repo/gc.py
+++ b/dvc/repo/gc.py
@@ -46,7 +46,6 @@ def gc(
         all_tags=all_tags,
         all_commits=all_commits,
         all_branches=all_branches,
-        cloud=cloud,
     )
 
     from contextlib import ExitStack

tab completion scripts need more love
Seems to me it's about time we review and update tab completion scripts again (like in #1732)? There are some new and removed commands, options, etc, I think. See also #2983. Some other examples:

- `gc --all-commits` has been hidden from help output (#2681).
- `dvc run --remove-outs` is now deprecated.

And maybe we could add a checkbox to .github/PULL_REQUEST_TEMPLATE.md similar to the one for updating docs but for tab completion scripts?
diff --git a/tests/func/remote/test_local.py b/tests/func/remote/test_local.py
index cc0f7ded1..48c89c354 100644
--- a/tests/func/remote/test_local.py
+++ b/tests/func/remote/test_local.py
@@ -57,6 +57,7 @@ def test_dir_cache_changed_on_single_cache_file_modification(tmp_dir, dvc):
     assert os.path.exists(unpacked_dir)
 
     cache_file_path = dvc.cache.local.get(file_md5)
+    os.chmod(cache_file_path, 0o644)
     with open(cache_file_path, "a") as fobj:
         fobj.write("modification")
 
diff --git a/tests/func/test_add.py b/tests/func/test_add.py
index 9d966452d..6a23ee832 100644
--- a/tests/func/test_add.py
+++ b/tests/func/test_add.py
@@ -489,12 +489,12 @@ class TestAddUnprotected(TestDvc):
         ret = main(["config", "cache.type", "hardlink"])
         self.assertEqual(ret, 0)
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["add", self.FOO])
         self.assertEqual(ret, 0)
 
+        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertTrue(System.is_hardlink(self.FOO))
+
         ret = main(["unprotect", self.FOO])
         self.assertEqual(ret, 0)
 
@@ -561,7 +561,6 @@ def test_readding_dir_should_not_unprotect_all(tmp_dir, dvc, mocker):
     tmp_dir.gen("dir/data", "data")
 
     dvc.cache.local.cache_types = ["symlink"]
-    dvc.cache.local.protected = True
 
     dvc.add("dir")
     tmp_dir.gen("dir/new_file", "new_file_content")
@@ -618,7 +617,6 @@ def test_should_relink_on_repeated_add(
 @pytest.mark.parametrize("link", ["hardlink", "symlink", "copy"])
 def test_should_protect_on_repeated_add(link, tmp_dir, dvc):
     dvc.cache.local.cache_types = [link]
-    dvc.cache.local.protected = True
 
     tmp_dir.dvc_gen({"foo": "foo"})
 
@@ -626,7 +624,16 @@ def test_should_protect_on_repeated_add(link, tmp_dir, dvc):
 
     dvc.add("foo")
 
-    assert not os.access("foo", os.W_OK)
+    assert not os.access(
+        os.path.join(".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"),
+        os.W_OK,
+    )
+
+    # NOTE: Windows symlink perms don't propagate to the target
+    if link == "copy" or (link == "symlink" and os.name == "nt"):
+        assert os.access("foo", os.W_OK)
+    else:
+        assert not os.access("foo", os.W_OK)
 
 
 def test_escape_gitignore_entries(tmp_dir, scm, dvc):
diff --git a/tests/func/test_api.py b/tests/func/test_api.py
index 9a62edfff..00b0500ca 100644
--- a/tests/func/test_api.py
+++ b/tests/func/test_api.py
@@ -1,5 +1,4 @@
 import os
-import shutil
 
 import pytest
 
@@ -9,6 +8,8 @@ from dvc.compat import fspath
 from dvc.exceptions import FileMissingError
 from dvc.main import main
 from dvc.path_info import URLInfo
+from dvc.utils.fs import remove
+
 from tests.remotes import Azure, GCP, HDFS, Local, OSS, S3, SSH
 
 
@@ -65,7 +66,7 @@ def test_open(remote_url, tmp_dir, dvc):
     run_dvc("push")
 
     # Remove cache to force download
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     with api.open("foo") as fd:
         assert fd.read() == "foo-text"
@@ -85,7 +86,7 @@ def test_open_external(remote_url, erepo_dir):
     erepo_dir.dvc.push(all_branches=True)
 
     # Remove cache to force download
-    shutil.rmtree(erepo_dir.dvc.cache.local.cache_dir)
+    remove(erepo_dir.dvc.cache.local.cache_dir)
 
     # Using file url to force clone to tmp repo
     repo_url = "file://{}".format(erepo_dir)
@@ -101,7 +102,7 @@ def test_missing(remote_url, tmp_dir, dvc):
     run_dvc("remote", "add", "-d", "upstream", remote_url)
 
     # Remove cache to make foo missing
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     with pytest.raises(FileMissingError):
         api.read("foo")
diff --git a/tests/func/test_cache.py b/tests/func/test_cache.py
index 147840ee4..8d692938d 100644
--- a/tests/func/test_cache.py
+++ b/tests/func/test_cache.py
@@ -184,12 +184,12 @@ def test_default_cache_type(dvc):
 
 @pytest.mark.skipif(os.name == "nt", reason="Not supported for Windows.")
 @pytest.mark.parametrize(
-    "protected,dir_mode,file_mode",
-    [(False, 0o775, 0o664), (True, 0o775, 0o444)],
+    "group, dir_mode", [(False, 0o755), (True, 0o775)],
 )
-def test_shared_cache(tmp_dir, dvc, protected, dir_mode, file_mode):
-    with dvc.config.edit() as conf:
-        conf["cache"].update({"shared": "group", "protected": str(protected)})
+def test_shared_cache(tmp_dir, dvc, group, dir_mode):
+    if group:
+        with dvc.config.edit() as conf:
+            conf["cache"].update({"shared": "group"})
     dvc.cache = Cache(dvc)
 
     tmp_dir.dvc_gen(
@@ -203,4 +203,4 @@ def test_shared_cache(tmp_dir, dvc, protected, dir_mode, file_mode):
 
         for fname in fnames:
             path = os.path.join(root, fname)
-            assert stat.S_IMODE(os.stat(path).st_mode) == file_mode
+            assert stat.S_IMODE(os.stat(path).st_mode) == 0o444
diff --git a/tests/func/test_checkout.py b/tests/func/test_checkout.py
index a75367975..e6b92111a 100644
--- a/tests/func/test_checkout.py
+++ b/tests/func/test_checkout.py
@@ -72,6 +72,7 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
     def test(self):
         cache = self.foo_stage.outs[0].cache_path
 
+        os.chmod(cache, 0o644)
         with open(cache, "a") as fd:
             fd.write("1")
 
@@ -100,6 +101,7 @@ class TestCheckoutCorruptedCacheDir(TestDvc):
         checksum = entry[self.dvc.cache.local.PARAM_CHECKSUM]
         cache = self.dvc.cache.local.get(checksum)
 
+        os.chmod(cache, 0o644)
         with open(cache, "w+") as fobj:
             fobj.write("1")
 
@@ -502,7 +504,6 @@ def test_checkout_relink(tmp_dir, dvc, link, link_test_func):
 @pytest.mark.parametrize("link", ["hardlink", "symlink", "copy"])
 def test_checkout_relink_protected(tmp_dir, dvc, link):
     dvc.cache.local.cache_types = [link]
-    dvc.cache.local.protected = True
 
     tmp_dir.dvc_gen("foo", "foo")
     dvc.unprotect("foo")
@@ -510,7 +511,12 @@ def test_checkout_relink_protected(tmp_dir, dvc, link):
 
     stats = dvc.checkout(["foo.dvc"], relink=True)
     assert stats == empty_checkout
-    assert not os.access("foo", os.W_OK)
+
+    # NOTE: Windows symlink perms don't propagate to the target
+    if link == "copy" or (link == "symlink" and os.name == "nt"):
+        assert os.access("foo", os.W_OK)
+    else:
+        assert not os.access("foo", os.W_OK)
 
 
 @pytest.mark.parametrize(
@@ -582,6 +588,7 @@ def test_checkout_stats_on_failure(tmp_dir, dvc, scm):
 
     # corrupt cache
     cache = stage.outs[0].cache_path
+    os.chmod(cache, 0o644)
     with open(cache, "a") as fd:
         fd.write("destroy cache")
 
diff --git a/tests/func/test_data_cloud.py b/tests/func/test_data_cloud.py
index aaf1a12c5..590094d8a 100644
--- a/tests/func/test_data_cloud.py
+++ b/tests/func/test_data_cloud.py
@@ -22,6 +22,7 @@ from dvc.remote import RemoteS3
 from dvc.remote import RemoteSSH
 from dvc.remote.base import STATUS_DELETED, STATUS_NEW, STATUS_OK
 from dvc.utils import file_md5
+from dvc.utils.fs import remove
 from dvc.utils.stage import dump_stage_file, load_stage_file
 from tests.basic_env import TestDvc
 
@@ -149,7 +150,7 @@ class TestDataCloudBase(TestDvc):
             self.assertEqual(status_dir, expected)
 
             # Remove and check status
-            shutil.rmtree(self.dvc.cache.local.cache_dir)
+            remove(self.dvc.cache.local.cache_dir)
 
             status = self.cloud.status(info, show_checksums=True)
             expected = {md5: {"name": md5, "status": STATUS_DELETED}}
@@ -348,7 +349,7 @@ class TestDataCloudCLIBase(TestDvc):
         self.assertTrue(os.path.isfile(cache))
         self.assertTrue(os.path.isfile(cache_dir))
 
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
         self.main(["fetch"] + args)
         self.assertTrue(os.path.exists(cache))
@@ -367,7 +368,7 @@ class TestDataCloudCLIBase(TestDvc):
         self.assertTrue(os.path.isfile(cache_dir))
 
         # NOTE: check if remote gc works correctly on directories
-        self.main(["gc", "-c", "-f"] + args)
+        self.main(["gc", "-cw", "-f"] + args)
         shutil.move(
             self.dvc.cache.local.cache_dir,
             self.dvc.cache.local.cache_dir + ".back",
@@ -572,7 +573,7 @@ class TestRecursiveSyncOperations(Local, TestDataCloudBase):
         self.assertTrue(os.path.exists(self.DATA_SUB))
 
     def _clear_local_cache(self):
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
     def _test_recursive_fetch(self, data_md5, data_sub_md5):
         self._clear_local_cache()
@@ -644,7 +645,7 @@ class TestShouldWarnOnNoChecksumInLocalAndRemoteCache(TestDvc):
         self.assertEqual(0, ret)
 
         # purge cache
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
         ret = main(["remote", "add", "remote_name", "-d", cache_dir])
         self.assertEqual(0, ret)
@@ -693,9 +694,9 @@ def test_verify_checksums(tmp_dir, scm, dvc, mocker, tmp_path_factory):
     dvc.push()
 
     # remove artifacts and cache to trigger fetching
-    os.remove("file")
-    shutil.rmtree("dir")
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove("file")
+    remove("dir")
+    remove(dvc.cache.local.cache_dir)
 
     checksum_spy = mocker.spy(dvc.cache.local, "get_file_checksum")
 
@@ -703,7 +704,7 @@ def test_verify_checksums(tmp_dir, scm, dvc, mocker, tmp_path_factory):
     assert checksum_spy.call_count == 0
 
     # Removing cache will invalidate existing state entries
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     dvc.config["remote"]["local_remote"]["verify"] = True
 
diff --git a/tests/func/test_diff.py b/tests/func/test_diff.py
index 668fa5ebf..bb474cd24 100644
--- a/tests/func/test_diff.py
+++ b/tests/func/test_diff.py
@@ -1,11 +1,11 @@
 import hashlib
 import os
 import pytest
-import shutil
 
 from funcy import first
 
 from dvc.compat import fspath
+from dvc.utils.fs import remove
 from dvc.exceptions import DvcException
 
 
@@ -36,7 +36,7 @@ def test_no_cache_entry(tmp_dir, scm, dvc):
     tmp_dir.dvc_gen({"dir": {"1": "1", "2": "2"}})
     tmp_dir.dvc_gen("file", "second")
 
-    shutil.rmtree(fspath(tmp_dir / ".dvc" / "cache"))
+    remove(fspath(tmp_dir / ".dvc" / "cache"))
     (tmp_dir / ".dvc" / "state").unlink()
 
     dir_checksum = "5fb6b29836c388e093ca0715c872fe2a.dir"
@@ -167,8 +167,8 @@ def test_diff_no_cache(tmp_dir, scm, dvc):
         {"dir": {"file": "modified file content"}}, commit="second"
     )
 
-    os.remove(first(stage.outs).cache_path)
-    shutil.rmtree("dir")
+    remove(first(stage.outs).cache_path)
+    remove("dir")
 
     # invalidate_dir_info to force cache loading
     dvc.cache.local._dir_info = {}
diff --git a/tests/func/test_external_repo.py b/tests/func/test_external_repo.py
index fbbaf37fd..43b8394ad 100644
--- a/tests/func/test_external_repo.py
+++ b/tests/func/test_external_repo.py
@@ -1,5 +1,4 @@
 import os
-import shutil
 
 from mock import patch
 from dvc.compat import fspath
@@ -8,6 +7,7 @@ from dvc.external_repo import external_repo
 from dvc.scm.git import Git
 from dvc.remote import RemoteLOCAL
 from dvc.utils import relpath
+from dvc.utils.fs import remove
 
 
 def test_external_repo(erepo_dir):
@@ -110,7 +110,7 @@ def test_relative_remote(erepo_dir, tmp_dir):
     erepo_dir.dvc.push()
 
     (erepo_dir / "file").unlink()
-    shutil.rmtree(erepo_dir.dvc.cache.local.cache_dir)
+    remove(erepo_dir.dvc.cache.local.cache_dir)
 
     url = fspath(erepo_dir)
 
diff --git a/tests/func/test_gc.py b/tests/func/test_gc.py
index bb395669a..c6dba9d73 100644
--- a/tests/func/test_gc.py
+++ b/tests/func/test_gc.py
@@ -10,6 +10,7 @@ from dvc.exceptions import CollectCacheError
 from dvc.main import main
 from dvc.repo import Repo as DvcRepo
 from dvc.remote.local import RemoteLOCAL
+from dvc.utils.fs import remove
 from tests.basic_env import TestDir, TestDvcGit
 
 
@@ -195,7 +196,7 @@ def test_gc_no_dir_cache(tmp_dir, dvc):
     tmp_dir.dvc_gen({"foo": "foo", "bar": "bar"})
     (dir_stage,) = tmp_dir.dvc_gen({"dir": {"x": "x", "subdir": {"y": "y"}}})
 
-    os.unlink(dir_stage.outs[0].cache_path)
+    remove(dir_stage.outs[0].cache_path)
 
     with pytest.raises(CollectCacheError):
         dvc.gc(workspace=True)
@@ -236,6 +237,20 @@ def test_gc_without_workspace_raises_error(tmp_dir, dvc):
         dvc.gc(force=True, workspace=False)
 
 
+def test_gc_cloud_with_or_without_specifier(tmp_dir, erepo_dir):
+    dvc = erepo_dir.dvc
+    with erepo_dir.chdir():
+        from dvc.exceptions import InvalidArgumentError
+
+        with pytest.raises(InvalidArgumentError):
+            dvc.gc(force=True, cloud=True)
+
+        dvc.gc(cloud=True, all_tags=True)
+        dvc.gc(cloud=True, all_commits=True)
+        dvc.gc(cloud=True, all_branches=True)
+        dvc.gc(cloud=True, all_commits=False, all_branches=True, all_tags=True)
+
+
 def test_gc_without_workspace_on_tags_branches_commits(tmp_dir, dvc):
     dvc.gc(force=True, all_tags=True)
     dvc.gc(force=True, all_commits=True)
@@ -253,6 +268,13 @@ def test_gc_without_workspace(tmp_dir, dvc, caplog):
     assert "Invalid Arguments" in caplog.text
 
 
+def test_gc_cloud_without_any_specifier(tmp_dir, dvc, caplog):
+    with caplog.at_level(logging.WARNING, logger="dvc"):
+        assert main(["gc", "-cvf"]) == 255
+
+    assert "Invalid Arguments" in caplog.text
+
+
 def test_gc_with_possible_args_positive(tmp_dir, dvc):
     for flag in [
         "-w",
@@ -274,5 +296,5 @@ def test_gc_cloud_positive(tmp_dir, dvc, tmp_path_factory):
 
     dvc.push()
 
-    for flag in ["-c", "-ca", "-cT", "-caT", "-cwT"]:
+    for flag in ["-cw", "-ca", "-cT", "-caT", "-cwT"]:
         assert main(["gc", "-vf", flag]) == 0
diff --git a/tests/func/test_import.py b/tests/func/test_import.py
index 23ede6236..c4fa1dc17 100644
--- a/tests/func/test_import.py
+++ b/tests/func/test_import.py
@@ -1,6 +1,5 @@
 import filecmp
 import os
-import shutil
 from dvc.compat import fspath
 
 import pytest
@@ -11,7 +10,7 @@ from dvc.exceptions import DownloadError, PathMissingError
 from dvc.config import NoRemoteError
 from dvc.stage import Stage
 from dvc.system import System
-from dvc.utils.fs import makedirs
+from dvc.utils.fs import makedirs, remove
 import dvc.data_cloud as cloud
 from tests.utils import trees_equal
 
@@ -187,8 +186,8 @@ def test_pull_imported_stage(tmp_dir, dvc, erepo_dir):
     dst_stage = Stage.load(dvc, "foo_imported.dvc")
     dst_cache = dst_stage.outs[0].cache_path
 
-    os.remove("foo_imported")
-    os.remove(dst_cache)
+    remove("foo_imported")
+    remove(dst_cache)
     dvc.pull(["foo_imported.dvc"])
 
     assert os.path.isfile("foo_imported")
@@ -220,8 +219,8 @@ def test_pull_imported_directory_stage(tmp_dir, dvc, erepo_dir):
 
     dvc.imp(fspath(erepo_dir), "dir", "dir_imported")
 
-    shutil.rmtree("dir_imported")
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove("dir_imported")
+    remove(dvc.cache.local.cache_dir)
 
     dvc.pull(["dir_imported.dvc"])
 
@@ -237,8 +236,8 @@ def test_download_error_pulling_imported_stage(tmp_dir, dvc, erepo_dir):
     dst_stage = Stage.load(dvc, "foo_imported.dvc")
     dst_cache = dst_stage.outs[0].cache_path
 
-    os.remove("foo_imported")
-    os.remove(dst_cache)
+    remove("foo_imported")
+    remove(dst_cache)
 
     with patch(
         "dvc.remote.RemoteLOCAL._download", side_effect=Exception
@@ -276,7 +275,7 @@ def test_pull_non_workspace(tmp_dir, scm, dvc, erepo_dir):
     # Overwrite via import
     dvc.imp(fspath(erepo_dir), "foo", "foo_imported", rev="master")
 
-    os.remove(stage.outs[0].cache_path)
+    remove(stage.outs[0].cache_path)
     dvc.fetch(all_tags=True)
     assert os.path.exists(stage.outs[0].cache_path)
 
@@ -299,7 +298,7 @@ def test_pull_no_rev_lock(erepo_dir, tmp_dir, dvc):
     stage.deps[0].def_repo.pop("rev_lock")
     stage.dump()
 
-    os.remove(stage.outs[0].cache_path)
+    remove(stage.outs[0].cache_path)
     (tmp_dir / "foo_imported").unlink()
 
     dvc.pull([stage.path])
diff --git a/tests/func/test_remote.py b/tests/func/test_remote.py
index 7ee4546bc..f1ebd3506 100644
--- a/tests/func/test_remote.py
+++ b/tests/func/test_remote.py
@@ -1,6 +1,5 @@
 import errno
 import os
-import shutil
 
 import configobj
 import pytest
@@ -13,6 +12,7 @@ from dvc.path_info import PathInfo
 from dvc.remote import RemoteLOCAL
 from dvc.remote.base import RemoteBASE, RemoteCacheRequiredError
 from dvc.compat import fspath
+from dvc.utils.fs import remove
 from tests.basic_env import TestDvc
 from tests.remotes import Local
 
@@ -197,7 +197,7 @@ def test_partial_push_n_pull(tmp_dir, dvc, tmp_path_factory):
 
     # Push everything and delete local cache
     dvc.push()
-    shutil.rmtree(dvc.cache.local.cache_dir)
+    remove(dvc.cache.local.cache_dir)
 
     with patch.object(RemoteLOCAL, "_download", side_effect=Exception):
         with pytest.raises(DownloadError) as download_error_info:
diff --git a/tests/func/test_remove.py b/tests/func/test_remove.py
index b55b837d4..d2a622af3 100644
--- a/tests/func/test_remove.py
+++ b/tests/func/test_remove.py
@@ -1,5 +1,4 @@
 import os
-import shutil
 
 from mock import patch
 
@@ -8,6 +7,7 @@ from dvc.main import main
 from dvc.stage import Stage
 from dvc.stage import StageFileDoesNotExistError
 from dvc.system import System
+from dvc.utils.fs import remove
 from tests.basic_env import TestDvc
 
 
@@ -44,7 +44,7 @@ class TestRemoveBrokenSymlink(TestDvc):
         ret = main(["add", self.FOO])
         self.assertEqual(ret, 0)
 
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
 
         self.assertTrue(System.is_symlink(self.FOO))
 
diff --git a/tests/func/test_repro.py b/tests/func/test_repro.py
index d084de26b..17acb2433 100644
--- a/tests/func/test_repro.py
+++ b/tests/func/test_repro.py
@@ -34,6 +34,7 @@ from dvc.utils import file_md5
 from dvc.utils import relpath
 from dvc.utils.stage import dump_stage_file
 from dvc.utils.stage import load_stage_file
+from dvc.utils.fs import remove
 from tests.basic_env import TestDvc
 from tests.remotes import (
     GCP,
@@ -1274,7 +1275,7 @@ class TestReproAllPipelines(TestDvc):
 
 class TestReproNoCommit(TestRepro):
     def test(self):
-        shutil.rmtree(self.dvc.cache.local.cache_dir)
+        remove(self.dvc.cache.local.cache_dir)
         ret = main(["repro", self.file1_stage, "--no-commit"])
         self.assertEqual(ret, 0)
         self.assertFalse(os.path.exists(self.dvc.cache.local.cache_dir))
diff --git a/tests/func/test_run.py b/tests/func/test_run.py
index 1b49bc540..0f97672a6 100644
--- a/tests/func/test_run.py
+++ b/tests/func/test_run.py
@@ -304,9 +304,6 @@ class TestRunUnprotectOutsCopy(TestDvc):
             fobj.write("with open(sys.argv[1], 'a+') as fobj:\n")
             fobj.write("    fobj.write('foo')\n")
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["config", "cache.type", "copy"])
         self.assertEqual(ret, 0)
 
@@ -323,7 +320,7 @@ class TestRunUnprotectOutsCopy(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertTrue(os.access(self.FOO, os.W_OK))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
 
@@ -342,7 +339,7 @@ class TestRunUnprotectOutsCopy(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertTrue(os.access(self.FOO, os.W_OK))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
 
@@ -355,9 +352,6 @@ class TestRunUnprotectOutsSymlink(TestDvc):
             fobj.write("with open(sys.argv[1], 'a+') as fobj:\n")
             fobj.write("    fobj.write('foo')\n")
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["config", "cache.type", "symlink"])
         self.assertEqual(ret, 0)
 
@@ -375,7 +369,13 @@ class TestRunUnprotectOutsSymlink(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+
+        if os.name == "nt":
+            # NOTE: Windows symlink perms don't propagate to the target
+            self.assertTrue(os.access(self.FOO, os.W_OK))
+        else:
+            self.assertFalse(os.access(self.FOO, os.W_OK))
+
         self.assertTrue(System.is_symlink(self.FOO))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
@@ -395,7 +395,13 @@ class TestRunUnprotectOutsSymlink(TestDvc):
             ]
         )
         self.assertEqual(ret, 0)
-        self.assertFalse(os.access(self.FOO, os.W_OK))
+
+        if os.name == "nt":
+            # NOTE: Windows symlink perms don't propagate to the target
+            self.assertTrue(os.access(self.FOO, os.W_OK))
+        else:
+            self.assertFalse(os.access(self.FOO, os.W_OK))
+
         self.assertTrue(System.is_symlink(self.FOO))
         with open(self.FOO, "r") as fd:
             self.assertEqual(fd.read(), "foo")
@@ -409,9 +415,6 @@ class TestRunUnprotectOutsHardlink(TestDvc):
             fobj.write("with open(sys.argv[1], 'a+') as fobj:\n")
             fobj.write("    fobj.write('foo')\n")
 
-        ret = main(["config", "cache.protected", "true"])
-        self.assertEqual(ret, 0)
-
         ret = main(["config", "cache.type", "hardlink"])
         self.assertEqual(ret, 0)
 
@@ -848,6 +851,7 @@ class TestShouldNotCheckoutUponCorruptedLocalHardlinkCache(TestDvc):
         cmd = "python {} {} {}".format(self.CODE, self.FOO, self.BAR)
         stage = self.dvc.run(deps=[self.FOO], outs=[self.BAR], cmd=cmd)
 
+        os.chmod(self.BAR, 0o644)
         with open(self.BAR, "w") as fd:
             fd.write("corrupting the output cache")
 
diff --git a/tests/func/test_unprotect.py b/tests/func/test_unprotect.py
index 5e504b214..782ee7c5c 100644
--- a/tests/func/test_unprotect.py
+++ b/tests/func/test_unprotect.py
@@ -1,5 +1,4 @@
 import os
-import stat
 
 from dvc.main import main
 from tests.basic_env import TestDvc
@@ -7,28 +6,29 @@ from tests.basic_env import TestDvc
 
 class TestUnprotect(TestDvc):
     def test(self):
-        ret = main(["config", "cache.protected", "true"])
+        ret = main(["config", "cache.type", "hardlink"])
         self.assertEqual(ret, 0)
 
         ret = main(["add", self.FOO])
         self.assertEqual(ret, 0)
 
-        self.assertNotEqual(
-            os.stat(self.FOO).st_mode
-            & (stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH),
-            0,
-        )
-        self.assertEqual(
-            os.stat(self.FOO).st_mode
-            & (stat.S_IWRITE | stat.S_IWGRP | stat.S_IWOTH),
-            0,
+        cache = os.path.join(
+            ".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"
         )
 
+        self.assertFalse(os.access(self.FOO, os.W_OK))
+        self.assertFalse(os.access(cache, os.W_OK))
+
         ret = main(["unprotect", self.FOO])
         self.assertEqual(ret, 0)
 
-        self.assertNotEqual(
-            os.stat(self.FOO).st_mode
-            & (stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH | stat.S_IWRITE),
-            0,
-        )
+        self.assertTrue(os.access(self.FOO, os.W_OK))
+
+        # NOTE: cache is now unprotected, bceause we try to set write perms
+        # on files that we try to delete, as some filesystems require that
+        # (e.g. NTFS). But it should be restored after the next cache check,
+        # hence why we call `dvc status` here.
+        self.assertTrue(os.access(cache, os.W_OK))
+        ret = main(["status"])
+        self.assertEqual(ret, 0)
+        self.assertFalse(os.access(cache, os.W_OK))
diff --git a/tests/unit/remote/test_local.py b/tests/unit/remote/test_local.py
index 461e37b79..d18e3b4fb 100644
--- a/tests/unit/remote/test_local.py
+++ b/tests/unit/remote/test_local.py
@@ -1,4 +1,9 @@
+import os
+
+import pytest
+
 from dvc.cache import NamedCache
+from dvc.path_info import PathInfo
 from dvc.remote.local import RemoteLOCAL
 
 
@@ -23,3 +28,33 @@ def test_status_download_optimization(mocker):
     remote.status(infos, other_remote, download=True)
 
     assert other_remote.cache_exists.call_count == 0
+
+
+@pytest.mark.parametrize("link_name", ["hardlink", "symlink"])
+def test_is_protected(tmp_dir, link_name):
+    remote = RemoteLOCAL(None, {})
+    link_method = getattr(remote, link_name)
+
+    (tmp_dir / "foo").write_text("foo")
+
+    foo = PathInfo(tmp_dir / "foo")
+    link = PathInfo(tmp_dir / "link")
+
+    link_method(foo, link)
+
+    assert not remote.is_protected(foo)
+    assert not remote.is_protected(link)
+
+    remote.protect(foo)
+
+    assert remote.is_protected(foo)
+    assert remote.is_protected(link)
+
+    remote.unprotect(link)
+
+    assert not remote.is_protected(link)
+    if link_name == "symlink" and os.name == "nt":
+        # NOTE: Windows symlink perms don't propagate to the target
+        assert remote.is_protected(foo)
+    else:
+        assert not remote.is_protected(foo)

pip install -e .[all]
parse_log_pytest
pytest
pytest pytest-timeout pytest-cov pytest-xdist pytest-mock flaky mock xmltodict awscli google-compute-engine Pygments collective.checkdocs flake8 psutil flake8-docstrings pydocstyle jaraco.windows mock-ssh-server moto rangehttpserver
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aliyun-python-sdk-core==2.16.0
aliyun-python-sdk-core-v3==2.13.33
aliyun-python-sdk-kms==2.16.5
appdirs==1.4.4
atpublic==3.1.2
attrs @ file:///croot/attrs_1668696182826/work
autocommand==2.2.2
awscli==1.31.13
azure-common==1.1.28
azure-storage-blob==2.1.0
azure-storage-common==2.1.0
bcrypt==4.2.1
boto==2.49.0
boto3==1.33.13
botocore==1.33.13
cachetools==4.2.4
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
collective.checkdocs==0.2
colorama==0.4.4
configobj==5.0.9
coverage==7.2.7
crcmod==1.7
cryptography==44.0.2
decorator==5.1.1
distro==1.9.0
docutils==0.16
-e git+https://github.com/iterative/dvc.git@e1334448ea75090e57751fb5ebdc6446d6397c77#egg=dvc
execnet==2.0.2
flake8==5.0.4
flake8-docstrings==1.7.0
flaky==3.8.1
flatten-json==0.1.14
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
flufl.lock==7.1.1
funcy==2.0
future==1.0.0
gitdb==4.0.12
GitPython==3.1.44
google-api-core==2.10.2
google-api-python-client==2.166.0
google-auth==1.35.0
google-auth-httplib2==0.2.0
google-cloud-core==1.7.3
google-cloud-storage==1.19.0
google-compute-engine==2.8.13
google-crc32c==1.5.0
google-resumable-media==2.7.2
googleapis-common-protos==1.69.2
grandalf==0.6
httplib2==0.22.0
humanize==4.6.0
idna==3.10
importlib-metadata==4.2.0
importlib-resources==5.12.0
inflect==3.0.2
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
jaraco.classes==3.2.3
jaraco.collections==4.2.0
jaraco.context==4.3.0
jaraco.functools==3.7.0
jaraco.structures==2.1.0
jaraco.text==3.11.1
jaraco.ui==2.3.0
jaraco.windows==5.7.0
Jinja2==3.1.6
jmespath==0.10.0
jsonpath-ng==1.7.0
MarkupSafe==2.1.5
mccabe==0.7.0
mock==5.2.0
mock-ssh-server==0.9.1
more-itertools==9.1.0
moto==4.2.14
nanotime==0.5.2
networkx==2.3
numpy==1.21.6
oauth2client==4.1.3
oss2==2.6.1
packaging @ file:///croot/packaging_1671697413597/work
paramiko==3.5.1
path==16.6.0
pathspec==0.11.2
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
ply==3.11
protobuf==4.24.4
psutil==7.0.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyarrow==0.15.1
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycodestyle==2.9.1
pycparser==2.21
pycryptodome==3.22.0
pydantic==1.10.21
pydocstyle==6.3.0
pydot==2.0.0
PyDrive2==1.15.4
pyflakes==2.5.0
Pygments==2.17.2
pygtrie==2.3.2
PyNaCl==1.5.0
pyOpenSSL==25.0.0
pyparsing==3.1.4
pytest==7.1.2
pytest-cov==4.1.0
pytest-mock==3.11.1
pytest-timeout==2.3.1
pytest-xdist==3.5.0
python-dateutil==2.8.0
PyYAML==5.1.2
rangehttpserver==1.4.0
requests==2.31.0
responses==0.23.3
rsa==4.7.2
ruamel.yaml==0.18.10
ruamel.yaml.clib==0.2.8
s3transfer==0.8.2
shortuuid==1.0.13
six==1.17.0
smmap==5.0.2
snowballstemmer==2.2.0
texttable==1.7.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tqdm==4.67.1
treelib==1.7.1
types-PyYAML==6.0.12.12
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
uritemplate==4.1.1
urllib3==1.26.20
voluptuous==0.14.1
Werkzeug==2.2.3
xmltodict==0.14.2
zc.lockfile==3.0.post1
zipp @ file:///croot/zipp_1672387121353/work

name: dvc
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aliyun-python-sdk-core==2.16.0
      - aliyun-python-sdk-core-v3==2.13.33
      - aliyun-python-sdk-kms==2.16.5
      - appdirs==1.4.4
      - atpublic==3.1.2
      - autocommand==2.2.2
      - awscli==1.31.13
      - azure-common==1.1.28
      - azure-storage-blob==2.1.0
      - azure-storage-common==2.1.0
      - bcrypt==4.2.1
      - boto==2.49.0
      - boto3==1.33.13
      - botocore==1.33.13
      - cachetools==4.2.4
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - collective-checkdocs==0.2
      - colorama==0.4.4
      - configobj==5.0.9
      - coverage==7.2.7
      - crcmod==1.7
      - cryptography==44.0.2
      - decorator==5.1.1
      - distro==1.9.0
      - docutils==0.16
      - dvc==0.89.0+e13344
      - execnet==2.0.2
      - flake8==5.0.4
      - flake8-docstrings==1.7.0
      - flaky==3.8.1
      - flatten-json==0.1.14
      - flufl-lock==7.1.1
      - funcy==2.0
      - future==1.0.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - google-api-core==2.10.2
      - google-api-python-client==2.166.0
      - google-auth==1.35.0
      - google-auth-httplib2==0.2.0
      - google-cloud-core==1.7.3
      - google-cloud-storage==1.19.0
      - google-compute-engine==2.8.13
      - google-crc32c==1.5.0
      - google-resumable-media==2.7.2
      - googleapis-common-protos==1.69.2
      - grandalf==0.6
      - httplib2==0.22.0
      - humanize==4.6.0
      - idna==3.10
      - importlib-metadata==4.2.0
      - importlib-resources==5.12.0
      - inflect==3.0.2
      - jaraco-classes==3.2.3
      - jaraco-collections==4.2.0
      - jaraco-context==4.3.0
      - jaraco-functools==3.7.0
      - jaraco-structures==2.1.0
      - jaraco-text==3.11.1
      - jaraco-ui==2.3.0
      - jaraco-windows==5.7.0
      - jinja2==3.1.6
      - jmespath==0.10.0
      - jsonpath-ng==1.7.0
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - mock==5.2.0
      - mock-ssh-server==0.9.1
      - more-itertools==9.1.0
      - moto==4.2.14
      - nanotime==0.5.2
      - networkx==2.3
      - numpy==1.21.6
      - oauth2client==4.1.3
      - oss2==2.6.1
      - paramiko==3.5.1
      - path==16.6.0
      - pathspec==0.11.2
      - ply==3.11
      - protobuf==4.24.4
      - psutil==7.0.0
      - pyarrow==0.15.1
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pycryptodome==3.22.0
      - pydantic==1.10.21
      - pydocstyle==6.3.0
      - pydot==2.0.0
      - pydrive2==1.15.4
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pygtrie==2.3.2
      - pynacl==1.5.0
      - pyopenssl==25.0.0
      - pyparsing==3.1.4
      - pytest-cov==4.1.0
      - pytest-mock==3.11.1
      - pytest-timeout==2.3.1
      - pytest-xdist==3.5.0
      - python-dateutil==2.8.0
      - pyyaml==5.1.2
      - rangehttpserver==1.4.0
      - requests==2.31.0
      - responses==0.23.3
      - rsa==4.7.2
      - ruamel-yaml==0.18.10
      - ruamel-yaml-clib==0.2.8
      - s3transfer==0.8.2
      - shortuuid==1.0.13
      - six==1.17.0
      - smmap==5.0.2
      - snowballstemmer==2.2.0
      - texttable==1.7.0
      - tqdm==4.67.1
      - treelib==1.7.1
      - types-pyyaml==6.0.12.12
      - uritemplate==4.1.1
      - urllib3==1.26.20
      - voluptuous==0.14.1
      - werkzeug==2.2.3
      - xmltodict==0.14.2
      - zc-lockfile==3.0.post1
prefix: /opt/conda/envs/dvc

tests/func/test_cache.py::test_shared_cache[False-493] tests/func/test_cache.py::test_shared_cache[True-509] tests/func/test_gc.py::test_gc_cloud_with_or_without_specifier tests/func/test_gc.py::test_gc_cloud_without_any_specifier tests/unit/remote/test_local.py::test_is_protected[hardlink] tests/unit/remote/test_local.py::test_is_protected[symlink]
tests/func/test_add.py::TestAddUnprotected::test tests/func/test_add.py::test_should_protect_on_repeated_add[hardlink] tests/func/test_add.py::test_should_protect_on_repeated_add[symlink] tests/func/test_add.py::test_should_protect_on_repeated_add[copy] tests/func/test_checkout.py::test_checkout_relink_protected[hardlink] tests/func/test_checkout.py::test_checkout_relink_protected[symlink] tests/func/test_repro.py::TestReproShell::test tests/func/test_run.py::TestRunUnprotectOutsSymlink::test tests/func/test_run.py::TestRunUnprotectOutsHardlink::test tests/func/test_unprotect.py::TestUnprotect::test
tests/func/remote/test_local.py::test_dont_fail_on_unpacked_create_fail tests/func/remote/test_local.py::test_remove_unpacked_on_create_fail tests/func/remote/test_local.py::test_create_unpacked_on_status tests/func/remote/test_local.py::test_dir_cache_changed_on_single_cache_file_modification tests/func/test_add.py::test_add tests/func/test_add.py::test_add_unicode tests/func/test_add.py::test_add_unsupported_file tests/func/test_add.py::test_add_directory tests/func/test_add.py::TestAddDirectoryRecursive::test tests/func/test_add.py::TestAddCmdDirectoryRecursive::test tests/func/test_add.py::TestAddCmdDirectoryRecursive::test_warn_about_large_directories tests/func/test_add.py::TestAddDirectoryWithForwardSlash::test tests/func/test_add.py::test_add_tracked_file tests/func/test_add.py::TestAddDirWithExistingCache::test tests/func/test_add.py::TestAddModifiedDir::test tests/func/test_add.py::test_add_file_in_dir tests/func/test_add.py::TestAddExternalLocalFile::test tests/func/test_add.py::TestAddLocalRemoteFile::test tests/func/test_add.py::TestCmdAdd::test tests/func/test_add.py::TestDoubleAddUnchanged::test_dir tests/func/test_add.py::TestDoubleAddUnchanged::test_file tests/func/test_add.py::test_should_update_state_entry_for_file_after_add tests/func/test_add.py::test_should_update_state_entry_for_directory_after_add tests/func/test_add.py::TestAddCommit::test tests/func/test_add.py::test_should_collect_dir_cache_only_once tests/func/test_add.py::TestShouldAddDataFromExternalSymlink::test tests/func/test_add.py::TestShouldAddDataFromInternalSymlink::test tests/func/test_add.py::TestShouldPlaceStageInDataDirIfRepositoryBelowSymlink::test tests/func/test_add.py::TestShouldThrowProperExceptionOnCorruptedStageFile::test tests/func/test_add.py::TestAddFilename::test tests/func/test_add.py::test_failed_add_cleanup tests/func/test_add.py::test_should_not_track_git_internal_files tests/func/test_add.py::test_readding_dir_should_not_unprotect_all tests/func/test_add.py::test_should_not_checkout_when_adding_cached_copy tests/func/test_add.py::test_should_relink_on_repeated_add[hardlink-copy-<lambda>] tests/func/test_add.py::test_should_relink_on_repeated_add[symlink-copy-<lambda>] tests/func/test_add.py::test_should_relink_on_repeated_add[copy-hardlink-is_hardlink] tests/func/test_add.py::test_should_relink_on_repeated_add[copy-symlink-is_symlink] tests/func/test_add.py::test_escape_gitignore_entries tests/func/test_add.py::test_add_from_data_dir tests/func/test_add.py::test_not_raises_on_re_add tests/func/test_add.py::test_add_empty_files[hardlink] tests/func/test_add.py::test_add_empty_files[symlink] tests/func/test_add.py::test_add_empty_files[copy] tests/func/test_add.py::test_add_optimization_for_hardlink_on_empty_files tests/func/test_api.py::test_get_url_requires_dvc tests/func/test_api.py::test_open[Local] tests/func/test_api.py::test_open_external[Local] tests/func/test_api.py::test_missing[Local] tests/func/test_api.py::test_open_scm_controlled tests/func/test_api.py::test_open_not_cached tests/func/test_cache.py::TestCache::test_all tests/func/test_cache.py::TestCache::test_get tests/func/test_cache.py::TestCacheLoadBadDirCache::test tests/func/test_cache.py::TestExternalCacheDir::test tests/func/test_cache.py::TestExternalCacheDir::test_remote_references tests/func/test_cache.py::TestSharedCacheDir::test tests/func/test_cache.py::TestCacheLinkType::test tests/func/test_cache.py::TestCmdCacheDir::test tests/func/test_cache.py::TestCmdCacheDir::test_abs_path tests/func/test_cache.py::TestCmdCacheDir::test_relative_path tests/func/test_cache.py::test_default_cache_type tests/func/test_checkout.py::TestCheckout::test tests/func/test_checkout.py::TestCheckoutSingleStage::test tests/func/test_checkout.py::TestCheckoutCorruptedCacheFile::test tests/func/test_checkout.py::TestCheckoutCorruptedCacheDir::test tests/func/test_checkout.py::TestCmdCheckout::test tests/func/test_checkout.py::TestRemoveFilesWhenCheckout::test tests/func/test_checkout.py::TestCheckoutCleanWorkingDir::test tests/func/test_checkout.py::TestCheckoutCleanWorkingDir::test_force tests/func/test_checkout.py::TestCheckoutSelectiveRemove::test tests/func/test_checkout.py::TestGitIgnoreBasic::test tests/func/test_checkout.py::TestGitIgnoreWhenCheckout::test tests/func/test_checkout.py::TestCheckoutMissingMd5InStageFile::test tests/func/test_checkout.py::TestCheckoutEmptyDir::test tests/func/test_checkout.py::TestCheckoutNotCachedFile::test tests/func/test_checkout.py::TestCheckoutWithDeps::test tests/func/test_checkout.py::TestCheckoutDirectory::test tests/func/test_checkout.py::TestCheckoutHook::test tests/func/test_checkout.py::TestCheckoutSuggestGit::test tests/func/test_checkout.py::TestCheckoutTargetRecursiveShouldNotRemoveOtherUsedFiles::test tests/func/test_checkout.py::TestCheckoutRecursiveNotDirectory::test tests/func/test_checkout.py::TestCheckoutMovedCacheDirWithSymlinks::test tests/func/test_checkout.py::test_checkout_no_checksum tests/func/test_checkout.py::test_checkout_relink[hardlink-is_hardlink] tests/func/test_checkout.py::test_checkout_relink[symlink-is_symlink] tests/func/test_checkout.py::test_checkout_relink_protected[copy] tests/func/test_checkout.py::test_partial_checkout[dir/subdir] tests/func/test_checkout.py::test_partial_checkout[dir/subdir/file] tests/func/test_checkout.py::test_stats_on_empty_checkout tests/func/test_checkout.py::test_stats_on_checkout tests/func/test_checkout.py::test_checkout_stats_on_failure tests/func/test_checkout.py::test_stats_on_added_file_from_tracked_dir tests/func/test_checkout.py::test_stats_on_updated_file_from_tracked_dir tests/func/test_checkout.py::test_stats_on_removed_file_from_tracked_dir tests/func/test_checkout.py::test_stats_on_show_changes_does_not_show_summary tests/func/test_checkout.py::test_stats_does_not_show_changes_by_default tests/func/test_checkout.py::test_checkout_with_relink_existing[hardlink] tests/func/test_checkout.py::test_checkout_with_relink_existing[symlink] tests/func/test_checkout.py::test_checkout_with_relink_existing[copy] tests/func/test_checkout.py::test_checkout_with_deps tests/func/test_checkout.py::test_checkout_recursive tests/func/test_data_cloud.py::TestDataCloud::test tests/func/test_data_cloud.py::TestRemoteLOCAL::test tests/func/test_data_cloud.py::TestRemoteSSHMocked::test tests/func/test_data_cloud.py::TestRemoteHTTP::test tests/func/test_data_cloud.py::TestRemoteLOCALCLI::test tests/func/test_data_cloud.py::TestDataCloudErrorCLI::test_error tests/func/test_data_cloud.py::TestWarnOnOutdatedStage::test tests/func/test_data_cloud.py::TestRecursiveSyncOperations::test tests/func/test_data_cloud.py::test_checksum_recalculation tests/func/test_data_cloud.py::TestShouldWarnOnNoChecksumInLocalAndRemoteCache::test tests/func/test_data_cloud.py::test_verify_checksums tests/func/test_diff.py::test_no_scm tests/func/test_diff.py::test_added tests/func/test_diff.py::test_no_cache_entry tests/func/test_diff.py::test_deleted tests/func/test_diff.py::test_modified tests/func/test_diff.py::test_refs tests/func/test_diff.py::test_directories tests/func/test_diff.py::test_diff_no_cache tests/func/test_external_repo.py::test_external_repo tests/func/test_external_repo.py::test_source_change tests/func/test_external_repo.py::test_cache_reused tests/func/test_external_repo.py::test_known_sha tests/func/test_external_repo.py::test_pull_subdir_file tests/func/test_external_repo.py::test_relative_remote tests/func/test_gc.py::TestGC::test_api tests/func/test_gc.py::TestGC::test_cli tests/func/test_gc.py::TestGCBranchesTags::test tests/func/test_gc.py::TestGCMultipleDvcRepos::test tests/func/test_gc.py::test_all_commits tests/func/test_gc.py::test_gc_no_dir_cache tests/func/test_gc.py::test_gc_no_unpacked_dir tests/func/test_gc.py::test_gc_without_workspace_raises_error tests/func/test_gc.py::test_gc_without_workspace_on_tags_branches_commits tests/func/test_gc.py::test_gc_without_workspace tests/func/test_gc.py::test_gc_with_possible_args_positive tests/func/test_gc.py::test_gc_cloud_positive tests/func/test_import.py::test_import tests/func/test_import.py::test_import_git_file[True] tests/func/test_import.py::test_import_git_file[False] tests/func/test_import.py::test_import_cached_file tests/func/test_import.py::test_import_git_dir[True] tests/func/test_import.py::test_import_git_dir[False] tests/func/test_import.py::test_import_dir tests/func/test_import.py::test_import_file_from_dir tests/func/test_import.py::test_import_non_cached tests/func/test_import.py::test_import_rev tests/func/test_import.py::test_pull_imported_stage tests/func/test_import.py::test_cache_type_is_properly_overridden tests/func/test_import.py::test_pull_imported_directory_stage tests/func/test_import.py::test_download_error_pulling_imported_stage tests/func/test_import.py::test_import_to_dir[.] tests/func/test_import.py::test_import_to_dir[dir] tests/func/test_import.py::test_import_to_dir[dir/subdir] tests/func/test_import.py::test_pull_non_workspace tests/func/test_import.py::test_import_non_existing tests/func/test_import.py::test_pull_no_rev_lock tests/func/test_remote.py::TestRemote::test tests/func/test_remote.py::TestRemote::test_overwrite tests/func/test_remote.py::TestRemote::test_referencing_other_remotes tests/func/test_remote.py::TestRemote::test_relative_path tests/func/test_remote.py::test_remove_default tests/func/test_remote.py::TestRemoteRemove::test tests/func/test_remote.py::TestRemoteDefault::test tests/func/test_remote.py::test_show_default tests/func/test_remote.py::TestRemoteShouldHandleUppercaseRemoteName::test tests/func/test_remote.py::test_dir_checksum_should_be_key_order_agnostic tests/func/test_remote.py::test_partial_push_n_pull tests/func/test_remote.py::test_raise_on_too_many_open_files tests/func/test_remote.py::test_modify_missing_remote tests/func/test_remote.py::test_external_dir_resource_on_no_cache tests/func/test_remove.py::TestRemove::test tests/func/test_remove.py::TestRemoveNonExistentFile::test tests/func/test_remove.py::TestRemoveBrokenSymlink::test tests/func/test_remove.py::TestRemoveDirectory::test tests/func/test_remove.py::TestCmdRemove::test tests/func/test_remove.py::TestRemovePurge::test tests/func/test_remove.py::TestRemovePurge::test_force tests/func/test_repro.py::TestReproFail::test tests/func/test_repro.py::TestReproCyclicGraph::test tests/func/test_repro.py::TestReproWorkingDirectoryAsOutput::test tests/func/test_repro.py::TestReproWorkingDirectoryAsOutput::test_nested tests/func/test_repro.py::TestReproWorkingDirectoryAsOutput::test_similar_paths tests/func/test_repro.py::TestReproDepUnderDir::test tests/func/test_repro.py::TestReproDepDirWithOutputsUnderIt::test tests/func/test_repro.py::TestReproNoDeps::test tests/func/test_repro.py::TestReproForce::test tests/func/test_repro.py::TestReproChangedCode::test tests/func/test_repro.py::TestReproChangedData::test tests/func/test_repro.py::TestReproDry::test tests/func/test_repro.py::TestReproUpToDate::test tests/func/test_repro.py::TestReproDryNoExec::test tests/func/test_repro.py::TestReproChangedDeepData::test tests/func/test_repro.py::TestReproIgnoreBuildCache::test tests/func/test_repro.py::TestReproPipeline::test tests/func/test_repro.py::TestReproPipeline::test_cli tests/func/test_repro.py::TestReproPipelines::test tests/func/test_repro.py::TestReproPipelines::test_cli tests/func/test_repro.py::TestReproLocked::test tests/func/test_repro.py::TestReproLocked::test_non_existing tests/func/test_repro.py::TestReproLockedCallback::test tests/func/test_repro.py::TestReproLockedUnchanged::test tests/func/test_repro.py::TestReproMetricsAddUnchanged::test tests/func/test_repro.py::TestReproPhony::test tests/func/test_repro.py::TestNonExistingOutput::test tests/func/test_repro.py::TestReproDataSource::test tests/func/test_repro.py::TestReproChangedDir::test tests/func/test_repro.py::TestReproChangedDirData::test tests/func/test_repro.py::TestReproMissingMd5InStageFile::test tests/func/test_repro.py::TestCmdRepro::test tests/func/test_repro.py::TestCmdReproChdirCwdBackwardCompatible::test tests/func/test_repro.py::TestCmdReproChdir::test tests/func/test_repro.py::TestReproExternalLOCAL::test tests/func/test_repro.py::TestReproExternalHTTP::test tests/func/test_repro.py::TestReproAllPipelines::test tests/func/test_repro.py::TestReproNoCommit::test tests/func/test_repro.py::TestReproAlreadyCached::test tests/func/test_repro.py::TestReproAlreadyCached::test_force_import tests/func/test_repro.py::TestReproAlreadyCached::test_force_with_dependencies tests/func/test_repro.py::TestShouldDisplayMetricsOnReproWithMetricsOption::test tests/func/test_repro.py::test_recursive_repro_default tests/func/test_repro.py::test_recursive_repro_single tests/func/test_repro.py::test_recursive_repro_single_force tests/func/test_repro.py::test_recursive_repro_empty_dir tests/func/test_repro.py::test_recursive_repro_recursive_missing_file tests/func/test_repro.py::test_recursive_repro_on_stage_file tests/func/test_repro.py::test_dvc_formatting_retained tests/func/test_repro.py::TestReproDownstream::test tests/func/test_repro.py::test_ssh_dir_out tests/func/test_run.py::TestRun::test tests/func/test_run.py::TestRunEmpty::test tests/func/test_run.py::TestRunMissingDep::test tests/func/test_run.py::TestRunBadStageFilename::test tests/func/test_run.py::TestRunNoExec::test tests/func/test_run.py::TestRunCircularDependency::test tests/func/test_run.py::TestRunCircularDependency::test_graph tests/func/test_run.py::TestRunCircularDependency::test_non_normalized_paths tests/func/test_run.py::TestRunCircularDependency::test_outs_no_cache tests/func/test_run.py::TestRunDuplicatedArguments::test tests/func/test_run.py::TestRunDuplicatedArguments::test_non_normalized_paths tests/func/test_run.py::TestRunDuplicatedArguments::test_outs_no_cache tests/func/test_run.py::TestRunStageInsideOutput::test_cwd tests/func/test_run.py::TestRunStageInsideOutput::test_file_name tests/func/test_run.py::TestRunBadCwd::test tests/func/test_run.py::TestRunBadCwd::test_same_prefix tests/func/test_run.py::TestRunBadWdir::test tests/func/test_run.py::TestRunBadWdir::test_not_dir tests/func/test_run.py::TestRunBadWdir::test_not_found tests/func/test_run.py::TestRunBadWdir::test_same_prefix tests/func/test_run.py::TestRunBadName::test tests/func/test_run.py::TestRunBadName::test_not_found tests/func/test_run.py::TestRunBadName::test_same_prefix tests/func/test_run.py::TestRunRemoveOuts::test tests/func/test_run.py::TestRunUnprotectOutsCopy::test tests/func/test_run.py::TestCmdRunOverwrite::test tests/func/test_run.py::TestCmdRunCliMetrics::test_cached tests/func/test_run.py::TestCmdRunCliMetrics::test_not_cached tests/func/test_run.py::TestCmdRunWorkingDirectory::test_cwd_is_ignored tests/func/test_run.py::TestCmdRunWorkingDirectory::test_default_wdir_is_not_written tests/func/test_run.py::TestCmdRunWorkingDirectory::test_fname_changes_path_and_wdir tests/func/test_run.py::test_rerun_deterministic tests/func/test_run.py::test_rerun_deterministic_ignore_cache tests/func/test_run.py::test_rerun_callback tests/func/test_run.py::test_rerun_changed_dep tests/func/test_run.py::test_rerun_changed_stage tests/func/test_run.py::test_rerun_changed_out tests/func/test_run.py::TestRunCommit::test tests/func/test_run.py::TestRunPersistOuts::test tests/func/test_run.py::TestRunPersistOutsNoCache::test tests/func/test_run.py::TestShouldRaiseOnOverlappingOutputPaths::test tests/func/test_run.py::TestNewRunShouldRemoveOutsOnNoPersist::test tests/func/test_run.py::TestNewRunShouldNotRemoveOutsOnPersist::test tests/func/test_run.py::TestShouldNotCheckoutUponCorruptedLocalHardlinkCache::test tests/func/test_run.py::TestPersistentOutput::test_ignore_build_cache tests/func/test_run.py::test_bad_stage_fname tests/func/test_run.py::test_should_raise_on_stage_dependency tests/func/test_run.py::test_should_raise_on_stage_output tests/unit/remote/test_local.py::test_status_download_optimization
dvc/config.py dvc/repo/gc.py dvc/command/unprotect.py .github/PULL_REQUEST_TEMPLATE.md dvc/remote/ssh/__init__.py dvc/remote/base.py dvc/remote/local.py dvc/command/gc.py
dvc/config.py dvc/repo/gc.py dvc/command/unprotect.py .github/PULL_REQUEST_TEMPLATE.md dvc/remote/ssh/__init__.py dvc/remote/base.py dvc/remote/local.py dvc/command/gc.py