##// END OF EJS Templates
phases: avoid N² behavior in `advanceboundary`...
marmoute -
r52398:c9ceb4f6 6.7 stable
parent child Browse files
Show More
@@ -1,1220 +1,1223 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 push = heapq.heappush
707 push = heapq.heappush
707 pop = heapq.heappop
708 pop = heapq.heappop
708 parents = cl.parentrevs
709 parents = cl.parentrevs
709 get_phase = self.phase
710 get_phase = self.phase
710 changed = {} # set of revisions to be changed
711 changed = {} # set of revisions to be changed
711 # set of root deleted by this path
712 # set of root deleted by this path
712 delroots = set()
713 delroots = set()
713 new_roots = {p: set() for p in affectable_phases}
714 new_roots = {p: set() for p in affectable_phases}
714 new_target_roots = set()
715 new_target_roots = set()
715 # revision to walk down
716 # revision to walk down
716 revs = [-r for r in new_revs]
717 revs = [-r for r in new_revs]
717 heapq.heapify(revs)
718 heapq.heapify(revs)
718 while revs:
719 while revs:
719 current = -pop(revs)
720 current = -pop(revs)
720 current_phase = get_phase(repo, current)
721 current_phase = get_phase(repo, current)
721 changed[current] = current_phase
722 changed[current] = current_phase
722 p1, p2 = parents(current)
723 p1, p2 = parents(current)
723 if p1 == nullrev:
724 if p1 == nullrev:
724 p1_phase = public
725 p1_phase = public
725 else:
726 else:
726 p1_phase = get_phase(repo, p1)
727 p1_phase = get_phase(repo, p1)
727 if p2 == nullrev:
728 if p2 == nullrev:
728 p2_phase = public
729 p2_phase = public
729 else:
730 else:
730 p2_phase = get_phase(repo, p2)
731 p2_phase = get_phase(repo, p2)
731 # do we have a root ?
732 # do we have a root ?
732 if current_phase != p1_phase and current_phase != p2_phase:
733 if current_phase != p1_phase and current_phase != p2_phase:
733 # do not record phase, because we could have "duplicated"
734 # do not record phase, because we could have "duplicated"
734 # 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
735 # higher phases
736 # higher phases
736 delroots.add(current)
737 delroots.add(current)
737 # schedule a walk down if needed
738 # schedule a walk down if needed
738 if p1_phase > targetphase:
739 if p1_phase > targetphase and p1 not in seen:
740 seen.add(p1)
739 push(revs, -p1)
741 push(revs, -p1)
740 if p2_phase > targetphase:
742 if p2_phase > targetphase and p2 not in seen:
743 seen.add(p2)
741 push(revs, -p2)
744 push(revs, -p2)
742 if p1_phase < targetphase and p2_phase < targetphase:
745 if p1_phase < targetphase and p2_phase < targetphase:
743 new_target_roots.add(current)
746 new_target_roots.add(current)
744
747
745 # the last iteration was done with the smallest value
748 # the last iteration was done with the smallest value
746 min_current = current
749 min_current = current
747 # do we have unwalked children that might be new roots
750 # do we have unwalked children that might be new roots
748 if (min_current + len(changed)) < len(cl):
751 if (min_current + len(changed)) < len(cl):
749 for r in range(min_current, len(cl)):
752 for r in range(min_current, len(cl)):
750 if r in changed:
753 if r in changed:
751 continue
754 continue
752 phase = get_phase(repo, r)
755 phase = get_phase(repo, r)
753 if phase <= targetphase:
756 if phase <= targetphase:
754 continue
757 continue
755 p1, p2 = parents(r)
758 p1, p2 = parents(r)
756 if not (p1 in changed or p2 in changed):
759 if not (p1 in changed or p2 in changed):
757 continue # not affected
760 continue # not affected
758 if p1 != nullrev and p1 not in changed:
761 if p1 != nullrev and p1 not in changed:
759 p1_phase = get_phase(repo, p1)
762 p1_phase = get_phase(repo, p1)
760 if p1_phase == phase:
763 if p1_phase == phase:
761 continue # not a root
764 continue # not a root
762 if p2 != nullrev and p2 not in changed:
765 if p2 != nullrev and p2 not in changed:
763 p2_phase = get_phase(repo, p2)
766 p2_phase = get_phase(repo, p2)
764 if p2_phase == phase:
767 if p2_phase == phase:
765 continue # not a root
768 continue # not a root
766 new_roots[phase].add(r)
769 new_roots[phase].add(r)
767
770
768 # apply the changes
771 # apply the changes
769 if not dryrun:
772 if not dryrun:
770 for r, p in changed.items():
773 for r, p in changed.items():
771 _trackphasechange(phasetracking, r, p, targetphase)
774 _trackphasechange(phasetracking, r, p, targetphase)
772 if targetphase > public:
775 if targetphase > public:
773 self._phasesets[targetphase].update(changed)
776 self._phasesets[targetphase].update(changed)
774 for phase in affectable_phases:
777 for phase in affectable_phases:
775 roots = self._phaseroots[phase]
778 roots = self._phaseroots[phase]
776 removed = roots & delroots
779 removed = roots & delroots
777 if removed or new_roots[phase]:
780 if removed or new_roots[phase]:
778 self._phasesets[phase].difference_update(changed)
781 self._phasesets[phase].difference_update(changed)
779 # Be careful to preserve shallow-copied values: do not
782 # Be careful to preserve shallow-copied values: do not
780 # update phaseroots values, replace them.
783 # update phaseroots values, replace them.
781 final_roots = roots - delroots | new_roots[phase]
784 final_roots = roots - delroots | new_roots[phase]
782 self._updateroots(
785 self._updateroots(
783 repo, phase, final_roots, tr, invalidate=False
786 repo, phase, final_roots, tr, invalidate=False
784 )
787 )
785 if new_target_roots:
788 if new_target_roots:
786 # Thanks for previous filtering, we can't replace existing
789 # Thanks for previous filtering, we can't replace existing
787 # roots
790 # roots
788 new_target_roots |= self._phaseroots[targetphase]
791 new_target_roots |= self._phaseroots[targetphase]
789 self._updateroots(
792 self._updateroots(
790 repo, targetphase, new_target_roots, tr, invalidate=False
793 repo, targetphase, new_target_roots, tr, invalidate=False
791 )
794 )
792 repo.invalidatevolatilesets()
795 repo.invalidatevolatilesets()
793 return changed
796 return changed
794
797
795 def retractboundary(self, repo, tr, targetphase, nodes):
798 def retractboundary(self, repo, tr, targetphase, nodes):
796 if tr is None:
799 if tr is None:
797 phasetracking = None
800 phasetracking = None
798 else:
801 else:
799 phasetracking = tr.changes.get(b'phases')
802 phasetracking = tr.changes.get(b'phases')
800 repo = repo.unfiltered()
803 repo = repo.unfiltered()
801 retracted = self._retractboundary(repo, tr, targetphase, nodes)
804 retracted = self._retractboundary(repo, tr, targetphase, nodes)
802 if retracted and phasetracking is not None:
805 if retracted and phasetracking is not None:
803 for r, old_phase in sorted(retracted.items()):
806 for r, old_phase in sorted(retracted.items()):
804 _trackphasechange(phasetracking, r, old_phase, targetphase)
807 _trackphasechange(phasetracking, r, old_phase, targetphase)
805 repo.invalidatevolatilesets()
808 repo.invalidatevolatilesets()
806
809
807 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
810 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
808 if targetphase == public:
811 if targetphase == public:
809 return {}
812 return {}
810 if (
813 if (
811 targetphase == internal
814 targetphase == internal
812 and not supportinternal(repo)
815 and not supportinternal(repo)
813 or targetphase == archived
816 or targetphase == archived
814 and not supportarchived(repo)
817 and not supportarchived(repo)
815 ):
818 ):
816 name = phasenames[targetphase]
819 name = phasenames[targetphase]
817 msg = b'this repository does not support the %s phase' % name
820 msg = b'this repository does not support the %s phase' % name
818 raise error.ProgrammingError(msg)
821 raise error.ProgrammingError(msg)
819 assert repo.filtername is None
822 assert repo.filtername is None
820 cl = repo.changelog
823 cl = repo.changelog
821 torev = cl.index.rev
824 torev = cl.index.rev
822 new_revs = set()
825 new_revs = set()
823 if revs is not None:
826 if revs is not None:
824 new_revs.update(revs)
827 new_revs.update(revs)
825 if nodes is not None:
828 if nodes is not None:
826 new_revs.update(torev(node) for node in nodes)
829 new_revs.update(torev(node) for node in nodes)
827 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
828 return {} # note: why do people call retractboundary with nothing ?
831 return {} # note: why do people call retractboundary with nothing ?
829
832
830 if nullrev in new_revs:
833 if nullrev in new_revs:
831 raise error.Abort(_(b'cannot change null revision phase'))
834 raise error.Abort(_(b'cannot change null revision phase'))
832
835
833 # Filter revision that are already in the right phase
836 # Filter revision that are already in the right phase
834 self._ensure_phase_sets(repo)
837 self._ensure_phase_sets(repo)
835 for phase, revs in self._phasesets.items():
838 for phase, revs in self._phasesets.items():
836 if phase >= targetphase:
839 if phase >= targetphase:
837 new_revs -= revs
840 new_revs -= revs
838 if not new_revs: # all revisions already in the right phases
841 if not new_revs: # all revisions already in the right phases
839 return {}
842 return {}
840
843
841 # Compute change in phase roots by walking the graph
844 # Compute change in phase roots by walking the graph
842 #
845 #
843 # 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
844 # something even cheaper/more-bounded
847 # something even cheaper/more-bounded
845 #
848 #
846 # 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
847 # descendant with phases >= target_phase.
850 # descendant with phases >= target_phase.
848 #
851 #
849 # 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 >=
850 # target_phase or reachable though another new_revs
853 # target_phase or reachable though another new_revs
851 # 2) This detect replaced current_roots as we reach them
854 # 2) This detect replaced current_roots as we reach them
852 # 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
853 # branch.
856 # branch.
854 #
857 #
855 # 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
856 # revision to the tip to avoid missing any potential children.
859 # revision to the tip to avoid missing any potential children.
857 #
860 #
858 # 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
859 # 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.
860 parents = cl.parentrevs
863 parents = cl.parentrevs
861 phase = self.phase
864 phase = self.phase
862 new_roots = set() # roots added by this phases
865 new_roots = set() # roots added by this phases
863 changed_revs = {} # revision affected by this call
866 changed_revs = {} # revision affected by this call
864 replaced_roots = set() # older roots replaced by this call
867 replaced_roots = set() # older roots replaced by this call
865 currentroots = self._phaseroots[targetphase]
868 currentroots = self._phaseroots[targetphase]
866 start = min(new_revs)
869 start = min(new_revs)
867 end = len(cl)
870 end = len(cl)
868 rev_phases = [None] * (end - start)
871 rev_phases = [None] * (end - start)
869 for r in range(start, end):
872 for r in range(start, end):
870
873
871 # gather information about the current_rev
874 # gather information about the current_rev
872 r_phase = phase(repo, r)
875 r_phase = phase(repo, r)
873 p_phase = None # phase inherited from parents
876 p_phase = None # phase inherited from parents
874 p1, p2 = parents(r)
877 p1, p2 = parents(r)
875 if p1 >= start:
878 if p1 >= start:
876 p1_phase = rev_phases[p1 - start]
879 p1_phase = rev_phases[p1 - start]
877 if p1_phase is not None:
880 if p1_phase is not None:
878 p_phase = p1_phase
881 p_phase = p1_phase
879 if p2 >= start:
882 if p2 >= start:
880 p2_phase = rev_phases[p2 - start]
883 p2_phase = rev_phases[p2 - start]
881 if p2_phase is not None:
884 if p2_phase is not None:
882 if p_phase is not None:
885 if p_phase is not None:
883 p_phase = max(p_phase, p2_phase)
886 p_phase = max(p_phase, p2_phase)
884 else:
887 else:
885 p_phase = p2_phase
888 p_phase = p2_phase
886
889
887 # assess the situation
890 # assess the situation
888 if r in new_revs and r_phase < targetphase:
891 if r in new_revs and r_phase < targetphase:
889 if p_phase is None or p_phase < targetphase:
892 if p_phase is None or p_phase < targetphase:
890 new_roots.add(r)
893 new_roots.add(r)
891 rev_phases[r - start] = targetphase
894 rev_phases[r - start] = targetphase
892 changed_revs[r] = r_phase
895 changed_revs[r] = r_phase
893 elif p_phase is None:
896 elif p_phase is None:
894 rev_phases[r - start] = r_phase
897 rev_phases[r - start] = r_phase
895 else:
898 else:
896 if p_phase > r_phase:
899 if p_phase > r_phase:
897 rev_phases[r - start] = p_phase
900 rev_phases[r - start] = p_phase
898 else:
901 else:
899 rev_phases[r - start] = r_phase
902 rev_phases[r - start] = r_phase
900 if p_phase == targetphase:
903 if p_phase == targetphase:
901 if p_phase > r_phase:
904 if p_phase > r_phase:
902 changed_revs[r] = r_phase
905 changed_revs[r] = r_phase
903 elif r in currentroots:
906 elif r in currentroots:
904 replaced_roots.add(r)
907 replaced_roots.add(r)
905 sets = self._phasesets
908 sets = self._phasesets
906 sets[targetphase].update(changed_revs)
909 sets[targetphase].update(changed_revs)
907 for r, old in changed_revs.items():
910 for r, old in changed_revs.items():
908 if old > public:
911 if old > public:
909 sets[old].discard(r)
912 sets[old].discard(r)
910
913
911 if new_roots:
914 if new_roots:
912 assert changed_revs
915 assert changed_revs
913
916
914 final_roots = new_roots | currentroots - replaced_roots
917 final_roots = new_roots | currentroots - replaced_roots
915 self._updateroots(
918 self._updateroots(
916 repo,
919 repo,
917 targetphase,
920 targetphase,
918 final_roots,
921 final_roots,
919 tr,
922 tr,
920 invalidate=False,
923 invalidate=False,
921 )
924 )
922 if targetphase > 1:
925 if targetphase > 1:
923 retracted = set(changed_revs)
926 retracted = set(changed_revs)
924 for lower_phase in range(1, targetphase):
927 for lower_phase in range(1, targetphase):
925 lower_roots = self._phaseroots.get(lower_phase)
928 lower_roots = self._phaseroots.get(lower_phase)
926 if lower_roots is None:
929 if lower_roots is None:
927 continue
930 continue
928 if lower_roots & retracted:
931 if lower_roots & retracted:
929 simpler_roots = lower_roots - retracted
932 simpler_roots = lower_roots - retracted
930 self._updateroots(
933 self._updateroots(
931 repo,
934 repo,
932 lower_phase,
935 lower_phase,
933 simpler_roots,
936 simpler_roots,
934 tr,
937 tr,
935 invalidate=False,
938 invalidate=False,
936 )
939 )
937 return changed_revs
940 return changed_revs
938 else:
941 else:
939 assert not changed_revs
942 assert not changed_revs
940 assert not replaced_roots
943 assert not replaced_roots
941 return {}
944 return {}
942
945
943 def register_strip(
946 def register_strip(
944 self,
947 self,
945 repo,
948 repo,
946 tr,
949 tr,
947 strip_rev: int,
950 strip_rev: int,
948 ):
951 ):
949 """announce a strip to the phase cache
952 """announce a strip to the phase cache
950
953
951 Any roots higher than the stripped revision should be dropped.
954 Any roots higher than the stripped revision should be dropped.
952 """
955 """
953 for targetphase, roots in list(self._phaseroots.items()):
956 for targetphase, roots in list(self._phaseroots.items()):
954 filtered = {r for r in roots if r >= strip_rev}
957 filtered = {r for r in roots if r >= strip_rev}
955 if filtered:
958 if filtered:
956 self._updateroots(repo, targetphase, roots - filtered, tr)
959 self._updateroots(repo, targetphase, roots - filtered, tr)
957 self.invalidate()
960 self.invalidate()
958
961
959
962
960 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
963 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
961 """Add nodes to a phase changing other nodes phases if necessary.
964 """Add nodes to a phase changing other nodes phases if necessary.
962
965
963 This function move boundary *forward* this means that all nodes
966 This function move boundary *forward* this means that all nodes
964 are set in the target phase or kept in a *lower* phase.
967 are set in the target phase or kept in a *lower* phase.
965
968
966 Simplify boundary to contains phase roots only.
969 Simplify boundary to contains phase roots only.
967
970
968 If dryrun is True, no actions will be performed
971 If dryrun is True, no actions will be performed
969
972
970 Returns a set of revs whose phase is changed or should be changed
973 Returns a set of revs whose phase is changed or should be changed
971 """
974 """
972 if revs is None:
975 if revs is None:
973 revs = []
976 revs = []
974 phcache = repo._phasecache.copy()
977 phcache = repo._phasecache.copy()
975 changes = phcache.advanceboundary(
978 changes = phcache.advanceboundary(
976 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
979 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
977 )
980 )
978 if not dryrun:
981 if not dryrun:
979 repo._phasecache.replace(phcache)
982 repo._phasecache.replace(phcache)
980 return changes
983 return changes
981
984
982
985
983 def retractboundary(repo, tr, targetphase, nodes):
986 def retractboundary(repo, tr, targetphase, nodes):
984 """Set nodes back to a phase changing other nodes phases if
987 """Set nodes back to a phase changing other nodes phases if
985 necessary.
988 necessary.
986
989
987 This function move boundary *backward* this means that all nodes
990 This function move boundary *backward* this means that all nodes
988 are set in the target phase or kept in a *higher* phase.
991 are set in the target phase or kept in a *higher* phase.
989
992
990 Simplify boundary to contains phase roots only."""
993 Simplify boundary to contains phase roots only."""
991 phcache = repo._phasecache.copy()
994 phcache = repo._phasecache.copy()
992 phcache.retractboundary(repo, tr, targetphase, nodes)
995 phcache.retractboundary(repo, tr, targetphase, nodes)
993 repo._phasecache.replace(phcache)
996 repo._phasecache.replace(phcache)
994
997
995
998
996 def registernew(repo, tr, targetphase, revs):
999 def registernew(repo, tr, targetphase, revs):
997 """register a new revision and its phase
1000 """register a new revision and its phase
998
1001
999 Code adding revisions to the repository should use this function to
1002 Code adding revisions to the repository should use this function to
1000 set new changeset in their target phase (or higher).
1003 set new changeset in their target phase (or higher).
1001 """
1004 """
1002 phcache = repo._phasecache.copy()
1005 phcache = repo._phasecache.copy()
1003 phcache.registernew(repo, tr, targetphase, revs)
1006 phcache.registernew(repo, tr, targetphase, revs)
1004 repo._phasecache.replace(phcache)
1007 repo._phasecache.replace(phcache)
1005
1008
1006
1009
1007 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
1010 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
1008 """List phases root for serialization over pushkey"""
1011 """List phases root for serialization over pushkey"""
1009 # Use ordered dictionary so behavior is deterministic.
1012 # Use ordered dictionary so behavior is deterministic.
1010 keys = util.sortdict()
1013 keys = util.sortdict()
1011 value = b'%i' % draft
1014 value = b'%i' % draft
1012 cl = repo.unfiltered().changelog
1015 cl = repo.unfiltered().changelog
1013 to_node = cl.node
1016 to_node = cl.node
1014 for root in repo._phasecache._phaseroots[draft]:
1017 for root in repo._phasecache._phaseroots[draft]:
1015 if repo._phasecache.phase(repo, root) <= draft:
1018 if repo._phasecache.phase(repo, root) <= draft:
1016 keys[hex(to_node(root))] = value
1019 keys[hex(to_node(root))] = value
1017
1020
1018 if repo.publishing():
1021 if repo.publishing():
1019 # Add an extra data to let remote know we are a publishing
1022 # Add an extra data to let remote know we are a publishing
1020 # repo. Publishing repo can't just pretend they are old repo.
1023 # repo. Publishing repo can't just pretend they are old repo.
1021 # When pushing to a publishing repo, the client still need to
1024 # When pushing to a publishing repo, the client still need to
1022 # push phase boundary
1025 # push phase boundary
1023 #
1026 #
1024 # Push do not only push changeset. It also push phase data.
1027 # Push do not only push changeset. It also push phase data.
1025 # New phase data may apply to common changeset which won't be
1028 # New phase data may apply to common changeset which won't be
1026 # push (as they are common). Here is a very simple example:
1029 # push (as they are common). Here is a very simple example:
1027 #
1030 #
1028 # 1) repo A push changeset X as draft to repo B
1031 # 1) repo A push changeset X as draft to repo B
1029 # 2) repo B make changeset X public
1032 # 2) repo B make changeset X public
1030 # 3) repo B push to repo A. X is not pushed but the data that
1033 # 3) repo B push to repo A. X is not pushed but the data that
1031 # X as now public should
1034 # X as now public should
1032 #
1035 #
1033 # The server can't handle it on it's own as it has no idea of
1036 # The server can't handle it on it's own as it has no idea of
1034 # client phase data.
1037 # client phase data.
1035 keys[b'publishing'] = b'True'
1038 keys[b'publishing'] = b'True'
1036 return keys
1039 return keys
1037
1040
1038
1041
1039 def pushphase(
1042 def pushphase(
1040 repo: "localrepo.localrepository",
1043 repo: "localrepo.localrepository",
1041 nhex: bytes,
1044 nhex: bytes,
1042 oldphasestr: bytes,
1045 oldphasestr: bytes,
1043 newphasestr: bytes,
1046 newphasestr: bytes,
1044 ) -> bool:
1047 ) -> bool:
1045 """List phases root for serialization over pushkey"""
1048 """List phases root for serialization over pushkey"""
1046 repo = repo.unfiltered()
1049 repo = repo.unfiltered()
1047 with repo.lock():
1050 with repo.lock():
1048 currentphase = repo[nhex].phase()
1051 currentphase = repo[nhex].phase()
1049 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
1052 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
1050 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
1053 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
1051 if currentphase == oldphase and newphase < oldphase:
1054 if currentphase == oldphase and newphase < oldphase:
1052 with repo.transaction(b'pushkey-phase') as tr:
1055 with repo.transaction(b'pushkey-phase') as tr:
1053 advanceboundary(repo, tr, newphase, [bin(nhex)])
1056 advanceboundary(repo, tr, newphase, [bin(nhex)])
1054 return True
1057 return True
1055 elif currentphase == newphase:
1058 elif currentphase == newphase:
1056 # raced, but got correct result
1059 # raced, but got correct result
1057 return True
1060 return True
1058 else:
1061 else:
1059 return False
1062 return False
1060
1063
1061
1064
1062 def subsetphaseheads(repo, subset):
1065 def subsetphaseheads(repo, subset):
1063 """Finds the phase heads for a subset of a history
1066 """Finds the phase heads for a subset of a history
1064
1067
1065 Returns a list indexed by phase number where each item is a list of phase
1068 Returns a list indexed by phase number where each item is a list of phase
1066 head nodes.
1069 head nodes.
1067 """
1070 """
1068 cl = repo.changelog
1071 cl = repo.changelog
1069
1072
1070 headsbyphase = {i: [] for i in allphases}
1073 headsbyphase = {i: [] for i in allphases}
1071 for phase in allphases:
1074 for phase in allphases:
1072 revset = b"heads(%%ln & _phase(%d))" % phase
1075 revset = b"heads(%%ln & _phase(%d))" % phase
1073 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
1076 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
1074 return headsbyphase
1077 return headsbyphase
1075
1078
1076
1079
1077 def updatephases(repo, trgetter, headsbyphase):
1080 def updatephases(repo, trgetter, headsbyphase):
1078 """Updates the repo with the given phase heads"""
1081 """Updates the repo with the given phase heads"""
1079 # Now advance phase boundaries of all phases
1082 # Now advance phase boundaries of all phases
1080 #
1083 #
1081 # run the update (and fetch transaction) only if there are actually things
1084 # run the update (and fetch transaction) only if there are actually things
1082 # to update. This avoid creating empty transaction during no-op operation.
1085 # to update. This avoid creating empty transaction during no-op operation.
1083
1086
1084 for phase in allphases:
1087 for phase in allphases:
1085 revset = b'%ln - _phase(%s)'
1088 revset = b'%ln - _phase(%s)'
1086 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1089 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1087 if heads:
1090 if heads:
1088 advanceboundary(repo, trgetter(), phase, heads)
1091 advanceboundary(repo, trgetter(), phase, heads)
1089
1092
1090
1093
1091 def analyzeremotephases(repo, subset, roots):
1094 def analyzeremotephases(repo, subset, roots):
1092 """Compute phases heads and root in a subset of node from root dict
1095 """Compute phases heads and root in a subset of node from root dict
1093
1096
1094 * subset is heads of the subset
1097 * subset is heads of the subset
1095 * roots is {<nodeid> => phase} mapping. key and value are string.
1098 * roots is {<nodeid> => phase} mapping. key and value are string.
1096
1099
1097 Accept unknown element input
1100 Accept unknown element input
1098 """
1101 """
1099 repo = repo.unfiltered()
1102 repo = repo.unfiltered()
1100 # build list from dictionary
1103 # build list from dictionary
1101 draftroots = []
1104 draftroots = []
1102 has_node = repo.changelog.index.has_node # to filter unknown nodes
1105 has_node = repo.changelog.index.has_node # to filter unknown nodes
1103 for nhex, phase in roots.items():
1106 for nhex, phase in roots.items():
1104 if nhex == b'publishing': # ignore data related to publish option
1107 if nhex == b'publishing': # ignore data related to publish option
1105 continue
1108 continue
1106 node = bin(nhex)
1109 node = bin(nhex)
1107 phase = int(phase)
1110 phase = int(phase)
1108 if phase == public:
1111 if phase == public:
1109 if node != repo.nullid:
1112 if node != repo.nullid:
1110 repo.ui.warn(
1113 repo.ui.warn(
1111 _(
1114 _(
1112 b'ignoring inconsistent public root'
1115 b'ignoring inconsistent public root'
1113 b' from remote: %s\n'
1116 b' from remote: %s\n'
1114 )
1117 )
1115 % nhex
1118 % nhex
1116 )
1119 )
1117 elif phase == draft:
1120 elif phase == draft:
1118 if has_node(node):
1121 if has_node(node):
1119 draftroots.append(node)
1122 draftroots.append(node)
1120 else:
1123 else:
1121 repo.ui.warn(
1124 repo.ui.warn(
1122 _(b'ignoring unexpected root from remote: %i %s\n')
1125 _(b'ignoring unexpected root from remote: %i %s\n')
1123 % (phase, nhex)
1126 % (phase, nhex)
1124 )
1127 )
1125 # compute heads
1128 # compute heads
1126 publicheads = newheads(repo, subset, draftroots)
1129 publicheads = newheads(repo, subset, draftroots)
1127 return publicheads, draftroots
1130 return publicheads, draftroots
1128
1131
1129
1132
1130 class remotephasessummary:
1133 class remotephasessummary:
1131 """summarize phase information on the remote side
1134 """summarize phase information on the remote side
1132
1135
1133 :publishing: True is the remote is publishing
1136 :publishing: True is the remote is publishing
1134 :publicheads: list of remote public phase heads (nodes)
1137 :publicheads: list of remote public phase heads (nodes)
1135 :draftheads: list of remote draft phase heads (nodes)
1138 :draftheads: list of remote draft phase heads (nodes)
1136 :draftroots: list of remote draft phase root (nodes)
1139 :draftroots: list of remote draft phase root (nodes)
1137 """
1140 """
1138
1141
1139 def __init__(self, repo, remotesubset, remoteroots):
1142 def __init__(self, repo, remotesubset, remoteroots):
1140 unfi = repo.unfiltered()
1143 unfi = repo.unfiltered()
1141 self._allremoteroots = remoteroots
1144 self._allremoteroots = remoteroots
1142
1145
1143 self.publishing = remoteroots.get(b'publishing', False)
1146 self.publishing = remoteroots.get(b'publishing', False)
1144
1147
1145 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1148 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1146 self.publicheads, self.draftroots = ana
1149 self.publicheads, self.draftroots = ana
1147 # Get the list of all "heads" revs draft on remote
1150 # Get the list of all "heads" revs draft on remote
1148 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1151 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1149 self.draftheads = [c.node() for c in dheads]
1152 self.draftheads = [c.node() for c in dheads]
1150
1153
1151
1154
1152 def newheads(repo, heads, roots):
1155 def newheads(repo, heads, roots):
1153 """compute new head of a subset minus another
1156 """compute new head of a subset minus another
1154
1157
1155 * `heads`: define the first subset
1158 * `heads`: define the first subset
1156 * `roots`: define the second we subtract from the first"""
1159 * `roots`: define the second we subtract from the first"""
1157 # prevent an import cycle
1160 # prevent an import cycle
1158 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1161 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1159 from . import dagop
1162 from . import dagop
1160
1163
1161 repo = repo.unfiltered()
1164 repo = repo.unfiltered()
1162 cl = repo.changelog
1165 cl = repo.changelog
1163 rev = cl.index.get_rev
1166 rev = cl.index.get_rev
1164 if not roots:
1167 if not roots:
1165 return heads
1168 return heads
1166 if not heads or heads == [repo.nullid]:
1169 if not heads or heads == [repo.nullid]:
1167 return []
1170 return []
1168 # The logic operated on revisions, convert arguments early for convenience
1171 # The logic operated on revisions, convert arguments early for convenience
1169 new_heads = {rev(n) for n in heads if n != repo.nullid}
1172 new_heads = {rev(n) for n in heads if n != repo.nullid}
1170 roots = [rev(n) for n in roots]
1173 roots = [rev(n) for n in roots]
1171 # compute the area we need to remove
1174 # compute the area we need to remove
1172 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1175 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1173 # heads in the area are no longer heads
1176 # heads in the area are no longer heads
1174 new_heads.difference_update(affected_zone)
1177 new_heads.difference_update(affected_zone)
1175 # revisions in the area have children outside of it,
1178 # revisions in the area have children outside of it,
1176 # They might be new heads
1179 # They might be new heads
1177 candidates = repo.revs(
1180 candidates = repo.revs(
1178 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1181 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1179 )
1182 )
1180 candidates -= affected_zone
1183 candidates -= affected_zone
1181 if new_heads or candidates:
1184 if new_heads or candidates:
1182 # remove candidate that are ancestors of other heads
1185 # remove candidate that are ancestors of other heads
1183 new_heads.update(candidates)
1186 new_heads.update(candidates)
1184 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1187 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1185 pruned = dagop.reachableroots(repo, candidates, prunestart)
1188 pruned = dagop.reachableroots(repo, candidates, prunestart)
1186 new_heads.difference_update(pruned)
1189 new_heads.difference_update(pruned)
1187
1190
1188 return pycompat.maplist(cl.node, sorted(new_heads))
1191 return pycompat.maplist(cl.node, sorted(new_heads))
1189
1192
1190
1193
1191 def newcommitphase(ui: "uimod.ui") -> int:
1194 def newcommitphase(ui: "uimod.ui") -> int:
1192 """helper to get the target phase of new commit
1195 """helper to get the target phase of new commit
1193
1196
1194 Handle all possible values for the phases.new-commit options.
1197 Handle all possible values for the phases.new-commit options.
1195
1198
1196 """
1199 """
1197 v = ui.config(b'phases', b'new-commit')
1200 v = ui.config(b'phases', b'new-commit')
1198 try:
1201 try:
1199 return phasenumber2[v]
1202 return phasenumber2[v]
1200 except KeyError:
1203 except KeyError:
1201 raise error.ConfigError(
1204 raise error.ConfigError(
1202 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1205 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1203 )
1206 )
1204
1207
1205
1208
1206 def hassecret(repo: "localrepo.localrepository") -> bool:
1209 def hassecret(repo: "localrepo.localrepository") -> bool:
1207 """utility function that check if a repo have any secret changeset."""
1210 """utility function that check if a repo have any secret changeset."""
1208 return bool(repo._phasecache._phaseroots[secret])
1211 return bool(repo._phasecache._phaseroots[secret])
1209
1212
1210
1213
1211 def preparehookargs(
1214 def preparehookargs(
1212 node: bytes,
1215 node: bytes,
1213 old: Optional[int],
1216 old: Optional[int],
1214 new: Optional[int],
1217 new: Optional[int],
1215 ) -> Dict[bytes, bytes]:
1218 ) -> Dict[bytes, bytes]:
1216 if old is None:
1219 if old is None:
1217 old = b''
1220 old = b''
1218 else:
1221 else:
1219 old = phasenames[old]
1222 old = phasenames[old]
1220 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
1223 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