##// END OF EJS Templates
wireproto: move clonebundles command from extension (issue4931)...
Gregory Szorc -
r26857:e5a1df51 stable
parent child Browse files
Show More
@@ -1,266 +1,254 b''
1 1 # This software may be used and distributed according to the terms of the
2 2 # GNU General Public License version 2 or any later version.
3 3
4 4 """advertise pre-generated bundles to seed clones (experimental)
5 5
6 6 "clonebundles" is a server-side extension used to advertise the existence
7 7 of pre-generated, externally hosted bundle files to clients that are
8 8 cloning so that cloning can be faster, more reliable, and require less
9 9 resources on the server.
10 10
11 11 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
12 12 the server, in response to a client's request to clone, dynamically generates
13 13 a bundle containing the entire repository content and sends it to the client.
14 14 There is no caching on the server and the server will have to redundantly
15 15 generate the same outgoing bundle in response to each clone request. For
16 16 servers with large repositories or with high clone volume, the load from
17 17 clones can make scaling the server challenging and costly.
18 18
19 19 This extension provides server operators the ability to offload potentially
20 20 expensive clone load to an external service. Here's how it works.
21 21
22 22 1. A server operator establishes a mechanism for making bundle files available
23 23 on a hosting service where Mercurial clients can fetch them.
24 24 2. A manifest file listing available bundle URLs and some optional metadata
25 25 is added to the Mercurial repository on the server.
26 26 3. A client initiates a clone against a clone bundles aware server.
27 27 4. The client sees the server is advertising clone bundles and fetches the
28 28 manifest listing available bundles.
29 29 5. The client filters and sorts the available bundles based on what it
30 30 supports and prefers.
31 31 6. The client downloads and applies an available bundle from the
32 32 server-specified URL.
33 33 7. The client reconnects to the original server and performs the equivalent
34 34 of :hg:`pull` to retrieve all repository data not in the bundle. (The
35 35 repository could have been updated between when the bundle was created
36 36 and when the client started the clone.)
37 37
38 38 Instead of the server generating full repository bundles for every clone
39 39 request, it generates full bundles once and they are subsequently reused to
40 40 bootstrap new clones. The server may still transfer data at clone time.
41 41 However, this is only data that has been added/changed since the bundle was
42 42 created. For large, established repositories, this can reduce server load for
43 43 clones to less than 1% of original.
44 44
45 45 To work, this extension requires the following of server operators:
46 46
47 47 * Generating bundle files of repository content (typically periodically,
48 48 such as once per day).
49 49 * A file server that clients have network access to and that Python knows
50 50 how to talk to through its normal URL handling facility (typically a
51 51 HTTP server).
52 52 * A process for keeping the bundles manifest in sync with available bundle
53 53 files.
54 54
55 55 Strictly speaking, using a static file hosting server isn't required: a server
56 56 operator could use a dynamic service for retrieving bundle data. However,
57 57 static file hosting services are simple and scalable and should be sufficient
58 58 for most needs.
59 59
60 60 Bundle files can be generated with the :hg:`bundle` comand. Typically
61 61 :hg:`bundle --all` is used to produce a bundle of the entire repository.
62 62
63 63 :hg:`debugcreatestreamclonebundle` can be used to produce a special
64 64 *streaming clone bundle*. These are bundle files that are extremely efficient
65 65 to produce and consume (read: fast). However, they are larger than
66 66 traditional bundle formats and require that clients support the exact set
67 67 of repository data store formats in use by the repository that created them.
68 68 Typically, a newer server can serve data that is compatible with older clients.
69 69 However, *streaming clone bundles* don't have this guarantee. **Server
70 70 operators need to be aware that newer versions of Mercurial may produce
71 71 streaming clone bundles incompatible with older Mercurial versions.**
72 72
73 73 The list of requirements printed by :hg:`debugcreatestreamclonebundle` should
74 74 be specified in the ``requirements`` parameter of the *bundle specification
75 75 string* for the ``BUNDLESPEC`` manifest property described below. e.g.
76 76 ``BUNDLESPEC=none-packed1;requirements%3Drevlogv1``.
77 77
78 78 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
79 79 file containing the list of available bundle files suitable for seeding
80 80 clones. If this file does not exist, the repository will not advertise the
81 81 existence of clone bundles when clients connect.
82 82
83 83 The manifest file contains a newline (\n) delimited list of entries.
84 84
85 85 Each line in this file defines an available bundle. Lines have the format:
86 86
87 87 <URL> [<key>=<value>[ <key>=<value>]]
88 88
89 89 That is, a URL followed by an optional, space-delimited list of key=value
90 90 pairs describing additional properties of this bundle. Both keys and values
91 91 are URI encoded.
92 92
93 93 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
94 94 All non-uppercase keys can be used by site installations. An example use
95 95 for custom properties is to use the *datacenter* attribute to define which
96 96 data center a file is hosted in. Clients could then prefer a server in the
97 97 data center closest to them.
98 98
99 99 The following reserved keys are currently defined:
100 100
101 101 BUNDLESPEC
102 102 A "bundle specification" string that describes the type of the bundle.
103 103
104 104 These are string values that are accepted by the "--type" argument of
105 105 :hg:`bundle`.
106 106
107 107 The values are parsed in strict mode, which means they must be of the
108 108 "<compression>-<type>" form. See
109 109 mercurial.exchange.parsebundlespec() for more details.
110 110
111 111 Clients will automatically filter out specifications that are unknown or
112 112 unsupported so they won't attempt to download something that likely won't
113 113 apply.
114 114
115 115 The actual value doesn't impact client behavior beyond filtering:
116 116 clients will still sniff the bundle type from the header of downloaded
117 117 files.
118 118
119 119 **Use of this key is highly recommended**, as it allows clients to
120 120 easily skip unsupported bundles.
121 121
122 122 REQUIRESNI
123 123 Whether Server Name Indication (SNI) is required to connect to the URL.
124 124 SNI allows servers to use multiple certificates on the same IP. It is
125 125 somewhat common in CDNs and other hosting providers. Older Python
126 126 versions do not support SNI. Defining this attribute enables clients
127 127 with older Python versions to filter this entry without experiencing
128 128 an opaque SSL failure at connection time.
129 129
130 130 If this is defined, it is important to advertise a non-SNI fallback
131 131 URL or clients running old Python releases may not be able to clone
132 132 with the clonebundles facility.
133 133
134 134 Value should be "true".
135 135
136 136 Manifests can contain multiple entries. Assuming metadata is defined, clients
137 137 will filter entries from the manifest that they don't support. The remaining
138 138 entries are optionally sorted by client preferences
139 139 (``experimental.clonebundleprefers`` config option). The client then attempts
140 140 to fetch the bundle at the first URL in the remaining list.
141 141
142 142 **Errors when downloading a bundle will fail the entire clone operation:
143 143 clients do not automatically fall back to a traditional clone.** The reason
144 144 for this is that if a server is using clone bundles, it is probably doing so
145 145 because the feature is necessary to help it scale. In other words, there
146 146 is an assumption that clone load will be offloaded to another service and
147 147 that the Mercurial server isn't responsible for serving this clone load.
148 148 If that other service experiences issues and clients start mass falling back to
149 149 the original Mercurial server, the added clone load could overwhelm the server
150 150 due to unexpected load and effectively take it offline. Not having clients
151 151 automatically fall back to cloning from the original server mitigates this
152 152 scenario.
153 153
154 154 Because there is no automatic Mercurial server fallback on failure of the
155 155 bundle hosting service, it is important for server operators to view the bundle
156 156 hosting service as an extension of the Mercurial server in terms of
157 157 availability and service level agreements: if the bundle hosting service goes
158 158 down, so does the ability for clients to clone. Note: clients will see a
159 159 message informing them how to bypass the clone bundles facility when a failure
160 160 occurs. So server operators should prepare for some people to follow these
161 161 instructions when a failure occurs, thus driving more load to the original
162 162 Mercurial server when the bundle hosting service fails.
163 163
164 164 The following config options influence the behavior of the clone bundles
165 165 feature:
166 166
167 167 ui.clonebundleadvertise
168 168 Whether the server advertises the existence of the clone bundles feature
169 169 to compatible clients that aren't using it.
170 170
171 171 When this is enabled (the default), a server will send a message to
172 172 compatible clients performing a traditional clone informing them of the
173 173 available clone bundles feature. Compatible clients are those that support
174 174 bundle2 and are advertising support for the clone bundles feature.
175 175
176 176 ui.clonebundlefallback
177 177 Whether to automatically fall back to a traditional clone in case of
178 178 clone bundles failure. Defaults to false for reasons described above.
179 179
180 180 experimental.clonebundles
181 181 Whether the clone bundles feature is enabled on clients. Defaults to true.
182 182
183 183 experimental.clonebundleprefers
184 184 List of "key=value" properties the client prefers in bundles. Downloaded
185 185 bundle manifests will be sorted by the preferences in this list. e.g.
186 186 the value "BUNDLESPEC=gzip-v1, BUNDLESPEC=bzip2=v1" will prefer a gzipped
187 187 version 1 bundle type then bzip2 version 1 bundle type.
188 188
189 189 If not defined, the order in the manifest will be used and the first
190 190 available bundle will be downloaded.
191 191 """
192 192
193 193 from mercurial.i18n import _
194 194 from mercurial.node import nullid
195 195 from mercurial import (
196 196 exchange,
197 197 extensions,
198 198 wireproto,
199 199 )
200 200
201 201 testedwith = 'internal'
202 202
203 203 def capabilities(orig, repo, proto):
204 204 caps = orig(repo, proto)
205 205
206 206 # Only advertise if a manifest exists. This does add some I/O to requests.
207 207 # But this should be cheaper than a wasted network round trip due to
208 208 # missing file.
209 209 if repo.opener.exists('clonebundles.manifest'):
210 210 caps.append('clonebundles')
211 211
212 212 return caps
213 213
214 @wireproto.wireprotocommand('clonebundles', '')
215 def bundles(repo, proto):
216 """Server command for returning info for available bundles to seed clones.
217
218 Clients will parse this response and determine what bundle to fetch.
219
220 Other extensions may wrap this command to filter or dynamically emit
221 data depending on the request. e.g. you could advertise URLs for
222 the closest data center given the client's IP address.
223 """
224 return repo.opener.tryread('clonebundles.manifest')
225
226 214 @exchange.getbundle2partsgenerator('clonebundlesadvertise', 0)
227 215 def advertiseclonebundlespart(bundler, repo, source, bundlecaps=None,
228 216 b2caps=None, heads=None, common=None,
229 217 cbattempted=None, **kwargs):
230 218 """Inserts an output part to advertise clone bundles availability."""
231 219 # Allow server operators to disable this behavior.
232 220 # # experimental config: ui.clonebundleadvertise
233 221 if not repo.ui.configbool('ui', 'clonebundleadvertise', True):
234 222 return
235 223
236 224 # Only advertise if a manifest is present.
237 225 if not repo.opener.exists('clonebundles.manifest'):
238 226 return
239 227
240 228 # And when changegroup data is requested.
241 229 if not kwargs.get('cg', True):
242 230 return
243 231
244 232 # And when the client supports clone bundles.
245 233 if cbattempted is None:
246 234 return
247 235
248 236 # And when the client didn't attempt a clone bundle as part of this pull.
249 237 if cbattempted:
250 238 return
251 239
252 240 # And when a full clone is requested.
253 241 # Note: client should not send "cbattempted" for regular pulls. This check
254 242 # is defense in depth.
255 243 if common and common != [nullid]:
256 244 return
257 245
258 246 msg = _('this server supports the experimental "clone bundles" feature '
259 247 'that should enable faster and more reliable cloning\n'
260 248 'help test it by setting the "experimental.clonebundles" config '
261 249 'flag to "true"')
262 250
263 251 bundler.newpart('output', data=msg)
264 252
265 253 def extsetup(ui):
266 254 extensions.wrapfunction(wireproto, '_capabilities', capabilities)
@@ -1,816 +1,827 b''
1 1 # wireproto.py - generic wire protocol support functions
2 2 #
3 3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11 import sys
12 12 import tempfile
13 13 import urllib
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 bin,
18 18 hex,
19 19 )
20 20
21 21 from . import (
22 22 bundle2,
23 23 changegroup as changegroupmod,
24 24 encoding,
25 25 error,
26 26 exchange,
27 27 peer,
28 28 pushkey as pushkeymod,
29 29 streamclone,
30 30 util,
31 31 )
32 32
33 33 class abstractserverproto(object):
34 34 """abstract class that summarizes the protocol API
35 35
36 36 Used as reference and documentation.
37 37 """
38 38
39 39 def getargs(self, args):
40 40 """return the value for arguments in <args>
41 41
42 42 returns a list of values (same order as <args>)"""
43 43 raise NotImplementedError()
44 44
45 45 def getfile(self, fp):
46 46 """write the whole content of a file into a file like object
47 47
48 48 The file is in the form::
49 49
50 50 (<chunk-size>\n<chunk>)+0\n
51 51
52 52 chunk size is the ascii version of the int.
53 53 """
54 54 raise NotImplementedError()
55 55
56 56 def redirect(self):
57 57 """may setup interception for stdout and stderr
58 58
59 59 See also the `restore` method."""
60 60 raise NotImplementedError()
61 61
62 62 # If the `redirect` function does install interception, the `restore`
63 63 # function MUST be defined. If interception is not used, this function
64 64 # MUST NOT be defined.
65 65 #
66 66 # left commented here on purpose
67 67 #
68 68 #def restore(self):
69 69 # """reinstall previous stdout and stderr and return intercepted stdout
70 70 # """
71 71 # raise NotImplementedError()
72 72
73 73 def groupchunks(self, cg):
74 74 """return 4096 chunks from a changegroup object
75 75
76 76 Some protocols may have compressed the contents."""
77 77 raise NotImplementedError()
78 78
79 79 class remotebatch(peer.batcher):
80 80 '''batches the queued calls; uses as few roundtrips as possible'''
81 81 def __init__(self, remote):
82 82 '''remote must support _submitbatch(encbatch) and
83 83 _submitone(op, encargs)'''
84 84 peer.batcher.__init__(self)
85 85 self.remote = remote
86 86 def submit(self):
87 87 req, rsp = [], []
88 88 for name, args, opts, resref in self.calls:
89 89 mtd = getattr(self.remote, name)
90 90 batchablefn = getattr(mtd, 'batchable', None)
91 91 if batchablefn is not None:
92 92 batchable = batchablefn(mtd.im_self, *args, **opts)
93 93 encargsorres, encresref = batchable.next()
94 94 if encresref:
95 95 req.append((name, encargsorres,))
96 96 rsp.append((batchable, encresref, resref,))
97 97 else:
98 98 resref.set(encargsorres)
99 99 else:
100 100 if req:
101 101 self._submitreq(req, rsp)
102 102 req, rsp = [], []
103 103 resref.set(mtd(*args, **opts))
104 104 if req:
105 105 self._submitreq(req, rsp)
106 106 def _submitreq(self, req, rsp):
107 107 encresults = self.remote._submitbatch(req)
108 108 for encres, r in zip(encresults, rsp):
109 109 batchable, encresref, resref = r
110 110 encresref.set(encres)
111 111 resref.set(batchable.next())
112 112
113 113 # Forward a couple of names from peer to make wireproto interactions
114 114 # slightly more sensible.
115 115 batchable = peer.batchable
116 116 future = peer.future
117 117
118 118 # list of nodes encoding / decoding
119 119
120 120 def decodelist(l, sep=' '):
121 121 if l:
122 122 return map(bin, l.split(sep))
123 123 return []
124 124
125 125 def encodelist(l, sep=' '):
126 126 try:
127 127 return sep.join(map(hex, l))
128 128 except TypeError:
129 129 raise
130 130
131 131 # batched call argument encoding
132 132
133 133 def escapearg(plain):
134 134 return (plain
135 135 .replace(':', ':c')
136 136 .replace(',', ':o')
137 137 .replace(';', ':s')
138 138 .replace('=', ':e'))
139 139
140 140 def unescapearg(escaped):
141 141 return (escaped
142 142 .replace(':e', '=')
143 143 .replace(':s', ';')
144 144 .replace(':o', ',')
145 145 .replace(':c', ':'))
146 146
147 147 # mapping of options accepted by getbundle and their types
148 148 #
149 149 # Meant to be extended by extensions. It is extensions responsibility to ensure
150 150 # such options are properly processed in exchange.getbundle.
151 151 #
152 152 # supported types are:
153 153 #
154 154 # :nodes: list of binary nodes
155 155 # :csv: list of comma-separated values
156 156 # :scsv: list of comma-separated values return as set
157 157 # :plain: string with no transformation needed.
158 158 gboptsmap = {'heads': 'nodes',
159 159 'common': 'nodes',
160 160 'obsmarkers': 'boolean',
161 161 'bundlecaps': 'scsv',
162 162 'listkeys': 'csv',
163 163 'cg': 'boolean',
164 164 'cbattempted': 'boolean'}
165 165
166 166 # client side
167 167
168 168 class wirepeer(peer.peerrepository):
169 169
170 170 def batch(self):
171 171 if self.capable('batch'):
172 172 return remotebatch(self)
173 173 else:
174 174 return peer.localbatch(self)
175 175 def _submitbatch(self, req):
176 176 cmds = []
177 177 for op, argsdict in req:
178 178 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
179 179 for k, v in argsdict.iteritems())
180 180 cmds.append('%s %s' % (op, args))
181 181 rsp = self._call("batch", cmds=';'.join(cmds))
182 182 return [unescapearg(r) for r in rsp.split(';')]
183 183 def _submitone(self, op, args):
184 184 return self._call(op, **args)
185 185
186 186 @batchable
187 187 def lookup(self, key):
188 188 self.requirecap('lookup', _('look up remote revision'))
189 189 f = future()
190 190 yield {'key': encoding.fromlocal(key)}, f
191 191 d = f.value
192 192 success, data = d[:-1].split(" ", 1)
193 193 if int(success):
194 194 yield bin(data)
195 195 self._abort(error.RepoError(data))
196 196
197 197 @batchable
198 198 def heads(self):
199 199 f = future()
200 200 yield {}, f
201 201 d = f.value
202 202 try:
203 203 yield decodelist(d[:-1])
204 204 except ValueError:
205 205 self._abort(error.ResponseError(_("unexpected response:"), d))
206 206
207 207 @batchable
208 208 def known(self, nodes):
209 209 f = future()
210 210 yield {'nodes': encodelist(nodes)}, f
211 211 d = f.value
212 212 try:
213 213 yield [bool(int(b)) for b in d]
214 214 except ValueError:
215 215 self._abort(error.ResponseError(_("unexpected response:"), d))
216 216
217 217 @batchable
218 218 def branchmap(self):
219 219 f = future()
220 220 yield {}, f
221 221 d = f.value
222 222 try:
223 223 branchmap = {}
224 224 for branchpart in d.splitlines():
225 225 branchname, branchheads = branchpart.split(' ', 1)
226 226 branchname = encoding.tolocal(urllib.unquote(branchname))
227 227 branchheads = decodelist(branchheads)
228 228 branchmap[branchname] = branchheads
229 229 yield branchmap
230 230 except TypeError:
231 231 self._abort(error.ResponseError(_("unexpected response:"), d))
232 232
233 233 def branches(self, nodes):
234 234 n = encodelist(nodes)
235 235 d = self._call("branches", nodes=n)
236 236 try:
237 237 br = [tuple(decodelist(b)) for b in d.splitlines()]
238 238 return br
239 239 except ValueError:
240 240 self._abort(error.ResponseError(_("unexpected response:"), d))
241 241
242 242 def between(self, pairs):
243 243 batch = 8 # avoid giant requests
244 244 r = []
245 245 for i in xrange(0, len(pairs), batch):
246 246 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
247 247 d = self._call("between", pairs=n)
248 248 try:
249 249 r.extend(l and decodelist(l) or [] for l in d.splitlines())
250 250 except ValueError:
251 251 self._abort(error.ResponseError(_("unexpected response:"), d))
252 252 return r
253 253
254 254 @batchable
255 255 def pushkey(self, namespace, key, old, new):
256 256 if not self.capable('pushkey'):
257 257 yield False, None
258 258 f = future()
259 259 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
260 260 yield {'namespace': encoding.fromlocal(namespace),
261 261 'key': encoding.fromlocal(key),
262 262 'old': encoding.fromlocal(old),
263 263 'new': encoding.fromlocal(new)}, f
264 264 d = f.value
265 265 d, output = d.split('\n', 1)
266 266 try:
267 267 d = bool(int(d))
268 268 except ValueError:
269 269 raise error.ResponseError(
270 270 _('push failed (unexpected response):'), d)
271 271 for l in output.splitlines(True):
272 272 self.ui.status(_('remote: '), l)
273 273 yield d
274 274
275 275 @batchable
276 276 def listkeys(self, namespace):
277 277 if not self.capable('pushkey'):
278 278 yield {}, None
279 279 f = future()
280 280 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
281 281 yield {'namespace': encoding.fromlocal(namespace)}, f
282 282 d = f.value
283 283 self.ui.debug('received listkey for "%s": %i bytes\n'
284 284 % (namespace, len(d)))
285 285 yield pushkeymod.decodekeys(d)
286 286
287 287 def stream_out(self):
288 288 return self._callstream('stream_out')
289 289
290 290 def changegroup(self, nodes, kind):
291 291 n = encodelist(nodes)
292 292 f = self._callcompressable("changegroup", roots=n)
293 293 return changegroupmod.cg1unpacker(f, 'UN')
294 294
295 295 def changegroupsubset(self, bases, heads, kind):
296 296 self.requirecap('changegroupsubset', _('look up remote changes'))
297 297 bases = encodelist(bases)
298 298 heads = encodelist(heads)
299 299 f = self._callcompressable("changegroupsubset",
300 300 bases=bases, heads=heads)
301 301 return changegroupmod.cg1unpacker(f, 'UN')
302 302
303 303 def getbundle(self, source, **kwargs):
304 304 self.requirecap('getbundle', _('look up remote changes'))
305 305 opts = {}
306 306 bundlecaps = kwargs.get('bundlecaps')
307 307 if bundlecaps is not None:
308 308 kwargs['bundlecaps'] = sorted(bundlecaps)
309 309 else:
310 310 bundlecaps = () # kwargs could have it to None
311 311 for key, value in kwargs.iteritems():
312 312 if value is None:
313 313 continue
314 314 keytype = gboptsmap.get(key)
315 315 if keytype is None:
316 316 assert False, 'unexpected'
317 317 elif keytype == 'nodes':
318 318 value = encodelist(value)
319 319 elif keytype in ('csv', 'scsv'):
320 320 value = ','.join(value)
321 321 elif keytype == 'boolean':
322 322 value = '%i' % bool(value)
323 323 elif keytype != 'plain':
324 324 raise KeyError('unknown getbundle option type %s'
325 325 % keytype)
326 326 opts[key] = value
327 327 f = self._callcompressable("getbundle", **opts)
328 328 if any((cap.startswith('HG2') for cap in bundlecaps)):
329 329 return bundle2.getunbundler(self.ui, f)
330 330 else:
331 331 return changegroupmod.cg1unpacker(f, 'UN')
332 332
333 333 def unbundle(self, cg, heads, source):
334 334 '''Send cg (a readable file-like object representing the
335 335 changegroup to push, typically a chunkbuffer object) to the
336 336 remote server as a bundle.
337 337
338 338 When pushing a bundle10 stream, return an integer indicating the
339 339 result of the push (see localrepository.addchangegroup()).
340 340
341 341 When pushing a bundle20 stream, return a bundle20 stream.'''
342 342
343 343 if heads != ['force'] and self.capable('unbundlehash'):
344 344 heads = encodelist(['hashed',
345 345 util.sha1(''.join(sorted(heads))).digest()])
346 346 else:
347 347 heads = encodelist(heads)
348 348
349 349 if util.safehasattr(cg, 'deltaheader'):
350 350 # this a bundle10, do the old style call sequence
351 351 ret, output = self._callpush("unbundle", cg, heads=heads)
352 352 if ret == "":
353 353 raise error.ResponseError(
354 354 _('push failed:'), output)
355 355 try:
356 356 ret = int(ret)
357 357 except ValueError:
358 358 raise error.ResponseError(
359 359 _('push failed (unexpected response):'), ret)
360 360
361 361 for l in output.splitlines(True):
362 362 self.ui.status(_('remote: '), l)
363 363 else:
364 364 # bundle2 push. Send a stream, fetch a stream.
365 365 stream = self._calltwowaystream('unbundle', cg, heads=heads)
366 366 ret = bundle2.getunbundler(self.ui, stream)
367 367 return ret
368 368
369 369 def debugwireargs(self, one, two, three=None, four=None, five=None):
370 370 # don't pass optional arguments left at their default value
371 371 opts = {}
372 372 if three is not None:
373 373 opts['three'] = three
374 374 if four is not None:
375 375 opts['four'] = four
376 376 return self._call('debugwireargs', one=one, two=two, **opts)
377 377
378 378 def _call(self, cmd, **args):
379 379 """execute <cmd> on the server
380 380
381 381 The command is expected to return a simple string.
382 382
383 383 returns the server reply as a string."""
384 384 raise NotImplementedError()
385 385
386 386 def _callstream(self, cmd, **args):
387 387 """execute <cmd> on the server
388 388
389 389 The command is expected to return a stream.
390 390
391 391 returns the server reply as a file like object."""
392 392 raise NotImplementedError()
393 393
394 394 def _callcompressable(self, cmd, **args):
395 395 """execute <cmd> on the server
396 396
397 397 The command is expected to return a stream.
398 398
399 399 The stream may have been compressed in some implementations. This
400 400 function takes care of the decompression. This is the only difference
401 401 with _callstream.
402 402
403 403 returns the server reply as a file like object.
404 404 """
405 405 raise NotImplementedError()
406 406
407 407 def _callpush(self, cmd, fp, **args):
408 408 """execute a <cmd> on server
409 409
410 410 The command is expected to be related to a push. Push has a special
411 411 return method.
412 412
413 413 returns the server reply as a (ret, output) tuple. ret is either
414 414 empty (error) or a stringified int.
415 415 """
416 416 raise NotImplementedError()
417 417
418 418 def _calltwowaystream(self, cmd, fp, **args):
419 419 """execute <cmd> on server
420 420
421 421 The command will send a stream to the server and get a stream in reply.
422 422 """
423 423 raise NotImplementedError()
424 424
425 425 def _abort(self, exception):
426 426 """clearly abort the wire protocol connection and raise the exception
427 427 """
428 428 raise NotImplementedError()
429 429
430 430 # server side
431 431
432 432 # wire protocol command can either return a string or one of these classes.
433 433 class streamres(object):
434 434 """wireproto reply: binary stream
435 435
436 436 The call was successful and the result is a stream.
437 437 Iterate on the `self.gen` attribute to retrieve chunks.
438 438 """
439 439 def __init__(self, gen):
440 440 self.gen = gen
441 441
442 442 class pushres(object):
443 443 """wireproto reply: success with simple integer return
444 444
445 445 The call was successful and returned an integer contained in `self.res`.
446 446 """
447 447 def __init__(self, res):
448 448 self.res = res
449 449
450 450 class pusherr(object):
451 451 """wireproto reply: failure
452 452
453 453 The call failed. The `self.res` attribute contains the error message.
454 454 """
455 455 def __init__(self, res):
456 456 self.res = res
457 457
458 458 class ooberror(object):
459 459 """wireproto reply: failure of a batch of operation
460 460
461 461 Something failed during a batch call. The error message is stored in
462 462 `self.message`.
463 463 """
464 464 def __init__(self, message):
465 465 self.message = message
466 466
467 467 def dispatch(repo, proto, command):
468 468 repo = repo.filtered("served")
469 469 func, spec = commands[command]
470 470 args = proto.getargs(spec)
471 471 return func(repo, proto, *args)
472 472
473 473 def options(cmd, keys, others):
474 474 opts = {}
475 475 for k in keys:
476 476 if k in others:
477 477 opts[k] = others[k]
478 478 del others[k]
479 479 if others:
480 480 sys.stderr.write("warning: %s ignored unexpected arguments %s\n"
481 481 % (cmd, ",".join(others)))
482 482 return opts
483 483
484 484 # list of commands
485 485 commands = {}
486 486
487 487 def wireprotocommand(name, args=''):
488 488 """decorator for wire protocol command"""
489 489 def register(func):
490 490 commands[name] = (func, args)
491 491 return func
492 492 return register
493 493
494 494 @wireprotocommand('batch', 'cmds *')
495 495 def batch(repo, proto, cmds, others):
496 496 repo = repo.filtered("served")
497 497 res = []
498 498 for pair in cmds.split(';'):
499 499 op, args = pair.split(' ', 1)
500 500 vals = {}
501 501 for a in args.split(','):
502 502 if a:
503 503 n, v = a.split('=')
504 504 vals[n] = unescapearg(v)
505 505 func, spec = commands[op]
506 506 if spec:
507 507 keys = spec.split()
508 508 data = {}
509 509 for k in keys:
510 510 if k == '*':
511 511 star = {}
512 512 for key in vals.keys():
513 513 if key not in keys:
514 514 star[key] = vals[key]
515 515 data['*'] = star
516 516 else:
517 517 data[k] = vals[k]
518 518 result = func(repo, proto, *[data[k] for k in keys])
519 519 else:
520 520 result = func(repo, proto)
521 521 if isinstance(result, ooberror):
522 522 return result
523 523 res.append(escapearg(result))
524 524 return ';'.join(res)
525 525
526 526 @wireprotocommand('between', 'pairs')
527 527 def between(repo, proto, pairs):
528 528 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
529 529 r = []
530 530 for b in repo.between(pairs):
531 531 r.append(encodelist(b) + "\n")
532 532 return "".join(r)
533 533
534 534 @wireprotocommand('branchmap')
535 535 def branchmap(repo, proto):
536 536 branchmap = repo.branchmap()
537 537 heads = []
538 538 for branch, nodes in branchmap.iteritems():
539 539 branchname = urllib.quote(encoding.fromlocal(branch))
540 540 branchnodes = encodelist(nodes)
541 541 heads.append('%s %s' % (branchname, branchnodes))
542 542 return '\n'.join(heads)
543 543
544 544 @wireprotocommand('branches', 'nodes')
545 545 def branches(repo, proto, nodes):
546 546 nodes = decodelist(nodes)
547 547 r = []
548 548 for b in repo.branches(nodes):
549 549 r.append(encodelist(b) + "\n")
550 550 return "".join(r)
551 551
552 @wireprotocommand('clonebundles', '')
553 def clonebundles(repo, proto):
554 """Server command for returning info for available bundles to seed clones.
555
556 Clients will parse this response and determine what bundle to fetch.
557
558 Extensions may wrap this command to filter or dynamically emit data
559 depending on the request. e.g. you could advertise URLs for the closest
560 data center given the client's IP address.
561 """
562 return repo.opener.tryread('clonebundles.manifest')
552 563
553 564 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
554 565 'known', 'getbundle', 'unbundlehash', 'batch']
555 566
556 567 def _capabilities(repo, proto):
557 568 """return a list of capabilities for a repo
558 569
559 570 This function exists to allow extensions to easily wrap capabilities
560 571 computation
561 572
562 573 - returns a lists: easy to alter
563 574 - change done here will be propagated to both `capabilities` and `hello`
564 575 command without any other action needed.
565 576 """
566 577 # copy to prevent modification of the global list
567 578 caps = list(wireprotocaps)
568 579 if streamclone.allowservergeneration(repo.ui):
569 580 if repo.ui.configbool('server', 'preferuncompressed', False):
570 581 caps.append('stream-preferred')
571 582 requiredformats = repo.requirements & repo.supportedformats
572 583 # if our local revlogs are just revlogv1, add 'stream' cap
573 584 if not requiredformats - set(('revlogv1',)):
574 585 caps.append('stream')
575 586 # otherwise, add 'streamreqs' detailing our local revlog format
576 587 else:
577 588 caps.append('streamreqs=%s' % ','.join(requiredformats))
578 589 if repo.ui.configbool('experimental', 'bundle2-advertise', True):
579 590 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
580 591 caps.append('bundle2=' + urllib.quote(capsblob))
581 592 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
582 593 caps.append(
583 594 'httpheader=%d' % repo.ui.configint('server', 'maxhttpheaderlen', 1024))
584 595 return caps
585 596
586 597 # If you are writing an extension and consider wrapping this function. Wrap
587 598 # `_capabilities` instead.
588 599 @wireprotocommand('capabilities')
589 600 def capabilities(repo, proto):
590 601 return ' '.join(_capabilities(repo, proto))
591 602
592 603 @wireprotocommand('changegroup', 'roots')
593 604 def changegroup(repo, proto, roots):
594 605 nodes = decodelist(roots)
595 606 cg = changegroupmod.changegroup(repo, nodes, 'serve')
596 607 return streamres(proto.groupchunks(cg))
597 608
598 609 @wireprotocommand('changegroupsubset', 'bases heads')
599 610 def changegroupsubset(repo, proto, bases, heads):
600 611 bases = decodelist(bases)
601 612 heads = decodelist(heads)
602 613 cg = changegroupmod.changegroupsubset(repo, bases, heads, 'serve')
603 614 return streamres(proto.groupchunks(cg))
604 615
605 616 @wireprotocommand('debugwireargs', 'one two *')
606 617 def debugwireargs(repo, proto, one, two, others):
607 618 # only accept optional args from the known set
608 619 opts = options('debugwireargs', ['three', 'four'], others)
609 620 return repo.debugwireargs(one, two, **opts)
610 621
611 622 # List of options accepted by getbundle.
612 623 #
613 624 # Meant to be extended by extensions. It is the extension's responsibility to
614 625 # ensure such options are properly processed in exchange.getbundle.
615 626 gboptslist = ['heads', 'common', 'bundlecaps']
616 627
617 628 @wireprotocommand('getbundle', '*')
618 629 def getbundle(repo, proto, others):
619 630 opts = options('getbundle', gboptsmap.keys(), others)
620 631 for k, v in opts.iteritems():
621 632 keytype = gboptsmap[k]
622 633 if keytype == 'nodes':
623 634 opts[k] = decodelist(v)
624 635 elif keytype == 'csv':
625 636 opts[k] = list(v.split(','))
626 637 elif keytype == 'scsv':
627 638 opts[k] = set(v.split(','))
628 639 elif keytype == 'boolean':
629 640 # Client should serialize False as '0', which is a non-empty string
630 641 # so it evaluates as a True bool.
631 642 if v == '0':
632 643 opts[k] = False
633 644 else:
634 645 opts[k] = bool(v)
635 646 elif keytype != 'plain':
636 647 raise KeyError('unknown getbundle option type %s'
637 648 % keytype)
638 649 cg = exchange.getbundle(repo, 'serve', **opts)
639 650 return streamres(proto.groupchunks(cg))
640 651
641 652 @wireprotocommand('heads')
642 653 def heads(repo, proto):
643 654 h = repo.heads()
644 655 return encodelist(h) + "\n"
645 656
646 657 @wireprotocommand('hello')
647 658 def hello(repo, proto):
648 659 '''the hello command returns a set of lines describing various
649 660 interesting things about the server, in an RFC822-like format.
650 661 Currently the only one defined is "capabilities", which
651 662 consists of a line in the form:
652 663
653 664 capabilities: space separated list of tokens
654 665 '''
655 666 return "capabilities: %s\n" % (capabilities(repo, proto))
656 667
657 668 @wireprotocommand('listkeys', 'namespace')
658 669 def listkeys(repo, proto, namespace):
659 670 d = repo.listkeys(encoding.tolocal(namespace)).items()
660 671 return pushkeymod.encodekeys(d)
661 672
662 673 @wireprotocommand('lookup', 'key')
663 674 def lookup(repo, proto, key):
664 675 try:
665 676 k = encoding.tolocal(key)
666 677 c = repo[k]
667 678 r = c.hex()
668 679 success = 1
669 680 except Exception as inst:
670 681 r = str(inst)
671 682 success = 0
672 683 return "%s %s\n" % (success, r)
673 684
674 685 @wireprotocommand('known', 'nodes *')
675 686 def known(repo, proto, nodes, others):
676 687 return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes)))
677 688
678 689 @wireprotocommand('pushkey', 'namespace key old new')
679 690 def pushkey(repo, proto, namespace, key, old, new):
680 691 # compatibility with pre-1.8 clients which were accidentally
681 692 # sending raw binary nodes rather than utf-8-encoded hex
682 693 if len(new) == 20 and new.encode('string-escape') != new:
683 694 # looks like it could be a binary node
684 695 try:
685 696 new.decode('utf-8')
686 697 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
687 698 except UnicodeDecodeError:
688 699 pass # binary, leave unmodified
689 700 else:
690 701 new = encoding.tolocal(new) # normal path
691 702
692 703 if util.safehasattr(proto, 'restore'):
693 704
694 705 proto.redirect()
695 706
696 707 try:
697 708 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
698 709 encoding.tolocal(old), new) or False
699 710 except error.Abort:
700 711 r = False
701 712
702 713 output = proto.restore()
703 714
704 715 return '%s\n%s' % (int(r), output)
705 716
706 717 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
707 718 encoding.tolocal(old), new)
708 719 return '%s\n' % int(r)
709 720
710 721 @wireprotocommand('stream_out')
711 722 def stream(repo, proto):
712 723 '''If the server supports streaming clone, it advertises the "stream"
713 724 capability with a value representing the version and flags of the repo
714 725 it is serving. Client checks to see if it understands the format.
715 726 '''
716 727 if not streamclone.allowservergeneration(repo.ui):
717 728 return '1\n'
718 729
719 730 def getstream(it):
720 731 yield '0\n'
721 732 for chunk in it:
722 733 yield chunk
723 734
724 735 try:
725 736 # LockError may be raised before the first result is yielded. Don't
726 737 # emit output until we're sure we got the lock successfully.
727 738 it = streamclone.generatev1wireproto(repo)
728 739 return streamres(getstream(it))
729 740 except error.LockError:
730 741 return '2\n'
731 742
732 743 @wireprotocommand('unbundle', 'heads')
733 744 def unbundle(repo, proto, heads):
734 745 their_heads = decodelist(heads)
735 746
736 747 try:
737 748 proto.redirect()
738 749
739 750 exchange.check_heads(repo, their_heads, 'preparing changes')
740 751
741 752 # write bundle data to temporary file because it can be big
742 753 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
743 754 fp = os.fdopen(fd, 'wb+')
744 755 r = 0
745 756 try:
746 757 proto.getfile(fp)
747 758 fp.seek(0)
748 759 gen = exchange.readbundle(repo.ui, fp, None)
749 760 r = exchange.unbundle(repo, gen, their_heads, 'serve',
750 761 proto._client())
751 762 if util.safehasattr(r, 'addpart'):
752 763 # The return looks streamable, we are in the bundle2 case and
753 764 # should return a stream.
754 765 return streamres(r.getchunks())
755 766 return pushres(r)
756 767
757 768 finally:
758 769 fp.close()
759 770 os.unlink(tempname)
760 771
761 772 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
762 773 # handle non-bundle2 case first
763 774 if not getattr(exc, 'duringunbundle2', False):
764 775 try:
765 776 raise
766 777 except error.Abort:
767 778 # The old code we moved used sys.stderr directly.
768 779 # We did not change it to minimise code change.
769 780 # This need to be moved to something proper.
770 781 # Feel free to do it.
771 782 sys.stderr.write("abort: %s\n" % exc)
772 783 return pushres(0)
773 784 except error.PushRaced:
774 785 return pusherr(str(exc))
775 786
776 787 bundler = bundle2.bundle20(repo.ui)
777 788 for out in getattr(exc, '_bundle2salvagedoutput', ()):
778 789 bundler.addpart(out)
779 790 try:
780 791 try:
781 792 raise
782 793 except error.PushkeyFailed as exc:
783 794 # check client caps
784 795 remotecaps = getattr(exc, '_replycaps', None)
785 796 if (remotecaps is not None
786 797 and 'pushkey' not in remotecaps.get('error', ())):
787 798 # no support remote side, fallback to Abort handler.
788 799 raise
789 800 part = bundler.newpart('error:pushkey')
790 801 part.addparam('in-reply-to', exc.partid)
791 802 if exc.namespace is not None:
792 803 part.addparam('namespace', exc.namespace, mandatory=False)
793 804 if exc.key is not None:
794 805 part.addparam('key', exc.key, mandatory=False)
795 806 if exc.new is not None:
796 807 part.addparam('new', exc.new, mandatory=False)
797 808 if exc.old is not None:
798 809 part.addparam('old', exc.old, mandatory=False)
799 810 if exc.ret is not None:
800 811 part.addparam('ret', exc.ret, mandatory=False)
801 812 except error.BundleValueError as exc:
802 813 errpart = bundler.newpart('error:unsupportedcontent')
803 814 if exc.parttype is not None:
804 815 errpart.addparam('parttype', exc.parttype)
805 816 if exc.params:
806 817 errpart.addparam('params', '\0'.join(exc.params))
807 818 except error.Abort as exc:
808 819 manargs = [('message', str(exc))]
809 820 advargs = []
810 821 if exc.hint is not None:
811 822 advargs.append(('hint', exc.hint))
812 823 bundler.addpart(bundle2.bundlepart('error:abort',
813 824 manargs, advargs))
814 825 except error.PushRaced as exc:
815 826 bundler.newpart('error:pushraced', [('message', str(exc))])
816 827 return streamres(bundler.getchunks())
@@ -1,450 +1,462 b''
1 1 Set up a server
2 2
3 3 $ hg init server
4 4 $ cd server
5 5 $ cat >> .hg/hgrc << EOF
6 6 > [extensions]
7 7 > clonebundles =
8 8 > EOF
9 9
10 10 $ touch foo
11 11 $ hg -q commit -A -m 'add foo'
12 12 $ touch bar
13 13 $ hg -q commit -A -m 'add bar'
14 14
15 15 $ hg serve -d -p $HGPORT --pid-file hg.pid --accesslog access.log
16 16 $ cat hg.pid >> $DAEMON_PIDS
17 17 $ cd ..
18 18
19 19 Feature disabled by default
20 20 (client should not request manifest)
21 21
22 22 $ hg clone -U http://localhost:$HGPORT feature-disabled
23 23 requesting all changes
24 24 adding changesets
25 25 adding manifests
26 26 adding file changes
27 27 added 2 changesets with 2 changes to 2 files
28 28
29 29 $ cat server/access.log
30 30 * - - [*] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
31 31 * - - [*] "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D (glob)
32 32 * - - [*] "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bundlecaps=HG20%2Cbundle2%3DHG20%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps&cg=1&common=0000000000000000000000000000000000000000&heads=aaff8d2ffbbf07a46dd1f05d8ae7877e3f56e2a2&listkeys=phase%2Cbookmarks (glob)
33 33 * - - [*] "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases (glob)
34 34
35 35 $ cat >> $HGRCPATH << EOF
36 36 > [experimental]
37 37 > clonebundles = true
38 38 > EOF
39 39
40 40 Missing manifest should not result in server lookup
41 41
42 42 $ hg --verbose clone -U http://localhost:$HGPORT no-manifest
43 43 requesting all changes
44 44 adding changesets
45 45 adding manifests
46 46 adding file changes
47 47 added 2 changesets with 2 changes to 2 files
48 48
49 49 $ tail -4 server/access.log
50 50 * - - [*] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
51 51 * - - [*] "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D (glob)
52 52 * - - [*] "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bundlecaps=HG20%2Cbundle2%3DHG20%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps&cg=1&common=0000000000000000000000000000000000000000&heads=aaff8d2ffbbf07a46dd1f05d8ae7877e3f56e2a2&listkeys=phase%2Cbookmarks (glob)
53 53 * - - [*] "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases (glob)
54 54
55 55 Empty manifest file results in retrieval
56 56 (the extension only checks if the manifest file exists)
57 57
58 58 $ touch server/.hg/clonebundles.manifest
59 59 $ hg --verbose clone -U http://localhost:$HGPORT empty-manifest
60 60 no clone bundles available on remote; falling back to regular clone
61 61 requesting all changes
62 62 adding changesets
63 63 adding manifests
64 64 adding file changes
65 65 added 2 changesets with 2 changes to 2 files
66 66
67 67 Server advertises presence of feature to client requesting full clone
68 68
69 69 $ hg --config experimental.clonebundles=false clone -U http://localhost:$HGPORT advertise-on-clone
70 70 requesting all changes
71 71 remote: this server supports the experimental "clone bundles" feature that should enable faster and more reliable cloning
72 72 remote: help test it by setting the "experimental.clonebundles" config flag to "true"
73 73 adding changesets
74 74 adding manifests
75 75 adding file changes
76 76 added 2 changesets with 2 changes to 2 files
77 77
78 78 Manifest file with invalid URL aborts
79 79
80 80 $ echo 'http://does.not.exist/bundle.hg' > server/.hg/clonebundles.manifest
81 81 $ hg clone http://localhost:$HGPORT 404-url
82 82 applying clone bundle from http://does.not.exist/bundle.hg
83 83 error fetching bundle: * not known (glob)
84 84 abort: error applying bundle
85 85 (if this error persists, consider contacting the server operator or disable clone bundles via "--config experimental.clonebundles=false")
86 86 [255]
87 87
88 88 Server is not running aborts
89 89
90 90 $ echo "http://localhost:$HGPORT1/bundle.hg" > server/.hg/clonebundles.manifest
91 91 $ hg clone http://localhost:$HGPORT server-not-runner
92 92 applying clone bundle from http://localhost:$HGPORT1/bundle.hg
93 93 error fetching bundle: Connection refused
94 94 abort: error applying bundle
95 95 (if this error persists, consider contacting the server operator or disable clone bundles via "--config experimental.clonebundles=false")
96 96 [255]
97 97
98 98 Server returns 404
99 99
100 100 $ python $TESTDIR/dumbhttp.py -p $HGPORT1 --pid http.pid
101 101 $ cat http.pid >> $DAEMON_PIDS
102 102 $ hg clone http://localhost:$HGPORT running-404
103 103 applying clone bundle from http://localhost:$HGPORT1/bundle.hg
104 104 HTTP error fetching bundle: HTTP Error 404: File not found
105 105 abort: error applying bundle
106 106 (if this error persists, consider contacting the server operator or disable clone bundles via "--config experimental.clonebundles=false")
107 107 [255]
108 108
109 109 We can override failure to fall back to regular clone
110 110
111 111 $ hg --config ui.clonebundlefallback=true clone -U http://localhost:$HGPORT 404-fallback
112 112 applying clone bundle from http://localhost:$HGPORT1/bundle.hg
113 113 HTTP error fetching bundle: HTTP Error 404: File not found
114 114 falling back to normal clone
115 115 requesting all changes
116 116 adding changesets
117 117 adding manifests
118 118 adding file changes
119 119 added 2 changesets with 2 changes to 2 files
120 120
121 121 Bundle with partial content works
122 122
123 123 $ hg -R server bundle --type gzip-v1 --base null -r 53245c60e682 partial.hg
124 124 1 changesets found
125 125
126 126 We verify exact bundle content as an extra check against accidental future
127 127 changes. If this output changes, we could break old clients.
128 128
129 129 $ f --size --hexdump partial.hg
130 130 partial.hg: size=208
131 131 0000: 48 47 31 30 47 5a 78 9c 63 60 60 98 17 ac 12 93 |HG10GZx.c``.....|
132 132 0010: f0 ac a9 23 45 70 cb bf 0d 5f 59 4e 4a 7f 79 21 |...#Ep..._YNJ.y!|
133 133 0020: 9b cc 40 24 20 a0 d7 ce 2c d1 38 25 cd 24 25 d5 |..@$ ...,.8%.$%.|
134 134 0030: d8 c2 22 cd 38 d9 24 cd 22 d5 c8 22 cd 24 cd 32 |..".8.$."..".$.2|
135 135 0040: d1 c2 d0 c4 c8 d2 32 d1 38 39 29 c9 34 cd d4 80 |......2.89).4...|
136 136 0050: ab 24 b5 b8 84 cb 40 c1 80 2b 2d 3f 9f 8b 2b 31 |.$....@..+-?..+1|
137 137 0060: 25 45 01 c8 80 9a d2 9b 65 fb e5 9e 45 bf 8d 7f |%E......e...E...|
138 138 0070: 9f c6 97 9f 2b 44 34 67 d9 ec 8e 0f a0 92 0b 75 |....+D4g.......u|
139 139 0080: 41 d6 24 59 18 a4 a4 9a a6 18 1a 5b 98 9b 5a 98 |A.$Y.......[..Z.|
140 140 0090: 9a 18 26 9b a6 19 98 1a 99 99 26 a6 18 9a 98 24 |..&.......&....$|
141 141 00a0: 26 59 a6 25 5a 98 a5 18 a6 24 71 41 35 b1 43 dc |&Y.%Z....$qA5.C.|
142 142 00b0: 96 b0 83 f7 e9 45 8b d2 56 c7 a3 1f 82 52 d7 8a |.....E..V....R..|
143 143 00c0: 78 ed fc d5 76 f1 36 95 dc 05 07 00 ad 39 5e d3 |x...v.6......9^.|
144 144
145 145 $ echo "http://localhost:$HGPORT1/partial.hg" > server/.hg/clonebundles.manifest
146 146 $ hg clone -U http://localhost:$HGPORT partial-bundle
147 147 applying clone bundle from http://localhost:$HGPORT1/partial.hg
148 148 adding changesets
149 149 adding manifests
150 150 adding file changes
151 151 added 1 changesets with 1 changes to 1 files
152 152 finished applying clone bundle
153 153 searching for changes
154 154 adding changesets
155 155 adding manifests
156 156 adding file changes
157 157 added 1 changesets with 1 changes to 1 files
158 158
159 159 Incremental pull doesn't fetch bundle
160 160
161 161 $ hg clone -r 53245c60e682 -U http://localhost:$HGPORT partial-clone
162 162 adding changesets
163 163 adding manifests
164 164 adding file changes
165 165 added 1 changesets with 1 changes to 1 files
166 166
167 167 $ cd partial-clone
168 168 $ hg pull
169 169 pulling from http://localhost:$HGPORT/
170 170 searching for changes
171 171 adding changesets
172 172 adding manifests
173 173 adding file changes
174 174 added 1 changesets with 1 changes to 1 files
175 175 (run 'hg update' to get a working copy)
176 176 $ cd ..
177 177
178 178 Bundle with full content works
179 179
180 180 $ hg -R server bundle --type gzip-v2 --base null -r tip full.hg
181 181 2 changesets found
182 182
183 183 Again, we perform an extra check against bundle content changes. If this content
184 184 changes, clone bundles produced by new Mercurial versions may not be readable
185 185 by old clients.
186 186
187 187 $ f --size --hexdump full.hg
188 188 full.hg: size=408
189 189 0000: 48 47 32 30 00 00 00 0e 43 6f 6d 70 72 65 73 73 |HG20....Compress|
190 190 0010: 69 6f 6e 3d 47 5a 78 9c 63 60 60 90 e5 76 f6 70 |ion=GZx.c``..v.p|
191 191 0020: f4 73 77 75 0f f2 0f 0d 60 00 02 46 06 76 a6 b2 |.swu....`..F.v..|
192 192 0030: d4 a2 e2 cc fc 3c 03 23 06 06 e6 7d 40 b1 4d c1 |.....<.#...}@.M.|
193 193 0040: 2a 31 09 cf 9a 3a 52 04 b7 fc db f0 95 e5 a4 f4 |*1...:R.........|
194 194 0050: 97 17 b2 c9 0c 14 00 02 e6 d9 99 25 1a a7 a4 99 |...........%....|
195 195 0060: a4 a4 1a 5b 58 a4 19 27 9b a4 59 a4 1a 59 a4 99 |...[X..'..Y..Y..|
196 196 0070: a4 59 26 5a 18 9a 18 59 5a 26 1a 27 27 25 99 a6 |.Y&Z...YZ&.''%..|
197 197 0080: 99 1a 70 95 a4 16 97 70 19 28 18 70 a5 e5 e7 73 |..p....p.(.p...s|
198 198 0090: 71 25 a6 a4 28 00 19 40 13 0e ac fa df ab ff 7b |q%..(..@.......{|
199 199 00a0: 3f fb 92 dc 8b 1f 62 bb 9e b7 d7 d9 87 3d 5a 44 |?.....b......=ZD|
200 200 00b0: ac 2f b0 a9 c3 66 1e 54 b9 26 08 a7 1a 1b 1a a7 |./...f.T.&......|
201 201 00c0: 25 1b 9a 1b 99 19 9a 5a 18 9b a6 18 19 00 dd 67 |%......Z.......g|
202 202 00d0: 61 61 98 06 f4 80 49 4a 8a 65 52 92 41 9a 81 81 |aa....IJ.eR.A...|
203 203 00e0: a5 11 17 50 31 30 58 19 cc 80 98 25 29 b1 08 c4 |...P10X....%)...|
204 204 00f0: 37 07 79 19 88 d9 41 ee 07 8a 41 cd 5d 98 65 fb |7.y...A...A.].e.|
205 205 0100: e5 9e 45 bf 8d 7f 9f c6 97 9f 2b 44 34 67 d9 ec |..E.......+D4g..|
206 206 0110: 8e 0f a0 61 a8 eb 82 82 2e c9 c2 20 25 d5 34 c5 |...a....... %.4.|
207 207 0120: d0 d8 c2 dc d4 c2 d4 c4 30 d9 34 cd c0 d4 c8 cc |........0.4.....|
208 208 0130: 34 31 c5 d0 c4 24 31 c9 32 2d d1 c2 2c c5 30 25 |41...$1.2-..,.0%|
209 209 0140: 09 e4 ee 85 8f 85 ff 88 ab 89 36 c7 2a c4 47 34 |..........6.*.G4|
210 210 0150: fe f8 ec 7b 73 37 3f c3 24 62 1d 8d 4d 1d 9e 40 |...{s7?.$b..M..@|
211 211 0160: 06 3b 10 14 36 a4 38 10 04 d8 21 01 5a b2 83 f7 |.;..6.8...!.Z...|
212 212 0170: e9 45 8b d2 56 c7 a3 1f 82 52 d7 8a 78 ed fc d5 |.E..V....R..x...|
213 213 0180: 76 f1 36 25 81 49 c0 ad 30 c0 0e 49 8f 54 b7 9e |v.6%.I..0..I.T..|
214 214 0190: d4 1c 09 00 bb 8d f0 bd |........|
215 215
216 216 $ echo "http://localhost:$HGPORT1/full.hg" > server/.hg/clonebundles.manifest
217 217 $ hg clone -U http://localhost:$HGPORT full-bundle
218 218 applying clone bundle from http://localhost:$HGPORT1/full.hg
219 219 adding changesets
220 220 adding manifests
221 221 adding file changes
222 222 added 2 changesets with 2 changes to 2 files
223 223 finished applying clone bundle
224 224 searching for changes
225 225 no changes found
226 226
227 Feature works over SSH
228
229 $ hg clone -U -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/server ssh-full-clone
230 applying clone bundle from http://localhost:$HGPORT1/full.hg
231 adding changesets
232 adding manifests
233 adding file changes
234 added 2 changesets with 2 changes to 2 files
235 finished applying clone bundle
236 searching for changes
237 no changes found
238
227 239 Entry with unknown BUNDLESPEC is filtered and not used
228 240
229 241 $ cat > server/.hg/clonebundles.manifest << EOF
230 242 > http://bad.entry1 BUNDLESPEC=UNKNOWN
231 243 > http://bad.entry2 BUNDLESPEC=xz-v1
232 244 > http://bad.entry3 BUNDLESPEC=none-v100
233 245 > http://localhost:$HGPORT1/full.hg BUNDLESPEC=gzip-v2
234 246 > EOF
235 247
236 248 $ hg clone -U http://localhost:$HGPORT filter-unknown-type
237 249 applying clone bundle from http://localhost:$HGPORT1/full.hg
238 250 adding changesets
239 251 adding manifests
240 252 adding file changes
241 253 added 2 changesets with 2 changes to 2 files
242 254 finished applying clone bundle
243 255 searching for changes
244 256 no changes found
245 257
246 258 Automatic fallback when all entries are filtered
247 259
248 260 $ cat > server/.hg/clonebundles.manifest << EOF
249 261 > http://bad.entry BUNDLESPEC=UNKNOWN
250 262 > EOF
251 263
252 264 $ hg clone -U http://localhost:$HGPORT filter-all
253 265 no compatible clone bundles available on server; falling back to regular clone
254 266 (you may want to report this to the server operator)
255 267 requesting all changes
256 268 adding changesets
257 269 adding manifests
258 270 adding file changes
259 271 added 2 changesets with 2 changes to 2 files
260 272
261 273 URLs requiring SNI are filtered in Python <2.7.9
262 274
263 275 $ cp full.hg sni.hg
264 276 $ cat > server/.hg/clonebundles.manifest << EOF
265 277 > http://localhost:$HGPORT1/sni.hg REQUIRESNI=true
266 278 > http://localhost:$HGPORT1/full.hg
267 279 > EOF
268 280
269 281 #if sslcontext
270 282 Python 2.7.9+ support SNI
271 283
272 284 $ hg clone -U http://localhost:$HGPORT sni-supported
273 285 applying clone bundle from http://localhost:$HGPORT1/sni.hg
274 286 adding changesets
275 287 adding manifests
276 288 adding file changes
277 289 added 2 changesets with 2 changes to 2 files
278 290 finished applying clone bundle
279 291 searching for changes
280 292 no changes found
281 293 #else
282 294 Python <2.7.9 will filter SNI URLs
283 295
284 296 $ hg clone -U http://localhost:$HGPORT sni-unsupported
285 297 applying clone bundle from http://localhost:$HGPORT1/full.hg
286 298 adding changesets
287 299 adding manifests
288 300 adding file changes
289 301 added 2 changesets with 2 changes to 2 files
290 302 finished applying clone bundle
291 303 searching for changes
292 304 no changes found
293 305 #endif
294 306
295 307 Stream clone bundles are supported
296 308
297 309 $ hg -R server debugcreatestreamclonebundle packed.hg
298 310 writing 613 bytes for 4 files
299 311 bundle requirements: revlogv1
300 312
301 313 No bundle spec should work
302 314
303 315 $ cat > server/.hg/clonebundles.manifest << EOF
304 316 > http://localhost:$HGPORT1/packed.hg
305 317 > EOF
306 318
307 319 $ hg clone -U http://localhost:$HGPORT stream-clone-no-spec
308 320 applying clone bundle from http://localhost:$HGPORT1/packed.hg
309 321 4 files to transfer, 613 bytes of data
310 322 transferred 613 bytes in *.* seconds (*) (glob)
311 323 finished applying clone bundle
312 324 searching for changes
313 325 no changes found
314 326
315 327 Bundle spec without parameters should work
316 328
317 329 $ cat > server/.hg/clonebundles.manifest << EOF
318 330 > http://localhost:$HGPORT1/packed.hg BUNDLESPEC=none-packed1
319 331 > EOF
320 332
321 333 $ hg clone -U http://localhost:$HGPORT stream-clone-vanilla-spec
322 334 applying clone bundle from http://localhost:$HGPORT1/packed.hg
323 335 4 files to transfer, 613 bytes of data
324 336 transferred 613 bytes in *.* seconds (*) (glob)
325 337 finished applying clone bundle
326 338 searching for changes
327 339 no changes found
328 340
329 341 Bundle spec with format requirements should work
330 342
331 343 $ cat > server/.hg/clonebundles.manifest << EOF
332 344 > http://localhost:$HGPORT1/packed.hg BUNDLESPEC=none-packed1;requirements%3Drevlogv1
333 345 > EOF
334 346
335 347 $ hg clone -U http://localhost:$HGPORT stream-clone-supported-requirements
336 348 applying clone bundle from http://localhost:$HGPORT1/packed.hg
337 349 4 files to transfer, 613 bytes of data
338 350 transferred 613 bytes in *.* seconds (*) (glob)
339 351 finished applying clone bundle
340 352 searching for changes
341 353 no changes found
342 354
343 355 Stream bundle spec with unknown requirements should be filtered out
344 356
345 357 $ cat > server/.hg/clonebundles.manifest << EOF
346 358 > http://localhost:$HGPORT1/packed.hg BUNDLESPEC=none-packed1;requirements%3Drevlogv42
347 359 > EOF
348 360
349 361 $ hg clone -U http://localhost:$HGPORT stream-clone-unsupported-requirements
350 362 no compatible clone bundles available on server; falling back to regular clone
351 363 (you may want to report this to the server operator)
352 364 requesting all changes
353 365 adding changesets
354 366 adding manifests
355 367 adding file changes
356 368 added 2 changesets with 2 changes to 2 files
357 369
358 370 Set up manifest for testing preferences
359 371 (Remember, the TYPE does not have to match reality - the URL is
360 372 important)
361 373
362 374 $ cp full.hg gz-a.hg
363 375 $ cp full.hg gz-b.hg
364 376 $ cp full.hg bz2-a.hg
365 377 $ cp full.hg bz2-b.hg
366 378 $ cat > server/.hg/clonebundles.manifest << EOF
367 379 > http://localhost:$HGPORT1/gz-a.hg BUNDLESPEC=gzip-v2 extra=a
368 380 > http://localhost:$HGPORT1/bz2-a.hg BUNDLESPEC=bzip2-v2 extra=a
369 381 > http://localhost:$HGPORT1/gz-b.hg BUNDLESPEC=gzip-v2 extra=b
370 382 > http://localhost:$HGPORT1/bz2-b.hg BUNDLESPEC=bzip2-v2 extra=b
371 383 > EOF
372 384
373 385 Preferring an undefined attribute will take first entry
374 386
375 387 $ hg --config experimental.clonebundleprefers=foo=bar clone -U http://localhost:$HGPORT prefer-foo
376 388 applying clone bundle from http://localhost:$HGPORT1/gz-a.hg
377 389 adding changesets
378 390 adding manifests
379 391 adding file changes
380 392 added 2 changesets with 2 changes to 2 files
381 393 finished applying clone bundle
382 394 searching for changes
383 395 no changes found
384 396
385 397 Preferring bz2 type will download first entry of that type
386 398
387 399 $ hg --config experimental.clonebundleprefers=COMPRESSION=bzip2 clone -U http://localhost:$HGPORT prefer-bz
388 400 applying clone bundle from http://localhost:$HGPORT1/bz2-a.hg
389 401 adding changesets
390 402 adding manifests
391 403 adding file changes
392 404 added 2 changesets with 2 changes to 2 files
393 405 finished applying clone bundle
394 406 searching for changes
395 407 no changes found
396 408
397 409 Preferring multiple values of an option works
398 410
399 411 $ hg --config experimental.clonebundleprefers=COMPRESSION=unknown,COMPRESSION=bzip2 clone -U http://localhost:$HGPORT prefer-multiple-bz
400 412 applying clone bundle from http://localhost:$HGPORT1/bz2-a.hg
401 413 adding changesets
402 414 adding manifests
403 415 adding file changes
404 416 added 2 changesets with 2 changes to 2 files
405 417 finished applying clone bundle
406 418 searching for changes
407 419 no changes found
408 420
409 421 Sorting multiple values should get us back to original first entry
410 422
411 423 $ hg --config experimental.clonebundleprefers=BUNDLESPEC=unknown,BUNDLESPEC=gzip-v2,BUNDLESPEC=bzip2-v2 clone -U http://localhost:$HGPORT prefer-multiple-gz
412 424 applying clone bundle from http://localhost:$HGPORT1/gz-a.hg
413 425 adding changesets
414 426 adding manifests
415 427 adding file changes
416 428 added 2 changesets with 2 changes to 2 files
417 429 finished applying clone bundle
418 430 searching for changes
419 431 no changes found
420 432
421 433 Preferring multiple attributes has correct order
422 434
423 435 $ hg --config experimental.clonebundleprefers=extra=b,BUNDLESPEC=bzip2-v2 clone -U http://localhost:$HGPORT prefer-separate-attributes
424 436 applying clone bundle from http://localhost:$HGPORT1/bz2-b.hg
425 437 adding changesets
426 438 adding manifests
427 439 adding file changes
428 440 added 2 changesets with 2 changes to 2 files
429 441 finished applying clone bundle
430 442 searching for changes
431 443 no changes found
432 444
433 445 Test where attribute is missing from some entries
434 446
435 447 $ cat > server/.hg/clonebundles.manifest << EOF
436 448 > http://localhost:$HGPORT1/gz-a.hg BUNDLESPEC=gzip-v2
437 449 > http://localhost:$HGPORT1/bz2-a.hg BUNDLESPEC=bzip2-v2
438 450 > http://localhost:$HGPORT1/gz-b.hg BUNDLESPEC=gzip-v2 extra=b
439 451 > http://localhost:$HGPORT1/bz2-b.hg BUNDLESPEC=bzip2-v2 extra=b
440 452 > EOF
441 453
442 454 $ hg --config experimental.clonebundleprefers=extra=b clone -U http://localhost:$HGPORT prefer-partially-defined-attribute
443 455 applying clone bundle from http://localhost:$HGPORT1/gz-b.hg
444 456 adding changesets
445 457 adding manifests
446 458 adding file changes
447 459 added 2 changesets with 2 changes to 2 files
448 460 finished applying clone bundle
449 461 searching for changes
450 462 no changes found
General Comments 0
You need to be logged in to leave comments. Login now