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