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