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