##// END OF EJS Templates
stringutil: move _formatsetrepr() from smartset...
Yuya Nishihara -
r38595:a3130208 default
parent child Browse files
Show More
@@ -1,1131 +1,1111 b''
1 # smartset.py - data structure for revision set
1 # smartset.py - data structure for revision set
2 #
2 #
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from . import (
10 from . import (
11 encoding,
11 encoding,
12 error,
12 error,
13 pycompat,
13 pycompat,
14 util,
14 util,
15 )
15 )
16
16 from .utils import (
17 def _formatsetrepr(r):
17 stringutil,
18 """Format an optional printable representation of a set
18 )
19
20 ======== =================================
21 type(r) example
22 ======== =================================
23 tuple ('<not %r>', other)
24 bytes '<branch closed>'
25 callable lambda: '<branch %r>' % sorted(b)
26 object other
27 ======== =================================
28 """
29 if r is None:
30 return ''
31 elif isinstance(r, tuple):
32 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
33 elif isinstance(r, bytes):
34 return r
35 elif callable(r):
36 return r()
37 else:
38 return pycompat.byterepr(r)
39
19
40 def _typename(o):
20 def _typename(o):
41 return pycompat.sysbytes(type(o).__name__).lstrip('_')
21 return pycompat.sysbytes(type(o).__name__).lstrip('_')
42
22
43 class abstractsmartset(object):
23 class abstractsmartset(object):
44
24
45 def __nonzero__(self):
25 def __nonzero__(self):
46 """True if the smartset is not empty"""
26 """True if the smartset is not empty"""
47 raise NotImplementedError()
27 raise NotImplementedError()
48
28
49 __bool__ = __nonzero__
29 __bool__ = __nonzero__
50
30
51 def __contains__(self, rev):
31 def __contains__(self, rev):
52 """provide fast membership testing"""
32 """provide fast membership testing"""
53 raise NotImplementedError()
33 raise NotImplementedError()
54
34
55 def __iter__(self):
35 def __iter__(self):
56 """iterate the set in the order it is supposed to be iterated"""
36 """iterate the set in the order it is supposed to be iterated"""
57 raise NotImplementedError()
37 raise NotImplementedError()
58
38
59 # Attributes containing a function to perform a fast iteration in a given
39 # Attributes containing a function to perform a fast iteration in a given
60 # direction. A smartset can have none, one, or both defined.
40 # direction. A smartset can have none, one, or both defined.
61 #
41 #
62 # Default value is None instead of a function returning None to avoid
42 # Default value is None instead of a function returning None to avoid
63 # initializing an iterator just for testing if a fast method exists.
43 # initializing an iterator just for testing if a fast method exists.
64 fastasc = None
44 fastasc = None
65 fastdesc = None
45 fastdesc = None
66
46
67 def isascending(self):
47 def isascending(self):
68 """True if the set will iterate in ascending order"""
48 """True if the set will iterate in ascending order"""
69 raise NotImplementedError()
49 raise NotImplementedError()
70
50
71 def isdescending(self):
51 def isdescending(self):
72 """True if the set will iterate in descending order"""
52 """True if the set will iterate in descending order"""
73 raise NotImplementedError()
53 raise NotImplementedError()
74
54
75 def istopo(self):
55 def istopo(self):
76 """True if the set will iterate in topographical order"""
56 """True if the set will iterate in topographical order"""
77 raise NotImplementedError()
57 raise NotImplementedError()
78
58
79 def min(self):
59 def min(self):
80 """return the minimum element in the set"""
60 """return the minimum element in the set"""
81 if self.fastasc is None:
61 if self.fastasc is None:
82 v = min(self)
62 v = min(self)
83 else:
63 else:
84 for v in self.fastasc():
64 for v in self.fastasc():
85 break
65 break
86 else:
66 else:
87 raise ValueError('arg is an empty sequence')
67 raise ValueError('arg is an empty sequence')
88 self.min = lambda: v
68 self.min = lambda: v
89 return v
69 return v
90
70
91 def max(self):
71 def max(self):
92 """return the maximum element in the set"""
72 """return the maximum element in the set"""
93 if self.fastdesc is None:
73 if self.fastdesc is None:
94 return max(self)
74 return max(self)
95 else:
75 else:
96 for v in self.fastdesc():
76 for v in self.fastdesc():
97 break
77 break
98 else:
78 else:
99 raise ValueError('arg is an empty sequence')
79 raise ValueError('arg is an empty sequence')
100 self.max = lambda: v
80 self.max = lambda: v
101 return v
81 return v
102
82
103 def first(self):
83 def first(self):
104 """return the first element in the set (user iteration perspective)
84 """return the first element in the set (user iteration perspective)
105
85
106 Return None if the set is empty"""
86 Return None if the set is empty"""
107 raise NotImplementedError()
87 raise NotImplementedError()
108
88
109 def last(self):
89 def last(self):
110 """return the last element in the set (user iteration perspective)
90 """return the last element in the set (user iteration perspective)
111
91
112 Return None if the set is empty"""
92 Return None if the set is empty"""
113 raise NotImplementedError()
93 raise NotImplementedError()
114
94
115 def __len__(self):
95 def __len__(self):
116 """return the length of the smartsets
96 """return the length of the smartsets
117
97
118 This can be expensive on smartset that could be lazy otherwise."""
98 This can be expensive on smartset that could be lazy otherwise."""
119 raise NotImplementedError()
99 raise NotImplementedError()
120
100
121 def reverse(self):
101 def reverse(self):
122 """reverse the expected iteration order"""
102 """reverse the expected iteration order"""
123 raise NotImplementedError()
103 raise NotImplementedError()
124
104
125 def sort(self, reverse=False):
105 def sort(self, reverse=False):
126 """get the set to iterate in an ascending or descending order"""
106 """get the set to iterate in an ascending or descending order"""
127 raise NotImplementedError()
107 raise NotImplementedError()
128
108
129 def __and__(self, other):
109 def __and__(self, other):
130 """Returns a new object with the intersection of the two collections.
110 """Returns a new object with the intersection of the two collections.
131
111
132 This is part of the mandatory API for smartset."""
112 This is part of the mandatory API for smartset."""
133 if isinstance(other, fullreposet):
113 if isinstance(other, fullreposet):
134 return self
114 return self
135 return self.filter(other.__contains__, condrepr=other, cache=False)
115 return self.filter(other.__contains__, condrepr=other, cache=False)
136
116
137 def __add__(self, other):
117 def __add__(self, other):
138 """Returns a new object with the union of the two collections.
118 """Returns a new object with the union of the two collections.
139
119
140 This is part of the mandatory API for smartset."""
120 This is part of the mandatory API for smartset."""
141 return addset(self, other)
121 return addset(self, other)
142
122
143 def __sub__(self, other):
123 def __sub__(self, other):
144 """Returns a new object with the substraction of the two collections.
124 """Returns a new object with the substraction of the two collections.
145
125
146 This is part of the mandatory API for smartset."""
126 This is part of the mandatory API for smartset."""
147 c = other.__contains__
127 c = other.__contains__
148 return self.filter(lambda r: not c(r), condrepr=('<not %r>', other),
128 return self.filter(lambda r: not c(r), condrepr=('<not %r>', other),
149 cache=False)
129 cache=False)
150
130
151 def filter(self, condition, condrepr=None, cache=True):
131 def filter(self, condition, condrepr=None, cache=True):
152 """Returns this smartset filtered by condition as a new smartset.
132 """Returns this smartset filtered by condition as a new smartset.
153
133
154 `condition` is a callable which takes a revision number and returns a
134 `condition` is a callable which takes a revision number and returns a
155 boolean. Optional `condrepr` provides a printable representation of
135 boolean. Optional `condrepr` provides a printable representation of
156 the given `condition`.
136 the given `condition`.
157
137
158 This is part of the mandatory API for smartset."""
138 This is part of the mandatory API for smartset."""
159 # builtin cannot be cached. but do not needs to
139 # builtin cannot be cached. but do not needs to
160 if cache and util.safehasattr(condition, 'func_code'):
140 if cache and util.safehasattr(condition, 'func_code'):
161 condition = util.cachefunc(condition)
141 condition = util.cachefunc(condition)
162 return filteredset(self, condition, condrepr)
142 return filteredset(self, condition, condrepr)
163
143
164 def slice(self, start, stop):
144 def slice(self, start, stop):
165 """Return new smartset that contains selected elements from this set"""
145 """Return new smartset that contains selected elements from this set"""
166 if start < 0 or stop < 0:
146 if start < 0 or stop < 0:
167 raise error.ProgrammingError('negative index not allowed')
147 raise error.ProgrammingError('negative index not allowed')
168 return self._slice(start, stop)
148 return self._slice(start, stop)
169
149
170 def _slice(self, start, stop):
150 def _slice(self, start, stop):
171 # sub classes may override this. start and stop must not be negative,
151 # sub classes may override this. start and stop must not be negative,
172 # but start > stop is allowed, which should be an empty set.
152 # but start > stop is allowed, which should be an empty set.
173 ys = []
153 ys = []
174 it = iter(self)
154 it = iter(self)
175 for x in xrange(start):
155 for x in xrange(start):
176 y = next(it, None)
156 y = next(it, None)
177 if y is None:
157 if y is None:
178 break
158 break
179 for x in xrange(stop - start):
159 for x in xrange(stop - start):
180 y = next(it, None)
160 y = next(it, None)
181 if y is None:
161 if y is None:
182 break
162 break
183 ys.append(y)
163 ys.append(y)
184 return baseset(ys, datarepr=('slice=%d:%d %r', start, stop, self))
164 return baseset(ys, datarepr=('slice=%d:%d %r', start, stop, self))
185
165
186 class baseset(abstractsmartset):
166 class baseset(abstractsmartset):
187 """Basic data structure that represents a revset and contains the basic
167 """Basic data structure that represents a revset and contains the basic
188 operation that it should be able to perform.
168 operation that it should be able to perform.
189
169
190 Every method in this class should be implemented by any smartset class.
170 Every method in this class should be implemented by any smartset class.
191
171
192 This class could be constructed by an (unordered) set, or an (ordered)
172 This class could be constructed by an (unordered) set, or an (ordered)
193 list-like object. If a set is provided, it'll be sorted lazily.
173 list-like object. If a set is provided, it'll be sorted lazily.
194
174
195 >>> x = [4, 0, 7, 6]
175 >>> x = [4, 0, 7, 6]
196 >>> y = [5, 6, 7, 3]
176 >>> y = [5, 6, 7, 3]
197
177
198 Construct by a set:
178 Construct by a set:
199 >>> xs = baseset(set(x))
179 >>> xs = baseset(set(x))
200 >>> ys = baseset(set(y))
180 >>> ys = baseset(set(y))
201 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
181 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
202 [[0, 4, 6, 7, 3, 5], [6, 7], [0, 4]]
182 [[0, 4, 6, 7, 3, 5], [6, 7], [0, 4]]
203 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
183 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
204 ['addset', 'baseset', 'baseset']
184 ['addset', 'baseset', 'baseset']
205
185
206 Construct by a list-like:
186 Construct by a list-like:
207 >>> xs = baseset(x)
187 >>> xs = baseset(x)
208 >>> ys = baseset(i for i in y)
188 >>> ys = baseset(i for i in y)
209 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
189 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
210 [[4, 0, 7, 6, 5, 3], [7, 6], [4, 0]]
190 [[4, 0, 7, 6, 5, 3], [7, 6], [4, 0]]
211 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
191 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
212 ['addset', 'filteredset', 'filteredset']
192 ['addset', 'filteredset', 'filteredset']
213
193
214 Populate "_set" fields in the lists so set optimization may be used:
194 Populate "_set" fields in the lists so set optimization may be used:
215 >>> [1 in xs, 3 in ys]
195 >>> [1 in xs, 3 in ys]
216 [False, True]
196 [False, True]
217
197
218 Without sort(), results won't be changed:
198 Without sort(), results won't be changed:
219 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
199 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
220 [[4, 0, 7, 6, 5, 3], [7, 6], [4, 0]]
200 [[4, 0, 7, 6, 5, 3], [7, 6], [4, 0]]
221 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
201 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
222 ['addset', 'filteredset', 'filteredset']
202 ['addset', 'filteredset', 'filteredset']
223
203
224 With sort(), set optimization could be used:
204 With sort(), set optimization could be used:
225 >>> xs.sort(reverse=True)
205 >>> xs.sort(reverse=True)
226 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
206 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
227 [[7, 6, 4, 0, 5, 3], [7, 6], [4, 0]]
207 [[7, 6, 4, 0, 5, 3], [7, 6], [4, 0]]
228 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
208 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
229 ['addset', 'baseset', 'baseset']
209 ['addset', 'baseset', 'baseset']
230
210
231 >>> ys.sort()
211 >>> ys.sort()
232 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
212 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
233 [[7, 6, 4, 0, 3, 5], [7, 6], [4, 0]]
213 [[7, 6, 4, 0, 3, 5], [7, 6], [4, 0]]
234 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
214 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
235 ['addset', 'baseset', 'baseset']
215 ['addset', 'baseset', 'baseset']
236
216
237 istopo is preserved across set operations
217 istopo is preserved across set operations
238 >>> xs = baseset(set(x), istopo=True)
218 >>> xs = baseset(set(x), istopo=True)
239 >>> rs = xs & ys
219 >>> rs = xs & ys
240 >>> type(rs).__name__
220 >>> type(rs).__name__
241 'baseset'
221 'baseset'
242 >>> rs._istopo
222 >>> rs._istopo
243 True
223 True
244 """
224 """
245 def __init__(self, data=(), datarepr=None, istopo=False):
225 def __init__(self, data=(), datarepr=None, istopo=False):
246 """
226 """
247 datarepr: a tuple of (format, obj, ...), a function or an object that
227 datarepr: a tuple of (format, obj, ...), a function or an object that
248 provides a printable representation of the given data.
228 provides a printable representation of the given data.
249 """
229 """
250 self._ascending = None
230 self._ascending = None
251 self._istopo = istopo
231 self._istopo = istopo
252 if isinstance(data, set):
232 if isinstance(data, set):
253 # converting set to list has a cost, do it lazily
233 # converting set to list has a cost, do it lazily
254 self._set = data
234 self._set = data
255 # set has no order we pick one for stability purpose
235 # set has no order we pick one for stability purpose
256 self._ascending = True
236 self._ascending = True
257 else:
237 else:
258 if not isinstance(data, list):
238 if not isinstance(data, list):
259 data = list(data)
239 data = list(data)
260 self._list = data
240 self._list = data
261 self._datarepr = datarepr
241 self._datarepr = datarepr
262
242
263 @util.propertycache
243 @util.propertycache
264 def _set(self):
244 def _set(self):
265 return set(self._list)
245 return set(self._list)
266
246
267 @util.propertycache
247 @util.propertycache
268 def _asclist(self):
248 def _asclist(self):
269 asclist = self._list[:]
249 asclist = self._list[:]
270 asclist.sort()
250 asclist.sort()
271 return asclist
251 return asclist
272
252
273 @util.propertycache
253 @util.propertycache
274 def _list(self):
254 def _list(self):
275 # _list is only lazily constructed if we have _set
255 # _list is only lazily constructed if we have _set
276 assert r'_set' in self.__dict__
256 assert r'_set' in self.__dict__
277 return list(self._set)
257 return list(self._set)
278
258
279 def __iter__(self):
259 def __iter__(self):
280 if self._ascending is None:
260 if self._ascending is None:
281 return iter(self._list)
261 return iter(self._list)
282 elif self._ascending:
262 elif self._ascending:
283 return iter(self._asclist)
263 return iter(self._asclist)
284 else:
264 else:
285 return reversed(self._asclist)
265 return reversed(self._asclist)
286
266
287 def fastasc(self):
267 def fastasc(self):
288 return iter(self._asclist)
268 return iter(self._asclist)
289
269
290 def fastdesc(self):
270 def fastdesc(self):
291 return reversed(self._asclist)
271 return reversed(self._asclist)
292
272
293 @util.propertycache
273 @util.propertycache
294 def __contains__(self):
274 def __contains__(self):
295 return self._set.__contains__
275 return self._set.__contains__
296
276
297 def __nonzero__(self):
277 def __nonzero__(self):
298 return bool(len(self))
278 return bool(len(self))
299
279
300 __bool__ = __nonzero__
280 __bool__ = __nonzero__
301
281
302 def sort(self, reverse=False):
282 def sort(self, reverse=False):
303 self._ascending = not bool(reverse)
283 self._ascending = not bool(reverse)
304 self._istopo = False
284 self._istopo = False
305
285
306 def reverse(self):
286 def reverse(self):
307 if self._ascending is None:
287 if self._ascending is None:
308 self._list.reverse()
288 self._list.reverse()
309 else:
289 else:
310 self._ascending = not self._ascending
290 self._ascending = not self._ascending
311 self._istopo = False
291 self._istopo = False
312
292
313 def __len__(self):
293 def __len__(self):
314 if r'_list' in self.__dict__:
294 if r'_list' in self.__dict__:
315 return len(self._list)
295 return len(self._list)
316 else:
296 else:
317 return len(self._set)
297 return len(self._set)
318
298
319 def isascending(self):
299 def isascending(self):
320 """Returns True if the collection is ascending order, False if not.
300 """Returns True if the collection is ascending order, False if not.
321
301
322 This is part of the mandatory API for smartset."""
302 This is part of the mandatory API for smartset."""
323 if len(self) <= 1:
303 if len(self) <= 1:
324 return True
304 return True
325 return self._ascending is not None and self._ascending
305 return self._ascending is not None and self._ascending
326
306
327 def isdescending(self):
307 def isdescending(self):
328 """Returns True if the collection is descending order, False if not.
308 """Returns True if the collection is descending order, False if not.
329
309
330 This is part of the mandatory API for smartset."""
310 This is part of the mandatory API for smartset."""
331 if len(self) <= 1:
311 if len(self) <= 1:
332 return True
312 return True
333 return self._ascending is not None and not self._ascending
313 return self._ascending is not None and not self._ascending
334
314
335 def istopo(self):
315 def istopo(self):
336 """Is the collection is in topographical order or not.
316 """Is the collection is in topographical order or not.
337
317
338 This is part of the mandatory API for smartset."""
318 This is part of the mandatory API for smartset."""
339 if len(self) <= 1:
319 if len(self) <= 1:
340 return True
320 return True
341 return self._istopo
321 return self._istopo
342
322
343 def first(self):
323 def first(self):
344 if self:
324 if self:
345 if self._ascending is None:
325 if self._ascending is None:
346 return self._list[0]
326 return self._list[0]
347 elif self._ascending:
327 elif self._ascending:
348 return self._asclist[0]
328 return self._asclist[0]
349 else:
329 else:
350 return self._asclist[-1]
330 return self._asclist[-1]
351 return None
331 return None
352
332
353 def last(self):
333 def last(self):
354 if self:
334 if self:
355 if self._ascending is None:
335 if self._ascending is None:
356 return self._list[-1]
336 return self._list[-1]
357 elif self._ascending:
337 elif self._ascending:
358 return self._asclist[-1]
338 return self._asclist[-1]
359 else:
339 else:
360 return self._asclist[0]
340 return self._asclist[0]
361 return None
341 return None
362
342
363 def _fastsetop(self, other, op):
343 def _fastsetop(self, other, op):
364 # try to use native set operations as fast paths
344 # try to use native set operations as fast paths
365 if (type(other) is baseset and r'_set' in other.__dict__ and r'_set' in
345 if (type(other) is baseset and r'_set' in other.__dict__ and r'_set' in
366 self.__dict__ and self._ascending is not None):
346 self.__dict__ and self._ascending is not None):
367 s = baseset(data=getattr(self._set, op)(other._set),
347 s = baseset(data=getattr(self._set, op)(other._set),
368 istopo=self._istopo)
348 istopo=self._istopo)
369 s._ascending = self._ascending
349 s._ascending = self._ascending
370 else:
350 else:
371 s = getattr(super(baseset, self), op)(other)
351 s = getattr(super(baseset, self), op)(other)
372 return s
352 return s
373
353
374 def __and__(self, other):
354 def __and__(self, other):
375 return self._fastsetop(other, '__and__')
355 return self._fastsetop(other, '__and__')
376
356
377 def __sub__(self, other):
357 def __sub__(self, other):
378 return self._fastsetop(other, '__sub__')
358 return self._fastsetop(other, '__sub__')
379
359
380 def _slice(self, start, stop):
360 def _slice(self, start, stop):
381 # creating new list should be generally cheaper than iterating items
361 # creating new list should be generally cheaper than iterating items
382 if self._ascending is None:
362 if self._ascending is None:
383 return baseset(self._list[start:stop], istopo=self._istopo)
363 return baseset(self._list[start:stop], istopo=self._istopo)
384
364
385 data = self._asclist
365 data = self._asclist
386 if not self._ascending:
366 if not self._ascending:
387 start, stop = max(len(data) - stop, 0), max(len(data) - start, 0)
367 start, stop = max(len(data) - stop, 0), max(len(data) - start, 0)
388 s = baseset(data[start:stop], istopo=self._istopo)
368 s = baseset(data[start:stop], istopo=self._istopo)
389 s._ascending = self._ascending
369 s._ascending = self._ascending
390 return s
370 return s
391
371
392 @encoding.strmethod
372 @encoding.strmethod
393 def __repr__(self):
373 def __repr__(self):
394 d = {None: '', False: '-', True: '+'}[self._ascending]
374 d = {None: '', False: '-', True: '+'}[self._ascending]
395 s = _formatsetrepr(self._datarepr)
375 s = stringutil.buildrepr(self._datarepr)
396 if not s:
376 if not s:
397 l = self._list
377 l = self._list
398 # if _list has been built from a set, it might have a different
378 # if _list has been built from a set, it might have a different
399 # order from one python implementation to another.
379 # order from one python implementation to another.
400 # We fallback to the sorted version for a stable output.
380 # We fallback to the sorted version for a stable output.
401 if self._ascending is not None:
381 if self._ascending is not None:
402 l = self._asclist
382 l = self._asclist
403 s = pycompat.byterepr(l)
383 s = pycompat.byterepr(l)
404 return '<%s%s %s>' % (_typename(self), d, s)
384 return '<%s%s %s>' % (_typename(self), d, s)
405
385
406 class filteredset(abstractsmartset):
386 class filteredset(abstractsmartset):
407 """Duck type for baseset class which iterates lazily over the revisions in
387 """Duck type for baseset class which iterates lazily over the revisions in
408 the subset and contains a function which tests for membership in the
388 the subset and contains a function which tests for membership in the
409 revset
389 revset
410 """
390 """
411 def __init__(self, subset, condition=lambda x: True, condrepr=None):
391 def __init__(self, subset, condition=lambda x: True, condrepr=None):
412 """
392 """
413 condition: a function that decide whether a revision in the subset
393 condition: a function that decide whether a revision in the subset
414 belongs to the revset or not.
394 belongs to the revset or not.
415 condrepr: a tuple of (format, obj, ...), a function or an object that
395 condrepr: a tuple of (format, obj, ...), a function or an object that
416 provides a printable representation of the given condition.
396 provides a printable representation of the given condition.
417 """
397 """
418 self._subset = subset
398 self._subset = subset
419 self._condition = condition
399 self._condition = condition
420 self._condrepr = condrepr
400 self._condrepr = condrepr
421
401
422 def __contains__(self, x):
402 def __contains__(self, x):
423 return x in self._subset and self._condition(x)
403 return x in self._subset and self._condition(x)
424
404
425 def __iter__(self):
405 def __iter__(self):
426 return self._iterfilter(self._subset)
406 return self._iterfilter(self._subset)
427
407
428 def _iterfilter(self, it):
408 def _iterfilter(self, it):
429 cond = self._condition
409 cond = self._condition
430 for x in it:
410 for x in it:
431 if cond(x):
411 if cond(x):
432 yield x
412 yield x
433
413
434 @property
414 @property
435 def fastasc(self):
415 def fastasc(self):
436 it = self._subset.fastasc
416 it = self._subset.fastasc
437 if it is None:
417 if it is None:
438 return None
418 return None
439 return lambda: self._iterfilter(it())
419 return lambda: self._iterfilter(it())
440
420
441 @property
421 @property
442 def fastdesc(self):
422 def fastdesc(self):
443 it = self._subset.fastdesc
423 it = self._subset.fastdesc
444 if it is None:
424 if it is None:
445 return None
425 return None
446 return lambda: self._iterfilter(it())
426 return lambda: self._iterfilter(it())
447
427
448 def __nonzero__(self):
428 def __nonzero__(self):
449 fast = None
429 fast = None
450 candidates = [self.fastasc if self.isascending() else None,
430 candidates = [self.fastasc if self.isascending() else None,
451 self.fastdesc if self.isdescending() else None,
431 self.fastdesc if self.isdescending() else None,
452 self.fastasc,
432 self.fastasc,
453 self.fastdesc]
433 self.fastdesc]
454 for candidate in candidates:
434 for candidate in candidates:
455 if candidate is not None:
435 if candidate is not None:
456 fast = candidate
436 fast = candidate
457 break
437 break
458
438
459 if fast is not None:
439 if fast is not None:
460 it = fast()
440 it = fast()
461 else:
441 else:
462 it = self
442 it = self
463
443
464 for r in it:
444 for r in it:
465 return True
445 return True
466 return False
446 return False
467
447
468 __bool__ = __nonzero__
448 __bool__ = __nonzero__
469
449
470 def __len__(self):
450 def __len__(self):
471 # Basic implementation to be changed in future patches.
451 # Basic implementation to be changed in future patches.
472 # until this gets improved, we use generator expression
452 # until this gets improved, we use generator expression
473 # here, since list comprehensions are free to call __len__ again
453 # here, since list comprehensions are free to call __len__ again
474 # causing infinite recursion
454 # causing infinite recursion
475 l = baseset(r for r in self)
455 l = baseset(r for r in self)
476 return len(l)
456 return len(l)
477
457
478 def sort(self, reverse=False):
458 def sort(self, reverse=False):
479 self._subset.sort(reverse=reverse)
459 self._subset.sort(reverse=reverse)
480
460
481 def reverse(self):
461 def reverse(self):
482 self._subset.reverse()
462 self._subset.reverse()
483
463
484 def isascending(self):
464 def isascending(self):
485 return self._subset.isascending()
465 return self._subset.isascending()
486
466
487 def isdescending(self):
467 def isdescending(self):
488 return self._subset.isdescending()
468 return self._subset.isdescending()
489
469
490 def istopo(self):
470 def istopo(self):
491 return self._subset.istopo()
471 return self._subset.istopo()
492
472
493 def first(self):
473 def first(self):
494 for x in self:
474 for x in self:
495 return x
475 return x
496 return None
476 return None
497
477
498 def last(self):
478 def last(self):
499 it = None
479 it = None
500 if self.isascending():
480 if self.isascending():
501 it = self.fastdesc
481 it = self.fastdesc
502 elif self.isdescending():
482 elif self.isdescending():
503 it = self.fastasc
483 it = self.fastasc
504 if it is not None:
484 if it is not None:
505 for x in it():
485 for x in it():
506 return x
486 return x
507 return None #empty case
487 return None #empty case
508 else:
488 else:
509 x = None
489 x = None
510 for x in self:
490 for x in self:
511 pass
491 pass
512 return x
492 return x
513
493
514 @encoding.strmethod
494 @encoding.strmethod
515 def __repr__(self):
495 def __repr__(self):
516 xs = [pycompat.byterepr(self._subset)]
496 xs = [pycompat.byterepr(self._subset)]
517 s = _formatsetrepr(self._condrepr)
497 s = stringutil.buildrepr(self._condrepr)
518 if s:
498 if s:
519 xs.append(s)
499 xs.append(s)
520 return '<%s %s>' % (_typename(self), ', '.join(xs))
500 return '<%s %s>' % (_typename(self), ', '.join(xs))
521
501
522 def _iterordered(ascending, iter1, iter2):
502 def _iterordered(ascending, iter1, iter2):
523 """produce an ordered iteration from two iterators with the same order
503 """produce an ordered iteration from two iterators with the same order
524
504
525 The ascending is used to indicated the iteration direction.
505 The ascending is used to indicated the iteration direction.
526 """
506 """
527 choice = max
507 choice = max
528 if ascending:
508 if ascending:
529 choice = min
509 choice = min
530
510
531 val1 = None
511 val1 = None
532 val2 = None
512 val2 = None
533 try:
513 try:
534 # Consume both iterators in an ordered way until one is empty
514 # Consume both iterators in an ordered way until one is empty
535 while True:
515 while True:
536 if val1 is None:
516 if val1 is None:
537 val1 = next(iter1)
517 val1 = next(iter1)
538 if val2 is None:
518 if val2 is None:
539 val2 = next(iter2)
519 val2 = next(iter2)
540 n = choice(val1, val2)
520 n = choice(val1, val2)
541 yield n
521 yield n
542 if val1 == n:
522 if val1 == n:
543 val1 = None
523 val1 = None
544 if val2 == n:
524 if val2 == n:
545 val2 = None
525 val2 = None
546 except StopIteration:
526 except StopIteration:
547 # Flush any remaining values and consume the other one
527 # Flush any remaining values and consume the other one
548 it = iter2
528 it = iter2
549 if val1 is not None:
529 if val1 is not None:
550 yield val1
530 yield val1
551 it = iter1
531 it = iter1
552 elif val2 is not None:
532 elif val2 is not None:
553 # might have been equality and both are empty
533 # might have been equality and both are empty
554 yield val2
534 yield val2
555 for val in it:
535 for val in it:
556 yield val
536 yield val
557
537
558 class addset(abstractsmartset):
538 class addset(abstractsmartset):
559 """Represent the addition of two sets
539 """Represent the addition of two sets
560
540
561 Wrapper structure for lazily adding two structures without losing much
541 Wrapper structure for lazily adding two structures without losing much
562 performance on the __contains__ method
542 performance on the __contains__ method
563
543
564 If the ascending attribute is set, that means the two structures are
544 If the ascending attribute is set, that means the two structures are
565 ordered in either an ascending or descending way. Therefore, we can add
545 ordered in either an ascending or descending way. Therefore, we can add
566 them maintaining the order by iterating over both at the same time
546 them maintaining the order by iterating over both at the same time
567
547
568 >>> xs = baseset([0, 3, 2])
548 >>> xs = baseset([0, 3, 2])
569 >>> ys = baseset([5, 2, 4])
549 >>> ys = baseset([5, 2, 4])
570
550
571 >>> rs = addset(xs, ys)
551 >>> rs = addset(xs, ys)
572 >>> bool(rs), 0 in rs, 1 in rs, 5 in rs, rs.first(), rs.last()
552 >>> bool(rs), 0 in rs, 1 in rs, 5 in rs, rs.first(), rs.last()
573 (True, True, False, True, 0, 4)
553 (True, True, False, True, 0, 4)
574 >>> rs = addset(xs, baseset([]))
554 >>> rs = addset(xs, baseset([]))
575 >>> bool(rs), 0 in rs, 1 in rs, rs.first(), rs.last()
555 >>> bool(rs), 0 in rs, 1 in rs, rs.first(), rs.last()
576 (True, True, False, 0, 2)
556 (True, True, False, 0, 2)
577 >>> rs = addset(baseset([]), baseset([]))
557 >>> rs = addset(baseset([]), baseset([]))
578 >>> bool(rs), 0 in rs, rs.first(), rs.last()
558 >>> bool(rs), 0 in rs, rs.first(), rs.last()
579 (False, False, None, None)
559 (False, False, None, None)
580
560
581 iterate unsorted:
561 iterate unsorted:
582 >>> rs = addset(xs, ys)
562 >>> rs = addset(xs, ys)
583 >>> # (use generator because pypy could call len())
563 >>> # (use generator because pypy could call len())
584 >>> list(x for x in rs) # without _genlist
564 >>> list(x for x in rs) # without _genlist
585 [0, 3, 2, 5, 4]
565 [0, 3, 2, 5, 4]
586 >>> assert not rs._genlist
566 >>> assert not rs._genlist
587 >>> len(rs)
567 >>> len(rs)
588 5
568 5
589 >>> [x for x in rs] # with _genlist
569 >>> [x for x in rs] # with _genlist
590 [0, 3, 2, 5, 4]
570 [0, 3, 2, 5, 4]
591 >>> assert rs._genlist
571 >>> assert rs._genlist
592
572
593 iterate ascending:
573 iterate ascending:
594 >>> rs = addset(xs, ys, ascending=True)
574 >>> rs = addset(xs, ys, ascending=True)
595 >>> # (use generator because pypy could call len())
575 >>> # (use generator because pypy could call len())
596 >>> list(x for x in rs), list(x for x in rs.fastasc()) # without _asclist
576 >>> list(x for x in rs), list(x for x in rs.fastasc()) # without _asclist
597 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
577 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
598 >>> assert not rs._asclist
578 >>> assert not rs._asclist
599 >>> len(rs)
579 >>> len(rs)
600 5
580 5
601 >>> [x for x in rs], [x for x in rs.fastasc()]
581 >>> [x for x in rs], [x for x in rs.fastasc()]
602 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
582 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
603 >>> assert rs._asclist
583 >>> assert rs._asclist
604
584
605 iterate descending:
585 iterate descending:
606 >>> rs = addset(xs, ys, ascending=False)
586 >>> rs = addset(xs, ys, ascending=False)
607 >>> # (use generator because pypy could call len())
587 >>> # (use generator because pypy could call len())
608 >>> list(x for x in rs), list(x for x in rs.fastdesc()) # without _asclist
588 >>> list(x for x in rs), list(x for x in rs.fastdesc()) # without _asclist
609 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
589 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
610 >>> assert not rs._asclist
590 >>> assert not rs._asclist
611 >>> len(rs)
591 >>> len(rs)
612 5
592 5
613 >>> [x for x in rs], [x for x in rs.fastdesc()]
593 >>> [x for x in rs], [x for x in rs.fastdesc()]
614 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
594 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
615 >>> assert rs._asclist
595 >>> assert rs._asclist
616
596
617 iterate ascending without fastasc:
597 iterate ascending without fastasc:
618 >>> rs = addset(xs, generatorset(ys), ascending=True)
598 >>> rs = addset(xs, generatorset(ys), ascending=True)
619 >>> assert rs.fastasc is None
599 >>> assert rs.fastasc is None
620 >>> [x for x in rs]
600 >>> [x for x in rs]
621 [0, 2, 3, 4, 5]
601 [0, 2, 3, 4, 5]
622
602
623 iterate descending without fastdesc:
603 iterate descending without fastdesc:
624 >>> rs = addset(generatorset(xs), ys, ascending=False)
604 >>> rs = addset(generatorset(xs), ys, ascending=False)
625 >>> assert rs.fastdesc is None
605 >>> assert rs.fastdesc is None
626 >>> [x for x in rs]
606 >>> [x for x in rs]
627 [5, 4, 3, 2, 0]
607 [5, 4, 3, 2, 0]
628 """
608 """
629 def __init__(self, revs1, revs2, ascending=None):
609 def __init__(self, revs1, revs2, ascending=None):
630 self._r1 = revs1
610 self._r1 = revs1
631 self._r2 = revs2
611 self._r2 = revs2
632 self._iter = None
612 self._iter = None
633 self._ascending = ascending
613 self._ascending = ascending
634 self._genlist = None
614 self._genlist = None
635 self._asclist = None
615 self._asclist = None
636
616
637 def __len__(self):
617 def __len__(self):
638 return len(self._list)
618 return len(self._list)
639
619
640 def __nonzero__(self):
620 def __nonzero__(self):
641 return bool(self._r1) or bool(self._r2)
621 return bool(self._r1) or bool(self._r2)
642
622
643 __bool__ = __nonzero__
623 __bool__ = __nonzero__
644
624
645 @util.propertycache
625 @util.propertycache
646 def _list(self):
626 def _list(self):
647 if not self._genlist:
627 if not self._genlist:
648 self._genlist = baseset(iter(self))
628 self._genlist = baseset(iter(self))
649 return self._genlist
629 return self._genlist
650
630
651 def __iter__(self):
631 def __iter__(self):
652 """Iterate over both collections without repeating elements
632 """Iterate over both collections without repeating elements
653
633
654 If the ascending attribute is not set, iterate over the first one and
634 If the ascending attribute is not set, iterate over the first one and
655 then over the second one checking for membership on the first one so we
635 then over the second one checking for membership on the first one so we
656 dont yield any duplicates.
636 dont yield any duplicates.
657
637
658 If the ascending attribute is set, iterate over both collections at the
638 If the ascending attribute is set, iterate over both collections at the
659 same time, yielding only one value at a time in the given order.
639 same time, yielding only one value at a time in the given order.
660 """
640 """
661 if self._ascending is None:
641 if self._ascending is None:
662 if self._genlist:
642 if self._genlist:
663 return iter(self._genlist)
643 return iter(self._genlist)
664 def arbitraryordergen():
644 def arbitraryordergen():
665 for r in self._r1:
645 for r in self._r1:
666 yield r
646 yield r
667 inr1 = self._r1.__contains__
647 inr1 = self._r1.__contains__
668 for r in self._r2:
648 for r in self._r2:
669 if not inr1(r):
649 if not inr1(r):
670 yield r
650 yield r
671 return arbitraryordergen()
651 return arbitraryordergen()
672 # try to use our own fast iterator if it exists
652 # try to use our own fast iterator if it exists
673 self._trysetasclist()
653 self._trysetasclist()
674 if self._ascending:
654 if self._ascending:
675 attr = 'fastasc'
655 attr = 'fastasc'
676 else:
656 else:
677 attr = 'fastdesc'
657 attr = 'fastdesc'
678 it = getattr(self, attr)
658 it = getattr(self, attr)
679 if it is not None:
659 if it is not None:
680 return it()
660 return it()
681 # maybe half of the component supports fast
661 # maybe half of the component supports fast
682 # get iterator for _r1
662 # get iterator for _r1
683 iter1 = getattr(self._r1, attr)
663 iter1 = getattr(self._r1, attr)
684 if iter1 is None:
664 if iter1 is None:
685 # let's avoid side effect (not sure it matters)
665 # let's avoid side effect (not sure it matters)
686 iter1 = iter(sorted(self._r1, reverse=not self._ascending))
666 iter1 = iter(sorted(self._r1, reverse=not self._ascending))
687 else:
667 else:
688 iter1 = iter1()
668 iter1 = iter1()
689 # get iterator for _r2
669 # get iterator for _r2
690 iter2 = getattr(self._r2, attr)
670 iter2 = getattr(self._r2, attr)
691 if iter2 is None:
671 if iter2 is None:
692 # let's avoid side effect (not sure it matters)
672 # let's avoid side effect (not sure it matters)
693 iter2 = iter(sorted(self._r2, reverse=not self._ascending))
673 iter2 = iter(sorted(self._r2, reverse=not self._ascending))
694 else:
674 else:
695 iter2 = iter2()
675 iter2 = iter2()
696 return _iterordered(self._ascending, iter1, iter2)
676 return _iterordered(self._ascending, iter1, iter2)
697
677
698 def _trysetasclist(self):
678 def _trysetasclist(self):
699 """populate the _asclist attribute if possible and necessary"""
679 """populate the _asclist attribute if possible and necessary"""
700 if self._genlist is not None and self._asclist is None:
680 if self._genlist is not None and self._asclist is None:
701 self._asclist = sorted(self._genlist)
681 self._asclist = sorted(self._genlist)
702
682
703 @property
683 @property
704 def fastasc(self):
684 def fastasc(self):
705 self._trysetasclist()
685 self._trysetasclist()
706 if self._asclist is not None:
686 if self._asclist is not None:
707 return self._asclist.__iter__
687 return self._asclist.__iter__
708 iter1 = self._r1.fastasc
688 iter1 = self._r1.fastasc
709 iter2 = self._r2.fastasc
689 iter2 = self._r2.fastasc
710 if None in (iter1, iter2):
690 if None in (iter1, iter2):
711 return None
691 return None
712 return lambda: _iterordered(True, iter1(), iter2())
692 return lambda: _iterordered(True, iter1(), iter2())
713
693
714 @property
694 @property
715 def fastdesc(self):
695 def fastdesc(self):
716 self._trysetasclist()
696 self._trysetasclist()
717 if self._asclist is not None:
697 if self._asclist is not None:
718 return self._asclist.__reversed__
698 return self._asclist.__reversed__
719 iter1 = self._r1.fastdesc
699 iter1 = self._r1.fastdesc
720 iter2 = self._r2.fastdesc
700 iter2 = self._r2.fastdesc
721 if None in (iter1, iter2):
701 if None in (iter1, iter2):
722 return None
702 return None
723 return lambda: _iterordered(False, iter1(), iter2())
703 return lambda: _iterordered(False, iter1(), iter2())
724
704
725 def __contains__(self, x):
705 def __contains__(self, x):
726 return x in self._r1 or x in self._r2
706 return x in self._r1 or x in self._r2
727
707
728 def sort(self, reverse=False):
708 def sort(self, reverse=False):
729 """Sort the added set
709 """Sort the added set
730
710
731 For this we use the cached list with all the generated values and if we
711 For this we use the cached list with all the generated values and if we
732 know they are ascending or descending we can sort them in a smart way.
712 know they are ascending or descending we can sort them in a smart way.
733 """
713 """
734 self._ascending = not reverse
714 self._ascending = not reverse
735
715
736 def isascending(self):
716 def isascending(self):
737 return self._ascending is not None and self._ascending
717 return self._ascending is not None and self._ascending
738
718
739 def isdescending(self):
719 def isdescending(self):
740 return self._ascending is not None and not self._ascending
720 return self._ascending is not None and not self._ascending
741
721
742 def istopo(self):
722 def istopo(self):
743 # not worth the trouble asserting if the two sets combined are still
723 # not worth the trouble asserting if the two sets combined are still
744 # in topographical order. Use the sort() predicate to explicitly sort
724 # in topographical order. Use the sort() predicate to explicitly sort
745 # again instead.
725 # again instead.
746 return False
726 return False
747
727
748 def reverse(self):
728 def reverse(self):
749 if self._ascending is None:
729 if self._ascending is None:
750 self._list.reverse()
730 self._list.reverse()
751 else:
731 else:
752 self._ascending = not self._ascending
732 self._ascending = not self._ascending
753
733
754 def first(self):
734 def first(self):
755 for x in self:
735 for x in self:
756 return x
736 return x
757 return None
737 return None
758
738
759 def last(self):
739 def last(self):
760 self.reverse()
740 self.reverse()
761 val = self.first()
741 val = self.first()
762 self.reverse()
742 self.reverse()
763 return val
743 return val
764
744
765 @encoding.strmethod
745 @encoding.strmethod
766 def __repr__(self):
746 def __repr__(self):
767 d = {None: '', False: '-', True: '+'}[self._ascending]
747 d = {None: '', False: '-', True: '+'}[self._ascending]
768 return '<%s%s %r, %r>' % (_typename(self), d, self._r1, self._r2)
748 return '<%s%s %r, %r>' % (_typename(self), d, self._r1, self._r2)
769
749
770 class generatorset(abstractsmartset):
750 class generatorset(abstractsmartset):
771 """Wrap a generator for lazy iteration
751 """Wrap a generator for lazy iteration
772
752
773 Wrapper structure for generators that provides lazy membership and can
753 Wrapper structure for generators that provides lazy membership and can
774 be iterated more than once.
754 be iterated more than once.
775 When asked for membership it generates values until either it finds the
755 When asked for membership it generates values until either it finds the
776 requested one or has gone through all the elements in the generator
756 requested one or has gone through all the elements in the generator
777
757
778 >>> xs = generatorset([0, 1, 4], iterasc=True)
758 >>> xs = generatorset([0, 1, 4], iterasc=True)
779 >>> assert xs.last() == xs.last()
759 >>> assert xs.last() == xs.last()
780 >>> xs.last() # cached
760 >>> xs.last() # cached
781 4
761 4
782 """
762 """
783 def __new__(cls, gen, iterasc=None):
763 def __new__(cls, gen, iterasc=None):
784 if iterasc is None:
764 if iterasc is None:
785 typ = cls
765 typ = cls
786 elif iterasc:
766 elif iterasc:
787 typ = _generatorsetasc
767 typ = _generatorsetasc
788 else:
768 else:
789 typ = _generatorsetdesc
769 typ = _generatorsetdesc
790
770
791 return super(generatorset, cls).__new__(typ)
771 return super(generatorset, cls).__new__(typ)
792
772
793 def __init__(self, gen, iterasc=None):
773 def __init__(self, gen, iterasc=None):
794 """
774 """
795 gen: a generator producing the values for the generatorset.
775 gen: a generator producing the values for the generatorset.
796 """
776 """
797 self._gen = gen
777 self._gen = gen
798 self._asclist = None
778 self._asclist = None
799 self._cache = {}
779 self._cache = {}
800 self._genlist = []
780 self._genlist = []
801 self._finished = False
781 self._finished = False
802 self._ascending = True
782 self._ascending = True
803
783
804 def __nonzero__(self):
784 def __nonzero__(self):
805 # Do not use 'for r in self' because it will enforce the iteration
785 # Do not use 'for r in self' because it will enforce the iteration
806 # order (default ascending), possibly unrolling a whole descending
786 # order (default ascending), possibly unrolling a whole descending
807 # iterator.
787 # iterator.
808 if self._genlist:
788 if self._genlist:
809 return True
789 return True
810 for r in self._consumegen():
790 for r in self._consumegen():
811 return True
791 return True
812 return False
792 return False
813
793
814 __bool__ = __nonzero__
794 __bool__ = __nonzero__
815
795
816 def __contains__(self, x):
796 def __contains__(self, x):
817 if x in self._cache:
797 if x in self._cache:
818 return self._cache[x]
798 return self._cache[x]
819
799
820 # Use new values only, as existing values would be cached.
800 # Use new values only, as existing values would be cached.
821 for l in self._consumegen():
801 for l in self._consumegen():
822 if l == x:
802 if l == x:
823 return True
803 return True
824
804
825 self._cache[x] = False
805 self._cache[x] = False
826 return False
806 return False
827
807
828 def __iter__(self):
808 def __iter__(self):
829 if self._ascending:
809 if self._ascending:
830 it = self.fastasc
810 it = self.fastasc
831 else:
811 else:
832 it = self.fastdesc
812 it = self.fastdesc
833 if it is not None:
813 if it is not None:
834 return it()
814 return it()
835 # we need to consume the iterator
815 # we need to consume the iterator
836 for x in self._consumegen():
816 for x in self._consumegen():
837 pass
817 pass
838 # recall the same code
818 # recall the same code
839 return iter(self)
819 return iter(self)
840
820
841 def _iterator(self):
821 def _iterator(self):
842 if self._finished:
822 if self._finished:
843 return iter(self._genlist)
823 return iter(self._genlist)
844
824
845 # We have to use this complex iteration strategy to allow multiple
825 # We have to use this complex iteration strategy to allow multiple
846 # iterations at the same time. We need to be able to catch revision
826 # iterations at the same time. We need to be able to catch revision
847 # removed from _consumegen and added to genlist in another instance.
827 # removed from _consumegen and added to genlist in another instance.
848 #
828 #
849 # Getting rid of it would provide an about 15% speed up on this
829 # Getting rid of it would provide an about 15% speed up on this
850 # iteration.
830 # iteration.
851 genlist = self._genlist
831 genlist = self._genlist
852 nextgen = self._consumegen()
832 nextgen = self._consumegen()
853 _len, _next = len, next # cache global lookup
833 _len, _next = len, next # cache global lookup
854 def gen():
834 def gen():
855 i = 0
835 i = 0
856 while True:
836 while True:
857 if i < _len(genlist):
837 if i < _len(genlist):
858 yield genlist[i]
838 yield genlist[i]
859 else:
839 else:
860 try:
840 try:
861 yield _next(nextgen)
841 yield _next(nextgen)
862 except StopIteration:
842 except StopIteration:
863 return
843 return
864 i += 1
844 i += 1
865 return gen()
845 return gen()
866
846
867 def _consumegen(self):
847 def _consumegen(self):
868 cache = self._cache
848 cache = self._cache
869 genlist = self._genlist.append
849 genlist = self._genlist.append
870 for item in self._gen:
850 for item in self._gen:
871 cache[item] = True
851 cache[item] = True
872 genlist(item)
852 genlist(item)
873 yield item
853 yield item
874 if not self._finished:
854 if not self._finished:
875 self._finished = True
855 self._finished = True
876 asc = self._genlist[:]
856 asc = self._genlist[:]
877 asc.sort()
857 asc.sort()
878 self._asclist = asc
858 self._asclist = asc
879 self.fastasc = asc.__iter__
859 self.fastasc = asc.__iter__
880 self.fastdesc = asc.__reversed__
860 self.fastdesc = asc.__reversed__
881
861
882 def __len__(self):
862 def __len__(self):
883 for x in self._consumegen():
863 for x in self._consumegen():
884 pass
864 pass
885 return len(self._genlist)
865 return len(self._genlist)
886
866
887 def sort(self, reverse=False):
867 def sort(self, reverse=False):
888 self._ascending = not reverse
868 self._ascending = not reverse
889
869
890 def reverse(self):
870 def reverse(self):
891 self._ascending = not self._ascending
871 self._ascending = not self._ascending
892
872
893 def isascending(self):
873 def isascending(self):
894 return self._ascending
874 return self._ascending
895
875
896 def isdescending(self):
876 def isdescending(self):
897 return not self._ascending
877 return not self._ascending
898
878
899 def istopo(self):
879 def istopo(self):
900 # not worth the trouble asserting if the two sets combined are still
880 # not worth the trouble asserting if the two sets combined are still
901 # in topographical order. Use the sort() predicate to explicitly sort
881 # in topographical order. Use the sort() predicate to explicitly sort
902 # again instead.
882 # again instead.
903 return False
883 return False
904
884
905 def first(self):
885 def first(self):
906 if self._ascending:
886 if self._ascending:
907 it = self.fastasc
887 it = self.fastasc
908 else:
888 else:
909 it = self.fastdesc
889 it = self.fastdesc
910 if it is None:
890 if it is None:
911 # we need to consume all and try again
891 # we need to consume all and try again
912 for x in self._consumegen():
892 for x in self._consumegen():
913 pass
893 pass
914 return self.first()
894 return self.first()
915 return next(it(), None)
895 return next(it(), None)
916
896
917 def last(self):
897 def last(self):
918 if self._ascending:
898 if self._ascending:
919 it = self.fastdesc
899 it = self.fastdesc
920 else:
900 else:
921 it = self.fastasc
901 it = self.fastasc
922 if it is None:
902 if it is None:
923 # we need to consume all and try again
903 # we need to consume all and try again
924 for x in self._consumegen():
904 for x in self._consumegen():
925 pass
905 pass
926 return self.last()
906 return self.last()
927 return next(it(), None)
907 return next(it(), None)
928
908
929 @encoding.strmethod
909 @encoding.strmethod
930 def __repr__(self):
910 def __repr__(self):
931 d = {False: '-', True: '+'}[self._ascending]
911 d = {False: '-', True: '+'}[self._ascending]
932 return '<%s%s>' % (_typename(self), d)
912 return '<%s%s>' % (_typename(self), d)
933
913
934 class _generatorsetasc(generatorset):
914 class _generatorsetasc(generatorset):
935 """Special case of generatorset optimized for ascending generators."""
915 """Special case of generatorset optimized for ascending generators."""
936
916
937 fastasc = generatorset._iterator
917 fastasc = generatorset._iterator
938
918
939 def __contains__(self, x):
919 def __contains__(self, x):
940 if x in self._cache:
920 if x in self._cache:
941 return self._cache[x]
921 return self._cache[x]
942
922
943 # Use new values only, as existing values would be cached.
923 # Use new values only, as existing values would be cached.
944 for l in self._consumegen():
924 for l in self._consumegen():
945 if l == x:
925 if l == x:
946 return True
926 return True
947 if l > x:
927 if l > x:
948 break
928 break
949
929
950 self._cache[x] = False
930 self._cache[x] = False
951 return False
931 return False
952
932
953 class _generatorsetdesc(generatorset):
933 class _generatorsetdesc(generatorset):
954 """Special case of generatorset optimized for descending generators."""
934 """Special case of generatorset optimized for descending generators."""
955
935
956 fastdesc = generatorset._iterator
936 fastdesc = generatorset._iterator
957
937
958 def __contains__(self, x):
938 def __contains__(self, x):
959 if x in self._cache:
939 if x in self._cache:
960 return self._cache[x]
940 return self._cache[x]
961
941
962 # Use new values only, as existing values would be cached.
942 # Use new values only, as existing values would be cached.
963 for l in self._consumegen():
943 for l in self._consumegen():
964 if l == x:
944 if l == x:
965 return True
945 return True
966 if l < x:
946 if l < x:
967 break
947 break
968
948
969 self._cache[x] = False
949 self._cache[x] = False
970 return False
950 return False
971
951
972 def spanset(repo, start=0, end=None):
952 def spanset(repo, start=0, end=None):
973 """Create a spanset that represents a range of repository revisions
953 """Create a spanset that represents a range of repository revisions
974
954
975 start: first revision included the set (default to 0)
955 start: first revision included the set (default to 0)
976 end: first revision excluded (last+1) (default to len(repo))
956 end: first revision excluded (last+1) (default to len(repo))
977
957
978 Spanset will be descending if `end` < `start`.
958 Spanset will be descending if `end` < `start`.
979 """
959 """
980 if end is None:
960 if end is None:
981 end = len(repo)
961 end = len(repo)
982 ascending = start <= end
962 ascending = start <= end
983 if not ascending:
963 if not ascending:
984 start, end = end + 1, start + 1
964 start, end = end + 1, start + 1
985 return _spanset(start, end, ascending, repo.changelog.filteredrevs)
965 return _spanset(start, end, ascending, repo.changelog.filteredrevs)
986
966
987 class _spanset(abstractsmartset):
967 class _spanset(abstractsmartset):
988 """Duck type for baseset class which represents a range of revisions and
968 """Duck type for baseset class which represents a range of revisions and
989 can work lazily and without having all the range in memory
969 can work lazily and without having all the range in memory
990
970
991 Note that spanset(x, y) behave almost like xrange(x, y) except for two
971 Note that spanset(x, y) behave almost like xrange(x, y) except for two
992 notable points:
972 notable points:
993 - when x < y it will be automatically descending,
973 - when x < y it will be automatically descending,
994 - revision filtered with this repoview will be skipped.
974 - revision filtered with this repoview will be skipped.
995
975
996 """
976 """
997 def __init__(self, start, end, ascending, hiddenrevs):
977 def __init__(self, start, end, ascending, hiddenrevs):
998 self._start = start
978 self._start = start
999 self._end = end
979 self._end = end
1000 self._ascending = ascending
980 self._ascending = ascending
1001 self._hiddenrevs = hiddenrevs
981 self._hiddenrevs = hiddenrevs
1002
982
1003 def sort(self, reverse=False):
983 def sort(self, reverse=False):
1004 self._ascending = not reverse
984 self._ascending = not reverse
1005
985
1006 def reverse(self):
986 def reverse(self):
1007 self._ascending = not self._ascending
987 self._ascending = not self._ascending
1008
988
1009 def istopo(self):
989 def istopo(self):
1010 # not worth the trouble asserting if the two sets combined are still
990 # not worth the trouble asserting if the two sets combined are still
1011 # in topographical order. Use the sort() predicate to explicitly sort
991 # in topographical order. Use the sort() predicate to explicitly sort
1012 # again instead.
992 # again instead.
1013 return False
993 return False
1014
994
1015 def _iterfilter(self, iterrange):
995 def _iterfilter(self, iterrange):
1016 s = self._hiddenrevs
996 s = self._hiddenrevs
1017 for r in iterrange:
997 for r in iterrange:
1018 if r not in s:
998 if r not in s:
1019 yield r
999 yield r
1020
1000
1021 def __iter__(self):
1001 def __iter__(self):
1022 if self._ascending:
1002 if self._ascending:
1023 return self.fastasc()
1003 return self.fastasc()
1024 else:
1004 else:
1025 return self.fastdesc()
1005 return self.fastdesc()
1026
1006
1027 def fastasc(self):
1007 def fastasc(self):
1028 iterrange = xrange(self._start, self._end)
1008 iterrange = xrange(self._start, self._end)
1029 if self._hiddenrevs:
1009 if self._hiddenrevs:
1030 return self._iterfilter(iterrange)
1010 return self._iterfilter(iterrange)
1031 return iter(iterrange)
1011 return iter(iterrange)
1032
1012
1033 def fastdesc(self):
1013 def fastdesc(self):
1034 iterrange = xrange(self._end - 1, self._start - 1, -1)
1014 iterrange = xrange(self._end - 1, self._start - 1, -1)
1035 if self._hiddenrevs:
1015 if self._hiddenrevs:
1036 return self._iterfilter(iterrange)
1016 return self._iterfilter(iterrange)
1037 return iter(iterrange)
1017 return iter(iterrange)
1038
1018
1039 def __contains__(self, rev):
1019 def __contains__(self, rev):
1040 hidden = self._hiddenrevs
1020 hidden = self._hiddenrevs
1041 return ((self._start <= rev < self._end)
1021 return ((self._start <= rev < self._end)
1042 and not (hidden and rev in hidden))
1022 and not (hidden and rev in hidden))
1043
1023
1044 def __nonzero__(self):
1024 def __nonzero__(self):
1045 for r in self:
1025 for r in self:
1046 return True
1026 return True
1047 return False
1027 return False
1048
1028
1049 __bool__ = __nonzero__
1029 __bool__ = __nonzero__
1050
1030
1051 def __len__(self):
1031 def __len__(self):
1052 if not self._hiddenrevs:
1032 if not self._hiddenrevs:
1053 return abs(self._end - self._start)
1033 return abs(self._end - self._start)
1054 else:
1034 else:
1055 count = 0
1035 count = 0
1056 start = self._start
1036 start = self._start
1057 end = self._end
1037 end = self._end
1058 for rev in self._hiddenrevs:
1038 for rev in self._hiddenrevs:
1059 if (end < rev <= start) or (start <= rev < end):
1039 if (end < rev <= start) or (start <= rev < end):
1060 count += 1
1040 count += 1
1061 return abs(self._end - self._start) - count
1041 return abs(self._end - self._start) - count
1062
1042
1063 def isascending(self):
1043 def isascending(self):
1064 return self._ascending
1044 return self._ascending
1065
1045
1066 def isdescending(self):
1046 def isdescending(self):
1067 return not self._ascending
1047 return not self._ascending
1068
1048
1069 def first(self):
1049 def first(self):
1070 if self._ascending:
1050 if self._ascending:
1071 it = self.fastasc
1051 it = self.fastasc
1072 else:
1052 else:
1073 it = self.fastdesc
1053 it = self.fastdesc
1074 for x in it():
1054 for x in it():
1075 return x
1055 return x
1076 return None
1056 return None
1077
1057
1078 def last(self):
1058 def last(self):
1079 if self._ascending:
1059 if self._ascending:
1080 it = self.fastdesc
1060 it = self.fastdesc
1081 else:
1061 else:
1082 it = self.fastasc
1062 it = self.fastasc
1083 for x in it():
1063 for x in it():
1084 return x
1064 return x
1085 return None
1065 return None
1086
1066
1087 def _slice(self, start, stop):
1067 def _slice(self, start, stop):
1088 if self._hiddenrevs:
1068 if self._hiddenrevs:
1089 # unoptimized since all hidden revisions in range has to be scanned
1069 # unoptimized since all hidden revisions in range has to be scanned
1090 return super(_spanset, self)._slice(start, stop)
1070 return super(_spanset, self)._slice(start, stop)
1091 if self._ascending:
1071 if self._ascending:
1092 x = min(self._start + start, self._end)
1072 x = min(self._start + start, self._end)
1093 y = min(self._start + stop, self._end)
1073 y = min(self._start + stop, self._end)
1094 else:
1074 else:
1095 x = max(self._end - stop, self._start)
1075 x = max(self._end - stop, self._start)
1096 y = max(self._end - start, self._start)
1076 y = max(self._end - start, self._start)
1097 return _spanset(x, y, self._ascending, self._hiddenrevs)
1077 return _spanset(x, y, self._ascending, self._hiddenrevs)
1098
1078
1099 @encoding.strmethod
1079 @encoding.strmethod
1100 def __repr__(self):
1080 def __repr__(self):
1101 d = {False: '-', True: '+'}[self._ascending]
1081 d = {False: '-', True: '+'}[self._ascending]
1102 return '<%s%s %d:%d>' % (_typename(self), d, self._start, self._end)
1082 return '<%s%s %d:%d>' % (_typename(self), d, self._start, self._end)
1103
1083
1104 class fullreposet(_spanset):
1084 class fullreposet(_spanset):
1105 """a set containing all revisions in the repo
1085 """a set containing all revisions in the repo
1106
1086
1107 This class exists to host special optimization and magic to handle virtual
1087 This class exists to host special optimization and magic to handle virtual
1108 revisions such as "null".
1088 revisions such as "null".
1109 """
1089 """
1110
1090
1111 def __init__(self, repo):
1091 def __init__(self, repo):
1112 super(fullreposet, self).__init__(0, len(repo), True,
1092 super(fullreposet, self).__init__(0, len(repo), True,
1113 repo.changelog.filteredrevs)
1093 repo.changelog.filteredrevs)
1114
1094
1115 def __and__(self, other):
1095 def __and__(self, other):
1116 """As self contains the whole repo, all of the other set should also be
1096 """As self contains the whole repo, all of the other set should also be
1117 in self. Therefore `self & other = other`.
1097 in self. Therefore `self & other = other`.
1118
1098
1119 This boldly assumes the other contains valid revs only.
1099 This boldly assumes the other contains valid revs only.
1120 """
1100 """
1121 # other not a smartset, make is so
1101 # other not a smartset, make is so
1122 if not util.safehasattr(other, 'isascending'):
1102 if not util.safehasattr(other, 'isascending'):
1123 # filter out hidden revision
1103 # filter out hidden revision
1124 # (this boldly assumes all smartset are pure)
1104 # (this boldly assumes all smartset are pure)
1125 #
1105 #
1126 # `other` was used with "&", let's assume this is a set like
1106 # `other` was used with "&", let's assume this is a set like
1127 # object.
1107 # object.
1128 other = baseset(other - self._hiddenrevs)
1108 other = baseset(other - self._hiddenrevs)
1129
1109
1130 other.sort(reverse=self.isdescending())
1110 other.sort(reverse=self.isdescending())
1131 return other
1111 return other
@@ -1,560 +1,583 b''
1 # stringutil.py - utility for generic string formatting, parsing, etc.
1 # stringutil.py - utility for generic string formatting, parsing, etc.
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import ast
12 import ast
13 import codecs
13 import codecs
14 import re as remod
14 import re as remod
15 import textwrap
15 import textwrap
16
16
17 from ..i18n import _
17 from ..i18n import _
18 from ..thirdparty import attr
18 from ..thirdparty import attr
19
19
20 from .. import (
20 from .. import (
21 encoding,
21 encoding,
22 error,
22 error,
23 pycompat,
23 pycompat,
24 )
24 )
25
25
26 # regex special chars pulled from https://bugs.python.org/issue29995
26 # regex special chars pulled from https://bugs.python.org/issue29995
27 # which was part of Python 3.7.
27 # which was part of Python 3.7.
28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
30
30
31 def reescape(pat):
31 def reescape(pat):
32 """Drop-in replacement for re.escape."""
32 """Drop-in replacement for re.escape."""
33 # NOTE: it is intentional that this works on unicodes and not
33 # NOTE: it is intentional that this works on unicodes and not
34 # bytes, as it's only possible to do the escaping with
34 # bytes, as it's only possible to do the escaping with
35 # unicode.translate, not bytes.translate. Sigh.
35 # unicode.translate, not bytes.translate. Sigh.
36 wantuni = True
36 wantuni = True
37 if isinstance(pat, bytes):
37 if isinstance(pat, bytes):
38 wantuni = False
38 wantuni = False
39 pat = pat.decode('latin1')
39 pat = pat.decode('latin1')
40 pat = pat.translate(_regexescapemap)
40 pat = pat.translate(_regexescapemap)
41 if wantuni:
41 if wantuni:
42 return pat
42 return pat
43 return pat.encode('latin1')
43 return pat.encode('latin1')
44
44
45 def pprint(o, bprefix=False):
45 def pprint(o, bprefix=False):
46 """Pretty print an object."""
46 """Pretty print an object."""
47 if isinstance(o, bytes):
47 if isinstance(o, bytes):
48 if bprefix:
48 if bprefix:
49 return "b'%s'" % escapestr(o)
49 return "b'%s'" % escapestr(o)
50 return "'%s'" % escapestr(o)
50 return "'%s'" % escapestr(o)
51 elif isinstance(o, bytearray):
51 elif isinstance(o, bytearray):
52 # codecs.escape_encode() can't handle bytearray, so escapestr fails
52 # codecs.escape_encode() can't handle bytearray, so escapestr fails
53 # without coercion.
53 # without coercion.
54 return "bytearray['%s']" % escapestr(bytes(o))
54 return "bytearray['%s']" % escapestr(bytes(o))
55 elif isinstance(o, list):
55 elif isinstance(o, list):
56 return '[%s]' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
56 return '[%s]' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
57 elif isinstance(o, dict):
57 elif isinstance(o, dict):
58 return '{%s}' % (b', '.join(
58 return '{%s}' % (b', '.join(
59 '%s: %s' % (pprint(k, bprefix=bprefix),
59 '%s: %s' % (pprint(k, bprefix=bprefix),
60 pprint(v, bprefix=bprefix))
60 pprint(v, bprefix=bprefix))
61 for k, v in sorted(o.items())))
61 for k, v in sorted(o.items())))
62 elif isinstance(o, tuple):
62 elif isinstance(o, tuple):
63 return '(%s)' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
63 return '(%s)' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
64 else:
64 else:
65 return pycompat.byterepr(o)
65 return pycompat.byterepr(o)
66
66
67 def prettyrepr(o):
67 def prettyrepr(o):
68 """Pretty print a representation of a possibly-nested object"""
68 """Pretty print a representation of a possibly-nested object"""
69 lines = []
69 lines = []
70 rs = pycompat.byterepr(o)
70 rs = pycompat.byterepr(o)
71 p0 = p1 = 0
71 p0 = p1 = 0
72 while p0 < len(rs):
72 while p0 < len(rs):
73 # '... field=<type ... field=<type ...'
73 # '... field=<type ... field=<type ...'
74 # ~~~~~~~~~~~~~~~~
74 # ~~~~~~~~~~~~~~~~
75 # p0 p1 q0 q1
75 # p0 p1 q0 q1
76 q0 = -1
76 q0 = -1
77 q1 = rs.find('<', p1 + 1)
77 q1 = rs.find('<', p1 + 1)
78 if q1 < 0:
78 if q1 < 0:
79 q1 = len(rs)
79 q1 = len(rs)
80 elif q1 > p1 + 1 and rs.startswith('=', q1 - 1):
80 elif q1 > p1 + 1 and rs.startswith('=', q1 - 1):
81 # backtrack for ' field=<'
81 # backtrack for ' field=<'
82 q0 = rs.rfind(' ', p1 + 1, q1 - 1)
82 q0 = rs.rfind(' ', p1 + 1, q1 - 1)
83 if q0 < 0:
83 if q0 < 0:
84 q0 = q1
84 q0 = q1
85 else:
85 else:
86 q0 += 1 # skip ' '
86 q0 += 1 # skip ' '
87 l = rs.count('<', 0, p0) - rs.count('>', 0, p0)
87 l = rs.count('<', 0, p0) - rs.count('>', 0, p0)
88 assert l >= 0
88 assert l >= 0
89 lines.append((l, rs[p0:q0].rstrip()))
89 lines.append((l, rs[p0:q0].rstrip()))
90 p0, p1 = q0, q1
90 p0, p1 = q0, q1
91 return '\n'.join(' ' * l + s for l, s in lines)
91 return '\n'.join(' ' * l + s for l, s in lines)
92
92
93 def buildrepr(r):
94 """Format an optional printable representation from unexpanded bits
95
96 ======== =================================
97 type(r) example
98 ======== =================================
99 tuple ('<not %r>', other)
100 bytes '<branch closed>'
101 callable lambda: '<branch %r>' % sorted(b)
102 object other
103 ======== =================================
104 """
105 if r is None:
106 return ''
107 elif isinstance(r, tuple):
108 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
109 elif isinstance(r, bytes):
110 return r
111 elif callable(r):
112 return r()
113 else:
114 return pycompat.byterepr(r)
115
93 def binary(s):
116 def binary(s):
94 """return true if a string is binary data"""
117 """return true if a string is binary data"""
95 return bool(s and '\0' in s)
118 return bool(s and '\0' in s)
96
119
97 def stringmatcher(pattern, casesensitive=True):
120 def stringmatcher(pattern, casesensitive=True):
98 """
121 """
99 accepts a string, possibly starting with 're:' or 'literal:' prefix.
122 accepts a string, possibly starting with 're:' or 'literal:' prefix.
100 returns the matcher name, pattern, and matcher function.
123 returns the matcher name, pattern, and matcher function.
101 missing or unknown prefixes are treated as literal matches.
124 missing or unknown prefixes are treated as literal matches.
102
125
103 helper for tests:
126 helper for tests:
104 >>> def test(pattern, *tests):
127 >>> def test(pattern, *tests):
105 ... kind, pattern, matcher = stringmatcher(pattern)
128 ... kind, pattern, matcher = stringmatcher(pattern)
106 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
129 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
107 >>> def itest(pattern, *tests):
130 >>> def itest(pattern, *tests):
108 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
131 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
109 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
132 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
110
133
111 exact matching (no prefix):
134 exact matching (no prefix):
112 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
135 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
113 ('literal', 'abcdefg', [False, False, True])
136 ('literal', 'abcdefg', [False, False, True])
114
137
115 regex matching ('re:' prefix)
138 regex matching ('re:' prefix)
116 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
139 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
117 ('re', 'a.+b', [False, False, True])
140 ('re', 'a.+b', [False, False, True])
118
141
119 force exact matches ('literal:' prefix)
142 force exact matches ('literal:' prefix)
120 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
143 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
121 ('literal', 're:foobar', [False, True])
144 ('literal', 're:foobar', [False, True])
122
145
123 unknown prefixes are ignored and treated as literals
146 unknown prefixes are ignored and treated as literals
124 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
147 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
125 ('literal', 'foo:bar', [False, False, True])
148 ('literal', 'foo:bar', [False, False, True])
126
149
127 case insensitive regex matches
150 case insensitive regex matches
128 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
151 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
129 ('re', 'A.+b', [False, False, True])
152 ('re', 'A.+b', [False, False, True])
130
153
131 case insensitive literal matches
154 case insensitive literal matches
132 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
155 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
133 ('literal', 'ABCDEFG', [False, False, True])
156 ('literal', 'ABCDEFG', [False, False, True])
134 """
157 """
135 if pattern.startswith('re:'):
158 if pattern.startswith('re:'):
136 pattern = pattern[3:]
159 pattern = pattern[3:]
137 try:
160 try:
138 flags = 0
161 flags = 0
139 if not casesensitive:
162 if not casesensitive:
140 flags = remod.I
163 flags = remod.I
141 regex = remod.compile(pattern, flags)
164 regex = remod.compile(pattern, flags)
142 except remod.error as e:
165 except remod.error as e:
143 raise error.ParseError(_('invalid regular expression: %s')
166 raise error.ParseError(_('invalid regular expression: %s')
144 % e)
167 % e)
145 return 're', pattern, regex.search
168 return 're', pattern, regex.search
146 elif pattern.startswith('literal:'):
169 elif pattern.startswith('literal:'):
147 pattern = pattern[8:]
170 pattern = pattern[8:]
148
171
149 match = pattern.__eq__
172 match = pattern.__eq__
150
173
151 if not casesensitive:
174 if not casesensitive:
152 ipat = encoding.lower(pattern)
175 ipat = encoding.lower(pattern)
153 match = lambda s: ipat == encoding.lower(s)
176 match = lambda s: ipat == encoding.lower(s)
154 return 'literal', pattern, match
177 return 'literal', pattern, match
155
178
156 def shortuser(user):
179 def shortuser(user):
157 """Return a short representation of a user name or email address."""
180 """Return a short representation of a user name or email address."""
158 f = user.find('@')
181 f = user.find('@')
159 if f >= 0:
182 if f >= 0:
160 user = user[:f]
183 user = user[:f]
161 f = user.find('<')
184 f = user.find('<')
162 if f >= 0:
185 if f >= 0:
163 user = user[f + 1:]
186 user = user[f + 1:]
164 f = user.find(' ')
187 f = user.find(' ')
165 if f >= 0:
188 if f >= 0:
166 user = user[:f]
189 user = user[:f]
167 f = user.find('.')
190 f = user.find('.')
168 if f >= 0:
191 if f >= 0:
169 user = user[:f]
192 user = user[:f]
170 return user
193 return user
171
194
172 def emailuser(user):
195 def emailuser(user):
173 """Return the user portion of an email address."""
196 """Return the user portion of an email address."""
174 f = user.find('@')
197 f = user.find('@')
175 if f >= 0:
198 if f >= 0:
176 user = user[:f]
199 user = user[:f]
177 f = user.find('<')
200 f = user.find('<')
178 if f >= 0:
201 if f >= 0:
179 user = user[f + 1:]
202 user = user[f + 1:]
180 return user
203 return user
181
204
182 def email(author):
205 def email(author):
183 '''get email of author.'''
206 '''get email of author.'''
184 r = author.find('>')
207 r = author.find('>')
185 if r == -1:
208 if r == -1:
186 r = None
209 r = None
187 return author[author.find('<') + 1:r]
210 return author[author.find('<') + 1:r]
188
211
189 def person(author):
212 def person(author):
190 """Returns the name before an email address,
213 """Returns the name before an email address,
191 interpreting it as per RFC 5322
214 interpreting it as per RFC 5322
192
215
193 >>> person(b'foo@bar')
216 >>> person(b'foo@bar')
194 'foo'
217 'foo'
195 >>> person(b'Foo Bar <foo@bar>')
218 >>> person(b'Foo Bar <foo@bar>')
196 'Foo Bar'
219 'Foo Bar'
197 >>> person(b'"Foo Bar" <foo@bar>')
220 >>> person(b'"Foo Bar" <foo@bar>')
198 'Foo Bar'
221 'Foo Bar'
199 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
222 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
200 'Foo "buz" Bar'
223 'Foo "buz" Bar'
201 >>> # The following are invalid, but do exist in real-life
224 >>> # The following are invalid, but do exist in real-life
202 ...
225 ...
203 >>> person(b'Foo "buz" Bar <foo@bar>')
226 >>> person(b'Foo "buz" Bar <foo@bar>')
204 'Foo "buz" Bar'
227 'Foo "buz" Bar'
205 >>> person(b'"Foo Bar <foo@bar>')
228 >>> person(b'"Foo Bar <foo@bar>')
206 'Foo Bar'
229 'Foo Bar'
207 """
230 """
208 if '@' not in author:
231 if '@' not in author:
209 return author
232 return author
210 f = author.find('<')
233 f = author.find('<')
211 if f != -1:
234 if f != -1:
212 return author[:f].strip(' "').replace('\\"', '"')
235 return author[:f].strip(' "').replace('\\"', '"')
213 f = author.find('@')
236 f = author.find('@')
214 return author[:f].replace('.', ' ')
237 return author[:f].replace('.', ' ')
215
238
216 @attr.s(hash=True)
239 @attr.s(hash=True)
217 class mailmapping(object):
240 class mailmapping(object):
218 '''Represents a username/email key or value in
241 '''Represents a username/email key or value in
219 a mailmap file'''
242 a mailmap file'''
220 email = attr.ib()
243 email = attr.ib()
221 name = attr.ib(default=None)
244 name = attr.ib(default=None)
222
245
223 def _ismailmaplineinvalid(names, emails):
246 def _ismailmaplineinvalid(names, emails):
224 '''Returns True if the parsed names and emails
247 '''Returns True if the parsed names and emails
225 in a mailmap entry are invalid.
248 in a mailmap entry are invalid.
226
249
227 >>> # No names or emails fails
250 >>> # No names or emails fails
228 >>> names, emails = [], []
251 >>> names, emails = [], []
229 >>> _ismailmaplineinvalid(names, emails)
252 >>> _ismailmaplineinvalid(names, emails)
230 True
253 True
231 >>> # Only one email fails
254 >>> # Only one email fails
232 >>> emails = [b'email@email.com']
255 >>> emails = [b'email@email.com']
233 >>> _ismailmaplineinvalid(names, emails)
256 >>> _ismailmaplineinvalid(names, emails)
234 True
257 True
235 >>> # One email and one name passes
258 >>> # One email and one name passes
236 >>> names = [b'Test Name']
259 >>> names = [b'Test Name']
237 >>> _ismailmaplineinvalid(names, emails)
260 >>> _ismailmaplineinvalid(names, emails)
238 False
261 False
239 >>> # No names but two emails passes
262 >>> # No names but two emails passes
240 >>> names = []
263 >>> names = []
241 >>> emails = [b'proper@email.com', b'commit@email.com']
264 >>> emails = [b'proper@email.com', b'commit@email.com']
242 >>> _ismailmaplineinvalid(names, emails)
265 >>> _ismailmaplineinvalid(names, emails)
243 False
266 False
244 '''
267 '''
245 return not emails or not names and len(emails) < 2
268 return not emails or not names and len(emails) < 2
246
269
247 def parsemailmap(mailmapcontent):
270 def parsemailmap(mailmapcontent):
248 """Parses data in the .mailmap format
271 """Parses data in the .mailmap format
249
272
250 >>> mmdata = b"\\n".join([
273 >>> mmdata = b"\\n".join([
251 ... b'# Comment',
274 ... b'# Comment',
252 ... b'Name <commit1@email.xx>',
275 ... b'Name <commit1@email.xx>',
253 ... b'<name@email.xx> <commit2@email.xx>',
276 ... b'<name@email.xx> <commit2@email.xx>',
254 ... b'Name <proper@email.xx> <commit3@email.xx>',
277 ... b'Name <proper@email.xx> <commit3@email.xx>',
255 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
278 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
256 ... ])
279 ... ])
257 >>> mm = parsemailmap(mmdata)
280 >>> mm = parsemailmap(mmdata)
258 >>> for key in sorted(mm.keys()):
281 >>> for key in sorted(mm.keys()):
259 ... print(key)
282 ... print(key)
260 mailmapping(email='commit1@email.xx', name=None)
283 mailmapping(email='commit1@email.xx', name=None)
261 mailmapping(email='commit2@email.xx', name=None)
284 mailmapping(email='commit2@email.xx', name=None)
262 mailmapping(email='commit3@email.xx', name=None)
285 mailmapping(email='commit3@email.xx', name=None)
263 mailmapping(email='commit4@email.xx', name='Commit')
286 mailmapping(email='commit4@email.xx', name='Commit')
264 >>> for val in sorted(mm.values()):
287 >>> for val in sorted(mm.values()):
265 ... print(val)
288 ... print(val)
266 mailmapping(email='commit1@email.xx', name='Name')
289 mailmapping(email='commit1@email.xx', name='Name')
267 mailmapping(email='name@email.xx', name=None)
290 mailmapping(email='name@email.xx', name=None)
268 mailmapping(email='proper@email.xx', name='Name')
291 mailmapping(email='proper@email.xx', name='Name')
269 mailmapping(email='proper@email.xx', name='Name')
292 mailmapping(email='proper@email.xx', name='Name')
270 """
293 """
271 mailmap = {}
294 mailmap = {}
272
295
273 if mailmapcontent is None:
296 if mailmapcontent is None:
274 return mailmap
297 return mailmap
275
298
276 for line in mailmapcontent.splitlines():
299 for line in mailmapcontent.splitlines():
277
300
278 # Don't bother checking the line if it is a comment or
301 # Don't bother checking the line if it is a comment or
279 # is an improperly formed author field
302 # is an improperly formed author field
280 if line.lstrip().startswith('#'):
303 if line.lstrip().startswith('#'):
281 continue
304 continue
282
305
283 # names, emails hold the parsed emails and names for each line
306 # names, emails hold the parsed emails and names for each line
284 # name_builder holds the words in a persons name
307 # name_builder holds the words in a persons name
285 names, emails = [], []
308 names, emails = [], []
286 namebuilder = []
309 namebuilder = []
287
310
288 for element in line.split():
311 for element in line.split():
289 if element.startswith('#'):
312 if element.startswith('#'):
290 # If we reach a comment in the mailmap file, move on
313 # If we reach a comment in the mailmap file, move on
291 break
314 break
292
315
293 elif element.startswith('<') and element.endswith('>'):
316 elif element.startswith('<') and element.endswith('>'):
294 # We have found an email.
317 # We have found an email.
295 # Parse it, and finalize any names from earlier
318 # Parse it, and finalize any names from earlier
296 emails.append(element[1:-1]) # Slice off the "<>"
319 emails.append(element[1:-1]) # Slice off the "<>"
297
320
298 if namebuilder:
321 if namebuilder:
299 names.append(' '.join(namebuilder))
322 names.append(' '.join(namebuilder))
300 namebuilder = []
323 namebuilder = []
301
324
302 # Break if we have found a second email, any other
325 # Break if we have found a second email, any other
303 # data does not fit the spec for .mailmap
326 # data does not fit the spec for .mailmap
304 if len(emails) > 1:
327 if len(emails) > 1:
305 break
328 break
306
329
307 else:
330 else:
308 # We have found another word in the committers name
331 # We have found another word in the committers name
309 namebuilder.append(element)
332 namebuilder.append(element)
310
333
311 # Check to see if we have parsed the line into a valid form
334 # Check to see if we have parsed the line into a valid form
312 # We require at least one email, and either at least one
335 # We require at least one email, and either at least one
313 # name or a second email
336 # name or a second email
314 if _ismailmaplineinvalid(names, emails):
337 if _ismailmaplineinvalid(names, emails):
315 continue
338 continue
316
339
317 mailmapkey = mailmapping(
340 mailmapkey = mailmapping(
318 email=emails[-1],
341 email=emails[-1],
319 name=names[-1] if len(names) == 2 else None,
342 name=names[-1] if len(names) == 2 else None,
320 )
343 )
321
344
322 mailmap[mailmapkey] = mailmapping(
345 mailmap[mailmapkey] = mailmapping(
323 email=emails[0],
346 email=emails[0],
324 name=names[0] if names else None,
347 name=names[0] if names else None,
325 )
348 )
326
349
327 return mailmap
350 return mailmap
328
351
329 def mapname(mailmap, author):
352 def mapname(mailmap, author):
330 """Returns the author field according to the mailmap cache, or
353 """Returns the author field according to the mailmap cache, or
331 the original author field.
354 the original author field.
332
355
333 >>> mmdata = b"\\n".join([
356 >>> mmdata = b"\\n".join([
334 ... b'# Comment',
357 ... b'# Comment',
335 ... b'Name <commit1@email.xx>',
358 ... b'Name <commit1@email.xx>',
336 ... b'<name@email.xx> <commit2@email.xx>',
359 ... b'<name@email.xx> <commit2@email.xx>',
337 ... b'Name <proper@email.xx> <commit3@email.xx>',
360 ... b'Name <proper@email.xx> <commit3@email.xx>',
338 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
361 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
339 ... ])
362 ... ])
340 >>> m = parsemailmap(mmdata)
363 >>> m = parsemailmap(mmdata)
341 >>> mapname(m, b'Commit <commit1@email.xx>')
364 >>> mapname(m, b'Commit <commit1@email.xx>')
342 'Name <commit1@email.xx>'
365 'Name <commit1@email.xx>'
343 >>> mapname(m, b'Name <commit2@email.xx>')
366 >>> mapname(m, b'Name <commit2@email.xx>')
344 'Name <name@email.xx>'
367 'Name <name@email.xx>'
345 >>> mapname(m, b'Commit <commit3@email.xx>')
368 >>> mapname(m, b'Commit <commit3@email.xx>')
346 'Name <proper@email.xx>'
369 'Name <proper@email.xx>'
347 >>> mapname(m, b'Commit <commit4@email.xx>')
370 >>> mapname(m, b'Commit <commit4@email.xx>')
348 'Name <proper@email.xx>'
371 'Name <proper@email.xx>'
349 >>> mapname(m, b'Unknown Name <unknown@email.com>')
372 >>> mapname(m, b'Unknown Name <unknown@email.com>')
350 'Unknown Name <unknown@email.com>'
373 'Unknown Name <unknown@email.com>'
351 """
374 """
352 # If the author field coming in isn't in the correct format,
375 # If the author field coming in isn't in the correct format,
353 # or the mailmap is empty just return the original author field
376 # or the mailmap is empty just return the original author field
354 if not isauthorwellformed(author) or not mailmap:
377 if not isauthorwellformed(author) or not mailmap:
355 return author
378 return author
356
379
357 # Turn the user name into a mailmapping
380 # Turn the user name into a mailmapping
358 commit = mailmapping(name=person(author), email=email(author))
381 commit = mailmapping(name=person(author), email=email(author))
359
382
360 try:
383 try:
361 # Try and use both the commit email and name as the key
384 # Try and use both the commit email and name as the key
362 proper = mailmap[commit]
385 proper = mailmap[commit]
363
386
364 except KeyError:
387 except KeyError:
365 # If the lookup fails, use just the email as the key instead
388 # If the lookup fails, use just the email as the key instead
366 # We call this commit2 as not to erase original commit fields
389 # We call this commit2 as not to erase original commit fields
367 commit2 = mailmapping(email=commit.email)
390 commit2 = mailmapping(email=commit.email)
368 proper = mailmap.get(commit2, mailmapping(None, None))
391 proper = mailmap.get(commit2, mailmapping(None, None))
369
392
370 # Return the author field with proper values filled in
393 # Return the author field with proper values filled in
371 return '%s <%s>' % (
394 return '%s <%s>' % (
372 proper.name if proper.name else commit.name,
395 proper.name if proper.name else commit.name,
373 proper.email if proper.email else commit.email,
396 proper.email if proper.email else commit.email,
374 )
397 )
375
398
376 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
399 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
377
400
378 def isauthorwellformed(author):
401 def isauthorwellformed(author):
379 '''Return True if the author field is well formed
402 '''Return True if the author field is well formed
380 (ie "Contributor Name <contrib@email.dom>")
403 (ie "Contributor Name <contrib@email.dom>")
381
404
382 >>> isauthorwellformed(b'Good Author <good@author.com>')
405 >>> isauthorwellformed(b'Good Author <good@author.com>')
383 True
406 True
384 >>> isauthorwellformed(b'Author <good@author.com>')
407 >>> isauthorwellformed(b'Author <good@author.com>')
385 True
408 True
386 >>> isauthorwellformed(b'Bad Author')
409 >>> isauthorwellformed(b'Bad Author')
387 False
410 False
388 >>> isauthorwellformed(b'Bad Author <author@author.com')
411 >>> isauthorwellformed(b'Bad Author <author@author.com')
389 False
412 False
390 >>> isauthorwellformed(b'Bad Author author@author.com')
413 >>> isauthorwellformed(b'Bad Author author@author.com')
391 False
414 False
392 >>> isauthorwellformed(b'<author@author.com>')
415 >>> isauthorwellformed(b'<author@author.com>')
393 False
416 False
394 >>> isauthorwellformed(b'Bad Author <author>')
417 >>> isauthorwellformed(b'Bad Author <author>')
395 False
418 False
396 '''
419 '''
397 return _correctauthorformat.match(author) is not None
420 return _correctauthorformat.match(author) is not None
398
421
399 def ellipsis(text, maxlength=400):
422 def ellipsis(text, maxlength=400):
400 """Trim string to at most maxlength (default: 400) columns in display."""
423 """Trim string to at most maxlength (default: 400) columns in display."""
401 return encoding.trim(text, maxlength, ellipsis='...')
424 return encoding.trim(text, maxlength, ellipsis='...')
402
425
403 def escapestr(s):
426 def escapestr(s):
404 # call underlying function of s.encode('string_escape') directly for
427 # call underlying function of s.encode('string_escape') directly for
405 # Python 3 compatibility
428 # Python 3 compatibility
406 return codecs.escape_encode(s)[0]
429 return codecs.escape_encode(s)[0]
407
430
408 def unescapestr(s):
431 def unescapestr(s):
409 return codecs.escape_decode(s)[0]
432 return codecs.escape_decode(s)[0]
410
433
411 def forcebytestr(obj):
434 def forcebytestr(obj):
412 """Portably format an arbitrary object (e.g. exception) into a byte
435 """Portably format an arbitrary object (e.g. exception) into a byte
413 string."""
436 string."""
414 try:
437 try:
415 return pycompat.bytestr(obj)
438 return pycompat.bytestr(obj)
416 except UnicodeEncodeError:
439 except UnicodeEncodeError:
417 # non-ascii string, may be lossy
440 # non-ascii string, may be lossy
418 return pycompat.bytestr(encoding.strtolocal(str(obj)))
441 return pycompat.bytestr(encoding.strtolocal(str(obj)))
419
442
420 def uirepr(s):
443 def uirepr(s):
421 # Avoid double backslash in Windows path repr()
444 # Avoid double backslash in Windows path repr()
422 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
445 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
423
446
424 # delay import of textwrap
447 # delay import of textwrap
425 def _MBTextWrapper(**kwargs):
448 def _MBTextWrapper(**kwargs):
426 class tw(textwrap.TextWrapper):
449 class tw(textwrap.TextWrapper):
427 """
450 """
428 Extend TextWrapper for width-awareness.
451 Extend TextWrapper for width-awareness.
429
452
430 Neither number of 'bytes' in any encoding nor 'characters' is
453 Neither number of 'bytes' in any encoding nor 'characters' is
431 appropriate to calculate terminal columns for specified string.
454 appropriate to calculate terminal columns for specified string.
432
455
433 Original TextWrapper implementation uses built-in 'len()' directly,
456 Original TextWrapper implementation uses built-in 'len()' directly,
434 so overriding is needed to use width information of each characters.
457 so overriding is needed to use width information of each characters.
435
458
436 In addition, characters classified into 'ambiguous' width are
459 In addition, characters classified into 'ambiguous' width are
437 treated as wide in East Asian area, but as narrow in other.
460 treated as wide in East Asian area, but as narrow in other.
438
461
439 This requires use decision to determine width of such characters.
462 This requires use decision to determine width of such characters.
440 """
463 """
441 def _cutdown(self, ucstr, space_left):
464 def _cutdown(self, ucstr, space_left):
442 l = 0
465 l = 0
443 colwidth = encoding.ucolwidth
466 colwidth = encoding.ucolwidth
444 for i in xrange(len(ucstr)):
467 for i in xrange(len(ucstr)):
445 l += colwidth(ucstr[i])
468 l += colwidth(ucstr[i])
446 if space_left < l:
469 if space_left < l:
447 return (ucstr[:i], ucstr[i:])
470 return (ucstr[:i], ucstr[i:])
448 return ucstr, ''
471 return ucstr, ''
449
472
450 # overriding of base class
473 # overriding of base class
451 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
474 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
452 space_left = max(width - cur_len, 1)
475 space_left = max(width - cur_len, 1)
453
476
454 if self.break_long_words:
477 if self.break_long_words:
455 cut, res = self._cutdown(reversed_chunks[-1], space_left)
478 cut, res = self._cutdown(reversed_chunks[-1], space_left)
456 cur_line.append(cut)
479 cur_line.append(cut)
457 reversed_chunks[-1] = res
480 reversed_chunks[-1] = res
458 elif not cur_line:
481 elif not cur_line:
459 cur_line.append(reversed_chunks.pop())
482 cur_line.append(reversed_chunks.pop())
460
483
461 # this overriding code is imported from TextWrapper of Python 2.6
484 # this overriding code is imported from TextWrapper of Python 2.6
462 # to calculate columns of string by 'encoding.ucolwidth()'
485 # to calculate columns of string by 'encoding.ucolwidth()'
463 def _wrap_chunks(self, chunks):
486 def _wrap_chunks(self, chunks):
464 colwidth = encoding.ucolwidth
487 colwidth = encoding.ucolwidth
465
488
466 lines = []
489 lines = []
467 if self.width <= 0:
490 if self.width <= 0:
468 raise ValueError("invalid width %r (must be > 0)" % self.width)
491 raise ValueError("invalid width %r (must be > 0)" % self.width)
469
492
470 # Arrange in reverse order so items can be efficiently popped
493 # Arrange in reverse order so items can be efficiently popped
471 # from a stack of chucks.
494 # from a stack of chucks.
472 chunks.reverse()
495 chunks.reverse()
473
496
474 while chunks:
497 while chunks:
475
498
476 # Start the list of chunks that will make up the current line.
499 # Start the list of chunks that will make up the current line.
477 # cur_len is just the length of all the chunks in cur_line.
500 # cur_len is just the length of all the chunks in cur_line.
478 cur_line = []
501 cur_line = []
479 cur_len = 0
502 cur_len = 0
480
503
481 # Figure out which static string will prefix this line.
504 # Figure out which static string will prefix this line.
482 if lines:
505 if lines:
483 indent = self.subsequent_indent
506 indent = self.subsequent_indent
484 else:
507 else:
485 indent = self.initial_indent
508 indent = self.initial_indent
486
509
487 # Maximum width for this line.
510 # Maximum width for this line.
488 width = self.width - len(indent)
511 width = self.width - len(indent)
489
512
490 # First chunk on line is whitespace -- drop it, unless this
513 # First chunk on line is whitespace -- drop it, unless this
491 # is the very beginning of the text (i.e. no lines started yet).
514 # is the very beginning of the text (i.e. no lines started yet).
492 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
515 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
493 del chunks[-1]
516 del chunks[-1]
494
517
495 while chunks:
518 while chunks:
496 l = colwidth(chunks[-1])
519 l = colwidth(chunks[-1])
497
520
498 # Can at least squeeze this chunk onto the current line.
521 # Can at least squeeze this chunk onto the current line.
499 if cur_len + l <= width:
522 if cur_len + l <= width:
500 cur_line.append(chunks.pop())
523 cur_line.append(chunks.pop())
501 cur_len += l
524 cur_len += l
502
525
503 # Nope, this line is full.
526 # Nope, this line is full.
504 else:
527 else:
505 break
528 break
506
529
507 # The current line is full, and the next chunk is too big to
530 # The current line is full, and the next chunk is too big to
508 # fit on *any* line (not just this one).
531 # fit on *any* line (not just this one).
509 if chunks and colwidth(chunks[-1]) > width:
532 if chunks and colwidth(chunks[-1]) > width:
510 self._handle_long_word(chunks, cur_line, cur_len, width)
533 self._handle_long_word(chunks, cur_line, cur_len, width)
511
534
512 # If the last chunk on this line is all whitespace, drop it.
535 # If the last chunk on this line is all whitespace, drop it.
513 if (self.drop_whitespace and
536 if (self.drop_whitespace and
514 cur_line and cur_line[-1].strip() == r''):
537 cur_line and cur_line[-1].strip() == r''):
515 del cur_line[-1]
538 del cur_line[-1]
516
539
517 # Convert current line back to a string and store it in list
540 # Convert current line back to a string and store it in list
518 # of all lines (return value).
541 # of all lines (return value).
519 if cur_line:
542 if cur_line:
520 lines.append(indent + r''.join(cur_line))
543 lines.append(indent + r''.join(cur_line))
521
544
522 return lines
545 return lines
523
546
524 global _MBTextWrapper
547 global _MBTextWrapper
525 _MBTextWrapper = tw
548 _MBTextWrapper = tw
526 return tw(**kwargs)
549 return tw(**kwargs)
527
550
528 def wrap(line, width, initindent='', hangindent=''):
551 def wrap(line, width, initindent='', hangindent=''):
529 maxindent = max(len(hangindent), len(initindent))
552 maxindent = max(len(hangindent), len(initindent))
530 if width <= maxindent:
553 if width <= maxindent:
531 # adjust for weird terminal size
554 # adjust for weird terminal size
532 width = max(78, maxindent + 1)
555 width = max(78, maxindent + 1)
533 line = line.decode(pycompat.sysstr(encoding.encoding),
556 line = line.decode(pycompat.sysstr(encoding.encoding),
534 pycompat.sysstr(encoding.encodingmode))
557 pycompat.sysstr(encoding.encodingmode))
535 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
558 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
536 pycompat.sysstr(encoding.encodingmode))
559 pycompat.sysstr(encoding.encodingmode))
537 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
560 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
538 pycompat.sysstr(encoding.encodingmode))
561 pycompat.sysstr(encoding.encodingmode))
539 wrapper = _MBTextWrapper(width=width,
562 wrapper = _MBTextWrapper(width=width,
540 initial_indent=initindent,
563 initial_indent=initindent,
541 subsequent_indent=hangindent)
564 subsequent_indent=hangindent)
542 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
565 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
543
566
544 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
567 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
545 '0': False, 'no': False, 'false': False, 'off': False,
568 '0': False, 'no': False, 'false': False, 'off': False,
546 'never': False}
569 'never': False}
547
570
548 def parsebool(s):
571 def parsebool(s):
549 """Parse s into a boolean.
572 """Parse s into a boolean.
550
573
551 If s is not a valid boolean, returns None.
574 If s is not a valid boolean, returns None.
552 """
575 """
553 return _booleans.get(s.lower(), None)
576 return _booleans.get(s.lower(), None)
554
577
555 def evalpythonliteral(s):
578 def evalpythonliteral(s):
556 """Evaluate a string containing a Python literal expression"""
579 """Evaluate a string containing a Python literal expression"""
557 # We could backport our tokenizer hack to rewrite '' to u'' if we want
580 # We could backport our tokenizer hack to rewrite '' to u'' if we want
558 if pycompat.ispy3:
581 if pycompat.ispy3:
559 return ast.literal_eval(s.decode('latin1'))
582 return ast.literal_eval(s.decode('latin1'))
560 return ast.literal_eval(s)
583 return ast.literal_eval(s)
General Comments 0
You need to be logged in to leave comments. Login now