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