##// END OF EJS Templates
crecord: fix revert -ir '.^' crash caused by 3649c3f2cd...
Jun Wu -
r35067:57c79542 default
parent child Browse files
Show More
@@ -1,1755 +1,1755
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 'revert': _('Select hunks to revert'),
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 # the currently selected header, hunk, or hunk-line
585 585 self.currentselecteditem = self.headerlist[0]
586 586
587 587 # updated when printing out patch-display -- the 'lines' here are the
588 588 # line positions *in the pad*, not on the screen.
589 589 self.selecteditemstartline = 0
590 590 self.selecteditemendline = None
591 591
592 592 # define indentation levels
593 593 self.headerindentnumchars = 0
594 594 self.hunkindentnumchars = 3
595 595 self.hunklineindentnumchars = 6
596 596
597 597 # the first line of the pad to print to the screen
598 598 self.firstlineofpadtoprint = 0
599 599
600 600 # keeps track of the number of lines in the pad
601 601 self.numpadlines = None
602 602
603 603 self.numstatuslines = 1
604 604
605 605 # keep a running count of the number of lines printed to the pad
606 606 # (used for determining when the selected item begins/ends)
607 607 self.linesprintedtopadsofar = 0
608 608
609 609 # the first line of the pad which is visible on the screen
610 610 self.firstlineofpadtoprint = 0
611 611
612 612 # stores optional text for a commit comment provided by the user
613 613 self.commenttext = ""
614 614
615 615 # if the last 'toggle all' command caused all changes to be applied
616 616 self.waslasttoggleallapplied = True
617 617
618 618 # affects some ui text
619 619 if operation not in _headermessages:
620 620 raise error.ProgrammingError('unexpected operation: %s' % operation)
621 621 self.operation = operation
622 622
623 623 def uparrowevent(self):
624 624 """
625 625 try to select the previous item to the current item that has the
626 626 most-indented level. for example, if a hunk is selected, try to select
627 627 the last hunkline of the hunk prior to the selected hunk. or, if
628 628 the first hunkline of a hunk is currently selected, then select the
629 629 hunk itself.
630 630 """
631 631 currentitem = self.currentselecteditem
632 632
633 633 nextitem = currentitem.previtem()
634 634
635 635 if nextitem is None:
636 636 # if no parent item (i.e. currentitem is the first header), then
637 637 # no change...
638 638 nextitem = currentitem
639 639
640 640 self.currentselecteditem = nextitem
641 641
642 642 def uparrowshiftevent(self):
643 643 """
644 644 select (if possible) the previous item on the same level as the
645 645 currently selected item. otherwise, select (if possible) the
646 646 parent-item of the currently selected item.
647 647 """
648 648 currentitem = self.currentselecteditem
649 649 nextitem = currentitem.prevsibling()
650 650 # if there's no previous sibling, try choosing the parent
651 651 if nextitem is None:
652 652 nextitem = currentitem.parentitem()
653 653 if nextitem is None:
654 654 # if no parent item (i.e. currentitem is the first header), then
655 655 # no change...
656 656 nextitem = currentitem
657 657
658 658 self.currentselecteditem = nextitem
659 659
660 660 def downarrowevent(self):
661 661 """
662 662 try to select the next item to the current item that has the
663 663 most-indented level. for example, if a hunk is selected, select
664 664 the first hunkline of the selected hunk. or, if the last hunkline of
665 665 a hunk is currently selected, then select the next hunk, if one exists,
666 666 or if not, the next header if one exists.
667 667 """
668 668 #self.startprintline += 1 #debug
669 669 currentitem = self.currentselecteditem
670 670
671 671 nextitem = currentitem.nextitem()
672 672 # if there's no next item, keep the selection as-is
673 673 if nextitem is None:
674 674 nextitem = currentitem
675 675
676 676 self.currentselecteditem = nextitem
677 677
678 678 def downarrowshiftevent(self):
679 679 """
680 680 select (if possible) the next item on the same level as the currently
681 681 selected item. otherwise, select (if possible) the next item on the
682 682 same level as the parent item of the currently selected item.
683 683 """
684 684 currentitem = self.currentselecteditem
685 685 nextitem = currentitem.nextsibling()
686 686 # if there's no next sibling, try choosing the parent's nextsibling
687 687 if nextitem is None:
688 688 try:
689 689 nextitem = currentitem.parentitem().nextsibling()
690 690 except AttributeError:
691 691 # parentitem returned None, so nextsibling() can't be called
692 692 nextitem = None
693 693 if nextitem is None:
694 694 # if parent has no next sibling, then no change...
695 695 nextitem = currentitem
696 696
697 697 self.currentselecteditem = nextitem
698 698
699 699 def rightarrowevent(self):
700 700 """
701 701 select (if possible) the first of this item's child-items.
702 702 """
703 703 currentitem = self.currentselecteditem
704 704 nextitem = currentitem.firstchild()
705 705
706 706 # turn off folding if we want to show a child-item
707 707 if currentitem.folded:
708 708 self.togglefolded(currentitem)
709 709
710 710 if nextitem is None:
711 711 # if no next item on parent-level, then no change...
712 712 nextitem = currentitem
713 713
714 714 self.currentselecteditem = nextitem
715 715
716 716 def leftarrowevent(self):
717 717 """
718 718 if the current item can be folded (i.e. it is an unfolded header or
719 719 hunk), then fold it. otherwise try select (if possible) the parent
720 720 of this item.
721 721 """
722 722 currentitem = self.currentselecteditem
723 723
724 724 # try to fold the item
725 725 if not isinstance(currentitem, uihunkline):
726 726 if not currentitem.folded:
727 727 self.togglefolded(item=currentitem)
728 728 return
729 729
730 730 # if it can't be folded, try to select the parent item
731 731 nextitem = currentitem.parentitem()
732 732
733 733 if nextitem is None:
734 734 # if no item on parent-level, then no change...
735 735 nextitem = currentitem
736 736 if not nextitem.folded:
737 737 self.togglefolded(item=nextitem)
738 738
739 739 self.currentselecteditem = nextitem
740 740
741 741 def leftarrowshiftevent(self):
742 742 """
743 743 select the header of the current item (or fold current item if the
744 744 current item is already a header).
745 745 """
746 746 currentitem = self.currentselecteditem
747 747
748 748 if isinstance(currentitem, uiheader):
749 749 if not currentitem.folded:
750 750 self.togglefolded(item=currentitem)
751 751 return
752 752
753 753 # select the parent item recursively until we're at a header
754 754 while True:
755 755 nextitem = currentitem.parentitem()
756 756 if nextitem is None:
757 757 break
758 758 else:
759 759 currentitem = nextitem
760 760
761 761 self.currentselecteditem = currentitem
762 762
763 763 def updatescroll(self):
764 764 "scroll the screen to fully show the currently-selected"
765 765 selstart = self.selecteditemstartline
766 766 selend = self.selecteditemendline
767 767
768 768 padstart = self.firstlineofpadtoprint
769 769 padend = padstart + self.yscreensize - self.numstatuslines - 1
770 770 # 'buffered' pad start/end values which scroll with a certain
771 771 # top/bottom context margin
772 772 padstartbuffered = padstart + 3
773 773 padendbuffered = padend - 3
774 774
775 775 if selend > padendbuffered:
776 776 self.scrolllines(selend - padendbuffered)
777 777 elif selstart < padstartbuffered:
778 778 # negative values scroll in pgup direction
779 779 self.scrolllines(selstart - padstartbuffered)
780 780
781 781 def scrolllines(self, numlines):
782 782 "scroll the screen up (down) by numlines when numlines >0 (<0)."
783 783 self.firstlineofpadtoprint += numlines
784 784 if self.firstlineofpadtoprint < 0:
785 785 self.firstlineofpadtoprint = 0
786 786 if self.firstlineofpadtoprint > self.numpadlines - 1:
787 787 self.firstlineofpadtoprint = self.numpadlines - 1
788 788
789 789 def toggleapply(self, item=None):
790 790 """
791 791 toggle the applied flag of the specified item. if no item is specified,
792 792 toggle the flag of the currently selected item.
793 793 """
794 794 if item is None:
795 795 item = self.currentselecteditem
796 796
797 797 item.applied = not item.applied
798 798
799 799 if isinstance(item, uiheader):
800 800 item.partial = False
801 801 if item.applied:
802 802 # apply all its hunks
803 803 for hnk in item.hunks:
804 804 hnk.applied = True
805 805 # apply all their hunklines
806 806 for hunkline in hnk.changedlines:
807 807 hunkline.applied = True
808 808 else:
809 809 # un-apply all its hunks
810 810 for hnk in item.hunks:
811 811 hnk.applied = False
812 812 hnk.partial = False
813 813 # un-apply all their hunklines
814 814 for hunkline in hnk.changedlines:
815 815 hunkline.applied = False
816 816 elif isinstance(item, uihunk):
817 817 item.partial = False
818 818 # apply all it's hunklines
819 819 for hunkline in item.changedlines:
820 820 hunkline.applied = item.applied
821 821
822 822 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
823 823 allsiblingsapplied = not (False in siblingappliedstatus)
824 824 nosiblingsapplied = not (True in siblingappliedstatus)
825 825
826 826 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
827 827 somesiblingspartial = (True in siblingspartialstatus)
828 828
829 829 #cases where applied or partial should be removed from header
830 830
831 831 # if no 'sibling' hunks are applied (including this hunk)
832 832 if nosiblingsapplied:
833 833 if not item.header.special():
834 834 item.header.applied = False
835 835 item.header.partial = False
836 836 else: # some/all parent siblings are applied
837 837 item.header.applied = True
838 838 item.header.partial = (somesiblingspartial or
839 839 not allsiblingsapplied)
840 840
841 841 elif isinstance(item, uihunkline):
842 842 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
843 843 allsiblingsapplied = not (False in siblingappliedstatus)
844 844 nosiblingsapplied = not (True in siblingappliedstatus)
845 845
846 846 # if no 'sibling' lines are applied
847 847 if nosiblingsapplied:
848 848 item.hunk.applied = False
849 849 item.hunk.partial = False
850 850 elif allsiblingsapplied:
851 851 item.hunk.applied = True
852 852 item.hunk.partial = False
853 853 else: # some siblings applied
854 854 item.hunk.applied = True
855 855 item.hunk.partial = True
856 856
857 857 parentsiblingsapplied = [hnk.applied for hnk
858 858 in item.hunk.header.hunks]
859 859 noparentsiblingsapplied = not (True in parentsiblingsapplied)
860 860 allparentsiblingsapplied = not (False in parentsiblingsapplied)
861 861
862 862 parentsiblingspartial = [hnk.partial for hnk
863 863 in item.hunk.header.hunks]
864 864 someparentsiblingspartial = (True in parentsiblingspartial)
865 865
866 866 # if all parent hunks are not applied, un-apply header
867 867 if noparentsiblingsapplied:
868 868 if not item.hunk.header.special():
869 869 item.hunk.header.applied = False
870 870 item.hunk.header.partial = False
871 871 # set the applied and partial status of the header if needed
872 872 else: # some/all parent siblings are applied
873 873 item.hunk.header.applied = True
874 874 item.hunk.header.partial = (someparentsiblingspartial or
875 875 not allparentsiblingsapplied)
876 876
877 877 def toggleall(self):
878 878 "toggle the applied flag of all items."
879 879 if self.waslasttoggleallapplied: # then unapply them this time
880 880 for item in self.headerlist:
881 881 if item.applied:
882 882 self.toggleapply(item)
883 883 else:
884 884 for item in self.headerlist:
885 885 if not item.applied:
886 886 self.toggleapply(item)
887 887 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
888 888
889 889 def togglefolded(self, item=None, foldparent=False):
890 890 "toggle folded flag of specified item (defaults to currently selected)"
891 891 if item is None:
892 892 item = self.currentselecteditem
893 893 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
894 894 if not isinstance(item, uiheader):
895 895 # we need to select the parent item in this case
896 896 self.currentselecteditem = item = item.parentitem()
897 897 elif item.neverunfolded:
898 898 item.neverunfolded = False
899 899
900 900 # also fold any foldable children of the parent/current item
901 901 if isinstance(item, uiheader): # the original or 'new' item
902 902 for child in item.allchildren():
903 903 child.folded = not item.folded
904 904
905 905 if isinstance(item, (uiheader, uihunk)):
906 906 item.folded = not item.folded
907 907
908 908 def alignstring(self, instr, window):
909 909 """
910 910 add whitespace to the end of a string in order to make it fill
911 911 the screen in the x direction. the current cursor position is
912 912 taken into account when making this calculation. the string can span
913 913 multiple lines.
914 914 """
915 915 y, xstart = window.getyx()
916 916 width = self.xscreensize
917 917 # turn tabs into spaces
918 918 instr = instr.expandtabs(4)
919 919 strwidth = encoding.colwidth(instr)
920 920 numspaces = (width - ((strwidth + xstart) % width) - 1)
921 921 return instr + " " * numspaces + "\n"
922 922
923 923 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
924 924 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
925 925 """
926 926 print the string, text, with the specified colors and attributes, to
927 927 the specified curses window object.
928 928
929 929 the foreground and background colors are of the form
930 930 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
931 931 magenta, red, white, yellow]. if pairname is provided, a color
932 932 pair will be looked up in the self.colorpairnames dictionary.
933 933
934 934 attrlist is a list containing text attributes in the form of
935 935 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
936 936 underline].
937 937
938 938 if align == True, whitespace is added to the printed string such that
939 939 the string stretches to the right border of the window.
940 940
941 941 if showwhtspc == True, trailing whitespace of a string is highlighted.
942 942 """
943 943 # preprocess the text, converting tabs to spaces
944 944 text = text.expandtabs(4)
945 945 # strip \n, and convert control characters to ^[char] representation
946 946 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
947 947 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
948 948
949 949 if pair is not None:
950 950 colorpair = pair
951 951 elif pairname is not None:
952 952 colorpair = self.colorpairnames[pairname]
953 953 else:
954 954 if fgcolor is None:
955 955 fgcolor = -1
956 956 if bgcolor is None:
957 957 bgcolor = -1
958 958 if (fgcolor, bgcolor) in self.colorpairs:
959 959 colorpair = self.colorpairs[(fgcolor, bgcolor)]
960 960 else:
961 961 colorpair = self.getcolorpair(fgcolor, bgcolor)
962 962 # add attributes if possible
963 963 if attrlist is None:
964 964 attrlist = []
965 965 if colorpair < 256:
966 966 # then it is safe to apply all attributes
967 967 for textattr in attrlist:
968 968 colorpair |= textattr
969 969 else:
970 970 # just apply a select few (safe?) attributes
971 971 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
972 972 if textattr in attrlist:
973 973 colorpair |= textattr
974 974
975 975 y, xstart = self.chunkpad.getyx()
976 976 t = "" # variable for counting lines printed
977 977 # if requested, show trailing whitespace
978 978 if showwhtspc:
979 979 origlen = len(text)
980 980 text = text.rstrip(' \n') # tabs have already been expanded
981 981 strippedlen = len(text)
982 982 numtrailingspaces = origlen - strippedlen
983 983
984 984 if towin:
985 985 window.addstr(text, colorpair)
986 986 t += text
987 987
988 988 if showwhtspc:
989 989 wscolorpair = colorpair | curses.A_REVERSE
990 990 if towin:
991 991 for i in range(numtrailingspaces):
992 992 window.addch(curses.ACS_CKBOARD, wscolorpair)
993 993 t += " " * numtrailingspaces
994 994
995 995 if align:
996 996 if towin:
997 997 extrawhitespace = self.alignstring("", window)
998 998 window.addstr(extrawhitespace, colorpair)
999 999 else:
1000 1000 # need to use t, since the x position hasn't incremented
1001 1001 extrawhitespace = self.alignstring(t, window)
1002 1002 t += extrawhitespace
1003 1003
1004 1004 # is reset to 0 at the beginning of printitem()
1005 1005
1006 1006 linesprinted = (xstart + len(t)) / self.xscreensize
1007 1007 self.linesprintedtopadsofar += linesprinted
1008 1008 return t
1009 1009
1010 1010 def _getstatuslinesegments(self):
1011 1011 """-> [str]. return segments"""
1012 1012 selected = self.currentselecteditem.applied
1013 1013 spaceselect = _('space: select')
1014 1014 spacedeselect = _('space: deselect')
1015 1015 # Format the selected label into a place as long as the longer of the
1016 1016 # two possible labels. This may vary by language.
1017 1017 spacelen = max(len(spaceselect), len(spacedeselect))
1018 1018 selectedlabel = '%-*s' % (spacelen,
1019 1019 spacedeselect if selected else spaceselect)
1020 1020 segments = [
1021 1021 _headermessages[self.operation],
1022 1022 '-',
1023 1023 _('[x]=selected **=collapsed'),
1024 1024 _('c: confirm'),
1025 1025 _('q: abort'),
1026 1026 _('arrow keys: move/expand/collapse'),
1027 1027 selectedlabel,
1028 1028 _('?: help'),
1029 1029 ]
1030 1030 return segments
1031 1031
1032 1032 def _getstatuslines(self):
1033 1033 """() -> [str]. return short help used in the top status window"""
1034 1034 if self.errorstr is not None:
1035 1035 lines = [self.errorstr, _('Press any key to continue')]
1036 1036 else:
1037 1037 # wrap segments to lines
1038 1038 segments = self._getstatuslinesegments()
1039 1039 width = self.xscreensize
1040 1040 lines = []
1041 1041 lastwidth = width
1042 1042 for s in segments:
1043 1043 w = encoding.colwidth(s)
1044 1044 sep = ' ' * (1 + (s and s[0] not in '-['))
1045 1045 if lastwidth + w + len(sep) >= width:
1046 1046 lines.append(s)
1047 1047 lastwidth = w
1048 1048 else:
1049 1049 lines[-1] += sep + s
1050 1050 lastwidth += w + len(sep)
1051 1051 if len(lines) != self.numstatuslines:
1052 1052 self.numstatuslines = len(lines)
1053 1053 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1054 1054 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
1055 1055
1056 1056 def updatescreen(self):
1057 1057 self.statuswin.erase()
1058 1058 self.chunkpad.erase()
1059 1059
1060 1060 printstring = self.printstring
1061 1061
1062 1062 # print out the status lines at the top
1063 1063 try:
1064 1064 for line in self._getstatuslines():
1065 1065 printstring(self.statuswin, line, pairname="legend")
1066 1066 self.statuswin.refresh()
1067 1067 except curses.error:
1068 1068 pass
1069 1069 if self.errorstr is not None:
1070 1070 return
1071 1071
1072 1072 # print out the patch in the remaining part of the window
1073 1073 try:
1074 1074 self.printitem()
1075 1075 self.updatescroll()
1076 1076 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1077 1077 self.numstatuslines, 0,
1078 1078 self.yscreensize - self.numstatuslines,
1079 1079 self.xscreensize)
1080 1080 except curses.error:
1081 1081 pass
1082 1082
1083 1083 def getstatusprefixstring(self, item):
1084 1084 """
1085 1085 create a string to prefix a line with which indicates whether 'item'
1086 1086 is applied and/or folded.
1087 1087 """
1088 1088
1089 1089 # create checkbox string
1090 1090 if item.applied:
1091 1091 if not isinstance(item, uihunkline) and item.partial:
1092 1092 checkbox = "[~]"
1093 1093 else:
1094 1094 checkbox = "[x]"
1095 1095 else:
1096 1096 checkbox = "[ ]"
1097 1097
1098 1098 try:
1099 1099 if item.folded:
1100 1100 checkbox += "**"
1101 1101 if isinstance(item, uiheader):
1102 1102 # one of "m", "a", or "d" (modified, added, deleted)
1103 1103 filestatus = item.changetype
1104 1104
1105 1105 checkbox += filestatus + " "
1106 1106 else:
1107 1107 checkbox += " "
1108 1108 if isinstance(item, uiheader):
1109 1109 # add two more spaces for headers
1110 1110 checkbox += " "
1111 1111 except AttributeError: # not foldable
1112 1112 checkbox += " "
1113 1113
1114 1114 return checkbox
1115 1115
1116 1116 def printheader(self, header, selected=False, towin=True,
1117 1117 ignorefolding=False):
1118 1118 """
1119 1119 print the header to the pad. if countlines is True, don't print
1120 1120 anything, but just count the number of lines which would be printed.
1121 1121 """
1122 1122
1123 1123 outstr = ""
1124 1124 text = header.prettystr()
1125 1125 chunkindex = self.chunklist.index(header)
1126 1126
1127 1127 if chunkindex != 0 and not header.folded:
1128 1128 # add separating line before headers
1129 1129 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1130 1130 towin=towin, align=False)
1131 1131 # select color-pair based on if the header is selected
1132 1132 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1133 1133 attrlist=[curses.A_BOLD])
1134 1134
1135 1135 # print out each line of the chunk, expanding it to screen width
1136 1136
1137 1137 # number of characters to indent lines on this level by
1138 1138 indentnumchars = 0
1139 1139 checkbox = self.getstatusprefixstring(header)
1140 1140 if not header.folded or ignorefolding:
1141 1141 textlist = text.split("\n")
1142 1142 linestr = checkbox + textlist[0]
1143 1143 else:
1144 1144 linestr = checkbox + header.filename()
1145 1145 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1146 1146 towin=towin)
1147 1147 if not header.folded or ignorefolding:
1148 1148 if len(textlist) > 1:
1149 1149 for line in textlist[1:]:
1150 1150 linestr = " "*(indentnumchars + len(checkbox)) + line
1151 1151 outstr += self.printstring(self.chunkpad, linestr,
1152 1152 pair=colorpair, towin=towin)
1153 1153
1154 1154 return outstr
1155 1155
1156 1156 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1157 1157 ignorefolding=False):
1158 1158 "includes start/end line indicator"
1159 1159 outstr = ""
1160 1160 # where hunk is in list of siblings
1161 1161 hunkindex = hunk.header.hunks.index(hunk)
1162 1162
1163 1163 if hunkindex != 0:
1164 1164 # add separating line before headers
1165 1165 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1166 1166 towin=towin, align=False)
1167 1167
1168 1168 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1169 1169 attrlist=[curses.A_BOLD])
1170 1170
1171 1171 # print out from-to line with checkbox
1172 1172 checkbox = self.getstatusprefixstring(hunk)
1173 1173
1174 1174 lineprefix = " "*self.hunkindentnumchars + checkbox
1175 1175 frtoline = " " + hunk.getfromtoline().strip("\n")
1176 1176
1177 1177 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1178 1178 align=False) # add uncolored checkbox/indent
1179 1179 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1180 1180 towin=towin)
1181 1181
1182 1182 if hunk.folded and not ignorefolding:
1183 1183 # skip remainder of output
1184 1184 return outstr
1185 1185
1186 1186 # print out lines of the chunk preceeding changed-lines
1187 1187 for line in hunk.before:
1188 1188 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1189 1189 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1190 1190
1191 1191 return outstr
1192 1192
1193 1193 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1194 1194 outstr = ""
1195 1195 if hunk.folded and not ignorefolding:
1196 1196 return outstr
1197 1197
1198 1198 # a bit superfluous, but to avoid hard-coding indent amount
1199 1199 checkbox = self.getstatusprefixstring(hunk)
1200 1200 for line in hunk.after:
1201 1201 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1202 1202 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1203 1203
1204 1204 return outstr
1205 1205
1206 1206 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1207 1207 outstr = ""
1208 1208 checkbox = self.getstatusprefixstring(hunkline)
1209 1209
1210 1210 linestr = hunkline.prettystr().strip("\n")
1211 1211
1212 1212 # select color-pair based on whether line is an addition/removal
1213 1213 if selected:
1214 1214 colorpair = self.getcolorpair(name="selected")
1215 1215 elif linestr.startswith("+"):
1216 1216 colorpair = self.getcolorpair(name="addition")
1217 1217 elif linestr.startswith("-"):
1218 1218 colorpair = self.getcolorpair(name="deletion")
1219 1219 elif linestr.startswith("\\"):
1220 1220 colorpair = self.getcolorpair(name="normal")
1221 1221
1222 1222 lineprefix = " "*self.hunklineindentnumchars + checkbox
1223 1223 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1224 1224 align=False) # add uncolored checkbox/indent
1225 1225 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1226 1226 towin=towin, showwhtspc=True)
1227 1227 return outstr
1228 1228
1229 1229 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1230 1230 towin=True):
1231 1231 """
1232 1232 use __printitem() to print the the specified item.applied.
1233 1233 if item is not specified, then print the entire patch.
1234 1234 (hiding folded elements, etc. -- see __printitem() docstring)
1235 1235 """
1236 1236
1237 1237 if item is None:
1238 1238 item = self.headerlist
1239 1239 if recursechildren:
1240 1240 self.linesprintedtopadsofar = 0
1241 1241
1242 1242 outstr = []
1243 1243 self.__printitem(item, ignorefolding, recursechildren, outstr,
1244 1244 towin=towin)
1245 1245 return ''.join(outstr)
1246 1246
1247 1247 def outofdisplayedarea(self):
1248 1248 y, _ = self.chunkpad.getyx() # cursor location
1249 1249 # * 2 here works but an optimization would be the max number of
1250 1250 # consecutive non selectable lines
1251 1251 # i.e the max number of context line for any hunk in the patch
1252 1252 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1253 1253 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1254 1254 return y < miny or y > maxy
1255 1255
1256 1256 def handleselection(self, item, recursechildren):
1257 1257 selected = (item is self.currentselecteditem)
1258 1258 if selected and recursechildren:
1259 1259 # assumes line numbering starting from line 0
1260 1260 self.selecteditemstartline = self.linesprintedtopadsofar
1261 1261 selecteditemlines = self.getnumlinesdisplayed(item,
1262 1262 recursechildren=False)
1263 1263 self.selecteditemendline = (self.selecteditemstartline +
1264 1264 selecteditemlines - 1)
1265 1265 return selected
1266 1266
1267 1267 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1268 1268 towin=True):
1269 1269 """
1270 1270 recursive method for printing out patch/header/hunk/hunk-line data to
1271 1271 screen. also returns a string with all of the content of the displayed
1272 1272 patch (not including coloring, etc.).
1273 1273
1274 1274 if ignorefolding is True, then folded items are printed out.
1275 1275
1276 1276 if recursechildren is False, then only print the item without its
1277 1277 child items.
1278 1278 """
1279 1279
1280 1280 if towin and self.outofdisplayedarea():
1281 1281 return
1282 1282
1283 1283 selected = self.handleselection(item, recursechildren)
1284 1284
1285 1285 # patch object is a list of headers
1286 1286 if isinstance(item, patch):
1287 1287 if recursechildren:
1288 1288 for hdr in item:
1289 1289 self.__printitem(hdr, ignorefolding,
1290 1290 recursechildren, outstr, towin)
1291 1291 # todo: eliminate all isinstance() calls
1292 1292 if isinstance(item, uiheader):
1293 1293 outstr.append(self.printheader(item, selected, towin=towin,
1294 1294 ignorefolding=ignorefolding))
1295 1295 if recursechildren:
1296 1296 for hnk in item.hunks:
1297 1297 self.__printitem(hnk, ignorefolding,
1298 1298 recursechildren, outstr, towin)
1299 1299 elif (isinstance(item, uihunk) and
1300 1300 ((not item.header.folded) or ignorefolding)):
1301 1301 # print the hunk data which comes before the changed-lines
1302 1302 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1303 1303 ignorefolding=ignorefolding))
1304 1304 if recursechildren:
1305 1305 for l in item.changedlines:
1306 1306 self.__printitem(l, ignorefolding,
1307 1307 recursechildren, outstr, towin)
1308 1308 outstr.append(self.printhunklinesafter(item, towin=towin,
1309 1309 ignorefolding=ignorefolding))
1310 1310 elif (isinstance(item, uihunkline) and
1311 1311 ((not item.hunk.folded) or ignorefolding)):
1312 1312 outstr.append(self.printhunkchangedline(item, selected,
1313 1313 towin=towin))
1314 1314
1315 1315 return outstr
1316 1316
1317 1317 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1318 1318 recursechildren=True):
1319 1319 """
1320 1320 return the number of lines which would be displayed if the item were
1321 1321 to be printed to the display. the item will not be printed to the
1322 1322 display (pad).
1323 1323 if no item is given, assume the entire patch.
1324 1324 if ignorefolding is True, folded items will be unfolded when counting
1325 1325 the number of lines.
1326 1326 """
1327 1327
1328 1328 # temporarily disable printing to windows by printstring
1329 1329 patchdisplaystring = self.printitem(item, ignorefolding,
1330 1330 recursechildren, towin=False)
1331 1331 numlines = len(patchdisplaystring) / self.xscreensize
1332 1332 return numlines
1333 1333
1334 1334 def sigwinchhandler(self, n, frame):
1335 1335 "handle window resizing"
1336 1336 try:
1337 1337 curses.endwin()
1338 1338 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1339 1339 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1340 1340 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1341 1341 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1342 1342 except curses.error:
1343 1343 pass
1344 1344
1345 1345 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1346 1346 attrlist=None):
1347 1347 """
1348 1348 get a curses color pair, adding it to self.colorpairs if it is not
1349 1349 already defined. an optional string, name, can be passed as a shortcut
1350 1350 for referring to the color-pair. by default, if no arguments are
1351 1351 specified, the white foreground / black background color-pair is
1352 1352 returned.
1353 1353
1354 1354 it is expected that this function will be used exclusively for
1355 1355 initializing color pairs, and not curses.init_pair().
1356 1356
1357 1357 attrlist is used to 'flavor' the returned color-pair. this information
1358 1358 is not stored in self.colorpairs. it contains attribute values like
1359 1359 curses.A_BOLD.
1360 1360 """
1361 1361
1362 1362 if (name is not None) and name in self.colorpairnames:
1363 1363 # then get the associated color pair and return it
1364 1364 colorpair = self.colorpairnames[name]
1365 1365 else:
1366 1366 if fgcolor is None:
1367 1367 fgcolor = -1
1368 1368 if bgcolor is None:
1369 1369 bgcolor = -1
1370 1370 if (fgcolor, bgcolor) in self.colorpairs:
1371 1371 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1372 1372 else:
1373 1373 pairindex = len(self.colorpairs) + 1
1374 1374 curses.init_pair(pairindex, fgcolor, bgcolor)
1375 1375 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1376 1376 curses.color_pair(pairindex))
1377 1377 if name is not None:
1378 1378 self.colorpairnames[name] = curses.color_pair(pairindex)
1379 1379
1380 1380 # add attributes if possible
1381 1381 if attrlist is None:
1382 1382 attrlist = []
1383 1383 if colorpair < 256:
1384 1384 # then it is safe to apply all attributes
1385 1385 for textattr in attrlist:
1386 1386 colorpair |= textattr
1387 1387 else:
1388 1388 # just apply a select few (safe?) attributes
1389 1389 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1390 1390 if textattrib in attrlist:
1391 1391 colorpair |= textattrib
1392 1392 return colorpair
1393 1393
1394 1394 def initcolorpair(self, *args, **kwargs):
1395 1395 "same as getcolorpair."
1396 1396 self.getcolorpair(*args, **kwargs)
1397 1397
1398 1398 def helpwindow(self):
1399 1399 "print a help window to the screen. exit after any keypress."
1400 1400 helptext = _(
1401 1401 """ [press any key to return to the patch-display]
1402 1402
1403 1403 crecord allows you to interactively choose among the changes you have made,
1404 1404 and confirm only those changes you select for further processing by the command
1405 1405 you are running (commit/shelve/revert), after confirming the selected
1406 1406 changes, the unselected changes are still present in your working copy, so you
1407 1407 can use crecord multiple times to split large changes into smaller changesets.
1408 1408 the following are valid keystrokes:
1409 1409
1410 1410 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1411 1411 A : (un-)select all items
1412 1412 up/down-arrow [k/j] : go to previous/next unfolded item
1413 1413 pgup/pgdn [K/J] : go to previous/next item of same type
1414 1414 right/left-arrow [l/h] : go to child item / parent item
1415 1415 shift-left-arrow [H] : go to parent header / fold selected header
1416 1416 f : fold / unfold item, hiding/revealing its children
1417 1417 F : fold / unfold parent item and all of its ancestors
1418 1418 ctrl-l : scroll the selected line to the top of the screen
1419 1419 m : edit / resume editing the commit message
1420 1420 e : edit the currently selected hunk
1421 1421 a : toggle amend mode, only with commit -i
1422 1422 c : confirm selected changes
1423 1423 r : review/edit and confirm selected changes
1424 1424 q : quit without confirming (no changes will be made)
1425 1425 ? : help (what you're currently reading)""")
1426 1426
1427 1427 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1428 1428 helplines = helptext.split("\n")
1429 1429 helplines = helplines + [" "]*(
1430 1430 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1431 1431 try:
1432 1432 for line in helplines:
1433 1433 self.printstring(helpwin, line, pairname="legend")
1434 1434 except curses.error:
1435 1435 pass
1436 1436 helpwin.refresh()
1437 1437 try:
1438 1438 with self.ui.timeblockedsection('crecord'):
1439 1439 helpwin.getkey()
1440 1440 except curses.error:
1441 1441 pass
1442 1442
1443 1443 def commitMessageWindow(self):
1444 1444 "Create a temporary commit message editing window on the screen."
1445 1445
1446 1446 curses.raw()
1447 1447 curses.def_prog_mode()
1448 1448 curses.endwin()
1449 1449 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1450 1450 curses.cbreak()
1451 1451 self.stdscr.refresh()
1452 1452 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1453 1453
1454 1454 def confirmationwindow(self, windowtext):
1455 1455 "display an informational window, then wait for and return a keypress."
1456 1456
1457 1457 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1458 1458 try:
1459 1459 lines = windowtext.split("\n")
1460 1460 for line in lines:
1461 1461 self.printstring(confirmwin, line, pairname="selected")
1462 1462 except curses.error:
1463 1463 pass
1464 1464 self.stdscr.refresh()
1465 1465 confirmwin.refresh()
1466 1466 try:
1467 1467 with self.ui.timeblockedsection('crecord'):
1468 1468 response = chr(self.stdscr.getch())
1469 1469 except ValueError:
1470 1470 response = None
1471 1471
1472 1472 return response
1473 1473
1474 1474 def reviewcommit(self):
1475 1475 """ask for 'y' to be pressed to confirm selected. return True if
1476 1476 confirmed."""
1477 1477 confirmtext = _(
1478 1478 """if you answer yes to the following, the your currently chosen patch chunks
1479 1479 will be loaded into an editor. you may modify the patch from the editor, and
1480 1480 save the changes if you wish to change the patch. otherwise, you can just
1481 1481 close the editor without saving to accept the current patch as-is.
1482 1482
1483 1483 note: don't add/remove lines unless you also modify the range information.
1484 1484 failing to follow this rule will result in the commit aborting.
1485 1485
1486 1486 are you sure you want to review/edit and confirm the selected changes [yn]?
1487 1487 """)
1488 1488 with self.ui.timeblockedsection('crecord'):
1489 1489 response = self.confirmationwindow(confirmtext)
1490 1490 if response is None:
1491 1491 response = "n"
1492 1492 if response.lower().startswith("y"):
1493 1493 return True
1494 1494 else:
1495 1495 return False
1496 1496
1497 1497 def toggleamend(self, opts, test):
1498 1498 """Toggle the amend flag.
1499 1499
1500 1500 When the amend flag is set, a commit will modify the most recently
1501 1501 committed changeset, instead of creating a new changeset. Otherwise, a
1502 1502 new changeset will be created (the normal commit behavior).
1503 1503 """
1504 1504
1505 1505 try:
1506 1506 ver = float(util.version()[:3])
1507 1507 except ValueError:
1508 1508 ver = 1
1509 1509 if ver < 2.19:
1510 1510 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1511 1511 "Press any key to continue.")
1512 1512 elif opts.get('amend') is None:
1513 1513 opts['amend'] = True
1514 1514 msg = _("Amend option is turned on -- committing the currently "
1515 1515 "selected changes will not create a new changeset, but "
1516 1516 "instead update the most recently committed changeset.\n\n"
1517 1517 "Press any key to continue.")
1518 1518 elif opts.get('amend') is True:
1519 1519 opts['amend'] = None
1520 1520 msg = _("Amend option is turned off -- committing the currently "
1521 1521 "selected changes will create a new changeset.\n\n"
1522 1522 "Press any key to continue.")
1523 1523 if not test:
1524 1524 self.confirmationwindow(msg)
1525 1525
1526 1526 def recenterdisplayedarea(self):
1527 1527 """
1528 1528 once we scrolled with pg up pg down we can be pointing outside of the
1529 1529 display zone. we print the patch with towin=False to compute the
1530 1530 location of the selected item even though it is outside of the displayed
1531 1531 zone and then update the scroll.
1532 1532 """
1533 1533 self.printitem(towin=False)
1534 1534 self.updatescroll()
1535 1535
1536 1536 def toggleedit(self, item=None, test=False):
1537 1537 """
1538 1538 edit the currently selected chunk
1539 1539 """
1540 1540 def updateui(self):
1541 1541 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1542 1542 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1543 1543 self.updatescroll()
1544 1544 self.stdscr.refresh()
1545 1545 self.statuswin.refresh()
1546 1546 self.stdscr.keypad(1)
1547 1547
1548 1548 def editpatchwitheditor(self, chunk):
1549 1549 if chunk is None:
1550 1550 self.ui.write(_('cannot edit patch for whole file'))
1551 1551 self.ui.write("\n")
1552 1552 return None
1553 1553 if chunk.header.binary():
1554 1554 self.ui.write(_('cannot edit patch for binary file'))
1555 1555 self.ui.write("\n")
1556 1556 return None
1557 1557
1558 1558 # write the initial patch
1559 1559 patch = stringio()
1560 1560 patch.write(diffhelptext + hunkhelptext)
1561 1561 chunk.header.write(patch)
1562 1562 chunk.write(patch)
1563 1563
1564 1564 # start the editor and wait for it to complete
1565 1565 try:
1566 1566 patch = self.ui.edit(patch.getvalue(), "", action="diff")
1567 1567 except error.Abort as exc:
1568 1568 self.errorstr = str(exc)
1569 1569 return None
1570 1570
1571 1571 # remove comment lines
1572 1572 patch = [line + '\n' for line in patch.splitlines()
1573 1573 if not line.startswith('#')]
1574 1574 return patchmod.parsepatch(patch)
1575 1575
1576 1576 if item is None:
1577 1577 item = self.currentselecteditem
1578 1578 if isinstance(item, uiheader):
1579 1579 return
1580 1580 if isinstance(item, uihunkline):
1581 1581 item = item.parentitem()
1582 1582 if not isinstance(item, uihunk):
1583 1583 return
1584 1584
1585 1585 # To go back to that hunk or its replacement at the end of the edit
1586 1586 itemindex = item.parentitem().hunks.index(item)
1587 1587
1588 1588 beforeadded, beforeremoved = item.added, item.removed
1589 1589 newpatches = editpatchwitheditor(self, item)
1590 1590 if newpatches is None:
1591 1591 if not test:
1592 1592 updateui(self)
1593 1593 return
1594 1594 header = item.header
1595 1595 editedhunkindex = header.hunks.index(item)
1596 1596 hunksbefore = header.hunks[:editedhunkindex]
1597 1597 hunksafter = header.hunks[editedhunkindex + 1:]
1598 1598 newpatchheader = newpatches[0]
1599 1599 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1600 1600 newadded = sum([h.added for h in newhunks])
1601 1601 newremoved = sum([h.removed for h in newhunks])
1602 1602 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1603 1603
1604 1604 for h in hunksafter:
1605 1605 h.toline += offset
1606 1606 for h in newhunks:
1607 1607 h.folded = False
1608 1608 header.hunks = hunksbefore + newhunks + hunksafter
1609 1609 if self.emptypatch():
1610 1610 header.hunks = hunksbefore + [item] + hunksafter
1611 1611 self.currentselecteditem = header
1612 1612 if len(header.hunks) > itemindex:
1613 1613 self.currentselecteditem = header.hunks[itemindex]
1614 1614
1615 1615 if not test:
1616 1616 updateui(self)
1617 1617
1618 1618 def emptypatch(self):
1619 1619 item = self.headerlist
1620 1620 if not item:
1621 1621 return True
1622 1622 for header in item:
1623 1623 if header.hunks:
1624 1624 return False
1625 1625 return True
1626 1626
1627 1627 def handlekeypressed(self, keypressed, test=False):
1628 1628 """
1629 1629 Perform actions based on pressed keys.
1630 1630
1631 1631 Return true to exit the main loop.
1632 1632 """
1633 1633 if keypressed in ["k", "KEY_UP"]:
1634 1634 self.uparrowevent()
1635 1635 if keypressed in ["K", "KEY_PPAGE"]:
1636 1636 self.uparrowshiftevent()
1637 1637 elif keypressed in ["j", "KEY_DOWN"]:
1638 1638 self.downarrowevent()
1639 1639 elif keypressed in ["J", "KEY_NPAGE"]:
1640 1640 self.downarrowshiftevent()
1641 1641 elif keypressed in ["l", "KEY_RIGHT"]:
1642 1642 self.rightarrowevent()
1643 1643 elif keypressed in ["h", "KEY_LEFT"]:
1644 1644 self.leftarrowevent()
1645 1645 elif keypressed in ["H", "KEY_SLEFT"]:
1646 1646 self.leftarrowshiftevent()
1647 1647 elif keypressed in ["q"]:
1648 1648 raise error.Abort(_('user quit'))
1649 1649 elif keypressed in ['a']:
1650 1650 self.toggleamend(self.opts, test)
1651 1651 elif keypressed in ["c"]:
1652 1652 return True
1653 1653 elif test and keypressed in ['X']:
1654 1654 return True
1655 1655 elif keypressed in ["r"]:
1656 1656 if self.reviewcommit():
1657 1657 self.opts['review'] = True
1658 1658 return True
1659 1659 elif test and keypressed in ['R']:
1660 1660 self.opts['review'] = True
1661 1661 return True
1662 1662 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1663 1663 self.toggleapply()
1664 1664 if self.ui.configbool('experimental', 'spacemovesdown'):
1665 1665 self.downarrowevent()
1666 1666 elif keypressed in ['A']:
1667 1667 self.toggleall()
1668 1668 elif keypressed in ['e']:
1669 1669 self.toggleedit(test=test)
1670 1670 elif keypressed in ["f"]:
1671 1671 self.togglefolded()
1672 1672 elif keypressed in ["F"]:
1673 1673 self.togglefolded(foldparent=True)
1674 1674 elif keypressed in ["m"]:
1675 1675 self.commitMessageWindow()
1676 1676 elif keypressed in ["?"]:
1677 1677 self.helpwindow()
1678 1678 self.stdscr.clear()
1679 1679 self.stdscr.refresh()
1680 1680 elif curses.unctrl(keypressed) in ["^L"]:
1681 1681 # scroll the current line to the top of the screen
1682 1682 self.scrolllines(self.selecteditemstartline)
1683 1683
1684 1684 def main(self, stdscr):
1685 1685 """
1686 1686 method to be wrapped by curses.wrapper() for selecting chunks.
1687 1687 """
1688 1688
1689 1689 origsigwinch = sentinel = object()
1690 1690 if util.safehasattr(signal, 'SIGWINCH'):
1691 1691 origsigwinch = signal.signal(signal.SIGWINCH,
1692 1692 self.sigwinchhandler)
1693 1693 try:
1694 1694 return self._main(stdscr)
1695 1695 finally:
1696 1696 if origsigwinch is not sentinel:
1697 1697 signal.signal(signal.SIGWINCH, origsigwinch)
1698 1698
1699 1699 def _main(self, stdscr):
1700 1700 self.stdscr = stdscr
1701 1701 # error during initialization, cannot be printed in the curses
1702 1702 # interface, it should be printed by the calling code
1703 1703 self.initerr = None
1704 1704 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1705 1705
1706 1706 curses.start_color()
1707 1707 curses.use_default_colors()
1708 1708
1709 1709 # available colors: black, blue, cyan, green, magenta, white, yellow
1710 1710 # init_pair(color_id, foreground_color, background_color)
1711 1711 self.initcolorpair(None, None, name="normal")
1712 1712 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1713 1713 name="selected")
1714 1714 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1715 1715 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1716 1716 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1717 1717 # newwin([height, width,] begin_y, begin_x)
1718 1718 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1719 1719 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1720 1720
1721 1721 # figure out how much space to allocate for the chunk-pad which is
1722 1722 # used for displaying the patch
1723 1723
1724 1724 # stupid hack to prevent getnumlinesdisplayed from failing
1725 1725 self.chunkpad = curses.newpad(1, self.xscreensize)
1726 1726
1727 1727 # add 1 so to account for last line text reaching end of line
1728 1728 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1729 1729
1730 1730 try:
1731 1731 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1732 1732 except curses.error:
1733 1733 self.initerr = _('this diff is too large to be displayed')
1734 1734 return
1735 1735 # initialize selecteditemendline (initial start-line is 0)
1736 1736 self.selecteditemendline = self.getnumlinesdisplayed(
1737 1737 self.currentselecteditem, recursechildren=False)
1738 1738
1739 1739 while True:
1740 1740 self.updatescreen()
1741 1741 try:
1742 1742 with self.ui.timeblockedsection('crecord'):
1743 1743 keypressed = self.statuswin.getkey()
1744 1744 if self.errorstr is not None:
1745 1745 self.errorstr = None
1746 1746 continue
1747 1747 except curses.error:
1748 1748 keypressed = "foobar"
1749 1749 if self.handlekeypressed(keypressed):
1750 1750 break
1751 1751
1752 1752 if self.commenttext != "":
1753 1753 whitespaceremoved = re.sub("(?m)^\s.*(\n|$)", "", self.commenttext)
1754 1754 if whitespaceremoved != "":
1755 1755 self.opts['message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now