##// END OF EJS Templates
phases: fast path public phase advance when everything is public...
marmoute -
r52301:330d7475 default
parent child Browse files
Show More
@@ -1,1002 +1,1004 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 if targetphase == public and not self.hasnonpublicphases(repo):
601 return set()
600 # Be careful to preserve shallow-copied values: do not update
602 # Be careful to preserve shallow-copied values: do not update
601 # phaseroots values, replace them.
603 # phaseroots values, replace them.
602 if revs is None:
604 if revs is None:
603 revs = []
605 revs = []
604 if not revs and not nodes:
606 if not revs and not nodes:
605 return set()
607 return set()
606 if tr is None:
608 if tr is None:
607 phasetracking = None
609 phasetracking = None
608 else:
610 else:
609 phasetracking = tr.changes.get(b'phases')
611 phasetracking = tr.changes.get(b'phases')
610
612
611 repo = repo.unfiltered()
613 repo = repo.unfiltered()
612 revs = [repo[n].rev() for n in nodes] + [r for r in revs]
614 revs = [repo[n].rev() for n in nodes] + [r for r in revs]
613
615
614 changes = set() # set of revisions to be changed
616 changes = set() # set of revisions to be changed
615 delroots = [] # set of root deleted by this path
617 delroots = [] # set of root deleted by this path
616 for phase in (phase for phase in allphases if phase > targetphase):
618 for phase in (phase for phase in allphases if phase > targetphase):
617 # filter nodes that are not in a compatible phase already
619 # filter nodes that are not in a compatible phase already
618 revs = [rev for rev in revs if self.phase(repo, rev) >= phase]
620 revs = [rev for rev in revs if self.phase(repo, rev) >= phase]
619 if not revs:
621 if not revs:
620 break # no roots to move anymore
622 break # no roots to move anymore
621
623
622 olds = self._phaseroots[phase]
624 olds = self._phaseroots[phase]
623
625
624 affected = repo.revs(b'%ld::%ld', olds, revs)
626 affected = repo.revs(b'%ld::%ld', olds, revs)
625 changes.update(affected)
627 changes.update(affected)
626 if dryrun:
628 if dryrun:
627 continue
629 continue
628 for r in affected:
630 for r in affected:
629 _trackphasechange(
631 _trackphasechange(
630 phasetracking, r, self.phase(repo, r), targetphase
632 phasetracking, r, self.phase(repo, r), targetphase
631 )
633 )
632
634
633 roots = set(repo.revs(b'roots((%ld::) - %ld)', olds, affected))
635 roots = set(repo.revs(b'roots((%ld::) - %ld)', olds, affected))
634 if olds != roots:
636 if olds != roots:
635 self._updateroots(repo, phase, roots, tr)
637 self._updateroots(repo, phase, roots, tr)
636 # some roots may need to be declared for lower phases
638 # some roots may need to be declared for lower phases
637 delroots.extend(olds - roots)
639 delroots.extend(olds - roots)
638 if not dryrun:
640 if not dryrun:
639 # declare deleted root in the target phase
641 # declare deleted root in the target phase
640 if targetphase != 0:
642 if targetphase != 0:
641 self._retractboundary(repo, tr, targetphase, revs=delroots)
643 self._retractboundary(repo, tr, targetphase, revs=delroots)
642 repo.invalidatevolatilesets()
644 repo.invalidatevolatilesets()
643 return changes
645 return changes
644
646
645 def retractboundary(self, repo, tr, targetphase, nodes):
647 def retractboundary(self, repo, tr, targetphase, nodes):
646 oldroots = {
648 oldroots = {
647 phase: revs
649 phase: revs
648 for phase, revs in self._phaseroots.items()
650 for phase, revs in self._phaseroots.items()
649 if phase <= targetphase
651 if phase <= targetphase
650 }
652 }
651 if tr is None:
653 if tr is None:
652 phasetracking = None
654 phasetracking = None
653 else:
655 else:
654 phasetracking = tr.changes.get(b'phases')
656 phasetracking = tr.changes.get(b'phases')
655 repo = repo.unfiltered()
657 repo = repo.unfiltered()
656 retracted = self._retractboundary(repo, tr, targetphase, nodes)
658 retracted = self._retractboundary(repo, tr, targetphase, nodes)
657 if retracted and phasetracking is not None:
659 if retracted and phasetracking is not None:
658
660
659 # find the affected revisions
661 # find the affected revisions
660 new = self._phaseroots[targetphase]
662 new = self._phaseroots[targetphase]
661 old = oldroots[targetphase]
663 old = oldroots[targetphase]
662 affected = set(repo.revs(b'(%ld::) - (%ld::)', new, old))
664 affected = set(repo.revs(b'(%ld::) - (%ld::)', new, old))
663
665
664 # find the phase of the affected revision
666 # find the phase of the affected revision
665 for phase in range(targetphase, -1, -1):
667 for phase in range(targetphase, -1, -1):
666 if phase:
668 if phase:
667 roots = oldroots.get(phase, [])
669 roots = oldroots.get(phase, [])
668 revs = set(repo.revs(b'%ld::%ld', roots, affected))
670 revs = set(repo.revs(b'%ld::%ld', roots, affected))
669 affected -= revs
671 affected -= revs
670 else: # public phase
672 else: # public phase
671 revs = affected
673 revs = affected
672 for r in sorted(revs):
674 for r in sorted(revs):
673 _trackphasechange(phasetracking, r, phase, targetphase)
675 _trackphasechange(phasetracking, r, phase, targetphase)
674 repo.invalidatevolatilesets()
676 repo.invalidatevolatilesets()
675
677
676 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
678 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
677 if targetphase == public:
679 if targetphase == public:
678 return False
680 return False
679 # Be careful to preserve shallow-copied values: do not update
681 # Be careful to preserve shallow-copied values: do not update
680 # phaseroots values, replace them.
682 # phaseroots values, replace them.
681 if revs is None:
683 if revs is None:
682 revs = []
684 revs = []
683 if nodes is None:
685 if nodes is None:
684 nodes = []
686 nodes = []
685 if not revs and not nodes:
687 if not revs and not nodes:
686 return False
688 return False
687 if (
689 if (
688 targetphase == internal
690 targetphase == internal
689 and not supportinternal(repo)
691 and not supportinternal(repo)
690 or targetphase == archived
692 or targetphase == archived
691 and not supportarchived(repo)
693 and not supportarchived(repo)
692 ):
694 ):
693 name = phasenames[targetphase]
695 name = phasenames[targetphase]
694 msg = b'this repository does not support the %s phase' % name
696 msg = b'this repository does not support the %s phase' % name
695 raise error.ProgrammingError(msg)
697 raise error.ProgrammingError(msg)
696
698
697 torev = repo.changelog.index.rev
699 torev = repo.changelog.index.rev
698 currentroots = self._phaseroots[targetphase]
700 currentroots = self._phaseroots[targetphase]
699 finalroots = oldroots = set(currentroots)
701 finalroots = oldroots = set(currentroots)
700 newroots = [torev(node) for node in nodes] + [r for r in revs]
702 newroots = [torev(node) for node in nodes] + [r for r in revs]
701 newroots = [
703 newroots = [
702 rev for rev in newroots if self.phase(repo, rev) < targetphase
704 rev for rev in newroots if self.phase(repo, rev) < targetphase
703 ]
705 ]
704
706
705 if newroots:
707 if newroots:
706 if nullrev in newroots:
708 if nullrev in newroots:
707 raise error.Abort(_(b'cannot change null revision phase'))
709 raise error.Abort(_(b'cannot change null revision phase'))
708 # do not break the CoW assumption of the shallow copy
710 # do not break the CoW assumption of the shallow copy
709 currentroots = currentroots.copy()
711 currentroots = currentroots.copy()
710 currentroots.update(newroots)
712 currentroots.update(newroots)
711
713
712 # Only compute new roots for revs above the roots that are being
714 # Only compute new roots for revs above the roots that are being
713 # retracted.
715 # retracted.
714 minnewroot = min(newroots)
716 minnewroot = min(newroots)
715 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
717 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
716 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
718 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
717
719
718 finalroots = {rev for rev in currentroots if rev < minnewroot}
720 finalroots = {rev for rev in currentroots if rev < minnewroot}
719 finalroots.update(updatedroots)
721 finalroots.update(updatedroots)
720 if finalroots != oldroots:
722 if finalroots != oldroots:
721 self._updateroots(repo, targetphase, finalroots, tr)
723 self._updateroots(repo, targetphase, finalroots, tr)
722 return True
724 return True
723 return False
725 return False
724
726
725 def register_strip(
727 def register_strip(
726 self,
728 self,
727 repo,
729 repo,
728 tr,
730 tr,
729 strip_rev: int,
731 strip_rev: int,
730 ):
732 ):
731 """announce a strip to the phase cache
733 """announce a strip to the phase cache
732
734
733 Any roots higher than the stripped revision should be dropped.
735 Any roots higher than the stripped revision should be dropped.
734 """
736 """
735 for targetphase, roots in list(self._phaseroots.items()):
737 for targetphase, roots in list(self._phaseroots.items()):
736 filtered = {r for r in roots if r >= strip_rev}
738 filtered = {r for r in roots if r >= strip_rev}
737 if filtered:
739 if filtered:
738 self._updateroots(repo, targetphase, roots - filtered, tr)
740 self._updateroots(repo, targetphase, roots - filtered, tr)
739 self.invalidate()
741 self.invalidate()
740
742
741
743
742 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
744 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
743 """Add nodes to a phase changing other nodes phases if necessary.
745 """Add nodes to a phase changing other nodes phases if necessary.
744
746
745 This function move boundary *forward* this means that all nodes
747 This function move boundary *forward* this means that all nodes
746 are set in the target phase or kept in a *lower* phase.
748 are set in the target phase or kept in a *lower* phase.
747
749
748 Simplify boundary to contains phase roots only.
750 Simplify boundary to contains phase roots only.
749
751
750 If dryrun is True, no actions will be performed
752 If dryrun is True, no actions will be performed
751
753
752 Returns a set of revs whose phase is changed or should be changed
754 Returns a set of revs whose phase is changed or should be changed
753 """
755 """
754 if revs is None:
756 if revs is None:
755 revs = []
757 revs = []
756 phcache = repo._phasecache.copy()
758 phcache = repo._phasecache.copy()
757 changes = phcache.advanceboundary(
759 changes = phcache.advanceboundary(
758 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
760 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
759 )
761 )
760 if not dryrun:
762 if not dryrun:
761 repo._phasecache.replace(phcache)
763 repo._phasecache.replace(phcache)
762 return changes
764 return changes
763
765
764
766
765 def retractboundary(repo, tr, targetphase, nodes):
767 def retractboundary(repo, tr, targetphase, nodes):
766 """Set nodes back to a phase changing other nodes phases if
768 """Set nodes back to a phase changing other nodes phases if
767 necessary.
769 necessary.
768
770
769 This function move boundary *backward* this means that all nodes
771 This function move boundary *backward* this means that all nodes
770 are set in the target phase or kept in a *higher* phase.
772 are set in the target phase or kept in a *higher* phase.
771
773
772 Simplify boundary to contains phase roots only."""
774 Simplify boundary to contains phase roots only."""
773 phcache = repo._phasecache.copy()
775 phcache = repo._phasecache.copy()
774 phcache.retractboundary(repo, tr, targetphase, nodes)
776 phcache.retractboundary(repo, tr, targetphase, nodes)
775 repo._phasecache.replace(phcache)
777 repo._phasecache.replace(phcache)
776
778
777
779
778 def registernew(repo, tr, targetphase, revs):
780 def registernew(repo, tr, targetphase, revs):
779 """register a new revision and its phase
781 """register a new revision and its phase
780
782
781 Code adding revisions to the repository should use this function to
783 Code adding revisions to the repository should use this function to
782 set new changeset in their target phase (or higher).
784 set new changeset in their target phase (or higher).
783 """
785 """
784 phcache = repo._phasecache.copy()
786 phcache = repo._phasecache.copy()
785 phcache.registernew(repo, tr, targetphase, revs)
787 phcache.registernew(repo, tr, targetphase, revs)
786 repo._phasecache.replace(phcache)
788 repo._phasecache.replace(phcache)
787
789
788
790
789 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
791 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
790 """List phases root for serialization over pushkey"""
792 """List phases root for serialization over pushkey"""
791 # Use ordered dictionary so behavior is deterministic.
793 # Use ordered dictionary so behavior is deterministic.
792 keys = util.sortdict()
794 keys = util.sortdict()
793 value = b'%i' % draft
795 value = b'%i' % draft
794 cl = repo.unfiltered().changelog
796 cl = repo.unfiltered().changelog
795 to_node = cl.node
797 to_node = cl.node
796 for root in repo._phasecache._phaseroots[draft]:
798 for root in repo._phasecache._phaseroots[draft]:
797 if repo._phasecache.phase(repo, root) <= draft:
799 if repo._phasecache.phase(repo, root) <= draft:
798 keys[hex(to_node(root))] = value
800 keys[hex(to_node(root))] = value
799
801
800 if repo.publishing():
802 if repo.publishing():
801 # Add an extra data to let remote know we are a publishing
803 # Add an extra data to let remote know we are a publishing
802 # repo. Publishing repo can't just pretend they are old repo.
804 # repo. Publishing repo can't just pretend they are old repo.
803 # When pushing to a publishing repo, the client still need to
805 # When pushing to a publishing repo, the client still need to
804 # push phase boundary
806 # push phase boundary
805 #
807 #
806 # Push do not only push changeset. It also push phase data.
808 # Push do not only push changeset. It also push phase data.
807 # New phase data may apply to common changeset which won't be
809 # New phase data may apply to common changeset which won't be
808 # push (as they are common). Here is a very simple example:
810 # push (as they are common). Here is a very simple example:
809 #
811 #
810 # 1) repo A push changeset X as draft to repo B
812 # 1) repo A push changeset X as draft to repo B
811 # 2) repo B make changeset X public
813 # 2) repo B make changeset X public
812 # 3) repo B push to repo A. X is not pushed but the data that
814 # 3) repo B push to repo A. X is not pushed but the data that
813 # X as now public should
815 # X as now public should
814 #
816 #
815 # The server can't handle it on it's own as it has no idea of
817 # The server can't handle it on it's own as it has no idea of
816 # client phase data.
818 # client phase data.
817 keys[b'publishing'] = b'True'
819 keys[b'publishing'] = b'True'
818 return keys
820 return keys
819
821
820
822
821 def pushphase(
823 def pushphase(
822 repo: "localrepo.localrepository",
824 repo: "localrepo.localrepository",
823 nhex: bytes,
825 nhex: bytes,
824 oldphasestr: bytes,
826 oldphasestr: bytes,
825 newphasestr: bytes,
827 newphasestr: bytes,
826 ) -> bool:
828 ) -> bool:
827 """List phases root for serialization over pushkey"""
829 """List phases root for serialization over pushkey"""
828 repo = repo.unfiltered()
830 repo = repo.unfiltered()
829 with repo.lock():
831 with repo.lock():
830 currentphase = repo[nhex].phase()
832 currentphase = repo[nhex].phase()
831 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
833 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
832 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
834 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
833 if currentphase == oldphase and newphase < oldphase:
835 if currentphase == oldphase and newphase < oldphase:
834 with repo.transaction(b'pushkey-phase') as tr:
836 with repo.transaction(b'pushkey-phase') as tr:
835 advanceboundary(repo, tr, newphase, [bin(nhex)])
837 advanceboundary(repo, tr, newphase, [bin(nhex)])
836 return True
838 return True
837 elif currentphase == newphase:
839 elif currentphase == newphase:
838 # raced, but got correct result
840 # raced, but got correct result
839 return True
841 return True
840 else:
842 else:
841 return False
843 return False
842
844
843
845
844 def subsetphaseheads(repo, subset):
846 def subsetphaseheads(repo, subset):
845 """Finds the phase heads for a subset of a history
847 """Finds the phase heads for a subset of a history
846
848
847 Returns a list indexed by phase number where each item is a list of phase
849 Returns a list indexed by phase number where each item is a list of phase
848 head nodes.
850 head nodes.
849 """
851 """
850 cl = repo.changelog
852 cl = repo.changelog
851
853
852 headsbyphase = {i: [] for i in allphases}
854 headsbyphase = {i: [] for i in allphases}
853 for phase in allphases:
855 for phase in allphases:
854 revset = b"heads(%%ln & _phase(%d))" % phase
856 revset = b"heads(%%ln & _phase(%d))" % phase
855 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
857 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
856 return headsbyphase
858 return headsbyphase
857
859
858
860
859 def updatephases(repo, trgetter, headsbyphase):
861 def updatephases(repo, trgetter, headsbyphase):
860 """Updates the repo with the given phase heads"""
862 """Updates the repo with the given phase heads"""
861 # Now advance phase boundaries of all phases
863 # Now advance phase boundaries of all phases
862 #
864 #
863 # run the update (and fetch transaction) only if there are actually things
865 # run the update (and fetch transaction) only if there are actually things
864 # to update. This avoid creating empty transaction during no-op operation.
866 # to update. This avoid creating empty transaction during no-op operation.
865
867
866 for phase in allphases:
868 for phase in allphases:
867 revset = b'%ln - _phase(%s)'
869 revset = b'%ln - _phase(%s)'
868 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
870 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
869 if heads:
871 if heads:
870 advanceboundary(repo, trgetter(), phase, heads)
872 advanceboundary(repo, trgetter(), phase, heads)
871
873
872
874
873 def analyzeremotephases(repo, subset, roots):
875 def analyzeremotephases(repo, subset, roots):
874 """Compute phases heads and root in a subset of node from root dict
876 """Compute phases heads and root in a subset of node from root dict
875
877
876 * subset is heads of the subset
878 * subset is heads of the subset
877 * roots is {<nodeid> => phase} mapping. key and value are string.
879 * roots is {<nodeid> => phase} mapping. key and value are string.
878
880
879 Accept unknown element input
881 Accept unknown element input
880 """
882 """
881 repo = repo.unfiltered()
883 repo = repo.unfiltered()
882 # build list from dictionary
884 # build list from dictionary
883 draftroots = []
885 draftroots = []
884 has_node = repo.changelog.index.has_node # to filter unknown nodes
886 has_node = repo.changelog.index.has_node # to filter unknown nodes
885 for nhex, phase in roots.items():
887 for nhex, phase in roots.items():
886 if nhex == b'publishing': # ignore data related to publish option
888 if nhex == b'publishing': # ignore data related to publish option
887 continue
889 continue
888 node = bin(nhex)
890 node = bin(nhex)
889 phase = int(phase)
891 phase = int(phase)
890 if phase == public:
892 if phase == public:
891 if node != repo.nullid:
893 if node != repo.nullid:
892 repo.ui.warn(
894 repo.ui.warn(
893 _(
895 _(
894 b'ignoring inconsistent public root'
896 b'ignoring inconsistent public root'
895 b' from remote: %s\n'
897 b' from remote: %s\n'
896 )
898 )
897 % nhex
899 % nhex
898 )
900 )
899 elif phase == draft:
901 elif phase == draft:
900 if has_node(node):
902 if has_node(node):
901 draftroots.append(node)
903 draftroots.append(node)
902 else:
904 else:
903 repo.ui.warn(
905 repo.ui.warn(
904 _(b'ignoring unexpected root from remote: %i %s\n')
906 _(b'ignoring unexpected root from remote: %i %s\n')
905 % (phase, nhex)
907 % (phase, nhex)
906 )
908 )
907 # compute heads
909 # compute heads
908 publicheads = newheads(repo, subset, draftroots)
910 publicheads = newheads(repo, subset, draftroots)
909 return publicheads, draftroots
911 return publicheads, draftroots
910
912
911
913
912 class remotephasessummary:
914 class remotephasessummary:
913 """summarize phase information on the remote side
915 """summarize phase information on the remote side
914
916
915 :publishing: True is the remote is publishing
917 :publishing: True is the remote is publishing
916 :publicheads: list of remote public phase heads (nodes)
918 :publicheads: list of remote public phase heads (nodes)
917 :draftheads: list of remote draft phase heads (nodes)
919 :draftheads: list of remote draft phase heads (nodes)
918 :draftroots: list of remote draft phase root (nodes)
920 :draftroots: list of remote draft phase root (nodes)
919 """
921 """
920
922
921 def __init__(self, repo, remotesubset, remoteroots):
923 def __init__(self, repo, remotesubset, remoteroots):
922 unfi = repo.unfiltered()
924 unfi = repo.unfiltered()
923 self._allremoteroots = remoteroots
925 self._allremoteroots = remoteroots
924
926
925 self.publishing = remoteroots.get(b'publishing', False)
927 self.publishing = remoteroots.get(b'publishing', False)
926
928
927 ana = analyzeremotephases(repo, remotesubset, remoteroots)
929 ana = analyzeremotephases(repo, remotesubset, remoteroots)
928 self.publicheads, self.draftroots = ana
930 self.publicheads, self.draftroots = ana
929 # Get the list of all "heads" revs draft on remote
931 # Get the list of all "heads" revs draft on remote
930 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
932 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
931 self.draftheads = [c.node() for c in dheads]
933 self.draftheads = [c.node() for c in dheads]
932
934
933
935
934 def newheads(repo, heads, roots):
936 def newheads(repo, heads, roots):
935 """compute new head of a subset minus another
937 """compute new head of a subset minus another
936
938
937 * `heads`: define the first subset
939 * `heads`: define the first subset
938 * `roots`: define the second we subtract from the first"""
940 * `roots`: define the second we subtract from the first"""
939 # prevent an import cycle
941 # prevent an import cycle
940 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
942 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
941 from . import dagop
943 from . import dagop
942
944
943 repo = repo.unfiltered()
945 repo = repo.unfiltered()
944 cl = repo.changelog
946 cl = repo.changelog
945 rev = cl.index.get_rev
947 rev = cl.index.get_rev
946 if not roots:
948 if not roots:
947 return heads
949 return heads
948 if not heads or heads == [repo.nullid]:
950 if not heads or heads == [repo.nullid]:
949 return []
951 return []
950 # The logic operated on revisions, convert arguments early for convenience
952 # The logic operated on revisions, convert arguments early for convenience
951 new_heads = {rev(n) for n in heads if n != repo.nullid}
953 new_heads = {rev(n) for n in heads if n != repo.nullid}
952 roots = [rev(n) for n in roots]
954 roots = [rev(n) for n in roots]
953 # compute the area we need to remove
955 # compute the area we need to remove
954 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
956 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
955 # heads in the area are no longer heads
957 # heads in the area are no longer heads
956 new_heads.difference_update(affected_zone)
958 new_heads.difference_update(affected_zone)
957 # revisions in the area have children outside of it,
959 # revisions in the area have children outside of it,
958 # They might be new heads
960 # They might be new heads
959 candidates = repo.revs(
961 candidates = repo.revs(
960 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
962 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
961 )
963 )
962 candidates -= affected_zone
964 candidates -= affected_zone
963 if new_heads or candidates:
965 if new_heads or candidates:
964 # remove candidate that are ancestors of other heads
966 # remove candidate that are ancestors of other heads
965 new_heads.update(candidates)
967 new_heads.update(candidates)
966 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
968 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
967 pruned = dagop.reachableroots(repo, candidates, prunestart)
969 pruned = dagop.reachableroots(repo, candidates, prunestart)
968 new_heads.difference_update(pruned)
970 new_heads.difference_update(pruned)
969
971
970 return pycompat.maplist(cl.node, sorted(new_heads))
972 return pycompat.maplist(cl.node, sorted(new_heads))
971
973
972
974
973 def newcommitphase(ui: "uimod.ui") -> int:
975 def newcommitphase(ui: "uimod.ui") -> int:
974 """helper to get the target phase of new commit
976 """helper to get the target phase of new commit
975
977
976 Handle all possible values for the phases.new-commit options.
978 Handle all possible values for the phases.new-commit options.
977
979
978 """
980 """
979 v = ui.config(b'phases', b'new-commit')
981 v = ui.config(b'phases', b'new-commit')
980 try:
982 try:
981 return phasenumber2[v]
983 return phasenumber2[v]
982 except KeyError:
984 except KeyError:
983 raise error.ConfigError(
985 raise error.ConfigError(
984 _(b"phases.new-commit: not a valid phase name ('%s')") % v
986 _(b"phases.new-commit: not a valid phase name ('%s')") % v
985 )
987 )
986
988
987
989
988 def hassecret(repo: "localrepo.localrepository") -> bool:
990 def hassecret(repo: "localrepo.localrepository") -> bool:
989 """utility function that check if a repo have any secret changeset."""
991 """utility function that check if a repo have any secret changeset."""
990 return bool(repo._phasecache._phaseroots[secret])
992 return bool(repo._phasecache._phaseroots[secret])
991
993
992
994
993 def preparehookargs(
995 def preparehookargs(
994 node: bytes,
996 node: bytes,
995 old: Optional[int],
997 old: Optional[int],
996 new: Optional[int],
998 new: Optional[int],
997 ) -> Dict[bytes, bytes]:
999 ) -> Dict[bytes, bytes]:
998 if old is None:
1000 if old is None:
999 old = b''
1001 old = b''
1000 else:
1002 else:
1001 old = phasenames[old]
1003 old = phasenames[old]
1002 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
1004 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