77import logging
88import os
99import re
10+ from dataclasses import dataclass
1011import shlex
1112import warnings
1213from gitdb .db .loose import LooseObjectDB
4142from git .types import TBD ,PathLike ,Lit_config_levels ,Commit_ish ,Tree_ish ,assert_never
4243from typing import (Any ,BinaryIO ,Callable ,Dict ,
4344Iterator ,List ,Mapping ,Optional ,Sequence ,
44- TextIO ,Tuple ,Type ,Union ,
45+ TextIO ,Tuple ,Type ,TypedDict , Union ,
4546NamedTuple ,cast ,TYPE_CHECKING )
4647
4748from git .types import ConfigLevels_Tup
5354from git .objects .submodule .base import UpdateProgress
5455from git .remote import RemoteProgress
5556
56-
5757# -----------------------------------------------------------
5858
5959log = logging .getLogger (__name__ )
@@ -874,7 +874,7 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat
874874range (orig_lineno ,orig_lineno + num_lines ))
875875
876876def blame (self ,rev :Union [str ,HEAD ],file :str ,incremental :bool = False ,** kwargs :Any
877- )-> Union [ List [List [Union [ Optional [ ' Commit' ], List [str ]]]], Optional [ Iterator [BlameEntry ]]] :
877+ )-> List [List [Commit | List [str | bytes ] | None ]] | Iterator [BlameEntry ]| None :
878878"""The blame information for the given file at the given revision.
879879
880880 :param rev: revision specifier, see git-rev-parse for viable options.
@@ -886,25 +886,52 @@ def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **k
886886if incremental :
887887return self .blame_incremental (rev ,file ,** kwargs )
888888
889- data = self .git .blame (rev ,'--' ,file ,p = True ,stdout_as_string = False ,** kwargs )
890- commits :Dict [str ,TBD ]= {}
891- blames :List [List [Union [Optional ['Commit' ],List [str ]]]]= []
892-
893- info :Dict [str ,TBD ]= {}# use Any until TypedDict available
889+ data :bytes = self .git .blame (rev ,'--' ,file ,p = True ,stdout_as_string = False ,** kwargs )
890+ commits :Dict [str ,Commit ]= {}
891+ blames :List [List [Commit | List [str | bytes ]| None ]]= []
892+
893+ class InfoTC (TypedDict ,total = False ):
894+ sha :str
895+ id :str
896+ filename :str
897+ summary :str
898+ author :str
899+ author_email :str
900+ author_date :int
901+ committer :str
902+ committer_email :str
903+ committer_date :int
904+
905+ @dataclass
906+ class InfoDC (Dict [str ,Union [str ,int ]]):
907+ sha :str = ''
908+ id :str = ''
909+ filename :str = ''
910+ summary :str = ''
911+ author :str = ''
912+ author_email :str = ''
913+ author_date :int = 0
914+ committer :str = ''
915+ committer_email :str = ''
916+ committer_date :int = 0
917+
918+ # info: InfoTD = {}
919+ info = InfoDC ()
894920
895921keepends = True
896- for line in data .splitlines (keepends ):
922+ for line_bytes in data .splitlines (keepends ):
897923try :
898- line = line .rstrip ().decode (defenc )
924+ line_str = line_bytes .rstrip ().decode (defenc )
899925except UnicodeDecodeError :
900926firstpart = ''
927+ parts = ['' ]
901928is_binary = True
902929else :
903930# As we don't have an idea when the binary data ends, as it could contain multiple newlines
904931# in the process. So we rely on being able to decode to tell us what is is.
905932# This can absolutely fail even on text files, but even if it does, we should be fine treating it
906933# as binary instead
907- parts = self .re_whitespace .split (line ,1 )
934+ parts = self .re_whitespace .split (line_str ,1 )
908935firstpart = parts [0 ]
909936is_binary = False
910937# end handle decode of line
@@ -916,10 +943,10 @@ def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **k
916943# another line of blame with the same data
917944digits = parts [- 1 ].split (" " )
918945if len (digits )== 3 :
919- info = { 'id' : firstpart }
946+ info . id = firstpart
920947blames .append ([None , []])
921- elif info [ 'id' ] != firstpart :
922- info = { 'id' : firstpart }
948+ elif info . id != firstpart :
949+ info . id = firstpart
923950blames .append ([commits .get (firstpart ), []])
924951# END blame data initialization
925952else :
@@ -936,9 +963,9 @@ def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **k
936963# committer-tz -0700 - IGNORED BY US
937964role = m .group (0 )
938965if firstpart .endswith ('-mail' ):
939- info ["%s_email" % role ]= parts [- 1 ]
966+ info [f" { role } _email" ]= parts [- 1 ]
940967elif firstpart .endswith ('-time' ):
941- info ["%s_date" % role ]= int (parts [- 1 ])
968+ info [f" { role } _date" ]= int (parts [- 1 ])
942969elif role == firstpart :
943970info [role ]= parts [- 1 ]
944971# END distinguish mail,time,name
@@ -953,38 +980,40 @@ def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **k
953980info ['summary' ]= parts [- 1 ]
954981elif firstpart == '' :
955982if info :
956- sha = info [ 'id' ]
983+ sha = info . id
957984c = commits .get (sha )
958985if c is None :
959986c = Commit (self ,hex_to_bin (sha ),
960- author = Actor ._from_string (info [ ' author' ] + ' ' + info [ ' author_email' ] ),
961- authored_date = info [ ' author_date' ] ,
987+ author = Actor ._from_string (info . author + ' ' + info . author_email ),
988+ authored_date = info . author_date ,
962989committer = Actor ._from_string (
963- info [ ' committer' ] + ' ' + info [ ' committer_email' ] ),
964- committed_date = info [ ' committer_date' ] )
990+ info . committer + ' ' + info . committer_email ),
991+ committed_date = info . committer_date )
965992commits [sha ]= c
993+ blames [- 1 ][0 ]= c
966994# END if commit objects needs initial creation
967- if not is_binary :
968- if line and line [0 ]== '\t ' :
969- line = line [1 :]
970- else :
971- # NOTE: We are actually parsing lines out of binary data, which can lead to the
972- # binary being split up along the newline separator. We will append this to the blame
973- # we are currently looking at, even though it should be concatenated with the last line
974- # we have seen.
975- pass
976- # end handle line contents
977- blames [- 1 ][0 ]= c
978995if blames [- 1 ][1 ]is not None :
979- blames [- 1 ][1 ].append (line )
980- info = {'id' :sha }
996+ if not is_binary :
997+ if line_str and line_str [0 ]== '\t ' :
998+ line_str = line_str [1 :]
999+
1000+ blames [- 1 ][1 ].append (line_str )
1001+ else :
1002+ # NOTE: We are actually parsing lines out of binary data, which can lead to the
1003+ # binary being split up along the newline separator. We will append this to the
1004+ # blame we are currently looking at, even though it should be concatenated with
1005+ # the last line we have seen.
1006+ blames [- 1 ][1 ].append (line_bytes )
1007+ # end handle line contents
1008+
1009+ info .id = sha
9811010# END if we collected commit info
9821011# END distinguish filename,summary,rest
9831012# END distinguish author|committer vs filename,summary,rest
9841013# END distinguish hexsha vs other information
9851014return blames
9861015
987- @classmethod
1016+ @classmethod
9881017def init (cls ,path :Union [PathLike ,None ]= None ,mkdir :bool = True ,odbt :Type [GitCmdObjectDB ]= GitCmdObjectDB ,
9891018expand_vars :bool = True ,** kwargs :Any )-> 'Repo' :
9901019"""Initialize a git repository at the given path if specified
@@ -1023,7 +1052,7 @@ def init(cls, path: Union[PathLike, None] = None, mkdir: bool = True, odbt: Type
10231052git .init (** kwargs )
10241053return cls (path ,odbt = odbt )
10251054
1026- @classmethod
1055+ @classmethod
10271056def _clone (cls ,git :'Git' ,url :PathLike ,path :PathLike ,odb_default_type :Type [GitCmdObjectDB ],
10281057progress :Union ['RemoteProgress' ,'UpdateProgress' ,Callable [...,'RemoteProgress' ],None ]= None ,
10291058multi_options :Optional [List [str ]]= None ,** kwargs :Any
@@ -1101,7 +1130,7 @@ def clone(self, path: PathLike, progress: Optional[Callable] = None,
11011130 :return: ``git.Repo`` (the newly cloned repo)"""
11021131return self ._clone (self .git ,self .common_dir ,path ,type (self .odb ),progress ,multi_options ,** kwargs )
11031132
1104- @classmethod
1133+ @classmethod
11051134def clone_from (cls ,url :PathLike ,to_path :PathLike ,progress :Optional [Callable ]= None ,
11061135env :Optional [Mapping [str ,Any ]]= None ,
11071136multi_options :Optional [List [str ]]= None ,** kwargs :Any )-> 'Repo' :