##// END OF EJS Templates
phases: drop set building in `hasnonpublicphases`...
marmoute -
r52309:0c04074f default
parent child Browse files
Show More
@@ -1,1048 +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 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 class phasecache:
368 class phasecache:
369 def __init__(
369 def __init__(
370 self,
370 self,
371 repo: "localrepo.localrepository",
371 repo: "localrepo.localrepository",
372 phasedefaults: Optional["Phasedefaults"],
372 phasedefaults: Optional["Phasedefaults"],
373 _load: bool = True,
373 _load: bool = True,
374 ):
374 ):
375 if _load:
375 if _load:
376 # Cheap trick to allow shallow-copy without copy module
376 # Cheap trick to allow shallow-copy without copy module
377 loaded = _readroots(repo, phasedefaults)
377 loaded = _readroots(repo, phasedefaults)
378 self._phaseroots: Phaseroots = loaded[0]
378 self._phaseroots: Phaseroots = loaded[0]
379 self.dirty: bool = loaded[1]
379 self.dirty: bool = loaded[1]
380 self._loadedrevslen = 0
380 self._loadedrevslen = 0
381 self._phasesets: PhaseSets = None
381 self._phasesets: PhaseSets = None
382
382
383 def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool:
383 def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool:
384 """detect if there are revisions with non-public phase"""
384 """detect if there are revisions with non-public phase"""
385 repo = repo.unfiltered()
385 # XXX deprecate the unused repo argument
386 self._ensure_phase_sets(repo)
387 return any(
386 return any(
388 revs for phase, revs in self._phaseroots.items() if phase != public
387 revs for phase, revs in self._phaseroots.items() if phase != public
389 )
388 )
390
389
391 def nonpublicphaseroots(
390 def nonpublicphaseroots(
392 self, repo: "localrepo.localrepository"
391 self, repo: "localrepo.localrepository"
393 ) -> Set[int]:
392 ) -> Set[int]:
394 """returns the roots of all non-public phases
393 """returns the roots of all non-public phases
395
394
396 The roots are not minimized, so if the secret revisions are
395 The roots are not minimized, so if the secret revisions are
397 descendants of draft revisions, their roots will still be present.
396 descendants of draft revisions, their roots will still be present.
398 """
397 """
399 repo = repo.unfiltered()
398 repo = repo.unfiltered()
400 self._ensure_phase_sets(repo)
399 self._ensure_phase_sets(repo)
401 return set().union(
400 return set().union(
402 *[
401 *[
403 revs
402 revs
404 for phase, revs in self._phaseroots.items()
403 for phase, revs in self._phaseroots.items()
405 if phase != public
404 if phase != public
406 ]
405 ]
407 )
406 )
408
407
409 def getrevset(
408 def getrevset(
410 self,
409 self,
411 repo: "localrepo.localrepository",
410 repo: "localrepo.localrepository",
412 phases: Iterable[int],
411 phases: Iterable[int],
413 subset: Optional[Any] = None,
412 subset: Optional[Any] = None,
414 ) -> Any:
413 ) -> Any:
415 # TODO: finish typing this
414 # TODO: finish typing this
416 """return a smartset for the given phases"""
415 """return a smartset for the given phases"""
417 self._ensure_phase_sets(repo) # ensure phase's sets are loaded
416 self._ensure_phase_sets(repo) # ensure phase's sets are loaded
418 phases = set(phases)
417 phases = set(phases)
419 publicphase = public in phases
418 publicphase = public in phases
420
419
421 if publicphase:
420 if publicphase:
422 # In this case, phases keeps all the *other* phases.
421 # In this case, phases keeps all the *other* phases.
423 phases = set(allphases).difference(phases)
422 phases = set(allphases).difference(phases)
424 if not phases:
423 if not phases:
425 return smartset.fullreposet(repo)
424 return smartset.fullreposet(repo)
426
425
427 # fast path: _phasesets contains the interesting sets,
426 # fast path: _phasesets contains the interesting sets,
428 # might only need a union and post-filtering.
427 # might only need a union and post-filtering.
429 revsneedscopy = False
428 revsneedscopy = False
430 if len(phases) == 1:
429 if len(phases) == 1:
431 [p] = phases
430 [p] = phases
432 revs = self._phasesets[p]
431 revs = self._phasesets[p]
433 revsneedscopy = True # Don't modify _phasesets
432 revsneedscopy = True # Don't modify _phasesets
434 else:
433 else:
435 # revs has the revisions in all *other* phases.
434 # revs has the revisions in all *other* phases.
436 revs = set.union(*[self._phasesets[p] for p in phases])
435 revs = set.union(*[self._phasesets[p] for p in phases])
437
436
438 def _addwdir(wdirsubset, wdirrevs):
437 def _addwdir(wdirsubset, wdirrevs):
439 if wdirrev in wdirsubset and repo[None].phase() in phases:
438 if wdirrev in wdirsubset and repo[None].phase() in phases:
440 if revsneedscopy:
439 if revsneedscopy:
441 wdirrevs = wdirrevs.copy()
440 wdirrevs = wdirrevs.copy()
442 # The working dir would never be in the # cache, but it was in
441 # The working dir would never be in the # cache, but it was in
443 # the subset being filtered for its phase (or filtered out,
442 # the subset being filtered for its phase (or filtered out,
444 # depending on publicphase), so add it to the output to be
443 # depending on publicphase), so add it to the output to be
445 # included (or filtered out).
444 # included (or filtered out).
446 wdirrevs.add(wdirrev)
445 wdirrevs.add(wdirrev)
447 return wdirrevs
446 return wdirrevs
448
447
449 if not publicphase:
448 if not publicphase:
450 if repo.changelog.filteredrevs:
449 if repo.changelog.filteredrevs:
451 revs = revs - repo.changelog.filteredrevs
450 revs = revs - repo.changelog.filteredrevs
452
451
453 if subset is None:
452 if subset is None:
454 return smartset.baseset(revs)
453 return smartset.baseset(revs)
455 else:
454 else:
456 revs = _addwdir(subset, revs)
455 revs = _addwdir(subset, revs)
457 return subset & smartset.baseset(revs)
456 return subset & smartset.baseset(revs)
458 else:
457 else:
459 if subset is None:
458 if subset is None:
460 subset = smartset.fullreposet(repo)
459 subset = smartset.fullreposet(repo)
461
460
462 revs = _addwdir(subset, revs)
461 revs = _addwdir(subset, revs)
463
462
464 if not revs:
463 if not revs:
465 return subset
464 return subset
466 return subset.filter(lambda r: r not in revs)
465 return subset.filter(lambda r: r not in revs)
467
466
468 def copy(self):
467 def copy(self):
469 # Shallow copy meant to ensure isolation in
468 # Shallow copy meant to ensure isolation in
470 # advance/retractboundary(), nothing more.
469 # advance/retractboundary(), nothing more.
471 ph = self.__class__(None, None, _load=False)
470 ph = self.__class__(None, None, _load=False)
472 ph._phaseroots = self._phaseroots.copy()
471 ph._phaseroots = self._phaseroots.copy()
473 ph.dirty = self.dirty
472 ph.dirty = self.dirty
474 ph._loadedrevslen = self._loadedrevslen
473 ph._loadedrevslen = self._loadedrevslen
475 ph._phasesets = self._phasesets
474 ph._phasesets = self._phasesets
476 return ph
475 return ph
477
476
478 def replace(self, phcache):
477 def replace(self, phcache):
479 """replace all values in 'self' with content of phcache"""
478 """replace all values in 'self' with content of phcache"""
480 for a in (
479 for a in (
481 '_phaseroots',
480 '_phaseroots',
482 'dirty',
481 'dirty',
483 '_loadedrevslen',
482 '_loadedrevslen',
484 '_phasesets',
483 '_phasesets',
485 ):
484 ):
486 setattr(self, a, getattr(phcache, a))
485 setattr(self, a, getattr(phcache, a))
487
486
488 def _getphaserevsnative(self, repo):
487 def _getphaserevsnative(self, repo):
489 repo = repo.unfiltered()
488 repo = repo.unfiltered()
490 return repo.changelog.computephases(self._phaseroots)
489 return repo.changelog.computephases(self._phaseroots)
491
490
492 def _computephaserevspure(self, repo):
491 def _computephaserevspure(self, repo):
493 repo = repo.unfiltered()
492 repo = repo.unfiltered()
494 cl = repo.changelog
493 cl = repo.changelog
495 self._phasesets = {phase: set() for phase in allphases}
494 self._phasesets = {phase: set() for phase in allphases}
496 lowerroots = set()
495 lowerroots = set()
497 for phase in reversed(trackedphases):
496 for phase in reversed(trackedphases):
498 roots = self._phaseroots[phase]
497 roots = self._phaseroots[phase]
499 if roots:
498 if roots:
500 ps = set(cl.descendants(roots))
499 ps = set(cl.descendants(roots))
501 for root in roots:
500 for root in roots:
502 ps.add(root)
501 ps.add(root)
503 ps.difference_update(lowerroots)
502 ps.difference_update(lowerroots)
504 lowerroots.update(ps)
503 lowerroots.update(ps)
505 self._phasesets[phase] = ps
504 self._phasesets[phase] = ps
506 self._loadedrevslen = len(cl)
505 self._loadedrevslen = len(cl)
507
506
508 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
507 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
509 """ensure phase information is loaded in the object and up to date"""
508 """ensure phase information is loaded in the object and up to date"""
510 update = False
509 update = False
511 if self._phasesets is None:
510 if self._phasesets is None:
512 update = True
511 update = True
513 elif len(repo.changelog) > self._loadedrevslen:
512 elif len(repo.changelog) > self._loadedrevslen:
514 update = True
513 update = True
515 if update:
514 if update:
516 try:
515 try:
517 res = self._getphaserevsnative(repo)
516 res = self._getphaserevsnative(repo)
518 self._loadedrevslen, self._phasesets = res
517 self._loadedrevslen, self._phasesets = res
519 except AttributeError:
518 except AttributeError:
520 self._computephaserevspure(repo)
519 self._computephaserevspure(repo)
521 assert self._loadedrevslen == len(repo.changelog)
520 assert self._loadedrevslen == len(repo.changelog)
522
521
523 def invalidate(self):
522 def invalidate(self):
524 self._loadedrevslen = 0
523 self._loadedrevslen = 0
525 self._phasesets = None
524 self._phasesets = None
526
525
527 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
526 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
528 # We need a repo argument here to be able to build _phasesets
527 # We need a repo argument here to be able to build _phasesets
529 # if necessary. The repository instance is not stored in
528 # if necessary. The repository instance is not stored in
530 # phasecache to avoid reference cycles. The changelog instance
529 # phasecache to avoid reference cycles. The changelog instance
531 # is not stored because it is a filecache() property and can
530 # is not stored because it is a filecache() property and can
532 # be replaced without us being notified.
531 # be replaced without us being notified.
533 if rev == nullrev:
532 if rev == nullrev:
534 return public
533 return public
535 if rev < nullrev:
534 if rev < nullrev:
536 raise ValueError(_(b'cannot lookup negative revision'))
535 raise ValueError(_(b'cannot lookup negative revision'))
537 # double check self._loadedrevslen to avoid an extra method call as
536 # double check self._loadedrevslen to avoid an extra method call as
538 # python is slow for that.
537 # python is slow for that.
539 if rev >= self._loadedrevslen:
538 if rev >= self._loadedrevslen:
540 self._ensure_phase_sets(repo)
539 self._ensure_phase_sets(repo)
541 for phase in trackedphases:
540 for phase in trackedphases:
542 if rev in self._phasesets[phase]:
541 if rev in self._phasesets[phase]:
543 return phase
542 return phase
544 return public
543 return public
545
544
546 def write(self, repo):
545 def write(self, repo):
547 if not self.dirty:
546 if not self.dirty:
548 return
547 return
549 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
548 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
550 try:
549 try:
551 self._write(repo.unfiltered(), f)
550 self._write(repo.unfiltered(), f)
552 finally:
551 finally:
553 f.close()
552 f.close()
554
553
555 def _write(self, repo, fp):
554 def _write(self, repo, fp):
556 assert repo.filtername is None
555 assert repo.filtername is None
557 to_node = repo.changelog.node
556 to_node = repo.changelog.node
558 for phase, roots in self._phaseroots.items():
557 for phase, roots in self._phaseroots.items():
559 for r in sorted(roots):
558 for r in sorted(roots):
560 h = to_node(r)
559 h = to_node(r)
561 fp.write(b'%i %s\n' % (phase, hex(h)))
560 fp.write(b'%i %s\n' % (phase, hex(h)))
562 self.dirty = False
561 self.dirty = False
563
562
564 def _updateroots(self, repo, phase, newroots, tr):
563 def _updateroots(self, repo, phase, newroots, tr):
565 self._phaseroots[phase] = newroots
564 self._phaseroots[phase] = newroots
566 self.invalidate()
565 self.invalidate()
567 self.dirty = True
566 self.dirty = True
568
567
569 assert repo.filtername is None
568 assert repo.filtername is None
570 wrepo = weakref.ref(repo)
569 wrepo = weakref.ref(repo)
571
570
572 def tr_write(fp):
571 def tr_write(fp):
573 repo = wrepo()
572 repo = wrepo()
574 assert repo is not None
573 assert repo is not None
575 self._write(repo, fp)
574 self._write(repo, fp)
576
575
577 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
576 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
578 tr.hookargs[b'phases_moved'] = b'1'
577 tr.hookargs[b'phases_moved'] = b'1'
579
578
580 def registernew(self, repo, tr, targetphase, revs):
579 def registernew(self, repo, tr, targetphase, revs):
581 repo = repo.unfiltered()
580 repo = repo.unfiltered()
582 self._retractboundary(repo, tr, targetphase, [], revs=revs)
581 self._retractboundary(repo, tr, targetphase, [], revs=revs)
583 if tr is not None and b'phases' in tr.changes:
582 if tr is not None and b'phases' in tr.changes:
584 phasetracking = tr.changes[b'phases']
583 phasetracking = tr.changes[b'phases']
585 phase = self.phase
584 phase = self.phase
586 for rev in sorted(revs):
585 for rev in sorted(revs):
587 revphase = phase(repo, rev)
586 revphase = phase(repo, rev)
588 _trackphasechange(phasetracking, rev, None, revphase)
587 _trackphasechange(phasetracking, rev, None, revphase)
589 repo.invalidatevolatilesets()
588 repo.invalidatevolatilesets()
590
589
591 def advanceboundary(
590 def advanceboundary(
592 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
591 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
593 ):
592 ):
594 """Set all 'nodes' to phase 'targetphase'
593 """Set all 'nodes' to phase 'targetphase'
595
594
596 Nodes with a phase lower than 'targetphase' are not affected.
595 Nodes with a phase lower than 'targetphase' are not affected.
597
596
598 If dryrun is True, no actions will be performed
597 If dryrun is True, no actions will be performed
599
598
600 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
601 """
600 """
602 if targetphase == public and not self.hasnonpublicphases(repo):
601 if targetphase == public and not self.hasnonpublicphases(repo):
603 return set()
602 return set()
604 # Be careful to preserve shallow-copied values: do not update
603 # Be careful to preserve shallow-copied values: do not update
605 # phaseroots values, replace them.
604 # phaseroots values, replace them.
606 if revs is None:
605 if revs is None:
607 revs = []
606 revs = []
608 if not revs and not nodes:
607 if not revs and not nodes:
609 return set()
608 return set()
610 if tr is None:
609 if tr is None:
611 phasetracking = None
610 phasetracking = None
612 else:
611 else:
613 phasetracking = tr.changes.get(b'phases')
612 phasetracking = tr.changes.get(b'phases')
614
613
615 repo = repo.unfiltered()
614 repo = repo.unfiltered()
616 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]
617
616
618 changes = set() # set of revisions to be changed
617 changes = set() # set of revisions to be changed
619 delroots = [] # set of root deleted by this path
618 delroots = [] # set of root deleted by this path
620 for phase in (phase for phase in allphases if phase > targetphase):
619 for phase in (phase for phase in allphases if phase > targetphase):
621 # filter nodes that are not in a compatible phase already
620 # filter nodes that are not in a compatible phase already
622 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]
623 if not revs:
622 if not revs:
624 break # no roots to move anymore
623 break # no roots to move anymore
625
624
626 olds = self._phaseroots[phase]
625 olds = self._phaseroots[phase]
627
626
628 affected = repo.revs(b'%ld::%ld', olds, revs)
627 affected = repo.revs(b'%ld::%ld', olds, revs)
629 changes.update(affected)
628 changes.update(affected)
630 if dryrun:
629 if dryrun:
631 continue
630 continue
632 for r in affected:
631 for r in affected:
633 _trackphasechange(
632 _trackphasechange(
634 phasetracking, r, self.phase(repo, r), targetphase
633 phasetracking, r, self.phase(repo, r), targetphase
635 )
634 )
636
635
637 roots = set(repo.revs(b'roots((%ld::) - %ld)', olds, affected))
636 roots = set(repo.revs(b'roots((%ld::) - %ld)', olds, affected))
638 if olds != roots:
637 if olds != roots:
639 self._updateroots(repo, phase, roots, tr)
638 self._updateroots(repo, phase, roots, tr)
640 # some roots may need to be declared for lower phases
639 # some roots may need to be declared for lower phases
641 delroots.extend(olds - roots)
640 delroots.extend(olds - roots)
642 if not dryrun:
641 if not dryrun:
643 # declare deleted root in the target phase
642 # declare deleted root in the target phase
644 if targetphase != 0:
643 if targetphase != 0:
645 self._retractboundary(repo, tr, targetphase, revs=delroots)
644 self._retractboundary(repo, tr, targetphase, revs=delroots)
646 repo.invalidatevolatilesets()
645 repo.invalidatevolatilesets()
647 return changes
646 return changes
648
647
649 def retractboundary(self, repo, tr, targetphase, nodes):
648 def retractboundary(self, repo, tr, targetphase, nodes):
650 if tr is None:
649 if tr is None:
651 phasetracking = None
650 phasetracking = None
652 else:
651 else:
653 phasetracking = tr.changes.get(b'phases')
652 phasetracking = tr.changes.get(b'phases')
654 repo = repo.unfiltered()
653 repo = repo.unfiltered()
655 retracted = self._retractboundary(repo, tr, targetphase, nodes)
654 retracted = self._retractboundary(repo, tr, targetphase, nodes)
656 if retracted and phasetracking is not None:
655 if retracted and phasetracking is not None:
657 for r, old_phase in sorted(retracted.items()):
656 for r, old_phase in sorted(retracted.items()):
658 _trackphasechange(phasetracking, r, old_phase, targetphase)
657 _trackphasechange(phasetracking, r, old_phase, targetphase)
659 repo.invalidatevolatilesets()
658 repo.invalidatevolatilesets()
660
659
661 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
660 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
662 if targetphase == public:
661 if targetphase == public:
663 return {}
662 return {}
664 if (
663 if (
665 targetphase == internal
664 targetphase == internal
666 and not supportinternal(repo)
665 and not supportinternal(repo)
667 or targetphase == archived
666 or targetphase == archived
668 and not supportarchived(repo)
667 and not supportarchived(repo)
669 ):
668 ):
670 name = phasenames[targetphase]
669 name = phasenames[targetphase]
671 msg = b'this repository does not support the %s phase' % name
670 msg = b'this repository does not support the %s phase' % name
672 raise error.ProgrammingError(msg)
671 raise error.ProgrammingError(msg)
673 assert repo.filtername is None
672 assert repo.filtername is None
674 cl = repo.changelog
673 cl = repo.changelog
675 torev = cl.index.rev
674 torev = cl.index.rev
676 new_revs = set()
675 new_revs = set()
677 if revs is not None:
676 if revs is not None:
678 new_revs.update(revs)
677 new_revs.update(revs)
679 if nodes is not None:
678 if nodes is not None:
680 new_revs.update(torev(node) for node in nodes)
679 new_revs.update(torev(node) for node in nodes)
681 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
682 return {} # note: why do people call retractboundary with nothing ?
681 return {} # note: why do people call retractboundary with nothing ?
683
682
684 if nullrev in new_revs:
683 if nullrev in new_revs:
685 raise error.Abort(_(b'cannot change null revision phase'))
684 raise error.Abort(_(b'cannot change null revision phase'))
686
685
687 # Compute change in phase roots by walking the graph
686 # Compute change in phase roots by walking the graph
688 #
687 #
689 # 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
690 # something even cheaper/more-bounded
689 # something even cheaper/more-bounded
691 #
690 #
692 # 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
693 # descendant with phases >= target_phase.
692 # descendant with phases >= target_phase.
694 #
693 #
695 # 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 >=
696 # target_phase or reachable though another new_revs
695 # target_phase or reachable though another new_revs
697 # 2) This detect replaced current_roots as we reach them
696 # 2) This detect replaced current_roots as we reach them
698 # 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
699 # branch.
698 # branch.
700 #
699 #
701 # 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
702 # revision to the tip to avoid missing any potential children.
701 # revision to the tip to avoid missing any potential children.
703 #
702 #
704 # 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
705 # 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.
706 parents = cl.parentrevs
705 parents = cl.parentrevs
707 phase = self.phase
706 phase = self.phase
708 new_roots = set() # roots added by this phases
707 new_roots = set() # roots added by this phases
709 changed_revs = {} # revision affected by this call
708 changed_revs = {} # revision affected by this call
710 replaced_roots = set() # older roots replaced by this call
709 replaced_roots = set() # older roots replaced by this call
711 currentroots = self._phaseroots[targetphase]
710 currentroots = self._phaseroots[targetphase]
712 start = min(new_revs)
711 start = min(new_revs)
713 end = len(cl)
712 end = len(cl)
714 rev_phases = [None] * (end - start)
713 rev_phases = [None] * (end - start)
715 for r in range(start, end):
714 for r in range(start, end):
716
715
717 # gather information about the current_rev
716 # gather information about the current_rev
718 r_phase = phase(repo, r)
717 r_phase = phase(repo, r)
719 p_phase = None # phase inherited from parents
718 p_phase = None # phase inherited from parents
720 p1, p2 = parents(r)
719 p1, p2 = parents(r)
721 if p1 >= start:
720 if p1 >= start:
722 p1_phase = rev_phases[p1 - start]
721 p1_phase = rev_phases[p1 - start]
723 if p1_phase is not None:
722 if p1_phase is not None:
724 p_phase = p1_phase
723 p_phase = p1_phase
725 if p2 >= start:
724 if p2 >= start:
726 p2_phase = rev_phases[p2 - start]
725 p2_phase = rev_phases[p2 - start]
727 if p2_phase is not None:
726 if p2_phase is not None:
728 if p_phase is not None:
727 if p_phase is not None:
729 p_phase = max(p_phase, p2_phase)
728 p_phase = max(p_phase, p2_phase)
730 else:
729 else:
731 p_phase = p2_phase
730 p_phase = p2_phase
732
731
733 # assess the situation
732 # assess the situation
734 if r in new_revs and r_phase < targetphase:
733 if r in new_revs and r_phase < targetphase:
735 if p_phase is None or p_phase < targetphase:
734 if p_phase is None or p_phase < targetphase:
736 new_roots.add(r)
735 new_roots.add(r)
737 rev_phases[r - start] = targetphase
736 rev_phases[r - start] = targetphase
738 changed_revs[r] = r_phase
737 changed_revs[r] = r_phase
739 elif p_phase is None:
738 elif p_phase is None:
740 rev_phases[r - start] = r_phase
739 rev_phases[r - start] = r_phase
741 else:
740 else:
742 if p_phase > r_phase:
741 if p_phase > r_phase:
743 rev_phases[r - start] = p_phase
742 rev_phases[r - start] = p_phase
744 else:
743 else:
745 rev_phases[r - start] = r_phase
744 rev_phases[r - start] = r_phase
746 if p_phase == targetphase:
745 if p_phase == targetphase:
747 if p_phase > r_phase:
746 if p_phase > r_phase:
748 changed_revs[r] = r_phase
747 changed_revs[r] = r_phase
749 elif r in currentroots:
748 elif r in currentroots:
750 replaced_roots.add(r)
749 replaced_roots.add(r)
751
750
752 if new_roots:
751 if new_roots:
753 assert changed_revs
752 assert changed_revs
754 final_roots = new_roots | currentroots - replaced_roots
753 final_roots = new_roots | currentroots - replaced_roots
755 self._updateroots(repo, targetphase, final_roots, tr)
754 self._updateroots(repo, targetphase, final_roots, tr)
756 if targetphase > 1:
755 if targetphase > 1:
757 retracted = set(changed_revs)
756 retracted = set(changed_revs)
758 for lower_phase in range(1, targetphase):
757 for lower_phase in range(1, targetphase):
759 lower_roots = self._phaseroots.get(lower_phase)
758 lower_roots = self._phaseroots.get(lower_phase)
760 if lower_roots is None:
759 if lower_roots is None:
761 continue
760 continue
762 if lower_roots & retracted:
761 if lower_roots & retracted:
763 simpler_roots = lower_roots - retracted
762 simpler_roots = lower_roots - retracted
764 self._updateroots(repo, lower_phase, simpler_roots, tr)
763 self._updateroots(repo, lower_phase, simpler_roots, tr)
765 return changed_revs
764 return changed_revs
766 else:
765 else:
767 assert not changed_revs
766 assert not changed_revs
768 assert not replaced_roots
767 assert not replaced_roots
769 return {}
768 return {}
770
769
771 def register_strip(
770 def register_strip(
772 self,
771 self,
773 repo,
772 repo,
774 tr,
773 tr,
775 strip_rev: int,
774 strip_rev: int,
776 ):
775 ):
777 """announce a strip to the phase cache
776 """announce a strip to the phase cache
778
777
779 Any roots higher than the stripped revision should be dropped.
778 Any roots higher than the stripped revision should be dropped.
780 """
779 """
781 for targetphase, roots in list(self._phaseroots.items()):
780 for targetphase, roots in list(self._phaseroots.items()):
782 filtered = {r for r in roots if r >= strip_rev}
781 filtered = {r for r in roots if r >= strip_rev}
783 if filtered:
782 if filtered:
784 self._updateroots(repo, targetphase, roots - filtered, tr)
783 self._updateroots(repo, targetphase, roots - filtered, tr)
785 self.invalidate()
784 self.invalidate()
786
785
787
786
788 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
787 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
789 """Add nodes to a phase changing other nodes phases if necessary.
788 """Add nodes to a phase changing other nodes phases if necessary.
790
789
791 This function move boundary *forward* this means that all nodes
790 This function move boundary *forward* this means that all nodes
792 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.
793
792
794 Simplify boundary to contains phase roots only.
793 Simplify boundary to contains phase roots only.
795
794
796 If dryrun is True, no actions will be performed
795 If dryrun is True, no actions will be performed
797
796
798 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
799 """
798 """
800 if revs is None:
799 if revs is None:
801 revs = []
800 revs = []
802 phcache = repo._phasecache.copy()
801 phcache = repo._phasecache.copy()
803 changes = phcache.advanceboundary(
802 changes = phcache.advanceboundary(
804 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
803 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
805 )
804 )
806 if not dryrun:
805 if not dryrun:
807 repo._phasecache.replace(phcache)
806 repo._phasecache.replace(phcache)
808 return changes
807 return changes
809
808
810
809
811 def retractboundary(repo, tr, targetphase, nodes):
810 def retractboundary(repo, tr, targetphase, nodes):
812 """Set nodes back to a phase changing other nodes phases if
811 """Set nodes back to a phase changing other nodes phases if
813 necessary.
812 necessary.
814
813
815 This function move boundary *backward* this means that all nodes
814 This function move boundary *backward* this means that all nodes
816 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.
817
816
818 Simplify boundary to contains phase roots only."""
817 Simplify boundary to contains phase roots only."""
819 phcache = repo._phasecache.copy()
818 phcache = repo._phasecache.copy()
820 phcache.retractboundary(repo, tr, targetphase, nodes)
819 phcache.retractboundary(repo, tr, targetphase, nodes)
821 repo._phasecache.replace(phcache)
820 repo._phasecache.replace(phcache)
822
821
823
822
824 def registernew(repo, tr, targetphase, revs):
823 def registernew(repo, tr, targetphase, revs):
825 """register a new revision and its phase
824 """register a new revision and its phase
826
825
827 Code adding revisions to the repository should use this function to
826 Code adding revisions to the repository should use this function to
828 set new changeset in their target phase (or higher).
827 set new changeset in their target phase (or higher).
829 """
828 """
830 phcache = repo._phasecache.copy()
829 phcache = repo._phasecache.copy()
831 phcache.registernew(repo, tr, targetphase, revs)
830 phcache.registernew(repo, tr, targetphase, revs)
832 repo._phasecache.replace(phcache)
831 repo._phasecache.replace(phcache)
833
832
834
833
835 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
834 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
836 """List phases root for serialization over pushkey"""
835 """List phases root for serialization over pushkey"""
837 # Use ordered dictionary so behavior is deterministic.
836 # Use ordered dictionary so behavior is deterministic.
838 keys = util.sortdict()
837 keys = util.sortdict()
839 value = b'%i' % draft
838 value = b'%i' % draft
840 cl = repo.unfiltered().changelog
839 cl = repo.unfiltered().changelog
841 to_node = cl.node
840 to_node = cl.node
842 for root in repo._phasecache._phaseroots[draft]:
841 for root in repo._phasecache._phaseroots[draft]:
843 if repo._phasecache.phase(repo, root) <= draft:
842 if repo._phasecache.phase(repo, root) <= draft:
844 keys[hex(to_node(root))] = value
843 keys[hex(to_node(root))] = value
845
844
846 if repo.publishing():
845 if repo.publishing():
847 # 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
848 # repo. Publishing repo can't just pretend they are old repo.
847 # repo. Publishing repo can't just pretend they are old repo.
849 # When pushing to a publishing repo, the client still need to
848 # When pushing to a publishing repo, the client still need to
850 # push phase boundary
849 # push phase boundary
851 #
850 #
852 # Push do not only push changeset. It also push phase data.
851 # Push do not only push changeset. It also push phase data.
853 # 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
854 # push (as they are common). Here is a very simple example:
853 # push (as they are common). Here is a very simple example:
855 #
854 #
856 # 1) repo A push changeset X as draft to repo B
855 # 1) repo A push changeset X as draft to repo B
857 # 2) repo B make changeset X public
856 # 2) repo B make changeset X public
858 # 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
859 # X as now public should
858 # X as now public should
860 #
859 #
861 # 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
862 # client phase data.
861 # client phase data.
863 keys[b'publishing'] = b'True'
862 keys[b'publishing'] = b'True'
864 return keys
863 return keys
865
864
866
865
867 def pushphase(
866 def pushphase(
868 repo: "localrepo.localrepository",
867 repo: "localrepo.localrepository",
869 nhex: bytes,
868 nhex: bytes,
870 oldphasestr: bytes,
869 oldphasestr: bytes,
871 newphasestr: bytes,
870 newphasestr: bytes,
872 ) -> bool:
871 ) -> bool:
873 """List phases root for serialization over pushkey"""
872 """List phases root for serialization over pushkey"""
874 repo = repo.unfiltered()
873 repo = repo.unfiltered()
875 with repo.lock():
874 with repo.lock():
876 currentphase = repo[nhex].phase()
875 currentphase = repo[nhex].phase()
877 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
876 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
878 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
877 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
879 if currentphase == oldphase and newphase < oldphase:
878 if currentphase == oldphase and newphase < oldphase:
880 with repo.transaction(b'pushkey-phase') as tr:
879 with repo.transaction(b'pushkey-phase') as tr:
881 advanceboundary(repo, tr, newphase, [bin(nhex)])
880 advanceboundary(repo, tr, newphase, [bin(nhex)])
882 return True
881 return True
883 elif currentphase == newphase:
882 elif currentphase == newphase:
884 # raced, but got correct result
883 # raced, but got correct result
885 return True
884 return True
886 else:
885 else:
887 return False
886 return False
888
887
889
888
890 def subsetphaseheads(repo, subset):
889 def subsetphaseheads(repo, subset):
891 """Finds the phase heads for a subset of a history
890 """Finds the phase heads for a subset of a history
892
891
893 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
894 head nodes.
893 head nodes.
895 """
894 """
896 cl = repo.changelog
895 cl = repo.changelog
897
896
898 headsbyphase = {i: [] for i in allphases}
897 headsbyphase = {i: [] for i in allphases}
899 for phase in allphases:
898 for phase in allphases:
900 revset = b"heads(%%ln & _phase(%d))" % phase
899 revset = b"heads(%%ln & _phase(%d))" % phase
901 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)]
902 return headsbyphase
901 return headsbyphase
903
902
904
903
905 def updatephases(repo, trgetter, headsbyphase):
904 def updatephases(repo, trgetter, headsbyphase):
906 """Updates the repo with the given phase heads"""
905 """Updates the repo with the given phase heads"""
907 # Now advance phase boundaries of all phases
906 # Now advance phase boundaries of all phases
908 #
907 #
909 # 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
910 # to update. This avoid creating empty transaction during no-op operation.
909 # to update. This avoid creating empty transaction during no-op operation.
911
910
912 for phase in allphases:
911 for phase in allphases:
913 revset = b'%ln - _phase(%s)'
912 revset = b'%ln - _phase(%s)'
914 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)]
915 if heads:
914 if heads:
916 advanceboundary(repo, trgetter(), phase, heads)
915 advanceboundary(repo, trgetter(), phase, heads)
917
916
918
917
919 def analyzeremotephases(repo, subset, roots):
918 def analyzeremotephases(repo, subset, roots):
920 """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
921
920
922 * subset is heads of the subset
921 * subset is heads of the subset
923 * roots is {<nodeid> => phase} mapping. key and value are string.
922 * roots is {<nodeid> => phase} mapping. key and value are string.
924
923
925 Accept unknown element input
924 Accept unknown element input
926 """
925 """
927 repo = repo.unfiltered()
926 repo = repo.unfiltered()
928 # build list from dictionary
927 # build list from dictionary
929 draftroots = []
928 draftroots = []
930 has_node = repo.changelog.index.has_node # to filter unknown nodes
929 has_node = repo.changelog.index.has_node # to filter unknown nodes
931 for nhex, phase in roots.items():
930 for nhex, phase in roots.items():
932 if nhex == b'publishing': # ignore data related to publish option
931 if nhex == b'publishing': # ignore data related to publish option
933 continue
932 continue
934 node = bin(nhex)
933 node = bin(nhex)
935 phase = int(phase)
934 phase = int(phase)
936 if phase == public:
935 if phase == public:
937 if node != repo.nullid:
936 if node != repo.nullid:
938 repo.ui.warn(
937 repo.ui.warn(
939 _(
938 _(
940 b'ignoring inconsistent public root'
939 b'ignoring inconsistent public root'
941 b' from remote: %s\n'
940 b' from remote: %s\n'
942 )
941 )
943 % nhex
942 % nhex
944 )
943 )
945 elif phase == draft:
944 elif phase == draft:
946 if has_node(node):
945 if has_node(node):
947 draftroots.append(node)
946 draftroots.append(node)
948 else:
947 else:
949 repo.ui.warn(
948 repo.ui.warn(
950 _(b'ignoring unexpected root from remote: %i %s\n')
949 _(b'ignoring unexpected root from remote: %i %s\n')
951 % (phase, nhex)
950 % (phase, nhex)
952 )
951 )
953 # compute heads
952 # compute heads
954 publicheads = newheads(repo, subset, draftroots)
953 publicheads = newheads(repo, subset, draftroots)
955 return publicheads, draftroots
954 return publicheads, draftroots
956
955
957
956
958 class remotephasessummary:
957 class remotephasessummary:
959 """summarize phase information on the remote side
958 """summarize phase information on the remote side
960
959
961 :publishing: True is the remote is publishing
960 :publishing: True is the remote is publishing
962 :publicheads: list of remote public phase heads (nodes)
961 :publicheads: list of remote public phase heads (nodes)
963 :draftheads: list of remote draft phase heads (nodes)
962 :draftheads: list of remote draft phase heads (nodes)
964 :draftroots: list of remote draft phase root (nodes)
963 :draftroots: list of remote draft phase root (nodes)
965 """
964 """
966
965
967 def __init__(self, repo, remotesubset, remoteroots):
966 def __init__(self, repo, remotesubset, remoteroots):
968 unfi = repo.unfiltered()
967 unfi = repo.unfiltered()
969 self._allremoteroots = remoteroots
968 self._allremoteroots = remoteroots
970
969
971 self.publishing = remoteroots.get(b'publishing', False)
970 self.publishing = remoteroots.get(b'publishing', False)
972
971
973 ana = analyzeremotephases(repo, remotesubset, remoteroots)
972 ana = analyzeremotephases(repo, remotesubset, remoteroots)
974 self.publicheads, self.draftroots = ana
973 self.publicheads, self.draftroots = ana
975 # Get the list of all "heads" revs draft on remote
974 # Get the list of all "heads" revs draft on remote
976 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
975 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
977 self.draftheads = [c.node() for c in dheads]
976 self.draftheads = [c.node() for c in dheads]
978
977
979
978
980 def newheads(repo, heads, roots):
979 def newheads(repo, heads, roots):
981 """compute new head of a subset minus another
980 """compute new head of a subset minus another
982
981
983 * `heads`: define the first subset
982 * `heads`: define the first subset
984 * `roots`: define the second we subtract from the first"""
983 * `roots`: define the second we subtract from the first"""
985 # prevent an import cycle
984 # prevent an import cycle
986 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
985 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
987 from . import dagop
986 from . import dagop
988
987
989 repo = repo.unfiltered()
988 repo = repo.unfiltered()
990 cl = repo.changelog
989 cl = repo.changelog
991 rev = cl.index.get_rev
990 rev = cl.index.get_rev
992 if not roots:
991 if not roots:
993 return heads
992 return heads
994 if not heads or heads == [repo.nullid]:
993 if not heads or heads == [repo.nullid]:
995 return []
994 return []
996 # The logic operated on revisions, convert arguments early for convenience
995 # The logic operated on revisions, convert arguments early for convenience
997 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}
998 roots = [rev(n) for n in roots]
997 roots = [rev(n) for n in roots]
999 # compute the area we need to remove
998 # compute the area we need to remove
1000 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
999 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1001 # heads in the area are no longer heads
1000 # heads in the area are no longer heads
1002 new_heads.difference_update(affected_zone)
1001 new_heads.difference_update(affected_zone)
1003 # revisions in the area have children outside of it,
1002 # revisions in the area have children outside of it,
1004 # They might be new heads
1003 # They might be new heads
1005 candidates = repo.revs(
1004 candidates = repo.revs(
1006 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
1007 )
1006 )
1008 candidates -= affected_zone
1007 candidates -= affected_zone
1009 if new_heads or candidates:
1008 if new_heads or candidates:
1010 # remove candidate that are ancestors of other heads
1009 # remove candidate that are ancestors of other heads
1011 new_heads.update(candidates)
1010 new_heads.update(candidates)
1012 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1011 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1013 pruned = dagop.reachableroots(repo, candidates, prunestart)
1012 pruned = dagop.reachableroots(repo, candidates, prunestart)
1014 new_heads.difference_update(pruned)
1013 new_heads.difference_update(pruned)
1015
1014
1016 return pycompat.maplist(cl.node, sorted(new_heads))
1015 return pycompat.maplist(cl.node, sorted(new_heads))
1017
1016
1018
1017
1019 def newcommitphase(ui: "uimod.ui") -> int:
1018 def newcommitphase(ui: "uimod.ui") -> int:
1020 """helper to get the target phase of new commit
1019 """helper to get the target phase of new commit
1021
1020
1022 Handle all possible values for the phases.new-commit options.
1021 Handle all possible values for the phases.new-commit options.
1023
1022
1024 """
1023 """
1025 v = ui.config(b'phases', b'new-commit')
1024 v = ui.config(b'phases', b'new-commit')
1026 try:
1025 try:
1027 return phasenumber2[v]
1026 return phasenumber2[v]
1028 except KeyError:
1027 except KeyError:
1029 raise error.ConfigError(
1028 raise error.ConfigError(
1030 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1029 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1031 )
1030 )
1032
1031
1033
1032
1034 def hassecret(repo: "localrepo.localrepository") -> bool:
1033 def hassecret(repo: "localrepo.localrepository") -> bool:
1035 """utility function that check if a repo have any secret changeset."""
1034 """utility function that check if a repo have any secret changeset."""
1036 return bool(repo._phasecache._phaseroots[secret])
1035 return bool(repo._phasecache._phaseroots[secret])
1037
1036
1038
1037
1039 def preparehookargs(
1038 def preparehookargs(
1040 node: bytes,
1039 node: bytes,
1041 old: Optional[int],
1040 old: Optional[int],
1042 new: Optional[int],
1041 new: Optional[int],
1043 ) -> Dict[bytes, bytes]:
1042 ) -> Dict[bytes, bytes]:
1044 if old is None:
1043 if old is None:
1045 old = b''
1044 old = b''
1046 else:
1045 else:
1047 old = phasenames[old]
1046 old = phasenames[old]
1048 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