##// END OF EJS Templates
phases: really fix native phase computation...
Pierre-Yves David -
r25527:262e6ad9 default
parent child Browse files
Show More
@@ -1,464 +1,464 b''
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 import os
103 import os
104 import errno
104 import errno
105 from node import nullid, nullrev, bin, hex, short
105 from node import nullid, nullrev, bin, hex, short
106 from i18n import _
106 from i18n import _
107 import util, error
107 import util, error
108
108
109 allphases = public, draft, secret = range(3)
109 allphases = public, draft, secret = range(3)
110 trackedphases = allphases[1:]
110 trackedphases = allphases[1:]
111 phasenames = ['public', 'draft', 'secret']
111 phasenames = ['public', 'draft', 'secret']
112
112
113 def _readroots(repo, phasedefaults=None):
113 def _readroots(repo, phasedefaults=None):
114 """Read phase roots from disk
114 """Read phase roots from disk
115
115
116 phasedefaults is a list of fn(repo, roots) callable, which are
116 phasedefaults is a list of fn(repo, roots) callable, which are
117 executed if the phase roots file does not exist. When phases are
117 executed if the phase roots file does not exist. When phases are
118 being initialized on an existing repository, this could be used to
118 being initialized on an existing repository, this could be used to
119 set selected changesets phase to something else than public.
119 set selected changesets phase to something else than public.
120
120
121 Return (roots, dirty) where dirty is true if roots differ from
121 Return (roots, dirty) where dirty is true if roots differ from
122 what is being stored.
122 what is being stored.
123 """
123 """
124 repo = repo.unfiltered()
124 repo = repo.unfiltered()
125 dirty = False
125 dirty = False
126 roots = [set() for i in allphases]
126 roots = [set() for i in allphases]
127 try:
127 try:
128 f = None
128 f = None
129 if 'HG_PENDING' in os.environ:
129 if 'HG_PENDING' in os.environ:
130 try:
130 try:
131 f = repo.svfs('phaseroots.pending')
131 f = repo.svfs('phaseroots.pending')
132 except IOError, inst:
132 except IOError, inst:
133 if inst.errno != errno.ENOENT:
133 if inst.errno != errno.ENOENT:
134 raise
134 raise
135 if f is None:
135 if f is None:
136 f = repo.svfs('phaseroots')
136 f = repo.svfs('phaseroots')
137 try:
137 try:
138 for line in f:
138 for line in f:
139 phase, nh = line.split()
139 phase, nh = line.split()
140 roots[int(phase)].add(bin(nh))
140 roots[int(phase)].add(bin(nh))
141 finally:
141 finally:
142 f.close()
142 f.close()
143 except IOError, inst:
143 except IOError, inst:
144 if inst.errno != errno.ENOENT:
144 if inst.errno != errno.ENOENT:
145 raise
145 raise
146 if phasedefaults:
146 if phasedefaults:
147 for f in phasedefaults:
147 for f in phasedefaults:
148 roots = f(repo, roots)
148 roots = f(repo, roots)
149 dirty = True
149 dirty = True
150 return roots, dirty
150 return roots, dirty
151
151
152 class phasecache(object):
152 class phasecache(object):
153 def __init__(self, repo, phasedefaults, _load=True):
153 def __init__(self, repo, phasedefaults, _load=True):
154 if _load:
154 if _load:
155 # Cheap trick to allow shallow-copy without copy module
155 # Cheap trick to allow shallow-copy without copy module
156 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
156 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
157 self._phaserevs = None
157 self._phaserevs = None
158 self._phasesets = None
158 self._phasesets = None
159 self.filterunknown(repo)
159 self.filterunknown(repo)
160 self.opener = repo.svfs
160 self.opener = repo.svfs
161
161
162 def copy(self):
162 def copy(self):
163 # Shallow copy meant to ensure isolation in
163 # Shallow copy meant to ensure isolation in
164 # advance/retractboundary(), nothing more.
164 # advance/retractboundary(), nothing more.
165 ph = self.__class__(None, None, _load=False)
165 ph = self.__class__(None, None, _load=False)
166 ph.phaseroots = self.phaseroots[:]
166 ph.phaseroots = self.phaseroots[:]
167 ph.dirty = self.dirty
167 ph.dirty = self.dirty
168 ph.opener = self.opener
168 ph.opener = self.opener
169 ph._phaserevs = self._phaserevs
169 ph._phaserevs = self._phaserevs
170 return ph
170 return ph
171
171
172 def replace(self, phcache):
172 def replace(self, phcache):
173 for a in 'phaseroots dirty opener _phaserevs'.split():
173 for a in 'phaseroots dirty opener _phaserevs'.split():
174 setattr(self, a, getattr(phcache, a))
174 setattr(self, a, getattr(phcache, a))
175
175
176 def _getphaserevsnative(self, repo):
176 def _getphaserevsnative(self, repo):
177 repo = repo.unfiltered()
177 repo = repo.unfiltered()
178 nativeroots = []
178 nativeroots = []
179 for phase in trackedphases:
179 for phase in trackedphases:
180 nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase]))
180 nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase]))
181 return repo.changelog.computephasesmapsets(nativeroots)
181 return repo.changelog.computephases(nativeroots)
182
182
183 def _computephaserevspure(self, repo):
183 def _computephaserevspure(self, repo):
184 repo = repo.unfiltered()
184 repo = repo.unfiltered()
185 revs = [public] * len(repo.changelog)
185 revs = [public] * len(repo.changelog)
186 self._phaserevs = revs
186 self._phaserevs = revs
187 self._populatephaseroots(repo)
187 self._populatephaseroots(repo)
188 for phase in trackedphases:
188 for phase in trackedphases:
189 roots = map(repo.changelog.rev, self.phaseroots[phase])
189 roots = map(repo.changelog.rev, self.phaseroots[phase])
190 if roots:
190 if roots:
191 for rev in roots:
191 for rev in roots:
192 revs[rev] = phase
192 revs[rev] = phase
193 for rev in repo.changelog.descendants(roots):
193 for rev in repo.changelog.descendants(roots):
194 revs[rev] = phase
194 revs[rev] = phase
195
195
196 def getphaserevs(self, repo):
196 def getphaserevs(self, repo):
197 if self._phaserevs is None:
197 if self._phaserevs is None:
198 try:
198 try:
199 if repo.ui.configbool('experimental',
199 if repo.ui.configbool('experimental',
200 'nativephaseskillswitch'):
200 'nativephaseskillswitch'):
201 self._computephaserevspure(repo)
201 self._computephaserevspure(repo)
202 else:
202 else:
203 res = self._getphaserevsnative(repo)
203 res = self._getphaserevsnative(repo)
204 self._phaserevs, self._phasesets = res
204 self._phaserevs, self._phasesets = res
205 except AttributeError:
205 except AttributeError:
206 self._computephaserevspure(repo)
206 self._computephaserevspure(repo)
207 return self._phaserevs
207 return self._phaserevs
208
208
209 def invalidate(self):
209 def invalidate(self):
210 self._phaserevs = None
210 self._phaserevs = None
211
211
212 def _populatephaseroots(self, repo):
212 def _populatephaseroots(self, repo):
213 """Fills the _phaserevs cache with phases for the roots.
213 """Fills the _phaserevs cache with phases for the roots.
214 """
214 """
215 cl = repo.changelog
215 cl = repo.changelog
216 phaserevs = self._phaserevs
216 phaserevs = self._phaserevs
217 for phase in trackedphases:
217 for phase in trackedphases:
218 roots = map(cl.rev, self.phaseroots[phase])
218 roots = map(cl.rev, self.phaseroots[phase])
219 for root in roots:
219 for root in roots:
220 phaserevs[root] = phase
220 phaserevs[root] = phase
221
221
222 def phase(self, repo, rev):
222 def phase(self, repo, rev):
223 # We need a repo argument here to be able to build _phaserevs
223 # We need a repo argument here to be able to build _phaserevs
224 # if necessary. The repository instance is not stored in
224 # if necessary. The repository instance is not stored in
225 # phasecache to avoid reference cycles. The changelog instance
225 # phasecache to avoid reference cycles. The changelog instance
226 # is not stored because it is a filecache() property and can
226 # is not stored because it is a filecache() property and can
227 # be replaced without us being notified.
227 # be replaced without us being notified.
228 if rev == nullrev:
228 if rev == nullrev:
229 return public
229 return public
230 if rev < nullrev:
230 if rev < nullrev:
231 raise ValueError(_('cannot lookup negative revision'))
231 raise ValueError(_('cannot lookup negative revision'))
232 if self._phaserevs is None or rev >= len(self._phaserevs):
232 if self._phaserevs is None or rev >= len(self._phaserevs):
233 self.invalidate()
233 self.invalidate()
234 self._phaserevs = self.getphaserevs(repo)
234 self._phaserevs = self.getphaserevs(repo)
235 return self._phaserevs[rev]
235 return self._phaserevs[rev]
236
236
237 def write(self):
237 def write(self):
238 if not self.dirty:
238 if not self.dirty:
239 return
239 return
240 f = self.opener('phaseroots', 'w', atomictemp=True)
240 f = self.opener('phaseroots', 'w', atomictemp=True)
241 try:
241 try:
242 self._write(f)
242 self._write(f)
243 finally:
243 finally:
244 f.close()
244 f.close()
245
245
246 def _write(self, fp):
246 def _write(self, fp):
247 for phase, roots in enumerate(self.phaseroots):
247 for phase, roots in enumerate(self.phaseroots):
248 for h in roots:
248 for h in roots:
249 fp.write('%i %s\n' % (phase, hex(h)))
249 fp.write('%i %s\n' % (phase, hex(h)))
250 self.dirty = False
250 self.dirty = False
251
251
252 def _updateroots(self, phase, newroots, tr):
252 def _updateroots(self, phase, newroots, tr):
253 self.phaseroots[phase] = newroots
253 self.phaseroots[phase] = newroots
254 self.invalidate()
254 self.invalidate()
255 self.dirty = True
255 self.dirty = True
256
256
257 tr.addfilegenerator('phase', ('phaseroots',), self._write)
257 tr.addfilegenerator('phase', ('phaseroots',), self._write)
258 tr.hookargs['phases_moved'] = '1'
258 tr.hookargs['phases_moved'] = '1'
259
259
260 def advanceboundary(self, repo, tr, targetphase, nodes):
260 def advanceboundary(self, repo, tr, targetphase, nodes):
261 # Be careful to preserve shallow-copied values: do not update
261 # Be careful to preserve shallow-copied values: do not update
262 # phaseroots values, replace them.
262 # phaseroots values, replace them.
263
263
264 repo = repo.unfiltered()
264 repo = repo.unfiltered()
265 delroots = [] # set of root deleted by this path
265 delroots = [] # set of root deleted by this path
266 for phase in xrange(targetphase + 1, len(allphases)):
266 for phase in xrange(targetphase + 1, len(allphases)):
267 # filter nodes that are not in a compatible phase already
267 # filter nodes that are not in a compatible phase already
268 nodes = [n for n in nodes
268 nodes = [n for n in nodes
269 if self.phase(repo, repo[n].rev()) >= phase]
269 if self.phase(repo, repo[n].rev()) >= phase]
270 if not nodes:
270 if not nodes:
271 break # no roots to move anymore
271 break # no roots to move anymore
272 olds = self.phaseroots[phase]
272 olds = self.phaseroots[phase]
273 roots = set(ctx.node() for ctx in repo.set(
273 roots = set(ctx.node() for ctx in repo.set(
274 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
274 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
275 if olds != roots:
275 if olds != roots:
276 self._updateroots(phase, roots, tr)
276 self._updateroots(phase, roots, tr)
277 # some roots may need to be declared for lower phases
277 # some roots may need to be declared for lower phases
278 delroots.extend(olds - roots)
278 delroots.extend(olds - roots)
279 # declare deleted root in the target phase
279 # declare deleted root in the target phase
280 if targetphase != 0:
280 if targetphase != 0:
281 self.retractboundary(repo, tr, targetphase, delroots)
281 self.retractboundary(repo, tr, targetphase, delroots)
282 repo.invalidatevolatilesets()
282 repo.invalidatevolatilesets()
283
283
284 def retractboundary(self, repo, tr, targetphase, nodes):
284 def retractboundary(self, repo, tr, targetphase, nodes):
285 # Be careful to preserve shallow-copied values: do not update
285 # Be careful to preserve shallow-copied values: do not update
286 # phaseroots values, replace them.
286 # phaseroots values, replace them.
287
287
288 repo = repo.unfiltered()
288 repo = repo.unfiltered()
289 currentroots = self.phaseroots[targetphase]
289 currentroots = self.phaseroots[targetphase]
290 newroots = [n for n in nodes
290 newroots = [n for n in nodes
291 if self.phase(repo, repo[n].rev()) < targetphase]
291 if self.phase(repo, repo[n].rev()) < targetphase]
292 if newroots:
292 if newroots:
293 if nullid in newroots:
293 if nullid in newroots:
294 raise util.Abort(_('cannot change null revision phase'))
294 raise util.Abort(_('cannot change null revision phase'))
295 currentroots = currentroots.copy()
295 currentroots = currentroots.copy()
296 currentroots.update(newroots)
296 currentroots.update(newroots)
297 ctxs = repo.set('roots(%ln::)', currentroots)
297 ctxs = repo.set('roots(%ln::)', currentroots)
298 currentroots.intersection_update(ctx.node() for ctx in ctxs)
298 currentroots.intersection_update(ctx.node() for ctx in ctxs)
299 self._updateroots(targetphase, currentroots, tr)
299 self._updateroots(targetphase, currentroots, tr)
300 repo.invalidatevolatilesets()
300 repo.invalidatevolatilesets()
301
301
302 def filterunknown(self, repo):
302 def filterunknown(self, repo):
303 """remove unknown nodes from the phase boundary
303 """remove unknown nodes from the phase boundary
304
304
305 Nothing is lost as unknown nodes only hold data for their descendants.
305 Nothing is lost as unknown nodes only hold data for their descendants.
306 """
306 """
307 filtered = False
307 filtered = False
308 nodemap = repo.changelog.nodemap # to filter unknown nodes
308 nodemap = repo.changelog.nodemap # to filter unknown nodes
309 for phase, nodes in enumerate(self.phaseroots):
309 for phase, nodes in enumerate(self.phaseroots):
310 missing = sorted(node for node in nodes if node not in nodemap)
310 missing = sorted(node for node in nodes if node not in nodemap)
311 if missing:
311 if missing:
312 for mnode in missing:
312 for mnode in missing:
313 repo.ui.debug(
313 repo.ui.debug(
314 'removing unknown node %s from %i-phase boundary\n'
314 'removing unknown node %s from %i-phase boundary\n'
315 % (short(mnode), phase))
315 % (short(mnode), phase))
316 nodes.symmetric_difference_update(missing)
316 nodes.symmetric_difference_update(missing)
317 filtered = True
317 filtered = True
318 if filtered:
318 if filtered:
319 self.dirty = True
319 self.dirty = True
320 # filterunknown is called by repo.destroyed, we may have no changes in
320 # filterunknown is called by repo.destroyed, we may have no changes in
321 # root but phaserevs contents is certainly invalid (or at least we
321 # root but phaserevs contents is certainly invalid (or at least we
322 # have not proper way to check that). related to issue 3858.
322 # have not proper way to check that). related to issue 3858.
323 #
323 #
324 # The other caller is __init__ that have no _phaserevs initialized
324 # The other caller is __init__ that have no _phaserevs initialized
325 # anyway. If this change we should consider adding a dedicated
325 # anyway. If this change we should consider adding a dedicated
326 # "destroyed" function to phasecache or a proper cache key mechanism
326 # "destroyed" function to phasecache or a proper cache key mechanism
327 # (see branchmap one)
327 # (see branchmap one)
328 self.invalidate()
328 self.invalidate()
329
329
330 def advanceboundary(repo, tr, targetphase, nodes):
330 def advanceboundary(repo, tr, targetphase, nodes):
331 """Add nodes to a phase changing other nodes phases if necessary.
331 """Add nodes to a phase changing other nodes phases if necessary.
332
332
333 This function move boundary *forward* this means that all nodes
333 This function move boundary *forward* this means that all nodes
334 are set in the target phase or kept in a *lower* phase.
334 are set in the target phase or kept in a *lower* phase.
335
335
336 Simplify boundary to contains phase roots only."""
336 Simplify boundary to contains phase roots only."""
337 phcache = repo._phasecache.copy()
337 phcache = repo._phasecache.copy()
338 phcache.advanceboundary(repo, tr, targetphase, nodes)
338 phcache.advanceboundary(repo, tr, targetphase, nodes)
339 repo._phasecache.replace(phcache)
339 repo._phasecache.replace(phcache)
340
340
341 def retractboundary(repo, tr, targetphase, nodes):
341 def retractboundary(repo, tr, targetphase, nodes):
342 """Set nodes back to a phase changing other nodes phases if
342 """Set nodes back to a phase changing other nodes phases if
343 necessary.
343 necessary.
344
344
345 This function move boundary *backward* this means that all nodes
345 This function move boundary *backward* this means that all nodes
346 are set in the target phase or kept in a *higher* phase.
346 are set in the target phase or kept in a *higher* phase.
347
347
348 Simplify boundary to contains phase roots only."""
348 Simplify boundary to contains phase roots only."""
349 phcache = repo._phasecache.copy()
349 phcache = repo._phasecache.copy()
350 phcache.retractboundary(repo, tr, targetphase, nodes)
350 phcache.retractboundary(repo, tr, targetphase, nodes)
351 repo._phasecache.replace(phcache)
351 repo._phasecache.replace(phcache)
352
352
353 def listphases(repo):
353 def listphases(repo):
354 """List phases root for serialization over pushkey"""
354 """List phases root for serialization over pushkey"""
355 keys = {}
355 keys = {}
356 value = '%i' % draft
356 value = '%i' % draft
357 for root in repo._phasecache.phaseroots[draft]:
357 for root in repo._phasecache.phaseroots[draft]:
358 keys[hex(root)] = value
358 keys[hex(root)] = value
359
359
360 if repo.ui.configbool('phases', 'publish', True):
360 if repo.ui.configbool('phases', 'publish', True):
361 # Add an extra data to let remote know we are a publishing
361 # Add an extra data to let remote know we are a publishing
362 # repo. Publishing repo can't just pretend they are old repo.
362 # repo. Publishing repo can't just pretend they are old repo.
363 # When pushing to a publishing repo, the client still need to
363 # When pushing to a publishing repo, the client still need to
364 # push phase boundary
364 # push phase boundary
365 #
365 #
366 # Push do not only push changeset. It also push phase data.
366 # Push do not only push changeset. It also push phase data.
367 # New phase data may apply to common changeset which won't be
367 # New phase data may apply to common changeset which won't be
368 # push (as they are common). Here is a very simple example:
368 # push (as they are common). Here is a very simple example:
369 #
369 #
370 # 1) repo A push changeset X as draft to repo B
370 # 1) repo A push changeset X as draft to repo B
371 # 2) repo B make changeset X public
371 # 2) repo B make changeset X public
372 # 3) repo B push to repo A. X is not pushed but the data that
372 # 3) repo B push to repo A. X is not pushed but the data that
373 # X as now public should
373 # X as now public should
374 #
374 #
375 # The server can't handle it on it's own as it has no idea of
375 # The server can't handle it on it's own as it has no idea of
376 # client phase data.
376 # client phase data.
377 keys['publishing'] = 'True'
377 keys['publishing'] = 'True'
378 return keys
378 return keys
379
379
380 def pushphase(repo, nhex, oldphasestr, newphasestr):
380 def pushphase(repo, nhex, oldphasestr, newphasestr):
381 """List phases root for serialization over pushkey"""
381 """List phases root for serialization over pushkey"""
382 repo = repo.unfiltered()
382 repo = repo.unfiltered()
383 tr = None
383 tr = None
384 lock = repo.lock()
384 lock = repo.lock()
385 try:
385 try:
386 currentphase = repo[nhex].phase()
386 currentphase = repo[nhex].phase()
387 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
387 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
388 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
388 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
389 if currentphase == oldphase and newphase < oldphase:
389 if currentphase == oldphase and newphase < oldphase:
390 tr = repo.transaction('pushkey-phase')
390 tr = repo.transaction('pushkey-phase')
391 advanceboundary(repo, tr, newphase, [bin(nhex)])
391 advanceboundary(repo, tr, newphase, [bin(nhex)])
392 tr.close()
392 tr.close()
393 return 1
393 return 1
394 elif currentphase == newphase:
394 elif currentphase == newphase:
395 # raced, but got correct result
395 # raced, but got correct result
396 return 1
396 return 1
397 else:
397 else:
398 return 0
398 return 0
399 finally:
399 finally:
400 if tr:
400 if tr:
401 tr.release()
401 tr.release()
402 lock.release()
402 lock.release()
403
403
404 def analyzeremotephases(repo, subset, roots):
404 def analyzeremotephases(repo, subset, roots):
405 """Compute phases heads and root in a subset of node from root dict
405 """Compute phases heads and root in a subset of node from root dict
406
406
407 * subset is heads of the subset
407 * subset is heads of the subset
408 * roots is {<nodeid> => phase} mapping. key and value are string.
408 * roots is {<nodeid> => phase} mapping. key and value are string.
409
409
410 Accept unknown element input
410 Accept unknown element input
411 """
411 """
412 repo = repo.unfiltered()
412 repo = repo.unfiltered()
413 # build list from dictionary
413 # build list from dictionary
414 draftroots = []
414 draftroots = []
415 nodemap = repo.changelog.nodemap # to filter unknown nodes
415 nodemap = repo.changelog.nodemap # to filter unknown nodes
416 for nhex, phase in roots.iteritems():
416 for nhex, phase in roots.iteritems():
417 if nhex == 'publishing': # ignore data related to publish option
417 if nhex == 'publishing': # ignore data related to publish option
418 continue
418 continue
419 node = bin(nhex)
419 node = bin(nhex)
420 phase = int(phase)
420 phase = int(phase)
421 if phase == 0:
421 if phase == 0:
422 if node != nullid:
422 if node != nullid:
423 repo.ui.warn(_('ignoring inconsistent public root'
423 repo.ui.warn(_('ignoring inconsistent public root'
424 ' from remote: %s\n') % nhex)
424 ' from remote: %s\n') % nhex)
425 elif phase == 1:
425 elif phase == 1:
426 if node in nodemap:
426 if node in nodemap:
427 draftroots.append(node)
427 draftroots.append(node)
428 else:
428 else:
429 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
429 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
430 % (phase, nhex))
430 % (phase, nhex))
431 # compute heads
431 # compute heads
432 publicheads = newheads(repo, subset, draftroots)
432 publicheads = newheads(repo, subset, draftroots)
433 return publicheads, draftroots
433 return publicheads, draftroots
434
434
435 def newheads(repo, heads, roots):
435 def newheads(repo, heads, roots):
436 """compute new head of a subset minus another
436 """compute new head of a subset minus another
437
437
438 * `heads`: define the first subset
438 * `heads`: define the first subset
439 * `roots`: define the second we subtract from the first"""
439 * `roots`: define the second we subtract from the first"""
440 repo = repo.unfiltered()
440 repo = repo.unfiltered()
441 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
441 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
442 heads, roots, roots, heads)
442 heads, roots, roots, heads)
443 return [c.node() for c in revset]
443 return [c.node() for c in revset]
444
444
445
445
446 def newcommitphase(ui):
446 def newcommitphase(ui):
447 """helper to get the target phase of new commit
447 """helper to get the target phase of new commit
448
448
449 Handle all possible values for the phases.new-commit options.
449 Handle all possible values for the phases.new-commit options.
450
450
451 """
451 """
452 v = ui.config('phases', 'new-commit', draft)
452 v = ui.config('phases', 'new-commit', draft)
453 try:
453 try:
454 return phasenames.index(v)
454 return phasenames.index(v)
455 except ValueError:
455 except ValueError:
456 try:
456 try:
457 return int(v)
457 return int(v)
458 except ValueError:
458 except ValueError:
459 msg = _("phases.new-commit: not a valid phase name ('%s')")
459 msg = _("phases.new-commit: not a valid phase name ('%s')")
460 raise error.ConfigError(msg % v)
460 raise error.ConfigError(msg % v)
461
461
462 def hassecret(repo):
462 def hassecret(repo):
463 """utility function that check if a repo have any secret changeset."""
463 """utility function that check if a repo have any secret changeset."""
464 return bool(repo._phasecache.phaseroots[2])
464 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now