Repository: conan-io/conan
Pull request #17123 (2024-10-06 19:25:22)

Add support for including paths that are ignored in `.conanignore`
Changelog: Feature: Add support for including paths that are ignored in `.conanignore`
Docs: TODO



Patch:
diff --git a/conan/internal/api/config/config_installer.py b/conan/internal/api/config/config_installer.py
index ad4ac2971..3a7e63679 100644
--- a/conan/internal/api/config/config_installer.py
+++ b/conan/internal/api/config/config_installer.py
@@ -16,16 +16,30 @@ class _ConanIgnoreMatcher:
     def __init__(self, conanignore_path, ignore=None):
         conanignore_path = os.path.abspath(conanignore_path)
         self._ignored_entries = {".conanignore"}
+        self._included_entries = set()
         if os.path.exists(conanignore_path):
             with open(conanignore_path, 'r') as conanignore:
                 for line in conanignore:
                     line_content = line.split("#", maxsplit=1)[0].strip()
                     if line_content:
-                        self._ignored_entries.add(line_content)
+                        if line_content.startswith("!"):
+                            self._included_entries.add(line_content[1:])
+                        else:
+                            self._ignored_entries.add(line_content)
         if ignore:
             self._ignored_entries.update(ignore)
 
     def matches(self, path):
+        """Returns whether the path should be ignored
+
+        It's ignored if:
+         - The path does not match any of the included entries
+         - And the path matches any of the ignored entries
+
+        In any other, the path is not ignored"""
+        for include_entry in self._included_entries:
+            if fnmatch.fnmatch(path, include_entry):
+                return False
         for ignore_entry in self._ignored_entries:
             if fnmatch.fnmatch(path, ignore_entry):
                 return True


Test patch:
diff --git a/test/integration/command/config_test.py b/test/integration/command/config_test.py
index 04b749a47..a85a51e2b 100644
--- a/test/integration/command/config_test.py
+++ b/test/integration/command/config_test.py
@@ -73,14 +73,18 @@ def test_config_install_conanignore():
     tests/*
     # Next line is commented out, so it should be ignored
     # other_tests/*
+    !b/c/important_file
+    !b/c/important_folder/*
     """)
     tc.save({
         'config_folder/.conanignore': conanignore,
-        "config_folder/a/test": '',
+        'config_folder/a/test': '',
         'config_folder/abracadabra': '',
         'config_folder/b/bison': '',
         'config_folder/b/a/test2': '',
         'config_folder/b/c/helmet': '',
+        'config_folder/b/c/important_file': '',
+        'config_folder/b/c/important_folder/contents': '',
         'config_folder/d/prix': '',
         'config_folder/d/foo/bar': '',
         'config_folder/foo': '',
@@ -107,7 +111,10 @@ def test_config_install_conanignore():
     _assert_config_exists("b/bison")
     _assert_config_exists("b/a/test2")
     _assert_config_not_exists("b/c/helmet")
-    _assert_config_not_exists("b/c")
+
+    _assert_config_exists("b/c")
+    _assert_config_exists("b/c/important_file")
+    _assert_config_exists("b/c/important_folder/contents")
 
     _assert_config_not_exists("d/prix")
     _assert_config_not_exists("d/foo/bar")
@@ -118,7 +125,39 @@ def test_config_install_conanignore():
     _assert_config_not_exists("tests/tester")
     _assert_config_exists("other_tests/tester2")
 
-    os.listdir(tc.current_folder)
+
+def test_config_install_conanignore_ignore_all_allow_specific_workflow():
+    tc = TestClient()
+    conanignore = textwrap.dedent("""
+    *
+    !important_folder/*
+    !important_file
+    # We can even include the conanignore that we skip by default!
+    !.conanignore
+    """)
+    tc.save({
+        'config_folder/.conanignore': conanignore,
+        'config_folder/a/test': '',
+        'config_folder/abracadabra': '',
+        'config_folder/important_folder/contents': '',
+        'config_folder/important_file': '',
+    })
+
+    def _assert_config_exists(path):
+        assert os.path.exists(os.path.join(tc.cache_folder, path))
+
+    def _assert_config_not_exists(path):
+        assert not os.path.exists(os.path.join(tc.cache_folder, path))
+
+    tc.run('config install config_folder')
+
+    _assert_config_exists(".conanignore")
+
+    _assert_config_not_exists("a")
+    _assert_config_not_exists("abracadabra")
+
+    _assert_config_exists("important_folder/contents")
+    _assert_config_exists("important_file")
 
 
 def test_config_show():