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