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