Files
strukto-ai--mirage/python/mirage/commands/builtin/generic/patch.py
T
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

167 lines
5.8 KiB
Python

import re
from collections.abc import AsyncIterator, Awaitable, Callable
from mirage.accessor.base import Accessor
from mirage.commands.builtin.utils.lines import split_lines
from mirage.commands.builtin.utils.stream import _read_stdin_async
from mirage.commands.spec.types import CommandName
from mirage.commands.spec.usage import extra_operand_error
from mirage.io.types import ByteSource, IOResult
from mirage.types import PathSpec
def _strip_path(path: str, strip_count: int) -> str:
parts = path.split("/")
return "/".join(
parts[strip_count:]) if strip_count < len(parts) else parts[-1]
def _apply_hunks(original_lines: list[str],
hunks: list[tuple[int, list[str]]],
forward_only: bool = False) -> list[str]:
result: list[str] = []
src_idx = 0
for start_line, hunk_lines in hunks:
hunk_start = start_line - 1
while src_idx < hunk_start and src_idx < len(original_lines):
result.append(original_lines[src_idx])
src_idx += 1
if forward_only:
expected = [
hl[1:] for hl in hunk_lines
if hl.startswith(" ") or hl.startswith("-")
]
actual = original_lines[src_idx:src_idx + len(expected)]
if expected != actual:
for _ in expected:
if src_idx < len(original_lines):
result.append(original_lines[src_idx])
src_idx += 1
continue
for hl in hunk_lines:
if hl.startswith(" "):
result.append(hl[1:])
src_idx += 1
elif hl.startswith("-"):
src_idx += 1
elif hl.startswith("+"):
result.append(hl[1:])
while src_idx < len(original_lines):
result.append(original_lines[src_idx])
src_idx += 1
return result
def _parse_patch(patch_text: str,
strip_count: int) -> dict[str, list[tuple[int, list[str]]]]:
files: dict[str, list[tuple[int, list[str]]]] = {}
current_file: str | None = None
current_hunks: list[tuple[int, list[str]]] = []
current_hunk_lines: list[str] = []
current_start = 0
for line in split_lines(patch_text):
if line.startswith("--- "):
continue
if line.startswith("+++ "):
if current_file and current_hunk_lines:
current_hunks.append((current_start, current_hunk_lines))
if current_file:
files[current_file] = current_hunks
raw_path = line[4:].split("\t")[0].strip()
current_file = "/" + _strip_path(raw_path, strip_count).lstrip("/")
current_hunks = []
current_hunk_lines = []
continue
m = re.match(r"@@ -(\d+)", line)
if m:
if current_hunk_lines:
current_hunks.append((current_start, current_hunk_lines))
current_start = int(m.group(1))
current_hunk_lines = []
continue
if current_file and (line.startswith("+") or line.startswith("-")
or line.startswith(" ")):
current_hunk_lines.append(line)
if current_file and current_hunk_lines:
current_hunks.append((current_start, current_hunk_lines))
if current_file:
files[current_file] = current_hunks
return files
def _reverse_hunks(
hunks: list[tuple[int, list[str]]]) -> list[tuple[int, list[str]]]:
out: list[tuple[int, list[str]]] = []
for start, hunk_lines in hunks:
reversed_lines: list[str] = []
for hl in hunk_lines:
if hl.startswith("+"):
reversed_lines.append("-" + hl[1:])
elif hl.startswith("-"):
reversed_lines.append("+" + hl[1:])
else:
reversed_lines.append(hl)
out.append((start, reversed_lines))
return out
async def _load_patch_data(
i: PathSpec | None,
paths: list[PathSpec],
has_resource: bool,
stdin: AsyncIterator[bytes] | bytes | None,
read_bytes: Callable[..., Awaitable[bytes]],
accessor: Accessor,
) -> bytes:
if i is not None and has_resource:
return await read_bytes(accessor, i.mount_path)
if paths and has_resource:
return await read_bytes(accessor, paths[0])
data = await _read_stdin_async(stdin)
if data is None:
return b""
return data
async def patch(
paths: list[PathSpec],
*,
read_bytes: Callable[..., Awaitable[bytes]],
write_bytes: Callable[..., Awaitable[None]],
has_resource: bool,
accessor: Accessor | None = None,
stdin: AsyncIterator[bytes] | bytes | None = None,
p: str | None = None,
R: bool = False,
i: PathSpec | None = None,
N: bool = False,
) -> tuple[ByteSource | None, IOResult]:
if len(paths) > 2:
raise extra_operand_error(CommandName.PATCH, paths[2].raw_path)
strip_count = int(p) if p else 0
patch_data = await _load_patch_data(i, paths, has_resource, stdin,
read_bytes, accessor)
patch_text = patch_data.decode(errors="replace")
file_hunks = _parse_patch(patch_text, strip_count)
writes: dict[str, bytes] = {}
for file_path, hunks in file_hunks.items():
try:
original = (await read_bytes(accessor,
file_path)).decode(errors="replace")
except FileNotFoundError:
original = ""
original_lines = split_lines(original)
if R:
hunks = _reverse_hunks(hunks)
patched_lines = _apply_hunks(original_lines, hunks, forward_only=N)
patched_data = ("\n".join(patched_lines) + "\n").encode()
await write_bytes(accessor, file_path, patched_data)
writes[file_path] = patched_data
return None, IOResult(writes=writes)
__all__ = ["patch"]