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