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
@@ -689,25 +711,24 @@ def _normalize_filenames(self):
689711
690712 [x] always use forward slashes to be crossplatform
691713 (diff/patch were born as a unix utility after all)
692-
714+
693715 return None
694716 """
695717 if debugmode :
696718 debug ("normalize filenames" )
697719 for i ,p in enumerate (self .items ):
698720 if debugmode :
699- debug (" patch type = " + p .type )
700- debug (" source = " + p .source )
701- debug (" target = " + p .target )
721+ debug (" patch type = %s" % p .type )
722+ debug (" source = %s" % p .source )
723+ debug (" target = %s" % p .target )
702724 if p .type in (HG , GIT ):
703- # TODO: figure out how to deal with /dev/null entries
704725 debug ("stripping a/ and b/ prefixes" )
705- if p .source != '/dev/null' :
726+ if p .source != b '/dev/null' :
706727 if not p .source .startswith (b"a/" ):
707728 warning ("invalid source filename" )
708729 else :
709730 p .source = p .source [2 :]
710- if p .target != '/dev/null' :
731+ if p .target != b '/dev/null' :
711732 if not p .target .startswith (b"b/" ):
712733 warning ("invalid target filename" )
713734 else :
@@ -730,16 +751,17 @@ def _normalize_filenames(self):
730751 while p .target .startswith (b".." + sep ):
731752 p .target = p .target .partition (sep )[2 ]
732753 # absolute paths are not allowed
733- 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' ):
734756 warning ("error: absolute paths are not allowed - file no.%d" % (i + 1 ))
735757 self .warnings += 1
736- if xisabs (p .source ):
758+ if xisabs (p .source ) and p . source != b'/dev/null' :
737759 warning ("stripping absolute path from source name '%s'" % p .source )
738760 p .source = xstrip (p .source )
739- if xisabs (p .target ):
761+ if xisabs (p .target ) and p . target != b'/dev/null' :
740762 warning ("stripping absolute path from target name '%s'" % p .target )
741763 p .target = xstrip (p .target )
742-
764+
743765 self .items [i ].source = p .source
744766 self .items [i ].target = p .target
745767
@@ -801,12 +823,24 @@ def diffstat(self):
801823 return output
802824
803825
804- def findfile (self , old , new ):
805- """ return name of file to be patched or None """
806- if exists (old ):
807- 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
808840 elif exists (new ):
809- return new
841+ return new , new
842+ elif new == b'/dev/null' :
843+ return None , None
810844 else :
811845 # [w] Google Code generates broken patches with its online editor
812846 debug ("broken patch from Google Code, stripping prefixes.." )
@@ -815,10 +849,10 @@ def findfile(self, old, new):
815849 debug (" %s" % old )
816850 debug (" %s" % new )
817851 if exists (old ):
818- return old
852+ return old , old
819853 elif exists (new ):
820- return new
821- return None
854+ return new , new
855+ return None , None
822856
823857
824858 def apply (self , strip = 0 , root = None ):
@@ -849,27 +883,27 @@ def apply(self, strip=0, root=None):
849883 debug ("stripping %s leading component(s) from:" % strip )
850884 debug (" %s" % p .source )
851885 debug (" %s" % p .target )
852- old = pathstrip (p .source , strip )
853- 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 )
854888 else :
855889 old , new = p .source , p .target
856890
857- filename = self .findfile (old , new )
891+ filenameo , filenamen = self .findfiles (old , new )
858892
859- if not filename :
893+ if not filenameo or not filenamen :
860894 warning ("source/target file does not exist:\n --- %s\n +++ %s" % (old , new ))
861895 errors += 1
862896 continue
863- if not isfile (filename ):
864- warning ("not a file - %s" % filename )
897+ if not isfile (filenameo ):
898+ warning ("not a file - %s" % filenameo )
865899 errors += 1
866900 continue
867901
868902 # [ ] check absolute paths security here
869- debug ("processing %d/%d:\t %s" % (i + 1 , total , filename ))
903+ debug ("processing %d/%d:\t %s" % (i + 1 , total , filenamen ))
870904
871905 # validate before patching
872- f2fp = open (filename , 'rb' )
906+ f2fp = open (filenameo , 'rb' )
873907 hunkno = 0
874908 hunk = p .hunks [hunkno ]
875909 hunkfind = []
@@ -892,7 +926,7 @@ def apply(self, strip=0, root=None):
892926 if line .rstrip (b"\r \n " ) == hunkfind [hunklineno ]:
893927 hunklineno += 1
894928 else :
895- info ("file %d/%d:\t %s" % (i + 1 , total , filename ))
929+ info ("file %d/%d:\t %s" % (i + 1 , total , filenamen ))
896930 info (" hunk no.%d doesn't match source file at line %d" % (hunkno + 1 , lineno + 1 ))
897931 info (" expected: %s" % hunkfind [hunklineno ])
898932 info (" actual : %s" % line .rstrip (b"\r \n " ))
@@ -912,8 +946,8 @@ def apply(self, strip=0, root=None):
912946 break
913947
914948 # check if processed line is the last line
915- if lineno + 1 == hunk .startsrc + len (hunkfind )- 1 :
916- 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 ))
917951 hunkno += 1
918952 validhunks += 1
919953 if hunkno < len (p .hunks ):
@@ -925,34 +959,39 @@ def apply(self, strip=0, root=None):
925959 break
926960 else :
927961 if hunkno < len (p .hunks ):
928- 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 ))
929963 errors += 1
930964
931965 f2fp .close ()
932966
933967 if validhunks < len (p .hunks ):
934- if self ._match_file_hunks (filename , p .hunks ):
935- warning ("already patched %s" % filename )
968+ if self ._match_file_hunks (filenameo , p .hunks ):
969+ warning ("already patched %s" % filenameo )
936970 else :
937- warning ("source file is different - %s" % filename )
971+ warning ("source file is different - %s" % filenameo )
938972 errors += 1
939973 if canpatch :
940- backupname = filename + b".orig"
974+ backupname = filenamen + b".orig"
941975 if exists (backupname ):
942976 warning ("can't backup original file to %s - aborting" % backupname )
943977 else :
944978 import shutil
945- shutil .move (filename , backupname )
946- if self .write_hunks (backupname , filename , p .hunks ):
947- 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 ))
948982 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 )
949988 else :
950989 errors += 1
951- warning ("error patching file %s" % filename )
952- shutil .copy (filename , filename + ".invalid" )
953- 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" )
954993 # todo: proper rejects
955- shutil .move (backupname , filename )
994+ shutil .move (backupname , filenamen )
956995
957996 if root :
958997 os .chdir (prevdir )
0 commit comments