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