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