##// END OF EJS Templates
phases: Add a third phase for secret changeset
Pierre-Yves David -
r15696:0cb45fef default
parent child Browse files
Show More
@@ -1,264 +1,264 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 of the
10 10 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 phases' is an indicator that tells us how a changeset is
21 21 manipulated and communicated. The details of each phase is described below,
22 22 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 permanent and
25 25 leave no audit trail.
26 26
27 27 First, no changeset can be in two phases at once. Phases are ordered, so they
28 28 can be considered from lowest to highest. The default, lowest phase is 'public'
29 29 - this is the normal phase of existing changesets. A child changeset can not be
30 30 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
38 38 local commits are draft by default
39 39
40 40 Phase movement and exchange
41 41 ============================
42 42
43 43 Phase data are exchanged by pushkey on pull and push. Some server have a
44 44 publish option set, we call them publishing server. Pushing to such server make
45 45 draft changeset publish.
46 46
47 47 A small list of fact/rules define the exchange of phase:
48 48
49 49 * old client never changes server states
50 50 * pull never changes server states
51 51 * publish and old server csets are seen as public by client
52 52
53 53
54 54 Here is the final table summing up the 49 possible usecase of phase exchange:
55 55
56 56 server
57 57 old publish non-publish
58 58 N X N D P N D P
59 59 old client
60 60 pull
61 61 N - X/X - X/D X/P - X/D X/P
62 62 X - X/X - X/D X/P - X/D X/P
63 63 push
64 64 X X/X X/X X/P X/P X/P X/D X/D X/P
65 65 new client
66 66 pull
67 67 N - P/X - P/D P/P - D/D P/P
68 68 D - P/X - P/D P/P - D/D P/P
69 69 P - P/X - P/D P/P - P/D P/P
70 70 push
71 71 D P/X P/X P/P P/P P/P D/D D/D P/P
72 72 P P/X P/X P/P P/P P/P P/P P/P P/P
73 73
74 74 Legend:
75 75
76 76 A/B = final state on client / state on server
77 77
78 78 * N = new/not present,
79 79 * P = public,
80 80 * D = draft,
81 81 * X = not tracked (ie: the old client or server has no internal way of
82 82 recording the phase.)
83 83
84 84 passive = only pushes
85 85
86 86
87 87 A cell here can be read like this:
88 88
89 89 "When a new client pushes a draft changeset (D) to a publishing server
90 90 where it's not present (N), it's marked public on both sides (P/P)."
91 91
92 92 Note: old client behave as publish server with Draft only content
93 93 - other people see it as public
94 94 - content is pushed as draft
95 95
96 96 """
97 97
98 98 import errno
99 99 from node import nullid, bin, hex, short
100 100 from i18n import _
101 101
102 allphases = range(2)
102 allphases = range(3)
103 103 trackedphases = allphases[1:]
104 104
105 105 def readroots(repo):
106 106 """Read phase roots from disk"""
107 107 roots = [set() for i in allphases]
108 108 roots[0].add(nullid)
109 109 try:
110 110 f = repo.sopener('phaseroots')
111 111 try:
112 112 for line in f:
113 113 phase, nh = line.strip().split()
114 114 roots[int(phase)].add(bin(nh))
115 115 finally:
116 116 f.close()
117 117 except IOError, inst:
118 118 if inst.errno != errno.ENOENT:
119 119 raise
120 120 return roots
121 121
122 122 def writeroots(repo):
123 123 """Write phase roots from disk"""
124 124 f = repo.sopener('phaseroots', 'w', atomictemp=True)
125 125 try:
126 126 for phase, roots in enumerate(repo._phaseroots):
127 127 for h in roots:
128 128 f.write('%i %s\n' % (phase, hex(h)))
129 129 repo._dirtyphases = False
130 130 finally:
131 131 f.close()
132 132
133 133 def filterunknown(repo, phaseroots=None):
134 134 """remove unknown nodes from the phase boundary
135 135
136 136 no data is lost as unknown node only old data for their descentants
137 137 """
138 138 if phaseroots is None:
139 139 phaseroots = repo._phaseroots
140 140 for phase, nodes in enumerate(phaseroots):
141 141 missing = [node for node in nodes if node not in repo]
142 142 if missing:
143 143 for mnode in missing:
144 144 msg = _('Removing unknown node %(n)s from %(p)i-phase boundary')
145 145 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
146 146 nodes.symmetric_difference_update(missing)
147 147 repo._dirtyphases = True
148 148
149 149 def advanceboundary(repo, targetphase, nodes):
150 150 """Add nodes to a phase changing other nodes phases if necessary.
151 151
152 152 This function move boundary *forward* this means that all nodes are set
153 153 in the target phase or kept in a *lower* phase.
154 154
155 155 Simplify boundary to contains phase roots only."""
156 156 delroots = [] # set of root deleted by this path
157 157 for phase in xrange(targetphase + 1, len(allphases)):
158 158 # filter nodes that are not in a compatible phase already
159 159 # XXX rev phase cache might have been invalidated by a previous loop
160 160 # XXX we need to be smarter here
161 161 nodes = [n for n in nodes if repo[n].phase() >= phase]
162 162 if not nodes:
163 163 break # no roots to move anymore
164 164 roots = repo._phaseroots[phase]
165 165 olds = roots.copy()
166 166 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
167 167 roots.clear()
168 168 roots.update(ctx.node() for ctx in ctxs)
169 169 if olds != roots:
170 170 # invalidate cache (we probably could be smarter here
171 171 if '_phaserev' in vars(repo):
172 172 del repo._phaserev
173 173 repo._dirtyphases = True
174 174 # some roots may need to be declared for lower phases
175 175 delroots.extend(olds - roots)
176 176 # declare deleted root in the target phase
177 177 if targetphase != 0:
178 178 retractboundary(repo, targetphase, delroots)
179 179
180 180
181 181 def retractboundary(repo, targetphase, nodes):
182 182 """Set nodes back to a phase changing other nodes phases if necessary.
183 183
184 184 This function move boundary *backward* this means that all nodes are set
185 185 in the target phase or kept in a *higher* phase.
186 186
187 187 Simplify boundary to contains phase roots only."""
188 188 currentroots = repo._phaseroots[targetphase]
189 189 newroots = [n for n in nodes if repo[n].phase() < targetphase]
190 190 if newroots:
191 191 currentroots.update(newroots)
192 192 ctxs = repo.set('roots(%ln::)', currentroots)
193 193 currentroots.intersection_update(ctx.node() for ctx in ctxs)
194 194 if '_phaserev' in vars(repo):
195 195 del repo._phaserev
196 196 repo._dirtyphases = True
197 197
198 198
199 199 def listphases(repo):
200 200 """List phases root for serialisation over pushkey"""
201 201 keys = {}
202 202 for phase in trackedphases:
203 203 for root in repo._phaseroots[phase]:
204 204 keys[hex(root)] = '%i' % phase
205 205 if repo.ui.configbool('phases', 'publish', True):
206 206 # Add an extra data to let remote know we are a publishing repo.
207 207 # Publishing repo can't just pretend they are old repo. When pushing to
208 208 # a publishing repo, the client still need to push phase boundary
209 209 #
210 210 # Push do not only push changeset. It also push phase data. New
211 211 # phase data may apply to common changeset which won't be push (as they
212 212 # are common). Here is a very simple example:
213 213 #
214 214 # 1) repo A push changeset X as draft to repo B
215 215 # 2) repo B make changeset X public
216 216 # 3) repo B push to repo A. X is not pushed but the data that X as now
217 217 # public should
218 218 #
219 219 # The server can't handle it on it's own as it has no idea of client
220 220 # phase data.
221 221 keys['publishing'] = 'True'
222 222 return keys
223 223
224 224 def pushphase(repo, nhex, oldphasestr, newphasestr):
225 225 """List phases root for serialisation over pushkey"""
226 226 lock = repo.lock()
227 227 try:
228 228 currentphase = repo[nhex].phase()
229 229 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
230 230 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
231 231 if currentphase == oldphase and newphase < oldphase:
232 232 advanceboundary(repo, newphase, [bin(nhex)])
233 233 return 1
234 234 else:
235 235 return 0
236 236 finally:
237 237 lock.release()
238 238
239 239 def analyzeremotephases(repo, subset, roots):
240 240 """Compute phases heads and root in a subset of node from root dict
241 241
242 242 * subset is heads of the subset
243 243 * roots is {<nodeid> => phase} mapping. key and value are string.
244 244
245 245 Accept unknown element input
246 246 """
247 247 # build list from dictionary
248 248 phaseroots = [[] for p in allphases]
249 249 for nhex, phase in roots.iteritems():
250 250 if nhex == 'publishing': # ignore data related to publish option
251 251 continue
252 252 node = bin(nhex)
253 253 phase = int(phase)
254 254 if node in repo:
255 255 phaseroots[phase].append(node)
256 256 # compute heads
257 257 phaseheads = [[] for p in allphases]
258 258 for phase in allphases[:-1]:
259 259 toproof = phaseroots[phase + 1]
260 260 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
261 261 subset, toproof, toproof, subset)
262 262 phaseheads[phase].extend(c.node() for c in revset)
263 263 return phaseheads, phaseroots
264 264
General Comments 0
You need to be logged in to leave comments. Login now