comparison mercurial/cmdutil.py @ 35885: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 1bee7762fd46
children b0014780c7fc
comparison
equal deleted inserted replaced
35884:197d10e157ce 35885:7625b4f7db70
6 # GNU General Public License version 2 or any later version. 6 # GNU General Public License version 2 or any later version.
7 7
8 from __future__ import absolute_import 8 from __future__ import absolute_import
9 9
10 import errno 10 import errno
11 import itertools
12 import os 11 import os
13 import re 12 import re
14 import tempfile 13 import tempfile
15 14
16 from .i18n import _ 15 from .i18n import _
24 from . import ( 23 from . import (
25 bookmarks, 24 bookmarks,
26 changelog, 25 changelog,
27 copies, 26 copies,
28 crecord as crecordmod, 27 crecord as crecordmod,
29 dagop,
30 dirstateguard, 28 dirstateguard,
31 encoding, 29 encoding,
32 error, 30 error,
33 formatter, 31 formatter,
34 graphmod, 32 logcmdutil,
35 match as matchmod, 33 match as matchmod,
36 mdiff,
37 obsolete, 34 obsolete,
38 patch, 35 patch,
39 pathutil, 36 pathutil,
40 pycompat, 37 pycompat,
41 registrar, 38 registrar,
42 revlog, 39 revlog,
43 revset,
44 revsetlang,
45 rewriteutil, 40 rewriteutil,
46 scmutil, 41 scmutil,
47 smartset, 42 smartset,
48 templatekw,
49 templater, 43 templater,
50 util, 44 util,
51 vfs as vfsmod, 45 vfs as vfsmod,
52 ) 46 )
53 stringio = util.stringio 47 stringio = util.stringio
48
49 loglimit = logcmdutil.loglimit
50 diffordiffstat = logcmdutil.diffordiffstat
51 _changesetlabels = logcmdutil._changesetlabels
52 changeset_printer = logcmdutil.changeset_printer
53 jsonchangeset = logcmdutil.jsonchangeset
54 changeset_templater = logcmdutil.changeset_templater
55 logtemplatespec = logcmdutil.logtemplatespec
56 makelogtemplater = logcmdutil.makelogtemplater
57 show_changeset = logcmdutil.show_changeset
58 getlogrevs = logcmdutil.getlogrevs
59 getloglinerangerevs = logcmdutil.getloglinerangerevs
60 displaygraph = logcmdutil.displaygraph
61 graphlog = logcmdutil.graphlog
62 checkunsupportedgraphflags = logcmdutil.checkunsupportedgraphflags
63 graphrevs = logcmdutil.graphrevs
54 64
55 # templates of common command options 65 # templates of common command options
56 66
57 dryrunopts = [ 67 dryrunopts = [
58 ('n', 'dry-run', None, 68 ('n', 'dry-run', None,
895 editform=editform) 905 editform=editform)
896 elif editform: 906 elif editform:
897 return lambda r, c, s: commiteditor(r, c, s, editform=editform) 907 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
898 else: 908 else:
899 return commiteditor 909 return commiteditor
900
901 def loglimit(opts):
902 """get the log limit according to option -l/--limit"""
903 limit = opts.get('limit')
904 if limit:
905 try:
906 limit = int(limit)
907 except ValueError:
908 raise error.Abort(_('limit must be a positive integer'))
909 if limit <= 0:
910 raise error.Abort(_('limit must be positive'))
911 else:
912 limit = None
913 return limit
914 910
915 def makefilename(repo, pat, node, desc=None, 911 def makefilename(repo, pat, node, desc=None,
916 total=None, seqno=None, revwidth=None, pathname=None): 912 total=None, seqno=None, revwidth=None, pathname=None):
917 node_expander = { 913 node_expander = {
918 'H': lambda: hex(node), 914 'H': lambda: hex(node),
1581 _exportsingle( 1577 _exportsingle(
1582 repo, ctx, match, switch_parent, rev, seqno, write, opts) 1578 repo, ctx, match, switch_parent, rev, seqno, write, opts)
1583 if fo is not None: 1579 if fo is not None:
1584 fo.close() 1580 fo.close()
1585 1581
1586 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1587 changes=None, stat=False, fp=None, prefix='',
1588 root='', listsubrepos=False, hunksfilterfn=None):
1589 '''show diff or diffstat.'''
1590 if fp is None:
1591 write = ui.write
1592 else:
1593 def write(s, **kw):
1594 fp.write(s)
1595
1596 if root:
1597 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1598 else:
1599 relroot = ''
1600 if relroot != '':
1601 # XXX relative roots currently don't work if the root is within a
1602 # subrepo
1603 uirelroot = match.uipath(relroot)
1604 relroot += '/'
1605 for matchroot in match.files():
1606 if not matchroot.startswith(relroot):
1607 ui.warn(_('warning: %s not inside relative root %s\n') % (
1608 match.uipath(matchroot), uirelroot))
1609
1610 if stat:
1611 diffopts = diffopts.copy(context=0, noprefix=False)
1612 width = 80
1613 if not ui.plain():
1614 width = ui.termwidth()
1615 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
1616 prefix=prefix, relroot=relroot,
1617 hunksfilterfn=hunksfilterfn)
1618 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1619 width=width):
1620 write(chunk, label=label)
1621 else:
1622 for chunk, label in patch.diffui(repo, node1, node2, match,
1623 changes, opts=diffopts, prefix=prefix,
1624 relroot=relroot,
1625 hunksfilterfn=hunksfilterfn):
1626 write(chunk, label=label)
1627
1628 if listsubrepos:
1629 ctx1 = repo[node1]
1630 ctx2 = repo[node2]
1631 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1632 tempnode2 = node2
1633 try:
1634 if node2 is not None:
1635 tempnode2 = ctx2.substate[subpath][1]
1636 except KeyError:
1637 # A subrepo that existed in node1 was deleted between node1 and
1638 # node2 (inclusive). Thus, ctx2's substate won't contain that
1639 # subpath. The best we can do is to ignore it.
1640 tempnode2 = None
1641 submatch = matchmod.subdirmatcher(subpath, match)
1642 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1643 stat=stat, fp=fp, prefix=prefix)
1644
1645 def _changesetlabels(ctx):
1646 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1647 if ctx.obsolete():
1648 labels.append('changeset.obsolete')
1649 if ctx.isunstable():
1650 labels.append('changeset.unstable')
1651 for instability in ctx.instabilities():
1652 labels.append('instability.%s' % instability)
1653 return ' '.join(labels)
1654
1655 class changeset_printer(object):
1656 '''show changeset information when templating not requested.'''
1657
1658 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1659 self.ui = ui
1660 self.repo = repo
1661 self.buffered = buffered
1662 self.matchfn = matchfn
1663 self.diffopts = diffopts
1664 self.header = {}
1665 self.hunk = {}
1666 self.lastheader = None
1667 self.footer = None
1668 self._columns = templatekw.getlogcolumns()
1669
1670 def flush(self, ctx):
1671 rev = ctx.rev()
1672 if rev in self.header:
1673 h = self.header[rev]
1674 if h != self.lastheader:
1675 self.lastheader = h
1676 self.ui.write(h)
1677 del self.header[rev]
1678 if rev in self.hunk:
1679 self.ui.write(self.hunk[rev])
1680 del self.hunk[rev]
1681
1682 def close(self):
1683 if self.footer:
1684 self.ui.write(self.footer)
1685
1686 def show(self, ctx, copies=None, matchfn=None, hunksfilterfn=None,
1687 **props):
1688 props = pycompat.byteskwargs(props)
1689 if self.buffered:
1690 self.ui.pushbuffer(labeled=True)
1691 self._show(ctx, copies, matchfn, hunksfilterfn, props)
1692 self.hunk[ctx.rev()] = self.ui.popbuffer()
1693 else:
1694 self._show(ctx, copies, matchfn, hunksfilterfn, props)
1695
1696 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
1697 '''show a single changeset or file revision'''
1698 changenode = ctx.node()
1699 rev = ctx.rev()
1700
1701 if self.ui.quiet:
1702 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
1703 label='log.node')
1704 return
1705
1706 columns = self._columns
1707 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
1708 label=_changesetlabels(ctx))
1709
1710 # branches are shown first before any other names due to backwards
1711 # compatibility
1712 branch = ctx.branch()
1713 # don't show the default branch name
1714 if branch != 'default':
1715 self.ui.write(columns['branch'] % branch, label='log.branch')
1716
1717 for nsname, ns in self.repo.names.iteritems():
1718 # branches has special logic already handled above, so here we just
1719 # skip it
1720 if nsname == 'branches':
1721 continue
1722 # we will use the templatename as the color name since those two
1723 # should be the same
1724 for name in ns.names(self.repo, changenode):
1725 self.ui.write(ns.logfmt % name,
1726 label='log.%s' % ns.colorname)
1727 if self.ui.debugflag:
1728 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
1729 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1730 label = 'log.parent changeset.%s' % pctx.phasestr()
1731 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
1732 label=label)
1733
1734 if self.ui.debugflag and rev is not None:
1735 mnode = ctx.manifestnode()
1736 mrev = self.repo.manifestlog._revlog.rev(mnode)
1737 self.ui.write(columns['manifest']
1738 % scmutil.formatrevnode(self.ui, mrev, mnode),
1739 label='ui.debug log.manifest')
1740 self.ui.write(columns['user'] % ctx.user(), label='log.user')
1741 self.ui.write(columns['date'] % util.datestr(ctx.date()),
1742 label='log.date')
1743
1744 if ctx.isunstable():
1745 instabilities = ctx.instabilities()
1746 self.ui.write(columns['instability'] % ', '.join(instabilities),
1747 label='log.instability')
1748
1749 elif ctx.obsolete():
1750 self._showobsfate(ctx)
1751
1752 self._exthook(ctx)
1753
1754 if self.ui.debugflag:
1755 files = ctx.p1().status(ctx)[:3]
1756 for key, value in zip(['files', 'files+', 'files-'], files):
1757 if value:
1758 self.ui.write(columns[key] % " ".join(value),
1759 label='ui.debug log.files')
1760 elif ctx.files() and self.ui.verbose:
1761 self.ui.write(columns['files'] % " ".join(ctx.files()),
1762 label='ui.note log.files')
1763 if copies and self.ui.verbose:
1764 copies = ['%s (%s)' % c for c in copies]
1765 self.ui.write(columns['copies'] % ' '.join(copies),
1766 label='ui.note log.copies')
1767
1768 extra = ctx.extra()
1769 if extra and self.ui.debugflag:
1770 for key, value in sorted(extra.items()):
1771 self.ui.write(columns['extra'] % (key, util.escapestr(value)),
1772 label='ui.debug log.extra')
1773
1774 description = ctx.description().strip()
1775 if description:
1776 if self.ui.verbose:
1777 self.ui.write(_("description:\n"),
1778 label='ui.note log.description')
1779 self.ui.write(description,
1780 label='ui.note log.description')
1781 self.ui.write("\n\n")
1782 else:
1783 self.ui.write(columns['summary'] % description.splitlines()[0],
1784 label='log.summary')
1785 self.ui.write("\n")
1786
1787 self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
1788
1789 def _showobsfate(self, ctx):
1790 obsfate = templatekw.showobsfate(repo=self.repo, ctx=ctx, ui=self.ui)
1791
1792 if obsfate:
1793 for obsfateline in obsfate:
1794 self.ui.write(self._columns['obsolete'] % obsfateline,
1795 label='log.obsfate')
1796
1797 def _exthook(self, ctx):
1798 '''empty method used by extension as a hook point
1799 '''
1800
1801 def showpatch(self, ctx, matchfn, hunksfilterfn=None):
1802 if not matchfn:
1803 matchfn = self.matchfn
1804 if matchfn:
1805 stat = self.diffopts.get('stat')
1806 diff = self.diffopts.get('patch')
1807 diffopts = patch.diffallopts(self.ui, self.diffopts)
1808 node = ctx.node()
1809 prev = ctx.p1().node()
1810 if stat:
1811 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1812 match=matchfn, stat=True,
1813 hunksfilterfn=hunksfilterfn)
1814 if diff:
1815 if stat:
1816 self.ui.write("\n")
1817 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1818 match=matchfn, stat=False,
1819 hunksfilterfn=hunksfilterfn)
1820 if stat or diff:
1821 self.ui.write("\n")
1822
1823 class jsonchangeset(changeset_printer):
1824 '''format changeset information.'''
1825
1826 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1827 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1828 self.cache = {}
1829 self._first = True
1830
1831 def close(self):
1832 if not self._first:
1833 self.ui.write("\n]\n")
1834 else:
1835 self.ui.write("[]\n")
1836
1837 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
1838 '''show a single changeset or file revision'''
1839 rev = ctx.rev()
1840 if rev is None:
1841 jrev = jnode = 'null'
1842 else:
1843 jrev = '%d' % rev
1844 jnode = '"%s"' % hex(ctx.node())
1845 j = encoding.jsonescape
1846
1847 if self._first:
1848 self.ui.write("[\n {")
1849 self._first = False
1850 else:
1851 self.ui.write(",\n {")
1852
1853 if self.ui.quiet:
1854 self.ui.write(('\n "rev": %s') % jrev)
1855 self.ui.write((',\n "node": %s') % jnode)
1856 self.ui.write('\n }')
1857 return
1858
1859 self.ui.write(('\n "rev": %s') % jrev)
1860 self.ui.write((',\n "node": %s') % jnode)
1861 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1862 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1863 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1864 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1865 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1866
1867 self.ui.write((',\n "bookmarks": [%s]') %
1868 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1869 self.ui.write((',\n "tags": [%s]') %
1870 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1871 self.ui.write((',\n "parents": [%s]') %
1872 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1873
1874 if self.ui.debugflag:
1875 if rev is None:
1876 jmanifestnode = 'null'
1877 else:
1878 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1879 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1880
1881 self.ui.write((',\n "extra": {%s}') %
1882 ", ".join('"%s": "%s"' % (j(k), j(v))
1883 for k, v in ctx.extra().items()))
1884
1885 files = ctx.p1().status(ctx)
1886 self.ui.write((',\n "modified": [%s]') %
1887 ", ".join('"%s"' % j(f) for f in files[0]))
1888 self.ui.write((',\n "added": [%s]') %
1889 ", ".join('"%s"' % j(f) for f in files[1]))
1890 self.ui.write((',\n "removed": [%s]') %
1891 ", ".join('"%s"' % j(f) for f in files[2]))
1892
1893 elif self.ui.verbose:
1894 self.ui.write((',\n "files": [%s]') %
1895 ", ".join('"%s"' % j(f) for f in ctx.files()))
1896
1897 if copies:
1898 self.ui.write((',\n "copies": {%s}') %
1899 ", ".join('"%s": "%s"' % (j(k), j(v))
1900 for k, v in copies))
1901
1902 matchfn = self.matchfn
1903 if matchfn:
1904 stat = self.diffopts.get('stat')
1905 diff = self.diffopts.get('patch')
1906 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1907 node, prev = ctx.node(), ctx.p1().node()
1908 if stat:
1909 self.ui.pushbuffer()
1910 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1911 match=matchfn, stat=True)
1912 self.ui.write((',\n "diffstat": "%s"')
1913 % j(self.ui.popbuffer()))
1914 if diff:
1915 self.ui.pushbuffer()
1916 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1917 match=matchfn, stat=False)
1918 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1919
1920 self.ui.write("\n }")
1921
1922 class changeset_templater(changeset_printer):
1923 '''format changeset information.
1924
1925 Note: there are a variety of convenience functions to build a
1926 changeset_templater for common cases. See functions such as:
1927 makelogtemplater, show_changeset, buildcommittemplate, or other
1928 functions that use changesest_templater.
1929 '''
1930
1931 # Arguments before "buffered" used to be positional. Consider not
1932 # adding/removing arguments before "buffered" to not break callers.
1933 def __init__(self, ui, repo, tmplspec, matchfn=None, diffopts=None,
1934 buffered=False):
1935 diffopts = diffopts or {}
1936
1937 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1938 tres = formatter.templateresources(ui, repo)
1939 self.t = formatter.loadtemplater(ui, tmplspec,
1940 defaults=templatekw.keywords,
1941 resources=tres,
1942 cache=templatekw.defaulttempl)
1943 self._counter = itertools.count()
1944 self.cache = tres['cache'] # shared with _graphnodeformatter()
1945
1946 self._tref = tmplspec.ref
1947 self._parts = {'header': '', 'footer': '',
1948 tmplspec.ref: tmplspec.ref,
1949 'docheader': '', 'docfooter': '',
1950 'separator': ''}
1951 if tmplspec.mapfile:
1952 # find correct templates for current mode, for backward
1953 # compatibility with 'log -v/-q/--debug' using a mapfile
1954 tmplmodes = [
1955 (True, ''),
1956 (self.ui.verbose, '_verbose'),
1957 (self.ui.quiet, '_quiet'),
1958 (self.ui.debugflag, '_debug'),
1959 ]
1960 for mode, postfix in tmplmodes:
1961 for t in self._parts:
1962 cur = t + postfix
1963 if mode and cur in self.t:
1964 self._parts[t] = cur
1965 else:
1966 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
1967 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
1968 self._parts.update(m)
1969
1970 if self._parts['docheader']:
1971 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1972
1973 def close(self):
1974 if self._parts['docfooter']:
1975 if not self.footer:
1976 self.footer = ""
1977 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1978 return super(changeset_templater, self).close()
1979
1980 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
1981 '''show a single changeset or file revision'''
1982 props = props.copy()
1983 props['ctx'] = ctx
1984 props['index'] = index = next(self._counter)
1985 props['revcache'] = {'copies': copies}
1986 props = pycompat.strkwargs(props)
1987
1988 # write separator, which wouldn't work well with the header part below
1989 # since there's inherently a conflict between header (across items) and
1990 # separator (per item)
1991 if self._parts['separator'] and index > 0:
1992 self.ui.write(templater.stringify(self.t(self._parts['separator'])))
1993
1994 # write header
1995 if self._parts['header']:
1996 h = templater.stringify(self.t(self._parts['header'], **props))
1997 if self.buffered:
1998 self.header[ctx.rev()] = h
1999 else:
2000 if self.lastheader != h:
2001 self.lastheader = h
2002 self.ui.write(h)
2003
2004 # write changeset metadata, then patch if requested
2005 key = self._parts[self._tref]
2006 self.ui.write(templater.stringify(self.t(key, **props)))
2007 self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
2008
2009 if self._parts['footer']:
2010 if not self.footer:
2011 self.footer = templater.stringify(
2012 self.t(self._parts['footer'], **props))
2013
2014 def logtemplatespec(tmpl, mapfile):
2015 if mapfile:
2016 return formatter.templatespec('changeset', tmpl, mapfile)
2017 else:
2018 return formatter.templatespec('', tmpl, None)
2019
2020 def _lookuplogtemplate(ui, tmpl, style):
2021 """Find the template matching the given template spec or style
2022
2023 See formatter.lookuptemplate() for details.
2024 """
2025
2026 # ui settings
2027 if not tmpl and not style: # template are stronger than style
2028 tmpl = ui.config('ui', 'logtemplate')
2029 if tmpl:
2030 return logtemplatespec(templater.unquotestring(tmpl), None)
2031 else:
2032 style = util.expandpath(ui.config('ui', 'style'))
2033
2034 if not tmpl and style:
2035 mapfile = style
2036 if not os.path.split(mapfile)[0]:
2037 mapname = (templater.templatepath('map-cmdline.' + mapfile)
2038 or templater.templatepath(mapfile))
2039 if mapname:
2040 mapfile = mapname
2041 return logtemplatespec(None, mapfile)
2042
2043 if not tmpl:
2044 return logtemplatespec(None, None)
2045
2046 return formatter.lookuptemplate(ui, 'changeset', tmpl)
2047
2048 def makelogtemplater(ui, repo, tmpl, buffered=False):
2049 """Create a changeset_templater from a literal template 'tmpl'
2050 byte-string."""
2051 spec = logtemplatespec(tmpl, None)
2052 return changeset_templater(ui, repo, spec, buffered=buffered)
2053
2054 def show_changeset(ui, repo, opts, buffered=False):
2055 """show one changeset using template or regular display.
2056
2057 Display format will be the first non-empty hit of:
2058 1. option 'template'
2059 2. option 'style'
2060 3. [ui] setting 'logtemplate'
2061 4. [ui] setting 'style'
2062 If all of these values are either the unset or the empty string,
2063 regular display via changeset_printer() is done.
2064 """
2065 # options
2066 match = None
2067 if opts.get('patch') or opts.get('stat'):
2068 match = scmutil.matchall(repo)
2069
2070 if opts.get('template') == 'json':
2071 return jsonchangeset(ui, repo, match, opts, buffered)
2072
2073 spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
2074
2075 if not spec.ref and not spec.tmpl and not spec.mapfile:
2076 return changeset_printer(ui, repo, match, opts, buffered)
2077
2078 return changeset_templater(ui, repo, spec, match, opts, buffered)
2079
2080 class _regrettablereprbytes(bytes): 1582 class _regrettablereprbytes(bytes):
2081 """Bytes subclass that makes the repr the same on Python 3 as Python 2. 1583 """Bytes subclass that makes the repr the same on Python 3 as Python 2.
2082 1584
2083 This is a huge hack. 1585 This is a huge hack.
2084 """ 1586 """
2426 1928
2427 if stopiteration: 1929 if stopiteration:
2428 break 1930 break
2429 1931
2430 return iterate() 1932 return iterate()
2431
2432 def _makelogmatcher(repo, revs, pats, opts):
2433 """Build matcher and expanded patterns from log options
2434
2435 If --follow, revs are the revisions to follow from.
2436
2437 Returns (match, pats, slowpath) where
2438 - match: a matcher built from the given pats and -I/-X opts
2439 - pats: patterns used (globs are expanded on Windows)
2440 - slowpath: True if patterns aren't as simple as scanning filelogs
2441 """
2442 # pats/include/exclude are passed to match.match() directly in
2443 # _matchfiles() revset but walkchangerevs() builds its matcher with
2444 # scmutil.match(). The difference is input pats are globbed on
2445 # platforms without shell expansion (windows).
2446 wctx = repo[None]
2447 match, pats = scmutil.matchandpats(wctx, pats, opts)
2448 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
2449 if not slowpath:
2450 follow = opts.get('follow') or opts.get('follow_first')
2451 startctxs = []
2452 if follow and opts.get('rev'):
2453 startctxs = [repo[r] for r in revs]
2454 for f in match.files():
2455 if follow and startctxs:
2456 # No idea if the path was a directory at that revision, so
2457 # take the slow path.
2458 if any(f not in c for c in startctxs):
2459 slowpath = True
2460 continue
2461 elif follow and f not in wctx:
2462 # If the file exists, it may be a directory, so let it
2463 # take the slow path.
2464 if os.path.exists(repo.wjoin(f)):
2465 slowpath = True
2466 continue
2467 else:
2468 raise error.Abort(_('cannot follow file not in parent '
2469 'revision: "%s"') % f)
2470 filelog = repo.file(f)
2471 if not filelog:
2472 # A zero count may be a directory or deleted file, so
2473 # try to find matching entries on the slow path.
2474 if follow:
2475 raise error.Abort(
2476 _('cannot follow nonexistent file: "%s"') % f)
2477 slowpath = True
2478
2479 # We decided to fall back to the slowpath because at least one
2480 # of the paths was not a file. Check to see if at least one of them
2481 # existed in history - in that case, we'll continue down the
2482 # slowpath; otherwise, we can turn off the slowpath
2483 if slowpath:
2484 for path in match.files():
2485 if path == '.' or path in repo.store:
2486 break
2487 else:
2488 slowpath = False
2489
2490 return match, pats, slowpath
2491
2492 def _fileancestors(repo, revs, match, followfirst):
2493 fctxs = []
2494 for r in revs:
2495 ctx = repo[r]
2496 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
2497
2498 # When displaying a revision with --patch --follow FILE, we have
2499 # to know which file of the revision must be diffed. With
2500 # --follow, we want the names of the ancestors of FILE in the
2501 # revision, stored in "fcache". "fcache" is populated as a side effect
2502 # of the graph traversal.
2503 fcache = {}
2504 def filematcher(rev):
2505 return scmutil.matchfiles(repo, fcache.get(rev, []))
2506
2507 def revgen():
2508 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
2509 fcache[rev] = [c.path() for c in cs]
2510 yield rev
2511 return smartset.generatorset(revgen(), iterasc=False), filematcher
2512
2513 def _makenofollowlogfilematcher(repo, pats, opts):
2514 '''hook for extensions to override the filematcher for non-follow cases'''
2515 return None
2516
2517 _opt2logrevset = {
2518 'no_merges': ('not merge()', None),
2519 'only_merges': ('merge()', None),
2520 '_matchfiles': (None, '_matchfiles(%ps)'),
2521 'date': ('date(%s)', None),
2522 'branch': ('branch(%s)', '%lr'),
2523 '_patslog': ('filelog(%s)', '%lr'),
2524 'keyword': ('keyword(%s)', '%lr'),
2525 'prune': ('ancestors(%s)', 'not %lr'),
2526 'user': ('user(%s)', '%lr'),
2527 }
2528
2529 def _makelogrevset(repo, match, pats, slowpath, opts):
2530 """Return a revset string built from log options and file patterns"""
2531 opts = dict(opts)
2532 # follow or not follow?
2533 follow = opts.get('follow') or opts.get('follow_first')
2534
2535 # branch and only_branch are really aliases and must be handled at
2536 # the same time
2537 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
2538 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
2539
2540 if slowpath:
2541 # See walkchangerevs() slow path.
2542 #
2543 # pats/include/exclude cannot be represented as separate
2544 # revset expressions as their filtering logic applies at file
2545 # level. For instance "-I a -X b" matches a revision touching
2546 # "a" and "b" while "file(a) and not file(b)" does
2547 # not. Besides, filesets are evaluated against the working
2548 # directory.
2549 matchargs = ['r:', 'd:relpath']
2550 for p in pats:
2551 matchargs.append('p:' + p)
2552 for p in opts.get('include', []):
2553 matchargs.append('i:' + p)
2554 for p in opts.get('exclude', []):
2555 matchargs.append('x:' + p)
2556 opts['_matchfiles'] = matchargs
2557 elif not follow:
2558 opts['_patslog'] = list(pats)
2559
2560 expr = []
2561 for op, val in sorted(opts.iteritems()):
2562 if not val:
2563 continue
2564 if op not in _opt2logrevset:
2565 continue
2566 revop, listop = _opt2logrevset[op]
2567 if revop and '%' not in revop:
2568 expr.append(revop)
2569 elif not listop:
2570 expr.append(revsetlang.formatspec(revop, val))
2571 else:
2572 if revop:
2573 val = [revsetlang.formatspec(revop, v) for v in val]
2574 expr.append(revsetlang.formatspec(listop, val))
2575
2576 if expr:
2577 expr = '(' + ' and '.join(expr) + ')'
2578 else:
2579 expr = None
2580 return expr
2581
2582 def _logrevs(repo, opts):
2583 """Return the initial set of revisions to be filtered or followed"""
2584 follow = opts.get('follow') or opts.get('follow_first')
2585 if opts.get('rev'):
2586 revs = scmutil.revrange(repo, opts['rev'])
2587 elif follow and repo.dirstate.p1() == nullid:
2588 revs = smartset.baseset()
2589 elif follow:
2590 revs = repo.revs('.')
2591 else:
2592 revs = smartset.spanset(repo)
2593 revs.reverse()
2594 return revs
2595
2596 def getlogrevs(repo, pats, opts):
2597 """Return (revs, filematcher) where revs is a smartset
2598
2599 filematcher is a callable taking a revision number and returning a match
2600 objects filtering the files to be detailed when displaying the revision.
2601 """
2602 follow = opts.get('follow') or opts.get('follow_first')
2603 followfirst = opts.get('follow_first')
2604 limit = loglimit(opts)
2605 revs = _logrevs(repo, opts)
2606 if not revs:
2607 return smartset.baseset(), None
2608 match, pats, slowpath = _makelogmatcher(repo, revs, pats, opts)
2609 filematcher = None
2610 if follow:
2611 if slowpath or match.always():
2612 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
2613 else:
2614 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
2615 revs.reverse()
2616 if filematcher is None:
2617 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2618 if filematcher is None:
2619 def filematcher(rev):
2620 return match
2621
2622 expr = _makelogrevset(repo, match, pats, slowpath, opts)
2623 if opts.get('graph') and opts.get('rev'):
2624 # User-specified revs might be unsorted, but don't sort before
2625 # _makelogrevset because it might depend on the order of revs
2626 if not (revs.isdescending() or revs.istopo()):
2627 revs.sort(reverse=True)
2628 if expr:
2629 matcher = revset.match(None, expr)
2630 revs = matcher(repo, revs)
2631 if limit is not None:
2632 revs = revs.slice(0, limit)
2633 return revs, filematcher
2634
2635 def _parselinerangelogopt(repo, opts):
2636 """Parse --line-range log option and return a list of tuples (filename,
2637 (fromline, toline)).
2638 """
2639 linerangebyfname = []
2640 for pat in opts.get('line_range', []):
2641 try:
2642 pat, linerange = pat.rsplit(',', 1)
2643 except ValueError:
2644 raise error.Abort(_('malformatted line-range pattern %s') % pat)
2645 try:
2646 fromline, toline = map(int, linerange.split(':'))
2647 except ValueError:
2648 raise error.Abort(_("invalid line range for %s") % pat)
2649 msg = _("line range pattern '%s' must match exactly one file") % pat
2650 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
2651 linerangebyfname.append(
2652 (fname, util.processlinerange(fromline, toline)))
2653 return linerangebyfname
2654
2655 def getloglinerangerevs(repo, userrevs, opts):
2656 """Return (revs, filematcher, hunksfilter).
2657
2658 "revs" are revisions obtained by processing "line-range" log options and
2659 walking block ancestors of each specified file/line-range.
2660
2661 "filematcher(rev) -> match" is a factory function returning a match object
2662 for a given revision for file patterns specified in --line-range option.
2663 If neither --stat nor --patch options are passed, "filematcher" is None.
2664
2665 "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
2666 returning a hunks filtering function.
2667 If neither --stat nor --patch options are passed, "filterhunks" is None.
2668 """
2669 wctx = repo[None]
2670
2671 # Two-levels map of "rev -> file ctx -> [line range]".
2672 linerangesbyrev = {}
2673 for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
2674 if fname not in wctx:
2675 raise error.Abort(_('cannot follow file not in parent '
2676 'revision: "%s"') % fname)
2677 fctx = wctx.filectx(fname)
2678 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
2679 rev = fctx.introrev()
2680 if rev not in userrevs:
2681 continue
2682 linerangesbyrev.setdefault(
2683 rev, {}).setdefault(
2684 fctx.path(), []).append(linerange)
2685
2686 filematcher = None
2687 hunksfilter = None
2688 if opts.get('patch') or opts.get('stat'):
2689
2690 def nofilterhunksfn(fctx, hunks):
2691 return hunks
2692
2693 def hunksfilter(rev):
2694 fctxlineranges = linerangesbyrev.get(rev)
2695 if fctxlineranges is None:
2696 return nofilterhunksfn
2697
2698 def filterfn(fctx, hunks):
2699 lineranges = fctxlineranges.get(fctx.path())
2700 if lineranges is not None:
2701 for hr, lines in hunks:
2702 if hr is None: # binary
2703 yield hr, lines
2704 continue
2705 if any(mdiff.hunkinrange(hr[2:], lr)
2706 for lr in lineranges):
2707 yield hr, lines
2708 else:
2709 for hunk in hunks:
2710 yield hunk
2711
2712 return filterfn
2713
2714 def filematcher(rev):
2715 files = list(linerangesbyrev.get(rev, []))
2716 return scmutil.matchfiles(repo, files)
2717
2718 revs = sorted(linerangesbyrev, reverse=True)
2719
2720 return revs, filematcher, hunksfilter
2721
2722 def _graphnodeformatter(ui, displayer):
2723 spec = ui.config('ui', 'graphnodetemplate')
2724 if not spec:
2725 return templatekw.showgraphnode # fast path for "{graphnode}"
2726
2727 spec = templater.unquotestring(spec)
2728 tres = formatter.templateresources(ui)
2729 if isinstance(displayer, changeset_templater):
2730 tres['cache'] = displayer.cache # reuse cache of slow templates
2731 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
2732 resources=tres)
2733 def formatnode(repo, ctx):
2734 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
2735 return templ.render(props)
2736 return formatnode
2737
2738 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2739 filematcher=None, props=None):
2740 props = props or {}
2741 formatnode = _graphnodeformatter(ui, displayer)
2742 state = graphmod.asciistate()
2743 styles = state['styles']
2744
2745 # only set graph styling if HGPLAIN is not set.
2746 if ui.plain('graph'):
2747 # set all edge styles to |, the default pre-3.8 behaviour
2748 styles.update(dict.fromkeys(styles, '|'))
2749 else:
2750 edgetypes = {
2751 'parent': graphmod.PARENT,
2752 'grandparent': graphmod.GRANDPARENT,
2753 'missing': graphmod.MISSINGPARENT
2754 }
2755 for name, key in edgetypes.items():
2756 # experimental config: experimental.graphstyle.*
2757 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2758 styles[key])
2759 if not styles[key]:
2760 styles[key] = None
2761
2762 # experimental config: experimental.graphshorten
2763 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2764
2765 for rev, type, ctx, parents in dag:
2766 char = formatnode(repo, ctx)
2767 copies = None
2768 if getrenamed and ctx.rev():
2769 copies = []
2770 for fn in ctx.files():
2771 rename = getrenamed(fn, ctx.rev())
2772 if rename:
2773 copies.append((fn, rename[0]))
2774 revmatchfn = None
2775 if filematcher is not None:
2776 revmatchfn = filematcher(ctx.rev())
2777 edges = edgefn(type, char, state, rev, parents)
2778 firstedge = next(edges)
2779 width = firstedge[2]
2780 displayer.show(ctx, copies=copies, matchfn=revmatchfn,
2781 _graphwidth=width, **pycompat.strkwargs(props))
2782 lines = displayer.hunk.pop(rev).split('\n')
2783 if not lines[-1]:
2784 del lines[-1]
2785 displayer.flush(ctx)
2786 for type, char, width, coldata in itertools.chain([firstedge], edges):
2787 graphmod.ascii(ui, state, type, char, lines, coldata)
2788 lines = []
2789 displayer.close()
2790
2791 def graphlog(ui, repo, revs, filematcher, opts):
2792 # Parameters are identical to log command ones
2793 revdag = graphmod.dagwalker(repo, revs)
2794
2795 getrenamed = None
2796 if opts.get('copies'):
2797 endrev = None
2798 if opts.get('rev'):
2799 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2800 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2801
2802 ui.pager('log')
2803 displayer = show_changeset(ui, repo, opts, buffered=True)
2804 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2805 filematcher)
2806
2807 def checkunsupportedgraphflags(pats, opts):
2808 for op in ["newest_first"]:
2809 if op in opts and opts[op]:
2810 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2811 % op.replace("_", "-"))
2812
2813 def graphrevs(repo, nodes, opts):
2814 limit = loglimit(opts)
2815 nodes.reverse()
2816 if limit is not None:
2817 nodes = nodes[:limit]
2818 return graphmod.nodes(repo, nodes)
2819 1933
2820 def add(ui, repo, match, prefix, explicitonly, **opts): 1934 def add(ui, repo, match, prefix, explicitonly, **opts):
2821 join = lambda f: os.path.join(prefix, f) 1935 join = lambda f: os.path.join(prefix, f)
2822 bad = [] 1936 bad = []
2823 1937