##// END OF EJS Templates
log: do no expect templateresources() returning a dict...
Yuya Nishihara -
r37090:d64ae4fe default
parent child Browse files
Show More
@@ -1,936 +1,937 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 from .utils import dateutil
38 from .utils import dateutil
39
39
40 def getlimit(opts):
40 def getlimit(opts):
41 """get the log limit according to option -l/--limit"""
41 """get the log limit according to option -l/--limit"""
42 limit = opts.get('limit')
42 limit = opts.get('limit')
43 if limit:
43 if limit:
44 try:
44 try:
45 limit = int(limit)
45 limit = int(limit)
46 except ValueError:
46 except ValueError:
47 raise error.Abort(_('limit must be a positive integer'))
47 raise error.Abort(_('limit must be a positive integer'))
48 if limit <= 0:
48 if limit <= 0:
49 raise error.Abort(_('limit must be positive'))
49 raise error.Abort(_('limit must be positive'))
50 else:
50 else:
51 limit = None
51 limit = None
52 return limit
52 return limit
53
53
54 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
54 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
55 changes=None, stat=False, fp=None, prefix='',
55 changes=None, stat=False, fp=None, prefix='',
56 root='', listsubrepos=False, hunksfilterfn=None):
56 root='', listsubrepos=False, hunksfilterfn=None):
57 '''show diff or diffstat.'''
57 '''show diff or diffstat.'''
58 if root:
58 if root:
59 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
59 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
60 else:
60 else:
61 relroot = ''
61 relroot = ''
62 if relroot != '':
62 if relroot != '':
63 # XXX relative roots currently don't work if the root is within a
63 # XXX relative roots currently don't work if the root is within a
64 # subrepo
64 # subrepo
65 uirelroot = match.uipath(relroot)
65 uirelroot = match.uipath(relroot)
66 relroot += '/'
66 relroot += '/'
67 for matchroot in match.files():
67 for matchroot in match.files():
68 if not matchroot.startswith(relroot):
68 if not matchroot.startswith(relroot):
69 ui.warn(_('warning: %s not inside relative root %s\n') % (
69 ui.warn(_('warning: %s not inside relative root %s\n') % (
70 match.uipath(matchroot), uirelroot))
70 match.uipath(matchroot), uirelroot))
71
71
72 if stat:
72 if stat:
73 diffopts = diffopts.copy(context=0, noprefix=False)
73 diffopts = diffopts.copy(context=0, noprefix=False)
74 width = 80
74 width = 80
75 if not ui.plain():
75 if not ui.plain():
76 width = ui.termwidth()
76 width = ui.termwidth()
77
77
78 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
78 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
79 prefix=prefix, relroot=relroot,
79 prefix=prefix, relroot=relroot,
80 hunksfilterfn=hunksfilterfn)
80 hunksfilterfn=hunksfilterfn)
81
81
82 if fp is not None or ui.canwritewithoutlabels():
82 if fp is not None or ui.canwritewithoutlabels():
83 out = fp or ui
83 out = fp or ui
84 if stat:
84 if stat:
85 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
85 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
86 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
86 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
87 out.write(chunk)
87 out.write(chunk)
88 else:
88 else:
89 if stat:
89 if stat:
90 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
90 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
91 else:
91 else:
92 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
92 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
93 opts=diffopts)
93 opts=diffopts)
94 if ui.canbatchlabeledwrites():
94 if ui.canbatchlabeledwrites():
95 def gen():
95 def gen():
96 for chunk, label in chunks:
96 for chunk, label in chunks:
97 yield ui.label(chunk, label=label)
97 yield ui.label(chunk, label=label)
98 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
98 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
99 ui.write(chunk)
99 ui.write(chunk)
100 else:
100 else:
101 for chunk, label in chunks:
101 for chunk, label in chunks:
102 ui.write(chunk, label=label)
102 ui.write(chunk, label=label)
103
103
104 if listsubrepos:
104 if listsubrepos:
105 ctx1 = repo[node1]
105 ctx1 = repo[node1]
106 ctx2 = repo[node2]
106 ctx2 = repo[node2]
107 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
107 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
108 tempnode2 = node2
108 tempnode2 = node2
109 try:
109 try:
110 if node2 is not None:
110 if node2 is not None:
111 tempnode2 = ctx2.substate[subpath][1]
111 tempnode2 = ctx2.substate[subpath][1]
112 except KeyError:
112 except KeyError:
113 # A subrepo that existed in node1 was deleted between node1 and
113 # A subrepo that existed in node1 was deleted between node1 and
114 # node2 (inclusive). Thus, ctx2's substate won't contain that
114 # node2 (inclusive). Thus, ctx2's substate won't contain that
115 # subpath. The best we can do is to ignore it.
115 # subpath. The best we can do is to ignore it.
116 tempnode2 = None
116 tempnode2 = None
117 submatch = matchmod.subdirmatcher(subpath, match)
117 submatch = matchmod.subdirmatcher(subpath, match)
118 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
118 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
119 stat=stat, fp=fp, prefix=prefix)
119 stat=stat, fp=fp, prefix=prefix)
120
120
121 class changesetdiffer(object):
121 class changesetdiffer(object):
122 """Generate diff of changeset with pre-configured filtering functions"""
122 """Generate diff of changeset with pre-configured filtering functions"""
123
123
124 def _makefilematcher(self, ctx):
124 def _makefilematcher(self, ctx):
125 return scmutil.matchall(ctx.repo())
125 return scmutil.matchall(ctx.repo())
126
126
127 def _makehunksfilter(self, ctx):
127 def _makehunksfilter(self, ctx):
128 return None
128 return None
129
129
130 def showdiff(self, ui, ctx, diffopts, stat=False):
130 def showdiff(self, ui, ctx, diffopts, stat=False):
131 repo = ctx.repo()
131 repo = ctx.repo()
132 node = ctx.node()
132 node = ctx.node()
133 prev = ctx.p1().node()
133 prev = ctx.p1().node()
134 diffordiffstat(ui, repo, diffopts, prev, node,
134 diffordiffstat(ui, repo, diffopts, prev, node,
135 match=self._makefilematcher(ctx), stat=stat,
135 match=self._makefilematcher(ctx), stat=stat,
136 hunksfilterfn=self._makehunksfilter(ctx))
136 hunksfilterfn=self._makehunksfilter(ctx))
137
137
138 def changesetlabels(ctx):
138 def changesetlabels(ctx):
139 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
139 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
140 if ctx.obsolete():
140 if ctx.obsolete():
141 labels.append('changeset.obsolete')
141 labels.append('changeset.obsolete')
142 if ctx.isunstable():
142 if ctx.isunstable():
143 labels.append('changeset.unstable')
143 labels.append('changeset.unstable')
144 for instability in ctx.instabilities():
144 for instability in ctx.instabilities():
145 labels.append('instability.%s' % instability)
145 labels.append('instability.%s' % instability)
146 return ' '.join(labels)
146 return ' '.join(labels)
147
147
148 class changesetprinter(object):
148 class changesetprinter(object):
149 '''show changeset information when templating not requested.'''
149 '''show changeset information when templating not requested.'''
150
150
151 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
151 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
152 self.ui = ui
152 self.ui = ui
153 self.repo = repo
153 self.repo = repo
154 self.buffered = buffered
154 self.buffered = buffered
155 self._differ = differ or changesetdiffer()
155 self._differ = differ or changesetdiffer()
156 self.diffopts = diffopts or {}
156 self.diffopts = diffopts or {}
157 self.header = {}
157 self.header = {}
158 self.hunk = {}
158 self.hunk = {}
159 self.lastheader = None
159 self.lastheader = None
160 self.footer = None
160 self.footer = None
161 self._columns = templatekw.getlogcolumns()
161 self._columns = templatekw.getlogcolumns()
162
162
163 def flush(self, ctx):
163 def flush(self, ctx):
164 rev = ctx.rev()
164 rev = ctx.rev()
165 if rev in self.header:
165 if rev in self.header:
166 h = self.header[rev]
166 h = self.header[rev]
167 if h != self.lastheader:
167 if h != self.lastheader:
168 self.lastheader = h
168 self.lastheader = h
169 self.ui.write(h)
169 self.ui.write(h)
170 del self.header[rev]
170 del self.header[rev]
171 if rev in self.hunk:
171 if rev in self.hunk:
172 self.ui.write(self.hunk[rev])
172 self.ui.write(self.hunk[rev])
173 del self.hunk[rev]
173 del self.hunk[rev]
174
174
175 def close(self):
175 def close(self):
176 if self.footer:
176 if self.footer:
177 self.ui.write(self.footer)
177 self.ui.write(self.footer)
178
178
179 def show(self, ctx, copies=None, **props):
179 def show(self, ctx, copies=None, **props):
180 props = pycompat.byteskwargs(props)
180 props = pycompat.byteskwargs(props)
181 if self.buffered:
181 if self.buffered:
182 self.ui.pushbuffer(labeled=True)
182 self.ui.pushbuffer(labeled=True)
183 self._show(ctx, copies, props)
183 self._show(ctx, copies, props)
184 self.hunk[ctx.rev()] = self.ui.popbuffer()
184 self.hunk[ctx.rev()] = self.ui.popbuffer()
185 else:
185 else:
186 self._show(ctx, copies, props)
186 self._show(ctx, copies, props)
187
187
188 def _show(self, ctx, copies, props):
188 def _show(self, ctx, copies, props):
189 '''show a single changeset or file revision'''
189 '''show a single changeset or file revision'''
190 changenode = ctx.node()
190 changenode = ctx.node()
191 rev = ctx.rev()
191 rev = ctx.rev()
192
192
193 if self.ui.quiet:
193 if self.ui.quiet:
194 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
194 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
195 label='log.node')
195 label='log.node')
196 return
196 return
197
197
198 columns = self._columns
198 columns = self._columns
199 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
199 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
200 label=changesetlabels(ctx))
200 label=changesetlabels(ctx))
201
201
202 # branches are shown first before any other names due to backwards
202 # branches are shown first before any other names due to backwards
203 # compatibility
203 # compatibility
204 branch = ctx.branch()
204 branch = ctx.branch()
205 # don't show the default branch name
205 # don't show the default branch name
206 if branch != 'default':
206 if branch != 'default':
207 self.ui.write(columns['branch'] % branch, label='log.branch')
207 self.ui.write(columns['branch'] % branch, label='log.branch')
208
208
209 for nsname, ns in self.repo.names.iteritems():
209 for nsname, ns in self.repo.names.iteritems():
210 # branches has special logic already handled above, so here we just
210 # branches has special logic already handled above, so here we just
211 # skip it
211 # skip it
212 if nsname == 'branches':
212 if nsname == 'branches':
213 continue
213 continue
214 # we will use the templatename as the color name since those two
214 # we will use the templatename as the color name since those two
215 # should be the same
215 # should be the same
216 for name in ns.names(self.repo, changenode):
216 for name in ns.names(self.repo, changenode):
217 self.ui.write(ns.logfmt % name,
217 self.ui.write(ns.logfmt % name,
218 label='log.%s' % ns.colorname)
218 label='log.%s' % ns.colorname)
219 if self.ui.debugflag:
219 if self.ui.debugflag:
220 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
220 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
221 for pctx in scmutil.meaningfulparents(self.repo, ctx):
221 for pctx in scmutil.meaningfulparents(self.repo, ctx):
222 label = 'log.parent changeset.%s' % pctx.phasestr()
222 label = 'log.parent changeset.%s' % pctx.phasestr()
223 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
223 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
224 label=label)
224 label=label)
225
225
226 if self.ui.debugflag and rev is not None:
226 if self.ui.debugflag and rev is not None:
227 mnode = ctx.manifestnode()
227 mnode = ctx.manifestnode()
228 mrev = self.repo.manifestlog._revlog.rev(mnode)
228 mrev = self.repo.manifestlog._revlog.rev(mnode)
229 self.ui.write(columns['manifest']
229 self.ui.write(columns['manifest']
230 % scmutil.formatrevnode(self.ui, mrev, mnode),
230 % scmutil.formatrevnode(self.ui, mrev, mnode),
231 label='ui.debug log.manifest')
231 label='ui.debug log.manifest')
232 self.ui.write(columns['user'] % ctx.user(), label='log.user')
232 self.ui.write(columns['user'] % ctx.user(), label='log.user')
233 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
233 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
234 label='log.date')
234 label='log.date')
235
235
236 if ctx.isunstable():
236 if ctx.isunstable():
237 instabilities = ctx.instabilities()
237 instabilities = ctx.instabilities()
238 self.ui.write(columns['instability'] % ', '.join(instabilities),
238 self.ui.write(columns['instability'] % ', '.join(instabilities),
239 label='log.instability')
239 label='log.instability')
240
240
241 elif ctx.obsolete():
241 elif ctx.obsolete():
242 self._showobsfate(ctx)
242 self._showobsfate(ctx)
243
243
244 self._exthook(ctx)
244 self._exthook(ctx)
245
245
246 if self.ui.debugflag:
246 if self.ui.debugflag:
247 files = ctx.p1().status(ctx)[:3]
247 files = ctx.p1().status(ctx)[:3]
248 for key, value in zip(['files', 'files+', 'files-'], files):
248 for key, value in zip(['files', 'files+', 'files-'], files):
249 if value:
249 if value:
250 self.ui.write(columns[key] % " ".join(value),
250 self.ui.write(columns[key] % " ".join(value),
251 label='ui.debug log.files')
251 label='ui.debug log.files')
252 elif ctx.files() and self.ui.verbose:
252 elif ctx.files() and self.ui.verbose:
253 self.ui.write(columns['files'] % " ".join(ctx.files()),
253 self.ui.write(columns['files'] % " ".join(ctx.files()),
254 label='ui.note log.files')
254 label='ui.note log.files')
255 if copies and self.ui.verbose:
255 if copies and self.ui.verbose:
256 copies = ['%s (%s)' % c for c in copies]
256 copies = ['%s (%s)' % c for c in copies]
257 self.ui.write(columns['copies'] % ' '.join(copies),
257 self.ui.write(columns['copies'] % ' '.join(copies),
258 label='ui.note log.copies')
258 label='ui.note log.copies')
259
259
260 extra = ctx.extra()
260 extra = ctx.extra()
261 if extra and self.ui.debugflag:
261 if extra and self.ui.debugflag:
262 for key, value in sorted(extra.items()):
262 for key, value in sorted(extra.items()):
263 self.ui.write(columns['extra'] % (key, util.escapestr(value)),
263 self.ui.write(columns['extra'] % (key, util.escapestr(value)),
264 label='ui.debug log.extra')
264 label='ui.debug log.extra')
265
265
266 description = ctx.description().strip()
266 description = ctx.description().strip()
267 if description:
267 if description:
268 if self.ui.verbose:
268 if self.ui.verbose:
269 self.ui.write(_("description:\n"),
269 self.ui.write(_("description:\n"),
270 label='ui.note log.description')
270 label='ui.note log.description')
271 self.ui.write(description,
271 self.ui.write(description,
272 label='ui.note log.description')
272 label='ui.note log.description')
273 self.ui.write("\n\n")
273 self.ui.write("\n\n")
274 else:
274 else:
275 self.ui.write(columns['summary'] % description.splitlines()[0],
275 self.ui.write(columns['summary'] % description.splitlines()[0],
276 label='log.summary')
276 label='log.summary')
277 self.ui.write("\n")
277 self.ui.write("\n")
278
278
279 self._showpatch(ctx)
279 self._showpatch(ctx)
280
280
281 def _showobsfate(self, ctx):
281 def _showobsfate(self, ctx):
282 # TODO: do not depend on templater
282 # TODO: do not depend on templater
283 tres = formatter.templateresources(self.repo.ui, self.repo)
283 tres = formatter.templateresources(self.repo.ui, self.repo)
284 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
284 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
285 defaults=templatekw.keywords,
285 defaults=templatekw.keywords,
286 resources=tres)
286 resources=tres)
287 obsfate = t.renderdefault({'ctx': ctx, 'revcache': {}}).splitlines()
287 obsfate = t.renderdefault({'ctx': ctx, 'revcache': {}}).splitlines()
288
288
289 if obsfate:
289 if obsfate:
290 for obsfateline in obsfate:
290 for obsfateline in obsfate:
291 self.ui.write(self._columns['obsolete'] % obsfateline,
291 self.ui.write(self._columns['obsolete'] % obsfateline,
292 label='log.obsfate')
292 label='log.obsfate')
293
293
294 def _exthook(self, ctx):
294 def _exthook(self, ctx):
295 '''empty method used by extension as a hook point
295 '''empty method used by extension as a hook point
296 '''
296 '''
297
297
298 def _showpatch(self, ctx):
298 def _showpatch(self, ctx):
299 stat = self.diffopts.get('stat')
299 stat = self.diffopts.get('stat')
300 diff = self.diffopts.get('patch')
300 diff = self.diffopts.get('patch')
301 diffopts = patch.diffallopts(self.ui, self.diffopts)
301 diffopts = patch.diffallopts(self.ui, self.diffopts)
302 if stat:
302 if stat:
303 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
303 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
304 if stat and diff:
304 if stat and diff:
305 self.ui.write("\n")
305 self.ui.write("\n")
306 if diff:
306 if diff:
307 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
307 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
308 if stat or diff:
308 if stat or diff:
309 self.ui.write("\n")
309 self.ui.write("\n")
310
310
311 class jsonchangeset(changesetprinter):
311 class jsonchangeset(changesetprinter):
312 '''format changeset information.'''
312 '''format changeset information.'''
313
313
314 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
314 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
315 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
315 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
316 self.cache = {}
316 self.cache = {}
317 self._first = True
317 self._first = True
318
318
319 def close(self):
319 def close(self):
320 if not self._first:
320 if not self._first:
321 self.ui.write("\n]\n")
321 self.ui.write("\n]\n")
322 else:
322 else:
323 self.ui.write("[]\n")
323 self.ui.write("[]\n")
324
324
325 def _show(self, ctx, copies, props):
325 def _show(self, ctx, copies, props):
326 '''show a single changeset or file revision'''
326 '''show a single changeset or file revision'''
327 rev = ctx.rev()
327 rev = ctx.rev()
328 if rev is None:
328 if rev is None:
329 jrev = jnode = 'null'
329 jrev = jnode = 'null'
330 else:
330 else:
331 jrev = '%d' % rev
331 jrev = '%d' % rev
332 jnode = '"%s"' % hex(ctx.node())
332 jnode = '"%s"' % hex(ctx.node())
333 j = encoding.jsonescape
333 j = encoding.jsonescape
334
334
335 if self._first:
335 if self._first:
336 self.ui.write("[\n {")
336 self.ui.write("[\n {")
337 self._first = False
337 self._first = False
338 else:
338 else:
339 self.ui.write(",\n {")
339 self.ui.write(",\n {")
340
340
341 if self.ui.quiet:
341 if self.ui.quiet:
342 self.ui.write(('\n "rev": %s') % jrev)
342 self.ui.write(('\n "rev": %s') % jrev)
343 self.ui.write((',\n "node": %s') % jnode)
343 self.ui.write((',\n "node": %s') % jnode)
344 self.ui.write('\n }')
344 self.ui.write('\n }')
345 return
345 return
346
346
347 self.ui.write(('\n "rev": %s') % jrev)
347 self.ui.write(('\n "rev": %s') % jrev)
348 self.ui.write((',\n "node": %s') % jnode)
348 self.ui.write((',\n "node": %s') % jnode)
349 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
349 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
350 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
350 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
351 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
351 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
352 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
352 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
353 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
353 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
354
354
355 self.ui.write((',\n "bookmarks": [%s]') %
355 self.ui.write((',\n "bookmarks": [%s]') %
356 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
356 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
357 self.ui.write((',\n "tags": [%s]') %
357 self.ui.write((',\n "tags": [%s]') %
358 ", ".join('"%s"' % j(t) for t in ctx.tags()))
358 ", ".join('"%s"' % j(t) for t in ctx.tags()))
359 self.ui.write((',\n "parents": [%s]') %
359 self.ui.write((',\n "parents": [%s]') %
360 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
360 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
361
361
362 if self.ui.debugflag:
362 if self.ui.debugflag:
363 if rev is None:
363 if rev is None:
364 jmanifestnode = 'null'
364 jmanifestnode = 'null'
365 else:
365 else:
366 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
366 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
367 self.ui.write((',\n "manifest": %s') % jmanifestnode)
367 self.ui.write((',\n "manifest": %s') % jmanifestnode)
368
368
369 self.ui.write((',\n "extra": {%s}') %
369 self.ui.write((',\n "extra": {%s}') %
370 ", ".join('"%s": "%s"' % (j(k), j(v))
370 ", ".join('"%s": "%s"' % (j(k), j(v))
371 for k, v in ctx.extra().items()))
371 for k, v in ctx.extra().items()))
372
372
373 files = ctx.p1().status(ctx)
373 files = ctx.p1().status(ctx)
374 self.ui.write((',\n "modified": [%s]') %
374 self.ui.write((',\n "modified": [%s]') %
375 ", ".join('"%s"' % j(f) for f in files[0]))
375 ", ".join('"%s"' % j(f) for f in files[0]))
376 self.ui.write((',\n "added": [%s]') %
376 self.ui.write((',\n "added": [%s]') %
377 ", ".join('"%s"' % j(f) for f in files[1]))
377 ", ".join('"%s"' % j(f) for f in files[1]))
378 self.ui.write((',\n "removed": [%s]') %
378 self.ui.write((',\n "removed": [%s]') %
379 ", ".join('"%s"' % j(f) for f in files[2]))
379 ", ".join('"%s"' % j(f) for f in files[2]))
380
380
381 elif self.ui.verbose:
381 elif self.ui.verbose:
382 self.ui.write((',\n "files": [%s]') %
382 self.ui.write((',\n "files": [%s]') %
383 ", ".join('"%s"' % j(f) for f in ctx.files()))
383 ", ".join('"%s"' % j(f) for f in ctx.files()))
384
384
385 if copies:
385 if copies:
386 self.ui.write((',\n "copies": {%s}') %
386 self.ui.write((',\n "copies": {%s}') %
387 ", ".join('"%s": "%s"' % (j(k), j(v))
387 ", ".join('"%s": "%s"' % (j(k), j(v))
388 for k, v in copies))
388 for k, v in copies))
389
389
390 stat = self.diffopts.get('stat')
390 stat = self.diffopts.get('stat')
391 diff = self.diffopts.get('patch')
391 diff = self.diffopts.get('patch')
392 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
392 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
393 if stat:
393 if stat:
394 self.ui.pushbuffer()
394 self.ui.pushbuffer()
395 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
395 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
396 self.ui.write((',\n "diffstat": "%s"')
396 self.ui.write((',\n "diffstat": "%s"')
397 % j(self.ui.popbuffer()))
397 % j(self.ui.popbuffer()))
398 if diff:
398 if diff:
399 self.ui.pushbuffer()
399 self.ui.pushbuffer()
400 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
400 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
401 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
401 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
402
402
403 self.ui.write("\n }")
403 self.ui.write("\n }")
404
404
405 class changesettemplater(changesetprinter):
405 class changesettemplater(changesetprinter):
406 '''format changeset information.
406 '''format changeset information.
407
407
408 Note: there are a variety of convenience functions to build a
408 Note: there are a variety of convenience functions to build a
409 changesettemplater for common cases. See functions such as:
409 changesettemplater for common cases. See functions such as:
410 maketemplater, changesetdisplayer, buildcommittemplate, or other
410 maketemplater, changesetdisplayer, buildcommittemplate, or other
411 functions that use changesest_templater.
411 functions that use changesest_templater.
412 '''
412 '''
413
413
414 # Arguments before "buffered" used to be positional. Consider not
414 # Arguments before "buffered" used to be positional. Consider not
415 # adding/removing arguments before "buffered" to not break callers.
415 # adding/removing arguments before "buffered" to not break callers.
416 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
416 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
417 buffered=False):
417 buffered=False):
418 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
418 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
419 tres = formatter.templateresources(ui, repo)
419 # tres is shared with _graphnodeformatter()
420 self._tresources = tres = formatter.templateresources(ui, repo)
420 self.t = formatter.loadtemplater(ui, tmplspec,
421 self.t = formatter.loadtemplater(ui, tmplspec,
421 defaults=templatekw.keywords,
422 defaults=templatekw.keywords,
422 resources=tres,
423 resources=tres,
423 cache=templatekw.defaulttempl)
424 cache=templatekw.defaulttempl)
424 self._counter = itertools.count()
425 self._counter = itertools.count()
425 self._getcache = tres['cache'] # shared with _graphnodeformatter()
426
426
427 self._tref = tmplspec.ref
427 self._tref = tmplspec.ref
428 self._parts = {'header': '', 'footer': '',
428 self._parts = {'header': '', 'footer': '',
429 tmplspec.ref: tmplspec.ref,
429 tmplspec.ref: tmplspec.ref,
430 'docheader': '', 'docfooter': '',
430 'docheader': '', 'docfooter': '',
431 'separator': ''}
431 'separator': ''}
432 if tmplspec.mapfile:
432 if tmplspec.mapfile:
433 # find correct templates for current mode, for backward
433 # find correct templates for current mode, for backward
434 # compatibility with 'log -v/-q/--debug' using a mapfile
434 # compatibility with 'log -v/-q/--debug' using a mapfile
435 tmplmodes = [
435 tmplmodes = [
436 (True, ''),
436 (True, ''),
437 (self.ui.verbose, '_verbose'),
437 (self.ui.verbose, '_verbose'),
438 (self.ui.quiet, '_quiet'),
438 (self.ui.quiet, '_quiet'),
439 (self.ui.debugflag, '_debug'),
439 (self.ui.debugflag, '_debug'),
440 ]
440 ]
441 for mode, postfix in tmplmodes:
441 for mode, postfix in tmplmodes:
442 for t in self._parts:
442 for t in self._parts:
443 cur = t + postfix
443 cur = t + postfix
444 if mode and cur in self.t:
444 if mode and cur in self.t:
445 self._parts[t] = cur
445 self._parts[t] = cur
446 else:
446 else:
447 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
447 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
448 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
448 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
449 self._parts.update(m)
449 self._parts.update(m)
450
450
451 if self._parts['docheader']:
451 if self._parts['docheader']:
452 self.ui.write(self.t.render(self._parts['docheader'], {}))
452 self.ui.write(self.t.render(self._parts['docheader'], {}))
453
453
454 def close(self):
454 def close(self):
455 if self._parts['docfooter']:
455 if self._parts['docfooter']:
456 if not self.footer:
456 if not self.footer:
457 self.footer = ""
457 self.footer = ""
458 self.footer += self.t.render(self._parts['docfooter'], {})
458 self.footer += self.t.render(self._parts['docfooter'], {})
459 return super(changesettemplater, self).close()
459 return super(changesettemplater, self).close()
460
460
461 def _show(self, ctx, copies, props):
461 def _show(self, ctx, copies, props):
462 '''show a single changeset or file revision'''
462 '''show a single changeset or file revision'''
463 props = props.copy()
463 props = props.copy()
464 props['ctx'] = ctx
464 props['ctx'] = ctx
465 props['index'] = index = next(self._counter)
465 props['index'] = index = next(self._counter)
466 props['revcache'] = {'copies': copies}
466 props['revcache'] = {'copies': copies}
467
467
468 # write separator, which wouldn't work well with the header part below
468 # write separator, which wouldn't work well with the header part below
469 # since there's inherently a conflict between header (across items) and
469 # since there's inherently a conflict between header (across items) and
470 # separator (per item)
470 # separator (per item)
471 if self._parts['separator'] and index > 0:
471 if self._parts['separator'] and index > 0:
472 self.ui.write(self.t.render(self._parts['separator'], {}))
472 self.ui.write(self.t.render(self._parts['separator'], {}))
473
473
474 # write header
474 # write header
475 if self._parts['header']:
475 if self._parts['header']:
476 h = self.t.render(self._parts['header'], props)
476 h = self.t.render(self._parts['header'], props)
477 if self.buffered:
477 if self.buffered:
478 self.header[ctx.rev()] = h
478 self.header[ctx.rev()] = h
479 else:
479 else:
480 if self.lastheader != h:
480 if self.lastheader != h:
481 self.lastheader = h
481 self.lastheader = h
482 self.ui.write(h)
482 self.ui.write(h)
483
483
484 # write changeset metadata, then patch if requested
484 # write changeset metadata, then patch if requested
485 key = self._parts[self._tref]
485 key = self._parts[self._tref]
486 self.ui.write(self.t.render(key, props))
486 self.ui.write(self.t.render(key, props))
487 self._showpatch(ctx)
487 self._showpatch(ctx)
488
488
489 if self._parts['footer']:
489 if self._parts['footer']:
490 if not self.footer:
490 if not self.footer:
491 self.footer = self.t.render(self._parts['footer'], props)
491 self.footer = self.t.render(self._parts['footer'], props)
492
492
493 def templatespec(tmpl, mapfile):
493 def templatespec(tmpl, mapfile):
494 if mapfile:
494 if mapfile:
495 return formatter.templatespec('changeset', tmpl, mapfile)
495 return formatter.templatespec('changeset', tmpl, mapfile)
496 else:
496 else:
497 return formatter.templatespec('', tmpl, None)
497 return formatter.templatespec('', tmpl, None)
498
498
499 def _lookuptemplate(ui, tmpl, style):
499 def _lookuptemplate(ui, tmpl, style):
500 """Find the template matching the given template spec or style
500 """Find the template matching the given template spec or style
501
501
502 See formatter.lookuptemplate() for details.
502 See formatter.lookuptemplate() for details.
503 """
503 """
504
504
505 # ui settings
505 # ui settings
506 if not tmpl and not style: # template are stronger than style
506 if not tmpl and not style: # template are stronger than style
507 tmpl = ui.config('ui', 'logtemplate')
507 tmpl = ui.config('ui', 'logtemplate')
508 if tmpl:
508 if tmpl:
509 return templatespec(templater.unquotestring(tmpl), None)
509 return templatespec(templater.unquotestring(tmpl), None)
510 else:
510 else:
511 style = util.expandpath(ui.config('ui', 'style'))
511 style = util.expandpath(ui.config('ui', 'style'))
512
512
513 if not tmpl and style:
513 if not tmpl and style:
514 mapfile = style
514 mapfile = style
515 if not os.path.split(mapfile)[0]:
515 if not os.path.split(mapfile)[0]:
516 mapname = (templater.templatepath('map-cmdline.' + mapfile)
516 mapname = (templater.templatepath('map-cmdline.' + mapfile)
517 or templater.templatepath(mapfile))
517 or templater.templatepath(mapfile))
518 if mapname:
518 if mapname:
519 mapfile = mapname
519 mapfile = mapname
520 return templatespec(None, mapfile)
520 return templatespec(None, mapfile)
521
521
522 if not tmpl:
522 if not tmpl:
523 return templatespec(None, None)
523 return templatespec(None, None)
524
524
525 return formatter.lookuptemplate(ui, 'changeset', tmpl)
525 return formatter.lookuptemplate(ui, 'changeset', tmpl)
526
526
527 def maketemplater(ui, repo, tmpl, buffered=False):
527 def maketemplater(ui, repo, tmpl, buffered=False):
528 """Create a changesettemplater from a literal template 'tmpl'
528 """Create a changesettemplater from a literal template 'tmpl'
529 byte-string."""
529 byte-string."""
530 spec = templatespec(tmpl, None)
530 spec = templatespec(tmpl, None)
531 return changesettemplater(ui, repo, spec, buffered=buffered)
531 return changesettemplater(ui, repo, spec, buffered=buffered)
532
532
533 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
533 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
534 """show one changeset using template or regular display.
534 """show one changeset using template or regular display.
535
535
536 Display format will be the first non-empty hit of:
536 Display format will be the first non-empty hit of:
537 1. option 'template'
537 1. option 'template'
538 2. option 'style'
538 2. option 'style'
539 3. [ui] setting 'logtemplate'
539 3. [ui] setting 'logtemplate'
540 4. [ui] setting 'style'
540 4. [ui] setting 'style'
541 If all of these values are either the unset or the empty string,
541 If all of these values are either the unset or the empty string,
542 regular display via changesetprinter() is done.
542 regular display via changesetprinter() is done.
543 """
543 """
544 postargs = (differ, opts, buffered)
544 postargs = (differ, opts, buffered)
545 if opts.get('template') == 'json':
545 if opts.get('template') == 'json':
546 return jsonchangeset(ui, repo, *postargs)
546 return jsonchangeset(ui, repo, *postargs)
547
547
548 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
548 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
549
549
550 if not spec.ref and not spec.tmpl and not spec.mapfile:
550 if not spec.ref and not spec.tmpl and not spec.mapfile:
551 return changesetprinter(ui, repo, *postargs)
551 return changesetprinter(ui, repo, *postargs)
552
552
553 return changesettemplater(ui, repo, spec, *postargs)
553 return changesettemplater(ui, repo, spec, *postargs)
554
554
555 def _makematcher(repo, revs, pats, opts):
555 def _makematcher(repo, revs, pats, opts):
556 """Build matcher and expanded patterns from log options
556 """Build matcher and expanded patterns from log options
557
557
558 If --follow, revs are the revisions to follow from.
558 If --follow, revs are the revisions to follow from.
559
559
560 Returns (match, pats, slowpath) where
560 Returns (match, pats, slowpath) where
561 - match: a matcher built from the given pats and -I/-X opts
561 - match: a matcher built from the given pats and -I/-X opts
562 - pats: patterns used (globs are expanded on Windows)
562 - pats: patterns used (globs are expanded on Windows)
563 - slowpath: True if patterns aren't as simple as scanning filelogs
563 - slowpath: True if patterns aren't as simple as scanning filelogs
564 """
564 """
565 # pats/include/exclude are passed to match.match() directly in
565 # pats/include/exclude are passed to match.match() directly in
566 # _matchfiles() revset but walkchangerevs() builds its matcher with
566 # _matchfiles() revset but walkchangerevs() builds its matcher with
567 # scmutil.match(). The difference is input pats are globbed on
567 # scmutil.match(). The difference is input pats are globbed on
568 # platforms without shell expansion (windows).
568 # platforms without shell expansion (windows).
569 wctx = repo[None]
569 wctx = repo[None]
570 match, pats = scmutil.matchandpats(wctx, pats, opts)
570 match, pats = scmutil.matchandpats(wctx, pats, opts)
571 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
571 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
572 if not slowpath:
572 if not slowpath:
573 follow = opts.get('follow') or opts.get('follow_first')
573 follow = opts.get('follow') or opts.get('follow_first')
574 startctxs = []
574 startctxs = []
575 if follow and opts.get('rev'):
575 if follow and opts.get('rev'):
576 startctxs = [repo[r] for r in revs]
576 startctxs = [repo[r] for r in revs]
577 for f in match.files():
577 for f in match.files():
578 if follow and startctxs:
578 if follow and startctxs:
579 # No idea if the path was a directory at that revision, so
579 # No idea if the path was a directory at that revision, so
580 # take the slow path.
580 # take the slow path.
581 if any(f not in c for c in startctxs):
581 if any(f not in c for c in startctxs):
582 slowpath = True
582 slowpath = True
583 continue
583 continue
584 elif follow and f not in wctx:
584 elif follow and f not in wctx:
585 # If the file exists, it may be a directory, so let it
585 # If the file exists, it may be a directory, so let it
586 # take the slow path.
586 # take the slow path.
587 if os.path.exists(repo.wjoin(f)):
587 if os.path.exists(repo.wjoin(f)):
588 slowpath = True
588 slowpath = True
589 continue
589 continue
590 else:
590 else:
591 raise error.Abort(_('cannot follow file not in parent '
591 raise error.Abort(_('cannot follow file not in parent '
592 'revision: "%s"') % f)
592 'revision: "%s"') % f)
593 filelog = repo.file(f)
593 filelog = repo.file(f)
594 if not filelog:
594 if not filelog:
595 # A zero count may be a directory or deleted file, so
595 # A zero count may be a directory or deleted file, so
596 # try to find matching entries on the slow path.
596 # try to find matching entries on the slow path.
597 if follow:
597 if follow:
598 raise error.Abort(
598 raise error.Abort(
599 _('cannot follow nonexistent file: "%s"') % f)
599 _('cannot follow nonexistent file: "%s"') % f)
600 slowpath = True
600 slowpath = True
601
601
602 # We decided to fall back to the slowpath because at least one
602 # We decided to fall back to the slowpath because at least one
603 # of the paths was not a file. Check to see if at least one of them
603 # of the paths was not a file. Check to see if at least one of them
604 # existed in history - in that case, we'll continue down the
604 # existed in history - in that case, we'll continue down the
605 # slowpath; otherwise, we can turn off the slowpath
605 # slowpath; otherwise, we can turn off the slowpath
606 if slowpath:
606 if slowpath:
607 for path in match.files():
607 for path in match.files():
608 if path == '.' or path in repo.store:
608 if path == '.' or path in repo.store:
609 break
609 break
610 else:
610 else:
611 slowpath = False
611 slowpath = False
612
612
613 return match, pats, slowpath
613 return match, pats, slowpath
614
614
615 def _fileancestors(repo, revs, match, followfirst):
615 def _fileancestors(repo, revs, match, followfirst):
616 fctxs = []
616 fctxs = []
617 for r in revs:
617 for r in revs:
618 ctx = repo[r]
618 ctx = repo[r]
619 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
619 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
620
620
621 # When displaying a revision with --patch --follow FILE, we have
621 # When displaying a revision with --patch --follow FILE, we have
622 # to know which file of the revision must be diffed. With
622 # to know which file of the revision must be diffed. With
623 # --follow, we want the names of the ancestors of FILE in the
623 # --follow, we want the names of the ancestors of FILE in the
624 # revision, stored in "fcache". "fcache" is populated as a side effect
624 # revision, stored in "fcache". "fcache" is populated as a side effect
625 # of the graph traversal.
625 # of the graph traversal.
626 fcache = {}
626 fcache = {}
627 def filematcher(ctx):
627 def filematcher(ctx):
628 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
628 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
629
629
630 def revgen():
630 def revgen():
631 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
631 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
632 fcache[rev] = [c.path() for c in cs]
632 fcache[rev] = [c.path() for c in cs]
633 yield rev
633 yield rev
634 return smartset.generatorset(revgen(), iterasc=False), filematcher
634 return smartset.generatorset(revgen(), iterasc=False), filematcher
635
635
636 def _makenofollowfilematcher(repo, pats, opts):
636 def _makenofollowfilematcher(repo, pats, opts):
637 '''hook for extensions to override the filematcher for non-follow cases'''
637 '''hook for extensions to override the filematcher for non-follow cases'''
638 return None
638 return None
639
639
640 _opt2logrevset = {
640 _opt2logrevset = {
641 'no_merges': ('not merge()', None),
641 'no_merges': ('not merge()', None),
642 'only_merges': ('merge()', None),
642 'only_merges': ('merge()', None),
643 '_matchfiles': (None, '_matchfiles(%ps)'),
643 '_matchfiles': (None, '_matchfiles(%ps)'),
644 'date': ('date(%s)', None),
644 'date': ('date(%s)', None),
645 'branch': ('branch(%s)', '%lr'),
645 'branch': ('branch(%s)', '%lr'),
646 '_patslog': ('filelog(%s)', '%lr'),
646 '_patslog': ('filelog(%s)', '%lr'),
647 'keyword': ('keyword(%s)', '%lr'),
647 'keyword': ('keyword(%s)', '%lr'),
648 'prune': ('ancestors(%s)', 'not %lr'),
648 'prune': ('ancestors(%s)', 'not %lr'),
649 'user': ('user(%s)', '%lr'),
649 'user': ('user(%s)', '%lr'),
650 }
650 }
651
651
652 def _makerevset(repo, match, pats, slowpath, opts):
652 def _makerevset(repo, match, pats, slowpath, opts):
653 """Return a revset string built from log options and file patterns"""
653 """Return a revset string built from log options and file patterns"""
654 opts = dict(opts)
654 opts = dict(opts)
655 # follow or not follow?
655 # follow or not follow?
656 follow = opts.get('follow') or opts.get('follow_first')
656 follow = opts.get('follow') or opts.get('follow_first')
657
657
658 # branch and only_branch are really aliases and must be handled at
658 # branch and only_branch are really aliases and must be handled at
659 # the same time
659 # the same time
660 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
660 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
661 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
661 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
662
662
663 if slowpath:
663 if slowpath:
664 # See walkchangerevs() slow path.
664 # See walkchangerevs() slow path.
665 #
665 #
666 # pats/include/exclude cannot be represented as separate
666 # pats/include/exclude cannot be represented as separate
667 # revset expressions as their filtering logic applies at file
667 # revset expressions as their filtering logic applies at file
668 # level. For instance "-I a -X b" matches a revision touching
668 # level. For instance "-I a -X b" matches a revision touching
669 # "a" and "b" while "file(a) and not file(b)" does
669 # "a" and "b" while "file(a) and not file(b)" does
670 # not. Besides, filesets are evaluated against the working
670 # not. Besides, filesets are evaluated against the working
671 # directory.
671 # directory.
672 matchargs = ['r:', 'd:relpath']
672 matchargs = ['r:', 'd:relpath']
673 for p in pats:
673 for p in pats:
674 matchargs.append('p:' + p)
674 matchargs.append('p:' + p)
675 for p in opts.get('include', []):
675 for p in opts.get('include', []):
676 matchargs.append('i:' + p)
676 matchargs.append('i:' + p)
677 for p in opts.get('exclude', []):
677 for p in opts.get('exclude', []):
678 matchargs.append('x:' + p)
678 matchargs.append('x:' + p)
679 opts['_matchfiles'] = matchargs
679 opts['_matchfiles'] = matchargs
680 elif not follow:
680 elif not follow:
681 opts['_patslog'] = list(pats)
681 opts['_patslog'] = list(pats)
682
682
683 expr = []
683 expr = []
684 for op, val in sorted(opts.iteritems()):
684 for op, val in sorted(opts.iteritems()):
685 if not val:
685 if not val:
686 continue
686 continue
687 if op not in _opt2logrevset:
687 if op not in _opt2logrevset:
688 continue
688 continue
689 revop, listop = _opt2logrevset[op]
689 revop, listop = _opt2logrevset[op]
690 if revop and '%' not in revop:
690 if revop and '%' not in revop:
691 expr.append(revop)
691 expr.append(revop)
692 elif not listop:
692 elif not listop:
693 expr.append(revsetlang.formatspec(revop, val))
693 expr.append(revsetlang.formatspec(revop, val))
694 else:
694 else:
695 if revop:
695 if revop:
696 val = [revsetlang.formatspec(revop, v) for v in val]
696 val = [revsetlang.formatspec(revop, v) for v in val]
697 expr.append(revsetlang.formatspec(listop, val))
697 expr.append(revsetlang.formatspec(listop, val))
698
698
699 if expr:
699 if expr:
700 expr = '(' + ' and '.join(expr) + ')'
700 expr = '(' + ' and '.join(expr) + ')'
701 else:
701 else:
702 expr = None
702 expr = None
703 return expr
703 return expr
704
704
705 def _initialrevs(repo, opts):
705 def _initialrevs(repo, opts):
706 """Return the initial set of revisions to be filtered or followed"""
706 """Return the initial set of revisions to be filtered or followed"""
707 follow = opts.get('follow') or opts.get('follow_first')
707 follow = opts.get('follow') or opts.get('follow_first')
708 if opts.get('rev'):
708 if opts.get('rev'):
709 revs = scmutil.revrange(repo, opts['rev'])
709 revs = scmutil.revrange(repo, opts['rev'])
710 elif follow and repo.dirstate.p1() == nullid:
710 elif follow and repo.dirstate.p1() == nullid:
711 revs = smartset.baseset()
711 revs = smartset.baseset()
712 elif follow:
712 elif follow:
713 revs = repo.revs('.')
713 revs = repo.revs('.')
714 else:
714 else:
715 revs = smartset.spanset(repo)
715 revs = smartset.spanset(repo)
716 revs.reverse()
716 revs.reverse()
717 return revs
717 return revs
718
718
719 def getrevs(repo, pats, opts):
719 def getrevs(repo, pats, opts):
720 """Return (revs, differ) where revs is a smartset
720 """Return (revs, differ) where revs is a smartset
721
721
722 differ is a changesetdiffer with pre-configured file matcher.
722 differ is a changesetdiffer with pre-configured file matcher.
723 """
723 """
724 follow = opts.get('follow') or opts.get('follow_first')
724 follow = opts.get('follow') or opts.get('follow_first')
725 followfirst = opts.get('follow_first')
725 followfirst = opts.get('follow_first')
726 limit = getlimit(opts)
726 limit = getlimit(opts)
727 revs = _initialrevs(repo, opts)
727 revs = _initialrevs(repo, opts)
728 if not revs:
728 if not revs:
729 return smartset.baseset(), None
729 return smartset.baseset(), None
730 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
730 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
731 filematcher = None
731 filematcher = None
732 if follow:
732 if follow:
733 if slowpath or match.always():
733 if slowpath or match.always():
734 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
734 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
735 else:
735 else:
736 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
736 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
737 revs.reverse()
737 revs.reverse()
738 if filematcher is None:
738 if filematcher is None:
739 filematcher = _makenofollowfilematcher(repo, pats, opts)
739 filematcher = _makenofollowfilematcher(repo, pats, opts)
740 if filematcher is None:
740 if filematcher is None:
741 def filematcher(ctx):
741 def filematcher(ctx):
742 return match
742 return match
743
743
744 expr = _makerevset(repo, match, pats, slowpath, opts)
744 expr = _makerevset(repo, match, pats, slowpath, opts)
745 if opts.get('graph') and opts.get('rev'):
745 if opts.get('graph') and opts.get('rev'):
746 # User-specified revs might be unsorted, but don't sort before
746 # User-specified revs might be unsorted, but don't sort before
747 # _makerevset because it might depend on the order of revs
747 # _makerevset because it might depend on the order of revs
748 if not (revs.isdescending() or revs.istopo()):
748 if not (revs.isdescending() or revs.istopo()):
749 revs.sort(reverse=True)
749 revs.sort(reverse=True)
750 if expr:
750 if expr:
751 matcher = revset.match(None, expr)
751 matcher = revset.match(None, expr)
752 revs = matcher(repo, revs)
752 revs = matcher(repo, revs)
753 if limit is not None:
753 if limit is not None:
754 revs = revs.slice(0, limit)
754 revs = revs.slice(0, limit)
755
755
756 differ = changesetdiffer()
756 differ = changesetdiffer()
757 differ._makefilematcher = filematcher
757 differ._makefilematcher = filematcher
758 return revs, differ
758 return revs, differ
759
759
760 def _parselinerangeopt(repo, opts):
760 def _parselinerangeopt(repo, opts):
761 """Parse --line-range log option and return a list of tuples (filename,
761 """Parse --line-range log option and return a list of tuples (filename,
762 (fromline, toline)).
762 (fromline, toline)).
763 """
763 """
764 linerangebyfname = []
764 linerangebyfname = []
765 for pat in opts.get('line_range', []):
765 for pat in opts.get('line_range', []):
766 try:
766 try:
767 pat, linerange = pat.rsplit(',', 1)
767 pat, linerange = pat.rsplit(',', 1)
768 except ValueError:
768 except ValueError:
769 raise error.Abort(_('malformatted line-range pattern %s') % pat)
769 raise error.Abort(_('malformatted line-range pattern %s') % pat)
770 try:
770 try:
771 fromline, toline = map(int, linerange.split(':'))
771 fromline, toline = map(int, linerange.split(':'))
772 except ValueError:
772 except ValueError:
773 raise error.Abort(_("invalid line range for %s") % pat)
773 raise error.Abort(_("invalid line range for %s") % pat)
774 msg = _("line range pattern '%s' must match exactly one file") % pat
774 msg = _("line range pattern '%s' must match exactly one file") % pat
775 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
775 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
776 linerangebyfname.append(
776 linerangebyfname.append(
777 (fname, util.processlinerange(fromline, toline)))
777 (fname, util.processlinerange(fromline, toline)))
778 return linerangebyfname
778 return linerangebyfname
779
779
780 def getlinerangerevs(repo, userrevs, opts):
780 def getlinerangerevs(repo, userrevs, opts):
781 """Return (revs, differ).
781 """Return (revs, differ).
782
782
783 "revs" are revisions obtained by processing "line-range" log options and
783 "revs" are revisions obtained by processing "line-range" log options and
784 walking block ancestors of each specified file/line-range.
784 walking block ancestors of each specified file/line-range.
785
785
786 "differ" is a changesetdiffer with pre-configured file matcher and hunks
786 "differ" is a changesetdiffer with pre-configured file matcher and hunks
787 filter.
787 filter.
788 """
788 """
789 wctx = repo[None]
789 wctx = repo[None]
790
790
791 # Two-levels map of "rev -> file ctx -> [line range]".
791 # Two-levels map of "rev -> file ctx -> [line range]".
792 linerangesbyrev = {}
792 linerangesbyrev = {}
793 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
793 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
794 if fname not in wctx:
794 if fname not in wctx:
795 raise error.Abort(_('cannot follow file not in parent '
795 raise error.Abort(_('cannot follow file not in parent '
796 'revision: "%s"') % fname)
796 'revision: "%s"') % fname)
797 fctx = wctx.filectx(fname)
797 fctx = wctx.filectx(fname)
798 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
798 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
799 rev = fctx.introrev()
799 rev = fctx.introrev()
800 if rev not in userrevs:
800 if rev not in userrevs:
801 continue
801 continue
802 linerangesbyrev.setdefault(
802 linerangesbyrev.setdefault(
803 rev, {}).setdefault(
803 rev, {}).setdefault(
804 fctx.path(), []).append(linerange)
804 fctx.path(), []).append(linerange)
805
805
806 def nofilterhunksfn(fctx, hunks):
806 def nofilterhunksfn(fctx, hunks):
807 return hunks
807 return hunks
808
808
809 def hunksfilter(ctx):
809 def hunksfilter(ctx):
810 fctxlineranges = linerangesbyrev.get(ctx.rev())
810 fctxlineranges = linerangesbyrev.get(ctx.rev())
811 if fctxlineranges is None:
811 if fctxlineranges is None:
812 return nofilterhunksfn
812 return nofilterhunksfn
813
813
814 def filterfn(fctx, hunks):
814 def filterfn(fctx, hunks):
815 lineranges = fctxlineranges.get(fctx.path())
815 lineranges = fctxlineranges.get(fctx.path())
816 if lineranges is not None:
816 if lineranges is not None:
817 for hr, lines in hunks:
817 for hr, lines in hunks:
818 if hr is None: # binary
818 if hr is None: # binary
819 yield hr, lines
819 yield hr, lines
820 continue
820 continue
821 if any(mdiff.hunkinrange(hr[2:], lr)
821 if any(mdiff.hunkinrange(hr[2:], lr)
822 for lr in lineranges):
822 for lr in lineranges):
823 yield hr, lines
823 yield hr, lines
824 else:
824 else:
825 for hunk in hunks:
825 for hunk in hunks:
826 yield hunk
826 yield hunk
827
827
828 return filterfn
828 return filterfn
829
829
830 def filematcher(ctx):
830 def filematcher(ctx):
831 files = list(linerangesbyrev.get(ctx.rev(), []))
831 files = list(linerangesbyrev.get(ctx.rev(), []))
832 return scmutil.matchfiles(repo, files)
832 return scmutil.matchfiles(repo, files)
833
833
834 revs = sorted(linerangesbyrev, reverse=True)
834 revs = sorted(linerangesbyrev, reverse=True)
835
835
836 differ = changesetdiffer()
836 differ = changesetdiffer()
837 differ._makefilematcher = filematcher
837 differ._makefilematcher = filematcher
838 differ._makehunksfilter = hunksfilter
838 differ._makehunksfilter = hunksfilter
839 return revs, differ
839 return revs, differ
840
840
841 def _graphnodeformatter(ui, displayer):
841 def _graphnodeformatter(ui, displayer):
842 spec = ui.config('ui', 'graphnodetemplate')
842 spec = ui.config('ui', 'graphnodetemplate')
843 if not spec:
843 if not spec:
844 return templatekw.getgraphnode # fast path for "{graphnode}"
844 return templatekw.getgraphnode # fast path for "{graphnode}"
845
845
846 spec = templater.unquotestring(spec)
846 spec = templater.unquotestring(spec)
847 tres = formatter.templateresources(ui)
848 if isinstance(displayer, changesettemplater):
847 if isinstance(displayer, changesettemplater):
849 # reuse cache of slow templates
848 # reuse cache of slow templates
850 tres['cache'] = displayer._getcache
849 tres = displayer._tresources
850 else:
851 tres = formatter.templateresources(ui)
851 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
852 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
852 resources=tres)
853 resources=tres)
853 def formatnode(repo, ctx):
854 def formatnode(repo, ctx):
854 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
855 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
855 return templ.renderdefault(props)
856 return templ.renderdefault(props)
856 return formatnode
857 return formatnode
857
858
858 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
859 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
859 props = props or {}
860 props = props or {}
860 formatnode = _graphnodeformatter(ui, displayer)
861 formatnode = _graphnodeformatter(ui, displayer)
861 state = graphmod.asciistate()
862 state = graphmod.asciistate()
862 styles = state['styles']
863 styles = state['styles']
863
864
864 # only set graph styling if HGPLAIN is not set.
865 # only set graph styling if HGPLAIN is not set.
865 if ui.plain('graph'):
866 if ui.plain('graph'):
866 # set all edge styles to |, the default pre-3.8 behaviour
867 # set all edge styles to |, the default pre-3.8 behaviour
867 styles.update(dict.fromkeys(styles, '|'))
868 styles.update(dict.fromkeys(styles, '|'))
868 else:
869 else:
869 edgetypes = {
870 edgetypes = {
870 'parent': graphmod.PARENT,
871 'parent': graphmod.PARENT,
871 'grandparent': graphmod.GRANDPARENT,
872 'grandparent': graphmod.GRANDPARENT,
872 'missing': graphmod.MISSINGPARENT
873 'missing': graphmod.MISSINGPARENT
873 }
874 }
874 for name, key in edgetypes.items():
875 for name, key in edgetypes.items():
875 # experimental config: experimental.graphstyle.*
876 # experimental config: experimental.graphstyle.*
876 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
877 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
877 styles[key])
878 styles[key])
878 if not styles[key]:
879 if not styles[key]:
879 styles[key] = None
880 styles[key] = None
880
881
881 # experimental config: experimental.graphshorten
882 # experimental config: experimental.graphshorten
882 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
883 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
883
884
884 for rev, type, ctx, parents in dag:
885 for rev, type, ctx, parents in dag:
885 char = formatnode(repo, ctx)
886 char = formatnode(repo, ctx)
886 copies = None
887 copies = None
887 if getrenamed and ctx.rev():
888 if getrenamed and ctx.rev():
888 copies = []
889 copies = []
889 for fn in ctx.files():
890 for fn in ctx.files():
890 rename = getrenamed(fn, ctx.rev())
891 rename = getrenamed(fn, ctx.rev())
891 if rename:
892 if rename:
892 copies.append((fn, rename[0]))
893 copies.append((fn, rename[0]))
893 edges = edgefn(type, char, state, rev, parents)
894 edges = edgefn(type, char, state, rev, parents)
894 firstedge = next(edges)
895 firstedge = next(edges)
895 width = firstedge[2]
896 width = firstedge[2]
896 displayer.show(ctx, copies=copies,
897 displayer.show(ctx, copies=copies,
897 graphwidth=width, **pycompat.strkwargs(props))
898 graphwidth=width, **pycompat.strkwargs(props))
898 lines = displayer.hunk.pop(rev).split('\n')
899 lines = displayer.hunk.pop(rev).split('\n')
899 if not lines[-1]:
900 if not lines[-1]:
900 del lines[-1]
901 del lines[-1]
901 displayer.flush(ctx)
902 displayer.flush(ctx)
902 for type, char, width, coldata in itertools.chain([firstedge], edges):
903 for type, char, width, coldata in itertools.chain([firstedge], edges):
903 graphmod.ascii(ui, state, type, char, lines, coldata)
904 graphmod.ascii(ui, state, type, char, lines, coldata)
904 lines = []
905 lines = []
905 displayer.close()
906 displayer.close()
906
907
907 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
908 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
908 revdag = graphmod.dagwalker(repo, revs)
909 revdag = graphmod.dagwalker(repo, revs)
909 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
910 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
910
911
911 def displayrevs(ui, repo, revs, displayer, getrenamed):
912 def displayrevs(ui, repo, revs, displayer, getrenamed):
912 for rev in revs:
913 for rev in revs:
913 ctx = repo[rev]
914 ctx = repo[rev]
914 copies = None
915 copies = None
915 if getrenamed is not None and rev:
916 if getrenamed is not None and rev:
916 copies = []
917 copies = []
917 for fn in ctx.files():
918 for fn in ctx.files():
918 rename = getrenamed(fn, rev)
919 rename = getrenamed(fn, rev)
919 if rename:
920 if rename:
920 copies.append((fn, rename[0]))
921 copies.append((fn, rename[0]))
921 displayer.show(ctx, copies=copies)
922 displayer.show(ctx, copies=copies)
922 displayer.flush(ctx)
923 displayer.flush(ctx)
923 displayer.close()
924 displayer.close()
924
925
925 def checkunsupportedgraphflags(pats, opts):
926 def checkunsupportedgraphflags(pats, opts):
926 for op in ["newest_first"]:
927 for op in ["newest_first"]:
927 if op in opts and opts[op]:
928 if op in opts and opts[op]:
928 raise error.Abort(_("-G/--graph option is incompatible with --%s")
929 raise error.Abort(_("-G/--graph option is incompatible with --%s")
929 % op.replace("_", "-"))
930 % op.replace("_", "-"))
930
931
931 def graphrevs(repo, nodes, opts):
932 def graphrevs(repo, nodes, opts):
932 limit = getlimit(opts)
933 limit = getlimit(opts)
933 nodes.reverse()
934 nodes.reverse()
934 if limit is not None:
935 if limit is not None:
935 nodes = nodes[:limit]
936 nodes = nodes[:limit]
936 return graphmod.nodes(repo, nodes)
937 return graphmod.nodes(repo, nodes)
General Comments 0
You need to be logged in to leave comments. Login now