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