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