##// END OF EJS Templates
phases: provide a test and accessor for non-public phase roots...
Joerg Sonnenberger -
r45674:e2d17974 default
parent child Browse files
Show More
@@ -1,897 +1,919 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 hasnonpublicphases(self, repo):
327 """detect if there are revisions with non-public phase"""
328 repo = repo.unfiltered()
329 cl = repo.changelog
330 if len(cl) >= self._loadedrevslen:
331 self.invalidate()
332 self.loadphaserevs(repo)
333 return any(self.phaseroots[1:])
334
335 def nonpublicphaseroots(self, repo):
336 """returns the roots of all non-public phases
337
338 The roots are not minimized, so if the secret revisions are
339 descendants of draft revisions, their roots will still be present.
340 """
341 repo = repo.unfiltered()
342 cl = repo.changelog
343 if len(cl) >= self._loadedrevslen:
344 self.invalidate()
345 self.loadphaserevs(repo)
346 return set().union(*[roots for roots in self.phaseroots[1:] if roots])
347
326 def getrevset(self, repo, phases, subset=None):
348 def getrevset(self, repo, phases, subset=None):
327 """return a smartset for the given phases"""
349 """return a smartset for the given phases"""
328 self.loadphaserevs(repo) # ensure phase's sets are loaded
350 self.loadphaserevs(repo) # ensure phase's sets are loaded
329 phases = set(phases)
351 phases = set(phases)
330 publicphase = public in phases
352 publicphase = public in phases
331
353
332 if publicphase:
354 if publicphase:
333 # In this case, phases keeps all the *other* phases.
355 # In this case, phases keeps all the *other* phases.
334 phases = set(allphases).difference(phases)
356 phases = set(allphases).difference(phases)
335 if not phases:
357 if not phases:
336 return smartset.fullreposet(repo)
358 return smartset.fullreposet(repo)
337
359
338 # fast path: _phasesets contains the interesting sets,
360 # fast path: _phasesets contains the interesting sets,
339 # might only need a union and post-filtering.
361 # might only need a union and post-filtering.
340 revsneedscopy = False
362 revsneedscopy = False
341 if len(phases) == 1:
363 if len(phases) == 1:
342 [p] = phases
364 [p] = phases
343 revs = self._phasesets[p]
365 revs = self._phasesets[p]
344 revsneedscopy = True # Don't modify _phasesets
366 revsneedscopy = True # Don't modify _phasesets
345 else:
367 else:
346 # revs has the revisions in all *other* phases.
368 # revs has the revisions in all *other* phases.
347 revs = set.union(*[self._phasesets[p] for p in phases])
369 revs = set.union(*[self._phasesets[p] for p in phases])
348
370
349 def _addwdir(wdirsubset, wdirrevs):
371 def _addwdir(wdirsubset, wdirrevs):
350 if wdirrev in wdirsubset and repo[None].phase() in phases:
372 if wdirrev in wdirsubset and repo[None].phase() in phases:
351 if revsneedscopy:
373 if revsneedscopy:
352 wdirrevs = wdirrevs.copy()
374 wdirrevs = wdirrevs.copy()
353 # The working dir would never be in the # cache, but it was in
375 # The working dir would never be in the # cache, but it was in
354 # the subset being filtered for its phase (or filtered out,
376 # the subset being filtered for its phase (or filtered out,
355 # depending on publicphase), so add it to the output to be
377 # depending on publicphase), so add it to the output to be
356 # included (or filtered out).
378 # included (or filtered out).
357 wdirrevs.add(wdirrev)
379 wdirrevs.add(wdirrev)
358 return wdirrevs
380 return wdirrevs
359
381
360 if not publicphase:
382 if not publicphase:
361 if repo.changelog.filteredrevs:
383 if repo.changelog.filteredrevs:
362 revs = revs - repo.changelog.filteredrevs
384 revs = revs - repo.changelog.filteredrevs
363
385
364 if subset is None:
386 if subset is None:
365 return smartset.baseset(revs)
387 return smartset.baseset(revs)
366 else:
388 else:
367 revs = _addwdir(subset, revs)
389 revs = _addwdir(subset, revs)
368 return subset & smartset.baseset(revs)
390 return subset & smartset.baseset(revs)
369 else:
391 else:
370 if subset is None:
392 if subset is None:
371 subset = smartset.fullreposet(repo)
393 subset = smartset.fullreposet(repo)
372
394
373 revs = _addwdir(subset, revs)
395 revs = _addwdir(subset, revs)
374
396
375 if not revs:
397 if not revs:
376 return subset
398 return subset
377 return subset.filter(lambda r: r not in revs)
399 return subset.filter(lambda r: r not in revs)
378
400
379 def copy(self):
401 def copy(self):
380 # Shallow copy meant to ensure isolation in
402 # Shallow copy meant to ensure isolation in
381 # advance/retractboundary(), nothing more.
403 # advance/retractboundary(), nothing more.
382 ph = self.__class__(None, None, _load=False)
404 ph = self.__class__(None, None, _load=False)
383 ph.phaseroots = self.phaseroots[:]
405 ph.phaseroots = self.phaseroots[:]
384 ph.dirty = self.dirty
406 ph.dirty = self.dirty
385 ph.opener = self.opener
407 ph.opener = self.opener
386 ph._loadedrevslen = self._loadedrevslen
408 ph._loadedrevslen = self._loadedrevslen
387 ph._phasesets = self._phasesets
409 ph._phasesets = self._phasesets
388 return ph
410 return ph
389
411
390 def replace(self, phcache):
412 def replace(self, phcache):
391 """replace all values in 'self' with content of phcache"""
413 """replace all values in 'self' with content of phcache"""
392 for a in (
414 for a in (
393 b'phaseroots',
415 b'phaseroots',
394 b'dirty',
416 b'dirty',
395 b'opener',
417 b'opener',
396 b'_loadedrevslen',
418 b'_loadedrevslen',
397 b'_phasesets',
419 b'_phasesets',
398 ):
420 ):
399 setattr(self, a, getattr(phcache, a))
421 setattr(self, a, getattr(phcache, a))
400
422
401 def _getphaserevsnative(self, repo):
423 def _getphaserevsnative(self, repo):
402 repo = repo.unfiltered()
424 repo = repo.unfiltered()
403 nativeroots = []
425 nativeroots = []
404 for phase in trackedphases:
426 for phase in trackedphases:
405 nativeroots.append(
427 nativeroots.append(
406 pycompat.maplist(repo.changelog.rev, self.phaseroots[phase])
428 pycompat.maplist(repo.changelog.rev, self.phaseroots[phase])
407 )
429 )
408 return repo.changelog.computephases(nativeroots)
430 return repo.changelog.computephases(nativeroots)
409
431
410 def _computephaserevspure(self, repo):
432 def _computephaserevspure(self, repo):
411 repo = repo.unfiltered()
433 repo = repo.unfiltered()
412 cl = repo.changelog
434 cl = repo.changelog
413 self._phasesets = [set() for phase in allphases]
435 self._phasesets = [set() for phase in allphases]
414 lowerroots = set()
436 lowerroots = set()
415 for phase in reversed(trackedphases):
437 for phase in reversed(trackedphases):
416 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
438 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
417 if roots:
439 if roots:
418 ps = set(cl.descendants(roots))
440 ps = set(cl.descendants(roots))
419 for root in roots:
441 for root in roots:
420 ps.add(root)
442 ps.add(root)
421 ps.difference_update(lowerroots)
443 ps.difference_update(lowerroots)
422 lowerroots.update(ps)
444 lowerroots.update(ps)
423 self._phasesets[phase] = ps
445 self._phasesets[phase] = ps
424 self._loadedrevslen = len(cl)
446 self._loadedrevslen = len(cl)
425
447
426 def loadphaserevs(self, repo):
448 def loadphaserevs(self, repo):
427 """ensure phase information is loaded in the object"""
449 """ensure phase information is loaded in the object"""
428 if self._phasesets is None:
450 if self._phasesets is None:
429 try:
451 try:
430 res = self._getphaserevsnative(repo)
452 res = self._getphaserevsnative(repo)
431 self._loadedrevslen, self._phasesets = res
453 self._loadedrevslen, self._phasesets = res
432 except AttributeError:
454 except AttributeError:
433 self._computephaserevspure(repo)
455 self._computephaserevspure(repo)
434
456
435 def invalidate(self):
457 def invalidate(self):
436 self._loadedrevslen = 0
458 self._loadedrevslen = 0
437 self._phasesets = None
459 self._phasesets = None
438
460
439 def phase(self, repo, rev):
461 def phase(self, repo, rev):
440 # We need a repo argument here to be able to build _phasesets
462 # We need a repo argument here to be able to build _phasesets
441 # if necessary. The repository instance is not stored in
463 # if necessary. The repository instance is not stored in
442 # phasecache to avoid reference cycles. The changelog instance
464 # phasecache to avoid reference cycles. The changelog instance
443 # is not stored because it is a filecache() property and can
465 # is not stored because it is a filecache() property and can
444 # be replaced without us being notified.
466 # be replaced without us being notified.
445 if rev == nullrev:
467 if rev == nullrev:
446 return public
468 return public
447 if rev < nullrev:
469 if rev < nullrev:
448 raise ValueError(_(b'cannot lookup negative revision'))
470 raise ValueError(_(b'cannot lookup negative revision'))
449 if rev >= self._loadedrevslen:
471 if rev >= self._loadedrevslen:
450 self.invalidate()
472 self.invalidate()
451 self.loadphaserevs(repo)
473 self.loadphaserevs(repo)
452 for phase in trackedphases:
474 for phase in trackedphases:
453 if rev in self._phasesets[phase]:
475 if rev in self._phasesets[phase]:
454 return phase
476 return phase
455 return public
477 return public
456
478
457 def write(self):
479 def write(self):
458 if not self.dirty:
480 if not self.dirty:
459 return
481 return
460 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
482 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
461 try:
483 try:
462 self._write(f)
484 self._write(f)
463 finally:
485 finally:
464 f.close()
486 f.close()
465
487
466 def _write(self, fp):
488 def _write(self, fp):
467 for phase, roots in enumerate(self.phaseroots):
489 for phase, roots in enumerate(self.phaseroots):
468 for h in sorted(roots):
490 for h in sorted(roots):
469 fp.write(b'%i %s\n' % (phase, hex(h)))
491 fp.write(b'%i %s\n' % (phase, hex(h)))
470 self.dirty = False
492 self.dirty = False
471
493
472 def _updateroots(self, phase, newroots, tr):
494 def _updateroots(self, phase, newroots, tr):
473 self.phaseroots[phase] = newroots
495 self.phaseroots[phase] = newroots
474 self.invalidate()
496 self.invalidate()
475 self.dirty = True
497 self.dirty = True
476
498
477 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
499 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
478 tr.hookargs[b'phases_moved'] = b'1'
500 tr.hookargs[b'phases_moved'] = b'1'
479
501
480 def registernew(self, repo, tr, targetphase, nodes):
502 def registernew(self, repo, tr, targetphase, nodes):
481 repo = repo.unfiltered()
503 repo = repo.unfiltered()
482 self._retractboundary(repo, tr, targetphase, nodes)
504 self._retractboundary(repo, tr, targetphase, nodes)
483 if tr is not None and b'phases' in tr.changes:
505 if tr is not None and b'phases' in tr.changes:
484 phasetracking = tr.changes[b'phases']
506 phasetracking = tr.changes[b'phases']
485 torev = repo.changelog.rev
507 torev = repo.changelog.rev
486 phase = self.phase
508 phase = self.phase
487 revs = [torev(node) for node in nodes]
509 revs = [torev(node) for node in nodes]
488 revs.sort()
510 revs.sort()
489 for rev in revs:
511 for rev in revs:
490 revphase = phase(repo, rev)
512 revphase = phase(repo, rev)
491 _trackphasechange(phasetracking, rev, None, revphase)
513 _trackphasechange(phasetracking, rev, None, revphase)
492 repo.invalidatevolatilesets()
514 repo.invalidatevolatilesets()
493
515
494 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
516 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
495 """Set all 'nodes' to phase 'targetphase'
517 """Set all 'nodes' to phase 'targetphase'
496
518
497 Nodes with a phase lower than 'targetphase' are not affected.
519 Nodes with a phase lower than 'targetphase' are not affected.
498
520
499 If dryrun is True, no actions will be performed
521 If dryrun is True, no actions will be performed
500
522
501 Returns a set of revs whose phase is changed or should be changed
523 Returns a set of revs whose phase is changed or should be changed
502 """
524 """
503 # Be careful to preserve shallow-copied values: do not update
525 # Be careful to preserve shallow-copied values: do not update
504 # phaseroots values, replace them.
526 # phaseroots values, replace them.
505 if tr is None:
527 if tr is None:
506 phasetracking = None
528 phasetracking = None
507 else:
529 else:
508 phasetracking = tr.changes.get(b'phases')
530 phasetracking = tr.changes.get(b'phases')
509
531
510 repo = repo.unfiltered()
532 repo = repo.unfiltered()
511
533
512 changes = set() # set of revisions to be changed
534 changes = set() # set of revisions to be changed
513 delroots = [] # set of root deleted by this path
535 delroots = [] # set of root deleted by this path
514 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
536 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
515 # filter nodes that are not in a compatible phase already
537 # filter nodes that are not in a compatible phase already
516 nodes = [
538 nodes = [
517 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
539 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
518 ]
540 ]
519 if not nodes:
541 if not nodes:
520 break # no roots to move anymore
542 break # no roots to move anymore
521
543
522 olds = self.phaseroots[phase]
544 olds = self.phaseroots[phase]
523
545
524 affected = repo.revs(b'%ln::%ln', olds, nodes)
546 affected = repo.revs(b'%ln::%ln', olds, nodes)
525 changes.update(affected)
547 changes.update(affected)
526 if dryrun:
548 if dryrun:
527 continue
549 continue
528 for r in affected:
550 for r in affected:
529 _trackphasechange(
551 _trackphasechange(
530 phasetracking, r, self.phase(repo, r), targetphase
552 phasetracking, r, self.phase(repo, r), targetphase
531 )
553 )
532
554
533 roots = {
555 roots = {
534 ctx.node()
556 ctx.node()
535 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
557 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
536 }
558 }
537 if olds != roots:
559 if olds != roots:
538 self._updateroots(phase, roots, tr)
560 self._updateroots(phase, roots, tr)
539 # some roots may need to be declared for lower phases
561 # some roots may need to be declared for lower phases
540 delroots.extend(olds - roots)
562 delroots.extend(olds - roots)
541 if not dryrun:
563 if not dryrun:
542 # declare deleted root in the target phase
564 # declare deleted root in the target phase
543 if targetphase != 0:
565 if targetphase != 0:
544 self._retractboundary(repo, tr, targetphase, delroots)
566 self._retractboundary(repo, tr, targetphase, delroots)
545 repo.invalidatevolatilesets()
567 repo.invalidatevolatilesets()
546 return changes
568 return changes
547
569
548 def retractboundary(self, repo, tr, targetphase, nodes):
570 def retractboundary(self, repo, tr, targetphase, nodes):
549 oldroots = self.phaseroots[: targetphase + 1]
571 oldroots = self.phaseroots[: targetphase + 1]
550 if tr is None:
572 if tr is None:
551 phasetracking = None
573 phasetracking = None
552 else:
574 else:
553 phasetracking = tr.changes.get(b'phases')
575 phasetracking = tr.changes.get(b'phases')
554 repo = repo.unfiltered()
576 repo = repo.unfiltered()
555 if (
577 if (
556 self._retractboundary(repo, tr, targetphase, nodes)
578 self._retractboundary(repo, tr, targetphase, nodes)
557 and phasetracking is not None
579 and phasetracking is not None
558 ):
580 ):
559
581
560 # find the affected revisions
582 # find the affected revisions
561 new = self.phaseroots[targetphase]
583 new = self.phaseroots[targetphase]
562 old = oldroots[targetphase]
584 old = oldroots[targetphase]
563 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
585 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
564
586
565 # find the phase of the affected revision
587 # find the phase of the affected revision
566 for phase in pycompat.xrange(targetphase, -1, -1):
588 for phase in pycompat.xrange(targetphase, -1, -1):
567 if phase:
589 if phase:
568 roots = oldroots[phase]
590 roots = oldroots[phase]
569 revs = set(repo.revs(b'%ln::%ld', roots, affected))
591 revs = set(repo.revs(b'%ln::%ld', roots, affected))
570 affected -= revs
592 affected -= revs
571 else: # public phase
593 else: # public phase
572 revs = affected
594 revs = affected
573 for r in sorted(revs):
595 for r in sorted(revs):
574 _trackphasechange(phasetracking, r, phase, targetphase)
596 _trackphasechange(phasetracking, r, phase, targetphase)
575 repo.invalidatevolatilesets()
597 repo.invalidatevolatilesets()
576
598
577 def _retractboundary(self, repo, tr, targetphase, nodes):
599 def _retractboundary(self, repo, tr, targetphase, nodes):
578 # Be careful to preserve shallow-copied values: do not update
600 # Be careful to preserve shallow-copied values: do not update
579 # phaseroots values, replace them.
601 # phaseroots values, replace them.
580 if targetphase in (archived, internal) and not supportinternal(repo):
602 if targetphase in (archived, internal) and not supportinternal(repo):
581 name = phasenames[targetphase]
603 name = phasenames[targetphase]
582 msg = b'this repository does not support the %s phase' % name
604 msg = b'this repository does not support the %s phase' % name
583 raise error.ProgrammingError(msg)
605 raise error.ProgrammingError(msg)
584
606
585 repo = repo.unfiltered()
607 repo = repo.unfiltered()
586 torev = repo.changelog.rev
608 torev = repo.changelog.rev
587 tonode = repo.changelog.node
609 tonode = repo.changelog.node
588 currentroots = {torev(node) for node in self.phaseroots[targetphase]}
610 currentroots = {torev(node) for node in self.phaseroots[targetphase]}
589 finalroots = oldroots = set(currentroots)
611 finalroots = oldroots = set(currentroots)
590 newroots = [torev(node) for node in nodes]
612 newroots = [torev(node) for node in nodes]
591 newroots = [
613 newroots = [
592 rev for rev in newroots if self.phase(repo, rev) < targetphase
614 rev for rev in newroots if self.phase(repo, rev) < targetphase
593 ]
615 ]
594
616
595 if newroots:
617 if newroots:
596 if nullrev in newroots:
618 if nullrev in newroots:
597 raise error.Abort(_(b'cannot change null revision phase'))
619 raise error.Abort(_(b'cannot change null revision phase'))
598 currentroots.update(newroots)
620 currentroots.update(newroots)
599
621
600 # Only compute new roots for revs above the roots that are being
622 # Only compute new roots for revs above the roots that are being
601 # retracted.
623 # retracted.
602 minnewroot = min(newroots)
624 minnewroot = min(newroots)
603 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
625 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
604 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
626 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
605
627
606 finalroots = {rev for rev in currentroots if rev < minnewroot}
628 finalroots = {rev for rev in currentroots if rev < minnewroot}
607 finalroots.update(updatedroots)
629 finalroots.update(updatedroots)
608 if finalroots != oldroots:
630 if finalroots != oldroots:
609 self._updateroots(
631 self._updateroots(
610 targetphase, {tonode(rev) for rev in finalroots}, tr
632 targetphase, {tonode(rev) for rev in finalroots}, tr
611 )
633 )
612 return True
634 return True
613 return False
635 return False
614
636
615 def filterunknown(self, repo):
637 def filterunknown(self, repo):
616 """remove unknown nodes from the phase boundary
638 """remove unknown nodes from the phase boundary
617
639
618 Nothing is lost as unknown nodes only hold data for their descendants.
640 Nothing is lost as unknown nodes only hold data for their descendants.
619 """
641 """
620 filtered = False
642 filtered = False
621 has_node = repo.changelog.index.has_node # to filter unknown nodes
643 has_node = repo.changelog.index.has_node # to filter unknown nodes
622 for phase, nodes in enumerate(self.phaseroots):
644 for phase, nodes in enumerate(self.phaseroots):
623 missing = sorted(node for node in nodes if not has_node(node))
645 missing = sorted(node for node in nodes if not has_node(node))
624 if missing:
646 if missing:
625 for mnode in missing:
647 for mnode in missing:
626 repo.ui.debug(
648 repo.ui.debug(
627 b'removing unknown node %s from %i-phase boundary\n'
649 b'removing unknown node %s from %i-phase boundary\n'
628 % (short(mnode), phase)
650 % (short(mnode), phase)
629 )
651 )
630 nodes.symmetric_difference_update(missing)
652 nodes.symmetric_difference_update(missing)
631 filtered = True
653 filtered = True
632 if filtered:
654 if filtered:
633 self.dirty = True
655 self.dirty = True
634 # filterunknown is called by repo.destroyed, we may have no changes in
656 # filterunknown is called by repo.destroyed, we may have no changes in
635 # root but _phasesets contents is certainly invalid (or at least we
657 # root but _phasesets contents is certainly invalid (or at least we
636 # have not proper way to check that). related to issue 3858.
658 # have not proper way to check that). related to issue 3858.
637 #
659 #
638 # The other caller is __init__ that have no _phasesets initialized
660 # The other caller is __init__ that have no _phasesets initialized
639 # anyway. If this change we should consider adding a dedicated
661 # anyway. If this change we should consider adding a dedicated
640 # "destroyed" function to phasecache or a proper cache key mechanism
662 # "destroyed" function to phasecache or a proper cache key mechanism
641 # (see branchmap one)
663 # (see branchmap one)
642 self.invalidate()
664 self.invalidate()
643
665
644
666
645 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
667 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
646 """Add nodes to a phase changing other nodes phases if necessary.
668 """Add nodes to a phase changing other nodes phases if necessary.
647
669
648 This function move boundary *forward* this means that all nodes
670 This function move boundary *forward* this means that all nodes
649 are set in the target phase or kept in a *lower* phase.
671 are set in the target phase or kept in a *lower* phase.
650
672
651 Simplify boundary to contains phase roots only.
673 Simplify boundary to contains phase roots only.
652
674
653 If dryrun is True, no actions will be performed
675 If dryrun is True, no actions will be performed
654
676
655 Returns a set of revs whose phase is changed or should be changed
677 Returns a set of revs whose phase is changed or should be changed
656 """
678 """
657 phcache = repo._phasecache.copy()
679 phcache = repo._phasecache.copy()
658 changes = phcache.advanceboundary(
680 changes = phcache.advanceboundary(
659 repo, tr, targetphase, nodes, dryrun=dryrun
681 repo, tr, targetphase, nodes, dryrun=dryrun
660 )
682 )
661 if not dryrun:
683 if not dryrun:
662 repo._phasecache.replace(phcache)
684 repo._phasecache.replace(phcache)
663 return changes
685 return changes
664
686
665
687
666 def retractboundary(repo, tr, targetphase, nodes):
688 def retractboundary(repo, tr, targetphase, nodes):
667 """Set nodes back to a phase changing other nodes phases if
689 """Set nodes back to a phase changing other nodes phases if
668 necessary.
690 necessary.
669
691
670 This function move boundary *backward* this means that all nodes
692 This function move boundary *backward* this means that all nodes
671 are set in the target phase or kept in a *higher* phase.
693 are set in the target phase or kept in a *higher* phase.
672
694
673 Simplify boundary to contains phase roots only."""
695 Simplify boundary to contains phase roots only."""
674 phcache = repo._phasecache.copy()
696 phcache = repo._phasecache.copy()
675 phcache.retractboundary(repo, tr, targetphase, nodes)
697 phcache.retractboundary(repo, tr, targetphase, nodes)
676 repo._phasecache.replace(phcache)
698 repo._phasecache.replace(phcache)
677
699
678
700
679 def registernew(repo, tr, targetphase, nodes):
701 def registernew(repo, tr, targetphase, nodes):
680 """register a new revision and its phase
702 """register a new revision and its phase
681
703
682 Code adding revisions to the repository should use this function to
704 Code adding revisions to the repository should use this function to
683 set new changeset in their target phase (or higher).
705 set new changeset in their target phase (or higher).
684 """
706 """
685 phcache = repo._phasecache.copy()
707 phcache = repo._phasecache.copy()
686 phcache.registernew(repo, tr, targetphase, nodes)
708 phcache.registernew(repo, tr, targetphase, nodes)
687 repo._phasecache.replace(phcache)
709 repo._phasecache.replace(phcache)
688
710
689
711
690 def listphases(repo):
712 def listphases(repo):
691 """List phases root for serialization over pushkey"""
713 """List phases root for serialization over pushkey"""
692 # Use ordered dictionary so behavior is deterministic.
714 # Use ordered dictionary so behavior is deterministic.
693 keys = util.sortdict()
715 keys = util.sortdict()
694 value = b'%i' % draft
716 value = b'%i' % draft
695 cl = repo.unfiltered().changelog
717 cl = repo.unfiltered().changelog
696 for root in repo._phasecache.phaseroots[draft]:
718 for root in repo._phasecache.phaseroots[draft]:
697 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
719 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
698 keys[hex(root)] = value
720 keys[hex(root)] = value
699
721
700 if repo.publishing():
722 if repo.publishing():
701 # Add an extra data to let remote know we are a publishing
723 # Add an extra data to let remote know we are a publishing
702 # repo. Publishing repo can't just pretend they are old repo.
724 # repo. Publishing repo can't just pretend they are old repo.
703 # When pushing to a publishing repo, the client still need to
725 # When pushing to a publishing repo, the client still need to
704 # push phase boundary
726 # push phase boundary
705 #
727 #
706 # Push do not only push changeset. It also push phase data.
728 # Push do not only push changeset. It also push phase data.
707 # New phase data may apply to common changeset which won't be
729 # New phase data may apply to common changeset which won't be
708 # push (as they are common). Here is a very simple example:
730 # push (as they are common). Here is a very simple example:
709 #
731 #
710 # 1) repo A push changeset X as draft to repo B
732 # 1) repo A push changeset X as draft to repo B
711 # 2) repo B make changeset X public
733 # 2) repo B make changeset X public
712 # 3) repo B push to repo A. X is not pushed but the data that
734 # 3) repo B push to repo A. X is not pushed but the data that
713 # X as now public should
735 # X as now public should
714 #
736 #
715 # The server can't handle it on it's own as it has no idea of
737 # The server can't handle it on it's own as it has no idea of
716 # client phase data.
738 # client phase data.
717 keys[b'publishing'] = b'True'
739 keys[b'publishing'] = b'True'
718 return keys
740 return keys
719
741
720
742
721 def pushphase(repo, nhex, oldphasestr, newphasestr):
743 def pushphase(repo, nhex, oldphasestr, newphasestr):
722 """List phases root for serialization over pushkey"""
744 """List phases root for serialization over pushkey"""
723 repo = repo.unfiltered()
745 repo = repo.unfiltered()
724 with repo.lock():
746 with repo.lock():
725 currentphase = repo[nhex].phase()
747 currentphase = repo[nhex].phase()
726 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
748 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
727 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
749 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
728 if currentphase == oldphase and newphase < oldphase:
750 if currentphase == oldphase and newphase < oldphase:
729 with repo.transaction(b'pushkey-phase') as tr:
751 with repo.transaction(b'pushkey-phase') as tr:
730 advanceboundary(repo, tr, newphase, [bin(nhex)])
752 advanceboundary(repo, tr, newphase, [bin(nhex)])
731 return True
753 return True
732 elif currentphase == newphase:
754 elif currentphase == newphase:
733 # raced, but got correct result
755 # raced, but got correct result
734 return True
756 return True
735 else:
757 else:
736 return False
758 return False
737
759
738
760
739 def subsetphaseheads(repo, subset):
761 def subsetphaseheads(repo, subset):
740 """Finds the phase heads for a subset of a history
762 """Finds the phase heads for a subset of a history
741
763
742 Returns a list indexed by phase number where each item is a list of phase
764 Returns a list indexed by phase number where each item is a list of phase
743 head nodes.
765 head nodes.
744 """
766 """
745 cl = repo.changelog
767 cl = repo.changelog
746
768
747 headsbyphase = [[] for i in allphases]
769 headsbyphase = [[] for i in allphases]
748 # No need to keep track of secret phase; any heads in the subset that
770 # No need to keep track of secret phase; any heads in the subset that
749 # are not mentioned are implicitly secret.
771 # are not mentioned are implicitly secret.
750 for phase in allphases[:secret]:
772 for phase in allphases[:secret]:
751 revset = b"heads(%%ln & %s())" % phasenames[phase]
773 revset = b"heads(%%ln & %s())" % phasenames[phase]
752 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
774 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
753 return headsbyphase
775 return headsbyphase
754
776
755
777
756 def updatephases(repo, trgetter, headsbyphase):
778 def updatephases(repo, trgetter, headsbyphase):
757 """Updates the repo with the given phase heads"""
779 """Updates the repo with the given phase heads"""
758 # Now advance phase boundaries of all but secret phase
780 # Now advance phase boundaries of all but secret phase
759 #
781 #
760 # run the update (and fetch transaction) only if there are actually things
782 # run the update (and fetch transaction) only if there are actually things
761 # to update. This avoid creating empty transaction during no-op operation.
783 # to update. This avoid creating empty transaction during no-op operation.
762
784
763 for phase in allphases[:-1]:
785 for phase in allphases[:-1]:
764 revset = b'%ln - _phase(%s)'
786 revset = b'%ln - _phase(%s)'
765 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
787 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
766 if heads:
788 if heads:
767 advanceboundary(repo, trgetter(), phase, heads)
789 advanceboundary(repo, trgetter(), phase, heads)
768
790
769
791
770 def analyzeremotephases(repo, subset, roots):
792 def analyzeremotephases(repo, subset, roots):
771 """Compute phases heads and root in a subset of node from root dict
793 """Compute phases heads and root in a subset of node from root dict
772
794
773 * subset is heads of the subset
795 * subset is heads of the subset
774 * roots is {<nodeid> => phase} mapping. key and value are string.
796 * roots is {<nodeid> => phase} mapping. key and value are string.
775
797
776 Accept unknown element input
798 Accept unknown element input
777 """
799 """
778 repo = repo.unfiltered()
800 repo = repo.unfiltered()
779 # build list from dictionary
801 # build list from dictionary
780 draftroots = []
802 draftroots = []
781 has_node = repo.changelog.index.has_node # to filter unknown nodes
803 has_node = repo.changelog.index.has_node # to filter unknown nodes
782 for nhex, phase in pycompat.iteritems(roots):
804 for nhex, phase in pycompat.iteritems(roots):
783 if nhex == b'publishing': # ignore data related to publish option
805 if nhex == b'publishing': # ignore data related to publish option
784 continue
806 continue
785 node = bin(nhex)
807 node = bin(nhex)
786 phase = int(phase)
808 phase = int(phase)
787 if phase == public:
809 if phase == public:
788 if node != nullid:
810 if node != nullid:
789 repo.ui.warn(
811 repo.ui.warn(
790 _(
812 _(
791 b'ignoring inconsistent public root'
813 b'ignoring inconsistent public root'
792 b' from remote: %s\n'
814 b' from remote: %s\n'
793 )
815 )
794 % nhex
816 % nhex
795 )
817 )
796 elif phase == draft:
818 elif phase == draft:
797 if has_node(node):
819 if has_node(node):
798 draftroots.append(node)
820 draftroots.append(node)
799 else:
821 else:
800 repo.ui.warn(
822 repo.ui.warn(
801 _(b'ignoring unexpected root from remote: %i %s\n')
823 _(b'ignoring unexpected root from remote: %i %s\n')
802 % (phase, nhex)
824 % (phase, nhex)
803 )
825 )
804 # compute heads
826 # compute heads
805 publicheads = newheads(repo, subset, draftroots)
827 publicheads = newheads(repo, subset, draftroots)
806 return publicheads, draftroots
828 return publicheads, draftroots
807
829
808
830
809 class remotephasessummary(object):
831 class remotephasessummary(object):
810 """summarize phase information on the remote side
832 """summarize phase information on the remote side
811
833
812 :publishing: True is the remote is publishing
834 :publishing: True is the remote is publishing
813 :publicheads: list of remote public phase heads (nodes)
835 :publicheads: list of remote public phase heads (nodes)
814 :draftheads: list of remote draft phase heads (nodes)
836 :draftheads: list of remote draft phase heads (nodes)
815 :draftroots: list of remote draft phase root (nodes)
837 :draftroots: list of remote draft phase root (nodes)
816 """
838 """
817
839
818 def __init__(self, repo, remotesubset, remoteroots):
840 def __init__(self, repo, remotesubset, remoteroots):
819 unfi = repo.unfiltered()
841 unfi = repo.unfiltered()
820 self._allremoteroots = remoteroots
842 self._allremoteroots = remoteroots
821
843
822 self.publishing = remoteroots.get(b'publishing', False)
844 self.publishing = remoteroots.get(b'publishing', False)
823
845
824 ana = analyzeremotephases(repo, remotesubset, remoteroots)
846 ana = analyzeremotephases(repo, remotesubset, remoteroots)
825 self.publicheads, self.draftroots = ana
847 self.publicheads, self.draftroots = ana
826 # Get the list of all "heads" revs draft on remote
848 # Get the list of all "heads" revs draft on remote
827 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
849 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
828 self.draftheads = [c.node() for c in dheads]
850 self.draftheads = [c.node() for c in dheads]
829
851
830
852
831 def newheads(repo, heads, roots):
853 def newheads(repo, heads, roots):
832 """compute new head of a subset minus another
854 """compute new head of a subset minus another
833
855
834 * `heads`: define the first subset
856 * `heads`: define the first subset
835 * `roots`: define the second we subtract from the first"""
857 * `roots`: define the second we subtract from the first"""
836 # prevent an import cycle
858 # prevent an import cycle
837 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
859 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
838 from . import dagop
860 from . import dagop
839
861
840 repo = repo.unfiltered()
862 repo = repo.unfiltered()
841 cl = repo.changelog
863 cl = repo.changelog
842 rev = cl.index.get_rev
864 rev = cl.index.get_rev
843 if not roots:
865 if not roots:
844 return heads
866 return heads
845 if not heads or heads == [nullid]:
867 if not heads or heads == [nullid]:
846 return []
868 return []
847 # The logic operated on revisions, convert arguments early for convenience
869 # The logic operated on revisions, convert arguments early for convenience
848 new_heads = {rev(n) for n in heads if n != nullid}
870 new_heads = {rev(n) for n in heads if n != nullid}
849 roots = [rev(n) for n in roots]
871 roots = [rev(n) for n in roots]
850 # compute the area we need to remove
872 # compute the area we need to remove
851 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
873 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
852 # heads in the area are no longer heads
874 # heads in the area are no longer heads
853 new_heads.difference_update(affected_zone)
875 new_heads.difference_update(affected_zone)
854 # revisions in the area have children outside of it,
876 # revisions in the area have children outside of it,
855 # They might be new heads
877 # They might be new heads
856 candidates = repo.revs(
878 candidates = repo.revs(
857 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
879 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
858 )
880 )
859 candidates -= affected_zone
881 candidates -= affected_zone
860 if new_heads or candidates:
882 if new_heads or candidates:
861 # remove candidate that are ancestors of other heads
883 # remove candidate that are ancestors of other heads
862 new_heads.update(candidates)
884 new_heads.update(candidates)
863 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
885 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
864 pruned = dagop.reachableroots(repo, candidates, prunestart)
886 pruned = dagop.reachableroots(repo, candidates, prunestart)
865 new_heads.difference_update(pruned)
887 new_heads.difference_update(pruned)
866
888
867 return pycompat.maplist(cl.node, sorted(new_heads))
889 return pycompat.maplist(cl.node, sorted(new_heads))
868
890
869
891
870 def newcommitphase(ui):
892 def newcommitphase(ui):
871 """helper to get the target phase of new commit
893 """helper to get the target phase of new commit
872
894
873 Handle all possible values for the phases.new-commit options.
895 Handle all possible values for the phases.new-commit options.
874
896
875 """
897 """
876 v = ui.config(b'phases', b'new-commit')
898 v = ui.config(b'phases', b'new-commit')
877 try:
899 try:
878 return phasenames.index(v)
900 return phasenames.index(v)
879 except ValueError:
901 except ValueError:
880 try:
902 try:
881 return int(v)
903 return int(v)
882 except ValueError:
904 except ValueError:
883 msg = _(b"phases.new-commit: not a valid phase name ('%s')")
905 msg = _(b"phases.new-commit: not a valid phase name ('%s')")
884 raise error.ConfigError(msg % v)
906 raise error.ConfigError(msg % v)
885
907
886
908
887 def hassecret(repo):
909 def hassecret(repo):
888 """utility function that check if a repo have any secret changeset."""
910 """utility function that check if a repo have any secret changeset."""
889 return bool(repo._phasecache.phaseroots[secret])
911 return bool(repo._phasecache.phaseroots[secret])
890
912
891
913
892 def preparehookargs(node, old, new):
914 def preparehookargs(node, old, new):
893 if old is None:
915 if old is None:
894 old = b''
916 old = b''
895 else:
917 else:
896 old = phasenames[old]
918 old = phasenames[old]
897 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
919 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
@@ -1,471 +1,471 b''
1 # repoview.py - Filtered view of a localrepo object
1 # repoview.py - Filtered view of a localrepo object
2 #
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import weakref
12 import weakref
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 hex,
16 hex,
17 nullrev,
17 nullrev,
18 )
18 )
19 from .pycompat import (
19 from .pycompat import (
20 delattr,
20 delattr,
21 getattr,
21 getattr,
22 setattr,
22 setattr,
23 )
23 )
24 from . import (
24 from . import (
25 error,
25 error,
26 obsolete,
26 obsolete,
27 phases,
27 phases,
28 pycompat,
28 pycompat,
29 tags as tagsmod,
29 tags as tagsmod,
30 util,
30 util,
31 )
31 )
32 from .utils import repoviewutil
32 from .utils import repoviewutil
33
33
34
34
35 def hideablerevs(repo):
35 def hideablerevs(repo):
36 """Revision candidates to be hidden
36 """Revision candidates to be hidden
37
37
38 This is a standalone function to allow extensions to wrap it.
38 This is a standalone function to allow extensions to wrap it.
39
39
40 Because we use the set of immutable changesets as a fallback subset in
40 Because we use the set of immutable changesets as a fallback subset in
41 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
41 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
42 "public" changesets as "hideable". Doing so would break multiple code
42 "public" changesets as "hideable". Doing so would break multiple code
43 assertions and lead to crashes."""
43 assertions and lead to crashes."""
44 obsoletes = obsolete.getrevs(repo, b'obsolete')
44 obsoletes = obsolete.getrevs(repo, b'obsolete')
45 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
45 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
46 internals = frozenset(internals)
46 internals = frozenset(internals)
47 return obsoletes | internals
47 return obsoletes | internals
48
48
49
49
50 def pinnedrevs(repo):
50 def pinnedrevs(repo):
51 """revisions blocking hidden changesets from being filtered
51 """revisions blocking hidden changesets from being filtered
52 """
52 """
53
53
54 cl = repo.changelog
54 cl = repo.changelog
55 pinned = set()
55 pinned = set()
56 pinned.update([par.rev() for par in repo[None].parents()])
56 pinned.update([par.rev() for par in repo[None].parents()])
57 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
57 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
58
58
59 tags = {}
59 tags = {}
60 tagsmod.readlocaltags(repo.ui, repo, tags, {})
60 tagsmod.readlocaltags(repo.ui, repo, tags, {})
61 if tags:
61 if tags:
62 rev = cl.index.get_rev
62 rev = cl.index.get_rev
63 pinned.update(rev(t[0]) for t in tags.values())
63 pinned.update(rev(t[0]) for t in tags.values())
64 pinned.discard(None)
64 pinned.discard(None)
65 return pinned
65 return pinned
66
66
67
67
68 def _revealancestors(pfunc, hidden, revs):
68 def _revealancestors(pfunc, hidden, revs):
69 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
69 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
70 from 'hidden'
70 from 'hidden'
71
71
72 - pfunc(r): a funtion returning parent of 'r',
72 - pfunc(r): a funtion returning parent of 'r',
73 - hidden: the (preliminary) hidden revisions, to be updated
73 - hidden: the (preliminary) hidden revisions, to be updated
74 - revs: iterable of revnum,
74 - revs: iterable of revnum,
75
75
76 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
76 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
77 *not* revealed)
77 *not* revealed)
78 """
78 """
79 stack = list(revs)
79 stack = list(revs)
80 while stack:
80 while stack:
81 for p in pfunc(stack.pop()):
81 for p in pfunc(stack.pop()):
82 if p != nullrev and p in hidden:
82 if p != nullrev and p in hidden:
83 hidden.remove(p)
83 hidden.remove(p)
84 stack.append(p)
84 stack.append(p)
85
85
86
86
87 def computehidden(repo, visibilityexceptions=None):
87 def computehidden(repo, visibilityexceptions=None):
88 """compute the set of hidden revision to filter
88 """compute the set of hidden revision to filter
89
89
90 During most operation hidden should be filtered."""
90 During most operation hidden should be filtered."""
91 assert not repo.changelog.filteredrevs
91 assert not repo.changelog.filteredrevs
92
92
93 hidden = hideablerevs(repo)
93 hidden = hideablerevs(repo)
94 if hidden:
94 if hidden:
95 hidden = set(hidden - pinnedrevs(repo))
95 hidden = set(hidden - pinnedrevs(repo))
96 if visibilityexceptions:
96 if visibilityexceptions:
97 hidden -= visibilityexceptions
97 hidden -= visibilityexceptions
98 pfunc = repo.changelog.parentrevs
98 pfunc = repo.changelog.parentrevs
99 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
99 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
100
100
101 visible = mutable - hidden
101 visible = mutable - hidden
102 _revealancestors(pfunc, hidden, visible)
102 _revealancestors(pfunc, hidden, visible)
103 return frozenset(hidden)
103 return frozenset(hidden)
104
104
105
105
106 def computesecret(repo, visibilityexceptions=None):
106 def computesecret(repo, visibilityexceptions=None):
107 """compute the set of revision that can never be exposed through hgweb
107 """compute the set of revision that can never be exposed through hgweb
108
108
109 Changeset in the secret phase (or above) should stay unaccessible."""
109 Changeset in the secret phase (or above) should stay unaccessible."""
110 assert not repo.changelog.filteredrevs
110 assert not repo.changelog.filteredrevs
111 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
111 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
112 return frozenset(secrets)
112 return frozenset(secrets)
113
113
114
114
115 def computeunserved(repo, visibilityexceptions=None):
115 def computeunserved(repo, visibilityexceptions=None):
116 """compute the set of revision that should be filtered when used a server
116 """compute the set of revision that should be filtered when used a server
117
117
118 Secret and hidden changeset should not pretend to be here."""
118 Secret and hidden changeset should not pretend to be here."""
119 assert not repo.changelog.filteredrevs
119 assert not repo.changelog.filteredrevs
120 # fast path in simple case to avoid impact of non optimised code
120 # fast path in simple case to avoid impact of non optimised code
121 hiddens = filterrevs(repo, b'visible')
121 hiddens = filterrevs(repo, b'visible')
122 secrets = filterrevs(repo, b'served.hidden')
122 secrets = filterrevs(repo, b'served.hidden')
123 if secrets:
123 if secrets:
124 return frozenset(hiddens | secrets)
124 return frozenset(hiddens | secrets)
125 else:
125 else:
126 return hiddens
126 return hiddens
127
127
128
128
129 def computemutable(repo, visibilityexceptions=None):
129 def computemutable(repo, visibilityexceptions=None):
130 assert not repo.changelog.filteredrevs
130 assert not repo.changelog.filteredrevs
131 # fast check to avoid revset call on huge repo
131 # fast check to avoid revset call on huge repo
132 if any(repo._phasecache.phaseroots[1:]):
132 if repo._phasecache.hasnonpublicphases(repo):
133 getphase = repo._phasecache.phase
133 getphase = repo._phasecache.phase
134 maymutable = filterrevs(repo, b'base')
134 maymutable = filterrevs(repo, b'base')
135 return frozenset(r for r in maymutable if getphase(repo, r))
135 return frozenset(r for r in maymutable if getphase(repo, r))
136 return frozenset()
136 return frozenset()
137
137
138
138
139 def computeimpactable(repo, visibilityexceptions=None):
139 def computeimpactable(repo, visibilityexceptions=None):
140 """Everything impactable by mutable revision
140 """Everything impactable by mutable revision
141
141
142 The immutable filter still have some chance to get invalidated. This will
142 The immutable filter still have some chance to get invalidated. This will
143 happen when:
143 happen when:
144
144
145 - you garbage collect hidden changeset,
145 - you garbage collect hidden changeset,
146 - public phase is moved backward,
146 - public phase is moved backward,
147 - something is changed in the filtering (this could be fixed)
147 - something is changed in the filtering (this could be fixed)
148
148
149 This filter out any mutable changeset and any public changeset that may be
149 This filter out any mutable changeset and any public changeset that may be
150 impacted by something happening to a mutable revision.
150 impacted by something happening to a mutable revision.
151
151
152 This is achieved by filtered everything with a revision number egal or
152 This is achieved by filtered everything with a revision number egal or
153 higher than the first mutable changeset is filtered."""
153 higher than the first mutable changeset is filtered."""
154 assert not repo.changelog.filteredrevs
154 assert not repo.changelog.filteredrevs
155 cl = repo.changelog
155 cl = repo.changelog
156 firstmutable = len(cl)
156 firstmutable = len(cl)
157 for roots in repo._phasecache.phaseroots[1:]:
157 roots = repo._phasecache.nonpublicphaseroots(repo)
158 if roots:
158 if roots:
159 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
159 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
160 # protect from nullrev root
160 # protect from nullrev root
161 firstmutable = max(0, firstmutable)
161 firstmutable = max(0, firstmutable)
162 return frozenset(pycompat.xrange(firstmutable, len(cl)))
162 return frozenset(pycompat.xrange(firstmutable, len(cl)))
163
163
164
164
165 # function to compute filtered set
165 # function to compute filtered set
166 #
166 #
167 # When adding a new filter you MUST update the table at:
167 # When adding a new filter you MUST update the table at:
168 # mercurial.utils.repoviewutil.subsettable
168 # mercurial.utils.repoviewutil.subsettable
169 # Otherwise your filter will have to recompute all its branches cache
169 # Otherwise your filter will have to recompute all its branches cache
170 # from scratch (very slow).
170 # from scratch (very slow).
171 filtertable = {
171 filtertable = {
172 b'visible': computehidden,
172 b'visible': computehidden,
173 b'visible-hidden': computehidden,
173 b'visible-hidden': computehidden,
174 b'served.hidden': computesecret,
174 b'served.hidden': computesecret,
175 b'served': computeunserved,
175 b'served': computeunserved,
176 b'immutable': computemutable,
176 b'immutable': computemutable,
177 b'base': computeimpactable,
177 b'base': computeimpactable,
178 }
178 }
179
179
180 # set of filter level that will include the working copy parent no matter what.
180 # set of filter level that will include the working copy parent no matter what.
181 filter_has_wc = {b'visible', b'visible-hidden'}
181 filter_has_wc = {b'visible', b'visible-hidden'}
182
182
183 _basefiltername = list(filtertable)
183 _basefiltername = list(filtertable)
184
184
185
185
186 def extrafilter(ui):
186 def extrafilter(ui):
187 """initialize extra filter and return its id
187 """initialize extra filter and return its id
188
188
189 If extra filtering is configured, we make sure the associated filtered view
189 If extra filtering is configured, we make sure the associated filtered view
190 are declared and return the associated id.
190 are declared and return the associated id.
191 """
191 """
192 frevs = ui.config(b'experimental', b'extra-filter-revs')
192 frevs = ui.config(b'experimental', b'extra-filter-revs')
193 if frevs is None:
193 if frevs is None:
194 return None
194 return None
195
195
196 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
196 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
197
197
198 combine = lambda fname: fname + b'%' + fid
198 combine = lambda fname: fname + b'%' + fid
199
199
200 subsettable = repoviewutil.subsettable
200 subsettable = repoviewutil.subsettable
201
201
202 if combine(b'base') not in filtertable:
202 if combine(b'base') not in filtertable:
203 for name in _basefiltername:
203 for name in _basefiltername:
204
204
205 def extrafilteredrevs(repo, *args, **kwargs):
205 def extrafilteredrevs(repo, *args, **kwargs):
206 baserevs = filtertable[name](repo, *args, **kwargs)
206 baserevs = filtertable[name](repo, *args, **kwargs)
207 extrarevs = frozenset(repo.revs(frevs))
207 extrarevs = frozenset(repo.revs(frevs))
208 return baserevs | extrarevs
208 return baserevs | extrarevs
209
209
210 filtertable[combine(name)] = extrafilteredrevs
210 filtertable[combine(name)] = extrafilteredrevs
211 if name in subsettable:
211 if name in subsettable:
212 subsettable[combine(name)] = combine(subsettable[name])
212 subsettable[combine(name)] = combine(subsettable[name])
213 return fid
213 return fid
214
214
215
215
216 def filterrevs(repo, filtername, visibilityexceptions=None):
216 def filterrevs(repo, filtername, visibilityexceptions=None):
217 """returns set of filtered revision for this filter name
217 """returns set of filtered revision for this filter name
218
218
219 visibilityexceptions is a set of revs which must are exceptions for
219 visibilityexceptions is a set of revs which must are exceptions for
220 hidden-state and must be visible. They are dynamic and hence we should not
220 hidden-state and must be visible. They are dynamic and hence we should not
221 cache it's result"""
221 cache it's result"""
222 if filtername not in repo.filteredrevcache:
222 if filtername not in repo.filteredrevcache:
223 if repo.ui.configbool(b'devel', b'debug.repo-filters'):
223 if repo.ui.configbool(b'devel', b'debug.repo-filters'):
224 msg = b'computing revision filter for "%s"'
224 msg = b'computing revision filter for "%s"'
225 msg %= filtername
225 msg %= filtername
226 if repo.ui.tracebackflag and repo.ui.debugflag:
226 if repo.ui.tracebackflag and repo.ui.debugflag:
227 # XXX use ui.write_err
227 # XXX use ui.write_err
228 util.debugstacktrace(
228 util.debugstacktrace(
229 msg,
229 msg,
230 f=repo.ui._fout,
230 f=repo.ui._fout,
231 otherf=repo.ui._ferr,
231 otherf=repo.ui._ferr,
232 prefix=b'debug.filters: ',
232 prefix=b'debug.filters: ',
233 )
233 )
234 else:
234 else:
235 repo.ui.debug(b'debug.filters: %s\n' % msg)
235 repo.ui.debug(b'debug.filters: %s\n' % msg)
236 func = filtertable[filtername]
236 func = filtertable[filtername]
237 if visibilityexceptions:
237 if visibilityexceptions:
238 return func(repo.unfiltered, visibilityexceptions)
238 return func(repo.unfiltered, visibilityexceptions)
239 repo.filteredrevcache[filtername] = func(repo.unfiltered())
239 repo.filteredrevcache[filtername] = func(repo.unfiltered())
240 return repo.filteredrevcache[filtername]
240 return repo.filteredrevcache[filtername]
241
241
242
242
243 def wrapchangelog(unfichangelog, filteredrevs):
243 def wrapchangelog(unfichangelog, filteredrevs):
244 cl = copy.copy(unfichangelog)
244 cl = copy.copy(unfichangelog)
245 cl.filteredrevs = filteredrevs
245 cl.filteredrevs = filteredrevs
246
246
247 class filteredchangelog(filteredchangelogmixin, cl.__class__):
247 class filteredchangelog(filteredchangelogmixin, cl.__class__):
248 pass
248 pass
249
249
250 cl.__class__ = filteredchangelog
250 cl.__class__ = filteredchangelog
251
251
252 return cl
252 return cl
253
253
254
254
255 class filteredchangelogmixin(object):
255 class filteredchangelogmixin(object):
256 def tiprev(self):
256 def tiprev(self):
257 """filtered version of revlog.tiprev"""
257 """filtered version of revlog.tiprev"""
258 for i in pycompat.xrange(len(self) - 1, -2, -1):
258 for i in pycompat.xrange(len(self) - 1, -2, -1):
259 if i not in self.filteredrevs:
259 if i not in self.filteredrevs:
260 return i
260 return i
261
261
262 def __contains__(self, rev):
262 def __contains__(self, rev):
263 """filtered version of revlog.__contains__"""
263 """filtered version of revlog.__contains__"""
264 return 0 <= rev < len(self) and rev not in self.filteredrevs
264 return 0 <= rev < len(self) and rev not in self.filteredrevs
265
265
266 def __iter__(self):
266 def __iter__(self):
267 """filtered version of revlog.__iter__"""
267 """filtered version of revlog.__iter__"""
268
268
269 def filterediter():
269 def filterediter():
270 for i in pycompat.xrange(len(self)):
270 for i in pycompat.xrange(len(self)):
271 if i not in self.filteredrevs:
271 if i not in self.filteredrevs:
272 yield i
272 yield i
273
273
274 return filterediter()
274 return filterediter()
275
275
276 def revs(self, start=0, stop=None):
276 def revs(self, start=0, stop=None):
277 """filtered version of revlog.revs"""
277 """filtered version of revlog.revs"""
278 for i in super(filteredchangelogmixin, self).revs(start, stop):
278 for i in super(filteredchangelogmixin, self).revs(start, stop):
279 if i not in self.filteredrevs:
279 if i not in self.filteredrevs:
280 yield i
280 yield i
281
281
282 def _checknofilteredinrevs(self, revs):
282 def _checknofilteredinrevs(self, revs):
283 """raise the appropriate error if 'revs' contains a filtered revision
283 """raise the appropriate error if 'revs' contains a filtered revision
284
284
285 This returns a version of 'revs' to be used thereafter by the caller.
285 This returns a version of 'revs' to be used thereafter by the caller.
286 In particular, if revs is an iterator, it is converted into a set.
286 In particular, if revs is an iterator, it is converted into a set.
287 """
287 """
288 safehasattr = util.safehasattr
288 safehasattr = util.safehasattr
289 if safehasattr(revs, '__next__'):
289 if safehasattr(revs, '__next__'):
290 # Note that inspect.isgenerator() is not true for iterators,
290 # Note that inspect.isgenerator() is not true for iterators,
291 revs = set(revs)
291 revs = set(revs)
292
292
293 filteredrevs = self.filteredrevs
293 filteredrevs = self.filteredrevs
294 if safehasattr(revs, 'first'): # smartset
294 if safehasattr(revs, 'first'): # smartset
295 offenders = revs & filteredrevs
295 offenders = revs & filteredrevs
296 else:
296 else:
297 offenders = filteredrevs.intersection(revs)
297 offenders = filteredrevs.intersection(revs)
298
298
299 for rev in offenders:
299 for rev in offenders:
300 raise error.FilteredIndexError(rev)
300 raise error.FilteredIndexError(rev)
301 return revs
301 return revs
302
302
303 def headrevs(self, revs=None):
303 def headrevs(self, revs=None):
304 if revs is None:
304 if revs is None:
305 try:
305 try:
306 return self.index.headrevsfiltered(self.filteredrevs)
306 return self.index.headrevsfiltered(self.filteredrevs)
307 # AttributeError covers non-c-extension environments and
307 # AttributeError covers non-c-extension environments and
308 # old c extensions without filter handling.
308 # old c extensions without filter handling.
309 except AttributeError:
309 except AttributeError:
310 return self._headrevs()
310 return self._headrevs()
311
311
312 revs = self._checknofilteredinrevs(revs)
312 revs = self._checknofilteredinrevs(revs)
313 return super(filteredchangelogmixin, self).headrevs(revs)
313 return super(filteredchangelogmixin, self).headrevs(revs)
314
314
315 def strip(self, *args, **kwargs):
315 def strip(self, *args, **kwargs):
316 # XXX make something better than assert
316 # XXX make something better than assert
317 # We can't expect proper strip behavior if we are filtered.
317 # We can't expect proper strip behavior if we are filtered.
318 assert not self.filteredrevs
318 assert not self.filteredrevs
319 super(filteredchangelogmixin, self).strip(*args, **kwargs)
319 super(filteredchangelogmixin, self).strip(*args, **kwargs)
320
320
321 def rev(self, node):
321 def rev(self, node):
322 """filtered version of revlog.rev"""
322 """filtered version of revlog.rev"""
323 r = super(filteredchangelogmixin, self).rev(node)
323 r = super(filteredchangelogmixin, self).rev(node)
324 if r in self.filteredrevs:
324 if r in self.filteredrevs:
325 raise error.FilteredLookupError(
325 raise error.FilteredLookupError(
326 hex(node), self.indexfile, _(b'filtered node')
326 hex(node), self.indexfile, _(b'filtered node')
327 )
327 )
328 return r
328 return r
329
329
330 def node(self, rev):
330 def node(self, rev):
331 """filtered version of revlog.node"""
331 """filtered version of revlog.node"""
332 if rev in self.filteredrevs:
332 if rev in self.filteredrevs:
333 raise error.FilteredIndexError(rev)
333 raise error.FilteredIndexError(rev)
334 return super(filteredchangelogmixin, self).node(rev)
334 return super(filteredchangelogmixin, self).node(rev)
335
335
336 def linkrev(self, rev):
336 def linkrev(self, rev):
337 """filtered version of revlog.linkrev"""
337 """filtered version of revlog.linkrev"""
338 if rev in self.filteredrevs:
338 if rev in self.filteredrevs:
339 raise error.FilteredIndexError(rev)
339 raise error.FilteredIndexError(rev)
340 return super(filteredchangelogmixin, self).linkrev(rev)
340 return super(filteredchangelogmixin, self).linkrev(rev)
341
341
342 def parentrevs(self, rev):
342 def parentrevs(self, rev):
343 """filtered version of revlog.parentrevs"""
343 """filtered version of revlog.parentrevs"""
344 if rev in self.filteredrevs:
344 if rev in self.filteredrevs:
345 raise error.FilteredIndexError(rev)
345 raise error.FilteredIndexError(rev)
346 return super(filteredchangelogmixin, self).parentrevs(rev)
346 return super(filteredchangelogmixin, self).parentrevs(rev)
347
347
348 def flags(self, rev):
348 def flags(self, rev):
349 """filtered version of revlog.flags"""
349 """filtered version of revlog.flags"""
350 if rev in self.filteredrevs:
350 if rev in self.filteredrevs:
351 raise error.FilteredIndexError(rev)
351 raise error.FilteredIndexError(rev)
352 return super(filteredchangelogmixin, self).flags(rev)
352 return super(filteredchangelogmixin, self).flags(rev)
353
353
354
354
355 class repoview(object):
355 class repoview(object):
356 """Provide a read/write view of a repo through a filtered changelog
356 """Provide a read/write view of a repo through a filtered changelog
357
357
358 This object is used to access a filtered version of a repository without
358 This object is used to access a filtered version of a repository without
359 altering the original repository object itself. We can not alter the
359 altering the original repository object itself. We can not alter the
360 original object for two main reasons:
360 original object for two main reasons:
361 - It prevents the use of a repo with multiple filters at the same time. In
361 - It prevents the use of a repo with multiple filters at the same time. In
362 particular when multiple threads are involved.
362 particular when multiple threads are involved.
363 - It makes scope of the filtering harder to control.
363 - It makes scope of the filtering harder to control.
364
364
365 This object behaves very closely to the original repository. All attribute
365 This object behaves very closely to the original repository. All attribute
366 operations are done on the original repository:
366 operations are done on the original repository:
367 - An access to `repoview.someattr` actually returns `repo.someattr`,
367 - An access to `repoview.someattr` actually returns `repo.someattr`,
368 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
368 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
369 - A deletion of `repoview.someattr` actually drops `someattr`
369 - A deletion of `repoview.someattr` actually drops `someattr`
370 from `repo.__dict__`.
370 from `repo.__dict__`.
371
371
372 The only exception is the `changelog` property. It is overridden to return
372 The only exception is the `changelog` property. It is overridden to return
373 a (surface) copy of `repo.changelog` with some revisions filtered. The
373 a (surface) copy of `repo.changelog` with some revisions filtered. The
374 `filtername` attribute of the view control the revisions that need to be
374 `filtername` attribute of the view control the revisions that need to be
375 filtered. (the fact the changelog is copied is an implementation detail).
375 filtered. (the fact the changelog is copied is an implementation detail).
376
376
377 Unlike attributes, this object intercepts all method calls. This means that
377 Unlike attributes, this object intercepts all method calls. This means that
378 all methods are run on the `repoview` object with the filtered `changelog`
378 all methods are run on the `repoview` object with the filtered `changelog`
379 property. For this purpose the simple `repoview` class must be mixed with
379 property. For this purpose the simple `repoview` class must be mixed with
380 the actual class of the repository. This ensures that the resulting
380 the actual class of the repository. This ensures that the resulting
381 `repoview` object have the very same methods than the repo object. This
381 `repoview` object have the very same methods than the repo object. This
382 leads to the property below.
382 leads to the property below.
383
383
384 repoview.method() --> repo.__class__.method(repoview)
384 repoview.method() --> repo.__class__.method(repoview)
385
385
386 The inheritance has to be done dynamically because `repo` can be of any
386 The inheritance has to be done dynamically because `repo` can be of any
387 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
387 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
388 """
388 """
389
389
390 def __init__(self, repo, filtername, visibilityexceptions=None):
390 def __init__(self, repo, filtername, visibilityexceptions=None):
391 object.__setattr__(self, '_unfilteredrepo', repo)
391 object.__setattr__(self, '_unfilteredrepo', repo)
392 object.__setattr__(self, 'filtername', filtername)
392 object.__setattr__(self, 'filtername', filtername)
393 object.__setattr__(self, '_clcachekey', None)
393 object.__setattr__(self, '_clcachekey', None)
394 object.__setattr__(self, '_clcache', None)
394 object.__setattr__(self, '_clcache', None)
395 # revs which are exceptions and must not be hidden
395 # revs which are exceptions and must not be hidden
396 object.__setattr__(self, '_visibilityexceptions', visibilityexceptions)
396 object.__setattr__(self, '_visibilityexceptions', visibilityexceptions)
397
397
398 # not a propertycache on purpose we shall implement a proper cache later
398 # not a propertycache on purpose we shall implement a proper cache later
399 @property
399 @property
400 def changelog(self):
400 def changelog(self):
401 """return a filtered version of the changeset
401 """return a filtered version of the changeset
402
402
403 this changelog must not be used for writing"""
403 this changelog must not be used for writing"""
404 # some cache may be implemented later
404 # some cache may be implemented later
405 unfi = self._unfilteredrepo
405 unfi = self._unfilteredrepo
406 unfichangelog = unfi.changelog
406 unfichangelog = unfi.changelog
407 # bypass call to changelog.method
407 # bypass call to changelog.method
408 unfiindex = unfichangelog.index
408 unfiindex = unfichangelog.index
409 unfilen = len(unfiindex)
409 unfilen = len(unfiindex)
410 unfinode = unfiindex[unfilen - 1][7]
410 unfinode = unfiindex[unfilen - 1][7]
411 with util.timedcm('repo filter for %s', self.filtername):
411 with util.timedcm('repo filter for %s', self.filtername):
412 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
412 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
413 cl = self._clcache
413 cl = self._clcache
414 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
414 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
415 # if cl.index is not unfiindex, unfi.changelog would be
415 # if cl.index is not unfiindex, unfi.changelog would be
416 # recreated, and our clcache refers to garbage object
416 # recreated, and our clcache refers to garbage object
417 if cl is not None and (
417 if cl is not None and (
418 cl.index is not unfiindex or newkey != self._clcachekey
418 cl.index is not unfiindex or newkey != self._clcachekey
419 ):
419 ):
420 cl = None
420 cl = None
421 # could have been made None by the previous if
421 # could have been made None by the previous if
422 if cl is None:
422 if cl is None:
423 # Only filter if there's something to filter
423 # Only filter if there's something to filter
424 cl = wrapchangelog(unfichangelog, revs) if revs else unfichangelog
424 cl = wrapchangelog(unfichangelog, revs) if revs else unfichangelog
425 object.__setattr__(self, '_clcache', cl)
425 object.__setattr__(self, '_clcache', cl)
426 object.__setattr__(self, '_clcachekey', newkey)
426 object.__setattr__(self, '_clcachekey', newkey)
427 return cl
427 return cl
428
428
429 def unfiltered(self):
429 def unfiltered(self):
430 """Return an unfiltered version of a repo"""
430 """Return an unfiltered version of a repo"""
431 return self._unfilteredrepo
431 return self._unfilteredrepo
432
432
433 def filtered(self, name, visibilityexceptions=None):
433 def filtered(self, name, visibilityexceptions=None):
434 """Return a filtered version of a repository"""
434 """Return a filtered version of a repository"""
435 if name == self.filtername and not visibilityexceptions:
435 if name == self.filtername and not visibilityexceptions:
436 return self
436 return self
437 return self.unfiltered().filtered(name, visibilityexceptions)
437 return self.unfiltered().filtered(name, visibilityexceptions)
438
438
439 def __repr__(self):
439 def __repr__(self):
440 return '<%s:%s %r>' % (
440 return '<%s:%s %r>' % (
441 self.__class__.__name__,
441 self.__class__.__name__,
442 pycompat.sysstr(self.filtername),
442 pycompat.sysstr(self.filtername),
443 self.unfiltered(),
443 self.unfiltered(),
444 )
444 )
445
445
446 # everything access are forwarded to the proxied repo
446 # everything access are forwarded to the proxied repo
447 def __getattr__(self, attr):
447 def __getattr__(self, attr):
448 return getattr(self._unfilteredrepo, attr)
448 return getattr(self._unfilteredrepo, attr)
449
449
450 def __setattr__(self, attr, value):
450 def __setattr__(self, attr, value):
451 return setattr(self._unfilteredrepo, attr, value)
451 return setattr(self._unfilteredrepo, attr, value)
452
452
453 def __delattr__(self, attr):
453 def __delattr__(self, attr):
454 return delattr(self._unfilteredrepo, attr)
454 return delattr(self._unfilteredrepo, attr)
455
455
456
456
457 # Python <3.4 easily leaks types via __mro__. See
457 # Python <3.4 easily leaks types via __mro__. See
458 # https://bugs.python.org/issue17950. We cache dynamically created types
458 # https://bugs.python.org/issue17950. We cache dynamically created types
459 # so they won't be leaked on every invocation of repo.filtered().
459 # so they won't be leaked on every invocation of repo.filtered().
460 _filteredrepotypes = weakref.WeakKeyDictionary()
460 _filteredrepotypes = weakref.WeakKeyDictionary()
461
461
462
462
463 def newtype(base):
463 def newtype(base):
464 """Create a new type with the repoview mixin and the given base class"""
464 """Create a new type with the repoview mixin and the given base class"""
465 if base not in _filteredrepotypes:
465 if base not in _filteredrepotypes:
466
466
467 class filteredrepo(repoview, base):
467 class filteredrepo(repoview, base):
468 pass
468 pass
469
469
470 _filteredrepotypes[base] = filteredrepo
470 _filteredrepotypes[base] = filteredrepo
471 return _filteredrepotypes[base]
471 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now