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