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"""
3449return self .format ()
3550
36- def format (self ):
51+ def format (self )-> str :
3752""":return: a string suitable to be placed in a reflog file"""
3853act = self .actor
3954time = self .time
@@ -46,35 +61,36 @@ def format(self):
4661self .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"""
5166return 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"""
5671return self [1 ]
5772
5873@property
59- def actor (self ):
74+ def actor (self )-> Actor :
6075"""Actor instance, providing access"""
6176return 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 """
6984return self [3 ]
7085
7186@property
72- def message (self ):
87+ def message (self )-> str :
7388"""Message describing the operation that acted on the reference"""
7489return 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"""
7995if not isinstance (actor ,Actor ):
8096raise 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
113129actor = 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
116133return 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 containsreflog entries , each of which defines a certain state
138+ """A reflog containsRefLogEntrys , 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' :
131148inst = super (RefLog ,cls ).__new__ (cls )
132149return 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
143160def _read_from_file (self ):
144161try :
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 )
146164except OSError :
147165# it is possible and allowed that the file doesn't exist !
148166return
@@ -154,10 +172,10 @@ def _read_from_file(self):
154172fmap .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):
166184return 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):
175193return 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- orbasestring instance pointing to a file to read"""
201+ orstring instance pointing to a file to read"""
184202new_entry = RefLogEntry .from_line
185203if 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
188210while True :
189- line = stream .readline ()
211+ line = _stream .readline ()
190212if not line :
191213return
192214yield 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):
210234if index < 0 :
211235return RefLogEntry .from_line (fp .readlines ()[index ].strip ())
212236# read until index is reached
237+
213238for i in range (index + 1 ):
214239line = fp .readline ()
215240if 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-
224246return 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"""
230252lfd = 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+
259285if len (oldbinsha )!= 20 or len (newbinsha )!= 20 :
260286raise ValueError ("Shas need to be given in binary format" )
261287# END handle sha type
262288assure_directory_exists (filepath ,is_file = True )
263289first_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 )
265294entry = RefLogEntry ((
266295bin_to_hex (oldbinsha ).decode ('ascii' ),
267296bin_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
281310return 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"""
286315if 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
289319self .to_file (self ._path )
290320return 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' :
296326write = stream .write
297327
298328# write all entries
299329for e in self :
300330write (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' :
304335self .extend (self .iter_entries (stream ))
305- #} END serializable interface
336+ # } END serializable interface
337+ return self