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