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