##// END OF EJS Templates
crecord: add mechanism for error reporting...
Laurent Charignon -
r25556:40f0e9e5 default
parent child Browse files
Show More
@@ -1,1607 +1,1617 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 self.errorstr = None
514 515 # list of all chunks
515 516 self.chunklist = []
516 517 for h in headerlist:
517 518 self.chunklist.append(h)
518 519 self.chunklist.extend(h.hunks)
519 520
520 521 # dictionary mapping (fgcolor, bgcolor) pairs to the
521 522 # corresponding curses color-pair value.
522 523 self.colorpairs = {}
523 524 # maps custom nicknames of color-pairs to curses color-pair values
524 525 self.colorpairnames = {}
525 526
526 527 # the currently selected header, hunk, or hunk-line
527 528 self.currentselecteditem = self.headerlist[0]
528 529
529 530 # updated when printing out patch-display -- the 'lines' here are the
530 531 # line positions *in the pad*, not on the screen.
531 532 self.selecteditemstartline = 0
532 533 self.selecteditemendline = None
533 534
534 535 # define indentation levels
535 536 self.headerindentnumchars = 0
536 537 self.hunkindentnumchars = 3
537 538 self.hunklineindentnumchars = 6
538 539
539 540 # the first line of the pad to print to the screen
540 541 self.firstlineofpadtoprint = 0
541 542
542 543 # keeps track of the number of lines in the pad
543 544 self.numpadlines = None
544 545
545 546 self.numstatuslines = 2
546 547
547 548 # keep a running count of the number of lines printed to the pad
548 549 # (used for determining when the selected item begins/ends)
549 550 self.linesprintedtopadsofar = 0
550 551
551 552 # the first line of the pad which is visible on the screen
552 553 self.firstlineofpadtoprint = 0
553 554
554 555 # stores optional text for a commit comment provided by the user
555 556 self.commenttext = ""
556 557
557 558 # if the last 'toggle all' command caused all changes to be applied
558 559 self.waslasttoggleallapplied = True
559 560
560 561 def uparrowevent(self):
561 562 """
562 563 try to select the previous item to the current item that has the
563 564 most-indented level. for example, if a hunk is selected, try to select
564 565 the last hunkline of the hunk prior to the selected hunk. or, if
565 566 the first hunkline of a hunk is currently selected, then select the
566 567 hunk itself.
567 568
568 569 if the currently selected item is already at the top of the screen,
569 570 scroll the screen down to show the new-selected item.
570 571
571 572 """
572 573 currentitem = self.currentselecteditem
573 574
574 575 nextitem = currentitem.previtem(constrainlevel=False)
575 576
576 577 if nextitem is None:
577 578 # if no parent item (i.e. currentitem is the first header), then
578 579 # no change...
579 580 nextitem = currentitem
580 581
581 582 self.currentselecteditem = nextitem
582 583
583 584 def uparrowshiftevent(self):
584 585 """
585 586 select (if possible) the previous item on the same level as the
586 587 currently selected item. otherwise, select (if possible) the
587 588 parent-item of the currently selected item.
588 589
589 590 if the currently selected item is already at the top of the screen,
590 591 scroll the screen down to show the new-selected item.
591 592
592 593 """
593 594 currentitem = self.currentselecteditem
594 595 nextitem = currentitem.previtem()
595 596 # if there's no previous item on this level, try choosing the parent
596 597 if nextitem is None:
597 598 nextitem = currentitem.parentitem()
598 599 if nextitem is None:
599 600 # if no parent item (i.e. currentitem is the first header), then
600 601 # no change...
601 602 nextitem = currentitem
602 603
603 604 self.currentselecteditem = nextitem
604 605
605 606 def downarrowevent(self):
606 607 """
607 608 try to select the next item to the current item that has the
608 609 most-indented level. for example, if a hunk is selected, select
609 610 the first hunkline of the selected hunk. or, if the last hunkline of
610 611 a hunk is currently selected, then select the next hunk, if one exists,
611 612 or if not, the next header if one exists.
612 613
613 614 if the currently selected item is already at the bottom of the screen,
614 615 scroll the screen up to show the new-selected item.
615 616
616 617 """
617 618 #self.startprintline += 1 #debug
618 619 currentitem = self.currentselecteditem
619 620
620 621 nextitem = currentitem.nextitem(constrainlevel=False)
621 622 # if there's no next item, keep the selection as-is
622 623 if nextitem is None:
623 624 nextitem = currentitem
624 625
625 626 self.currentselecteditem = nextitem
626 627
627 628 def downarrowshiftevent(self):
628 629 """
629 630 if the cursor is already at the bottom chunk, scroll the screen up and
630 631 move the cursor-position to the subsequent chunk. otherwise, only move
631 632 the cursor position down one chunk.
632 633
633 634 """
634 635 # todo: update docstring
635 636
636 637 currentitem = self.currentselecteditem
637 638 nextitem = currentitem.nextitem()
638 639 # if there's no previous item on this level, try choosing the parent's
639 640 # nextitem.
640 641 if nextitem is None:
641 642 try:
642 643 nextitem = currentitem.parentitem().nextitem()
643 644 except AttributeError:
644 645 # parentitem returned None, so nextitem() can't be called
645 646 nextitem = None
646 647 if nextitem is None:
647 648 # if no next item on parent-level, then no change...
648 649 nextitem = currentitem
649 650
650 651 self.currentselecteditem = nextitem
651 652
652 653 def rightarrowevent(self):
653 654 """
654 655 select (if possible) the first of this item's child-items.
655 656
656 657 """
657 658 currentitem = self.currentselecteditem
658 659 nextitem = currentitem.firstchild()
659 660
660 661 # turn off folding if we want to show a child-item
661 662 if currentitem.folded:
662 663 self.togglefolded(currentitem)
663 664
664 665 if nextitem is None:
665 666 # if no next item on parent-level, then no change...
666 667 nextitem = currentitem
667 668
668 669 self.currentselecteditem = nextitem
669 670
670 671 def leftarrowevent(self):
671 672 """
672 673 if the current item can be folded (i.e. it is an unfolded header or
673 674 hunk), then fold it. otherwise try select (if possible) the parent
674 675 of this item.
675 676
676 677 """
677 678 currentitem = self.currentselecteditem
678 679
679 680 # try to fold the item
680 681 if not isinstance(currentitem, uihunkline):
681 682 if not currentitem.folded:
682 683 self.togglefolded(item=currentitem)
683 684 return
684 685
685 686 # if it can't be folded, try to select the parent item
686 687 nextitem = currentitem.parentitem()
687 688
688 689 if nextitem is None:
689 690 # if no item on parent-level, then no change...
690 691 nextitem = currentitem
691 692 if not nextitem.folded:
692 693 self.togglefolded(item=nextitem)
693 694
694 695 self.currentselecteditem = nextitem
695 696
696 697 def leftarrowshiftevent(self):
697 698 """
698 699 select the header of the current item (or fold current item if the
699 700 current item is already a header).
700 701
701 702 """
702 703 currentitem = self.currentselecteditem
703 704
704 705 if isinstance(currentitem, uiheader):
705 706 if not currentitem.folded:
706 707 self.togglefolded(item=currentitem)
707 708 return
708 709
709 710 # select the parent item recursively until we're at a header
710 711 while True:
711 712 nextitem = currentitem.parentitem()
712 713 if nextitem is None:
713 714 break
714 715 else:
715 716 currentitem = nextitem
716 717
717 718 self.currentselecteditem = currentitem
718 719
719 720 def updatescroll(self):
720 721 "scroll the screen to fully show the currently-selected"
721 722 selstart = self.selecteditemstartline
722 723 selend = self.selecteditemendline
723 724 #selnumlines = selend - selstart
724 725 padstart = self.firstlineofpadtoprint
725 726 padend = padstart + self.yscreensize - self.numstatuslines - 1
726 727 # 'buffered' pad start/end values which scroll with a certain
727 728 # top/bottom context margin
728 729 padstartbuffered = padstart + 3
729 730 padendbuffered = padend - 3
730 731
731 732 if selend > padendbuffered:
732 733 self.scrolllines(selend - padendbuffered)
733 734 elif selstart < padstartbuffered:
734 735 # negative values scroll in pgup direction
735 736 self.scrolllines(selstart - padstartbuffered)
736 737
737 738
738 739 def scrolllines(self, numlines):
739 740 "scroll the screen up (down) by numlines when numlines >0 (<0)."
740 741 self.firstlineofpadtoprint += numlines
741 742 if self.firstlineofpadtoprint < 0:
742 743 self.firstlineofpadtoprint = 0
743 744 if self.firstlineofpadtoprint > self.numpadlines - 1:
744 745 self.firstlineofpadtoprint = self.numpadlines - 1
745 746
746 747 def toggleapply(self, item=None):
747 748 """
748 749 toggle the applied flag of the specified item. if no item is specified,
749 750 toggle the flag of the currently selected item.
750 751
751 752 """
752 753 if item is None:
753 754 item = self.currentselecteditem
754 755
755 756 item.applied = not item.applied
756 757
757 758 if isinstance(item, uiheader):
758 759 item.partial = False
759 760 if item.applied:
760 761 # apply all its hunks
761 762 for hnk in item.hunks:
762 763 hnk.applied = True
763 764 # apply all their hunklines
764 765 for hunkline in hnk.changedlines:
765 766 hunkline.applied = True
766 767 else:
767 768 # un-apply all its hunks
768 769 for hnk in item.hunks:
769 770 hnk.applied = False
770 771 hnk.partial = False
771 772 # un-apply all their hunklines
772 773 for hunkline in hnk.changedlines:
773 774 hunkline.applied = False
774 775 elif isinstance(item, uihunk):
775 776 item.partial = False
776 777 # apply all it's hunklines
777 778 for hunkline in item.changedlines:
778 779 hunkline.applied = item.applied
779 780
780 781 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
781 782 allsiblingsapplied = not (False in siblingappliedstatus)
782 783 nosiblingsapplied = not (True in siblingappliedstatus)
783 784
784 785 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
785 786 somesiblingspartial = (True in siblingspartialstatus)
786 787
787 788 #cases where applied or partial should be removed from header
788 789
789 790 # if no 'sibling' hunks are applied (including this hunk)
790 791 if nosiblingsapplied:
791 792 if not item.header.special():
792 793 item.header.applied = False
793 794 item.header.partial = False
794 795 else: # some/all parent siblings are applied
795 796 item.header.applied = True
796 797 item.header.partial = (somesiblingspartial or
797 798 not allsiblingsapplied)
798 799
799 800 elif isinstance(item, uihunkline):
800 801 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
801 802 allsiblingsapplied = not (False in siblingappliedstatus)
802 803 nosiblingsapplied = not (True in siblingappliedstatus)
803 804
804 805 # if no 'sibling' lines are applied
805 806 if nosiblingsapplied:
806 807 item.hunk.applied = False
807 808 item.hunk.partial = False
808 809 elif allsiblingsapplied:
809 810 item.hunk.applied = True
810 811 item.hunk.partial = False
811 812 else: # some siblings applied
812 813 item.hunk.applied = True
813 814 item.hunk.partial = True
814 815
815 816 parentsiblingsapplied = [hnk.applied for hnk
816 817 in item.hunk.header.hunks]
817 818 noparentsiblingsapplied = not (True in parentsiblingsapplied)
818 819 allparentsiblingsapplied = not (False in parentsiblingsapplied)
819 820
820 821 parentsiblingspartial = [hnk.partial for hnk
821 822 in item.hunk.header.hunks]
822 823 someparentsiblingspartial = (True in parentsiblingspartial)
823 824
824 825 # if all parent hunks are not applied, un-apply header
825 826 if noparentsiblingsapplied:
826 827 if not item.hunk.header.special():
827 828 item.hunk.header.applied = False
828 829 item.hunk.header.partial = False
829 830 # set the applied and partial status of the header if needed
830 831 else: # some/all parent siblings are applied
831 832 item.hunk.header.applied = True
832 833 item.hunk.header.partial = (someparentsiblingspartial or
833 834 not allparentsiblingsapplied)
834 835
835 836 def toggleall(self):
836 837 "toggle the applied flag of all items."
837 838 if self.waslasttoggleallapplied: # then unapply them this time
838 839 for item in self.headerlist:
839 840 if item.applied:
840 841 self.toggleapply(item)
841 842 else:
842 843 for item in self.headerlist:
843 844 if not item.applied:
844 845 self.toggleapply(item)
845 846 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
846 847
847 848 def togglefolded(self, item=None, foldparent=False):
848 849 "toggle folded flag of specified item (defaults to currently selected)"
849 850 if item is None:
850 851 item = self.currentselecteditem
851 852 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
852 853 if not isinstance(item, uiheader):
853 854 # we need to select the parent item in this case
854 855 self.currentselecteditem = item = item.parentitem()
855 856 elif item.neverunfolded:
856 857 item.neverunfolded = False
857 858
858 859 # also fold any foldable children of the parent/current item
859 860 if isinstance(item, uiheader): # the original or 'new' item
860 861 for child in item.allchildren():
861 862 child.folded = not item.folded
862 863
863 864 if isinstance(item, (uiheader, uihunk)):
864 865 item.folded = not item.folded
865 866
866 867
867 868 def alignstring(self, instr, window):
868 869 """
869 870 add whitespace to the end of a string in order to make it fill
870 871 the screen in the x direction. the current cursor position is
871 872 taken into account when making this calculation. the string can span
872 873 multiple lines.
873 874
874 875 """
875 876 y, xstart = window.getyx()
876 877 width = self.xscreensize
877 878 # turn tabs into spaces
878 879 instr = instr.expandtabs(4)
879 880 strwidth = encoding.colwidth(instr)
880 881 numspaces = (width - ((strwidth + xstart) % width) - 1)
881 882 return instr + " " * numspaces + "\n"
882 883
883 884 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
884 885 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
885 886 """
886 887 print the string, text, with the specified colors and attributes, to
887 888 the specified curses window object.
888 889
889 890 the foreground and background colors are of the form
890 891 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
891 892 magenta, red, white, yellow]. if pairname is provided, a color
892 893 pair will be looked up in the self.colorpairnames dictionary.
893 894
894 895 attrlist is a list containing text attributes in the form of
895 896 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
896 897 underline].
897 898
898 899 if align == True, whitespace is added to the printed string such that
899 900 the string stretches to the right border of the window.
900 901
901 902 if showwhtspc == True, trailing whitespace of a string is highlighted.
902 903
903 904 """
904 905 # preprocess the text, converting tabs to spaces
905 906 text = text.expandtabs(4)
906 907 # strip \n, and convert control characters to ^[char] representation
907 908 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
908 909 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
909 910
910 911 if pair is not None:
911 912 colorpair = pair
912 913 elif pairname is not None:
913 914 colorpair = self.colorpairnames[pairname]
914 915 else:
915 916 if fgcolor is None:
916 917 fgcolor = -1
917 918 if bgcolor is None:
918 919 bgcolor = -1
919 920 if (fgcolor, bgcolor) in self.colorpairs:
920 921 colorpair = self.colorpairs[(fgcolor, bgcolor)]
921 922 else:
922 923 colorpair = self.getcolorpair(fgcolor, bgcolor)
923 924 # add attributes if possible
924 925 if attrlist is None:
925 926 attrlist = []
926 927 if colorpair < 256:
927 928 # then it is safe to apply all attributes
928 929 for textattr in attrlist:
929 930 colorpair |= textattr
930 931 else:
931 932 # just apply a select few (safe?) attributes
932 933 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
933 934 if textattr in attrlist:
934 935 colorpair |= textattr
935 936
936 937 y, xstart = self.chunkpad.getyx()
937 938 t = "" # variable for counting lines printed
938 939 # if requested, show trailing whitespace
939 940 if showwhtspc:
940 941 origlen = len(text)
941 942 text = text.rstrip(' \n') # tabs have already been expanded
942 943 strippedlen = len(text)
943 944 numtrailingspaces = origlen - strippedlen
944 945
945 946 if towin:
946 947 window.addstr(text, colorpair)
947 948 t += text
948 949
949 950 if showwhtspc:
950 951 wscolorpair = colorpair | curses.A_REVERSE
951 952 if towin:
952 953 for i in range(numtrailingspaces):
953 954 window.addch(curses.ACS_CKBOARD, wscolorpair)
954 955 t += " " * numtrailingspaces
955 956
956 957 if align:
957 958 if towin:
958 959 extrawhitespace = self.alignstring("", window)
959 960 window.addstr(extrawhitespace, colorpair)
960 961 else:
961 962 # need to use t, since the x position hasn't incremented
962 963 extrawhitespace = self.alignstring(t, window)
963 964 t += extrawhitespace
964 965
965 966 # is reset to 0 at the beginning of printitem()
966 967
967 968 linesprinted = (xstart + len(t)) / self.xscreensize
968 969 self.linesprintedtopadsofar += linesprinted
969 970 return t
970 971
971 972 def updatescreen(self):
972 973 self.statuswin.erase()
973 974 self.chunkpad.erase()
974 975
975 976 printstring = self.printstring
976 977
977 978 # print out the status lines at the top
978 979 try:
980 if self.errorstr is not None:
981 printstring(self.statuswin, self.errorstr, pairname='legend')
982 printstring(self.statuswin, 'Press any key to continue',
983 pairname='legend')
984 self.statuswin.refresh()
985 return
979 986 printstring(self.statuswin,
980 987 "SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; "
981 988 "(space/A) toggle hunk/all; (e)dit hunk;",
982 989 pairname="legend")
983 990 printstring(self.statuswin,
984 991 " (f)old/unfold; (c)onfirm applied; (q)uit; (?) help "
985 992 "| [X]=hunk applied **=folded",
986 993 pairname="legend")
987 994 except curses.error:
988 995 pass
989 996
990 997 # print out the patch in the remaining part of the window
991 998 try:
992 999 self.printitem()
993 1000 self.updatescroll()
994 1001 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
995 1002 self.numstatuslines, 0,
996 1003 self.yscreensize + 1 - self.numstatuslines,
997 1004 self.xscreensize)
998 1005 except curses.error:
999 1006 pass
1000 1007
1001 1008 # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
1002 1009 self.statuswin.refresh()
1003 1010
1004 1011 def getstatusprefixstring(self, item):
1005 1012 """
1006 1013 create a string to prefix a line with which indicates whether 'item'
1007 1014 is applied and/or folded.
1008 1015
1009 1016 """
1010 1017 # create checkbox string
1011 1018 if item.applied:
1012 1019 if not isinstance(item, uihunkline) and item.partial:
1013 1020 checkbox = "[~]"
1014 1021 else:
1015 1022 checkbox = "[x]"
1016 1023 else:
1017 1024 checkbox = "[ ]"
1018 1025
1019 1026 try:
1020 1027 if item.folded:
1021 1028 checkbox += "**"
1022 1029 if isinstance(item, uiheader):
1023 1030 # one of "m", "a", or "d" (modified, added, deleted)
1024 1031 filestatus = item.changetype
1025 1032
1026 1033 checkbox += filestatus + " "
1027 1034 else:
1028 1035 checkbox += " "
1029 1036 if isinstance(item, uiheader):
1030 1037 # add two more spaces for headers
1031 1038 checkbox += " "
1032 1039 except AttributeError: # not foldable
1033 1040 checkbox += " "
1034 1041
1035 1042 return checkbox
1036 1043
1037 1044 def printheader(self, header, selected=False, towin=True,
1038 1045 ignorefolding=False):
1039 1046 """
1040 1047 print the header to the pad. if countlines is True, don't print
1041 1048 anything, but just count the number of lines which would be printed.
1042 1049
1043 1050 """
1044 1051 outstr = ""
1045 1052 text = header.prettystr()
1046 1053 chunkindex = self.chunklist.index(header)
1047 1054
1048 1055 if chunkindex != 0 and not header.folded:
1049 1056 # add separating line before headers
1050 1057 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1051 1058 towin=towin, align=False)
1052 1059 # select color-pair based on if the header is selected
1053 1060 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1054 1061 attrlist=[curses.A_BOLD])
1055 1062
1056 1063 # print out each line of the chunk, expanding it to screen width
1057 1064
1058 1065 # number of characters to indent lines on this level by
1059 1066 indentnumchars = 0
1060 1067 checkbox = self.getstatusprefixstring(header)
1061 1068 if not header.folded or ignorefolding:
1062 1069 textlist = text.split("\n")
1063 1070 linestr = checkbox + textlist[0]
1064 1071 else:
1065 1072 linestr = checkbox + header.filename()
1066 1073 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1067 1074 towin=towin)
1068 1075 if not header.folded or ignorefolding:
1069 1076 if len(textlist) > 1:
1070 1077 for line in textlist[1:]:
1071 1078 linestr = " "*(indentnumchars + len(checkbox)) + line
1072 1079 outstr += self.printstring(self.chunkpad, linestr,
1073 1080 pair=colorpair, towin=towin)
1074 1081
1075 1082 return outstr
1076 1083
1077 1084 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1078 1085 ignorefolding=False):
1079 1086 "includes start/end line indicator"
1080 1087 outstr = ""
1081 1088 # where hunk is in list of siblings
1082 1089 hunkindex = hunk.header.hunks.index(hunk)
1083 1090
1084 1091 if hunkindex != 0:
1085 1092 # add separating line before headers
1086 1093 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1087 1094 towin=towin, align=False)
1088 1095
1089 1096 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1090 1097 attrlist=[curses.A_BOLD])
1091 1098
1092 1099 # print out from-to line with checkbox
1093 1100 checkbox = self.getstatusprefixstring(hunk)
1094 1101
1095 1102 lineprefix = " "*self.hunkindentnumchars + checkbox
1096 1103 frtoline = " " + hunk.getfromtoline().strip("\n")
1097 1104
1098 1105
1099 1106 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1100 1107 align=False) # add uncolored checkbox/indent
1101 1108 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1102 1109 towin=towin)
1103 1110
1104 1111 if hunk.folded and not ignorefolding:
1105 1112 # skip remainder of output
1106 1113 return outstr
1107 1114
1108 1115 # print out lines of the chunk preceeding changed-lines
1109 1116 for line in hunk.before:
1110 1117 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1111 1118 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1112 1119
1113 1120 return outstr
1114 1121
1115 1122 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1116 1123 outstr = ""
1117 1124 if hunk.folded and not ignorefolding:
1118 1125 return outstr
1119 1126
1120 1127 # a bit superfluous, but to avoid hard-coding indent amount
1121 1128 checkbox = self.getstatusprefixstring(hunk)
1122 1129 for line in hunk.after:
1123 1130 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1124 1131 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1125 1132
1126 1133 return outstr
1127 1134
1128 1135 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1129 1136 outstr = ""
1130 1137 checkbox = self.getstatusprefixstring(hunkline)
1131 1138
1132 1139 linestr = hunkline.prettystr().strip("\n")
1133 1140
1134 1141 # select color-pair based on whether line is an addition/removal
1135 1142 if selected:
1136 1143 colorpair = self.getcolorpair(name="selected")
1137 1144 elif linestr.startswith("+"):
1138 1145 colorpair = self.getcolorpair(name="addition")
1139 1146 elif linestr.startswith("-"):
1140 1147 colorpair = self.getcolorpair(name="deletion")
1141 1148 elif linestr.startswith("\\"):
1142 1149 colorpair = self.getcolorpair(name="normal")
1143 1150
1144 1151 lineprefix = " "*self.hunklineindentnumchars + checkbox
1145 1152 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1146 1153 align=False) # add uncolored checkbox/indent
1147 1154 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1148 1155 towin=towin, showwhtspc=True)
1149 1156 return outstr
1150 1157
1151 1158 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1152 1159 towin=True):
1153 1160 """
1154 1161 use __printitem() to print the the specified item.applied.
1155 1162 if item is not specified, then print the entire patch.
1156 1163 (hiding folded elements, etc. -- see __printitem() docstring)
1157 1164 """
1158 1165 if item is None:
1159 1166 item = self.headerlist
1160 1167 if recursechildren:
1161 1168 self.linesprintedtopadsofar = 0
1162 1169
1163 1170 outstr = []
1164 1171 self.__printitem(item, ignorefolding, recursechildren, outstr,
1165 1172 towin=towin)
1166 1173 return ''.join(outstr)
1167 1174
1168 1175 def outofdisplayedarea(self):
1169 1176 y, _ = self.chunkpad.getyx() # cursor location
1170 1177 # * 2 here works but an optimization would be the max number of
1171 1178 # consecutive non selectable lines
1172 1179 # i.e the max number of context line for any hunk in the patch
1173 1180 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1174 1181 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1175 1182 return y < miny or y > maxy
1176 1183
1177 1184 def handleselection(self, item, recursechildren):
1178 1185 selected = (item is self.currentselecteditem)
1179 1186 if selected and recursechildren:
1180 1187 # assumes line numbering starting from line 0
1181 1188 self.selecteditemstartline = self.linesprintedtopadsofar
1182 1189 selecteditemlines = self.getnumlinesdisplayed(item,
1183 1190 recursechildren=False)
1184 1191 self.selecteditemendline = (self.selecteditemstartline +
1185 1192 selecteditemlines - 1)
1186 1193 return selected
1187 1194
1188 1195 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1189 1196 towin=True):
1190 1197 """
1191 1198 recursive method for printing out patch/header/hunk/hunk-line data to
1192 1199 screen. also returns a string with all of the content of the displayed
1193 1200 patch (not including coloring, etc.).
1194 1201
1195 1202 if ignorefolding is True, then folded items are printed out.
1196 1203
1197 1204 if recursechildren is False, then only print the item without its
1198 1205 child items.
1199 1206
1200 1207 """
1201 1208 if towin and self.outofdisplayedarea():
1202 1209 return
1203 1210
1204 1211 selected = self.handleselection(item, recursechildren)
1205 1212
1206 1213 # patch object is a list of headers
1207 1214 if isinstance(item, patch):
1208 1215 if recursechildren:
1209 1216 for hdr in item:
1210 1217 self.__printitem(hdr, ignorefolding,
1211 1218 recursechildren, outstr, towin)
1212 1219 # todo: eliminate all isinstance() calls
1213 1220 if isinstance(item, uiheader):
1214 1221 outstr.append(self.printheader(item, selected, towin=towin,
1215 1222 ignorefolding=ignorefolding))
1216 1223 if recursechildren:
1217 1224 for hnk in item.hunks:
1218 1225 self.__printitem(hnk, ignorefolding,
1219 1226 recursechildren, outstr, towin)
1220 1227 elif (isinstance(item, uihunk) and
1221 1228 ((not item.header.folded) or ignorefolding)):
1222 1229 # print the hunk data which comes before the changed-lines
1223 1230 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1224 1231 ignorefolding=ignorefolding))
1225 1232 if recursechildren:
1226 1233 for l in item.changedlines:
1227 1234 self.__printitem(l, ignorefolding,
1228 1235 recursechildren, outstr, towin)
1229 1236 outstr.append(self.printhunklinesafter(item, towin=towin,
1230 1237 ignorefolding=ignorefolding))
1231 1238 elif (isinstance(item, uihunkline) and
1232 1239 ((not item.hunk.folded) or ignorefolding)):
1233 1240 outstr.append(self.printhunkchangedline(item, selected,
1234 1241 towin=towin))
1235 1242
1236 1243 return outstr
1237 1244
1238 1245 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1239 1246 recursechildren=True):
1240 1247 """
1241 1248 return the number of lines which would be displayed if the item were
1242 1249 to be printed to the display. the item will not be printed to the
1243 1250 display (pad).
1244 1251 if no item is given, assume the entire patch.
1245 1252 if ignorefolding is True, folded items will be unfolded when counting
1246 1253 the number of lines.
1247 1254
1248 1255 """
1249 1256 # temporarily disable printing to windows by printstring
1250 1257 patchdisplaystring = self.printitem(item, ignorefolding,
1251 1258 recursechildren, towin=False)
1252 1259 numlines = len(patchdisplaystring) / self.xscreensize
1253 1260 return numlines
1254 1261
1255 1262 def sigwinchhandler(self, n, frame):
1256 1263 "handle window resizing"
1257 1264 try:
1258 1265 curses.endwin()
1259 1266 self.yscreensize, self.xscreensize = gethw()
1260 1267 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1261 1268 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1262 1269 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1263 1270 # todo: try to resize commit message window if possible
1264 1271 except curses.error:
1265 1272 pass
1266 1273
1267 1274 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1268 1275 attrlist=None):
1269 1276 """
1270 1277 get a curses color pair, adding it to self.colorpairs if it is not
1271 1278 already defined. an optional string, name, can be passed as a shortcut
1272 1279 for referring to the color-pair. by default, if no arguments are
1273 1280 specified, the white foreground / black background color-pair is
1274 1281 returned.
1275 1282
1276 1283 it is expected that this function will be used exclusively for
1277 1284 initializing color pairs, and not curses.init_pair().
1278 1285
1279 1286 attrlist is used to 'flavor' the returned color-pair. this information
1280 1287 is not stored in self.colorpairs. it contains attribute values like
1281 1288 curses.A_BOLD.
1282 1289
1283 1290 """
1284 1291 if (name is not None) and name in self.colorpairnames:
1285 1292 # then get the associated color pair and return it
1286 1293 colorpair = self.colorpairnames[name]
1287 1294 else:
1288 1295 if fgcolor is None:
1289 1296 fgcolor = -1
1290 1297 if bgcolor is None:
1291 1298 bgcolor = -1
1292 1299 if (fgcolor, bgcolor) in self.colorpairs:
1293 1300 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1294 1301 else:
1295 1302 pairindex = len(self.colorpairs) + 1
1296 1303 curses.init_pair(pairindex, fgcolor, bgcolor)
1297 1304 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1298 1305 curses.color_pair(pairindex))
1299 1306 if name is not None:
1300 1307 self.colorpairnames[name] = curses.color_pair(pairindex)
1301 1308
1302 1309 # add attributes if possible
1303 1310 if attrlist is None:
1304 1311 attrlist = []
1305 1312 if colorpair < 256:
1306 1313 # then it is safe to apply all attributes
1307 1314 for textattr in attrlist:
1308 1315 colorpair |= textattr
1309 1316 else:
1310 1317 # just apply a select few (safe?) attributes
1311 1318 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1312 1319 if textattrib in attrlist:
1313 1320 colorpair |= textattrib
1314 1321 return colorpair
1315 1322
1316 1323 def initcolorpair(self, *args, **kwargs):
1317 1324 "same as getcolorpair."
1318 1325 self.getcolorpair(*args, **kwargs)
1319 1326
1320 1327 def helpwindow(self):
1321 1328 "print a help window to the screen. exit after any keypress."
1322 1329 helptext = """ [press any key to return to the patch-display]
1323 1330
1324 1331 crecord allows you to interactively choose among the changes you have made,
1325 1332 and confirm only those changes you select for further processing by the command
1326 1333 you are running (commit/shelve/revert), after confirming the selected
1327 1334 changes, the unselected changes are still present in your working copy, so you
1328 1335 can use crecord multiple times to split large changes into smaller changesets.
1329 1336 the following are valid keystrokes:
1330 1337
1331 1338 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1332 1339 a : (un-)select all items
1333 1340 up/down-arrow [k/j] : go to previous/next unfolded item
1334 1341 pgup/pgdn [K/J] : go to previous/next item of same type
1335 1342 right/left-arrow [l/h] : go to child item / parent item
1336 1343 shift-left-arrow [H] : go to parent header / fold selected header
1337 1344 f : fold / unfold item, hiding/revealing its children
1338 1345 F : fold / unfold parent item and all of its ancestors
1339 1346 m : edit / resume editing the commit message
1340 1347 e : edit the currently selected hunk
1341 1348 a : toggle amend mode (hg rev >= 2.2)
1342 1349 c : confirm selected changes
1343 1350 r : review/edit and confirm selected changes
1344 1351 q : quit without confirming (no changes will be made)
1345 1352 ? : help (what you're currently reading)"""
1346 1353
1347 1354 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1348 1355 helplines = helptext.split("\n")
1349 1356 helplines = helplines + [" "]*(
1350 1357 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1351 1358 try:
1352 1359 for line in helplines:
1353 1360 self.printstring(helpwin, line, pairname="legend")
1354 1361 except curses.error:
1355 1362 pass
1356 1363 helpwin.refresh()
1357 1364 try:
1358 1365 helpwin.getkey()
1359 1366 except curses.error:
1360 1367 pass
1361 1368
1362 1369 def confirmationwindow(self, windowtext):
1363 1370 "display an informational window, then wait for and return a keypress."
1364 1371
1365 1372 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1366 1373 try:
1367 1374 lines = windowtext.split("\n")
1368 1375 for line in lines:
1369 1376 self.printstring(confirmwin, line, pairname="selected")
1370 1377 except curses.error:
1371 1378 pass
1372 1379 self.stdscr.refresh()
1373 1380 confirmwin.refresh()
1374 1381 try:
1375 1382 response = chr(self.stdscr.getch())
1376 1383 except ValueError:
1377 1384 response = None
1378 1385
1379 1386 return response
1380 1387
1381 1388 def confirmcommit(self, review=False):
1382 1389 """ask for 'y' to be pressed to confirm selected. return True if
1383 1390 confirmed."""
1384 1391 if review:
1385 1392 confirmtext = (
1386 1393 """if you answer yes to the following, the your currently chosen patch chunks
1387 1394 will be loaded into an editor. you may modify the patch from the editor, and
1388 1395 save the changes if you wish to change the patch. otherwise, you can just
1389 1396 close the editor without saving to accept the current patch as-is.
1390 1397
1391 1398 note: don't add/remove lines unless you also modify the range information.
1392 1399 failing to follow this rule will result in the commit aborting.
1393 1400
1394 1401 are you sure you want to review/edit and confirm the selected changes [yn]?
1395 1402 """)
1396 1403 else:
1397 1404 confirmtext = (
1398 1405 "are you sure you want to confirm the selected changes [yn]? ")
1399 1406
1400 1407 response = self.confirmationwindow(confirmtext)
1401 1408 if response is None:
1402 1409 response = "n"
1403 1410 if response.lower().startswith("y"):
1404 1411 return True
1405 1412 else:
1406 1413 return False
1407 1414
1408 1415 def recenterdisplayedarea(self):
1409 1416 """
1410 1417 once we scrolled with pg up pg down we can be pointing outside of the
1411 1418 display zone. we print the patch with towin=False to compute the
1412 1419 location of the selected item eventhough it is outside of the displayed
1413 1420 zone and then update the scroll.
1414 1421 """
1415 1422 self.printitem(towin=False)
1416 1423 self.updatescroll()
1417 1424
1418 1425 def toggleedit(self, item=None, test=False):
1419 1426 """
1420 1427 edit the currently chelected chunk
1421 1428 """
1422 1429 def updateui(self):
1423 1430 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1424 1431 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1425 1432 self.updatescroll()
1426 1433 self.stdscr.refresh()
1427 1434 self.statuswin.refresh()
1428 1435 self.stdscr.keypad(1)
1429 1436
1430 1437 def editpatchwitheditor(self, chunk):
1431 1438 if chunk is None:
1432 1439 self.ui.write(_('cannot edit patch for whole file'))
1433 1440 self.ui.write("\n")
1434 1441 return None
1435 1442 if chunk.header.binary():
1436 1443 self.ui.write(_('cannot edit patch for binary file'))
1437 1444 self.ui.write("\n")
1438 1445 return None
1439 1446 # patch comment based on the git one (based on comment at end of
1440 1447 # http://mercurial.selenic.com/wiki/recordextension)
1441 1448 phelp = '---' + _("""
1442 1449 to remove '-' lines, make them ' ' lines (context).
1443 1450 to remove '+' lines, delete them.
1444 1451 lines starting with # will be removed from the patch.
1445 1452
1446 1453 if the patch applies cleanly, the edited hunk will immediately be
1447 1454 added to the record list. if it does not apply cleanly, a rejects
1448 1455 file will be generated: you can use that when you try again. if
1449 1456 all lines of the hunk are removed, then the edit is aborted and
1450 1457 the hunk is left unchanged.
1451 1458 """)
1452 1459 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1453 1460 suffix=".diff", text=True)
1454 1461 ncpatchfp = None
1455 1462 try:
1456 1463 # write the initial patch
1457 1464 f = os.fdopen(patchfd, "w")
1458 1465 chunk.header.write(f)
1459 1466 chunk.write(f)
1460 1467 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1461 1468 f.close()
1462 1469 # start the editor and wait for it to complete
1463 1470 editor = self.ui.geteditor()
1464 1471 self.ui.system("%s \"%s\"" % (editor, patchfn),
1465 1472 environ={'hguser': self.ui.username()},
1466 1473 onerr=util.Abort, errprefix=_("edit failed"))
1467 1474 # remove comment lines
1468 1475 patchfp = open(patchfn)
1469 1476 ncpatchfp = cStringIO.StringIO()
1470 1477 for line in patchfp:
1471 1478 if not line.startswith('#'):
1472 1479 ncpatchfp.write(line)
1473 1480 patchfp.close()
1474 1481 ncpatchfp.seek(0)
1475 1482 newpatches = patchmod.parsepatch(ncpatchfp)
1476 1483 finally:
1477 1484 os.unlink(patchfn)
1478 1485 del ncpatchfp
1479 1486 return newpatches
1480 1487 if item is None:
1481 1488 item = self.currentselecteditem
1482 1489 if isinstance(item, uiheader):
1483 1490 return
1484 1491 if isinstance(item, uihunkline):
1485 1492 item = item.parentitem()
1486 1493 if not isinstance(item, uihunk):
1487 1494 return
1488 1495
1489 1496 beforeadded, beforeremoved = item.added, item.removed
1490 1497 newpatches = editpatchwitheditor(self, item)
1491 1498 header = item.header
1492 1499 editedhunkindex = header.hunks.index(item)
1493 1500 hunksbefore = header.hunks[:editedhunkindex]
1494 1501 hunksafter = header.hunks[editedhunkindex + 1:]
1495 1502 newpatchheader = newpatches[0]
1496 1503 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1497 1504 newadded = sum([h.added for h in newhunks])
1498 1505 newremoved = sum([h.removed for h in newhunks])
1499 1506 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1500 1507
1501 1508 for h in hunksafter:
1502 1509 h.toline += offset
1503 1510 for h in newhunks:
1504 1511 h.folded = False
1505 1512 header.hunks = hunksbefore + newhunks + hunksafter
1506 1513 if self.emptypatch():
1507 1514 header.hunks = hunksbefore + [item] + hunksafter
1508 1515 self.currentselecteditem = header
1509 1516
1510 1517 if not test:
1511 1518 updateui(self)
1512 1519
1513 1520 def emptypatch(self):
1514 1521 item = self.headerlist
1515 1522 if not item:
1516 1523 return True
1517 1524 for header in item:
1518 1525 if header.hunks:
1519 1526 return False
1520 1527 return True
1521 1528
1522 1529 def handlekeypressed(self, keypressed, test=False):
1523 1530 if keypressed in ["k", "KEY_UP"]:
1524 1531 self.uparrowevent()
1525 1532 if keypressed in ["K", "KEY_PPAGE"]:
1526 1533 self.uparrowshiftevent()
1527 1534 elif keypressed in ["j", "KEY_DOWN"]:
1528 1535 self.downarrowevent()
1529 1536 elif keypressed in ["J", "KEY_NPAGE"]:
1530 1537 self.downarrowshiftevent()
1531 1538 elif keypressed in ["l", "KEY_RIGHT"]:
1532 1539 self.rightarrowevent()
1533 1540 elif keypressed in ["h", "KEY_LEFT"]:
1534 1541 self.leftarrowevent()
1535 1542 elif keypressed in ["H", "KEY_SLEFT"]:
1536 1543 self.leftarrowshiftevent()
1537 1544 elif keypressed in ["q"]:
1538 1545 raise util.Abort(_('user quit'))
1539 1546 elif keypressed in ["c"]:
1540 1547 if self.confirmcommit():
1541 1548 return True
1542 1549 elif keypressed in ["r"]:
1543 1550 if self.confirmcommit(review=True):
1544 1551 return True
1545 1552 elif test and keypressed in ['X']:
1546 1553 return True
1547 1554 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1548 1555 self.toggleapply()
1549 1556 elif keypressed in ['A']:
1550 1557 self.toggleall()
1551 1558 elif keypressed in ['e']:
1552 1559 self.toggleedit(test=test)
1553 1560 elif keypressed in ["f"]:
1554 1561 self.togglefolded()
1555 1562 elif keypressed in ["F"]:
1556 1563 self.togglefolded(foldparent=True)
1557 1564 elif keypressed in ["?"]:
1558 1565 self.helpwindow()
1559 1566 self.stdscr.clear()
1560 1567 self.stdscr.refresh()
1561 1568
1562 1569 def main(self, stdscr):
1563 1570 """
1564 1571 method to be wrapped by curses.wrapper() for selecting chunks.
1565 1572
1566 1573 """
1567 1574 signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1568 1575 self.stdscr = stdscr
1569 1576 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1570 1577
1571 1578 curses.start_color()
1572 1579 curses.use_default_colors()
1573 1580
1574 1581 # available colors: black, blue, cyan, green, magenta, white, yellow
1575 1582 # init_pair(color_id, foreground_color, background_color)
1576 1583 self.initcolorpair(None, None, name="normal")
1577 1584 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1578 1585 name="selected")
1579 1586 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1580 1587 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1581 1588 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1582 1589 # newwin([height, width,] begin_y, begin_x)
1583 1590 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1584 1591 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1585 1592
1586 1593 # figure out how much space to allocate for the chunk-pad which is
1587 1594 # used for displaying the patch
1588 1595
1589 1596 # stupid hack to prevent getnumlinesdisplayed from failing
1590 1597 self.chunkpad = curses.newpad(1, self.xscreensize)
1591 1598
1592 1599 # add 1 so to account for last line text reaching end of line
1593 1600 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1594 1601 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1595 1602
1596 1603 # initialize selecteitemendline (initial start-line is 0)
1597 1604 self.selecteditemendline = self.getnumlinesdisplayed(
1598 1605 self.currentselecteditem, recursechildren=False)
1599 1606
1600 1607 while True:
1601 1608 self.updatescreen()
1602 1609 try:
1603 1610 keypressed = self.statuswin.getkey()
1611 if self.errorstr is not None:
1612 self.errorstr = None
1613 continue
1604 1614 except curses.error:
1605 1615 keypressed = "foobar"
1606 1616 if self.handlekeypressed(keypressed):
1607 1617 break
General Comments 0
You need to be logged in to leave comments. Login now