# templatekw.py - common changeset template keywords # # Copyright 2005-2009 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. from node import hex import patch, util, error import hbisect # This helper class allows us to handle both: # "{files}" (legacy command-line-specific list hack) and # "{files % '{file}\n'}" (hgweb-style with inlining and function support) class _hybrid(object): def __init__(self, gen, values, joinfmt=None): self.gen = gen self.values = values if joinfmt: self.joinfmt = joinfmt else: self.joinfmt = lambda x: x.values()[0] def __iter__(self): return self.gen def __call__(self): for x in self.values: yield x def showlist(name, values, plural=None, element=None, **args): if not element: element = name f = _showlist(name, values, plural, **args) return _hybrid(f, [{element: x} for x in values]) def _showlist(name, values, plural=None, **args): '''expand set of values. name is name of key in template map. values is list of strings or dicts. plural is plural of name, if not simply name + 's'. expansion works like this, given name 'foo'. if values is empty, expand 'no_foos'. if 'foo' not in template map, return values as a string, joined by space. expand 'start_foos'. for each value, expand 'foo'. if 'last_foo' in template map, expand it instead of 'foo' for last key. expand 'end_foos'. ''' templ = args['templ'] if plural: names = plural else: names = name + 's' if not values: noname = 'no_' + names if noname in templ: yield templ(noname, **args) return if name not in templ: if isinstance(values[0], str): yield ' '.join(values) else: for v in values: yield dict(v, **args) return startname = 'start_' + names if startname in templ: yield templ(startname, **args) vargs = args.copy() def one(v, tag=name): try: vargs.update(v) except (AttributeError, ValueError): try: for a, b in v: vargs[a] = b except ValueError: vargs[name] = v return templ(tag, **vargs) lastname = 'last_' + name if lastname in templ: last = values.pop() else: last = None for v in values: yield one(v) if last is not None: yield one(last, tag=lastname) endname = 'end_' + names if endname in templ: yield templ(endname, **args) def getfiles(repo, ctx, revcache): if 'files' not in revcache: revcache['files'] = repo.status(ctx.p1().node(), ctx.node())[:3] return revcache['files'] def getlatesttags(repo, ctx, cache): '''return date, distance and name for the latest tag of rev''' if 'latesttags' not in cache: # Cache mapping from rev to a tuple with tag date, tag # distance and tag name cache['latesttags'] = {-1: (0, 0, 'null')} latesttags = cache['latesttags'] rev = ctx.rev() todo = [rev] while todo: rev = todo.pop() if rev in latesttags: continue ctx = repo[rev] tags = [t for t in ctx.tags() if repo.tagtype(t) == 'global'] if tags: latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags)) continue try: # The tuples are laid out so the right one can be found by # comparison. pdate, pdist, ptag = max( latesttags[p.rev()] for p in ctx.parents()) except KeyError: # Cache miss - recurse todo.append(rev) todo.extend(p.rev() for p in ctx.parents()) continue latesttags[rev] = pdate, pdist + 1, ptag return latesttags[rev] def getrenamedfn(repo, endrev=None): rcache = {} if endrev is None: endrev = len(repo) def getrenamed(fn, rev): '''looks up all renames for a file (up to endrev) the first time the file is given. It indexes on the changerev and only parses the manifest if linkrev != changerev. Returns rename info for fn at changerev rev.''' if fn not in rcache: rcache[fn] = {} fl = repo.file(fn) for i in fl: lr = fl.linkrev(i) renamed = fl.renamed(fl.node(i)) rcache[fn][lr] = renamed if lr >= endrev: break if rev in rcache[fn]: return rcache[fn][rev] # If linkrev != rev (i.e. rev not found in rcache) fallback to # filectx logic. try: return repo[rev][fn].renamed() except error.LookupError: return None return getrenamed def showauthor(repo, ctx, templ, **args): """:author: String. The unmodified author of the changeset.""" return ctx.user() def showbisect(repo, ctx, templ, **args): """:bisect: String. The changeset bisection status.""" return hbisect.label(repo, ctx.node()) def showbranch(**args): """:branch: String. The name of the branch on which the changeset was committed. """ return args['ctx'].branch() def showbranches(**args): """:branches: List of strings. The name of the branch on which the changeset was committed. Will be empty if the branch name was default. """ branch = args['ctx'].branch() if branch != 'default': return showlist('branch', [branch], plural='branches', **args) def showbookmarks(**args): """:bookmarks: List of strings. Any bookmarks associated with the changeset. """ bookmarks = args['ctx'].bookmarks() return showlist('bookmark', bookmarks, **args) def showchildren(**args): """:children: List of strings. The children of the changeset.""" ctx = args['ctx'] childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()] return showlist('children', childrevs, element='child', **args) def showdate(repo, ctx, templ, **args): """:date: Date information. The date when the changeset was committed.""" return ctx.date() def showdescription(repo, ctx, templ, **args): """:desc: String. The text of the changeset description.""" return ctx.description().strip() def showdiffstat(repo, ctx, templ, **args): """:diffstat: String. Statistics of changes with the following format: "modified files: +added/-removed lines" """ stats = patch.diffstatdata(util.iterlines(ctx.diff())) maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats) return '%s: +%s/-%s' % (len(stats), adds, removes) def showextras(**args): templ = args['templ'] for key, value in sorted(args['ctx'].extra().items()): args = args.copy() args.update(dict(key=key, value=value)) yield templ('extra', **args) def showfileadds(**args): """:file_adds: List of strings. Files added by this changeset.""" repo, ctx, revcache = args['repo'], args['ctx'], args['revcache'] return showlist('file_add', getfiles(repo, ctx, revcache)[1], element='file', **args) def showfilecopies(**args): """:file_copies: List of strings. Files copied in this changeset with their sources. """ cache, ctx = args['cache'], args['ctx'] copies = args['revcache'].get('copies') if copies is None: if 'getrenamed' not in cache: cache['getrenamed'] = getrenamedfn(args['repo']) copies = [] getrenamed = cache['getrenamed'] for fn in ctx.files(): rename = getrenamed(fn, ctx.rev()) if rename: copies.append((fn, rename[0])) c = [{'name': x[0], 'source': x[1]} for x in copies] f = _showlist('file_copy', c, plural='file_copies', **args) return _hybrid(f, c, lambda x: '%s (%s)' % (x['name'], x['source'])) # showfilecopiesswitch() displays file copies only if copy records are # provided before calling the templater, usually with a --copies # command line switch. def showfilecopiesswitch(**args): """:file_copies_switch: List of strings. Like "file_copies" but displayed only if the --copied switch is set. """ copies = args['revcache'].get('copies') or [] c = [{'name': x[0], 'source': x[1]} for x in copies] f = _showlist('file_copy', c, plural='file_copies', **args) return _hybrid(f, c, lambda x: '%s (%s)' % (x['name'], x['source'])) def showfiledels(**args): """:file_dels: List of strings. Files removed by this changeset.""" repo, ctx, revcache = args['repo'], args['ctx'], args['revcache'] return showlist('file_del', getfiles(repo, ctx, revcache)[2], element='file', **args) def showfilemods(**args): """:file_mods: List of strings. Files modified by this changeset.""" repo, ctx, revcache = args['repo'], args['ctx'], args['revcache'] return showlist('file_mod', getfiles(repo, ctx, revcache)[0], element='file', **args) def showfiles(**args): """:files: List of strings. All files modified, added, or removed by this changeset. """ return showlist('file', args['ctx'].files(), **args) def showlatesttag(repo, ctx, templ, cache, **args): """:latesttag: String. Most recent global tag in the ancestors of this changeset. """ return getlatesttags(repo, ctx, cache)[2] def showlatesttagdistance(repo, ctx, templ, cache, **args): """:latesttagdistance: Integer. Longest path to the latest tag.""" return getlatesttags(repo, ctx, cache)[1] def showmanifest(**args): repo, ctx, templ = args['repo'], args['ctx'], args['templ'] args = args.copy() args.update(dict(rev=repo.manifest.rev(ctx.changeset()[0]), node=hex(ctx.changeset()[0]))) return templ('manifest', **args) def shownode(repo, ctx, templ, **args): """:node: String. The changeset identification hash, as a 40 hexadecimal digit string. """ return ctx.hex() def showp1rev(repo, ctx, templ, **args): """:p1rev: Integer. The repository-local revision number of the changeset's first parent, or -1 if the changeset has no parents.""" return ctx.p1().rev() def showp2rev(repo, ctx, templ, **args): """:p2rev: Integer. The repository-local revision number of the changeset's second parent, or -1 if the changeset has no second parent.""" return ctx.p2().rev() def showp1node(repo, ctx, templ, **args): """:p1node: String. The identification hash of the changeset's first parent, as a 40 digit hexadecimal string. If the changeset has no parents, all digits are 0.""" return ctx.p1().hex() def showp2node(repo, ctx, templ, **args): """:p2node: String. The identification hash of the changeset's second parent, as a 40 digit hexadecimal string. If the changeset has no second parent, all digits are 0.""" return ctx.p2().hex() def showphase(repo, ctx, templ, **args): """:phase: String. The changeset phase name.""" return ctx.phasestr() def showphaseidx(repo, ctx, templ, **args): """:phaseidx: Integer. The changeset phase index.""" return ctx.phase() def showrev(repo, ctx, templ, **args): """:rev: Integer. The repository-local changeset revision number.""" return ctx.rev() def showtags(**args): """:tags: List of strings. Any tags associated with the changeset.""" return showlist('tag', args['ctx'].tags(), **args) # keywords are callables like: # fn(repo, ctx, templ, cache, revcache, **args) # with: # repo - current repository instance # ctx - the changectx being displayed # templ - the templater instance # cache - a cache dictionary for the whole templater run # revcache - a cache dictionary for the current revision keywords = { 'author': showauthor, 'bisect': showbisect, 'branch': showbranch, 'branches': showbranches, 'bookmarks': showbookmarks, 'children': showchildren, 'date': showdate, 'desc': showdescription, 'diffstat': showdiffstat, 'extras': showextras, 'file_adds': showfileadds, 'file_copies': showfilecopies, 'file_copies_switch': showfilecopiesswitch, 'file_dels': showfiledels, 'file_mods': showfilemods, 'files': showfiles, 'latesttag': showlatesttag, 'latesttagdistance': showlatesttagdistance, 'manifest': showmanifest, 'node': shownode, 'p1rev': showp1rev, 'p1node': showp1node, 'p2rev': showp2rev, 'p2node': showp2node, 'phase': showphase, 'phaseidx': showphaseidx, 'rev': showrev, 'tags': showtags, } def _showparents(**args): """:parents: List of strings. The parents of the changeset in "rev:node" format. If the changeset has only one "natural" parent (the predecessor revision) nothing is shown.""" pass dockeywords = { 'parents': _showparents, } dockeywords.update(keywords) # tell hggettext to extract docstrings from these functions: i18nfunctions = dockeywords.values()