comparison hgext/narrow/narrowcommands.py @ 36079:a2a6e724d61a

narrow: import experimental extension from narrowhg revision cb51d673e9c5 Adjustments: * renamed src to hgext/narrow * marked extension experimental * added correct copyright header where it was missing * updated hgrc extension enable line in library.sh * renamed library.sh to narrow-library.sh * dropped all files from repo root as they're not interesting * dropped test-pyflakes.t, test-check-code.t and test-check-py3-compat.t * renamed remaining tests to all be test-narrow-* when they didn't already * fixed test-narrow-expanddirstate.t to refer to narrow and not narrowhg * fixed tests that wanted `update -C .` instead of `merge --abort` * corrected a two-space indent in narrowspec.py * added a missing _() in narrowcommands.py * fixed imports to pass the import checker * narrow only adds its --include and --exclude to clone if sparse isn't enabled to avoid breaking test-duplicateoptions.py. This is a kludge, and we'll need to come up with a better solution in the future. These were more or less the minimum to import something that would pass tests and not create a bunch of files we'll never use. Changes I intend to make as followups: * rework the test-narrow-*-tree.t tests to use the new testcases functionality in run-tests.py * remove lots of monkeypatches of core things Differential Revision: https://phab.mercurial-scm.org/D1974
author Augie Fackler <augie@google.com>
date Mon, 29 Jan 2018 16:19:33 -0500
parents
children bc01f48c18cc
comparison
equal deleted inserted replaced
36078:7f68235f23ff 36079:a2a6e724d61a
1 # narrowcommands.py - command modifications for narrowhg extension
2 #
3 # Copyright 2017 Google, Inc.
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
8
9 import itertools
10
11 from mercurial.i18n import _
12 from mercurial import (
13 cmdutil,
14 commands,
15 discovery,
16 error,
17 exchange,
18 extensions,
19 hg,
20 merge,
21 node,
22 registrar,
23 repair,
24 repoview,
25 util,
26 )
27
28 from . import (
29 narrowbundle2,
30 narrowrepo,
31 narrowspec,
32 )
33
34 table = {}
35 command = registrar.command(table)
36
37 def setup():
38 """Wraps user-facing mercurial commands with narrow-aware versions."""
39
40 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
41 entry[1].append(('', 'narrow', None,
42 _("create a narrow clone of select files")))
43 entry[1].append(('', 'depth', '',
44 _("limit the history fetched by distance from heads")))
45 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
46 if 'sparse' not in extensions.enabled():
47 entry[1].append(('', 'include', [],
48 _("specifically fetch this file/directory")))
49 entry[1].append(
50 ('', 'exclude', [],
51 _("do not fetch this file/directory, even if included")))
52
53 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
54 entry[1].append(('', 'depth', '',
55 _("limit the history fetched by distance from heads")))
56
57 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
58
59 def expandpull(pullop, includepats, excludepats):
60 if not narrowspec.needsexpansion(includepats):
61 return includepats, excludepats
62
63 heads = pullop.heads or pullop.rheads
64 includepats, excludepats = pullop.remote.expandnarrow(
65 includepats, excludepats, heads)
66 pullop.repo.ui.debug('Expanded narrowspec to inc=%s, exc=%s\n' % (
67 includepats, excludepats))
68 return set(includepats), set(excludepats)
69
70 def clonenarrowcmd(orig, ui, repo, *args, **opts):
71 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
72 wrappedextraprepare = util.nullcontextmanager()
73 opts_narrow = opts['narrow']
74 if opts_narrow:
75 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
76 # Create narrow spec patterns from clone flags
77 includepats = narrowspec.parsepatterns(opts['include'])
78 excludepats = narrowspec.parsepatterns(opts['exclude'])
79
80 # If necessary, ask the server to expand the narrowspec.
81 includepats, excludepats = expandpull(
82 pullop, includepats, excludepats)
83
84 if not includepats and excludepats:
85 # If nothing was included, we assume the user meant to include
86 # everything, except what they asked to exclude.
87 includepats = {'path:.'}
88
89 narrowspec.save(pullop.repo, includepats, excludepats)
90
91 # This will populate 'includepats' etc with the values from the
92 # narrowspec we just saved.
93 orig(pullop, kwargs)
94
95 if opts.get('depth'):
96 kwargs['depth'] = opts['depth']
97 wrappedextraprepare = extensions.wrappedfunction(exchange,
98 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
99
100 def pullnarrow(orig, repo, *args, **kwargs):
101 narrowrepo.wraprepo(repo.unfiltered(), opts_narrow)
102 if isinstance(repo, repoview.repoview):
103 repo.__class__.__bases__ = (repo.__class__.__bases__[0],
104 repo.unfiltered().__class__)
105 if opts_narrow:
106 repo.requirements.add(narrowrepo.requirement)
107 repo._writerequirements()
108
109 return orig(repo, *args, **kwargs)
110
111 wrappedpull = extensions.wrappedfunction(exchange, 'pull', pullnarrow)
112
113 with wrappedextraprepare, wrappedpull:
114 return orig(ui, repo, *args, **opts)
115
116 def pullnarrowcmd(orig, ui, repo, *args, **opts):
117 """Wraps pull command to allow modifying narrow spec."""
118 wrappedextraprepare = util.nullcontextmanager()
119 if narrowrepo.requirement in repo.requirements:
120
121 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
122 orig(pullop, kwargs)
123 if opts.get('depth'):
124 kwargs['depth'] = opts['depth']
125 wrappedextraprepare = extensions.wrappedfunction(exchange,
126 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
127
128 with wrappedextraprepare:
129 return orig(ui, repo, *args, **opts)
130
131 def archivenarrowcmd(orig, ui, repo, *args, **opts):
132 """Wraps archive command to narrow the default includes."""
133 if narrowrepo.requirement in repo.requirements:
134 repo_includes, repo_excludes = repo.narrowpats
135 includes = set(opts.get('include', []))
136 excludes = set(opts.get('exclude', []))
137 includes, excludes = narrowspec.restrictpatterns(
138 includes, excludes, repo_includes, repo_excludes)
139 if includes:
140 opts['include'] = includes
141 if excludes:
142 opts['exclude'] = excludes
143 return orig(ui, repo, *args, **opts)
144
145 def pullbundle2extraprepare(orig, pullop, kwargs):
146 repo = pullop.repo
147 if narrowrepo.requirement not in repo.requirements:
148 return orig(pullop, kwargs)
149
150 if narrowbundle2.narrowcap not in pullop.remotebundle2caps:
151 raise error.Abort(_("server doesn't support narrow clones"))
152 orig(pullop, kwargs)
153 kwargs['narrow'] = True
154 include, exclude = repo.narrowpats
155 kwargs['oldincludepats'] = include
156 kwargs['oldexcludepats'] = exclude
157 kwargs['includepats'] = include
158 kwargs['excludepats'] = exclude
159 kwargs['known'] = [node.hex(ctx.node()) for ctx in
160 repo.set('::%ln', pullop.common)
161 if ctx.node() != node.nullid]
162 if not kwargs['known']:
163 # Mercurial serialized an empty list as '' and deserializes it as
164 # [''], so delete it instead to avoid handling the empty string on the
165 # server.
166 del kwargs['known']
167
168 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
169 pullbundle2extraprepare)
170
171 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
172 newincludes, newexcludes, force):
173 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
174 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
175
176 # This is essentially doing "hg outgoing" to find all local-only
177 # commits. We will then check that the local-only commits don't
178 # have any changes to files that will be untracked.
179 unfi = repo.unfiltered()
180 outgoing = discovery.findcommonoutgoing(unfi, remote,
181 commoninc=commoninc)
182 ui.status(_('looking for local changes to affected paths\n'))
183 localnodes = []
184 for n in itertools.chain(outgoing.missing, outgoing.excluded):
185 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
186 localnodes.append(n)
187 revstostrip = unfi.revs('descendants(%ln)', localnodes)
188 hiddenrevs = repoview.filterrevs(repo, 'visible')
189 visibletostrip = list(repo.changelog.node(r)
190 for r in (revstostrip - hiddenrevs))
191 if visibletostrip:
192 ui.status(_('The following changeset(s) or their ancestors have '
193 'local changes not on the remote:\n'))
194 maxnodes = 10
195 if ui.verbose or len(visibletostrip) <= maxnodes:
196 for n in visibletostrip:
197 ui.status('%s\n' % node.short(n))
198 else:
199 for n in visibletostrip[:maxnodes]:
200 ui.status('%s\n' % node.short(n))
201 ui.status(_('...and %d more, use --verbose to list all\n') %
202 (len(visibletostrip) - maxnodes))
203 if not force:
204 raise error.Abort(_('local changes found'),
205 hint=_('use --force-delete-local-changes to '
206 'ignore'))
207
208 if revstostrip:
209 tostrip = [unfi.changelog.node(r) for r in revstostrip]
210 if repo['.'].node() in tostrip:
211 # stripping working copy, so move to a different commit first
212 urev = max(repo.revs('(::%n) - %ln + null',
213 repo['.'].node(), visibletostrip))
214 hg.clean(repo, urev)
215 repair.strip(ui, unfi, tostrip, topic='narrow')
216
217 todelete = []
218 for f, f2, size in repo.store.datafiles():
219 if f.startswith('data/'):
220 file = f[5:-2]
221 if not newmatch(file):
222 todelete.append(f)
223 elif f.startswith('meta/'):
224 dir = f[5:-13]
225 dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
226 include = True
227 for d in dirs:
228 visit = newmatch.visitdir(d)
229 if not visit:
230 include = False
231 break
232 if visit == 'all':
233 break
234 if not include:
235 todelete.append(f)
236
237 repo.destroying()
238
239 with repo.transaction("narrowing"):
240 for f in todelete:
241 ui.status(_('deleting %s\n') % f)
242 util.unlinkpath(repo.svfs.join(f))
243 repo.store.markremoved(f)
244
245 for f in repo.dirstate:
246 if not newmatch(f):
247 repo.dirstate.drop(f)
248 repo.wvfs.unlinkpath(f)
249 repo.setnarrowpats(newincludes, newexcludes)
250
251 repo.destroyed()
252
253 def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
254 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
255
256 # TODO(martinvonz): Get expansion working with widening/narrowing.
257 if narrowspec.needsexpansion(newincludes):
258 raise error.Abort('Expansion not yet supported on pull')
259
260 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
261 orig(pullop, kwargs)
262 # The old{in,ex}cludepats have already been set by orig()
263 kwargs['includepats'] = newincludes
264 kwargs['excludepats'] = newexcludes
265 wrappedextraprepare = extensions.wrappedfunction(exchange,
266 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
267
268 # define a function that narrowbundle2 can call after creating the
269 # backup bundle, but before applying the bundle from the server
270 def setnewnarrowpats():
271 repo.setnarrowpats(newincludes, newexcludes)
272 repo.setnewnarrowpats = setnewnarrowpats
273
274 ds = repo.dirstate
275 p1, p2 = ds.p1(), ds.p2()
276 with ds.parentchange():
277 ds.setparents(node.nullid, node.nullid)
278 common = commoninc[0]
279 with wrappedextraprepare:
280 exchange.pull(repo, remote, heads=common)
281 with ds.parentchange():
282 ds.setparents(p1, p2)
283
284 actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
285 addgaction = actions['g'].append
286
287 mf = repo['.'].manifest().matches(newmatch)
288 for f, fn in mf.iteritems():
289 if f not in repo.dirstate:
290 addgaction((f, (mf.flags(f), False),
291 "add from widened narrow clone"))
292
293 merge.applyupdates(repo, actions, wctx=repo[None],
294 mctx=repo['.'], overwrite=False)
295 merge.recordupdates(repo, actions, branchmerge=False)
296
297 # TODO(rdamazio): Make new matcher format and update description
298 @command('tracked',
299 [('', 'addinclude', [], _('new paths to include')),
300 ('', 'removeinclude', [], _('old paths to no longer include')),
301 ('', 'addexclude', [], _('new paths to exclude')),
302 ('', 'removeexclude', [], _('old paths to no longer exclude')),
303 ('', 'clear', False, _('whether to replace the existing narrowspec')),
304 ('', 'force-delete-local-changes', False,
305 _('forces deletion of local changes when narrowing')),
306 ] + commands.remoteopts,
307 _('[OPTIONS]... [REMOTE]'),
308 inferrepo=True)
309 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
310 """show or change the current narrowspec
311
312 With no argument, shows the current narrowspec entries, one per line. Each
313 line will be prefixed with 'I' or 'X' for included or excluded patterns,
314 respectively.
315
316 The narrowspec is comprised of expressions to match remote files and/or
317 directories that should be pulled into your client.
318 The narrowspec has *include* and *exclude* expressions, with excludes always
319 trumping includes: that is, if a file matches an exclude expression, it will
320 be excluded even if it also matches an include expression.
321 Excluding files that were never included has no effect.
322
323 Each included or excluded entry is in the format described by
324 'hg help patterns'.
325
326 The options allow you to add or remove included and excluded expressions.
327
328 If --clear is specified, then all previous includes and excludes are DROPPED
329 and replaced by the new ones specified to --addinclude and --addexclude.
330 If --clear is specified without any further options, the narrowspec will be
331 empty and will not match any files.
332 """
333 if narrowrepo.requirement not in repo.requirements:
334 ui.warn(_('The narrow command is only supported on respositories cloned'
335 ' with --narrow.\n'))
336 return 1
337
338 # Before supporting, decide whether it "hg tracked --clear" should mean
339 # tracking no paths or all paths.
340 if opts['clear']:
341 ui.warn(_('The --clear option is not yet supported.\n'))
342 return 1
343
344 if narrowspec.needsexpansion(opts['addinclude'] + opts['addexclude']):
345 raise error.Abort('Expansion not yet supported on widen/narrow')
346
347 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
348 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
349 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
350 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
351 widening = addedincludes or removedexcludes
352 narrowing = removedincludes or addedexcludes
353 only_show = not widening and not narrowing
354
355 # Only print the current narrowspec.
356 if only_show:
357 include, exclude = repo.narrowpats
358
359 ui.pager('tracked')
360 fm = ui.formatter('narrow', opts)
361 for i in sorted(include):
362 fm.startitem()
363 fm.write('status', '%s ', 'I', label='narrow.included')
364 fm.write('pat', '%s\n', i, label='narrow.included')
365 for i in sorted(exclude):
366 fm.startitem()
367 fm.write('status', '%s ', 'X', label='narrow.excluded')
368 fm.write('pat', '%s\n', i, label='narrow.excluded')
369 fm.end()
370 return 0
371
372 with repo.wlock(), repo.lock():
373 cmdutil.bailifchanged(repo)
374
375 # Find the revisions we have in common with the remote. These will
376 # be used for finding local-only changes for narrowing. They will
377 # also define the set of revisions to update for widening.
378 remotepath = ui.expandpath(remotepath or 'default')
379 url, branches = hg.parseurl(remotepath)
380 ui.status(_('comparing with %s\n') % util.hidepassword(url))
381 remote = hg.peer(repo, opts, url)
382 commoninc = discovery.findcommonincoming(repo, remote)
383
384 oldincludes, oldexcludes = repo.narrowpats
385 if narrowing:
386 newincludes = oldincludes - removedincludes
387 newexcludes = oldexcludes | addedexcludes
388 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
389 newincludes, newexcludes,
390 opts['force_delete_local_changes'])
391 # _narrow() updated the narrowspec and _widen() below needs to
392 # use the updated values as its base (otherwise removed includes
393 # and addedexcludes will be lost in the resulting narrowspec)
394 oldincludes = newincludes
395 oldexcludes = newexcludes
396
397 if widening:
398 newincludes = oldincludes | addedincludes
399 newexcludes = oldexcludes - removedexcludes
400 _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
401
402 return 0