##// END OF EJS Templates
cmdutil: split functions of log-like commands to new module (API)...
Yuya Nishihara -
r35903:7625b4f7 default
parent child Browse files
Show More
@@ -19,6 +19,7 b' from mercurial import ('
19 cmdutil,
19 cmdutil,
20 error,
20 error,
21 hg,
21 hg,
22 logcmdutil,
22 match as matchmod,
23 match as matchmod,
23 pathutil,
24 pathutil,
24 pycompat,
25 pycompat,
@@ -394,14 +395,16 b' def overridelog(orig, ui, repo, *pats, *'
394 return lambda rev: match
395 return lambda rev: match
395
396
396 oldmatchandpats = installmatchandpatsfn(overridematchandpats)
397 oldmatchandpats = installmatchandpatsfn(overridematchandpats)
397 oldmakelogfilematcher = cmdutil._makenofollowlogfilematcher
398 oldmakelogfilematcher = logcmdutil._makenofollowlogfilematcher
398 setattr(cmdutil, '_makenofollowlogfilematcher', overridemakelogfilematcher)
399 setattr(logcmdutil, '_makenofollowlogfilematcher',
400 overridemakelogfilematcher)
399
401
400 try:
402 try:
401 return orig(ui, repo, *pats, **opts)
403 return orig(ui, repo, *pats, **opts)
402 finally:
404 finally:
403 restorematchandpatsfn()
405 restorematchandpatsfn()
404 setattr(cmdutil, '_makenofollowlogfilematcher', oldmakelogfilematcher)
406 setattr(logcmdutil, '_makenofollowlogfilematcher',
407 oldmakelogfilematcher)
405
408
406 def overrideverify(orig, ui, repo, *pats, **opts):
409 def overrideverify(orig, ui, repo, *pats, **opts):
407 large = opts.pop(r'large', False)
410 large = opts.pop(r'large', False)
@@ -75,12 +75,12 b' from __future__ import absolute_import'
75
75
76 from mercurial.i18n import _
76 from mercurial.i18n import _
77 from mercurial import (
77 from mercurial import (
78 cmdutil,
79 commands,
78 commands,
80 dirstate,
79 dirstate,
81 error,
80 error,
82 extensions,
81 extensions,
83 hg,
82 hg,
83 logcmdutil,
84 match as matchmod,
84 match as matchmod,
85 pycompat,
85 pycompat,
86 registrar,
86 registrar,
@@ -135,7 +135,7 b' def _setuplog(ui):'
135 return any(f for f in ctx.files() if sparsematch(f))
135 return any(f for f in ctx.files() if sparsematch(f))
136 revs = revs.filter(ctxmatch)
136 revs = revs.filter(ctxmatch)
137 return revs
137 return revs
138 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
138 extensions.wrapfunction(logcmdutil, '_logrevs', _logrevs)
139
139
140 def _clonesparsecmd(orig, ui, repo, *args, **opts):
140 def _clonesparsecmd(orig, ui, repo, *args, **opts):
141 include_pat = opts.get('include')
141 include_pat = opts.get('include')
This diff has been collapsed as it changes many lines, (920 lines changed) Show them Hide them
@@ -8,7 +8,6 b''
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
@@ -26,32 +25,43 b' from . import ('
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
54
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
64
55 # templates of common command options
65 # templates of common command options
56
66
57 dryrunopts = [
67 dryrunopts = [
@@ -898,20 +908,6 b' def getcommiteditor(edit=False, finishde'
898 else:
908 else:
899 return commiteditor
909 return commiteditor
900
910
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
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 = {
@@ -1583,500 +1579,6 b" def export(repo, revs, fntemplate='hg-%h"
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
@@ -2429,394 +1931,6 b' def walkchangerevs(repo, match, opts, pr'
2429
1931
2430 return iterate()
1932 return iterate()
2431
1933
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
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 = []
This diff has been collapsed as it changes many lines, (3110 lines changed) Show them Hide them
@@ -1,4 +1,4 b''
1 # cmdutil.py - help for command processing in mercurial
1 # logcmdutil.py - utility for log-like commands
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
@@ -7,896 +7,34 b''
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
11 import itertools
10 import itertools
12 import os
11 import os
13 import re
14 import tempfile
15
12
16 from .i18n import _
13 from .i18n import _
17 from .node import (
14 from .node import (
18 hex,
15 hex,
19 nullid,
16 nullid,
20 nullrev,
21 short,
22 )
17 )
23
18
24 from . import (
19 from . import (
25 bookmarks,
26 changelog,
27 copies,
28 crecord as crecordmod,
29 dagop,
20 dagop,
30 dirstateguard,
31 encoding,
21 encoding,
32 error,
22 error,
33 formatter,
23 formatter,
34 graphmod,
24 graphmod,
35 match as matchmod,
25 match as matchmod,
36 mdiff,
26 mdiff,
37 obsolete,
38 patch,
27 patch,
39 pathutil,
28 pathutil,
40 pycompat,
29 pycompat,
41 registrar,
42 revlog,
43 revset,
30 revset,
44 revsetlang,
31 revsetlang,
45 rewriteutil,
46 scmutil,
32 scmutil,
47 smartset,
33 smartset,
48 templatekw,
34 templatekw,
49 templater,
35 templater,
50 util,
36 util,
51 vfs as vfsmod,
52 )
37 )
53 stringio = util.stringio
54
55 # templates of common command options
56
57 dryrunopts = [
58 ('n', 'dry-run', None,
59 _('do not perform actions, just print output')),
60 ]
61
62 remoteopts = [
63 ('e', 'ssh', '',
64 _('specify ssh command to use'), _('CMD')),
65 ('', 'remotecmd', '',
66 _('specify hg command to run on the remote side'), _('CMD')),
67 ('', 'insecure', None,
68 _('do not verify server certificate (ignoring web.cacerts config)')),
69 ]
70
71 walkopts = [
72 ('I', 'include', [],
73 _('include names matching the given patterns'), _('PATTERN')),
74 ('X', 'exclude', [],
75 _('exclude names matching the given patterns'), _('PATTERN')),
76 ]
77
78 commitopts = [
79 ('m', 'message', '',
80 _('use text as commit message'), _('TEXT')),
81 ('l', 'logfile', '',
82 _('read commit message from file'), _('FILE')),
83 ]
84
85 commitopts2 = [
86 ('d', 'date', '',
87 _('record the specified date as commit date'), _('DATE')),
88 ('u', 'user', '',
89 _('record the specified user as committer'), _('USER')),
90 ]
91
92 # hidden for now
93 formatteropts = [
94 ('T', 'template', '',
95 _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
96 ]
97
98 templateopts = [
99 ('', 'style', '',
100 _('display using template map file (DEPRECATED)'), _('STYLE')),
101 ('T', 'template', '',
102 _('display with template'), _('TEMPLATE')),
103 ]
104
105 logopts = [
106 ('p', 'patch', None, _('show patch')),
107 ('g', 'git', None, _('use git extended diff format')),
108 ('l', 'limit', '',
109 _('limit number of changes displayed'), _('NUM')),
110 ('M', 'no-merges', None, _('do not show merges')),
111 ('', 'stat', None, _('output diffstat-style summary of changes')),
112 ('G', 'graph', None, _("show the revision DAG")),
113 ] + templateopts
114
115 diffopts = [
116 ('a', 'text', None, _('treat all files as text')),
117 ('g', 'git', None, _('use git extended diff format')),
118 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
119 ('', 'nodates', None, _('omit dates from diff headers'))
120 ]
121
122 diffwsopts = [
123 ('w', 'ignore-all-space', None,
124 _('ignore white space when comparing lines')),
125 ('b', 'ignore-space-change', None,
126 _('ignore changes in the amount of white space')),
127 ('B', 'ignore-blank-lines', None,
128 _('ignore changes whose lines are all blank')),
129 ('Z', 'ignore-space-at-eol', None,
130 _('ignore changes in whitespace at EOL')),
131 ]
132
133 diffopts2 = [
134 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
135 ('p', 'show-function', None, _('show which function each change is in')),
136 ('', 'reverse', None, _('produce a diff that undoes the changes')),
137 ] + diffwsopts + [
138 ('U', 'unified', '',
139 _('number of lines of context to show'), _('NUM')),
140 ('', 'stat', None, _('output diffstat-style summary of changes')),
141 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
142 ]
143
144 mergetoolopts = [
145 ('t', 'tool', '', _('specify merge tool')),
146 ]
147
148 similarityopts = [
149 ('s', 'similarity', '',
150 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
151 ]
152
153 subrepoopts = [
154 ('S', 'subrepos', None,
155 _('recurse into subrepositories'))
156 ]
157
158 debugrevlogopts = [
159 ('c', 'changelog', False, _('open changelog')),
160 ('m', 'manifest', False, _('open manifest')),
161 ('', 'dir', '', _('open directory manifest')),
162 ]
163
164 # special string such that everything below this line will be ingored in the
165 # editor text
166 _linebelow = "^HG: ------------------------ >8 ------------------------$"
167
168 def ishunk(x):
169 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
170 return isinstance(x, hunkclasses)
171
172 def newandmodified(chunks, originalchunks):
173 newlyaddedandmodifiedfiles = set()
174 for chunk in chunks:
175 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
176 originalchunks:
177 newlyaddedandmodifiedfiles.add(chunk.header.filename())
178 return newlyaddedandmodifiedfiles
179
180 def parsealiases(cmd):
181 return cmd.lstrip("^").split("|")
182
183 def setupwrapcolorwrite(ui):
184 # wrap ui.write so diff output can be labeled/colorized
185 def wrapwrite(orig, *args, **kw):
186 label = kw.pop(r'label', '')
187 for chunk, l in patch.difflabel(lambda: args):
188 orig(chunk, label=label + l)
189
190 oldwrite = ui.write
191 def wrap(*args, **kwargs):
192 return wrapwrite(oldwrite, *args, **kwargs)
193 setattr(ui, 'write', wrap)
194 return oldwrite
195
196 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
197 if usecurses:
198 if testfile:
199 recordfn = crecordmod.testdecorator(testfile,
200 crecordmod.testchunkselector)
201 else:
202 recordfn = crecordmod.chunkselector
203
204 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
205
206 else:
207 return patch.filterpatch(ui, originalhunks, operation)
208
209 def recordfilter(ui, originalhunks, operation=None):
210 """ Prompts the user to filter the originalhunks and return a list of
211 selected hunks.
212 *operation* is used for to build ui messages to indicate the user what
213 kind of filtering they are doing: reverting, committing, shelving, etc.
214 (see patch.filterpatch).
215 """
216 usecurses = crecordmod.checkcurses(ui)
217 testfile = ui.config('experimental', 'crecordtest')
218 oldwrite = setupwrapcolorwrite(ui)
219 try:
220 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
221 testfile, operation)
222 finally:
223 ui.write = oldwrite
224 return newchunks, newopts
225
226 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
227 filterfn, *pats, **opts):
228 from . import merge as mergemod
229 opts = pycompat.byteskwargs(opts)
230 if not ui.interactive():
231 if cmdsuggest:
232 msg = _('running non-interactively, use %s instead') % cmdsuggest
233 else:
234 msg = _('running non-interactively')
235 raise error.Abort(msg)
236
237 # make sure username is set before going interactive
238 if not opts.get('user'):
239 ui.username() # raise exception, username not provided
240
241 def recordfunc(ui, repo, message, match, opts):
242 """This is generic record driver.
243
244 Its job is to interactively filter local changes, and
245 accordingly prepare working directory into a state in which the
246 job can be delegated to a non-interactive commit command such as
247 'commit' or 'qrefresh'.
248
249 After the actual job is done by non-interactive command, the
250 working directory is restored to its original state.
251
252 In the end we'll record interesting changes, and everything else
253 will be left in place, so the user can continue working.
254 """
255
256 checkunfinished(repo, commit=True)
257 wctx = repo[None]
258 merge = len(wctx.parents()) > 1
259 if merge:
260 raise error.Abort(_('cannot partially commit a merge '
261 '(use "hg commit" instead)'))
262
263 def fail(f, msg):
264 raise error.Abort('%s: %s' % (f, msg))
265
266 force = opts.get('force')
267 if not force:
268 vdirs = []
269 match.explicitdir = vdirs.append
270 match.bad = fail
271
272 status = repo.status(match=match)
273 if not force:
274 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
275 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
276 diffopts.nodates = True
277 diffopts.git = True
278 diffopts.showfunc = True
279 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
280 originalchunks = patch.parsepatch(originaldiff)
281
282 # 1. filter patch, since we are intending to apply subset of it
283 try:
284 chunks, newopts = filterfn(ui, originalchunks)
285 except error.PatchError as err:
286 raise error.Abort(_('error parsing patch: %s') % err)
287 opts.update(newopts)
288
289 # We need to keep a backup of files that have been newly added and
290 # modified during the recording process because there is a previous
291 # version without the edit in the workdir
292 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
293 contenders = set()
294 for h in chunks:
295 try:
296 contenders.update(set(h.files()))
297 except AttributeError:
298 pass
299
300 changed = status.modified + status.added + status.removed
301 newfiles = [f for f in changed if f in contenders]
302 if not newfiles:
303 ui.status(_('no changes to record\n'))
304 return 0
305
306 modified = set(status.modified)
307
308 # 2. backup changed files, so we can restore them in the end
309
310 if backupall:
311 tobackup = changed
312 else:
313 tobackup = [f for f in newfiles if f in modified or f in \
314 newlyaddedandmodifiedfiles]
315 backups = {}
316 if tobackup:
317 backupdir = repo.vfs.join('record-backups')
318 try:
319 os.mkdir(backupdir)
320 except OSError as err:
321 if err.errno != errno.EEXIST:
322 raise
323 try:
324 # backup continues
325 for f in tobackup:
326 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
327 dir=backupdir)
328 os.close(fd)
329 ui.debug('backup %r as %r\n' % (f, tmpname))
330 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
331 backups[f] = tmpname
332
333 fp = stringio()
334 for c in chunks:
335 fname = c.filename()
336 if fname in backups:
337 c.write(fp)
338 dopatch = fp.tell()
339 fp.seek(0)
340
341 # 2.5 optionally review / modify patch in text editor
342 if opts.get('review', False):
343 patchtext = (crecordmod.diffhelptext
344 + crecordmod.patchhelptext
345 + fp.read())
346 reviewedpatch = ui.edit(patchtext, "",
347 action="diff",
348 repopath=repo.path)
349 fp.truncate(0)
350 fp.write(reviewedpatch)
351 fp.seek(0)
352
353 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
354 # 3a. apply filtered patch to clean repo (clean)
355 if backups:
356 # Equivalent to hg.revert
357 m = scmutil.matchfiles(repo, backups.keys())
358 mergemod.update(repo, repo.dirstate.p1(),
359 False, True, matcher=m)
360
361 # 3b. (apply)
362 if dopatch:
363 try:
364 ui.debug('applying patch\n')
365 ui.debug(fp.getvalue())
366 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
367 except error.PatchError as err:
368 raise error.Abort(str(err))
369 del fp
370
371 # 4. We prepared working directory according to filtered
372 # patch. Now is the time to delegate the job to
373 # commit/qrefresh or the like!
374
375 # Make all of the pathnames absolute.
376 newfiles = [repo.wjoin(nf) for nf in newfiles]
377 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
378 finally:
379 # 5. finally restore backed-up files
380 try:
381 dirstate = repo.dirstate
382 for realname, tmpname in backups.iteritems():
383 ui.debug('restoring %r to %r\n' % (tmpname, realname))
384
385 if dirstate[realname] == 'n':
386 # without normallookup, restoring timestamp
387 # may cause partially committed files
388 # to be treated as unmodified
389 dirstate.normallookup(realname)
390
391 # copystat=True here and above are a hack to trick any
392 # editors that have f open that we haven't modified them.
393 #
394 # Also note that this racy as an editor could notice the
395 # file's mtime before we've finished writing it.
396 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
397 os.unlink(tmpname)
398 if tobackup:
399 os.rmdir(backupdir)
400 except OSError:
401 pass
402
403 def recordinwlock(ui, repo, message, match, opts):
404 with repo.wlock():
405 return recordfunc(ui, repo, message, match, opts)
406
407 return commit(ui, repo, recordinwlock, pats, opts)
408
409 class dirnode(object):
410 """
411 Represent a directory in user working copy with information required for
412 the purpose of tersing its status.
413
414 path is the path to the directory
415
416 statuses is a set of statuses of all files in this directory (this includes
417 all the files in all the subdirectories too)
418
419 files is a list of files which are direct child of this directory
420
421 subdirs is a dictionary of sub-directory name as the key and it's own
422 dirnode object as the value
423 """
424
425 def __init__(self, dirpath):
426 self.path = dirpath
427 self.statuses = set([])
428 self.files = []
429 self.subdirs = {}
430
431 def _addfileindir(self, filename, status):
432 """Add a file in this directory as a direct child."""
433 self.files.append((filename, status))
434
435 def addfile(self, filename, status):
436 """
437 Add a file to this directory or to its direct parent directory.
438
439 If the file is not direct child of this directory, we traverse to the
440 directory of which this file is a direct child of and add the file
441 there.
442 """
443
444 # the filename contains a path separator, it means it's not the direct
445 # child of this directory
446 if '/' in filename:
447 subdir, filep = filename.split('/', 1)
448
449 # does the dirnode object for subdir exists
450 if subdir not in self.subdirs:
451 subdirpath = os.path.join(self.path, subdir)
452 self.subdirs[subdir] = dirnode(subdirpath)
453
454 # try adding the file in subdir
455 self.subdirs[subdir].addfile(filep, status)
456
457 else:
458 self._addfileindir(filename, status)
459
460 if status not in self.statuses:
461 self.statuses.add(status)
462
463 def iterfilepaths(self):
464 """Yield (status, path) for files directly under this directory."""
465 for f, st in self.files:
466 yield st, os.path.join(self.path, f)
467
468 def tersewalk(self, terseargs):
469 """
470 Yield (status, path) obtained by processing the status of this
471 dirnode.
472
473 terseargs is the string of arguments passed by the user with `--terse`
474 flag.
475
476 Following are the cases which can happen:
477
478 1) All the files in the directory (including all the files in its
479 subdirectories) share the same status and the user has asked us to terse
480 that status. -> yield (status, dirpath)
481
482 2) Otherwise, we do following:
483
484 a) Yield (status, filepath) for all the files which are in this
485 directory (only the ones in this directory, not the subdirs)
486
487 b) Recurse the function on all the subdirectories of this
488 directory
489 """
490
491 if len(self.statuses) == 1:
492 onlyst = self.statuses.pop()
493
494 # Making sure we terse only when the status abbreviation is
495 # passed as terse argument
496 if onlyst in terseargs:
497 yield onlyst, self.path + pycompat.ossep
498 return
499
500 # add the files to status list
501 for st, fpath in self.iterfilepaths():
502 yield st, fpath
503
504 #recurse on the subdirs
505 for dirobj in self.subdirs.values():
506 for st, fpath in dirobj.tersewalk(terseargs):
507 yield st, fpath
508
509 def tersedir(statuslist, terseargs):
510 """
511 Terse the status if all the files in a directory shares the same status.
512
513 statuslist is scmutil.status() object which contains a list of files for
514 each status.
515 terseargs is string which is passed by the user as the argument to `--terse`
516 flag.
517
518 The function makes a tree of objects of dirnode class, and at each node it
519 stores the information required to know whether we can terse a certain
520 directory or not.
521 """
522 # the order matters here as that is used to produce final list
523 allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
524
525 # checking the argument validity
526 for s in pycompat.bytestr(terseargs):
527 if s not in allst:
528 raise error.Abort(_("'%s' not recognized") % s)
529
530 # creating a dirnode object for the root of the repo
531 rootobj = dirnode('')
532 pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
533 'ignored', 'removed')
534
535 tersedict = {}
536 for attrname in pstatus:
537 statuschar = attrname[0:1]
538 for f in getattr(statuslist, attrname):
539 rootobj.addfile(f, statuschar)
540 tersedict[statuschar] = []
541
542 # we won't be tersing the root dir, so add files in it
543 for st, fpath in rootobj.iterfilepaths():
544 tersedict[st].append(fpath)
545
546 # process each sub-directory and build tersedict
547 for subdir in rootobj.subdirs.values():
548 for st, f in subdir.tersewalk(terseargs):
549 tersedict[st].append(f)
550
551 tersedlist = []
552 for st in allst:
553 tersedict[st].sort()
554 tersedlist.append(tersedict[st])
555
556 return tersedlist
557
558 def _commentlines(raw):
559 '''Surround lineswith a comment char and a new line'''
560 lines = raw.splitlines()
561 commentedlines = ['# %s' % line for line in lines]
562 return '\n'.join(commentedlines) + '\n'
563
564 def _conflictsmsg(repo):
565 # avoid merge cycle
566 from . import merge as mergemod
567 mergestate = mergemod.mergestate.read(repo)
568 if not mergestate.active():
569 return
570
571 m = scmutil.match(repo[None])
572 unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
573 if unresolvedlist:
574 mergeliststr = '\n'.join(
575 [' %s' % util.pathto(repo.root, pycompat.getcwd(), path)
576 for path in unresolvedlist])
577 msg = _('''Unresolved merge conflicts:
578
579 %s
580
581 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
582 else:
583 msg = _('No unresolved merge conflicts.')
584
585 return _commentlines(msg)
586
587 def _helpmessage(continuecmd, abortcmd):
588 msg = _('To continue: %s\n'
589 'To abort: %s') % (continuecmd, abortcmd)
590 return _commentlines(msg)
591
592 def _rebasemsg():
593 return _helpmessage('hg rebase --continue', 'hg rebase --abort')
594
595 def _histeditmsg():
596 return _helpmessage('hg histedit --continue', 'hg histedit --abort')
597
598 def _unshelvemsg():
599 return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
600
601 def _updatecleanmsg(dest=None):
602 warning = _('warning: this will discard uncommitted changes')
603 return 'hg update --clean %s (%s)' % (dest or '.', warning)
604
605 def _graftmsg():
606 # tweakdefaults requires `update` to have a rev hence the `.`
607 return _helpmessage('hg graft --continue', _updatecleanmsg())
608
609 def _mergemsg():
610 # tweakdefaults requires `update` to have a rev hence the `.`
611 return _helpmessage('hg commit', _updatecleanmsg())
612
613 def _bisectmsg():
614 msg = _('To mark the changeset good: hg bisect --good\n'
615 'To mark the changeset bad: hg bisect --bad\n'
616 'To abort: hg bisect --reset\n')
617 return _commentlines(msg)
618
619 def fileexistspredicate(filename):
620 return lambda repo: repo.vfs.exists(filename)
621
622 def _mergepredicate(repo):
623 return len(repo[None].parents()) > 1
624
625 STATES = (
626 # (state, predicate to detect states, helpful message function)
627 ('histedit', fileexistspredicate('histedit-state'), _histeditmsg),
628 ('bisect', fileexistspredicate('bisect.state'), _bisectmsg),
629 ('graft', fileexistspredicate('graftstate'), _graftmsg),
630 ('unshelve', fileexistspredicate('unshelverebasestate'), _unshelvemsg),
631 ('rebase', fileexistspredicate('rebasestate'), _rebasemsg),
632 # The merge state is part of a list that will be iterated over.
633 # They need to be last because some of the other unfinished states may also
634 # be in a merge or update state (eg. rebase, histedit, graft, etc).
635 # We want those to have priority.
636 ('merge', _mergepredicate, _mergemsg),
637 )
638
639 def _getrepostate(repo):
640 # experimental config: commands.status.skipstates
641 skip = set(repo.ui.configlist('commands', 'status.skipstates'))
642 for state, statedetectionpredicate, msgfn in STATES:
643 if state in skip:
644 continue
645 if statedetectionpredicate(repo):
646 return (state, statedetectionpredicate, msgfn)
647
648 def morestatus(repo, fm):
649 statetuple = _getrepostate(repo)
650 label = 'status.morestatus'
651 if statetuple:
652 fm.startitem()
653 state, statedetectionpredicate, helpfulmsg = statetuple
654 statemsg = _('The repository is in an unfinished *%s* state.') % state
655 fm.write('statemsg', '%s\n', _commentlines(statemsg), label=label)
656 conmsg = _conflictsmsg(repo)
657 if conmsg:
658 fm.write('conflictsmsg', '%s\n', conmsg, label=label)
659 if helpfulmsg:
660 helpmsg = helpfulmsg()
661 fm.write('helpmsg', '%s\n', helpmsg, label=label)
662
663 def findpossible(cmd, table, strict=False):
664 """
665 Return cmd -> (aliases, command table entry)
666 for each matching command.
667 Return debug commands (or their aliases) only if no normal command matches.
668 """
669 choice = {}
670 debugchoice = {}
671
672 if cmd in table:
673 # short-circuit exact matches, "log" alias beats "^log|history"
674 keys = [cmd]
675 else:
676 keys = table.keys()
677
678 allcmds = []
679 for e in keys:
680 aliases = parsealiases(e)
681 allcmds.extend(aliases)
682 found = None
683 if cmd in aliases:
684 found = cmd
685 elif not strict:
686 for a in aliases:
687 if a.startswith(cmd):
688 found = a
689 break
690 if found is not None:
691 if aliases[0].startswith("debug") or found.startswith("debug"):
692 debugchoice[found] = (aliases, table[e])
693 else:
694 choice[found] = (aliases, table[e])
695
696 if not choice and debugchoice:
697 choice = debugchoice
698
699 return choice, allcmds
700
701 def findcmd(cmd, table, strict=True):
702 """Return (aliases, command table entry) for command string."""
703 choice, allcmds = findpossible(cmd, table, strict)
704
705 if cmd in choice:
706 return choice[cmd]
707
708 if len(choice) > 1:
709 clist = sorted(choice)
710 raise error.AmbiguousCommand(cmd, clist)
711
712 if choice:
713 return list(choice.values())[0]
714
715 raise error.UnknownCommand(cmd, allcmds)
716
717 def changebranch(ui, repo, revs, label):
718 """ Change the branch name of given revs to label """
719
720 with repo.wlock(), repo.lock(), repo.transaction('branches'):
721 # abort in case of uncommitted merge or dirty wdir
722 bailifchanged(repo)
723 revs = scmutil.revrange(repo, revs)
724 if not revs:
725 raise error.Abort("empty revision set")
726 roots = repo.revs('roots(%ld)', revs)
727 if len(roots) > 1:
728 raise error.Abort(_("cannot change branch of non-linear revisions"))
729 rewriteutil.precheck(repo, revs, 'change branch of')
730
731 root = repo[roots.first()]
732 if not root.p1().branch() == label and label in repo.branchmap():
733 raise error.Abort(_("a branch of the same name already exists"))
734
735 if repo.revs('merge() and %ld', revs):
736 raise error.Abort(_("cannot change branch of a merge commit"))
737 if repo.revs('obsolete() and %ld', revs):
738 raise error.Abort(_("cannot change branch of a obsolete changeset"))
739
740 # make sure only topological heads
741 if repo.revs('heads(%ld) - head()', revs):
742 raise error.Abort(_("cannot change branch in middle of a stack"))
743
744 replacements = {}
745 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
746 # mercurial.subrepo -> mercurial.cmdutil
747 from . import context
748 for rev in revs:
749 ctx = repo[rev]
750 oldbranch = ctx.branch()
751 # check if ctx has same branch
752 if oldbranch == label:
753 continue
754
755 def filectxfn(repo, newctx, path):
756 try:
757 return ctx[path]
758 except error.ManifestLookupError:
759 return None
760
761 ui.debug("changing branch of '%s' from '%s' to '%s'\n"
762 % (hex(ctx.node()), oldbranch, label))
763 extra = ctx.extra()
764 extra['branch_change'] = hex(ctx.node())
765 # While changing branch of set of linear commits, make sure that
766 # we base our commits on new parent rather than old parent which
767 # was obsoleted while changing the branch
768 p1 = ctx.p1().node()
769 p2 = ctx.p2().node()
770 if p1 in replacements:
771 p1 = replacements[p1][0]
772 if p2 in replacements:
773 p2 = replacements[p2][0]
774
775 mc = context.memctx(repo, (p1, p2),
776 ctx.description(),
777 ctx.files(),
778 filectxfn,
779 user=ctx.user(),
780 date=ctx.date(),
781 extra=extra,
782 branch=label)
783
784 commitphase = ctx.phase()
785 overrides = {('phases', 'new-commit'): commitphase}
786 with repo.ui.configoverride(overrides, 'branch-change'):
787 newnode = repo.commitctx(mc)
788
789 replacements[ctx.node()] = (newnode,)
790 ui.debug('new node id is %s\n' % hex(newnode))
791
792 # create obsmarkers and move bookmarks
793 scmutil.cleanupnodes(repo, replacements, 'branch-change')
794
795 # move the working copy too
796 wctx = repo[None]
797 # in-progress merge is a bit too complex for now.
798 if len(wctx.parents()) == 1:
799 newid = replacements.get(wctx.p1().node())
800 if newid is not None:
801 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
802 # mercurial.cmdutil
803 from . import hg
804 hg.update(repo, newid[0], quietempty=True)
805
806 ui.status(_("changed branch on %d changesets\n") % len(replacements))
807
808 def findrepo(p):
809 while not os.path.isdir(os.path.join(p, ".hg")):
810 oldp, p = p, os.path.dirname(p)
811 if p == oldp:
812 return None
813
814 return p
815
816 def bailifchanged(repo, merge=True, hint=None):
817 """ enforce the precondition that working directory must be clean.
818
819 'merge' can be set to false if a pending uncommitted merge should be
820 ignored (such as when 'update --check' runs).
821
822 'hint' is the usual hint given to Abort exception.
823 """
824
825 if merge and repo.dirstate.p2() != nullid:
826 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
827 modified, added, removed, deleted = repo.status()[:4]
828 if modified or added or removed or deleted:
829 raise error.Abort(_('uncommitted changes'), hint=hint)
830 ctx = repo[None]
831 for s in sorted(ctx.substate):
832 ctx.sub(s).bailifchanged(hint=hint)
833
834 def logmessage(ui, opts):
835 """ get the log message according to -m and -l option """
836 message = opts.get('message')
837 logfile = opts.get('logfile')
838
839 if message and logfile:
840 raise error.Abort(_('options --message and --logfile are mutually '
841 'exclusive'))
842 if not message and logfile:
843 try:
844 if isstdiofilename(logfile):
845 message = ui.fin.read()
846 else:
847 message = '\n'.join(util.readfile(logfile).splitlines())
848 except IOError as inst:
849 raise error.Abort(_("can't read commit message '%s': %s") %
850 (logfile, encoding.strtolocal(inst.strerror)))
851 return message
852
853 def mergeeditform(ctxorbool, baseformname):
854 """return appropriate editform name (referencing a committemplate)
855
856 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
857 merging is committed.
858
859 This returns baseformname with '.merge' appended if it is a merge,
860 otherwise '.normal' is appended.
861 """
862 if isinstance(ctxorbool, bool):
863 if ctxorbool:
864 return baseformname + ".merge"
865 elif 1 < len(ctxorbool.parents()):
866 return baseformname + ".merge"
867
868 return baseformname + ".normal"
869
870 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
871 editform='', **opts):
872 """get appropriate commit message editor according to '--edit' option
873
874 'finishdesc' is a function to be called with edited commit message
875 (= 'description' of the new changeset) just after editing, but
876 before checking empty-ness. It should return actual text to be
877 stored into history. This allows to change description before
878 storing.
879
880 'extramsg' is a extra message to be shown in the editor instead of
881 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
882 is automatically added.
883
884 'editform' is a dot-separated list of names, to distinguish
885 the purpose of commit text editing.
886
887 'getcommiteditor' returns 'commitforceeditor' regardless of
888 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
889 they are specific for usage in MQ.
890 """
891 if edit or finishdesc or extramsg:
892 return lambda r, c, s: commitforceeditor(r, c, s,
893 finishdesc=finishdesc,
894 extramsg=extramsg,
895 editform=editform)
896 elif editform:
897 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
898 else:
899 return commiteditor
900
38
901 def loglimit(opts):
39 def loglimit(opts):
902 """get the log limit according to option -l/--limit"""
40 """get the log limit according to option -l/--limit"""
@@ -912,677 +50,6 b' def loglimit(opts):'
912 limit = None
50 limit = None
913 return limit
51 return limit
914
52
915 def makefilename(repo, pat, node, desc=None,
916 total=None, seqno=None, revwidth=None, pathname=None):
917 node_expander = {
918 'H': lambda: hex(node),
919 'R': lambda: '%d' % repo.changelog.rev(node),
920 'h': lambda: short(node),
921 'm': lambda: re.sub('[^\w]', '_', desc or '')
922 }
923 expander = {
924 '%': lambda: '%',
925 'b': lambda: os.path.basename(repo.root),
926 }
927
928 try:
929 if node:
930 expander.update(node_expander)
931 if node:
932 expander['r'] = (lambda:
933 ('%d' % repo.changelog.rev(node)).zfill(revwidth or 0))
934 if total is not None:
935 expander['N'] = lambda: '%d' % total
936 if seqno is not None:
937 expander['n'] = lambda: '%d' % seqno
938 if total is not None and seqno is not None:
939 expander['n'] = (lambda: ('%d' % seqno).zfill(len('%d' % total)))
940 if pathname is not None:
941 expander['s'] = lambda: os.path.basename(pathname)
942 expander['d'] = lambda: os.path.dirname(pathname) or '.'
943 expander['p'] = lambda: pathname
944
945 newname = []
946 patlen = len(pat)
947 i = 0
948 while i < patlen:
949 c = pat[i:i + 1]
950 if c == '%':
951 i += 1
952 c = pat[i:i + 1]
953 c = expander[c]()
954 newname.append(c)
955 i += 1
956 return ''.join(newname)
957 except KeyError as inst:
958 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
959 inst.args[0])
960
961 def isstdiofilename(pat):
962 """True if the given pat looks like a filename denoting stdin/stdout"""
963 return not pat or pat == '-'
964
965 class _unclosablefile(object):
966 def __init__(self, fp):
967 self._fp = fp
968
969 def close(self):
970 pass
971
972 def __iter__(self):
973 return iter(self._fp)
974
975 def __getattr__(self, attr):
976 return getattr(self._fp, attr)
977
978 def __enter__(self):
979 return self
980
981 def __exit__(self, exc_type, exc_value, exc_tb):
982 pass
983
984 def makefileobj(repo, pat, node=None, desc=None, total=None,
985 seqno=None, revwidth=None, mode='wb', modemap=None,
986 pathname=None):
987
988 writable = mode not in ('r', 'rb')
989
990 if isstdiofilename(pat):
991 if writable:
992 fp = repo.ui.fout
993 else:
994 fp = repo.ui.fin
995 return _unclosablefile(fp)
996 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
997 if modemap is not None:
998 mode = modemap.get(fn, mode)
999 if mode == 'wb':
1000 modemap[fn] = 'ab'
1001 return open(fn, mode)
1002
1003 def openrevlog(repo, cmd, file_, opts):
1004 """opens the changelog, manifest, a filelog or a given revlog"""
1005 cl = opts['changelog']
1006 mf = opts['manifest']
1007 dir = opts['dir']
1008 msg = None
1009 if cl and mf:
1010 msg = _('cannot specify --changelog and --manifest at the same time')
1011 elif cl and dir:
1012 msg = _('cannot specify --changelog and --dir at the same time')
1013 elif cl or mf or dir:
1014 if file_:
1015 msg = _('cannot specify filename with --changelog or --manifest')
1016 elif not repo:
1017 msg = _('cannot specify --changelog or --manifest or --dir '
1018 'without a repository')
1019 if msg:
1020 raise error.Abort(msg)
1021
1022 r = None
1023 if repo:
1024 if cl:
1025 r = repo.unfiltered().changelog
1026 elif dir:
1027 if 'treemanifest' not in repo.requirements:
1028 raise error.Abort(_("--dir can only be used on repos with "
1029 "treemanifest enabled"))
1030 dirlog = repo.manifestlog._revlog.dirlog(dir)
1031 if len(dirlog):
1032 r = dirlog
1033 elif mf:
1034 r = repo.manifestlog._revlog
1035 elif file_:
1036 filelog = repo.file(file_)
1037 if len(filelog):
1038 r = filelog
1039 if not r:
1040 if not file_:
1041 raise error.CommandError(cmd, _('invalid arguments'))
1042 if not os.path.isfile(file_):
1043 raise error.Abort(_("revlog '%s' not found") % file_)
1044 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
1045 file_[:-2] + ".i")
1046 return r
1047
1048 def copy(ui, repo, pats, opts, rename=False):
1049 # called with the repo lock held
1050 #
1051 # hgsep => pathname that uses "/" to separate directories
1052 # ossep => pathname that uses os.sep to separate directories
1053 cwd = repo.getcwd()
1054 targets = {}
1055 after = opts.get("after")
1056 dryrun = opts.get("dry_run")
1057 wctx = repo[None]
1058
1059 def walkpat(pat):
1060 srcs = []
1061 if after:
1062 badstates = '?'
1063 else:
1064 badstates = '?r'
1065 m = scmutil.match(wctx, [pat], opts, globbed=True)
1066 for abs in wctx.walk(m):
1067 state = repo.dirstate[abs]
1068 rel = m.rel(abs)
1069 exact = m.exact(abs)
1070 if state in badstates:
1071 if exact and state == '?':
1072 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1073 if exact and state == 'r':
1074 ui.warn(_('%s: not copying - file has been marked for'
1075 ' remove\n') % rel)
1076 continue
1077 # abs: hgsep
1078 # rel: ossep
1079 srcs.append((abs, rel, exact))
1080 return srcs
1081
1082 # abssrc: hgsep
1083 # relsrc: ossep
1084 # otarget: ossep
1085 def copyfile(abssrc, relsrc, otarget, exact):
1086 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1087 if '/' in abstarget:
1088 # We cannot normalize abstarget itself, this would prevent
1089 # case only renames, like a => A.
1090 abspath, absname = abstarget.rsplit('/', 1)
1091 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1092 reltarget = repo.pathto(abstarget, cwd)
1093 target = repo.wjoin(abstarget)
1094 src = repo.wjoin(abssrc)
1095 state = repo.dirstate[abstarget]
1096
1097 scmutil.checkportable(ui, abstarget)
1098
1099 # check for collisions
1100 prevsrc = targets.get(abstarget)
1101 if prevsrc is not None:
1102 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1103 (reltarget, repo.pathto(abssrc, cwd),
1104 repo.pathto(prevsrc, cwd)))
1105 return
1106
1107 # check for overwrites
1108 exists = os.path.lexists(target)
1109 samefile = False
1110 if exists and abssrc != abstarget:
1111 if (repo.dirstate.normalize(abssrc) ==
1112 repo.dirstate.normalize(abstarget)):
1113 if not rename:
1114 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1115 return
1116 exists = False
1117 samefile = True
1118
1119 if not after and exists or after and state in 'mn':
1120 if not opts['force']:
1121 if state in 'mn':
1122 msg = _('%s: not overwriting - file already committed\n')
1123 if after:
1124 flags = '--after --force'
1125 else:
1126 flags = '--force'
1127 if rename:
1128 hint = _('(hg rename %s to replace the file by '
1129 'recording a rename)\n') % flags
1130 else:
1131 hint = _('(hg copy %s to replace the file by '
1132 'recording a copy)\n') % flags
1133 else:
1134 msg = _('%s: not overwriting - file exists\n')
1135 if rename:
1136 hint = _('(hg rename --after to record the rename)\n')
1137 else:
1138 hint = _('(hg copy --after to record the copy)\n')
1139 ui.warn(msg % reltarget)
1140 ui.warn(hint)
1141 return
1142
1143 if after:
1144 if not exists:
1145 if rename:
1146 ui.warn(_('%s: not recording move - %s does not exist\n') %
1147 (relsrc, reltarget))
1148 else:
1149 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1150 (relsrc, reltarget))
1151 return
1152 elif not dryrun:
1153 try:
1154 if exists:
1155 os.unlink(target)
1156 targetdir = os.path.dirname(target) or '.'
1157 if not os.path.isdir(targetdir):
1158 os.makedirs(targetdir)
1159 if samefile:
1160 tmp = target + "~hgrename"
1161 os.rename(src, tmp)
1162 os.rename(tmp, target)
1163 else:
1164 util.copyfile(src, target)
1165 srcexists = True
1166 except IOError as inst:
1167 if inst.errno == errno.ENOENT:
1168 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1169 srcexists = False
1170 else:
1171 ui.warn(_('%s: cannot copy - %s\n') %
1172 (relsrc, encoding.strtolocal(inst.strerror)))
1173 return True # report a failure
1174
1175 if ui.verbose or not exact:
1176 if rename:
1177 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1178 else:
1179 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1180
1181 targets[abstarget] = abssrc
1182
1183 # fix up dirstate
1184 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1185 dryrun=dryrun, cwd=cwd)
1186 if rename and not dryrun:
1187 if not after and srcexists and not samefile:
1188 repo.wvfs.unlinkpath(abssrc)
1189 wctx.forget([abssrc])
1190
1191 # pat: ossep
1192 # dest ossep
1193 # srcs: list of (hgsep, hgsep, ossep, bool)
1194 # return: function that takes hgsep and returns ossep
1195 def targetpathfn(pat, dest, srcs):
1196 if os.path.isdir(pat):
1197 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1198 abspfx = util.localpath(abspfx)
1199 if destdirexists:
1200 striplen = len(os.path.split(abspfx)[0])
1201 else:
1202 striplen = len(abspfx)
1203 if striplen:
1204 striplen += len(pycompat.ossep)
1205 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1206 elif destdirexists:
1207 res = lambda p: os.path.join(dest,
1208 os.path.basename(util.localpath(p)))
1209 else:
1210 res = lambda p: dest
1211 return res
1212
1213 # pat: ossep
1214 # dest ossep
1215 # srcs: list of (hgsep, hgsep, ossep, bool)
1216 # return: function that takes hgsep and returns ossep
1217 def targetpathafterfn(pat, dest, srcs):
1218 if matchmod.patkind(pat):
1219 # a mercurial pattern
1220 res = lambda p: os.path.join(dest,
1221 os.path.basename(util.localpath(p)))
1222 else:
1223 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1224 if len(abspfx) < len(srcs[0][0]):
1225 # A directory. Either the target path contains the last
1226 # component of the source path or it does not.
1227 def evalpath(striplen):
1228 score = 0
1229 for s in srcs:
1230 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1231 if os.path.lexists(t):
1232 score += 1
1233 return score
1234
1235 abspfx = util.localpath(abspfx)
1236 striplen = len(abspfx)
1237 if striplen:
1238 striplen += len(pycompat.ossep)
1239 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1240 score = evalpath(striplen)
1241 striplen1 = len(os.path.split(abspfx)[0])
1242 if striplen1:
1243 striplen1 += len(pycompat.ossep)
1244 if evalpath(striplen1) > score:
1245 striplen = striplen1
1246 res = lambda p: os.path.join(dest,
1247 util.localpath(p)[striplen:])
1248 else:
1249 # a file
1250 if destdirexists:
1251 res = lambda p: os.path.join(dest,
1252 os.path.basename(util.localpath(p)))
1253 else:
1254 res = lambda p: dest
1255 return res
1256
1257 pats = scmutil.expandpats(pats)
1258 if not pats:
1259 raise error.Abort(_('no source or destination specified'))
1260 if len(pats) == 1:
1261 raise error.Abort(_('no destination specified'))
1262 dest = pats.pop()
1263 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1264 if not destdirexists:
1265 if len(pats) > 1 or matchmod.patkind(pats[0]):
1266 raise error.Abort(_('with multiple sources, destination must be an '
1267 'existing directory'))
1268 if util.endswithsep(dest):
1269 raise error.Abort(_('destination %s is not a directory') % dest)
1270
1271 tfn = targetpathfn
1272 if after:
1273 tfn = targetpathafterfn
1274 copylist = []
1275 for pat in pats:
1276 srcs = walkpat(pat)
1277 if not srcs:
1278 continue
1279 copylist.append((tfn(pat, dest, srcs), srcs))
1280 if not copylist:
1281 raise error.Abort(_('no files to copy'))
1282
1283 errors = 0
1284 for targetpath, srcs in copylist:
1285 for abssrc, relsrc, exact in srcs:
1286 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1287 errors += 1
1288
1289 if errors:
1290 ui.warn(_('(consider using --after)\n'))
1291
1292 return errors != 0
1293
1294 ## facility to let extension process additional data into an import patch
1295 # list of identifier to be executed in order
1296 extrapreimport = [] # run before commit
1297 extrapostimport = [] # run after commit
1298 # mapping from identifier to actual import function
1299 #
1300 # 'preimport' are run before the commit is made and are provided the following
1301 # arguments:
1302 # - repo: the localrepository instance,
1303 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1304 # - extra: the future extra dictionary of the changeset, please mutate it,
1305 # - opts: the import options.
1306 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1307 # mutation of in memory commit and more. Feel free to rework the code to get
1308 # there.
1309 extrapreimportmap = {}
1310 # 'postimport' are run after the commit is made and are provided the following
1311 # argument:
1312 # - ctx: the changectx created by import.
1313 extrapostimportmap = {}
1314
1315 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
1316 """Utility function used by commands.import to import a single patch
1317
1318 This function is explicitly defined here to help the evolve extension to
1319 wrap this part of the import logic.
1320
1321 The API is currently a bit ugly because it a simple code translation from
1322 the import command. Feel free to make it better.
1323
1324 :hunk: a patch (as a binary string)
1325 :parents: nodes that will be parent of the created commit
1326 :opts: the full dict of option passed to the import command
1327 :msgs: list to save commit message to.
1328 (used in case we need to save it when failing)
1329 :updatefunc: a function that update a repo to a given node
1330 updatefunc(<repo>, <node>)
1331 """
1332 # avoid cycle context -> subrepo -> cmdutil
1333 from . import context
1334 extractdata = patch.extract(ui, hunk)
1335 tmpname = extractdata.get('filename')
1336 message = extractdata.get('message')
1337 user = opts.get('user') or extractdata.get('user')
1338 date = opts.get('date') or extractdata.get('date')
1339 branch = extractdata.get('branch')
1340 nodeid = extractdata.get('nodeid')
1341 p1 = extractdata.get('p1')
1342 p2 = extractdata.get('p2')
1343
1344 nocommit = opts.get('no_commit')
1345 importbranch = opts.get('import_branch')
1346 update = not opts.get('bypass')
1347 strip = opts["strip"]
1348 prefix = opts["prefix"]
1349 sim = float(opts.get('similarity') or 0)
1350 if not tmpname:
1351 return (None, None, False)
1352
1353 rejects = False
1354
1355 try:
1356 cmdline_message = logmessage(ui, opts)
1357 if cmdline_message:
1358 # pickup the cmdline msg
1359 message = cmdline_message
1360 elif message:
1361 # pickup the patch msg
1362 message = message.strip()
1363 else:
1364 # launch the editor
1365 message = None
1366 ui.debug('message:\n%s\n' % message)
1367
1368 if len(parents) == 1:
1369 parents.append(repo[nullid])
1370 if opts.get('exact'):
1371 if not nodeid or not p1:
1372 raise error.Abort(_('not a Mercurial patch'))
1373 p1 = repo[p1]
1374 p2 = repo[p2 or nullid]
1375 elif p2:
1376 try:
1377 p1 = repo[p1]
1378 p2 = repo[p2]
1379 # Without any options, consider p2 only if the
1380 # patch is being applied on top of the recorded
1381 # first parent.
1382 if p1 != parents[0]:
1383 p1 = parents[0]
1384 p2 = repo[nullid]
1385 except error.RepoError:
1386 p1, p2 = parents
1387 if p2.node() == nullid:
1388 ui.warn(_("warning: import the patch as a normal revision\n"
1389 "(use --exact to import the patch as a merge)\n"))
1390 else:
1391 p1, p2 = parents
1392
1393 n = None
1394 if update:
1395 if p1 != parents[0]:
1396 updatefunc(repo, p1.node())
1397 if p2 != parents[1]:
1398 repo.setparents(p1.node(), p2.node())
1399
1400 if opts.get('exact') or importbranch:
1401 repo.dirstate.setbranch(branch or 'default')
1402
1403 partial = opts.get('partial', False)
1404 files = set()
1405 try:
1406 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1407 files=files, eolmode=None, similarity=sim / 100.0)
1408 except error.PatchError as e:
1409 if not partial:
1410 raise error.Abort(str(e))
1411 if partial:
1412 rejects = True
1413
1414 files = list(files)
1415 if nocommit:
1416 if message:
1417 msgs.append(message)
1418 else:
1419 if opts.get('exact') or p2:
1420 # If you got here, you either use --force and know what
1421 # you are doing or used --exact or a merge patch while
1422 # being updated to its first parent.
1423 m = None
1424 else:
1425 m = scmutil.matchfiles(repo, files or [])
1426 editform = mergeeditform(repo[None], 'import.normal')
1427 if opts.get('exact'):
1428 editor = None
1429 else:
1430 editor = getcommiteditor(editform=editform,
1431 **pycompat.strkwargs(opts))
1432 extra = {}
1433 for idfunc in extrapreimport:
1434 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1435 overrides = {}
1436 if partial:
1437 overrides[('ui', 'allowemptycommit')] = True
1438 with repo.ui.configoverride(overrides, 'import'):
1439 n = repo.commit(message, user,
1440 date, match=m,
1441 editor=editor, extra=extra)
1442 for idfunc in extrapostimport:
1443 extrapostimportmap[idfunc](repo[n])
1444 else:
1445 if opts.get('exact') or importbranch:
1446 branch = branch or 'default'
1447 else:
1448 branch = p1.branch()
1449 store = patch.filestore()
1450 try:
1451 files = set()
1452 try:
1453 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1454 files, eolmode=None)
1455 except error.PatchError as e:
1456 raise error.Abort(str(e))
1457 if opts.get('exact'):
1458 editor = None
1459 else:
1460 editor = getcommiteditor(editform='import.bypass')
1461 memctx = context.memctx(repo, (p1.node(), p2.node()),
1462 message,
1463 files=files,
1464 filectxfn=store,
1465 user=user,
1466 date=date,
1467 branch=branch,
1468 editor=editor)
1469 n = memctx.commit()
1470 finally:
1471 store.close()
1472 if opts.get('exact') and nocommit:
1473 # --exact with --no-commit is still useful in that it does merge
1474 # and branch bits
1475 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1476 elif opts.get('exact') and hex(n) != nodeid:
1477 raise error.Abort(_('patch is damaged or loses information'))
1478 msg = _('applied to working directory')
1479 if n:
1480 # i18n: refers to a short changeset id
1481 msg = _('created %s') % short(n)
1482 return (msg, n, rejects)
1483 finally:
1484 os.unlink(tmpname)
1485
1486 # facility to let extensions include additional data in an exported patch
1487 # list of identifiers to be executed in order
1488 extraexport = []
1489 # mapping from identifier to actual export function
1490 # function as to return a string to be added to the header or None
1491 # it is given two arguments (sequencenumber, changectx)
1492 extraexportmap = {}
1493
1494 def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, diffopts):
1495 node = scmutil.binnode(ctx)
1496 parents = [p.node() for p in ctx.parents() if p]
1497 branch = ctx.branch()
1498 if switch_parent:
1499 parents.reverse()
1500
1501 if parents:
1502 prev = parents[0]
1503 else:
1504 prev = nullid
1505
1506 write("# HG changeset patch\n")
1507 write("# User %s\n" % ctx.user())
1508 write("# Date %d %d\n" % ctx.date())
1509 write("# %s\n" % util.datestr(ctx.date()))
1510 if branch and branch != 'default':
1511 write("# Branch %s\n" % branch)
1512 write("# Node ID %s\n" % hex(node))
1513 write("# Parent %s\n" % hex(prev))
1514 if len(parents) > 1:
1515 write("# Parent %s\n" % hex(parents[1]))
1516
1517 for headerid in extraexport:
1518 header = extraexportmap[headerid](seqno, ctx)
1519 if header is not None:
1520 write('# %s\n' % header)
1521 write(ctx.description().rstrip())
1522 write("\n\n")
1523
1524 for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
1525 write(chunk, label=label)
1526
1527 def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
1528 opts=None, match=None):
1529 '''export changesets as hg patches
1530
1531 Args:
1532 repo: The repository from which we're exporting revisions.
1533 revs: A list of revisions to export as revision numbers.
1534 fntemplate: An optional string to use for generating patch file names.
1535 fp: An optional file-like object to which patches should be written.
1536 switch_parent: If True, show diffs against second parent when not nullid.
1537 Default is false, which always shows diff against p1.
1538 opts: diff options to use for generating the patch.
1539 match: If specified, only export changes to files matching this matcher.
1540
1541 Returns:
1542 Nothing.
1543
1544 Side Effect:
1545 "HG Changeset Patch" data is emitted to one of the following
1546 destinations:
1547 fp is specified: All revs are written to the specified
1548 file-like object.
1549 fntemplate specified: Each rev is written to a unique file named using
1550 the given template.
1551 Neither fp nor template specified: All revs written to repo.ui.write()
1552 '''
1553
1554 total = len(revs)
1555 revwidth = max(len(str(rev)) for rev in revs)
1556 filemode = {}
1557
1558 write = None
1559 dest = '<unnamed>'
1560 if fp:
1561 dest = getattr(fp, 'name', dest)
1562 def write(s, **kw):
1563 fp.write(s)
1564 elif not fntemplate:
1565 write = repo.ui.write
1566
1567 for seqno, rev in enumerate(revs, 1):
1568 ctx = repo[rev]
1569 fo = None
1570 if not fp and fntemplate:
1571 desc_lines = ctx.description().rstrip().split('\n')
1572 desc = desc_lines[0] #Commit always has a first line.
1573 fo = makefileobj(repo, fntemplate, ctx.node(), desc=desc,
1574 total=total, seqno=seqno, revwidth=revwidth,
1575 mode='wb', modemap=filemode)
1576 dest = fo.name
1577 def write(s, **kw):
1578 fo.write(s)
1579 if not dest.startswith('<'):
1580 repo.ui.note("%s\n" % dest)
1581 _exportsingle(
1582 repo, ctx, match, switch_parent, rev, seqno, write, opts)
1583 if fo is not None:
1584 fo.close()
1585
1586 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
53 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1587 changes=None, stat=False, fp=None, prefix='',
54 changes=None, stat=False, fp=None, prefix='',
1588 root='', listsubrepos=False, hunksfilterfn=None):
55 root='', listsubrepos=False, hunksfilterfn=None):
@@ -2077,358 +544,6 b' def show_changeset(ui, repo, opts, buffe'
2077
544
2078 return changeset_templater(ui, repo, spec, match, opts, buffered)
545 return changeset_templater(ui, repo, spec, match, opts, buffered)
2079
546
2080 class _regrettablereprbytes(bytes):
2081 """Bytes subclass that makes the repr the same on Python 3 as Python 2.
2082
2083 This is a huge hack.
2084 """
2085 def __repr__(self):
2086 return repr(pycompat.sysstr(self))
2087
2088 def _maybebytestr(v):
2089 if pycompat.ispy3 and isinstance(v, bytes):
2090 return _regrettablereprbytes(v)
2091 return v
2092
2093 def showmarker(fm, marker, index=None):
2094 """utility function to display obsolescence marker in a readable way
2095
2096 To be used by debug function."""
2097 if index is not None:
2098 fm.write('index', '%i ', index)
2099 fm.write('prednode', '%s ', hex(marker.prednode()))
2100 succs = marker.succnodes()
2101 fm.condwrite(succs, 'succnodes', '%s ',
2102 fm.formatlist(map(hex, succs), name='node'))
2103 fm.write('flag', '%X ', marker.flags())
2104 parents = marker.parentnodes()
2105 if parents is not None:
2106 fm.write('parentnodes', '{%s} ',
2107 fm.formatlist(map(hex, parents), name='node', sep=', '))
2108 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
2109 meta = marker.metadata().copy()
2110 meta.pop('date', None)
2111 smeta = {_maybebytestr(k): _maybebytestr(v) for k, v in meta.iteritems()}
2112 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
2113 fm.plain('\n')
2114
2115 def finddate(ui, repo, date):
2116 """Find the tipmost changeset that matches the given date spec"""
2117
2118 df = util.matchdate(date)
2119 m = scmutil.matchall(repo)
2120 results = {}
2121
2122 def prep(ctx, fns):
2123 d = ctx.date()
2124 if df(d[0]):
2125 results[ctx.rev()] = d
2126
2127 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
2128 rev = ctx.rev()
2129 if rev in results:
2130 ui.status(_("found revision %s from %s\n") %
2131 (rev, util.datestr(results[rev])))
2132 return '%d' % rev
2133
2134 raise error.Abort(_("revision matching date not found"))
2135
2136 def increasingwindows(windowsize=8, sizelimit=512):
2137 while True:
2138 yield windowsize
2139 if windowsize < sizelimit:
2140 windowsize *= 2
2141
2142 def _walkrevs(repo, opts):
2143 # Default --rev value depends on --follow but --follow behavior
2144 # depends on revisions resolved from --rev...
2145 follow = opts.get('follow') or opts.get('follow_first')
2146 if opts.get('rev'):
2147 revs = scmutil.revrange(repo, opts['rev'])
2148 elif follow and repo.dirstate.p1() == nullid:
2149 revs = smartset.baseset()
2150 elif follow:
2151 revs = repo.revs('reverse(:.)')
2152 else:
2153 revs = smartset.spanset(repo)
2154 revs.reverse()
2155 return revs
2156
2157 class FileWalkError(Exception):
2158 pass
2159
2160 def walkfilerevs(repo, match, follow, revs, fncache):
2161 '''Walks the file history for the matched files.
2162
2163 Returns the changeset revs that are involved in the file history.
2164
2165 Throws FileWalkError if the file history can't be walked using
2166 filelogs alone.
2167 '''
2168 wanted = set()
2169 copies = []
2170 minrev, maxrev = min(revs), max(revs)
2171 def filerevgen(filelog, last):
2172 """
2173 Only files, no patterns. Check the history of each file.
2174
2175 Examines filelog entries within minrev, maxrev linkrev range
2176 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
2177 tuples in backwards order
2178 """
2179 cl_count = len(repo)
2180 revs = []
2181 for j in xrange(0, last + 1):
2182 linkrev = filelog.linkrev(j)
2183 if linkrev < minrev:
2184 continue
2185 # only yield rev for which we have the changelog, it can
2186 # happen while doing "hg log" during a pull or commit
2187 if linkrev >= cl_count:
2188 break
2189
2190 parentlinkrevs = []
2191 for p in filelog.parentrevs(j):
2192 if p != nullrev:
2193 parentlinkrevs.append(filelog.linkrev(p))
2194 n = filelog.node(j)
2195 revs.append((linkrev, parentlinkrevs,
2196 follow and filelog.renamed(n)))
2197
2198 return reversed(revs)
2199 def iterfiles():
2200 pctx = repo['.']
2201 for filename in match.files():
2202 if follow:
2203 if filename not in pctx:
2204 raise error.Abort(_('cannot follow file not in parent '
2205 'revision: "%s"') % filename)
2206 yield filename, pctx[filename].filenode()
2207 else:
2208 yield filename, None
2209 for filename_node in copies:
2210 yield filename_node
2211
2212 for file_, node in iterfiles():
2213 filelog = repo.file(file_)
2214 if not len(filelog):
2215 if node is None:
2216 # A zero count may be a directory or deleted file, so
2217 # try to find matching entries on the slow path.
2218 if follow:
2219 raise error.Abort(
2220 _('cannot follow nonexistent file: "%s"') % file_)
2221 raise FileWalkError("Cannot walk via filelog")
2222 else:
2223 continue
2224
2225 if node is None:
2226 last = len(filelog) - 1
2227 else:
2228 last = filelog.rev(node)
2229
2230 # keep track of all ancestors of the file
2231 ancestors = {filelog.linkrev(last)}
2232
2233 # iterate from latest to oldest revision
2234 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
2235 if not follow:
2236 if rev > maxrev:
2237 continue
2238 else:
2239 # Note that last might not be the first interesting
2240 # rev to us:
2241 # if the file has been changed after maxrev, we'll
2242 # have linkrev(last) > maxrev, and we still need
2243 # to explore the file graph
2244 if rev not in ancestors:
2245 continue
2246 # XXX insert 1327 fix here
2247 if flparentlinkrevs:
2248 ancestors.update(flparentlinkrevs)
2249
2250 fncache.setdefault(rev, []).append(file_)
2251 wanted.add(rev)
2252 if copied:
2253 copies.append(copied)
2254
2255 return wanted
2256
2257 class _followfilter(object):
2258 def __init__(self, repo, onlyfirst=False):
2259 self.repo = repo
2260 self.startrev = nullrev
2261 self.roots = set()
2262 self.onlyfirst = onlyfirst
2263
2264 def match(self, rev):
2265 def realparents(rev):
2266 if self.onlyfirst:
2267 return self.repo.changelog.parentrevs(rev)[0:1]
2268 else:
2269 return filter(lambda x: x != nullrev,
2270 self.repo.changelog.parentrevs(rev))
2271
2272 if self.startrev == nullrev:
2273 self.startrev = rev
2274 return True
2275
2276 if rev > self.startrev:
2277 # forward: all descendants
2278 if not self.roots:
2279 self.roots.add(self.startrev)
2280 for parent in realparents(rev):
2281 if parent in self.roots:
2282 self.roots.add(rev)
2283 return True
2284 else:
2285 # backwards: all parents
2286 if not self.roots:
2287 self.roots.update(realparents(self.startrev))
2288 if rev in self.roots:
2289 self.roots.remove(rev)
2290 self.roots.update(realparents(rev))
2291 return True
2292
2293 return False
2294
2295 def walkchangerevs(repo, match, opts, prepare):
2296 '''Iterate over files and the revs in which they changed.
2297
2298 Callers most commonly need to iterate backwards over the history
2299 in which they are interested. Doing so has awful (quadratic-looking)
2300 performance, so we use iterators in a "windowed" way.
2301
2302 We walk a window of revisions in the desired order. Within the
2303 window, we first walk forwards to gather data, then in the desired
2304 order (usually backwards) to display it.
2305
2306 This function returns an iterator yielding contexts. Before
2307 yielding each context, the iterator will first call the prepare
2308 function on each context in the window in forward order.'''
2309
2310 follow = opts.get('follow') or opts.get('follow_first')
2311 revs = _walkrevs(repo, opts)
2312 if not revs:
2313 return []
2314 wanted = set()
2315 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
2316 fncache = {}
2317 change = repo.changectx
2318
2319 # First step is to fill wanted, the set of revisions that we want to yield.
2320 # When it does not induce extra cost, we also fill fncache for revisions in
2321 # wanted: a cache of filenames that were changed (ctx.files()) and that
2322 # match the file filtering conditions.
2323
2324 if match.always():
2325 # No files, no patterns. Display all revs.
2326 wanted = revs
2327 elif not slowpath:
2328 # We only have to read through the filelog to find wanted revisions
2329
2330 try:
2331 wanted = walkfilerevs(repo, match, follow, revs, fncache)
2332 except FileWalkError:
2333 slowpath = True
2334
2335 # We decided to fall back to the slowpath because at least one
2336 # of the paths was not a file. Check to see if at least one of them
2337 # existed in history, otherwise simply return
2338 for path in match.files():
2339 if path == '.' or path in repo.store:
2340 break
2341 else:
2342 return []
2343
2344 if slowpath:
2345 # We have to read the changelog to match filenames against
2346 # changed files
2347
2348 if follow:
2349 raise error.Abort(_('can only follow copies/renames for explicit '
2350 'filenames'))
2351
2352 # The slow path checks files modified in every changeset.
2353 # This is really slow on large repos, so compute the set lazily.
2354 class lazywantedset(object):
2355 def __init__(self):
2356 self.set = set()
2357 self.revs = set(revs)
2358
2359 # No need to worry about locality here because it will be accessed
2360 # in the same order as the increasing window below.
2361 def __contains__(self, value):
2362 if value in self.set:
2363 return True
2364 elif not value in self.revs:
2365 return False
2366 else:
2367 self.revs.discard(value)
2368 ctx = change(value)
2369 matches = filter(match, ctx.files())
2370 if matches:
2371 fncache[value] = matches
2372 self.set.add(value)
2373 return True
2374 return False
2375
2376 def discard(self, value):
2377 self.revs.discard(value)
2378 self.set.discard(value)
2379
2380 wanted = lazywantedset()
2381
2382 # it might be worthwhile to do this in the iterator if the rev range
2383 # is descending and the prune args are all within that range
2384 for rev in opts.get('prune', ()):
2385 rev = repo[rev].rev()
2386 ff = _followfilter(repo)
2387 stop = min(revs[0], revs[-1])
2388 for x in xrange(rev, stop - 1, -1):
2389 if ff.match(x):
2390 wanted = wanted - [x]
2391
2392 # Now that wanted is correctly initialized, we can iterate over the
2393 # revision range, yielding only revisions in wanted.
2394 def iterate():
2395 if follow and match.always():
2396 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
2397 def want(rev):
2398 return ff.match(rev) and rev in wanted
2399 else:
2400 def want(rev):
2401 return rev in wanted
2402
2403 it = iter(revs)
2404 stopiteration = False
2405 for windowsize in increasingwindows():
2406 nrevs = []
2407 for i in xrange(windowsize):
2408 rev = next(it, None)
2409 if rev is None:
2410 stopiteration = True
2411 break
2412 elif want(rev):
2413 nrevs.append(rev)
2414 for rev in sorted(nrevs):
2415 fns = fncache.get(rev)
2416 ctx = change(rev)
2417 if not fns:
2418 def fns_generator():
2419 for f in ctx.files():
2420 if match(f):
2421 yield f
2422 fns = fns_generator()
2423 prepare(ctx, fns)
2424 for rev in nrevs:
2425 yield change(rev)
2426
2427 if stopiteration:
2428 break
2429
2430 return iterate()
2431
2432 def _makelogmatcher(repo, revs, pats, opts):
547 def _makelogmatcher(repo, revs, pats, opts):
2433 """Build matcher and expanded patterns from log options
548 """Build matcher and expanded patterns from log options
2434
549
@@ -2816,1226 +931,3 b' def graphrevs(repo, nodes, opts):'
2816 if limit is not None:
931 if limit is not None:
2817 nodes = nodes[:limit]
932 nodes = nodes[:limit]
2818 return graphmod.nodes(repo, nodes)
933 return graphmod.nodes(repo, nodes)
2819
2820 def add(ui, repo, match, prefix, explicitonly, **opts):
2821 join = lambda f: os.path.join(prefix, f)
2822 bad = []
2823
2824 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2825 names = []
2826 wctx = repo[None]
2827 cca = None
2828 abort, warn = scmutil.checkportabilityalert(ui)
2829 if abort or warn:
2830 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2831
2832 badmatch = matchmod.badmatch(match, badfn)
2833 dirstate = repo.dirstate
2834 # We don't want to just call wctx.walk here, since it would return a lot of
2835 # clean files, which we aren't interested in and takes time.
2836 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
2837 unknown=True, ignored=False, full=False)):
2838 exact = match.exact(f)
2839 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2840 if cca:
2841 cca(f)
2842 names.append(f)
2843 if ui.verbose or not exact:
2844 ui.status(_('adding %s\n') % match.rel(f))
2845
2846 for subpath in sorted(wctx.substate):
2847 sub = wctx.sub(subpath)
2848 try:
2849 submatch = matchmod.subdirmatcher(subpath, match)
2850 if opts.get(r'subrepos'):
2851 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2852 else:
2853 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2854 except error.LookupError:
2855 ui.status(_("skipping missing subrepository: %s\n")
2856 % join(subpath))
2857
2858 if not opts.get(r'dry_run'):
2859 rejected = wctx.add(names, prefix)
2860 bad.extend(f for f in rejected if f in match.files())
2861 return bad
2862
2863 def addwebdirpath(repo, serverpath, webconf):
2864 webconf[serverpath] = repo.root
2865 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2866
2867 for r in repo.revs('filelog("path:.hgsub")'):
2868 ctx = repo[r]
2869 for subpath in ctx.substate:
2870 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2871
2872 def forget(ui, repo, match, prefix, explicitonly):
2873 join = lambda f: os.path.join(prefix, f)
2874 bad = []
2875 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2876 wctx = repo[None]
2877 forgot = []
2878
2879 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2880 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2881 if explicitonly:
2882 forget = [f for f in forget if match.exact(f)]
2883
2884 for subpath in sorted(wctx.substate):
2885 sub = wctx.sub(subpath)
2886 try:
2887 submatch = matchmod.subdirmatcher(subpath, match)
2888 subbad, subforgot = sub.forget(submatch, prefix)
2889 bad.extend([subpath + '/' + f for f in subbad])
2890 forgot.extend([subpath + '/' + f for f in subforgot])
2891 except error.LookupError:
2892 ui.status(_("skipping missing subrepository: %s\n")
2893 % join(subpath))
2894
2895 if not explicitonly:
2896 for f in match.files():
2897 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2898 if f not in forgot:
2899 if repo.wvfs.exists(f):
2900 # Don't complain if the exact case match wasn't given.
2901 # But don't do this until after checking 'forgot', so
2902 # that subrepo files aren't normalized, and this op is
2903 # purely from data cached by the status walk above.
2904 if repo.dirstate.normalize(f) in repo.dirstate:
2905 continue
2906 ui.warn(_('not removing %s: '
2907 'file is already untracked\n')
2908 % match.rel(f))
2909 bad.append(f)
2910
2911 for f in forget:
2912 if ui.verbose or not match.exact(f):
2913 ui.status(_('removing %s\n') % match.rel(f))
2914
2915 rejected = wctx.forget(forget, prefix)
2916 bad.extend(f for f in rejected if f in match.files())
2917 forgot.extend(f for f in forget if f not in rejected)
2918 return bad, forgot
2919
2920 def files(ui, ctx, m, fm, fmt, subrepos):
2921 rev = ctx.rev()
2922 ret = 1
2923 ds = ctx.repo().dirstate
2924
2925 for f in ctx.matches(m):
2926 if rev is None and ds[f] == 'r':
2927 continue
2928 fm.startitem()
2929 if ui.verbose:
2930 fc = ctx[f]
2931 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2932 fm.data(abspath=f)
2933 fm.write('path', fmt, m.rel(f))
2934 ret = 0
2935
2936 for subpath in sorted(ctx.substate):
2937 submatch = matchmod.subdirmatcher(subpath, m)
2938 if (subrepos or m.exact(subpath) or any(submatch.files())):
2939 sub = ctx.sub(subpath)
2940 try:
2941 recurse = m.exact(subpath) or subrepos
2942 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2943 ret = 0
2944 except error.LookupError:
2945 ui.status(_("skipping missing subrepository: %s\n")
2946 % m.abs(subpath))
2947
2948 return ret
2949
2950 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2951 join = lambda f: os.path.join(prefix, f)
2952 ret = 0
2953 s = repo.status(match=m, clean=True)
2954 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2955
2956 wctx = repo[None]
2957
2958 if warnings is None:
2959 warnings = []
2960 warn = True
2961 else:
2962 warn = False
2963
2964 subs = sorted(wctx.substate)
2965 total = len(subs)
2966 count = 0
2967 for subpath in subs:
2968 count += 1
2969 submatch = matchmod.subdirmatcher(subpath, m)
2970 if subrepos or m.exact(subpath) or any(submatch.files()):
2971 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2972 sub = wctx.sub(subpath)
2973 try:
2974 if sub.removefiles(submatch, prefix, after, force, subrepos,
2975 warnings):
2976 ret = 1
2977 except error.LookupError:
2978 warnings.append(_("skipping missing subrepository: %s\n")
2979 % join(subpath))
2980 ui.progress(_('searching'), None)
2981
2982 # warn about failure to delete explicit files/dirs
2983 deleteddirs = util.dirs(deleted)
2984 files = m.files()
2985 total = len(files)
2986 count = 0
2987 for f in files:
2988 def insubrepo():
2989 for subpath in wctx.substate:
2990 if f.startswith(subpath + '/'):
2991 return True
2992 return False
2993
2994 count += 1
2995 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2996 isdir = f in deleteddirs or wctx.hasdir(f)
2997 if (f in repo.dirstate or isdir or f == '.'
2998 or insubrepo() or f in subs):
2999 continue
3000
3001 if repo.wvfs.exists(f):
3002 if repo.wvfs.isdir(f):
3003 warnings.append(_('not removing %s: no tracked files\n')
3004 % m.rel(f))
3005 else:
3006 warnings.append(_('not removing %s: file is untracked\n')
3007 % m.rel(f))
3008 # missing files will generate a warning elsewhere
3009 ret = 1
3010 ui.progress(_('deleting'), None)
3011
3012 if force:
3013 list = modified + deleted + clean + added
3014 elif after:
3015 list = deleted
3016 remaining = modified + added + clean
3017 total = len(remaining)
3018 count = 0
3019 for f in remaining:
3020 count += 1
3021 ui.progress(_('skipping'), count, total=total, unit=_('files'))
3022 if ui.verbose or (f in files):
3023 warnings.append(_('not removing %s: file still exists\n')
3024 % m.rel(f))
3025 ret = 1
3026 ui.progress(_('skipping'), None)
3027 else:
3028 list = deleted + clean
3029 total = len(modified) + len(added)
3030 count = 0
3031 for f in modified:
3032 count += 1
3033 ui.progress(_('skipping'), count, total=total, unit=_('files'))
3034 warnings.append(_('not removing %s: file is modified (use -f'
3035 ' to force removal)\n') % m.rel(f))
3036 ret = 1
3037 for f in added:
3038 count += 1
3039 ui.progress(_('skipping'), count, total=total, unit=_('files'))
3040 warnings.append(_("not removing %s: file has been marked for add"
3041 " (use 'hg forget' to undo add)\n") % m.rel(f))
3042 ret = 1
3043 ui.progress(_('skipping'), None)
3044
3045 list = sorted(list)
3046 total = len(list)
3047 count = 0
3048 for f in list:
3049 count += 1
3050 if ui.verbose or not m.exact(f):
3051 ui.progress(_('deleting'), count, total=total, unit=_('files'))
3052 ui.status(_('removing %s\n') % m.rel(f))
3053 ui.progress(_('deleting'), None)
3054
3055 with repo.wlock():
3056 if not after:
3057 for f in list:
3058 if f in added:
3059 continue # we never unlink added files on remove
3060 repo.wvfs.unlinkpath(f, ignoremissing=True)
3061 repo[None].forget(list)
3062
3063 if warn:
3064 for warning in warnings:
3065 ui.warn(warning)
3066
3067 return ret
3068
3069 def _updatecatformatter(fm, ctx, matcher, path, decode):
3070 """Hook for adding data to the formatter used by ``hg cat``.
3071
3072 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
3073 this method first."""
3074 data = ctx[path].data()
3075 if decode:
3076 data = ctx.repo().wwritedata(path, data)
3077 fm.startitem()
3078 fm.write('data', '%s', data)
3079 fm.data(abspath=path, path=matcher.rel(path))
3080
3081 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
3082 err = 1
3083 opts = pycompat.byteskwargs(opts)
3084
3085 def write(path):
3086 filename = None
3087 if fntemplate:
3088 filename = makefilename(repo, fntemplate, ctx.node(),
3089 pathname=os.path.join(prefix, path))
3090 # attempt to create the directory if it does not already exist
3091 try:
3092 os.makedirs(os.path.dirname(filename))
3093 except OSError:
3094 pass
3095 with formatter.maybereopen(basefm, filename, opts) as fm:
3096 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
3097
3098 # Automation often uses hg cat on single files, so special case it
3099 # for performance to avoid the cost of parsing the manifest.
3100 if len(matcher.files()) == 1 and not matcher.anypats():
3101 file = matcher.files()[0]
3102 mfl = repo.manifestlog
3103 mfnode = ctx.manifestnode()
3104 try:
3105 if mfnode and mfl[mfnode].find(file)[0]:
3106 write(file)
3107 return 0
3108 except KeyError:
3109 pass
3110
3111 for abs in ctx.walk(matcher):
3112 write(abs)
3113 err = 0
3114
3115 for subpath in sorted(ctx.substate):
3116 sub = ctx.sub(subpath)
3117 try:
3118 submatch = matchmod.subdirmatcher(subpath, matcher)
3119
3120 if not sub.cat(submatch, basefm, fntemplate,
3121 os.path.join(prefix, sub._path),
3122 **pycompat.strkwargs(opts)):
3123 err = 0
3124 except error.RepoLookupError:
3125 ui.status(_("skipping missing subrepository: %s\n")
3126 % os.path.join(prefix, subpath))
3127
3128 return err
3129
3130 def commit(ui, repo, commitfunc, pats, opts):
3131 '''commit the specified files or all outstanding changes'''
3132 date = opts.get('date')
3133 if date:
3134 opts['date'] = util.parsedate(date)
3135 message = logmessage(ui, opts)
3136 matcher = scmutil.match(repo[None], pats, opts)
3137
3138 dsguard = None
3139 # extract addremove carefully -- this function can be called from a command
3140 # that doesn't support addremove
3141 if opts.get('addremove'):
3142 dsguard = dirstateguard.dirstateguard(repo, 'commit')
3143 with dsguard or util.nullcontextmanager():
3144 if dsguard:
3145 if scmutil.addremove(repo, matcher, "", opts) != 0:
3146 raise error.Abort(
3147 _("failed to mark all new/missing files as added/removed"))
3148
3149 return commitfunc(ui, repo, message, matcher, opts)
3150
3151 def samefile(f, ctx1, ctx2):
3152 if f in ctx1.manifest():
3153 a = ctx1.filectx(f)
3154 if f in ctx2.manifest():
3155 b = ctx2.filectx(f)
3156 return (not a.cmp(b)
3157 and a.flags() == b.flags())
3158 else:
3159 return False
3160 else:
3161 return f not in ctx2.manifest()
3162
3163 def amend(ui, repo, old, extra, pats, opts):
3164 # avoid cycle context -> subrepo -> cmdutil
3165 from . import context
3166
3167 # amend will reuse the existing user if not specified, but the obsolete
3168 # marker creation requires that the current user's name is specified.
3169 if obsolete.isenabled(repo, obsolete.createmarkersopt):
3170 ui.username() # raise exception if username not set
3171
3172 ui.note(_('amending changeset %s\n') % old)
3173 base = old.p1()
3174
3175 with repo.wlock(), repo.lock(), repo.transaction('amend'):
3176 # Participating changesets:
3177 #
3178 # wctx o - workingctx that contains changes from working copy
3179 # | to go into amending commit
3180 # |
3181 # old o - changeset to amend
3182 # |
3183 # base o - first parent of the changeset to amend
3184 wctx = repo[None]
3185
3186 # Copy to avoid mutating input
3187 extra = extra.copy()
3188 # Update extra dict from amended commit (e.g. to preserve graft
3189 # source)
3190 extra.update(old.extra())
3191
3192 # Also update it from the from the wctx
3193 extra.update(wctx.extra())
3194
3195 user = opts.get('user') or old.user()
3196 date = opts.get('date') or old.date()
3197
3198 # Parse the date to allow comparison between date and old.date()
3199 date = util.parsedate(date)
3200
3201 if len(old.parents()) > 1:
3202 # ctx.files() isn't reliable for merges, so fall back to the
3203 # slower repo.status() method
3204 files = set([fn for st in repo.status(base, old)[:3]
3205 for fn in st])
3206 else:
3207 files = set(old.files())
3208
3209 # add/remove the files to the working copy if the "addremove" option
3210 # was specified.
3211 matcher = scmutil.match(wctx, pats, opts)
3212 if (opts.get('addremove')
3213 and scmutil.addremove(repo, matcher, "", opts)):
3214 raise error.Abort(
3215 _("failed to mark all new/missing files as added/removed"))
3216
3217 # Check subrepos. This depends on in-place wctx._status update in
3218 # subrepo.precommit(). To minimize the risk of this hack, we do
3219 # nothing if .hgsub does not exist.
3220 if '.hgsub' in wctx or '.hgsub' in old:
3221 from . import subrepo # avoid cycle: cmdutil -> subrepo -> cmdutil
3222 subs, commitsubs, newsubstate = subrepo.precommit(
3223 ui, wctx, wctx._status, matcher)
3224 # amend should abort if commitsubrepos is enabled
3225 assert not commitsubs
3226 if subs:
3227 subrepo.writestate(repo, newsubstate)
3228
3229 filestoamend = set(f for f in wctx.files() if matcher(f))
3230
3231 changes = (len(filestoamend) > 0)
3232 if changes:
3233 # Recompute copies (avoid recording a -> b -> a)
3234 copied = copies.pathcopies(base, wctx, matcher)
3235 if old.p2:
3236 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
3237
3238 # Prune files which were reverted by the updates: if old
3239 # introduced file X and the file was renamed in the working
3240 # copy, then those two files are the same and
3241 # we can discard X from our list of files. Likewise if X
3242 # was removed, it's no longer relevant. If X is missing (aka
3243 # deleted), old X must be preserved.
3244 files.update(filestoamend)
3245 files = [f for f in files if (not samefile(f, wctx, base)
3246 or f in wctx.deleted())]
3247
3248 def filectxfn(repo, ctx_, path):
3249 try:
3250 # If the file being considered is not amongst the files
3251 # to be amended, we should return the file context from the
3252 # old changeset. This avoids issues when only some files in
3253 # the working copy are being amended but there are also
3254 # changes to other files from the old changeset.
3255 if path not in filestoamend:
3256 return old.filectx(path)
3257
3258 # Return None for removed files.
3259 if path in wctx.removed():
3260 return None
3261
3262 fctx = wctx[path]
3263 flags = fctx.flags()
3264 mctx = context.memfilectx(repo, ctx_,
3265 fctx.path(), fctx.data(),
3266 islink='l' in flags,
3267 isexec='x' in flags,
3268 copied=copied.get(path))
3269 return mctx
3270 except KeyError:
3271 return None
3272 else:
3273 ui.note(_('copying changeset %s to %s\n') % (old, base))
3274
3275 # Use version of files as in the old cset
3276 def filectxfn(repo, ctx_, path):
3277 try:
3278 return old.filectx(path)
3279 except KeyError:
3280 return None
3281
3282 # See if we got a message from -m or -l, if not, open the editor with
3283 # the message of the changeset to amend.
3284 message = logmessage(ui, opts)
3285
3286 editform = mergeeditform(old, 'commit.amend')
3287 editor = getcommiteditor(editform=editform,
3288 **pycompat.strkwargs(opts))
3289
3290 if not message:
3291 editor = getcommiteditor(edit=True, editform=editform)
3292 message = old.description()
3293
3294 pureextra = extra.copy()
3295 extra['amend_source'] = old.hex()
3296
3297 new = context.memctx(repo,
3298 parents=[base.node(), old.p2().node()],
3299 text=message,
3300 files=files,
3301 filectxfn=filectxfn,
3302 user=user,
3303 date=date,
3304 extra=extra,
3305 editor=editor)
3306
3307 newdesc = changelog.stripdesc(new.description())
3308 if ((not changes)
3309 and newdesc == old.description()
3310 and user == old.user()
3311 and date == old.date()
3312 and pureextra == old.extra()):
3313 # nothing changed. continuing here would create a new node
3314 # anyway because of the amend_source noise.
3315 #
3316 # This not what we expect from amend.
3317 return old.node()
3318
3319 if opts.get('secret'):
3320 commitphase = 'secret'
3321 else:
3322 commitphase = old.phase()
3323 overrides = {('phases', 'new-commit'): commitphase}
3324 with ui.configoverride(overrides, 'amend'):
3325 newid = repo.commitctx(new)
3326
3327 # Reroute the working copy parent to the new changeset
3328 repo.setparents(newid, nullid)
3329 mapping = {old.node(): (newid,)}
3330 obsmetadata = None
3331 if opts.get('note'):
3332 obsmetadata = {'note': opts['note']}
3333 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata)
3334
3335 # Fixing the dirstate because localrepo.commitctx does not update
3336 # it. This is rather convenient because we did not need to update
3337 # the dirstate for all the files in the new commit which commitctx
3338 # could have done if it updated the dirstate. Now, we can
3339 # selectively update the dirstate only for the amended files.
3340 dirstate = repo.dirstate
3341
3342 # Update the state of the files which were added and
3343 # and modified in the amend to "normal" in the dirstate.
3344 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3345 for f in normalfiles:
3346 dirstate.normal(f)
3347
3348 # Update the state of files which were removed in the amend
3349 # to "removed" in the dirstate.
3350 removedfiles = set(wctx.removed()) & filestoamend
3351 for f in removedfiles:
3352 dirstate.drop(f)
3353
3354 return newid
3355
3356 def commiteditor(repo, ctx, subs, editform=''):
3357 if ctx.description():
3358 return ctx.description()
3359 return commitforceeditor(repo, ctx, subs, editform=editform,
3360 unchangedmessagedetection=True)
3361
3362 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
3363 editform='', unchangedmessagedetection=False):
3364 if not extramsg:
3365 extramsg = _("Leave message empty to abort commit.")
3366
3367 forms = [e for e in editform.split('.') if e]
3368 forms.insert(0, 'changeset')
3369 templatetext = None
3370 while forms:
3371 ref = '.'.join(forms)
3372 if repo.ui.config('committemplate', ref):
3373 templatetext = committext = buildcommittemplate(
3374 repo, ctx, subs, extramsg, ref)
3375 break
3376 forms.pop()
3377 else:
3378 committext = buildcommittext(repo, ctx, subs, extramsg)
3379
3380 # run editor in the repository root
3381 olddir = pycompat.getcwd()
3382 os.chdir(repo.root)
3383
3384 # make in-memory changes visible to external process
3385 tr = repo.currenttransaction()
3386 repo.dirstate.write(tr)
3387 pending = tr and tr.writepending() and repo.root
3388
3389 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
3390 editform=editform, pending=pending,
3391 repopath=repo.path, action='commit')
3392 text = editortext
3393
3394 # strip away anything below this special string (used for editors that want
3395 # to display the diff)
3396 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3397 if stripbelow:
3398 text = text[:stripbelow.start()]
3399
3400 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
3401 os.chdir(olddir)
3402
3403 if finishdesc:
3404 text = finishdesc(text)
3405 if not text.strip():
3406 raise error.Abort(_("empty commit message"))
3407 if unchangedmessagedetection and editortext == templatetext:
3408 raise error.Abort(_("commit message unchanged"))
3409
3410 return text
3411
3412 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3413 ui = repo.ui
3414 spec = formatter.templatespec(ref, None, None)
3415 t = changeset_templater(ui, repo, spec, None, {}, False)
3416 t.t.cache.update((k, templater.unquotestring(v))
3417 for k, v in repo.ui.configitems('committemplate'))
3418
3419 if not extramsg:
3420 extramsg = '' # ensure that extramsg is string
3421
3422 ui.pushbuffer()
3423 t.show(ctx, extramsg=extramsg)
3424 return ui.popbuffer()
3425
3426 def hgprefix(msg):
3427 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
3428
3429 def buildcommittext(repo, ctx, subs, extramsg):
3430 edittext = []
3431 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3432 if ctx.description():
3433 edittext.append(ctx.description())
3434 edittext.append("")
3435 edittext.append("") # Empty line between message and comments.
3436 edittext.append(hgprefix(_("Enter commit message."
3437 " Lines beginning with 'HG:' are removed.")))
3438 edittext.append(hgprefix(extramsg))
3439 edittext.append("HG: --")
3440 edittext.append(hgprefix(_("user: %s") % ctx.user()))
3441 if ctx.p2():
3442 edittext.append(hgprefix(_("branch merge")))
3443 if ctx.branch():
3444 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
3445 if bookmarks.isactivewdirparent(repo):
3446 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
3447 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
3448 edittext.extend([hgprefix(_("added %s") % f) for f in added])
3449 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
3450 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
3451 if not added and not modified and not removed:
3452 edittext.append(hgprefix(_("no files changed")))
3453 edittext.append("")
3454
3455 return "\n".join(edittext)
3456
3457 def commitstatus(repo, node, branch, bheads=None, opts=None):
3458 if opts is None:
3459 opts = {}
3460 ctx = repo[node]
3461 parents = ctx.parents()
3462
3463 if (not opts.get('amend') and bheads and node not in bheads and not
3464 [x for x in parents if x.node() in bheads and x.branch() == branch]):
3465 repo.ui.status(_('created new head\n'))
3466 # The message is not printed for initial roots. For the other
3467 # changesets, it is printed in the following situations:
3468 #
3469 # Par column: for the 2 parents with ...
3470 # N: null or no parent
3471 # B: parent is on another named branch
3472 # C: parent is a regular non head changeset
3473 # H: parent was a branch head of the current branch
3474 # Msg column: whether we print "created new head" message
3475 # In the following, it is assumed that there already exists some
3476 # initial branch heads of the current branch, otherwise nothing is
3477 # printed anyway.
3478 #
3479 # Par Msg Comment
3480 # N N y additional topo root
3481 #
3482 # B N y additional branch root
3483 # C N y additional topo head
3484 # H N n usual case
3485 #
3486 # B B y weird additional branch root
3487 # C B y branch merge
3488 # H B n merge with named branch
3489 #
3490 # C C y additional head from merge
3491 # C H n merge with a head
3492 #
3493 # H H n head merge: head count decreases
3494
3495 if not opts.get('close_branch'):
3496 for r in parents:
3497 if r.closesbranch() and r.branch() == branch:
3498 repo.ui.status(_('reopening closed branch head %d\n') % r)
3499
3500 if repo.ui.debugflag:
3501 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
3502 elif repo.ui.verbose:
3503 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
3504
3505 def postcommitstatus(repo, pats, opts):
3506 return repo.status(match=scmutil.match(repo[None], pats, opts))
3507
3508 def revert(ui, repo, ctx, parents, *pats, **opts):
3509 opts = pycompat.byteskwargs(opts)
3510 parent, p2 = parents
3511 node = ctx.node()
3512
3513 mf = ctx.manifest()
3514 if node == p2:
3515 parent = p2
3516
3517 # need all matching names in dirstate and manifest of target rev,
3518 # so have to walk both. do not print errors if files exist in one
3519 # but not other. in both cases, filesets should be evaluated against
3520 # workingctx to get consistent result (issue4497). this means 'set:**'
3521 # cannot be used to select missing files from target rev.
3522
3523 # `names` is a mapping for all elements in working copy and target revision
3524 # The mapping is in the form:
3525 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3526 names = {}
3527
3528 with repo.wlock():
3529 ## filling of the `names` mapping
3530 # walk dirstate to fill `names`
3531
3532 interactive = opts.get('interactive', False)
3533 wctx = repo[None]
3534 m = scmutil.match(wctx, pats, opts)
3535
3536 # we'll need this later
3537 targetsubs = sorted(s for s in wctx.substate if m(s))
3538
3539 if not m.always():
3540 matcher = matchmod.badmatch(m, lambda x, y: False)
3541 for abs in wctx.walk(matcher):
3542 names[abs] = m.rel(abs), m.exact(abs)
3543
3544 # walk target manifest to fill `names`
3545
3546 def badfn(path, msg):
3547 if path in names:
3548 return
3549 if path in ctx.substate:
3550 return
3551 path_ = path + '/'
3552 for f in names:
3553 if f.startswith(path_):
3554 return
3555 ui.warn("%s: %s\n" % (m.rel(path), msg))
3556
3557 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3558 if abs not in names:
3559 names[abs] = m.rel(abs), m.exact(abs)
3560
3561 # Find status of all file in `names`.
3562 m = scmutil.matchfiles(repo, names)
3563
3564 changes = repo.status(node1=node, match=m,
3565 unknown=True, ignored=True, clean=True)
3566 else:
3567 changes = repo.status(node1=node, match=m)
3568 for kind in changes:
3569 for abs in kind:
3570 names[abs] = m.rel(abs), m.exact(abs)
3571
3572 m = scmutil.matchfiles(repo, names)
3573
3574 modified = set(changes.modified)
3575 added = set(changes.added)
3576 removed = set(changes.removed)
3577 _deleted = set(changes.deleted)
3578 unknown = set(changes.unknown)
3579 unknown.update(changes.ignored)
3580 clean = set(changes.clean)
3581 modadded = set()
3582
3583 # We need to account for the state of the file in the dirstate,
3584 # even when we revert against something else than parent. This will
3585 # slightly alter the behavior of revert (doing back up or not, delete
3586 # or just forget etc).
3587 if parent == node:
3588 dsmodified = modified
3589 dsadded = added
3590 dsremoved = removed
3591 # store all local modifications, useful later for rename detection
3592 localchanges = dsmodified | dsadded
3593 modified, added, removed = set(), set(), set()
3594 else:
3595 changes = repo.status(node1=parent, match=m)
3596 dsmodified = set(changes.modified)
3597 dsadded = set(changes.added)
3598 dsremoved = set(changes.removed)
3599 # store all local modifications, useful later for rename detection
3600 localchanges = dsmodified | dsadded
3601
3602 # only take into account for removes between wc and target
3603 clean |= dsremoved - removed
3604 dsremoved &= removed
3605 # distinct between dirstate remove and other
3606 removed -= dsremoved
3607
3608 modadded = added & dsmodified
3609 added -= modadded
3610
3611 # tell newly modified apart.
3612 dsmodified &= modified
3613 dsmodified |= modified & dsadded # dirstate added may need backup
3614 modified -= dsmodified
3615
3616 # We need to wait for some post-processing to update this set
3617 # before making the distinction. The dirstate will be used for
3618 # that purpose.
3619 dsadded = added
3620
3621 # in case of merge, files that are actually added can be reported as
3622 # modified, we need to post process the result
3623 if p2 != nullid:
3624 mergeadd = set(dsmodified)
3625 for path in dsmodified:
3626 if path in mf:
3627 mergeadd.remove(path)
3628 dsadded |= mergeadd
3629 dsmodified -= mergeadd
3630
3631 # if f is a rename, update `names` to also revert the source
3632 cwd = repo.getcwd()
3633 for f in localchanges:
3634 src = repo.dirstate.copied(f)
3635 # XXX should we check for rename down to target node?
3636 if src and src not in names and repo.dirstate[src] == 'r':
3637 dsremoved.add(src)
3638 names[src] = (repo.pathto(src, cwd), True)
3639
3640 # determine the exact nature of the deleted changesets
3641 deladded = set(_deleted)
3642 for path in _deleted:
3643 if path in mf:
3644 deladded.remove(path)
3645 deleted = _deleted - deladded
3646
3647 # distinguish between file to forget and the other
3648 added = set()
3649 for abs in dsadded:
3650 if repo.dirstate[abs] != 'a':
3651 added.add(abs)
3652 dsadded -= added
3653
3654 for abs in deladded:
3655 if repo.dirstate[abs] == 'a':
3656 dsadded.add(abs)
3657 deladded -= dsadded
3658
3659 # For files marked as removed, we check if an unknown file is present at
3660 # the same path. If a such file exists it may need to be backed up.
3661 # Making the distinction at this stage helps have simpler backup
3662 # logic.
3663 removunk = set()
3664 for abs in removed:
3665 target = repo.wjoin(abs)
3666 if os.path.lexists(target):
3667 removunk.add(abs)
3668 removed -= removunk
3669
3670 dsremovunk = set()
3671 for abs in dsremoved:
3672 target = repo.wjoin(abs)
3673 if os.path.lexists(target):
3674 dsremovunk.add(abs)
3675 dsremoved -= dsremovunk
3676
3677 # action to be actually performed by revert
3678 # (<list of file>, message>) tuple
3679 actions = {'revert': ([], _('reverting %s\n')),
3680 'add': ([], _('adding %s\n')),
3681 'remove': ([], _('removing %s\n')),
3682 'drop': ([], _('removing %s\n')),
3683 'forget': ([], _('forgetting %s\n')),
3684 'undelete': ([], _('undeleting %s\n')),
3685 'noop': (None, _('no changes needed to %s\n')),
3686 'unknown': (None, _('file not managed: %s\n')),
3687 }
3688
3689 # "constant" that convey the backup strategy.
3690 # All set to `discard` if `no-backup` is set do avoid checking
3691 # no_backup lower in the code.
3692 # These values are ordered for comparison purposes
3693 backupinteractive = 3 # do backup if interactively modified
3694 backup = 2 # unconditionally do backup
3695 check = 1 # check if the existing file differs from target
3696 discard = 0 # never do backup
3697 if opts.get('no_backup'):
3698 backupinteractive = backup = check = discard
3699 if interactive:
3700 dsmodifiedbackup = backupinteractive
3701 else:
3702 dsmodifiedbackup = backup
3703 tobackup = set()
3704
3705 backupanddel = actions['remove']
3706 if not opts.get('no_backup'):
3707 backupanddel = actions['drop']
3708
3709 disptable = (
3710 # dispatch table:
3711 # file state
3712 # action
3713 # make backup
3714
3715 ## Sets that results that will change file on disk
3716 # Modified compared to target, no local change
3717 (modified, actions['revert'], discard),
3718 # Modified compared to target, but local file is deleted
3719 (deleted, actions['revert'], discard),
3720 # Modified compared to target, local change
3721 (dsmodified, actions['revert'], dsmodifiedbackup),
3722 # Added since target
3723 (added, actions['remove'], discard),
3724 # Added in working directory
3725 (dsadded, actions['forget'], discard),
3726 # Added since target, have local modification
3727 (modadded, backupanddel, backup),
3728 # Added since target but file is missing in working directory
3729 (deladded, actions['drop'], discard),
3730 # Removed since target, before working copy parent
3731 (removed, actions['add'], discard),
3732 # Same as `removed` but an unknown file exists at the same path
3733 (removunk, actions['add'], check),
3734 # Removed since targe, marked as such in working copy parent
3735 (dsremoved, actions['undelete'], discard),
3736 # Same as `dsremoved` but an unknown file exists at the same path
3737 (dsremovunk, actions['undelete'], check),
3738 ## the following sets does not result in any file changes
3739 # File with no modification
3740 (clean, actions['noop'], discard),
3741 # Existing file, not tracked anywhere
3742 (unknown, actions['unknown'], discard),
3743 )
3744
3745 for abs, (rel, exact) in sorted(names.items()):
3746 # target file to be touch on disk (relative to cwd)
3747 target = repo.wjoin(abs)
3748 # search the entry in the dispatch table.
3749 # if the file is in any of these sets, it was touched in the working
3750 # directory parent and we are sure it needs to be reverted.
3751 for table, (xlist, msg), dobackup in disptable:
3752 if abs not in table:
3753 continue
3754 if xlist is not None:
3755 xlist.append(abs)
3756 if dobackup:
3757 # If in interactive mode, don't automatically create
3758 # .orig files (issue4793)
3759 if dobackup == backupinteractive:
3760 tobackup.add(abs)
3761 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3762 bakname = scmutil.origpath(ui, repo, rel)
3763 ui.note(_('saving current version of %s as %s\n') %
3764 (rel, bakname))
3765 if not opts.get('dry_run'):
3766 if interactive:
3767 util.copyfile(target, bakname)
3768 else:
3769 util.rename(target, bakname)
3770 if ui.verbose or not exact:
3771 if not isinstance(msg, bytes):
3772 msg = msg(abs)
3773 ui.status(msg % rel)
3774 elif exact:
3775 ui.warn(msg % rel)
3776 break
3777
3778 if not opts.get('dry_run'):
3779 needdata = ('revert', 'add', 'undelete')
3780 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3781 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3782
3783 if targetsubs:
3784 # Revert the subrepos on the revert list
3785 for sub in targetsubs:
3786 try:
3787 wctx.sub(sub).revert(ctx.substate[sub], *pats,
3788 **pycompat.strkwargs(opts))
3789 except KeyError:
3790 raise error.Abort("subrepository '%s' does not exist in %s!"
3791 % (sub, short(ctx.node())))
3792
3793 def _revertprefetch(repo, ctx, *files):
3794 """Let extension changing the storage layer prefetch content"""
3795
3796 def _performrevert(repo, parents, ctx, actions, interactive=False,
3797 tobackup=None):
3798 """function that actually perform all the actions computed for revert
3799
3800 This is an independent function to let extension to plug in and react to
3801 the imminent revert.
3802
3803 Make sure you have the working directory locked when calling this function.
3804 """
3805 parent, p2 = parents
3806 node = ctx.node()
3807 excluded_files = []
3808 matcher_opts = {"exclude": excluded_files}
3809
3810 def checkout(f):
3811 fc = ctx[f]
3812 repo.wwrite(f, fc.data(), fc.flags())
3813
3814 def doremove(f):
3815 try:
3816 repo.wvfs.unlinkpath(f)
3817 except OSError:
3818 pass
3819 repo.dirstate.remove(f)
3820
3821 audit_path = pathutil.pathauditor(repo.root, cached=True)
3822 for f in actions['forget'][0]:
3823 if interactive:
3824 choice = repo.ui.promptchoice(
3825 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3826 if choice == 0:
3827 repo.dirstate.drop(f)
3828 else:
3829 excluded_files.append(repo.wjoin(f))
3830 else:
3831 repo.dirstate.drop(f)
3832 for f in actions['remove'][0]:
3833 audit_path(f)
3834 if interactive:
3835 choice = repo.ui.promptchoice(
3836 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3837 if choice == 0:
3838 doremove(f)
3839 else:
3840 excluded_files.append(repo.wjoin(f))
3841 else:
3842 doremove(f)
3843 for f in actions['drop'][0]:
3844 audit_path(f)
3845 repo.dirstate.remove(f)
3846
3847 normal = None
3848 if node == parent:
3849 # We're reverting to our parent. If possible, we'd like status
3850 # to report the file as clean. We have to use normallookup for
3851 # merges to avoid losing information about merged/dirty files.
3852 if p2 != nullid:
3853 normal = repo.dirstate.normallookup
3854 else:
3855 normal = repo.dirstate.normal
3856
3857 newlyaddedandmodifiedfiles = set()
3858 if interactive:
3859 # Prompt the user for changes to revert
3860 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3861 m = scmutil.match(ctx, torevert, matcher_opts)
3862 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3863 diffopts.nodates = True
3864 diffopts.git = True
3865 operation = 'discard'
3866 reversehunks = True
3867 if node != parent:
3868 operation = 'apply'
3869 reversehunks = False
3870 if reversehunks:
3871 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3872 else:
3873 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3874 originalchunks = patch.parsepatch(diff)
3875
3876 try:
3877
3878 chunks, opts = recordfilter(repo.ui, originalchunks,
3879 operation=operation)
3880 if reversehunks:
3881 chunks = patch.reversehunks(chunks)
3882
3883 except error.PatchError as err:
3884 raise error.Abort(_('error parsing patch: %s') % err)
3885
3886 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3887 if tobackup is None:
3888 tobackup = set()
3889 # Apply changes
3890 fp = stringio()
3891 for c in chunks:
3892 # Create a backup file only if this hunk should be backed up
3893 if ishunk(c) and c.header.filename() in tobackup:
3894 abs = c.header.filename()
3895 target = repo.wjoin(abs)
3896 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3897 util.copyfile(target, bakname)
3898 tobackup.remove(abs)
3899 c.write(fp)
3900 dopatch = fp.tell()
3901 fp.seek(0)
3902 if dopatch:
3903 try:
3904 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3905 except error.PatchError as err:
3906 raise error.Abort(str(err))
3907 del fp
3908 else:
3909 for f in actions['revert'][0]:
3910 checkout(f)
3911 if normal:
3912 normal(f)
3913
3914 for f in actions['add'][0]:
3915 # Don't checkout modified files, they are already created by the diff
3916 if f not in newlyaddedandmodifiedfiles:
3917 checkout(f)
3918 repo.dirstate.add(f)
3919
3920 normal = repo.dirstate.normallookup
3921 if node == parent and p2 == nullid:
3922 normal = repo.dirstate.normal
3923 for f in actions['undelete'][0]:
3924 checkout(f)
3925 normal(f)
3926
3927 copied = copies.pathcopies(repo[parent], ctx)
3928
3929 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3930 if f in copied:
3931 repo.dirstate.copy(copied[f], f)
3932
3933 class command(registrar.command):
3934 """deprecated: used registrar.command instead"""
3935 def _doregister(self, func, name, *args, **kwargs):
3936 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3937 return super(command, self)._doregister(func, name, *args, **kwargs)
3938
3939 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3940 # commands.outgoing. "missing" is "missing" of the result of
3941 # "findcommonoutgoing()"
3942 outgoinghooks = util.hooks()
3943
3944 # a list of (ui, repo) functions called by commands.summary
3945 summaryhooks = util.hooks()
3946
3947 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3948 #
3949 # functions should return tuple of booleans below, if 'changes' is None:
3950 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3951 #
3952 # otherwise, 'changes' is a tuple of tuples below:
3953 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3954 # - (desturl, destbranch, destpeer, outgoing)
3955 summaryremotehooks = util.hooks()
3956
3957 # A list of state files kept by multistep operations like graft.
3958 # Since graft cannot be aborted, it is considered 'clearable' by update.
3959 # note: bisect is intentionally excluded
3960 # (state file, clearable, allowcommit, error, hint)
3961 unfinishedstates = [
3962 ('graftstate', True, False, _('graft in progress'),
3963 _("use 'hg graft --continue' or 'hg update' to abort")),
3964 ('updatestate', True, False, _('last update was interrupted'),
3965 _("use 'hg update' to get a consistent checkout"))
3966 ]
3967
3968 def checkunfinished(repo, commit=False):
3969 '''Look for an unfinished multistep operation, like graft, and abort
3970 if found. It's probably good to check this right before
3971 bailifchanged().
3972 '''
3973 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3974 if commit and allowcommit:
3975 continue
3976 if repo.vfs.exists(f):
3977 raise error.Abort(msg, hint=hint)
3978
3979 def clearunfinished(repo):
3980 '''Check for unfinished operations (as above), and clear the ones
3981 that are clearable.
3982 '''
3983 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3984 if not clearable and repo.vfs.exists(f):
3985 raise error.Abort(msg, hint=hint)
3986 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3987 if clearable and repo.vfs.exists(f):
3988 util.unlink(repo.vfs.join(f))
3989
3990 afterresolvedstates = [
3991 ('graftstate',
3992 _('hg graft --continue')),
3993 ]
3994
3995 def howtocontinue(repo):
3996 '''Check for an unfinished operation and return the command to finish
3997 it.
3998
3999 afterresolvedstates tuples define a .hg/{file} and the corresponding
4000 command needed to finish it.
4001
4002 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
4003 a boolean.
4004 '''
4005 contmsg = _("continue: %s")
4006 for f, msg in afterresolvedstates:
4007 if repo.vfs.exists(f):
4008 return contmsg % msg, True
4009 if repo[None].dirty(missing=True, merge=False, branch=False):
4010 return contmsg % _("hg commit"), False
4011 return None, None
4012
4013 def checkafterresolved(repo):
4014 '''Inform the user about the next action after completing hg resolve
4015
4016 If there's a matching afterresolvedstates, howtocontinue will yield
4017 repo.ui.warn as the reporter.
4018
4019 Otherwise, it will yield repo.ui.note.
4020 '''
4021 msg, warning = howtocontinue(repo)
4022 if msg is not None:
4023 if warning:
4024 repo.ui.warn("%s\n" % msg)
4025 else:
4026 repo.ui.note("%s\n" % msg)
4027
4028 def wrongtooltocontinue(repo, task):
4029 '''Raise an abort suggesting how to properly continue if there is an
4030 active task.
4031
4032 Uses howtocontinue() to find the active task.
4033
4034 If there's no task (repo.ui.note for 'hg commit'), it does not offer
4035 a hint.
4036 '''
4037 after = howtocontinue(repo)
4038 hint = None
4039 if after[1]:
4040 hint = after[0]
4041 raise error.Abort(_('no %s in progress') % task, hint=hint)
@@ -87,16 +87,17 b' o (0) root'
87 > cmdutil,
87 > cmdutil,
88 > commands,
88 > commands,
89 > extensions,
89 > extensions,
90 > logcmdutil,
90 > revsetlang,
91 > revsetlang,
91 > smartset,
92 > smartset,
92 > )
93 > )
93 >
94 >
94 > def logrevset(repo, pats, opts):
95 > def logrevset(repo, pats, opts):
95 > revs = cmdutil._logrevs(repo, opts)
96 > revs = logcmdutil._logrevs(repo, opts)
96 > if not revs:
97 > if not revs:
97 > return None
98 > return None
98 > match, pats, slowpath = cmdutil._makelogmatcher(repo, revs, pats, opts)
99 > match, pats, slowpath = logcmdutil._makelogmatcher(repo, revs, pats, opts)
99 > return cmdutil._makelogrevset(repo, match, pats, slowpath, opts)
100 > return logcmdutil._makelogrevset(repo, match, pats, slowpath, opts)
100 >
101 >
101 > def uisetup(ui):
102 > def uisetup(ui):
102 > def printrevset(orig, repo, pats, opts):
103 > def printrevset(orig, repo, pats, opts):
General Comments 0
You need to be logged in to leave comments. Login now