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