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