##// END OF EJS Templates
templatekw: make getrenamed() return only filename, not nodeid...
Martin von Zweigbergk -
r38422:ec37df90 default
parent child Browse files
Show More
@@ -1,912 +1,912 b''
1 # logcmdutil.py - utility for log-like commands
1 # logcmdutil.py - utility for log-like commands
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import itertools
10 import itertools
11 import os
11 import os
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 nullid,
15 nullid,
16 )
16 )
17
17
18 from . import (
18 from . import (
19 dagop,
19 dagop,
20 error,
20 error,
21 formatter,
21 formatter,
22 graphmod,
22 graphmod,
23 match as matchmod,
23 match as matchmod,
24 mdiff,
24 mdiff,
25 patch,
25 patch,
26 pathutil,
26 pathutil,
27 pycompat,
27 pycompat,
28 revset,
28 revset,
29 revsetlang,
29 revsetlang,
30 scmutil,
30 scmutil,
31 smartset,
31 smartset,
32 templatekw,
32 templatekw,
33 templater,
33 templater,
34 util,
34 util,
35 )
35 )
36 from .utils import (
36 from .utils import (
37 dateutil,
37 dateutil,
38 stringutil,
38 stringutil,
39 )
39 )
40
40
41 def getlimit(opts):
41 def getlimit(opts):
42 """get the log limit according to option -l/--limit"""
42 """get the log limit according to option -l/--limit"""
43 limit = opts.get('limit')
43 limit = opts.get('limit')
44 if limit:
44 if limit:
45 try:
45 try:
46 limit = int(limit)
46 limit = int(limit)
47 except ValueError:
47 except ValueError:
48 raise error.Abort(_('limit must be a positive integer'))
48 raise error.Abort(_('limit must be a positive integer'))
49 if limit <= 0:
49 if limit <= 0:
50 raise error.Abort(_('limit must be positive'))
50 raise error.Abort(_('limit must be positive'))
51 else:
51 else:
52 limit = None
52 limit = None
53 return limit
53 return limit
54
54
55 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
55 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
56 changes=None, stat=False, fp=None, prefix='',
56 changes=None, stat=False, fp=None, prefix='',
57 root='', listsubrepos=False, hunksfilterfn=None):
57 root='', listsubrepos=False, hunksfilterfn=None):
58 '''show diff or diffstat.'''
58 '''show diff or diffstat.'''
59 if root:
59 if root:
60 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
60 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
61 else:
61 else:
62 relroot = ''
62 relroot = ''
63 if relroot != '':
63 if relroot != '':
64 # XXX relative roots currently don't work if the root is within a
64 # XXX relative roots currently don't work if the root is within a
65 # subrepo
65 # subrepo
66 uirelroot = match.uipath(relroot)
66 uirelroot = match.uipath(relroot)
67 relroot += '/'
67 relroot += '/'
68 for matchroot in match.files():
68 for matchroot in match.files():
69 if not matchroot.startswith(relroot):
69 if not matchroot.startswith(relroot):
70 ui.warn(_('warning: %s not inside relative root %s\n') % (
70 ui.warn(_('warning: %s not inside relative root %s\n') % (
71 match.uipath(matchroot), uirelroot))
71 match.uipath(matchroot), uirelroot))
72
72
73 if stat:
73 if stat:
74 diffopts = diffopts.copy(context=0, noprefix=False)
74 diffopts = diffopts.copy(context=0, noprefix=False)
75 width = 80
75 width = 80
76 if not ui.plain():
76 if not ui.plain():
77 width = ui.termwidth()
77 width = ui.termwidth()
78
78
79 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
79 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
80 prefix=prefix, relroot=relroot,
80 prefix=prefix, relroot=relroot,
81 hunksfilterfn=hunksfilterfn)
81 hunksfilterfn=hunksfilterfn)
82
82
83 if fp is not None or ui.canwritewithoutlabels():
83 if fp is not None or ui.canwritewithoutlabels():
84 out = fp or ui
84 out = fp or ui
85 if stat:
85 if stat:
86 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
86 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
87 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
87 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
88 out.write(chunk)
88 out.write(chunk)
89 else:
89 else:
90 if stat:
90 if stat:
91 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
91 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
92 else:
92 else:
93 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
93 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
94 opts=diffopts)
94 opts=diffopts)
95 if ui.canbatchlabeledwrites():
95 if ui.canbatchlabeledwrites():
96 def gen():
96 def gen():
97 for chunk, label in chunks:
97 for chunk, label in chunks:
98 yield ui.label(chunk, label=label)
98 yield ui.label(chunk, label=label)
99 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
99 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
100 ui.write(chunk)
100 ui.write(chunk)
101 else:
101 else:
102 for chunk, label in chunks:
102 for chunk, label in chunks:
103 ui.write(chunk, label=label)
103 ui.write(chunk, label=label)
104
104
105 if listsubrepos:
105 if listsubrepos:
106 ctx1 = repo[node1]
106 ctx1 = repo[node1]
107 ctx2 = repo[node2]
107 ctx2 = repo[node2]
108 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
108 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
109 tempnode2 = node2
109 tempnode2 = node2
110 try:
110 try:
111 if node2 is not None:
111 if node2 is not None:
112 tempnode2 = ctx2.substate[subpath][1]
112 tempnode2 = ctx2.substate[subpath][1]
113 except KeyError:
113 except KeyError:
114 # A subrepo that existed in node1 was deleted between node1 and
114 # A subrepo that existed in node1 was deleted between node1 and
115 # node2 (inclusive). Thus, ctx2's substate won't contain that
115 # node2 (inclusive). Thus, ctx2's substate won't contain that
116 # subpath. The best we can do is to ignore it.
116 # subpath. The best we can do is to ignore it.
117 tempnode2 = None
117 tempnode2 = None
118 submatch = matchmod.subdirmatcher(subpath, match)
118 submatch = matchmod.subdirmatcher(subpath, match)
119 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
119 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
120 stat=stat, fp=fp, prefix=prefix)
120 stat=stat, fp=fp, prefix=prefix)
121
121
122 class changesetdiffer(object):
122 class changesetdiffer(object):
123 """Generate diff of changeset with pre-configured filtering functions"""
123 """Generate diff of changeset with pre-configured filtering functions"""
124
124
125 def _makefilematcher(self, ctx):
125 def _makefilematcher(self, ctx):
126 return scmutil.matchall(ctx.repo())
126 return scmutil.matchall(ctx.repo())
127
127
128 def _makehunksfilter(self, ctx):
128 def _makehunksfilter(self, ctx):
129 return None
129 return None
130
130
131 def showdiff(self, ui, ctx, diffopts, stat=False):
131 def showdiff(self, ui, ctx, diffopts, stat=False):
132 repo = ctx.repo()
132 repo = ctx.repo()
133 node = ctx.node()
133 node = ctx.node()
134 prev = ctx.p1().node()
134 prev = ctx.p1().node()
135 diffordiffstat(ui, repo, diffopts, prev, node,
135 diffordiffstat(ui, repo, diffopts, prev, node,
136 match=self._makefilematcher(ctx), stat=stat,
136 match=self._makefilematcher(ctx), stat=stat,
137 hunksfilterfn=self._makehunksfilter(ctx))
137 hunksfilterfn=self._makehunksfilter(ctx))
138
138
139 def changesetlabels(ctx):
139 def changesetlabels(ctx):
140 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
140 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
141 if ctx.obsolete():
141 if ctx.obsolete():
142 labels.append('changeset.obsolete')
142 labels.append('changeset.obsolete')
143 if ctx.isunstable():
143 if ctx.isunstable():
144 labels.append('changeset.unstable')
144 labels.append('changeset.unstable')
145 for instability in ctx.instabilities():
145 for instability in ctx.instabilities():
146 labels.append('instability.%s' % instability)
146 labels.append('instability.%s' % instability)
147 return ' '.join(labels)
147 return ' '.join(labels)
148
148
149 class changesetprinter(object):
149 class changesetprinter(object):
150 '''show changeset information when templating not requested.'''
150 '''show changeset information when templating not requested.'''
151
151
152 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
152 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
153 self.ui = ui
153 self.ui = ui
154 self.repo = repo
154 self.repo = repo
155 self.buffered = buffered
155 self.buffered = buffered
156 self._differ = differ or changesetdiffer()
156 self._differ = differ or changesetdiffer()
157 self._diffopts = patch.diffallopts(ui, diffopts)
157 self._diffopts = patch.diffallopts(ui, diffopts)
158 self._includestat = diffopts and diffopts.get('stat')
158 self._includestat = diffopts and diffopts.get('stat')
159 self._includediff = diffopts and diffopts.get('patch')
159 self._includediff = diffopts and diffopts.get('patch')
160 self.header = {}
160 self.header = {}
161 self.hunk = {}
161 self.hunk = {}
162 self.lastheader = None
162 self.lastheader = None
163 self.footer = None
163 self.footer = None
164 self._columns = templatekw.getlogcolumns()
164 self._columns = templatekw.getlogcolumns()
165
165
166 def flush(self, ctx):
166 def flush(self, ctx):
167 rev = ctx.rev()
167 rev = ctx.rev()
168 if rev in self.header:
168 if rev in self.header:
169 h = self.header[rev]
169 h = self.header[rev]
170 if h != self.lastheader:
170 if h != self.lastheader:
171 self.lastheader = h
171 self.lastheader = h
172 self.ui.write(h)
172 self.ui.write(h)
173 del self.header[rev]
173 del self.header[rev]
174 if rev in self.hunk:
174 if rev in self.hunk:
175 self.ui.write(self.hunk[rev])
175 self.ui.write(self.hunk[rev])
176 del self.hunk[rev]
176 del self.hunk[rev]
177
177
178 def close(self):
178 def close(self):
179 if self.footer:
179 if self.footer:
180 self.ui.write(self.footer)
180 self.ui.write(self.footer)
181
181
182 def show(self, ctx, copies=None, **props):
182 def show(self, ctx, copies=None, **props):
183 props = pycompat.byteskwargs(props)
183 props = pycompat.byteskwargs(props)
184 if self.buffered:
184 if self.buffered:
185 self.ui.pushbuffer(labeled=True)
185 self.ui.pushbuffer(labeled=True)
186 self._show(ctx, copies, props)
186 self._show(ctx, copies, props)
187 self.hunk[ctx.rev()] = self.ui.popbuffer()
187 self.hunk[ctx.rev()] = self.ui.popbuffer()
188 else:
188 else:
189 self._show(ctx, copies, props)
189 self._show(ctx, copies, props)
190
190
191 def _show(self, ctx, copies, props):
191 def _show(self, ctx, copies, props):
192 '''show a single changeset or file revision'''
192 '''show a single changeset or file revision'''
193 changenode = ctx.node()
193 changenode = ctx.node()
194 rev = ctx.rev()
194 rev = ctx.rev()
195
195
196 if self.ui.quiet:
196 if self.ui.quiet:
197 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
197 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
198 label='log.node')
198 label='log.node')
199 return
199 return
200
200
201 columns = self._columns
201 columns = self._columns
202 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
202 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
203 label=changesetlabels(ctx))
203 label=changesetlabels(ctx))
204
204
205 # branches are shown first before any other names due to backwards
205 # branches are shown first before any other names due to backwards
206 # compatibility
206 # compatibility
207 branch = ctx.branch()
207 branch = ctx.branch()
208 # don't show the default branch name
208 # don't show the default branch name
209 if branch != 'default':
209 if branch != 'default':
210 self.ui.write(columns['branch'] % branch, label='log.branch')
210 self.ui.write(columns['branch'] % branch, label='log.branch')
211
211
212 for nsname, ns in self.repo.names.iteritems():
212 for nsname, ns in self.repo.names.iteritems():
213 # branches has special logic already handled above, so here we just
213 # branches has special logic already handled above, so here we just
214 # skip it
214 # skip it
215 if nsname == 'branches':
215 if nsname == 'branches':
216 continue
216 continue
217 # we will use the templatename as the color name since those two
217 # we will use the templatename as the color name since those two
218 # should be the same
218 # should be the same
219 for name in ns.names(self.repo, changenode):
219 for name in ns.names(self.repo, changenode):
220 self.ui.write(ns.logfmt % name,
220 self.ui.write(ns.logfmt % name,
221 label='log.%s' % ns.colorname)
221 label='log.%s' % ns.colorname)
222 if self.ui.debugflag:
222 if self.ui.debugflag:
223 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
223 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
224 for pctx in scmutil.meaningfulparents(self.repo, ctx):
224 for pctx in scmutil.meaningfulparents(self.repo, ctx):
225 label = 'log.parent changeset.%s' % pctx.phasestr()
225 label = 'log.parent changeset.%s' % pctx.phasestr()
226 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
226 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
227 label=label)
227 label=label)
228
228
229 if self.ui.debugflag and rev is not None:
229 if self.ui.debugflag and rev is not None:
230 mnode = ctx.manifestnode()
230 mnode = ctx.manifestnode()
231 mrev = self.repo.manifestlog._revlog.rev(mnode)
231 mrev = self.repo.manifestlog._revlog.rev(mnode)
232 self.ui.write(columns['manifest']
232 self.ui.write(columns['manifest']
233 % scmutil.formatrevnode(self.ui, mrev, mnode),
233 % scmutil.formatrevnode(self.ui, mrev, mnode),
234 label='ui.debug log.manifest')
234 label='ui.debug log.manifest')
235 self.ui.write(columns['user'] % ctx.user(), label='log.user')
235 self.ui.write(columns['user'] % ctx.user(), label='log.user')
236 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
236 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
237 label='log.date')
237 label='log.date')
238
238
239 if ctx.isunstable():
239 if ctx.isunstable():
240 instabilities = ctx.instabilities()
240 instabilities = ctx.instabilities()
241 self.ui.write(columns['instability'] % ', '.join(instabilities),
241 self.ui.write(columns['instability'] % ', '.join(instabilities),
242 label='log.instability')
242 label='log.instability')
243
243
244 elif ctx.obsolete():
244 elif ctx.obsolete():
245 self._showobsfate(ctx)
245 self._showobsfate(ctx)
246
246
247 self._exthook(ctx)
247 self._exthook(ctx)
248
248
249 if self.ui.debugflag:
249 if self.ui.debugflag:
250 files = ctx.p1().status(ctx)[:3]
250 files = ctx.p1().status(ctx)[:3]
251 for key, value in zip(['files', 'files+', 'files-'], files):
251 for key, value in zip(['files', 'files+', 'files-'], files):
252 if value:
252 if value:
253 self.ui.write(columns[key] % " ".join(value),
253 self.ui.write(columns[key] % " ".join(value),
254 label='ui.debug log.files')
254 label='ui.debug log.files')
255 elif ctx.files() and self.ui.verbose:
255 elif ctx.files() and self.ui.verbose:
256 self.ui.write(columns['files'] % " ".join(ctx.files()),
256 self.ui.write(columns['files'] % " ".join(ctx.files()),
257 label='ui.note log.files')
257 label='ui.note log.files')
258 if copies and self.ui.verbose:
258 if copies and self.ui.verbose:
259 copies = ['%s (%s)' % c for c in copies]
259 copies = ['%s (%s)' % c for c in copies]
260 self.ui.write(columns['copies'] % ' '.join(copies),
260 self.ui.write(columns['copies'] % ' '.join(copies),
261 label='ui.note log.copies')
261 label='ui.note log.copies')
262
262
263 extra = ctx.extra()
263 extra = ctx.extra()
264 if extra and self.ui.debugflag:
264 if extra and self.ui.debugflag:
265 for key, value in sorted(extra.items()):
265 for key, value in sorted(extra.items()):
266 self.ui.write(columns['extra']
266 self.ui.write(columns['extra']
267 % (key, stringutil.escapestr(value)),
267 % (key, stringutil.escapestr(value)),
268 label='ui.debug log.extra')
268 label='ui.debug log.extra')
269
269
270 description = ctx.description().strip()
270 description = ctx.description().strip()
271 if description:
271 if description:
272 if self.ui.verbose:
272 if self.ui.verbose:
273 self.ui.write(_("description:\n"),
273 self.ui.write(_("description:\n"),
274 label='ui.note log.description')
274 label='ui.note log.description')
275 self.ui.write(description,
275 self.ui.write(description,
276 label='ui.note log.description')
276 label='ui.note log.description')
277 self.ui.write("\n\n")
277 self.ui.write("\n\n")
278 else:
278 else:
279 self.ui.write(columns['summary'] % description.splitlines()[0],
279 self.ui.write(columns['summary'] % description.splitlines()[0],
280 label='log.summary')
280 label='log.summary')
281 self.ui.write("\n")
281 self.ui.write("\n")
282
282
283 self._showpatch(ctx)
283 self._showpatch(ctx)
284
284
285 def _showobsfate(self, ctx):
285 def _showobsfate(self, ctx):
286 # TODO: do not depend on templater
286 # TODO: do not depend on templater
287 tres = formatter.templateresources(self.repo.ui, self.repo)
287 tres = formatter.templateresources(self.repo.ui, self.repo)
288 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
288 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
289 defaults=templatekw.keywords,
289 defaults=templatekw.keywords,
290 resources=tres)
290 resources=tres)
291 obsfate = t.renderdefault({'ctx': ctx}).splitlines()
291 obsfate = t.renderdefault({'ctx': ctx}).splitlines()
292
292
293 if obsfate:
293 if obsfate:
294 for obsfateline in obsfate:
294 for obsfateline in obsfate:
295 self.ui.write(self._columns['obsolete'] % obsfateline,
295 self.ui.write(self._columns['obsolete'] % obsfateline,
296 label='log.obsfate')
296 label='log.obsfate')
297
297
298 def _exthook(self, ctx):
298 def _exthook(self, ctx):
299 '''empty method used by extension as a hook point
299 '''empty method used by extension as a hook point
300 '''
300 '''
301
301
302 def _showpatch(self, ctx):
302 def _showpatch(self, ctx):
303 if self._includestat:
303 if self._includestat:
304 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
304 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
305 if self._includestat and self._includediff:
305 if self._includestat and self._includediff:
306 self.ui.write("\n")
306 self.ui.write("\n")
307 if self._includediff:
307 if self._includediff:
308 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
308 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
309 if self._includestat or self._includediff:
309 if self._includestat or self._includediff:
310 self.ui.write("\n")
310 self.ui.write("\n")
311
311
312 class changesetformatter(changesetprinter):
312 class changesetformatter(changesetprinter):
313 """Format changeset information by generic formatter"""
313 """Format changeset information by generic formatter"""
314
314
315 def __init__(self, ui, repo, fm, differ=None, diffopts=None,
315 def __init__(self, ui, repo, fm, differ=None, diffopts=None,
316 buffered=False):
316 buffered=False):
317 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
317 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
318 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
318 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
319 self._fm = fm
319 self._fm = fm
320
320
321 def close(self):
321 def close(self):
322 self._fm.end()
322 self._fm.end()
323
323
324 def _show(self, ctx, copies, props):
324 def _show(self, ctx, copies, props):
325 '''show a single changeset or file revision'''
325 '''show a single changeset or file revision'''
326 fm = self._fm
326 fm = self._fm
327 fm.startitem()
327 fm.startitem()
328
328
329 # TODO: maybe this should be wdirrev/wdirnode?
329 # TODO: maybe this should be wdirrev/wdirnode?
330 rev = ctx.rev()
330 rev = ctx.rev()
331 if rev is None:
331 if rev is None:
332 hexnode = None
332 hexnode = None
333 else:
333 else:
334 hexnode = fm.hexfunc(ctx.node())
334 hexnode = fm.hexfunc(ctx.node())
335 fm.data(rev=rev,
335 fm.data(rev=rev,
336 node=hexnode)
336 node=hexnode)
337
337
338 if self.ui.quiet:
338 if self.ui.quiet:
339 return
339 return
340
340
341 fm.data(branch=ctx.branch(),
341 fm.data(branch=ctx.branch(),
342 phase=ctx.phasestr(),
342 phase=ctx.phasestr(),
343 user=ctx.user(),
343 user=ctx.user(),
344 date=fm.formatdate(ctx.date()),
344 date=fm.formatdate(ctx.date()),
345 desc=ctx.description(),
345 desc=ctx.description(),
346 bookmarks=fm.formatlist(ctx.bookmarks(), name='bookmark'),
346 bookmarks=fm.formatlist(ctx.bookmarks(), name='bookmark'),
347 tags=fm.formatlist(ctx.tags(), name='tag'),
347 tags=fm.formatlist(ctx.tags(), name='tag'),
348 parents=fm.formatlist([fm.hexfunc(c.node())
348 parents=fm.formatlist([fm.hexfunc(c.node())
349 for c in ctx.parents()], name='node'))
349 for c in ctx.parents()], name='node'))
350
350
351 if self.ui.debugflag:
351 if self.ui.debugflag:
352 if rev is None:
352 if rev is None:
353 hexnode = None
353 hexnode = None
354 else:
354 else:
355 hexnode = fm.hexfunc(ctx.manifestnode())
355 hexnode = fm.hexfunc(ctx.manifestnode())
356 fm.data(manifest=hexnode,
356 fm.data(manifest=hexnode,
357 extra=fm.formatdict(ctx.extra()))
357 extra=fm.formatdict(ctx.extra()))
358
358
359 files = ctx.p1().status(ctx)
359 files = ctx.p1().status(ctx)
360 fm.data(modified=fm.formatlist(files[0], name='file'),
360 fm.data(modified=fm.formatlist(files[0], name='file'),
361 added=fm.formatlist(files[1], name='file'),
361 added=fm.formatlist(files[1], name='file'),
362 removed=fm.formatlist(files[2], name='file'))
362 removed=fm.formatlist(files[2], name='file'))
363
363
364 elif self.ui.verbose:
364 elif self.ui.verbose:
365 fm.data(files=fm.formatlist(ctx.files(), name='file'))
365 fm.data(files=fm.formatlist(ctx.files(), name='file'))
366 if copies:
366 if copies:
367 fm.data(copies=fm.formatdict(copies,
367 fm.data(copies=fm.formatdict(copies,
368 key='name', value='source'))
368 key='name', value='source'))
369
369
370 if self._includestat:
370 if self._includestat:
371 self.ui.pushbuffer()
371 self.ui.pushbuffer()
372 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
372 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
373 fm.data(diffstat=self.ui.popbuffer())
373 fm.data(diffstat=self.ui.popbuffer())
374 if self._includediff:
374 if self._includediff:
375 self.ui.pushbuffer()
375 self.ui.pushbuffer()
376 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
376 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
377 fm.data(diff=self.ui.popbuffer())
377 fm.data(diff=self.ui.popbuffer())
378
378
379 class changesettemplater(changesetprinter):
379 class changesettemplater(changesetprinter):
380 '''format changeset information.
380 '''format changeset information.
381
381
382 Note: there are a variety of convenience functions to build a
382 Note: there are a variety of convenience functions to build a
383 changesettemplater for common cases. See functions such as:
383 changesettemplater for common cases. See functions such as:
384 maketemplater, changesetdisplayer, buildcommittemplate, or other
384 maketemplater, changesetdisplayer, buildcommittemplate, or other
385 functions that use changesest_templater.
385 functions that use changesest_templater.
386 '''
386 '''
387
387
388 # Arguments before "buffered" used to be positional. Consider not
388 # Arguments before "buffered" used to be positional. Consider not
389 # adding/removing arguments before "buffered" to not break callers.
389 # adding/removing arguments before "buffered" to not break callers.
390 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
390 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
391 buffered=False):
391 buffered=False):
392 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
392 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
393 # tres is shared with _graphnodeformatter()
393 # tres is shared with _graphnodeformatter()
394 self._tresources = tres = formatter.templateresources(ui, repo)
394 self._tresources = tres = formatter.templateresources(ui, repo)
395 self.t = formatter.loadtemplater(ui, tmplspec,
395 self.t = formatter.loadtemplater(ui, tmplspec,
396 defaults=templatekw.keywords,
396 defaults=templatekw.keywords,
397 resources=tres,
397 resources=tres,
398 cache=templatekw.defaulttempl)
398 cache=templatekw.defaulttempl)
399 self._counter = itertools.count()
399 self._counter = itertools.count()
400
400
401 self._tref = tmplspec.ref
401 self._tref = tmplspec.ref
402 self._parts = {'header': '', 'footer': '',
402 self._parts = {'header': '', 'footer': '',
403 tmplspec.ref: tmplspec.ref,
403 tmplspec.ref: tmplspec.ref,
404 'docheader': '', 'docfooter': '',
404 'docheader': '', 'docfooter': '',
405 'separator': ''}
405 'separator': ''}
406 if tmplspec.mapfile:
406 if tmplspec.mapfile:
407 # find correct templates for current mode, for backward
407 # find correct templates for current mode, for backward
408 # compatibility with 'log -v/-q/--debug' using a mapfile
408 # compatibility with 'log -v/-q/--debug' using a mapfile
409 tmplmodes = [
409 tmplmodes = [
410 (True, ''),
410 (True, ''),
411 (self.ui.verbose, '_verbose'),
411 (self.ui.verbose, '_verbose'),
412 (self.ui.quiet, '_quiet'),
412 (self.ui.quiet, '_quiet'),
413 (self.ui.debugflag, '_debug'),
413 (self.ui.debugflag, '_debug'),
414 ]
414 ]
415 for mode, postfix in tmplmodes:
415 for mode, postfix in tmplmodes:
416 for t in self._parts:
416 for t in self._parts:
417 cur = t + postfix
417 cur = t + postfix
418 if mode and cur in self.t:
418 if mode and cur in self.t:
419 self._parts[t] = cur
419 self._parts[t] = cur
420 else:
420 else:
421 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
421 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
422 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
422 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
423 self._parts.update(m)
423 self._parts.update(m)
424
424
425 if self._parts['docheader']:
425 if self._parts['docheader']:
426 self.ui.write(self.t.render(self._parts['docheader'], {}))
426 self.ui.write(self.t.render(self._parts['docheader'], {}))
427
427
428 def close(self):
428 def close(self):
429 if self._parts['docfooter']:
429 if self._parts['docfooter']:
430 if not self.footer:
430 if not self.footer:
431 self.footer = ""
431 self.footer = ""
432 self.footer += self.t.render(self._parts['docfooter'], {})
432 self.footer += self.t.render(self._parts['docfooter'], {})
433 return super(changesettemplater, self).close()
433 return super(changesettemplater, self).close()
434
434
435 def _show(self, ctx, copies, props):
435 def _show(self, ctx, copies, props):
436 '''show a single changeset or file revision'''
436 '''show a single changeset or file revision'''
437 props = props.copy()
437 props = props.copy()
438 props['ctx'] = ctx
438 props['ctx'] = ctx
439 props['index'] = index = next(self._counter)
439 props['index'] = index = next(self._counter)
440 props['revcache'] = {'copies': copies}
440 props['revcache'] = {'copies': copies}
441
441
442 # write separator, which wouldn't work well with the header part below
442 # write separator, which wouldn't work well with the header part below
443 # since there's inherently a conflict between header (across items) and
443 # since there's inherently a conflict between header (across items) and
444 # separator (per item)
444 # separator (per item)
445 if self._parts['separator'] and index > 0:
445 if self._parts['separator'] and index > 0:
446 self.ui.write(self.t.render(self._parts['separator'], {}))
446 self.ui.write(self.t.render(self._parts['separator'], {}))
447
447
448 # write header
448 # write header
449 if self._parts['header']:
449 if self._parts['header']:
450 h = self.t.render(self._parts['header'], props)
450 h = self.t.render(self._parts['header'], props)
451 if self.buffered:
451 if self.buffered:
452 self.header[ctx.rev()] = h
452 self.header[ctx.rev()] = h
453 else:
453 else:
454 if self.lastheader != h:
454 if self.lastheader != h:
455 self.lastheader = h
455 self.lastheader = h
456 self.ui.write(h)
456 self.ui.write(h)
457
457
458 # write changeset metadata, then patch if requested
458 # write changeset metadata, then patch if requested
459 key = self._parts[self._tref]
459 key = self._parts[self._tref]
460 self.ui.write(self.t.render(key, props))
460 self.ui.write(self.t.render(key, props))
461 self._showpatch(ctx)
461 self._showpatch(ctx)
462
462
463 if self._parts['footer']:
463 if self._parts['footer']:
464 if not self.footer:
464 if not self.footer:
465 self.footer = self.t.render(self._parts['footer'], props)
465 self.footer = self.t.render(self._parts['footer'], props)
466
466
467 def templatespec(tmpl, mapfile):
467 def templatespec(tmpl, mapfile):
468 if mapfile:
468 if mapfile:
469 return formatter.templatespec('changeset', tmpl, mapfile)
469 return formatter.templatespec('changeset', tmpl, mapfile)
470 else:
470 else:
471 return formatter.templatespec('', tmpl, None)
471 return formatter.templatespec('', tmpl, None)
472
472
473 def _lookuptemplate(ui, tmpl, style):
473 def _lookuptemplate(ui, tmpl, style):
474 """Find the template matching the given template spec or style
474 """Find the template matching the given template spec or style
475
475
476 See formatter.lookuptemplate() for details.
476 See formatter.lookuptemplate() for details.
477 """
477 """
478
478
479 # ui settings
479 # ui settings
480 if not tmpl and not style: # template are stronger than style
480 if not tmpl and not style: # template are stronger than style
481 tmpl = ui.config('ui', 'logtemplate')
481 tmpl = ui.config('ui', 'logtemplate')
482 if tmpl:
482 if tmpl:
483 return templatespec(templater.unquotestring(tmpl), None)
483 return templatespec(templater.unquotestring(tmpl), None)
484 else:
484 else:
485 style = util.expandpath(ui.config('ui', 'style'))
485 style = util.expandpath(ui.config('ui', 'style'))
486
486
487 if not tmpl and style:
487 if not tmpl and style:
488 mapfile = style
488 mapfile = style
489 if not os.path.split(mapfile)[0]:
489 if not os.path.split(mapfile)[0]:
490 mapname = (templater.templatepath('map-cmdline.' + mapfile)
490 mapname = (templater.templatepath('map-cmdline.' + mapfile)
491 or templater.templatepath(mapfile))
491 or templater.templatepath(mapfile))
492 if mapname:
492 if mapname:
493 mapfile = mapname
493 mapfile = mapname
494 return templatespec(None, mapfile)
494 return templatespec(None, mapfile)
495
495
496 if not tmpl:
496 if not tmpl:
497 return templatespec(None, None)
497 return templatespec(None, None)
498
498
499 return formatter.lookuptemplate(ui, 'changeset', tmpl)
499 return formatter.lookuptemplate(ui, 'changeset', tmpl)
500
500
501 def maketemplater(ui, repo, tmpl, buffered=False):
501 def maketemplater(ui, repo, tmpl, buffered=False):
502 """Create a changesettemplater from a literal template 'tmpl'
502 """Create a changesettemplater from a literal template 'tmpl'
503 byte-string."""
503 byte-string."""
504 spec = templatespec(tmpl, None)
504 spec = templatespec(tmpl, None)
505 return changesettemplater(ui, repo, spec, buffered=buffered)
505 return changesettemplater(ui, repo, spec, buffered=buffered)
506
506
507 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
507 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
508 """show one changeset using template or regular display.
508 """show one changeset using template or regular display.
509
509
510 Display format will be the first non-empty hit of:
510 Display format will be the first non-empty hit of:
511 1. option 'template'
511 1. option 'template'
512 2. option 'style'
512 2. option 'style'
513 3. [ui] setting 'logtemplate'
513 3. [ui] setting 'logtemplate'
514 4. [ui] setting 'style'
514 4. [ui] setting 'style'
515 If all of these values are either the unset or the empty string,
515 If all of these values are either the unset or the empty string,
516 regular display via changesetprinter() is done.
516 regular display via changesetprinter() is done.
517 """
517 """
518 postargs = (differ, opts, buffered)
518 postargs = (differ, opts, buffered)
519 if opts.get('template') == 'json':
519 if opts.get('template') == 'json':
520 fm = ui.formatter('log', opts)
520 fm = ui.formatter('log', opts)
521 return changesetformatter(ui, repo, fm, *postargs)
521 return changesetformatter(ui, repo, fm, *postargs)
522
522
523 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
523 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
524
524
525 if not spec.ref and not spec.tmpl and not spec.mapfile:
525 if not spec.ref and not spec.tmpl and not spec.mapfile:
526 return changesetprinter(ui, repo, *postargs)
526 return changesetprinter(ui, repo, *postargs)
527
527
528 return changesettemplater(ui, repo, spec, *postargs)
528 return changesettemplater(ui, repo, spec, *postargs)
529
529
530 def _makematcher(repo, revs, pats, opts):
530 def _makematcher(repo, revs, pats, opts):
531 """Build matcher and expanded patterns from log options
531 """Build matcher and expanded patterns from log options
532
532
533 If --follow, revs are the revisions to follow from.
533 If --follow, revs are the revisions to follow from.
534
534
535 Returns (match, pats, slowpath) where
535 Returns (match, pats, slowpath) where
536 - match: a matcher built from the given pats and -I/-X opts
536 - match: a matcher built from the given pats and -I/-X opts
537 - pats: patterns used (globs are expanded on Windows)
537 - pats: patterns used (globs are expanded on Windows)
538 - slowpath: True if patterns aren't as simple as scanning filelogs
538 - slowpath: True if patterns aren't as simple as scanning filelogs
539 """
539 """
540 # pats/include/exclude are passed to match.match() directly in
540 # pats/include/exclude are passed to match.match() directly in
541 # _matchfiles() revset but walkchangerevs() builds its matcher with
541 # _matchfiles() revset but walkchangerevs() builds its matcher with
542 # scmutil.match(). The difference is input pats are globbed on
542 # scmutil.match(). The difference is input pats are globbed on
543 # platforms without shell expansion (windows).
543 # platforms without shell expansion (windows).
544 wctx = repo[None]
544 wctx = repo[None]
545 match, pats = scmutil.matchandpats(wctx, pats, opts)
545 match, pats = scmutil.matchandpats(wctx, pats, opts)
546 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
546 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
547 if not slowpath:
547 if not slowpath:
548 follow = opts.get('follow') or opts.get('follow_first')
548 follow = opts.get('follow') or opts.get('follow_first')
549 startctxs = []
549 startctxs = []
550 if follow and opts.get('rev'):
550 if follow and opts.get('rev'):
551 startctxs = [repo[r] for r in revs]
551 startctxs = [repo[r] for r in revs]
552 for f in match.files():
552 for f in match.files():
553 if follow and startctxs:
553 if follow and startctxs:
554 # No idea if the path was a directory at that revision, so
554 # No idea if the path was a directory at that revision, so
555 # take the slow path.
555 # take the slow path.
556 if any(f not in c for c in startctxs):
556 if any(f not in c for c in startctxs):
557 slowpath = True
557 slowpath = True
558 continue
558 continue
559 elif follow and f not in wctx:
559 elif follow and f not in wctx:
560 # If the file exists, it may be a directory, so let it
560 # If the file exists, it may be a directory, so let it
561 # take the slow path.
561 # take the slow path.
562 if os.path.exists(repo.wjoin(f)):
562 if os.path.exists(repo.wjoin(f)):
563 slowpath = True
563 slowpath = True
564 continue
564 continue
565 else:
565 else:
566 raise error.Abort(_('cannot follow file not in parent '
566 raise error.Abort(_('cannot follow file not in parent '
567 'revision: "%s"') % f)
567 'revision: "%s"') % f)
568 filelog = repo.file(f)
568 filelog = repo.file(f)
569 if not filelog:
569 if not filelog:
570 # A zero count may be a directory or deleted file, so
570 # A zero count may be a directory or deleted file, so
571 # try to find matching entries on the slow path.
571 # try to find matching entries on the slow path.
572 if follow:
572 if follow:
573 raise error.Abort(
573 raise error.Abort(
574 _('cannot follow nonexistent file: "%s"') % f)
574 _('cannot follow nonexistent file: "%s"') % f)
575 slowpath = True
575 slowpath = True
576
576
577 # We decided to fall back to the slowpath because at least one
577 # We decided to fall back to the slowpath because at least one
578 # of the paths was not a file. Check to see if at least one of them
578 # of the paths was not a file. Check to see if at least one of them
579 # existed in history - in that case, we'll continue down the
579 # existed in history - in that case, we'll continue down the
580 # slowpath; otherwise, we can turn off the slowpath
580 # slowpath; otherwise, we can turn off the slowpath
581 if slowpath:
581 if slowpath:
582 for path in match.files():
582 for path in match.files():
583 if path == '.' or path in repo.store:
583 if path == '.' or path in repo.store:
584 break
584 break
585 else:
585 else:
586 slowpath = False
586 slowpath = False
587
587
588 return match, pats, slowpath
588 return match, pats, slowpath
589
589
590 def _fileancestors(repo, revs, match, followfirst):
590 def _fileancestors(repo, revs, match, followfirst):
591 fctxs = []
591 fctxs = []
592 for r in revs:
592 for r in revs:
593 ctx = repo[r]
593 ctx = repo[r]
594 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
594 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
595
595
596 # When displaying a revision with --patch --follow FILE, we have
596 # When displaying a revision with --patch --follow FILE, we have
597 # to know which file of the revision must be diffed. With
597 # to know which file of the revision must be diffed. With
598 # --follow, we want the names of the ancestors of FILE in the
598 # --follow, we want the names of the ancestors of FILE in the
599 # revision, stored in "fcache". "fcache" is populated as a side effect
599 # revision, stored in "fcache". "fcache" is populated as a side effect
600 # of the graph traversal.
600 # of the graph traversal.
601 fcache = {}
601 fcache = {}
602 def filematcher(ctx):
602 def filematcher(ctx):
603 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
603 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
604
604
605 def revgen():
605 def revgen():
606 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
606 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
607 fcache[rev] = [c.path() for c in cs]
607 fcache[rev] = [c.path() for c in cs]
608 yield rev
608 yield rev
609 return smartset.generatorset(revgen(), iterasc=False), filematcher
609 return smartset.generatorset(revgen(), iterasc=False), filematcher
610
610
611 def _makenofollowfilematcher(repo, pats, opts):
611 def _makenofollowfilematcher(repo, pats, opts):
612 '''hook for extensions to override the filematcher for non-follow cases'''
612 '''hook for extensions to override the filematcher for non-follow cases'''
613 return None
613 return None
614
614
615 _opt2logrevset = {
615 _opt2logrevset = {
616 'no_merges': ('not merge()', None),
616 'no_merges': ('not merge()', None),
617 'only_merges': ('merge()', None),
617 'only_merges': ('merge()', None),
618 '_matchfiles': (None, '_matchfiles(%ps)'),
618 '_matchfiles': (None, '_matchfiles(%ps)'),
619 'date': ('date(%s)', None),
619 'date': ('date(%s)', None),
620 'branch': ('branch(%s)', '%lr'),
620 'branch': ('branch(%s)', '%lr'),
621 '_patslog': ('filelog(%s)', '%lr'),
621 '_patslog': ('filelog(%s)', '%lr'),
622 'keyword': ('keyword(%s)', '%lr'),
622 'keyword': ('keyword(%s)', '%lr'),
623 'prune': ('ancestors(%s)', 'not %lr'),
623 'prune': ('ancestors(%s)', 'not %lr'),
624 'user': ('user(%s)', '%lr'),
624 'user': ('user(%s)', '%lr'),
625 }
625 }
626
626
627 def _makerevset(repo, match, pats, slowpath, opts):
627 def _makerevset(repo, match, pats, slowpath, opts):
628 """Return a revset string built from log options and file patterns"""
628 """Return a revset string built from log options and file patterns"""
629 opts = dict(opts)
629 opts = dict(opts)
630 # follow or not follow?
630 # follow or not follow?
631 follow = opts.get('follow') or opts.get('follow_first')
631 follow = opts.get('follow') or opts.get('follow_first')
632
632
633 # branch and only_branch are really aliases and must be handled at
633 # branch and only_branch are really aliases and must be handled at
634 # the same time
634 # the same time
635 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
635 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
636 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
636 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
637
637
638 if slowpath:
638 if slowpath:
639 # See walkchangerevs() slow path.
639 # See walkchangerevs() slow path.
640 #
640 #
641 # pats/include/exclude cannot be represented as separate
641 # pats/include/exclude cannot be represented as separate
642 # revset expressions as their filtering logic applies at file
642 # revset expressions as their filtering logic applies at file
643 # level. For instance "-I a -X b" matches a revision touching
643 # level. For instance "-I a -X b" matches a revision touching
644 # "a" and "b" while "file(a) and not file(b)" does
644 # "a" and "b" while "file(a) and not file(b)" does
645 # not. Besides, filesets are evaluated against the working
645 # not. Besides, filesets are evaluated against the working
646 # directory.
646 # directory.
647 matchargs = ['r:', 'd:relpath']
647 matchargs = ['r:', 'd:relpath']
648 for p in pats:
648 for p in pats:
649 matchargs.append('p:' + p)
649 matchargs.append('p:' + p)
650 for p in opts.get('include', []):
650 for p in opts.get('include', []):
651 matchargs.append('i:' + p)
651 matchargs.append('i:' + p)
652 for p in opts.get('exclude', []):
652 for p in opts.get('exclude', []):
653 matchargs.append('x:' + p)
653 matchargs.append('x:' + p)
654 opts['_matchfiles'] = matchargs
654 opts['_matchfiles'] = matchargs
655 elif not follow:
655 elif not follow:
656 opts['_patslog'] = list(pats)
656 opts['_patslog'] = list(pats)
657
657
658 expr = []
658 expr = []
659 for op, val in sorted(opts.iteritems()):
659 for op, val in sorted(opts.iteritems()):
660 if not val:
660 if not val:
661 continue
661 continue
662 if op not in _opt2logrevset:
662 if op not in _opt2logrevset:
663 continue
663 continue
664 revop, listop = _opt2logrevset[op]
664 revop, listop = _opt2logrevset[op]
665 if revop and '%' not in revop:
665 if revop and '%' not in revop:
666 expr.append(revop)
666 expr.append(revop)
667 elif not listop:
667 elif not listop:
668 expr.append(revsetlang.formatspec(revop, val))
668 expr.append(revsetlang.formatspec(revop, val))
669 else:
669 else:
670 if revop:
670 if revop:
671 val = [revsetlang.formatspec(revop, v) for v in val]
671 val = [revsetlang.formatspec(revop, v) for v in val]
672 expr.append(revsetlang.formatspec(listop, val))
672 expr.append(revsetlang.formatspec(listop, val))
673
673
674 if expr:
674 if expr:
675 expr = '(' + ' and '.join(expr) + ')'
675 expr = '(' + ' and '.join(expr) + ')'
676 else:
676 else:
677 expr = None
677 expr = None
678 return expr
678 return expr
679
679
680 def _initialrevs(repo, opts):
680 def _initialrevs(repo, opts):
681 """Return the initial set of revisions to be filtered or followed"""
681 """Return the initial set of revisions to be filtered or followed"""
682 follow = opts.get('follow') or opts.get('follow_first')
682 follow = opts.get('follow') or opts.get('follow_first')
683 if opts.get('rev'):
683 if opts.get('rev'):
684 revs = scmutil.revrange(repo, opts['rev'])
684 revs = scmutil.revrange(repo, opts['rev'])
685 elif follow and repo.dirstate.p1() == nullid:
685 elif follow and repo.dirstate.p1() == nullid:
686 revs = smartset.baseset()
686 revs = smartset.baseset()
687 elif follow:
687 elif follow:
688 revs = repo.revs('.')
688 revs = repo.revs('.')
689 else:
689 else:
690 revs = smartset.spanset(repo)
690 revs = smartset.spanset(repo)
691 revs.reverse()
691 revs.reverse()
692 return revs
692 return revs
693
693
694 def getrevs(repo, pats, opts):
694 def getrevs(repo, pats, opts):
695 """Return (revs, differ) where revs is a smartset
695 """Return (revs, differ) where revs is a smartset
696
696
697 differ is a changesetdiffer with pre-configured file matcher.
697 differ is a changesetdiffer with pre-configured file matcher.
698 """
698 """
699 follow = opts.get('follow') or opts.get('follow_first')
699 follow = opts.get('follow') or opts.get('follow_first')
700 followfirst = opts.get('follow_first')
700 followfirst = opts.get('follow_first')
701 limit = getlimit(opts)
701 limit = getlimit(opts)
702 revs = _initialrevs(repo, opts)
702 revs = _initialrevs(repo, opts)
703 if not revs:
703 if not revs:
704 return smartset.baseset(), None
704 return smartset.baseset(), None
705 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
705 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
706 filematcher = None
706 filematcher = None
707 if follow:
707 if follow:
708 if slowpath or match.always():
708 if slowpath or match.always():
709 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
709 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
710 else:
710 else:
711 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
711 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
712 revs.reverse()
712 revs.reverse()
713 if filematcher is None:
713 if filematcher is None:
714 filematcher = _makenofollowfilematcher(repo, pats, opts)
714 filematcher = _makenofollowfilematcher(repo, pats, opts)
715 if filematcher is None:
715 if filematcher is None:
716 def filematcher(ctx):
716 def filematcher(ctx):
717 return match
717 return match
718
718
719 expr = _makerevset(repo, match, pats, slowpath, opts)
719 expr = _makerevset(repo, match, pats, slowpath, opts)
720 if opts.get('graph') and opts.get('rev'):
720 if opts.get('graph') and opts.get('rev'):
721 # User-specified revs might be unsorted, but don't sort before
721 # User-specified revs might be unsorted, but don't sort before
722 # _makerevset because it might depend on the order of revs
722 # _makerevset because it might depend on the order of revs
723 if not (revs.isdescending() or revs.istopo()):
723 if not (revs.isdescending() or revs.istopo()):
724 revs.sort(reverse=True)
724 revs.sort(reverse=True)
725 if expr:
725 if expr:
726 matcher = revset.match(None, expr)
726 matcher = revset.match(None, expr)
727 revs = matcher(repo, revs)
727 revs = matcher(repo, revs)
728 if limit is not None:
728 if limit is not None:
729 revs = revs.slice(0, limit)
729 revs = revs.slice(0, limit)
730
730
731 differ = changesetdiffer()
731 differ = changesetdiffer()
732 differ._makefilematcher = filematcher
732 differ._makefilematcher = filematcher
733 return revs, differ
733 return revs, differ
734
734
735 def _parselinerangeopt(repo, opts):
735 def _parselinerangeopt(repo, opts):
736 """Parse --line-range log option and return a list of tuples (filename,
736 """Parse --line-range log option and return a list of tuples (filename,
737 (fromline, toline)).
737 (fromline, toline)).
738 """
738 """
739 linerangebyfname = []
739 linerangebyfname = []
740 for pat in opts.get('line_range', []):
740 for pat in opts.get('line_range', []):
741 try:
741 try:
742 pat, linerange = pat.rsplit(',', 1)
742 pat, linerange = pat.rsplit(',', 1)
743 except ValueError:
743 except ValueError:
744 raise error.Abort(_('malformatted line-range pattern %s') % pat)
744 raise error.Abort(_('malformatted line-range pattern %s') % pat)
745 try:
745 try:
746 fromline, toline = map(int, linerange.split(':'))
746 fromline, toline = map(int, linerange.split(':'))
747 except ValueError:
747 except ValueError:
748 raise error.Abort(_("invalid line range for %s") % pat)
748 raise error.Abort(_("invalid line range for %s") % pat)
749 msg = _("line range pattern '%s' must match exactly one file") % pat
749 msg = _("line range pattern '%s' must match exactly one file") % pat
750 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
750 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
751 linerangebyfname.append(
751 linerangebyfname.append(
752 (fname, util.processlinerange(fromline, toline)))
752 (fname, util.processlinerange(fromline, toline)))
753 return linerangebyfname
753 return linerangebyfname
754
754
755 def getlinerangerevs(repo, userrevs, opts):
755 def getlinerangerevs(repo, userrevs, opts):
756 """Return (revs, differ).
756 """Return (revs, differ).
757
757
758 "revs" are revisions obtained by processing "line-range" log options and
758 "revs" are revisions obtained by processing "line-range" log options and
759 walking block ancestors of each specified file/line-range.
759 walking block ancestors of each specified file/line-range.
760
760
761 "differ" is a changesetdiffer with pre-configured file matcher and hunks
761 "differ" is a changesetdiffer with pre-configured file matcher and hunks
762 filter.
762 filter.
763 """
763 """
764 wctx = repo[None]
764 wctx = repo[None]
765
765
766 # Two-levels map of "rev -> file ctx -> [line range]".
766 # Two-levels map of "rev -> file ctx -> [line range]".
767 linerangesbyrev = {}
767 linerangesbyrev = {}
768 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
768 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
769 if fname not in wctx:
769 if fname not in wctx:
770 raise error.Abort(_('cannot follow file not in parent '
770 raise error.Abort(_('cannot follow file not in parent '
771 'revision: "%s"') % fname)
771 'revision: "%s"') % fname)
772 fctx = wctx.filectx(fname)
772 fctx = wctx.filectx(fname)
773 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
773 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
774 rev = fctx.introrev()
774 rev = fctx.introrev()
775 if rev not in userrevs:
775 if rev not in userrevs:
776 continue
776 continue
777 linerangesbyrev.setdefault(
777 linerangesbyrev.setdefault(
778 rev, {}).setdefault(
778 rev, {}).setdefault(
779 fctx.path(), []).append(linerange)
779 fctx.path(), []).append(linerange)
780
780
781 def nofilterhunksfn(fctx, hunks):
781 def nofilterhunksfn(fctx, hunks):
782 return hunks
782 return hunks
783
783
784 def hunksfilter(ctx):
784 def hunksfilter(ctx):
785 fctxlineranges = linerangesbyrev.get(ctx.rev())
785 fctxlineranges = linerangesbyrev.get(ctx.rev())
786 if fctxlineranges is None:
786 if fctxlineranges is None:
787 return nofilterhunksfn
787 return nofilterhunksfn
788
788
789 def filterfn(fctx, hunks):
789 def filterfn(fctx, hunks):
790 lineranges = fctxlineranges.get(fctx.path())
790 lineranges = fctxlineranges.get(fctx.path())
791 if lineranges is not None:
791 if lineranges is not None:
792 for hr, lines in hunks:
792 for hr, lines in hunks:
793 if hr is None: # binary
793 if hr is None: # binary
794 yield hr, lines
794 yield hr, lines
795 continue
795 continue
796 if any(mdiff.hunkinrange(hr[2:], lr)
796 if any(mdiff.hunkinrange(hr[2:], lr)
797 for lr in lineranges):
797 for lr in lineranges):
798 yield hr, lines
798 yield hr, lines
799 else:
799 else:
800 for hunk in hunks:
800 for hunk in hunks:
801 yield hunk
801 yield hunk
802
802
803 return filterfn
803 return filterfn
804
804
805 def filematcher(ctx):
805 def filematcher(ctx):
806 files = list(linerangesbyrev.get(ctx.rev(), []))
806 files = list(linerangesbyrev.get(ctx.rev(), []))
807 return scmutil.matchfiles(repo, files)
807 return scmutil.matchfiles(repo, files)
808
808
809 revs = sorted(linerangesbyrev, reverse=True)
809 revs = sorted(linerangesbyrev, reverse=True)
810
810
811 differ = changesetdiffer()
811 differ = changesetdiffer()
812 differ._makefilematcher = filematcher
812 differ._makefilematcher = filematcher
813 differ._makehunksfilter = hunksfilter
813 differ._makehunksfilter = hunksfilter
814 return revs, differ
814 return revs, differ
815
815
816 def _graphnodeformatter(ui, displayer):
816 def _graphnodeformatter(ui, displayer):
817 spec = ui.config('ui', 'graphnodetemplate')
817 spec = ui.config('ui', 'graphnodetemplate')
818 if not spec:
818 if not spec:
819 return templatekw.getgraphnode # fast path for "{graphnode}"
819 return templatekw.getgraphnode # fast path for "{graphnode}"
820
820
821 spec = templater.unquotestring(spec)
821 spec = templater.unquotestring(spec)
822 if isinstance(displayer, changesettemplater):
822 if isinstance(displayer, changesettemplater):
823 # reuse cache of slow templates
823 # reuse cache of slow templates
824 tres = displayer._tresources
824 tres = displayer._tresources
825 else:
825 else:
826 tres = formatter.templateresources(ui)
826 tres = formatter.templateresources(ui)
827 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
827 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
828 resources=tres)
828 resources=tres)
829 def formatnode(repo, ctx):
829 def formatnode(repo, ctx):
830 props = {'ctx': ctx, 'repo': repo}
830 props = {'ctx': ctx, 'repo': repo}
831 return templ.renderdefault(props)
831 return templ.renderdefault(props)
832 return formatnode
832 return formatnode
833
833
834 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
834 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
835 props = props or {}
835 props = props or {}
836 formatnode = _graphnodeformatter(ui, displayer)
836 formatnode = _graphnodeformatter(ui, displayer)
837 state = graphmod.asciistate()
837 state = graphmod.asciistate()
838 styles = state['styles']
838 styles = state['styles']
839
839
840 # only set graph styling if HGPLAIN is not set.
840 # only set graph styling if HGPLAIN is not set.
841 if ui.plain('graph'):
841 if ui.plain('graph'):
842 # set all edge styles to |, the default pre-3.8 behaviour
842 # set all edge styles to |, the default pre-3.8 behaviour
843 styles.update(dict.fromkeys(styles, '|'))
843 styles.update(dict.fromkeys(styles, '|'))
844 else:
844 else:
845 edgetypes = {
845 edgetypes = {
846 'parent': graphmod.PARENT,
846 'parent': graphmod.PARENT,
847 'grandparent': graphmod.GRANDPARENT,
847 'grandparent': graphmod.GRANDPARENT,
848 'missing': graphmod.MISSINGPARENT
848 'missing': graphmod.MISSINGPARENT
849 }
849 }
850 for name, key in edgetypes.items():
850 for name, key in edgetypes.items():
851 # experimental config: experimental.graphstyle.*
851 # experimental config: experimental.graphstyle.*
852 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
852 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
853 styles[key])
853 styles[key])
854 if not styles[key]:
854 if not styles[key]:
855 styles[key] = None
855 styles[key] = None
856
856
857 # experimental config: experimental.graphshorten
857 # experimental config: experimental.graphshorten
858 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
858 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
859
859
860 for rev, type, ctx, parents in dag:
860 for rev, type, ctx, parents in dag:
861 char = formatnode(repo, ctx)
861 char = formatnode(repo, ctx)
862 copies = None
862 copies = None
863 if getrenamed and ctx.rev():
863 if getrenamed and ctx.rev():
864 copies = []
864 copies = []
865 for fn in ctx.files():
865 for fn in ctx.files():
866 rename = getrenamed(fn, ctx.rev())
866 rename = getrenamed(fn, ctx.rev())
867 if rename:
867 if rename:
868 copies.append((fn, rename[0]))
868 copies.append((fn, rename))
869 edges = edgefn(type, char, state, rev, parents)
869 edges = edgefn(type, char, state, rev, parents)
870 firstedge = next(edges)
870 firstedge = next(edges)
871 width = firstedge[2]
871 width = firstedge[2]
872 displayer.show(ctx, copies=copies,
872 displayer.show(ctx, copies=copies,
873 graphwidth=width, **pycompat.strkwargs(props))
873 graphwidth=width, **pycompat.strkwargs(props))
874 lines = displayer.hunk.pop(rev).split('\n')
874 lines = displayer.hunk.pop(rev).split('\n')
875 if not lines[-1]:
875 if not lines[-1]:
876 del lines[-1]
876 del lines[-1]
877 displayer.flush(ctx)
877 displayer.flush(ctx)
878 for type, char, width, coldata in itertools.chain([firstedge], edges):
878 for type, char, width, coldata in itertools.chain([firstedge], edges):
879 graphmod.ascii(ui, state, type, char, lines, coldata)
879 graphmod.ascii(ui, state, type, char, lines, coldata)
880 lines = []
880 lines = []
881 displayer.close()
881 displayer.close()
882
882
883 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
883 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
884 revdag = graphmod.dagwalker(repo, revs)
884 revdag = graphmod.dagwalker(repo, revs)
885 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
885 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
886
886
887 def displayrevs(ui, repo, revs, displayer, getrenamed):
887 def displayrevs(ui, repo, revs, displayer, getrenamed):
888 for rev in revs:
888 for rev in revs:
889 ctx = repo[rev]
889 ctx = repo[rev]
890 copies = None
890 copies = None
891 if getrenamed is not None and rev:
891 if getrenamed is not None and rev:
892 copies = []
892 copies = []
893 for fn in ctx.files():
893 for fn in ctx.files():
894 rename = getrenamed(fn, rev)
894 rename = getrenamed(fn, rev)
895 if rename:
895 if rename:
896 copies.append((fn, rename[0]))
896 copies.append((fn, rename))
897 displayer.show(ctx, copies=copies)
897 displayer.show(ctx, copies=copies)
898 displayer.flush(ctx)
898 displayer.flush(ctx)
899 displayer.close()
899 displayer.close()
900
900
901 def checkunsupportedgraphflags(pats, opts):
901 def checkunsupportedgraphflags(pats, opts):
902 for op in ["newest_first"]:
902 for op in ["newest_first"]:
903 if op in opts and opts[op]:
903 if op in opts and opts[op]:
904 raise error.Abort(_("-G/--graph option is incompatible with --%s")
904 raise error.Abort(_("-G/--graph option is incompatible with --%s")
905 % op.replace("_", "-"))
905 % op.replace("_", "-"))
906
906
907 def graphrevs(repo, nodes, opts):
907 def graphrevs(repo, nodes, opts):
908 limit = getlimit(opts)
908 limit = getlimit(opts)
909 nodes.reverse()
909 nodes.reverse()
910 if limit is not None:
910 if limit is not None:
911 nodes = nodes[:limit]
911 nodes = nodes[:limit]
912 return graphmod.nodes(repo, nodes)
912 return graphmod.nodes(repo, nodes)
@@ -1,809 +1,810 b''
1 # templatekw.py - common changeset template keywords
1 # templatekw.py - common changeset template keywords
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from .i18n import _
10 from .i18n import _
11 from .node import (
11 from .node import (
12 hex,
12 hex,
13 nullid,
13 nullid,
14 )
14 )
15
15
16 from . import (
16 from . import (
17 encoding,
17 encoding,
18 error,
18 error,
19 hbisect,
19 hbisect,
20 i18n,
20 i18n,
21 obsutil,
21 obsutil,
22 patch,
22 patch,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 scmutil,
25 scmutil,
26 templateutil,
26 templateutil,
27 util,
27 util,
28 )
28 )
29 from .utils import (
29 from .utils import (
30 stringutil,
30 stringutil,
31 )
31 )
32
32
33 _hybrid = templateutil.hybrid
33 _hybrid = templateutil.hybrid
34 _mappable = templateutil.mappable
34 _mappable = templateutil.mappable
35 hybriddict = templateutil.hybriddict
35 hybriddict = templateutil.hybriddict
36 hybridlist = templateutil.hybridlist
36 hybridlist = templateutil.hybridlist
37 compatdict = templateutil.compatdict
37 compatdict = templateutil.compatdict
38 compatlist = templateutil.compatlist
38 compatlist = templateutil.compatlist
39 _showcompatlist = templateutil._showcompatlist
39 _showcompatlist = templateutil._showcompatlist
40
40
41 def getlatesttags(context, mapping, pattern=None):
41 def getlatesttags(context, mapping, pattern=None):
42 '''return date, distance and name for the latest tag of rev'''
42 '''return date, distance and name for the latest tag of rev'''
43 repo = context.resource(mapping, 'repo')
43 repo = context.resource(mapping, 'repo')
44 ctx = context.resource(mapping, 'ctx')
44 ctx = context.resource(mapping, 'ctx')
45 cache = context.resource(mapping, 'cache')
45 cache = context.resource(mapping, 'cache')
46
46
47 cachename = 'latesttags'
47 cachename = 'latesttags'
48 if pattern is not None:
48 if pattern is not None:
49 cachename += '-' + pattern
49 cachename += '-' + pattern
50 match = stringutil.stringmatcher(pattern)[2]
50 match = stringutil.stringmatcher(pattern)[2]
51 else:
51 else:
52 match = util.always
52 match = util.always
53
53
54 if cachename not in cache:
54 if cachename not in cache:
55 # Cache mapping from rev to a tuple with tag date, tag
55 # Cache mapping from rev to a tuple with tag date, tag
56 # distance and tag name
56 # distance and tag name
57 cache[cachename] = {-1: (0, 0, ['null'])}
57 cache[cachename] = {-1: (0, 0, ['null'])}
58 latesttags = cache[cachename]
58 latesttags = cache[cachename]
59
59
60 rev = ctx.rev()
60 rev = ctx.rev()
61 todo = [rev]
61 todo = [rev]
62 while todo:
62 while todo:
63 rev = todo.pop()
63 rev = todo.pop()
64 if rev in latesttags:
64 if rev in latesttags:
65 continue
65 continue
66 ctx = repo[rev]
66 ctx = repo[rev]
67 tags = [t for t in ctx.tags()
67 tags = [t for t in ctx.tags()
68 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
68 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
69 and match(t))]
69 and match(t))]
70 if tags:
70 if tags:
71 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
71 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
72 continue
72 continue
73 try:
73 try:
74 ptags = [latesttags[p.rev()] for p in ctx.parents()]
74 ptags = [latesttags[p.rev()] for p in ctx.parents()]
75 if len(ptags) > 1:
75 if len(ptags) > 1:
76 if ptags[0][2] == ptags[1][2]:
76 if ptags[0][2] == ptags[1][2]:
77 # The tuples are laid out so the right one can be found by
77 # The tuples are laid out so the right one can be found by
78 # comparison in this case.
78 # comparison in this case.
79 pdate, pdist, ptag = max(ptags)
79 pdate, pdist, ptag = max(ptags)
80 else:
80 else:
81 def key(x):
81 def key(x):
82 changessincetag = len(repo.revs('only(%d, %s)',
82 changessincetag = len(repo.revs('only(%d, %s)',
83 ctx.rev(), x[2][0]))
83 ctx.rev(), x[2][0]))
84 # Smallest number of changes since tag wins. Date is
84 # Smallest number of changes since tag wins. Date is
85 # used as tiebreaker.
85 # used as tiebreaker.
86 return [-changessincetag, x[0]]
86 return [-changessincetag, x[0]]
87 pdate, pdist, ptag = max(ptags, key=key)
87 pdate, pdist, ptag = max(ptags, key=key)
88 else:
88 else:
89 pdate, pdist, ptag = ptags[0]
89 pdate, pdist, ptag = ptags[0]
90 except KeyError:
90 except KeyError:
91 # Cache miss - recurse
91 # Cache miss - recurse
92 todo.append(rev)
92 todo.append(rev)
93 todo.extend(p.rev() for p in ctx.parents())
93 todo.extend(p.rev() for p in ctx.parents())
94 continue
94 continue
95 latesttags[rev] = pdate, pdist + 1, ptag
95 latesttags[rev] = pdate, pdist + 1, ptag
96 return latesttags[rev]
96 return latesttags[rev]
97
97
98 def getrenamedfn(repo, endrev=None):
98 def getrenamedfn(repo, endrev=None):
99 rcache = {}
99 rcache = {}
100 if endrev is None:
100 if endrev is None:
101 endrev = len(repo)
101 endrev = len(repo)
102
102
103 def getrenamed(fn, rev):
103 def getrenamed(fn, rev):
104 '''looks up all renames for a file (up to endrev) the first
104 '''looks up all renames for a file (up to endrev) the first
105 time the file is given. It indexes on the changerev and only
105 time the file is given. It indexes on the changerev and only
106 parses the manifest if linkrev != changerev.
106 parses the manifest if linkrev != changerev.
107 Returns rename info for fn at changerev rev.'''
107 Returns rename info for fn at changerev rev.'''
108 if fn not in rcache:
108 if fn not in rcache:
109 rcache[fn] = {}
109 rcache[fn] = {}
110 fl = repo.file(fn)
110 fl = repo.file(fn)
111 for i in fl:
111 for i in fl:
112 lr = fl.linkrev(i)
112 lr = fl.linkrev(i)
113 renamed = fl.renamed(fl.node(i))
113 renamed = fl.renamed(fl.node(i))
114 rcache[fn][lr] = renamed
114 rcache[fn][lr] = renamed and renamed[0]
115 if lr >= endrev:
115 if lr >= endrev:
116 break
116 break
117 if rev in rcache[fn]:
117 if rev in rcache[fn]:
118 return rcache[fn][rev]
118 return rcache[fn][rev]
119
119
120 # If linkrev != rev (i.e. rev not found in rcache) fallback to
120 # If linkrev != rev (i.e. rev not found in rcache) fallback to
121 # filectx logic.
121 # filectx logic.
122 try:
122 try:
123 return repo[rev][fn].renamed()
123 renamed = repo[rev][fn].renamed()
124 return renamed and renamed[0]
124 except error.LookupError:
125 except error.LookupError:
125 return None
126 return None
126
127
127 return getrenamed
128 return getrenamed
128
129
129 def getlogcolumns():
130 def getlogcolumns():
130 """Return a dict of log column labels"""
131 """Return a dict of log column labels"""
131 _ = pycompat.identity # temporarily disable gettext
132 _ = pycompat.identity # temporarily disable gettext
132 # i18n: column positioning for "hg log"
133 # i18n: column positioning for "hg log"
133 columns = _('bookmark: %s\n'
134 columns = _('bookmark: %s\n'
134 'branch: %s\n'
135 'branch: %s\n'
135 'changeset: %s\n'
136 'changeset: %s\n'
136 'copies: %s\n'
137 'copies: %s\n'
137 'date: %s\n'
138 'date: %s\n'
138 'extra: %s=%s\n'
139 'extra: %s=%s\n'
139 'files+: %s\n'
140 'files+: %s\n'
140 'files-: %s\n'
141 'files-: %s\n'
141 'files: %s\n'
142 'files: %s\n'
142 'instability: %s\n'
143 'instability: %s\n'
143 'manifest: %s\n'
144 'manifest: %s\n'
144 'obsolete: %s\n'
145 'obsolete: %s\n'
145 'parent: %s\n'
146 'parent: %s\n'
146 'phase: %s\n'
147 'phase: %s\n'
147 'summary: %s\n'
148 'summary: %s\n'
148 'tag: %s\n'
149 'tag: %s\n'
149 'user: %s\n')
150 'user: %s\n')
150 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
151 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
151 i18n._(columns).splitlines(True)))
152 i18n._(columns).splitlines(True)))
152
153
153 # default templates internally used for rendering of lists
154 # default templates internally used for rendering of lists
154 defaulttempl = {
155 defaulttempl = {
155 'parent': '{rev}:{node|formatnode} ',
156 'parent': '{rev}:{node|formatnode} ',
156 'manifest': '{rev}:{node|formatnode}',
157 'manifest': '{rev}:{node|formatnode}',
157 'file_copy': '{name} ({source})',
158 'file_copy': '{name} ({source})',
158 'envvar': '{key}={value}',
159 'envvar': '{key}={value}',
159 'extra': '{key}={value|stringescape}'
160 'extra': '{key}={value|stringescape}'
160 }
161 }
161 # filecopy is preserved for compatibility reasons
162 # filecopy is preserved for compatibility reasons
162 defaulttempl['filecopy'] = defaulttempl['file_copy']
163 defaulttempl['filecopy'] = defaulttempl['file_copy']
163
164
164 # keywords are callables (see registrar.templatekeyword for details)
165 # keywords are callables (see registrar.templatekeyword for details)
165 keywords = {}
166 keywords = {}
166 templatekeyword = registrar.templatekeyword(keywords)
167 templatekeyword = registrar.templatekeyword(keywords)
167
168
168 @templatekeyword('author', requires={'ctx'})
169 @templatekeyword('author', requires={'ctx'})
169 def showauthor(context, mapping):
170 def showauthor(context, mapping):
170 """String. The unmodified author of the changeset."""
171 """String. The unmodified author of the changeset."""
171 ctx = context.resource(mapping, 'ctx')
172 ctx = context.resource(mapping, 'ctx')
172 return ctx.user()
173 return ctx.user()
173
174
174 @templatekeyword('bisect', requires={'repo', 'ctx'})
175 @templatekeyword('bisect', requires={'repo', 'ctx'})
175 def showbisect(context, mapping):
176 def showbisect(context, mapping):
176 """String. The changeset bisection status."""
177 """String. The changeset bisection status."""
177 repo = context.resource(mapping, 'repo')
178 repo = context.resource(mapping, 'repo')
178 ctx = context.resource(mapping, 'ctx')
179 ctx = context.resource(mapping, 'ctx')
179 return hbisect.label(repo, ctx.node())
180 return hbisect.label(repo, ctx.node())
180
181
181 @templatekeyword('branch', requires={'ctx'})
182 @templatekeyword('branch', requires={'ctx'})
182 def showbranch(context, mapping):
183 def showbranch(context, mapping):
183 """String. The name of the branch on which the changeset was
184 """String. The name of the branch on which the changeset was
184 committed.
185 committed.
185 """
186 """
186 ctx = context.resource(mapping, 'ctx')
187 ctx = context.resource(mapping, 'ctx')
187 return ctx.branch()
188 return ctx.branch()
188
189
189 @templatekeyword('branches', requires={'ctx'})
190 @templatekeyword('branches', requires={'ctx'})
190 def showbranches(context, mapping):
191 def showbranches(context, mapping):
191 """List of strings. The name of the branch on which the
192 """List of strings. The name of the branch on which the
192 changeset was committed. Will be empty if the branch name was
193 changeset was committed. Will be empty if the branch name was
193 default. (DEPRECATED)
194 default. (DEPRECATED)
194 """
195 """
195 ctx = context.resource(mapping, 'ctx')
196 ctx = context.resource(mapping, 'ctx')
196 branch = ctx.branch()
197 branch = ctx.branch()
197 if branch != 'default':
198 if branch != 'default':
198 return compatlist(context, mapping, 'branch', [branch],
199 return compatlist(context, mapping, 'branch', [branch],
199 plural='branches')
200 plural='branches')
200 return compatlist(context, mapping, 'branch', [], plural='branches')
201 return compatlist(context, mapping, 'branch', [], plural='branches')
201
202
202 @templatekeyword('bookmarks', requires={'repo', 'ctx'})
203 @templatekeyword('bookmarks', requires={'repo', 'ctx'})
203 def showbookmarks(context, mapping):
204 def showbookmarks(context, mapping):
204 """List of strings. Any bookmarks associated with the
205 """List of strings. Any bookmarks associated with the
205 changeset. Also sets 'active', the name of the active bookmark.
206 changeset. Also sets 'active', the name of the active bookmark.
206 """
207 """
207 repo = context.resource(mapping, 'repo')
208 repo = context.resource(mapping, 'repo')
208 ctx = context.resource(mapping, 'ctx')
209 ctx = context.resource(mapping, 'ctx')
209 bookmarks = ctx.bookmarks()
210 bookmarks = ctx.bookmarks()
210 active = repo._activebookmark
211 active = repo._activebookmark
211 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
212 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
212 f = _showcompatlist(context, mapping, 'bookmark', bookmarks)
213 f = _showcompatlist(context, mapping, 'bookmark', bookmarks)
213 return _hybrid(f, bookmarks, makemap, pycompat.identity)
214 return _hybrid(f, bookmarks, makemap, pycompat.identity)
214
215
215 @templatekeyword('children', requires={'ctx'})
216 @templatekeyword('children', requires={'ctx'})
216 def showchildren(context, mapping):
217 def showchildren(context, mapping):
217 """List of strings. The children of the changeset."""
218 """List of strings. The children of the changeset."""
218 ctx = context.resource(mapping, 'ctx')
219 ctx = context.resource(mapping, 'ctx')
219 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
220 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
220 return compatlist(context, mapping, 'children', childrevs, element='child')
221 return compatlist(context, mapping, 'children', childrevs, element='child')
221
222
222 # Deprecated, but kept alive for help generation a purpose.
223 # Deprecated, but kept alive for help generation a purpose.
223 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
224 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
224 def showcurrentbookmark(context, mapping):
225 def showcurrentbookmark(context, mapping):
225 """String. The active bookmark, if it is associated with the changeset.
226 """String. The active bookmark, if it is associated with the changeset.
226 (DEPRECATED)"""
227 (DEPRECATED)"""
227 return showactivebookmark(context, mapping)
228 return showactivebookmark(context, mapping)
228
229
229 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
230 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
230 def showactivebookmark(context, mapping):
231 def showactivebookmark(context, mapping):
231 """String. The active bookmark, if it is associated with the changeset."""
232 """String. The active bookmark, if it is associated with the changeset."""
232 repo = context.resource(mapping, 'repo')
233 repo = context.resource(mapping, 'repo')
233 ctx = context.resource(mapping, 'ctx')
234 ctx = context.resource(mapping, 'ctx')
234 active = repo._activebookmark
235 active = repo._activebookmark
235 if active and active in ctx.bookmarks():
236 if active and active in ctx.bookmarks():
236 return active
237 return active
237 return ''
238 return ''
238
239
239 @templatekeyword('date', requires={'ctx'})
240 @templatekeyword('date', requires={'ctx'})
240 def showdate(context, mapping):
241 def showdate(context, mapping):
241 """Date information. The date when the changeset was committed."""
242 """Date information. The date when the changeset was committed."""
242 ctx = context.resource(mapping, 'ctx')
243 ctx = context.resource(mapping, 'ctx')
243 return ctx.date()
244 return ctx.date()
244
245
245 @templatekeyword('desc', requires={'ctx'})
246 @templatekeyword('desc', requires={'ctx'})
246 def showdescription(context, mapping):
247 def showdescription(context, mapping):
247 """String. The text of the changeset description."""
248 """String. The text of the changeset description."""
248 ctx = context.resource(mapping, 'ctx')
249 ctx = context.resource(mapping, 'ctx')
249 s = ctx.description()
250 s = ctx.description()
250 if isinstance(s, encoding.localstr):
251 if isinstance(s, encoding.localstr):
251 # try hard to preserve utf-8 bytes
252 # try hard to preserve utf-8 bytes
252 return encoding.tolocal(encoding.fromlocal(s).strip())
253 return encoding.tolocal(encoding.fromlocal(s).strip())
253 elif isinstance(s, encoding.safelocalstr):
254 elif isinstance(s, encoding.safelocalstr):
254 return encoding.safelocalstr(s.strip())
255 return encoding.safelocalstr(s.strip())
255 else:
256 else:
256 return s.strip()
257 return s.strip()
257
258
258 @templatekeyword('diffstat', requires={'ctx'})
259 @templatekeyword('diffstat', requires={'ctx'})
259 def showdiffstat(context, mapping):
260 def showdiffstat(context, mapping):
260 """String. Statistics of changes with the following format:
261 """String. Statistics of changes with the following format:
261 "modified files: +added/-removed lines"
262 "modified files: +added/-removed lines"
262 """
263 """
263 ctx = context.resource(mapping, 'ctx')
264 ctx = context.resource(mapping, 'ctx')
264 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
265 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
265 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
266 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
266 return '%d: +%d/-%d' % (len(stats), adds, removes)
267 return '%d: +%d/-%d' % (len(stats), adds, removes)
267
268
268 @templatekeyword('envvars', requires={'ui'})
269 @templatekeyword('envvars', requires={'ui'})
269 def showenvvars(context, mapping):
270 def showenvvars(context, mapping):
270 """A dictionary of environment variables. (EXPERIMENTAL)"""
271 """A dictionary of environment variables. (EXPERIMENTAL)"""
271 ui = context.resource(mapping, 'ui')
272 ui = context.resource(mapping, 'ui')
272 env = ui.exportableenviron()
273 env = ui.exportableenviron()
273 env = util.sortdict((k, env[k]) for k in sorted(env))
274 env = util.sortdict((k, env[k]) for k in sorted(env))
274 return compatdict(context, mapping, 'envvar', env, plural='envvars')
275 return compatdict(context, mapping, 'envvar', env, plural='envvars')
275
276
276 @templatekeyword('extras', requires={'ctx'})
277 @templatekeyword('extras', requires={'ctx'})
277 def showextras(context, mapping):
278 def showextras(context, mapping):
278 """List of dicts with key, value entries of the 'extras'
279 """List of dicts with key, value entries of the 'extras'
279 field of this changeset."""
280 field of this changeset."""
280 ctx = context.resource(mapping, 'ctx')
281 ctx = context.resource(mapping, 'ctx')
281 extras = ctx.extra()
282 extras = ctx.extra()
282 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
283 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
283 makemap = lambda k: {'key': k, 'value': extras[k]}
284 makemap = lambda k: {'key': k, 'value': extras[k]}
284 c = [makemap(k) for k in extras]
285 c = [makemap(k) for k in extras]
285 f = _showcompatlist(context, mapping, 'extra', c, plural='extras')
286 f = _showcompatlist(context, mapping, 'extra', c, plural='extras')
286 return _hybrid(f, extras, makemap,
287 return _hybrid(f, extras, makemap,
287 lambda k: '%s=%s' % (k, stringutil.escapestr(extras[k])))
288 lambda k: '%s=%s' % (k, stringutil.escapestr(extras[k])))
288
289
289 def _showfilesbystat(context, mapping, name, index):
290 def _showfilesbystat(context, mapping, name, index):
290 repo = context.resource(mapping, 'repo')
291 repo = context.resource(mapping, 'repo')
291 ctx = context.resource(mapping, 'ctx')
292 ctx = context.resource(mapping, 'ctx')
292 revcache = context.resource(mapping, 'revcache')
293 revcache = context.resource(mapping, 'revcache')
293 if 'files' not in revcache:
294 if 'files' not in revcache:
294 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
295 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
295 files = revcache['files'][index]
296 files = revcache['files'][index]
296 return compatlist(context, mapping, name, files, element='file')
297 return compatlist(context, mapping, name, files, element='file')
297
298
298 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache'})
299 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache'})
299 def showfileadds(context, mapping):
300 def showfileadds(context, mapping):
300 """List of strings. Files added by this changeset."""
301 """List of strings. Files added by this changeset."""
301 return _showfilesbystat(context, mapping, 'file_add', 1)
302 return _showfilesbystat(context, mapping, 'file_add', 1)
302
303
303 @templatekeyword('file_copies',
304 @templatekeyword('file_copies',
304 requires={'repo', 'ctx', 'cache', 'revcache'})
305 requires={'repo', 'ctx', 'cache', 'revcache'})
305 def showfilecopies(context, mapping):
306 def showfilecopies(context, mapping):
306 """List of strings. Files copied in this changeset with
307 """List of strings. Files copied in this changeset with
307 their sources.
308 their sources.
308 """
309 """
309 repo = context.resource(mapping, 'repo')
310 repo = context.resource(mapping, 'repo')
310 ctx = context.resource(mapping, 'ctx')
311 ctx = context.resource(mapping, 'ctx')
311 cache = context.resource(mapping, 'cache')
312 cache = context.resource(mapping, 'cache')
312 copies = context.resource(mapping, 'revcache').get('copies')
313 copies = context.resource(mapping, 'revcache').get('copies')
313 if copies is None:
314 if copies is None:
314 if 'getrenamed' not in cache:
315 if 'getrenamed' not in cache:
315 cache['getrenamed'] = getrenamedfn(repo)
316 cache['getrenamed'] = getrenamedfn(repo)
316 copies = []
317 copies = []
317 getrenamed = cache['getrenamed']
318 getrenamed = cache['getrenamed']
318 for fn in ctx.files():
319 for fn in ctx.files():
319 rename = getrenamed(fn, ctx.rev())
320 rename = getrenamed(fn, ctx.rev())
320 if rename:
321 if rename:
321 copies.append((fn, rename[0]))
322 copies.append((fn, rename))
322
323
323 copies = util.sortdict(copies)
324 copies = util.sortdict(copies)
324 return compatdict(context, mapping, 'file_copy', copies,
325 return compatdict(context, mapping, 'file_copy', copies,
325 key='name', value='source', fmt='%s (%s)',
326 key='name', value='source', fmt='%s (%s)',
326 plural='file_copies')
327 plural='file_copies')
327
328
328 # showfilecopiesswitch() displays file copies only if copy records are
329 # showfilecopiesswitch() displays file copies only if copy records are
329 # provided before calling the templater, usually with a --copies
330 # provided before calling the templater, usually with a --copies
330 # command line switch.
331 # command line switch.
331 @templatekeyword('file_copies_switch', requires={'revcache'})
332 @templatekeyword('file_copies_switch', requires={'revcache'})
332 def showfilecopiesswitch(context, mapping):
333 def showfilecopiesswitch(context, mapping):
333 """List of strings. Like "file_copies" but displayed
334 """List of strings. Like "file_copies" but displayed
334 only if the --copied switch is set.
335 only if the --copied switch is set.
335 """
336 """
336 copies = context.resource(mapping, 'revcache').get('copies') or []
337 copies = context.resource(mapping, 'revcache').get('copies') or []
337 copies = util.sortdict(copies)
338 copies = util.sortdict(copies)
338 return compatdict(context, mapping, 'file_copy', copies,
339 return compatdict(context, mapping, 'file_copy', copies,
339 key='name', value='source', fmt='%s (%s)',
340 key='name', value='source', fmt='%s (%s)',
340 plural='file_copies')
341 plural='file_copies')
341
342
342 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache'})
343 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache'})
343 def showfiledels(context, mapping):
344 def showfiledels(context, mapping):
344 """List of strings. Files removed by this changeset."""
345 """List of strings. Files removed by this changeset."""
345 return _showfilesbystat(context, mapping, 'file_del', 2)
346 return _showfilesbystat(context, mapping, 'file_del', 2)
346
347
347 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache'})
348 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache'})
348 def showfilemods(context, mapping):
349 def showfilemods(context, mapping):
349 """List of strings. Files modified by this changeset."""
350 """List of strings. Files modified by this changeset."""
350 return _showfilesbystat(context, mapping, 'file_mod', 0)
351 return _showfilesbystat(context, mapping, 'file_mod', 0)
351
352
352 @templatekeyword('files', requires={'ctx'})
353 @templatekeyword('files', requires={'ctx'})
353 def showfiles(context, mapping):
354 def showfiles(context, mapping):
354 """List of strings. All files modified, added, or removed by this
355 """List of strings. All files modified, added, or removed by this
355 changeset.
356 changeset.
356 """
357 """
357 ctx = context.resource(mapping, 'ctx')
358 ctx = context.resource(mapping, 'ctx')
358 return compatlist(context, mapping, 'file', ctx.files())
359 return compatlist(context, mapping, 'file', ctx.files())
359
360
360 @templatekeyword('graphnode', requires={'repo', 'ctx'})
361 @templatekeyword('graphnode', requires={'repo', 'ctx'})
361 def showgraphnode(context, mapping):
362 def showgraphnode(context, mapping):
362 """String. The character representing the changeset node in an ASCII
363 """String. The character representing the changeset node in an ASCII
363 revision graph."""
364 revision graph."""
364 repo = context.resource(mapping, 'repo')
365 repo = context.resource(mapping, 'repo')
365 ctx = context.resource(mapping, 'ctx')
366 ctx = context.resource(mapping, 'ctx')
366 return getgraphnode(repo, ctx)
367 return getgraphnode(repo, ctx)
367
368
368 def getgraphnode(repo, ctx):
369 def getgraphnode(repo, ctx):
369 return getgraphnodecurrent(repo, ctx) or getgraphnodesymbol(ctx)
370 return getgraphnodecurrent(repo, ctx) or getgraphnodesymbol(ctx)
370
371
371 def getgraphnodecurrent(repo, ctx):
372 def getgraphnodecurrent(repo, ctx):
372 wpnodes = repo.dirstate.parents()
373 wpnodes = repo.dirstate.parents()
373 if wpnodes[1] == nullid:
374 if wpnodes[1] == nullid:
374 wpnodes = wpnodes[:1]
375 wpnodes = wpnodes[:1]
375 if ctx.node() in wpnodes:
376 if ctx.node() in wpnodes:
376 return '@'
377 return '@'
377 else:
378 else:
378 return ''
379 return ''
379
380
380 def getgraphnodesymbol(ctx):
381 def getgraphnodesymbol(ctx):
381 if ctx.obsolete():
382 if ctx.obsolete():
382 return 'x'
383 return 'x'
383 elif ctx.isunstable():
384 elif ctx.isunstable():
384 return '*'
385 return '*'
385 elif ctx.closesbranch():
386 elif ctx.closesbranch():
386 return '_'
387 return '_'
387 else:
388 else:
388 return 'o'
389 return 'o'
389
390
390 @templatekeyword('graphwidth', requires=())
391 @templatekeyword('graphwidth', requires=())
391 def showgraphwidth(context, mapping):
392 def showgraphwidth(context, mapping):
392 """Integer. The width of the graph drawn by 'log --graph' or zero."""
393 """Integer. The width of the graph drawn by 'log --graph' or zero."""
393 # just hosts documentation; should be overridden by template mapping
394 # just hosts documentation; should be overridden by template mapping
394 return 0
395 return 0
395
396
396 @templatekeyword('index', requires=())
397 @templatekeyword('index', requires=())
397 def showindex(context, mapping):
398 def showindex(context, mapping):
398 """Integer. The current iteration of the loop. (0 indexed)"""
399 """Integer. The current iteration of the loop. (0 indexed)"""
399 # just hosts documentation; should be overridden by template mapping
400 # just hosts documentation; should be overridden by template mapping
400 raise error.Abort(_("can't use index in this context"))
401 raise error.Abort(_("can't use index in this context"))
401
402
402 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache'})
403 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache'})
403 def showlatesttag(context, mapping):
404 def showlatesttag(context, mapping):
404 """List of strings. The global tags on the most recent globally
405 """List of strings. The global tags on the most recent globally
405 tagged ancestor of this changeset. If no such tags exist, the list
406 tagged ancestor of this changeset. If no such tags exist, the list
406 consists of the single string "null".
407 consists of the single string "null".
407 """
408 """
408 return showlatesttags(context, mapping, None)
409 return showlatesttags(context, mapping, None)
409
410
410 def showlatesttags(context, mapping, pattern):
411 def showlatesttags(context, mapping, pattern):
411 """helper method for the latesttag keyword and function"""
412 """helper method for the latesttag keyword and function"""
412 latesttags = getlatesttags(context, mapping, pattern)
413 latesttags = getlatesttags(context, mapping, pattern)
413
414
414 # latesttag[0] is an implementation detail for sorting csets on different
415 # latesttag[0] is an implementation detail for sorting csets on different
415 # branches in a stable manner- it is the date the tagged cset was created,
416 # branches in a stable manner- it is the date the tagged cset was created,
416 # not the date the tag was created. Therefore it isn't made visible here.
417 # not the date the tag was created. Therefore it isn't made visible here.
417 makemap = lambda v: {
418 makemap = lambda v: {
418 'changes': _showchangessincetag,
419 'changes': _showchangessincetag,
419 'distance': latesttags[1],
420 'distance': latesttags[1],
420 'latesttag': v, # BC with {latesttag % '{latesttag}'}
421 'latesttag': v, # BC with {latesttag % '{latesttag}'}
421 'tag': v
422 'tag': v
422 }
423 }
423
424
424 tags = latesttags[2]
425 tags = latesttags[2]
425 f = _showcompatlist(context, mapping, 'latesttag', tags, separator=':')
426 f = _showcompatlist(context, mapping, 'latesttag', tags, separator=':')
426 return _hybrid(f, tags, makemap, pycompat.identity)
427 return _hybrid(f, tags, makemap, pycompat.identity)
427
428
428 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
429 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
429 def showlatesttagdistance(context, mapping):
430 def showlatesttagdistance(context, mapping):
430 """Integer. Longest path to the latest tag."""
431 """Integer. Longest path to the latest tag."""
431 return getlatesttags(context, mapping)[1]
432 return getlatesttags(context, mapping)[1]
432
433
433 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
434 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
434 def showchangessincelatesttag(context, mapping):
435 def showchangessincelatesttag(context, mapping):
435 """Integer. All ancestors not in the latest tag."""
436 """Integer. All ancestors not in the latest tag."""
436 tag = getlatesttags(context, mapping)[2][0]
437 tag = getlatesttags(context, mapping)[2][0]
437 mapping = context.overlaymap(mapping, {'tag': tag})
438 mapping = context.overlaymap(mapping, {'tag': tag})
438 return _showchangessincetag(context, mapping)
439 return _showchangessincetag(context, mapping)
439
440
440 def _showchangessincetag(context, mapping):
441 def _showchangessincetag(context, mapping):
441 repo = context.resource(mapping, 'repo')
442 repo = context.resource(mapping, 'repo')
442 ctx = context.resource(mapping, 'ctx')
443 ctx = context.resource(mapping, 'ctx')
443 offset = 0
444 offset = 0
444 revs = [ctx.rev()]
445 revs = [ctx.rev()]
445 tag = context.symbol(mapping, 'tag')
446 tag = context.symbol(mapping, 'tag')
446
447
447 # The only() revset doesn't currently support wdir()
448 # The only() revset doesn't currently support wdir()
448 if ctx.rev() is None:
449 if ctx.rev() is None:
449 offset = 1
450 offset = 1
450 revs = [p.rev() for p in ctx.parents()]
451 revs = [p.rev() for p in ctx.parents()]
451
452
452 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
453 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
453
454
454 # teach templater latesttags.changes is switched to (context, mapping) API
455 # teach templater latesttags.changes is switched to (context, mapping) API
455 _showchangessincetag._requires = {'repo', 'ctx'}
456 _showchangessincetag._requires = {'repo', 'ctx'}
456
457
457 @templatekeyword('manifest', requires={'repo', 'ctx'})
458 @templatekeyword('manifest', requires={'repo', 'ctx'})
458 def showmanifest(context, mapping):
459 def showmanifest(context, mapping):
459 repo = context.resource(mapping, 'repo')
460 repo = context.resource(mapping, 'repo')
460 ctx = context.resource(mapping, 'ctx')
461 ctx = context.resource(mapping, 'ctx')
461 mnode = ctx.manifestnode()
462 mnode = ctx.manifestnode()
462 if mnode is None:
463 if mnode is None:
463 # just avoid crash, we might want to use the 'ff...' hash in future
464 # just avoid crash, we might want to use the 'ff...' hash in future
464 return
465 return
465 mrev = repo.manifestlog._revlog.rev(mnode)
466 mrev = repo.manifestlog._revlog.rev(mnode)
466 mhex = hex(mnode)
467 mhex = hex(mnode)
467 mapping = context.overlaymap(mapping, {'rev': mrev, 'node': mhex})
468 mapping = context.overlaymap(mapping, {'rev': mrev, 'node': mhex})
468 f = context.process('manifest', mapping)
469 f = context.process('manifest', mapping)
469 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
470 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
470 # rev and node are completely different from changeset's.
471 # rev and node are completely different from changeset's.
471 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
472 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
472
473
473 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx'})
474 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx'})
474 def showobsfate(context, mapping):
475 def showobsfate(context, mapping):
475 # this function returns a list containing pre-formatted obsfate strings.
476 # this function returns a list containing pre-formatted obsfate strings.
476 #
477 #
477 # This function will be replaced by templates fragments when we will have
478 # This function will be replaced by templates fragments when we will have
478 # the verbosity templatekw available.
479 # the verbosity templatekw available.
479 succsandmarkers = showsuccsandmarkers(context, mapping)
480 succsandmarkers = showsuccsandmarkers(context, mapping)
480
481
481 ui = context.resource(mapping, 'ui')
482 ui = context.resource(mapping, 'ui')
482 repo = context.resource(mapping, 'repo')
483 repo = context.resource(mapping, 'repo')
483 values = []
484 values = []
484
485
485 for x in succsandmarkers.tovalue(context, mapping):
486 for x in succsandmarkers.tovalue(context, mapping):
486 v = obsutil.obsfateprinter(ui, repo, x['successors'], x['markers'],
487 v = obsutil.obsfateprinter(ui, repo, x['successors'], x['markers'],
487 scmutil.formatchangeid)
488 scmutil.formatchangeid)
488 values.append(v)
489 values.append(v)
489
490
490 return compatlist(context, mapping, "fate", values)
491 return compatlist(context, mapping, "fate", values)
491
492
492 def shownames(context, mapping, namespace):
493 def shownames(context, mapping, namespace):
493 """helper method to generate a template keyword for a namespace"""
494 """helper method to generate a template keyword for a namespace"""
494 repo = context.resource(mapping, 'repo')
495 repo = context.resource(mapping, 'repo')
495 ctx = context.resource(mapping, 'ctx')
496 ctx = context.resource(mapping, 'ctx')
496 ns = repo.names[namespace]
497 ns = repo.names[namespace]
497 names = ns.names(repo, ctx.node())
498 names = ns.names(repo, ctx.node())
498 return compatlist(context, mapping, ns.templatename, names,
499 return compatlist(context, mapping, ns.templatename, names,
499 plural=namespace)
500 plural=namespace)
500
501
501 @templatekeyword('namespaces', requires={'repo', 'ctx'})
502 @templatekeyword('namespaces', requires={'repo', 'ctx'})
502 def shownamespaces(context, mapping):
503 def shownamespaces(context, mapping):
503 """Dict of lists. Names attached to this changeset per
504 """Dict of lists. Names attached to this changeset per
504 namespace."""
505 namespace."""
505 repo = context.resource(mapping, 'repo')
506 repo = context.resource(mapping, 'repo')
506 ctx = context.resource(mapping, 'ctx')
507 ctx = context.resource(mapping, 'ctx')
507
508
508 namespaces = util.sortdict()
509 namespaces = util.sortdict()
509 def makensmapfn(ns):
510 def makensmapfn(ns):
510 # 'name' for iterating over namespaces, templatename for local reference
511 # 'name' for iterating over namespaces, templatename for local reference
511 return lambda v: {'name': v, ns.templatename: v}
512 return lambda v: {'name': v, ns.templatename: v}
512
513
513 for k, ns in repo.names.iteritems():
514 for k, ns in repo.names.iteritems():
514 names = ns.names(repo, ctx.node())
515 names = ns.names(repo, ctx.node())
515 f = _showcompatlist(context, mapping, 'name', names)
516 f = _showcompatlist(context, mapping, 'name', names)
516 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
517 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
517
518
518 f = _showcompatlist(context, mapping, 'namespace', list(namespaces))
519 f = _showcompatlist(context, mapping, 'namespace', list(namespaces))
519
520
520 def makemap(ns):
521 def makemap(ns):
521 return {
522 return {
522 'namespace': ns,
523 'namespace': ns,
523 'names': namespaces[ns],
524 'names': namespaces[ns],
524 'builtin': repo.names[ns].builtin,
525 'builtin': repo.names[ns].builtin,
525 'colorname': repo.names[ns].colorname,
526 'colorname': repo.names[ns].colorname,
526 }
527 }
527
528
528 return _hybrid(f, namespaces, makemap, pycompat.identity)
529 return _hybrid(f, namespaces, makemap, pycompat.identity)
529
530
530 @templatekeyword('node', requires={'ctx'})
531 @templatekeyword('node', requires={'ctx'})
531 def shownode(context, mapping):
532 def shownode(context, mapping):
532 """String. The changeset identification hash, as a 40 hexadecimal
533 """String. The changeset identification hash, as a 40 hexadecimal
533 digit string.
534 digit string.
534 """
535 """
535 ctx = context.resource(mapping, 'ctx')
536 ctx = context.resource(mapping, 'ctx')
536 return ctx.hex()
537 return ctx.hex()
537
538
538 @templatekeyword('obsolete', requires={'ctx'})
539 @templatekeyword('obsolete', requires={'ctx'})
539 def showobsolete(context, mapping):
540 def showobsolete(context, mapping):
540 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
541 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
541 ctx = context.resource(mapping, 'ctx')
542 ctx = context.resource(mapping, 'ctx')
542 if ctx.obsolete():
543 if ctx.obsolete():
543 return 'obsolete'
544 return 'obsolete'
544 return ''
545 return ''
545
546
546 @templatekeyword('peerurls', requires={'repo'})
547 @templatekeyword('peerurls', requires={'repo'})
547 def showpeerurls(context, mapping):
548 def showpeerurls(context, mapping):
548 """A dictionary of repository locations defined in the [paths] section
549 """A dictionary of repository locations defined in the [paths] section
549 of your configuration file."""
550 of your configuration file."""
550 repo = context.resource(mapping, 'repo')
551 repo = context.resource(mapping, 'repo')
551 # see commands.paths() for naming of dictionary keys
552 # see commands.paths() for naming of dictionary keys
552 paths = repo.ui.paths
553 paths = repo.ui.paths
553 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
554 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
554 def makemap(k):
555 def makemap(k):
555 p = paths[k]
556 p = paths[k]
556 d = {'name': k, 'url': p.rawloc}
557 d = {'name': k, 'url': p.rawloc}
557 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
558 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
558 return d
559 return d
559 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
560 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
560
561
561 @templatekeyword("predecessors", requires={'repo', 'ctx'})
562 @templatekeyword("predecessors", requires={'repo', 'ctx'})
562 def showpredecessors(context, mapping):
563 def showpredecessors(context, mapping):
563 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
564 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
564 repo = context.resource(mapping, 'repo')
565 repo = context.resource(mapping, 'repo')
565 ctx = context.resource(mapping, 'ctx')
566 ctx = context.resource(mapping, 'ctx')
566 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
567 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
567 predecessors = map(hex, predecessors)
568 predecessors = map(hex, predecessors)
568
569
569 return _hybrid(None, predecessors,
570 return _hybrid(None, predecessors,
570 lambda x: {'ctx': repo[x]},
571 lambda x: {'ctx': repo[x]},
571 lambda x: scmutil.formatchangeid(repo[x]))
572 lambda x: scmutil.formatchangeid(repo[x]))
572
573
573 @templatekeyword('reporoot', requires={'repo'})
574 @templatekeyword('reporoot', requires={'repo'})
574 def showreporoot(context, mapping):
575 def showreporoot(context, mapping):
575 """String. The root directory of the current repository."""
576 """String. The root directory of the current repository."""
576 repo = context.resource(mapping, 'repo')
577 repo = context.resource(mapping, 'repo')
577 return repo.root
578 return repo.root
578
579
579 @templatekeyword("successorssets", requires={'repo', 'ctx'})
580 @templatekeyword("successorssets", requires={'repo', 'ctx'})
580 def showsuccessorssets(context, mapping):
581 def showsuccessorssets(context, mapping):
581 """Returns a string of sets of successors for a changectx. Format used
582 """Returns a string of sets of successors for a changectx. Format used
582 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
583 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
583 while also diverged into ctx3. (EXPERIMENTAL)"""
584 while also diverged into ctx3. (EXPERIMENTAL)"""
584 repo = context.resource(mapping, 'repo')
585 repo = context.resource(mapping, 'repo')
585 ctx = context.resource(mapping, 'ctx')
586 ctx = context.resource(mapping, 'ctx')
586 if not ctx.obsolete():
587 if not ctx.obsolete():
587 return ''
588 return ''
588
589
589 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
590 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
590 ssets = [[hex(n) for n in ss] for ss in ssets]
591 ssets = [[hex(n) for n in ss] for ss in ssets]
591
592
592 data = []
593 data = []
593 for ss in ssets:
594 for ss in ssets:
594 h = _hybrid(None, ss, lambda x: {'ctx': repo[x]},
595 h = _hybrid(None, ss, lambda x: {'ctx': repo[x]},
595 lambda x: scmutil.formatchangeid(repo[x]))
596 lambda x: scmutil.formatchangeid(repo[x]))
596 data.append(h)
597 data.append(h)
597
598
598 # Format the successorssets
599 # Format the successorssets
599 def render(d):
600 def render(d):
600 return templateutil.stringify(context, mapping, d)
601 return templateutil.stringify(context, mapping, d)
601
602
602 def gen(data):
603 def gen(data):
603 yield "; ".join(render(d) for d in data)
604 yield "; ".join(render(d) for d in data)
604
605
605 return _hybrid(gen(data), data, lambda x: {'successorset': x},
606 return _hybrid(gen(data), data, lambda x: {'successorset': x},
606 pycompat.identity)
607 pycompat.identity)
607
608
608 @templatekeyword("succsandmarkers", requires={'repo', 'ctx'})
609 @templatekeyword("succsandmarkers", requires={'repo', 'ctx'})
609 def showsuccsandmarkers(context, mapping):
610 def showsuccsandmarkers(context, mapping):
610 """Returns a list of dict for each final successor of ctx. The dict
611 """Returns a list of dict for each final successor of ctx. The dict
611 contains successors node id in "successors" keys and the list of
612 contains successors node id in "successors" keys and the list of
612 obs-markers from ctx to the set of successors in "markers".
613 obs-markers from ctx to the set of successors in "markers".
613 (EXPERIMENTAL)
614 (EXPERIMENTAL)
614 """
615 """
615 repo = context.resource(mapping, 'repo')
616 repo = context.resource(mapping, 'repo')
616 ctx = context.resource(mapping, 'ctx')
617 ctx = context.resource(mapping, 'ctx')
617
618
618 values = obsutil.successorsandmarkers(repo, ctx)
619 values = obsutil.successorsandmarkers(repo, ctx)
619
620
620 if values is None:
621 if values is None:
621 values = []
622 values = []
622
623
623 # Format successors and markers to avoid exposing binary to templates
624 # Format successors and markers to avoid exposing binary to templates
624 data = []
625 data = []
625 for i in values:
626 for i in values:
626 # Format successors
627 # Format successors
627 successors = i['successors']
628 successors = i['successors']
628
629
629 successors = [hex(n) for n in successors]
630 successors = [hex(n) for n in successors]
630 successors = _hybrid(None, successors,
631 successors = _hybrid(None, successors,
631 lambda x: {'ctx': repo[x]},
632 lambda x: {'ctx': repo[x]},
632 lambda x: scmutil.formatchangeid(repo[x]))
633 lambda x: scmutil.formatchangeid(repo[x]))
633
634
634 # Format markers
635 # Format markers
635 finalmarkers = []
636 finalmarkers = []
636 for m in i['markers']:
637 for m in i['markers']:
637 hexprec = hex(m[0])
638 hexprec = hex(m[0])
638 hexsucs = tuple(hex(n) for n in m[1])
639 hexsucs = tuple(hex(n) for n in m[1])
639 hexparents = None
640 hexparents = None
640 if m[5] is not None:
641 if m[5] is not None:
641 hexparents = tuple(hex(n) for n in m[5])
642 hexparents = tuple(hex(n) for n in m[5])
642 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
643 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
643 finalmarkers.append(newmarker)
644 finalmarkers.append(newmarker)
644
645
645 data.append({'successors': successors, 'markers': finalmarkers})
646 data.append({'successors': successors, 'markers': finalmarkers})
646
647
647 return templateutil.mappinglist(data)
648 return templateutil.mappinglist(data)
648
649
649 @templatekeyword('p1rev', requires={'ctx'})
650 @templatekeyword('p1rev', requires={'ctx'})
650 def showp1rev(context, mapping):
651 def showp1rev(context, mapping):
651 """Integer. The repository-local revision number of the changeset's
652 """Integer. The repository-local revision number of the changeset's
652 first parent, or -1 if the changeset has no parents."""
653 first parent, or -1 if the changeset has no parents."""
653 ctx = context.resource(mapping, 'ctx')
654 ctx = context.resource(mapping, 'ctx')
654 return ctx.p1().rev()
655 return ctx.p1().rev()
655
656
656 @templatekeyword('p2rev', requires={'ctx'})
657 @templatekeyword('p2rev', requires={'ctx'})
657 def showp2rev(context, mapping):
658 def showp2rev(context, mapping):
658 """Integer. The repository-local revision number of the changeset's
659 """Integer. The repository-local revision number of the changeset's
659 second parent, or -1 if the changeset has no second parent."""
660 second parent, or -1 if the changeset has no second parent."""
660 ctx = context.resource(mapping, 'ctx')
661 ctx = context.resource(mapping, 'ctx')
661 return ctx.p2().rev()
662 return ctx.p2().rev()
662
663
663 @templatekeyword('p1node', requires={'ctx'})
664 @templatekeyword('p1node', requires={'ctx'})
664 def showp1node(context, mapping):
665 def showp1node(context, mapping):
665 """String. The identification hash of the changeset's first parent,
666 """String. The identification hash of the changeset's first parent,
666 as a 40 digit hexadecimal string. If the changeset has no parents, all
667 as a 40 digit hexadecimal string. If the changeset has no parents, all
667 digits are 0."""
668 digits are 0."""
668 ctx = context.resource(mapping, 'ctx')
669 ctx = context.resource(mapping, 'ctx')
669 return ctx.p1().hex()
670 return ctx.p1().hex()
670
671
671 @templatekeyword('p2node', requires={'ctx'})
672 @templatekeyword('p2node', requires={'ctx'})
672 def showp2node(context, mapping):
673 def showp2node(context, mapping):
673 """String. The identification hash of the changeset's second
674 """String. The identification hash of the changeset's second
674 parent, as a 40 digit hexadecimal string. If the changeset has no second
675 parent, as a 40 digit hexadecimal string. If the changeset has no second
675 parent, all digits are 0."""
676 parent, all digits are 0."""
676 ctx = context.resource(mapping, 'ctx')
677 ctx = context.resource(mapping, 'ctx')
677 return ctx.p2().hex()
678 return ctx.p2().hex()
678
679
679 @templatekeyword('parents', requires={'repo', 'ctx'})
680 @templatekeyword('parents', requires={'repo', 'ctx'})
680 def showparents(context, mapping):
681 def showparents(context, mapping):
681 """List of strings. The parents of the changeset in "rev:node"
682 """List of strings. The parents of the changeset in "rev:node"
682 format. If the changeset has only one "natural" parent (the predecessor
683 format. If the changeset has only one "natural" parent (the predecessor
683 revision) nothing is shown."""
684 revision) nothing is shown."""
684 repo = context.resource(mapping, 'repo')
685 repo = context.resource(mapping, 'repo')
685 ctx = context.resource(mapping, 'ctx')
686 ctx = context.resource(mapping, 'ctx')
686 pctxs = scmutil.meaningfulparents(repo, ctx)
687 pctxs = scmutil.meaningfulparents(repo, ctx)
687 prevs = [p.rev() for p in pctxs]
688 prevs = [p.rev() for p in pctxs]
688 parents = [[('rev', p.rev()),
689 parents = [[('rev', p.rev()),
689 ('node', p.hex()),
690 ('node', p.hex()),
690 ('phase', p.phasestr())]
691 ('phase', p.phasestr())]
691 for p in pctxs]
692 for p in pctxs]
692 f = _showcompatlist(context, mapping, 'parent', parents)
693 f = _showcompatlist(context, mapping, 'parent', parents)
693 return _hybrid(f, prevs, lambda x: {'ctx': repo[x]},
694 return _hybrid(f, prevs, lambda x: {'ctx': repo[x]},
694 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
695 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
695
696
696 @templatekeyword('phase', requires={'ctx'})
697 @templatekeyword('phase', requires={'ctx'})
697 def showphase(context, mapping):
698 def showphase(context, mapping):
698 """String. The changeset phase name."""
699 """String. The changeset phase name."""
699 ctx = context.resource(mapping, 'ctx')
700 ctx = context.resource(mapping, 'ctx')
700 return ctx.phasestr()
701 return ctx.phasestr()
701
702
702 @templatekeyword('phaseidx', requires={'ctx'})
703 @templatekeyword('phaseidx', requires={'ctx'})
703 def showphaseidx(context, mapping):
704 def showphaseidx(context, mapping):
704 """Integer. The changeset phase index. (ADVANCED)"""
705 """Integer. The changeset phase index. (ADVANCED)"""
705 ctx = context.resource(mapping, 'ctx')
706 ctx = context.resource(mapping, 'ctx')
706 return ctx.phase()
707 return ctx.phase()
707
708
708 @templatekeyword('rev', requires={'ctx'})
709 @templatekeyword('rev', requires={'ctx'})
709 def showrev(context, mapping):
710 def showrev(context, mapping):
710 """Integer. The repository-local changeset revision number."""
711 """Integer. The repository-local changeset revision number."""
711 ctx = context.resource(mapping, 'ctx')
712 ctx = context.resource(mapping, 'ctx')
712 return scmutil.intrev(ctx)
713 return scmutil.intrev(ctx)
713
714
714 def showrevslist(context, mapping, name, revs):
715 def showrevslist(context, mapping, name, revs):
715 """helper to generate a list of revisions in which a mapped template will
716 """helper to generate a list of revisions in which a mapped template will
716 be evaluated"""
717 be evaluated"""
717 repo = context.resource(mapping, 'repo')
718 repo = context.resource(mapping, 'repo')
718 f = _showcompatlist(context, mapping, name, ['%d' % r for r in revs])
719 f = _showcompatlist(context, mapping, name, ['%d' % r for r in revs])
719 return _hybrid(f, revs,
720 return _hybrid(f, revs,
720 lambda x: {name: x, 'ctx': repo[x]},
721 lambda x: {name: x, 'ctx': repo[x]},
721 pycompat.identity, keytype=int)
722 pycompat.identity, keytype=int)
722
723
723 @templatekeyword('subrepos', requires={'ctx'})
724 @templatekeyword('subrepos', requires={'ctx'})
724 def showsubrepos(context, mapping):
725 def showsubrepos(context, mapping):
725 """List of strings. Updated subrepositories in the changeset."""
726 """List of strings. Updated subrepositories in the changeset."""
726 ctx = context.resource(mapping, 'ctx')
727 ctx = context.resource(mapping, 'ctx')
727 substate = ctx.substate
728 substate = ctx.substate
728 if not substate:
729 if not substate:
729 return compatlist(context, mapping, 'subrepo', [])
730 return compatlist(context, mapping, 'subrepo', [])
730 psubstate = ctx.parents()[0].substate or {}
731 psubstate = ctx.parents()[0].substate or {}
731 subrepos = []
732 subrepos = []
732 for sub in substate:
733 for sub in substate:
733 if sub not in psubstate or substate[sub] != psubstate[sub]:
734 if sub not in psubstate or substate[sub] != psubstate[sub]:
734 subrepos.append(sub) # modified or newly added in ctx
735 subrepos.append(sub) # modified or newly added in ctx
735 for sub in psubstate:
736 for sub in psubstate:
736 if sub not in substate:
737 if sub not in substate:
737 subrepos.append(sub) # removed in ctx
738 subrepos.append(sub) # removed in ctx
738 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
739 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
739
740
740 # don't remove "showtags" definition, even though namespaces will put
741 # don't remove "showtags" definition, even though namespaces will put
741 # a helper function for "tags" keyword into "keywords" map automatically,
742 # a helper function for "tags" keyword into "keywords" map automatically,
742 # because online help text is built without namespaces initialization
743 # because online help text is built without namespaces initialization
743 @templatekeyword('tags', requires={'repo', 'ctx'})
744 @templatekeyword('tags', requires={'repo', 'ctx'})
744 def showtags(context, mapping):
745 def showtags(context, mapping):
745 """List of strings. Any tags associated with the changeset."""
746 """List of strings. Any tags associated with the changeset."""
746 return shownames(context, mapping, 'tags')
747 return shownames(context, mapping, 'tags')
747
748
748 @templatekeyword('termwidth', requires={'ui'})
749 @templatekeyword('termwidth', requires={'ui'})
749 def showtermwidth(context, mapping):
750 def showtermwidth(context, mapping):
750 """Integer. The width of the current terminal."""
751 """Integer. The width of the current terminal."""
751 ui = context.resource(mapping, 'ui')
752 ui = context.resource(mapping, 'ui')
752 return ui.termwidth()
753 return ui.termwidth()
753
754
754 @templatekeyword('instabilities', requires={'ctx'})
755 @templatekeyword('instabilities', requires={'ctx'})
755 def showinstabilities(context, mapping):
756 def showinstabilities(context, mapping):
756 """List of strings. Evolution instabilities affecting the changeset.
757 """List of strings. Evolution instabilities affecting the changeset.
757 (EXPERIMENTAL)
758 (EXPERIMENTAL)
758 """
759 """
759 ctx = context.resource(mapping, 'ctx')
760 ctx = context.resource(mapping, 'ctx')
760 return compatlist(context, mapping, 'instability', ctx.instabilities(),
761 return compatlist(context, mapping, 'instability', ctx.instabilities(),
761 plural='instabilities')
762 plural='instabilities')
762
763
763 @templatekeyword('verbosity', requires={'ui'})
764 @templatekeyword('verbosity', requires={'ui'})
764 def showverbosity(context, mapping):
765 def showverbosity(context, mapping):
765 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
766 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
766 or ''."""
767 or ''."""
767 ui = context.resource(mapping, 'ui')
768 ui = context.resource(mapping, 'ui')
768 # see logcmdutil.changesettemplater for priority of these flags
769 # see logcmdutil.changesettemplater for priority of these flags
769 if ui.debugflag:
770 if ui.debugflag:
770 return 'debug'
771 return 'debug'
771 elif ui.quiet:
772 elif ui.quiet:
772 return 'quiet'
773 return 'quiet'
773 elif ui.verbose:
774 elif ui.verbose:
774 return 'verbose'
775 return 'verbose'
775 return ''
776 return ''
776
777
777 @templatekeyword('whyunstable', requires={'repo', 'ctx'})
778 @templatekeyword('whyunstable', requires={'repo', 'ctx'})
778 def showwhyunstable(context, mapping):
779 def showwhyunstable(context, mapping):
779 """List of dicts explaining all instabilities of a changeset.
780 """List of dicts explaining all instabilities of a changeset.
780 (EXPERIMENTAL)
781 (EXPERIMENTAL)
781 """
782 """
782 repo = context.resource(mapping, 'repo')
783 repo = context.resource(mapping, 'repo')
783 ctx = context.resource(mapping, 'ctx')
784 ctx = context.resource(mapping, 'ctx')
784
785
785 def formatnode(ctx):
786 def formatnode(ctx):
786 return '%s (%s)' % (scmutil.formatchangeid(ctx), ctx.phasestr())
787 return '%s (%s)' % (scmutil.formatchangeid(ctx), ctx.phasestr())
787
788
788 entries = obsutil.whyunstable(repo, ctx)
789 entries = obsutil.whyunstable(repo, ctx)
789
790
790 for entry in entries:
791 for entry in entries:
791 if entry.get('divergentnodes'):
792 if entry.get('divergentnodes'):
792 dnodes = entry['divergentnodes']
793 dnodes = entry['divergentnodes']
793 dnhybrid = _hybrid(None, [dnode.hex() for dnode in dnodes],
794 dnhybrid = _hybrid(None, [dnode.hex() for dnode in dnodes],
794 lambda x: {'ctx': repo[x]},
795 lambda x: {'ctx': repo[x]},
795 lambda x: formatnode(repo[x]))
796 lambda x: formatnode(repo[x]))
796 entry['divergentnodes'] = dnhybrid
797 entry['divergentnodes'] = dnhybrid
797
798
798 tmpl = ('{instability}:{if(divergentnodes, " ")}{divergentnodes} '
799 tmpl = ('{instability}:{if(divergentnodes, " ")}{divergentnodes} '
799 '{reason} {node|short}')
800 '{reason} {node|short}')
800 return templateutil.mappinglist(entries, tmpl=tmpl, sep='\n')
801 return templateutil.mappinglist(entries, tmpl=tmpl, sep='\n')
801
802
802 def loadkeyword(ui, extname, registrarobj):
803 def loadkeyword(ui, extname, registrarobj):
803 """Load template keyword from specified registrarobj
804 """Load template keyword from specified registrarobj
804 """
805 """
805 for name, func in registrarobj._table.iteritems():
806 for name, func in registrarobj._table.iteritems():
806 keywords[name] = func
807 keywords[name] = func
807
808
808 # tell hggettext to extract docstrings from these functions:
809 # tell hggettext to extract docstrings from these functions:
809 i18nfunctions = keywords.values()
810 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now