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