Procházet zdrojové kódy

Fix Windows config loading to only use current Git installation (#1755)

Previously, Dulwich would load multiple gitconfig files on Windows
including defunct ones from old installations.

Changes:
- Modified get_win_system_paths() to only return the current Git config
path
- Added get_win_legacy_system_paths() for diagnostic/migration purposes
- Added debug logging to show which gitconfig files are loaded

Fixes #1732
Jelmer Vernooij před 5 měsíci
rodič
revize
1fb412e403
3 změnil soubory, kde provedl 84 přidání a 11 odebrání
  1. 3 0
      NEWS
  2. 27 1
      dulwich/config.py
  3. 54 10
      tests/test_config.py

+ 3 - 0
NEWS

@@ -15,6 +15,9 @@
  * Fix Windows path handling in ``_ensure_parent_dir_exists`` to correctly
    handle drive letters during checkout operations. (Jelmer Vernooij, #1751)
 
+ * Fix Windows config loading to only use current Git config path,
+   avoiding loading older config files.  (Jelmer Vernooij, #1732)
+
 0.24.1	2025-08-01
 
  * Require ``typing_extensions`` on Python 3.10.

+ 27 - 1
dulwich/config.py

@@ -1185,14 +1185,36 @@ def _find_git_in_win_reg() -> Iterator[str]:
 
 # There is no set standard for system config dirs on windows. We try the
 # following:
-#   - %PROGRAMDATA%/Git/config - (deprecated) Windows config dir per CGit docs
 #   - %PROGRAMFILES%/Git/etc/gitconfig - Git for Windows (msysgit) config dir
 #     Used if CGit installation (Git/bin/git.exe) is found in PATH in the
 #     system registry
 def get_win_system_paths() -> Iterator[str]:
+    """Get current Windows system Git config paths.
+
+    Only returns the current Git for Windows config location, not legacy paths.
+    """
+    # Try to find Git installation from PATH first
+    for git_dir in _find_git_in_win_path():
+        yield os.path.join(git_dir, "etc", "gitconfig")
+        return  # Only use the first found path
+
+    # Fall back to registry if not found in PATH
+    for git_dir in _find_git_in_win_reg():
+        yield os.path.join(git_dir, "etc", "gitconfig")
+        return  # Only use the first found path
+
+
+def get_win_legacy_system_paths() -> Iterator[str]:
+    """Get legacy Windows system Git config paths.
+
+    Returns all possible config paths including deprecated locations.
+    This function can be used for diagnostics or migration purposes.
+    """
+    # Include deprecated PROGRAMDATA location
     if "PROGRAMDATA" in os.environ:
         yield os.path.join(os.environ["PROGRAMDATA"], "Git", "config")
 
+    # Include all Git installations found
     for git_dir in _find_git_in_win_path():
         yield os.path.join(git_dir, "etc", "gitconfig")
     for git_dir in _find_git_in_win_reg():
@@ -1239,11 +1261,15 @@ class StackedConfig(Config):
                 if sys.platform == "win32":
                     paths.extend(get_win_system_paths())
 
+        logger.debug("Loading gitconfig from paths: %s", paths)
+
         backends = []
         for path in paths:
             try:
                 cf = ConfigFile.from_path(path)
+                logger.debug("Successfully loaded gitconfig from: %s", path)
             except FileNotFoundError:
+                logger.debug("Gitconfig file not found: %s", path)
                 continue
             backends.append(cf)
         return backends

+ 54 - 10
tests/test_config.py

@@ -820,12 +820,10 @@ class StackedConfigTests(TestCase):
         install_dir = os.path.join("C:", "foo", "Git")
         self.overrideEnv("PATH", os.path.join(install_dir, "cmd"))
         with patch("os.path.exists", return_value=True):
-            paths = set(get_win_system_paths())
+            paths = list(get_win_system_paths())
+        # Should only return the current Git config path, not legacy paths
         self.assertEqual(
-            {
-                os.path.join(os.environ.get("PROGRAMDATA"), "Git", "config"),
-                os.path.join(install_dir, "etc", "gitconfig"),
-            },
+            [os.path.join(install_dir, "etc", "gitconfig")],
             paths,
         )
 
@@ -842,12 +840,29 @@ class StackedConfigTests(TestCase):
                 "winreg.QueryValueEx",
                 return_value=(install_dir, winreg.REG_SZ),
             ):
-                paths = set(get_win_system_paths())
+                paths = list(get_win_system_paths())
+        # Should only return the current Git config path, not legacy paths
         self.assertEqual(
-            {
-                os.path.join(os.environ.get("PROGRAMDATA"), "Git", "config"),
-                os.path.join(install_dir, "etc", "gitconfig"),
-            },
+            [os.path.join(install_dir, "etc", "gitconfig")],
+            paths,
+        )
+
+    @skipIf(sys.platform != "win32", "Windows specific config location.")
+    def test_windows_legacy_paths(self) -> None:
+        from dulwich.config import get_win_legacy_system_paths
+
+        install_dir = os.path.join("C:", "foo", "Git")
+        self.overrideEnv("PATH", os.path.join(install_dir, "cmd"))
+        self.overrideEnv("PROGRAMDATA", "C:\\ProgramData")
+        with patch("os.path.exists", return_value=True):
+            paths = list(get_win_legacy_system_paths())
+        # Should return all paths including legacy PROGRAMDATA location
+        self.assertIn(
+            os.path.join("C:\\ProgramData", "Git", "config"),
+            paths,
+        )
+        self.assertIn(
+            os.path.join(install_dir, "etc", "gitconfig"),
             paths,
         )
 
@@ -902,6 +917,35 @@ class StackedConfigTests(TestCase):
         finally:
             os.unlink(config_path)
 
+    def test_debug_logging(self) -> None:
+        """Test that debug logging outputs gitconfig paths."""
+        import logging
+
+        # Set up a custom handler to capture log messages
+        log_messages = []
+        handler = logging.Handler()
+        handler.emit = lambda record: log_messages.append(record.getMessage())
+
+        # Get the dulwich.config logger and add our handler
+        logger = logging.getLogger("dulwich.config")
+        old_level = logger.level
+        logger.setLevel(logging.DEBUG)
+        logger.addHandler(handler)
+
+        try:
+            # Call default_backends which now logs unconditionally
+            StackedConfig.default_backends()
+
+            # Check that debug messages were logged
+            self.assertTrue(
+                any("Loading gitconfig from paths:" in msg for msg in log_messages),
+                "Debug logging should log gitconfig paths",
+            )
+        finally:
+            # Clean up
+            logger.removeHandler(handler)
+            logger.setLevel(old_level)
+
     def test_git_config_nosystem_precedence(self) -> None:
         # GIT_CONFIG_SYSTEM should take precedence over GIT_CONFIG_NOSYSTEM
         with tempfile.NamedTemporaryFile(