##// END OF EJS Templates
narrow: drop support for remote expansion (BC)...
Gregory Szorc -
r39581:10a8472f default
parent child Browse files
Show More
@@ -1,37 +1,30 b''
1 1 Integration with the share extension needs improvement. Right now
2 2 we've seen some odd bugs, and the way we modify the contents of the
3 3 .hg/shared file is unfortunate. See wrappostshare() and unsharenarrowspec().
4 4
5 5 Resolve commentary on narrowrepo.wraprepo.narrowrepository.status
6 6 about the filtering of status being done at an awkward layer. This
7 7 came up the import to hgext, but nobody's got concrete improvement
8 8 ideas as of then.
9 9
10 10 Fold most (or preferably all) of narrowrevlog.py into core.
11 11
12 12 Address commentary in narrowrevlog.excludedmanifestrevlog.add -
13 13 specifically we should improve the collaboration with core so that
14 14 add() never gets called on an excluded directory and we can improve
15 15 the stand-in to raise a ProgrammingError.
16 16
17 17 Figure out how to correctly produce narrowmanifestrevlog and
18 18 narrowfilelog instances instead of monkeypatching regular revlogs at
19 19 runtime to our subclass. Even better, merge the narrowing logic
20 20 directly into core.
21 21
22 22 Reason more completely about rename-filtering logic in
23 23 narrowfilelog. There could be some surprises lurking there.
24 24
25 25 Formally document the narrowspec format. Unify with sparse, if at all
26 26 possible. For bonus points, unify with the server-specified narrowspec
27 27 format.
28 28
29 29 narrowrepo.setnarrowpats() or narrowspec.save() need to make sure
30 30 they're holding the wlock.
31
32 Implement a simple version of the expandnarrow wireproto command for
33 core. Having configurable shorthands for narrowspecs has been useful
34 at Google (and sparse has a similar feature from Facebook), so it
35 probably makes sense to implement the feature in core. (Google's
36 handler is entirely custom to Google, with a custom format related to
37 bazel's build language, so it's not in the narrowhg distribution.)
@@ -1,463 +1,433 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 def expandpull(pullop, includepats, excludepats):
66 if not narrowspec.needsexpansion(includepats):
67 return includepats, excludepats
68
69 heads = pullop.heads or pullop.rheads
70 includepats, excludepats = pullop.remote.expandnarrow(
71 includepats, excludepats, heads)
72 pullop.repo.ui.debug('Expanded narrowspec to inc=%s, exc=%s\n' % (
73 includepats, 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
83
84 65 def clonenarrowcmd(orig, ui, repo, *args, **opts):
85 66 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
86 67 opts = pycompat.byteskwargs(opts)
87 68 wrappedextraprepare = util.nullcontextmanager()
88 69 opts_narrow = opts['narrow']
89 70 narrowspecfile = opts['narrowspec']
90 71
91 72 if narrowspecfile:
92 73 filepath = os.path.join(pycompat.getcwd(), narrowspecfile)
93 74 ui.status(_("reading narrowspec from '%s'\n") % filepath)
94 75 try:
95 76 fdata = util.readfile(filepath)
96 77 except IOError as inst:
97 78 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
98 79 (filepath, encoding.strtolocal(inst.strerror)))
99 80
100 81 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
101 82 if profiles:
102 83 raise error.Abort(_("cannot specify other files using '%include' in"
103 84 " narrowspec"))
104 85
105 86 narrowspec.validatepatterns(includes)
106 87 narrowspec.validatepatterns(excludes)
107 88
108 89 # narrowspec is passed so we should assume that user wants narrow clone
109 90 opts_narrow = True
110 91 opts['include'].extend(includes)
111 92 opts['exclude'].extend(excludes)
112 93
113 94 if opts_narrow:
114 95 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
115 96 # Create narrow spec patterns from clone flags
116 97 includepats = narrowspec.parsepatterns(opts['include'])
117 98 excludepats = narrowspec.parsepatterns(opts['exclude'])
118 99
119 # If necessary, ask the server to expand the narrowspec.
120 includepats, excludepats = expandpull(
121 pullop, includepats, excludepats)
122
123 100 if not includepats and excludepats:
124 101 # If nothing was included, we assume the user meant to include
125 102 # everything, except what they asked to exclude.
126 103 includepats = {'path:.'}
127 104
128 105 pullop.repo.setnarrowpats(includepats, excludepats)
129 106
130 107 # This will populate 'includepats' etc with the values from the
131 108 # narrowspec we just saved.
132 109 orig(pullop, kwargs)
133 110
134 111 if opts.get('depth'):
135 112 kwargs['depth'] = opts['depth']
136 113 wrappedextraprepare = extensions.wrappedfunction(exchange,
137 114 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
138 115
139 116 def pullnarrow(orig, repo, *args, **kwargs):
140 117 if opts_narrow:
141 118 repo.requirements.add(repository.NARROW_REQUIREMENT)
142 119 repo._writerequirements()
143 120
144 121 return orig(repo, *args, **kwargs)
145 122
146 123 wrappedpull = extensions.wrappedfunction(exchange, 'pull', pullnarrow)
147 124
148 125 with wrappedextraprepare, wrappedpull:
149 126 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
150 127
151 128 def pullnarrowcmd(orig, ui, repo, *args, **opts):
152 129 """Wraps pull command to allow modifying narrow spec."""
153 130 wrappedextraprepare = util.nullcontextmanager()
154 131 if repository.NARROW_REQUIREMENT in repo.requirements:
155 132
156 133 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
157 134 orig(pullop, kwargs)
158 135 if opts.get(r'depth'):
159 136 kwargs['depth'] = opts[r'depth']
160 137 wrappedextraprepare = extensions.wrappedfunction(exchange,
161 138 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
162 139
163 140 with wrappedextraprepare:
164 141 return orig(ui, repo, *args, **opts)
165 142
166 143 def archivenarrowcmd(orig, ui, repo, *args, **opts):
167 144 """Wraps archive command to narrow the default includes."""
168 145 if repository.NARROW_REQUIREMENT in repo.requirements:
169 146 repo_includes, repo_excludes = repo.narrowpats
170 147 includes = set(opts.get(r'include', []))
171 148 excludes = set(opts.get(r'exclude', []))
172 149 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
173 150 includes, excludes, repo_includes, repo_excludes)
174 151 if includes:
175 152 opts[r'include'] = includes
176 153 if excludes:
177 154 opts[r'exclude'] = excludes
178 155 return orig(ui, repo, *args, **opts)
179 156
180 157 def pullbundle2extraprepare(orig, pullop, kwargs):
181 158 repo = pullop.repo
182 159 if repository.NARROW_REQUIREMENT not in repo.requirements:
183 160 return orig(pullop, kwargs)
184 161
185 162 if narrowwirepeer.NARROWCAP not in pullop.remote.capabilities():
186 163 raise error.Abort(_("server doesn't support narrow clones"))
187 164 orig(pullop, kwargs)
188 165 kwargs['narrow'] = True
189 166 include, exclude = repo.narrowpats
190 167 kwargs['oldincludepats'] = include
191 168 kwargs['oldexcludepats'] = exclude
192 169 kwargs['includepats'] = include
193 170 kwargs['excludepats'] = exclude
194 171 # calculate known nodes only in ellipses cases because in non-ellipses cases
195 172 # we have all the nodes
196 173 if narrowwirepeer.ELLIPSESCAP in pullop.remote.capabilities():
197 174 kwargs['known'] = [node.hex(ctx.node()) for ctx in
198 175 repo.set('::%ln', pullop.common)
199 176 if ctx.node() != node.nullid]
200 177 if not kwargs['known']:
201 178 # Mercurial serializes an empty list as '' and deserializes it as
202 179 # [''], so delete it instead to avoid handling the empty string on
203 180 # the server.
204 181 del kwargs['known']
205 182
206 183 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
207 184 pullbundle2extraprepare)
208 185
209 186 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
210 187 newincludes, newexcludes, force):
211 188 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
212 189 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
213 190
214 191 # This is essentially doing "hg outgoing" to find all local-only
215 192 # commits. We will then check that the local-only commits don't
216 193 # have any changes to files that will be untracked.
217 194 unfi = repo.unfiltered()
218 195 outgoing = discovery.findcommonoutgoing(unfi, remote,
219 196 commoninc=commoninc)
220 197 ui.status(_('looking for local changes to affected paths\n'))
221 198 localnodes = []
222 199 for n in itertools.chain(outgoing.missing, outgoing.excluded):
223 200 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
224 201 localnodes.append(n)
225 202 revstostrip = unfi.revs('descendants(%ln)', localnodes)
226 203 hiddenrevs = repoview.filterrevs(repo, 'visible')
227 204 visibletostrip = list(repo.changelog.node(r)
228 205 for r in (revstostrip - hiddenrevs))
229 206 if visibletostrip:
230 207 ui.status(_('The following changeset(s) or their ancestors have '
231 208 'local changes not on the remote:\n'))
232 209 maxnodes = 10
233 210 if ui.verbose or len(visibletostrip) <= maxnodes:
234 211 for n in visibletostrip:
235 212 ui.status('%s\n' % node.short(n))
236 213 else:
237 214 for n in visibletostrip[:maxnodes]:
238 215 ui.status('%s\n' % node.short(n))
239 216 ui.status(_('...and %d more, use --verbose to list all\n') %
240 217 (len(visibletostrip) - maxnodes))
241 218 if not force:
242 219 raise error.Abort(_('local changes found'),
243 220 hint=_('use --force-delete-local-changes to '
244 221 'ignore'))
245 222
246 223 with ui.uninterruptable():
247 224 if revstostrip:
248 225 tostrip = [unfi.changelog.node(r) for r in revstostrip]
249 226 if repo['.'].node() in tostrip:
250 227 # stripping working copy, so move to a different commit first
251 228 urev = max(repo.revs('(::%n) - %ln + null',
252 229 repo['.'].node(), visibletostrip))
253 230 hg.clean(repo, urev)
254 231 repair.strip(ui, unfi, tostrip, topic='narrow')
255 232
256 233 todelete = []
257 234 for f, f2, size in repo.store.datafiles():
258 235 if f.startswith('data/'):
259 236 file = f[5:-2]
260 237 if not newmatch(file):
261 238 todelete.append(f)
262 239 elif f.startswith('meta/'):
263 240 dir = f[5:-13]
264 241 dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
265 242 include = True
266 243 for d in dirs:
267 244 visit = newmatch.visitdir(d)
268 245 if not visit:
269 246 include = False
270 247 break
271 248 if visit == 'all':
272 249 break
273 250 if not include:
274 251 todelete.append(f)
275 252
276 253 repo.destroying()
277 254
278 255 with repo.transaction("narrowing"):
279 256 for f in todelete:
280 257 ui.status(_('deleting %s\n') % f)
281 258 util.unlinkpath(repo.svfs.join(f))
282 259 repo.store.markremoved(f)
283 260
284 261 for f in repo.dirstate:
285 262 if not newmatch(f):
286 263 repo.dirstate.drop(f)
287 264 repo.wvfs.unlinkpath(f)
288 265 repo.setnarrowpats(newincludes, newexcludes)
289 266
290 267 repo.destroyed()
291 268
292 269 def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
293 270 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
294 271
295 # TODO(martinvonz): Get expansion working with widening/narrowing.
296 if narrowspec.needsexpansion(newincludes):
297 raise error.Abort('Expansion not yet supported on pull')
298
299 272 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
300 273 orig(pullop, kwargs)
301 274 # The old{in,ex}cludepats have already been set by orig()
302 275 kwargs['includepats'] = newincludes
303 276 kwargs['excludepats'] = newexcludes
304 277 kwargs['widen'] = True
305 278 wrappedextraprepare = extensions.wrappedfunction(exchange,
306 279 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
307 280
308 281 # define a function that narrowbundle2 can call after creating the
309 282 # backup bundle, but before applying the bundle from the server
310 283 def setnewnarrowpats():
311 284 repo.setnarrowpats(newincludes, newexcludes)
312 285 repo.setnewnarrowpats = setnewnarrowpats
313 286
314 287 with ui.uninterruptable():
315 288 ds = repo.dirstate
316 289 p1, p2 = ds.p1(), ds.p2()
317 290 with ds.parentchange():
318 291 ds.setparents(node.nullid, node.nullid)
319 292 common = commoninc[0]
320 293 with wrappedextraprepare:
321 294 exchange.pull(repo, remote, heads=common)
322 295 with ds.parentchange():
323 296 ds.setparents(p1, p2)
324 297
325 298 repo.setnewnarrowpats()
326 299 actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
327 300 addgaction = actions['g'].append
328 301
329 302 mf = repo['.'].manifest().matches(newmatch)
330 303 for f, fn in mf.iteritems():
331 304 if f not in repo.dirstate:
332 305 addgaction((f, (mf.flags(f), False),
333 306 "add from widened narrow clone"))
334 307
335 308 merge.applyupdates(repo, actions, wctx=repo[None],
336 309 mctx=repo['.'], overwrite=False)
337 310 merge.recordupdates(repo, actions, branchmerge=False)
338 311
339 312 # TODO(rdamazio): Make new matcher format and update description
340 313 @command('tracked',
341 314 [('', 'addinclude', [], _('new paths to include')),
342 315 ('', 'removeinclude', [], _('old paths to no longer include')),
343 316 ('', 'addexclude', [], _('new paths to exclude')),
344 317 ('', 'import-rules', '', _('import narrowspecs from a file')),
345 318 ('', 'removeexclude', [], _('old paths to no longer exclude')),
346 319 ('', 'clear', False, _('whether to replace the existing narrowspec')),
347 320 ('', 'force-delete-local-changes', False,
348 321 _('forces deletion of local changes when narrowing')),
349 322 ] + commands.remoteopts,
350 323 _('[OPTIONS]... [REMOTE]'),
351 324 inferrepo=True)
352 325 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
353 326 """show or change the current narrowspec
354 327
355 328 With no argument, shows the current narrowspec entries, one per line. Each
356 329 line will be prefixed with 'I' or 'X' for included or excluded patterns,
357 330 respectively.
358 331
359 332 The narrowspec is comprised of expressions to match remote files and/or
360 333 directories that should be pulled into your client.
361 334 The narrowspec has *include* and *exclude* expressions, with excludes always
362 335 trumping includes: that is, if a file matches an exclude expression, it will
363 336 be excluded even if it also matches an include expression.
364 337 Excluding files that were never included has no effect.
365 338
366 339 Each included or excluded entry is in the format described by
367 340 'hg help patterns'.
368 341
369 342 The options allow you to add or remove included and excluded expressions.
370 343
371 344 If --clear is specified, then all previous includes and excludes are DROPPED
372 345 and replaced by the new ones specified to --addinclude and --addexclude.
373 346 If --clear is specified without any further options, the narrowspec will be
374 347 empty and will not match any files.
375 348 """
376 349 opts = pycompat.byteskwargs(opts)
377 350 if repository.NARROW_REQUIREMENT not in repo.requirements:
378 351 ui.warn(_('The narrow command is only supported on respositories cloned'
379 352 ' with --narrow.\n'))
380 353 return 1
381 354
382 355 # Before supporting, decide whether it "hg tracked --clear" should mean
383 356 # tracking no paths or all paths.
384 357 if opts['clear']:
385 358 ui.warn(_('The --clear option is not yet supported.\n'))
386 359 return 1
387 360
388 361 # import rules from a file
389 362 newrules = opts.get('import_rules')
390 363 if newrules:
391 364 try:
392 365 filepath = os.path.join(pycompat.getcwd(), newrules)
393 366 fdata = util.readfile(filepath)
394 367 except IOError as inst:
395 368 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
396 369 (filepath, encoding.strtolocal(inst.strerror)))
397 370 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
398 371 'narrow')
399 372 if profiles:
400 373 raise error.Abort(_("including other spec files using '%include' "
401 374 "is not supported in narrowspec"))
402 375 opts['addinclude'].extend(includepats)
403 376 opts['addexclude'].extend(excludepats)
404 377
405 if narrowspec.needsexpansion(opts['addinclude'] + opts['addexclude']):
406 raise error.Abort('Expansion not yet supported on widen/narrow')
407
408 378 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
409 379 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
410 380 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
411 381 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
412 382 widening = addedincludes or removedexcludes
413 383 narrowing = removedincludes or addedexcludes
414 384 only_show = not widening and not narrowing
415 385
416 386 # Only print the current narrowspec.
417 387 if only_show:
418 388 include, exclude = repo.narrowpats
419 389
420 390 ui.pager('tracked')
421 391 fm = ui.formatter('narrow', opts)
422 392 for i in sorted(include):
423 393 fm.startitem()
424 394 fm.write('status', '%s ', 'I', label='narrow.included')
425 395 fm.write('pat', '%s\n', i, label='narrow.included')
426 396 for i in sorted(exclude):
427 397 fm.startitem()
428 398 fm.write('status', '%s ', 'X', label='narrow.excluded')
429 399 fm.write('pat', '%s\n', i, label='narrow.excluded')
430 400 fm.end()
431 401 return 0
432 402
433 403 with repo.wlock(), repo.lock():
434 404 cmdutil.bailifchanged(repo)
435 405
436 406 # Find the revisions we have in common with the remote. These will
437 407 # be used for finding local-only changes for narrowing. They will
438 408 # also define the set of revisions to update for widening.
439 409 remotepath = ui.expandpath(remotepath or 'default')
440 410 url, branches = hg.parseurl(remotepath)
441 411 ui.status(_('comparing with %s\n') % util.hidepassword(url))
442 412 remote = hg.peer(repo, opts, url)
443 413 commoninc = discovery.findcommonincoming(repo, remote)
444 414
445 415 oldincludes, oldexcludes = repo.narrowpats
446 416 if narrowing:
447 417 newincludes = oldincludes - removedincludes
448 418 newexcludes = oldexcludes | addedexcludes
449 419 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
450 420 newincludes, newexcludes,
451 421 opts['force_delete_local_changes'])
452 422 # _narrow() updated the narrowspec and _widen() below needs to
453 423 # use the updated values as its base (otherwise removed includes
454 424 # and addedexcludes will be lost in the resulting narrowspec)
455 425 oldincludes = newincludes
456 426 oldexcludes = newexcludes
457 427
458 428 if widening:
459 429 newincludes = oldincludes | addedincludes
460 430 newexcludes = oldexcludes - removedexcludes
461 431 _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
462 432
463 433 return 0
@@ -1,66 +1,41 b''
1 1 # narrowwirepeer.py - passes narrow spec with unbundle command
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
8 8 from __future__ import absolute_import
9 9
10 from mercurial.i18n import _
11 10 from mercurial import (
12 error,
13 11 extensions,
14 12 hg,
15 narrowspec,
16 node,
17 13 wireprotov1server,
18 14 )
19 15
20 16 NARROWCAP = 'exp-narrow-1'
21 17 ELLIPSESCAP = 'exp-ellipses-1'
22 18
23 19 def uisetup():
24 def peersetup(ui, peer):
25 # We must set up the expansion before reposetup below, since it's used
26 # at clone time before we have a repo.
27 class expandingpeer(peer.__class__):
28 def expandnarrow(self, narrow_include, narrow_exclude, nodes):
29 ui.status(_("expanding narrowspec\n"))
30 if not self.capable('exp-expandnarrow'):
31 raise error.Abort(
32 'peer does not support expanding narrowspecs')
33
34 hex_nodes = (node.hex(n) for n in nodes)
35 new_narrowspec = self._call(
36 'expandnarrow',
37 includepats=','.join(narrow_include),
38 excludepats=','.join(narrow_exclude),
39 nodes=','.join(hex_nodes))
40
41 return narrowspec.parseserverpatterns(new_narrowspec)
42 peer.__class__ = expandingpeer
43 hg.wirepeersetupfuncs.append(peersetup)
44
45 20 extensions.wrapfunction(wireprotov1server, '_capabilities', addnarrowcap)
46 21
47 22 def addnarrowcap(orig, repo, proto):
48 23 """add the narrow capability to the server"""
49 24 caps = orig(repo, proto)
50 25 caps.append(NARROWCAP)
51 26 if repo.ui.configbool('experimental', 'narrowservebrokenellipses'):
52 27 caps.append(ELLIPSESCAP)
53 28 return caps
54 29
55 30 def reposetup(repo):
56 31 def wirereposetup(ui, peer):
57 32 def wrapped(orig, cmd, *args, **kwargs):
58 33 if cmd == 'unbundle':
59 34 # TODO: don't blindly add include/exclude wireproto
60 35 # arguments to unbundle.
61 36 include, exclude = repo.narrowpats
62 37 kwargs[r"includepats"] = ','.join(include)
63 38 kwargs[r"excludepats"] = ','.join(exclude)
64 39 return orig(cmd, *args, **kwargs)
65 40 extensions.wrapfunction(peer, '_calltwowaystream', wrapped)
66 41 hg.wirepeersetupfuncs.append(wirereposetup)
@@ -1,247 +1,244 b''
1 1 # narrowspec.py - methods for working with a narrow view of a repository
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
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 match as matchmod,
16 16 repository,
17 17 sparse,
18 18 util,
19 19 )
20 20
21 21 FILENAME = 'narrowspec'
22 22
23 23 # Pattern prefixes that are allowed in narrow patterns. This list MUST
24 24 # only contain patterns that are fast and safe to evaluate. Keep in mind
25 25 # that patterns are supplied by clients and executed on remote servers
26 26 # as part of wire protocol commands.
27 27 VALID_PREFIXES = (
28 28 b'path:',
29 29 b'rootfilesin:',
30 30 )
31 31
32 32 def parseserverpatterns(text):
33 33 """Parses the narrowspec format that's returned by the server."""
34 34 includepats = set()
35 35 excludepats = set()
36 36
37 37 # We get one entry per line, in the format "<key> <value>".
38 38 # It's OK for value to contain other spaces.
39 39 for kp in (l.split(' ', 1) for l in text.splitlines()):
40 40 if len(kp) != 2:
41 41 raise error.Abort(_('Invalid narrowspec pattern line: "%s"') % kp)
42 42 key = kp[0]
43 43 pat = kp[1]
44 44 if key == 'include':
45 45 includepats.add(pat)
46 46 elif key == 'exclude':
47 47 excludepats.add(pat)
48 48 else:
49 49 raise error.Abort(_('Invalid key "%s" in server response') % key)
50 50
51 51 return includepats, excludepats
52 52
53 53 def normalizesplitpattern(kind, pat):
54 54 """Returns the normalized version of a pattern and kind.
55 55
56 56 Returns a tuple with the normalized kind and normalized pattern.
57 57 """
58 58 pat = pat.rstrip('/')
59 59 _validatepattern(pat)
60 60 return kind, pat
61 61
62 62 def _numlines(s):
63 63 """Returns the number of lines in s, including ending empty lines."""
64 64 # We use splitlines because it is Unicode-friendly and thus Python 3
65 65 # compatible. However, it does not count empty lines at the end, so trick
66 66 # it by adding a character at the end.
67 67 return len((s + 'x').splitlines())
68 68
69 69 def _validatepattern(pat):
70 70 """Validates the pattern and aborts if it is invalid.
71 71
72 72 Patterns are stored in the narrowspec as newline-separated
73 73 POSIX-style bytestring paths. There's no escaping.
74 74 """
75 75
76 76 # We use newlines as separators in the narrowspec file, so don't allow them
77 77 # in patterns.
78 78 if _numlines(pat) > 1:
79 79 raise error.Abort(_('newlines are not allowed in narrowspec paths'))
80 80
81 81 components = pat.split('/')
82 82 if '.' in components or '..' in components:
83 83 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
84 84
85 85 def normalizepattern(pattern, defaultkind='path'):
86 86 """Returns the normalized version of a text-format pattern.
87 87
88 88 If the pattern has no kind, the default will be added.
89 89 """
90 90 kind, pat = matchmod._patsplit(pattern, defaultkind)
91 91 return '%s:%s' % normalizesplitpattern(kind, pat)
92 92
93 93 def parsepatterns(pats):
94 94 """Parses an iterable of patterns into a typed pattern set.
95 95
96 96 Patterns are assumed to be ``path:`` if no prefix is present.
97 97 For safety and performance reasons, only some prefixes are allowed.
98 98 See ``validatepatterns()``.
99 99
100 100 This function should be used on patterns that come from the user to
101 101 normalize and validate them to the internal data structure used for
102 102 representing patterns.
103 103 """
104 104 res = {normalizepattern(orig) for orig in pats}
105 105 validatepatterns(res)
106 106 return res
107 107
108 108 def validatepatterns(pats):
109 109 """Validate that patterns are in the expected data structure and format.
110 110
111 111 And that is a set of normalized patterns beginning with ``path:`` or
112 112 ``rootfilesin:``.
113 113
114 114 This function should be used to validate internal data structures
115 115 and patterns that are loaded from sources that use the internal,
116 116 prefixed pattern representation (but can't necessarily be fully trusted).
117 117 """
118 118 if not isinstance(pats, set):
119 119 raise error.ProgrammingError('narrow patterns should be a set; '
120 120 'got %r' % pats)
121 121
122 122 for pat in pats:
123 123 if not pat.startswith(VALID_PREFIXES):
124 124 # Use a Mercurial exception because this can happen due to user
125 125 # bugs (e.g. manually updating spec file).
126 126 raise error.Abort(_('invalid prefix on narrow pattern: %s') % pat,
127 127 hint=_('narrow patterns must begin with one of '
128 128 'the following: %s') %
129 129 ', '.join(VALID_PREFIXES))
130 130
131 131 def format(includes, excludes):
132 132 output = '[include]\n'
133 133 for i in sorted(includes - excludes):
134 134 output += i + '\n'
135 135 output += '[exclude]\n'
136 136 for e in sorted(excludes):
137 137 output += e + '\n'
138 138 return output
139 139
140 140 def match(root, include=None, exclude=None):
141 141 if not include:
142 142 # Passing empty include and empty exclude to matchmod.match()
143 143 # gives a matcher that matches everything, so explicitly use
144 144 # the nevermatcher.
145 145 return matchmod.never(root, '')
146 146 return matchmod.match(root, '', [], include=include or [],
147 147 exclude=exclude or [])
148 148
149 def needsexpansion(includes):
150 return [i for i in includes if i.startswith('include:')]
151
152 149 def load(repo):
153 150 try:
154 151 spec = repo.svfs.read(FILENAME)
155 152 except IOError as e:
156 153 # Treat "narrowspec does not exist" the same as "narrowspec file exists
157 154 # and is empty".
158 155 if e.errno == errno.ENOENT:
159 156 return set(), set()
160 157 raise
161 158 # maybe we should care about the profiles returned too
162 159 includepats, excludepats, profiles = sparse.parseconfig(repo.ui, spec,
163 160 'narrow')
164 161 if profiles:
165 162 raise error.Abort(_("including other spec files using '%include' is not"
166 163 " supported in narrowspec"))
167 164
168 165 validatepatterns(includepats)
169 166 validatepatterns(excludepats)
170 167
171 168 return includepats, excludepats
172 169
173 170 def save(repo, includepats, excludepats):
174 171 validatepatterns(includepats)
175 172 validatepatterns(excludepats)
176 173 spec = format(includepats, excludepats)
177 174 repo.svfs.write(FILENAME, spec)
178 175
179 176 def savebackup(repo, backupname):
180 177 if repository.NARROW_REQUIREMENT not in repo.requirements:
181 178 return
182 179 vfs = repo.vfs
183 180 vfs.tryunlink(backupname)
184 181 util.copyfile(repo.svfs.join(FILENAME), vfs.join(backupname), hardlink=True)
185 182
186 183 def restorebackup(repo, backupname):
187 184 if repository.NARROW_REQUIREMENT not in repo.requirements:
188 185 return
189 186 util.rename(repo.vfs.join(backupname), repo.svfs.join(FILENAME))
190 187
191 188 def clearbackup(repo, backupname):
192 189 if repository.NARROW_REQUIREMENT not in repo.requirements:
193 190 return
194 191 repo.vfs.unlink(backupname)
195 192
196 193 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
197 194 r""" Restricts the patterns according to repo settings,
198 195 results in a logical AND operation
199 196
200 197 :param req_includes: requested includes
201 198 :param req_excludes: requested excludes
202 199 :param repo_includes: repo includes
203 200 :param repo_excludes: repo excludes
204 201 :return: include patterns, exclude patterns, and invalid include patterns.
205 202
206 203 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
207 204 (set(['f1']), {}, [])
208 205 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
209 206 (set(['f1']), {}, [])
210 207 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
211 208 (set(['f1/fc1']), {}, [])
212 209 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
213 210 ([], set(['path:.']), [])
214 211 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
215 212 (set(['f2/fc2']), {}, [])
216 213 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
217 214 ([], set(['path:.']), [])
218 215 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
219 216 (set(['f1/$non_exitent_var']), {}, [])
220 217 """
221 218 res_excludes = set(req_excludes)
222 219 res_excludes.update(repo_excludes)
223 220 invalid_includes = []
224 221 if not req_includes:
225 222 res_includes = set(repo_includes)
226 223 elif 'path:.' not in repo_includes:
227 224 res_includes = []
228 225 for req_include in req_includes:
229 226 req_include = util.expandpath(util.normpath(req_include))
230 227 if req_include in repo_includes:
231 228 res_includes.append(req_include)
232 229 continue
233 230 valid = False
234 231 for repo_include in repo_includes:
235 232 if req_include.startswith(repo_include + '/'):
236 233 valid = True
237 234 res_includes.append(req_include)
238 235 break
239 236 if not valid:
240 237 invalid_includes.append(req_include)
241 238 if len(res_includes) == 0:
242 239 res_excludes = {'path:.'}
243 240 else:
244 241 res_includes = set(res_includes)
245 242 else:
246 243 res_includes = set(req_includes)
247 244 return res_includes, res_excludes, invalid_includes
General Comments 0
You need to be logged in to leave comments. Login now