mercurial/cmdutil.py
changeset 35885 7625b4f7db70
parent 35874 1bee7762fd46
child 35886 b0014780c7fc
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