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