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