Mercurial > hg
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 |