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