[PATCH 1 of 4] cmdutil: split functions of log-like commands to new module (API)
Yuya Nishihara
yuya at tcha.org
Fri Feb 2 11:47:21 UTC 2018
# HG changeset patch
# User Yuya Nishihara <yuya at tcha.org>
# Date 1516505202 -32400
# Sun Jan 21 12:26:42 2018 +0900
# Node ID bc1c16b34e84ba7f08a80cfd48d43b6b60388282
# Parent 522864fae7f6de76a49f80baf267d455a9a480dd
cmdutil: split functions of log-like commands to new module (API)
cmdutil.py is painfully big and makes Emacs slow. Let's split log-related
functions.
% wc -l mercurial/cmdutil.py
4027 mercurial/cmdutil.py
% wc -l mercurial/cmdutil.py mercurial/logcmdutil.py
3141 mercurial/cmdutil.py
933 mercurial/logcmdutil.py
4074 total
diff --git a/hgext/largefiles/overrides.py b/hgext/largefiles/overrides.py
--- a/hgext/largefiles/overrides.py
+++ b/hgext/largefiles/overrides.py
@@ -19,6 +19,7 @@ from mercurial import (
cmdutil,
error,
hg,
+ logcmdutil,
match as matchmod,
pathutil,
pycompat,
@@ -394,14 +395,16 @@ def overridelog(orig, ui, repo, *pats, *
return lambda rev: match
oldmatchandpats = installmatchandpatsfn(overridematchandpats)
- oldmakelogfilematcher = cmdutil._makenofollowlogfilematcher
- setattr(cmdutil, '_makenofollowlogfilematcher', overridemakelogfilematcher)
+ oldmakelogfilematcher = logcmdutil._makenofollowlogfilematcher
+ setattr(logcmdutil, '_makenofollowlogfilematcher',
+ overridemakelogfilematcher)
try:
return orig(ui, repo, *pats, **opts)
finally:
restorematchandpatsfn()
- setattr(cmdutil, '_makenofollowlogfilematcher', oldmakelogfilematcher)
+ setattr(logcmdutil, '_makenofollowlogfilematcher',
+ oldmakelogfilematcher)
def overrideverify(orig, ui, repo, *pats, **opts):
large = opts.pop(r'large', False)
diff --git a/hgext/sparse.py b/hgext/sparse.py
--- a/hgext/sparse.py
+++ b/hgext/sparse.py
@@ -75,12 +75,12 @@ from __future__ import absolute_import
from mercurial.i18n import _
from mercurial import (
- cmdutil,
commands,
dirstate,
error,
extensions,
hg,
+ logcmdutil,
match as matchmod,
pycompat,
registrar,
@@ -135,7 +135,7 @@ def _setuplog(ui):
return any(f for f in ctx.files() if sparsematch(f))
revs = revs.filter(ctxmatch)
return revs
- extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
+ extensions.wrapfunction(logcmdutil, '_logrevs', _logrevs)
def _clonesparsecmd(orig, ui, repo, *args, **opts):
include_pat = opts.get('include')
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -8,7 +8,6 @@
from __future__ import absolute_import
import errno
-import itertools
import os
import re
import tempfile
@@ -26,32 +25,43 @@ from . import (
changelog,
copies,
crecord as crecordmod,
- dagop,
dirstateguard,
encoding,
error,
formatter,
- graphmod,
+ logcmdutil,
match as matchmod,
- mdiff,
obsolete,
patch,
pathutil,
pycompat,
registrar,
revlog,
- revset,
- revsetlang,
rewriteutil,
scmutil,
smartset,
- templatekw,
templater,
util,
vfs as vfsmod,
)
stringio = util.stringio
+loglimit = logcmdutil.loglimit
+diffordiffstat = logcmdutil.diffordiffstat
+_changesetlabels = logcmdutil._changesetlabels
+changeset_printer = logcmdutil.changeset_printer
+jsonchangeset = logcmdutil.jsonchangeset
+changeset_templater = logcmdutil.changeset_templater
+logtemplatespec = logcmdutil.logtemplatespec
+makelogtemplater = logcmdutil.makelogtemplater
+show_changeset = logcmdutil.show_changeset
+getlogrevs = logcmdutil.getlogrevs
+getloglinerangerevs = logcmdutil.getloglinerangerevs
+displaygraph = logcmdutil.displaygraph
+graphlog = logcmdutil.graphlog
+checkunsupportedgraphflags = logcmdutil.checkunsupportedgraphflags
+graphrevs = logcmdutil.graphrevs
+
# templates of common command options
dryrunopts = [
@@ -898,20 +908,6 @@ def getcommiteditor(edit=False, finishde
else:
return commiteditor
-def loglimit(opts):
- """get the log limit according to option -l/--limit"""
- limit = opts.get('limit')
- if limit:
- try:
- limit = int(limit)
- except ValueError:
- raise error.Abort(_('limit must be a positive integer'))
- if limit <= 0:
- raise error.Abort(_('limit must be positive'))
- else:
- limit = None
- return limit
-
def makefilename(repo, pat, node, desc=None,
total=None, seqno=None, revwidth=None, pathname=None):
node_expander = {
@@ -1583,500 +1579,6 @@ def export(repo, revs, fntemplate='hg-%h
if fo is not None:
fo.close()
-def diffordiffstat(ui, repo, diffopts, node1, node2, match,
- changes=None, stat=False, fp=None, prefix='',
- root='', listsubrepos=False, hunksfilterfn=None):
- '''show diff or diffstat.'''
- if fp is None:
- write = ui.write
- else:
- def write(s, **kw):
- fp.write(s)
-
- if root:
- relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
- else:
- relroot = ''
- if relroot != '':
- # XXX relative roots currently don't work if the root is within a
- # subrepo
- uirelroot = match.uipath(relroot)
- relroot += '/'
- for matchroot in match.files():
- if not matchroot.startswith(relroot):
- ui.warn(_('warning: %s not inside relative root %s\n') % (
- match.uipath(matchroot), uirelroot))
-
- if stat:
- diffopts = diffopts.copy(context=0, noprefix=False)
- width = 80
- if not ui.plain():
- width = ui.termwidth()
- chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
- prefix=prefix, relroot=relroot,
- hunksfilterfn=hunksfilterfn)
- for chunk, label in patch.diffstatui(util.iterlines(chunks),
- width=width):
- write(chunk, label=label)
- else:
- for chunk, label in patch.diffui(repo, node1, node2, match,
- changes, opts=diffopts, prefix=prefix,
- relroot=relroot,
- hunksfilterfn=hunksfilterfn):
- write(chunk, label=label)
-
- if listsubrepos:
- ctx1 = repo[node1]
- ctx2 = repo[node2]
- for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
- tempnode2 = node2
- try:
- if node2 is not None:
- tempnode2 = ctx2.substate[subpath][1]
- except KeyError:
- # A subrepo that existed in node1 was deleted between node1 and
- # node2 (inclusive). Thus, ctx2's substate won't contain that
- # subpath. The best we can do is to ignore it.
- tempnode2 = None
- submatch = matchmod.subdirmatcher(subpath, match)
- sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
- stat=stat, fp=fp, prefix=prefix)
-
-def _changesetlabels(ctx):
- labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
- if ctx.obsolete():
- labels.append('changeset.obsolete')
- if ctx.isunstable():
- labels.append('changeset.unstable')
- for instability in ctx.instabilities():
- labels.append('instability.%s' % instability)
- return ' '.join(labels)
-
-class changeset_printer(object):
- '''show changeset information when templating not requested.'''
-
- def __init__(self, ui, repo, matchfn, diffopts, buffered):
- self.ui = ui
- self.repo = repo
- self.buffered = buffered
- self.matchfn = matchfn
- self.diffopts = diffopts
- self.header = {}
- self.hunk = {}
- self.lastheader = None
- self.footer = None
- self._columns = templatekw.getlogcolumns()
-
- def flush(self, ctx):
- rev = ctx.rev()
- if rev in self.header:
- h = self.header[rev]
- if h != self.lastheader:
- self.lastheader = h
- self.ui.write(h)
- del self.header[rev]
- if rev in self.hunk:
- self.ui.write(self.hunk[rev])
- del self.hunk[rev]
-
- def close(self):
- if self.footer:
- self.ui.write(self.footer)
-
- def show(self, ctx, copies=None, matchfn=None, hunksfilterfn=None,
- **props):
- props = pycompat.byteskwargs(props)
- if self.buffered:
- self.ui.pushbuffer(labeled=True)
- self._show(ctx, copies, matchfn, hunksfilterfn, props)
- self.hunk[ctx.rev()] = self.ui.popbuffer()
- else:
- self._show(ctx, copies, matchfn, hunksfilterfn, props)
-
- def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
- '''show a single changeset or file revision'''
- changenode = ctx.node()
- rev = ctx.rev()
-
- if self.ui.quiet:
- self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
- label='log.node')
- return
-
- columns = self._columns
- self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
- label=_changesetlabels(ctx))
-
- # branches are shown first before any other names due to backwards
- # compatibility
- branch = ctx.branch()
- # don't show the default branch name
- if branch != 'default':
- self.ui.write(columns['branch'] % branch, label='log.branch')
-
- for nsname, ns in self.repo.names.iteritems():
- # branches has special logic already handled above, so here we just
- # skip it
- if nsname == 'branches':
- continue
- # we will use the templatename as the color name since those two
- # should be the same
- for name in ns.names(self.repo, changenode):
- self.ui.write(ns.logfmt % name,
- label='log.%s' % ns.colorname)
- if self.ui.debugflag:
- self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
- for pctx in scmutil.meaningfulparents(self.repo, ctx):
- label = 'log.parent changeset.%s' % pctx.phasestr()
- self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
- label=label)
-
- if self.ui.debugflag and rev is not None:
- mnode = ctx.manifestnode()
- mrev = self.repo.manifestlog._revlog.rev(mnode)
- self.ui.write(columns['manifest']
- % scmutil.formatrevnode(self.ui, mrev, mnode),
- label='ui.debug log.manifest')
- self.ui.write(columns['user'] % ctx.user(), label='log.user')
- self.ui.write(columns['date'] % util.datestr(ctx.date()),
- label='log.date')
-
- if ctx.isunstable():
- instabilities = ctx.instabilities()
- self.ui.write(columns['instability'] % ', '.join(instabilities),
- label='log.instability')
-
- elif ctx.obsolete():
- self._showobsfate(ctx)
-
- self._exthook(ctx)
-
- if self.ui.debugflag:
- files = ctx.p1().status(ctx)[:3]
- for key, value in zip(['files', 'files+', 'files-'], files):
- if value:
- self.ui.write(columns[key] % " ".join(value),
- label='ui.debug log.files')
- elif ctx.files() and self.ui.verbose:
- self.ui.write(columns['files'] % " ".join(ctx.files()),
- label='ui.note log.files')
- if copies and self.ui.verbose:
- copies = ['%s (%s)' % c for c in copies]
- self.ui.write(columns['copies'] % ' '.join(copies),
- label='ui.note log.copies')
-
- extra = ctx.extra()
- if extra and self.ui.debugflag:
- for key, value in sorted(extra.items()):
- self.ui.write(columns['extra'] % (key, util.escapestr(value)),
- label='ui.debug log.extra')
-
- description = ctx.description().strip()
- if description:
- if self.ui.verbose:
- self.ui.write(_("description:\n"),
- label='ui.note log.description')
- self.ui.write(description,
- label='ui.note log.description')
- self.ui.write("\n\n")
- else:
- self.ui.write(columns['summary'] % description.splitlines()[0],
- label='log.summary')
- self.ui.write("\n")
-
- self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
-
- def _showobsfate(self, ctx):
- obsfate = templatekw.showobsfate(repo=self.repo, ctx=ctx, ui=self.ui)
-
- if obsfate:
- for obsfateline in obsfate:
- self.ui.write(self._columns['obsolete'] % obsfateline,
- label='log.obsfate')
-
- def _exthook(self, ctx):
- '''empty method used by extension as a hook point
- '''
-
- def showpatch(self, ctx, matchfn, hunksfilterfn=None):
- if not matchfn:
- matchfn = self.matchfn
- if matchfn:
- stat = self.diffopts.get('stat')
- diff = self.diffopts.get('patch')
- diffopts = patch.diffallopts(self.ui, self.diffopts)
- node = ctx.node()
- prev = ctx.p1().node()
- if stat:
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=True,
- hunksfilterfn=hunksfilterfn)
- if diff:
- if stat:
- self.ui.write("\n")
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=False,
- hunksfilterfn=hunksfilterfn)
- if stat or diff:
- self.ui.write("\n")
-
-class jsonchangeset(changeset_printer):
- '''format changeset information.'''
-
- def __init__(self, ui, repo, matchfn, diffopts, buffered):
- changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
- self.cache = {}
- self._first = True
-
- def close(self):
- if not self._first:
- self.ui.write("\n]\n")
- else:
- self.ui.write("[]\n")
-
- def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
- '''show a single changeset or file revision'''
- rev = ctx.rev()
- if rev is None:
- jrev = jnode = 'null'
- else:
- jrev = '%d' % rev
- jnode = '"%s"' % hex(ctx.node())
- j = encoding.jsonescape
-
- if self._first:
- self.ui.write("[\n {")
- self._first = False
- else:
- self.ui.write(",\n {")
-
- if self.ui.quiet:
- self.ui.write(('\n "rev": %s') % jrev)
- self.ui.write((',\n "node": %s') % jnode)
- self.ui.write('\n }')
- return
-
- self.ui.write(('\n "rev": %s') % jrev)
- self.ui.write((',\n "node": %s') % jnode)
- self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
- self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
- self.ui.write((',\n "user": "%s"') % j(ctx.user()))
- self.ui.write((',\n "date": [%d, %d]') % ctx.date())
- self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
-
- self.ui.write((',\n "bookmarks": [%s]') %
- ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
- self.ui.write((',\n "tags": [%s]') %
- ", ".join('"%s"' % j(t) for t in ctx.tags()))
- self.ui.write((',\n "parents": [%s]') %
- ", ".join('"%s"' % c.hex() for c in ctx.parents()))
-
- if self.ui.debugflag:
- if rev is None:
- jmanifestnode = 'null'
- else:
- jmanifestnode = '"%s"' % hex(ctx.manifestnode())
- self.ui.write((',\n "manifest": %s') % jmanifestnode)
-
- self.ui.write((',\n "extra": {%s}') %
- ", ".join('"%s": "%s"' % (j(k), j(v))
- for k, v in ctx.extra().items()))
-
- files = ctx.p1().status(ctx)
- self.ui.write((',\n "modified": [%s]') %
- ", ".join('"%s"' % j(f) for f in files[0]))
- self.ui.write((',\n "added": [%s]') %
- ", ".join('"%s"' % j(f) for f in files[1]))
- self.ui.write((',\n "removed": [%s]') %
- ", ".join('"%s"' % j(f) for f in files[2]))
-
- elif self.ui.verbose:
- self.ui.write((',\n "files": [%s]') %
- ", ".join('"%s"' % j(f) for f in ctx.files()))
-
- if copies:
- self.ui.write((',\n "copies": {%s}') %
- ", ".join('"%s": "%s"' % (j(k), j(v))
- for k, v in copies))
-
- matchfn = self.matchfn
- if matchfn:
- stat = self.diffopts.get('stat')
- diff = self.diffopts.get('patch')
- diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
- node, prev = ctx.node(), ctx.p1().node()
- if stat:
- self.ui.pushbuffer()
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=True)
- self.ui.write((',\n "diffstat": "%s"')
- % j(self.ui.popbuffer()))
- if diff:
- self.ui.pushbuffer()
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=False)
- self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
-
- self.ui.write("\n }")
-
-class changeset_templater(changeset_printer):
- '''format changeset information.
-
- Note: there are a variety of convenience functions to build a
- changeset_templater for common cases. See functions such as:
- makelogtemplater, show_changeset, buildcommittemplate, or other
- functions that use changesest_templater.
- '''
-
- # Arguments before "buffered" used to be positional. Consider not
- # adding/removing arguments before "buffered" to not break callers.
- def __init__(self, ui, repo, tmplspec, matchfn=None, diffopts=None,
- buffered=False):
- diffopts = diffopts or {}
-
- changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
- tres = formatter.templateresources(ui, repo)
- self.t = formatter.loadtemplater(ui, tmplspec,
- defaults=templatekw.keywords,
- resources=tres,
- cache=templatekw.defaulttempl)
- self._counter = itertools.count()
- self.cache = tres['cache'] # shared with _graphnodeformatter()
-
- self._tref = tmplspec.ref
- self._parts = {'header': '', 'footer': '',
- tmplspec.ref: tmplspec.ref,
- 'docheader': '', 'docfooter': '',
- 'separator': ''}
- if tmplspec.mapfile:
- # find correct templates for current mode, for backward
- # compatibility with 'log -v/-q/--debug' using a mapfile
- tmplmodes = [
- (True, ''),
- (self.ui.verbose, '_verbose'),
- (self.ui.quiet, '_quiet'),
- (self.ui.debugflag, '_debug'),
- ]
- for mode, postfix in tmplmodes:
- for t in self._parts:
- cur = t + postfix
- if mode and cur in self.t:
- self._parts[t] = cur
- else:
- partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
- m = formatter.templatepartsmap(tmplspec, self.t, partnames)
- self._parts.update(m)
-
- if self._parts['docheader']:
- self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
-
- def close(self):
- if self._parts['docfooter']:
- if not self.footer:
- self.footer = ""
- self.footer += templater.stringify(self.t(self._parts['docfooter']))
- return super(changeset_templater, self).close()
-
- def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
- '''show a single changeset or file revision'''
- props = props.copy()
- props['ctx'] = ctx
- props['index'] = index = next(self._counter)
- props['revcache'] = {'copies': copies}
- props = pycompat.strkwargs(props)
-
- # write separator, which wouldn't work well with the header part below
- # since there's inherently a conflict between header (across items) and
- # separator (per item)
- if self._parts['separator'] and index > 0:
- self.ui.write(templater.stringify(self.t(self._parts['separator'])))
-
- # write header
- if self._parts['header']:
- h = templater.stringify(self.t(self._parts['header'], **props))
- if self.buffered:
- self.header[ctx.rev()] = h
- else:
- if self.lastheader != h:
- self.lastheader = h
- self.ui.write(h)
-
- # write changeset metadata, then patch if requested
- key = self._parts[self._tref]
- self.ui.write(templater.stringify(self.t(key, **props)))
- self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
-
- if self._parts['footer']:
- if not self.footer:
- self.footer = templater.stringify(
- self.t(self._parts['footer'], **props))
-
-def logtemplatespec(tmpl, mapfile):
- if mapfile:
- return formatter.templatespec('changeset', tmpl, mapfile)
- else:
- return formatter.templatespec('', tmpl, None)
-
-def _lookuplogtemplate(ui, tmpl, style):
- """Find the template matching the given template spec or style
-
- See formatter.lookuptemplate() for details.
- """
-
- # ui settings
- if not tmpl and not style: # template are stronger than style
- tmpl = ui.config('ui', 'logtemplate')
- if tmpl:
- return logtemplatespec(templater.unquotestring(tmpl), None)
- else:
- style = util.expandpath(ui.config('ui', 'style'))
-
- if not tmpl and style:
- mapfile = style
- if not os.path.split(mapfile)[0]:
- mapname = (templater.templatepath('map-cmdline.' + mapfile)
- or templater.templatepath(mapfile))
- if mapname:
- mapfile = mapname
- return logtemplatespec(None, mapfile)
-
- if not tmpl:
- return logtemplatespec(None, None)
-
- return formatter.lookuptemplate(ui, 'changeset', tmpl)
-
-def makelogtemplater(ui, repo, tmpl, buffered=False):
- """Create a changeset_templater from a literal template 'tmpl'
- byte-string."""
- spec = logtemplatespec(tmpl, None)
- return changeset_templater(ui, repo, spec, buffered=buffered)
-
-def show_changeset(ui, repo, opts, buffered=False):
- """show one changeset using template or regular display.
-
- Display format will be the first non-empty hit of:
- 1. option 'template'
- 2. option 'style'
- 3. [ui] setting 'logtemplate'
- 4. [ui] setting 'style'
- If all of these values are either the unset or the empty string,
- regular display via changeset_printer() is done.
- """
- # options
- match = None
- if opts.get('patch') or opts.get('stat'):
- match = scmutil.matchall(repo)
-
- if opts.get('template') == 'json':
- return jsonchangeset(ui, repo, match, opts, buffered)
-
- spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
-
- if not spec.ref and not spec.tmpl and not spec.mapfile:
- return changeset_printer(ui, repo, match, opts, buffered)
-
- return changeset_templater(ui, repo, spec, match, opts, buffered)
-
class _regrettablereprbytes(bytes):
"""Bytes subclass that makes the repr the same on Python 3 as Python 2.
@@ -2429,394 +1931,6 @@ def walkchangerevs(repo, match, opts, pr
return iterate()
-def _makelogmatcher(repo, revs, pats, opts):
- """Build matcher and expanded patterns from log options
-
- If --follow, revs are the revisions to follow from.
-
- Returns (match, pats, slowpath) where
- - match: a matcher built from the given pats and -I/-X opts
- - pats: patterns used (globs are expanded on Windows)
- - slowpath: True if patterns aren't as simple as scanning filelogs
- """
- # pats/include/exclude are passed to match.match() directly in
- # _matchfiles() revset but walkchangerevs() builds its matcher with
- # scmutil.match(). The difference is input pats are globbed on
- # platforms without shell expansion (windows).
- wctx = repo[None]
- match, pats = scmutil.matchandpats(wctx, pats, opts)
- slowpath = match.anypats() or (not match.always() and opts.get('removed'))
- if not slowpath:
- follow = opts.get('follow') or opts.get('follow_first')
- startctxs = []
- if follow and opts.get('rev'):
- startctxs = [repo[r] for r in revs]
- for f in match.files():
- if follow and startctxs:
- # No idea if the path was a directory at that revision, so
- # take the slow path.
- if any(f not in c for c in startctxs):
- slowpath = True
- continue
- elif follow and f not in wctx:
- # If the file exists, it may be a directory, so let it
- # take the slow path.
- if os.path.exists(repo.wjoin(f)):
- slowpath = True
- continue
- else:
- raise error.Abort(_('cannot follow file not in parent '
- 'revision: "%s"') % f)
- filelog = repo.file(f)
- if not filelog:
- # A zero count may be a directory or deleted file, so
- # try to find matching entries on the slow path.
- if follow:
- raise error.Abort(
- _('cannot follow nonexistent file: "%s"') % f)
- slowpath = True
-
- # We decided to fall back to the slowpath because at least one
- # of the paths was not a file. Check to see if at least one of them
- # existed in history - in that case, we'll continue down the
- # slowpath; otherwise, we can turn off the slowpath
- if slowpath:
- for path in match.files():
- if path == '.' or path in repo.store:
- break
- else:
- slowpath = False
-
- return match, pats, slowpath
-
-def _fileancestors(repo, revs, match, followfirst):
- fctxs = []
- for r in revs:
- ctx = repo[r]
- fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
-
- # When displaying a revision with --patch --follow FILE, we have
- # to know which file of the revision must be diffed. With
- # --follow, we want the names of the ancestors of FILE in the
- # revision, stored in "fcache". "fcache" is populated as a side effect
- # of the graph traversal.
- fcache = {}
- def filematcher(rev):
- return scmutil.matchfiles(repo, fcache.get(rev, []))
-
- def revgen():
- for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
- fcache[rev] = [c.path() for c in cs]
- yield rev
- return smartset.generatorset(revgen(), iterasc=False), filematcher
-
-def _makenofollowlogfilematcher(repo, pats, opts):
- '''hook for extensions to override the filematcher for non-follow cases'''
- return None
-
-_opt2logrevset = {
- 'no_merges': ('not merge()', None),
- 'only_merges': ('merge()', None),
- '_matchfiles': (None, '_matchfiles(%ps)'),
- 'date': ('date(%s)', None),
- 'branch': ('branch(%s)', '%lr'),
- '_patslog': ('filelog(%s)', '%lr'),
- 'keyword': ('keyword(%s)', '%lr'),
- 'prune': ('ancestors(%s)', 'not %lr'),
- 'user': ('user(%s)', '%lr'),
-}
-
-def _makelogrevset(repo, match, pats, slowpath, opts):
- """Return a revset string built from log options and file patterns"""
- opts = dict(opts)
- # follow or not follow?
- follow = opts.get('follow') or opts.get('follow_first')
-
- # branch and only_branch are really aliases and must be handled at
- # the same time
- opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
- opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
-
- if slowpath:
- # See walkchangerevs() slow path.
- #
- # pats/include/exclude cannot be represented as separate
- # revset expressions as their filtering logic applies at file
- # level. For instance "-I a -X b" matches a revision touching
- # "a" and "b" while "file(a) and not file(b)" does
- # not. Besides, filesets are evaluated against the working
- # directory.
- matchargs = ['r:', 'd:relpath']
- for p in pats:
- matchargs.append('p:' + p)
- for p in opts.get('include', []):
- matchargs.append('i:' + p)
- for p in opts.get('exclude', []):
- matchargs.append('x:' + p)
- opts['_matchfiles'] = matchargs
- elif not follow:
- opts['_patslog'] = list(pats)
-
- expr = []
- for op, val in sorted(opts.iteritems()):
- if not val:
- continue
- if op not in _opt2logrevset:
- continue
- revop, listop = _opt2logrevset[op]
- if revop and '%' not in revop:
- expr.append(revop)
- elif not listop:
- expr.append(revsetlang.formatspec(revop, val))
- else:
- if revop:
- val = [revsetlang.formatspec(revop, v) for v in val]
- expr.append(revsetlang.formatspec(listop, val))
-
- if expr:
- expr = '(' + ' and '.join(expr) + ')'
- else:
- expr = None
- return expr
-
-def _logrevs(repo, opts):
- """Return the initial set of revisions to be filtered or followed"""
- follow = opts.get('follow') or opts.get('follow_first')
- if opts.get('rev'):
- revs = scmutil.revrange(repo, opts['rev'])
- elif follow and repo.dirstate.p1() == nullid:
- revs = smartset.baseset()
- elif follow:
- revs = repo.revs('.')
- else:
- revs = smartset.spanset(repo)
- revs.reverse()
- return revs
-
-def getlogrevs(repo, pats, opts):
- """Return (revs, filematcher) where revs is a smartset
-
- filematcher is a callable taking a revision number and returning a match
- objects filtering the files to be detailed when displaying the revision.
- """
- follow = opts.get('follow') or opts.get('follow_first')
- followfirst = opts.get('follow_first')
- limit = loglimit(opts)
- revs = _logrevs(repo, opts)
- if not revs:
- return smartset.baseset(), None
- match, pats, slowpath = _makelogmatcher(repo, revs, pats, opts)
- filematcher = None
- if follow:
- if slowpath or match.always():
- revs = dagop.revancestors(repo, revs, followfirst=followfirst)
- else:
- revs, filematcher = _fileancestors(repo, revs, match, followfirst)
- revs.reverse()
- if filematcher is None:
- filematcher = _makenofollowlogfilematcher(repo, pats, opts)
- if filematcher is None:
- def filematcher(rev):
- return match
-
- expr = _makelogrevset(repo, match, pats, slowpath, opts)
- if opts.get('graph') and opts.get('rev'):
- # User-specified revs might be unsorted, but don't sort before
- # _makelogrevset because it might depend on the order of revs
- if not (revs.isdescending() or revs.istopo()):
- revs.sort(reverse=True)
- if expr:
- matcher = revset.match(None, expr)
- revs = matcher(repo, revs)
- if limit is not None:
- revs = revs.slice(0, limit)
- return revs, filematcher
-
-def _parselinerangelogopt(repo, opts):
- """Parse --line-range log option and return a list of tuples (filename,
- (fromline, toline)).
- """
- linerangebyfname = []
- for pat in opts.get('line_range', []):
- try:
- pat, linerange = pat.rsplit(',', 1)
- except ValueError:
- raise error.Abort(_('malformatted line-range pattern %s') % pat)
- try:
- fromline, toline = map(int, linerange.split(':'))
- except ValueError:
- raise error.Abort(_("invalid line range for %s") % pat)
- msg = _("line range pattern '%s' must match exactly one file") % pat
- fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
- linerangebyfname.append(
- (fname, util.processlinerange(fromline, toline)))
- return linerangebyfname
-
-def getloglinerangerevs(repo, userrevs, opts):
- """Return (revs, filematcher, hunksfilter).
-
- "revs" are revisions obtained by processing "line-range" log options and
- walking block ancestors of each specified file/line-range.
-
- "filematcher(rev) -> match" is a factory function returning a match object
- for a given revision for file patterns specified in --line-range option.
- If neither --stat nor --patch options are passed, "filematcher" is None.
-
- "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
- returning a hunks filtering function.
- If neither --stat nor --patch options are passed, "filterhunks" is None.
- """
- wctx = repo[None]
-
- # Two-levels map of "rev -> file ctx -> [line range]".
- linerangesbyrev = {}
- for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
- if fname not in wctx:
- raise error.Abort(_('cannot follow file not in parent '
- 'revision: "%s"') % fname)
- fctx = wctx.filectx(fname)
- for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
- rev = fctx.introrev()
- if rev not in userrevs:
- continue
- linerangesbyrev.setdefault(
- rev, {}).setdefault(
- fctx.path(), []).append(linerange)
-
- filematcher = None
- hunksfilter = None
- if opts.get('patch') or opts.get('stat'):
-
- def nofilterhunksfn(fctx, hunks):
- return hunks
-
- def hunksfilter(rev):
- fctxlineranges = linerangesbyrev.get(rev)
- if fctxlineranges is None:
- return nofilterhunksfn
-
- def filterfn(fctx, hunks):
- lineranges = fctxlineranges.get(fctx.path())
- if lineranges is not None:
- for hr, lines in hunks:
- if hr is None: # binary
- yield hr, lines
- continue
- if any(mdiff.hunkinrange(hr[2:], lr)
- for lr in lineranges):
- yield hr, lines
- else:
- for hunk in hunks:
- yield hunk
-
- return filterfn
-
- def filematcher(rev):
- files = list(linerangesbyrev.get(rev, []))
- return scmutil.matchfiles(repo, files)
-
- revs = sorted(linerangesbyrev, reverse=True)
-
- return revs, filematcher, hunksfilter
-
-def _graphnodeformatter(ui, displayer):
- spec = ui.config('ui', 'graphnodetemplate')
- if not spec:
- return templatekw.showgraphnode # fast path for "{graphnode}"
-
- spec = templater.unquotestring(spec)
- tres = formatter.templateresources(ui)
- if isinstance(displayer, changeset_templater):
- tres['cache'] = displayer.cache # reuse cache of slow templates
- templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
- resources=tres)
- def formatnode(repo, ctx):
- props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
- return templ.render(props)
- return formatnode
-
-def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
- filematcher=None, props=None):
- props = props or {}
- formatnode = _graphnodeformatter(ui, displayer)
- state = graphmod.asciistate()
- styles = state['styles']
-
- # only set graph styling if HGPLAIN is not set.
- if ui.plain('graph'):
- # set all edge styles to |, the default pre-3.8 behaviour
- styles.update(dict.fromkeys(styles, '|'))
- else:
- edgetypes = {
- 'parent': graphmod.PARENT,
- 'grandparent': graphmod.GRANDPARENT,
- 'missing': graphmod.MISSINGPARENT
- }
- for name, key in edgetypes.items():
- # experimental config: experimental.graphstyle.*
- styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
- styles[key])
- if not styles[key]:
- styles[key] = None
-
- # experimental config: experimental.graphshorten
- state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
-
- for rev, type, ctx, parents in dag:
- char = formatnode(repo, ctx)
- copies = None
- if getrenamed and ctx.rev():
- copies = []
- for fn in ctx.files():
- rename = getrenamed(fn, ctx.rev())
- if rename:
- copies.append((fn, rename[0]))
- revmatchfn = None
- if filematcher is not None:
- revmatchfn = filematcher(ctx.rev())
- edges = edgefn(type, char, state, rev, parents)
- firstedge = next(edges)
- width = firstedge[2]
- displayer.show(ctx, copies=copies, matchfn=revmatchfn,
- _graphwidth=width, **pycompat.strkwargs(props))
- lines = displayer.hunk.pop(rev).split('\n')
- if not lines[-1]:
- del lines[-1]
- displayer.flush(ctx)
- for type, char, width, coldata in itertools.chain([firstedge], edges):
- graphmod.ascii(ui, state, type, char, lines, coldata)
- lines = []
- displayer.close()
-
-def graphlog(ui, repo, revs, filematcher, opts):
- # Parameters are identical to log command ones
- revdag = graphmod.dagwalker(repo, revs)
-
- getrenamed = None
- if opts.get('copies'):
- endrev = None
- if opts.get('rev'):
- endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
- getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
-
- ui.pager('log')
- displayer = show_changeset(ui, repo, opts, buffered=True)
- displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
- filematcher)
-
-def checkunsupportedgraphflags(pats, opts):
- for op in ["newest_first"]:
- if op in opts and opts[op]:
- raise error.Abort(_("-G/--graph option is incompatible with --%s")
- % op.replace("_", "-"))
-
-def graphrevs(repo, nodes, opts):
- limit = loglimit(opts)
- nodes.reverse()
- if limit is not None:
- nodes = nodes[:limit]
- return graphmod.nodes(repo, nodes)
-
def add(ui, repo, match, prefix, explicitonly, **opts):
join = lambda f: os.path.join(prefix, f)
bad = []
diff --git a/mercurial/cmdutil.py b/mercurial/logcmdutil.py
copy from mercurial/cmdutil.py
copy to mercurial/logcmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/logcmdutil.py
@@ -1,4 +1,4 @@
-# cmdutil.py - help for command processing in mercurial
+# logcmdutil.py - utility for log-like commands
#
# Copyright 2005-2007 Matt Mackall <mpm at selenic.com>
#
@@ -7,896 +7,34 @@
from __future__ import absolute_import
-import errno
import itertools
import os
-import re
-import tempfile
from .i18n import _
from .node import (
hex,
nullid,
- nullrev,
- short,
)
from . import (
- bookmarks,
- changelog,
- copies,
- crecord as crecordmod,
dagop,
- dirstateguard,
encoding,
error,
formatter,
graphmod,
match as matchmod,
mdiff,
- obsolete,
patch,
pathutil,
pycompat,
- registrar,
- revlog,
revset,
revsetlang,
- rewriteutil,
scmutil,
smartset,
templatekw,
templater,
util,
- vfs as vfsmod,
)
-stringio = util.stringio
-
-# templates of common command options
-
-dryrunopts = [
- ('n', 'dry-run', None,
- _('do not perform actions, just print output')),
-]
-
-remoteopts = [
- ('e', 'ssh', '',
- _('specify ssh command to use'), _('CMD')),
- ('', 'remotecmd', '',
- _('specify hg command to run on the remote side'), _('CMD')),
- ('', 'insecure', None,
- _('do not verify server certificate (ignoring web.cacerts config)')),
-]
-
-walkopts = [
- ('I', 'include', [],
- _('include names matching the given patterns'), _('PATTERN')),
- ('X', 'exclude', [],
- _('exclude names matching the given patterns'), _('PATTERN')),
-]
-
-commitopts = [
- ('m', 'message', '',
- _('use text as commit message'), _('TEXT')),
- ('l', 'logfile', '',
- _('read commit message from file'), _('FILE')),
-]
-
-commitopts2 = [
- ('d', 'date', '',
- _('record the specified date as commit date'), _('DATE')),
- ('u', 'user', '',
- _('record the specified user as committer'), _('USER')),
-]
-
-# hidden for now
-formatteropts = [
- ('T', 'template', '',
- _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
-]
-
-templateopts = [
- ('', 'style', '',
- _('display using template map file (DEPRECATED)'), _('STYLE')),
- ('T', 'template', '',
- _('display with template'), _('TEMPLATE')),
-]
-
-logopts = [
- ('p', 'patch', None, _('show patch')),
- ('g', 'git', None, _('use git extended diff format')),
- ('l', 'limit', '',
- _('limit number of changes displayed'), _('NUM')),
- ('M', 'no-merges', None, _('do not show merges')),
- ('', 'stat', None, _('output diffstat-style summary of changes')),
- ('G', 'graph', None, _("show the revision DAG")),
-] + templateopts
-
-diffopts = [
- ('a', 'text', None, _('treat all files as text')),
- ('g', 'git', None, _('use git extended diff format')),
- ('', 'binary', None, _('generate binary diffs in git mode (default)')),
- ('', 'nodates', None, _('omit dates from diff headers'))
-]
-
-diffwsopts = [
- ('w', 'ignore-all-space', None,
- _('ignore white space when comparing lines')),
- ('b', 'ignore-space-change', None,
- _('ignore changes in the amount of white space')),
- ('B', 'ignore-blank-lines', None,
- _('ignore changes whose lines are all blank')),
- ('Z', 'ignore-space-at-eol', None,
- _('ignore changes in whitespace at EOL')),
-]
-
-diffopts2 = [
- ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
- ('p', 'show-function', None, _('show which function each change is in')),
- ('', 'reverse', None, _('produce a diff that undoes the changes')),
-] + diffwsopts + [
- ('U', 'unified', '',
- _('number of lines of context to show'), _('NUM')),
- ('', 'stat', None, _('output diffstat-style summary of changes')),
- ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
-]
-
-mergetoolopts = [
- ('t', 'tool', '', _('specify merge tool')),
-]
-
-similarityopts = [
- ('s', 'similarity', '',
- _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
-]
-
-subrepoopts = [
- ('S', 'subrepos', None,
- _('recurse into subrepositories'))
-]
-
-debugrevlogopts = [
- ('c', 'changelog', False, _('open changelog')),
- ('m', 'manifest', False, _('open manifest')),
- ('', 'dir', '', _('open directory manifest')),
-]
-
-# special string such that everything below this line will be ingored in the
-# editor text
-_linebelow = "^HG: ------------------------ >8 ------------------------$"
-
-def ishunk(x):
- hunkclasses = (crecordmod.uihunk, patch.recordhunk)
- return isinstance(x, hunkclasses)
-
-def newandmodified(chunks, originalchunks):
- newlyaddedandmodifiedfiles = set()
- for chunk in chunks:
- if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
- originalchunks:
- newlyaddedandmodifiedfiles.add(chunk.header.filename())
- return newlyaddedandmodifiedfiles
-
-def parsealiases(cmd):
- return cmd.lstrip("^").split("|")
-
-def setupwrapcolorwrite(ui):
- # wrap ui.write so diff output can be labeled/colorized
- def wrapwrite(orig, *args, **kw):
- label = kw.pop(r'label', '')
- for chunk, l in patch.difflabel(lambda: args):
- orig(chunk, label=label + l)
-
- oldwrite = ui.write
- def wrap(*args, **kwargs):
- return wrapwrite(oldwrite, *args, **kwargs)
- setattr(ui, 'write', wrap)
- return oldwrite
-
-def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
- if usecurses:
- if testfile:
- recordfn = crecordmod.testdecorator(testfile,
- crecordmod.testchunkselector)
- else:
- recordfn = crecordmod.chunkselector
-
- return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
-
- else:
- return patch.filterpatch(ui, originalhunks, operation)
-
-def recordfilter(ui, originalhunks, operation=None):
- """ Prompts the user to filter the originalhunks and return a list of
- selected hunks.
- *operation* is used for to build ui messages to indicate the user what
- kind of filtering they are doing: reverting, committing, shelving, etc.
- (see patch.filterpatch).
- """
- usecurses = crecordmod.checkcurses(ui)
- testfile = ui.config('experimental', 'crecordtest')
- oldwrite = setupwrapcolorwrite(ui)
- try:
- newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
- testfile, operation)
- finally:
- ui.write = oldwrite
- return newchunks, newopts
-
-def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
- filterfn, *pats, **opts):
- from . import merge as mergemod
- opts = pycompat.byteskwargs(opts)
- if not ui.interactive():
- if cmdsuggest:
- msg = _('running non-interactively, use %s instead') % cmdsuggest
- else:
- msg = _('running non-interactively')
- raise error.Abort(msg)
-
- # make sure username is set before going interactive
- if not opts.get('user'):
- ui.username() # raise exception, username not provided
-
- def recordfunc(ui, repo, message, match, opts):
- """This is generic record driver.
-
- Its job is to interactively filter local changes, and
- accordingly prepare working directory into a state in which the
- job can be delegated to a non-interactive commit command such as
- 'commit' or 'qrefresh'.
-
- After the actual job is done by non-interactive command, the
- working directory is restored to its original state.
-
- In the end we'll record interesting changes, and everything else
- will be left in place, so the user can continue working.
- """
-
- checkunfinished(repo, commit=True)
- wctx = repo[None]
- merge = len(wctx.parents()) > 1
- if merge:
- raise error.Abort(_('cannot partially commit a merge '
- '(use "hg commit" instead)'))
-
- def fail(f, msg):
- raise error.Abort('%s: %s' % (f, msg))
-
- force = opts.get('force')
- if not force:
- vdirs = []
- match.explicitdir = vdirs.append
- match.bad = fail
-
- status = repo.status(match=match)
- if not force:
- repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
- diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
- diffopts.nodates = True
- diffopts.git = True
- diffopts.showfunc = True
- originaldiff = patch.diff(repo, changes=status, opts=diffopts)
- originalchunks = patch.parsepatch(originaldiff)
-
- # 1. filter patch, since we are intending to apply subset of it
- try:
- chunks, newopts = filterfn(ui, originalchunks)
- except error.PatchError as err:
- raise error.Abort(_('error parsing patch: %s') % err)
- opts.update(newopts)
-
- # We need to keep a backup of files that have been newly added and
- # modified during the recording process because there is a previous
- # version without the edit in the workdir
- newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
- contenders = set()
- for h in chunks:
- try:
- contenders.update(set(h.files()))
- except AttributeError:
- pass
-
- changed = status.modified + status.added + status.removed
- newfiles = [f for f in changed if f in contenders]
- if not newfiles:
- ui.status(_('no changes to record\n'))
- return 0
-
- modified = set(status.modified)
-
- # 2. backup changed files, so we can restore them in the end
-
- if backupall:
- tobackup = changed
- else:
- tobackup = [f for f in newfiles if f in modified or f in \
- newlyaddedandmodifiedfiles]
- backups = {}
- if tobackup:
- backupdir = repo.vfs.join('record-backups')
- try:
- os.mkdir(backupdir)
- except OSError as err:
- if err.errno != errno.EEXIST:
- raise
- try:
- # backup continues
- for f in tobackup:
- fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
- dir=backupdir)
- os.close(fd)
- ui.debug('backup %r as %r\n' % (f, tmpname))
- util.copyfile(repo.wjoin(f), tmpname, copystat=True)
- backups[f] = tmpname
-
- fp = stringio()
- for c in chunks:
- fname = c.filename()
- if fname in backups:
- c.write(fp)
- dopatch = fp.tell()
- fp.seek(0)
-
- # 2.5 optionally review / modify patch in text editor
- if opts.get('review', False):
- patchtext = (crecordmod.diffhelptext
- + crecordmod.patchhelptext
- + fp.read())
- reviewedpatch = ui.edit(patchtext, "",
- action="diff",
- repopath=repo.path)
- fp.truncate(0)
- fp.write(reviewedpatch)
- fp.seek(0)
-
- [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
- # 3a. apply filtered patch to clean repo (clean)
- if backups:
- # Equivalent to hg.revert
- m = scmutil.matchfiles(repo, backups.keys())
- mergemod.update(repo, repo.dirstate.p1(),
- False, True, matcher=m)
-
- # 3b. (apply)
- if dopatch:
- try:
- ui.debug('applying patch\n')
- ui.debug(fp.getvalue())
- patch.internalpatch(ui, repo, fp, 1, eolmode=None)
- except error.PatchError as err:
- raise error.Abort(str(err))
- del fp
-
- # 4. We prepared working directory according to filtered
- # patch. Now is the time to delegate the job to
- # commit/qrefresh or the like!
-
- # Make all of the pathnames absolute.
- newfiles = [repo.wjoin(nf) for nf in newfiles]
- return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
- finally:
- # 5. finally restore backed-up files
- try:
- dirstate = repo.dirstate
- for realname, tmpname in backups.iteritems():
- ui.debug('restoring %r to %r\n' % (tmpname, realname))
-
- if dirstate[realname] == 'n':
- # without normallookup, restoring timestamp
- # may cause partially committed files
- # to be treated as unmodified
- dirstate.normallookup(realname)
-
- # copystat=True here and above are a hack to trick any
- # editors that have f open that we haven't modified them.
- #
- # Also note that this racy as an editor could notice the
- # file's mtime before we've finished writing it.
- util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
- os.unlink(tmpname)
- if tobackup:
- os.rmdir(backupdir)
- except OSError:
- pass
-
- def recordinwlock(ui, repo, message, match, opts):
- with repo.wlock():
- return recordfunc(ui, repo, message, match, opts)
-
- return commit(ui, repo, recordinwlock, pats, opts)
-
-class dirnode(object):
- """
- Represent a directory in user working copy with information required for
- the purpose of tersing its status.
-
- path is the path to the directory
-
- statuses is a set of statuses of all files in this directory (this includes
- all the files in all the subdirectories too)
-
- files is a list of files which are direct child of this directory
-
- subdirs is a dictionary of sub-directory name as the key and it's own
- dirnode object as the value
- """
-
- def __init__(self, dirpath):
- self.path = dirpath
- self.statuses = set([])
- self.files = []
- self.subdirs = {}
-
- def _addfileindir(self, filename, status):
- """Add a file in this directory as a direct child."""
- self.files.append((filename, status))
-
- def addfile(self, filename, status):
- """
- Add a file to this directory or to its direct parent directory.
-
- If the file is not direct child of this directory, we traverse to the
- directory of which this file is a direct child of and add the file
- there.
- """
-
- # the filename contains a path separator, it means it's not the direct
- # child of this directory
- if '/' in filename:
- subdir, filep = filename.split('/', 1)
-
- # does the dirnode object for subdir exists
- if subdir not in self.subdirs:
- subdirpath = os.path.join(self.path, subdir)
- self.subdirs[subdir] = dirnode(subdirpath)
-
- # try adding the file in subdir
- self.subdirs[subdir].addfile(filep, status)
-
- else:
- self._addfileindir(filename, status)
-
- if status not in self.statuses:
- self.statuses.add(status)
-
- def iterfilepaths(self):
- """Yield (status, path) for files directly under this directory."""
- for f, st in self.files:
- yield st, os.path.join(self.path, f)
-
- def tersewalk(self, terseargs):
- """
- Yield (status, path) obtained by processing the status of this
- dirnode.
-
- terseargs is the string of arguments passed by the user with `--terse`
- flag.
-
- Following are the cases which can happen:
-
- 1) All the files in the directory (including all the files in its
- subdirectories) share the same status and the user has asked us to terse
- that status. -> yield (status, dirpath)
-
- 2) Otherwise, we do following:
-
- a) Yield (status, filepath) for all the files which are in this
- directory (only the ones in this directory, not the subdirs)
-
- b) Recurse the function on all the subdirectories of this
- directory
- """
-
- if len(self.statuses) == 1:
- onlyst = self.statuses.pop()
-
- # Making sure we terse only when the status abbreviation is
- # passed as terse argument
- if onlyst in terseargs:
- yield onlyst, self.path + pycompat.ossep
- return
-
- # add the files to status list
- for st, fpath in self.iterfilepaths():
- yield st, fpath
-
- #recurse on the subdirs
- for dirobj in self.subdirs.values():
- for st, fpath in dirobj.tersewalk(terseargs):
- yield st, fpath
-
-def tersedir(statuslist, terseargs):
- """
- Terse the status if all the files in a directory shares the same status.
-
- statuslist is scmutil.status() object which contains a list of files for
- each status.
- terseargs is string which is passed by the user as the argument to `--terse`
- flag.
-
- The function makes a tree of objects of dirnode class, and at each node it
- stores the information required to know whether we can terse a certain
- directory or not.
- """
- # the order matters here as that is used to produce final list
- allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
-
- # checking the argument validity
- for s in pycompat.bytestr(terseargs):
- if s not in allst:
- raise error.Abort(_("'%s' not recognized") % s)
-
- # creating a dirnode object for the root of the repo
- rootobj = dirnode('')
- pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
- 'ignored', 'removed')
-
- tersedict = {}
- for attrname in pstatus:
- statuschar = attrname[0:1]
- for f in getattr(statuslist, attrname):
- rootobj.addfile(f, statuschar)
- tersedict[statuschar] = []
-
- # we won't be tersing the root dir, so add files in it
- for st, fpath in rootobj.iterfilepaths():
- tersedict[st].append(fpath)
-
- # process each sub-directory and build tersedict
- for subdir in rootobj.subdirs.values():
- for st, f in subdir.tersewalk(terseargs):
- tersedict[st].append(f)
-
- tersedlist = []
- for st in allst:
- tersedict[st].sort()
- tersedlist.append(tersedict[st])
-
- return tersedlist
-
-def _commentlines(raw):
- '''Surround lineswith a comment char and a new line'''
- lines = raw.splitlines()
- commentedlines = ['# %s' % line for line in lines]
- return '\n'.join(commentedlines) + '\n'
-
-def _conflictsmsg(repo):
- # avoid merge cycle
- from . import merge as mergemod
- mergestate = mergemod.mergestate.read(repo)
- if not mergestate.active():
- return
-
- m = scmutil.match(repo[None])
- unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
- if unresolvedlist:
- mergeliststr = '\n'.join(
- [' %s' % util.pathto(repo.root, pycompat.getcwd(), path)
- for path in unresolvedlist])
- msg = _('''Unresolved merge conflicts:
-
-%s
-
-To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
- else:
- msg = _('No unresolved merge conflicts.')
-
- return _commentlines(msg)
-
-def _helpmessage(continuecmd, abortcmd):
- msg = _('To continue: %s\n'
- 'To abort: %s') % (continuecmd, abortcmd)
- return _commentlines(msg)
-
-def _rebasemsg():
- return _helpmessage('hg rebase --continue', 'hg rebase --abort')
-
-def _histeditmsg():
- return _helpmessage('hg histedit --continue', 'hg histedit --abort')
-
-def _unshelvemsg():
- return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
-
-def _updatecleanmsg(dest=None):
- warning = _('warning: this will discard uncommitted changes')
- return 'hg update --clean %s (%s)' % (dest or '.', warning)
-
-def _graftmsg():
- # tweakdefaults requires `update` to have a rev hence the `.`
- return _helpmessage('hg graft --continue', _updatecleanmsg())
-
-def _mergemsg():
- # tweakdefaults requires `update` to have a rev hence the `.`
- return _helpmessage('hg commit', _updatecleanmsg())
-
-def _bisectmsg():
- msg = _('To mark the changeset good: hg bisect --good\n'
- 'To mark the changeset bad: hg bisect --bad\n'
- 'To abort: hg bisect --reset\n')
- return _commentlines(msg)
-
-def fileexistspredicate(filename):
- return lambda repo: repo.vfs.exists(filename)
-
-def _mergepredicate(repo):
- return len(repo[None].parents()) > 1
-
-STATES = (
- # (state, predicate to detect states, helpful message function)
- ('histedit', fileexistspredicate('histedit-state'), _histeditmsg),
- ('bisect', fileexistspredicate('bisect.state'), _bisectmsg),
- ('graft', fileexistspredicate('graftstate'), _graftmsg),
- ('unshelve', fileexistspredicate('unshelverebasestate'), _unshelvemsg),
- ('rebase', fileexistspredicate('rebasestate'), _rebasemsg),
- # The merge state is part of a list that will be iterated over.
- # They need to be last because some of the other unfinished states may also
- # be in a merge or update state (eg. rebase, histedit, graft, etc).
- # We want those to have priority.
- ('merge', _mergepredicate, _mergemsg),
-)
-
-def _getrepostate(repo):
- # experimental config: commands.status.skipstates
- skip = set(repo.ui.configlist('commands', 'status.skipstates'))
- for state, statedetectionpredicate, msgfn in STATES:
- if state in skip:
- continue
- if statedetectionpredicate(repo):
- return (state, statedetectionpredicate, msgfn)
-
-def morestatus(repo, fm):
- statetuple = _getrepostate(repo)
- label = 'status.morestatus'
- if statetuple:
- fm.startitem()
- state, statedetectionpredicate, helpfulmsg = statetuple
- statemsg = _('The repository is in an unfinished *%s* state.') % state
- fm.write('statemsg', '%s\n', _commentlines(statemsg), label=label)
- conmsg = _conflictsmsg(repo)
- if conmsg:
- fm.write('conflictsmsg', '%s\n', conmsg, label=label)
- if helpfulmsg:
- helpmsg = helpfulmsg()
- fm.write('helpmsg', '%s\n', helpmsg, label=label)
-
-def findpossible(cmd, table, strict=False):
- """
- Return cmd -> (aliases, command table entry)
- for each matching command.
- Return debug commands (or their aliases) only if no normal command matches.
- """
- choice = {}
- debugchoice = {}
-
- if cmd in table:
- # short-circuit exact matches, "log" alias beats "^log|history"
- keys = [cmd]
- else:
- keys = table.keys()
-
- allcmds = []
- for e in keys:
- aliases = parsealiases(e)
- allcmds.extend(aliases)
- found = None
- if cmd in aliases:
- found = cmd
- elif not strict:
- for a in aliases:
- if a.startswith(cmd):
- found = a
- break
- if found is not None:
- if aliases[0].startswith("debug") or found.startswith("debug"):
- debugchoice[found] = (aliases, table[e])
- else:
- choice[found] = (aliases, table[e])
-
- if not choice and debugchoice:
- choice = debugchoice
-
- return choice, allcmds
-
-def findcmd(cmd, table, strict=True):
- """Return (aliases, command table entry) for command string."""
- choice, allcmds = findpossible(cmd, table, strict)
-
- if cmd in choice:
- return choice[cmd]
-
- if len(choice) > 1:
- clist = sorted(choice)
- raise error.AmbiguousCommand(cmd, clist)
-
- if choice:
- return list(choice.values())[0]
-
- raise error.UnknownCommand(cmd, allcmds)
-
-def changebranch(ui, repo, revs, label):
- """ Change the branch name of given revs to label """
-
- with repo.wlock(), repo.lock(), repo.transaction('branches'):
- # abort in case of uncommitted merge or dirty wdir
- bailifchanged(repo)
- revs = scmutil.revrange(repo, revs)
- if not revs:
- raise error.Abort("empty revision set")
- roots = repo.revs('roots(%ld)', revs)
- if len(roots) > 1:
- raise error.Abort(_("cannot change branch of non-linear revisions"))
- rewriteutil.precheck(repo, revs, 'change branch of')
-
- root = repo[roots.first()]
- if not root.p1().branch() == label and label in repo.branchmap():
- raise error.Abort(_("a branch of the same name already exists"))
-
- if repo.revs('merge() and %ld', revs):
- raise error.Abort(_("cannot change branch of a merge commit"))
- if repo.revs('obsolete() and %ld', revs):
- raise error.Abort(_("cannot change branch of a obsolete changeset"))
-
- # make sure only topological heads
- if repo.revs('heads(%ld) - head()', revs):
- raise error.Abort(_("cannot change branch in middle of a stack"))
-
- replacements = {}
- # avoid import cycle mercurial.cmdutil -> mercurial.context ->
- # mercurial.subrepo -> mercurial.cmdutil
- from . import context
- for rev in revs:
- ctx = repo[rev]
- oldbranch = ctx.branch()
- # check if ctx has same branch
- if oldbranch == label:
- continue
-
- def filectxfn(repo, newctx, path):
- try:
- return ctx[path]
- except error.ManifestLookupError:
- return None
-
- ui.debug("changing branch of '%s' from '%s' to '%s'\n"
- % (hex(ctx.node()), oldbranch, label))
- extra = ctx.extra()
- extra['branch_change'] = hex(ctx.node())
- # While changing branch of set of linear commits, make sure that
- # we base our commits on new parent rather than old parent which
- # was obsoleted while changing the branch
- p1 = ctx.p1().node()
- p2 = ctx.p2().node()
- if p1 in replacements:
- p1 = replacements[p1][0]
- if p2 in replacements:
- p2 = replacements[p2][0]
-
- mc = context.memctx(repo, (p1, p2),
- ctx.description(),
- ctx.files(),
- filectxfn,
- user=ctx.user(),
- date=ctx.date(),
- extra=extra,
- branch=label)
-
- commitphase = ctx.phase()
- overrides = {('phases', 'new-commit'): commitphase}
- with repo.ui.configoverride(overrides, 'branch-change'):
- newnode = repo.commitctx(mc)
-
- replacements[ctx.node()] = (newnode,)
- ui.debug('new node id is %s\n' % hex(newnode))
-
- # create obsmarkers and move bookmarks
- scmutil.cleanupnodes(repo, replacements, 'branch-change')
-
- # move the working copy too
- wctx = repo[None]
- # in-progress merge is a bit too complex for now.
- if len(wctx.parents()) == 1:
- newid = replacements.get(wctx.p1().node())
- if newid is not None:
- # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
- # mercurial.cmdutil
- from . import hg
- hg.update(repo, newid[0], quietempty=True)
-
- ui.status(_("changed branch on %d changesets\n") % len(replacements))
-
-def findrepo(p):
- while not os.path.isdir(os.path.join(p, ".hg")):
- oldp, p = p, os.path.dirname(p)
- if p == oldp:
- return None
-
- return p
-
-def bailifchanged(repo, merge=True, hint=None):
- """ enforce the precondition that working directory must be clean.
-
- 'merge' can be set to false if a pending uncommitted merge should be
- ignored (such as when 'update --check' runs).
-
- 'hint' is the usual hint given to Abort exception.
- """
-
- if merge and repo.dirstate.p2() != nullid:
- raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
- modified, added, removed, deleted = repo.status()[:4]
- if modified or added or removed or deleted:
- raise error.Abort(_('uncommitted changes'), hint=hint)
- ctx = repo[None]
- for s in sorted(ctx.substate):
- ctx.sub(s).bailifchanged(hint=hint)
-
-def logmessage(ui, opts):
- """ get the log message according to -m and -l option """
- message = opts.get('message')
- logfile = opts.get('logfile')
-
- if message and logfile:
- raise error.Abort(_('options --message and --logfile are mutually '
- 'exclusive'))
- if not message and logfile:
- try:
- if isstdiofilename(logfile):
- message = ui.fin.read()
- else:
- message = '\n'.join(util.readfile(logfile).splitlines())
- except IOError as inst:
- raise error.Abort(_("can't read commit message '%s': %s") %
- (logfile, encoding.strtolocal(inst.strerror)))
- return message
-
-def mergeeditform(ctxorbool, baseformname):
- """return appropriate editform name (referencing a committemplate)
-
- 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
- merging is committed.
-
- This returns baseformname with '.merge' appended if it is a merge,
- otherwise '.normal' is appended.
- """
- if isinstance(ctxorbool, bool):
- if ctxorbool:
- return baseformname + ".merge"
- elif 1 < len(ctxorbool.parents()):
- return baseformname + ".merge"
-
- return baseformname + ".normal"
-
-def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
- editform='', **opts):
- """get appropriate commit message editor according to '--edit' option
-
- 'finishdesc' is a function to be called with edited commit message
- (= 'description' of the new changeset) just after editing, but
- before checking empty-ness. It should return actual text to be
- stored into history. This allows to change description before
- storing.
-
- 'extramsg' is a extra message to be shown in the editor instead of
- 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
- is automatically added.
-
- 'editform' is a dot-separated list of names, to distinguish
- the purpose of commit text editing.
-
- 'getcommiteditor' returns 'commitforceeditor' regardless of
- 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
- they are specific for usage in MQ.
- """
- if edit or finishdesc or extramsg:
- return lambda r, c, s: commitforceeditor(r, c, s,
- finishdesc=finishdesc,
- extramsg=extramsg,
- editform=editform)
- elif editform:
- return lambda r, c, s: commiteditor(r, c, s, editform=editform)
- else:
- return commiteditor
def loglimit(opts):
"""get the log limit according to option -l/--limit"""
@@ -912,677 +50,6 @@ def loglimit(opts):
limit = None
return limit
-def makefilename(repo, pat, node, desc=None,
- total=None, seqno=None, revwidth=None, pathname=None):
- node_expander = {
- 'H': lambda: hex(node),
- 'R': lambda: '%d' % repo.changelog.rev(node),
- 'h': lambda: short(node),
- 'm': lambda: re.sub('[^\w]', '_', desc or '')
- }
- expander = {
- '%': lambda: '%',
- 'b': lambda: os.path.basename(repo.root),
- }
-
- try:
- if node:
- expander.update(node_expander)
- if node:
- expander['r'] = (lambda:
- ('%d' % repo.changelog.rev(node)).zfill(revwidth or 0))
- if total is not None:
- expander['N'] = lambda: '%d' % total
- if seqno is not None:
- expander['n'] = lambda: '%d' % seqno
- if total is not None and seqno is not None:
- expander['n'] = (lambda: ('%d' % seqno).zfill(len('%d' % total)))
- if pathname is not None:
- expander['s'] = lambda: os.path.basename(pathname)
- expander['d'] = lambda: os.path.dirname(pathname) or '.'
- expander['p'] = lambda: pathname
-
- newname = []
- patlen = len(pat)
- i = 0
- while i < patlen:
- c = pat[i:i + 1]
- if c == '%':
- i += 1
- c = pat[i:i + 1]
- c = expander[c]()
- newname.append(c)
- i += 1
- return ''.join(newname)
- except KeyError as inst:
- raise error.Abort(_("invalid format spec '%%%s' in output filename") %
- inst.args[0])
-
-def isstdiofilename(pat):
- """True if the given pat looks like a filename denoting stdin/stdout"""
- return not pat or pat == '-'
-
-class _unclosablefile(object):
- def __init__(self, fp):
- self._fp = fp
-
- def close(self):
- pass
-
- def __iter__(self):
- return iter(self._fp)
-
- def __getattr__(self, attr):
- return getattr(self._fp, attr)
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_value, exc_tb):
- pass
-
-def makefileobj(repo, pat, node=None, desc=None, total=None,
- seqno=None, revwidth=None, mode='wb', modemap=None,
- pathname=None):
-
- writable = mode not in ('r', 'rb')
-
- if isstdiofilename(pat):
- if writable:
- fp = repo.ui.fout
- else:
- fp = repo.ui.fin
- return _unclosablefile(fp)
- fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
- if modemap is not None:
- mode = modemap.get(fn, mode)
- if mode == 'wb':
- modemap[fn] = 'ab'
- return open(fn, mode)
-
-def openrevlog(repo, cmd, file_, opts):
- """opens the changelog, manifest, a filelog or a given revlog"""
- cl = opts['changelog']
- mf = opts['manifest']
- dir = opts['dir']
- msg = None
- if cl and mf:
- msg = _('cannot specify --changelog and --manifest at the same time')
- elif cl and dir:
- msg = _('cannot specify --changelog and --dir at the same time')
- elif cl or mf or dir:
- if file_:
- msg = _('cannot specify filename with --changelog or --manifest')
- elif not repo:
- msg = _('cannot specify --changelog or --manifest or --dir '
- 'without a repository')
- if msg:
- raise error.Abort(msg)
-
- r = None
- if repo:
- if cl:
- r = repo.unfiltered().changelog
- elif dir:
- if 'treemanifest' not in repo.requirements:
- raise error.Abort(_("--dir can only be used on repos with "
- "treemanifest enabled"))
- dirlog = repo.manifestlog._revlog.dirlog(dir)
- if len(dirlog):
- r = dirlog
- elif mf:
- r = repo.manifestlog._revlog
- elif file_:
- filelog = repo.file(file_)
- if len(filelog):
- r = filelog
- if not r:
- if not file_:
- raise error.CommandError(cmd, _('invalid arguments'))
- if not os.path.isfile(file_):
- raise error.Abort(_("revlog '%s' not found") % file_)
- r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
- file_[:-2] + ".i")
- return r
-
-def copy(ui, repo, pats, opts, rename=False):
- # called with the repo lock held
- #
- # hgsep => pathname that uses "/" to separate directories
- # ossep => pathname that uses os.sep to separate directories
- cwd = repo.getcwd()
- targets = {}
- after = opts.get("after")
- dryrun = opts.get("dry_run")
- wctx = repo[None]
-
- def walkpat(pat):
- srcs = []
- if after:
- badstates = '?'
- else:
- badstates = '?r'
- m = scmutil.match(wctx, [pat], opts, globbed=True)
- for abs in wctx.walk(m):
- state = repo.dirstate[abs]
- rel = m.rel(abs)
- exact = m.exact(abs)
- if state in badstates:
- if exact and state == '?':
- ui.warn(_('%s: not copying - file is not managed\n') % rel)
- if exact and state == 'r':
- ui.warn(_('%s: not copying - file has been marked for'
- ' remove\n') % rel)
- continue
- # abs: hgsep
- # rel: ossep
- srcs.append((abs, rel, exact))
- return srcs
-
- # abssrc: hgsep
- # relsrc: ossep
- # otarget: ossep
- def copyfile(abssrc, relsrc, otarget, exact):
- abstarget = pathutil.canonpath(repo.root, cwd, otarget)
- if '/' in abstarget:
- # We cannot normalize abstarget itself, this would prevent
- # case only renames, like a => A.
- abspath, absname = abstarget.rsplit('/', 1)
- abstarget = repo.dirstate.normalize(abspath) + '/' + absname
- reltarget = repo.pathto(abstarget, cwd)
- target = repo.wjoin(abstarget)
- src = repo.wjoin(abssrc)
- state = repo.dirstate[abstarget]
-
- scmutil.checkportable(ui, abstarget)
-
- # check for collisions
- prevsrc = targets.get(abstarget)
- if prevsrc is not None:
- ui.warn(_('%s: not overwriting - %s collides with %s\n') %
- (reltarget, repo.pathto(abssrc, cwd),
- repo.pathto(prevsrc, cwd)))
- return
-
- # check for overwrites
- exists = os.path.lexists(target)
- samefile = False
- if exists and abssrc != abstarget:
- if (repo.dirstate.normalize(abssrc) ==
- repo.dirstate.normalize(abstarget)):
- if not rename:
- ui.warn(_("%s: can't copy - same file\n") % reltarget)
- return
- exists = False
- samefile = True
-
- if not after and exists or after and state in 'mn':
- if not opts['force']:
- if state in 'mn':
- msg = _('%s: not overwriting - file already committed\n')
- if after:
- flags = '--after --force'
- else:
- flags = '--force'
- if rename:
- hint = _('(hg rename %s to replace the file by '
- 'recording a rename)\n') % flags
- else:
- hint = _('(hg copy %s to replace the file by '
- 'recording a copy)\n') % flags
- else:
- msg = _('%s: not overwriting - file exists\n')
- if rename:
- hint = _('(hg rename --after to record the rename)\n')
- else:
- hint = _('(hg copy --after to record the copy)\n')
- ui.warn(msg % reltarget)
- ui.warn(hint)
- return
-
- if after:
- if not exists:
- if rename:
- ui.warn(_('%s: not recording move - %s does not exist\n') %
- (relsrc, reltarget))
- else:
- ui.warn(_('%s: not recording copy - %s does not exist\n') %
- (relsrc, reltarget))
- return
- elif not dryrun:
- try:
- if exists:
- os.unlink(target)
- targetdir = os.path.dirname(target) or '.'
- if not os.path.isdir(targetdir):
- os.makedirs(targetdir)
- if samefile:
- tmp = target + "~hgrename"
- os.rename(src, tmp)
- os.rename(tmp, target)
- else:
- util.copyfile(src, target)
- srcexists = True
- except IOError as inst:
- if inst.errno == errno.ENOENT:
- ui.warn(_('%s: deleted in working directory\n') % relsrc)
- srcexists = False
- else:
- ui.warn(_('%s: cannot copy - %s\n') %
- (relsrc, encoding.strtolocal(inst.strerror)))
- return True # report a failure
-
- if ui.verbose or not exact:
- if rename:
- ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
- else:
- ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
-
- targets[abstarget] = abssrc
-
- # fix up dirstate
- scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
- dryrun=dryrun, cwd=cwd)
- if rename and not dryrun:
- if not after and srcexists and not samefile:
- repo.wvfs.unlinkpath(abssrc)
- wctx.forget([abssrc])
-
- # pat: ossep
- # dest ossep
- # srcs: list of (hgsep, hgsep, ossep, bool)
- # return: function that takes hgsep and returns ossep
- def targetpathfn(pat, dest, srcs):
- if os.path.isdir(pat):
- abspfx = pathutil.canonpath(repo.root, cwd, pat)
- abspfx = util.localpath(abspfx)
- if destdirexists:
- striplen = len(os.path.split(abspfx)[0])
- else:
- striplen = len(abspfx)
- if striplen:
- striplen += len(pycompat.ossep)
- res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
- elif destdirexists:
- res = lambda p: os.path.join(dest,
- os.path.basename(util.localpath(p)))
- else:
- res = lambda p: dest
- return res
-
- # pat: ossep
- # dest ossep
- # srcs: list of (hgsep, hgsep, ossep, bool)
- # return: function that takes hgsep and returns ossep
- def targetpathafterfn(pat, dest, srcs):
- if matchmod.patkind(pat):
- # a mercurial pattern
- res = lambda p: os.path.join(dest,
- os.path.basename(util.localpath(p)))
- else:
- abspfx = pathutil.canonpath(repo.root, cwd, pat)
- if len(abspfx) < len(srcs[0][0]):
- # A directory. Either the target path contains the last
- # component of the source path or it does not.
- def evalpath(striplen):
- score = 0
- for s in srcs:
- t = os.path.join(dest, util.localpath(s[0])[striplen:])
- if os.path.lexists(t):
- score += 1
- return score
-
- abspfx = util.localpath(abspfx)
- striplen = len(abspfx)
- if striplen:
- striplen += len(pycompat.ossep)
- if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
- score = evalpath(striplen)
- striplen1 = len(os.path.split(abspfx)[0])
- if striplen1:
- striplen1 += len(pycompat.ossep)
- if evalpath(striplen1) > score:
- striplen = striplen1
- res = lambda p: os.path.join(dest,
- util.localpath(p)[striplen:])
- else:
- # a file
- if destdirexists:
- res = lambda p: os.path.join(dest,
- os.path.basename(util.localpath(p)))
- else:
- res = lambda p: dest
- return res
-
- pats = scmutil.expandpats(pats)
- if not pats:
- raise error.Abort(_('no source or destination specified'))
- if len(pats) == 1:
- raise error.Abort(_('no destination specified'))
- dest = pats.pop()
- destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
- if not destdirexists:
- if len(pats) > 1 or matchmod.patkind(pats[0]):
- raise error.Abort(_('with multiple sources, destination must be an '
- 'existing directory'))
- if util.endswithsep(dest):
- raise error.Abort(_('destination %s is not a directory') % dest)
-
- tfn = targetpathfn
- if after:
- tfn = targetpathafterfn
- copylist = []
- for pat in pats:
- srcs = walkpat(pat)
- if not srcs:
- continue
- copylist.append((tfn(pat, dest, srcs), srcs))
- if not copylist:
- raise error.Abort(_('no files to copy'))
-
- errors = 0
- for targetpath, srcs in copylist:
- for abssrc, relsrc, exact in srcs:
- if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
- errors += 1
-
- if errors:
- ui.warn(_('(consider using --after)\n'))
-
- return errors != 0
-
-## facility to let extension process additional data into an import patch
-# list of identifier to be executed in order
-extrapreimport = [] # run before commit
-extrapostimport = [] # run after commit
-# mapping from identifier to actual import function
-#
-# 'preimport' are run before the commit is made and are provided the following
-# arguments:
-# - repo: the localrepository instance,
-# - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
-# - extra: the future extra dictionary of the changeset, please mutate it,
-# - opts: the import options.
-# XXX ideally, we would just pass an ctx ready to be computed, that would allow
-# mutation of in memory commit and more. Feel free to rework the code to get
-# there.
-extrapreimportmap = {}
-# 'postimport' are run after the commit is made and are provided the following
-# argument:
-# - ctx: the changectx created by import.
-extrapostimportmap = {}
-
-def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
- """Utility function used by commands.import to import a single patch
-
- This function is explicitly defined here to help the evolve extension to
- wrap this part of the import logic.
-
- The API is currently a bit ugly because it a simple code translation from
- the import command. Feel free to make it better.
-
- :hunk: a patch (as a binary string)
- :parents: nodes that will be parent of the created commit
- :opts: the full dict of option passed to the import command
- :msgs: list to save commit message to.
- (used in case we need to save it when failing)
- :updatefunc: a function that update a repo to a given node
- updatefunc(<repo>, <node>)
- """
- # avoid cycle context -> subrepo -> cmdutil
- from . import context
- extractdata = patch.extract(ui, hunk)
- tmpname = extractdata.get('filename')
- message = extractdata.get('message')
- user = opts.get('user') or extractdata.get('user')
- date = opts.get('date') or extractdata.get('date')
- branch = extractdata.get('branch')
- nodeid = extractdata.get('nodeid')
- p1 = extractdata.get('p1')
- p2 = extractdata.get('p2')
-
- nocommit = opts.get('no_commit')
- importbranch = opts.get('import_branch')
- update = not opts.get('bypass')
- strip = opts["strip"]
- prefix = opts["prefix"]
- sim = float(opts.get('similarity') or 0)
- if not tmpname:
- return (None, None, False)
-
- rejects = False
-
- try:
- cmdline_message = logmessage(ui, opts)
- if cmdline_message:
- # pickup the cmdline msg
- message = cmdline_message
- elif message:
- # pickup the patch msg
- message = message.strip()
- else:
- # launch the editor
- message = None
- ui.debug('message:\n%s\n' % message)
-
- if len(parents) == 1:
- parents.append(repo[nullid])
- if opts.get('exact'):
- if not nodeid or not p1:
- raise error.Abort(_('not a Mercurial patch'))
- p1 = repo[p1]
- p2 = repo[p2 or nullid]
- elif p2:
- try:
- p1 = repo[p1]
- p2 = repo[p2]
- # Without any options, consider p2 only if the
- # patch is being applied on top of the recorded
- # first parent.
- if p1 != parents[0]:
- p1 = parents[0]
- p2 = repo[nullid]
- except error.RepoError:
- p1, p2 = parents
- if p2.node() == nullid:
- ui.warn(_("warning: import the patch as a normal revision\n"
- "(use --exact to import the patch as a merge)\n"))
- else:
- p1, p2 = parents
-
- n = None
- if update:
- if p1 != parents[0]:
- updatefunc(repo, p1.node())
- if p2 != parents[1]:
- repo.setparents(p1.node(), p2.node())
-
- if opts.get('exact') or importbranch:
- repo.dirstate.setbranch(branch or 'default')
-
- partial = opts.get('partial', False)
- files = set()
- try:
- patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
- files=files, eolmode=None, similarity=sim / 100.0)
- except error.PatchError as e:
- if not partial:
- raise error.Abort(str(e))
- if partial:
- rejects = True
-
- files = list(files)
- if nocommit:
- if message:
- msgs.append(message)
- else:
- if opts.get('exact') or p2:
- # If you got here, you either use --force and know what
- # you are doing or used --exact or a merge patch while
- # being updated to its first parent.
- m = None
- else:
- m = scmutil.matchfiles(repo, files or [])
- editform = mergeeditform(repo[None], 'import.normal')
- if opts.get('exact'):
- editor = None
- else:
- editor = getcommiteditor(editform=editform,
- **pycompat.strkwargs(opts))
- extra = {}
- for idfunc in extrapreimport:
- extrapreimportmap[idfunc](repo, extractdata, extra, opts)
- overrides = {}
- if partial:
- overrides[('ui', 'allowemptycommit')] = True
- with repo.ui.configoverride(overrides, 'import'):
- n = repo.commit(message, user,
- date, match=m,
- editor=editor, extra=extra)
- for idfunc in extrapostimport:
- extrapostimportmap[idfunc](repo[n])
- else:
- if opts.get('exact') or importbranch:
- branch = branch or 'default'
- else:
- branch = p1.branch()
- store = patch.filestore()
- try:
- files = set()
- try:
- patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
- files, eolmode=None)
- except error.PatchError as e:
- raise error.Abort(str(e))
- if opts.get('exact'):
- editor = None
- else:
- editor = getcommiteditor(editform='import.bypass')
- memctx = context.memctx(repo, (p1.node(), p2.node()),
- message,
- files=files,
- filectxfn=store,
- user=user,
- date=date,
- branch=branch,
- editor=editor)
- n = memctx.commit()
- finally:
- store.close()
- if opts.get('exact') and nocommit:
- # --exact with --no-commit is still useful in that it does merge
- # and branch bits
- ui.warn(_("warning: can't check exact import with --no-commit\n"))
- elif opts.get('exact') and hex(n) != nodeid:
- raise error.Abort(_('patch is damaged or loses information'))
- msg = _('applied to working directory')
- if n:
- # i18n: refers to a short changeset id
- msg = _('created %s') % short(n)
- return (msg, n, rejects)
- finally:
- os.unlink(tmpname)
-
-# facility to let extensions include additional data in an exported patch
-# list of identifiers to be executed in order
-extraexport = []
-# mapping from identifier to actual export function
-# function as to return a string to be added to the header or None
-# it is given two arguments (sequencenumber, changectx)
-extraexportmap = {}
-
-def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, diffopts):
- node = scmutil.binnode(ctx)
- parents = [p.node() for p in ctx.parents() if p]
- branch = ctx.branch()
- if switch_parent:
- parents.reverse()
-
- if parents:
- prev = parents[0]
- else:
- prev = nullid
-
- write("# HG changeset patch\n")
- write("# User %s\n" % ctx.user())
- write("# Date %d %d\n" % ctx.date())
- write("# %s\n" % util.datestr(ctx.date()))
- if branch and branch != 'default':
- write("# Branch %s\n" % branch)
- write("# Node ID %s\n" % hex(node))
- write("# Parent %s\n" % hex(prev))
- if len(parents) > 1:
- write("# Parent %s\n" % hex(parents[1]))
-
- for headerid in extraexport:
- header = extraexportmap[headerid](seqno, ctx)
- if header is not None:
- write('# %s\n' % header)
- write(ctx.description().rstrip())
- write("\n\n")
-
- for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
- write(chunk, label=label)
-
-def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
- opts=None, match=None):
- '''export changesets as hg patches
-
- Args:
- repo: The repository from which we're exporting revisions.
- revs: A list of revisions to export as revision numbers.
- fntemplate: An optional string to use for generating patch file names.
- fp: An optional file-like object to which patches should be written.
- switch_parent: If True, show diffs against second parent when not nullid.
- Default is false, which always shows diff against p1.
- opts: diff options to use for generating the patch.
- match: If specified, only export changes to files matching this matcher.
-
- Returns:
- Nothing.
-
- Side Effect:
- "HG Changeset Patch" data is emitted to one of the following
- destinations:
- fp is specified: All revs are written to the specified
- file-like object.
- fntemplate specified: Each rev is written to a unique file named using
- the given template.
- Neither fp nor template specified: All revs written to repo.ui.write()
- '''
-
- total = len(revs)
- revwidth = max(len(str(rev)) for rev in revs)
- filemode = {}
-
- write = None
- dest = '<unnamed>'
- if fp:
- dest = getattr(fp, 'name', dest)
- def write(s, **kw):
- fp.write(s)
- elif not fntemplate:
- write = repo.ui.write
-
- for seqno, rev in enumerate(revs, 1):
- ctx = repo[rev]
- fo = None
- if not fp and fntemplate:
- desc_lines = ctx.description().rstrip().split('\n')
- desc = desc_lines[0] #Commit always has a first line.
- fo = makefileobj(repo, fntemplate, ctx.node(), desc=desc,
- total=total, seqno=seqno, revwidth=revwidth,
- mode='wb', modemap=filemode)
- dest = fo.name
- def write(s, **kw):
- fo.write(s)
- if not dest.startswith('<'):
- repo.ui.note("%s\n" % dest)
- _exportsingle(
- repo, ctx, match, switch_parent, rev, seqno, write, opts)
- if fo is not None:
- fo.close()
-
def diffordiffstat(ui, repo, diffopts, node1, node2, match,
changes=None, stat=False, fp=None, prefix='',
root='', listsubrepos=False, hunksfilterfn=None):
@@ -2077,358 +544,6 @@ def show_changeset(ui, repo, opts, buffe
return changeset_templater(ui, repo, spec, match, opts, buffered)
-class _regrettablereprbytes(bytes):
- """Bytes subclass that makes the repr the same on Python 3 as Python 2.
-
- This is a huge hack.
- """
- def __repr__(self):
- return repr(pycompat.sysstr(self))
-
-def _maybebytestr(v):
- if pycompat.ispy3 and isinstance(v, bytes):
- return _regrettablereprbytes(v)
- return v
-
-def showmarker(fm, marker, index=None):
- """utility function to display obsolescence marker in a readable way
-
- To be used by debug function."""
- if index is not None:
- fm.write('index', '%i ', index)
- fm.write('prednode', '%s ', hex(marker.prednode()))
- succs = marker.succnodes()
- fm.condwrite(succs, 'succnodes', '%s ',
- fm.formatlist(map(hex, succs), name='node'))
- fm.write('flag', '%X ', marker.flags())
- parents = marker.parentnodes()
- if parents is not None:
- fm.write('parentnodes', '{%s} ',
- fm.formatlist(map(hex, parents), name='node', sep=', '))
- fm.write('date', '(%s) ', fm.formatdate(marker.date()))
- meta = marker.metadata().copy()
- meta.pop('date', None)
- smeta = {_maybebytestr(k): _maybebytestr(v) for k, v in meta.iteritems()}
- fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
- fm.plain('\n')
-
-def finddate(ui, repo, date):
- """Find the tipmost changeset that matches the given date spec"""
-
- df = util.matchdate(date)
- m = scmutil.matchall(repo)
- results = {}
-
- def prep(ctx, fns):
- d = ctx.date()
- if df(d[0]):
- results[ctx.rev()] = d
-
- for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
- rev = ctx.rev()
- if rev in results:
- ui.status(_("found revision %s from %s\n") %
- (rev, util.datestr(results[rev])))
- return '%d' % rev
-
- raise error.Abort(_("revision matching date not found"))
-
-def increasingwindows(windowsize=8, sizelimit=512):
- while True:
- yield windowsize
- if windowsize < sizelimit:
- windowsize *= 2
-
-def _walkrevs(repo, opts):
- # Default --rev value depends on --follow but --follow behavior
- # depends on revisions resolved from --rev...
- follow = opts.get('follow') or opts.get('follow_first')
- if opts.get('rev'):
- revs = scmutil.revrange(repo, opts['rev'])
- elif follow and repo.dirstate.p1() == nullid:
- revs = smartset.baseset()
- elif follow:
- revs = repo.revs('reverse(:.)')
- else:
- revs = smartset.spanset(repo)
- revs.reverse()
- return revs
-
-class FileWalkError(Exception):
- pass
-
-def walkfilerevs(repo, match, follow, revs, fncache):
- '''Walks the file history for the matched files.
-
- Returns the changeset revs that are involved in the file history.
-
- Throws FileWalkError if the file history can't be walked using
- filelogs alone.
- '''
- wanted = set()
- copies = []
- minrev, maxrev = min(revs), max(revs)
- def filerevgen(filelog, last):
- """
- Only files, no patterns. Check the history of each file.
-
- Examines filelog entries within minrev, maxrev linkrev range
- Returns an iterator yielding (linkrev, parentlinkrevs, copied)
- tuples in backwards order
- """
- cl_count = len(repo)
- revs = []
- for j in xrange(0, last + 1):
- linkrev = filelog.linkrev(j)
- if linkrev < minrev:
- continue
- # only yield rev for which we have the changelog, it can
- # happen while doing "hg log" during a pull or commit
- if linkrev >= cl_count:
- break
-
- parentlinkrevs = []
- for p in filelog.parentrevs(j):
- if p != nullrev:
- parentlinkrevs.append(filelog.linkrev(p))
- n = filelog.node(j)
- revs.append((linkrev, parentlinkrevs,
- follow and filelog.renamed(n)))
-
- return reversed(revs)
- def iterfiles():
- pctx = repo['.']
- for filename in match.files():
- if follow:
- if filename not in pctx:
- raise error.Abort(_('cannot follow file not in parent '
- 'revision: "%s"') % filename)
- yield filename, pctx[filename].filenode()
- else:
- yield filename, None
- for filename_node in copies:
- yield filename_node
-
- for file_, node in iterfiles():
- filelog = repo.file(file_)
- if not len(filelog):
- if node is None:
- # A zero count may be a directory or deleted file, so
- # try to find matching entries on the slow path.
- if follow:
- raise error.Abort(
- _('cannot follow nonexistent file: "%s"') % file_)
- raise FileWalkError("Cannot walk via filelog")
- else:
- continue
-
- if node is None:
- last = len(filelog) - 1
- else:
- last = filelog.rev(node)
-
- # keep track of all ancestors of the file
- ancestors = {filelog.linkrev(last)}
-
- # iterate from latest to oldest revision
- for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
- if not follow:
- if rev > maxrev:
- continue
- else:
- # Note that last might not be the first interesting
- # rev to us:
- # if the file has been changed after maxrev, we'll
- # have linkrev(last) > maxrev, and we still need
- # to explore the file graph
- if rev not in ancestors:
- continue
- # XXX insert 1327 fix here
- if flparentlinkrevs:
- ancestors.update(flparentlinkrevs)
-
- fncache.setdefault(rev, []).append(file_)
- wanted.add(rev)
- if copied:
- copies.append(copied)
-
- return wanted
-
-class _followfilter(object):
- def __init__(self, repo, onlyfirst=False):
- self.repo = repo
- self.startrev = nullrev
- self.roots = set()
- self.onlyfirst = onlyfirst
-
- def match(self, rev):
- def realparents(rev):
- if self.onlyfirst:
- return self.repo.changelog.parentrevs(rev)[0:1]
- else:
- return filter(lambda x: x != nullrev,
- self.repo.changelog.parentrevs(rev))
-
- if self.startrev == nullrev:
- self.startrev = rev
- return True
-
- if rev > self.startrev:
- # forward: all descendants
- if not self.roots:
- self.roots.add(self.startrev)
- for parent in realparents(rev):
- if parent in self.roots:
- self.roots.add(rev)
- return True
- else:
- # backwards: all parents
- if not self.roots:
- self.roots.update(realparents(self.startrev))
- if rev in self.roots:
- self.roots.remove(rev)
- self.roots.update(realparents(rev))
- return True
-
- return False
-
-def walkchangerevs(repo, match, opts, prepare):
- '''Iterate over files and the revs in which they changed.
-
- Callers most commonly need to iterate backwards over the history
- in which they are interested. Doing so has awful (quadratic-looking)
- performance, so we use iterators in a "windowed" way.
-
- We walk a window of revisions in the desired order. Within the
- window, we first walk forwards to gather data, then in the desired
- order (usually backwards) to display it.
-
- This function returns an iterator yielding contexts. Before
- yielding each context, the iterator will first call the prepare
- function on each context in the window in forward order.'''
-
- follow = opts.get('follow') or opts.get('follow_first')
- revs = _walkrevs(repo, opts)
- if not revs:
- return []
- wanted = set()
- slowpath = match.anypats() or (not match.always() and opts.get('removed'))
- fncache = {}
- change = repo.changectx
-
- # First step is to fill wanted, the set of revisions that we want to yield.
- # When it does not induce extra cost, we also fill fncache for revisions in
- # wanted: a cache of filenames that were changed (ctx.files()) and that
- # match the file filtering conditions.
-
- if match.always():
- # No files, no patterns. Display all revs.
- wanted = revs
- elif not slowpath:
- # We only have to read through the filelog to find wanted revisions
-
- try:
- wanted = walkfilerevs(repo, match, follow, revs, fncache)
- except FileWalkError:
- slowpath = True
-
- # We decided to fall back to the slowpath because at least one
- # of the paths was not a file. Check to see if at least one of them
- # existed in history, otherwise simply return
- for path in match.files():
- if path == '.' or path in repo.store:
- break
- else:
- return []
-
- if slowpath:
- # We have to read the changelog to match filenames against
- # changed files
-
- if follow:
- raise error.Abort(_('can only follow copies/renames for explicit '
- 'filenames'))
-
- # The slow path checks files modified in every changeset.
- # This is really slow on large repos, so compute the set lazily.
- class lazywantedset(object):
- def __init__(self):
- self.set = set()
- self.revs = set(revs)
-
- # No need to worry about locality here because it will be accessed
- # in the same order as the increasing window below.
- def __contains__(self, value):
- if value in self.set:
- return True
- elif not value in self.revs:
- return False
- else:
- self.revs.discard(value)
- ctx = change(value)
- matches = filter(match, ctx.files())
- if matches:
- fncache[value] = matches
- self.set.add(value)
- return True
- return False
-
- def discard(self, value):
- self.revs.discard(value)
- self.set.discard(value)
-
- wanted = lazywantedset()
-
- # it might be worthwhile to do this in the iterator if the rev range
- # is descending and the prune args are all within that range
- for rev in opts.get('prune', ()):
- rev = repo[rev].rev()
- ff = _followfilter(repo)
- stop = min(revs[0], revs[-1])
- for x in xrange(rev, stop - 1, -1):
- if ff.match(x):
- wanted = wanted - [x]
-
- # Now that wanted is correctly initialized, we can iterate over the
- # revision range, yielding only revisions in wanted.
- def iterate():
- if follow and match.always():
- ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
- def want(rev):
- return ff.match(rev) and rev in wanted
- else:
- def want(rev):
- return rev in wanted
-
- it = iter(revs)
- stopiteration = False
- for windowsize in increasingwindows():
- nrevs = []
- for i in xrange(windowsize):
- rev = next(it, None)
- if rev is None:
- stopiteration = True
- break
- elif want(rev):
- nrevs.append(rev)
- for rev in sorted(nrevs):
- fns = fncache.get(rev)
- ctx = change(rev)
- if not fns:
- def fns_generator():
- for f in ctx.files():
- if match(f):
- yield f
- fns = fns_generator()
- prepare(ctx, fns)
- for rev in nrevs:
- yield change(rev)
-
- if stopiteration:
- break
-
- return iterate()
-
def _makelogmatcher(repo, revs, pats, opts):
"""Build matcher and expanded patterns from log options
@@ -2816,1226 +931,3 @@ def graphrevs(repo, nodes, opts):
if limit is not None:
nodes = nodes[:limit]
return graphmod.nodes(repo, nodes)
-
-def add(ui, repo, match, prefix, explicitonly, **opts):
- join = lambda f: os.path.join(prefix, f)
- bad = []
-
- badfn = lambda x, y: bad.append(x) or match.bad(x, y)
- names = []
- wctx = repo[None]
- cca = None
- abort, warn = scmutil.checkportabilityalert(ui)
- if abort or warn:
- cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
-
- badmatch = matchmod.badmatch(match, badfn)
- dirstate = repo.dirstate
- # We don't want to just call wctx.walk here, since it would return a lot of
- # clean files, which we aren't interested in and takes time.
- for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
- unknown=True, ignored=False, full=False)):
- exact = match.exact(f)
- if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
- if cca:
- cca(f)
- names.append(f)
- if ui.verbose or not exact:
- ui.status(_('adding %s\n') % match.rel(f))
-
- for subpath in sorted(wctx.substate):
- sub = wctx.sub(subpath)
- try:
- submatch = matchmod.subdirmatcher(subpath, match)
- if opts.get(r'subrepos'):
- bad.extend(sub.add(ui, submatch, prefix, False, **opts))
- else:
- bad.extend(sub.add(ui, submatch, prefix, True, **opts))
- except error.LookupError:
- ui.status(_("skipping missing subrepository: %s\n")
- % join(subpath))
-
- if not opts.get(r'dry_run'):
- rejected = wctx.add(names, prefix)
- bad.extend(f for f in rejected if f in match.files())
- return bad
-
-def addwebdirpath(repo, serverpath, webconf):
- webconf[serverpath] = repo.root
- repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
-
- for r in repo.revs('filelog("path:.hgsub")'):
- ctx = repo[r]
- for subpath in ctx.substate:
- ctx.sub(subpath).addwebdirpath(serverpath, webconf)
-
-def forget(ui, repo, match, prefix, explicitonly):
- join = lambda f: os.path.join(prefix, f)
- bad = []
- badfn = lambda x, y: bad.append(x) or match.bad(x, y)
- wctx = repo[None]
- forgot = []
-
- s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
- forget = sorted(s.modified + s.added + s.deleted + s.clean)
- if explicitonly:
- forget = [f for f in forget if match.exact(f)]
-
- for subpath in sorted(wctx.substate):
- sub = wctx.sub(subpath)
- try:
- submatch = matchmod.subdirmatcher(subpath, match)
- subbad, subforgot = sub.forget(submatch, prefix)
- bad.extend([subpath + '/' + f for f in subbad])
- forgot.extend([subpath + '/' + f for f in subforgot])
- except error.LookupError:
- ui.status(_("skipping missing subrepository: %s\n")
- % join(subpath))
-
- if not explicitonly:
- for f in match.files():
- if f not in repo.dirstate and not repo.wvfs.isdir(f):
- if f not in forgot:
- if repo.wvfs.exists(f):
- # Don't complain if the exact case match wasn't given.
- # But don't do this until after checking 'forgot', so
- # that subrepo files aren't normalized, and this op is
- # purely from data cached by the status walk above.
- if repo.dirstate.normalize(f) in repo.dirstate:
- continue
- ui.warn(_('not removing %s: '
- 'file is already untracked\n')
- % match.rel(f))
- bad.append(f)
-
- for f in forget:
- if ui.verbose or not match.exact(f):
- ui.status(_('removing %s\n') % match.rel(f))
-
- rejected = wctx.forget(forget, prefix)
- bad.extend(f for f in rejected if f in match.files())
- forgot.extend(f for f in forget if f not in rejected)
- return bad, forgot
-
-def files(ui, ctx, m, fm, fmt, subrepos):
- rev = ctx.rev()
- ret = 1
- ds = ctx.repo().dirstate
-
- for f in ctx.matches(m):
- if rev is None and ds[f] == 'r':
- continue
- fm.startitem()
- if ui.verbose:
- fc = ctx[f]
- fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
- fm.data(abspath=f)
- fm.write('path', fmt, m.rel(f))
- ret = 0
-
- for subpath in sorted(ctx.substate):
- submatch = matchmod.subdirmatcher(subpath, m)
- if (subrepos or m.exact(subpath) or any(submatch.files())):
- sub = ctx.sub(subpath)
- try:
- recurse = m.exact(subpath) or subrepos
- if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
- ret = 0
- except error.LookupError:
- ui.status(_("skipping missing subrepository: %s\n")
- % m.abs(subpath))
-
- return ret
-
-def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
- join = lambda f: os.path.join(prefix, f)
- ret = 0
- s = repo.status(match=m, clean=True)
- modified, added, deleted, clean = s[0], s[1], s[3], s[6]
-
- wctx = repo[None]
-
- if warnings is None:
- warnings = []
- warn = True
- else:
- warn = False
-
- subs = sorted(wctx.substate)
- total = len(subs)
- count = 0
- for subpath in subs:
- count += 1
- submatch = matchmod.subdirmatcher(subpath, m)
- if subrepos or m.exact(subpath) or any(submatch.files()):
- ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
- sub = wctx.sub(subpath)
- try:
- if sub.removefiles(submatch, prefix, after, force, subrepos,
- warnings):
- ret = 1
- except error.LookupError:
- warnings.append(_("skipping missing subrepository: %s\n")
- % join(subpath))
- ui.progress(_('searching'), None)
-
- # warn about failure to delete explicit files/dirs
- deleteddirs = util.dirs(deleted)
- files = m.files()
- total = len(files)
- count = 0
- for f in files:
- def insubrepo():
- for subpath in wctx.substate:
- if f.startswith(subpath + '/'):
- return True
- return False
-
- count += 1
- ui.progress(_('deleting'), count, total=total, unit=_('files'))
- isdir = f in deleteddirs or wctx.hasdir(f)
- if (f in repo.dirstate or isdir or f == '.'
- or insubrepo() or f in subs):
- continue
-
- if repo.wvfs.exists(f):
- if repo.wvfs.isdir(f):
- warnings.append(_('not removing %s: no tracked files\n')
- % m.rel(f))
- else:
- warnings.append(_('not removing %s: file is untracked\n')
- % m.rel(f))
- # missing files will generate a warning elsewhere
- ret = 1
- ui.progress(_('deleting'), None)
-
- if force:
- list = modified + deleted + clean + added
- elif after:
- list = deleted
- remaining = modified + added + clean
- total = len(remaining)
- count = 0
- for f in remaining:
- count += 1
- ui.progress(_('skipping'), count, total=total, unit=_('files'))
- if ui.verbose or (f in files):
- warnings.append(_('not removing %s: file still exists\n')
- % m.rel(f))
- ret = 1
- ui.progress(_('skipping'), None)
- else:
- list = deleted + clean
- total = len(modified) + len(added)
- count = 0
- for f in modified:
- count += 1
- ui.progress(_('skipping'), count, total=total, unit=_('files'))
- warnings.append(_('not removing %s: file is modified (use -f'
- ' to force removal)\n') % m.rel(f))
- ret = 1
- for f in added:
- count += 1
- ui.progress(_('skipping'), count, total=total, unit=_('files'))
- warnings.append(_("not removing %s: file has been marked for add"
- " (use 'hg forget' to undo add)\n") % m.rel(f))
- ret = 1
- ui.progress(_('skipping'), None)
-
- list = sorted(list)
- total = len(list)
- count = 0
- for f in list:
- count += 1
- if ui.verbose or not m.exact(f):
- ui.progress(_('deleting'), count, total=total, unit=_('files'))
- ui.status(_('removing %s\n') % m.rel(f))
- ui.progress(_('deleting'), None)
-
- with repo.wlock():
- if not after:
- for f in list:
- if f in added:
- continue # we never unlink added files on remove
- repo.wvfs.unlinkpath(f, ignoremissing=True)
- repo[None].forget(list)
-
- if warn:
- for warning in warnings:
- ui.warn(warning)
-
- return ret
-
-def _updatecatformatter(fm, ctx, matcher, path, decode):
- """Hook for adding data to the formatter used by ``hg cat``.
-
- Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
- this method first."""
- data = ctx[path].data()
- if decode:
- data = ctx.repo().wwritedata(path, data)
- fm.startitem()
- fm.write('data', '%s', data)
- fm.data(abspath=path, path=matcher.rel(path))
-
-def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
- err = 1
- opts = pycompat.byteskwargs(opts)
-
- def write(path):
- filename = None
- if fntemplate:
- filename = makefilename(repo, fntemplate, ctx.node(),
- pathname=os.path.join(prefix, path))
- # attempt to create the directory if it does not already exist
- try:
- os.makedirs(os.path.dirname(filename))
- except OSError:
- pass
- with formatter.maybereopen(basefm, filename, opts) as fm:
- _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
-
- # Automation often uses hg cat on single files, so special case it
- # for performance to avoid the cost of parsing the manifest.
- if len(matcher.files()) == 1 and not matcher.anypats():
- file = matcher.files()[0]
- mfl = repo.manifestlog
- mfnode = ctx.manifestnode()
- try:
- if mfnode and mfl[mfnode].find(file)[0]:
- write(file)
- return 0
- except KeyError:
- pass
-
- for abs in ctx.walk(matcher):
- write(abs)
- err = 0
-
- for subpath in sorted(ctx.substate):
- sub = ctx.sub(subpath)
- try:
- submatch = matchmod.subdirmatcher(subpath, matcher)
-
- if not sub.cat(submatch, basefm, fntemplate,
- os.path.join(prefix, sub._path),
- **pycompat.strkwargs(opts)):
- err = 0
- except error.RepoLookupError:
- ui.status(_("skipping missing subrepository: %s\n")
- % os.path.join(prefix, subpath))
-
- return err
-
-def commit(ui, repo, commitfunc, pats, opts):
- '''commit the specified files or all outstanding changes'''
- date = opts.get('date')
- if date:
- opts['date'] = util.parsedate(date)
- message = logmessage(ui, opts)
- matcher = scmutil.match(repo[None], pats, opts)
-
- dsguard = None
- # extract addremove carefully -- this function can be called from a command
- # that doesn't support addremove
- if opts.get('addremove'):
- dsguard = dirstateguard.dirstateguard(repo, 'commit')
- with dsguard or util.nullcontextmanager():
- if dsguard:
- if scmutil.addremove(repo, matcher, "", opts) != 0:
- raise error.Abort(
- _("failed to mark all new/missing files as added/removed"))
-
- return commitfunc(ui, repo, message, matcher, opts)
-
-def samefile(f, ctx1, ctx2):
- if f in ctx1.manifest():
- a = ctx1.filectx(f)
- if f in ctx2.manifest():
- b = ctx2.filectx(f)
- return (not a.cmp(b)
- and a.flags() == b.flags())
- else:
- return False
- else:
- return f not in ctx2.manifest()
-
-def amend(ui, repo, old, extra, pats, opts):
- # avoid cycle context -> subrepo -> cmdutil
- from . import context
-
- # amend will reuse the existing user if not specified, but the obsolete
- # marker creation requires that the current user's name is specified.
- if obsolete.isenabled(repo, obsolete.createmarkersopt):
- ui.username() # raise exception if username not set
-
- ui.note(_('amending changeset %s\n') % old)
- base = old.p1()
-
- with repo.wlock(), repo.lock(), repo.transaction('amend'):
- # Participating changesets:
- #
- # wctx o - workingctx that contains changes from working copy
- # | to go into amending commit
- # |
- # old o - changeset to amend
- # |
- # base o - first parent of the changeset to amend
- wctx = repo[None]
-
- # Copy to avoid mutating input
- extra = extra.copy()
- # Update extra dict from amended commit (e.g. to preserve graft
- # source)
- extra.update(old.extra())
-
- # Also update it from the from the wctx
- extra.update(wctx.extra())
-
- user = opts.get('user') or old.user()
- date = opts.get('date') or old.date()
-
- # Parse the date to allow comparison between date and old.date()
- date = util.parsedate(date)
-
- if len(old.parents()) > 1:
- # ctx.files() isn't reliable for merges, so fall back to the
- # slower repo.status() method
- files = set([fn for st in repo.status(base, old)[:3]
- for fn in st])
- else:
- files = set(old.files())
-
- # add/remove the files to the working copy if the "addremove" option
- # was specified.
- matcher = scmutil.match(wctx, pats, opts)
- if (opts.get('addremove')
- and scmutil.addremove(repo, matcher, "", opts)):
- raise error.Abort(
- _("failed to mark all new/missing files as added/removed"))
-
- # Check subrepos. This depends on in-place wctx._status update in
- # subrepo.precommit(). To minimize the risk of this hack, we do
- # nothing if .hgsub does not exist.
- if '.hgsub' in wctx or '.hgsub' in old:
- from . import subrepo # avoid cycle: cmdutil -> subrepo -> cmdutil
- subs, commitsubs, newsubstate = subrepo.precommit(
- ui, wctx, wctx._status, matcher)
- # amend should abort if commitsubrepos is enabled
- assert not commitsubs
- if subs:
- subrepo.writestate(repo, newsubstate)
-
- filestoamend = set(f for f in wctx.files() if matcher(f))
-
- changes = (len(filestoamend) > 0)
- if changes:
- # Recompute copies (avoid recording a -> b -> a)
- copied = copies.pathcopies(base, wctx, matcher)
- if old.p2:
- copied.update(copies.pathcopies(old.p2(), wctx, matcher))
-
- # Prune files which were reverted by the updates: if old
- # introduced file X and the file was renamed in the working
- # copy, then those two files are the same and
- # we can discard X from our list of files. Likewise if X
- # was removed, it's no longer relevant. If X is missing (aka
- # deleted), old X must be preserved.
- files.update(filestoamend)
- files = [f for f in files if (not samefile(f, wctx, base)
- or f in wctx.deleted())]
-
- def filectxfn(repo, ctx_, path):
- try:
- # If the file being considered is not amongst the files
- # to be amended, we should return the file context from the
- # old changeset. This avoids issues when only some files in
- # the working copy are being amended but there are also
- # changes to other files from the old changeset.
- if path not in filestoamend:
- return old.filectx(path)
-
- # Return None for removed files.
- if path in wctx.removed():
- return None
-
- fctx = wctx[path]
- flags = fctx.flags()
- mctx = context.memfilectx(repo, ctx_,
- fctx.path(), fctx.data(),
- islink='l' in flags,
- isexec='x' in flags,
- copied=copied.get(path))
- return mctx
- except KeyError:
- return None
- else:
- ui.note(_('copying changeset %s to %s\n') % (old, base))
-
- # Use version of files as in the old cset
- def filectxfn(repo, ctx_, path):
- try:
- return old.filectx(path)
- except KeyError:
- return None
-
- # See if we got a message from -m or -l, if not, open the editor with
- # the message of the changeset to amend.
- message = logmessage(ui, opts)
-
- editform = mergeeditform(old, 'commit.amend')
- editor = getcommiteditor(editform=editform,
- **pycompat.strkwargs(opts))
-
- if not message:
- editor = getcommiteditor(edit=True, editform=editform)
- message = old.description()
-
- pureextra = extra.copy()
- extra['amend_source'] = old.hex()
-
- new = context.memctx(repo,
- parents=[base.node(), old.p2().node()],
- text=message,
- files=files,
- filectxfn=filectxfn,
- user=user,
- date=date,
- extra=extra,
- editor=editor)
-
- newdesc = changelog.stripdesc(new.description())
- if ((not changes)
- and newdesc == old.description()
- and user == old.user()
- and date == old.date()
- and pureextra == old.extra()):
- # nothing changed. continuing here would create a new node
- # anyway because of the amend_source noise.
- #
- # This not what we expect from amend.
- return old.node()
-
- if opts.get('secret'):
- commitphase = 'secret'
- else:
- commitphase = old.phase()
- overrides = {('phases', 'new-commit'): commitphase}
- with ui.configoverride(overrides, 'amend'):
- newid = repo.commitctx(new)
-
- # Reroute the working copy parent to the new changeset
- repo.setparents(newid, nullid)
- mapping = {old.node(): (newid,)}
- obsmetadata = None
- if opts.get('note'):
- obsmetadata = {'note': opts['note']}
- scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata)
-
- # Fixing the dirstate because localrepo.commitctx does not update
- # it. This is rather convenient because we did not need to update
- # the dirstate for all the files in the new commit which commitctx
- # could have done if it updated the dirstate. Now, we can
- # selectively update the dirstate only for the amended files.
- dirstate = repo.dirstate
-
- # Update the state of the files which were added and
- # and modified in the amend to "normal" in the dirstate.
- normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
- for f in normalfiles:
- dirstate.normal(f)
-
- # Update the state of files which were removed in the amend
- # to "removed" in the dirstate.
- removedfiles = set(wctx.removed()) & filestoamend
- for f in removedfiles:
- dirstate.drop(f)
-
- return newid
-
-def commiteditor(repo, ctx, subs, editform=''):
- if ctx.description():
- return ctx.description()
- return commitforceeditor(repo, ctx, subs, editform=editform,
- unchangedmessagedetection=True)
-
-def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
- editform='', unchangedmessagedetection=False):
- if not extramsg:
- extramsg = _("Leave message empty to abort commit.")
-
- forms = [e for e in editform.split('.') if e]
- forms.insert(0, 'changeset')
- templatetext = None
- while forms:
- ref = '.'.join(forms)
- if repo.ui.config('committemplate', ref):
- templatetext = committext = buildcommittemplate(
- repo, ctx, subs, extramsg, ref)
- break
- forms.pop()
- else:
- committext = buildcommittext(repo, ctx, subs, extramsg)
-
- # run editor in the repository root
- olddir = pycompat.getcwd()
- os.chdir(repo.root)
-
- # make in-memory changes visible to external process
- tr = repo.currenttransaction()
- repo.dirstate.write(tr)
- pending = tr and tr.writepending() and repo.root
-
- editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
- editform=editform, pending=pending,
- repopath=repo.path, action='commit')
- text = editortext
-
- # strip away anything below this special string (used for editors that want
- # to display the diff)
- stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
- if stripbelow:
- text = text[:stripbelow.start()]
-
- text = re.sub("(?m)^HG:.*(\n|$)", "", text)
- os.chdir(olddir)
-
- if finishdesc:
- text = finishdesc(text)
- if not text.strip():
- raise error.Abort(_("empty commit message"))
- if unchangedmessagedetection and editortext == templatetext:
- raise error.Abort(_("commit message unchanged"))
-
- return text
-
-def buildcommittemplate(repo, ctx, subs, extramsg, ref):
- ui = repo.ui
- spec = formatter.templatespec(ref, None, None)
- t = changeset_templater(ui, repo, spec, None, {}, False)
- t.t.cache.update((k, templater.unquotestring(v))
- for k, v in repo.ui.configitems('committemplate'))
-
- if not extramsg:
- extramsg = '' # ensure that extramsg is string
-
- ui.pushbuffer()
- t.show(ctx, extramsg=extramsg)
- return ui.popbuffer()
-
-def hgprefix(msg):
- return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
-
-def buildcommittext(repo, ctx, subs, extramsg):
- edittext = []
- modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
- if ctx.description():
- edittext.append(ctx.description())
- edittext.append("")
- edittext.append("") # Empty line between message and comments.
- edittext.append(hgprefix(_("Enter commit message."
- " Lines beginning with 'HG:' are removed.")))
- edittext.append(hgprefix(extramsg))
- edittext.append("HG: --")
- edittext.append(hgprefix(_("user: %s") % ctx.user()))
- if ctx.p2():
- edittext.append(hgprefix(_("branch merge")))
- if ctx.branch():
- edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
- if bookmarks.isactivewdirparent(repo):
- edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
- edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
- edittext.extend([hgprefix(_("added %s") % f) for f in added])
- edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
- edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
- if not added and not modified and not removed:
- edittext.append(hgprefix(_("no files changed")))
- edittext.append("")
-
- return "\n".join(edittext)
-
-def commitstatus(repo, node, branch, bheads=None, opts=None):
- if opts is None:
- opts = {}
- ctx = repo[node]
- parents = ctx.parents()
-
- if (not opts.get('amend') and bheads and node not in bheads and not
- [x for x in parents if x.node() in bheads and x.branch() == branch]):
- repo.ui.status(_('created new head\n'))
- # The message is not printed for initial roots. For the other
- # changesets, it is printed in the following situations:
- #
- # Par column: for the 2 parents with ...
- # N: null or no parent
- # B: parent is on another named branch
- # C: parent is a regular non head changeset
- # H: parent was a branch head of the current branch
- # Msg column: whether we print "created new head" message
- # In the following, it is assumed that there already exists some
- # initial branch heads of the current branch, otherwise nothing is
- # printed anyway.
- #
- # Par Msg Comment
- # N N y additional topo root
- #
- # B N y additional branch root
- # C N y additional topo head
- # H N n usual case
- #
- # B B y weird additional branch root
- # C B y branch merge
- # H B n merge with named branch
- #
- # C C y additional head from merge
- # C H n merge with a head
- #
- # H H n head merge: head count decreases
-
- if not opts.get('close_branch'):
- for r in parents:
- if r.closesbranch() and r.branch() == branch:
- repo.ui.status(_('reopening closed branch head %d\n') % r)
-
- if repo.ui.debugflag:
- repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
- elif repo.ui.verbose:
- repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
-
-def postcommitstatus(repo, pats, opts):
- return repo.status(match=scmutil.match(repo[None], pats, opts))
-
-def revert(ui, repo, ctx, parents, *pats, **opts):
- opts = pycompat.byteskwargs(opts)
- parent, p2 = parents
- node = ctx.node()
-
- mf = ctx.manifest()
- if node == p2:
- parent = p2
-
- # need all matching names in dirstate and manifest of target rev,
- # so have to walk both. do not print errors if files exist in one
- # but not other. in both cases, filesets should be evaluated against
- # workingctx to get consistent result (issue4497). this means 'set:**'
- # cannot be used to select missing files from target rev.
-
- # `names` is a mapping for all elements in working copy and target revision
- # The mapping is in the form:
- # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
- names = {}
-
- with repo.wlock():
- ## filling of the `names` mapping
- # walk dirstate to fill `names`
-
- interactive = opts.get('interactive', False)
- wctx = repo[None]
- m = scmutil.match(wctx, pats, opts)
-
- # we'll need this later
- targetsubs = sorted(s for s in wctx.substate if m(s))
-
- if not m.always():
- matcher = matchmod.badmatch(m, lambda x, y: False)
- for abs in wctx.walk(matcher):
- names[abs] = m.rel(abs), m.exact(abs)
-
- # walk target manifest to fill `names`
-
- def badfn(path, msg):
- if path in names:
- return
- if path in ctx.substate:
- return
- path_ = path + '/'
- for f in names:
- if f.startswith(path_):
- return
- ui.warn("%s: %s\n" % (m.rel(path), msg))
-
- for abs in ctx.walk(matchmod.badmatch(m, badfn)):
- if abs not in names:
- names[abs] = m.rel(abs), m.exact(abs)
-
- # Find status of all file in `names`.
- m = scmutil.matchfiles(repo, names)
-
- changes = repo.status(node1=node, match=m,
- unknown=True, ignored=True, clean=True)
- else:
- changes = repo.status(node1=node, match=m)
- for kind in changes:
- for abs in kind:
- names[abs] = m.rel(abs), m.exact(abs)
-
- m = scmutil.matchfiles(repo, names)
-
- modified = set(changes.modified)
- added = set(changes.added)
- removed = set(changes.removed)
- _deleted = set(changes.deleted)
- unknown = set(changes.unknown)
- unknown.update(changes.ignored)
- clean = set(changes.clean)
- modadded = set()
-
- # We need to account for the state of the file in the dirstate,
- # even when we revert against something else than parent. This will
- # slightly alter the behavior of revert (doing back up or not, delete
- # or just forget etc).
- if parent == node:
- dsmodified = modified
- dsadded = added
- dsremoved = removed
- # store all local modifications, useful later for rename detection
- localchanges = dsmodified | dsadded
- modified, added, removed = set(), set(), set()
- else:
- changes = repo.status(node1=parent, match=m)
- dsmodified = set(changes.modified)
- dsadded = set(changes.added)
- dsremoved = set(changes.removed)
- # store all local modifications, useful later for rename detection
- localchanges = dsmodified | dsadded
-
- # only take into account for removes between wc and target
- clean |= dsremoved - removed
- dsremoved &= removed
- # distinct between dirstate remove and other
- removed -= dsremoved
-
- modadded = added & dsmodified
- added -= modadded
-
- # tell newly modified apart.
- dsmodified &= modified
- dsmodified |= modified & dsadded # dirstate added may need backup
- modified -= dsmodified
-
- # We need to wait for some post-processing to update this set
- # before making the distinction. The dirstate will be used for
- # that purpose.
- dsadded = added
-
- # in case of merge, files that are actually added can be reported as
- # modified, we need to post process the result
- if p2 != nullid:
- mergeadd = set(dsmodified)
- for path in dsmodified:
- if path in mf:
- mergeadd.remove(path)
- dsadded |= mergeadd
- dsmodified -= mergeadd
-
- # if f is a rename, update `names` to also revert the source
- cwd = repo.getcwd()
- for f in localchanges:
- src = repo.dirstate.copied(f)
- # XXX should we check for rename down to target node?
- if src and src not in names and repo.dirstate[src] == 'r':
- dsremoved.add(src)
- names[src] = (repo.pathto(src, cwd), True)
-
- # determine the exact nature of the deleted changesets
- deladded = set(_deleted)
- for path in _deleted:
- if path in mf:
- deladded.remove(path)
- deleted = _deleted - deladded
-
- # distinguish between file to forget and the other
- added = set()
- for abs in dsadded:
- if repo.dirstate[abs] != 'a':
- added.add(abs)
- dsadded -= added
-
- for abs in deladded:
- if repo.dirstate[abs] == 'a':
- dsadded.add(abs)
- deladded -= dsadded
-
- # For files marked as removed, we check if an unknown file is present at
- # the same path. If a such file exists it may need to be backed up.
- # Making the distinction at this stage helps have simpler backup
- # logic.
- removunk = set()
- for abs in removed:
- target = repo.wjoin(abs)
- if os.path.lexists(target):
- removunk.add(abs)
- removed -= removunk
-
- dsremovunk = set()
- for abs in dsremoved:
- target = repo.wjoin(abs)
- if os.path.lexists(target):
- dsremovunk.add(abs)
- dsremoved -= dsremovunk
-
- # action to be actually performed by revert
- # (<list of file>, message>) tuple
- actions = {'revert': ([], _('reverting %s\n')),
- 'add': ([], _('adding %s\n')),
- 'remove': ([], _('removing %s\n')),
- 'drop': ([], _('removing %s\n')),
- 'forget': ([], _('forgetting %s\n')),
- 'undelete': ([], _('undeleting %s\n')),
- 'noop': (None, _('no changes needed to %s\n')),
- 'unknown': (None, _('file not managed: %s\n')),
- }
-
- # "constant" that convey the backup strategy.
- # All set to `discard` if `no-backup` is set do avoid checking
- # no_backup lower in the code.
- # These values are ordered for comparison purposes
- backupinteractive = 3 # do backup if interactively modified
- backup = 2 # unconditionally do backup
- check = 1 # check if the existing file differs from target
- discard = 0 # never do backup
- if opts.get('no_backup'):
- backupinteractive = backup = check = discard
- if interactive:
- dsmodifiedbackup = backupinteractive
- else:
- dsmodifiedbackup = backup
- tobackup = set()
-
- backupanddel = actions['remove']
- if not opts.get('no_backup'):
- backupanddel = actions['drop']
-
- disptable = (
- # dispatch table:
- # file state
- # action
- # make backup
-
- ## Sets that results that will change file on disk
- # Modified compared to target, no local change
- (modified, actions['revert'], discard),
- # Modified compared to target, but local file is deleted
- (deleted, actions['revert'], discard),
- # Modified compared to target, local change
- (dsmodified, actions['revert'], dsmodifiedbackup),
- # Added since target
- (added, actions['remove'], discard),
- # Added in working directory
- (dsadded, actions['forget'], discard),
- # Added since target, have local modification
- (modadded, backupanddel, backup),
- # Added since target but file is missing in working directory
- (deladded, actions['drop'], discard),
- # Removed since target, before working copy parent
- (removed, actions['add'], discard),
- # Same as `removed` but an unknown file exists at the same path
- (removunk, actions['add'], check),
- # Removed since targe, marked as such in working copy parent
- (dsremoved, actions['undelete'], discard),
- # Same as `dsremoved` but an unknown file exists at the same path
- (dsremovunk, actions['undelete'], check),
- ## the following sets does not result in any file changes
- # File with no modification
- (clean, actions['noop'], discard),
- # Existing file, not tracked anywhere
- (unknown, actions['unknown'], discard),
- )
-
- for abs, (rel, exact) in sorted(names.items()):
- # target file to be touch on disk (relative to cwd)
- target = repo.wjoin(abs)
- # search the entry in the dispatch table.
- # if the file is in any of these sets, it was touched in the working
- # directory parent and we are sure it needs to be reverted.
- for table, (xlist, msg), dobackup in disptable:
- if abs not in table:
- continue
- if xlist is not None:
- xlist.append(abs)
- if dobackup:
- # If in interactive mode, don't automatically create
- # .orig files (issue4793)
- if dobackup == backupinteractive:
- tobackup.add(abs)
- elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
- bakname = scmutil.origpath(ui, repo, rel)
- ui.note(_('saving current version of %s as %s\n') %
- (rel, bakname))
- if not opts.get('dry_run'):
- if interactive:
- util.copyfile(target, bakname)
- else:
- util.rename(target, bakname)
- if ui.verbose or not exact:
- if not isinstance(msg, bytes):
- msg = msg(abs)
- ui.status(msg % rel)
- elif exact:
- ui.warn(msg % rel)
- break
-
- if not opts.get('dry_run'):
- needdata = ('revert', 'add', 'undelete')
- _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
- _performrevert(repo, parents, ctx, actions, interactive, tobackup)
-
- if targetsubs:
- # Revert the subrepos on the revert list
- for sub in targetsubs:
- try:
- wctx.sub(sub).revert(ctx.substate[sub], *pats,
- **pycompat.strkwargs(opts))
- except KeyError:
- raise error.Abort("subrepository '%s' does not exist in %s!"
- % (sub, short(ctx.node())))
-
-def _revertprefetch(repo, ctx, *files):
- """Let extension changing the storage layer prefetch content"""
-
-def _performrevert(repo, parents, ctx, actions, interactive=False,
- tobackup=None):
- """function that actually perform all the actions computed for revert
-
- This is an independent function to let extension to plug in and react to
- the imminent revert.
-
- Make sure you have the working directory locked when calling this function.
- """
- parent, p2 = parents
- node = ctx.node()
- excluded_files = []
- matcher_opts = {"exclude": excluded_files}
-
- def checkout(f):
- fc = ctx[f]
- repo.wwrite(f, fc.data(), fc.flags())
-
- def doremove(f):
- try:
- repo.wvfs.unlinkpath(f)
- except OSError:
- pass
- repo.dirstate.remove(f)
-
- audit_path = pathutil.pathauditor(repo.root, cached=True)
- for f in actions['forget'][0]:
- if interactive:
- choice = repo.ui.promptchoice(
- _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
- if choice == 0:
- repo.dirstate.drop(f)
- else:
- excluded_files.append(repo.wjoin(f))
- else:
- repo.dirstate.drop(f)
- for f in actions['remove'][0]:
- audit_path(f)
- if interactive:
- choice = repo.ui.promptchoice(
- _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
- if choice == 0:
- doremove(f)
- else:
- excluded_files.append(repo.wjoin(f))
- else:
- doremove(f)
- for f in actions['drop'][0]:
- audit_path(f)
- repo.dirstate.remove(f)
-
- normal = None
- if node == parent:
- # We're reverting to our parent. If possible, we'd like status
- # to report the file as clean. We have to use normallookup for
- # merges to avoid losing information about merged/dirty files.
- if p2 != nullid:
- normal = repo.dirstate.normallookup
- else:
- normal = repo.dirstate.normal
-
- newlyaddedandmodifiedfiles = set()
- if interactive:
- # Prompt the user for changes to revert
- torevert = [repo.wjoin(f) for f in actions['revert'][0]]
- m = scmutil.match(ctx, torevert, matcher_opts)
- diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
- diffopts.nodates = True
- diffopts.git = True
- operation = 'discard'
- reversehunks = True
- if node != parent:
- operation = 'apply'
- reversehunks = False
- if reversehunks:
- diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
- else:
- diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
- originalchunks = patch.parsepatch(diff)
-
- try:
-
- chunks, opts = recordfilter(repo.ui, originalchunks,
- operation=operation)
- if reversehunks:
- chunks = patch.reversehunks(chunks)
-
- except error.PatchError as err:
- raise error.Abort(_('error parsing patch: %s') % err)
-
- newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
- if tobackup is None:
- tobackup = set()
- # Apply changes
- fp = stringio()
- for c in chunks:
- # Create a backup file only if this hunk should be backed up
- if ishunk(c) and c.header.filename() in tobackup:
- abs = c.header.filename()
- target = repo.wjoin(abs)
- bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
- util.copyfile(target, bakname)
- tobackup.remove(abs)
- c.write(fp)
- dopatch = fp.tell()
- fp.seek(0)
- if dopatch:
- try:
- patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
- except error.PatchError as err:
- raise error.Abort(str(err))
- del fp
- else:
- for f in actions['revert'][0]:
- checkout(f)
- if normal:
- normal(f)
-
- for f in actions['add'][0]:
- # Don't checkout modified files, they are already created by the diff
- if f not in newlyaddedandmodifiedfiles:
- checkout(f)
- repo.dirstate.add(f)
-
- normal = repo.dirstate.normallookup
- if node == parent and p2 == nullid:
- normal = repo.dirstate.normal
- for f in actions['undelete'][0]:
- checkout(f)
- normal(f)
-
- copied = copies.pathcopies(repo[parent], ctx)
-
- for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
- if f in copied:
- repo.dirstate.copy(copied[f], f)
-
-class command(registrar.command):
- """deprecated: used registrar.command instead"""
- def _doregister(self, func, name, *args, **kwargs):
- func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
- return super(command, self)._doregister(func, name, *args, **kwargs)
-
-# a list of (ui, repo, otherpeer, opts, missing) functions called by
-# commands.outgoing. "missing" is "missing" of the result of
-# "findcommonoutgoing()"
-outgoinghooks = util.hooks()
-
-# a list of (ui, repo) functions called by commands.summary
-summaryhooks = util.hooks()
-
-# a list of (ui, repo, opts, changes) functions called by commands.summary.
-#
-# functions should return tuple of booleans below, if 'changes' is None:
-# (whether-incomings-are-needed, whether-outgoings-are-needed)
-#
-# otherwise, 'changes' is a tuple of tuples below:
-# - (sourceurl, sourcebranch, sourcepeer, incoming)
-# - (desturl, destbranch, destpeer, outgoing)
-summaryremotehooks = util.hooks()
-
-# A list of state files kept by multistep operations like graft.
-# Since graft cannot be aborted, it is considered 'clearable' by update.
-# note: bisect is intentionally excluded
-# (state file, clearable, allowcommit, error, hint)
-unfinishedstates = [
- ('graftstate', True, False, _('graft in progress'),
- _("use 'hg graft --continue' or 'hg update' to abort")),
- ('updatestate', True, False, _('last update was interrupted'),
- _("use 'hg update' to get a consistent checkout"))
- ]
-
-def checkunfinished(repo, commit=False):
- '''Look for an unfinished multistep operation, like graft, and abort
- if found. It's probably good to check this right before
- bailifchanged().
- '''
- for f, clearable, allowcommit, msg, hint in unfinishedstates:
- if commit and allowcommit:
- continue
- if repo.vfs.exists(f):
- raise error.Abort(msg, hint=hint)
-
-def clearunfinished(repo):
- '''Check for unfinished operations (as above), and clear the ones
- that are clearable.
- '''
- for f, clearable, allowcommit, msg, hint in unfinishedstates:
- if not clearable and repo.vfs.exists(f):
- raise error.Abort(msg, hint=hint)
- for f, clearable, allowcommit, msg, hint in unfinishedstates:
- if clearable and repo.vfs.exists(f):
- util.unlink(repo.vfs.join(f))
-
-afterresolvedstates = [
- ('graftstate',
- _('hg graft --continue')),
- ]
-
-def howtocontinue(repo):
- '''Check for an unfinished operation and return the command to finish
- it.
-
- afterresolvedstates tuples define a .hg/{file} and the corresponding
- command needed to finish it.
-
- Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
- a boolean.
- '''
- contmsg = _("continue: %s")
- for f, msg in afterresolvedstates:
- if repo.vfs.exists(f):
- return contmsg % msg, True
- if repo[None].dirty(missing=True, merge=False, branch=False):
- return contmsg % _("hg commit"), False
- return None, None
-
-def checkafterresolved(repo):
- '''Inform the user about the next action after completing hg resolve
-
- If there's a matching afterresolvedstates, howtocontinue will yield
- repo.ui.warn as the reporter.
-
- Otherwise, it will yield repo.ui.note.
- '''
- msg, warning = howtocontinue(repo)
- if msg is not None:
- if warning:
- repo.ui.warn("%s\n" % msg)
- else:
- repo.ui.note("%s\n" % msg)
-
-def wrongtooltocontinue(repo, task):
- '''Raise an abort suggesting how to properly continue if there is an
- active task.
-
- Uses howtocontinue() to find the active task.
-
- If there's no task (repo.ui.note for 'hg commit'), it does not offer
- a hint.
- '''
- after = howtocontinue(repo)
- hint = None
- if after[1]:
- hint = after[0]
- raise error.Abort(_('no %s in progress') % task, hint=hint)
diff --git a/tests/test-glog.t b/tests/test-glog.t
--- a/tests/test-glog.t
+++ b/tests/test-glog.t
@@ -87,16 +87,17 @@ o (0) root
> cmdutil,
> commands,
> extensions,
+ > logcmdutil,
> revsetlang,
> smartset,
> )
>
> def logrevset(repo, pats, opts):
- > revs = cmdutil._logrevs(repo, opts)
+ > revs = logcmdutil._logrevs(repo, opts)
> if not revs:
> return None
- > match, pats, slowpath = cmdutil._makelogmatcher(repo, revs, pats, opts)
- > return cmdutil._makelogrevset(repo, match, pats, slowpath, opts)
+ > match, pats, slowpath = logcmdutil._makelogmatcher(repo, revs, pats, opts)
+ > return logcmdutil._makelogrevset(repo, match, pats, slowpath, opts)
>
> def uisetup(ui):
> def printrevset(orig, repo, pats, opts):
More information about the Mercurial-devel
mailing list