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