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