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