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