##// END OF EJS Templates
logcmdutil: mark changesetprinter.showpatch() as private
Yuya Nishihara -
r35973:218b77c4 default
parent child Browse files
Show More
@@ -1,931 +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=None, diffopts=None, buffered=False):
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 or {}
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=None, diffopts=None, buffered=False):
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 changesetprinter.__init__(self, ui, repo, matchfn, diffopts, buffered)
402 changesetprinter.__init__(self, ui, repo, matchfn, diffopts, buffered)
403 tres = formatter.templateresources(ui, repo)
403 tres = formatter.templateresources(ui, repo)
404 self.t = formatter.loadtemplater(ui, tmplspec,
404 self.t = formatter.loadtemplater(ui, tmplspec,
405 defaults=templatekw.keywords,
405 defaults=templatekw.keywords,
406 resources=tres,
406 resources=tres,
407 cache=templatekw.defaulttempl)
407 cache=templatekw.defaulttempl)
408 self._counter = itertools.count()
408 self._counter = itertools.count()
409 self.cache = tres['cache'] # shared with _graphnodeformatter()
409 self.cache = tres['cache'] # shared with _graphnodeformatter()
410
410
411 self._tref = tmplspec.ref
411 self._tref = tmplspec.ref
412 self._parts = {'header': '', 'footer': '',
412 self._parts = {'header': '', 'footer': '',
413 tmplspec.ref: tmplspec.ref,
413 tmplspec.ref: tmplspec.ref,
414 'docheader': '', 'docfooter': '',
414 'docheader': '', 'docfooter': '',
415 'separator': ''}
415 'separator': ''}
416 if tmplspec.mapfile:
416 if tmplspec.mapfile:
417 # find correct templates for current mode, for backward
417 # find correct templates for current mode, for backward
418 # compatibility with 'log -v/-q/--debug' using a mapfile
418 # compatibility with 'log -v/-q/--debug' using a mapfile
419 tmplmodes = [
419 tmplmodes = [
420 (True, ''),
420 (True, ''),
421 (self.ui.verbose, '_verbose'),
421 (self.ui.verbose, '_verbose'),
422 (self.ui.quiet, '_quiet'),
422 (self.ui.quiet, '_quiet'),
423 (self.ui.debugflag, '_debug'),
423 (self.ui.debugflag, '_debug'),
424 ]
424 ]
425 for mode, postfix in tmplmodes:
425 for mode, postfix in tmplmodes:
426 for t in self._parts:
426 for t in self._parts:
427 cur = t + postfix
427 cur = t + postfix
428 if mode and cur in self.t:
428 if mode and cur in self.t:
429 self._parts[t] = cur
429 self._parts[t] = cur
430 else:
430 else:
431 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]
432 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
432 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
433 self._parts.update(m)
433 self._parts.update(m)
434
434
435 if self._parts['docheader']:
435 if self._parts['docheader']:
436 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
436 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
437
437
438 def close(self):
438 def close(self):
439 if self._parts['docfooter']:
439 if self._parts['docfooter']:
440 if not self.footer:
440 if not self.footer:
441 self.footer = ""
441 self.footer = ""
442 self.footer += templater.stringify(self.t(self._parts['docfooter']))
442 self.footer += templater.stringify(self.t(self._parts['docfooter']))
443 return super(changesettemplater, self).close()
443 return super(changesettemplater, self).close()
444
444
445 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
445 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
446 '''show a single changeset or file revision'''
446 '''show a single changeset or file revision'''
447 props = props.copy()
447 props = props.copy()
448 props['ctx'] = ctx
448 props['ctx'] = ctx
449 props['index'] = index = next(self._counter)
449 props['index'] = index = next(self._counter)
450 props['revcache'] = {'copies': copies}
450 props['revcache'] = {'copies': copies}
451 props = pycompat.strkwargs(props)
451 props = pycompat.strkwargs(props)
452
452
453 # 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
454 # since there's inherently a conflict between header (across items) and
454 # since there's inherently a conflict between header (across items) and
455 # separator (per item)
455 # separator (per item)
456 if self._parts['separator'] and index > 0:
456 if self._parts['separator'] and index > 0:
457 self.ui.write(templater.stringify(self.t(self._parts['separator'])))
457 self.ui.write(templater.stringify(self.t(self._parts['separator'])))
458
458
459 # write header
459 # write header
460 if self._parts['header']:
460 if self._parts['header']:
461 h = templater.stringify(self.t(self._parts['header'], **props))
461 h = templater.stringify(self.t(self._parts['header'], **props))
462 if self.buffered:
462 if self.buffered:
463 self.header[ctx.rev()] = h
463 self.header[ctx.rev()] = h
464 else:
464 else:
465 if self.lastheader != h:
465 if self.lastheader != h:
466 self.lastheader = h
466 self.lastheader = h
467 self.ui.write(h)
467 self.ui.write(h)
468
468
469 # write changeset metadata, then patch if requested
469 # write changeset metadata, then patch if requested
470 key = self._parts[self._tref]
470 key = self._parts[self._tref]
471 self.ui.write(templater.stringify(self.t(key, **props)))
471 self.ui.write(templater.stringify(self.t(key, **props)))
472 self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
472 self._showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
473
473
474 if self._parts['footer']:
474 if self._parts['footer']:
475 if not self.footer:
475 if not self.footer:
476 self.footer = templater.stringify(
476 self.footer = templater.stringify(
477 self.t(self._parts['footer'], **props))
477 self.t(self._parts['footer'], **props))
478
478
479 def templatespec(tmpl, mapfile):
479 def templatespec(tmpl, mapfile):
480 if mapfile:
480 if mapfile:
481 return formatter.templatespec('changeset', tmpl, mapfile)
481 return formatter.templatespec('changeset', tmpl, mapfile)
482 else:
482 else:
483 return formatter.templatespec('', tmpl, None)
483 return formatter.templatespec('', tmpl, None)
484
484
485 def _lookuptemplate(ui, tmpl, style):
485 def _lookuptemplate(ui, tmpl, style):
486 """Find the template matching the given template spec or style
486 """Find the template matching the given template spec or style
487
487
488 See formatter.lookuptemplate() for details.
488 See formatter.lookuptemplate() for details.
489 """
489 """
490
490
491 # ui settings
491 # ui settings
492 if not tmpl and not style: # template are stronger than style
492 if not tmpl and not style: # template are stronger than style
493 tmpl = ui.config('ui', 'logtemplate')
493 tmpl = ui.config('ui', 'logtemplate')
494 if tmpl:
494 if tmpl:
495 return templatespec(templater.unquotestring(tmpl), None)
495 return templatespec(templater.unquotestring(tmpl), None)
496 else:
496 else:
497 style = util.expandpath(ui.config('ui', 'style'))
497 style = util.expandpath(ui.config('ui', 'style'))
498
498
499 if not tmpl and style:
499 if not tmpl and style:
500 mapfile = style
500 mapfile = style
501 if not os.path.split(mapfile)[0]:
501 if not os.path.split(mapfile)[0]:
502 mapname = (templater.templatepath('map-cmdline.' + mapfile)
502 mapname = (templater.templatepath('map-cmdline.' + mapfile)
503 or templater.templatepath(mapfile))
503 or templater.templatepath(mapfile))
504 if mapname:
504 if mapname:
505 mapfile = mapname
505 mapfile = mapname
506 return templatespec(None, mapfile)
506 return templatespec(None, mapfile)
507
507
508 if not tmpl:
508 if not tmpl:
509 return templatespec(None, None)
509 return templatespec(None, None)
510
510
511 return formatter.lookuptemplate(ui, 'changeset', tmpl)
511 return formatter.lookuptemplate(ui, 'changeset', tmpl)
512
512
513 def maketemplater(ui, repo, tmpl, buffered=False):
513 def maketemplater(ui, repo, tmpl, buffered=False):
514 """Create a changesettemplater from a literal template 'tmpl'
514 """Create a changesettemplater from a literal template 'tmpl'
515 byte-string."""
515 byte-string."""
516 spec = templatespec(tmpl, None)
516 spec = templatespec(tmpl, None)
517 return changesettemplater(ui, repo, spec, buffered=buffered)
517 return changesettemplater(ui, repo, spec, buffered=buffered)
518
518
519 def changesetdisplayer(ui, repo, opts, buffered=False):
519 def changesetdisplayer(ui, repo, opts, buffered=False):
520 """show one changeset using template or regular display.
520 """show one changeset using template or regular display.
521
521
522 Display format will be the first non-empty hit of:
522 Display format will be the first non-empty hit of:
523 1. option 'template'
523 1. option 'template'
524 2. option 'style'
524 2. option 'style'
525 3. [ui] setting 'logtemplate'
525 3. [ui] setting 'logtemplate'
526 4. [ui] setting 'style'
526 4. [ui] setting 'style'
527 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,
528 regular display via changesetprinter() is done.
528 regular display via changesetprinter() is done.
529 """
529 """
530 # options
530 # options
531 match = None
531 match = None
532 if opts.get('patch') or opts.get('stat'):
532 if opts.get('patch') or opts.get('stat'):
533 match = scmutil.matchall(repo)
533 match = scmutil.matchall(repo)
534
534
535 if opts.get('template') == 'json':
535 if opts.get('template') == 'json':
536 return jsonchangeset(ui, repo, match, opts, buffered)
536 return jsonchangeset(ui, repo, match, opts, buffered)
537
537
538 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
538 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
539
539
540 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:
541 return changesetprinter(ui, repo, match, opts, buffered)
541 return changesetprinter(ui, repo, match, opts, buffered)
542
542
543 return changesettemplater(ui, repo, spec, match, opts, buffered)
543 return changesettemplater(ui, repo, spec, match, opts, buffered)
544
544
545 def _makematcher(repo, revs, pats, opts):
545 def _makematcher(repo, revs, pats, opts):
546 """Build matcher and expanded patterns from log options
546 """Build matcher and expanded patterns from log options
547
547
548 If --follow, revs are the revisions to follow from.
548 If --follow, revs are the revisions to follow from.
549
549
550 Returns (match, pats, slowpath) where
550 Returns (match, pats, slowpath) where
551 - 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
552 - pats: patterns used (globs are expanded on Windows)
552 - pats: patterns used (globs are expanded on Windows)
553 - slowpath: True if patterns aren't as simple as scanning filelogs
553 - slowpath: True if patterns aren't as simple as scanning filelogs
554 """
554 """
555 # pats/include/exclude are passed to match.match() directly in
555 # pats/include/exclude are passed to match.match() directly in
556 # _matchfiles() revset but walkchangerevs() builds its matcher with
556 # _matchfiles() revset but walkchangerevs() builds its matcher with
557 # scmutil.match(). The difference is input pats are globbed on
557 # scmutil.match(). The difference is input pats are globbed on
558 # platforms without shell expansion (windows).
558 # platforms without shell expansion (windows).
559 wctx = repo[None]
559 wctx = repo[None]
560 match, pats = scmutil.matchandpats(wctx, pats, opts)
560 match, pats = scmutil.matchandpats(wctx, pats, opts)
561 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
561 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
562 if not slowpath:
562 if not slowpath:
563 follow = opts.get('follow') or opts.get('follow_first')
563 follow = opts.get('follow') or opts.get('follow_first')
564 startctxs = []
564 startctxs = []
565 if follow and opts.get('rev'):
565 if follow and opts.get('rev'):
566 startctxs = [repo[r] for r in revs]
566 startctxs = [repo[r] for r in revs]
567 for f in match.files():
567 for f in match.files():
568 if follow and startctxs:
568 if follow and startctxs:
569 # 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
570 # take the slow path.
570 # take the slow path.
571 if any(f not in c for c in startctxs):
571 if any(f not in c for c in startctxs):
572 slowpath = True
572 slowpath = True
573 continue
573 continue
574 elif follow and f not in wctx:
574 elif follow and f not in wctx:
575 # 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
576 # take the slow path.
576 # take the slow path.
577 if os.path.exists(repo.wjoin(f)):
577 if os.path.exists(repo.wjoin(f)):
578 slowpath = True
578 slowpath = True
579 continue
579 continue
580 else:
580 else:
581 raise error.Abort(_('cannot follow file not in parent '
581 raise error.Abort(_('cannot follow file not in parent '
582 'revision: "%s"') % f)
582 'revision: "%s"') % f)
583 filelog = repo.file(f)
583 filelog = repo.file(f)
584 if not filelog:
584 if not filelog:
585 # A zero count may be a directory or deleted file, so
585 # A zero count may be a directory or deleted file, so
586 # try to find matching entries on the slow path.
586 # try to find matching entries on the slow path.
587 if follow:
587 if follow:
588 raise error.Abort(
588 raise error.Abort(
589 _('cannot follow nonexistent file: "%s"') % f)
589 _('cannot follow nonexistent file: "%s"') % f)
590 slowpath = True
590 slowpath = True
591
591
592 # 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
593 # 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
594 # existed in history - in that case, we'll continue down the
594 # existed in history - in that case, we'll continue down the
595 # slowpath; otherwise, we can turn off the slowpath
595 # slowpath; otherwise, we can turn off the slowpath
596 if slowpath:
596 if slowpath:
597 for path in match.files():
597 for path in match.files():
598 if path == '.' or path in repo.store:
598 if path == '.' or path in repo.store:
599 break
599 break
600 else:
600 else:
601 slowpath = False
601 slowpath = False
602
602
603 return match, pats, slowpath
603 return match, pats, slowpath
604
604
605 def _fileancestors(repo, revs, match, followfirst):
605 def _fileancestors(repo, revs, match, followfirst):
606 fctxs = []
606 fctxs = []
607 for r in revs:
607 for r in revs:
608 ctx = repo[r]
608 ctx = repo[r]
609 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
609 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
610
610
611 # When displaying a revision with --patch --follow FILE, we have
611 # When displaying a revision with --patch --follow FILE, we have
612 # to know which file of the revision must be diffed. With
612 # to know which file of the revision must be diffed. With
613 # --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
614 # revision, stored in "fcache". "fcache" is populated as a side effect
614 # revision, stored in "fcache". "fcache" is populated as a side effect
615 # of the graph traversal.
615 # of the graph traversal.
616 fcache = {}
616 fcache = {}
617 def filematcher(rev):
617 def filematcher(rev):
618 return scmutil.matchfiles(repo, fcache.get(rev, []))
618 return scmutil.matchfiles(repo, fcache.get(rev, []))
619
619
620 def revgen():
620 def revgen():
621 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
621 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
622 fcache[rev] = [c.path() for c in cs]
622 fcache[rev] = [c.path() for c in cs]
623 yield rev
623 yield rev
624 return smartset.generatorset(revgen(), iterasc=False), filematcher
624 return smartset.generatorset(revgen(), iterasc=False), filematcher
625
625
626 def _makenofollowfilematcher(repo, pats, opts):
626 def _makenofollowfilematcher(repo, pats, opts):
627 '''hook for extensions to override the filematcher for non-follow cases'''
627 '''hook for extensions to override the filematcher for non-follow cases'''
628 return None
628 return None
629
629
630 _opt2logrevset = {
630 _opt2logrevset = {
631 'no_merges': ('not merge()', None),
631 'no_merges': ('not merge()', None),
632 'only_merges': ('merge()', None),
632 'only_merges': ('merge()', None),
633 '_matchfiles': (None, '_matchfiles(%ps)'),
633 '_matchfiles': (None, '_matchfiles(%ps)'),
634 'date': ('date(%s)', None),
634 'date': ('date(%s)', None),
635 'branch': ('branch(%s)', '%lr'),
635 'branch': ('branch(%s)', '%lr'),
636 '_patslog': ('filelog(%s)', '%lr'),
636 '_patslog': ('filelog(%s)', '%lr'),
637 'keyword': ('keyword(%s)', '%lr'),
637 'keyword': ('keyword(%s)', '%lr'),
638 'prune': ('ancestors(%s)', 'not %lr'),
638 'prune': ('ancestors(%s)', 'not %lr'),
639 'user': ('user(%s)', '%lr'),
639 'user': ('user(%s)', '%lr'),
640 }
640 }
641
641
642 def _makerevset(repo, match, pats, slowpath, opts):
642 def _makerevset(repo, match, pats, slowpath, opts):
643 """Return a revset string built from log options and file patterns"""
643 """Return a revset string built from log options and file patterns"""
644 opts = dict(opts)
644 opts = dict(opts)
645 # follow or not follow?
645 # follow or not follow?
646 follow = opts.get('follow') or opts.get('follow_first')
646 follow = opts.get('follow') or opts.get('follow_first')
647
647
648 # 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
649 # the same time
649 # the same time
650 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
650 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
651 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
651 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
652
652
653 if slowpath:
653 if slowpath:
654 # See walkchangerevs() slow path.
654 # See walkchangerevs() slow path.
655 #
655 #
656 # pats/include/exclude cannot be represented as separate
656 # pats/include/exclude cannot be represented as separate
657 # revset expressions as their filtering logic applies at file
657 # revset expressions as their filtering logic applies at file
658 # level. For instance "-I a -X b" matches a revision touching
658 # level. For instance "-I a -X b" matches a revision touching
659 # "a" and "b" while "file(a) and not file(b)" does
659 # "a" and "b" while "file(a) and not file(b)" does
660 # not. Besides, filesets are evaluated against the working
660 # not. Besides, filesets are evaluated against the working
661 # directory.
661 # directory.
662 matchargs = ['r:', 'd:relpath']
662 matchargs = ['r:', 'd:relpath']
663 for p in pats:
663 for p in pats:
664 matchargs.append('p:' + p)
664 matchargs.append('p:' + p)
665 for p in opts.get('include', []):
665 for p in opts.get('include', []):
666 matchargs.append('i:' + p)
666 matchargs.append('i:' + p)
667 for p in opts.get('exclude', []):
667 for p in opts.get('exclude', []):
668 matchargs.append('x:' + p)
668 matchargs.append('x:' + p)
669 opts['_matchfiles'] = matchargs
669 opts['_matchfiles'] = matchargs
670 elif not follow:
670 elif not follow:
671 opts['_patslog'] = list(pats)
671 opts['_patslog'] = list(pats)
672
672
673 expr = []
673 expr = []
674 for op, val in sorted(opts.iteritems()):
674 for op, val in sorted(opts.iteritems()):
675 if not val:
675 if not val:
676 continue
676 continue
677 if op not in _opt2logrevset:
677 if op not in _opt2logrevset:
678 continue
678 continue
679 revop, listop = _opt2logrevset[op]
679 revop, listop = _opt2logrevset[op]
680 if revop and '%' not in revop:
680 if revop and '%' not in revop:
681 expr.append(revop)
681 expr.append(revop)
682 elif not listop:
682 elif not listop:
683 expr.append(revsetlang.formatspec(revop, val))
683 expr.append(revsetlang.formatspec(revop, val))
684 else:
684 else:
685 if revop:
685 if revop:
686 val = [revsetlang.formatspec(revop, v) for v in val]
686 val = [revsetlang.formatspec(revop, v) for v in val]
687 expr.append(revsetlang.formatspec(listop, val))
687 expr.append(revsetlang.formatspec(listop, val))
688
688
689 if expr:
689 if expr:
690 expr = '(' + ' and '.join(expr) + ')'
690 expr = '(' + ' and '.join(expr) + ')'
691 else:
691 else:
692 expr = None
692 expr = None
693 return expr
693 return expr
694
694
695 def _initialrevs(repo, opts):
695 def _initialrevs(repo, opts):
696 """Return the initial set of revisions to be filtered or followed"""
696 """Return the initial set of revisions to be filtered or followed"""
697 follow = opts.get('follow') or opts.get('follow_first')
697 follow = opts.get('follow') or opts.get('follow_first')
698 if opts.get('rev'):
698 if opts.get('rev'):
699 revs = scmutil.revrange(repo, opts['rev'])
699 revs = scmutil.revrange(repo, opts['rev'])
700 elif follow and repo.dirstate.p1() == nullid:
700 elif follow and repo.dirstate.p1() == nullid:
701 revs = smartset.baseset()
701 revs = smartset.baseset()
702 elif follow:
702 elif follow:
703 revs = repo.revs('.')
703 revs = repo.revs('.')
704 else:
704 else:
705 revs = smartset.spanset(repo)
705 revs = smartset.spanset(repo)
706 revs.reverse()
706 revs.reverse()
707 return revs
707 return revs
708
708
709 def getrevs(repo, pats, opts):
709 def getrevs(repo, pats, opts):
710 """Return (revs, filematcher) where revs is a smartset
710 """Return (revs, filematcher) where revs is a smartset
711
711
712 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
713 objects filtering the files to be detailed when displaying the revision.
713 objects filtering the files to be detailed when displaying the revision.
714 """
714 """
715 follow = opts.get('follow') or opts.get('follow_first')
715 follow = opts.get('follow') or opts.get('follow_first')
716 followfirst = opts.get('follow_first')
716 followfirst = opts.get('follow_first')
717 limit = getlimit(opts)
717 limit = getlimit(opts)
718 revs = _initialrevs(repo, opts)
718 revs = _initialrevs(repo, opts)
719 if not revs:
719 if not revs:
720 return smartset.baseset(), None
720 return smartset.baseset(), None
721 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
721 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
722 filematcher = None
722 filematcher = None
723 if follow:
723 if follow:
724 if slowpath or match.always():
724 if slowpath or match.always():
725 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
725 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
726 else:
726 else:
727 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
727 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
728 revs.reverse()
728 revs.reverse()
729 if filematcher is None:
729 if filematcher is None:
730 filematcher = _makenofollowfilematcher(repo, pats, opts)
730 filematcher = _makenofollowfilematcher(repo, pats, opts)
731 if filematcher is None:
731 if filematcher is None:
732 def filematcher(rev):
732 def filematcher(rev):
733 return match
733 return match
734
734
735 expr = _makerevset(repo, match, pats, slowpath, opts)
735 expr = _makerevset(repo, match, pats, slowpath, opts)
736 if opts.get('graph') and opts.get('rev'):
736 if opts.get('graph') and opts.get('rev'):
737 # User-specified revs might be unsorted, but don't sort before
737 # User-specified revs might be unsorted, but don't sort before
738 # _makerevset because it might depend on the order of revs
738 # _makerevset because it might depend on the order of revs
739 if not (revs.isdescending() or revs.istopo()):
739 if not (revs.isdescending() or revs.istopo()):
740 revs.sort(reverse=True)
740 revs.sort(reverse=True)
741 if expr:
741 if expr:
742 matcher = revset.match(None, expr)
742 matcher = revset.match(None, expr)
743 revs = matcher(repo, revs)
743 revs = matcher(repo, revs)
744 if limit is not None:
744 if limit is not None:
745 revs = revs.slice(0, limit)
745 revs = revs.slice(0, limit)
746 return revs, filematcher
746 return revs, filematcher
747
747
748 def _parselinerangeopt(repo, opts):
748 def _parselinerangeopt(repo, opts):
749 """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,
750 (fromline, toline)).
750 (fromline, toline)).
751 """
751 """
752 linerangebyfname = []
752 linerangebyfname = []
753 for pat in opts.get('line_range', []):
753 for pat in opts.get('line_range', []):
754 try:
754 try:
755 pat, linerange = pat.rsplit(',', 1)
755 pat, linerange = pat.rsplit(',', 1)
756 except ValueError:
756 except ValueError:
757 raise error.Abort(_('malformatted line-range pattern %s') % pat)
757 raise error.Abort(_('malformatted line-range pattern %s') % pat)
758 try:
758 try:
759 fromline, toline = map(int, linerange.split(':'))
759 fromline, toline = map(int, linerange.split(':'))
760 except ValueError:
760 except ValueError:
761 raise error.Abort(_("invalid line range for %s") % pat)
761 raise error.Abort(_("invalid line range for %s") % pat)
762 msg = _("line range pattern '%s' must match exactly one file") % pat
762 msg = _("line range pattern '%s' must match exactly one file") % pat
763 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
763 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
764 linerangebyfname.append(
764 linerangebyfname.append(
765 (fname, util.processlinerange(fromline, toline)))
765 (fname, util.processlinerange(fromline, toline)))
766 return linerangebyfname
766 return linerangebyfname
767
767
768 def getlinerangerevs(repo, userrevs, opts):
768 def getlinerangerevs(repo, userrevs, opts):
769 """Return (revs, filematcher, hunksfilter).
769 """Return (revs, filematcher, hunksfilter).
770
770
771 "revs" are revisions obtained by processing "line-range" log options and
771 "revs" are revisions obtained by processing "line-range" log options and
772 walking block ancestors of each specified file/line-range.
772 walking block ancestors of each specified file/line-range.
773
773
774 "filematcher(rev) -> match" is a factory function returning a match object
774 "filematcher(rev) -> match" is a factory function returning a match object
775 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.
776 If neither --stat nor --patch options are passed, "filematcher" is None.
776 If neither --stat nor --patch options are passed, "filematcher" is None.
777
777
778 "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
778 "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
779 returning a hunks filtering function.
779 returning a hunks filtering function.
780 If neither --stat nor --patch options are passed, "filterhunks" is None.
780 If neither --stat nor --patch options are passed, "filterhunks" is None.
781 """
781 """
782 wctx = repo[None]
782 wctx = repo[None]
783
783
784 # Two-levels map of "rev -> file ctx -> [line range]".
784 # Two-levels map of "rev -> file ctx -> [line range]".
785 linerangesbyrev = {}
785 linerangesbyrev = {}
786 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
786 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
787 if fname not in wctx:
787 if fname not in wctx:
788 raise error.Abort(_('cannot follow file not in parent '
788 raise error.Abort(_('cannot follow file not in parent '
789 'revision: "%s"') % fname)
789 'revision: "%s"') % fname)
790 fctx = wctx.filectx(fname)
790 fctx = wctx.filectx(fname)
791 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
791 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
792 rev = fctx.introrev()
792 rev = fctx.introrev()
793 if rev not in userrevs:
793 if rev not in userrevs:
794 continue
794 continue
795 linerangesbyrev.setdefault(
795 linerangesbyrev.setdefault(
796 rev, {}).setdefault(
796 rev, {}).setdefault(
797 fctx.path(), []).append(linerange)
797 fctx.path(), []).append(linerange)
798
798
799 filematcher = None
799 filematcher = None
800 hunksfilter = None
800 hunksfilter = None
801 if opts.get('patch') or opts.get('stat'):
801 if opts.get('patch') or opts.get('stat'):
802
802
803 def nofilterhunksfn(fctx, hunks):
803 def nofilterhunksfn(fctx, hunks):
804 return hunks
804 return hunks
805
805
806 def hunksfilter(rev):
806 def hunksfilter(rev):
807 fctxlineranges = linerangesbyrev.get(rev)
807 fctxlineranges = linerangesbyrev.get(rev)
808 if fctxlineranges is None:
808 if fctxlineranges is None:
809 return nofilterhunksfn
809 return nofilterhunksfn
810
810
811 def filterfn(fctx, hunks):
811 def filterfn(fctx, hunks):
812 lineranges = fctxlineranges.get(fctx.path())
812 lineranges = fctxlineranges.get(fctx.path())
813 if lineranges is not None:
813 if lineranges is not None:
814 for hr, lines in hunks:
814 for hr, lines in hunks:
815 if hr is None: # binary
815 if hr is None: # binary
816 yield hr, lines
816 yield hr, lines
817 continue
817 continue
818 if any(mdiff.hunkinrange(hr[2:], lr)
818 if any(mdiff.hunkinrange(hr[2:], lr)
819 for lr in lineranges):
819 for lr in lineranges):
820 yield hr, lines
820 yield hr, lines
821 else:
821 else:
822 for hunk in hunks:
822 for hunk in hunks:
823 yield hunk
823 yield hunk
824
824
825 return filterfn
825 return filterfn
826
826
827 def filematcher(rev):
827 def filematcher(rev):
828 files = list(linerangesbyrev.get(rev, []))
828 files = list(linerangesbyrev.get(rev, []))
829 return scmutil.matchfiles(repo, files)
829 return scmutil.matchfiles(repo, files)
830
830
831 revs = sorted(linerangesbyrev, reverse=True)
831 revs = sorted(linerangesbyrev, reverse=True)
832
832
833 return revs, filematcher, hunksfilter
833 return revs, filematcher, hunksfilter
834
834
835 def _graphnodeformatter(ui, displayer):
835 def _graphnodeformatter(ui, displayer):
836 spec = ui.config('ui', 'graphnodetemplate')
836 spec = ui.config('ui', 'graphnodetemplate')
837 if not spec:
837 if not spec:
838 return templatekw.showgraphnode # fast path for "{graphnode}"
838 return templatekw.showgraphnode # fast path for "{graphnode}"
839
839
840 spec = templater.unquotestring(spec)
840 spec = templater.unquotestring(spec)
841 tres = formatter.templateresources(ui)
841 tres = formatter.templateresources(ui)
842 if isinstance(displayer, changesettemplater):
842 if isinstance(displayer, changesettemplater):
843 tres['cache'] = displayer.cache # reuse cache of slow templates
843 tres['cache'] = displayer.cache # reuse cache of slow templates
844 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
844 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
845 resources=tres)
845 resources=tres)
846 def formatnode(repo, ctx):
846 def formatnode(repo, ctx):
847 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
847 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
848 return templ.render(props)
848 return templ.render(props)
849 return formatnode
849 return formatnode
850
850
851 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
851 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
852 filematcher=None, props=None):
852 filematcher=None, props=None):
853 props = props or {}
853 props = props or {}
854 formatnode = _graphnodeformatter(ui, displayer)
854 formatnode = _graphnodeformatter(ui, displayer)
855 state = graphmod.asciistate()
855 state = graphmod.asciistate()
856 styles = state['styles']
856 styles = state['styles']
857
857
858 # only set graph styling if HGPLAIN is not set.
858 # only set graph styling if HGPLAIN is not set.
859 if ui.plain('graph'):
859 if ui.plain('graph'):
860 # set all edge styles to |, the default pre-3.8 behaviour
860 # set all edge styles to |, the default pre-3.8 behaviour
861 styles.update(dict.fromkeys(styles, '|'))
861 styles.update(dict.fromkeys(styles, '|'))
862 else:
862 else:
863 edgetypes = {
863 edgetypes = {
864 'parent': graphmod.PARENT,
864 'parent': graphmod.PARENT,
865 'grandparent': graphmod.GRANDPARENT,
865 'grandparent': graphmod.GRANDPARENT,
866 'missing': graphmod.MISSINGPARENT
866 'missing': graphmod.MISSINGPARENT
867 }
867 }
868 for name, key in edgetypes.items():
868 for name, key in edgetypes.items():
869 # experimental config: experimental.graphstyle.*
869 # experimental config: experimental.graphstyle.*
870 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
870 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
871 styles[key])
871 styles[key])
872 if not styles[key]:
872 if not styles[key]:
873 styles[key] = None
873 styles[key] = None
874
874
875 # experimental config: experimental.graphshorten
875 # experimental config: experimental.graphshorten
876 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
876 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
877
877
878 for rev, type, ctx, parents in dag:
878 for rev, type, ctx, parents in dag:
879 char = formatnode(repo, ctx)
879 char = formatnode(repo, ctx)
880 copies = None
880 copies = None
881 if getrenamed and ctx.rev():
881 if getrenamed and ctx.rev():
882 copies = []
882 copies = []
883 for fn in ctx.files():
883 for fn in ctx.files():
884 rename = getrenamed(fn, ctx.rev())
884 rename = getrenamed(fn, ctx.rev())
885 if rename:
885 if rename:
886 copies.append((fn, rename[0]))
886 copies.append((fn, rename[0]))
887 revmatchfn = None
887 revmatchfn = None
888 if filematcher is not None:
888 if filematcher is not None:
889 revmatchfn = filematcher(ctx.rev())
889 revmatchfn = filematcher(ctx.rev())
890 edges = edgefn(type, char, state, rev, parents)
890 edges = edgefn(type, char, state, rev, parents)
891 firstedge = next(edges)
891 firstedge = next(edges)
892 width = firstedge[2]
892 width = firstedge[2]
893 displayer.show(ctx, copies=copies, matchfn=revmatchfn,
893 displayer.show(ctx, copies=copies, matchfn=revmatchfn,
894 _graphwidth=width, **pycompat.strkwargs(props))
894 _graphwidth=width, **pycompat.strkwargs(props))
895 lines = displayer.hunk.pop(rev).split('\n')
895 lines = displayer.hunk.pop(rev).split('\n')
896 if not lines[-1]:
896 if not lines[-1]:
897 del lines[-1]
897 del lines[-1]
898 displayer.flush(ctx)
898 displayer.flush(ctx)
899 for type, char, width, coldata in itertools.chain([firstedge], edges):
899 for type, char, width, coldata in itertools.chain([firstedge], edges):
900 graphmod.ascii(ui, state, type, char, lines, coldata)
900 graphmod.ascii(ui, state, type, char, lines, coldata)
901 lines = []
901 lines = []
902 displayer.close()
902 displayer.close()
903
903
904 def graphlog(ui, repo, revs, filematcher, opts):
904 def graphlog(ui, repo, revs, filematcher, opts):
905 # Parameters are identical to log command ones
905 # Parameters are identical to log command ones
906 revdag = graphmod.dagwalker(repo, revs)
906 revdag = graphmod.dagwalker(repo, revs)
907
907
908 getrenamed = None
908 getrenamed = None
909 if opts.get('copies'):
909 if opts.get('copies'):
910 endrev = None
910 endrev = None
911 if opts.get('rev'):
911 if opts.get('rev'):
912 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
912 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
913 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
913 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
914
914
915 ui.pager('log')
915 ui.pager('log')
916 displayer = changesetdisplayer(ui, repo, opts, buffered=True)
916 displayer = changesetdisplayer(ui, repo, opts, buffered=True)
917 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
917 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
918 filematcher)
918 filematcher)
919
919
920 def checkunsupportedgraphflags(pats, opts):
920 def checkunsupportedgraphflags(pats, opts):
921 for op in ["newest_first"]:
921 for op in ["newest_first"]:
922 if op in opts and opts[op]:
922 if op in opts and opts[op]:
923 raise error.Abort(_("-G/--graph option is incompatible with --%s")
923 raise error.Abort(_("-G/--graph option is incompatible with --%s")
924 % op.replace("_", "-"))
924 % op.replace("_", "-"))
925
925
926 def graphrevs(repo, nodes, opts):
926 def graphrevs(repo, nodes, opts):
927 limit = getlimit(opts)
927 limit = getlimit(opts)
928 nodes.reverse()
928 nodes.reverse()
929 if limit is not None:
929 if limit is not None:
930 nodes = nodes[:limit]
930 nodes = nodes[:limit]
931 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