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