Przeglądaj źródła

Implement rerere automatic resolution application

Jelmer Vernooij 2 miesięcy temu
rodzic
commit
fc9633a6b7
4 zmienionych plików z 150 dodań i 33 usunięć
  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:
         if parsed_args.subcommand is None:
             # Record current conflicts
             # Record current conflicts
-            recorded = porcelain.rerere(parsed_args.gitdir)
+            recorded, resolved = porcelain.rerere(parsed_args.gitdir)
             if not recorded:
             if not recorded:
                 sys.stdout.write("No conflicts to record.\n")
                 sys.stdout.write("No conflicts to record.\n")
             else:
             else:
@@ -6446,6 +6446,10 @@ class cmd_rerere(Command):
                     sys.stdout.write(
                     sys.stdout.write(
                         f"Recorded resolution for {path.decode('utf-8')}: {conflict_id}\n"
                         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":
         elif parsed_args.subcommand == "status":
             status_list = porcelain.rerere_status(parsed_args.gitdir)
             status_list = porcelain.rerere_status(parsed_args.gitdir)

+ 8 - 5
dulwich/porcelain.py

@@ -8828,17 +8828,20 @@ def mailinfo(
     return result
     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
     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:
     Args:
         repo: Path to the repository
         repo: Path to the repository
 
 
     Returns:
     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
     from dulwich.rerere import rerere_auto
 
 
@@ -8853,7 +8856,7 @@ def rerere(repo: RepoPath = ".") -> list[tuple[bytes, str]]:
             if isinstance(entry, ConflictedIndexEntry):
             if isinstance(entry, ConflictedIndexEntry):
                 conflicts.append(path)
                 conflicts.append(path)
 
 
-        # Record conflicts
+        # Record conflicts and apply known resolutions
         working_tree = r.path
         working_tree = r.path
         return rerere_auto(r, working_tree, conflicts)
         return rerere_auto(r, working_tree, conflicts)
 
 

+ 33 - 18
dulwich/rerere.py

@@ -248,23 +248,22 @@ class RerereCache:
 
 
         return conflict_id
         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.
         """Record a resolution for a previously recorded conflict.
 
 
         Args:
         Args:
-            path: Path to the resolved file
+            conflict_id: The conflict ID to record resolution for
             content: Resolved file content (without conflict markers)
             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:
     def has_resolution(self, conflict_id: str) -> bool:
         """Check if a resolution exists for a conflict.
         """Check if a resolution exists for a conflict.
@@ -466,7 +465,7 @@ def rerere_auto(
     repo: "Repo",
     repo: "Repo",
     working_tree_path: bytes | str,
     working_tree_path: bytes | str,
     conflicts: list[bytes],
     conflicts: list[bytes],
-) -> list[tuple[bytes, str]]:
+) -> tuple[list[tuple[bytes, str]], list[bytes]]:
     """Automatically record conflicts and apply known resolutions.
     """Automatically record conflicts and apply known resolutions.
 
 
     This is the main entry point for rerere integration with merge operations.
     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
         conflicts: List of conflicted file paths
 
 
     Returns:
     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()
     config = repo.get_config_stack()
     if not is_rerere_enabled(config):
     if not is_rerere_enabled(config):
-        return []
+        return [], []
 
 
     cache = RerereCache.from_repo(repo)
     cache = RerereCache.from_repo(repo)
     recorded = []
     recorded = []
+    resolved = []
 
 
     if isinstance(working_tree_path, bytes):
     if isinstance(working_tree_path, bytes):
         working_tree_path = os.fsdecode(working_tree_path)
         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:
     for path in conflicts:
         # Read the file from the working tree
         # Read the file from the working tree
         file_path = os.path.join(working_tree_path, os.fsdecode(path))
         file_path = os.path.join(working_tree_path, os.fsdecode(path))
@@ -504,7 +508,18 @@ def rerere_auto(
 
 
         # Record the conflict
         # Record the conflict
         conflict_id = cache.record_conflict(path, content)
         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:
     def test_rerere_auto_records_conflicts(self) -> None:
         """Test that rerere_auto records conflicts from working tree."""
         """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(b"test.txt", path)
         self.assertEqual(40, len(conflict_id))  # SHA-1 hash length
         self.assertEqual(40, len(conflict_id))  # SHA-1 hash length
 
 
@@ -374,13 +376,106 @@ line 2
         with open(file_path, "wb") as f:
         with open(file_path, "wb") as f:
             f.write(b"line 1\nline 2\n")
             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:
     def test_rerere_auto_handles_missing_files(self) -> None:
         """Test that rerere_auto handles deleted files gracefully."""
         """Test that rerere_auto handles deleted files gracefully."""
         from dulwich.rerere import rerere_auto
         from dulwich.rerere import rerere_auto
 
 
         # Don't create the file
         # 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)