Skip to content

Commit 7017c57

Browse files
authored
Merge pull request #6 from conan-io/hotfix/fuzzy-patch
Support fuzzy patch
2 parents 2780af1 + e9adaf8 commit 7017c57

File tree

12 files changed

+118
-42
lines changed

12 files changed

+118
-42
lines changed

patch_ng.py

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from __future__ import print_function
3232

3333
__author__ = "Conan.io <info@conan.io>"
34-
__version__ = "1.17.1"
34+
__version__ = "1.17.2"
3535
__license__ = "MIT"
3636
__url__ = "https://github.com/conan-io/python-patch"
3737

@@ -82,11 +82,12 @@ def tostr(b):
8282
# Logging is controlled by logger named after the
8383
# module name (e.g. 'patch' for patch_ng.py module)
8484

85-
logger = logging.getLogger(__name__)
85+
logger = logging.getLogger("patch_ng")
8686

8787
debug = logger.debug
8888
info = logger.info
8989
warning = logger.warning
90+
error = logger.error
9091

9192
class NullHandler(logging.Handler):
9293
""" Copied from Python 2.7 to avoid getting
@@ -305,16 +306,6 @@ def __init__(self):
305306
self.desc=''
306307
self.text=[]
307308

308-
# def apply(self, estream):
309-
# """ write hunk data into enumerable stream
310-
# return strings one by one until hunk is
311-
# over
312-
#
313-
# enumerable stream are tuples (lineno, line)
314-
# where lineno starts with 0
315-
# """
316-
# pass
317-
318309

319310
class Patch(object):
320311
""" Patch for a single file.
@@ -966,9 +957,12 @@ def strip_path(self, path, base_path, strip=0):
966957

967958

968959

969-
def apply(self, strip=0, root=None):
960+
def apply(self, strip=0, root=None, fuzz=False):
970961
""" Apply parsed patch, optionally stripping leading components
971962
from file paths. `root` parameter specifies working dir.
963+
:param strip: Strip patch path
964+
:param root: Folder to apply the patch
965+
:param fuzz: Accept fuzzy patches
972966
return True on success
973967
"""
974968
items = []
@@ -1018,11 +1012,11 @@ def apply(self, strip=0, root=None):
10181012
filenameo, filenamen = self.findfiles(old, new)
10191013

10201014
if not filenameo or not filenamen:
1021-
warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new))
1015+
error("source/target file does not exist:\n --- %s\n +++ %s" % (old, new))
10221016
errors += 1
10231017
continue
10241018
if not isfile(filenameo):
1025-
warning("not a file - %s" % filenameo)
1019+
error("not a file - %s" % filenameo)
10261020
errors += 1
10271021
continue
10281022

@@ -1049,28 +1043,31 @@ def apply(self, strip=0, root=None):
10491043
# todo \ No newline at end of file
10501044

10511045
# check hunks in source file
1052-
if lineno+1 < hunk.startsrc+len(hunkfind)-1:
1046+
if lineno+1 < hunk.startsrc+len(hunkfind):
10531047
if line.rstrip(b"\r\n") == hunkfind[hunklineno]:
1054-
hunklineno+=1
1048+
hunklineno += 1
10551049
else:
1056-
info("file %d/%d:\t %s" % (i+1, total, filenamen))
1057-
info(" hunk no.%d doesn't match source file at line %d" % (hunkno+1, lineno+1))
1058-
info(" expected: %s" % hunkfind[hunklineno])
1059-
info(" actual : %s" % line.rstrip(b"\r\n"))
1060-
# not counting this as error, because file may already be patched.
1061-
# check if file is already patched is done after the number of
1062-
# invalid hunks if found
1063-
# TODO: check hunks against source/target file in one pass
1064-
# API - check(stream, srchunks, tgthunks)
1065-
# return tuple (srcerrs, tgterrs)
1066-
1067-
# continue to check other hunks for completeness
1068-
hunkno += 1
1069-
if hunkno < len(p.hunks):
1070-
hunk = p.hunks[hunkno]
1071-
continue
1050+
warning("file %d/%d:\t %s" % (i+1, total, filenamen))
1051+
warning(" hunk no.%d doesn't match source file at line %d" % (hunkno+1, lineno+1))
1052+
warning(" expected: %s" % hunkfind[hunklineno])
1053+
warning(" actual : %s" % line.rstrip(b"\r\n"))
1054+
if fuzz:
1055+
hunklineno += 1
10721056
else:
1073-
break
1057+
# not counting this as error, because file may already be patched.
1058+
# check if file is already patched is done after the number of
1059+
# invalid hunks if found
1060+
# TODO: check hunks against source/target file in one pass
1061+
# API - check(stream, srchunks, tgthunks)
1062+
# return tuple (srcerrs, tgterrs)
1063+
1064+
# continue to check other hunks for completeness
1065+
hunkno += 1
1066+
if hunkno < len(p.hunks):
1067+
hunk = p.hunks[hunkno]
1068+
continue
1069+
else:
1070+
break
10741071

10751072
# check if processed line is the last line
10761073
if len(hunkfind) == 0 or lineno+1 == hunk.startsrc+len(hunkfind)-1:
@@ -1086,7 +1083,7 @@ def apply(self, strip=0, root=None):
10861083
break
10871084
else:
10881085
if hunkno < len(p.hunks):
1089-
warning("premature end of source file %s at hunk %d" % (filenameo, hunkno+1))
1086+
error("premature end of source file %s at hunk %d" % (filenameo, hunkno+1))
10901087
errors += 1
10911088

10921089
f2fp.close()
@@ -1095,8 +1092,11 @@ def apply(self, strip=0, root=None):
10951092
if self._match_file_hunks(filenameo, p.hunks):
10961093
warning("already patched %s" % filenameo)
10971094
else:
1098-
warning("source file is different - %s" % filenameo)
1099-
errors += 1
1095+
if fuzz:
1096+
warning("source file is different - %s" % filenameo)
1097+
else:
1098+
error("source file is different - %s" % filenameo)
1099+
errors += 1
11001100
if canpatch:
11011101
backupname = filenamen+b".orig"
11021102
if exists(backupname):
@@ -1247,8 +1247,9 @@ def get_line():
12471247
continue
12481248
else:
12491249
if not hline.startswith(b"+"):
1250-
get_line()
1250+
yield get_line()
12511251
srclineno += 1
1252+
continue
12521253
line2write = hline[1:]
12531254
# detect if line ends are consistent in source file
12541255
if sum([bool(lineends[x]) for x in lineends]) == 1:
@@ -1310,6 +1311,7 @@ def main():
13101311
help="strip N path components from filenames")
13111312
opt.add_option("--revert", action="store_true",
13121313
help="apply patch in reverse order (unpatch)")
1314+
opt.add_option("-f", "--fuzz", action="store_true", dest="fuzz", help="Accept fuuzzy patches")
13131315
(options, args) = opt.parse_args()
13141316

13151317
if not args and sys.argv[-1:] != ['--']:
@@ -1344,11 +1346,15 @@ def main():
13441346
print(patch.diffstat())
13451347
sys.exit(0)
13461348

1349+
if not patch:
1350+
error("Could not parse patch")
1351+
sys.exit(-1)
1352+
13471353
#pprint(patch)
13481354
if options.revert:
13491355
patch.revert(options.strip, root=options.directory) or sys.exit(-1)
13501356
else:
1351-
patch.apply(options.strip, root=options.directory) or sys.exit(-1)
1357+
patch.apply(options.strip, root=options.directory, fuzz=options.fuzz) or sys.exit(-1)
13521358

13531359
# todo: document and test line ends handling logic - patch_ng.py detects proper line-endings
13541360
# for inserted hunks and issues a warning if patched file has incosistent line ends

tests/07google_code_wiki.from

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,4 @@ How to create your own I/O Spyder plugins:
8484

8585
== Other Spyder plugins ==
8686

87-
See the example of the `pylint` third-party plugin in Spyder v2.0.
87+
See the example of the `pylint` third-party plugin in Spyder v2.0.

tests/10fuzzy/10fuzzy.patch

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
diff --git a/Jamroot b/Jamroot
2+
index a6981dd..0c08f09 100644
3+
--- a/Jamroot
4+
+++ b/Jamroot
5+
@@ -1,3 +1,4 @@
6+
X
7+
YYYY
8+
+V
9+
W

tests/10fuzzy/Jamroot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
X
2+
Y
3+
Z

tests/10fuzzy/[result]/Jamroot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
X
2+
Y
3+
V
4+
Z

tests/10fuzzyafter/10fuzzyafter.patch

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
diff --git a/Jamroot b/Jamroot
2+
index a6981dd..0c08f09 100644
3+
--- a/Jamroot
4+
+++ b/Jamroot
5+
@@ -1,3 +1,4 @@
6+
X
7+
Y
8+
+V
9+
W

tests/10fuzzyafter/Jamroot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
X
2+
Y
3+
Z

tests/10fuzzyafter/[result]/Jamroot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
X
2+
Y
3+
V
4+
Z
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
diff --git a/Jamroot b/Jamroot
2+
index a6981dd..0c08f09 100644
3+
--- a/Jamroot
4+
+++ b/Jamroot
5+
@@ -1,3 +1,4 @@
6+
T
7+
YYYY
8+
+V
9+
Z

tests/10fuzzybefore/Jamroot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
X
2+
Y
3+
Z

tests/10fuzzybefore/[result]/Jamroot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
X
2+
Y
3+
V
4+
Z

tests/run_tests.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,12 @@ def _run_test(self, testname):
151151
patch_tool = join(dirname(TESTS), "patch_ng.py")
152152
save_cwd = getcwdu()
153153
os.chdir(tmpdir)
154+
extra = "-f" if "10fuzzy" in testname else ""
154155
if verbose:
155-
cmd = '%s %s "%s"' % (sys.executable, patch_tool, patch_file)
156+
cmd = '%s %s %s "%s"' % (sys.executable, patch_tool, extra, patch_file)
156157
print("\n"+cmd)
157158
else:
158-
cmd = '%s %s -q "%s"' % (sys.executable, patch_tool, patch_file)
159+
cmd = '%s %s -q %s "%s"' % (sys.executable, patch_tool, extra, patch_file)
159160
ret = os.system(cmd)
160161
assert ret == 0, "Error %d running test %s" % (ret, testname)
161162
os.chdir(save_cwd)
@@ -424,6 +425,27 @@ def test_delete_file(self):
424425
pto.apply(strip=0, root=treeroot)
425426
self.assertFalse(os.path.exists(os.path.join(treeroot, 'deleted')))
426427

428+
def test_fuzzy_patch_both(self):
429+
treeroot = join(self.tmpdir, 'rootparent')
430+
shutil.copytree(join(TESTS, '10fuzzy'), treeroot)
431+
pto = patch_ng.fromfile(join(TESTS, '10fuzzy/10fuzzy.patch'))
432+
self.assertTrue(pto.apply(root=treeroot, fuzz=True))
433+
self.assertFalse(pto.apply(root=treeroot, fuzz=False))
434+
435+
def test_fuzzy_patch_before(self):
436+
treeroot = join(self.tmpdir, 'rootparent')
437+
shutil.copytree(join(TESTS, '10fuzzybefore'), treeroot)
438+
pto = patch_ng.fromfile(join(TESTS, '10fuzzybefore/10fuzzybefore.patch'))
439+
self.assertTrue(pto.apply(root=treeroot, fuzz=True))
440+
self.assertFalse(pto.apply(root=treeroot, fuzz=False))
441+
442+
def test_fuzzy_patch_after(self):
443+
treeroot = join(self.tmpdir, 'rootparent')
444+
shutil.copytree(join(TESTS, '10fuzzyafter'), treeroot)
445+
pto = patch_ng.fromfile(join(TESTS, '10fuzzyafter/10fuzzyafter.patch'))
446+
self.assertTrue(pto.apply(root=treeroot, fuzz=True))
447+
self.assertFalse(pto.apply(root=treeroot, fuzz=False))
448+
427449

428450
class TestHelpers(unittest.TestCase):
429451
# unittest setting

0 commit comments

Comments
 (0)