logcmdutil.py
1339 lines
| 43.4 KiB
| text/x-python
|
PythonLexer
/ mercurial / logcmdutil.py
Yuya Nishihara
|
r35903 | # logcmdutil.py - utility for log-like commands | ||
# | ||||
Raphaël Gomès
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
Yuya Nishihara
|
r35903 | # | ||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Yuya Nishihara
|
r35903 | |||
import itertools | ||||
import os | ||||
Martin von Zweigbergk
|
r41795 | import posixpath | ||
Matt Harbison
|
r52622 | import typing | ||
Yuya Nishihara
|
r35903 | |||
r52178 | from typing import ( | |||
Any, | ||||
Callable, | ||||
Dict, | ||||
Optional, | ||||
Sequence, | ||||
Tuple, | ||||
) | ||||
Yuya Nishihara
|
r35903 | from .i18n import _ | ||
pacien
|
r52036 | from .node import wdirrev | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r46139 | from .thirdparty import attr | ||
Matt Harbison
|
r52622 | # Force pytype to use the non-vendored package | ||
if typing.TYPE_CHECKING: | ||||
# noinspection PyPackageRequirements | ||||
import attr | ||||
Yuya Nishihara
|
r35903 | from . import ( | ||
dagop, | ||||
pacien
|
r52036 | diffutil, | ||
Yuya Nishihara
|
r35903 | error, | ||
formatter, | ||||
graphmod, | ||||
match as matchmod, | ||||
mdiff, | ||||
patch, | ||||
pathutil, | ||||
pycompat, | ||||
revset, | ||||
revsetlang, | ||||
scmutil, | ||||
smartset, | ||||
templatekw, | ||||
templater, | ||||
util, | ||||
) | ||||
Yuya Nishihara
|
r37102 | from .utils import ( | ||
dateutil, | ||||
stringutil, | ||||
) | ||||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r44100 | |||
Yuya Nishihara
|
r35905 | def getlimit(opts): | ||
Yuya Nishihara
|
r35903 | """get the log limit according to option -l/--limit""" | ||
Augie Fackler
|
r43347 | limit = opts.get(b'limit') | ||
Yuya Nishihara
|
r35903 | if limit: | ||
try: | ||||
limit = int(limit) | ||||
except ValueError: | ||||
Martin von Zweigbergk
|
r49401 | raise error.InputError(_(b'limit must be a positive integer')) | ||
Yuya Nishihara
|
r35903 | if limit <= 0: | ||
Martin von Zweigbergk
|
r49401 | raise error.InputError(_(b'limit must be positive')) | ||
Yuya Nishihara
|
r35903 | else: | ||
limit = None | ||||
return limit | ||||
Augie Fackler
|
r43346 | |||
zegervdv
|
r52035 | def get_diff_chunks( | ||
Augie Fackler
|
r43346 | ui, | ||
repo, | ||||
diffopts, | ||||
Augie Fackler
|
r45340 | ctx1, | ||
ctx2, | ||||
Augie Fackler
|
r43346 | match, | ||
changes=None, | ||||
stat=False, | ||||
Augie Fackler
|
r43347 | prefix=b'', | ||
root=b'', | ||||
Augie Fackler
|
r43346 | hunksfilterfn=None, | ||
): | ||||
Yuya Nishihara
|
r35903 | if root: | ||
relroot = pathutil.canonpath(repo.root, repo.getcwd(), root) | ||||
else: | ||||
Augie Fackler
|
r43347 | relroot = b'' | ||
Martin von Zweigbergk
|
r41769 | copysourcematch = None | ||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r41796 | def compose(f, g): | ||
return lambda x: f(g(x)) | ||||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r41795 | def pathfn(f): | ||
return posixpath.join(prefix, f) | ||||
Augie Fackler
|
r43346 | |||
Augie Fackler
|
r43347 | if relroot != b'': | ||
Yuya Nishihara
|
r35903 | # XXX relative roots currently don't work if the root is within a | ||
# subrepo | ||||
Martin von Zweigbergk
|
r41804 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True) | ||
uirelroot = uipathfn(pathfn(relroot)) | ||||
Augie Fackler
|
r43347 | relroot += b'/' | ||
Yuya Nishihara
|
r35903 | for matchroot in match.files(): | ||
if not matchroot.startswith(relroot): | ||||
Augie Fackler
|
r43346 | ui.warn( | ||
Augie Fackler
|
r43347 | _(b'warning: %s not inside relative root %s\n') | ||
Augie Fackler
|
r43346 | % (uipathfn(pathfn(matchroot)), uirelroot) | ||
) | ||||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43347 | relrootmatch = scmutil.match(ctx2, pats=[relroot], default=b'path') | ||
Martin von Zweigbergk
|
r41768 | match = matchmod.intersectmatchers(match, relrootmatch) | ||
Martin von Zweigbergk
|
r41769 | copysourcematch = relrootmatch | ||
Martin von Zweigbergk
|
r41768 | |||
Augie Fackler
|
r43346 | checkroot = repo.ui.configbool( | ||
Augie Fackler
|
r43347 | b'devel', b'all-warnings' | ||
) or repo.ui.configbool(b'devel', b'check-relroot') | ||||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r41796 | def relrootpathfn(f): | ||
Martin von Zweigbergk
|
r41795 | if checkroot and not f.startswith(relroot): | ||
raise AssertionError( | ||||
Augie Fackler
|
r43347 | b"file %s doesn't start with relroot %s" % (f, relroot) | ||
Augie Fackler
|
r43346 | ) | ||
return f[len(relroot) :] | ||||
Martin von Zweigbergk
|
r41796 | pathfn = compose(relrootpathfn, pathfn) | ||
Martin von Zweigbergk
|
r41795 | |||
Yuya Nishihara
|
r35903 | if stat: | ||
diffopts = diffopts.copy(context=0, noprefix=False) | ||||
Martin von Zweigbergk
|
r41819 | # If an explicit --root was given, don't respect ui.relative-paths | ||
if not relroot: | ||||
pathfn = compose(scmutil.getuipathfn(repo), pathfn) | ||||
Joerg Sonnenberger
|
r35979 | |||
zegervdv
|
r52035 | return ctx2.diff( | ||
Augie Fackler
|
r43346 | ctx1, | ||
match, | ||||
changes, | ||||
opts=diffopts, | ||||
pathfn=pathfn, | ||||
copysourcematch=copysourcematch, | ||||
hunksfilterfn=hunksfilterfn, | ||||
) | ||||
Joerg Sonnenberger
|
r35979 | |||
zegervdv
|
r52035 | |||
def diffordiffstat( | ||||
ui, | ||||
repo, | ||||
diffopts, | ||||
ctx1, | ||||
ctx2, | ||||
match, | ||||
changes=None, | ||||
stat=False, | ||||
fp=None, | ||||
graphwidth=0, | ||||
prefix=b'', | ||||
root=b'', | ||||
listsubrepos=False, | ||||
hunksfilterfn=None, | ||||
): | ||||
'''show diff or diffstat.''' | ||||
chunks = get_diff_chunks( | ||||
ui, | ||||
repo, | ||||
diffopts, | ||||
ctx1, | ||||
ctx2, | ||||
match, | ||||
changes=changes, | ||||
stat=stat, | ||||
prefix=prefix, | ||||
root=root, | ||||
hunksfilterfn=hunksfilterfn, | ||||
) | ||||
if stat: | ||||
diffopts = diffopts.copy(context=0, noprefix=False) | ||||
width = 80 | ||||
if not ui.plain(): | ||||
width = ui.termwidth() - graphwidth | ||||
Joerg Sonnenberger
|
r35979 | if fp is not None or ui.canwritewithoutlabels(): | ||
Yuya Nishihara
|
r36025 | out = fp or ui | ||
Joerg Sonnenberger
|
r35979 | if stat: | ||
Gregory Szorc
|
r36132 | chunks = [patch.diffstat(util.iterlines(chunks), width=width)] | ||
Joerg Sonnenberger
|
r35979 | for chunk in util.filechunkiter(util.chunkbuffer(chunks)): | ||
Yuya Nishihara
|
r36025 | out.write(chunk) | ||
Yuya Nishihara
|
r35903 | else: | ||
Joerg Sonnenberger
|
r35979 | if stat: | ||
chunks = patch.diffstatui(util.iterlines(chunks), width=width) | ||||
else: | ||||
Augie Fackler
|
r43346 | chunks = patch.difflabel( | ||
lambda chunks, **kwargs: chunks, chunks, opts=diffopts | ||||
) | ||||
Joerg Sonnenberger
|
r35979 | if ui.canbatchlabeledwrites(): | ||
Augie Fackler
|
r43346 | |||
Joerg Sonnenberger
|
r35979 | def gen(): | ||
for chunk, label in chunks: | ||||
yield ui.label(chunk, label=label) | ||||
Augie Fackler
|
r43346 | |||
Joerg Sonnenberger
|
r35979 | for chunk in util.filechunkiter(util.chunkbuffer(gen())): | ||
Yuya Nishihara
|
r36025 | ui.write(chunk) | ||
Joerg Sonnenberger
|
r35979 | else: | ||
for chunk, label in chunks: | ||||
Yuya Nishihara
|
r36025 | ui.write(chunk, label=label) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r45340 | node2 = ctx2.node() | ||
Matt Harbison
|
r42177 | for subpath, sub in scmutil.itersubrepos(ctx1, ctx2): | ||
tempnode2 = node2 | ||||
try: | ||||
if node2 is not None: | ||||
tempnode2 = ctx2.substate[subpath][1] | ||||
except KeyError: | ||||
# A subrepo that existed in node1 was deleted between node1 and | ||||
# node2 (inclusive). Thus, ctx2's substate won't contain that | ||||
# subpath. The best we can do is to ignore it. | ||||
tempnode2 = None | ||||
submatch = matchmod.subdirmatcher(subpath, match) | ||||
subprefix = repo.wvfs.reljoin(prefix, subpath) | ||||
if listsubrepos or match.exact(subpath) or any(submatch.files()): | ||||
Augie Fackler
|
r43346 | sub.diff( | ||
ui, | ||||
diffopts, | ||||
tempnode2, | ||||
submatch, | ||||
changes=changes, | ||||
stat=stat, | ||||
fp=fp, | ||||
prefix=subprefix, | ||||
) | ||||
Yuya Nishihara
|
r35903 | |||
Gregory Szorc
|
r49801 | class changesetdiffer: | ||
Yuya Nishihara
|
r36024 | """Generate diff of changeset with pre-configured filtering functions""" | ||
def _makefilematcher(self, ctx): | ||||
return scmutil.matchall(ctx.repo()) | ||||
def _makehunksfilter(self, ctx): | ||||
return None | ||||
Navaneeth Suresh
|
r41129 | def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False): | ||
Augie Fackler
|
r43346 | diffordiffstat( | ||
ui, | ||||
Augie Fackler
|
r45340 | ctx.repo(), | ||
Augie Fackler
|
r43346 | diffopts, | ||
pacien
|
r52036 | diffutil.diff_parent(ctx), | ||
Augie Fackler
|
r45340 | ctx, | ||
Augie Fackler
|
r43346 | match=self._makefilematcher(ctx), | ||
stat=stat, | ||||
graphwidth=graphwidth, | ||||
hunksfilterfn=self._makehunksfilter(ctx), | ||||
) | ||||
zegervdv
|
r52035 | def getdiffstats(self, ui, ctx, diffopts, stat=False): | ||
chunks = get_diff_chunks( | ||||
ui, | ||||
ctx.repo(), | ||||
diffopts, | ||||
pacien
|
r52036 | diffutil.diff_parent(ctx), | ||
zegervdv
|
r52035 | ctx, | ||
match=self._makefilematcher(ctx), | ||||
stat=stat, | ||||
hunksfilterfn=self._makehunksfilter(ctx), | ||||
) | ||||
diffdata = [] | ||||
for filename, additions, removals, binary in patch.diffstatdata( | ||||
util.iterlines(chunks) | ||||
): | ||||
diffdata.append( | ||||
{ | ||||
b"name": filename, | ||||
b"additions": additions, | ||||
b"removals": removals, | ||||
b"binary": binary, | ||||
} | ||||
) | ||||
return diffdata | ||||
Yuya Nishihara
|
r36024 | |||
Yuya Nishihara
|
r35904 | def changesetlabels(ctx): | ||
Augie Fackler
|
r43347 | labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()] | ||
Yuya Nishihara
|
r35903 | if ctx.obsolete(): | ||
Augie Fackler
|
r43347 | labels.append(b'changeset.obsolete') | ||
Yuya Nishihara
|
r35903 | if ctx.isunstable(): | ||
Augie Fackler
|
r43347 | labels.append(b'changeset.unstable') | ||
Yuya Nishihara
|
r35903 | for instability in ctx.instabilities(): | ||
Augie Fackler
|
r43347 | labels.append(b'instability.%s' % instability) | ||
return b' '.join(labels) | ||||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r49801 | class changesetprinter: | ||
Yuya Nishihara
|
r35903 | '''show changeset information when templating not requested.''' | ||
Yuya Nishihara
|
r36024 | def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False): | ||
Yuya Nishihara
|
r35903 | self.ui = ui | ||
self.repo = repo | ||||
self.buffered = buffered | ||||
Yuya Nishihara
|
r36024 | self._differ = differ or changesetdiffer() | ||
Yuya Nishihara
|
r37858 | self._diffopts = patch.diffallopts(ui, diffopts) | ||
Augie Fackler
|
r43347 | self._includestat = diffopts and diffopts.get(b'stat') | ||
self._includediff = diffopts and diffopts.get(b'patch') | ||||
Yuya Nishihara
|
r35903 | self.header = {} | ||
self.hunk = {} | ||||
self.lastheader = None | ||||
self.footer = None | ||||
self._columns = templatekw.getlogcolumns() | ||||
def flush(self, ctx): | ||||
rev = ctx.rev() | ||||
if rev in self.header: | ||||
h = self.header[rev] | ||||
if h != self.lastheader: | ||||
self.lastheader = h | ||||
self.ui.write(h) | ||||
del self.header[rev] | ||||
if rev in self.hunk: | ||||
self.ui.write(self.hunk[rev]) | ||||
del self.hunk[rev] | ||||
def close(self): | ||||
if self.footer: | ||||
self.ui.write(self.footer) | ||||
Yuya Nishihara
|
r36020 | def show(self, ctx, copies=None, **props): | ||
Yuya Nishihara
|
r35903 | props = pycompat.byteskwargs(props) | ||
if self.buffered: | ||||
self.ui.pushbuffer(labeled=True) | ||||
Yuya Nishihara
|
r36020 | self._show(ctx, copies, props) | ||
Yuya Nishihara
|
r35903 | self.hunk[ctx.rev()] = self.ui.popbuffer() | ||
else: | ||||
Yuya Nishihara
|
r36020 | self._show(ctx, copies, props) | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36020 | def _show(self, ctx, copies, props): | ||
Yuya Nishihara
|
r35903 | '''show a single changeset or file revision''' | ||
changenode = ctx.node() | ||||
Augie Fackler
|
r43347 | graphwidth = props.get(b'graphwidth', 0) | ||
Yuya Nishihara
|
r35903 | |||
if self.ui.quiet: | ||||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | b"%s\n" % scmutil.formatchangeid(ctx), label=b'log.node' | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | return | ||
columns = self._columns | ||||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'changeset'] % scmutil.formatchangeid(ctx), | ||
Augie Fackler
|
r43346 | label=changesetlabels(ctx), | ||
) | ||||
Yuya Nishihara
|
r35903 | |||
# branches are shown first before any other names due to backwards | ||||
# compatibility | ||||
branch = ctx.branch() | ||||
# don't show the default branch name | ||||
Augie Fackler
|
r43347 | if branch != b'default': | ||
self.ui.write(columns[b'branch'] % branch, label=b'log.branch') | ||||
Yuya Nishihara
|
r35903 | |||
Gregory Szorc
|
r49768 | for nsname, ns in self.repo.names.items(): | ||
Yuya Nishihara
|
r35903 | # branches has special logic already handled above, so here we just | ||
# skip it | ||||
Augie Fackler
|
r43347 | if nsname == b'branches': | ||
Yuya Nishihara
|
r35903 | continue | ||
# we will use the templatename as the color name since those two | ||||
# should be the same | ||||
for name in ns.names(self.repo, changenode): | ||||
Augie Fackler
|
r43347 | self.ui.write(ns.logfmt % name, label=b'log.%s' % ns.colorname) | ||
Yuya Nishihara
|
r35903 | if self.ui.debugflag: | ||
Augie Fackler
|
r43347 | self.ui.write( | ||
columns[b'phase'] % ctx.phasestr(), label=b'log.phase' | ||||
) | ||||
Yuya Nishihara
|
r35903 | for pctx in scmutil.meaningfulparents(self.repo, ctx): | ||
Augie Fackler
|
r43347 | label = b'log.parent changeset.%s' % pctx.phasestr() | ||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'parent'] % scmutil.formatchangeid(pctx), label=label | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r39832 | if self.ui.debugflag: | ||
Yuya Nishihara
|
r35903 | mnode = ctx.manifestnode() | ||
Yuya Nishihara
|
r39832 | if mnode is None: | ||
Joerg Sonnenberger
|
r47771 | mnode = self.repo.nodeconstants.wdirid | ||
Yuya Nishihara
|
r39832 | mrev = wdirrev | ||
else: | ||||
mrev = self.repo.manifestlog.rev(mnode) | ||||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'manifest'] | ||
Augie Fackler
|
r43346 | % scmutil.formatrevnode(self.ui, mrev, mnode), | ||
Augie Fackler
|
r43347 | label=b'ui.debug log.manifest', | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | self.ui.write(columns[b'user'] % ctx.user(), label=b'log.user') | ||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'date'] % dateutil.datestr(ctx.date()), label=b'log.date' | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
if ctx.isunstable(): | ||||
instabilities = ctx.instabilities() | ||||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'instability'] % b', '.join(instabilities), | ||
label=b'log.instability', | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
elif ctx.obsolete(): | ||||
self._showobsfate(ctx) | ||||
self._exthook(ctx) | ||||
if self.ui.debugflag: | ||||
Augie Fackler
|
r44047 | for key, value in zip( | ||
[b'files', b'files+', b'files-'], | ||||
r50210 | [ctx.filesmodified(), ctx.filesadded(), ctx.filesremoved()], | |||
Augie Fackler
|
r44047 | ): | ||
Yuya Nishihara
|
r35903 | if value: | ||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[key] % b" ".join(value), | ||
label=b'ui.debug log.files', | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | elif ctx.files() and self.ui.verbose: | ||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'files'] % b" ".join(ctx.files()), | ||
label=b'ui.note log.files', | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | if copies and self.ui.verbose: | ||
Augie Fackler
|
r43347 | copies = [b'%s (%s)' % c for c in copies] | ||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'copies'] % b' '.join(copies), | ||
label=b'ui.note log.copies', | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
extra = ctx.extra() | ||||
if extra and self.ui.debugflag: | ||||
for key, value in sorted(extra.items()): | ||||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | columns[b'extra'] % (key, stringutil.escapestr(value)), | ||
label=b'ui.debug log.extra', | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
description = ctx.description().strip() | ||||
if description: | ||||
if self.ui.verbose: | ||||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | _(b"description:\n"), label=b'ui.note log.description' | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | self.ui.write(description, label=b'ui.note log.description') | ||
self.ui.write(b"\n\n") | ||||
Yuya Nishihara
|
r35903 | else: | ||
Augie Fackler
|
r43346 | self.ui.write( | ||
Martin von Zweigbergk
|
r49893 | columns[b'summary'] % stringutil.firstline(description), | ||
Augie Fackler
|
r43347 | label=b'log.summary', | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | self.ui.write(b"\n") | ||
Yuya Nishihara
|
r35903 | |||
Navaneeth Suresh
|
r41129 | self._showpatch(ctx, graphwidth) | ||
Yuya Nishihara
|
r35903 | |||
def _showobsfate(self, ctx): | ||||
Yuya Nishihara
|
r36534 | # TODO: do not depend on templater | ||
tres = formatter.templateresources(self.repo.ui, self.repo) | ||||
Augie Fackler
|
r43346 | t = formatter.maketemplater( | ||
self.repo.ui, | ||||
Augie Fackler
|
r43347 | b'{join(obsfate, "\n")}', | ||
Augie Fackler
|
r43346 | defaults=templatekw.keywords, | ||
resources=tres, | ||||
) | ||||
Augie Fackler
|
r43347 | obsfate = t.renderdefault({b'ctx': ctx}).splitlines() | ||
Yuya Nishihara
|
r35903 | |||
if obsfate: | ||||
for obsfateline in obsfate: | ||||
Augie Fackler
|
r43346 | self.ui.write( | ||
Augie Fackler
|
r43347 | self._columns[b'obsolete'] % obsfateline, | ||
label=b'log.obsfate', | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
def _exthook(self, ctx): | ||||
Augie Fackler
|
r46554 | """empty method used by extension as a hook point""" | ||
Yuya Nishihara
|
r35903 | |||
Navaneeth Suresh
|
r41129 | def _showpatch(self, ctx, graphwidth=0): | ||
Yuya Nishihara
|
r37859 | if self._includestat: | ||
Augie Fackler
|
r43346 | self._differ.showdiff( | ||
self.ui, ctx, self._diffopts, graphwidth, stat=True | ||||
) | ||||
Yuya Nishihara
|
r37859 | if self._includestat and self._includediff: | ||
Augie Fackler
|
r43347 | self.ui.write(b"\n") | ||
Yuya Nishihara
|
r37859 | if self._includediff: | ||
Augie Fackler
|
r43346 | self._differ.showdiff( | ||
self.ui, ctx, self._diffopts, graphwidth, stat=False | ||||
) | ||||
Yuya Nishihara
|
r37859 | if self._includestat or self._includediff: | ||
Augie Fackler
|
r43347 | self.ui.write(b"\n") | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r37791 | class changesetformatter(changesetprinter): | ||
"""Format changeset information by generic formatter""" | ||||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | def __init__( | ||
self, ui, repo, fm, differ=None, diffopts=None, buffered=False | ||||
): | ||||
Yuya Nishihara
|
r36024 | changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered) | ||
Yuya Nishihara
|
r37858 | self._diffopts = patch.difffeatureopts(ui, diffopts, git=True) | ||
Yuya Nishihara
|
r37791 | self._fm = fm | ||
Yuya Nishihara
|
r35903 | |||
def close(self): | ||||
Yuya Nishihara
|
r37790 | self._fm.end() | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36020 | def _show(self, ctx, copies, props): | ||
Yuya Nishihara
|
r35903 | '''show a single changeset or file revision''' | ||
Yuya Nishihara
|
r37790 | fm = self._fm | ||
fm.startitem() | ||||
Yuya Nishihara
|
r39741 | fm.context(ctx=ctx) | ||
Augie Fackler
|
r43346 | fm.data(rev=scmutil.intrev(ctx), node=fm.hexfunc(scmutil.binnode(ctx))) | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r43372 | datahint = fm.datahint() | ||
if self.ui.quiet and not datahint: | ||||
Yuya Nishihara
|
r35903 | return | ||
Augie Fackler
|
r43346 | fm.data( | ||
branch=ctx.branch(), | ||||
phase=ctx.phasestr(), | ||||
user=ctx.user(), | ||||
date=fm.formatdate(ctx.date()), | ||||
desc=ctx.description(), | ||||
Augie Fackler
|
r43347 | bookmarks=fm.formatlist(ctx.bookmarks(), name=b'bookmark'), | ||
tags=fm.formatlist(ctx.tags(), name=b'tag'), | ||||
Augie Fackler
|
r43346 | parents=fm.formatlist( | ||
Augie Fackler
|
r43347 | [fm.hexfunc(c.node()) for c in ctx.parents()], name=b'node' | ||
Augie Fackler
|
r43346 | ), | ||
) | ||||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r43372 | if self.ui.debugflag or b'manifest' in datahint: | ||
Joerg Sonnenberger
|
r47771 | fm.data( | ||
manifest=fm.hexfunc( | ||||
ctx.manifestnode() or self.repo.nodeconstants.wdirid | ||||
) | ||||
) | ||||
Yuya Nishihara
|
r43372 | if self.ui.debugflag or b'extra' in datahint: | ||
fm.data(extra=fm.formatdict(ctx.extra())) | ||||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r43372 | if ( | ||
self.ui.debugflag | ||||
or b'modified' in datahint | ||||
or b'added' in datahint | ||||
or b'removed' in datahint | ||||
): | ||||
Augie Fackler
|
r43346 | fm.data( | ||
r50210 | modified=fm.formatlist(ctx.filesmodified(), name=b'file'), | |||
added=fm.formatlist(ctx.filesadded(), name=b'file'), | ||||
removed=fm.formatlist(ctx.filesremoved(), name=b'file'), | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r43372 | verbose = not self.ui.debugflag and self.ui.verbose | ||
if verbose or b'files' in datahint: | ||||
Augie Fackler
|
r43347 | fm.data(files=fm.formatlist(ctx.files(), name=b'file')) | ||
Yuya Nishihara
|
r43372 | if verbose and copies or b'copies' in datahint: | ||
fm.data( | ||||
copies=fm.formatdict(copies or {}, key=b'name', value=b'source') | ||||
) | ||||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r43372 | if self._includestat or b'diffstat' in datahint: | ||
zegervdv
|
r52035 | data = self._differ.getdiffstats( | ||
self.ui, ctx, self._diffopts, stat=True | ||||
) | ||||
fm.data(diffstat=fm.formatlist(data, name=b'diffstat')) | ||||
Yuya Nishihara
|
r43372 | if self._includediff or b'diff' in datahint: | ||
Yuya Nishihara
|
r36021 | self.ui.pushbuffer() | ||
Yuya Nishihara
|
r37858 | self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False) | ||
Yuya Nishihara
|
r37790 | fm.data(diff=self.ui.popbuffer()) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35904 | class changesettemplater(changesetprinter): | ||
Augie Fackler
|
r46554 | """format changeset information. | ||
Yuya Nishihara
|
r35903 | |||
Note: there are a variety of convenience functions to build a | ||||
Yuya Nishihara
|
r35904 | changesettemplater for common cases. See functions such as: | ||
Yuya Nishihara
|
r35905 | maketemplater, changesetdisplayer, buildcommittemplate, or other | ||
Yuya Nishihara
|
r35903 | functions that use changesest_templater. | ||
Augie Fackler
|
r46554 | """ | ||
Yuya Nishihara
|
r35903 | |||
Matt Harbison
|
r52708 | _tresources: formatter.templateresources | ||
lastheader: Optional[bytes] | ||||
t: templater.templater | ||||
Yuya Nishihara
|
r35903 | # Arguments before "buffered" used to be positional. Consider not | ||
# adding/removing arguments before "buffered" to not break callers. | ||||
Augie Fackler
|
r43346 | def __init__( | ||
self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False | ||||
): | ||||
Yuya Nishihara
|
r36024 | changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered) | ||
Yuya Nishihara
|
r37090 | # tres is shared with _graphnodeformatter() | ||
self._tresources = tres = formatter.templateresources(ui, repo) | ||||
Augie Fackler
|
r43346 | self.t = formatter.loadtemplater( | ||
ui, | ||||
tmplspec, | ||||
defaults=templatekw.keywords, | ||||
resources=tres, | ||||
cache=templatekw.defaulttempl, | ||||
) | ||||
Yuya Nishihara
|
r35903 | self._counter = itertools.count() | ||
self._tref = tmplspec.ref | ||||
Augie Fackler
|
r43346 | self._parts = { | ||
Augie Fackler
|
r43347 | b'header': b'', | ||
b'footer': b'', | ||||
Augie Fackler
|
r43346 | tmplspec.ref: tmplspec.ref, | ||
Augie Fackler
|
r43347 | b'docheader': b'', | ||
b'docfooter': b'', | ||||
b'separator': b'', | ||||
Augie Fackler
|
r43346 | } | ||
Yuya Nishihara
|
r35903 | if tmplspec.mapfile: | ||
# find correct templates for current mode, for backward | ||||
# compatibility with 'log -v/-q/--debug' using a mapfile | ||||
tmplmodes = [ | ||||
Augie Fackler
|
r43347 | (True, b''), | ||
(self.ui.verbose, b'_verbose'), | ||||
(self.ui.quiet, b'_quiet'), | ||||
(self.ui.debugflag, b'_debug'), | ||||
Yuya Nishihara
|
r35903 | ] | ||
for mode, postfix in tmplmodes: | ||||
for t in self._parts: | ||||
cur = t + postfix | ||||
if mode and cur in self.t: | ||||
self._parts[t] = cur | ||||
else: | ||||
partnames = [p for p in self._parts.keys() if p != tmplspec.ref] | ||||
m = formatter.templatepartsmap(tmplspec, self.t, partnames) | ||||
self._parts.update(m) | ||||
Augie Fackler
|
r43347 | if self._parts[b'docheader']: | ||
self.ui.write(self.t.render(self._parts[b'docheader'], {})) | ||||
Yuya Nishihara
|
r35903 | |||
def close(self): | ||||
Augie Fackler
|
r43347 | if self._parts[b'docfooter']: | ||
Yuya Nishihara
|
r35903 | if not self.footer: | ||
Augie Fackler
|
r43347 | self.footer = b"" | ||
self.footer += self.t.render(self._parts[b'docfooter'], {}) | ||||
Yuya Nishihara
|
r35904 | return super(changesettemplater, self).close() | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36020 | def _show(self, ctx, copies, props): | ||
Yuya Nishihara
|
r35903 | '''show a single changeset or file revision''' | ||
props = props.copy() | ||||
Augie Fackler
|
r43347 | props[b'ctx'] = ctx | ||
props[b'index'] = index = next(self._counter) | ||||
props[b'revcache'] = {b'copies': copies} | ||||
graphwidth = props.get(b'graphwidth', 0) | ||||
Yuya Nishihara
|
r35903 | |||
# write separator, which wouldn't work well with the header part below | ||||
# since there's inherently a conflict between header (across items) and | ||||
# separator (per item) | ||||
Augie Fackler
|
r43347 | if self._parts[b'separator'] and index > 0: | ||
self.ui.write(self.t.render(self._parts[b'separator'], {})) | ||||
Yuya Nishihara
|
r35903 | |||
# write header | ||||
Augie Fackler
|
r43347 | if self._parts[b'header']: | ||
h = self.t.render(self._parts[b'header'], props) | ||||
Yuya Nishihara
|
r35903 | if self.buffered: | ||
self.header[ctx.rev()] = h | ||||
else: | ||||
if self.lastheader != h: | ||||
self.lastheader = h | ||||
self.ui.write(h) | ||||
# write changeset metadata, then patch if requested | ||||
key = self._parts[self._tref] | ||||
Yuya Nishihara
|
r37004 | self.ui.write(self.t.render(key, props)) | ||
Denis Laxalde
|
r44282 | self._exthook(ctx) | ||
Navaneeth Suresh
|
r41129 | self._showpatch(ctx, graphwidth) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43347 | if self._parts[b'footer']: | ||
Yuya Nishihara
|
r35903 | if not self.footer: | ||
Augie Fackler
|
r43347 | self.footer = self.t.render(self._parts[b'footer'], props) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Matt Harbison
|
r52708 | def templatespec(tmpl, mapfile) -> formatter.templatespec: | ||
Martin von Zweigbergk
|
r45825 | assert not (tmpl and mapfile) | ||
Yuya Nishihara
|
r35903 | if mapfile: | ||
Martin von Zweigbergk
|
r45825 | return formatter.mapfile_templatespec(b'changeset', mapfile) | ||
Yuya Nishihara
|
r35903 | else: | ||
Martin von Zweigbergk
|
r45825 | return formatter.literal_templatespec(tmpl) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Matt Harbison
|
r52708 | def _lookuptemplate(ui, tmpl, style) -> formatter.templatespec: | ||
Yuya Nishihara
|
r35903 | """Find the template matching the given template spec or style | ||
See formatter.lookuptemplate() for details. | ||||
""" | ||||
# ui settings | ||||
Augie Fackler
|
r43346 | if not tmpl and not style: # template are stronger than style | ||
Martin von Zweigbergk
|
r46350 | tmpl = ui.config(b'command-templates', b'log') | ||
Yuya Nishihara
|
r35903 | if tmpl: | ||
Martin von Zweigbergk
|
r45827 | return formatter.literal_templatespec(templater.unquotestring(tmpl)) | ||
Yuya Nishihara
|
r35903 | else: | ||
Augie Fackler
|
r43347 | style = util.expandpath(ui.config(b'ui', b'style')) | ||
Yuya Nishihara
|
r35903 | |||
if not tmpl and style: | ||||
mapfile = style | ||||
Martin von Zweigbergk
|
r45870 | fp = None | ||
Yuya Nishihara
|
r35903 | if not os.path.split(mapfile)[0]: | ||
Martin von Zweigbergk
|
r45880 | (mapname, fp) = templater.try_open_template( | ||
Augie Fackler
|
r43347 | b'map-cmdline.' + mapfile | ||
Martin von Zweigbergk
|
r45880 | ) or templater.try_open_template(mapfile) | ||
Yuya Nishihara
|
r35903 | if mapname: | ||
mapfile = mapname | ||||
Martin von Zweigbergk
|
r45870 | return formatter.mapfile_templatespec(b'changeset', mapfile, fp) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43347 | return formatter.lookuptemplate(ui, b'changeset', tmpl) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35905 | def maketemplater(ui, repo, tmpl, buffered=False): | ||
Yuya Nishihara
|
r35904 | """Create a changesettemplater from a literal template 'tmpl' | ||
Yuya Nishihara
|
r35903 | byte-string.""" | ||
Martin von Zweigbergk
|
r45827 | spec = formatter.literal_templatespec(tmpl) | ||
Yuya Nishihara
|
r35904 | return changesettemplater(ui, repo, spec, buffered=buffered) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r36024 | def changesetdisplayer(ui, repo, opts, differ=None, buffered=False): | ||
Yuya Nishihara
|
r35903 | """show one changeset using template or regular display. | ||
Display format will be the first non-empty hit of: | ||||
1. option 'template' | ||||
2. option 'style' | ||||
Martin von Zweigbergk
|
r46350 | 3. [command-templates] setting 'log' | ||
Yuya Nishihara
|
r35903 | 4. [ui] setting 'style' | ||
If all of these values are either the unset or the empty string, | ||||
Yuya Nishihara
|
r35904 | regular display via changesetprinter() is done. | ||
Yuya Nishihara
|
r35903 | """ | ||
Yuya Nishihara
|
r36024 | postargs = (differ, opts, buffered) | ||
Yuya Nishihara
|
r43370 | spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style')) | ||
# machine-readable formats have slightly different keyword set than | ||||
# plain templates, which are handled by changesetformatter. | ||||
# note that {b'pickle', b'debug'} can also be added to the list if needed. | ||||
if spec.ref in {b'cbor', b'json'}: | ||||
Augie Fackler
|
r43347 | fm = ui.formatter(b'log', opts) | ||
Yuya Nishihara
|
r37791 | return changesetformatter(ui, repo, fm, *postargs) | ||
Yuya Nishihara
|
r35903 | |||
if not spec.ref and not spec.tmpl and not spec.mapfile: | ||||
Yuya Nishihara
|
r36020 | return changesetprinter(ui, repo, *postargs) | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36020 | return changesettemplater(ui, repo, spec, *postargs) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r46139 | @attr.s | ||
Gregory Szorc
|
r49801 | class walkopts: | ||
Yuya Nishihara
|
r46139 | """Options to configure a set of revisions and file matcher factory | ||
to scan revision/file history | ||||
""" | ||||
# raw command-line parameters, which a matcher will be built from | ||||
Matt Harbison
|
r48827 | pats = attr.ib() | ||
opts = attr.ib() | ||||
Yuya Nishihara
|
r46139 | |||
Yuya Nishihara
|
r46142 | # a list of revset expressions to be traversed; if follow, it specifies | ||
# the start revisions | ||||
Matt Harbison
|
r48827 | revspec = attr.ib() | ||
Yuya Nishihara
|
r46142 | |||
Yuya Nishihara
|
r46201 | # miscellaneous queries to filter revisions (see "hg help log" for details) | ||
Matt Harbison
|
r48827 | bookmarks = attr.ib(default=attr.Factory(list)) | ||
branches = attr.ib(default=attr.Factory(list)) | ||||
date = attr.ib(default=None) | ||||
keywords = attr.ib(default=attr.Factory(list)) | ||||
no_merges = attr.ib(default=False) | ||||
only_merges = attr.ib(default=False) | ||||
prune_ancestors = attr.ib(default=attr.Factory(list)) | ||||
users = attr.ib(default=attr.Factory(list)) | ||||
Yuya Nishihara
|
r46201 | |||
# miscellaneous matcher arguments | ||||
Matt Harbison
|
r48827 | include_pats = attr.ib(default=attr.Factory(list)) | ||
exclude_pats = attr.ib(default=attr.Factory(list)) | ||||
Yuya Nishihara
|
r46201 | |||
Yuya Nishihara
|
r46140 | # 0: no follow, 1: follow first, 2: follow both parents | ||
Matt Harbison
|
r48827 | follow = attr.ib(default=0) | ||
Yuya Nishihara
|
r46140 | |||
Yuya Nishihara
|
r46200 | # do not attempt filelog-based traversal, which may be fast but cannot | ||
# include revisions where files were removed | ||||
Matt Harbison
|
r48827 | force_changelog_traversal = attr.ib(default=False) | ||
Yuya Nishihara
|
r46200 | |||
Yuya Nishihara
|
r46224 | # filter revisions by file patterns, which should be disabled only if | ||
# you want to include revisions where files were unmodified | ||||
Matt Harbison
|
r48827 | filter_revisions_by_pats = attr.ib(default=True) | ||
Yuya Nishihara
|
r46224 | |||
Yuya Nishihara
|
r46202 | # sort revisions prior to traversal: 'desc', 'topo', or None | ||
Matt Harbison
|
r48827 | sort_revisions = attr.ib(default=None) | ||
Yuya Nishihara
|
r46202 | |||
Yuya Nishihara
|
r46141 | # limit number of changes displayed; None means unlimited | ||
Matt Harbison
|
r48827 | limit = attr.ib(default=None) | ||
Yuya Nishihara
|
r46141 | |||
Yuya Nishihara
|
r46139 | |||
r52180 | def parseopts( | |||
ui: Any, | ||||
pats: Sequence[bytes], | ||||
opts: Dict[bytes, Any], | ||||
) -> walkopts: | ||||
Yuya Nishihara
|
r46139 | """Parse log command options into walkopts | ||
Yuya Nishihara
|
r46203 | The returned walkopts will be passed in to getrevs() or makewalker(). | ||
Yuya Nishihara
|
r46139 | """ | ||
Yuya Nishihara
|
r46140 | if opts.get(b'follow_first'): | ||
follow = 1 | ||||
elif opts.get(b'follow'): | ||||
follow = 2 | ||||
else: | ||||
follow = 0 | ||||
Yuya Nishihara
|
r46202 | if opts.get(b'graph'): | ||
if ui.configbool(b'experimental', b'log.topo'): | ||||
sort_revisions = b'topo' | ||||
else: | ||||
sort_revisions = b'desc' | ||||
else: | ||||
sort_revisions = None | ||||
Yuya Nishihara
|
r46142 | return walkopts( | ||
pats=pats, | ||||
opts=opts, | ||||
revspec=opts.get(b'rev', []), | ||||
Yuya Nishihara
|
r46656 | bookmarks=opts.get(b'bookmark', []), | ||
Yuya Nishihara
|
r46201 | # branch and only_branch are really aliases and must be handled at | ||
# the same time | ||||
branches=opts.get(b'branch', []) + opts.get(b'only_branch', []), | ||||
date=opts.get(b'date'), | ||||
keywords=opts.get(b'keyword', []), | ||||
no_merges=bool(opts.get(b'no_merges')), | ||||
only_merges=bool(opts.get(b'only_merges')), | ||||
prune_ancestors=opts.get(b'prune', []), | ||||
users=opts.get(b'user', []), | ||||
include_pats=opts.get(b'include', []), | ||||
exclude_pats=opts.get(b'exclude', []), | ||||
Yuya Nishihara
|
r46142 | follow=follow, | ||
Yuya Nishihara
|
r46200 | force_changelog_traversal=bool(opts.get(b'removed')), | ||
Yuya Nishihara
|
r46202 | sort_revisions=sort_revisions, | ||
Yuya Nishihara
|
r46142 | limit=getlimit(opts), | ||
) | ||||
Yuya Nishihara
|
r46139 | |||
def _makematcher(repo, revs, wopts): | ||||
Yuya Nishihara
|
r35903 | """Build matcher and expanded patterns from log options | ||
If --follow, revs are the revisions to follow from. | ||||
Returns (match, pats, slowpath) where | ||||
- match: a matcher built from the given pats and -I/-X opts | ||||
- pats: patterns used (globs are expanded on Windows) | ||||
- slowpath: True if patterns aren't as simple as scanning filelogs | ||||
""" | ||||
# pats/include/exclude are passed to match.match() directly in | ||||
Yuya Nishihara
|
r46228 | # _matchfiles() revset, but a log-like command should build its matcher | ||
# with scmutil.match(). The difference is input pats are globbed on | ||||
Yuya Nishihara
|
r35903 | # platforms without shell expansion (windows). | ||
wctx = repo[None] | ||||
Yuya Nishihara
|
r46139 | match, pats = scmutil.matchandpats(wctx, wopts.pats, wopts.opts) | ||
slowpath = match.anypats() or ( | ||||
Yuya Nishihara
|
r46200 | not match.always() and wopts.force_changelog_traversal | ||
Yuya Nishihara
|
r46139 | ) | ||
Yuya Nishihara
|
r35903 | if not slowpath: | ||
Yuya Nishihara
|
r46142 | if wopts.follow and wopts.revspec: | ||
Yuya Nishihara
|
r46045 | # There may be the case that a path doesn't exist in some (but | ||
# not all) of the specified start revisions, but let's consider | ||||
# the path is valid. Missing files will be warned by the matcher. | ||||
r50517 | all_files = list(match.files()) | |||
missing_files = set(all_files) | ||||
files = all_files | ||||
for r in revs: | ||||
if not files: | ||||
# We don't have any file to check anymore. | ||||
break | ||||
ctx = repo[r] | ||||
for f in files: | ||||
if f in ctx: | ||||
missing_files.discard(f) | ||||
elif ctx.hasdir(f): | ||||
Yuya Nishihara
|
r46045 | # If a directory exists in any of the start revisions, | ||
# take the slow path. | ||||
r50517 | missing_files.discard(f) | |||
slowpath = True | ||||
# we found on slow path, no need to search for more. | ||||
files = missing_files | ||||
for f in all_files: | ||||
if f in missing_files: | ||||
Martin von Zweigbergk
|
r49402 | raise error.StateError( | ||
Yuya Nishihara
|
r46045 | _( | ||
b'cannot follow file not in any of the specified ' | ||||
b'revisions: "%s"' | ||||
) | ||||
% f | ||||
) | ||||
Yuya Nishihara
|
r46140 | elif wopts.follow: | ||
Yuya Nishihara
|
r46043 | for f in match.files(): | ||
if f not in wctx: | ||||
# If the file exists, it may be a directory, so let it | ||||
# take the slow path. | ||||
if os.path.exists(repo.wjoin(f)): | ||||
slowpath = True | ||||
continue | ||||
else: | ||||
Martin von Zweigbergk
|
r49402 | raise error.StateError( | ||
Yuya Nishihara
|
r46043 | _( | ||
b'cannot follow file not in parent ' | ||||
b'revision: "%s"' | ||||
) | ||||
% f | ||||
) | ||||
filelog = repo.file(f) | ||||
if not filelog: | ||||
Yuya Nishihara
|
r46044 | # A file exists in wdir but not in history, which means | ||
# the file isn't committed yet. | ||||
Martin von Zweigbergk
|
r49402 | raise error.StateError( | ||
Yuya Nishihara
|
r46043 | _(b'cannot follow nonexistent file: "%s"') % f | ||
) | ||||
else: | ||||
for f in match.files(): | ||||
filelog = repo.file(f) | ||||
if not filelog: | ||||
# A zero count may be a directory or deleted file, so | ||||
# try to find matching entries on the slow path. | ||||
slowpath = True | ||||
Yuya Nishihara
|
r35903 | |||
# We decided to fall back to the slowpath because at least one | ||||
# of the paths was not a file. Check to see if at least one of them | ||||
# existed in history - in that case, we'll continue down the | ||||
# slowpath; otherwise, we can turn off the slowpath | ||||
if slowpath: | ||||
for path in match.files(): | ||||
Yuya Nishihara
|
r47352 | if not path or path in repo.store: | ||
Yuya Nishihara
|
r35903 | break | ||
else: | ||||
slowpath = False | ||||
return match, pats, slowpath | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35903 | def _fileancestors(repo, revs, match, followfirst): | ||
fctxs = [] | ||||
for r in revs: | ||||
ctx = repo[r] | ||||
fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match)) | ||||
# When displaying a revision with --patch --follow FILE, we have | ||||
# to know which file of the revision must be diffed. With | ||||
# --follow, we want the names of the ancestors of FILE in the | ||||
# revision, stored in "fcache". "fcache" is populated as a side effect | ||||
# of the graph traversal. | ||||
fcache = {} | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r36019 | def filematcher(ctx): | ||
Yuya Nishihara
|
r46019 | return scmutil.matchfiles(repo, fcache.get(scmutil.intrev(ctx), [])) | ||
Yuya Nishihara
|
r35903 | |||
def revgen(): | ||||
for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst): | ||||
fcache[rev] = [c.path() for c in cs] | ||||
yield rev | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35903 | return smartset.generatorset(revgen(), iterasc=False), filematcher | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35905 | def _makenofollowfilematcher(repo, pats, opts): | ||
Yuya Nishihara
|
r35903 | '''hook for extensions to override the filematcher for non-follow cases''' | ||
return None | ||||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r48930 | def revsingle(repo, revspec, default=b'.', localalias=None): | ||
"""Resolves user-provided revset(s) into a single revision. | ||||
This just wraps the lower-level scmutil.revsingle() in order to raise an | ||||
exception indicating user error. | ||||
""" | ||||
try: | ||||
return scmutil.revsingle(repo, revspec, default, localalias) | ||||
except error.RepoLookupError as e: | ||||
raise error.InputError(e.args[0], hint=e.hint) | ||||
Martin von Zweigbergk
|
r48929 | def revpair(repo, revs): | ||
"""Resolves user-provided revset(s) into two revisions. | ||||
This just wraps the lower-level scmutil.revpair() in order to raise an | ||||
exception indicating user error. | ||||
""" | ||||
try: | ||||
return scmutil.revpair(repo, revs) | ||||
except error.RepoLookupError as e: | ||||
raise error.InputError(e.args[0], hint=e.hint) | ||||
Martin von Zweigbergk
|
r48928 | def revrange(repo, specs, localalias=None): | ||
"""Resolves user-provided revset(s). | ||||
This just wraps the lower-level scmutil.revrange() in order to raise an | ||||
exception indicating user error. | ||||
""" | ||||
try: | ||||
return scmutil.revrange(repo, specs, localalias) | ||||
except error.RepoLookupError as e: | ||||
raise error.InputError(e.args[0], hint=e.hint) | ||||
Yuya Nishihara
|
r35903 | _opt2logrevset = { | ||
Augie Fackler
|
r43347 | b'no_merges': (b'not merge()', None), | ||
b'only_merges': (b'merge()', None), | ||||
b'_matchfiles': (None, b'_matchfiles(%ps)'), | ||||
b'date': (b'date(%s)', None), | ||||
b'branch': (b'branch(%s)', b'%lr'), | ||||
b'_patslog': (b'filelog(%s)', b'%lr'), | ||||
b'keyword': (b'keyword(%s)', b'%lr'), | ||||
b'prune': (b'ancestors(%s)', b'not %lr'), | ||||
b'user': (b'user(%s)', b'%lr'), | ||||
Yuya Nishihara
|
r35903 | } | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r46139 | def _makerevset(repo, wopts, slowpath): | ||
Yuya Nishihara
|
r35903 | """Return a revset string built from log options and file patterns""" | ||
Yuya Nishihara
|
r46201 | opts = { | ||
Yuya Nishihara
|
r46657 | b'branch': [b'literal:' + repo.lookupbranch(b) for b in wopts.branches], | ||
Yuya Nishihara
|
r46201 | b'date': wopts.date, | ||
b'keyword': wopts.keywords, | ||||
b'no_merges': wopts.no_merges, | ||||
b'only_merges': wopts.only_merges, | ||||
b'prune': wopts.prune_ancestors, | ||||
Yuya Nishihara
|
r46657 | b'user': [b'literal:' + v for v in wopts.users], | ||
Yuya Nishihara
|
r46201 | } | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r46224 | if wopts.filter_revisions_by_pats and slowpath: | ||
Yuya Nishihara
|
r35903 | # pats/include/exclude cannot be represented as separate | ||
# revset expressions as their filtering logic applies at file | ||||
# level. For instance "-I a -X b" matches a revision touching | ||||
# "a" and "b" while "file(a) and not file(b)" does | ||||
# not. Besides, filesets are evaluated against the working | ||||
# directory. | ||||
Augie Fackler
|
r43347 | matchargs = [b'r:', b'd:relpath'] | ||
Yuya Nishihara
|
r46139 | for p in wopts.pats: | ||
Augie Fackler
|
r43347 | matchargs.append(b'p:' + p) | ||
Yuya Nishihara
|
r46201 | for p in wopts.include_pats: | ||
Augie Fackler
|
r43347 | matchargs.append(b'i:' + p) | ||
Yuya Nishihara
|
r46201 | for p in wopts.exclude_pats: | ||
Augie Fackler
|
r43347 | matchargs.append(b'x:' + p) | ||
opts[b'_matchfiles'] = matchargs | ||||
Yuya Nishihara
|
r46224 | elif wopts.filter_revisions_by_pats and not wopts.follow: | ||
Yuya Nishihara
|
r46139 | opts[b'_patslog'] = list(wopts.pats) | ||
Yuya Nishihara
|
r35903 | |||
expr = [] | ||||
Gregory Szorc
|
r49768 | for op, val in sorted(opts.items()): | ||
Yuya Nishihara
|
r35903 | if not val: | ||
continue | ||||
revop, listop = _opt2logrevset[op] | ||||
Augie Fackler
|
r43347 | if revop and b'%' not in revop: | ||
Yuya Nishihara
|
r35903 | expr.append(revop) | ||
elif not listop: | ||||
expr.append(revsetlang.formatspec(revop, val)) | ||||
else: | ||||
if revop: | ||||
val = [revsetlang.formatspec(revop, v) for v in val] | ||||
expr.append(revsetlang.formatspec(listop, val)) | ||||
Yuya Nishihara
|
r46656 | if wopts.bookmarks: | ||
expr.append( | ||||
revsetlang.formatspec( | ||||
b'%lr', | ||||
[scmutil.format_bookmark_revspec(v) for v in wopts.bookmarks], | ||||
) | ||||
) | ||||
Yuya Nishihara
|
r35903 | if expr: | ||
Augie Fackler
|
r43347 | expr = b'(' + b' and '.join(expr) + b')' | ||
Yuya Nishihara
|
r35903 | else: | ||
expr = None | ||||
return expr | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r46139 | def _initialrevs(repo, wopts): | ||
Yuya Nishihara
|
r35903 | """Return the initial set of revisions to be filtered or followed""" | ||
Yuya Nishihara
|
r46142 | if wopts.revspec: | ||
Martin von Zweigbergk
|
r48928 | revs = revrange(repo, wopts.revspec) | ||
Joerg Sonnenberger
|
r47771 | elif wopts.follow and repo.dirstate.p1() == repo.nullid: | ||
Yuya Nishihara
|
r35903 | revs = smartset.baseset() | ||
Yuya Nishihara
|
r46140 | elif wopts.follow: | ||
Augie Fackler
|
r43347 | revs = repo.revs(b'.') | ||
Yuya Nishihara
|
r35903 | else: | ||
revs = smartset.spanset(repo) | ||||
revs.reverse() | ||||
return revs | ||||
Augie Fackler
|
r43346 | |||
r52180 | def makewalker( | |||
repo: Any, | ||||
wopts: walkopts, | ||||
) -> Tuple[ | ||||
smartset.abstractsmartset, Optional[Callable[[Any], matchmod.basematcher]] | ||||
]: | ||||
Yuya Nishihara
|
r46203 | """Build (revs, makefilematcher) to scan revision/file history | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r46203 | - revs is the smartset to be traversed. | ||
- makefilematcher is a function to map ctx to a matcher for that revision | ||||
Yuya Nishihara
|
r35903 | """ | ||
Yuya Nishihara
|
r46139 | revs = _initialrevs(repo, wopts) | ||
Yuya Nishihara
|
r35903 | if not revs: | ||
return smartset.baseset(), None | ||||
Yuya Nishihara
|
r46200 | # TODO: might want to merge slowpath with wopts.force_changelog_traversal | ||
Yuya Nishihara
|
r46139 | match, pats, slowpath = _makematcher(repo, revs, wopts) | ||
wopts = attr.evolve(wopts, pats=pats) | ||||
Yuya Nishihara
|
r35903 | filematcher = None | ||
Yuya Nishihara
|
r46140 | if wopts.follow: | ||
Yuya Nishihara
|
r35903 | if slowpath or match.always(): | ||
Yuya Nishihara
|
r46140 | revs = dagop.revancestors(repo, revs, followfirst=wopts.follow == 1) | ||
Yuya Nishihara
|
r35903 | else: | ||
Yuya Nishihara
|
r46200 | assert not wopts.force_changelog_traversal | ||
Yuya Nishihara
|
r46140 | revs, filematcher = _fileancestors( | ||
repo, revs, match, followfirst=wopts.follow == 1 | ||||
) | ||||
Yuya Nishihara
|
r35903 | revs.reverse() | ||
if filematcher is None: | ||||
Yuya Nishihara
|
r46139 | filematcher = _makenofollowfilematcher(repo, wopts.pats, wopts.opts) | ||
Yuya Nishihara
|
r35903 | if filematcher is None: | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r36019 | def filematcher(ctx): | ||
Yuya Nishihara
|
r35903 | return match | ||
Yuya Nishihara
|
r46139 | expr = _makerevset(repo, wopts, slowpath) | ||
Yuya Nishihara
|
r46202 | if wopts.sort_revisions: | ||
assert wopts.sort_revisions in {b'topo', b'desc'} | ||||
if wopts.sort_revisions == b'topo': | ||||
Martin von Zweigbergk
|
r42500 | if not revs.istopo(): | ||
revs = dagop.toposort(revs, repo.changelog.parentrevs) | ||||
# TODO: try to iterate the set lazily | ||||
Yuya Nishihara
|
r42510 | revs = revset.baseset(list(revs), istopo=True) | ||
Martin von Zweigbergk
|
r42500 | elif not (revs.isdescending() or revs.istopo()): | ||
Yuya Nishihara
|
r46137 | # User-specified revs might be unsorted | ||
Yuya Nishihara
|
r35903 | revs.sort(reverse=True) | ||
if expr: | ||||
matcher = revset.match(None, expr) | ||||
revs = matcher(repo, revs) | ||||
Yuya Nishihara
|
r46141 | if wopts.limit is not None: | ||
revs = revs.slice(0, wopts.limit) | ||||
Yuya Nishihara
|
r36024 | |||
Yuya Nishihara
|
r46203 | return revs, filematcher | ||
r52180 | def getrevs( | |||
repo: Any, | ||||
wopts: walkopts, | ||||
) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]: | ||||
Yuya Nishihara
|
r46203 | """Return (revs, differ) where revs is a smartset | ||
differ is a changesetdiffer with pre-configured file matcher. | ||||
""" | ||||
revs, filematcher = makewalker(repo, wopts) | ||||
if not revs: | ||||
return revs, None | ||||
Yuya Nishihara
|
r36024 | differ = changesetdiffer() | ||
differ._makefilematcher = filematcher | ||||
return revs, differ | ||||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35905 | def _parselinerangeopt(repo, opts): | ||
Yuya Nishihara
|
r35903 | """Parse --line-range log option and return a list of tuples (filename, | ||
(fromline, toline)). | ||||
""" | ||||
linerangebyfname = [] | ||||
Augie Fackler
|
r43347 | for pat in opts.get(b'line_range', []): | ||
Yuya Nishihara
|
r35903 | try: | ||
Augie Fackler
|
r43347 | pat, linerange = pat.rsplit(b',', 1) | ||
Yuya Nishihara
|
r35903 | except ValueError: | ||
Martin von Zweigbergk
|
r49401 | raise error.InputError( | ||
_(b'malformatted line-range pattern %s') % pat | ||||
) | ||||
Yuya Nishihara
|
r35903 | try: | ||
Augie Fackler
|
r43347 | fromline, toline = map(int, linerange.split(b':')) | ||
Yuya Nishihara
|
r35903 | except ValueError: | ||
Martin von Zweigbergk
|
r49401 | raise error.InputError(_(b"invalid line range for %s") % pat) | ||
Augie Fackler
|
r43347 | msg = _(b"line range pattern '%s' must match exactly one file") % pat | ||
Yuya Nishihara
|
r35903 | fname = scmutil.parsefollowlinespattern(repo, None, pat, msg) | ||
linerangebyfname.append( | ||||
Augie Fackler
|
r43346 | (fname, util.processlinerange(fromline, toline)) | ||
) | ||||
Yuya Nishihara
|
r35903 | return linerangebyfname | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35905 | def getlinerangerevs(repo, userrevs, opts): | ||
Yuya Nishihara
|
r36024 | """Return (revs, differ). | ||
Yuya Nishihara
|
r35903 | |||
"revs" are revisions obtained by processing "line-range" log options and | ||||
walking block ancestors of each specified file/line-range. | ||||
Yuya Nishihara
|
r36024 | "differ" is a changesetdiffer with pre-configured file matcher and hunks | ||
filter. | ||||
Yuya Nishihara
|
r35903 | """ | ||
wctx = repo[None] | ||||
# Two-levels map of "rev -> file ctx -> [line range]". | ||||
linerangesbyrev = {} | ||||
Yuya Nishihara
|
r35905 | for fname, (fromline, toline) in _parselinerangeopt(repo, opts): | ||
Yuya Nishihara
|
r35903 | if fname not in wctx: | ||
Martin von Zweigbergk
|
r49402 | raise error.StateError( | ||
Martin von Zweigbergk
|
r43387 | _(b'cannot follow file not in parent revision: "%s"') % fname | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | fctx = wctx.filectx(fname) | ||
for fctx, linerange in dagop.blockancestors(fctx, fromline, toline): | ||||
rev = fctx.introrev() | ||||
Denis Laxalde
|
r44173 | if rev is None: | ||
rev = wdirrev | ||||
Yuya Nishihara
|
r35903 | if rev not in userrevs: | ||
continue | ||||
Augie Fackler
|
r43346 | linerangesbyrev.setdefault(rev, {}).setdefault( | ||
fctx.path(), [] | ||||
).append(linerange) | ||||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36022 | def nofilterhunksfn(fctx, hunks): | ||
return hunks | ||||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36022 | def hunksfilter(ctx): | ||
Denis Laxalde
|
r44173 | fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx)) | ||
Yuya Nishihara
|
r36022 | if fctxlineranges is None: | ||
return nofilterhunksfn | ||||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36022 | def filterfn(fctx, hunks): | ||
lineranges = fctxlineranges.get(fctx.path()) | ||||
if lineranges is not None: | ||||
for hr, lines in hunks: | ||||
Augie Fackler
|
r43346 | if hr is None: # binary | ||
Yuya Nishihara
|
r36022 | yield hr, lines | ||
continue | ||||
Augie Fackler
|
r43346 | if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges): | ||
Yuya Nishihara
|
r36022 | yield hr, lines | ||
else: | ||||
for hunk in hunks: | ||||
yield hunk | ||||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36022 | return filterfn | ||
Yuya Nishihara
|
r35903 | |||
Yuya Nishihara
|
r36022 | def filematcher(ctx): | ||
Denis Laxalde
|
r44173 | files = list(linerangesbyrev.get(scmutil.intrev(ctx), [])) | ||
Yuya Nishihara
|
r36022 | return scmutil.matchfiles(repo, files) | ||
Yuya Nishihara
|
r35903 | |||
revs = sorted(linerangesbyrev, reverse=True) | ||||
Yuya Nishihara
|
r36024 | differ = changesetdiffer() | ||
differ._makefilematcher = filematcher | ||||
differ._makehunksfilter = hunksfilter | ||||
Denis Laxalde
|
r44029 | return smartset.baseset(revs), differ | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35903 | def _graphnodeformatter(ui, displayer): | ||
Martin von Zweigbergk
|
r46351 | spec = ui.config(b'command-templates', b'graphnode') | ||
Yuya Nishihara
|
r35903 | if not spec: | ||
Yuya Nishihara
|
r36530 | return templatekw.getgraphnode # fast path for "{graphnode}" | ||
Yuya Nishihara
|
r35903 | |||
spec = templater.unquotestring(spec) | ||||
Yuya Nishihara
|
r35904 | if isinstance(displayer, changesettemplater): | ||
Yuya Nishihara
|
r36997 | # reuse cache of slow templates | ||
Yuya Nishihara
|
r37090 | tres = displayer._tresources | ||
else: | ||||
tres = formatter.templateresources(ui) | ||||
Augie Fackler
|
r43346 | templ = formatter.maketemplater( | ||
ui, spec, defaults=templatekw.keywords, resources=tres | ||||
) | ||||
Martin von Zweigbergk
|
r44819 | def formatnode(repo, ctx, cache): | ||
Augie Fackler
|
r43347 | props = {b'ctx': ctx, b'repo': repo} | ||
Yuya Nishihara
|
r37003 | return templ.renderdefault(props) | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35903 | return formatnode | ||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r42704 | def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None): | ||
Yuya Nishihara
|
r35903 | props = props or {} | ||
formatnode = _graphnodeformatter(ui, displayer) | ||||
state = graphmod.asciistate() | ||||
Yuya Nishihara
|
r44215 | styles = state.styles | ||
Yuya Nishihara
|
r35903 | |||
# only set graph styling if HGPLAIN is not set. | ||||
Augie Fackler
|
r43347 | if ui.plain(b'graph'): | ||
Yuya Nishihara
|
r35903 | # set all edge styles to |, the default pre-3.8 behaviour | ||
Augie Fackler
|
r43347 | styles.update(dict.fromkeys(styles, b'|')) | ||
Yuya Nishihara
|
r35903 | else: | ||
edgetypes = { | ||||
Augie Fackler
|
r43347 | b'parent': graphmod.PARENT, | ||
b'grandparent': graphmod.GRANDPARENT, | ||||
b'missing': graphmod.MISSINGPARENT, | ||||
Yuya Nishihara
|
r35903 | } | ||
for name, key in edgetypes.items(): | ||||
# experimental config: experimental.graphstyle.* | ||||
Augie Fackler
|
r43346 | styles[key] = ui.config( | ||
Augie Fackler
|
r43347 | b'experimental', b'graphstyle.%s' % name, styles[key] | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | if not styles[key]: | ||
styles[key] = None | ||||
# experimental config: experimental.graphshorten | ||||
Yuya Nishihara
|
r44215 | state.graphshorten = ui.configbool(b'experimental', b'graphshorten') | ||
Yuya Nishihara
|
r35903 | |||
Martin von Zweigbergk
|
r44819 | formatnode_cache = {} | ||
Yuya Nishihara
|
r35903 | for rev, type, ctx, parents in dag: | ||
Martin von Zweigbergk
|
r44819 | char = formatnode(repo, ctx, formatnode_cache) | ||
Martin von Zweigbergk
|
r42704 | copies = getcopies(ctx) if getcopies else None | ||
Yuya Nishihara
|
r35903 | edges = edgefn(type, char, state, rev, parents) | ||
firstedge = next(edges) | ||||
width = firstedge[2] | ||||
Augie Fackler
|
r43346 | displayer.show( | ||
ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props) | ||||
) | ||||
Augie Fackler
|
r43347 | lines = displayer.hunk.pop(rev).split(b'\n') | ||
Yuya Nishihara
|
r35903 | if not lines[-1]: | ||
del lines[-1] | ||||
displayer.flush(ctx) | ||||
for type, char, width, coldata in itertools.chain([firstedge], edges): | ||||
graphmod.ascii(ui, state, type, char, lines, coldata) | ||||
lines = [] | ||||
displayer.close() | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r36216 | def displaygraphrevs(ui, repo, revs, displayer, getrenamed): | ||
Yuya Nishihara
|
r35903 | revdag = graphmod.dagwalker(repo, revs) | ||
Yuya Nishihara
|
r36020 | displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed) | ||
Yuya Nishihara
|
r35903 | |||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r42704 | def displayrevs(ui, repo, revs, displayer, getcopies): | ||
Yuya Nishihara
|
r36216 | for rev in revs: | ||
ctx = repo[rev] | ||||
Martin von Zweigbergk
|
r42704 | copies = getcopies(ctx) if getcopies else None | ||
Yuya Nishihara
|
r36216 | displayer.show(ctx, copies=copies) | ||
displayer.flush(ctx) | ||||
displayer.close() | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35903 | def checkunsupportedgraphflags(pats, opts): | ||
Augie Fackler
|
r43347 | for op in [b"newest_first"]: | ||
Yuya Nishihara
|
r35903 | if op in opts and opts[op]: | ||
Martin von Zweigbergk
|
r49401 | raise error.InputError( | ||
Augie Fackler
|
r43347 | _(b"-G/--graph option is incompatible with --%s") | ||
% op.replace(b"_", b"-") | ||||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r35903 | |||
def graphrevs(repo, nodes, opts): | ||||
Yuya Nishihara
|
r35905 | limit = getlimit(opts) | ||
Yuya Nishihara
|
r35903 | nodes.reverse() | ||
if limit is not None: | ||||
nodes = nodes[:limit] | ||||
return graphmod.nodes(repo, nodes) | ||||