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