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