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