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