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