##// END OF EJS Templates
narrowspec: move module into core...
Gregory Szorc -
r36178:9fd8c2a3 default
parent child Browse files
Show More
@@ -1,494 +1,494 b''
1 1 # narrowbundle2.py - bundle2 extensions for narrow repository support
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 collections
11 11 import errno
12 12 import struct
13 13
14 14 from mercurial.i18n import _
15 15 from mercurial.node import (
16 16 bin,
17 17 nullid,
18 18 nullrev,
19 19 )
20 20 from mercurial import (
21 21 bundle2,
22 22 changegroup,
23 23 dagutil,
24 24 error,
25 25 exchange,
26 26 extensions,
27 narrowspec,
27 28 repair,
28 29 util,
29 30 wireproto,
30 31 )
31 32
32 33 from . import (
33 34 narrowrepo,
34 narrowspec,
35 35 )
36 36
37 37 NARROWCAP = 'narrow'
38 38 _NARROWACL_SECTION = 'narrowhgacl'
39 39 _CHANGESPECPART = NARROWCAP + ':changespec'
40 40 _SPECPART = NARROWCAP + ':spec'
41 41 _SPECPART_INCLUDE = 'include'
42 42 _SPECPART_EXCLUDE = 'exclude'
43 43 _KILLNODESIGNAL = 'KILL'
44 44 _DONESIGNAL = 'DONE'
45 45 _ELIDEDCSHEADER = '>20s20s20sl' # cset id, p1, p2, len(text)
46 46 _ELIDEDMFHEADER = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
47 47 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
48 48 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
49 49
50 50 # When advertising capabilities, always include narrow clone support.
51 51 def getrepocaps_narrow(orig, repo, **kwargs):
52 52 caps = orig(repo, **kwargs)
53 53 caps[NARROWCAP] = ['v0']
54 54 return caps
55 55
56 56 def _computeellipsis(repo, common, heads, known, match, depth=None):
57 57 """Compute the shape of a narrowed DAG.
58 58
59 59 Args:
60 60 repo: The repository we're transferring.
61 61 common: The roots of the DAG range we're transferring.
62 62 May be just [nullid], which means all ancestors of heads.
63 63 heads: The heads of the DAG range we're transferring.
64 64 match: The narrowmatcher that allows us to identify relevant changes.
65 65 depth: If not None, only consider nodes to be full nodes if they are at
66 66 most depth changesets away from one of heads.
67 67
68 68 Returns:
69 69 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
70 70
71 71 visitnodes: The list of nodes (either full or ellipsis) which
72 72 need to be sent to the client.
73 73 relevant_nodes: The set of changelog nodes which change a file inside
74 74 the narrowspec. The client needs these as non-ellipsis nodes.
75 75 ellipsisroots: A dict of {rev: parents} that is used in
76 76 narrowchangegroup to produce ellipsis nodes with the
77 77 correct parents.
78 78 """
79 79 cl = repo.changelog
80 80 mfl = repo.manifestlog
81 81
82 82 cldag = dagutil.revlogdag(cl)
83 83 # dagutil does not like nullid/nullrev
84 84 commonrevs = cldag.internalizeall(common - set([nullid])) | set([nullrev])
85 85 headsrevs = cldag.internalizeall(heads)
86 86 if depth:
87 87 revdepth = {h: 0 for h in headsrevs}
88 88
89 89 ellipsisheads = collections.defaultdict(set)
90 90 ellipsisroots = collections.defaultdict(set)
91 91
92 92 def addroot(head, curchange):
93 93 """Add a root to an ellipsis head, splitting heads with 3 roots."""
94 94 ellipsisroots[head].add(curchange)
95 95 # Recursively split ellipsis heads with 3 roots by finding the
96 96 # roots' youngest common descendant which is an elided merge commit.
97 97 # That descendant takes 2 of the 3 roots as its own, and becomes a
98 98 # root of the head.
99 99 while len(ellipsisroots[head]) > 2:
100 100 child, roots = splithead(head)
101 101 splitroots(head, child, roots)
102 102 head = child # Recurse in case we just added a 3rd root
103 103
104 104 def splitroots(head, child, roots):
105 105 ellipsisroots[head].difference_update(roots)
106 106 ellipsisroots[head].add(child)
107 107 ellipsisroots[child].update(roots)
108 108 ellipsisroots[child].discard(child)
109 109
110 110 def splithead(head):
111 111 r1, r2, r3 = sorted(ellipsisroots[head])
112 112 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
113 113 mid = repo.revs('sort(merge() & %d::%d & %d::%d, -rev)',
114 114 nr1, head, nr2, head)
115 115 for j in mid:
116 116 if j == nr2:
117 117 return nr2, (nr1, nr2)
118 118 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
119 119 return j, (nr1, nr2)
120 120 raise error.Abort('Failed to split up ellipsis node! head: %d, '
121 121 'roots: %d %d %d' % (head, r1, r2, r3))
122 122
123 123 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
124 124 visit = reversed(missing)
125 125 relevant_nodes = set()
126 126 visitnodes = map(cl.node, missing)
127 127 required = set(headsrevs) | known
128 128 for rev in visit:
129 129 clrev = cl.changelogrevision(rev)
130 130 ps = cldag.parents(rev)
131 131 if depth is not None:
132 132 curdepth = revdepth[rev]
133 133 for p in ps:
134 134 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
135 135 needed = False
136 136 shallow_enough = depth is None or revdepth[rev] <= depth
137 137 if shallow_enough:
138 138 curmf = mfl[clrev.manifest].read()
139 139 if ps:
140 140 # We choose to not trust the changed files list in
141 141 # changesets because it's not always correct. TODO: could
142 142 # we trust it for the non-merge case?
143 143 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
144 144 needed = any(match(f) for f in curmf.diff(p1mf).iterkeys())
145 145 if not needed and len(ps) > 1:
146 146 # For merge changes, the list of changed files is not
147 147 # helpful, since we need to emit the merge if a file
148 148 # in the narrow spec has changed on either side of the
149 149 # merge. As a result, we do a manifest diff to check.
150 150 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
151 151 needed = any(match(f) for f in curmf.diff(p2mf).iterkeys())
152 152 else:
153 153 # For a root node, we need to include the node if any
154 154 # files in the node match the narrowspec.
155 155 needed = any(match(f) for f in curmf)
156 156
157 157 if needed:
158 158 for head in ellipsisheads[rev]:
159 159 addroot(head, rev)
160 160 for p in ps:
161 161 required.add(p)
162 162 relevant_nodes.add(cl.node(rev))
163 163 else:
164 164 if not ps:
165 165 ps = [nullrev]
166 166 if rev in required:
167 167 for head in ellipsisheads[rev]:
168 168 addroot(head, rev)
169 169 for p in ps:
170 170 ellipsisheads[p].add(rev)
171 171 else:
172 172 for p in ps:
173 173 ellipsisheads[p] |= ellipsisheads[rev]
174 174
175 175 # add common changesets as roots of their reachable ellipsis heads
176 176 for c in commonrevs:
177 177 for head in ellipsisheads[c]:
178 178 addroot(head, c)
179 179 return visitnodes, relevant_nodes, ellipsisroots
180 180
181 181 def _packellipsischangegroup(repo, common, match, relevant_nodes,
182 182 ellipsisroots, visitnodes, depth, source, version):
183 183 if version in ('01', '02'):
184 184 raise error.Abort(
185 185 'ellipsis nodes require at least cg3 on client and server, '
186 186 'but negotiated version %s' % version)
187 187 # We wrap cg1packer.revchunk, using a side channel to pass
188 188 # relevant_nodes into that area. Then if linknode isn't in the
189 189 # set, we know we have an ellipsis node and we should defer
190 190 # sending that node's data. We override close() to detect
191 191 # pending ellipsis nodes and flush them.
192 192 packer = changegroup.getbundler(version, repo)
193 193 # Let the packer have access to the narrow matcher so it can
194 194 # omit filelogs and dirlogs as needed
195 195 packer._narrow_matcher = lambda : match
196 196 # Give the packer the list of nodes which should not be
197 197 # ellipsis nodes. We store this rather than the set of nodes
198 198 # that should be an ellipsis because for very large histories
199 199 # we expect this to be significantly smaller.
200 200 packer.full_nodes = relevant_nodes
201 201 # Maps ellipsis revs to their roots at the changelog level.
202 202 packer.precomputed_ellipsis = ellipsisroots
203 203 # Maps CL revs to per-revlog revisions. Cleared in close() at
204 204 # the end of each group.
205 205 packer.clrev_to_localrev = {}
206 206 packer.next_clrev_to_localrev = {}
207 207 # Maps changelog nodes to changelog revs. Filled in once
208 208 # during changelog stage and then left unmodified.
209 209 packer.clnode_to_rev = {}
210 210 packer.changelog_done = False
211 211 # If true, informs the packer that it is serving shallow content and might
212 212 # need to pack file contents not introduced by the changes being packed.
213 213 packer.is_shallow = depth is not None
214 214
215 215 return packer.generate(common, visitnodes, False, source)
216 216
217 217 # Serve a changegroup for a client with a narrow clone.
218 218 def getbundlechangegrouppart_narrow(bundler, repo, source,
219 219 bundlecaps=None, b2caps=None, heads=None,
220 220 common=None, **kwargs):
221 221 cgversions = b2caps.get('changegroup')
222 222 getcgkwargs = {}
223 223 if cgversions: # 3.1 and 3.2 ship with an empty value
224 224 cgversions = [v for v in cgversions
225 225 if v in changegroup.supportedoutgoingversions(repo)]
226 226 if not cgversions:
227 227 raise ValueError(_('no common changegroup version'))
228 228 version = getcgkwargs['version'] = max(cgversions)
229 229 else:
230 230 raise ValueError(_("server does not advertise changegroup version,"
231 231 " can't negotiate support for ellipsis nodes"))
232 232
233 233 include = sorted(filter(bool, kwargs.get('includepats', [])))
234 234 exclude = sorted(filter(bool, kwargs.get('excludepats', [])))
235 235 newmatch = narrowspec.match(repo.root, include=include, exclude=exclude)
236 236 if not repo.ui.configbool("experimental", "narrowservebrokenellipses"):
237 237 outgoing = exchange._computeoutgoing(repo, heads, common)
238 238 if not outgoing.missing:
239 239 return
240 240 def wrappedgetbundler(orig, *args, **kwargs):
241 241 bundler = orig(*args, **kwargs)
242 242 bundler._narrow_matcher = lambda : newmatch
243 243 return bundler
244 244 with extensions.wrappedfunction(changegroup, 'getbundler',
245 245 wrappedgetbundler):
246 246 cg = changegroup.makestream(repo, outgoing, version, source)
247 247 part = bundler.newpart('changegroup', data=cg)
248 248 part.addparam('version', version)
249 249 if 'treemanifest' in repo.requirements:
250 250 part.addparam('treemanifest', '1')
251 251
252 252 if include or exclude:
253 253 narrowspecpart = bundler.newpart(_SPECPART)
254 254 if include:
255 255 narrowspecpart.addparam(
256 256 _SPECPART_INCLUDE, '\n'.join(include), mandatory=True)
257 257 if exclude:
258 258 narrowspecpart.addparam(
259 259 _SPECPART_EXCLUDE, '\n'.join(exclude), mandatory=True)
260 260
261 261 return
262 262
263 263 depth = kwargs.get('depth', None)
264 264 if depth is not None:
265 265 depth = int(depth)
266 266 if depth < 1:
267 267 raise error.Abort(_('depth must be positive, got %d') % depth)
268 268
269 269 heads = set(heads or repo.heads())
270 270 common = set(common or [nullid])
271 271 oldinclude = sorted(filter(bool, kwargs.get('oldincludepats', [])))
272 272 oldexclude = sorted(filter(bool, kwargs.get('oldexcludepats', [])))
273 273 known = {bin(n) for n in kwargs.get('known', [])}
274 274 if known and (oldinclude != include or oldexclude != exclude):
275 275 # Steps:
276 276 # 1. Send kill for "$known & ::common"
277 277 #
278 278 # 2. Send changegroup for ::common
279 279 #
280 280 # 3. Proceed.
281 281 #
282 282 # In the future, we can send kills for only the specific
283 283 # nodes we know should go away or change shape, and then
284 284 # send a data stream that tells the client something like this:
285 285 #
286 286 # a) apply this changegroup
287 287 # b) apply nodes XXX, YYY, ZZZ that you already have
288 288 # c) goto a
289 289 #
290 290 # until they've built up the full new state.
291 291 # Convert to revnums and intersect with "common". The client should
292 292 # have made it a subset of "common" already, but let's be safe.
293 293 known = set(repo.revs("%ln & ::%ln", known, common))
294 294 # TODO: we could send only roots() of this set, and the
295 295 # list of nodes in common, and the client could work out
296 296 # what to strip, instead of us explicitly sending every
297 297 # single node.
298 298 deadrevs = known
299 299 def genkills():
300 300 for r in deadrevs:
301 301 yield _KILLNODESIGNAL
302 302 yield repo.changelog.node(r)
303 303 yield _DONESIGNAL
304 304 bundler.newpart(_CHANGESPECPART, data=genkills())
305 305 newvisit, newfull, newellipsis = _computeellipsis(
306 306 repo, set(), common, known, newmatch)
307 307 if newvisit:
308 308 cg = _packellipsischangegroup(
309 309 repo, common, newmatch, newfull, newellipsis,
310 310 newvisit, depth, source, version)
311 311 part = bundler.newpart('changegroup', data=cg)
312 312 part.addparam('version', version)
313 313 if 'treemanifest' in repo.requirements:
314 314 part.addparam('treemanifest', '1')
315 315
316 316 visitnodes, relevant_nodes, ellipsisroots = _computeellipsis(
317 317 repo, common, heads, set(), newmatch, depth=depth)
318 318
319 319 repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes))
320 320 if visitnodes:
321 321 cg = _packellipsischangegroup(
322 322 repo, common, newmatch, relevant_nodes, ellipsisroots,
323 323 visitnodes, depth, source, version)
324 324 part = bundler.newpart('changegroup', data=cg)
325 325 part.addparam('version', version)
326 326 if 'treemanifest' in repo.requirements:
327 327 part.addparam('treemanifest', '1')
328 328
329 329 def applyacl_narrow(repo, kwargs):
330 330 username = repo.ui.shortuser(repo.ui.username())
331 331 user_includes = repo.ui.configlist(
332 332 _NARROWACL_SECTION, username + '.includes',
333 333 repo.ui.configlist(_NARROWACL_SECTION, 'default.includes'))
334 334 user_excludes = repo.ui.configlist(
335 335 _NARROWACL_SECTION, username + '.excludes',
336 336 repo.ui.configlist(_NARROWACL_SECTION, 'default.excludes'))
337 337 if not user_includes:
338 338 raise error.Abort(_("{} configuration for user {} is empty")
339 339 .format(_NARROWACL_SECTION, username))
340 340
341 341 user_includes = [
342 342 'path:.' if p == '*' else 'path:' + p for p in user_includes]
343 343 user_excludes = [
344 344 'path:.' if p == '*' else 'path:' + p for p in user_excludes]
345 345
346 346 req_includes = set(kwargs.get('includepats', []))
347 347 req_excludes = set(kwargs.get('excludepats', []))
348 348
349 349 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
350 350 req_includes, req_excludes, user_includes, user_excludes)
351 351
352 352 if invalid_includes:
353 353 raise error.Abort(
354 354 _("The following includes are not accessible for {}: {}")
355 355 .format(username, invalid_includes))
356 356
357 357 new_args = {}
358 358 new_args.update(kwargs)
359 359 new_args['includepats'] = req_includes
360 360 if req_excludes:
361 361 new_args['excludepats'] = req_excludes
362 362 return new_args
363 363
364 364 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
365 365 def _handlechangespec_2(op, inpart):
366 366 includepats = set(inpart.params.get(_SPECPART_INCLUDE, '').splitlines())
367 367 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, '').splitlines())
368 368 narrowspec.save(op.repo, includepats, excludepats)
369 369 if not narrowrepo.REQUIREMENT in op.repo.requirements:
370 370 op.repo.requirements.add(narrowrepo.REQUIREMENT)
371 371 op.repo._writerequirements()
372 372 op.repo.invalidate(clearfilecache=True)
373 373
374 374 @bundle2.parthandler(_CHANGESPECPART)
375 375 def _handlechangespec(op, inpart):
376 376 repo = op.repo
377 377 cl = repo.changelog
378 378
379 379 # changesets which need to be stripped entirely. either they're no longer
380 380 # needed in the new narrow spec, or the server is sending a replacement
381 381 # in the changegroup part.
382 382 clkills = set()
383 383
384 384 # A changespec part contains all the updates to ellipsis nodes
385 385 # that will happen as a result of widening or narrowing a
386 386 # repo. All the changes that this block encounters are ellipsis
387 387 # nodes or flags to kill an existing ellipsis.
388 388 chunksignal = changegroup.readexactly(inpart, 4)
389 389 while chunksignal != _DONESIGNAL:
390 390 if chunksignal == _KILLNODESIGNAL:
391 391 # a node used to be an ellipsis but isn't anymore
392 392 ck = changegroup.readexactly(inpart, 20)
393 393 if cl.hasnode(ck):
394 394 clkills.add(ck)
395 395 else:
396 396 raise error.Abort(
397 397 _('unexpected changespec node chunk type: %s') % chunksignal)
398 398 chunksignal = changegroup.readexactly(inpart, 4)
399 399
400 400 if clkills:
401 401 # preserve bookmarks that repair.strip() would otherwise strip
402 402 bmstore = repo._bookmarks
403 403 class dummybmstore(dict):
404 404 def applychanges(self, repo, tr, changes):
405 405 pass
406 406 def recordchange(self, tr): # legacy version
407 407 pass
408 408 repo._bookmarks = dummybmstore()
409 409 chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True,
410 410 topic='widen')
411 411 repo._bookmarks = bmstore
412 412 if chgrpfile:
413 413 # presence of _widen_bundle attribute activates widen handler later
414 414 op._widen_bundle = chgrpfile
415 415 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
416 416 # will currently always be there when using the core+narrowhg server, but
417 417 # other servers may include a changespec part even when not widening (e.g.
418 418 # because we're deepening a shallow repo).
419 419 if util.safehasattr(repo, 'setnewnarrowpats'):
420 420 repo.setnewnarrowpats()
421 421
422 422 def handlechangegroup_widen(op, inpart):
423 423 """Changegroup exchange handler which restores temporarily-stripped nodes"""
424 424 # We saved a bundle with stripped node data we must now restore.
425 425 # This approach is based on mercurial/repair.py@6ee26a53c111.
426 426 repo = op.repo
427 427 ui = op.ui
428 428
429 429 chgrpfile = op._widen_bundle
430 430 del op._widen_bundle
431 431 vfs = repo.vfs
432 432
433 433 ui.note(_("adding branch\n"))
434 434 f = vfs.open(chgrpfile, "rb")
435 435 try:
436 436 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
437 437 if not ui.verbose:
438 438 # silence internal shuffling chatter
439 439 ui.pushbuffer()
440 440 if isinstance(gen, bundle2.unbundle20):
441 441 with repo.transaction('strip') as tr:
442 442 bundle2.processbundle(repo, gen, lambda: tr)
443 443 else:
444 444 gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True)
445 445 if not ui.verbose:
446 446 ui.popbuffer()
447 447 finally:
448 448 f.close()
449 449
450 450 # remove undo files
451 451 for undovfs, undofile in repo.undofiles():
452 452 try:
453 453 undovfs.unlink(undofile)
454 454 except OSError as e:
455 455 if e.errno != errno.ENOENT:
456 456 ui.warn(_('error removing %s: %s\n') %
457 457 (undovfs.join(undofile), str(e)))
458 458
459 459 # Remove partial backup only if there were no exceptions
460 460 vfs.unlink(chgrpfile)
461 461
462 462 def setup():
463 463 """Enable narrow repo support in bundle2-related extension points."""
464 464 extensions.wrapfunction(bundle2, 'getrepocaps', getrepocaps_narrow)
465 465
466 466 wireproto.gboptsmap['narrow'] = 'boolean'
467 467 wireproto.gboptsmap['depth'] = 'plain'
468 468 wireproto.gboptsmap['oldincludepats'] = 'csv'
469 469 wireproto.gboptsmap['oldexcludepats'] = 'csv'
470 470 wireproto.gboptsmap['includepats'] = 'csv'
471 471 wireproto.gboptsmap['excludepats'] = 'csv'
472 472 wireproto.gboptsmap['known'] = 'csv'
473 473
474 474 # Extend changegroup serving to handle requests from narrow clients.
475 475 origcgfn = exchange.getbundle2partsmapping['changegroup']
476 476 def wrappedcgfn(*args, **kwargs):
477 477 repo = args[1]
478 478 if repo.ui.has_section(_NARROWACL_SECTION):
479 479 getbundlechangegrouppart_narrow(
480 480 *args, **applyacl_narrow(repo, kwargs))
481 481 elif kwargs.get('narrow', False):
482 482 getbundlechangegrouppart_narrow(*args, **kwargs)
483 483 else:
484 484 origcgfn(*args, **kwargs)
485 485 exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn
486 486
487 487 # Extend changegroup receiver so client can fixup after widen requests.
488 488 origcghandler = bundle2.parthandlermapping['changegroup']
489 489 def wrappedcghandler(op, inpart):
490 490 origcghandler(op, inpart)
491 491 if util.safehasattr(op, '_widen_bundle'):
492 492 handlechangegroup_widen(op, inpart)
493 493 wrappedcghandler.params = origcghandler.params
494 494 bundle2.parthandlermapping['changegroup'] = wrappedcghandler
@@ -1,404 +1,404 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 cmdutil,
14 14 commands,
15 15 discovery,
16 16 error,
17 17 exchange,
18 18 extensions,
19 19 hg,
20 20 merge,
21 narrowspec,
21 22 node,
22 23 pycompat,
23 24 registrar,
24 25 repair,
25 26 repoview,
26 27 util,
27 28 )
28 29
29 30 from . import (
30 31 narrowbundle2,
31 32 narrowrepo,
32 narrowspec,
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 narrowspec.save(pullop.repo, 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 narrowrepo.wraprepo(repo.unfiltered(), opts_narrow)
104 104 if isinstance(repo, repoview.repoview):
105 105 repo.__class__.__bases__ = (repo.__class__.__bases__[0],
106 106 repo.unfiltered().__class__)
107 107 if opts_narrow:
108 108 repo.requirements.add(narrowrepo.REQUIREMENT)
109 109 repo._writerequirements()
110 110
111 111 return orig(repo, *args, **kwargs)
112 112
113 113 wrappedpull = extensions.wrappedfunction(exchange, 'pull', pullnarrow)
114 114
115 115 with wrappedextraprepare, wrappedpull:
116 116 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
117 117
118 118 def pullnarrowcmd(orig, ui, repo, *args, **opts):
119 119 """Wraps pull command to allow modifying narrow spec."""
120 120 wrappedextraprepare = util.nullcontextmanager()
121 121 if narrowrepo.REQUIREMENT in repo.requirements:
122 122
123 123 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
124 124 orig(pullop, kwargs)
125 125 if opts.get('depth'):
126 126 kwargs['depth'] = opts['depth']
127 127 wrappedextraprepare = extensions.wrappedfunction(exchange,
128 128 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
129 129
130 130 with wrappedextraprepare:
131 131 return orig(ui, repo, *args, **opts)
132 132
133 133 def archivenarrowcmd(orig, ui, repo, *args, **opts):
134 134 """Wraps archive command to narrow the default includes."""
135 135 if narrowrepo.REQUIREMENT in repo.requirements:
136 136 repo_includes, repo_excludes = repo.narrowpats
137 137 includes = set(opts.get('include', []))
138 138 excludes = set(opts.get('exclude', []))
139 139 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
140 140 includes, excludes, repo_includes, repo_excludes)
141 141 if includes:
142 142 opts['include'] = includes
143 143 if excludes:
144 144 opts['exclude'] = excludes
145 145 return orig(ui, repo, *args, **opts)
146 146
147 147 def pullbundle2extraprepare(orig, pullop, kwargs):
148 148 repo = pullop.repo
149 149 if narrowrepo.REQUIREMENT not in repo.requirements:
150 150 return orig(pullop, kwargs)
151 151
152 152 if narrowbundle2.NARROWCAP not in pullop.remotebundle2caps:
153 153 raise error.Abort(_("server doesn't support narrow clones"))
154 154 orig(pullop, kwargs)
155 155 kwargs['narrow'] = True
156 156 include, exclude = repo.narrowpats
157 157 kwargs['oldincludepats'] = include
158 158 kwargs['oldexcludepats'] = exclude
159 159 kwargs['includepats'] = include
160 160 kwargs['excludepats'] = exclude
161 161 kwargs['known'] = [node.hex(ctx.node()) for ctx in
162 162 repo.set('::%ln', pullop.common)
163 163 if ctx.node() != node.nullid]
164 164 if not kwargs['known']:
165 165 # Mercurial serialized an empty list as '' and deserializes it as
166 166 # [''], so delete it instead to avoid handling the empty string on the
167 167 # server.
168 168 del kwargs['known']
169 169
170 170 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
171 171 pullbundle2extraprepare)
172 172
173 173 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
174 174 newincludes, newexcludes, force):
175 175 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
176 176 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
177 177
178 178 # This is essentially doing "hg outgoing" to find all local-only
179 179 # commits. We will then check that the local-only commits don't
180 180 # have any changes to files that will be untracked.
181 181 unfi = repo.unfiltered()
182 182 outgoing = discovery.findcommonoutgoing(unfi, remote,
183 183 commoninc=commoninc)
184 184 ui.status(_('looking for local changes to affected paths\n'))
185 185 localnodes = []
186 186 for n in itertools.chain(outgoing.missing, outgoing.excluded):
187 187 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
188 188 localnodes.append(n)
189 189 revstostrip = unfi.revs('descendants(%ln)', localnodes)
190 190 hiddenrevs = repoview.filterrevs(repo, 'visible')
191 191 visibletostrip = list(repo.changelog.node(r)
192 192 for r in (revstostrip - hiddenrevs))
193 193 if visibletostrip:
194 194 ui.status(_('The following changeset(s) or their ancestors have '
195 195 'local changes not on the remote:\n'))
196 196 maxnodes = 10
197 197 if ui.verbose or len(visibletostrip) <= maxnodes:
198 198 for n in visibletostrip:
199 199 ui.status('%s\n' % node.short(n))
200 200 else:
201 201 for n in visibletostrip[:maxnodes]:
202 202 ui.status('%s\n' % node.short(n))
203 203 ui.status(_('...and %d more, use --verbose to list all\n') %
204 204 (len(visibletostrip) - maxnodes))
205 205 if not force:
206 206 raise error.Abort(_('local changes found'),
207 207 hint=_('use --force-delete-local-changes to '
208 208 'ignore'))
209 209
210 210 if revstostrip:
211 211 tostrip = [unfi.changelog.node(r) for r in revstostrip]
212 212 if repo['.'].node() in tostrip:
213 213 # stripping working copy, so move to a different commit first
214 214 urev = max(repo.revs('(::%n) - %ln + null',
215 215 repo['.'].node(), visibletostrip))
216 216 hg.clean(repo, urev)
217 217 repair.strip(ui, unfi, tostrip, topic='narrow')
218 218
219 219 todelete = []
220 220 for f, f2, size in repo.store.datafiles():
221 221 if f.startswith('data/'):
222 222 file = f[5:-2]
223 223 if not newmatch(file):
224 224 todelete.append(f)
225 225 elif f.startswith('meta/'):
226 226 dir = f[5:-13]
227 227 dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
228 228 include = True
229 229 for d in dirs:
230 230 visit = newmatch.visitdir(d)
231 231 if not visit:
232 232 include = False
233 233 break
234 234 if visit == 'all':
235 235 break
236 236 if not include:
237 237 todelete.append(f)
238 238
239 239 repo.destroying()
240 240
241 241 with repo.transaction("narrowing"):
242 242 for f in todelete:
243 243 ui.status(_('deleting %s\n') % f)
244 244 util.unlinkpath(repo.svfs.join(f))
245 245 repo.store.markremoved(f)
246 246
247 247 for f in repo.dirstate:
248 248 if not newmatch(f):
249 249 repo.dirstate.drop(f)
250 250 repo.wvfs.unlinkpath(f)
251 251 repo.setnarrowpats(newincludes, newexcludes)
252 252
253 253 repo.destroyed()
254 254
255 255 def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
256 256 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
257 257
258 258 # TODO(martinvonz): Get expansion working with widening/narrowing.
259 259 if narrowspec.needsexpansion(newincludes):
260 260 raise error.Abort('Expansion not yet supported on pull')
261 261
262 262 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
263 263 orig(pullop, kwargs)
264 264 # The old{in,ex}cludepats have already been set by orig()
265 265 kwargs['includepats'] = newincludes
266 266 kwargs['excludepats'] = newexcludes
267 267 wrappedextraprepare = extensions.wrappedfunction(exchange,
268 268 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
269 269
270 270 # define a function that narrowbundle2 can call after creating the
271 271 # backup bundle, but before applying the bundle from the server
272 272 def setnewnarrowpats():
273 273 repo.setnarrowpats(newincludes, newexcludes)
274 274 repo.setnewnarrowpats = setnewnarrowpats
275 275
276 276 ds = repo.dirstate
277 277 p1, p2 = ds.p1(), ds.p2()
278 278 with ds.parentchange():
279 279 ds.setparents(node.nullid, node.nullid)
280 280 common = commoninc[0]
281 281 with wrappedextraprepare:
282 282 exchange.pull(repo, remote, heads=common)
283 283 with ds.parentchange():
284 284 ds.setparents(p1, p2)
285 285
286 286 actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
287 287 addgaction = actions['g'].append
288 288
289 289 mf = repo['.'].manifest().matches(newmatch)
290 290 for f, fn in mf.iteritems():
291 291 if f not in repo.dirstate:
292 292 addgaction((f, (mf.flags(f), False),
293 293 "add from widened narrow clone"))
294 294
295 295 merge.applyupdates(repo, actions, wctx=repo[None],
296 296 mctx=repo['.'], overwrite=False)
297 297 merge.recordupdates(repo, actions, branchmerge=False)
298 298
299 299 # TODO(rdamazio): Make new matcher format and update description
300 300 @command('tracked',
301 301 [('', 'addinclude', [], _('new paths to include')),
302 302 ('', 'removeinclude', [], _('old paths to no longer include')),
303 303 ('', 'addexclude', [], _('new paths to exclude')),
304 304 ('', 'removeexclude', [], _('old paths to no longer exclude')),
305 305 ('', 'clear', False, _('whether to replace the existing narrowspec')),
306 306 ('', 'force-delete-local-changes', False,
307 307 _('forces deletion of local changes when narrowing')),
308 308 ] + commands.remoteopts,
309 309 _('[OPTIONS]... [REMOTE]'),
310 310 inferrepo=True)
311 311 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
312 312 """show or change the current narrowspec
313 313
314 314 With no argument, shows the current narrowspec entries, one per line. Each
315 315 line will be prefixed with 'I' or 'X' for included or excluded patterns,
316 316 respectively.
317 317
318 318 The narrowspec is comprised of expressions to match remote files and/or
319 319 directories that should be pulled into your client.
320 320 The narrowspec has *include* and *exclude* expressions, with excludes always
321 321 trumping includes: that is, if a file matches an exclude expression, it will
322 322 be excluded even if it also matches an include expression.
323 323 Excluding files that were never included has no effect.
324 324
325 325 Each included or excluded entry is in the format described by
326 326 'hg help patterns'.
327 327
328 328 The options allow you to add or remove included and excluded expressions.
329 329
330 330 If --clear is specified, then all previous includes and excludes are DROPPED
331 331 and replaced by the new ones specified to --addinclude and --addexclude.
332 332 If --clear is specified without any further options, the narrowspec will be
333 333 empty and will not match any files.
334 334 """
335 335 if narrowrepo.REQUIREMENT not in repo.requirements:
336 336 ui.warn(_('The narrow command is only supported on respositories cloned'
337 337 ' with --narrow.\n'))
338 338 return 1
339 339
340 340 # Before supporting, decide whether it "hg tracked --clear" should mean
341 341 # tracking no paths or all paths.
342 342 if opts['clear']:
343 343 ui.warn(_('The --clear option is not yet supported.\n'))
344 344 return 1
345 345
346 346 if narrowspec.needsexpansion(opts['addinclude'] + opts['addexclude']):
347 347 raise error.Abort('Expansion not yet supported on widen/narrow')
348 348
349 349 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
350 350 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
351 351 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
352 352 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
353 353 widening = addedincludes or removedexcludes
354 354 narrowing = removedincludes or addedexcludes
355 355 only_show = not widening and not narrowing
356 356
357 357 # Only print the current narrowspec.
358 358 if only_show:
359 359 include, exclude = repo.narrowpats
360 360
361 361 ui.pager('tracked')
362 362 fm = ui.formatter('narrow', opts)
363 363 for i in sorted(include):
364 364 fm.startitem()
365 365 fm.write('status', '%s ', 'I', label='narrow.included')
366 366 fm.write('pat', '%s\n', i, label='narrow.included')
367 367 for i in sorted(exclude):
368 368 fm.startitem()
369 369 fm.write('status', '%s ', 'X', label='narrow.excluded')
370 370 fm.write('pat', '%s\n', i, label='narrow.excluded')
371 371 fm.end()
372 372 return 0
373 373
374 374 with repo.wlock(), repo.lock():
375 375 cmdutil.bailifchanged(repo)
376 376
377 377 # Find the revisions we have in common with the remote. These will
378 378 # be used for finding local-only changes for narrowing. They will
379 379 # also define the set of revisions to update for widening.
380 380 remotepath = ui.expandpath(remotepath or 'default')
381 381 url, branches = hg.parseurl(remotepath)
382 382 ui.status(_('comparing with %s\n') % util.hidepassword(url))
383 383 remote = hg.peer(repo, opts, url)
384 384 commoninc = discovery.findcommonincoming(repo, remote)
385 385
386 386 oldincludes, oldexcludes = repo.narrowpats
387 387 if narrowing:
388 388 newincludes = oldincludes - removedincludes
389 389 newexcludes = oldexcludes | addedexcludes
390 390 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
391 391 newincludes, newexcludes,
392 392 opts['force_delete_local_changes'])
393 393 # _narrow() updated the narrowspec and _widen() below needs to
394 394 # use the updated values as its base (otherwise removed includes
395 395 # and addedexcludes will be lost in the resulting narrowspec)
396 396 oldincludes = newincludes
397 397 oldexcludes = newexcludes
398 398
399 399 if widening:
400 400 newincludes = oldincludes | addedincludes
401 401 newexcludes = oldexcludes - removedexcludes
402 402 _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
403 403
404 404 return 0
@@ -1,80 +1,79 b''
1 1 # narrowdirstate.py - extensions to mercurial dirstate to support narrow clones
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 from mercurial.i18n import _
11 11 from mercurial import (
12 12 dirstate,
13 13 error,
14 14 extensions,
15 15 match as matchmod,
16 narrowspec,
16 17 util as hgutil,
17 18 )
18 19
19 from . import narrowspec
20
21 20 def setup(repo):
22 21 """Add narrow spec dirstate ignore, block changes outside narrow spec."""
23 22
24 23 def walk(orig, self, match, subrepos, unknown, ignored, full=True,
25 24 narrowonly=True):
26 25 if narrowonly:
27 26 narrowmatch = repo.narrowmatch()
28 27 match = matchmod.intersectmatchers(match, narrowmatch)
29 28 return orig(self, match, subrepos, unknown, ignored, full)
30 29
31 30 extensions.wrapfunction(dirstate.dirstate, 'walk', walk)
32 31
33 32 # Prevent adding files that are outside the sparse checkout
34 33 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
35 34 for func in editfuncs:
36 35 def _wrapper(orig, self, *args):
37 36 dirstate = repo.dirstate
38 37 narrowmatch = repo.narrowmatch()
39 38 for f in args:
40 39 if f is not None and not narrowmatch(f) and f not in dirstate:
41 40 raise error.Abort(_("cannot track '%s' - it is outside " +
42 41 "the narrow clone") % f)
43 42 return orig(self, *args)
44 43 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
45 44
46 45 def filterrebuild(orig, self, parent, allfiles, changedfiles=None):
47 46 if changedfiles is None:
48 47 # Rebuilding entire dirstate, let's filter allfiles to match the
49 48 # narrowspec.
50 49 allfiles = [f for f in allfiles if repo.narrowmatch()(f)]
51 50 orig(self, parent, allfiles, changedfiles)
52 51
53 52 extensions.wrapfunction(dirstate.dirstate, 'rebuild', filterrebuild)
54 53
55 54 def _narrowbackupname(backupname):
56 55 assert 'dirstate' in backupname
57 56 return backupname.replace('dirstate', narrowspec.FILENAME)
58 57
59 58 def restorebackup(orig, self, tr, backupname):
60 59 self._opener.rename(_narrowbackupname(backupname), narrowspec.FILENAME,
61 60 checkambig=True)
62 61 orig(self, tr, backupname)
63 62
64 63 extensions.wrapfunction(dirstate.dirstate, 'restorebackup', restorebackup)
65 64
66 65 def savebackup(orig, self, tr, backupname):
67 66 orig(self, tr, backupname)
68 67
69 68 narrowbackupname = _narrowbackupname(backupname)
70 69 self._opener.tryunlink(narrowbackupname)
71 70 hgutil.copyfile(self._opener.join(narrowspec.FILENAME),
72 71 self._opener.join(narrowbackupname), hardlink=True)
73 72
74 73 extensions.wrapfunction(dirstate.dirstate, 'savebackup', savebackup)
75 74
76 75 def clearbackup(orig, self, tr, backupname):
77 76 orig(self, tr, backupname)
78 77 self._opener.unlink(_narrowbackupname(backupname))
79 78
80 79 extensions.wrapfunction(dirstate.dirstate, 'clearbackup', clearbackup)
@@ -1,113 +1,113 b''
1 1 # narrowrepo.py - repository which supports narrow revlogs, lazy loading
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 from mercurial import (
11 11 bundlerepo,
12 12 hg,
13 13 localrepo,
14 14 match as matchmod,
15 narrowspec,
15 16 scmutil,
16 17 )
17 18
18 19 from . import (
19 20 narrowrevlog,
20 narrowspec,
21 21 )
22 22
23 23 # When narrowing is finalized and no longer subject to format changes,
24 24 # we should move this to just "narrow" or similar.
25 25 REQUIREMENT = 'narrowhg-experimental'
26 26
27 27 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
28 28 orig(sourcerepo, destrepo, **kwargs)
29 29 if REQUIREMENT in sourcerepo.requirements:
30 30 with destrepo.wlock():
31 31 with destrepo.vfs('shared', 'a') as fp:
32 32 fp.write(narrowspec.FILENAME + '\n')
33 33
34 34 def unsharenarrowspec(orig, ui, repo, repopath):
35 35 if (REQUIREMENT in repo.requirements
36 36 and repo.path == repopath and repo.shared()):
37 37 srcrepo = hg.sharedreposource(repo)
38 38 with srcrepo.vfs(narrowspec.FILENAME) as f:
39 39 spec = f.read()
40 40 with repo.vfs(narrowspec.FILENAME, 'w') as f:
41 41 f.write(spec)
42 42 return orig(ui, repo, repopath)
43 43
44 44 def wraprepo(repo, opts_narrow):
45 45 """Enables narrow clone functionality on a single local repository."""
46 46
47 47 cacheprop = localrepo.storecache
48 48 if isinstance(repo, bundlerepo.bundlerepository):
49 49 # We have to use a different caching property decorator for
50 50 # bundlerepo because storecache blows up in strange ways on a
51 51 # bundlerepo. Fortunately, there's no risk of data changing in
52 52 # a bundlerepo.
53 53 cacheprop = lambda name: localrepo.unfilteredpropertycache
54 54
55 55 class narrowrepository(repo.__class__):
56 56
57 57 def _constructmanifest(self):
58 58 manifest = super(narrowrepository, self)._constructmanifest()
59 59 narrowrevlog.makenarrowmanifestrevlog(manifest, repo)
60 60 return manifest
61 61
62 62 @cacheprop('00manifest.i')
63 63 def manifestlog(self):
64 64 mfl = super(narrowrepository, self).manifestlog
65 65 narrowrevlog.makenarrowmanifestlog(mfl, self)
66 66 return mfl
67 67
68 68 def file(self, f):
69 69 fl = super(narrowrepository, self).file(f)
70 70 narrowrevlog.makenarrowfilelog(fl, self.narrowmatch())
71 71 return fl
72 72
73 73 @localrepo.repofilecache(narrowspec.FILENAME)
74 74 def narrowpats(self):
75 75 """matcher patterns for this repository's narrowspec
76 76
77 77 A tuple of (includes, excludes).
78 78 """
79 79 return narrowspec.load(self)
80 80
81 81 @localrepo.repofilecache(narrowspec.FILENAME)
82 82 def _narrowmatch(self):
83 83 include, exclude = self.narrowpats
84 84 if not opts_narrow and not include and not exclude:
85 85 return matchmod.always(self.root, '')
86 86 return narrowspec.match(self.root, include=include, exclude=exclude)
87 87
88 88 # TODO(martinvonz): make this property-like instead?
89 89 def narrowmatch(self):
90 90 return self._narrowmatch
91 91
92 92 def setnarrowpats(self, newincludes, newexcludes):
93 93 narrowspec.save(self, newincludes, newexcludes)
94 94 self.invalidate(clearfilecache=True)
95 95
96 96 # I'm not sure this is the right place to do this filter.
97 97 # context._manifestmatches() would probably be better, or perhaps
98 98 # move it to a later place, in case some of the callers do want to know
99 99 # which directories changed. This seems to work for now, though.
100 100 def status(self, *args, **kwargs):
101 101 s = super(narrowrepository, self).status(*args, **kwargs)
102 102 narrowmatch = self.narrowmatch()
103 103 modified = list(filter(narrowmatch, s.modified))
104 104 added = list(filter(narrowmatch, s.added))
105 105 removed = list(filter(narrowmatch, s.removed))
106 106 deleted = list(filter(narrowmatch, s.deleted))
107 107 unknown = list(filter(narrowmatch, s.unknown))
108 108 ignored = list(filter(narrowmatch, s.ignored))
109 109 clean = list(filter(narrowmatch, s.clean))
110 110 return scmutil.status(modified, added, removed, deleted, unknown,
111 111 ignored, clean)
112 112
113 113 repo.__class__ = narrowrepository
@@ -1,53 +1,52 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 10 from mercurial.i18n import _
11 11 from mercurial import (
12 12 error,
13 13 extensions,
14 14 hg,
15 narrowspec,
15 16 node,
16 17 )
17 18
18 from . import narrowspec
19
20 19 def uisetup():
21 20 def peersetup(ui, peer):
22 21 # We must set up the expansion before reposetup below, since it's used
23 22 # at clone time before we have a repo.
24 23 class expandingpeer(peer.__class__):
25 24 def expandnarrow(self, narrow_include, narrow_exclude, nodes):
26 25 ui.status(_("expanding narrowspec\n"))
27 26 if not self.capable('exp-expandnarrow'):
28 27 raise error.Abort(
29 28 'peer does not support expanding narrowspecs')
30 29
31 30 hex_nodes = (node.hex(n) for n in nodes)
32 31 new_narrowspec = self._call(
33 32 'expandnarrow',
34 33 includepats=','.join(narrow_include),
35 34 excludepats=','.join(narrow_exclude),
36 35 nodes=','.join(hex_nodes))
37 36
38 37 return narrowspec.parseserverpatterns(new_narrowspec)
39 38 peer.__class__ = expandingpeer
40 39 hg.wirepeersetupfuncs.append(peersetup)
41 40
42 41 def reposetup(repo):
43 42 def wirereposetup(ui, peer):
44 43 def wrapped(orig, cmd, *args, **kwargs):
45 44 if cmd == 'unbundle':
46 45 # TODO: don't blindly add include/exclude wireproto
47 46 # arguments to unbundle.
48 47 include, exclude = repo.narrowpats
49 48 kwargs["includepats"] = ','.join(include)
50 49 kwargs["excludepats"] = ','.join(exclude)
51 50 return orig(cmd, *args, **kwargs)
52 51 extensions.wrapfunction(peer, '_calltwowaystream', wrapped)
53 52 hg.wirepeersetupfuncs.append(wirereposetup)
@@ -1,204 +1,204 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 from mercurial.i18n import _
13 from mercurial import (
12 from .i18n import _
13 from . import (
14 14 error,
15 15 hg,
16 16 match as matchmod,
17 17 util,
18 18 )
19 19
20 20 FILENAME = 'narrowspec'
21 21
22 22 def _parsestoredpatterns(text):
23 23 """Parses the narrowspec format that's stored on disk."""
24 24 patlist = None
25 25 includepats = []
26 26 excludepats = []
27 27 for l in text.splitlines():
28 28 if l == '[includes]':
29 29 if patlist is None:
30 30 patlist = includepats
31 31 else:
32 32 raise error.Abort(_('narrowspec includes section must appear '
33 33 'at most once, before excludes'))
34 34 elif l == '[excludes]':
35 35 if patlist is not excludepats:
36 36 patlist = excludepats
37 37 else:
38 38 raise error.Abort(_('narrowspec excludes section must appear '
39 39 'at most once'))
40 40 else:
41 41 patlist.append(l)
42 42
43 43 return set(includepats), set(excludepats)
44 44
45 45 def parseserverpatterns(text):
46 46 """Parses the narrowspec format that's returned by the server."""
47 47 includepats = set()
48 48 excludepats = set()
49 49
50 50 # We get one entry per line, in the format "<key> <value>".
51 51 # It's OK for value to contain other spaces.
52 52 for kp in (l.split(' ', 1) for l in text.splitlines()):
53 53 if len(kp) != 2:
54 54 raise error.Abort(_('Invalid narrowspec pattern line: "%s"') % kp)
55 55 key = kp[0]
56 56 pat = kp[1]
57 57 if key == 'include':
58 58 includepats.add(pat)
59 59 elif key == 'exclude':
60 60 excludepats.add(pat)
61 61 else:
62 62 raise error.Abort(_('Invalid key "%s" in server response') % key)
63 63
64 64 return includepats, excludepats
65 65
66 66 def normalizesplitpattern(kind, pat):
67 67 """Returns the normalized version of a pattern and kind.
68 68
69 69 Returns a tuple with the normalized kind and normalized pattern.
70 70 """
71 71 pat = pat.rstrip('/')
72 72 _validatepattern(pat)
73 73 return kind, pat
74 74
75 75 def _numlines(s):
76 76 """Returns the number of lines in s, including ending empty lines."""
77 77 # We use splitlines because it is Unicode-friendly and thus Python 3
78 78 # compatible. However, it does not count empty lines at the end, so trick
79 79 # it by adding a character at the end.
80 80 return len((s + 'x').splitlines())
81 81
82 82 def _validatepattern(pat):
83 83 """Validates the pattern and aborts if it is invalid.
84 84
85 85 Patterns are stored in the narrowspec as newline-separated
86 86 POSIX-style bytestring paths. There's no escaping.
87 87 """
88 88
89 89 # We use newlines as separators in the narrowspec file, so don't allow them
90 90 # in patterns.
91 91 if _numlines(pat) > 1:
92 raise error.Abort('newlines are not allowed in narrowspec paths')
92 raise error.Abort(_('newlines are not allowed in narrowspec paths'))
93 93
94 94 components = pat.split('/')
95 95 if '.' in components or '..' in components:
96 96 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
97 97
98 98 def normalizepattern(pattern, defaultkind='path'):
99 99 """Returns the normalized version of a text-format pattern.
100 100
101 101 If the pattern has no kind, the default will be added.
102 102 """
103 103 kind, pat = matchmod._patsplit(pattern, defaultkind)
104 104 return '%s:%s' % normalizesplitpattern(kind, pat)
105 105
106 106 def parsepatterns(pats):
107 107 """Parses a list of patterns into a typed pattern set."""
108 108 return set(normalizepattern(p) for p in pats)
109 109
110 110 def format(includes, excludes):
111 111 output = '[includes]\n'
112 112 for i in sorted(includes - excludes):
113 113 output += i + '\n'
114 114 output += '[excludes]\n'
115 115 for e in sorted(excludes):
116 116 output += e + '\n'
117 117 return output
118 118
119 119 def match(root, include=None, exclude=None):
120 120 if not include:
121 121 # Passing empty include and empty exclude to matchmod.match()
122 122 # gives a matcher that matches everything, so explicitly use
123 123 # the nevermatcher.
124 124 return matchmod.never(root, '')
125 125 return matchmod.match(root, '', [], include=include or [],
126 126 exclude=exclude or [])
127 127
128 128 def needsexpansion(includes):
129 129 return [i for i in includes if i.startswith('include:')]
130 130
131 131 def load(repo):
132 132 if repo.shared():
133 133 repo = hg.sharedreposource(repo)
134 134 try:
135 135 spec = repo.vfs.read(FILENAME)
136 136 except IOError as e:
137 137 # Treat "narrowspec does not exist" the same as "narrowspec file exists
138 138 # and is empty".
139 139 if e.errno == errno.ENOENT:
140 140 # Without this the next call to load will use the cached
141 141 # non-existence of the file, which can cause some odd issues.
142 142 repo.invalidate(clearfilecache=True)
143 143 return set(), set()
144 144 raise
145 145 return _parsestoredpatterns(spec)
146 146
147 147 def save(repo, includepats, excludepats):
148 148 spec = format(includepats, excludepats)
149 149 if repo.shared():
150 150 repo = hg.sharedreposource(repo)
151 151 repo.vfs.write(FILENAME, spec)
152 152
153 153 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
154 154 r""" Restricts the patterns according to repo settings,
155 155 results in a logical AND operation
156 156
157 157 :param req_includes: requested includes
158 158 :param req_excludes: requested excludes
159 159 :param repo_includes: repo includes
160 160 :param repo_excludes: repo excludes
161 161 :return: include patterns, exclude patterns, and invalid include patterns.
162 162
163 163 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
164 164 (set(['f1']), {}, [])
165 165 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
166 166 (set(['f1']), {}, [])
167 167 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
168 168 (set(['f1/fc1']), {}, [])
169 169 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
170 170 ([], set(['path:.']), [])
171 171 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
172 172 (set(['f2/fc2']), {}, [])
173 173 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
174 174 ([], set(['path:.']), [])
175 175 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
176 176 (set(['f1/$non_exitent_var']), {}, [])
177 177 """
178 178 res_excludes = set(req_excludes)
179 179 res_excludes.update(repo_excludes)
180 180 invalid_includes = []
181 181 if not req_includes:
182 182 res_includes = set(repo_includes)
183 183 elif 'path:.' not in repo_includes:
184 184 res_includes = []
185 185 for req_include in req_includes:
186 186 req_include = util.expandpath(util.normpath(req_include))
187 187 if req_include in repo_includes:
188 188 res_includes.append(req_include)
189 189 continue
190 190 valid = False
191 191 for repo_include in repo_includes:
192 192 if req_include.startswith(repo_include + '/'):
193 193 valid = True
194 194 res_includes.append(req_include)
195 195 break
196 196 if not valid:
197 197 invalid_includes.append(req_include)
198 198 if len(res_includes) == 0:
199 199 res_excludes = {'path:.'}
200 200 else:
201 201 res_includes = set(res_includes)
202 202 else:
203 203 res_includes = set(req_includes)
204 204 return res_includes, res_excludes, invalid_includes
@@ -1,170 +1,170 b''
1 1 $ . "$TESTDIR/narrow-library.sh"
2 2
3 3 $ hg init master
4 4 $ cd master
5 5
6 6 $ mkdir inside
7 7 $ echo inside > inside/f1
8 8 $ mkdir outside
9 9 $ echo outside > outside/f2
10 10 $ mkdir patchdir
11 11 $ echo patch_this > patchdir/f3
12 12 $ hg ci -Aqm 'initial'
13 13
14 14 $ cd ..
15 15
16 16 $ hg clone --narrow ssh://user@dummy/master narrow --include inside
17 17 requesting all changes
18 18 adding changesets
19 19 adding manifests
20 20 adding file changes
21 21 added 1 changesets with 1 changes to 1 files
22 22 new changesets dff6a2a6d433
23 23 updating to branch default
24 24 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
25 25
26 26 $ cd narrow
27 27
28 28 $ mkdir outside
29 29 $ echo other_contents > outside/f2
30 30 $ grep outside .hg/narrowspec
31 31 [1]
32 32 $ grep outside .hg/dirstate
33 33 [1]
34 34 $ hg status
35 35
36 36 `hg status` did not add outside.
37 37 $ grep outside .hg/narrowspec
38 38 [1]
39 39 $ grep outside .hg/dirstate
40 40 [1]
41 41
42 42 Unfortunately this is not really a candidate for adding to narrowhg proper,
43 43 since it depends on some other source for providing the manifests (when using
44 44 treemanifests) and file contents. Something like a virtual filesystem and/or
45 45 remotefilelog. We want to be useful when not using those systems, so we do not
46 46 have this method available in narrowhg proper at the moment.
47 47 $ cat > "$TESTTMP/expand_extension.py" <<EOF
48 48 > import os
49 49 > import sys
50 50 >
51 51 > from mercurial import extensions
52 52 > from mercurial import localrepo
53 53 > from mercurial import match as matchmod
54 > from mercurial import narrowspec
54 55 > from mercurial import patch
55 56 > from mercurial import util as hgutil
56 57 >
57 58 > def expandnarrowspec(ui, repo, newincludes=None):
58 59 > if not newincludes:
59 60 > return
60 61 > import sys
61 62 > newincludes = set([newincludes])
62 > narrowhg = extensions.find('narrow')
63 63 > includes, excludes = repo.narrowpats
64 > currentmatcher = narrowhg.narrowspec.match(repo.root, includes, excludes)
64 > currentmatcher = narrowspec.match(repo.root, includes, excludes)
65 65 > includes = includes | newincludes
66 66 > if not repo.currenttransaction():
67 67 > ui.develwarn('expandnarrowspec called outside of transaction!')
68 68 > repo.setnarrowpats(includes, excludes)
69 > newmatcher = narrowhg.narrowspec.match(repo.root, includes, excludes)
69 > newmatcher = narrowspec.match(repo.root, includes, excludes)
70 70 > added = matchmod.differencematcher(newmatcher, currentmatcher)
71 71 > for f in repo['.'].manifest().walk(added):
72 72 > repo.dirstate.normallookup(f)
73 73 >
74 74 > def makeds(ui, repo):
75 75 > def wrapds(orig, self):
76 76 > ds = orig(self)
77 77 > class expandingdirstate(ds.__class__):
78 78 > # Mercurial 4.4 uses this version.
79 79 > @hgutil.propertycache
80 80 > def _map(self):
81 81 > ret = super(expandingdirstate, self)._map
82 82 > with repo.wlock(), repo.lock(), repo.transaction(
83 83 > 'expandnarrowspec'):
84 84 > expandnarrowspec(ui, repo, os.environ.get('DIRSTATEINCLUDES'))
85 85 > return ret
86 86 > # Mercurial 4.3.3 and earlier uses this version. It seems that
87 87 > # narrowhg does not currently support this version, but we include
88 88 > # it just in case backwards compatibility is restored.
89 89 > def _read(self):
90 90 > ret = super(expandingdirstate, self)._read()
91 91 > with repo.wlock(), repo.lock(), repo.transaction(
92 92 > 'expandnarrowspec'):
93 93 > expandnarrowspec(ui, repo, os.environ.get('DIRSTATEINCLUDES'))
94 94 > return ret
95 95 > ds.__class__ = expandingdirstate
96 96 > return ds
97 97 > return wrapds
98 98 >
99 99 > def reposetup(ui, repo):
100 100 > extensions.wrapfilecache(localrepo.localrepository, 'dirstate',
101 101 > makeds(ui, repo))
102 102 > def overridepatch(orig, *args, **kwargs):
103 103 > with repo.wlock():
104 104 > expandnarrowspec(ui, repo, os.environ.get('PATCHINCLUDES'))
105 105 > return orig(*args, **kwargs)
106 106 >
107 107 > extensions.wrapfunction(patch, 'patch', overridepatch)
108 108 > EOF
109 109 $ cat >> ".hg/hgrc" <<EOF
110 110 > [extensions]
111 111 > expand_extension = $TESTTMP/expand_extension.py
112 112 > EOF
113 113
114 114 Since we do not have the ability to rely on a virtual filesystem or
115 115 remotefilelog in the test, we just fake it by copying the data from the 'master'
116 116 repo.
117 117 $ cp -a ../master/.hg/store/data/* .hg/store/data
118 118 Do that for patchdir as well.
119 119 $ cp -a ../master/patchdir .
120 120
121 121 `hg status` will now add outside, but not patchdir.
122 122 $ DIRSTATEINCLUDES=path:outside hg status
123 123 M outside/f2
124 124 $ grep outside .hg/narrowspec
125 125 path:outside
126 126 $ grep outside .hg/dirstate > /dev/null
127 127 $ grep patchdir .hg/narrowspec
128 128 [1]
129 129 $ grep patchdir .hg/dirstate
130 130 [1]
131 131
132 132 Get rid of the modification to outside/f2.
133 133 $ hg update -C .
134 134 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
135 135
136 136 This patch will not apply cleanly at the moment, so `hg import` will break
137 137 $ cat > "$TESTTMP/foo.patch" <<EOF
138 138 > --- patchdir/f3
139 139 > +++ patchdir/f3
140 140 > @@ -1,1 +1,1 @@
141 141 > -this should be "patch_this", but its not, so patch fails
142 142 > +this text is irrelevant
143 143 > EOF
144 144 $ PATCHINCLUDES=path:patchdir hg import -p0 -e "$TESTTMP/foo.patch" -m ignored
145 145 applying $TESTTMP/foo.patch
146 146 patching file patchdir/f3
147 147 Hunk #1 FAILED at 0
148 148 1 out of 1 hunks FAILED -- saving rejects to file patchdir/f3.rej
149 149 abort: patch failed to apply
150 150 [255]
151 151 $ grep patchdir .hg/narrowspec
152 152 [1]
153 153 $ grep patchdir .hg/dirstate > /dev/null
154 154 [1]
155 155
156 156 Let's make it apply cleanly and see that it *did* expand properly
157 157 $ cat > "$TESTTMP/foo.patch" <<EOF
158 158 > --- patchdir/f3
159 159 > +++ patchdir/f3
160 160 > @@ -1,1 +1,1 @@
161 161 > -patch_this
162 162 > +patched_this
163 163 > EOF
164 164 $ PATCHINCLUDES=path:patchdir hg import -p0 -e "$TESTTMP/foo.patch" -m message
165 165 applying $TESTTMP/foo.patch
166 166 $ cat patchdir/f3
167 167 patched_this
168 168 $ grep patchdir .hg/narrowspec
169 169 path:patchdir
170 170 $ grep patchdir .hg/dirstate > /dev/null
General Comments 0
You need to be logged in to leave comments. Login now