##// END OF EJS Templates
phases: write default phase data as soon as possible....
Pierre-Yves David -
r16025:6697498b stable
parent child Browse files
Show More
@@ -1,299 +1,300 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 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 are exchanged by pushkey on pull and push. Some server have a
45 45 publish option set, we call them publishing server. Pushing to such server make
46 46 draft changeset publish.
47 47
48 48 A small list of fact/rules define the exchange of phase:
49 49
50 50 * old client never changes server states
51 51 * pull never changes server states
52 52 * publish and old server csets are seen as public by client
53 53
54 54 * Any secret changeset seens in another repository is lowered to at least draft
55 55
56 56
57 57 Here is the final table summing up the 49 possible usecase of phase exchange:
58 58
59 59 server
60 60 old publish non-publish
61 61 N X N D P N D P
62 62 old client
63 63 pull
64 64 N - X/X - X/D X/P - X/D X/P
65 65 X - X/X - X/D X/P - X/D X/P
66 66 push
67 67 X X/X X/X X/P X/P X/P X/D X/D X/P
68 68 new client
69 69 pull
70 70 N - P/X - P/D P/P - D/D P/P
71 71 D - P/X - P/D P/P - D/D P/P
72 72 P - P/X - P/D P/P - P/D P/P
73 73 push
74 74 D P/X P/X P/P P/P P/P D/D D/D P/P
75 75 P P/X P/X P/P P/P P/P P/P P/P P/P
76 76
77 77 Legend:
78 78
79 79 A/B = final state on client / state on server
80 80
81 81 * N = new/not present,
82 82 * P = public,
83 83 * D = draft,
84 84 * X = not tracked (ie: the old client or server has no internal way of
85 85 recording the phase.)
86 86
87 87 passive = only pushes
88 88
89 89
90 90 A cell here can be read like this:
91 91
92 92 "When a new client pushes a draft changeset (D) to a publishing server
93 93 where it's not present (N), it's marked public on both sides (P/P)."
94 94
95 95 Note: old client behave as publish server with Draft only content
96 96 - other people see it as public
97 97 - content is pushed as draft
98 98
99 99 """
100 100
101 101 import errno
102 102 from node import nullid, bin, hex, short
103 103 from i18n import _
104 104
105 105 allphases = public, draft, secret = range(3)
106 106 trackedphases = allphases[1:]
107 107 phasenames = ['public', 'draft', 'secret']
108 108
109 109 def readroots(repo):
110 110 """Read phase roots from disk"""
111 111 roots = [set() for i in allphases]
112 112 try:
113 113 f = repo.sopener('phaseroots')
114 114 try:
115 115 for line in f:
116 116 phase, nh = line.strip().split()
117 117 roots[int(phase)].add(bin(nh))
118 118 finally:
119 119 f.close()
120 120 except IOError, inst:
121 121 if inst.errno != errno.ENOENT:
122 122 raise
123 123 for f in repo._phasedefaults:
124 124 roots = f(repo, roots)
125 repo._dirtyphases = True
125 126 return roots
126 127
127 128 def writeroots(repo):
128 129 """Write phase roots from disk"""
129 130 f = repo.sopener('phaseroots', 'w', atomictemp=True)
130 131 try:
131 132 for phase, roots in enumerate(repo._phaseroots):
132 133 for h in roots:
133 134 f.write('%i %s\n' % (phase, hex(h)))
134 135 repo._dirtyphases = False
135 136 finally:
136 137 f.close()
137 138
138 139 def filterunknown(repo, phaseroots=None):
139 140 """remove unknown nodes from the phase boundary
140 141
141 142 no data is lost as unknown node only old data for their descentants
142 143 """
143 144 if phaseroots is None:
144 145 phaseroots = repo._phaseroots
145 146 nodemap = repo.changelog.nodemap # to filter unknown nodes
146 147 for phase, nodes in enumerate(phaseroots):
147 148 missing = [node for node in nodes if node not in nodemap]
148 149 if missing:
149 150 for mnode in missing:
150 151 msg = 'Removing unknown node %(n)s from %(p)i-phase boundary'
151 152 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
152 153 nodes.symmetric_difference_update(missing)
153 154 repo._dirtyphases = True
154 155
155 156 def advanceboundary(repo, targetphase, nodes):
156 157 """Add nodes to a phase changing other nodes phases if necessary.
157 158
158 159 This function move boundary *forward* this means that all nodes are set
159 160 in the target phase or kept in a *lower* phase.
160 161
161 162 Simplify boundary to contains phase roots only."""
162 163 delroots = [] # set of root deleted by this path
163 164 for phase in xrange(targetphase + 1, len(allphases)):
164 165 # filter nodes that are not in a compatible phase already
165 166 # XXX rev phase cache might have been invalidated by a previous loop
166 167 # XXX we need to be smarter here
167 168 nodes = [n for n in nodes if repo[n].phase() >= phase]
168 169 if not nodes:
169 170 break # no roots to move anymore
170 171 roots = repo._phaseroots[phase]
171 172 olds = roots.copy()
172 173 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
173 174 roots.clear()
174 175 roots.update(ctx.node() for ctx in ctxs)
175 176 if olds != roots:
176 177 # invalidate cache (we probably could be smarter here
177 178 if '_phaserev' in vars(repo):
178 179 del repo._phaserev
179 180 repo._dirtyphases = True
180 181 # some roots may need to be declared for lower phases
181 182 delroots.extend(olds - roots)
182 183 # declare deleted root in the target phase
183 184 if targetphase != 0:
184 185 retractboundary(repo, targetphase, delroots)
185 186
186 187
187 188 def retractboundary(repo, targetphase, nodes):
188 189 """Set nodes back to a phase changing other nodes phases if necessary.
189 190
190 191 This function move boundary *backward* this means that all nodes are set
191 192 in the target phase or kept in a *higher* phase.
192 193
193 194 Simplify boundary to contains phase roots only."""
194 195 currentroots = repo._phaseroots[targetphase]
195 196 newroots = [n for n in nodes if repo[n].phase() < targetphase]
196 197 if newroots:
197 198 currentroots.update(newroots)
198 199 ctxs = repo.set('roots(%ln::)', currentroots)
199 200 currentroots.intersection_update(ctx.node() for ctx in ctxs)
200 201 if '_phaserev' in vars(repo):
201 202 del repo._phaserev
202 203 repo._dirtyphases = True
203 204
204 205
205 206 def listphases(repo):
206 207 """List phases root for serialisation over pushkey"""
207 208 keys = {}
208 209 value = '%i' % draft
209 210 for root in repo._phaseroots[draft]:
210 211 keys[hex(root)] = value
211 212
212 213 if repo.ui.configbool('phases', 'publish', True):
213 214 # Add an extra data to let remote know we are a publishing repo.
214 215 # Publishing repo can't just pretend they are old repo. When pushing to
215 216 # a publishing repo, the client still need to push phase boundary
216 217 #
217 218 # Push do not only push changeset. It also push phase data. New
218 219 # phase data may apply to common changeset which won't be push (as they
219 220 # are common). Here is a very simple example:
220 221 #
221 222 # 1) repo A push changeset X as draft to repo B
222 223 # 2) repo B make changeset X public
223 224 # 3) repo B push to repo A. X is not pushed but the data that X as now
224 225 # public should
225 226 #
226 227 # The server can't handle it on it's own as it has no idea of client
227 228 # phase data.
228 229 keys['publishing'] = 'True'
229 230 return keys
230 231
231 232 def pushphase(repo, nhex, oldphasestr, newphasestr):
232 233 """List phases root for serialisation over pushkey"""
233 234 lock = repo.lock()
234 235 try:
235 236 currentphase = repo[nhex].phase()
236 237 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
237 238 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
238 239 if currentphase == oldphase and newphase < oldphase:
239 240 advanceboundary(repo, newphase, [bin(nhex)])
240 241 return 1
241 242 else:
242 243 return 0
243 244 finally:
244 245 lock.release()
245 246
246 247 def visibleheads(repo):
247 248 """return the set of visible head of this repo"""
248 249 # XXX we want a cache on this
249 250 sroots = repo._phaseroots[secret]
250 251 if sroots:
251 252 # XXX very slow revset. storing heads or secret "boundary" would help.
252 253 revset = repo.set('heads(not (%ln::))', sroots)
253 254
254 255 vheads = [ctx.node() for ctx in revset]
255 256 if not vheads:
256 257 vheads.append(nullid)
257 258 else:
258 259 vheads = repo.heads()
259 260 return vheads
260 261
261 262 def analyzeremotephases(repo, subset, roots):
262 263 """Compute phases heads and root in a subset of node from root dict
263 264
264 265 * subset is heads of the subset
265 266 * roots is {<nodeid> => phase} mapping. key and value are string.
266 267
267 268 Accept unknown element input
268 269 """
269 270 # build list from dictionary
270 271 draftroots = []
271 272 nodemap = repo.changelog.nodemap # to filter unknown nodes
272 273 for nhex, phase in roots.iteritems():
273 274 if nhex == 'publishing': # ignore data related to publish option
274 275 continue
275 276 node = bin(nhex)
276 277 phase = int(phase)
277 278 if phase == 0:
278 279 if node != nullid:
279 280 repo.ui.warn(_('ignoring inconsistent public root'
280 281 ' from remote: %s\n') % nhex)
281 282 elif phase == 1:
282 283 if node in nodemap:
283 284 draftroots.append(node)
284 285 else:
285 286 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
286 287 % (phase, nhex))
287 288 # compute heads
288 289 publicheads = newheads(repo, subset, draftroots)
289 290 return publicheads, draftroots
290 291
291 292 def newheads(repo, heads, roots):
292 293 """compute new head of a subset minus another
293 294
294 295 * `heads`: define the first subset
295 296 * `rroots`: define the second we substract to the first"""
296 297 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
297 298 heads, roots, roots, heads)
298 299 return [c.node() for c in revset]
299 300
@@ -1,148 +1,149 b''
1 1 test that new files created in .hg inherit the permissions from .hg/store
2 2
3 3
4 4 $ "$TESTDIR/hghave" unix-permissions || exit 80
5 5
6 6 $ mkdir dir
7 7
8 8 just in case somebody has a strange $TMPDIR
9 9
10 10 $ chmod g-s dir
11 11 $ cd dir
12 12
13 13 $ cat >printmodes.py <<EOF
14 14 > import os, sys
15 15 >
16 16 > allnames = []
17 17 > isdir = {}
18 18 > for root, dirs, files in os.walk(sys.argv[1]):
19 19 > for d in dirs:
20 20 > name = os.path.join(root, d)
21 21 > isdir[name] = 1
22 22 > allnames.append(name)
23 23 > for f in files:
24 24 > name = os.path.join(root, f)
25 25 > allnames.append(name)
26 26 > allnames.sort()
27 27 > for name in allnames:
28 28 > suffix = name in isdir and '/' or ''
29 29 > print '%05o %s%s' % (os.lstat(name).st_mode & 07777, name, suffix)
30 30 > EOF
31 31
32 32 $ cat >mode.py <<EOF
33 33 > import sys
34 34 > import os
35 35 > print '%05o' % os.lstat(sys.argv[1]).st_mode
36 36 > EOF
37 37
38 38 $ umask 077
39 39
40 40 $ hg init repo
41 41 $ cd repo
42 42
43 43 $ chmod 0770 .hg/store
44 44
45 45 before commit
46 46 store can be written by the group, other files cannot
47 47 store is setgid
48 48
49 49 $ python ../printmodes.py .
50 50 00700 ./.hg/
51 51 00600 ./.hg/00changelog.i
52 52 00600 ./.hg/requires
53 53 00770 ./.hg/store/
54 54
55 55 $ mkdir dir
56 56 $ touch foo dir/bar
57 57 $ hg ci -qAm 'add files'
58 58
59 59 after commit
60 60 working dir files can only be written by the owner
61 61 files created in .hg can be written by the group
62 62 (in particular, store/**, dirstate, branch cache file, undo files)
63 63 new directories are setgid
64 64
65 65 $ python ../printmodes.py .
66 66 00700 ./.hg/
67 67 00600 ./.hg/00changelog.i
68 68 00770 ./.hg/cache/
69 69 00660 ./.hg/cache/branchheads
70 70 00660 ./.hg/dirstate
71 71 00660 ./.hg/last-message.txt
72 72 00600 ./.hg/requires
73 73 00770 ./.hg/store/
74 74 00660 ./.hg/store/00changelog.i
75 75 00660 ./.hg/store/00manifest.i
76 76 00770 ./.hg/store/data/
77 77 00770 ./.hg/store/data/dir/
78 78 00660 ./.hg/store/data/dir/bar.i
79 79 00660 ./.hg/store/data/foo.i
80 80 00660 ./.hg/store/fncache
81 81 00660 ./.hg/store/phaseroots
82 82 00660 ./.hg/store/undo
83 83 00660 ./.hg/store/undo.phaseroots
84 84 00660 ./.hg/undo.bookmarks
85 85 00660 ./.hg/undo.branch
86 86 00660 ./.hg/undo.desc
87 87 00660 ./.hg/undo.dirstate
88 88 00700 ./dir/
89 89 00600 ./dir/bar
90 90 00600 ./foo
91 91
92 92 $ umask 007
93 93 $ hg init ../push
94 94
95 95 before push
96 96 group can write everything
97 97
98 98 $ python ../printmodes.py ../push
99 99 00770 ../push/.hg/
100 100 00660 ../push/.hg/00changelog.i
101 101 00660 ../push/.hg/requires
102 102 00770 ../push/.hg/store/
103 103
104 104 $ umask 077
105 105 $ hg -q push ../push
106 106
107 107 after push
108 108 group can still write everything
109 109
110 110 $ python ../printmodes.py ../push
111 111 00770 ../push/.hg/
112 112 00660 ../push/.hg/00changelog.i
113 113 00770 ../push/.hg/cache/
114 114 00660 ../push/.hg/cache/branchheads
115 115 00660 ../push/.hg/requires
116 116 00770 ../push/.hg/store/
117 117 00660 ../push/.hg/store/00changelog.i
118 118 00660 ../push/.hg/store/00manifest.i
119 119 00770 ../push/.hg/store/data/
120 120 00770 ../push/.hg/store/data/dir/
121 121 00660 ../push/.hg/store/data/dir/bar.i
122 122 00660 ../push/.hg/store/data/foo.i
123 123 00660 ../push/.hg/store/fncache
124 00660 ../push/.hg/store/phaseroots
124 125 00660 ../push/.hg/store/undo
125 126 00660 ../push/.hg/store/undo.phaseroots
126 127 00660 ../push/.hg/undo.bookmarks
127 128 00660 ../push/.hg/undo.branch
128 129 00660 ../push/.hg/undo.desc
129 130 00660 ../push/.hg/undo.dirstate
130 131
131 132
132 133 Test that we don't lose the setgid bit when we call chmod.
133 134 Not all systems support setgid directories (e.g. HFS+), so
134 135 just check that directories have the same mode.
135 136
136 137 $ cd ..
137 138 $ hg init setgid
138 139 $ cd setgid
139 140 $ chmod g+rwx .hg/store
140 141 $ chmod g+s .hg/store 2> /dev/null
141 142 $ mkdir dir
142 143 $ touch dir/file
143 144 $ hg ci -qAm 'add dir/file'
144 145 $ storemode=`python ../mode.py .hg/store`
145 146 $ dirmode=`python ../mode.py .hg/store/data/dir`
146 147 $ if [ "$storemode" != "$dirmode" ]; then
147 148 > echo "$storemode != $dirmode"
148 149 $ fi
General Comments 0
You need to be logged in to leave comments. Login now