##// END OF EJS Templates
py3: use pycompat.strkwargs() to convert kwargs keys to str before passing
Pulkit Goyal -
r32896:e14484e7 default
parent child Browse files
Show More
@@ -1,2009 +1,2010
1 # exchange.py - utility to exchange data between repos.
1 # exchange.py - utility to exchange data between repos.
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
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 hashlib
11 import hashlib
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 hex,
15 hex,
16 nullid,
16 nullid,
17 )
17 )
18 from . import (
18 from . import (
19 bookmarks as bookmod,
19 bookmarks as bookmod,
20 bundle2,
20 bundle2,
21 changegroup,
21 changegroup,
22 discovery,
22 discovery,
23 error,
23 error,
24 lock as lockmod,
24 lock as lockmod,
25 obsolete,
25 obsolete,
26 phases,
26 phases,
27 pushkey,
27 pushkey,
28 pycompat,
28 scmutil,
29 scmutil,
29 sslutil,
30 sslutil,
30 streamclone,
31 streamclone,
31 url as urlmod,
32 url as urlmod,
32 util,
33 util,
33 )
34 )
34
35
35 urlerr = util.urlerr
36 urlerr = util.urlerr
36 urlreq = util.urlreq
37 urlreq = util.urlreq
37
38
38 # Maps bundle version human names to changegroup versions.
39 # Maps bundle version human names to changegroup versions.
39 _bundlespeccgversions = {'v1': '01',
40 _bundlespeccgversions = {'v1': '01',
40 'v2': '02',
41 'v2': '02',
41 'packed1': 's1',
42 'packed1': 's1',
42 'bundle2': '02', #legacy
43 'bundle2': '02', #legacy
43 }
44 }
44
45
45 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
46 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
46 _bundlespecv1compengines = {'gzip', 'bzip2', 'none'}
47 _bundlespecv1compengines = {'gzip', 'bzip2', 'none'}
47
48
48 def parsebundlespec(repo, spec, strict=True, externalnames=False):
49 def parsebundlespec(repo, spec, strict=True, externalnames=False):
49 """Parse a bundle string specification into parts.
50 """Parse a bundle string specification into parts.
50
51
51 Bundle specifications denote a well-defined bundle/exchange format.
52 Bundle specifications denote a well-defined bundle/exchange format.
52 The content of a given specification should not change over time in
53 The content of a given specification should not change over time in
53 order to ensure that bundles produced by a newer version of Mercurial are
54 order to ensure that bundles produced by a newer version of Mercurial are
54 readable from an older version.
55 readable from an older version.
55
56
56 The string currently has the form:
57 The string currently has the form:
57
58
58 <compression>-<type>[;<parameter0>[;<parameter1>]]
59 <compression>-<type>[;<parameter0>[;<parameter1>]]
59
60
60 Where <compression> is one of the supported compression formats
61 Where <compression> is one of the supported compression formats
61 and <type> is (currently) a version string. A ";" can follow the type and
62 and <type> is (currently) a version string. A ";" can follow the type and
62 all text afterwards is interpreted as URI encoded, ";" delimited key=value
63 all text afterwards is interpreted as URI encoded, ";" delimited key=value
63 pairs.
64 pairs.
64
65
65 If ``strict`` is True (the default) <compression> is required. Otherwise,
66 If ``strict`` is True (the default) <compression> is required. Otherwise,
66 it is optional.
67 it is optional.
67
68
68 If ``externalnames`` is False (the default), the human-centric names will
69 If ``externalnames`` is False (the default), the human-centric names will
69 be converted to their internal representation.
70 be converted to their internal representation.
70
71
71 Returns a 3-tuple of (compression, version, parameters). Compression will
72 Returns a 3-tuple of (compression, version, parameters). Compression will
72 be ``None`` if not in strict mode and a compression isn't defined.
73 be ``None`` if not in strict mode and a compression isn't defined.
73
74
74 An ``InvalidBundleSpecification`` is raised when the specification is
75 An ``InvalidBundleSpecification`` is raised when the specification is
75 not syntactically well formed.
76 not syntactically well formed.
76
77
77 An ``UnsupportedBundleSpecification`` is raised when the compression or
78 An ``UnsupportedBundleSpecification`` is raised when the compression or
78 bundle type/version is not recognized.
79 bundle type/version is not recognized.
79
80
80 Note: this function will likely eventually return a more complex data
81 Note: this function will likely eventually return a more complex data
81 structure, including bundle2 part information.
82 structure, including bundle2 part information.
82 """
83 """
83 def parseparams(s):
84 def parseparams(s):
84 if ';' not in s:
85 if ';' not in s:
85 return s, {}
86 return s, {}
86
87
87 params = {}
88 params = {}
88 version, paramstr = s.split(';', 1)
89 version, paramstr = s.split(';', 1)
89
90
90 for p in paramstr.split(';'):
91 for p in paramstr.split(';'):
91 if '=' not in p:
92 if '=' not in p:
92 raise error.InvalidBundleSpecification(
93 raise error.InvalidBundleSpecification(
93 _('invalid bundle specification: '
94 _('invalid bundle specification: '
94 'missing "=" in parameter: %s') % p)
95 'missing "=" in parameter: %s') % p)
95
96
96 key, value = p.split('=', 1)
97 key, value = p.split('=', 1)
97 key = urlreq.unquote(key)
98 key = urlreq.unquote(key)
98 value = urlreq.unquote(value)
99 value = urlreq.unquote(value)
99 params[key] = value
100 params[key] = value
100
101
101 return version, params
102 return version, params
102
103
103
104
104 if strict and '-' not in spec:
105 if strict and '-' not in spec:
105 raise error.InvalidBundleSpecification(
106 raise error.InvalidBundleSpecification(
106 _('invalid bundle specification; '
107 _('invalid bundle specification; '
107 'must be prefixed with compression: %s') % spec)
108 'must be prefixed with compression: %s') % spec)
108
109
109 if '-' in spec:
110 if '-' in spec:
110 compression, version = spec.split('-', 1)
111 compression, version = spec.split('-', 1)
111
112
112 if compression not in util.compengines.supportedbundlenames:
113 if compression not in util.compengines.supportedbundlenames:
113 raise error.UnsupportedBundleSpecification(
114 raise error.UnsupportedBundleSpecification(
114 _('%s compression is not supported') % compression)
115 _('%s compression is not supported') % compression)
115
116
116 version, params = parseparams(version)
117 version, params = parseparams(version)
117
118
118 if version not in _bundlespeccgversions:
119 if version not in _bundlespeccgversions:
119 raise error.UnsupportedBundleSpecification(
120 raise error.UnsupportedBundleSpecification(
120 _('%s is not a recognized bundle version') % version)
121 _('%s is not a recognized bundle version') % version)
121 else:
122 else:
122 # Value could be just the compression or just the version, in which
123 # Value could be just the compression or just the version, in which
123 # case some defaults are assumed (but only when not in strict mode).
124 # case some defaults are assumed (but only when not in strict mode).
124 assert not strict
125 assert not strict
125
126
126 spec, params = parseparams(spec)
127 spec, params = parseparams(spec)
127
128
128 if spec in util.compengines.supportedbundlenames:
129 if spec in util.compengines.supportedbundlenames:
129 compression = spec
130 compression = spec
130 version = 'v1'
131 version = 'v1'
131 # Generaldelta repos require v2.
132 # Generaldelta repos require v2.
132 if 'generaldelta' in repo.requirements:
133 if 'generaldelta' in repo.requirements:
133 version = 'v2'
134 version = 'v2'
134 # Modern compression engines require v2.
135 # Modern compression engines require v2.
135 if compression not in _bundlespecv1compengines:
136 if compression not in _bundlespecv1compengines:
136 version = 'v2'
137 version = 'v2'
137 elif spec in _bundlespeccgversions:
138 elif spec in _bundlespeccgversions:
138 if spec == 'packed1':
139 if spec == 'packed1':
139 compression = 'none'
140 compression = 'none'
140 else:
141 else:
141 compression = 'bzip2'
142 compression = 'bzip2'
142 version = spec
143 version = spec
143 else:
144 else:
144 raise error.UnsupportedBundleSpecification(
145 raise error.UnsupportedBundleSpecification(
145 _('%s is not a recognized bundle specification') % spec)
146 _('%s is not a recognized bundle specification') % spec)
146
147
147 # Bundle version 1 only supports a known set of compression engines.
148 # Bundle version 1 only supports a known set of compression engines.
148 if version == 'v1' and compression not in _bundlespecv1compengines:
149 if version == 'v1' and compression not in _bundlespecv1compengines:
149 raise error.UnsupportedBundleSpecification(
150 raise error.UnsupportedBundleSpecification(
150 _('compression engine %s is not supported on v1 bundles') %
151 _('compression engine %s is not supported on v1 bundles') %
151 compression)
152 compression)
152
153
153 # The specification for packed1 can optionally declare the data formats
154 # The specification for packed1 can optionally declare the data formats
154 # required to apply it. If we see this metadata, compare against what the
155 # required to apply it. If we see this metadata, compare against what the
155 # repo supports and error if the bundle isn't compatible.
156 # repo supports and error if the bundle isn't compatible.
156 if version == 'packed1' and 'requirements' in params:
157 if version == 'packed1' and 'requirements' in params:
157 requirements = set(params['requirements'].split(','))
158 requirements = set(params['requirements'].split(','))
158 missingreqs = requirements - repo.supportedformats
159 missingreqs = requirements - repo.supportedformats
159 if missingreqs:
160 if missingreqs:
160 raise error.UnsupportedBundleSpecification(
161 raise error.UnsupportedBundleSpecification(
161 _('missing support for repository features: %s') %
162 _('missing support for repository features: %s') %
162 ', '.join(sorted(missingreqs)))
163 ', '.join(sorted(missingreqs)))
163
164
164 if not externalnames:
165 if not externalnames:
165 engine = util.compengines.forbundlename(compression)
166 engine = util.compengines.forbundlename(compression)
166 compression = engine.bundletype()[1]
167 compression = engine.bundletype()[1]
167 version = _bundlespeccgversions[version]
168 version = _bundlespeccgversions[version]
168 return compression, version, params
169 return compression, version, params
169
170
170 def readbundle(ui, fh, fname, vfs=None):
171 def readbundle(ui, fh, fname, vfs=None):
171 header = changegroup.readexactly(fh, 4)
172 header = changegroup.readexactly(fh, 4)
172
173
173 alg = None
174 alg = None
174 if not fname:
175 if not fname:
175 fname = "stream"
176 fname = "stream"
176 if not header.startswith('HG') and header.startswith('\0'):
177 if not header.startswith('HG') and header.startswith('\0'):
177 fh = changegroup.headerlessfixup(fh, header)
178 fh = changegroup.headerlessfixup(fh, header)
178 header = "HG10"
179 header = "HG10"
179 alg = 'UN'
180 alg = 'UN'
180 elif vfs:
181 elif vfs:
181 fname = vfs.join(fname)
182 fname = vfs.join(fname)
182
183
183 magic, version = header[0:2], header[2:4]
184 magic, version = header[0:2], header[2:4]
184
185
185 if magic != 'HG':
186 if magic != 'HG':
186 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
187 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
187 if version == '10':
188 if version == '10':
188 if alg is None:
189 if alg is None:
189 alg = changegroup.readexactly(fh, 2)
190 alg = changegroup.readexactly(fh, 2)
190 return changegroup.cg1unpacker(fh, alg)
191 return changegroup.cg1unpacker(fh, alg)
191 elif version.startswith('2'):
192 elif version.startswith('2'):
192 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
193 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
193 elif version == 'S1':
194 elif version == 'S1':
194 return streamclone.streamcloneapplier(fh)
195 return streamclone.streamcloneapplier(fh)
195 else:
196 else:
196 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
197 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
197
198
198 def getbundlespec(ui, fh):
199 def getbundlespec(ui, fh):
199 """Infer the bundlespec from a bundle file handle.
200 """Infer the bundlespec from a bundle file handle.
200
201
201 The input file handle is seeked and the original seek position is not
202 The input file handle is seeked and the original seek position is not
202 restored.
203 restored.
203 """
204 """
204 def speccompression(alg):
205 def speccompression(alg):
205 try:
206 try:
206 return util.compengines.forbundletype(alg).bundletype()[0]
207 return util.compengines.forbundletype(alg).bundletype()[0]
207 except KeyError:
208 except KeyError:
208 return None
209 return None
209
210
210 b = readbundle(ui, fh, None)
211 b = readbundle(ui, fh, None)
211 if isinstance(b, changegroup.cg1unpacker):
212 if isinstance(b, changegroup.cg1unpacker):
212 alg = b._type
213 alg = b._type
213 if alg == '_truncatedBZ':
214 if alg == '_truncatedBZ':
214 alg = 'BZ'
215 alg = 'BZ'
215 comp = speccompression(alg)
216 comp = speccompression(alg)
216 if not comp:
217 if not comp:
217 raise error.Abort(_('unknown compression algorithm: %s') % alg)
218 raise error.Abort(_('unknown compression algorithm: %s') % alg)
218 return '%s-v1' % comp
219 return '%s-v1' % comp
219 elif isinstance(b, bundle2.unbundle20):
220 elif isinstance(b, bundle2.unbundle20):
220 if 'Compression' in b.params:
221 if 'Compression' in b.params:
221 comp = speccompression(b.params['Compression'])
222 comp = speccompression(b.params['Compression'])
222 if not comp:
223 if not comp:
223 raise error.Abort(_('unknown compression algorithm: %s') % comp)
224 raise error.Abort(_('unknown compression algorithm: %s') % comp)
224 else:
225 else:
225 comp = 'none'
226 comp = 'none'
226
227
227 version = None
228 version = None
228 for part in b.iterparts():
229 for part in b.iterparts():
229 if part.type == 'changegroup':
230 if part.type == 'changegroup':
230 version = part.params['version']
231 version = part.params['version']
231 if version in ('01', '02'):
232 if version in ('01', '02'):
232 version = 'v2'
233 version = 'v2'
233 else:
234 else:
234 raise error.Abort(_('changegroup version %s does not have '
235 raise error.Abort(_('changegroup version %s does not have '
235 'a known bundlespec') % version,
236 'a known bundlespec') % version,
236 hint=_('try upgrading your Mercurial '
237 hint=_('try upgrading your Mercurial '
237 'client'))
238 'client'))
238
239
239 if not version:
240 if not version:
240 raise error.Abort(_('could not identify changegroup version in '
241 raise error.Abort(_('could not identify changegroup version in '
241 'bundle'))
242 'bundle'))
242
243
243 return '%s-%s' % (comp, version)
244 return '%s-%s' % (comp, version)
244 elif isinstance(b, streamclone.streamcloneapplier):
245 elif isinstance(b, streamclone.streamcloneapplier):
245 requirements = streamclone.readbundle1header(fh)[2]
246 requirements = streamclone.readbundle1header(fh)[2]
246 params = 'requirements=%s' % ','.join(sorted(requirements))
247 params = 'requirements=%s' % ','.join(sorted(requirements))
247 return 'none-packed1;%s' % urlreq.quote(params)
248 return 'none-packed1;%s' % urlreq.quote(params)
248 else:
249 else:
249 raise error.Abort(_('unknown bundle type: %s') % b)
250 raise error.Abort(_('unknown bundle type: %s') % b)
250
251
251 def _computeoutgoing(repo, heads, common):
252 def _computeoutgoing(repo, heads, common):
252 """Computes which revs are outgoing given a set of common
253 """Computes which revs are outgoing given a set of common
253 and a set of heads.
254 and a set of heads.
254
255
255 This is a separate function so extensions can have access to
256 This is a separate function so extensions can have access to
256 the logic.
257 the logic.
257
258
258 Returns a discovery.outgoing object.
259 Returns a discovery.outgoing object.
259 """
260 """
260 cl = repo.changelog
261 cl = repo.changelog
261 if common:
262 if common:
262 hasnode = cl.hasnode
263 hasnode = cl.hasnode
263 common = [n for n in common if hasnode(n)]
264 common = [n for n in common if hasnode(n)]
264 else:
265 else:
265 common = [nullid]
266 common = [nullid]
266 if not heads:
267 if not heads:
267 heads = cl.heads()
268 heads = cl.heads()
268 return discovery.outgoing(repo, common, heads)
269 return discovery.outgoing(repo, common, heads)
269
270
270 def _forcebundle1(op):
271 def _forcebundle1(op):
271 """return true if a pull/push must use bundle1
272 """return true if a pull/push must use bundle1
272
273
273 This function is used to allow testing of the older bundle version"""
274 This function is used to allow testing of the older bundle version"""
274 ui = op.repo.ui
275 ui = op.repo.ui
275 forcebundle1 = False
276 forcebundle1 = False
276 # The goal is this config is to allow developer to choose the bundle
277 # The goal is this config is to allow developer to choose the bundle
277 # version used during exchanged. This is especially handy during test.
278 # version used during exchanged. This is especially handy during test.
278 # Value is a list of bundle version to be picked from, highest version
279 # Value is a list of bundle version to be picked from, highest version
279 # should be used.
280 # should be used.
280 #
281 #
281 # developer config: devel.legacy.exchange
282 # developer config: devel.legacy.exchange
282 exchange = ui.configlist('devel', 'legacy.exchange')
283 exchange = ui.configlist('devel', 'legacy.exchange')
283 forcebundle1 = 'bundle2' not in exchange and 'bundle1' in exchange
284 forcebundle1 = 'bundle2' not in exchange and 'bundle1' in exchange
284 return forcebundle1 or not op.remote.capable('bundle2')
285 return forcebundle1 or not op.remote.capable('bundle2')
285
286
286 class pushoperation(object):
287 class pushoperation(object):
287 """A object that represent a single push operation
288 """A object that represent a single push operation
288
289
289 Its purpose is to carry push related state and very common operations.
290 Its purpose is to carry push related state and very common operations.
290
291
291 A new pushoperation should be created at the beginning of each push and
292 A new pushoperation should be created at the beginning of each push and
292 discarded afterward.
293 discarded afterward.
293 """
294 """
294
295
295 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
296 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
296 bookmarks=()):
297 bookmarks=()):
297 # repo we push from
298 # repo we push from
298 self.repo = repo
299 self.repo = repo
299 self.ui = repo.ui
300 self.ui = repo.ui
300 # repo we push to
301 # repo we push to
301 self.remote = remote
302 self.remote = remote
302 # force option provided
303 # force option provided
303 self.force = force
304 self.force = force
304 # revs to be pushed (None is "all")
305 # revs to be pushed (None is "all")
305 self.revs = revs
306 self.revs = revs
306 # bookmark explicitly pushed
307 # bookmark explicitly pushed
307 self.bookmarks = bookmarks
308 self.bookmarks = bookmarks
308 # allow push of new branch
309 # allow push of new branch
309 self.newbranch = newbranch
310 self.newbranch = newbranch
310 # did a local lock get acquired?
311 # did a local lock get acquired?
311 self.locallocked = None
312 self.locallocked = None
312 # step already performed
313 # step already performed
313 # (used to check what steps have been already performed through bundle2)
314 # (used to check what steps have been already performed through bundle2)
314 self.stepsdone = set()
315 self.stepsdone = set()
315 # Integer version of the changegroup push result
316 # Integer version of the changegroup push result
316 # - None means nothing to push
317 # - None means nothing to push
317 # - 0 means HTTP error
318 # - 0 means HTTP error
318 # - 1 means we pushed and remote head count is unchanged *or*
319 # - 1 means we pushed and remote head count is unchanged *or*
319 # we have outgoing changesets but refused to push
320 # we have outgoing changesets but refused to push
320 # - other values as described by addchangegroup()
321 # - other values as described by addchangegroup()
321 self.cgresult = None
322 self.cgresult = None
322 # Boolean value for the bookmark push
323 # Boolean value for the bookmark push
323 self.bkresult = None
324 self.bkresult = None
324 # discover.outgoing object (contains common and outgoing data)
325 # discover.outgoing object (contains common and outgoing data)
325 self.outgoing = None
326 self.outgoing = None
326 # all remote topological heads before the push
327 # all remote topological heads before the push
327 self.remoteheads = None
328 self.remoteheads = None
328 # Details of the remote branch pre and post push
329 # Details of the remote branch pre and post push
329 #
330 #
330 # mapping: {'branch': ([remoteheads],
331 # mapping: {'branch': ([remoteheads],
331 # [newheads],
332 # [newheads],
332 # [unsyncedheads],
333 # [unsyncedheads],
333 # [discardedheads])}
334 # [discardedheads])}
334 # - branch: the branch name
335 # - branch: the branch name
335 # - remoteheads: the list of remote heads known locally
336 # - remoteheads: the list of remote heads known locally
336 # None if the branch is new
337 # None if the branch is new
337 # - newheads: the new remote heads (known locally) with outgoing pushed
338 # - newheads: the new remote heads (known locally) with outgoing pushed
338 # - unsyncedheads: the list of remote heads unknown locally.
339 # - unsyncedheads: the list of remote heads unknown locally.
339 # - discardedheads: the list of remote heads made obsolete by the push
340 # - discardedheads: the list of remote heads made obsolete by the push
340 self.pushbranchmap = None
341 self.pushbranchmap = None
341 # testable as a boolean indicating if any nodes are missing locally.
342 # testable as a boolean indicating if any nodes are missing locally.
342 self.incoming = None
343 self.incoming = None
343 # phases changes that must be pushed along side the changesets
344 # phases changes that must be pushed along side the changesets
344 self.outdatedphases = None
345 self.outdatedphases = None
345 # phases changes that must be pushed if changeset push fails
346 # phases changes that must be pushed if changeset push fails
346 self.fallbackoutdatedphases = None
347 self.fallbackoutdatedphases = None
347 # outgoing obsmarkers
348 # outgoing obsmarkers
348 self.outobsmarkers = set()
349 self.outobsmarkers = set()
349 # outgoing bookmarks
350 # outgoing bookmarks
350 self.outbookmarks = []
351 self.outbookmarks = []
351 # transaction manager
352 # transaction manager
352 self.trmanager = None
353 self.trmanager = None
353 # map { pushkey partid -> callback handling failure}
354 # map { pushkey partid -> callback handling failure}
354 # used to handle exception from mandatory pushkey part failure
355 # used to handle exception from mandatory pushkey part failure
355 self.pkfailcb = {}
356 self.pkfailcb = {}
356
357
357 @util.propertycache
358 @util.propertycache
358 def futureheads(self):
359 def futureheads(self):
359 """future remote heads if the changeset push succeeds"""
360 """future remote heads if the changeset push succeeds"""
360 return self.outgoing.missingheads
361 return self.outgoing.missingheads
361
362
362 @util.propertycache
363 @util.propertycache
363 def fallbackheads(self):
364 def fallbackheads(self):
364 """future remote heads if the changeset push fails"""
365 """future remote heads if the changeset push fails"""
365 if self.revs is None:
366 if self.revs is None:
366 # not target to push, all common are relevant
367 # not target to push, all common are relevant
367 return self.outgoing.commonheads
368 return self.outgoing.commonheads
368 unfi = self.repo.unfiltered()
369 unfi = self.repo.unfiltered()
369 # I want cheads = heads(::missingheads and ::commonheads)
370 # I want cheads = heads(::missingheads and ::commonheads)
370 # (missingheads is revs with secret changeset filtered out)
371 # (missingheads is revs with secret changeset filtered out)
371 #
372 #
372 # This can be expressed as:
373 # This can be expressed as:
373 # cheads = ( (missingheads and ::commonheads)
374 # cheads = ( (missingheads and ::commonheads)
374 # + (commonheads and ::missingheads))"
375 # + (commonheads and ::missingheads))"
375 # )
376 # )
376 #
377 #
377 # while trying to push we already computed the following:
378 # while trying to push we already computed the following:
378 # common = (::commonheads)
379 # common = (::commonheads)
379 # missing = ((commonheads::missingheads) - commonheads)
380 # missing = ((commonheads::missingheads) - commonheads)
380 #
381 #
381 # We can pick:
382 # We can pick:
382 # * missingheads part of common (::commonheads)
383 # * missingheads part of common (::commonheads)
383 common = self.outgoing.common
384 common = self.outgoing.common
384 nm = self.repo.changelog.nodemap
385 nm = self.repo.changelog.nodemap
385 cheads = [node for node in self.revs if nm[node] in common]
386 cheads = [node for node in self.revs if nm[node] in common]
386 # and
387 # and
387 # * commonheads parents on missing
388 # * commonheads parents on missing
388 revset = unfi.set('%ln and parents(roots(%ln))',
389 revset = unfi.set('%ln and parents(roots(%ln))',
389 self.outgoing.commonheads,
390 self.outgoing.commonheads,
390 self.outgoing.missing)
391 self.outgoing.missing)
391 cheads.extend(c.node() for c in revset)
392 cheads.extend(c.node() for c in revset)
392 return cheads
393 return cheads
393
394
394 @property
395 @property
395 def commonheads(self):
396 def commonheads(self):
396 """set of all common heads after changeset bundle push"""
397 """set of all common heads after changeset bundle push"""
397 if self.cgresult:
398 if self.cgresult:
398 return self.futureheads
399 return self.futureheads
399 else:
400 else:
400 return self.fallbackheads
401 return self.fallbackheads
401
402
402 # mapping of message used when pushing bookmark
403 # mapping of message used when pushing bookmark
403 bookmsgmap = {'update': (_("updating bookmark %s\n"),
404 bookmsgmap = {'update': (_("updating bookmark %s\n"),
404 _('updating bookmark %s failed!\n')),
405 _('updating bookmark %s failed!\n')),
405 'export': (_("exporting bookmark %s\n"),
406 'export': (_("exporting bookmark %s\n"),
406 _('exporting bookmark %s failed!\n')),
407 _('exporting bookmark %s failed!\n')),
407 'delete': (_("deleting remote bookmark %s\n"),
408 'delete': (_("deleting remote bookmark %s\n"),
408 _('deleting remote bookmark %s failed!\n')),
409 _('deleting remote bookmark %s failed!\n')),
409 }
410 }
410
411
411
412
412 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
413 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
413 opargs=None):
414 opargs=None):
414 '''Push outgoing changesets (limited by revs) from a local
415 '''Push outgoing changesets (limited by revs) from a local
415 repository to remote. Return an integer:
416 repository to remote. Return an integer:
416 - None means nothing to push
417 - None means nothing to push
417 - 0 means HTTP error
418 - 0 means HTTP error
418 - 1 means we pushed and remote head count is unchanged *or*
419 - 1 means we pushed and remote head count is unchanged *or*
419 we have outgoing changesets but refused to push
420 we have outgoing changesets but refused to push
420 - other values as described by addchangegroup()
421 - other values as described by addchangegroup()
421 '''
422 '''
422 if opargs is None:
423 if opargs is None:
423 opargs = {}
424 opargs = {}
424 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
425 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
425 **opargs)
426 **opargs)
426 if pushop.remote.local():
427 if pushop.remote.local():
427 missing = (set(pushop.repo.requirements)
428 missing = (set(pushop.repo.requirements)
428 - pushop.remote.local().supported)
429 - pushop.remote.local().supported)
429 if missing:
430 if missing:
430 msg = _("required features are not"
431 msg = _("required features are not"
431 " supported in the destination:"
432 " supported in the destination:"
432 " %s") % (', '.join(sorted(missing)))
433 " %s") % (', '.join(sorted(missing)))
433 raise error.Abort(msg)
434 raise error.Abort(msg)
434
435
435 # there are two ways to push to remote repo:
436 # there are two ways to push to remote repo:
436 #
437 #
437 # addchangegroup assumes local user can lock remote
438 # addchangegroup assumes local user can lock remote
438 # repo (local filesystem, old ssh servers).
439 # repo (local filesystem, old ssh servers).
439 #
440 #
440 # unbundle assumes local user cannot lock remote repo (new ssh
441 # unbundle assumes local user cannot lock remote repo (new ssh
441 # servers, http servers).
442 # servers, http servers).
442
443
443 if not pushop.remote.canpush():
444 if not pushop.remote.canpush():
444 raise error.Abort(_("destination does not support push"))
445 raise error.Abort(_("destination does not support push"))
445 # get local lock as we might write phase data
446 # get local lock as we might write phase data
446 localwlock = locallock = None
447 localwlock = locallock = None
447 try:
448 try:
448 # bundle2 push may receive a reply bundle touching bookmarks or other
449 # bundle2 push may receive a reply bundle touching bookmarks or other
449 # things requiring the wlock. Take it now to ensure proper ordering.
450 # things requiring the wlock. Take it now to ensure proper ordering.
450 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
451 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
451 if (not _forcebundle1(pushop)) and maypushback:
452 if (not _forcebundle1(pushop)) and maypushback:
452 localwlock = pushop.repo.wlock()
453 localwlock = pushop.repo.wlock()
453 locallock = pushop.repo.lock()
454 locallock = pushop.repo.lock()
454 pushop.locallocked = True
455 pushop.locallocked = True
455 except IOError as err:
456 except IOError as err:
456 pushop.locallocked = False
457 pushop.locallocked = False
457 if err.errno != errno.EACCES:
458 if err.errno != errno.EACCES:
458 raise
459 raise
459 # source repo cannot be locked.
460 # source repo cannot be locked.
460 # We do not abort the push, but just disable the local phase
461 # We do not abort the push, but just disable the local phase
461 # synchronisation.
462 # synchronisation.
462 msg = 'cannot lock source repository: %s\n' % err
463 msg = 'cannot lock source repository: %s\n' % err
463 pushop.ui.debug(msg)
464 pushop.ui.debug(msg)
464 try:
465 try:
465 if pushop.locallocked:
466 if pushop.locallocked:
466 pushop.trmanager = transactionmanager(pushop.repo,
467 pushop.trmanager = transactionmanager(pushop.repo,
467 'push-response',
468 'push-response',
468 pushop.remote.url())
469 pushop.remote.url())
469 pushop.repo.checkpush(pushop)
470 pushop.repo.checkpush(pushop)
470 lock = None
471 lock = None
471 unbundle = pushop.remote.capable('unbundle')
472 unbundle = pushop.remote.capable('unbundle')
472 if not unbundle:
473 if not unbundle:
473 lock = pushop.remote.lock()
474 lock = pushop.remote.lock()
474 try:
475 try:
475 _pushdiscovery(pushop)
476 _pushdiscovery(pushop)
476 if not _forcebundle1(pushop):
477 if not _forcebundle1(pushop):
477 _pushbundle2(pushop)
478 _pushbundle2(pushop)
478 _pushchangeset(pushop)
479 _pushchangeset(pushop)
479 _pushsyncphase(pushop)
480 _pushsyncphase(pushop)
480 _pushobsolete(pushop)
481 _pushobsolete(pushop)
481 _pushbookmark(pushop)
482 _pushbookmark(pushop)
482 finally:
483 finally:
483 if lock is not None:
484 if lock is not None:
484 lock.release()
485 lock.release()
485 if pushop.trmanager:
486 if pushop.trmanager:
486 pushop.trmanager.close()
487 pushop.trmanager.close()
487 finally:
488 finally:
488 if pushop.trmanager:
489 if pushop.trmanager:
489 pushop.trmanager.release()
490 pushop.trmanager.release()
490 if locallock is not None:
491 if locallock is not None:
491 locallock.release()
492 locallock.release()
492 if localwlock is not None:
493 if localwlock is not None:
493 localwlock.release()
494 localwlock.release()
494
495
495 return pushop
496 return pushop
496
497
497 # list of steps to perform discovery before push
498 # list of steps to perform discovery before push
498 pushdiscoveryorder = []
499 pushdiscoveryorder = []
499
500
500 # Mapping between step name and function
501 # Mapping between step name and function
501 #
502 #
502 # This exists to help extensions wrap steps if necessary
503 # This exists to help extensions wrap steps if necessary
503 pushdiscoverymapping = {}
504 pushdiscoverymapping = {}
504
505
505 def pushdiscovery(stepname):
506 def pushdiscovery(stepname):
506 """decorator for function performing discovery before push
507 """decorator for function performing discovery before push
507
508
508 The function is added to the step -> function mapping and appended to the
509 The function is added to the step -> function mapping and appended to the
509 list of steps. Beware that decorated function will be added in order (this
510 list of steps. Beware that decorated function will be added in order (this
510 may matter).
511 may matter).
511
512
512 You can only use this decorator for a new step, if you want to wrap a step
513 You can only use this decorator for a new step, if you want to wrap a step
513 from an extension, change the pushdiscovery dictionary directly."""
514 from an extension, change the pushdiscovery dictionary directly."""
514 def dec(func):
515 def dec(func):
515 assert stepname not in pushdiscoverymapping
516 assert stepname not in pushdiscoverymapping
516 pushdiscoverymapping[stepname] = func
517 pushdiscoverymapping[stepname] = func
517 pushdiscoveryorder.append(stepname)
518 pushdiscoveryorder.append(stepname)
518 return func
519 return func
519 return dec
520 return dec
520
521
521 def _pushdiscovery(pushop):
522 def _pushdiscovery(pushop):
522 """Run all discovery steps"""
523 """Run all discovery steps"""
523 for stepname in pushdiscoveryorder:
524 for stepname in pushdiscoveryorder:
524 step = pushdiscoverymapping[stepname]
525 step = pushdiscoverymapping[stepname]
525 step(pushop)
526 step(pushop)
526
527
527 @pushdiscovery('changeset')
528 @pushdiscovery('changeset')
528 def _pushdiscoverychangeset(pushop):
529 def _pushdiscoverychangeset(pushop):
529 """discover the changeset that need to be pushed"""
530 """discover the changeset that need to be pushed"""
530 fci = discovery.findcommonincoming
531 fci = discovery.findcommonincoming
531 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
532 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
532 common, inc, remoteheads = commoninc
533 common, inc, remoteheads = commoninc
533 fco = discovery.findcommonoutgoing
534 fco = discovery.findcommonoutgoing
534 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
535 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
535 commoninc=commoninc, force=pushop.force)
536 commoninc=commoninc, force=pushop.force)
536 pushop.outgoing = outgoing
537 pushop.outgoing = outgoing
537 pushop.remoteheads = remoteheads
538 pushop.remoteheads = remoteheads
538 pushop.incoming = inc
539 pushop.incoming = inc
539
540
540 @pushdiscovery('phase')
541 @pushdiscovery('phase')
541 def _pushdiscoveryphase(pushop):
542 def _pushdiscoveryphase(pushop):
542 """discover the phase that needs to be pushed
543 """discover the phase that needs to be pushed
543
544
544 (computed for both success and failure case for changesets push)"""
545 (computed for both success and failure case for changesets push)"""
545 outgoing = pushop.outgoing
546 outgoing = pushop.outgoing
546 unfi = pushop.repo.unfiltered()
547 unfi = pushop.repo.unfiltered()
547 remotephases = pushop.remote.listkeys('phases')
548 remotephases = pushop.remote.listkeys('phases')
548 publishing = remotephases.get('publishing', False)
549 publishing = remotephases.get('publishing', False)
549 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
550 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
550 and remotephases # server supports phases
551 and remotephases # server supports phases
551 and not pushop.outgoing.missing # no changesets to be pushed
552 and not pushop.outgoing.missing # no changesets to be pushed
552 and publishing):
553 and publishing):
553 # When:
554 # When:
554 # - this is a subrepo push
555 # - this is a subrepo push
555 # - and remote support phase
556 # - and remote support phase
556 # - and no changeset are to be pushed
557 # - and no changeset are to be pushed
557 # - and remote is publishing
558 # - and remote is publishing
558 # We may be in issue 3871 case!
559 # We may be in issue 3871 case!
559 # We drop the possible phase synchronisation done by
560 # We drop the possible phase synchronisation done by
560 # courtesy to publish changesets possibly locally draft
561 # courtesy to publish changesets possibly locally draft
561 # on the remote.
562 # on the remote.
562 remotephases = {'publishing': 'True'}
563 remotephases = {'publishing': 'True'}
563 ana = phases.analyzeremotephases(pushop.repo,
564 ana = phases.analyzeremotephases(pushop.repo,
564 pushop.fallbackheads,
565 pushop.fallbackheads,
565 remotephases)
566 remotephases)
566 pheads, droots = ana
567 pheads, droots = ana
567 extracond = ''
568 extracond = ''
568 if not publishing:
569 if not publishing:
569 extracond = ' and public()'
570 extracond = ' and public()'
570 revset = 'heads((%%ln::%%ln) %s)' % extracond
571 revset = 'heads((%%ln::%%ln) %s)' % extracond
571 # Get the list of all revs draft on remote by public here.
572 # Get the list of all revs draft on remote by public here.
572 # XXX Beware that revset break if droots is not strictly
573 # XXX Beware that revset break if droots is not strictly
573 # XXX root we may want to ensure it is but it is costly
574 # XXX root we may want to ensure it is but it is costly
574 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
575 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
575 if not outgoing.missing:
576 if not outgoing.missing:
576 future = fallback
577 future = fallback
577 else:
578 else:
578 # adds changeset we are going to push as draft
579 # adds changeset we are going to push as draft
579 #
580 #
580 # should not be necessary for publishing server, but because of an
581 # should not be necessary for publishing server, but because of an
581 # issue fixed in xxxxx we have to do it anyway.
582 # issue fixed in xxxxx we have to do it anyway.
582 fdroots = list(unfi.set('roots(%ln + %ln::)',
583 fdroots = list(unfi.set('roots(%ln + %ln::)',
583 outgoing.missing, droots))
584 outgoing.missing, droots))
584 fdroots = [f.node() for f in fdroots]
585 fdroots = [f.node() for f in fdroots]
585 future = list(unfi.set(revset, fdroots, pushop.futureheads))
586 future = list(unfi.set(revset, fdroots, pushop.futureheads))
586 pushop.outdatedphases = future
587 pushop.outdatedphases = future
587 pushop.fallbackoutdatedphases = fallback
588 pushop.fallbackoutdatedphases = fallback
588
589
589 @pushdiscovery('obsmarker')
590 @pushdiscovery('obsmarker')
590 def _pushdiscoveryobsmarkers(pushop):
591 def _pushdiscoveryobsmarkers(pushop):
591 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
592 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
592 and pushop.repo.obsstore
593 and pushop.repo.obsstore
593 and 'obsolete' in pushop.remote.listkeys('namespaces')):
594 and 'obsolete' in pushop.remote.listkeys('namespaces')):
594 repo = pushop.repo
595 repo = pushop.repo
595 # very naive computation, that can be quite expensive on big repo.
596 # very naive computation, that can be quite expensive on big repo.
596 # However: evolution is currently slow on them anyway.
597 # However: evolution is currently slow on them anyway.
597 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
598 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
598 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
599 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
599
600
600 @pushdiscovery('bookmarks')
601 @pushdiscovery('bookmarks')
601 def _pushdiscoverybookmarks(pushop):
602 def _pushdiscoverybookmarks(pushop):
602 ui = pushop.ui
603 ui = pushop.ui
603 repo = pushop.repo.unfiltered()
604 repo = pushop.repo.unfiltered()
604 remote = pushop.remote
605 remote = pushop.remote
605 ui.debug("checking for updated bookmarks\n")
606 ui.debug("checking for updated bookmarks\n")
606 ancestors = ()
607 ancestors = ()
607 if pushop.revs:
608 if pushop.revs:
608 revnums = map(repo.changelog.rev, pushop.revs)
609 revnums = map(repo.changelog.rev, pushop.revs)
609 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
610 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
610 remotebookmark = remote.listkeys('bookmarks')
611 remotebookmark = remote.listkeys('bookmarks')
611
612
612 explicit = set([repo._bookmarks.expandname(bookmark)
613 explicit = set([repo._bookmarks.expandname(bookmark)
613 for bookmark in pushop.bookmarks])
614 for bookmark in pushop.bookmarks])
614
615
615 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
616 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
616 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
617 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
617
618
618 def safehex(x):
619 def safehex(x):
619 if x is None:
620 if x is None:
620 return x
621 return x
621 return hex(x)
622 return hex(x)
622
623
623 def hexifycompbookmarks(bookmarks):
624 def hexifycompbookmarks(bookmarks):
624 for b, scid, dcid in bookmarks:
625 for b, scid, dcid in bookmarks:
625 yield b, safehex(scid), safehex(dcid)
626 yield b, safehex(scid), safehex(dcid)
626
627
627 comp = [hexifycompbookmarks(marks) for marks in comp]
628 comp = [hexifycompbookmarks(marks) for marks in comp]
628 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
629 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
629
630
630 for b, scid, dcid in advsrc:
631 for b, scid, dcid in advsrc:
631 if b in explicit:
632 if b in explicit:
632 explicit.remove(b)
633 explicit.remove(b)
633 if not ancestors or repo[scid].rev() in ancestors:
634 if not ancestors or repo[scid].rev() in ancestors:
634 pushop.outbookmarks.append((b, dcid, scid))
635 pushop.outbookmarks.append((b, dcid, scid))
635 # search added bookmark
636 # search added bookmark
636 for b, scid, dcid in addsrc:
637 for b, scid, dcid in addsrc:
637 if b in explicit:
638 if b in explicit:
638 explicit.remove(b)
639 explicit.remove(b)
639 pushop.outbookmarks.append((b, '', scid))
640 pushop.outbookmarks.append((b, '', scid))
640 # search for overwritten bookmark
641 # search for overwritten bookmark
641 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
642 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
642 if b in explicit:
643 if b in explicit:
643 explicit.remove(b)
644 explicit.remove(b)
644 pushop.outbookmarks.append((b, dcid, scid))
645 pushop.outbookmarks.append((b, dcid, scid))
645 # search for bookmark to delete
646 # search for bookmark to delete
646 for b, scid, dcid in adddst:
647 for b, scid, dcid in adddst:
647 if b in explicit:
648 if b in explicit:
648 explicit.remove(b)
649 explicit.remove(b)
649 # treat as "deleted locally"
650 # treat as "deleted locally"
650 pushop.outbookmarks.append((b, dcid, ''))
651 pushop.outbookmarks.append((b, dcid, ''))
651 # identical bookmarks shouldn't get reported
652 # identical bookmarks shouldn't get reported
652 for b, scid, dcid in same:
653 for b, scid, dcid in same:
653 if b in explicit:
654 if b in explicit:
654 explicit.remove(b)
655 explicit.remove(b)
655
656
656 if explicit:
657 if explicit:
657 explicit = sorted(explicit)
658 explicit = sorted(explicit)
658 # we should probably list all of them
659 # we should probably list all of them
659 ui.warn(_('bookmark %s does not exist on the local '
660 ui.warn(_('bookmark %s does not exist on the local '
660 'or remote repository!\n') % explicit[0])
661 'or remote repository!\n') % explicit[0])
661 pushop.bkresult = 2
662 pushop.bkresult = 2
662
663
663 pushop.outbookmarks.sort()
664 pushop.outbookmarks.sort()
664
665
665 def _pushcheckoutgoing(pushop):
666 def _pushcheckoutgoing(pushop):
666 outgoing = pushop.outgoing
667 outgoing = pushop.outgoing
667 unfi = pushop.repo.unfiltered()
668 unfi = pushop.repo.unfiltered()
668 if not outgoing.missing:
669 if not outgoing.missing:
669 # nothing to push
670 # nothing to push
670 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
671 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
671 return False
672 return False
672 # something to push
673 # something to push
673 if not pushop.force:
674 if not pushop.force:
674 # if repo.obsstore == False --> no obsolete
675 # if repo.obsstore == False --> no obsolete
675 # then, save the iteration
676 # then, save the iteration
676 if unfi.obsstore:
677 if unfi.obsstore:
677 # this message are here for 80 char limit reason
678 # this message are here for 80 char limit reason
678 mso = _("push includes obsolete changeset: %s!")
679 mso = _("push includes obsolete changeset: %s!")
679 mst = {"unstable": _("push includes unstable changeset: %s!"),
680 mst = {"unstable": _("push includes unstable changeset: %s!"),
680 "bumped": _("push includes bumped changeset: %s!"),
681 "bumped": _("push includes bumped changeset: %s!"),
681 "divergent": _("push includes divergent changeset: %s!")}
682 "divergent": _("push includes divergent changeset: %s!")}
682 # If we are to push if there is at least one
683 # If we are to push if there is at least one
683 # obsolete or unstable changeset in missing, at
684 # obsolete or unstable changeset in missing, at
684 # least one of the missinghead will be obsolete or
685 # least one of the missinghead will be obsolete or
685 # unstable. So checking heads only is ok
686 # unstable. So checking heads only is ok
686 for node in outgoing.missingheads:
687 for node in outgoing.missingheads:
687 ctx = unfi[node]
688 ctx = unfi[node]
688 if ctx.obsolete():
689 if ctx.obsolete():
689 raise error.Abort(mso % ctx)
690 raise error.Abort(mso % ctx)
690 elif ctx.troubled():
691 elif ctx.troubled():
691 raise error.Abort(mst[ctx.troubles()[0]] % ctx)
692 raise error.Abort(mst[ctx.troubles()[0]] % ctx)
692
693
693 discovery.checkheads(pushop)
694 discovery.checkheads(pushop)
694 return True
695 return True
695
696
696 # List of names of steps to perform for an outgoing bundle2, order matters.
697 # List of names of steps to perform for an outgoing bundle2, order matters.
697 b2partsgenorder = []
698 b2partsgenorder = []
698
699
699 # Mapping between step name and function
700 # Mapping between step name and function
700 #
701 #
701 # This exists to help extensions wrap steps if necessary
702 # This exists to help extensions wrap steps if necessary
702 b2partsgenmapping = {}
703 b2partsgenmapping = {}
703
704
704 def b2partsgenerator(stepname, idx=None):
705 def b2partsgenerator(stepname, idx=None):
705 """decorator for function generating bundle2 part
706 """decorator for function generating bundle2 part
706
707
707 The function is added to the step -> function mapping and appended to the
708 The function is added to the step -> function mapping and appended to the
708 list of steps. Beware that decorated functions will be added in order
709 list of steps. Beware that decorated functions will be added in order
709 (this may matter).
710 (this may matter).
710
711
711 You can only use this decorator for new steps, if you want to wrap a step
712 You can only use this decorator for new steps, if you want to wrap a step
712 from an extension, attack the b2partsgenmapping dictionary directly."""
713 from an extension, attack the b2partsgenmapping dictionary directly."""
713 def dec(func):
714 def dec(func):
714 assert stepname not in b2partsgenmapping
715 assert stepname not in b2partsgenmapping
715 b2partsgenmapping[stepname] = func
716 b2partsgenmapping[stepname] = func
716 if idx is None:
717 if idx is None:
717 b2partsgenorder.append(stepname)
718 b2partsgenorder.append(stepname)
718 else:
719 else:
719 b2partsgenorder.insert(idx, stepname)
720 b2partsgenorder.insert(idx, stepname)
720 return func
721 return func
721 return dec
722 return dec
722
723
723 def _pushb2ctxcheckheads(pushop, bundler):
724 def _pushb2ctxcheckheads(pushop, bundler):
724 """Generate race condition checking parts
725 """Generate race condition checking parts
725
726
726 Exists as an independent function to aid extensions
727 Exists as an independent function to aid extensions
727 """
728 """
728 # * 'force' do not check for push race,
729 # * 'force' do not check for push race,
729 # * if we don't push anything, there are nothing to check.
730 # * if we don't push anything, there are nothing to check.
730 if not pushop.force and pushop.outgoing.missingheads:
731 if not pushop.force and pushop.outgoing.missingheads:
731 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
732 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
732 if not allowunrelated:
733 if not allowunrelated:
733 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
734 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
734 else:
735 else:
735 affected = set()
736 affected = set()
736 for branch, heads in pushop.pushbranchmap.iteritems():
737 for branch, heads in pushop.pushbranchmap.iteritems():
737 remoteheads, newheads, unsyncedheads, discardedheads = heads
738 remoteheads, newheads, unsyncedheads, discardedheads = heads
738 if remoteheads is not None:
739 if remoteheads is not None:
739 remote = set(remoteheads)
740 remote = set(remoteheads)
740 affected |= set(discardedheads) & remote
741 affected |= set(discardedheads) & remote
741 affected |= remote - set(newheads)
742 affected |= remote - set(newheads)
742 if affected:
743 if affected:
743 data = iter(sorted(affected))
744 data = iter(sorted(affected))
744 bundler.newpart('check:updated-heads', data=data)
745 bundler.newpart('check:updated-heads', data=data)
745
746
746 @b2partsgenerator('changeset')
747 @b2partsgenerator('changeset')
747 def _pushb2ctx(pushop, bundler):
748 def _pushb2ctx(pushop, bundler):
748 """handle changegroup push through bundle2
749 """handle changegroup push through bundle2
749
750
750 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
751 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
751 """
752 """
752 if 'changesets' in pushop.stepsdone:
753 if 'changesets' in pushop.stepsdone:
753 return
754 return
754 pushop.stepsdone.add('changesets')
755 pushop.stepsdone.add('changesets')
755 # Send known heads to the server for race detection.
756 # Send known heads to the server for race detection.
756 if not _pushcheckoutgoing(pushop):
757 if not _pushcheckoutgoing(pushop):
757 return
758 return
758 pushop.repo.prepushoutgoinghooks(pushop)
759 pushop.repo.prepushoutgoinghooks(pushop)
759
760
760 _pushb2ctxcheckheads(pushop, bundler)
761 _pushb2ctxcheckheads(pushop, bundler)
761
762
762 b2caps = bundle2.bundle2caps(pushop.remote)
763 b2caps = bundle2.bundle2caps(pushop.remote)
763 version = '01'
764 version = '01'
764 cgversions = b2caps.get('changegroup')
765 cgversions = b2caps.get('changegroup')
765 if cgversions: # 3.1 and 3.2 ship with an empty value
766 if cgversions: # 3.1 and 3.2 ship with an empty value
766 cgversions = [v for v in cgversions
767 cgversions = [v for v in cgversions
767 if v in changegroup.supportedoutgoingversions(
768 if v in changegroup.supportedoutgoingversions(
768 pushop.repo)]
769 pushop.repo)]
769 if not cgversions:
770 if not cgversions:
770 raise ValueError(_('no common changegroup version'))
771 raise ValueError(_('no common changegroup version'))
771 version = max(cgversions)
772 version = max(cgversions)
772 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
773 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
773 pushop.outgoing,
774 pushop.outgoing,
774 version=version)
775 version=version)
775 cgpart = bundler.newpart('changegroup', data=cg)
776 cgpart = bundler.newpart('changegroup', data=cg)
776 if cgversions:
777 if cgversions:
777 cgpart.addparam('version', version)
778 cgpart.addparam('version', version)
778 if 'treemanifest' in pushop.repo.requirements:
779 if 'treemanifest' in pushop.repo.requirements:
779 cgpart.addparam('treemanifest', '1')
780 cgpart.addparam('treemanifest', '1')
780 def handlereply(op):
781 def handlereply(op):
781 """extract addchangegroup returns from server reply"""
782 """extract addchangegroup returns from server reply"""
782 cgreplies = op.records.getreplies(cgpart.id)
783 cgreplies = op.records.getreplies(cgpart.id)
783 assert len(cgreplies['changegroup']) == 1
784 assert len(cgreplies['changegroup']) == 1
784 pushop.cgresult = cgreplies['changegroup'][0]['return']
785 pushop.cgresult = cgreplies['changegroup'][0]['return']
785 return handlereply
786 return handlereply
786
787
787 @b2partsgenerator('phase')
788 @b2partsgenerator('phase')
788 def _pushb2phases(pushop, bundler):
789 def _pushb2phases(pushop, bundler):
789 """handle phase push through bundle2"""
790 """handle phase push through bundle2"""
790 if 'phases' in pushop.stepsdone:
791 if 'phases' in pushop.stepsdone:
791 return
792 return
792 b2caps = bundle2.bundle2caps(pushop.remote)
793 b2caps = bundle2.bundle2caps(pushop.remote)
793 if not 'pushkey' in b2caps:
794 if not 'pushkey' in b2caps:
794 return
795 return
795 pushop.stepsdone.add('phases')
796 pushop.stepsdone.add('phases')
796 part2node = []
797 part2node = []
797
798
798 def handlefailure(pushop, exc):
799 def handlefailure(pushop, exc):
799 targetid = int(exc.partid)
800 targetid = int(exc.partid)
800 for partid, node in part2node:
801 for partid, node in part2node:
801 if partid == targetid:
802 if partid == targetid:
802 raise error.Abort(_('updating %s to public failed') % node)
803 raise error.Abort(_('updating %s to public failed') % node)
803
804
804 enc = pushkey.encode
805 enc = pushkey.encode
805 for newremotehead in pushop.outdatedphases:
806 for newremotehead in pushop.outdatedphases:
806 part = bundler.newpart('pushkey')
807 part = bundler.newpart('pushkey')
807 part.addparam('namespace', enc('phases'))
808 part.addparam('namespace', enc('phases'))
808 part.addparam('key', enc(newremotehead.hex()))
809 part.addparam('key', enc(newremotehead.hex()))
809 part.addparam('old', enc(str(phases.draft)))
810 part.addparam('old', enc(str(phases.draft)))
810 part.addparam('new', enc(str(phases.public)))
811 part.addparam('new', enc(str(phases.public)))
811 part2node.append((part.id, newremotehead))
812 part2node.append((part.id, newremotehead))
812 pushop.pkfailcb[part.id] = handlefailure
813 pushop.pkfailcb[part.id] = handlefailure
813
814
814 def handlereply(op):
815 def handlereply(op):
815 for partid, node in part2node:
816 for partid, node in part2node:
816 partrep = op.records.getreplies(partid)
817 partrep = op.records.getreplies(partid)
817 results = partrep['pushkey']
818 results = partrep['pushkey']
818 assert len(results) <= 1
819 assert len(results) <= 1
819 msg = None
820 msg = None
820 if not results:
821 if not results:
821 msg = _('server ignored update of %s to public!\n') % node
822 msg = _('server ignored update of %s to public!\n') % node
822 elif not int(results[0]['return']):
823 elif not int(results[0]['return']):
823 msg = _('updating %s to public failed!\n') % node
824 msg = _('updating %s to public failed!\n') % node
824 if msg is not None:
825 if msg is not None:
825 pushop.ui.warn(msg)
826 pushop.ui.warn(msg)
826 return handlereply
827 return handlereply
827
828
828 @b2partsgenerator('obsmarkers')
829 @b2partsgenerator('obsmarkers')
829 def _pushb2obsmarkers(pushop, bundler):
830 def _pushb2obsmarkers(pushop, bundler):
830 if 'obsmarkers' in pushop.stepsdone:
831 if 'obsmarkers' in pushop.stepsdone:
831 return
832 return
832 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
833 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
833 if obsolete.commonversion(remoteversions) is None:
834 if obsolete.commonversion(remoteversions) is None:
834 return
835 return
835 pushop.stepsdone.add('obsmarkers')
836 pushop.stepsdone.add('obsmarkers')
836 if pushop.outobsmarkers:
837 if pushop.outobsmarkers:
837 markers = sorted(pushop.outobsmarkers)
838 markers = sorted(pushop.outobsmarkers)
838 bundle2.buildobsmarkerspart(bundler, markers)
839 bundle2.buildobsmarkerspart(bundler, markers)
839
840
840 @b2partsgenerator('bookmarks')
841 @b2partsgenerator('bookmarks')
841 def _pushb2bookmarks(pushop, bundler):
842 def _pushb2bookmarks(pushop, bundler):
842 """handle bookmark push through bundle2"""
843 """handle bookmark push through bundle2"""
843 if 'bookmarks' in pushop.stepsdone:
844 if 'bookmarks' in pushop.stepsdone:
844 return
845 return
845 b2caps = bundle2.bundle2caps(pushop.remote)
846 b2caps = bundle2.bundle2caps(pushop.remote)
846 if 'pushkey' not in b2caps:
847 if 'pushkey' not in b2caps:
847 return
848 return
848 pushop.stepsdone.add('bookmarks')
849 pushop.stepsdone.add('bookmarks')
849 part2book = []
850 part2book = []
850 enc = pushkey.encode
851 enc = pushkey.encode
851
852
852 def handlefailure(pushop, exc):
853 def handlefailure(pushop, exc):
853 targetid = int(exc.partid)
854 targetid = int(exc.partid)
854 for partid, book, action in part2book:
855 for partid, book, action in part2book:
855 if partid == targetid:
856 if partid == targetid:
856 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
857 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
857 # we should not be called for part we did not generated
858 # we should not be called for part we did not generated
858 assert False
859 assert False
859
860
860 for book, old, new in pushop.outbookmarks:
861 for book, old, new in pushop.outbookmarks:
861 part = bundler.newpart('pushkey')
862 part = bundler.newpart('pushkey')
862 part.addparam('namespace', enc('bookmarks'))
863 part.addparam('namespace', enc('bookmarks'))
863 part.addparam('key', enc(book))
864 part.addparam('key', enc(book))
864 part.addparam('old', enc(old))
865 part.addparam('old', enc(old))
865 part.addparam('new', enc(new))
866 part.addparam('new', enc(new))
866 action = 'update'
867 action = 'update'
867 if not old:
868 if not old:
868 action = 'export'
869 action = 'export'
869 elif not new:
870 elif not new:
870 action = 'delete'
871 action = 'delete'
871 part2book.append((part.id, book, action))
872 part2book.append((part.id, book, action))
872 pushop.pkfailcb[part.id] = handlefailure
873 pushop.pkfailcb[part.id] = handlefailure
873
874
874 def handlereply(op):
875 def handlereply(op):
875 ui = pushop.ui
876 ui = pushop.ui
876 for partid, book, action in part2book:
877 for partid, book, action in part2book:
877 partrep = op.records.getreplies(partid)
878 partrep = op.records.getreplies(partid)
878 results = partrep['pushkey']
879 results = partrep['pushkey']
879 assert len(results) <= 1
880 assert len(results) <= 1
880 if not results:
881 if not results:
881 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
882 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
882 else:
883 else:
883 ret = int(results[0]['return'])
884 ret = int(results[0]['return'])
884 if ret:
885 if ret:
885 ui.status(bookmsgmap[action][0] % book)
886 ui.status(bookmsgmap[action][0] % book)
886 else:
887 else:
887 ui.warn(bookmsgmap[action][1] % book)
888 ui.warn(bookmsgmap[action][1] % book)
888 if pushop.bkresult is not None:
889 if pushop.bkresult is not None:
889 pushop.bkresult = 1
890 pushop.bkresult = 1
890 return handlereply
891 return handlereply
891
892
892
893
893 def _pushbundle2(pushop):
894 def _pushbundle2(pushop):
894 """push data to the remote using bundle2
895 """push data to the remote using bundle2
895
896
896 The only currently supported type of data is changegroup but this will
897 The only currently supported type of data is changegroup but this will
897 evolve in the future."""
898 evolve in the future."""
898 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
899 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
899 pushback = (pushop.trmanager
900 pushback = (pushop.trmanager
900 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
901 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
901
902
902 # create reply capability
903 # create reply capability
903 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
904 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
904 allowpushback=pushback))
905 allowpushback=pushback))
905 bundler.newpart('replycaps', data=capsblob)
906 bundler.newpart('replycaps', data=capsblob)
906 replyhandlers = []
907 replyhandlers = []
907 for partgenname in b2partsgenorder:
908 for partgenname in b2partsgenorder:
908 partgen = b2partsgenmapping[partgenname]
909 partgen = b2partsgenmapping[partgenname]
909 ret = partgen(pushop, bundler)
910 ret = partgen(pushop, bundler)
910 if callable(ret):
911 if callable(ret):
911 replyhandlers.append(ret)
912 replyhandlers.append(ret)
912 # do not push if nothing to push
913 # do not push if nothing to push
913 if bundler.nbparts <= 1:
914 if bundler.nbparts <= 1:
914 return
915 return
915 stream = util.chunkbuffer(bundler.getchunks())
916 stream = util.chunkbuffer(bundler.getchunks())
916 try:
917 try:
917 try:
918 try:
918 reply = pushop.remote.unbundle(
919 reply = pushop.remote.unbundle(
919 stream, ['force'], pushop.remote.url())
920 stream, ['force'], pushop.remote.url())
920 except error.BundleValueError as exc:
921 except error.BundleValueError as exc:
921 raise error.Abort(_('missing support for %s') % exc)
922 raise error.Abort(_('missing support for %s') % exc)
922 try:
923 try:
923 trgetter = None
924 trgetter = None
924 if pushback:
925 if pushback:
925 trgetter = pushop.trmanager.transaction
926 trgetter = pushop.trmanager.transaction
926 op = bundle2.processbundle(pushop.repo, reply, trgetter)
927 op = bundle2.processbundle(pushop.repo, reply, trgetter)
927 except error.BundleValueError as exc:
928 except error.BundleValueError as exc:
928 raise error.Abort(_('missing support for %s') % exc)
929 raise error.Abort(_('missing support for %s') % exc)
929 except bundle2.AbortFromPart as exc:
930 except bundle2.AbortFromPart as exc:
930 pushop.ui.status(_('remote: %s\n') % exc)
931 pushop.ui.status(_('remote: %s\n') % exc)
931 if exc.hint is not None:
932 if exc.hint is not None:
932 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
933 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
933 raise error.Abort(_('push failed on remote'))
934 raise error.Abort(_('push failed on remote'))
934 except error.PushkeyFailed as exc:
935 except error.PushkeyFailed as exc:
935 partid = int(exc.partid)
936 partid = int(exc.partid)
936 if partid not in pushop.pkfailcb:
937 if partid not in pushop.pkfailcb:
937 raise
938 raise
938 pushop.pkfailcb[partid](pushop, exc)
939 pushop.pkfailcb[partid](pushop, exc)
939 for rephand in replyhandlers:
940 for rephand in replyhandlers:
940 rephand(op)
941 rephand(op)
941
942
942 def _pushchangeset(pushop):
943 def _pushchangeset(pushop):
943 """Make the actual push of changeset bundle to remote repo"""
944 """Make the actual push of changeset bundle to remote repo"""
944 if 'changesets' in pushop.stepsdone:
945 if 'changesets' in pushop.stepsdone:
945 return
946 return
946 pushop.stepsdone.add('changesets')
947 pushop.stepsdone.add('changesets')
947 if not _pushcheckoutgoing(pushop):
948 if not _pushcheckoutgoing(pushop):
948 return
949 return
949 pushop.repo.prepushoutgoinghooks(pushop)
950 pushop.repo.prepushoutgoinghooks(pushop)
950 outgoing = pushop.outgoing
951 outgoing = pushop.outgoing
951 unbundle = pushop.remote.capable('unbundle')
952 unbundle = pushop.remote.capable('unbundle')
952 # TODO: get bundlecaps from remote
953 # TODO: get bundlecaps from remote
953 bundlecaps = None
954 bundlecaps = None
954 # create a changegroup from local
955 # create a changegroup from local
955 if pushop.revs is None and not (outgoing.excluded
956 if pushop.revs is None and not (outgoing.excluded
956 or pushop.repo.changelog.filteredrevs):
957 or pushop.repo.changelog.filteredrevs):
957 # push everything,
958 # push everything,
958 # use the fast path, no race possible on push
959 # use the fast path, no race possible on push
959 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
960 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
960 cg = changegroup.getsubset(pushop.repo,
961 cg = changegroup.getsubset(pushop.repo,
961 outgoing,
962 outgoing,
962 bundler,
963 bundler,
963 'push',
964 'push',
964 fastpath=True)
965 fastpath=True)
965 else:
966 else:
966 cg = changegroup.getchangegroup(pushop.repo, 'push', outgoing,
967 cg = changegroup.getchangegroup(pushop.repo, 'push', outgoing,
967 bundlecaps=bundlecaps)
968 bundlecaps=bundlecaps)
968
969
969 # apply changegroup to remote
970 # apply changegroup to remote
970 if unbundle:
971 if unbundle:
971 # local repo finds heads on server, finds out what
972 # local repo finds heads on server, finds out what
972 # revs it must push. once revs transferred, if server
973 # revs it must push. once revs transferred, if server
973 # finds it has different heads (someone else won
974 # finds it has different heads (someone else won
974 # commit/push race), server aborts.
975 # commit/push race), server aborts.
975 if pushop.force:
976 if pushop.force:
976 remoteheads = ['force']
977 remoteheads = ['force']
977 else:
978 else:
978 remoteheads = pushop.remoteheads
979 remoteheads = pushop.remoteheads
979 # ssh: return remote's addchangegroup()
980 # ssh: return remote's addchangegroup()
980 # http: return remote's addchangegroup() or 0 for error
981 # http: return remote's addchangegroup() or 0 for error
981 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
982 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
982 pushop.repo.url())
983 pushop.repo.url())
983 else:
984 else:
984 # we return an integer indicating remote head count
985 # we return an integer indicating remote head count
985 # change
986 # change
986 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
987 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
987 pushop.repo.url())
988 pushop.repo.url())
988
989
989 def _pushsyncphase(pushop):
990 def _pushsyncphase(pushop):
990 """synchronise phase information locally and remotely"""
991 """synchronise phase information locally and remotely"""
991 cheads = pushop.commonheads
992 cheads = pushop.commonheads
992 # even when we don't push, exchanging phase data is useful
993 # even when we don't push, exchanging phase data is useful
993 remotephases = pushop.remote.listkeys('phases')
994 remotephases = pushop.remote.listkeys('phases')
994 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
995 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
995 and remotephases # server supports phases
996 and remotephases # server supports phases
996 and pushop.cgresult is None # nothing was pushed
997 and pushop.cgresult is None # nothing was pushed
997 and remotephases.get('publishing', False)):
998 and remotephases.get('publishing', False)):
998 # When:
999 # When:
999 # - this is a subrepo push
1000 # - this is a subrepo push
1000 # - and remote support phase
1001 # - and remote support phase
1001 # - and no changeset was pushed
1002 # - and no changeset was pushed
1002 # - and remote is publishing
1003 # - and remote is publishing
1003 # We may be in issue 3871 case!
1004 # We may be in issue 3871 case!
1004 # We drop the possible phase synchronisation done by
1005 # We drop the possible phase synchronisation done by
1005 # courtesy to publish changesets possibly locally draft
1006 # courtesy to publish changesets possibly locally draft
1006 # on the remote.
1007 # on the remote.
1007 remotephases = {'publishing': 'True'}
1008 remotephases = {'publishing': 'True'}
1008 if not remotephases: # old server or public only reply from non-publishing
1009 if not remotephases: # old server or public only reply from non-publishing
1009 _localphasemove(pushop, cheads)
1010 _localphasemove(pushop, cheads)
1010 # don't push any phase data as there is nothing to push
1011 # don't push any phase data as there is nothing to push
1011 else:
1012 else:
1012 ana = phases.analyzeremotephases(pushop.repo, cheads,
1013 ana = phases.analyzeremotephases(pushop.repo, cheads,
1013 remotephases)
1014 remotephases)
1014 pheads, droots = ana
1015 pheads, droots = ana
1015 ### Apply remote phase on local
1016 ### Apply remote phase on local
1016 if remotephases.get('publishing', False):
1017 if remotephases.get('publishing', False):
1017 _localphasemove(pushop, cheads)
1018 _localphasemove(pushop, cheads)
1018 else: # publish = False
1019 else: # publish = False
1019 _localphasemove(pushop, pheads)
1020 _localphasemove(pushop, pheads)
1020 _localphasemove(pushop, cheads, phases.draft)
1021 _localphasemove(pushop, cheads, phases.draft)
1021 ### Apply local phase on remote
1022 ### Apply local phase on remote
1022
1023
1023 if pushop.cgresult:
1024 if pushop.cgresult:
1024 if 'phases' in pushop.stepsdone:
1025 if 'phases' in pushop.stepsdone:
1025 # phases already pushed though bundle2
1026 # phases already pushed though bundle2
1026 return
1027 return
1027 outdated = pushop.outdatedphases
1028 outdated = pushop.outdatedphases
1028 else:
1029 else:
1029 outdated = pushop.fallbackoutdatedphases
1030 outdated = pushop.fallbackoutdatedphases
1030
1031
1031 pushop.stepsdone.add('phases')
1032 pushop.stepsdone.add('phases')
1032
1033
1033 # filter heads already turned public by the push
1034 # filter heads already turned public by the push
1034 outdated = [c for c in outdated if c.node() not in pheads]
1035 outdated = [c for c in outdated if c.node() not in pheads]
1035 # fallback to independent pushkey command
1036 # fallback to independent pushkey command
1036 for newremotehead in outdated:
1037 for newremotehead in outdated:
1037 r = pushop.remote.pushkey('phases',
1038 r = pushop.remote.pushkey('phases',
1038 newremotehead.hex(),
1039 newremotehead.hex(),
1039 str(phases.draft),
1040 str(phases.draft),
1040 str(phases.public))
1041 str(phases.public))
1041 if not r:
1042 if not r:
1042 pushop.ui.warn(_('updating %s to public failed!\n')
1043 pushop.ui.warn(_('updating %s to public failed!\n')
1043 % newremotehead)
1044 % newremotehead)
1044
1045
1045 def _localphasemove(pushop, nodes, phase=phases.public):
1046 def _localphasemove(pushop, nodes, phase=phases.public):
1046 """move <nodes> to <phase> in the local source repo"""
1047 """move <nodes> to <phase> in the local source repo"""
1047 if pushop.trmanager:
1048 if pushop.trmanager:
1048 phases.advanceboundary(pushop.repo,
1049 phases.advanceboundary(pushop.repo,
1049 pushop.trmanager.transaction(),
1050 pushop.trmanager.transaction(),
1050 phase,
1051 phase,
1051 nodes)
1052 nodes)
1052 else:
1053 else:
1053 # repo is not locked, do not change any phases!
1054 # repo is not locked, do not change any phases!
1054 # Informs the user that phases should have been moved when
1055 # Informs the user that phases should have been moved when
1055 # applicable.
1056 # applicable.
1056 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1057 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1057 phasestr = phases.phasenames[phase]
1058 phasestr = phases.phasenames[phase]
1058 if actualmoves:
1059 if actualmoves:
1059 pushop.ui.status(_('cannot lock source repo, skipping '
1060 pushop.ui.status(_('cannot lock source repo, skipping '
1060 'local %s phase update\n') % phasestr)
1061 'local %s phase update\n') % phasestr)
1061
1062
1062 def _pushobsolete(pushop):
1063 def _pushobsolete(pushop):
1063 """utility function to push obsolete markers to a remote"""
1064 """utility function to push obsolete markers to a remote"""
1064 if 'obsmarkers' in pushop.stepsdone:
1065 if 'obsmarkers' in pushop.stepsdone:
1065 return
1066 return
1066 repo = pushop.repo
1067 repo = pushop.repo
1067 remote = pushop.remote
1068 remote = pushop.remote
1068 pushop.stepsdone.add('obsmarkers')
1069 pushop.stepsdone.add('obsmarkers')
1069 if pushop.outobsmarkers:
1070 if pushop.outobsmarkers:
1070 pushop.ui.debug('try to push obsolete markers to remote\n')
1071 pushop.ui.debug('try to push obsolete markers to remote\n')
1071 rslts = []
1072 rslts = []
1072 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1073 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1073 for key in sorted(remotedata, reverse=True):
1074 for key in sorted(remotedata, reverse=True):
1074 # reverse sort to ensure we end with dump0
1075 # reverse sort to ensure we end with dump0
1075 data = remotedata[key]
1076 data = remotedata[key]
1076 rslts.append(remote.pushkey('obsolete', key, '', data))
1077 rslts.append(remote.pushkey('obsolete', key, '', data))
1077 if [r for r in rslts if not r]:
1078 if [r for r in rslts if not r]:
1078 msg = _('failed to push some obsolete markers!\n')
1079 msg = _('failed to push some obsolete markers!\n')
1079 repo.ui.warn(msg)
1080 repo.ui.warn(msg)
1080
1081
1081 def _pushbookmark(pushop):
1082 def _pushbookmark(pushop):
1082 """Update bookmark position on remote"""
1083 """Update bookmark position on remote"""
1083 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1084 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1084 return
1085 return
1085 pushop.stepsdone.add('bookmarks')
1086 pushop.stepsdone.add('bookmarks')
1086 ui = pushop.ui
1087 ui = pushop.ui
1087 remote = pushop.remote
1088 remote = pushop.remote
1088
1089
1089 for b, old, new in pushop.outbookmarks:
1090 for b, old, new in pushop.outbookmarks:
1090 action = 'update'
1091 action = 'update'
1091 if not old:
1092 if not old:
1092 action = 'export'
1093 action = 'export'
1093 elif not new:
1094 elif not new:
1094 action = 'delete'
1095 action = 'delete'
1095 if remote.pushkey('bookmarks', b, old, new):
1096 if remote.pushkey('bookmarks', b, old, new):
1096 ui.status(bookmsgmap[action][0] % b)
1097 ui.status(bookmsgmap[action][0] % b)
1097 else:
1098 else:
1098 ui.warn(bookmsgmap[action][1] % b)
1099 ui.warn(bookmsgmap[action][1] % b)
1099 # discovery can have set the value form invalid entry
1100 # discovery can have set the value form invalid entry
1100 if pushop.bkresult is not None:
1101 if pushop.bkresult is not None:
1101 pushop.bkresult = 1
1102 pushop.bkresult = 1
1102
1103
1103 class pulloperation(object):
1104 class pulloperation(object):
1104 """A object that represent a single pull operation
1105 """A object that represent a single pull operation
1105
1106
1106 It purpose is to carry pull related state and very common operation.
1107 It purpose is to carry pull related state and very common operation.
1107
1108
1108 A new should be created at the beginning of each pull and discarded
1109 A new should be created at the beginning of each pull and discarded
1109 afterward.
1110 afterward.
1110 """
1111 """
1111
1112
1112 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1113 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1113 remotebookmarks=None, streamclonerequested=None):
1114 remotebookmarks=None, streamclonerequested=None):
1114 # repo we pull into
1115 # repo we pull into
1115 self.repo = repo
1116 self.repo = repo
1116 # repo we pull from
1117 # repo we pull from
1117 self.remote = remote
1118 self.remote = remote
1118 # revision we try to pull (None is "all")
1119 # revision we try to pull (None is "all")
1119 self.heads = heads
1120 self.heads = heads
1120 # bookmark pulled explicitly
1121 # bookmark pulled explicitly
1121 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1122 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1122 for bookmark in bookmarks]
1123 for bookmark in bookmarks]
1123 # do we force pull?
1124 # do we force pull?
1124 self.force = force
1125 self.force = force
1125 # whether a streaming clone was requested
1126 # whether a streaming clone was requested
1126 self.streamclonerequested = streamclonerequested
1127 self.streamclonerequested = streamclonerequested
1127 # transaction manager
1128 # transaction manager
1128 self.trmanager = None
1129 self.trmanager = None
1129 # set of common changeset between local and remote before pull
1130 # set of common changeset between local and remote before pull
1130 self.common = None
1131 self.common = None
1131 # set of pulled head
1132 # set of pulled head
1132 self.rheads = None
1133 self.rheads = None
1133 # list of missing changeset to fetch remotely
1134 # list of missing changeset to fetch remotely
1134 self.fetch = None
1135 self.fetch = None
1135 # remote bookmarks data
1136 # remote bookmarks data
1136 self.remotebookmarks = remotebookmarks
1137 self.remotebookmarks = remotebookmarks
1137 # result of changegroup pulling (used as return code by pull)
1138 # result of changegroup pulling (used as return code by pull)
1138 self.cgresult = None
1139 self.cgresult = None
1139 # list of step already done
1140 # list of step already done
1140 self.stepsdone = set()
1141 self.stepsdone = set()
1141 # Whether we attempted a clone from pre-generated bundles.
1142 # Whether we attempted a clone from pre-generated bundles.
1142 self.clonebundleattempted = False
1143 self.clonebundleattempted = False
1143
1144
1144 @util.propertycache
1145 @util.propertycache
1145 def pulledsubset(self):
1146 def pulledsubset(self):
1146 """heads of the set of changeset target by the pull"""
1147 """heads of the set of changeset target by the pull"""
1147 # compute target subset
1148 # compute target subset
1148 if self.heads is None:
1149 if self.heads is None:
1149 # We pulled every thing possible
1150 # We pulled every thing possible
1150 # sync on everything common
1151 # sync on everything common
1151 c = set(self.common)
1152 c = set(self.common)
1152 ret = list(self.common)
1153 ret = list(self.common)
1153 for n in self.rheads:
1154 for n in self.rheads:
1154 if n not in c:
1155 if n not in c:
1155 ret.append(n)
1156 ret.append(n)
1156 return ret
1157 return ret
1157 else:
1158 else:
1158 # We pulled a specific subset
1159 # We pulled a specific subset
1159 # sync on this subset
1160 # sync on this subset
1160 return self.heads
1161 return self.heads
1161
1162
1162 @util.propertycache
1163 @util.propertycache
1163 def canusebundle2(self):
1164 def canusebundle2(self):
1164 return not _forcebundle1(self)
1165 return not _forcebundle1(self)
1165
1166
1166 @util.propertycache
1167 @util.propertycache
1167 def remotebundle2caps(self):
1168 def remotebundle2caps(self):
1168 return bundle2.bundle2caps(self.remote)
1169 return bundle2.bundle2caps(self.remote)
1169
1170
1170 def gettransaction(self):
1171 def gettransaction(self):
1171 # deprecated; talk to trmanager directly
1172 # deprecated; talk to trmanager directly
1172 return self.trmanager.transaction()
1173 return self.trmanager.transaction()
1173
1174
1174 class transactionmanager(object):
1175 class transactionmanager(object):
1175 """An object to manage the life cycle of a transaction
1176 """An object to manage the life cycle of a transaction
1176
1177
1177 It creates the transaction on demand and calls the appropriate hooks when
1178 It creates the transaction on demand and calls the appropriate hooks when
1178 closing the transaction."""
1179 closing the transaction."""
1179 def __init__(self, repo, source, url):
1180 def __init__(self, repo, source, url):
1180 self.repo = repo
1181 self.repo = repo
1181 self.source = source
1182 self.source = source
1182 self.url = url
1183 self.url = url
1183 self._tr = None
1184 self._tr = None
1184
1185
1185 def transaction(self):
1186 def transaction(self):
1186 """Return an open transaction object, constructing if necessary"""
1187 """Return an open transaction object, constructing if necessary"""
1187 if not self._tr:
1188 if not self._tr:
1188 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1189 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1189 self._tr = self.repo.transaction(trname)
1190 self._tr = self.repo.transaction(trname)
1190 self._tr.hookargs['source'] = self.source
1191 self._tr.hookargs['source'] = self.source
1191 self._tr.hookargs['url'] = self.url
1192 self._tr.hookargs['url'] = self.url
1192 return self._tr
1193 return self._tr
1193
1194
1194 def close(self):
1195 def close(self):
1195 """close transaction if created"""
1196 """close transaction if created"""
1196 if self._tr is not None:
1197 if self._tr is not None:
1197 self._tr.close()
1198 self._tr.close()
1198
1199
1199 def release(self):
1200 def release(self):
1200 """release transaction if created"""
1201 """release transaction if created"""
1201 if self._tr is not None:
1202 if self._tr is not None:
1202 self._tr.release()
1203 self._tr.release()
1203
1204
1204 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1205 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1205 streamclonerequested=None):
1206 streamclonerequested=None):
1206 """Fetch repository data from a remote.
1207 """Fetch repository data from a remote.
1207
1208
1208 This is the main function used to retrieve data from a remote repository.
1209 This is the main function used to retrieve data from a remote repository.
1209
1210
1210 ``repo`` is the local repository to clone into.
1211 ``repo`` is the local repository to clone into.
1211 ``remote`` is a peer instance.
1212 ``remote`` is a peer instance.
1212 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1213 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1213 default) means to pull everything from the remote.
1214 default) means to pull everything from the remote.
1214 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1215 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1215 default, all remote bookmarks are pulled.
1216 default, all remote bookmarks are pulled.
1216 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1217 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1217 initialization.
1218 initialization.
1218 ``streamclonerequested`` is a boolean indicating whether a "streaming
1219 ``streamclonerequested`` is a boolean indicating whether a "streaming
1219 clone" is requested. A "streaming clone" is essentially a raw file copy
1220 clone" is requested. A "streaming clone" is essentially a raw file copy
1220 of revlogs from the server. This only works when the local repository is
1221 of revlogs from the server. This only works when the local repository is
1221 empty. The default value of ``None`` means to respect the server
1222 empty. The default value of ``None`` means to respect the server
1222 configuration for preferring stream clones.
1223 configuration for preferring stream clones.
1223
1224
1224 Returns the ``pulloperation`` created for this pull.
1225 Returns the ``pulloperation`` created for this pull.
1225 """
1226 """
1226 if opargs is None:
1227 if opargs is None:
1227 opargs = {}
1228 opargs = {}
1228 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1229 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1229 streamclonerequested=streamclonerequested, **opargs)
1230 streamclonerequested=streamclonerequested, **opargs)
1230 if pullop.remote.local():
1231 if pullop.remote.local():
1231 missing = set(pullop.remote.requirements) - pullop.repo.supported
1232 missing = set(pullop.remote.requirements) - pullop.repo.supported
1232 if missing:
1233 if missing:
1233 msg = _("required features are not"
1234 msg = _("required features are not"
1234 " supported in the destination:"
1235 " supported in the destination:"
1235 " %s") % (', '.join(sorted(missing)))
1236 " %s") % (', '.join(sorted(missing)))
1236 raise error.Abort(msg)
1237 raise error.Abort(msg)
1237
1238
1238 wlock = lock = None
1239 wlock = lock = None
1239 try:
1240 try:
1240 wlock = pullop.repo.wlock()
1241 wlock = pullop.repo.wlock()
1241 lock = pullop.repo.lock()
1242 lock = pullop.repo.lock()
1242 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1243 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1243 streamclone.maybeperformlegacystreamclone(pullop)
1244 streamclone.maybeperformlegacystreamclone(pullop)
1244 # This should ideally be in _pullbundle2(). However, it needs to run
1245 # This should ideally be in _pullbundle2(). However, it needs to run
1245 # before discovery to avoid extra work.
1246 # before discovery to avoid extra work.
1246 _maybeapplyclonebundle(pullop)
1247 _maybeapplyclonebundle(pullop)
1247 _pulldiscovery(pullop)
1248 _pulldiscovery(pullop)
1248 if pullop.canusebundle2:
1249 if pullop.canusebundle2:
1249 _pullbundle2(pullop)
1250 _pullbundle2(pullop)
1250 _pullchangeset(pullop)
1251 _pullchangeset(pullop)
1251 _pullphase(pullop)
1252 _pullphase(pullop)
1252 _pullbookmarks(pullop)
1253 _pullbookmarks(pullop)
1253 _pullobsolete(pullop)
1254 _pullobsolete(pullop)
1254 pullop.trmanager.close()
1255 pullop.trmanager.close()
1255 finally:
1256 finally:
1256 lockmod.release(pullop.trmanager, lock, wlock)
1257 lockmod.release(pullop.trmanager, lock, wlock)
1257
1258
1258 return pullop
1259 return pullop
1259
1260
1260 # list of steps to perform discovery before pull
1261 # list of steps to perform discovery before pull
1261 pulldiscoveryorder = []
1262 pulldiscoveryorder = []
1262
1263
1263 # Mapping between step name and function
1264 # Mapping between step name and function
1264 #
1265 #
1265 # This exists to help extensions wrap steps if necessary
1266 # This exists to help extensions wrap steps if necessary
1266 pulldiscoverymapping = {}
1267 pulldiscoverymapping = {}
1267
1268
1268 def pulldiscovery(stepname):
1269 def pulldiscovery(stepname):
1269 """decorator for function performing discovery before pull
1270 """decorator for function performing discovery before pull
1270
1271
1271 The function is added to the step -> function mapping and appended to the
1272 The function is added to the step -> function mapping and appended to the
1272 list of steps. Beware that decorated function will be added in order (this
1273 list of steps. Beware that decorated function will be added in order (this
1273 may matter).
1274 may matter).
1274
1275
1275 You can only use this decorator for a new step, if you want to wrap a step
1276 You can only use this decorator for a new step, if you want to wrap a step
1276 from an extension, change the pulldiscovery dictionary directly."""
1277 from an extension, change the pulldiscovery dictionary directly."""
1277 def dec(func):
1278 def dec(func):
1278 assert stepname not in pulldiscoverymapping
1279 assert stepname not in pulldiscoverymapping
1279 pulldiscoverymapping[stepname] = func
1280 pulldiscoverymapping[stepname] = func
1280 pulldiscoveryorder.append(stepname)
1281 pulldiscoveryorder.append(stepname)
1281 return func
1282 return func
1282 return dec
1283 return dec
1283
1284
1284 def _pulldiscovery(pullop):
1285 def _pulldiscovery(pullop):
1285 """Run all discovery steps"""
1286 """Run all discovery steps"""
1286 for stepname in pulldiscoveryorder:
1287 for stepname in pulldiscoveryorder:
1287 step = pulldiscoverymapping[stepname]
1288 step = pulldiscoverymapping[stepname]
1288 step(pullop)
1289 step(pullop)
1289
1290
1290 @pulldiscovery('b1:bookmarks')
1291 @pulldiscovery('b1:bookmarks')
1291 def _pullbookmarkbundle1(pullop):
1292 def _pullbookmarkbundle1(pullop):
1292 """fetch bookmark data in bundle1 case
1293 """fetch bookmark data in bundle1 case
1293
1294
1294 If not using bundle2, we have to fetch bookmarks before changeset
1295 If not using bundle2, we have to fetch bookmarks before changeset
1295 discovery to reduce the chance and impact of race conditions."""
1296 discovery to reduce the chance and impact of race conditions."""
1296 if pullop.remotebookmarks is not None:
1297 if pullop.remotebookmarks is not None:
1297 return
1298 return
1298 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1299 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1299 # all known bundle2 servers now support listkeys, but lets be nice with
1300 # all known bundle2 servers now support listkeys, but lets be nice with
1300 # new implementation.
1301 # new implementation.
1301 return
1302 return
1302 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1303 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1303
1304
1304
1305
1305 @pulldiscovery('changegroup')
1306 @pulldiscovery('changegroup')
1306 def _pulldiscoverychangegroup(pullop):
1307 def _pulldiscoverychangegroup(pullop):
1307 """discovery phase for the pull
1308 """discovery phase for the pull
1308
1309
1309 Current handle changeset discovery only, will change handle all discovery
1310 Current handle changeset discovery only, will change handle all discovery
1310 at some point."""
1311 at some point."""
1311 tmp = discovery.findcommonincoming(pullop.repo,
1312 tmp = discovery.findcommonincoming(pullop.repo,
1312 pullop.remote,
1313 pullop.remote,
1313 heads=pullop.heads,
1314 heads=pullop.heads,
1314 force=pullop.force)
1315 force=pullop.force)
1315 common, fetch, rheads = tmp
1316 common, fetch, rheads = tmp
1316 nm = pullop.repo.unfiltered().changelog.nodemap
1317 nm = pullop.repo.unfiltered().changelog.nodemap
1317 if fetch and rheads:
1318 if fetch and rheads:
1318 # If a remote heads in filtered locally, lets drop it from the unknown
1319 # If a remote heads in filtered locally, lets drop it from the unknown
1319 # remote heads and put in back in common.
1320 # remote heads and put in back in common.
1320 #
1321 #
1321 # This is a hackish solution to catch most of "common but locally
1322 # This is a hackish solution to catch most of "common but locally
1322 # hidden situation". We do not performs discovery on unfiltered
1323 # hidden situation". We do not performs discovery on unfiltered
1323 # repository because it end up doing a pathological amount of round
1324 # repository because it end up doing a pathological amount of round
1324 # trip for w huge amount of changeset we do not care about.
1325 # trip for w huge amount of changeset we do not care about.
1325 #
1326 #
1326 # If a set of such "common but filtered" changeset exist on the server
1327 # If a set of such "common but filtered" changeset exist on the server
1327 # but are not including a remote heads, we'll not be able to detect it,
1328 # but are not including a remote heads, we'll not be able to detect it,
1328 scommon = set(common)
1329 scommon = set(common)
1329 filteredrheads = []
1330 filteredrheads = []
1330 for n in rheads:
1331 for n in rheads:
1331 if n in nm:
1332 if n in nm:
1332 if n not in scommon:
1333 if n not in scommon:
1333 common.append(n)
1334 common.append(n)
1334 else:
1335 else:
1335 filteredrheads.append(n)
1336 filteredrheads.append(n)
1336 if not filteredrheads:
1337 if not filteredrheads:
1337 fetch = []
1338 fetch = []
1338 rheads = filteredrheads
1339 rheads = filteredrheads
1339 pullop.common = common
1340 pullop.common = common
1340 pullop.fetch = fetch
1341 pullop.fetch = fetch
1341 pullop.rheads = rheads
1342 pullop.rheads = rheads
1342
1343
1343 def _pullbundle2(pullop):
1344 def _pullbundle2(pullop):
1344 """pull data using bundle2
1345 """pull data using bundle2
1345
1346
1346 For now, the only supported data are changegroup."""
1347 For now, the only supported data are changegroup."""
1347 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1348 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1348
1349
1349 # At the moment we don't do stream clones over bundle2. If that is
1350 # At the moment we don't do stream clones over bundle2. If that is
1350 # implemented then here's where the check for that will go.
1351 # implemented then here's where the check for that will go.
1351 streaming = False
1352 streaming = False
1352
1353
1353 # pulling changegroup
1354 # pulling changegroup
1354 pullop.stepsdone.add('changegroup')
1355 pullop.stepsdone.add('changegroup')
1355
1356
1356 kwargs['common'] = pullop.common
1357 kwargs['common'] = pullop.common
1357 kwargs['heads'] = pullop.heads or pullop.rheads
1358 kwargs['heads'] = pullop.heads or pullop.rheads
1358 kwargs['cg'] = pullop.fetch
1359 kwargs['cg'] = pullop.fetch
1359 if 'listkeys' in pullop.remotebundle2caps:
1360 if 'listkeys' in pullop.remotebundle2caps:
1360 kwargs['listkeys'] = ['phases']
1361 kwargs['listkeys'] = ['phases']
1361 if pullop.remotebookmarks is None:
1362 if pullop.remotebookmarks is None:
1362 # make sure to always includes bookmark data when migrating
1363 # make sure to always includes bookmark data when migrating
1363 # `hg incoming --bundle` to using this function.
1364 # `hg incoming --bundle` to using this function.
1364 kwargs['listkeys'].append('bookmarks')
1365 kwargs['listkeys'].append('bookmarks')
1365
1366
1366 # If this is a full pull / clone and the server supports the clone bundles
1367 # If this is a full pull / clone and the server supports the clone bundles
1367 # feature, tell the server whether we attempted a clone bundle. The
1368 # feature, tell the server whether we attempted a clone bundle. The
1368 # presence of this flag indicates the client supports clone bundles. This
1369 # presence of this flag indicates the client supports clone bundles. This
1369 # will enable the server to treat clients that support clone bundles
1370 # will enable the server to treat clients that support clone bundles
1370 # differently from those that don't.
1371 # differently from those that don't.
1371 if (pullop.remote.capable('clonebundles')
1372 if (pullop.remote.capable('clonebundles')
1372 and pullop.heads is None and list(pullop.common) == [nullid]):
1373 and pullop.heads is None and list(pullop.common) == [nullid]):
1373 kwargs['cbattempted'] = pullop.clonebundleattempted
1374 kwargs['cbattempted'] = pullop.clonebundleattempted
1374
1375
1375 if streaming:
1376 if streaming:
1376 pullop.repo.ui.status(_('streaming all changes\n'))
1377 pullop.repo.ui.status(_('streaming all changes\n'))
1377 elif not pullop.fetch:
1378 elif not pullop.fetch:
1378 pullop.repo.ui.status(_("no changes found\n"))
1379 pullop.repo.ui.status(_("no changes found\n"))
1379 pullop.cgresult = 0
1380 pullop.cgresult = 0
1380 else:
1381 else:
1381 if pullop.heads is None and list(pullop.common) == [nullid]:
1382 if pullop.heads is None and list(pullop.common) == [nullid]:
1382 pullop.repo.ui.status(_("requesting all changes\n"))
1383 pullop.repo.ui.status(_("requesting all changes\n"))
1383 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1384 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1384 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1385 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1385 if obsolete.commonversion(remoteversions) is not None:
1386 if obsolete.commonversion(remoteversions) is not None:
1386 kwargs['obsmarkers'] = True
1387 kwargs['obsmarkers'] = True
1387 pullop.stepsdone.add('obsmarkers')
1388 pullop.stepsdone.add('obsmarkers')
1388 _pullbundle2extraprepare(pullop, kwargs)
1389 _pullbundle2extraprepare(pullop, kwargs)
1389 bundle = pullop.remote.getbundle('pull', **kwargs)
1390 bundle = pullop.remote.getbundle('pull', **pycompat.strkwargs(kwargs))
1390 try:
1391 try:
1391 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1392 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1392 except bundle2.AbortFromPart as exc:
1393 except bundle2.AbortFromPart as exc:
1393 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1394 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1394 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1395 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1395 except error.BundleValueError as exc:
1396 except error.BundleValueError as exc:
1396 raise error.Abort(_('missing support for %s') % exc)
1397 raise error.Abort(_('missing support for %s') % exc)
1397
1398
1398 if pullop.fetch:
1399 if pullop.fetch:
1399 results = [cg['return'] for cg in op.records['changegroup']]
1400 results = [cg['return'] for cg in op.records['changegroup']]
1400 pullop.cgresult = changegroup.combineresults(results)
1401 pullop.cgresult = changegroup.combineresults(results)
1401
1402
1402 # processing phases change
1403 # processing phases change
1403 for namespace, value in op.records['listkeys']:
1404 for namespace, value in op.records['listkeys']:
1404 if namespace == 'phases':
1405 if namespace == 'phases':
1405 _pullapplyphases(pullop, value)
1406 _pullapplyphases(pullop, value)
1406
1407
1407 # processing bookmark update
1408 # processing bookmark update
1408 for namespace, value in op.records['listkeys']:
1409 for namespace, value in op.records['listkeys']:
1409 if namespace == 'bookmarks':
1410 if namespace == 'bookmarks':
1410 pullop.remotebookmarks = value
1411 pullop.remotebookmarks = value
1411
1412
1412 # bookmark data were either already there or pulled in the bundle
1413 # bookmark data were either already there or pulled in the bundle
1413 if pullop.remotebookmarks is not None:
1414 if pullop.remotebookmarks is not None:
1414 _pullbookmarks(pullop)
1415 _pullbookmarks(pullop)
1415
1416
1416 def _pullbundle2extraprepare(pullop, kwargs):
1417 def _pullbundle2extraprepare(pullop, kwargs):
1417 """hook function so that extensions can extend the getbundle call"""
1418 """hook function so that extensions can extend the getbundle call"""
1418 pass
1419 pass
1419
1420
1420 def _pullchangeset(pullop):
1421 def _pullchangeset(pullop):
1421 """pull changeset from unbundle into the local repo"""
1422 """pull changeset from unbundle into the local repo"""
1422 # We delay the open of the transaction as late as possible so we
1423 # We delay the open of the transaction as late as possible so we
1423 # don't open transaction for nothing or you break future useful
1424 # don't open transaction for nothing or you break future useful
1424 # rollback call
1425 # rollback call
1425 if 'changegroup' in pullop.stepsdone:
1426 if 'changegroup' in pullop.stepsdone:
1426 return
1427 return
1427 pullop.stepsdone.add('changegroup')
1428 pullop.stepsdone.add('changegroup')
1428 if not pullop.fetch:
1429 if not pullop.fetch:
1429 pullop.repo.ui.status(_("no changes found\n"))
1430 pullop.repo.ui.status(_("no changes found\n"))
1430 pullop.cgresult = 0
1431 pullop.cgresult = 0
1431 return
1432 return
1432 pullop.gettransaction()
1433 pullop.gettransaction()
1433 if pullop.heads is None and list(pullop.common) == [nullid]:
1434 if pullop.heads is None and list(pullop.common) == [nullid]:
1434 pullop.repo.ui.status(_("requesting all changes\n"))
1435 pullop.repo.ui.status(_("requesting all changes\n"))
1435 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1436 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1436 # issue1320, avoid a race if remote changed after discovery
1437 # issue1320, avoid a race if remote changed after discovery
1437 pullop.heads = pullop.rheads
1438 pullop.heads = pullop.rheads
1438
1439
1439 if pullop.remote.capable('getbundle'):
1440 if pullop.remote.capable('getbundle'):
1440 # TODO: get bundlecaps from remote
1441 # TODO: get bundlecaps from remote
1441 cg = pullop.remote.getbundle('pull', common=pullop.common,
1442 cg = pullop.remote.getbundle('pull', common=pullop.common,
1442 heads=pullop.heads or pullop.rheads)
1443 heads=pullop.heads or pullop.rheads)
1443 elif pullop.heads is None:
1444 elif pullop.heads is None:
1444 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1445 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1445 elif not pullop.remote.capable('changegroupsubset'):
1446 elif not pullop.remote.capable('changegroupsubset'):
1446 raise error.Abort(_("partial pull cannot be done because "
1447 raise error.Abort(_("partial pull cannot be done because "
1447 "other repository doesn't support "
1448 "other repository doesn't support "
1448 "changegroupsubset."))
1449 "changegroupsubset."))
1449 else:
1450 else:
1450 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1451 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1451 pullop.cgresult = cg.apply(pullop.repo, 'pull', pullop.remote.url())
1452 pullop.cgresult = cg.apply(pullop.repo, 'pull', pullop.remote.url())
1452
1453
1453 def _pullphase(pullop):
1454 def _pullphase(pullop):
1454 # Get remote phases data from remote
1455 # Get remote phases data from remote
1455 if 'phases' in pullop.stepsdone:
1456 if 'phases' in pullop.stepsdone:
1456 return
1457 return
1457 remotephases = pullop.remote.listkeys('phases')
1458 remotephases = pullop.remote.listkeys('phases')
1458 _pullapplyphases(pullop, remotephases)
1459 _pullapplyphases(pullop, remotephases)
1459
1460
1460 def _pullapplyphases(pullop, remotephases):
1461 def _pullapplyphases(pullop, remotephases):
1461 """apply phase movement from observed remote state"""
1462 """apply phase movement from observed remote state"""
1462 if 'phases' in pullop.stepsdone:
1463 if 'phases' in pullop.stepsdone:
1463 return
1464 return
1464 pullop.stepsdone.add('phases')
1465 pullop.stepsdone.add('phases')
1465 publishing = bool(remotephases.get('publishing', False))
1466 publishing = bool(remotephases.get('publishing', False))
1466 if remotephases and not publishing:
1467 if remotephases and not publishing:
1467 # remote is new and non-publishing
1468 # remote is new and non-publishing
1468 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1469 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1469 pullop.pulledsubset,
1470 pullop.pulledsubset,
1470 remotephases)
1471 remotephases)
1471 dheads = pullop.pulledsubset
1472 dheads = pullop.pulledsubset
1472 else:
1473 else:
1473 # Remote is old or publishing all common changesets
1474 # Remote is old or publishing all common changesets
1474 # should be seen as public
1475 # should be seen as public
1475 pheads = pullop.pulledsubset
1476 pheads = pullop.pulledsubset
1476 dheads = []
1477 dheads = []
1477 unfi = pullop.repo.unfiltered()
1478 unfi = pullop.repo.unfiltered()
1478 phase = unfi._phasecache.phase
1479 phase = unfi._phasecache.phase
1479 rev = unfi.changelog.nodemap.get
1480 rev = unfi.changelog.nodemap.get
1480 public = phases.public
1481 public = phases.public
1481 draft = phases.draft
1482 draft = phases.draft
1482
1483
1483 # exclude changesets already public locally and update the others
1484 # exclude changesets already public locally and update the others
1484 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1485 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1485 if pheads:
1486 if pheads:
1486 tr = pullop.gettransaction()
1487 tr = pullop.gettransaction()
1487 phases.advanceboundary(pullop.repo, tr, public, pheads)
1488 phases.advanceboundary(pullop.repo, tr, public, pheads)
1488
1489
1489 # exclude changesets already draft locally and update the others
1490 # exclude changesets already draft locally and update the others
1490 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1491 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1491 if dheads:
1492 if dheads:
1492 tr = pullop.gettransaction()
1493 tr = pullop.gettransaction()
1493 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1494 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1494
1495
1495 def _pullbookmarks(pullop):
1496 def _pullbookmarks(pullop):
1496 """process the remote bookmark information to update the local one"""
1497 """process the remote bookmark information to update the local one"""
1497 if 'bookmarks' in pullop.stepsdone:
1498 if 'bookmarks' in pullop.stepsdone:
1498 return
1499 return
1499 pullop.stepsdone.add('bookmarks')
1500 pullop.stepsdone.add('bookmarks')
1500 repo = pullop.repo
1501 repo = pullop.repo
1501 remotebookmarks = pullop.remotebookmarks
1502 remotebookmarks = pullop.remotebookmarks
1502 remotebookmarks = bookmod.unhexlifybookmarks(remotebookmarks)
1503 remotebookmarks = bookmod.unhexlifybookmarks(remotebookmarks)
1503 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1504 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1504 pullop.remote.url(),
1505 pullop.remote.url(),
1505 pullop.gettransaction,
1506 pullop.gettransaction,
1506 explicit=pullop.explicitbookmarks)
1507 explicit=pullop.explicitbookmarks)
1507
1508
1508 def _pullobsolete(pullop):
1509 def _pullobsolete(pullop):
1509 """utility function to pull obsolete markers from a remote
1510 """utility function to pull obsolete markers from a remote
1510
1511
1511 The `gettransaction` is function that return the pull transaction, creating
1512 The `gettransaction` is function that return the pull transaction, creating
1512 one if necessary. We return the transaction to inform the calling code that
1513 one if necessary. We return the transaction to inform the calling code that
1513 a new transaction have been created (when applicable).
1514 a new transaction have been created (when applicable).
1514
1515
1515 Exists mostly to allow overriding for experimentation purpose"""
1516 Exists mostly to allow overriding for experimentation purpose"""
1516 if 'obsmarkers' in pullop.stepsdone:
1517 if 'obsmarkers' in pullop.stepsdone:
1517 return
1518 return
1518 pullop.stepsdone.add('obsmarkers')
1519 pullop.stepsdone.add('obsmarkers')
1519 tr = None
1520 tr = None
1520 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1521 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1521 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1522 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1522 remoteobs = pullop.remote.listkeys('obsolete')
1523 remoteobs = pullop.remote.listkeys('obsolete')
1523 if 'dump0' in remoteobs:
1524 if 'dump0' in remoteobs:
1524 tr = pullop.gettransaction()
1525 tr = pullop.gettransaction()
1525 markers = []
1526 markers = []
1526 for key in sorted(remoteobs, reverse=True):
1527 for key in sorted(remoteobs, reverse=True):
1527 if key.startswith('dump'):
1528 if key.startswith('dump'):
1528 data = util.b85decode(remoteobs[key])
1529 data = util.b85decode(remoteobs[key])
1529 version, newmarks = obsolete._readmarkers(data)
1530 version, newmarks = obsolete._readmarkers(data)
1530 markers += newmarks
1531 markers += newmarks
1531 if markers:
1532 if markers:
1532 pullop.repo.obsstore.add(tr, markers)
1533 pullop.repo.obsstore.add(tr, markers)
1533 pullop.repo.invalidatevolatilesets()
1534 pullop.repo.invalidatevolatilesets()
1534 return tr
1535 return tr
1535
1536
1536 def caps20to10(repo):
1537 def caps20to10(repo):
1537 """return a set with appropriate options to use bundle20 during getbundle"""
1538 """return a set with appropriate options to use bundle20 during getbundle"""
1538 caps = {'HG20'}
1539 caps = {'HG20'}
1539 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1540 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1540 caps.add('bundle2=' + urlreq.quote(capsblob))
1541 caps.add('bundle2=' + urlreq.quote(capsblob))
1541 return caps
1542 return caps
1542
1543
1543 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1544 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1544 getbundle2partsorder = []
1545 getbundle2partsorder = []
1545
1546
1546 # Mapping between step name and function
1547 # Mapping between step name and function
1547 #
1548 #
1548 # This exists to help extensions wrap steps if necessary
1549 # This exists to help extensions wrap steps if necessary
1549 getbundle2partsmapping = {}
1550 getbundle2partsmapping = {}
1550
1551
1551 def getbundle2partsgenerator(stepname, idx=None):
1552 def getbundle2partsgenerator(stepname, idx=None):
1552 """decorator for function generating bundle2 part for getbundle
1553 """decorator for function generating bundle2 part for getbundle
1553
1554
1554 The function is added to the step -> function mapping and appended to the
1555 The function is added to the step -> function mapping and appended to the
1555 list of steps. Beware that decorated functions will be added in order
1556 list of steps. Beware that decorated functions will be added in order
1556 (this may matter).
1557 (this may matter).
1557
1558
1558 You can only use this decorator for new steps, if you want to wrap a step
1559 You can only use this decorator for new steps, if you want to wrap a step
1559 from an extension, attack the getbundle2partsmapping dictionary directly."""
1560 from an extension, attack the getbundle2partsmapping dictionary directly."""
1560 def dec(func):
1561 def dec(func):
1561 assert stepname not in getbundle2partsmapping
1562 assert stepname not in getbundle2partsmapping
1562 getbundle2partsmapping[stepname] = func
1563 getbundle2partsmapping[stepname] = func
1563 if idx is None:
1564 if idx is None:
1564 getbundle2partsorder.append(stepname)
1565 getbundle2partsorder.append(stepname)
1565 else:
1566 else:
1566 getbundle2partsorder.insert(idx, stepname)
1567 getbundle2partsorder.insert(idx, stepname)
1567 return func
1568 return func
1568 return dec
1569 return dec
1569
1570
1570 def bundle2requested(bundlecaps):
1571 def bundle2requested(bundlecaps):
1571 if bundlecaps is not None:
1572 if bundlecaps is not None:
1572 return any(cap.startswith('HG2') for cap in bundlecaps)
1573 return any(cap.startswith('HG2') for cap in bundlecaps)
1573 return False
1574 return False
1574
1575
1575 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
1576 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
1576 **kwargs):
1577 **kwargs):
1577 """Return chunks constituting a bundle's raw data.
1578 """Return chunks constituting a bundle's raw data.
1578
1579
1579 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1580 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1580 passed.
1581 passed.
1581
1582
1582 Returns an iterator over raw chunks (of varying sizes).
1583 Returns an iterator over raw chunks (of varying sizes).
1583 """
1584 """
1584 usebundle2 = bundle2requested(bundlecaps)
1585 usebundle2 = bundle2requested(bundlecaps)
1585 # bundle10 case
1586 # bundle10 case
1586 if not usebundle2:
1587 if not usebundle2:
1587 if bundlecaps and not kwargs.get('cg', True):
1588 if bundlecaps and not kwargs.get('cg', True):
1588 raise ValueError(_('request for bundle10 must include changegroup'))
1589 raise ValueError(_('request for bundle10 must include changegroup'))
1589
1590
1590 if kwargs:
1591 if kwargs:
1591 raise ValueError(_('unsupported getbundle arguments: %s')
1592 raise ValueError(_('unsupported getbundle arguments: %s')
1592 % ', '.join(sorted(kwargs.keys())))
1593 % ', '.join(sorted(kwargs.keys())))
1593 outgoing = _computeoutgoing(repo, heads, common)
1594 outgoing = _computeoutgoing(repo, heads, common)
1594 bundler = changegroup.getbundler('01', repo, bundlecaps)
1595 bundler = changegroup.getbundler('01', repo, bundlecaps)
1595 return changegroup.getsubsetraw(repo, outgoing, bundler, source)
1596 return changegroup.getsubsetraw(repo, outgoing, bundler, source)
1596
1597
1597 # bundle20 case
1598 # bundle20 case
1598 b2caps = {}
1599 b2caps = {}
1599 for bcaps in bundlecaps:
1600 for bcaps in bundlecaps:
1600 if bcaps.startswith('bundle2='):
1601 if bcaps.startswith('bundle2='):
1601 blob = urlreq.unquote(bcaps[len('bundle2='):])
1602 blob = urlreq.unquote(bcaps[len('bundle2='):])
1602 b2caps.update(bundle2.decodecaps(blob))
1603 b2caps.update(bundle2.decodecaps(blob))
1603 bundler = bundle2.bundle20(repo.ui, b2caps)
1604 bundler = bundle2.bundle20(repo.ui, b2caps)
1604
1605
1605 kwargs['heads'] = heads
1606 kwargs['heads'] = heads
1606 kwargs['common'] = common
1607 kwargs['common'] = common
1607
1608
1608 for name in getbundle2partsorder:
1609 for name in getbundle2partsorder:
1609 func = getbundle2partsmapping[name]
1610 func = getbundle2partsmapping[name]
1610 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1611 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1611 **kwargs)
1612 **kwargs)
1612
1613
1613 return bundler.getchunks()
1614 return bundler.getchunks()
1614
1615
1615 @getbundle2partsgenerator('changegroup')
1616 @getbundle2partsgenerator('changegroup')
1616 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1617 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1617 b2caps=None, heads=None, common=None, **kwargs):
1618 b2caps=None, heads=None, common=None, **kwargs):
1618 """add a changegroup part to the requested bundle"""
1619 """add a changegroup part to the requested bundle"""
1619 cg = None
1620 cg = None
1620 if kwargs.get('cg', True):
1621 if kwargs.get('cg', True):
1621 # build changegroup bundle here.
1622 # build changegroup bundle here.
1622 version = '01'
1623 version = '01'
1623 cgversions = b2caps.get('changegroup')
1624 cgversions = b2caps.get('changegroup')
1624 if cgversions: # 3.1 and 3.2 ship with an empty value
1625 if cgversions: # 3.1 and 3.2 ship with an empty value
1625 cgversions = [v for v in cgversions
1626 cgversions = [v for v in cgversions
1626 if v in changegroup.supportedoutgoingversions(repo)]
1627 if v in changegroup.supportedoutgoingversions(repo)]
1627 if not cgversions:
1628 if not cgversions:
1628 raise ValueError(_('no common changegroup version'))
1629 raise ValueError(_('no common changegroup version'))
1629 version = max(cgversions)
1630 version = max(cgversions)
1630 outgoing = _computeoutgoing(repo, heads, common)
1631 outgoing = _computeoutgoing(repo, heads, common)
1631 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1632 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1632 bundlecaps=bundlecaps,
1633 bundlecaps=bundlecaps,
1633 version=version)
1634 version=version)
1634
1635
1635 if cg:
1636 if cg:
1636 part = bundler.newpart('changegroup', data=cg)
1637 part = bundler.newpart('changegroup', data=cg)
1637 if cgversions:
1638 if cgversions:
1638 part.addparam('version', version)
1639 part.addparam('version', version)
1639 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1640 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1640 if 'treemanifest' in repo.requirements:
1641 if 'treemanifest' in repo.requirements:
1641 part.addparam('treemanifest', '1')
1642 part.addparam('treemanifest', '1')
1642
1643
1643 @getbundle2partsgenerator('listkeys')
1644 @getbundle2partsgenerator('listkeys')
1644 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1645 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1645 b2caps=None, **kwargs):
1646 b2caps=None, **kwargs):
1646 """add parts containing listkeys namespaces to the requested bundle"""
1647 """add parts containing listkeys namespaces to the requested bundle"""
1647 listkeys = kwargs.get('listkeys', ())
1648 listkeys = kwargs.get('listkeys', ())
1648 for namespace in listkeys:
1649 for namespace in listkeys:
1649 part = bundler.newpart('listkeys')
1650 part = bundler.newpart('listkeys')
1650 part.addparam('namespace', namespace)
1651 part.addparam('namespace', namespace)
1651 keys = repo.listkeys(namespace).items()
1652 keys = repo.listkeys(namespace).items()
1652 part.data = pushkey.encodekeys(keys)
1653 part.data = pushkey.encodekeys(keys)
1653
1654
1654 @getbundle2partsgenerator('obsmarkers')
1655 @getbundle2partsgenerator('obsmarkers')
1655 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1656 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1656 b2caps=None, heads=None, **kwargs):
1657 b2caps=None, heads=None, **kwargs):
1657 """add an obsolescence markers part to the requested bundle"""
1658 """add an obsolescence markers part to the requested bundle"""
1658 if kwargs.get('obsmarkers', False):
1659 if kwargs.get('obsmarkers', False):
1659 if heads is None:
1660 if heads is None:
1660 heads = repo.heads()
1661 heads = repo.heads()
1661 subset = [c.node() for c in repo.set('::%ln', heads)]
1662 subset = [c.node() for c in repo.set('::%ln', heads)]
1662 markers = repo.obsstore.relevantmarkers(subset)
1663 markers = repo.obsstore.relevantmarkers(subset)
1663 markers = sorted(markers)
1664 markers = sorted(markers)
1664 bundle2.buildobsmarkerspart(bundler, markers)
1665 bundle2.buildobsmarkerspart(bundler, markers)
1665
1666
1666 @getbundle2partsgenerator('hgtagsfnodes')
1667 @getbundle2partsgenerator('hgtagsfnodes')
1667 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1668 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1668 b2caps=None, heads=None, common=None,
1669 b2caps=None, heads=None, common=None,
1669 **kwargs):
1670 **kwargs):
1670 """Transfer the .hgtags filenodes mapping.
1671 """Transfer the .hgtags filenodes mapping.
1671
1672
1672 Only values for heads in this bundle will be transferred.
1673 Only values for heads in this bundle will be transferred.
1673
1674
1674 The part data consists of pairs of 20 byte changeset node and .hgtags
1675 The part data consists of pairs of 20 byte changeset node and .hgtags
1675 filenodes raw values.
1676 filenodes raw values.
1676 """
1677 """
1677 # Don't send unless:
1678 # Don't send unless:
1678 # - changeset are being exchanged,
1679 # - changeset are being exchanged,
1679 # - the client supports it.
1680 # - the client supports it.
1680 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1681 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1681 return
1682 return
1682
1683
1683 outgoing = _computeoutgoing(repo, heads, common)
1684 outgoing = _computeoutgoing(repo, heads, common)
1684 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
1685 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
1685
1686
1686 def _getbookmarks(repo, **kwargs):
1687 def _getbookmarks(repo, **kwargs):
1687 """Returns bookmark to node mapping.
1688 """Returns bookmark to node mapping.
1688
1689
1689 This function is primarily used to generate `bookmarks` bundle2 part.
1690 This function is primarily used to generate `bookmarks` bundle2 part.
1690 It is a separate function in order to make it easy to wrap it
1691 It is a separate function in order to make it easy to wrap it
1691 in extensions. Passing `kwargs` to the function makes it easy to
1692 in extensions. Passing `kwargs` to the function makes it easy to
1692 add new parameters in extensions.
1693 add new parameters in extensions.
1693 """
1694 """
1694
1695
1695 return dict(bookmod.listbinbookmarks(repo))
1696 return dict(bookmod.listbinbookmarks(repo))
1696
1697
1697 def check_heads(repo, their_heads, context):
1698 def check_heads(repo, their_heads, context):
1698 """check if the heads of a repo have been modified
1699 """check if the heads of a repo have been modified
1699
1700
1700 Used by peer for unbundling.
1701 Used by peer for unbundling.
1701 """
1702 """
1702 heads = repo.heads()
1703 heads = repo.heads()
1703 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
1704 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
1704 if not (their_heads == ['force'] or their_heads == heads or
1705 if not (their_heads == ['force'] or their_heads == heads or
1705 their_heads == ['hashed', heads_hash]):
1706 their_heads == ['hashed', heads_hash]):
1706 # someone else committed/pushed/unbundled while we
1707 # someone else committed/pushed/unbundled while we
1707 # were transferring data
1708 # were transferring data
1708 raise error.PushRaced('repository changed while %s - '
1709 raise error.PushRaced('repository changed while %s - '
1709 'please try again' % context)
1710 'please try again' % context)
1710
1711
1711 def unbundle(repo, cg, heads, source, url):
1712 def unbundle(repo, cg, heads, source, url):
1712 """Apply a bundle to a repo.
1713 """Apply a bundle to a repo.
1713
1714
1714 this function makes sure the repo is locked during the application and have
1715 this function makes sure the repo is locked during the application and have
1715 mechanism to check that no push race occurred between the creation of the
1716 mechanism to check that no push race occurred between the creation of the
1716 bundle and its application.
1717 bundle and its application.
1717
1718
1718 If the push was raced as PushRaced exception is raised."""
1719 If the push was raced as PushRaced exception is raised."""
1719 r = 0
1720 r = 0
1720 # need a transaction when processing a bundle2 stream
1721 # need a transaction when processing a bundle2 stream
1721 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1722 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1722 lockandtr = [None, None, None]
1723 lockandtr = [None, None, None]
1723 recordout = None
1724 recordout = None
1724 # quick fix for output mismatch with bundle2 in 3.4
1725 # quick fix for output mismatch with bundle2 in 3.4
1725 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture',
1726 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture',
1726 False)
1727 False)
1727 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1728 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1728 captureoutput = True
1729 captureoutput = True
1729 try:
1730 try:
1730 # note: outside bundle1, 'heads' is expected to be empty and this
1731 # note: outside bundle1, 'heads' is expected to be empty and this
1731 # 'check_heads' call wil be a no-op
1732 # 'check_heads' call wil be a no-op
1732 check_heads(repo, heads, 'uploading changes')
1733 check_heads(repo, heads, 'uploading changes')
1733 # push can proceed
1734 # push can proceed
1734 if not isinstance(cg, bundle2.unbundle20):
1735 if not isinstance(cg, bundle2.unbundle20):
1735 # legacy case: bundle1 (changegroup 01)
1736 # legacy case: bundle1 (changegroup 01)
1736 with repo.lock():
1737 with repo.lock():
1737 r = cg.apply(repo, source, url)
1738 r = cg.apply(repo, source, url)
1738 else:
1739 else:
1739 r = None
1740 r = None
1740 try:
1741 try:
1741 def gettransaction():
1742 def gettransaction():
1742 if not lockandtr[2]:
1743 if not lockandtr[2]:
1743 lockandtr[0] = repo.wlock()
1744 lockandtr[0] = repo.wlock()
1744 lockandtr[1] = repo.lock()
1745 lockandtr[1] = repo.lock()
1745 lockandtr[2] = repo.transaction(source)
1746 lockandtr[2] = repo.transaction(source)
1746 lockandtr[2].hookargs['source'] = source
1747 lockandtr[2].hookargs['source'] = source
1747 lockandtr[2].hookargs['url'] = url
1748 lockandtr[2].hookargs['url'] = url
1748 lockandtr[2].hookargs['bundle2'] = '1'
1749 lockandtr[2].hookargs['bundle2'] = '1'
1749 return lockandtr[2]
1750 return lockandtr[2]
1750
1751
1751 # Do greedy locking by default until we're satisfied with lazy
1752 # Do greedy locking by default until we're satisfied with lazy
1752 # locking.
1753 # locking.
1753 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1754 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1754 gettransaction()
1755 gettransaction()
1755
1756
1756 op = bundle2.bundleoperation(repo, gettransaction,
1757 op = bundle2.bundleoperation(repo, gettransaction,
1757 captureoutput=captureoutput)
1758 captureoutput=captureoutput)
1758 try:
1759 try:
1759 op = bundle2.processbundle(repo, cg, op=op)
1760 op = bundle2.processbundle(repo, cg, op=op)
1760 finally:
1761 finally:
1761 r = op.reply
1762 r = op.reply
1762 if captureoutput and r is not None:
1763 if captureoutput and r is not None:
1763 repo.ui.pushbuffer(error=True, subproc=True)
1764 repo.ui.pushbuffer(error=True, subproc=True)
1764 def recordout(output):
1765 def recordout(output):
1765 r.newpart('output', data=output, mandatory=False)
1766 r.newpart('output', data=output, mandatory=False)
1766 if lockandtr[2] is not None:
1767 if lockandtr[2] is not None:
1767 lockandtr[2].close()
1768 lockandtr[2].close()
1768 except BaseException as exc:
1769 except BaseException as exc:
1769 exc.duringunbundle2 = True
1770 exc.duringunbundle2 = True
1770 if captureoutput and r is not None:
1771 if captureoutput and r is not None:
1771 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1772 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1772 def recordout(output):
1773 def recordout(output):
1773 part = bundle2.bundlepart('output', data=output,
1774 part = bundle2.bundlepart('output', data=output,
1774 mandatory=False)
1775 mandatory=False)
1775 parts.append(part)
1776 parts.append(part)
1776 raise
1777 raise
1777 finally:
1778 finally:
1778 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1779 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1779 if recordout is not None:
1780 if recordout is not None:
1780 recordout(repo.ui.popbuffer())
1781 recordout(repo.ui.popbuffer())
1781 return r
1782 return r
1782
1783
1783 def _maybeapplyclonebundle(pullop):
1784 def _maybeapplyclonebundle(pullop):
1784 """Apply a clone bundle from a remote, if possible."""
1785 """Apply a clone bundle from a remote, if possible."""
1785
1786
1786 repo = pullop.repo
1787 repo = pullop.repo
1787 remote = pullop.remote
1788 remote = pullop.remote
1788
1789
1789 if not repo.ui.configbool('ui', 'clonebundles', True):
1790 if not repo.ui.configbool('ui', 'clonebundles', True):
1790 return
1791 return
1791
1792
1792 # Only run if local repo is empty.
1793 # Only run if local repo is empty.
1793 if len(repo):
1794 if len(repo):
1794 return
1795 return
1795
1796
1796 if pullop.heads:
1797 if pullop.heads:
1797 return
1798 return
1798
1799
1799 if not remote.capable('clonebundles'):
1800 if not remote.capable('clonebundles'):
1800 return
1801 return
1801
1802
1802 res = remote._call('clonebundles')
1803 res = remote._call('clonebundles')
1803
1804
1804 # If we call the wire protocol command, that's good enough to record the
1805 # If we call the wire protocol command, that's good enough to record the
1805 # attempt.
1806 # attempt.
1806 pullop.clonebundleattempted = True
1807 pullop.clonebundleattempted = True
1807
1808
1808 entries = parseclonebundlesmanifest(repo, res)
1809 entries = parseclonebundlesmanifest(repo, res)
1809 if not entries:
1810 if not entries:
1810 repo.ui.note(_('no clone bundles available on remote; '
1811 repo.ui.note(_('no clone bundles available on remote; '
1811 'falling back to regular clone\n'))
1812 'falling back to regular clone\n'))
1812 return
1813 return
1813
1814
1814 entries = filterclonebundleentries(repo, entries)
1815 entries = filterclonebundleentries(repo, entries)
1815 if not entries:
1816 if not entries:
1816 # There is a thundering herd concern here. However, if a server
1817 # There is a thundering herd concern here. However, if a server
1817 # operator doesn't advertise bundles appropriate for its clients,
1818 # operator doesn't advertise bundles appropriate for its clients,
1818 # they deserve what's coming. Furthermore, from a client's
1819 # they deserve what's coming. Furthermore, from a client's
1819 # perspective, no automatic fallback would mean not being able to
1820 # perspective, no automatic fallback would mean not being able to
1820 # clone!
1821 # clone!
1821 repo.ui.warn(_('no compatible clone bundles available on server; '
1822 repo.ui.warn(_('no compatible clone bundles available on server; '
1822 'falling back to regular clone\n'))
1823 'falling back to regular clone\n'))
1823 repo.ui.warn(_('(you may want to report this to the server '
1824 repo.ui.warn(_('(you may want to report this to the server '
1824 'operator)\n'))
1825 'operator)\n'))
1825 return
1826 return
1826
1827
1827 entries = sortclonebundleentries(repo.ui, entries)
1828 entries = sortclonebundleentries(repo.ui, entries)
1828
1829
1829 url = entries[0]['URL']
1830 url = entries[0]['URL']
1830 repo.ui.status(_('applying clone bundle from %s\n') % url)
1831 repo.ui.status(_('applying clone bundle from %s\n') % url)
1831 if trypullbundlefromurl(repo.ui, repo, url):
1832 if trypullbundlefromurl(repo.ui, repo, url):
1832 repo.ui.status(_('finished applying clone bundle\n'))
1833 repo.ui.status(_('finished applying clone bundle\n'))
1833 # Bundle failed.
1834 # Bundle failed.
1834 #
1835 #
1835 # We abort by default to avoid the thundering herd of
1836 # We abort by default to avoid the thundering herd of
1836 # clients flooding a server that was expecting expensive
1837 # clients flooding a server that was expecting expensive
1837 # clone load to be offloaded.
1838 # clone load to be offloaded.
1838 elif repo.ui.configbool('ui', 'clonebundlefallback', False):
1839 elif repo.ui.configbool('ui', 'clonebundlefallback', False):
1839 repo.ui.warn(_('falling back to normal clone\n'))
1840 repo.ui.warn(_('falling back to normal clone\n'))
1840 else:
1841 else:
1841 raise error.Abort(_('error applying bundle'),
1842 raise error.Abort(_('error applying bundle'),
1842 hint=_('if this error persists, consider contacting '
1843 hint=_('if this error persists, consider contacting '
1843 'the server operator or disable clone '
1844 'the server operator or disable clone '
1844 'bundles via '
1845 'bundles via '
1845 '"--config ui.clonebundles=false"'))
1846 '"--config ui.clonebundles=false"'))
1846
1847
1847 def parseclonebundlesmanifest(repo, s):
1848 def parseclonebundlesmanifest(repo, s):
1848 """Parses the raw text of a clone bundles manifest.
1849 """Parses the raw text of a clone bundles manifest.
1849
1850
1850 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1851 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1851 to the URL and other keys are the attributes for the entry.
1852 to the URL and other keys are the attributes for the entry.
1852 """
1853 """
1853 m = []
1854 m = []
1854 for line in s.splitlines():
1855 for line in s.splitlines():
1855 fields = line.split()
1856 fields = line.split()
1856 if not fields:
1857 if not fields:
1857 continue
1858 continue
1858 attrs = {'URL': fields[0]}
1859 attrs = {'URL': fields[0]}
1859 for rawattr in fields[1:]:
1860 for rawattr in fields[1:]:
1860 key, value = rawattr.split('=', 1)
1861 key, value = rawattr.split('=', 1)
1861 key = urlreq.unquote(key)
1862 key = urlreq.unquote(key)
1862 value = urlreq.unquote(value)
1863 value = urlreq.unquote(value)
1863 attrs[key] = value
1864 attrs[key] = value
1864
1865
1865 # Parse BUNDLESPEC into components. This makes client-side
1866 # Parse BUNDLESPEC into components. This makes client-side
1866 # preferences easier to specify since you can prefer a single
1867 # preferences easier to specify since you can prefer a single
1867 # component of the BUNDLESPEC.
1868 # component of the BUNDLESPEC.
1868 if key == 'BUNDLESPEC':
1869 if key == 'BUNDLESPEC':
1869 try:
1870 try:
1870 comp, version, params = parsebundlespec(repo, value,
1871 comp, version, params = parsebundlespec(repo, value,
1871 externalnames=True)
1872 externalnames=True)
1872 attrs['COMPRESSION'] = comp
1873 attrs['COMPRESSION'] = comp
1873 attrs['VERSION'] = version
1874 attrs['VERSION'] = version
1874 except error.InvalidBundleSpecification:
1875 except error.InvalidBundleSpecification:
1875 pass
1876 pass
1876 except error.UnsupportedBundleSpecification:
1877 except error.UnsupportedBundleSpecification:
1877 pass
1878 pass
1878
1879
1879 m.append(attrs)
1880 m.append(attrs)
1880
1881
1881 return m
1882 return m
1882
1883
1883 def filterclonebundleentries(repo, entries):
1884 def filterclonebundleentries(repo, entries):
1884 """Remove incompatible clone bundle manifest entries.
1885 """Remove incompatible clone bundle manifest entries.
1885
1886
1886 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1887 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1887 and returns a new list consisting of only the entries that this client
1888 and returns a new list consisting of only the entries that this client
1888 should be able to apply.
1889 should be able to apply.
1889
1890
1890 There is no guarantee we'll be able to apply all returned entries because
1891 There is no guarantee we'll be able to apply all returned entries because
1891 the metadata we use to filter on may be missing or wrong.
1892 the metadata we use to filter on may be missing or wrong.
1892 """
1893 """
1893 newentries = []
1894 newentries = []
1894 for entry in entries:
1895 for entry in entries:
1895 spec = entry.get('BUNDLESPEC')
1896 spec = entry.get('BUNDLESPEC')
1896 if spec:
1897 if spec:
1897 try:
1898 try:
1898 parsebundlespec(repo, spec, strict=True)
1899 parsebundlespec(repo, spec, strict=True)
1899 except error.InvalidBundleSpecification as e:
1900 except error.InvalidBundleSpecification as e:
1900 repo.ui.debug(str(e) + '\n')
1901 repo.ui.debug(str(e) + '\n')
1901 continue
1902 continue
1902 except error.UnsupportedBundleSpecification as e:
1903 except error.UnsupportedBundleSpecification as e:
1903 repo.ui.debug('filtering %s because unsupported bundle '
1904 repo.ui.debug('filtering %s because unsupported bundle '
1904 'spec: %s\n' % (entry['URL'], str(e)))
1905 'spec: %s\n' % (entry['URL'], str(e)))
1905 continue
1906 continue
1906
1907
1907 if 'REQUIRESNI' in entry and not sslutil.hassni:
1908 if 'REQUIRESNI' in entry and not sslutil.hassni:
1908 repo.ui.debug('filtering %s because SNI not supported\n' %
1909 repo.ui.debug('filtering %s because SNI not supported\n' %
1909 entry['URL'])
1910 entry['URL'])
1910 continue
1911 continue
1911
1912
1912 newentries.append(entry)
1913 newentries.append(entry)
1913
1914
1914 return newentries
1915 return newentries
1915
1916
1916 class clonebundleentry(object):
1917 class clonebundleentry(object):
1917 """Represents an item in a clone bundles manifest.
1918 """Represents an item in a clone bundles manifest.
1918
1919
1919 This rich class is needed to support sorting since sorted() in Python 3
1920 This rich class is needed to support sorting since sorted() in Python 3
1920 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
1921 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
1921 won't work.
1922 won't work.
1922 """
1923 """
1923
1924
1924 def __init__(self, value, prefers):
1925 def __init__(self, value, prefers):
1925 self.value = value
1926 self.value = value
1926 self.prefers = prefers
1927 self.prefers = prefers
1927
1928
1928 def _cmp(self, other):
1929 def _cmp(self, other):
1929 for prefkey, prefvalue in self.prefers:
1930 for prefkey, prefvalue in self.prefers:
1930 avalue = self.value.get(prefkey)
1931 avalue = self.value.get(prefkey)
1931 bvalue = other.value.get(prefkey)
1932 bvalue = other.value.get(prefkey)
1932
1933
1933 # Special case for b missing attribute and a matches exactly.
1934 # Special case for b missing attribute and a matches exactly.
1934 if avalue is not None and bvalue is None and avalue == prefvalue:
1935 if avalue is not None and bvalue is None and avalue == prefvalue:
1935 return -1
1936 return -1
1936
1937
1937 # Special case for a missing attribute and b matches exactly.
1938 # Special case for a missing attribute and b matches exactly.
1938 if bvalue is not None and avalue is None and bvalue == prefvalue:
1939 if bvalue is not None and avalue is None and bvalue == prefvalue:
1939 return 1
1940 return 1
1940
1941
1941 # We can't compare unless attribute present on both.
1942 # We can't compare unless attribute present on both.
1942 if avalue is None or bvalue is None:
1943 if avalue is None or bvalue is None:
1943 continue
1944 continue
1944
1945
1945 # Same values should fall back to next attribute.
1946 # Same values should fall back to next attribute.
1946 if avalue == bvalue:
1947 if avalue == bvalue:
1947 continue
1948 continue
1948
1949
1949 # Exact matches come first.
1950 # Exact matches come first.
1950 if avalue == prefvalue:
1951 if avalue == prefvalue:
1951 return -1
1952 return -1
1952 if bvalue == prefvalue:
1953 if bvalue == prefvalue:
1953 return 1
1954 return 1
1954
1955
1955 # Fall back to next attribute.
1956 # Fall back to next attribute.
1956 continue
1957 continue
1957
1958
1958 # If we got here we couldn't sort by attributes and prefers. Fall
1959 # If we got here we couldn't sort by attributes and prefers. Fall
1959 # back to index order.
1960 # back to index order.
1960 return 0
1961 return 0
1961
1962
1962 def __lt__(self, other):
1963 def __lt__(self, other):
1963 return self._cmp(other) < 0
1964 return self._cmp(other) < 0
1964
1965
1965 def __gt__(self, other):
1966 def __gt__(self, other):
1966 return self._cmp(other) > 0
1967 return self._cmp(other) > 0
1967
1968
1968 def __eq__(self, other):
1969 def __eq__(self, other):
1969 return self._cmp(other) == 0
1970 return self._cmp(other) == 0
1970
1971
1971 def __le__(self, other):
1972 def __le__(self, other):
1972 return self._cmp(other) <= 0
1973 return self._cmp(other) <= 0
1973
1974
1974 def __ge__(self, other):
1975 def __ge__(self, other):
1975 return self._cmp(other) >= 0
1976 return self._cmp(other) >= 0
1976
1977
1977 def __ne__(self, other):
1978 def __ne__(self, other):
1978 return self._cmp(other) != 0
1979 return self._cmp(other) != 0
1979
1980
1980 def sortclonebundleentries(ui, entries):
1981 def sortclonebundleentries(ui, entries):
1981 prefers = ui.configlist('ui', 'clonebundleprefers', default=[])
1982 prefers = ui.configlist('ui', 'clonebundleprefers', default=[])
1982 if not prefers:
1983 if not prefers:
1983 return list(entries)
1984 return list(entries)
1984
1985
1985 prefers = [p.split('=', 1) for p in prefers]
1986 prefers = [p.split('=', 1) for p in prefers]
1986
1987
1987 items = sorted(clonebundleentry(v, prefers) for v in entries)
1988 items = sorted(clonebundleentry(v, prefers) for v in entries)
1988 return [i.value for i in items]
1989 return [i.value for i in items]
1989
1990
1990 def trypullbundlefromurl(ui, repo, url):
1991 def trypullbundlefromurl(ui, repo, url):
1991 """Attempt to apply a bundle from a URL."""
1992 """Attempt to apply a bundle from a URL."""
1992 with repo.lock(), repo.transaction('bundleurl') as tr:
1993 with repo.lock(), repo.transaction('bundleurl') as tr:
1993 try:
1994 try:
1994 fh = urlmod.open(ui, url)
1995 fh = urlmod.open(ui, url)
1995 cg = readbundle(ui, fh, 'stream')
1996 cg = readbundle(ui, fh, 'stream')
1996
1997
1997 if isinstance(cg, bundle2.unbundle20):
1998 if isinstance(cg, bundle2.unbundle20):
1998 bundle2.processbundle(repo, cg, lambda: tr)
1999 bundle2.processbundle(repo, cg, lambda: tr)
1999 elif isinstance(cg, streamclone.streamcloneapplier):
2000 elif isinstance(cg, streamclone.streamcloneapplier):
2000 cg.apply(repo)
2001 cg.apply(repo)
2001 else:
2002 else:
2002 cg.apply(repo, 'clonebundles', url)
2003 cg.apply(repo, 'clonebundles', url)
2003 return True
2004 return True
2004 except urlerr.httperror as e:
2005 except urlerr.httperror as e:
2005 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
2006 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
2006 except urlerr.urlerror as e:
2007 except urlerr.urlerror as e:
2007 ui.warn(_('error fetching bundle: %s\n') % e.reason)
2008 ui.warn(_('error fetching bundle: %s\n') % e.reason)
2008
2009
2009 return False
2010 return False
@@ -1,490 +1,491
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
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 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or template output
16 - fm.data() to provide extra data to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.fout = sys.stdout # redirect to doctest
48 ... ui.fout = sys.stdout # redirect to doctest
49 ... ui.verbose = verbose
49 ... ui.verbose = verbose
50 ... return fn(ui, ui.formatter(fn.__name__, opts))
50 ... return fn(ui, ui.formatter(fn.__name__, opts))
51
51
52 Basic example:
52 Basic example:
53
53
54 >>> def files(ui, fm):
54 >>> def files(ui, fm):
55 ... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
55 ... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
56 ... for f in files:
56 ... for f in files:
57 ... fm.startitem()
57 ... fm.startitem()
58 ... fm.write('path', '%s', f[0])
58 ... fm.write('path', '%s', f[0])
59 ... fm.condwrite(ui.verbose, 'date', ' %s',
59 ... fm.condwrite(ui.verbose, 'date', ' %s',
60 ... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
60 ... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
61 ... fm.data(size=f[1])
61 ... fm.data(size=f[1])
62 ... fm.plain('\\n')
62 ... fm.plain('\\n')
63 ... fm.end()
63 ... fm.end()
64 >>> show(files)
64 >>> show(files)
65 foo
65 foo
66 bar
66 bar
67 >>> show(files, verbose=True)
67 >>> show(files, verbose=True)
68 foo 1970-01-01 00:00:00
68 foo 1970-01-01 00:00:00
69 bar 1970-01-01 00:00:01
69 bar 1970-01-01 00:00:01
70 >>> show(files, template='json')
70 >>> show(files, template='json')
71 [
71 [
72 {
72 {
73 "date": [0, 0],
73 "date": [0, 0],
74 "path": "foo",
74 "path": "foo",
75 "size": 123
75 "size": 123
76 },
76 },
77 {
77 {
78 "date": [1, 0],
78 "date": [1, 0],
79 "path": "bar",
79 "path": "bar",
80 "size": 456
80 "size": 456
81 }
81 }
82 ]
82 ]
83 >>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
83 >>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
84 path: foo
84 path: foo
85 date: 1970-01-01T00:00:00+00:00
85 date: 1970-01-01T00:00:00+00:00
86 path: bar
86 path: bar
87 date: 1970-01-01T00:00:01+00:00
87 date: 1970-01-01T00:00:01+00:00
88
88
89 Nested example:
89 Nested example:
90
90
91 >>> def subrepos(ui, fm):
91 >>> def subrepos(ui, fm):
92 ... fm.startitem()
92 ... fm.startitem()
93 ... fm.write('repo', '[%s]\\n', 'baz')
93 ... fm.write('repo', '[%s]\\n', 'baz')
94 ... files(ui, fm.nested('files'))
94 ... files(ui, fm.nested('files'))
95 ... fm.end()
95 ... fm.end()
96 >>> show(subrepos)
96 >>> show(subrepos)
97 [baz]
97 [baz]
98 foo
98 foo
99 bar
99 bar
100 >>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
100 >>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
101 baz: foo, bar
101 baz: foo, bar
102 """
102 """
103
103
104 from __future__ import absolute_import
104 from __future__ import absolute_import
105
105
106 import collections
106 import collections
107 import contextlib
107 import contextlib
108 import itertools
108 import itertools
109 import os
109 import os
110
110
111 from .i18n import _
111 from .i18n import _
112 from .node import (
112 from .node import (
113 hex,
113 hex,
114 short,
114 short,
115 )
115 )
116
116
117 from . import (
117 from . import (
118 error,
118 error,
119 pycompat,
119 pycompat,
120 templatefilters,
120 templatefilters,
121 templatekw,
121 templatekw,
122 templater,
122 templater,
123 util,
123 util,
124 )
124 )
125
125
126 pickle = util.pickle
126 pickle = util.pickle
127
127
128 class _nullconverter(object):
128 class _nullconverter(object):
129 '''convert non-primitive data types to be processed by formatter'''
129 '''convert non-primitive data types to be processed by formatter'''
130 @staticmethod
130 @staticmethod
131 def formatdate(date, fmt):
131 def formatdate(date, fmt):
132 '''convert date tuple to appropriate format'''
132 '''convert date tuple to appropriate format'''
133 return date
133 return date
134 @staticmethod
134 @staticmethod
135 def formatdict(data, key, value, fmt, sep):
135 def formatdict(data, key, value, fmt, sep):
136 '''convert dict or key-value pairs to appropriate dict format'''
136 '''convert dict or key-value pairs to appropriate dict format'''
137 # use plain dict instead of util.sortdict so that data can be
137 # use plain dict instead of util.sortdict so that data can be
138 # serialized as a builtin dict in pickle output
138 # serialized as a builtin dict in pickle output
139 return dict(data)
139 return dict(data)
140 @staticmethod
140 @staticmethod
141 def formatlist(data, name, fmt, sep):
141 def formatlist(data, name, fmt, sep):
142 '''convert iterable to appropriate list format'''
142 '''convert iterable to appropriate list format'''
143 return list(data)
143 return list(data)
144
144
145 class baseformatter(object):
145 class baseformatter(object):
146 def __init__(self, ui, topic, opts, converter):
146 def __init__(self, ui, topic, opts, converter):
147 self._ui = ui
147 self._ui = ui
148 self._topic = topic
148 self._topic = topic
149 self._style = opts.get("style")
149 self._style = opts.get("style")
150 self._template = opts.get("template")
150 self._template = opts.get("template")
151 self._converter = converter
151 self._converter = converter
152 self._item = None
152 self._item = None
153 # function to convert node to string suitable for this output
153 # function to convert node to string suitable for this output
154 self.hexfunc = hex
154 self.hexfunc = hex
155 def __enter__(self):
155 def __enter__(self):
156 return self
156 return self
157 def __exit__(self, exctype, excvalue, traceback):
157 def __exit__(self, exctype, excvalue, traceback):
158 if exctype is None:
158 if exctype is None:
159 self.end()
159 self.end()
160 def _showitem(self):
160 def _showitem(self):
161 '''show a formatted item once all data is collected'''
161 '''show a formatted item once all data is collected'''
162 pass
162 pass
163 def startitem(self):
163 def startitem(self):
164 '''begin an item in the format list'''
164 '''begin an item in the format list'''
165 if self._item is not None:
165 if self._item is not None:
166 self._showitem()
166 self._showitem()
167 self._item = {}
167 self._item = {}
168 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
168 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
169 '''convert date tuple to appropriate format'''
169 '''convert date tuple to appropriate format'''
170 return self._converter.formatdate(date, fmt)
170 return self._converter.formatdate(date, fmt)
171 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
171 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
172 '''convert dict or key-value pairs to appropriate dict format'''
172 '''convert dict or key-value pairs to appropriate dict format'''
173 return self._converter.formatdict(data, key, value, fmt, sep)
173 return self._converter.formatdict(data, key, value, fmt, sep)
174 def formatlist(self, data, name, fmt='%s', sep=' '):
174 def formatlist(self, data, name, fmt='%s', sep=' '):
175 '''convert iterable to appropriate list format'''
175 '''convert iterable to appropriate list format'''
176 # name is mandatory argument for now, but it could be optional if
176 # name is mandatory argument for now, but it could be optional if
177 # we have default template keyword, e.g. {item}
177 # we have default template keyword, e.g. {item}
178 return self._converter.formatlist(data, name, fmt, sep)
178 return self._converter.formatlist(data, name, fmt, sep)
179 def context(self, **ctxs):
179 def context(self, **ctxs):
180 '''insert context objects to be used to render template keywords'''
180 '''insert context objects to be used to render template keywords'''
181 pass
181 pass
182 def data(self, **data):
182 def data(self, **data):
183 '''insert data into item that's not shown in default output'''
183 '''insert data into item that's not shown in default output'''
184 data = pycompat.byteskwargs(data)
184 data = pycompat.byteskwargs(data)
185 self._item.update(data)
185 self._item.update(data)
186 def write(self, fields, deftext, *fielddata, **opts):
186 def write(self, fields, deftext, *fielddata, **opts):
187 '''do default text output while assigning data to item'''
187 '''do default text output while assigning data to item'''
188 fieldkeys = fields.split()
188 fieldkeys = fields.split()
189 assert len(fieldkeys) == len(fielddata)
189 assert len(fieldkeys) == len(fielddata)
190 self._item.update(zip(fieldkeys, fielddata))
190 self._item.update(zip(fieldkeys, fielddata))
191 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
191 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
192 '''do conditional write (primarily for plain formatter)'''
192 '''do conditional write (primarily for plain formatter)'''
193 fieldkeys = fields.split()
193 fieldkeys = fields.split()
194 assert len(fieldkeys) == len(fielddata)
194 assert len(fieldkeys) == len(fielddata)
195 self._item.update(zip(fieldkeys, fielddata))
195 self._item.update(zip(fieldkeys, fielddata))
196 def plain(self, text, **opts):
196 def plain(self, text, **opts):
197 '''show raw text for non-templated mode'''
197 '''show raw text for non-templated mode'''
198 pass
198 pass
199 def isplain(self):
199 def isplain(self):
200 '''check for plain formatter usage'''
200 '''check for plain formatter usage'''
201 return False
201 return False
202 def nested(self, field):
202 def nested(self, field):
203 '''sub formatter to store nested data in the specified field'''
203 '''sub formatter to store nested data in the specified field'''
204 self._item[field] = data = []
204 self._item[field] = data = []
205 return _nestedformatter(self._ui, self._converter, data)
205 return _nestedformatter(self._ui, self._converter, data)
206 def end(self):
206 def end(self):
207 '''end output for the formatter'''
207 '''end output for the formatter'''
208 if self._item is not None:
208 if self._item is not None:
209 self._showitem()
209 self._showitem()
210
210
211 def nullformatter(ui, topic):
211 def nullformatter(ui, topic):
212 '''formatter that prints nothing'''
212 '''formatter that prints nothing'''
213 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
213 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
214
214
215 class _nestedformatter(baseformatter):
215 class _nestedformatter(baseformatter):
216 '''build sub items and store them in the parent formatter'''
216 '''build sub items and store them in the parent formatter'''
217 def __init__(self, ui, converter, data):
217 def __init__(self, ui, converter, data):
218 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
218 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
219 self._data = data
219 self._data = data
220 def _showitem(self):
220 def _showitem(self):
221 self._data.append(self._item)
221 self._data.append(self._item)
222
222
223 def _iteritems(data):
223 def _iteritems(data):
224 '''iterate key-value pairs in stable order'''
224 '''iterate key-value pairs in stable order'''
225 if isinstance(data, dict):
225 if isinstance(data, dict):
226 return sorted(data.iteritems())
226 return sorted(data.iteritems())
227 return data
227 return data
228
228
229 class _plainconverter(object):
229 class _plainconverter(object):
230 '''convert non-primitive data types to text'''
230 '''convert non-primitive data types to text'''
231 @staticmethod
231 @staticmethod
232 def formatdate(date, fmt):
232 def formatdate(date, fmt):
233 '''stringify date tuple in the given format'''
233 '''stringify date tuple in the given format'''
234 return util.datestr(date, fmt)
234 return util.datestr(date, fmt)
235 @staticmethod
235 @staticmethod
236 def formatdict(data, key, value, fmt, sep):
236 def formatdict(data, key, value, fmt, sep):
237 '''stringify key-value pairs separated by sep'''
237 '''stringify key-value pairs separated by sep'''
238 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
238 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
239 @staticmethod
239 @staticmethod
240 def formatlist(data, name, fmt, sep):
240 def formatlist(data, name, fmt, sep):
241 '''stringify iterable separated by sep'''
241 '''stringify iterable separated by sep'''
242 return sep.join(fmt % e for e in data)
242 return sep.join(fmt % e for e in data)
243
243
244 class plainformatter(baseformatter):
244 class plainformatter(baseformatter):
245 '''the default text output scheme'''
245 '''the default text output scheme'''
246 def __init__(self, ui, out, topic, opts):
246 def __init__(self, ui, out, topic, opts):
247 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
247 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
248 if ui.debugflag:
248 if ui.debugflag:
249 self.hexfunc = hex
249 self.hexfunc = hex
250 else:
250 else:
251 self.hexfunc = short
251 self.hexfunc = short
252 if ui is out:
252 if ui is out:
253 self._write = ui.write
253 self._write = ui.write
254 else:
254 else:
255 self._write = lambda s, **opts: out.write(s)
255 self._write = lambda s, **opts: out.write(s)
256 def startitem(self):
256 def startitem(self):
257 pass
257 pass
258 def data(self, **data):
258 def data(self, **data):
259 pass
259 pass
260 def write(self, fields, deftext, *fielddata, **opts):
260 def write(self, fields, deftext, *fielddata, **opts):
261 self._write(deftext % fielddata, **opts)
261 self._write(deftext % fielddata, **opts)
262 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
262 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
263 '''do conditional write'''
263 '''do conditional write'''
264 if cond:
264 if cond:
265 self._write(deftext % fielddata, **opts)
265 self._write(deftext % fielddata, **opts)
266 def plain(self, text, **opts):
266 def plain(self, text, **opts):
267 self._write(text, **opts)
267 self._write(text, **opts)
268 def isplain(self):
268 def isplain(self):
269 return True
269 return True
270 def nested(self, field):
270 def nested(self, field):
271 # nested data will be directly written to ui
271 # nested data will be directly written to ui
272 return self
272 return self
273 def end(self):
273 def end(self):
274 pass
274 pass
275
275
276 class debugformatter(baseformatter):
276 class debugformatter(baseformatter):
277 def __init__(self, ui, out, topic, opts):
277 def __init__(self, ui, out, topic, opts):
278 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
278 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
279 self._out = out
279 self._out = out
280 self._out.write("%s = [\n" % self._topic)
280 self._out.write("%s = [\n" % self._topic)
281 def _showitem(self):
281 def _showitem(self):
282 self._out.write(" " + repr(self._item) + ",\n")
282 self._out.write(" " + repr(self._item) + ",\n")
283 def end(self):
283 def end(self):
284 baseformatter.end(self)
284 baseformatter.end(self)
285 self._out.write("]\n")
285 self._out.write("]\n")
286
286
287 class pickleformatter(baseformatter):
287 class pickleformatter(baseformatter):
288 def __init__(self, ui, out, topic, opts):
288 def __init__(self, ui, out, topic, opts):
289 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
289 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
290 self._out = out
290 self._out = out
291 self._data = []
291 self._data = []
292 def _showitem(self):
292 def _showitem(self):
293 self._data.append(self._item)
293 self._data.append(self._item)
294 def end(self):
294 def end(self):
295 baseformatter.end(self)
295 baseformatter.end(self)
296 self._out.write(pickle.dumps(self._data))
296 self._out.write(pickle.dumps(self._data))
297
297
298 class jsonformatter(baseformatter):
298 class jsonformatter(baseformatter):
299 def __init__(self, ui, out, topic, opts):
299 def __init__(self, ui, out, topic, opts):
300 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
300 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
301 self._out = out
301 self._out = out
302 self._out.write("[")
302 self._out.write("[")
303 self._first = True
303 self._first = True
304 def _showitem(self):
304 def _showitem(self):
305 if self._first:
305 if self._first:
306 self._first = False
306 self._first = False
307 else:
307 else:
308 self._out.write(",")
308 self._out.write(",")
309
309
310 self._out.write("\n {\n")
310 self._out.write("\n {\n")
311 first = True
311 first = True
312 for k, v in sorted(self._item.items()):
312 for k, v in sorted(self._item.items()):
313 if first:
313 if first:
314 first = False
314 first = False
315 else:
315 else:
316 self._out.write(",\n")
316 self._out.write(",\n")
317 u = templatefilters.json(v, paranoid=False)
317 u = templatefilters.json(v, paranoid=False)
318 self._out.write(' "%s": %s' % (k, u))
318 self._out.write(' "%s": %s' % (k, u))
319 self._out.write("\n }")
319 self._out.write("\n }")
320 def end(self):
320 def end(self):
321 baseformatter.end(self)
321 baseformatter.end(self)
322 self._out.write("\n]\n")
322 self._out.write("\n]\n")
323
323
324 class _templateconverter(object):
324 class _templateconverter(object):
325 '''convert non-primitive data types to be processed by templater'''
325 '''convert non-primitive data types to be processed by templater'''
326 @staticmethod
326 @staticmethod
327 def formatdate(date, fmt):
327 def formatdate(date, fmt):
328 '''return date tuple'''
328 '''return date tuple'''
329 return date
329 return date
330 @staticmethod
330 @staticmethod
331 def formatdict(data, key, value, fmt, sep):
331 def formatdict(data, key, value, fmt, sep):
332 '''build object that can be evaluated as either plain string or dict'''
332 '''build object that can be evaluated as either plain string or dict'''
333 data = util.sortdict(_iteritems(data))
333 data = util.sortdict(_iteritems(data))
334 def f():
334 def f():
335 yield _plainconverter.formatdict(data, key, value, fmt, sep)
335 yield _plainconverter.formatdict(data, key, value, fmt, sep)
336 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt,
336 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt,
337 gen=f())
337 gen=f())
338 @staticmethod
338 @staticmethod
339 def formatlist(data, name, fmt, sep):
339 def formatlist(data, name, fmt, sep):
340 '''build object that can be evaluated as either plain string or list'''
340 '''build object that can be evaluated as either plain string or list'''
341 data = list(data)
341 data = list(data)
342 def f():
342 def f():
343 yield _plainconverter.formatlist(data, name, fmt, sep)
343 yield _plainconverter.formatlist(data, name, fmt, sep)
344 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f())
344 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f())
345
345
346 class templateformatter(baseformatter):
346 class templateformatter(baseformatter):
347 def __init__(self, ui, out, topic, opts):
347 def __init__(self, ui, out, topic, opts):
348 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
348 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
349 self._out = out
349 self._out = out
350 spec = lookuptemplate(ui, topic, opts.get('template', ''))
350 spec = lookuptemplate(ui, topic, opts.get('template', ''))
351 self._tref = spec.ref
351 self._tref = spec.ref
352 self._t = loadtemplater(ui, spec, cache=templatekw.defaulttempl)
352 self._t = loadtemplater(ui, spec, cache=templatekw.defaulttempl)
353 self._counter = itertools.count()
353 self._counter = itertools.count()
354 self._cache = {} # for templatekw/funcs to store reusable data
354 self._cache = {} # for templatekw/funcs to store reusable data
355 def context(self, **ctxs):
355 def context(self, **ctxs):
356 '''insert context objects to be used to render template keywords'''
356 '''insert context objects to be used to render template keywords'''
357 assert all(k == 'ctx' for k in ctxs)
357 assert all(k == 'ctx' for k in ctxs)
358 self._item.update(ctxs)
358 self._item.update(ctxs)
359 def _showitem(self):
359 def _showitem(self):
360 # TODO: add support for filectx. probably each template keyword or
360 # TODO: add support for filectx. probably each template keyword or
361 # function will have to declare dependent resources. e.g.
361 # function will have to declare dependent resources. e.g.
362 # @templatekeyword(..., requires=('ctx',))
362 # @templatekeyword(..., requires=('ctx',))
363 props = {}
363 props = {}
364 if 'ctx' in self._item:
364 if 'ctx' in self._item:
365 props.update(templatekw.keywords)
365 props.update(templatekw.keywords)
366 props['index'] = next(self._counter)
366 props['index'] = next(self._counter)
367 # explicitly-defined fields precede templatekw
367 # explicitly-defined fields precede templatekw
368 props.update(self._item)
368 props.update(self._item)
369 if 'ctx' in self._item:
369 if 'ctx' in self._item:
370 # but template resources must be always available
370 # but template resources must be always available
371 props['templ'] = self._t
371 props['templ'] = self._t
372 props['repo'] = props['ctx'].repo()
372 props['repo'] = props['ctx'].repo()
373 props['revcache'] = {}
373 props['revcache'] = {}
374 props = pycompat.strkwargs(props)
374 g = self._t(self._tref, ui=self._ui, cache=self._cache, **props)
375 g = self._t(self._tref, ui=self._ui, cache=self._cache, **props)
375 self._out.write(templater.stringify(g))
376 self._out.write(templater.stringify(g))
376
377
377 templatespec = collections.namedtuple(r'templatespec',
378 templatespec = collections.namedtuple(r'templatespec',
378 r'ref tmpl mapfile')
379 r'ref tmpl mapfile')
379
380
380 def lookuptemplate(ui, topic, tmpl):
381 def lookuptemplate(ui, topic, tmpl):
381 """Find the template matching the given -T/--template spec 'tmpl'
382 """Find the template matching the given -T/--template spec 'tmpl'
382
383
383 'tmpl' can be any of the following:
384 'tmpl' can be any of the following:
384
385
385 - a literal template (e.g. '{rev}')
386 - a literal template (e.g. '{rev}')
386 - a map-file name or path (e.g. 'changelog')
387 - a map-file name or path (e.g. 'changelog')
387 - a reference to [templates] in config file
388 - a reference to [templates] in config file
388 - a path to raw template file
389 - a path to raw template file
389
390
390 A map file defines a stand-alone template environment. If a map file
391 A map file defines a stand-alone template environment. If a map file
391 selected, all templates defined in the file will be loaded, and the
392 selected, all templates defined in the file will be loaded, and the
392 template matching the given topic will be rendered. No aliases will be
393 template matching the given topic will be rendered. No aliases will be
393 loaded from user config.
394 loaded from user config.
394
395
395 If no map file selected, all templates in [templates] section will be
396 If no map file selected, all templates in [templates] section will be
396 available as well as aliases in [templatealias].
397 available as well as aliases in [templatealias].
397 """
398 """
398
399
399 # looks like a literal template?
400 # looks like a literal template?
400 if '{' in tmpl:
401 if '{' in tmpl:
401 return templatespec('', tmpl, None)
402 return templatespec('', tmpl, None)
402
403
403 # perhaps a stock style?
404 # perhaps a stock style?
404 if not os.path.split(tmpl)[0]:
405 if not os.path.split(tmpl)[0]:
405 mapname = (templater.templatepath('map-cmdline.' + tmpl)
406 mapname = (templater.templatepath('map-cmdline.' + tmpl)
406 or templater.templatepath(tmpl))
407 or templater.templatepath(tmpl))
407 if mapname and os.path.isfile(mapname):
408 if mapname and os.path.isfile(mapname):
408 return templatespec(topic, None, mapname)
409 return templatespec(topic, None, mapname)
409
410
410 # perhaps it's a reference to [templates]
411 # perhaps it's a reference to [templates]
411 if ui.config('templates', tmpl):
412 if ui.config('templates', tmpl):
412 return templatespec(tmpl, None, None)
413 return templatespec(tmpl, None, None)
413
414
414 if tmpl == 'list':
415 if tmpl == 'list':
415 ui.write(_("available styles: %s\n") % templater.stylelist())
416 ui.write(_("available styles: %s\n") % templater.stylelist())
416 raise error.Abort(_("specify a template"))
417 raise error.Abort(_("specify a template"))
417
418
418 # perhaps it's a path to a map or a template
419 # perhaps it's a path to a map or a template
419 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
420 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
420 # is it a mapfile for a style?
421 # is it a mapfile for a style?
421 if os.path.basename(tmpl).startswith("map-"):
422 if os.path.basename(tmpl).startswith("map-"):
422 return templatespec(topic, None, os.path.realpath(tmpl))
423 return templatespec(topic, None, os.path.realpath(tmpl))
423 with util.posixfile(tmpl, 'rb') as f:
424 with util.posixfile(tmpl, 'rb') as f:
424 tmpl = f.read()
425 tmpl = f.read()
425 return templatespec('', tmpl, None)
426 return templatespec('', tmpl, None)
426
427
427 # constant string?
428 # constant string?
428 return templatespec('', tmpl, None)
429 return templatespec('', tmpl, None)
429
430
430 def loadtemplater(ui, spec, cache=None):
431 def loadtemplater(ui, spec, cache=None):
431 """Create a templater from either a literal template or loading from
432 """Create a templater from either a literal template or loading from
432 a map file"""
433 a map file"""
433 assert not (spec.tmpl and spec.mapfile)
434 assert not (spec.tmpl and spec.mapfile)
434 if spec.mapfile:
435 if spec.mapfile:
435 return templater.templater.frommapfile(spec.mapfile, cache=cache)
436 return templater.templater.frommapfile(spec.mapfile, cache=cache)
436 return maketemplater(ui, spec.tmpl, cache=cache)
437 return maketemplater(ui, spec.tmpl, cache=cache)
437
438
438 def maketemplater(ui, tmpl, cache=None):
439 def maketemplater(ui, tmpl, cache=None):
439 """Create a templater from a string template 'tmpl'"""
440 """Create a templater from a string template 'tmpl'"""
440 aliases = ui.configitems('templatealias')
441 aliases = ui.configitems('templatealias')
441 t = templater.templater(cache=cache, aliases=aliases)
442 t = templater.templater(cache=cache, aliases=aliases)
442 t.cache.update((k, templater.unquotestring(v))
443 t.cache.update((k, templater.unquotestring(v))
443 for k, v in ui.configitems('templates'))
444 for k, v in ui.configitems('templates'))
444 if tmpl:
445 if tmpl:
445 t.cache[''] = tmpl
446 t.cache[''] = tmpl
446 return t
447 return t
447
448
448 def formatter(ui, out, topic, opts):
449 def formatter(ui, out, topic, opts):
449 template = opts.get("template", "")
450 template = opts.get("template", "")
450 if template == "json":
451 if template == "json":
451 return jsonformatter(ui, out, topic, opts)
452 return jsonformatter(ui, out, topic, opts)
452 elif template == "pickle":
453 elif template == "pickle":
453 return pickleformatter(ui, out, topic, opts)
454 return pickleformatter(ui, out, topic, opts)
454 elif template == "debug":
455 elif template == "debug":
455 return debugformatter(ui, out, topic, opts)
456 return debugformatter(ui, out, topic, opts)
456 elif template != "":
457 elif template != "":
457 return templateformatter(ui, out, topic, opts)
458 return templateformatter(ui, out, topic, opts)
458 # developer config: ui.formatdebug
459 # developer config: ui.formatdebug
459 elif ui.configbool('ui', 'formatdebug'):
460 elif ui.configbool('ui', 'formatdebug'):
460 return debugformatter(ui, out, topic, opts)
461 return debugformatter(ui, out, topic, opts)
461 # deprecated config: ui.formatjson
462 # deprecated config: ui.formatjson
462 elif ui.configbool('ui', 'formatjson'):
463 elif ui.configbool('ui', 'formatjson'):
463 return jsonformatter(ui, out, topic, opts)
464 return jsonformatter(ui, out, topic, opts)
464 return plainformatter(ui, out, topic, opts)
465 return plainformatter(ui, out, topic, opts)
465
466
466 @contextlib.contextmanager
467 @contextlib.contextmanager
467 def openformatter(ui, filename, topic, opts):
468 def openformatter(ui, filename, topic, opts):
468 """Create a formatter that writes outputs to the specified file
469 """Create a formatter that writes outputs to the specified file
469
470
470 Must be invoked using the 'with' statement.
471 Must be invoked using the 'with' statement.
471 """
472 """
472 with util.posixfile(filename, 'wb') as out:
473 with util.posixfile(filename, 'wb') as out:
473 with formatter(ui, out, topic, opts) as fm:
474 with formatter(ui, out, topic, opts) as fm:
474 yield fm
475 yield fm
475
476
476 @contextlib.contextmanager
477 @contextlib.contextmanager
477 def _neverending(fm):
478 def _neverending(fm):
478 yield fm
479 yield fm
479
480
480 def maybereopen(fm, filename, opts):
481 def maybereopen(fm, filename, opts):
481 """Create a formatter backed by file if filename specified, else return
482 """Create a formatter backed by file if filename specified, else return
482 the given formatter
483 the given formatter
483
484
484 Must be invoked using the 'with' statement. This will never call fm.end()
485 Must be invoked using the 'with' statement. This will never call fm.end()
485 of the given formatter.
486 of the given formatter.
486 """
487 """
487 if filename:
488 if filename:
488 return openformatter(fm._ui, filename, fm._topic, opts)
489 return openformatter(fm._ui, filename, fm._topic, opts)
489 else:
490 else:
490 return _neverending(fm)
491 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now