Mercurial > public > mercurial-scm > hg
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 |