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