Skip to content

Commit 5c2518a

Browse files
committed
Add support for creating and removing files
1 parent 240a39d commit 5c2518a

File tree

7 files changed

+107
-41
lines changed

7 files changed

+107
-41
lines changed

patch.py

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import copy
1919
import logging
2020
import re
21+
import tempfile
2122

2223
# cStringIO doesn't support unicode in 2.5
2324
try:
@@ -477,11 +478,26 @@ def lineno(self):
477478
# XXX header += srcname
478479
# double source filename line is encountered
479480
# attempt to restart from this second line
480-
re_filename = b"^--- ([^\t]+)"
481-
match = re.match(re_filename, line)
481+
482+
# Files dated at Unix epoch don't exist, e.g.:
483+
# '1970-01-01 01:00:00.000000000 +0100'
484+
# They include timezone offsets.
485+
# .. which can be parsed (if we remove the nanoseconds)
486+
# .. by strptime() with:
487+
# '%Y-%m-%d %H:%M:%S %z'
488+
# .. but unfortunately this relies on the OSes libc
489+
# strptime function and %z support is patchy, so we drop
490+
# everything from the . onwards and group the year and time
491+
# separately.
492+
re_filename_date_time = b"^--- ([^\t]+)(?:\s([0-9-]+)\s([0-9:]+)|.*)"
493+
match = re.match(re_filename_date_time, line)
482494
# todo: support spaces in filenames
483495
if match:
484496
srcname = match.group(1).strip()
497+
date = match.group(2)
498+
time = match.group(3)
499+
if (date == b'1970-01-01' or date == b'1969-12-31') and time.split(b':',1)[1] == b'00:00':
500+
srcname = b'/dev/null'
485501
else:
486502
warning("skipping invalid filename at line %d" % (lineno+1))
487503
self.errors += 1
@@ -516,8 +532,8 @@ def lineno(self):
516532
filenames = False
517533
headscan = True
518534
else:
519-
re_filename = b"^\+\+\+ ([^\t]+)"
520-
match = re.match(re_filename, line)
535+
re_filename_date_time = b"^\+\+\+ ([^\t]+)(?:\s([0-9-]+)\s([0-9:]+)|.*)"
536+
match = re.match(re_filename_date_time, line)
521537
if not match:
522538
warning("skipping invalid patch - no target filename at line %d" % (lineno+1))
523539
self.errors += 1
@@ -526,12 +542,18 @@ def lineno(self):
526542
filenames = False
527543
headscan = True
528544
else:
545+
tgtname = match.group(1).strip()
546+
date = match.group(2)
547+
time = match.group(3)
548+
if (date == b'1970-01-01' or date == b'1969-12-31') and time.split(b':',1)[1] == b'00:00':
549+
tgtname = b'/dev/null'
529550
if p: # for the first run p is None
530551
self.items.append(p)
531552
p = Patch()
532553
p.source = srcname
533554
srcname = None
534-
p.target = match.group(1).strip()
555+
p.target = tgtname
556+
tgtname = None
535557
p.header = header
536558
header = []
537559
# switch to hunkhead state
@@ -729,16 +751,17 @@ def _normalize_filenames(self):
729751
while p.target.startswith(b".." + sep):
730752
p.target = p.target.partition(sep)[2]
731753
# absolute paths are not allowed
732-
if xisabs(p.source) or xisabs(p.target):
754+
if (xisabs(p.source) and p.source != b'/dev/null') or \
755+
(xisabs(p.target) and p.target != b'/dev/null'):
733756
warning("error: absolute paths are not allowed - file no.%d" % (i+1))
734757
self.warnings += 1
735-
if xisabs(p.source):
758+
if xisabs(p.source) and p.source != b'/dev/null':
736759
warning("stripping absolute path from source name '%s'" % p.source)
737760
p.source = xstrip(p.source)
738-
if xisabs(p.target):
761+
if xisabs(p.target) and p.target != b'/dev/null':
739762
warning("stripping absolute path from target name '%s'" % p.target)
740763
p.target = xstrip(p.target)
741-
764+
742765
self.items[i].source = p.source
743766
self.items[i].target = p.target
744767

@@ -800,12 +823,23 @@ def diffstat(self):
800823
return output
801824

802825

803-
def findfile(self, old, new):
804-
""" return name of file to be patched or None """
805-
if exists(old):
806-
return old
826+
def findfiles(self, old, new):
827+
""" return tuple of source file, target file """
828+
if old == b'/dev/null':
829+
handle, abspath = tempfile.mkstemp(suffix=b'pypatch')
830+
# The source file must contain a line for the hunk matching to succeed.
831+
os.write(handle, b' ')
832+
os.close(handle)
833+
if not exists(new):
834+
handle = open(new, 'wb')
835+
handle.close()
836+
return abspath, new
837+
elif exists(old):
838+
return old, old
807839
elif exists(new):
808-
return new
840+
return new, new
841+
elif new == b'/dev/null':
842+
return None, None
809843
else:
810844
# [w] Google Code generates broken patches with its online editor
811845
debug("broken patch from Google Code, stripping prefixes..")
@@ -814,10 +848,10 @@ def findfile(self, old, new):
814848
debug(" %s" % old)
815849
debug(" %s" % new)
816850
if exists(old):
817-
return old
851+
return old, old
818852
elif exists(new):
819-
return new
820-
return None
853+
return new, new
854+
return None, None
821855

822856

823857
def apply(self, strip=0, root=None):
@@ -848,27 +882,27 @@ def apply(self, strip=0, root=None):
848882
debug("stripping %s leading component(s) from:" % strip)
849883
debug(" %s" % p.source)
850884
debug(" %s" % p.target)
851-
old = pathstrip(p.source, strip)
852-
new = pathstrip(p.target, strip)
885+
old = p.source if p.source == b'/dev/null' else pathstrip(p.source, strip)
886+
new = p.target if p.target == b'/dev/null' else pathstrip(p.target, strip)
853887
else:
854888
old, new = p.source, p.target
855889

856-
filename = self.findfile(old, new)
890+
filenameo, filenamen = self.findfiles(old, new)
857891

858-
if not filename:
892+
if not filenameo or not filenamen:
859893
warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new))
860894
errors += 1
861895
continue
862-
if not isfile(filename):
863-
warning("not a file - %s" % filename)
896+
if not isfile(filenameo):
897+
warning("not a file - %s" % filenameo)
864898
errors += 1
865899
continue
866900

867901
# [ ] check absolute paths security here
868-
debug("processing %d/%d:\t %s" % (i+1, total, filename))
902+
debug("processing %d/%d:\t %s" % (i+1, total, filenamen))
869903

870904
# validate before patching
871-
f2fp = open(filename, 'rb')
905+
f2fp = open(filenameo, 'rb')
872906
hunkno = 0
873907
hunk = p.hunks[hunkno]
874908
hunkfind = []
@@ -891,7 +925,7 @@ def apply(self, strip=0, root=None):
891925
if line.rstrip(b"\r\n") == hunkfind[hunklineno]:
892926
hunklineno+=1
893927
else:
894-
info("file %d/%d:\t %s" % (i+1, total, filename))
928+
info("file %d/%d:\t %s" % (i+1, total, filenamen))
895929
info(" hunk no.%d doesn't match source file at line %d" % (hunkno+1, lineno+1))
896930
info(" expected: %s" % hunkfind[hunklineno])
897931
info(" actual : %s" % line.rstrip(b"\r\n"))
@@ -911,8 +945,8 @@ def apply(self, strip=0, root=None):
911945
break
912946

913947
# check if processed line is the last line
914-
if lineno+1 == hunk.startsrc+len(hunkfind)-1:
915-
debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1, filename))
948+
if len(hunkfind) == 0 or lineno+1 == hunk.startsrc+len(hunkfind)-1:
949+
debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1, filenamen))
916950
hunkno+=1
917951
validhunks+=1
918952
if hunkno < len(p.hunks):
@@ -924,34 +958,39 @@ def apply(self, strip=0, root=None):
924958
break
925959
else:
926960
if hunkno < len(p.hunks):
927-
warning("premature end of source file %s at hunk %d" % (filename, hunkno+1))
961+
warning("premature end of source file %s at hunk %d" % (filenameo, hunkno+1))
928962
errors += 1
929963

930964
f2fp.close()
931965

932966
if validhunks < len(p.hunks):
933-
if self._match_file_hunks(filename, p.hunks):
934-
warning("already patched %s" % filename)
967+
if self._match_file_hunks(filenameo, p.hunks):
968+
warning("already patched %s" % filenameo)
935969
else:
936-
warning("source file is different - %s" % filename)
970+
warning("source file is different - %s" % filenameo)
937971
errors += 1
938972
if canpatch:
939-
backupname = filename+b".orig"
973+
backupname = filenamen+b".orig"
940974
if exists(backupname):
941975
warning("can't backup original file to %s - aborting" % backupname)
942976
else:
943977
import shutil
944-
shutil.move(filename, backupname)
945-
if self.write_hunks(backupname, filename, p.hunks):
946-
info("successfully patched %d/%d:\t %s" % (i+1, total, filename))
978+
shutil.move(filenamen, backupname)
979+
if self.write_hunks(backupname if filenameo == filenamen else filenameo, filenamen, p.hunks):
980+
info("successfully patched %d/%d:\t %s" % (i+1, total, filenamen))
947981
os.unlink(backupname)
982+
if new == b'/dev/null':
983+
# check that filename is of size 0 and delete it.
984+
if os.path.getsize(filenamen) > 0:
985+
warning("expected patched file to be empty as it's marked as deletion:\t %s" % filenamen)
986+
os.unlink(filenamen)
948987
else:
949988
errors += 1
950-
warning("error patching file %s" % filename)
951-
shutil.copy(filename, filename+".invalid")
952-
warning("invalid version is saved to %s" % filename+".invalid")
989+
warning("error patching file %s" % filenamen)
990+
shutil.copy(filenamen, filenamen+".invalid")
991+
warning("invalid version is saved to %s" % filenamen+".invalid")
953992
# todo: proper rejects
954-
shutil.move(backupname, filename)
993+
shutil.move(backupname, filenamen)
955994

956995
if root:
957996
os.chdir(prevdir)

tests/08create/08create.patch

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
diff -urN from/created to/created
2+
--- created 1970-01-01 01:00:00.000000000 +0100
3+
+++ created 2016-06-07 00:43:08.701304500 +0100
4+
@@ -0,0 +1 @@
5+
+Created by patch

tests/08create/[result]/created

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Created by patch

tests/09delete/09delete.patch

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
diff -urN from/deleted to/deleted
2+
--- deleted 2016-06-07 00:44:19.093323300 +0100
3+
+++ deleted 1970-01-01 01:00:00.000000000 +0100
4+
@@ -1 +0,0 @@
5+
-Deleted by patch

tests/09delete/[result]/.gitkeep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

tests/09delete/deleted

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Deleted by patch

tests/run_tests.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def _run_test(self, testname):
169169
# recursive comparison
170170
self._assert_dirs_equal(join(basepath, "[result]"),
171171
tmpdir,
172-
ignore=["%s.patch" % testname, ".svn", "[result]"])
172+
ignore=["%s.patch" % testname, ".svn", ".gitkeep", "[result]"])
173173

174174

175175
shutil.rmtree(tmpdir)
@@ -410,6 +410,20 @@ def test_apply_strip(self):
410410
p.target = b'nasty/prefix/' + p.target
411411
self.assertTrue(pto.apply(strip=2, root=treeroot))
412412

413+
def test_create_file(self):
414+
treeroot = join(self.tmpdir, 'rootparent')
415+
os.makedirs(treeroot)
416+
pto = patch.fromfile(join(TESTS, '08create/08create.patch'))
417+
pto.apply(strip=0, root=treeroot)
418+
self.assertTrue(os.path.exists(os.path.join(treeroot, 'created')))
419+
420+
def test_delete_file(self):
421+
treeroot = join(self.tmpdir, 'rootparent')
422+
shutil.copytree(join(TESTS, '09delete'), treeroot)
423+
pto = patch.fromfile(join(TESTS, '09delete/09delete.patch'))
424+
pto.apply(strip=0, root=treeroot)
425+
self.assertFalse(os.path.exists(os.path.join(treeroot, 'deleted')))
426+
413427

414428
class TestHelpers(unittest.TestCase):
415429
# unittest setting

0 commit comments

Comments
 (0)