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