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