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