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