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