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