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