##// END OF EJS Templates
extensions: prevent a crash on py3 when testing a bad extension minimum...
Matt Harbison -
r48828:a9bedc56 default
parent child Browse files
Show More
@@ -1,953 +1,957 b''
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import ast
11 11 import collections
12 12 import functools
13 13 import imp
14 14 import inspect
15 15 import os
16 16
17 17 from .i18n import (
18 18 _,
19 19 gettext,
20 20 )
21 21 from .pycompat import (
22 22 getattr,
23 23 open,
24 24 setattr,
25 25 )
26 26
27 27 from . import (
28 28 cmdutil,
29 29 configitems,
30 30 error,
31 31 pycompat,
32 32 util,
33 33 )
34 34
35 35 from .utils import stringutil
36 36
37 37 _extensions = {}
38 38 _disabledextensions = {}
39 39 _aftercallbacks = {}
40 40 _order = []
41 41 _builtin = {
42 42 b'hbisect',
43 43 b'bookmarks',
44 44 b'color',
45 45 b'parentrevspec',
46 46 b'progress',
47 47 b'interhg',
48 48 b'inotify',
49 49 b'hgcia',
50 50 b'shelve',
51 51 }
52 52
53 53
54 54 def extensions(ui=None):
55 55 if ui:
56 56
57 57 def enabled(name):
58 58 for format in [b'%s', b'hgext.%s']:
59 59 conf = ui.config(b'extensions', format % name)
60 60 if conf is not None and not conf.startswith(b'!'):
61 61 return True
62 62
63 63 else:
64 64 enabled = lambda name: True
65 65 for name in _order:
66 66 module = _extensions[name]
67 67 if module and enabled(name):
68 68 yield name, module
69 69
70 70
71 71 def find(name):
72 72 '''return module with given extension name'''
73 73 mod = None
74 74 try:
75 75 mod = _extensions[name]
76 76 except KeyError:
77 77 for k, v in pycompat.iteritems(_extensions):
78 78 if k.endswith(b'.' + name) or k.endswith(b'/' + name):
79 79 mod = v
80 80 break
81 81 if not mod:
82 82 raise KeyError(name)
83 83 return mod
84 84
85 85
86 86 def loadpath(path, module_name):
87 87 module_name = module_name.replace(b'.', b'_')
88 88 path = util.normpath(util.expandpath(path))
89 89 module_name = pycompat.fsdecode(module_name)
90 90 path = pycompat.fsdecode(path)
91 91 if os.path.isdir(path):
92 92 # module/__init__.py style
93 93 d, f = os.path.split(path)
94 94 fd, fpath, desc = imp.find_module(f, [d])
95 95 # When https://github.com/python/typeshed/issues/3466 is fixed
96 96 # and in a pytype release we can drop this disable.
97 97 return imp.load_module(
98 98 module_name, fd, fpath, desc # pytype: disable=wrong-arg-types
99 99 )
100 100 else:
101 101 try:
102 102 return imp.load_source(module_name, path)
103 103 except IOError as exc:
104 104 if not exc.filename:
105 105 exc.filename = path # python does not fill this
106 106 raise
107 107
108 108
109 109 def _importh(name):
110 110 """import and return the <name> module"""
111 111 mod = __import__(pycompat.sysstr(name))
112 112 components = name.split(b'.')
113 113 for comp in components[1:]:
114 114 mod = getattr(mod, comp)
115 115 return mod
116 116
117 117
118 118 def _importext(name, path=None, reportfunc=None):
119 119 if path:
120 120 # the module will be loaded in sys.modules
121 121 # choose an unique name so that it doesn't
122 122 # conflicts with other modules
123 123 mod = loadpath(path, b'hgext.%s' % name)
124 124 else:
125 125 try:
126 126 mod = _importh(b"hgext.%s" % name)
127 127 except ImportError as err:
128 128 if reportfunc:
129 129 reportfunc(err, b"hgext.%s" % name, b"hgext3rd.%s" % name)
130 130 try:
131 131 mod = _importh(b"hgext3rd.%s" % name)
132 132 except ImportError as err:
133 133 if reportfunc:
134 134 reportfunc(err, b"hgext3rd.%s" % name, name)
135 135 mod = _importh(name)
136 136 return mod
137 137
138 138
139 139 def _reportimporterror(ui, err, failed, next):
140 140 # note: this ui.log happens before --debug is processed,
141 141 # Use --config ui.debug=1 to see them.
142 142 ui.log(
143 143 b'extension',
144 144 b' - could not import %s (%s): trying %s\n',
145 145 failed,
146 146 stringutil.forcebytestr(err),
147 147 next,
148 148 )
149 149 if ui.debugflag and ui.configbool(b'devel', b'debug.extensions'):
150 150 ui.traceback()
151 151
152 152
153 153 def _rejectunicode(name, xs):
154 154 if isinstance(xs, (list, set, tuple)):
155 155 for x in xs:
156 156 _rejectunicode(name, x)
157 157 elif isinstance(xs, dict):
158 158 for k, v in xs.items():
159 159 _rejectunicode(name, k)
160 160 _rejectunicode(b'%s.%s' % (name, stringutil.forcebytestr(k)), v)
161 161 elif isinstance(xs, type(u'')):
162 162 raise error.ProgrammingError(
163 163 b"unicode %r found in %s" % (xs, name),
164 164 hint=b"use b'' to make it byte string",
165 165 )
166 166
167 167
168 168 # attributes set by registrar.command
169 169 _cmdfuncattrs = (b'norepo', b'optionalrepo', b'inferrepo')
170 170
171 171
172 172 def _validatecmdtable(ui, cmdtable):
173 173 """Check if extension commands have required attributes"""
174 174 for c, e in pycompat.iteritems(cmdtable):
175 175 f = e[0]
176 176 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
177 177 if not missing:
178 178 continue
179 179 raise error.ProgrammingError(
180 180 b'missing attributes: %s' % b', '.join(missing),
181 181 hint=b"use @command decorator to register '%s'" % c,
182 182 )
183 183
184 184
185 185 def _validatetables(ui, mod):
186 186 """Sanity check for loadable tables provided by extension module"""
187 187 for t in [b'cmdtable', b'colortable', b'configtable']:
188 188 _rejectunicode(t, getattr(mod, t, {}))
189 189 for t in [
190 190 b'filesetpredicate',
191 191 b'internalmerge',
192 192 b'revsetpredicate',
193 193 b'templatefilter',
194 194 b'templatefunc',
195 195 b'templatekeyword',
196 196 ]:
197 197 o = getattr(mod, t, None)
198 198 if o:
199 199 _rejectunicode(t, o._table)
200 200 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
201 201
202 202
203 203 def load(ui, name, path, loadingtime=None):
204 204 if name.startswith(b'hgext.') or name.startswith(b'hgext/'):
205 205 shortname = name[6:]
206 206 else:
207 207 shortname = name
208 208 if shortname in _builtin:
209 209 return None
210 210 if shortname in _extensions:
211 211 return _extensions[shortname]
212 212 ui.log(b'extension', b' - loading extension: %s\n', shortname)
213 213 _extensions[shortname] = None
214 214 with util.timedcm('load extension %s', shortname) as stats:
215 215 mod = _importext(name, path, bind(_reportimporterror, ui))
216 216 ui.log(b'extension', b' > %s extension loaded in %s\n', shortname, stats)
217 217 if loadingtime is not None:
218 218 loadingtime[shortname] += stats.elapsed
219 219
220 220 # Before we do anything with the extension, check against minimum stated
221 221 # compatibility. This gives extension authors a mechanism to have their
222 222 # extensions short circuit when loaded with a known incompatible version
223 223 # of Mercurial.
224 224 minver = getattr(mod, 'minimumhgversion', None)
225 225 if minver:
226 226 curver = util.versiontuple(n=2)
227 extmin = util.versiontuple(minver, 2)
227 228
228 if None in curver or util.versiontuple(minver, 2) > curver:
229 if None in extmin:
230 extmin = (extmin[0] or 0, extmin[1] or 0)
231
232 if None in curver or extmin > curver:
229 233 msg = _(
230 234 b'(third party extension %s requires version %s or newer '
231 235 b'of Mercurial (current: %s); disabling)\n'
232 236 )
233 237 ui.warn(msg % (shortname, minver, util.version()))
234 238 return
235 239 ui.log(b'extension', b' - validating extension tables: %s\n', shortname)
236 240 _validatetables(ui, mod)
237 241
238 242 _extensions[shortname] = mod
239 243 _order.append(shortname)
240 244 ui.log(
241 245 b'extension', b' - invoking registered callbacks: %s\n', shortname
242 246 )
243 247 with util.timedcm('callbacks extension %s', shortname) as stats:
244 248 for fn in _aftercallbacks.get(shortname, []):
245 249 fn(loaded=True)
246 250 ui.log(b'extension', b' > callbacks completed in %s\n', stats)
247 251 return mod
248 252
249 253
250 254 def _runuisetup(name, ui):
251 255 uisetup = getattr(_extensions[name], 'uisetup', None)
252 256 if uisetup:
253 257 try:
254 258 uisetup(ui)
255 259 except Exception as inst:
256 260 ui.traceback(force=True)
257 261 msg = stringutil.forcebytestr(inst)
258 262 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
259 263 return False
260 264 return True
261 265
262 266
263 267 def _runextsetup(name, ui):
264 268 extsetup = getattr(_extensions[name], 'extsetup', None)
265 269 if extsetup:
266 270 try:
267 271 extsetup(ui)
268 272 except Exception as inst:
269 273 ui.traceback(force=True)
270 274 msg = stringutil.forcebytestr(inst)
271 275 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
272 276 return False
273 277 return True
274 278
275 279
276 280 def loadall(ui, whitelist=None):
277 281 loadingtime = collections.defaultdict(int)
278 282 result = ui.configitems(b"extensions")
279 283 if whitelist is not None:
280 284 result = [(k, v) for (k, v) in result if k in whitelist]
281 285 newindex = len(_order)
282 286 ui.log(
283 287 b'extension',
284 288 b'loading %sextensions\n',
285 289 b'additional ' if newindex else b'',
286 290 )
287 291 ui.log(b'extension', b'- processing %d entries\n', len(result))
288 292 with util.timedcm('load all extensions') as stats:
289 293 for (name, path) in result:
290 294 if path:
291 295 if path[0:1] == b'!':
292 296 if name not in _disabledextensions:
293 297 ui.log(
294 298 b'extension',
295 299 b' - skipping disabled extension: %s\n',
296 300 name,
297 301 )
298 302 _disabledextensions[name] = path[1:]
299 303 continue
300 304 try:
301 305 load(ui, name, path, loadingtime)
302 306 except Exception as inst:
303 307 msg = stringutil.forcebytestr(inst)
304 308 if path:
305 309 ui.warn(
306 310 _(b"*** failed to import extension %s from %s: %s\n")
307 311 % (name, path, msg)
308 312 )
309 313 else:
310 314 ui.warn(
311 315 _(b"*** failed to import extension %s: %s\n")
312 316 % (name, msg)
313 317 )
314 318 if isinstance(inst, error.Hint) and inst.hint:
315 319 ui.warn(_(b"*** (%s)\n") % inst.hint)
316 320 ui.traceback()
317 321
318 322 ui.log(
319 323 b'extension',
320 324 b'> loaded %d extensions, total time %s\n',
321 325 len(_order) - newindex,
322 326 stats,
323 327 )
324 328 # list of (objname, loadermod, loadername) tuple:
325 329 # - objname is the name of an object in extension module,
326 330 # from which extra information is loaded
327 331 # - loadermod is the module where loader is placed
328 332 # - loadername is the name of the function,
329 333 # which takes (ui, extensionname, extraobj) arguments
330 334 #
331 335 # This one is for the list of item that must be run before running any setup
332 336 earlyextraloaders = [
333 337 (b'configtable', configitems, b'loadconfigtable'),
334 338 ]
335 339
336 340 ui.log(b'extension', b'- loading configtable attributes\n')
337 341 _loadextra(ui, newindex, earlyextraloaders)
338 342
339 343 broken = set()
340 344 ui.log(b'extension', b'- executing uisetup hooks\n')
341 345 with util.timedcm('all uisetup') as alluisetupstats:
342 346 for name in _order[newindex:]:
343 347 ui.log(b'extension', b' - running uisetup for %s\n', name)
344 348 with util.timedcm('uisetup %s', name) as stats:
345 349 if not _runuisetup(name, ui):
346 350 ui.log(
347 351 b'extension',
348 352 b' - the %s extension uisetup failed\n',
349 353 name,
350 354 )
351 355 broken.add(name)
352 356 ui.log(b'extension', b' > uisetup for %s took %s\n', name, stats)
353 357 loadingtime[name] += stats.elapsed
354 358 ui.log(b'extension', b'> all uisetup took %s\n', alluisetupstats)
355 359
356 360 ui.log(b'extension', b'- executing extsetup hooks\n')
357 361 with util.timedcm('all extsetup') as allextetupstats:
358 362 for name in _order[newindex:]:
359 363 if name in broken:
360 364 continue
361 365 ui.log(b'extension', b' - running extsetup for %s\n', name)
362 366 with util.timedcm('extsetup %s', name) as stats:
363 367 if not _runextsetup(name, ui):
364 368 ui.log(
365 369 b'extension',
366 370 b' - the %s extension extsetup failed\n',
367 371 name,
368 372 )
369 373 broken.add(name)
370 374 ui.log(b'extension', b' > extsetup for %s took %s\n', name, stats)
371 375 loadingtime[name] += stats.elapsed
372 376 ui.log(b'extension', b'> all extsetup took %s\n', allextetupstats)
373 377
374 378 for name in broken:
375 379 ui.log(b'extension', b' - disabling broken %s extension\n', name)
376 380 _extensions[name] = None
377 381
378 382 # Call aftercallbacks that were never met.
379 383 ui.log(b'extension', b'- executing remaining aftercallbacks\n')
380 384 with util.timedcm('aftercallbacks') as stats:
381 385 for shortname in _aftercallbacks:
382 386 if shortname in _extensions:
383 387 continue
384 388
385 389 for fn in _aftercallbacks[shortname]:
386 390 ui.log(
387 391 b'extension',
388 392 b' - extension %s not loaded, notify callbacks\n',
389 393 shortname,
390 394 )
391 395 fn(loaded=False)
392 396 ui.log(b'extension', b'> remaining aftercallbacks completed in %s\n', stats)
393 397
394 398 # loadall() is called multiple times and lingering _aftercallbacks
395 399 # entries could result in double execution. See issue4646.
396 400 _aftercallbacks.clear()
397 401
398 402 # delay importing avoids cyclic dependency (especially commands)
399 403 from . import (
400 404 color,
401 405 commands,
402 406 filemerge,
403 407 fileset,
404 408 revset,
405 409 templatefilters,
406 410 templatefuncs,
407 411 templatekw,
408 412 )
409 413
410 414 # list of (objname, loadermod, loadername) tuple:
411 415 # - objname is the name of an object in extension module,
412 416 # from which extra information is loaded
413 417 # - loadermod is the module where loader is placed
414 418 # - loadername is the name of the function,
415 419 # which takes (ui, extensionname, extraobj) arguments
416 420 ui.log(b'extension', b'- loading extension registration objects\n')
417 421 extraloaders = [
418 422 (b'cmdtable', commands, b'loadcmdtable'),
419 423 (b'colortable', color, b'loadcolortable'),
420 424 (b'filesetpredicate', fileset, b'loadpredicate'),
421 425 (b'internalmerge', filemerge, b'loadinternalmerge'),
422 426 (b'revsetpredicate', revset, b'loadpredicate'),
423 427 (b'templatefilter', templatefilters, b'loadfilter'),
424 428 (b'templatefunc', templatefuncs, b'loadfunction'),
425 429 (b'templatekeyword', templatekw, b'loadkeyword'),
426 430 ]
427 431 with util.timedcm('load registration objects') as stats:
428 432 _loadextra(ui, newindex, extraloaders)
429 433 ui.log(
430 434 b'extension',
431 435 b'> extension registration object loading took %s\n',
432 436 stats,
433 437 )
434 438
435 439 # Report per extension loading time (except reposetup)
436 440 for name in sorted(loadingtime):
437 441 ui.log(
438 442 b'extension',
439 443 b'> extension %s take a total of %s to load\n',
440 444 name,
441 445 util.timecount(loadingtime[name]),
442 446 )
443 447
444 448 ui.log(b'extension', b'extension loading complete\n')
445 449
446 450
447 451 def _loadextra(ui, newindex, extraloaders):
448 452 for name in _order[newindex:]:
449 453 module = _extensions[name]
450 454 if not module:
451 455 continue # loading this module failed
452 456
453 457 for objname, loadermod, loadername in extraloaders:
454 458 extraobj = getattr(module, objname, None)
455 459 if extraobj is not None:
456 460 getattr(loadermod, loadername)(ui, name, extraobj)
457 461
458 462
459 463 def afterloaded(extension, callback):
460 464 """Run the specified function after a named extension is loaded.
461 465
462 466 If the named extension is already loaded, the callback will be called
463 467 immediately.
464 468
465 469 If the named extension never loads, the callback will be called after
466 470 all extensions have been loaded.
467 471
468 472 The callback receives the named argument ``loaded``, which is a boolean
469 473 indicating whether the dependent extension actually loaded.
470 474 """
471 475
472 476 if extension in _extensions:
473 477 # Report loaded as False if the extension is disabled
474 478 loaded = _extensions[extension] is not None
475 479 callback(loaded=loaded)
476 480 else:
477 481 _aftercallbacks.setdefault(extension, []).append(callback)
478 482
479 483
480 484 def populateui(ui):
481 485 """Run extension hooks on the given ui to populate additional members,
482 486 extend the class dynamically, etc.
483 487
484 488 This will be called after the configuration is loaded, and/or extensions
485 489 are loaded. In general, it's once per ui instance, but in command-server
486 490 and hgweb, this may be called more than once with the same ui.
487 491 """
488 492 for name, mod in extensions(ui):
489 493 hook = getattr(mod, 'uipopulate', None)
490 494 if not hook:
491 495 continue
492 496 try:
493 497 hook(ui)
494 498 except Exception as inst:
495 499 ui.traceback(force=True)
496 500 ui.warn(
497 501 _(b'*** failed to populate ui by extension %s: %s\n')
498 502 % (name, stringutil.forcebytestr(inst))
499 503 )
500 504
501 505
502 506 def bind(func, *args):
503 507 """Partial function application
504 508
505 509 Returns a new function that is the partial application of args and kwargs
506 510 to func. For example,
507 511
508 512 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)"""
509 513 assert callable(func)
510 514
511 515 def closure(*a, **kw):
512 516 return func(*(args + a), **kw)
513 517
514 518 return closure
515 519
516 520
517 521 def _updatewrapper(wrap, origfn, unboundwrapper):
518 522 '''Copy and add some useful attributes to wrapper'''
519 523 try:
520 524 wrap.__name__ = origfn.__name__
521 525 except AttributeError:
522 526 pass
523 527 wrap.__module__ = getattr(origfn, '__module__')
524 528 wrap.__doc__ = getattr(origfn, '__doc__')
525 529 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
526 530 wrap._origfunc = origfn
527 531 wrap._unboundwrapper = unboundwrapper
528 532
529 533
530 534 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
531 535 '''Wrap the command named `command' in table
532 536
533 537 Replace command in the command table with wrapper. The wrapped command will
534 538 be inserted into the command table specified by the table argument.
535 539
536 540 The wrapper will be called like
537 541
538 542 wrapper(orig, *args, **kwargs)
539 543
540 544 where orig is the original (wrapped) function, and *args, **kwargs
541 545 are the arguments passed to it.
542 546
543 547 Optionally append to the command synopsis and docstring, used for help.
544 548 For example, if your extension wraps the ``bookmarks`` command to add the
545 549 flags ``--remote`` and ``--all`` you might call this function like so:
546 550
547 551 synopsis = ' [-a] [--remote]'
548 552 docstring = """
549 553
550 554 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
551 555 flags to the bookmarks command. Either flag will show the remote bookmarks
552 556 known to the repository; ``--remote`` will also suppress the output of the
553 557 local bookmarks.
554 558 """
555 559
556 560 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
557 561 synopsis, docstring)
558 562 '''
559 563 assert callable(wrapper)
560 564 aliases, entry = cmdutil.findcmd(command, table)
561 565 for alias, e in pycompat.iteritems(table):
562 566 if e is entry:
563 567 key = alias
564 568 break
565 569
566 570 origfn = entry[0]
567 571 wrap = functools.partial(
568 572 util.checksignature(wrapper), util.checksignature(origfn)
569 573 )
570 574 _updatewrapper(wrap, origfn, wrapper)
571 575 if docstring is not None:
572 576 wrap.__doc__ += docstring
573 577
574 578 newentry = list(entry)
575 579 newentry[0] = wrap
576 580 if synopsis is not None:
577 581 newentry[2] += synopsis
578 582 table[key] = tuple(newentry)
579 583 return entry
580 584
581 585
582 586 def wrapfilecache(cls, propname, wrapper):
583 587 """Wraps a filecache property.
584 588
585 589 These can't be wrapped using the normal wrapfunction.
586 590 """
587 591 propname = pycompat.sysstr(propname)
588 592 assert callable(wrapper)
589 593 for currcls in cls.__mro__:
590 594 if propname in currcls.__dict__:
591 595 origfn = currcls.__dict__[propname].func
592 596 assert callable(origfn)
593 597
594 598 def wrap(*args, **kwargs):
595 599 return wrapper(origfn, *args, **kwargs)
596 600
597 601 currcls.__dict__[propname].func = wrap
598 602 break
599 603
600 604 if currcls is object:
601 605 raise AttributeError("type '%s' has no property '%s'" % (cls, propname))
602 606
603 607
604 608 class wrappedfunction(object):
605 609 '''context manager for temporarily wrapping a function'''
606 610
607 611 def __init__(self, container, funcname, wrapper):
608 612 assert callable(wrapper)
609 613 self._container = container
610 614 self._funcname = funcname
611 615 self._wrapper = wrapper
612 616
613 617 def __enter__(self):
614 618 wrapfunction(self._container, self._funcname, self._wrapper)
615 619
616 620 def __exit__(self, exctype, excvalue, traceback):
617 621 unwrapfunction(self._container, self._funcname, self._wrapper)
618 622
619 623
620 624 def wrapfunction(container, funcname, wrapper):
621 625 """Wrap the function named funcname in container
622 626
623 627 Replace the funcname member in the given container with the specified
624 628 wrapper. The container is typically a module, class, or instance.
625 629
626 630 The wrapper will be called like
627 631
628 632 wrapper(orig, *args, **kwargs)
629 633
630 634 where orig is the original (wrapped) function, and *args, **kwargs
631 635 are the arguments passed to it.
632 636
633 637 Wrapping methods of the repository object is not recommended since
634 638 it conflicts with extensions that extend the repository by
635 639 subclassing. All extensions that need to extend methods of
636 640 localrepository should use this subclassing trick: namely,
637 641 reposetup() should look like
638 642
639 643 def reposetup(ui, repo):
640 644 class myrepo(repo.__class__):
641 645 def whatever(self, *args, **kwargs):
642 646 [...extension stuff...]
643 647 super(myrepo, self).whatever(*args, **kwargs)
644 648 [...extension stuff...]
645 649
646 650 repo.__class__ = myrepo
647 651
648 652 In general, combining wrapfunction() with subclassing does not
649 653 work. Since you cannot control what other extensions are loaded by
650 654 your end users, you should play nicely with others by using the
651 655 subclass trick.
652 656 """
653 657 assert callable(wrapper)
654 658
655 659 origfn = getattr(container, funcname)
656 660 assert callable(origfn)
657 661 if inspect.ismodule(container):
658 662 # origfn is not an instance or class method. "partial" can be used.
659 663 # "partial" won't insert a frame in traceback.
660 664 wrap = functools.partial(wrapper, origfn)
661 665 else:
662 666 # "partial" cannot be safely used. Emulate its effect by using "bind".
663 667 # The downside is one more frame in traceback.
664 668 wrap = bind(wrapper, origfn)
665 669 _updatewrapper(wrap, origfn, wrapper)
666 670 setattr(container, funcname, wrap)
667 671 return origfn
668 672
669 673
670 674 def unwrapfunction(container, funcname, wrapper=None):
671 675 """undo wrapfunction
672 676
673 677 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
674 678 from the chain of wrappers.
675 679
676 680 Return the removed wrapper.
677 681 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
678 682 wrapper is not None but is not found in the wrapper chain.
679 683 """
680 684 chain = getwrapperchain(container, funcname)
681 685 origfn = chain.pop()
682 686 if wrapper is None:
683 687 wrapper = chain[0]
684 688 chain.remove(wrapper)
685 689 setattr(container, funcname, origfn)
686 690 for w in reversed(chain):
687 691 wrapfunction(container, funcname, w)
688 692 return wrapper
689 693
690 694
691 695 def getwrapperchain(container, funcname):
692 696 """get a chain of wrappers of a function
693 697
694 698 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
695 699
696 700 The wrapper functions are the ones passed to wrapfunction, whose first
697 701 argument is origfunc.
698 702 """
699 703 result = []
700 704 fn = getattr(container, funcname)
701 705 while fn:
702 706 assert callable(fn)
703 707 result.append(getattr(fn, '_unboundwrapper', fn))
704 708 fn = getattr(fn, '_origfunc', None)
705 709 return result
706 710
707 711
708 712 def _disabledpaths():
709 713 '''find paths of disabled extensions. returns a dict of {name: path}'''
710 714 import hgext
711 715
712 716 # The hgext might not have a __file__ attribute (e.g. in PyOxidizer) and
713 717 # it might not be on a filesystem even if it does.
714 718 if util.safehasattr(hgext, '__file__'):
715 719 extpath = os.path.dirname(
716 720 util.abspath(pycompat.fsencode(hgext.__file__))
717 721 )
718 722 try:
719 723 files = os.listdir(extpath)
720 724 except OSError:
721 725 return {}
722 726 else:
723 727 return {}
724 728
725 729 exts = {}
726 730 for e in files:
727 731 if e.endswith(b'.py'):
728 732 name = e.rsplit(b'.', 1)[0]
729 733 path = os.path.join(extpath, e)
730 734 else:
731 735 name = e
732 736 path = os.path.join(extpath, e, b'__init__.py')
733 737 if not os.path.exists(path):
734 738 continue
735 739 if name in exts or name in _order or name == b'__init__':
736 740 continue
737 741 exts[name] = path
738 742 for name, path in pycompat.iteritems(_disabledextensions):
739 743 # If no path was provided for a disabled extension (e.g. "color=!"),
740 744 # don't replace the path we already found by the scan above.
741 745 if path:
742 746 exts[name] = path
743 747 return exts
744 748
745 749
746 750 def _moduledoc(file):
747 751 """return the top-level python documentation for the given file
748 752
749 753 Loosely inspired by pydoc.source_synopsis(), but rewritten to
750 754 handle triple quotes and to return the whole text instead of just
751 755 the synopsis"""
752 756 result = []
753 757
754 758 line = file.readline()
755 759 while line[:1] == b'#' or not line.strip():
756 760 line = file.readline()
757 761 if not line:
758 762 break
759 763
760 764 start = line[:3]
761 765 if start == b'"""' or start == b"'''":
762 766 line = line[3:]
763 767 while line:
764 768 if line.rstrip().endswith(start):
765 769 line = line.split(start)[0]
766 770 if line:
767 771 result.append(line)
768 772 break
769 773 elif not line:
770 774 return None # unmatched delimiter
771 775 result.append(line)
772 776 line = file.readline()
773 777 else:
774 778 return None
775 779
776 780 return b''.join(result)
777 781
778 782
779 783 def _disabledhelp(path):
780 784 '''retrieve help synopsis of a disabled extension (without importing)'''
781 785 try:
782 786 with open(path, b'rb') as src:
783 787 doc = _moduledoc(src)
784 788 except IOError:
785 789 return
786 790
787 791 if doc: # extracting localized synopsis
788 792 return gettext(doc)
789 793 else:
790 794 return _(b'(no help text available)')
791 795
792 796
793 797 def disabled():
794 798 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
795 799 try:
796 800 from hgext import __index__ # pytype: disable=import-error
797 801
798 802 return {
799 803 name: gettext(desc)
800 804 for name, desc in pycompat.iteritems(__index__.docs)
801 805 if name not in _order
802 806 }
803 807 except (ImportError, AttributeError):
804 808 pass
805 809
806 810 paths = _disabledpaths()
807 811 if not paths:
808 812 return {}
809 813
810 814 exts = {}
811 815 for name, path in pycompat.iteritems(paths):
812 816 doc = _disabledhelp(path)
813 817 if doc and name != b'__index__':
814 818 exts[name] = doc.splitlines()[0]
815 819
816 820 return exts
817 821
818 822
819 823 def disabled_help(name):
820 824 """Obtain the full help text for a disabled extension, or None."""
821 825 paths = _disabledpaths()
822 826 if name in paths:
823 827 return _disabledhelp(paths[name])
824 828
825 829
826 830 def _walkcommand(node):
827 831 """Scan @command() decorators in the tree starting at node"""
828 832 todo = collections.deque([node])
829 833 while todo:
830 834 node = todo.popleft()
831 835 if not isinstance(node, ast.FunctionDef):
832 836 todo.extend(ast.iter_child_nodes(node))
833 837 continue
834 838 for d in node.decorator_list:
835 839 if not isinstance(d, ast.Call):
836 840 continue
837 841 if not isinstance(d.func, ast.Name):
838 842 continue
839 843 if d.func.id != 'command':
840 844 continue
841 845 yield d
842 846
843 847
844 848 def _disabledcmdtable(path):
845 849 """Construct a dummy command table without loading the extension module
846 850
847 851 This may raise IOError or SyntaxError.
848 852 """
849 853 with open(path, b'rb') as src:
850 854 root = ast.parse(src.read(), path)
851 855 cmdtable = {}
852 856 for node in _walkcommand(root):
853 857 if not node.args:
854 858 continue
855 859 a = node.args[0]
856 860 if isinstance(a, ast.Str):
857 861 name = pycompat.sysbytes(a.s)
858 862 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
859 863 name = a.s
860 864 else:
861 865 continue
862 866 cmdtable[name] = (None, [], b'')
863 867 return cmdtable
864 868
865 869
866 870 def _finddisabledcmd(ui, cmd, name, path, strict):
867 871 try:
868 872 cmdtable = _disabledcmdtable(path)
869 873 except (IOError, SyntaxError):
870 874 return
871 875 try:
872 876 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
873 877 except (error.AmbiguousCommand, error.UnknownCommand):
874 878 return
875 879 for c in aliases:
876 880 if c.startswith(cmd):
877 881 cmd = c
878 882 break
879 883 else:
880 884 cmd = aliases[0]
881 885 doc = _disabledhelp(path)
882 886 return (cmd, name, doc)
883 887
884 888
885 889 def disabledcmd(ui, cmd, strict=False):
886 890 """find cmd from disabled extensions without importing.
887 891 returns (cmdname, extname, doc)"""
888 892
889 893 paths = _disabledpaths()
890 894 if not paths:
891 895 raise error.UnknownCommand(cmd)
892 896
893 897 ext = None
894 898 # first, search for an extension with the same name as the command
895 899 path = paths.pop(cmd, None)
896 900 if path:
897 901 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
898 902 if not ext:
899 903 # otherwise, interrogate each extension until there's a match
900 904 for name, path in pycompat.iteritems(paths):
901 905 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
902 906 if ext:
903 907 break
904 908 if ext:
905 909 return ext
906 910
907 911 raise error.UnknownCommand(cmd)
908 912
909 913
910 914 def enabled(shortname=True):
911 915 '''return a dict of {name: desc} of extensions'''
912 916 exts = {}
913 917 for ename, ext in extensions():
914 918 doc = gettext(ext.__doc__) or _(b'(no help text available)')
915 919 assert doc is not None # help pytype
916 920 if shortname:
917 921 ename = ename.split(b'.')[-1]
918 922 exts[ename] = doc.splitlines()[0].strip()
919 923
920 924 return exts
921 925
922 926
923 927 def notloaded():
924 928 '''return short names of extensions that failed to load'''
925 929 return [
926 930 name for name, mod in pycompat.iteritems(_extensions) if mod is None
927 931 ]
928 932
929 933
930 934 def moduleversion(module):
931 935 '''return version information from given module as a string'''
932 936 if util.safehasattr(module, b'getversion') and callable(module.getversion):
933 937 try:
934 938 version = module.getversion()
935 939 except Exception:
936 940 version = b'unknown'
937 941
938 942 elif util.safehasattr(module, b'__version__'):
939 943 version = module.__version__
940 944 else:
941 945 version = b''
942 946 if isinstance(version, (list, tuple)):
943 947 version = b'.'.join(pycompat.bytestr(o) for o in version)
944 948 else:
945 949 # version data should be bytes, but not all extensions are ported
946 950 # to py3.
947 951 version = stringutil.forcebytestr(version)
948 952 return version
949 953
950 954
951 955 def ismoduleinternal(module):
952 956 exttestedwith = getattr(module, 'testedwith', None)
953 957 return exttestedwith == b"ships-with-hg-core"
@@ -1,1926 +1,1945 b''
1 1 Test basic extension support
2 2 $ cat > unflush.py <<EOF
3 3 > import sys
4 4 > from mercurial import pycompat
5 5 > if pycompat.ispy3:
6 6 > # no changes required
7 7 > sys.exit(0)
8 8 > with open(sys.argv[1], 'rb') as f:
9 9 > data = f.read()
10 10 > with open(sys.argv[1], 'wb') as f:
11 11 > f.write(data.replace(b', flush=True', b''))
12 12 > EOF
13 13
14 14 $ cat > foobar.py <<EOF
15 15 > import os
16 16 > from mercurial import commands, exthelper, registrar
17 17 >
18 18 > eh = exthelper.exthelper()
19 19 > eh.configitem(b'tests', b'foo', default=b"Foo")
20 20 >
21 21 > uisetup = eh.finaluisetup
22 22 > uipopulate = eh.finaluipopulate
23 23 > reposetup = eh.finalreposetup
24 24 > cmdtable = eh.cmdtable
25 25 > configtable = eh.configtable
26 26 >
27 27 > @eh.uisetup
28 28 > def _uisetup(ui):
29 29 > ui.debug(b"uisetup called [debug]\\n")
30 30 > ui.write(b"uisetup called\\n")
31 31 > ui.status(b"uisetup called [status]\\n")
32 32 > ui.flush()
33 33 > @eh.uipopulate
34 34 > def _uipopulate(ui):
35 35 > ui._populatecnt = getattr(ui, "_populatecnt", 0) + 1
36 36 > ui.write(b"uipopulate called (%d times)\n" % ui._populatecnt)
37 37 > @eh.reposetup
38 38 > def _reposetup(ui, repo):
39 39 > ui.write(b"reposetup called for %s\\n" % os.path.basename(repo.root))
40 40 > ui.write(b"ui %s= repo.ui\\n" % (ui == repo.ui and b"=" or b"!"))
41 41 > ui.flush()
42 42 > @eh.command(b'foo', [], b'hg foo')
43 43 > def foo(ui, *args, **kwargs):
44 44 > foo = ui.config(b'tests', b'foo')
45 45 > ui.write(foo)
46 46 > ui.write(b"\\n")
47 47 > @eh.command(b'bar', [], b'hg bar', norepo=True)
48 48 > def bar(ui, *args, **kwargs):
49 49 > ui.write(b"Bar\\n")
50 50 > EOF
51 51 $ abspath=`pwd`/foobar.py
52 52
53 53 $ mkdir barfoo
54 54 $ cp foobar.py barfoo/__init__.py
55 55 $ barfoopath=`pwd`/barfoo
56 56
57 57 $ hg init a
58 58 $ cd a
59 59 $ echo foo > file
60 60 $ hg add file
61 61 $ hg commit -m 'add file'
62 62
63 63 $ echo '[extensions]' >> $HGRCPATH
64 64 $ echo "foobar = $abspath" >> $HGRCPATH
65 65 $ hg foo
66 66 uisetup called
67 67 uisetup called [status]
68 68 uipopulate called (1 times)
69 69 uipopulate called (1 times)
70 70 uipopulate called (1 times)
71 71 reposetup called for a
72 72 ui == repo.ui
73 73 uipopulate called (1 times) (chg !)
74 74 uipopulate called (1 times) (chg !)
75 75 uipopulate called (1 times) (chg !)
76 76 uipopulate called (1 times) (chg !)
77 77 uipopulate called (1 times) (chg !)
78 78 reposetup called for a (chg !)
79 79 ui == repo.ui (chg !)
80 80 Foo
81 81 $ hg foo --quiet
82 82 uisetup called (no-chg !)
83 83 uipopulate called (1 times)
84 84 uipopulate called (1 times)
85 85 uipopulate called (1 times) (chg !)
86 86 uipopulate called (1 times) (chg !)
87 87 uipopulate called (1 times)
88 88 reposetup called for a
89 89 ui == repo.ui
90 90 Foo
91 91 $ hg foo --debug
92 92 uisetup called [debug] (no-chg !)
93 93 uisetup called (no-chg !)
94 94 uisetup called [status] (no-chg !)
95 95 uipopulate called (1 times)
96 96 uipopulate called (1 times)
97 97 uipopulate called (1 times) (chg !)
98 98 uipopulate called (1 times) (chg !)
99 99 uipopulate called (1 times)
100 100 reposetup called for a
101 101 ui == repo.ui
102 102 Foo
103 103
104 104 $ cd ..
105 105 $ hg clone a b
106 106 uisetup called (no-chg !)
107 107 uisetup called [status] (no-chg !)
108 108 uipopulate called (1 times)
109 109 uipopulate called (1 times) (chg !)
110 110 uipopulate called (1 times)
111 111 reposetup called for a
112 112 ui == repo.ui
113 113 uipopulate called (1 times)
114 114 uipopulate called (1 times)
115 115 reposetup called for b
116 116 ui == repo.ui
117 117 updating to branch default
118 118 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
119 119
120 120 $ hg bar
121 121 uisetup called (no-chg !)
122 122 uisetup called [status] (no-chg !)
123 123 uipopulate called (1 times)
124 124 uipopulate called (1 times) (chg !)
125 125 Bar
126 126 $ echo 'foobar = !' >> $HGRCPATH
127 127
128 128 module/__init__.py-style
129 129
130 130 $ echo "barfoo = $barfoopath" >> $HGRCPATH
131 131 $ cd a
132 132 $ hg foo
133 133 uisetup called
134 134 uisetup called [status]
135 135 uipopulate called (1 times)
136 136 uipopulate called (1 times)
137 137 uipopulate called (1 times)
138 138 reposetup called for a
139 139 ui == repo.ui
140 140 uipopulate called (1 times) (chg !)
141 141 uipopulate called (1 times) (chg !)
142 142 uipopulate called (1 times) (chg !)
143 143 uipopulate called (1 times) (chg !)
144 144 uipopulate called (1 times) (chg !)
145 145 reposetup called for a (chg !)
146 146 ui == repo.ui (chg !)
147 147 Foo
148 148 $ echo 'barfoo = !' >> $HGRCPATH
149 149
150 150 Check that extensions are loaded in phases:
151 151
152 152 $ cat > foo.py <<EOF
153 153 > from __future__ import print_function
154 154 > import os
155 155 > from mercurial import exthelper
156 156 > from mercurial.utils import procutil
157 157 >
158 158 > def write(msg):
159 159 > procutil.stdout.write(msg)
160 160 > procutil.stdout.flush()
161 161 >
162 162 > name = os.path.basename(__file__).rsplit('.', 1)[0]
163 163 > bytesname = name.encode('utf-8')
164 164 > write(b"1) %s imported\n" % bytesname)
165 165 > eh = exthelper.exthelper()
166 166 > @eh.uisetup
167 167 > def _uisetup(ui):
168 168 > write(b"2) %s uisetup\n" % bytesname)
169 169 > @eh.extsetup
170 170 > def _extsetup(ui):
171 171 > write(b"3) %s extsetup\n" % bytesname)
172 172 > @eh.uipopulate
173 173 > def _uipopulate(ui):
174 174 > write(b"4) %s uipopulate\n" % bytesname)
175 175 > @eh.reposetup
176 176 > def _reposetup(ui, repo):
177 177 > write(b"5) %s reposetup\n" % bytesname)
178 178 >
179 179 > extsetup = eh.finalextsetup
180 180 > reposetup = eh.finalreposetup
181 181 > uipopulate = eh.finaluipopulate
182 182 > uisetup = eh.finaluisetup
183 183 > revsetpredicate = eh.revsetpredicate
184 184 >
185 185 > # custom predicate to check registration of functions at loading
186 186 > from mercurial import (
187 187 > smartset,
188 188 > )
189 189 > @eh.revsetpredicate(bytesname, safe=True) # safe=True for query via hgweb
190 190 > def custompredicate(repo, subset, x):
191 191 > return smartset.baseset([r for r in subset if r in {0}])
192 192 > EOF
193 193 $ "$PYTHON" $TESTTMP/unflush.py foo.py
194 194
195 195 $ cp foo.py bar.py
196 196 $ echo 'foo = foo.py' >> $HGRCPATH
197 197 $ echo 'bar = bar.py' >> $HGRCPATH
198 198
199 199 Check normal command's load order of extensions and registration of functions
200 200
201 201 On chg server, extension should be first set up by the server. Then
202 202 object-level setup should follow in the worker process.
203 203
204 204 $ hg log -r "foo() and bar()" -q
205 205 1) foo imported
206 206 1) bar imported
207 207 2) foo uisetup
208 208 2) bar uisetup
209 209 3) foo extsetup
210 210 3) bar extsetup
211 211 4) foo uipopulate
212 212 4) bar uipopulate
213 213 4) foo uipopulate
214 214 4) bar uipopulate
215 215 4) foo uipopulate
216 216 4) bar uipopulate
217 217 5) foo reposetup
218 218 5) bar reposetup
219 219 4) foo uipopulate (chg !)
220 220 4) bar uipopulate (chg !)
221 221 4) foo uipopulate (chg !)
222 222 4) bar uipopulate (chg !)
223 223 4) foo uipopulate (chg !)
224 224 4) bar uipopulate (chg !)
225 225 4) foo uipopulate (chg !)
226 226 4) bar uipopulate (chg !)
227 227 4) foo uipopulate (chg !)
228 228 4) bar uipopulate (chg !)
229 229 5) foo reposetup (chg !)
230 230 5) bar reposetup (chg !)
231 231 0:c24b9ac61126
232 232
233 233 Check hgweb's load order of extensions and registration of functions
234 234
235 235 $ cat > hgweb.cgi <<EOF
236 236 > #!$PYTHON
237 237 > from mercurial import demandimport; demandimport.enable()
238 238 > from mercurial.hgweb import hgweb
239 239 > from mercurial.hgweb import wsgicgi
240 240 > application = hgweb(b'.', b'test repo')
241 241 > wsgicgi.launch(application)
242 242 > EOF
243 243 $ . "$TESTDIR/cgienv"
244 244
245 245 $ PATH_INFO='/' SCRIPT_NAME='' "$PYTHON" hgweb.cgi \
246 246 > | grep '^[0-9]) ' # ignores HTML output
247 247 1) foo imported
248 248 1) bar imported
249 249 2) foo uisetup
250 250 2) bar uisetup
251 251 3) foo extsetup
252 252 3) bar extsetup
253 253 4) foo uipopulate
254 254 4) bar uipopulate
255 255 4) foo uipopulate
256 256 4) bar uipopulate
257 257 5) foo reposetup
258 258 5) bar reposetup
259 259
260 260 (check that revset predicate foo() and bar() are available)
261 261
262 262 #if msys
263 263 $ PATH_INFO='//shortlog'
264 264 #else
265 265 $ PATH_INFO='/shortlog'
266 266 #endif
267 267 $ export PATH_INFO
268 268 $ SCRIPT_NAME='' QUERY_STRING='rev=foo() and bar()' "$PYTHON" hgweb.cgi \
269 269 > | grep '<a href="/rev/[0-9a-z]*">'
270 270 <a href="/rev/c24b9ac61126">add file</a>
271 271
272 272 $ echo 'foo = !' >> $HGRCPATH
273 273 $ echo 'bar = !' >> $HGRCPATH
274 274
275 275 Check "from __future__ import absolute_import" support for external libraries
276 276
277 277 (import-checker.py reports issues for some of heredoc python code
278 278 fragments below, because import-checker.py does not know test specific
279 279 package hierarchy. NO_CHECK_* should be used as a limit mark of
280 280 heredoc, in order to make import-checker.py ignore them. For
281 281 simplicity, all python code fragments below are generated with such
282 282 limit mark, regardless of importing module or not.)
283 283
284 284 #if windows
285 285 $ PATHSEP=";"
286 286 #else
287 287 $ PATHSEP=":"
288 288 #endif
289 289 $ export PATHSEP
290 290
291 291 $ mkdir $TESTTMP/libroot
292 292 $ echo "s = 'libroot/ambig.py'" > $TESTTMP/libroot/ambig.py
293 293 $ mkdir $TESTTMP/libroot/mod
294 294 $ touch $TESTTMP/libroot/mod/__init__.py
295 295 $ echo "s = 'libroot/mod/ambig.py'" > $TESTTMP/libroot/mod/ambig.py
296 296
297 297 $ cat > $TESTTMP/libroot/mod/ambigabs.py <<NO_CHECK_EOF
298 298 > from __future__ import absolute_import, print_function
299 299 > import ambig # should load "libroot/ambig.py"
300 300 > s = ambig.s
301 301 > NO_CHECK_EOF
302 302 $ cat > loadabs.py <<NO_CHECK_EOF
303 303 > import mod.ambigabs as ambigabs
304 304 > def extsetup(ui):
305 305 > print('ambigabs.s=%s' % ambigabs.s, flush=True)
306 306 > NO_CHECK_EOF
307 307 $ "$PYTHON" $TESTTMP/unflush.py loadabs.py
308 308 $ (PYTHONPATH=${PYTHONPATH}${PATHSEP}${TESTTMP}/libroot; hg --config extensions.loadabs=loadabs.py root)
309 309 ambigabs.s=libroot/ambig.py
310 310 $TESTTMP/a
311 311
312 312 #if no-py3
313 313 $ cat > $TESTTMP/libroot/mod/ambigrel.py <<NO_CHECK_EOF
314 314 > from __future__ import print_function
315 315 > import ambig # should load "libroot/mod/ambig.py"
316 316 > s = ambig.s
317 317 > NO_CHECK_EOF
318 318 $ cat > loadrel.py <<NO_CHECK_EOF
319 319 > import mod.ambigrel as ambigrel
320 320 > def extsetup(ui):
321 321 > print('ambigrel.s=%s' % ambigrel.s, flush=True)
322 322 > NO_CHECK_EOF
323 323 $ "$PYTHON" $TESTTMP/unflush.py loadrel.py
324 324 $ (PYTHONPATH=${PYTHONPATH}${PATHSEP}${TESTTMP}/libroot; hg --config extensions.loadrel=loadrel.py root)
325 325 ambigrel.s=libroot/mod/ambig.py
326 326 $TESTTMP/a
327 327 #endif
328 328
329 329 Check absolute/relative import of extension specific modules
330 330
331 331 $ mkdir $TESTTMP/extroot
332 332 $ cat > $TESTTMP/extroot/bar.py <<NO_CHECK_EOF
333 333 > s = b'this is extroot.bar'
334 334 > NO_CHECK_EOF
335 335 $ mkdir $TESTTMP/extroot/sub1
336 336 $ cat > $TESTTMP/extroot/sub1/__init__.py <<NO_CHECK_EOF
337 337 > s = b'this is extroot.sub1.__init__'
338 338 > NO_CHECK_EOF
339 339 $ cat > $TESTTMP/extroot/sub1/baz.py <<NO_CHECK_EOF
340 340 > s = b'this is extroot.sub1.baz'
341 341 > NO_CHECK_EOF
342 342 $ cat > $TESTTMP/extroot/__init__.py <<NO_CHECK_EOF
343 343 > from __future__ import absolute_import
344 344 > s = b'this is extroot.__init__'
345 345 > from . import foo
346 346 > def extsetup(ui):
347 347 > ui.write(b'(extroot) ', foo.func(), b'\n')
348 348 > ui.flush()
349 349 > NO_CHECK_EOF
350 350
351 351 $ cat > $TESTTMP/extroot/foo.py <<NO_CHECK_EOF
352 352 > # test absolute import
353 353 > buf = []
354 354 > def func():
355 355 > # "not locals" case
356 356 > import extroot.bar
357 357 > buf.append(b'import extroot.bar in func(): %s' % extroot.bar.s)
358 358 > return b'\n(extroot) '.join(buf)
359 359 > # b"fromlist == ('*',)" case
360 360 > from extroot.bar import *
361 361 > buf.append(b'from extroot.bar import *: %s' % s)
362 362 > # "not fromlist" and "if '.' in name" case
363 363 > import extroot.sub1.baz
364 364 > buf.append(b'import extroot.sub1.baz: %s' % extroot.sub1.baz.s)
365 365 > # "not fromlist" and NOT "if '.' in name" case
366 366 > import extroot
367 367 > buf.append(b'import extroot: %s' % extroot.s)
368 368 > # NOT "not fromlist" and NOT "level != -1" case
369 369 > from extroot.bar import s
370 370 > buf.append(b'from extroot.bar import s: %s' % s)
371 371 > NO_CHECK_EOF
372 372 $ (PYTHONPATH=${PYTHONPATH}${PATHSEP}${TESTTMP}; hg --config extensions.extroot=$TESTTMP/extroot root)
373 373 (extroot) from extroot.bar import *: this is extroot.bar
374 374 (extroot) import extroot.sub1.baz: this is extroot.sub1.baz
375 375 (extroot) import extroot: this is extroot.__init__
376 376 (extroot) from extroot.bar import s: this is extroot.bar
377 377 (extroot) import extroot.bar in func(): this is extroot.bar
378 378 $TESTTMP/a
379 379
380 380 #if no-py3
381 381 $ rm "$TESTTMP"/extroot/foo.*
382 382 $ rm -Rf "$TESTTMP/extroot/__pycache__"
383 383 $ cat > $TESTTMP/extroot/foo.py <<NO_CHECK_EOF
384 384 > # test relative import
385 385 > buf = []
386 386 > def func():
387 387 > # "not locals" case
388 388 > import bar
389 389 > buf.append('import bar in func(): %s' % bar.s)
390 390 > return '\n(extroot) '.join(buf)
391 391 > # "fromlist == ('*',)" case
392 392 > from bar import *
393 393 > buf.append('from bar import *: %s' % s)
394 394 > # "not fromlist" and "if '.' in name" case
395 395 > import sub1.baz
396 396 > buf.append('import sub1.baz: %s' % sub1.baz.s)
397 397 > # "not fromlist" and NOT "if '.' in name" case
398 398 > import sub1
399 399 > buf.append('import sub1: %s' % sub1.s)
400 400 > # NOT "not fromlist" and NOT "level != -1" case
401 401 > from bar import s
402 402 > buf.append('from bar import s: %s' % s)
403 403 > NO_CHECK_EOF
404 404 $ hg --config extensions.extroot=$TESTTMP/extroot root
405 405 (extroot) from bar import *: this is extroot.bar
406 406 (extroot) import sub1.baz: this is extroot.sub1.baz
407 407 (extroot) import sub1: this is extroot.sub1.__init__
408 408 (extroot) from bar import s: this is extroot.bar
409 409 (extroot) import bar in func(): this is extroot.bar
410 410 $TESTTMP/a
411 411 #endif
412 412
413 413 #if demandimport
414 414
415 415 Examine whether module loading is delayed until actual referring, even
416 416 though module is imported with "absolute_import" feature.
417 417
418 418 Files below in each packages are used for described purpose:
419 419
420 420 - "called": examine whether "from MODULE import ATTR" works correctly
421 421 - "unused": examine whether loading is delayed correctly
422 422 - "used": examine whether "from PACKAGE import MODULE" works correctly
423 423
424 424 Package hierarchy is needed to examine whether demand importing works
425 425 as expected for "from SUB.PACK.AGE import MODULE".
426 426
427 427 Setup "external library" to be imported with "absolute_import"
428 428 feature.
429 429
430 430 $ mkdir -p $TESTTMP/extlibroot/lsub1/lsub2
431 431 $ touch $TESTTMP/extlibroot/__init__.py
432 432 $ touch $TESTTMP/extlibroot/lsub1/__init__.py
433 433 $ touch $TESTTMP/extlibroot/lsub1/lsub2/__init__.py
434 434
435 435 $ cat > $TESTTMP/extlibroot/lsub1/lsub2/called.py <<NO_CHECK_EOF
436 436 > def func():
437 437 > return b"this is extlibroot.lsub1.lsub2.called.func()"
438 438 > NO_CHECK_EOF
439 439 $ cat > $TESTTMP/extlibroot/lsub1/lsub2/unused.py <<NO_CHECK_EOF
440 440 > raise Exception("extlibroot.lsub1.lsub2.unused is loaded unintentionally")
441 441 > NO_CHECK_EOF
442 442 $ cat > $TESTTMP/extlibroot/lsub1/lsub2/used.py <<NO_CHECK_EOF
443 443 > detail = b"this is extlibroot.lsub1.lsub2.used"
444 444 > NO_CHECK_EOF
445 445
446 446 Setup sub-package of "external library", which causes instantiation of
447 447 demandmod in "recurse down the module chain" code path. Relative
448 448 importing with "absolute_import" feature isn't tested, because "level
449 449 >=1 " doesn't cause instantiation of demandmod.
450 450
451 451 $ mkdir -p $TESTTMP/extlibroot/recursedown/abs
452 452 $ cat > $TESTTMP/extlibroot/recursedown/abs/used.py <<NO_CHECK_EOF
453 453 > detail = b"this is extlibroot.recursedown.abs.used"
454 454 > NO_CHECK_EOF
455 455 $ cat > $TESTTMP/extlibroot/recursedown/abs/__init__.py <<NO_CHECK_EOF
456 456 > from __future__ import absolute_import
457 457 > from extlibroot.recursedown.abs.used import detail
458 458 > NO_CHECK_EOF
459 459
460 460 $ mkdir -p $TESTTMP/extlibroot/recursedown/legacy
461 461 $ cat > $TESTTMP/extlibroot/recursedown/legacy/used.py <<NO_CHECK_EOF
462 462 > detail = b"this is extlibroot.recursedown.legacy.used"
463 463 > NO_CHECK_EOF
464 464 $ cat > $TESTTMP/extlibroot/recursedown/legacy/__init__.py <<NO_CHECK_EOF
465 465 > # legacy style (level == -1) import
466 466 > from extlibroot.recursedown.legacy.used import detail
467 467 > NO_CHECK_EOF
468 468
469 469 $ cat > $TESTTMP/extlibroot/recursedown/__init__.py <<NO_CHECK_EOF
470 470 > from __future__ import absolute_import
471 471 > from extlibroot.recursedown.abs import detail as absdetail
472 472 > from .legacy import detail as legacydetail
473 473 > NO_CHECK_EOF
474 474
475 475 Setup package that re-exports an attribute of its submodule as the same
476 476 name. This leaves 'shadowing.used' pointing to 'used.detail', but still
477 477 the submodule 'used' should be somehow accessible. (issue5617)
478 478
479 479 $ mkdir -p $TESTTMP/extlibroot/shadowing
480 480 $ cat > $TESTTMP/extlibroot/shadowing/used.py <<NO_CHECK_EOF
481 481 > detail = b"this is extlibroot.shadowing.used"
482 482 > NO_CHECK_EOF
483 483 $ cat > $TESTTMP/extlibroot/shadowing/proxied.py <<NO_CHECK_EOF
484 484 > from __future__ import absolute_import
485 485 > from extlibroot.shadowing.used import detail
486 486 > NO_CHECK_EOF
487 487 $ cat > $TESTTMP/extlibroot/shadowing/__init__.py <<NO_CHECK_EOF
488 488 > from __future__ import absolute_import
489 489 > from .used import detail as used
490 490 > NO_CHECK_EOF
491 491
492 492 Setup extension local modules to be imported with "absolute_import"
493 493 feature.
494 494
495 495 $ mkdir -p $TESTTMP/absextroot/xsub1/xsub2
496 496 $ touch $TESTTMP/absextroot/xsub1/__init__.py
497 497 $ touch $TESTTMP/absextroot/xsub1/xsub2/__init__.py
498 498
499 499 $ cat > $TESTTMP/absextroot/xsub1/xsub2/called.py <<NO_CHECK_EOF
500 500 > def func():
501 501 > return b"this is absextroot.xsub1.xsub2.called.func()"
502 502 > NO_CHECK_EOF
503 503 $ cat > $TESTTMP/absextroot/xsub1/xsub2/unused.py <<NO_CHECK_EOF
504 504 > raise Exception("absextroot.xsub1.xsub2.unused is loaded unintentionally")
505 505 > NO_CHECK_EOF
506 506 $ cat > $TESTTMP/absextroot/xsub1/xsub2/used.py <<NO_CHECK_EOF
507 507 > detail = b"this is absextroot.xsub1.xsub2.used"
508 508 > NO_CHECK_EOF
509 509
510 510 Setup extension local modules to examine whether demand importing
511 511 works as expected in "level > 1" case.
512 512
513 513 $ cat > $TESTTMP/absextroot/relimportee.py <<NO_CHECK_EOF
514 514 > detail = b"this is absextroot.relimportee"
515 515 > NO_CHECK_EOF
516 516 $ cat > $TESTTMP/absextroot/xsub1/xsub2/relimporter.py <<NO_CHECK_EOF
517 517 > from __future__ import absolute_import
518 518 > from mercurial import pycompat
519 519 > from ... import relimportee
520 520 > detail = b"this relimporter imports %r" % (
521 521 > pycompat.bytestr(relimportee.detail))
522 522 > NO_CHECK_EOF
523 523
524 524 Setup modules, which actually import extension local modules at
525 525 runtime.
526 526
527 527 $ cat > $TESTTMP/absextroot/absolute.py << NO_CHECK_EOF
528 528 > from __future__ import absolute_import
529 529 >
530 530 > # import extension local modules absolutely (level = 0)
531 531 > from absextroot.xsub1.xsub2 import used, unused
532 532 > from absextroot.xsub1.xsub2.called import func
533 533 >
534 534 > def getresult():
535 535 > result = []
536 536 > result.append(used.detail)
537 537 > result.append(func())
538 538 > return result
539 539 > NO_CHECK_EOF
540 540
541 541 $ cat > $TESTTMP/absextroot/relative.py << NO_CHECK_EOF
542 542 > from __future__ import absolute_import
543 543 >
544 544 > # import extension local modules relatively (level == 1)
545 545 > from .xsub1.xsub2 import used, unused
546 546 > from .xsub1.xsub2.called import func
547 547 >
548 548 > # import a module, which implies "importing with level > 1"
549 549 > from .xsub1.xsub2 import relimporter
550 550 >
551 551 > def getresult():
552 552 > result = []
553 553 > result.append(used.detail)
554 554 > result.append(func())
555 555 > result.append(relimporter.detail)
556 556 > return result
557 557 > NO_CHECK_EOF
558 558
559 559 Setup main procedure of extension.
560 560
561 561 $ cat > $TESTTMP/absextroot/__init__.py <<NO_CHECK_EOF
562 562 > from __future__ import absolute_import
563 563 > from mercurial import registrar
564 564 > cmdtable = {}
565 565 > command = registrar.command(cmdtable)
566 566 >
567 567 > # "absolute" and "relative" shouldn't be imported before actual
568 568 > # command execution, because (1) they import same modules, and (2)
569 569 > # preceding import (= instantiate "demandmod" object instead of
570 570 > # real "module" object) might hide problem of succeeding import.
571 571 >
572 572 > @command(b'showabsolute', [], norepo=True)
573 573 > def showabsolute(ui, *args, **opts):
574 574 > from absextroot import absolute
575 575 > ui.write(b'ABS: %s\n' % b'\nABS: '.join(absolute.getresult()))
576 576 >
577 577 > @command(b'showrelative', [], norepo=True)
578 578 > def showrelative(ui, *args, **opts):
579 579 > from . import relative
580 580 > ui.write(b'REL: %s\n' % b'\nREL: '.join(relative.getresult()))
581 581 >
582 582 > # import modules from external library
583 583 > from extlibroot.lsub1.lsub2 import used as lused, unused as lunused
584 584 > from extlibroot.lsub1.lsub2.called import func as lfunc
585 585 > from extlibroot.recursedown import absdetail, legacydetail
586 586 > from extlibroot.shadowing import proxied
587 587 >
588 588 > def uisetup(ui):
589 589 > result = []
590 590 > result.append(lused.detail)
591 591 > result.append(lfunc())
592 592 > result.append(absdetail)
593 593 > result.append(legacydetail)
594 594 > result.append(proxied.detail)
595 595 > ui.write(b'LIB: %s\n' % b'\nLIB: '.join(result))
596 596 > NO_CHECK_EOF
597 597
598 598 Examine module importing.
599 599
600 600 $ (PYTHONPATH=${PYTHONPATH}${PATHSEP}${TESTTMP}; hg --config extensions.absextroot=$TESTTMP/absextroot showabsolute)
601 601 LIB: this is extlibroot.lsub1.lsub2.used
602 602 LIB: this is extlibroot.lsub1.lsub2.called.func()
603 603 LIB: this is extlibroot.recursedown.abs.used
604 604 LIB: this is extlibroot.recursedown.legacy.used
605 605 LIB: this is extlibroot.shadowing.used
606 606 ABS: this is absextroot.xsub1.xsub2.used
607 607 ABS: this is absextroot.xsub1.xsub2.called.func()
608 608
609 609 $ (PYTHONPATH=${PYTHONPATH}${PATHSEP}${TESTTMP}; hg --config extensions.absextroot=$TESTTMP/absextroot showrelative)
610 610 LIB: this is extlibroot.lsub1.lsub2.used
611 611 LIB: this is extlibroot.lsub1.lsub2.called.func()
612 612 LIB: this is extlibroot.recursedown.abs.used
613 613 LIB: this is extlibroot.recursedown.legacy.used
614 614 LIB: this is extlibroot.shadowing.used
615 615 REL: this is absextroot.xsub1.xsub2.used
616 616 REL: this is absextroot.xsub1.xsub2.called.func()
617 617 REL: this relimporter imports 'this is absextroot.relimportee'
618 618
619 619 Examine whether sub-module is imported relatively as expected.
620 620
621 621 See also issue5208 for detail about example case on Python 3.x.
622 622
623 623 $ f -q $TESTTMP/extlibroot/lsub1/lsub2/notexist.py
624 624 $TESTTMP/extlibroot/lsub1/lsub2/notexist.py: file not found
625 625
626 626 $ cat > $TESTTMP/notexist.py <<NO_CHECK_EOF
627 627 > text = 'notexist.py at root is loaded unintentionally\n'
628 628 > NO_CHECK_EOF
629 629
630 630 $ cat > $TESTTMP/checkrelativity.py <<NO_CHECK_EOF
631 631 > from mercurial import registrar
632 632 > cmdtable = {}
633 633 > command = registrar.command(cmdtable)
634 634 >
635 635 > # demand import avoids failure of importing notexist here, but only on
636 636 > # Python 2.
637 637 > import extlibroot.lsub1.lsub2.notexist
638 638 >
639 639 > @command(b'checkrelativity', [], norepo=True)
640 640 > def checkrelativity(ui, *args, **opts):
641 641 > try:
642 642 > ui.write(extlibroot.lsub1.lsub2.notexist.text)
643 643 > return 1 # unintentional success
644 644 > except ImportError:
645 645 > pass # intentional failure
646 646 > NO_CHECK_EOF
647 647
648 648 Python 3's lazy importer verifies modules exist before returning the lazy
649 649 module stub. Our custom lazy importer for Python 2 always returns a stub.
650 650
651 651 $ (PYTHONPATH=${PYTHONPATH}${PATHSEP}${TESTTMP}; hg --config extensions.checkrelativity=$TESTTMP/checkrelativity.py checkrelativity) || true
652 652 *** failed to import extension checkrelativity from $TESTTMP/checkrelativity.py: No module named 'extlibroot.lsub1.lsub2.notexist' (py3 !)
653 653 hg: unknown command 'checkrelativity' (py3 !)
654 654 (use 'hg help' for a list of commands) (py3 !)
655 655
656 656 #endif
657 657
658 658 (Here, module importing tests are finished. Therefore, use other than
659 659 NO_CHECK_* limit mark for heredoc python files, in order to apply
660 660 import-checker.py or so on their contents)
661 661
662 662 Make sure a broken uisetup doesn't globally break hg:
663 663 $ cat > $TESTTMP/baduisetup.py <<EOF
664 664 > def uisetup(ui):
665 665 > 1 / 0
666 666 > EOF
667 667
668 668 Even though the extension fails during uisetup, hg is still basically usable:
669 669 $ hg --config extensions.baduisetup=$TESTTMP/baduisetup.py version
670 670 Traceback (most recent call last):
671 671 File "*/mercurial/extensions.py", line *, in _runuisetup (glob) (no-pyoxidizer !)
672 672 File "mercurial.extensions", line *, in _runuisetup (glob) (pyoxidizer !)
673 673 uisetup(ui)
674 674 File "$TESTTMP/baduisetup.py", line 2, in uisetup
675 675 1 / 0
676 676 ZeroDivisionError: * by zero (glob)
677 677 *** failed to set up extension baduisetup: * by zero (glob)
678 678 Mercurial Distributed SCM (version *) (glob)
679 679 (see https://mercurial-scm.org for more information)
680 680
681 681 Copyright (C) 2005-* Olivia Mackall and others (glob)
682 682 This is free software; see the source for copying conditions. There is NO
683 683 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
684 684
685 685 $ cd ..
686 686
687 687 hide outer repo
688 688 $ hg init
689 689
690 690 $ cat > empty.py <<EOF
691 691 > '''empty cmdtable
692 692 > '''
693 693 > cmdtable = {}
694 694 > EOF
695 695 $ emptypath=`pwd`/empty.py
696 696 $ echo "empty = $emptypath" >> $HGRCPATH
697 697 $ hg help empty
698 698 empty extension - empty cmdtable
699 699
700 700 no commands defined
701 701
702 702
703 703 $ echo 'empty = !' >> $HGRCPATH
704 704
705 705 $ cat > debugextension.py <<EOF
706 706 > '''only debugcommands
707 707 > '''
708 708 > from mercurial import registrar
709 709 > cmdtable = {}
710 710 > command = registrar.command(cmdtable)
711 711 > @command(b'debugfoobar', [], b'hg debugfoobar')
712 712 > def debugfoobar(ui, repo, *args, **opts):
713 713 > "yet another debug command"
714 714 > @command(b'foo', [], b'hg foo')
715 715 > def foo(ui, repo, *args, **opts):
716 716 > """yet another foo command
717 717 > This command has been DEPRECATED since forever.
718 718 > """
719 719 > EOF
720 720 $ debugpath=`pwd`/debugextension.py
721 721 $ echo "debugextension = $debugpath" >> $HGRCPATH
722 722
723 723 $ hg help debugextension
724 724 hg debugextensions
725 725
726 726 show information about active extensions
727 727
728 728 options:
729 729
730 730 -T --template TEMPLATE display with template
731 731
732 732 (some details hidden, use --verbose to show complete help)
733 733
734 734
735 735 $ hg --verbose help debugextension
736 736 hg debugextensions
737 737
738 738 show information about active extensions
739 739
740 740 options:
741 741
742 742 -T --template TEMPLATE display with template
743 743
744 744 global options ([+] can be repeated):
745 745
746 746 -R --repository REPO repository root directory or name of overlay bundle
747 747 file
748 748 --cwd DIR change working directory
749 749 -y --noninteractive do not prompt, automatically pick the first choice for
750 750 all prompts
751 751 -q --quiet suppress output
752 752 -v --verbose enable additional output
753 753 --color TYPE when to colorize (boolean, always, auto, never, or
754 754 debug)
755 755 --config CONFIG [+] set/override config option (use 'section.name=value')
756 756 --debug enable debugging output
757 757 --debugger start debugger
758 758 --encoding ENCODE set the charset encoding (default: ascii)
759 759 --encodingmode MODE set the charset encoding mode (default: strict)
760 760 --traceback always print a traceback on exception
761 761 --time time how long the command takes
762 762 --profile print command execution profile
763 763 --version output version information and exit
764 764 -h --help display help and exit
765 765 --hidden consider hidden changesets
766 766 --pager TYPE when to paginate (boolean, always, auto, or never)
767 767 (default: auto)
768 768
769 769
770 770
771 771
772 772
773 773
774 774 $ hg --debug help debugextension
775 775 hg debugextensions
776 776
777 777 show information about active extensions
778 778
779 779 options:
780 780
781 781 -T --template TEMPLATE display with template
782 782
783 783 global options ([+] can be repeated):
784 784
785 785 -R --repository REPO repository root directory or name of overlay bundle
786 786 file
787 787 --cwd DIR change working directory
788 788 -y --noninteractive do not prompt, automatically pick the first choice for
789 789 all prompts
790 790 -q --quiet suppress output
791 791 -v --verbose enable additional output
792 792 --color TYPE when to colorize (boolean, always, auto, never, or
793 793 debug)
794 794 --config CONFIG [+] set/override config option (use 'section.name=value')
795 795 --debug enable debugging output
796 796 --debugger start debugger
797 797 --encoding ENCODE set the charset encoding (default: ascii)
798 798 --encodingmode MODE set the charset encoding mode (default: strict)
799 799 --traceback always print a traceback on exception
800 800 --time time how long the command takes
801 801 --profile print command execution profile
802 802 --version output version information and exit
803 803 -h --help display help and exit
804 804 --hidden consider hidden changesets
805 805 --pager TYPE when to paginate (boolean, always, auto, or never)
806 806 (default: auto)
807 807
808 808
809 809
810 810
811 811
812 812 $ echo 'debugextension = !' >> $HGRCPATH
813 813
814 814 Asking for help about a deprecated extension should do something useful:
815 815
816 816 $ hg help glog
817 817 'glog' is provided by the following extension:
818 818
819 819 graphlog command to view revision graphs from a shell (DEPRECATED)
820 820
821 821 (use 'hg help extensions' for information on enabling extensions)
822 822
823 823 Extension module help vs command help:
824 824
825 825 $ echo 'extdiff =' >> $HGRCPATH
826 826 $ hg help extdiff
827 827 hg extdiff [OPT]... [FILE]...
828 828
829 829 use external program to diff repository (or selected files)
830 830
831 831 Show differences between revisions for the specified files, using an
832 832 external program. The default program used is diff, with default options
833 833 "-Npru".
834 834
835 835 To select a different program, use the -p/--program option. The program
836 836 will be passed the names of two directories to compare, unless the --per-
837 837 file option is specified (see below). To pass additional options to the
838 838 program, use -o/--option. These will be passed before the names of the
839 839 directories or files to compare.
840 840
841 841 The --from, --to, and --change options work the same way they do for 'hg
842 842 diff'.
843 843
844 844 The --per-file option runs the external program repeatedly on each file to
845 845 diff, instead of once on two directories. By default, this happens one by
846 846 one, where the next file diff is open in the external program only once
847 847 the previous external program (for the previous file diff) has exited. If
848 848 the external program has a graphical interface, it can open all the file
849 849 diffs at once instead of one by one. See 'hg help -e extdiff' for
850 850 information about how to tell Mercurial that a given program has a
851 851 graphical interface.
852 852
853 853 The --confirm option will prompt the user before each invocation of the
854 854 external program. It is ignored if --per-file isn't specified.
855 855
856 856 (use 'hg help -e extdiff' to show help for the extdiff extension)
857 857
858 858 options ([+] can be repeated):
859 859
860 860 -p --program CMD comparison program to run
861 861 -o --option OPT [+] pass option to comparison program
862 862 --from REV1 revision to diff from
863 863 --to REV2 revision to diff to
864 864 -c --change REV change made by revision
865 865 --per-file compare each file instead of revision snapshots
866 866 --confirm prompt user before each external program invocation
867 867 --patch compare patches for two revisions
868 868 -I --include PATTERN [+] include names matching the given patterns
869 869 -X --exclude PATTERN [+] exclude names matching the given patterns
870 870 -S --subrepos recurse into subrepositories
871 871
872 872 (some details hidden, use --verbose to show complete help)
873 873
874 874
875 875
876 876
877 877
878 878
879 879
880 880
881 881
882 882
883 883 $ hg help --extension extdiff
884 884 extdiff extension - command to allow external programs to compare revisions
885 885
886 886 The extdiff Mercurial extension allows you to use external programs to compare
887 887 revisions, or revision with working directory. The external diff programs are
888 888 called with a configurable set of options and two non-option arguments: paths
889 889 to directories containing snapshots of files to compare.
890 890
891 891 If there is more than one file being compared and the "child" revision is the
892 892 working directory, any modifications made in the external diff program will be
893 893 copied back to the working directory from the temporary directory.
894 894
895 895 The extdiff extension also allows you to configure new diff commands, so you
896 896 do not need to type 'hg extdiff -p kdiff3' always.
897 897
898 898 [extdiff]
899 899 # add new command that runs GNU diff(1) in 'context diff' mode
900 900 cdiff = gdiff -Nprc5
901 901 ## or the old way:
902 902 #cmd.cdiff = gdiff
903 903 #opts.cdiff = -Nprc5
904 904
905 905 # add new command called meld, runs meld (no need to name twice). If
906 906 # the meld executable is not available, the meld tool in [merge-tools]
907 907 # will be used, if available
908 908 meld =
909 909
910 910 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
911 911 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
912 912 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
913 913 # your .vimrc
914 914 vimdiff = gvim -f "+next" \
915 915 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
916 916
917 917 Tool arguments can include variables that are expanded at runtime:
918 918
919 919 $parent1, $plabel1 - filename, descriptive label of first parent
920 920 $child, $clabel - filename, descriptive label of child revision
921 921 $parent2, $plabel2 - filename, descriptive label of second parent
922 922 $root - repository root
923 923 $parent is an alias for $parent1.
924 924
925 925 The extdiff extension will look in your [diff-tools] and [merge-tools]
926 926 sections for diff tool arguments, when none are specified in [extdiff].
927 927
928 928 [extdiff]
929 929 kdiff3 =
930 930
931 931 [diff-tools]
932 932 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
933 933
934 934 If a program has a graphical interface, it might be interesting to tell
935 935 Mercurial about it. It will prevent the program from being mistakenly used in
936 936 a terminal-only environment (such as an SSH terminal session), and will make
937 937 'hg extdiff --per-file' open multiple file diffs at once instead of one by one
938 938 (if you still want to open file diffs one by one, you can use the --confirm
939 939 option).
940 940
941 941 Declaring that a tool has a graphical interface can be done with the "gui"
942 942 flag next to where "diffargs" are specified:
943 943
944 944 [diff-tools]
945 945 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
946 946 kdiff3.gui = true
947 947
948 948 You can use -I/-X and list of file or directory names like normal 'hg diff'
949 949 command. The extdiff extension makes snapshots of only needed files, so
950 950 running the external diff program will actually be pretty fast (at least
951 951 faster than having to compare the entire tree).
952 952
953 953 list of commands:
954 954
955 955 extdiff use external program to diff repository (or selected files)
956 956
957 957 (use 'hg help -v -e extdiff' to show built-in aliases and global options)
958 958
959 959
960 960
961 961
962 962
963 963
964 964
965 965
966 966
967 967
968 968
969 969
970 970
971 971
972 972
973 973
974 974 $ echo 'extdiff = !' >> $HGRCPATH
975 975
976 976 Test help topic with same name as extension
977 977
978 978 $ cat > multirevs.py <<EOF
979 979 > from mercurial import commands, registrar
980 980 > cmdtable = {}
981 981 > command = registrar.command(cmdtable)
982 982 > """multirevs extension
983 983 > Big multi-line module docstring."""
984 984 > @command(b'multirevs', [], b'ARG', norepo=True)
985 985 > def multirevs(ui, repo, arg, *args, **opts):
986 986 > """multirevs command"""
987 987 > EOF
988 988 $ echo "multirevs = multirevs.py" >> $HGRCPATH
989 989
990 990 $ hg help multirevs | tail
991 991 used):
992 992
993 993 hg update :@
994 994
995 995 - Show diff between tags 1.3 and 1.5 (this works because the first and the
996 996 last revisions of the revset are used):
997 997
998 998 hg diff -r 1.3::1.5
999 999
1000 1000 use 'hg help -c multirevs' to see help for the multirevs command
1001 1001
1002 1002
1003 1003
1004 1004
1005 1005
1006 1006
1007 1007 $ hg help -c multirevs
1008 1008 hg multirevs ARG
1009 1009
1010 1010 multirevs command
1011 1011
1012 1012 (some details hidden, use --verbose to show complete help)
1013 1013
1014 1014
1015 1015
1016 1016 $ hg multirevs
1017 1017 hg multirevs: invalid arguments
1018 1018 hg multirevs ARG
1019 1019
1020 1020 multirevs command
1021 1021
1022 1022 (use 'hg multirevs -h' to show more help)
1023 1023 [10]
1024 1024
1025 1025
1026 1026
1027 1027 $ echo "multirevs = !" >> $HGRCPATH
1028 1028
1029 1029 Issue811: Problem loading extensions twice (by site and by user)
1030 1030
1031 1031 $ cat <<EOF >> $HGRCPATH
1032 1032 > mq =
1033 1033 > strip =
1034 1034 > hgext.mq =
1035 1035 > hgext/mq =
1036 1036 > EOF
1037 1037
1038 1038 Show extensions:
1039 1039 (note that mq force load strip, also checking it's not loaded twice)
1040 1040
1041 1041 #if no-extraextensions
1042 1042 $ hg debugextensions
1043 1043 mq
1044 1044 strip
1045 1045 #endif
1046 1046
1047 1047 For extensions, which name matches one of its commands, help
1048 1048 message should ask '-v -e' to get list of built-in aliases
1049 1049 along with extension help itself
1050 1050
1051 1051 $ mkdir $TESTTMP/d
1052 1052 $ cat > $TESTTMP/d/dodo.py <<EOF
1053 1053 > """
1054 1054 > This is an awesome 'dodo' extension. It does nothing and
1055 1055 > writes 'Foo foo'
1056 1056 > """
1057 1057 > from mercurial import commands, registrar
1058 1058 > cmdtable = {}
1059 1059 > command = registrar.command(cmdtable)
1060 1060 > @command(b'dodo', [], b'hg dodo')
1061 1061 > def dodo(ui, *args, **kwargs):
1062 1062 > """Does nothing"""
1063 1063 > ui.write(b"I do nothing. Yay\\n")
1064 1064 > @command(b'foofoo', [], b'hg foofoo')
1065 1065 > def foofoo(ui, *args, **kwargs):
1066 1066 > """Writes 'Foo foo'"""
1067 1067 > ui.write(b"Foo foo\\n")
1068 1068 > EOF
1069 1069 $ dodopath=$TESTTMP/d/dodo.py
1070 1070
1071 1071 $ echo "dodo = $dodopath" >> $HGRCPATH
1072 1072
1073 1073 Make sure that user is asked to enter '-v -e' to get list of built-in aliases
1074 1074 $ hg help -e dodo
1075 1075 dodo extension -
1076 1076
1077 1077 This is an awesome 'dodo' extension. It does nothing and writes 'Foo foo'
1078 1078
1079 1079 list of commands:
1080 1080
1081 1081 dodo Does nothing
1082 1082 foofoo Writes 'Foo foo'
1083 1083
1084 1084 (use 'hg help -v -e dodo' to show built-in aliases and global options)
1085 1085
1086 1086 Make sure that '-v -e' prints list of built-in aliases along with
1087 1087 extension help itself
1088 1088 $ hg help -v -e dodo
1089 1089 dodo extension -
1090 1090
1091 1091 This is an awesome 'dodo' extension. It does nothing and writes 'Foo foo'
1092 1092
1093 1093 list of commands:
1094 1094
1095 1095 dodo Does nothing
1096 1096 foofoo Writes 'Foo foo'
1097 1097
1098 1098 global options ([+] can be repeated):
1099 1099
1100 1100 -R --repository REPO repository root directory or name of overlay bundle
1101 1101 file
1102 1102 --cwd DIR change working directory
1103 1103 -y --noninteractive do not prompt, automatically pick the first choice for
1104 1104 all prompts
1105 1105 -q --quiet suppress output
1106 1106 -v --verbose enable additional output
1107 1107 --color TYPE when to colorize (boolean, always, auto, never, or
1108 1108 debug)
1109 1109 --config CONFIG [+] set/override config option (use 'section.name=value')
1110 1110 --debug enable debugging output
1111 1111 --debugger start debugger
1112 1112 --encoding ENCODE set the charset encoding (default: ascii)
1113 1113 --encodingmode MODE set the charset encoding mode (default: strict)
1114 1114 --traceback always print a traceback on exception
1115 1115 --time time how long the command takes
1116 1116 --profile print command execution profile
1117 1117 --version output version information and exit
1118 1118 -h --help display help and exit
1119 1119 --hidden consider hidden changesets
1120 1120 --pager TYPE when to paginate (boolean, always, auto, or never)
1121 1121 (default: auto)
1122 1122
1123 1123 Make sure that single '-v' option shows help and built-ins only for 'dodo' command
1124 1124 $ hg help -v dodo
1125 1125 hg dodo
1126 1126
1127 1127 Does nothing
1128 1128
1129 1129 (use 'hg help -e dodo' to show help for the dodo extension)
1130 1130
1131 1131 options:
1132 1132
1133 1133 --mq operate on patch repository
1134 1134
1135 1135 global options ([+] can be repeated):
1136 1136
1137 1137 -R --repository REPO repository root directory or name of overlay bundle
1138 1138 file
1139 1139 --cwd DIR change working directory
1140 1140 -y --noninteractive do not prompt, automatically pick the first choice for
1141 1141 all prompts
1142 1142 -q --quiet suppress output
1143 1143 -v --verbose enable additional output
1144 1144 --color TYPE when to colorize (boolean, always, auto, never, or
1145 1145 debug)
1146 1146 --config CONFIG [+] set/override config option (use 'section.name=value')
1147 1147 --debug enable debugging output
1148 1148 --debugger start debugger
1149 1149 --encoding ENCODE set the charset encoding (default: ascii)
1150 1150 --encodingmode MODE set the charset encoding mode (default: strict)
1151 1151 --traceback always print a traceback on exception
1152 1152 --time time how long the command takes
1153 1153 --profile print command execution profile
1154 1154 --version output version information and exit
1155 1155 -h --help display help and exit
1156 1156 --hidden consider hidden changesets
1157 1157 --pager TYPE when to paginate (boolean, always, auto, or never)
1158 1158 (default: auto)
1159 1159
1160 1160 In case when extension name doesn't match any of its commands,
1161 1161 help message should ask for '-v' to get list of built-in aliases
1162 1162 along with extension help
1163 1163 $ cat > $TESTTMP/d/dudu.py <<EOF
1164 1164 > """
1165 1165 > This is an awesome 'dudu' extension. It does something and
1166 1166 > also writes 'Beep beep'
1167 1167 > """
1168 1168 > from mercurial import commands, registrar
1169 1169 > cmdtable = {}
1170 1170 > command = registrar.command(cmdtable)
1171 1171 > @command(b'something', [], b'hg something')
1172 1172 > def something(ui, *args, **kwargs):
1173 1173 > """Does something"""
1174 1174 > ui.write(b"I do something. Yaaay\\n")
1175 1175 > @command(b'beep', [], b'hg beep')
1176 1176 > def beep(ui, *args, **kwargs):
1177 1177 > """Writes 'Beep beep'"""
1178 1178 > ui.write(b"Beep beep\\n")
1179 1179 > EOF
1180 1180 $ dudupath=$TESTTMP/d/dudu.py
1181 1181
1182 1182 $ echo "dudu = $dudupath" >> $HGRCPATH
1183 1183
1184 1184 $ hg help -e dudu
1185 1185 dudu extension -
1186 1186
1187 1187 This is an awesome 'dudu' extension. It does something and also writes 'Beep
1188 1188 beep'
1189 1189
1190 1190 list of commands:
1191 1191
1192 1192 beep Writes 'Beep beep'
1193 1193 something Does something
1194 1194
1195 1195 (use 'hg help -v dudu' to show built-in aliases and global options)
1196 1196
1197 1197 In case when extension name doesn't match any of its commands,
1198 1198 help options '-v' and '-v -e' should be equivalent
1199 1199 $ hg help -v dudu
1200 1200 dudu extension -
1201 1201
1202 1202 This is an awesome 'dudu' extension. It does something and also writes 'Beep
1203 1203 beep'
1204 1204
1205 1205 list of commands:
1206 1206
1207 1207 beep Writes 'Beep beep'
1208 1208 something Does something
1209 1209
1210 1210 global options ([+] can be repeated):
1211 1211
1212 1212 -R --repository REPO repository root directory or name of overlay bundle
1213 1213 file
1214 1214 --cwd DIR change working directory
1215 1215 -y --noninteractive do not prompt, automatically pick the first choice for
1216 1216 all prompts
1217 1217 -q --quiet suppress output
1218 1218 -v --verbose enable additional output
1219 1219 --color TYPE when to colorize (boolean, always, auto, never, or
1220 1220 debug)
1221 1221 --config CONFIG [+] set/override config option (use 'section.name=value')
1222 1222 --debug enable debugging output
1223 1223 --debugger start debugger
1224 1224 --encoding ENCODE set the charset encoding (default: ascii)
1225 1225 --encodingmode MODE set the charset encoding mode (default: strict)
1226 1226 --traceback always print a traceback on exception
1227 1227 --time time how long the command takes
1228 1228 --profile print command execution profile
1229 1229 --version output version information and exit
1230 1230 -h --help display help and exit
1231 1231 --hidden consider hidden changesets
1232 1232 --pager TYPE when to paginate (boolean, always, auto, or never)
1233 1233 (default: auto)
1234 1234
1235 1235 $ hg help -v -e dudu
1236 1236 dudu extension -
1237 1237
1238 1238 This is an awesome 'dudu' extension. It does something and also writes 'Beep
1239 1239 beep'
1240 1240
1241 1241 list of commands:
1242 1242
1243 1243 beep Writes 'Beep beep'
1244 1244 something Does something
1245 1245
1246 1246 global options ([+] can be repeated):
1247 1247
1248 1248 -R --repository REPO repository root directory or name of overlay bundle
1249 1249 file
1250 1250 --cwd DIR change working directory
1251 1251 -y --noninteractive do not prompt, automatically pick the first choice for
1252 1252 all prompts
1253 1253 -q --quiet suppress output
1254 1254 -v --verbose enable additional output
1255 1255 --color TYPE when to colorize (boolean, always, auto, never, or
1256 1256 debug)
1257 1257 --config CONFIG [+] set/override config option (use 'section.name=value')
1258 1258 --debug enable debugging output
1259 1259 --debugger start debugger
1260 1260 --encoding ENCODE set the charset encoding (default: ascii)
1261 1261 --encodingmode MODE set the charset encoding mode (default: strict)
1262 1262 --traceback always print a traceback on exception
1263 1263 --time time how long the command takes
1264 1264 --profile print command execution profile
1265 1265 --version output version information and exit
1266 1266 -h --help display help and exit
1267 1267 --hidden consider hidden changesets
1268 1268 --pager TYPE when to paginate (boolean, always, auto, or never)
1269 1269 (default: auto)
1270 1270
1271 1271 Disabled extension commands:
1272 1272
1273 1273 $ ORGHGRCPATH=$HGRCPATH
1274 1274 $ HGRCPATH=
1275 1275 $ export HGRCPATH
1276 1276 $ hg help email
1277 1277 'email' is provided by the following extension:
1278 1278
1279 1279 patchbomb command to send changesets as (a series of) patch emails
1280 1280
1281 1281 (use 'hg help extensions' for information on enabling extensions)
1282 1282
1283 1283
1284 1284 $ hg qdel
1285 1285 hg: unknown command 'qdel'
1286 1286 'qdelete' is provided by the following extension:
1287 1287
1288 1288 mq manage a stack of patches
1289 1289
1290 1290 (use 'hg help extensions' for information on enabling extensions)
1291 1291 [255]
1292 1292
1293 1293
1294 1294 $ hg churn
1295 1295 hg: unknown command 'churn'
1296 1296 'churn' is provided by the following extension:
1297 1297
1298 1298 churn command to display statistics about repository history
1299 1299
1300 1300 (use 'hg help extensions' for information on enabling extensions)
1301 1301 [255]
1302 1302
1303 1303
1304 1304
1305 1305 Disabled extensions:
1306 1306
1307 1307 $ hg help churn
1308 1308 churn extension - command to display statistics about repository history
1309 1309
1310 1310 (use 'hg help extensions' for information on enabling extensions)
1311 1311
1312 1312 $ hg help patchbomb
1313 1313 patchbomb extension - command to send changesets as (a series of) patch emails
1314 1314
1315 1315 The series is started off with a "[PATCH 0 of N]" introduction, which
1316 1316 describes the series as a whole.
1317 1317
1318 1318 Each patch email has a Subject line of "[PATCH M of N] ...", using the first
1319 1319 line of the changeset description as the subject text. The message contains
1320 1320 two or three body parts:
1321 1321
1322 1322 - The changeset description.
1323 1323 - [Optional] The result of running diffstat on the patch.
1324 1324 - The patch itself, as generated by 'hg export'.
1325 1325
1326 1326 Each message refers to the first in the series using the In-Reply-To and
1327 1327 References headers, so they will show up as a sequence in threaded mail and
1328 1328 news readers, and in mail archives.
1329 1329
1330 1330 To configure other defaults, add a section like this to your configuration
1331 1331 file:
1332 1332
1333 1333 [email]
1334 1334 from = My Name <my@email>
1335 1335 to = recipient1, recipient2, ...
1336 1336 cc = cc1, cc2, ...
1337 1337 bcc = bcc1, bcc2, ...
1338 1338 reply-to = address1, address2, ...
1339 1339
1340 1340 Use "[patchbomb]" as configuration section name if you need to override global
1341 1341 "[email]" address settings.
1342 1342
1343 1343 Then you can use the 'hg email' command to mail a series of changesets as a
1344 1344 patchbomb.
1345 1345
1346 1346 You can also either configure the method option in the email section to be a
1347 1347 sendmail compatible mailer or fill out the [smtp] section so that the
1348 1348 patchbomb extension can automatically send patchbombs directly from the
1349 1349 commandline. See the [email] and [smtp] sections in hgrc(5) for details.
1350 1350
1351 1351 By default, 'hg email' will prompt for a "To" or "CC" header if you do not
1352 1352 supply one via configuration or the command line. You can override this to
1353 1353 never prompt by configuring an empty value:
1354 1354
1355 1355 [email]
1356 1356 cc =
1357 1357
1358 1358 You can control the default inclusion of an introduction message with the
1359 1359 "patchbomb.intro" configuration option. The configuration is always
1360 1360 overwritten by command line flags like --intro and --desc:
1361 1361
1362 1362 [patchbomb]
1363 1363 intro=auto # include introduction message if more than 1 patch (default)
1364 1364 intro=never # never include an introduction message
1365 1365 intro=always # always include an introduction message
1366 1366
1367 1367 You can specify a template for flags to be added in subject prefixes. Flags
1368 1368 specified by --flag option are exported as "{flags}" keyword:
1369 1369
1370 1370 [patchbomb]
1371 1371 flagtemplate = "{separate(' ',
1372 1372 ifeq(branch, 'default', '', branch|upper),
1373 1373 flags)}"
1374 1374
1375 1375 You can set patchbomb to always ask for confirmation by setting
1376 1376 "patchbomb.confirm" to true.
1377 1377
1378 1378 (use 'hg help extensions' for information on enabling extensions)
1379 1379
1380 1380
1381 1381 Help can find unimported extensions
1382 1382 -----------------------------------
1383 1383
1384 1384 XXX-PYOXIDIZER since the frozen binary does not have source directory tree,
1385 1385 this make the checking for actual file under `hgext` a bit complicated. In
1386 1386 addition these tests do some strange dance to ensure some other module are the
1387 1387 first in `sys.path` (since the current install path is always in front
1388 1388 otherwise) that are fragile and that does not match reality in the field. So
1389 1389 for now we disable this test untill a deeper rework of that logic is done.
1390 1390
1391 1391 #if no-pyoxidizer
1392 1392
1393 1393 Broken disabled extension and command:
1394 1394
1395 1395 $ mkdir hgext
1396 1396 $ echo > hgext/__init__.py
1397 1397 $ cat > hgext/broken.py <<NO_CHECK_EOF
1398 1398 > "broken extension'
1399 1399 > NO_CHECK_EOF
1400 1400 $ cat > path.py <<EOF
1401 1401 > import os
1402 1402 > import sys
1403 1403 > sys.path.insert(0, os.environ['HGEXTPATH'])
1404 1404 > EOF
1405 1405 $ HGEXTPATH=`pwd`
1406 1406 $ export HGEXTPATH
1407 1407
1408 1408 $ hg --config extensions.path=./path.py help broken
1409 1409 broken extension - (no help text available)
1410 1410
1411 1411 (use 'hg help extensions' for information on enabling extensions)
1412 1412
1413 1413
1414 1414 $ cat > hgext/forest.py <<EOF
1415 1415 > cmdtable = None
1416 1416 > @command()
1417 1417 > def f():
1418 1418 > pass
1419 1419 > @command(123)
1420 1420 > def g():
1421 1421 > pass
1422 1422 > EOF
1423 1423 $ hg --config extensions.path=./path.py help foo
1424 1424 abort: no such help topic: foo
1425 1425 (try 'hg help --keyword foo')
1426 1426 [255]
1427 1427
1428 1428 #endif
1429 1429
1430 1430 ---
1431 1431
1432 1432 $ cat > throw.py <<EOF
1433 1433 > from mercurial import commands, registrar, util
1434 1434 > cmdtable = {}
1435 1435 > command = registrar.command(cmdtable)
1436 1436 > class Bogon(Exception): pass
1437 1437 > # NB: version should be bytes; simulating extension not ported to py3
1438 1438 > __version__ = '1.0.0'
1439 1439 > @command(b'throw', [], b'hg throw', norepo=True)
1440 1440 > def throw(ui, **opts):
1441 1441 > """throws an exception"""
1442 1442 > raise Bogon()
1443 1443 > EOF
1444 1444
1445 1445 Test extension without proper byteification of key attributes doesn't crash when
1446 1446 accessed.
1447 1447
1448 1448 $ hg version -v --config extensions.throw=throw.py | grep '^ '
1449 1449 throw external 1.0.0
1450 1450
1451 1451 No declared supported version, extension complains:
1452 1452 $ hg --config extensions.throw=throw.py throw 2>&1 | egrep '^\*\*'
1453 1453 ** Unknown exception encountered with possibly-broken third-party extension "throw" 1.0.0
1454 1454 ** which supports versions unknown of Mercurial.
1455 1455 ** Please disable "throw" and try your action again.
1456 1456 ** If that fixes the bug please report it to the extension author.
1457 1457 ** Python * (glob)
1458 1458 ** Mercurial Distributed SCM * (glob)
1459 1459 ** Extensions loaded: throw 1.0.0
1460 1460
1461 1461 empty declaration of supported version, extension complains (but doesn't choke if
1462 1462 the value is improperly a str instead of bytes):
1463 1463 $ echo "testedwith = ''" >> throw.py
1464 1464 $ hg --config extensions.throw=throw.py throw 2>&1 | egrep '^\*\*'
1465 1465 ** Unknown exception encountered with possibly-broken third-party extension "throw" 1.0.0
1466 1466 ** which supports versions unknown of Mercurial.
1467 1467 ** Please disable "throw" and try your action again.
1468 1468 ** If that fixes the bug please report it to the extension author.
1469 1469 ** Python * (glob)
1470 1470 ** Mercurial Distributed SCM (*) (glob)
1471 1471 ** Extensions loaded: throw 1.0.0
1472 1472
1473 1473 If the extension specifies a buglink, show that (but don't choke if the value is
1474 1474 improperly a str instead of bytes):
1475 1475 $ echo 'buglink = "http://example.com/bts"' >> throw.py
1476 1476 $ rm -f throw.pyc throw.pyo
1477 1477 $ rm -Rf __pycache__
1478 1478 $ hg --config extensions.throw=throw.py throw 2>&1 | egrep '^\*\*'
1479 1479 ** Unknown exception encountered with possibly-broken third-party extension "throw" 1.0.0
1480 1480 ** which supports versions unknown of Mercurial.
1481 1481 ** Please disable "throw" and try your action again.
1482 1482 ** If that fixes the bug please report it to http://example.com/bts
1483 1483 ** Python * (glob)
1484 1484 ** Mercurial Distributed SCM (*) (glob)
1485 1485 ** Extensions loaded: throw 1.0.0
1486 1486
1487 1487 If the extensions declare outdated versions, accuse the older extension first:
1488 1488 $ echo "from mercurial import util" >> older.py
1489 1489 $ echo "util.version = lambda:b'2.2'" >> older.py
1490 1490 $ echo "testedwith = b'1.9.3'" >> older.py
1491 1491 $ echo "testedwith = b'2.1.1'" >> throw.py
1492 1492 $ rm -f throw.pyc throw.pyo
1493 1493 $ rm -Rf __pycache__
1494 1494 $ hg --config extensions.throw=throw.py --config extensions.older=older.py \
1495 1495 > throw 2>&1 | egrep '^\*\*'
1496 1496 ** Unknown exception encountered with possibly-broken third-party extension "older" (version N/A)
1497 1497 ** which supports versions 1.9 of Mercurial.
1498 1498 ** Please disable "older" and try your action again.
1499 1499 ** If that fixes the bug please report it to the extension author.
1500 1500 ** Python * (glob)
1501 1501 ** Mercurial Distributed SCM (version 2.2)
1502 1502 ** Extensions loaded: older, throw 1.0.0
1503 1503
1504 1504 One extension only tested with older, one only with newer versions:
1505 1505 $ echo "util.version = lambda:b'2.1'" >> older.py
1506 1506 $ rm -f older.pyc older.pyo
1507 1507 $ rm -Rf __pycache__
1508 1508 $ hg --config extensions.throw=throw.py --config extensions.older=older.py \
1509 1509 > throw 2>&1 | egrep '^\*\*'
1510 1510 ** Unknown exception encountered with possibly-broken third-party extension "older" (version N/A)
1511 1511 ** which supports versions 1.9 of Mercurial.
1512 1512 ** Please disable "older" and try your action again.
1513 1513 ** If that fixes the bug please report it to the extension author.
1514 1514 ** Python * (glob)
1515 1515 ** Mercurial Distributed SCM (version 2.1)
1516 1516 ** Extensions loaded: older, throw 1.0.0
1517 1517
1518 1518 Older extension is tested with current version, the other only with newer:
1519 1519 $ echo "util.version = lambda:b'1.9.3'" >> older.py
1520 1520 $ rm -f older.pyc older.pyo
1521 1521 $ rm -Rf __pycache__
1522 1522 $ hg --config extensions.throw=throw.py --config extensions.older=older.py \
1523 1523 > throw 2>&1 | egrep '^\*\*'
1524 1524 ** Unknown exception encountered with possibly-broken third-party extension "throw" 1.0.0
1525 1525 ** which supports versions 2.1 of Mercurial.
1526 1526 ** Please disable "throw" and try your action again.
1527 1527 ** If that fixes the bug please report it to http://example.com/bts
1528 1528 ** Python * (glob)
1529 1529 ** Mercurial Distributed SCM (version 1.9.3)
1530 1530 ** Extensions loaded: older, throw 1.0.0
1531 1531
1532 1532 Ability to point to a different point
1533 1533 $ hg --config extensions.throw=throw.py --config extensions.older=older.py \
1534 1534 > --config ui.supportcontact='Your Local Goat Lenders' throw 2>&1 | egrep '^\*\*'
1535 1535 ** unknown exception encountered, please report by visiting
1536 1536 ** Your Local Goat Lenders
1537 1537 ** Python * (glob)
1538 1538 ** Mercurial Distributed SCM (*) (glob)
1539 1539 ** Extensions loaded: older, throw 1.0.0
1540 1540
1541 1541 Declare the version as supporting this hg version, show regular bts link:
1542 1542 $ hgver=`hg debuginstall -T '{hgver}'`
1543 1543 $ echo 'testedwith = """'"$hgver"'"""' >> throw.py
1544 1544 $ if [ -z "$hgver" ]; then
1545 1545 > echo "unable to fetch a mercurial version. Make sure __version__ is correct";
1546 1546 > fi
1547 1547 $ rm -f throw.pyc throw.pyo
1548 1548 $ rm -Rf __pycache__
1549 1549 $ hg --config extensions.throw=throw.py throw 2>&1 | egrep '^\*\*'
1550 1550 ** unknown exception encountered, please report by visiting
1551 1551 ** https://mercurial-scm.org/wiki/BugTracker
1552 1552 ** Python * (glob)
1553 1553 ** Mercurial Distributed SCM (*) (glob)
1554 1554 ** Extensions loaded: throw 1.0.0
1555 1555
1556 1556 Patch version is ignored during compatibility check
1557 1557 $ echo "testedwith = b'3.2'" >> throw.py
1558 1558 $ echo "util.version = lambda:b'3.2.2'" >> throw.py
1559 1559 $ rm -f throw.pyc throw.pyo
1560 1560 $ rm -Rf __pycache__
1561 1561 $ hg --config extensions.throw=throw.py throw 2>&1 | egrep '^\*\*'
1562 1562 ** unknown exception encountered, please report by visiting
1563 1563 ** https://mercurial-scm.org/wiki/BugTracker
1564 1564 ** Python * (glob)
1565 1565 ** Mercurial Distributed SCM (*) (glob)
1566 1566 ** Extensions loaded: throw 1.0.0
1567 1567
1568 1568 Test version number support in 'hg version':
1569 1569 $ echo '__version__ = (1, 2, 3)' >> throw.py
1570 1570 $ rm -f throw.pyc throw.pyo
1571 1571 $ rm -Rf __pycache__
1572 1572 $ hg version -v
1573 1573 Mercurial Distributed SCM (version *) (glob)
1574 1574 (see https://mercurial-scm.org for more information)
1575 1575
1576 1576 Copyright (C) 2005-* Olivia Mackall and others (glob)
1577 1577 This is free software; see the source for copying conditions. There is NO
1578 1578 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1579 1579
1580 1580 Enabled extensions:
1581 1581
1582 1582
1583 1583 $ hg version -v --config extensions.throw=throw.py
1584 1584 Mercurial Distributed SCM (version *) (glob)
1585 1585 (see https://mercurial-scm.org for more information)
1586 1586
1587 1587 Copyright (C) 2005-* Olivia Mackall and others (glob)
1588 1588 This is free software; see the source for copying conditions. There is NO
1589 1589 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1590 1590
1591 1591 Enabled extensions:
1592 1592
1593 1593 throw external 1.2.3
1594 1594 $ echo 'getversion = lambda: b"1.twentythree"' >> throw.py
1595 1595 $ rm -f throw.pyc throw.pyo
1596 1596 $ rm -Rf __pycache__
1597 1597 $ hg version -v --config extensions.throw=throw.py --config extensions.strip=
1598 1598 Mercurial Distributed SCM (version *) (glob)
1599 1599 (see https://mercurial-scm.org for more information)
1600 1600
1601 1601 Copyright (C) 2005-* Olivia Mackall and others (glob)
1602 1602 This is free software; see the source for copying conditions. There is NO
1603 1603 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1604 1604
1605 1605 Enabled extensions:
1606 1606
1607 1607 strip internal
1608 1608 throw external 1.twentythree
1609 1609
1610 1610 $ hg version -q --config extensions.throw=throw.py
1611 1611 Mercurial Distributed SCM (version *) (glob)
1612 1612
1613 1613 Test template output:
1614 1614
1615 1615 $ hg version --config extensions.strip= -T'{extensions}'
1616 1616 strip
1617 1617
1618 1618 Test JSON output of version:
1619 1619
1620 1620 $ hg version -Tjson
1621 1621 [
1622 1622 {
1623 1623 "extensions": [],
1624 1624 "ver": "*" (glob)
1625 1625 }
1626 1626 ]
1627 1627
1628 1628 $ hg version --config extensions.throw=throw.py -Tjson
1629 1629 [
1630 1630 {
1631 1631 "extensions": [{"bundled": false, "name": "throw", "ver": "1.twentythree"}],
1632 1632 "ver": "3.2.2"
1633 1633 }
1634 1634 ]
1635 1635
1636 1636 $ hg version --config extensions.strip= -Tjson
1637 1637 [
1638 1638 {
1639 1639 "extensions": [{"bundled": true, "name": "strip", "ver": null}],
1640 1640 "ver": "*" (glob)
1641 1641 }
1642 1642 ]
1643 1643
1644 1644 Test template output of version:
1645 1645
1646 1646 $ hg version --config extensions.throw=throw.py --config extensions.strip= \
1647 1647 > -T'{extensions % "{name} {pad(ver, 16)} ({if(bundled, "internal", "external")})\n"}'
1648 1648 strip (internal)
1649 1649 throw 1.twentythree (external)
1650 1650
1651 1651 Refuse to load extensions with minimum version requirements
1652 1652
1653 1653 $ cat > minversion1.py << EOF
1654 1654 > from mercurial import util
1655 1655 > util.version = lambda: b'3.5.2'
1656 1656 > minimumhgversion = b'3.6'
1657 1657 > EOF
1658 1658 $ hg --config extensions.minversion=minversion1.py version
1659 1659 (third party extension minversion requires version 3.6 or newer of Mercurial (current: 3.5.2); disabling)
1660 1660 Mercurial Distributed SCM (version 3.5.2)
1661 1661 (see https://mercurial-scm.org for more information)
1662 1662
1663 1663 Copyright (C) 2005-* Olivia Mackall and others (glob)
1664 1664 This is free software; see the source for copying conditions. There is NO
1665 1665 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1666 1666
1667 1667 $ cat > minversion2.py << EOF
1668 1668 > from mercurial import util
1669 1669 > util.version = lambda: b'3.6'
1670 1670 > minimumhgversion = b'3.7'
1671 1671 > EOF
1672 1672 $ hg --config extensions.minversion=minversion2.py version 2>&1 | egrep '\(third'
1673 1673 (third party extension minversion requires version 3.7 or newer of Mercurial (current: 3.6); disabling)
1674 1674
1675 1675 Can load version that is only off by point release
1676 1676
1677 1677 $ cat > minversion2.py << EOF
1678 1678 > from mercurial import util
1679 1679 > util.version = lambda: b'3.6.1'
1680 1680 > minimumhgversion = b'3.6'
1681 1681 > EOF
1682 1682 $ hg --config extensions.minversion=minversion3.py version 2>&1 | egrep '\(third'
1683 1683 [1]
1684 1684
1685 1685 Can load minimum version identical to current
1686 1686
1687 1687 $ cat > minversion3.py << EOF
1688 1688 > from mercurial import util
1689 1689 > util.version = lambda: b'3.5'
1690 1690 > minimumhgversion = b'3.5'
1691 1691 > EOF
1692 1692 $ hg --config extensions.minversion=minversion3.py version 2>&1 | egrep '\(third'
1693 1693 [1]
1694 1694
1695 Don't explode on py3 with a bad version number
1696
1697 $ cat > minversion4.py << EOF
1698 > from mercurial import util
1699 > util.version = lambda: b'3.5'
1700 > minimumhgversion = b'3'
1701 > EOF
1702 $ hg --config extensions.minversion=minversion4.py version -v
1703 Mercurial Distributed SCM (version 3.5)
1704 (see https://mercurial-scm.org for more information)
1705
1706 Copyright (C) 2005-* Olivia Mackall and others (glob)
1707 This is free software; see the source for copying conditions. There is NO
1708 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1709
1710 Enabled extensions:
1711
1712 minversion external
1713
1695 1714 Restore HGRCPATH
1696 1715
1697 1716 $ HGRCPATH=$ORGHGRCPATH
1698 1717 $ export HGRCPATH
1699 1718
1700 1719 Commands handling multiple repositories at a time should invoke only
1701 1720 "reposetup()" of extensions enabling in the target repository.
1702 1721
1703 1722 $ mkdir reposetup-test
1704 1723 $ cd reposetup-test
1705 1724
1706 1725 $ cat > $TESTTMP/reposetuptest.py <<EOF
1707 1726 > from mercurial import extensions
1708 1727 > def reposetup(ui, repo):
1709 1728 > ui.write(b'reposetup() for %s\n' % (repo.root))
1710 1729 > ui.flush()
1711 1730 > EOF
1712 1731 $ hg init src
1713 1732 $ echo a > src/a
1714 1733 $ hg -R src commit -Am '#0 at src/a'
1715 1734 adding a
1716 1735 $ echo '[extensions]' >> src/.hg/hgrc
1717 1736 $ echo '# enable extension locally' >> src/.hg/hgrc
1718 1737 $ echo "reposetuptest = $TESTTMP/reposetuptest.py" >> src/.hg/hgrc
1719 1738 $ hg -R src status
1720 1739 reposetup() for $TESTTMP/reposetup-test/src
1721 1740 reposetup() for $TESTTMP/reposetup-test/src (chg !)
1722 1741
1723 1742 #if no-extraextensions
1724 1743 $ hg --cwd src debugextensions
1725 1744 reposetup() for $TESTTMP/reposetup-test/src
1726 1745 dodo (untested!)
1727 1746 dudu (untested!)
1728 1747 mq
1729 1748 reposetuptest (untested!)
1730 1749 strip
1731 1750 #endif
1732 1751
1733 1752 $ hg clone -U src clone-dst1
1734 1753 reposetup() for $TESTTMP/reposetup-test/src
1735 1754 $ hg init push-dst1
1736 1755 $ hg -q -R src push push-dst1
1737 1756 reposetup() for $TESTTMP/reposetup-test/src
1738 1757 $ hg init pull-src1
1739 1758 $ hg -q -R pull-src1 pull src
1740 1759 reposetup() for $TESTTMP/reposetup-test/src
1741 1760
1742 1761 $ cat <<EOF >> $HGRCPATH
1743 1762 > [extensions]
1744 1763 > # disable extension globally and explicitly
1745 1764 > reposetuptest = !
1746 1765 > EOF
1747 1766 $ hg clone -U src clone-dst2
1748 1767 reposetup() for $TESTTMP/reposetup-test/src
1749 1768 $ hg init push-dst2
1750 1769 $ hg -q -R src push push-dst2
1751 1770 reposetup() for $TESTTMP/reposetup-test/src
1752 1771 $ hg init pull-src2
1753 1772 $ hg -q -R pull-src2 pull src
1754 1773 reposetup() for $TESTTMP/reposetup-test/src
1755 1774
1756 1775 $ cat <<EOF >> $HGRCPATH
1757 1776 > [extensions]
1758 1777 > # enable extension globally
1759 1778 > reposetuptest = $TESTTMP/reposetuptest.py
1760 1779 > EOF
1761 1780 $ hg clone -U src clone-dst3
1762 1781 reposetup() for $TESTTMP/reposetup-test/src
1763 1782 reposetup() for $TESTTMP/reposetup-test/clone-dst3
1764 1783 $ hg init push-dst3
1765 1784 reposetup() for $TESTTMP/reposetup-test/push-dst3
1766 1785 $ hg -q -R src push push-dst3
1767 1786 reposetup() for $TESTTMP/reposetup-test/src
1768 1787 reposetup() for $TESTTMP/reposetup-test/push-dst3
1769 1788 $ hg init pull-src3
1770 1789 reposetup() for $TESTTMP/reposetup-test/pull-src3
1771 1790 $ hg -q -R pull-src3 pull src
1772 1791 reposetup() for $TESTTMP/reposetup-test/pull-src3
1773 1792 reposetup() for $TESTTMP/reposetup-test/src
1774 1793
1775 1794 $ echo '[extensions]' >> src/.hg/hgrc
1776 1795 $ echo '# disable extension locally' >> src/.hg/hgrc
1777 1796 $ echo 'reposetuptest = !' >> src/.hg/hgrc
1778 1797 $ hg clone -U src clone-dst4
1779 1798 reposetup() for $TESTTMP/reposetup-test/clone-dst4
1780 1799 $ hg init push-dst4
1781 1800 reposetup() for $TESTTMP/reposetup-test/push-dst4
1782 1801 $ hg -q -R src push push-dst4
1783 1802 reposetup() for $TESTTMP/reposetup-test/push-dst4
1784 1803 $ hg init pull-src4
1785 1804 reposetup() for $TESTTMP/reposetup-test/pull-src4
1786 1805 $ hg -q -R pull-src4 pull src
1787 1806 reposetup() for $TESTTMP/reposetup-test/pull-src4
1788 1807
1789 1808 disabling in command line overlays with all configuration
1790 1809 $ hg --config extensions.reposetuptest=! clone -U src clone-dst5
1791 1810 $ hg --config extensions.reposetuptest=! init push-dst5
1792 1811 $ hg --config extensions.reposetuptest=! -q -R src push push-dst5
1793 1812 $ hg --config extensions.reposetuptest=! init pull-src5
1794 1813 $ hg --config extensions.reposetuptest=! -q -R pull-src5 pull src
1795 1814
1796 1815 $ cat <<EOF >> $HGRCPATH
1797 1816 > [extensions]
1798 1817 > # disable extension globally and explicitly
1799 1818 > reposetuptest = !
1800 1819 > EOF
1801 1820 $ hg init parent
1802 1821 $ hg init parent/sub1
1803 1822 $ echo 1 > parent/sub1/1
1804 1823 $ hg -R parent/sub1 commit -Am '#0 at parent/sub1'
1805 1824 adding 1
1806 1825 $ hg init parent/sub2
1807 1826 $ hg init parent/sub2/sub21
1808 1827 $ echo 21 > parent/sub2/sub21/21
1809 1828 $ hg -R parent/sub2/sub21 commit -Am '#0 at parent/sub2/sub21'
1810 1829 adding 21
1811 1830 $ cat > parent/sub2/.hgsub <<EOF
1812 1831 > sub21 = sub21
1813 1832 > EOF
1814 1833 $ hg -R parent/sub2 commit -Am '#0 at parent/sub2'
1815 1834 adding .hgsub
1816 1835 $ hg init parent/sub3
1817 1836 $ echo 3 > parent/sub3/3
1818 1837 $ hg -R parent/sub3 commit -Am '#0 at parent/sub3'
1819 1838 adding 3
1820 1839 $ cat > parent/.hgsub <<EOF
1821 1840 > sub1 = sub1
1822 1841 > sub2 = sub2
1823 1842 > sub3 = sub3
1824 1843 > EOF
1825 1844 $ hg -R parent commit -Am '#0 at parent'
1826 1845 adding .hgsub
1827 1846 $ echo '[extensions]' >> parent/.hg/hgrc
1828 1847 $ echo '# enable extension locally' >> parent/.hg/hgrc
1829 1848 $ echo "reposetuptest = $TESTTMP/reposetuptest.py" >> parent/.hg/hgrc
1830 1849 $ cp parent/.hg/hgrc parent/sub2/.hg/hgrc
1831 1850 $ hg -R parent status -S -A
1832 1851 reposetup() for $TESTTMP/reposetup-test/parent
1833 1852 reposetup() for $TESTTMP/reposetup-test/parent/sub2
1834 1853 C .hgsub
1835 1854 C .hgsubstate
1836 1855 C sub1/1
1837 1856 C sub2/.hgsub
1838 1857 C sub2/.hgsubstate
1839 1858 C sub2/sub21/21
1840 1859 C sub3/3
1841 1860
1842 1861 $ cd ..
1843 1862
1844 1863 Prohibit registration of commands that don't use @command (issue5137)
1845 1864
1846 1865 $ hg init deprecated
1847 1866 $ cd deprecated
1848 1867
1849 1868 $ cat <<EOF > deprecatedcmd.py
1850 1869 > def deprecatedcmd(repo, ui):
1851 1870 > pass
1852 1871 > cmdtable = {
1853 1872 > b'deprecatedcmd': (deprecatedcmd, [], b''),
1854 1873 > }
1855 1874 > EOF
1856 1875 $ cat <<EOF > .hg/hgrc
1857 1876 > [extensions]
1858 1877 > deprecatedcmd = `pwd`/deprecatedcmd.py
1859 1878 > mq = !
1860 1879 > hgext.mq = !
1861 1880 > hgext/mq = !
1862 1881 > EOF
1863 1882
1864 1883 $ hg deprecatedcmd > /dev/null
1865 1884 *** failed to import extension deprecatedcmd from $TESTTMP/deprecated/deprecatedcmd.py: missing attributes: norepo, optionalrepo, inferrepo
1866 1885 *** (use @command decorator to register 'deprecatedcmd')
1867 1886 hg: unknown command 'deprecatedcmd'
1868 1887 (use 'hg help' for a list of commands)
1869 1888 [10]
1870 1889
1871 1890 the extension shouldn't be loaded at all so the mq works:
1872 1891
1873 1892 $ hg qseries --config extensions.mq= > /dev/null
1874 1893 *** failed to import extension deprecatedcmd from $TESTTMP/deprecated/deprecatedcmd.py: missing attributes: norepo, optionalrepo, inferrepo
1875 1894 *** (use @command decorator to register 'deprecatedcmd')
1876 1895
1877 1896 $ cd ..
1878 1897
1879 1898 Test synopsis and docstring extending
1880 1899
1881 1900 $ hg init exthelp
1882 1901 $ cat > exthelp.py <<EOF
1883 1902 > from mercurial import commands, extensions
1884 1903 > def exbookmarks(orig, *args, **opts):
1885 1904 > return orig(*args, **opts)
1886 1905 > def uisetup(ui):
1887 1906 > synopsis = b' GREPME [--foo] [-x]'
1888 1907 > docstring = '''
1889 1908 > GREPME make sure that this is in the help!
1890 1909 > '''
1891 1910 > extensions.wrapcommand(commands.table, b'bookmarks', exbookmarks,
1892 1911 > synopsis, docstring)
1893 1912 > EOF
1894 1913 $ abspath=`pwd`/exthelp.py
1895 1914 $ echo '[extensions]' >> $HGRCPATH
1896 1915 $ echo "exthelp = $abspath" >> $HGRCPATH
1897 1916 $ cd exthelp
1898 1917 $ hg help bookmarks | grep GREPME
1899 1918 hg bookmarks [OPTIONS]... [NAME]... GREPME [--foo] [-x]
1900 1919 GREPME make sure that this is in the help!
1901 1920 $ cd ..
1902 1921
1903 1922 Prohibit the use of unicode strings as the default value of options
1904 1923
1905 1924 $ hg init $TESTTMP/opt-unicode-default
1906 1925
1907 1926 $ cat > $TESTTMP/test_unicode_default_value.py << EOF
1908 1927 > from __future__ import print_function
1909 1928 > from mercurial import registrar
1910 1929 > cmdtable = {}
1911 1930 > command = registrar.command(cmdtable)
1912 1931 > @command(b'dummy', [(b'', b'opt', u'value', u'help')], 'ext [OPTIONS]')
1913 1932 > def ext(*args, **opts):
1914 1933 > print(opts[b'opt'], flush=True)
1915 1934 > EOF
1916 1935 $ "$PYTHON" $TESTTMP/unflush.py $TESTTMP/test_unicode_default_value.py
1917 1936 $ cat > $TESTTMP/opt-unicode-default/.hg/hgrc << EOF
1918 1937 > [extensions]
1919 1938 > test_unicode_default_value = $TESTTMP/test_unicode_default_value.py
1920 1939 > EOF
1921 1940 $ hg -R $TESTTMP/opt-unicode-default dummy
1922 1941 *** failed to import extension test_unicode_default_value from $TESTTMP/test_unicode_default_value.py: unicode *'value' found in cmdtable.dummy (glob)
1923 1942 *** (use b'' to make it byte string)
1924 1943 hg: unknown command 'dummy'
1925 1944 (did you mean summary?)
1926 1945 [10]
General Comments 0
You need to be logged in to leave comments. Login now