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