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