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