1+
2+ from mmap import mmap
13import re
2- import time
4+ import time as _time
35
46from git .compat import defenc
57from git .objects .util import (
2022import os .path as osp
2123
2224
25+ # typing ------------------------------------------------------------------
26+
27+ from typing import Iterator , List , Tuple , Union , TYPE_CHECKING
28+
29+ from git .types import PathLike
30+
31+ if TYPE_CHECKING :
32+ from git .refs import SymbolicReference
33+ from io import BytesIO
34+ from git .config import GitConfigParser , SectionConstraint # NOQA
35+
36+ # ------------------------------------------------------------------------------
37+
2338__all__ = ["RefLog" , "RefLogEntry" ]
2439
2540
26- class RefLogEntry (tuple ):
41+ class RefLogEntry (Tuple [ str , str , Actor , Tuple [ int , int ], str ] ):
2742
2843 """Named tuple allowing easy access to the revlog data fields"""
2944 _re_hexsha_only = re .compile ('^[0-9A-Fa-f]{40}$' )
3045 __slots__ = ()
3146
32- def __repr__ (self ):
47+ def __repr__ (self ) -> str :
3348 """Representation of ourselves in git reflog format"""
3449 return self .format ()
3550
36- def format (self ):
51+ def format (self ) -> str :
3752 """:return: a string suitable to be placed in a reflog file"""
3853 act = self .actor
3954 time = self .time
@@ -46,35 +61,36 @@ def format(self):
4661 self .message )
4762
4863 @property
49- def oldhexsha (self ):
64+ def oldhexsha (self ) -> str :
5065 """The hexsha to the commit the ref pointed to before the change"""
5166 return self [0 ]
5267
5368 @property
54- def newhexsha (self ):
69+ def newhexsha (self ) -> str :
5570 """The hexsha to the commit the ref now points to, after the change"""
5671 return self [1 ]
5772
5873 @property
59- def actor (self ):
74+ def actor (self ) -> Actor :
6075 """Actor instance, providing access"""
6176 return self [2 ]
6277
6378 @property
64- def time (self ):
79+ def time (self ) -> Tuple [ int , int ] :
6580 """time as tuple:
6681
6782 * [0] = int(time)
6883 * [1] = int(timezone_offset) in time.altzone format """
6984 return self [3 ]
7085
7186 @property
72- def message (self ):
87+ def message (self ) -> str :
7388 """Message describing the operation that acted on the reference"""
7489 return self [4 ]
7590
7691 @classmethod
77- def new (cls , oldhexsha , newhexsha , actor , time , tz_offset , message ): # skipcq: PYL-W0621
92+ def new (cls , oldhexsha : str , newhexsha : str , actor : Actor , time : int , tz_offset : int , message : str
93+ ) -> 'RefLogEntry' : # skipcq: PYL-W0621
7894 """:return: New instance of a RefLogEntry"""
7995 if not isinstance (actor , Actor ):
8096 raise ValueError ("Need actor instance, got %s" % actor )
@@ -111,14 +127,15 @@ def from_line(cls, line: bytes) -> 'RefLogEntry':
111127 # END handle missing end brace
112128
113129 actor = Actor ._from_string (info [82 :email_end + 1 ])
114- time , tz_offset = parse_date (info [email_end + 2 :]) # skipcq: PYL-W0621
130+ time , tz_offset = parse_date (
131+ info [email_end + 2 :]) # skipcq: PYL-W0621
115132
116133 return RefLogEntry ((oldhexsha , newhexsha , actor , (time , tz_offset ), msg ))
117134
118135
119- class RefLog (list , Serializable ):
136+ class RefLog (List [ RefLogEntry ] , Serializable ):
120137
121- """A reflog contains reflog entries , each of which defines a certain state
138+ """A reflog contains RefLogEntrys , each of which defines a certain state
122139 of the head in question. Custom query methods allow to retrieve log entries
123140 by date or by other criteria.
124141
@@ -127,11 +144,11 @@ class RefLog(list, Serializable):
127144
128145 __slots__ = ('_path' , )
129146
130- def __new__ (cls , filepath = None ):
147+ def __new__ (cls , filepath : Union [ PathLike , None ] = None ) -> 'RefLog' :
131148 inst = super (RefLog , cls ).__new__ (cls )
132149 return inst
133150
134- def __init__ (self , filepath = None ):
151+ def __init__ (self , filepath : Union [ PathLike , None ] = None ):
135152 """Initialize this instance with an optional filepath, from which we will
136153 initialize our data. The path is also used to write changes back using
137154 the write() method"""
@@ -142,7 +159,8 @@ def __init__(self, filepath=None):
142159
143160 def _read_from_file (self ):
144161 try :
145- fmap = file_contents_ro_filepath (self ._path , stream = True , allow_mmap = True )
162+ fmap = file_contents_ro_filepath (
163+ self ._path , stream = True , allow_mmap = True )
146164 except OSError :
147165 # it is possible and allowed that the file doesn't exist !
148166 return
@@ -154,10 +172,10 @@ def _read_from_file(self):
154172 fmap .close ()
155173 # END handle closing of handle
156174
157- #{ Interface
175+ # { Interface
158176
159177 @classmethod
160- def from_file (cls , filepath ) :
178+ def from_file (cls , filepath : PathLike ) -> 'RefLog' :
161179 """
162180 :return: a new RefLog instance containing all entries from the reflog
163181 at the given filepath
@@ -166,7 +184,7 @@ def from_file(cls, filepath):
166184 return cls (filepath )
167185
168186 @classmethod
169- def path (cls , ref ) :
187+ def path (cls , ref : 'SymbolicReference' ) -> str :
170188 """
171189 :return: string to absolute path at which the reflog of the given ref
172190 instance would be found. The path is not guaranteed to point to a valid
@@ -175,28 +193,34 @@ def path(cls, ref):
175193 return osp .join (ref .repo .git_dir , "logs" , to_native_path (ref .path ))
176194
177195 @classmethod
178- def iter_entries (cls , stream ) :
196+ def iter_entries (cls , stream : Union [ str , 'BytesIO' , mmap ]) -> Iterator [ RefLogEntry ] :
179197 """
180198 :return: Iterator yielding RefLogEntry instances, one for each line read
181199 sfrom the given stream.
182200 :param stream: file-like object containing the revlog in its native format
183- or basestring instance pointing to a file to read"""
201+ or string instance pointing to a file to read"""
184202 new_entry = RefLogEntry .from_line
185203 if isinstance (stream , str ):
186- stream = file_contents_ro_filepath (stream )
204+ # default args return mmap on py>3
205+ _stream = file_contents_ro_filepath (stream )
206+ assert isinstance (_stream , mmap )
207+ else :
208+ _stream = stream
187209 # END handle stream type
188210 while True :
189- line = stream .readline ()
211+ line = _stream .readline ()
190212 if not line :
191213 return
192214 yield new_entry (line .strip ())
193215 # END endless loop
194- stream .close ()
195216
196217 @classmethod
197- def entry_at (cls , filepath , index ):
198- """:return: RefLogEntry at the given index
218+ def entry_at (cls , filepath : PathLike , index : int ) -> 'RefLogEntry' :
219+ """
220+ :return: RefLogEntry at the given index
221+
199222 :param filepath: full path to the index file from which to read the entry
223+
200224 :param index: python list compatible index, i.e. it may be negative to
201225 specify an entry counted from the end of the list
202226
@@ -210,21 +234,19 @@ def entry_at(cls, filepath, index):
210234 if index < 0 :
211235 return RefLogEntry .from_line (fp .readlines ()[index ].strip ())
212236 # read until index is reached
237+
213238 for i in range (index + 1 ):
214239 line = fp .readline ()
215240 if not line :
216- break
241+ raise IndexError (
242+ f"Index file ended at line { i + 1 } , before given index was reached" )
217243 # END abort on eof
218244 # END handle runup
219245
220- if i != index or not line : # skipcq:PYL-W0631
221- raise IndexError
222- # END handle exception
223-
224246 return RefLogEntry .from_line (line .strip ())
225247 # END handle index
226248
227- def to_file (self , filepath ) :
249+ def to_file (self , filepath : PathLike ) -> None :
228250 """Write the contents of the reflog instance to a file at the given filepath.
229251 :param filepath: path to file, parent directories are assumed to exist"""
230252 lfd = LockedFD (filepath )
@@ -241,65 +263,75 @@ def to_file(self, filepath):
241263 # END handle change
242264
243265 @classmethod
244- def append_entry (cls , config_reader , filepath , oldbinsha , newbinsha , message ):
266+ def append_entry (cls , config_reader : Union [Actor , 'GitConfigParser' , 'SectionConstraint' , None ],
267+ filepath : PathLike , oldbinsha : bytes , newbinsha : bytes , message : str ,
268+ write : bool = True ) -> 'RefLogEntry' :
245269 """Append a new log entry to the revlog at filepath.
246270
247271 :param config_reader: configuration reader of the repository - used to obtain
248- user information. May also be an Actor instance identifying the committer directly.
249- May also be None
272+ user information. May also be an Actor instance identifying the committer directly or None.
250273 :param filepath: full path to the log file
251274 :param oldbinsha: binary sha of the previous commit
252275 :param newbinsha: binary sha of the current commit
253276 :param message: message describing the change to the reference
254277 :param write: If True, the changes will be written right away. Otherwise
255278 the change will not be written
279+
256280 :return: RefLogEntry objects which was appended to the log
281+
257282 :note: As we are append-only, concurrent access is not a problem as we
258283 do not interfere with readers."""
284+
259285 if len (oldbinsha ) != 20 or len (newbinsha ) != 20 :
260286 raise ValueError ("Shas need to be given in binary format" )
261287 # END handle sha type
262288 assure_directory_exists (filepath , is_file = True )
263289 first_line = message .split ('\n ' )[0 ]
264- committer = isinstance (config_reader , Actor ) and config_reader or Actor .committer (config_reader )
290+ if isinstance (config_reader , Actor ):
291+ committer = config_reader # mypy thinks this is Actor | Gitconfigparser, but why?
292+ else :
293+ committer = Actor .committer (config_reader )
265294 entry = RefLogEntry ((
266295 bin_to_hex (oldbinsha ).decode ('ascii' ),
267296 bin_to_hex (newbinsha ).decode ('ascii' ),
268- committer , (int (time .time ()), time .altzone ), first_line
297+ committer , (int (_time .time ()), _time .altzone ), first_line
269298 ))
270299
271- lf = LockFile ( filepath )
272- lf . _obtain_lock_or_raise ( )
273- fd = open ( filepath , 'ab' )
274- try :
275- fd . write ( entry . format (). encode ( defenc ))
276- finally :
277- fd . close ()
278- lf . _release_lock ()
279- # END handle write operation
280-
300+ if write :
301+ lf = LockFile ( filepath )
302+ lf . _obtain_lock_or_raise ( )
303+ fd = open ( filepath , 'ab' )
304+ try :
305+ fd . write ( entry . format (). encode ( defenc ))
306+ finally :
307+ fd . close ()
308+ lf . _release_lock ()
309+ # END handle write operation
281310 return entry
282311
283- def write (self ):
312+ def write (self ) -> 'RefLog' :
284313 """Write this instance's data to the file we are originating from
285314 :return: self"""
286315 if self ._path is None :
287- raise ValueError ("Instance was not initialized with a path, use to_file(...) instead" )
316+ raise ValueError (
317+ "Instance was not initialized with a path, use to_file(...) instead" )
288318 # END assert path
289319 self .to_file (self ._path )
290320 return self
291321
292- #} END interface
322+ # } END interface
293323
294- #{ Serializable Interface
295- def _serialize (self , stream ) :
324+ # { Serializable Interface
325+ def _serialize (self , stream : 'BytesIO' ) -> 'RefLog' :
296326 write = stream .write
297327
298328 # write all entries
299329 for e in self :
300330 write (e .format ().encode (defenc ))
301331 # END for each entry
332+ return self
302333
303- def _deserialize (self , stream ) :
334+ def _deserialize (self , stream : 'BytesIO' ) -> 'RefLog' :
304335 self .extend (self .iter_entries (stream ))
305- #} END serializable interface
336+ # } END serializable interface
337+ return self
0 commit comments