Repository: iterative/dvc

Issue:
Add a way to suppress dvc changes to .gitignore
At the moment, DVC make changes to the .gitignore after commands like `dvc add`. There are cases, like mine, where it's not needed as .gitignore already exclude whole directory where data is kept.
Add a configuration that allow to suppress dvc changes to .gitignore.

Discussion:
efiop: @Aljo-Rovco Thank for the PR! :pray: Looks great! Please see my comment above and also please install `pre-commit` hooks as described in https://dvc.org/doc/user-guide/contributing/core to check the formatting.

Patch:
diff --git a/dvc/rwlock.py b/dvc/rwlock.py
index c9d15eae2..6b1d4de14 100644
--- a/dvc/rwlock.py
+++ b/dvc/rwlock.py
@@ -66,10 +66,11 @@ def _infos_to_str(infos):
 def _check_blockers(lock, info, *, mode, waiters):
     for path_info in waiters:
         blockers = [
-            info
+            blocker
             for path, infos in lock[mode].items()
             if path_info.overlaps(path)
-            if info not in (infos if type(infos) is list else [infos])
+            for blocker in (infos if isinstance(infos, list) else [infos])
+            if blocker != info
         ]
 
         if not blockers:
diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py
index 79fbdb360..ac63854ff 100644
--- a/dvc/scm/git/__init__.py
+++ b/dvc/scm/git/__init__.py
@@ -18,7 +18,6 @@ from dvc.utils import is_binary
 from dvc.utils import relpath
 from dvc.utils.fs import path_isin
 
-
 logger = logging.getLogger(__name__)
 
 
@@ -133,20 +132,19 @@ class Git(Base):
 
         return entry, gitignore
 
-    @staticmethod
-    def _ignored(entry, gitignore_path):
-        if os.path.exists(gitignore_path):
-            with open(gitignore_path, "r") as fobj:
-                ignore_list = fobj.readlines()
-            return any(
-                filter(lambda x: x.strip() == entry.strip(), ignore_list)
-            )
-        return False
+    def _ignored(self, path):
+        from git.exc import GitCommandError
+
+        try:
+            self.repo.git.check_ignore(path)
+            return True
+        except GitCommandError:
+            return False
 
     def ignore(self, path):
         entry, gitignore = self._get_gitignore(path)
 
-        if self._ignored(entry, gitignore):
+        if self._ignored(path):
             return
 
         msg = "Adding '{}' to '{}'.".format(relpath(path), relpath(gitignore))


Test patch:
diff --git a/tests/dir_helpers.py b/tests/dir_helpers.py
index 4fc44e211..19580bbf6 100644
--- a/tests/dir_helpers.py
+++ b/tests/dir_helpers.py
@@ -45,6 +45,7 @@ from global repo template to creating everything inplace, which:
 
 import os
 import pathlib
+import logging
 from contextlib import contextmanager
 
 import pytest
@@ -65,6 +66,11 @@ __all__ = [
 ]
 
 
+# see https://github.com/iterative/dvc/issues/3167
+git_logger = logging.getLogger("git")
+git_logger.setLevel(logging.CRITICAL)
+
+
 class TmpDir(pathlib.Path):
     def __new__(cls, *args, **kwargs):
         if cls is TmpDir:
diff --git a/tests/func/test_get.py b/tests/func/test_get.py
index 45f839355..aa6f57642 100644
--- a/tests/func/test_get.py
+++ b/tests/func/test_get.py
@@ -163,11 +163,7 @@ def test_get_from_non_dvc_master(tmp_dir, git_dir, caplog):
 
     caplog.clear()
 
-    # removing `git` import in conftest resulted in unexpected logs from
-    # that package, see https://github.com/iterative/dvc/issues/3167
-    with caplog.at_level(logging.INFO, logger="git"), caplog.at_level(
-        logging.INFO, logger="dvc"
-    ):
+    with caplog.at_level(logging.INFO, logger="dvc"):
         Repo.get(fspath(git_dir), "some_file", out="some_dst", rev="branch")
 
     assert caplog.text == ""
diff --git a/tests/func/test_scm.py b/tests/func/test_scm.py
index 23d303e1e..185c8b60c 100644
--- a/tests/func/test_scm.py
+++ b/tests/func/test_scm.py
@@ -83,6 +83,14 @@ def test_ignore(tmp_dir, scm):
     assert _count_gitignore_entries(target) == 0
 
 
+def test_ignored(tmp_dir, scm):
+    tmp_dir.gen({"dir1": {"file1.jpg": "cont", "file2.txt": "cont"}})
+    tmp_dir.gen({".gitignore": "dir1/*.jpg"})
+
+    assert scm._ignored(fspath(tmp_dir / "dir1" / "file1.jpg"))
+    assert not scm._ignored(fspath(tmp_dir / "dir1" / "file2.txt"))
+
+
 def test_get_gitignore(tmp_dir, scm):
     tmp_dir.gen({"file1": "contents", "dir": {}})
 
diff --git a/tests/unit/test_rwlock.py b/tests/unit/test_rwlock.py
index f44e0303f..4bb3f1969 100644
--- a/tests/unit/test_rwlock.py
+++ b/tests/unit/test_rwlock.py
@@ -62,12 +62,12 @@ def test_rwlock_subdirs(tmp_path):
     subfoo = PathInfo("foo/subfoo")
 
     with rwlock(path, "cmd1", [foo], []):
-        with pytest.raises(LockError):
+        with pytest.raises(LockError, match=r"subfoo(.|\n)*cmd1"):
             with rwlock(path, "cmd2", [], [subfoo]):
                 pass
 
     with rwlock(path, "cmd1", [], [subfoo]):
-        with pytest.raises(LockError):
+        with pytest.raises(LockError, match=r"'foo'(.|\n)*cmd1"):
             with rwlock(path, "cmd2", [foo], []):
                 pass