##// END OF EJS Templates
narrow: keep bookmarks temporarily stripped for as long as commits are...
Martin von Zweigbergk -
r40903:109a267a default
parent child Browse files
Show More
@@ -1,274 +1,279 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 errno
10 import errno
11 import struct
11 import struct
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial.node import (
14 from mercurial.node import (
15 bin,
15 bin,
16 nullid,
16 nullid,
17 )
17 )
18 from mercurial import (
18 from mercurial import (
19 bundle2,
19 bundle2,
20 changegroup,
20 changegroup,
21 error,
21 error,
22 exchange,
22 exchange,
23 localrepo,
23 narrowspec,
24 narrowspec,
24 repair,
25 repair,
25 repository,
26 repository,
26 util,
27 util,
27 wireprototypes,
28 wireprototypes,
28 )
29 )
29 from mercurial.utils import (
30 from mercurial.utils import (
30 stringutil,
31 stringutil,
31 )
32 )
32
33
33 _NARROWACL_SECTION = 'narrowhgacl'
34 _NARROWACL_SECTION = 'narrowhgacl'
34 _CHANGESPECPART = 'narrow:changespec'
35 _CHANGESPECPART = 'narrow:changespec'
35 _SPECPART = 'narrow:spec'
36 _SPECPART = 'narrow:spec'
36 _SPECPART_INCLUDE = 'include'
37 _SPECPART_INCLUDE = 'include'
37 _SPECPART_EXCLUDE = 'exclude'
38 _SPECPART_EXCLUDE = 'exclude'
38 _KILLNODESIGNAL = 'KILL'
39 _KILLNODESIGNAL = 'KILL'
39 _DONESIGNAL = 'DONE'
40 _DONESIGNAL = 'DONE'
40 _ELIDEDCSHEADER = '>20s20s20sl' # cset id, p1, p2, len(text)
41 _ELIDEDCSHEADER = '>20s20s20sl' # cset id, p1, p2, len(text)
41 _ELIDEDMFHEADER = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
42 _ELIDEDMFHEADER = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
42 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
43 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
43 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
44 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
44
45
45 # Serve a changegroup for a client with a narrow clone.
46 # Serve a changegroup for a client with a narrow clone.
46 def getbundlechangegrouppart_narrow(bundler, repo, source,
47 def getbundlechangegrouppart_narrow(bundler, repo, source,
47 bundlecaps=None, b2caps=None, heads=None,
48 bundlecaps=None, b2caps=None, heads=None,
48 common=None, **kwargs):
49 common=None, **kwargs):
49 assert repo.ui.configbool('experimental', 'narrowservebrokenellipses')
50 assert repo.ui.configbool('experimental', 'narrowservebrokenellipses')
50
51
51 cgversions = b2caps.get('changegroup')
52 cgversions = b2caps.get('changegroup')
52 if cgversions: # 3.1 and 3.2 ship with an empty value
53 if cgversions: # 3.1 and 3.2 ship with an empty value
53 cgversions = [v for v in cgversions
54 cgversions = [v for v in cgversions
54 if v in changegroup.supportedoutgoingversions(repo)]
55 if v in changegroup.supportedoutgoingversions(repo)]
55 if not cgversions:
56 if not cgversions:
56 raise ValueError(_('no common changegroup version'))
57 raise ValueError(_('no common changegroup version'))
57 version = max(cgversions)
58 version = max(cgversions)
58 else:
59 else:
59 raise ValueError(_("server does not advertise changegroup version,"
60 raise ValueError(_("server does not advertise changegroup version,"
60 " can't negotiate support for ellipsis nodes"))
61 " can't negotiate support for ellipsis nodes"))
61
62
62 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
63 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
63 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
64 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
64 newmatch = narrowspec.match(repo.root, include=include, exclude=exclude)
65 newmatch = narrowspec.match(repo.root, include=include, exclude=exclude)
65
66
66 depth = kwargs.get(r'depth', None)
67 depth = kwargs.get(r'depth', None)
67 if depth is not None:
68 if depth is not None:
68 depth = int(depth)
69 depth = int(depth)
69 if depth < 1:
70 if depth < 1:
70 raise error.Abort(_('depth must be positive, got %d') % depth)
71 raise error.Abort(_('depth must be positive, got %d') % depth)
71
72
72 heads = set(heads or repo.heads())
73 heads = set(heads or repo.heads())
73 common = set(common or [nullid])
74 common = set(common or [nullid])
74 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
75 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
75 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
76 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
76 known = {bin(n) for n in kwargs.get(r'known', [])}
77 known = {bin(n) for n in kwargs.get(r'known', [])}
77 if known and (oldinclude != include or oldexclude != exclude):
78 if known and (oldinclude != include or oldexclude != exclude):
78 # Steps:
79 # Steps:
79 # 1. Send kill for "$known & ::common"
80 # 1. Send kill for "$known & ::common"
80 #
81 #
81 # 2. Send changegroup for ::common
82 # 2. Send changegroup for ::common
82 #
83 #
83 # 3. Proceed.
84 # 3. Proceed.
84 #
85 #
85 # In the future, we can send kills for only the specific
86 # In the future, we can send kills for only the specific
86 # nodes we know should go away or change shape, and then
87 # nodes we know should go away or change shape, and then
87 # send a data stream that tells the client something like this:
88 # send a data stream that tells the client something like this:
88 #
89 #
89 # a) apply this changegroup
90 # a) apply this changegroup
90 # b) apply nodes XXX, YYY, ZZZ that you already have
91 # b) apply nodes XXX, YYY, ZZZ that you already have
91 # c) goto a
92 # c) goto a
92 #
93 #
93 # until they've built up the full new state.
94 # until they've built up the full new state.
94 # Convert to revnums and intersect with "common". The client should
95 # Convert to revnums and intersect with "common". The client should
95 # have made it a subset of "common" already, but let's be safe.
96 # have made it a subset of "common" already, but let's be safe.
96 known = set(repo.revs("%ln & ::%ln", known, common))
97 known = set(repo.revs("%ln & ::%ln", known, common))
97 # TODO: we could send only roots() of this set, and the
98 # TODO: we could send only roots() of this set, and the
98 # list of nodes in common, and the client could work out
99 # list of nodes in common, and the client could work out
99 # what to strip, instead of us explicitly sending every
100 # what to strip, instead of us explicitly sending every
100 # single node.
101 # single node.
101 deadrevs = known
102 deadrevs = known
102 def genkills():
103 def genkills():
103 for r in deadrevs:
104 for r in deadrevs:
104 yield _KILLNODESIGNAL
105 yield _KILLNODESIGNAL
105 yield repo.changelog.node(r)
106 yield repo.changelog.node(r)
106 yield _DONESIGNAL
107 yield _DONESIGNAL
107 bundler.newpart(_CHANGESPECPART, data=genkills())
108 bundler.newpart(_CHANGESPECPART, data=genkills())
108 newvisit, newfull, newellipsis = exchange._computeellipsis(
109 newvisit, newfull, newellipsis = exchange._computeellipsis(
109 repo, set(), common, known, newmatch)
110 repo, set(), common, known, newmatch)
110 if newvisit:
111 if newvisit:
111 packer = changegroup.getbundler(version, repo,
112 packer = changegroup.getbundler(version, repo,
112 matcher=newmatch,
113 matcher=newmatch,
113 ellipses=True,
114 ellipses=True,
114 shallow=depth is not None,
115 shallow=depth is not None,
115 ellipsisroots=newellipsis,
116 ellipsisroots=newellipsis,
116 fullnodes=newfull)
117 fullnodes=newfull)
117 cgdata = packer.generate(common, newvisit, False, 'narrow_widen')
118 cgdata = packer.generate(common, newvisit, False, 'narrow_widen')
118
119
119 part = bundler.newpart('changegroup', data=cgdata)
120 part = bundler.newpart('changegroup', data=cgdata)
120 part.addparam('version', version)
121 part.addparam('version', version)
121 if 'treemanifest' in repo.requirements:
122 if 'treemanifest' in repo.requirements:
122 part.addparam('treemanifest', '1')
123 part.addparam('treemanifest', '1')
123
124
124 visitnodes, relevant_nodes, ellipsisroots = exchange._computeellipsis(
125 visitnodes, relevant_nodes, ellipsisroots = exchange._computeellipsis(
125 repo, common, heads, set(), newmatch, depth=depth)
126 repo, common, heads, set(), newmatch, depth=depth)
126
127
127 repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes))
128 repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes))
128 if visitnodes:
129 if visitnodes:
129 packer = changegroup.getbundler(version, repo,
130 packer = changegroup.getbundler(version, repo,
130 matcher=newmatch,
131 matcher=newmatch,
131 ellipses=True,
132 ellipses=True,
132 shallow=depth is not None,
133 shallow=depth is not None,
133 ellipsisroots=ellipsisroots,
134 ellipsisroots=ellipsisroots,
134 fullnodes=relevant_nodes)
135 fullnodes=relevant_nodes)
135 cgdata = packer.generate(common, visitnodes, False, 'narrow_widen')
136 cgdata = packer.generate(common, visitnodes, False, 'narrow_widen')
136
137
137 part = bundler.newpart('changegroup', data=cgdata)
138 part = bundler.newpart('changegroup', data=cgdata)
138 part.addparam('version', version)
139 part.addparam('version', version)
139 if 'treemanifest' in repo.requirements:
140 if 'treemanifest' in repo.requirements:
140 part.addparam('treemanifest', '1')
141 part.addparam('treemanifest', '1')
141
142
142 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
143 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
143 def _handlechangespec_2(op, inpart):
144 def _handlechangespec_2(op, inpart):
144 includepats = set(inpart.params.get(_SPECPART_INCLUDE, '').splitlines())
145 includepats = set(inpart.params.get(_SPECPART_INCLUDE, '').splitlines())
145 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, '').splitlines())
146 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, '').splitlines())
146 narrowspec.validatepatterns(includepats)
147 narrowspec.validatepatterns(includepats)
147 narrowspec.validatepatterns(excludepats)
148 narrowspec.validatepatterns(excludepats)
148
149
149 if not repository.NARROW_REQUIREMENT in op.repo.requirements:
150 if not repository.NARROW_REQUIREMENT in op.repo.requirements:
150 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
151 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
151 op.repo._writerequirements()
152 op.repo._writerequirements()
152 op.repo.setnarrowpats(includepats, excludepats)
153 op.repo.setnarrowpats(includepats, excludepats)
153
154
154 @bundle2.parthandler(_CHANGESPECPART)
155 @bundle2.parthandler(_CHANGESPECPART)
155 def _handlechangespec(op, inpart):
156 def _handlechangespec(op, inpart):
156 repo = op.repo
157 repo = op.repo
157 cl = repo.changelog
158 cl = repo.changelog
158
159
159 # changesets which need to be stripped entirely. either they're no longer
160 # changesets which need to be stripped entirely. either they're no longer
160 # needed in the new narrow spec, or the server is sending a replacement
161 # needed in the new narrow spec, or the server is sending a replacement
161 # in the changegroup part.
162 # in the changegroup part.
162 clkills = set()
163 clkills = set()
163
164
164 # A changespec part contains all the updates to ellipsis nodes
165 # A changespec part contains all the updates to ellipsis nodes
165 # that will happen as a result of widening or narrowing a
166 # that will happen as a result of widening or narrowing a
166 # repo. All the changes that this block encounters are ellipsis
167 # repo. All the changes that this block encounters are ellipsis
167 # nodes or flags to kill an existing ellipsis.
168 # nodes or flags to kill an existing ellipsis.
168 chunksignal = changegroup.readexactly(inpart, 4)
169 chunksignal = changegroup.readexactly(inpart, 4)
169 while chunksignal != _DONESIGNAL:
170 while chunksignal != _DONESIGNAL:
170 if chunksignal == _KILLNODESIGNAL:
171 if chunksignal == _KILLNODESIGNAL:
171 # a node used to be an ellipsis but isn't anymore
172 # a node used to be an ellipsis but isn't anymore
172 ck = changegroup.readexactly(inpart, 20)
173 ck = changegroup.readexactly(inpart, 20)
173 if cl.hasnode(ck):
174 if cl.hasnode(ck):
174 clkills.add(ck)
175 clkills.add(ck)
175 else:
176 else:
176 raise error.Abort(
177 raise error.Abort(
177 _('unexpected changespec node chunk type: %s') % chunksignal)
178 _('unexpected changespec node chunk type: %s') % chunksignal)
178 chunksignal = changegroup.readexactly(inpart, 4)
179 chunksignal = changegroup.readexactly(inpart, 4)
179
180
180 if clkills:
181 if clkills:
181 # preserve bookmarks that repair.strip() would otherwise strip
182 # preserve bookmarks that repair.strip() would otherwise strip
182 bmstore = repo._bookmarks
183 op._bookmarksbackup = repo._bookmarks
183 class dummybmstore(dict):
184 class dummybmstore(dict):
184 def applychanges(self, repo, tr, changes):
185 def applychanges(self, repo, tr, changes):
185 pass
186 pass
186 repo._bookmarks = dummybmstore()
187 localrepo.localrepository._bookmarks.set(repo, dummybmstore())
187 chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True,
188 chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True,
188 topic='widen')
189 topic='widen')
189 repo._bookmarks = bmstore
190 if chgrpfile:
190 if chgrpfile:
191 op._widen_uninterr = repo.ui.uninterruptable()
191 op._widen_uninterr = repo.ui.uninterruptable()
192 op._widen_uninterr.__enter__()
192 op._widen_uninterr.__enter__()
193 # presence of _widen_bundle attribute activates widen handler later
193 # presence of _widen_bundle attribute activates widen handler later
194 op._widen_bundle = chgrpfile
194 op._widen_bundle = chgrpfile
195 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
195 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
196 # will currently always be there when using the core+narrowhg server, but
196 # will currently always be there when using the core+narrowhg server, but
197 # other servers may include a changespec part even when not widening (e.g.
197 # other servers may include a changespec part even when not widening (e.g.
198 # because we're deepening a shallow repo).
198 # because we're deepening a shallow repo).
199 if util.safehasattr(repo, 'setnewnarrowpats'):
199 if util.safehasattr(repo, 'setnewnarrowpats'):
200 repo.setnewnarrowpats()
200 repo.setnewnarrowpats()
201
201
202 def handlechangegroup_widen(op, inpart):
202 def handlechangegroup_widen(op, inpart):
203 """Changegroup exchange handler which restores temporarily-stripped nodes"""
203 """Changegroup exchange handler which restores temporarily-stripped nodes"""
204 # We saved a bundle with stripped node data we must now restore.
204 # We saved a bundle with stripped node data we must now restore.
205 # This approach is based on mercurial/repair.py@6ee26a53c111.
205 # This approach is based on mercurial/repair.py@6ee26a53c111.
206 repo = op.repo
206 repo = op.repo
207 ui = op.ui
207 ui = op.ui
208
208
209 chgrpfile = op._widen_bundle
209 chgrpfile = op._widen_bundle
210 del op._widen_bundle
210 del op._widen_bundle
211 vfs = repo.vfs
211 vfs = repo.vfs
212
212
213 ui.note(_("adding branch\n"))
213 ui.note(_("adding branch\n"))
214 f = vfs.open(chgrpfile, "rb")
214 f = vfs.open(chgrpfile, "rb")
215 try:
215 try:
216 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
216 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
217 if not ui.verbose:
217 if not ui.verbose:
218 # silence internal shuffling chatter
218 # silence internal shuffling chatter
219 ui.pushbuffer()
219 ui.pushbuffer()
220 if isinstance(gen, bundle2.unbundle20):
220 if isinstance(gen, bundle2.unbundle20):
221 with repo.transaction('strip') as tr:
221 with repo.transaction('strip') as tr:
222 bundle2.processbundle(repo, gen, lambda: tr)
222 bundle2.processbundle(repo, gen, lambda: tr)
223 else:
223 else:
224 gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True)
224 gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True)
225 if not ui.verbose:
225 if not ui.verbose:
226 ui.popbuffer()
226 ui.popbuffer()
227 finally:
227 finally:
228 f.close()
228 f.close()
229
229
230 # remove undo files
230 # remove undo files
231 for undovfs, undofile in repo.undofiles():
231 for undovfs, undofile in repo.undofiles():
232 try:
232 try:
233 undovfs.unlink(undofile)
233 undovfs.unlink(undofile)
234 except OSError as e:
234 except OSError as e:
235 if e.errno != errno.ENOENT:
235 if e.errno != errno.ENOENT:
236 ui.warn(_('error removing %s: %s\n') %
236 ui.warn(_('error removing %s: %s\n') %
237 (undovfs.join(undofile), stringutil.forcebytestr(e)))
237 (undovfs.join(undofile), stringutil.forcebytestr(e)))
238
238
239 # Remove partial backup only if there were no exceptions
239 # Remove partial backup only if there were no exceptions
240 op._widen_uninterr.__exit__(None, None, None)
240 op._widen_uninterr.__exit__(None, None, None)
241 vfs.unlink(chgrpfile)
241 vfs.unlink(chgrpfile)
242
242
243 def setup():
243 def setup():
244 """Enable narrow repo support in bundle2-related extension points."""
244 """Enable narrow repo support in bundle2-related extension points."""
245 getbundleargs = wireprototypes.GETBUNDLE_ARGUMENTS
245 getbundleargs = wireprototypes.GETBUNDLE_ARGUMENTS
246
246
247 getbundleargs['narrow'] = 'boolean'
247 getbundleargs['narrow'] = 'boolean'
248 getbundleargs['depth'] = 'plain'
248 getbundleargs['depth'] = 'plain'
249 getbundleargs['oldincludepats'] = 'csv'
249 getbundleargs['oldincludepats'] = 'csv'
250 getbundleargs['oldexcludepats'] = 'csv'
250 getbundleargs['oldexcludepats'] = 'csv'
251 getbundleargs['known'] = 'csv'
251 getbundleargs['known'] = 'csv'
252
252
253 # Extend changegroup serving to handle requests from narrow clients.
253 # Extend changegroup serving to handle requests from narrow clients.
254 origcgfn = exchange.getbundle2partsmapping['changegroup']
254 origcgfn = exchange.getbundle2partsmapping['changegroup']
255 def wrappedcgfn(*args, **kwargs):
255 def wrappedcgfn(*args, **kwargs):
256 repo = args[1]
256 repo = args[1]
257 if repo.ui.has_section(_NARROWACL_SECTION):
257 if repo.ui.has_section(_NARROWACL_SECTION):
258 kwargs = exchange.applynarrowacl(repo, kwargs)
258 kwargs = exchange.applynarrowacl(repo, kwargs)
259
259
260 if (kwargs.get(r'narrow', False) and
260 if (kwargs.get(r'narrow', False) and
261 repo.ui.configbool('experimental', 'narrowservebrokenellipses')):
261 repo.ui.configbool('experimental', 'narrowservebrokenellipses')):
262 getbundlechangegrouppart_narrow(*args, **kwargs)
262 getbundlechangegrouppart_narrow(*args, **kwargs)
263 else:
263 else:
264 origcgfn(*args, **kwargs)
264 origcgfn(*args, **kwargs)
265 exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn
265 exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn
266
266
267 # Extend changegroup receiver so client can fixup after widen requests.
267 # Extend changegroup receiver so client can fixup after widen requests.
268 origcghandler = bundle2.parthandlermapping['changegroup']
268 origcghandler = bundle2.parthandlermapping['changegroup']
269 def wrappedcghandler(op, inpart):
269 def wrappedcghandler(op, inpart):
270 origcghandler(op, inpart)
270 origcghandler(op, inpart)
271 if util.safehasattr(op, '_widen_bundle'):
271 if util.safehasattr(op, '_widen_bundle'):
272 handlechangegroup_widen(op, inpart)
272 handlechangegroup_widen(op, inpart)
273 if util.safehasattr(op, '_bookmarksbackup'):
274 localrepo.localrepository._bookmarks.set(op.repo,
275 op._bookmarksbackup)
276 del op._bookmarksbackup
277
273 wrappedcghandler.params = origcghandler.params
278 wrappedcghandler.params = origcghandler.params
274 bundle2.parthandlermapping['changegroup'] = wrappedcghandler
279 bundle2.parthandlermapping['changegroup'] = wrappedcghandler
General Comments 0
You need to be logged in to leave comments. Login now