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