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