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