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