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