##// END OF EJS Templates
py3: make sure regex is bytes in crecord.py...
Pulkit Goyal -
r36323:a8bca9d0 default
parent child Browse files
Show More
@@ -1,1773 +1,1773 b''
1 1 # stuff related specifically to patch manipulation / parsing
2 2 #
3 3 # Copyright 2008 Mark Edgington <edgimar@gmail.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 # This code is based on the Mark Edgington's crecord extension.
9 9 # (Itself based on Bryan O'Sullivan's record extension.)
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import locale
14 14 import os
15 15 import re
16 16 import signal
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 encoding,
21 21 error,
22 22 patch as patchmod,
23 23 scmutil,
24 24 util,
25 25 )
26 26 stringio = util.stringio
27 27
28 28 # This is required for ncurses to display non-ASCII characters in default user
29 29 # locale encoding correctly. --immerrr
30 30 locale.setlocale(locale.LC_ALL, u'')
31 31
32 32 # patch comments based on the git one
33 33 diffhelptext = _("""# To remove '-' lines, make them ' ' lines (context).
34 34 # To remove '+' lines, delete them.
35 35 # Lines starting with # will be removed from the patch.
36 36 """)
37 37
38 38 hunkhelptext = _("""#
39 39 # If the patch applies cleanly, the edited hunk will immediately be
40 40 # added to the record list. If it does not apply cleanly, a rejects file
41 41 # will be generated. You can use that when you try again. If all lines
42 42 # of the hunk are removed, then the edit is aborted and the hunk is left
43 43 # unchanged.
44 44 """)
45 45
46 46 patchhelptext = _("""#
47 47 # If the patch applies cleanly, the edited patch will immediately
48 48 # be finalised. If it does not apply cleanly, rejects files will be
49 49 # generated. You can use those when you try again.
50 50 """)
51 51
52 52 try:
53 53 import curses
54 54 curses.error
55 55 except ImportError:
56 56 # I have no idea if wcurses works with crecord...
57 57 try:
58 58 import wcurses as curses
59 59 curses.error
60 60 except ImportError:
61 61 # wcurses is not shipped on Windows by default, or python is not
62 62 # compiled with curses
63 63 curses = False
64 64
65 65 def checkcurses(ui):
66 66 """Return True if the user wants to use curses
67 67
68 68 This method returns True if curses is found (and that python is built with
69 69 it) and that the user has the correct flag for the ui.
70 70 """
71 71 return curses and ui.interface("chunkselector") == "curses"
72 72
73 73 class patchnode(object):
74 74 """abstract class for patch graph nodes
75 75 (i.e. patchroot, header, hunk, hunkline)
76 76 """
77 77
78 78 def firstchild(self):
79 79 raise NotImplementedError("method must be implemented by subclass")
80 80
81 81 def lastchild(self):
82 82 raise NotImplementedError("method must be implemented by subclass")
83 83
84 84 def allchildren(self):
85 85 "Return a list of all of the direct children of this node"
86 86 raise NotImplementedError("method must be implemented by subclass")
87 87
88 88 def nextsibling(self):
89 89 """
90 90 Return the closest next item of the same type where there are no items
91 91 of different types between the current item and this closest item.
92 92 If no such item exists, return None.
93 93 """
94 94 raise NotImplementedError("method must be implemented by subclass")
95 95
96 96 def prevsibling(self):
97 97 """
98 98 Return the closest previous item of the same type where there are no
99 99 items of different types between the current item and this closest item.
100 100 If no such item exists, return None.
101 101 """
102 102 raise NotImplementedError("method must be implemented by subclass")
103 103
104 104 def parentitem(self):
105 105 raise NotImplementedError("method must be implemented by subclass")
106 106
107 107 def nextitem(self, skipfolded=True):
108 108 """
109 109 Try to return the next item closest to this item, regardless of item's
110 110 type (header, hunk, or hunkline).
111 111
112 112 If skipfolded == True, and the current item is folded, then the child
113 113 items that are hidden due to folding will be skipped when determining
114 114 the next item.
115 115
116 116 If it is not possible to get the next item, return None.
117 117 """
118 118 try:
119 119 itemfolded = self.folded
120 120 except AttributeError:
121 121 itemfolded = False
122 122 if skipfolded and itemfolded:
123 123 nextitem = self.nextsibling()
124 124 if nextitem is None:
125 125 try:
126 126 nextitem = self.parentitem().nextsibling()
127 127 except AttributeError:
128 128 nextitem = None
129 129 return nextitem
130 130 else:
131 131 # try child
132 132 item = self.firstchild()
133 133 if item is not None:
134 134 return item
135 135
136 136 # else try next sibling
137 137 item = self.nextsibling()
138 138 if item is not None:
139 139 return item
140 140
141 141 try:
142 142 # else try parent's next sibling
143 143 item = self.parentitem().nextsibling()
144 144 if item is not None:
145 145 return item
146 146
147 147 # else return grandparent's next sibling (or None)
148 148 return self.parentitem().parentitem().nextsibling()
149 149
150 150 except AttributeError: # parent and/or grandparent was None
151 151 return None
152 152
153 153 def previtem(self):
154 154 """
155 155 Try to return the previous item closest to this item, regardless of
156 156 item's type (header, hunk, or hunkline).
157 157
158 158 If it is not possible to get the previous item, return None.
159 159 """
160 160 # try previous sibling's last child's last child,
161 161 # else try previous sibling's last child, else try previous sibling
162 162 prevsibling = self.prevsibling()
163 163 if prevsibling is not None:
164 164 prevsiblinglastchild = prevsibling.lastchild()
165 165 if ((prevsiblinglastchild is not None) and
166 166 not prevsibling.folded):
167 167 prevsiblinglclc = prevsiblinglastchild.lastchild()
168 168 if ((prevsiblinglclc is not None) and
169 169 not prevsiblinglastchild.folded):
170 170 return prevsiblinglclc
171 171 else:
172 172 return prevsiblinglastchild
173 173 else:
174 174 return prevsibling
175 175
176 176 # try parent (or None)
177 177 return self.parentitem()
178 178
179 179 class patch(patchnode, list): # todo: rename patchroot
180 180 """
181 181 list of header objects representing the patch.
182 182 """
183 183 def __init__(self, headerlist):
184 184 self.extend(headerlist)
185 185 # add parent patch object reference to each header
186 186 for header in self:
187 187 header.patch = self
188 188
189 189 class uiheader(patchnode):
190 190 """patch header
191 191
192 192 xxx shouldn't we move this to mercurial/patch.py ?
193 193 """
194 194
195 195 def __init__(self, header):
196 196 self.nonuiheader = header
197 197 # flag to indicate whether to apply this chunk
198 198 self.applied = True
199 199 # flag which only affects the status display indicating if a node's
200 200 # children are partially applied (i.e. some applied, some not).
201 201 self.partial = False
202 202
203 203 # flag to indicate whether to display as folded/unfolded to user
204 204 self.folded = True
205 205
206 206 # list of all headers in patch
207 207 self.patch = None
208 208
209 209 # flag is False if this header was ever unfolded from initial state
210 210 self.neverunfolded = True
211 211 self.hunks = [uihunk(h, self) for h in self.hunks]
212 212
213 213 def prettystr(self):
214 214 x = stringio()
215 215 self.pretty(x)
216 216 return x.getvalue()
217 217
218 218 def nextsibling(self):
219 219 numheadersinpatch = len(self.patch)
220 220 indexofthisheader = self.patch.index(self)
221 221
222 222 if indexofthisheader < numheadersinpatch - 1:
223 223 nextheader = self.patch[indexofthisheader + 1]
224 224 return nextheader
225 225 else:
226 226 return None
227 227
228 228 def prevsibling(self):
229 229 indexofthisheader = self.patch.index(self)
230 230 if indexofthisheader > 0:
231 231 previousheader = self.patch[indexofthisheader - 1]
232 232 return previousheader
233 233 else:
234 234 return None
235 235
236 236 def parentitem(self):
237 237 """
238 238 there is no 'real' parent item of a header that can be selected,
239 239 so return None.
240 240 """
241 241 return None
242 242
243 243 def firstchild(self):
244 244 "return the first child of this item, if one exists. otherwise None."
245 245 if len(self.hunks) > 0:
246 246 return self.hunks[0]
247 247 else:
248 248 return None
249 249
250 250 def lastchild(self):
251 251 "return the last child of this item, if one exists. otherwise None."
252 252 if len(self.hunks) > 0:
253 253 return self.hunks[-1]
254 254 else:
255 255 return None
256 256
257 257 def allchildren(self):
258 258 "return a list of all of the direct children of this node"
259 259 return self.hunks
260 260
261 261 def __getattr__(self, name):
262 262 return getattr(self.nonuiheader, name)
263 263
264 264 class uihunkline(patchnode):
265 265 "represents a changed line in a hunk"
266 266 def __init__(self, linetext, hunk):
267 267 self.linetext = linetext
268 268 self.applied = True
269 269 # the parent hunk to which this line belongs
270 270 self.hunk = hunk
271 271 # folding lines currently is not used/needed, but this flag is needed
272 272 # in the previtem method.
273 273 self.folded = False
274 274
275 275 def prettystr(self):
276 276 return self.linetext
277 277
278 278 def nextsibling(self):
279 279 numlinesinhunk = len(self.hunk.changedlines)
280 280 indexofthisline = self.hunk.changedlines.index(self)
281 281
282 282 if (indexofthisline < numlinesinhunk - 1):
283 283 nextline = self.hunk.changedlines[indexofthisline + 1]
284 284 return nextline
285 285 else:
286 286 return None
287 287
288 288 def prevsibling(self):
289 289 indexofthisline = self.hunk.changedlines.index(self)
290 290 if indexofthisline > 0:
291 291 previousline = self.hunk.changedlines[indexofthisline - 1]
292 292 return previousline
293 293 else:
294 294 return None
295 295
296 296 def parentitem(self):
297 297 "return the parent to the current item"
298 298 return self.hunk
299 299
300 300 def firstchild(self):
301 301 "return the first child of this item, if one exists. otherwise None."
302 302 # hunk-lines don't have children
303 303 return None
304 304
305 305 def lastchild(self):
306 306 "return the last child of this item, if one exists. otherwise None."
307 307 # hunk-lines don't have children
308 308 return None
309 309
310 310 class uihunk(patchnode):
311 311 """ui patch hunk, wraps a hunk and keep track of ui behavior """
312 312 maxcontext = 3
313 313
314 314 def __init__(self, hunk, header):
315 315 self._hunk = hunk
316 316 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
317 317 self.header = header
318 318 # used at end for detecting how many removed lines were un-applied
319 319 self.originalremoved = self.removed
320 320
321 321 # flag to indicate whether to display as folded/unfolded to user
322 322 self.folded = True
323 323 # flag to indicate whether to apply this chunk
324 324 self.applied = True
325 325 # flag which only affects the status display indicating if a node's
326 326 # children are partially applied (i.e. some applied, some not).
327 327 self.partial = False
328 328
329 329 def nextsibling(self):
330 330 numhunksinheader = len(self.header.hunks)
331 331 indexofthishunk = self.header.hunks.index(self)
332 332
333 333 if (indexofthishunk < numhunksinheader - 1):
334 334 nexthunk = self.header.hunks[indexofthishunk + 1]
335 335 return nexthunk
336 336 else:
337 337 return None
338 338
339 339 def prevsibling(self):
340 340 indexofthishunk = self.header.hunks.index(self)
341 341 if indexofthishunk > 0:
342 342 previoushunk = self.header.hunks[indexofthishunk - 1]
343 343 return previoushunk
344 344 else:
345 345 return None
346 346
347 347 def parentitem(self):
348 348 "return the parent to the current item"
349 349 return self.header
350 350
351 351 def firstchild(self):
352 352 "return the first child of this item, if one exists. otherwise None."
353 353 if len(self.changedlines) > 0:
354 354 return self.changedlines[0]
355 355 else:
356 356 return None
357 357
358 358 def lastchild(self):
359 359 "return the last child of this item, if one exists. otherwise None."
360 360 if len(self.changedlines) > 0:
361 361 return self.changedlines[-1]
362 362 else:
363 363 return None
364 364
365 365 def allchildren(self):
366 366 "return a list of all of the direct children of this node"
367 367 return self.changedlines
368 368
369 369 def countchanges(self):
370 370 """changedlines -> (n+,n-)"""
371 371 add = len([l for l in self.changedlines if l.applied
372 372 and l.prettystr()[0] == '+'])
373 373 rem = len([l for l in self.changedlines if l.applied
374 374 and l.prettystr()[0] == '-'])
375 375 return add, rem
376 376
377 377 def getfromtoline(self):
378 378 # calculate the number of removed lines converted to context lines
379 379 removedconvertedtocontext = self.originalremoved - self.removed
380 380
381 381 contextlen = (len(self.before) + len(self.after) +
382 382 removedconvertedtocontext)
383 383 if self.after and self.after[-1] == '\\ No newline at end of file\n':
384 384 contextlen -= 1
385 385 fromlen = contextlen + self.removed
386 386 tolen = contextlen + self.added
387 387
388 388 # diffutils manual, section "2.2.2.2 detailed description of unified
389 389 # format": "an empty hunk is considered to end at the line that
390 390 # precedes the hunk."
391 391 #
392 392 # so, if either of hunks is empty, decrease its line start. --immerrr
393 393 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
394 394 fromline, toline = self.fromline, self.toline
395 395 if fromline != 0:
396 396 if fromlen == 0:
397 397 fromline -= 1
398 398 if tolen == 0:
399 399 toline -= 1
400 400
401 401 fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
402 402 fromline, fromlen, toline, tolen,
403 403 self.proc and (' ' + self.proc))
404 404 return fromtoline
405 405
406 406 def write(self, fp):
407 407 # updated self.added/removed, which are used by getfromtoline()
408 408 self.added, self.removed = self.countchanges()
409 409 fp.write(self.getfromtoline())
410 410
411 411 hunklinelist = []
412 412 # add the following to the list: (1) all applied lines, and
413 413 # (2) all unapplied removal lines (convert these to context lines)
414 414 for changedline in self.changedlines:
415 415 changedlinestr = changedline.prettystr()
416 416 if changedline.applied:
417 417 hunklinelist.append(changedlinestr)
418 418 elif changedlinestr[0] == "-":
419 419 hunklinelist.append(" " + changedlinestr[1:])
420 420
421 421 fp.write(''.join(self.before + hunklinelist + self.after))
422 422
423 423 pretty = write
424 424
425 425 def prettystr(self):
426 426 x = stringio()
427 427 self.pretty(x)
428 428 return x.getvalue()
429 429
430 430 def reversehunk(self):
431 431 """return a recordhunk which is the reverse of the hunk
432 432
433 433 Assuming the displayed patch is diff(A, B) result. The returned hunk is
434 434 intended to be applied to B, instead of A.
435 435
436 436 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
437 437 the user made the following selection:
438 438
439 439 0
440 440 [x] -1 [x]: selected
441 441 [ ] -2 [ ]: not selected
442 442 [x] +3
443 443 [ ] +4
444 444 [x] +5
445 445 6
446 446
447 447 This function returns a hunk like:
448 448
449 449 0
450 450 -3
451 451 -4
452 452 -5
453 453 +1
454 454 +4
455 455 6
456 456
457 457 Note "4" was first deleted then added. That's because "4" exists in B
458 458 side and "-4" must exist between "-3" and "-5" to make the patch
459 459 applicable to B.
460 460 """
461 461 dels = []
462 462 adds = []
463 463 for line in self.changedlines:
464 464 text = line.linetext
465 465 if line.applied:
466 466 if text[0] == '+':
467 467 dels.append(text[1:])
468 468 elif text[0] == '-':
469 469 adds.append(text[1:])
470 470 elif text[0] == '+':
471 471 dels.append(text[1:])
472 472 adds.append(text[1:])
473 473 hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
474 474 h = self._hunk
475 475 return patchmod.recordhunk(h.header, h.toline, h.fromline, h.proc,
476 476 h.before, hunk, h.after)
477 477
478 478 def __getattr__(self, name):
479 479 return getattr(self._hunk, name)
480 480
481 481 def __repr__(self):
482 482 return '<hunk %r@%d>' % (self.filename(), self.fromline)
483 483
484 484 def filterpatch(ui, chunks, chunkselector, operation=None):
485 485 """interactively filter patch chunks into applied-only chunks"""
486 486 chunks = list(chunks)
487 487 # convert chunks list into structure suitable for displaying/modifying
488 488 # with curses. create a list of headers only.
489 489 headers = [c for c in chunks if isinstance(c, patchmod.header)]
490 490
491 491 # if there are no changed files
492 492 if len(headers) == 0:
493 493 return [], {}
494 494 uiheaders = [uiheader(h) for h in headers]
495 495 # let user choose headers/hunks/lines, and mark their applied flags
496 496 # accordingly
497 497 ret = chunkselector(ui, uiheaders, operation=operation)
498 498 appliedhunklist = []
499 499 for hdr in uiheaders:
500 500 if (hdr.applied and
501 501 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
502 502 appliedhunklist.append(hdr)
503 503 fixoffset = 0
504 504 for hnk in hdr.hunks:
505 505 if hnk.applied:
506 506 appliedhunklist.append(hnk)
507 507 # adjust the 'to'-line offset of the hunk to be correct
508 508 # after de-activating some of the other hunks for this file
509 509 if fixoffset:
510 510 #hnk = copy.copy(hnk) # necessary??
511 511 hnk.toline += fixoffset
512 512 else:
513 513 fixoffset += hnk.removed - hnk.added
514 514
515 515 return (appliedhunklist, ret)
516 516
517 517 def chunkselector(ui, headerlist, operation=None):
518 518 """
519 519 curses interface to get selection of chunks, and mark the applied flags
520 520 of the chosen chunks.
521 521 """
522 522 ui.write(_('starting interactive selection\n'))
523 523 chunkselector = curseschunkselector(headerlist, ui, operation)
524 524 origsigtstp = sentinel = object()
525 525 if util.safehasattr(signal, 'SIGTSTP'):
526 526 origsigtstp = signal.getsignal(signal.SIGTSTP)
527 527 try:
528 528 curses.wrapper(chunkselector.main)
529 529 if chunkselector.initerr is not None:
530 530 raise error.Abort(chunkselector.initerr)
531 531 # ncurses does not restore signal handler for SIGTSTP
532 532 finally:
533 533 if origsigtstp is not sentinel:
534 534 signal.signal(signal.SIGTSTP, origsigtstp)
535 535 return chunkselector.opts
536 536
537 537 def testdecorator(testfn, f):
538 538 def u(*args, **kwargs):
539 539 return f(testfn, *args, **kwargs)
540 540 return u
541 541
542 542 def testchunkselector(testfn, ui, headerlist, operation=None):
543 543 """
544 544 test interface to get selection of chunks, and mark the applied flags
545 545 of the chosen chunks.
546 546 """
547 547 chunkselector = curseschunkselector(headerlist, ui, operation)
548 548 if testfn and os.path.exists(testfn):
549 549 testf = open(testfn)
550 550 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
551 551 testf.close()
552 552 while True:
553 553 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
554 554 break
555 555 return chunkselector.opts
556 556
557 557 _headermessages = { # {operation: text}
558 558 'apply': _('Select hunks to apply'),
559 559 'discard': _('Select hunks to discard'),
560 560 None: _('Select hunks to record'),
561 561 }
562 562
563 563 class curseschunkselector(object):
564 564 def __init__(self, headerlist, ui, operation=None):
565 565 # put the headers into a patch object
566 566 self.headerlist = patch(headerlist)
567 567
568 568 self.ui = ui
569 569 self.opts = {}
570 570
571 571 self.errorstr = None
572 572 # list of all chunks
573 573 self.chunklist = []
574 574 for h in headerlist:
575 575 self.chunklist.append(h)
576 576 self.chunklist.extend(h.hunks)
577 577
578 578 # dictionary mapping (fgcolor, bgcolor) pairs to the
579 579 # corresponding curses color-pair value.
580 580 self.colorpairs = {}
581 581 # maps custom nicknames of color-pairs to curses color-pair values
582 582 self.colorpairnames = {}
583 583
584 584 # Honor color setting of ui section. Keep colored setup as
585 585 # long as not explicitly set to a falsy value - especially,
586 586 # when not set at all. This is to stay most compatible with
587 587 # previous (color only) behaviour.
588 588 uicolor = util.parsebool(self.ui.config('ui', 'color'))
589 589 self.usecolor = uicolor is not False
590 590
591 591 # the currently selected header, hunk, or hunk-line
592 592 self.currentselecteditem = self.headerlist[0]
593 593
594 594 # updated when printing out patch-display -- the 'lines' here are the
595 595 # line positions *in the pad*, not on the screen.
596 596 self.selecteditemstartline = 0
597 597 self.selecteditemendline = None
598 598
599 599 # define indentation levels
600 600 self.headerindentnumchars = 0
601 601 self.hunkindentnumchars = 3
602 602 self.hunklineindentnumchars = 6
603 603
604 604 # the first line of the pad to print to the screen
605 605 self.firstlineofpadtoprint = 0
606 606
607 607 # keeps track of the number of lines in the pad
608 608 self.numpadlines = None
609 609
610 610 self.numstatuslines = 1
611 611
612 612 # keep a running count of the number of lines printed to the pad
613 613 # (used for determining when the selected item begins/ends)
614 614 self.linesprintedtopadsofar = 0
615 615
616 616 # the first line of the pad which is visible on the screen
617 617 self.firstlineofpadtoprint = 0
618 618
619 619 # stores optional text for a commit comment provided by the user
620 620 self.commenttext = ""
621 621
622 622 # if the last 'toggle all' command caused all changes to be applied
623 623 self.waslasttoggleallapplied = True
624 624
625 625 # affects some ui text
626 626 if operation not in _headermessages:
627 627 raise error.ProgrammingError('unexpected operation: %s' % operation)
628 628 self.operation = operation
629 629
630 630 def uparrowevent(self):
631 631 """
632 632 try to select the previous item to the current item that has the
633 633 most-indented level. for example, if a hunk is selected, try to select
634 634 the last hunkline of the hunk prior to the selected hunk. or, if
635 635 the first hunkline of a hunk is currently selected, then select the
636 636 hunk itself.
637 637 """
638 638 currentitem = self.currentselecteditem
639 639
640 640 nextitem = currentitem.previtem()
641 641
642 642 if nextitem is None:
643 643 # if no parent item (i.e. currentitem is the first header), then
644 644 # no change...
645 645 nextitem = currentitem
646 646
647 647 self.currentselecteditem = nextitem
648 648
649 649 def uparrowshiftevent(self):
650 650 """
651 651 select (if possible) the previous item on the same level as the
652 652 currently selected item. otherwise, select (if possible) the
653 653 parent-item of the currently selected item.
654 654 """
655 655 currentitem = self.currentselecteditem
656 656 nextitem = currentitem.prevsibling()
657 657 # if there's no previous sibling, try choosing the parent
658 658 if nextitem is None:
659 659 nextitem = currentitem.parentitem()
660 660 if nextitem is None:
661 661 # if no parent item (i.e. currentitem is the first header), then
662 662 # no change...
663 663 nextitem = currentitem
664 664
665 665 self.currentselecteditem = nextitem
666 666
667 667 def downarrowevent(self):
668 668 """
669 669 try to select the next item to the current item that has the
670 670 most-indented level. for example, if a hunk is selected, select
671 671 the first hunkline of the selected hunk. or, if the last hunkline of
672 672 a hunk is currently selected, then select the next hunk, if one exists,
673 673 or if not, the next header if one exists.
674 674 """
675 675 #self.startprintline += 1 #debug
676 676 currentitem = self.currentselecteditem
677 677
678 678 nextitem = currentitem.nextitem()
679 679 # if there's no next item, keep the selection as-is
680 680 if nextitem is None:
681 681 nextitem = currentitem
682 682
683 683 self.currentselecteditem = nextitem
684 684
685 685 def downarrowshiftevent(self):
686 686 """
687 687 select (if possible) the next item on the same level as the currently
688 688 selected item. otherwise, select (if possible) the next item on the
689 689 same level as the parent item of the currently selected item.
690 690 """
691 691 currentitem = self.currentselecteditem
692 692 nextitem = currentitem.nextsibling()
693 693 # if there's no next sibling, try choosing the parent's nextsibling
694 694 if nextitem is None:
695 695 try:
696 696 nextitem = currentitem.parentitem().nextsibling()
697 697 except AttributeError:
698 698 # parentitem returned None, so nextsibling() can't be called
699 699 nextitem = None
700 700 if nextitem is None:
701 701 # if parent has no next sibling, then no change...
702 702 nextitem = currentitem
703 703
704 704 self.currentselecteditem = nextitem
705 705
706 706 def rightarrowevent(self):
707 707 """
708 708 select (if possible) the first of this item's child-items.
709 709 """
710 710 currentitem = self.currentselecteditem
711 711 nextitem = currentitem.firstchild()
712 712
713 713 # turn off folding if we want to show a child-item
714 714 if currentitem.folded:
715 715 self.togglefolded(currentitem)
716 716
717 717 if nextitem is None:
718 718 # if no next item on parent-level, then no change...
719 719 nextitem = currentitem
720 720
721 721 self.currentselecteditem = nextitem
722 722
723 723 def leftarrowevent(self):
724 724 """
725 725 if the current item can be folded (i.e. it is an unfolded header or
726 726 hunk), then fold it. otherwise try select (if possible) the parent
727 727 of this item.
728 728 """
729 729 currentitem = self.currentselecteditem
730 730
731 731 # try to fold the item
732 732 if not isinstance(currentitem, uihunkline):
733 733 if not currentitem.folded:
734 734 self.togglefolded(item=currentitem)
735 735 return
736 736
737 737 # if it can't be folded, try to select the parent item
738 738 nextitem = currentitem.parentitem()
739 739
740 740 if nextitem is None:
741 741 # if no item on parent-level, then no change...
742 742 nextitem = currentitem
743 743 if not nextitem.folded:
744 744 self.togglefolded(item=nextitem)
745 745
746 746 self.currentselecteditem = nextitem
747 747
748 748 def leftarrowshiftevent(self):
749 749 """
750 750 select the header of the current item (or fold current item if the
751 751 current item is already a header).
752 752 """
753 753 currentitem = self.currentselecteditem
754 754
755 755 if isinstance(currentitem, uiheader):
756 756 if not currentitem.folded:
757 757 self.togglefolded(item=currentitem)
758 758 return
759 759
760 760 # select the parent item recursively until we're at a header
761 761 while True:
762 762 nextitem = currentitem.parentitem()
763 763 if nextitem is None:
764 764 break
765 765 else:
766 766 currentitem = nextitem
767 767
768 768 self.currentselecteditem = currentitem
769 769
770 770 def updatescroll(self):
771 771 "scroll the screen to fully show the currently-selected"
772 772 selstart = self.selecteditemstartline
773 773 selend = self.selecteditemendline
774 774
775 775 padstart = self.firstlineofpadtoprint
776 776 padend = padstart + self.yscreensize - self.numstatuslines - 1
777 777 # 'buffered' pad start/end values which scroll with a certain
778 778 # top/bottom context margin
779 779 padstartbuffered = padstart + 3
780 780 padendbuffered = padend - 3
781 781
782 782 if selend > padendbuffered:
783 783 self.scrolllines(selend - padendbuffered)
784 784 elif selstart < padstartbuffered:
785 785 # negative values scroll in pgup direction
786 786 self.scrolllines(selstart - padstartbuffered)
787 787
788 788 def scrolllines(self, numlines):
789 789 "scroll the screen up (down) by numlines when numlines >0 (<0)."
790 790 self.firstlineofpadtoprint += numlines
791 791 if self.firstlineofpadtoprint < 0:
792 792 self.firstlineofpadtoprint = 0
793 793 if self.firstlineofpadtoprint > self.numpadlines - 1:
794 794 self.firstlineofpadtoprint = self.numpadlines - 1
795 795
796 796 def toggleapply(self, item=None):
797 797 """
798 798 toggle the applied flag of the specified item. if no item is specified,
799 799 toggle the flag of the currently selected item.
800 800 """
801 801 if item is None:
802 802 item = self.currentselecteditem
803 803
804 804 item.applied = not item.applied
805 805
806 806 if isinstance(item, uiheader):
807 807 item.partial = False
808 808 if item.applied:
809 809 # apply all its hunks
810 810 for hnk in item.hunks:
811 811 hnk.applied = True
812 812 # apply all their hunklines
813 813 for hunkline in hnk.changedlines:
814 814 hunkline.applied = True
815 815 else:
816 816 # un-apply all its hunks
817 817 for hnk in item.hunks:
818 818 hnk.applied = False
819 819 hnk.partial = False
820 820 # un-apply all their hunklines
821 821 for hunkline in hnk.changedlines:
822 822 hunkline.applied = False
823 823 elif isinstance(item, uihunk):
824 824 item.partial = False
825 825 # apply all it's hunklines
826 826 for hunkline in item.changedlines:
827 827 hunkline.applied = item.applied
828 828
829 829 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
830 830 allsiblingsapplied = not (False in siblingappliedstatus)
831 831 nosiblingsapplied = not (True in siblingappliedstatus)
832 832
833 833 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
834 834 somesiblingspartial = (True in siblingspartialstatus)
835 835
836 836 #cases where applied or partial should be removed from header
837 837
838 838 # if no 'sibling' hunks are applied (including this hunk)
839 839 if nosiblingsapplied:
840 840 if not item.header.special():
841 841 item.header.applied = False
842 842 item.header.partial = False
843 843 else: # some/all parent siblings are applied
844 844 item.header.applied = True
845 845 item.header.partial = (somesiblingspartial or
846 846 not allsiblingsapplied)
847 847
848 848 elif isinstance(item, uihunkline):
849 849 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
850 850 allsiblingsapplied = not (False in siblingappliedstatus)
851 851 nosiblingsapplied = not (True in siblingappliedstatus)
852 852
853 853 # if no 'sibling' lines are applied
854 854 if nosiblingsapplied:
855 855 item.hunk.applied = False
856 856 item.hunk.partial = False
857 857 elif allsiblingsapplied:
858 858 item.hunk.applied = True
859 859 item.hunk.partial = False
860 860 else: # some siblings applied
861 861 item.hunk.applied = True
862 862 item.hunk.partial = True
863 863
864 864 parentsiblingsapplied = [hnk.applied for hnk
865 865 in item.hunk.header.hunks]
866 866 noparentsiblingsapplied = not (True in parentsiblingsapplied)
867 867 allparentsiblingsapplied = not (False in parentsiblingsapplied)
868 868
869 869 parentsiblingspartial = [hnk.partial for hnk
870 870 in item.hunk.header.hunks]
871 871 someparentsiblingspartial = (True in parentsiblingspartial)
872 872
873 873 # if all parent hunks are not applied, un-apply header
874 874 if noparentsiblingsapplied:
875 875 if not item.hunk.header.special():
876 876 item.hunk.header.applied = False
877 877 item.hunk.header.partial = False
878 878 # set the applied and partial status of the header if needed
879 879 else: # some/all parent siblings are applied
880 880 item.hunk.header.applied = True
881 881 item.hunk.header.partial = (someparentsiblingspartial or
882 882 not allparentsiblingsapplied)
883 883
884 884 def toggleall(self):
885 885 "toggle the applied flag of all items."
886 886 if self.waslasttoggleallapplied: # then unapply them this time
887 887 for item in self.headerlist:
888 888 if item.applied:
889 889 self.toggleapply(item)
890 890 else:
891 891 for item in self.headerlist:
892 892 if not item.applied:
893 893 self.toggleapply(item)
894 894 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
895 895
896 896 def togglefolded(self, item=None, foldparent=False):
897 897 "toggle folded flag of specified item (defaults to currently selected)"
898 898 if item is None:
899 899 item = self.currentselecteditem
900 900 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
901 901 if not isinstance(item, uiheader):
902 902 # we need to select the parent item in this case
903 903 self.currentselecteditem = item = item.parentitem()
904 904 elif item.neverunfolded:
905 905 item.neverunfolded = False
906 906
907 907 # also fold any foldable children of the parent/current item
908 908 if isinstance(item, uiheader): # the original or 'new' item
909 909 for child in item.allchildren():
910 910 child.folded = not item.folded
911 911
912 912 if isinstance(item, (uiheader, uihunk)):
913 913 item.folded = not item.folded
914 914
915 915 def alignstring(self, instr, window):
916 916 """
917 917 add whitespace to the end of a string in order to make it fill
918 918 the screen in the x direction. the current cursor position is
919 919 taken into account when making this calculation. the string can span
920 920 multiple lines.
921 921 """
922 922 y, xstart = window.getyx()
923 923 width = self.xscreensize
924 924 # turn tabs into spaces
925 925 instr = instr.expandtabs(4)
926 926 strwidth = encoding.colwidth(instr)
927 927 numspaces = (width - ((strwidth + xstart) % width) - 1)
928 928 return instr + " " * numspaces + "\n"
929 929
930 930 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
931 931 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
932 932 """
933 933 print the string, text, with the specified colors and attributes, to
934 934 the specified curses window object.
935 935
936 936 the foreground and background colors are of the form
937 937 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
938 938 magenta, red, white, yellow]. if pairname is provided, a color
939 939 pair will be looked up in the self.colorpairnames dictionary.
940 940
941 941 attrlist is a list containing text attributes in the form of
942 942 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
943 943 underline].
944 944
945 945 if align == True, whitespace is added to the printed string such that
946 946 the string stretches to the right border of the window.
947 947
948 948 if showwhtspc == True, trailing whitespace of a string is highlighted.
949 949 """
950 950 # preprocess the text, converting tabs to spaces
951 951 text = text.expandtabs(4)
952 952 # strip \n, and convert control characters to ^[char] representation
953 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
953 text = re.sub(br'[\x00-\x08\x0a-\x1f]',
954 954 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
955 955
956 956 if pair is not None:
957 957 colorpair = pair
958 958 elif pairname is not None:
959 959 colorpair = self.colorpairnames[pairname]
960 960 else:
961 961 if fgcolor is None:
962 962 fgcolor = -1
963 963 if bgcolor is None:
964 964 bgcolor = -1
965 965 if (fgcolor, bgcolor) in self.colorpairs:
966 966 colorpair = self.colorpairs[(fgcolor, bgcolor)]
967 967 else:
968 968 colorpair = self.getcolorpair(fgcolor, bgcolor)
969 969 # add attributes if possible
970 970 if attrlist is None:
971 971 attrlist = []
972 972 if colorpair < 256:
973 973 # then it is safe to apply all attributes
974 974 for textattr in attrlist:
975 975 colorpair |= textattr
976 976 else:
977 977 # just apply a select few (safe?) attributes
978 978 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
979 979 if textattr in attrlist:
980 980 colorpair |= textattr
981 981
982 982 y, xstart = self.chunkpad.getyx()
983 983 t = "" # variable for counting lines printed
984 984 # if requested, show trailing whitespace
985 985 if showwhtspc:
986 986 origlen = len(text)
987 987 text = text.rstrip(' \n') # tabs have already been expanded
988 988 strippedlen = len(text)
989 989 numtrailingspaces = origlen - strippedlen
990 990
991 991 if towin:
992 992 window.addstr(text, colorpair)
993 993 t += text
994 994
995 995 if showwhtspc:
996 996 wscolorpair = colorpair | curses.A_REVERSE
997 997 if towin:
998 998 for i in range(numtrailingspaces):
999 999 window.addch(curses.ACS_CKBOARD, wscolorpair)
1000 1000 t += " " * numtrailingspaces
1001 1001
1002 1002 if align:
1003 1003 if towin:
1004 1004 extrawhitespace = self.alignstring("", window)
1005 1005 window.addstr(extrawhitespace, colorpair)
1006 1006 else:
1007 1007 # need to use t, since the x position hasn't incremented
1008 1008 extrawhitespace = self.alignstring(t, window)
1009 1009 t += extrawhitespace
1010 1010
1011 1011 # is reset to 0 at the beginning of printitem()
1012 1012
1013 1013 linesprinted = (xstart + len(t)) / self.xscreensize
1014 1014 self.linesprintedtopadsofar += linesprinted
1015 1015 return t
1016 1016
1017 1017 def _getstatuslinesegments(self):
1018 1018 """-> [str]. return segments"""
1019 1019 selected = self.currentselecteditem.applied
1020 1020 spaceselect = _('space: select')
1021 1021 spacedeselect = _('space: deselect')
1022 1022 # Format the selected label into a place as long as the longer of the
1023 1023 # two possible labels. This may vary by language.
1024 1024 spacelen = max(len(spaceselect), len(spacedeselect))
1025 1025 selectedlabel = '%-*s' % (spacelen,
1026 1026 spacedeselect if selected else spaceselect)
1027 1027 segments = [
1028 1028 _headermessages[self.operation],
1029 1029 '-',
1030 1030 _('[x]=selected **=collapsed'),
1031 1031 _('c: confirm'),
1032 1032 _('q: abort'),
1033 1033 _('arrow keys: move/expand/collapse'),
1034 1034 selectedlabel,
1035 1035 _('?: help'),
1036 1036 ]
1037 1037 return segments
1038 1038
1039 1039 def _getstatuslines(self):
1040 1040 """() -> [str]. return short help used in the top status window"""
1041 1041 if self.errorstr is not None:
1042 1042 lines = [self.errorstr, _('Press any key to continue')]
1043 1043 else:
1044 1044 # wrap segments to lines
1045 1045 segments = self._getstatuslinesegments()
1046 1046 width = self.xscreensize
1047 1047 lines = []
1048 1048 lastwidth = width
1049 1049 for s in segments:
1050 1050 w = encoding.colwidth(s)
1051 1051 sep = ' ' * (1 + (s and s[0] not in '-['))
1052 1052 if lastwidth + w + len(sep) >= width:
1053 1053 lines.append(s)
1054 1054 lastwidth = w
1055 1055 else:
1056 1056 lines[-1] += sep + s
1057 1057 lastwidth += w + len(sep)
1058 1058 if len(lines) != self.numstatuslines:
1059 1059 self.numstatuslines = len(lines)
1060 1060 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1061 1061 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
1062 1062
1063 1063 def updatescreen(self):
1064 1064 self.statuswin.erase()
1065 1065 self.chunkpad.erase()
1066 1066
1067 1067 printstring = self.printstring
1068 1068
1069 1069 # print out the status lines at the top
1070 1070 try:
1071 1071 for line in self._getstatuslines():
1072 1072 printstring(self.statuswin, line, pairname="legend")
1073 1073 self.statuswin.refresh()
1074 1074 except curses.error:
1075 1075 pass
1076 1076 if self.errorstr is not None:
1077 1077 return
1078 1078
1079 1079 # print out the patch in the remaining part of the window
1080 1080 try:
1081 1081 self.printitem()
1082 1082 self.updatescroll()
1083 1083 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1084 1084 self.numstatuslines, 0,
1085 1085 self.yscreensize - self.numstatuslines,
1086 1086 self.xscreensize)
1087 1087 except curses.error:
1088 1088 pass
1089 1089
1090 1090 def getstatusprefixstring(self, item):
1091 1091 """
1092 1092 create a string to prefix a line with which indicates whether 'item'
1093 1093 is applied and/or folded.
1094 1094 """
1095 1095
1096 1096 # create checkbox string
1097 1097 if item.applied:
1098 1098 if not isinstance(item, uihunkline) and item.partial:
1099 1099 checkbox = "[~]"
1100 1100 else:
1101 1101 checkbox = "[x]"
1102 1102 else:
1103 1103 checkbox = "[ ]"
1104 1104
1105 1105 try:
1106 1106 if item.folded:
1107 1107 checkbox += "**"
1108 1108 if isinstance(item, uiheader):
1109 1109 # one of "m", "a", or "d" (modified, added, deleted)
1110 1110 filestatus = item.changetype
1111 1111
1112 1112 checkbox += filestatus + " "
1113 1113 else:
1114 1114 checkbox += " "
1115 1115 if isinstance(item, uiheader):
1116 1116 # add two more spaces for headers
1117 1117 checkbox += " "
1118 1118 except AttributeError: # not foldable
1119 1119 checkbox += " "
1120 1120
1121 1121 return checkbox
1122 1122
1123 1123 def printheader(self, header, selected=False, towin=True,
1124 1124 ignorefolding=False):
1125 1125 """
1126 1126 print the header to the pad. if countlines is True, don't print
1127 1127 anything, but just count the number of lines which would be printed.
1128 1128 """
1129 1129
1130 1130 outstr = ""
1131 1131 text = header.prettystr()
1132 1132 chunkindex = self.chunklist.index(header)
1133 1133
1134 1134 if chunkindex != 0 and not header.folded:
1135 1135 # add separating line before headers
1136 1136 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1137 1137 towin=towin, align=False)
1138 1138 # select color-pair based on if the header is selected
1139 1139 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1140 1140 attrlist=[curses.A_BOLD])
1141 1141
1142 1142 # print out each line of the chunk, expanding it to screen width
1143 1143
1144 1144 # number of characters to indent lines on this level by
1145 1145 indentnumchars = 0
1146 1146 checkbox = self.getstatusprefixstring(header)
1147 1147 if not header.folded or ignorefolding:
1148 1148 textlist = text.split("\n")
1149 1149 linestr = checkbox + textlist[0]
1150 1150 else:
1151 1151 linestr = checkbox + header.filename()
1152 1152 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1153 1153 towin=towin)
1154 1154 if not header.folded or ignorefolding:
1155 1155 if len(textlist) > 1:
1156 1156 for line in textlist[1:]:
1157 1157 linestr = " "*(indentnumchars + len(checkbox)) + line
1158 1158 outstr += self.printstring(self.chunkpad, linestr,
1159 1159 pair=colorpair, towin=towin)
1160 1160
1161 1161 return outstr
1162 1162
1163 1163 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1164 1164 ignorefolding=False):
1165 1165 "includes start/end line indicator"
1166 1166 outstr = ""
1167 1167 # where hunk is in list of siblings
1168 1168 hunkindex = hunk.header.hunks.index(hunk)
1169 1169
1170 1170 if hunkindex != 0:
1171 1171 # add separating line before headers
1172 1172 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1173 1173 towin=towin, align=False)
1174 1174
1175 1175 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1176 1176 attrlist=[curses.A_BOLD])
1177 1177
1178 1178 # print out from-to line with checkbox
1179 1179 checkbox = self.getstatusprefixstring(hunk)
1180 1180
1181 1181 lineprefix = " "*self.hunkindentnumchars + checkbox
1182 1182 frtoline = " " + hunk.getfromtoline().strip("\n")
1183 1183
1184 1184 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1185 1185 align=False) # add uncolored checkbox/indent
1186 1186 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1187 1187 towin=towin)
1188 1188
1189 1189 if hunk.folded and not ignorefolding:
1190 1190 # skip remainder of output
1191 1191 return outstr
1192 1192
1193 1193 # print out lines of the chunk preceeding changed-lines
1194 1194 for line in hunk.before:
1195 1195 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1196 1196 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1197 1197
1198 1198 return outstr
1199 1199
1200 1200 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1201 1201 outstr = ""
1202 1202 if hunk.folded and not ignorefolding:
1203 1203 return outstr
1204 1204
1205 1205 # a bit superfluous, but to avoid hard-coding indent amount
1206 1206 checkbox = self.getstatusprefixstring(hunk)
1207 1207 for line in hunk.after:
1208 1208 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1209 1209 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1210 1210
1211 1211 return outstr
1212 1212
1213 1213 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1214 1214 outstr = ""
1215 1215 checkbox = self.getstatusprefixstring(hunkline)
1216 1216
1217 1217 linestr = hunkline.prettystr().strip("\n")
1218 1218
1219 1219 # select color-pair based on whether line is an addition/removal
1220 1220 if selected:
1221 1221 colorpair = self.getcolorpair(name="selected")
1222 1222 elif linestr.startswith("+"):
1223 1223 colorpair = self.getcolorpair(name="addition")
1224 1224 elif linestr.startswith("-"):
1225 1225 colorpair = self.getcolorpair(name="deletion")
1226 1226 elif linestr.startswith("\\"):
1227 1227 colorpair = self.getcolorpair(name="normal")
1228 1228
1229 1229 lineprefix = " "*self.hunklineindentnumchars + checkbox
1230 1230 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1231 1231 align=False) # add uncolored checkbox/indent
1232 1232 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1233 1233 towin=towin, showwhtspc=True)
1234 1234 return outstr
1235 1235
1236 1236 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1237 1237 towin=True):
1238 1238 """
1239 1239 use __printitem() to print the the specified item.applied.
1240 1240 if item is not specified, then print the entire patch.
1241 1241 (hiding folded elements, etc. -- see __printitem() docstring)
1242 1242 """
1243 1243
1244 1244 if item is None:
1245 1245 item = self.headerlist
1246 1246 if recursechildren:
1247 1247 self.linesprintedtopadsofar = 0
1248 1248
1249 1249 outstr = []
1250 1250 self.__printitem(item, ignorefolding, recursechildren, outstr,
1251 1251 towin=towin)
1252 1252 return ''.join(outstr)
1253 1253
1254 1254 def outofdisplayedarea(self):
1255 1255 y, _ = self.chunkpad.getyx() # cursor location
1256 1256 # * 2 here works but an optimization would be the max number of
1257 1257 # consecutive non selectable lines
1258 1258 # i.e the max number of context line for any hunk in the patch
1259 1259 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1260 1260 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1261 1261 return y < miny or y > maxy
1262 1262
1263 1263 def handleselection(self, item, recursechildren):
1264 1264 selected = (item is self.currentselecteditem)
1265 1265 if selected and recursechildren:
1266 1266 # assumes line numbering starting from line 0
1267 1267 self.selecteditemstartline = self.linesprintedtopadsofar
1268 1268 selecteditemlines = self.getnumlinesdisplayed(item,
1269 1269 recursechildren=False)
1270 1270 self.selecteditemendline = (self.selecteditemstartline +
1271 1271 selecteditemlines - 1)
1272 1272 return selected
1273 1273
1274 1274 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1275 1275 towin=True):
1276 1276 """
1277 1277 recursive method for printing out patch/header/hunk/hunk-line data to
1278 1278 screen. also returns a string with all of the content of the displayed
1279 1279 patch (not including coloring, etc.).
1280 1280
1281 1281 if ignorefolding is True, then folded items are printed out.
1282 1282
1283 1283 if recursechildren is False, then only print the item without its
1284 1284 child items.
1285 1285 """
1286 1286
1287 1287 if towin and self.outofdisplayedarea():
1288 1288 return
1289 1289
1290 1290 selected = self.handleselection(item, recursechildren)
1291 1291
1292 1292 # patch object is a list of headers
1293 1293 if isinstance(item, patch):
1294 1294 if recursechildren:
1295 1295 for hdr in item:
1296 1296 self.__printitem(hdr, ignorefolding,
1297 1297 recursechildren, outstr, towin)
1298 1298 # todo: eliminate all isinstance() calls
1299 1299 if isinstance(item, uiheader):
1300 1300 outstr.append(self.printheader(item, selected, towin=towin,
1301 1301 ignorefolding=ignorefolding))
1302 1302 if recursechildren:
1303 1303 for hnk in item.hunks:
1304 1304 self.__printitem(hnk, ignorefolding,
1305 1305 recursechildren, outstr, towin)
1306 1306 elif (isinstance(item, uihunk) and
1307 1307 ((not item.header.folded) or ignorefolding)):
1308 1308 # print the hunk data which comes before the changed-lines
1309 1309 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1310 1310 ignorefolding=ignorefolding))
1311 1311 if recursechildren:
1312 1312 for l in item.changedlines:
1313 1313 self.__printitem(l, ignorefolding,
1314 1314 recursechildren, outstr, towin)
1315 1315 outstr.append(self.printhunklinesafter(item, towin=towin,
1316 1316 ignorefolding=ignorefolding))
1317 1317 elif (isinstance(item, uihunkline) and
1318 1318 ((not item.hunk.folded) or ignorefolding)):
1319 1319 outstr.append(self.printhunkchangedline(item, selected,
1320 1320 towin=towin))
1321 1321
1322 1322 return outstr
1323 1323
1324 1324 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1325 1325 recursechildren=True):
1326 1326 """
1327 1327 return the number of lines which would be displayed if the item were
1328 1328 to be printed to the display. the item will not be printed to the
1329 1329 display (pad).
1330 1330 if no item is given, assume the entire patch.
1331 1331 if ignorefolding is True, folded items will be unfolded when counting
1332 1332 the number of lines.
1333 1333 """
1334 1334
1335 1335 # temporarily disable printing to windows by printstring
1336 1336 patchdisplaystring = self.printitem(item, ignorefolding,
1337 1337 recursechildren, towin=False)
1338 1338 numlines = len(patchdisplaystring) / self.xscreensize
1339 1339 return numlines
1340 1340
1341 1341 def sigwinchhandler(self, n, frame):
1342 1342 "handle window resizing"
1343 1343 try:
1344 1344 curses.endwin()
1345 1345 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1346 1346 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1347 1347 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1348 1348 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1349 1349 except curses.error:
1350 1350 pass
1351 1351
1352 1352 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1353 1353 attrlist=None):
1354 1354 """
1355 1355 get a curses color pair, adding it to self.colorpairs if it is not
1356 1356 already defined. an optional string, name, can be passed as a shortcut
1357 1357 for referring to the color-pair. by default, if no arguments are
1358 1358 specified, the white foreground / black background color-pair is
1359 1359 returned.
1360 1360
1361 1361 it is expected that this function will be used exclusively for
1362 1362 initializing color pairs, and not curses.init_pair().
1363 1363
1364 1364 attrlist is used to 'flavor' the returned color-pair. this information
1365 1365 is not stored in self.colorpairs. it contains attribute values like
1366 1366 curses.A_BOLD.
1367 1367 """
1368 1368
1369 1369 if (name is not None) and name in self.colorpairnames:
1370 1370 # then get the associated color pair and return it
1371 1371 colorpair = self.colorpairnames[name]
1372 1372 else:
1373 1373 if fgcolor is None:
1374 1374 fgcolor = -1
1375 1375 if bgcolor is None:
1376 1376 bgcolor = -1
1377 1377 if (fgcolor, bgcolor) in self.colorpairs:
1378 1378 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1379 1379 else:
1380 1380 pairindex = len(self.colorpairs) + 1
1381 1381 if self.usecolor:
1382 1382 curses.init_pair(pairindex, fgcolor, bgcolor)
1383 1383 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1384 1384 curses.color_pair(pairindex))
1385 1385 if name is not None:
1386 1386 self.colorpairnames[name] = curses.color_pair(pairindex)
1387 1387 else:
1388 1388 cval = 0
1389 1389 if name is not None:
1390 1390 if name == 'selected':
1391 1391 cval = curses.A_REVERSE
1392 1392 self.colorpairnames[name] = cval
1393 1393 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1394 1394
1395 1395 # add attributes if possible
1396 1396 if attrlist is None:
1397 1397 attrlist = []
1398 1398 if colorpair < 256:
1399 1399 # then it is safe to apply all attributes
1400 1400 for textattr in attrlist:
1401 1401 colorpair |= textattr
1402 1402 else:
1403 1403 # just apply a select few (safe?) attributes
1404 1404 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1405 1405 if textattrib in attrlist:
1406 1406 colorpair |= textattrib
1407 1407 return colorpair
1408 1408
1409 1409 def initcolorpair(self, *args, **kwargs):
1410 1410 "same as getcolorpair."
1411 1411 self.getcolorpair(*args, **kwargs)
1412 1412
1413 1413 def helpwindow(self):
1414 1414 "print a help window to the screen. exit after any keypress."
1415 1415 helptext = _(
1416 1416 """ [press any key to return to the patch-display]
1417 1417
1418 1418 crecord allows you to interactively choose among the changes you have made,
1419 1419 and confirm only those changes you select for further processing by the command
1420 1420 you are running (commit/shelve/revert), after confirming the selected
1421 1421 changes, the unselected changes are still present in your working copy, so you
1422 1422 can use crecord multiple times to split large changes into smaller changesets.
1423 1423 the following are valid keystrokes:
1424 1424
1425 1425 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1426 1426 A : (un-)select all items
1427 1427 up/down-arrow [k/j] : go to previous/next unfolded item
1428 1428 pgup/pgdn [K/J] : go to previous/next item of same type
1429 1429 right/left-arrow [l/h] : go to child item / parent item
1430 1430 shift-left-arrow [H] : go to parent header / fold selected header
1431 1431 f : fold / unfold item, hiding/revealing its children
1432 1432 F : fold / unfold parent item and all of its ancestors
1433 1433 ctrl-l : scroll the selected line to the top of the screen
1434 1434 m : edit / resume editing the commit message
1435 1435 e : edit the currently selected hunk
1436 1436 a : toggle amend mode, only with commit -i
1437 1437 c : confirm selected changes
1438 1438 r : review/edit and confirm selected changes
1439 1439 q : quit without confirming (no changes will be made)
1440 1440 ? : help (what you're currently reading)""")
1441 1441
1442 1442 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1443 1443 helplines = helptext.split("\n")
1444 1444 helplines = helplines + [" "]*(
1445 1445 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1446 1446 try:
1447 1447 for line in helplines:
1448 1448 self.printstring(helpwin, line, pairname="legend")
1449 1449 except curses.error:
1450 1450 pass
1451 1451 helpwin.refresh()
1452 1452 try:
1453 1453 with self.ui.timeblockedsection('crecord'):
1454 1454 helpwin.getkey()
1455 1455 except curses.error:
1456 1456 pass
1457 1457
1458 1458 def commitMessageWindow(self):
1459 1459 "Create a temporary commit message editing window on the screen."
1460 1460
1461 1461 curses.raw()
1462 1462 curses.def_prog_mode()
1463 1463 curses.endwin()
1464 1464 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1465 1465 curses.cbreak()
1466 1466 self.stdscr.refresh()
1467 1467 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1468 1468
1469 1469 def confirmationwindow(self, windowtext):
1470 1470 "display an informational window, then wait for and return a keypress."
1471 1471
1472 1472 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1473 1473 try:
1474 1474 lines = windowtext.split("\n")
1475 1475 for line in lines:
1476 1476 self.printstring(confirmwin, line, pairname="selected")
1477 1477 except curses.error:
1478 1478 pass
1479 1479 self.stdscr.refresh()
1480 1480 confirmwin.refresh()
1481 1481 try:
1482 1482 with self.ui.timeblockedsection('crecord'):
1483 1483 response = chr(self.stdscr.getch())
1484 1484 except ValueError:
1485 1485 response = None
1486 1486
1487 1487 return response
1488 1488
1489 1489 def reviewcommit(self):
1490 1490 """ask for 'y' to be pressed to confirm selected. return True if
1491 1491 confirmed."""
1492 1492 confirmtext = _(
1493 1493 """if you answer yes to the following, the your currently chosen patch chunks
1494 1494 will be loaded into an editor. you may modify the patch from the editor, and
1495 1495 save the changes if you wish to change the patch. otherwise, you can just
1496 1496 close the editor without saving to accept the current patch as-is.
1497 1497
1498 1498 note: don't add/remove lines unless you also modify the range information.
1499 1499 failing to follow this rule will result in the commit aborting.
1500 1500
1501 1501 are you sure you want to review/edit and confirm the selected changes [yn]?
1502 1502 """)
1503 1503 with self.ui.timeblockedsection('crecord'):
1504 1504 response = self.confirmationwindow(confirmtext)
1505 1505 if response is None:
1506 1506 response = "n"
1507 1507 if response.lower().startswith("y"):
1508 1508 return True
1509 1509 else:
1510 1510 return False
1511 1511
1512 1512 def toggleamend(self, opts, test):
1513 1513 """Toggle the amend flag.
1514 1514
1515 1515 When the amend flag is set, a commit will modify the most recently
1516 1516 committed changeset, instead of creating a new changeset. Otherwise, a
1517 1517 new changeset will be created (the normal commit behavior).
1518 1518 """
1519 1519
1520 1520 try:
1521 1521 ver = float(util.version()[:3])
1522 1522 except ValueError:
1523 1523 ver = 1
1524 1524 if ver < 2.19:
1525 1525 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1526 1526 "Press any key to continue.")
1527 1527 elif opts.get('amend') is None:
1528 1528 opts['amend'] = True
1529 1529 msg = _("Amend option is turned on -- committing the currently "
1530 1530 "selected changes will not create a new changeset, but "
1531 1531 "instead update the most recently committed changeset.\n\n"
1532 1532 "Press any key to continue.")
1533 1533 elif opts.get('amend') is True:
1534 1534 opts['amend'] = None
1535 1535 msg = _("Amend option is turned off -- committing the currently "
1536 1536 "selected changes will create a new changeset.\n\n"
1537 1537 "Press any key to continue.")
1538 1538 if not test:
1539 1539 self.confirmationwindow(msg)
1540 1540
1541 1541 def recenterdisplayedarea(self):
1542 1542 """
1543 1543 once we scrolled with pg up pg down we can be pointing outside of the
1544 1544 display zone. we print the patch with towin=False to compute the
1545 1545 location of the selected item even though it is outside of the displayed
1546 1546 zone and then update the scroll.
1547 1547 """
1548 1548 self.printitem(towin=False)
1549 1549 self.updatescroll()
1550 1550
1551 1551 def toggleedit(self, item=None, test=False):
1552 1552 """
1553 1553 edit the currently selected chunk
1554 1554 """
1555 1555 def updateui(self):
1556 1556 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1557 1557 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1558 1558 self.updatescroll()
1559 1559 self.stdscr.refresh()
1560 1560 self.statuswin.refresh()
1561 1561 self.stdscr.keypad(1)
1562 1562
1563 1563 def editpatchwitheditor(self, chunk):
1564 1564 if chunk is None:
1565 1565 self.ui.write(_('cannot edit patch for whole file'))
1566 1566 self.ui.write("\n")
1567 1567 return None
1568 1568 if chunk.header.binary():
1569 1569 self.ui.write(_('cannot edit patch for binary file'))
1570 1570 self.ui.write("\n")
1571 1571 return None
1572 1572
1573 1573 # write the initial patch
1574 1574 patch = stringio()
1575 1575 patch.write(diffhelptext + hunkhelptext)
1576 1576 chunk.header.write(patch)
1577 1577 chunk.write(patch)
1578 1578
1579 1579 # start the editor and wait for it to complete
1580 1580 try:
1581 1581 patch = self.ui.edit(patch.getvalue(), "", action="diff")
1582 1582 except error.Abort as exc:
1583 1583 self.errorstr = str(exc)
1584 1584 return None
1585 1585
1586 1586 # remove comment lines
1587 1587 patch = [line + '\n' for line in patch.splitlines()
1588 1588 if not line.startswith('#')]
1589 1589 return patchmod.parsepatch(patch)
1590 1590
1591 1591 if item is None:
1592 1592 item = self.currentselecteditem
1593 1593 if isinstance(item, uiheader):
1594 1594 return
1595 1595 if isinstance(item, uihunkline):
1596 1596 item = item.parentitem()
1597 1597 if not isinstance(item, uihunk):
1598 1598 return
1599 1599
1600 1600 # To go back to that hunk or its replacement at the end of the edit
1601 1601 itemindex = item.parentitem().hunks.index(item)
1602 1602
1603 1603 beforeadded, beforeremoved = item.added, item.removed
1604 1604 newpatches = editpatchwitheditor(self, item)
1605 1605 if newpatches is None:
1606 1606 if not test:
1607 1607 updateui(self)
1608 1608 return
1609 1609 header = item.header
1610 1610 editedhunkindex = header.hunks.index(item)
1611 1611 hunksbefore = header.hunks[:editedhunkindex]
1612 1612 hunksafter = header.hunks[editedhunkindex + 1:]
1613 1613 newpatchheader = newpatches[0]
1614 1614 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1615 1615 newadded = sum([h.added for h in newhunks])
1616 1616 newremoved = sum([h.removed for h in newhunks])
1617 1617 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1618 1618
1619 1619 for h in hunksafter:
1620 1620 h.toline += offset
1621 1621 for h in newhunks:
1622 1622 h.folded = False
1623 1623 header.hunks = hunksbefore + newhunks + hunksafter
1624 1624 if self.emptypatch():
1625 1625 header.hunks = hunksbefore + [item] + hunksafter
1626 1626 self.currentselecteditem = header
1627 1627 if len(header.hunks) > itemindex:
1628 1628 self.currentselecteditem = header.hunks[itemindex]
1629 1629
1630 1630 if not test:
1631 1631 updateui(self)
1632 1632
1633 1633 def emptypatch(self):
1634 1634 item = self.headerlist
1635 1635 if not item:
1636 1636 return True
1637 1637 for header in item:
1638 1638 if header.hunks:
1639 1639 return False
1640 1640 return True
1641 1641
1642 1642 def handlekeypressed(self, keypressed, test=False):
1643 1643 """
1644 1644 Perform actions based on pressed keys.
1645 1645
1646 1646 Return true to exit the main loop.
1647 1647 """
1648 1648 if keypressed in ["k", "KEY_UP"]:
1649 1649 self.uparrowevent()
1650 1650 if keypressed in ["K", "KEY_PPAGE"]:
1651 1651 self.uparrowshiftevent()
1652 1652 elif keypressed in ["j", "KEY_DOWN"]:
1653 1653 self.downarrowevent()
1654 1654 elif keypressed in ["J", "KEY_NPAGE"]:
1655 1655 self.downarrowshiftevent()
1656 1656 elif keypressed in ["l", "KEY_RIGHT"]:
1657 1657 self.rightarrowevent()
1658 1658 elif keypressed in ["h", "KEY_LEFT"]:
1659 1659 self.leftarrowevent()
1660 1660 elif keypressed in ["H", "KEY_SLEFT"]:
1661 1661 self.leftarrowshiftevent()
1662 1662 elif keypressed in ["q"]:
1663 1663 raise error.Abort(_('user quit'))
1664 1664 elif keypressed in ['a']:
1665 1665 self.toggleamend(self.opts, test)
1666 1666 elif keypressed in ["c"]:
1667 1667 return True
1668 1668 elif test and keypressed in ['X']:
1669 1669 return True
1670 1670 elif keypressed in ["r"]:
1671 1671 if self.reviewcommit():
1672 1672 self.opts['review'] = True
1673 1673 return True
1674 1674 elif test and keypressed in ['R']:
1675 1675 self.opts['review'] = True
1676 1676 return True
1677 1677 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1678 1678 self.toggleapply()
1679 1679 if self.ui.configbool('experimental', 'spacemovesdown'):
1680 1680 self.downarrowevent()
1681 1681 elif keypressed in ['A']:
1682 1682 self.toggleall()
1683 1683 elif keypressed in ['e']:
1684 1684 self.toggleedit(test=test)
1685 1685 elif keypressed in ["f"]:
1686 1686 self.togglefolded()
1687 1687 elif keypressed in ["F"]:
1688 1688 self.togglefolded(foldparent=True)
1689 1689 elif keypressed in ["m"]:
1690 1690 self.commitMessageWindow()
1691 1691 elif keypressed in ["?"]:
1692 1692 self.helpwindow()
1693 1693 self.stdscr.clear()
1694 1694 self.stdscr.refresh()
1695 1695 elif curses.unctrl(keypressed) in ["^L"]:
1696 1696 # scroll the current line to the top of the screen
1697 1697 self.scrolllines(self.selecteditemstartline)
1698 1698
1699 1699 def main(self, stdscr):
1700 1700 """
1701 1701 method to be wrapped by curses.wrapper() for selecting chunks.
1702 1702 """
1703 1703
1704 1704 origsigwinch = sentinel = object()
1705 1705 if util.safehasattr(signal, 'SIGWINCH'):
1706 1706 origsigwinch = signal.signal(signal.SIGWINCH,
1707 1707 self.sigwinchhandler)
1708 1708 try:
1709 1709 return self._main(stdscr)
1710 1710 finally:
1711 1711 if origsigwinch is not sentinel:
1712 1712 signal.signal(signal.SIGWINCH, origsigwinch)
1713 1713
1714 1714 def _main(self, stdscr):
1715 1715 self.stdscr = stdscr
1716 1716 # error during initialization, cannot be printed in the curses
1717 1717 # interface, it should be printed by the calling code
1718 1718 self.initerr = None
1719 1719 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1720 1720
1721 1721 curses.start_color()
1722 1722 try:
1723 1723 curses.use_default_colors()
1724 1724 except curses.error:
1725 1725 self.usecolor = False
1726 1726
1727 1727 # available colors: black, blue, cyan, green, magenta, white, yellow
1728 1728 # init_pair(color_id, foreground_color, background_color)
1729 1729 self.initcolorpair(None, None, name="normal")
1730 1730 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1731 1731 name="selected")
1732 1732 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1733 1733 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1734 1734 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1735 1735 # newwin([height, width,] begin_y, begin_x)
1736 1736 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1737 1737 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1738 1738
1739 1739 # figure out how much space to allocate for the chunk-pad which is
1740 1740 # used for displaying the patch
1741 1741
1742 1742 # stupid hack to prevent getnumlinesdisplayed from failing
1743 1743 self.chunkpad = curses.newpad(1, self.xscreensize)
1744 1744
1745 1745 # add 1 so to account for last line text reaching end of line
1746 1746 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1747 1747
1748 1748 try:
1749 1749 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1750 1750 except curses.error:
1751 1751 self.initerr = _('this diff is too large to be displayed')
1752 1752 return
1753 1753 # initialize selecteditemendline (initial start-line is 0)
1754 1754 self.selecteditemendline = self.getnumlinesdisplayed(
1755 1755 self.currentselecteditem, recursechildren=False)
1756 1756
1757 1757 while True:
1758 1758 self.updatescreen()
1759 1759 try:
1760 1760 with self.ui.timeblockedsection('crecord'):
1761 1761 keypressed = self.statuswin.getkey()
1762 1762 if self.errorstr is not None:
1763 1763 self.errorstr = None
1764 1764 continue
1765 1765 except curses.error:
1766 1766 keypressed = "foobar"
1767 1767 if self.handlekeypressed(keypressed):
1768 1768 break
1769 1769
1770 1770 if self.commenttext != "":
1771 1771 whitespaceremoved = re.sub("(?m)^\s.*(\n|$)", "", self.commenttext)
1772 1772 if whitespaceremoved != "":
1773 1773 self.opts['message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now