##// END OF EJS Templates
templatespec: use new factory functions in logcmdutil...
Martin von Zweigbergk -
r45827:c1915cfa default
parent child Browse files
Show More
@@ -1,1084 +1,1084 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 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)
606 assert not (tmpl and mapfile)
607 if mapfile:
607 if mapfile:
608 return formatter.mapfile_templatespec(b'changeset', mapfile)
608 return formatter.mapfile_templatespec(b'changeset', mapfile)
609 else:
609 else:
610 return formatter.literal_templatespec(tmpl)
610 return formatter.literal_templatespec(tmpl)
611
611
612
612
613 def _lookuptemplate(ui, tmpl, style):
613 def _lookuptemplate(ui, tmpl, style):
614 """Find the template matching the given template spec or style
614 """Find the template matching the given template spec or style
615
615
616 See formatter.lookuptemplate() for details.
616 See formatter.lookuptemplate() for details.
617 """
617 """
618
618
619 # ui settings
619 # ui settings
620 if not tmpl and not style: # template are stronger than style
620 if not tmpl and not style: # template are stronger than style
621 tmpl = ui.config(b'ui', b'logtemplate')
621 tmpl = ui.config(b'ui', b'logtemplate')
622 if tmpl:
622 if tmpl:
623 return templatespec(templater.unquotestring(tmpl), None)
623 return formatter.literal_templatespec(templater.unquotestring(tmpl))
624 else:
624 else:
625 style = util.expandpath(ui.config(b'ui', b'style'))
625 style = util.expandpath(ui.config(b'ui', b'style'))
626
626
627 if not tmpl and style:
627 if not tmpl and style:
628 mapfile = style
628 mapfile = style
629 if not os.path.split(mapfile)[0]:
629 if not os.path.split(mapfile)[0]:
630 mapname = templater.templatepath(
630 mapname = templater.templatepath(
631 b'map-cmdline.' + mapfile
631 b'map-cmdline.' + mapfile
632 ) or templater.templatepath(mapfile)
632 ) or templater.templatepath(mapfile)
633 if mapname:
633 if mapname:
634 mapfile = mapname
634 mapfile = mapname
635 return templatespec(None, mapfile)
635 return formatter.mapfile_templatespec(b'changeset', mapfile)
636
636
637 return formatter.lookuptemplate(ui, b'changeset', tmpl)
637 return formatter.lookuptemplate(ui, b'changeset', tmpl)
638
638
639
639
640 def maketemplater(ui, repo, tmpl, buffered=False):
640 def maketemplater(ui, repo, tmpl, buffered=False):
641 """Create a changesettemplater from a literal template 'tmpl'
641 """Create a changesettemplater from a literal template 'tmpl'
642 byte-string."""
642 byte-string."""
643 spec = templatespec(tmpl, None)
643 spec = formatter.literal_templatespec(tmpl)
644 return changesettemplater(ui, repo, spec, buffered=buffered)
644 return changesettemplater(ui, repo, spec, buffered=buffered)
645
645
646
646
647 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
647 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
648 """show one changeset using template or regular display.
648 """show one changeset using template or regular display.
649
649
650 Display format will be the first non-empty hit of:
650 Display format will be the first non-empty hit of:
651 1. option 'template'
651 1. option 'template'
652 2. option 'style'
652 2. option 'style'
653 3. [ui] setting 'logtemplate'
653 3. [ui] setting 'logtemplate'
654 4. [ui] setting 'style'
654 4. [ui] setting 'style'
655 If all of these values are either the unset or the empty string,
655 If all of these values are either the unset or the empty string,
656 regular display via changesetprinter() is done.
656 regular display via changesetprinter() is done.
657 """
657 """
658 postargs = (differ, opts, buffered)
658 postargs = (differ, opts, buffered)
659 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
659 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
660
660
661 # machine-readable formats have slightly different keyword set than
661 # machine-readable formats have slightly different keyword set than
662 # plain templates, which are handled by changesetformatter.
662 # plain templates, which are handled by changesetformatter.
663 # note that {b'pickle', b'debug'} can also be added to the list if needed.
663 # note that {b'pickle', b'debug'} can also be added to the list if needed.
664 if spec.ref in {b'cbor', b'json'}:
664 if spec.ref in {b'cbor', b'json'}:
665 fm = ui.formatter(b'log', opts)
665 fm = ui.formatter(b'log', opts)
666 return changesetformatter(ui, repo, fm, *postargs)
666 return changesetformatter(ui, repo, fm, *postargs)
667
667
668 if not spec.ref and not spec.tmpl and not spec.mapfile:
668 if not spec.ref and not spec.tmpl and not spec.mapfile:
669 return changesetprinter(ui, repo, *postargs)
669 return changesetprinter(ui, repo, *postargs)
670
670
671 return changesettemplater(ui, repo, spec, *postargs)
671 return changesettemplater(ui, repo, spec, *postargs)
672
672
673
673
674 def _makematcher(repo, revs, pats, opts):
674 def _makematcher(repo, revs, pats, opts):
675 """Build matcher and expanded patterns from log options
675 """Build matcher and expanded patterns from log options
676
676
677 If --follow, revs are the revisions to follow from.
677 If --follow, revs are the revisions to follow from.
678
678
679 Returns (match, pats, slowpath) where
679 Returns (match, pats, slowpath) where
680 - match: a matcher built from the given pats and -I/-X opts
680 - match: a matcher built from the given pats and -I/-X opts
681 - pats: patterns used (globs are expanded on Windows)
681 - pats: patterns used (globs are expanded on Windows)
682 - slowpath: True if patterns aren't as simple as scanning filelogs
682 - slowpath: True if patterns aren't as simple as scanning filelogs
683 """
683 """
684 # pats/include/exclude are passed to match.match() directly in
684 # pats/include/exclude are passed to match.match() directly in
685 # _matchfiles() revset but walkchangerevs() builds its matcher with
685 # _matchfiles() revset but walkchangerevs() builds its matcher with
686 # scmutil.match(). The difference is input pats are globbed on
686 # scmutil.match(). The difference is input pats are globbed on
687 # platforms without shell expansion (windows).
687 # platforms without shell expansion (windows).
688 wctx = repo[None]
688 wctx = repo[None]
689 match, pats = scmutil.matchandpats(wctx, pats, opts)
689 match, pats = scmutil.matchandpats(wctx, pats, opts)
690 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
690 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
691 if not slowpath:
691 if not slowpath:
692 follow = opts.get(b'follow') or opts.get(b'follow_first')
692 follow = opts.get(b'follow') or opts.get(b'follow_first')
693 startctxs = []
693 startctxs = []
694 if follow and opts.get(b'rev'):
694 if follow and opts.get(b'rev'):
695 startctxs = [repo[r] for r in revs]
695 startctxs = [repo[r] for r in revs]
696 for f in match.files():
696 for f in match.files():
697 if follow and startctxs:
697 if follow and startctxs:
698 # No idea if the path was a directory at that revision, so
698 # No idea if the path was a directory at that revision, so
699 # take the slow path.
699 # take the slow path.
700 if any(f not in c for c in startctxs):
700 if any(f not in c for c in startctxs):
701 slowpath = True
701 slowpath = True
702 continue
702 continue
703 elif follow and f not in wctx:
703 elif follow and f not in wctx:
704 # If the file exists, it may be a directory, so let it
704 # If the file exists, it may be a directory, so let it
705 # take the slow path.
705 # take the slow path.
706 if os.path.exists(repo.wjoin(f)):
706 if os.path.exists(repo.wjoin(f)):
707 slowpath = True
707 slowpath = True
708 continue
708 continue
709 else:
709 else:
710 raise error.Abort(
710 raise error.Abort(
711 _(
711 _(
712 b'cannot follow file not in parent '
712 b'cannot follow file not in parent '
713 b'revision: "%s"'
713 b'revision: "%s"'
714 )
714 )
715 % f
715 % f
716 )
716 )
717 filelog = repo.file(f)
717 filelog = repo.file(f)
718 if not filelog:
718 if not filelog:
719 # A zero count may be a directory or deleted file, so
719 # A zero count may be a directory or deleted file, so
720 # try to find matching entries on the slow path.
720 # try to find matching entries on the slow path.
721 if follow:
721 if follow:
722 raise error.Abort(
722 raise error.Abort(
723 _(b'cannot follow nonexistent file: "%s"') % f
723 _(b'cannot follow nonexistent file: "%s"') % f
724 )
724 )
725 slowpath = True
725 slowpath = True
726
726
727 # We decided to fall back to the slowpath because at least one
727 # We decided to fall back to the slowpath because at least one
728 # of the paths was not a file. Check to see if at least one of them
728 # of the paths was not a file. Check to see if at least one of them
729 # existed in history - in that case, we'll continue down the
729 # existed in history - in that case, we'll continue down the
730 # slowpath; otherwise, we can turn off the slowpath
730 # slowpath; otherwise, we can turn off the slowpath
731 if slowpath:
731 if slowpath:
732 for path in match.files():
732 for path in match.files():
733 if path == b'.' or path in repo.store:
733 if path == b'.' or path in repo.store:
734 break
734 break
735 else:
735 else:
736 slowpath = False
736 slowpath = False
737
737
738 return match, pats, slowpath
738 return match, pats, slowpath
739
739
740
740
741 def _fileancestors(repo, revs, match, followfirst):
741 def _fileancestors(repo, revs, match, followfirst):
742 fctxs = []
742 fctxs = []
743 for r in revs:
743 for r in revs:
744 ctx = repo[r]
744 ctx = repo[r]
745 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
745 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
746
746
747 # When displaying a revision with --patch --follow FILE, we have
747 # When displaying a revision with --patch --follow FILE, we have
748 # to know which file of the revision must be diffed. With
748 # to know which file of the revision must be diffed. With
749 # --follow, we want the names of the ancestors of FILE in the
749 # --follow, we want the names of the ancestors of FILE in the
750 # revision, stored in "fcache". "fcache" is populated as a side effect
750 # revision, stored in "fcache". "fcache" is populated as a side effect
751 # of the graph traversal.
751 # of the graph traversal.
752 fcache = {}
752 fcache = {}
753
753
754 def filematcher(ctx):
754 def filematcher(ctx):
755 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
755 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
756
756
757 def revgen():
757 def revgen():
758 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
758 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
759 fcache[rev] = [c.path() for c in cs]
759 fcache[rev] = [c.path() for c in cs]
760 yield rev
760 yield rev
761
761
762 return smartset.generatorset(revgen(), iterasc=False), filematcher
762 return smartset.generatorset(revgen(), iterasc=False), filematcher
763
763
764
764
765 def _makenofollowfilematcher(repo, pats, opts):
765 def _makenofollowfilematcher(repo, pats, opts):
766 '''hook for extensions to override the filematcher for non-follow cases'''
766 '''hook for extensions to override the filematcher for non-follow cases'''
767 return None
767 return None
768
768
769
769
770 _opt2logrevset = {
770 _opt2logrevset = {
771 b'no_merges': (b'not merge()', None),
771 b'no_merges': (b'not merge()', None),
772 b'only_merges': (b'merge()', None),
772 b'only_merges': (b'merge()', None),
773 b'_matchfiles': (None, b'_matchfiles(%ps)'),
773 b'_matchfiles': (None, b'_matchfiles(%ps)'),
774 b'date': (b'date(%s)', None),
774 b'date': (b'date(%s)', None),
775 b'branch': (b'branch(%s)', b'%lr'),
775 b'branch': (b'branch(%s)', b'%lr'),
776 b'_patslog': (b'filelog(%s)', b'%lr'),
776 b'_patslog': (b'filelog(%s)', b'%lr'),
777 b'keyword': (b'keyword(%s)', b'%lr'),
777 b'keyword': (b'keyword(%s)', b'%lr'),
778 b'prune': (b'ancestors(%s)', b'not %lr'),
778 b'prune': (b'ancestors(%s)', b'not %lr'),
779 b'user': (b'user(%s)', b'%lr'),
779 b'user': (b'user(%s)', b'%lr'),
780 }
780 }
781
781
782
782
783 def _makerevset(repo, match, pats, slowpath, opts):
783 def _makerevset(repo, match, pats, slowpath, opts):
784 """Return a revset string built from log options and file patterns"""
784 """Return a revset string built from log options and file patterns"""
785 opts = dict(opts)
785 opts = dict(opts)
786 # follow or not follow?
786 # follow or not follow?
787 follow = opts.get(b'follow') or opts.get(b'follow_first')
787 follow = opts.get(b'follow') or opts.get(b'follow_first')
788
788
789 # branch and only_branch are really aliases and must be handled at
789 # branch and only_branch are really aliases and must be handled at
790 # the same time
790 # the same time
791 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
791 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
792 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
792 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
793
793
794 if slowpath:
794 if slowpath:
795 # See walkchangerevs() slow path.
795 # See walkchangerevs() slow path.
796 #
796 #
797 # pats/include/exclude cannot be represented as separate
797 # pats/include/exclude cannot be represented as separate
798 # revset expressions as their filtering logic applies at file
798 # revset expressions as their filtering logic applies at file
799 # level. For instance "-I a -X b" matches a revision touching
799 # level. For instance "-I a -X b" matches a revision touching
800 # "a" and "b" while "file(a) and not file(b)" does
800 # "a" and "b" while "file(a) and not file(b)" does
801 # not. Besides, filesets are evaluated against the working
801 # not. Besides, filesets are evaluated against the working
802 # directory.
802 # directory.
803 matchargs = [b'r:', b'd:relpath']
803 matchargs = [b'r:', b'd:relpath']
804 for p in pats:
804 for p in pats:
805 matchargs.append(b'p:' + p)
805 matchargs.append(b'p:' + p)
806 for p in opts.get(b'include', []):
806 for p in opts.get(b'include', []):
807 matchargs.append(b'i:' + p)
807 matchargs.append(b'i:' + p)
808 for p in opts.get(b'exclude', []):
808 for p in opts.get(b'exclude', []):
809 matchargs.append(b'x:' + p)
809 matchargs.append(b'x:' + p)
810 opts[b'_matchfiles'] = matchargs
810 opts[b'_matchfiles'] = matchargs
811 elif not follow:
811 elif not follow:
812 opts[b'_patslog'] = list(pats)
812 opts[b'_patslog'] = list(pats)
813
813
814 expr = []
814 expr = []
815 for op, val in sorted(pycompat.iteritems(opts)):
815 for op, val in sorted(pycompat.iteritems(opts)):
816 if not val:
816 if not val:
817 continue
817 continue
818 if op not in _opt2logrevset:
818 if op not in _opt2logrevset:
819 continue
819 continue
820 revop, listop = _opt2logrevset[op]
820 revop, listop = _opt2logrevset[op]
821 if revop and b'%' not in revop:
821 if revop and b'%' not in revop:
822 expr.append(revop)
822 expr.append(revop)
823 elif not listop:
823 elif not listop:
824 expr.append(revsetlang.formatspec(revop, val))
824 expr.append(revsetlang.formatspec(revop, val))
825 else:
825 else:
826 if revop:
826 if revop:
827 val = [revsetlang.formatspec(revop, v) for v in val]
827 val = [revsetlang.formatspec(revop, v) for v in val]
828 expr.append(revsetlang.formatspec(listop, val))
828 expr.append(revsetlang.formatspec(listop, val))
829
829
830 if expr:
830 if expr:
831 expr = b'(' + b' and '.join(expr) + b')'
831 expr = b'(' + b' and '.join(expr) + b')'
832 else:
832 else:
833 expr = None
833 expr = None
834 return expr
834 return expr
835
835
836
836
837 def _initialrevs(repo, opts):
837 def _initialrevs(repo, opts):
838 """Return the initial set of revisions to be filtered or followed"""
838 """Return the initial set of revisions to be filtered or followed"""
839 follow = opts.get(b'follow') or opts.get(b'follow_first')
839 follow = opts.get(b'follow') or opts.get(b'follow_first')
840 if opts.get(b'rev'):
840 if opts.get(b'rev'):
841 revs = scmutil.revrange(repo, opts[b'rev'])
841 revs = scmutil.revrange(repo, opts[b'rev'])
842 elif follow and repo.dirstate.p1() == nullid:
842 elif follow and repo.dirstate.p1() == nullid:
843 revs = smartset.baseset()
843 revs = smartset.baseset()
844 elif follow:
844 elif follow:
845 revs = repo.revs(b'.')
845 revs = repo.revs(b'.')
846 else:
846 else:
847 revs = smartset.spanset(repo)
847 revs = smartset.spanset(repo)
848 revs.reverse()
848 revs.reverse()
849 return revs
849 return revs
850
850
851
851
852 def getrevs(repo, pats, opts):
852 def getrevs(repo, pats, opts):
853 # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
853 # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
854 """Return (revs, differ) where revs is a smartset
854 """Return (revs, differ) where revs is a smartset
855
855
856 differ is a changesetdiffer with pre-configured file matcher.
856 differ is a changesetdiffer with pre-configured file matcher.
857 """
857 """
858 follow = opts.get(b'follow') or opts.get(b'follow_first')
858 follow = opts.get(b'follow') or opts.get(b'follow_first')
859 followfirst = opts.get(b'follow_first')
859 followfirst = opts.get(b'follow_first')
860 limit = getlimit(opts)
860 limit = getlimit(opts)
861 revs = _initialrevs(repo, opts)
861 revs = _initialrevs(repo, opts)
862 if not revs:
862 if not revs:
863 return smartset.baseset(), None
863 return smartset.baseset(), None
864 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
864 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
865 filematcher = None
865 filematcher = None
866 if follow:
866 if follow:
867 if slowpath or match.always():
867 if slowpath or match.always():
868 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
868 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
869 else:
869 else:
870 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
870 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
871 revs.reverse()
871 revs.reverse()
872 if filematcher is None:
872 if filematcher is None:
873 filematcher = _makenofollowfilematcher(repo, pats, opts)
873 filematcher = _makenofollowfilematcher(repo, pats, opts)
874 if filematcher is None:
874 if filematcher is None:
875
875
876 def filematcher(ctx):
876 def filematcher(ctx):
877 return match
877 return match
878
878
879 expr = _makerevset(repo, match, pats, slowpath, opts)
879 expr = _makerevset(repo, match, pats, slowpath, opts)
880 if opts.get(b'graph'):
880 if opts.get(b'graph'):
881 # User-specified revs might be unsorted, but don't sort before
881 # User-specified revs might be unsorted, but don't sort before
882 # _makerevset because it might depend on the order of revs
882 # _makerevset because it might depend on the order of revs
883 if repo.ui.configbool(b'experimental', b'log.topo'):
883 if repo.ui.configbool(b'experimental', b'log.topo'):
884 if not revs.istopo():
884 if not revs.istopo():
885 revs = dagop.toposort(revs, repo.changelog.parentrevs)
885 revs = dagop.toposort(revs, repo.changelog.parentrevs)
886 # TODO: try to iterate the set lazily
886 # TODO: try to iterate the set lazily
887 revs = revset.baseset(list(revs), istopo=True)
887 revs = revset.baseset(list(revs), istopo=True)
888 elif not (revs.isdescending() or revs.istopo()):
888 elif not (revs.isdescending() or revs.istopo()):
889 revs.sort(reverse=True)
889 revs.sort(reverse=True)
890 if expr:
890 if expr:
891 matcher = revset.match(None, expr)
891 matcher = revset.match(None, expr)
892 revs = matcher(repo, revs)
892 revs = matcher(repo, revs)
893 if limit is not None:
893 if limit is not None:
894 revs = revs.slice(0, limit)
894 revs = revs.slice(0, limit)
895
895
896 differ = changesetdiffer()
896 differ = changesetdiffer()
897 differ._makefilematcher = filematcher
897 differ._makefilematcher = filematcher
898 return revs, differ
898 return revs, differ
899
899
900
900
901 def _parselinerangeopt(repo, opts):
901 def _parselinerangeopt(repo, opts):
902 """Parse --line-range log option and return a list of tuples (filename,
902 """Parse --line-range log option and return a list of tuples (filename,
903 (fromline, toline)).
903 (fromline, toline)).
904 """
904 """
905 linerangebyfname = []
905 linerangebyfname = []
906 for pat in opts.get(b'line_range', []):
906 for pat in opts.get(b'line_range', []):
907 try:
907 try:
908 pat, linerange = pat.rsplit(b',', 1)
908 pat, linerange = pat.rsplit(b',', 1)
909 except ValueError:
909 except ValueError:
910 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
910 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
911 try:
911 try:
912 fromline, toline = map(int, linerange.split(b':'))
912 fromline, toline = map(int, linerange.split(b':'))
913 except ValueError:
913 except ValueError:
914 raise error.Abort(_(b"invalid line range for %s") % pat)
914 raise error.Abort(_(b"invalid line range for %s") % pat)
915 msg = _(b"line range pattern '%s' must match exactly one file") % pat
915 msg = _(b"line range pattern '%s' must match exactly one file") % pat
916 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
916 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
917 linerangebyfname.append(
917 linerangebyfname.append(
918 (fname, util.processlinerange(fromline, toline))
918 (fname, util.processlinerange(fromline, toline))
919 )
919 )
920 return linerangebyfname
920 return linerangebyfname
921
921
922
922
923 def getlinerangerevs(repo, userrevs, opts):
923 def getlinerangerevs(repo, userrevs, opts):
924 """Return (revs, differ).
924 """Return (revs, differ).
925
925
926 "revs" are revisions obtained by processing "line-range" log options and
926 "revs" are revisions obtained by processing "line-range" log options and
927 walking block ancestors of each specified file/line-range.
927 walking block ancestors of each specified file/line-range.
928
928
929 "differ" is a changesetdiffer with pre-configured file matcher and hunks
929 "differ" is a changesetdiffer with pre-configured file matcher and hunks
930 filter.
930 filter.
931 """
931 """
932 wctx = repo[None]
932 wctx = repo[None]
933
933
934 # Two-levels map of "rev -> file ctx -> [line range]".
934 # Two-levels map of "rev -> file ctx -> [line range]".
935 linerangesbyrev = {}
935 linerangesbyrev = {}
936 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
936 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
937 if fname not in wctx:
937 if fname not in wctx:
938 raise error.Abort(
938 raise error.Abort(
939 _(b'cannot follow file not in parent revision: "%s"') % fname
939 _(b'cannot follow file not in parent revision: "%s"') % fname
940 )
940 )
941 fctx = wctx.filectx(fname)
941 fctx = wctx.filectx(fname)
942 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
942 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
943 rev = fctx.introrev()
943 rev = fctx.introrev()
944 if rev is None:
944 if rev is None:
945 rev = wdirrev
945 rev = wdirrev
946 if rev not in userrevs:
946 if rev not in userrevs:
947 continue
947 continue
948 linerangesbyrev.setdefault(rev, {}).setdefault(
948 linerangesbyrev.setdefault(rev, {}).setdefault(
949 fctx.path(), []
949 fctx.path(), []
950 ).append(linerange)
950 ).append(linerange)
951
951
952 def nofilterhunksfn(fctx, hunks):
952 def nofilterhunksfn(fctx, hunks):
953 return hunks
953 return hunks
954
954
955 def hunksfilter(ctx):
955 def hunksfilter(ctx):
956 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
956 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
957 if fctxlineranges is None:
957 if fctxlineranges is None:
958 return nofilterhunksfn
958 return nofilterhunksfn
959
959
960 def filterfn(fctx, hunks):
960 def filterfn(fctx, hunks):
961 lineranges = fctxlineranges.get(fctx.path())
961 lineranges = fctxlineranges.get(fctx.path())
962 if lineranges is not None:
962 if lineranges is not None:
963 for hr, lines in hunks:
963 for hr, lines in hunks:
964 if hr is None: # binary
964 if hr is None: # binary
965 yield hr, lines
965 yield hr, lines
966 continue
966 continue
967 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
967 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
968 yield hr, lines
968 yield hr, lines
969 else:
969 else:
970 for hunk in hunks:
970 for hunk in hunks:
971 yield hunk
971 yield hunk
972
972
973 return filterfn
973 return filterfn
974
974
975 def filematcher(ctx):
975 def filematcher(ctx):
976 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
976 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
977 return scmutil.matchfiles(repo, files)
977 return scmutil.matchfiles(repo, files)
978
978
979 revs = sorted(linerangesbyrev, reverse=True)
979 revs = sorted(linerangesbyrev, reverse=True)
980
980
981 differ = changesetdiffer()
981 differ = changesetdiffer()
982 differ._makefilematcher = filematcher
982 differ._makefilematcher = filematcher
983 differ._makehunksfilter = hunksfilter
983 differ._makehunksfilter = hunksfilter
984 return smartset.baseset(revs), differ
984 return smartset.baseset(revs), differ
985
985
986
986
987 def _graphnodeformatter(ui, displayer):
987 def _graphnodeformatter(ui, displayer):
988 spec = ui.config(b'ui', b'graphnodetemplate')
988 spec = ui.config(b'ui', b'graphnodetemplate')
989 if not spec:
989 if not spec:
990 return templatekw.getgraphnode # fast path for "{graphnode}"
990 return templatekw.getgraphnode # fast path for "{graphnode}"
991
991
992 spec = templater.unquotestring(spec)
992 spec = templater.unquotestring(spec)
993 if isinstance(displayer, changesettemplater):
993 if isinstance(displayer, changesettemplater):
994 # reuse cache of slow templates
994 # reuse cache of slow templates
995 tres = displayer._tresources
995 tres = displayer._tresources
996 else:
996 else:
997 tres = formatter.templateresources(ui)
997 tres = formatter.templateresources(ui)
998 templ = formatter.maketemplater(
998 templ = formatter.maketemplater(
999 ui, spec, defaults=templatekw.keywords, resources=tres
999 ui, spec, defaults=templatekw.keywords, resources=tres
1000 )
1000 )
1001
1001
1002 def formatnode(repo, ctx, cache):
1002 def formatnode(repo, ctx, cache):
1003 props = {b'ctx': ctx, b'repo': repo}
1003 props = {b'ctx': ctx, b'repo': repo}
1004 return templ.renderdefault(props)
1004 return templ.renderdefault(props)
1005
1005
1006 return formatnode
1006 return formatnode
1007
1007
1008
1008
1009 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1009 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1010 props = props or {}
1010 props = props or {}
1011 formatnode = _graphnodeformatter(ui, displayer)
1011 formatnode = _graphnodeformatter(ui, displayer)
1012 state = graphmod.asciistate()
1012 state = graphmod.asciistate()
1013 styles = state.styles
1013 styles = state.styles
1014
1014
1015 # only set graph styling if HGPLAIN is not set.
1015 # only set graph styling if HGPLAIN is not set.
1016 if ui.plain(b'graph'):
1016 if ui.plain(b'graph'):
1017 # set all edge styles to |, the default pre-3.8 behaviour
1017 # set all edge styles to |, the default pre-3.8 behaviour
1018 styles.update(dict.fromkeys(styles, b'|'))
1018 styles.update(dict.fromkeys(styles, b'|'))
1019 else:
1019 else:
1020 edgetypes = {
1020 edgetypes = {
1021 b'parent': graphmod.PARENT,
1021 b'parent': graphmod.PARENT,
1022 b'grandparent': graphmod.GRANDPARENT,
1022 b'grandparent': graphmod.GRANDPARENT,
1023 b'missing': graphmod.MISSINGPARENT,
1023 b'missing': graphmod.MISSINGPARENT,
1024 }
1024 }
1025 for name, key in edgetypes.items():
1025 for name, key in edgetypes.items():
1026 # experimental config: experimental.graphstyle.*
1026 # experimental config: experimental.graphstyle.*
1027 styles[key] = ui.config(
1027 styles[key] = ui.config(
1028 b'experimental', b'graphstyle.%s' % name, styles[key]
1028 b'experimental', b'graphstyle.%s' % name, styles[key]
1029 )
1029 )
1030 if not styles[key]:
1030 if not styles[key]:
1031 styles[key] = None
1031 styles[key] = None
1032
1032
1033 # experimental config: experimental.graphshorten
1033 # experimental config: experimental.graphshorten
1034 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1034 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1035
1035
1036 formatnode_cache = {}
1036 formatnode_cache = {}
1037 for rev, type, ctx, parents in dag:
1037 for rev, type, ctx, parents in dag:
1038 char = formatnode(repo, ctx, formatnode_cache)
1038 char = formatnode(repo, ctx, formatnode_cache)
1039 copies = getcopies(ctx) if getcopies else None
1039 copies = getcopies(ctx) if getcopies else None
1040 edges = edgefn(type, char, state, rev, parents)
1040 edges = edgefn(type, char, state, rev, parents)
1041 firstedge = next(edges)
1041 firstedge = next(edges)
1042 width = firstedge[2]
1042 width = firstedge[2]
1043 displayer.show(
1043 displayer.show(
1044 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1044 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1045 )
1045 )
1046 lines = displayer.hunk.pop(rev).split(b'\n')
1046 lines = displayer.hunk.pop(rev).split(b'\n')
1047 if not lines[-1]:
1047 if not lines[-1]:
1048 del lines[-1]
1048 del lines[-1]
1049 displayer.flush(ctx)
1049 displayer.flush(ctx)
1050 for type, char, width, coldata in itertools.chain([firstedge], edges):
1050 for type, char, width, coldata in itertools.chain([firstedge], edges):
1051 graphmod.ascii(ui, state, type, char, lines, coldata)
1051 graphmod.ascii(ui, state, type, char, lines, coldata)
1052 lines = []
1052 lines = []
1053 displayer.close()
1053 displayer.close()
1054
1054
1055
1055
1056 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1056 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1057 revdag = graphmod.dagwalker(repo, revs)
1057 revdag = graphmod.dagwalker(repo, revs)
1058 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1058 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1059
1059
1060
1060
1061 def displayrevs(ui, repo, revs, displayer, getcopies):
1061 def displayrevs(ui, repo, revs, displayer, getcopies):
1062 for rev in revs:
1062 for rev in revs:
1063 ctx = repo[rev]
1063 ctx = repo[rev]
1064 copies = getcopies(ctx) if getcopies else None
1064 copies = getcopies(ctx) if getcopies else None
1065 displayer.show(ctx, copies=copies)
1065 displayer.show(ctx, copies=copies)
1066 displayer.flush(ctx)
1066 displayer.flush(ctx)
1067 displayer.close()
1067 displayer.close()
1068
1068
1069
1069
1070 def checkunsupportedgraphflags(pats, opts):
1070 def checkunsupportedgraphflags(pats, opts):
1071 for op in [b"newest_first"]:
1071 for op in [b"newest_first"]:
1072 if op in opts and opts[op]:
1072 if op in opts and opts[op]:
1073 raise error.Abort(
1073 raise error.Abort(
1074 _(b"-G/--graph option is incompatible with --%s")
1074 _(b"-G/--graph option is incompatible with --%s")
1075 % op.replace(b"_", b"-")
1075 % op.replace(b"_", b"-")
1076 )
1076 )
1077
1077
1078
1078
1079 def graphrevs(repo, nodes, opts):
1079 def graphrevs(repo, nodes, opts):
1080 limit = getlimit(opts)
1080 limit = getlimit(opts)
1081 nodes.reverse()
1081 nodes.reverse()
1082 if limit is not None:
1082 if limit is not None:
1083 nodes = nodes[:limit]
1083 nodes = nodes[:limit]
1084 return graphmod.nodes(repo, nodes)
1084 return graphmod.nodes(repo, nodes)
General Comments 0
You need to be logged in to leave comments. Login now