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