##// END OF EJS Templates
patch: implement a new worddiff algorithm...
Jun Wu -
r37750:35632d39 default
parent child Browse files
Show More
@@ -1,532 +1,534 b''
1 1 # utility for color output for Mercurial commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com> and other
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 from __future__ import absolute_import
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13
14 14 from . import (
15 15 encoding,
16 16 pycompat,
17 17 )
18 18
19 19 from .utils import (
20 20 stringutil,
21 21 )
22 22
23 23 try:
24 24 import curses
25 25 # Mapping from effect name to terminfo attribute name (or raw code) or
26 26 # color number. This will also force-load the curses module.
27 27 _baseterminfoparams = {
28 28 'none': (True, 'sgr0', ''),
29 29 'standout': (True, 'smso', ''),
30 30 'underline': (True, 'smul', ''),
31 31 'reverse': (True, 'rev', ''),
32 32 'inverse': (True, 'rev', ''),
33 33 'blink': (True, 'blink', ''),
34 34 'dim': (True, 'dim', ''),
35 35 'bold': (True, 'bold', ''),
36 36 'invisible': (True, 'invis', ''),
37 37 'italic': (True, 'sitm', ''),
38 38 'black': (False, curses.COLOR_BLACK, ''),
39 39 'red': (False, curses.COLOR_RED, ''),
40 40 'green': (False, curses.COLOR_GREEN, ''),
41 41 'yellow': (False, curses.COLOR_YELLOW, ''),
42 42 'blue': (False, curses.COLOR_BLUE, ''),
43 43 'magenta': (False, curses.COLOR_MAGENTA, ''),
44 44 'cyan': (False, curses.COLOR_CYAN, ''),
45 45 'white': (False, curses.COLOR_WHITE, ''),
46 46 }
47 47 except ImportError:
48 48 curses = None
49 49 _baseterminfoparams = {}
50 50
51 51 # start and stop parameters for effects
52 52 _effects = {
53 53 'none': 0,
54 54 'black': 30,
55 55 'red': 31,
56 56 'green': 32,
57 57 'yellow': 33,
58 58 'blue': 34,
59 59 'magenta': 35,
60 60 'cyan': 36,
61 61 'white': 37,
62 62 'bold': 1,
63 63 'italic': 3,
64 64 'underline': 4,
65 65 'inverse': 7,
66 66 'dim': 2,
67 67 'black_background': 40,
68 68 'red_background': 41,
69 69 'green_background': 42,
70 70 'yellow_background': 43,
71 71 'blue_background': 44,
72 72 'purple_background': 45,
73 73 'cyan_background': 46,
74 74 'white_background': 47,
75 75 }
76 76
77 77 _defaultstyles = {
78 78 'grep.match': 'red bold',
79 79 'grep.linenumber': 'green',
80 80 'grep.rev': 'green',
81 81 'grep.change': 'green',
82 82 'grep.sep': 'cyan',
83 83 'grep.filename': 'magenta',
84 84 'grep.user': 'magenta',
85 85 'grep.date': 'magenta',
86 86 'bookmarks.active': 'green',
87 87 'branches.active': 'none',
88 88 'branches.closed': 'black bold',
89 89 'branches.current': 'green',
90 90 'branches.inactive': 'none',
91 91 'diff.changed': 'white',
92 92 'diff.deleted': 'red',
93 'diff.deleted.highlight': 'red bold underline',
93 'diff.deleted.changed': 'red',
94 'diff.deleted.unchanged': 'red dim',
94 95 'diff.diffline': 'bold',
95 96 'diff.extended': 'cyan bold',
96 97 'diff.file_a': 'red bold',
97 98 'diff.file_b': 'green bold',
98 99 'diff.hunk': 'magenta',
99 100 'diff.inserted': 'green',
100 'diff.inserted.highlight': 'green bold underline',
101 'diff.inserted.changed': 'green',
102 'diff.inserted.unchanged': 'green dim',
101 103 'diff.tab': '',
102 104 'diff.trailingwhitespace': 'bold red_background',
103 105 'changeset.public': '',
104 106 'changeset.draft': '',
105 107 'changeset.secret': '',
106 108 'diffstat.deleted': 'red',
107 109 'diffstat.inserted': 'green',
108 110 'formatvariant.name.mismatchconfig': 'red',
109 111 'formatvariant.name.mismatchdefault': 'yellow',
110 112 'formatvariant.name.uptodate': 'green',
111 113 'formatvariant.repo.mismatchconfig': 'red',
112 114 'formatvariant.repo.mismatchdefault': 'yellow',
113 115 'formatvariant.repo.uptodate': 'green',
114 116 'formatvariant.config.special': 'yellow',
115 117 'formatvariant.config.default': 'green',
116 118 'formatvariant.default': '',
117 119 'histedit.remaining': 'red bold',
118 120 'ui.prompt': 'yellow',
119 121 'log.changeset': 'yellow',
120 122 'patchbomb.finalsummary': '',
121 123 'patchbomb.from': 'magenta',
122 124 'patchbomb.to': 'cyan',
123 125 'patchbomb.subject': 'green',
124 126 'patchbomb.diffstats': '',
125 127 'rebase.rebased': 'blue',
126 128 'rebase.remaining': 'red bold',
127 129 'resolve.resolved': 'green bold',
128 130 'resolve.unresolved': 'red bold',
129 131 'shelve.age': 'cyan',
130 132 'shelve.newest': 'green bold',
131 133 'shelve.name': 'blue bold',
132 134 'status.added': 'green bold',
133 135 'status.clean': 'none',
134 136 'status.copied': 'none',
135 137 'status.deleted': 'cyan bold underline',
136 138 'status.ignored': 'black bold',
137 139 'status.modified': 'blue bold',
138 140 'status.removed': 'red bold',
139 141 'status.unknown': 'magenta bold underline',
140 142 'tags.normal': 'green',
141 143 'tags.local': 'black bold',
142 144 }
143 145
144 146 def loadcolortable(ui, extname, colortable):
145 147 _defaultstyles.update(colortable)
146 148
147 149 def _terminfosetup(ui, mode, formatted):
148 150 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
149 151
150 152 # If we failed to load curses, we go ahead and return.
151 153 if curses is None:
152 154 return
153 155 # Otherwise, see what the config file says.
154 156 if mode not in ('auto', 'terminfo'):
155 157 return
156 158 ui._terminfoparams.update(_baseterminfoparams)
157 159
158 160 for key, val in ui.configitems('color'):
159 161 if key.startswith('color.'):
160 162 newval = (False, int(val), '')
161 163 ui._terminfoparams[key[6:]] = newval
162 164 elif key.startswith('terminfo.'):
163 165 newval = (True, '', val.replace('\\E', '\x1b'))
164 166 ui._terminfoparams[key[9:]] = newval
165 167 try:
166 168 curses.setupterm()
167 169 except curses.error as e:
168 170 ui._terminfoparams.clear()
169 171 return
170 172
171 173 for key, (b, e, c) in ui._terminfoparams.copy().items():
172 174 if not b:
173 175 continue
174 176 if not c and not curses.tigetstr(pycompat.sysstr(e)):
175 177 # Most terminals don't support dim, invis, etc, so don't be
176 178 # noisy and use ui.debug().
177 179 ui.debug("no terminfo entry for %s\n" % e)
178 180 del ui._terminfoparams[key]
179 181 if not curses.tigetstr(r'setaf') or not curses.tigetstr(r'setab'):
180 182 # Only warn about missing terminfo entries if we explicitly asked for
181 183 # terminfo mode and we're in a formatted terminal.
182 184 if mode == "terminfo" and formatted:
183 185 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
184 186 "ECMA-48 color\n"))
185 187 ui._terminfoparams.clear()
186 188
187 189 def setup(ui):
188 190 """configure color on a ui
189 191
190 192 That function both set the colormode for the ui object and read
191 193 the configuration looking for custom colors and effect definitions."""
192 194 mode = _modesetup(ui)
193 195 ui._colormode = mode
194 196 if mode and mode != 'debug':
195 197 configstyles(ui)
196 198
197 199 def _modesetup(ui):
198 200 if ui.plain('color'):
199 201 return None
200 202 config = ui.config('ui', 'color')
201 203 if config == 'debug':
202 204 return 'debug'
203 205
204 206 auto = (config == 'auto')
205 207 always = False
206 208 if not auto and stringutil.parsebool(config):
207 209 # We want the config to behave like a boolean, "on" is actually auto,
208 210 # but "always" value is treated as a special case to reduce confusion.
209 211 if ui.configsource('ui', 'color') == '--color' or config == 'always':
210 212 always = True
211 213 else:
212 214 auto = True
213 215
214 216 if not always and not auto:
215 217 return None
216 218
217 219 formatted = (always or (encoding.environ.get('TERM') != 'dumb'
218 220 and ui.formatted()))
219 221
220 222 mode = ui.config('color', 'mode')
221 223
222 224 # If pager is active, color.pagermode overrides color.mode.
223 225 if getattr(ui, 'pageractive', False):
224 226 mode = ui.config('color', 'pagermode', mode)
225 227
226 228 realmode = mode
227 229 if pycompat.iswindows:
228 230 from . import win32
229 231
230 232 term = encoding.environ.get('TERM')
231 233 # TERM won't be defined in a vanilla cmd.exe environment.
232 234
233 235 # UNIX-like environments on Windows such as Cygwin and MSYS will
234 236 # set TERM. They appear to make a best effort attempt at setting it
235 237 # to something appropriate. However, not all environments with TERM
236 238 # defined support ANSI.
237 239 ansienviron = term and 'xterm' in term
238 240
239 241 if mode == 'auto':
240 242 # Since "ansi" could result in terminal gibberish, we error on the
241 243 # side of selecting "win32". However, if w32effects is not defined,
242 244 # we almost certainly don't support "win32", so don't even try.
243 245 # w32ffects is not populated when stdout is redirected, so checking
244 246 # it first avoids win32 calls in a state known to error out.
245 247 if ansienviron or not w32effects or win32.enablevtmode():
246 248 realmode = 'ansi'
247 249 else:
248 250 realmode = 'win32'
249 251 # An empty w32effects is a clue that stdout is redirected, and thus
250 252 # cannot enable VT mode.
251 253 elif mode == 'ansi' and w32effects and not ansienviron:
252 254 win32.enablevtmode()
253 255 elif mode == 'auto':
254 256 realmode = 'ansi'
255 257
256 258 def modewarn():
257 259 # only warn if color.mode was explicitly set and we're in
258 260 # a formatted terminal
259 261 if mode == realmode and formatted:
260 262 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
261 263
262 264 if realmode == 'win32':
263 265 ui._terminfoparams.clear()
264 266 if not w32effects:
265 267 modewarn()
266 268 return None
267 269 elif realmode == 'ansi':
268 270 ui._terminfoparams.clear()
269 271 elif realmode == 'terminfo':
270 272 _terminfosetup(ui, mode, formatted)
271 273 if not ui._terminfoparams:
272 274 ## FIXME Shouldn't we return None in this case too?
273 275 modewarn()
274 276 realmode = 'ansi'
275 277 else:
276 278 return None
277 279
278 280 if always or (auto and formatted):
279 281 return realmode
280 282 return None
281 283
282 284 def configstyles(ui):
283 285 ui._styles.update(_defaultstyles)
284 286 for status, cfgeffects in ui.configitems('color'):
285 287 if '.' not in status or status.startswith(('color.', 'terminfo.')):
286 288 continue
287 289 cfgeffects = ui.configlist('color', status)
288 290 if cfgeffects:
289 291 good = []
290 292 for e in cfgeffects:
291 293 if valideffect(ui, e):
292 294 good.append(e)
293 295 else:
294 296 ui.warn(_("ignoring unknown color/effect %r "
295 297 "(configured in color.%s)\n")
296 298 % (e, status))
297 299 ui._styles[status] = ' '.join(good)
298 300
299 301 def _activeeffects(ui):
300 302 '''Return the effects map for the color mode set on the ui.'''
301 303 if ui._colormode == 'win32':
302 304 return w32effects
303 305 elif ui._colormode is not None:
304 306 return _effects
305 307 return {}
306 308
307 309 def valideffect(ui, effect):
308 310 'Determine if the effect is valid or not.'
309 311 return ((not ui._terminfoparams and effect in _activeeffects(ui))
310 312 or (effect in ui._terminfoparams
311 313 or effect[:-11] in ui._terminfoparams))
312 314
313 315 def _effect_str(ui, effect):
314 316 '''Helper function for render_effects().'''
315 317
316 318 bg = False
317 319 if effect.endswith('_background'):
318 320 bg = True
319 321 effect = effect[:-11]
320 322 try:
321 323 attr, val, termcode = ui._terminfoparams[effect]
322 324 except KeyError:
323 325 return ''
324 326 if attr:
325 327 if termcode:
326 328 return termcode
327 329 else:
328 330 return curses.tigetstr(pycompat.sysstr(val))
329 331 elif bg:
330 332 return curses.tparm(curses.tigetstr(r'setab'), val)
331 333 else:
332 334 return curses.tparm(curses.tigetstr(r'setaf'), val)
333 335
334 336 def _mergeeffects(text, start, stop):
335 337 """Insert start sequence at every occurrence of stop sequence
336 338
337 339 >>> s = _mergeeffects(b'cyan', b'[C]', b'|')
338 340 >>> s = _mergeeffects(s + b'yellow', b'[Y]', b'|')
339 341 >>> s = _mergeeffects(b'ma' + s + b'genta', b'[M]', b'|')
340 342 >>> s = _mergeeffects(b'red' + s, b'[R]', b'|')
341 343 >>> s
342 344 '[R]red[M]ma[Y][C]cyan|[R][M][Y]yellow|[R][M]genta|'
343 345 """
344 346 parts = []
345 347 for t in text.split(stop):
346 348 if not t:
347 349 continue
348 350 parts.extend([start, t, stop])
349 351 return ''.join(parts)
350 352
351 353 def _render_effects(ui, text, effects):
352 354 'Wrap text in commands to turn on each effect.'
353 355 if not text:
354 356 return text
355 357 if ui._terminfoparams:
356 358 start = ''.join(_effect_str(ui, effect)
357 359 for effect in ['none'] + effects.split())
358 360 stop = _effect_str(ui, 'none')
359 361 else:
360 362 activeeffects = _activeeffects(ui)
361 363 start = [pycompat.bytestr(activeeffects[e])
362 364 for e in ['none'] + effects.split()]
363 365 start = '\033[' + ';'.join(start) + 'm'
364 366 stop = '\033[' + pycompat.bytestr(activeeffects['none']) + 'm'
365 367 return _mergeeffects(text, start, stop)
366 368
367 369 _ansieffectre = re.compile(br'\x1b\[[0-9;]*m')
368 370
369 371 def stripeffects(text):
370 372 """Strip ANSI control codes which could be inserted by colorlabel()"""
371 373 return _ansieffectre.sub('', text)
372 374
373 375 def colorlabel(ui, msg, label):
374 376 """add color control code according to the mode"""
375 377 if ui._colormode == 'debug':
376 378 if label and msg:
377 379 if msg.endswith('\n'):
378 380 msg = "[%s|%s]\n" % (label, msg[:-1])
379 381 else:
380 382 msg = "[%s|%s]" % (label, msg)
381 383 elif ui._colormode is not None:
382 384 effects = []
383 385 for l in label.split():
384 386 s = ui._styles.get(l, '')
385 387 if s:
386 388 effects.append(s)
387 389 elif valideffect(ui, l):
388 390 effects.append(l)
389 391 effects = ' '.join(effects)
390 392 if effects:
391 393 msg = '\n'.join([_render_effects(ui, line, effects)
392 394 for line in msg.split('\n')])
393 395 return msg
394 396
395 397 w32effects = None
396 398 if pycompat.iswindows:
397 399 import ctypes
398 400
399 401 _kernel32 = ctypes.windll.kernel32
400 402
401 403 _WORD = ctypes.c_ushort
402 404
403 405 _INVALID_HANDLE_VALUE = -1
404 406
405 407 class _COORD(ctypes.Structure):
406 408 _fields_ = [('X', ctypes.c_short),
407 409 ('Y', ctypes.c_short)]
408 410
409 411 class _SMALL_RECT(ctypes.Structure):
410 412 _fields_ = [('Left', ctypes.c_short),
411 413 ('Top', ctypes.c_short),
412 414 ('Right', ctypes.c_short),
413 415 ('Bottom', ctypes.c_short)]
414 416
415 417 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
416 418 _fields_ = [('dwSize', _COORD),
417 419 ('dwCursorPosition', _COORD),
418 420 ('wAttributes', _WORD),
419 421 ('srWindow', _SMALL_RECT),
420 422 ('dwMaximumWindowSize', _COORD)]
421 423
422 424 _STD_OUTPUT_HANDLE = 0xfffffff5 # (DWORD)-11
423 425 _STD_ERROR_HANDLE = 0xfffffff4 # (DWORD)-12
424 426
425 427 _FOREGROUND_BLUE = 0x0001
426 428 _FOREGROUND_GREEN = 0x0002
427 429 _FOREGROUND_RED = 0x0004
428 430 _FOREGROUND_INTENSITY = 0x0008
429 431
430 432 _BACKGROUND_BLUE = 0x0010
431 433 _BACKGROUND_GREEN = 0x0020
432 434 _BACKGROUND_RED = 0x0040
433 435 _BACKGROUND_INTENSITY = 0x0080
434 436
435 437 _COMMON_LVB_REVERSE_VIDEO = 0x4000
436 438 _COMMON_LVB_UNDERSCORE = 0x8000
437 439
438 440 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
439 441 w32effects = {
440 442 'none': -1,
441 443 'black': 0,
442 444 'red': _FOREGROUND_RED,
443 445 'green': _FOREGROUND_GREEN,
444 446 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
445 447 'blue': _FOREGROUND_BLUE,
446 448 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
447 449 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
448 450 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
449 451 'bold': _FOREGROUND_INTENSITY,
450 452 'black_background': 0x100, # unused value > 0x0f
451 453 'red_background': _BACKGROUND_RED,
452 454 'green_background': _BACKGROUND_GREEN,
453 455 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
454 456 'blue_background': _BACKGROUND_BLUE,
455 457 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
456 458 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
457 459 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
458 460 _BACKGROUND_BLUE),
459 461 'bold_background': _BACKGROUND_INTENSITY,
460 462 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
461 463 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
462 464 }
463 465
464 466 passthrough = {_FOREGROUND_INTENSITY,
465 467 _BACKGROUND_INTENSITY,
466 468 _COMMON_LVB_UNDERSCORE,
467 469 _COMMON_LVB_REVERSE_VIDEO}
468 470
469 471 stdout = _kernel32.GetStdHandle(
470 472 _STD_OUTPUT_HANDLE) # don't close the handle returned
471 473 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
472 474 w32effects = None
473 475 else:
474 476 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
475 477 if not _kernel32.GetConsoleScreenBufferInfo(
476 478 stdout, ctypes.byref(csbi)):
477 479 # stdout may not support GetConsoleScreenBufferInfo()
478 480 # when called from subprocess or redirected
479 481 w32effects = None
480 482 else:
481 483 origattr = csbi.wAttributes
482 484 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
483 485 re.MULTILINE | re.DOTALL)
484 486
485 487 def win32print(ui, writefunc, *msgs, **opts):
486 488 for text in msgs:
487 489 _win32print(ui, text, writefunc, **opts)
488 490
489 491 def _win32print(ui, text, writefunc, **opts):
490 492 label = opts.get(r'label', '')
491 493 attr = origattr
492 494
493 495 def mapcolor(val, attr):
494 496 if val == -1:
495 497 return origattr
496 498 elif val in passthrough:
497 499 return attr | val
498 500 elif val > 0x0f:
499 501 return (val & 0x70) | (attr & 0x8f)
500 502 else:
501 503 return (val & 0x07) | (attr & 0xf8)
502 504
503 505 # determine console attributes based on labels
504 506 for l in label.split():
505 507 style = ui._styles.get(l, '')
506 508 for effect in style.split():
507 509 try:
508 510 attr = mapcolor(w32effects[effect], attr)
509 511 except KeyError:
510 512 # w32effects could not have certain attributes so we skip
511 513 # them if not found
512 514 pass
513 515 # hack to ensure regexp finds data
514 516 if not text.startswith('\033['):
515 517 text = '\033[m' + text
516 518
517 519 # Look for ANSI-like codes embedded in text
518 520 m = re.match(ansire, text)
519 521
520 522 try:
521 523 while m:
522 524 for sattr in m.group(1).split(';'):
523 525 if sattr:
524 526 attr = mapcolor(int(sattr), attr)
525 527 ui.flush()
526 528 _kernel32.SetConsoleTextAttribute(stdout, attr)
527 529 writefunc(m.group(2), **opts)
528 530 m = re.match(ansire, m.group(3))
529 531 finally:
530 532 # Explicitly reset original attributes
531 533 ui.flush()
532 534 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,2875 +1,2946 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, print_function
10 10
11 11 import collections
12 12 import contextlib
13 13 import copy
14 14 import email
15 15 import errno
16 16 import hashlib
17 17 import os
18 18 import posixpath
19 19 import re
20 20 import shutil
21 21 import tempfile
22 22 import zlib
23 23
24 24 from .i18n import _
25 25 from .node import (
26 26 hex,
27 27 short,
28 28 )
29 29 from . import (
30 30 copies,
31 31 diffhelpers,
32 32 encoding,
33 33 error,
34 34 mail,
35 35 mdiff,
36 36 pathutil,
37 37 pycompat,
38 38 scmutil,
39 39 similar,
40 40 util,
41 41 vfs as vfsmod,
42 42 )
43 43 from .utils import (
44 44 dateutil,
45 45 procutil,
46 46 stringutil,
47 47 )
48 48
49 49 stringio = util.stringio
50 50
51 51 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
52 52 tabsplitter = re.compile(br'(\t+|[^\t]+)')
53 _nonwordre = re.compile(br'([^a-zA-Z0-9_\x80-\xff])')
53 wordsplitter = re.compile(br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|'
54 '[^ \ta-zA-Z0-9_\x80-\xff])')
54 55
55 56 PatchError = error.PatchError
56 57
57 58 # public functions
58 59
59 60 def split(stream):
60 61 '''return an iterator of individual patches from a stream'''
61 62 def isheader(line, inheader):
62 63 if inheader and line.startswith((' ', '\t')):
63 64 # continuation
64 65 return True
65 66 if line.startswith((' ', '-', '+')):
66 67 # diff line - don't check for header pattern in there
67 68 return False
68 69 l = line.split(': ', 1)
69 70 return len(l) == 2 and ' ' not in l[0]
70 71
71 72 def chunk(lines):
72 73 return stringio(''.join(lines))
73 74
74 75 def hgsplit(stream, cur):
75 76 inheader = True
76 77
77 78 for line in stream:
78 79 if not line.strip():
79 80 inheader = False
80 81 if not inheader and line.startswith('# HG changeset patch'):
81 82 yield chunk(cur)
82 83 cur = []
83 84 inheader = True
84 85
85 86 cur.append(line)
86 87
87 88 if cur:
88 89 yield chunk(cur)
89 90
90 91 def mboxsplit(stream, cur):
91 92 for line in stream:
92 93 if line.startswith('From '):
93 94 for c in split(chunk(cur[1:])):
94 95 yield c
95 96 cur = []
96 97
97 98 cur.append(line)
98 99
99 100 if cur:
100 101 for c in split(chunk(cur[1:])):
101 102 yield c
102 103
103 104 def mimesplit(stream, cur):
104 105 def msgfp(m):
105 106 fp = stringio()
106 107 g = email.Generator.Generator(fp, mangle_from_=False)
107 108 g.flatten(m)
108 109 fp.seek(0)
109 110 return fp
110 111
111 112 for line in stream:
112 113 cur.append(line)
113 114 c = chunk(cur)
114 115
115 116 m = pycompat.emailparser().parse(c)
116 117 if not m.is_multipart():
117 118 yield msgfp(m)
118 119 else:
119 120 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
120 121 for part in m.walk():
121 122 ct = part.get_content_type()
122 123 if ct not in ok_types:
123 124 continue
124 125 yield msgfp(part)
125 126
126 127 def headersplit(stream, cur):
127 128 inheader = False
128 129
129 130 for line in stream:
130 131 if not inheader and isheader(line, inheader):
131 132 yield chunk(cur)
132 133 cur = []
133 134 inheader = True
134 135 if inheader and not isheader(line, inheader):
135 136 inheader = False
136 137
137 138 cur.append(line)
138 139
139 140 if cur:
140 141 yield chunk(cur)
141 142
142 143 def remainder(cur):
143 144 yield chunk(cur)
144 145
145 146 class fiter(object):
146 147 def __init__(self, fp):
147 148 self.fp = fp
148 149
149 150 def __iter__(self):
150 151 return self
151 152
152 153 def next(self):
153 154 l = self.fp.readline()
154 155 if not l:
155 156 raise StopIteration
156 157 return l
157 158
158 159 __next__ = next
159 160
160 161 inheader = False
161 162 cur = []
162 163
163 164 mimeheaders = ['content-type']
164 165
165 166 if not util.safehasattr(stream, 'next'):
166 167 # http responses, for example, have readline but not next
167 168 stream = fiter(stream)
168 169
169 170 for line in stream:
170 171 cur.append(line)
171 172 if line.startswith('# HG changeset patch'):
172 173 return hgsplit(stream, cur)
173 174 elif line.startswith('From '):
174 175 return mboxsplit(stream, cur)
175 176 elif isheader(line, inheader):
176 177 inheader = True
177 178 if line.split(':', 1)[0].lower() in mimeheaders:
178 179 # let email parser handle this
179 180 return mimesplit(stream, cur)
180 181 elif line.startswith('--- ') and inheader:
181 182 # No evil headers seen by diff start, split by hand
182 183 return headersplit(stream, cur)
183 184 # Not enough info, keep reading
184 185
185 186 # if we are here, we have a very plain patch
186 187 return remainder(cur)
187 188
188 189 ## Some facility for extensible patch parsing:
189 190 # list of pairs ("header to match", "data key")
190 191 patchheadermap = [('Date', 'date'),
191 192 ('Branch', 'branch'),
192 193 ('Node ID', 'nodeid'),
193 194 ]
194 195
195 196 @contextlib.contextmanager
196 197 def extract(ui, fileobj):
197 198 '''extract patch from data read from fileobj.
198 199
199 200 patch can be a normal patch or contained in an email message.
200 201
201 202 return a dictionary. Standard keys are:
202 203 - filename,
203 204 - message,
204 205 - user,
205 206 - date,
206 207 - branch,
207 208 - node,
208 209 - p1,
209 210 - p2.
210 211 Any item can be missing from the dictionary. If filename is missing,
211 212 fileobj did not contain a patch. Caller must unlink filename when done.'''
212 213
213 214 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
214 215 tmpfp = os.fdopen(fd, r'wb')
215 216 try:
216 217 yield _extract(ui, fileobj, tmpname, tmpfp)
217 218 finally:
218 219 tmpfp.close()
219 220 os.unlink(tmpname)
220 221
221 222 def _extract(ui, fileobj, tmpname, tmpfp):
222 223
223 224 # attempt to detect the start of a patch
224 225 # (this heuristic is borrowed from quilt)
225 226 diffre = re.compile(br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
226 227 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
227 228 br'---[ \t].*?^\+\+\+[ \t]|'
228 229 br'\*\*\*[ \t].*?^---[ \t])',
229 230 re.MULTILINE | re.DOTALL)
230 231
231 232 data = {}
232 233
233 234 msg = pycompat.emailparser().parse(fileobj)
234 235
235 236 subject = msg[r'Subject'] and mail.headdecode(msg[r'Subject'])
236 237 data['user'] = msg[r'From'] and mail.headdecode(msg[r'From'])
237 238 if not subject and not data['user']:
238 239 # Not an email, restore parsed headers if any
239 240 subject = '\n'.join(': '.join(map(encoding.strtolocal, h))
240 241 for h in msg.items()) + '\n'
241 242
242 243 # should try to parse msg['Date']
243 244 parents = []
244 245
245 246 if subject:
246 247 if subject.startswith('[PATCH'):
247 248 pend = subject.find(']')
248 249 if pend >= 0:
249 250 subject = subject[pend + 1:].lstrip()
250 251 subject = re.sub(br'\n[ \t]+', ' ', subject)
251 252 ui.debug('Subject: %s\n' % subject)
252 253 if data['user']:
253 254 ui.debug('From: %s\n' % data['user'])
254 255 diffs_seen = 0
255 256 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
256 257 message = ''
257 258 for part in msg.walk():
258 259 content_type = pycompat.bytestr(part.get_content_type())
259 260 ui.debug('Content-Type: %s\n' % content_type)
260 261 if content_type not in ok_types:
261 262 continue
262 263 payload = part.get_payload(decode=True)
263 264 m = diffre.search(payload)
264 265 if m:
265 266 hgpatch = False
266 267 hgpatchheader = False
267 268 ignoretext = False
268 269
269 270 ui.debug('found patch at byte %d\n' % m.start(0))
270 271 diffs_seen += 1
271 272 cfp = stringio()
272 273 for line in payload[:m.start(0)].splitlines():
273 274 if line.startswith('# HG changeset patch') and not hgpatch:
274 275 ui.debug('patch generated by hg export\n')
275 276 hgpatch = True
276 277 hgpatchheader = True
277 278 # drop earlier commit message content
278 279 cfp.seek(0)
279 280 cfp.truncate()
280 281 subject = None
281 282 elif hgpatchheader:
282 283 if line.startswith('# User '):
283 284 data['user'] = line[7:]
284 285 ui.debug('From: %s\n' % data['user'])
285 286 elif line.startswith("# Parent "):
286 287 parents.append(line[9:].lstrip())
287 288 elif line.startswith("# "):
288 289 for header, key in patchheadermap:
289 290 prefix = '# %s ' % header
290 291 if line.startswith(prefix):
291 292 data[key] = line[len(prefix):]
292 293 else:
293 294 hgpatchheader = False
294 295 elif line == '---':
295 296 ignoretext = True
296 297 if not hgpatchheader and not ignoretext:
297 298 cfp.write(line)
298 299 cfp.write('\n')
299 300 message = cfp.getvalue()
300 301 if tmpfp:
301 302 tmpfp.write(payload)
302 303 if not payload.endswith('\n'):
303 304 tmpfp.write('\n')
304 305 elif not diffs_seen and message and content_type == 'text/plain':
305 306 message += '\n' + payload
306 307
307 308 if subject and not message.startswith(subject):
308 309 message = '%s\n%s' % (subject, message)
309 310 data['message'] = message
310 311 tmpfp.close()
311 312 if parents:
312 313 data['p1'] = parents.pop(0)
313 314 if parents:
314 315 data['p2'] = parents.pop(0)
315 316
316 317 if diffs_seen:
317 318 data['filename'] = tmpname
318 319
319 320 return data
320 321
321 322 class patchmeta(object):
322 323 """Patched file metadata
323 324
324 325 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
325 326 or COPY. 'path' is patched file path. 'oldpath' is set to the
326 327 origin file when 'op' is either COPY or RENAME, None otherwise. If
327 328 file mode is changed, 'mode' is a tuple (islink, isexec) where
328 329 'islink' is True if the file is a symlink and 'isexec' is True if
329 330 the file is executable. Otherwise, 'mode' is None.
330 331 """
331 332 def __init__(self, path):
332 333 self.path = path
333 334 self.oldpath = None
334 335 self.mode = None
335 336 self.op = 'MODIFY'
336 337 self.binary = False
337 338
338 339 def setmode(self, mode):
339 340 islink = mode & 0o20000
340 341 isexec = mode & 0o100
341 342 self.mode = (islink, isexec)
342 343
343 344 def copy(self):
344 345 other = patchmeta(self.path)
345 346 other.oldpath = self.oldpath
346 347 other.mode = self.mode
347 348 other.op = self.op
348 349 other.binary = self.binary
349 350 return other
350 351
351 352 def _ispatchinga(self, afile):
352 353 if afile == '/dev/null':
353 354 return self.op == 'ADD'
354 355 return afile == 'a/' + (self.oldpath or self.path)
355 356
356 357 def _ispatchingb(self, bfile):
357 358 if bfile == '/dev/null':
358 359 return self.op == 'DELETE'
359 360 return bfile == 'b/' + self.path
360 361
361 362 def ispatching(self, afile, bfile):
362 363 return self._ispatchinga(afile) and self._ispatchingb(bfile)
363 364
364 365 def __repr__(self):
365 366 return "<patchmeta %s %r>" % (self.op, self.path)
366 367
367 368 def readgitpatch(lr):
368 369 """extract git-style metadata about patches from <patchname>"""
369 370
370 371 # Filter patch for git information
371 372 gp = None
372 373 gitpatches = []
373 374 for line in lr:
374 375 line = line.rstrip(' \r\n')
375 376 if line.startswith('diff --git a/'):
376 377 m = gitre.match(line)
377 378 if m:
378 379 if gp:
379 380 gitpatches.append(gp)
380 381 dst = m.group(2)
381 382 gp = patchmeta(dst)
382 383 elif gp:
383 384 if line.startswith('--- '):
384 385 gitpatches.append(gp)
385 386 gp = None
386 387 continue
387 388 if line.startswith('rename from '):
388 389 gp.op = 'RENAME'
389 390 gp.oldpath = line[12:]
390 391 elif line.startswith('rename to '):
391 392 gp.path = line[10:]
392 393 elif line.startswith('copy from '):
393 394 gp.op = 'COPY'
394 395 gp.oldpath = line[10:]
395 396 elif line.startswith('copy to '):
396 397 gp.path = line[8:]
397 398 elif line.startswith('deleted file'):
398 399 gp.op = 'DELETE'
399 400 elif line.startswith('new file mode '):
400 401 gp.op = 'ADD'
401 402 gp.setmode(int(line[-6:], 8))
402 403 elif line.startswith('new mode '):
403 404 gp.setmode(int(line[-6:], 8))
404 405 elif line.startswith('GIT binary patch'):
405 406 gp.binary = True
406 407 if gp:
407 408 gitpatches.append(gp)
408 409
409 410 return gitpatches
410 411
411 412 class linereader(object):
412 413 # simple class to allow pushing lines back into the input stream
413 414 def __init__(self, fp):
414 415 self.fp = fp
415 416 self.buf = []
416 417
417 418 def push(self, line):
418 419 if line is not None:
419 420 self.buf.append(line)
420 421
421 422 def readline(self):
422 423 if self.buf:
423 424 l = self.buf[0]
424 425 del self.buf[0]
425 426 return l
426 427 return self.fp.readline()
427 428
428 429 def __iter__(self):
429 430 return iter(self.readline, '')
430 431
431 432 class abstractbackend(object):
432 433 def __init__(self, ui):
433 434 self.ui = ui
434 435
435 436 def getfile(self, fname):
436 437 """Return target file data and flags as a (data, (islink,
437 438 isexec)) tuple. Data is None if file is missing/deleted.
438 439 """
439 440 raise NotImplementedError
440 441
441 442 def setfile(self, fname, data, mode, copysource):
442 443 """Write data to target file fname and set its mode. mode is a
443 444 (islink, isexec) tuple. If data is None, the file content should
444 445 be left unchanged. If the file is modified after being copied,
445 446 copysource is set to the original file name.
446 447 """
447 448 raise NotImplementedError
448 449
449 450 def unlink(self, fname):
450 451 """Unlink target file."""
451 452 raise NotImplementedError
452 453
453 454 def writerej(self, fname, failed, total, lines):
454 455 """Write rejected lines for fname. total is the number of hunks
455 456 which failed to apply and total the total number of hunks for this
456 457 files.
457 458 """
458 459
459 460 def exists(self, fname):
460 461 raise NotImplementedError
461 462
462 463 def close(self):
463 464 raise NotImplementedError
464 465
465 466 class fsbackend(abstractbackend):
466 467 def __init__(self, ui, basedir):
467 468 super(fsbackend, self).__init__(ui)
468 469 self.opener = vfsmod.vfs(basedir)
469 470
470 471 def getfile(self, fname):
471 472 if self.opener.islink(fname):
472 473 return (self.opener.readlink(fname), (True, False))
473 474
474 475 isexec = False
475 476 try:
476 477 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
477 478 except OSError as e:
478 479 if e.errno != errno.ENOENT:
479 480 raise
480 481 try:
481 482 return (self.opener.read(fname), (False, isexec))
482 483 except IOError as e:
483 484 if e.errno != errno.ENOENT:
484 485 raise
485 486 return None, None
486 487
487 488 def setfile(self, fname, data, mode, copysource):
488 489 islink, isexec = mode
489 490 if data is None:
490 491 self.opener.setflags(fname, islink, isexec)
491 492 return
492 493 if islink:
493 494 self.opener.symlink(data, fname)
494 495 else:
495 496 self.opener.write(fname, data)
496 497 if isexec:
497 498 self.opener.setflags(fname, False, True)
498 499
499 500 def unlink(self, fname):
500 501 self.opener.unlinkpath(fname, ignoremissing=True)
501 502
502 503 def writerej(self, fname, failed, total, lines):
503 504 fname = fname + ".rej"
504 505 self.ui.warn(
505 506 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
506 507 (failed, total, fname))
507 508 fp = self.opener(fname, 'w')
508 509 fp.writelines(lines)
509 510 fp.close()
510 511
511 512 def exists(self, fname):
512 513 return self.opener.lexists(fname)
513 514
514 515 class workingbackend(fsbackend):
515 516 def __init__(self, ui, repo, similarity):
516 517 super(workingbackend, self).__init__(ui, repo.root)
517 518 self.repo = repo
518 519 self.similarity = similarity
519 520 self.removed = set()
520 521 self.changed = set()
521 522 self.copied = []
522 523
523 524 def _checkknown(self, fname):
524 525 if self.repo.dirstate[fname] == '?' and self.exists(fname):
525 526 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
526 527
527 528 def setfile(self, fname, data, mode, copysource):
528 529 self._checkknown(fname)
529 530 super(workingbackend, self).setfile(fname, data, mode, copysource)
530 531 if copysource is not None:
531 532 self.copied.append((copysource, fname))
532 533 self.changed.add(fname)
533 534
534 535 def unlink(self, fname):
535 536 self._checkknown(fname)
536 537 super(workingbackend, self).unlink(fname)
537 538 self.removed.add(fname)
538 539 self.changed.add(fname)
539 540
540 541 def close(self):
541 542 wctx = self.repo[None]
542 543 changed = set(self.changed)
543 544 for src, dst in self.copied:
544 545 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
545 546 if self.removed:
546 547 wctx.forget(sorted(self.removed))
547 548 for f in self.removed:
548 549 if f not in self.repo.dirstate:
549 550 # File was deleted and no longer belongs to the
550 551 # dirstate, it was probably marked added then
551 552 # deleted, and should not be considered by
552 553 # marktouched().
553 554 changed.discard(f)
554 555 if changed:
555 556 scmutil.marktouched(self.repo, changed, self.similarity)
556 557 return sorted(self.changed)
557 558
558 559 class filestore(object):
559 560 def __init__(self, maxsize=None):
560 561 self.opener = None
561 562 self.files = {}
562 563 self.created = 0
563 564 self.maxsize = maxsize
564 565 if self.maxsize is None:
565 566 self.maxsize = 4*(2**20)
566 567 self.size = 0
567 568 self.data = {}
568 569
569 570 def setfile(self, fname, data, mode, copied=None):
570 571 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
571 572 self.data[fname] = (data, mode, copied)
572 573 self.size += len(data)
573 574 else:
574 575 if self.opener is None:
575 576 root = tempfile.mkdtemp(prefix='hg-patch-')
576 577 self.opener = vfsmod.vfs(root)
577 578 # Avoid filename issues with these simple names
578 579 fn = '%d' % self.created
579 580 self.opener.write(fn, data)
580 581 self.created += 1
581 582 self.files[fname] = (fn, mode, copied)
582 583
583 584 def getfile(self, fname):
584 585 if fname in self.data:
585 586 return self.data[fname]
586 587 if not self.opener or fname not in self.files:
587 588 return None, None, None
588 589 fn, mode, copied = self.files[fname]
589 590 return self.opener.read(fn), mode, copied
590 591
591 592 def close(self):
592 593 if self.opener:
593 594 shutil.rmtree(self.opener.base)
594 595
595 596 class repobackend(abstractbackend):
596 597 def __init__(self, ui, repo, ctx, store):
597 598 super(repobackend, self).__init__(ui)
598 599 self.repo = repo
599 600 self.ctx = ctx
600 601 self.store = store
601 602 self.changed = set()
602 603 self.removed = set()
603 604 self.copied = {}
604 605
605 606 def _checkknown(self, fname):
606 607 if fname not in self.ctx:
607 608 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
608 609
609 610 def getfile(self, fname):
610 611 try:
611 612 fctx = self.ctx[fname]
612 613 except error.LookupError:
613 614 return None, None
614 615 flags = fctx.flags()
615 616 return fctx.data(), ('l' in flags, 'x' in flags)
616 617
617 618 def setfile(self, fname, data, mode, copysource):
618 619 if copysource:
619 620 self._checkknown(copysource)
620 621 if data is None:
621 622 data = self.ctx[fname].data()
622 623 self.store.setfile(fname, data, mode, copysource)
623 624 self.changed.add(fname)
624 625 if copysource:
625 626 self.copied[fname] = copysource
626 627
627 628 def unlink(self, fname):
628 629 self._checkknown(fname)
629 630 self.removed.add(fname)
630 631
631 632 def exists(self, fname):
632 633 return fname in self.ctx
633 634
634 635 def close(self):
635 636 return self.changed | self.removed
636 637
637 638 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
638 639 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
639 640 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
640 641 eolmodes = ['strict', 'crlf', 'lf', 'auto']
641 642
642 643 class patchfile(object):
643 644 def __init__(self, ui, gp, backend, store, eolmode='strict'):
644 645 self.fname = gp.path
645 646 self.eolmode = eolmode
646 647 self.eol = None
647 648 self.backend = backend
648 649 self.ui = ui
649 650 self.lines = []
650 651 self.exists = False
651 652 self.missing = True
652 653 self.mode = gp.mode
653 654 self.copysource = gp.oldpath
654 655 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
655 656 self.remove = gp.op == 'DELETE'
656 657 if self.copysource is None:
657 658 data, mode = backend.getfile(self.fname)
658 659 else:
659 660 data, mode = store.getfile(self.copysource)[:2]
660 661 if data is not None:
661 662 self.exists = self.copysource is None or backend.exists(self.fname)
662 663 self.missing = False
663 664 if data:
664 665 self.lines = mdiff.splitnewlines(data)
665 666 if self.mode is None:
666 667 self.mode = mode
667 668 if self.lines:
668 669 # Normalize line endings
669 670 if self.lines[0].endswith('\r\n'):
670 671 self.eol = '\r\n'
671 672 elif self.lines[0].endswith('\n'):
672 673 self.eol = '\n'
673 674 if eolmode != 'strict':
674 675 nlines = []
675 676 for l in self.lines:
676 677 if l.endswith('\r\n'):
677 678 l = l[:-2] + '\n'
678 679 nlines.append(l)
679 680 self.lines = nlines
680 681 else:
681 682 if self.create:
682 683 self.missing = False
683 684 if self.mode is None:
684 685 self.mode = (False, False)
685 686 if self.missing:
686 687 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
687 688 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
688 689 "current directory)\n"))
689 690
690 691 self.hash = {}
691 692 self.dirty = 0
692 693 self.offset = 0
693 694 self.skew = 0
694 695 self.rej = []
695 696 self.fileprinted = False
696 697 self.printfile(False)
697 698 self.hunks = 0
698 699
699 700 def writelines(self, fname, lines, mode):
700 701 if self.eolmode == 'auto':
701 702 eol = self.eol
702 703 elif self.eolmode == 'crlf':
703 704 eol = '\r\n'
704 705 else:
705 706 eol = '\n'
706 707
707 708 if self.eolmode != 'strict' and eol and eol != '\n':
708 709 rawlines = []
709 710 for l in lines:
710 711 if l and l[-1] == '\n':
711 712 l = l[:-1] + eol
712 713 rawlines.append(l)
713 714 lines = rawlines
714 715
715 716 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
716 717
717 718 def printfile(self, warn):
718 719 if self.fileprinted:
719 720 return
720 721 if warn or self.ui.verbose:
721 722 self.fileprinted = True
722 723 s = _("patching file %s\n") % self.fname
723 724 if warn:
724 725 self.ui.warn(s)
725 726 else:
726 727 self.ui.note(s)
727 728
728 729
729 730 def findlines(self, l, linenum):
730 731 # looks through the hash and finds candidate lines. The
731 732 # result is a list of line numbers sorted based on distance
732 733 # from linenum
733 734
734 735 cand = self.hash.get(l, [])
735 736 if len(cand) > 1:
736 737 # resort our list of potentials forward then back.
737 738 cand.sort(key=lambda x: abs(x - linenum))
738 739 return cand
739 740
740 741 def write_rej(self):
741 742 # our rejects are a little different from patch(1). This always
742 743 # creates rejects in the same form as the original patch. A file
743 744 # header is inserted so that you can run the reject through patch again
744 745 # without having to type the filename.
745 746 if not self.rej:
746 747 return
747 748 base = os.path.basename(self.fname)
748 749 lines = ["--- %s\n+++ %s\n" % (base, base)]
749 750 for x in self.rej:
750 751 for l in x.hunk:
751 752 lines.append(l)
752 753 if l[-1:] != '\n':
753 754 lines.append("\n\ No newline at end of file\n")
754 755 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
755 756
756 757 def apply(self, h):
757 758 if not h.complete():
758 759 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
759 760 (h.number, h.desc, len(h.a), h.lena, len(h.b),
760 761 h.lenb))
761 762
762 763 self.hunks += 1
763 764
764 765 if self.missing:
765 766 self.rej.append(h)
766 767 return -1
767 768
768 769 if self.exists and self.create:
769 770 if self.copysource:
770 771 self.ui.warn(_("cannot create %s: destination already "
771 772 "exists\n") % self.fname)
772 773 else:
773 774 self.ui.warn(_("file %s already exists\n") % self.fname)
774 775 self.rej.append(h)
775 776 return -1
776 777
777 778 if isinstance(h, binhunk):
778 779 if self.remove:
779 780 self.backend.unlink(self.fname)
780 781 else:
781 782 l = h.new(self.lines)
782 783 self.lines[:] = l
783 784 self.offset += len(l)
784 785 self.dirty = True
785 786 return 0
786 787
787 788 horig = h
788 789 if (self.eolmode in ('crlf', 'lf')
789 790 or self.eolmode == 'auto' and self.eol):
790 791 # If new eols are going to be normalized, then normalize
791 792 # hunk data before patching. Otherwise, preserve input
792 793 # line-endings.
793 794 h = h.getnormalized()
794 795
795 796 # fast case first, no offsets, no fuzz
796 797 old, oldstart, new, newstart = h.fuzzit(0, False)
797 798 oldstart += self.offset
798 799 orig_start = oldstart
799 800 # if there's skew we want to emit the "(offset %d lines)" even
800 801 # when the hunk cleanly applies at start + skew, so skip the
801 802 # fast case code
802 803 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, oldstart):
803 804 if self.remove:
804 805 self.backend.unlink(self.fname)
805 806 else:
806 807 self.lines[oldstart:oldstart + len(old)] = new
807 808 self.offset += len(new) - len(old)
808 809 self.dirty = True
809 810 return 0
810 811
811 812 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
812 813 self.hash = {}
813 814 for x, s in enumerate(self.lines):
814 815 self.hash.setdefault(s, []).append(x)
815 816
816 817 for fuzzlen in xrange(self.ui.configint("patch", "fuzz") + 1):
817 818 for toponly in [True, False]:
818 819 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
819 820 oldstart = oldstart + self.offset + self.skew
820 821 oldstart = min(oldstart, len(self.lines))
821 822 if old:
822 823 cand = self.findlines(old[0][1:], oldstart)
823 824 else:
824 825 # Only adding lines with no or fuzzed context, just
825 826 # take the skew in account
826 827 cand = [oldstart]
827 828
828 829 for l in cand:
829 830 if not old or diffhelpers.testhunk(old, self.lines, l):
830 831 self.lines[l : l + len(old)] = new
831 832 self.offset += len(new) - len(old)
832 833 self.skew = l - orig_start
833 834 self.dirty = True
834 835 offset = l - orig_start - fuzzlen
835 836 if fuzzlen:
836 837 msg = _("Hunk #%d succeeded at %d "
837 838 "with fuzz %d "
838 839 "(offset %d lines).\n")
839 840 self.printfile(True)
840 841 self.ui.warn(msg %
841 842 (h.number, l + 1, fuzzlen, offset))
842 843 else:
843 844 msg = _("Hunk #%d succeeded at %d "
844 845 "(offset %d lines).\n")
845 846 self.ui.note(msg % (h.number, l + 1, offset))
846 847 return fuzzlen
847 848 self.printfile(True)
848 849 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
849 850 self.rej.append(horig)
850 851 return -1
851 852
852 853 def close(self):
853 854 if self.dirty:
854 855 self.writelines(self.fname, self.lines, self.mode)
855 856 self.write_rej()
856 857 return len(self.rej)
857 858
858 859 class header(object):
859 860 """patch header
860 861 """
861 862 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
862 863 diff_re = re.compile('diff -r .* (.*)$')
863 864 allhunks_re = re.compile('(?:index|deleted file) ')
864 865 pretty_re = re.compile('(?:new file|deleted file) ')
865 866 special_re = re.compile('(?:index|deleted|copy|rename) ')
866 867 newfile_re = re.compile('(?:new file)')
867 868
868 869 def __init__(self, header):
869 870 self.header = header
870 871 self.hunks = []
871 872
872 873 def binary(self):
873 874 return any(h.startswith('index ') for h in self.header)
874 875
875 876 def pretty(self, fp):
876 877 for h in self.header:
877 878 if h.startswith('index '):
878 879 fp.write(_('this modifies a binary file (all or nothing)\n'))
879 880 break
880 881 if self.pretty_re.match(h):
881 882 fp.write(h)
882 883 if self.binary():
883 884 fp.write(_('this is a binary file\n'))
884 885 break
885 886 if h.startswith('---'):
886 887 fp.write(_('%d hunks, %d lines changed\n') %
887 888 (len(self.hunks),
888 889 sum([max(h.added, h.removed) for h in self.hunks])))
889 890 break
890 891 fp.write(h)
891 892
892 893 def write(self, fp):
893 894 fp.write(''.join(self.header))
894 895
895 896 def allhunks(self):
896 897 return any(self.allhunks_re.match(h) for h in self.header)
897 898
898 899 def files(self):
899 900 match = self.diffgit_re.match(self.header[0])
900 901 if match:
901 902 fromfile, tofile = match.groups()
902 903 if fromfile == tofile:
903 904 return [fromfile]
904 905 return [fromfile, tofile]
905 906 else:
906 907 return self.diff_re.match(self.header[0]).groups()
907 908
908 909 def filename(self):
909 910 return self.files()[-1]
910 911
911 912 def __repr__(self):
912 913 return '<header %s>' % (' '.join(map(repr, self.files())))
913 914
914 915 def isnewfile(self):
915 916 return any(self.newfile_re.match(h) for h in self.header)
916 917
917 918 def special(self):
918 919 # Special files are shown only at the header level and not at the hunk
919 920 # level for example a file that has been deleted is a special file.
920 921 # The user cannot change the content of the operation, in the case of
921 922 # the deleted file he has to take the deletion or not take it, he
922 923 # cannot take some of it.
923 924 # Newly added files are special if they are empty, they are not special
924 925 # if they have some content as we want to be able to change it
925 926 nocontent = len(self.header) == 2
926 927 emptynewfile = self.isnewfile() and nocontent
927 928 return emptynewfile or \
928 929 any(self.special_re.match(h) for h in self.header)
929 930
930 931 class recordhunk(object):
931 932 """patch hunk
932 933
933 934 XXX shouldn't we merge this with the other hunk class?
934 935 """
935 936
936 937 def __init__(self, header, fromline, toline, proc, before, hunk, after,
937 938 maxcontext=None):
938 939 def trimcontext(lines, reverse=False):
939 940 if maxcontext is not None:
940 941 delta = len(lines) - maxcontext
941 942 if delta > 0:
942 943 if reverse:
943 944 return delta, lines[delta:]
944 945 else:
945 946 return delta, lines[:maxcontext]
946 947 return 0, lines
947 948
948 949 self.header = header
949 950 trimedbefore, self.before = trimcontext(before, True)
950 951 self.fromline = fromline + trimedbefore
951 952 self.toline = toline + trimedbefore
952 953 _trimedafter, self.after = trimcontext(after, False)
953 954 self.proc = proc
954 955 self.hunk = hunk
955 956 self.added, self.removed = self.countchanges(self.hunk)
956 957
957 958 def __eq__(self, v):
958 959 if not isinstance(v, recordhunk):
959 960 return False
960 961
961 962 return ((v.hunk == self.hunk) and
962 963 (v.proc == self.proc) and
963 964 (self.fromline == v.fromline) and
964 965 (self.header.files() == v.header.files()))
965 966
966 967 def __hash__(self):
967 968 return hash((tuple(self.hunk),
968 969 tuple(self.header.files()),
969 970 self.fromline,
970 971 self.proc))
971 972
972 973 def countchanges(self, hunk):
973 974 """hunk -> (n+,n-)"""
974 975 add = len([h for h in hunk if h.startswith('+')])
975 976 rem = len([h for h in hunk if h.startswith('-')])
976 977 return add, rem
977 978
978 979 def reversehunk(self):
979 980 """return another recordhunk which is the reverse of the hunk
980 981
981 982 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
982 983 that, swap fromline/toline and +/- signs while keep other things
983 984 unchanged.
984 985 """
985 986 m = {'+': '-', '-': '+', '\\': '\\'}
986 987 hunk = ['%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
987 988 return recordhunk(self.header, self.toline, self.fromline, self.proc,
988 989 self.before, hunk, self.after)
989 990
990 991 def write(self, fp):
991 992 delta = len(self.before) + len(self.after)
992 993 if self.after and self.after[-1] == '\\ No newline at end of file\n':
993 994 delta -= 1
994 995 fromlen = delta + self.removed
995 996 tolen = delta + self.added
996 997 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
997 998 (self.fromline, fromlen, self.toline, tolen,
998 999 self.proc and (' ' + self.proc)))
999 1000 fp.write(''.join(self.before + self.hunk + self.after))
1000 1001
1001 1002 pretty = write
1002 1003
1003 1004 def filename(self):
1004 1005 return self.header.filename()
1005 1006
1006 1007 def __repr__(self):
1007 1008 return '<hunk %r@%d>' % (self.filename(), self.fromline)
1008 1009
1009 1010 def getmessages():
1010 1011 return {
1011 1012 'multiple': {
1012 1013 'apply': _("apply change %d/%d to '%s'?"),
1013 1014 'discard': _("discard change %d/%d to '%s'?"),
1014 1015 'record': _("record change %d/%d to '%s'?"),
1015 1016 },
1016 1017 'single': {
1017 1018 'apply': _("apply this change to '%s'?"),
1018 1019 'discard': _("discard this change to '%s'?"),
1019 1020 'record': _("record this change to '%s'?"),
1020 1021 },
1021 1022 'help': {
1022 1023 'apply': _('[Ynesfdaq?]'
1023 1024 '$$ &Yes, apply this change'
1024 1025 '$$ &No, skip this change'
1025 1026 '$$ &Edit this change manually'
1026 1027 '$$ &Skip remaining changes to this file'
1027 1028 '$$ Apply remaining changes to this &file'
1028 1029 '$$ &Done, skip remaining changes and files'
1029 1030 '$$ Apply &all changes to all remaining files'
1030 1031 '$$ &Quit, applying no changes'
1031 1032 '$$ &? (display help)'),
1032 1033 'discard': _('[Ynesfdaq?]'
1033 1034 '$$ &Yes, discard this change'
1034 1035 '$$ &No, skip this change'
1035 1036 '$$ &Edit this change manually'
1036 1037 '$$ &Skip remaining changes to this file'
1037 1038 '$$ Discard remaining changes to this &file'
1038 1039 '$$ &Done, skip remaining changes and files'
1039 1040 '$$ Discard &all changes to all remaining files'
1040 1041 '$$ &Quit, discarding no changes'
1041 1042 '$$ &? (display help)'),
1042 1043 'record': _('[Ynesfdaq?]'
1043 1044 '$$ &Yes, record this change'
1044 1045 '$$ &No, skip this change'
1045 1046 '$$ &Edit this change manually'
1046 1047 '$$ &Skip remaining changes to this file'
1047 1048 '$$ Record remaining changes to this &file'
1048 1049 '$$ &Done, skip remaining changes and files'
1049 1050 '$$ Record &all changes to all remaining files'
1050 1051 '$$ &Quit, recording no changes'
1051 1052 '$$ &? (display help)'),
1052 1053 }
1053 1054 }
1054 1055
1055 1056 def filterpatch(ui, headers, operation=None):
1056 1057 """Interactively filter patch chunks into applied-only chunks"""
1057 1058 messages = getmessages()
1058 1059
1059 1060 if operation is None:
1060 1061 operation = 'record'
1061 1062
1062 1063 def prompt(skipfile, skipall, query, chunk):
1063 1064 """prompt query, and process base inputs
1064 1065
1065 1066 - y/n for the rest of file
1066 1067 - y/n for the rest
1067 1068 - ? (help)
1068 1069 - q (quit)
1069 1070
1070 1071 Return True/False and possibly updated skipfile and skipall.
1071 1072 """
1072 1073 newpatches = None
1073 1074 if skipall is not None:
1074 1075 return skipall, skipfile, skipall, newpatches
1075 1076 if skipfile is not None:
1076 1077 return skipfile, skipfile, skipall, newpatches
1077 1078 while True:
1078 1079 resps = messages['help'][operation]
1079 1080 r = ui.promptchoice("%s %s" % (query, resps))
1080 1081 ui.write("\n")
1081 1082 if r == 8: # ?
1082 1083 for c, t in ui.extractchoices(resps)[1]:
1083 1084 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1084 1085 continue
1085 1086 elif r == 0: # yes
1086 1087 ret = True
1087 1088 elif r == 1: # no
1088 1089 ret = False
1089 1090 elif r == 2: # Edit patch
1090 1091 if chunk is None:
1091 1092 ui.write(_('cannot edit patch for whole file'))
1092 1093 ui.write("\n")
1093 1094 continue
1094 1095 if chunk.header.binary():
1095 1096 ui.write(_('cannot edit patch for binary file'))
1096 1097 ui.write("\n")
1097 1098 continue
1098 1099 # Patch comment based on the Git one (based on comment at end of
1099 1100 # https://mercurial-scm.org/wiki/RecordExtension)
1100 1101 phelp = '---' + _("""
1101 1102 To remove '-' lines, make them ' ' lines (context).
1102 1103 To remove '+' lines, delete them.
1103 1104 Lines starting with # will be removed from the patch.
1104 1105
1105 1106 If the patch applies cleanly, the edited hunk will immediately be
1106 1107 added to the record list. If it does not apply cleanly, a rejects
1107 1108 file will be generated: you can use that when you try again. If
1108 1109 all lines of the hunk are removed, then the edit is aborted and
1109 1110 the hunk is left unchanged.
1110 1111 """)
1111 1112 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1112 1113 suffix=".diff")
1113 1114 ncpatchfp = None
1114 1115 try:
1115 1116 # Write the initial patch
1116 1117 f = util.nativeeolwriter(os.fdopen(patchfd, r'wb'))
1117 1118 chunk.header.write(f)
1118 1119 chunk.write(f)
1119 1120 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1120 1121 f.close()
1121 1122 # Start the editor and wait for it to complete
1122 1123 editor = ui.geteditor()
1123 1124 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1124 1125 environ={'HGUSER': ui.username()},
1125 1126 blockedtag='filterpatch')
1126 1127 if ret != 0:
1127 1128 ui.warn(_("editor exited with exit code %d\n") % ret)
1128 1129 continue
1129 1130 # Remove comment lines
1130 1131 patchfp = open(patchfn, r'rb')
1131 1132 ncpatchfp = stringio()
1132 1133 for line in util.iterfile(patchfp):
1133 1134 line = util.fromnativeeol(line)
1134 1135 if not line.startswith('#'):
1135 1136 ncpatchfp.write(line)
1136 1137 patchfp.close()
1137 1138 ncpatchfp.seek(0)
1138 1139 newpatches = parsepatch(ncpatchfp)
1139 1140 finally:
1140 1141 os.unlink(patchfn)
1141 1142 del ncpatchfp
1142 1143 # Signal that the chunk shouldn't be applied as-is, but
1143 1144 # provide the new patch to be used instead.
1144 1145 ret = False
1145 1146 elif r == 3: # Skip
1146 1147 ret = skipfile = False
1147 1148 elif r == 4: # file (Record remaining)
1148 1149 ret = skipfile = True
1149 1150 elif r == 5: # done, skip remaining
1150 1151 ret = skipall = False
1151 1152 elif r == 6: # all
1152 1153 ret = skipall = True
1153 1154 elif r == 7: # quit
1154 1155 raise error.Abort(_('user quit'))
1155 1156 return ret, skipfile, skipall, newpatches
1156 1157
1157 1158 seen = set()
1158 1159 applied = {} # 'filename' -> [] of chunks
1159 1160 skipfile, skipall = None, None
1160 1161 pos, total = 1, sum(len(h.hunks) for h in headers)
1161 1162 for h in headers:
1162 1163 pos += len(h.hunks)
1163 1164 skipfile = None
1164 1165 fixoffset = 0
1165 1166 hdr = ''.join(h.header)
1166 1167 if hdr in seen:
1167 1168 continue
1168 1169 seen.add(hdr)
1169 1170 if skipall is None:
1170 1171 h.pretty(ui)
1171 1172 msg = (_('examine changes to %s?') %
1172 1173 _(' and ').join("'%s'" % f for f in h.files()))
1173 1174 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1174 1175 if not r:
1175 1176 continue
1176 1177 applied[h.filename()] = [h]
1177 1178 if h.allhunks():
1178 1179 applied[h.filename()] += h.hunks
1179 1180 continue
1180 1181 for i, chunk in enumerate(h.hunks):
1181 1182 if skipfile is None and skipall is None:
1182 1183 chunk.pretty(ui)
1183 1184 if total == 1:
1184 1185 msg = messages['single'][operation] % chunk.filename()
1185 1186 else:
1186 1187 idx = pos - len(h.hunks) + i
1187 1188 msg = messages['multiple'][operation] % (idx, total,
1188 1189 chunk.filename())
1189 1190 r, skipfile, skipall, newpatches = prompt(skipfile,
1190 1191 skipall, msg, chunk)
1191 1192 if r:
1192 1193 if fixoffset:
1193 1194 chunk = copy.copy(chunk)
1194 1195 chunk.toline += fixoffset
1195 1196 applied[chunk.filename()].append(chunk)
1196 1197 elif newpatches is not None:
1197 1198 for newpatch in newpatches:
1198 1199 for newhunk in newpatch.hunks:
1199 1200 if fixoffset:
1200 1201 newhunk.toline += fixoffset
1201 1202 applied[newhunk.filename()].append(newhunk)
1202 1203 else:
1203 1204 fixoffset += chunk.removed - chunk.added
1204 1205 return (sum([h for h in applied.itervalues()
1205 1206 if h[0].special() or len(h) > 1], []), {})
1206 1207 class hunk(object):
1207 1208 def __init__(self, desc, num, lr, context):
1208 1209 self.number = num
1209 1210 self.desc = desc
1210 1211 self.hunk = [desc]
1211 1212 self.a = []
1212 1213 self.b = []
1213 1214 self.starta = self.lena = None
1214 1215 self.startb = self.lenb = None
1215 1216 if lr is not None:
1216 1217 if context:
1217 1218 self.read_context_hunk(lr)
1218 1219 else:
1219 1220 self.read_unified_hunk(lr)
1220 1221
1221 1222 def getnormalized(self):
1222 1223 """Return a copy with line endings normalized to LF."""
1223 1224
1224 1225 def normalize(lines):
1225 1226 nlines = []
1226 1227 for line in lines:
1227 1228 if line.endswith('\r\n'):
1228 1229 line = line[:-2] + '\n'
1229 1230 nlines.append(line)
1230 1231 return nlines
1231 1232
1232 1233 # Dummy object, it is rebuilt manually
1233 1234 nh = hunk(self.desc, self.number, None, None)
1234 1235 nh.number = self.number
1235 1236 nh.desc = self.desc
1236 1237 nh.hunk = self.hunk
1237 1238 nh.a = normalize(self.a)
1238 1239 nh.b = normalize(self.b)
1239 1240 nh.starta = self.starta
1240 1241 nh.startb = self.startb
1241 1242 nh.lena = self.lena
1242 1243 nh.lenb = self.lenb
1243 1244 return nh
1244 1245
1245 1246 def read_unified_hunk(self, lr):
1246 1247 m = unidesc.match(self.desc)
1247 1248 if not m:
1248 1249 raise PatchError(_("bad hunk #%d") % self.number)
1249 1250 self.starta, self.lena, self.startb, self.lenb = m.groups()
1250 1251 if self.lena is None:
1251 1252 self.lena = 1
1252 1253 else:
1253 1254 self.lena = int(self.lena)
1254 1255 if self.lenb is None:
1255 1256 self.lenb = 1
1256 1257 else:
1257 1258 self.lenb = int(self.lenb)
1258 1259 self.starta = int(self.starta)
1259 1260 self.startb = int(self.startb)
1260 1261 try:
1261 1262 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb,
1262 1263 self.a, self.b)
1263 1264 except error.ParseError as e:
1264 1265 raise PatchError(_("bad hunk #%d: %s") % (self.number, e))
1265 1266 # if we hit eof before finishing out the hunk, the last line will
1266 1267 # be zero length. Lets try to fix it up.
1267 1268 while len(self.hunk[-1]) == 0:
1268 1269 del self.hunk[-1]
1269 1270 del self.a[-1]
1270 1271 del self.b[-1]
1271 1272 self.lena -= 1
1272 1273 self.lenb -= 1
1273 1274 self._fixnewline(lr)
1274 1275
1275 1276 def read_context_hunk(self, lr):
1276 1277 self.desc = lr.readline()
1277 1278 m = contextdesc.match(self.desc)
1278 1279 if not m:
1279 1280 raise PatchError(_("bad hunk #%d") % self.number)
1280 1281 self.starta, aend = m.groups()
1281 1282 self.starta = int(self.starta)
1282 1283 if aend is None:
1283 1284 aend = self.starta
1284 1285 self.lena = int(aend) - self.starta
1285 1286 if self.starta:
1286 1287 self.lena += 1
1287 1288 for x in xrange(self.lena):
1288 1289 l = lr.readline()
1289 1290 if l.startswith('---'):
1290 1291 # lines addition, old block is empty
1291 1292 lr.push(l)
1292 1293 break
1293 1294 s = l[2:]
1294 1295 if l.startswith('- ') or l.startswith('! '):
1295 1296 u = '-' + s
1296 1297 elif l.startswith(' '):
1297 1298 u = ' ' + s
1298 1299 else:
1299 1300 raise PatchError(_("bad hunk #%d old text line %d") %
1300 1301 (self.number, x))
1301 1302 self.a.append(u)
1302 1303 self.hunk.append(u)
1303 1304
1304 1305 l = lr.readline()
1305 1306 if l.startswith('\ '):
1306 1307 s = self.a[-1][:-1]
1307 1308 self.a[-1] = s
1308 1309 self.hunk[-1] = s
1309 1310 l = lr.readline()
1310 1311 m = contextdesc.match(l)
1311 1312 if not m:
1312 1313 raise PatchError(_("bad hunk #%d") % self.number)
1313 1314 self.startb, bend = m.groups()
1314 1315 self.startb = int(self.startb)
1315 1316 if bend is None:
1316 1317 bend = self.startb
1317 1318 self.lenb = int(bend) - self.startb
1318 1319 if self.startb:
1319 1320 self.lenb += 1
1320 1321 hunki = 1
1321 1322 for x in xrange(self.lenb):
1322 1323 l = lr.readline()
1323 1324 if l.startswith('\ '):
1324 1325 # XXX: the only way to hit this is with an invalid line range.
1325 1326 # The no-eol marker is not counted in the line range, but I
1326 1327 # guess there are diff(1) out there which behave differently.
1327 1328 s = self.b[-1][:-1]
1328 1329 self.b[-1] = s
1329 1330 self.hunk[hunki - 1] = s
1330 1331 continue
1331 1332 if not l:
1332 1333 # line deletions, new block is empty and we hit EOF
1333 1334 lr.push(l)
1334 1335 break
1335 1336 s = l[2:]
1336 1337 if l.startswith('+ ') or l.startswith('! '):
1337 1338 u = '+' + s
1338 1339 elif l.startswith(' '):
1339 1340 u = ' ' + s
1340 1341 elif len(self.b) == 0:
1341 1342 # line deletions, new block is empty
1342 1343 lr.push(l)
1343 1344 break
1344 1345 else:
1345 1346 raise PatchError(_("bad hunk #%d old text line %d") %
1346 1347 (self.number, x))
1347 1348 self.b.append(s)
1348 1349 while True:
1349 1350 if hunki >= len(self.hunk):
1350 1351 h = ""
1351 1352 else:
1352 1353 h = self.hunk[hunki]
1353 1354 hunki += 1
1354 1355 if h == u:
1355 1356 break
1356 1357 elif h.startswith('-'):
1357 1358 continue
1358 1359 else:
1359 1360 self.hunk.insert(hunki - 1, u)
1360 1361 break
1361 1362
1362 1363 if not self.a:
1363 1364 # this happens when lines were only added to the hunk
1364 1365 for x in self.hunk:
1365 1366 if x.startswith('-') or x.startswith(' '):
1366 1367 self.a.append(x)
1367 1368 if not self.b:
1368 1369 # this happens when lines were only deleted from the hunk
1369 1370 for x in self.hunk:
1370 1371 if x.startswith('+') or x.startswith(' '):
1371 1372 self.b.append(x[1:])
1372 1373 # @@ -start,len +start,len @@
1373 1374 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1374 1375 self.startb, self.lenb)
1375 1376 self.hunk[0] = self.desc
1376 1377 self._fixnewline(lr)
1377 1378
1378 1379 def _fixnewline(self, lr):
1379 1380 l = lr.readline()
1380 1381 if l.startswith('\ '):
1381 1382 diffhelpers.fixnewline(self.hunk, self.a, self.b)
1382 1383 else:
1383 1384 lr.push(l)
1384 1385
1385 1386 def complete(self):
1386 1387 return len(self.a) == self.lena and len(self.b) == self.lenb
1387 1388
1388 1389 def _fuzzit(self, old, new, fuzz, toponly):
1389 1390 # this removes context lines from the top and bottom of list 'l'. It
1390 1391 # checks the hunk to make sure only context lines are removed, and then
1391 1392 # returns a new shortened list of lines.
1392 1393 fuzz = min(fuzz, len(old))
1393 1394 if fuzz:
1394 1395 top = 0
1395 1396 bot = 0
1396 1397 hlen = len(self.hunk)
1397 1398 for x in xrange(hlen - 1):
1398 1399 # the hunk starts with the @@ line, so use x+1
1399 1400 if self.hunk[x + 1].startswith(' '):
1400 1401 top += 1
1401 1402 else:
1402 1403 break
1403 1404 if not toponly:
1404 1405 for x in xrange(hlen - 1):
1405 1406 if self.hunk[hlen - bot - 1].startswith(' '):
1406 1407 bot += 1
1407 1408 else:
1408 1409 break
1409 1410
1410 1411 bot = min(fuzz, bot)
1411 1412 top = min(fuzz, top)
1412 1413 return old[top:len(old) - bot], new[top:len(new) - bot], top
1413 1414 return old, new, 0
1414 1415
1415 1416 def fuzzit(self, fuzz, toponly):
1416 1417 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1417 1418 oldstart = self.starta + top
1418 1419 newstart = self.startb + top
1419 1420 # zero length hunk ranges already have their start decremented
1420 1421 if self.lena and oldstart > 0:
1421 1422 oldstart -= 1
1422 1423 if self.lenb and newstart > 0:
1423 1424 newstart -= 1
1424 1425 return old, oldstart, new, newstart
1425 1426
1426 1427 class binhunk(object):
1427 1428 'A binary patch file.'
1428 1429 def __init__(self, lr, fname):
1429 1430 self.text = None
1430 1431 self.delta = False
1431 1432 self.hunk = ['GIT binary patch\n']
1432 1433 self._fname = fname
1433 1434 self._read(lr)
1434 1435
1435 1436 def complete(self):
1436 1437 return self.text is not None
1437 1438
1438 1439 def new(self, lines):
1439 1440 if self.delta:
1440 1441 return [applybindelta(self.text, ''.join(lines))]
1441 1442 return [self.text]
1442 1443
1443 1444 def _read(self, lr):
1444 1445 def getline(lr, hunk):
1445 1446 l = lr.readline()
1446 1447 hunk.append(l)
1447 1448 return l.rstrip('\r\n')
1448 1449
1449 1450 size = 0
1450 1451 while True:
1451 1452 line = getline(lr, self.hunk)
1452 1453 if not line:
1453 1454 raise PatchError(_('could not extract "%s" binary data')
1454 1455 % self._fname)
1455 1456 if line.startswith('literal '):
1456 1457 size = int(line[8:].rstrip())
1457 1458 break
1458 1459 if line.startswith('delta '):
1459 1460 size = int(line[6:].rstrip())
1460 1461 self.delta = True
1461 1462 break
1462 1463 dec = []
1463 1464 line = getline(lr, self.hunk)
1464 1465 while len(line) > 1:
1465 1466 l = line[0:1]
1466 1467 if l <= 'Z' and l >= 'A':
1467 1468 l = ord(l) - ord('A') + 1
1468 1469 else:
1469 1470 l = ord(l) - ord('a') + 27
1470 1471 try:
1471 1472 dec.append(util.b85decode(line[1:])[:l])
1472 1473 except ValueError as e:
1473 1474 raise PatchError(_('could not decode "%s" binary patch: %s')
1474 1475 % (self._fname, stringutil.forcebytestr(e)))
1475 1476 line = getline(lr, self.hunk)
1476 1477 text = zlib.decompress(''.join(dec))
1477 1478 if len(text) != size:
1478 1479 raise PatchError(_('"%s" length is %d bytes, should be %d')
1479 1480 % (self._fname, len(text), size))
1480 1481 self.text = text
1481 1482
1482 1483 def parsefilename(str):
1483 1484 # --- filename \t|space stuff
1484 1485 s = str[4:].rstrip('\r\n')
1485 1486 i = s.find('\t')
1486 1487 if i < 0:
1487 1488 i = s.find(' ')
1488 1489 if i < 0:
1489 1490 return s
1490 1491 return s[:i]
1491 1492
1492 1493 def reversehunks(hunks):
1493 1494 '''reverse the signs in the hunks given as argument
1494 1495
1495 1496 This function operates on hunks coming out of patch.filterpatch, that is
1496 1497 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1497 1498
1498 1499 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1499 1500 ... --- a/folder1/g
1500 1501 ... +++ b/folder1/g
1501 1502 ... @@ -1,7 +1,7 @@
1502 1503 ... +firstline
1503 1504 ... c
1504 1505 ... 1
1505 1506 ... 2
1506 1507 ... + 3
1507 1508 ... -4
1508 1509 ... 5
1509 1510 ... d
1510 1511 ... +lastline"""
1511 1512 >>> hunks = parsepatch([rawpatch])
1512 1513 >>> hunkscomingfromfilterpatch = []
1513 1514 >>> for h in hunks:
1514 1515 ... hunkscomingfromfilterpatch.append(h)
1515 1516 ... hunkscomingfromfilterpatch.extend(h.hunks)
1516 1517
1517 1518 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1518 1519 >>> from . import util
1519 1520 >>> fp = util.stringio()
1520 1521 >>> for c in reversedhunks:
1521 1522 ... c.write(fp)
1522 1523 >>> fp.seek(0) or None
1523 1524 >>> reversedpatch = fp.read()
1524 1525 >>> print(pycompat.sysstr(reversedpatch))
1525 1526 diff --git a/folder1/g b/folder1/g
1526 1527 --- a/folder1/g
1527 1528 +++ b/folder1/g
1528 1529 @@ -1,4 +1,3 @@
1529 1530 -firstline
1530 1531 c
1531 1532 1
1532 1533 2
1533 1534 @@ -2,6 +1,6 @@
1534 1535 c
1535 1536 1
1536 1537 2
1537 1538 - 3
1538 1539 +4
1539 1540 5
1540 1541 d
1541 1542 @@ -6,3 +5,2 @@
1542 1543 5
1543 1544 d
1544 1545 -lastline
1545 1546
1546 1547 '''
1547 1548
1548 1549 newhunks = []
1549 1550 for c in hunks:
1550 1551 if util.safehasattr(c, 'reversehunk'):
1551 1552 c = c.reversehunk()
1552 1553 newhunks.append(c)
1553 1554 return newhunks
1554 1555
1555 1556 def parsepatch(originalchunks, maxcontext=None):
1556 1557 """patch -> [] of headers -> [] of hunks
1557 1558
1558 1559 If maxcontext is not None, trim context lines if necessary.
1559 1560
1560 1561 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1561 1562 ... --- a/folder1/g
1562 1563 ... +++ b/folder1/g
1563 1564 ... @@ -1,8 +1,10 @@
1564 1565 ... 1
1565 1566 ... 2
1566 1567 ... -3
1567 1568 ... 4
1568 1569 ... 5
1569 1570 ... 6
1570 1571 ... +6.1
1571 1572 ... +6.2
1572 1573 ... 7
1573 1574 ... 8
1574 1575 ... +9'''
1575 1576 >>> out = util.stringio()
1576 1577 >>> headers = parsepatch([rawpatch], maxcontext=1)
1577 1578 >>> for header in headers:
1578 1579 ... header.write(out)
1579 1580 ... for hunk in header.hunks:
1580 1581 ... hunk.write(out)
1581 1582 >>> print(pycompat.sysstr(out.getvalue()))
1582 1583 diff --git a/folder1/g b/folder1/g
1583 1584 --- a/folder1/g
1584 1585 +++ b/folder1/g
1585 1586 @@ -2,3 +2,2 @@
1586 1587 2
1587 1588 -3
1588 1589 4
1589 1590 @@ -6,2 +5,4 @@
1590 1591 6
1591 1592 +6.1
1592 1593 +6.2
1593 1594 7
1594 1595 @@ -8,1 +9,2 @@
1595 1596 8
1596 1597 +9
1597 1598 """
1598 1599 class parser(object):
1599 1600 """patch parsing state machine"""
1600 1601 def __init__(self):
1601 1602 self.fromline = 0
1602 1603 self.toline = 0
1603 1604 self.proc = ''
1604 1605 self.header = None
1605 1606 self.context = []
1606 1607 self.before = []
1607 1608 self.hunk = []
1608 1609 self.headers = []
1609 1610
1610 1611 def addrange(self, limits):
1611 1612 fromstart, fromend, tostart, toend, proc = limits
1612 1613 self.fromline = int(fromstart)
1613 1614 self.toline = int(tostart)
1614 1615 self.proc = proc
1615 1616
1616 1617 def addcontext(self, context):
1617 1618 if self.hunk:
1618 1619 h = recordhunk(self.header, self.fromline, self.toline,
1619 1620 self.proc, self.before, self.hunk, context, maxcontext)
1620 1621 self.header.hunks.append(h)
1621 1622 self.fromline += len(self.before) + h.removed
1622 1623 self.toline += len(self.before) + h.added
1623 1624 self.before = []
1624 1625 self.hunk = []
1625 1626 self.context = context
1626 1627
1627 1628 def addhunk(self, hunk):
1628 1629 if self.context:
1629 1630 self.before = self.context
1630 1631 self.context = []
1631 1632 self.hunk = hunk
1632 1633
1633 1634 def newfile(self, hdr):
1634 1635 self.addcontext([])
1635 1636 h = header(hdr)
1636 1637 self.headers.append(h)
1637 1638 self.header = h
1638 1639
1639 1640 def addother(self, line):
1640 1641 pass # 'other' lines are ignored
1641 1642
1642 1643 def finished(self):
1643 1644 self.addcontext([])
1644 1645 return self.headers
1645 1646
1646 1647 transitions = {
1647 1648 'file': {'context': addcontext,
1648 1649 'file': newfile,
1649 1650 'hunk': addhunk,
1650 1651 'range': addrange},
1651 1652 'context': {'file': newfile,
1652 1653 'hunk': addhunk,
1653 1654 'range': addrange,
1654 1655 'other': addother},
1655 1656 'hunk': {'context': addcontext,
1656 1657 'file': newfile,
1657 1658 'range': addrange},
1658 1659 'range': {'context': addcontext,
1659 1660 'hunk': addhunk},
1660 1661 'other': {'other': addother},
1661 1662 }
1662 1663
1663 1664 p = parser()
1664 1665 fp = stringio()
1665 1666 fp.write(''.join(originalchunks))
1666 1667 fp.seek(0)
1667 1668
1668 1669 state = 'context'
1669 1670 for newstate, data in scanpatch(fp):
1670 1671 try:
1671 1672 p.transitions[state][newstate](p, data)
1672 1673 except KeyError:
1673 1674 raise PatchError('unhandled transition: %s -> %s' %
1674 1675 (state, newstate))
1675 1676 state = newstate
1676 1677 del fp
1677 1678 return p.finished()
1678 1679
1679 1680 def pathtransform(path, strip, prefix):
1680 1681 '''turn a path from a patch into a path suitable for the repository
1681 1682
1682 1683 prefix, if not empty, is expected to be normalized with a / at the end.
1683 1684
1684 1685 Returns (stripped components, path in repository).
1685 1686
1686 1687 >>> pathtransform(b'a/b/c', 0, b'')
1687 1688 ('', 'a/b/c')
1688 1689 >>> pathtransform(b' a/b/c ', 0, b'')
1689 1690 ('', ' a/b/c')
1690 1691 >>> pathtransform(b' a/b/c ', 2, b'')
1691 1692 ('a/b/', 'c')
1692 1693 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1693 1694 ('', 'd/e/a/b/c')
1694 1695 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1695 1696 ('a//b/', 'd/e/c')
1696 1697 >>> pathtransform(b'a/b/c', 3, b'')
1697 1698 Traceback (most recent call last):
1698 1699 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1699 1700 '''
1700 1701 pathlen = len(path)
1701 1702 i = 0
1702 1703 if strip == 0:
1703 1704 return '', prefix + path.rstrip()
1704 1705 count = strip
1705 1706 while count > 0:
1706 1707 i = path.find('/', i)
1707 1708 if i == -1:
1708 1709 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1709 1710 (count, strip, path))
1710 1711 i += 1
1711 1712 # consume '//' in the path
1712 1713 while i < pathlen - 1 and path[i:i + 1] == '/':
1713 1714 i += 1
1714 1715 count -= 1
1715 1716 return path[:i].lstrip(), prefix + path[i:].rstrip()
1716 1717
1717 1718 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1718 1719 nulla = afile_orig == "/dev/null"
1719 1720 nullb = bfile_orig == "/dev/null"
1720 1721 create = nulla and hunk.starta == 0 and hunk.lena == 0
1721 1722 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1722 1723 abase, afile = pathtransform(afile_orig, strip, prefix)
1723 1724 gooda = not nulla and backend.exists(afile)
1724 1725 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1725 1726 if afile == bfile:
1726 1727 goodb = gooda
1727 1728 else:
1728 1729 goodb = not nullb and backend.exists(bfile)
1729 1730 missing = not goodb and not gooda and not create
1730 1731
1731 1732 # some diff programs apparently produce patches where the afile is
1732 1733 # not /dev/null, but afile starts with bfile
1733 1734 abasedir = afile[:afile.rfind('/') + 1]
1734 1735 bbasedir = bfile[:bfile.rfind('/') + 1]
1735 1736 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1736 1737 and hunk.starta == 0 and hunk.lena == 0):
1737 1738 create = True
1738 1739 missing = False
1739 1740
1740 1741 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1741 1742 # diff is between a file and its backup. In this case, the original
1742 1743 # file should be patched (see original mpatch code).
1743 1744 isbackup = (abase == bbase and bfile.startswith(afile))
1744 1745 fname = None
1745 1746 if not missing:
1746 1747 if gooda and goodb:
1747 1748 if isbackup:
1748 1749 fname = afile
1749 1750 else:
1750 1751 fname = bfile
1751 1752 elif gooda:
1752 1753 fname = afile
1753 1754
1754 1755 if not fname:
1755 1756 if not nullb:
1756 1757 if isbackup:
1757 1758 fname = afile
1758 1759 else:
1759 1760 fname = bfile
1760 1761 elif not nulla:
1761 1762 fname = afile
1762 1763 else:
1763 1764 raise PatchError(_("undefined source and destination files"))
1764 1765
1765 1766 gp = patchmeta(fname)
1766 1767 if create:
1767 1768 gp.op = 'ADD'
1768 1769 elif remove:
1769 1770 gp.op = 'DELETE'
1770 1771 return gp
1771 1772
1772 1773 def scanpatch(fp):
1773 1774 """like patch.iterhunks, but yield different events
1774 1775
1775 1776 - ('file', [header_lines + fromfile + tofile])
1776 1777 - ('context', [context_lines])
1777 1778 - ('hunk', [hunk_lines])
1778 1779 - ('range', (-start,len, +start,len, proc))
1779 1780 """
1780 1781 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1781 1782 lr = linereader(fp)
1782 1783
1783 1784 def scanwhile(first, p):
1784 1785 """scan lr while predicate holds"""
1785 1786 lines = [first]
1786 1787 for line in iter(lr.readline, ''):
1787 1788 if p(line):
1788 1789 lines.append(line)
1789 1790 else:
1790 1791 lr.push(line)
1791 1792 break
1792 1793 return lines
1793 1794
1794 1795 for line in iter(lr.readline, ''):
1795 1796 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1796 1797 def notheader(line):
1797 1798 s = line.split(None, 1)
1798 1799 return not s or s[0] not in ('---', 'diff')
1799 1800 header = scanwhile(line, notheader)
1800 1801 fromfile = lr.readline()
1801 1802 if fromfile.startswith('---'):
1802 1803 tofile = lr.readline()
1803 1804 header += [fromfile, tofile]
1804 1805 else:
1805 1806 lr.push(fromfile)
1806 1807 yield 'file', header
1807 1808 elif line.startswith(' '):
1808 1809 cs = (' ', '\\')
1809 1810 yield 'context', scanwhile(line, lambda l: l.startswith(cs))
1810 1811 elif line.startswith(('-', '+')):
1811 1812 cs = ('-', '+', '\\')
1812 1813 yield 'hunk', scanwhile(line, lambda l: l.startswith(cs))
1813 1814 else:
1814 1815 m = lines_re.match(line)
1815 1816 if m:
1816 1817 yield 'range', m.groups()
1817 1818 else:
1818 1819 yield 'other', line
1819 1820
1820 1821 def scangitpatch(lr, firstline):
1821 1822 """
1822 1823 Git patches can emit:
1823 1824 - rename a to b
1824 1825 - change b
1825 1826 - copy a to c
1826 1827 - change c
1827 1828
1828 1829 We cannot apply this sequence as-is, the renamed 'a' could not be
1829 1830 found for it would have been renamed already. And we cannot copy
1830 1831 from 'b' instead because 'b' would have been changed already. So
1831 1832 we scan the git patch for copy and rename commands so we can
1832 1833 perform the copies ahead of time.
1833 1834 """
1834 1835 pos = 0
1835 1836 try:
1836 1837 pos = lr.fp.tell()
1837 1838 fp = lr.fp
1838 1839 except IOError:
1839 1840 fp = stringio(lr.fp.read())
1840 1841 gitlr = linereader(fp)
1841 1842 gitlr.push(firstline)
1842 1843 gitpatches = readgitpatch(gitlr)
1843 1844 fp.seek(pos)
1844 1845 return gitpatches
1845 1846
1846 1847 def iterhunks(fp):
1847 1848 """Read a patch and yield the following events:
1848 1849 - ("file", afile, bfile, firsthunk): select a new target file.
1849 1850 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1850 1851 "file" event.
1851 1852 - ("git", gitchanges): current diff is in git format, gitchanges
1852 1853 maps filenames to gitpatch records. Unique event.
1853 1854 """
1854 1855 afile = ""
1855 1856 bfile = ""
1856 1857 state = None
1857 1858 hunknum = 0
1858 1859 emitfile = newfile = False
1859 1860 gitpatches = None
1860 1861
1861 1862 # our states
1862 1863 BFILE = 1
1863 1864 context = None
1864 1865 lr = linereader(fp)
1865 1866
1866 1867 for x in iter(lr.readline, ''):
1867 1868 if state == BFILE and (
1868 1869 (not context and x.startswith('@'))
1869 1870 or (context is not False and x.startswith('***************'))
1870 1871 or x.startswith('GIT binary patch')):
1871 1872 gp = None
1872 1873 if (gitpatches and
1873 1874 gitpatches[-1].ispatching(afile, bfile)):
1874 1875 gp = gitpatches.pop()
1875 1876 if x.startswith('GIT binary patch'):
1876 1877 h = binhunk(lr, gp.path)
1877 1878 else:
1878 1879 if context is None and x.startswith('***************'):
1879 1880 context = True
1880 1881 h = hunk(x, hunknum + 1, lr, context)
1881 1882 hunknum += 1
1882 1883 if emitfile:
1883 1884 emitfile = False
1884 1885 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1885 1886 yield 'hunk', h
1886 1887 elif x.startswith('diff --git a/'):
1887 1888 m = gitre.match(x.rstrip(' \r\n'))
1888 1889 if not m:
1889 1890 continue
1890 1891 if gitpatches is None:
1891 1892 # scan whole input for git metadata
1892 1893 gitpatches = scangitpatch(lr, x)
1893 1894 yield 'git', [g.copy() for g in gitpatches
1894 1895 if g.op in ('COPY', 'RENAME')]
1895 1896 gitpatches.reverse()
1896 1897 afile = 'a/' + m.group(1)
1897 1898 bfile = 'b/' + m.group(2)
1898 1899 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1899 1900 gp = gitpatches.pop()
1900 1901 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1901 1902 if not gitpatches:
1902 1903 raise PatchError(_('failed to synchronize metadata for "%s"')
1903 1904 % afile[2:])
1904 1905 gp = gitpatches[-1]
1905 1906 newfile = True
1906 1907 elif x.startswith('---'):
1907 1908 # check for a unified diff
1908 1909 l2 = lr.readline()
1909 1910 if not l2.startswith('+++'):
1910 1911 lr.push(l2)
1911 1912 continue
1912 1913 newfile = True
1913 1914 context = False
1914 1915 afile = parsefilename(x)
1915 1916 bfile = parsefilename(l2)
1916 1917 elif x.startswith('***'):
1917 1918 # check for a context diff
1918 1919 l2 = lr.readline()
1919 1920 if not l2.startswith('---'):
1920 1921 lr.push(l2)
1921 1922 continue
1922 1923 l3 = lr.readline()
1923 1924 lr.push(l3)
1924 1925 if not l3.startswith("***************"):
1925 1926 lr.push(l2)
1926 1927 continue
1927 1928 newfile = True
1928 1929 context = True
1929 1930 afile = parsefilename(x)
1930 1931 bfile = parsefilename(l2)
1931 1932
1932 1933 if newfile:
1933 1934 newfile = False
1934 1935 emitfile = True
1935 1936 state = BFILE
1936 1937 hunknum = 0
1937 1938
1938 1939 while gitpatches:
1939 1940 gp = gitpatches.pop()
1940 1941 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1941 1942
1942 1943 def applybindelta(binchunk, data):
1943 1944 """Apply a binary delta hunk
1944 1945 The algorithm used is the algorithm from git's patch-delta.c
1945 1946 """
1946 1947 def deltahead(binchunk):
1947 1948 i = 0
1948 1949 for c in binchunk:
1949 1950 i += 1
1950 1951 if not (ord(c) & 0x80):
1951 1952 return i
1952 1953 return i
1953 1954 out = ""
1954 1955 s = deltahead(binchunk)
1955 1956 binchunk = binchunk[s:]
1956 1957 s = deltahead(binchunk)
1957 1958 binchunk = binchunk[s:]
1958 1959 i = 0
1959 1960 while i < len(binchunk):
1960 1961 cmd = ord(binchunk[i])
1961 1962 i += 1
1962 1963 if (cmd & 0x80):
1963 1964 offset = 0
1964 1965 size = 0
1965 1966 if (cmd & 0x01):
1966 1967 offset = ord(binchunk[i])
1967 1968 i += 1
1968 1969 if (cmd & 0x02):
1969 1970 offset |= ord(binchunk[i]) << 8
1970 1971 i += 1
1971 1972 if (cmd & 0x04):
1972 1973 offset |= ord(binchunk[i]) << 16
1973 1974 i += 1
1974 1975 if (cmd & 0x08):
1975 1976 offset |= ord(binchunk[i]) << 24
1976 1977 i += 1
1977 1978 if (cmd & 0x10):
1978 1979 size = ord(binchunk[i])
1979 1980 i += 1
1980 1981 if (cmd & 0x20):
1981 1982 size |= ord(binchunk[i]) << 8
1982 1983 i += 1
1983 1984 if (cmd & 0x40):
1984 1985 size |= ord(binchunk[i]) << 16
1985 1986 i += 1
1986 1987 if size == 0:
1987 1988 size = 0x10000
1988 1989 offset_end = offset + size
1989 1990 out += data[offset:offset_end]
1990 1991 elif cmd != 0:
1991 1992 offset_end = i + cmd
1992 1993 out += binchunk[i:offset_end]
1993 1994 i += cmd
1994 1995 else:
1995 1996 raise PatchError(_('unexpected delta opcode 0'))
1996 1997 return out
1997 1998
1998 1999 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1999 2000 """Reads a patch from fp and tries to apply it.
2000 2001
2001 2002 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
2002 2003 there was any fuzz.
2003 2004
2004 2005 If 'eolmode' is 'strict', the patch content and patched file are
2005 2006 read in binary mode. Otherwise, line endings are ignored when
2006 2007 patching then normalized according to 'eolmode'.
2007 2008 """
2008 2009 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
2009 2010 prefix=prefix, eolmode=eolmode)
2010 2011
2011 2012 def _canonprefix(repo, prefix):
2012 2013 if prefix:
2013 2014 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2014 2015 if prefix != '':
2015 2016 prefix += '/'
2016 2017 return prefix
2017 2018
2018 2019 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
2019 2020 eolmode='strict'):
2020 2021 prefix = _canonprefix(backend.repo, prefix)
2021 2022 def pstrip(p):
2022 2023 return pathtransform(p, strip - 1, prefix)[1]
2023 2024
2024 2025 rejects = 0
2025 2026 err = 0
2026 2027 current_file = None
2027 2028
2028 2029 for state, values in iterhunks(fp):
2029 2030 if state == 'hunk':
2030 2031 if not current_file:
2031 2032 continue
2032 2033 ret = current_file.apply(values)
2033 2034 if ret > 0:
2034 2035 err = 1
2035 2036 elif state == 'file':
2036 2037 if current_file:
2037 2038 rejects += current_file.close()
2038 2039 current_file = None
2039 2040 afile, bfile, first_hunk, gp = values
2040 2041 if gp:
2041 2042 gp.path = pstrip(gp.path)
2042 2043 if gp.oldpath:
2043 2044 gp.oldpath = pstrip(gp.oldpath)
2044 2045 else:
2045 2046 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2046 2047 prefix)
2047 2048 if gp.op == 'RENAME':
2048 2049 backend.unlink(gp.oldpath)
2049 2050 if not first_hunk:
2050 2051 if gp.op == 'DELETE':
2051 2052 backend.unlink(gp.path)
2052 2053 continue
2053 2054 data, mode = None, None
2054 2055 if gp.op in ('RENAME', 'COPY'):
2055 2056 data, mode = store.getfile(gp.oldpath)[:2]
2056 2057 if data is None:
2057 2058 # This means that the old path does not exist
2058 2059 raise PatchError(_("source file '%s' does not exist")
2059 2060 % gp.oldpath)
2060 2061 if gp.mode:
2061 2062 mode = gp.mode
2062 2063 if gp.op == 'ADD':
2063 2064 # Added files without content have no hunk and
2064 2065 # must be created
2065 2066 data = ''
2066 2067 if data or mode:
2067 2068 if (gp.op in ('ADD', 'RENAME', 'COPY')
2068 2069 and backend.exists(gp.path)):
2069 2070 raise PatchError(_("cannot create %s: destination "
2070 2071 "already exists") % gp.path)
2071 2072 backend.setfile(gp.path, data, mode, gp.oldpath)
2072 2073 continue
2073 2074 try:
2074 2075 current_file = patcher(ui, gp, backend, store,
2075 2076 eolmode=eolmode)
2076 2077 except PatchError as inst:
2077 2078 ui.warn(str(inst) + '\n')
2078 2079 current_file = None
2079 2080 rejects += 1
2080 2081 continue
2081 2082 elif state == 'git':
2082 2083 for gp in values:
2083 2084 path = pstrip(gp.oldpath)
2084 2085 data, mode = backend.getfile(path)
2085 2086 if data is None:
2086 2087 # The error ignored here will trigger a getfile()
2087 2088 # error in a place more appropriate for error
2088 2089 # handling, and will not interrupt the patching
2089 2090 # process.
2090 2091 pass
2091 2092 else:
2092 2093 store.setfile(path, data, mode)
2093 2094 else:
2094 2095 raise error.Abort(_('unsupported parser state: %s') % state)
2095 2096
2096 2097 if current_file:
2097 2098 rejects += current_file.close()
2098 2099
2099 2100 if rejects:
2100 2101 return -1
2101 2102 return err
2102 2103
2103 2104 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2104 2105 similarity):
2105 2106 """use <patcher> to apply <patchname> to the working directory.
2106 2107 returns whether patch was applied with fuzz factor."""
2107 2108
2108 2109 fuzz = False
2109 2110 args = []
2110 2111 cwd = repo.root
2111 2112 if cwd:
2112 2113 args.append('-d %s' % procutil.shellquote(cwd))
2113 2114 cmd = ('%s %s -p%d < %s'
2114 2115 % (patcher, ' '.join(args), strip, procutil.shellquote(patchname)))
2115 2116 fp = procutil.popen(cmd, 'rb')
2116 2117 try:
2117 2118 for line in util.iterfile(fp):
2118 2119 line = line.rstrip()
2119 2120 ui.note(line + '\n')
2120 2121 if line.startswith('patching file '):
2121 2122 pf = util.parsepatchoutput(line)
2122 2123 printed_file = False
2123 2124 files.add(pf)
2124 2125 elif line.find('with fuzz') >= 0:
2125 2126 fuzz = True
2126 2127 if not printed_file:
2127 2128 ui.warn(pf + '\n')
2128 2129 printed_file = True
2129 2130 ui.warn(line + '\n')
2130 2131 elif line.find('saving rejects to file') >= 0:
2131 2132 ui.warn(line + '\n')
2132 2133 elif line.find('FAILED') >= 0:
2133 2134 if not printed_file:
2134 2135 ui.warn(pf + '\n')
2135 2136 printed_file = True
2136 2137 ui.warn(line + '\n')
2137 2138 finally:
2138 2139 if files:
2139 2140 scmutil.marktouched(repo, files, similarity)
2140 2141 code = fp.close()
2141 2142 if code:
2142 2143 raise PatchError(_("patch command failed: %s") %
2143 2144 procutil.explainexit(code))
2144 2145 return fuzz
2145 2146
2146 2147 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2147 2148 eolmode='strict'):
2148 2149 if files is None:
2149 2150 files = set()
2150 2151 if eolmode is None:
2151 2152 eolmode = ui.config('patch', 'eol')
2152 2153 if eolmode.lower() not in eolmodes:
2153 2154 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2154 2155 eolmode = eolmode.lower()
2155 2156
2156 2157 store = filestore()
2157 2158 try:
2158 2159 fp = open(patchobj, 'rb')
2159 2160 except TypeError:
2160 2161 fp = patchobj
2161 2162 try:
2162 2163 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2163 2164 eolmode=eolmode)
2164 2165 finally:
2165 2166 if fp != patchobj:
2166 2167 fp.close()
2167 2168 files.update(backend.close())
2168 2169 store.close()
2169 2170 if ret < 0:
2170 2171 raise PatchError(_('patch failed to apply'))
2171 2172 return ret > 0
2172 2173
2173 2174 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2174 2175 eolmode='strict', similarity=0):
2175 2176 """use builtin patch to apply <patchobj> to the working directory.
2176 2177 returns whether patch was applied with fuzz factor."""
2177 2178 backend = workingbackend(ui, repo, similarity)
2178 2179 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2179 2180
2180 2181 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2181 2182 eolmode='strict'):
2182 2183 backend = repobackend(ui, repo, ctx, store)
2183 2184 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2184 2185
2185 2186 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2186 2187 similarity=0):
2187 2188 """Apply <patchname> to the working directory.
2188 2189
2189 2190 'eolmode' specifies how end of lines should be handled. It can be:
2190 2191 - 'strict': inputs are read in binary mode, EOLs are preserved
2191 2192 - 'crlf': EOLs are ignored when patching and reset to CRLF
2192 2193 - 'lf': EOLs are ignored when patching and reset to LF
2193 2194 - None: get it from user settings, default to 'strict'
2194 2195 'eolmode' is ignored when using an external patcher program.
2195 2196
2196 2197 Returns whether patch was applied with fuzz factor.
2197 2198 """
2198 2199 patcher = ui.config('ui', 'patch')
2199 2200 if files is None:
2200 2201 files = set()
2201 2202 if patcher:
2202 2203 return _externalpatch(ui, repo, patcher, patchname, strip,
2203 2204 files, similarity)
2204 2205 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2205 2206 similarity)
2206 2207
2207 2208 def changedfiles(ui, repo, patchpath, strip=1, prefix=''):
2208 2209 backend = fsbackend(ui, repo.root)
2209 2210 prefix = _canonprefix(repo, prefix)
2210 2211 with open(patchpath, 'rb') as fp:
2211 2212 changed = set()
2212 2213 for state, values in iterhunks(fp):
2213 2214 if state == 'file':
2214 2215 afile, bfile, first_hunk, gp = values
2215 2216 if gp:
2216 2217 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2217 2218 if gp.oldpath:
2218 2219 gp.oldpath = pathtransform(gp.oldpath, strip - 1,
2219 2220 prefix)[1]
2220 2221 else:
2221 2222 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2222 2223 prefix)
2223 2224 changed.add(gp.path)
2224 2225 if gp.op == 'RENAME':
2225 2226 changed.add(gp.oldpath)
2226 2227 elif state not in ('hunk', 'git'):
2227 2228 raise error.Abort(_('unsupported parser state: %s') % state)
2228 2229 return changed
2229 2230
2230 2231 class GitDiffRequired(Exception):
2231 2232 pass
2232 2233
2233 2234 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2234 2235 '''return diffopts with all features supported and parsed'''
2235 2236 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2236 2237 git=True, whitespace=True, formatchanging=True)
2237 2238
2238 2239 diffopts = diffallopts
2239 2240
2240 2241 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2241 2242 whitespace=False, formatchanging=False):
2242 2243 '''return diffopts with only opted-in features parsed
2243 2244
2244 2245 Features:
2245 2246 - git: git-style diffs
2246 2247 - whitespace: whitespace options like ignoreblanklines and ignorews
2247 2248 - formatchanging: options that will likely break or cause correctness issues
2248 2249 with most diff parsers
2249 2250 '''
2250 2251 def get(key, name=None, getter=ui.configbool, forceplain=None):
2251 2252 if opts:
2252 2253 v = opts.get(key)
2253 2254 # diffopts flags are either None-default (which is passed
2254 2255 # through unchanged, so we can identify unset values), or
2255 2256 # some other falsey default (eg --unified, which defaults
2256 2257 # to an empty string). We only want to override the config
2257 2258 # entries from hgrc with command line values if they
2258 2259 # appear to have been set, which is any truthy value,
2259 2260 # True, or False.
2260 2261 if v or isinstance(v, bool):
2261 2262 return v
2262 2263 if forceplain is not None and ui.plain():
2263 2264 return forceplain
2264 2265 return getter(section, name or key, untrusted=untrusted)
2265 2266
2266 2267 # core options, expected to be understood by every diff parser
2267 2268 buildopts = {
2268 2269 'nodates': get('nodates'),
2269 2270 'showfunc': get('show_function', 'showfunc'),
2270 2271 'context': get('unified', getter=ui.config),
2271 2272 }
2272 2273 buildopts['worddiff'] = ui.configbool('experimental', 'worddiff')
2273 2274 buildopts['xdiff'] = ui.configbool('experimental', 'xdiff')
2274 2275
2275 2276 if git:
2276 2277 buildopts['git'] = get('git')
2277 2278
2278 2279 # since this is in the experimental section, we need to call
2279 2280 # ui.configbool directory
2280 2281 buildopts['showsimilarity'] = ui.configbool('experimental',
2281 2282 'extendedheader.similarity')
2282 2283
2283 2284 # need to inspect the ui object instead of using get() since we want to
2284 2285 # test for an int
2285 2286 hconf = ui.config('experimental', 'extendedheader.index')
2286 2287 if hconf is not None:
2287 2288 hlen = None
2288 2289 try:
2289 2290 # the hash config could be an integer (for length of hash) or a
2290 2291 # word (e.g. short, full, none)
2291 2292 hlen = int(hconf)
2292 2293 if hlen < 0 or hlen > 40:
2293 2294 msg = _("invalid length for extendedheader.index: '%d'\n")
2294 2295 ui.warn(msg % hlen)
2295 2296 except ValueError:
2296 2297 # default value
2297 2298 if hconf == 'short' or hconf == '':
2298 2299 hlen = 12
2299 2300 elif hconf == 'full':
2300 2301 hlen = 40
2301 2302 elif hconf != 'none':
2302 2303 msg = _("invalid value for extendedheader.index: '%s'\n")
2303 2304 ui.warn(msg % hconf)
2304 2305 finally:
2305 2306 buildopts['index'] = hlen
2306 2307
2307 2308 if whitespace:
2308 2309 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2309 2310 buildopts['ignorewsamount'] = get('ignore_space_change',
2310 2311 'ignorewsamount')
2311 2312 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2312 2313 'ignoreblanklines')
2313 2314 buildopts['ignorewseol'] = get('ignore_space_at_eol', 'ignorewseol')
2314 2315 if formatchanging:
2315 2316 buildopts['text'] = opts and opts.get('text')
2316 2317 binary = None if opts is None else opts.get('binary')
2317 2318 buildopts['nobinary'] = (not binary if binary is not None
2318 2319 else get('nobinary', forceplain=False))
2319 2320 buildopts['noprefix'] = get('noprefix', forceplain=False)
2320 2321
2321 2322 return mdiff.diffopts(**pycompat.strkwargs(buildopts))
2322 2323
2323 2324 def diff(repo, node1=None, node2=None, match=None, changes=None,
2324 2325 opts=None, losedatafn=None, prefix='', relroot='', copy=None,
2325 2326 hunksfilterfn=None):
2326 2327 '''yields diff of changes to files between two nodes, or node and
2327 2328 working directory.
2328 2329
2329 2330 if node1 is None, use first dirstate parent instead.
2330 2331 if node2 is None, compare node1 with working directory.
2331 2332
2332 2333 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2333 2334 every time some change cannot be represented with the current
2334 2335 patch format. Return False to upgrade to git patch format, True to
2335 2336 accept the loss or raise an exception to abort the diff. It is
2336 2337 called with the name of current file being diffed as 'fn'. If set
2337 2338 to None, patches will always be upgraded to git format when
2338 2339 necessary.
2339 2340
2340 2341 prefix is a filename prefix that is prepended to all filenames on
2341 2342 display (used for subrepos).
2342 2343
2343 2344 relroot, if not empty, must be normalized with a trailing /. Any match
2344 2345 patterns that fall outside it will be ignored.
2345 2346
2346 2347 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2347 2348 information.
2348 2349
2349 2350 hunksfilterfn, if not None, should be a function taking a filectx and
2350 2351 hunks generator that may yield filtered hunks.
2351 2352 '''
2352 2353 for fctx1, fctx2, hdr, hunks in diffhunks(
2353 2354 repo, node1=node1, node2=node2,
2354 2355 match=match, changes=changes, opts=opts,
2355 2356 losedatafn=losedatafn, prefix=prefix, relroot=relroot, copy=copy,
2356 2357 ):
2357 2358 if hunksfilterfn is not None:
2358 2359 # If the file has been removed, fctx2 is None; but this should
2359 2360 # not occur here since we catch removed files early in
2360 2361 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2361 2362 assert fctx2 is not None, \
2362 2363 'fctx2 unexpectly None in diff hunks filtering'
2363 2364 hunks = hunksfilterfn(fctx2, hunks)
2364 2365 text = ''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2365 2366 if hdr and (text or len(hdr) > 1):
2366 2367 yield '\n'.join(hdr) + '\n'
2367 2368 if text:
2368 2369 yield text
2369 2370
2370 2371 def diffhunks(repo, node1=None, node2=None, match=None, changes=None,
2371 2372 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2372 2373 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2373 2374 where `header` is a list of diff headers and `hunks` is an iterable of
2374 2375 (`hunkrange`, `hunklines`) tuples.
2375 2376
2376 2377 See diff() for the meaning of parameters.
2377 2378 """
2378 2379
2379 2380 if opts is None:
2380 2381 opts = mdiff.defaultopts
2381 2382
2382 2383 if not node1 and not node2:
2383 2384 node1 = repo.dirstate.p1()
2384 2385
2385 2386 def lrugetfilectx():
2386 2387 cache = {}
2387 2388 order = collections.deque()
2388 2389 def getfilectx(f, ctx):
2389 2390 fctx = ctx.filectx(f, filelog=cache.get(f))
2390 2391 if f not in cache:
2391 2392 if len(cache) > 20:
2392 2393 del cache[order.popleft()]
2393 2394 cache[f] = fctx.filelog()
2394 2395 else:
2395 2396 order.remove(f)
2396 2397 order.append(f)
2397 2398 return fctx
2398 2399 return getfilectx
2399 2400 getfilectx = lrugetfilectx()
2400 2401
2401 2402 ctx1 = repo[node1]
2402 2403 ctx2 = repo[node2]
2403 2404
2404 2405 relfiltered = False
2405 2406 if relroot != '' and match.always():
2406 2407 # as a special case, create a new matcher with just the relroot
2407 2408 pats = [relroot]
2408 2409 match = scmutil.match(ctx2, pats, default='path')
2409 2410 relfiltered = True
2410 2411
2411 2412 if not changes:
2412 2413 changes = repo.status(ctx1, ctx2, match=match)
2413 2414 modified, added, removed = changes[:3]
2414 2415
2415 2416 if not modified and not added and not removed:
2416 2417 return []
2417 2418
2418 2419 if repo.ui.debugflag:
2419 2420 hexfunc = hex
2420 2421 else:
2421 2422 hexfunc = short
2422 2423 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2423 2424
2424 2425 if copy is None:
2425 2426 copy = {}
2426 2427 if opts.git or opts.upgrade:
2427 2428 copy = copies.pathcopies(ctx1, ctx2, match=match)
2428 2429
2429 2430 if relroot is not None:
2430 2431 if not relfiltered:
2431 2432 # XXX this would ideally be done in the matcher, but that is
2432 2433 # generally meant to 'or' patterns, not 'and' them. In this case we
2433 2434 # need to 'and' all the patterns from the matcher with relroot.
2434 2435 def filterrel(l):
2435 2436 return [f for f in l if f.startswith(relroot)]
2436 2437 modified = filterrel(modified)
2437 2438 added = filterrel(added)
2438 2439 removed = filterrel(removed)
2439 2440 relfiltered = True
2440 2441 # filter out copies where either side isn't inside the relative root
2441 2442 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2442 2443 if dst.startswith(relroot)
2443 2444 and src.startswith(relroot)))
2444 2445
2445 2446 modifiedset = set(modified)
2446 2447 addedset = set(added)
2447 2448 removedset = set(removed)
2448 2449 for f in modified:
2449 2450 if f not in ctx1:
2450 2451 # Fix up added, since merged-in additions appear as
2451 2452 # modifications during merges
2452 2453 modifiedset.remove(f)
2453 2454 addedset.add(f)
2454 2455 for f in removed:
2455 2456 if f not in ctx1:
2456 2457 # Merged-in additions that are then removed are reported as removed.
2457 2458 # They are not in ctx1, so We don't want to show them in the diff.
2458 2459 removedset.remove(f)
2459 2460 modified = sorted(modifiedset)
2460 2461 added = sorted(addedset)
2461 2462 removed = sorted(removedset)
2462 2463 for dst, src in list(copy.items()):
2463 2464 if src not in ctx1:
2464 2465 # Files merged in during a merge and then copied/renamed are
2465 2466 # reported as copies. We want to show them in the diff as additions.
2466 2467 del copy[dst]
2467 2468
2468 2469 def difffn(opts, losedata):
2469 2470 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2470 2471 copy, getfilectx, opts, losedata, prefix, relroot)
2471 2472 if opts.upgrade and not opts.git:
2472 2473 try:
2473 2474 def losedata(fn):
2474 2475 if not losedatafn or not losedatafn(fn=fn):
2475 2476 raise GitDiffRequired
2476 2477 # Buffer the whole output until we are sure it can be generated
2477 2478 return list(difffn(opts.copy(git=False), losedata))
2478 2479 except GitDiffRequired:
2479 2480 return difffn(opts.copy(git=True), None)
2480 2481 else:
2481 2482 return difffn(opts, None)
2482 2483
2483 2484 def diffsinglehunk(hunklines):
2484 2485 """yield tokens for a list of lines in a single hunk"""
2485 2486 for line in hunklines:
2486 2487 # chomp
2487 2488 chompline = line.rstrip('\n')
2488 2489 # highlight tabs and trailing whitespace
2489 2490 stripline = chompline.rstrip()
2490 2491 if line[0] == '-':
2491 2492 label = 'diff.deleted'
2492 2493 elif line[0] == '+':
2493 2494 label = 'diff.inserted'
2494 2495 else:
2495 2496 raise error.ProgrammingError('unexpected hunk line: %s' % line)
2496 2497 for token in tabsplitter.findall(stripline):
2497 2498 if '\t' == token[0]:
2498 2499 yield (token, 'diff.tab')
2499 2500 else:
2500 2501 yield (token, label)
2501 2502
2502 2503 if chompline != stripline:
2503 2504 yield (chompline[len(stripline):], 'diff.trailingwhitespace')
2504 2505 if chompline != line:
2505 2506 yield (line[len(chompline):], '')
2506 2507
2508 def diffsinglehunkinline(hunklines):
2509 """yield tokens for a list of lines in a single hunk, with inline colors"""
2510 # prepare deleted, and inserted content
2511 a = ''
2512 b = ''
2513 for line in hunklines:
2514 if line[0] == '-':
2515 a += line[1:]
2516 elif line[0] == '+':
2517 b += line[1:]
2518 else:
2519 raise error.ProgrammingError('unexpected hunk line: %s' % line)
2520 # fast path: if either side is empty, use diffsinglehunk
2521 if not a or not b:
2522 for t in diffsinglehunk(hunklines):
2523 yield t
2524 return
2525 # re-split the content into words
2526 al = wordsplitter.findall(a)
2527 bl = wordsplitter.findall(b)
2528 # re-arrange the words to lines since the diff algorithm is line-based
2529 aln = [s if s == '\n' else s + '\n' for s in al]
2530 bln = [s if s == '\n' else s + '\n' for s in bl]
2531 an = ''.join(aln)
2532 bn = ''.join(bln)
2533 # run the diff algorithm, prepare atokens and btokens
2534 atokens = []
2535 btokens = []
2536 blocks = mdiff.allblocks(an, bn, lines1=aln, lines2=bln)
2537 for (a1, a2, b1, b2), btype in blocks:
2538 changed = btype == '!'
2539 for token in mdiff.splitnewlines(''.join(al[a1:a2])):
2540 atokens.append((changed, token))
2541 for token in mdiff.splitnewlines(''.join(bl[b1:b2])):
2542 btokens.append((changed, token))
2543
2544 # yield deleted tokens, then inserted ones
2545 for prefix, label, tokens in [('-', 'diff.deleted', atokens),
2546 ('+', 'diff.inserted', btokens)]:
2547 nextisnewline = True
2548 for changed, token in tokens:
2549 if nextisnewline:
2550 yield (prefix, label)
2551 nextisnewline = False
2552 # special handling line end
2553 isendofline = token.endswith('\n')
2554 if isendofline:
2555 chomp = token[:-1] # chomp
2556 token = chomp.rstrip() # detect spaces at the end
2557 endspaces = chomp[len(token):]
2558 # scan tabs
2559 for maybetab in tabsplitter.findall(token):
2560 if '\t' == maybetab[0]:
2561 currentlabel = 'diff.tab'
2562 else:
2563 if changed:
2564 currentlabel = label + '.changed'
2565 else:
2566 currentlabel = label + '.unchanged'
2567 yield (maybetab, currentlabel)
2568 if isendofline:
2569 if endspaces:
2570 yield (endspaces, 'diff.trailingwhitespace')
2571 yield ('\n', '')
2572 nextisnewline = True
2573
2507 2574 def difflabel(func, *args, **kw):
2508 2575 '''yields 2-tuples of (output, label) based on the output of func()'''
2576 if kw.get(r'opts') and kw[r'opts'].worddiff:
2577 dodiffhunk = diffsinglehunkinline
2578 else:
2579 dodiffhunk = diffsinglehunk
2509 2580 headprefixes = [('diff', 'diff.diffline'),
2510 2581 ('copy', 'diff.extended'),
2511 2582 ('rename', 'diff.extended'),
2512 2583 ('old', 'diff.extended'),
2513 2584 ('new', 'diff.extended'),
2514 2585 ('deleted', 'diff.extended'),
2515 2586 ('index', 'diff.extended'),
2516 2587 ('similarity', 'diff.extended'),
2517 2588 ('---', 'diff.file_a'),
2518 2589 ('+++', 'diff.file_b')]
2519 2590 textprefixes = [('@', 'diff.hunk'),
2520 2591 # - and + are handled by diffsinglehunk
2521 2592 ]
2522 2593 head = False
2523 2594
2524 2595 # buffers a hunk, i.e. adjacent "-", "+" lines without other changes.
2525 2596 hunkbuffer = []
2526 2597 def consumehunkbuffer():
2527 2598 if hunkbuffer:
2528 for token in diffsinglehunk(hunkbuffer):
2599 for token in dodiffhunk(hunkbuffer):
2529 2600 yield token
2530 2601 hunkbuffer[:] = []
2531 2602
2532 2603 for chunk in func(*args, **kw):
2533 2604 lines = chunk.split('\n')
2534 2605 linecount = len(lines)
2535 2606 for i, line in enumerate(lines):
2536 2607 if head:
2537 2608 if line.startswith('@'):
2538 2609 head = False
2539 2610 else:
2540 2611 if line and not line.startswith((' ', '+', '-', '@', '\\')):
2541 2612 head = True
2542 2613 diffline = False
2543 2614 if not head and line and line.startswith(('+', '-')):
2544 2615 diffline = True
2545 2616
2546 2617 prefixes = textprefixes
2547 2618 if head:
2548 2619 prefixes = headprefixes
2549 2620 if diffline:
2550 2621 # buffered
2551 2622 bufferedline = line
2552 2623 if i + 1 < linecount:
2553 2624 bufferedline += "\n"
2554 2625 hunkbuffer.append(bufferedline)
2555 2626 else:
2556 2627 # unbuffered
2557 2628 for token in consumehunkbuffer():
2558 2629 yield token
2559 2630 stripline = line.rstrip()
2560 2631 for prefix, label in prefixes:
2561 2632 if stripline.startswith(prefix):
2562 2633 yield (stripline, label)
2563 2634 if line != stripline:
2564 2635 yield (line[len(stripline):],
2565 2636 'diff.trailingwhitespace')
2566 2637 break
2567 2638 else:
2568 2639 yield (line, '')
2569 2640 if i + 1 < linecount:
2570 2641 yield ('\n', '')
2571 2642 for token in consumehunkbuffer():
2572 2643 yield token
2573 2644
2574 2645 def diffui(*args, **kw):
2575 2646 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2576 2647 return difflabel(diff, *args, **kw)
2577 2648
2578 2649 def _filepairs(modified, added, removed, copy, opts):
2579 2650 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2580 2651 before and f2 is the the name after. For added files, f1 will be None,
2581 2652 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2582 2653 or 'rename' (the latter two only if opts.git is set).'''
2583 2654 gone = set()
2584 2655
2585 2656 copyto = dict([(v, k) for k, v in copy.items()])
2586 2657
2587 2658 addedset, removedset = set(added), set(removed)
2588 2659
2589 2660 for f in sorted(modified + added + removed):
2590 2661 copyop = None
2591 2662 f1, f2 = f, f
2592 2663 if f in addedset:
2593 2664 f1 = None
2594 2665 if f in copy:
2595 2666 if opts.git:
2596 2667 f1 = copy[f]
2597 2668 if f1 in removedset and f1 not in gone:
2598 2669 copyop = 'rename'
2599 2670 gone.add(f1)
2600 2671 else:
2601 2672 copyop = 'copy'
2602 2673 elif f in removedset:
2603 2674 f2 = None
2604 2675 if opts.git:
2605 2676 # have we already reported a copy above?
2606 2677 if (f in copyto and copyto[f] in addedset
2607 2678 and copy[copyto[f]] == f):
2608 2679 continue
2609 2680 yield f1, f2, copyop
2610 2681
2611 2682 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2612 2683 copy, getfilectx, opts, losedatafn, prefix, relroot):
2613 2684 '''given input data, generate a diff and yield it in blocks
2614 2685
2615 2686 If generating a diff would lose data like flags or binary data and
2616 2687 losedatafn is not None, it will be called.
2617 2688
2618 2689 relroot is removed and prefix is added to every path in the diff output.
2619 2690
2620 2691 If relroot is not empty, this function expects every path in modified,
2621 2692 added, removed and copy to start with it.'''
2622 2693
2623 2694 def gitindex(text):
2624 2695 if not text:
2625 2696 text = ""
2626 2697 l = len(text)
2627 2698 s = hashlib.sha1('blob %d\0' % l)
2628 2699 s.update(text)
2629 2700 return hex(s.digest())
2630 2701
2631 2702 if opts.noprefix:
2632 2703 aprefix = bprefix = ''
2633 2704 else:
2634 2705 aprefix = 'a/'
2635 2706 bprefix = 'b/'
2636 2707
2637 2708 def diffline(f, revs):
2638 2709 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2639 2710 return 'diff %s %s' % (revinfo, f)
2640 2711
2641 2712 def isempty(fctx):
2642 2713 return fctx is None or fctx.size() == 0
2643 2714
2644 2715 date1 = dateutil.datestr(ctx1.date())
2645 2716 date2 = dateutil.datestr(ctx2.date())
2646 2717
2647 2718 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2648 2719
2649 2720 if relroot != '' and (repo.ui.configbool('devel', 'all-warnings')
2650 2721 or repo.ui.configbool('devel', 'check-relroot')):
2651 2722 for f in modified + added + removed + list(copy) + list(copy.values()):
2652 2723 if f is not None and not f.startswith(relroot):
2653 2724 raise AssertionError(
2654 2725 "file %s doesn't start with relroot %s" % (f, relroot))
2655 2726
2656 2727 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2657 2728 content1 = None
2658 2729 content2 = None
2659 2730 fctx1 = None
2660 2731 fctx2 = None
2661 2732 flag1 = None
2662 2733 flag2 = None
2663 2734 if f1:
2664 2735 fctx1 = getfilectx(f1, ctx1)
2665 2736 if opts.git or losedatafn:
2666 2737 flag1 = ctx1.flags(f1)
2667 2738 if f2:
2668 2739 fctx2 = getfilectx(f2, ctx2)
2669 2740 if opts.git or losedatafn:
2670 2741 flag2 = ctx2.flags(f2)
2671 2742 # if binary is True, output "summary" or "base85", but not "text diff"
2672 2743 if opts.text:
2673 2744 binary = False
2674 2745 else:
2675 2746 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
2676 2747
2677 2748 if losedatafn and not opts.git:
2678 2749 if (binary or
2679 2750 # copy/rename
2680 2751 f2 in copy or
2681 2752 # empty file creation
2682 2753 (not f1 and isempty(fctx2)) or
2683 2754 # empty file deletion
2684 2755 (isempty(fctx1) and not f2) or
2685 2756 # create with flags
2686 2757 (not f1 and flag2) or
2687 2758 # change flags
2688 2759 (f1 and f2 and flag1 != flag2)):
2689 2760 losedatafn(f2 or f1)
2690 2761
2691 2762 path1 = f1 or f2
2692 2763 path2 = f2 or f1
2693 2764 path1 = posixpath.join(prefix, path1[len(relroot):])
2694 2765 path2 = posixpath.join(prefix, path2[len(relroot):])
2695 2766 header = []
2696 2767 if opts.git:
2697 2768 header.append('diff --git %s%s %s%s' %
2698 2769 (aprefix, path1, bprefix, path2))
2699 2770 if not f1: # added
2700 2771 header.append('new file mode %s' % gitmode[flag2])
2701 2772 elif not f2: # removed
2702 2773 header.append('deleted file mode %s' % gitmode[flag1])
2703 2774 else: # modified/copied/renamed
2704 2775 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2705 2776 if mode1 != mode2:
2706 2777 header.append('old mode %s' % mode1)
2707 2778 header.append('new mode %s' % mode2)
2708 2779 if copyop is not None:
2709 2780 if opts.showsimilarity:
2710 2781 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2711 2782 header.append('similarity index %d%%' % sim)
2712 2783 header.append('%s from %s' % (copyop, path1))
2713 2784 header.append('%s to %s' % (copyop, path2))
2714 2785 elif revs and not repo.ui.quiet:
2715 2786 header.append(diffline(path1, revs))
2716 2787
2717 2788 # fctx.is | diffopts | what to | is fctx.data()
2718 2789 # binary() | text nobinary git index | output? | outputted?
2719 2790 # ------------------------------------|----------------------------
2720 2791 # yes | no no no * | summary | no
2721 2792 # yes | no no yes * | base85 | yes
2722 2793 # yes | no yes no * | summary | no
2723 2794 # yes | no yes yes 0 | summary | no
2724 2795 # yes | no yes yes >0 | summary | semi [1]
2725 2796 # yes | yes * * * | text diff | yes
2726 2797 # no | * * * * | text diff | yes
2727 2798 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
2728 2799 if binary and (not opts.git or (opts.git and opts.nobinary and not
2729 2800 opts.index)):
2730 2801 # fast path: no binary content will be displayed, content1 and
2731 2802 # content2 are only used for equivalent test. cmp() could have a
2732 2803 # fast path.
2733 2804 if fctx1 is not None:
2734 2805 content1 = b'\0'
2735 2806 if fctx2 is not None:
2736 2807 if fctx1 is not None and not fctx1.cmp(fctx2):
2737 2808 content2 = b'\0' # not different
2738 2809 else:
2739 2810 content2 = b'\0\0'
2740 2811 else:
2741 2812 # normal path: load contents
2742 2813 if fctx1 is not None:
2743 2814 content1 = fctx1.data()
2744 2815 if fctx2 is not None:
2745 2816 content2 = fctx2.data()
2746 2817
2747 2818 if binary and opts.git and not opts.nobinary:
2748 2819 text = mdiff.b85diff(content1, content2)
2749 2820 if text:
2750 2821 header.append('index %s..%s' %
2751 2822 (gitindex(content1), gitindex(content2)))
2752 2823 hunks = (None, [text]),
2753 2824 else:
2754 2825 if opts.git and opts.index > 0:
2755 2826 flag = flag1
2756 2827 if flag is None:
2757 2828 flag = flag2
2758 2829 header.append('index %s..%s %s' %
2759 2830 (gitindex(content1)[0:opts.index],
2760 2831 gitindex(content2)[0:opts.index],
2761 2832 gitmode[flag]))
2762 2833
2763 2834 uheaders, hunks = mdiff.unidiff(content1, date1,
2764 2835 content2, date2,
2765 2836 path1, path2,
2766 2837 binary=binary, opts=opts)
2767 2838 header.extend(uheaders)
2768 2839 yield fctx1, fctx2, header, hunks
2769 2840
2770 2841 def diffstatsum(stats):
2771 2842 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2772 2843 for f, a, r, b in stats:
2773 2844 maxfile = max(maxfile, encoding.colwidth(f))
2774 2845 maxtotal = max(maxtotal, a + r)
2775 2846 addtotal += a
2776 2847 removetotal += r
2777 2848 binary = binary or b
2778 2849
2779 2850 return maxfile, maxtotal, addtotal, removetotal, binary
2780 2851
2781 2852 def diffstatdata(lines):
2782 2853 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2783 2854
2784 2855 results = []
2785 2856 filename, adds, removes, isbinary = None, 0, 0, False
2786 2857
2787 2858 def addresult():
2788 2859 if filename:
2789 2860 results.append((filename, adds, removes, isbinary))
2790 2861
2791 2862 # inheader is used to track if a line is in the
2792 2863 # header portion of the diff. This helps properly account
2793 2864 # for lines that start with '--' or '++'
2794 2865 inheader = False
2795 2866
2796 2867 for line in lines:
2797 2868 if line.startswith('diff'):
2798 2869 addresult()
2799 2870 # starting a new file diff
2800 2871 # set numbers to 0 and reset inheader
2801 2872 inheader = True
2802 2873 adds, removes, isbinary = 0, 0, False
2803 2874 if line.startswith('diff --git a/'):
2804 2875 filename = gitre.search(line).group(2)
2805 2876 elif line.startswith('diff -r'):
2806 2877 # format: "diff -r ... -r ... filename"
2807 2878 filename = diffre.search(line).group(1)
2808 2879 elif line.startswith('@@'):
2809 2880 inheader = False
2810 2881 elif line.startswith('+') and not inheader:
2811 2882 adds += 1
2812 2883 elif line.startswith('-') and not inheader:
2813 2884 removes += 1
2814 2885 elif (line.startswith('GIT binary patch') or
2815 2886 line.startswith('Binary file')):
2816 2887 isbinary = True
2817 2888 addresult()
2818 2889 return results
2819 2890
2820 2891 def diffstat(lines, width=80):
2821 2892 output = []
2822 2893 stats = diffstatdata(lines)
2823 2894 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2824 2895
2825 2896 countwidth = len(str(maxtotal))
2826 2897 if hasbinary and countwidth < 3:
2827 2898 countwidth = 3
2828 2899 graphwidth = width - countwidth - maxname - 6
2829 2900 if graphwidth < 10:
2830 2901 graphwidth = 10
2831 2902
2832 2903 def scale(i):
2833 2904 if maxtotal <= graphwidth:
2834 2905 return i
2835 2906 # If diffstat runs out of room it doesn't print anything,
2836 2907 # which isn't very useful, so always print at least one + or -
2837 2908 # if there were at least some changes.
2838 2909 return max(i * graphwidth // maxtotal, int(bool(i)))
2839 2910
2840 2911 for filename, adds, removes, isbinary in stats:
2841 2912 if isbinary:
2842 2913 count = 'Bin'
2843 2914 else:
2844 2915 count = '%d' % (adds + removes)
2845 2916 pluses = '+' * scale(adds)
2846 2917 minuses = '-' * scale(removes)
2847 2918 output.append(' %s%s | %*s %s%s\n' %
2848 2919 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2849 2920 countwidth, count, pluses, minuses))
2850 2921
2851 2922 if stats:
2852 2923 output.append(_(' %d files changed, %d insertions(+), '
2853 2924 '%d deletions(-)\n')
2854 2925 % (len(stats), totaladds, totalremoves))
2855 2926
2856 2927 return ''.join(output)
2857 2928
2858 2929 def diffstatui(*args, **kw):
2859 2930 '''like diffstat(), but yields 2-tuples of (output, label) for
2860 2931 ui.write()
2861 2932 '''
2862 2933
2863 2934 for line in diffstat(*args, **kw).splitlines():
2864 2935 if line and line[-1] in '+-':
2865 2936 name, graph = line.rsplit(' ', 1)
2866 2937 yield (name + ' ', '')
2867 2938 m = re.search(br'\++', graph)
2868 2939 if m:
2869 2940 yield (m.group(0), 'diffstat.inserted')
2870 2941 m = re.search(br'-+', graph)
2871 2942 if m:
2872 2943 yield (m.group(0), 'diffstat.deleted')
2873 2944 else:
2874 2945 yield (line, '')
2875 2946 yield ('\n', '')
@@ -1,397 +1,393 b''
1 1 Setup
2 2
3 3 $ cat <<EOF >> $HGRCPATH
4 4 > [ui]
5 5 > color = yes
6 6 > formatted = always
7 7 > paginate = never
8 8 > [color]
9 9 > mode = ansi
10 10 > EOF
11 11 $ hg init repo
12 12 $ cd repo
13 13 $ cat > a <<EOF
14 14 > c
15 15 > c
16 16 > a
17 17 > a
18 18 > b
19 19 > a
20 20 > a
21 21 > c
22 22 > c
23 23 > EOF
24 24 $ hg ci -Am adda
25 25 adding a
26 26 $ cat > a <<EOF
27 27 > c
28 28 > c
29 29 > a
30 30 > a
31 31 > dd
32 32 > a
33 33 > a
34 34 > c
35 35 > c
36 36 > EOF
37 37
38 38 default context
39 39
40 40 $ hg diff --nodates
41 41 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
42 42 \x1b[0;31;1m--- a/a\x1b[0m (esc)
43 43 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
44 44 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
45 45 c
46 46 a
47 47 a
48 48 \x1b[0;31m-b\x1b[0m (esc)
49 49 \x1b[0;32m+dd\x1b[0m (esc)
50 50 a
51 51 a
52 52 c
53 53
54 54 (check that 'ui.color=yes' match '--color=auto')
55 55
56 56 $ hg diff --nodates --config ui.formatted=no
57 57 diff -r cf9f4ba66af2 a
58 58 --- a/a
59 59 +++ b/a
60 60 @@ -2,7 +2,7 @@
61 61 c
62 62 a
63 63 a
64 64 -b
65 65 +dd
66 66 a
67 67 a
68 68 c
69 69
70 70 (check that 'ui.color=no' disable color)
71 71
72 72 $ hg diff --nodates --config ui.formatted=yes --config ui.color=no
73 73 diff -r cf9f4ba66af2 a
74 74 --- a/a
75 75 +++ b/a
76 76 @@ -2,7 +2,7 @@
77 77 c
78 78 a
79 79 a
80 80 -b
81 81 +dd
82 82 a
83 83 a
84 84 c
85 85
86 86 (check that 'ui.color=always' force color)
87 87
88 88 $ hg diff --nodates --config ui.formatted=no --config ui.color=always
89 89 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
90 90 \x1b[0;31;1m--- a/a\x1b[0m (esc)
91 91 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
92 92 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
93 93 c
94 94 a
95 95 a
96 96 \x1b[0;31m-b\x1b[0m (esc)
97 97 \x1b[0;32m+dd\x1b[0m (esc)
98 98 a
99 99 a
100 100 c
101 101
102 102 --unified=2
103 103
104 104 $ hg diff --nodates -U 2
105 105 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
106 106 \x1b[0;31;1m--- a/a\x1b[0m (esc)
107 107 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
108 108 \x1b[0;35m@@ -3,5 +3,5 @@\x1b[0m (esc)
109 109 a
110 110 a
111 111 \x1b[0;31m-b\x1b[0m (esc)
112 112 \x1b[0;32m+dd\x1b[0m (esc)
113 113 a
114 114 a
115 115
116 116 diffstat
117 117
118 118 $ hg diff --stat
119 119 a | 2 \x1b[0;32m+\x1b[0m\x1b[0;31m-\x1b[0m (esc)
120 120 1 files changed, 1 insertions(+), 1 deletions(-)
121 121 $ cat <<EOF >> $HGRCPATH
122 122 > [extensions]
123 123 > record =
124 124 > [ui]
125 125 > interactive = true
126 126 > [diff]
127 127 > git = True
128 128 > EOF
129 129
130 130 #if execbit
131 131
132 132 record
133 133
134 134 $ chmod +x a
135 135 $ hg record -m moda a <<EOF
136 136 > y
137 137 > y
138 138 > EOF
139 139 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
140 140 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
141 141 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
142 142 1 hunks, 1 lines changed
143 143 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m y (esc)
144 144
145 145 \x1b[0;35m@@ -2,7 +2,7 @@ c\x1b[0m (esc)
146 146 c
147 147 a
148 148 a
149 149 \x1b[0;31m-b\x1b[0m (esc)
150 150 \x1b[0;32m+dd\x1b[0m (esc)
151 151 a
152 152 a
153 153 c
154 154 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m y (esc)
155 155
156 156
157 157 $ echo "[extensions]" >> $HGRCPATH
158 158 $ echo "mq=" >> $HGRCPATH
159 159 $ hg rollback
160 160 repository tip rolled back to revision 0 (undo commit)
161 161 working directory now based on revision 0
162 162
163 163 qrecord
164 164
165 165 $ hg qrecord -m moda patch <<EOF
166 166 > y
167 167 > y
168 168 > EOF
169 169 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
170 170 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
171 171 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
172 172 1 hunks, 1 lines changed
173 173 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m y (esc)
174 174
175 175 \x1b[0;35m@@ -2,7 +2,7 @@ c\x1b[0m (esc)
176 176 c
177 177 a
178 178 a
179 179 \x1b[0;31m-b\x1b[0m (esc)
180 180 \x1b[0;32m+dd\x1b[0m (esc)
181 181 a
182 182 a
183 183 c
184 184 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m y (esc)
185 185
186 186
187 187 $ hg qpop -a
188 188 popping patch
189 189 patch queue now empty
190 190
191 191 #endif
192 192
193 193 issue3712: test colorization of subrepo diff
194 194
195 195 $ hg init sub
196 196 $ echo b > sub/b
197 197 $ hg -R sub commit -Am 'create sub'
198 198 adding b
199 199 $ echo 'sub = sub' > .hgsub
200 200 $ hg add .hgsub
201 201 $ hg commit -m 'add subrepo sub'
202 202 $ echo aa >> a
203 203 $ echo bb >> sub/b
204 204
205 205 $ hg diff -S
206 206 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
207 207 \x1b[0;31;1m--- a/a\x1b[0m (esc)
208 208 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
209 209 \x1b[0;35m@@ -7,3 +7,4 @@\x1b[0m (esc)
210 210 a
211 211 c
212 212 c
213 213 \x1b[0;32m+aa\x1b[0m (esc)
214 214 \x1b[0;1mdiff --git a/sub/b b/sub/b\x1b[0m (esc)
215 215 \x1b[0;31;1m--- a/sub/b\x1b[0m (esc)
216 216 \x1b[0;32;1m+++ b/sub/b\x1b[0m (esc)
217 217 \x1b[0;35m@@ -1,1 +1,2 @@\x1b[0m (esc)
218 218 b
219 219 \x1b[0;32m+bb\x1b[0m (esc)
220 220
221 221 test tabs
222 222
223 223 $ cat >> a <<EOF
224 224 > one tab
225 225 > two tabs
226 226 > end tab
227 227 > mid tab
228 228 > all tabs
229 229 > EOF
230 230 $ hg diff --nodates
231 231 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
232 232 \x1b[0;31;1m--- a/a\x1b[0m (esc)
233 233 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
234 234 \x1b[0;35m@@ -7,3 +7,9 @@\x1b[0m (esc)
235 235 a
236 236 c
237 237 c
238 238 \x1b[0;32m+aa\x1b[0m (esc)
239 239 \x1b[0;32m+\x1b[0m \x1b[0;32mone tab\x1b[0m (esc)
240 240 \x1b[0;32m+\x1b[0m \x1b[0;32mtwo tabs\x1b[0m (esc)
241 241 \x1b[0;32m+end tab\x1b[0m\x1b[0;1;41m \x1b[0m (esc)
242 242 \x1b[0;32m+mid\x1b[0m \x1b[0;32mtab\x1b[0m (esc)
243 243 \x1b[0;32m+\x1b[0m \x1b[0;32mall\x1b[0m \x1b[0;32mtabs\x1b[0m\x1b[0;1;41m \x1b[0m (esc)
244 244 $ echo "[color]" >> $HGRCPATH
245 245 $ echo "diff.tab = bold magenta" >> $HGRCPATH
246 246 $ hg diff --nodates
247 247 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
248 248 \x1b[0;31;1m--- a/a\x1b[0m (esc)
249 249 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
250 250 \x1b[0;35m@@ -7,3 +7,9 @@\x1b[0m (esc)
251 251 a
252 252 c
253 253 c
254 254 \x1b[0;32m+aa\x1b[0m (esc)
255 255 \x1b[0;32m+\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mone tab\x1b[0m (esc)
256 256 \x1b[0;32m+\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mtwo tabs\x1b[0m (esc)
257 257 \x1b[0;32m+end tab\x1b[0m\x1b[0;1;41m \x1b[0m (esc)
258 258 \x1b[0;32m+mid\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mtab\x1b[0m (esc)
259 259 \x1b[0;32m+\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mall\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mtabs\x1b[0m\x1b[0;1;41m \x1b[0m (esc)
260 260
261 261 $ cd ..
262 262
263 263 test inline color diff
264 264
265 265 $ hg init inline
266 266 $ cd inline
267 267 $ cat > file1 << EOF
268 268 > this is the first line
269 269 > this is the second line
270 270 > third line starts with space
271 271 > + starts with a plus sign
272 272 > this one with one tab
273 273 > now with full two tabs
274 274 > now tabs everywhere, much fun
275 275 >
276 276 > this line won't change
277 277 >
278 278 > two lines are going to
279 279 > be changed into three!
280 280 >
281 281 > three of those lines will
282 282 > collapse onto one
283 283 > (to see if it works)
284 284 > EOF
285 285 $ hg add file1
286 286 $ hg ci -m 'commit'
287 287
288 288 $ cat > file1 << EOF
289 289 > that is the first paragraph
290 290 > this is the second line
291 291 > third line starts with space
292 292 > - starts with a minus sign
293 293 > this one with two tab
294 294 > now with full three tabs
295 295 > now there are tabs everywhere, much fun
296 296 >
297 297 > this line won't change
298 298 >
299 299 > two lines are going to
300 300 > (entirely magically,
301 301 > assuming this works)
302 302 > be changed into four!
303 303 >
304 304 > three of those lines have
305 305 > collapsed onto one
306 306 > EOF
307 307 $ hg diff --config experimental.worddiff=False --color=debug
308 308 [diff.diffline|diff --git a/file1 b/file1]
309 309 [diff.file_a|--- a/file1]
310 310 [diff.file_b|+++ b/file1]
311 311 [diff.hunk|@@ -1,16 +1,17 @@]
312 312 [diff.deleted|-this is the first line]
313 313 [diff.deleted|-this is the second line]
314 314 [diff.deleted|- third line starts with space]
315 315 [diff.deleted|-+ starts with a plus sign]
316 316 [diff.deleted|-][diff.tab| ][diff.deleted|this one with one tab]
317 317 [diff.deleted|-][diff.tab| ][diff.deleted|now with full two tabs]
318 318 [diff.deleted|-][diff.tab| ][diff.deleted|now tabs][diff.tab| ][diff.deleted|everywhere, much fun]
319 319 [diff.inserted|+that is the first paragraph]
320 320 [diff.inserted|+ this is the second line]
321 321 [diff.inserted|+third line starts with space]
322 322 [diff.inserted|+- starts with a minus sign]
323 323 [diff.inserted|+][diff.tab| ][diff.inserted|this one with two tab]
324 324 [diff.inserted|+][diff.tab| ][diff.inserted|now with full three tabs]
325 325 [diff.inserted|+][diff.tab| ][diff.inserted|now there are tabs][diff.tab| ][diff.inserted|everywhere, much fun]
326 326
327 327 this line won't change
328 328
329 329 two lines are going to
330 330 [diff.deleted|-be changed into three!]
331 331 [diff.inserted|+(entirely magically,]
332 332 [diff.inserted|+ assuming this works)]
333 333 [diff.inserted|+be changed into four!]
334 334
335 335 [diff.deleted|-three of those lines will]
336 336 [diff.deleted|-collapse onto one]
337 337 [diff.deleted|-(to see if it works)]
338 338 [diff.inserted|+three of those lines have]
339 339 [diff.inserted|+collapsed onto one]
340 #if false
341 340 $ hg diff --config experimental.worddiff=True --color=debug
342 341 [diff.diffline|diff --git a/file1 b/file1]
343 342 [diff.file_a|--- a/file1]
344 343 [diff.file_b|+++ b/file1]
345 344 [diff.hunk|@@ -1,16 +1,17 @@]
346 [diff.deleted|-this is the ][diff.deleted.highlight|first][diff.deleted| line]
347 [diff.deleted|-this is the second line]
348 [diff.deleted|-][diff.deleted.highlight| ][diff.deleted|third line starts with space]
349 [diff.deleted|-][diff.deleted.highlight|+][diff.deleted| starts with a ][diff.deleted.highlight|plus][diff.deleted| sign]
350 [diff.deleted|-][diff.tab| ][diff.deleted|this one with ][diff.deleted.highlight|one][diff.deleted| tab]
351 [diff.deleted|-][diff.tab| ][diff.deleted|now with full ][diff.deleted.highlight|two][diff.deleted| tabs]
352 [diff.deleted|-][diff.tab| ][diff.deleted|now tabs][diff.tab| ][diff.deleted|everywhere, much fun]
353 [diff.inserted|+that is the first paragraph]
354 [diff.inserted|+][diff.inserted.highlight| ][diff.inserted|this is the ][diff.inserted.highlight|second][diff.inserted| line]
355 [diff.inserted|+third line starts with space]
356 [diff.inserted|+][diff.inserted.highlight|-][diff.inserted| starts with a ][diff.inserted.highlight|minus][diff.inserted| sign]
357 [diff.inserted|+][diff.tab| ][diff.inserted|this one with ][diff.inserted.highlight|two][diff.inserted| tab]
358 [diff.inserted|+][diff.tab| ][diff.inserted|now with full ][diff.inserted.highlight|three][diff.inserted| tabs]
359 [diff.inserted|+][diff.tab| ][diff.inserted|now][diff.inserted.highlight| there are][diff.inserted| tabs][diff.tab| ][diff.inserted|everywhere, much fun]
345 [diff.deleted|-][diff.deleted.changed|this][diff.deleted.unchanged| is the first ][diff.deleted.changed|line]
346 [diff.deleted|-][diff.deleted.unchanged|this is the second line]
347 [diff.deleted|-][diff.deleted.changed| ][diff.deleted.unchanged|third line starts with space]
348 [diff.deleted|-][diff.deleted.changed|+][diff.deleted.unchanged| starts with a ][diff.deleted.changed|plus][diff.deleted.unchanged| sign]
349 [diff.deleted|-][diff.tab| ][diff.deleted.unchanged|this one with ][diff.deleted.changed|one][diff.deleted.unchanged| tab]
350 [diff.deleted|-][diff.tab| ][diff.deleted.unchanged|now with full ][diff.deleted.changed|two][diff.deleted.unchanged| tabs]
351 [diff.deleted|-][diff.tab| ][diff.deleted.unchanged|now ][diff.deleted.unchanged|tabs][diff.tab| ][diff.deleted.unchanged|everywhere, much fun]
352 [diff.inserted|+][diff.inserted.changed|that][diff.inserted.unchanged| is the first ][diff.inserted.changed|paragraph]
353 [diff.inserted|+][diff.inserted.changed| ][diff.inserted.unchanged|this is the second line]
354 [diff.inserted|+][diff.inserted.unchanged|third line starts with space]
355 [diff.inserted|+][diff.inserted.changed|-][diff.inserted.unchanged| starts with a ][diff.inserted.changed|minus][diff.inserted.unchanged| sign]
356 [diff.inserted|+][diff.tab| ][diff.inserted.unchanged|this one with ][diff.inserted.changed|two][diff.inserted.unchanged| tab]
357 [diff.inserted|+][diff.tab| ][diff.inserted.unchanged|now with full ][diff.inserted.changed|three][diff.inserted.unchanged| tabs]
358 [diff.inserted|+][diff.tab| ][diff.inserted.unchanged|now ][diff.inserted.changed|there are ][diff.inserted.unchanged|tabs][diff.tab| ][diff.inserted.unchanged|everywhere, much fun]
360 359
361 360 this line won't change
362 361
363 362 two lines are going to
364 [diff.deleted|-be changed into ][diff.deleted.highlight|three][diff.deleted|!]
365 [diff.inserted|+(entirely magically,]
366 [diff.inserted|+ assuming this works)]
367 [diff.inserted|+be changed into ][diff.inserted.highlight|four][diff.inserted|!]
363 [diff.deleted|-][diff.deleted.unchanged|be changed into ][diff.deleted.changed|three][diff.deleted.unchanged|!]
364 [diff.inserted|+][diff.inserted.changed|(entirely magically,]
365 [diff.inserted|+][diff.inserted.changed| assuming this works)]
366 [diff.inserted|+][diff.inserted.unchanged|be changed into ][diff.inserted.changed|four][diff.inserted.unchanged|!]
368 367
369 [diff.deleted|-three of those lines ][diff.deleted.highlight|will]
370 [diff.deleted|-][diff.deleted.highlight|collapse][diff.deleted| onto one]
371 [diff.deleted|-(to see if it works)]
372 [diff.inserted|+three of those lines ][diff.inserted.highlight|have]
373 [diff.inserted|+][diff.inserted.highlight|collapsed][diff.inserted| onto one]
374 #endif
368 [diff.deleted|-][diff.deleted.unchanged|three of those lines ][diff.deleted.changed|will]
369 [diff.deleted|-][diff.deleted.changed|collapse][diff.deleted.unchanged| onto one]
370 [diff.deleted|-][diff.deleted.changed|(to see if it works)]
371 [diff.inserted|+][diff.inserted.unchanged|three of those lines ][diff.inserted.changed|have]
372 [diff.inserted|+][diff.inserted.changed|collapsed][diff.inserted.unchanged| onto one]
375 373
376 374 multibyte character shouldn't be broken up in word diff:
377 375
378 376 $ $PYTHON <<'EOF'
379 377 > with open("utf8", "wb") as f:
380 378 > f.write(b"blah \xe3\x82\xa2 blah\n")
381 379 > EOF
382 380 $ hg ci -Am 'add utf8 char' utf8
383 381 $ $PYTHON <<'EOF'
384 382 > with open("utf8", "wb") as f:
385 383 > f.write(b"blah \xe3\x82\xa4 blah\n")
386 384 > EOF
387 385 $ hg ci -m 'slightly change utf8 char' utf8
388 386
389 #if false
390 387 $ hg diff --config experimental.worddiff=True --color=debug -c.
391 388 [diff.diffline|diff --git a/utf8 b/utf8]
392 389 [diff.file_a|--- a/utf8]
393 390 [diff.file_b|+++ b/utf8]
394 391 [diff.hunk|@@ -1,1 +1,1 @@]
395 [diff.deleted|-blah ][diff.deleted.highlight|\xe3\x82\xa2][diff.deleted| blah] (esc)
396 [diff.inserted|+blah ][diff.inserted.highlight|\xe3\x82\xa4][diff.inserted| blah] (esc)
397 #endif
392 [diff.deleted|-][diff.deleted.unchanged|blah ][diff.deleted.changed|\xe3\x82\xa2][diff.deleted.unchanged| blah] (esc)
393 [diff.inserted|+][diff.inserted.unchanged|blah ][diff.inserted.changed|\xe3\x82\xa4][diff.inserted.unchanged| blah] (esc)
General Comments 0
You need to be logged in to leave comments. Login now