|
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 |