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