##// END OF EJS Templates
phases: avoid a potentially costly dictionary interation in some case...
marmoute -
r52409:e0f92bd9 stable
parent child Browse files
Show More
@@ -1,1223 +1,1224 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 Dict,
112 Dict,
113 Iterable,
113 Iterable,
114 List,
114 List,
115 Optional,
115 Optional,
116 Set,
116 Set,
117 Tuple,
117 Tuple,
118 )
118 )
119
119
120 from .i18n import _
120 from .i18n import _
121 from .node import (
121 from .node import (
122 bin,
122 bin,
123 hex,
123 hex,
124 nullrev,
124 nullrev,
125 short,
125 short,
126 wdirrev,
126 wdirrev,
127 )
127 )
128 from . import (
128 from . import (
129 error,
129 error,
130 pycompat,
130 pycompat,
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: 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 getrevset(
417 def getrevset(
418 self,
418 self,
419 repo: "localrepo.localrepository",
419 repo: "localrepo.localrepository",
420 phases: Iterable[int],
420 phases: Iterable[int],
421 subset: Optional[Any] = None,
421 subset: Optional[Any] = None,
422 ) -> Any:
422 ) -> Any:
423 # TODO: finish typing this
423 # TODO: finish typing this
424 """return a smartset for the given phases"""
424 """return a smartset for the given phases"""
425 self._ensure_phase_sets(repo.unfiltered())
425 self._ensure_phase_sets(repo.unfiltered())
426 phases = set(phases)
426 phases = set(phases)
427 publicphase = public in phases
427 publicphase = public in phases
428
428
429 if publicphase:
429 if publicphase:
430 # In this case, phases keeps all the *other* phases.
430 # In this case, phases keeps all the *other* phases.
431 phases = set(allphases).difference(phases)
431 phases = set(allphases).difference(phases)
432 if not phases:
432 if not phases:
433 return smartset.fullreposet(repo)
433 return smartset.fullreposet(repo)
434
434
435 # fast path: _phasesets contains the interesting sets,
435 # fast path: _phasesets contains the interesting sets,
436 # might only need a union and post-filtering.
436 # might only need a union and post-filtering.
437 revsneedscopy = False
437 revsneedscopy = False
438 if len(phases) == 1:
438 if len(phases) == 1:
439 [p] = phases
439 [p] = phases
440 revs = self._phasesets[p]
440 revs = self._phasesets[p]
441 revsneedscopy = True # Don't modify _phasesets
441 revsneedscopy = True # Don't modify _phasesets
442 else:
442 else:
443 # revs has the revisions in all *other* phases.
443 # revs has the revisions in all *other* phases.
444 revs = set.union(*[self._phasesets[p] for p in phases])
444 revs = set.union(*[self._phasesets[p] for p in phases])
445
445
446 def _addwdir(wdirsubset, wdirrevs):
446 def _addwdir(wdirsubset, wdirrevs):
447 if wdirrev in wdirsubset and repo[None].phase() in phases:
447 if wdirrev in wdirsubset and repo[None].phase() in phases:
448 if revsneedscopy:
448 if revsneedscopy:
449 wdirrevs = wdirrevs.copy()
449 wdirrevs = wdirrevs.copy()
450 # The working dir would never be in the # cache, but it was in
450 # The working dir would never be in the # cache, but it was in
451 # the subset being filtered for its phase (or filtered out,
451 # the subset being filtered for its phase (or filtered out,
452 # depending on publicphase), so add it to the output to be
452 # depending on publicphase), so add it to the output to be
453 # included (or filtered out).
453 # included (or filtered out).
454 wdirrevs.add(wdirrev)
454 wdirrevs.add(wdirrev)
455 return wdirrevs
455 return wdirrevs
456
456
457 if not publicphase:
457 if not publicphase:
458 if repo.changelog.filteredrevs:
458 if repo.changelog.filteredrevs:
459 revs = revs - repo.changelog.filteredrevs
459 revs = revs - repo.changelog.filteredrevs
460
460
461 if subset is None:
461 if subset is None:
462 return smartset.baseset(revs)
462 return smartset.baseset(revs)
463 else:
463 else:
464 revs = _addwdir(subset, revs)
464 revs = _addwdir(subset, revs)
465 return subset & smartset.baseset(revs)
465 return subset & smartset.baseset(revs)
466 else:
466 else:
467 if subset is None:
467 if subset is None:
468 subset = smartset.fullreposet(repo)
468 subset = smartset.fullreposet(repo)
469
469
470 revs = _addwdir(subset, revs)
470 revs = _addwdir(subset, revs)
471
471
472 if not revs:
472 if not revs:
473 return subset
473 return subset
474 return subset.filter(lambda r: r not in revs)
474 return subset.filter(lambda r: r not in revs)
475
475
476 def copy(self):
476 def copy(self):
477 # Shallow copy meant to ensure isolation in
477 # Shallow copy meant to ensure isolation in
478 # advance/retractboundary(), nothing more.
478 # advance/retractboundary(), nothing more.
479 ph = self.__class__(None, None, _load=False)
479 ph = self.__class__(None, None, _load=False)
480 ph._phaseroots = self._phaseroots.copy()
480 ph._phaseroots = self._phaseroots.copy()
481 ph.dirty = self.dirty
481 ph.dirty = self.dirty
482 ph._loadedrevslen = self._loadedrevslen
482 ph._loadedrevslen = self._loadedrevslen
483 if self._phasesets is None:
483 if self._phasesets is None:
484 ph._phasesets = None
484 ph._phasesets = None
485 else:
485 else:
486 ph._phasesets = self._phasesets.copy()
486 ph._phasesets = self._phasesets.copy()
487 return ph
487 return ph
488
488
489 def replace(self, phcache):
489 def replace(self, phcache):
490 """replace all values in 'self' with content of phcache"""
490 """replace all values in 'self' with content of phcache"""
491 for a in (
491 for a in (
492 '_phaseroots',
492 '_phaseroots',
493 'dirty',
493 'dirty',
494 '_loadedrevslen',
494 '_loadedrevslen',
495 '_phasesets',
495 '_phasesets',
496 ):
496 ):
497 setattr(self, a, getattr(phcache, a))
497 setattr(self, a, getattr(phcache, a))
498
498
499 def _getphaserevsnative(self, repo):
499 def _getphaserevsnative(self, repo):
500 repo = repo.unfiltered()
500 repo = repo.unfiltered()
501 return repo.changelog.computephases(self._phaseroots)
501 return repo.changelog.computephases(self._phaseroots)
502
502
503 def _computephaserevspure(self, repo):
503 def _computephaserevspure(self, repo):
504 repo = repo.unfiltered()
504 repo = repo.unfiltered()
505 cl = repo.changelog
505 cl = repo.changelog
506 self._phasesets = {phase: set() for phase in allphases}
506 self._phasesets = {phase: set() for phase in allphases}
507 lowerroots = set()
507 lowerroots = set()
508 for phase in reversed(trackedphases):
508 for phase in reversed(trackedphases):
509 roots = self._phaseroots[phase]
509 roots = self._phaseroots[phase]
510 if roots:
510 if roots:
511 ps = set(cl.descendants(roots))
511 ps = set(cl.descendants(roots))
512 for root in roots:
512 for root in roots:
513 ps.add(root)
513 ps.add(root)
514 ps.difference_update(lowerroots)
514 ps.difference_update(lowerroots)
515 lowerroots.update(ps)
515 lowerroots.update(ps)
516 self._phasesets[phase] = ps
516 self._phasesets[phase] = ps
517 self._loadedrevslen = len(cl)
517 self._loadedrevslen = len(cl)
518
518
519 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
519 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
520 """ensure phase information is loaded in the object"""
520 """ensure phase information is loaded in the object"""
521 assert repo.filtername is None
521 assert repo.filtername is None
522 update = -1
522 update = -1
523 cl = repo.changelog
523 cl = repo.changelog
524 cl_size = len(cl)
524 cl_size = len(cl)
525 if self._phasesets is None:
525 if self._phasesets is None:
526 update = 0
526 update = 0
527 else:
527 else:
528 if cl_size > self._loadedrevslen:
528 if cl_size > self._loadedrevslen:
529 # check if an incremental update is worth it.
529 # check if an incremental update is worth it.
530 # note we need a tradeoff here because the whole logic is not
530 # note we need a tradeoff here because the whole logic is not
531 # stored and implemented in native code nd datastructure.
531 # stored and implemented in native code nd datastructure.
532 # Otherwise the incremental update woul always be a win.
532 # Otherwise the incremental update woul always be a win.
533 missing = cl_size - self._loadedrevslen
533 missing = cl_size - self._loadedrevslen
534 if missing <= INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE:
534 if missing <= INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE:
535 update = self._loadedrevslen
535 update = self._loadedrevslen
536 else:
536 else:
537 update = 0
537 update = 0
538
538
539 if update == 0:
539 if update == 0:
540 try:
540 try:
541 res = self._getphaserevsnative(repo)
541 res = self._getphaserevsnative(repo)
542 self._loadedrevslen, self._phasesets = res
542 self._loadedrevslen, self._phasesets = res
543 except AttributeError:
543 except AttributeError:
544 self._computephaserevspure(repo)
544 self._computephaserevspure(repo)
545 assert self._loadedrevslen == len(repo.changelog)
545 assert self._loadedrevslen == len(repo.changelog)
546 elif update > 0:
546 elif update > 0:
547 # good candidate for native code
547 # good candidate for native code
548 assert update == self._loadedrevslen
548 assert update == self._loadedrevslen
549 if self.hasnonpublicphases(repo):
549 if self.hasnonpublicphases(repo):
550 start = self._loadedrevslen
550 start = self._loadedrevslen
551 get_phase = self.phase
551 get_phase = self.phase
552 rev_phases = [0] * missing
552 rev_phases = [0] * missing
553 parents = cl.parentrevs
553 parents = cl.parentrevs
554 sets = {phase: set() for phase in self._phasesets}
554 sets = {phase: set() for phase in self._phasesets}
555 for phase, roots in self._phaseroots.items():
555 for phase, roots in self._phaseroots.items():
556 # XXX should really store the max somewhere
556 # XXX should really store the max somewhere
557 for r in roots:
557 for r in roots:
558 if r >= start:
558 if r >= start:
559 rev_phases[r - start] = phase
559 rev_phases[r - start] = phase
560 for rev in range(start, cl_size):
560 for rev in range(start, cl_size):
561 phase = rev_phases[rev - start]
561 phase = rev_phases[rev - start]
562 p1, p2 = parents(rev)
562 p1, p2 = parents(rev)
563 if p1 == nullrev:
563 if p1 == nullrev:
564 p1_phase = public
564 p1_phase = public
565 elif p1 >= start:
565 elif p1 >= start:
566 p1_phase = rev_phases[p1 - start]
566 p1_phase = rev_phases[p1 - start]
567 else:
567 else:
568 p1_phase = max(phase, get_phase(repo, p1))
568 p1_phase = max(phase, get_phase(repo, p1))
569 if p2 == nullrev:
569 if p2 == nullrev:
570 p2_phase = public
570 p2_phase = public
571 elif p2 >= start:
571 elif p2 >= start:
572 p2_phase = rev_phases[p2 - start]
572 p2_phase = rev_phases[p2 - start]
573 else:
573 else:
574 p2_phase = max(phase, get_phase(repo, p2))
574 p2_phase = max(phase, get_phase(repo, p2))
575 phase = max(phase, p1_phase, p2_phase)
575 phase = max(phase, p1_phase, p2_phase)
576 if phase > public:
576 if phase > public:
577 rev_phases[rev - start] = phase
577 rev_phases[rev - start] = phase
578 sets[phase].add(rev)
578 sets[phase].add(rev)
579
579
580 # Be careful to preserve shallow-copied values: do not update
580 # Be careful to preserve shallow-copied values: do not update
581 # phaseroots values, replace them.
581 # phaseroots values, replace them.
582 for phase, extra in sets.items():
582 for phase, extra in sets.items():
583 if extra:
583 if extra:
584 self._phasesets[phase] = self._phasesets[phase] | extra
584 self._phasesets[phase] = self._phasesets[phase] | extra
585 self._loadedrevslen = cl_size
585 self._loadedrevslen = cl_size
586
586
587 def invalidate(self):
587 def invalidate(self):
588 self._loadedrevslen = 0
588 self._loadedrevslen = 0
589 self._phasesets = None
589 self._phasesets = None
590
590
591 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
591 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
592 # We need a repo argument here to be able to build _phasesets
592 # We need a repo argument here to be able to build _phasesets
593 # if necessary. The repository instance is not stored in
593 # if necessary. The repository instance is not stored in
594 # phasecache to avoid reference cycles. The changelog instance
594 # phasecache to avoid reference cycles. The changelog instance
595 # is not stored because it is a filecache() property and can
595 # is not stored because it is a filecache() property and can
596 # be replaced without us being notified.
596 # be replaced without us being notified.
597 if rev == nullrev:
597 if rev == nullrev:
598 return public
598 return public
599 if rev < nullrev:
599 if rev < nullrev:
600 raise ValueError(_(b'cannot lookup negative revision'))
600 raise ValueError(_(b'cannot lookup negative revision'))
601 # double check self._loadedrevslen to avoid an extra method call as
601 # double check self._loadedrevslen to avoid an extra method call as
602 # python is slow for that.
602 # python is slow for that.
603 if rev >= self._loadedrevslen:
603 if rev >= self._loadedrevslen:
604 self._ensure_phase_sets(repo.unfiltered())
604 self._ensure_phase_sets(repo.unfiltered())
605 for phase in trackedphases:
605 for phase in trackedphases:
606 if rev in self._phasesets[phase]:
606 if rev in self._phasesets[phase]:
607 return phase
607 return phase
608 return public
608 return public
609
609
610 def write(self, repo):
610 def write(self, repo):
611 if not self.dirty:
611 if not self.dirty:
612 return
612 return
613 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
613 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
614 try:
614 try:
615 self._write(repo.unfiltered(), f)
615 self._write(repo.unfiltered(), f)
616 finally:
616 finally:
617 f.close()
617 f.close()
618
618
619 def _write(self, repo, fp):
619 def _write(self, repo, fp):
620 assert repo.filtername is None
620 assert repo.filtername is None
621 to_node = repo.changelog.node
621 to_node = repo.changelog.node
622 for phase, roots in self._phaseroots.items():
622 for phase, roots in self._phaseroots.items():
623 for r in sorted(roots):
623 for r in sorted(roots):
624 h = to_node(r)
624 h = to_node(r)
625 fp.write(b'%i %s\n' % (phase, hex(h)))
625 fp.write(b'%i %s\n' % (phase, hex(h)))
626 self.dirty = False
626 self.dirty = False
627
627
628 def _updateroots(self, repo, phase, newroots, tr, invalidate=True):
628 def _updateroots(self, repo, phase, newroots, tr, invalidate=True):
629 self._phaseroots[phase] = newroots
629 self._phaseroots[phase] = newroots
630 self.dirty = True
630 self.dirty = True
631 if invalidate:
631 if invalidate:
632 self.invalidate()
632 self.invalidate()
633
633
634 assert repo.filtername is None
634 assert repo.filtername is None
635 wrepo = weakref.ref(repo)
635 wrepo = weakref.ref(repo)
636
636
637 def tr_write(fp):
637 def tr_write(fp):
638 repo = wrepo()
638 repo = wrepo()
639 assert repo is not None
639 assert repo is not None
640 self._write(repo, fp)
640 self._write(repo, fp)
641
641
642 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
642 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
643 tr.hookargs[b'phases_moved'] = b'1'
643 tr.hookargs[b'phases_moved'] = b'1'
644
644
645 def registernew(self, repo, tr, targetphase, revs):
645 def registernew(self, repo, tr, targetphase, revs):
646 repo = repo.unfiltered()
646 repo = repo.unfiltered()
647 self._retractboundary(repo, tr, targetphase, [], revs=revs)
647 self._retractboundary(repo, tr, targetphase, [], revs=revs)
648 if tr is not None and b'phases' in tr.changes:
648 if tr is not None and b'phases' in tr.changes:
649 phasetracking = tr.changes[b'phases']
649 phasetracking = tr.changes[b'phases']
650 phase = self.phase
650 phase = self.phase
651 for rev in sorted(revs):
651 for rev in sorted(revs):
652 revphase = phase(repo, rev)
652 revphase = phase(repo, rev)
653 _trackphasechange(phasetracking, rev, None, revphase)
653 _trackphasechange(phasetracking, rev, None, revphase)
654 repo.invalidatevolatilesets()
654 repo.invalidatevolatilesets()
655
655
656 def advanceboundary(
656 def advanceboundary(
657 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
657 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
658 ):
658 ):
659 """Set all 'nodes' to phase 'targetphase'
659 """Set all 'nodes' to phase 'targetphase'
660
660
661 Nodes with a phase lower than 'targetphase' are not affected.
661 Nodes with a phase lower than 'targetphase' are not affected.
662
662
663 If dryrun is True, no actions will be performed
663 If dryrun is True, no actions will be performed
664
664
665 Returns a set of revs whose phase is changed or should be changed
665 Returns a set of revs whose phase is changed or should be changed
666 """
666 """
667 if targetphase == public and not self.hasnonpublicphases(repo):
667 if targetphase == public and not self.hasnonpublicphases(repo):
668 return set()
668 return set()
669 repo = repo.unfiltered()
669 repo = repo.unfiltered()
670 cl = repo.changelog
670 cl = repo.changelog
671 torev = cl.index.rev
671 torev = cl.index.rev
672 # Be careful to preserve shallow-copied values: do not update
672 # Be careful to preserve shallow-copied values: do not update
673 # phaseroots values, replace them.
673 # phaseroots values, replace them.
674 new_revs = set()
674 new_revs = set()
675 if revs is not None:
675 if revs is not None:
676 new_revs.update(revs)
676 new_revs.update(revs)
677 if nodes is not None:
677 if nodes is not None:
678 new_revs.update(torev(node) for node in nodes)
678 new_revs.update(torev(node) for node in nodes)
679 if not new_revs: # bail out early to avoid the loadphaserevs call
679 if not new_revs: # bail out early to avoid the loadphaserevs call
680 return (
680 return (
681 set()
681 set()
682 ) # note: why do people call advanceboundary with nothing?
682 ) # note: why do people call advanceboundary with nothing?
683
683
684 if tr is None:
684 if tr is None:
685 phasetracking = None
685 phasetracking = None
686 else:
686 else:
687 phasetracking = tr.changes.get(b'phases')
687 phasetracking = tr.changes.get(b'phases')
688
688
689 affectable_phases = sorted(
689 affectable_phases = sorted(
690 p for p in allphases if p > targetphase and self._phaseroots[p]
690 p for p in allphases if p > targetphase and self._phaseroots[p]
691 )
691 )
692 # filter revision already in the right phases
692 # filter revision already in the right phases
693 candidates = new_revs
693 candidates = new_revs
694 new_revs = set()
694 new_revs = set()
695 self._ensure_phase_sets(repo)
695 self._ensure_phase_sets(repo)
696 for phase in affectable_phases:
696 for phase in affectable_phases:
697 found = candidates & self._phasesets[phase]
697 found = candidates & self._phasesets[phase]
698 new_revs |= found
698 new_revs |= found
699 candidates -= found
699 candidates -= found
700 if not candidates:
700 if not candidates:
701 break
701 break
702 if not new_revs:
702 if not new_revs:
703 return set()
703 return set()
704
704
705 # search for affected high phase changesets and roots
705 # search for affected high phase changesets and roots
706 seen = set(new_revs)
706 seen = set(new_revs)
707 push = heapq.heappush
707 push = heapq.heappush
708 pop = heapq.heappop
708 pop = heapq.heappop
709 parents = cl.parentrevs
709 parents = cl.parentrevs
710 get_phase = self.phase
710 get_phase = self.phase
711 changed = {} # set of revisions to be changed
711 changed = {} # set of revisions to be changed
712 # set of root deleted by this path
712 # set of root deleted by this path
713 delroots = set()
713 delroots = set()
714 new_roots = {p: set() for p in affectable_phases}
714 new_roots = {p: set() for p in affectable_phases}
715 new_target_roots = set()
715 new_target_roots = set()
716 # revision to walk down
716 # revision to walk down
717 revs = [-r for r in new_revs]
717 revs = [-r for r in new_revs]
718 heapq.heapify(revs)
718 heapq.heapify(revs)
719 while revs:
719 while revs:
720 current = -pop(revs)
720 current = -pop(revs)
721 current_phase = get_phase(repo, current)
721 current_phase = get_phase(repo, current)
722 changed[current] = current_phase
722 changed[current] = current_phase
723 p1, p2 = parents(current)
723 p1, p2 = parents(current)
724 if p1 == nullrev:
724 if p1 == nullrev:
725 p1_phase = public
725 p1_phase = public
726 else:
726 else:
727 p1_phase = get_phase(repo, p1)
727 p1_phase = get_phase(repo, p1)
728 if p2 == nullrev:
728 if p2 == nullrev:
729 p2_phase = public
729 p2_phase = public
730 else:
730 else:
731 p2_phase = get_phase(repo, p2)
731 p2_phase = get_phase(repo, p2)
732 # do we have a root ?
732 # do we have a root ?
733 if current_phase != p1_phase and current_phase != p2_phase:
733 if current_phase != p1_phase and current_phase != p2_phase:
734 # do not record phase, because we could have "duplicated"
734 # do not record phase, because we could have "duplicated"
735 # roots, were one root is shadowed by the very same roots of an
735 # roots, were one root is shadowed by the very same roots of an
736 # higher phases
736 # higher phases
737 delroots.add(current)
737 delroots.add(current)
738 # schedule a walk down if needed
738 # schedule a walk down if needed
739 if p1_phase > targetphase and p1 not in seen:
739 if p1_phase > targetphase and p1 not in seen:
740 seen.add(p1)
740 seen.add(p1)
741 push(revs, -p1)
741 push(revs, -p1)
742 if p2_phase > targetphase and p2 not in seen:
742 if p2_phase > targetphase and p2 not in seen:
743 seen.add(p2)
743 seen.add(p2)
744 push(revs, -p2)
744 push(revs, -p2)
745 if p1_phase < targetphase and p2_phase < targetphase:
745 if p1_phase < targetphase and p2_phase < targetphase:
746 new_target_roots.add(current)
746 new_target_roots.add(current)
747
747
748 # the last iteration was done with the smallest value
748 # the last iteration was done with the smallest value
749 min_current = current
749 min_current = current
750 # do we have unwalked children that might be new roots
750 # do we have unwalked children that might be new roots
751 if (min_current + len(changed)) < len(cl):
751 if (min_current + len(changed)) < len(cl):
752 for r in range(min_current, len(cl)):
752 for r in range(min_current, len(cl)):
753 if r in changed:
753 if r in changed:
754 continue
754 continue
755 phase = get_phase(repo, r)
755 phase = get_phase(repo, r)
756 if phase <= targetphase:
756 if phase <= targetphase:
757 continue
757 continue
758 p1, p2 = parents(r)
758 p1, p2 = parents(r)
759 if not (p1 in changed or p2 in changed):
759 if not (p1 in changed or p2 in changed):
760 continue # not affected
760 continue # not affected
761 if p1 != nullrev and p1 not in changed:
761 if p1 != nullrev and p1 not in changed:
762 p1_phase = get_phase(repo, p1)
762 p1_phase = get_phase(repo, p1)
763 if p1_phase == phase:
763 if p1_phase == phase:
764 continue # not a root
764 continue # not a root
765 if p2 != nullrev and p2 not in changed:
765 if p2 != nullrev and p2 not in changed:
766 p2_phase = get_phase(repo, p2)
766 p2_phase = get_phase(repo, p2)
767 if p2_phase == phase:
767 if p2_phase == phase:
768 continue # not a root
768 continue # not a root
769 new_roots[phase].add(r)
769 new_roots[phase].add(r)
770
770
771 # apply the changes
771 # apply the changes
772 if not dryrun:
772 if not dryrun:
773 for r, p in changed.items():
773 for r, p in changed.items():
774 _trackphasechange(phasetracking, r, p, targetphase)
774 _trackphasechange(phasetracking, r, p, targetphase)
775 if targetphase > public:
775 if targetphase > public:
776 self._phasesets[targetphase].update(changed)
776 self._phasesets[targetphase].update(changed)
777 for phase in affectable_phases:
777 for phase in affectable_phases:
778 roots = self._phaseroots[phase]
778 roots = self._phaseroots[phase]
779 removed = roots & delroots
779 removed = roots & delroots
780 if removed or new_roots[phase]:
780 if removed or new_roots[phase]:
781 self._phasesets[phase].difference_update(changed)
781 self._phasesets[phase].difference_update(changed)
782 # Be careful to preserve shallow-copied values: do not
782 # Be careful to preserve shallow-copied values: do not
783 # update phaseroots values, replace them.
783 # update phaseroots values, replace them.
784 final_roots = roots - delroots | new_roots[phase]
784 final_roots = roots - delroots | new_roots[phase]
785 self._updateroots(
785 self._updateroots(
786 repo, phase, final_roots, tr, invalidate=False
786 repo, phase, final_roots, tr, invalidate=False
787 )
787 )
788 if new_target_roots:
788 if new_target_roots:
789 # Thanks for previous filtering, we can't replace existing
789 # Thanks for previous filtering, we can't replace existing
790 # roots
790 # roots
791 new_target_roots |= self._phaseroots[targetphase]
791 new_target_roots |= self._phaseroots[targetphase]
792 self._updateroots(
792 self._updateroots(
793 repo, targetphase, new_target_roots, tr, invalidate=False
793 repo, targetphase, new_target_roots, tr, invalidate=False
794 )
794 )
795 repo.invalidatevolatilesets()
795 repo.invalidatevolatilesets()
796 return changed
796 return changed
797
797
798 def retractboundary(self, repo, tr, targetphase, nodes):
798 def retractboundary(self, repo, tr, targetphase, nodes):
799 if tr is None:
799 if tr is None:
800 phasetracking = None
800 phasetracking = None
801 else:
801 else:
802 phasetracking = tr.changes.get(b'phases')
802 phasetracking = tr.changes.get(b'phases')
803 repo = repo.unfiltered()
803 repo = repo.unfiltered()
804 retracted = self._retractboundary(repo, tr, targetphase, nodes)
804 retracted = self._retractboundary(repo, tr, targetphase, nodes)
805 if retracted and phasetracking is not None:
805 if retracted and phasetracking is not None:
806 for r, old_phase in sorted(retracted.items()):
806 for r, old_phase in sorted(retracted.items()):
807 _trackphasechange(phasetracking, r, old_phase, targetphase)
807 _trackphasechange(phasetracking, r, old_phase, targetphase)
808 repo.invalidatevolatilesets()
808 repo.invalidatevolatilesets()
809
809
810 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
810 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
811 if targetphase == public:
811 if targetphase == public:
812 return {}
812 return {}
813 if (
813 if (
814 targetphase == internal
814 targetphase == internal
815 and not supportinternal(repo)
815 and not supportinternal(repo)
816 or targetphase == archived
816 or targetphase == archived
817 and not supportarchived(repo)
817 and not supportarchived(repo)
818 ):
818 ):
819 name = phasenames[targetphase]
819 name = phasenames[targetphase]
820 msg = b'this repository does not support the %s phase' % name
820 msg = b'this repository does not support the %s phase' % name
821 raise error.ProgrammingError(msg)
821 raise error.ProgrammingError(msg)
822 assert repo.filtername is None
822 assert repo.filtername is None
823 cl = repo.changelog
823 cl = repo.changelog
824 torev = cl.index.rev
824 torev = cl.index.rev
825 new_revs = set()
825 new_revs = set()
826 if revs is not None:
826 if revs is not None:
827 new_revs.update(revs)
827 new_revs.update(revs)
828 if nodes is not None:
828 if nodes is not None:
829 new_revs.update(torev(node) for node in nodes)
829 new_revs.update(torev(node) for node in nodes)
830 if not new_revs: # bail out early to avoid the loadphaserevs call
830 if not new_revs: # bail out early to avoid the loadphaserevs call
831 return {} # note: why do people call retractboundary with nothing ?
831 return {} # note: why do people call retractboundary with nothing ?
832
832
833 if nullrev in new_revs:
833 if nullrev in new_revs:
834 raise error.Abort(_(b'cannot change null revision phase'))
834 raise error.Abort(_(b'cannot change null revision phase'))
835
835
836 # Filter revision that are already in the right phase
836 # Filter revision that are already in the right phase
837 self._ensure_phase_sets(repo)
837 self._ensure_phase_sets(repo)
838 for phase, revs in self._phasesets.items():
838 for phase, revs in self._phasesets.items():
839 if phase >= targetphase:
839 if phase >= targetphase:
840 new_revs -= revs
840 new_revs -= revs
841 if not new_revs: # all revisions already in the right phases
841 if not new_revs: # all revisions already in the right phases
842 return {}
842 return {}
843
843
844 # Compute change in phase roots by walking the graph
844 # Compute change in phase roots by walking the graph
845 #
845 #
846 # note: If we had a cheap parent → children mapping we could do
846 # note: If we had a cheap parent → children mapping we could do
847 # something even cheaper/more-bounded
847 # something even cheaper/more-bounded
848 #
848 #
849 # The idea would be to walk from item in new_revs stopping at
849 # The idea would be to walk from item in new_revs stopping at
850 # descendant with phases >= target_phase.
850 # descendant with phases >= target_phase.
851 #
851 #
852 # 1) This detect new_revs that are not new_roots (either already >=
852 # 1) This detect new_revs that are not new_roots (either already >=
853 # target_phase or reachable though another new_revs
853 # target_phase or reachable though another new_revs
854 # 2) This detect replaced current_roots as we reach them
854 # 2) This detect replaced current_roots as we reach them
855 # 3) This can avoid walking to the tip if we retract over a small
855 # 3) This can avoid walking to the tip if we retract over a small
856 # branch.
856 # branch.
857 #
857 #
858 # So instead, we do a variation of this, we walk from the smaller new
858 # So instead, we do a variation of this, we walk from the smaller new
859 # revision to the tip to avoid missing any potential children.
859 # revision to the tip to avoid missing any potential children.
860 #
860 #
861 # The following code would be a good candidate for native code… if only
861 # The following code would be a good candidate for native code… if only
862 # we could knew the phase of a changeset efficiently in native code.
862 # we could knew the phase of a changeset efficiently in native code.
863 parents = cl.parentrevs
863 parents = cl.parentrevs
864 phase = self.phase
864 phase = self.phase
865 new_roots = set() # roots added by this phases
865 new_roots = set() # roots added by this phases
866 changed_revs = {} # revision affected by this call
866 changed_revs = {} # revision affected by this call
867 replaced_roots = set() # older roots replaced by this call
867 replaced_roots = set() # older roots replaced by this call
868 currentroots = self._phaseroots[targetphase]
868 currentroots = self._phaseroots[targetphase]
869 start = min(new_revs)
869 start = min(new_revs)
870 end = len(cl)
870 end = len(cl)
871 rev_phases = [None] * (end - start)
871 rev_phases = [None] * (end - start)
872 for r in range(start, end):
872 for r in range(start, end):
873
873
874 # gather information about the current_rev
874 # gather information about the current_rev
875 r_phase = phase(repo, r)
875 r_phase = phase(repo, r)
876 p_phase = None # phase inherited from parents
876 p_phase = None # phase inherited from parents
877 p1, p2 = parents(r)
877 p1, p2 = parents(r)
878 if p1 >= start:
878 if p1 >= start:
879 p1_phase = rev_phases[p1 - start]
879 p1_phase = rev_phases[p1 - start]
880 if p1_phase is not None:
880 if p1_phase is not None:
881 p_phase = p1_phase
881 p_phase = p1_phase
882 if p2 >= start:
882 if p2 >= start:
883 p2_phase = rev_phases[p2 - start]
883 p2_phase = rev_phases[p2 - start]
884 if p2_phase is not None:
884 if p2_phase is not None:
885 if p_phase is not None:
885 if p_phase is not None:
886 p_phase = max(p_phase, p2_phase)
886 p_phase = max(p_phase, p2_phase)
887 else:
887 else:
888 p_phase = p2_phase
888 p_phase = p2_phase
889
889
890 # assess the situation
890 # assess the situation
891 if r in new_revs and r_phase < targetphase:
891 if r in new_revs and r_phase < targetphase:
892 if p_phase is None or p_phase < targetphase:
892 if p_phase is None or p_phase < targetphase:
893 new_roots.add(r)
893 new_roots.add(r)
894 rev_phases[r - start] = targetphase
894 rev_phases[r - start] = targetphase
895 changed_revs[r] = r_phase
895 changed_revs[r] = r_phase
896 elif p_phase is None:
896 elif p_phase is None:
897 rev_phases[r - start] = r_phase
897 rev_phases[r - start] = r_phase
898 else:
898 else:
899 if p_phase > r_phase:
899 if p_phase > r_phase:
900 rev_phases[r - start] = p_phase
900 rev_phases[r - start] = p_phase
901 else:
901 else:
902 rev_phases[r - start] = r_phase
902 rev_phases[r - start] = r_phase
903 if p_phase == targetphase:
903 if p_phase == targetphase:
904 if p_phase > r_phase:
904 if p_phase > r_phase:
905 changed_revs[r] = r_phase
905 changed_revs[r] = r_phase
906 elif r in currentroots:
906 elif r in currentroots:
907 replaced_roots.add(r)
907 replaced_roots.add(r)
908 sets = self._phasesets
908 sets = self._phasesets
909 sets[targetphase].update(changed_revs)
909 sets[targetphase].update(changed_revs)
910 if targetphase > draft:
910 for r, old in changed_revs.items():
911 for r, old in changed_revs.items():
911 if old > public:
912 if old > public:
912 sets[old].discard(r)
913 sets[old].discard(r)
913
914
914 if new_roots:
915 if new_roots:
915 assert changed_revs
916 assert changed_revs
916
917
917 final_roots = new_roots | currentroots - replaced_roots
918 final_roots = new_roots | currentroots - replaced_roots
918 self._updateroots(
919 self._updateroots(
919 repo,
920 repo,
920 targetphase,
921 targetphase,
921 final_roots,
922 final_roots,
922 tr,
923 tr,
923 invalidate=False,
924 invalidate=False,
924 )
925 )
925 if targetphase > 1:
926 if targetphase > 1:
926 retracted = set(changed_revs)
927 retracted = set(changed_revs)
927 for lower_phase in range(1, targetphase):
928 for lower_phase in range(1, targetphase):
928 lower_roots = self._phaseroots.get(lower_phase)
929 lower_roots = self._phaseroots.get(lower_phase)
929 if lower_roots is None:
930 if lower_roots is None:
930 continue
931 continue
931 if lower_roots & retracted:
932 if lower_roots & retracted:
932 simpler_roots = lower_roots - retracted
933 simpler_roots = lower_roots - retracted
933 self._updateroots(
934 self._updateroots(
934 repo,
935 repo,
935 lower_phase,
936 lower_phase,
936 simpler_roots,
937 simpler_roots,
937 tr,
938 tr,
938 invalidate=False,
939 invalidate=False,
939 )
940 )
940 return changed_revs
941 return changed_revs
941 else:
942 else:
942 assert not changed_revs
943 assert not changed_revs
943 assert not replaced_roots
944 assert not replaced_roots
944 return {}
945 return {}
945
946
946 def register_strip(
947 def register_strip(
947 self,
948 self,
948 repo,
949 repo,
949 tr,
950 tr,
950 strip_rev: int,
951 strip_rev: int,
951 ):
952 ):
952 """announce a strip to the phase cache
953 """announce a strip to the phase cache
953
954
954 Any roots higher than the stripped revision should be dropped.
955 Any roots higher than the stripped revision should be dropped.
955 """
956 """
956 for targetphase, roots in list(self._phaseroots.items()):
957 for targetphase, roots in list(self._phaseroots.items()):
957 filtered = {r for r in roots if r >= strip_rev}
958 filtered = {r for r in roots if r >= strip_rev}
958 if filtered:
959 if filtered:
959 self._updateroots(repo, targetphase, roots - filtered, tr)
960 self._updateroots(repo, targetphase, roots - filtered, tr)
960 self.invalidate()
961 self.invalidate()
961
962
962
963
963 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
964 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
964 """Add nodes to a phase changing other nodes phases if necessary.
965 """Add nodes to a phase changing other nodes phases if necessary.
965
966
966 This function move boundary *forward* this means that all nodes
967 This function move boundary *forward* this means that all nodes
967 are set in the target phase or kept in a *lower* phase.
968 are set in the target phase or kept in a *lower* phase.
968
969
969 Simplify boundary to contains phase roots only.
970 Simplify boundary to contains phase roots only.
970
971
971 If dryrun is True, no actions will be performed
972 If dryrun is True, no actions will be performed
972
973
973 Returns a set of revs whose phase is changed or should be changed
974 Returns a set of revs whose phase is changed or should be changed
974 """
975 """
975 if revs is None:
976 if revs is None:
976 revs = []
977 revs = []
977 phcache = repo._phasecache.copy()
978 phcache = repo._phasecache.copy()
978 changes = phcache.advanceboundary(
979 changes = phcache.advanceboundary(
979 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
980 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
980 )
981 )
981 if not dryrun:
982 if not dryrun:
982 repo._phasecache.replace(phcache)
983 repo._phasecache.replace(phcache)
983 return changes
984 return changes
984
985
985
986
986 def retractboundary(repo, tr, targetphase, nodes):
987 def retractboundary(repo, tr, targetphase, nodes):
987 """Set nodes back to a phase changing other nodes phases if
988 """Set nodes back to a phase changing other nodes phases if
988 necessary.
989 necessary.
989
990
990 This function move boundary *backward* this means that all nodes
991 This function move boundary *backward* this means that all nodes
991 are set in the target phase or kept in a *higher* phase.
992 are set in the target phase or kept in a *higher* phase.
992
993
993 Simplify boundary to contains phase roots only."""
994 Simplify boundary to contains phase roots only."""
994 phcache = repo._phasecache.copy()
995 phcache = repo._phasecache.copy()
995 phcache.retractboundary(repo, tr, targetphase, nodes)
996 phcache.retractboundary(repo, tr, targetphase, nodes)
996 repo._phasecache.replace(phcache)
997 repo._phasecache.replace(phcache)
997
998
998
999
999 def registernew(repo, tr, targetphase, revs):
1000 def registernew(repo, tr, targetphase, revs):
1000 """register a new revision and its phase
1001 """register a new revision and its phase
1001
1002
1002 Code adding revisions to the repository should use this function to
1003 Code adding revisions to the repository should use this function to
1003 set new changeset in their target phase (or higher).
1004 set new changeset in their target phase (or higher).
1004 """
1005 """
1005 phcache = repo._phasecache.copy()
1006 phcache = repo._phasecache.copy()
1006 phcache.registernew(repo, tr, targetphase, revs)
1007 phcache.registernew(repo, tr, targetphase, revs)
1007 repo._phasecache.replace(phcache)
1008 repo._phasecache.replace(phcache)
1008
1009
1009
1010
1010 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
1011 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
1011 """List phases root for serialization over pushkey"""
1012 """List phases root for serialization over pushkey"""
1012 # Use ordered dictionary so behavior is deterministic.
1013 # Use ordered dictionary so behavior is deterministic.
1013 keys = util.sortdict()
1014 keys = util.sortdict()
1014 value = b'%i' % draft
1015 value = b'%i' % draft
1015 cl = repo.unfiltered().changelog
1016 cl = repo.unfiltered().changelog
1016 to_node = cl.node
1017 to_node = cl.node
1017 for root in repo._phasecache._phaseroots[draft]:
1018 for root in repo._phasecache._phaseroots[draft]:
1018 if repo._phasecache.phase(repo, root) <= draft:
1019 if repo._phasecache.phase(repo, root) <= draft:
1019 keys[hex(to_node(root))] = value
1020 keys[hex(to_node(root))] = value
1020
1021
1021 if repo.publishing():
1022 if repo.publishing():
1022 # Add an extra data to let remote know we are a publishing
1023 # Add an extra data to let remote know we are a publishing
1023 # repo. Publishing repo can't just pretend they are old repo.
1024 # repo. Publishing repo can't just pretend they are old repo.
1024 # When pushing to a publishing repo, the client still need to
1025 # When pushing to a publishing repo, the client still need to
1025 # push phase boundary
1026 # push phase boundary
1026 #
1027 #
1027 # Push do not only push changeset. It also push phase data.
1028 # Push do not only push changeset. It also push phase data.
1028 # New phase data may apply to common changeset which won't be
1029 # New phase data may apply to common changeset which won't be
1029 # push (as they are common). Here is a very simple example:
1030 # push (as they are common). Here is a very simple example:
1030 #
1031 #
1031 # 1) repo A push changeset X as draft to repo B
1032 # 1) repo A push changeset X as draft to repo B
1032 # 2) repo B make changeset X public
1033 # 2) repo B make changeset X public
1033 # 3) repo B push to repo A. X is not pushed but the data that
1034 # 3) repo B push to repo A. X is not pushed but the data that
1034 # X as now public should
1035 # X as now public should
1035 #
1036 #
1036 # The server can't handle it on it's own as it has no idea of
1037 # The server can't handle it on it's own as it has no idea of
1037 # client phase data.
1038 # client phase data.
1038 keys[b'publishing'] = b'True'
1039 keys[b'publishing'] = b'True'
1039 return keys
1040 return keys
1040
1041
1041
1042
1042 def pushphase(
1043 def pushphase(
1043 repo: "localrepo.localrepository",
1044 repo: "localrepo.localrepository",
1044 nhex: bytes,
1045 nhex: bytes,
1045 oldphasestr: bytes,
1046 oldphasestr: bytes,
1046 newphasestr: bytes,
1047 newphasestr: bytes,
1047 ) -> bool:
1048 ) -> bool:
1048 """List phases root for serialization over pushkey"""
1049 """List phases root for serialization over pushkey"""
1049 repo = repo.unfiltered()
1050 repo = repo.unfiltered()
1050 with repo.lock():
1051 with repo.lock():
1051 currentphase = repo[nhex].phase()
1052 currentphase = repo[nhex].phase()
1052 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
1053 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
1053 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
1054 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
1054 if currentphase == oldphase and newphase < oldphase:
1055 if currentphase == oldphase and newphase < oldphase:
1055 with repo.transaction(b'pushkey-phase') as tr:
1056 with repo.transaction(b'pushkey-phase') as tr:
1056 advanceboundary(repo, tr, newphase, [bin(nhex)])
1057 advanceboundary(repo, tr, newphase, [bin(nhex)])
1057 return True
1058 return True
1058 elif currentphase == newphase:
1059 elif currentphase == newphase:
1059 # raced, but got correct result
1060 # raced, but got correct result
1060 return True
1061 return True
1061 else:
1062 else:
1062 return False
1063 return False
1063
1064
1064
1065
1065 def subsetphaseheads(repo, subset):
1066 def subsetphaseheads(repo, subset):
1066 """Finds the phase heads for a subset of a history
1067 """Finds the phase heads for a subset of a history
1067
1068
1068 Returns a list indexed by phase number where each item is a list of phase
1069 Returns a list indexed by phase number where each item is a list of phase
1069 head nodes.
1070 head nodes.
1070 """
1071 """
1071 cl = repo.changelog
1072 cl = repo.changelog
1072
1073
1073 headsbyphase = {i: [] for i in allphases}
1074 headsbyphase = {i: [] for i in allphases}
1074 for phase in allphases:
1075 for phase in allphases:
1075 revset = b"heads(%%ln & _phase(%d))" % phase
1076 revset = b"heads(%%ln & _phase(%d))" % phase
1076 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
1077 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
1077 return headsbyphase
1078 return headsbyphase
1078
1079
1079
1080
1080 def updatephases(repo, trgetter, headsbyphase):
1081 def updatephases(repo, trgetter, headsbyphase):
1081 """Updates the repo with the given phase heads"""
1082 """Updates the repo with the given phase heads"""
1082 # Now advance phase boundaries of all phases
1083 # Now advance phase boundaries of all phases
1083 #
1084 #
1084 # run the update (and fetch transaction) only if there are actually things
1085 # run the update (and fetch transaction) only if there are actually things
1085 # to update. This avoid creating empty transaction during no-op operation.
1086 # to update. This avoid creating empty transaction during no-op operation.
1086
1087
1087 for phase in allphases:
1088 for phase in allphases:
1088 revset = b'%ln - _phase(%s)'
1089 revset = b'%ln - _phase(%s)'
1089 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1090 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1090 if heads:
1091 if heads:
1091 advanceboundary(repo, trgetter(), phase, heads)
1092 advanceboundary(repo, trgetter(), phase, heads)
1092
1093
1093
1094
1094 def analyzeremotephases(repo, subset, roots):
1095 def analyzeremotephases(repo, subset, roots):
1095 """Compute phases heads and root in a subset of node from root dict
1096 """Compute phases heads and root in a subset of node from root dict
1096
1097
1097 * subset is heads of the subset
1098 * subset is heads of the subset
1098 * roots is {<nodeid> => phase} mapping. key and value are string.
1099 * roots is {<nodeid> => phase} mapping. key and value are string.
1099
1100
1100 Accept unknown element input
1101 Accept unknown element input
1101 """
1102 """
1102 repo = repo.unfiltered()
1103 repo = repo.unfiltered()
1103 # build list from dictionary
1104 # build list from dictionary
1104 draftroots = []
1105 draftroots = []
1105 has_node = repo.changelog.index.has_node # to filter unknown nodes
1106 has_node = repo.changelog.index.has_node # to filter unknown nodes
1106 for nhex, phase in roots.items():
1107 for nhex, phase in roots.items():
1107 if nhex == b'publishing': # ignore data related to publish option
1108 if nhex == b'publishing': # ignore data related to publish option
1108 continue
1109 continue
1109 node = bin(nhex)
1110 node = bin(nhex)
1110 phase = int(phase)
1111 phase = int(phase)
1111 if phase == public:
1112 if phase == public:
1112 if node != repo.nullid:
1113 if node != repo.nullid:
1113 repo.ui.warn(
1114 repo.ui.warn(
1114 _(
1115 _(
1115 b'ignoring inconsistent public root'
1116 b'ignoring inconsistent public root'
1116 b' from remote: %s\n'
1117 b' from remote: %s\n'
1117 )
1118 )
1118 % nhex
1119 % nhex
1119 )
1120 )
1120 elif phase == draft:
1121 elif phase == draft:
1121 if has_node(node):
1122 if has_node(node):
1122 draftroots.append(node)
1123 draftroots.append(node)
1123 else:
1124 else:
1124 repo.ui.warn(
1125 repo.ui.warn(
1125 _(b'ignoring unexpected root from remote: %i %s\n')
1126 _(b'ignoring unexpected root from remote: %i %s\n')
1126 % (phase, nhex)
1127 % (phase, nhex)
1127 )
1128 )
1128 # compute heads
1129 # compute heads
1129 publicheads = newheads(repo, subset, draftroots)
1130 publicheads = newheads(repo, subset, draftroots)
1130 return publicheads, draftroots
1131 return publicheads, draftroots
1131
1132
1132
1133
1133 class remotephasessummary:
1134 class remotephasessummary:
1134 """summarize phase information on the remote side
1135 """summarize phase information on the remote side
1135
1136
1136 :publishing: True is the remote is publishing
1137 :publishing: True is the remote is publishing
1137 :publicheads: list of remote public phase heads (nodes)
1138 :publicheads: list of remote public phase heads (nodes)
1138 :draftheads: list of remote draft phase heads (nodes)
1139 :draftheads: list of remote draft phase heads (nodes)
1139 :draftroots: list of remote draft phase root (nodes)
1140 :draftroots: list of remote draft phase root (nodes)
1140 """
1141 """
1141
1142
1142 def __init__(self, repo, remotesubset, remoteroots):
1143 def __init__(self, repo, remotesubset, remoteroots):
1143 unfi = repo.unfiltered()
1144 unfi = repo.unfiltered()
1144 self._allremoteroots = remoteroots
1145 self._allremoteroots = remoteroots
1145
1146
1146 self.publishing = remoteroots.get(b'publishing', False)
1147 self.publishing = remoteroots.get(b'publishing', False)
1147
1148
1148 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1149 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1149 self.publicheads, self.draftroots = ana
1150 self.publicheads, self.draftroots = ana
1150 # Get the list of all "heads" revs draft on remote
1151 # Get the list of all "heads" revs draft on remote
1151 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1152 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1152 self.draftheads = [c.node() for c in dheads]
1153 self.draftheads = [c.node() for c in dheads]
1153
1154
1154
1155
1155 def newheads(repo, heads, roots):
1156 def newheads(repo, heads, roots):
1156 """compute new head of a subset minus another
1157 """compute new head of a subset minus another
1157
1158
1158 * `heads`: define the first subset
1159 * `heads`: define the first subset
1159 * `roots`: define the second we subtract from the first"""
1160 * `roots`: define the second we subtract from the first"""
1160 # prevent an import cycle
1161 # prevent an import cycle
1161 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1162 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1162 from . import dagop
1163 from . import dagop
1163
1164
1164 repo = repo.unfiltered()
1165 repo = repo.unfiltered()
1165 cl = repo.changelog
1166 cl = repo.changelog
1166 rev = cl.index.get_rev
1167 rev = cl.index.get_rev
1167 if not roots:
1168 if not roots:
1168 return heads
1169 return heads
1169 if not heads or heads == [repo.nullid]:
1170 if not heads or heads == [repo.nullid]:
1170 return []
1171 return []
1171 # The logic operated on revisions, convert arguments early for convenience
1172 # The logic operated on revisions, convert arguments early for convenience
1172 new_heads = {rev(n) for n in heads if n != repo.nullid}
1173 new_heads = {rev(n) for n in heads if n != repo.nullid}
1173 roots = [rev(n) for n in roots]
1174 roots = [rev(n) for n in roots]
1174 # compute the area we need to remove
1175 # compute the area we need to remove
1175 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1176 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1176 # heads in the area are no longer heads
1177 # heads in the area are no longer heads
1177 new_heads.difference_update(affected_zone)
1178 new_heads.difference_update(affected_zone)
1178 # revisions in the area have children outside of it,
1179 # revisions in the area have children outside of it,
1179 # They might be new heads
1180 # They might be new heads
1180 candidates = repo.revs(
1181 candidates = repo.revs(
1181 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1182 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1182 )
1183 )
1183 candidates -= affected_zone
1184 candidates -= affected_zone
1184 if new_heads or candidates:
1185 if new_heads or candidates:
1185 # remove candidate that are ancestors of other heads
1186 # remove candidate that are ancestors of other heads
1186 new_heads.update(candidates)
1187 new_heads.update(candidates)
1187 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1188 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1188 pruned = dagop.reachableroots(repo, candidates, prunestart)
1189 pruned = dagop.reachableroots(repo, candidates, prunestart)
1189 new_heads.difference_update(pruned)
1190 new_heads.difference_update(pruned)
1190
1191
1191 return pycompat.maplist(cl.node, sorted(new_heads))
1192 return pycompat.maplist(cl.node, sorted(new_heads))
1192
1193
1193
1194
1194 def newcommitphase(ui: "uimod.ui") -> int:
1195 def newcommitphase(ui: "uimod.ui") -> int:
1195 """helper to get the target phase of new commit
1196 """helper to get the target phase of new commit
1196
1197
1197 Handle all possible values for the phases.new-commit options.
1198 Handle all possible values for the phases.new-commit options.
1198
1199
1199 """
1200 """
1200 v = ui.config(b'phases', b'new-commit')
1201 v = ui.config(b'phases', b'new-commit')
1201 try:
1202 try:
1202 return phasenumber2[v]
1203 return phasenumber2[v]
1203 except KeyError:
1204 except KeyError:
1204 raise error.ConfigError(
1205 raise error.ConfigError(
1205 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1206 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1206 )
1207 )
1207
1208
1208
1209
1209 def hassecret(repo: "localrepo.localrepository") -> bool:
1210 def hassecret(repo: "localrepo.localrepository") -> bool:
1210 """utility function that check if a repo have any secret changeset."""
1211 """utility function that check if a repo have any secret changeset."""
1211 return bool(repo._phasecache._phaseroots[secret])
1212 return bool(repo._phasecache._phaseroots[secret])
1212
1213
1213
1214
1214 def preparehookargs(
1215 def preparehookargs(
1215 node: bytes,
1216 node: bytes,
1216 old: Optional[int],
1217 old: Optional[int],
1217 new: Optional[int],
1218 new: Optional[int],
1218 ) -> Dict[bytes, bytes]:
1219 ) -> Dict[bytes, bytes]:
1219 if old is None:
1220 if old is None:
1220 old = b''
1221 old = b''
1221 else:
1222 else:
1222 old = phasenames[old]
1223 old = phasenames[old]
1223 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
1224 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