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