Mercurial > hg
comparison mercurial/crecord.py @ 24310:6409fb6c934d
record: add crecord's ui logic to core
Code adapted from https://bitbucket.org/edgimar/crecord/src to respect
coding convention an record's interface
author | Laurent Charignon <lcharignon@fb.com> |
---|---|
date | Thu, 12 Mar 2015 14:19:11 -0700 |
parents | |
children | ed535f2c15c3 |
comparison
equal
deleted
inserted
replaced
24309:fefcafda10b8 | 24310:6409fb6c934d |
---|---|
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 |