hgext/narrow/narrowcommands.py
changeset 36079 a2a6e724d61a
child 36086 bc01f48c18cc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/narrow/narrowcommands.py	Mon Jan 29 16:19:33 2018 -0500
@@ -0,0 +1,402 @@
+# narrowcommands.py - command modifications for narrowhg extension
+#
+# Copyright 2017 Google, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+from __future__ import absolute_import
+
+import itertools
+
+from mercurial.i18n import _
+from mercurial import (
+    cmdutil,
+    commands,
+    discovery,
+    error,
+    exchange,
+    extensions,
+    hg,
+    merge,
+    node,
+    registrar,
+    repair,
+    repoview,
+    util,
+)
+
+from . import (
+    narrowbundle2,
+    narrowrepo,
+    narrowspec,
+)
+
+table = {}
+command = registrar.command(table)
+
+def setup():
+    """Wraps user-facing mercurial commands with narrow-aware versions."""
+
+    entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
+    entry[1].append(('', 'narrow', None,
+                     _("create a narrow clone of select files")))
+    entry[1].append(('', 'depth', '',
+                     _("limit the history fetched by distance from heads")))
+    # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
+    if 'sparse' not in extensions.enabled():
+        entry[1].append(('', 'include', [],
+                         _("specifically fetch this file/directory")))
+        entry[1].append(
+            ('', 'exclude', [],
+             _("do not fetch this file/directory, even if included")))
+
+    entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
+    entry[1].append(('', 'depth', '',
+                     _("limit the history fetched by distance from heads")))
+
+    extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
+
+def expandpull(pullop, includepats, excludepats):
+    if not narrowspec.needsexpansion(includepats):
+        return includepats, excludepats
+
+    heads = pullop.heads or pullop.rheads
+    includepats, excludepats = pullop.remote.expandnarrow(
+        includepats, excludepats, heads)
+    pullop.repo.ui.debug('Expanded narrowspec to inc=%s, exc=%s\n' % (
+        includepats, excludepats))
+    return set(includepats), set(excludepats)
+
+def clonenarrowcmd(orig, ui, repo, *args, **opts):
+    """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
+    wrappedextraprepare = util.nullcontextmanager()
+    opts_narrow = opts['narrow']
+    if opts_narrow:
+        def pullbundle2extraprepare_widen(orig, pullop, kwargs):
+            # Create narrow spec patterns from clone flags
+            includepats = narrowspec.parsepatterns(opts['include'])
+            excludepats = narrowspec.parsepatterns(opts['exclude'])
+
+            # If necessary, ask the server to expand the narrowspec.
+            includepats, excludepats = expandpull(
+                pullop, includepats, excludepats)
+
+            if not includepats and excludepats:
+                # If nothing was included, we assume the user meant to include
+                # everything, except what they asked to exclude.
+                includepats = {'path:.'}
+
+            narrowspec.save(pullop.repo, includepats, excludepats)
+
+            # This will populate 'includepats' etc with the values from the
+            # narrowspec we just saved.
+            orig(pullop, kwargs)
+
+            if opts.get('depth'):
+                kwargs['depth'] = opts['depth']
+        wrappedextraprepare = extensions.wrappedfunction(exchange,
+            '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
+
+    def pullnarrow(orig, repo, *args, **kwargs):
+        narrowrepo.wraprepo(repo.unfiltered(), opts_narrow)
+        if isinstance(repo, repoview.repoview):
+            repo.__class__.__bases__ = (repo.__class__.__bases__[0],
+                                        repo.unfiltered().__class__)
+        if opts_narrow:
+            repo.requirements.add(narrowrepo.requirement)
+            repo._writerequirements()
+
+        return orig(repo, *args, **kwargs)
+
+    wrappedpull = extensions.wrappedfunction(exchange, 'pull', pullnarrow)
+
+    with wrappedextraprepare, wrappedpull:
+        return orig(ui, repo, *args, **opts)
+
+def pullnarrowcmd(orig, ui, repo, *args, **opts):
+    """Wraps pull command to allow modifying narrow spec."""
+    wrappedextraprepare = util.nullcontextmanager()
+    if narrowrepo.requirement in repo.requirements:
+
+        def pullbundle2extraprepare_widen(orig, pullop, kwargs):
+            orig(pullop, kwargs)
+            if opts.get('depth'):
+                kwargs['depth'] = opts['depth']
+        wrappedextraprepare = extensions.wrappedfunction(exchange,
+            '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
+
+    with wrappedextraprepare:
+        return orig(ui, repo, *args, **opts)
+
+def archivenarrowcmd(orig, ui, repo, *args, **opts):
+    """Wraps archive command to narrow the default includes."""
+    if narrowrepo.requirement in repo.requirements:
+        repo_includes, repo_excludes = repo.narrowpats
+        includes = set(opts.get('include', []))
+        excludes = set(opts.get('exclude', []))
+        includes, excludes = narrowspec.restrictpatterns(
+            includes, excludes, repo_includes, repo_excludes)
+        if includes:
+            opts['include'] = includes
+        if excludes:
+            opts['exclude'] = excludes
+    return orig(ui, repo, *args, **opts)
+
+def pullbundle2extraprepare(orig, pullop, kwargs):
+    repo = pullop.repo
+    if narrowrepo.requirement not in repo.requirements:
+        return orig(pullop, kwargs)
+
+    if narrowbundle2.narrowcap not in pullop.remotebundle2caps:
+        raise error.Abort(_("server doesn't support narrow clones"))
+    orig(pullop, kwargs)
+    kwargs['narrow'] = True
+    include, exclude = repo.narrowpats
+    kwargs['oldincludepats'] = include
+    kwargs['oldexcludepats'] = exclude
+    kwargs['includepats'] = include
+    kwargs['excludepats'] = exclude
+    kwargs['known'] = [node.hex(ctx.node()) for ctx in
+                       repo.set('::%ln', pullop.common)
+                       if ctx.node() != node.nullid]
+    if not kwargs['known']:
+        # Mercurial serialized an empty list as '' and deserializes it as
+        # [''], so delete it instead to avoid handling the empty string on the
+        # server.
+        del kwargs['known']
+
+extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
+                        pullbundle2extraprepare)
+
+def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
+            newincludes, newexcludes, force):
+    oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
+    newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
+
+    # This is essentially doing "hg outgoing" to find all local-only
+    # commits. We will then check that the local-only commits don't
+    # have any changes to files that will be untracked.
+    unfi = repo.unfiltered()
+    outgoing = discovery.findcommonoutgoing(unfi, remote,
+                                            commoninc=commoninc)
+    ui.status(_('looking for local changes to affected paths\n'))
+    localnodes = []
+    for n in itertools.chain(outgoing.missing, outgoing.excluded):
+        if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
+            localnodes.append(n)
+    revstostrip = unfi.revs('descendants(%ln)', localnodes)
+    hiddenrevs = repoview.filterrevs(repo, 'visible')
+    visibletostrip = list(repo.changelog.node(r)
+                          for r in (revstostrip - hiddenrevs))
+    if visibletostrip:
+        ui.status(_('The following changeset(s) or their ancestors have '
+                    'local changes not on the remote:\n'))
+        maxnodes = 10
+        if ui.verbose or len(visibletostrip) <= maxnodes:
+            for n in visibletostrip:
+                ui.status('%s\n' % node.short(n))
+        else:
+            for n in visibletostrip[:maxnodes]:
+                ui.status('%s\n' % node.short(n))
+            ui.status(_('...and %d more, use --verbose to list all\n') %
+                      (len(visibletostrip) - maxnodes))
+        if not force:
+            raise error.Abort(_('local changes found'),
+                              hint=_('use --force-delete-local-changes to '
+                                     'ignore'))
+
+    if revstostrip:
+        tostrip = [unfi.changelog.node(r) for r in revstostrip]
+        if repo['.'].node() in tostrip:
+            # stripping working copy, so move to a different commit first
+            urev = max(repo.revs('(::%n) - %ln + null',
+                                 repo['.'].node(), visibletostrip))
+            hg.clean(repo, urev)
+        repair.strip(ui, unfi, tostrip, topic='narrow')
+
+    todelete = []
+    for f, f2, size in repo.store.datafiles():
+        if f.startswith('data/'):
+            file = f[5:-2]
+            if not newmatch(file):
+                todelete.append(f)
+        elif f.startswith('meta/'):
+            dir = f[5:-13]
+            dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
+            include = True
+            for d in dirs:
+                visit = newmatch.visitdir(d)
+                if not visit:
+                    include = False
+                    break
+                if visit == 'all':
+                    break
+            if not include:
+                todelete.append(f)
+
+    repo.destroying()
+
+    with repo.transaction("narrowing"):
+        for f in todelete:
+            ui.status(_('deleting %s\n') % f)
+            util.unlinkpath(repo.svfs.join(f))
+            repo.store.markremoved(f)
+
+        for f in repo.dirstate:
+            if not newmatch(f):
+                repo.dirstate.drop(f)
+                repo.wvfs.unlinkpath(f)
+        repo.setnarrowpats(newincludes, newexcludes)
+
+    repo.destroyed()
+
+def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
+    newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
+
+    # TODO(martinvonz): Get expansion working with widening/narrowing.
+    if narrowspec.needsexpansion(newincludes):
+        raise error.Abort('Expansion not yet supported on pull')
+
+    def pullbundle2extraprepare_widen(orig, pullop, kwargs):
+        orig(pullop, kwargs)
+        # The old{in,ex}cludepats have already been set by orig()
+        kwargs['includepats'] = newincludes
+        kwargs['excludepats'] = newexcludes
+    wrappedextraprepare = extensions.wrappedfunction(exchange,
+        '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
+
+    # define a function that narrowbundle2 can call after creating the
+    # backup bundle, but before applying the bundle from the server
+    def setnewnarrowpats():
+        repo.setnarrowpats(newincludes, newexcludes)
+    repo.setnewnarrowpats = setnewnarrowpats
+
+    ds = repo.dirstate
+    p1, p2 = ds.p1(), ds.p2()
+    with ds.parentchange():
+        ds.setparents(node.nullid, node.nullid)
+    common = commoninc[0]
+    with wrappedextraprepare:
+        exchange.pull(repo, remote, heads=common)
+    with ds.parentchange():
+        ds.setparents(p1, p2)
+
+    actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
+    addgaction = actions['g'].append
+
+    mf = repo['.'].manifest().matches(newmatch)
+    for f, fn in mf.iteritems():
+        if f not in repo.dirstate:
+            addgaction((f, (mf.flags(f), False),
+                        "add from widened narrow clone"))
+
+    merge.applyupdates(repo, actions, wctx=repo[None],
+                       mctx=repo['.'], overwrite=False)
+    merge.recordupdates(repo, actions, branchmerge=False)
+
+# TODO(rdamazio): Make new matcher format and update description
+@command('tracked',
+    [('', 'addinclude', [], _('new paths to include')),
+     ('', 'removeinclude', [], _('old paths to no longer include')),
+     ('', 'addexclude', [], _('new paths to exclude')),
+     ('', 'removeexclude', [], _('old paths to no longer exclude')),
+     ('', 'clear', False, _('whether to replace the existing narrowspec')),
+     ('', 'force-delete-local-changes', False,
+       _('forces deletion of local changes when narrowing')),
+    ] + commands.remoteopts,
+    _('[OPTIONS]... [REMOTE]'),
+    inferrepo=True)
+def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
+    """show or change the current narrowspec
+
+    With no argument, shows the current narrowspec entries, one per line. Each
+    line will be prefixed with 'I' or 'X' for included or excluded patterns,
+    respectively.
+
+    The narrowspec is comprised of expressions to match remote files and/or
+    directories that should be pulled into your client.
+    The narrowspec has *include* and *exclude* expressions, with excludes always
+    trumping includes: that is, if a file matches an exclude expression, it will
+    be excluded even if it also matches an include expression.
+    Excluding files that were never included has no effect.
+
+    Each included or excluded entry is in the format described by
+    'hg help patterns'.
+
+    The options allow you to add or remove included and excluded expressions.
+
+    If --clear is specified, then all previous includes and excludes are DROPPED
+    and replaced by the new ones specified to --addinclude and --addexclude.
+    If --clear is specified without any further options, the narrowspec will be
+    empty and will not match any files.
+    """
+    if narrowrepo.requirement not in repo.requirements:
+        ui.warn(_('The narrow command is only supported on respositories cloned'
+                  ' with --narrow.\n'))
+        return 1
+
+    # Before supporting, decide whether it "hg tracked --clear" should mean
+    # tracking no paths or all paths.
+    if opts['clear']:
+        ui.warn(_('The --clear option is not yet supported.\n'))
+        return 1
+
+    if narrowspec.needsexpansion(opts['addinclude'] + opts['addexclude']):
+        raise error.Abort('Expansion not yet supported on widen/narrow')
+
+    addedincludes = narrowspec.parsepatterns(opts['addinclude'])
+    removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
+    addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
+    removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
+    widening = addedincludes or removedexcludes
+    narrowing = removedincludes or addedexcludes
+    only_show = not widening and not narrowing
+
+    # Only print the current narrowspec.
+    if only_show:
+        include, exclude = repo.narrowpats
+
+        ui.pager('tracked')
+        fm = ui.formatter('narrow', opts)
+        for i in sorted(include):
+            fm.startitem()
+            fm.write('status', '%s ', 'I', label='narrow.included')
+            fm.write('pat', '%s\n', i, label='narrow.included')
+        for i in sorted(exclude):
+            fm.startitem()
+            fm.write('status', '%s ', 'X', label='narrow.excluded')
+            fm.write('pat', '%s\n', i, label='narrow.excluded')
+        fm.end()
+        return 0
+
+    with repo.wlock(), repo.lock():
+        cmdutil.bailifchanged(repo)
+
+        # Find the revisions we have in common with the remote. These will
+        # be used for finding local-only changes for narrowing. They will
+        # also define the set of revisions to update for widening.
+        remotepath = ui.expandpath(remotepath or 'default')
+        url, branches = hg.parseurl(remotepath)
+        ui.status(_('comparing with %s\n') % util.hidepassword(url))
+        remote = hg.peer(repo, opts, url)
+        commoninc = discovery.findcommonincoming(repo, remote)
+
+        oldincludes, oldexcludes = repo.narrowpats
+        if narrowing:
+            newincludes = oldincludes - removedincludes
+            newexcludes = oldexcludes | addedexcludes
+            _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
+                    newincludes, newexcludes,
+                    opts['force_delete_local_changes'])
+            # _narrow() updated the narrowspec and _widen() below needs to
+            # use the updated values as its base (otherwise removed includes
+            # and addedexcludes will be lost in the resulting narrowspec)
+            oldincludes = newincludes
+            oldexcludes = newexcludes
+
+        if widening:
+            newincludes = oldincludes | addedincludes
+            newexcludes = oldexcludes - removedexcludes
+            _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
+
+    return 0