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