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