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