##// END OF EJS Templates
phases: reduce code duplication in phasecache.getrevset...
Rodrigo Damazio Bovendorp -
r44521:8eb3c523 default
parent child Browse files
Show More
@@ -1,811 +1,808 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 _trackphasechange(data, rev, old, new):
219 def _trackphasechange(data, rev, old, new):
220 """add a phase move the <data> dictionnary
220 """add a phase move the <data> dictionnary
221
221
222 If data is None, nothing happens.
222 If data is None, nothing happens.
223 """
223 """
224 if data is None:
224 if data is None:
225 return
225 return
226 existing = data.get(rev)
226 existing = data.get(rev)
227 if existing is not None:
227 if existing is not None:
228 old = existing[0]
228 old = existing[0]
229 data[rev] = (old, new)
229 data[rev] = (old, new)
230
230
231
231
232 class phasecache(object):
232 class phasecache(object):
233 def __init__(self, repo, phasedefaults, _load=True):
233 def __init__(self, repo, phasedefaults, _load=True):
234 if _load:
234 if _load:
235 # Cheap trick to allow shallow-copy without copy module
235 # Cheap trick to allow shallow-copy without copy module
236 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
236 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
237 self._loadedrevslen = 0
237 self._loadedrevslen = 0
238 self._phasesets = None
238 self._phasesets = None
239 self.filterunknown(repo)
239 self.filterunknown(repo)
240 self.opener = repo.svfs
240 self.opener = repo.svfs
241
241
242 def getrevset(self, repo, phases, subset=None):
242 def getrevset(self, repo, phases, subset=None):
243 """return a smartset for the given phases"""
243 """return a smartset for the given phases"""
244 self.loadphaserevs(repo) # ensure phase's sets are loaded
244 self.loadphaserevs(repo) # ensure phase's sets are loaded
245 phases = set(phases)
245 phases = set(phases)
246 publicphase = public in phases
246
247
247 if public not in phases:
248 if publicphase:
248 # fast path: _phasesets contains the interesting sets,
249 # In this case, phases keeps all the *other* phases.
249 # might only need a union and post-filtering.
250 phases = set(allphases).difference(phases)
250 if len(phases) == 1:
251 if not phases:
251 [p] = phases
252 return smartset.fullreposet(repo)
252 revs = self._phasesets[p]
253
253 else:
254 # fast path: _phasesets contains the interesting sets,
254 revs = set.union(*[self._phasesets[p] for p in phases])
255 # might only need a union and post-filtering.
256 if len(phases) == 1:
257 [p] = phases
258 revs = self._phasesets[p]
259 else:
260 # revs has the revisions in all *other* phases.
261 revs = set.union(*[self._phasesets[p] for p in phases])
262
263 def _addwdir(wdirsubset, wdirrevs):
264 if wdirrev in wdirsubset and repo[None].phase() in phases:
265 # The working dir would never be in the # cache, but it was in
266 # the subset being filtered for its phase (or filtered out,
267 # depending on publicphase), so add it to the output to be
268 # included (or filtered out).
269 wdirrevs.add(wdirrev)
270 return wdirrevs
271
272 if not publicphase:
255 if repo.changelog.filteredrevs:
273 if repo.changelog.filteredrevs:
256 revs = revs - repo.changelog.filteredrevs
274 revs = revs - repo.changelog.filteredrevs
257
275
258 if subset is None:
276 if subset is None:
259 return smartset.baseset(revs)
277 return smartset.baseset(revs)
260 else:
278 else:
261 if wdirrev in subset and repo[None].phase() in phases:
279 revs = _addwdir(subset, revs)
262 # The working dir would never be in the cache, but it was
263 # in the subset being filtered for its phase, so add it to
264 # the output.
265 revs.add(wdirrev)
266
267 return subset & smartset.baseset(revs)
280 return subset & smartset.baseset(revs)
268 else:
281 else:
269 # phases keeps all the *other* phases.
270 phases = set(allphases).difference(phases)
271 if not phases:
272 return smartset.fullreposet(repo)
273
274 # revs has the revisions in all *other* phases.
275 if len(phases) == 1:
276 [p] = phases
277 revs = self._phasesets[p]
278 else:
279 revs = set.union(*[self._phasesets[p] for p in phases])
280
281 if subset is None:
282 if subset is None:
282 subset = smartset.fullreposet(repo)
283 subset = smartset.fullreposet(repo)
283
284
284 if wdirrev in subset and repo[None].phase() in phases:
285 revs = _addwdir(subset, revs)
285 # The working dir is in the subset being filtered, and its
286 # phase is in the phases *not* being returned, so add it to the
287 # set of revisions to filter out.
288 revs.add(wdirrev)
289
286
290 if not revs:
287 if not revs:
291 return subset
288 return subset
292 return subset.filter(lambda r: r not in revs)
289 return subset.filter(lambda r: r not in revs)
293
290
294 def copy(self):
291 def copy(self):
295 # Shallow copy meant to ensure isolation in
292 # Shallow copy meant to ensure isolation in
296 # advance/retractboundary(), nothing more.
293 # advance/retractboundary(), nothing more.
297 ph = self.__class__(None, None, _load=False)
294 ph = self.__class__(None, None, _load=False)
298 ph.phaseroots = self.phaseroots[:]
295 ph.phaseroots = self.phaseroots[:]
299 ph.dirty = self.dirty
296 ph.dirty = self.dirty
300 ph.opener = self.opener
297 ph.opener = self.opener
301 ph._loadedrevslen = self._loadedrevslen
298 ph._loadedrevslen = self._loadedrevslen
302 ph._phasesets = self._phasesets
299 ph._phasesets = self._phasesets
303 return ph
300 return ph
304
301
305 def replace(self, phcache):
302 def replace(self, phcache):
306 """replace all values in 'self' with content of phcache"""
303 """replace all values in 'self' with content of phcache"""
307 for a in (
304 for a in (
308 b'phaseroots',
305 b'phaseroots',
309 b'dirty',
306 b'dirty',
310 b'opener',
307 b'opener',
311 b'_loadedrevslen',
308 b'_loadedrevslen',
312 b'_phasesets',
309 b'_phasesets',
313 ):
310 ):
314 setattr(self, a, getattr(phcache, a))
311 setattr(self, a, getattr(phcache, a))
315
312
316 def _getphaserevsnative(self, repo):
313 def _getphaserevsnative(self, repo):
317 repo = repo.unfiltered()
314 repo = repo.unfiltered()
318 nativeroots = []
315 nativeroots = []
319 for phase in trackedphases:
316 for phase in trackedphases:
320 nativeroots.append(
317 nativeroots.append(
321 pycompat.maplist(repo.changelog.rev, self.phaseroots[phase])
318 pycompat.maplist(repo.changelog.rev, self.phaseroots[phase])
322 )
319 )
323 return repo.changelog.computephases(nativeroots)
320 return repo.changelog.computephases(nativeroots)
324
321
325 def _computephaserevspure(self, repo):
322 def _computephaserevspure(self, repo):
326 repo = repo.unfiltered()
323 repo = repo.unfiltered()
327 cl = repo.changelog
324 cl = repo.changelog
328 self._phasesets = [set() for phase in allphases]
325 self._phasesets = [set() for phase in allphases]
329 lowerroots = set()
326 lowerroots = set()
330 for phase in reversed(trackedphases):
327 for phase in reversed(trackedphases):
331 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
328 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
332 if roots:
329 if roots:
333 ps = set(cl.descendants(roots))
330 ps = set(cl.descendants(roots))
334 for root in roots:
331 for root in roots:
335 ps.add(root)
332 ps.add(root)
336 ps.difference_update(lowerroots)
333 ps.difference_update(lowerroots)
337 lowerroots.update(ps)
334 lowerroots.update(ps)
338 self._phasesets[phase] = ps
335 self._phasesets[phase] = ps
339 self._loadedrevslen = len(cl)
336 self._loadedrevslen = len(cl)
340
337
341 def loadphaserevs(self, repo):
338 def loadphaserevs(self, repo):
342 """ensure phase information is loaded in the object"""
339 """ensure phase information is loaded in the object"""
343 if self._phasesets is None:
340 if self._phasesets is None:
344 try:
341 try:
345 res = self._getphaserevsnative(repo)
342 res = self._getphaserevsnative(repo)
346 self._loadedrevslen, self._phasesets = res
343 self._loadedrevslen, self._phasesets = res
347 except AttributeError:
344 except AttributeError:
348 self._computephaserevspure(repo)
345 self._computephaserevspure(repo)
349
346
350 def invalidate(self):
347 def invalidate(self):
351 self._loadedrevslen = 0
348 self._loadedrevslen = 0
352 self._phasesets = None
349 self._phasesets = None
353
350
354 def phase(self, repo, rev):
351 def phase(self, repo, rev):
355 # We need a repo argument here to be able to build _phasesets
352 # We need a repo argument here to be able to build _phasesets
356 # if necessary. The repository instance is not stored in
353 # if necessary. The repository instance is not stored in
357 # phasecache to avoid reference cycles. The changelog instance
354 # phasecache to avoid reference cycles. The changelog instance
358 # is not stored because it is a filecache() property and can
355 # is not stored because it is a filecache() property and can
359 # be replaced without us being notified.
356 # be replaced without us being notified.
360 if rev == nullrev:
357 if rev == nullrev:
361 return public
358 return public
362 if rev < nullrev:
359 if rev < nullrev:
363 raise ValueError(_(b'cannot lookup negative revision'))
360 raise ValueError(_(b'cannot lookup negative revision'))
364 if rev >= self._loadedrevslen:
361 if rev >= self._loadedrevslen:
365 self.invalidate()
362 self.invalidate()
366 self.loadphaserevs(repo)
363 self.loadphaserevs(repo)
367 for phase in trackedphases:
364 for phase in trackedphases:
368 if rev in self._phasesets[phase]:
365 if rev in self._phasesets[phase]:
369 return phase
366 return phase
370 return public
367 return public
371
368
372 def write(self):
369 def write(self):
373 if not self.dirty:
370 if not self.dirty:
374 return
371 return
375 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
372 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
376 try:
373 try:
377 self._write(f)
374 self._write(f)
378 finally:
375 finally:
379 f.close()
376 f.close()
380
377
381 def _write(self, fp):
378 def _write(self, fp):
382 for phase, roots in enumerate(self.phaseroots):
379 for phase, roots in enumerate(self.phaseroots):
383 for h in sorted(roots):
380 for h in sorted(roots):
384 fp.write(b'%i %s\n' % (phase, hex(h)))
381 fp.write(b'%i %s\n' % (phase, hex(h)))
385 self.dirty = False
382 self.dirty = False
386
383
387 def _updateroots(self, phase, newroots, tr):
384 def _updateroots(self, phase, newroots, tr):
388 self.phaseroots[phase] = newroots
385 self.phaseroots[phase] = newroots
389 self.invalidate()
386 self.invalidate()
390 self.dirty = True
387 self.dirty = True
391
388
392 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
389 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
393 tr.hookargs[b'phases_moved'] = b'1'
390 tr.hookargs[b'phases_moved'] = b'1'
394
391
395 def registernew(self, repo, tr, targetphase, nodes):
392 def registernew(self, repo, tr, targetphase, nodes):
396 repo = repo.unfiltered()
393 repo = repo.unfiltered()
397 self._retractboundary(repo, tr, targetphase, nodes)
394 self._retractboundary(repo, tr, targetphase, nodes)
398 if tr is not None and b'phases' in tr.changes:
395 if tr is not None and b'phases' in tr.changes:
399 phasetracking = tr.changes[b'phases']
396 phasetracking = tr.changes[b'phases']
400 torev = repo.changelog.rev
397 torev = repo.changelog.rev
401 phase = self.phase
398 phase = self.phase
402 for n in nodes:
399 for n in nodes:
403 rev = torev(n)
400 rev = torev(n)
404 revphase = phase(repo, rev)
401 revphase = phase(repo, rev)
405 _trackphasechange(phasetracking, rev, None, revphase)
402 _trackphasechange(phasetracking, rev, None, revphase)
406 repo.invalidatevolatilesets()
403 repo.invalidatevolatilesets()
407
404
408 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
405 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
409 """Set all 'nodes' to phase 'targetphase'
406 """Set all 'nodes' to phase 'targetphase'
410
407
411 Nodes with a phase lower than 'targetphase' are not affected.
408 Nodes with a phase lower than 'targetphase' are not affected.
412
409
413 If dryrun is True, no actions will be performed
410 If dryrun is True, no actions will be performed
414
411
415 Returns a set of revs whose phase is changed or should be changed
412 Returns a set of revs whose phase is changed or should be changed
416 """
413 """
417 # Be careful to preserve shallow-copied values: do not update
414 # Be careful to preserve shallow-copied values: do not update
418 # phaseroots values, replace them.
415 # phaseroots values, replace them.
419 if tr is None:
416 if tr is None:
420 phasetracking = None
417 phasetracking = None
421 else:
418 else:
422 phasetracking = tr.changes.get(b'phases')
419 phasetracking = tr.changes.get(b'phases')
423
420
424 repo = repo.unfiltered()
421 repo = repo.unfiltered()
425
422
426 changes = set() # set of revisions to be changed
423 changes = set() # set of revisions to be changed
427 delroots = [] # set of root deleted by this path
424 delroots = [] # set of root deleted by this path
428 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
425 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
429 # filter nodes that are not in a compatible phase already
426 # filter nodes that are not in a compatible phase already
430 nodes = [
427 nodes = [
431 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
428 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
432 ]
429 ]
433 if not nodes:
430 if not nodes:
434 break # no roots to move anymore
431 break # no roots to move anymore
435
432
436 olds = self.phaseroots[phase]
433 olds = self.phaseroots[phase]
437
434
438 affected = repo.revs(b'%ln::%ln', olds, nodes)
435 affected = repo.revs(b'%ln::%ln', olds, nodes)
439 changes.update(affected)
436 changes.update(affected)
440 if dryrun:
437 if dryrun:
441 continue
438 continue
442 for r in affected:
439 for r in affected:
443 _trackphasechange(
440 _trackphasechange(
444 phasetracking, r, self.phase(repo, r), targetphase
441 phasetracking, r, self.phase(repo, r), targetphase
445 )
442 )
446
443
447 roots = set(
444 roots = set(
448 ctx.node()
445 ctx.node()
449 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
446 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
450 )
447 )
451 if olds != roots:
448 if olds != roots:
452 self._updateroots(phase, roots, tr)
449 self._updateroots(phase, roots, tr)
453 # some roots may need to be declared for lower phases
450 # some roots may need to be declared for lower phases
454 delroots.extend(olds - roots)
451 delroots.extend(olds - roots)
455 if not dryrun:
452 if not dryrun:
456 # declare deleted root in the target phase
453 # declare deleted root in the target phase
457 if targetphase != 0:
454 if targetphase != 0:
458 self._retractboundary(repo, tr, targetphase, delroots)
455 self._retractboundary(repo, tr, targetphase, delroots)
459 repo.invalidatevolatilesets()
456 repo.invalidatevolatilesets()
460 return changes
457 return changes
461
458
462 def retractboundary(self, repo, tr, targetphase, nodes):
459 def retractboundary(self, repo, tr, targetphase, nodes):
463 oldroots = self.phaseroots[: targetphase + 1]
460 oldroots = self.phaseroots[: targetphase + 1]
464 if tr is None:
461 if tr is None:
465 phasetracking = None
462 phasetracking = None
466 else:
463 else:
467 phasetracking = tr.changes.get(b'phases')
464 phasetracking = tr.changes.get(b'phases')
468 repo = repo.unfiltered()
465 repo = repo.unfiltered()
469 if (
466 if (
470 self._retractboundary(repo, tr, targetphase, nodes)
467 self._retractboundary(repo, tr, targetphase, nodes)
471 and phasetracking is not None
468 and phasetracking is not None
472 ):
469 ):
473
470
474 # find the affected revisions
471 # find the affected revisions
475 new = self.phaseroots[targetphase]
472 new = self.phaseroots[targetphase]
476 old = oldroots[targetphase]
473 old = oldroots[targetphase]
477 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
474 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
478
475
479 # find the phase of the affected revision
476 # find the phase of the affected revision
480 for phase in pycompat.xrange(targetphase, -1, -1):
477 for phase in pycompat.xrange(targetphase, -1, -1):
481 if phase:
478 if phase:
482 roots = oldroots[phase]
479 roots = oldroots[phase]
483 revs = set(repo.revs(b'%ln::%ld', roots, affected))
480 revs = set(repo.revs(b'%ln::%ld', roots, affected))
484 affected -= revs
481 affected -= revs
485 else: # public phase
482 else: # public phase
486 revs = affected
483 revs = affected
487 for r in revs:
484 for r in revs:
488 _trackphasechange(phasetracking, r, phase, targetphase)
485 _trackphasechange(phasetracking, r, phase, targetphase)
489 repo.invalidatevolatilesets()
486 repo.invalidatevolatilesets()
490
487
491 def _retractboundary(self, repo, tr, targetphase, nodes):
488 def _retractboundary(self, repo, tr, targetphase, nodes):
492 # Be careful to preserve shallow-copied values: do not update
489 # Be careful to preserve shallow-copied values: do not update
493 # phaseroots values, replace them.
490 # phaseroots values, replace them.
494 if targetphase in (archived, internal) and not supportinternal(repo):
491 if targetphase in (archived, internal) and not supportinternal(repo):
495 name = phasenames[targetphase]
492 name = phasenames[targetphase]
496 msg = b'this repository does not support the %s phase' % name
493 msg = b'this repository does not support the %s phase' % name
497 raise error.ProgrammingError(msg)
494 raise error.ProgrammingError(msg)
498
495
499 repo = repo.unfiltered()
496 repo = repo.unfiltered()
500 currentroots = self.phaseroots[targetphase]
497 currentroots = self.phaseroots[targetphase]
501 finalroots = oldroots = set(currentroots)
498 finalroots = oldroots = set(currentroots)
502 newroots = [
499 newroots = [
503 n for n in nodes if self.phase(repo, repo[n].rev()) < targetphase
500 n for n in nodes if self.phase(repo, repo[n].rev()) < targetphase
504 ]
501 ]
505 if newroots:
502 if newroots:
506
503
507 if nullid in newroots:
504 if nullid in newroots:
508 raise error.Abort(_(b'cannot change null revision phase'))
505 raise error.Abort(_(b'cannot change null revision phase'))
509 currentroots = currentroots.copy()
506 currentroots = currentroots.copy()
510 currentroots.update(newroots)
507 currentroots.update(newroots)
511
508
512 # Only compute new roots for revs above the roots that are being
509 # Only compute new roots for revs above the roots that are being
513 # retracted.
510 # retracted.
514 minnewroot = min(repo[n].rev() for n in newroots)
511 minnewroot = min(repo[n].rev() for n in newroots)
515 aboveroots = [
512 aboveroots = [
516 n for n in currentroots if repo[n].rev() >= minnewroot
513 n for n in currentroots if repo[n].rev() >= minnewroot
517 ]
514 ]
518 updatedroots = repo.set(b'roots(%ln::)', aboveroots)
515 updatedroots = repo.set(b'roots(%ln::)', aboveroots)
519
516
520 finalroots = set(
517 finalroots = set(
521 n for n in currentroots if repo[n].rev() < minnewroot
518 n for n in currentroots if repo[n].rev() < minnewroot
522 )
519 )
523 finalroots.update(ctx.node() for ctx in updatedroots)
520 finalroots.update(ctx.node() for ctx in updatedroots)
524 if finalroots != oldroots:
521 if finalroots != oldroots:
525 self._updateroots(targetphase, finalroots, tr)
522 self._updateroots(targetphase, finalroots, tr)
526 return True
523 return True
527 return False
524 return False
528
525
529 def filterunknown(self, repo):
526 def filterunknown(self, repo):
530 """remove unknown nodes from the phase boundary
527 """remove unknown nodes from the phase boundary
531
528
532 Nothing is lost as unknown nodes only hold data for their descendants.
529 Nothing is lost as unknown nodes only hold data for their descendants.
533 """
530 """
534 filtered = False
531 filtered = False
535 has_node = repo.changelog.index.has_node # to filter unknown nodes
532 has_node = repo.changelog.index.has_node # to filter unknown nodes
536 for phase, nodes in enumerate(self.phaseroots):
533 for phase, nodes in enumerate(self.phaseroots):
537 missing = sorted(node for node in nodes if not has_node(node))
534 missing = sorted(node for node in nodes if not has_node(node))
538 if missing:
535 if missing:
539 for mnode in missing:
536 for mnode in missing:
540 repo.ui.debug(
537 repo.ui.debug(
541 b'removing unknown node %s from %i-phase boundary\n'
538 b'removing unknown node %s from %i-phase boundary\n'
542 % (short(mnode), phase)
539 % (short(mnode), phase)
543 )
540 )
544 nodes.symmetric_difference_update(missing)
541 nodes.symmetric_difference_update(missing)
545 filtered = True
542 filtered = True
546 if filtered:
543 if filtered:
547 self.dirty = True
544 self.dirty = True
548 # filterunknown is called by repo.destroyed, we may have no changes in
545 # filterunknown is called by repo.destroyed, we may have no changes in
549 # root but _phasesets contents is certainly invalid (or at least we
546 # root but _phasesets contents is certainly invalid (or at least we
550 # have not proper way to check that). related to issue 3858.
547 # have not proper way to check that). related to issue 3858.
551 #
548 #
552 # The other caller is __init__ that have no _phasesets initialized
549 # The other caller is __init__ that have no _phasesets initialized
553 # anyway. If this change we should consider adding a dedicated
550 # anyway. If this change we should consider adding a dedicated
554 # "destroyed" function to phasecache or a proper cache key mechanism
551 # "destroyed" function to phasecache or a proper cache key mechanism
555 # (see branchmap one)
552 # (see branchmap one)
556 self.invalidate()
553 self.invalidate()
557
554
558
555
559 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
556 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
560 """Add nodes to a phase changing other nodes phases if necessary.
557 """Add nodes to a phase changing other nodes phases if necessary.
561
558
562 This function move boundary *forward* this means that all nodes
559 This function move boundary *forward* this means that all nodes
563 are set in the target phase or kept in a *lower* phase.
560 are set in the target phase or kept in a *lower* phase.
564
561
565 Simplify boundary to contains phase roots only.
562 Simplify boundary to contains phase roots only.
566
563
567 If dryrun is True, no actions will be performed
564 If dryrun is True, no actions will be performed
568
565
569 Returns a set of revs whose phase is changed or should be changed
566 Returns a set of revs whose phase is changed or should be changed
570 """
567 """
571 phcache = repo._phasecache.copy()
568 phcache = repo._phasecache.copy()
572 changes = phcache.advanceboundary(
569 changes = phcache.advanceboundary(
573 repo, tr, targetphase, nodes, dryrun=dryrun
570 repo, tr, targetphase, nodes, dryrun=dryrun
574 )
571 )
575 if not dryrun:
572 if not dryrun:
576 repo._phasecache.replace(phcache)
573 repo._phasecache.replace(phcache)
577 return changes
574 return changes
578
575
579
576
580 def retractboundary(repo, tr, targetphase, nodes):
577 def retractboundary(repo, tr, targetphase, nodes):
581 """Set nodes back to a phase changing other nodes phases if
578 """Set nodes back to a phase changing other nodes phases if
582 necessary.
579 necessary.
583
580
584 This function move boundary *backward* this means that all nodes
581 This function move boundary *backward* this means that all nodes
585 are set in the target phase or kept in a *higher* phase.
582 are set in the target phase or kept in a *higher* phase.
586
583
587 Simplify boundary to contains phase roots only."""
584 Simplify boundary to contains phase roots only."""
588 phcache = repo._phasecache.copy()
585 phcache = repo._phasecache.copy()
589 phcache.retractboundary(repo, tr, targetphase, nodes)
586 phcache.retractboundary(repo, tr, targetphase, nodes)
590 repo._phasecache.replace(phcache)
587 repo._phasecache.replace(phcache)
591
588
592
589
593 def registernew(repo, tr, targetphase, nodes):
590 def registernew(repo, tr, targetphase, nodes):
594 """register a new revision and its phase
591 """register a new revision and its phase
595
592
596 Code adding revisions to the repository should use this function to
593 Code adding revisions to the repository should use this function to
597 set new changeset in their target phase (or higher).
594 set new changeset in their target phase (or higher).
598 """
595 """
599 phcache = repo._phasecache.copy()
596 phcache = repo._phasecache.copy()
600 phcache.registernew(repo, tr, targetphase, nodes)
597 phcache.registernew(repo, tr, targetphase, nodes)
601 repo._phasecache.replace(phcache)
598 repo._phasecache.replace(phcache)
602
599
603
600
604 def listphases(repo):
601 def listphases(repo):
605 """List phases root for serialization over pushkey"""
602 """List phases root for serialization over pushkey"""
606 # Use ordered dictionary so behavior is deterministic.
603 # Use ordered dictionary so behavior is deterministic.
607 keys = util.sortdict()
604 keys = util.sortdict()
608 value = b'%i' % draft
605 value = b'%i' % draft
609 cl = repo.unfiltered().changelog
606 cl = repo.unfiltered().changelog
610 for root in repo._phasecache.phaseroots[draft]:
607 for root in repo._phasecache.phaseroots[draft]:
611 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
608 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
612 keys[hex(root)] = value
609 keys[hex(root)] = value
613
610
614 if repo.publishing():
611 if repo.publishing():
615 # Add an extra data to let remote know we are a publishing
612 # Add an extra data to let remote know we are a publishing
616 # repo. Publishing repo can't just pretend they are old repo.
613 # repo. Publishing repo can't just pretend they are old repo.
617 # When pushing to a publishing repo, the client still need to
614 # When pushing to a publishing repo, the client still need to
618 # push phase boundary
615 # push phase boundary
619 #
616 #
620 # Push do not only push changeset. It also push phase data.
617 # Push do not only push changeset. It also push phase data.
621 # New phase data may apply to common changeset which won't be
618 # New phase data may apply to common changeset which won't be
622 # push (as they are common). Here is a very simple example:
619 # push (as they are common). Here is a very simple example:
623 #
620 #
624 # 1) repo A push changeset X as draft to repo B
621 # 1) repo A push changeset X as draft to repo B
625 # 2) repo B make changeset X public
622 # 2) repo B make changeset X public
626 # 3) repo B push to repo A. X is not pushed but the data that
623 # 3) repo B push to repo A. X is not pushed but the data that
627 # X as now public should
624 # X as now public should
628 #
625 #
629 # The server can't handle it on it's own as it has no idea of
626 # The server can't handle it on it's own as it has no idea of
630 # client phase data.
627 # client phase data.
631 keys[b'publishing'] = b'True'
628 keys[b'publishing'] = b'True'
632 return keys
629 return keys
633
630
634
631
635 def pushphase(repo, nhex, oldphasestr, newphasestr):
632 def pushphase(repo, nhex, oldphasestr, newphasestr):
636 """List phases root for serialization over pushkey"""
633 """List phases root for serialization over pushkey"""
637 repo = repo.unfiltered()
634 repo = repo.unfiltered()
638 with repo.lock():
635 with repo.lock():
639 currentphase = repo[nhex].phase()
636 currentphase = repo[nhex].phase()
640 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
637 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
641 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
638 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
642 if currentphase == oldphase and newphase < oldphase:
639 if currentphase == oldphase and newphase < oldphase:
643 with repo.transaction(b'pushkey-phase') as tr:
640 with repo.transaction(b'pushkey-phase') as tr:
644 advanceboundary(repo, tr, newphase, [bin(nhex)])
641 advanceboundary(repo, tr, newphase, [bin(nhex)])
645 return True
642 return True
646 elif currentphase == newphase:
643 elif currentphase == newphase:
647 # raced, but got correct result
644 # raced, but got correct result
648 return True
645 return True
649 else:
646 else:
650 return False
647 return False
651
648
652
649
653 def subsetphaseheads(repo, subset):
650 def subsetphaseheads(repo, subset):
654 """Finds the phase heads for a subset of a history
651 """Finds the phase heads for a subset of a history
655
652
656 Returns a list indexed by phase number where each item is a list of phase
653 Returns a list indexed by phase number where each item is a list of phase
657 head nodes.
654 head nodes.
658 """
655 """
659 cl = repo.changelog
656 cl = repo.changelog
660
657
661 headsbyphase = [[] for i in allphases]
658 headsbyphase = [[] for i in allphases]
662 # No need to keep track of secret phase; any heads in the subset that
659 # No need to keep track of secret phase; any heads in the subset that
663 # are not mentioned are implicitly secret.
660 # are not mentioned are implicitly secret.
664 for phase in allphases[:secret]:
661 for phase in allphases[:secret]:
665 revset = b"heads(%%ln & %s())" % phasenames[phase]
662 revset = b"heads(%%ln & %s())" % phasenames[phase]
666 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
663 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
667 return headsbyphase
664 return headsbyphase
668
665
669
666
670 def updatephases(repo, trgetter, headsbyphase):
667 def updatephases(repo, trgetter, headsbyphase):
671 """Updates the repo with the given phase heads"""
668 """Updates the repo with the given phase heads"""
672 # Now advance phase boundaries of all but secret phase
669 # Now advance phase boundaries of all but secret phase
673 #
670 #
674 # run the update (and fetch transaction) only if there are actually things
671 # run the update (and fetch transaction) only if there are actually things
675 # to update. This avoid creating empty transaction during no-op operation.
672 # to update. This avoid creating empty transaction during no-op operation.
676
673
677 for phase in allphases[:-1]:
674 for phase in allphases[:-1]:
678 revset = b'%ln - _phase(%s)'
675 revset = b'%ln - _phase(%s)'
679 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
676 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
680 if heads:
677 if heads:
681 advanceboundary(repo, trgetter(), phase, heads)
678 advanceboundary(repo, trgetter(), phase, heads)
682
679
683
680
684 def analyzeremotephases(repo, subset, roots):
681 def analyzeremotephases(repo, subset, roots):
685 """Compute phases heads and root in a subset of node from root dict
682 """Compute phases heads and root in a subset of node from root dict
686
683
687 * subset is heads of the subset
684 * subset is heads of the subset
688 * roots is {<nodeid> => phase} mapping. key and value are string.
685 * roots is {<nodeid> => phase} mapping. key and value are string.
689
686
690 Accept unknown element input
687 Accept unknown element input
691 """
688 """
692 repo = repo.unfiltered()
689 repo = repo.unfiltered()
693 # build list from dictionary
690 # build list from dictionary
694 draftroots = []
691 draftroots = []
695 has_node = repo.changelog.index.has_node # to filter unknown nodes
692 has_node = repo.changelog.index.has_node # to filter unknown nodes
696 for nhex, phase in pycompat.iteritems(roots):
693 for nhex, phase in pycompat.iteritems(roots):
697 if nhex == b'publishing': # ignore data related to publish option
694 if nhex == b'publishing': # ignore data related to publish option
698 continue
695 continue
699 node = bin(nhex)
696 node = bin(nhex)
700 phase = int(phase)
697 phase = int(phase)
701 if phase == public:
698 if phase == public:
702 if node != nullid:
699 if node != nullid:
703 repo.ui.warn(
700 repo.ui.warn(
704 _(
701 _(
705 b'ignoring inconsistent public root'
702 b'ignoring inconsistent public root'
706 b' from remote: %s\n'
703 b' from remote: %s\n'
707 )
704 )
708 % nhex
705 % nhex
709 )
706 )
710 elif phase == draft:
707 elif phase == draft:
711 if has_node(node):
708 if has_node(node):
712 draftroots.append(node)
709 draftroots.append(node)
713 else:
710 else:
714 repo.ui.warn(
711 repo.ui.warn(
715 _(b'ignoring unexpected root from remote: %i %s\n')
712 _(b'ignoring unexpected root from remote: %i %s\n')
716 % (phase, nhex)
713 % (phase, nhex)
717 )
714 )
718 # compute heads
715 # compute heads
719 publicheads = newheads(repo, subset, draftroots)
716 publicheads = newheads(repo, subset, draftroots)
720 return publicheads, draftroots
717 return publicheads, draftroots
721
718
722
719
723 class remotephasessummary(object):
720 class remotephasessummary(object):
724 """summarize phase information on the remote side
721 """summarize phase information on the remote side
725
722
726 :publishing: True is the remote is publishing
723 :publishing: True is the remote is publishing
727 :publicheads: list of remote public phase heads (nodes)
724 :publicheads: list of remote public phase heads (nodes)
728 :draftheads: list of remote draft phase heads (nodes)
725 :draftheads: list of remote draft phase heads (nodes)
729 :draftroots: list of remote draft phase root (nodes)
726 :draftroots: list of remote draft phase root (nodes)
730 """
727 """
731
728
732 def __init__(self, repo, remotesubset, remoteroots):
729 def __init__(self, repo, remotesubset, remoteroots):
733 unfi = repo.unfiltered()
730 unfi = repo.unfiltered()
734 self._allremoteroots = remoteroots
731 self._allremoteroots = remoteroots
735
732
736 self.publishing = remoteroots.get(b'publishing', False)
733 self.publishing = remoteroots.get(b'publishing', False)
737
734
738 ana = analyzeremotephases(repo, remotesubset, remoteroots)
735 ana = analyzeremotephases(repo, remotesubset, remoteroots)
739 self.publicheads, self.draftroots = ana
736 self.publicheads, self.draftroots = ana
740 # Get the list of all "heads" revs draft on remote
737 # Get the list of all "heads" revs draft on remote
741 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
738 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
742 self.draftheads = [c.node() for c in dheads]
739 self.draftheads = [c.node() for c in dheads]
743
740
744
741
745 def newheads(repo, heads, roots):
742 def newheads(repo, heads, roots):
746 """compute new head of a subset minus another
743 """compute new head of a subset minus another
747
744
748 * `heads`: define the first subset
745 * `heads`: define the first subset
749 * `roots`: define the second we subtract from the first"""
746 * `roots`: define the second we subtract from the first"""
750 # prevent an import cycle
747 # prevent an import cycle
751 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
748 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
752 from . import dagop
749 from . import dagop
753
750
754 repo = repo.unfiltered()
751 repo = repo.unfiltered()
755 cl = repo.changelog
752 cl = repo.changelog
756 rev = cl.index.get_rev
753 rev = cl.index.get_rev
757 if not roots:
754 if not roots:
758 return heads
755 return heads
759 if not heads or heads == [nullid]:
756 if not heads or heads == [nullid]:
760 return []
757 return []
761 # The logic operated on revisions, convert arguments early for convenience
758 # The logic operated on revisions, convert arguments early for convenience
762 new_heads = set(rev(n) for n in heads if n != nullid)
759 new_heads = set(rev(n) for n in heads if n != nullid)
763 roots = [rev(n) for n in roots]
760 roots = [rev(n) for n in roots]
764 # compute the area we need to remove
761 # compute the area we need to remove
765 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
762 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
766 # heads in the area are no longer heads
763 # heads in the area are no longer heads
767 new_heads.difference_update(affected_zone)
764 new_heads.difference_update(affected_zone)
768 # revisions in the area have children outside of it,
765 # revisions in the area have children outside of it,
769 # They might be new heads
766 # They might be new heads
770 candidates = repo.revs(
767 candidates = repo.revs(
771 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
768 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
772 )
769 )
773 candidates -= affected_zone
770 candidates -= affected_zone
774 if new_heads or candidates:
771 if new_heads or candidates:
775 # remove candidate that are ancestors of other heads
772 # remove candidate that are ancestors of other heads
776 new_heads.update(candidates)
773 new_heads.update(candidates)
777 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
774 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
778 pruned = dagop.reachableroots(repo, candidates, prunestart)
775 pruned = dagop.reachableroots(repo, candidates, prunestart)
779 new_heads.difference_update(pruned)
776 new_heads.difference_update(pruned)
780
777
781 return pycompat.maplist(cl.node, sorted(new_heads))
778 return pycompat.maplist(cl.node, sorted(new_heads))
782
779
783
780
784 def newcommitphase(ui):
781 def newcommitphase(ui):
785 """helper to get the target phase of new commit
782 """helper to get the target phase of new commit
786
783
787 Handle all possible values for the phases.new-commit options.
784 Handle all possible values for the phases.new-commit options.
788
785
789 """
786 """
790 v = ui.config(b'phases', b'new-commit')
787 v = ui.config(b'phases', b'new-commit')
791 try:
788 try:
792 return phasenames.index(v)
789 return phasenames.index(v)
793 except ValueError:
790 except ValueError:
794 try:
791 try:
795 return int(v)
792 return int(v)
796 except ValueError:
793 except ValueError:
797 msg = _(b"phases.new-commit: not a valid phase name ('%s')")
794 msg = _(b"phases.new-commit: not a valid phase name ('%s')")
798 raise error.ConfigError(msg % v)
795 raise error.ConfigError(msg % v)
799
796
800
797
801 def hassecret(repo):
798 def hassecret(repo):
802 """utility function that check if a repo have any secret changeset."""
799 """utility function that check if a repo have any secret changeset."""
803 return bool(repo._phasecache.phaseroots[2])
800 return bool(repo._phasecache.phaseroots[2])
804
801
805
802
806 def preparehookargs(node, old, new):
803 def preparehookargs(node, old, new):
807 if old is None:
804 if old is None:
808 old = b''
805 old = b''
809 else:
806 else:
810 old = phasenames[old]
807 old = phasenames[old]
811 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
808 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