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