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