##// END OF EJS Templates
en-us: serialization
timeless@mozdev.org -
r17535:63e302be default
parent child Browse files
Show More
@@ -1,387 +1,387 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
107 107
108 108 allphases = public, draft, secret = range(3)
109 109 trackedphases = allphases[1:]
110 110 phasenames = ['public', 'draft', 'secret']
111 111
112 112 def _filterunknown(ui, changelog, phaseroots):
113 113 """remove unknown nodes from the phase boundary
114 114
115 115 Nothing is lost as unknown nodes only hold data for their descendants.
116 116 """
117 117 updated = False
118 118 nodemap = changelog.nodemap # to filter unknown nodes
119 119 for phase, nodes in enumerate(phaseroots):
120 120 missing = [node for node in nodes if node not in nodemap]
121 121 if missing:
122 122 for mnode in missing:
123 123 ui.debug(
124 124 'removing unknown node %s from %i-phase boundary\n'
125 125 % (short(mnode), phase))
126 126 nodes.symmetric_difference_update(missing)
127 127 updated = True
128 128 return updated
129 129
130 130 def _readroots(repo, phasedefaults=None):
131 131 """Read phase roots from disk
132 132
133 133 phasedefaults is a list of fn(repo, roots) callable, which are
134 134 executed if the phase roots file does not exist. When phases are
135 135 being initialized on an existing repository, this could be used to
136 136 set selected changesets phase to something else than public.
137 137
138 138 Return (roots, dirty) where dirty is true if roots differ from
139 139 what is being stored.
140 140 """
141 141 dirty = False
142 142 roots = [set() for i in allphases]
143 143 try:
144 144 f = repo.sopener('phaseroots')
145 145 try:
146 146 for line in f:
147 147 phase, nh = line.split()
148 148 roots[int(phase)].add(bin(nh))
149 149 finally:
150 150 f.close()
151 151 except IOError, inst:
152 152 if inst.errno != errno.ENOENT:
153 153 raise
154 154 if phasedefaults:
155 155 for f in phasedefaults:
156 156 roots = f(repo, roots)
157 157 dirty = True
158 158 if _filterunknown(repo.ui, repo.changelog, roots):
159 159 dirty = True
160 160 return roots, dirty
161 161
162 162 class phasecache(object):
163 163 def __init__(self, repo, phasedefaults, _load=True):
164 164 if _load:
165 165 # Cheap trick to allow shallow-copy without copy module
166 166 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
167 167 self.opener = repo.sopener
168 168 self._phaserevs = None
169 169
170 170 def copy(self):
171 171 # Shallow copy meant to ensure isolation in
172 172 # advance/retractboundary(), nothing more.
173 173 ph = phasecache(None, None, _load=False)
174 174 ph.phaseroots = self.phaseroots[:]
175 175 ph.dirty = self.dirty
176 176 ph.opener = self.opener
177 177 ph._phaserevs = self._phaserevs
178 178 return ph
179 179
180 180 def replace(self, phcache):
181 181 for a in 'phaseroots dirty opener _phaserevs'.split():
182 182 setattr(self, a, getattr(phcache, a))
183 183
184 184 def getphaserevs(self, repo, rebuild=False):
185 185 if rebuild or self._phaserevs is None:
186 186 revs = [public] * len(repo.changelog)
187 187 for phase in trackedphases:
188 188 roots = map(repo.changelog.rev, self.phaseroots[phase])
189 189 if roots:
190 190 for rev in roots:
191 191 revs[rev] = phase
192 192 for rev in repo.changelog.descendants(roots):
193 193 revs[rev] = phase
194 194 self._phaserevs = revs
195 195 return self._phaserevs
196 196
197 197 def phase(self, repo, rev):
198 198 # We need a repo argument here to be able to build _phaserev
199 199 # if necessary. The repository instance is not stored in
200 200 # phasecache to avoid reference cycles. The changelog instance
201 201 # is not stored because it is a filecache() property and can
202 202 # be replaced without us being notified.
203 203 if rev == nullrev:
204 204 return public
205 205 if self._phaserevs is None or rev >= len(self._phaserevs):
206 206 self._phaserevs = self.getphaserevs(repo, rebuild=True)
207 207 return self._phaserevs[rev]
208 208
209 209 def write(self):
210 210 if not self.dirty:
211 211 return
212 212 f = self.opener('phaseroots', 'w', atomictemp=True)
213 213 try:
214 214 for phase, roots in enumerate(self.phaseroots):
215 215 for h in roots:
216 216 f.write('%i %s\n' % (phase, hex(h)))
217 217 finally:
218 218 f.close()
219 219 self.dirty = False
220 220
221 221 def _updateroots(self, phase, newroots):
222 222 self.phaseroots[phase] = newroots
223 223 self._phaserevs = None
224 224 self.dirty = True
225 225
226 226 def advanceboundary(self, repo, targetphase, nodes):
227 227 # Be careful to preserve shallow-copied values: do not update
228 228 # phaseroots values, replace them.
229 229
230 230 delroots = [] # set of root deleted by this path
231 231 for phase in xrange(targetphase + 1, len(allphases)):
232 232 # filter nodes that are not in a compatible phase already
233 233 nodes = [n for n in nodes
234 234 if self.phase(repo, repo[n].rev()) >= phase]
235 235 if not nodes:
236 236 break # no roots to move anymore
237 237 olds = self.phaseroots[phase]
238 238 roots = set(ctx.node() for ctx in repo.set(
239 239 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
240 240 if olds != roots:
241 241 self._updateroots(phase, roots)
242 242 # some roots may need to be declared for lower phases
243 243 delroots.extend(olds - roots)
244 244 # declare deleted root in the target phase
245 245 if targetphase != 0:
246 246 self.retractboundary(repo, targetphase, delroots)
247 247
248 248 def retractboundary(self, repo, targetphase, nodes):
249 249 # Be careful to preserve shallow-copied values: do not update
250 250 # phaseroots values, replace them.
251 251
252 252 currentroots = self.phaseroots[targetphase]
253 253 newroots = [n for n in nodes
254 254 if self.phase(repo, repo[n].rev()) < targetphase]
255 255 if newroots:
256 256 if nullid in newroots:
257 257 raise util.Abort(_('cannot change null revision phase'))
258 258 currentroots = currentroots.copy()
259 259 currentroots.update(newroots)
260 260 ctxs = repo.set('roots(%ln::)', currentroots)
261 261 currentroots.intersection_update(ctx.node() for ctx in ctxs)
262 262 self._updateroots(targetphase, currentroots)
263 263
264 264 def advanceboundary(repo, targetphase, nodes):
265 265 """Add nodes to a phase changing other nodes phases if necessary.
266 266
267 267 This function move boundary *forward* this means that all nodes
268 268 are set in the target phase or kept in a *lower* phase.
269 269
270 270 Simplify boundary to contains phase roots only."""
271 271 phcache = repo._phasecache.copy()
272 272 phcache.advanceboundary(repo, targetphase, nodes)
273 273 repo._phasecache.replace(phcache)
274 274
275 275 def retractboundary(repo, targetphase, nodes):
276 276 """Set nodes back to a phase changing other nodes phases if
277 277 necessary.
278 278
279 279 This function move boundary *backward* this means that all nodes
280 280 are set in the target phase or kept in a *higher* phase.
281 281
282 282 Simplify boundary to contains phase roots only."""
283 283 phcache = repo._phasecache.copy()
284 284 phcache.retractboundary(repo, targetphase, nodes)
285 285 repo._phasecache.replace(phcache)
286 286
287 287 def listphases(repo):
288 288 """List phases root for serialization over pushkey"""
289 289 keys = {}
290 290 value = '%i' % draft
291 291 for root in repo._phasecache.phaseroots[draft]:
292 292 keys[hex(root)] = value
293 293
294 294 if repo.ui.configbool('phases', 'publish', True):
295 295 # Add an extra data to let remote know we are a publishing
296 296 # repo. Publishing repo can't just pretend they are old repo.
297 297 # When pushing to a publishing repo, the client still need to
298 298 # push phase boundary
299 299 #
300 300 # Push do not only push changeset. It also push phase data.
301 301 # New phase data may apply to common changeset which won't be
302 302 # push (as they are common). Here is a very simple example:
303 303 #
304 304 # 1) repo A push changeset X as draft to repo B
305 305 # 2) repo B make changeset X public
306 306 # 3) repo B push to repo A. X is not pushed but the data that
307 307 # X as now public should
308 308 #
309 309 # The server can't handle it on it's own as it has no idea of
310 310 # client phase data.
311 311 keys['publishing'] = 'True'
312 312 return keys
313 313
314 314 def pushphase(repo, nhex, oldphasestr, newphasestr):
315 """List phases root for serialisation over pushkey"""
315 """List phases root for serialization over pushkey"""
316 316 lock = repo.lock()
317 317 try:
318 318 currentphase = repo[nhex].phase()
319 319 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
320 320 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
321 321 if currentphase == oldphase and newphase < oldphase:
322 322 advanceboundary(repo, newphase, [bin(nhex)])
323 323 return 1
324 324 elif currentphase == newphase:
325 325 # raced, but got correct result
326 326 return 1
327 327 else:
328 328 return 0
329 329 finally:
330 330 lock.release()
331 331
332 332 def analyzeremotephases(repo, subset, roots):
333 333 """Compute phases heads and root in a subset of node from root dict
334 334
335 335 * subset is heads of the subset
336 336 * roots is {<nodeid> => phase} mapping. key and value are string.
337 337
338 338 Accept unknown element input
339 339 """
340 340 # build list from dictionary
341 341 draftroots = []
342 342 nodemap = repo.changelog.nodemap # to filter unknown nodes
343 343 for nhex, phase in roots.iteritems():
344 344 if nhex == 'publishing': # ignore data related to publish option
345 345 continue
346 346 node = bin(nhex)
347 347 phase = int(phase)
348 348 if phase == 0:
349 349 if node != nullid:
350 350 repo.ui.warn(_('ignoring inconsistent public root'
351 351 ' from remote: %s\n') % nhex)
352 352 elif phase == 1:
353 353 if node in nodemap:
354 354 draftroots.append(node)
355 355 else:
356 356 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
357 357 % (phase, nhex))
358 358 # compute heads
359 359 publicheads = newheads(repo, subset, draftroots)
360 360 return publicheads, draftroots
361 361
362 362 def newheads(repo, heads, roots):
363 363 """compute new head of a subset minus another
364 364
365 365 * `heads`: define the first subset
366 366 * `rroots`: define the second we substract to the first"""
367 367 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
368 368 heads, roots, roots, heads)
369 369 return [c.node() for c in revset]
370 370
371 371
372 372 def newcommitphase(ui):
373 373 """helper to get the target phase of new commit
374 374
375 375 Handle all possible values for the phases.new-commit options.
376 376
377 377 """
378 378 v = ui.config('phases', 'new-commit', draft)
379 379 try:
380 380 return phasenames.index(v)
381 381 except ValueError:
382 382 try:
383 383 return int(v)
384 384 except ValueError:
385 385 msg = _("phases.new-commit: not a valid phase name ('%s')")
386 386 raise error.ConfigError(msg % v)
387 387
General Comments 0
You need to be logged in to leave comments. Login now