narrowcommands.py
430 lines
| 17.0 KiB
| text/x-python
|
PythonLexer
Augie Fackler
|
r36096 | # 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 | ||||
Pulkit Goyal
|
r39283 | import os | ||
Augie Fackler
|
r36096 | |||
from mercurial.i18n import _ | ||||
from mercurial import ( | ||||
cmdutil, | ||||
commands, | ||||
discovery, | ||||
error, | ||||
exchange, | ||||
extensions, | ||||
hg, | ||||
merge, | ||||
Gregory Szorc
|
r36178 | narrowspec, | ||
Augie Fackler
|
r36096 | node, | ||
Augie Fackler
|
r36172 | pycompat, | ||
Augie Fackler
|
r36096 | registrar, | ||
repair, | ||||
Martin von Zweigbergk
|
r38871 | repository, | ||
Augie Fackler
|
r36096 | repoview, | ||
Pulkit Goyal
|
r39283 | sparse, | ||
Augie Fackler
|
r36096 | util, | ||
) | ||||
from . import ( | ||||
narrowbundle2, | ||||
) | ||||
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"))) | ||||
Pulkit Goyal
|
r39283 | entry[1].append(('', 'narrowspec', '', | ||
_("read narrowspecs from file"))) | ||||
Augie Fackler
|
r36096 | # 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().""" | ||||
Augie Fackler
|
r36172 | opts = pycompat.byteskwargs(opts) | ||
Augie Fackler
|
r36096 | wrappedextraprepare = util.nullcontextmanager() | ||
opts_narrow = opts['narrow'] | ||||
Pulkit Goyal
|
r39283 | narrowspecfile = opts['narrowspec'] | ||
if narrowspecfile: | ||||
filepath = os.path.join(pycompat.getcwd(), narrowspecfile) | ||||
ui.status(_("reading narrowspec from '%s'\n") % filepath) | ||||
try: | ||||
fp = open(filepath, 'rb') | ||||
except IOError: | ||||
raise error.Abort(_("file '%s' not found") % filepath) | ||||
includes, excludes, profiles = sparse.parseconfig(ui, fp.read(), | ||||
'narrow') | ||||
if profiles: | ||||
raise error.Abort(_("cannot specify other files using '%include' in" | ||||
" narrowspec")) | ||||
# narrowspec is passed so we should assume that user wants narrow clone | ||||
opts_narrow = True | ||||
opts['include'].extend(includes) | ||||
opts['exclude'].extend(excludes) | ||||
Augie Fackler
|
r36096 | 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:.'} | ||||
Martin von Zweigbergk
|
r36487 | pullop.repo.setnarrowpats(includepats, excludepats) | ||
Augie Fackler
|
r36096 | |||
# 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): | ||||
if opts_narrow: | ||||
Martin von Zweigbergk
|
r38871 | repo.requirements.add(repository.NARROW_REQUIREMENT) | ||
Augie Fackler
|
r36096 | repo._writerequirements() | ||
return orig(repo, *args, **kwargs) | ||||
wrappedpull = extensions.wrappedfunction(exchange, 'pull', pullnarrow) | ||||
with wrappedextraprepare, wrappedpull: | ||||
Augie Fackler
|
r36172 | return orig(ui, repo, *args, **pycompat.strkwargs(opts)) | ||
Augie Fackler
|
r36096 | |||
def pullnarrowcmd(orig, ui, repo, *args, **opts): | ||||
"""Wraps pull command to allow modifying narrow spec.""" | ||||
wrappedextraprepare = util.nullcontextmanager() | ||||
Martin von Zweigbergk
|
r38871 | if repository.NARROW_REQUIREMENT in repo.requirements: | ||
Augie Fackler
|
r36096 | |||
def pullbundle2extraprepare_widen(orig, pullop, kwargs): | ||||
orig(pullop, kwargs) | ||||
Pulkit Goyal
|
r36474 | if opts.get(r'depth'): | ||
kwargs['depth'] = opts[r'depth'] | ||||
Augie Fackler
|
r36096 | 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.""" | ||||
Martin von Zweigbergk
|
r38871 | if repository.NARROW_REQUIREMENT in repo.requirements: | ||
Augie Fackler
|
r36096 | repo_includes, repo_excludes = repo.narrowpats | ||
Augie Fackler
|
r36370 | includes = set(opts.get(r'include', [])) | ||
excludes = set(opts.get(r'exclude', [])) | ||||
Augie Fackler
|
r36117 | includes, excludes, unused_invalid = narrowspec.restrictpatterns( | ||
Augie Fackler
|
r36096 | includes, excludes, repo_includes, repo_excludes) | ||
if includes: | ||||
Augie Fackler
|
r36370 | opts[r'include'] = includes | ||
Augie Fackler
|
r36096 | if excludes: | ||
Augie Fackler
|
r36370 | opts[r'exclude'] = excludes | ||
Augie Fackler
|
r36096 | return orig(ui, repo, *args, **opts) | ||
def pullbundle2extraprepare(orig, pullop, kwargs): | ||||
repo = pullop.repo | ||||
Martin von Zweigbergk
|
r38871 | if repository.NARROW_REQUIREMENT not in repo.requirements: | ||
Augie Fackler
|
r36096 | return orig(pullop, kwargs) | ||
Augie Fackler
|
r36103 | if narrowbundle2.NARROWCAP not in pullop.remotebundle2caps: | ||
Augie Fackler
|
r36096 | 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')) | ||||
Augie Fackler
|
r38547 | with ui.uninterruptable(): | ||
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') | ||||
Augie Fackler
|
r36096 | |||
Augie Fackler
|
r38547 | 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) | ||||
Augie Fackler
|
r36096 | |||
Augie Fackler
|
r38547 | repo.destroying() | ||
Augie Fackler
|
r36096 | |||
Augie Fackler
|
r38547 | with repo.transaction("narrowing"): | ||
for f in todelete: | ||||
ui.status(_('deleting %s\n') % f) | ||||
util.unlinkpath(repo.svfs.join(f)) | ||||
repo.store.markremoved(f) | ||||
Augie Fackler
|
r36096 | |||
Augie Fackler
|
r38547 | for f in repo.dirstate: | ||
if not newmatch(f): | ||||
repo.dirstate.drop(f) | ||||
repo.wvfs.unlinkpath(f) | ||||
repo.setnarrowpats(newincludes, newexcludes) | ||||
Augie Fackler
|
r36096 | |||
Augie Fackler
|
r38547 | repo.destroyed() | ||
Augie Fackler
|
r36096 | |||
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 | ||||
Pulkit Goyal
|
r39392 | kwargs['widen'] = True | ||
Augie Fackler
|
r36096 | 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 | ||||
Augie Fackler
|
r38547 | with ui.uninterruptable(): | ||
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) | ||||
Augie Fackler
|
r36096 | |||
Pulkit Goyal
|
r39393 | repo.setnewnarrowpats() | ||
Augie Fackler
|
r38547 | actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()} | ||
addgaction = actions['g'].append | ||||
Augie Fackler
|
r36096 | |||
Augie Fackler
|
r38547 | 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")) | ||||
Augie Fackler
|
r36096 | |||
Augie Fackler
|
r38547 | merge.applyupdates(repo, actions, wctx=repo[None], | ||
mctx=repo['.'], overwrite=False) | ||||
merge.recordupdates(repo, actions, branchmerge=False) | ||||
Augie Fackler
|
r36096 | |||
# 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. | ||||
""" | ||||
Augie Fackler
|
r36184 | opts = pycompat.byteskwargs(opts) | ||
Martin von Zweigbergk
|
r38871 | if repository.NARROW_REQUIREMENT not in repo.requirements: | ||
Augie Fackler
|
r36096 | 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 | ||||