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