##// END OF EJS Templates
py3: do not pass a list of iterators to computephasesmapsets()
Yuya Nishihara -
r36642:31187662 default
parent child Browse files
Show More
@@ -1,679 +1,680 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(pycompat.maplist(repo.changelog.rev,
266 self.phaseroots[phase]))
266 return repo.changelog.computephases(nativeroots)
267 return repo.changelog.computephases(nativeroots)
267
268
268 def _computephaserevspure(self, repo):
269 def _computephaserevspure(self, repo):
269 repo = repo.unfiltered()
270 repo = repo.unfiltered()
270 cl = repo.changelog
271 cl = repo.changelog
271 self._phasesets = [set() for phase in allphases]
272 self._phasesets = [set() for phase in allphases]
272 roots = pycompat.maplist(cl.rev, self.phaseroots[secret])
273 roots = pycompat.maplist(cl.rev, self.phaseroots[secret])
273 if roots:
274 if roots:
274 ps = set(cl.descendants(roots))
275 ps = set(cl.descendants(roots))
275 for root in roots:
276 for root in roots:
276 ps.add(root)
277 ps.add(root)
277 self._phasesets[secret] = ps
278 self._phasesets[secret] = ps
278 roots = pycompat.maplist(cl.rev, self.phaseroots[draft])
279 roots = pycompat.maplist(cl.rev, self.phaseroots[draft])
279 if roots:
280 if roots:
280 ps = set(cl.descendants(roots))
281 ps = set(cl.descendants(roots))
281 for root in roots:
282 for root in roots:
282 ps.add(root)
283 ps.add(root)
283 ps.difference_update(self._phasesets[secret])
284 ps.difference_update(self._phasesets[secret])
284 self._phasesets[draft] = ps
285 self._phasesets[draft] = ps
285 self._loadedrevslen = len(cl)
286 self._loadedrevslen = len(cl)
286
287
287 def loadphaserevs(self, repo):
288 def loadphaserevs(self, repo):
288 """ensure phase information is loaded in the object"""
289 """ensure phase information is loaded in the object"""
289 if self._phasesets is None:
290 if self._phasesets is None:
290 try:
291 try:
291 res = self._getphaserevsnative(repo)
292 res = self._getphaserevsnative(repo)
292 self._loadedrevslen, self._phasesets = res
293 self._loadedrevslen, self._phasesets = res
293 except AttributeError:
294 except AttributeError:
294 self._computephaserevspure(repo)
295 self._computephaserevspure(repo)
295
296
296 def invalidate(self):
297 def invalidate(self):
297 self._loadedrevslen = 0
298 self._loadedrevslen = 0
298 self._phasesets = None
299 self._phasesets = None
299
300
300 def phase(self, repo, rev):
301 def phase(self, repo, rev):
301 # We need a repo argument here to be able to build _phasesets
302 # We need a repo argument here to be able to build _phasesets
302 # if necessary. The repository instance is not stored in
303 # if necessary. The repository instance is not stored in
303 # phasecache to avoid reference cycles. The changelog instance
304 # phasecache to avoid reference cycles. The changelog instance
304 # is not stored because it is a filecache() property and can
305 # is not stored because it is a filecache() property and can
305 # be replaced without us being notified.
306 # be replaced without us being notified.
306 if rev == nullrev:
307 if rev == nullrev:
307 return public
308 return public
308 if rev < nullrev:
309 if rev < nullrev:
309 raise ValueError(_('cannot lookup negative revision'))
310 raise ValueError(_('cannot lookup negative revision'))
310 if rev >= self._loadedrevslen:
311 if rev >= self._loadedrevslen:
311 self.invalidate()
312 self.invalidate()
312 self.loadphaserevs(repo)
313 self.loadphaserevs(repo)
313 for phase in trackedphases:
314 for phase in trackedphases:
314 if rev in self._phasesets[phase]:
315 if rev in self._phasesets[phase]:
315 return phase
316 return phase
316 return public
317 return public
317
318
318 def write(self):
319 def write(self):
319 if not self.dirty:
320 if not self.dirty:
320 return
321 return
321 f = self.opener('phaseroots', 'w', atomictemp=True, checkambig=True)
322 f = self.opener('phaseroots', 'w', atomictemp=True, checkambig=True)
322 try:
323 try:
323 self._write(f)
324 self._write(f)
324 finally:
325 finally:
325 f.close()
326 f.close()
326
327
327 def _write(self, fp):
328 def _write(self, fp):
328 for phase, roots in enumerate(self.phaseroots):
329 for phase, roots in enumerate(self.phaseroots):
329 for h in sorted(roots):
330 for h in sorted(roots):
330 fp.write('%i %s\n' % (phase, hex(h)))
331 fp.write('%i %s\n' % (phase, hex(h)))
331 self.dirty = False
332 self.dirty = False
332
333
333 def _updateroots(self, phase, newroots, tr):
334 def _updateroots(self, phase, newroots, tr):
334 self.phaseroots[phase] = newroots
335 self.phaseroots[phase] = newroots
335 self.invalidate()
336 self.invalidate()
336 self.dirty = True
337 self.dirty = True
337
338
338 tr.addfilegenerator('phase', ('phaseroots',), self._write)
339 tr.addfilegenerator('phase', ('phaseroots',), self._write)
339 tr.hookargs['phases_moved'] = '1'
340 tr.hookargs['phases_moved'] = '1'
340
341
341 def registernew(self, repo, tr, targetphase, nodes):
342 def registernew(self, repo, tr, targetphase, nodes):
342 repo = repo.unfiltered()
343 repo = repo.unfiltered()
343 self._retractboundary(repo, tr, targetphase, nodes)
344 self._retractboundary(repo, tr, targetphase, nodes)
344 if tr is not None and 'phases' in tr.changes:
345 if tr is not None and 'phases' in tr.changes:
345 phasetracking = tr.changes['phases']
346 phasetracking = tr.changes['phases']
346 torev = repo.changelog.rev
347 torev = repo.changelog.rev
347 phase = self.phase
348 phase = self.phase
348 for n in nodes:
349 for n in nodes:
349 rev = torev(n)
350 rev = torev(n)
350 revphase = phase(repo, rev)
351 revphase = phase(repo, rev)
351 _trackphasechange(phasetracking, rev, None, revphase)
352 _trackphasechange(phasetracking, rev, None, revphase)
352 repo.invalidatevolatilesets()
353 repo.invalidatevolatilesets()
353
354
354 def advanceboundary(self, repo, tr, targetphase, nodes):
355 def advanceboundary(self, repo, tr, targetphase, nodes):
355 """Set all 'nodes' to phase 'targetphase'
356 """Set all 'nodes' to phase 'targetphase'
356
357
357 Nodes with a phase lower than 'targetphase' are not affected.
358 Nodes with a phase lower than 'targetphase' are not affected.
358 """
359 """
359 # Be careful to preserve shallow-copied values: do not update
360 # Be careful to preserve shallow-copied values: do not update
360 # phaseroots values, replace them.
361 # phaseroots values, replace them.
361 if tr is None:
362 if tr is None:
362 phasetracking = None
363 phasetracking = None
363 else:
364 else:
364 phasetracking = tr.changes.get('phases')
365 phasetracking = tr.changes.get('phases')
365
366
366 repo = repo.unfiltered()
367 repo = repo.unfiltered()
367
368
368 delroots = [] # set of root deleted by this path
369 delroots = [] # set of root deleted by this path
369 for phase in xrange(targetphase + 1, len(allphases)):
370 for phase in xrange(targetphase + 1, len(allphases)):
370 # filter nodes that are not in a compatible phase already
371 # filter nodes that are not in a compatible phase already
371 nodes = [n for n in nodes
372 nodes = [n for n in nodes
372 if self.phase(repo, repo[n].rev()) >= phase]
373 if self.phase(repo, repo[n].rev()) >= phase]
373 if not nodes:
374 if not nodes:
374 break # no roots to move anymore
375 break # no roots to move anymore
375
376
376 olds = self.phaseroots[phase]
377 olds = self.phaseroots[phase]
377
378
378 affected = repo.revs('%ln::%ln', olds, nodes)
379 affected = repo.revs('%ln::%ln', olds, nodes)
379 for r in affected:
380 for r in affected:
380 _trackphasechange(phasetracking, r, self.phase(repo, r),
381 _trackphasechange(phasetracking, r, self.phase(repo, r),
381 targetphase)
382 targetphase)
382
383
383 roots = set(ctx.node() for ctx in repo.set(
384 roots = set(ctx.node() for ctx in repo.set(
384 'roots((%ln::) - %ld)', olds, affected))
385 'roots((%ln::) - %ld)', olds, affected))
385 if olds != roots:
386 if olds != roots:
386 self._updateroots(phase, roots, tr)
387 self._updateroots(phase, roots, tr)
387 # some roots may need to be declared for lower phases
388 # some roots may need to be declared for lower phases
388 delroots.extend(olds - roots)
389 delroots.extend(olds - roots)
389 # declare deleted root in the target phase
390 # declare deleted root in the target phase
390 if targetphase != 0:
391 if targetphase != 0:
391 self._retractboundary(repo, tr, targetphase, delroots)
392 self._retractboundary(repo, tr, targetphase, delroots)
392 repo.invalidatevolatilesets()
393 repo.invalidatevolatilesets()
393
394
394 def retractboundary(self, repo, tr, targetphase, nodes):
395 def retractboundary(self, repo, tr, targetphase, nodes):
395 oldroots = self.phaseroots[:targetphase + 1]
396 oldroots = self.phaseroots[:targetphase + 1]
396 if tr is None:
397 if tr is None:
397 phasetracking = None
398 phasetracking = None
398 else:
399 else:
399 phasetracking = tr.changes.get('phases')
400 phasetracking = tr.changes.get('phases')
400 repo = repo.unfiltered()
401 repo = repo.unfiltered()
401 if (self._retractboundary(repo, tr, targetphase, nodes)
402 if (self._retractboundary(repo, tr, targetphase, nodes)
402 and phasetracking is not None):
403 and phasetracking is not None):
403
404
404 # find the affected revisions
405 # find the affected revisions
405 new = self.phaseroots[targetphase]
406 new = self.phaseroots[targetphase]
406 old = oldroots[targetphase]
407 old = oldroots[targetphase]
407 affected = set(repo.revs('(%ln::) - (%ln::)', new, old))
408 affected = set(repo.revs('(%ln::) - (%ln::)', new, old))
408
409
409 # find the phase of the affected revision
410 # find the phase of the affected revision
410 for phase in xrange(targetphase, -1, -1):
411 for phase in xrange(targetphase, -1, -1):
411 if phase:
412 if phase:
412 roots = oldroots[phase]
413 roots = oldroots[phase]
413 revs = set(repo.revs('%ln::%ld', roots, affected))
414 revs = set(repo.revs('%ln::%ld', roots, affected))
414 affected -= revs
415 affected -= revs
415 else: # public phase
416 else: # public phase
416 revs = affected
417 revs = affected
417 for r in revs:
418 for r in revs:
418 _trackphasechange(phasetracking, r, phase, targetphase)
419 _trackphasechange(phasetracking, r, phase, targetphase)
419 repo.invalidatevolatilesets()
420 repo.invalidatevolatilesets()
420
421
421 def _retractboundary(self, repo, tr, targetphase, nodes):
422 def _retractboundary(self, repo, tr, targetphase, nodes):
422 # Be careful to preserve shallow-copied values: do not update
423 # Be careful to preserve shallow-copied values: do not update
423 # phaseroots values, replace them.
424 # phaseroots values, replace them.
424
425
425 repo = repo.unfiltered()
426 repo = repo.unfiltered()
426 currentroots = self.phaseroots[targetphase]
427 currentroots = self.phaseroots[targetphase]
427 finalroots = oldroots = set(currentroots)
428 finalroots = oldroots = set(currentroots)
428 newroots = [n for n in nodes
429 newroots = [n for n in nodes
429 if self.phase(repo, repo[n].rev()) < targetphase]
430 if self.phase(repo, repo[n].rev()) < targetphase]
430 if newroots:
431 if newroots:
431
432
432 if nullid in newroots:
433 if nullid in newroots:
433 raise error.Abort(_('cannot change null revision phase'))
434 raise error.Abort(_('cannot change null revision phase'))
434 currentroots = currentroots.copy()
435 currentroots = currentroots.copy()
435 currentroots.update(newroots)
436 currentroots.update(newroots)
436
437
437 # Only compute new roots for revs above the roots that are being
438 # Only compute new roots for revs above the roots that are being
438 # retracted.
439 # retracted.
439 minnewroot = min(repo[n].rev() for n in newroots)
440 minnewroot = min(repo[n].rev() for n in newroots)
440 aboveroots = [n for n in currentroots
441 aboveroots = [n for n in currentroots
441 if repo[n].rev() >= minnewroot]
442 if repo[n].rev() >= minnewroot]
442 updatedroots = repo.set('roots(%ln::)', aboveroots)
443 updatedroots = repo.set('roots(%ln::)', aboveroots)
443
444
444 finalroots = set(n for n in currentroots if repo[n].rev() <
445 finalroots = set(n for n in currentroots if repo[n].rev() <
445 minnewroot)
446 minnewroot)
446 finalroots.update(ctx.node() for ctx in updatedroots)
447 finalroots.update(ctx.node() for ctx in updatedroots)
447 if finalroots != oldroots:
448 if finalroots != oldroots:
448 self._updateroots(targetphase, finalroots, tr)
449 self._updateroots(targetphase, finalroots, tr)
449 return True
450 return True
450 return False
451 return False
451
452
452 def filterunknown(self, repo):
453 def filterunknown(self, repo):
453 """remove unknown nodes from the phase boundary
454 """remove unknown nodes from the phase boundary
454
455
455 Nothing is lost as unknown nodes only hold data for their descendants.
456 Nothing is lost as unknown nodes only hold data for their descendants.
456 """
457 """
457 filtered = False
458 filtered = False
458 nodemap = repo.changelog.nodemap # to filter unknown nodes
459 nodemap = repo.changelog.nodemap # to filter unknown nodes
459 for phase, nodes in enumerate(self.phaseroots):
460 for phase, nodes in enumerate(self.phaseroots):
460 missing = sorted(node for node in nodes if node not in nodemap)
461 missing = sorted(node for node in nodes if node not in nodemap)
461 if missing:
462 if missing:
462 for mnode in missing:
463 for mnode in missing:
463 repo.ui.debug(
464 repo.ui.debug(
464 'removing unknown node %s from %i-phase boundary\n'
465 'removing unknown node %s from %i-phase boundary\n'
465 % (short(mnode), phase))
466 % (short(mnode), phase))
466 nodes.symmetric_difference_update(missing)
467 nodes.symmetric_difference_update(missing)
467 filtered = True
468 filtered = True
468 if filtered:
469 if filtered:
469 self.dirty = True
470 self.dirty = True
470 # filterunknown is called by repo.destroyed, we may have no changes in
471 # filterunknown is called by repo.destroyed, we may have no changes in
471 # root but _phasesets contents is certainly invalid (or at least we
472 # root but _phasesets contents is certainly invalid (or at least we
472 # have not proper way to check that). related to issue 3858.
473 # have not proper way to check that). related to issue 3858.
473 #
474 #
474 # The other caller is __init__ that have no _phasesets initialized
475 # The other caller is __init__ that have no _phasesets initialized
475 # anyway. If this change we should consider adding a dedicated
476 # anyway. If this change we should consider adding a dedicated
476 # "destroyed" function to phasecache or a proper cache key mechanism
477 # "destroyed" function to phasecache or a proper cache key mechanism
477 # (see branchmap one)
478 # (see branchmap one)
478 self.invalidate()
479 self.invalidate()
479
480
480 def advanceboundary(repo, tr, targetphase, nodes):
481 def advanceboundary(repo, tr, targetphase, nodes):
481 """Add nodes to a phase changing other nodes phases if necessary.
482 """Add nodes to a phase changing other nodes phases if necessary.
482
483
483 This function move boundary *forward* this means that all nodes
484 This function move boundary *forward* this means that all nodes
484 are set in the target phase or kept in a *lower* phase.
485 are set in the target phase or kept in a *lower* phase.
485
486
486 Simplify boundary to contains phase roots only."""
487 Simplify boundary to contains phase roots only."""
487 phcache = repo._phasecache.copy()
488 phcache = repo._phasecache.copy()
488 phcache.advanceboundary(repo, tr, targetphase, nodes)
489 phcache.advanceboundary(repo, tr, targetphase, nodes)
489 repo._phasecache.replace(phcache)
490 repo._phasecache.replace(phcache)
490
491
491 def retractboundary(repo, tr, targetphase, nodes):
492 def retractboundary(repo, tr, targetphase, nodes):
492 """Set nodes back to a phase changing other nodes phases if
493 """Set nodes back to a phase changing other nodes phases if
493 necessary.
494 necessary.
494
495
495 This function move boundary *backward* this means that all nodes
496 This function move boundary *backward* this means that all nodes
496 are set in the target phase or kept in a *higher* phase.
497 are set in the target phase or kept in a *higher* phase.
497
498
498 Simplify boundary to contains phase roots only."""
499 Simplify boundary to contains phase roots only."""
499 phcache = repo._phasecache.copy()
500 phcache = repo._phasecache.copy()
500 phcache.retractboundary(repo, tr, targetphase, nodes)
501 phcache.retractboundary(repo, tr, targetphase, nodes)
501 repo._phasecache.replace(phcache)
502 repo._phasecache.replace(phcache)
502
503
503 def registernew(repo, tr, targetphase, nodes):
504 def registernew(repo, tr, targetphase, nodes):
504 """register a new revision and its phase
505 """register a new revision and its phase
505
506
506 Code adding revisions to the repository should use this function to
507 Code adding revisions to the repository should use this function to
507 set new changeset in their target phase (or higher).
508 set new changeset in their target phase (or higher).
508 """
509 """
509 phcache = repo._phasecache.copy()
510 phcache = repo._phasecache.copy()
510 phcache.registernew(repo, tr, targetphase, nodes)
511 phcache.registernew(repo, tr, targetphase, nodes)
511 repo._phasecache.replace(phcache)
512 repo._phasecache.replace(phcache)
512
513
513 def listphases(repo):
514 def listphases(repo):
514 """List phases root for serialization over pushkey"""
515 """List phases root for serialization over pushkey"""
515 # Use ordered dictionary so behavior is deterministic.
516 # Use ordered dictionary so behavior is deterministic.
516 keys = util.sortdict()
517 keys = util.sortdict()
517 value = '%i' % draft
518 value = '%i' % draft
518 cl = repo.unfiltered().changelog
519 cl = repo.unfiltered().changelog
519 for root in repo._phasecache.phaseroots[draft]:
520 for root in repo._phasecache.phaseroots[draft]:
520 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
521 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
521 keys[hex(root)] = value
522 keys[hex(root)] = value
522
523
523 if repo.publishing():
524 if repo.publishing():
524 # Add an extra data to let remote know we are a publishing
525 # Add an extra data to let remote know we are a publishing
525 # repo. Publishing repo can't just pretend they are old repo.
526 # repo. Publishing repo can't just pretend they are old repo.
526 # When pushing to a publishing repo, the client still need to
527 # When pushing to a publishing repo, the client still need to
527 # push phase boundary
528 # push phase boundary
528 #
529 #
529 # Push do not only push changeset. It also push phase data.
530 # Push do not only push changeset. It also push phase data.
530 # New phase data may apply to common changeset which won't be
531 # New phase data may apply to common changeset which won't be
531 # push (as they are common). Here is a very simple example:
532 # push (as they are common). Here is a very simple example:
532 #
533 #
533 # 1) repo A push changeset X as draft to repo B
534 # 1) repo A push changeset X as draft to repo B
534 # 2) repo B make changeset X public
535 # 2) repo B make changeset X public
535 # 3) repo B push to repo A. X is not pushed but the data that
536 # 3) repo B push to repo A. X is not pushed but the data that
536 # X as now public should
537 # X as now public should
537 #
538 #
538 # The server can't handle it on it's own as it has no idea of
539 # The server can't handle it on it's own as it has no idea of
539 # client phase data.
540 # client phase data.
540 keys['publishing'] = 'True'
541 keys['publishing'] = 'True'
541 return keys
542 return keys
542
543
543 def pushphase(repo, nhex, oldphasestr, newphasestr):
544 def pushphase(repo, nhex, oldphasestr, newphasestr):
544 """List phases root for serialization over pushkey"""
545 """List phases root for serialization over pushkey"""
545 repo = repo.unfiltered()
546 repo = repo.unfiltered()
546 with repo.lock():
547 with repo.lock():
547 currentphase = repo[nhex].phase()
548 currentphase = repo[nhex].phase()
548 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
549 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
549 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
550 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
550 if currentphase == oldphase and newphase < oldphase:
551 if currentphase == oldphase and newphase < oldphase:
551 with repo.transaction('pushkey-phase') as tr:
552 with repo.transaction('pushkey-phase') as tr:
552 advanceboundary(repo, tr, newphase, [bin(nhex)])
553 advanceboundary(repo, tr, newphase, [bin(nhex)])
553 return True
554 return True
554 elif currentphase == newphase:
555 elif currentphase == newphase:
555 # raced, but got correct result
556 # raced, but got correct result
556 return True
557 return True
557 else:
558 else:
558 return False
559 return False
559
560
560 def subsetphaseheads(repo, subset):
561 def subsetphaseheads(repo, subset):
561 """Finds the phase heads for a subset of a history
562 """Finds the phase heads for a subset of a history
562
563
563 Returns a list indexed by phase number where each item is a list of phase
564 Returns a list indexed by phase number where each item is a list of phase
564 head nodes.
565 head nodes.
565 """
566 """
566 cl = repo.changelog
567 cl = repo.changelog
567
568
568 headsbyphase = [[] for i in allphases]
569 headsbyphase = [[] for i in allphases]
569 # No need to keep track of secret phase; any heads in the subset that
570 # No need to keep track of secret phase; any heads in the subset that
570 # are not mentioned are implicitly secret.
571 # are not mentioned are implicitly secret.
571 for phase in allphases[:-1]:
572 for phase in allphases[:-1]:
572 revset = "heads(%%ln & %s())" % phasenames[phase]
573 revset = "heads(%%ln & %s())" % phasenames[phase]
573 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
574 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
574 return headsbyphase
575 return headsbyphase
575
576
576 def updatephases(repo, trgetter, headsbyphase):
577 def updatephases(repo, trgetter, headsbyphase):
577 """Updates the repo with the given phase heads"""
578 """Updates the repo with the given phase heads"""
578 # Now advance phase boundaries of all but secret phase
579 # Now advance phase boundaries of all but secret phase
579 #
580 #
580 # run the update (and fetch transaction) only if there are actually things
581 # run the update (and fetch transaction) only if there are actually things
581 # to update. This avoid creating empty transaction during no-op operation.
582 # to update. This avoid creating empty transaction during no-op operation.
582
583
583 for phase in allphases[:-1]:
584 for phase in allphases[:-1]:
584 revset = '%%ln - %s()' % phasenames[phase]
585 revset = '%%ln - %s()' % phasenames[phase]
585 heads = [c.node() for c in repo.set(revset, headsbyphase[phase])]
586 heads = [c.node() for c in repo.set(revset, headsbyphase[phase])]
586 if heads:
587 if heads:
587 advanceboundary(repo, trgetter(), phase, heads)
588 advanceboundary(repo, trgetter(), phase, heads)
588
589
589 def analyzeremotephases(repo, subset, roots):
590 def analyzeremotephases(repo, subset, roots):
590 """Compute phases heads and root in a subset of node from root dict
591 """Compute phases heads and root in a subset of node from root dict
591
592
592 * subset is heads of the subset
593 * subset is heads of the subset
593 * roots is {<nodeid> => phase} mapping. key and value are string.
594 * roots is {<nodeid> => phase} mapping. key and value are string.
594
595
595 Accept unknown element input
596 Accept unknown element input
596 """
597 """
597 repo = repo.unfiltered()
598 repo = repo.unfiltered()
598 # build list from dictionary
599 # build list from dictionary
599 draftroots = []
600 draftroots = []
600 nodemap = repo.changelog.nodemap # to filter unknown nodes
601 nodemap = repo.changelog.nodemap # to filter unknown nodes
601 for nhex, phase in roots.iteritems():
602 for nhex, phase in roots.iteritems():
602 if nhex == 'publishing': # ignore data related to publish option
603 if nhex == 'publishing': # ignore data related to publish option
603 continue
604 continue
604 node = bin(nhex)
605 node = bin(nhex)
605 phase = int(phase)
606 phase = int(phase)
606 if phase == public:
607 if phase == public:
607 if node != nullid:
608 if node != nullid:
608 repo.ui.warn(_('ignoring inconsistent public root'
609 repo.ui.warn(_('ignoring inconsistent public root'
609 ' from remote: %s\n') % nhex)
610 ' from remote: %s\n') % nhex)
610 elif phase == draft:
611 elif phase == draft:
611 if node in nodemap:
612 if node in nodemap:
612 draftroots.append(node)
613 draftroots.append(node)
613 else:
614 else:
614 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
615 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
615 % (phase, nhex))
616 % (phase, nhex))
616 # compute heads
617 # compute heads
617 publicheads = newheads(repo, subset, draftroots)
618 publicheads = newheads(repo, subset, draftroots)
618 return publicheads, draftroots
619 return publicheads, draftroots
619
620
620 class remotephasessummary(object):
621 class remotephasessummary(object):
621 """summarize phase information on the remote side
622 """summarize phase information on the remote side
622
623
623 :publishing: True is the remote is publishing
624 :publishing: True is the remote is publishing
624 :publicheads: list of remote public phase heads (nodes)
625 :publicheads: list of remote public phase heads (nodes)
625 :draftheads: list of remote draft phase heads (nodes)
626 :draftheads: list of remote draft phase heads (nodes)
626 :draftroots: list of remote draft phase root (nodes)
627 :draftroots: list of remote draft phase root (nodes)
627 """
628 """
628
629
629 def __init__(self, repo, remotesubset, remoteroots):
630 def __init__(self, repo, remotesubset, remoteroots):
630 unfi = repo.unfiltered()
631 unfi = repo.unfiltered()
631 self._allremoteroots = remoteroots
632 self._allremoteroots = remoteroots
632
633
633 self.publishing = remoteroots.get('publishing', False)
634 self.publishing = remoteroots.get('publishing', False)
634
635
635 ana = analyzeremotephases(repo, remotesubset, remoteroots)
636 ana = analyzeremotephases(repo, remotesubset, remoteroots)
636 self.publicheads, self.draftroots = ana
637 self.publicheads, self.draftroots = ana
637 # Get the list of all "heads" revs draft on remote
638 # Get the list of all "heads" revs draft on remote
638 dheads = unfi.set('heads(%ln::%ln)', self.draftroots, remotesubset)
639 dheads = unfi.set('heads(%ln::%ln)', self.draftroots, remotesubset)
639 self.draftheads = [c.node() for c in dheads]
640 self.draftheads = [c.node() for c in dheads]
640
641
641 def newheads(repo, heads, roots):
642 def newheads(repo, heads, roots):
642 """compute new head of a subset minus another
643 """compute new head of a subset minus another
643
644
644 * `heads`: define the first subset
645 * `heads`: define the first subset
645 * `roots`: define the second we subtract from the first"""
646 * `roots`: define the second we subtract from the first"""
646 repo = repo.unfiltered()
647 repo = repo.unfiltered()
647 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
648 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
648 heads, roots, roots, heads)
649 heads, roots, roots, heads)
649 return [c.node() for c in revset]
650 return [c.node() for c in revset]
650
651
651
652
652 def newcommitphase(ui):
653 def newcommitphase(ui):
653 """helper to get the target phase of new commit
654 """helper to get the target phase of new commit
654
655
655 Handle all possible values for the phases.new-commit options.
656 Handle all possible values for the phases.new-commit options.
656
657
657 """
658 """
658 v = ui.config('phases', 'new-commit')
659 v = ui.config('phases', 'new-commit')
659 try:
660 try:
660 return phasenames.index(v)
661 return phasenames.index(v)
661 except ValueError:
662 except ValueError:
662 try:
663 try:
663 return int(v)
664 return int(v)
664 except ValueError:
665 except ValueError:
665 msg = _("phases.new-commit: not a valid phase name ('%s')")
666 msg = _("phases.new-commit: not a valid phase name ('%s')")
666 raise error.ConfigError(msg % v)
667 raise error.ConfigError(msg % v)
667
668
668 def hassecret(repo):
669 def hassecret(repo):
669 """utility function that check if a repo have any secret changeset."""
670 """utility function that check if a repo have any secret changeset."""
670 return bool(repo._phasecache.phaseroots[2])
671 return bool(repo._phasecache.phaseroots[2])
671
672
672 def preparehookargs(node, old, new):
673 def preparehookargs(node, old, new):
673 if old is None:
674 if old is None:
674 old = ''
675 old = ''
675 else:
676 else:
676 old = phasenames[old]
677 old = phasenames[old]
677 return {'node': node,
678 return {'node': node,
678 'oldphase': old,
679 'oldphase': old,
679 'phase': phasenames[new]}
680 'phase': phasenames[new]}
General Comments 0
You need to be logged in to leave comments. Login now