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