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