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