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 |