##// END OF EJS Templates
phase: improve retractboundary perf...
Durham Goode -
r26909:e3611881 default
parent child Browse files
Show More
@@ -1,478 +1,488
1 """ Mercurial phases support code
1 """ Mercurial phases support code
2
2
3 ---
3 ---
4
4
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 Logilab SA <contact@logilab.fr>
6 Logilab SA <contact@logilab.fr>
7 Augie Fackler <durin42@gmail.com>
7 Augie Fackler <durin42@gmail.com>
8
8
9 This software may be used and distributed according to the terms
9 This software may be used and distributed according to the terms
10 of the GNU General Public License version 2 or any later version.
10 of the GNU General Public License version 2 or any later version.
11
11
12 ---
12 ---
13
13
14 This module implements most phase logic in mercurial.
14 This module implements most phase logic in mercurial.
15
15
16
16
17 Basic Concept
17 Basic Concept
18 =============
18 =============
19
19
20 A 'changeset phase' is an indicator that tells us how a changeset is
20 A 'changeset phase' is an indicator that tells us how a changeset is
21 manipulated and communicated. The details of each phase is described
21 manipulated and communicated. The details of each phase is described
22 below, here we describe the properties they have in common.
22 below, here we describe the properties they have in common.
23
23
24 Like bookmarks, phases are not stored in history and thus are not
24 Like bookmarks, phases are not stored in history and thus are not
25 permanent and leave no audit trail.
25 permanent and leave no audit trail.
26
26
27 First, no changeset can be in two phases at once. Phases are ordered,
27 First, no changeset can be in two phases at once. Phases are ordered,
28 so they can be considered from lowest to highest. The default, lowest
28 so they can be considered from lowest to highest. The default, lowest
29 phase is 'public' - this is the normal phase of existing changesets. A
29 phase is 'public' - this is the normal phase of existing changesets. A
30 child changeset can not be in a lower phase than its parents.
30 child changeset can not be in a lower phase than its parents.
31
31
32 These phases share a hierarchy of traits:
32 These phases share a hierarchy of traits:
33
33
34 immutable shared
34 immutable shared
35 public: X X
35 public: X X
36 draft: X
36 draft: X
37 secret:
37 secret:
38
38
39 Local commits are draft by default.
39 Local commits are draft by default.
40
40
41 Phase Movement and Exchange
41 Phase Movement and Exchange
42 ===========================
42 ===========================
43
43
44 Phase data is exchanged by pushkey on pull and push. Some servers have
44 Phase data is exchanged by pushkey on pull and push. Some servers have
45 a publish option set, we call such a server a "publishing server".
45 a publish option set, we call such a server a "publishing server".
46 Pushing a draft changeset to a publishing server changes the phase to
46 Pushing a draft changeset to a publishing server changes the phase to
47 public.
47 public.
48
48
49 A small list of fact/rules define the exchange of phase:
49 A small list of fact/rules define the exchange of phase:
50
50
51 * old client never changes server states
51 * old client never changes server states
52 * pull never changes server states
52 * pull never changes server states
53 * publish and old server changesets are seen as public by client
53 * publish and old server changesets are seen as public by client
54 * any secret changeset seen in another repository is lowered to at
54 * any secret changeset seen in another repository is lowered to at
55 least draft
55 least draft
56
56
57 Here is the final table summing up the 49 possible use cases of phase
57 Here is the final table summing up the 49 possible use cases of phase
58 exchange:
58 exchange:
59
59
60 server
60 server
61 old publish non-publish
61 old publish non-publish
62 N X N D P N D P
62 N X N D P N D P
63 old client
63 old client
64 pull
64 pull
65 N - X/X - X/D X/P - X/D X/P
65 N - X/X - X/D X/P - X/D X/P
66 X - X/X - X/D X/P - X/D X/P
66 X - X/X - X/D X/P - X/D X/P
67 push
67 push
68 X X/X X/X X/P X/P X/P X/D X/D X/P
68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 new client
69 new client
70 pull
70 pull
71 N - P/X - P/D P/P - D/D P/P
71 N - P/X - P/D P/P - D/D P/P
72 D - P/X - P/D P/P - D/D P/P
72 D - P/X - P/D P/P - D/D P/P
73 P - P/X - P/D P/P - P/D P/P
73 P - P/X - P/D P/P - P/D P/P
74 push
74 push
75 D P/X P/X P/P P/P P/P D/D D/D P/P
75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 P P/X P/X P/P P/P P/P P/P P/P P/P
76 P P/X P/X P/P P/P P/P P/P P/P P/P
77
77
78 Legend:
78 Legend:
79
79
80 A/B = final state on client / state on server
80 A/B = final state on client / state on server
81
81
82 * N = new/not present,
82 * N = new/not present,
83 * P = public,
83 * P = public,
84 * D = draft,
84 * D = draft,
85 * X = not tracked (i.e., the old client or server has no internal
85 * X = not tracked (i.e., the old client or server has no internal
86 way of recording the phase.)
86 way of recording the phase.)
87
87
88 passive = only pushes
88 passive = only pushes
89
89
90
90
91 A cell here can be read like this:
91 A cell here can be read like this:
92
92
93 "When a new client pushes a draft changeset (D) to a publishing
93 "When a new client pushes a draft changeset (D) to a publishing
94 server where it's not present (N), it's marked public on both
94 server where it's not present (N), it's marked public on both
95 sides (P/P)."
95 sides (P/P)."
96
96
97 Note: old client behave as a publishing server with draft only content
97 Note: old client behave as a publishing server with draft only content
98 - other people see it as public
98 - other people see it as public
99 - content is pushed as draft
99 - content is pushed as draft
100
100
101 """
101 """
102
102
103 from __future__ import absolute_import
103 from __future__ import absolute_import
104
104
105 import errno
105 import errno
106 import os
106 import os
107
107
108 from .i18n import _
108 from .i18n import _
109 from .node import (
109 from .node import (
110 bin,
110 bin,
111 hex,
111 hex,
112 nullid,
112 nullid,
113 nullrev,
113 nullrev,
114 short,
114 short,
115 )
115 )
116 from . import (
116 from . import (
117 error,
117 error,
118 )
118 )
119
119
120 allphases = public, draft, secret = range(3)
120 allphases = public, draft, secret = range(3)
121 trackedphases = allphases[1:]
121 trackedphases = allphases[1:]
122 phasenames = ['public', 'draft', 'secret']
122 phasenames = ['public', 'draft', 'secret']
123
123
124 def _readroots(repo, phasedefaults=None):
124 def _readroots(repo, phasedefaults=None):
125 """Read phase roots from disk
125 """Read phase roots from disk
126
126
127 phasedefaults is a list of fn(repo, roots) callable, which are
127 phasedefaults is a list of fn(repo, roots) callable, which are
128 executed if the phase roots file does not exist. When phases are
128 executed if the phase roots file does not exist. When phases are
129 being initialized on an existing repository, this could be used to
129 being initialized on an existing repository, this could be used to
130 set selected changesets phase to something else than public.
130 set selected changesets phase to something else than public.
131
131
132 Return (roots, dirty) where dirty is true if roots differ from
132 Return (roots, dirty) where dirty is true if roots differ from
133 what is being stored.
133 what is being stored.
134 """
134 """
135 repo = repo.unfiltered()
135 repo = repo.unfiltered()
136 dirty = False
136 dirty = False
137 roots = [set() for i in allphases]
137 roots = [set() for i in allphases]
138 try:
138 try:
139 f = None
139 f = None
140 if 'HG_PENDING' in os.environ:
140 if 'HG_PENDING' in os.environ:
141 try:
141 try:
142 f = repo.svfs('phaseroots.pending')
142 f = repo.svfs('phaseroots.pending')
143 except IOError as inst:
143 except IOError as inst:
144 if inst.errno != errno.ENOENT:
144 if inst.errno != errno.ENOENT:
145 raise
145 raise
146 if f is None:
146 if f is None:
147 f = repo.svfs('phaseroots')
147 f = repo.svfs('phaseroots')
148 try:
148 try:
149 for line in f:
149 for line in f:
150 phase, nh = line.split()
150 phase, nh = line.split()
151 roots[int(phase)].add(bin(nh))
151 roots[int(phase)].add(bin(nh))
152 finally:
152 finally:
153 f.close()
153 f.close()
154 except IOError as inst:
154 except IOError as inst:
155 if inst.errno != errno.ENOENT:
155 if inst.errno != errno.ENOENT:
156 raise
156 raise
157 if phasedefaults:
157 if phasedefaults:
158 for f in phasedefaults:
158 for f in phasedefaults:
159 roots = f(repo, roots)
159 roots = f(repo, roots)
160 dirty = True
160 dirty = True
161 return roots, dirty
161 return roots, dirty
162
162
163 class phasecache(object):
163 class phasecache(object):
164 def __init__(self, repo, phasedefaults, _load=True):
164 def __init__(self, repo, phasedefaults, _load=True):
165 if _load:
165 if _load:
166 # Cheap trick to allow shallow-copy without copy module
166 # Cheap trick to allow shallow-copy without copy module
167 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
167 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
168 self._phaserevs = None
168 self._phaserevs = None
169 self._phasesets = None
169 self._phasesets = None
170 self.filterunknown(repo)
170 self.filterunknown(repo)
171 self.opener = repo.svfs
171 self.opener = repo.svfs
172
172
173 def copy(self):
173 def copy(self):
174 # Shallow copy meant to ensure isolation in
174 # Shallow copy meant to ensure isolation in
175 # advance/retractboundary(), nothing more.
175 # advance/retractboundary(), nothing more.
176 ph = self.__class__(None, None, _load=False)
176 ph = self.__class__(None, None, _load=False)
177 ph.phaseroots = self.phaseroots[:]
177 ph.phaseroots = self.phaseroots[:]
178 ph.dirty = self.dirty
178 ph.dirty = self.dirty
179 ph.opener = self.opener
179 ph.opener = self.opener
180 ph._phaserevs = self._phaserevs
180 ph._phaserevs = self._phaserevs
181 ph._phasesets = self._phasesets
181 ph._phasesets = self._phasesets
182 return ph
182 return ph
183
183
184 def replace(self, phcache):
184 def replace(self, phcache):
185 """replace all values in 'self' with content of phcache"""
185 """replace all values in 'self' with content of phcache"""
186 for a in ('phaseroots', 'dirty', 'opener', '_phaserevs', '_phasesets'):
186 for a in ('phaseroots', 'dirty', 'opener', '_phaserevs', '_phasesets'):
187 setattr(self, a, getattr(phcache, a))
187 setattr(self, a, getattr(phcache, a))
188
188
189 def _getphaserevsnative(self, repo):
189 def _getphaserevsnative(self, repo):
190 repo = repo.unfiltered()
190 repo = repo.unfiltered()
191 nativeroots = []
191 nativeroots = []
192 for phase in trackedphases:
192 for phase in trackedphases:
193 nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase]))
193 nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase]))
194 return repo.changelog.computephases(nativeroots)
194 return repo.changelog.computephases(nativeroots)
195
195
196 def _computephaserevspure(self, repo):
196 def _computephaserevspure(self, repo):
197 repo = repo.unfiltered()
197 repo = repo.unfiltered()
198 revs = [public] * len(repo.changelog)
198 revs = [public] * len(repo.changelog)
199 self._phaserevs = revs
199 self._phaserevs = revs
200 self._populatephaseroots(repo)
200 self._populatephaseroots(repo)
201 for phase in trackedphases:
201 for phase in trackedphases:
202 roots = map(repo.changelog.rev, self.phaseroots[phase])
202 roots = map(repo.changelog.rev, self.phaseroots[phase])
203 if roots:
203 if roots:
204 for rev in roots:
204 for rev in roots:
205 revs[rev] = phase
205 revs[rev] = phase
206 for rev in repo.changelog.descendants(roots):
206 for rev in repo.changelog.descendants(roots):
207 revs[rev] = phase
207 revs[rev] = phase
208
208
209 def loadphaserevs(self, repo):
209 def loadphaserevs(self, repo):
210 """ensure phase information is loaded in the object"""
210 """ensure phase information is loaded in the object"""
211 if self._phaserevs is None:
211 if self._phaserevs is None:
212 try:
212 try:
213 if repo.ui.configbool('experimental',
213 if repo.ui.configbool('experimental',
214 'nativephaseskillswitch'):
214 'nativephaseskillswitch'):
215 self._computephaserevspure(repo)
215 self._computephaserevspure(repo)
216 else:
216 else:
217 res = self._getphaserevsnative(repo)
217 res = self._getphaserevsnative(repo)
218 self._phaserevs, self._phasesets = res
218 self._phaserevs, self._phasesets = res
219 except AttributeError:
219 except AttributeError:
220 self._computephaserevspure(repo)
220 self._computephaserevspure(repo)
221
221
222 def invalidate(self):
222 def invalidate(self):
223 self._phaserevs = None
223 self._phaserevs = None
224 self._phasesets = None
224 self._phasesets = None
225
225
226 def _populatephaseroots(self, repo):
226 def _populatephaseroots(self, repo):
227 """Fills the _phaserevs cache with phases for the roots.
227 """Fills the _phaserevs cache with phases for the roots.
228 """
228 """
229 cl = repo.changelog
229 cl = repo.changelog
230 phaserevs = self._phaserevs
230 phaserevs = self._phaserevs
231 for phase in trackedphases:
231 for phase in trackedphases:
232 roots = map(cl.rev, self.phaseroots[phase])
232 roots = map(cl.rev, self.phaseroots[phase])
233 for root in roots:
233 for root in roots:
234 phaserevs[root] = phase
234 phaserevs[root] = phase
235
235
236 def phase(self, repo, rev):
236 def phase(self, repo, rev):
237 # We need a repo argument here to be able to build _phaserevs
237 # We need a repo argument here to be able to build _phaserevs
238 # if necessary. The repository instance is not stored in
238 # if necessary. The repository instance is not stored in
239 # phasecache to avoid reference cycles. The changelog instance
239 # phasecache to avoid reference cycles. The changelog instance
240 # is not stored because it is a filecache() property and can
240 # is not stored because it is a filecache() property and can
241 # be replaced without us being notified.
241 # be replaced without us being notified.
242 if rev == nullrev:
242 if rev == nullrev:
243 return public
243 return public
244 if rev < nullrev:
244 if rev < nullrev:
245 raise ValueError(_('cannot lookup negative revision'))
245 raise ValueError(_('cannot lookup negative revision'))
246 if self._phaserevs is None or rev >= len(self._phaserevs):
246 if self._phaserevs is None or rev >= len(self._phaserevs):
247 self.invalidate()
247 self.invalidate()
248 self.loadphaserevs(repo)
248 self.loadphaserevs(repo)
249 return self._phaserevs[rev]
249 return self._phaserevs[rev]
250
250
251 def write(self):
251 def write(self):
252 if not self.dirty:
252 if not self.dirty:
253 return
253 return
254 f = self.opener('phaseroots', 'w', atomictemp=True)
254 f = self.opener('phaseroots', 'w', atomictemp=True)
255 try:
255 try:
256 self._write(f)
256 self._write(f)
257 finally:
257 finally:
258 f.close()
258 f.close()
259
259
260 def _write(self, fp):
260 def _write(self, fp):
261 for phase, roots in enumerate(self.phaseroots):
261 for phase, roots in enumerate(self.phaseroots):
262 for h in roots:
262 for h in roots:
263 fp.write('%i %s\n' % (phase, hex(h)))
263 fp.write('%i %s\n' % (phase, hex(h)))
264 self.dirty = False
264 self.dirty = False
265
265
266 def _updateroots(self, phase, newroots, tr):
266 def _updateroots(self, phase, newroots, tr):
267 self.phaseroots[phase] = newroots
267 self.phaseroots[phase] = newroots
268 self.invalidate()
268 self.invalidate()
269 self.dirty = True
269 self.dirty = True
270
270
271 tr.addfilegenerator('phase', ('phaseroots',), self._write)
271 tr.addfilegenerator('phase', ('phaseroots',), self._write)
272 tr.hookargs['phases_moved'] = '1'
272 tr.hookargs['phases_moved'] = '1'
273
273
274 def advanceboundary(self, repo, tr, targetphase, nodes):
274 def advanceboundary(self, repo, tr, targetphase, nodes):
275 # Be careful to preserve shallow-copied values: do not update
275 # Be careful to preserve shallow-copied values: do not update
276 # phaseroots values, replace them.
276 # phaseroots values, replace them.
277
277
278 repo = repo.unfiltered()
278 repo = repo.unfiltered()
279 delroots = [] # set of root deleted by this path
279 delroots = [] # set of root deleted by this path
280 for phase in xrange(targetphase + 1, len(allphases)):
280 for phase in xrange(targetphase + 1, len(allphases)):
281 # filter nodes that are not in a compatible phase already
281 # filter nodes that are not in a compatible phase already
282 nodes = [n for n in nodes
282 nodes = [n for n in nodes
283 if self.phase(repo, repo[n].rev()) >= phase]
283 if self.phase(repo, repo[n].rev()) >= phase]
284 if not nodes:
284 if not nodes:
285 break # no roots to move anymore
285 break # no roots to move anymore
286 olds = self.phaseroots[phase]
286 olds = self.phaseroots[phase]
287 roots = set(ctx.node() for ctx in repo.set(
287 roots = set(ctx.node() for ctx in repo.set(
288 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
288 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
289 if olds != roots:
289 if olds != roots:
290 self._updateroots(phase, roots, tr)
290 self._updateroots(phase, roots, tr)
291 # some roots may need to be declared for lower phases
291 # some roots may need to be declared for lower phases
292 delroots.extend(olds - roots)
292 delroots.extend(olds - roots)
293 # declare deleted root in the target phase
293 # declare deleted root in the target phase
294 if targetphase != 0:
294 if targetphase != 0:
295 self.retractboundary(repo, tr, targetphase, delroots)
295 self.retractboundary(repo, tr, targetphase, delroots)
296 repo.invalidatevolatilesets()
296 repo.invalidatevolatilesets()
297
297
298 def retractboundary(self, repo, tr, targetphase, nodes):
298 def retractboundary(self, repo, tr, targetphase, nodes):
299 # Be careful to preserve shallow-copied values: do not update
299 # Be careful to preserve shallow-copied values: do not update
300 # phaseroots values, replace them.
300 # phaseroots values, replace them.
301
301
302 repo = repo.unfiltered()
302 repo = repo.unfiltered()
303 currentroots = self.phaseroots[targetphase]
303 currentroots = self.phaseroots[targetphase]
304 newroots = [n for n in nodes
304 newroots = [n for n in nodes
305 if self.phase(repo, repo[n].rev()) < targetphase]
305 if self.phase(repo, repo[n].rev()) < targetphase]
306 if newroots:
306 if newroots:
307 if nullid in newroots:
307 if nullid in newroots:
308 raise error.Abort(_('cannot change null revision phase'))
308 raise error.Abort(_('cannot change null revision phase'))
309 currentroots = currentroots.copy()
309 currentroots = currentroots.copy()
310 currentroots.update(newroots)
310 currentroots.update(newroots)
311 ctxs = repo.set('roots(%ln::)', currentroots)
311
312 currentroots.intersection_update(ctx.node() for ctx in ctxs)
312 # Only compute new roots for revs above the roots that are being
313 self._updateroots(targetphase, currentroots, tr)
313 # retracted.
314 minnewroot = min(repo[n].rev() for n in newroots)
315 aboveroots = [n for n in currentroots
316 if repo[n].rev() >= minnewroot]
317 updatedroots = repo.set('roots(%ln::)', aboveroots)
318
319 finalroots = set(n for n in currentroots if repo[n].rev() <
320 minnewroot)
321 finalroots.update(ctx.node() for ctx in updatedroots)
322
323 self._updateroots(targetphase, finalroots, tr)
314 repo.invalidatevolatilesets()
324 repo.invalidatevolatilesets()
315
325
316 def filterunknown(self, repo):
326 def filterunknown(self, repo):
317 """remove unknown nodes from the phase boundary
327 """remove unknown nodes from the phase boundary
318
328
319 Nothing is lost as unknown nodes only hold data for their descendants.
329 Nothing is lost as unknown nodes only hold data for their descendants.
320 """
330 """
321 filtered = False
331 filtered = False
322 nodemap = repo.changelog.nodemap # to filter unknown nodes
332 nodemap = repo.changelog.nodemap # to filter unknown nodes
323 for phase, nodes in enumerate(self.phaseroots):
333 for phase, nodes in enumerate(self.phaseroots):
324 missing = sorted(node for node in nodes if node not in nodemap)
334 missing = sorted(node for node in nodes if node not in nodemap)
325 if missing:
335 if missing:
326 for mnode in missing:
336 for mnode in missing:
327 repo.ui.debug(
337 repo.ui.debug(
328 'removing unknown node %s from %i-phase boundary\n'
338 'removing unknown node %s from %i-phase boundary\n'
329 % (short(mnode), phase))
339 % (short(mnode), phase))
330 nodes.symmetric_difference_update(missing)
340 nodes.symmetric_difference_update(missing)
331 filtered = True
341 filtered = True
332 if filtered:
342 if filtered:
333 self.dirty = True
343 self.dirty = True
334 # filterunknown is called by repo.destroyed, we may have no changes in
344 # filterunknown is called by repo.destroyed, we may have no changes in
335 # root but phaserevs contents is certainly invalid (or at least we
345 # root but phaserevs contents is certainly invalid (or at least we
336 # have not proper way to check that). related to issue 3858.
346 # have not proper way to check that). related to issue 3858.
337 #
347 #
338 # The other caller is __init__ that have no _phaserevs initialized
348 # The other caller is __init__ that have no _phaserevs initialized
339 # anyway. If this change we should consider adding a dedicated
349 # anyway. If this change we should consider adding a dedicated
340 # "destroyed" function to phasecache or a proper cache key mechanism
350 # "destroyed" function to phasecache or a proper cache key mechanism
341 # (see branchmap one)
351 # (see branchmap one)
342 self.invalidate()
352 self.invalidate()
343
353
344 def advanceboundary(repo, tr, targetphase, nodes):
354 def advanceboundary(repo, tr, targetphase, nodes):
345 """Add nodes to a phase changing other nodes phases if necessary.
355 """Add nodes to a phase changing other nodes phases if necessary.
346
356
347 This function move boundary *forward* this means that all nodes
357 This function move boundary *forward* this means that all nodes
348 are set in the target phase or kept in a *lower* phase.
358 are set in the target phase or kept in a *lower* phase.
349
359
350 Simplify boundary to contains phase roots only."""
360 Simplify boundary to contains phase roots only."""
351 phcache = repo._phasecache.copy()
361 phcache = repo._phasecache.copy()
352 phcache.advanceboundary(repo, tr, targetphase, nodes)
362 phcache.advanceboundary(repo, tr, targetphase, nodes)
353 repo._phasecache.replace(phcache)
363 repo._phasecache.replace(phcache)
354
364
355 def retractboundary(repo, tr, targetphase, nodes):
365 def retractboundary(repo, tr, targetphase, nodes):
356 """Set nodes back to a phase changing other nodes phases if
366 """Set nodes back to a phase changing other nodes phases if
357 necessary.
367 necessary.
358
368
359 This function move boundary *backward* this means that all nodes
369 This function move boundary *backward* this means that all nodes
360 are set in the target phase or kept in a *higher* phase.
370 are set in the target phase or kept in a *higher* phase.
361
371
362 Simplify boundary to contains phase roots only."""
372 Simplify boundary to contains phase roots only."""
363 phcache = repo._phasecache.copy()
373 phcache = repo._phasecache.copy()
364 phcache.retractboundary(repo, tr, targetphase, nodes)
374 phcache.retractboundary(repo, tr, targetphase, nodes)
365 repo._phasecache.replace(phcache)
375 repo._phasecache.replace(phcache)
366
376
367 def listphases(repo):
377 def listphases(repo):
368 """List phases root for serialization over pushkey"""
378 """List phases root for serialization over pushkey"""
369 keys = {}
379 keys = {}
370 value = '%i' % draft
380 value = '%i' % draft
371 for root in repo._phasecache.phaseroots[draft]:
381 for root in repo._phasecache.phaseroots[draft]:
372 keys[hex(root)] = value
382 keys[hex(root)] = value
373
383
374 if repo.publishing():
384 if repo.publishing():
375 # Add an extra data to let remote know we are a publishing
385 # Add an extra data to let remote know we are a publishing
376 # repo. Publishing repo can't just pretend they are old repo.
386 # repo. Publishing repo can't just pretend they are old repo.
377 # When pushing to a publishing repo, the client still need to
387 # When pushing to a publishing repo, the client still need to
378 # push phase boundary
388 # push phase boundary
379 #
389 #
380 # Push do not only push changeset. It also push phase data.
390 # Push do not only push changeset. It also push phase data.
381 # New phase data may apply to common changeset which won't be
391 # New phase data may apply to common changeset which won't be
382 # push (as they are common). Here is a very simple example:
392 # push (as they are common). Here is a very simple example:
383 #
393 #
384 # 1) repo A push changeset X as draft to repo B
394 # 1) repo A push changeset X as draft to repo B
385 # 2) repo B make changeset X public
395 # 2) repo B make changeset X public
386 # 3) repo B push to repo A. X is not pushed but the data that
396 # 3) repo B push to repo A. X is not pushed but the data that
387 # X as now public should
397 # X as now public should
388 #
398 #
389 # The server can't handle it on it's own as it has no idea of
399 # The server can't handle it on it's own as it has no idea of
390 # client phase data.
400 # client phase data.
391 keys['publishing'] = 'True'
401 keys['publishing'] = 'True'
392 return keys
402 return keys
393
403
394 def pushphase(repo, nhex, oldphasestr, newphasestr):
404 def pushphase(repo, nhex, oldphasestr, newphasestr):
395 """List phases root for serialization over pushkey"""
405 """List phases root for serialization over pushkey"""
396 repo = repo.unfiltered()
406 repo = repo.unfiltered()
397 tr = None
407 tr = None
398 lock = repo.lock()
408 lock = repo.lock()
399 try:
409 try:
400 currentphase = repo[nhex].phase()
410 currentphase = repo[nhex].phase()
401 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
411 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
402 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
412 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
403 if currentphase == oldphase and newphase < oldphase:
413 if currentphase == oldphase and newphase < oldphase:
404 tr = repo.transaction('pushkey-phase')
414 tr = repo.transaction('pushkey-phase')
405 advanceboundary(repo, tr, newphase, [bin(nhex)])
415 advanceboundary(repo, tr, newphase, [bin(nhex)])
406 tr.close()
416 tr.close()
407 return 1
417 return 1
408 elif currentphase == newphase:
418 elif currentphase == newphase:
409 # raced, but got correct result
419 # raced, but got correct result
410 return 1
420 return 1
411 else:
421 else:
412 return 0
422 return 0
413 finally:
423 finally:
414 if tr:
424 if tr:
415 tr.release()
425 tr.release()
416 lock.release()
426 lock.release()
417
427
418 def analyzeremotephases(repo, subset, roots):
428 def analyzeremotephases(repo, subset, roots):
419 """Compute phases heads and root in a subset of node from root dict
429 """Compute phases heads and root in a subset of node from root dict
420
430
421 * subset is heads of the subset
431 * subset is heads of the subset
422 * roots is {<nodeid> => phase} mapping. key and value are string.
432 * roots is {<nodeid> => phase} mapping. key and value are string.
423
433
424 Accept unknown element input
434 Accept unknown element input
425 """
435 """
426 repo = repo.unfiltered()
436 repo = repo.unfiltered()
427 # build list from dictionary
437 # build list from dictionary
428 draftroots = []
438 draftroots = []
429 nodemap = repo.changelog.nodemap # to filter unknown nodes
439 nodemap = repo.changelog.nodemap # to filter unknown nodes
430 for nhex, phase in roots.iteritems():
440 for nhex, phase in roots.iteritems():
431 if nhex == 'publishing': # ignore data related to publish option
441 if nhex == 'publishing': # ignore data related to publish option
432 continue
442 continue
433 node = bin(nhex)
443 node = bin(nhex)
434 phase = int(phase)
444 phase = int(phase)
435 if phase == 0:
445 if phase == 0:
436 if node != nullid:
446 if node != nullid:
437 repo.ui.warn(_('ignoring inconsistent public root'
447 repo.ui.warn(_('ignoring inconsistent public root'
438 ' from remote: %s\n') % nhex)
448 ' from remote: %s\n') % nhex)
439 elif phase == 1:
449 elif phase == 1:
440 if node in nodemap:
450 if node in nodemap:
441 draftroots.append(node)
451 draftroots.append(node)
442 else:
452 else:
443 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
453 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
444 % (phase, nhex))
454 % (phase, nhex))
445 # compute heads
455 # compute heads
446 publicheads = newheads(repo, subset, draftroots)
456 publicheads = newheads(repo, subset, draftroots)
447 return publicheads, draftroots
457 return publicheads, draftroots
448
458
449 def newheads(repo, heads, roots):
459 def newheads(repo, heads, roots):
450 """compute new head of a subset minus another
460 """compute new head of a subset minus another
451
461
452 * `heads`: define the first subset
462 * `heads`: define the first subset
453 * `roots`: define the second we subtract from the first"""
463 * `roots`: define the second we subtract from the first"""
454 repo = repo.unfiltered()
464 repo = repo.unfiltered()
455 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
465 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
456 heads, roots, roots, heads)
466 heads, roots, roots, heads)
457 return [c.node() for c in revset]
467 return [c.node() for c in revset]
458
468
459
469
460 def newcommitphase(ui):
470 def newcommitphase(ui):
461 """helper to get the target phase of new commit
471 """helper to get the target phase of new commit
462
472
463 Handle all possible values for the phases.new-commit options.
473 Handle all possible values for the phases.new-commit options.
464
474
465 """
475 """
466 v = ui.config('phases', 'new-commit', draft)
476 v = ui.config('phases', 'new-commit', draft)
467 try:
477 try:
468 return phasenames.index(v)
478 return phasenames.index(v)
469 except ValueError:
479 except ValueError:
470 try:
480 try:
471 return int(v)
481 return int(v)
472 except ValueError:
482 except ValueError:
473 msg = _("phases.new-commit: not a valid phase name ('%s')")
483 msg = _("phases.new-commit: not a valid phase name ('%s')")
474 raise error.ConfigError(msg % v)
484 raise error.ConfigError(msg % v)
475
485
476 def hassecret(repo):
486 def hassecret(repo):
477 """utility function that check if a repo have any secret changeset."""
487 """utility function that check if a repo have any secret changeset."""
478 return bool(repo._phasecache.phaseroots[2])
488 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now