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