##// END OF EJS Templates
bundlerepo: implement safe phasecache...
Eric Sumner -
r23631:b8260abf default
parent child Browse files
Show More
@@ -1,403 +1,424
1 # bundlerepo.py - repository class for viewing uncompressed bundles
1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 #
2 #
3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Repository class for viewing uncompressed bundles.
8 """Repository class for viewing uncompressed bundles.
9
9
10 This provides a read-only repository interface to bundles as if they
10 This provides a read-only repository interface to bundles as if they
11 were part of the actual repository.
11 were part of the actual repository.
12 """
12 """
13
13
14 from node import nullid
14 from node import nullid
15 from i18n import _
15 from i18n import _
16 import os, tempfile, shutil
16 import os, tempfile, shutil
17 import changegroup, util, mdiff, discovery, cmdutil, scmutil, exchange
17 import changegroup, util, mdiff, discovery, cmdutil, scmutil, exchange
18 import localrepo, changelog, manifest, filelog, revlog, error
18 import localrepo, changelog, manifest, filelog, revlog, error, phases
19
19
20 class bundlerevlog(revlog.revlog):
20 class bundlerevlog(revlog.revlog):
21 def __init__(self, opener, indexfile, bundle, linkmapper):
21 def __init__(self, opener, indexfile, bundle, linkmapper):
22 # How it works:
22 # How it works:
23 # To retrieve a revision, we need to know the offset of the revision in
23 # To retrieve a revision, we need to know the offset of the revision in
24 # the bundle (an unbundle object). We store this offset in the index
24 # the bundle (an unbundle object). We store this offset in the index
25 # (start). The base of the delta is stored in the base field.
25 # (start). The base of the delta is stored in the base field.
26 #
26 #
27 # To differentiate a rev in the bundle from a rev in the revlog, we
27 # To differentiate a rev in the bundle from a rev in the revlog, we
28 # check revision against repotiprev.
28 # check revision against repotiprev.
29 opener = scmutil.readonlyvfs(opener)
29 opener = scmutil.readonlyvfs(opener)
30 revlog.revlog.__init__(self, opener, indexfile)
30 revlog.revlog.__init__(self, opener, indexfile)
31 self.bundle = bundle
31 self.bundle = bundle
32 n = len(self)
32 n = len(self)
33 self.repotiprev = n - 1
33 self.repotiprev = n - 1
34 chain = None
34 chain = None
35 self.bundlerevs = set() # used by 'bundle()' revset expression
35 self.bundlerevs = set() # used by 'bundle()' revset expression
36 while True:
36 while True:
37 chunkdata = bundle.deltachunk(chain)
37 chunkdata = bundle.deltachunk(chain)
38 if not chunkdata:
38 if not chunkdata:
39 break
39 break
40 node = chunkdata['node']
40 node = chunkdata['node']
41 p1 = chunkdata['p1']
41 p1 = chunkdata['p1']
42 p2 = chunkdata['p2']
42 p2 = chunkdata['p2']
43 cs = chunkdata['cs']
43 cs = chunkdata['cs']
44 deltabase = chunkdata['deltabase']
44 deltabase = chunkdata['deltabase']
45 delta = chunkdata['delta']
45 delta = chunkdata['delta']
46
46
47 size = len(delta)
47 size = len(delta)
48 start = bundle.tell() - size
48 start = bundle.tell() - size
49
49
50 link = linkmapper(cs)
50 link = linkmapper(cs)
51 if node in self.nodemap:
51 if node in self.nodemap:
52 # this can happen if two branches make the same change
52 # this can happen if two branches make the same change
53 chain = node
53 chain = node
54 self.bundlerevs.add(self.nodemap[node])
54 self.bundlerevs.add(self.nodemap[node])
55 continue
55 continue
56
56
57 for p in (p1, p2):
57 for p in (p1, p2):
58 if p not in self.nodemap:
58 if p not in self.nodemap:
59 raise error.LookupError(p, self.indexfile,
59 raise error.LookupError(p, self.indexfile,
60 _("unknown parent"))
60 _("unknown parent"))
61
61
62 if deltabase not in self.nodemap:
62 if deltabase not in self.nodemap:
63 raise LookupError(deltabase, self.indexfile,
63 raise LookupError(deltabase, self.indexfile,
64 _('unknown delta base'))
64 _('unknown delta base'))
65
65
66 baserev = self.rev(deltabase)
66 baserev = self.rev(deltabase)
67 # start, size, full unc. size, base (unused), link, p1, p2, node
67 # start, size, full unc. size, base (unused), link, p1, p2, node
68 e = (revlog.offset_type(start, 0), size, -1, baserev, link,
68 e = (revlog.offset_type(start, 0), size, -1, baserev, link,
69 self.rev(p1), self.rev(p2), node)
69 self.rev(p1), self.rev(p2), node)
70 self.index.insert(-1, e)
70 self.index.insert(-1, e)
71 self.nodemap[node] = n
71 self.nodemap[node] = n
72 self.bundlerevs.add(n)
72 self.bundlerevs.add(n)
73 chain = node
73 chain = node
74 n += 1
74 n += 1
75
75
76 def _chunk(self, rev):
76 def _chunk(self, rev):
77 # Warning: in case of bundle, the diff is against what we stored as
77 # Warning: in case of bundle, the diff is against what we stored as
78 # delta base, not against rev - 1
78 # delta base, not against rev - 1
79 # XXX: could use some caching
79 # XXX: could use some caching
80 if rev <= self.repotiprev:
80 if rev <= self.repotiprev:
81 return revlog.revlog._chunk(self, rev)
81 return revlog.revlog._chunk(self, rev)
82 self.bundle.seek(self.start(rev))
82 self.bundle.seek(self.start(rev))
83 return self.bundle.read(self.length(rev))
83 return self.bundle.read(self.length(rev))
84
84
85 def revdiff(self, rev1, rev2):
85 def revdiff(self, rev1, rev2):
86 """return or calculate a delta between two revisions"""
86 """return or calculate a delta between two revisions"""
87 if rev1 > self.repotiprev and rev2 > self.repotiprev:
87 if rev1 > self.repotiprev and rev2 > self.repotiprev:
88 # hot path for bundle
88 # hot path for bundle
89 revb = self.index[rev2][3]
89 revb = self.index[rev2][3]
90 if revb == rev1:
90 if revb == rev1:
91 return self._chunk(rev2)
91 return self._chunk(rev2)
92 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
92 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
93 return revlog.revlog.revdiff(self, rev1, rev2)
93 return revlog.revlog.revdiff(self, rev1, rev2)
94
94
95 return mdiff.textdiff(self.revision(self.node(rev1)),
95 return mdiff.textdiff(self.revision(self.node(rev1)),
96 self.revision(self.node(rev2)))
96 self.revision(self.node(rev2)))
97
97
98 def revision(self, nodeorrev):
98 def revision(self, nodeorrev):
99 """return an uncompressed revision of a given node or revision
99 """return an uncompressed revision of a given node or revision
100 number.
100 number.
101 """
101 """
102 if isinstance(nodeorrev, int):
102 if isinstance(nodeorrev, int):
103 rev = nodeorrev
103 rev = nodeorrev
104 node = self.node(rev)
104 node = self.node(rev)
105 else:
105 else:
106 node = nodeorrev
106 node = nodeorrev
107 rev = self.rev(node)
107 rev = self.rev(node)
108
108
109 if node == nullid:
109 if node == nullid:
110 return ""
110 return ""
111
111
112 text = None
112 text = None
113 chain = []
113 chain = []
114 iterrev = rev
114 iterrev = rev
115 # reconstruct the revision if it is from a changegroup
115 # reconstruct the revision if it is from a changegroup
116 while iterrev > self.repotiprev:
116 while iterrev > self.repotiprev:
117 if self._cache and self._cache[1] == iterrev:
117 if self._cache and self._cache[1] == iterrev:
118 text = self._cache[2]
118 text = self._cache[2]
119 break
119 break
120 chain.append(iterrev)
120 chain.append(iterrev)
121 iterrev = self.index[iterrev][3]
121 iterrev = self.index[iterrev][3]
122 if text is None:
122 if text is None:
123 text = self.baserevision(iterrev)
123 text = self.baserevision(iterrev)
124
124
125 while chain:
125 while chain:
126 delta = self._chunk(chain.pop())
126 delta = self._chunk(chain.pop())
127 text = mdiff.patches(text, [delta])
127 text = mdiff.patches(text, [delta])
128
128
129 self._checkhash(text, node, rev)
129 self._checkhash(text, node, rev)
130 self._cache = (node, rev, text)
130 self._cache = (node, rev, text)
131 return text
131 return text
132
132
133 def baserevision(self, nodeorrev):
133 def baserevision(self, nodeorrev):
134 # Revlog subclasses may override 'revision' method to modify format of
134 # Revlog subclasses may override 'revision' method to modify format of
135 # content retrieved from revlog. To use bundlerevlog with such class one
135 # content retrieved from revlog. To use bundlerevlog with such class one
136 # needs to override 'baserevision' and make more specific call here.
136 # needs to override 'baserevision' and make more specific call here.
137 return revlog.revlog.revision(self, nodeorrev)
137 return revlog.revlog.revision(self, nodeorrev)
138
138
139 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
139 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
140 raise NotImplementedError
140 raise NotImplementedError
141 def addgroup(self, revs, linkmapper, transaction):
141 def addgroup(self, revs, linkmapper, transaction):
142 raise NotImplementedError
142 raise NotImplementedError
143 def strip(self, rev, minlink):
143 def strip(self, rev, minlink):
144 raise NotImplementedError
144 raise NotImplementedError
145 def checksize(self):
145 def checksize(self):
146 raise NotImplementedError
146 raise NotImplementedError
147
147
148 class bundlechangelog(bundlerevlog, changelog.changelog):
148 class bundlechangelog(bundlerevlog, changelog.changelog):
149 def __init__(self, opener, bundle):
149 def __init__(self, opener, bundle):
150 changelog.changelog.__init__(self, opener)
150 changelog.changelog.__init__(self, opener)
151 linkmapper = lambda x: x
151 linkmapper = lambda x: x
152 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
152 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
153 linkmapper)
153 linkmapper)
154
154
155 def baserevision(self, nodeorrev):
155 def baserevision(self, nodeorrev):
156 # Although changelog doesn't override 'revision' method, some extensions
156 # Although changelog doesn't override 'revision' method, some extensions
157 # may replace this class with another that does. Same story with
157 # may replace this class with another that does. Same story with
158 # manifest and filelog classes.
158 # manifest and filelog classes.
159 return changelog.changelog.revision(self, nodeorrev)
159 return changelog.changelog.revision(self, nodeorrev)
160
160
161 class bundlemanifest(bundlerevlog, manifest.manifest):
161 class bundlemanifest(bundlerevlog, manifest.manifest):
162 def __init__(self, opener, bundle, linkmapper):
162 def __init__(self, opener, bundle, linkmapper):
163 manifest.manifest.__init__(self, opener)
163 manifest.manifest.__init__(self, opener)
164 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
164 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
165 linkmapper)
165 linkmapper)
166
166
167 def baserevision(self, nodeorrev):
167 def baserevision(self, nodeorrev):
168 return manifest.manifest.revision(self, nodeorrev)
168 return manifest.manifest.revision(self, nodeorrev)
169
169
170 class bundlefilelog(bundlerevlog, filelog.filelog):
170 class bundlefilelog(bundlerevlog, filelog.filelog):
171 def __init__(self, opener, path, bundle, linkmapper, repo):
171 def __init__(self, opener, path, bundle, linkmapper, repo):
172 filelog.filelog.__init__(self, opener, path)
172 filelog.filelog.__init__(self, opener, path)
173 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
173 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
174 linkmapper)
174 linkmapper)
175 self._repo = repo
175 self._repo = repo
176
176
177 def baserevision(self, nodeorrev):
177 def baserevision(self, nodeorrev):
178 return filelog.filelog.revision(self, nodeorrev)
178 return filelog.filelog.revision(self, nodeorrev)
179
179
180 def _file(self, f):
180 def _file(self, f):
181 self._repo.file(f)
181 self._repo.file(f)
182
182
183 class bundlepeer(localrepo.localpeer):
183 class bundlepeer(localrepo.localpeer):
184 def canpush(self):
184 def canpush(self):
185 return False
185 return False
186
186
187 class bundlephasecache(phases.phasecache):
188 def __init__(self, *args, **kwargs):
189 super(bundlephasecache, self).__init__(*args, **kwargs)
190 if util.safehasattr(self, 'opener'):
191 self.opener = scmutil.readonlyvfs(self.opener)
192
193 def write(self):
194 raise NotImplementedError
195
196 def _write(self, fp):
197 raise NotImplementedError
198
199 def _updateroots(self, phase, newroots, tr):
200 self.phaseroots[phase] = newroots
201 self.invalidate()
202 self.dirty = True
203
187 class bundlerepository(localrepo.localrepository):
204 class bundlerepository(localrepo.localrepository):
188 def __init__(self, ui, path, bundlename):
205 def __init__(self, ui, path, bundlename):
189 self._tempparent = None
206 self._tempparent = None
190 try:
207 try:
191 localrepo.localrepository.__init__(self, ui, path)
208 localrepo.localrepository.__init__(self, ui, path)
192 except error.RepoError:
209 except error.RepoError:
193 self._tempparent = tempfile.mkdtemp()
210 self._tempparent = tempfile.mkdtemp()
194 localrepo.instance(ui, self._tempparent, 1)
211 localrepo.instance(ui, self._tempparent, 1)
195 localrepo.localrepository.__init__(self, ui, self._tempparent)
212 localrepo.localrepository.__init__(self, ui, self._tempparent)
196 self.ui.setconfig('phases', 'publish', False, 'bundlerepo')
213 self.ui.setconfig('phases', 'publish', False, 'bundlerepo')
197
214
198 if path:
215 if path:
199 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
216 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
200 else:
217 else:
201 self._url = 'bundle:' + bundlename
218 self._url = 'bundle:' + bundlename
202
219
203 self.tempfile = None
220 self.tempfile = None
204 f = util.posixfile(bundlename, "rb")
221 f = util.posixfile(bundlename, "rb")
205 self.bundle = exchange.readbundle(ui, f, bundlename)
222 self.bundle = exchange.readbundle(ui, f, bundlename)
206 if self.bundle.compressed():
223 if self.bundle.compressed():
207 fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
224 fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
208 suffix=".hg10un")
225 suffix=".hg10un")
209 self.tempfile = temp
226 self.tempfile = temp
210 fptemp = os.fdopen(fdtemp, 'wb')
227 fptemp = os.fdopen(fdtemp, 'wb')
211
228
212 try:
229 try:
213 fptemp.write("HG10UN")
230 fptemp.write("HG10UN")
214 while True:
231 while True:
215 chunk = self.bundle.read(2**18)
232 chunk = self.bundle.read(2**18)
216 if not chunk:
233 if not chunk:
217 break
234 break
218 fptemp.write(chunk)
235 fptemp.write(chunk)
219 finally:
236 finally:
220 fptemp.close()
237 fptemp.close()
221
238
222 f = self.vfs.open(self.tempfile, mode="rb")
239 f = self.vfs.open(self.tempfile, mode="rb")
223 self.bundle = exchange.readbundle(ui, f, bundlename, self.vfs)
240 self.bundle = exchange.readbundle(ui, f, bundlename, self.vfs)
224
241
225 # dict with the mapping 'filename' -> position in the bundle
242 # dict with the mapping 'filename' -> position in the bundle
226 self.bundlefilespos = {}
243 self.bundlefilespos = {}
227
244
228 @localrepo.unfilteredpropertycache
245 @localrepo.unfilteredpropertycache
246 def _phasecache(self):
247 return bundlephasecache(self, self._phasedefaults)
248
249 @localrepo.unfilteredpropertycache
229 def changelog(self):
250 def changelog(self):
230 # consume the header if it exists
251 # consume the header if it exists
231 self.bundle.changelogheader()
252 self.bundle.changelogheader()
232 c = bundlechangelog(self.sopener, self.bundle)
253 c = bundlechangelog(self.sopener, self.bundle)
233 self.manstart = self.bundle.tell()
254 self.manstart = self.bundle.tell()
234 return c
255 return c
235
256
236 @localrepo.unfilteredpropertycache
257 @localrepo.unfilteredpropertycache
237 def manifest(self):
258 def manifest(self):
238 self.bundle.seek(self.manstart)
259 self.bundle.seek(self.manstart)
239 # consume the header if it exists
260 # consume the header if it exists
240 self.bundle.manifestheader()
261 self.bundle.manifestheader()
241 m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev)
262 m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev)
242 self.filestart = self.bundle.tell()
263 self.filestart = self.bundle.tell()
243 return m
264 return m
244
265
245 @localrepo.unfilteredpropertycache
266 @localrepo.unfilteredpropertycache
246 def manstart(self):
267 def manstart(self):
247 self.changelog
268 self.changelog
248 return self.manstart
269 return self.manstart
249
270
250 @localrepo.unfilteredpropertycache
271 @localrepo.unfilteredpropertycache
251 def filestart(self):
272 def filestart(self):
252 self.manifest
273 self.manifest
253 return self.filestart
274 return self.filestart
254
275
255 def url(self):
276 def url(self):
256 return self._url
277 return self._url
257
278
258 def file(self, f):
279 def file(self, f):
259 if not self.bundlefilespos:
280 if not self.bundlefilespos:
260 self.bundle.seek(self.filestart)
281 self.bundle.seek(self.filestart)
261 while True:
282 while True:
262 chunkdata = self.bundle.filelogheader()
283 chunkdata = self.bundle.filelogheader()
263 if not chunkdata:
284 if not chunkdata:
264 break
285 break
265 fname = chunkdata['filename']
286 fname = chunkdata['filename']
266 self.bundlefilespos[fname] = self.bundle.tell()
287 self.bundlefilespos[fname] = self.bundle.tell()
267 while True:
288 while True:
268 c = self.bundle.deltachunk(None)
289 c = self.bundle.deltachunk(None)
269 if not c:
290 if not c:
270 break
291 break
271
292
272 if f in self.bundlefilespos:
293 if f in self.bundlefilespos:
273 self.bundle.seek(self.bundlefilespos[f])
294 self.bundle.seek(self.bundlefilespos[f])
274 return bundlefilelog(self.sopener, f, self.bundle,
295 return bundlefilelog(self.sopener, f, self.bundle,
275 self.changelog.rev, self)
296 self.changelog.rev, self)
276 else:
297 else:
277 return filelog.filelog(self.sopener, f)
298 return filelog.filelog(self.sopener, f)
278
299
279 def close(self):
300 def close(self):
280 """Close assigned bundle file immediately."""
301 """Close assigned bundle file immediately."""
281 self.bundle.close()
302 self.bundle.close()
282 if self.tempfile is not None:
303 if self.tempfile is not None:
283 self.vfs.unlink(self.tempfile)
304 self.vfs.unlink(self.tempfile)
284 if self._tempparent:
305 if self._tempparent:
285 shutil.rmtree(self._tempparent, True)
306 shutil.rmtree(self._tempparent, True)
286
307
287 def cancopy(self):
308 def cancopy(self):
288 return False
309 return False
289
310
290 def peer(self):
311 def peer(self):
291 return bundlepeer(self)
312 return bundlepeer(self)
292
313
293 def getcwd(self):
314 def getcwd(self):
294 return os.getcwd() # always outside the repo
315 return os.getcwd() # always outside the repo
295
316
296
317
297 def instance(ui, path, create):
318 def instance(ui, path, create):
298 if create:
319 if create:
299 raise util.Abort(_('cannot create new bundle repository'))
320 raise util.Abort(_('cannot create new bundle repository'))
300 parentpath = ui.config("bundle", "mainreporoot", "")
321 parentpath = ui.config("bundle", "mainreporoot", "")
301 if not parentpath:
322 if not parentpath:
302 # try to find the correct path to the working directory repo
323 # try to find the correct path to the working directory repo
303 parentpath = cmdutil.findrepo(os.getcwd())
324 parentpath = cmdutil.findrepo(os.getcwd())
304 if parentpath is None:
325 if parentpath is None:
305 parentpath = ''
326 parentpath = ''
306 if parentpath:
327 if parentpath:
307 # Try to make the full path relative so we get a nice, short URL.
328 # Try to make the full path relative so we get a nice, short URL.
308 # In particular, we don't want temp dir names in test outputs.
329 # In particular, we don't want temp dir names in test outputs.
309 cwd = os.getcwd()
330 cwd = os.getcwd()
310 if parentpath == cwd:
331 if parentpath == cwd:
311 parentpath = ''
332 parentpath = ''
312 else:
333 else:
313 cwd = os.path.join(cwd,'')
334 cwd = os.path.join(cwd,'')
314 if parentpath.startswith(cwd):
335 if parentpath.startswith(cwd):
315 parentpath = parentpath[len(cwd):]
336 parentpath = parentpath[len(cwd):]
316 u = util.url(path)
337 u = util.url(path)
317 path = u.localpath()
338 path = u.localpath()
318 if u.scheme == 'bundle':
339 if u.scheme == 'bundle':
319 s = path.split("+", 1)
340 s = path.split("+", 1)
320 if len(s) == 1:
341 if len(s) == 1:
321 repopath, bundlename = parentpath, s[0]
342 repopath, bundlename = parentpath, s[0]
322 else:
343 else:
323 repopath, bundlename = s
344 repopath, bundlename = s
324 else:
345 else:
325 repopath, bundlename = parentpath, path
346 repopath, bundlename = parentpath, path
326 return bundlerepository(ui, repopath, bundlename)
347 return bundlerepository(ui, repopath, bundlename)
327
348
328 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
349 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
329 force=False):
350 force=False):
330 '''obtains a bundle of changes incoming from other
351 '''obtains a bundle of changes incoming from other
331
352
332 "onlyheads" restricts the returned changes to those reachable from the
353 "onlyheads" restricts the returned changes to those reachable from the
333 specified heads.
354 specified heads.
334 "bundlename", if given, stores the bundle to this file path permanently;
355 "bundlename", if given, stores the bundle to this file path permanently;
335 otherwise it's stored to a temp file and gets deleted again when you call
356 otherwise it's stored to a temp file and gets deleted again when you call
336 the returned "cleanupfn".
357 the returned "cleanupfn".
337 "force" indicates whether to proceed on unrelated repos.
358 "force" indicates whether to proceed on unrelated repos.
338
359
339 Returns a tuple (local, csets, cleanupfn):
360 Returns a tuple (local, csets, cleanupfn):
340
361
341 "local" is a local repo from which to obtain the actual incoming
362 "local" is a local repo from which to obtain the actual incoming
342 changesets; it is a bundlerepo for the obtained bundle when the
363 changesets; it is a bundlerepo for the obtained bundle when the
343 original "other" is remote.
364 original "other" is remote.
344 "csets" lists the incoming changeset node ids.
365 "csets" lists the incoming changeset node ids.
345 "cleanupfn" must be called without arguments when you're done processing
366 "cleanupfn" must be called without arguments when you're done processing
346 the changes; it closes both the original "other" and the one returned
367 the changes; it closes both the original "other" and the one returned
347 here.
368 here.
348 '''
369 '''
349 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
370 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
350 force=force)
371 force=force)
351 common, incoming, rheads = tmp
372 common, incoming, rheads = tmp
352 if not incoming:
373 if not incoming:
353 try:
374 try:
354 if bundlename:
375 if bundlename:
355 os.unlink(bundlename)
376 os.unlink(bundlename)
356 except OSError:
377 except OSError:
357 pass
378 pass
358 return repo, [], other.close
379 return repo, [], other.close
359
380
360 commonset = set(common)
381 commonset = set(common)
361 rheads = [x for x in rheads if x not in commonset]
382 rheads = [x for x in rheads if x not in commonset]
362
383
363 bundle = None
384 bundle = None
364 bundlerepo = None
385 bundlerepo = None
365 localrepo = other.local()
386 localrepo = other.local()
366 if bundlename or not localrepo:
387 if bundlename or not localrepo:
367 # create a bundle (uncompressed if other repo is not local)
388 # create a bundle (uncompressed if other repo is not local)
368
389
369 if other.capable('getbundle'):
390 if other.capable('getbundle'):
370 cg = other.getbundle('incoming', common=common, heads=rheads)
391 cg = other.getbundle('incoming', common=common, heads=rheads)
371 elif onlyheads is None and not other.capable('changegroupsubset'):
392 elif onlyheads is None and not other.capable('changegroupsubset'):
372 # compat with older servers when pulling all remote heads
393 # compat with older servers when pulling all remote heads
373 cg = other.changegroup(incoming, "incoming")
394 cg = other.changegroup(incoming, "incoming")
374 rheads = None
395 rheads = None
375 else:
396 else:
376 cg = other.changegroupsubset(incoming, rheads, 'incoming')
397 cg = other.changegroupsubset(incoming, rheads, 'incoming')
377 bundletype = localrepo and "HG10BZ" or "HG10UN"
398 bundletype = localrepo and "HG10BZ" or "HG10UN"
378 fname = bundle = changegroup.writebundle(cg, bundlename, bundletype)
399 fname = bundle = changegroup.writebundle(cg, bundlename, bundletype)
379 # keep written bundle?
400 # keep written bundle?
380 if bundlename:
401 if bundlename:
381 bundle = None
402 bundle = None
382 if not localrepo:
403 if not localrepo:
383 # use the created uncompressed bundlerepo
404 # use the created uncompressed bundlerepo
384 localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
405 localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
385 fname)
406 fname)
386 # this repo contains local and other now, so filter out local again
407 # this repo contains local and other now, so filter out local again
387 common = repo.heads()
408 common = repo.heads()
388 if localrepo:
409 if localrepo:
389 # Part of common may be remotely filtered
410 # Part of common may be remotely filtered
390 # So use an unfiltered version
411 # So use an unfiltered version
391 # The discovery process probably need cleanup to avoid that
412 # The discovery process probably need cleanup to avoid that
392 localrepo = localrepo.unfiltered()
413 localrepo = localrepo.unfiltered()
393
414
394 csets = localrepo.changelog.findmissing(common, rheads)
415 csets = localrepo.changelog.findmissing(common, rheads)
395
416
396 def cleanup():
417 def cleanup():
397 if bundlerepo:
418 if bundlerepo:
398 bundlerepo.close()
419 bundlerepo.close()
399 if bundle:
420 if bundle:
400 os.unlink(bundle)
421 os.unlink(bundle)
401 other.close()
422 other.close()
402
423
403 return (localrepo, csets, cleanup)
424 return (localrepo, csets, cleanup)
@@ -1,445 +1,445
1 """ Mercurial phases support code
1 """ Mercurial phases support code
2
2
3 ---
3 ---
4
4
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 Logilab SA <contact@logilab.fr>
6 Logilab SA <contact@logilab.fr>
7 Augie Fackler <durin42@gmail.com>
7 Augie Fackler <durin42@gmail.com>
8
8
9 This software may be used and distributed according to the terms
9 This software may be used and distributed according to the terms
10 of the GNU General Public License version 2 or any later version.
10 of the GNU General Public License version 2 or any later version.
11
11
12 ---
12 ---
13
13
14 This module implements most phase logic in mercurial.
14 This module implements most phase logic in mercurial.
15
15
16
16
17 Basic Concept
17 Basic Concept
18 =============
18 =============
19
19
20 A 'changeset phase' is an indicator that tells us how a changeset is
20 A 'changeset phase' is an indicator that tells us how a changeset is
21 manipulated and communicated. The details of each phase is described
21 manipulated and communicated. The details of each phase is described
22 below, here we describe the properties they have in common.
22 below, here we describe the properties they have in common.
23
23
24 Like bookmarks, phases are not stored in history and thus are not
24 Like bookmarks, phases are not stored in history and thus are not
25 permanent and leave no audit trail.
25 permanent and leave no audit trail.
26
26
27 First, no changeset can be in two phases at once. Phases are ordered,
27 First, no changeset can be in two phases at once. Phases are ordered,
28 so they can be considered from lowest to highest. The default, lowest
28 so they can be considered from lowest to highest. The default, lowest
29 phase is 'public' - this is the normal phase of existing changesets. A
29 phase is 'public' - this is the normal phase of existing changesets. A
30 child changeset can not be in a lower phase than its parents.
30 child changeset can not be in a lower phase than its parents.
31
31
32 These phases share a hierarchy of traits:
32 These phases share a hierarchy of traits:
33
33
34 immutable shared
34 immutable shared
35 public: X X
35 public: X X
36 draft: X
36 draft: X
37 secret:
37 secret:
38
38
39 Local commits are draft by default.
39 Local commits are draft by default.
40
40
41 Phase Movement and Exchange
41 Phase Movement and Exchange
42 ===========================
42 ===========================
43
43
44 Phase data is exchanged by pushkey on pull and push. Some servers have
44 Phase data is exchanged by pushkey on pull and push. Some servers have
45 a publish option set, we call such a server a "publishing server".
45 a publish option set, we call such a server a "publishing server".
46 Pushing a draft changeset to a publishing server changes the phase to
46 Pushing a draft changeset to a publishing server changes the phase to
47 public.
47 public.
48
48
49 A small list of fact/rules define the exchange of phase:
49 A small list of fact/rules define the exchange of phase:
50
50
51 * old client never changes server states
51 * old client never changes server states
52 * pull never changes server states
52 * pull never changes server states
53 * publish and old server changesets are seen as public by client
53 * publish and old server changesets are seen as public by client
54 * any secret changeset seen in another repository is lowered to at
54 * any secret changeset seen in another repository is lowered to at
55 least draft
55 least draft
56
56
57 Here is the final table summing up the 49 possible use cases of phase
57 Here is the final table summing up the 49 possible use cases of phase
58 exchange:
58 exchange:
59
59
60 server
60 server
61 old publish non-publish
61 old publish non-publish
62 N X N D P N D P
62 N X N D P N D P
63 old client
63 old client
64 pull
64 pull
65 N - X/X - X/D X/P - X/D X/P
65 N - X/X - X/D X/P - X/D X/P
66 X - X/X - X/D X/P - X/D X/P
66 X - X/X - X/D X/P - X/D X/P
67 push
67 push
68 X X/X X/X X/P X/P X/P X/D X/D X/P
68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 new client
69 new client
70 pull
70 pull
71 N - P/X - P/D P/P - D/D P/P
71 N - P/X - P/D P/P - D/D P/P
72 D - P/X - P/D P/P - D/D P/P
72 D - P/X - P/D P/P - D/D P/P
73 P - P/X - P/D P/P - P/D P/P
73 P - P/X - P/D P/P - P/D P/P
74 push
74 push
75 D P/X P/X P/P P/P P/P D/D D/D P/P
75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 P P/X P/X P/P P/P P/P P/P P/P P/P
76 P P/X P/X P/P P/P P/P P/P P/P P/P
77
77
78 Legend:
78 Legend:
79
79
80 A/B = final state on client / state on server
80 A/B = final state on client / state on server
81
81
82 * N = new/not present,
82 * N = new/not present,
83 * P = public,
83 * P = public,
84 * D = draft,
84 * D = draft,
85 * X = not tracked (i.e., the old client or server has no internal
85 * X = not tracked (i.e., the old client or server has no internal
86 way of recording the phase.)
86 way of recording the phase.)
87
87
88 passive = only pushes
88 passive = only pushes
89
89
90
90
91 A cell here can be read like this:
91 A cell here can be read like this:
92
92
93 "When a new client pushes a draft changeset (D) to a publishing
93 "When a new client pushes a draft changeset (D) to a publishing
94 server where it's not present (N), it's marked public on both
94 server where it's not present (N), it's marked public on both
95 sides (P/P)."
95 sides (P/P)."
96
96
97 Note: old client behave as a publishing server with draft only content
97 Note: old client behave as a publishing server with draft only content
98 - other people see it as public
98 - other people see it as public
99 - content is pushed as draft
99 - content is pushed as draft
100
100
101 """
101 """
102
102
103 import os
103 import os
104 import errno
104 import errno
105 from node import nullid, nullrev, bin, hex, short
105 from node import nullid, nullrev, bin, hex, short
106 from i18n import _
106 from i18n import _
107 import util, error
107 import util, error
108
108
109 allphases = public, draft, secret = range(3)
109 allphases = public, draft, secret = range(3)
110 trackedphases = allphases[1:]
110 trackedphases = allphases[1:]
111 phasenames = ['public', 'draft', 'secret']
111 phasenames = ['public', 'draft', 'secret']
112
112
113 def _readroots(repo, phasedefaults=None):
113 def _readroots(repo, phasedefaults=None):
114 """Read phase roots from disk
114 """Read phase roots from disk
115
115
116 phasedefaults is a list of fn(repo, roots) callable, which are
116 phasedefaults is a list of fn(repo, roots) callable, which are
117 executed if the phase roots file does not exist. When phases are
117 executed if the phase roots file does not exist. When phases are
118 being initialized on an existing repository, this could be used to
118 being initialized on an existing repository, this could be used to
119 set selected changesets phase to something else than public.
119 set selected changesets phase to something else than public.
120
120
121 Return (roots, dirty) where dirty is true if roots differ from
121 Return (roots, dirty) where dirty is true if roots differ from
122 what is being stored.
122 what is being stored.
123 """
123 """
124 repo = repo.unfiltered()
124 repo = repo.unfiltered()
125 dirty = False
125 dirty = False
126 roots = [set() for i in allphases]
126 roots = [set() for i in allphases]
127 try:
127 try:
128 f = None
128 f = None
129 if 'HG_PENDING' in os.environ:
129 if 'HG_PENDING' in os.environ:
130 try:
130 try:
131 f = repo.svfs('phaseroots.pending')
131 f = repo.svfs('phaseroots.pending')
132 except IOError, inst:
132 except IOError, inst:
133 if inst.errno != errno.ENOENT:
133 if inst.errno != errno.ENOENT:
134 raise
134 raise
135 if f is None:
135 if f is None:
136 f = repo.sopener('phaseroots')
136 f = repo.sopener('phaseroots')
137 try:
137 try:
138 for line in f:
138 for line in f:
139 phase, nh = line.split()
139 phase, nh = line.split()
140 roots[int(phase)].add(bin(nh))
140 roots[int(phase)].add(bin(nh))
141 finally:
141 finally:
142 f.close()
142 f.close()
143 except IOError, inst:
143 except IOError, inst:
144 if inst.errno != errno.ENOENT:
144 if inst.errno != errno.ENOENT:
145 raise
145 raise
146 if phasedefaults:
146 if phasedefaults:
147 for f in phasedefaults:
147 for f in phasedefaults:
148 roots = f(repo, roots)
148 roots = f(repo, roots)
149 dirty = True
149 dirty = True
150 return roots, dirty
150 return roots, dirty
151
151
152 class phasecache(object):
152 class phasecache(object):
153 def __init__(self, repo, phasedefaults, _load=True):
153 def __init__(self, repo, phasedefaults, _load=True):
154 if _load:
154 if _load:
155 # Cheap trick to allow shallow-copy without copy module
155 # Cheap trick to allow shallow-copy without copy module
156 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
156 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
157 self._phaserevs = None
157 self._phaserevs = None
158 self.filterunknown(repo)
158 self.filterunknown(repo)
159 self.opener = repo.sopener
159 self.opener = repo.sopener
160
160
161 def copy(self):
161 def copy(self):
162 # Shallow copy meant to ensure isolation in
162 # Shallow copy meant to ensure isolation in
163 # advance/retractboundary(), nothing more.
163 # advance/retractboundary(), nothing more.
164 ph = phasecache(None, None, _load=False)
164 ph = self.__class__(None, None, _load=False)
165 ph.phaseroots = self.phaseroots[:]
165 ph.phaseroots = self.phaseroots[:]
166 ph.dirty = self.dirty
166 ph.dirty = self.dirty
167 ph.opener = self.opener
167 ph.opener = self.opener
168 ph._phaserevs = self._phaserevs
168 ph._phaserevs = self._phaserevs
169 return ph
169 return ph
170
170
171 def replace(self, phcache):
171 def replace(self, phcache):
172 for a in 'phaseroots dirty opener _phaserevs'.split():
172 for a in 'phaseroots dirty opener _phaserevs'.split():
173 setattr(self, a, getattr(phcache, a))
173 setattr(self, a, getattr(phcache, a))
174
174
175 def getphaserevs(self, repo):
175 def getphaserevs(self, repo):
176 if self._phaserevs is None:
176 if self._phaserevs is None:
177 repo = repo.unfiltered()
177 repo = repo.unfiltered()
178 revs = [public] * len(repo.changelog)
178 revs = [public] * len(repo.changelog)
179 self._phaserevs = revs
179 self._phaserevs = revs
180 self._populatephaseroots(repo)
180 self._populatephaseroots(repo)
181 for phase in trackedphases:
181 for phase in trackedphases:
182 roots = map(repo.changelog.rev, self.phaseroots[phase])
182 roots = map(repo.changelog.rev, self.phaseroots[phase])
183 if roots:
183 if roots:
184 for rev in roots:
184 for rev in roots:
185 revs[rev] = phase
185 revs[rev] = phase
186 for rev in repo.changelog.descendants(roots):
186 for rev in repo.changelog.descendants(roots):
187 revs[rev] = phase
187 revs[rev] = phase
188 return self._phaserevs
188 return self._phaserevs
189
189
190 def invalidate(self):
190 def invalidate(self):
191 self._phaserevs = None
191 self._phaserevs = None
192
192
193 def _populatephaseroots(self, repo):
193 def _populatephaseroots(self, repo):
194 """Fills the _phaserevs cache with phases for the roots.
194 """Fills the _phaserevs cache with phases for the roots.
195 """
195 """
196 cl = repo.changelog
196 cl = repo.changelog
197 phaserevs = self._phaserevs
197 phaserevs = self._phaserevs
198 for phase in trackedphases:
198 for phase in trackedphases:
199 roots = map(cl.rev, self.phaseroots[phase])
199 roots = map(cl.rev, self.phaseroots[phase])
200 for root in roots:
200 for root in roots:
201 phaserevs[root] = phase
201 phaserevs[root] = phase
202
202
203 def phase(self, repo, rev):
203 def phase(self, repo, rev):
204 # We need a repo argument here to be able to build _phaserevs
204 # We need a repo argument here to be able to build _phaserevs
205 # if necessary. The repository instance is not stored in
205 # if necessary. The repository instance is not stored in
206 # phasecache to avoid reference cycles. The changelog instance
206 # phasecache to avoid reference cycles. The changelog instance
207 # is not stored because it is a filecache() property and can
207 # is not stored because it is a filecache() property and can
208 # be replaced without us being notified.
208 # be replaced without us being notified.
209 if rev == nullrev:
209 if rev == nullrev:
210 return public
210 return public
211 if rev < nullrev:
211 if rev < nullrev:
212 raise ValueError(_('cannot lookup negative revision'))
212 raise ValueError(_('cannot lookup negative revision'))
213 if self._phaserevs is None or rev >= len(self._phaserevs):
213 if self._phaserevs is None or rev >= len(self._phaserevs):
214 self.invalidate()
214 self.invalidate()
215 self._phaserevs = self.getphaserevs(repo)
215 self._phaserevs = self.getphaserevs(repo)
216 return self._phaserevs[rev]
216 return self._phaserevs[rev]
217
217
218 def write(self):
218 def write(self):
219 if not self.dirty:
219 if not self.dirty:
220 return
220 return
221 f = self.opener('phaseroots', 'w', atomictemp=True)
221 f = self.opener('phaseroots', 'w', atomictemp=True)
222 try:
222 try:
223 self._write(f)
223 self._write(f)
224 finally:
224 finally:
225 f.close()
225 f.close()
226
226
227 def _write(self, fp):
227 def _write(self, fp):
228 for phase, roots in enumerate(self.phaseroots):
228 for phase, roots in enumerate(self.phaseroots):
229 for h in roots:
229 for h in roots:
230 fp.write('%i %s\n' % (phase, hex(h)))
230 fp.write('%i %s\n' % (phase, hex(h)))
231 self.dirty = False
231 self.dirty = False
232
232
233 def _updateroots(self, phase, newroots, tr):
233 def _updateroots(self, phase, newroots, tr):
234 self.phaseroots[phase] = newroots
234 self.phaseroots[phase] = newroots
235 self.invalidate()
235 self.invalidate()
236 self.dirty = True
236 self.dirty = True
237
237
238 tr.addfilegenerator('phase', ('phaseroots',), self._write)
238 tr.addfilegenerator('phase', ('phaseroots',), self._write)
239 tr.hookargs['phases_moved'] = '1'
239 tr.hookargs['phases_moved'] = '1'
240
240
241 def advanceboundary(self, repo, tr, targetphase, nodes):
241 def advanceboundary(self, repo, tr, targetphase, nodes):
242 # Be careful to preserve shallow-copied values: do not update
242 # Be careful to preserve shallow-copied values: do not update
243 # phaseroots values, replace them.
243 # phaseroots values, replace them.
244
244
245 repo = repo.unfiltered()
245 repo = repo.unfiltered()
246 delroots = [] # set of root deleted by this path
246 delroots = [] # set of root deleted by this path
247 for phase in xrange(targetphase + 1, len(allphases)):
247 for phase in xrange(targetphase + 1, len(allphases)):
248 # filter nodes that are not in a compatible phase already
248 # filter nodes that are not in a compatible phase already
249 nodes = [n for n in nodes
249 nodes = [n for n in nodes
250 if self.phase(repo, repo[n].rev()) >= phase]
250 if self.phase(repo, repo[n].rev()) >= phase]
251 if not nodes:
251 if not nodes:
252 break # no roots to move anymore
252 break # no roots to move anymore
253 olds = self.phaseroots[phase]
253 olds = self.phaseroots[phase]
254 roots = set(ctx.node() for ctx in repo.set(
254 roots = set(ctx.node() for ctx in repo.set(
255 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
255 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
256 if olds != roots:
256 if olds != roots:
257 self._updateroots(phase, roots, tr)
257 self._updateroots(phase, roots, tr)
258 # some roots may need to be declared for lower phases
258 # some roots may need to be declared for lower phases
259 delroots.extend(olds - roots)
259 delroots.extend(olds - roots)
260 # declare deleted root in the target phase
260 # declare deleted root in the target phase
261 if targetphase != 0:
261 if targetphase != 0:
262 self.retractboundary(repo, tr, targetphase, delroots)
262 self.retractboundary(repo, tr, targetphase, delroots)
263 repo.invalidatevolatilesets()
263 repo.invalidatevolatilesets()
264
264
265 def retractboundary(self, repo, tr, targetphase, nodes):
265 def retractboundary(self, repo, tr, targetphase, nodes):
266 # Be careful to preserve shallow-copied values: do not update
266 # Be careful to preserve shallow-copied values: do not update
267 # phaseroots values, replace them.
267 # phaseroots values, replace them.
268
268
269 repo = repo.unfiltered()
269 repo = repo.unfiltered()
270 currentroots = self.phaseroots[targetphase]
270 currentroots = self.phaseroots[targetphase]
271 newroots = [n for n in nodes
271 newroots = [n for n in nodes
272 if self.phase(repo, repo[n].rev()) < targetphase]
272 if self.phase(repo, repo[n].rev()) < targetphase]
273 if newroots:
273 if newroots:
274 if nullid in newroots:
274 if nullid in newroots:
275 raise util.Abort(_('cannot change null revision phase'))
275 raise util.Abort(_('cannot change null revision phase'))
276 currentroots = currentroots.copy()
276 currentroots = currentroots.copy()
277 currentroots.update(newroots)
277 currentroots.update(newroots)
278 ctxs = repo.set('roots(%ln::)', currentroots)
278 ctxs = repo.set('roots(%ln::)', currentroots)
279 currentroots.intersection_update(ctx.node() for ctx in ctxs)
279 currentroots.intersection_update(ctx.node() for ctx in ctxs)
280 self._updateroots(targetphase, currentroots, tr)
280 self._updateroots(targetphase, currentroots, tr)
281 repo.invalidatevolatilesets()
281 repo.invalidatevolatilesets()
282
282
283 def filterunknown(self, repo):
283 def filterunknown(self, repo):
284 """remove unknown nodes from the phase boundary
284 """remove unknown nodes from the phase boundary
285
285
286 Nothing is lost as unknown nodes only hold data for their descendants.
286 Nothing is lost as unknown nodes only hold data for their descendants.
287 """
287 """
288 filtered = False
288 filtered = False
289 nodemap = repo.changelog.nodemap # to filter unknown nodes
289 nodemap = repo.changelog.nodemap # to filter unknown nodes
290 for phase, nodes in enumerate(self.phaseroots):
290 for phase, nodes in enumerate(self.phaseroots):
291 missing = sorted(node for node in nodes if node not in nodemap)
291 missing = sorted(node for node in nodes if node not in nodemap)
292 if missing:
292 if missing:
293 for mnode in missing:
293 for mnode in missing:
294 repo.ui.debug(
294 repo.ui.debug(
295 'removing unknown node %s from %i-phase boundary\n'
295 'removing unknown node %s from %i-phase boundary\n'
296 % (short(mnode), phase))
296 % (short(mnode), phase))
297 nodes.symmetric_difference_update(missing)
297 nodes.symmetric_difference_update(missing)
298 filtered = True
298 filtered = True
299 if filtered:
299 if filtered:
300 self.dirty = True
300 self.dirty = True
301 # filterunknown is called by repo.destroyed, we may have no changes in
301 # filterunknown is called by repo.destroyed, we may have no changes in
302 # root but phaserevs contents is certainly invalid (or at least we
302 # root but phaserevs contents is certainly invalid (or at least we
303 # have not proper way to check that). related to issue 3858.
303 # have not proper way to check that). related to issue 3858.
304 #
304 #
305 # The other caller is __init__ that have no _phaserevs initialized
305 # The other caller is __init__ that have no _phaserevs initialized
306 # anyway. If this change we should consider adding a dedicated
306 # anyway. If this change we should consider adding a dedicated
307 # "destroyed" function to phasecache or a proper cache key mechanism
307 # "destroyed" function to phasecache or a proper cache key mechanism
308 # (see branchmap one)
308 # (see branchmap one)
309 self.invalidate()
309 self.invalidate()
310
310
311 def advanceboundary(repo, tr, targetphase, nodes):
311 def advanceboundary(repo, tr, targetphase, nodes):
312 """Add nodes to a phase changing other nodes phases if necessary.
312 """Add nodes to a phase changing other nodes phases if necessary.
313
313
314 This function move boundary *forward* this means that all nodes
314 This function move boundary *forward* this means that all nodes
315 are set in the target phase or kept in a *lower* phase.
315 are set in the target phase or kept in a *lower* phase.
316
316
317 Simplify boundary to contains phase roots only."""
317 Simplify boundary to contains phase roots only."""
318 phcache = repo._phasecache.copy()
318 phcache = repo._phasecache.copy()
319 phcache.advanceboundary(repo, tr, targetphase, nodes)
319 phcache.advanceboundary(repo, tr, targetphase, nodes)
320 repo._phasecache.replace(phcache)
320 repo._phasecache.replace(phcache)
321
321
322 def retractboundary(repo, tr, targetphase, nodes):
322 def retractboundary(repo, tr, targetphase, nodes):
323 """Set nodes back to a phase changing other nodes phases if
323 """Set nodes back to a phase changing other nodes phases if
324 necessary.
324 necessary.
325
325
326 This function move boundary *backward* this means that all nodes
326 This function move boundary *backward* this means that all nodes
327 are set in the target phase or kept in a *higher* phase.
327 are set in the target phase or kept in a *higher* phase.
328
328
329 Simplify boundary to contains phase roots only."""
329 Simplify boundary to contains phase roots only."""
330 phcache = repo._phasecache.copy()
330 phcache = repo._phasecache.copy()
331 phcache.retractboundary(repo, tr, targetphase, nodes)
331 phcache.retractboundary(repo, tr, targetphase, nodes)
332 repo._phasecache.replace(phcache)
332 repo._phasecache.replace(phcache)
333
333
334 def listphases(repo):
334 def listphases(repo):
335 """List phases root for serialization over pushkey"""
335 """List phases root for serialization over pushkey"""
336 keys = {}
336 keys = {}
337 value = '%i' % draft
337 value = '%i' % draft
338 for root in repo._phasecache.phaseroots[draft]:
338 for root in repo._phasecache.phaseroots[draft]:
339 keys[hex(root)] = value
339 keys[hex(root)] = value
340
340
341 if repo.ui.configbool('phases', 'publish', True):
341 if repo.ui.configbool('phases', 'publish', True):
342 # Add an extra data to let remote know we are a publishing
342 # Add an extra data to let remote know we are a publishing
343 # repo. Publishing repo can't just pretend they are old repo.
343 # repo. Publishing repo can't just pretend they are old repo.
344 # When pushing to a publishing repo, the client still need to
344 # When pushing to a publishing repo, the client still need to
345 # push phase boundary
345 # push phase boundary
346 #
346 #
347 # Push do not only push changeset. It also push phase data.
347 # Push do not only push changeset. It also push phase data.
348 # New phase data may apply to common changeset which won't be
348 # New phase data may apply to common changeset which won't be
349 # push (as they are common). Here is a very simple example:
349 # push (as they are common). Here is a very simple example:
350 #
350 #
351 # 1) repo A push changeset X as draft to repo B
351 # 1) repo A push changeset X as draft to repo B
352 # 2) repo B make changeset X public
352 # 2) repo B make changeset X public
353 # 3) repo B push to repo A. X is not pushed but the data that
353 # 3) repo B push to repo A. X is not pushed but the data that
354 # X as now public should
354 # X as now public should
355 #
355 #
356 # The server can't handle it on it's own as it has no idea of
356 # The server can't handle it on it's own as it has no idea of
357 # client phase data.
357 # client phase data.
358 keys['publishing'] = 'True'
358 keys['publishing'] = 'True'
359 return keys
359 return keys
360
360
361 def pushphase(repo, nhex, oldphasestr, newphasestr):
361 def pushphase(repo, nhex, oldphasestr, newphasestr):
362 """List phases root for serialization over pushkey"""
362 """List phases root for serialization over pushkey"""
363 repo = repo.unfiltered()
363 repo = repo.unfiltered()
364 tr = None
364 tr = None
365 lock = repo.lock()
365 lock = repo.lock()
366 try:
366 try:
367 currentphase = repo[nhex].phase()
367 currentphase = repo[nhex].phase()
368 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
368 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
369 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
369 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
370 if currentphase == oldphase and newphase < oldphase:
370 if currentphase == oldphase and newphase < oldphase:
371 tr = repo.transaction('pushkey-phase')
371 tr = repo.transaction('pushkey-phase')
372 advanceboundary(repo, tr, newphase, [bin(nhex)])
372 advanceboundary(repo, tr, newphase, [bin(nhex)])
373 tr.close()
373 tr.close()
374 return 1
374 return 1
375 elif currentphase == newphase:
375 elif currentphase == newphase:
376 # raced, but got correct result
376 # raced, but got correct result
377 return 1
377 return 1
378 else:
378 else:
379 return 0
379 return 0
380 finally:
380 finally:
381 if tr:
381 if tr:
382 tr.release()
382 tr.release()
383 lock.release()
383 lock.release()
384
384
385 def analyzeremotephases(repo, subset, roots):
385 def analyzeremotephases(repo, subset, roots):
386 """Compute phases heads and root in a subset of node from root dict
386 """Compute phases heads and root in a subset of node from root dict
387
387
388 * subset is heads of the subset
388 * subset is heads of the subset
389 * roots is {<nodeid> => phase} mapping. key and value are string.
389 * roots is {<nodeid> => phase} mapping. key and value are string.
390
390
391 Accept unknown element input
391 Accept unknown element input
392 """
392 """
393 repo = repo.unfiltered()
393 repo = repo.unfiltered()
394 # build list from dictionary
394 # build list from dictionary
395 draftroots = []
395 draftroots = []
396 nodemap = repo.changelog.nodemap # to filter unknown nodes
396 nodemap = repo.changelog.nodemap # to filter unknown nodes
397 for nhex, phase in roots.iteritems():
397 for nhex, phase in roots.iteritems():
398 if nhex == 'publishing': # ignore data related to publish option
398 if nhex == 'publishing': # ignore data related to publish option
399 continue
399 continue
400 node = bin(nhex)
400 node = bin(nhex)
401 phase = int(phase)
401 phase = int(phase)
402 if phase == 0:
402 if phase == 0:
403 if node != nullid:
403 if node != nullid:
404 repo.ui.warn(_('ignoring inconsistent public root'
404 repo.ui.warn(_('ignoring inconsistent public root'
405 ' from remote: %s\n') % nhex)
405 ' from remote: %s\n') % nhex)
406 elif phase == 1:
406 elif phase == 1:
407 if node in nodemap:
407 if node in nodemap:
408 draftroots.append(node)
408 draftroots.append(node)
409 else:
409 else:
410 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
410 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
411 % (phase, nhex))
411 % (phase, nhex))
412 # compute heads
412 # compute heads
413 publicheads = newheads(repo, subset, draftroots)
413 publicheads = newheads(repo, subset, draftroots)
414 return publicheads, draftroots
414 return publicheads, draftroots
415
415
416 def newheads(repo, heads, roots):
416 def newheads(repo, heads, roots):
417 """compute new head of a subset minus another
417 """compute new head of a subset minus another
418
418
419 * `heads`: define the first subset
419 * `heads`: define the first subset
420 * `roots`: define the second we subtract from the first"""
420 * `roots`: define the second we subtract from the first"""
421 repo = repo.unfiltered()
421 repo = repo.unfiltered()
422 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
422 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
423 heads, roots, roots, heads)
423 heads, roots, roots, heads)
424 return [c.node() for c in revset]
424 return [c.node() for c in revset]
425
425
426
426
427 def newcommitphase(ui):
427 def newcommitphase(ui):
428 """helper to get the target phase of new commit
428 """helper to get the target phase of new commit
429
429
430 Handle all possible values for the phases.new-commit options.
430 Handle all possible values for the phases.new-commit options.
431
431
432 """
432 """
433 v = ui.config('phases', 'new-commit', draft)
433 v = ui.config('phases', 'new-commit', draft)
434 try:
434 try:
435 return phasenames.index(v)
435 return phasenames.index(v)
436 except ValueError:
436 except ValueError:
437 try:
437 try:
438 return int(v)
438 return int(v)
439 except ValueError:
439 except ValueError:
440 msg = _("phases.new-commit: not a valid phase name ('%s')")
440 msg = _("phases.new-commit: not a valid phase name ('%s')")
441 raise error.ConfigError(msg % v)
441 raise error.ConfigError(msg % v)
442
442
443 def hassecret(repo):
443 def hassecret(repo):
444 """utility function that check if a repo have any secret changeset."""
444 """utility function that check if a repo have any secret changeset."""
445 return bool(repo._phasecache.phaseroots[2])
445 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now