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