##// END OF EJS Templates
record: add default value for operation argument...
Laurent Charignon -
r25359:724421cb default
parent child Browse files
Show More
@@ -1,1601 +1,1603 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 if operation is None:
432 operation = _('confirm')
431 433 chunks = list(chunks)
432 434 # convert chunks list into structure suitable for displaying/modifying
433 435 # with curses. create a list of headers only.
434 436 headers = [c for c in chunks if isinstance(c, patchmod.header)]
435 437
436 438 # if there are no changed files
437 439 if len(headers) == 0:
438 440 return []
439 441 uiheaders = [uiheader(h) for h in headers]
440 442 # let user choose headers/hunks/lines, and mark their applied flags
441 443 # accordingly
442 444 chunkselector(ui, uiheaders)
443 445 appliedhunklist = []
444 446 for hdr in uiheaders:
445 447 if (hdr.applied and
446 448 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
447 449 appliedhunklist.append(hdr)
448 450 fixoffset = 0
449 451 for hnk in hdr.hunks:
450 452 if hnk.applied:
451 453 appliedhunklist.append(hnk)
452 454 # adjust the 'to'-line offset of the hunk to be correct
453 455 # after de-activating some of the other hunks for this file
454 456 if fixoffset:
455 457 #hnk = copy.copy(hnk) # necessary??
456 458 hnk.toline += fixoffset
457 459 else:
458 460 fixoffset += hnk.removed - hnk.added
459 461
460 462 return appliedhunklist
461 463
462 464 def gethw():
463 465 """
464 466 magically get the current height and width of the window (without initscr)
465 467
466 468 this is a rip-off of a rip-off - taken from the bpython code. it is
467 469 useful / necessary because otherwise curses.initscr() must be called,
468 470 which can leave the terminal in a nasty state after exiting.
469 471
470 472 """
471 473 h, w = struct.unpack(
472 474 "hhhh", fcntl.ioctl(_origstdout, termios.TIOCGWINSZ, "\000"*8))[0:2]
473 475 return h, w
474 476
475 477 def chunkselector(ui, headerlist):
476 478 """
477 479 curses interface to get selection of chunks, and mark the applied flags
478 480 of the chosen chunks.
479 481
480 482 """
481 483 ui.write(_('starting interactive selection\n'))
482 484 chunkselector = curseschunkselector(headerlist, ui)
483 485 curses.wrapper(chunkselector.main)
484 486
485 487 def testdecorator(testfn, f):
486 488 def u(*args, **kwargs):
487 489 return f(testfn, *args, **kwargs)
488 490 return u
489 491
490 492 def testchunkselector(testfn, ui, headerlist):
491 493 """
492 494 test interface to get selection of chunks, and mark the applied flags
493 495 of the chosen chunks.
494 496
495 497 """
496 498 chunkselector = curseschunkselector(headerlist, ui)
497 499 if testfn and os.path.exists(testfn):
498 500 testf = open(testfn)
499 501 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
500 502 testf.close()
501 503 while True:
502 504 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
503 505 break
504 506
505 507 class curseschunkselector(object):
506 508 def __init__(self, headerlist, ui):
507 509 # put the headers into a patch object
508 510 self.headerlist = patch(headerlist)
509 511
510 512 self.ui = ui
511 513
512 514 # list of all chunks
513 515 self.chunklist = []
514 516 for h in headerlist:
515 517 self.chunklist.append(h)
516 518 self.chunklist.extend(h.hunks)
517 519
518 520 # dictionary mapping (fgcolor, bgcolor) pairs to the
519 521 # corresponding curses color-pair value.
520 522 self.colorpairs = {}
521 523 # maps custom nicknames of color-pairs to curses color-pair values
522 524 self.colorpairnames = {}
523 525
524 526 # the currently selected header, hunk, or hunk-line
525 527 self.currentselecteditem = self.headerlist[0]
526 528
527 529 # updated when printing out patch-display -- the 'lines' here are the
528 530 # line positions *in the pad*, not on the screen.
529 531 self.selecteditemstartline = 0
530 532 self.selecteditemendline = None
531 533
532 534 # define indentation levels
533 535 self.headerindentnumchars = 0
534 536 self.hunkindentnumchars = 3
535 537 self.hunklineindentnumchars = 6
536 538
537 539 # the first line of the pad to print to the screen
538 540 self.firstlineofpadtoprint = 0
539 541
540 542 # keeps track of the number of lines in the pad
541 543 self.numpadlines = None
542 544
543 545 self.numstatuslines = 2
544 546
545 547 # keep a running count of the number of lines printed to the pad
546 548 # (used for determining when the selected item begins/ends)
547 549 self.linesprintedtopadsofar = 0
548 550
549 551 # the first line of the pad which is visible on the screen
550 552 self.firstlineofpadtoprint = 0
551 553
552 554 # stores optional text for a commit comment provided by the user
553 555 self.commenttext = ""
554 556
555 557 # if the last 'toggle all' command caused all changes to be applied
556 558 self.waslasttoggleallapplied = True
557 559
558 560 def uparrowevent(self):
559 561 """
560 562 try to select the previous item to the current item that has the
561 563 most-indented level. for example, if a hunk is selected, try to select
562 564 the last hunkline of the hunk prior to the selected hunk. or, if
563 565 the first hunkline of a hunk is currently selected, then select the
564 566 hunk itself.
565 567
566 568 if the currently selected item is already at the top of the screen,
567 569 scroll the screen down to show the new-selected item.
568 570
569 571 """
570 572 currentitem = self.currentselecteditem
571 573
572 574 nextitem = currentitem.previtem(constrainlevel=False)
573 575
574 576 if nextitem is None:
575 577 # if no parent item (i.e. currentitem is the first header), then
576 578 # no change...
577 579 nextitem = currentitem
578 580
579 581 self.currentselecteditem = nextitem
580 582
581 583 def uparrowshiftevent(self):
582 584 """
583 585 select (if possible) the previous item on the same level as the
584 586 currently selected item. otherwise, select (if possible) the
585 587 parent-item of the currently selected item.
586 588
587 589 if the currently selected item is already at the top of the screen,
588 590 scroll the screen down to show the new-selected item.
589 591
590 592 """
591 593 currentitem = self.currentselecteditem
592 594 nextitem = currentitem.previtem()
593 595 # if there's no previous item on this level, try choosing the parent
594 596 if nextitem is None:
595 597 nextitem = currentitem.parentitem()
596 598 if nextitem is None:
597 599 # if no parent item (i.e. currentitem is the first header), then
598 600 # no change...
599 601 nextitem = currentitem
600 602
601 603 self.currentselecteditem = nextitem
602 604
603 605 def downarrowevent(self):
604 606 """
605 607 try to select the next item to the current item that has the
606 608 most-indented level. for example, if a hunk is selected, select
607 609 the first hunkline of the selected hunk. or, if the last hunkline of
608 610 a hunk is currently selected, then select the next hunk, if one exists,
609 611 or if not, the next header if one exists.
610 612
611 613 if the currently selected item is already at the bottom of the screen,
612 614 scroll the screen up to show the new-selected item.
613 615
614 616 """
615 617 #self.startprintline += 1 #debug
616 618 currentitem = self.currentselecteditem
617 619
618 620 nextitem = currentitem.nextitem(constrainlevel=False)
619 621 # if there's no next item, keep the selection as-is
620 622 if nextitem is None:
621 623 nextitem = currentitem
622 624
623 625 self.currentselecteditem = nextitem
624 626
625 627 def downarrowshiftevent(self):
626 628 """
627 629 if the cursor is already at the bottom chunk, scroll the screen up and
628 630 move the cursor-position to the subsequent chunk. otherwise, only move
629 631 the cursor position down one chunk.
630 632
631 633 """
632 634 # todo: update docstring
633 635
634 636 currentitem = self.currentselecteditem
635 637 nextitem = currentitem.nextitem()
636 638 # if there's no previous item on this level, try choosing the parent's
637 639 # nextitem.
638 640 if nextitem is None:
639 641 try:
640 642 nextitem = currentitem.parentitem().nextitem()
641 643 except AttributeError:
642 644 # parentitem returned None, so nextitem() can't be called
643 645 nextitem = None
644 646 if nextitem is None:
645 647 # if no next item on parent-level, then no change...
646 648 nextitem = currentitem
647 649
648 650 self.currentselecteditem = nextitem
649 651
650 652 def rightarrowevent(self):
651 653 """
652 654 select (if possible) the first of this item's child-items.
653 655
654 656 """
655 657 currentitem = self.currentselecteditem
656 658 nextitem = currentitem.firstchild()
657 659
658 660 # turn off folding if we want to show a child-item
659 661 if currentitem.folded:
660 662 self.togglefolded(currentitem)
661 663
662 664 if nextitem is None:
663 665 # if no next item on parent-level, then no change...
664 666 nextitem = currentitem
665 667
666 668 self.currentselecteditem = nextitem
667 669
668 670 def leftarrowevent(self):
669 671 """
670 672 if the current item can be folded (i.e. it is an unfolded header or
671 673 hunk), then fold it. otherwise try select (if possible) the parent
672 674 of this item.
673 675
674 676 """
675 677 currentitem = self.currentselecteditem
676 678
677 679 # try to fold the item
678 680 if not isinstance(currentitem, uihunkline):
679 681 if not currentitem.folded:
680 682 self.togglefolded(item=currentitem)
681 683 return
682 684
683 685 # if it can't be folded, try to select the parent item
684 686 nextitem = currentitem.parentitem()
685 687
686 688 if nextitem is None:
687 689 # if no item on parent-level, then no change...
688 690 nextitem = currentitem
689 691 if not nextitem.folded:
690 692 self.togglefolded(item=nextitem)
691 693
692 694 self.currentselecteditem = nextitem
693 695
694 696 def leftarrowshiftevent(self):
695 697 """
696 698 select the header of the current item (or fold current item if the
697 699 current item is already a header).
698 700
699 701 """
700 702 currentitem = self.currentselecteditem
701 703
702 704 if isinstance(currentitem, uiheader):
703 705 if not currentitem.folded:
704 706 self.togglefolded(item=currentitem)
705 707 return
706 708
707 709 # select the parent item recursively until we're at a header
708 710 while True:
709 711 nextitem = currentitem.parentitem()
710 712 if nextitem is None:
711 713 break
712 714 else:
713 715 currentitem = nextitem
714 716
715 717 self.currentselecteditem = currentitem
716 718
717 719 def updatescroll(self):
718 720 "scroll the screen to fully show the currently-selected"
719 721 selstart = self.selecteditemstartline
720 722 selend = self.selecteditemendline
721 723 #selnumlines = selend - selstart
722 724 padstart = self.firstlineofpadtoprint
723 725 padend = padstart + self.yscreensize - self.numstatuslines - 1
724 726 # 'buffered' pad start/end values which scroll with a certain
725 727 # top/bottom context margin
726 728 padstartbuffered = padstart + 3
727 729 padendbuffered = padend - 3
728 730
729 731 if selend > padendbuffered:
730 732 self.scrolllines(selend - padendbuffered)
731 733 elif selstart < padstartbuffered:
732 734 # negative values scroll in pgup direction
733 735 self.scrolllines(selstart - padstartbuffered)
734 736
735 737
736 738 def scrolllines(self, numlines):
737 739 "scroll the screen up (down) by numlines when numlines >0 (<0)."
738 740 self.firstlineofpadtoprint += numlines
739 741 if self.firstlineofpadtoprint < 0:
740 742 self.firstlineofpadtoprint = 0
741 743 if self.firstlineofpadtoprint > self.numpadlines - 1:
742 744 self.firstlineofpadtoprint = self.numpadlines - 1
743 745
744 746 def toggleapply(self, item=None):
745 747 """
746 748 toggle the applied flag of the specified item. if no item is specified,
747 749 toggle the flag of the currently selected item.
748 750
749 751 """
750 752 if item is None:
751 753 item = self.currentselecteditem
752 754
753 755 item.applied = not item.applied
754 756
755 757 if isinstance(item, uiheader):
756 758 item.partial = False
757 759 if item.applied:
758 760 # apply all its hunks
759 761 for hnk in item.hunks:
760 762 hnk.applied = True
761 763 # apply all their hunklines
762 764 for hunkline in hnk.changedlines:
763 765 hunkline.applied = True
764 766 else:
765 767 # un-apply all its hunks
766 768 for hnk in item.hunks:
767 769 hnk.applied = False
768 770 hnk.partial = False
769 771 # un-apply all their hunklines
770 772 for hunkline in hnk.changedlines:
771 773 hunkline.applied = False
772 774 elif isinstance(item, uihunk):
773 775 item.partial = False
774 776 # apply all it's hunklines
775 777 for hunkline in item.changedlines:
776 778 hunkline.applied = item.applied
777 779
778 780 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
779 781 allsiblingsapplied = not (False in siblingappliedstatus)
780 782 nosiblingsapplied = not (True in siblingappliedstatus)
781 783
782 784 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
783 785 somesiblingspartial = (True in siblingspartialstatus)
784 786
785 787 #cases where applied or partial should be removed from header
786 788
787 789 # if no 'sibling' hunks are applied (including this hunk)
788 790 if nosiblingsapplied:
789 791 if not item.header.special():
790 792 item.header.applied = False
791 793 item.header.partial = False
792 794 else: # some/all parent siblings are applied
793 795 item.header.applied = True
794 796 item.header.partial = (somesiblingspartial or
795 797 not allsiblingsapplied)
796 798
797 799 elif isinstance(item, uihunkline):
798 800 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
799 801 allsiblingsapplied = not (False in siblingappliedstatus)
800 802 nosiblingsapplied = not (True in siblingappliedstatus)
801 803
802 804 # if no 'sibling' lines are applied
803 805 if nosiblingsapplied:
804 806 item.hunk.applied = False
805 807 item.hunk.partial = False
806 808 elif allsiblingsapplied:
807 809 item.hunk.applied = True
808 810 item.hunk.partial = False
809 811 else: # some siblings applied
810 812 item.hunk.applied = True
811 813 item.hunk.partial = True
812 814
813 815 parentsiblingsapplied = [hnk.applied for hnk
814 816 in item.hunk.header.hunks]
815 817 noparentsiblingsapplied = not (True in parentsiblingsapplied)
816 818 allparentsiblingsapplied = not (False in parentsiblingsapplied)
817 819
818 820 parentsiblingspartial = [hnk.partial for hnk
819 821 in item.hunk.header.hunks]
820 822 someparentsiblingspartial = (True in parentsiblingspartial)
821 823
822 824 # if all parent hunks are not applied, un-apply header
823 825 if noparentsiblingsapplied:
824 826 if not item.hunk.header.special():
825 827 item.hunk.header.applied = False
826 828 item.hunk.header.partial = False
827 829 # set the applied and partial status of the header if needed
828 830 else: # some/all parent siblings are applied
829 831 item.hunk.header.applied = True
830 832 item.hunk.header.partial = (someparentsiblingspartial or
831 833 not allparentsiblingsapplied)
832 834
833 835 def toggleall(self):
834 836 "toggle the applied flag of all items."
835 837 if self.waslasttoggleallapplied: # then unapply them this time
836 838 for item in self.headerlist:
837 839 if item.applied:
838 840 self.toggleapply(item)
839 841 else:
840 842 for item in self.headerlist:
841 843 if not item.applied:
842 844 self.toggleapply(item)
843 845 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
844 846
845 847 def togglefolded(self, item=None, foldparent=False):
846 848 "toggle folded flag of specified item (defaults to currently selected)"
847 849 if item is None:
848 850 item = self.currentselecteditem
849 851 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
850 852 if not isinstance(item, uiheader):
851 853 # we need to select the parent item in this case
852 854 self.currentselecteditem = item = item.parentitem()
853 855 elif item.neverunfolded:
854 856 item.neverunfolded = False
855 857
856 858 # also fold any foldable children of the parent/current item
857 859 if isinstance(item, uiheader): # the original or 'new' item
858 860 for child in item.allchildren():
859 861 child.folded = not item.folded
860 862
861 863 if isinstance(item, (uiheader, uihunk)):
862 864 item.folded = not item.folded
863 865
864 866
865 867 def alignstring(self, instr, window):
866 868 """
867 869 add whitespace to the end of a string in order to make it fill
868 870 the screen in the x direction. the current cursor position is
869 871 taken into account when making this calculation. the string can span
870 872 multiple lines.
871 873
872 874 """
873 875 y, xstart = window.getyx()
874 876 width = self.xscreensize
875 877 # turn tabs into spaces
876 878 instr = instr.expandtabs(4)
877 879 strwidth = encoding.colwidth(instr)
878 880 numspaces = (width - ((strwidth + xstart) % width) - 1)
879 881 return instr + " " * numspaces + "\n"
880 882
881 883 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
882 884 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
883 885 """
884 886 print the string, text, with the specified colors and attributes, to
885 887 the specified curses window object.
886 888
887 889 the foreground and background colors are of the form
888 890 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
889 891 magenta, red, white, yellow]. if pairname is provided, a color
890 892 pair will be looked up in the self.colorpairnames dictionary.
891 893
892 894 attrlist is a list containing text attributes in the form of
893 895 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
894 896 underline].
895 897
896 898 if align == True, whitespace is added to the printed string such that
897 899 the string stretches to the right border of the window.
898 900
899 901 if showwhtspc == True, trailing whitespace of a string is highlighted.
900 902
901 903 """
902 904 # preprocess the text, converting tabs to spaces
903 905 text = text.expandtabs(4)
904 906 # strip \n, and convert control characters to ^[char] representation
905 907 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
906 908 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
907 909
908 910 if pair is not None:
909 911 colorpair = pair
910 912 elif pairname is not None:
911 913 colorpair = self.colorpairnames[pairname]
912 914 else:
913 915 if fgcolor is None:
914 916 fgcolor = -1
915 917 if bgcolor is None:
916 918 bgcolor = -1
917 919 if (fgcolor, bgcolor) in self.colorpairs:
918 920 colorpair = self.colorpairs[(fgcolor, bgcolor)]
919 921 else:
920 922 colorpair = self.getcolorpair(fgcolor, bgcolor)
921 923 # add attributes if possible
922 924 if attrlist is None:
923 925 attrlist = []
924 926 if colorpair < 256:
925 927 # then it is safe to apply all attributes
926 928 for textattr in attrlist:
927 929 colorpair |= textattr
928 930 else:
929 931 # just apply a select few (safe?) attributes
930 932 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
931 933 if textattr in attrlist:
932 934 colorpair |= textattr
933 935
934 936 y, xstart = self.chunkpad.getyx()
935 937 t = "" # variable for counting lines printed
936 938 # if requested, show trailing whitespace
937 939 if showwhtspc:
938 940 origlen = len(text)
939 941 text = text.rstrip(' \n') # tabs have already been expanded
940 942 strippedlen = len(text)
941 943 numtrailingspaces = origlen - strippedlen
942 944
943 945 if towin:
944 946 window.addstr(text, colorpair)
945 947 t += text
946 948
947 949 if showwhtspc:
948 950 wscolorpair = colorpair | curses.A_REVERSE
949 951 if towin:
950 952 for i in range(numtrailingspaces):
951 953 window.addch(curses.ACS_CKBOARD, wscolorpair)
952 954 t += " " * numtrailingspaces
953 955
954 956 if align:
955 957 if towin:
956 958 extrawhitespace = self.alignstring("", window)
957 959 window.addstr(extrawhitespace, colorpair)
958 960 else:
959 961 # need to use t, since the x position hasn't incremented
960 962 extrawhitespace = self.alignstring(t, window)
961 963 t += extrawhitespace
962 964
963 965 # is reset to 0 at the beginning of printitem()
964 966
965 967 linesprinted = (xstart + len(t)) / self.xscreensize
966 968 self.linesprintedtopadsofar += linesprinted
967 969 return t
968 970
969 971 def updatescreen(self):
970 972 self.statuswin.erase()
971 973 self.chunkpad.erase()
972 974
973 975 printstring = self.printstring
974 976
975 977 # print out the status lines at the top
976 978 try:
977 979 printstring(self.statuswin,
978 980 "SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; "
979 981 "(space/A) toggle hunk/all; (e)dit hunk;",
980 982 pairname="legend")
981 983 printstring(self.statuswin,
982 984 " (f)old/unfold; (c)onfirm applied; (q)uit; (?) help "
983 985 "| [X]=hunk applied **=folded",
984 986 pairname="legend")
985 987 except curses.error:
986 988 pass
987 989
988 990 # print out the patch in the remaining part of the window
989 991 try:
990 992 self.printitem()
991 993 self.updatescroll()
992 994 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
993 995 self.numstatuslines, 0,
994 996 self.yscreensize + 1 - self.numstatuslines,
995 997 self.xscreensize)
996 998 except curses.error:
997 999 pass
998 1000
999 1001 # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
1000 1002 self.statuswin.refresh()
1001 1003
1002 1004 def getstatusprefixstring(self, item):
1003 1005 """
1004 1006 create a string to prefix a line with which indicates whether 'item'
1005 1007 is applied and/or folded.
1006 1008
1007 1009 """
1008 1010 # create checkbox string
1009 1011 if item.applied:
1010 1012 if not isinstance(item, uihunkline) and item.partial:
1011 1013 checkbox = "[~]"
1012 1014 else:
1013 1015 checkbox = "[x]"
1014 1016 else:
1015 1017 checkbox = "[ ]"
1016 1018
1017 1019 try:
1018 1020 if item.folded:
1019 1021 checkbox += "**"
1020 1022 if isinstance(item, uiheader):
1021 1023 # one of "m", "a", or "d" (modified, added, deleted)
1022 1024 filestatus = item.changetype
1023 1025
1024 1026 checkbox += filestatus + " "
1025 1027 else:
1026 1028 checkbox += " "
1027 1029 if isinstance(item, uiheader):
1028 1030 # add two more spaces for headers
1029 1031 checkbox += " "
1030 1032 except AttributeError: # not foldable
1031 1033 checkbox += " "
1032 1034
1033 1035 return checkbox
1034 1036
1035 1037 def printheader(self, header, selected=False, towin=True,
1036 1038 ignorefolding=False):
1037 1039 """
1038 1040 print the header to the pad. if countlines is True, don't print
1039 1041 anything, but just count the number of lines which would be printed.
1040 1042
1041 1043 """
1042 1044 outstr = ""
1043 1045 text = header.prettystr()
1044 1046 chunkindex = self.chunklist.index(header)
1045 1047
1046 1048 if chunkindex != 0 and not header.folded:
1047 1049 # add separating line before headers
1048 1050 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1049 1051 towin=towin, align=False)
1050 1052 # select color-pair based on if the header is selected
1051 1053 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1052 1054 attrlist=[curses.A_BOLD])
1053 1055
1054 1056 # print out each line of the chunk, expanding it to screen width
1055 1057
1056 1058 # number of characters to indent lines on this level by
1057 1059 indentnumchars = 0
1058 1060 checkbox = self.getstatusprefixstring(header)
1059 1061 if not header.folded or ignorefolding:
1060 1062 textlist = text.split("\n")
1061 1063 linestr = checkbox + textlist[0]
1062 1064 else:
1063 1065 linestr = checkbox + header.filename()
1064 1066 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1065 1067 towin=towin)
1066 1068 if not header.folded or ignorefolding:
1067 1069 if len(textlist) > 1:
1068 1070 for line in textlist[1:]:
1069 1071 linestr = " "*(indentnumchars + len(checkbox)) + line
1070 1072 outstr += self.printstring(self.chunkpad, linestr,
1071 1073 pair=colorpair, towin=towin)
1072 1074
1073 1075 return outstr
1074 1076
1075 1077 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1076 1078 ignorefolding=False):
1077 1079 "includes start/end line indicator"
1078 1080 outstr = ""
1079 1081 # where hunk is in list of siblings
1080 1082 hunkindex = hunk.header.hunks.index(hunk)
1081 1083
1082 1084 if hunkindex != 0:
1083 1085 # add separating line before headers
1084 1086 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1085 1087 towin=towin, align=False)
1086 1088
1087 1089 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1088 1090 attrlist=[curses.A_BOLD])
1089 1091
1090 1092 # print out from-to line with checkbox
1091 1093 checkbox = self.getstatusprefixstring(hunk)
1092 1094
1093 1095 lineprefix = " "*self.hunkindentnumchars + checkbox
1094 1096 frtoline = " " + hunk.getfromtoline().strip("\n")
1095 1097
1096 1098
1097 1099 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1098 1100 align=False) # add uncolored checkbox/indent
1099 1101 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1100 1102 towin=towin)
1101 1103
1102 1104 if hunk.folded and not ignorefolding:
1103 1105 # skip remainder of output
1104 1106 return outstr
1105 1107
1106 1108 # print out lines of the chunk preceeding changed-lines
1107 1109 for line in hunk.before:
1108 1110 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1109 1111 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1110 1112
1111 1113 return outstr
1112 1114
1113 1115 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1114 1116 outstr = ""
1115 1117 if hunk.folded and not ignorefolding:
1116 1118 return outstr
1117 1119
1118 1120 # a bit superfluous, but to avoid hard-coding indent amount
1119 1121 checkbox = self.getstatusprefixstring(hunk)
1120 1122 for line in hunk.after:
1121 1123 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1122 1124 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1123 1125
1124 1126 return outstr
1125 1127
1126 1128 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1127 1129 outstr = ""
1128 1130 checkbox = self.getstatusprefixstring(hunkline)
1129 1131
1130 1132 linestr = hunkline.prettystr().strip("\n")
1131 1133
1132 1134 # select color-pair based on whether line is an addition/removal
1133 1135 if selected:
1134 1136 colorpair = self.getcolorpair(name="selected")
1135 1137 elif linestr.startswith("+"):
1136 1138 colorpair = self.getcolorpair(name="addition")
1137 1139 elif linestr.startswith("-"):
1138 1140 colorpair = self.getcolorpair(name="deletion")
1139 1141 elif linestr.startswith("\\"):
1140 1142 colorpair = self.getcolorpair(name="normal")
1141 1143
1142 1144 lineprefix = " "*self.hunklineindentnumchars + checkbox
1143 1145 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1144 1146 align=False) # add uncolored checkbox/indent
1145 1147 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1146 1148 towin=towin, showwhtspc=True)
1147 1149 return outstr
1148 1150
1149 1151 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1150 1152 towin=True):
1151 1153 """
1152 1154 use __printitem() to print the the specified item.applied.
1153 1155 if item is not specified, then print the entire patch.
1154 1156 (hiding folded elements, etc. -- see __printitem() docstring)
1155 1157 """
1156 1158 if item is None:
1157 1159 item = self.headerlist
1158 1160 if recursechildren:
1159 1161 self.linesprintedtopadsofar = 0
1160 1162
1161 1163 outstr = []
1162 1164 self.__printitem(item, ignorefolding, recursechildren, outstr,
1163 1165 towin=towin)
1164 1166 return ''.join(outstr)
1165 1167
1166 1168 def outofdisplayedarea(self):
1167 1169 y, _ = self.chunkpad.getyx() # cursor location
1168 1170 # * 2 here works but an optimization would be the max number of
1169 1171 # consecutive non selectable lines
1170 1172 # i.e the max number of context line for any hunk in the patch
1171 1173 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1172 1174 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1173 1175 return y < miny or y > maxy
1174 1176
1175 1177 def handleselection(self, item, recursechildren):
1176 1178 selected = (item is self.currentselecteditem)
1177 1179 if selected and recursechildren:
1178 1180 # assumes line numbering starting from line 0
1179 1181 self.selecteditemstartline = self.linesprintedtopadsofar
1180 1182 selecteditemlines = self.getnumlinesdisplayed(item,
1181 1183 recursechildren=False)
1182 1184 self.selecteditemendline = (self.selecteditemstartline +
1183 1185 selecteditemlines - 1)
1184 1186 return selected
1185 1187
1186 1188 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1187 1189 towin=True):
1188 1190 """
1189 1191 recursive method for printing out patch/header/hunk/hunk-line data to
1190 1192 screen. also returns a string with all of the content of the displayed
1191 1193 patch (not including coloring, etc.).
1192 1194
1193 1195 if ignorefolding is True, then folded items are printed out.
1194 1196
1195 1197 if recursechildren is False, then only print the item without its
1196 1198 child items.
1197 1199
1198 1200 """
1199 1201 if towin and self.outofdisplayedarea():
1200 1202 return
1201 1203
1202 1204 selected = self.handleselection(item, recursechildren)
1203 1205
1204 1206 # patch object is a list of headers
1205 1207 if isinstance(item, patch):
1206 1208 if recursechildren:
1207 1209 for hdr in item:
1208 1210 self.__printitem(hdr, ignorefolding,
1209 1211 recursechildren, outstr, towin)
1210 1212 # todo: eliminate all isinstance() calls
1211 1213 if isinstance(item, uiheader):
1212 1214 outstr.append(self.printheader(item, selected, towin=towin,
1213 1215 ignorefolding=ignorefolding))
1214 1216 if recursechildren:
1215 1217 for hnk in item.hunks:
1216 1218 self.__printitem(hnk, ignorefolding,
1217 1219 recursechildren, outstr, towin)
1218 1220 elif (isinstance(item, uihunk) and
1219 1221 ((not item.header.folded) or ignorefolding)):
1220 1222 # print the hunk data which comes before the changed-lines
1221 1223 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1222 1224 ignorefolding=ignorefolding))
1223 1225 if recursechildren:
1224 1226 for l in item.changedlines:
1225 1227 self.__printitem(l, ignorefolding,
1226 1228 recursechildren, outstr, towin)
1227 1229 outstr.append(self.printhunklinesafter(item, towin=towin,
1228 1230 ignorefolding=ignorefolding))
1229 1231 elif (isinstance(item, uihunkline) and
1230 1232 ((not item.hunk.folded) or ignorefolding)):
1231 1233 outstr.append(self.printhunkchangedline(item, selected,
1232 1234 towin=towin))
1233 1235
1234 1236 return outstr
1235 1237
1236 1238 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1237 1239 recursechildren=True):
1238 1240 """
1239 1241 return the number of lines which would be displayed if the item were
1240 1242 to be printed to the display. the item will not be printed to the
1241 1243 display (pad).
1242 1244 if no item is given, assume the entire patch.
1243 1245 if ignorefolding is True, folded items will be unfolded when counting
1244 1246 the number of lines.
1245 1247
1246 1248 """
1247 1249 # temporarily disable printing to windows by printstring
1248 1250 patchdisplaystring = self.printitem(item, ignorefolding,
1249 1251 recursechildren, towin=False)
1250 1252 numlines = len(patchdisplaystring) / self.xscreensize
1251 1253 return numlines
1252 1254
1253 1255 def sigwinchhandler(self, n, frame):
1254 1256 "handle window resizing"
1255 1257 try:
1256 1258 curses.endwin()
1257 1259 self.yscreensize, self.xscreensize = gethw()
1258 1260 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1259 1261 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1260 1262 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1261 1263 # todo: try to resize commit message window if possible
1262 1264 except curses.error:
1263 1265 pass
1264 1266
1265 1267 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1266 1268 attrlist=None):
1267 1269 """
1268 1270 get a curses color pair, adding it to self.colorpairs if it is not
1269 1271 already defined. an optional string, name, can be passed as a shortcut
1270 1272 for referring to the color-pair. by default, if no arguments are
1271 1273 specified, the white foreground / black background color-pair is
1272 1274 returned.
1273 1275
1274 1276 it is expected that this function will be used exclusively for
1275 1277 initializing color pairs, and not curses.init_pair().
1276 1278
1277 1279 attrlist is used to 'flavor' the returned color-pair. this information
1278 1280 is not stored in self.colorpairs. it contains attribute values like
1279 1281 curses.A_BOLD.
1280 1282
1281 1283 """
1282 1284 if (name is not None) and name in self.colorpairnames:
1283 1285 # then get the associated color pair and return it
1284 1286 colorpair = self.colorpairnames[name]
1285 1287 else:
1286 1288 if fgcolor is None:
1287 1289 fgcolor = -1
1288 1290 if bgcolor is None:
1289 1291 bgcolor = -1
1290 1292 if (fgcolor, bgcolor) in self.colorpairs:
1291 1293 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1292 1294 else:
1293 1295 pairindex = len(self.colorpairs) + 1
1294 1296 curses.init_pair(pairindex, fgcolor, bgcolor)
1295 1297 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1296 1298 curses.color_pair(pairindex))
1297 1299 if name is not None:
1298 1300 self.colorpairnames[name] = curses.color_pair(pairindex)
1299 1301
1300 1302 # add attributes if possible
1301 1303 if attrlist is None:
1302 1304 attrlist = []
1303 1305 if colorpair < 256:
1304 1306 # then it is safe to apply all attributes
1305 1307 for textattr in attrlist:
1306 1308 colorpair |= textattr
1307 1309 else:
1308 1310 # just apply a select few (safe?) attributes
1309 1311 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1310 1312 if textattrib in attrlist:
1311 1313 colorpair |= textattrib
1312 1314 return colorpair
1313 1315
1314 1316 def initcolorpair(self, *args, **kwargs):
1315 1317 "same as getcolorpair."
1316 1318 self.getcolorpair(*args, **kwargs)
1317 1319
1318 1320 def helpwindow(self):
1319 1321 "print a help window to the screen. exit after any keypress."
1320 1322 helptext = """ [press any key to return to the patch-display]
1321 1323
1322 1324 crecord allows you to interactively choose among the changes you have made,
1323 1325 and confirm only those changes you select for further processing by the command
1324 1326 you are running (commit/shelve/revert), after confirming the selected
1325 1327 changes, the unselected changes are still present in your working copy, so you
1326 1328 can use crecord multiple times to split large changes into smaller changesets.
1327 1329 the following are valid keystrokes:
1328 1330
1329 1331 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1330 1332 a : (un-)select all items
1331 1333 up/down-arrow [k/j] : go to previous/next unfolded item
1332 1334 pgup/pgdn [k/j] : go to previous/next item of same type
1333 1335 right/left-arrow [l/h] : go to child item / parent item
1334 1336 shift-left-arrow [h] : go to parent header / fold selected header
1335 1337 f : fold / unfold item, hiding/revealing its children
1336 1338 f : fold / unfold parent item and all of its ancestors
1337 1339 m : edit / resume editing the commit message
1338 1340 e : edit the currently selected hunk
1339 1341 a : toggle amend mode (hg rev >= 2.2)
1340 1342 c : confirm selected changes
1341 1343 r : review/edit and confirm selected changes
1342 1344 q : quit without confirming (no changes will be made)
1343 1345 ? : help (what you're currently reading)"""
1344 1346
1345 1347 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1346 1348 helplines = helptext.split("\n")
1347 1349 helplines = helplines + [" "]*(
1348 1350 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1349 1351 try:
1350 1352 for line in helplines:
1351 1353 self.printstring(helpwin, line, pairname="legend")
1352 1354 except curses.error:
1353 1355 pass
1354 1356 helpwin.refresh()
1355 1357 try:
1356 1358 helpwin.getkey()
1357 1359 except curses.error:
1358 1360 pass
1359 1361
1360 1362 def confirmationwindow(self, windowtext):
1361 1363 "display an informational window, then wait for and return a keypress."
1362 1364
1363 1365 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1364 1366 try:
1365 1367 lines = windowtext.split("\n")
1366 1368 for line in lines:
1367 1369 self.printstring(confirmwin, line, pairname="selected")
1368 1370 except curses.error:
1369 1371 pass
1370 1372 self.stdscr.refresh()
1371 1373 confirmwin.refresh()
1372 1374 try:
1373 1375 response = chr(self.stdscr.getch())
1374 1376 except ValueError:
1375 1377 response = None
1376 1378
1377 1379 return response
1378 1380
1379 1381 def confirmcommit(self, review=False):
1380 1382 """ask for 'y' to be pressed to confirm selected. return True if
1381 1383 confirmed."""
1382 1384 if review:
1383 1385 confirmtext = (
1384 1386 """if you answer yes to the following, the your currently chosen patch chunks
1385 1387 will be loaded into an editor. you may modify the patch from the editor, and
1386 1388 save the changes if you wish to change the patch. otherwise, you can just
1387 1389 close the editor without saving to accept the current patch as-is.
1388 1390
1389 1391 note: don't add/remove lines unless you also modify the range information.
1390 1392 failing to follow this rule will result in the commit aborting.
1391 1393
1392 1394 are you sure you want to review/edit and confirm the selected changes [yn]?
1393 1395 """)
1394 1396 else:
1395 1397 confirmtext = (
1396 1398 "are you sure you want to confirm the selected changes [yn]? ")
1397 1399
1398 1400 response = self.confirmationwindow(confirmtext)
1399 1401 if response is None:
1400 1402 response = "n"
1401 1403 if response.lower().startswith("y"):
1402 1404 return True
1403 1405 else:
1404 1406 return False
1405 1407
1406 1408 def recenterdisplayedarea(self):
1407 1409 """
1408 1410 once we scrolled with pg up pg down we can be pointing outside of the
1409 1411 display zone. we print the patch with towin=False to compute the
1410 1412 location of the selected item eventhough it is outside of the displayed
1411 1413 zone and then update the scroll.
1412 1414 """
1413 1415 self.printitem(towin=False)
1414 1416 self.updatescroll()
1415 1417
1416 1418 def toggleedit(self, item=None, test=False):
1417 1419 """
1418 1420 edit the currently chelected chunk
1419 1421 """
1420 1422
1421 1423 def editpatchwitheditor(self, chunk):
1422 1424 if chunk is None:
1423 1425 self.ui.write(_('cannot edit patch for whole file'))
1424 1426 self.ui.write("\n")
1425 1427 return None
1426 1428 if chunk.header.binary():
1427 1429 self.ui.write(_('cannot edit patch for binary file'))
1428 1430 self.ui.write("\n")
1429 1431 return None
1430 1432 # patch comment based on the git one (based on comment at end of
1431 1433 # http://mercurial.selenic.com/wiki/recordextension)
1432 1434 phelp = '---' + _("""
1433 1435 to remove '-' lines, make them ' ' lines (context).
1434 1436 to remove '+' lines, delete them.
1435 1437 lines starting with # will be removed from the patch.
1436 1438
1437 1439 if the patch applies cleanly, the edited hunk will immediately be
1438 1440 added to the record list. if it does not apply cleanly, a rejects
1439 1441 file will be generated: you can use that when you try again. if
1440 1442 all lines of the hunk are removed, then the edit is aborted and
1441 1443 the hunk is left unchanged.
1442 1444 """)
1443 1445 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1444 1446 suffix=".diff", text=True)
1445 1447 ncpatchfp = None
1446 1448 try:
1447 1449 # write the initial patch
1448 1450 f = os.fdopen(patchfd, "w")
1449 1451 chunk.header.write(f)
1450 1452 chunk.write(f)
1451 1453 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1452 1454 f.close()
1453 1455 # start the editor and wait for it to complete
1454 1456 editor = self.ui.geteditor()
1455 1457 self.ui.system("%s \"%s\"" % (editor, patchfn),
1456 1458 environ={'hguser': self.ui.username()},
1457 1459 onerr=util.Abort, errprefix=_("edit failed"))
1458 1460 # remove comment lines
1459 1461 patchfp = open(patchfn)
1460 1462 ncpatchfp = cStringIO.StringIO()
1461 1463 for line in patchfp:
1462 1464 if not line.startswith('#'):
1463 1465 ncpatchfp.write(line)
1464 1466 patchfp.close()
1465 1467 ncpatchfp.seek(0)
1466 1468 newpatches = patchmod.parsepatch(ncpatchfp)
1467 1469 finally:
1468 1470 os.unlink(patchfn)
1469 1471 del ncpatchfp
1470 1472 return newpatches
1471 1473 if item is None:
1472 1474 item = self.currentselecteditem
1473 1475 if isinstance(item, uiheader):
1474 1476 return
1475 1477 if isinstance(item, uihunkline):
1476 1478 item = item.parentitem()
1477 1479 if not isinstance(item, uihunk):
1478 1480 return
1479 1481
1480 1482 beforeadded, beforeremoved = item.added, item.removed
1481 1483 newpatches = editpatchwitheditor(self, item)
1482 1484 header = item.header
1483 1485 editedhunkindex = header.hunks.index(item)
1484 1486 hunksbefore = header.hunks[:editedhunkindex]
1485 1487 hunksafter = header.hunks[editedhunkindex + 1:]
1486 1488 newpatchheader = newpatches[0]
1487 1489 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1488 1490 newadded = sum([h.added for h in newhunks])
1489 1491 newremoved = sum([h.removed for h in newhunks])
1490 1492 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1491 1493
1492 1494 for h in hunksafter:
1493 1495 h.toline += offset
1494 1496 for h in newhunks:
1495 1497 h.folded = False
1496 1498 header.hunks = hunksbefore + newhunks + hunksafter
1497 1499 if self.emptypatch():
1498 1500 header.hunks = hunksbefore + [item] + hunksafter
1499 1501 self.currentselecteditem = header
1500 1502
1501 1503 if not test:
1502 1504 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1503 1505 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1504 1506 self.updatescroll()
1505 1507 self.stdscr.refresh()
1506 1508 self.statuswin.refresh()
1507 1509 self.stdscr.keypad(1)
1508 1510
1509 1511 def emptypatch(self):
1510 1512 item = self.headerlist
1511 1513 if not item:
1512 1514 return True
1513 1515 for header in item:
1514 1516 if header.hunks:
1515 1517 return False
1516 1518 return True
1517 1519
1518 1520 def handlekeypressed(self, keypressed, test=False):
1519 1521 if keypressed in ["k", "KEY_UP"]:
1520 1522 self.uparrowevent()
1521 1523 if keypressed in ["k", "KEY_PPAGE"]:
1522 1524 self.uparrowshiftevent()
1523 1525 elif keypressed in ["j", "KEY_DOWN"]:
1524 1526 self.downarrowevent()
1525 1527 elif keypressed in ["j", "KEY_NPAGE"]:
1526 1528 self.downarrowshiftevent()
1527 1529 elif keypressed in ["l", "KEY_RIGHT"]:
1528 1530 self.rightarrowevent()
1529 1531 elif keypressed in ["h", "KEY_LEFT"]:
1530 1532 self.leftarrowevent()
1531 1533 elif keypressed in ["h", "KEY_SLEFT"]:
1532 1534 self.leftarrowshiftevent()
1533 1535 elif keypressed in ["q"]:
1534 1536 raise util.Abort(_('user quit'))
1535 1537 elif keypressed in ["c"]:
1536 1538 if self.confirmcommit():
1537 1539 return True
1538 1540 elif keypressed in ["r"]:
1539 1541 if self.confirmcommit(review=True):
1540 1542 return True
1541 1543 elif test and keypressed in ['X']:
1542 1544 return True
1543 1545 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1544 1546 self.toggleapply()
1545 1547 elif keypressed in ['A']:
1546 1548 self.toggleall()
1547 1549 elif keypressed in ['e']:
1548 1550 self.toggleedit(test=test)
1549 1551 elif keypressed in ["f"]:
1550 1552 self.togglefolded()
1551 1553 elif keypressed in ["f"]:
1552 1554 self.togglefolded(foldparent=True)
1553 1555 elif keypressed in ["?"]:
1554 1556 self.helpwindow()
1555 1557
1556 1558 def main(self, stdscr):
1557 1559 """
1558 1560 method to be wrapped by curses.wrapper() for selecting chunks.
1559 1561
1560 1562 """
1561 1563 signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1562 1564 self.stdscr = stdscr
1563 1565 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1564 1566
1565 1567 curses.start_color()
1566 1568 curses.use_default_colors()
1567 1569
1568 1570 # available colors: black, blue, cyan, green, magenta, white, yellow
1569 1571 # init_pair(color_id, foreground_color, background_color)
1570 1572 self.initcolorpair(None, None, name="normal")
1571 1573 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1572 1574 name="selected")
1573 1575 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1574 1576 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1575 1577 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1576 1578 # newwin([height, width,] begin_y, begin_x)
1577 1579 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1578 1580 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1579 1581
1580 1582 # figure out how much space to allocate for the chunk-pad which is
1581 1583 # used for displaying the patch
1582 1584
1583 1585 # stupid hack to prevent getnumlinesdisplayed from failing
1584 1586 self.chunkpad = curses.newpad(1, self.xscreensize)
1585 1587
1586 1588 # add 1 so to account for last line text reaching end of line
1587 1589 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1588 1590 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1589 1591
1590 1592 # initialize selecteitemendline (initial start-line is 0)
1591 1593 self.selecteditemendline = self.getnumlinesdisplayed(
1592 1594 self.currentselecteditem, recursechildren=False)
1593 1595
1594 1596 while True:
1595 1597 self.updatescreen()
1596 1598 try:
1597 1599 keypressed = self.statuswin.getkey()
1598 1600 except curses.error:
1599 1601 keypressed = "foobar"
1600 1602 if self.handlekeypressed(keypressed):
1601 1603 break
@@ -1,2478 +1,2480 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import collections
10 10 import cStringIO, email, os, errno, re, posixpath, copy
11 11 import tempfile, zlib, shutil
12 12 # On python2.4 you have to import these by name or they fail to
13 13 # load. This was not a problem on Python 2.7.
14 14 import email.Generator
15 15 import email.Parser
16 16
17 17 from i18n import _
18 18 from node import hex, short
19 19 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
20 20 import pathutil
21 21
22 22 gitre = re.compile('diff --git a/(.*) b/(.*)')
23 23 tabsplitter = re.compile(r'(\t+|[^\t]+)')
24 24
25 25 class PatchError(Exception):
26 26 pass
27 27
28 28
29 29 # public functions
30 30
31 31 def split(stream):
32 32 '''return an iterator of individual patches from a stream'''
33 33 def isheader(line, inheader):
34 34 if inheader and line[0] in (' ', '\t'):
35 35 # continuation
36 36 return True
37 37 if line[0] in (' ', '-', '+'):
38 38 # diff line - don't check for header pattern in there
39 39 return False
40 40 l = line.split(': ', 1)
41 41 return len(l) == 2 and ' ' not in l[0]
42 42
43 43 def chunk(lines):
44 44 return cStringIO.StringIO(''.join(lines))
45 45
46 46 def hgsplit(stream, cur):
47 47 inheader = True
48 48
49 49 for line in stream:
50 50 if not line.strip():
51 51 inheader = False
52 52 if not inheader and line.startswith('# HG changeset patch'):
53 53 yield chunk(cur)
54 54 cur = []
55 55 inheader = True
56 56
57 57 cur.append(line)
58 58
59 59 if cur:
60 60 yield chunk(cur)
61 61
62 62 def mboxsplit(stream, cur):
63 63 for line in stream:
64 64 if line.startswith('From '):
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67 cur = []
68 68
69 69 cur.append(line)
70 70
71 71 if cur:
72 72 for c in split(chunk(cur[1:])):
73 73 yield c
74 74
75 75 def mimesplit(stream, cur):
76 76 def msgfp(m):
77 77 fp = cStringIO.StringIO()
78 78 g = email.Generator.Generator(fp, mangle_from_=False)
79 79 g.flatten(m)
80 80 fp.seek(0)
81 81 return fp
82 82
83 83 for line in stream:
84 84 cur.append(line)
85 85 c = chunk(cur)
86 86
87 87 m = email.Parser.Parser().parse(c)
88 88 if not m.is_multipart():
89 89 yield msgfp(m)
90 90 else:
91 91 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
92 92 for part in m.walk():
93 93 ct = part.get_content_type()
94 94 if ct not in ok_types:
95 95 continue
96 96 yield msgfp(part)
97 97
98 98 def headersplit(stream, cur):
99 99 inheader = False
100 100
101 101 for line in stream:
102 102 if not inheader and isheader(line, inheader):
103 103 yield chunk(cur)
104 104 cur = []
105 105 inheader = True
106 106 if inheader and not isheader(line, inheader):
107 107 inheader = False
108 108
109 109 cur.append(line)
110 110
111 111 if cur:
112 112 yield chunk(cur)
113 113
114 114 def remainder(cur):
115 115 yield chunk(cur)
116 116
117 117 class fiter(object):
118 118 def __init__(self, fp):
119 119 self.fp = fp
120 120
121 121 def __iter__(self):
122 122 return self
123 123
124 124 def next(self):
125 125 l = self.fp.readline()
126 126 if not l:
127 127 raise StopIteration
128 128 return l
129 129
130 130 inheader = False
131 131 cur = []
132 132
133 133 mimeheaders = ['content-type']
134 134
135 135 if not util.safehasattr(stream, 'next'):
136 136 # http responses, for example, have readline but not next
137 137 stream = fiter(stream)
138 138
139 139 for line in stream:
140 140 cur.append(line)
141 141 if line.startswith('# HG changeset patch'):
142 142 return hgsplit(stream, cur)
143 143 elif line.startswith('From '):
144 144 return mboxsplit(stream, cur)
145 145 elif isheader(line, inheader):
146 146 inheader = True
147 147 if line.split(':', 1)[0].lower() in mimeheaders:
148 148 # let email parser handle this
149 149 return mimesplit(stream, cur)
150 150 elif line.startswith('--- ') and inheader:
151 151 # No evil headers seen by diff start, split by hand
152 152 return headersplit(stream, cur)
153 153 # Not enough info, keep reading
154 154
155 155 # if we are here, we have a very plain patch
156 156 return remainder(cur)
157 157
158 158 def extract(ui, fileobj):
159 159 '''extract patch from data read from fileobj.
160 160
161 161 patch can be a normal patch or contained in an email message.
162 162
163 163 return tuple (filename, message, user, date, branch, node, p1, p2).
164 164 Any item in the returned tuple can be None. If filename is None,
165 165 fileobj did not contain a patch. Caller must unlink filename when done.'''
166 166
167 167 # attempt to detect the start of a patch
168 168 # (this heuristic is borrowed from quilt)
169 169 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
170 170 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
171 171 r'---[ \t].*?^\+\+\+[ \t]|'
172 172 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
173 173
174 174 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
175 175 tmpfp = os.fdopen(fd, 'w')
176 176 try:
177 177 msg = email.Parser.Parser().parse(fileobj)
178 178
179 179 subject = msg['Subject']
180 180 user = msg['From']
181 181 if not subject and not user:
182 182 # Not an email, restore parsed headers if any
183 183 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
184 184
185 185 # should try to parse msg['Date']
186 186 date = None
187 187 nodeid = None
188 188 branch = None
189 189 parents = []
190 190
191 191 if subject:
192 192 if subject.startswith('[PATCH'):
193 193 pend = subject.find(']')
194 194 if pend >= 0:
195 195 subject = subject[pend + 1:].lstrip()
196 196 subject = re.sub(r'\n[ \t]+', ' ', subject)
197 197 ui.debug('Subject: %s\n' % subject)
198 198 if user:
199 199 ui.debug('From: %s\n' % user)
200 200 diffs_seen = 0
201 201 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
202 202 message = ''
203 203 for part in msg.walk():
204 204 content_type = part.get_content_type()
205 205 ui.debug('Content-Type: %s\n' % content_type)
206 206 if content_type not in ok_types:
207 207 continue
208 208 payload = part.get_payload(decode=True)
209 209 m = diffre.search(payload)
210 210 if m:
211 211 hgpatch = False
212 212 hgpatchheader = False
213 213 ignoretext = False
214 214
215 215 ui.debug('found patch at byte %d\n' % m.start(0))
216 216 diffs_seen += 1
217 217 cfp = cStringIO.StringIO()
218 218 for line in payload[:m.start(0)].splitlines():
219 219 if line.startswith('# HG changeset patch') and not hgpatch:
220 220 ui.debug('patch generated by hg export\n')
221 221 hgpatch = True
222 222 hgpatchheader = True
223 223 # drop earlier commit message content
224 224 cfp.seek(0)
225 225 cfp.truncate()
226 226 subject = None
227 227 elif hgpatchheader:
228 228 if line.startswith('# User '):
229 229 user = line[7:]
230 230 ui.debug('From: %s\n' % user)
231 231 elif line.startswith("# Date "):
232 232 date = line[7:]
233 233 elif line.startswith("# Branch "):
234 234 branch = line[9:]
235 235 elif line.startswith("# Node ID "):
236 236 nodeid = line[10:]
237 237 elif line.startswith("# Parent "):
238 238 parents.append(line[9:].lstrip())
239 239 elif not line.startswith("# "):
240 240 hgpatchheader = False
241 241 elif line == '---':
242 242 ignoretext = True
243 243 if not hgpatchheader and not ignoretext:
244 244 cfp.write(line)
245 245 cfp.write('\n')
246 246 message = cfp.getvalue()
247 247 if tmpfp:
248 248 tmpfp.write(payload)
249 249 if not payload.endswith('\n'):
250 250 tmpfp.write('\n')
251 251 elif not diffs_seen and message and content_type == 'text/plain':
252 252 message += '\n' + payload
253 253 except: # re-raises
254 254 tmpfp.close()
255 255 os.unlink(tmpname)
256 256 raise
257 257
258 258 if subject and not message.startswith(subject):
259 259 message = '%s\n%s' % (subject, message)
260 260 tmpfp.close()
261 261 if not diffs_seen:
262 262 os.unlink(tmpname)
263 263 return None, message, user, date, branch, None, None, None
264 264
265 265 if parents:
266 266 p1 = parents.pop(0)
267 267 else:
268 268 p1 = None
269 269
270 270 if parents:
271 271 p2 = parents.pop(0)
272 272 else:
273 273 p2 = None
274 274
275 275 return tmpname, message, user, date, branch, nodeid, p1, p2
276 276
277 277 class patchmeta(object):
278 278 """Patched file metadata
279 279
280 280 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
281 281 or COPY. 'path' is patched file path. 'oldpath' is set to the
282 282 origin file when 'op' is either COPY or RENAME, None otherwise. If
283 283 file mode is changed, 'mode' is a tuple (islink, isexec) where
284 284 'islink' is True if the file is a symlink and 'isexec' is True if
285 285 the file is executable. Otherwise, 'mode' is None.
286 286 """
287 287 def __init__(self, path):
288 288 self.path = path
289 289 self.oldpath = None
290 290 self.mode = None
291 291 self.op = 'MODIFY'
292 292 self.binary = False
293 293
294 294 def setmode(self, mode):
295 295 islink = mode & 020000
296 296 isexec = mode & 0100
297 297 self.mode = (islink, isexec)
298 298
299 299 def copy(self):
300 300 other = patchmeta(self.path)
301 301 other.oldpath = self.oldpath
302 302 other.mode = self.mode
303 303 other.op = self.op
304 304 other.binary = self.binary
305 305 return other
306 306
307 307 def _ispatchinga(self, afile):
308 308 if afile == '/dev/null':
309 309 return self.op == 'ADD'
310 310 return afile == 'a/' + (self.oldpath or self.path)
311 311
312 312 def _ispatchingb(self, bfile):
313 313 if bfile == '/dev/null':
314 314 return self.op == 'DELETE'
315 315 return bfile == 'b/' + self.path
316 316
317 317 def ispatching(self, afile, bfile):
318 318 return self._ispatchinga(afile) and self._ispatchingb(bfile)
319 319
320 320 def __repr__(self):
321 321 return "<patchmeta %s %r>" % (self.op, self.path)
322 322
323 323 def readgitpatch(lr):
324 324 """extract git-style metadata about patches from <patchname>"""
325 325
326 326 # Filter patch for git information
327 327 gp = None
328 328 gitpatches = []
329 329 for line in lr:
330 330 line = line.rstrip(' \r\n')
331 331 if line.startswith('diff --git a/'):
332 332 m = gitre.match(line)
333 333 if m:
334 334 if gp:
335 335 gitpatches.append(gp)
336 336 dst = m.group(2)
337 337 gp = patchmeta(dst)
338 338 elif gp:
339 339 if line.startswith('--- '):
340 340 gitpatches.append(gp)
341 341 gp = None
342 342 continue
343 343 if line.startswith('rename from '):
344 344 gp.op = 'RENAME'
345 345 gp.oldpath = line[12:]
346 346 elif line.startswith('rename to '):
347 347 gp.path = line[10:]
348 348 elif line.startswith('copy from '):
349 349 gp.op = 'COPY'
350 350 gp.oldpath = line[10:]
351 351 elif line.startswith('copy to '):
352 352 gp.path = line[8:]
353 353 elif line.startswith('deleted file'):
354 354 gp.op = 'DELETE'
355 355 elif line.startswith('new file mode '):
356 356 gp.op = 'ADD'
357 357 gp.setmode(int(line[-6:], 8))
358 358 elif line.startswith('new mode '):
359 359 gp.setmode(int(line[-6:], 8))
360 360 elif line.startswith('GIT binary patch'):
361 361 gp.binary = True
362 362 if gp:
363 363 gitpatches.append(gp)
364 364
365 365 return gitpatches
366 366
367 367 class linereader(object):
368 368 # simple class to allow pushing lines back into the input stream
369 369 def __init__(self, fp):
370 370 self.fp = fp
371 371 self.buf = []
372 372
373 373 def push(self, line):
374 374 if line is not None:
375 375 self.buf.append(line)
376 376
377 377 def readline(self):
378 378 if self.buf:
379 379 l = self.buf[0]
380 380 del self.buf[0]
381 381 return l
382 382 return self.fp.readline()
383 383
384 384 def __iter__(self):
385 385 while True:
386 386 l = self.readline()
387 387 if not l:
388 388 break
389 389 yield l
390 390
391 391 class abstractbackend(object):
392 392 def __init__(self, ui):
393 393 self.ui = ui
394 394
395 395 def getfile(self, fname):
396 396 """Return target file data and flags as a (data, (islink,
397 397 isexec)) tuple. Data is None if file is missing/deleted.
398 398 """
399 399 raise NotImplementedError
400 400
401 401 def setfile(self, fname, data, mode, copysource):
402 402 """Write data to target file fname and set its mode. mode is a
403 403 (islink, isexec) tuple. If data is None, the file content should
404 404 be left unchanged. If the file is modified after being copied,
405 405 copysource is set to the original file name.
406 406 """
407 407 raise NotImplementedError
408 408
409 409 def unlink(self, fname):
410 410 """Unlink target file."""
411 411 raise NotImplementedError
412 412
413 413 def writerej(self, fname, failed, total, lines):
414 414 """Write rejected lines for fname. total is the number of hunks
415 415 which failed to apply and total the total number of hunks for this
416 416 files.
417 417 """
418 418 pass
419 419
420 420 def exists(self, fname):
421 421 raise NotImplementedError
422 422
423 423 class fsbackend(abstractbackend):
424 424 def __init__(self, ui, basedir):
425 425 super(fsbackend, self).__init__(ui)
426 426 self.opener = scmutil.opener(basedir)
427 427
428 428 def _join(self, f):
429 429 return os.path.join(self.opener.base, f)
430 430
431 431 def getfile(self, fname):
432 432 if self.opener.islink(fname):
433 433 return (self.opener.readlink(fname), (True, False))
434 434
435 435 isexec = False
436 436 try:
437 437 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
438 438 except OSError, e:
439 439 if e.errno != errno.ENOENT:
440 440 raise
441 441 try:
442 442 return (self.opener.read(fname), (False, isexec))
443 443 except IOError, e:
444 444 if e.errno != errno.ENOENT:
445 445 raise
446 446 return None, None
447 447
448 448 def setfile(self, fname, data, mode, copysource):
449 449 islink, isexec = mode
450 450 if data is None:
451 451 self.opener.setflags(fname, islink, isexec)
452 452 return
453 453 if islink:
454 454 self.opener.symlink(data, fname)
455 455 else:
456 456 self.opener.write(fname, data)
457 457 if isexec:
458 458 self.opener.setflags(fname, False, True)
459 459
460 460 def unlink(self, fname):
461 461 self.opener.unlinkpath(fname, ignoremissing=True)
462 462
463 463 def writerej(self, fname, failed, total, lines):
464 464 fname = fname + ".rej"
465 465 self.ui.warn(
466 466 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
467 467 (failed, total, fname))
468 468 fp = self.opener(fname, 'w')
469 469 fp.writelines(lines)
470 470 fp.close()
471 471
472 472 def exists(self, fname):
473 473 return self.opener.lexists(fname)
474 474
475 475 class workingbackend(fsbackend):
476 476 def __init__(self, ui, repo, similarity):
477 477 super(workingbackend, self).__init__(ui, repo.root)
478 478 self.repo = repo
479 479 self.similarity = similarity
480 480 self.removed = set()
481 481 self.changed = set()
482 482 self.copied = []
483 483
484 484 def _checkknown(self, fname):
485 485 if self.repo.dirstate[fname] == '?' and self.exists(fname):
486 486 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
487 487
488 488 def setfile(self, fname, data, mode, copysource):
489 489 self._checkknown(fname)
490 490 super(workingbackend, self).setfile(fname, data, mode, copysource)
491 491 if copysource is not None:
492 492 self.copied.append((copysource, fname))
493 493 self.changed.add(fname)
494 494
495 495 def unlink(self, fname):
496 496 self._checkknown(fname)
497 497 super(workingbackend, self).unlink(fname)
498 498 self.removed.add(fname)
499 499 self.changed.add(fname)
500 500
501 501 def close(self):
502 502 wctx = self.repo[None]
503 503 changed = set(self.changed)
504 504 for src, dst in self.copied:
505 505 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
506 506 if self.removed:
507 507 wctx.forget(sorted(self.removed))
508 508 for f in self.removed:
509 509 if f not in self.repo.dirstate:
510 510 # File was deleted and no longer belongs to the
511 511 # dirstate, it was probably marked added then
512 512 # deleted, and should not be considered by
513 513 # marktouched().
514 514 changed.discard(f)
515 515 if changed:
516 516 scmutil.marktouched(self.repo, changed, self.similarity)
517 517 return sorted(self.changed)
518 518
519 519 class filestore(object):
520 520 def __init__(self, maxsize=None):
521 521 self.opener = None
522 522 self.files = {}
523 523 self.created = 0
524 524 self.maxsize = maxsize
525 525 if self.maxsize is None:
526 526 self.maxsize = 4*(2**20)
527 527 self.size = 0
528 528 self.data = {}
529 529
530 530 def setfile(self, fname, data, mode, copied=None):
531 531 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
532 532 self.data[fname] = (data, mode, copied)
533 533 self.size += len(data)
534 534 else:
535 535 if self.opener is None:
536 536 root = tempfile.mkdtemp(prefix='hg-patch-')
537 537 self.opener = scmutil.opener(root)
538 538 # Avoid filename issues with these simple names
539 539 fn = str(self.created)
540 540 self.opener.write(fn, data)
541 541 self.created += 1
542 542 self.files[fname] = (fn, mode, copied)
543 543
544 544 def getfile(self, fname):
545 545 if fname in self.data:
546 546 return self.data[fname]
547 547 if not self.opener or fname not in self.files:
548 548 return None, None, None
549 549 fn, mode, copied = self.files[fname]
550 550 return self.opener.read(fn), mode, copied
551 551
552 552 def close(self):
553 553 if self.opener:
554 554 shutil.rmtree(self.opener.base)
555 555
556 556 class repobackend(abstractbackend):
557 557 def __init__(self, ui, repo, ctx, store):
558 558 super(repobackend, self).__init__(ui)
559 559 self.repo = repo
560 560 self.ctx = ctx
561 561 self.store = store
562 562 self.changed = set()
563 563 self.removed = set()
564 564 self.copied = {}
565 565
566 566 def _checkknown(self, fname):
567 567 if fname not in self.ctx:
568 568 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
569 569
570 570 def getfile(self, fname):
571 571 try:
572 572 fctx = self.ctx[fname]
573 573 except error.LookupError:
574 574 return None, None
575 575 flags = fctx.flags()
576 576 return fctx.data(), ('l' in flags, 'x' in flags)
577 577
578 578 def setfile(self, fname, data, mode, copysource):
579 579 if copysource:
580 580 self._checkknown(copysource)
581 581 if data is None:
582 582 data = self.ctx[fname].data()
583 583 self.store.setfile(fname, data, mode, copysource)
584 584 self.changed.add(fname)
585 585 if copysource:
586 586 self.copied[fname] = copysource
587 587
588 588 def unlink(self, fname):
589 589 self._checkknown(fname)
590 590 self.removed.add(fname)
591 591
592 592 def exists(self, fname):
593 593 return fname in self.ctx
594 594
595 595 def close(self):
596 596 return self.changed | self.removed
597 597
598 598 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
599 599 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
600 600 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
601 601 eolmodes = ['strict', 'crlf', 'lf', 'auto']
602 602
603 603 class patchfile(object):
604 604 def __init__(self, ui, gp, backend, store, eolmode='strict'):
605 605 self.fname = gp.path
606 606 self.eolmode = eolmode
607 607 self.eol = None
608 608 self.backend = backend
609 609 self.ui = ui
610 610 self.lines = []
611 611 self.exists = False
612 612 self.missing = True
613 613 self.mode = gp.mode
614 614 self.copysource = gp.oldpath
615 615 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
616 616 self.remove = gp.op == 'DELETE'
617 617 if self.copysource is None:
618 618 data, mode = backend.getfile(self.fname)
619 619 else:
620 620 data, mode = store.getfile(self.copysource)[:2]
621 621 if data is not None:
622 622 self.exists = self.copysource is None or backend.exists(self.fname)
623 623 self.missing = False
624 624 if data:
625 625 self.lines = mdiff.splitnewlines(data)
626 626 if self.mode is None:
627 627 self.mode = mode
628 628 if self.lines:
629 629 # Normalize line endings
630 630 if self.lines[0].endswith('\r\n'):
631 631 self.eol = '\r\n'
632 632 elif self.lines[0].endswith('\n'):
633 633 self.eol = '\n'
634 634 if eolmode != 'strict':
635 635 nlines = []
636 636 for l in self.lines:
637 637 if l.endswith('\r\n'):
638 638 l = l[:-2] + '\n'
639 639 nlines.append(l)
640 640 self.lines = nlines
641 641 else:
642 642 if self.create:
643 643 self.missing = False
644 644 if self.mode is None:
645 645 self.mode = (False, False)
646 646 if self.missing:
647 647 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
648 648
649 649 self.hash = {}
650 650 self.dirty = 0
651 651 self.offset = 0
652 652 self.skew = 0
653 653 self.rej = []
654 654 self.fileprinted = False
655 655 self.printfile(False)
656 656 self.hunks = 0
657 657
658 658 def writelines(self, fname, lines, mode):
659 659 if self.eolmode == 'auto':
660 660 eol = self.eol
661 661 elif self.eolmode == 'crlf':
662 662 eol = '\r\n'
663 663 else:
664 664 eol = '\n'
665 665
666 666 if self.eolmode != 'strict' and eol and eol != '\n':
667 667 rawlines = []
668 668 for l in lines:
669 669 if l and l[-1] == '\n':
670 670 l = l[:-1] + eol
671 671 rawlines.append(l)
672 672 lines = rawlines
673 673
674 674 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
675 675
676 676 def printfile(self, warn):
677 677 if self.fileprinted:
678 678 return
679 679 if warn or self.ui.verbose:
680 680 self.fileprinted = True
681 681 s = _("patching file %s\n") % self.fname
682 682 if warn:
683 683 self.ui.warn(s)
684 684 else:
685 685 self.ui.note(s)
686 686
687 687
688 688 def findlines(self, l, linenum):
689 689 # looks through the hash and finds candidate lines. The
690 690 # result is a list of line numbers sorted based on distance
691 691 # from linenum
692 692
693 693 cand = self.hash.get(l, [])
694 694 if len(cand) > 1:
695 695 # resort our list of potentials forward then back.
696 696 cand.sort(key=lambda x: abs(x - linenum))
697 697 return cand
698 698
699 699 def write_rej(self):
700 700 # our rejects are a little different from patch(1). This always
701 701 # creates rejects in the same form as the original patch. A file
702 702 # header is inserted so that you can run the reject through patch again
703 703 # without having to type the filename.
704 704 if not self.rej:
705 705 return
706 706 base = os.path.basename(self.fname)
707 707 lines = ["--- %s\n+++ %s\n" % (base, base)]
708 708 for x in self.rej:
709 709 for l in x.hunk:
710 710 lines.append(l)
711 711 if l[-1] != '\n':
712 712 lines.append("\n\ No newline at end of file\n")
713 713 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
714 714
715 715 def apply(self, h):
716 716 if not h.complete():
717 717 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
718 718 (h.number, h.desc, len(h.a), h.lena, len(h.b),
719 719 h.lenb))
720 720
721 721 self.hunks += 1
722 722
723 723 if self.missing:
724 724 self.rej.append(h)
725 725 return -1
726 726
727 727 if self.exists and self.create:
728 728 if self.copysource:
729 729 self.ui.warn(_("cannot create %s: destination already "
730 730 "exists\n") % self.fname)
731 731 else:
732 732 self.ui.warn(_("file %s already exists\n") % self.fname)
733 733 self.rej.append(h)
734 734 return -1
735 735
736 736 if isinstance(h, binhunk):
737 737 if self.remove:
738 738 self.backend.unlink(self.fname)
739 739 else:
740 740 l = h.new(self.lines)
741 741 self.lines[:] = l
742 742 self.offset += len(l)
743 743 self.dirty = True
744 744 return 0
745 745
746 746 horig = h
747 747 if (self.eolmode in ('crlf', 'lf')
748 748 or self.eolmode == 'auto' and self.eol):
749 749 # If new eols are going to be normalized, then normalize
750 750 # hunk data before patching. Otherwise, preserve input
751 751 # line-endings.
752 752 h = h.getnormalized()
753 753
754 754 # fast case first, no offsets, no fuzz
755 755 old, oldstart, new, newstart = h.fuzzit(0, False)
756 756 oldstart += self.offset
757 757 orig_start = oldstart
758 758 # if there's skew we want to emit the "(offset %d lines)" even
759 759 # when the hunk cleanly applies at start + skew, so skip the
760 760 # fast case code
761 761 if (self.skew == 0 and
762 762 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
763 763 if self.remove:
764 764 self.backend.unlink(self.fname)
765 765 else:
766 766 self.lines[oldstart:oldstart + len(old)] = new
767 767 self.offset += len(new) - len(old)
768 768 self.dirty = True
769 769 return 0
770 770
771 771 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
772 772 self.hash = {}
773 773 for x, s in enumerate(self.lines):
774 774 self.hash.setdefault(s, []).append(x)
775 775
776 776 for fuzzlen in xrange(3):
777 777 for toponly in [True, False]:
778 778 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
779 779 oldstart = oldstart + self.offset + self.skew
780 780 oldstart = min(oldstart, len(self.lines))
781 781 if old:
782 782 cand = self.findlines(old[0][1:], oldstart)
783 783 else:
784 784 # Only adding lines with no or fuzzed context, just
785 785 # take the skew in account
786 786 cand = [oldstart]
787 787
788 788 for l in cand:
789 789 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
790 790 self.lines[l : l + len(old)] = new
791 791 self.offset += len(new) - len(old)
792 792 self.skew = l - orig_start
793 793 self.dirty = True
794 794 offset = l - orig_start - fuzzlen
795 795 if fuzzlen:
796 796 msg = _("Hunk #%d succeeded at %d "
797 797 "with fuzz %d "
798 798 "(offset %d lines).\n")
799 799 self.printfile(True)
800 800 self.ui.warn(msg %
801 801 (h.number, l + 1, fuzzlen, offset))
802 802 else:
803 803 msg = _("Hunk #%d succeeded at %d "
804 804 "(offset %d lines).\n")
805 805 self.ui.note(msg % (h.number, l + 1, offset))
806 806 return fuzzlen
807 807 self.printfile(True)
808 808 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
809 809 self.rej.append(horig)
810 810 return -1
811 811
812 812 def close(self):
813 813 if self.dirty:
814 814 self.writelines(self.fname, self.lines, self.mode)
815 815 self.write_rej()
816 816 return len(self.rej)
817 817
818 818 class header(object):
819 819 """patch header
820 820 """
821 821 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
822 822 diff_re = re.compile('diff -r .* (.*)$')
823 823 allhunks_re = re.compile('(?:index|deleted file) ')
824 824 pretty_re = re.compile('(?:new file|deleted file) ')
825 825 special_re = re.compile('(?:index|deleted|copy|rename) ')
826 826 newfile_re = re.compile('(?:new file)')
827 827
828 828 def __init__(self, header):
829 829 self.header = header
830 830 self.hunks = []
831 831
832 832 def binary(self):
833 833 return any(h.startswith('index ') for h in self.header)
834 834
835 835 def pretty(self, fp):
836 836 for h in self.header:
837 837 if h.startswith('index '):
838 838 fp.write(_('this modifies a binary file (all or nothing)\n'))
839 839 break
840 840 if self.pretty_re.match(h):
841 841 fp.write(h)
842 842 if self.binary():
843 843 fp.write(_('this is a binary file\n'))
844 844 break
845 845 if h.startswith('---'):
846 846 fp.write(_('%d hunks, %d lines changed\n') %
847 847 (len(self.hunks),
848 848 sum([max(h.added, h.removed) for h in self.hunks])))
849 849 break
850 850 fp.write(h)
851 851
852 852 def write(self, fp):
853 853 fp.write(''.join(self.header))
854 854
855 855 def allhunks(self):
856 856 return any(self.allhunks_re.match(h) for h in self.header)
857 857
858 858 def files(self):
859 859 match = self.diffgit_re.match(self.header[0])
860 860 if match:
861 861 fromfile, tofile = match.groups()
862 862 if fromfile == tofile:
863 863 return [fromfile]
864 864 return [fromfile, tofile]
865 865 else:
866 866 return self.diff_re.match(self.header[0]).groups()
867 867
868 868 def filename(self):
869 869 return self.files()[-1]
870 870
871 871 def __repr__(self):
872 872 return '<header %s>' % (' '.join(map(repr, self.files())))
873 873
874 874 def isnewfile(self):
875 875 return any(self.newfile_re.match(h) for h in self.header)
876 876
877 877 def special(self):
878 878 # Special files are shown only at the header level and not at the hunk
879 879 # level for example a file that has been deleted is a special file.
880 880 # The user cannot change the content of the operation, in the case of
881 881 # the deleted file he has to take the deletion or not take it, he
882 882 # cannot take some of it.
883 883 # Newly added files are special if they are empty, they are not special
884 884 # if they have some content as we want to be able to change it
885 885 nocontent = len(self.header) == 2
886 886 emptynewfile = self.isnewfile() and nocontent
887 887 return emptynewfile or \
888 888 any(self.special_re.match(h) for h in self.header)
889 889
890 890 class recordhunk(object):
891 891 """patch hunk
892 892
893 893 XXX shouldn't we merge this with the other hunk class?
894 894 """
895 895 maxcontext = 3
896 896
897 897 def __init__(self, header, fromline, toline, proc, before, hunk, after):
898 898 def trimcontext(number, lines):
899 899 delta = len(lines) - self.maxcontext
900 900 if False and delta > 0:
901 901 return number + delta, lines[:self.maxcontext]
902 902 return number, lines
903 903
904 904 self.header = header
905 905 self.fromline, self.before = trimcontext(fromline, before)
906 906 self.toline, self.after = trimcontext(toline, after)
907 907 self.proc = proc
908 908 self.hunk = hunk
909 909 self.added, self.removed = self.countchanges(self.hunk)
910 910
911 911 def __eq__(self, v):
912 912 if not isinstance(v, recordhunk):
913 913 return False
914 914
915 915 return ((v.hunk == self.hunk) and
916 916 (v.proc == self.proc) and
917 917 (self.fromline == v.fromline) and
918 918 (self.header.files() == v.header.files()))
919 919
920 920 def __hash__(self):
921 921 return hash((tuple(self.hunk),
922 922 tuple(self.header.files()),
923 923 self.fromline,
924 924 self.proc))
925 925
926 926 def countchanges(self, hunk):
927 927 """hunk -> (n+,n-)"""
928 928 add = len([h for h in hunk if h[0] == '+'])
929 929 rem = len([h for h in hunk if h[0] == '-'])
930 930 return add, rem
931 931
932 932 def write(self, fp):
933 933 delta = len(self.before) + len(self.after)
934 934 if self.after and self.after[-1] == '\\ No newline at end of file\n':
935 935 delta -= 1
936 936 fromlen = delta + self.removed
937 937 tolen = delta + self.added
938 938 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
939 939 (self.fromline, fromlen, self.toline, tolen,
940 940 self.proc and (' ' + self.proc)))
941 941 fp.write(''.join(self.before + self.hunk + self.after))
942 942
943 943 pretty = write
944 944
945 945 def filename(self):
946 946 return self.header.filename()
947 947
948 948 def __repr__(self):
949 949 return '<hunk %r@%d>' % (self.filename(), self.fromline)
950 950
951 951 def filterpatch(ui, headers, operation=None):
952 952 """Interactively filter patch chunks into applied-only chunks"""
953 if operation is None:
954 operation = _('record')
953 955
954 956 def prompt(skipfile, skipall, query, chunk):
955 957 """prompt query, and process base inputs
956 958
957 959 - y/n for the rest of file
958 960 - y/n for the rest
959 961 - ? (help)
960 962 - q (quit)
961 963
962 964 Return True/False and possibly updated skipfile and skipall.
963 965 """
964 966 newpatches = None
965 967 if skipall is not None:
966 968 return skipall, skipfile, skipall, newpatches
967 969 if skipfile is not None:
968 970 return skipfile, skipfile, skipall, newpatches
969 971 while True:
970 972 resps = _('[Ynesfdaq?]'
971 973 '$$ &Yes, record this change'
972 974 '$$ &No, skip this change'
973 975 '$$ &Edit this change manually'
974 976 '$$ &Skip remaining changes to this file'
975 977 '$$ Record remaining changes to this &file'
976 978 '$$ &Done, skip remaining changes and files'
977 979 '$$ Record &all changes to all remaining files'
978 980 '$$ &Quit, recording no changes'
979 981 '$$ &? (display help)')
980 982 r = ui.promptchoice("%s %s" % (query, resps))
981 983 ui.write("\n")
982 984 if r == 8: # ?
983 985 for c, t in ui.extractchoices(resps)[1]:
984 986 ui.write('%s - %s\n' % (c, t.lower()))
985 987 continue
986 988 elif r == 0: # yes
987 989 ret = True
988 990 elif r == 1: # no
989 991 ret = False
990 992 elif r == 2: # Edit patch
991 993 if chunk is None:
992 994 ui.write(_('cannot edit patch for whole file'))
993 995 ui.write("\n")
994 996 continue
995 997 if chunk.header.binary():
996 998 ui.write(_('cannot edit patch for binary file'))
997 999 ui.write("\n")
998 1000 continue
999 1001 # Patch comment based on the Git one (based on comment at end of
1000 1002 # http://mercurial.selenic.com/wiki/RecordExtension)
1001 1003 phelp = '---' + _("""
1002 1004 To remove '-' lines, make them ' ' lines (context).
1003 1005 To remove '+' lines, delete them.
1004 1006 Lines starting with # will be removed from the patch.
1005 1007
1006 1008 If the patch applies cleanly, the edited hunk will immediately be
1007 1009 added to the record list. If it does not apply cleanly, a rejects
1008 1010 file will be generated: you can use that when you try again. If
1009 1011 all lines of the hunk are removed, then the edit is aborted and
1010 1012 the hunk is left unchanged.
1011 1013 """)
1012 1014 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1013 1015 suffix=".diff", text=True)
1014 1016 ncpatchfp = None
1015 1017 try:
1016 1018 # Write the initial patch
1017 1019 f = os.fdopen(patchfd, "w")
1018 1020 chunk.header.write(f)
1019 1021 chunk.write(f)
1020 1022 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1021 1023 f.close()
1022 1024 # Start the editor and wait for it to complete
1023 1025 editor = ui.geteditor()
1024 1026 ui.system("%s \"%s\"" % (editor, patchfn),
1025 1027 environ={'HGUSER': ui.username()},
1026 1028 onerr=util.Abort, errprefix=_("edit failed"))
1027 1029 # Remove comment lines
1028 1030 patchfp = open(patchfn)
1029 1031 ncpatchfp = cStringIO.StringIO()
1030 1032 for line in patchfp:
1031 1033 if not line.startswith('#'):
1032 1034 ncpatchfp.write(line)
1033 1035 patchfp.close()
1034 1036 ncpatchfp.seek(0)
1035 1037 newpatches = parsepatch(ncpatchfp)
1036 1038 finally:
1037 1039 os.unlink(patchfn)
1038 1040 del ncpatchfp
1039 1041 # Signal that the chunk shouldn't be applied as-is, but
1040 1042 # provide the new patch to be used instead.
1041 1043 ret = False
1042 1044 elif r == 3: # Skip
1043 1045 ret = skipfile = False
1044 1046 elif r == 4: # file (Record remaining)
1045 1047 ret = skipfile = True
1046 1048 elif r == 5: # done, skip remaining
1047 1049 ret = skipall = False
1048 1050 elif r == 6: # all
1049 1051 ret = skipall = True
1050 1052 elif r == 7: # quit
1051 1053 raise util.Abort(_('user quit'))
1052 1054 return ret, skipfile, skipall, newpatches
1053 1055
1054 1056 seen = set()
1055 1057 applied = {} # 'filename' -> [] of chunks
1056 1058 skipfile, skipall = None, None
1057 1059 pos, total = 1, sum(len(h.hunks) for h in headers)
1058 1060 for h in headers:
1059 1061 pos += len(h.hunks)
1060 1062 skipfile = None
1061 1063 fixoffset = 0
1062 1064 hdr = ''.join(h.header)
1063 1065 if hdr in seen:
1064 1066 continue
1065 1067 seen.add(hdr)
1066 1068 if skipall is None:
1067 1069 h.pretty(ui)
1068 1070 msg = (_('examine changes to %s?') %
1069 1071 _(' and ').join("'%s'" % f for f in h.files()))
1070 1072 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1071 1073 if not r:
1072 1074 continue
1073 1075 applied[h.filename()] = [h]
1074 1076 if h.allhunks():
1075 1077 applied[h.filename()] += h.hunks
1076 1078 continue
1077 1079 for i, chunk in enumerate(h.hunks):
1078 1080 if skipfile is None and skipall is None:
1079 1081 chunk.pretty(ui)
1080 1082 if total == 1:
1081 1083 msg = _("record this change to '%s'?") % chunk.filename()
1082 1084 else:
1083 1085 idx = pos - len(h.hunks) + i
1084 1086 msg = _("record change %d/%d to '%s'?") % (idx, total,
1085 1087 chunk.filename())
1086 1088 r, skipfile, skipall, newpatches = prompt(skipfile,
1087 1089 skipall, msg, chunk)
1088 1090 if r:
1089 1091 if fixoffset:
1090 1092 chunk = copy.copy(chunk)
1091 1093 chunk.toline += fixoffset
1092 1094 applied[chunk.filename()].append(chunk)
1093 1095 elif newpatches is not None:
1094 1096 for newpatch in newpatches:
1095 1097 for newhunk in newpatch.hunks:
1096 1098 if fixoffset:
1097 1099 newhunk.toline += fixoffset
1098 1100 applied[newhunk.filename()].append(newhunk)
1099 1101 else:
1100 1102 fixoffset += chunk.removed - chunk.added
1101 1103 return sum([h for h in applied.itervalues()
1102 1104 if h[0].special() or len(h) > 1], [])
1103 1105 class hunk(object):
1104 1106 def __init__(self, desc, num, lr, context):
1105 1107 self.number = num
1106 1108 self.desc = desc
1107 1109 self.hunk = [desc]
1108 1110 self.a = []
1109 1111 self.b = []
1110 1112 self.starta = self.lena = None
1111 1113 self.startb = self.lenb = None
1112 1114 if lr is not None:
1113 1115 if context:
1114 1116 self.read_context_hunk(lr)
1115 1117 else:
1116 1118 self.read_unified_hunk(lr)
1117 1119
1118 1120 def getnormalized(self):
1119 1121 """Return a copy with line endings normalized to LF."""
1120 1122
1121 1123 def normalize(lines):
1122 1124 nlines = []
1123 1125 for line in lines:
1124 1126 if line.endswith('\r\n'):
1125 1127 line = line[:-2] + '\n'
1126 1128 nlines.append(line)
1127 1129 return nlines
1128 1130
1129 1131 # Dummy object, it is rebuilt manually
1130 1132 nh = hunk(self.desc, self.number, None, None)
1131 1133 nh.number = self.number
1132 1134 nh.desc = self.desc
1133 1135 nh.hunk = self.hunk
1134 1136 nh.a = normalize(self.a)
1135 1137 nh.b = normalize(self.b)
1136 1138 nh.starta = self.starta
1137 1139 nh.startb = self.startb
1138 1140 nh.lena = self.lena
1139 1141 nh.lenb = self.lenb
1140 1142 return nh
1141 1143
1142 1144 def read_unified_hunk(self, lr):
1143 1145 m = unidesc.match(self.desc)
1144 1146 if not m:
1145 1147 raise PatchError(_("bad hunk #%d") % self.number)
1146 1148 self.starta, self.lena, self.startb, self.lenb = m.groups()
1147 1149 if self.lena is None:
1148 1150 self.lena = 1
1149 1151 else:
1150 1152 self.lena = int(self.lena)
1151 1153 if self.lenb is None:
1152 1154 self.lenb = 1
1153 1155 else:
1154 1156 self.lenb = int(self.lenb)
1155 1157 self.starta = int(self.starta)
1156 1158 self.startb = int(self.startb)
1157 1159 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1158 1160 self.b)
1159 1161 # if we hit eof before finishing out the hunk, the last line will
1160 1162 # be zero length. Lets try to fix it up.
1161 1163 while len(self.hunk[-1]) == 0:
1162 1164 del self.hunk[-1]
1163 1165 del self.a[-1]
1164 1166 del self.b[-1]
1165 1167 self.lena -= 1
1166 1168 self.lenb -= 1
1167 1169 self._fixnewline(lr)
1168 1170
1169 1171 def read_context_hunk(self, lr):
1170 1172 self.desc = lr.readline()
1171 1173 m = contextdesc.match(self.desc)
1172 1174 if not m:
1173 1175 raise PatchError(_("bad hunk #%d") % self.number)
1174 1176 self.starta, aend = m.groups()
1175 1177 self.starta = int(self.starta)
1176 1178 if aend is None:
1177 1179 aend = self.starta
1178 1180 self.lena = int(aend) - self.starta
1179 1181 if self.starta:
1180 1182 self.lena += 1
1181 1183 for x in xrange(self.lena):
1182 1184 l = lr.readline()
1183 1185 if l.startswith('---'):
1184 1186 # lines addition, old block is empty
1185 1187 lr.push(l)
1186 1188 break
1187 1189 s = l[2:]
1188 1190 if l.startswith('- ') or l.startswith('! '):
1189 1191 u = '-' + s
1190 1192 elif l.startswith(' '):
1191 1193 u = ' ' + s
1192 1194 else:
1193 1195 raise PatchError(_("bad hunk #%d old text line %d") %
1194 1196 (self.number, x))
1195 1197 self.a.append(u)
1196 1198 self.hunk.append(u)
1197 1199
1198 1200 l = lr.readline()
1199 1201 if l.startswith('\ '):
1200 1202 s = self.a[-1][:-1]
1201 1203 self.a[-1] = s
1202 1204 self.hunk[-1] = s
1203 1205 l = lr.readline()
1204 1206 m = contextdesc.match(l)
1205 1207 if not m:
1206 1208 raise PatchError(_("bad hunk #%d") % self.number)
1207 1209 self.startb, bend = m.groups()
1208 1210 self.startb = int(self.startb)
1209 1211 if bend is None:
1210 1212 bend = self.startb
1211 1213 self.lenb = int(bend) - self.startb
1212 1214 if self.startb:
1213 1215 self.lenb += 1
1214 1216 hunki = 1
1215 1217 for x in xrange(self.lenb):
1216 1218 l = lr.readline()
1217 1219 if l.startswith('\ '):
1218 1220 # XXX: the only way to hit this is with an invalid line range.
1219 1221 # The no-eol marker is not counted in the line range, but I
1220 1222 # guess there are diff(1) out there which behave differently.
1221 1223 s = self.b[-1][:-1]
1222 1224 self.b[-1] = s
1223 1225 self.hunk[hunki - 1] = s
1224 1226 continue
1225 1227 if not l:
1226 1228 # line deletions, new block is empty and we hit EOF
1227 1229 lr.push(l)
1228 1230 break
1229 1231 s = l[2:]
1230 1232 if l.startswith('+ ') or l.startswith('! '):
1231 1233 u = '+' + s
1232 1234 elif l.startswith(' '):
1233 1235 u = ' ' + s
1234 1236 elif len(self.b) == 0:
1235 1237 # line deletions, new block is empty
1236 1238 lr.push(l)
1237 1239 break
1238 1240 else:
1239 1241 raise PatchError(_("bad hunk #%d old text line %d") %
1240 1242 (self.number, x))
1241 1243 self.b.append(s)
1242 1244 while True:
1243 1245 if hunki >= len(self.hunk):
1244 1246 h = ""
1245 1247 else:
1246 1248 h = self.hunk[hunki]
1247 1249 hunki += 1
1248 1250 if h == u:
1249 1251 break
1250 1252 elif h.startswith('-'):
1251 1253 continue
1252 1254 else:
1253 1255 self.hunk.insert(hunki - 1, u)
1254 1256 break
1255 1257
1256 1258 if not self.a:
1257 1259 # this happens when lines were only added to the hunk
1258 1260 for x in self.hunk:
1259 1261 if x.startswith('-') or x.startswith(' '):
1260 1262 self.a.append(x)
1261 1263 if not self.b:
1262 1264 # this happens when lines were only deleted from the hunk
1263 1265 for x in self.hunk:
1264 1266 if x.startswith('+') or x.startswith(' '):
1265 1267 self.b.append(x[1:])
1266 1268 # @@ -start,len +start,len @@
1267 1269 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1268 1270 self.startb, self.lenb)
1269 1271 self.hunk[0] = self.desc
1270 1272 self._fixnewline(lr)
1271 1273
1272 1274 def _fixnewline(self, lr):
1273 1275 l = lr.readline()
1274 1276 if l.startswith('\ '):
1275 1277 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1276 1278 else:
1277 1279 lr.push(l)
1278 1280
1279 1281 def complete(self):
1280 1282 return len(self.a) == self.lena and len(self.b) == self.lenb
1281 1283
1282 1284 def _fuzzit(self, old, new, fuzz, toponly):
1283 1285 # this removes context lines from the top and bottom of list 'l'. It
1284 1286 # checks the hunk to make sure only context lines are removed, and then
1285 1287 # returns a new shortened list of lines.
1286 1288 fuzz = min(fuzz, len(old))
1287 1289 if fuzz:
1288 1290 top = 0
1289 1291 bot = 0
1290 1292 hlen = len(self.hunk)
1291 1293 for x in xrange(hlen - 1):
1292 1294 # the hunk starts with the @@ line, so use x+1
1293 1295 if self.hunk[x + 1][0] == ' ':
1294 1296 top += 1
1295 1297 else:
1296 1298 break
1297 1299 if not toponly:
1298 1300 for x in xrange(hlen - 1):
1299 1301 if self.hunk[hlen - bot - 1][0] == ' ':
1300 1302 bot += 1
1301 1303 else:
1302 1304 break
1303 1305
1304 1306 bot = min(fuzz, bot)
1305 1307 top = min(fuzz, top)
1306 1308 return old[top:len(old) - bot], new[top:len(new) - bot], top
1307 1309 return old, new, 0
1308 1310
1309 1311 def fuzzit(self, fuzz, toponly):
1310 1312 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1311 1313 oldstart = self.starta + top
1312 1314 newstart = self.startb + top
1313 1315 # zero length hunk ranges already have their start decremented
1314 1316 if self.lena and oldstart > 0:
1315 1317 oldstart -= 1
1316 1318 if self.lenb and newstart > 0:
1317 1319 newstart -= 1
1318 1320 return old, oldstart, new, newstart
1319 1321
1320 1322 class binhunk(object):
1321 1323 'A binary patch file.'
1322 1324 def __init__(self, lr, fname):
1323 1325 self.text = None
1324 1326 self.delta = False
1325 1327 self.hunk = ['GIT binary patch\n']
1326 1328 self._fname = fname
1327 1329 self._read(lr)
1328 1330
1329 1331 def complete(self):
1330 1332 return self.text is not None
1331 1333
1332 1334 def new(self, lines):
1333 1335 if self.delta:
1334 1336 return [applybindelta(self.text, ''.join(lines))]
1335 1337 return [self.text]
1336 1338
1337 1339 def _read(self, lr):
1338 1340 def getline(lr, hunk):
1339 1341 l = lr.readline()
1340 1342 hunk.append(l)
1341 1343 return l.rstrip('\r\n')
1342 1344
1343 1345 size = 0
1344 1346 while True:
1345 1347 line = getline(lr, self.hunk)
1346 1348 if not line:
1347 1349 raise PatchError(_('could not extract "%s" binary data')
1348 1350 % self._fname)
1349 1351 if line.startswith('literal '):
1350 1352 size = int(line[8:].rstrip())
1351 1353 break
1352 1354 if line.startswith('delta '):
1353 1355 size = int(line[6:].rstrip())
1354 1356 self.delta = True
1355 1357 break
1356 1358 dec = []
1357 1359 line = getline(lr, self.hunk)
1358 1360 while len(line) > 1:
1359 1361 l = line[0]
1360 1362 if l <= 'Z' and l >= 'A':
1361 1363 l = ord(l) - ord('A') + 1
1362 1364 else:
1363 1365 l = ord(l) - ord('a') + 27
1364 1366 try:
1365 1367 dec.append(base85.b85decode(line[1:])[:l])
1366 1368 except ValueError, e:
1367 1369 raise PatchError(_('could not decode "%s" binary patch: %s')
1368 1370 % (self._fname, str(e)))
1369 1371 line = getline(lr, self.hunk)
1370 1372 text = zlib.decompress(''.join(dec))
1371 1373 if len(text) != size:
1372 1374 raise PatchError(_('"%s" length is %d bytes, should be %d')
1373 1375 % (self._fname, len(text), size))
1374 1376 self.text = text
1375 1377
1376 1378 def parsefilename(str):
1377 1379 # --- filename \t|space stuff
1378 1380 s = str[4:].rstrip('\r\n')
1379 1381 i = s.find('\t')
1380 1382 if i < 0:
1381 1383 i = s.find(' ')
1382 1384 if i < 0:
1383 1385 return s
1384 1386 return s[:i]
1385 1387
1386 1388 def parsepatch(originalchunks):
1387 1389 """patch -> [] of headers -> [] of hunks """
1388 1390 class parser(object):
1389 1391 """patch parsing state machine"""
1390 1392 def __init__(self):
1391 1393 self.fromline = 0
1392 1394 self.toline = 0
1393 1395 self.proc = ''
1394 1396 self.header = None
1395 1397 self.context = []
1396 1398 self.before = []
1397 1399 self.hunk = []
1398 1400 self.headers = []
1399 1401
1400 1402 def addrange(self, limits):
1401 1403 fromstart, fromend, tostart, toend, proc = limits
1402 1404 self.fromline = int(fromstart)
1403 1405 self.toline = int(tostart)
1404 1406 self.proc = proc
1405 1407
1406 1408 def addcontext(self, context):
1407 1409 if self.hunk:
1408 1410 h = recordhunk(self.header, self.fromline, self.toline,
1409 1411 self.proc, self.before, self.hunk, context)
1410 1412 self.header.hunks.append(h)
1411 1413 self.fromline += len(self.before) + h.removed
1412 1414 self.toline += len(self.before) + h.added
1413 1415 self.before = []
1414 1416 self.hunk = []
1415 1417 self.proc = ''
1416 1418 self.context = context
1417 1419
1418 1420 def addhunk(self, hunk):
1419 1421 if self.context:
1420 1422 self.before = self.context
1421 1423 self.context = []
1422 1424 self.hunk = hunk
1423 1425
1424 1426 def newfile(self, hdr):
1425 1427 self.addcontext([])
1426 1428 h = header(hdr)
1427 1429 self.headers.append(h)
1428 1430 self.header = h
1429 1431
1430 1432 def addother(self, line):
1431 1433 pass # 'other' lines are ignored
1432 1434
1433 1435 def finished(self):
1434 1436 self.addcontext([])
1435 1437 return self.headers
1436 1438
1437 1439 transitions = {
1438 1440 'file': {'context': addcontext,
1439 1441 'file': newfile,
1440 1442 'hunk': addhunk,
1441 1443 'range': addrange},
1442 1444 'context': {'file': newfile,
1443 1445 'hunk': addhunk,
1444 1446 'range': addrange,
1445 1447 'other': addother},
1446 1448 'hunk': {'context': addcontext,
1447 1449 'file': newfile,
1448 1450 'range': addrange},
1449 1451 'range': {'context': addcontext,
1450 1452 'hunk': addhunk},
1451 1453 'other': {'other': addother},
1452 1454 }
1453 1455
1454 1456 p = parser()
1455 1457 fp = cStringIO.StringIO()
1456 1458 fp.write(''.join(originalchunks))
1457 1459 fp.seek(0)
1458 1460
1459 1461 state = 'context'
1460 1462 for newstate, data in scanpatch(fp):
1461 1463 try:
1462 1464 p.transitions[state][newstate](p, data)
1463 1465 except KeyError:
1464 1466 raise PatchError('unhandled transition: %s -> %s' %
1465 1467 (state, newstate))
1466 1468 state = newstate
1467 1469 del fp
1468 1470 return p.finished()
1469 1471
1470 1472 def pathtransform(path, strip, prefix):
1471 1473 '''turn a path from a patch into a path suitable for the repository
1472 1474
1473 1475 prefix, if not empty, is expected to be normalized with a / at the end.
1474 1476
1475 1477 Returns (stripped components, path in repository).
1476 1478
1477 1479 >>> pathtransform('a/b/c', 0, '')
1478 1480 ('', 'a/b/c')
1479 1481 >>> pathtransform(' a/b/c ', 0, '')
1480 1482 ('', ' a/b/c')
1481 1483 >>> pathtransform(' a/b/c ', 2, '')
1482 1484 ('a/b/', 'c')
1483 1485 >>> pathtransform('a/b/c', 0, 'd/e/')
1484 1486 ('', 'd/e/a/b/c')
1485 1487 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1486 1488 ('a//b/', 'd/e/c')
1487 1489 >>> pathtransform('a/b/c', 3, '')
1488 1490 Traceback (most recent call last):
1489 1491 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1490 1492 '''
1491 1493 pathlen = len(path)
1492 1494 i = 0
1493 1495 if strip == 0:
1494 1496 return '', prefix + path.rstrip()
1495 1497 count = strip
1496 1498 while count > 0:
1497 1499 i = path.find('/', i)
1498 1500 if i == -1:
1499 1501 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1500 1502 (count, strip, path))
1501 1503 i += 1
1502 1504 # consume '//' in the path
1503 1505 while i < pathlen - 1 and path[i] == '/':
1504 1506 i += 1
1505 1507 count -= 1
1506 1508 return path[:i].lstrip(), prefix + path[i:].rstrip()
1507 1509
1508 1510 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1509 1511 nulla = afile_orig == "/dev/null"
1510 1512 nullb = bfile_orig == "/dev/null"
1511 1513 create = nulla and hunk.starta == 0 and hunk.lena == 0
1512 1514 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1513 1515 abase, afile = pathtransform(afile_orig, strip, prefix)
1514 1516 gooda = not nulla and backend.exists(afile)
1515 1517 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1516 1518 if afile == bfile:
1517 1519 goodb = gooda
1518 1520 else:
1519 1521 goodb = not nullb and backend.exists(bfile)
1520 1522 missing = not goodb and not gooda and not create
1521 1523
1522 1524 # some diff programs apparently produce patches where the afile is
1523 1525 # not /dev/null, but afile starts with bfile
1524 1526 abasedir = afile[:afile.rfind('/') + 1]
1525 1527 bbasedir = bfile[:bfile.rfind('/') + 1]
1526 1528 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1527 1529 and hunk.starta == 0 and hunk.lena == 0):
1528 1530 create = True
1529 1531 missing = False
1530 1532
1531 1533 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1532 1534 # diff is between a file and its backup. In this case, the original
1533 1535 # file should be patched (see original mpatch code).
1534 1536 isbackup = (abase == bbase and bfile.startswith(afile))
1535 1537 fname = None
1536 1538 if not missing:
1537 1539 if gooda and goodb:
1538 1540 if isbackup:
1539 1541 fname = afile
1540 1542 else:
1541 1543 fname = bfile
1542 1544 elif gooda:
1543 1545 fname = afile
1544 1546
1545 1547 if not fname:
1546 1548 if not nullb:
1547 1549 if isbackup:
1548 1550 fname = afile
1549 1551 else:
1550 1552 fname = bfile
1551 1553 elif not nulla:
1552 1554 fname = afile
1553 1555 else:
1554 1556 raise PatchError(_("undefined source and destination files"))
1555 1557
1556 1558 gp = patchmeta(fname)
1557 1559 if create:
1558 1560 gp.op = 'ADD'
1559 1561 elif remove:
1560 1562 gp.op = 'DELETE'
1561 1563 return gp
1562 1564
1563 1565 def scanpatch(fp):
1564 1566 """like patch.iterhunks, but yield different events
1565 1567
1566 1568 - ('file', [header_lines + fromfile + tofile])
1567 1569 - ('context', [context_lines])
1568 1570 - ('hunk', [hunk_lines])
1569 1571 - ('range', (-start,len, +start,len, proc))
1570 1572 """
1571 1573 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1572 1574 lr = linereader(fp)
1573 1575
1574 1576 def scanwhile(first, p):
1575 1577 """scan lr while predicate holds"""
1576 1578 lines = [first]
1577 1579 while True:
1578 1580 line = lr.readline()
1579 1581 if not line:
1580 1582 break
1581 1583 if p(line):
1582 1584 lines.append(line)
1583 1585 else:
1584 1586 lr.push(line)
1585 1587 break
1586 1588 return lines
1587 1589
1588 1590 while True:
1589 1591 line = lr.readline()
1590 1592 if not line:
1591 1593 break
1592 1594 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1593 1595 def notheader(line):
1594 1596 s = line.split(None, 1)
1595 1597 return not s or s[0] not in ('---', 'diff')
1596 1598 header = scanwhile(line, notheader)
1597 1599 fromfile = lr.readline()
1598 1600 if fromfile.startswith('---'):
1599 1601 tofile = lr.readline()
1600 1602 header += [fromfile, tofile]
1601 1603 else:
1602 1604 lr.push(fromfile)
1603 1605 yield 'file', header
1604 1606 elif line[0] == ' ':
1605 1607 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1606 1608 elif line[0] in '-+':
1607 1609 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1608 1610 else:
1609 1611 m = lines_re.match(line)
1610 1612 if m:
1611 1613 yield 'range', m.groups()
1612 1614 else:
1613 1615 yield 'other', line
1614 1616
1615 1617 def scangitpatch(lr, firstline):
1616 1618 """
1617 1619 Git patches can emit:
1618 1620 - rename a to b
1619 1621 - change b
1620 1622 - copy a to c
1621 1623 - change c
1622 1624
1623 1625 We cannot apply this sequence as-is, the renamed 'a' could not be
1624 1626 found for it would have been renamed already. And we cannot copy
1625 1627 from 'b' instead because 'b' would have been changed already. So
1626 1628 we scan the git patch for copy and rename commands so we can
1627 1629 perform the copies ahead of time.
1628 1630 """
1629 1631 pos = 0
1630 1632 try:
1631 1633 pos = lr.fp.tell()
1632 1634 fp = lr.fp
1633 1635 except IOError:
1634 1636 fp = cStringIO.StringIO(lr.fp.read())
1635 1637 gitlr = linereader(fp)
1636 1638 gitlr.push(firstline)
1637 1639 gitpatches = readgitpatch(gitlr)
1638 1640 fp.seek(pos)
1639 1641 return gitpatches
1640 1642
1641 1643 def iterhunks(fp):
1642 1644 """Read a patch and yield the following events:
1643 1645 - ("file", afile, bfile, firsthunk): select a new target file.
1644 1646 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1645 1647 "file" event.
1646 1648 - ("git", gitchanges): current diff is in git format, gitchanges
1647 1649 maps filenames to gitpatch records. Unique event.
1648 1650 """
1649 1651 afile = ""
1650 1652 bfile = ""
1651 1653 state = None
1652 1654 hunknum = 0
1653 1655 emitfile = newfile = False
1654 1656 gitpatches = None
1655 1657
1656 1658 # our states
1657 1659 BFILE = 1
1658 1660 context = None
1659 1661 lr = linereader(fp)
1660 1662
1661 1663 while True:
1662 1664 x = lr.readline()
1663 1665 if not x:
1664 1666 break
1665 1667 if state == BFILE and (
1666 1668 (not context and x[0] == '@')
1667 1669 or (context is not False and x.startswith('***************'))
1668 1670 or x.startswith('GIT binary patch')):
1669 1671 gp = None
1670 1672 if (gitpatches and
1671 1673 gitpatches[-1].ispatching(afile, bfile)):
1672 1674 gp = gitpatches.pop()
1673 1675 if x.startswith('GIT binary patch'):
1674 1676 h = binhunk(lr, gp.path)
1675 1677 else:
1676 1678 if context is None and x.startswith('***************'):
1677 1679 context = True
1678 1680 h = hunk(x, hunknum + 1, lr, context)
1679 1681 hunknum += 1
1680 1682 if emitfile:
1681 1683 emitfile = False
1682 1684 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1683 1685 yield 'hunk', h
1684 1686 elif x.startswith('diff --git a/'):
1685 1687 m = gitre.match(x.rstrip(' \r\n'))
1686 1688 if not m:
1687 1689 continue
1688 1690 if gitpatches is None:
1689 1691 # scan whole input for git metadata
1690 1692 gitpatches = scangitpatch(lr, x)
1691 1693 yield 'git', [g.copy() for g in gitpatches
1692 1694 if g.op in ('COPY', 'RENAME')]
1693 1695 gitpatches.reverse()
1694 1696 afile = 'a/' + m.group(1)
1695 1697 bfile = 'b/' + m.group(2)
1696 1698 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1697 1699 gp = gitpatches.pop()
1698 1700 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1699 1701 if not gitpatches:
1700 1702 raise PatchError(_('failed to synchronize metadata for "%s"')
1701 1703 % afile[2:])
1702 1704 gp = gitpatches[-1]
1703 1705 newfile = True
1704 1706 elif x.startswith('---'):
1705 1707 # check for a unified diff
1706 1708 l2 = lr.readline()
1707 1709 if not l2.startswith('+++'):
1708 1710 lr.push(l2)
1709 1711 continue
1710 1712 newfile = True
1711 1713 context = False
1712 1714 afile = parsefilename(x)
1713 1715 bfile = parsefilename(l2)
1714 1716 elif x.startswith('***'):
1715 1717 # check for a context diff
1716 1718 l2 = lr.readline()
1717 1719 if not l2.startswith('---'):
1718 1720 lr.push(l2)
1719 1721 continue
1720 1722 l3 = lr.readline()
1721 1723 lr.push(l3)
1722 1724 if not l3.startswith("***************"):
1723 1725 lr.push(l2)
1724 1726 continue
1725 1727 newfile = True
1726 1728 context = True
1727 1729 afile = parsefilename(x)
1728 1730 bfile = parsefilename(l2)
1729 1731
1730 1732 if newfile:
1731 1733 newfile = False
1732 1734 emitfile = True
1733 1735 state = BFILE
1734 1736 hunknum = 0
1735 1737
1736 1738 while gitpatches:
1737 1739 gp = gitpatches.pop()
1738 1740 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1739 1741
1740 1742 def applybindelta(binchunk, data):
1741 1743 """Apply a binary delta hunk
1742 1744 The algorithm used is the algorithm from git's patch-delta.c
1743 1745 """
1744 1746 def deltahead(binchunk):
1745 1747 i = 0
1746 1748 for c in binchunk:
1747 1749 i += 1
1748 1750 if not (ord(c) & 0x80):
1749 1751 return i
1750 1752 return i
1751 1753 out = ""
1752 1754 s = deltahead(binchunk)
1753 1755 binchunk = binchunk[s:]
1754 1756 s = deltahead(binchunk)
1755 1757 binchunk = binchunk[s:]
1756 1758 i = 0
1757 1759 while i < len(binchunk):
1758 1760 cmd = ord(binchunk[i])
1759 1761 i += 1
1760 1762 if (cmd & 0x80):
1761 1763 offset = 0
1762 1764 size = 0
1763 1765 if (cmd & 0x01):
1764 1766 offset = ord(binchunk[i])
1765 1767 i += 1
1766 1768 if (cmd & 0x02):
1767 1769 offset |= ord(binchunk[i]) << 8
1768 1770 i += 1
1769 1771 if (cmd & 0x04):
1770 1772 offset |= ord(binchunk[i]) << 16
1771 1773 i += 1
1772 1774 if (cmd & 0x08):
1773 1775 offset |= ord(binchunk[i]) << 24
1774 1776 i += 1
1775 1777 if (cmd & 0x10):
1776 1778 size = ord(binchunk[i])
1777 1779 i += 1
1778 1780 if (cmd & 0x20):
1779 1781 size |= ord(binchunk[i]) << 8
1780 1782 i += 1
1781 1783 if (cmd & 0x40):
1782 1784 size |= ord(binchunk[i]) << 16
1783 1785 i += 1
1784 1786 if size == 0:
1785 1787 size = 0x10000
1786 1788 offset_end = offset + size
1787 1789 out += data[offset:offset_end]
1788 1790 elif cmd != 0:
1789 1791 offset_end = i + cmd
1790 1792 out += binchunk[i:offset_end]
1791 1793 i += cmd
1792 1794 else:
1793 1795 raise PatchError(_('unexpected delta opcode 0'))
1794 1796 return out
1795 1797
1796 1798 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1797 1799 """Reads a patch from fp and tries to apply it.
1798 1800
1799 1801 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1800 1802 there was any fuzz.
1801 1803
1802 1804 If 'eolmode' is 'strict', the patch content and patched file are
1803 1805 read in binary mode. Otherwise, line endings are ignored when
1804 1806 patching then normalized according to 'eolmode'.
1805 1807 """
1806 1808 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1807 1809 prefix=prefix, eolmode=eolmode)
1808 1810
1809 1811 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1810 1812 eolmode='strict'):
1811 1813
1812 1814 if prefix:
1813 1815 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1814 1816 prefix)
1815 1817 if prefix != '':
1816 1818 prefix += '/'
1817 1819 def pstrip(p):
1818 1820 return pathtransform(p, strip - 1, prefix)[1]
1819 1821
1820 1822 rejects = 0
1821 1823 err = 0
1822 1824 current_file = None
1823 1825
1824 1826 for state, values in iterhunks(fp):
1825 1827 if state == 'hunk':
1826 1828 if not current_file:
1827 1829 continue
1828 1830 ret = current_file.apply(values)
1829 1831 if ret > 0:
1830 1832 err = 1
1831 1833 elif state == 'file':
1832 1834 if current_file:
1833 1835 rejects += current_file.close()
1834 1836 current_file = None
1835 1837 afile, bfile, first_hunk, gp = values
1836 1838 if gp:
1837 1839 gp.path = pstrip(gp.path)
1838 1840 if gp.oldpath:
1839 1841 gp.oldpath = pstrip(gp.oldpath)
1840 1842 else:
1841 1843 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1842 1844 prefix)
1843 1845 if gp.op == 'RENAME':
1844 1846 backend.unlink(gp.oldpath)
1845 1847 if not first_hunk:
1846 1848 if gp.op == 'DELETE':
1847 1849 backend.unlink(gp.path)
1848 1850 continue
1849 1851 data, mode = None, None
1850 1852 if gp.op in ('RENAME', 'COPY'):
1851 1853 data, mode = store.getfile(gp.oldpath)[:2]
1852 1854 # FIXME: failing getfile has never been handled here
1853 1855 assert data is not None
1854 1856 if gp.mode:
1855 1857 mode = gp.mode
1856 1858 if gp.op == 'ADD':
1857 1859 # Added files without content have no hunk and
1858 1860 # must be created
1859 1861 data = ''
1860 1862 if data or mode:
1861 1863 if (gp.op in ('ADD', 'RENAME', 'COPY')
1862 1864 and backend.exists(gp.path)):
1863 1865 raise PatchError(_("cannot create %s: destination "
1864 1866 "already exists") % gp.path)
1865 1867 backend.setfile(gp.path, data, mode, gp.oldpath)
1866 1868 continue
1867 1869 try:
1868 1870 current_file = patcher(ui, gp, backend, store,
1869 1871 eolmode=eolmode)
1870 1872 except PatchError, inst:
1871 1873 ui.warn(str(inst) + '\n')
1872 1874 current_file = None
1873 1875 rejects += 1
1874 1876 continue
1875 1877 elif state == 'git':
1876 1878 for gp in values:
1877 1879 path = pstrip(gp.oldpath)
1878 1880 data, mode = backend.getfile(path)
1879 1881 if data is None:
1880 1882 # The error ignored here will trigger a getfile()
1881 1883 # error in a place more appropriate for error
1882 1884 # handling, and will not interrupt the patching
1883 1885 # process.
1884 1886 pass
1885 1887 else:
1886 1888 store.setfile(path, data, mode)
1887 1889 else:
1888 1890 raise util.Abort(_('unsupported parser state: %s') % state)
1889 1891
1890 1892 if current_file:
1891 1893 rejects += current_file.close()
1892 1894
1893 1895 if rejects:
1894 1896 return -1
1895 1897 return err
1896 1898
1897 1899 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1898 1900 similarity):
1899 1901 """use <patcher> to apply <patchname> to the working directory.
1900 1902 returns whether patch was applied with fuzz factor."""
1901 1903
1902 1904 fuzz = False
1903 1905 args = []
1904 1906 cwd = repo.root
1905 1907 if cwd:
1906 1908 args.append('-d %s' % util.shellquote(cwd))
1907 1909 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1908 1910 util.shellquote(patchname)))
1909 1911 try:
1910 1912 for line in fp:
1911 1913 line = line.rstrip()
1912 1914 ui.note(line + '\n')
1913 1915 if line.startswith('patching file '):
1914 1916 pf = util.parsepatchoutput(line)
1915 1917 printed_file = False
1916 1918 files.add(pf)
1917 1919 elif line.find('with fuzz') >= 0:
1918 1920 fuzz = True
1919 1921 if not printed_file:
1920 1922 ui.warn(pf + '\n')
1921 1923 printed_file = True
1922 1924 ui.warn(line + '\n')
1923 1925 elif line.find('saving rejects to file') >= 0:
1924 1926 ui.warn(line + '\n')
1925 1927 elif line.find('FAILED') >= 0:
1926 1928 if not printed_file:
1927 1929 ui.warn(pf + '\n')
1928 1930 printed_file = True
1929 1931 ui.warn(line + '\n')
1930 1932 finally:
1931 1933 if files:
1932 1934 scmutil.marktouched(repo, files, similarity)
1933 1935 code = fp.close()
1934 1936 if code:
1935 1937 raise PatchError(_("patch command failed: %s") %
1936 1938 util.explainexit(code)[0])
1937 1939 return fuzz
1938 1940
1939 1941 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
1940 1942 eolmode='strict'):
1941 1943 if files is None:
1942 1944 files = set()
1943 1945 if eolmode is None:
1944 1946 eolmode = ui.config('patch', 'eol', 'strict')
1945 1947 if eolmode.lower() not in eolmodes:
1946 1948 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1947 1949 eolmode = eolmode.lower()
1948 1950
1949 1951 store = filestore()
1950 1952 try:
1951 1953 fp = open(patchobj, 'rb')
1952 1954 except TypeError:
1953 1955 fp = patchobj
1954 1956 try:
1955 1957 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
1956 1958 eolmode=eolmode)
1957 1959 finally:
1958 1960 if fp != patchobj:
1959 1961 fp.close()
1960 1962 files.update(backend.close())
1961 1963 store.close()
1962 1964 if ret < 0:
1963 1965 raise PatchError(_('patch failed to apply'))
1964 1966 return ret > 0
1965 1967
1966 1968 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
1967 1969 eolmode='strict', similarity=0):
1968 1970 """use builtin patch to apply <patchobj> to the working directory.
1969 1971 returns whether patch was applied with fuzz factor."""
1970 1972 backend = workingbackend(ui, repo, similarity)
1971 1973 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1972 1974
1973 1975 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
1974 1976 eolmode='strict'):
1975 1977 backend = repobackend(ui, repo, ctx, store)
1976 1978 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1977 1979
1978 1980 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
1979 1981 similarity=0):
1980 1982 """Apply <patchname> to the working directory.
1981 1983
1982 1984 'eolmode' specifies how end of lines should be handled. It can be:
1983 1985 - 'strict': inputs are read in binary mode, EOLs are preserved
1984 1986 - 'crlf': EOLs are ignored when patching and reset to CRLF
1985 1987 - 'lf': EOLs are ignored when patching and reset to LF
1986 1988 - None: get it from user settings, default to 'strict'
1987 1989 'eolmode' is ignored when using an external patcher program.
1988 1990
1989 1991 Returns whether patch was applied with fuzz factor.
1990 1992 """
1991 1993 patcher = ui.config('ui', 'patch')
1992 1994 if files is None:
1993 1995 files = set()
1994 1996 if patcher:
1995 1997 return _externalpatch(ui, repo, patcher, patchname, strip,
1996 1998 files, similarity)
1997 1999 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
1998 2000 similarity)
1999 2001
2000 2002 def changedfiles(ui, repo, patchpath, strip=1):
2001 2003 backend = fsbackend(ui, repo.root)
2002 2004 fp = open(patchpath, 'rb')
2003 2005 try:
2004 2006 changed = set()
2005 2007 for state, values in iterhunks(fp):
2006 2008 if state == 'file':
2007 2009 afile, bfile, first_hunk, gp = values
2008 2010 if gp:
2009 2011 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2010 2012 if gp.oldpath:
2011 2013 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2012 2014 else:
2013 2015 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2014 2016 '')
2015 2017 changed.add(gp.path)
2016 2018 if gp.op == 'RENAME':
2017 2019 changed.add(gp.oldpath)
2018 2020 elif state not in ('hunk', 'git'):
2019 2021 raise util.Abort(_('unsupported parser state: %s') % state)
2020 2022 return changed
2021 2023 finally:
2022 2024 fp.close()
2023 2025
2024 2026 class GitDiffRequired(Exception):
2025 2027 pass
2026 2028
2027 2029 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2028 2030 '''return diffopts with all features supported and parsed'''
2029 2031 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2030 2032 git=True, whitespace=True, formatchanging=True)
2031 2033
2032 2034 diffopts = diffallopts
2033 2035
2034 2036 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2035 2037 whitespace=False, formatchanging=False):
2036 2038 '''return diffopts with only opted-in features parsed
2037 2039
2038 2040 Features:
2039 2041 - git: git-style diffs
2040 2042 - whitespace: whitespace options like ignoreblanklines and ignorews
2041 2043 - formatchanging: options that will likely break or cause correctness issues
2042 2044 with most diff parsers
2043 2045 '''
2044 2046 def get(key, name=None, getter=ui.configbool, forceplain=None):
2045 2047 if opts:
2046 2048 v = opts.get(key)
2047 2049 if v:
2048 2050 return v
2049 2051 if forceplain is not None and ui.plain():
2050 2052 return forceplain
2051 2053 return getter(section, name or key, None, untrusted=untrusted)
2052 2054
2053 2055 # core options, expected to be understood by every diff parser
2054 2056 buildopts = {
2055 2057 'nodates': get('nodates'),
2056 2058 'showfunc': get('show_function', 'showfunc'),
2057 2059 'context': get('unified', getter=ui.config),
2058 2060 }
2059 2061
2060 2062 if git:
2061 2063 buildopts['git'] = get('git')
2062 2064 if whitespace:
2063 2065 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2064 2066 buildopts['ignorewsamount'] = get('ignore_space_change',
2065 2067 'ignorewsamount')
2066 2068 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2067 2069 'ignoreblanklines')
2068 2070 if formatchanging:
2069 2071 buildopts['text'] = opts and opts.get('text')
2070 2072 buildopts['nobinary'] = get('nobinary')
2071 2073 buildopts['noprefix'] = get('noprefix', forceplain=False)
2072 2074
2073 2075 return mdiff.diffopts(**buildopts)
2074 2076
2075 2077 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2076 2078 losedatafn=None, prefix='', relroot=''):
2077 2079 '''yields diff of changes to files between two nodes, or node and
2078 2080 working directory.
2079 2081
2080 2082 if node1 is None, use first dirstate parent instead.
2081 2083 if node2 is None, compare node1 with working directory.
2082 2084
2083 2085 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2084 2086 every time some change cannot be represented with the current
2085 2087 patch format. Return False to upgrade to git patch format, True to
2086 2088 accept the loss or raise an exception to abort the diff. It is
2087 2089 called with the name of current file being diffed as 'fn'. If set
2088 2090 to None, patches will always be upgraded to git format when
2089 2091 necessary.
2090 2092
2091 2093 prefix is a filename prefix that is prepended to all filenames on
2092 2094 display (used for subrepos).
2093 2095
2094 2096 relroot, if not empty, must be normalized with a trailing /. Any match
2095 2097 patterns that fall outside it will be ignored.'''
2096 2098
2097 2099 if opts is None:
2098 2100 opts = mdiff.defaultopts
2099 2101
2100 2102 if not node1 and not node2:
2101 2103 node1 = repo.dirstate.p1()
2102 2104
2103 2105 def lrugetfilectx():
2104 2106 cache = {}
2105 2107 order = collections.deque()
2106 2108 def getfilectx(f, ctx):
2107 2109 fctx = ctx.filectx(f, filelog=cache.get(f))
2108 2110 if f not in cache:
2109 2111 if len(cache) > 20:
2110 2112 del cache[order.popleft()]
2111 2113 cache[f] = fctx.filelog()
2112 2114 else:
2113 2115 order.remove(f)
2114 2116 order.append(f)
2115 2117 return fctx
2116 2118 return getfilectx
2117 2119 getfilectx = lrugetfilectx()
2118 2120
2119 2121 ctx1 = repo[node1]
2120 2122 ctx2 = repo[node2]
2121 2123
2122 2124 relfiltered = False
2123 2125 if relroot != '' and match.always():
2124 2126 # as a special case, create a new matcher with just the relroot
2125 2127 pats = [relroot]
2126 2128 match = scmutil.match(ctx2, pats, default='path')
2127 2129 relfiltered = True
2128 2130
2129 2131 if not changes:
2130 2132 changes = repo.status(ctx1, ctx2, match=match)
2131 2133 modified, added, removed = changes[:3]
2132 2134
2133 2135 if not modified and not added and not removed:
2134 2136 return []
2135 2137
2136 2138 if repo.ui.debugflag:
2137 2139 hexfunc = hex
2138 2140 else:
2139 2141 hexfunc = short
2140 2142 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2141 2143
2142 2144 copy = {}
2143 2145 if opts.git or opts.upgrade:
2144 2146 copy = copies.pathcopies(ctx1, ctx2, match=match)
2145 2147
2146 2148 if relroot is not None:
2147 2149 if not relfiltered:
2148 2150 # XXX this would ideally be done in the matcher, but that is
2149 2151 # generally meant to 'or' patterns, not 'and' them. In this case we
2150 2152 # need to 'and' all the patterns from the matcher with relroot.
2151 2153 def filterrel(l):
2152 2154 return [f for f in l if f.startswith(relroot)]
2153 2155 modified = filterrel(modified)
2154 2156 added = filterrel(added)
2155 2157 removed = filterrel(removed)
2156 2158 relfiltered = True
2157 2159 # filter out copies where either side isn't inside the relative root
2158 2160 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2159 2161 if dst.startswith(relroot)
2160 2162 and src.startswith(relroot)))
2161 2163
2162 2164 def difffn(opts, losedata):
2163 2165 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2164 2166 copy, getfilectx, opts, losedata, prefix, relroot)
2165 2167 if opts.upgrade and not opts.git:
2166 2168 try:
2167 2169 def losedata(fn):
2168 2170 if not losedatafn or not losedatafn(fn=fn):
2169 2171 raise GitDiffRequired
2170 2172 # Buffer the whole output until we are sure it can be generated
2171 2173 return list(difffn(opts.copy(git=False), losedata))
2172 2174 except GitDiffRequired:
2173 2175 return difffn(opts.copy(git=True), None)
2174 2176 else:
2175 2177 return difffn(opts, None)
2176 2178
2177 2179 def difflabel(func, *args, **kw):
2178 2180 '''yields 2-tuples of (output, label) based on the output of func()'''
2179 2181 headprefixes = [('diff', 'diff.diffline'),
2180 2182 ('copy', 'diff.extended'),
2181 2183 ('rename', 'diff.extended'),
2182 2184 ('old', 'diff.extended'),
2183 2185 ('new', 'diff.extended'),
2184 2186 ('deleted', 'diff.extended'),
2185 2187 ('---', 'diff.file_a'),
2186 2188 ('+++', 'diff.file_b')]
2187 2189 textprefixes = [('@', 'diff.hunk'),
2188 2190 ('-', 'diff.deleted'),
2189 2191 ('+', 'diff.inserted')]
2190 2192 head = False
2191 2193 for chunk in func(*args, **kw):
2192 2194 lines = chunk.split('\n')
2193 2195 for i, line in enumerate(lines):
2194 2196 if i != 0:
2195 2197 yield ('\n', '')
2196 2198 if head:
2197 2199 if line.startswith('@'):
2198 2200 head = False
2199 2201 else:
2200 2202 if line and line[0] not in ' +-@\\':
2201 2203 head = True
2202 2204 stripline = line
2203 2205 diffline = False
2204 2206 if not head and line and line[0] in '+-':
2205 2207 # highlight tabs and trailing whitespace, but only in
2206 2208 # changed lines
2207 2209 stripline = line.rstrip()
2208 2210 diffline = True
2209 2211
2210 2212 prefixes = textprefixes
2211 2213 if head:
2212 2214 prefixes = headprefixes
2213 2215 for prefix, label in prefixes:
2214 2216 if stripline.startswith(prefix):
2215 2217 if diffline:
2216 2218 for token in tabsplitter.findall(stripline):
2217 2219 if '\t' == token[0]:
2218 2220 yield (token, 'diff.tab')
2219 2221 else:
2220 2222 yield (token, label)
2221 2223 else:
2222 2224 yield (stripline, label)
2223 2225 break
2224 2226 else:
2225 2227 yield (line, '')
2226 2228 if line != stripline:
2227 2229 yield (line[len(stripline):], 'diff.trailingwhitespace')
2228 2230
2229 2231 def diffui(*args, **kw):
2230 2232 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2231 2233 return difflabel(diff, *args, **kw)
2232 2234
2233 2235 def _filepairs(ctx1, modified, added, removed, copy, opts):
2234 2236 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2235 2237 before and f2 is the the name after. For added files, f1 will be None,
2236 2238 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2237 2239 or 'rename' (the latter two only if opts.git is set).'''
2238 2240 gone = set()
2239 2241
2240 2242 copyto = dict([(v, k) for k, v in copy.items()])
2241 2243
2242 2244 addedset, removedset = set(added), set(removed)
2243 2245 # Fix up added, since merged-in additions appear as
2244 2246 # modifications during merges
2245 2247 for f in modified:
2246 2248 if f not in ctx1:
2247 2249 addedset.add(f)
2248 2250
2249 2251 for f in sorted(modified + added + removed):
2250 2252 copyop = None
2251 2253 f1, f2 = f, f
2252 2254 if f in addedset:
2253 2255 f1 = None
2254 2256 if f in copy:
2255 2257 if opts.git:
2256 2258 f1 = copy[f]
2257 2259 if f1 in removedset and f1 not in gone:
2258 2260 copyop = 'rename'
2259 2261 gone.add(f1)
2260 2262 else:
2261 2263 copyop = 'copy'
2262 2264 elif f in removedset:
2263 2265 f2 = None
2264 2266 if opts.git:
2265 2267 # have we already reported a copy above?
2266 2268 if (f in copyto and copyto[f] in addedset
2267 2269 and copy[copyto[f]] == f):
2268 2270 continue
2269 2271 yield f1, f2, copyop
2270 2272
2271 2273 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2272 2274 copy, getfilectx, opts, losedatafn, prefix, relroot):
2273 2275 '''given input data, generate a diff and yield it in blocks
2274 2276
2275 2277 If generating a diff would lose data like flags or binary data and
2276 2278 losedatafn is not None, it will be called.
2277 2279
2278 2280 relroot is removed and prefix is added to every path in the diff output.
2279 2281
2280 2282 If relroot is not empty, this function expects every path in modified,
2281 2283 added, removed and copy to start with it.'''
2282 2284
2283 2285 def gitindex(text):
2284 2286 if not text:
2285 2287 text = ""
2286 2288 l = len(text)
2287 2289 s = util.sha1('blob %d\0' % l)
2288 2290 s.update(text)
2289 2291 return s.hexdigest()
2290 2292
2291 2293 if opts.noprefix:
2292 2294 aprefix = bprefix = ''
2293 2295 else:
2294 2296 aprefix = 'a/'
2295 2297 bprefix = 'b/'
2296 2298
2297 2299 def diffline(f, revs):
2298 2300 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2299 2301 return 'diff %s %s' % (revinfo, f)
2300 2302
2301 2303 date1 = util.datestr(ctx1.date())
2302 2304 date2 = util.datestr(ctx2.date())
2303 2305
2304 2306 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2305 2307
2306 2308 if relroot != '' and (repo.ui.configbool('devel', 'all')
2307 2309 or repo.ui.configbool('devel', 'check-relroot')):
2308 2310 for f in modified + added + removed + copy.keys() + copy.values():
2309 2311 if f is not None and not f.startswith(relroot):
2310 2312 raise AssertionError(
2311 2313 "file %s doesn't start with relroot %s" % (f, relroot))
2312 2314
2313 2315 for f1, f2, copyop in _filepairs(
2314 2316 ctx1, modified, added, removed, copy, opts):
2315 2317 content1 = None
2316 2318 content2 = None
2317 2319 flag1 = None
2318 2320 flag2 = None
2319 2321 if f1:
2320 2322 content1 = getfilectx(f1, ctx1).data()
2321 2323 if opts.git or losedatafn:
2322 2324 flag1 = ctx1.flags(f1)
2323 2325 if f2:
2324 2326 content2 = getfilectx(f2, ctx2).data()
2325 2327 if opts.git or losedatafn:
2326 2328 flag2 = ctx2.flags(f2)
2327 2329 binary = False
2328 2330 if opts.git or losedatafn:
2329 2331 binary = util.binary(content1) or util.binary(content2)
2330 2332
2331 2333 if losedatafn and not opts.git:
2332 2334 if (binary or
2333 2335 # copy/rename
2334 2336 f2 in copy or
2335 2337 # empty file creation
2336 2338 (not f1 and not content2) or
2337 2339 # empty file deletion
2338 2340 (not content1 and not f2) or
2339 2341 # create with flags
2340 2342 (not f1 and flag2) or
2341 2343 # change flags
2342 2344 (f1 and f2 and flag1 != flag2)):
2343 2345 losedatafn(f2 or f1)
2344 2346
2345 2347 path1 = f1 or f2
2346 2348 path2 = f2 or f1
2347 2349 path1 = posixpath.join(prefix, path1[len(relroot):])
2348 2350 path2 = posixpath.join(prefix, path2[len(relroot):])
2349 2351 header = []
2350 2352 if opts.git:
2351 2353 header.append('diff --git %s%s %s%s' %
2352 2354 (aprefix, path1, bprefix, path2))
2353 2355 if not f1: # added
2354 2356 header.append('new file mode %s' % gitmode[flag2])
2355 2357 elif not f2: # removed
2356 2358 header.append('deleted file mode %s' % gitmode[flag1])
2357 2359 else: # modified/copied/renamed
2358 2360 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2359 2361 if mode1 != mode2:
2360 2362 header.append('old mode %s' % mode1)
2361 2363 header.append('new mode %s' % mode2)
2362 2364 if copyop is not None:
2363 2365 header.append('%s from %s' % (copyop, path1))
2364 2366 header.append('%s to %s' % (copyop, path2))
2365 2367 elif revs and not repo.ui.quiet:
2366 2368 header.append(diffline(path1, revs))
2367 2369
2368 2370 if binary and opts.git and not opts.nobinary:
2369 2371 text = mdiff.b85diff(content1, content2)
2370 2372 if text:
2371 2373 header.append('index %s..%s' %
2372 2374 (gitindex(content1), gitindex(content2)))
2373 2375 else:
2374 2376 text = mdiff.unidiff(content1, date1,
2375 2377 content2, date2,
2376 2378 path1, path2, opts=opts)
2377 2379 if header and (text or len(header) > 1):
2378 2380 yield '\n'.join(header) + '\n'
2379 2381 if text:
2380 2382 yield text
2381 2383
2382 2384 def diffstatsum(stats):
2383 2385 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2384 2386 for f, a, r, b in stats:
2385 2387 maxfile = max(maxfile, encoding.colwidth(f))
2386 2388 maxtotal = max(maxtotal, a + r)
2387 2389 addtotal += a
2388 2390 removetotal += r
2389 2391 binary = binary or b
2390 2392
2391 2393 return maxfile, maxtotal, addtotal, removetotal, binary
2392 2394
2393 2395 def diffstatdata(lines):
2394 2396 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2395 2397
2396 2398 results = []
2397 2399 filename, adds, removes, isbinary = None, 0, 0, False
2398 2400
2399 2401 def addresult():
2400 2402 if filename:
2401 2403 results.append((filename, adds, removes, isbinary))
2402 2404
2403 2405 for line in lines:
2404 2406 if line.startswith('diff'):
2405 2407 addresult()
2406 2408 # set numbers to 0 anyway when starting new file
2407 2409 adds, removes, isbinary = 0, 0, False
2408 2410 if line.startswith('diff --git a/'):
2409 2411 filename = gitre.search(line).group(2)
2410 2412 elif line.startswith('diff -r'):
2411 2413 # format: "diff -r ... -r ... filename"
2412 2414 filename = diffre.search(line).group(1)
2413 2415 elif line.startswith('+') and not line.startswith('+++ '):
2414 2416 adds += 1
2415 2417 elif line.startswith('-') and not line.startswith('--- '):
2416 2418 removes += 1
2417 2419 elif (line.startswith('GIT binary patch') or
2418 2420 line.startswith('Binary file')):
2419 2421 isbinary = True
2420 2422 addresult()
2421 2423 return results
2422 2424
2423 2425 def diffstat(lines, width=80, git=False):
2424 2426 output = []
2425 2427 stats = diffstatdata(lines)
2426 2428 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2427 2429
2428 2430 countwidth = len(str(maxtotal))
2429 2431 if hasbinary and countwidth < 3:
2430 2432 countwidth = 3
2431 2433 graphwidth = width - countwidth - maxname - 6
2432 2434 if graphwidth < 10:
2433 2435 graphwidth = 10
2434 2436
2435 2437 def scale(i):
2436 2438 if maxtotal <= graphwidth:
2437 2439 return i
2438 2440 # If diffstat runs out of room it doesn't print anything,
2439 2441 # which isn't very useful, so always print at least one + or -
2440 2442 # if there were at least some changes.
2441 2443 return max(i * graphwidth // maxtotal, int(bool(i)))
2442 2444
2443 2445 for filename, adds, removes, isbinary in stats:
2444 2446 if isbinary:
2445 2447 count = 'Bin'
2446 2448 else:
2447 2449 count = adds + removes
2448 2450 pluses = '+' * scale(adds)
2449 2451 minuses = '-' * scale(removes)
2450 2452 output.append(' %s%s | %*s %s%s\n' %
2451 2453 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2452 2454 countwidth, count, pluses, minuses))
2453 2455
2454 2456 if stats:
2455 2457 output.append(_(' %d files changed, %d insertions(+), '
2456 2458 '%d deletions(-)\n')
2457 2459 % (len(stats), totaladds, totalremoves))
2458 2460
2459 2461 return ''.join(output)
2460 2462
2461 2463 def diffstatui(*args, **kw):
2462 2464 '''like diffstat(), but yields 2-tuples of (output, label) for
2463 2465 ui.write()
2464 2466 '''
2465 2467
2466 2468 for line in diffstat(*args, **kw).splitlines():
2467 2469 if line and line[-1] in '+-':
2468 2470 name, graph = line.rsplit(' ', 1)
2469 2471 yield (name + ' ', '')
2470 2472 m = re.search(r'\++', graph)
2471 2473 if m:
2472 2474 yield (m.group(0), 'diffstat.inserted')
2473 2475 m = re.search(r'-+', graph)
2474 2476 if m:
2475 2477 yield (m.group(0), 'diffstat.deleted')
2476 2478 else:
2477 2479 yield (line, '')
2478 2480 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now