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