7
7
import logging
8
8
import os
9
9
import re
10
+ from dataclasses import dataclass
10
11
import shlex
11
12
import warnings
12
13
from gitdb .db .loose import LooseObjectDB
41
42
from git .types import TBD , PathLike , Lit_config_levels , Commit_ish , Tree_ish , assert_never
42
43
from typing import (Any , BinaryIO , Callable , Dict ,
43
44
Iterator , List , Mapping , Optional , Sequence ,
44
- TextIO , Tuple , Type , Union ,
45
+ TextIO , Tuple , Type , TypedDict , Union ,
45
46
NamedTuple , cast , TYPE_CHECKING )
46
47
47
48
from git .types import ConfigLevels_Tup
53
54
from git .objects .submodule .base import UpdateProgress
54
55
from git .remote import RemoteProgress
55
56
56
-
57
57
# -----------------------------------------------------------
58
58
59
59
log = logging .getLogger (__name__ )
@@ -874,7 +874,7 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat
874
874
range (orig_lineno , orig_lineno + num_lines ))
875
875
876
876
def 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 :
878
878
"""The blame information for the given file at the given revision.
879
879
880
880
: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
886
886
if incremental :
887
887
return self .blame_incremental (rev , file , ** kwargs )
888
888
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 ()
894
920
895
921
keepends = True
896
- for line in data .splitlines (keepends ):
922
+ for line_bytes in data .splitlines (keepends ):
897
923
try :
898
- line = line .rstrip ().decode (defenc )
924
+ line_str = line_bytes .rstrip ().decode (defenc )
899
925
except UnicodeDecodeError :
900
926
firstpart = ''
927
+ parts = ['' ]
901
928
is_binary = True
902
929
else :
903
930
# As we don't have an idea when the binary data ends, as it could contain multiple newlines
904
931
# in the process. So we rely on being able to decode to tell us what is is.
905
932
# This can absolutely fail even on text files, but even if it does, we should be fine treating it
906
933
# as binary instead
907
- parts = self .re_whitespace .split (line , 1 )
934
+ parts = self .re_whitespace .split (line_str , 1 )
908
935
firstpart = parts [0 ]
909
936
is_binary = False
910
937
# end handle decode of line
@@ -916,10 +943,10 @@ def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **k
916
943
# another line of blame with the same data
917
944
digits = parts [- 1 ].split (" " )
918
945
if len (digits ) == 3 :
919
- info = { 'id' : firstpart }
946
+ info . id = firstpart
920
947
blames .append ([None , []])
921
- elif info [ 'id' ] != firstpart :
922
- info = { 'id' : firstpart }
948
+ elif info . id != firstpart :
949
+ info . id = firstpart
923
950
blames .append ([commits .get (firstpart ), []])
924
951
# END blame data initialization
925
952
else :
@@ -936,9 +963,9 @@ def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **k
936
963
# committer-tz -0700 - IGNORED BY US
937
964
role = m .group (0 )
938
965
if firstpart .endswith ('-mail' ):
939
- info ["%s_email" % role ] = parts [- 1 ]
966
+ info [f" { role } _email" ] = parts [- 1 ]
940
967
elif firstpart .endswith ('-time' ):
941
- info ["%s_date" % role ] = int (parts [- 1 ])
968
+ info [f" { role } _date" ] = int (parts [- 1 ])
942
969
elif role == firstpart :
943
970
info [role ] = parts [- 1 ]
944
971
# END distinguish mail,time,name
@@ -953,38 +980,40 @@ def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **k
953
980
info ['summary' ] = parts [- 1 ]
954
981
elif firstpart == '' :
955
982
if info :
956
- sha = info [ 'id' ]
983
+ sha = info . id
957
984
c = commits .get (sha )
958
985
if c is None :
959
986
c = 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 ,
962
989
committer = 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 )
965
992
commits [sha ] = c
993
+ blames [- 1 ][0 ] = c
966
994
# 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
978
995
if 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
981
1010
# END if we collected commit info
982
1011
# END distinguish filename,summary,rest
983
1012
# END distinguish author|committer vs filename,summary,rest
984
1013
# END distinguish hexsha vs other information
985
1014
return blames
986
1015
987
- @classmethod
1016
+ @ classmethod
988
1017
def init (cls , path : Union [PathLike , None ] = None , mkdir : bool = True , odbt : Type [GitCmdObjectDB ] = GitCmdObjectDB ,
989
1018
expand_vars : bool = True , ** kwargs : Any ) -> 'Repo' :
990
1019
"""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
1023
1052
git .init (** kwargs )
1024
1053
return cls (path , odbt = odbt )
1025
1054
1026
- @classmethod
1055
+ @ classmethod
1027
1056
def _clone (cls , git : 'Git' , url : PathLike , path : PathLike , odb_default_type : Type [GitCmdObjectDB ],
1028
1057
progress : Union ['RemoteProgress' , 'UpdateProgress' , Callable [..., 'RemoteProgress' ], None ] = None ,
1029
1058
multi_options : Optional [List [str ]] = None , ** kwargs : Any
@@ -1101,7 +1130,7 @@ def clone(self, path: PathLike, progress: Optional[Callable] = None,
1101
1130
:return: ``git.Repo`` (the newly cloned repo)"""
1102
1131
return self ._clone (self .git , self .common_dir , path , type (self .odb ), progress , multi_options , ** kwargs )
1103
1132
1104
- @classmethod
1133
+ @ classmethod
1105
1134
def clone_from (cls , url : PathLike , to_path : PathLike , progress : Optional [Callable ] = None ,
1106
1135
env : Optional [Mapping [str , Any ]] = None ,
1107
1136
multi_options : Optional [List [str ]] = None , ** kwargs : Any ) -> 'Repo' :
0 commit comments