##// END OF EJS Templates
dynamic-import: use sysstr for importing extension and others...
marmoute -
r51816:c642c039 default
parent child Browse files
Show More
@@ -1,1011 +1,1011 b''
1 # extensions.py - extension handling for mercurial
1 # extensions.py - extension handling for mercurial
2 #
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
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
8
9 import ast
9 import ast
10 import collections
10 import collections
11 import functools
11 import functools
12 import importlib
12 import importlib
13 import inspect
13 import inspect
14 import os
14 import os
15 import sys
15 import sys
16
16
17 from .i18n import (
17 from .i18n import (
18 _,
18 _,
19 gettext,
19 gettext,
20 )
20 )
21 from .pycompat import (
21 from .pycompat import (
22 getattr,
22 getattr,
23 open,
23 open,
24 setattr,
24 setattr,
25 )
25 )
26
26
27 from . import (
27 from . import (
28 cmdutil,
28 cmdutil,
29 configitems,
29 configitems,
30 error,
30 error,
31 pycompat,
31 pycompat,
32 util,
32 util,
33 )
33 )
34
34
35 from .utils import stringutil
35 from .utils import stringutil
36
36
37 _extensions = {}
37 _extensions = {}
38 _disabledextensions = {}
38 _disabledextensions = {}
39 _aftercallbacks = {}
39 _aftercallbacks = {}
40 _order = []
40 _order = []
41 _builtin = {
41 _builtin = {
42 b'hbisect',
42 b'hbisect',
43 b'bookmarks',
43 b'bookmarks',
44 b'color',
44 b'color',
45 b'parentrevspec',
45 b'parentrevspec',
46 b'progress',
46 b'progress',
47 b'interhg',
47 b'interhg',
48 b'inotify',
48 b'inotify',
49 b'hgcia',
49 b'hgcia',
50 b'shelve',
50 b'shelve',
51 }
51 }
52
52
53
53
54 def extensions(ui=None):
54 def extensions(ui=None):
55 if ui:
55 if ui:
56
56
57 def enabled(name):
57 def enabled(name):
58 for format in [b'%s', b'hgext.%s']:
58 for format in [b'%s', b'hgext.%s']:
59 conf = ui.config(b'extensions', format % name)
59 conf = ui.config(b'extensions', format % name)
60 if conf is not None and not conf.startswith(b'!'):
60 if conf is not None and not conf.startswith(b'!'):
61 return True
61 return True
62
62
63 else:
63 else:
64 enabled = lambda name: True
64 enabled = lambda name: True
65 for name in _order:
65 for name in _order:
66 module = _extensions[name]
66 module = _extensions[name]
67 if module and enabled(name):
67 if module and enabled(name):
68 yield name, module
68 yield name, module
69
69
70
70
71 def find(name):
71 def find(name):
72 '''return module with given extension name'''
72 '''return module with given extension name'''
73 mod = None
73 mod = None
74 try:
74 try:
75 mod = _extensions[name]
75 mod = _extensions[name]
76 except KeyError:
76 except KeyError:
77 for k, v in _extensions.items():
77 for k, v in _extensions.items():
78 if k.endswith(b'.' + name) or k.endswith(b'/' + name):
78 if k.endswith(b'.' + name) or k.endswith(b'/' + name):
79 mod = v
79 mod = v
80 break
80 break
81 if not mod:
81 if not mod:
82 raise KeyError(name)
82 raise KeyError(name)
83 return mod
83 return mod
84
84
85
85
86 def loadpath(path, module_name):
86 def loadpath(path, module_name):
87 module_name = module_name.replace(b'.', b'_')
87 module_name = module_name.replace('.', '_')
88 path = util.normpath(util.expandpath(path))
88 path = util.normpath(util.expandpath(path))
89 module_name = pycompat.fsdecode(module_name)
90 path = pycompat.fsdecode(path)
89 path = pycompat.fsdecode(path)
91 if os.path.isdir(path):
90 if os.path.isdir(path):
92 # module/__init__.py style
91 # module/__init__.py style
93 init_py_path = os.path.join(path, '__init__.py')
92 init_py_path = os.path.join(path, '__init__.py')
94 if not os.path.exists(init_py_path):
93 if not os.path.exists(init_py_path):
95 raise ImportError("No module named '%s'" % os.path.basename(path))
94 raise ImportError("No module named '%s'" % os.path.basename(path))
96 path = init_py_path
95 path = init_py_path
97
96
98 loader = importlib.machinery.SourceFileLoader(module_name, path)
97 loader = importlib.machinery.SourceFileLoader(module_name, path)
99 spec = importlib.util.spec_from_file_location(module_name, loader=loader)
98 spec = importlib.util.spec_from_file_location(module_name, loader=loader)
100 assert spec is not None # help Pytype
99 assert spec is not None # help Pytype
101 module = importlib.util.module_from_spec(spec)
100 module = importlib.util.module_from_spec(spec)
102 sys.modules[module_name] = module
101 sys.modules[module_name] = module
103 spec.loader.exec_module(module)
102 spec.loader.exec_module(module)
104 return module
103 return module
105
104
106
105
107 def _importh(name):
106 def _importh(name):
108 """import and return the <name> module"""
107 """import and return the <name> module"""
109 mod = __import__(pycompat.sysstr(name))
108 mod = __import__(name)
110 components = name.split(b'.')
109 components = name.split('.')
111 for comp in components[1:]:
110 for comp in components[1:]:
112 mod = getattr(mod, comp)
111 mod = getattr(mod, comp)
113 return mod
112 return mod
114
113
115
114
116 def _importext(name, path=None, reportfunc=None):
115 def _importext(name, path=None, reportfunc=None):
116 name = pycompat.fsdecode(name)
117 if path:
117 if path:
118 # the module will be loaded in sys.modules
118 # the module will be loaded in sys.modules
119 # choose an unique name so that it doesn't
119 # choose an unique name so that it doesn't
120 # conflicts with other modules
120 # conflicts with other modules
121 mod = loadpath(path, b'hgext.%s' % name)
121 mod = loadpath(path, 'hgext.%s' % name)
122 else:
122 else:
123 try:
123 try:
124 mod = _importh(b"hgext.%s" % name)
124 mod = _importh("hgext.%s" % name)
125 except ImportError as err:
125 except ImportError as err:
126 if reportfunc:
126 if reportfunc:
127 reportfunc(err, b"hgext.%s" % name, b"hgext3rd.%s" % name)
127 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
128 try:
128 try:
129 mod = _importh(b"hgext3rd.%s" % name)
129 mod = _importh("hgext3rd.%s" % name)
130 except ImportError as err:
130 except ImportError as err:
131 if reportfunc:
131 if reportfunc:
132 reportfunc(err, b"hgext3rd.%s" % name, name)
132 reportfunc(err, "hgext3rd.%s" % name, name)
133 mod = _importh(name)
133 mod = _importh(name)
134 return mod
134 return mod
135
135
136
136
137 def _reportimporterror(ui, err, failed, next):
137 def _reportimporterror(ui, err, failed, next):
138 # note: this ui.log happens before --debug is processed,
138 # note: this ui.log happens before --debug is processed,
139 # Use --config ui.debug=1 to see them.
139 # Use --config ui.debug=1 to see them.
140 ui.log(
140 ui.log(
141 b'extension',
141 b'extension',
142 b' - could not import %s (%s): trying %s\n',
142 b' - could not import %s (%s): trying %s\n',
143 failed,
143 stringutil.forcebytestr(failed),
144 stringutil.forcebytestr(err),
144 stringutil.forcebytestr(err),
145 next,
145 stringutil.forcebytestr(next),
146 )
146 )
147 if ui.debugflag and ui.configbool(b'devel', b'debug.extensions'):
147 if ui.debugflag and ui.configbool(b'devel', b'debug.extensions'):
148 ui.traceback()
148 ui.traceback()
149
149
150
150
151 def _rejectunicode(name, xs):
151 def _rejectunicode(name, xs):
152 if isinstance(xs, (list, set, tuple)):
152 if isinstance(xs, (list, set, tuple)):
153 for x in xs:
153 for x in xs:
154 _rejectunicode(name, x)
154 _rejectunicode(name, x)
155 elif isinstance(xs, dict):
155 elif isinstance(xs, dict):
156 for k, v in xs.items():
156 for k, v in xs.items():
157 _rejectunicode(name, k)
157 _rejectunicode(name, k)
158 k = pycompat.sysstr(k)
158 k = pycompat.sysstr(k)
159 _rejectunicode('%s.%s' % (name, k), v)
159 _rejectunicode('%s.%s' % (name, k), v)
160 elif isinstance(xs, str):
160 elif isinstance(xs, str):
161 raise error.ProgrammingError(
161 raise error.ProgrammingError(
162 b"unicode %r found in %s" % (xs, stringutil.forcebytestr(name)),
162 b"unicode %r found in %s" % (xs, stringutil.forcebytestr(name)),
163 hint=b"use b'' to make it byte string",
163 hint=b"use b'' to make it byte string",
164 )
164 )
165
165
166
166
167 # attributes set by registrar.command
167 # attributes set by registrar.command
168 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
168 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
169
169
170
170
171 def _validatecmdtable(ui, cmdtable):
171 def _validatecmdtable(ui, cmdtable):
172 """Check if extension commands have required attributes"""
172 """Check if extension commands have required attributes"""
173 for c, e in cmdtable.items():
173 for c, e in cmdtable.items():
174 f = e[0]
174 f = e[0]
175 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
175 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
176 if not missing:
176 if not missing:
177 continue
177 continue
178 msg = b'missing attributes: %s'
178 msg = b'missing attributes: %s'
179 msg %= b', '.join([stringutil.forcebytestr(m) for m in missing])
179 msg %= b', '.join([stringutil.forcebytestr(m) for m in missing])
180 hint = b"use @command decorator to register '%s'" % c
180 hint = b"use @command decorator to register '%s'" % c
181 raise error.ProgrammingError(msg, hint=hint)
181 raise error.ProgrammingError(msg, hint=hint)
182
182
183
183
184 def _validatetables(ui, mod):
184 def _validatetables(ui, mod):
185 """Sanity check for loadable tables provided by extension module"""
185 """Sanity check for loadable tables provided by extension module"""
186 for t in ['cmdtable', 'colortable', 'configtable']:
186 for t in ['cmdtable', 'colortable', 'configtable']:
187 _rejectunicode(t, getattr(mod, t, {}))
187 _rejectunicode(t, getattr(mod, t, {}))
188 for t in [
188 for t in [
189 'filesetpredicate',
189 'filesetpredicate',
190 'internalmerge',
190 'internalmerge',
191 'revsetpredicate',
191 'revsetpredicate',
192 'templatefilter',
192 'templatefilter',
193 'templatefunc',
193 'templatefunc',
194 'templatekeyword',
194 'templatekeyword',
195 ]:
195 ]:
196 o = getattr(mod, t, None)
196 o = getattr(mod, t, None)
197 if o:
197 if o:
198 _rejectunicode(t, o._table)
198 _rejectunicode(t, o._table)
199 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
199 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
200
200
201
201
202 def load(ui, name, path, loadingtime=None):
202 def load(ui, name, path, loadingtime=None):
203 if name.startswith(b'hgext.') or name.startswith(b'hgext/'):
203 if name.startswith(b'hgext.') or name.startswith(b'hgext/'):
204 shortname = name[6:]
204 shortname = name[6:]
205 else:
205 else:
206 shortname = name
206 shortname = name
207 if shortname in _builtin:
207 if shortname in _builtin:
208 return None
208 return None
209 if shortname in _extensions:
209 if shortname in _extensions:
210 return _extensions[shortname]
210 return _extensions[shortname]
211 ui.log(b'extension', b' - loading extension: %s\n', shortname)
211 ui.log(b'extension', b' - loading extension: %s\n', shortname)
212 _extensions[shortname] = None
212 _extensions[shortname] = None
213 with util.timedcm('load extension %s', shortname) as stats:
213 with util.timedcm('load extension %s', shortname) as stats:
214 mod = _importext(name, path, bind(_reportimporterror, ui))
214 mod = _importext(name, path, bind(_reportimporterror, ui))
215 ui.log(b'extension', b' > %s extension loaded in %s\n', shortname, stats)
215 ui.log(b'extension', b' > %s extension loaded in %s\n', shortname, stats)
216 if loadingtime is not None:
216 if loadingtime is not None:
217 loadingtime[shortname] += stats.elapsed
217 loadingtime[shortname] += stats.elapsed
218
218
219 # Before we do anything with the extension, check against minimum stated
219 # Before we do anything with the extension, check against minimum stated
220 # compatibility. This gives extension authors a mechanism to have their
220 # compatibility. This gives extension authors a mechanism to have their
221 # extensions short circuit when loaded with a known incompatible version
221 # extensions short circuit when loaded with a known incompatible version
222 # of Mercurial.
222 # of Mercurial.
223 minver = getattr(mod, 'minimumhgversion', None)
223 minver = getattr(mod, 'minimumhgversion', None)
224 if minver:
224 if minver:
225 curver = util.versiontuple(n=2)
225 curver = util.versiontuple(n=2)
226 extmin = util.versiontuple(stringutil.forcebytestr(minver), 2)
226 extmin = util.versiontuple(stringutil.forcebytestr(minver), 2)
227
227
228 if None in extmin:
228 if None in extmin:
229 extmin = (extmin[0] or 0, extmin[1] or 0)
229 extmin = (extmin[0] or 0, extmin[1] or 0)
230
230
231 if None in curver or extmin > curver:
231 if None in curver or extmin > curver:
232 msg = _(
232 msg = _(
233 b'(third party extension %s requires version %s or newer '
233 b'(third party extension %s requires version %s or newer '
234 b'of Mercurial (current: %s); disabling)\n'
234 b'of Mercurial (current: %s); disabling)\n'
235 )
235 )
236 ui.warn(msg % (shortname, minver, util.version()))
236 ui.warn(msg % (shortname, minver, util.version()))
237 return
237 return
238 ui.log(b'extension', b' - validating extension tables: %s\n', shortname)
238 ui.log(b'extension', b' - validating extension tables: %s\n', shortname)
239 _validatetables(ui, mod)
239 _validatetables(ui, mod)
240
240
241 _extensions[shortname] = mod
241 _extensions[shortname] = mod
242 _order.append(shortname)
242 _order.append(shortname)
243 ui.log(
243 ui.log(
244 b'extension', b' - invoking registered callbacks: %s\n', shortname
244 b'extension', b' - invoking registered callbacks: %s\n', shortname
245 )
245 )
246 with util.timedcm('callbacks extension %s', shortname) as stats:
246 with util.timedcm('callbacks extension %s', shortname) as stats:
247 for fn in _aftercallbacks.get(shortname, []):
247 for fn in _aftercallbacks.get(shortname, []):
248 fn(loaded=True)
248 fn(loaded=True)
249 ui.log(b'extension', b' > callbacks completed in %s\n', stats)
249 ui.log(b'extension', b' > callbacks completed in %s\n', stats)
250 return mod
250 return mod
251
251
252
252
253 def _runuisetup(name, ui):
253 def _runuisetup(name, ui):
254 uisetup = getattr(_extensions[name], 'uisetup', None)
254 uisetup = getattr(_extensions[name], 'uisetup', None)
255 if uisetup:
255 if uisetup:
256 try:
256 try:
257 uisetup(ui)
257 uisetup(ui)
258 except Exception as inst:
258 except Exception as inst:
259 ui.traceback(force=True)
259 ui.traceback(force=True)
260 msg = stringutil.forcebytestr(inst)
260 msg = stringutil.forcebytestr(inst)
261 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
261 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
262 return False
262 return False
263 return True
263 return True
264
264
265
265
266 def _runextsetup(name, ui):
266 def _runextsetup(name, ui):
267 extsetup = getattr(_extensions[name], 'extsetup', None)
267 extsetup = getattr(_extensions[name], 'extsetup', None)
268 if extsetup:
268 if extsetup:
269 try:
269 try:
270 extsetup(ui)
270 extsetup(ui)
271 except Exception as inst:
271 except Exception as inst:
272 ui.traceback(force=True)
272 ui.traceback(force=True)
273 msg = stringutil.forcebytestr(inst)
273 msg = stringutil.forcebytestr(inst)
274 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
274 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
275 return False
275 return False
276 return True
276 return True
277
277
278
278
279 def loadall(ui, whitelist=None):
279 def loadall(ui, whitelist=None):
280 loadingtime = collections.defaultdict(int)
280 loadingtime = collections.defaultdict(int)
281 result = ui.configitems(b"extensions")
281 result = ui.configitems(b"extensions")
282 if whitelist is not None:
282 if whitelist is not None:
283 result = [(k, v) for (k, v) in result if k in whitelist]
283 result = [(k, v) for (k, v) in result if k in whitelist]
284 result = [(k, v) for (k, v) in result if b':' not in k]
284 result = [(k, v) for (k, v) in result if b':' not in k]
285 newindex = len(_order)
285 newindex = len(_order)
286 ui.log(
286 ui.log(
287 b'extension',
287 b'extension',
288 b'loading %sextensions\n',
288 b'loading %sextensions\n',
289 b'additional ' if newindex else b'',
289 b'additional ' if newindex else b'',
290 )
290 )
291 ui.log(b'extension', b'- processing %d entries\n', len(result))
291 ui.log(b'extension', b'- processing %d entries\n', len(result))
292 with util.timedcm('load all extensions') as stats:
292 with util.timedcm('load all extensions') as stats:
293 default_sub_options = ui.configsuboptions(b"extensions", b"*")[1]
293 default_sub_options = ui.configsuboptions(b"extensions", b"*")[1]
294
294
295 for (name, path) in result:
295 for (name, path) in result:
296 if path:
296 if path:
297 if path[0:1] == b'!':
297 if path[0:1] == b'!':
298 if name not in _disabledextensions:
298 if name not in _disabledextensions:
299 ui.log(
299 ui.log(
300 b'extension',
300 b'extension',
301 b' - skipping disabled extension: %s\n',
301 b' - skipping disabled extension: %s\n',
302 name,
302 name,
303 )
303 )
304 _disabledextensions[name] = path[1:]
304 _disabledextensions[name] = path[1:]
305 continue
305 continue
306 try:
306 try:
307 load(ui, name, path, loadingtime)
307 load(ui, name, path, loadingtime)
308 except Exception as inst:
308 except Exception as inst:
309 msg = stringutil.forcebytestr(inst)
309 msg = stringutil.forcebytestr(inst)
310 if path:
310 if path:
311 error_msg = _(
311 error_msg = _(
312 b'failed to import extension "%s" from %s: %s'
312 b'failed to import extension "%s" from %s: %s'
313 )
313 )
314 error_msg %= (name, path, msg)
314 error_msg %= (name, path, msg)
315 else:
315 else:
316 error_msg = _(b'failed to import extension "%s": %s')
316 error_msg = _(b'failed to import extension "%s": %s')
317 error_msg %= (name, msg)
317 error_msg %= (name, msg)
318
318
319 options = default_sub_options.copy()
319 options = default_sub_options.copy()
320 ext_options = ui.configsuboptions(b"extensions", name)[1]
320 ext_options = ui.configsuboptions(b"extensions", name)[1]
321 options.update(ext_options)
321 options.update(ext_options)
322 if stringutil.parsebool(options.get(b"required", b'no')):
322 if stringutil.parsebool(options.get(b"required", b'no')):
323 hint = None
323 hint = None
324 if isinstance(inst, error.Hint) and inst.hint:
324 if isinstance(inst, error.Hint) and inst.hint:
325 hint = inst.hint
325 hint = inst.hint
326 if hint is None:
326 if hint is None:
327 hint = _(
327 hint = _(
328 b"loading of this extension was required, "
328 b"loading of this extension was required, "
329 b"see `hg help config.extensions` for details"
329 b"see `hg help config.extensions` for details"
330 )
330 )
331 raise error.Abort(error_msg, hint=hint)
331 raise error.Abort(error_msg, hint=hint)
332 else:
332 else:
333 ui.warn((b"*** %s\n") % error_msg)
333 ui.warn((b"*** %s\n") % error_msg)
334 if isinstance(inst, error.Hint) and inst.hint:
334 if isinstance(inst, error.Hint) and inst.hint:
335 ui.warn(_(b"*** (%s)\n") % inst.hint)
335 ui.warn(_(b"*** (%s)\n") % inst.hint)
336 ui.traceback()
336 ui.traceback()
337
337
338 ui.log(
338 ui.log(
339 b'extension',
339 b'extension',
340 b'> loaded %d extensions, total time %s\n',
340 b'> loaded %d extensions, total time %s\n',
341 len(_order) - newindex,
341 len(_order) - newindex,
342 stats,
342 stats,
343 )
343 )
344 # list of (objname, loadermod, loadername) tuple:
344 # list of (objname, loadermod, loadername) tuple:
345 # - objname is the name of an object in extension module,
345 # - objname is the name of an object in extension module,
346 # from which extra information is loaded
346 # from which extra information is loaded
347 # - loadermod is the module where loader is placed
347 # - loadermod is the module where loader is placed
348 # - loadername is the name of the function,
348 # - loadername is the name of the function,
349 # which takes (ui, extensionname, extraobj) arguments
349 # which takes (ui, extensionname, extraobj) arguments
350 #
350 #
351 # This one is for the list of item that must be run before running any setup
351 # This one is for the list of item that must be run before running any setup
352 earlyextraloaders = [
352 earlyextraloaders = [
353 ('configtable', configitems, 'loadconfigtable'),
353 ('configtable', configitems, 'loadconfigtable'),
354 ]
354 ]
355
355
356 ui.log(b'extension', b'- loading configtable attributes\n')
356 ui.log(b'extension', b'- loading configtable attributes\n')
357 _loadextra(ui, newindex, earlyextraloaders)
357 _loadextra(ui, newindex, earlyextraloaders)
358
358
359 broken = set()
359 broken = set()
360 ui.log(b'extension', b'- executing uisetup hooks\n')
360 ui.log(b'extension', b'- executing uisetup hooks\n')
361 with util.timedcm('all uisetup') as alluisetupstats:
361 with util.timedcm('all uisetup') as alluisetupstats:
362 for name in _order[newindex:]:
362 for name in _order[newindex:]:
363 ui.log(b'extension', b' - running uisetup for %s\n', name)
363 ui.log(b'extension', b' - running uisetup for %s\n', name)
364 with util.timedcm('uisetup %s', name) as stats:
364 with util.timedcm('uisetup %s', name) as stats:
365 if not _runuisetup(name, ui):
365 if not _runuisetup(name, ui):
366 ui.log(
366 ui.log(
367 b'extension',
367 b'extension',
368 b' - the %s extension uisetup failed\n',
368 b' - the %s extension uisetup failed\n',
369 name,
369 name,
370 )
370 )
371 broken.add(name)
371 broken.add(name)
372 ui.log(b'extension', b' > uisetup for %s took %s\n', name, stats)
372 ui.log(b'extension', b' > uisetup for %s took %s\n', name, stats)
373 loadingtime[name] += stats.elapsed
373 loadingtime[name] += stats.elapsed
374 ui.log(b'extension', b'> all uisetup took %s\n', alluisetupstats)
374 ui.log(b'extension', b'> all uisetup took %s\n', alluisetupstats)
375
375
376 ui.log(b'extension', b'- executing extsetup hooks\n')
376 ui.log(b'extension', b'- executing extsetup hooks\n')
377 with util.timedcm('all extsetup') as allextetupstats:
377 with util.timedcm('all extsetup') as allextetupstats:
378 for name in _order[newindex:]:
378 for name in _order[newindex:]:
379 if name in broken:
379 if name in broken:
380 continue
380 continue
381 ui.log(b'extension', b' - running extsetup for %s\n', name)
381 ui.log(b'extension', b' - running extsetup for %s\n', name)
382 with util.timedcm('extsetup %s', name) as stats:
382 with util.timedcm('extsetup %s', name) as stats:
383 if not _runextsetup(name, ui):
383 if not _runextsetup(name, ui):
384 ui.log(
384 ui.log(
385 b'extension',
385 b'extension',
386 b' - the %s extension extsetup failed\n',
386 b' - the %s extension extsetup failed\n',
387 name,
387 name,
388 )
388 )
389 broken.add(name)
389 broken.add(name)
390 ui.log(b'extension', b' > extsetup for %s took %s\n', name, stats)
390 ui.log(b'extension', b' > extsetup for %s took %s\n', name, stats)
391 loadingtime[name] += stats.elapsed
391 loadingtime[name] += stats.elapsed
392 ui.log(b'extension', b'> all extsetup took %s\n', allextetupstats)
392 ui.log(b'extension', b'> all extsetup took %s\n', allextetupstats)
393
393
394 for name in broken:
394 for name in broken:
395 ui.log(b'extension', b' - disabling broken %s extension\n', name)
395 ui.log(b'extension', b' - disabling broken %s extension\n', name)
396 _extensions[name] = None
396 _extensions[name] = None
397
397
398 # Call aftercallbacks that were never met.
398 # Call aftercallbacks that were never met.
399 ui.log(b'extension', b'- executing remaining aftercallbacks\n')
399 ui.log(b'extension', b'- executing remaining aftercallbacks\n')
400 with util.timedcm('aftercallbacks') as stats:
400 with util.timedcm('aftercallbacks') as stats:
401 for shortname in _aftercallbacks:
401 for shortname in _aftercallbacks:
402 if shortname in _extensions:
402 if shortname in _extensions:
403 continue
403 continue
404
404
405 for fn in _aftercallbacks[shortname]:
405 for fn in _aftercallbacks[shortname]:
406 ui.log(
406 ui.log(
407 b'extension',
407 b'extension',
408 b' - extension %s not loaded, notify callbacks\n',
408 b' - extension %s not loaded, notify callbacks\n',
409 shortname,
409 shortname,
410 )
410 )
411 fn(loaded=False)
411 fn(loaded=False)
412 ui.log(b'extension', b'> remaining aftercallbacks completed in %s\n', stats)
412 ui.log(b'extension', b'> remaining aftercallbacks completed in %s\n', stats)
413
413
414 # loadall() is called multiple times and lingering _aftercallbacks
414 # loadall() is called multiple times and lingering _aftercallbacks
415 # entries could result in double execution. See issue4646.
415 # entries could result in double execution. See issue4646.
416 _aftercallbacks.clear()
416 _aftercallbacks.clear()
417
417
418 # delay importing avoids cyclic dependency (especially commands)
418 # delay importing avoids cyclic dependency (especially commands)
419 from . import (
419 from . import (
420 color,
420 color,
421 commands,
421 commands,
422 filemerge,
422 filemerge,
423 fileset,
423 fileset,
424 revset,
424 revset,
425 templatefilters,
425 templatefilters,
426 templatefuncs,
426 templatefuncs,
427 templatekw,
427 templatekw,
428 )
428 )
429
429
430 # list of (objname, loadermod, loadername) tuple:
430 # list of (objname, loadermod, loadername) tuple:
431 # - objname is the name of an object in extension module,
431 # - objname is the name of an object in extension module,
432 # from which extra information is loaded
432 # from which extra information is loaded
433 # - loadermod is the module where loader is placed
433 # - loadermod is the module where loader is placed
434 # - loadername is the name of the function,
434 # - loadername is the name of the function,
435 # which takes (ui, extensionname, extraobj) arguments
435 # which takes (ui, extensionname, extraobj) arguments
436 ui.log(b'extension', b'- loading extension registration objects\n')
436 ui.log(b'extension', b'- loading extension registration objects\n')
437 extraloaders = [
437 extraloaders = [
438 ('cmdtable', commands, 'loadcmdtable'),
438 ('cmdtable', commands, 'loadcmdtable'),
439 ('colortable', color, 'loadcolortable'),
439 ('colortable', color, 'loadcolortable'),
440 ('filesetpredicate', fileset, 'loadpredicate'),
440 ('filesetpredicate', fileset, 'loadpredicate'),
441 ('internalmerge', filemerge, 'loadinternalmerge'),
441 ('internalmerge', filemerge, 'loadinternalmerge'),
442 ('revsetpredicate', revset, 'loadpredicate'),
442 ('revsetpredicate', revset, 'loadpredicate'),
443 ('templatefilter', templatefilters, 'loadfilter'),
443 ('templatefilter', templatefilters, 'loadfilter'),
444 ('templatefunc', templatefuncs, 'loadfunction'),
444 ('templatefunc', templatefuncs, 'loadfunction'),
445 ('templatekeyword', templatekw, 'loadkeyword'),
445 ('templatekeyword', templatekw, 'loadkeyword'),
446 ]
446 ]
447 with util.timedcm('load registration objects') as stats:
447 with util.timedcm('load registration objects') as stats:
448 _loadextra(ui, newindex, extraloaders)
448 _loadextra(ui, newindex, extraloaders)
449 ui.log(
449 ui.log(
450 b'extension',
450 b'extension',
451 b'> extension registration object loading took %s\n',
451 b'> extension registration object loading took %s\n',
452 stats,
452 stats,
453 )
453 )
454
454
455 # Report per extension loading time (except reposetup)
455 # Report per extension loading time (except reposetup)
456 for name in sorted(loadingtime):
456 for name in sorted(loadingtime):
457 ui.log(
457 ui.log(
458 b'extension',
458 b'extension',
459 b'> extension %s take a total of %s to load\n',
459 b'> extension %s take a total of %s to load\n',
460 name,
460 name,
461 util.timecount(loadingtime[name]),
461 util.timecount(loadingtime[name]),
462 )
462 )
463
463
464 ui.log(b'extension', b'extension loading complete\n')
464 ui.log(b'extension', b'extension loading complete\n')
465
465
466
466
467 def _loadextra(ui, newindex, extraloaders):
467 def _loadextra(ui, newindex, extraloaders):
468 for name in _order[newindex:]:
468 for name in _order[newindex:]:
469 module = _extensions[name]
469 module = _extensions[name]
470 if not module:
470 if not module:
471 continue # loading this module failed
471 continue # loading this module failed
472
472
473 for objname, loadermod, loadername in extraloaders:
473 for objname, loadermod, loadername in extraloaders:
474 extraobj = getattr(module, objname, None)
474 extraobj = getattr(module, objname, None)
475 if extraobj is not None:
475 if extraobj is not None:
476 getattr(loadermod, loadername)(ui, name, extraobj)
476 getattr(loadermod, loadername)(ui, name, extraobj)
477
477
478
478
479 def afterloaded(extension, callback):
479 def afterloaded(extension, callback):
480 """Run the specified function after a named extension is loaded.
480 """Run the specified function after a named extension is loaded.
481
481
482 If the named extension is already loaded, the callback will be called
482 If the named extension is already loaded, the callback will be called
483 immediately.
483 immediately.
484
484
485 If the named extension never loads, the callback will be called after
485 If the named extension never loads, the callback will be called after
486 all extensions have been loaded.
486 all extensions have been loaded.
487
487
488 The callback receives the named argument ``loaded``, which is a boolean
488 The callback receives the named argument ``loaded``, which is a boolean
489 indicating whether the dependent extension actually loaded.
489 indicating whether the dependent extension actually loaded.
490 """
490 """
491
491
492 if extension in _extensions:
492 if extension in _extensions:
493 # Report loaded as False if the extension is disabled
493 # Report loaded as False if the extension is disabled
494 loaded = _extensions[extension] is not None
494 loaded = _extensions[extension] is not None
495 callback(loaded=loaded)
495 callback(loaded=loaded)
496 else:
496 else:
497 _aftercallbacks.setdefault(extension, []).append(callback)
497 _aftercallbacks.setdefault(extension, []).append(callback)
498
498
499
499
500 def populateui(ui):
500 def populateui(ui):
501 """Run extension hooks on the given ui to populate additional members,
501 """Run extension hooks on the given ui to populate additional members,
502 extend the class dynamically, etc.
502 extend the class dynamically, etc.
503
503
504 This will be called after the configuration is loaded, and/or extensions
504 This will be called after the configuration is loaded, and/or extensions
505 are loaded. In general, it's once per ui instance, but in command-server
505 are loaded. In general, it's once per ui instance, but in command-server
506 and hgweb, this may be called more than once with the same ui.
506 and hgweb, this may be called more than once with the same ui.
507 """
507 """
508 for name, mod in extensions(ui):
508 for name, mod in extensions(ui):
509 hook = getattr(mod, 'uipopulate', None)
509 hook = getattr(mod, 'uipopulate', None)
510 if not hook:
510 if not hook:
511 continue
511 continue
512 try:
512 try:
513 hook(ui)
513 hook(ui)
514 except Exception as inst:
514 except Exception as inst:
515 ui.traceback(force=True)
515 ui.traceback(force=True)
516 ui.warn(
516 ui.warn(
517 _(b'*** failed to populate ui by extension %s: %s\n')
517 _(b'*** failed to populate ui by extension %s: %s\n')
518 % (name, stringutil.forcebytestr(inst))
518 % (name, stringutil.forcebytestr(inst))
519 )
519 )
520
520
521
521
522 def bind(func, *args):
522 def bind(func, *args):
523 """Partial function application
523 """Partial function application
524
524
525 Returns a new function that is the partial application of args and kwargs
525 Returns a new function that is the partial application of args and kwargs
526 to func. For example,
526 to func. For example,
527
527
528 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)"""
528 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)"""
529 assert callable(func)
529 assert callable(func)
530
530
531 def closure(*a, **kw):
531 def closure(*a, **kw):
532 return func(*(args + a), **kw)
532 return func(*(args + a), **kw)
533
533
534 return closure
534 return closure
535
535
536
536
537 def _updatewrapper(wrap, origfn, unboundwrapper):
537 def _updatewrapper(wrap, origfn, unboundwrapper):
538 '''Copy and add some useful attributes to wrapper'''
538 '''Copy and add some useful attributes to wrapper'''
539 try:
539 try:
540 wrap.__name__ = origfn.__name__
540 wrap.__name__ = origfn.__name__
541 except AttributeError:
541 except AttributeError:
542 pass
542 pass
543 wrap.__module__ = getattr(origfn, '__module__')
543 wrap.__module__ = getattr(origfn, '__module__')
544 wrap.__doc__ = getattr(origfn, '__doc__')
544 wrap.__doc__ = getattr(origfn, '__doc__')
545 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
545 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
546 wrap._origfunc = origfn
546 wrap._origfunc = origfn
547 wrap._unboundwrapper = unboundwrapper
547 wrap._unboundwrapper = unboundwrapper
548
548
549
549
550 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
550 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
551 '''Wrap the command named `command' in table
551 '''Wrap the command named `command' in table
552
552
553 Replace command in the command table with wrapper. The wrapped command will
553 Replace command in the command table with wrapper. The wrapped command will
554 be inserted into the command table specified by the table argument.
554 be inserted into the command table specified by the table argument.
555
555
556 The wrapper will be called like
556 The wrapper will be called like
557
557
558 wrapper(orig, *args, **kwargs)
558 wrapper(orig, *args, **kwargs)
559
559
560 where orig is the original (wrapped) function, and *args, **kwargs
560 where orig is the original (wrapped) function, and *args, **kwargs
561 are the arguments passed to it.
561 are the arguments passed to it.
562
562
563 Optionally append to the command synopsis and docstring, used for help.
563 Optionally append to the command synopsis and docstring, used for help.
564 For example, if your extension wraps the ``bookmarks`` command to add the
564 For example, if your extension wraps the ``bookmarks`` command to add the
565 flags ``--remote`` and ``--all`` you might call this function like so:
565 flags ``--remote`` and ``--all`` you might call this function like so:
566
566
567 synopsis = ' [-a] [--remote]'
567 synopsis = ' [-a] [--remote]'
568 docstring = """
568 docstring = """
569
569
570 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
570 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
571 flags to the bookmarks command. Either flag will show the remote bookmarks
571 flags to the bookmarks command. Either flag will show the remote bookmarks
572 known to the repository; ``--remote`` will also suppress the output of the
572 known to the repository; ``--remote`` will also suppress the output of the
573 local bookmarks.
573 local bookmarks.
574 """
574 """
575
575
576 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
576 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
577 synopsis, docstring)
577 synopsis, docstring)
578 '''
578 '''
579 assert callable(wrapper)
579 assert callable(wrapper)
580 aliases, entry = cmdutil.findcmd(command, table)
580 aliases, entry = cmdutil.findcmd(command, table)
581 for alias, e in table.items():
581 for alias, e in table.items():
582 if e is entry:
582 if e is entry:
583 key = alias
583 key = alias
584 break
584 break
585
585
586 origfn = entry[0]
586 origfn = entry[0]
587 wrap = functools.partial(
587 wrap = functools.partial(
588 util.checksignature(wrapper), util.checksignature(origfn)
588 util.checksignature(wrapper), util.checksignature(origfn)
589 )
589 )
590 _updatewrapper(wrap, origfn, wrapper)
590 _updatewrapper(wrap, origfn, wrapper)
591 if docstring is not None:
591 if docstring is not None:
592 wrap.__doc__ += docstring
592 wrap.__doc__ += docstring
593
593
594 newentry = list(entry)
594 newentry = list(entry)
595 newentry[0] = wrap
595 newentry[0] = wrap
596 if synopsis is not None:
596 if synopsis is not None:
597 newentry[2] += synopsis
597 newentry[2] += synopsis
598 table[key] = tuple(newentry)
598 table[key] = tuple(newentry)
599 return entry
599 return entry
600
600
601
601
602 def wrapfilecache(cls, propname, wrapper):
602 def wrapfilecache(cls, propname, wrapper):
603 """Wraps a filecache property.
603 """Wraps a filecache property.
604
604
605 These can't be wrapped using the normal wrapfunction.
605 These can't be wrapped using the normal wrapfunction.
606 """
606 """
607 propname = pycompat.sysstr(propname)
607 propname = pycompat.sysstr(propname)
608 assert callable(wrapper)
608 assert callable(wrapper)
609 for currcls in cls.__mro__:
609 for currcls in cls.__mro__:
610 if propname in currcls.__dict__:
610 if propname in currcls.__dict__:
611 origfn = currcls.__dict__[propname].func
611 origfn = currcls.__dict__[propname].func
612 assert callable(origfn)
612 assert callable(origfn)
613
613
614 def wrap(*args, **kwargs):
614 def wrap(*args, **kwargs):
615 return wrapper(origfn, *args, **kwargs)
615 return wrapper(origfn, *args, **kwargs)
616
616
617 currcls.__dict__[propname].func = wrap
617 currcls.__dict__[propname].func = wrap
618 break
618 break
619
619
620 if currcls is object:
620 if currcls is object:
621 raise AttributeError("type '%s' has no property '%s'" % (cls, propname))
621 raise AttributeError("type '%s' has no property '%s'" % (cls, propname))
622
622
623
623
624 class wrappedfunction:
624 class wrappedfunction:
625 '''context manager for temporarily wrapping a function'''
625 '''context manager for temporarily wrapping a function'''
626
626
627 def __init__(self, container, funcname, wrapper):
627 def __init__(self, container, funcname, wrapper):
628 assert callable(wrapper)
628 assert callable(wrapper)
629 if not isinstance(funcname, str):
629 if not isinstance(funcname, str):
630 msg = b"pass wrappedfunction target name as `str`, not `bytes`"
630 msg = b"pass wrappedfunction target name as `str`, not `bytes`"
631 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
631 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
632 funcname = pycompat.sysstr(funcname)
632 funcname = pycompat.sysstr(funcname)
633 self._container = container
633 self._container = container
634 self._funcname = funcname
634 self._funcname = funcname
635 self._wrapper = wrapper
635 self._wrapper = wrapper
636
636
637 def __enter__(self):
637 def __enter__(self):
638 wrapfunction(self._container, self._funcname, self._wrapper)
638 wrapfunction(self._container, self._funcname, self._wrapper)
639
639
640 def __exit__(self, exctype, excvalue, traceback):
640 def __exit__(self, exctype, excvalue, traceback):
641 unwrapfunction(self._container, self._funcname, self._wrapper)
641 unwrapfunction(self._container, self._funcname, self._wrapper)
642
642
643
643
644 def wrapfunction(container, funcname, wrapper):
644 def wrapfunction(container, funcname, wrapper):
645 """Wrap the function named funcname in container
645 """Wrap the function named funcname in container
646
646
647 Replace the funcname member in the given container with the specified
647 Replace the funcname member in the given container with the specified
648 wrapper. The container is typically a module, class, or instance.
648 wrapper. The container is typically a module, class, or instance.
649
649
650 The wrapper will be called like
650 The wrapper will be called like
651
651
652 wrapper(orig, *args, **kwargs)
652 wrapper(orig, *args, **kwargs)
653
653
654 where orig is the original (wrapped) function, and *args, **kwargs
654 where orig is the original (wrapped) function, and *args, **kwargs
655 are the arguments passed to it.
655 are the arguments passed to it.
656
656
657 Wrapping methods of the repository object is not recommended since
657 Wrapping methods of the repository object is not recommended since
658 it conflicts with extensions that extend the repository by
658 it conflicts with extensions that extend the repository by
659 subclassing. All extensions that need to extend methods of
659 subclassing. All extensions that need to extend methods of
660 localrepository should use this subclassing trick: namely,
660 localrepository should use this subclassing trick: namely,
661 reposetup() should look like
661 reposetup() should look like
662
662
663 def reposetup(ui, repo):
663 def reposetup(ui, repo):
664 class myrepo(repo.__class__):
664 class myrepo(repo.__class__):
665 def whatever(self, *args, **kwargs):
665 def whatever(self, *args, **kwargs):
666 [...extension stuff...]
666 [...extension stuff...]
667 super(myrepo, self).whatever(*args, **kwargs)
667 super(myrepo, self).whatever(*args, **kwargs)
668 [...extension stuff...]
668 [...extension stuff...]
669
669
670 repo.__class__ = myrepo
670 repo.__class__ = myrepo
671
671
672 In general, combining wrapfunction() with subclassing does not
672 In general, combining wrapfunction() with subclassing does not
673 work. Since you cannot control what other extensions are loaded by
673 work. Since you cannot control what other extensions are loaded by
674 your end users, you should play nicely with others by using the
674 your end users, you should play nicely with others by using the
675 subclass trick.
675 subclass trick.
676 """
676 """
677 assert callable(wrapper)
677 assert callable(wrapper)
678
678
679 if not isinstance(funcname, str):
679 if not isinstance(funcname, str):
680 msg = b"pass wrapfunction target name as `str`, not `bytes`"
680 msg = b"pass wrapfunction target name as `str`, not `bytes`"
681 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
681 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
682 funcname = pycompat.sysstr(funcname)
682 funcname = pycompat.sysstr(funcname)
683
683
684 origfn = getattr(container, funcname)
684 origfn = getattr(container, funcname)
685 assert callable(origfn)
685 assert callable(origfn)
686 if inspect.ismodule(container):
686 if inspect.ismodule(container):
687 # origfn is not an instance or class method. "partial" can be used.
687 # origfn is not an instance or class method. "partial" can be used.
688 # "partial" won't insert a frame in traceback.
688 # "partial" won't insert a frame in traceback.
689 wrap = functools.partial(wrapper, origfn)
689 wrap = functools.partial(wrapper, origfn)
690 else:
690 else:
691 # "partial" cannot be safely used. Emulate its effect by using "bind".
691 # "partial" cannot be safely used. Emulate its effect by using "bind".
692 # The downside is one more frame in traceback.
692 # The downside is one more frame in traceback.
693 wrap = bind(wrapper, origfn)
693 wrap = bind(wrapper, origfn)
694 _updatewrapper(wrap, origfn, wrapper)
694 _updatewrapper(wrap, origfn, wrapper)
695 setattr(container, funcname, wrap)
695 setattr(container, funcname, wrap)
696 return origfn
696 return origfn
697
697
698
698
699 def unwrapfunction(container, funcname, wrapper=None):
699 def unwrapfunction(container, funcname, wrapper=None):
700 """undo wrapfunction
700 """undo wrapfunction
701
701
702 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
702 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
703 from the chain of wrappers.
703 from the chain of wrappers.
704
704
705 Return the removed wrapper.
705 Return the removed wrapper.
706 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
706 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
707 wrapper is not None but is not found in the wrapper chain.
707 wrapper is not None but is not found in the wrapper chain.
708 """
708 """
709 chain = getwrapperchain(container, funcname)
709 chain = getwrapperchain(container, funcname)
710 origfn = chain.pop()
710 origfn = chain.pop()
711 if wrapper is None:
711 if wrapper is None:
712 wrapper = chain[0]
712 wrapper = chain[0]
713 chain.remove(wrapper)
713 chain.remove(wrapper)
714 setattr(container, funcname, origfn)
714 setattr(container, funcname, origfn)
715 for w in reversed(chain):
715 for w in reversed(chain):
716 wrapfunction(container, funcname, w)
716 wrapfunction(container, funcname, w)
717 return wrapper
717 return wrapper
718
718
719
719
720 def getwrapperchain(container, funcname):
720 def getwrapperchain(container, funcname):
721 """get a chain of wrappers of a function
721 """get a chain of wrappers of a function
722
722
723 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
723 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
724
724
725 The wrapper functions are the ones passed to wrapfunction, whose first
725 The wrapper functions are the ones passed to wrapfunction, whose first
726 argument is origfunc.
726 argument is origfunc.
727 """
727 """
728 result = []
728 result = []
729 fn = getattr(container, funcname)
729 fn = getattr(container, funcname)
730 while fn:
730 while fn:
731 assert callable(fn)
731 assert callable(fn)
732 result.append(getattr(fn, '_unboundwrapper', fn))
732 result.append(getattr(fn, '_unboundwrapper', fn))
733 fn = getattr(fn, '_origfunc', None)
733 fn = getattr(fn, '_origfunc', None)
734 return result
734 return result
735
735
736
736
737 def _disabledpaths():
737 def _disabledpaths():
738 '''find paths of disabled extensions. returns a dict of {name: path}'''
738 '''find paths of disabled extensions. returns a dict of {name: path}'''
739 import hgext
739 import hgext
740
740
741 exts = {}
741 exts = {}
742
742
743 # The hgext might not have a __file__ attribute (e.g. in PyOxidizer) and
743 # The hgext might not have a __file__ attribute (e.g. in PyOxidizer) and
744 # it might not be on a filesystem even if it does.
744 # it might not be on a filesystem even if it does.
745 if util.safehasattr(hgext, '__file__'):
745 if util.safehasattr(hgext, '__file__'):
746 extpath = os.path.dirname(
746 extpath = os.path.dirname(
747 util.abspath(pycompat.fsencode(hgext.__file__))
747 util.abspath(pycompat.fsencode(hgext.__file__))
748 )
748 )
749 try:
749 try:
750 files = os.listdir(extpath)
750 files = os.listdir(extpath)
751 except OSError:
751 except OSError:
752 pass
752 pass
753 else:
753 else:
754 for e in files:
754 for e in files:
755 if e.endswith(b'.py'):
755 if e.endswith(b'.py'):
756 name = e.rsplit(b'.', 1)[0]
756 name = e.rsplit(b'.', 1)[0]
757 path = os.path.join(extpath, e)
757 path = os.path.join(extpath, e)
758 else:
758 else:
759 name = e
759 name = e
760 path = os.path.join(extpath, e, b'__init__.py')
760 path = os.path.join(extpath, e, b'__init__.py')
761 if not os.path.exists(path):
761 if not os.path.exists(path):
762 continue
762 continue
763 if name in exts or name in _order or name == b'__init__':
763 if name in exts or name in _order or name == b'__init__':
764 continue
764 continue
765 exts[name] = path
765 exts[name] = path
766
766
767 for name, path in _disabledextensions.items():
767 for name, path in _disabledextensions.items():
768 # If no path was provided for a disabled extension (e.g. "color=!"),
768 # If no path was provided for a disabled extension (e.g. "color=!"),
769 # don't replace the path we already found by the scan above.
769 # don't replace the path we already found by the scan above.
770 if path:
770 if path:
771 exts[name] = path
771 exts[name] = path
772 return exts
772 return exts
773
773
774
774
775 def _moduledoc(file):
775 def _moduledoc(file):
776 """return the top-level python documentation for the given file
776 """return the top-level python documentation for the given file
777
777
778 Loosely inspired by pydoc.source_synopsis(), but rewritten to
778 Loosely inspired by pydoc.source_synopsis(), but rewritten to
779 handle triple quotes and to return the whole text instead of just
779 handle triple quotes and to return the whole text instead of just
780 the synopsis"""
780 the synopsis"""
781 result = []
781 result = []
782
782
783 line = file.readline()
783 line = file.readline()
784 while line[:1] == b'#' or not line.strip():
784 while line[:1] == b'#' or not line.strip():
785 line = file.readline()
785 line = file.readline()
786 if not line:
786 if not line:
787 break
787 break
788
788
789 start = line[:3]
789 start = line[:3]
790 if start == b'"""' or start == b"'''":
790 if start == b'"""' or start == b"'''":
791 line = line[3:]
791 line = line[3:]
792 while line:
792 while line:
793 if line.rstrip().endswith(start):
793 if line.rstrip().endswith(start):
794 line = line.split(start)[0]
794 line = line.split(start)[0]
795 if line:
795 if line:
796 result.append(line)
796 result.append(line)
797 break
797 break
798 elif not line:
798 elif not line:
799 return None # unmatched delimiter
799 return None # unmatched delimiter
800 result.append(line)
800 result.append(line)
801 line = file.readline()
801 line = file.readline()
802 else:
802 else:
803 return None
803 return None
804
804
805 return b''.join(result)
805 return b''.join(result)
806
806
807
807
808 def _disabledhelp(path):
808 def _disabledhelp(path):
809 '''retrieve help synopsis of a disabled extension (without importing)'''
809 '''retrieve help synopsis of a disabled extension (without importing)'''
810 try:
810 try:
811 with open(path, b'rb') as src:
811 with open(path, b'rb') as src:
812 doc = _moduledoc(src)
812 doc = _moduledoc(src)
813 except IOError:
813 except IOError:
814 return
814 return
815
815
816 if doc: # extracting localized synopsis
816 if doc: # extracting localized synopsis
817 return gettext(doc)
817 return gettext(doc)
818 else:
818 else:
819 return _(b'(no help text available)')
819 return _(b'(no help text available)')
820
820
821
821
822 def disabled():
822 def disabled():
823 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
823 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
824 try:
824 try:
825 from hgext import __index__ # pytype: disable=import-error
825 from hgext import __index__ # pytype: disable=import-error
826
826
827 return {
827 return {
828 name: gettext(desc)
828 name: gettext(desc)
829 for name, desc in __index__.docs.items()
829 for name, desc in __index__.docs.items()
830 if name not in _order
830 if name not in _order
831 }
831 }
832 except (ImportError, AttributeError):
832 except (ImportError, AttributeError):
833 pass
833 pass
834
834
835 paths = _disabledpaths()
835 paths = _disabledpaths()
836 if not paths:
836 if not paths:
837 return {}
837 return {}
838
838
839 exts = {}
839 exts = {}
840 for name, path in paths.items():
840 for name, path in paths.items():
841 doc = _disabledhelp(path)
841 doc = _disabledhelp(path)
842 if doc and name != b'__index__':
842 if doc and name != b'__index__':
843 exts[name] = stringutil.firstline(doc)
843 exts[name] = stringutil.firstline(doc)
844
844
845 return exts
845 return exts
846
846
847
847
848 def disabled_help(name):
848 def disabled_help(name):
849 """Obtain the full help text for a disabled extension, or None."""
849 """Obtain the full help text for a disabled extension, or None."""
850 paths = _disabledpaths()
850 paths = _disabledpaths()
851 if name in paths:
851 if name in paths:
852 return _disabledhelp(paths[name])
852 return _disabledhelp(paths[name])
853 else:
853 else:
854 try:
854 try:
855 import hgext
855 import hgext
856 from hgext import __index__ # pytype: disable=import-error
856 from hgext import __index__ # pytype: disable=import-error
857
857
858 # The extensions are filesystem based, so either an error occurred
858 # The extensions are filesystem based, so either an error occurred
859 # or all are enabled.
859 # or all are enabled.
860 if util.safehasattr(hgext, '__file__'):
860 if util.safehasattr(hgext, '__file__'):
861 return
861 return
862
862
863 if name in _order: # enabled
863 if name in _order: # enabled
864 return
864 return
865 else:
865 else:
866 return gettext(__index__.docs.get(name))
866 return gettext(__index__.docs.get(name))
867 except (ImportError, AttributeError):
867 except (ImportError, AttributeError):
868 pass
868 pass
869
869
870
870
871 def _walkcommand(node):
871 def _walkcommand(node):
872 """Scan @command() decorators in the tree starting at node"""
872 """Scan @command() decorators in the tree starting at node"""
873 todo = collections.deque([node])
873 todo = collections.deque([node])
874 while todo:
874 while todo:
875 node = todo.popleft()
875 node = todo.popleft()
876 if not isinstance(node, ast.FunctionDef):
876 if not isinstance(node, ast.FunctionDef):
877 todo.extend(ast.iter_child_nodes(node))
877 todo.extend(ast.iter_child_nodes(node))
878 continue
878 continue
879 for d in node.decorator_list:
879 for d in node.decorator_list:
880 if not isinstance(d, ast.Call):
880 if not isinstance(d, ast.Call):
881 continue
881 continue
882 if not isinstance(d.func, ast.Name):
882 if not isinstance(d.func, ast.Name):
883 continue
883 continue
884 if d.func.id != 'command':
884 if d.func.id != 'command':
885 continue
885 continue
886 yield d
886 yield d
887
887
888
888
889 def _disabledcmdtable(path):
889 def _disabledcmdtable(path):
890 """Construct a dummy command table without loading the extension module
890 """Construct a dummy command table without loading the extension module
891
891
892 This may raise IOError or SyntaxError.
892 This may raise IOError or SyntaxError.
893 """
893 """
894 with open(path, b'rb') as src:
894 with open(path, b'rb') as src:
895 root = ast.parse(src.read(), path)
895 root = ast.parse(src.read(), path)
896 cmdtable = {}
896 cmdtable = {}
897
897
898 # Python 3.12 started removing Bytes and Str and deprecate harder
898 # Python 3.12 started removing Bytes and Str and deprecate harder
899 use_constant = 'Bytes' not in vars(ast)
899 use_constant = 'Bytes' not in vars(ast)
900
900
901 for node in _walkcommand(root):
901 for node in _walkcommand(root):
902 if not node.args:
902 if not node.args:
903 continue
903 continue
904 a = node.args[0]
904 a = node.args[0]
905 if use_constant: # Valid since Python 3.8
905 if use_constant: # Valid since Python 3.8
906 if isinstance(a, ast.Constant):
906 if isinstance(a, ast.Constant):
907 if isinstance(a.value, str):
907 if isinstance(a.value, str):
908 name = pycompat.sysbytes(a.value)
908 name = pycompat.sysbytes(a.value)
909 elif isinstance(a.value, bytes):
909 elif isinstance(a.value, bytes):
910 name = a.value
910 name = a.value
911 else:
911 else:
912 continue
912 continue
913 else:
913 else:
914 continue
914 continue
915 else: # Valid until 3.11
915 else: # Valid until 3.11
916 if isinstance(a, ast.Str):
916 if isinstance(a, ast.Str):
917 name = pycompat.sysbytes(a.s)
917 name = pycompat.sysbytes(a.s)
918 elif isinstance(a, ast.Bytes):
918 elif isinstance(a, ast.Bytes):
919 name = a.s
919 name = a.s
920 else:
920 else:
921 continue
921 continue
922 cmdtable[name] = (None, [], b'')
922 cmdtable[name] = (None, [], b'')
923 return cmdtable
923 return cmdtable
924
924
925
925
926 def _finddisabledcmd(ui, cmd, name, path, strict):
926 def _finddisabledcmd(ui, cmd, name, path, strict):
927 try:
927 try:
928 cmdtable = _disabledcmdtable(path)
928 cmdtable = _disabledcmdtable(path)
929 except (IOError, SyntaxError):
929 except (IOError, SyntaxError):
930 return
930 return
931 try:
931 try:
932 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
932 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
933 except (error.AmbiguousCommand, error.UnknownCommand):
933 except (error.AmbiguousCommand, error.UnknownCommand):
934 return
934 return
935 for c in aliases:
935 for c in aliases:
936 if c.startswith(cmd):
936 if c.startswith(cmd):
937 cmd = c
937 cmd = c
938 break
938 break
939 else:
939 else:
940 cmd = aliases[0]
940 cmd = aliases[0]
941 doc = _disabledhelp(path)
941 doc = _disabledhelp(path)
942 return (cmd, name, doc)
942 return (cmd, name, doc)
943
943
944
944
945 def disabledcmd(ui, cmd, strict=False):
945 def disabledcmd(ui, cmd, strict=False):
946 """find cmd from disabled extensions without importing.
946 """find cmd from disabled extensions without importing.
947 returns (cmdname, extname, doc)"""
947 returns (cmdname, extname, doc)"""
948
948
949 paths = _disabledpaths()
949 paths = _disabledpaths()
950 if not paths:
950 if not paths:
951 raise error.UnknownCommand(cmd)
951 raise error.UnknownCommand(cmd)
952
952
953 ext = None
953 ext = None
954 # first, search for an extension with the same name as the command
954 # first, search for an extension with the same name as the command
955 path = paths.pop(cmd, None)
955 path = paths.pop(cmd, None)
956 if path:
956 if path:
957 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
957 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
958 if not ext:
958 if not ext:
959 # otherwise, interrogate each extension until there's a match
959 # otherwise, interrogate each extension until there's a match
960 for name, path in paths.items():
960 for name, path in paths.items():
961 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
961 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
962 if ext:
962 if ext:
963 break
963 break
964 if ext:
964 if ext:
965 return ext
965 return ext
966
966
967 raise error.UnknownCommand(cmd)
967 raise error.UnknownCommand(cmd)
968
968
969
969
970 def enabled(shortname=True):
970 def enabled(shortname=True):
971 '''return a dict of {name: desc} of extensions'''
971 '''return a dict of {name: desc} of extensions'''
972 exts = {}
972 exts = {}
973 for ename, ext in extensions():
973 for ename, ext in extensions():
974 doc = gettext(ext.__doc__) or _(b'(no help text available)')
974 doc = gettext(ext.__doc__) or _(b'(no help text available)')
975 assert doc is not None # help pytype
975 assert doc is not None # help pytype
976 if shortname:
976 if shortname:
977 ename = ename.split(b'.')[-1]
977 ename = ename.split(b'.')[-1]
978 exts[ename] = stringutil.firstline(doc).strip()
978 exts[ename] = stringutil.firstline(doc).strip()
979
979
980 return exts
980 return exts
981
981
982
982
983 def notloaded():
983 def notloaded():
984 '''return short names of extensions that failed to load'''
984 '''return short names of extensions that failed to load'''
985 return [name for name, mod in _extensions.items() if mod is None]
985 return [name for name, mod in _extensions.items() if mod is None]
986
986
987
987
988 def moduleversion(module):
988 def moduleversion(module):
989 '''return version information from given module as a string'''
989 '''return version information from given module as a string'''
990 if util.safehasattr(module, 'getversion') and callable(module.getversion):
990 if util.safehasattr(module, 'getversion') and callable(module.getversion):
991 try:
991 try:
992 version = module.getversion()
992 version = module.getversion()
993 except Exception:
993 except Exception:
994 version = b'unknown'
994 version = b'unknown'
995
995
996 elif util.safehasattr(module, '__version__'):
996 elif util.safehasattr(module, '__version__'):
997 version = module.__version__
997 version = module.__version__
998 else:
998 else:
999 version = b''
999 version = b''
1000 if isinstance(version, (list, tuple)):
1000 if isinstance(version, (list, tuple)):
1001 version = b'.'.join(pycompat.bytestr(o) for o in version)
1001 version = b'.'.join(pycompat.bytestr(o) for o in version)
1002 else:
1002 else:
1003 # version data should be bytes, but not all extensions are ported
1003 # version data should be bytes, but not all extensions are ported
1004 # to py3.
1004 # to py3.
1005 version = stringutil.forcebytestr(version)
1005 version = stringutil.forcebytestr(version)
1006 return version
1006 return version
1007
1007
1008
1008
1009 def ismoduleinternal(module):
1009 def ismoduleinternal(module):
1010 exttestedwith = getattr(module, 'testedwith', None)
1010 exttestedwith = getattr(module, 'testedwith', None)
1011 return exttestedwith == b"ships-with-hg-core"
1011 return exttestedwith == b"ships-with-hg-core"
@@ -1,1322 +1,1323 b''
1 # filemerge.py - file-level merge handling for Mercurial
1 # filemerge.py - file-level merge handling for Mercurial
2 #
2 #
3 # Copyright 2006, 2007, 2008 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2006, 2007, 2008 Olivia Mackall <olivia@selenic.com>
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
8
9 import contextlib
9 import contextlib
10 import os
10 import os
11 import re
11 import re
12 import shutil
12 import shutil
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 hex,
16 hex,
17 short,
17 short,
18 )
18 )
19 from .pycompat import (
19 from .pycompat import (
20 getattr,
20 getattr,
21 )
21 )
22
22
23 from . import (
23 from . import (
24 encoding,
24 encoding,
25 error,
25 error,
26 formatter,
26 formatter,
27 match,
27 match,
28 pycompat,
28 pycompat,
29 registrar,
29 registrar,
30 scmutil,
30 scmutil,
31 simplemerge,
31 simplemerge,
32 tagmerge,
32 tagmerge,
33 templatekw,
33 templatekw,
34 templater,
34 templater,
35 templateutil,
35 templateutil,
36 util,
36 util,
37 )
37 )
38
38
39 from .utils import (
39 from .utils import (
40 procutil,
40 procutil,
41 stringutil,
41 stringutil,
42 )
42 )
43
43
44
44
45 def _toolstr(ui, tool, part, *args):
45 def _toolstr(ui, tool, part, *args):
46 return ui.config(b"merge-tools", tool + b"." + part, *args)
46 return ui.config(b"merge-tools", tool + b"." + part, *args)
47
47
48
48
49 def _toolbool(ui, tool, part, *args):
49 def _toolbool(ui, tool, part, *args):
50 return ui.configbool(b"merge-tools", tool + b"." + part, *args)
50 return ui.configbool(b"merge-tools", tool + b"." + part, *args)
51
51
52
52
53 def _toollist(ui, tool, part):
53 def _toollist(ui, tool, part):
54 return ui.configlist(b"merge-tools", tool + b"." + part)
54 return ui.configlist(b"merge-tools", tool + b"." + part)
55
55
56
56
57 internals = {}
57 internals = {}
58 # Merge tools to document.
58 # Merge tools to document.
59 internalsdoc = {}
59 internalsdoc = {}
60
60
61 internaltool = registrar.internalmerge()
61 internaltool = registrar.internalmerge()
62
62
63 # internal tool merge types
63 # internal tool merge types
64 nomerge = internaltool.nomerge
64 nomerge = internaltool.nomerge
65 mergeonly = internaltool.mergeonly # just the full merge, no premerge
65 mergeonly = internaltool.mergeonly # just the full merge, no premerge
66 fullmerge = internaltool.fullmerge # both premerge and merge
66 fullmerge = internaltool.fullmerge # both premerge and merge
67
67
68 # IMPORTANT: keep the last line of this prompt very short ("What do you want to
68 # IMPORTANT: keep the last line of this prompt very short ("What do you want to
69 # do?") because of issue6158, ideally to <40 English characters (to allow other
69 # do?") because of issue6158, ideally to <40 English characters (to allow other
70 # languages that may take more columns to still have a chance to fit in an
70 # languages that may take more columns to still have a chance to fit in an
71 # 80-column screen).
71 # 80-column screen).
72 _localchangedotherdeletedmsg = _(
72 _localchangedotherdeletedmsg = _(
73 b"file '%(fd)s' was deleted in other%(o)s but was modified in local%(l)s.\n"
73 b"file '%(fd)s' was deleted in other%(o)s but was modified in local%(l)s.\n"
74 b"You can use (c)hanged version, (d)elete, or leave (u)nresolved.\n"
74 b"You can use (c)hanged version, (d)elete, or leave (u)nresolved.\n"
75 b"What do you want to do?"
75 b"What do you want to do?"
76 b"$$ &Changed $$ &Delete $$ &Unresolved"
76 b"$$ &Changed $$ &Delete $$ &Unresolved"
77 )
77 )
78
78
79 _otherchangedlocaldeletedmsg = _(
79 _otherchangedlocaldeletedmsg = _(
80 b"file '%(fd)s' was deleted in local%(l)s but was modified in other%(o)s.\n"
80 b"file '%(fd)s' was deleted in local%(l)s but was modified in other%(o)s.\n"
81 b"You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.\n"
81 b"You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.\n"
82 b"What do you want to do?"
82 b"What do you want to do?"
83 b"$$ &Changed $$ &Deleted $$ &Unresolved"
83 b"$$ &Changed $$ &Deleted $$ &Unresolved"
84 )
84 )
85
85
86
86
87 class absentfilectx:
87 class absentfilectx:
88 """Represents a file that's ostensibly in a context but is actually not
88 """Represents a file that's ostensibly in a context but is actually not
89 present in it.
89 present in it.
90
90
91 This is here because it's very specific to the filemerge code for now --
91 This is here because it's very specific to the filemerge code for now --
92 other code is likely going to break with the values this returns."""
92 other code is likely going to break with the values this returns."""
93
93
94 def __init__(self, ctx, f):
94 def __init__(self, ctx, f):
95 self._ctx = ctx
95 self._ctx = ctx
96 self._f = f
96 self._f = f
97
97
98 def __bytes__(self):
98 def __bytes__(self):
99 return b'absent file %s@%s' % (self._f, self._ctx)
99 return b'absent file %s@%s' % (self._f, self._ctx)
100
100
101 def path(self):
101 def path(self):
102 return self._f
102 return self._f
103
103
104 def size(self):
104 def size(self):
105 return None
105 return None
106
106
107 def data(self):
107 def data(self):
108 return None
108 return None
109
109
110 def filenode(self):
110 def filenode(self):
111 return self._ctx.repo().nullid
111 return self._ctx.repo().nullid
112
112
113 _customcmp = True
113 _customcmp = True
114
114
115 def cmp(self, fctx):
115 def cmp(self, fctx):
116 """compare with other file context
116 """compare with other file context
117
117
118 returns True if different from fctx.
118 returns True if different from fctx.
119 """
119 """
120 return not (
120 return not (
121 fctx.isabsent()
121 fctx.isabsent()
122 and fctx.changectx() == self.changectx()
122 and fctx.changectx() == self.changectx()
123 and fctx.path() == self.path()
123 and fctx.path() == self.path()
124 )
124 )
125
125
126 def flags(self):
126 def flags(self):
127 return b''
127 return b''
128
128
129 def changectx(self):
129 def changectx(self):
130 return self._ctx
130 return self._ctx
131
131
132 def isbinary(self):
132 def isbinary(self):
133 return False
133 return False
134
134
135 def isabsent(self):
135 def isabsent(self):
136 return True
136 return True
137
137
138
138
139 def _findtool(ui, tool):
139 def _findtool(ui, tool):
140 if tool in internals:
140 if tool in internals:
141 return tool
141 return tool
142 cmd = _toolstr(ui, tool, b"executable", tool)
142 cmd = _toolstr(ui, tool, b"executable", tool)
143 if cmd.startswith(b'python:'):
143 if cmd.startswith(b'python:'):
144 return cmd
144 return cmd
145 return findexternaltool(ui, tool)
145 return findexternaltool(ui, tool)
146
146
147
147
148 def _quotetoolpath(cmd):
148 def _quotetoolpath(cmd):
149 if cmd.startswith(b'python:'):
149 if cmd.startswith(b'python:'):
150 return cmd
150 return cmd
151 return procutil.shellquote(cmd)
151 return procutil.shellquote(cmd)
152
152
153
153
154 def findexternaltool(ui, tool):
154 def findexternaltool(ui, tool):
155 for kn in (b"regkey", b"regkeyalt"):
155 for kn in (b"regkey", b"regkeyalt"):
156 k = _toolstr(ui, tool, kn)
156 k = _toolstr(ui, tool, kn)
157 if not k:
157 if not k:
158 continue
158 continue
159 p = util.lookupreg(k, _toolstr(ui, tool, b"regname"))
159 p = util.lookupreg(k, _toolstr(ui, tool, b"regname"))
160 if p:
160 if p:
161 p = procutil.findexe(p + _toolstr(ui, tool, b"regappend"))
161 p = procutil.findexe(p + _toolstr(ui, tool, b"regappend"))
162 if p:
162 if p:
163 return p
163 return p
164 exe = _toolstr(ui, tool, b"executable", tool)
164 exe = _toolstr(ui, tool, b"executable", tool)
165 return procutil.findexe(util.expandpath(exe))
165 return procutil.findexe(util.expandpath(exe))
166
166
167
167
168 def _picktool(repo, ui, path, binary, symlink, changedelete):
168 def _picktool(repo, ui, path, binary, symlink, changedelete):
169 strictcheck = ui.configbool(b'merge', b'strict-capability-check')
169 strictcheck = ui.configbool(b'merge', b'strict-capability-check')
170
170
171 def hascapability(tool, capability, strict=False):
171 def hascapability(tool, capability, strict=False):
172 if tool in internals:
172 if tool in internals:
173 return strict and internals[tool].capabilities.get(capability)
173 return strict and internals[tool].capabilities.get(capability)
174 return _toolbool(ui, tool, capability)
174 return _toolbool(ui, tool, capability)
175
175
176 def supportscd(tool):
176 def supportscd(tool):
177 return tool in internals and internals[tool].mergetype == nomerge
177 return tool in internals and internals[tool].mergetype == nomerge
178
178
179 def check(tool, pat, symlink, binary, changedelete):
179 def check(tool, pat, symlink, binary, changedelete):
180 tmsg = tool
180 tmsg = tool
181 if pat:
181 if pat:
182 tmsg = _(b"%s (for pattern %s)") % (tool, pat)
182 tmsg = _(b"%s (for pattern %s)") % (tool, pat)
183 if not _findtool(ui, tool):
183 if not _findtool(ui, tool):
184 if pat: # explicitly requested tool deserves a warning
184 if pat: # explicitly requested tool deserves a warning
185 ui.warn(_(b"couldn't find merge tool %s\n") % tmsg)
185 ui.warn(_(b"couldn't find merge tool %s\n") % tmsg)
186 else: # configured but non-existing tools are more silent
186 else: # configured but non-existing tools are more silent
187 ui.note(_(b"couldn't find merge tool %s\n") % tmsg)
187 ui.note(_(b"couldn't find merge tool %s\n") % tmsg)
188 elif symlink and not hascapability(tool, b"symlink", strictcheck):
188 elif symlink and not hascapability(tool, b"symlink", strictcheck):
189 ui.warn(_(b"tool %s can't handle symlinks\n") % tmsg)
189 ui.warn(_(b"tool %s can't handle symlinks\n") % tmsg)
190 elif binary and not hascapability(tool, b"binary", strictcheck):
190 elif binary and not hascapability(tool, b"binary", strictcheck):
191 ui.warn(_(b"tool %s can't handle binary\n") % tmsg)
191 ui.warn(_(b"tool %s can't handle binary\n") % tmsg)
192 elif changedelete and not supportscd(tool):
192 elif changedelete and not supportscd(tool):
193 # the nomerge tools are the only tools that support change/delete
193 # the nomerge tools are the only tools that support change/delete
194 # conflicts
194 # conflicts
195 pass
195 pass
196 elif not procutil.gui() and _toolbool(ui, tool, b"gui"):
196 elif not procutil.gui() and _toolbool(ui, tool, b"gui"):
197 ui.warn(_(b"tool %s requires a GUI\n") % tmsg)
197 ui.warn(_(b"tool %s requires a GUI\n") % tmsg)
198 else:
198 else:
199 return True
199 return True
200 return False
200 return False
201
201
202 # internal config: ui.forcemerge
202 # internal config: ui.forcemerge
203 # forcemerge comes from command line arguments, highest priority
203 # forcemerge comes from command line arguments, highest priority
204 force = ui.config(b'ui', b'forcemerge')
204 force = ui.config(b'ui', b'forcemerge')
205 if force:
205 if force:
206 toolpath = _findtool(ui, force)
206 toolpath = _findtool(ui, force)
207 if changedelete and not supportscd(toolpath):
207 if changedelete and not supportscd(toolpath):
208 return b":prompt", None
208 return b":prompt", None
209 else:
209 else:
210 if toolpath:
210 if toolpath:
211 return (force, _quotetoolpath(toolpath))
211 return (force, _quotetoolpath(toolpath))
212 else:
212 else:
213 # mimic HGMERGE if given tool not found
213 # mimic HGMERGE if given tool not found
214 return (force, force)
214 return (force, force)
215
215
216 # HGMERGE takes next precedence
216 # HGMERGE takes next precedence
217 hgmerge = encoding.environ.get(b"HGMERGE")
217 hgmerge = encoding.environ.get(b"HGMERGE")
218 if hgmerge:
218 if hgmerge:
219 if changedelete and not supportscd(hgmerge):
219 if changedelete and not supportscd(hgmerge):
220 return b":prompt", None
220 return b":prompt", None
221 else:
221 else:
222 return (hgmerge, hgmerge)
222 return (hgmerge, hgmerge)
223
223
224 # then patterns
224 # then patterns
225
225
226 # whether binary capability should be checked strictly
226 # whether binary capability should be checked strictly
227 binarycap = binary and strictcheck
227 binarycap = binary and strictcheck
228
228
229 for pat, tool in ui.configitems(b"merge-patterns"):
229 for pat, tool in ui.configitems(b"merge-patterns"):
230 mf = match.match(repo.root, b'', [pat])
230 mf = match.match(repo.root, b'', [pat])
231 if mf(path) and check(tool, pat, symlink, binarycap, changedelete):
231 if mf(path) and check(tool, pat, symlink, binarycap, changedelete):
232 if binary and not hascapability(tool, b"binary", strict=True):
232 if binary and not hascapability(tool, b"binary", strict=True):
233 ui.warn(
233 ui.warn(
234 _(
234 _(
235 b"warning: check merge-patterns configurations,"
235 b"warning: check merge-patterns configurations,"
236 b" if %r for binary file %r is unintentional\n"
236 b" if %r for binary file %r is unintentional\n"
237 b"(see 'hg help merge-tools'"
237 b"(see 'hg help merge-tools'"
238 b" for binary files capability)\n"
238 b" for binary files capability)\n"
239 )
239 )
240 % (pycompat.bytestr(tool), pycompat.bytestr(path))
240 % (pycompat.bytestr(tool), pycompat.bytestr(path))
241 )
241 )
242 toolpath = _findtool(ui, tool)
242 toolpath = _findtool(ui, tool)
243 return (tool, _quotetoolpath(toolpath))
243 return (tool, _quotetoolpath(toolpath))
244
244
245 # then merge tools
245 # then merge tools
246 tools = {}
246 tools = {}
247 disabled = set()
247 disabled = set()
248 for k, v in ui.configitems(b"merge-tools"):
248 for k, v in ui.configitems(b"merge-tools"):
249 t = k.split(b'.')[0]
249 t = k.split(b'.')[0]
250 if t not in tools:
250 if t not in tools:
251 tools[t] = int(_toolstr(ui, t, b"priority"))
251 tools[t] = int(_toolstr(ui, t, b"priority"))
252 if _toolbool(ui, t, b"disabled"):
252 if _toolbool(ui, t, b"disabled"):
253 disabled.add(t)
253 disabled.add(t)
254 names = tools.keys()
254 names = tools.keys()
255 tools = sorted(
255 tools = sorted(
256 [(-p, tool) for tool, p in tools.items() if tool not in disabled]
256 [(-p, tool) for tool, p in tools.items() if tool not in disabled]
257 )
257 )
258 uimerge = ui.config(b"ui", b"merge")
258 uimerge = ui.config(b"ui", b"merge")
259 if uimerge:
259 if uimerge:
260 # external tools defined in uimerge won't be able to handle
260 # external tools defined in uimerge won't be able to handle
261 # change/delete conflicts
261 # change/delete conflicts
262 if check(uimerge, path, symlink, binary, changedelete):
262 if check(uimerge, path, symlink, binary, changedelete):
263 if uimerge not in names and not changedelete:
263 if uimerge not in names and not changedelete:
264 return (uimerge, uimerge)
264 return (uimerge, uimerge)
265 tools.insert(0, (None, uimerge)) # highest priority
265 tools.insert(0, (None, uimerge)) # highest priority
266 tools.append((None, b"hgmerge")) # the old default, if found
266 tools.append((None, b"hgmerge")) # the old default, if found
267 for p, t in tools:
267 for p, t in tools:
268 if check(t, None, symlink, binary, changedelete):
268 if check(t, None, symlink, binary, changedelete):
269 toolpath = _findtool(ui, t)
269 toolpath = _findtool(ui, t)
270 return (t, _quotetoolpath(toolpath))
270 return (t, _quotetoolpath(toolpath))
271
271
272 # internal merge or prompt as last resort
272 # internal merge or prompt as last resort
273 if symlink or binary or changedelete:
273 if symlink or binary or changedelete:
274 if not changedelete and len(tools):
274 if not changedelete and len(tools):
275 # any tool is rejected by capability for symlink or binary
275 # any tool is rejected by capability for symlink or binary
276 ui.warn(_(b"no tool found to merge %s\n") % path)
276 ui.warn(_(b"no tool found to merge %s\n") % path)
277 return b":prompt", None
277 return b":prompt", None
278 return b":merge", None
278 return b":merge", None
279
279
280
280
281 def _eoltype(data):
281 def _eoltype(data):
282 """Guess the EOL type of a file"""
282 """Guess the EOL type of a file"""
283 if b'\0' in data: # binary
283 if b'\0' in data: # binary
284 return None
284 return None
285 if b'\r\n' in data: # Windows
285 if b'\r\n' in data: # Windows
286 return b'\r\n'
286 return b'\r\n'
287 if b'\r' in data: # Old Mac
287 if b'\r' in data: # Old Mac
288 return b'\r'
288 return b'\r'
289 if b'\n' in data: # UNIX
289 if b'\n' in data: # UNIX
290 return b'\n'
290 return b'\n'
291 return None # unknown
291 return None # unknown
292
292
293
293
294 def _matcheol(file, backup):
294 def _matcheol(file, backup):
295 """Convert EOL markers in a file to match origfile"""
295 """Convert EOL markers in a file to match origfile"""
296 tostyle = _eoltype(backup.data()) # No repo.wread filters?
296 tostyle = _eoltype(backup.data()) # No repo.wread filters?
297 if tostyle:
297 if tostyle:
298 data = util.readfile(file)
298 data = util.readfile(file)
299 style = _eoltype(data)
299 style = _eoltype(data)
300 if style:
300 if style:
301 newdata = data.replace(style, tostyle)
301 newdata = data.replace(style, tostyle)
302 if newdata != data:
302 if newdata != data:
303 util.writefile(file, newdata)
303 util.writefile(file, newdata)
304
304
305
305
306 @internaltool(b'prompt', nomerge)
306 @internaltool(b'prompt', nomerge)
307 def _iprompt(repo, mynode, local, other, base, toolconf):
307 def _iprompt(repo, mynode, local, other, base, toolconf):
308 """Asks the user which of the local `p1()` or the other `p2()` version to
308 """Asks the user which of the local `p1()` or the other `p2()` version to
309 keep as the merged version."""
309 keep as the merged version."""
310 ui = repo.ui
310 ui = repo.ui
311 fd = local.fctx.path()
311 fd = local.fctx.path()
312 uipathfn = scmutil.getuipathfn(repo)
312 uipathfn = scmutil.getuipathfn(repo)
313
313
314 # Avoid prompting during an in-memory merge since it doesn't support merge
314 # Avoid prompting during an in-memory merge since it doesn't support merge
315 # conflicts.
315 # conflicts.
316 if local.fctx.changectx().isinmemory():
316 if local.fctx.changectx().isinmemory():
317 raise error.InMemoryMergeConflictsError(
317 raise error.InMemoryMergeConflictsError(
318 b'in-memory merge does not support file conflicts'
318 b'in-memory merge does not support file conflicts'
319 )
319 )
320
320
321 prompts = partextras([local.label, other.label])
321 prompts = partextras([local.label, other.label])
322 prompts[b'fd'] = uipathfn(fd)
322 prompts[b'fd'] = uipathfn(fd)
323 try:
323 try:
324 if other.fctx.isabsent():
324 if other.fctx.isabsent():
325 index = ui.promptchoice(_localchangedotherdeletedmsg % prompts, 2)
325 index = ui.promptchoice(_localchangedotherdeletedmsg % prompts, 2)
326 choice = [b'local', b'other', b'unresolved'][index]
326 choice = [b'local', b'other', b'unresolved'][index]
327 elif local.fctx.isabsent():
327 elif local.fctx.isabsent():
328 index = ui.promptchoice(_otherchangedlocaldeletedmsg % prompts, 2)
328 index = ui.promptchoice(_otherchangedlocaldeletedmsg % prompts, 2)
329 choice = [b'other', b'local', b'unresolved'][index]
329 choice = [b'other', b'local', b'unresolved'][index]
330 else:
330 else:
331 # IMPORTANT: keep the last line of this prompt ("What do you want to
331 # IMPORTANT: keep the last line of this prompt ("What do you want to
332 # do?") very short, see comment next to _localchangedotherdeletedmsg
332 # do?") very short, see comment next to _localchangedotherdeletedmsg
333 # at the top of the file for details.
333 # at the top of the file for details.
334 index = ui.promptchoice(
334 index = ui.promptchoice(
335 _(
335 _(
336 b"file '%(fd)s' needs to be resolved.\n"
336 b"file '%(fd)s' needs to be resolved.\n"
337 b"You can keep (l)ocal%(l)s, take (o)ther%(o)s, or leave "
337 b"You can keep (l)ocal%(l)s, take (o)ther%(o)s, or leave "
338 b"(u)nresolved.\n"
338 b"(u)nresolved.\n"
339 b"What do you want to do?"
339 b"What do you want to do?"
340 b"$$ &Local $$ &Other $$ &Unresolved"
340 b"$$ &Local $$ &Other $$ &Unresolved"
341 )
341 )
342 % prompts,
342 % prompts,
343 2,
343 2,
344 )
344 )
345 choice = [b'local', b'other', b'unresolved'][index]
345 choice = [b'local', b'other', b'unresolved'][index]
346
346
347 if choice == b'other':
347 if choice == b'other':
348 return _iother(repo, mynode, local, other, base, toolconf)
348 return _iother(repo, mynode, local, other, base, toolconf)
349 elif choice == b'local':
349 elif choice == b'local':
350 return _ilocal(repo, mynode, local, other, base, toolconf)
350 return _ilocal(repo, mynode, local, other, base, toolconf)
351 elif choice == b'unresolved':
351 elif choice == b'unresolved':
352 return _ifail(repo, mynode, local, other, base, toolconf)
352 return _ifail(repo, mynode, local, other, base, toolconf)
353 except error.ResponseExpected:
353 except error.ResponseExpected:
354 ui.write(b"\n")
354 ui.write(b"\n")
355 return _ifail(repo, mynode, local, other, base, toolconf)
355 return _ifail(repo, mynode, local, other, base, toolconf)
356
356
357
357
358 @internaltool(b'local', nomerge)
358 @internaltool(b'local', nomerge)
359 def _ilocal(repo, mynode, local, other, base, toolconf):
359 def _ilocal(repo, mynode, local, other, base, toolconf):
360 """Uses the local `p1()` version of files as the merged version."""
360 """Uses the local `p1()` version of files as the merged version."""
361 return 0, local.fctx.isabsent()
361 return 0, local.fctx.isabsent()
362
362
363
363
364 @internaltool(b'other', nomerge)
364 @internaltool(b'other', nomerge)
365 def _iother(repo, mynode, local, other, base, toolconf):
365 def _iother(repo, mynode, local, other, base, toolconf):
366 """Uses the other `p2()` version of files as the merged version."""
366 """Uses the other `p2()` version of files as the merged version."""
367 if other.fctx.isabsent():
367 if other.fctx.isabsent():
368 # local changed, remote deleted -- 'deleted' picked
368 # local changed, remote deleted -- 'deleted' picked
369 _underlyingfctxifabsent(local.fctx).remove()
369 _underlyingfctxifabsent(local.fctx).remove()
370 deleted = True
370 deleted = True
371 else:
371 else:
372 _underlyingfctxifabsent(local.fctx).write(
372 _underlyingfctxifabsent(local.fctx).write(
373 other.fctx.data(), other.fctx.flags()
373 other.fctx.data(), other.fctx.flags()
374 )
374 )
375 deleted = False
375 deleted = False
376 return 0, deleted
376 return 0, deleted
377
377
378
378
379 @internaltool(b'fail', nomerge)
379 @internaltool(b'fail', nomerge)
380 def _ifail(repo, mynode, local, other, base, toolconf):
380 def _ifail(repo, mynode, local, other, base, toolconf):
381 """
381 """
382 Rather than attempting to merge files that were modified on both
382 Rather than attempting to merge files that were modified on both
383 branches, it marks them as unresolved. The resolve command must be
383 branches, it marks them as unresolved. The resolve command must be
384 used to resolve these conflicts."""
384 used to resolve these conflicts."""
385 # for change/delete conflicts write out the changed version, then fail
385 # for change/delete conflicts write out the changed version, then fail
386 if local.fctx.isabsent():
386 if local.fctx.isabsent():
387 _underlyingfctxifabsent(local.fctx).write(
387 _underlyingfctxifabsent(local.fctx).write(
388 other.fctx.data(), other.fctx.flags()
388 other.fctx.data(), other.fctx.flags()
389 )
389 )
390 return 1, False
390 return 1, False
391
391
392
392
393 def _underlyingfctxifabsent(filectx):
393 def _underlyingfctxifabsent(filectx):
394 """Sometimes when resolving, our fcd is actually an absentfilectx, but
394 """Sometimes when resolving, our fcd is actually an absentfilectx, but
395 we want to write to it (to do the resolve). This helper returns the
395 we want to write to it (to do the resolve). This helper returns the
396 underyling workingfilectx in that case.
396 underyling workingfilectx in that case.
397 """
397 """
398 if filectx.isabsent():
398 if filectx.isabsent():
399 return filectx.changectx()[filectx.path()]
399 return filectx.changectx()[filectx.path()]
400 else:
400 else:
401 return filectx
401 return filectx
402
402
403
403
404 def _verifytext(input, ui):
404 def _verifytext(input, ui):
405 """verifies that text is non-binary"""
405 """verifies that text is non-binary"""
406 if stringutil.binary(input.text()):
406 if stringutil.binary(input.text()):
407 msg = _(b"%s looks like a binary file.") % input.fctx.path()
407 msg = _(b"%s looks like a binary file.") % input.fctx.path()
408 ui.warn(_(b'warning: %s\n') % msg)
408 ui.warn(_(b'warning: %s\n') % msg)
409 raise error.Abort(msg)
409 raise error.Abort(msg)
410
410
411
411
412 def _premerge(repo, local, other, base, toolconf):
412 def _premerge(repo, local, other, base, toolconf):
413 tool, toolpath, binary, symlink, scriptfn = toolconf
413 tool, toolpath, binary, symlink, scriptfn = toolconf
414 if symlink or local.fctx.isabsent() or other.fctx.isabsent():
414 if symlink or local.fctx.isabsent() or other.fctx.isabsent():
415 return 1
415 return 1
416
416
417 ui = repo.ui
417 ui = repo.ui
418
418
419 validkeep = [b'keep', b'keep-merge3', b'keep-mergediff']
419 validkeep = [b'keep', b'keep-merge3', b'keep-mergediff']
420
420
421 # do we attempt to simplemerge first?
421 # do we attempt to simplemerge first?
422 try:
422 try:
423 premerge = _toolbool(ui, tool, b"premerge", not binary)
423 premerge = _toolbool(ui, tool, b"premerge", not binary)
424 except error.ConfigError:
424 except error.ConfigError:
425 premerge = _toolstr(ui, tool, b"premerge", b"").lower()
425 premerge = _toolstr(ui, tool, b"premerge", b"").lower()
426 if premerge not in validkeep:
426 if premerge not in validkeep:
427 _valid = b', '.join([b"'" + v + b"'" for v in validkeep])
427 _valid = b', '.join([b"'" + v + b"'" for v in validkeep])
428 raise error.ConfigError(
428 raise error.ConfigError(
429 _(b"%s.premerge not valid ('%s' is neither boolean nor %s)")
429 _(b"%s.premerge not valid ('%s' is neither boolean nor %s)")
430 % (tool, premerge, _valid)
430 % (tool, premerge, _valid)
431 )
431 )
432
432
433 if premerge:
433 if premerge:
434 mode = b'merge'
434 mode = b'merge'
435 if premerge == b'keep-mergediff':
435 if premerge == b'keep-mergediff':
436 mode = b'mergediff'
436 mode = b'mergediff'
437 elif premerge == b'keep-merge3':
437 elif premerge == b'keep-merge3':
438 mode = b'merge3'
438 mode = b'merge3'
439 if any(
439 if any(
440 stringutil.binary(input.text()) for input in (local, base, other)
440 stringutil.binary(input.text()) for input in (local, base, other)
441 ):
441 ):
442 return 1 # continue merging
442 return 1 # continue merging
443 merged_text, conflicts = simplemerge.simplemerge(
443 merged_text, conflicts = simplemerge.simplemerge(
444 local, base, other, mode=mode
444 local, base, other, mode=mode
445 )
445 )
446 if not conflicts or premerge in validkeep:
446 if not conflicts or premerge in validkeep:
447 # fcd.flags() already has the merged flags (done in
447 # fcd.flags() already has the merged flags (done in
448 # mergestate.resolve())
448 # mergestate.resolve())
449 local.fctx.write(merged_text, local.fctx.flags())
449 local.fctx.write(merged_text, local.fctx.flags())
450 if not conflicts:
450 if not conflicts:
451 ui.debug(b" premerge successful\n")
451 ui.debug(b" premerge successful\n")
452 return 0
452 return 0
453 return 1 # continue merging
453 return 1 # continue merging
454
454
455
455
456 def _mergecheck(repo, mynode, fcd, fco, fca, toolconf):
456 def _mergecheck(repo, mynode, fcd, fco, fca, toolconf):
457 tool, toolpath, binary, symlink, scriptfn = toolconf
457 tool, toolpath, binary, symlink, scriptfn = toolconf
458 uipathfn = scmutil.getuipathfn(repo)
458 uipathfn = scmutil.getuipathfn(repo)
459 if symlink:
459 if symlink:
460 repo.ui.warn(
460 repo.ui.warn(
461 _(b'warning: internal %s cannot merge symlinks for %s\n')
461 _(b'warning: internal %s cannot merge symlinks for %s\n')
462 % (tool, uipathfn(fcd.path()))
462 % (tool, uipathfn(fcd.path()))
463 )
463 )
464 return False
464 return False
465 if fcd.isabsent() or fco.isabsent():
465 if fcd.isabsent() or fco.isabsent():
466 repo.ui.warn(
466 repo.ui.warn(
467 _(
467 _(
468 b'warning: internal %s cannot merge change/delete '
468 b'warning: internal %s cannot merge change/delete '
469 b'conflict for %s\n'
469 b'conflict for %s\n'
470 )
470 )
471 % (tool, uipathfn(fcd.path()))
471 % (tool, uipathfn(fcd.path()))
472 )
472 )
473 return False
473 return False
474 return True
474 return True
475
475
476
476
477 def _merge(repo, local, other, base, mode):
477 def _merge(repo, local, other, base, mode):
478 """
478 """
479 Uses the internal non-interactive simple merge algorithm for merging
479 Uses the internal non-interactive simple merge algorithm for merging
480 files. It will fail if there are any conflicts and leave markers in
480 files. It will fail if there are any conflicts and leave markers in
481 the partially merged file. Markers will have two sections, one for each
481 the partially merged file. Markers will have two sections, one for each
482 side of merge, unless mode equals 'union' or 'union-other-first' which
482 side of merge, unless mode equals 'union' or 'union-other-first' which
483 suppresses the markers."""
483 suppresses the markers."""
484 ui = repo.ui
484 ui = repo.ui
485
485
486 try:
486 try:
487 _verifytext(local, ui)
487 _verifytext(local, ui)
488 _verifytext(base, ui)
488 _verifytext(base, ui)
489 _verifytext(other, ui)
489 _verifytext(other, ui)
490 except error.Abort:
490 except error.Abort:
491 return True, True, False
491 return True, True, False
492 else:
492 else:
493 merged_text, conflicts = simplemerge.simplemerge(
493 merged_text, conflicts = simplemerge.simplemerge(
494 local, base, other, mode=mode
494 local, base, other, mode=mode
495 )
495 )
496 # fcd.flags() already has the merged flags (done in
496 # fcd.flags() already has the merged flags (done in
497 # mergestate.resolve())
497 # mergestate.resolve())
498 local.fctx.write(merged_text, local.fctx.flags())
498 local.fctx.write(merged_text, local.fctx.flags())
499 return True, conflicts, False
499 return True, conflicts, False
500
500
501
501
502 @internaltool(
502 @internaltool(
503 b'union',
503 b'union',
504 fullmerge,
504 fullmerge,
505 _(
505 _(
506 b"warning: conflicts while merging %s! "
506 b"warning: conflicts while merging %s! "
507 b"(edit, then use 'hg resolve --mark')\n"
507 b"(edit, then use 'hg resolve --mark')\n"
508 ),
508 ),
509 precheck=_mergecheck,
509 precheck=_mergecheck,
510 )
510 )
511 def _iunion(repo, mynode, local, other, base, toolconf, backup):
511 def _iunion(repo, mynode, local, other, base, toolconf, backup):
512 """
512 """
513 Uses the internal non-interactive simple merge algorithm for merging
513 Uses the internal non-interactive simple merge algorithm for merging
514 files. It will use both local and other sides for conflict regions by
514 files. It will use both local and other sides for conflict regions by
515 adding local on top of other.
515 adding local on top of other.
516 No markers are inserted."""
516 No markers are inserted."""
517 return _merge(repo, local, other, base, b'union')
517 return _merge(repo, local, other, base, b'union')
518
518
519
519
520 @internaltool(
520 @internaltool(
521 b'union-other-first',
521 b'union-other-first',
522 fullmerge,
522 fullmerge,
523 _(
523 _(
524 b"warning: conflicts while merging %s! "
524 b"warning: conflicts while merging %s! "
525 b"(edit, then use 'hg resolve --mark')\n"
525 b"(edit, then use 'hg resolve --mark')\n"
526 ),
526 ),
527 precheck=_mergecheck,
527 precheck=_mergecheck,
528 )
528 )
529 def _iunion_other_first(repo, mynode, local, other, base, toolconf, backup):
529 def _iunion_other_first(repo, mynode, local, other, base, toolconf, backup):
530 """
530 """
531 Like :union, but add other on top of local."""
531 Like :union, but add other on top of local."""
532 return _merge(repo, local, other, base, b'union-other-first')
532 return _merge(repo, local, other, base, b'union-other-first')
533
533
534
534
535 @internaltool(
535 @internaltool(
536 b'merge',
536 b'merge',
537 fullmerge,
537 fullmerge,
538 _(
538 _(
539 b"warning: conflicts while merging %s! "
539 b"warning: conflicts while merging %s! "
540 b"(edit, then use 'hg resolve --mark')\n"
540 b"(edit, then use 'hg resolve --mark')\n"
541 ),
541 ),
542 precheck=_mergecheck,
542 precheck=_mergecheck,
543 )
543 )
544 def _imerge(repo, mynode, local, other, base, toolconf, backup):
544 def _imerge(repo, mynode, local, other, base, toolconf, backup):
545 """
545 """
546 Uses the internal non-interactive simple merge algorithm for merging
546 Uses the internal non-interactive simple merge algorithm for merging
547 files. It will fail if there are any conflicts and leave markers in
547 files. It will fail if there are any conflicts and leave markers in
548 the partially merged file. Markers will have two sections, one for each side
548 the partially merged file. Markers will have two sections, one for each side
549 of merge."""
549 of merge."""
550 return _merge(repo, local, other, base, b'merge')
550 return _merge(repo, local, other, base, b'merge')
551
551
552
552
553 @internaltool(
553 @internaltool(
554 b'merge3',
554 b'merge3',
555 fullmerge,
555 fullmerge,
556 _(
556 _(
557 b"warning: conflicts while merging %s! "
557 b"warning: conflicts while merging %s! "
558 b"(edit, then use 'hg resolve --mark')\n"
558 b"(edit, then use 'hg resolve --mark')\n"
559 ),
559 ),
560 precheck=_mergecheck,
560 precheck=_mergecheck,
561 )
561 )
562 def _imerge3(repo, mynode, local, other, base, toolconf, backup):
562 def _imerge3(repo, mynode, local, other, base, toolconf, backup):
563 """
563 """
564 Uses the internal non-interactive simple merge algorithm for merging
564 Uses the internal non-interactive simple merge algorithm for merging
565 files. It will fail if there are any conflicts and leave markers in
565 files. It will fail if there are any conflicts and leave markers in
566 the partially merged file. Marker will have three sections, one from each
566 the partially merged file. Marker will have three sections, one from each
567 side of the merge and one for the base content."""
567 side of the merge and one for the base content."""
568 return _merge(repo, local, other, base, b'merge3')
568 return _merge(repo, local, other, base, b'merge3')
569
569
570
570
571 @internaltool(
571 @internaltool(
572 b'merge3-lie-about-conflicts',
572 b'merge3-lie-about-conflicts',
573 fullmerge,
573 fullmerge,
574 b'',
574 b'',
575 precheck=_mergecheck,
575 precheck=_mergecheck,
576 )
576 )
577 def _imerge3alwaysgood(*args, **kwargs):
577 def _imerge3alwaysgood(*args, **kwargs):
578 # Like merge3, but record conflicts as resolved with markers in place.
578 # Like merge3, but record conflicts as resolved with markers in place.
579 #
579 #
580 # This is used for `diff.merge` to show the differences between
580 # This is used for `diff.merge` to show the differences between
581 # the auto-merge state and the committed merge state. It may be
581 # the auto-merge state and the committed merge state. It may be
582 # useful for other things.
582 # useful for other things.
583 b1, junk, b2 = _imerge3(*args, **kwargs)
583 b1, junk, b2 = _imerge3(*args, **kwargs)
584 # TODO is this right? I'm not sure what these return values mean,
584 # TODO is this right? I'm not sure what these return values mean,
585 # but as far as I can tell this will indicate to callers tha the
585 # but as far as I can tell this will indicate to callers tha the
586 # merge succeeded.
586 # merge succeeded.
587 return b1, False, b2
587 return b1, False, b2
588
588
589
589
590 @internaltool(
590 @internaltool(
591 b'mergediff',
591 b'mergediff',
592 fullmerge,
592 fullmerge,
593 _(
593 _(
594 b"warning: conflicts while merging %s! "
594 b"warning: conflicts while merging %s! "
595 b"(edit, then use 'hg resolve --mark')\n"
595 b"(edit, then use 'hg resolve --mark')\n"
596 ),
596 ),
597 precheck=_mergecheck,
597 precheck=_mergecheck,
598 )
598 )
599 def _imerge_diff(repo, mynode, local, other, base, toolconf, backup):
599 def _imerge_diff(repo, mynode, local, other, base, toolconf, backup):
600 """
600 """
601 Uses the internal non-interactive simple merge algorithm for merging
601 Uses the internal non-interactive simple merge algorithm for merging
602 files. It will fail if there are any conflicts and leave markers in
602 files. It will fail if there are any conflicts and leave markers in
603 the partially merged file. The marker will have two sections, one with the
603 the partially merged file. The marker will have two sections, one with the
604 content from one side of the merge, and one with a diff from the base
604 content from one side of the merge, and one with a diff from the base
605 content to the content on the other side. (experimental)"""
605 content to the content on the other side. (experimental)"""
606 return _merge(repo, local, other, base, b'mergediff')
606 return _merge(repo, local, other, base, b'mergediff')
607
607
608
608
609 @internaltool(b'merge-local', mergeonly, precheck=_mergecheck)
609 @internaltool(b'merge-local', mergeonly, precheck=_mergecheck)
610 def _imergelocal(repo, mynode, local, other, base, toolconf, backup):
610 def _imergelocal(repo, mynode, local, other, base, toolconf, backup):
611 """
611 """
612 Like :merge, but resolve all conflicts non-interactively in favor
612 Like :merge, but resolve all conflicts non-interactively in favor
613 of the local `p1()` changes."""
613 of the local `p1()` changes."""
614 return _merge(repo, local, other, base, b'local')
614 return _merge(repo, local, other, base, b'local')
615
615
616
616
617 @internaltool(b'merge-other', mergeonly, precheck=_mergecheck)
617 @internaltool(b'merge-other', mergeonly, precheck=_mergecheck)
618 def _imergeother(repo, mynode, local, other, base, toolconf, backup):
618 def _imergeother(repo, mynode, local, other, base, toolconf, backup):
619 """
619 """
620 Like :merge, but resolve all conflicts non-interactively in favor
620 Like :merge, but resolve all conflicts non-interactively in favor
621 of the other `p2()` changes."""
621 of the other `p2()` changes."""
622 return _merge(repo, local, other, base, b'other')
622 return _merge(repo, local, other, base, b'other')
623
623
624
624
625 @internaltool(
625 @internaltool(
626 b'tagmerge',
626 b'tagmerge',
627 mergeonly,
627 mergeonly,
628 _(
628 _(
629 b"automatic tag merging of %s failed! "
629 b"automatic tag merging of %s failed! "
630 b"(use 'hg resolve --tool :merge' or another merge "
630 b"(use 'hg resolve --tool :merge' or another merge "
631 b"tool of your choice)\n"
631 b"tool of your choice)\n"
632 ),
632 ),
633 )
633 )
634 def _itagmerge(repo, mynode, local, other, base, toolconf, backup):
634 def _itagmerge(repo, mynode, local, other, base, toolconf, backup):
635 """
635 """
636 Uses the internal tag merge algorithm (experimental).
636 Uses the internal tag merge algorithm (experimental).
637 """
637 """
638 success, status = tagmerge.merge(repo, local.fctx, other.fctx, base.fctx)
638 success, status = tagmerge.merge(repo, local.fctx, other.fctx, base.fctx)
639 return success, status, False
639 return success, status, False
640
640
641
641
642 @internaltool(b'dump', fullmerge, binary=True, symlink=True)
642 @internaltool(b'dump', fullmerge, binary=True, symlink=True)
643 def _idump(repo, mynode, local, other, base, toolconf, backup):
643 def _idump(repo, mynode, local, other, base, toolconf, backup):
644 """
644 """
645 Creates three versions of the files to merge, containing the
645 Creates three versions of the files to merge, containing the
646 contents of local, other and base. These files can then be used to
646 contents of local, other and base. These files can then be used to
647 perform a merge manually. If the file to be merged is named
647 perform a merge manually. If the file to be merged is named
648 ``a.txt``, these files will accordingly be named ``a.txt.local``,
648 ``a.txt``, these files will accordingly be named ``a.txt.local``,
649 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
649 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
650 same directory as ``a.txt``.
650 same directory as ``a.txt``.
651
651
652 This implies premerge. Therefore, files aren't dumped, if premerge
652 This implies premerge. Therefore, files aren't dumped, if premerge
653 runs successfully. Use :forcedump to forcibly write files out.
653 runs successfully. Use :forcedump to forcibly write files out.
654 """
654 """
655 a = _workingpath(repo, local.fctx)
655 a = _workingpath(repo, local.fctx)
656 fd = local.fctx.path()
656 fd = local.fctx.path()
657
657
658 from . import context
658 from . import context
659
659
660 if isinstance(local.fctx, context.overlayworkingfilectx):
660 if isinstance(local.fctx, context.overlayworkingfilectx):
661 raise error.InMemoryMergeConflictsError(
661 raise error.InMemoryMergeConflictsError(
662 b'in-memory merge does not support the :dump tool.'
662 b'in-memory merge does not support the :dump tool.'
663 )
663 )
664
664
665 util.writefile(a + b".local", local.fctx.decodeddata())
665 util.writefile(a + b".local", local.fctx.decodeddata())
666 repo.wwrite(fd + b".other", other.fctx.data(), other.fctx.flags())
666 repo.wwrite(fd + b".other", other.fctx.data(), other.fctx.flags())
667 repo.wwrite(fd + b".base", base.fctx.data(), base.fctx.flags())
667 repo.wwrite(fd + b".base", base.fctx.data(), base.fctx.flags())
668 return False, 1, False
668 return False, 1, False
669
669
670
670
671 @internaltool(b'forcedump', mergeonly, binary=True, symlink=True)
671 @internaltool(b'forcedump', mergeonly, binary=True, symlink=True)
672 def _forcedump(repo, mynode, local, other, base, toolconf, backup):
672 def _forcedump(repo, mynode, local, other, base, toolconf, backup):
673 """
673 """
674 Creates three versions of the files as same as :dump, but omits premerge.
674 Creates three versions of the files as same as :dump, but omits premerge.
675 """
675 """
676 return _idump(repo, mynode, local, other, base, toolconf, backup)
676 return _idump(repo, mynode, local, other, base, toolconf, backup)
677
677
678
678
679 def _xmergeimm(repo, mynode, local, other, base, toolconf, backup):
679 def _xmergeimm(repo, mynode, local, other, base, toolconf, backup):
680 # In-memory merge simply raises an exception on all external merge tools,
680 # In-memory merge simply raises an exception on all external merge tools,
681 # for now.
681 # for now.
682 #
682 #
683 # It would be possible to run most tools with temporary files, but this
683 # It would be possible to run most tools with temporary files, but this
684 # raises the question of what to do if the user only partially resolves the
684 # raises the question of what to do if the user only partially resolves the
685 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
685 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
686 # directory and tell the user how to get it is my best idea, but it's
686 # directory and tell the user how to get it is my best idea, but it's
687 # clunky.)
687 # clunky.)
688 raise error.InMemoryMergeConflictsError(
688 raise error.InMemoryMergeConflictsError(
689 b'in-memory merge does not support external merge tools'
689 b'in-memory merge does not support external merge tools'
690 )
690 )
691
691
692
692
693 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
693 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
694 tmpl = ui.config(b'command-templates', b'pre-merge-tool-output')
694 tmpl = ui.config(b'command-templates', b'pre-merge-tool-output')
695 if not tmpl:
695 if not tmpl:
696 return
696 return
697
697
698 mappingdict = templateutil.mappingdict
698 mappingdict = templateutil.mappingdict
699 props = {
699 props = {
700 b'ctx': fcl.changectx(),
700 b'ctx': fcl.changectx(),
701 b'node': hex(mynode),
701 b'node': hex(mynode),
702 b'path': fcl.path(),
702 b'path': fcl.path(),
703 b'local': mappingdict(
703 b'local': mappingdict(
704 {
704 {
705 b'ctx': fcl.changectx(),
705 b'ctx': fcl.changectx(),
706 b'fctx': fcl,
706 b'fctx': fcl,
707 b'node': hex(mynode),
707 b'node': hex(mynode),
708 b'name': _(b'local'),
708 b'name': _(b'local'),
709 b'islink': b'l' in fcl.flags(),
709 b'islink': b'l' in fcl.flags(),
710 b'label': env[b'HG_MY_LABEL'],
710 b'label': env[b'HG_MY_LABEL'],
711 }
711 }
712 ),
712 ),
713 b'base': mappingdict(
713 b'base': mappingdict(
714 {
714 {
715 b'ctx': fcb.changectx(),
715 b'ctx': fcb.changectx(),
716 b'fctx': fcb,
716 b'fctx': fcb,
717 b'name': _(b'base'),
717 b'name': _(b'base'),
718 b'islink': b'l' in fcb.flags(),
718 b'islink': b'l' in fcb.flags(),
719 b'label': env[b'HG_BASE_LABEL'],
719 b'label': env[b'HG_BASE_LABEL'],
720 }
720 }
721 ),
721 ),
722 b'other': mappingdict(
722 b'other': mappingdict(
723 {
723 {
724 b'ctx': fco.changectx(),
724 b'ctx': fco.changectx(),
725 b'fctx': fco,
725 b'fctx': fco,
726 b'name': _(b'other'),
726 b'name': _(b'other'),
727 b'islink': b'l' in fco.flags(),
727 b'islink': b'l' in fco.flags(),
728 b'label': env[b'HG_OTHER_LABEL'],
728 b'label': env[b'HG_OTHER_LABEL'],
729 }
729 }
730 ),
730 ),
731 b'toolpath': toolpath,
731 b'toolpath': toolpath,
732 b'toolargs': args,
732 b'toolargs': args,
733 }
733 }
734
734
735 # TODO: make all of this something that can be specified on a per-tool basis
735 # TODO: make all of this something that can be specified on a per-tool basis
736 tmpl = templater.unquotestring(tmpl)
736 tmpl = templater.unquotestring(tmpl)
737
737
738 # Not using cmdutil.rendertemplate here since it causes errors importing
738 # Not using cmdutil.rendertemplate here since it causes errors importing
739 # things for us to import cmdutil.
739 # things for us to import cmdutil.
740 tres = formatter.templateresources(ui, repo)
740 tres = formatter.templateresources(ui, repo)
741 t = formatter.maketemplater(
741 t = formatter.maketemplater(
742 ui, tmpl, defaults=templatekw.keywords, resources=tres
742 ui, tmpl, defaults=templatekw.keywords, resources=tres
743 )
743 )
744 ui.status(t.renderdefault(props))
744 ui.status(t.renderdefault(props))
745
745
746
746
747 def _xmerge(repo, mynode, local, other, base, toolconf, backup):
747 def _xmerge(repo, mynode, local, other, base, toolconf, backup):
748 fcd = local.fctx
748 fcd = local.fctx
749 fco = other.fctx
749 fco = other.fctx
750 fca = base.fctx
750 fca = base.fctx
751 tool, toolpath, binary, symlink, scriptfn = toolconf
751 tool, toolpath, binary, symlink, scriptfn = toolconf
752 uipathfn = scmutil.getuipathfn(repo)
752 uipathfn = scmutil.getuipathfn(repo)
753 if fcd.isabsent() or fco.isabsent():
753 if fcd.isabsent() or fco.isabsent():
754 repo.ui.warn(
754 repo.ui.warn(
755 _(b'warning: %s cannot merge change/delete conflict for %s\n')
755 _(b'warning: %s cannot merge change/delete conflict for %s\n')
756 % (tool, uipathfn(fcd.path()))
756 % (tool, uipathfn(fcd.path()))
757 )
757 )
758 return False, 1, None
758 return False, 1, None
759 localpath = _workingpath(repo, fcd)
759 localpath = _workingpath(repo, fcd)
760 args = _toolstr(repo.ui, tool, b"args")
760 args = _toolstr(repo.ui, tool, b"args")
761
761
762 files = [
762 files = [
763 (b"base", fca.path(), fca.decodeddata()),
763 (b"base", fca.path(), fca.decodeddata()),
764 (b"other", fco.path(), fco.decodeddata()),
764 (b"other", fco.path(), fco.decodeddata()),
765 ]
765 ]
766 outpath = b""
766 outpath = b""
767 if b"$output" in args:
767 if b"$output" in args:
768 # read input from backup, write to original
768 # read input from backup, write to original
769 outpath = localpath
769 outpath = localpath
770 localoutputpath = backup.path()
770 localoutputpath = backup.path()
771 # Remove the .orig to make syntax-highlighting more likely.
771 # Remove the .orig to make syntax-highlighting more likely.
772 if localoutputpath.endswith(b'.orig'):
772 if localoutputpath.endswith(b'.orig'):
773 localoutputpath, ext = os.path.splitext(localoutputpath)
773 localoutputpath, ext = os.path.splitext(localoutputpath)
774 files.append((b"local", localoutputpath, backup.data()))
774 files.append((b"local", localoutputpath, backup.data()))
775
775
776 with _maketempfiles(files) as temppaths:
776 with _maketempfiles(files) as temppaths:
777 basepath, otherpath = temppaths[:2]
777 basepath, otherpath = temppaths[:2]
778 if len(temppaths) == 3:
778 if len(temppaths) == 3:
779 localpath = temppaths[2]
779 localpath = temppaths[2]
780
780
781 def format_label(input):
781 def format_label(input):
782 if input.label_detail:
782 if input.label_detail:
783 return b'%s: %s' % (input.label, input.label_detail)
783 return b'%s: %s' % (input.label, input.label_detail)
784 else:
784 else:
785 return input.label
785 return input.label
786
786
787 env = {
787 env = {
788 b'HG_FILE': fcd.path(),
788 b'HG_FILE': fcd.path(),
789 b'HG_MY_NODE': short(mynode),
789 b'HG_MY_NODE': short(mynode),
790 b'HG_OTHER_NODE': short(fco.changectx().node()),
790 b'HG_OTHER_NODE': short(fco.changectx().node()),
791 b'HG_BASE_NODE': short(fca.changectx().node()),
791 b'HG_BASE_NODE': short(fca.changectx().node()),
792 b'HG_MY_ISLINK': b'l' in fcd.flags(),
792 b'HG_MY_ISLINK': b'l' in fcd.flags(),
793 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
793 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
794 b'HG_BASE_ISLINK': b'l' in fca.flags(),
794 b'HG_BASE_ISLINK': b'l' in fca.flags(),
795 b'HG_MY_LABEL': format_label(local),
795 b'HG_MY_LABEL': format_label(local),
796 b'HG_OTHER_LABEL': format_label(other),
796 b'HG_OTHER_LABEL': format_label(other),
797 b'HG_BASE_LABEL': format_label(base),
797 b'HG_BASE_LABEL': format_label(base),
798 }
798 }
799 ui = repo.ui
799 ui = repo.ui
800
800
801 replace = {
801 replace = {
802 b'local': localpath,
802 b'local': localpath,
803 b'base': basepath,
803 b'base': basepath,
804 b'other': otherpath,
804 b'other': otherpath,
805 b'output': outpath,
805 b'output': outpath,
806 b'labellocal': format_label(local),
806 b'labellocal': format_label(local),
807 b'labelother': format_label(other),
807 b'labelother': format_label(other),
808 b'labelbase': format_label(base),
808 b'labelbase': format_label(base),
809 }
809 }
810 args = util.interpolate(
810 args = util.interpolate(
811 br'\$',
811 br'\$',
812 replace,
812 replace,
813 args,
813 args,
814 lambda s: procutil.shellquote(util.localpath(s)),
814 lambda s: procutil.shellquote(util.localpath(s)),
815 )
815 )
816 if _toolbool(ui, tool, b"gui"):
816 if _toolbool(ui, tool, b"gui"):
817 repo.ui.status(
817 repo.ui.status(
818 _(b'running merge tool %s for file %s\n')
818 _(b'running merge tool %s for file %s\n')
819 % (tool, uipathfn(fcd.path()))
819 % (tool, uipathfn(fcd.path()))
820 )
820 )
821 if scriptfn is None:
821 if scriptfn is None:
822 cmd = toolpath + b' ' + args
822 cmd = toolpath + b' ' + args
823 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
823 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
824 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
824 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
825 r = ui.system(
825 r = ui.system(
826 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
826 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
827 )
827 )
828 else:
828 else:
829 repo.ui.debug(
829 repo.ui.debug(
830 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
830 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
831 )
831 )
832 r = 0
832 r = 0
833 try:
833 try:
834 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
834 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
835 from . import extensions
835 from . import extensions
836
836
837 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
837 mod_name = 'hgmerge.%s' % pycompat.sysstr(tool)
838 mod = extensions.loadpath(toolpath, mod_name)
838 except Exception:
839 except Exception:
839 raise error.Abort(
840 raise error.Abort(
840 _(b"loading python merge script failed: %s") % toolpath
841 _(b"loading python merge script failed: %s") % toolpath
841 )
842 )
842 mergefn = getattr(mod, scriptfn, None)
843 mergefn = getattr(mod, pycompat.sysstr(scriptfn), None)
843 if mergefn is None:
844 if mergefn is None:
844 raise error.Abort(
845 raise error.Abort(
845 _(b"%s does not have function: %s") % (toolpath, scriptfn)
846 _(b"%s does not have function: %s") % (toolpath, scriptfn)
846 )
847 )
847 argslist = procutil.shellsplit(args)
848 argslist = procutil.shellsplit(args)
848 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
849 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
849 from . import hook
850 from . import hook
850
851
851 ret, raised = hook.pythonhook(
852 ret, raised = hook.pythonhook(
852 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
853 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
853 )
854 )
854 if raised:
855 if raised:
855 r = 1
856 r = 1
856 repo.ui.debug(b'merge tool returned: %d\n' % r)
857 repo.ui.debug(b'merge tool returned: %d\n' % r)
857 return True, r, False
858 return True, r, False
858
859
859
860
860 def _populate_label_detail(input, template):
861 def _populate_label_detail(input, template):
861 """Applies the given template to the ctx and stores it in the input."""
862 """Applies the given template to the ctx and stores it in the input."""
862 ctx = input.fctx.changectx()
863 ctx = input.fctx.changectx()
863 if ctx.node() is None:
864 if ctx.node() is None:
864 ctx = ctx.p1()
865 ctx = ctx.p1()
865
866
866 props = {b'ctx': ctx}
867 props = {b'ctx': ctx}
867 templateresult = template.renderdefault(props)
868 templateresult = template.renderdefault(props)
868 input.label_detail = stringutil.firstline(templateresult) # avoid '\n'
869 input.label_detail = stringutil.firstline(templateresult) # avoid '\n'
869
870
870
871
871 def _populate_label_details(repo, inputs, tool=None):
872 def _populate_label_details(repo, inputs, tool=None):
872 """Populates the label details using the conflict marker template."""
873 """Populates the label details using the conflict marker template."""
873 ui = repo.ui
874 ui = repo.ui
874 template = ui.config(b'command-templates', b'mergemarker')
875 template = ui.config(b'command-templates', b'mergemarker')
875 if tool is not None:
876 if tool is not None:
876 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
877 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
877 template = templater.unquotestring(template)
878 template = templater.unquotestring(template)
878 tres = formatter.templateresources(ui, repo)
879 tres = formatter.templateresources(ui, repo)
879 tmpl = formatter.maketemplater(
880 tmpl = formatter.maketemplater(
880 ui, template, defaults=templatekw.keywords, resources=tres
881 ui, template, defaults=templatekw.keywords, resources=tres
881 )
882 )
882
883
883 for input in inputs:
884 for input in inputs:
884 _populate_label_detail(input, tmpl)
885 _populate_label_detail(input, tmpl)
885
886
886
887
887 def partextras(labels):
888 def partextras(labels):
888 """Return a dictionary of extra labels for use in prompts to the user
889 """Return a dictionary of extra labels for use in prompts to the user
889
890
890 Intended use is in strings of the form "(l)ocal%(l)s".
891 Intended use is in strings of the form "(l)ocal%(l)s".
891 """
892 """
892 if labels is None:
893 if labels is None:
893 return {
894 return {
894 b"l": b"",
895 b"l": b"",
895 b"o": b"",
896 b"o": b"",
896 }
897 }
897
898
898 return {
899 return {
899 b"l": b" [%s]" % labels[0],
900 b"l": b" [%s]" % labels[0],
900 b"o": b" [%s]" % labels[1],
901 b"o": b" [%s]" % labels[1],
901 }
902 }
902
903
903
904
904 def _makebackup(repo, ui, fcd):
905 def _makebackup(repo, ui, fcd):
905 """Makes and returns a filectx-like object for ``fcd``'s backup file.
906 """Makes and returns a filectx-like object for ``fcd``'s backup file.
906
907
907 In addition to preserving the user's pre-existing modifications to `fcd`
908 In addition to preserving the user's pre-existing modifications to `fcd`
908 (if any), the backup is used to undo certain premerges, confirm whether a
909 (if any), the backup is used to undo certain premerges, confirm whether a
909 merge changed anything, and determine what line endings the new file should
910 merge changed anything, and determine what line endings the new file should
910 have.
911 have.
911
912
912 Backups only need to be written once since their content doesn't change
913 Backups only need to be written once since their content doesn't change
913 afterwards.
914 afterwards.
914 """
915 """
915 if fcd.isabsent():
916 if fcd.isabsent():
916 return None
917 return None
917 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
918 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
918 # merge -> filemerge). (I suspect the fileset import is the weakest link)
919 # merge -> filemerge). (I suspect the fileset import is the weakest link)
919 from . import context
920 from . import context
920
921
921 if isinstance(fcd, context.overlayworkingfilectx):
922 if isinstance(fcd, context.overlayworkingfilectx):
922 # If we're merging in-memory, we're free to put the backup anywhere.
923 # If we're merging in-memory, we're free to put the backup anywhere.
923 fd, backup = pycompat.mkstemp(b'hg-merge-backup')
924 fd, backup = pycompat.mkstemp(b'hg-merge-backup')
924 with os.fdopen(fd, 'wb') as f:
925 with os.fdopen(fd, 'wb') as f:
925 f.write(fcd.data())
926 f.write(fcd.data())
926 else:
927 else:
927 backup = scmutil.backuppath(ui, repo, fcd.path())
928 backup = scmutil.backuppath(ui, repo, fcd.path())
928 a = _workingpath(repo, fcd)
929 a = _workingpath(repo, fcd)
929 util.copyfile(a, backup)
930 util.copyfile(a, backup)
930
931
931 return context.arbitraryfilectx(backup, repo=repo)
932 return context.arbitraryfilectx(backup, repo=repo)
932
933
933
934
934 @contextlib.contextmanager
935 @contextlib.contextmanager
935 def _maketempfiles(files):
936 def _maketempfiles(files):
936 """Creates a temporary file for each (prefix, path, data) tuple in `files`,
937 """Creates a temporary file for each (prefix, path, data) tuple in `files`,
937 so an external merge tool may use them.
938 so an external merge tool may use them.
938 """
939 """
939 tmproot = pycompat.mkdtemp(prefix=b'hgmerge-')
940 tmproot = pycompat.mkdtemp(prefix=b'hgmerge-')
940
941
941 def maketempfrompath(prefix, path, data):
942 def maketempfrompath(prefix, path, data):
942 fullbase, ext = os.path.splitext(path)
943 fullbase, ext = os.path.splitext(path)
943 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
944 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
944 name = os.path.join(tmproot, pre)
945 name = os.path.join(tmproot, pre)
945 if ext:
946 if ext:
946 name += ext
947 name += ext
947 util.writefile(name, data)
948 util.writefile(name, data)
948 return name
949 return name
949
950
950 temp_files = []
951 temp_files = []
951 for prefix, path, data in files:
952 for prefix, path, data in files:
952 temp_files.append(maketempfrompath(prefix, path, data))
953 temp_files.append(maketempfrompath(prefix, path, data))
953 try:
954 try:
954 yield temp_files
955 yield temp_files
955 finally:
956 finally:
956 shutil.rmtree(tmproot)
957 shutil.rmtree(tmproot)
957
958
958
959
959 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
960 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
960 """perform a 3-way merge in the working directory
961 """perform a 3-way merge in the working directory
961
962
962 mynode = parent node before merge
963 mynode = parent node before merge
963 orig = original local filename before merge
964 orig = original local filename before merge
964 fco = other file context
965 fco = other file context
965 fca = ancestor file context
966 fca = ancestor file context
966 fcd = local file context for current/destination file
967 fcd = local file context for current/destination file
967
968
968 Returns whether the merge is complete, the return value of the merge, and
969 Returns whether the merge is complete, the return value of the merge, and
969 a boolean indicating whether the file was deleted from disk."""
970 a boolean indicating whether the file was deleted from disk."""
970 ui = repo.ui
971 ui = repo.ui
971 fd = fcd.path()
972 fd = fcd.path()
972 uipathfn = scmutil.getuipathfn(repo)
973 uipathfn = scmutil.getuipathfn(repo)
973 fduipath = uipathfn(fd)
974 fduipath = uipathfn(fd)
974 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
975 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
975 symlink = b'l' in fcd.flags() + fco.flags()
976 symlink = b'l' in fcd.flags() + fco.flags()
976 changedelete = fcd.isabsent() or fco.isabsent()
977 changedelete = fcd.isabsent() or fco.isabsent()
977 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
978 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
978 scriptfn = None
979 scriptfn = None
979 if tool in internals and tool.startswith(b'internal:'):
980 if tool in internals and tool.startswith(b'internal:'):
980 # normalize to new-style names (':merge' etc)
981 # normalize to new-style names (':merge' etc)
981 tool = tool[len(b'internal') :]
982 tool = tool[len(b'internal') :]
982 if toolpath and toolpath.startswith(b'python:'):
983 if toolpath and toolpath.startswith(b'python:'):
983 invalidsyntax = False
984 invalidsyntax = False
984 if toolpath.count(b':') >= 2:
985 if toolpath.count(b':') >= 2:
985 script, scriptfn = toolpath[7:].rsplit(b':', 1)
986 script, scriptfn = toolpath[7:].rsplit(b':', 1)
986 if not scriptfn:
987 if not scriptfn:
987 invalidsyntax = True
988 invalidsyntax = True
988 # missing :callable can lead to spliting on windows drive letter
989 # missing :callable can lead to spliting on windows drive letter
989 if b'\\' in scriptfn or b'/' in scriptfn:
990 if b'\\' in scriptfn or b'/' in scriptfn:
990 invalidsyntax = True
991 invalidsyntax = True
991 else:
992 else:
992 invalidsyntax = True
993 invalidsyntax = True
993 if invalidsyntax:
994 if invalidsyntax:
994 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
995 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
995 toolpath = script
996 toolpath = script
996 ui.debug(
997 ui.debug(
997 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
998 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
998 % (
999 % (
999 tool,
1000 tool,
1000 fduipath,
1001 fduipath,
1001 pycompat.bytestr(binary),
1002 pycompat.bytestr(binary),
1002 pycompat.bytestr(symlink),
1003 pycompat.bytestr(symlink),
1003 pycompat.bytestr(changedelete),
1004 pycompat.bytestr(changedelete),
1004 )
1005 )
1005 )
1006 )
1006
1007
1007 if tool in internals:
1008 if tool in internals:
1008 func = internals[tool]
1009 func = internals[tool]
1009 mergetype = func.mergetype
1010 mergetype = func.mergetype
1010 onfailure = func.onfailure
1011 onfailure = func.onfailure
1011 precheck = func.precheck
1012 precheck = func.precheck
1012 isexternal = False
1013 isexternal = False
1013 else:
1014 else:
1014 if wctx.isinmemory():
1015 if wctx.isinmemory():
1015 func = _xmergeimm
1016 func = _xmergeimm
1016 else:
1017 else:
1017 func = _xmerge
1018 func = _xmerge
1018 mergetype = fullmerge
1019 mergetype = fullmerge
1019 onfailure = _(b"merging %s failed!\n")
1020 onfailure = _(b"merging %s failed!\n")
1020 precheck = None
1021 precheck = None
1021 isexternal = True
1022 isexternal = True
1022
1023
1023 toolconf = tool, toolpath, binary, symlink, scriptfn
1024 toolconf = tool, toolpath, binary, symlink, scriptfn
1024
1025
1025 if not labels:
1026 if not labels:
1026 labels = [b'local', b'other']
1027 labels = [b'local', b'other']
1027 if len(labels) < 3:
1028 if len(labels) < 3:
1028 labels.append(b'base')
1029 labels.append(b'base')
1029 local = simplemerge.MergeInput(fcd, labels[0])
1030 local = simplemerge.MergeInput(fcd, labels[0])
1030 other = simplemerge.MergeInput(fco, labels[1])
1031 other = simplemerge.MergeInput(fco, labels[1])
1031 base = simplemerge.MergeInput(fca, labels[2])
1032 base = simplemerge.MergeInput(fca, labels[2])
1032 if mergetype == nomerge:
1033 if mergetype == nomerge:
1033 return func(
1034 return func(
1034 repo,
1035 repo,
1035 mynode,
1036 mynode,
1036 local,
1037 local,
1037 other,
1038 other,
1038 base,
1039 base,
1039 toolconf,
1040 toolconf,
1040 )
1041 )
1041
1042
1042 if orig != fco.path():
1043 if orig != fco.path():
1043 ui.status(
1044 ui.status(
1044 _(b"merging %s and %s to %s\n")
1045 _(b"merging %s and %s to %s\n")
1045 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1046 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1046 )
1047 )
1047 else:
1048 else:
1048 ui.status(_(b"merging %s\n") % fduipath)
1049 ui.status(_(b"merging %s\n") % fduipath)
1049
1050
1050 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1051 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1051
1052
1052 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1053 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1053 if onfailure:
1054 if onfailure:
1054 if wctx.isinmemory():
1055 if wctx.isinmemory():
1055 raise error.InMemoryMergeConflictsError(
1056 raise error.InMemoryMergeConflictsError(
1056 b'in-memory merge does not support merge conflicts'
1057 b'in-memory merge does not support merge conflicts'
1057 )
1058 )
1058 ui.warn(onfailure % fduipath)
1059 ui.warn(onfailure % fduipath)
1059 return 1, False
1060 return 1, False
1060
1061
1061 backup = _makebackup(repo, ui, fcd)
1062 backup = _makebackup(repo, ui, fcd)
1062 r = 1
1063 r = 1
1063 try:
1064 try:
1064 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1065 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1065 if isexternal:
1066 if isexternal:
1066 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1067 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1067 else:
1068 else:
1068 markerstyle = internalmarkerstyle
1069 markerstyle = internalmarkerstyle
1069
1070
1070 if mergetype == fullmerge:
1071 if mergetype == fullmerge:
1071 _run_partial_resolution_tools(repo, local, other, base)
1072 _run_partial_resolution_tools(repo, local, other, base)
1072 # conflict markers generated by premerge will use 'detailed'
1073 # conflict markers generated by premerge will use 'detailed'
1073 # settings if either ui.mergemarkers or the tool's mergemarkers
1074 # settings if either ui.mergemarkers or the tool's mergemarkers
1074 # setting is 'detailed'. This way tools can have basic labels in
1075 # setting is 'detailed'. This way tools can have basic labels in
1075 # space-constrained areas of the UI, but still get full information
1076 # space-constrained areas of the UI, but still get full information
1076 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1077 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1077 labeltool = None
1078 labeltool = None
1078 if markerstyle != b'basic':
1079 if markerstyle != b'basic':
1079 # respect 'tool's mergemarkertemplate (which defaults to
1080 # respect 'tool's mergemarkertemplate (which defaults to
1080 # command-templates.mergemarker)
1081 # command-templates.mergemarker)
1081 labeltool = tool
1082 labeltool = tool
1082 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1083 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1083 _populate_label_details(
1084 _populate_label_details(
1084 repo, [local, other, base], tool=labeltool
1085 repo, [local, other, base], tool=labeltool
1085 )
1086 )
1086
1087
1087 r = _premerge(
1088 r = _premerge(
1088 repo,
1089 repo,
1089 local,
1090 local,
1090 other,
1091 other,
1091 base,
1092 base,
1092 toolconf,
1093 toolconf,
1093 )
1094 )
1094 # we're done if premerge was successful (r is 0)
1095 # we're done if premerge was successful (r is 0)
1095 if not r:
1096 if not r:
1096 return r, False
1097 return r, False
1097
1098
1098 # Reset to basic labels
1099 # Reset to basic labels
1099 local.label_detail = None
1100 local.label_detail = None
1100 other.label_detail = None
1101 other.label_detail = None
1101 base.label_detail = None
1102 base.label_detail = None
1102
1103
1103 if markerstyle != b'basic':
1104 if markerstyle != b'basic':
1104 _populate_label_details(repo, [local, other, base], tool=tool)
1105 _populate_label_details(repo, [local, other, base], tool=tool)
1105
1106
1106 needcheck, r, deleted = func(
1107 needcheck, r, deleted = func(
1107 repo,
1108 repo,
1108 mynode,
1109 mynode,
1109 local,
1110 local,
1110 other,
1111 other,
1111 base,
1112 base,
1112 toolconf,
1113 toolconf,
1113 backup,
1114 backup,
1114 )
1115 )
1115
1116
1116 if needcheck:
1117 if needcheck:
1117 r = _check(repo, r, ui, tool, fcd, backup)
1118 r = _check(repo, r, ui, tool, fcd, backup)
1118
1119
1119 if r:
1120 if r:
1120 if onfailure:
1121 if onfailure:
1121 if wctx.isinmemory():
1122 if wctx.isinmemory():
1122 raise error.InMemoryMergeConflictsError(
1123 raise error.InMemoryMergeConflictsError(
1123 b'in-memory merge '
1124 b'in-memory merge '
1124 b'does not support '
1125 b'does not support '
1125 b'merge conflicts'
1126 b'merge conflicts'
1126 )
1127 )
1127 ui.warn(onfailure % fduipath)
1128 ui.warn(onfailure % fduipath)
1128 _onfilemergefailure(ui)
1129 _onfilemergefailure(ui)
1129
1130
1130 return r, deleted
1131 return r, deleted
1131 finally:
1132 finally:
1132 if not r and backup is not None:
1133 if not r and backup is not None:
1133 backup.remove()
1134 backup.remove()
1134
1135
1135
1136
1136 def _run_partial_resolution_tools(repo, local, other, base):
1137 def _run_partial_resolution_tools(repo, local, other, base):
1137 """Runs partial-resolution tools on the three inputs and updates them."""
1138 """Runs partial-resolution tools on the three inputs and updates them."""
1138 ui = repo.ui
1139 ui = repo.ui
1139 if ui.configbool(b'merge', b'disable-partial-tools'):
1140 if ui.configbool(b'merge', b'disable-partial-tools'):
1140 return
1141 return
1141 # Tuples of (order, name, executable path, args)
1142 # Tuples of (order, name, executable path, args)
1142 tools = []
1143 tools = []
1143 seen = set()
1144 seen = set()
1144 section = b"partial-merge-tools"
1145 section = b"partial-merge-tools"
1145 for k, v in ui.configitems(section):
1146 for k, v in ui.configitems(section):
1146 name = k.split(b'.')[0]
1147 name = k.split(b'.')[0]
1147 if name in seen:
1148 if name in seen:
1148 continue
1149 continue
1149 patterns = ui.configlist(section, b'%s.patterns' % name, [])
1150 patterns = ui.configlist(section, b'%s.patterns' % name, [])
1150 is_match = True
1151 is_match = True
1151 if patterns:
1152 if patterns:
1152 m = match.match(
1153 m = match.match(
1153 repo.root, b'', patterns, ctx=local.fctx.changectx()
1154 repo.root, b'', patterns, ctx=local.fctx.changectx()
1154 )
1155 )
1155 is_match = m(local.fctx.path())
1156 is_match = m(local.fctx.path())
1156 if is_match:
1157 if is_match:
1157 if ui.configbool(section, b'%s.disable' % name):
1158 if ui.configbool(section, b'%s.disable' % name):
1158 continue
1159 continue
1159 order = ui.configint(section, b'%s.order' % name, 0)
1160 order = ui.configint(section, b'%s.order' % name, 0)
1160 executable = ui.config(section, b'%s.executable' % name, name)
1161 executable = ui.config(section, b'%s.executable' % name, name)
1161 args = ui.config(section, b'%s.args' % name)
1162 args = ui.config(section, b'%s.args' % name)
1162 tools.append((order, name, executable, args))
1163 tools.append((order, name, executable, args))
1163
1164
1164 if not tools:
1165 if not tools:
1165 return
1166 return
1166 # Sort in configured order (first in tuple)
1167 # Sort in configured order (first in tuple)
1167 tools.sort()
1168 tools.sort()
1168
1169
1169 files = [
1170 files = [
1170 (b"local", local.fctx.path(), local.text()),
1171 (b"local", local.fctx.path(), local.text()),
1171 (b"base", base.fctx.path(), base.text()),
1172 (b"base", base.fctx.path(), base.text()),
1172 (b"other", other.fctx.path(), other.text()),
1173 (b"other", other.fctx.path(), other.text()),
1173 ]
1174 ]
1174
1175
1175 with _maketempfiles(files) as temppaths:
1176 with _maketempfiles(files) as temppaths:
1176 localpath, basepath, otherpath = temppaths
1177 localpath, basepath, otherpath = temppaths
1177
1178
1178 for order, name, executable, args in tools:
1179 for order, name, executable, args in tools:
1179 cmd = procutil.shellquote(executable)
1180 cmd = procutil.shellquote(executable)
1180 replace = {
1181 replace = {
1181 b'local': localpath,
1182 b'local': localpath,
1182 b'base': basepath,
1183 b'base': basepath,
1183 b'other': otherpath,
1184 b'other': otherpath,
1184 }
1185 }
1185 args = util.interpolate(
1186 args = util.interpolate(
1186 br'\$',
1187 br'\$',
1187 replace,
1188 replace,
1188 args,
1189 args,
1189 lambda s: procutil.shellquote(util.localpath(s)),
1190 lambda s: procutil.shellquote(util.localpath(s)),
1190 )
1191 )
1191
1192
1192 cmd = b'%s %s' % (cmd, args)
1193 cmd = b'%s %s' % (cmd, args)
1193 r = ui.system(cmd, cwd=repo.root, blockedtag=b'partial-mergetool')
1194 r = ui.system(cmd, cwd=repo.root, blockedtag=b'partial-mergetool')
1194 if r:
1195 if r:
1195 raise error.StateError(
1196 raise error.StateError(
1196 b'partial merge tool %s exited with code %d' % (name, r)
1197 b'partial merge tool %s exited with code %d' % (name, r)
1197 )
1198 )
1198 local_text = util.readfile(localpath)
1199 local_text = util.readfile(localpath)
1199 other_text = util.readfile(otherpath)
1200 other_text = util.readfile(otherpath)
1200 if local_text == other_text:
1201 if local_text == other_text:
1201 # No need to run other tools if all conflicts have been resolved
1202 # No need to run other tools if all conflicts have been resolved
1202 break
1203 break
1203
1204
1204 local.set_text(local_text)
1205 local.set_text(local_text)
1205 base.set_text(util.readfile(basepath))
1206 base.set_text(util.readfile(basepath))
1206 other.set_text(other_text)
1207 other.set_text(other_text)
1207
1208
1208
1209
1209 def _haltmerge():
1210 def _haltmerge():
1210 msg = _(b'merge halted after failed merge (see hg resolve)')
1211 msg = _(b'merge halted after failed merge (see hg resolve)')
1211 raise error.InterventionRequired(msg)
1212 raise error.InterventionRequired(msg)
1212
1213
1213
1214
1214 def _onfilemergefailure(ui):
1215 def _onfilemergefailure(ui):
1215 action = ui.config(b'merge', b'on-failure')
1216 action = ui.config(b'merge', b'on-failure')
1216 if action == b'prompt':
1217 if action == b'prompt':
1217 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1218 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1218 if ui.promptchoice(msg, 0) == 1:
1219 if ui.promptchoice(msg, 0) == 1:
1219 _haltmerge()
1220 _haltmerge()
1220 if action == b'halt':
1221 if action == b'halt':
1221 _haltmerge()
1222 _haltmerge()
1222 # default action is 'continue', in which case we neither prompt nor halt
1223 # default action is 'continue', in which case we neither prompt nor halt
1223
1224
1224
1225
1225 def hasconflictmarkers(data):
1226 def hasconflictmarkers(data):
1226 # Detect lines starting with a string of 7 identical characters from the
1227 # Detect lines starting with a string of 7 identical characters from the
1227 # subset Mercurial uses for conflict markers, followed by either the end of
1228 # subset Mercurial uses for conflict markers, followed by either the end of
1228 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1229 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1229 # `<><><><><` as a conflict marker, which we don't want.
1230 # `<><><><><` as a conflict marker, which we don't want.
1230 return bool(
1231 return bool(
1231 re.search(
1232 re.search(
1232 br"^([<>=+|-])\1{6}( .*)$",
1233 br"^([<>=+|-])\1{6}( .*)$",
1233 data,
1234 data,
1234 re.MULTILINE,
1235 re.MULTILINE,
1235 )
1236 )
1236 )
1237 )
1237
1238
1238
1239
1239 def _check(repo, r, ui, tool, fcd, backup):
1240 def _check(repo, r, ui, tool, fcd, backup):
1240 fd = fcd.path()
1241 fd = fcd.path()
1241 uipathfn = scmutil.getuipathfn(repo)
1242 uipathfn = scmutil.getuipathfn(repo)
1242
1243
1243 if not r and (
1244 if not r and (
1244 _toolbool(ui, tool, b"checkconflicts")
1245 _toolbool(ui, tool, b"checkconflicts")
1245 or b'conflicts' in _toollist(ui, tool, b"check")
1246 or b'conflicts' in _toollist(ui, tool, b"check")
1246 ):
1247 ):
1247 if hasconflictmarkers(fcd.data()):
1248 if hasconflictmarkers(fcd.data()):
1248 r = 1
1249 r = 1
1249
1250
1250 checked = False
1251 checked = False
1251 if b'prompt' in _toollist(ui, tool, b"check"):
1252 if b'prompt' in _toollist(ui, tool, b"check"):
1252 checked = True
1253 checked = True
1253 if ui.promptchoice(
1254 if ui.promptchoice(
1254 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1255 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1255 % uipathfn(fd),
1256 % uipathfn(fd),
1256 1,
1257 1,
1257 ):
1258 ):
1258 r = 1
1259 r = 1
1259
1260
1260 if (
1261 if (
1261 not r
1262 not r
1262 and not checked
1263 and not checked
1263 and (
1264 and (
1264 _toolbool(ui, tool, b"checkchanged")
1265 _toolbool(ui, tool, b"checkchanged")
1265 or b'changed' in _toollist(ui, tool, b"check")
1266 or b'changed' in _toollist(ui, tool, b"check")
1266 )
1267 )
1267 ):
1268 ):
1268 if backup is not None and not fcd.cmp(backup):
1269 if backup is not None and not fcd.cmp(backup):
1269 if ui.promptchoice(
1270 if ui.promptchoice(
1270 _(
1271 _(
1271 b" output file %s appears unchanged\n"
1272 b" output file %s appears unchanged\n"
1272 b"was merge successful (yn)?"
1273 b"was merge successful (yn)?"
1273 b"$$ &Yes $$ &No"
1274 b"$$ &Yes $$ &No"
1274 )
1275 )
1275 % uipathfn(fd),
1276 % uipathfn(fd),
1276 1,
1277 1,
1277 ):
1278 ):
1278 r = 1
1279 r = 1
1279
1280
1280 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1281 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1281 _matcheol(_workingpath(repo, fcd), backup)
1282 _matcheol(_workingpath(repo, fcd), backup)
1282
1283
1283 return r
1284 return r
1284
1285
1285
1286
1286 def _workingpath(repo, ctx):
1287 def _workingpath(repo, ctx):
1287 return repo.wjoin(ctx.path())
1288 return repo.wjoin(ctx.path())
1288
1289
1289
1290
1290 def loadinternalmerge(ui, extname, registrarobj):
1291 def loadinternalmerge(ui, extname, registrarobj):
1291 """Load internal merge tool from specified registrarobj"""
1292 """Load internal merge tool from specified registrarobj"""
1292 for name, func in registrarobj._table.items():
1293 for name, func in registrarobj._table.items():
1293 fullname = b':' + name
1294 fullname = b':' + name
1294 internals[fullname] = func
1295 internals[fullname] = func
1295 internals[b'internal:' + name] = func
1296 internals[b'internal:' + name] = func
1296 internalsdoc[fullname] = func
1297 internalsdoc[fullname] = func
1297
1298
1298 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1299 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1299 if capabilities:
1300 if capabilities:
1300 capdesc = b" (actual capabilities: %s)" % b', '.join(
1301 capdesc = b" (actual capabilities: %s)" % b', '.join(
1301 capabilities
1302 capabilities
1302 )
1303 )
1303 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1304 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1304
1305
1305 # to put i18n comments into hg.pot for automatically generated texts
1306 # to put i18n comments into hg.pot for automatically generated texts
1306
1307
1307 # i18n: "binary" and "symlink" are keywords
1308 # i18n: "binary" and "symlink" are keywords
1308 # i18n: this text is added automatically
1309 # i18n: this text is added automatically
1309 _(b" (actual capabilities: binary, symlink)")
1310 _(b" (actual capabilities: binary, symlink)")
1310 # i18n: "binary" is keyword
1311 # i18n: "binary" is keyword
1311 # i18n: this text is added automatically
1312 # i18n: this text is added automatically
1312 _(b" (actual capabilities: binary)")
1313 _(b" (actual capabilities: binary)")
1313 # i18n: "symlink" is keyword
1314 # i18n: "symlink" is keyword
1314 # i18n: this text is added automatically
1315 # i18n: this text is added automatically
1315 _(b" (actual capabilities: symlink)")
1316 _(b" (actual capabilities: symlink)")
1316
1317
1317
1318
1318 # load built-in merge tools explicitly to setup internalsdoc
1319 # load built-in merge tools explicitly to setup internalsdoc
1319 loadinternalmerge(None, None, internaltool)
1320 loadinternalmerge(None, None, internaltool)
1320
1321
1321 # tell hggettext to extract docstrings from these functions:
1322 # tell hggettext to extract docstrings from these functions:
1322 i18nfunctions = internals.values()
1323 i18nfunctions = internals.values()
@@ -1,366 +1,371 b''
1 # hook.py - hook support for mercurial
1 # hook.py - hook support for mercurial
2 #
2 #
3 # Copyright 2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2007 Olivia Mackall <olivia@selenic.com>
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
8
9 import contextlib
9 import contextlib
10 import errno
10 import errno
11 import os
11 import os
12 import sys
12 import sys
13
13
14 from .i18n import _
14 from .i18n import _
15 from .pycompat import getattr
15 from .pycompat import getattr
16 from . import (
16 from . import (
17 demandimport,
17 demandimport,
18 encoding,
18 encoding,
19 error,
19 error,
20 extensions,
20 extensions,
21 pycompat,
21 pycompat,
22 util,
22 util,
23 )
23 )
24 from .utils import (
24 from .utils import (
25 procutil,
25 procutil,
26 resourceutil,
26 resourceutil,
27 stringutil,
27 stringutil,
28 )
28 )
29
29
30
30
31 def pythonhook(ui, repo, htype, hname, funcname, args, throw):
31 def pythonhook(ui, repo, htype, hname, funcname, args, throw):
32 """call python hook. hook is callable object, looked up as
32 """call python hook. hook is callable object, looked up as
33 name in python module. if callable returns "true", hook
33 name in python module. if callable returns "true", hook
34 fails, else passes. if hook raises exception, treated as
34 fails, else passes. if hook raises exception, treated as
35 hook failure. exception propagates if throw is "true".
35 hook failure. exception propagates if throw is "true".
36
36
37 reason for "true" meaning "hook failed" is so that
37 reason for "true" meaning "hook failed" is so that
38 unmodified commands (e.g. mercurial.commands.update) can
38 unmodified commands (e.g. mercurial.commands.update) can
39 be run as hooks without wrappers to convert return values."""
39 be run as hooks without wrappers to convert return values."""
40
40
41 if callable(funcname):
41 if callable(funcname):
42 obj = funcname
42 obj = funcname
43 funcname = pycompat.sysbytes(obj.__module__ + "." + obj.__name__)
43 funcname = obj.__module__ + "." + obj.__name__
44 else:
44 else:
45 d = funcname.rfind(b'.')
45 funcname = pycompat.sysstr(funcname)
46 d = funcname.rfind('.')
46 if d == -1:
47 if d == -1:
47 raise error.HookLoadError(
48 raise error.HookLoadError(
48 _(b'%s hook is invalid: "%s" not in a module')
49 _(b'%s hook is invalid: "%s" not in a module')
49 % (hname, funcname)
50 % (hname, stringutil.forcebytestr(funcname))
50 )
51 )
51 modname = funcname[:d]
52 modname = funcname[:d]
52 oldpaths = sys.path
53 oldpaths = sys.path
53 if resourceutil.mainfrozen():
54 if resourceutil.mainfrozen():
54 # binary installs require sys.path manipulation
55 # binary installs require sys.path manipulation
55 modpath, modfile = os.path.split(modname)
56 modpath, modfile = os.path.split(modname)
56 if modpath and modfile:
57 if modpath and modfile:
57 sys.path = sys.path[:] + [modpath]
58 sys.path = sys.path[:] + [modpath]
58 modname = modfile
59 modname = modfile
59 with demandimport.deactivated():
60 with demandimport.deactivated():
60 try:
61 try:
61 obj = __import__(pycompat.sysstr(modname))
62 obj = __import__(pycompat.sysstr(modname))
62 except (ImportError, SyntaxError):
63 except (ImportError, SyntaxError):
63 e1 = sys.exc_info()
64 e1 = sys.exc_info()
64 try:
65 try:
65 # extensions are loaded with hgext_ prefix
66 # extensions are loaded with hgext_ prefix
66 obj = __import__("hgext_%s" % pycompat.sysstr(modname))
67 obj = __import__("hgext_%s" % pycompat.sysstr(modname))
67 except (ImportError, SyntaxError):
68 except (ImportError, SyntaxError):
68 e2 = sys.exc_info()
69 e2 = sys.exc_info()
69 if ui.tracebackflag:
70 if ui.tracebackflag:
70 ui.warn(
71 ui.warn(
71 _(
72 _(
72 b'exception from first failed import '
73 b'exception from first failed import '
73 b'attempt:\n'
74 b'attempt:\n'
74 )
75 )
75 )
76 )
76 ui.traceback(e1)
77 ui.traceback(e1)
77 if ui.tracebackflag:
78 if ui.tracebackflag:
78 ui.warn(
79 ui.warn(
79 _(
80 _(
80 b'exception from second failed import '
81 b'exception from second failed import '
81 b'attempt:\n'
82 b'attempt:\n'
82 )
83 )
83 )
84 )
84 ui.traceback(e2)
85 ui.traceback(e2)
85
86
86 if not ui.tracebackflag:
87 if not ui.tracebackflag:
87 tracebackhint = _(
88 tracebackhint = _(
88 b'run with --traceback for stack trace'
89 b'run with --traceback for stack trace'
89 )
90 )
90 else:
91 else:
91 tracebackhint = None
92 tracebackhint = None
92 raise error.HookLoadError(
93 msg = _(b'%s hook is invalid: import of "%s" failed')
93 _(b'%s hook is invalid: import of "%s" failed')
94 msg %= (
94 % (hname, modname),
95 stringutil.forcebytestr(hname),
95 hint=tracebackhint,
96 stringutil.forcebytestr(modname),
96 )
97 )
98 raise error.HookLoadError(msg, hint=tracebackhint)
97 sys.path = oldpaths
99 sys.path = oldpaths
98 try:
100 try:
99 for p in funcname.split(b'.')[1:]:
101 for p in funcname.split('.')[1:]:
100 obj = getattr(obj, p)
102 obj = getattr(obj, p)
101 except AttributeError:
103 except AttributeError:
102 raise error.HookLoadError(
104 raise error.HookLoadError(
103 _(b'%s hook is invalid: "%s" is not defined')
105 _(b'%s hook is invalid: "%s" is not defined')
104 % (hname, funcname)
106 % (hname, stringutil.forcebytestr(funcname))
105 )
107 )
106 if not callable(obj):
108 if not callable(obj):
107 raise error.HookLoadError(
109 raise error.HookLoadError(
108 _(b'%s hook is invalid: "%s" is not callable')
110 _(b'%s hook is invalid: "%s" is not callable')
109 % (hname, funcname)
111 % (hname, stringutil.forcebytestr(funcname))
110 )
112 )
111
113
112 ui.note(_(b"calling hook %s: %s\n") % (hname, funcname))
114 ui.note(
115 _(b"calling hook %s: %s\n") % (hname, stringutil.forcebytestr(funcname))
116 )
113 starttime = util.timer()
117 starttime = util.timer()
114
118
115 try:
119 try:
116 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
120 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
117 except Exception as exc:
121 except Exception as exc:
118 if isinstance(exc, error.Abort):
122 if isinstance(exc, error.Abort):
119 ui.warn(_(b'error: %s hook failed: %s\n') % (hname, exc.args[0]))
123 ui.warn(_(b'error: %s hook failed: %s\n') % (hname, exc.args[0]))
120 else:
124 else:
121 ui.warn(
125 ui.warn(
122 _(b'error: %s hook raised an exception: %s\n')
126 _(b'error: %s hook raised an exception: %s\n')
123 % (hname, stringutil.forcebytestr(exc))
127 % (hname, stringutil.forcebytestr(exc))
124 )
128 )
125 if throw:
129 if throw:
126 raise
130 raise
127 if not ui.tracebackflag:
131 if not ui.tracebackflag:
128 ui.warn(_(b'(run with --traceback for stack trace)\n'))
132 ui.warn(_(b'(run with --traceback for stack trace)\n'))
129 ui.traceback()
133 ui.traceback()
130 return True, True
134 return True, True
131 finally:
135 finally:
132 duration = util.timer() - starttime
136 duration = util.timer() - starttime
133 ui.log(
137 ui.log(
134 b'pythonhook',
138 b'pythonhook',
135 b'pythonhook-%s: %s finished in %0.2f seconds\n',
139 b'pythonhook-%s: %s finished in %0.2f seconds\n',
136 htype,
140 htype,
137 funcname,
141 stringutil.forcebytestr(funcname),
138 duration,
142 duration,
139 )
143 )
140 if r:
144 if r:
141 if throw:
145 if throw:
142 raise error.HookAbort(_(b'%s hook failed') % hname)
146 raise error.HookAbort(_(b'%s hook failed') % hname)
143 ui.warn(_(b'warning: %s hook failed\n') % hname)
147 ui.warn(_(b'warning: %s hook failed\n') % hname)
144 return r, False
148 return r, False
145
149
146
150
147 def _exthook(ui, repo, htype, name, cmd, args, throw):
151 def _exthook(ui, repo, htype, name, cmd, args, throw):
148 starttime = util.timer()
152 starttime = util.timer()
149 env = {}
153 env = {}
150
154
151 # make in-memory changes visible to external process
155 # make in-memory changes visible to external process
152 if repo is not None:
156 if repo is not None:
153 tr = repo.currenttransaction()
157 tr = repo.currenttransaction()
154 repo.dirstate.write(tr)
158 repo.dirstate.write(tr)
155 if tr and tr.writepending():
159 if tr and tr.writepending():
156 env[b'HG_PENDING'] = repo.root
160 env[b'HG_PENDING'] = repo.root
157 env[b'HG_HOOKTYPE'] = htype
161 env[b'HG_HOOKTYPE'] = htype
158 env[b'HG_HOOKNAME'] = name
162 env[b'HG_HOOKNAME'] = name
159
163
160 if ui.config(b'hooks', b'%s:run-with-plain' % name) == b'auto':
164 if ui.config(b'hooks', b'%s:run-with-plain' % name) == b'auto':
161 plain = ui.plain()
165 plain = ui.plain()
162 else:
166 else:
163 plain = ui.configbool(b'hooks', b'%s:run-with-plain' % name)
167 plain = ui.configbool(b'hooks', b'%s:run-with-plain' % name)
164 if plain:
168 if plain:
165 env[b'HGPLAIN'] = b'1'
169 env[b'HGPLAIN'] = b'1'
166 else:
170 else:
167 env[b'HGPLAIN'] = b''
171 env[b'HGPLAIN'] = b''
168
172
169 for k, v in args.items():
173 for k, v in args.items():
170 # transaction changes can accumulate MBs of data, so skip it
174 # transaction changes can accumulate MBs of data, so skip it
171 # for external hooks
175 # for external hooks
172 if k == b'changes':
176 if k == b'changes':
173 continue
177 continue
174 if callable(v):
178 if callable(v):
175 v = v()
179 v = v()
176 if isinstance(v, (dict, list)):
180 if isinstance(v, (dict, list)):
177 v = stringutil.pprint(v)
181 v = stringutil.pprint(v)
178 env[b'HG_' + k.upper()] = v
182 env[b'HG_' + k.upper()] = v
179
183
180 if ui.configbool(b'hooks', b'tonative.%s' % name, False):
184 if ui.configbool(b'hooks', b'tonative.%s' % name, False):
181 oldcmd = cmd
185 oldcmd = cmd
182 cmd = procutil.shelltonative(cmd, env)
186 cmd = procutil.shelltonative(cmd, env)
183 if cmd != oldcmd:
187 if cmd != oldcmd:
184 ui.note(_(b'converting hook "%s" to native\n') % name)
188 ui.note(_(b'converting hook "%s" to native\n') % name)
185
189
186 ui.note(_(b"running hook %s: %s\n") % (name, cmd))
190 ui.note(_(b"running hook %s: %s\n") % (name, cmd))
187
191
188 if repo:
192 if repo:
189 cwd = repo.root
193 cwd = repo.root
190 else:
194 else:
191 cwd = encoding.getcwd()
195 cwd = encoding.getcwd()
192 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag=b'exthook-%s' % (name,))
196 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag=b'exthook-%s' % (name,))
193
197
194 if repo is not None and repo.currentwlock() is None:
198 if repo is not None and repo.currentwlock() is None:
195 repo.invalidatedirstate()
199 repo.invalidatedirstate()
196 if repo is not None and repo.currentlock() is None:
200 if repo is not None and repo.currentlock() is None:
197 repo.invalidate()
201 repo.invalidate()
198
202
199 duration = util.timer() - starttime
203 duration = util.timer() - starttime
200 ui.log(
204 ui.log(
201 b'exthook',
205 b'exthook',
202 b'exthook-%s: %s finished in %0.2f seconds\n',
206 b'exthook-%s: %s finished in %0.2f seconds\n',
203 name,
207 name,
204 cmd,
208 cmd,
205 duration,
209 duration,
206 )
210 )
207 if r:
211 if r:
208 desc = procutil.explainexit(r)
212 desc = procutil.explainexit(r)
209 if throw:
213 if throw:
210 raise error.HookAbort(_(b'%s hook %s') % (name, desc))
214 raise error.HookAbort(_(b'%s hook %s') % (name, desc))
211 ui.warn(_(b'warning: %s hook %s\n') % (name, desc))
215 ui.warn(_(b'warning: %s hook %s\n') % (name, desc))
212 return r
216 return r
213
217
214
218
215 # represent an untrusted hook command
219 # represent an untrusted hook command
216 _fromuntrusted = object()
220 _fromuntrusted = object()
217
221
218
222
219 def _allhooks(ui):
223 def _allhooks(ui):
220 """return a list of (hook-id, cmd) pairs sorted by priority"""
224 """return a list of (hook-id, cmd) pairs sorted by priority"""
221 hooks = _hookitems(ui)
225 hooks = _hookitems(ui)
222 # Be careful in this section, propagating the real commands from untrusted
226 # Be careful in this section, propagating the real commands from untrusted
223 # sources would create a security vulnerability, make sure anything altered
227 # sources would create a security vulnerability, make sure anything altered
224 # in that section uses "_fromuntrusted" as its command.
228 # in that section uses "_fromuntrusted" as its command.
225 untrustedhooks = _hookitems(ui, _untrusted=True)
229 untrustedhooks = _hookitems(ui, _untrusted=True)
226 for name, value in untrustedhooks.items():
230 for name, value in untrustedhooks.items():
227 trustedvalue = hooks.get(name, ((), (), name, _fromuntrusted))
231 trustedvalue = hooks.get(name, ((), (), name, _fromuntrusted))
228 if value != trustedvalue:
232 if value != trustedvalue:
229 (lp, lo, lk, lv) = trustedvalue
233 (lp, lo, lk, lv) = trustedvalue
230 hooks[name] = (lp, lo, lk, _fromuntrusted)
234 hooks[name] = (lp, lo, lk, _fromuntrusted)
231 # (end of the security sensitive section)
235 # (end of the security sensitive section)
232 return [(k, v) for p, o, k, v in sorted(hooks.values())]
236 return [(k, v) for p, o, k, v in sorted(hooks.values())]
233
237
234
238
235 def _hookitems(ui, _untrusted=False):
239 def _hookitems(ui, _untrusted=False):
236 """return all hooks items ready to be sorted"""
240 """return all hooks items ready to be sorted"""
237 hooks = {}
241 hooks = {}
238 for name, cmd in ui.configitems(b'hooks', untrusted=_untrusted):
242 for name, cmd in ui.configitems(b'hooks', untrusted=_untrusted):
239 if (
243 if (
240 name.startswith(b'priority.')
244 name.startswith(b'priority.')
241 or name.startswith(b'tonative.')
245 or name.startswith(b'tonative.')
242 or b':' in name
246 or b':' in name
243 ):
247 ):
244 continue
248 continue
245
249
246 priority = ui.configint(b'hooks', b'priority.%s' % name, 0)
250 priority = ui.configint(b'hooks', b'priority.%s' % name, 0)
247 hooks[name] = ((-priority,), (len(hooks),), name, cmd)
251 hooks[name] = ((-priority,), (len(hooks),), name, cmd)
248 return hooks
252 return hooks
249
253
250
254
251 _redirect = False
255 _redirect = False
252
256
253
257
254 def redirect(state):
258 def redirect(state):
255 global _redirect
259 global _redirect
256 _redirect = state
260 _redirect = state
257
261
258
262
259 def hashook(ui, htype):
263 def hashook(ui, htype):
260 """return True if a hook is configured for 'htype'"""
264 """return True if a hook is configured for 'htype'"""
261 if not ui.callhooks:
265 if not ui.callhooks:
262 return False
266 return False
263 for hname, cmd in _allhooks(ui):
267 for hname, cmd in _allhooks(ui):
264 if hname.split(b'.')[0] == htype and cmd:
268 if hname.split(b'.')[0] == htype and cmd:
265 return True
269 return True
266 return False
270 return False
267
271
268
272
269 def hook(ui, repo, htype, throw=False, **args):
273 def hook(ui, repo, htype, throw=False, **args):
270 if not ui.callhooks:
274 if not ui.callhooks:
271 return False
275 return False
272
276
273 hooks = []
277 hooks = []
274 for hname, cmd in _allhooks(ui):
278 for hname, cmd in _allhooks(ui):
275 if hname.split(b'.')[0] == htype and cmd:
279 if hname.split(b'.')[0] == htype and cmd:
276 hooks.append((hname, cmd))
280 hooks.append((hname, cmd))
277
281
278 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
282 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
279 r = False
283 r = False
280 for hname, cmd in hooks:
284 for hname, cmd in hooks:
281 r = res[hname][0] or r
285 r = res[hname][0] or r
282 return r
286 return r
283
287
284
288
285 @contextlib.contextmanager
289 @contextlib.contextmanager
286 def redirect_stdio():
290 def redirect_stdio():
287 """Redirects stdout to stderr, if possible."""
291 """Redirects stdout to stderr, if possible."""
288
292
289 oldstdout = -1
293 oldstdout = -1
290 try:
294 try:
291 if _redirect:
295 if _redirect:
292 try:
296 try:
293 stdoutno = procutil.stdout.fileno()
297 stdoutno = procutil.stdout.fileno()
294 stderrno = procutil.stderr.fileno()
298 stderrno = procutil.stderr.fileno()
295 # temporarily redirect stdout to stderr, if possible
299 # temporarily redirect stdout to stderr, if possible
296 if stdoutno >= 0 and stderrno >= 0:
300 if stdoutno >= 0 and stderrno >= 0:
297 procutil.stdout.flush()
301 procutil.stdout.flush()
298 oldstdout = os.dup(stdoutno)
302 oldstdout = os.dup(stdoutno)
299 os.dup2(stderrno, stdoutno)
303 os.dup2(stderrno, stdoutno)
300 except (OSError, AttributeError):
304 except (OSError, AttributeError):
301 # files seem to be bogus, give up on redirecting (WSGI, etc)
305 # files seem to be bogus, give up on redirecting (WSGI, etc)
302 pass
306 pass
303
307
304 yield
308 yield
305
309
306 finally:
310 finally:
307 # The stderr is fully buffered on Windows when connected to a pipe.
311 # The stderr is fully buffered on Windows when connected to a pipe.
308 # A forcible flush is required to make small stderr data in the
312 # A forcible flush is required to make small stderr data in the
309 # remote side available to the client immediately.
313 # remote side available to the client immediately.
310 try:
314 try:
311 procutil.stderr.flush()
315 procutil.stderr.flush()
312 except IOError as err:
316 except IOError as err:
313 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
317 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
314 raise error.StdioError(err)
318 raise error.StdioError(err)
315
319
316 if _redirect and oldstdout >= 0:
320 if _redirect and oldstdout >= 0:
317 try:
321 try:
318 procutil.stdout.flush() # write hook output to stderr fd
322 procutil.stdout.flush() # write hook output to stderr fd
319 except IOError as err:
323 except IOError as err:
320 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
324 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
321 raise error.StdioError(err)
325 raise error.StdioError(err)
322 os.dup2(oldstdout, stdoutno)
326 os.dup2(oldstdout, stdoutno)
323 os.close(oldstdout)
327 os.close(oldstdout)
324
328
325
329
326 def runhooks(ui, repo, htype, hooks, throw=False, **args):
330 def runhooks(ui, repo, htype, hooks, throw=False, **args):
327 args = pycompat.byteskwargs(args)
331 args = pycompat.byteskwargs(args)
328 res = {}
332 res = {}
329
333
330 with redirect_stdio():
334 with redirect_stdio():
331 for hname, cmd in hooks:
335 for hname, cmd in hooks:
332 if cmd is _fromuntrusted:
336 if cmd is _fromuntrusted:
333 if throw:
337 if throw:
334 raise error.HookAbort(
338 raise error.HookAbort(
335 _(b'untrusted hook %s not executed') % hname,
339 _(b'untrusted hook %s not executed') % hname,
336 hint=_(b"see 'hg help config.trusted'"),
340 hint=_(b"see 'hg help config.trusted'"),
337 )
341 )
338 ui.warn(_(b'warning: untrusted hook %s not executed\n') % hname)
342 ui.warn(_(b'warning: untrusted hook %s not executed\n') % hname)
339 r = 1
343 r = 1
340 raised = False
344 raised = False
341 elif callable(cmd):
345 elif callable(cmd):
342 r, raised = pythonhook(ui, repo, htype, hname, cmd, args, throw)
346 r, raised = pythonhook(ui, repo, htype, hname, cmd, args, throw)
343 elif cmd.startswith(b'python:'):
347 elif cmd.startswith(b'python:'):
344 if cmd.count(b':') >= 2:
348 if cmd.count(b':') >= 2:
345 path, cmd = cmd[7:].rsplit(b':', 1)
349 path, cmd = cmd[7:].rsplit(b':', 1)
346 path = util.expandpath(path)
350 path = util.expandpath(path)
347 if repo:
351 if repo:
348 path = os.path.join(repo.root, path)
352 path = os.path.join(repo.root, path)
349 try:
353 try:
350 mod = extensions.loadpath(path, b'hghook.%s' % hname)
354 mod_name = 'hghook.%s' % pycompat.sysstr(hname)
355 mod = extensions.loadpath(path, mod_name)
351 except Exception:
356 except Exception:
352 ui.write(_(b"loading %s hook failed:\n") % hname)
357 ui.write(_(b"loading %s hook failed:\n") % hname)
353 raise
358 raise
354 hookfn = getattr(mod, cmd)
359 hookfn = getattr(mod, pycompat.sysstr(cmd))
355 else:
360 else:
356 hookfn = cmd[7:].strip()
361 hookfn = cmd[7:].strip()
357 r, raised = pythonhook(
362 r, raised = pythonhook(
358 ui, repo, htype, hname, hookfn, args, throw
363 ui, repo, htype, hname, hookfn, args, throw
359 )
364 )
360 else:
365 else:
361 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
366 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
362 raised = False
367 raised = False
363
368
364 res[hname] = r, raised
369 res[hname] = r, raised
365
370
366 return res
371 return res
@@ -1,1463 +1,1463 b''
1 commit hooks can see env vars
1 commit hooks can see env vars
2 (and post-transaction one are run unlocked)
2 (and post-transaction one are run unlocked)
3
3
4
4
5 $ cat > $TESTTMP/txnabort.checkargs.py <<EOF
5 $ cat > $TESTTMP/txnabort.checkargs.py <<EOF
6 > from mercurial import pycompat
6 > from mercurial import pycompat
7 > def showargs(ui, repo, hooktype, **kwargs):
7 > def showargs(ui, repo, hooktype, **kwargs):
8 > kwargs = pycompat.byteskwargs(kwargs)
8 > kwargs = pycompat.byteskwargs(kwargs)
9 > ui.write(b'%s Python hook: %s\n' % (hooktype,
9 > ui.write(b'%s Python hook: %s\n' % (hooktype,
10 > b','.join(sorted(kwargs))))
10 > b','.join(sorted(kwargs))))
11 > EOF
11 > EOF
12
12
13 $ hg init a
13 $ hg init a
14 $ cd a
14 $ cd a
15 $ cat > .hg/hgrc <<EOF
15 $ cat > .hg/hgrc <<EOF
16 > [hooks]
16 > [hooks]
17 > commit = sh -c "HG_LOCAL= HG_TAG= printenv.py --line commit"
17 > commit = sh -c "HG_LOCAL= HG_TAG= printenv.py --line commit"
18 > commit.b = sh -c "HG_LOCAL= HG_TAG= printenv.py --line commit.b"
18 > commit.b = sh -c "HG_LOCAL= HG_TAG= printenv.py --line commit.b"
19 > precommit = sh -c "HG_LOCAL= HG_NODE= HG_TAG= printenv.py --line precommit"
19 > precommit = sh -c "HG_LOCAL= HG_NODE= HG_TAG= printenv.py --line precommit"
20 > pretxncommit = sh -c "HG_LOCAL= HG_TAG= printenv.py --line pretxncommit"
20 > pretxncommit = sh -c "HG_LOCAL= HG_TAG= printenv.py --line pretxncommit"
21 > pretxncommit.tip = hg -q tip
21 > pretxncommit.tip = hg -q tip
22 > pre-identify = sh -c "printenv.py --line pre-identify 1"
22 > pre-identify = sh -c "printenv.py --line pre-identify 1"
23 > pre-cat = sh -c "printenv.py --line pre-cat"
23 > pre-cat = sh -c "printenv.py --line pre-cat"
24 > post-cat = sh -c "printenv.py --line post-cat"
24 > post-cat = sh -c "printenv.py --line post-cat"
25 > pretxnopen = sh -c "HG_LOCAL= HG_TAG= printenv.py --line pretxnopen"
25 > pretxnopen = sh -c "HG_LOCAL= HG_TAG= printenv.py --line pretxnopen"
26 > pretxnclose = sh -c "HG_LOCAL= HG_TAG= printenv.py --line pretxnclose"
26 > pretxnclose = sh -c "HG_LOCAL= HG_TAG= printenv.py --line pretxnclose"
27 > txnclose = sh -c "HG_LOCAL= HG_TAG= printenv.py --line txnclose"
27 > txnclose = sh -c "HG_LOCAL= HG_TAG= printenv.py --line txnclose"
28 > txnabort.0 = python:$TESTTMP/txnabort.checkargs.py:showargs
28 > txnabort.0 = python:$TESTTMP/txnabort.checkargs.py:showargs
29 > txnabort.1 = sh -c "HG_LOCAL= HG_TAG= printenv.py --line txnabort"
29 > txnabort.1 = sh -c "HG_LOCAL= HG_TAG= printenv.py --line txnabort"
30 > txnclose.checklock = sh -c "hg debuglock > /dev/null"
30 > txnclose.checklock = sh -c "hg debuglock > /dev/null"
31 > EOF
31 > EOF
32 $ echo a > a
32 $ echo a > a
33 $ hg add a
33 $ hg add a
34 $ hg commit -m a
34 $ hg commit -m a
35 precommit hook: HG_HOOKNAME=precommit
35 precommit hook: HG_HOOKNAME=precommit
36 HG_HOOKTYPE=precommit
36 HG_HOOKTYPE=precommit
37 HG_PARENT1=0000000000000000000000000000000000000000
37 HG_PARENT1=0000000000000000000000000000000000000000
38
38
39 pretxnopen hook: HG_HOOKNAME=pretxnopen
39 pretxnopen hook: HG_HOOKNAME=pretxnopen
40 HG_HOOKTYPE=pretxnopen
40 HG_HOOKTYPE=pretxnopen
41 HG_TXNID=TXN:$ID$
41 HG_TXNID=TXN:$ID$
42 HG_TXNNAME=commit
42 HG_TXNNAME=commit
43
43
44 pretxncommit hook: HG_HOOKNAME=pretxncommit
44 pretxncommit hook: HG_HOOKNAME=pretxncommit
45 HG_HOOKTYPE=pretxncommit
45 HG_HOOKTYPE=pretxncommit
46 HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
46 HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
47 HG_PARENT1=0000000000000000000000000000000000000000
47 HG_PARENT1=0000000000000000000000000000000000000000
48 HG_PENDING=$TESTTMP/a
48 HG_PENDING=$TESTTMP/a
49
49
50 0:cb9a9f314b8b
50 0:cb9a9f314b8b
51 pretxnclose hook: HG_HOOKNAME=pretxnclose
51 pretxnclose hook: HG_HOOKNAME=pretxnclose
52 HG_HOOKTYPE=pretxnclose
52 HG_HOOKTYPE=pretxnclose
53 HG_PENDING=$TESTTMP/a
53 HG_PENDING=$TESTTMP/a
54 HG_PHASES_MOVED=1
54 HG_PHASES_MOVED=1
55 HG_TXNID=TXN:$ID$
55 HG_TXNID=TXN:$ID$
56 HG_TXNNAME=commit
56 HG_TXNNAME=commit
57
57
58 txnclose hook: HG_HOOKNAME=txnclose
58 txnclose hook: HG_HOOKNAME=txnclose
59 HG_HOOKTYPE=txnclose
59 HG_HOOKTYPE=txnclose
60 HG_PHASES_MOVED=1
60 HG_PHASES_MOVED=1
61 HG_TXNID=TXN:$ID$
61 HG_TXNID=TXN:$ID$
62 HG_TXNNAME=commit
62 HG_TXNNAME=commit
63
63
64 commit hook: HG_HOOKNAME=commit
64 commit hook: HG_HOOKNAME=commit
65 HG_HOOKTYPE=commit
65 HG_HOOKTYPE=commit
66 HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
66 HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
67 HG_PARENT1=0000000000000000000000000000000000000000
67 HG_PARENT1=0000000000000000000000000000000000000000
68
68
69 commit.b hook: HG_HOOKNAME=commit.b
69 commit.b hook: HG_HOOKNAME=commit.b
70 HG_HOOKTYPE=commit
70 HG_HOOKTYPE=commit
71 HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
71 HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
72 HG_PARENT1=0000000000000000000000000000000000000000
72 HG_PARENT1=0000000000000000000000000000000000000000
73
73
74
74
75 $ hg clone . ../b
75 $ hg clone . ../b
76 updating to branch default
76 updating to branch default
77 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
77 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 $ cd ../b
78 $ cd ../b
79
79
80 changegroup hooks can see env vars
80 changegroup hooks can see env vars
81
81
82 $ cat > .hg/hgrc <<EOF
82 $ cat > .hg/hgrc <<EOF
83 > [hooks]
83 > [hooks]
84 > prechangegroup = sh -c "printenv.py --line prechangegroup"
84 > prechangegroup = sh -c "printenv.py --line prechangegroup"
85 > changegroup = sh -c "printenv.py --line changegroup"
85 > changegroup = sh -c "printenv.py --line changegroup"
86 > incoming = sh -c "printenv.py --line incoming"
86 > incoming = sh -c "printenv.py --line incoming"
87 > EOF
87 > EOF
88
88
89 pretxncommit and commit hooks can see both parents of merge
89 pretxncommit and commit hooks can see both parents of merge
90
90
91 $ cd ../a
91 $ cd ../a
92 $ echo b >> a
92 $ echo b >> a
93 $ hg commit -m a1 -d "1 0"
93 $ hg commit -m a1 -d "1 0"
94 precommit hook: HG_HOOKNAME=precommit
94 precommit hook: HG_HOOKNAME=precommit
95 HG_HOOKTYPE=precommit
95 HG_HOOKTYPE=precommit
96 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
96 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
97
97
98 pretxnopen hook: HG_HOOKNAME=pretxnopen
98 pretxnopen hook: HG_HOOKNAME=pretxnopen
99 HG_HOOKTYPE=pretxnopen
99 HG_HOOKTYPE=pretxnopen
100 HG_TXNID=TXN:$ID$
100 HG_TXNID=TXN:$ID$
101 HG_TXNNAME=commit
101 HG_TXNNAME=commit
102
102
103 pretxncommit hook: HG_HOOKNAME=pretxncommit
103 pretxncommit hook: HG_HOOKNAME=pretxncommit
104 HG_HOOKTYPE=pretxncommit
104 HG_HOOKTYPE=pretxncommit
105 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
105 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
106 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
106 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
107 HG_PENDING=$TESTTMP/a
107 HG_PENDING=$TESTTMP/a
108
108
109 1:ab228980c14d
109 1:ab228980c14d
110 pretxnclose hook: HG_HOOKNAME=pretxnclose
110 pretxnclose hook: HG_HOOKNAME=pretxnclose
111 HG_HOOKTYPE=pretxnclose
111 HG_HOOKTYPE=pretxnclose
112 HG_PENDING=$TESTTMP/a
112 HG_PENDING=$TESTTMP/a
113 HG_TXNID=TXN:$ID$
113 HG_TXNID=TXN:$ID$
114 HG_TXNNAME=commit
114 HG_TXNNAME=commit
115
115
116 txnclose hook: HG_HOOKNAME=txnclose
116 txnclose hook: HG_HOOKNAME=txnclose
117 HG_HOOKTYPE=txnclose
117 HG_HOOKTYPE=txnclose
118 HG_TXNID=TXN:$ID$
118 HG_TXNID=TXN:$ID$
119 HG_TXNNAME=commit
119 HG_TXNNAME=commit
120
120
121 commit hook: HG_HOOKNAME=commit
121 commit hook: HG_HOOKNAME=commit
122 HG_HOOKTYPE=commit
122 HG_HOOKTYPE=commit
123 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
123 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
124 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
124 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
125
125
126 commit.b hook: HG_HOOKNAME=commit.b
126 commit.b hook: HG_HOOKNAME=commit.b
127 HG_HOOKTYPE=commit
127 HG_HOOKTYPE=commit
128 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
128 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
129 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
129 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
130
130
131 $ hg update -C 0
131 $ hg update -C 0
132 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
132 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
133 $ echo b > b
133 $ echo b > b
134 $ hg add b
134 $ hg add b
135 $ hg commit -m b -d '1 0'
135 $ hg commit -m b -d '1 0'
136 precommit hook: HG_HOOKNAME=precommit
136 precommit hook: HG_HOOKNAME=precommit
137 HG_HOOKTYPE=precommit
137 HG_HOOKTYPE=precommit
138 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
138 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
139
139
140 pretxnopen hook: HG_HOOKNAME=pretxnopen
140 pretxnopen hook: HG_HOOKNAME=pretxnopen
141 HG_HOOKTYPE=pretxnopen
141 HG_HOOKTYPE=pretxnopen
142 HG_TXNID=TXN:$ID$
142 HG_TXNID=TXN:$ID$
143 HG_TXNNAME=commit
143 HG_TXNNAME=commit
144
144
145 pretxncommit hook: HG_HOOKNAME=pretxncommit
145 pretxncommit hook: HG_HOOKNAME=pretxncommit
146 HG_HOOKTYPE=pretxncommit
146 HG_HOOKTYPE=pretxncommit
147 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
147 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
148 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
148 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
149 HG_PENDING=$TESTTMP/a
149 HG_PENDING=$TESTTMP/a
150
150
151 2:ee9deb46ab31
151 2:ee9deb46ab31
152 pretxnclose hook: HG_HOOKNAME=pretxnclose
152 pretxnclose hook: HG_HOOKNAME=pretxnclose
153 HG_HOOKTYPE=pretxnclose
153 HG_HOOKTYPE=pretxnclose
154 HG_PENDING=$TESTTMP/a
154 HG_PENDING=$TESTTMP/a
155 HG_TXNID=TXN:$ID$
155 HG_TXNID=TXN:$ID$
156 HG_TXNNAME=commit
156 HG_TXNNAME=commit
157
157
158 created new head
158 created new head
159 txnclose hook: HG_HOOKNAME=txnclose
159 txnclose hook: HG_HOOKNAME=txnclose
160 HG_HOOKTYPE=txnclose
160 HG_HOOKTYPE=txnclose
161 HG_TXNID=TXN:$ID$
161 HG_TXNID=TXN:$ID$
162 HG_TXNNAME=commit
162 HG_TXNNAME=commit
163
163
164 commit hook: HG_HOOKNAME=commit
164 commit hook: HG_HOOKNAME=commit
165 HG_HOOKTYPE=commit
165 HG_HOOKTYPE=commit
166 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
166 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
167 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
167 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
168
168
169 commit.b hook: HG_HOOKNAME=commit.b
169 commit.b hook: HG_HOOKNAME=commit.b
170 HG_HOOKTYPE=commit
170 HG_HOOKTYPE=commit
171 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
171 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
172 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
172 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
173
173
174 $ hg merge 1
174 $ hg merge 1
175 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
175 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
176 (branch merge, don't forget to commit)
176 (branch merge, don't forget to commit)
177 $ hg commit -m merge -d '2 0'
177 $ hg commit -m merge -d '2 0'
178 precommit hook: HG_HOOKNAME=precommit
178 precommit hook: HG_HOOKNAME=precommit
179 HG_HOOKTYPE=precommit
179 HG_HOOKTYPE=precommit
180 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
180 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
181 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
181 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
182
182
183 pretxnopen hook: HG_HOOKNAME=pretxnopen
183 pretxnopen hook: HG_HOOKNAME=pretxnopen
184 HG_HOOKTYPE=pretxnopen
184 HG_HOOKTYPE=pretxnopen
185 HG_TXNID=TXN:$ID$
185 HG_TXNID=TXN:$ID$
186 HG_TXNNAME=commit
186 HG_TXNNAME=commit
187
187
188 pretxncommit hook: HG_HOOKNAME=pretxncommit
188 pretxncommit hook: HG_HOOKNAME=pretxncommit
189 HG_HOOKTYPE=pretxncommit
189 HG_HOOKTYPE=pretxncommit
190 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
190 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
191 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
191 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
192 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
192 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
193 HG_PENDING=$TESTTMP/a
193 HG_PENDING=$TESTTMP/a
194
194
195 3:07f3376c1e65
195 3:07f3376c1e65
196 pretxnclose hook: HG_HOOKNAME=pretxnclose
196 pretxnclose hook: HG_HOOKNAME=pretxnclose
197 HG_HOOKTYPE=pretxnclose
197 HG_HOOKTYPE=pretxnclose
198 HG_PENDING=$TESTTMP/a
198 HG_PENDING=$TESTTMP/a
199 HG_TXNID=TXN:$ID$
199 HG_TXNID=TXN:$ID$
200 HG_TXNNAME=commit
200 HG_TXNNAME=commit
201
201
202 txnclose hook: HG_HOOKNAME=txnclose
202 txnclose hook: HG_HOOKNAME=txnclose
203 HG_HOOKTYPE=txnclose
203 HG_HOOKTYPE=txnclose
204 HG_TXNID=TXN:$ID$
204 HG_TXNID=TXN:$ID$
205 HG_TXNNAME=commit
205 HG_TXNNAME=commit
206
206
207 commit hook: HG_HOOKNAME=commit
207 commit hook: HG_HOOKNAME=commit
208 HG_HOOKTYPE=commit
208 HG_HOOKTYPE=commit
209 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
209 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
210 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
210 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
211 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
211 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
212
212
213 commit.b hook: HG_HOOKNAME=commit.b
213 commit.b hook: HG_HOOKNAME=commit.b
214 HG_HOOKTYPE=commit
214 HG_HOOKTYPE=commit
215 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
215 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
216 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
216 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
217 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
217 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
218
218
219
219
220 test generic hooks
220 test generic hooks
221
221
222 $ hg id
222 $ hg id
223 pre-identify hook: HG_ARGS=id
223 pre-identify hook: HG_ARGS=id
224 HG_HOOKNAME=pre-identify
224 HG_HOOKNAME=pre-identify
225 HG_HOOKTYPE=pre-identify
225 HG_HOOKTYPE=pre-identify
226 HG_OPTS={'bookmarks': None, 'branch': None, 'id': None, 'insecure': None, 'num': None, 'remotecmd': '', 'rev': '', 'ssh': '', 'tags': None, 'template': ''}
226 HG_OPTS={'bookmarks': None, 'branch': None, 'id': None, 'insecure': None, 'num': None, 'remotecmd': '', 'rev': '', 'ssh': '', 'tags': None, 'template': ''}
227 HG_PATS=[]
227 HG_PATS=[]
228
228
229 abort: pre-identify hook exited with status 1
229 abort: pre-identify hook exited with status 1
230 [40]
230 [40]
231 $ hg cat b
231 $ hg cat b
232 pre-cat hook: HG_ARGS=cat b
232 pre-cat hook: HG_ARGS=cat b
233 HG_HOOKNAME=pre-cat
233 HG_HOOKNAME=pre-cat
234 HG_HOOKTYPE=pre-cat
234 HG_HOOKTYPE=pre-cat
235 HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''}
235 HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''}
236 HG_PATS=['b']
236 HG_PATS=['b']
237
237
238 b
238 b
239 post-cat hook: HG_ARGS=cat b
239 post-cat hook: HG_ARGS=cat b
240 HG_HOOKNAME=post-cat
240 HG_HOOKNAME=post-cat
241 HG_HOOKTYPE=post-cat
241 HG_HOOKTYPE=post-cat
242 HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''}
242 HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''}
243 HG_PATS=['b']
243 HG_PATS=['b']
244 HG_RESULT=0
244 HG_RESULT=0
245
245
246
246
247 $ cd ../b
247 $ cd ../b
248 $ hg pull ../a
248 $ hg pull ../a
249 pulling from ../a
249 pulling from ../a
250 searching for changes
250 searching for changes
251 prechangegroup hook: HG_HOOKNAME=prechangegroup
251 prechangegroup hook: HG_HOOKNAME=prechangegroup
252 HG_HOOKTYPE=prechangegroup
252 HG_HOOKTYPE=prechangegroup
253 HG_SOURCE=pull
253 HG_SOURCE=pull
254 HG_TXNID=TXN:$ID$
254 HG_TXNID=TXN:$ID$
255 HG_TXNNAME=pull
255 HG_TXNNAME=pull
256 file:/*/$TESTTMP/a (glob)
256 file:/*/$TESTTMP/a (glob)
257 HG_URL=file:$TESTTMP/a
257 HG_URL=file:$TESTTMP/a
258
258
259 adding changesets
259 adding changesets
260 adding manifests
260 adding manifests
261 adding file changes
261 adding file changes
262 added 3 changesets with 2 changes to 2 files
262 added 3 changesets with 2 changes to 2 files
263 new changesets ab228980c14d:07f3376c1e65
263 new changesets ab228980c14d:07f3376c1e65
264 changegroup hook: HG_HOOKNAME=changegroup
264 changegroup hook: HG_HOOKNAME=changegroup
265 HG_HOOKTYPE=changegroup
265 HG_HOOKTYPE=changegroup
266 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
266 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
267 HG_NODE_LAST=07f3376c1e655977439df2a814e3cc14b27abac2
267 HG_NODE_LAST=07f3376c1e655977439df2a814e3cc14b27abac2
268 HG_SOURCE=pull
268 HG_SOURCE=pull
269 HG_TXNID=TXN:$ID$
269 HG_TXNID=TXN:$ID$
270 HG_TXNNAME=pull
270 HG_TXNNAME=pull
271 file:/*/$TESTTMP/a (glob)
271 file:/*/$TESTTMP/a (glob)
272 HG_URL=file:$TESTTMP/a
272 HG_URL=file:$TESTTMP/a
273
273
274 incoming hook: HG_HOOKNAME=incoming
274 incoming hook: HG_HOOKNAME=incoming
275 HG_HOOKTYPE=incoming
275 HG_HOOKTYPE=incoming
276 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
276 HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd
277 HG_SOURCE=pull
277 HG_SOURCE=pull
278 HG_TXNID=TXN:$ID$
278 HG_TXNID=TXN:$ID$
279 HG_TXNNAME=pull
279 HG_TXNNAME=pull
280 file:/*/$TESTTMP/a (glob)
280 file:/*/$TESTTMP/a (glob)
281 HG_URL=file:$TESTTMP/a
281 HG_URL=file:$TESTTMP/a
282
282
283 incoming hook: HG_HOOKNAME=incoming
283 incoming hook: HG_HOOKNAME=incoming
284 HG_HOOKTYPE=incoming
284 HG_HOOKTYPE=incoming
285 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
285 HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2
286 HG_SOURCE=pull
286 HG_SOURCE=pull
287 HG_TXNID=TXN:$ID$
287 HG_TXNID=TXN:$ID$
288 HG_TXNNAME=pull
288 HG_TXNNAME=pull
289 file:/*/$TESTTMP/a (glob)
289 file:/*/$TESTTMP/a (glob)
290 HG_URL=file:$TESTTMP/a
290 HG_URL=file:$TESTTMP/a
291
291
292 incoming hook: HG_HOOKNAME=incoming
292 incoming hook: HG_HOOKNAME=incoming
293 HG_HOOKTYPE=incoming
293 HG_HOOKTYPE=incoming
294 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
294 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
295 HG_SOURCE=pull
295 HG_SOURCE=pull
296 HG_TXNID=TXN:$ID$
296 HG_TXNID=TXN:$ID$
297 HG_TXNNAME=pull
297 HG_TXNNAME=pull
298 file:/*/$TESTTMP/a (glob)
298 file:/*/$TESTTMP/a (glob)
299 HG_URL=file:$TESTTMP/a
299 HG_URL=file:$TESTTMP/a
300
300
301 (run 'hg update' to get a working copy)
301 (run 'hg update' to get a working copy)
302
302
303 tag hooks can see env vars
303 tag hooks can see env vars
304
304
305 $ cd ../a
305 $ cd ../a
306 $ cat >> .hg/hgrc <<EOF
306 $ cat >> .hg/hgrc <<EOF
307 > pretag = sh -c "printenv.py --line pretag"
307 > pretag = sh -c "printenv.py --line pretag"
308 > tag = sh -c "HG_PARENT1= HG_PARENT2= printenv.py --line tag"
308 > tag = sh -c "HG_PARENT1= HG_PARENT2= printenv.py --line tag"
309 > EOF
309 > EOF
310 $ hg tag -d '3 0' a
310 $ hg tag -d '3 0' a
311 pretag hook: HG_HOOKNAME=pretag
311 pretag hook: HG_HOOKNAME=pretag
312 HG_HOOKTYPE=pretag
312 HG_HOOKTYPE=pretag
313 HG_LOCAL=0
313 HG_LOCAL=0
314 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
314 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
315 HG_TAG=a
315 HG_TAG=a
316
316
317 precommit hook: HG_HOOKNAME=precommit
317 precommit hook: HG_HOOKNAME=precommit
318 HG_HOOKTYPE=precommit
318 HG_HOOKTYPE=precommit
319 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
319 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
320
320
321 pretxnopen hook: HG_HOOKNAME=pretxnopen
321 pretxnopen hook: HG_HOOKNAME=pretxnopen
322 HG_HOOKTYPE=pretxnopen
322 HG_HOOKTYPE=pretxnopen
323 HG_TXNID=TXN:$ID$
323 HG_TXNID=TXN:$ID$
324 HG_TXNNAME=commit
324 HG_TXNNAME=commit
325
325
326 pretxncommit hook: HG_HOOKNAME=pretxncommit
326 pretxncommit hook: HG_HOOKNAME=pretxncommit
327 HG_HOOKTYPE=pretxncommit
327 HG_HOOKTYPE=pretxncommit
328 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
328 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
329 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
329 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
330 HG_PENDING=$TESTTMP/a
330 HG_PENDING=$TESTTMP/a
331
331
332 4:539e4b31b6dc
332 4:539e4b31b6dc
333 pretxnclose hook: HG_HOOKNAME=pretxnclose
333 pretxnclose hook: HG_HOOKNAME=pretxnclose
334 HG_HOOKTYPE=pretxnclose
334 HG_HOOKTYPE=pretxnclose
335 HG_PENDING=$TESTTMP/a
335 HG_PENDING=$TESTTMP/a
336 HG_TXNID=TXN:$ID$
336 HG_TXNID=TXN:$ID$
337 HG_TXNNAME=commit
337 HG_TXNNAME=commit
338
338
339 tag hook: HG_HOOKNAME=tag
339 tag hook: HG_HOOKNAME=tag
340 HG_HOOKTYPE=tag
340 HG_HOOKTYPE=tag
341 HG_LOCAL=0
341 HG_LOCAL=0
342 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
342 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2
343 HG_TAG=a
343 HG_TAG=a
344
344
345 txnclose hook: HG_HOOKNAME=txnclose
345 txnclose hook: HG_HOOKNAME=txnclose
346 HG_HOOKTYPE=txnclose
346 HG_HOOKTYPE=txnclose
347 HG_TXNID=TXN:$ID$
347 HG_TXNID=TXN:$ID$
348 HG_TXNNAME=commit
348 HG_TXNNAME=commit
349
349
350 commit hook: HG_HOOKNAME=commit
350 commit hook: HG_HOOKNAME=commit
351 HG_HOOKTYPE=commit
351 HG_HOOKTYPE=commit
352 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
352 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
353 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
353 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
354
354
355 commit.b hook: HG_HOOKNAME=commit.b
355 commit.b hook: HG_HOOKNAME=commit.b
356 HG_HOOKTYPE=commit
356 HG_HOOKTYPE=commit
357 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
357 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
358 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
358 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
359
359
360 $ hg tag -l la
360 $ hg tag -l la
361 pretag hook: HG_HOOKNAME=pretag
361 pretag hook: HG_HOOKNAME=pretag
362 HG_HOOKTYPE=pretag
362 HG_HOOKTYPE=pretag
363 HG_LOCAL=1
363 HG_LOCAL=1
364 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
364 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
365 HG_TAG=la
365 HG_TAG=la
366
366
367 tag hook: HG_HOOKNAME=tag
367 tag hook: HG_HOOKNAME=tag
368 HG_HOOKTYPE=tag
368 HG_HOOKTYPE=tag
369 HG_LOCAL=1
369 HG_LOCAL=1
370 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
370 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
371 HG_TAG=la
371 HG_TAG=la
372
372
373
373
374 pretag hook can forbid tagging
374 pretag hook can forbid tagging
375
375
376 $ cat >> .hg/hgrc <<EOF
376 $ cat >> .hg/hgrc <<EOF
377 > pretag.forbid = sh -c "printenv.py --line pretag.forbid 1"
377 > pretag.forbid = sh -c "printenv.py --line pretag.forbid 1"
378 > EOF
378 > EOF
379 $ hg tag -d '4 0' fa
379 $ hg tag -d '4 0' fa
380 pretag hook: HG_HOOKNAME=pretag
380 pretag hook: HG_HOOKNAME=pretag
381 HG_HOOKTYPE=pretag
381 HG_HOOKTYPE=pretag
382 HG_LOCAL=0
382 HG_LOCAL=0
383 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
383 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
384 HG_TAG=fa
384 HG_TAG=fa
385
385
386 pretag.forbid hook: HG_HOOKNAME=pretag.forbid
386 pretag.forbid hook: HG_HOOKNAME=pretag.forbid
387 HG_HOOKTYPE=pretag
387 HG_HOOKTYPE=pretag
388 HG_LOCAL=0
388 HG_LOCAL=0
389 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
389 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
390 HG_TAG=fa
390 HG_TAG=fa
391
391
392 abort: pretag.forbid hook exited with status 1
392 abort: pretag.forbid hook exited with status 1
393 [40]
393 [40]
394 $ hg tag -l fla
394 $ hg tag -l fla
395 pretag hook: HG_HOOKNAME=pretag
395 pretag hook: HG_HOOKNAME=pretag
396 HG_HOOKTYPE=pretag
396 HG_HOOKTYPE=pretag
397 HG_LOCAL=1
397 HG_LOCAL=1
398 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
398 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
399 HG_TAG=fla
399 HG_TAG=fla
400
400
401 pretag.forbid hook: HG_HOOKNAME=pretag.forbid
401 pretag.forbid hook: HG_HOOKNAME=pretag.forbid
402 HG_HOOKTYPE=pretag
402 HG_HOOKTYPE=pretag
403 HG_LOCAL=1
403 HG_LOCAL=1
404 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
404 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
405 HG_TAG=fla
405 HG_TAG=fla
406
406
407 abort: pretag.forbid hook exited with status 1
407 abort: pretag.forbid hook exited with status 1
408 [40]
408 [40]
409
409
410 pretxncommit hook can see changeset, can roll back txn, changeset no
410 pretxncommit hook can see changeset, can roll back txn, changeset no
411 more there after
411 more there after
412
412
413 $ cat >> .hg/hgrc <<EOF
413 $ cat >> .hg/hgrc <<EOF
414 > pretxncommit.forbid0 = sh -c "hg tip -q"
414 > pretxncommit.forbid0 = sh -c "hg tip -q"
415 > pretxncommit.forbid1 = sh -c "printenv.py --line pretxncommit.forbid 1"
415 > pretxncommit.forbid1 = sh -c "printenv.py --line pretxncommit.forbid 1"
416 > EOF
416 > EOF
417 $ echo z > z
417 $ echo z > z
418 $ hg add z
418 $ hg add z
419 $ hg -q tip
419 $ hg -q tip
420 4:539e4b31b6dc
420 4:539e4b31b6dc
421 $ hg commit -m 'fail' -d '4 0'
421 $ hg commit -m 'fail' -d '4 0'
422 precommit hook: HG_HOOKNAME=precommit
422 precommit hook: HG_HOOKNAME=precommit
423 HG_HOOKTYPE=precommit
423 HG_HOOKTYPE=precommit
424 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
424 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
425
425
426 pretxnopen hook: HG_HOOKNAME=pretxnopen
426 pretxnopen hook: HG_HOOKNAME=pretxnopen
427 HG_HOOKTYPE=pretxnopen
427 HG_HOOKTYPE=pretxnopen
428 HG_TXNID=TXN:$ID$
428 HG_TXNID=TXN:$ID$
429 HG_TXNNAME=commit
429 HG_TXNNAME=commit
430
430
431 pretxncommit hook: HG_HOOKNAME=pretxncommit
431 pretxncommit hook: HG_HOOKNAME=pretxncommit
432 HG_HOOKTYPE=pretxncommit
432 HG_HOOKTYPE=pretxncommit
433 HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567
433 HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567
434 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
434 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
435 HG_PENDING=$TESTTMP/a
435 HG_PENDING=$TESTTMP/a
436
436
437 5:6f611f8018c1
437 5:6f611f8018c1
438 5:6f611f8018c1
438 5:6f611f8018c1
439 pretxncommit.forbid hook: HG_HOOKNAME=pretxncommit.forbid1
439 pretxncommit.forbid hook: HG_HOOKNAME=pretxncommit.forbid1
440 HG_HOOKTYPE=pretxncommit
440 HG_HOOKTYPE=pretxncommit
441 HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567
441 HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567
442 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
442 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
443 HG_PENDING=$TESTTMP/a
443 HG_PENDING=$TESTTMP/a
444
444
445 transaction abort!
445 transaction abort!
446 txnabort Python hook: changes,txnid,txnname
446 txnabort Python hook: changes,txnid,txnname
447 txnabort hook: HG_HOOKNAME=txnabort.1
447 txnabort hook: HG_HOOKNAME=txnabort.1
448 HG_HOOKTYPE=txnabort
448 HG_HOOKTYPE=txnabort
449 HG_TXNID=TXN:$ID$
449 HG_TXNID=TXN:$ID$
450 HG_TXNNAME=commit
450 HG_TXNNAME=commit
451
451
452 rollback completed
452 rollback completed
453 abort: pretxncommit.forbid1 hook exited with status 1
453 abort: pretxncommit.forbid1 hook exited with status 1
454 [40]
454 [40]
455 $ hg -q tip
455 $ hg -q tip
456 4:539e4b31b6dc
456 4:539e4b31b6dc
457
457
458 (Check that no 'changelog.i.a' file were left behind)
458 (Check that no 'changelog.i.a' file were left behind)
459
459
460 $ ls -1 .hg/store/
460 $ ls -1 .hg/store/
461 00changelog.i
461 00changelog.i
462 00manifest.i
462 00manifest.i
463 data
463 data
464 fncache (repofncache !)
464 fncache (repofncache !)
465 phaseroots
465 phaseroots
466 requires
466 requires
467 undo
467 undo
468 undo.backup.fncache.bck (repofncache !)
468 undo.backup.fncache.bck (repofncache !)
469 undo.backupfiles
469 undo.backupfiles
470
470
471
471
472 precommit hook can prevent commit
472 precommit hook can prevent commit
473
473
474 $ cat >> .hg/hgrc <<EOF
474 $ cat >> .hg/hgrc <<EOF
475 > precommit.forbid = sh -c "printenv.py --line precommit.forbid 1"
475 > precommit.forbid = sh -c "printenv.py --line precommit.forbid 1"
476 > EOF
476 > EOF
477 $ hg commit -m 'fail' -d '4 0'
477 $ hg commit -m 'fail' -d '4 0'
478 precommit hook: HG_HOOKNAME=precommit
478 precommit hook: HG_HOOKNAME=precommit
479 HG_HOOKTYPE=precommit
479 HG_HOOKTYPE=precommit
480 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
480 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
481
481
482 precommit.forbid hook: HG_HOOKNAME=precommit.forbid
482 precommit.forbid hook: HG_HOOKNAME=precommit.forbid
483 HG_HOOKTYPE=precommit
483 HG_HOOKTYPE=precommit
484 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
484 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
485
485
486 abort: precommit.forbid hook exited with status 1
486 abort: precommit.forbid hook exited with status 1
487 [40]
487 [40]
488 $ hg -q tip
488 $ hg -q tip
489 4:539e4b31b6dc
489 4:539e4b31b6dc
490
490
491 preupdate hook can prevent update
491 preupdate hook can prevent update
492
492
493 $ cat >> .hg/hgrc <<EOF
493 $ cat >> .hg/hgrc <<EOF
494 > preupdate = sh -c "printenv.py --line preupdate"
494 > preupdate = sh -c "printenv.py --line preupdate"
495 > EOF
495 > EOF
496 $ hg update 1
496 $ hg update 1
497 preupdate hook: HG_HOOKNAME=preupdate
497 preupdate hook: HG_HOOKNAME=preupdate
498 HG_HOOKTYPE=preupdate
498 HG_HOOKTYPE=preupdate
499 HG_PARENT1=ab228980c14d
499 HG_PARENT1=ab228980c14d
500
500
501 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
501 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
502
502
503 update hook
503 update hook
504
504
505 $ cat >> .hg/hgrc <<EOF
505 $ cat >> .hg/hgrc <<EOF
506 > update = sh -c "printenv.py --line update"
506 > update = sh -c "printenv.py --line update"
507 > EOF
507 > EOF
508 $ hg update
508 $ hg update
509 preupdate hook: HG_HOOKNAME=preupdate
509 preupdate hook: HG_HOOKNAME=preupdate
510 HG_HOOKTYPE=preupdate
510 HG_HOOKTYPE=preupdate
511 HG_PARENT1=539e4b31b6dc
511 HG_PARENT1=539e4b31b6dc
512
512
513 update hook: HG_ERROR=0
513 update hook: HG_ERROR=0
514 HG_HOOKNAME=update
514 HG_HOOKNAME=update
515 HG_HOOKTYPE=update
515 HG_HOOKTYPE=update
516 HG_PARENT1=539e4b31b6dc
516 HG_PARENT1=539e4b31b6dc
517
517
518 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
518 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
519
519
520 pushkey hook
520 pushkey hook
521
521
522 $ cat >> .hg/hgrc <<EOF
522 $ cat >> .hg/hgrc <<EOF
523 > pushkey = sh -c "printenv.py --line pushkey"
523 > pushkey = sh -c "printenv.py --line pushkey"
524 > EOF
524 > EOF
525 $ cd ../b
525 $ cd ../b
526 $ hg bookmark -r null foo
526 $ hg bookmark -r null foo
527 $ hg push -B foo ../a
527 $ hg push -B foo ../a
528 pushing to ../a
528 pushing to ../a
529 searching for changes
529 searching for changes
530 no changes found
530 no changes found
531 pretxnopen hook: HG_HOOKNAME=pretxnopen
531 pretxnopen hook: HG_HOOKNAME=pretxnopen
532 HG_HOOKTYPE=pretxnopen
532 HG_HOOKTYPE=pretxnopen
533 HG_TXNID=TXN:$ID$
533 HG_TXNID=TXN:$ID$
534 HG_TXNNAME=push
534 HG_TXNNAME=push
535
535
536 pretxnclose hook: HG_BOOKMARK_MOVED=1
536 pretxnclose hook: HG_BOOKMARK_MOVED=1
537 HG_BUNDLE2=1
537 HG_BUNDLE2=1
538 HG_HOOKNAME=pretxnclose
538 HG_HOOKNAME=pretxnclose
539 HG_HOOKTYPE=pretxnclose
539 HG_HOOKTYPE=pretxnclose
540 HG_PENDING=$TESTTMP/a
540 HG_PENDING=$TESTTMP/a
541 HG_SOURCE=push
541 HG_SOURCE=push
542 HG_TXNID=TXN:$ID$
542 HG_TXNID=TXN:$ID$
543 HG_TXNNAME=push
543 HG_TXNNAME=push
544 HG_URL=file:$TESTTMP/a
544 HG_URL=file:$TESTTMP/a
545
545
546 pushkey hook: HG_BUNDLE2=1
546 pushkey hook: HG_BUNDLE2=1
547 HG_HOOKNAME=pushkey
547 HG_HOOKNAME=pushkey
548 HG_HOOKTYPE=pushkey
548 HG_HOOKTYPE=pushkey
549 HG_KEY=foo
549 HG_KEY=foo
550 HG_NAMESPACE=bookmarks
550 HG_NAMESPACE=bookmarks
551 HG_NEW=0000000000000000000000000000000000000000
551 HG_NEW=0000000000000000000000000000000000000000
552 HG_PUSHKEYCOMPAT=1
552 HG_PUSHKEYCOMPAT=1
553 HG_SOURCE=push
553 HG_SOURCE=push
554 HG_TXNID=TXN:$ID$
554 HG_TXNID=TXN:$ID$
555 HG_TXNNAME=push
555 HG_TXNNAME=push
556 HG_URL=file:$TESTTMP/a
556 HG_URL=file:$TESTTMP/a
557
557
558 txnclose hook: HG_BOOKMARK_MOVED=1
558 txnclose hook: HG_BOOKMARK_MOVED=1
559 HG_BUNDLE2=1
559 HG_BUNDLE2=1
560 HG_HOOKNAME=txnclose
560 HG_HOOKNAME=txnclose
561 HG_HOOKTYPE=txnclose
561 HG_HOOKTYPE=txnclose
562 HG_SOURCE=push
562 HG_SOURCE=push
563 HG_TXNID=TXN:$ID$
563 HG_TXNID=TXN:$ID$
564 HG_TXNNAME=push
564 HG_TXNNAME=push
565 HG_URL=file:$TESTTMP/a
565 HG_URL=file:$TESTTMP/a
566
566
567 exporting bookmark foo
567 exporting bookmark foo
568 [1]
568 [1]
569 $ cd ../a
569 $ cd ../a
570
570
571 listkeys hook
571 listkeys hook
572
572
573 $ cat >> .hg/hgrc <<EOF
573 $ cat >> .hg/hgrc <<EOF
574 > listkeys = sh -c "printenv.py --line listkeys"
574 > listkeys = sh -c "printenv.py --line listkeys"
575 > EOF
575 > EOF
576 $ hg bookmark -r null bar
576 $ hg bookmark -r null bar
577 pretxnopen hook: HG_HOOKNAME=pretxnopen
577 pretxnopen hook: HG_HOOKNAME=pretxnopen
578 HG_HOOKTYPE=pretxnopen
578 HG_HOOKTYPE=pretxnopen
579 HG_TXNID=TXN:$ID$
579 HG_TXNID=TXN:$ID$
580 HG_TXNNAME=bookmark
580 HG_TXNNAME=bookmark
581
581
582 pretxnclose hook: HG_BOOKMARK_MOVED=1
582 pretxnclose hook: HG_BOOKMARK_MOVED=1
583 HG_HOOKNAME=pretxnclose
583 HG_HOOKNAME=pretxnclose
584 HG_HOOKTYPE=pretxnclose
584 HG_HOOKTYPE=pretxnclose
585 HG_PENDING=$TESTTMP/a
585 HG_PENDING=$TESTTMP/a
586 HG_TXNID=TXN:$ID$
586 HG_TXNID=TXN:$ID$
587 HG_TXNNAME=bookmark
587 HG_TXNNAME=bookmark
588
588
589 txnclose hook: HG_BOOKMARK_MOVED=1
589 txnclose hook: HG_BOOKMARK_MOVED=1
590 HG_HOOKNAME=txnclose
590 HG_HOOKNAME=txnclose
591 HG_HOOKTYPE=txnclose
591 HG_HOOKTYPE=txnclose
592 HG_TXNID=TXN:$ID$
592 HG_TXNID=TXN:$ID$
593 HG_TXNNAME=bookmark
593 HG_TXNNAME=bookmark
594
594
595 $ cd ../b
595 $ cd ../b
596 $ hg pull -B bar ../a
596 $ hg pull -B bar ../a
597 pulling from ../a
597 pulling from ../a
598 listkeys hook: HG_HOOKNAME=listkeys
598 listkeys hook: HG_HOOKNAME=listkeys
599 HG_HOOKTYPE=listkeys
599 HG_HOOKTYPE=listkeys
600 HG_NAMESPACE=bookmarks
600 HG_NAMESPACE=bookmarks
601 HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'}
601 HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'}
602
602
603 no changes found
603 no changes found
604 adding remote bookmark bar
604 adding remote bookmark bar
605 $ cd ../a
605 $ cd ../a
606
606
607 test that prepushkey can prevent incoming keys
607 test that prepushkey can prevent incoming keys
608
608
609 $ cat >> .hg/hgrc <<EOF
609 $ cat >> .hg/hgrc <<EOF
610 > prepushkey = sh -c "printenv.py --line prepushkey.forbid 1"
610 > prepushkey = sh -c "printenv.py --line prepushkey.forbid 1"
611 > EOF
611 > EOF
612 $ cd ../b
612 $ cd ../b
613 $ hg bookmark -r null baz
613 $ hg bookmark -r null baz
614 $ hg push -B baz ../a
614 $ hg push -B baz ../a
615 pushing to ../a
615 pushing to ../a
616 searching for changes
616 searching for changes
617 listkeys hook: HG_HOOKNAME=listkeys
617 listkeys hook: HG_HOOKNAME=listkeys
618 HG_HOOKTYPE=listkeys
618 HG_HOOKTYPE=listkeys
619 HG_NAMESPACE=phases
619 HG_NAMESPACE=phases
620 HG_VALUES={'cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b': '1', 'publishing': 'True'}
620 HG_VALUES={'cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b': '1', 'publishing': 'True'}
621
621
622 listkeys hook: HG_HOOKNAME=listkeys
622 listkeys hook: HG_HOOKNAME=listkeys
623 HG_HOOKTYPE=listkeys
623 HG_HOOKTYPE=listkeys
624 HG_NAMESPACE=bookmarks
624 HG_NAMESPACE=bookmarks
625 HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'}
625 HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'}
626
626
627 no changes found
627 no changes found
628 pretxnopen hook: HG_HOOKNAME=pretxnopen
628 pretxnopen hook: HG_HOOKNAME=pretxnopen
629 HG_HOOKTYPE=pretxnopen
629 HG_HOOKTYPE=pretxnopen
630 HG_TXNID=TXN:$ID$
630 HG_TXNID=TXN:$ID$
631 HG_TXNNAME=push
631 HG_TXNNAME=push
632
632
633 prepushkey.forbid hook: HG_BUNDLE2=1
633 prepushkey.forbid hook: HG_BUNDLE2=1
634 HG_HOOKNAME=prepushkey
634 HG_HOOKNAME=prepushkey
635 HG_HOOKTYPE=prepushkey
635 HG_HOOKTYPE=prepushkey
636 HG_KEY=baz
636 HG_KEY=baz
637 HG_NAMESPACE=bookmarks
637 HG_NAMESPACE=bookmarks
638 HG_NEW=0000000000000000000000000000000000000000
638 HG_NEW=0000000000000000000000000000000000000000
639 HG_PUSHKEYCOMPAT=1
639 HG_PUSHKEYCOMPAT=1
640 HG_SOURCE=push
640 HG_SOURCE=push
641 HG_TXNID=TXN:$ID$
641 HG_TXNID=TXN:$ID$
642 HG_TXNNAME=push
642 HG_TXNNAME=push
643 HG_URL=file:$TESTTMP/a
643 HG_URL=file:$TESTTMP/a
644
644
645 txnabort Python hook: bundle2,changes,source,txnid,txnname,url
645 txnabort Python hook: bundle2,changes,source,txnid,txnname,url
646 txnabort hook: HG_BUNDLE2=1
646 txnabort hook: HG_BUNDLE2=1
647 HG_HOOKNAME=txnabort.1
647 HG_HOOKNAME=txnabort.1
648 HG_HOOKTYPE=txnabort
648 HG_HOOKTYPE=txnabort
649 HG_SOURCE=push
649 HG_SOURCE=push
650 HG_TXNID=TXN:$ID$
650 HG_TXNID=TXN:$ID$
651 HG_TXNNAME=push
651 HG_TXNNAME=push
652 HG_URL=file:$TESTTMP/a
652 HG_URL=file:$TESTTMP/a
653
653
654 abort: prepushkey hook exited with status 1
654 abort: prepushkey hook exited with status 1
655 [40]
655 [40]
656 $ cd ../a
656 $ cd ../a
657
657
658 test that prelistkeys can prevent listing keys
658 test that prelistkeys can prevent listing keys
659
659
660 $ cat >> .hg/hgrc <<EOF
660 $ cat >> .hg/hgrc <<EOF
661 > prelistkeys = sh -c "printenv.py --line prelistkeys.forbid 1"
661 > prelistkeys = sh -c "printenv.py --line prelistkeys.forbid 1"
662 > EOF
662 > EOF
663 $ hg bookmark -r null quux
663 $ hg bookmark -r null quux
664 pretxnopen hook: HG_HOOKNAME=pretxnopen
664 pretxnopen hook: HG_HOOKNAME=pretxnopen
665 HG_HOOKTYPE=pretxnopen
665 HG_HOOKTYPE=pretxnopen
666 HG_TXNID=TXN:$ID$
666 HG_TXNID=TXN:$ID$
667 HG_TXNNAME=bookmark
667 HG_TXNNAME=bookmark
668
668
669 pretxnclose hook: HG_BOOKMARK_MOVED=1
669 pretxnclose hook: HG_BOOKMARK_MOVED=1
670 HG_HOOKNAME=pretxnclose
670 HG_HOOKNAME=pretxnclose
671 HG_HOOKTYPE=pretxnclose
671 HG_HOOKTYPE=pretxnclose
672 HG_PENDING=$TESTTMP/a
672 HG_PENDING=$TESTTMP/a
673 HG_TXNID=TXN:$ID$
673 HG_TXNID=TXN:$ID$
674 HG_TXNNAME=bookmark
674 HG_TXNNAME=bookmark
675
675
676 txnclose hook: HG_BOOKMARK_MOVED=1
676 txnclose hook: HG_BOOKMARK_MOVED=1
677 HG_HOOKNAME=txnclose
677 HG_HOOKNAME=txnclose
678 HG_HOOKTYPE=txnclose
678 HG_HOOKTYPE=txnclose
679 HG_TXNID=TXN:$ID$
679 HG_TXNID=TXN:$ID$
680 HG_TXNNAME=bookmark
680 HG_TXNNAME=bookmark
681
681
682 $ cd ../b
682 $ cd ../b
683 $ hg pull -B quux ../a
683 $ hg pull -B quux ../a
684 pulling from ../a
684 pulling from ../a
685 prelistkeys.forbid hook: HG_HOOKNAME=prelistkeys
685 prelistkeys.forbid hook: HG_HOOKNAME=prelistkeys
686 HG_HOOKTYPE=prelistkeys
686 HG_HOOKTYPE=prelistkeys
687 HG_NAMESPACE=bookmarks
687 HG_NAMESPACE=bookmarks
688
688
689 abort: prelistkeys hook exited with status 1
689 abort: prelistkeys hook exited with status 1
690 [40]
690 [40]
691 $ cd ../a
691 $ cd ../a
692 $ rm .hg/hgrc
692 $ rm .hg/hgrc
693
693
694 prechangegroup hook can prevent incoming changes
694 prechangegroup hook can prevent incoming changes
695
695
696 $ cd ../b
696 $ cd ../b
697 $ hg -q tip
697 $ hg -q tip
698 3:07f3376c1e65
698 3:07f3376c1e65
699 $ cat > .hg/hgrc <<EOF
699 $ cat > .hg/hgrc <<EOF
700 > [hooks]
700 > [hooks]
701 > prechangegroup.forbid = sh -c "printenv.py --line prechangegroup.forbid 1"
701 > prechangegroup.forbid = sh -c "printenv.py --line prechangegroup.forbid 1"
702 > EOF
702 > EOF
703 $ hg pull ../a
703 $ hg pull ../a
704 pulling from ../a
704 pulling from ../a
705 searching for changes
705 searching for changes
706 prechangegroup.forbid hook: HG_HOOKNAME=prechangegroup.forbid
706 prechangegroup.forbid hook: HG_HOOKNAME=prechangegroup.forbid
707 HG_HOOKTYPE=prechangegroup
707 HG_HOOKTYPE=prechangegroup
708 HG_SOURCE=pull
708 HG_SOURCE=pull
709 HG_TXNID=TXN:$ID$
709 HG_TXNID=TXN:$ID$
710 HG_TXNNAME=pull
710 HG_TXNNAME=pull
711 file:/*/$TESTTMP/a (glob)
711 file:/*/$TESTTMP/a (glob)
712 HG_URL=file:$TESTTMP/a
712 HG_URL=file:$TESTTMP/a
713
713
714 abort: prechangegroup.forbid hook exited with status 1
714 abort: prechangegroup.forbid hook exited with status 1
715 [40]
715 [40]
716
716
717 pretxnchangegroup hook can see incoming changes, can roll back txn,
717 pretxnchangegroup hook can see incoming changes, can roll back txn,
718 incoming changes no longer there after
718 incoming changes no longer there after
719
719
720 $ cat > .hg/hgrc <<EOF
720 $ cat > .hg/hgrc <<EOF
721 > [hooks]
721 > [hooks]
722 > pretxnchangegroup.forbid0 = hg tip -q
722 > pretxnchangegroup.forbid0 = hg tip -q
723 > pretxnchangegroup.forbid1 = sh -c "printenv.py --line pretxnchangegroup.forbid 1"
723 > pretxnchangegroup.forbid1 = sh -c "printenv.py --line pretxnchangegroup.forbid 1"
724 > EOF
724 > EOF
725 $ hg pull ../a
725 $ hg pull ../a
726 pulling from ../a
726 pulling from ../a
727 searching for changes
727 searching for changes
728 adding changesets
728 adding changesets
729 adding manifests
729 adding manifests
730 adding file changes
730 adding file changes
731 4:539e4b31b6dc
731 4:539e4b31b6dc
732 pretxnchangegroup.forbid hook: HG_HOOKNAME=pretxnchangegroup.forbid1
732 pretxnchangegroup.forbid hook: HG_HOOKNAME=pretxnchangegroup.forbid1
733 HG_HOOKTYPE=pretxnchangegroup
733 HG_HOOKTYPE=pretxnchangegroup
734 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
734 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
735 HG_NODE_LAST=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
735 HG_NODE_LAST=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
736 HG_PENDING=$TESTTMP/b
736 HG_PENDING=$TESTTMP/b
737 HG_SOURCE=pull
737 HG_SOURCE=pull
738 HG_TXNID=TXN:$ID$
738 HG_TXNID=TXN:$ID$
739 HG_TXNNAME=pull
739 HG_TXNNAME=pull
740 file:/*/$TESTTMP/a (glob)
740 file:/*/$TESTTMP/a (glob)
741 HG_URL=file:$TESTTMP/a
741 HG_URL=file:$TESTTMP/a
742
742
743 transaction abort!
743 transaction abort!
744 rollback completed
744 rollback completed
745 abort: pretxnchangegroup.forbid1 hook exited with status 1
745 abort: pretxnchangegroup.forbid1 hook exited with status 1
746 [40]
746 [40]
747 $ hg -q tip
747 $ hg -q tip
748 3:07f3376c1e65
748 3:07f3376c1e65
749
749
750 outgoing hooks can see env vars
750 outgoing hooks can see env vars
751
751
752 $ rm .hg/hgrc
752 $ rm .hg/hgrc
753 $ cat > ../a/.hg/hgrc <<EOF
753 $ cat > ../a/.hg/hgrc <<EOF
754 > [hooks]
754 > [hooks]
755 > preoutgoing = sh -c "printenv.py --line preoutgoing"
755 > preoutgoing = sh -c "printenv.py --line preoutgoing"
756 > outgoing = sh -c "printenv.py --line outgoing"
756 > outgoing = sh -c "printenv.py --line outgoing"
757 > EOF
757 > EOF
758 $ hg pull ../a
758 $ hg pull ../a
759 pulling from ../a
759 pulling from ../a
760 searching for changes
760 searching for changes
761 preoutgoing hook: HG_HOOKNAME=preoutgoing
761 preoutgoing hook: HG_HOOKNAME=preoutgoing
762 HG_HOOKTYPE=preoutgoing
762 HG_HOOKTYPE=preoutgoing
763 HG_SOURCE=pull
763 HG_SOURCE=pull
764
764
765 outgoing hook: HG_HOOKNAME=outgoing
765 outgoing hook: HG_HOOKNAME=outgoing
766 HG_HOOKTYPE=outgoing
766 HG_HOOKTYPE=outgoing
767 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
767 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
768 HG_SOURCE=pull
768 HG_SOURCE=pull
769
769
770 adding changesets
770 adding changesets
771 adding manifests
771 adding manifests
772 adding file changes
772 adding file changes
773 adding remote bookmark quux
773 adding remote bookmark quux
774 added 1 changesets with 1 changes to 1 files
774 added 1 changesets with 1 changes to 1 files
775 new changesets 539e4b31b6dc
775 new changesets 539e4b31b6dc
776 (run 'hg update' to get a working copy)
776 (run 'hg update' to get a working copy)
777 $ hg rollback
777 $ hg rollback
778 repository tip rolled back to revision 3 (undo pull)
778 repository tip rolled back to revision 3 (undo pull)
779
779
780 preoutgoing hook can prevent outgoing changes
780 preoutgoing hook can prevent outgoing changes
781
781
782 $ cat >> ../a/.hg/hgrc <<EOF
782 $ cat >> ../a/.hg/hgrc <<EOF
783 > preoutgoing.forbid = sh -c "printenv.py --line preoutgoing.forbid 1"
783 > preoutgoing.forbid = sh -c "printenv.py --line preoutgoing.forbid 1"
784 > EOF
784 > EOF
785 $ hg pull ../a
785 $ hg pull ../a
786 pulling from ../a
786 pulling from ../a
787 searching for changes
787 searching for changes
788 preoutgoing hook: HG_HOOKNAME=preoutgoing
788 preoutgoing hook: HG_HOOKNAME=preoutgoing
789 HG_HOOKTYPE=preoutgoing
789 HG_HOOKTYPE=preoutgoing
790 HG_SOURCE=pull
790 HG_SOURCE=pull
791
791
792 preoutgoing.forbid hook: HG_HOOKNAME=preoutgoing.forbid
792 preoutgoing.forbid hook: HG_HOOKNAME=preoutgoing.forbid
793 HG_HOOKTYPE=preoutgoing
793 HG_HOOKTYPE=preoutgoing
794 HG_SOURCE=pull
794 HG_SOURCE=pull
795
795
796 abort: preoutgoing.forbid hook exited with status 1
796 abort: preoutgoing.forbid hook exited with status 1
797 [40]
797 [40]
798
798
799 outgoing hooks work for local clones
799 outgoing hooks work for local clones
800
800
801 $ cd ..
801 $ cd ..
802 $ cat > a/.hg/hgrc <<EOF
802 $ cat > a/.hg/hgrc <<EOF
803 > [hooks]
803 > [hooks]
804 > preoutgoing = sh -c "printenv.py --line preoutgoing"
804 > preoutgoing = sh -c "printenv.py --line preoutgoing"
805 > outgoing = sh -c "printenv.py --line outgoing"
805 > outgoing = sh -c "printenv.py --line outgoing"
806 > EOF
806 > EOF
807 $ hg clone a c
807 $ hg clone a c
808 preoutgoing hook: HG_HOOKNAME=preoutgoing
808 preoutgoing hook: HG_HOOKNAME=preoutgoing
809 HG_HOOKTYPE=preoutgoing
809 HG_HOOKTYPE=preoutgoing
810 HG_SOURCE=clone
810 HG_SOURCE=clone
811
811
812 outgoing hook: HG_HOOKNAME=outgoing
812 outgoing hook: HG_HOOKNAME=outgoing
813 HG_HOOKTYPE=outgoing
813 HG_HOOKTYPE=outgoing
814 HG_NODE=0000000000000000000000000000000000000000
814 HG_NODE=0000000000000000000000000000000000000000
815 HG_SOURCE=clone
815 HG_SOURCE=clone
816
816
817 updating to branch default
817 updating to branch default
818 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
818 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
819 $ rm -rf c
819 $ rm -rf c
820
820
821 preoutgoing hook can prevent outgoing changes for local clones
821 preoutgoing hook can prevent outgoing changes for local clones
822
822
823 $ cat >> a/.hg/hgrc <<EOF
823 $ cat >> a/.hg/hgrc <<EOF
824 > preoutgoing.forbid = sh -c "printenv.py --line preoutgoing.forbid 1"
824 > preoutgoing.forbid = sh -c "printenv.py --line preoutgoing.forbid 1"
825 > EOF
825 > EOF
826 $ hg clone a zzz
826 $ hg clone a zzz
827 preoutgoing hook: HG_HOOKNAME=preoutgoing
827 preoutgoing hook: HG_HOOKNAME=preoutgoing
828 HG_HOOKTYPE=preoutgoing
828 HG_HOOKTYPE=preoutgoing
829 HG_SOURCE=clone
829 HG_SOURCE=clone
830
830
831 preoutgoing.forbid hook: HG_HOOKNAME=preoutgoing.forbid
831 preoutgoing.forbid hook: HG_HOOKNAME=preoutgoing.forbid
832 HG_HOOKTYPE=preoutgoing
832 HG_HOOKTYPE=preoutgoing
833 HG_SOURCE=clone
833 HG_SOURCE=clone
834
834
835 abort: preoutgoing.forbid hook exited with status 1
835 abort: preoutgoing.forbid hook exited with status 1
836 [40]
836 [40]
837
837
838 $ cd "$TESTTMP/b"
838 $ cd "$TESTTMP/b"
839
839
840 $ cat > hooktests.py <<EOF
840 $ cat > hooktests.py <<EOF
841 > from mercurial import (
841 > from mercurial import (
842 > error,
842 > error,
843 > pycompat,
843 > pycompat,
844 > )
844 > )
845 >
845 >
846 > uncallable = 0
846 > uncallable = 0
847 >
847 >
848 > def printargs(ui, args):
848 > def printargs(ui, args):
849 > a = list(pycompat.byteskwargs(args).items())
849 > a = list(pycompat.byteskwargs(args).items())
850 > a.sort()
850 > a.sort()
851 > ui.write(b'hook args:\n')
851 > ui.write(b'hook args:\n')
852 > for k, v in a:
852 > for k, v in a:
853 > ui.write(b' %s %s\n' % (k, v))
853 > ui.write(b' %s %s\n' % (k, v))
854 >
854 >
855 > def passhook(ui, repo, **args):
855 > def passhook(ui, repo, **args):
856 > printargs(ui, args)
856 > printargs(ui, args)
857 >
857 >
858 > def failhook(ui, repo, **args):
858 > def failhook(ui, repo, **args):
859 > printargs(ui, args)
859 > printargs(ui, args)
860 > return True
860 > return True
861 >
861 >
862 > class LocalException(Exception):
862 > class LocalException(Exception):
863 > pass
863 > pass
864 >
864 >
865 > def raisehook(**args):
865 > def raisehook(**args):
866 > raise LocalException('exception from hook')
866 > raise LocalException('exception from hook')
867 >
867 >
868 > def aborthook(**args):
868 > def aborthook(**args):
869 > raise error.Abort(b'raise abort from hook')
869 > raise error.Abort(b'raise abort from hook')
870 >
870 >
871 > def brokenhook(**args):
871 > def brokenhook(**args):
872 > return 1 + {}
872 > return 1 + {}
873 >
873 >
874 > def verbosehook(ui, **args):
874 > def verbosehook(ui, **args):
875 > ui.note(b'verbose output from hook\n')
875 > ui.note(b'verbose output from hook\n')
876 >
876 >
877 > def printtags(ui, repo, **args):
877 > def printtags(ui, repo, **args):
878 > ui.write(b'[%s]\n' % b', '.join(sorted(repo.tags())))
878 > ui.write(b'[%s]\n' % b', '.join(sorted(repo.tags())))
879 >
879 >
880 > class container(object):
880 > class container(object):
881 > unreachable = 1
881 > unreachable = 1
882 > EOF
882 > EOF
883
883
884 $ cat > syntaxerror.py << NO_CHECK_EOF
884 $ cat > syntaxerror.py << NO_CHECK_EOF
885 > (foo
885 > (foo
886 > NO_CHECK_EOF
886 > NO_CHECK_EOF
887
887
888 test python hooks
888 test python hooks
889
889
890 #if windows
890 #if windows
891 $ PYTHONPATH="$TESTTMP/b;$PYTHONPATH"
891 $ PYTHONPATH="$TESTTMP/b;$PYTHONPATH"
892 #else
892 #else
893 $ PYTHONPATH="$TESTTMP/b:$PYTHONPATH"
893 $ PYTHONPATH="$TESTTMP/b:$PYTHONPATH"
894 #endif
894 #endif
895 $ export PYTHONPATH
895 $ export PYTHONPATH
896
896
897 $ echo '[hooks]' > ../a/.hg/hgrc
897 $ echo '[hooks]' > ../a/.hg/hgrc
898 $ echo 'preoutgoing.broken = python:hooktests.brokenhook' >> ../a/.hg/hgrc
898 $ echo 'preoutgoing.broken = python:hooktests.brokenhook' >> ../a/.hg/hgrc
899 $ hg pull ../a 2>&1 | grep 'raised an exception'
899 $ hg pull ../a 2>&1 | grep 'raised an exception'
900 error: preoutgoing.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
900 error: preoutgoing.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
901
901
902 $ echo '[hooks]' > ../a/.hg/hgrc
902 $ echo '[hooks]' > ../a/.hg/hgrc
903 $ echo 'preoutgoing.raise = python:hooktests.raisehook' >> ../a/.hg/hgrc
903 $ echo 'preoutgoing.raise = python:hooktests.raisehook' >> ../a/.hg/hgrc
904 $ hg pull ../a 2>&1 | grep 'raised an exception'
904 $ hg pull ../a 2>&1 | grep 'raised an exception'
905 error: preoutgoing.raise hook raised an exception: exception from hook
905 error: preoutgoing.raise hook raised an exception: exception from hook
906
906
907 $ echo '[hooks]' > ../a/.hg/hgrc
907 $ echo '[hooks]' > ../a/.hg/hgrc
908 $ echo 'preoutgoing.abort = python:hooktests.aborthook' >> ../a/.hg/hgrc
908 $ echo 'preoutgoing.abort = python:hooktests.aborthook' >> ../a/.hg/hgrc
909 $ hg pull ../a
909 $ hg pull ../a
910 pulling from ../a
910 pulling from ../a
911 searching for changes
911 searching for changes
912 error: preoutgoing.abort hook failed: raise abort from hook
912 error: preoutgoing.abort hook failed: raise abort from hook
913 abort: raise abort from hook
913 abort: raise abort from hook
914 [255]
914 [255]
915
915
916 $ echo '[hooks]' > ../a/.hg/hgrc
916 $ echo '[hooks]' > ../a/.hg/hgrc
917 $ echo 'preoutgoing.fail = python:hooktests.failhook' >> ../a/.hg/hgrc
917 $ echo 'preoutgoing.fail = python:hooktests.failhook' >> ../a/.hg/hgrc
918 $ hg pull ../a
918 $ hg pull ../a
919 pulling from ../a
919 pulling from ../a
920 searching for changes
920 searching for changes
921 hook args:
921 hook args:
922 hooktype preoutgoing
922 hooktype preoutgoing
923 source pull
923 source pull
924 abort: preoutgoing.fail hook failed
924 abort: preoutgoing.fail hook failed
925 [40]
925 [40]
926
926
927 $ echo '[hooks]' > ../a/.hg/hgrc
927 $ echo '[hooks]' > ../a/.hg/hgrc
928 $ echo 'preoutgoing.uncallable = python:hooktests.uncallable' >> ../a/.hg/hgrc
928 $ echo 'preoutgoing.uncallable = python:hooktests.uncallable' >> ../a/.hg/hgrc
929 $ hg pull ../a
929 $ hg pull ../a
930 pulling from ../a
930 pulling from ../a
931 searching for changes
931 searching for changes
932 abort: preoutgoing.uncallable hook is invalid: "hooktests.uncallable" is not callable
932 abort: preoutgoing.uncallable hook is invalid: "hooktests.uncallable" is not callable
933 [255]
933 [255]
934
934
935 $ echo '[hooks]' > ../a/.hg/hgrc
935 $ echo '[hooks]' > ../a/.hg/hgrc
936 $ echo 'preoutgoing.nohook = python:hooktests.nohook' >> ../a/.hg/hgrc
936 $ echo 'preoutgoing.nohook = python:hooktests.nohook' >> ../a/.hg/hgrc
937 $ hg pull ../a
937 $ hg pull ../a
938 pulling from ../a
938 pulling from ../a
939 searching for changes
939 searching for changes
940 abort: preoutgoing.nohook hook is invalid: "hooktests.nohook" is not defined
940 abort: preoutgoing.nohook hook is invalid: "hooktests.nohook" is not defined
941 [255]
941 [255]
942
942
943 $ echo '[hooks]' > ../a/.hg/hgrc
943 $ echo '[hooks]' > ../a/.hg/hgrc
944 $ echo 'preoutgoing.nomodule = python:nomodule' >> ../a/.hg/hgrc
944 $ echo 'preoutgoing.nomodule = python:nomodule' >> ../a/.hg/hgrc
945 $ hg pull ../a
945 $ hg pull ../a
946 pulling from ../a
946 pulling from ../a
947 searching for changes
947 searching for changes
948 abort: preoutgoing.nomodule hook is invalid: "nomodule" not in a module
948 abort: preoutgoing.nomodule hook is invalid: "nomodule" not in a module
949 [255]
949 [255]
950
950
951 $ echo '[hooks]' > ../a/.hg/hgrc
951 $ echo '[hooks]' > ../a/.hg/hgrc
952 $ echo 'preoutgoing.badmodule = python:nomodule.nowhere' >> ../a/.hg/hgrc
952 $ echo 'preoutgoing.badmodule = python:nomodule.nowhere' >> ../a/.hg/hgrc
953 $ hg pull ../a
953 $ hg pull ../a
954 pulling from ../a
954 pulling from ../a
955 searching for changes
955 searching for changes
956 abort: preoutgoing.badmodule hook is invalid: import of "nomodule" failed
956 abort: preoutgoing.badmodule hook is invalid: import of "nomodule" failed
957 (run with --traceback for stack trace)
957 (run with --traceback for stack trace)
958 [255]
958 [255]
959
959
960 $ echo '[hooks]' > ../a/.hg/hgrc
960 $ echo '[hooks]' > ../a/.hg/hgrc
961 $ echo 'preoutgoing.unreachable = python:hooktests.container.unreachable' >> ../a/.hg/hgrc
961 $ echo 'preoutgoing.unreachable = python:hooktests.container.unreachable' >> ../a/.hg/hgrc
962 $ hg pull ../a
962 $ hg pull ../a
963 pulling from ../a
963 pulling from ../a
964 searching for changes
964 searching for changes
965 abort: preoutgoing.unreachable hook is invalid: import of "hooktests.container" failed
965 abort: preoutgoing.unreachable hook is invalid: import of "hooktests.container" failed
966 (run with --traceback for stack trace)
966 (run with --traceback for stack trace)
967 [255]
967 [255]
968
968
969 $ echo '[hooks]' > ../a/.hg/hgrc
969 $ echo '[hooks]' > ../a/.hg/hgrc
970 $ echo 'preoutgoing.syntaxerror = python:syntaxerror.syntaxerror' >> ../a/.hg/hgrc
970 $ echo 'preoutgoing.syntaxerror = python:syntaxerror.syntaxerror' >> ../a/.hg/hgrc
971 $ hg pull ../a
971 $ hg pull ../a
972 pulling from ../a
972 pulling from ../a
973 searching for changes
973 searching for changes
974 abort: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
974 abort: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
975 (run with --traceback for stack trace)
975 (run with --traceback for stack trace)
976 [255]
976 [255]
977
977
978 $ hg pull ../a --traceback 2>&1 | grep -E 'pulling|searching|^exception|Traceback|SyntaxError|ImportError|ModuleNotFoundError|HookLoadError|abort'
978 $ hg pull ../a --traceback 2>&1 | grep -E 'pulling|searching|^exception|Traceback|SyntaxError|ImportError|ModuleNotFoundError|HookLoadError|abort'
979 pulling from ../a
979 pulling from ../a
980 searching for changes
980 searching for changes
981 exception from first failed import attempt:
981 exception from first failed import attempt:
982 Traceback (most recent call last):
982 Traceback (most recent call last):
983 SyntaxError: * (glob)
983 SyntaxError: * (glob)
984 exception from second failed import attempt:
984 exception from second failed import attempt:
985 Traceback (most recent call last):
985 Traceback (most recent call last):
986 SyntaxError: * (glob)
986 SyntaxError: * (glob)
987 Traceback (most recent call last):
987 Traceback (most recent call last):
988 ModuleNotFoundError: No module named 'hgext_syntaxerror'
988 ModuleNotFoundError: No module named 'hgext_syntaxerror'
989 Traceback (most recent call last):
989 Traceback (most recent call last):
990 SyntaxError: * (glob)
990 SyntaxError: * (glob)
991 Traceback (most recent call last):
991 Traceback (most recent call last):
992 ModuleNotFoundError: No module named 'hgext_syntaxerror'
992 ModuleNotFoundError: No module named 'hgext_syntaxerror'
993 Traceback (most recent call last):
993 Traceback (most recent call last):
994 raise error.HookLoadError( (py38 !)
994 raise error.HookLoadError(msg, hint=tracebackhint) (py37 !)
995 mercurial.error.HookLoadError: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
995 mercurial.error.HookLoadError: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
996 abort: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
996 abort: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
997
997
998 $ echo '[hooks]' > ../a/.hg/hgrc
998 $ echo '[hooks]' > ../a/.hg/hgrc
999 $ echo 'preoutgoing.pass = python:hooktests.passhook' >> ../a/.hg/hgrc
999 $ echo 'preoutgoing.pass = python:hooktests.passhook' >> ../a/.hg/hgrc
1000 $ hg pull ../a
1000 $ hg pull ../a
1001 pulling from ../a
1001 pulling from ../a
1002 searching for changes
1002 searching for changes
1003 hook args:
1003 hook args:
1004 hooktype preoutgoing
1004 hooktype preoutgoing
1005 source pull
1005 source pull
1006 adding changesets
1006 adding changesets
1007 adding manifests
1007 adding manifests
1008 adding file changes
1008 adding file changes
1009 adding remote bookmark quux
1009 adding remote bookmark quux
1010 added 1 changesets with 1 changes to 1 files
1010 added 1 changesets with 1 changes to 1 files
1011 new changesets 539e4b31b6dc
1011 new changesets 539e4b31b6dc
1012 (run 'hg update' to get a working copy)
1012 (run 'hg update' to get a working copy)
1013
1013
1014 post- python hooks that fail to *run* don't cause an abort
1014 post- python hooks that fail to *run* don't cause an abort
1015 $ rm ../a/.hg/hgrc
1015 $ rm ../a/.hg/hgrc
1016 $ echo '[hooks]' > .hg/hgrc
1016 $ echo '[hooks]' > .hg/hgrc
1017 $ echo 'post-pull.broken = python:hooktests.brokenhook' >> .hg/hgrc
1017 $ echo 'post-pull.broken = python:hooktests.brokenhook' >> .hg/hgrc
1018 $ hg pull ../a
1018 $ hg pull ../a
1019 pulling from ../a
1019 pulling from ../a
1020 searching for changes
1020 searching for changes
1021 no changes found
1021 no changes found
1022 error: post-pull.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
1022 error: post-pull.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
1023 (run with --traceback for stack trace)
1023 (run with --traceback for stack trace)
1024
1024
1025 but post- python hooks that fail to *load* do
1025 but post- python hooks that fail to *load* do
1026 $ echo '[hooks]' > .hg/hgrc
1026 $ echo '[hooks]' > .hg/hgrc
1027 $ echo 'post-pull.nomodule = python:nomodule' >> .hg/hgrc
1027 $ echo 'post-pull.nomodule = python:nomodule' >> .hg/hgrc
1028 $ hg pull ../a
1028 $ hg pull ../a
1029 pulling from ../a
1029 pulling from ../a
1030 searching for changes
1030 searching for changes
1031 no changes found
1031 no changes found
1032 abort: post-pull.nomodule hook is invalid: "nomodule" not in a module
1032 abort: post-pull.nomodule hook is invalid: "nomodule" not in a module
1033 [255]
1033 [255]
1034
1034
1035 $ echo '[hooks]' > .hg/hgrc
1035 $ echo '[hooks]' > .hg/hgrc
1036 $ echo 'post-pull.badmodule = python:nomodule.nowhere' >> .hg/hgrc
1036 $ echo 'post-pull.badmodule = python:nomodule.nowhere' >> .hg/hgrc
1037 $ hg pull ../a
1037 $ hg pull ../a
1038 pulling from ../a
1038 pulling from ../a
1039 searching for changes
1039 searching for changes
1040 no changes found
1040 no changes found
1041 abort: post-pull.badmodule hook is invalid: import of "nomodule" failed
1041 abort: post-pull.badmodule hook is invalid: import of "nomodule" failed
1042 (run with --traceback for stack trace)
1042 (run with --traceback for stack trace)
1043 [255]
1043 [255]
1044
1044
1045 $ echo '[hooks]' > .hg/hgrc
1045 $ echo '[hooks]' > .hg/hgrc
1046 $ echo 'post-pull.nohook = python:hooktests.nohook' >> .hg/hgrc
1046 $ echo 'post-pull.nohook = python:hooktests.nohook' >> .hg/hgrc
1047 $ hg pull ../a
1047 $ hg pull ../a
1048 pulling from ../a
1048 pulling from ../a
1049 searching for changes
1049 searching for changes
1050 no changes found
1050 no changes found
1051 abort: post-pull.nohook hook is invalid: "hooktests.nohook" is not defined
1051 abort: post-pull.nohook hook is invalid: "hooktests.nohook" is not defined
1052 [255]
1052 [255]
1053
1053
1054 make sure --traceback works
1054 make sure --traceback works
1055
1055
1056 $ echo '[hooks]' > .hg/hgrc
1056 $ echo '[hooks]' > .hg/hgrc
1057 $ echo 'commit.abort = python:hooktests.aborthook' >> .hg/hgrc
1057 $ echo 'commit.abort = python:hooktests.aborthook' >> .hg/hgrc
1058
1058
1059 $ echo aa > a
1059 $ echo aa > a
1060 $ hg --traceback commit -d '0 0' -ma 2>&1 | grep '^Traceback'
1060 $ hg --traceback commit -d '0 0' -ma 2>&1 | grep '^Traceback'
1061 Traceback (most recent call last):
1061 Traceback (most recent call last):
1062
1062
1063 $ cd ..
1063 $ cd ..
1064 $ hg init c
1064 $ hg init c
1065 $ cd c
1065 $ cd c
1066
1066
1067 $ cat > hookext.py <<EOF
1067 $ cat > hookext.py <<EOF
1068 > def autohook(ui, **args):
1068 > def autohook(ui, **args):
1069 > ui.write(b'Automatically installed hook\n')
1069 > ui.write(b'Automatically installed hook\n')
1070 >
1070 >
1071 > def reposetup(ui, repo):
1071 > def reposetup(ui, repo):
1072 > repo.ui.setconfig(b"hooks", b"commit.auto", autohook)
1072 > repo.ui.setconfig(b"hooks", b"commit.auto", autohook)
1073 > EOF
1073 > EOF
1074 $ echo '[extensions]' >> .hg/hgrc
1074 $ echo '[extensions]' >> .hg/hgrc
1075 $ echo 'hookext = hookext.py' >> .hg/hgrc
1075 $ echo 'hookext = hookext.py' >> .hg/hgrc
1076
1076
1077 $ touch foo
1077 $ touch foo
1078 $ hg add foo
1078 $ hg add foo
1079 $ hg ci -d '0 0' -m 'add foo'
1079 $ hg ci -d '0 0' -m 'add foo'
1080 Automatically installed hook
1080 Automatically installed hook
1081 $ echo >> foo
1081 $ echo >> foo
1082 $ hg ci --debug -d '0 0' -m 'change foo'
1082 $ hg ci --debug -d '0 0' -m 'change foo'
1083 committing files:
1083 committing files:
1084 foo
1084 foo
1085 committing manifest
1085 committing manifest
1086 committing changelog
1086 committing changelog
1087 updating the branch cache
1087 updating the branch cache
1088 committed changeset 1:52998019f6252a2b893452765fcb0a47351a5708
1088 committed changeset 1:52998019f6252a2b893452765fcb0a47351a5708
1089 calling hook commit.auto: hgext_hookext.autohook
1089 calling hook commit.auto: hgext_hookext.autohook
1090 Automatically installed hook
1090 Automatically installed hook
1091
1091
1092 $ hg showconfig hooks
1092 $ hg showconfig hooks
1093 hooks.commit.auto=<function autohook at *> (glob)
1093 hooks.commit.auto=<function autohook at *> (glob)
1094
1094
1095 test python hook configured with python:[file]:[hook] syntax
1095 test python hook configured with python:[file]:[hook] syntax
1096
1096
1097 $ cd ..
1097 $ cd ..
1098 $ mkdir d
1098 $ mkdir d
1099 $ cd d
1099 $ cd d
1100 $ hg init repo
1100 $ hg init repo
1101 $ mkdir hooks
1101 $ mkdir hooks
1102
1102
1103 $ cd hooks
1103 $ cd hooks
1104 $ cat > testhooks.py <<EOF
1104 $ cat > testhooks.py <<EOF
1105 > def testhook(ui, **args):
1105 > def testhook(ui, **args):
1106 > ui.write(b'hook works\n')
1106 > ui.write(b'hook works\n')
1107 > EOF
1107 > EOF
1108 $ echo '[hooks]' > ../repo/.hg/hgrc
1108 $ echo '[hooks]' > ../repo/.hg/hgrc
1109 $ echo "pre-commit.test = python:`pwd`/testhooks.py:testhook" >> ../repo/.hg/hgrc
1109 $ echo "pre-commit.test = python:`pwd`/testhooks.py:testhook" >> ../repo/.hg/hgrc
1110
1110
1111 $ cd ../repo
1111 $ cd ../repo
1112 $ hg commit -d '0 0'
1112 $ hg commit -d '0 0'
1113 hook works
1113 hook works
1114 nothing changed
1114 nothing changed
1115 [1]
1115 [1]
1116
1116
1117 $ echo '[hooks]' > .hg/hgrc
1117 $ echo '[hooks]' > .hg/hgrc
1118 $ echo "update.ne = python:`pwd`/nonexistent.py:testhook" >> .hg/hgrc
1118 $ echo "update.ne = python:`pwd`/nonexistent.py:testhook" >> .hg/hgrc
1119 $ echo "pre-identify.npmd = python:`pwd`/:no_python_module_dir" >> .hg/hgrc
1119 $ echo "pre-identify.npmd = python:`pwd`/:no_python_module_dir" >> .hg/hgrc
1120
1120
1121 $ hg up null
1121 $ hg up null
1122 loading update.ne hook failed:
1122 loading update.ne hook failed:
1123 abort: $ENOENT$: '$TESTTMP/d/repo/nonexistent.py'
1123 abort: $ENOENT$: '$TESTTMP/d/repo/nonexistent.py'
1124 [255]
1124 [255]
1125
1125
1126 $ hg id
1126 $ hg id
1127 loading pre-identify.npmd hook failed:
1127 loading pre-identify.npmd hook failed:
1128 abort: No module named 'repo'
1128 abort: No module named 'repo'
1129 [255]
1129 [255]
1130
1130
1131 $ cd ../../b
1131 $ cd ../../b
1132
1132
1133 make sure --traceback works on hook import failure
1133 make sure --traceback works on hook import failure
1134
1134
1135 $ cat > importfail.py <<EOF
1135 $ cat > importfail.py <<EOF
1136 > import somebogusmodule
1136 > import somebogusmodule
1137 > # dereference something in the module to force demandimport to load it
1137 > # dereference something in the module to force demandimport to load it
1138 > somebogusmodule.whatever
1138 > somebogusmodule.whatever
1139 > EOF
1139 > EOF
1140
1140
1141 $ echo '[hooks]' > .hg/hgrc
1141 $ echo '[hooks]' > .hg/hgrc
1142 $ echo 'precommit.importfail = python:importfail.whatever' >> .hg/hgrc
1142 $ echo 'precommit.importfail = python:importfail.whatever' >> .hg/hgrc
1143
1143
1144 $ echo a >> a
1144 $ echo a >> a
1145 $ hg --traceback commit -ma 2>&1 | grep -E '^exception|ImportError|ModuleNotFoundError|Traceback|HookLoadError|abort'
1145 $ hg --traceback commit -ma 2>&1 | grep -E '^exception|ImportError|ModuleNotFoundError|Traceback|HookLoadError|abort'
1146 exception from first failed import attempt:
1146 exception from first failed import attempt:
1147 Traceback (most recent call last):
1147 Traceback (most recent call last):
1148 ModuleNotFoundError: No module named 'somebogusmodule'
1148 ModuleNotFoundError: No module named 'somebogusmodule'
1149 exception from second failed import attempt:
1149 exception from second failed import attempt:
1150 Traceback (most recent call last):
1150 Traceback (most recent call last):
1151 ModuleNotFoundError: No module named 'somebogusmodule'
1151 ModuleNotFoundError: No module named 'somebogusmodule'
1152 Traceback (most recent call last):
1152 Traceback (most recent call last):
1153 ModuleNotFoundError: No module named 'hgext_importfail'
1153 ModuleNotFoundError: No module named 'hgext_importfail'
1154 Traceback (most recent call last):
1154 Traceback (most recent call last):
1155 ModuleNotFoundError: No module named 'somebogusmodule'
1155 ModuleNotFoundError: No module named 'somebogusmodule'
1156 Traceback (most recent call last):
1156 Traceback (most recent call last):
1157 ModuleNotFoundError: No module named 'hgext_importfail'
1157 ModuleNotFoundError: No module named 'hgext_importfail'
1158 Traceback (most recent call last):
1158 Traceback (most recent call last):
1159 raise error.HookLoadError( (py38 !)
1159 raise error.HookLoadError(msg, hint=tracebackhint) (py37 !)
1160 mercurial.error.HookLoadError: precommit.importfail hook is invalid: import of "importfail" failed
1160 mercurial.error.HookLoadError: precommit.importfail hook is invalid: import of "importfail" failed
1161 abort: precommit.importfail hook is invalid: import of "importfail" failed
1161 abort: precommit.importfail hook is invalid: import of "importfail" failed
1162
1162
1163 Issue1827: Hooks Update & Commit not completely post operation
1163 Issue1827: Hooks Update & Commit not completely post operation
1164
1164
1165 commit and update hooks should run after command completion. The largefiles
1165 commit and update hooks should run after command completion. The largefiles
1166 use demonstrates a recursive wlock, showing the hook doesn't run until the
1166 use demonstrates a recursive wlock, showing the hook doesn't run until the
1167 final release (and dirstate flush).
1167 final release (and dirstate flush).
1168
1168
1169 $ echo '[hooks]' > .hg/hgrc
1169 $ echo '[hooks]' > .hg/hgrc
1170 $ echo 'commit = hg id' >> .hg/hgrc
1170 $ echo 'commit = hg id' >> .hg/hgrc
1171 $ echo 'update = hg id' >> .hg/hgrc
1171 $ echo 'update = hg id' >> .hg/hgrc
1172 $ echo bb > a
1172 $ echo bb > a
1173 $ hg ci -ma
1173 $ hg ci -ma
1174 223eafe2750c tip
1174 223eafe2750c tip
1175 $ hg up 0 --config extensions.largefiles=
1175 $ hg up 0 --config extensions.largefiles=
1176 The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
1176 The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
1177 cb9a9f314b8b
1177 cb9a9f314b8b
1178 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1178 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1179
1179
1180 make sure --verbose (and --quiet/--debug etc.) are propagated to the local ui
1180 make sure --verbose (and --quiet/--debug etc.) are propagated to the local ui
1181 that is passed to pre/post hooks
1181 that is passed to pre/post hooks
1182
1182
1183 $ echo '[hooks]' > .hg/hgrc
1183 $ echo '[hooks]' > .hg/hgrc
1184 $ echo 'pre-identify = python:hooktests.verbosehook' >> .hg/hgrc
1184 $ echo 'pre-identify = python:hooktests.verbosehook' >> .hg/hgrc
1185 $ hg id
1185 $ hg id
1186 cb9a9f314b8b
1186 cb9a9f314b8b
1187 $ hg id --verbose
1187 $ hg id --verbose
1188 calling hook pre-identify: hooktests.verbosehook
1188 calling hook pre-identify: hooktests.verbosehook
1189 verbose output from hook
1189 verbose output from hook
1190 cb9a9f314b8b
1190 cb9a9f314b8b
1191
1191
1192 Ensure hooks can be prioritized
1192 Ensure hooks can be prioritized
1193
1193
1194 $ echo '[hooks]' > .hg/hgrc
1194 $ echo '[hooks]' > .hg/hgrc
1195 $ echo 'pre-identify.a = python:hooktests.verbosehook' >> .hg/hgrc
1195 $ echo 'pre-identify.a = python:hooktests.verbosehook' >> .hg/hgrc
1196 $ echo 'pre-identify.b = python:hooktests.verbosehook' >> .hg/hgrc
1196 $ echo 'pre-identify.b = python:hooktests.verbosehook' >> .hg/hgrc
1197 $ echo 'priority.pre-identify.b = 1' >> .hg/hgrc
1197 $ echo 'priority.pre-identify.b = 1' >> .hg/hgrc
1198 $ echo 'pre-identify.c = python:hooktests.verbosehook' >> .hg/hgrc
1198 $ echo 'pre-identify.c = python:hooktests.verbosehook' >> .hg/hgrc
1199 $ hg id --verbose
1199 $ hg id --verbose
1200 calling hook pre-identify.b: hooktests.verbosehook
1200 calling hook pre-identify.b: hooktests.verbosehook
1201 verbose output from hook
1201 verbose output from hook
1202 calling hook pre-identify.a: hooktests.verbosehook
1202 calling hook pre-identify.a: hooktests.verbosehook
1203 verbose output from hook
1203 verbose output from hook
1204 calling hook pre-identify.c: hooktests.verbosehook
1204 calling hook pre-identify.c: hooktests.verbosehook
1205 verbose output from hook
1205 verbose output from hook
1206 cb9a9f314b8b
1206 cb9a9f314b8b
1207
1207
1208 new tags must be visible in pretxncommit (issue3210)
1208 new tags must be visible in pretxncommit (issue3210)
1209
1209
1210 $ echo 'pretxncommit.printtags = python:hooktests.printtags' >> .hg/hgrc
1210 $ echo 'pretxncommit.printtags = python:hooktests.printtags' >> .hg/hgrc
1211 $ hg tag -f foo
1211 $ hg tag -f foo
1212 [a, foo, tip]
1212 [a, foo, tip]
1213
1213
1214 post-init hooks must not crash (issue4983)
1214 post-init hooks must not crash (issue4983)
1215 This also creates the `to` repo for the next test block.
1215 This also creates the `to` repo for the next test block.
1216
1216
1217 $ cd ..
1217 $ cd ..
1218 $ cat << EOF >> hgrc-with-post-init-hook
1218 $ cat << EOF >> hgrc-with-post-init-hook
1219 > [hooks]
1219 > [hooks]
1220 > post-init = sh -c "printenv.py --line post-init"
1220 > post-init = sh -c "printenv.py --line post-init"
1221 > EOF
1221 > EOF
1222 $ HGRCPATH=hgrc-with-post-init-hook hg init to
1222 $ HGRCPATH=hgrc-with-post-init-hook hg init to
1223 post-init hook: HG_ARGS=init to
1223 post-init hook: HG_ARGS=init to
1224 HG_HOOKNAME=post-init
1224 HG_HOOKNAME=post-init
1225 HG_HOOKTYPE=post-init
1225 HG_HOOKTYPE=post-init
1226 HG_OPTS={'insecure': None, 'remotecmd': '', 'ssh': ''}
1226 HG_OPTS={'insecure': None, 'remotecmd': '', 'ssh': ''}
1227 HG_PATS=['to']
1227 HG_PATS=['to']
1228 HG_RESULT=0
1228 HG_RESULT=0
1229
1229
1230
1230
1231 new commits must be visible in pretxnchangegroup (issue3428)
1231 new commits must be visible in pretxnchangegroup (issue3428)
1232
1232
1233 $ echo '[hooks]' >> to/.hg/hgrc
1233 $ echo '[hooks]' >> to/.hg/hgrc
1234 $ echo 'prechangegroup = hg --traceback tip' >> to/.hg/hgrc
1234 $ echo 'prechangegroup = hg --traceback tip' >> to/.hg/hgrc
1235 $ echo 'pretxnchangegroup = hg --traceback tip' >> to/.hg/hgrc
1235 $ echo 'pretxnchangegroup = hg --traceback tip' >> to/.hg/hgrc
1236 $ echo a >> to/a
1236 $ echo a >> to/a
1237 $ hg --cwd to ci -Ama
1237 $ hg --cwd to ci -Ama
1238 adding a
1238 adding a
1239 $ hg clone to from
1239 $ hg clone to from
1240 updating to branch default
1240 updating to branch default
1241 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1241 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1242 $ echo aa >> from/a
1242 $ echo aa >> from/a
1243 $ hg --cwd from ci -mb
1243 $ hg --cwd from ci -mb
1244 $ hg --cwd from push
1244 $ hg --cwd from push
1245 pushing to $TESTTMP/to
1245 pushing to $TESTTMP/to
1246 searching for changes
1246 searching for changes
1247 changeset: 0:cb9a9f314b8b
1247 changeset: 0:cb9a9f314b8b
1248 tag: tip
1248 tag: tip
1249 user: test
1249 user: test
1250 date: Thu Jan 01 00:00:00 1970 +0000
1250 date: Thu Jan 01 00:00:00 1970 +0000
1251 summary: a
1251 summary: a
1252
1252
1253 adding changesets
1253 adding changesets
1254 adding manifests
1254 adding manifests
1255 adding file changes
1255 adding file changes
1256 changeset: 1:9836a07b9b9d
1256 changeset: 1:9836a07b9b9d
1257 tag: tip
1257 tag: tip
1258 user: test
1258 user: test
1259 date: Thu Jan 01 00:00:00 1970 +0000
1259 date: Thu Jan 01 00:00:00 1970 +0000
1260 summary: b
1260 summary: b
1261
1261
1262 added 1 changesets with 1 changes to 1 files
1262 added 1 changesets with 1 changes to 1 files
1263
1263
1264 pretxnclose hook failure should abort the transaction
1264 pretxnclose hook failure should abort the transaction
1265
1265
1266 $ hg init txnfailure
1266 $ hg init txnfailure
1267 $ cd txnfailure
1267 $ cd txnfailure
1268 $ touch a && hg commit -Aqm a
1268 $ touch a && hg commit -Aqm a
1269 $ cat >> .hg/hgrc <<EOF
1269 $ cat >> .hg/hgrc <<EOF
1270 > [hooks]
1270 > [hooks]
1271 > pretxnclose.error = exit 1
1271 > pretxnclose.error = exit 1
1272 > EOF
1272 > EOF
1273 $ hg strip -r 0 --config extensions.strip=
1273 $ hg strip -r 0 --config extensions.strip=
1274 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1274 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1275 saved backup bundle to * (glob)
1275 saved backup bundle to * (glob)
1276 transaction abort!
1276 transaction abort!
1277 rollback completed
1277 rollback completed
1278 strip failed, backup bundle stored in * (glob)
1278 strip failed, backup bundle stored in * (glob)
1279 abort: pretxnclose.error hook exited with status 1
1279 abort: pretxnclose.error hook exited with status 1
1280 [40]
1280 [40]
1281 $ hg recover
1281 $ hg recover
1282 no interrupted transaction available
1282 no interrupted transaction available
1283 [1]
1283 [1]
1284 $ cd ..
1284 $ cd ..
1285
1285
1286 check whether HG_PENDING makes pending changes only in related
1286 check whether HG_PENDING makes pending changes only in related
1287 repositories visible to an external hook.
1287 repositories visible to an external hook.
1288
1288
1289 (emulate a transaction running concurrently by copied
1289 (emulate a transaction running concurrently by copied
1290 .hg/store/00changelog.i.a in subsequent test)
1290 .hg/store/00changelog.i.a in subsequent test)
1291
1291
1292 $ cat > $TESTTMP/savepending.sh <<EOF
1292 $ cat > $TESTTMP/savepending.sh <<EOF
1293 > cp .hg/store/00changelog.i.a .hg/store/00changelog.i.a.saved
1293 > cp .hg/store/00changelog.i.a .hg/store/00changelog.i.a.saved
1294 > exit 1 # to avoid adding new revision for subsequent tests
1294 > exit 1 # to avoid adding new revision for subsequent tests
1295 > EOF
1295 > EOF
1296 $ cd a
1296 $ cd a
1297 $ hg tip -q
1297 $ hg tip -q
1298 4:539e4b31b6dc
1298 4:539e4b31b6dc
1299 $ hg --config hooks.pretxnclose="sh $TESTTMP/savepending.sh" commit -m "invisible"
1299 $ hg --config hooks.pretxnclose="sh $TESTTMP/savepending.sh" commit -m "invisible"
1300 transaction abort!
1300 transaction abort!
1301 rollback completed
1301 rollback completed
1302 abort: pretxnclose hook exited with status 1
1302 abort: pretxnclose hook exited with status 1
1303 [40]
1303 [40]
1304 $ cp .hg/store/00changelog.i.a.saved .hg/store/00changelog.i.a
1304 $ cp .hg/store/00changelog.i.a.saved .hg/store/00changelog.i.a
1305
1305
1306 (check (in)visibility of new changeset while transaction running in
1306 (check (in)visibility of new changeset while transaction running in
1307 repo)
1307 repo)
1308
1308
1309 $ cat > $TESTTMP/checkpending.sh <<EOF
1309 $ cat > $TESTTMP/checkpending.sh <<EOF
1310 > echo '@a'
1310 > echo '@a'
1311 > hg -R "$TESTTMP/a" tip -q
1311 > hg -R "$TESTTMP/a" tip -q
1312 > echo '@a/nested'
1312 > echo '@a/nested'
1313 > hg -R "$TESTTMP/a/nested" tip -q
1313 > hg -R "$TESTTMP/a/nested" tip -q
1314 > exit 1 # to avoid adding new revision for subsequent tests
1314 > exit 1 # to avoid adding new revision for subsequent tests
1315 > EOF
1315 > EOF
1316 $ hg init nested
1316 $ hg init nested
1317 $ cd nested
1317 $ cd nested
1318 $ echo a > a
1318 $ echo a > a
1319 $ hg add a
1319 $ hg add a
1320 $ hg --config hooks.pretxnclose="sh $TESTTMP/checkpending.sh" commit -m '#0'
1320 $ hg --config hooks.pretxnclose="sh $TESTTMP/checkpending.sh" commit -m '#0'
1321 @a
1321 @a
1322 4:539e4b31b6dc
1322 4:539e4b31b6dc
1323 @a/nested
1323 @a/nested
1324 0:bf5e395ced2c
1324 0:bf5e395ced2c
1325 transaction abort!
1325 transaction abort!
1326 rollback completed
1326 rollback completed
1327 abort: pretxnclose hook exited with status 1
1327 abort: pretxnclose hook exited with status 1
1328 [40]
1328 [40]
1329
1329
1330 Hook from untrusted hgrc are reported as failure
1330 Hook from untrusted hgrc are reported as failure
1331 ================================================
1331 ================================================
1332
1332
1333 $ cat << EOF > $TESTTMP/untrusted.py
1333 $ cat << EOF > $TESTTMP/untrusted.py
1334 > from mercurial import scmutil, util
1334 > from mercurial import scmutil, util
1335 > def uisetup(ui):
1335 > def uisetup(ui):
1336 > class untrustedui(ui.__class__):
1336 > class untrustedui(ui.__class__):
1337 > def _trusted(self, fp, f):
1337 > def _trusted(self, fp, f):
1338 > if util.normpath(fp.name).endswith(b'untrusted/.hg/hgrc'):
1338 > if util.normpath(fp.name).endswith(b'untrusted/.hg/hgrc'):
1339 > return False
1339 > return False
1340 > return super(untrustedui, self)._trusted(fp, f)
1340 > return super(untrustedui, self)._trusted(fp, f)
1341 > ui.__class__ = untrustedui
1341 > ui.__class__ = untrustedui
1342 > EOF
1342 > EOF
1343 $ cat << EOF >> $HGRCPATH
1343 $ cat << EOF >> $HGRCPATH
1344 > [extensions]
1344 > [extensions]
1345 > untrusted=$TESTTMP/untrusted.py
1345 > untrusted=$TESTTMP/untrusted.py
1346 > EOF
1346 > EOF
1347 $ hg init untrusted
1347 $ hg init untrusted
1348 $ cd untrusted
1348 $ cd untrusted
1349
1349
1350 Non-blocking hook
1350 Non-blocking hook
1351 -----------------
1351 -----------------
1352
1352
1353 $ cat << EOF >> .hg/hgrc
1353 $ cat << EOF >> .hg/hgrc
1354 > [hooks]
1354 > [hooks]
1355 > txnclose.testing=echo txnclose hook called
1355 > txnclose.testing=echo txnclose hook called
1356 > EOF
1356 > EOF
1357 $ touch a && hg commit -Aqm a
1357 $ touch a && hg commit -Aqm a
1358 warning: untrusted hook txnclose.testing not executed
1358 warning: untrusted hook txnclose.testing not executed
1359 $ hg log
1359 $ hg log
1360 changeset: 0:3903775176ed
1360 changeset: 0:3903775176ed
1361 tag: tip
1361 tag: tip
1362 user: test
1362 user: test
1363 date: Thu Jan 01 00:00:00 1970 +0000
1363 date: Thu Jan 01 00:00:00 1970 +0000
1364 summary: a
1364 summary: a
1365
1365
1366
1366
1367 Non-blocking hook
1367 Non-blocking hook
1368 -----------------
1368 -----------------
1369
1369
1370 $ cat << EOF >> .hg/hgrc
1370 $ cat << EOF >> .hg/hgrc
1371 > [hooks]
1371 > [hooks]
1372 > pretxnclose.testing=echo pre-txnclose hook called
1372 > pretxnclose.testing=echo pre-txnclose hook called
1373 > EOF
1373 > EOF
1374 $ touch b && hg commit -Aqm a
1374 $ touch b && hg commit -Aqm a
1375 transaction abort!
1375 transaction abort!
1376 rollback completed
1376 rollback completed
1377 abort: untrusted hook pretxnclose.testing not executed
1377 abort: untrusted hook pretxnclose.testing not executed
1378 (see 'hg help config.trusted')
1378 (see 'hg help config.trusted')
1379 [40]
1379 [40]
1380 $ hg log
1380 $ hg log
1381 changeset: 0:3903775176ed
1381 changeset: 0:3903775176ed
1382 tag: tip
1382 tag: tip
1383 user: test
1383 user: test
1384 date: Thu Jan 01 00:00:00 1970 +0000
1384 date: Thu Jan 01 00:00:00 1970 +0000
1385 summary: a
1385 summary: a
1386
1386
1387
1387
1388 unsetup the test
1388 unsetup the test
1389 ----------------
1389 ----------------
1390
1390
1391 # touch the file to unconfuse chg with a diffrent mtime
1391 # touch the file to unconfuse chg with a diffrent mtime
1392 $ sleep 1
1392 $ sleep 1
1393 $ touch $TESTTMP/untrusted.py
1393 $ touch $TESTTMP/untrusted.py
1394 $ cat << EOF >> $HGRCPATH
1394 $ cat << EOF >> $HGRCPATH
1395 > [extensions]
1395 > [extensions]
1396 > untrusted=!
1396 > untrusted=!
1397 > EOF
1397 > EOF
1398
1398
1399 HGPLAIN setting in hooks
1399 HGPLAIN setting in hooks
1400 ========================
1400 ========================
1401
1401
1402 $ cat << EOF >> .hg/hgrc
1402 $ cat << EOF >> .hg/hgrc
1403 > [hooks]
1403 > [hooks]
1404 > pre-version.testing-default=sh -c "echo '### default ###' plain: \${HGPLAIN:-'<unset>'}"
1404 > pre-version.testing-default=sh -c "echo '### default ###' plain: \${HGPLAIN:-'<unset>'}"
1405 > pre-version.testing-yes=sh -c "echo '### yes #######' plain: \${HGPLAIN:-'<unset>'}"
1405 > pre-version.testing-yes=sh -c "echo '### yes #######' plain: \${HGPLAIN:-'<unset>'}"
1406 > pre-version.testing-yes:run-with-plain=yes
1406 > pre-version.testing-yes:run-with-plain=yes
1407 > pre-version.testing-no=sh -c "echo '### no ########' plain: \${HGPLAIN:-'<unset>'}"
1407 > pre-version.testing-no=sh -c "echo '### no ########' plain: \${HGPLAIN:-'<unset>'}"
1408 > pre-version.testing-no:run-with-plain=no
1408 > pre-version.testing-no:run-with-plain=no
1409 > pre-version.testing-auto=sh -c "echo '### auto ######' plain: \${HGPLAIN:-'<unset>'}"
1409 > pre-version.testing-auto=sh -c "echo '### auto ######' plain: \${HGPLAIN:-'<unset>'}"
1410 > pre-version.testing-auto:run-with-plain=auto
1410 > pre-version.testing-auto:run-with-plain=auto
1411 > EOF
1411 > EOF
1412
1412
1413 $ (unset HGPLAIN; hg version --quiet)
1413 $ (unset HGPLAIN; hg version --quiet)
1414 ### default ### plain: 1
1414 ### default ### plain: 1
1415 ### yes ####### plain: 1
1415 ### yes ####### plain: 1
1416 ### no ######## plain: <unset>
1416 ### no ######## plain: <unset>
1417 ### auto ###### plain: <unset>
1417 ### auto ###### plain: <unset>
1418 Mercurial Distributed SCM (*) (glob)
1418 Mercurial Distributed SCM (*) (glob)
1419
1419
1420 $ HGPLAIN=1 hg version --quiet
1420 $ HGPLAIN=1 hg version --quiet
1421 ### default ### plain: 1
1421 ### default ### plain: 1
1422 ### yes ####### plain: 1
1422 ### yes ####### plain: 1
1423 ### no ######## plain: <unset>
1423 ### no ######## plain: <unset>
1424 ### auto ###### plain: 1
1424 ### auto ###### plain: 1
1425 Mercurial Distributed SCM (*) (glob)
1425 Mercurial Distributed SCM (*) (glob)
1426
1426
1427 Test hook that change the underlying repo
1427 Test hook that change the underlying repo
1428 =========================================
1428 =========================================
1429
1429
1430 blackbox access the dirstate afterward and can see a changelog / dirstate
1430 blackbox access the dirstate afterward and can see a changelog / dirstate
1431 desync.
1431 desync.
1432
1432
1433
1433
1434 $ cd $TESTTMP
1434 $ cd $TESTTMP
1435 $ cat <<EOF >> $HGRCPATH
1435 $ cat <<EOF >> $HGRCPATH
1436 > [extensions]
1436 > [extensions]
1437 > blackbox=
1437 > blackbox=
1438 > [hooks]
1438 > [hooks]
1439 > post-merge = hg commit -m "auto merge"
1439 > post-merge = hg commit -m "auto merge"
1440 > EOF
1440 > EOF
1441
1441
1442 $ hg init t
1442 $ hg init t
1443 $ cd t
1443 $ cd t
1444 $ touch ".hgignore"
1444 $ touch ".hgignore"
1445 $ hg commit -Am "initial" -d'0 0'
1445 $ hg commit -Am "initial" -d'0 0'
1446 adding .hgignore
1446 adding .hgignore
1447
1447
1448 $ echo This is file a1 > a
1448 $ echo This is file a1 > a
1449 $ hg commit -Am "commit #1" -d'0 0'
1449 $ hg commit -Am "commit #1" -d'0 0'
1450 adding a
1450 adding a
1451
1451
1452 $ hg update 0
1452 $ hg update 0
1453 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1453 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1454 $ echo This is file b1 > b
1454 $ echo This is file b1 > b
1455 $ hg commit -Am "commit #2" -d'0 0'
1455 $ hg commit -Am "commit #2" -d'0 0'
1456 adding b
1456 adding b
1457 created new head
1457 created new head
1458
1458
1459 $ hg merge 1
1459 $ hg merge 1
1460 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1460 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1461 (branch merge, don't forget to commit)
1461 (branch merge, don't forget to commit)
1462
1462
1463 $ cd ..
1463 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now