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