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