##// END OF EJS Templates
log: pass around --rev option by walkopts
Yuya Nishihara -
r46142:9a26fea2 default
parent child Browse files
Show More
@@ -1,1139 +1,1149 b''
1 # logcmdutil.py - utility for log-like commands
1 # logcmdutil.py - utility for log-like commands
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import itertools
10 import itertools
11 import os
11 import os
12 import posixpath
12 import posixpath
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 nullid,
16 nullid,
17 wdirid,
17 wdirid,
18 wdirrev,
18 wdirrev,
19 )
19 )
20
20
21 from .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 # a list of revset expressions to be traversed; if follow, it specifies
690 # the start revisions
691 revspec = attr.ib() # type: List[bytes]
692
689 # 0: no follow, 1: follow first, 2: follow both parents
693 # 0: no follow, 1: follow first, 2: follow both parents
690 follow = attr.ib(default=0) # type: int
694 follow = attr.ib(default=0) # type: int
691
695
692 # limit number of changes displayed; None means unlimited
696 # limit number of changes displayed; None means unlimited
693 limit = attr.ib(default=None) # type: Optional[int]
697 limit = attr.ib(default=None) # type: Optional[int]
694
698
695
699
696 def parseopts(ui, pats, opts):
700 def parseopts(ui, pats, opts):
697 # type: (Any, List[bytes], Dict[bytes, Any]) -> walkopts
701 # type: (Any, List[bytes], Dict[bytes, Any]) -> walkopts
698 """Parse log command options into walkopts
702 """Parse log command options into walkopts
699
703
700 The returned walkopts will be passed in to getrevs().
704 The returned walkopts will be passed in to getrevs().
701 """
705 """
702 if opts.get(b'follow_first'):
706 if opts.get(b'follow_first'):
703 follow = 1
707 follow = 1
704 elif opts.get(b'follow'):
708 elif opts.get(b'follow'):
705 follow = 2
709 follow = 2
706 else:
710 else:
707 follow = 0
711 follow = 0
708
712
709 return walkopts(pats=pats, opts=opts, follow=follow, limit=getlimit(opts))
713 return walkopts(
714 pats=pats,
715 opts=opts,
716 revspec=opts.get(b'rev', []),
717 follow=follow,
718 limit=getlimit(opts),
719 )
710
720
711
721
712 def _makematcher(repo, revs, wopts):
722 def _makematcher(repo, revs, wopts):
713 """Build matcher and expanded patterns from log options
723 """Build matcher and expanded patterns from log options
714
724
715 If --follow, revs are the revisions to follow from.
725 If --follow, revs are the revisions to follow from.
716
726
717 Returns (match, pats, slowpath) where
727 Returns (match, pats, slowpath) where
718 - match: a matcher built from the given pats and -I/-X opts
728 - match: a matcher built from the given pats and -I/-X opts
719 - pats: patterns used (globs are expanded on Windows)
729 - pats: patterns used (globs are expanded on Windows)
720 - slowpath: True if patterns aren't as simple as scanning filelogs
730 - slowpath: True if patterns aren't as simple as scanning filelogs
721 """
731 """
722 # pats/include/exclude are passed to match.match() directly in
732 # pats/include/exclude are passed to match.match() directly in
723 # _matchfiles() revset but walkchangerevs() builds its matcher with
733 # _matchfiles() revset but walkchangerevs() builds its matcher with
724 # scmutil.match(). The difference is input pats are globbed on
734 # scmutil.match(). The difference is input pats are globbed on
725 # platforms without shell expansion (windows).
735 # platforms without shell expansion (windows).
726 wctx = repo[None]
736 wctx = repo[None]
727 match, pats = scmutil.matchandpats(wctx, wopts.pats, wopts.opts)
737 match, pats = scmutil.matchandpats(wctx, wopts.pats, wopts.opts)
728 slowpath = match.anypats() or (
738 slowpath = match.anypats() or (
729 not match.always() and wopts.opts.get(b'removed')
739 not match.always() and wopts.opts.get(b'removed')
730 )
740 )
731 if not slowpath:
741 if not slowpath:
732 if wopts.follow and wopts.opts.get(b'rev'):
742 if wopts.follow and wopts.revspec:
733 # There may be the case that a path doesn't exist in some (but
743 # There may be the case that a path doesn't exist in some (but
734 # not all) of the specified start revisions, but let's consider
744 # not all) of the specified start revisions, but let's consider
735 # the path is valid. Missing files will be warned by the matcher.
745 # the path is valid. Missing files will be warned by the matcher.
736 startctxs = [repo[r] for r in revs]
746 startctxs = [repo[r] for r in revs]
737 for f in match.files():
747 for f in match.files():
738 found = False
748 found = False
739 for c in startctxs:
749 for c in startctxs:
740 if f in c:
750 if f in c:
741 found = True
751 found = True
742 elif c.hasdir(f):
752 elif c.hasdir(f):
743 # If a directory exists in any of the start revisions,
753 # If a directory exists in any of the start revisions,
744 # take the slow path.
754 # take the slow path.
745 found = slowpath = True
755 found = slowpath = True
746 if not found:
756 if not found:
747 raise error.Abort(
757 raise error.Abort(
748 _(
758 _(
749 b'cannot follow file not in any of the specified '
759 b'cannot follow file not in any of the specified '
750 b'revisions: "%s"'
760 b'revisions: "%s"'
751 )
761 )
752 % f
762 % f
753 )
763 )
754 elif wopts.follow:
764 elif wopts.follow:
755 for f in match.files():
765 for f in match.files():
756 if f not in wctx:
766 if f not in wctx:
757 # If the file exists, it may be a directory, so let it
767 # If the file exists, it may be a directory, so let it
758 # take the slow path.
768 # take the slow path.
759 if os.path.exists(repo.wjoin(f)):
769 if os.path.exists(repo.wjoin(f)):
760 slowpath = True
770 slowpath = True
761 continue
771 continue
762 else:
772 else:
763 raise error.Abort(
773 raise error.Abort(
764 _(
774 _(
765 b'cannot follow file not in parent '
775 b'cannot follow file not in parent '
766 b'revision: "%s"'
776 b'revision: "%s"'
767 )
777 )
768 % f
778 % f
769 )
779 )
770 filelog = repo.file(f)
780 filelog = repo.file(f)
771 if not filelog:
781 if not filelog:
772 # A file exists in wdir but not in history, which means
782 # A file exists in wdir but not in history, which means
773 # the file isn't committed yet.
783 # the file isn't committed yet.
774 raise error.Abort(
784 raise error.Abort(
775 _(b'cannot follow nonexistent file: "%s"') % f
785 _(b'cannot follow nonexistent file: "%s"') % f
776 )
786 )
777 else:
787 else:
778 for f in match.files():
788 for f in match.files():
779 filelog = repo.file(f)
789 filelog = repo.file(f)
780 if not filelog:
790 if not filelog:
781 # A zero count may be a directory or deleted file, so
791 # A zero count may be a directory or deleted file, so
782 # try to find matching entries on the slow path.
792 # try to find matching entries on the slow path.
783 slowpath = True
793 slowpath = True
784
794
785 # We decided to fall back to the slowpath because at least one
795 # We decided to fall back to the slowpath because at least one
786 # of the paths was not a file. Check to see if at least one of them
796 # of the paths was not a file. Check to see if at least one of them
787 # existed in history - in that case, we'll continue down the
797 # existed in history - in that case, we'll continue down the
788 # slowpath; otherwise, we can turn off the slowpath
798 # slowpath; otherwise, we can turn off the slowpath
789 if slowpath:
799 if slowpath:
790 for path in match.files():
800 for path in match.files():
791 if path == b'.' or path in repo.store:
801 if path == b'.' or path in repo.store:
792 break
802 break
793 else:
803 else:
794 slowpath = False
804 slowpath = False
795
805
796 return match, pats, slowpath
806 return match, pats, slowpath
797
807
798
808
799 def _fileancestors(repo, revs, match, followfirst):
809 def _fileancestors(repo, revs, match, followfirst):
800 fctxs = []
810 fctxs = []
801 for r in revs:
811 for r in revs:
802 ctx = repo[r]
812 ctx = repo[r]
803 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
813 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
804
814
805 # When displaying a revision with --patch --follow FILE, we have
815 # When displaying a revision with --patch --follow FILE, we have
806 # to know which file of the revision must be diffed. With
816 # to know which file of the revision must be diffed. With
807 # --follow, we want the names of the ancestors of FILE in the
817 # --follow, we want the names of the ancestors of FILE in the
808 # revision, stored in "fcache". "fcache" is populated as a side effect
818 # revision, stored in "fcache". "fcache" is populated as a side effect
809 # of the graph traversal.
819 # of the graph traversal.
810 fcache = {}
820 fcache = {}
811
821
812 def filematcher(ctx):
822 def filematcher(ctx):
813 return scmutil.matchfiles(repo, fcache.get(scmutil.intrev(ctx), []))
823 return scmutil.matchfiles(repo, fcache.get(scmutil.intrev(ctx), []))
814
824
815 def revgen():
825 def revgen():
816 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
826 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
817 fcache[rev] = [c.path() for c in cs]
827 fcache[rev] = [c.path() for c in cs]
818 yield rev
828 yield rev
819
829
820 return smartset.generatorset(revgen(), iterasc=False), filematcher
830 return smartset.generatorset(revgen(), iterasc=False), filematcher
821
831
822
832
823 def _makenofollowfilematcher(repo, pats, opts):
833 def _makenofollowfilematcher(repo, pats, opts):
824 '''hook for extensions to override the filematcher for non-follow cases'''
834 '''hook for extensions to override the filematcher for non-follow cases'''
825 return None
835 return None
826
836
827
837
828 _opt2logrevset = {
838 _opt2logrevset = {
829 b'no_merges': (b'not merge()', None),
839 b'no_merges': (b'not merge()', None),
830 b'only_merges': (b'merge()', None),
840 b'only_merges': (b'merge()', None),
831 b'_matchfiles': (None, b'_matchfiles(%ps)'),
841 b'_matchfiles': (None, b'_matchfiles(%ps)'),
832 b'date': (b'date(%s)', None),
842 b'date': (b'date(%s)', None),
833 b'branch': (b'branch(%s)', b'%lr'),
843 b'branch': (b'branch(%s)', b'%lr'),
834 b'_patslog': (b'filelog(%s)', b'%lr'),
844 b'_patslog': (b'filelog(%s)', b'%lr'),
835 b'keyword': (b'keyword(%s)', b'%lr'),
845 b'keyword': (b'keyword(%s)', b'%lr'),
836 b'prune': (b'ancestors(%s)', b'not %lr'),
846 b'prune': (b'ancestors(%s)', b'not %lr'),
837 b'user': (b'user(%s)', b'%lr'),
847 b'user': (b'user(%s)', b'%lr'),
838 }
848 }
839
849
840
850
841 def _makerevset(repo, wopts, slowpath):
851 def _makerevset(repo, wopts, slowpath):
842 """Return a revset string built from log options and file patterns"""
852 """Return a revset string built from log options and file patterns"""
843 opts = dict(wopts.opts)
853 opts = dict(wopts.opts)
844
854
845 # branch and only_branch are really aliases and must be handled at
855 # branch and only_branch are really aliases and must be handled at
846 # the same time
856 # the same time
847 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
857 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
848 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
858 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
849
859
850 if slowpath:
860 if slowpath:
851 # See walkchangerevs() slow path.
861 # See walkchangerevs() slow path.
852 #
862 #
853 # pats/include/exclude cannot be represented as separate
863 # pats/include/exclude cannot be represented as separate
854 # revset expressions as their filtering logic applies at file
864 # revset expressions as their filtering logic applies at file
855 # level. For instance "-I a -X b" matches a revision touching
865 # level. For instance "-I a -X b" matches a revision touching
856 # "a" and "b" while "file(a) and not file(b)" does
866 # "a" and "b" while "file(a) and not file(b)" does
857 # not. Besides, filesets are evaluated against the working
867 # not. Besides, filesets are evaluated against the working
858 # directory.
868 # directory.
859 matchargs = [b'r:', b'd:relpath']
869 matchargs = [b'r:', b'd:relpath']
860 for p in wopts.pats:
870 for p in wopts.pats:
861 matchargs.append(b'p:' + p)
871 matchargs.append(b'p:' + p)
862 for p in opts.get(b'include', []):
872 for p in opts.get(b'include', []):
863 matchargs.append(b'i:' + p)
873 matchargs.append(b'i:' + p)
864 for p in opts.get(b'exclude', []):
874 for p in opts.get(b'exclude', []):
865 matchargs.append(b'x:' + p)
875 matchargs.append(b'x:' + p)
866 opts[b'_matchfiles'] = matchargs
876 opts[b'_matchfiles'] = matchargs
867 elif not wopts.follow:
877 elif not wopts.follow:
868 opts[b'_patslog'] = list(wopts.pats)
878 opts[b'_patslog'] = list(wopts.pats)
869
879
870 expr = []
880 expr = []
871 for op, val in sorted(pycompat.iteritems(opts)):
881 for op, val in sorted(pycompat.iteritems(opts)):
872 if not val:
882 if not val:
873 continue
883 continue
874 if op not in _opt2logrevset:
884 if op not in _opt2logrevset:
875 continue
885 continue
876 revop, listop = _opt2logrevset[op]
886 revop, listop = _opt2logrevset[op]
877 if revop and b'%' not in revop:
887 if revop and b'%' not in revop:
878 expr.append(revop)
888 expr.append(revop)
879 elif not listop:
889 elif not listop:
880 expr.append(revsetlang.formatspec(revop, val))
890 expr.append(revsetlang.formatspec(revop, val))
881 else:
891 else:
882 if revop:
892 if revop:
883 val = [revsetlang.formatspec(revop, v) for v in val]
893 val = [revsetlang.formatspec(revop, v) for v in val]
884 expr.append(revsetlang.formatspec(listop, val))
894 expr.append(revsetlang.formatspec(listop, val))
885
895
886 if expr:
896 if expr:
887 expr = b'(' + b' and '.join(expr) + b')'
897 expr = b'(' + b' and '.join(expr) + b')'
888 else:
898 else:
889 expr = None
899 expr = None
890 return expr
900 return expr
891
901
892
902
893 def _initialrevs(repo, wopts):
903 def _initialrevs(repo, wopts):
894 """Return the initial set of revisions to be filtered or followed"""
904 """Return the initial set of revisions to be filtered or followed"""
895 if wopts.opts.get(b'rev'):
905 if wopts.revspec:
896 revs = scmutil.revrange(repo, wopts.opts[b'rev'])
906 revs = scmutil.revrange(repo, wopts.revspec)
897 elif wopts.follow and repo.dirstate.p1() == nullid:
907 elif wopts.follow and repo.dirstate.p1() == nullid:
898 revs = smartset.baseset()
908 revs = smartset.baseset()
899 elif wopts.follow:
909 elif wopts.follow:
900 revs = repo.revs(b'.')
910 revs = repo.revs(b'.')
901 else:
911 else:
902 revs = smartset.spanset(repo)
912 revs = smartset.spanset(repo)
903 revs.reverse()
913 revs.reverse()
904 return revs
914 return revs
905
915
906
916
907 def getrevs(repo, wopts):
917 def getrevs(repo, wopts):
908 # type: (Any, walkopts) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
918 # type: (Any, walkopts) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
909 """Return (revs, differ) where revs is a smartset
919 """Return (revs, differ) where revs is a smartset
910
920
911 differ is a changesetdiffer with pre-configured file matcher.
921 differ is a changesetdiffer with pre-configured file matcher.
912 """
922 """
913 revs = _initialrevs(repo, wopts)
923 revs = _initialrevs(repo, wopts)
914 if not revs:
924 if not revs:
915 return smartset.baseset(), None
925 return smartset.baseset(), None
916 match, pats, slowpath = _makematcher(repo, revs, wopts)
926 match, pats, slowpath = _makematcher(repo, revs, wopts)
917 wopts = attr.evolve(wopts, pats=pats)
927 wopts = attr.evolve(wopts, pats=pats)
918
928
919 filematcher = None
929 filematcher = None
920 if wopts.follow:
930 if wopts.follow:
921 if slowpath or match.always():
931 if slowpath or match.always():
922 revs = dagop.revancestors(repo, revs, followfirst=wopts.follow == 1)
932 revs = dagop.revancestors(repo, revs, followfirst=wopts.follow == 1)
923 else:
933 else:
924 revs, filematcher = _fileancestors(
934 revs, filematcher = _fileancestors(
925 repo, revs, match, followfirst=wopts.follow == 1
935 repo, revs, match, followfirst=wopts.follow == 1
926 )
936 )
927 revs.reverse()
937 revs.reverse()
928 if filematcher is None:
938 if filematcher is None:
929 filematcher = _makenofollowfilematcher(repo, wopts.pats, wopts.opts)
939 filematcher = _makenofollowfilematcher(repo, wopts.pats, wopts.opts)
930 if filematcher is None:
940 if filematcher is None:
931
941
932 def filematcher(ctx):
942 def filematcher(ctx):
933 return match
943 return match
934
944
935 expr = _makerevset(repo, wopts, slowpath)
945 expr = _makerevset(repo, wopts, slowpath)
936 if wopts.opts.get(b'graph'):
946 if wopts.opts.get(b'graph'):
937 if repo.ui.configbool(b'experimental', b'log.topo'):
947 if repo.ui.configbool(b'experimental', b'log.topo'):
938 if not revs.istopo():
948 if not revs.istopo():
939 revs = dagop.toposort(revs, repo.changelog.parentrevs)
949 revs = dagop.toposort(revs, repo.changelog.parentrevs)
940 # TODO: try to iterate the set lazily
950 # TODO: try to iterate the set lazily
941 revs = revset.baseset(list(revs), istopo=True)
951 revs = revset.baseset(list(revs), istopo=True)
942 elif not (revs.isdescending() or revs.istopo()):
952 elif not (revs.isdescending() or revs.istopo()):
943 # User-specified revs might be unsorted
953 # User-specified revs might be unsorted
944 revs.sort(reverse=True)
954 revs.sort(reverse=True)
945 if expr:
955 if expr:
946 matcher = revset.match(None, expr)
956 matcher = revset.match(None, expr)
947 revs = matcher(repo, revs)
957 revs = matcher(repo, revs)
948 if wopts.limit is not None:
958 if wopts.limit is not None:
949 revs = revs.slice(0, wopts.limit)
959 revs = revs.slice(0, wopts.limit)
950
960
951 differ = changesetdiffer()
961 differ = changesetdiffer()
952 differ._makefilematcher = filematcher
962 differ._makefilematcher = filematcher
953 return revs, differ
963 return revs, differ
954
964
955
965
956 def _parselinerangeopt(repo, opts):
966 def _parselinerangeopt(repo, opts):
957 """Parse --line-range log option and return a list of tuples (filename,
967 """Parse --line-range log option and return a list of tuples (filename,
958 (fromline, toline)).
968 (fromline, toline)).
959 """
969 """
960 linerangebyfname = []
970 linerangebyfname = []
961 for pat in opts.get(b'line_range', []):
971 for pat in opts.get(b'line_range', []):
962 try:
972 try:
963 pat, linerange = pat.rsplit(b',', 1)
973 pat, linerange = pat.rsplit(b',', 1)
964 except ValueError:
974 except ValueError:
965 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
975 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
966 try:
976 try:
967 fromline, toline = map(int, linerange.split(b':'))
977 fromline, toline = map(int, linerange.split(b':'))
968 except ValueError:
978 except ValueError:
969 raise error.Abort(_(b"invalid line range for %s") % pat)
979 raise error.Abort(_(b"invalid line range for %s") % pat)
970 msg = _(b"line range pattern '%s' must match exactly one file") % pat
980 msg = _(b"line range pattern '%s' must match exactly one file") % pat
971 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
981 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
972 linerangebyfname.append(
982 linerangebyfname.append(
973 (fname, util.processlinerange(fromline, toline))
983 (fname, util.processlinerange(fromline, toline))
974 )
984 )
975 return linerangebyfname
985 return linerangebyfname
976
986
977
987
978 def getlinerangerevs(repo, userrevs, opts):
988 def getlinerangerevs(repo, userrevs, opts):
979 """Return (revs, differ).
989 """Return (revs, differ).
980
990
981 "revs" are revisions obtained by processing "line-range" log options and
991 "revs" are revisions obtained by processing "line-range" log options and
982 walking block ancestors of each specified file/line-range.
992 walking block ancestors of each specified file/line-range.
983
993
984 "differ" is a changesetdiffer with pre-configured file matcher and hunks
994 "differ" is a changesetdiffer with pre-configured file matcher and hunks
985 filter.
995 filter.
986 """
996 """
987 wctx = repo[None]
997 wctx = repo[None]
988
998
989 # Two-levels map of "rev -> file ctx -> [line range]".
999 # Two-levels map of "rev -> file ctx -> [line range]".
990 linerangesbyrev = {}
1000 linerangesbyrev = {}
991 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
1001 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
992 if fname not in wctx:
1002 if fname not in wctx:
993 raise error.Abort(
1003 raise error.Abort(
994 _(b'cannot follow file not in parent revision: "%s"') % fname
1004 _(b'cannot follow file not in parent revision: "%s"') % fname
995 )
1005 )
996 fctx = wctx.filectx(fname)
1006 fctx = wctx.filectx(fname)
997 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
1007 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
998 rev = fctx.introrev()
1008 rev = fctx.introrev()
999 if rev is None:
1009 if rev is None:
1000 rev = wdirrev
1010 rev = wdirrev
1001 if rev not in userrevs:
1011 if rev not in userrevs:
1002 continue
1012 continue
1003 linerangesbyrev.setdefault(rev, {}).setdefault(
1013 linerangesbyrev.setdefault(rev, {}).setdefault(
1004 fctx.path(), []
1014 fctx.path(), []
1005 ).append(linerange)
1015 ).append(linerange)
1006
1016
1007 def nofilterhunksfn(fctx, hunks):
1017 def nofilterhunksfn(fctx, hunks):
1008 return hunks
1018 return hunks
1009
1019
1010 def hunksfilter(ctx):
1020 def hunksfilter(ctx):
1011 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
1021 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
1012 if fctxlineranges is None:
1022 if fctxlineranges is None:
1013 return nofilterhunksfn
1023 return nofilterhunksfn
1014
1024
1015 def filterfn(fctx, hunks):
1025 def filterfn(fctx, hunks):
1016 lineranges = fctxlineranges.get(fctx.path())
1026 lineranges = fctxlineranges.get(fctx.path())
1017 if lineranges is not None:
1027 if lineranges is not None:
1018 for hr, lines in hunks:
1028 for hr, lines in hunks:
1019 if hr is None: # binary
1029 if hr is None: # binary
1020 yield hr, lines
1030 yield hr, lines
1021 continue
1031 continue
1022 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
1032 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
1023 yield hr, lines
1033 yield hr, lines
1024 else:
1034 else:
1025 for hunk in hunks:
1035 for hunk in hunks:
1026 yield hunk
1036 yield hunk
1027
1037
1028 return filterfn
1038 return filterfn
1029
1039
1030 def filematcher(ctx):
1040 def filematcher(ctx):
1031 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
1041 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
1032 return scmutil.matchfiles(repo, files)
1042 return scmutil.matchfiles(repo, files)
1033
1043
1034 revs = sorted(linerangesbyrev, reverse=True)
1044 revs = sorted(linerangesbyrev, reverse=True)
1035
1045
1036 differ = changesetdiffer()
1046 differ = changesetdiffer()
1037 differ._makefilematcher = filematcher
1047 differ._makefilematcher = filematcher
1038 differ._makehunksfilter = hunksfilter
1048 differ._makehunksfilter = hunksfilter
1039 return smartset.baseset(revs), differ
1049 return smartset.baseset(revs), differ
1040
1050
1041
1051
1042 def _graphnodeformatter(ui, displayer):
1052 def _graphnodeformatter(ui, displayer):
1043 spec = ui.config(b'ui', b'graphnodetemplate')
1053 spec = ui.config(b'ui', b'graphnodetemplate')
1044 if not spec:
1054 if not spec:
1045 return templatekw.getgraphnode # fast path for "{graphnode}"
1055 return templatekw.getgraphnode # fast path for "{graphnode}"
1046
1056
1047 spec = templater.unquotestring(spec)
1057 spec = templater.unquotestring(spec)
1048 if isinstance(displayer, changesettemplater):
1058 if isinstance(displayer, changesettemplater):
1049 # reuse cache of slow templates
1059 # reuse cache of slow templates
1050 tres = displayer._tresources
1060 tres = displayer._tresources
1051 else:
1061 else:
1052 tres = formatter.templateresources(ui)
1062 tres = formatter.templateresources(ui)
1053 templ = formatter.maketemplater(
1063 templ = formatter.maketemplater(
1054 ui, spec, defaults=templatekw.keywords, resources=tres
1064 ui, spec, defaults=templatekw.keywords, resources=tres
1055 )
1065 )
1056
1066
1057 def formatnode(repo, ctx, cache):
1067 def formatnode(repo, ctx, cache):
1058 props = {b'ctx': ctx, b'repo': repo}
1068 props = {b'ctx': ctx, b'repo': repo}
1059 return templ.renderdefault(props)
1069 return templ.renderdefault(props)
1060
1070
1061 return formatnode
1071 return formatnode
1062
1072
1063
1073
1064 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1074 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1065 props = props or {}
1075 props = props or {}
1066 formatnode = _graphnodeformatter(ui, displayer)
1076 formatnode = _graphnodeformatter(ui, displayer)
1067 state = graphmod.asciistate()
1077 state = graphmod.asciistate()
1068 styles = state.styles
1078 styles = state.styles
1069
1079
1070 # only set graph styling if HGPLAIN is not set.
1080 # only set graph styling if HGPLAIN is not set.
1071 if ui.plain(b'graph'):
1081 if ui.plain(b'graph'):
1072 # set all edge styles to |, the default pre-3.8 behaviour
1082 # set all edge styles to |, the default pre-3.8 behaviour
1073 styles.update(dict.fromkeys(styles, b'|'))
1083 styles.update(dict.fromkeys(styles, b'|'))
1074 else:
1084 else:
1075 edgetypes = {
1085 edgetypes = {
1076 b'parent': graphmod.PARENT,
1086 b'parent': graphmod.PARENT,
1077 b'grandparent': graphmod.GRANDPARENT,
1087 b'grandparent': graphmod.GRANDPARENT,
1078 b'missing': graphmod.MISSINGPARENT,
1088 b'missing': graphmod.MISSINGPARENT,
1079 }
1089 }
1080 for name, key in edgetypes.items():
1090 for name, key in edgetypes.items():
1081 # experimental config: experimental.graphstyle.*
1091 # experimental config: experimental.graphstyle.*
1082 styles[key] = ui.config(
1092 styles[key] = ui.config(
1083 b'experimental', b'graphstyle.%s' % name, styles[key]
1093 b'experimental', b'graphstyle.%s' % name, styles[key]
1084 )
1094 )
1085 if not styles[key]:
1095 if not styles[key]:
1086 styles[key] = None
1096 styles[key] = None
1087
1097
1088 # experimental config: experimental.graphshorten
1098 # experimental config: experimental.graphshorten
1089 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1099 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1090
1100
1091 formatnode_cache = {}
1101 formatnode_cache = {}
1092 for rev, type, ctx, parents in dag:
1102 for rev, type, ctx, parents in dag:
1093 char = formatnode(repo, ctx, formatnode_cache)
1103 char = formatnode(repo, ctx, formatnode_cache)
1094 copies = getcopies(ctx) if getcopies else None
1104 copies = getcopies(ctx) if getcopies else None
1095 edges = edgefn(type, char, state, rev, parents)
1105 edges = edgefn(type, char, state, rev, parents)
1096 firstedge = next(edges)
1106 firstedge = next(edges)
1097 width = firstedge[2]
1107 width = firstedge[2]
1098 displayer.show(
1108 displayer.show(
1099 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1109 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1100 )
1110 )
1101 lines = displayer.hunk.pop(rev).split(b'\n')
1111 lines = displayer.hunk.pop(rev).split(b'\n')
1102 if not lines[-1]:
1112 if not lines[-1]:
1103 del lines[-1]
1113 del lines[-1]
1104 displayer.flush(ctx)
1114 displayer.flush(ctx)
1105 for type, char, width, coldata in itertools.chain([firstedge], edges):
1115 for type, char, width, coldata in itertools.chain([firstedge], edges):
1106 graphmod.ascii(ui, state, type, char, lines, coldata)
1116 graphmod.ascii(ui, state, type, char, lines, coldata)
1107 lines = []
1117 lines = []
1108 displayer.close()
1118 displayer.close()
1109
1119
1110
1120
1111 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1121 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1112 revdag = graphmod.dagwalker(repo, revs)
1122 revdag = graphmod.dagwalker(repo, revs)
1113 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1123 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1114
1124
1115
1125
1116 def displayrevs(ui, repo, revs, displayer, getcopies):
1126 def displayrevs(ui, repo, revs, displayer, getcopies):
1117 for rev in revs:
1127 for rev in revs:
1118 ctx = repo[rev]
1128 ctx = repo[rev]
1119 copies = getcopies(ctx) if getcopies else None
1129 copies = getcopies(ctx) if getcopies else None
1120 displayer.show(ctx, copies=copies)
1130 displayer.show(ctx, copies=copies)
1121 displayer.flush(ctx)
1131 displayer.flush(ctx)
1122 displayer.close()
1132 displayer.close()
1123
1133
1124
1134
1125 def checkunsupportedgraphflags(pats, opts):
1135 def checkunsupportedgraphflags(pats, opts):
1126 for op in [b"newest_first"]:
1136 for op in [b"newest_first"]:
1127 if op in opts and opts[op]:
1137 if op in opts and opts[op]:
1128 raise error.Abort(
1138 raise error.Abort(
1129 _(b"-G/--graph option is incompatible with --%s")
1139 _(b"-G/--graph option is incompatible with --%s")
1130 % op.replace(b"_", b"-")
1140 % op.replace(b"_", b"-")
1131 )
1141 )
1132
1142
1133
1143
1134 def graphrevs(repo, nodes, opts):
1144 def graphrevs(repo, nodes, opts):
1135 limit = getlimit(opts)
1145 limit = getlimit(opts)
1136 nodes.reverse()
1146 nodes.reverse()
1137 if limit is not None:
1147 if limit is not None:
1138 nodes = nodes[:limit]
1148 nodes = nodes[:limit]
1139 return graphmod.nodes(repo, nodes)
1149 return graphmod.nodes(repo, nodes)
General Comments 0
You need to be logged in to leave comments. Login now