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