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