##// END OF EJS Templates
phases: filter revision that are already in the right phase...
marmoute -
r52314:2eb93812 default
parent child Browse files
Show More
@@ -1,1130 +1,1138 b''
1 """ Mercurial phases support code
1 """ Mercurial phases support code
2
2
3 ---
3 ---
4
4
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 Logilab SA <contact@logilab.fr>
6 Logilab SA <contact@logilab.fr>
7 Augie Fackler <durin42@gmail.com>
7 Augie Fackler <durin42@gmail.com>
8
8
9 This software may be used and distributed according to the terms
9 This software may be used and distributed according to the terms
10 of the GNU General Public License version 2 or any later version.
10 of the GNU General Public License version 2 or any later version.
11
11
12 ---
12 ---
13
13
14 This module implements most phase logic in mercurial.
14 This module implements most phase logic in mercurial.
15
15
16
16
17 Basic Concept
17 Basic Concept
18 =============
18 =============
19
19
20 A 'changeset phase' is an indicator that tells us how a changeset is
20 A 'changeset phase' is an indicator that tells us how a changeset is
21 manipulated and communicated. The details of each phase is described
21 manipulated and communicated. The details of each phase is described
22 below, here we describe the properties they have in common.
22 below, here we describe the properties they have in common.
23
23
24 Like bookmarks, phases are not stored in history and thus are not
24 Like bookmarks, phases are not stored in history and thus are not
25 permanent and leave no audit trail.
25 permanent and leave no audit trail.
26
26
27 First, no changeset can be in two phases at once. Phases are ordered,
27 First, no changeset can be in two phases at once. Phases are ordered,
28 so they can be considered from lowest to highest. The default, lowest
28 so they can be considered from lowest to highest. The default, lowest
29 phase is 'public' - this is the normal phase of existing changesets. A
29 phase is 'public' - this is the normal phase of existing changesets. A
30 child changeset can not be in a lower phase than its parents.
30 child changeset can not be in a lower phase than its parents.
31
31
32 These phases share a hierarchy of traits:
32 These phases share a hierarchy of traits:
33
33
34 immutable shared
34 immutable shared
35 public: X X
35 public: X X
36 draft: X
36 draft: X
37 secret:
37 secret:
38
38
39 Local commits are draft by default.
39 Local commits are draft by default.
40
40
41 Phase Movement and Exchange
41 Phase Movement and Exchange
42 ===========================
42 ===========================
43
43
44 Phase data is exchanged by pushkey on pull and push. Some servers have
44 Phase data is exchanged by pushkey on pull and push. Some servers have
45 a publish option set, we call such a server a "publishing server".
45 a publish option set, we call such a server a "publishing server".
46 Pushing a draft changeset to a publishing server changes the phase to
46 Pushing a draft changeset to a publishing server changes the phase to
47 public.
47 public.
48
48
49 A small list of fact/rules define the exchange of phase:
49 A small list of fact/rules define the exchange of phase:
50
50
51 * old client never changes server states
51 * old client never changes server states
52 * pull never changes server states
52 * pull never changes server states
53 * publish and old server changesets are seen as public by client
53 * publish and old server changesets are seen as public by client
54 * any secret changeset seen in another repository is lowered to at
54 * any secret changeset seen in another repository is lowered to at
55 least draft
55 least draft
56
56
57 Here is the final table summing up the 49 possible use cases of phase
57 Here is the final table summing up the 49 possible use cases of phase
58 exchange:
58 exchange:
59
59
60 server
60 server
61 old publish non-publish
61 old publish non-publish
62 N X N D P N D P
62 N X N D P N D P
63 old client
63 old client
64 pull
64 pull
65 N - X/X - X/D X/P - X/D X/P
65 N - X/X - X/D X/P - X/D X/P
66 X - X/X - X/D X/P - X/D X/P
66 X - X/X - X/D X/P - X/D X/P
67 push
67 push
68 X X/X X/X X/P X/P X/P X/D X/D X/P
68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 new client
69 new client
70 pull
70 pull
71 N - P/X - P/D P/P - D/D P/P
71 N - P/X - P/D P/P - D/D P/P
72 D - P/X - P/D P/P - D/D P/P
72 D - P/X - P/D P/P - D/D P/P
73 P - P/X - P/D P/P - P/D P/P
73 P - P/X - P/D P/P - P/D P/P
74 push
74 push
75 D P/X P/X P/P P/P P/P D/D D/D P/P
75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 P P/X P/X P/P P/P P/P P/P P/P P/P
76 P P/X P/X P/P P/P P/P P/P P/P P/P
77
77
78 Legend:
78 Legend:
79
79
80 A/B = final state on client / state on server
80 A/B = final state on client / state on server
81
81
82 * N = new/not present,
82 * N = new/not present,
83 * P = public,
83 * P = public,
84 * D = draft,
84 * D = draft,
85 * X = not tracked (i.e., the old client or server has no internal
85 * X = not tracked (i.e., the old client or server has no internal
86 way of recording the phase.)
86 way of recording the phase.)
87
87
88 passive = only pushes
88 passive = only pushes
89
89
90
90
91 A cell here can be read like this:
91 A cell here can be read like this:
92
92
93 "When a new client pushes a draft changeset (D) to a publishing
93 "When a new client pushes a draft changeset (D) to a publishing
94 server where it's not present (N), it's marked public on both
94 server where it's not present (N), it's marked public on both
95 sides (P/P)."
95 sides (P/P)."
96
96
97 Note: old client behave as a publishing server with draft only content
97 Note: old client behave as a publishing server with draft only content
98 - other people see it as public
98 - other people see it as public
99 - content is pushed as draft
99 - content is pushed as draft
100
100
101 """
101 """
102
102
103
103
104 import struct
104 import struct
105 import typing
105 import typing
106 import weakref
106 import weakref
107
107
108 from typing import (
108 from typing import (
109 Any,
109 Any,
110 Callable,
110 Callable,
111 Dict,
111 Dict,
112 Iterable,
112 Iterable,
113 List,
113 List,
114 Optional,
114 Optional,
115 Set,
115 Set,
116 Tuple,
116 Tuple,
117 )
117 )
118
118
119 from .i18n import _
119 from .i18n import _
120 from .node import (
120 from .node import (
121 bin,
121 bin,
122 hex,
122 hex,
123 nullrev,
123 nullrev,
124 short,
124 short,
125 wdirrev,
125 wdirrev,
126 )
126 )
127 from . import (
127 from . import (
128 error,
128 error,
129 pycompat,
129 pycompat,
130 requirements,
130 requirements,
131 smartset,
131 smartset,
132 txnutil,
132 txnutil,
133 util,
133 util,
134 )
134 )
135
135
136 Phaseroots = Dict[int, Set[int]]
136 Phaseroots = Dict[int, Set[int]]
137 PhaseSets = Dict[int, Set[int]]
137 PhaseSets = Dict[int, Set[int]]
138
138
139 if typing.TYPE_CHECKING:
139 if typing.TYPE_CHECKING:
140 from . import (
140 from . import (
141 localrepo,
141 localrepo,
142 ui as uimod,
142 ui as uimod,
143 )
143 )
144
144
145 # keeps pyflakes happy
145 # keeps pyflakes happy
146 assert [uimod]
146 assert [uimod]
147
147
148 Phasedefaults = List[
148 Phasedefaults = List[
149 Callable[[localrepo.localrepository, Phaseroots], Phaseroots]
149 Callable[[localrepo.localrepository, Phaseroots], Phaseroots]
150 ]
150 ]
151
151
152
152
153 _fphasesentry = struct.Struct(b'>i20s')
153 _fphasesentry = struct.Struct(b'>i20s')
154
154
155 # record phase index
155 # record phase index
156 public: int = 0
156 public: int = 0
157 draft: int = 1
157 draft: int = 1
158 secret: int = 2
158 secret: int = 2
159 archived = 32 # non-continuous for compatibility
159 archived = 32 # non-continuous for compatibility
160 internal = 96 # non-continuous for compatibility
160 internal = 96 # non-continuous for compatibility
161 allphases = (public, draft, secret, archived, internal)
161 allphases = (public, draft, secret, archived, internal)
162 trackedphases = (draft, secret, archived, internal)
162 trackedphases = (draft, secret, archived, internal)
163 not_public_phases = trackedphases
163 not_public_phases = trackedphases
164 # record phase names
164 # record phase names
165 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
165 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
166 phasenames = dict(enumerate(cmdphasenames))
166 phasenames = dict(enumerate(cmdphasenames))
167 phasenames[archived] = b'archived'
167 phasenames[archived] = b'archived'
168 phasenames[internal] = b'internal'
168 phasenames[internal] = b'internal'
169 # map phase name to phase number
169 # map phase name to phase number
170 phasenumber = {name: phase for phase, name in phasenames.items()}
170 phasenumber = {name: phase for phase, name in phasenames.items()}
171 # like phasenumber, but also include maps for the numeric and binary
171 # like phasenumber, but also include maps for the numeric and binary
172 # phase number to the phase number
172 # phase number to the phase number
173 phasenumber2 = phasenumber.copy()
173 phasenumber2 = phasenumber.copy()
174 phasenumber2.update({phase: phase for phase in phasenames})
174 phasenumber2.update({phase: phase for phase in phasenames})
175 phasenumber2.update({b'%i' % phase: phase for phase in phasenames})
175 phasenumber2.update({b'%i' % phase: phase for phase in phasenames})
176 # record phase property
176 # record phase property
177 mutablephases = (draft, secret, archived, internal)
177 mutablephases = (draft, secret, archived, internal)
178 relevant_mutable_phases = (draft, secret) # could be obsolete or unstable
178 relevant_mutable_phases = (draft, secret) # could be obsolete or unstable
179 remotehiddenphases = (secret, archived, internal)
179 remotehiddenphases = (secret, archived, internal)
180 localhiddenphases = (internal, archived)
180 localhiddenphases = (internal, archived)
181
181
182 all_internal_phases = tuple(p for p in allphases if p & internal)
182 all_internal_phases = tuple(p for p in allphases if p & internal)
183 # We do not want any internal content to exit the repository, ever.
183 # We do not want any internal content to exit the repository, ever.
184 no_bundle_phases = all_internal_phases
184 no_bundle_phases = all_internal_phases
185
185
186
186
187 def supportinternal(repo: "localrepo.localrepository") -> bool:
187 def supportinternal(repo: "localrepo.localrepository") -> bool:
188 """True if the internal phase can be used on a repository"""
188 """True if the internal phase can be used on a repository"""
189 return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements
189 return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements
190
190
191
191
192 def supportarchived(repo: "localrepo.localrepository") -> bool:
192 def supportarchived(repo: "localrepo.localrepository") -> bool:
193 """True if the archived phase can be used on a repository"""
193 """True if the archived phase can be used on a repository"""
194 return requirements.ARCHIVED_PHASE_REQUIREMENT in repo.requirements
194 return requirements.ARCHIVED_PHASE_REQUIREMENT in repo.requirements
195
195
196
196
197 def _readroots(
197 def _readroots(
198 repo: "localrepo.localrepository",
198 repo: "localrepo.localrepository",
199 phasedefaults: Optional["Phasedefaults"] = None,
199 phasedefaults: Optional["Phasedefaults"] = None,
200 ) -> Tuple[Phaseroots, bool]:
200 ) -> Tuple[Phaseroots, bool]:
201 """Read phase roots from disk
201 """Read phase roots from disk
202
202
203 phasedefaults is a list of fn(repo, roots) callable, which are
203 phasedefaults is a list of fn(repo, roots) callable, which are
204 executed if the phase roots file does not exist. When phases are
204 executed if the phase roots file does not exist. When phases are
205 being initialized on an existing repository, this could be used to
205 being initialized on an existing repository, this could be used to
206 set selected changesets phase to something else than public.
206 set selected changesets phase to something else than public.
207
207
208 Return (roots, dirty) where dirty is true if roots differ from
208 Return (roots, dirty) where dirty is true if roots differ from
209 what is being stored.
209 what is being stored.
210 """
210 """
211 repo = repo.unfiltered()
211 repo = repo.unfiltered()
212 dirty = False
212 dirty = False
213 roots = {i: set() for i in allphases}
213 roots = {i: set() for i in allphases}
214 to_rev = repo.changelog.index.get_rev
214 to_rev = repo.changelog.index.get_rev
215 unknown_msg = b'removing unknown node %s from %i-phase boundary\n'
215 unknown_msg = b'removing unknown node %s from %i-phase boundary\n'
216 try:
216 try:
217 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
217 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
218 try:
218 try:
219 for line in f:
219 for line in f:
220 str_phase, hex_node = line.split()
220 str_phase, hex_node = line.split()
221 phase = int(str_phase)
221 phase = int(str_phase)
222 node = bin(hex_node)
222 node = bin(hex_node)
223 rev = to_rev(node)
223 rev = to_rev(node)
224 if rev is None:
224 if rev is None:
225 repo.ui.debug(unknown_msg % (short(hex_node), phase))
225 repo.ui.debug(unknown_msg % (short(hex_node), phase))
226 dirty = True
226 dirty = True
227 else:
227 else:
228 roots[phase].add(rev)
228 roots[phase].add(rev)
229 finally:
229 finally:
230 f.close()
230 f.close()
231 except FileNotFoundError:
231 except FileNotFoundError:
232 if phasedefaults:
232 if phasedefaults:
233 for f in phasedefaults:
233 for f in phasedefaults:
234 roots = f(repo, roots)
234 roots = f(repo, roots)
235 dirty = True
235 dirty = True
236 return roots, dirty
236 return roots, dirty
237
237
238
238
239 def binaryencode(phasemapping: Dict[int, List[bytes]]) -> bytes:
239 def binaryencode(phasemapping: Dict[int, List[bytes]]) -> bytes:
240 """encode a 'phase -> nodes' mapping into a binary stream
240 """encode a 'phase -> nodes' mapping into a binary stream
241
241
242 The revision lists are encoded as (phase, root) pairs.
242 The revision lists are encoded as (phase, root) pairs.
243 """
243 """
244 binarydata = []
244 binarydata = []
245 for phase, nodes in phasemapping.items():
245 for phase, nodes in phasemapping.items():
246 for head in nodes:
246 for head in nodes:
247 binarydata.append(_fphasesentry.pack(phase, head))
247 binarydata.append(_fphasesentry.pack(phase, head))
248 return b''.join(binarydata)
248 return b''.join(binarydata)
249
249
250
250
251 def binarydecode(stream) -> Dict[int, List[bytes]]:
251 def binarydecode(stream) -> Dict[int, List[bytes]]:
252 """decode a binary stream into a 'phase -> nodes' mapping
252 """decode a binary stream into a 'phase -> nodes' mapping
253
253
254 The (phase, root) pairs are turned back into a dictionary with
254 The (phase, root) pairs are turned back into a dictionary with
255 the phase as index and the aggregated roots of that phase as value."""
255 the phase as index and the aggregated roots of that phase as value."""
256 headsbyphase = {i: [] for i in allphases}
256 headsbyphase = {i: [] for i in allphases}
257 entrysize = _fphasesentry.size
257 entrysize = _fphasesentry.size
258 while True:
258 while True:
259 entry = stream.read(entrysize)
259 entry = stream.read(entrysize)
260 if len(entry) < entrysize:
260 if len(entry) < entrysize:
261 if entry:
261 if entry:
262 raise error.Abort(_(b'bad phase-heads stream'))
262 raise error.Abort(_(b'bad phase-heads stream'))
263 break
263 break
264 phase, node = _fphasesentry.unpack(entry)
264 phase, node = _fphasesentry.unpack(entry)
265 headsbyphase[phase].append(node)
265 headsbyphase[phase].append(node)
266 return headsbyphase
266 return headsbyphase
267
267
268
268
269 def _sortedrange_insert(data, idx, rev, t):
269 def _sortedrange_insert(data, idx, rev, t):
270 merge_before = False
270 merge_before = False
271 if idx:
271 if idx:
272 r1, t1 = data[idx - 1]
272 r1, t1 = data[idx - 1]
273 merge_before = r1[-1] + 1 == rev and t1 == t
273 merge_before = r1[-1] + 1 == rev and t1 == t
274 merge_after = False
274 merge_after = False
275 if idx < len(data):
275 if idx < len(data):
276 r2, t2 = data[idx]
276 r2, t2 = data[idx]
277 merge_after = r2[0] == rev + 1 and t2 == t
277 merge_after = r2[0] == rev + 1 and t2 == t
278
278
279 if merge_before and merge_after:
279 if merge_before and merge_after:
280 data[idx - 1] = (range(r1[0], r2[-1] + 1), t)
280 data[idx - 1] = (range(r1[0], r2[-1] + 1), t)
281 data.pop(idx)
281 data.pop(idx)
282 elif merge_before:
282 elif merge_before:
283 data[idx - 1] = (range(r1[0], rev + 1), t)
283 data[idx - 1] = (range(r1[0], rev + 1), t)
284 elif merge_after:
284 elif merge_after:
285 data[idx] = (range(rev, r2[-1] + 1), t)
285 data[idx] = (range(rev, r2[-1] + 1), t)
286 else:
286 else:
287 data.insert(idx, (range(rev, rev + 1), t))
287 data.insert(idx, (range(rev, rev + 1), t))
288
288
289
289
290 def _sortedrange_split(data, idx, rev, t):
290 def _sortedrange_split(data, idx, rev, t):
291 r1, t1 = data[idx]
291 r1, t1 = data[idx]
292 if t == t1:
292 if t == t1:
293 return
293 return
294 t = (t1[0], t[1])
294 t = (t1[0], t[1])
295 if len(r1) == 1:
295 if len(r1) == 1:
296 data.pop(idx)
296 data.pop(idx)
297 _sortedrange_insert(data, idx, rev, t)
297 _sortedrange_insert(data, idx, rev, t)
298 elif r1[0] == rev:
298 elif r1[0] == rev:
299 data[idx] = (range(rev + 1, r1[-1] + 1), t1)
299 data[idx] = (range(rev + 1, r1[-1] + 1), t1)
300 _sortedrange_insert(data, idx, rev, t)
300 _sortedrange_insert(data, idx, rev, t)
301 elif r1[-1] == rev:
301 elif r1[-1] == rev:
302 data[idx] = (range(r1[0], rev), t1)
302 data[idx] = (range(r1[0], rev), t1)
303 _sortedrange_insert(data, idx + 1, rev, t)
303 _sortedrange_insert(data, idx + 1, rev, t)
304 else:
304 else:
305 data[idx : idx + 1] = [
305 data[idx : idx + 1] = [
306 (range(r1[0], rev), t1),
306 (range(r1[0], rev), t1),
307 (range(rev, rev + 1), t),
307 (range(rev, rev + 1), t),
308 (range(rev + 1, r1[-1] + 1), t1),
308 (range(rev + 1, r1[-1] + 1), t1),
309 ]
309 ]
310
310
311
311
312 def _trackphasechange(data, rev, old, new):
312 def _trackphasechange(data, rev, old, new):
313 """add a phase move to the <data> list of ranges
313 """add a phase move to the <data> list of ranges
314
314
315 If data is None, nothing happens.
315 If data is None, nothing happens.
316 """
316 """
317 if data is None:
317 if data is None:
318 return
318 return
319
319
320 # If data is empty, create a one-revision range and done
320 # If data is empty, create a one-revision range and done
321 if not data:
321 if not data:
322 data.insert(0, (range(rev, rev + 1), (old, new)))
322 data.insert(0, (range(rev, rev + 1), (old, new)))
323 return
323 return
324
324
325 low = 0
325 low = 0
326 high = len(data)
326 high = len(data)
327 t = (old, new)
327 t = (old, new)
328 while low < high:
328 while low < high:
329 mid = (low + high) // 2
329 mid = (low + high) // 2
330 revs = data[mid][0]
330 revs = data[mid][0]
331 revs_low = revs[0]
331 revs_low = revs[0]
332 revs_high = revs[-1]
332 revs_high = revs[-1]
333
333
334 if rev >= revs_low and rev <= revs_high:
334 if rev >= revs_low and rev <= revs_high:
335 _sortedrange_split(data, mid, rev, t)
335 _sortedrange_split(data, mid, rev, t)
336 return
336 return
337
337
338 if revs_low == rev + 1:
338 if revs_low == rev + 1:
339 if mid and data[mid - 1][0][-1] == rev:
339 if mid and data[mid - 1][0][-1] == rev:
340 _sortedrange_split(data, mid - 1, rev, t)
340 _sortedrange_split(data, mid - 1, rev, t)
341 else:
341 else:
342 _sortedrange_insert(data, mid, rev, t)
342 _sortedrange_insert(data, mid, rev, t)
343 return
343 return
344
344
345 if revs_high == rev - 1:
345 if revs_high == rev - 1:
346 if mid + 1 < len(data) and data[mid + 1][0][0] == rev:
346 if mid + 1 < len(data) and data[mid + 1][0][0] == rev:
347 _sortedrange_split(data, mid + 1, rev, t)
347 _sortedrange_split(data, mid + 1, rev, t)
348 else:
348 else:
349 _sortedrange_insert(data, mid + 1, rev, t)
349 _sortedrange_insert(data, mid + 1, rev, t)
350 return
350 return
351
351
352 if revs_low > rev:
352 if revs_low > rev:
353 high = mid
353 high = mid
354 else:
354 else:
355 low = mid + 1
355 low = mid + 1
356
356
357 if low == len(data):
357 if low == len(data):
358 data.append((range(rev, rev + 1), t))
358 data.append((range(rev, rev + 1), t))
359 return
359 return
360
360
361 r1, t1 = data[low]
361 r1, t1 = data[low]
362 if r1[0] > rev:
362 if r1[0] > rev:
363 data.insert(low, (range(rev, rev + 1), t))
363 data.insert(low, (range(rev, rev + 1), t))
364 else:
364 else:
365 data.insert(low + 1, (range(rev, rev + 1), t))
365 data.insert(low + 1, (range(rev, rev + 1), t))
366
366
367
367
368 # consider incrementaly updating the phase set the update set is not bigger
368 # consider incrementaly updating the phase set the update set is not bigger
369 # than this size
369 # than this size
370 #
370 #
371 # Be warned, this number is picked arbitrarily, without any benchmark. It
371 # Be warned, this number is picked arbitrarily, without any benchmark. It
372 # should blindly pickup "small update"
372 # should blindly pickup "small update"
373 INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE = 100
373 INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE = 100
374
374
375
375
376 class phasecache:
376 class phasecache:
377 def __init__(
377 def __init__(
378 self,
378 self,
379 repo: "localrepo.localrepository",
379 repo: "localrepo.localrepository",
380 phasedefaults: Optional["Phasedefaults"],
380 phasedefaults: Optional["Phasedefaults"],
381 _load: bool = True,
381 _load: bool = True,
382 ):
382 ):
383 if _load:
383 if _load:
384 # Cheap trick to allow shallow-copy without copy module
384 # Cheap trick to allow shallow-copy without copy module
385 loaded = _readroots(repo, phasedefaults)
385 loaded = _readroots(repo, phasedefaults)
386 self._phaseroots: Phaseroots = loaded[0]
386 self._phaseroots: Phaseroots = loaded[0]
387 self.dirty: bool = loaded[1]
387 self.dirty: bool = loaded[1]
388 self._loadedrevslen = 0
388 self._loadedrevslen = 0
389 self._phasesets: PhaseSets = None
389 self._phasesets: PhaseSets = None
390
390
391 def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool:
391 def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool:
392 """detect if there are revisions with non-public phase"""
392 """detect if there are revisions with non-public phase"""
393 # XXX deprecate the unused repo argument
393 # XXX deprecate the unused repo argument
394 return any(
394 return any(
395 revs for phase, revs in self._phaseroots.items() if phase != public
395 revs for phase, revs in self._phaseroots.items() if phase != public
396 )
396 )
397
397
398 def nonpublicphaseroots(
398 def nonpublicphaseroots(
399 self, repo: "localrepo.localrepository"
399 self, repo: "localrepo.localrepository"
400 ) -> Set[int]:
400 ) -> Set[int]:
401 """returns the roots of all non-public phases
401 """returns the roots of all non-public phases
402
402
403 The roots are not minimized, so if the secret revisions are
403 The roots are not minimized, so if the secret revisions are
404 descendants of draft revisions, their roots will still be present.
404 descendants of draft revisions, their roots will still be present.
405 """
405 """
406 repo = repo.unfiltered()
406 repo = repo.unfiltered()
407 self._ensure_phase_sets(repo)
407 self._ensure_phase_sets(repo)
408 return set().union(
408 return set().union(
409 *[
409 *[
410 revs
410 revs
411 for phase, revs in self._phaseroots.items()
411 for phase, revs in self._phaseroots.items()
412 if phase != public
412 if phase != public
413 ]
413 ]
414 )
414 )
415
415
416 def getrevset(
416 def getrevset(
417 self,
417 self,
418 repo: "localrepo.localrepository",
418 repo: "localrepo.localrepository",
419 phases: Iterable[int],
419 phases: Iterable[int],
420 subset: Optional[Any] = None,
420 subset: Optional[Any] = None,
421 ) -> Any:
421 ) -> Any:
422 # TODO: finish typing this
422 # TODO: finish typing this
423 """return a smartset for the given phases"""
423 """return a smartset for the given phases"""
424 self._ensure_phase_sets(repo.unfiltered())
424 self._ensure_phase_sets(repo.unfiltered())
425 phases = set(phases)
425 phases = set(phases)
426 publicphase = public in phases
426 publicphase = public in phases
427
427
428 if publicphase:
428 if publicphase:
429 # In this case, phases keeps all the *other* phases.
429 # In this case, phases keeps all the *other* phases.
430 phases = set(allphases).difference(phases)
430 phases = set(allphases).difference(phases)
431 if not phases:
431 if not phases:
432 return smartset.fullreposet(repo)
432 return smartset.fullreposet(repo)
433
433
434 # fast path: _phasesets contains the interesting sets,
434 # fast path: _phasesets contains the interesting sets,
435 # might only need a union and post-filtering.
435 # might only need a union and post-filtering.
436 revsneedscopy = False
436 revsneedscopy = False
437 if len(phases) == 1:
437 if len(phases) == 1:
438 [p] = phases
438 [p] = phases
439 revs = self._phasesets[p]
439 revs = self._phasesets[p]
440 revsneedscopy = True # Don't modify _phasesets
440 revsneedscopy = True # Don't modify _phasesets
441 else:
441 else:
442 # revs has the revisions in all *other* phases.
442 # revs has the revisions in all *other* phases.
443 revs = set.union(*[self._phasesets[p] for p in phases])
443 revs = set.union(*[self._phasesets[p] for p in phases])
444
444
445 def _addwdir(wdirsubset, wdirrevs):
445 def _addwdir(wdirsubset, wdirrevs):
446 if wdirrev in wdirsubset and repo[None].phase() in phases:
446 if wdirrev in wdirsubset and repo[None].phase() in phases:
447 if revsneedscopy:
447 if revsneedscopy:
448 wdirrevs = wdirrevs.copy()
448 wdirrevs = wdirrevs.copy()
449 # The working dir would never be in the # cache, but it was in
449 # The working dir would never be in the # cache, but it was in
450 # the subset being filtered for its phase (or filtered out,
450 # the subset being filtered for its phase (or filtered out,
451 # depending on publicphase), so add it to the output to be
451 # depending on publicphase), so add it to the output to be
452 # included (or filtered out).
452 # included (or filtered out).
453 wdirrevs.add(wdirrev)
453 wdirrevs.add(wdirrev)
454 return wdirrevs
454 return wdirrevs
455
455
456 if not publicphase:
456 if not publicphase:
457 if repo.changelog.filteredrevs:
457 if repo.changelog.filteredrevs:
458 revs = revs - repo.changelog.filteredrevs
458 revs = revs - repo.changelog.filteredrevs
459
459
460 if subset is None:
460 if subset is None:
461 return smartset.baseset(revs)
461 return smartset.baseset(revs)
462 else:
462 else:
463 revs = _addwdir(subset, revs)
463 revs = _addwdir(subset, revs)
464 return subset & smartset.baseset(revs)
464 return subset & smartset.baseset(revs)
465 else:
465 else:
466 if subset is None:
466 if subset is None:
467 subset = smartset.fullreposet(repo)
467 subset = smartset.fullreposet(repo)
468
468
469 revs = _addwdir(subset, revs)
469 revs = _addwdir(subset, revs)
470
470
471 if not revs:
471 if not revs:
472 return subset
472 return subset
473 return subset.filter(lambda r: r not in revs)
473 return subset.filter(lambda r: r not in revs)
474
474
475 def copy(self):
475 def copy(self):
476 # Shallow copy meant to ensure isolation in
476 # Shallow copy meant to ensure isolation in
477 # advance/retractboundary(), nothing more.
477 # advance/retractboundary(), nothing more.
478 ph = self.__class__(None, None, _load=False)
478 ph = self.__class__(None, None, _load=False)
479 ph._phaseroots = self._phaseroots.copy()
479 ph._phaseroots = self._phaseroots.copy()
480 ph.dirty = self.dirty
480 ph.dirty = self.dirty
481 ph._loadedrevslen = self._loadedrevslen
481 ph._loadedrevslen = self._loadedrevslen
482 if self._phasesets is None:
482 if self._phasesets is None:
483 ph._phasesets = None
483 ph._phasesets = None
484 else:
484 else:
485 ph._phasesets = self._phasesets.copy()
485 ph._phasesets = self._phasesets.copy()
486 return ph
486 return ph
487
487
488 def replace(self, phcache):
488 def replace(self, phcache):
489 """replace all values in 'self' with content of phcache"""
489 """replace all values in 'self' with content of phcache"""
490 for a in (
490 for a in (
491 '_phaseroots',
491 '_phaseroots',
492 'dirty',
492 'dirty',
493 '_loadedrevslen',
493 '_loadedrevslen',
494 '_phasesets',
494 '_phasesets',
495 ):
495 ):
496 setattr(self, a, getattr(phcache, a))
496 setattr(self, a, getattr(phcache, a))
497
497
498 def _getphaserevsnative(self, repo):
498 def _getphaserevsnative(self, repo):
499 repo = repo.unfiltered()
499 repo = repo.unfiltered()
500 return repo.changelog.computephases(self._phaseroots)
500 return repo.changelog.computephases(self._phaseroots)
501
501
502 def _computephaserevspure(self, repo):
502 def _computephaserevspure(self, repo):
503 repo = repo.unfiltered()
503 repo = repo.unfiltered()
504 cl = repo.changelog
504 cl = repo.changelog
505 self._phasesets = {phase: set() for phase in allphases}
505 self._phasesets = {phase: set() for phase in allphases}
506 lowerroots = set()
506 lowerroots = set()
507 for phase in reversed(trackedphases):
507 for phase in reversed(trackedphases):
508 roots = self._phaseroots[phase]
508 roots = self._phaseroots[phase]
509 if roots:
509 if roots:
510 ps = set(cl.descendants(roots))
510 ps = set(cl.descendants(roots))
511 for root in roots:
511 for root in roots:
512 ps.add(root)
512 ps.add(root)
513 ps.difference_update(lowerroots)
513 ps.difference_update(lowerroots)
514 lowerroots.update(ps)
514 lowerroots.update(ps)
515 self._phasesets[phase] = ps
515 self._phasesets[phase] = ps
516 self._loadedrevslen = len(cl)
516 self._loadedrevslen = len(cl)
517
517
518 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
518 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
519 """ensure phase information is loaded in the object"""
519 """ensure phase information is loaded in the object"""
520 assert repo.filtername is None
520 assert repo.filtername is None
521 update = -1
521 update = -1
522 cl = repo.changelog
522 cl = repo.changelog
523 cl_size = len(cl)
523 cl_size = len(cl)
524 if self._phasesets is None:
524 if self._phasesets is None:
525 update = 0
525 update = 0
526 else:
526 else:
527 if cl_size > self._loadedrevslen:
527 if cl_size > self._loadedrevslen:
528 # check if an incremental update is worth it.
528 # check if an incremental update is worth it.
529 # note we need a tradeoff here because the whole logic is not
529 # note we need a tradeoff here because the whole logic is not
530 # stored and implemented in native code nd datastructure.
530 # stored and implemented in native code nd datastructure.
531 # Otherwise the incremental update woul always be a win.
531 # Otherwise the incremental update woul always be a win.
532 missing = cl_size - self._loadedrevslen
532 missing = cl_size - self._loadedrevslen
533 if missing <= INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE:
533 if missing <= INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE:
534 update = self._loadedrevslen
534 update = self._loadedrevslen
535 else:
535 else:
536 update = 0
536 update = 0
537
537
538 if update == 0:
538 if update == 0:
539 try:
539 try:
540 res = self._getphaserevsnative(repo)
540 res = self._getphaserevsnative(repo)
541 self._loadedrevslen, self._phasesets = res
541 self._loadedrevslen, self._phasesets = res
542 except AttributeError:
542 except AttributeError:
543 self._computephaserevspure(repo)
543 self._computephaserevspure(repo)
544 assert self._loadedrevslen == len(repo.changelog)
544 assert self._loadedrevslen == len(repo.changelog)
545 elif update > 0:
545 elif update > 0:
546 # good candidate for native code
546 # good candidate for native code
547 assert update == self._loadedrevslen
547 assert update == self._loadedrevslen
548 if self.hasnonpublicphases(repo):
548 if self.hasnonpublicphases(repo):
549 start = self._loadedrevslen
549 start = self._loadedrevslen
550 get_phase = self.phase
550 get_phase = self.phase
551 rev_phases = [0] * missing
551 rev_phases = [0] * missing
552 parents = cl.parentrevs
552 parents = cl.parentrevs
553 sets = {phase: set() for phase in self._phasesets}
553 sets = {phase: set() for phase in self._phasesets}
554 for phase, roots in self._phaseroots.items():
554 for phase, roots in self._phaseroots.items():
555 # XXX should really store the max somewhere
555 # XXX should really store the max somewhere
556 for r in roots:
556 for r in roots:
557 if r >= start:
557 if r >= start:
558 rev_phases[r - start] = phase
558 rev_phases[r - start] = phase
559 for rev in range(start, cl_size):
559 for rev in range(start, cl_size):
560 phase = rev_phases[rev - start]
560 phase = rev_phases[rev - start]
561 p1, p2 = parents(rev)
561 p1, p2 = parents(rev)
562 if p1 == nullrev:
562 if p1 == nullrev:
563 p1_phase = public
563 p1_phase = public
564 elif p1 >= start:
564 elif p1 >= start:
565 p1_phase = rev_phases[p1 - start]
565 p1_phase = rev_phases[p1 - start]
566 else:
566 else:
567 p1_phase = max(phase, get_phase(repo, p1))
567 p1_phase = max(phase, get_phase(repo, p1))
568 if p2 == nullrev:
568 if p2 == nullrev:
569 p2_phase = public
569 p2_phase = public
570 elif p2 >= start:
570 elif p2 >= start:
571 p2_phase = rev_phases[p2 - start]
571 p2_phase = rev_phases[p2 - start]
572 else:
572 else:
573 p2_phase = max(phase, get_phase(repo, p2))
573 p2_phase = max(phase, get_phase(repo, p2))
574 phase = max(phase, p1_phase, p2_phase)
574 phase = max(phase, p1_phase, p2_phase)
575 if phase > public:
575 if phase > public:
576 rev_phases[rev - start] = phase
576 rev_phases[rev - start] = phase
577 sets[phase].add(rev)
577 sets[phase].add(rev)
578
578
579 # Be careful to preserve shallow-copied values: do not update
579 # Be careful to preserve shallow-copied values: do not update
580 # phaseroots values, replace them.
580 # phaseroots values, replace them.
581 for phase, extra in sets.items():
581 for phase, extra in sets.items():
582 if extra:
582 if extra:
583 self._phasesets[phase] = self._phasesets[phase] | extra
583 self._phasesets[phase] = self._phasesets[phase] | extra
584 self._loadedrevslen = cl_size
584 self._loadedrevslen = cl_size
585
585
586 def invalidate(self):
586 def invalidate(self):
587 self._loadedrevslen = 0
587 self._loadedrevslen = 0
588 self._phasesets = None
588 self._phasesets = None
589
589
590 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
590 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
591 # We need a repo argument here to be able to build _phasesets
591 # We need a repo argument here to be able to build _phasesets
592 # if necessary. The repository instance is not stored in
592 # if necessary. The repository instance is not stored in
593 # phasecache to avoid reference cycles. The changelog instance
593 # phasecache to avoid reference cycles. The changelog instance
594 # is not stored because it is a filecache() property and can
594 # is not stored because it is a filecache() property and can
595 # be replaced without us being notified.
595 # be replaced without us being notified.
596 if rev == nullrev:
596 if rev == nullrev:
597 return public
597 return public
598 if rev < nullrev:
598 if rev < nullrev:
599 raise ValueError(_(b'cannot lookup negative revision'))
599 raise ValueError(_(b'cannot lookup negative revision'))
600 # double check self._loadedrevslen to avoid an extra method call as
600 # double check self._loadedrevslen to avoid an extra method call as
601 # python is slow for that.
601 # python is slow for that.
602 if rev >= self._loadedrevslen:
602 if rev >= self._loadedrevslen:
603 self._ensure_phase_sets(repo.unfiltered())
603 self._ensure_phase_sets(repo.unfiltered())
604 for phase in trackedphases:
604 for phase in trackedphases:
605 if rev in self._phasesets[phase]:
605 if rev in self._phasesets[phase]:
606 return phase
606 return phase
607 return public
607 return public
608
608
609 def write(self, repo):
609 def write(self, repo):
610 if not self.dirty:
610 if not self.dirty:
611 return
611 return
612 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
612 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
613 try:
613 try:
614 self._write(repo.unfiltered(), f)
614 self._write(repo.unfiltered(), f)
615 finally:
615 finally:
616 f.close()
616 f.close()
617
617
618 def _write(self, repo, fp):
618 def _write(self, repo, fp):
619 assert repo.filtername is None
619 assert repo.filtername is None
620 to_node = repo.changelog.node
620 to_node = repo.changelog.node
621 for phase, roots in self._phaseroots.items():
621 for phase, roots in self._phaseroots.items():
622 for r in sorted(roots):
622 for r in sorted(roots):
623 h = to_node(r)
623 h = to_node(r)
624 fp.write(b'%i %s\n' % (phase, hex(h)))
624 fp.write(b'%i %s\n' % (phase, hex(h)))
625 self.dirty = False
625 self.dirty = False
626
626
627 def _updateroots(self, repo, phase, newroots, tr, invalidate=True):
627 def _updateroots(self, repo, phase, newroots, tr, invalidate=True):
628 self._phaseroots[phase] = newroots
628 self._phaseroots[phase] = newroots
629 self.dirty = True
629 self.dirty = True
630 if invalidate:
630 if invalidate:
631 self.invalidate()
631 self.invalidate()
632
632
633 assert repo.filtername is None
633 assert repo.filtername is None
634 wrepo = weakref.ref(repo)
634 wrepo = weakref.ref(repo)
635
635
636 def tr_write(fp):
636 def tr_write(fp):
637 repo = wrepo()
637 repo = wrepo()
638 assert repo is not None
638 assert repo is not None
639 self._write(repo, fp)
639 self._write(repo, fp)
640
640
641 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
641 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
642 tr.hookargs[b'phases_moved'] = b'1'
642 tr.hookargs[b'phases_moved'] = b'1'
643
643
644 def registernew(self, repo, tr, targetphase, revs):
644 def registernew(self, repo, tr, targetphase, revs):
645 repo = repo.unfiltered()
645 repo = repo.unfiltered()
646 self._retractboundary(repo, tr, targetphase, [], revs=revs)
646 self._retractboundary(repo, tr, targetphase, [], revs=revs)
647 if tr is not None and b'phases' in tr.changes:
647 if tr is not None and b'phases' in tr.changes:
648 phasetracking = tr.changes[b'phases']
648 phasetracking = tr.changes[b'phases']
649 phase = self.phase
649 phase = self.phase
650 for rev in sorted(revs):
650 for rev in sorted(revs):
651 revphase = phase(repo, rev)
651 revphase = phase(repo, rev)
652 _trackphasechange(phasetracking, rev, None, revphase)
652 _trackphasechange(phasetracking, rev, None, revphase)
653 repo.invalidatevolatilesets()
653 repo.invalidatevolatilesets()
654
654
655 def advanceboundary(
655 def advanceboundary(
656 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
656 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
657 ):
657 ):
658 """Set all 'nodes' to phase 'targetphase'
658 """Set all 'nodes' to phase 'targetphase'
659
659
660 Nodes with a phase lower than 'targetphase' are not affected.
660 Nodes with a phase lower than 'targetphase' are not affected.
661
661
662 If dryrun is True, no actions will be performed
662 If dryrun is True, no actions will be performed
663
663
664 Returns a set of revs whose phase is changed or should be changed
664 Returns a set of revs whose phase is changed or should be changed
665 """
665 """
666 if targetphase == public and not self.hasnonpublicphases(repo):
666 if targetphase == public and not self.hasnonpublicphases(repo):
667 return set()
667 return set()
668 # Be careful to preserve shallow-copied values: do not update
668 # Be careful to preserve shallow-copied values: do not update
669 # phaseroots values, replace them.
669 # phaseroots values, replace them.
670 if revs is None:
670 if revs is None:
671 revs = []
671 revs = []
672 if not revs and not nodes:
672 if not revs and not nodes:
673 return set()
673 return set()
674 if tr is None:
674 if tr is None:
675 phasetracking = None
675 phasetracking = None
676 else:
676 else:
677 phasetracking = tr.changes.get(b'phases')
677 phasetracking = tr.changes.get(b'phases')
678
678
679 repo = repo.unfiltered()
679 repo = repo.unfiltered()
680 revs = [repo[n].rev() for n in nodes] + [r for r in revs]
680 revs = [repo[n].rev() for n in nodes] + [r for r in revs]
681
681
682 changes = set() # set of revisions to be changed
682 changes = set() # set of revisions to be changed
683 delroots = [] # set of root deleted by this path
683 delroots = [] # set of root deleted by this path
684 for phase in (phase for phase in allphases if phase > targetphase):
684 for phase in (phase for phase in allphases if phase > targetphase):
685 # filter nodes that are not in a compatible phase already
685 # filter nodes that are not in a compatible phase already
686 revs = [rev for rev in revs if self.phase(repo, rev) >= phase]
686 revs = [rev for rev in revs if self.phase(repo, rev) >= phase]
687 if not revs:
687 if not revs:
688 break # no roots to move anymore
688 break # no roots to move anymore
689
689
690 olds = self._phaseroots[phase]
690 olds = self._phaseroots[phase]
691
691
692 affected = repo.revs(b'%ld::%ld', olds, revs)
692 affected = repo.revs(b'%ld::%ld', olds, revs)
693 changes.update(affected)
693 changes.update(affected)
694 if dryrun:
694 if dryrun:
695 continue
695 continue
696 for r in affected:
696 for r in affected:
697 _trackphasechange(
697 _trackphasechange(
698 phasetracking, r, self.phase(repo, r), targetphase
698 phasetracking, r, self.phase(repo, r), targetphase
699 )
699 )
700
700
701 roots = set(repo.revs(b'roots((%ld::) - %ld)', olds, affected))
701 roots = set(repo.revs(b'roots((%ld::) - %ld)', olds, affected))
702 if olds != roots:
702 if olds != roots:
703 self._updateroots(repo, phase, roots, tr)
703 self._updateroots(repo, phase, roots, tr)
704 # some roots may need to be declared for lower phases
704 # some roots may need to be declared for lower phases
705 delroots.extend(olds - roots)
705 delroots.extend(olds - roots)
706 if not dryrun:
706 if not dryrun:
707 # declare deleted root in the target phase
707 # declare deleted root in the target phase
708 if targetphase != 0:
708 if targetphase != 0:
709 self._retractboundary(repo, tr, targetphase, revs=delroots)
709 self._retractboundary(repo, tr, targetphase, revs=delroots)
710 repo.invalidatevolatilesets()
710 repo.invalidatevolatilesets()
711 return changes
711 return changes
712
712
713 def retractboundary(self, repo, tr, targetphase, nodes):
713 def retractboundary(self, repo, tr, targetphase, nodes):
714 if tr is None:
714 if tr is None:
715 phasetracking = None
715 phasetracking = None
716 else:
716 else:
717 phasetracking = tr.changes.get(b'phases')
717 phasetracking = tr.changes.get(b'phases')
718 repo = repo.unfiltered()
718 repo = repo.unfiltered()
719 retracted = self._retractboundary(repo, tr, targetphase, nodes)
719 retracted = self._retractboundary(repo, tr, targetphase, nodes)
720 if retracted and phasetracking is not None:
720 if retracted and phasetracking is not None:
721 for r, old_phase in sorted(retracted.items()):
721 for r, old_phase in sorted(retracted.items()):
722 _trackphasechange(phasetracking, r, old_phase, targetphase)
722 _trackphasechange(phasetracking, r, old_phase, targetphase)
723 repo.invalidatevolatilesets()
723 repo.invalidatevolatilesets()
724
724
725 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
725 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
726 if targetphase == public:
726 if targetphase == public:
727 return {}
727 return {}
728 if (
728 if (
729 targetphase == internal
729 targetphase == internal
730 and not supportinternal(repo)
730 and not supportinternal(repo)
731 or targetphase == archived
731 or targetphase == archived
732 and not supportarchived(repo)
732 and not supportarchived(repo)
733 ):
733 ):
734 name = phasenames[targetphase]
734 name = phasenames[targetphase]
735 msg = b'this repository does not support the %s phase' % name
735 msg = b'this repository does not support the %s phase' % name
736 raise error.ProgrammingError(msg)
736 raise error.ProgrammingError(msg)
737 assert repo.filtername is None
737 assert repo.filtername is None
738 cl = repo.changelog
738 cl = repo.changelog
739 torev = cl.index.rev
739 torev = cl.index.rev
740 new_revs = set()
740 new_revs = set()
741 if revs is not None:
741 if revs is not None:
742 new_revs.update(revs)
742 new_revs.update(revs)
743 if nodes is not None:
743 if nodes is not None:
744 new_revs.update(torev(node) for node in nodes)
744 new_revs.update(torev(node) for node in nodes)
745 if not new_revs: # bail out early to avoid the loadphaserevs call
745 if not new_revs: # bail out early to avoid the loadphaserevs call
746 return {} # note: why do people call retractboundary with nothing ?
746 return {} # note: why do people call retractboundary with nothing ?
747
747
748 if nullrev in new_revs:
748 if nullrev in new_revs:
749 raise error.Abort(_(b'cannot change null revision phase'))
749 raise error.Abort(_(b'cannot change null revision phase'))
750
750
751 # Filter revision that are already in the right phase
752 self._ensure_phase_sets(repo)
753 for phase, revs in self._phasesets.items():
754 if phase >= targetphase:
755 new_revs -= revs
756 if not new_revs: # all revisions already in the right phases
757 return {}
758
751 # Compute change in phase roots by walking the graph
759 # Compute change in phase roots by walking the graph
752 #
760 #
753 # note: If we had a cheap parent β†’ children mapping we could do
761 # note: If we had a cheap parent β†’ children mapping we could do
754 # something even cheaper/more-bounded
762 # something even cheaper/more-bounded
755 #
763 #
756 # The idea would be to walk from item in new_revs stopping at
764 # The idea would be to walk from item in new_revs stopping at
757 # descendant with phases >= target_phase.
765 # descendant with phases >= target_phase.
758 #
766 #
759 # 1) This detect new_revs that are not new_roots (either already >=
767 # 1) This detect new_revs that are not new_roots (either already >=
760 # target_phase or reachable though another new_revs
768 # target_phase or reachable though another new_revs
761 # 2) This detect replaced current_roots as we reach them
769 # 2) This detect replaced current_roots as we reach them
762 # 3) This can avoid walking to the tip if we retract over a small
770 # 3) This can avoid walking to the tip if we retract over a small
763 # branch.
771 # branch.
764 #
772 #
765 # So instead, we do a variation of this, we walk from the smaller new
773 # So instead, we do a variation of this, we walk from the smaller new
766 # revision to the tip to avoid missing any potential children.
774 # revision to the tip to avoid missing any potential children.
767 #
775 #
768 # The following code would be a good candidate for native code… if only
776 # The following code would be a good candidate for native code… if only
769 # we could knew the phase of a changeset efficiently in native code.
777 # we could knew the phase of a changeset efficiently in native code.
770 parents = cl.parentrevs
778 parents = cl.parentrevs
771 phase = self.phase
779 phase = self.phase
772 new_roots = set() # roots added by this phases
780 new_roots = set() # roots added by this phases
773 changed_revs = {} # revision affected by this call
781 changed_revs = {} # revision affected by this call
774 replaced_roots = set() # older roots replaced by this call
782 replaced_roots = set() # older roots replaced by this call
775 currentroots = self._phaseroots[targetphase]
783 currentroots = self._phaseroots[targetphase]
776 start = min(new_revs)
784 start = min(new_revs)
777 end = len(cl)
785 end = len(cl)
778 rev_phases = [None] * (end - start)
786 rev_phases = [None] * (end - start)
779 for r in range(start, end):
787 for r in range(start, end):
780
788
781 # gather information about the current_rev
789 # gather information about the current_rev
782 r_phase = phase(repo, r)
790 r_phase = phase(repo, r)
783 p_phase = None # phase inherited from parents
791 p_phase = None # phase inherited from parents
784 p1, p2 = parents(r)
792 p1, p2 = parents(r)
785 if p1 >= start:
793 if p1 >= start:
786 p1_phase = rev_phases[p1 - start]
794 p1_phase = rev_phases[p1 - start]
787 if p1_phase is not None:
795 if p1_phase is not None:
788 p_phase = p1_phase
796 p_phase = p1_phase
789 if p2 >= start:
797 if p2 >= start:
790 p2_phase = rev_phases[p2 - start]
798 p2_phase = rev_phases[p2 - start]
791 if p2_phase is not None:
799 if p2_phase is not None:
792 if p_phase is not None:
800 if p_phase is not None:
793 p_phase = max(p_phase, p2_phase)
801 p_phase = max(p_phase, p2_phase)
794 else:
802 else:
795 p_phase = p2_phase
803 p_phase = p2_phase
796
804
797 # assess the situation
805 # assess the situation
798 if r in new_revs and r_phase < targetphase:
806 if r in new_revs and r_phase < targetphase:
799 if p_phase is None or p_phase < targetphase:
807 if p_phase is None or p_phase < targetphase:
800 new_roots.add(r)
808 new_roots.add(r)
801 rev_phases[r - start] = targetphase
809 rev_phases[r - start] = targetphase
802 changed_revs[r] = r_phase
810 changed_revs[r] = r_phase
803 elif p_phase is None:
811 elif p_phase is None:
804 rev_phases[r - start] = r_phase
812 rev_phases[r - start] = r_phase
805 else:
813 else:
806 if p_phase > r_phase:
814 if p_phase > r_phase:
807 rev_phases[r - start] = p_phase
815 rev_phases[r - start] = p_phase
808 else:
816 else:
809 rev_phases[r - start] = r_phase
817 rev_phases[r - start] = r_phase
810 if p_phase == targetphase:
818 if p_phase == targetphase:
811 if p_phase > r_phase:
819 if p_phase > r_phase:
812 changed_revs[r] = r_phase
820 changed_revs[r] = r_phase
813 elif r in currentroots:
821 elif r in currentroots:
814 replaced_roots.add(r)
822 replaced_roots.add(r)
815 sets = self._phasesets
823 sets = self._phasesets
816 sets[targetphase].update(changed_revs)
824 sets[targetphase].update(changed_revs)
817 for r, old in changed_revs.items():
825 for r, old in changed_revs.items():
818 if old > public:
826 if old > public:
819 sets[old].discard(r)
827 sets[old].discard(r)
820
828
821 if new_roots:
829 if new_roots:
822 assert changed_revs
830 assert changed_revs
823
831
824 final_roots = new_roots | currentroots - replaced_roots
832 final_roots = new_roots | currentroots - replaced_roots
825 self._updateroots(
833 self._updateroots(
826 repo,
834 repo,
827 targetphase,
835 targetphase,
828 final_roots,
836 final_roots,
829 tr,
837 tr,
830 invalidate=False,
838 invalidate=False,
831 )
839 )
832 if targetphase > 1:
840 if targetphase > 1:
833 retracted = set(changed_revs)
841 retracted = set(changed_revs)
834 for lower_phase in range(1, targetphase):
842 for lower_phase in range(1, targetphase):
835 lower_roots = self._phaseroots.get(lower_phase)
843 lower_roots = self._phaseroots.get(lower_phase)
836 if lower_roots is None:
844 if lower_roots is None:
837 continue
845 continue
838 if lower_roots & retracted:
846 if lower_roots & retracted:
839 simpler_roots = lower_roots - retracted
847 simpler_roots = lower_roots - retracted
840 self._updateroots(
848 self._updateroots(
841 repo,
849 repo,
842 lower_phase,
850 lower_phase,
843 simpler_roots,
851 simpler_roots,
844 tr,
852 tr,
845 invalidate=False,
853 invalidate=False,
846 )
854 )
847 return changed_revs
855 return changed_revs
848 else:
856 else:
849 assert not changed_revs
857 assert not changed_revs
850 assert not replaced_roots
858 assert not replaced_roots
851 return {}
859 return {}
852
860
853 def register_strip(
861 def register_strip(
854 self,
862 self,
855 repo,
863 repo,
856 tr,
864 tr,
857 strip_rev: int,
865 strip_rev: int,
858 ):
866 ):
859 """announce a strip to the phase cache
867 """announce a strip to the phase cache
860
868
861 Any roots higher than the stripped revision should be dropped.
869 Any roots higher than the stripped revision should be dropped.
862 """
870 """
863 for targetphase, roots in list(self._phaseroots.items()):
871 for targetphase, roots in list(self._phaseroots.items()):
864 filtered = {r for r in roots if r >= strip_rev}
872 filtered = {r for r in roots if r >= strip_rev}
865 if filtered:
873 if filtered:
866 self._updateroots(repo, targetphase, roots - filtered, tr)
874 self._updateroots(repo, targetphase, roots - filtered, tr)
867 self.invalidate()
875 self.invalidate()
868
876
869
877
870 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
878 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
871 """Add nodes to a phase changing other nodes phases if necessary.
879 """Add nodes to a phase changing other nodes phases if necessary.
872
880
873 This function move boundary *forward* this means that all nodes
881 This function move boundary *forward* this means that all nodes
874 are set in the target phase or kept in a *lower* phase.
882 are set in the target phase or kept in a *lower* phase.
875
883
876 Simplify boundary to contains phase roots only.
884 Simplify boundary to contains phase roots only.
877
885
878 If dryrun is True, no actions will be performed
886 If dryrun is True, no actions will be performed
879
887
880 Returns a set of revs whose phase is changed or should be changed
888 Returns a set of revs whose phase is changed or should be changed
881 """
889 """
882 if revs is None:
890 if revs is None:
883 revs = []
891 revs = []
884 phcache = repo._phasecache.copy()
892 phcache = repo._phasecache.copy()
885 changes = phcache.advanceboundary(
893 changes = phcache.advanceboundary(
886 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
894 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
887 )
895 )
888 if not dryrun:
896 if not dryrun:
889 repo._phasecache.replace(phcache)
897 repo._phasecache.replace(phcache)
890 return changes
898 return changes
891
899
892
900
893 def retractboundary(repo, tr, targetphase, nodes):
901 def retractboundary(repo, tr, targetphase, nodes):
894 """Set nodes back to a phase changing other nodes phases if
902 """Set nodes back to a phase changing other nodes phases if
895 necessary.
903 necessary.
896
904
897 This function move boundary *backward* this means that all nodes
905 This function move boundary *backward* this means that all nodes
898 are set in the target phase or kept in a *higher* phase.
906 are set in the target phase or kept in a *higher* phase.
899
907
900 Simplify boundary to contains phase roots only."""
908 Simplify boundary to contains phase roots only."""
901 phcache = repo._phasecache.copy()
909 phcache = repo._phasecache.copy()
902 phcache.retractboundary(repo, tr, targetphase, nodes)
910 phcache.retractboundary(repo, tr, targetphase, nodes)
903 repo._phasecache.replace(phcache)
911 repo._phasecache.replace(phcache)
904
912
905
913
906 def registernew(repo, tr, targetphase, revs):
914 def registernew(repo, tr, targetphase, revs):
907 """register a new revision and its phase
915 """register a new revision and its phase
908
916
909 Code adding revisions to the repository should use this function to
917 Code adding revisions to the repository should use this function to
910 set new changeset in their target phase (or higher).
918 set new changeset in their target phase (or higher).
911 """
919 """
912 phcache = repo._phasecache.copy()
920 phcache = repo._phasecache.copy()
913 phcache.registernew(repo, tr, targetphase, revs)
921 phcache.registernew(repo, tr, targetphase, revs)
914 repo._phasecache.replace(phcache)
922 repo._phasecache.replace(phcache)
915
923
916
924
917 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
925 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
918 """List phases root for serialization over pushkey"""
926 """List phases root for serialization over pushkey"""
919 # Use ordered dictionary so behavior is deterministic.
927 # Use ordered dictionary so behavior is deterministic.
920 keys = util.sortdict()
928 keys = util.sortdict()
921 value = b'%i' % draft
929 value = b'%i' % draft
922 cl = repo.unfiltered().changelog
930 cl = repo.unfiltered().changelog
923 to_node = cl.node
931 to_node = cl.node
924 for root in repo._phasecache._phaseroots[draft]:
932 for root in repo._phasecache._phaseroots[draft]:
925 if repo._phasecache.phase(repo, root) <= draft:
933 if repo._phasecache.phase(repo, root) <= draft:
926 keys[hex(to_node(root))] = value
934 keys[hex(to_node(root))] = value
927
935
928 if repo.publishing():
936 if repo.publishing():
929 # Add an extra data to let remote know we are a publishing
937 # Add an extra data to let remote know we are a publishing
930 # repo. Publishing repo can't just pretend they are old repo.
938 # repo. Publishing repo can't just pretend they are old repo.
931 # When pushing to a publishing repo, the client still need to
939 # When pushing to a publishing repo, the client still need to
932 # push phase boundary
940 # push phase boundary
933 #
941 #
934 # Push do not only push changeset. It also push phase data.
942 # Push do not only push changeset. It also push phase data.
935 # New phase data may apply to common changeset which won't be
943 # New phase data may apply to common changeset which won't be
936 # push (as they are common). Here is a very simple example:
944 # push (as they are common). Here is a very simple example:
937 #
945 #
938 # 1) repo A push changeset X as draft to repo B
946 # 1) repo A push changeset X as draft to repo B
939 # 2) repo B make changeset X public
947 # 2) repo B make changeset X public
940 # 3) repo B push to repo A. X is not pushed but the data that
948 # 3) repo B push to repo A. X is not pushed but the data that
941 # X as now public should
949 # X as now public should
942 #
950 #
943 # The server can't handle it on it's own as it has no idea of
951 # The server can't handle it on it's own as it has no idea of
944 # client phase data.
952 # client phase data.
945 keys[b'publishing'] = b'True'
953 keys[b'publishing'] = b'True'
946 return keys
954 return keys
947
955
948
956
949 def pushphase(
957 def pushphase(
950 repo: "localrepo.localrepository",
958 repo: "localrepo.localrepository",
951 nhex: bytes,
959 nhex: bytes,
952 oldphasestr: bytes,
960 oldphasestr: bytes,
953 newphasestr: bytes,
961 newphasestr: bytes,
954 ) -> bool:
962 ) -> bool:
955 """List phases root for serialization over pushkey"""
963 """List phases root for serialization over pushkey"""
956 repo = repo.unfiltered()
964 repo = repo.unfiltered()
957 with repo.lock():
965 with repo.lock():
958 currentphase = repo[nhex].phase()
966 currentphase = repo[nhex].phase()
959 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
967 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
960 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
968 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
961 if currentphase == oldphase and newphase < oldphase:
969 if currentphase == oldphase and newphase < oldphase:
962 with repo.transaction(b'pushkey-phase') as tr:
970 with repo.transaction(b'pushkey-phase') as tr:
963 advanceboundary(repo, tr, newphase, [bin(nhex)])
971 advanceboundary(repo, tr, newphase, [bin(nhex)])
964 return True
972 return True
965 elif currentphase == newphase:
973 elif currentphase == newphase:
966 # raced, but got correct result
974 # raced, but got correct result
967 return True
975 return True
968 else:
976 else:
969 return False
977 return False
970
978
971
979
972 def subsetphaseheads(repo, subset):
980 def subsetphaseheads(repo, subset):
973 """Finds the phase heads for a subset of a history
981 """Finds the phase heads for a subset of a history
974
982
975 Returns a list indexed by phase number where each item is a list of phase
983 Returns a list indexed by phase number where each item is a list of phase
976 head nodes.
984 head nodes.
977 """
985 """
978 cl = repo.changelog
986 cl = repo.changelog
979
987
980 headsbyphase = {i: [] for i in allphases}
988 headsbyphase = {i: [] for i in allphases}
981 for phase in allphases:
989 for phase in allphases:
982 revset = b"heads(%%ln & _phase(%d))" % phase
990 revset = b"heads(%%ln & _phase(%d))" % phase
983 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
991 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
984 return headsbyphase
992 return headsbyphase
985
993
986
994
987 def updatephases(repo, trgetter, headsbyphase):
995 def updatephases(repo, trgetter, headsbyphase):
988 """Updates the repo with the given phase heads"""
996 """Updates the repo with the given phase heads"""
989 # Now advance phase boundaries of all phases
997 # Now advance phase boundaries of all phases
990 #
998 #
991 # run the update (and fetch transaction) only if there are actually things
999 # run the update (and fetch transaction) only if there are actually things
992 # to update. This avoid creating empty transaction during no-op operation.
1000 # to update. This avoid creating empty transaction during no-op operation.
993
1001
994 for phase in allphases:
1002 for phase in allphases:
995 revset = b'%ln - _phase(%s)'
1003 revset = b'%ln - _phase(%s)'
996 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1004 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
997 if heads:
1005 if heads:
998 advanceboundary(repo, trgetter(), phase, heads)
1006 advanceboundary(repo, trgetter(), phase, heads)
999
1007
1000
1008
1001 def analyzeremotephases(repo, subset, roots):
1009 def analyzeremotephases(repo, subset, roots):
1002 """Compute phases heads and root in a subset of node from root dict
1010 """Compute phases heads and root in a subset of node from root dict
1003
1011
1004 * subset is heads of the subset
1012 * subset is heads of the subset
1005 * roots is {<nodeid> => phase} mapping. key and value are string.
1013 * roots is {<nodeid> => phase} mapping. key and value are string.
1006
1014
1007 Accept unknown element input
1015 Accept unknown element input
1008 """
1016 """
1009 repo = repo.unfiltered()
1017 repo = repo.unfiltered()
1010 # build list from dictionary
1018 # build list from dictionary
1011 draftroots = []
1019 draftroots = []
1012 has_node = repo.changelog.index.has_node # to filter unknown nodes
1020 has_node = repo.changelog.index.has_node # to filter unknown nodes
1013 for nhex, phase in roots.items():
1021 for nhex, phase in roots.items():
1014 if nhex == b'publishing': # ignore data related to publish option
1022 if nhex == b'publishing': # ignore data related to publish option
1015 continue
1023 continue
1016 node = bin(nhex)
1024 node = bin(nhex)
1017 phase = int(phase)
1025 phase = int(phase)
1018 if phase == public:
1026 if phase == public:
1019 if node != repo.nullid:
1027 if node != repo.nullid:
1020 repo.ui.warn(
1028 repo.ui.warn(
1021 _(
1029 _(
1022 b'ignoring inconsistent public root'
1030 b'ignoring inconsistent public root'
1023 b' from remote: %s\n'
1031 b' from remote: %s\n'
1024 )
1032 )
1025 % nhex
1033 % nhex
1026 )
1034 )
1027 elif phase == draft:
1035 elif phase == draft:
1028 if has_node(node):
1036 if has_node(node):
1029 draftroots.append(node)
1037 draftroots.append(node)
1030 else:
1038 else:
1031 repo.ui.warn(
1039 repo.ui.warn(
1032 _(b'ignoring unexpected root from remote: %i %s\n')
1040 _(b'ignoring unexpected root from remote: %i %s\n')
1033 % (phase, nhex)
1041 % (phase, nhex)
1034 )
1042 )
1035 # compute heads
1043 # compute heads
1036 publicheads = newheads(repo, subset, draftroots)
1044 publicheads = newheads(repo, subset, draftroots)
1037 return publicheads, draftroots
1045 return publicheads, draftroots
1038
1046
1039
1047
1040 class remotephasessummary:
1048 class remotephasessummary:
1041 """summarize phase information on the remote side
1049 """summarize phase information on the remote side
1042
1050
1043 :publishing: True is the remote is publishing
1051 :publishing: True is the remote is publishing
1044 :publicheads: list of remote public phase heads (nodes)
1052 :publicheads: list of remote public phase heads (nodes)
1045 :draftheads: list of remote draft phase heads (nodes)
1053 :draftheads: list of remote draft phase heads (nodes)
1046 :draftroots: list of remote draft phase root (nodes)
1054 :draftroots: list of remote draft phase root (nodes)
1047 """
1055 """
1048
1056
1049 def __init__(self, repo, remotesubset, remoteroots):
1057 def __init__(self, repo, remotesubset, remoteroots):
1050 unfi = repo.unfiltered()
1058 unfi = repo.unfiltered()
1051 self._allremoteroots = remoteroots
1059 self._allremoteroots = remoteroots
1052
1060
1053 self.publishing = remoteroots.get(b'publishing', False)
1061 self.publishing = remoteroots.get(b'publishing', False)
1054
1062
1055 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1063 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1056 self.publicheads, self.draftroots = ana
1064 self.publicheads, self.draftroots = ana
1057 # Get the list of all "heads" revs draft on remote
1065 # Get the list of all "heads" revs draft on remote
1058 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1066 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1059 self.draftheads = [c.node() for c in dheads]
1067 self.draftheads = [c.node() for c in dheads]
1060
1068
1061
1069
1062 def newheads(repo, heads, roots):
1070 def newheads(repo, heads, roots):
1063 """compute new head of a subset minus another
1071 """compute new head of a subset minus another
1064
1072
1065 * `heads`: define the first subset
1073 * `heads`: define the first subset
1066 * `roots`: define the second we subtract from the first"""
1074 * `roots`: define the second we subtract from the first"""
1067 # prevent an import cycle
1075 # prevent an import cycle
1068 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1076 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1069 from . import dagop
1077 from . import dagop
1070
1078
1071 repo = repo.unfiltered()
1079 repo = repo.unfiltered()
1072 cl = repo.changelog
1080 cl = repo.changelog
1073 rev = cl.index.get_rev
1081 rev = cl.index.get_rev
1074 if not roots:
1082 if not roots:
1075 return heads
1083 return heads
1076 if not heads or heads == [repo.nullid]:
1084 if not heads or heads == [repo.nullid]:
1077 return []
1085 return []
1078 # The logic operated on revisions, convert arguments early for convenience
1086 # The logic operated on revisions, convert arguments early for convenience
1079 new_heads = {rev(n) for n in heads if n != repo.nullid}
1087 new_heads = {rev(n) for n in heads if n != repo.nullid}
1080 roots = [rev(n) for n in roots]
1088 roots = [rev(n) for n in roots]
1081 # compute the area we need to remove
1089 # compute the area we need to remove
1082 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1090 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1083 # heads in the area are no longer heads
1091 # heads in the area are no longer heads
1084 new_heads.difference_update(affected_zone)
1092 new_heads.difference_update(affected_zone)
1085 # revisions in the area have children outside of it,
1093 # revisions in the area have children outside of it,
1086 # They might be new heads
1094 # They might be new heads
1087 candidates = repo.revs(
1095 candidates = repo.revs(
1088 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1096 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1089 )
1097 )
1090 candidates -= affected_zone
1098 candidates -= affected_zone
1091 if new_heads or candidates:
1099 if new_heads or candidates:
1092 # remove candidate that are ancestors of other heads
1100 # remove candidate that are ancestors of other heads
1093 new_heads.update(candidates)
1101 new_heads.update(candidates)
1094 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1102 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1095 pruned = dagop.reachableroots(repo, candidates, prunestart)
1103 pruned = dagop.reachableroots(repo, candidates, prunestart)
1096 new_heads.difference_update(pruned)
1104 new_heads.difference_update(pruned)
1097
1105
1098 return pycompat.maplist(cl.node, sorted(new_heads))
1106 return pycompat.maplist(cl.node, sorted(new_heads))
1099
1107
1100
1108
1101 def newcommitphase(ui: "uimod.ui") -> int:
1109 def newcommitphase(ui: "uimod.ui") -> int:
1102 """helper to get the target phase of new commit
1110 """helper to get the target phase of new commit
1103
1111
1104 Handle all possible values for the phases.new-commit options.
1112 Handle all possible values for the phases.new-commit options.
1105
1113
1106 """
1114 """
1107 v = ui.config(b'phases', b'new-commit')
1115 v = ui.config(b'phases', b'new-commit')
1108 try:
1116 try:
1109 return phasenumber2[v]
1117 return phasenumber2[v]
1110 except KeyError:
1118 except KeyError:
1111 raise error.ConfigError(
1119 raise error.ConfigError(
1112 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1120 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1113 )
1121 )
1114
1122
1115
1123
1116 def hassecret(repo: "localrepo.localrepository") -> bool:
1124 def hassecret(repo: "localrepo.localrepository") -> bool:
1117 """utility function that check if a repo have any secret changeset."""
1125 """utility function that check if a repo have any secret changeset."""
1118 return bool(repo._phasecache._phaseroots[secret])
1126 return bool(repo._phasecache._phaseroots[secret])
1119
1127
1120
1128
1121 def preparehookargs(
1129 def preparehookargs(
1122 node: bytes,
1130 node: bytes,
1123 old: Optional[int],
1131 old: Optional[int],
1124 new: Optional[int],
1132 new: Optional[int],
1125 ) -> Dict[bytes, bytes]:
1133 ) -> Dict[bytes, bytes]:
1126 if old is None:
1134 if old is None:
1127 old = b''
1135 old = b''
1128 else:
1136 else:
1129 old = phasenames[old]
1137 old = phasenames[old]
1130 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
1138 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