mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
129 lines
3.9 KiB
Python
129 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os.path
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from .exceptions import HunkApplyException, SubprocessException
|
|
from .patch import Change, diffobj
|
|
from .snippets import remove, which
|
|
|
|
|
|
def _apply_diff_with_subprocess(
|
|
diff: diffobj, lines: list[str], reverse: bool = False
|
|
) -> tuple[list[str], list[str] | None]:
|
|
# call out to patch program
|
|
patchexec = which('patch')
|
|
if not patchexec:
|
|
raise SubprocessException('cannot find patch program', code=-1)
|
|
|
|
tempdir = tempfile.gettempdir()
|
|
|
|
filepath = os.path.join(tempdir, 'wtp-' + str(hash(diff.header)))
|
|
oldfilepath = filepath + '.old'
|
|
newfilepath = filepath + '.new'
|
|
rejfilepath = filepath + '.rej'
|
|
patchfilepath = filepath + '.patch'
|
|
with open(oldfilepath, 'w') as f:
|
|
f.write('\n'.join(lines) + '\n')
|
|
|
|
with open(patchfilepath, 'w') as f:
|
|
f.write(diff.text)
|
|
|
|
args = [
|
|
patchexec,
|
|
'--reverse' if reverse else '--forward',
|
|
'--quiet',
|
|
'--no-backup-if-mismatch',
|
|
'-o',
|
|
newfilepath,
|
|
'-i',
|
|
patchfilepath,
|
|
'-r',
|
|
rejfilepath,
|
|
oldfilepath,
|
|
]
|
|
ret = subprocess.call(args)
|
|
|
|
with open(newfilepath) as f:
|
|
lines = f.read().splitlines()
|
|
|
|
try:
|
|
with open(rejfilepath) as f:
|
|
rejlines = f.read().splitlines()
|
|
except IOError:
|
|
rejlines = None
|
|
|
|
remove(oldfilepath)
|
|
remove(newfilepath)
|
|
remove(rejfilepath)
|
|
remove(patchfilepath)
|
|
|
|
# do this last to ensure files get cleaned up
|
|
if ret != 0:
|
|
raise SubprocessException('patch program failed', code=ret)
|
|
|
|
return lines, rejlines
|
|
|
|
|
|
def _reverse(changes: list[Change]) -> list[Change]:
|
|
def _reverse_change(c: Change) -> Change:
|
|
return c._replace(old=c.new, new=c.old)
|
|
|
|
return [_reverse_change(c) for c in changes]
|
|
|
|
|
|
def apply_diff(
|
|
diff: diffobj, text: str | list[str], reverse: bool = False, use_patch: bool = False
|
|
) -> list[str]:
|
|
lines = text.splitlines() if isinstance(text, str) else list(text)
|
|
|
|
if use_patch:
|
|
lines, _ = _apply_diff_with_subprocess(diff, lines, reverse)
|
|
return lines
|
|
|
|
n_lines = len(lines)
|
|
|
|
changes = _reverse(diff.changes) if reverse else diff.changes
|
|
# check that the source text matches the context of the diff
|
|
for old, new, line, hunk in changes:
|
|
# might have to check for line is None here for ed scripts
|
|
if old is not None and line is not None:
|
|
if old > n_lines:
|
|
raise HunkApplyException(
|
|
'context line {n}, "{line}" does not exist in source'.format(
|
|
n=old, line=line
|
|
),
|
|
hunk=hunk,
|
|
)
|
|
if lines[old - 1] != line:
|
|
# Try to normalize whitespace by replacing multiple spaces with a single space
|
|
# This helps with patches that have different indentation levels
|
|
normalized_line = ' '.join(line.split())
|
|
normalized_source = ' '.join(lines[old - 1].split())
|
|
if normalized_line != normalized_source:
|
|
raise HunkApplyException(
|
|
'context line {n}, "{line}" does not match "{sl}"'.format(
|
|
n=old, line=line, sl=lines[old - 1]
|
|
),
|
|
hunk=hunk,
|
|
)
|
|
|
|
# for calculating the old line
|
|
r = 0
|
|
i = 0
|
|
|
|
for old, new, line, hunk in changes:
|
|
if old is not None and new is None:
|
|
del lines[old - 1 - r + i]
|
|
r += 1
|
|
elif old is None and new is not None:
|
|
lines.insert(new - 1, line)
|
|
i += 1
|
|
elif old is not None and new is not None:
|
|
# Sometimes, people remove hunks from patches, making these
|
|
# numbers completely unreliable. Because they're jerks.
|
|
pass
|
|
|
|
return lines
|