##// END OF EJS Templates
clfilter: phases logic should be unfiltered...
Pierre-Yves David -
r18002:9bc5873e default
parent child Browse files
Show More
@@ -1,393 +1,400 b''
1 1 """ Mercurial phases support code
2 2
3 3 ---
4 4
5 5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 6 Logilab SA <contact@logilab.fr>
7 7 Augie Fackler <durin42@gmail.com>
8 8
9 9 This software may be used and distributed according to the terms
10 10 of the GNU General Public License version 2 or any later version.
11 11
12 12 ---
13 13
14 14 This module implements most phase logic in mercurial.
15 15
16 16
17 17 Basic Concept
18 18 =============
19 19
20 20 A 'changeset phase' is an indicator that tells us how a changeset is
21 21 manipulated and communicated. The details of each phase is described
22 22 below, here we describe the properties they have in common.
23 23
24 24 Like bookmarks, phases are not stored in history and thus are not
25 25 permanent and leave no audit trail.
26 26
27 27 First, no changeset can be in two phases at once. Phases are ordered,
28 28 so they can be considered from lowest to highest. The default, lowest
29 29 phase is 'public' - this is the normal phase of existing changesets. A
30 30 child changeset can not be in a lower phase than its parents.
31 31
32 32 These phases share a hierarchy of traits:
33 33
34 34 immutable shared
35 35 public: X X
36 36 draft: X
37 37 secret:
38 38
39 39 Local commits are draft by default.
40 40
41 41 Phase Movement and Exchange
42 42 ===========================
43 43
44 44 Phase data is exchanged by pushkey on pull and push. Some servers have
45 45 a publish option set, we call such a server a "publishing server".
46 46 Pushing a draft changeset to a publishing server changes the phase to
47 47 public.
48 48
49 49 A small list of fact/rules define the exchange of phase:
50 50
51 51 * old client never changes server states
52 52 * pull never changes server states
53 53 * publish and old server changesets are seen as public by client
54 54 * any secret changeset seen in another repository is lowered to at
55 55 least draft
56 56
57 57 Here is the final table summing up the 49 possible use cases of phase
58 58 exchange:
59 59
60 60 server
61 61 old publish non-publish
62 62 N X N D P N D P
63 63 old client
64 64 pull
65 65 N - X/X - X/D X/P - X/D X/P
66 66 X - X/X - X/D X/P - X/D X/P
67 67 push
68 68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 69 new client
70 70 pull
71 71 N - P/X - P/D P/P - D/D P/P
72 72 D - P/X - P/D P/P - D/D P/P
73 73 P - P/X - P/D P/P - P/D P/P
74 74 push
75 75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 76 P P/X P/X P/P P/P P/P P/P P/P P/P
77 77
78 78 Legend:
79 79
80 80 A/B = final state on client / state on server
81 81
82 82 * N = new/not present,
83 83 * P = public,
84 84 * D = draft,
85 85 * X = not tracked (i.e., the old client or server has no internal
86 86 way of recording the phase.)
87 87
88 88 passive = only pushes
89 89
90 90
91 91 A cell here can be read like this:
92 92
93 93 "When a new client pushes a draft changeset (D) to a publishing
94 94 server where it's not present (N), it's marked public on both
95 95 sides (P/P)."
96 96
97 97 Note: old client behave as a publishing server with draft only content
98 98 - other people see it as public
99 99 - content is pushed as draft
100 100
101 101 """
102 102
103 103 import errno
104 104 from node import nullid, nullrev, bin, hex, short
105 105 from i18n import _
106 106 import util, error
107 107 import obsolete
108 108
109 109 allphases = public, draft, secret = range(3)
110 110 trackedphases = allphases[1:]
111 111 phasenames = ['public', 'draft', 'secret']
112 112
113 113 def _filterunknown(ui, changelog, phaseroots):
114 114 """remove unknown nodes from the phase boundary
115 115
116 116 Nothing is lost as unknown nodes only hold data for their descendants.
117 117 """
118 118 updated = False
119 119 nodemap = changelog.nodemap # to filter unknown nodes
120 120 for phase, nodes in enumerate(phaseroots):
121 121 missing = [node for node in nodes if node not in nodemap]
122 122 if missing:
123 123 for mnode in missing:
124 124 ui.debug(
125 125 'removing unknown node %s from %i-phase boundary\n'
126 126 % (short(mnode), phase))
127 127 nodes.symmetric_difference_update(missing)
128 128 updated = True
129 129 return updated
130 130
131 131 def _readroots(repo, phasedefaults=None):
132 132 """Read phase roots from disk
133 133
134 134 phasedefaults is a list of fn(repo, roots) callable, which are
135 135 executed if the phase roots file does not exist. When phases are
136 136 being initialized on an existing repository, this could be used to
137 137 set selected changesets phase to something else than public.
138 138
139 139 Return (roots, dirty) where dirty is true if roots differ from
140 140 what is being stored.
141 141 """
142 repo = repo.unfiltered()
142 143 dirty = False
143 144 roots = [set() for i in allphases]
144 145 try:
145 146 f = repo.sopener('phaseroots')
146 147 try:
147 148 for line in f:
148 149 phase, nh = line.split()
149 150 roots[int(phase)].add(bin(nh))
150 151 finally:
151 152 f.close()
152 153 except IOError, inst:
153 154 if inst.errno != errno.ENOENT:
154 155 raise
155 156 if phasedefaults:
156 157 for f in phasedefaults:
157 158 roots = f(repo, roots)
158 159 dirty = True
159 160 if _filterunknown(repo.ui, repo.changelog, roots):
160 161 dirty = True
161 162 return roots, dirty
162 163
163 164 class phasecache(object):
164 165 def __init__(self, repo, phasedefaults, _load=True):
165 166 if _load:
166 167 # Cheap trick to allow shallow-copy without copy module
167 168 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
168 169 self.opener = repo.sopener
169 170 self._phaserevs = None
170 171
171 172 def copy(self):
172 173 # Shallow copy meant to ensure isolation in
173 174 # advance/retractboundary(), nothing more.
174 175 ph = phasecache(None, None, _load=False)
175 176 ph.phaseroots = self.phaseroots[:]
176 177 ph.dirty = self.dirty
177 178 ph.opener = self.opener
178 179 ph._phaserevs = self._phaserevs
179 180 return ph
180 181
181 182 def replace(self, phcache):
182 183 for a in 'phaseroots dirty opener _phaserevs'.split():
183 184 setattr(self, a, getattr(phcache, a))
184 185
185 186 def getphaserevs(self, repo, rebuild=False):
186 187 if rebuild or self._phaserevs is None:
188 repo = repo.unfiltered()
187 189 revs = [public] * len(repo.changelog)
188 190 for phase in trackedphases:
189 191 roots = map(repo.changelog.rev, self.phaseroots[phase])
190 192 if roots:
191 193 for rev in roots:
192 194 revs[rev] = phase
193 195 for rev in repo.changelog.descendants(roots):
194 196 revs[rev] = phase
195 197 self._phaserevs = revs
196 198 return self._phaserevs
197 199
198 200 def phase(self, repo, rev):
199 201 # We need a repo argument here to be able to build _phaserevs
200 202 # if necessary. The repository instance is not stored in
201 203 # phasecache to avoid reference cycles. The changelog instance
202 204 # is not stored because it is a filecache() property and can
203 205 # be replaced without us being notified.
204 206 if rev == nullrev:
205 207 return public
206 208 if self._phaserevs is None or rev >= len(self._phaserevs):
207 209 self._phaserevs = self.getphaserevs(repo, rebuild=True)
208 210 return self._phaserevs[rev]
209 211
210 212 def write(self):
211 213 if not self.dirty:
212 214 return
213 215 f = self.opener('phaseroots', 'w', atomictemp=True)
214 216 try:
215 217 for phase, roots in enumerate(self.phaseroots):
216 218 for h in roots:
217 219 f.write('%i %s\n' % (phase, hex(h)))
218 220 finally:
219 221 f.close()
220 222 self.dirty = False
221 223
222 224 def _updateroots(self, phase, newroots):
223 225 self.phaseroots[phase] = newroots
224 226 self._phaserevs = None
225 227 self.dirty = True
226 228
227 229 def advanceboundary(self, repo, targetphase, nodes):
228 230 # Be careful to preserve shallow-copied values: do not update
229 231 # phaseroots values, replace them.
230 232
233 repo = repo.unfiltered()
231 234 delroots = [] # set of root deleted by this path
232 235 for phase in xrange(targetphase + 1, len(allphases)):
233 236 # filter nodes that are not in a compatible phase already
234 237 nodes = [n for n in nodes
235 238 if self.phase(repo, repo[n].rev()) >= phase]
236 239 if not nodes:
237 240 break # no roots to move anymore
238 241 olds = self.phaseroots[phase]
239 242 roots = set(ctx.node() for ctx in repo.set(
240 243 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
241 244 if olds != roots:
242 245 self._updateroots(phase, roots)
243 246 # some roots may need to be declared for lower phases
244 247 delroots.extend(olds - roots)
245 248 # declare deleted root in the target phase
246 249 if targetphase != 0:
247 250 self.retractboundary(repo, targetphase, delroots)
248 251 obsolete.clearobscaches(repo)
249 252
250 253 def retractboundary(self, repo, targetphase, nodes):
251 254 # Be careful to preserve shallow-copied values: do not update
252 255 # phaseroots values, replace them.
253 256
257 repo = repo.unfiltered()
254 258 currentroots = self.phaseroots[targetphase]
255 259 newroots = [n for n in nodes
256 260 if self.phase(repo, repo[n].rev()) < targetphase]
257 261 if newroots:
258 262 if nullid in newroots:
259 263 raise util.Abort(_('cannot change null revision phase'))
260 264 currentroots = currentroots.copy()
261 265 currentroots.update(newroots)
262 266 ctxs = repo.set('roots(%ln::)', currentroots)
263 267 currentroots.intersection_update(ctx.node() for ctx in ctxs)
264 268 self._updateroots(targetphase, currentroots)
265 269 obsolete.clearobscaches(repo)
266 270
267 271 def advanceboundary(repo, targetphase, nodes):
268 272 """Add nodes to a phase changing other nodes phases if necessary.
269 273
270 274 This function move boundary *forward* this means that all nodes
271 275 are set in the target phase or kept in a *lower* phase.
272 276
273 277 Simplify boundary to contains phase roots only."""
274 278 phcache = repo._phasecache.copy()
275 279 phcache.advanceboundary(repo, targetphase, nodes)
276 280 repo._phasecache.replace(phcache)
277 281
278 282 def retractboundary(repo, targetphase, nodes):
279 283 """Set nodes back to a phase changing other nodes phases if
280 284 necessary.
281 285
282 286 This function move boundary *backward* this means that all nodes
283 287 are set in the target phase or kept in a *higher* phase.
284 288
285 289 Simplify boundary to contains phase roots only."""
286 290 phcache = repo._phasecache.copy()
287 291 phcache.retractboundary(repo, targetphase, nodes)
288 292 repo._phasecache.replace(phcache)
289 293
290 294 def listphases(repo):
291 295 """List phases root for serialization over pushkey"""
292 296 keys = {}
293 297 value = '%i' % draft
294 298 for root in repo._phasecache.phaseroots[draft]:
295 299 keys[hex(root)] = value
296 300
297 301 if repo.ui.configbool('phases', 'publish', True):
298 302 # Add an extra data to let remote know we are a publishing
299 303 # repo. Publishing repo can't just pretend they are old repo.
300 304 # When pushing to a publishing repo, the client still need to
301 305 # push phase boundary
302 306 #
303 307 # Push do not only push changeset. It also push phase data.
304 308 # New phase data may apply to common changeset which won't be
305 309 # push (as they are common). Here is a very simple example:
306 310 #
307 311 # 1) repo A push changeset X as draft to repo B
308 312 # 2) repo B make changeset X public
309 313 # 3) repo B push to repo A. X is not pushed but the data that
310 314 # X as now public should
311 315 #
312 316 # The server can't handle it on it's own as it has no idea of
313 317 # client phase data.
314 318 keys['publishing'] = 'True'
315 319 return keys
316 320
317 321 def pushphase(repo, nhex, oldphasestr, newphasestr):
318 322 """List phases root for serialization over pushkey"""
323 repo = repo.unfiltered()
319 324 lock = repo.lock()
320 325 try:
321 326 currentphase = repo[nhex].phase()
322 327 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
323 328 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
324 329 if currentphase == oldphase and newphase < oldphase:
325 330 advanceboundary(repo, newphase, [bin(nhex)])
326 331 return 1
327 332 elif currentphase == newphase:
328 333 # raced, but got correct result
329 334 return 1
330 335 else:
331 336 return 0
332 337 finally:
333 338 lock.release()
334 339
335 340 def analyzeremotephases(repo, subset, roots):
336 341 """Compute phases heads and root in a subset of node from root dict
337 342
338 343 * subset is heads of the subset
339 344 * roots is {<nodeid> => phase} mapping. key and value are string.
340 345
341 346 Accept unknown element input
342 347 """
348 repo = repo.unfiltered()
343 349 # build list from dictionary
344 350 draftroots = []
345 351 nodemap = repo.changelog.nodemap # to filter unknown nodes
346 352 for nhex, phase in roots.iteritems():
347 353 if nhex == 'publishing': # ignore data related to publish option
348 354 continue
349 355 node = bin(nhex)
350 356 phase = int(phase)
351 357 if phase == 0:
352 358 if node != nullid:
353 359 repo.ui.warn(_('ignoring inconsistent public root'
354 360 ' from remote: %s\n') % nhex)
355 361 elif phase == 1:
356 362 if node in nodemap:
357 363 draftroots.append(node)
358 364 else:
359 365 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
360 366 % (phase, nhex))
361 367 # compute heads
362 368 publicheads = newheads(repo, subset, draftroots)
363 369 return publicheads, draftroots
364 370
365 371 def newheads(repo, heads, roots):
366 372 """compute new head of a subset minus another
367 373
368 374 * `heads`: define the first subset
369 375 * `roots`: define the second we subtract from the first"""
376 repo = repo.unfiltered()
370 377 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
371 378 heads, roots, roots, heads)
372 379 return [c.node() for c in revset]
373 380
374 381
375 382 def newcommitphase(ui):
376 383 """helper to get the target phase of new commit
377 384
378 385 Handle all possible values for the phases.new-commit options.
379 386
380 387 """
381 388 v = ui.config('phases', 'new-commit', draft)
382 389 try:
383 390 return phasenames.index(v)
384 391 except ValueError:
385 392 try:
386 393 return int(v)
387 394 except ValueError:
388 395 msg = _("phases.new-commit: not a valid phase name ('%s')")
389 396 raise error.ConfigError(msg % v)
390 397
391 398 def hassecret(repo):
392 399 """utility function that check if a repo have any secret changeset."""
393 400 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now