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