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