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