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