@@ -274,6 +274,7 @@ def __init__(self):
274274
275275 self .type = None
276276 self .filemode = None
277+ self .mode = None
277278
278279 def __iter__ (self ):
279280 return iter (self .hunks )
@@ -378,6 +379,7 @@ def lineno(self):
378379 header = []
379380 srcname = None
380381 tgtname = None
382+ rename = False
381383
382384 # start of main cycle
383385 # each parsing block already has line available in fe.line
@@ -388,19 +390,26 @@ def lineno(self):
388390 # -- line fetched at the start of this cycle
389391 if hunkparsed :
390392 hunkparsed = False
393+ rename = False
391394 if re_hunk_start .match (fe .line ):
392395 hunkhead = True
393396 elif fe .line .startswith (b"--- " ):
394397 filenames = True
398+ elif fe .line .startswith (b"rename from " ):
399+ filenames = True
395400 else :
396401 headscan = True
397402 # -- ------------------------------------
398403
399404 # read out header
400405 if headscan :
401- while not fe .is_empty and not fe .line .startswith (b"--- " ):
402- header .append (fe .line )
403- fe .next ()
406+ while not fe .is_empty and not fe .line .startswith (b"--- " ) and not fe .line .startswith (b"rename from " ):
407+ header .append (fe .line )
408+ fe .next ()
409+ if not fe .is_empty and fe .line .startswith (b"rename from " ):
410+ rename = True
411+ hunkskip = True
412+ hunkbody = False
404413 if fe .is_empty :
405414 if p is None :
406415 debug ("no patch data found" ) # error is shown later
@@ -495,7 +504,7 @@ def lineno(self):
495504 # switch to hunkhead state
496505 hunkskip = False
497506 hunkhead = True
498- elif line .startswith (b"--- " ):
507+ elif line .startswith (b"--- " ) or line . startswith ( b"rename from " ) :
499508 # switch to filenames state
500509 hunkskip = False
501510 filenames = True
@@ -538,6 +547,50 @@ def lineno(self):
538547 # switch back to headscan state
539548 filenames = False
540549 headscan = True
550+ elif rename :
551+ if line .startswith (b"rename from " ):
552+ re_rename_from = br"^rename from (.+)"
553+ match = re .match (re_rename_from , line )
554+ if match :
555+ srcname = match .group (1 ).strip ()
556+ else :
557+ warning ("skipping invalid rename from at line %d" % (lineno + 1 ))
558+ self .errors += 1
559+ # XXX p.header += line
560+ # switch back to headscan state
561+ filenames = False
562+ headscan = True
563+ if not fe .is_empty :
564+ fe .next ()
565+ line = fe .line
566+ lineno = fe .lineno
567+ re_rename_to = br"^rename to (.+)"
568+ match = re .match (re_rename_to , line )
569+ if match :
570+ tgtname = match .group (1 ).strip ()
571+ else :
572+ warning ("skipping invalid rename from at line %d" % (lineno + 1 ))
573+ self .errors += 1
574+ # XXX p.header += line
575+ # switch back to headscan state
576+ filenames = False
577+ headscan = True
578+ if p : # for the first run p is None
579+ self .items .append (p )
580+ p = Patch ()
581+ p .source = srcname
582+ srcname = None
583+ p .target = tgtname
584+ tgtname = None
585+ p .header = header
586+ header = []
587+ # switch to hunkhead state
588+ filenames = False
589+ hunkhead = False
590+ nexthunkno = 0
591+ p .hunkends = lineends .copy ()
592+ hunkparsed = True
593+ continue
541594 elif not line .startswith (b"+++ " ):
542595 if srcname != None :
543596 warning ("skipping invalid patch with no target for %s" % srcname )
@@ -664,6 +717,7 @@ def lineno(self):
664717 self .items [idx ].type = self ._detect_type (p )
665718 if self .items [idx ].type == GIT :
666719 self .items [idx ].filemode = self ._detect_file_mode (p )
720+ self .items [idx ].mode = self ._detect_patch_mode (p )
667721
668722 types = set ([p .type for p in self .items ])
669723 if len (types ) > 1 :
@@ -720,38 +774,26 @@ def _detect_type(self, p):
720774 if DVCS :
721775 return GIT
722776
723- # Additional check: look for mode change patterns
724- # "old mode XXXXX" followed by "new mode XXXXX"
725- has_old_mode = False
726- has_new_mode = False
727-
728- for line in git_indicators :
729- if re .match (b'old mode \\ d+' , line ):
730- has_old_mode = True
731- elif re .match (b'new mode \\ d+' , line ):
732- has_new_mode = True
733-
734- # If we have both old and new mode, it's definitely Git
735- if has_old_mode and has_new_mode and DVCS :
736- return GIT
737-
738- # Check for similarity index (Git renames/copies)
739- for line in git_indicators :
740- if re .match (b'similarity index \\ d+%' , line ):
741- if DVCS :
742- return GIT
743-
744- # Check for rename patterns
745- for line in git_indicators :
746- if re .match (b'rename from .+' , line ) or re .match (b'rename to .+' , line ):
747- if DVCS :
748- return GIT
749-
750- # Check for copy patterns
751- for line in git_indicators :
752- if re .match (b'copy from .+' , line ) or re .match (b'copy to .+' , line ):
753- if DVCS :
754- return GIT
777+ # Additional check: look for mode change patterns
778+ # "old mode XXXXX" followed by "new mode XXXXX"
779+ has_old_mode = False
780+ has_new_mode = False
781+
782+ for line in git_indicators :
783+ if re .match (b'old mode \\ d+' , line ):
784+ has_old_mode = True
785+ elif re .match (b'new mode \\ d+' , line ):
786+ has_new_mode = True
787+
788+ # If we have both old and new mode, it's definitely Git
789+ if has_old_mode and has_new_mode and DVCS :
790+ return GIT
791+
792+ # Check for similarity index (Git renames/copies)
793+ for line in git_indicators :
794+ if re .match (b'similarity index \\ d+%' , line ):
795+ return GIT
796+
755797 # HG check
756798 #
757799 # - for plain HG format header is like "diff -r b2d9961ff1f5 filename"
@@ -809,6 +851,20 @@ def _apply_filemode(self, filepath, filemode):
809851 except Exception as error :
810852 warning (f"Could not set filemode { oct (filemode )} for { filepath } : { str (error )} " )
811853
854+ def _detect_patch_mode (self , p ):
855+ """Detect patch mode - add, delete, rename, etc.
856+ """
857+ if len (p .header ) > 1 :
858+ for idx in reversed (range (len (p .header ))):
859+ if p .header [idx ].startswith (b"diff --git" ):
860+ break
861+ change_pattern = re .compile (rb"^diff --git a/([^ ]+) b/(.+)" )
862+ match = change_pattern .match (p .header [idx ])
863+ if match :
864+ if match .group (1 ) != match .group (2 ) and not p .hunks and p .source != b'/dev/null' and p .target != b'/dev/null' :
865+ return 'rename'
866+ return None
867+
812868 def _normalize_filenames (self ):
813869 """ sanitize filenames, normalizing paths, i.e.:
814870 1. strip a/ and b/ prefixes from GIT and HG style patches
@@ -1006,6 +1062,13 @@ def apply(self, strip=0, root=None, fuzz=False):
10061062 elif "dev/null" in target :
10071063 source = self .strip_path (source , root , strip )
10081064 safe_unlink (source )
1065+ elif item .mode == 'rename' :
1066+ source = self .strip_path (source , root , strip )
1067+ target = self .strip_path (target , root , strip )
1068+ if exists (source ):
1069+ os .makedirs (os .path .dirname (target ), exist_ok = True )
1070+ shutil .move (source , target )
1071+ self ._apply_filemode (target , item .filemode )
10091072 else :
10101073 items .append (item )
10111074 self .items = items
0 commit comments