1
+
2
+ from mmap import mmap
1
3
import re
2
- import time
4
+ import time as _time
3
5
4
6
from git .compat import defenc
5
7
from git .objects .util import (
20
22
import os .path as osp
21
23
22
24
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
+
23
38
__all__ = ["RefLog" , "RefLogEntry" ]
24
39
25
40
26
- class RefLogEntry (tuple ):
41
+ class RefLogEntry (Tuple [ str , str , Actor , Tuple [ int , int ], str ] ):
27
42
28
43
"""Named tuple allowing easy access to the revlog data fields"""
29
44
_re_hexsha_only = re .compile ('^[0-9A-Fa-f]{40}$' )
30
45
__slots__ = ()
31
46
32
- def __repr__ (self ):
47
+ def __repr__ (self ) -> str :
33
48
"""Representation of ourselves in git reflog format"""
34
49
return self .format ()
35
50
36
- def format (self ):
51
+ def format (self ) -> str :
37
52
""":return: a string suitable to be placed in a reflog file"""
38
53
act = self .actor
39
54
time = self .time
@@ -46,35 +61,36 @@ def format(self):
46
61
self .message )
47
62
48
63
@property
49
- def oldhexsha (self ):
64
+ def oldhexsha (self ) -> str :
50
65
"""The hexsha to the commit the ref pointed to before the change"""
51
66
return self [0 ]
52
67
53
68
@property
54
- def newhexsha (self ):
69
+ def newhexsha (self ) -> str :
55
70
"""The hexsha to the commit the ref now points to, after the change"""
56
71
return self [1 ]
57
72
58
73
@property
59
- def actor (self ):
74
+ def actor (self ) -> Actor :
60
75
"""Actor instance, providing access"""
61
76
return self [2 ]
62
77
63
78
@property
64
- def time (self ):
79
+ def time (self ) -> Tuple [ int , int ] :
65
80
"""time as tuple:
66
81
67
82
* [0] = int(time)
68
83
* [1] = int(timezone_offset) in time.altzone format """
69
84
return self [3 ]
70
85
71
86
@property
72
- def message (self ):
87
+ def message (self ) -> str :
73
88
"""Message describing the operation that acted on the reference"""
74
89
return self [4 ]
75
90
76
91
@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
78
94
""":return: New instance of a RefLogEntry"""
79
95
if not isinstance (actor , Actor ):
80
96
raise ValueError ("Need actor instance, got %s" % actor )
@@ -111,14 +127,15 @@ def from_line(cls, line: bytes) -> 'RefLogEntry':
111
127
# END handle missing end brace
112
128
113
129
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
115
132
116
133
return RefLogEntry ((oldhexsha , newhexsha , actor , (time , tz_offset ), msg ))
117
134
118
135
119
- class RefLog (list , Serializable ):
136
+ class RefLog (List [ RefLogEntry ] , Serializable ):
120
137
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
122
139
of the head in question. Custom query methods allow to retrieve log entries
123
140
by date or by other criteria.
124
141
@@ -127,11 +144,11 @@ class RefLog(list, Serializable):
127
144
128
145
__slots__ = ('_path' , )
129
146
130
- def __new__ (cls , filepath = None ):
147
+ def __new__ (cls , filepath : Union [ PathLike , None ] = None ) -> 'RefLog' :
131
148
inst = super (RefLog , cls ).__new__ (cls )
132
149
return inst
133
150
134
- def __init__ (self , filepath = None ):
151
+ def __init__ (self , filepath : Union [ PathLike , None ] = None ):
135
152
"""Initialize this instance with an optional filepath, from which we will
136
153
initialize our data. The path is also used to write changes back using
137
154
the write() method"""
@@ -142,7 +159,8 @@ def __init__(self, filepath=None):
142
159
143
160
def _read_from_file (self ):
144
161
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 )
146
164
except OSError :
147
165
# it is possible and allowed that the file doesn't exist !
148
166
return
@@ -154,10 +172,10 @@ def _read_from_file(self):
154
172
fmap .close ()
155
173
# END handle closing of handle
156
174
157
- #{ Interface
175
+ # { Interface
158
176
159
177
@classmethod
160
- def from_file (cls , filepath ) :
178
+ def from_file (cls , filepath : PathLike ) -> 'RefLog' :
161
179
"""
162
180
:return: a new RefLog instance containing all entries from the reflog
163
181
at the given filepath
@@ -166,7 +184,7 @@ def from_file(cls, filepath):
166
184
return cls (filepath )
167
185
168
186
@classmethod
169
- def path (cls , ref ) :
187
+ def path (cls , ref : 'SymbolicReference' ) -> str :
170
188
"""
171
189
:return: string to absolute path at which the reflog of the given ref
172
190
instance would be found. The path is not guaranteed to point to a valid
@@ -175,28 +193,34 @@ def path(cls, ref):
175
193
return osp .join (ref .repo .git_dir , "logs" , to_native_path (ref .path ))
176
194
177
195
@classmethod
178
- def iter_entries (cls , stream ) :
196
+ def iter_entries (cls , stream : Union [ str , 'BytesIO' , mmap ]) -> Iterator [ RefLogEntry ] :
179
197
"""
180
198
:return: Iterator yielding RefLogEntry instances, one for each line read
181
199
sfrom the given stream.
182
200
: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"""
184
202
new_entry = RefLogEntry .from_line
185
203
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
187
209
# END handle stream type
188
210
while True :
189
- line = stream .readline ()
211
+ line = _stream .readline ()
190
212
if not line :
191
213
return
192
214
yield new_entry (line .strip ())
193
215
# END endless loop
194
- stream .close ()
195
216
196
217
@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
+
199
222
:param filepath: full path to the index file from which to read the entry
223
+
200
224
:param index: python list compatible index, i.e. it may be negative to
201
225
specify an entry counted from the end of the list
202
226
@@ -210,21 +234,19 @@ def entry_at(cls, filepath, index):
210
234
if index < 0 :
211
235
return RefLogEntry .from_line (fp .readlines ()[index ].strip ())
212
236
# read until index is reached
237
+
213
238
for i in range (index + 1 ):
214
239
line = fp .readline ()
215
240
if not line :
216
- break
241
+ raise IndexError (
242
+ f"Index file ended at line { i + 1 } , before given index was reached" )
217
243
# END abort on eof
218
244
# END handle runup
219
245
220
- if i != index or not line : # skipcq:PYL-W0631
221
- raise IndexError
222
- # END handle exception
223
-
224
246
return RefLogEntry .from_line (line .strip ())
225
247
# END handle index
226
248
227
- def to_file (self , filepath ) :
249
+ def to_file (self , filepath : PathLike ) -> None :
228
250
"""Write the contents of the reflog instance to a file at the given filepath.
229
251
:param filepath: path to file, parent directories are assumed to exist"""
230
252
lfd = LockedFD (filepath )
@@ -241,65 +263,75 @@ def to_file(self, filepath):
241
263
# END handle change
242
264
243
265
@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' :
245
269
"""Append a new log entry to the revlog at filepath.
246
270
247
271
: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.
250
273
:param filepath: full path to the log file
251
274
:param oldbinsha: binary sha of the previous commit
252
275
:param newbinsha: binary sha of the current commit
253
276
:param message: message describing the change to the reference
254
277
:param write: If True, the changes will be written right away. Otherwise
255
278
the change will not be written
279
+
256
280
:return: RefLogEntry objects which was appended to the log
281
+
257
282
:note: As we are append-only, concurrent access is not a problem as we
258
283
do not interfere with readers."""
284
+
259
285
if len (oldbinsha ) != 20 or len (newbinsha ) != 20 :
260
286
raise ValueError ("Shas need to be given in binary format" )
261
287
# END handle sha type
262
288
assure_directory_exists (filepath , is_file = True )
263
289
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 )
265
294
entry = RefLogEntry ((
266
295
bin_to_hex (oldbinsha ).decode ('ascii' ),
267
296
bin_to_hex (newbinsha ).decode ('ascii' ),
268
- committer , (int (time .time ()), time .altzone ), first_line
297
+ committer , (int (_time .time ()), _time .altzone ), first_line
269
298
))
270
299
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
281
310
return entry
282
311
283
- def write (self ):
312
+ def write (self ) -> 'RefLog' :
284
313
"""Write this instance's data to the file we are originating from
285
314
:return: self"""
286
315
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" )
288
318
# END assert path
289
319
self .to_file (self ._path )
290
320
return self
291
321
292
- #} END interface
322
+ # } END interface
293
323
294
- #{ Serializable Interface
295
- def _serialize (self , stream ) :
324
+ # { Serializable Interface
325
+ def _serialize (self , stream : 'BytesIO' ) -> 'RefLog' :
296
326
write = stream .write
297
327
298
328
# write all entries
299
329
for e in self :
300
330
write (e .format ().encode (defenc ))
301
331
# END for each entry
332
+ return self
302
333
303
- def _deserialize (self , stream ) :
334
+ def _deserialize (self , stream : 'BytesIO' ) -> 'RefLog' :
304
335
self .extend (self .iter_entries (stream ))
305
- #} END serializable interface
336
+ # } END serializable interface
337
+ return self
0 commit comments