MCPcopy Create free account
hub / github.com/HKUDS/OpenSpace / _apply_multi_file_patch

Function _apply_multi_file_patch

openspace/skill_engine/patch.py:773–826  ·  view source on GitHub ↗

Parse and apply a ``*** Begin Patch`` block to a skill directory. Two-phase: validate all hunks first, then write to disk.

(patch_text: str, skill_dir: Path)

Source from the content-addressed store, hash-verified

771 return "\n".join(new_lines)
772
773def _apply_multi_file_patch(patch_text: str, skill_dir: Path) -> None:
774 """Parse and apply a ``*** Begin Patch`` block to a skill directory.
775
776 Two-phase: validate all hunks first, then write to disk.
777 """
778 parsed = parse_patch(patch_text)
779 if not parsed.hunks:
780 raise PatchParseError("Patch contains no file operations")
781
782 resolved_dir = skill_dir.resolve()
783
784 # Phase 1: validate and compute new contents
785 changes: List[Tuple[str, Path, str, str]] = [] # (type, abs_path, old, new)
786
787 for hunk in parsed.hunks:
788 abs_path = (skill_dir / hunk.path).resolve()
789
790 # Security check
791 if not str(abs_path).startswith(str(resolved_dir)):
792 raise PatchError(f"Path escapes skill directory: {hunk.path}")
793
794 if hunk.type == "add":
795 new_content = hunk.contents
796 if new_content and not new_content.endswith("\n"):
797 new_content += "\n"
798 changes.append(("add", abs_path, "", new_content))
799
800 elif hunk.type == "delete":
801 if not abs_path.exists():
802 raise PatchError(f"Cannot delete non-existent file: {hunk.path}")
803 changes.append(("delete", abs_path, "", ""))
804
805 elif hunk.type == "update":
806 if not abs_path.exists():
807 raise PatchError(f"Cannot update non-existent file: {hunk.path}")
808 old_content = abs_path.read_text(encoding="utf-8")
809 new_content = apply_update_chunks(str(hunk.path), old_content, hunk.chunks)
810 changes.append(("update", abs_path, old_content, new_content))
811
812 # Phase 2: write all changes
813 for change_type, abs_path, _, new_content in changes:
814 if change_type == "add":
815 abs_path.parent.mkdir(parents=True, exist_ok=True)
816 abs_path.write_text(new_content, encoding="utf-8")
817 logger.debug(f"PATCH add: {abs_path.relative_to(resolved_dir)}")
818
819 elif change_type == "delete":
820 if abs_path.exists():
821 abs_path.unlink()
822 logger.debug(f"PATCH delete: {abs_path.relative_to(resolved_dir)}")
823
824 elif change_type == "update":
825 abs_path.write_text(new_content, encoding="utf-8")
826 logger.debug(f"PATCH update: {abs_path.relative_to(resolved_dir)}")
827
828
829# SEARCH/REPLACE (single-file DIFF)

Callers 3

fix_skillFunction · 0.85
derive_skillFunction · 0.85
create_skillFunction · 0.85

Calls 5

parse_patchFunction · 0.85
PatchParseErrorClass · 0.85
PatchErrorClass · 0.85
apply_update_chunksFunction · 0.85
resolveMethod · 0.80

Tested by

no test coverage detected