Explorar o código

Implement rerere automatic conflict recording

Jelmer Vernooij hai 2 meses
pai
achega
7860f08008
Modificáronse 4 ficheiros con 171 adicións e 23 borrados
  1. 17 4
      dulwich/cli.py
  2. 33 1
      dulwich/porcelain.py
  3. 28 18
      dulwich/rerere.py
  4. 93 0
      tests/test_rerere.py

+ 17 - 4
dulwich/cli.py

@@ -6421,9 +6421,9 @@ class cmd_rerere(Command):
         parser.add_argument(
             "subcommand",
             nargs="?",
-            default="status",
+            default=None,
             choices=["status", "diff", "forget", "clear", "gc"],
-            help="Subcommand to execute",
+            help="Subcommand to execute (default: record conflicts)",
         )
         parser.add_argument(
             "pathspec", nargs="?", help="Path specification (for forget subcommand)"
@@ -6436,7 +6436,18 @@ class cmd_rerere(Command):
         )
         parsed_args = parser.parse_args(args)
 
-        if parsed_args.subcommand == "status":
+        if parsed_args.subcommand is None:
+            # Record current conflicts
+            recorded = porcelain.rerere(parsed_args.gitdir)
+            if not recorded:
+                sys.stdout.write("No conflicts to record.\n")
+            else:
+                for path, conflict_id in recorded:
+                    sys.stdout.write(
+                        f"Recorded resolution for {path.decode('utf-8')}: {conflict_id}\n"
+                    )
+
+        elif parsed_args.subcommand == "status":
             status_list = porcelain.rerere_status(parsed_args.gitdir)
             if not status_list:
                 sys.stdout.write("No recorded resolutions.\n")
@@ -6472,7 +6483,9 @@ class cmd_rerere(Command):
 
         elif parsed_args.subcommand == "gc":
             porcelain.rerere_gc(parsed_args.gitdir, parsed_args.max_age_days)
-            sys.stdout.write(f"Cleaned up resolutions older than {parsed_args.max_age_days} days\n")
+            sys.stdout.write(
+                f"Cleaned up resolutions older than {parsed_args.max_age_days} days\n"
+            )
 
 
 commands = {

+ 33 - 1
dulwich/porcelain.py

@@ -8828,6 +8828,36 @@ def mailinfo(
     return result
 
 
+def rerere(repo: RepoPath = ".") -> list[tuple[bytes, str]]:
+    """Record current conflict resolutions.
+
+    This reads conflicted files from the working tree and records them
+    in the rerere cache.
+
+    Args:
+        repo: Path to the repository
+
+    Returns:
+        List of tuples (path, conflict_id) for recorded conflicts
+    """
+    from dulwich.rerere import rerere_auto
+
+    with open_repo_closing(repo) as r:
+        # Get conflicts from the index
+        index = r.open_index()
+        conflicts = []
+
+        from dulwich.index import ConflictedIndexEntry
+
+        for path, entry in index.items():
+            if isinstance(entry, ConflictedIndexEntry):
+                conflicts.append(path)
+
+        # Record conflicts
+        working_tree = r.path
+        return rerere_auto(r, working_tree, conflicts)
+
+
 def rerere_status(repo: RepoPath = ".") -> list[tuple[str, bool]]:
     """Get the status of all conflicts in the rerere cache.
 
@@ -8844,7 +8874,9 @@ def rerere_status(repo: RepoPath = ".") -> list[tuple[str, bool]]:
         return cache.status()
 
 
-def rerere_diff(repo: RepoPath = ".", conflict_id: str | None = None) -> list[tuple[str, bytes, bytes | None]]:
+def rerere_diff(
+    repo: RepoPath = ".", conflict_id: str | None = None
+) -> list[tuple[str, bytes, bytes | None]]:
     """Show differences for recorded rerere conflicts.
 
     Args:

+ 28 - 18
dulwich/rerere.py

@@ -13,7 +13,6 @@ from typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
     from dulwich.config import StackedConfig
-    from dulwich.index import Index
     from dulwich.repo import Repo
 
 
@@ -464,9 +463,9 @@ def is_rerere_autoupdate(config: "StackedConfig") -> bool:
 
 
 def rerere_auto(
-    repo_dir: bytes | str,
-    index: "Index",
-    config: "StackedConfig",
+    repo: "Repo",
+    working_tree_path: bytes | str,
+    conflicts: list[bytes],
 ) -> list[tuple[bytes, str]]:
     """Automatically record conflicts and apply known resolutions.
 
@@ -474,27 +473,38 @@ def rerere_auto(
     It should be called after a merge that resulted in conflicts.
 
     Args:
-        repo_dir: Path to the repository (.git directory)
-        index: Git index with conflicts
-        config: Git configuration
+        repo: Repository object
+        working_tree_path: Path to the working tree
+        conflicts: List of conflicted file paths
 
     Returns:
         List of tuples (path, conflict_id) for recorded conflicts
     """
+    config = repo.get_config_stack()
     if not is_rerere_enabled(config):
         return []
 
-    if isinstance(repo_dir, bytes):
-        repo_dir = os.fsdecode(repo_dir)
+    cache = RerereCache.from_repo(repo)
+    recorded = []
+
+    if isinstance(working_tree_path, bytes):
+        working_tree_path = os.fsdecode(working_tree_path)
+
+    # Record conflicts from the working tree
+    for path in conflicts:
+        # Read the file from the working tree
+        file_path = os.path.join(working_tree_path, os.fsdecode(path))
 
-    # rr_cache_dir = os.path.join(repo_dir, "rr-cache")
-    # cache = RerereCache(rr_cache_dir)
+        try:
+            with open(file_path, "rb") as f:
+                content = f.read()
+        except FileNotFoundError:
+            # File was deleted in conflict
+            continue
 
-    # TODO: Implement automatic conflict recording and resolution
-    # This requires:
-    # 1. Reading conflicted files from the working tree
-    # 2. Recording conflicts in the cache
-    # 3. Applying known resolutions if rerere.autoupdate is enabled
-    # 4. Updating the index with resolved content
+        # Record the conflict
+        conflict_id = cache.record_conflict(path, content)
+        if conflict_id:
+            recorded.append((path, conflict_id))
 
-    return []
+    return recorded

+ 93 - 0
tests/test_rerere.py

@@ -1,5 +1,6 @@
 """Tests for rerere functionality."""
 
+import os
 import tempfile
 import unittest
 
@@ -291,3 +292,95 @@ class ConfigTests(unittest.TestCase):
         config = ConfigDict()
         config.set((b"rerere",), b"autoupdate", b"true")
         self.assertTrue(is_rerere_autoupdate(config))
+
+
+class RerereAutoTests(unittest.TestCase):
+    """Tests for rerere_auto functionality."""
+
+    def setUp(self) -> None:
+        """Set up test fixtures."""
+
+        from dulwich.repo import Repo
+
+        self.tempdir = tempfile.mkdtemp()
+        self.repo = Repo.init(self.tempdir)
+
+        # Enable rerere
+        config = self.repo.get_config()
+        config.set((b"rerere",), b"enabled", b"true")
+        config.write_to_path()
+
+    def tearDown(self) -> None:
+        """Clean up test fixtures."""
+        import shutil
+
+        shutil.rmtree(self.tempdir, ignore_errors=True)
+
+    def test_rerere_auto_disabled(self) -> None:
+        """Test that rerere_auto does nothing when disabled."""
+        from dulwich.rerere import rerere_auto
+
+        # Disable rerere
+        config = self.repo.get_config()
+        config.set((b"rerere",), b"enabled", b"false")
+        config.write_to_path()
+
+        # Create a fake conflicted file
+        conflict_file = os.path.join(self.tempdir, "test.txt")
+        with open(conflict_file, "wb") as f:
+            f.write(
+                b"""<<<<<<< ours
+our change
+=======
+their change
+>>>>>>> theirs
+"""
+            )
+
+        result = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual([], result)
+
+    def test_rerere_auto_records_conflicts(self) -> None:
+        """Test that rerere_auto records conflicts from working tree."""
+        from dulwich.rerere import rerere_auto
+
+        # Create a conflicted file in the working tree
+        conflict_file = os.path.join(self.tempdir, "test.txt")
+        with open(conflict_file, "wb") as f:
+            f.write(
+                b"""line 1
+<<<<<<< ours
+our change
+=======
+their change
+>>>>>>> theirs
+line 2
+"""
+            )
+
+        result = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual(1, len(result))
+
+        path, conflict_id = result[0]
+        self.assertEqual(b"test.txt", path)
+        self.assertEqual(40, len(conflict_id))  # SHA-1 hash length
+
+    def test_rerere_auto_skips_non_conflicted_files(self) -> None:
+        """Test that rerere_auto skips files without conflict markers."""
+        from dulwich.rerere import rerere_auto
+
+        # Create a non-conflicted file
+        file_path = os.path.join(self.tempdir, "test.txt")
+        with open(file_path, "wb") as f:
+            f.write(b"line 1\nline 2\n")
+
+        result = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual([], result)
+
+    def test_rerere_auto_handles_missing_files(self) -> None:
+        """Test that rerere_auto handles deleted files gracefully."""
+        from dulwich.rerere import rerere_auto
+
+        # Don't create the file
+        result = rerere_auto(self.repo, self.tempdir, [b"missing.txt"])
+        self.assertEqual([], result)