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