# HG changeset patch # User Christian Ebert # Date 1316013573 -3600 # Node ID a4ea5c2d3ff3c37f0a04fda7500b00f10a48bd20 # Parent e5feed5534d7ccbb0d8bdd7490ba5d64a22086f9# Parent 639bb4617c704de72f61591be8563361c79634a7 (0.9.2compat) merge with default Adapt preserving filemode. Fix a typo. Clean up last merge. diff -r 639bb4617c70 -r a4ea5c2d3ff3 README --- a/README Sat Sep 10 13:23:41 2011 +0100 +++ b/README Wed Sep 14 16:19:33 2011 +0100 @@ -3,16 +3,25 @@ keyword extension for Mercurial SCM =================================== -CAVEAT: Please use the keyword extension distributed with -Mercurial > 1.0.2! -For Mercurial 0.9.2 to 1.0.2 install the 0.9.2compat branch. +[ standalone and backwards compatible version ] -The default and stable branches are meant for development. +CAVEAT: Only use this version with Mercurial 0.9.2 to 1.0.2! +With Mercurial > 1.0.2 use the extension included in the +distribution. install ------- -Run "python setup.py install". +Either copy hgkw/keyword.py into the hgext directory of your +Mercurial installation. +Then add the lines: + +[extensions] +hgext.keyword = + +to your hgrc file. + +Or run "python setup.py install". See also "pyton setup.py --help". Then add the line: @@ -32,5 +41,11 @@ testing ------- -$ cd tests -$ ./run-tests.py --with-hg=/path/to/hg +Copy hgkw/keyword.py into the hgext directory of your Mercurial +source tree. Copy tests/test-keyword, tests/test-keyword.out into +the tests directory of your Mercurial source tree. Change to that +directory and run: + +$ python run-tests.py test-keyword + +and then keep your fingers crossed ... diff -r 639bb4617c70 -r a4ea5c2d3ff3 hgkw/keyword.py --- a/hgkw/keyword.py Sat Sep 10 13:23:41 2011 +0100 +++ b/hgkw/keyword.py Wed Sep 14 16:19:33 2011 +0100 @@ -21,6 +21,14 @@ # # Binary files are not touched. # +# Setup in hgrc: +# +# [extensions] +# # enable extension +# keyword = /full/path/to/hgkw/keyword.py +# # or, if script in canonical hgext folder: +# # keyword = +# # Files to act upon/ignore are specified in the [keyword] section. # Customized keyword template mappings in the [keywordmaps] section. # @@ -35,115 +43,211 @@ change history. The mechanism can be regarded as a convenience for the current user or for archive distribution. -Keywords expand to the changeset data pertaining to the latest change -relative to the working directory parent of each file. +Configuration is done in the [keyword] and [keywordmaps] sections of +hgrc files. -Configuration is done in the [keyword], [keywordset] and [keywordmaps] -sections of hgrc files. - -Example:: +Example: [keyword] # expand keywords in every python file except those matching "x*" **.py = x* = ignore - [keywordset] - # prefer svn- over cvs-like default keywordmaps - svn = True - -.. note:: - The more specific you are in your filename patterns the less you - lose speed in huge repositories. +NOTE: the more specific you are in your filename patterns the less you +lose speed in huge repositories. For [keywordmaps] template mapping and expansion demonstration and -control run :hg:`kwdemo`. See :hg:`help templates` for a list of -available templates and filters. +control run "hg kwdemo". -Three additional date template filters are provided: +An additional date template filter {date|utcdate} is provided. It +returns a date like "2006/09/18 15:13:13". -:``utcdate``: "2006/09/18 15:13:13" -:``svnutcdate``: "2006-09-18 15:13:13Z" -:``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)" +The default template mappings (view with "hg kwdemo -d") can be +replaced with customized keywords and templates. Again, run "hg +kwdemo" to control the results of your config changes. -The default template mappings (view with :hg:`kwdemo -d`) can be -replaced with customized keywords and templates. Again, run -:hg:`kwdemo` to control the results of your configuration changes. - -Before changing/disabling active keywords, you must run :hg:`kwshrink` -to avoid storing expanded keywords in the change history. +Before changing/disabling active keywords, run "hg kwshrink" to avoid +the risk of inadvertently storing expanded keywords in the change +history. To force expansion after enabling it, or a configuration change, run -:hg:`kwexpand`. +"hg kwexpand". + +Also, when committing with the record extension or using mq's qrecord, +be aware that keywords cannot be updated. Again, run "hg kwexpand" on +the files in question to update keyword expansions after all changes +have been checked in. Expansions spanning more than one line and incremental expansions, like CVS' $Log$, are not supported. A keyword template map "Log = {desc}" expands to the first line of the changeset description. + +Caveat: With Mercurial versions prior to 4574925db5c0 "hg import" might + cause rejects if the patch context contains an active keyword. In that + case run "hg kwshrink", and then reimport. Or, better, use + bundle/unbundle to share changes. ''' -from mercurial import commands, context, cmdutil, dispatch, filelog, extensions -from mercurial import localrepo, match, patch, templatefilters, templater, util -from mercurial import scmutil -from mercurial.hgweb import webcommands -from mercurial.i18n import _ -import os, re, shutil, tempfile +from mercurial import commands, cmdutil, fancyopts, filelog +from mercurial import localrepo, patch, revlog, templater, util +from mercurial.node import nullid, hex +from mercurial.i18n import gettext as _ +import getopt, os, re, shutil, tempfile, time commands.optionalrepo += ' kwdemo' -cmdtable = {} -command = cmdutil.command(cmdtable) - # hg commands that do not act on keywords -nokwcommands = ('add addremove annotate bundle export grep incoming init log' - ' outgoing push tip verify convert email glog') +nokwcommands = ('add addremove annotate bundle copy export grep incoming init' + ' log outgoing push rename rollback tip verify' + ' convert email glog') # hg commands that trigger expansion only when writing to working dir, # not when reading filelog, and unexpand when reading from working dir -restricted = 'merge kwexpand kwshrink record qrecord resolve transplant' - -# names of extensions using dorecord -recordextensions = 'record' - -colortable = { - 'kwfiles.enabled': 'green bold', - 'kwfiles.deleted': 'cyan bold underline', - 'kwfiles.enabledunknown': 'green', - 'kwfiles.ignored': 'bold', - 'kwfiles.ignoredunknown': 'none' -} +restricted = ('merge record resolve qfold qimport qnew qpush qrefresh qrecord' + ' transplant') -# date like in cvs' $Date -def utcdate(text): - ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13". - ''' - return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S') -# date like in svn's $Date -def svnisodate(text): - ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13 - +0200 (Tue, 18 Aug 2009)". - ''' - return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') -# date like in svn's $Id -def svnutcdate(text): - ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18 - 11:00:13Z". - ''' - return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ') +# provide cvs-like UTC date filter +utcdate = lambda x: time.strftime('%Y/%m/%d %H:%M:%S', + time.gmtime(float(x[0]))) -templatefilters.filters.update({'utcdate': utcdate, - 'svnisodate': svnisodate, - 'svnutcdate': svnutcdate}) +def textsafe(s): + '''Safe version of util.binary with reversed logic. + Note: argument may not be None, which is allowed for util.binary.''' + return '\0' not in s # make keyword tools accessible kwtools = {'templater': None, 'hgcmd': ''} -def _defaultkwmaps(ui): - '''Returns default keywordmaps according to keywordset configuration.''' +# monkeypatch argument parsing +# due to backwards compatibility this can't be done in uisetup +# uisetup introduced with extensions module in 930ed513c864 +def _kwdispatch_parse(ui, args): + '''Monkeypatch dispatch._parse to obtain running hg command.''' + cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args) + kwtools['hgcmd'] = cmd + return cmd, func, args, options, cmdoptions + +try: + # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78 + from mercurial import dispatch + _dispatch_parse = dispatch._parse + dispatch._parse = _kwdispatch_parse +except ImportError: + try: + # commands.parse moves to cmdutil.parse in 0c61124ad877 + _dispatch_parse = cmdutil.parse + cmdutil.parse = _kwdispatch_parse + except AttributeError: + _dispatch_parse = commands.parse + commands.parse = _kwdispatch_parse + +try: + # templatefilters module introduced in 9f1e6ab76069 + from mercurial import templatefilters + template_filters = templatefilters.filters + template_firstline = templatefilters.firstline +except ImportError: + template_filters = templater.common_filters + template_firstline = templater.firstline + +def _wwrite(repo, f, data, mf): + '''Makes repo.wwrite backwards compatible.''' + # 656e06eebda7 removed file descriptor argument + # 67982d3ee76c added flags argument + try: + repo.wwrite(f, data, mf.flags(f)) + except (AttributeError, TypeError): + repo.wwrite(f, data) + +def _normal(repo, files): + '''Backwards compatible repo.dirstate.normal/update.''' + # 6fd953d5faea introduced dirstate.normal() + try: + for f in files: + repo.dirstate.normal(f) + except AttributeError: + repo.dirstate.update(files, 'n') + +def _pathto(repo, f, cwd=None): + '''kwfiles behaves similar to status, using pathto since 78b6add1f966.''' + try: + return repo.pathto(f, cwd) + except AttributeError: + return f + +'''Default match argument for commit, depending on version.''' +if hasattr(cmdutil, 'match'): + _defmatch = None +else: + _defmatch = util.always + +# commands.parse/cmdutil.parse returned nothing for +# "hg diff --rev" before 88803a69b24a due to bug in fancyopts +def _fancyopts(args, options, state): + '''Fixed fancyopts from a9b7e425674f.''' + namelist = [] + shortlist = '' + argmap = {} + defmap = {} + + for short, name, default, comment in options: + # convert opts to getopt format + oname = name + name = name.replace('-', '_') + + argmap['-' + short] = argmap['--' + oname] = name + defmap[name] = default + + # copy defaults to state + if isinstance(default, list): + state[name] = default[:] + elif callable(default): + print "whoa", name, default + state[name] = None + else: + state[name] = default + + # does it take a parameter? + if not (default is None or default is True or default is False): + if short: short += ':' + if oname: oname += '=' + if short: + shortlist += short + if name: + namelist.append(oname) + + # parse arguments + opts, args = getopt.getopt(args, shortlist, namelist) + + # transfer result to state + for opt, val in opts: + name = argmap[opt] + t = type(defmap[name]) + if t is type(fancyopts): + state[name] = defmap[name](val) + elif t is type(1): + state[name] = int(val) + elif t is type(''): + state[name] = val + elif t is type([]): + state[name].append(val) + elif t is type(None) or t is type(False): + state[name] = True + + # return unparsed args + return args + +fancyopts.fancyopts = _fancyopts + + +class kwtemplater(object): + ''' + Sets up keyword templates, corresponding keyword regex, and + provides keyword substitution functions. + ''' templates = { 'Revision': '{node|short}', 'Author': '{author|user}', - } - kwsets = ({ 'Date': '{date|utcdate}', 'RCSfile': '{file|basename},v', 'RCSFile': '{file|basename},v', # kept for backwards compatibility @@ -151,157 +255,137 @@ 'Source': '{root}/{file},v', 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}', 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}', - }, { - 'Date': '{date|svnisodate}', - 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}', - 'LastChangedRevision': '{node|short}', - 'LastChangedBy': '{author|user}', - 'LastChangedDate': '{date|svnisodate}', - }) - templates.update(kwsets[ui.configbool('keywordset', 'svn')]) - return templates - -def _shrinktext(text, subfunc): - '''Helper for keyword expansion removal in text. - Depending on subfunc also returns number of substitutions.''' - return subfunc(r'$\1$', text) - -def _preselect(wstatus, changed): - '''Retrieves modfied and added files from a working directory state - and returns the subset of each contained in given changed files - retrieved from a change context.''' - modified, added = wstatus[:2] - modified = [f for f in modified if f in changed] - added = [f for f in added if f in changed] - return modified, added - - -class kwtemplater(object): - ''' - Sets up keyword templates, corresponding keyword regex, and - provides keyword substitution functions. - ''' + } def __init__(self, ui, repo, inc, exc): self.ui = ui self.repo = repo - self.match = match.match(repo.root, '', [], inc, exc) + self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1] self.restrict = kwtools['hgcmd'] in restricted.split() - self.record = False kwmaps = self.ui.configitems('keywordmaps') if kwmaps: # override default templates - self.templates = dict((k, templater.parsestring(v, False)) - for k, v in kwmaps) - else: - self.templates = _defaultkwmaps(self.ui) + kwmaps = [(k, templater.parsestring(v, False)) + for (k, v) in kwmaps] + self.templates = dict(kwmaps) + escaped = map(re.escape, self.templates.keys()) + kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped) + self.re_kw = re.compile(kwpat) - @util.propertycache - def escape(self): - '''Returns bar-separated and escaped keywords.''' - return '|'.join(map(re.escape, self.templates.keys())) + template_filters['utcdate'] = utcdate + self.ct = self._changeset_templater() - @util.propertycache - def rekw(self): - '''Returns regex for unexpanded keywords.''' - return re.compile(r'\$(%s)\$' % self.escape) + def _changeset_templater(self): + '''Backwards compatible cmdutil.changeset_templater.''' + # before 1e0b94cfba0e there was an extra "brinfo" argument + try: + return cmdutil.changeset_templater(self.ui, self.repo, + False, '', False) + except TypeError: + return cmdutil.changeset_templater(self.ui, self.repo, + False, None, '', False) - @util.propertycache - def rekwexp(self): - '''Returns regex for expanded keywords.''' - return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape) + def getnode(self, path, fnode): + '''Derives changenode from file path and filenode.''' + # used by kwfilelog.read and kwexpand + c = self.repo.filectx(path, fileid=fnode) + return c.node() - def substitute(self, data, path, ctx, subfunc): + def substitute(self, data, path, node, subfunc): '''Replaces keywords in data with expanded template.''' def kwsub(mobj): kw = mobj.group(1) - ct = cmdutil.changeset_templater(self.ui, self.repo, - False, None, '', False) - ct.use_template(self.templates[kw]) + self.ct.use_template(self.templates[kw]) self.ui.pushbuffer() - ct.show(ctx, root=self.repo.root, file=path) - ekw = templatefilters.firstline(self.ui.popbuffer()) - return '$%s: %s $' % (kw, ekw) + self.ct.show(changenode=node, root=self.repo.root, file=path) + return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer())) return subfunc(kwsub, data) - def linkctx(self, path, fileid): - '''Similar to filelog.linkrev, but returns a changectx.''' - return self.repo.filectx(path, fileid=fileid).changectx() - def expand(self, path, node, data): '''Returns data with keywords expanded.''' - if not self.restrict and self.match(path) and not util.binary(data): - ctx = self.linkctx(path, node) - return self.substitute(data, path, ctx, self.rekw.sub) + if not self.restrict and self.matcher(path) and textsafe(data): + changenode = self.getnode(path, node) + return self.substitute(data, path, changenode, self.re_kw.sub) return data - def iskwfile(self, cand, ctx): - '''Returns subset of candidates which are configured for keyword - expansion are not symbolic links.''' - return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)] + def iskwfile(self, path, islink): + '''Returns true if path matches [keyword] pattern + and is not a symbolic link. + Caveat: localrepository._link fails on Windows.''' + return self.matcher(path) and not islink(path) - def overwrite(self, ctx, candidates, lookup, expand, rekw=False): + def overwrite(self, node, expand, candidates): '''Overwrites selected files expanding/shrinking keywords.''' - if self.restrict or lookup or self.record: # exclude kw_copy - candidates = self.iskwfile(candidates, ctx) - if not candidates: - return - kwcmd = self.restrict and lookup # kwexpand/kwshrink - if self.restrict or expand and lookup: + # repo[changeid] introduced in f6c00b17387c + if node is not None: # commit + try: + ctx = self.repo[node] + except TypeError: + ctx = self.repo.changectx(node) mf = ctx.manifest() - if self.restrict or rekw: - re_kw = self.rekw - else: - re_kw = self.rekwexp - if expand: - msg = _('overwriting %s expanding keywords\n') + candidates = [f for f in ctx.files() if f in mf] + else: # kwexpand/kwshrink + try: + ctx = self.repo['.'] + except TypeError: + ctx = self.repo.changectx() + mf = ctx.manifest() + if hasattr(ctx, 'flags'): + # 51b0e799352f + islink = lambda p: 'l' in ctx.flags(p) else: - msg = _('overwriting %s shrinking keywords\n') - for f in candidates: - if self.restrict: - data = self.repo.file(f).read(mf[f]) - else: - data = self.repo.wread(f) - if util.binary(data): - continue + islink = mf.linkf + candidates = [f for f in candidates if self.iskwfile(f, islink)] + if candidates: + self.restrict = True # do not expand when reading if expand: - if lookup: - ctx = self.linkctx(f, mf[f]) - data, found = self.substitute(data, f, ctx, re_kw.subn) - elif self.restrict: - found = re_kw.search(data) + msg = _('overwriting %s expanding keywords\n') else: - data, found = _shrinktext(data, re_kw.subn) - if found: - self.ui.note(msg % f) - fp = self.repo.wopener(f, "wb", atomictemp=True) - fp.write(data) - fp.close() - if kwcmd: - self.repo.dirstate.normal(f) - elif self.record: - self.repo.dirstate.normallookup(f) + msg = _('overwriting %s shrinking keywords\n') + overwritten = [] + for f in candidates: + fp = self.repo.file(f) + data = fp.read(mf[f]) + if not textsafe(data): + continue + if expand: + changenode = node or self.getnode(f, mf[f]) + data, found = self.substitute(data, f, changenode, + self.re_kw.subn) + else: + found = self.re_kw.search(data) + if found: + self.ui.note(msg % f) + fpath = self.repo.wjoin(f) + mode = os.lstat(fpath).st_mode + self.repo.wwrite(f, data, mf.flags(f)) + os.chmod(fpath, mode) + overwritten.append(f) + _normal(self.repo, overwritten) + self.restrict = False + + def shrinktext(self, text): + '''Unconditionally removes all keyword substitutions from text.''' + return self.re_kw.sub(r'$\1$', text) def shrink(self, fname, text): '''Returns text with all keyword substitutions removed.''' - if self.match(fname) and not util.binary(text): - return _shrinktext(text, self.rekwexp.sub) + if self.matcher(fname) and textsafe(text): + return self.shrinktext(text) return text def shrinklines(self, fname, lines): '''Returns lines with keyword substitutions removed.''' - if self.match(fname): + if self.matcher(fname): text = ''.join(lines) - if not util.binary(text): - return _shrinktext(text, self.rekwexp.sub).splitlines(True) + if textsafe(text): + return self.shrinktext(text).splitlines(True) return lines def wread(self, fname, data): '''If in restricted mode returns data read from wdir with keyword substitutions removed.''' - if self.restrict: - return self.shrink(fname, data) - return data + return self.restrict and self.shrink(fname, data) or data class kwfilelog(filelog.filelog): ''' @@ -316,8 +400,6 @@ def read(self, node): '''Expands keywords when reading filelog.''' data = super(kwfilelog, self).read(node) - if self.renamed(node): - return data return self.kwt.expand(self.path, node, data) def add(self, text, meta, tr, link, p1=None, p2=None): @@ -328,39 +410,48 @@ def cmp(self, node, text): '''Removes keyword substitutions for comparison.''' text = self.kwt.shrink(self.path, text) - return super(kwfilelog, self).cmp(node, text) + if self.renamed(node): + t2 = super(kwfilelog, self).read(node) + return t2 != text + return revlog.revlog.cmp(self, node, text) -def _status(ui, repo, wctx, kwt, *pats, **opts): +def _status(ui, repo, kwt, unknown, *pats, **opts): '''Bails out if [keyword] configuration is not active. Returns status of working directory.''' if kwt: - return repo.status(match=scmutil.match(wctx, pats, opts), clean=True, - unknown=opts.get('unknown') or opts.get('all')) + try: + # 0159b7a36184 ff. + matcher = cmdutil.match(repo, pats, opts) + try: + # 4faaa0535ea7 + return repo.status(match=matcher, unknown=unknown, clean=True) + except TypeError: + return repo.status(match=matcher, list_clean=True) + except AttributeError: + files, match, anypats = cmdutil.matchpats(repo, pats, opts) + return repo.status(files=files, match=match, list_clean=True) if ui.configitems('keyword'): raise util.Abort(_('[keyword] patterns cannot match')) raise util.Abort(_('no [keyword] patterns configured')) def _kwfwrite(ui, repo, expand, *pats, **opts): '''Selects files and passes them to kwtemplater.overwrite.''' - wctx = repo[None] - if len(wctx.parents()) > 1: + if repo.dirstate.parents()[1] != nullid: raise util.Abort(_('outstanding uncommitted merge')) kwt = kwtools['templater'] - wlock = repo.wlock() + status = _status(ui, repo, kwt, False, *pats, **opts) + modified, added, removed, deleted, unknown, ignored, clean = status + if modified or added or removed or deleted: + raise util.Abort(_('outstanding uncommitted changes')) + wlock = lock = None try: - status = _status(ui, repo, wctx, kwt, *pats, **opts) - modified, added, removed, deleted, unknown, ignored, clean = status - if modified or added or removed or deleted: - raise util.Abort(_('outstanding uncommitted changes')) - kwt.overwrite(wctx, clean, True, expand) + wlock = repo.wlock() + lock = repo.lock() + kwt.overwrite(None, expand, clean) finally: - wlock.release() + del wlock, lock -@command('kwdemo', - [('d', 'default', None, _('show default keyword template maps')), - ('f', 'rcfile', '', - _('read maps from rcfile'), _('FILE'))], - _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')) + def demo(ui, repo, *args, **opts): '''print [keywordmaps] configuration and an expansion example @@ -371,22 +462,19 @@ and using -f/--rcfile to source an external hgrc file. Use -d/--default to disable current configuration. - - See :hg:`help templates` for information on templates and filters. ''' def demoitems(section, items): + items.sort() ui.write('[%s]\n' % section) - for k, v in sorted(items): + for k, v in items: ui.write('%s = %s\n' % (k, v)) + kwstatus = 'current' fn = 'demo.txt' tmpdir = tempfile.mkdtemp('', 'kwdemo.') ui.note(_('creating temporary repository at %s\n') % tmpdir) repo = localrepo.localrepository(ui, tmpdir, True) ui.setconfig('keyword', fn, '') - svn = ui.configbool('keywordset', 'svn') - # explicitly set keywordset for demo output - ui.setconfig('keywordset', 'svn', svn) uikwmaps = ui.configitems('keywordmaps') if args or opts.get('rcfile'): @@ -394,10 +482,7 @@ if uikwmaps: ui.status(_('\textending current template maps\n')) if opts.get('default') or not uikwmaps: - if svn: - ui.status(_('\toverriding default svn keywordset\n')) - else: - ui.status(_('\toverriding default cvs keywordset\n')) + ui.status(_('\toverriding default template maps\n')) if opts.get('rcfile'): ui.readconfig(opts.get('rcfile')) if args: @@ -409,34 +494,37 @@ ui.readconfig(repo.join('hgrc')) kwmaps = dict(ui.configitems('keywordmaps')) elif opts.get('default'): - if svn: - ui.status(_('\n\tconfiguration using default svn keywordset\n')) - else: - ui.status(_('\n\tconfiguration using default cvs keywordset\n')) - kwmaps = _defaultkwmaps(ui) + ui.status(_('\n\tconfiguration using default keyword template maps\n')) + kwmaps = kwtemplater.templates if uikwmaps: ui.status(_('\tdisabling current template maps\n')) for k, v in kwmaps.iteritems(): ui.setconfig('keywordmaps', k, v) else: ui.status(_('\n\tconfiguration using current keyword template maps\n')) - if uikwmaps: - kwmaps = dict(uikwmaps) - else: - kwmaps = _defaultkwmaps(ui) + kwmaps = dict(uikwmaps) or kwtemplater.templates - uisetup(ui) reposetup(ui, repo) - ui.write('[extensions]\nkeyword =\n') + for k, v in ui.configitems('extensions'): + if k.endswith('keyword'): + extension = '%s = %s' % (k, v) + break + ui.status(_('\n\tconfig using %s keyword template maps\n') % kwstatus) + ui.write('[extensions]\n%s\n' % extension) demoitems('keyword', ui.configitems('keyword')) - demoitems('keywordset', ui.configitems('keywordset')) - demoitems('keywordmaps', kwmaps.iteritems()) - keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n' - repo.wopener.write(fn, keywords) - repo[None].add([fn]) + demoitems('keywordmaps', kwmaps.items()) + kwkeys = kwmaps.keys() + kwkeys.sort() + keywords = '$' + '$\n$'.join(kwkeys) + '$\n' + repo.wopener(fn, 'w').write(keywords) + repo.add([fn]) ui.note(_('\nkeywords written to %s:\n') % fn) ui.note(keywords) - repo.dirstate.setbranch('demobranch') + try: + repo.dirstate.setbranch('demobranch') + except AttributeError: + # before 7e1c8a565a4f + repo.opener('branch', 'w').write('demobranch\n') for name, cmd in ui.configitems('hooks'): if name.split('.', 1)[0].find('commit') > -1: repo.ui.setconfig('hooks', name, '') @@ -447,7 +535,6 @@ ui.write(repo.wread(fn)) shutil.rmtree(tmpdir, ignore_errors=True) -@command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...')) def expand(ui, repo, *pats, **opts): '''expand keywords in the working directory @@ -458,12 +545,6 @@ # 3rd argument sets expansion to True _kwfwrite(ui, repo, True, *pats, **opts) -@command('kwfiles', - [('A', 'all', None, _('show keyword status flags of all files')), - ('i', 'ignore', None, _('show files excluded from expansion')), - ('u', 'unknown', None, _('only show unknown (not tracked) files')), - ] + commands.walkopts, - _('hg kwfiles [OPTION]... [FILE]...')) def files(ui, repo, *pats, **opts): '''show files configured for keyword expansion @@ -474,11 +555,11 @@ execution by including only files that are actual candidates for expansion. - See :hg:`help keyword` on how to construct patterns both for + See "hg help keyword" on how to construct patterns both for inclusion and exclusion of files. With -A/--all and -v/--verbose the codes used to show the status - of files are:: + of files are: K = keyword expansion candidate k = keyword expansion candidate (not tracked) @@ -486,35 +567,48 @@ i = ignored (not tracked) ''' kwt = kwtools['templater'] - wctx = repo[None] - status = _status(ui, repo, wctx, kwt, *pats, **opts) + status = _status(ui, repo, kwt, *pats, **opts) cwd = pats and repo.getcwd() or '' modified, added, removed, deleted, unknown, ignored, clean = status files = [] if not opts.get('unknown') or opts.get('all'): - files = sorted(modified + added + clean) - kwfiles = kwt.iskwfile(files, wctx) - kwdeleted = kwt.iskwfile(deleted, wctx) - kwunknown = kwt.iskwfile(unknown, wctx) + try: + # f67d1468ac50 + files = util.sort(modified + added + clean) + except AttributeError: + files = modified + added + clean + files.sort() + try: + # f6c00b17387c + wctx = repo[None] + except TypeError: + wctx = repo.workingctx() + if hasattr(wctx, 'flags'): + islink = lambda p: 'l' in wctx.flags(p) + elif hasattr(wctx, 'fileflags'): + islink = lambda p: 'l' in wctx.fileflags(p) + else: + mf = wctx.manifest() + islink = mf.linkf + kwfiles = [f for f in files if kwt.iskwfile(f, islink)] + kwunknown = [f for f in unknown if kwt.iskwfile(f, islink)] if not opts.get('ignore') or opts.get('all'): - showfiles = kwfiles, kwdeleted, kwunknown + showfiles = kwfiles, kwunknown else: - showfiles = [], [], [] + showfiles = [], [] if opts.get('all') or opts.get('ignore'): showfiles += ([f for f in files if f not in kwfiles], [f for f in unknown if f not in kwunknown]) - kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split() - kwstates = zip('K!kIi', showfiles, kwlabels) - for char, filenames, kwstate in kwstates: + for char, filenames in zip('KkIi', showfiles): fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n' for f in filenames: - ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate) + ui.write(fmt % _pathto(repo, f, cwd)) -@command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...')) def shrink(ui, repo, *pats, **opts): '''revert expanded keywords in the working directory - Must be run before changing/disabling active keywords. + Run before changing/disabling active keywords or if you experience + problems with "hg import" or "hg merge". kwshrink refuses to run if given files contain local changes. ''' @@ -522,17 +616,6 @@ _kwfwrite(ui, repo, False, *pats, **opts) -def uisetup(ui): - ''' Monkeypatches dispatch._parse to retrieve user command.''' - - def kwdispatch_parse(orig, ui, args): - '''Monkeypatch dispatch._parse to obtain running hg command.''' - cmd, func, args, options, cmdoptions = orig(ui, args) - kwtools['hgcmd'] = cmd - return cmd, func, args, options, cmdoptions - - extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse) - def reposetup(ui, repo): '''Sets up repo as kwrepo for keyword substitution. Overrides file method to return kwfilelog instead of filelog @@ -543,7 +626,7 @@ try: if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split() - or '.hg' in util.splitpath(repo.root) + or '.hg' in repo.root.split(os.sep) or repo._url.startswith('bundle:')): return except AttributeError: @@ -570,132 +653,181 @@ data = super(kwrepo, self).wread(filename) return kwt.wread(filename, data) - def commit(self, *args, **opts): - # use custom commitctx for user commands - # other extensions can still wrap repo.commitctx directly - self.commitctx = self.kwcommitctx + def _commit(self, files, text, user, date, match, force, lock, wlock, + force_editor, p1, p2, extra, empty_ok): + '''Private commit wrapper for backwards compatibility.''' try: - return super(kwrepo, self).commit(*args, **opts) - finally: - del self.commitctx + return super(kwrepo, + self).commit(files=files, text=text, + user=user, date=date, match=match, + force=force, lock=lock, wlock=wlock, + force_editor=force_editor, + p1=p1, p2=p2, extra=extra) + except TypeError: + try: + return super(kwrepo, + self).commit(files=files, text=text, + user=user, date=date, + match=match, force=force, + force_editor=force_editor, + p1=p1, p2=p2, extra=extra, + empty_ok=empty_ok) + except TypeError: + return super(kwrepo, + self).commit(files=files, text=text, + user=user, date=date, + match=match, force=force, + force_editor=force_editor, + p1=p1, p2=p2, extra=extra) - def kwcommitctx(self, ctx, error=False): - n = super(kwrepo, self).commitctx(ctx, error) - # no lock needed, only called from repo.commit() which already locks - if not kwt.record: - restrict = kwt.restrict - kwt.restrict = True - kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()), - False, True) - kwt.restrict = restrict - return n + def commit(self, files=None, text='', user=None, date=None, + match=_defmatch, force=False, lock=None, wlock=None, + force_editor=False, p1=None, p2=None, extra={}, + empty_ok=False): + # (w)lock arguments removed in 126f527b3ba3 + # so they are None or what was passed to commit + # use private _(w)lock for deletion + _lock = lock + _wlock = wlock + del wlock, lock + _p1 = _p2 = None + try: + if not _wlock: + _wlock = self.wlock() + if not _lock: + _lock = self.lock() + # store and postpone commit hooks + commithooks = {} + for name, cmd in ui.configitems('hooks'): + if name.split('.', 1)[0] == 'commit': + commithooks[name] = cmd + ui.setconfig('hooks', name, '') + if commithooks: + # store parents for commit hook environment + if p1 is None: + _p1, _p2 = self.dirstate.parents() + else: + _p1, _p2 = p1, p2 or nullid + _p1 = hex(_p1) + if _p2 == nullid: + _p2 = '' + else: + _p2 = hex(_p2) - def rollback(self, dryrun=False): - wlock = self.wlock() - try: - if not dryrun: - changed = self['.'].files() - ret = super(kwrepo, self).rollback(dryrun) - if not dryrun: - ctx = self['.'] - modified, added = _preselect(self[None].status(), changed) - kwt.overwrite(ctx, modified, True, True) - kwt.overwrite(ctx, added, True, False) - return ret + n = self._commit(files, text, user, date, match, force, _lock, + _wlock, force_editor, p1, p2, extra, empty_ok) + + # restore commit hooks + for name, cmd in commithooks.iteritems(): + ui.setconfig('hooks', name, cmd) + if n is not None: + kwt.overwrite(n, True, None) + self.hook('commit', node=n, parent1=_p1, parent2=_p2) + return n finally: - wlock.release() + del _wlock, _lock + + repo.__class__ = kwrepo # monkeypatches - def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None): - '''Monkeypatch/wrap patch.patchfile.__init__ to avoid - rejects or conflicts due to expanded keywords in working dir.''' - orig(self, ui, gp, backend, store, eolmode) - # shrink keywords read from working dir - self.lines = kwt.shrinklines(self.fname, self.lines) + try: + # avoid spurious rejects if patchfile is available + def kwpatchfile_init(self, ui, fname, missing=False): + '''Monkeypatch/wrap patch.patchfile.__init__ to avoid + rejects or conflicts due to expanded keywords in working dir.''' + try: + patchfile_init(self, ui, fname, missing) + except TypeError: + # "missing" arg added in e90e72c6b4c7 + patchfile_init(self, ui, fname) + self.lines = kwt.shrinklines(self.fname, self.lines) - def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None, - opts=None, prefix=''): - '''Monkeypatch patch.diff to avoid expansion.''' - kwt.restrict = True - return orig(repo, node1, node2, match, changes, opts, prefix) - - def kwweb_skip(orig, web, req, tmpl): - '''Wraps webcommands.x turning off keyword expansion.''' - kwt.match = util.never - return orig(web, req, tmpl) + patchfile_init = patch.patchfile.__init__ + patch.patchfile.__init__ = kwpatchfile_init + except AttributeError: + pass - def kw_copy(orig, ui, repo, pats, opts, rename=False): - '''Wraps cmdutil.copy so that copy/rename destinations do not - contain expanded keywords. - Note that the source of a regular file destination may also be a - symlink: - hg cp sym x -> x is symlink - cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords) - For the latter we have to follow the symlink to find out whether its - target is configured for expansion and we therefore must unexpand the - keywords in the destination.''' - orig(ui, repo, pats, opts, rename) - if opts.get('dry_run'): - return - wctx = repo[None] - cwd = repo.getcwd() + def kw_diff(repo, node1=None, node2=None, files=None, match=_defmatch, + fp=None, changes=None, opts=None): + # only expand if comparing against working dir + if node2 is not None: + kwt.matcher = util.never + elif node1 is not None and node1 != repo.dirstate.parents()[0]: + kwt.restrict = True + try: + patch_diff(repo, node1=node1, node2=node2, files=files, + match=match, fp=fp, changes=changes, opts=opts) + except TypeError: + patch_diff(repo, node1=node1, node2=node2, match=match, fp=fp, + changes=changes, opts=opts) - def haskwsource(dest): - '''Returns true if dest is a regular file and configured for - expansion or a symlink which points to a file configured for - expansion. ''' - source = repo.dirstate.copied(dest) - if 'l' in wctx.flags(source): - source = scmutil.canonpath(repo.root, cwd, - os.path.realpath(source)) - return kwt.match(source) + patch_diff = patch.diff + if not kwt.restrict: + patch.diff = kw_diff + + try: + from mercurial.hgweb import webcommands + def kwweb_annotate(web, req, tmpl): + '''Wraps webcommands.annotate turning off keyword expansion.''' + kwt.matcher = util.never + return webcommands_annotate(web, req, tmpl) + + def kwweb_changeset(web, req, tmpl): + '''Wraps webcommands.changeset turning off keyword expansion.''' + kwt.matcher = util.never + return webcommands_changeset(web, req, tmpl) - candidates = [f for f in repo.dirstate.copies() if - not 'l' in wctx.flags(f) and haskwsource(f)] - kwt.overwrite(wctx, candidates, False, False) + def kwweb_filediff(web, req, tmpl): + '''Wraps webcommands.filediff turning off keyword expansion.''' + kwt.matcher = util.never + return webcommands_filediff(web, req, tmpl) + + webcommands_annotate = webcommands.annotate + webcommands_changeset = webcommands.changeset + webcommands_filediff = webcommands.filediff + + webcommands.annotate = kwweb_annotate + webcommands.changeset = webcommands.rev = kwweb_changeset + webcommands.filediff = webcommands.diff = kwweb_filediff - def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts): - '''Wraps record.dorecord expanding keywords after recording.''' - wlock = repo.wlock() - try: - # record returns 0 even when nothing has changed - # therefore compare nodes before and after - kwt.record = True - ctx = repo['.'] - wstatus = repo[None].status() - ret = orig(ui, repo, commitfunc, *pats, **opts) - recctx = repo['.'] - if ctx != recctx: - modified, added = _preselect(wstatus, recctx.files()) - kwt.restrict = False - kwt.overwrite(recctx, modified, False, True) - kwt.overwrite(recctx, added, False, True, True) - kwt.restrict = True - return ret - finally: - wlock.release() + except ImportError: + from mercurial.hgweb.hgweb_mod import hgweb + def kwweb_do_annotate(self, req): + kwt.matcher = util.never + hgweb_do_annotate(self, req) + + def kwweb_do_changeset(self, req): + kwt.matcher = util.never + hgweb_do_changeset(self, req) + + def kwweb_do_filediff(self, req): + kwt.matcher = util.never + hgweb_do_filediff(self, req) - def kwfilectx_cmp(orig, self, fctx): - # keyword affects data size, comparing wdir and filelog size does - # not make sense - if (fctx._filerev is None and - (self._repo._encodefilterpats or - kwt.match(fctx.path()) and not 'l' in fctx.flags()) or - self.size() == fctx.size()): - return self._filelog.cmp(self._filenode, fctx.data()) - return True + hgweb_do_annotate = hgweb.do_annotate + hgweb_do_changeset = hgweb.do_changeset + hgweb_do_filediff = hgweb.do_filediff + + hgweb.do_annotate = kwweb_do_annotate + hgweb.do_changeset = hgweb.do_rev = kwweb_do_changeset + hgweb.do_filediff = hgweb.do_diff = kwweb_do_filediff + - extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp) - extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init) - extensions.wrapfunction(patch, 'diff', kw_diff) - extensions.wrapfunction(cmdutil, 'copy', kw_copy) - for c in 'annotate changeset rev filediff diff'.split(): - extensions.wrapfunction(webcommands, c, kwweb_skip) - for name in recordextensions.split(): - try: - record = extensions.find(name) - extensions.wrapfunction(record, 'dorecord', kw_dorecord) - except KeyError: - pass - - repo.__class__ = kwrepo +cmdtable = { + 'kwdemo': + (demo, + [('d', 'default', None, _('show default keyword template maps')), + ('f', 'rcfile', '', _('read maps from rcfile'))], + _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')), + 'kwexpand': (expand, commands.walkopts, + _('hg kwexpand [OPTION]... [FILE]...')), + 'kwfiles': + (files, + [('A', 'all', None, _('show keyword status flags of all files')), + ('i', 'ignore', None, _('show files excluded from expansion')), + ('u', 'unknown', None, _('only show unknown (not tracked) files')), + ] + commands.walkopts, + _('hg kwfiles [OPTION]... [FILE]...')), + 'kwshrink': (shrink, commands.walkopts, + _('hg kwshrink [OPTION]... [FILE]...')), +} diff -r 639bb4617c70 -r a4ea5c2d3ff3 tests/bundles/test-keyword.hg Binary file tests/bundles/test-keyword.hg has changed diff -r 639bb4617c70 -r a4ea5c2d3ff3 tests/get-with-headers.py --- a/tests/get-with-headers.py Sat Sep 10 13:23:41 2011 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -#!/usr/bin/env python - -"""This does HTTP GET requests given a host:port and path and returns -a subset of the headers plus the body of the result.""" - -import httplib, sys, re - -try: - import msvcrt, os - msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) -except ImportError: - pass - -headers = [h.lower() for h in sys.argv[3:]] -conn = httplib.HTTPConnection(sys.argv[1]) -conn.request("GET", sys.argv[2]) -response = conn.getresponse() -print response.status, response.reason -for h in headers: - if response.getheader(h, None) is not None: - print "%s: %s" % (h, response.getheader(h)) -print -data = response.read() -sys.stdout.write(data) - -if 200 <= response.status <= 299: - sys.exit(0) -sys.exit(1) diff -r 639bb4617c70 -r a4ea5c2d3ff3 tests/run-tests.py --- a/tests/run-tests.py Sat Sep 10 13:23:41 2011 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1204 +0,0 @@ -#!/usr/bin/env python -# -# run-tests.py - Run a set of tests on Mercurial -# -# Copyright 2006 Matt Mackall -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -# Modifying this script is tricky because it has many modes: -# - serial (default) vs parallel (-jN, N > 1) -# - no coverage (default) vs coverage (-c, -C, -s) -# - temp install (default) vs specific hg script (--with-hg, --local) -# - tests are a mix of shell scripts and Python scripts -# -# If you change this script, it is recommended that you ensure you -# haven't broken it by running it in various modes with a representative -# sample of test scripts. For example: -# -# 1) serial, no coverage, temp install: -# ./run-tests.py test-s* -# 2) serial, no coverage, local hg: -# ./run-tests.py --local test-s* -# 3) serial, coverage, temp install: -# ./run-tests.py -c test-s* -# 4) serial, coverage, local hg: -# ./run-tests.py -c --local test-s* # unsupported -# 5) parallel, no coverage, temp install: -# ./run-tests.py -j2 test-s* -# 6) parallel, no coverage, local hg: -# ./run-tests.py -j2 --local test-s* -# 7) parallel, coverage, temp install: -# ./run-tests.py -j2 -c test-s* # currently broken -# 8) parallel, coverage, local install: -# ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) -# 9) parallel, custom tmp dir: -# ./run-tests.py -j2 --tmpdir /tmp/myhgtests -# -# (You could use any subset of the tests: test-s* happens to match -# enough that it's worth doing parallel runs, few enough that it -# completes fairly quickly, includes both shell and Python scripts, and -# includes some scripts that run daemon processes.) - -from distutils import version -import difflib -import errno -import optparse -import os -import shutil -import subprocess -import signal -import sys -import tempfile -import time -import re -import threading - -processlock = threading.Lock() - -closefds = os.name == 'posix' -def Popen4(cmd, wd, timeout): - processlock.acquire() - p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, - close_fds=closefds, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - processlock.release() - - p.fromchild = p.stdout - p.tochild = p.stdin - p.childerr = p.stderr - - p.timeout = False - if timeout: - def t(): - start = time.time() - while time.time() - start < timeout and p.returncode is None: - time.sleep(1) - p.timeout = True - if p.returncode is None: - terminate(p) - threading.Thread(target=t).start() - - return p - -# reserved exit code to skip test (used by hghave) -SKIPPED_STATUS = 80 -SKIPPED_PREFIX = 'skipped: ' -FAILED_PREFIX = 'hghave check failed: ' -PYTHON = sys.executable -IMPL_PATH = 'PYTHONPATH' -if 'java' in sys.platform: - IMPL_PATH = 'JYTHONPATH' - -requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"] - -defaults = { - 'jobs': ('HGTEST_JOBS', 1), - 'timeout': ('HGTEST_TIMEOUT', 180), - 'port': ('HGTEST_PORT', 20059), - 'shell': ('HGTEST_SHELL', '/bin/sh'), -} - -def parselistfiles(files, listtype, warn=True): - entries = dict() - for filename in files: - try: - path = os.path.expanduser(os.path.expandvars(filename)) - f = open(path, "r") - except IOError, err: - if err.errno != errno.ENOENT: - raise - if warn: - print "warning: no such %s file: %s" % (listtype, filename) - continue - - for line in f.readlines(): - line = line.split('#', 1)[0].strip() - if line: - entries[line] = filename - - f.close() - return entries - -def parseargs(): - parser = optparse.OptionParser("%prog [options] [tests]") - - # keep these sorted - parser.add_option("--blacklist", action="append", - help="skip tests listed in the specified blacklist file") - parser.add_option("--whitelist", action="append", - help="always run tests listed in the specified whitelist file") - parser.add_option("-C", "--annotate", action="store_true", - help="output files annotated with coverage") - parser.add_option("--child", type="int", - help="run as child process, summary to given fd") - parser.add_option("-c", "--cover", action="store_true", - help="print a test coverage report") - parser.add_option("-d", "--debug", action="store_true", - help="debug mode: write output of test scripts to console" - " rather than capturing and diff'ing it (disables timeout)") - parser.add_option("-f", "--first", action="store_true", - help="exit on the first test failure") - parser.add_option("--inotify", action="store_true", - help="enable inotify extension when running tests") - parser.add_option("-i", "--interactive", action="store_true", - help="prompt to accept changed output") - parser.add_option("-j", "--jobs", type="int", - help="number of jobs to run in parallel" - " (default: $%s or %d)" % defaults['jobs']) - parser.add_option("--keep-tmpdir", action="store_true", - help="keep temporary directory after running tests") - parser.add_option("-k", "--keywords", - help="run tests matching keywords") - parser.add_option("-l", "--local", action="store_true", - help="shortcut for --with-hg=/../hg") - parser.add_option("-n", "--nodiff", action="store_true", - help="skip showing test changes") - parser.add_option("-p", "--port", type="int", - help="port on which servers should listen" - " (default: $%s or %d)" % defaults['port']) - parser.add_option("--pure", action="store_true", - help="use pure Python code instead of C extensions") - parser.add_option("-R", "--restart", action="store_true", - help="restart at last error") - parser.add_option("-r", "--retest", action="store_true", - help="retest failed tests") - parser.add_option("-S", "--noskips", action="store_true", - help="don't report skip tests verbosely") - parser.add_option("--shell", type="string", - help="shell to use (default: $%s or %s)" % defaults['shell']) - parser.add_option("-t", "--timeout", type="int", - help="kill errant tests after TIMEOUT seconds" - " (default: $%s or %d)" % defaults['timeout']) - parser.add_option("--tmpdir", type="string", - help="run tests in the given temporary directory" - " (implies --keep-tmpdir)") - parser.add_option("-v", "--verbose", action="store_true", - help="output verbose messages") - parser.add_option("--view", type="string", - help="external diff viewer") - parser.add_option("--with-hg", type="string", - metavar="HG", - help="test using specified hg script rather than a " - "temporary installation") - parser.add_option("-3", "--py3k-warnings", action="store_true", - help="enable Py3k warnings on Python 2.6+") - parser.add_option('--extra-config-opt', action="append", - help='set the given config opt in the test hgrc') - - for option, (envvar, default) in defaults.items(): - defaults[option] = type(default)(os.environ.get(envvar, default)) - parser.set_defaults(**defaults) - (options, args) = parser.parse_args() - - # jython is always pure - if 'java' in sys.platform or '__pypy__' in sys.modules: - options.pure = True - - if options.with_hg: - if not (os.path.isfile(options.with_hg) and - os.access(options.with_hg, os.X_OK)): - parser.error('--with-hg must specify an executable hg script') - if not os.path.basename(options.with_hg) == 'hg': - sys.stderr.write('warning: --with-hg should specify an hg script\n') - if options.local: - testdir = os.path.dirname(os.path.realpath(sys.argv[0])) - hgbin = os.path.join(os.path.dirname(testdir), 'hg') - if not os.access(hgbin, os.X_OK): - parser.error('--local specified, but %r not found or not executable' - % hgbin) - options.with_hg = hgbin - - options.anycoverage = options.cover or options.annotate - if options.anycoverage: - try: - import coverage - covver = version.StrictVersion(coverage.__version__).version - if covver < (3, 3): - parser.error('coverage options require coverage 3.3 or later') - except ImportError: - parser.error('coverage options now require the coverage package') - - if options.anycoverage and options.local: - # this needs some path mangling somewhere, I guess - parser.error("sorry, coverage options do not work when --local " - "is specified") - - global vlog - if options.verbose: - if options.jobs > 1 or options.child is not None: - pid = "[%d]" % os.getpid() - else: - pid = None - def vlog(*msg): - iolock.acquire() - if pid: - print pid, - for m in msg: - print m, - print - sys.stdout.flush() - iolock.release() - else: - vlog = lambda *msg: None - - if options.tmpdir: - options.tmpdir = os.path.expanduser(options.tmpdir) - - if options.jobs < 1: - parser.error('--jobs must be positive') - if options.interactive and options.jobs > 1: - print '(--interactive overrides --jobs)' - options.jobs = 1 - if options.interactive and options.debug: - parser.error("-i/--interactive and -d/--debug are incompatible") - if options.debug: - if options.timeout != defaults['timeout']: - sys.stderr.write( - 'warning: --timeout option ignored with --debug\n') - options.timeout = 0 - if options.py3k_warnings: - if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): - parser.error('--py3k-warnings can only be used on Python 2.6+') - if options.blacklist: - options.blacklist = parselistfiles(options.blacklist, 'blacklist') - if options.whitelist: - options.whitelisted = parselistfiles(options.whitelist, 'whitelist', - warn=options.child is None) - else: - options.whitelisted = {} - - return (options, args) - -def rename(src, dst): - """Like os.rename(), trade atomicity and opened files friendliness - for existing destination support. - """ - shutil.copy(src, dst) - os.remove(src) - -def splitnewlines(text): - '''like str.splitlines, but only split on newlines. - keep line endings.''' - i = 0 - lines = [] - while True: - n = text.find('\n', i) - if n == -1: - last = text[i:] - if last: - lines.append(last) - return lines - lines.append(text[i:n + 1]) - i = n + 1 - -def parsehghaveoutput(lines): - '''Parse hghave log lines. - Return tuple of lists (missing, failed): - * the missing/unknown features - * the features for which existence check failed''' - missing = [] - failed = [] - for line in lines: - if line.startswith(SKIPPED_PREFIX): - line = line.splitlines()[0] - missing.append(line[len(SKIPPED_PREFIX):]) - elif line.startswith(FAILED_PREFIX): - line = line.splitlines()[0] - failed.append(line[len(FAILED_PREFIX):]) - - return missing, failed - -def showdiff(expected, output, ref, err): - print - for line in difflib.unified_diff(expected, output, ref, err): - sys.stdout.write(line) - -def findprogram(program): - """Search PATH for a executable program""" - for p in os.environ.get('PATH', os.defpath).split(os.pathsep): - name = os.path.join(p, program) - if os.name == 'nt' or os.access(name, os.X_OK): - return name - return None - -def checktools(): - # Before we go any further, check for pre-requisite tools - # stuff from coreutils (cat, rm, etc) are not tested - for p in requiredtools: - if os.name == 'nt': - p += '.exe' - found = findprogram(p) - if found: - vlog("# Found prerequisite", p, "at", found) - else: - print "WARNING: Did not find prerequisite tool: "+p - -def terminate(proc): - """Terminate subprocess (with fallback for Python versions < 2.6)""" - vlog('# Terminating process %d' % proc.pid) - try: - getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))() - except OSError: - pass - -def killdaemons(): - # Kill off any leftover daemon processes - try: - fp = open(DAEMON_PIDS) - for line in fp: - try: - pid = int(line) - except ValueError: - continue - try: - os.kill(pid, 0) - vlog('# Killing daemon process %d' % pid) - os.kill(pid, signal.SIGTERM) - time.sleep(0.25) - os.kill(pid, 0) - vlog('# Daemon process %d is stuck - really killing it' % pid) - os.kill(pid, signal.SIGKILL) - except OSError, err: - if err.errno != errno.ESRCH: - raise - fp.close() - os.unlink(DAEMON_PIDS) - except IOError: - pass - -def cleanup(options): - if not options.keep_tmpdir: - vlog("# Cleaning up HGTMP", HGTMP) - shutil.rmtree(HGTMP, True) - -def usecorrectpython(): - # some tests run python interpreter. they must use same - # interpreter we use or bad things will happen. - exedir, exename = os.path.split(sys.executable) - if exename in ('python', 'python.exe'): - path = findprogram(exename) - if os.path.dirname(path) == exedir: - return - else: - exename = 'python' - vlog('# Making python executable in test path use correct Python') - mypython = os.path.join(BINDIR, exename) - try: - os.symlink(sys.executable, mypython) - except AttributeError: - # windows fallback - shutil.copyfile(sys.executable, mypython) - shutil.copymode(sys.executable, mypython) - -def installhg(options): - vlog("# Performing temporary installation of HG") - installerrs = os.path.join("tests", "install.err") - pure = options.pure and "--pure" or "" - - # Run installer in hg root - script = os.path.realpath(sys.argv[0]) - hgroot = os.path.dirname(os.path.dirname(script)) - os.chdir(hgroot) - nohome = '--home=""' - if os.name == 'nt': - # The --home="" trick works only on OS where os.sep == '/' - # because of a distutils convert_path() fast-path. Avoid it at - # least on Windows for now, deal with .pydistutils.cfg bugs - # when they happen. - nohome = '' - cmd = ('%s setup.py %s clean --all' - ' build --build-base="%s"' - ' install --force --prefix="%s" --install-lib="%s"' - ' --install-scripts="%s" %s >%s 2>&1' - % (sys.executable, pure, os.path.join(HGTMP, "build"), - INST, PYTHONDIR, BINDIR, nohome, installerrs)) - vlog("# Running", cmd) - if os.system(cmd) == 0: - if not options.verbose: - os.remove(installerrs) - else: - f = open(installerrs) - for line in f: - print line, - f.close() - sys.exit(1) - os.chdir(TESTDIR) - - usecorrectpython() - - vlog("# Installing dummy diffstat") - f = open(os.path.join(BINDIR, 'diffstat'), 'w') - f.write('#!' + sys.executable + '\n' - 'import sys\n' - 'files = 0\n' - 'for line in sys.stdin:\n' - ' if line.startswith("diff "):\n' - ' files += 1\n' - 'sys.stdout.write("files patched: %d\\n" % files)\n') - f.close() - os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) - - if options.py3k_warnings and not options.anycoverage: - vlog("# Updating hg command to enable Py3k Warnings switch") - f = open(os.path.join(BINDIR, 'hg'), 'r') - lines = [line.rstrip() for line in f] - lines[0] += ' -3' - f.close() - f = open(os.path.join(BINDIR, 'hg'), 'w') - for line in lines: - f.write(line + '\n') - f.close() - - hgbat = os.path.join(BINDIR, 'hg.bat') - if os.path.isfile(hgbat): - # hg.bat expects to be put in bin/scripts while run-tests.py - # installation layout put it in bin/ directly. Fix it - f = open(hgbat, 'rb') - data = f.read() - f.close() - if '"%~dp0..\python" "%~dp0hg" %*' in data: - data = data.replace('"%~dp0..\python" "%~dp0hg" %*', - '"%~dp0python" "%~dp0hg" %*') - f = open(hgbat, 'wb') - f.write(data) - f.close() - else: - print 'WARNING: cannot fix hg.bat reference to python.exe' - - if options.anycoverage: - custom = os.path.join(TESTDIR, 'sitecustomize.py') - target = os.path.join(PYTHONDIR, 'sitecustomize.py') - vlog('# Installing coverage trigger to %s' % target) - shutil.copyfile(custom, target) - rc = os.path.join(TESTDIR, '.coveragerc') - vlog('# Installing coverage rc to %s' % rc) - os.environ['COVERAGE_PROCESS_START'] = rc - fn = os.path.join(INST, '..', '.coverage') - os.environ['COVERAGE_FILE'] = fn - -def outputcoverage(options): - - vlog('# Producing coverage report') - os.chdir(PYTHONDIR) - - def covrun(*args): - cmd = 'coverage %s' % ' '.join(args) - vlog('# Running: %s' % cmd) - os.system(cmd) - - if options.child: - return - - covrun('-c') - omit = ','.join([BINDIR, TESTDIR]) - covrun('-i', '-r', '"--omit=%s"' % omit) # report - if options.annotate: - adir = os.path.join(TESTDIR, 'annotated') - if not os.path.isdir(adir): - os.mkdir(adir) - covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit) - -def pytest(test, wd, options, replacements): - py3kswitch = options.py3k_warnings and ' -3' or '' - cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test) - vlog("# Running", cmd) - return run(cmd, wd, options, replacements) - -def shtest(test, wd, options, replacements): - cmd = '"%s"' % test - vlog("# Running", cmd) - return run(cmd, wd, options, replacements) - -needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search -escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub -escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256)) -escapemap.update({'\\': '\\\\', '\r': r'\r'}) -def escapef(m): - return escapemap[m.group(0)] -def stringescape(s): - return escapesub(escapef, s) - -def tsttest(test, wd, options, replacements): - t = open(test) - out = [] - script = [] - salt = "SALT" + str(time.time()) - - pos = prepos = -1 - after = {} - expected = {} - for n, l in enumerate(t): - if not l.endswith('\n'): - l += '\n' - if l.startswith(' $ '): # commands - after.setdefault(pos, []).append(l) - prepos = pos - pos = n - script.append('echo %s %s $?\n' % (salt, n)) - script.append(l[4:]) - elif l.startswith(' > '): # continuations - after.setdefault(prepos, []).append(l) - script.append(l[4:]) - elif l.startswith(' '): # results - # queue up a list of expected results - expected.setdefault(pos, []).append(l[2:]) - else: - # non-command/result - queue up for merged output - after.setdefault(pos, []).append(l) - - t.close() - - script.append('echo %s %s $?\n' % (salt, n + 1)) - - fd, name = tempfile.mkstemp(suffix='hg-tst') - - try: - for l in script: - os.write(fd, l) - os.close(fd) - - cmd = '"%s" "%s"' % (options.shell, name) - vlog("# Running", cmd) - exitcode, output = run(cmd, wd, options, replacements) - # do not merge output if skipped, return hghave message instead - # similarly, with --debug, output is None - if exitcode == SKIPPED_STATUS or output is None: - return exitcode, output - finally: - os.remove(name) - - def rematch(el, l): - try: - # ensure that the regex matches to the end of the string - return re.match(el + r'\Z', l) - except re.error: - # el is an invalid regex - return False - - def globmatch(el, l): - # The only supported special characters are * and ?. Escaping is - # supported. - i, n = 0, len(el) - res = '' - while i < n: - c = el[i] - i += 1 - if c == '\\' and el[i] in '*?\\': - res += el[i - 1:i + 1] - i += 1 - elif c == '*': - res += '.*' - elif c == '?': - res += '.' - else: - res += re.escape(c) - return rematch(res, l) - - pos = -1 - postout = [] - ret = 0 - for n, l in enumerate(output): - lout, lcmd = l, None - if salt in l: - lout, lcmd = l.split(salt, 1) - - if lout: - if lcmd: - lout += ' (no-eol)\n' - - el = None - if pos in expected and expected[pos]: - el = expected[pos].pop(0) - - if el == lout: # perfect match (fast) - postout.append(" " + lout) - elif (el and - (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or - el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout) - or el.endswith(" (esc)\n") and - el.decode('string-escape') == l)): - postout.append(" " + el) # fallback regex/glob/esc match - else: - if needescape(lout): - lout = stringescape(lout.rstrip('\n')) + " (esc)\n" - postout.append(" " + lout) # let diff deal with it - - if lcmd: - # add on last return code - ret = int(lcmd.split()[1]) - if ret != 0: - postout.append(" [%s]\n" % ret) - if pos in after: - postout += after.pop(pos) - pos = int(lcmd.split()[0]) - - if pos in after: - postout += after.pop(pos) - - return exitcode, postout - -wifexited = getattr(os, "WIFEXITED", lambda x: False) -def run(cmd, wd, options, replacements): - """Run command in a sub-process, capturing the output (stdout and stderr). - Return a tuple (exitcode, output). output is None in debug mode.""" - # TODO: Use subprocess.Popen if we're running on Python 2.4 - if options.debug: - proc = subprocess.Popen(cmd, shell=True, cwd=wd) - ret = proc.wait() - return (ret, None) - - proc = Popen4(cmd, wd, options.timeout) - def cleanup(): - terminate(proc) - ret = proc.wait() - if ret == 0: - ret = signal.SIGTERM << 8 - killdaemons() - return ret - - output = '' - proc.tochild.close() - - try: - output = proc.fromchild.read() - except KeyboardInterrupt: - vlog('# Handling keyboard interrupt') - cleanup() - raise - - ret = proc.wait() - if wifexited(ret): - ret = os.WEXITSTATUS(ret) - - if proc.timeout: - ret = 'timeout' - - if ret: - killdaemons() - - for s, r in replacements: - output = re.sub(s, r, output) - return ret, splitnewlines(output) - -def runone(options, test): - '''tristate output: - None -> skipped - True -> passed - False -> failed''' - - global results, resultslock, iolock - - testpath = os.path.join(TESTDIR, test) - - def result(l, e): - resultslock.acquire() - results[l].append(e) - resultslock.release() - - def skip(msg): - if not options.verbose: - result('s', (test, msg)) - else: - iolock.acquire() - print "\nSkipping %s: %s" % (testpath, msg) - iolock.release() - return None - - def fail(msg, ret): - if not options.nodiff: - iolock.acquire() - print "\nERROR: %s %s" % (testpath, msg) - iolock.release() - if (not ret and options.interactive - and os.path.exists(testpath + ".err")): - iolock.acquire() - print "Accept this change? [n] ", - answer = sys.stdin.readline().strip() - iolock.release() - if answer.lower() in "y yes".split(): - if test.endswith(".t"): - rename(testpath + ".err", testpath) - else: - rename(testpath + ".err", testpath + ".out") - result('p', test) - return - result('f', (test, msg)) - - def success(): - result('p', test) - - def ignore(msg): - result('i', (test, msg)) - - if (os.path.basename(test).startswith("test-") and '~' not in test and - ('.' not in test or test.endswith('.py') or - test.endswith('.bat') or test.endswith('.t'))): - if not os.path.exists(test): - skip("doesn't exist") - return None - else: - vlog('# Test file', test, 'not supported, ignoring') - return None # not a supported test, don't record - - if not (options.whitelisted and test in options.whitelisted): - if options.blacklist and test in options.blacklist: - skip("blacklisted") - return None - - if options.retest and not os.path.exists(test + ".err"): - ignore("not retesting") - return None - - if options.keywords: - fp = open(test) - t = fp.read().lower() + test.lower() - fp.close() - for k in options.keywords.lower().split(): - if k in t: - break - else: - ignore("doesn't match keyword") - return None - - vlog("# Test", test) - - # create a fresh hgrc - hgrc = open(HGRCPATH, 'w+') - hgrc.write('[ui]\n') - hgrc.write('slash = True\n') - hgrc.write('[defaults]\n') - hgrc.write('backout = -d "0 0"\n') - hgrc.write('commit = -d "0 0"\n') - hgrc.write('tag = -d "0 0"\n') - if options.inotify: - hgrc.write('[extensions]\n') - hgrc.write('inotify=\n') - hgrc.write('[inotify]\n') - hgrc.write('pidfile=%s\n' % DAEMON_PIDS) - hgrc.write('appendpid=True\n') - if options.extra_config_opt: - for opt in options.extra_config_opt: - section, key = opt.split('.', 1) - assert '=' in key, ('extra config opt %s must ' - 'have an = for assignment' % opt) - hgrc.write('[%s]\n%s\n' % (section, key)) - hgrc.close() - - ref = os.path.join(TESTDIR, test+".out") - err = os.path.join(TESTDIR, test+".err") - if os.path.exists(err): - os.remove(err) # Remove any previous output files - try: - tf = open(testpath) - firstline = tf.readline().rstrip() - tf.close() - except: - firstline = '' - lctest = test.lower() - - if lctest.endswith('.py') or firstline == '#!/usr/bin/env python': - runner = pytest - elif lctest.endswith('.t'): - runner = tsttest - ref = testpath - else: - # do not try to run non-executable programs - if not os.access(testpath, os.X_OK): - return skip("not executable") - runner = shtest - - # Make a tmp subdirectory to work in - testtmp = os.environ["TESTTMP"] = os.environ["HOME"] = \ - os.path.join(HGTMP, os.path.basename(test)) - - os.mkdir(testtmp) - ret, out = runner(testpath, testtmp, options, [ - (re.escape(testtmp), '$TESTTMP'), - (r':%s\b' % options.port, ':$HGPORT'), - (r':%s\b' % (options.port + 1), ':$HGPORT1'), - (r':%s\b' % (options.port + 2), ':$HGPORT2'), - ]) - vlog("# Ret was:", ret) - - mark = '.' - - skipped = (ret == SKIPPED_STATUS) - - # If we're not in --debug mode and reference output file exists, - # check test output against it. - if options.debug: - refout = None # to match "out is None" - elif os.path.exists(ref): - f = open(ref, "r") - refout = splitnewlines(f.read()) - f.close() - else: - refout = [] - - if (ret != 0 or out != refout) and not skipped and not options.debug: - # Save errors to a file for diagnosis - f = open(err, "wb") - for line in out: - f.write(line) - f.close() - - if skipped: - mark = 's' - if out is None: # debug mode: nothing to parse - missing = ['unknown'] - failed = None - else: - missing, failed = parsehghaveoutput(out) - if not missing: - missing = ['irrelevant'] - if failed: - fail("hghave failed checking for %s" % failed[-1], ret) - skipped = False - else: - skip(missing[-1]) - elif ret == 'timeout': - mark = 't' - fail("timed out", ret) - elif out != refout: - mark = '!' - if not options.nodiff: - iolock.acquire() - if options.view: - os.system("%s %s %s" % (options.view, ref, err)) - else: - showdiff(refout, out, ref, err) - iolock.release() - if ret: - fail("output changed and returned error code %d" % ret, ret) - else: - fail("output changed", ret) - ret = 1 - elif ret: - mark = '!' - fail("returned error code %d" % ret, ret) - else: - success() - - if not options.verbose: - iolock.acquire() - sys.stdout.write(mark) - sys.stdout.flush() - iolock.release() - - killdaemons() - - if not options.keep_tmpdir: - shutil.rmtree(testtmp, True) - if skipped: - return None - return ret == 0 - -_hgpath = None - -def _gethgpath(): - """Return the path to the mercurial package that is actually found by - the current Python interpreter.""" - global _hgpath - if _hgpath is not None: - return _hgpath - - cmd = '%s -c "import mercurial; print mercurial.__path__[0]"' - pipe = os.popen(cmd % PYTHON) - try: - _hgpath = pipe.read().strip() - finally: - pipe.close() - return _hgpath - -def _checkhglib(verb): - """Ensure that the 'mercurial' package imported by python is - the one we expect it to be. If not, print a warning to stderr.""" - expecthg = os.path.join(PYTHONDIR, 'mercurial') - actualhg = _gethgpath() - if os.path.abspath(actualhg) != os.path.abspath(expecthg): - sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n' - ' (expected %s)\n' - % (verb, actualhg, expecthg)) - -def runchildren(options, tests): - if INST: - installhg(options) - _checkhglib("Testing") - - optcopy = dict(options.__dict__) - optcopy['jobs'] = 1 - - # Because whitelist has to override keyword matches, we have to - # actually load the whitelist in the children as well, so we allow - # the list of whitelist files to pass through and be parsed in the - # children, but not the dict of whitelisted tests resulting from - # the parse, used here to override blacklisted tests. - whitelist = optcopy['whitelisted'] or [] - del optcopy['whitelisted'] - - blacklist = optcopy['blacklist'] or [] - del optcopy['blacklist'] - blacklisted = [] - - if optcopy['with_hg'] is None: - optcopy['with_hg'] = os.path.join(BINDIR, "hg") - optcopy.pop('anycoverage', None) - - opts = [] - for opt, value in optcopy.iteritems(): - name = '--' + opt.replace('_', '-') - if value is True: - opts.append(name) - elif isinstance(value, list): - for v in value: - opts.append(name + '=' + str(v)) - elif value is not None: - opts.append(name + '=' + str(value)) - - tests.reverse() - jobs = [[] for j in xrange(options.jobs)] - while tests: - for job in jobs: - if not tests: - break - test = tests.pop() - if test not in whitelist and test in blacklist: - blacklisted.append(test) - else: - job.append(test) - fps = {} - - for j, job in enumerate(jobs): - if not job: - continue - rfd, wfd = os.pipe() - childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] - childtmp = os.path.join(HGTMP, 'child%d' % j) - childopts += ['--tmpdir', childtmp] - cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job - vlog(' '.join(cmdline)) - fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') - os.close(wfd) - signal.signal(signal.SIGINT, signal.SIG_IGN) - failures = 0 - tested, skipped, failed = 0, 0, 0 - skips = [] - fails = [] - while fps: - pid, status = os.wait() - fp = fps.pop(pid) - l = fp.read().splitlines() - try: - test, skip, fail = map(int, l[:3]) - except ValueError: - test, skip, fail = 0, 0, 0 - split = -fail or len(l) - for s in l[3:split]: - skips.append(s.split(" ", 1)) - for s in l[split:]: - fails.append(s.split(" ", 1)) - tested += test - skipped += skip - failed += fail - vlog('pid %d exited, status %d' % (pid, status)) - failures |= status - print - skipped += len(blacklisted) - if not options.noskips: - for s in skips: - print "Skipped %s: %s" % (s[0], s[1]) - for s in blacklisted: - print "Skipped %s: blacklisted" % s - for s in fails: - print "Failed %s: %s" % (s[0], s[1]) - - _checkhglib("Tested") - print "# Ran %d tests, %d skipped, %d failed." % ( - tested, skipped, failed) - - if options.anycoverage: - outputcoverage(options) - sys.exit(failures != 0) - -results = dict(p=[], f=[], s=[], i=[]) -resultslock = threading.Lock() -iolock = threading.Lock() - -def runqueue(options, tests, results): - for test in tests: - ret = runone(options, test) - if options.first and ret is not None and not ret: - break - -def runtests(options, tests): - global DAEMON_PIDS, HGRCPATH - DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') - HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') - - try: - if INST: - installhg(options) - _checkhglib("Testing") - - if options.restart: - orig = list(tests) - while tests: - if os.path.exists(tests[0] + ".err"): - break - tests.pop(0) - if not tests: - print "running all tests" - tests = orig - - runqueue(options, tests, results) - - failed = len(results['f']) - tested = len(results['p']) + failed - skipped = len(results['s']) - ignored = len(results['i']) - - if options.child: - fp = os.fdopen(options.child, 'w') - fp.write('%d\n%d\n%d\n' % (tested, skipped, failed)) - for s in results['s']: - fp.write("%s %s\n" % s) - for s in results['f']: - fp.write("%s %s\n" % s) - fp.close() - else: - print - for s in results['s']: - print "Skipped %s: %s" % s - for s in results['f']: - print "Failed %s: %s" % s - _checkhglib("Tested") - print "# Ran %d tests, %d skipped, %d failed." % ( - tested, skipped + ignored, failed) - - if options.anycoverage: - outputcoverage(options) - except KeyboardInterrupt: - failed = True - print "\ninterrupted!" - - if failed: - sys.exit(1) - -def main(): - (options, args) = parseargs() - if not options.child: - os.umask(022) - - checktools() - - if len(args) == 0: - args = os.listdir(".") - args.sort() - - tests = args - - # Reset some environment variables to well-known values so that - # the tests produce repeatable output. - os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C' - os.environ['TZ'] = 'GMT' - os.environ["EMAIL"] = "Foo Bar " - os.environ['CDPATH'] = '' - os.environ['COLUMNS'] = '80' - os.environ['GREP_OPTIONS'] = '' - os.environ['http_proxy'] = '' - - # unset env related to hooks - for k in os.environ.keys(): - if k.startswith('HG_'): - # can't remove on solaris - os.environ[k] = '' - del os.environ[k] - - global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE - TESTDIR = os.environ["TESTDIR"] = os.getcwd() - if options.tmpdir: - options.keep_tmpdir = True - tmpdir = options.tmpdir - if os.path.exists(tmpdir): - # Meaning of tmpdir has changed since 1.3: we used to create - # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if - # tmpdir already exists. - sys.exit("error: temp dir %r already exists" % tmpdir) - - # Automatically removing tmpdir sounds convenient, but could - # really annoy anyone in the habit of using "--tmpdir=/tmp" - # or "--tmpdir=$HOME". - #vlog("# Removing temp dir", tmpdir) - #shutil.rmtree(tmpdir) - os.makedirs(tmpdir) - else: - tmpdir = tempfile.mkdtemp('', 'hgtests.') - HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir) - DAEMON_PIDS = None - HGRCPATH = None - - os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' - os.environ["HGMERGE"] = "internal:merge" - os.environ["HGUSER"] = "test" - os.environ["HGENCODING"] = "ascii" - os.environ["HGENCODINGMODE"] = "strict" - os.environ["HGPORT"] = str(options.port) - os.environ["HGPORT1"] = str(options.port + 1) - os.environ["HGPORT2"] = str(options.port + 2) - - if options.with_hg: - INST = None - BINDIR = os.path.dirname(os.path.realpath(options.with_hg)) - - # This looks redundant with how Python initializes sys.path from - # the location of the script being executed. Needed because the - # "hg" specified by --with-hg is not the only Python script - # executed in the test suite that needs to import 'mercurial' - # ... which means it's not really redundant at all. - PYTHONDIR = BINDIR - else: - INST = os.path.join(HGTMP, "install") - BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin") - PYTHONDIR = os.path.join(INST, "lib", "python") - - os.environ["BINDIR"] = BINDIR - os.environ["PYTHON"] = PYTHON - - if not options.child: - path = [BINDIR] + os.environ["PATH"].split(os.pathsep) - os.environ["PATH"] = os.pathsep.join(path) - - # Include TESTDIR in PYTHONPATH so that out-of-tree extensions - # can run .../tests/run-tests.py test-foo where test-foo - # adds an extension to HGRC - pypath = [PYTHONDIR, TESTDIR] - # We have to augment PYTHONPATH, rather than simply replacing - # it, in case external libraries are only available via current - # PYTHONPATH. (In particular, the Subversion bindings on OS X - # are in /opt/subversion.) - oldpypath = os.environ.get(IMPL_PATH) - if oldpypath: - pypath.append(oldpypath) - os.environ[IMPL_PATH] = os.pathsep.join(pypath) - - COVERAGE_FILE = os.path.join(TESTDIR, ".coverage") - - vlog("# Using TESTDIR", TESTDIR) - vlog("# Using HGTMP", HGTMP) - vlog("# Using PATH", os.environ["PATH"]) - vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH]) - - try: - if len(tests) > 1 and options.jobs > 1: - runchildren(options, tests) - else: - runtests(options, tests) - finally: - time.sleep(1) - cleanup(options) - -if __name__ == '__main__': - main() diff -r 639bb4617c70 -r a4ea5c2d3ff3 tests/test-keyword --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-keyword Wed Sep 14 16:19:33 2011 +0100 @@ -0,0 +1,334 @@ +#!/bin/sh + +cat <> $HGRCPATH +[extensions] +keyword = +mq = +notify = +EOF + +# demo before [keyword] files are set up +# would succeed without uisetup otherwise +echo % hg kwdemo +hg --quiet kwdemo \ +| sed -e 's![^ ][^ ]*demo.txt,v!/TMP/demo.txt,v!' \ + -e 's/,v [a-z0-9][a-z0-9]* /,v xxxxxxxxxxxx /' \ + -e '/[$]Revision/ s/: [a-z0-9][a-z0-9]* /: xxxxxxxxxxxx /' \ + -e 's! 20[0-9][0-9]/[01][0-9]/[0-3][0-9] [0-2][0-9]:[0-6][0-9]:[0-6][0-9]! 2000/00/00 00:00:00!' + +hg --quiet kwdemo "Branch = {branches}" + +cat <> $HGRCPATH +[keyword] +* = +b = ignore +[hooks] +commit= +commit.test=cp a hooktest +EOF + +hg init Test-bndl +cd Test-bndl + +echo % kwshrink should exit silently in empty/invalid repo +hg kwshrink + +# Symlinks cannot be created on Windows. The bundle was made with: +# +# hg init t +# cd t +# echo a > a +# ln -s a sym +# hg add sym +# hg ci -m addsym -u mercurial +# hg bundle --base null ../test-keyword.hg +# +hg pull -u "$TESTDIR/test-keyword.hg" \ + | sed 's/pulling from.*test-keyword.hg/pulling from test-keyword.hg/' + +echo 'expand $Id$' > a +echo 'do not process $Id:' >> a +echo 'xxx $' >> a +echo 'ignore $Id$' > b +echo % cat +cat a b + +echo % addremove +hg addremove +echo % status +hg status + +echo % default keyword expansion including commit hook +#echo % interrupted commit should not change state or run commit hook +#hg --debug commit +#echo % status +#hg status + +echo % commit +hg --debug commit -mab -d '0 0' -u 'User Name ' \ + | grep -v '^committed changeset' +echo % status +hg status +echo % identify +hg debugrebuildstate +hg --quiet identify +echo % cat +cat a b +echo % hg cat +hg cat a b + +echo +echo % diff a hooktest +diff a hooktest + +echo % removing commit hook from config +sed -e '/\[hooks\]/,$ d' "$HGRCPATH" > $HGRCPATH.nohook +mv "$HGRCPATH".nohook "$HGRCPATH" +rm hooktest + +echo % bundle +hg --quiet bundle --base null ../kw.hg + +cd .. +hg init Test +cd Test + +echo % pull from bundle +hg pull -u ../kw.hg + +echo % touch +touch a b +echo % status +hg status + +rm a b +echo % update +hg update +echo % cat +cat a b + +echo % check whether expansion is filewise and filemode is preserved +echo '$Id$' > c +echo 'tests for different changenodes' >> c +chmod 600 c +ls -l c | cut -b 1-10 +echo % commit c +hg commit -A -mcndiff -d '1 0' -u 'User Name ' +ls -l c | cut -b 1-10 +echo % force expansion +hg -v kwexpand +echo % compare changenodes in a c +cat a c + +echo % qinit -c +hg qinit -c +echo % qimport +hg qimport -r tip -n mqtest.diff +echo % qcommit +hg qcommit -mqtest +echo % keywords should not be expanded in patch +cat .hg/patches/mqtest.diff +echo % qpop +hg qpop +echo % qpush +hg qpush +echo % cat +cat c +echo % qpop and move on +hg qpop + +echo % copy +hg cp a c + +echo % kwfiles added +hg kwfiles + +echo % commit +hg --debug commit -ma2c -d '1 0' -u 'User Name ' \ + | grep -v '^committed changeset' +echo % cat a c +cat a c +echo % touch copied c after 1 second +sleep 1 +touch c +echo % status +hg status + +echo % kwfiles +hg kwfiles + +echo % diff --rev +hg diff --rev 1 | grep -v 'b/c' + +echo % rollback +hg rollback +echo % status +hg status +echo % update -C +hg update --clean + +echo % custom keyword expansion +echo % try with kwdemo +hg --quiet kwdemo "Xinfo = {author}: {desc}" + +cat <>$HGRCPATH +[keywordmaps] +Id = {file} {node|short} {date|rfc822date} {author|user} +Xinfo = {author}: {desc} +EOF + +echo % cat +cat a b +echo % hg cat +hg cat a b + +echo '$Xinfo$' >> a +cat <> log +firstline +secondline +EOF + +#echo % interrupted commit should not change state +#hg commit +#echo % status +#hg status + +echo % commit +hg --debug commit -l log -d '2 0' -u 'User Name ' \ + | grep -v '^committed changeset' +rm log +echo % status +hg status +echo % verify +hg verify + +echo % cat +cat a b +echo % hg cat +hg cat sym a b +echo +echo % annotate +hg annotate a + +echo % remove +hg debugrebuildstate +hg remove a +hg --debug commit -m rma | grep -v '^committed changeset' +echo % status +hg status +echo % rollback +hg rollback +echo % status +hg status +echo % revert a +hg revert --no-backup --rev tip a +echo % cat a +cat a + +echo % clone to test incoming +cd .. +# remove updating status for backwards compatibility +hg clone -r1 Test Test-a | grep -v 'working directory' +cd Test-a +cat <> .hg/hgrc +[paths] +default = ../Test +EOF +echo % incoming +# remove path to temp dir for backwards compatibility +hg incoming | grep -v '^comparing with' + +#sed -e 's/Id.*/& rejecttest/' a > a.new +#mv a.new a +#echo % commit rejecttest +#hg --debug commit -m'rejects?' -d '3 0' -u 'User Name ' +#echo % export +#hg export -o ../rejecttest.diff tip + +cd ../Test +#echo % import +#hg import ../rejecttest.diff +#echo % cat +#cat a b +#echo % rollback +#hg rollback +#echo % clean update +#hg update --clean + +echo % kwexpand/kwshrink on selected files +mkdir x +echo % copy a x/a +hg copy a x/a +echo % kwexpand a +hg --verbose kwexpand a +echo % kwexpand x/a should abort +hg --verbose kwexpand x/a +cd x +hg --debug commit -m xa -d '3 0' -u 'User Name ' \ + | grep -v '^committed changeset' +echo % cat a +cat a +echo % kwshrink a inside directory x +hg --verbose kwshrink a +echo % cat a +cat a +cd .. + +echo % kwexpand nonexistent +hg kwexpand nonexistent 2>&1 | sed 's/nonexistent:.*/nonexistent:/' + +#echo % hg serve +#hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log +#cat hg.pid >> $DAEMON_PIDS +#echo % expansion +#echo % hgweb file +#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/a/?style=raw') +#echo % no expansion +#echo % hgweb annotate +#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/annotate/tip/a/?style=raw') +#echo % hgweb changeset +#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/rev/tip/?style=raw') +#echo % hgweb filediff +#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/diff/bb948857c743/a?style=raw') +#echo % errors encountered +#cat errors.log + +#echo % merge/resolve +#echo '$Id$' > m +#hg add m +#hg commit -m 4kw +#echo foo >> m +#hg commit -m 5foo +#echo % simplemerge +#hg update 4 +#echo foo >> m +#hg commit -m 6foo +#hg merge +#hg commit -m simplemerge +#cat m +#echo % conflict +#hg update 4 +#echo bar >> m +#hg commit -m 8bar +#hg merge +#echo % keyword stays outside conflict zone +#cat m +#echo % resolve to local +#HGMERGE=internal:local hg resolve +#hg commit -m localresolve +#cat m + +echo % switch off expansion +echo % kwshrink with unknown file u +cp a u +hg --verbose kwshrink +echo % cat +cat a b +echo % hg cat +hg cat a b + +rm "$HGRCPATH" +echo % cat +cat a b +echo % hg cat +hg cat a b diff -r 639bb4617c70 -r a4ea5c2d3ff3 tests/test-keyword.hg Binary file tests/test-keyword.hg has changed diff -r 639bb4617c70 -r a4ea5c2d3ff3 tests/test-keyword.out --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-keyword.out Wed Sep 14 16:19:33 2011 +0100 @@ -0,0 +1,290 @@ +% hg kwdemo +[extensions] +keyword = +[keyword] +demo.txt = +[keywordmaps] +Author = {author|user} +Date = {date|utcdate} +Header = {root}/{file},v {node|short} {date|utcdate} {author|user} +Id = {file|basename},v {node|short} {date|utcdate} {author|user} +RCSFile = {file|basename},v +RCSfile = {file|basename},v +Revision = {node|short} +Source = {root}/{file},v +$Author: test $ +$Date: 2000/00/00 00:00:00 $ +$Header: /TMP/demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $ +$Id: demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $ +$RCSFile: demo.txt,v $ +$RCSfile: demo.txt,v $ +$Revision: xxxxxxxxxxxx $ +$Source: /TMP/demo.txt,v $ +[extensions] +keyword = +[keyword] +demo.txt = +[keywordmaps] +Branch = {branches} +$Branch: demobranch $ +% kwshrink should exit silently in empty/invalid repo +pulling from test-keyword.hg +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +% cat +expand $Id$ +do not process $Id: +xxx $ +ignore $Id$ +% addremove +adding a +adding b +% status +A a +A b +% default keyword expansion including commit hook +% commit +a +b +overwriting a expanding keywords +running hook commit.test: cp a hooktest +% status +? hooktest +% identify +475643d17242 +% cat +expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $ +do not process $Id: +xxx $ +ignore $Id$ +% hg cat +expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $ +do not process $Id: +xxx $ +ignore $Id$ + +% diff a hooktest +% removing commit hook from config +% bundle +% pull from bundle +pulling from ../kw.hg +requesting all changes +adding changesets +adding manifests +adding file changes +added 2 changesets with 3 changes to 3 files +3 files updated, 0 files merged, 0 files removed, 0 files unresolved +% touch +% status +% update +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +% cat +expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $ +do not process $Id: +xxx $ +ignore $Id$ +% check whether expansion is filewise and filemode is preserved +-rw------- +% commit c +adding c +-rw------- +% force expansion +overwriting a expanding keywords +overwriting c expanding keywords +% compare changenodes in a c +expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $ +do not process $Id: +xxx $ +$Id: c,v c08ff8b9d54e 1970/01/01 00:00:01 user $ +tests for different changenodes +% qinit -c +% qimport +% qcommit +% keywords should not be expanded in patch +# HG changeset patch +# User User Name +# Date 1 0 +# Node ID c08ff8b9d54ebf87b428631bd34b3d8b69cbf72f +# Parent 475643d17242703d20c30af0fc52b30fd231c6f6 +cndiff + +diff -r 475643d17242 -r c08ff8b9d54e c +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 ++++ b/c Thu Jan 01 00:00:01 1970 +0000 +@@ -0,0 +1,2 @@ ++$Id$ ++tests for different changenodes +% qpop +Patch queue now empty +% qpush +applying mqtest.diff +Now at: mqtest.diff +% cat +$Id: c,v c08ff8b9d54e 1970/01/01 00:00:01 user $ +tests for different changenodes +% qpop and move on +Patch queue now empty +% copy +% kwfiles added +a +c +% commit +c + c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292 +overwriting c expanding keywords +% cat a c +expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $ +do not process $Id: +xxx $ +expand $Id: c,v 1acf8f3ab611 1970/01/01 00:00:01 user $ +do not process $Id: +xxx $ +% touch copied c after 1 second +% status +% kwfiles +a +c +% diff --rev +diff -r 475643d17242 c +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +@@ -0,0 +1,3 @@ ++expand $Id$ ++do not process $Id: ++xxx $ +% rollback +rolling back last transaction +% status +A c +% update -C +0 files updated, 0 files merged, 1 files removed, 0 files unresolved +% custom keyword expansion +% try with kwdemo +[extensions] +keyword = +[keyword] +* = +b = ignore +demo.txt = +[keywordmaps] +Xinfo = {author}: {desc} +$Xinfo: test: hg keyword configuration and expansion example $ +% cat +expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $ +do not process $Id: +xxx $ +ignore $Id$ +% hg cat +expand $Id: a 475643d17242 Thu, 01 Jan 1970 00:00:00 +0000 user $ +do not process $Id: +xxx $ +ignore $Id$ +% commit +a +overwriting a expanding keywords +% status +% verify +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +3 files, 3 changesets, 4 total revisions +% cat +expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $ +do not process $Id: +xxx $ +$Xinfo: User Name : firstline $ +ignore $Id$ +% hg cat +expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $ +do not process $Id: +xxx $ +$Xinfo: User Name : firstline $ +ignore $Id$ +a +% annotate +1: expand $Id$ +1: do not process $Id: +1: xxx $ +2: $Xinfo$ +% remove +% status +% rollback +rolling back last transaction +% status +R a +% revert a +% cat a +expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $ +do not process $Id: +xxx $ +$Xinfo: User Name : firstline $ +% clone to test incoming +requesting all changes +adding changesets +adding manifests +adding file changes +added 2 changesets with 3 changes to 3 files +3 files updated, 0 files merged, 0 files removed, 0 files unresolved +% incoming +searching for changes +changeset: 2:9ea11ce3ca13 +tag: tip +user: User Name +date: Thu Jan 01 00:00:02 1970 +0000 +summary: firstline + +% kwexpand/kwshrink on selected files +% copy a x/a +% kwexpand a +overwriting a expanding keywords +% kwexpand x/a should abort +abort: outstanding uncommitted changes +x/a + x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e +overwriting x/a expanding keywords +% cat a +expand $Id: x/a 93c8b61c99cd Thu, 01 Jan 1970 00:00:03 +0000 user $ +do not process $Id: +xxx $ +$Xinfo: User Name : xa $ +% kwshrink a inside directory x +overwriting x/a shrinking keywords +% cat a +expand $Id$ +do not process $Id: +xxx $ +$Xinfo$ +% kwexpand nonexistent +nonexistent: +% switch off expansion +% kwshrink with unknown file u +overwriting a shrinking keywords +overwriting x/a shrinking keywords +% cat +expand $Id$ +do not process $Id: +xxx $ +$Xinfo$ +ignore $Id$ +% hg cat +expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $ +do not process $Id: +xxx $ +$Xinfo: User Name : firstline $ +ignore $Id$ +% cat +expand $Id$ +do not process $Id: +xxx $ +$Xinfo$ +ignore $Id$ +% hg cat +expand $Id$ +do not process $Id: +xxx $ +$Xinfo$ +ignore $Id$ diff -r 639bb4617c70 -r a4ea5c2d3ff3 tests/test-keyword.t --- a/tests/test-keyword.t Sat Sep 10 13:23:41 2011 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1083 +0,0 @@ - $ cat <> $HGRCPATH - > [extensions] - > keyword = - > mq = - > notify = - > record = - > transplant = - > [ui] - > interactive = true - > EOF - -Run kwdemo before [keyword] files are set up -as it would succeed without uisetup otherwise - - $ hg --quiet kwdemo - [extensions] - keyword = - [keyword] - demo.txt = - [keywordset] - svn = False - [keywordmaps] - Author = {author|user} - Date = {date|utcdate} - Header = {root}/{file},v {node|short} {date|utcdate} {author|user} - Id = {file|basename},v {node|short} {date|utcdate} {author|user} - RCSFile = {file|basename},v - RCSfile = {file|basename},v - Revision = {node|short} - Source = {root}/{file},v - $Author: test $ - $Date: ????/??/?? ??:??:?? $ (glob) - $Header: */demo.txt,v ???????????? ????/??/?? ??:??:?? test $ (glob) - $Id: demo.txt,v ???????????? ????/??/?? ??:??:?? test $ (glob) - $RCSFile: demo.txt,v $ - $RCSfile: demo.txt,v $ - $Revision: ???????????? $ (glob) - $Source: */demo.txt,v $ (glob) - - $ hg --quiet kwdemo "Branch = {branches}" - [extensions] - keyword = - [keyword] - demo.txt = - [keywordset] - svn = False - [keywordmaps] - Branch = {branches} - $Branch: demobranch $ - - $ cat <> $HGRCPATH - > [keyword] - > ** = - > b = ignore - > i = ignore - > [hooks] - > EOF - $ cp $HGRCPATH $HGRCPATH.nohooks - > cat <> $HGRCPATH - > commit= - > commit.test=cp a hooktest - > EOF - - $ hg init Test-bndl - $ cd Test-bndl - -kwshrink should exit silently in empty/invalid repo - - $ hg kwshrink - -Symlinks cannot be created on Windows. -A bundle to test this was made with: - hg init t - cd t - echo a > a - ln -s a sym - hg add sym - hg ci -m addsym -u mercurial - hg bundle --base null ../test-keyword.hg - - $ hg pull -u "$TESTDIR"/bundles/test-keyword.hg - pulling from *test-keyword.hg (glob) - requesting all changes - adding changesets - adding manifests - adding file changes - added 1 changesets with 1 changes to 1 files - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - - $ echo 'expand $Id$' > a - $ echo 'do not process $Id:' >> a - $ echo 'xxx $' >> a - $ echo 'ignore $Id$' > b - -Output files as they were created - - $ cat a b - expand $Id$ - do not process $Id: - xxx $ - ignore $Id$ - -no kwfiles - - $ hg kwfiles - -untracked candidates - - $ hg -v kwfiles --unknown - k a - -Add files and check status - - $ hg addremove - adding a - adding b - $ hg status - A a - A b - - -Default keyword expansion including commit hook -Interrupted commit should not change state or run commit hook - - $ hg --debug commit - abort: empty commit message - [255] - $ hg status - A a - A b - -Commit with several checks - - $ hg --debug commit -mabsym -u 'User Name ' - a - b - overwriting a expanding keywords - running hook commit.test: cp a hooktest - committed changeset 1:ef63ca68695bc9495032c6fda1350c71e6d256e9 - $ hg status - ? hooktest - $ hg debugrebuildstate - $ hg --quiet identify - ef63ca68695b - -cat files in working directory with keywords expanded - - $ cat a b - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - do not process $Id: - xxx $ - ignore $Id$ - -hg cat files and symlink, no expansion - - $ hg cat sym a b && echo - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - do not process $Id: - xxx $ - ignore $Id$ - a - -Test hook execution - - $ diff a hooktest - - $ cp $HGRCPATH.nohooks $HGRCPATH - $ rm hooktest - -bundle - - $ hg bundle --base null ../kw.hg - 2 changesets found - $ cd .. - $ hg init Test - $ cd Test - -Notify on pull to check whether keywords stay as is in email -ie. if patch.diff wrapper acts as it should - - $ cat <> $HGRCPATH - > [hooks] - > incoming.notify = python:hgext.notify.hook - > [notify] - > sources = pull - > diffstat = False - > maxsubject = 15 - > [reposubs] - > * = Test - > EOF - -Pull from bundle and trigger notify - - $ hg pull -u ../kw.hg - pulling from ../kw.hg - requesting all changes - adding changesets - adding manifests - adding file changes - added 2 changesets with 3 changes to 3 files - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - Date: * (glob) - Subject: changeset in... - From: mercurial - X-Hg-Notification: changeset a2392c293916 - Message-Id: (glob) - To: Test - - changeset a2392c293916 in $TESTTMP/Test - details: $TESTTMP/Test?cmd=changeset;node=a2392c293916 - description: - addsym - - diffs (6 lines): - - diff -r 000000000000 -r a2392c293916 sym - --- /dev/null Thu Jan 01 00:00:00 1970 +0000 - +++ b/sym Sat Feb 09 20:25:47 2008 +0100 - @@ -0,0 +1,1 @@ - +a - \ No newline at end of file - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - Date:* (glob) - Subject: changeset in... - From: User Name - X-Hg-Notification: changeset ef63ca68695b - Message-Id: (glob) - To: Test - - changeset ef63ca68695b in $TESTTMP/Test - details: $TESTTMP/Test?cmd=changeset;node=ef63ca68695b - description: - absym - - diffs (12 lines): - - diff -r a2392c293916 -r ef63ca68695b a - --- /dev/null Thu Jan 01 00:00:00 1970 +0000 - +++ b/a Thu Jan 01 00:00:00 1970 +0000 - @@ -0,0 +1,3 @@ - +expand $Id$ - +do not process $Id: - +xxx $ - diff -r a2392c293916 -r ef63ca68695b b - --- /dev/null Thu Jan 01 00:00:00 1970 +0000 - +++ b/b Thu Jan 01 00:00:00 1970 +0000 - @@ -0,0 +1,1 @@ - +ignore $Id$ - 3 files updated, 0 files merged, 0 files removed, 0 files unresolved - - $ cp $HGRCPATH.nohooks $HGRCPATH - -Touch files and check with status - - $ touch a b - $ hg status - -Update and expand - - $ rm sym a b - $ hg update -C - 3 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cat a b - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - do not process $Id: - xxx $ - ignore $Id$ - -Check whether expansion is filewise and file mode is preserved - - $ echo '$Id$' > c - $ echo 'tests for different changenodes' >> c - $ chmod 600 c - $ ls -l c | cut -b 1-10 - -rw------- - -commit file c - - $ hg commit -A -mcndiff -d '1 0' -u 'User Name ' - adding c - $ ls -l c | cut -b 1-10 - -rw------- - -force expansion - - $ hg -v kwexpand - overwriting a expanding keywords - overwriting c expanding keywords - -compare changenodes in a and c - - $ cat a c - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - do not process $Id: - xxx $ - $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $ - tests for different changenodes - -record - - $ echo '$Id$' > r - $ hg add r - -record chunk - - $ python -c \ - > 'l=open("a").readlines();l.insert(1,"foo\n");l.append("bar\n");open("a","w").writelines(l);' - $ hg record -d '1 10' -m rectest a< y - > y - > n - > EOF - diff --git a/a b/a - 2 hunks, 2 lines changed - examine changes to 'a'? [Ynsfdaq?] - @@ -1,3 +1,4 @@ - expand $Id$ - +foo - do not process $Id: - xxx $ - record change 1/2 to 'a'? [Ynsfdaq?] - @@ -2,2 +3,3 @@ - do not process $Id: - xxx $ - +bar - record change 2/2 to 'a'? [Ynsfdaq?] - - $ hg identify - d17e03c92c97+ tip - $ hg status - M a - A r - -Cat modified file a - - $ cat a - expand $Id: a,v d17e03c92c97 1970/01/01 00:00:01 test $ - foo - do not process $Id: - xxx $ - bar - -Diff remaining chunk - - $ hg diff a - diff -r d17e03c92c97 a - --- a/a Wed Dec 31 23:59:51 1969 -0000 - +++ b/a * (glob) - @@ -2,3 +2,4 @@ - foo - do not process $Id: - xxx $ - +bar - - $ hg rollback - repository tip rolled back to revision 2 (undo commit) - working directory now based on revision 2 - -Record all chunks in file a - - $ echo foo > msg - - - do not use "hg record -m" here! - - $ hg record -l msg -d '1 11' a< y - > y - > y - > EOF - diff --git a/a b/a - 2 hunks, 2 lines changed - examine changes to 'a'? [Ynsfdaq?] - @@ -1,3 +1,4 @@ - expand $Id$ - +foo - do not process $Id: - xxx $ - record change 1/2 to 'a'? [Ynsfdaq?] - @@ -2,2 +3,3 @@ - do not process $Id: - xxx $ - +bar - record change 2/2 to 'a'? [Ynsfdaq?] - -File a should be clean - - $ hg status -A a - C a - -rollback and revert expansion - - $ cat a - expand $Id: a,v 59f969a3b52c 1970/01/01 00:00:01 test $ - foo - do not process $Id: - xxx $ - bar - $ hg --verbose rollback - repository tip rolled back to revision 2 (undo commit) - working directory now based on revision 2 - overwriting a expanding keywords - $ hg status a - M a - $ cat a - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - foo - do not process $Id: - xxx $ - bar - $ echo '$Id$' > y - $ echo '$Id$' > z - $ hg add y - $ hg commit -Am "rollback only" z - $ cat z - $Id: z,v 45a5d3adce53 1970/01/01 00:00:00 test $ - $ hg --verbose rollback - repository tip rolled back to revision 2 (undo commit) - working directory now based on revision 2 - overwriting z shrinking keywords - -Only z should be overwritten - - $ hg status a y z - M a - A y - A z - $ cat z - $Id$ - $ hg forget y z - $ rm y z - -record added file alone - - $ hg -v record -l msg -d '1 12' r< y - > EOF - diff --git a/r b/r - new file mode 100644 - examine changes to 'r'? [Ynsfdaq?] - r - committed changeset 3:899491280810 - overwriting r expanding keywords - - status call required for dirstate.normallookup() check - $ hg status r - $ hg --verbose rollback - repository tip rolled back to revision 2 (undo commit) - working directory now based on revision 2 - overwriting r shrinking keywords - $ hg forget r - $ rm msg r - $ hg update -C - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - -record added keyword ignored file - - $ echo '$Id$' > i - $ hg add i - $ hg --verbose record -d '1 13' -m recignored< y - > EOF - diff --git a/i b/i - new file mode 100644 - examine changes to 'i'? [Ynsfdaq?] - i - committed changeset 3:5f40fe93bbdc - $ cat i - $Id$ - $ hg -q rollback - $ hg forget i - $ rm i - -Test patch queue repo - - $ hg init --mq - $ hg qimport -r tip -n mqtest.diff - $ hg commit --mq -m mqtest - -Keywords should not be expanded in patch - - $ cat .hg/patches/mqtest.diff - # HG changeset patch - # User User Name - # Date 1 0 - # Node ID 40a904bbbe4cd4ab0a1f28411e35db26341a40ad - # Parent ef63ca68695bc9495032c6fda1350c71e6d256e9 - cndiff - - diff -r ef63ca68695b -r 40a904bbbe4c c - --- /dev/null Thu Jan 01 00:00:00 1970 +0000 - +++ b/c Thu Jan 01 00:00:01 1970 +0000 - @@ -0,0 +1,2 @@ - +$Id$ - +tests for different changenodes - - $ hg qpop - popping mqtest.diff - patch queue now empty - -qgoto, implying qpush, should expand - - $ hg qgoto mqtest.diff - applying mqtest.diff - now at: mqtest.diff - $ cat c - $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $ - tests for different changenodes - $ hg cat c - $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $ - tests for different changenodes - -Keywords should not be expanded in filelog - - $ hg --config 'extensions.keyword=!' cat c - $Id$ - tests for different changenodes - -qpop and move on - - $ hg qpop - popping mqtest.diff - patch queue now empty - -Copy and show added kwfiles - - $ hg cp a c - $ hg kwfiles - a - c - -Commit and show expansion in original and copy - - $ hg --debug commit -ma2c -d '1 0' -u 'User Name ' - c - c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292 - overwriting c expanding keywords - committed changeset 2:25736cf2f5cbe41f6be4e6784ef6ecf9f3bbcc7d - $ cat a c - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - do not process $Id: - xxx $ - expand $Id: c,v 25736cf2f5cb 1970/01/01 00:00:01 user $ - do not process $Id: - xxx $ - -Touch copied c and check its status - - $ touch c - $ hg status - -Copy kwfile to keyword ignored file unexpanding keywords - - $ hg --verbose copy a i - copying a to i - overwriting i shrinking keywords - $ head -n 1 i - expand $Id$ - $ hg forget i - $ rm i - -Copy ignored file to ignored file: no overwriting - - $ hg --verbose copy b i - copying b to i - $ hg forget i - $ rm i - -cp symlink file; hg cp -A symlink file (part1) -- copied symlink points to kwfile: overwrite - - $ cp sym i - $ ls -l i - -rw-r--r--* (glob) - $ head -1 i - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - $ hg copy --after --verbose sym i - copying sym to i - overwriting i shrinking keywords - $ head -1 i - expand $Id$ - $ hg forget i - $ rm i - -Test different options of hg kwfiles - - $ hg kwfiles - a - c - $ hg -v kwfiles --ignore - I b - I sym - $ hg kwfiles --all - K a - K c - I b - I sym - -Diff specific revision - - $ hg diff --rev 1 - diff -r ef63ca68695b c - --- /dev/null Thu Jan 01 00:00:00 1970 +0000 - +++ b/c * (glob) - @@ -0,0 +1,3 @@ - +expand $Id$ - +do not process $Id: - +xxx $ - -Status after rollback: - - $ hg rollback - repository tip rolled back to revision 1 (undo commit) - working directory now based on revision 1 - $ hg status - A c - $ hg update --clean - 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - -cp symlink file; hg cp -A symlink file (part2) -- copied symlink points to kw ignored file: do not overwrite - - $ cat a > i - $ ln -s i symignored - $ hg commit -Am 'fake expansion in ignored and symlink' i symignored - $ cp symignored x - $ hg copy --after --verbose symignored x - copying symignored to x - $ head -n 1 x - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - $ hg forget x - $ rm x - - $ hg rollback - repository tip rolled back to revision 1 (undo commit) - working directory now based on revision 1 - $ hg update --clean - 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ rm i symignored - -Custom keywordmaps as argument to kwdemo - - $ hg --quiet kwdemo "Xinfo = {author}: {desc}" - [extensions] - keyword = - [keyword] - ** = - b = ignore - demo.txt = - i = ignore - [keywordset] - svn = False - [keywordmaps] - Xinfo = {author}: {desc} - $Xinfo: test: hg keyword configuration and expansion example $ - -Configure custom keywordmaps - - $ cat <>$HGRCPATH - > [keywordmaps] - > Id = {file} {node|short} {date|rfc822date} {author|user} - > Xinfo = {author}: {desc} - > EOF - -Cat and hg cat files before custom expansion - - $ cat a b - expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $ - do not process $Id: - xxx $ - ignore $Id$ - $ hg cat sym a b && echo - expand $Id: a ef63ca68695b Thu, 01 Jan 1970 00:00:00 +0000 user $ - do not process $Id: - xxx $ - ignore $Id$ - a - -Write custom keyword and prepare multiline commit message - - $ echo '$Xinfo$' >> a - $ cat <> log - > firstline - > secondline - > EOF - -Interrupted commit should not change state - - $ hg commit - abort: empty commit message - [255] - $ hg status - M a - ? c - ? log - -Commit with multiline message and custom expansion - - $ hg --debug commit -l log -d '2 0' -u 'User Name ' - a - overwriting a expanding keywords - committed changeset 2:bb948857c743469b22bbf51f7ec8112279ca5d83 - $ rm log - -Stat, verify and show custom expansion (firstline) - - $ hg status - ? c - $ hg verify - checking changesets - checking manifests - crosschecking files in changesets and manifests - checking files - 3 files, 3 changesets, 4 total revisions - $ cat a b - expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $ - do not process $Id: - xxx $ - $Xinfo: User Name : firstline $ - ignore $Id$ - $ hg cat sym a b && echo - expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $ - do not process $Id: - xxx $ - $Xinfo: User Name : firstline $ - ignore $Id$ - a - -annotate - - $ hg annotate a - 1: expand $Id$ - 1: do not process $Id: - 1: xxx $ - 2: $Xinfo$ - -remove with status checks - - $ hg debugrebuildstate - $ hg remove a - $ hg --debug commit -m rma - committed changeset 3:d14c712653769de926994cf7fbb06c8fbd68f012 - $ hg status - ? c - -Rollback, revert, and check expansion - - $ hg rollback - repository tip rolled back to revision 2 (undo commit) - working directory now based on revision 2 - $ hg status - R a - ? c - $ hg revert --no-backup --rev tip a - $ cat a - expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $ - do not process $Id: - xxx $ - $Xinfo: User Name : firstline $ - -Clone to test global and local configurations - - $ cd .. - -Expansion in destinaton with global configuration - - $ hg --quiet clone Test globalconf - $ cat globalconf/a - expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $ - do not process $Id: - xxx $ - $Xinfo: User Name : firstline $ - -No expansion in destination with local configuration in origin only - - $ hg --quiet --config 'keyword.**=ignore' clone Test localconf - $ cat localconf/a - expand $Id$ - do not process $Id: - xxx $ - $Xinfo$ - -Clone to test incoming - - $ hg clone -r1 Test Test-a - adding changesets - adding manifests - adding file changes - added 2 changesets with 3 changes to 3 files - updating to branch default - 3 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cd Test-a - $ cat <> .hg/hgrc - > [paths] - > default = ../Test - > EOF - $ hg incoming - comparing with $TESTTMP/Test - searching for changes - changeset: 2:bb948857c743 - tag: tip - user: User Name - date: Thu Jan 01 00:00:02 1970 +0000 - summary: firstline - -Imported patch should not be rejected - - $ python -c \ - > 'import re; s=re.sub("(Id.*)","\\1 rejecttest",open("a").read()); open("a","wb").write(s);' - $ hg --debug commit -m'rejects?' -d '3 0' -u 'User Name ' - a - overwriting a expanding keywords - committed changeset 2:85e279d709ffc28c9fdd1b868570985fc3d87082 - $ hg export -o ../rejecttest.diff tip - $ cd ../Test - $ hg import ../rejecttest.diff - applying ../rejecttest.diff - $ cat a b - expand $Id: a 4e0994474d25 Thu, 01 Jan 1970 00:00:03 +0000 user $ rejecttest - do not process $Id: rejecttest - xxx $ - $Xinfo: User Name : rejects? $ - ignore $Id$ - - $ hg rollback - repository tip rolled back to revision 2 (undo commit) - working directory now based on revision 2 - $ hg update --clean - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - -kwexpand/kwshrink on selected files - - $ mkdir x - $ hg copy a x/a - $ hg --verbose kwshrink a - overwriting a shrinking keywords - - sleep required for dirstate.normal() check - $ sleep 1 - $ hg status a - $ hg --verbose kwexpand a - overwriting a expanding keywords - $ hg status a - -kwexpand x/a should abort - - $ hg --verbose kwexpand x/a - abort: outstanding uncommitted changes - [255] - $ cd x - $ hg --debug commit -m xa -d '3 0' -u 'User Name ' - x/a - x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e - overwriting x/a expanding keywords - committed changeset 3:b4560182a3f9a358179fd2d835c15e9da379c1e4 - $ cat a - expand $Id: x/a b4560182a3f9 Thu, 01 Jan 1970 00:00:03 +0000 user $ - do not process $Id: - xxx $ - $Xinfo: User Name : xa $ - -kwshrink a inside directory x - - $ hg --verbose kwshrink a - overwriting x/a shrinking keywords - $ cat a - expand $Id$ - do not process $Id: - xxx $ - $Xinfo$ - $ cd .. - -kwexpand nonexistent - - $ hg kwexpand nonexistent - nonexistent:* (glob) - - -hg serve - - expand with hgweb file - - no expansion with hgweb annotate/changeset/filediff - - check errors - - $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log - $ cat hg.pid >> $DAEMON_PIDS - $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/file/tip/a/?style=raw' - 200 Script output follows - - expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $ - do not process $Id: - xxx $ - $Xinfo: User Name : firstline $ - $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/annotate/tip/a/?style=raw' - 200 Script output follows - - - user@1: expand $Id$ - user@1: do not process $Id: - user@1: xxx $ - user@2: $Xinfo$ - - - - - $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/rev/tip/?style=raw' - 200 Script output follows - - - # HG changeset patch - # User User Name - # Date 3 0 - # Node ID b4560182a3f9a358179fd2d835c15e9da379c1e4 - # Parent bb948857c743469b22bbf51f7ec8112279ca5d83 - xa - - diff -r bb948857c743 -r b4560182a3f9 x/a - --- /dev/null Thu Jan 01 00:00:00 1970 +0000 - +++ b/x/a Thu Jan 01 00:00:03 1970 +0000 - @@ -0,0 +1,4 @@ - +expand $Id$ - +do not process $Id: - +xxx $ - +$Xinfo$ - - $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/diff/bb948857c743/a?style=raw' - 200 Script output follows - - - diff -r ef63ca68695b -r bb948857c743 a - --- a/a Thu Jan 01 00:00:00 1970 +0000 - +++ b/a Thu Jan 01 00:00:02 1970 +0000 - @@ -1,3 +1,4 @@ - expand $Id$ - do not process $Id: - xxx $ - +$Xinfo$ - - - - - $ cat errors.log - -Prepare merge and resolve tests - - $ echo '$Id$' > m - $ hg add m - $ hg commit -m 4kw - $ echo foo >> m - $ hg commit -m 5foo - -simplemerge - - $ hg update 4 - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ echo foo >> m - $ hg commit -m 6foo - created new head - $ hg merge - 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - (branch merge, don't forget to commit) - $ hg commit -m simplemerge - $ cat m - $Id: m 27d48ee14f67 Thu, 01 Jan 1970 00:00:00 +0000 test $ - foo - -conflict: keyword should stay outside conflict zone - - $ hg update 4 - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ echo bar >> m - $ hg commit -m 8bar - created new head - $ hg merge - merging m - warning: conflicts during merge. - merging m failed! - 0 files updated, 0 files merged, 0 files removed, 1 files unresolved - use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon - [1] - $ cat m - $Id$ - <<<<<<< local - bar - ======= - foo - >>>>>>> other - -resolve to local - - $ HGMERGE=internal:local hg resolve -a - $ hg commit -m localresolve - $ cat m - $Id: m 800511b3a22d Thu, 01 Jan 1970 00:00:00 +0000 test $ - bar - -Test restricted mode with transplant -b - - $ hg update 6 - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg branch foo - marked working directory as branch foo - $ mv a a.bak - $ echo foobranch > a - $ cat a.bak >> a - $ rm a.bak - $ hg commit -m 9foobranch - $ hg update default - 2 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg -y transplant -b foo tip - applying 4aa30d025d50 - 4aa30d025d50 transplanted to e00abbf63521 - -Expansion in changeset but not in file - - $ hg tip -p - changeset: 11:e00abbf63521 - tag: tip - parent: 9:800511b3a22d - user: test - date: Thu Jan 01 00:00:00 1970 +0000 - summary: 9foobranch - - diff -r 800511b3a22d -r e00abbf63521 a - --- a/a Thu Jan 01 00:00:00 1970 +0000 - +++ b/a Thu Jan 01 00:00:00 1970 +0000 - @@ -1,3 +1,4 @@ - +foobranch - expand $Id$ - do not process $Id: - xxx $ - - $ head -n 2 a - foobranch - expand $Id: a e00abbf63521 Thu, 01 Jan 1970 00:00:00 +0000 test $ - -Turn off expansion - - $ hg -q rollback - $ hg -q update -C - -kwshrink with unknown file u - - $ cp a u - $ hg --verbose kwshrink - overwriting a shrinking keywords - overwriting m shrinking keywords - overwriting x/a shrinking keywords - -Keywords shrunk in working directory, but not yet disabled - - cat shows unexpanded keywords - - hg cat shows expanded keywords - - $ cat a b - expand $Id$ - do not process $Id: - xxx $ - $Xinfo$ - ignore $Id$ - $ hg cat sym a b && echo - expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $ - do not process $Id: - xxx $ - $Xinfo: User Name : firstline $ - ignore $Id$ - a - -Now disable keyword expansion - - $ rm "$HGRCPATH" - $ cat a b - expand $Id$ - do not process $Id: - xxx $ - $Xinfo$ - ignore $Id$ - $ hg cat sym a b && echo - expand $Id$ - do not process $Id: - xxx $ - $Xinfo$ - ignore $Id$ - a