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