##// END OF EJS Templates
advanceboundary: add dryrun parameter...
Sushil khanchi -
r38218:36ba5dba default
parent child Browse files
Show More
@@ -1,682 +1,700 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):
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
362 If dryrun is True, no actions will be performed
363
364 Returns a set of revs whose phase is changed or should be changed
361 """
365 """
362 # Be careful to preserve shallow-copied values: do not update
366 # Be careful to preserve shallow-copied values: do not update
363 # phaseroots values, replace them.
367 # phaseroots values, replace them.
364 if tr is None:
368 if tr is None:
365 phasetracking = None
369 phasetracking = None
366 else:
370 else:
367 phasetracking = tr.changes.get('phases')
371 phasetracking = tr.changes.get('phases')
368
372
369 repo = repo.unfiltered()
373 repo = repo.unfiltered()
370
374
375 changes = set() # set of revisions to be changed
371 delroots = [] # set of root deleted by this path
376 delroots = [] # set of root deleted by this path
372 for phase in xrange(targetphase + 1, len(allphases)):
377 for phase in xrange(targetphase + 1, len(allphases)):
373 # filter nodes that are not in a compatible phase already
378 # filter nodes that are not in a compatible phase already
374 nodes = [n for n in nodes
379 nodes = [n for n in nodes
375 if self.phase(repo, repo[n].rev()) >= phase]
380 if self.phase(repo, repo[n].rev()) >= phase]
376 if not nodes:
381 if not nodes:
377 break # no roots to move anymore
382 break # no roots to move anymore
378
383
379 olds = self.phaseroots[phase]
384 olds = self.phaseroots[phase]
380
385
381 affected = repo.revs('%ln::%ln', olds, nodes)
386 affected = repo.revs('%ln::%ln', olds, nodes)
387 changes.update(affected)
388 if dryrun:
389 continue
382 for r in affected:
390 for r in affected:
383 _trackphasechange(phasetracking, r, self.phase(repo, r),
391 _trackphasechange(phasetracking, r, self.phase(repo, r),
384 targetphase)
392 targetphase)
385
393
386 roots = set(ctx.node() for ctx in repo.set(
394 roots = set(ctx.node() for ctx in repo.set(
387 'roots((%ln::) - %ld)', olds, affected))
395 'roots((%ln::) - %ld)', olds, affected))
388 if olds != roots:
396 if olds != roots:
389 self._updateroots(phase, roots, tr)
397 self._updateroots(phase, roots, tr)
390 # some roots may need to be declared for lower phases
398 # some roots may need to be declared for lower phases
391 delroots.extend(olds - roots)
399 delroots.extend(olds - roots)
400 if not dryrun:
392 # declare deleted root in the target phase
401 # declare deleted root in the target phase
393 if targetphase != 0:
402 if targetphase != 0:
394 self._retractboundary(repo, tr, targetphase, delroots)
403 self._retractboundary(repo, tr, targetphase, delroots)
395 repo.invalidatevolatilesets()
404 repo.invalidatevolatilesets()
405 return changes
396
406
397 def retractboundary(self, repo, tr, targetphase, nodes):
407 def retractboundary(self, repo, tr, targetphase, nodes):
398 oldroots = self.phaseroots[:targetphase + 1]
408 oldroots = self.phaseroots[:targetphase + 1]
399 if tr is None:
409 if tr is None:
400 phasetracking = None
410 phasetracking = None
401 else:
411 else:
402 phasetracking = tr.changes.get('phases')
412 phasetracking = tr.changes.get('phases')
403 repo = repo.unfiltered()
413 repo = repo.unfiltered()
404 if (self._retractboundary(repo, tr, targetphase, nodes)
414 if (self._retractboundary(repo, tr, targetphase, nodes)
405 and phasetracking is not None):
415 and phasetracking is not None):
406
416
407 # find the affected revisions
417 # find the affected revisions
408 new = self.phaseroots[targetphase]
418 new = self.phaseroots[targetphase]
409 old = oldroots[targetphase]
419 old = oldroots[targetphase]
410 affected = set(repo.revs('(%ln::) - (%ln::)', new, old))
420 affected = set(repo.revs('(%ln::) - (%ln::)', new, old))
411
421
412 # find the phase of the affected revision
422 # find the phase of the affected revision
413 for phase in xrange(targetphase, -1, -1):
423 for phase in xrange(targetphase, -1, -1):
414 if phase:
424 if phase:
415 roots = oldroots[phase]
425 roots = oldroots[phase]
416 revs = set(repo.revs('%ln::%ld', roots, affected))
426 revs = set(repo.revs('%ln::%ld', roots, affected))
417 affected -= revs
427 affected -= revs
418 else: # public phase
428 else: # public phase
419 revs = affected
429 revs = affected
420 for r in revs:
430 for r in revs:
421 _trackphasechange(phasetracking, r, phase, targetphase)
431 _trackphasechange(phasetracking, r, phase, targetphase)
422 repo.invalidatevolatilesets()
432 repo.invalidatevolatilesets()
423
433
424 def _retractboundary(self, repo, tr, targetphase, nodes):
434 def _retractboundary(self, repo, tr, targetphase, nodes):
425 # Be careful to preserve shallow-copied values: do not update
435 # Be careful to preserve shallow-copied values: do not update
426 # phaseroots values, replace them.
436 # phaseroots values, replace them.
427
437
428 repo = repo.unfiltered()
438 repo = repo.unfiltered()
429 currentroots = self.phaseroots[targetphase]
439 currentroots = self.phaseroots[targetphase]
430 finalroots = oldroots = set(currentroots)
440 finalroots = oldroots = set(currentroots)
431 newroots = [n for n in nodes
441 newroots = [n for n in nodes
432 if self.phase(repo, repo[n].rev()) < targetphase]
442 if self.phase(repo, repo[n].rev()) < targetphase]
433 if newroots:
443 if newroots:
434
444
435 if nullid in newroots:
445 if nullid in newroots:
436 raise error.Abort(_('cannot change null revision phase'))
446 raise error.Abort(_('cannot change null revision phase'))
437 currentroots = currentroots.copy()
447 currentroots = currentroots.copy()
438 currentroots.update(newroots)
448 currentroots.update(newroots)
439
449
440 # 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
441 # retracted.
451 # retracted.
442 minnewroot = min(repo[n].rev() for n in newroots)
452 minnewroot = min(repo[n].rev() for n in newroots)
443 aboveroots = [n for n in currentroots
453 aboveroots = [n for n in currentroots
444 if repo[n].rev() >= minnewroot]
454 if repo[n].rev() >= minnewroot]
445 updatedroots = repo.set('roots(%ln::)', aboveroots)
455 updatedroots = repo.set('roots(%ln::)', aboveroots)
446
456
447 finalroots = set(n for n in currentroots if repo[n].rev() <
457 finalroots = set(n for n in currentroots if repo[n].rev() <
448 minnewroot)
458 minnewroot)
449 finalroots.update(ctx.node() for ctx in updatedroots)
459 finalroots.update(ctx.node() for ctx in updatedroots)
450 if finalroots != oldroots:
460 if finalroots != oldroots:
451 self._updateroots(targetphase, finalroots, tr)
461 self._updateroots(targetphase, finalroots, tr)
452 return True
462 return True
453 return False
463 return False
454
464
455 def filterunknown(self, repo):
465 def filterunknown(self, repo):
456 """remove unknown nodes from the phase boundary
466 """remove unknown nodes from the phase boundary
457
467
458 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.
459 """
469 """
460 filtered = False
470 filtered = False
461 nodemap = repo.changelog.nodemap # to filter unknown nodes
471 nodemap = repo.changelog.nodemap # to filter unknown nodes
462 for phase, nodes in enumerate(self.phaseroots):
472 for phase, nodes in enumerate(self.phaseroots):
463 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)
464 if missing:
474 if missing:
465 for mnode in missing:
475 for mnode in missing:
466 repo.ui.debug(
476 repo.ui.debug(
467 'removing unknown node %s from %i-phase boundary\n'
477 'removing unknown node %s from %i-phase boundary\n'
468 % (short(mnode), phase))
478 % (short(mnode), phase))
469 nodes.symmetric_difference_update(missing)
479 nodes.symmetric_difference_update(missing)
470 filtered = True
480 filtered = True
471 if filtered:
481 if filtered:
472 self.dirty = True
482 self.dirty = True
473 # 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
474 # root but _phasesets contents is certainly invalid (or at least we
484 # root but _phasesets contents is certainly invalid (or at least we
475 # have not proper way to check that). related to issue 3858.
485 # have not proper way to check that). related to issue 3858.
476 #
486 #
477 # The other caller is __init__ that have no _phasesets initialized
487 # The other caller is __init__ that have no _phasesets initialized
478 # anyway. If this change we should consider adding a dedicated
488 # anyway. If this change we should consider adding a dedicated
479 # "destroyed" function to phasecache or a proper cache key mechanism
489 # "destroyed" function to phasecache or a proper cache key mechanism
480 # (see branchmap one)
490 # (see branchmap one)
481 self.invalidate()
491 self.invalidate()
482
492
483 def advanceboundary(repo, tr, targetphase, nodes):
493 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
484 """Add nodes to a phase changing other nodes phases if necessary.
494 """Add nodes to a phase changing other nodes phases if necessary.
485
495
486 This function move boundary *forward* this means that all nodes
496 This function move boundary *forward* this means that all nodes
487 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.
488
498
489 Simplify boundary to contains phase roots only."""
499 Simplify boundary to contains phase roots only.
500
501 If dryrun is True, no actions will be performed
502
503 Returns a set of revs whose phase is changed or should be changed
504 """
490 phcache = repo._phasecache.copy()
505 phcache = repo._phasecache.copy()
491 phcache.advanceboundary(repo, tr, targetphase, nodes)
506 changes = phcache.advanceboundary(repo, tr, targetphase, nodes,
507 dryrun=dryrun)
508 if not dryrun:
492 repo._phasecache.replace(phcache)
509 repo._phasecache.replace(phcache)
510 return changes
493
511
494 def retractboundary(repo, tr, targetphase, nodes):
512 def retractboundary(repo, tr, targetphase, nodes):
495 """Set nodes back to a phase changing other nodes phases if
513 """Set nodes back to a phase changing other nodes phases if
496 necessary.
514 necessary.
497
515
498 This function move boundary *backward* this means that all nodes
516 This function move boundary *backward* this means that all nodes
499 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.
500
518
501 Simplify boundary to contains phase roots only."""
519 Simplify boundary to contains phase roots only."""
502 phcache = repo._phasecache.copy()
520 phcache = repo._phasecache.copy()
503 phcache.retractboundary(repo, tr, targetphase, nodes)
521 phcache.retractboundary(repo, tr, targetphase, nodes)
504 repo._phasecache.replace(phcache)
522 repo._phasecache.replace(phcache)
505
523
506 def registernew(repo, tr, targetphase, nodes):
524 def registernew(repo, tr, targetphase, nodes):
507 """register a new revision and its phase
525 """register a new revision and its phase
508
526
509 Code adding revisions to the repository should use this function to
527 Code adding revisions to the repository should use this function to
510 set new changeset in their target phase (or higher).
528 set new changeset in their target phase (or higher).
511 """
529 """
512 phcache = repo._phasecache.copy()
530 phcache = repo._phasecache.copy()
513 phcache.registernew(repo, tr, targetphase, nodes)
531 phcache.registernew(repo, tr, targetphase, nodes)
514 repo._phasecache.replace(phcache)
532 repo._phasecache.replace(phcache)
515
533
516 def listphases(repo):
534 def listphases(repo):
517 """List phases root for serialization over pushkey"""
535 """List phases root for serialization over pushkey"""
518 # Use ordered dictionary so behavior is deterministic.
536 # Use ordered dictionary so behavior is deterministic.
519 keys = util.sortdict()
537 keys = util.sortdict()
520 value = '%i' % draft
538 value = '%i' % draft
521 cl = repo.unfiltered().changelog
539 cl = repo.unfiltered().changelog
522 for root in repo._phasecache.phaseroots[draft]:
540 for root in repo._phasecache.phaseroots[draft]:
523 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
541 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
524 keys[hex(root)] = value
542 keys[hex(root)] = value
525
543
526 if repo.publishing():
544 if repo.publishing():
527 # 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
528 # repo. Publishing repo can't just pretend they are old repo.
546 # repo. Publishing repo can't just pretend they are old repo.
529 # When pushing to a publishing repo, the client still need to
547 # When pushing to a publishing repo, the client still need to
530 # push phase boundary
548 # push phase boundary
531 #
549 #
532 # Push do not only push changeset. It also push phase data.
550 # Push do not only push changeset. It also push phase data.
533 # 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
534 # push (as they are common). Here is a very simple example:
552 # push (as they are common). Here is a very simple example:
535 #
553 #
536 # 1) repo A push changeset X as draft to repo B
554 # 1) repo A push changeset X as draft to repo B
537 # 2) repo B make changeset X public
555 # 2) repo B make changeset X public
538 # 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
539 # X as now public should
557 # X as now public should
540 #
558 #
541 # 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
542 # client phase data.
560 # client phase data.
543 keys['publishing'] = 'True'
561 keys['publishing'] = 'True'
544 return keys
562 return keys
545
563
546 def pushphase(repo, nhex, oldphasestr, newphasestr):
564 def pushphase(repo, nhex, oldphasestr, newphasestr):
547 """List phases root for serialization over pushkey"""
565 """List phases root for serialization over pushkey"""
548 repo = repo.unfiltered()
566 repo = repo.unfiltered()
549 with repo.lock():
567 with repo.lock():
550 currentphase = repo[nhex].phase()
568 currentphase = repo[nhex].phase()
551 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
569 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
552 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
570 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
553 if currentphase == oldphase and newphase < oldphase:
571 if currentphase == oldphase and newphase < oldphase:
554 with repo.transaction('pushkey-phase') as tr:
572 with repo.transaction('pushkey-phase') as tr:
555 advanceboundary(repo, tr, newphase, [bin(nhex)])
573 advanceboundary(repo, tr, newphase, [bin(nhex)])
556 return True
574 return True
557 elif currentphase == newphase:
575 elif currentphase == newphase:
558 # raced, but got correct result
576 # raced, but got correct result
559 return True
577 return True
560 else:
578 else:
561 return False
579 return False
562
580
563 def subsetphaseheads(repo, subset):
581 def subsetphaseheads(repo, subset):
564 """Finds the phase heads for a subset of a history
582 """Finds the phase heads for a subset of a history
565
583
566 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
567 head nodes.
585 head nodes.
568 """
586 """
569 cl = repo.changelog
587 cl = repo.changelog
570
588
571 headsbyphase = [[] for i in allphases]
589 headsbyphase = [[] for i in allphases]
572 # 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
573 # are not mentioned are implicitly secret.
591 # are not mentioned are implicitly secret.
574 for phase in allphases[:-1]:
592 for phase in allphases[:-1]:
575 revset = "heads(%%ln & %s())" % phasenames[phase]
593 revset = "heads(%%ln & %s())" % phasenames[phase]
576 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)]
577 return headsbyphase
595 return headsbyphase
578
596
579 def updatephases(repo, trgetter, headsbyphase):
597 def updatephases(repo, trgetter, headsbyphase):
580 """Updates the repo with the given phase heads"""
598 """Updates the repo with the given phase heads"""
581 # Now advance phase boundaries of all but secret phase
599 # Now advance phase boundaries of all but secret phase
582 #
600 #
583 # 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
584 # to update. This avoid creating empty transaction during no-op operation.
602 # to update. This avoid creating empty transaction during no-op operation.
585
603
586 for phase in allphases[:-1]:
604 for phase in allphases[:-1]:
587 revset = '%%ln - %s()' % phasenames[phase]
605 revset = '%%ln - %s()' % phasenames[phase]
588 heads = [c.node() for c in repo.set(revset, headsbyphase[phase])]
606 heads = [c.node() for c in repo.set(revset, headsbyphase[phase])]
589 if heads:
607 if heads:
590 advanceboundary(repo, trgetter(), phase, heads)
608 advanceboundary(repo, trgetter(), phase, heads)
591
609
592 def analyzeremotephases(repo, subset, roots):
610 def analyzeremotephases(repo, subset, roots):
593 """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
594
612
595 * subset is heads of the subset
613 * subset is heads of the subset
596 * roots is {<nodeid> => phase} mapping. key and value are string.
614 * roots is {<nodeid> => phase} mapping. key and value are string.
597
615
598 Accept unknown element input
616 Accept unknown element input
599 """
617 """
600 repo = repo.unfiltered()
618 repo = repo.unfiltered()
601 # build list from dictionary
619 # build list from dictionary
602 draftroots = []
620 draftroots = []
603 nodemap = repo.changelog.nodemap # to filter unknown nodes
621 nodemap = repo.changelog.nodemap # to filter unknown nodes
604 for nhex, phase in roots.iteritems():
622 for nhex, phase in roots.iteritems():
605 if nhex == 'publishing': # ignore data related to publish option
623 if nhex == 'publishing': # ignore data related to publish option
606 continue
624 continue
607 node = bin(nhex)
625 node = bin(nhex)
608 phase = int(phase)
626 phase = int(phase)
609 if phase == public:
627 if phase == public:
610 if node != nullid:
628 if node != nullid:
611 repo.ui.warn(_('ignoring inconsistent public root'
629 repo.ui.warn(_('ignoring inconsistent public root'
612 ' from remote: %s\n') % nhex)
630 ' from remote: %s\n') % nhex)
613 elif phase == draft:
631 elif phase == draft:
614 if node in nodemap:
632 if node in nodemap:
615 draftroots.append(node)
633 draftroots.append(node)
616 else:
634 else:
617 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
635 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
618 % (phase, nhex))
636 % (phase, nhex))
619 # compute heads
637 # compute heads
620 publicheads = newheads(repo, subset, draftroots)
638 publicheads = newheads(repo, subset, draftroots)
621 return publicheads, draftroots
639 return publicheads, draftroots
622
640
623 class remotephasessummary(object):
641 class remotephasessummary(object):
624 """summarize phase information on the remote side
642 """summarize phase information on the remote side
625
643
626 :publishing: True is the remote is publishing
644 :publishing: True is the remote is publishing
627 :publicheads: list of remote public phase heads (nodes)
645 :publicheads: list of remote public phase heads (nodes)
628 :draftheads: list of remote draft phase heads (nodes)
646 :draftheads: list of remote draft phase heads (nodes)
629 :draftroots: list of remote draft phase root (nodes)
647 :draftroots: list of remote draft phase root (nodes)
630 """
648 """
631
649
632 def __init__(self, repo, remotesubset, remoteroots):
650 def __init__(self, repo, remotesubset, remoteroots):
633 unfi = repo.unfiltered()
651 unfi = repo.unfiltered()
634 self._allremoteroots = remoteroots
652 self._allremoteroots = remoteroots
635
653
636 self.publishing = remoteroots.get('publishing', False)
654 self.publishing = remoteroots.get('publishing', False)
637
655
638 ana = analyzeremotephases(repo, remotesubset, remoteroots)
656 ana = analyzeremotephases(repo, remotesubset, remoteroots)
639 self.publicheads, self.draftroots = ana
657 self.publicheads, self.draftroots = ana
640 # Get the list of all "heads" revs draft on remote
658 # Get the list of all "heads" revs draft on remote
641 dheads = unfi.set('heads(%ln::%ln)', self.draftroots, remotesubset)
659 dheads = unfi.set('heads(%ln::%ln)', self.draftroots, remotesubset)
642 self.draftheads = [c.node() for c in dheads]
660 self.draftheads = [c.node() for c in dheads]
643
661
644 def newheads(repo, heads, roots):
662 def newheads(repo, heads, roots):
645 """compute new head of a subset minus another
663 """compute new head of a subset minus another
646
664
647 * `heads`: define the first subset
665 * `heads`: define the first subset
648 * `roots`: define the second we subtract from the first"""
666 * `roots`: define the second we subtract from the first"""
649 repo = repo.unfiltered()
667 repo = repo.unfiltered()
650 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
668 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
651 heads, roots, roots, heads)
669 heads, roots, roots, heads)
652 return [c.node() for c in revset]
670 return [c.node() for c in revset]
653
671
654
672
655 def newcommitphase(ui):
673 def newcommitphase(ui):
656 """helper to get the target phase of new commit
674 """helper to get the target phase of new commit
657
675
658 Handle all possible values for the phases.new-commit options.
676 Handle all possible values for the phases.new-commit options.
659
677
660 """
678 """
661 v = ui.config('phases', 'new-commit')
679 v = ui.config('phases', 'new-commit')
662 try:
680 try:
663 return phasenames.index(v)
681 return phasenames.index(v)
664 except ValueError:
682 except ValueError:
665 try:
683 try:
666 return int(v)
684 return int(v)
667 except ValueError:
685 except ValueError:
668 msg = _("phases.new-commit: not a valid phase name ('%s')")
686 msg = _("phases.new-commit: not a valid phase name ('%s')")
669 raise error.ConfigError(msg % v)
687 raise error.ConfigError(msg % v)
670
688
671 def hassecret(repo):
689 def hassecret(repo):
672 """utility function that check if a repo have any secret changeset."""
690 """utility function that check if a repo have any secret changeset."""
673 return bool(repo._phasecache.phaseroots[2])
691 return bool(repo._phasecache.phaseroots[2])
674
692
675 def preparehookargs(node, old, new):
693 def preparehookargs(node, old, new):
676 if old is None:
694 if old is None:
677 old = ''
695 old = ''
678 else:
696 else:
679 old = phasenames[old]
697 old = phasenames[old]
680 return {'node': node,
698 return {'node': node,
681 'oldphase': old,
699 'oldphase': old,
682 'phase': phasenames[new]}
700 'phase': phasenames[new]}
General Comments 0
You need to be logged in to leave comments. Login now