1818import copy
1919import logging
2020import re
21+ import tempfile
2122
2223# cStringIO doesn't support unicode in 2.5
2324try :
@@ -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
@@ -654,7 +676,7 @@ def _detect_type(self, p):
654676 break
655677 if p .header [idx ].startswith (b'diff --git a/' ):
656678 if (idx + 1 < len (p .header )
657- and re .match (b'index \\ w{7}..\\ w{7} \\ d{6}' , p .header [idx + 1 ])):
679+ and re .match (b'(?: index \\ w{7}..\\ w{7} \\ d{6}|new file mode \\ d*) ' , p .header [idx + 1 ])):
658680 if DVCS :
659681 return GIT
660682
@@ -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,24 @@ 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 = 'pypatch' )
830+ abspath = abspath .encode ()
831+ # The source file must contain a line for the hunk matching to succeed.
832+ os .write (handle , b' ' )
833+ os .close (handle )
834+ if not exists (new ):
835+ handle = open (new , 'wb' )
836+ handle .close ()
837+ return abspath , new
838+ elif exists (old ):
839+ return old , old
807840 elif exists (new ):
808- return new
841+ return new , new
842+ elif new == b'/dev/null' :
843+ return None , None
809844 else :
810845 # [w] Google Code generates broken patches with its online editor
811846 debug ("broken patch from Google Code, stripping prefixes.." )
@@ -814,10 +849,10 @@ def findfile(self, old, new):
814849 debug (" %s" % old )
815850 debug (" %s" % new )
816851 if exists (old ):
817- return old
852+ return old , old
818853 elif exists (new ):
819- return new
820- return None
854+ return new , new
855+ return None , None
821856
822857
823858 def apply (self , strip = 0 , root = None ):
@@ -848,27 +883,27 @@ def apply(self, strip=0, root=None):
848883 debug ("stripping %s leading component(s) from:" % strip )
849884 debug (" %s" % p .source )
850885 debug (" %s" % p .target )
851- old = pathstrip (p .source , strip )
852- new = pathstrip (p .target , strip )
886+ old = p . source if p . source == b'/dev/null' else pathstrip (p .source , strip )
887+ new = p . target if p . target == b'/dev/null' else pathstrip (p .target , strip )
853888 else :
854889 old , new = p .source , p .target
855890
856- filename = self .findfile (old , new )
891+ filenameo , filenamen = self .findfiles (old , new )
857892
858- if not filename :
893+ if not filenameo or not filenamen :
859894 warning ("source/target file does not exist:\n --- %s\n +++ %s" % (old , new ))
860895 errors += 1
861896 continue
862- if not isfile (filename ):
863- warning ("not a file - %s" % filename )
897+ if not isfile (filenameo ):
898+ warning ("not a file - %s" % filenameo )
864899 errors += 1
865900 continue
866901
867902 # [ ] check absolute paths security here
868- debug ("processing %d/%d:\t %s" % (i + 1 , total , filename ))
903+ debug ("processing %d/%d:\t %s" % (i + 1 , total , filenamen ))
869904
870905 # validate before patching
871- f2fp = open (filename , 'rb' )
906+ f2fp = open (filenameo , 'rb' )
872907 hunkno = 0
873908 hunk = p .hunks [hunkno ]
874909 hunkfind = []
@@ -891,7 +926,7 @@ def apply(self, strip=0, root=None):
891926 if line .rstrip (b"\r \n " ) == hunkfind [hunklineno ]:
892927 hunklineno += 1
893928 else :
894- info ("file %d/%d:\t %s" % (i + 1 , total , filename ))
929+ info ("file %d/%d:\t %s" % (i + 1 , total , filenamen ))
895930 info (" hunk no.%d doesn't match source file at line %d" % (hunkno + 1 , lineno + 1 ))
896931 info (" expected: %s" % hunkfind [hunklineno ])
897932 info (" actual : %s" % line .rstrip (b"\r \n " ))
@@ -911,8 +946,8 @@ def apply(self, strip=0, root=None):
911946 break
912947
913948 # 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 ))
949+ if len ( hunkfind ) == 0 or lineno + 1 == hunk .startsrc + len (hunkfind )- 1 :
950+ debug (" hunk no.%d for file %s -- is ready to be patched" % (hunkno + 1 , filenamen ))
916951 hunkno += 1
917952 validhunks += 1
918953 if hunkno < len (p .hunks ):
@@ -924,34 +959,39 @@ def apply(self, strip=0, root=None):
924959 break
925960 else :
926961 if hunkno < len (p .hunks ):
927- warning ("premature end of source file %s at hunk %d" % (filename , hunkno + 1 ))
962+ warning ("premature end of source file %s at hunk %d" % (filenameo , hunkno + 1 ))
928963 errors += 1
929964
930965 f2fp .close ()
931966
932967 if validhunks < len (p .hunks ):
933- if self ._match_file_hunks (filename , p .hunks ):
934- warning ("already patched %s" % filename )
968+ if self ._match_file_hunks (filenameo , p .hunks ):
969+ warning ("already patched %s" % filenameo )
935970 else :
936- warning ("source file is different - %s" % filename )
971+ warning ("source file is different - %s" % filenameo )
937972 errors += 1
938973 if canpatch :
939- backupname = filename + b".orig"
974+ backupname = filenamen + b".orig"
940975 if exists (backupname ):
941976 warning ("can't backup original file to %s - aborting" % backupname )
942977 else :
943978 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 ))
979+ shutil .move (filenamen , backupname )
980+ if self .write_hunks (backupname if filenameo == filenamen else filenameo , filenamen , p .hunks ):
981+ info ("successfully patched %d/%d:\t %s" % (i + 1 , total , filenamen ))
947982 os .unlink (backupname )
983+ if new == b'/dev/null' :
984+ # check that filename is of size 0 and delete it.
985+ if os .path .getsize (filenamen ) > 0 :
986+ warning ("expected patched file to be empty as it's marked as deletion:\t %s" % filenamen )
987+ os .unlink (filenamen )
948988 else :
949989 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" )
990+ warning ("error patching file %s" % filenamen )
991+ shutil .copy (filenamen , filenamen + ".invalid" )
992+ warning ("invalid version is saved to %s" % filenamen + ".invalid" )
953993 # todo: proper rejects
954- shutil .move (backupname , filename )
994+ shutil .move (backupname , filenamen )
955995
956996 if root :
957997 os .chdir (prevdir )
0 commit comments