Просмотр исходного кода

Implement rerere automatic resolution application

Jelmer Vernooij 2 месяцев назад
Родитель
Сommit
fc9633a6b7
4 измененных файлов с 150 добавлено и 33 удалено
  1. 5 1
      dulwich/cli.py
  2. 8 5
      dulwich/porcelain.py
  3. 33 18
      dulwich/rerere.py
  4. 104 9
      tests/test_rerere.py

+ 5 - 1
dulwich/cli.py

@@ -6438,7 +6438,7 @@ class cmd_rerere(Command):
 
         if parsed_args.subcommand is None:
             # Record current conflicts
-            recorded = porcelain.rerere(parsed_args.gitdir)
+            recorded, resolved = porcelain.rerere(parsed_args.gitdir)
             if not recorded:
                 sys.stdout.write("No conflicts to record.\n")
             else:
@@ -6446,6 +6446,10 @@ class cmd_rerere(Command):
                     sys.stdout.write(
                         f"Recorded resolution for {path.decode('utf-8')}: {conflict_id}\n"
                     )
+                if resolved:
+                    sys.stdout.write("\nAutomatically resolved:\n")
+                    for path in resolved:
+                        sys.stdout.write(f"  {path.decode('utf-8')}\n")
 
         elif parsed_args.subcommand == "status":
             status_list = porcelain.rerere_status(parsed_args.gitdir)

+ 8 - 5
dulwich/porcelain.py

@@ -8828,17 +8828,20 @@ def mailinfo(
     return result
 
 
-def rerere(repo: RepoPath = ".") -> list[tuple[bytes, str]]:
-    """Record current conflict resolutions.
+def rerere(repo: RepoPath = ".") -> tuple[list[tuple[bytes, str]], list[bytes]]:
+    """Record current conflict resolutions and apply known resolutions.
 
     This reads conflicted files from the working tree and records them
-    in the rerere cache.
+    in the rerere cache. If rerere.autoupdate is enabled and a known
+    resolution exists, it will be automatically applied.
 
     Args:
         repo: Path to the repository
 
     Returns:
-        List of tuples (path, conflict_id) for recorded conflicts
+        Tuple of:
+        - List of tuples (path, conflict_id) for recorded conflicts
+        - List of paths where resolutions were automatically applied
     """
     from dulwich.rerere import rerere_auto
 
@@ -8853,7 +8856,7 @@ def rerere(repo: RepoPath = ".") -> list[tuple[bytes, str]]:
             if isinstance(entry, ConflictedIndexEntry):
                 conflicts.append(path)
 
-        # Record conflicts
+        # Record conflicts and apply known resolutions
         working_tree = r.path
         return rerere_auto(r, working_tree, conflicts)
 

+ 33 - 18
dulwich/rerere.py

@@ -248,23 +248,22 @@ class RerereCache:
 
         return conflict_id
 
-    def record_resolution(self, path: bytes, content: bytes) -> str | None:
+    def record_resolution(self, conflict_id: str, content: bytes) -> None:
         """Record a resolution for a previously recorded conflict.
 
         Args:
-            path: Path to the resolved file
+            conflict_id: The conflict ID to record resolution for
             content: Resolved file content (without conflict markers)
-
-        Returns:
-            The conflict ID if resolution was recorded, None otherwise
         """
-        # Find the conflict ID by checking existing preimages
-        # In practice, we need to track which conflicts are active
-        # For now, we'll compute the ID from the original conflict
+        # Write postimage
+        postimage_path = self._get_postimage_path(conflict_id)
 
-        # This is a simplified version - in real git, we'd track active conflicts
-        # and match them to resolutions
-        return None
+        # Ensure directory exists
+        conflict_dir = self._get_conflict_dir(conflict_id)
+        os.makedirs(conflict_dir, exist_ok=True)
+
+        with open(postimage_path, "wb") as f:
+            f.write(content)
 
     def has_resolution(self, conflict_id: str) -> bool:
         """Check if a resolution exists for a conflict.
@@ -466,7 +465,7 @@ def rerere_auto(
     repo: "Repo",
     working_tree_path: bytes | str,
     conflicts: list[bytes],
-) -> list[tuple[bytes, str]]:
+) -> tuple[list[tuple[bytes, str]], list[bytes]]:
     """Automatically record conflicts and apply known resolutions.
 
     This is the main entry point for rerere integration with merge operations.
@@ -478,19 +477,24 @@ def rerere_auto(
         conflicts: List of conflicted file paths
 
     Returns:
-        List of tuples (path, conflict_id) for recorded conflicts
+        Tuple of:
+        - List of tuples (path, conflict_id) for recorded conflicts
+        - List of paths where resolutions were automatically applied
     """
     config = repo.get_config_stack()
     if not is_rerere_enabled(config):
-        return []
+        return [], []
 
     cache = RerereCache.from_repo(repo)
     recorded = []
+    resolved = []
 
     if isinstance(working_tree_path, bytes):
         working_tree_path = os.fsdecode(working_tree_path)
 
-    # Record conflicts from the working tree
+    autoupdate = is_rerere_autoupdate(config)
+
+    # Record conflicts from the working tree and apply known resolutions
     for path in conflicts:
         # Read the file from the working tree
         file_path = os.path.join(working_tree_path, os.fsdecode(path))
@@ -504,7 +508,18 @@ def rerere_auto(
 
         # Record the conflict
         conflict_id = cache.record_conflict(path, content)
-        if conflict_id:
-            recorded.append((path, conflict_id))
+        if not conflict_id:
+            continue
+
+        recorded.append((path, conflict_id))
+
+        # Check if we have a resolution for this conflict
+        if autoupdate and cache.has_resolution(conflict_id):
+            resolution = cache.get_resolution(conflict_id)
+            if resolution is not None:
+                # Apply the resolution to the working tree
+                with open(file_path, "wb") as f:
+                    f.write(resolution)
+                resolved.append(path)
 
-    return recorded
+    return recorded, resolved

+ 104 - 9
tests/test_rerere.py

@@ -337,8 +337,9 @@ their change
 """
             )
 
-        result = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
-        self.assertEqual([], result)
+        recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual([], recorded)
+        self.assertEqual([], resolved)
 
     def test_rerere_auto_records_conflicts(self) -> None:
         """Test that rerere_auto records conflicts from working tree."""
@@ -358,10 +359,11 @@ line 2
 """
             )
 
-        result = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
-        self.assertEqual(1, len(result))
+        recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual(1, len(recorded))
+        self.assertEqual(0, len(resolved))
 
-        path, conflict_id = result[0]
+        path, conflict_id = recorded[0]
         self.assertEqual(b"test.txt", path)
         self.assertEqual(40, len(conflict_id))  # SHA-1 hash length
 
@@ -374,13 +376,106 @@ line 2
         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)
+        recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual([], recorded)
+        self.assertEqual([], resolved)
 
     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)
+        recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"missing.txt"])
+        self.assertEqual([], recorded)
+        self.assertEqual([], resolved)
+
+    def test_rerere_auto_applies_known_resolution(self) -> None:
+        """Test that rerere_auto applies known resolutions when autoupdate is enabled."""
+        from dulwich.rerere import RerereCache, rerere_auto
+
+        # Enable autoupdate
+        config = self.repo.get_config()
+        config.set((b"rerere",), b"autoupdate", b"true")
+        config.write_to_path()
+
+        # Create a conflicted file
+        conflict_file = os.path.join(self.tempdir, "test.txt")
+        conflict_content = b"""line 1
+<<<<<<< ours
+our change
+=======
+their change
+>>>>>>> theirs
+line 2
+"""
+        with open(conflict_file, "wb") as f:
+            f.write(conflict_content)
+
+        # Record the conflict first time
+        recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual(1, len(recorded))
+        self.assertEqual(0, len(resolved))  # No resolution yet
+
+        conflict_id = recorded[0][1]
+
+        # Manually record a resolution
+        cache = RerereCache.from_repo(self.repo)
+        resolution = b"line 1\nresolved change\nline 2\n"
+        cache.record_resolution(conflict_id, resolution)
+
+        # Create the same conflict again
+        with open(conflict_file, "wb") as f:
+            f.write(conflict_content)
+
+        # rerere_auto should now apply the resolution
+        recorded2, resolved2 = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual(1, len(recorded2))
+        self.assertEqual(1, len(resolved2))
+        self.assertEqual(b"test.txt", resolved2[0])
+
+        # Verify the file was resolved
+        with open(conflict_file, "rb") as f:
+            actual = f.read()
+        self.assertEqual(resolution, actual)
+
+    def test_rerere_auto_no_apply_without_autoupdate(self) -> None:
+        """Test that rerere_auto doesn't apply resolutions when autoupdate is disabled."""
+        from dulwich.rerere import RerereCache, rerere_auto
+
+        # autoupdate is disabled by default
+
+        # Create a conflicted file
+        conflict_file = os.path.join(self.tempdir, "test.txt")
+        conflict_content = b"""line 1
+<<<<<<< ours
+our change
+=======
+their change
+>>>>>>> theirs
+line 2
+"""
+        with open(conflict_file, "wb") as f:
+            f.write(conflict_content)
+
+        # Record the conflict first time
+        recorded, _resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        conflict_id = recorded[0][1]
+
+        # Manually record a resolution
+        cache = RerereCache.from_repo(self.repo)
+        resolution = b"line 1\nresolved change\nline 2\n"
+        cache.record_resolution(conflict_id, resolution)
+
+        # Create the same conflict again
+        with open(conflict_file, "wb") as f:
+            f.write(conflict_content)
+
+        # rerere_auto should NOT apply the resolution (autoupdate disabled)
+        recorded2, resolved2 = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
+        self.assertEqual(1, len(recorded2))
+        self.assertEqual(0, len(resolved2))  # Should not auto-apply
+
+        # Verify the file still has conflicts
+        with open(conflict_file, "rb") as f:
+            actual = f.read()
+        self.assertEqual(conflict_content, actual)