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