33#
44# This module is part of GitPython and is released under
55# the BSD License: http://www.opensource.org/licenses/bsd-license.php
6- import re
76
7+ import re
88from git .cmd import handle_process_output
99from git .compat import defenc
1010from git .util import finalize_process ,hex_to_bin
1313from .objects .util import mode_str_to_int
1414
1515
16+ # typing ------------------------------------------------------------------
17+
18+ from .objects .tree import Tree
19+ from git .repo .base import Repo
20+ from typing_extensions import Final ,Literal
21+ from git .types import TBD
22+ from typing import Any ,Iterator ,List ,Match ,Optional ,Tuple ,Type ,Union
23+ Lit_change_type = Literal ['A' ,'D' ,'M' ,'R' ,'T' ]
24+
25+ # ------------------------------------------------------------------------
26+
1627__all__ = ('Diffable' ,'DiffIndex' ,'Diff' ,'NULL_TREE' )
1728
1829# Special object to compare against the empty tree in diffs
19- NULL_TREE = object ()
30+ NULL_TREE : Final [ object ] = object ()
2031
2132_octal_byte_re = re .compile (b'\\ \\ ([0-9]{3})' )
2233
2334
24- def _octal_repl (matchobj ) :
35+ def _octal_repl (matchobj : Match ) -> bytes :
2536value = matchobj .group (1 )
2637value = int (value ,8 )
2738value = bytes (bytearray ((value ,)))
2839return value
2940
3041
31- def decode_path (path ,has_ab_prefix = True ):
42+ def decode_path (path : bytes ,has_ab_prefix : bool = True )-> Optional [ bytes ] :
3243if path == b'/dev/null' :
3344return None
3445
@@ -60,15 +71,17 @@ class Diffable(object):
6071class Index (object ):
6172pass
6273
63- def _process_diff_args (self ,args ) :
74+ def _process_diff_args (self ,args : List [ Union [ str , 'Diffable' , object ]]) -> List [ Union [ str , 'Diffable' , object ]] :
6475"""
6576 :return:
6677 possibly altered version of the given args list.
6778 Method is called right before git command execution.
6879 Subclasses can use it to alter the behaviour of the superclass"""
6980return args
7081
71- def diff (self ,other = Index ,paths = None ,create_patch = False ,** kwargs ):
82+ def diff (self ,other :Union [Type [Index ],Type [Tree ],object ,None ,str ]= Index ,
83+ paths :Union [str ,List [str ],Tuple [str , ...],None ]= None ,
84+ create_patch :bool = False ,** kwargs :Any )-> 'DiffIndex' :
7285"""Creates diffs between two items being trees, trees and index or an
7386 index and the working tree. It will detect renames automatically.
7487
@@ -99,7 +112,7 @@ def diff(self, other=Index, paths=None, create_patch=False, **kwargs):
99112 :note:
100113 On a bare repository, 'other' needs to be provided as Index or as
101114 as Tree/Commit, or a git command error will occur"""
102- args = []
115+ args = []# type: List[Union[str, Diffable, object]]
103116args .append ("--abbrev=40" )# we need full shas
104117args .append ("--full-index" )# get full index paths, not only filenames
105118
@@ -117,6 +130,9 @@ def diff(self, other=Index, paths=None, create_patch=False, **kwargs):
117130if paths is not None and not isinstance (paths , (tuple ,list )):
118131paths = [paths ]
119132
133+ if hasattr (self ,'repo' ):# else raise Error?
134+ self .repo = self .repo # type: 'Repo'
135+
120136diff_cmd = self .repo .git .diff
121137if other is self .Index :
122138args .insert (0 ,'--cached' )
@@ -163,7 +179,7 @@ class DiffIndex(list):
163179# T = Changed in the type
164180change_type = ("A" ,"C" ,"D" ,"R" ,"M" ,"T" )
165181
166- def iter_change_type (self ,change_type ) :
182+ def iter_change_type (self ,change_type : Lit_change_type ) -> Iterator [ 'Diff' ] :
167183"""
168184 :return:
169185 iterator yielding Diff instances that match the given change_type
@@ -180,7 +196,7 @@ def iter_change_type(self, change_type):
180196if change_type not in self .change_type :
181197raise ValueError ("Invalid change type: %s" % change_type )
182198
183- for diff in self :
199+ for diff in self :# type: 'Diff'
184200if diff .change_type == change_type :
185201yield diff
186202elif change_type == "A" and diff .new_file :
@@ -255,22 +271,21 @@ class Diff(object):
255271"new_file" ,"deleted_file" ,"copied_file" ,"raw_rename_from" ,
256272"raw_rename_to" ,"diff" ,"change_type" ,"score" )
257273
258- def __init__ (self ,repo ,a_rawpath ,b_rawpath ,a_blob_id ,b_blob_id ,a_mode ,
259- b_mode ,new_file ,deleted_file ,copied_file ,raw_rename_from ,
260- raw_rename_to ,diff ,change_type ,score ):
261-
262- self .a_mode = a_mode
263- self .b_mode = b_mode
274+ def __init__ (self ,repo :Repo ,
275+ a_rawpath :Optional [bytes ],b_rawpath :Optional [bytes ],
276+ a_blob_id :Union [str ,bytes ,None ],b_blob_id :Union [str ,bytes ,None ],
277+ a_mode :Union [bytes ,str ,None ],b_mode :Union [bytes ,str ,None ],
278+ new_file :bool ,deleted_file :bool ,copied_file :bool ,
279+ raw_rename_from :Optional [bytes ],raw_rename_to :Optional [bytes ],
280+ diff :Union [str ,bytes ,None ],change_type :Optional [str ],score :Optional [int ])-> None :
264281
265282assert a_rawpath is None or isinstance (a_rawpath ,bytes )
266283assert b_rawpath is None or isinstance (b_rawpath ,bytes )
267284self .a_rawpath = a_rawpath
268285self .b_rawpath = b_rawpath
269286
270- if self .a_mode :
271- self .a_mode = mode_str_to_int (self .a_mode )
272- if self .b_mode :
273- self .b_mode = mode_str_to_int (self .b_mode )
287+ self .a_mode = mode_str_to_int (a_mode )if a_mode else None
288+ self .b_mode = mode_str_to_int (b_mode )if b_mode else None
274289
275290# Determine whether this diff references a submodule, if it does then
276291# we need to overwrite "repo" to the corresponding submodule's repo instead
@@ -305,27 +320,27 @@ def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode,
305320self .change_type = change_type
306321self .score = score
307322
308- def __eq__ (self ,other ) :
323+ def __eq__ (self ,other : object ) -> bool :
309324for name in self .__slots__ :
310325if getattr (self ,name )!= getattr (other ,name ):
311326return False
312327# END for each name
313328return True
314329
315- def __ne__ (self ,other ) :
330+ def __ne__ (self ,other : object ) -> bool :
316331return not (self == other )
317332
318- def __hash__ (self ):
333+ def __hash__ (self )-> int :
319334return hash (tuple (getattr (self ,n )for n in self .__slots__ ))
320335
321- def __str__ (self ):
322- h = "%s"
336+ def __str__ (self )-> str :
337+ h = "%s" # type: str
323338if self .a_blob :
324339h %= self .a_blob .path
325340elif self .b_blob :
326341h %= self .b_blob .path
327342
328- msg = ''
343+ msg = '' # type: str
329344line = None # temp line
330345line_length = 0 # line length
331346for b ,n in zip ((self .a_blob ,self .b_blob ), ('lhs' ,'rhs' )):
@@ -354,7 +369,7 @@ def __str__(self):
354369if self .diff :
355370msg += '\n ---'
356371try :
357- msg += self .diff .decode (defenc )
372+ msg += self .diff .decode (defenc )if isinstance ( self . diff , bytes ) else self . diff
358373except UnicodeDecodeError :
359374msg += 'OMITTED BINARY DATA'
360375# end handle encoding
@@ -368,36 +383,36 @@ def __str__(self):
368383return res
369384
370385@property
371- def a_path (self ):
386+ def a_path (self )-> Optional [ str ] :
372387return self .a_rawpath .decode (defenc ,'replace' )if self .a_rawpath else None
373388
374389@property
375- def b_path (self ):
390+ def b_path (self )-> Optional [ str ] :
376391return self .b_rawpath .decode (defenc ,'replace' )if self .b_rawpath else None
377392
378393@property
379- def rename_from (self ):
394+ def rename_from (self )-> Optional [ str ] :
380395return self .raw_rename_from .decode (defenc ,'replace' )if self .raw_rename_from else None
381396
382397@property
383- def rename_to (self ):
398+ def rename_to (self )-> Optional [ str ] :
384399return self .raw_rename_to .decode (defenc ,'replace' )if self .raw_rename_to else None
385400
386401@property
387- def renamed (self ):
402+ def renamed (self )-> bool :
388403""":returns: True if the blob of our diff has been renamed
389404 :note: This property is deprecated, please use ``renamed_file`` instead.
390405 """
391406return self .renamed_file
392407
393408@property
394- def renamed_file (self ):
409+ def renamed_file (self )-> bool :
395410""":returns: True if the blob of our diff has been renamed
396411 """
397412return self .rename_from != self .rename_to
398413
399414@classmethod
400- def _pick_best_path (cls ,path_match ,rename_match ,path_fallback_match ) :
415+ def _pick_best_path (cls ,path_match : bytes ,rename_match : bytes ,path_fallback_match : bytes ) -> Optional [ bytes ] :
401416if path_match :
402417return decode_path (path_match )
403418
@@ -410,21 +425,23 @@ def _pick_best_path(cls, path_match, rename_match, path_fallback_match):
410425return None
411426
412427@classmethod
413- def _index_from_patch_format (cls ,repo ,proc ) :
428+ def _index_from_patch_format (cls ,repo : Repo ,proc : TBD ) -> DiffIndex :
414429"""Create a new DiffIndex from the given text which must be in patch format
415430 :param repo: is the repository we are operating on - it is required
416431 :param stream: result of 'git diff' as a stream (supporting file protocol)
417432 :return: git.DiffIndex """
418433
419434## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
420- text = []
421- handle_process_output (proc ,text .append ,None ,finalize_process ,decode_streams = False )
435+ text_list = [] # type: List[bytes ]
436+ handle_process_output (proc ,text_list .append ,None ,finalize_process ,decode_streams = False )
422437
423438# for now, we have to bake the stream
424- text = b'' .join (text )
439+ text = b'' .join (text_list )
425440index = DiffIndex ()
426441previous_header = None
427442header = None
443+ a_path ,b_path = None ,None # for mypy
444+ a_mode ,b_mode = None ,None # for mypy
428445for _header in cls .re_header .finditer (text ):
429446a_path_fallback ,b_path_fallback , \
430447old_mode ,new_mode , \
@@ -464,27 +481,28 @@ def _index_from_patch_format(cls, repo, proc):
464481previous_header = _header
465482header = _header
466483# end for each header we parse
467- if index :
484+ if index and header :
468485index [- 1 ].diff = text [header .end ():]
469486# end assign last diff
470487
471488return index
472489
473490@classmethod
474- def _index_from_raw_format (cls ,repo ,proc ) :
491+ def _index_from_raw_format (cls ,repo : 'Repo' ,proc : TBD ) -> DiffIndex :
475492"""Create a new DiffIndex from the given stream which must be in raw format.
476493 :return: git.DiffIndex"""
477494# handles
478495# :100644 100644 687099101... 37c5e30c8... M .gitignore
479496
480497index = DiffIndex ()
481498
482- def handle_diff_line (lines ) :
483- lines = lines .decode (defenc )
499+ def handle_diff_line (lines_bytes : bytes ) -> None :
500+ lines = lines_bytes .decode (defenc )
484501
485502for line in lines .split (':' )[1 :]:
486503meta ,_ ,path = line .partition ('\x00 ' )
487504path = path .rstrip ('\x00 ' )
505+ a_blob_id ,b_blob_id = None ,None # Type: Optional[str]
488506old_mode ,new_mode ,a_blob_id ,b_blob_id ,_change_type = meta .split (None ,4 )
489507# Change type can be R100
490508# R: status letter
@@ -504,20 +522,20 @@ def handle_diff_line(lines):
504522# NOTE: We cannot conclude from the existence of a blob to change type
505523# as diffs with the working do not have blobs yet
506524if change_type == 'D' :
507- b_blob_id = None
525+ b_blob_id = None # Optional[str]
508526deleted_file = True
509527elif change_type == 'A' :
510528a_blob_id = None
511529new_file = True
512530elif change_type == 'C' :
513531copied_file = True
514- a_path , b_path = path .split ('\x00 ' ,1 )
515- a_path = a_path .encode (defenc )
516- b_path = b_path .encode (defenc )
532+ a_path_str , b_path_str = path .split ('\x00 ' ,1 )
533+ a_path = a_path_str .encode (defenc )
534+ b_path = b_path_str .encode (defenc )
517535elif change_type == 'R' :
518- a_path , b_path = path .split ('\x00 ' ,1 )
519- a_path = a_path .encode (defenc )
520- b_path = b_path .encode (defenc )
536+ a_path_str , b_path_str = path .split ('\x00 ' ,1 )
537+ a_path = a_path_str .encode (defenc )
538+ b_path = b_path_str .encode (defenc )
521539rename_from ,rename_to = a_path ,b_path
522540elif change_type == 'T' :
523541# Nothing to do