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