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