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