##// END OF EJS Templates
obsolete: extract obsolescence marker pulling into a dedicated function...
Pierre-Yves David -
r19054:d5f968f7 default
parent child Browse files
Show More
@@ -1,2586 +1,2584 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from node import hex, nullid, short
8 8 from i18n import _
9 9 import peer, changegroup, subrepo, discovery, pushkey, obsolete, repoview
10 10 import changelog, dirstate, filelog, manifest, context, bookmarks, phases
11 import lock, transaction, store, encoding, base85
11 import lock, transaction, store, encoding
12 12 import scmutil, util, extensions, hook, error, revset
13 13 import match as matchmod
14 14 import merge as mergemod
15 15 import tags as tagsmod
16 16 from lock import release
17 17 import weakref, errno, os, time, inspect
18 18 import branchmap
19 19 propertycache = util.propertycache
20 20 filecache = scmutil.filecache
21 21
22 22 class repofilecache(filecache):
23 23 """All filecache usage on repo are done for logic that should be unfiltered
24 24 """
25 25
26 26 def __get__(self, repo, type=None):
27 27 return super(repofilecache, self).__get__(repo.unfiltered(), type)
28 28 def __set__(self, repo, value):
29 29 return super(repofilecache, self).__set__(repo.unfiltered(), value)
30 30 def __delete__(self, repo):
31 31 return super(repofilecache, self).__delete__(repo.unfiltered())
32 32
33 33 class storecache(repofilecache):
34 34 """filecache for files in the store"""
35 35 def join(self, obj, fname):
36 36 return obj.sjoin(fname)
37 37
38 38 class unfilteredpropertycache(propertycache):
39 39 """propertycache that apply to unfiltered repo only"""
40 40
41 41 def __get__(self, repo, type=None):
42 42 return super(unfilteredpropertycache, self).__get__(repo.unfiltered())
43 43
44 44 class filteredpropertycache(propertycache):
45 45 """propertycache that must take filtering in account"""
46 46
47 47 def cachevalue(self, obj, value):
48 48 object.__setattr__(obj, self.name, value)
49 49
50 50
51 51 def hasunfilteredcache(repo, name):
52 52 """check if a repo has an unfilteredpropertycache value for <name>"""
53 53 return name in vars(repo.unfiltered())
54 54
55 55 def unfilteredmethod(orig):
56 56 """decorate method that always need to be run on unfiltered version"""
57 57 def wrapper(repo, *args, **kwargs):
58 58 return orig(repo.unfiltered(), *args, **kwargs)
59 59 return wrapper
60 60
61 61 MODERNCAPS = set(('lookup', 'branchmap', 'pushkey', 'known', 'getbundle'))
62 62 LEGACYCAPS = MODERNCAPS.union(set(['changegroupsubset']))
63 63
64 64 class localpeer(peer.peerrepository):
65 65 '''peer for a local repo; reflects only the most recent API'''
66 66
67 67 def __init__(self, repo, caps=MODERNCAPS):
68 68 peer.peerrepository.__init__(self)
69 69 self._repo = repo.filtered('served')
70 70 self.ui = repo.ui
71 71 self._caps = repo._restrictcapabilities(caps)
72 72 self.requirements = repo.requirements
73 73 self.supportedformats = repo.supportedformats
74 74
75 75 def close(self):
76 76 self._repo.close()
77 77
78 78 def _capabilities(self):
79 79 return self._caps
80 80
81 81 def local(self):
82 82 return self._repo
83 83
84 84 def canpush(self):
85 85 return True
86 86
87 87 def url(self):
88 88 return self._repo.url()
89 89
90 90 def lookup(self, key):
91 91 return self._repo.lookup(key)
92 92
93 93 def branchmap(self):
94 94 return self._repo.branchmap()
95 95
96 96 def heads(self):
97 97 return self._repo.heads()
98 98
99 99 def known(self, nodes):
100 100 return self._repo.known(nodes)
101 101
102 102 def getbundle(self, source, heads=None, common=None):
103 103 return self._repo.getbundle(source, heads=heads, common=common)
104 104
105 105 # TODO We might want to move the next two calls into legacypeer and add
106 106 # unbundle instead.
107 107
108 108 def lock(self):
109 109 return self._repo.lock()
110 110
111 111 def addchangegroup(self, cg, source, url):
112 112 return self._repo.addchangegroup(cg, source, url)
113 113
114 114 def pushkey(self, namespace, key, old, new):
115 115 return self._repo.pushkey(namespace, key, old, new)
116 116
117 117 def listkeys(self, namespace):
118 118 return self._repo.listkeys(namespace)
119 119
120 120 def debugwireargs(self, one, two, three=None, four=None, five=None):
121 121 '''used to test argument passing over the wire'''
122 122 return "%s %s %s %s %s" % (one, two, three, four, five)
123 123
124 124 class locallegacypeer(localpeer):
125 125 '''peer extension which implements legacy methods too; used for tests with
126 126 restricted capabilities'''
127 127
128 128 def __init__(self, repo):
129 129 localpeer.__init__(self, repo, caps=LEGACYCAPS)
130 130
131 131 def branches(self, nodes):
132 132 return self._repo.branches(nodes)
133 133
134 134 def between(self, pairs):
135 135 return self._repo.between(pairs)
136 136
137 137 def changegroup(self, basenodes, source):
138 138 return self._repo.changegroup(basenodes, source)
139 139
140 140 def changegroupsubset(self, bases, heads, source):
141 141 return self._repo.changegroupsubset(bases, heads, source)
142 142
143 143 class localrepository(object):
144 144
145 145 supportedformats = set(('revlogv1', 'generaldelta'))
146 146 supported = supportedformats | set(('store', 'fncache', 'shared',
147 147 'dotencode'))
148 148 openerreqs = set(('revlogv1', 'generaldelta'))
149 149 requirements = ['revlogv1']
150 150 filtername = None
151 151
152 152 def _baserequirements(self, create):
153 153 return self.requirements[:]
154 154
155 155 def __init__(self, baseui, path=None, create=False):
156 156 self.wvfs = scmutil.vfs(path, expandpath=True, realpath=True)
157 157 self.wopener = self.wvfs
158 158 self.root = self.wvfs.base
159 159 self.path = self.wvfs.join(".hg")
160 160 self.origroot = path
161 161 self.auditor = scmutil.pathauditor(self.root, self._checknested)
162 162 self.vfs = scmutil.vfs(self.path)
163 163 self.opener = self.vfs
164 164 self.baseui = baseui
165 165 self.ui = baseui.copy()
166 166 # A list of callback to shape the phase if no data were found.
167 167 # Callback are in the form: func(repo, roots) --> processed root.
168 168 # This list it to be filled by extension during repo setup
169 169 self._phasedefaults = []
170 170 try:
171 171 self.ui.readconfig(self.join("hgrc"), self.root)
172 172 extensions.loadall(self.ui)
173 173 except IOError:
174 174 pass
175 175
176 176 if not self.vfs.isdir():
177 177 if create:
178 178 if not self.wvfs.exists():
179 179 self.wvfs.makedirs()
180 180 self.vfs.makedir(notindexed=True)
181 181 requirements = self._baserequirements(create)
182 182 if self.ui.configbool('format', 'usestore', True):
183 183 self.vfs.mkdir("store")
184 184 requirements.append("store")
185 185 if self.ui.configbool('format', 'usefncache', True):
186 186 requirements.append("fncache")
187 187 if self.ui.configbool('format', 'dotencode', True):
188 188 requirements.append('dotencode')
189 189 # create an invalid changelog
190 190 self.vfs.append(
191 191 "00changelog.i",
192 192 '\0\0\0\2' # represents revlogv2
193 193 ' dummy changelog to prevent using the old repo layout'
194 194 )
195 195 if self.ui.configbool('format', 'generaldelta', False):
196 196 requirements.append("generaldelta")
197 197 requirements = set(requirements)
198 198 else:
199 199 raise error.RepoError(_("repository %s not found") % path)
200 200 elif create:
201 201 raise error.RepoError(_("repository %s already exists") % path)
202 202 else:
203 203 try:
204 204 requirements = scmutil.readrequires(self.vfs, self.supported)
205 205 except IOError, inst:
206 206 if inst.errno != errno.ENOENT:
207 207 raise
208 208 requirements = set()
209 209
210 210 self.sharedpath = self.path
211 211 try:
212 212 vfs = scmutil.vfs(self.vfs.read("sharedpath").rstrip('\n'),
213 213 realpath=True)
214 214 s = vfs.base
215 215 if not vfs.exists():
216 216 raise error.RepoError(
217 217 _('.hg/sharedpath points to nonexistent directory %s') % s)
218 218 self.sharedpath = s
219 219 except IOError, inst:
220 220 if inst.errno != errno.ENOENT:
221 221 raise
222 222
223 223 self.store = store.store(requirements, self.sharedpath, scmutil.vfs)
224 224 self.spath = self.store.path
225 225 self.svfs = self.store.vfs
226 226 self.sopener = self.svfs
227 227 self.sjoin = self.store.join
228 228 self.vfs.createmode = self.store.createmode
229 229 self._applyrequirements(requirements)
230 230 if create:
231 231 self._writerequirements()
232 232
233 233
234 234 self._branchcaches = {}
235 235 self.filterpats = {}
236 236 self._datafilters = {}
237 237 self._transref = self._lockref = self._wlockref = None
238 238
239 239 # A cache for various files under .hg/ that tracks file changes,
240 240 # (used by the filecache decorator)
241 241 #
242 242 # Maps a property name to its util.filecacheentry
243 243 self._filecache = {}
244 244
245 245 # hold sets of revision to be filtered
246 246 # should be cleared when something might have changed the filter value:
247 247 # - new changesets,
248 248 # - phase change,
249 249 # - new obsolescence marker,
250 250 # - working directory parent change,
251 251 # - bookmark changes
252 252 self.filteredrevcache = {}
253 253
254 254 def close(self):
255 255 pass
256 256
257 257 def _restrictcapabilities(self, caps):
258 258 return caps
259 259
260 260 def _applyrequirements(self, requirements):
261 261 self.requirements = requirements
262 262 self.sopener.options = dict((r, 1) for r in requirements
263 263 if r in self.openerreqs)
264 264
265 265 def _writerequirements(self):
266 266 reqfile = self.opener("requires", "w")
267 267 for r in sorted(self.requirements):
268 268 reqfile.write("%s\n" % r)
269 269 reqfile.close()
270 270
271 271 def _checknested(self, path):
272 272 """Determine if path is a legal nested repository."""
273 273 if not path.startswith(self.root):
274 274 return False
275 275 subpath = path[len(self.root) + 1:]
276 276 normsubpath = util.pconvert(subpath)
277 277
278 278 # XXX: Checking against the current working copy is wrong in
279 279 # the sense that it can reject things like
280 280 #
281 281 # $ hg cat -r 10 sub/x.txt
282 282 #
283 283 # if sub/ is no longer a subrepository in the working copy
284 284 # parent revision.
285 285 #
286 286 # However, it can of course also allow things that would have
287 287 # been rejected before, such as the above cat command if sub/
288 288 # is a subrepository now, but was a normal directory before.
289 289 # The old path auditor would have rejected by mistake since it
290 290 # panics when it sees sub/.hg/.
291 291 #
292 292 # All in all, checking against the working copy seems sensible
293 293 # since we want to prevent access to nested repositories on
294 294 # the filesystem *now*.
295 295 ctx = self[None]
296 296 parts = util.splitpath(subpath)
297 297 while parts:
298 298 prefix = '/'.join(parts)
299 299 if prefix in ctx.substate:
300 300 if prefix == normsubpath:
301 301 return True
302 302 else:
303 303 sub = ctx.sub(prefix)
304 304 return sub.checknested(subpath[len(prefix) + 1:])
305 305 else:
306 306 parts.pop()
307 307 return False
308 308
309 309 def peer(self):
310 310 return localpeer(self) # not cached to avoid reference cycle
311 311
312 312 def unfiltered(self):
313 313 """Return unfiltered version of the repository
314 314
315 315 Intended to be overwritten by filtered repo."""
316 316 return self
317 317
318 318 def filtered(self, name):
319 319 """Return a filtered version of a repository"""
320 320 # build a new class with the mixin and the current class
321 321 # (possibly subclass of the repo)
322 322 class proxycls(repoview.repoview, self.unfiltered().__class__):
323 323 pass
324 324 return proxycls(self, name)
325 325
326 326 @repofilecache('bookmarks')
327 327 def _bookmarks(self):
328 328 return bookmarks.bmstore(self)
329 329
330 330 @repofilecache('bookmarks.current')
331 331 def _bookmarkcurrent(self):
332 332 return bookmarks.readcurrent(self)
333 333
334 334 def bookmarkheads(self, bookmark):
335 335 name = bookmark.split('@', 1)[0]
336 336 heads = []
337 337 for mark, n in self._bookmarks.iteritems():
338 338 if mark.split('@', 1)[0] == name:
339 339 heads.append(n)
340 340 return heads
341 341
342 342 @storecache('phaseroots')
343 343 def _phasecache(self):
344 344 return phases.phasecache(self, self._phasedefaults)
345 345
346 346 @storecache('obsstore')
347 347 def obsstore(self):
348 348 store = obsolete.obsstore(self.sopener)
349 349 if store and not obsolete._enabled:
350 350 # message is rare enough to not be translated
351 351 msg = 'obsolete feature not enabled but %i markers found!\n'
352 352 self.ui.warn(msg % len(list(store)))
353 353 return store
354 354
355 355 @storecache('00changelog.i')
356 356 def changelog(self):
357 357 c = changelog.changelog(self.sopener)
358 358 if 'HG_PENDING' in os.environ:
359 359 p = os.environ['HG_PENDING']
360 360 if p.startswith(self.root):
361 361 c.readpending('00changelog.i.a')
362 362 return c
363 363
364 364 @storecache('00manifest.i')
365 365 def manifest(self):
366 366 return manifest.manifest(self.sopener)
367 367
368 368 @repofilecache('dirstate')
369 369 def dirstate(self):
370 370 warned = [0]
371 371 def validate(node):
372 372 try:
373 373 self.changelog.rev(node)
374 374 return node
375 375 except error.LookupError:
376 376 if not warned[0]:
377 377 warned[0] = True
378 378 self.ui.warn(_("warning: ignoring unknown"
379 379 " working parent %s!\n") % short(node))
380 380 return nullid
381 381
382 382 return dirstate.dirstate(self.opener, self.ui, self.root, validate)
383 383
384 384 def __getitem__(self, changeid):
385 385 if changeid is None:
386 386 return context.workingctx(self)
387 387 return context.changectx(self, changeid)
388 388
389 389 def __contains__(self, changeid):
390 390 try:
391 391 return bool(self.lookup(changeid))
392 392 except error.RepoLookupError:
393 393 return False
394 394
395 395 def __nonzero__(self):
396 396 return True
397 397
398 398 def __len__(self):
399 399 return len(self.changelog)
400 400
401 401 def __iter__(self):
402 402 return iter(self.changelog)
403 403
404 404 def revs(self, expr, *args):
405 405 '''Return a list of revisions matching the given revset'''
406 406 expr = revset.formatspec(expr, *args)
407 407 m = revset.match(None, expr)
408 408 return [r for r in m(self, list(self))]
409 409
410 410 def set(self, expr, *args):
411 411 '''
412 412 Yield a context for each matching revision, after doing arg
413 413 replacement via revset.formatspec
414 414 '''
415 415 for r in self.revs(expr, *args):
416 416 yield self[r]
417 417
418 418 def url(self):
419 419 return 'file:' + self.root
420 420
421 421 def hook(self, name, throw=False, **args):
422 422 return hook.hook(self.ui, self, name, throw, **args)
423 423
424 424 @unfilteredmethod
425 425 def _tag(self, names, node, message, local, user, date, extra={}):
426 426 if isinstance(names, str):
427 427 names = (names,)
428 428
429 429 branches = self.branchmap()
430 430 for name in names:
431 431 self.hook('pretag', throw=True, node=hex(node), tag=name,
432 432 local=local)
433 433 if name in branches:
434 434 self.ui.warn(_("warning: tag %s conflicts with existing"
435 435 " branch name\n") % name)
436 436
437 437 def writetags(fp, names, munge, prevtags):
438 438 fp.seek(0, 2)
439 439 if prevtags and prevtags[-1] != '\n':
440 440 fp.write('\n')
441 441 for name in names:
442 442 m = munge and munge(name) or name
443 443 if (self._tagscache.tagtypes and
444 444 name in self._tagscache.tagtypes):
445 445 old = self.tags().get(name, nullid)
446 446 fp.write('%s %s\n' % (hex(old), m))
447 447 fp.write('%s %s\n' % (hex(node), m))
448 448 fp.close()
449 449
450 450 prevtags = ''
451 451 if local:
452 452 try:
453 453 fp = self.opener('localtags', 'r+')
454 454 except IOError:
455 455 fp = self.opener('localtags', 'a')
456 456 else:
457 457 prevtags = fp.read()
458 458
459 459 # local tags are stored in the current charset
460 460 writetags(fp, names, None, prevtags)
461 461 for name in names:
462 462 self.hook('tag', node=hex(node), tag=name, local=local)
463 463 return
464 464
465 465 try:
466 466 fp = self.wfile('.hgtags', 'rb+')
467 467 except IOError, e:
468 468 if e.errno != errno.ENOENT:
469 469 raise
470 470 fp = self.wfile('.hgtags', 'ab')
471 471 else:
472 472 prevtags = fp.read()
473 473
474 474 # committed tags are stored in UTF-8
475 475 writetags(fp, names, encoding.fromlocal, prevtags)
476 476
477 477 fp.close()
478 478
479 479 self.invalidatecaches()
480 480
481 481 if '.hgtags' not in self.dirstate:
482 482 self[None].add(['.hgtags'])
483 483
484 484 m = matchmod.exact(self.root, '', ['.hgtags'])
485 485 tagnode = self.commit(message, user, date, extra=extra, match=m)
486 486
487 487 for name in names:
488 488 self.hook('tag', node=hex(node), tag=name, local=local)
489 489
490 490 return tagnode
491 491
492 492 def tag(self, names, node, message, local, user, date):
493 493 '''tag a revision with one or more symbolic names.
494 494
495 495 names is a list of strings or, when adding a single tag, names may be a
496 496 string.
497 497
498 498 if local is True, the tags are stored in a per-repository file.
499 499 otherwise, they are stored in the .hgtags file, and a new
500 500 changeset is committed with the change.
501 501
502 502 keyword arguments:
503 503
504 504 local: whether to store tags in non-version-controlled file
505 505 (default False)
506 506
507 507 message: commit message to use if committing
508 508
509 509 user: name of user to use if committing
510 510
511 511 date: date tuple to use if committing'''
512 512
513 513 if not local:
514 514 for x in self.status()[:5]:
515 515 if '.hgtags' in x:
516 516 raise util.Abort(_('working copy of .hgtags is changed '
517 517 '(please commit .hgtags manually)'))
518 518
519 519 self.tags() # instantiate the cache
520 520 self._tag(names, node, message, local, user, date)
521 521
522 522 @filteredpropertycache
523 523 def _tagscache(self):
524 524 '''Returns a tagscache object that contains various tags related
525 525 caches.'''
526 526
527 527 # This simplifies its cache management by having one decorated
528 528 # function (this one) and the rest simply fetch things from it.
529 529 class tagscache(object):
530 530 def __init__(self):
531 531 # These two define the set of tags for this repository. tags
532 532 # maps tag name to node; tagtypes maps tag name to 'global' or
533 533 # 'local'. (Global tags are defined by .hgtags across all
534 534 # heads, and local tags are defined in .hg/localtags.)
535 535 # They constitute the in-memory cache of tags.
536 536 self.tags = self.tagtypes = None
537 537
538 538 self.nodetagscache = self.tagslist = None
539 539
540 540 cache = tagscache()
541 541 cache.tags, cache.tagtypes = self._findtags()
542 542
543 543 return cache
544 544
545 545 def tags(self):
546 546 '''return a mapping of tag to node'''
547 547 t = {}
548 548 if self.changelog.filteredrevs:
549 549 tags, tt = self._findtags()
550 550 else:
551 551 tags = self._tagscache.tags
552 552 for k, v in tags.iteritems():
553 553 try:
554 554 # ignore tags to unknown nodes
555 555 self.changelog.rev(v)
556 556 t[k] = v
557 557 except (error.LookupError, ValueError):
558 558 pass
559 559 return t
560 560
561 561 def _findtags(self):
562 562 '''Do the hard work of finding tags. Return a pair of dicts
563 563 (tags, tagtypes) where tags maps tag name to node, and tagtypes
564 564 maps tag name to a string like \'global\' or \'local\'.
565 565 Subclasses or extensions are free to add their own tags, but
566 566 should be aware that the returned dicts will be retained for the
567 567 duration of the localrepo object.'''
568 568
569 569 # XXX what tagtype should subclasses/extensions use? Currently
570 570 # mq and bookmarks add tags, but do not set the tagtype at all.
571 571 # Should each extension invent its own tag type? Should there
572 572 # be one tagtype for all such "virtual" tags? Or is the status
573 573 # quo fine?
574 574
575 575 alltags = {} # map tag name to (node, hist)
576 576 tagtypes = {}
577 577
578 578 tagsmod.findglobaltags(self.ui, self, alltags, tagtypes)
579 579 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
580 580
581 581 # Build the return dicts. Have to re-encode tag names because
582 582 # the tags module always uses UTF-8 (in order not to lose info
583 583 # writing to the cache), but the rest of Mercurial wants them in
584 584 # local encoding.
585 585 tags = {}
586 586 for (name, (node, hist)) in alltags.iteritems():
587 587 if node != nullid:
588 588 tags[encoding.tolocal(name)] = node
589 589 tags['tip'] = self.changelog.tip()
590 590 tagtypes = dict([(encoding.tolocal(name), value)
591 591 for (name, value) in tagtypes.iteritems()])
592 592 return (tags, tagtypes)
593 593
594 594 def tagtype(self, tagname):
595 595 '''
596 596 return the type of the given tag. result can be:
597 597
598 598 'local' : a local tag
599 599 'global' : a global tag
600 600 None : tag does not exist
601 601 '''
602 602
603 603 return self._tagscache.tagtypes.get(tagname)
604 604
605 605 def tagslist(self):
606 606 '''return a list of tags ordered by revision'''
607 607 if not self._tagscache.tagslist:
608 608 l = []
609 609 for t, n in self.tags().iteritems():
610 610 r = self.changelog.rev(n)
611 611 l.append((r, t, n))
612 612 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
613 613
614 614 return self._tagscache.tagslist
615 615
616 616 def nodetags(self, node):
617 617 '''return the tags associated with a node'''
618 618 if not self._tagscache.nodetagscache:
619 619 nodetagscache = {}
620 620 for t, n in self._tagscache.tags.iteritems():
621 621 nodetagscache.setdefault(n, []).append(t)
622 622 for tags in nodetagscache.itervalues():
623 623 tags.sort()
624 624 self._tagscache.nodetagscache = nodetagscache
625 625 return self._tagscache.nodetagscache.get(node, [])
626 626
627 627 def nodebookmarks(self, node):
628 628 marks = []
629 629 for bookmark, n in self._bookmarks.iteritems():
630 630 if n == node:
631 631 marks.append(bookmark)
632 632 return sorted(marks)
633 633
634 634 def branchmap(self):
635 635 '''returns a dictionary {branch: [branchheads]}'''
636 636 branchmap.updatecache(self)
637 637 return self._branchcaches[self.filtername]
638 638
639 639
640 640 def _branchtip(self, heads):
641 641 '''return the tipmost branch head in heads'''
642 642 tip = heads[-1]
643 643 for h in reversed(heads):
644 644 if not self[h].closesbranch():
645 645 tip = h
646 646 break
647 647 return tip
648 648
649 649 def branchtip(self, branch):
650 650 '''return the tip node for a given branch'''
651 651 if branch not in self.branchmap():
652 652 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
653 653 return self._branchtip(self.branchmap()[branch])
654 654
655 655 def branchtags(self):
656 656 '''return a dict where branch names map to the tipmost head of
657 657 the branch, open heads come before closed'''
658 658 bt = {}
659 659 for bn, heads in self.branchmap().iteritems():
660 660 bt[bn] = self._branchtip(heads)
661 661 return bt
662 662
663 663 def lookup(self, key):
664 664 return self[key].node()
665 665
666 666 def lookupbranch(self, key, remote=None):
667 667 repo = remote or self
668 668 if key in repo.branchmap():
669 669 return key
670 670
671 671 repo = (remote and remote.local()) and remote or self
672 672 return repo[key].branch()
673 673
674 674 def known(self, nodes):
675 675 nm = self.changelog.nodemap
676 676 pc = self._phasecache
677 677 result = []
678 678 for n in nodes:
679 679 r = nm.get(n)
680 680 resp = not (r is None or pc.phase(self, r) >= phases.secret)
681 681 result.append(resp)
682 682 return result
683 683
684 684 def local(self):
685 685 return self
686 686
687 687 def cancopy(self):
688 688 return self.local() # so statichttprepo's override of local() works
689 689
690 690 def join(self, f):
691 691 return os.path.join(self.path, f)
692 692
693 693 def wjoin(self, f):
694 694 return os.path.join(self.root, f)
695 695
696 696 def file(self, f):
697 697 if f[0] == '/':
698 698 f = f[1:]
699 699 return filelog.filelog(self.sopener, f)
700 700
701 701 def changectx(self, changeid):
702 702 return self[changeid]
703 703
704 704 def parents(self, changeid=None):
705 705 '''get list of changectxs for parents of changeid'''
706 706 return self[changeid].parents()
707 707
708 708 def setparents(self, p1, p2=nullid):
709 709 copies = self.dirstate.setparents(p1, p2)
710 710 pctx = self[p1]
711 711 if copies:
712 712 # Adjust copy records, the dirstate cannot do it, it
713 713 # requires access to parents manifests. Preserve them
714 714 # only for entries added to first parent.
715 715 for f in copies:
716 716 if f not in pctx and copies[f] in pctx:
717 717 self.dirstate.copy(copies[f], f)
718 718 if p2 == nullid:
719 719 for f, s in sorted(self.dirstate.copies().items()):
720 720 if f not in pctx and s not in pctx:
721 721 self.dirstate.copy(None, f)
722 722
723 723 def filectx(self, path, changeid=None, fileid=None):
724 724 """changeid can be a changeset revision, node, or tag.
725 725 fileid can be a file revision or node."""
726 726 return context.filectx(self, path, changeid, fileid)
727 727
728 728 def getcwd(self):
729 729 return self.dirstate.getcwd()
730 730
731 731 def pathto(self, f, cwd=None):
732 732 return self.dirstate.pathto(f, cwd)
733 733
734 734 def wfile(self, f, mode='r'):
735 735 return self.wopener(f, mode)
736 736
737 737 def _link(self, f):
738 738 return self.wvfs.islink(f)
739 739
740 740 def _loadfilter(self, filter):
741 741 if filter not in self.filterpats:
742 742 l = []
743 743 for pat, cmd in self.ui.configitems(filter):
744 744 if cmd == '!':
745 745 continue
746 746 mf = matchmod.match(self.root, '', [pat])
747 747 fn = None
748 748 params = cmd
749 749 for name, filterfn in self._datafilters.iteritems():
750 750 if cmd.startswith(name):
751 751 fn = filterfn
752 752 params = cmd[len(name):].lstrip()
753 753 break
754 754 if not fn:
755 755 fn = lambda s, c, **kwargs: util.filter(s, c)
756 756 # Wrap old filters not supporting keyword arguments
757 757 if not inspect.getargspec(fn)[2]:
758 758 oldfn = fn
759 759 fn = lambda s, c, **kwargs: oldfn(s, c)
760 760 l.append((mf, fn, params))
761 761 self.filterpats[filter] = l
762 762 return self.filterpats[filter]
763 763
764 764 def _filter(self, filterpats, filename, data):
765 765 for mf, fn, cmd in filterpats:
766 766 if mf(filename):
767 767 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
768 768 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
769 769 break
770 770
771 771 return data
772 772
773 773 @unfilteredpropertycache
774 774 def _encodefilterpats(self):
775 775 return self._loadfilter('encode')
776 776
777 777 @unfilteredpropertycache
778 778 def _decodefilterpats(self):
779 779 return self._loadfilter('decode')
780 780
781 781 def adddatafilter(self, name, filter):
782 782 self._datafilters[name] = filter
783 783
784 784 def wread(self, filename):
785 785 if self._link(filename):
786 786 data = self.wvfs.readlink(filename)
787 787 else:
788 788 data = self.wopener.read(filename)
789 789 return self._filter(self._encodefilterpats, filename, data)
790 790
791 791 def wwrite(self, filename, data, flags):
792 792 data = self._filter(self._decodefilterpats, filename, data)
793 793 if 'l' in flags:
794 794 self.wopener.symlink(data, filename)
795 795 else:
796 796 self.wopener.write(filename, data)
797 797 if 'x' in flags:
798 798 self.wvfs.setflags(filename, False, True)
799 799
800 800 def wwritedata(self, filename, data):
801 801 return self._filter(self._decodefilterpats, filename, data)
802 802
803 803 def transaction(self, desc):
804 804 tr = self._transref and self._transref() or None
805 805 if tr and tr.running():
806 806 return tr.nest()
807 807
808 808 # abort here if the journal already exists
809 809 if self.svfs.exists("journal"):
810 810 raise error.RepoError(
811 811 _("abandoned transaction found - run hg recover"))
812 812
813 813 self._writejournal(desc)
814 814 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
815 815
816 816 tr = transaction.transaction(self.ui.warn, self.sopener,
817 817 self.sjoin("journal"),
818 818 aftertrans(renames),
819 819 self.store.createmode)
820 820 self._transref = weakref.ref(tr)
821 821 return tr
822 822
823 823 def _journalfiles(self):
824 824 return ((self.svfs, 'journal'),
825 825 (self.vfs, 'journal.dirstate'),
826 826 (self.vfs, 'journal.branch'),
827 827 (self.vfs, 'journal.desc'),
828 828 (self.vfs, 'journal.bookmarks'),
829 829 (self.svfs, 'journal.phaseroots'))
830 830
831 831 def undofiles(self):
832 832 return [vfs.join(undoname(x)) for vfs, x in self._journalfiles()]
833 833
834 834 def _writejournal(self, desc):
835 835 self.opener.write("journal.dirstate",
836 836 self.opener.tryread("dirstate"))
837 837 self.opener.write("journal.branch",
838 838 encoding.fromlocal(self.dirstate.branch()))
839 839 self.opener.write("journal.desc",
840 840 "%d\n%s\n" % (len(self), desc))
841 841 self.opener.write("journal.bookmarks",
842 842 self.opener.tryread("bookmarks"))
843 843 self.sopener.write("journal.phaseroots",
844 844 self.sopener.tryread("phaseroots"))
845 845
846 846 def recover(self):
847 847 lock = self.lock()
848 848 try:
849 849 if self.svfs.exists("journal"):
850 850 self.ui.status(_("rolling back interrupted transaction\n"))
851 851 transaction.rollback(self.sopener, self.sjoin("journal"),
852 852 self.ui.warn)
853 853 self.invalidate()
854 854 return True
855 855 else:
856 856 self.ui.warn(_("no interrupted transaction available\n"))
857 857 return False
858 858 finally:
859 859 lock.release()
860 860
861 861 def rollback(self, dryrun=False, force=False):
862 862 wlock = lock = None
863 863 try:
864 864 wlock = self.wlock()
865 865 lock = self.lock()
866 866 if self.svfs.exists("undo"):
867 867 return self._rollback(dryrun, force)
868 868 else:
869 869 self.ui.warn(_("no rollback information available\n"))
870 870 return 1
871 871 finally:
872 872 release(lock, wlock)
873 873
874 874 @unfilteredmethod # Until we get smarter cache management
875 875 def _rollback(self, dryrun, force):
876 876 ui = self.ui
877 877 try:
878 878 args = self.opener.read('undo.desc').splitlines()
879 879 (oldlen, desc, detail) = (int(args[0]), args[1], None)
880 880 if len(args) >= 3:
881 881 detail = args[2]
882 882 oldtip = oldlen - 1
883 883
884 884 if detail and ui.verbose:
885 885 msg = (_('repository tip rolled back to revision %s'
886 886 ' (undo %s: %s)\n')
887 887 % (oldtip, desc, detail))
888 888 else:
889 889 msg = (_('repository tip rolled back to revision %s'
890 890 ' (undo %s)\n')
891 891 % (oldtip, desc))
892 892 except IOError:
893 893 msg = _('rolling back unknown transaction\n')
894 894 desc = None
895 895
896 896 if not force and self['.'] != self['tip'] and desc == 'commit':
897 897 raise util.Abort(
898 898 _('rollback of last commit while not checked out '
899 899 'may lose data'), hint=_('use -f to force'))
900 900
901 901 ui.status(msg)
902 902 if dryrun:
903 903 return 0
904 904
905 905 parents = self.dirstate.parents()
906 906 self.destroying()
907 907 transaction.rollback(self.sopener, self.sjoin('undo'), ui.warn)
908 908 if self.vfs.exists('undo.bookmarks'):
909 909 self.vfs.rename('undo.bookmarks', 'bookmarks')
910 910 if self.svfs.exists('undo.phaseroots'):
911 911 self.svfs.rename('undo.phaseroots', 'phaseroots')
912 912 self.invalidate()
913 913
914 914 parentgone = (parents[0] not in self.changelog.nodemap or
915 915 parents[1] not in self.changelog.nodemap)
916 916 if parentgone:
917 917 self.vfs.rename('undo.dirstate', 'dirstate')
918 918 try:
919 919 branch = self.opener.read('undo.branch')
920 920 self.dirstate.setbranch(encoding.tolocal(branch))
921 921 except IOError:
922 922 ui.warn(_('named branch could not be reset: '
923 923 'current branch is still \'%s\'\n')
924 924 % self.dirstate.branch())
925 925
926 926 self.dirstate.invalidate()
927 927 parents = tuple([p.rev() for p in self.parents()])
928 928 if len(parents) > 1:
929 929 ui.status(_('working directory now based on '
930 930 'revisions %d and %d\n') % parents)
931 931 else:
932 932 ui.status(_('working directory now based on '
933 933 'revision %d\n') % parents)
934 934 # TODO: if we know which new heads may result from this rollback, pass
935 935 # them to destroy(), which will prevent the branchhead cache from being
936 936 # invalidated.
937 937 self.destroyed()
938 938 return 0
939 939
940 940 def invalidatecaches(self):
941 941
942 942 if '_tagscache' in vars(self):
943 943 # can't use delattr on proxy
944 944 del self.__dict__['_tagscache']
945 945
946 946 self.unfiltered()._branchcaches.clear()
947 947 self.invalidatevolatilesets()
948 948
949 949 def invalidatevolatilesets(self):
950 950 self.filteredrevcache.clear()
951 951 obsolete.clearobscaches(self)
952 952
953 953 def invalidatedirstate(self):
954 954 '''Invalidates the dirstate, causing the next call to dirstate
955 955 to check if it was modified since the last time it was read,
956 956 rereading it if it has.
957 957
958 958 This is different to dirstate.invalidate() that it doesn't always
959 959 rereads the dirstate. Use dirstate.invalidate() if you want to
960 960 explicitly read the dirstate again (i.e. restoring it to a previous
961 961 known good state).'''
962 962 if hasunfilteredcache(self, 'dirstate'):
963 963 for k in self.dirstate._filecache:
964 964 try:
965 965 delattr(self.dirstate, k)
966 966 except AttributeError:
967 967 pass
968 968 delattr(self.unfiltered(), 'dirstate')
969 969
970 970 def invalidate(self):
971 971 unfiltered = self.unfiltered() # all file caches are stored unfiltered
972 972 for k in self._filecache:
973 973 # dirstate is invalidated separately in invalidatedirstate()
974 974 if k == 'dirstate':
975 975 continue
976 976
977 977 try:
978 978 delattr(unfiltered, k)
979 979 except AttributeError:
980 980 pass
981 981 self.invalidatecaches()
982 982
983 983 def _lock(self, lockname, wait, releasefn, acquirefn, desc):
984 984 try:
985 985 l = lock.lock(lockname, 0, releasefn, desc=desc)
986 986 except error.LockHeld, inst:
987 987 if not wait:
988 988 raise
989 989 self.ui.warn(_("waiting for lock on %s held by %r\n") %
990 990 (desc, inst.locker))
991 991 # default to 600 seconds timeout
992 992 l = lock.lock(lockname, int(self.ui.config("ui", "timeout", "600")),
993 993 releasefn, desc=desc)
994 994 if acquirefn:
995 995 acquirefn()
996 996 return l
997 997
998 998 def _afterlock(self, callback):
999 999 """add a callback to the current repository lock.
1000 1000
1001 1001 The callback will be executed on lock release."""
1002 1002 l = self._lockref and self._lockref()
1003 1003 if l:
1004 1004 l.postrelease.append(callback)
1005 1005 else:
1006 1006 callback()
1007 1007
1008 1008 def lock(self, wait=True):
1009 1009 '''Lock the repository store (.hg/store) and return a weak reference
1010 1010 to the lock. Use this before modifying the store (e.g. committing or
1011 1011 stripping). If you are opening a transaction, get a lock as well.)'''
1012 1012 l = self._lockref and self._lockref()
1013 1013 if l is not None and l.held:
1014 1014 l.lock()
1015 1015 return l
1016 1016
1017 1017 def unlock():
1018 1018 self.store.write()
1019 1019 if hasunfilteredcache(self, '_phasecache'):
1020 1020 self._phasecache.write()
1021 1021 for k, ce in self._filecache.items():
1022 1022 if k == 'dirstate' or k not in self.__dict__:
1023 1023 continue
1024 1024 ce.refresh()
1025 1025
1026 1026 l = self._lock(self.sjoin("lock"), wait, unlock,
1027 1027 self.invalidate, _('repository %s') % self.origroot)
1028 1028 self._lockref = weakref.ref(l)
1029 1029 return l
1030 1030
1031 1031 def wlock(self, wait=True):
1032 1032 '''Lock the non-store parts of the repository (everything under
1033 1033 .hg except .hg/store) and return a weak reference to the lock.
1034 1034 Use this before modifying files in .hg.'''
1035 1035 l = self._wlockref and self._wlockref()
1036 1036 if l is not None and l.held:
1037 1037 l.lock()
1038 1038 return l
1039 1039
1040 1040 def unlock():
1041 1041 self.dirstate.write()
1042 1042 self._filecache['dirstate'].refresh()
1043 1043
1044 1044 l = self._lock(self.join("wlock"), wait, unlock,
1045 1045 self.invalidatedirstate, _('working directory of %s') %
1046 1046 self.origroot)
1047 1047 self._wlockref = weakref.ref(l)
1048 1048 return l
1049 1049
1050 1050 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1051 1051 """
1052 1052 commit an individual file as part of a larger transaction
1053 1053 """
1054 1054
1055 1055 fname = fctx.path()
1056 1056 text = fctx.data()
1057 1057 flog = self.file(fname)
1058 1058 fparent1 = manifest1.get(fname, nullid)
1059 1059 fparent2 = fparent2o = manifest2.get(fname, nullid)
1060 1060
1061 1061 meta = {}
1062 1062 copy = fctx.renamed()
1063 1063 if copy and copy[0] != fname:
1064 1064 # Mark the new revision of this file as a copy of another
1065 1065 # file. This copy data will effectively act as a parent
1066 1066 # of this new revision. If this is a merge, the first
1067 1067 # parent will be the nullid (meaning "look up the copy data")
1068 1068 # and the second one will be the other parent. For example:
1069 1069 #
1070 1070 # 0 --- 1 --- 3 rev1 changes file foo
1071 1071 # \ / rev2 renames foo to bar and changes it
1072 1072 # \- 2 -/ rev3 should have bar with all changes and
1073 1073 # should record that bar descends from
1074 1074 # bar in rev2 and foo in rev1
1075 1075 #
1076 1076 # this allows this merge to succeed:
1077 1077 #
1078 1078 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1079 1079 # \ / merging rev3 and rev4 should use bar@rev2
1080 1080 # \- 2 --- 4 as the merge base
1081 1081 #
1082 1082
1083 1083 cfname = copy[0]
1084 1084 crev = manifest1.get(cfname)
1085 1085 newfparent = fparent2
1086 1086
1087 1087 if manifest2: # branch merge
1088 1088 if fparent2 == nullid or crev is None: # copied on remote side
1089 1089 if cfname in manifest2:
1090 1090 crev = manifest2[cfname]
1091 1091 newfparent = fparent1
1092 1092
1093 1093 # find source in nearest ancestor if we've lost track
1094 1094 if not crev:
1095 1095 self.ui.debug(" %s: searching for copy revision for %s\n" %
1096 1096 (fname, cfname))
1097 1097 for ancestor in self[None].ancestors():
1098 1098 if cfname in ancestor:
1099 1099 crev = ancestor[cfname].filenode()
1100 1100 break
1101 1101
1102 1102 if crev:
1103 1103 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1104 1104 meta["copy"] = cfname
1105 1105 meta["copyrev"] = hex(crev)
1106 1106 fparent1, fparent2 = nullid, newfparent
1107 1107 else:
1108 1108 self.ui.warn(_("warning: can't find ancestor for '%s' "
1109 1109 "copied from '%s'!\n") % (fname, cfname))
1110 1110
1111 1111 elif fparent2 != nullid:
1112 1112 # is one parent an ancestor of the other?
1113 1113 fparentancestor = flog.ancestor(fparent1, fparent2)
1114 1114 if fparentancestor == fparent1:
1115 1115 fparent1, fparent2 = fparent2, nullid
1116 1116 elif fparentancestor == fparent2:
1117 1117 fparent2 = nullid
1118 1118
1119 1119 # is the file changed?
1120 1120 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1121 1121 changelist.append(fname)
1122 1122 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1123 1123
1124 1124 # are just the flags changed during merge?
1125 1125 if fparent1 != fparent2o and manifest1.flags(fname) != fctx.flags():
1126 1126 changelist.append(fname)
1127 1127
1128 1128 return fparent1
1129 1129
1130 1130 @unfilteredmethod
1131 1131 def commit(self, text="", user=None, date=None, match=None, force=False,
1132 1132 editor=False, extra={}):
1133 1133 """Add a new revision to current repository.
1134 1134
1135 1135 Revision information is gathered from the working directory,
1136 1136 match can be used to filter the committed files. If editor is
1137 1137 supplied, it is called to get a commit message.
1138 1138 """
1139 1139
1140 1140 def fail(f, msg):
1141 1141 raise util.Abort('%s: %s' % (f, msg))
1142 1142
1143 1143 if not match:
1144 1144 match = matchmod.always(self.root, '')
1145 1145
1146 1146 if not force:
1147 1147 vdirs = []
1148 1148 match.dir = vdirs.append
1149 1149 match.bad = fail
1150 1150
1151 1151 wlock = self.wlock()
1152 1152 try:
1153 1153 wctx = self[None]
1154 1154 merge = len(wctx.parents()) > 1
1155 1155
1156 1156 if (not force and merge and match and
1157 1157 (match.files() or match.anypats())):
1158 1158 raise util.Abort(_('cannot partially commit a merge '
1159 1159 '(do not specify files or patterns)'))
1160 1160
1161 1161 changes = self.status(match=match, clean=force)
1162 1162 if force:
1163 1163 changes[0].extend(changes[6]) # mq may commit unchanged files
1164 1164
1165 1165 # check subrepos
1166 1166 subs = []
1167 1167 commitsubs = set()
1168 1168 newstate = wctx.substate.copy()
1169 1169 # only manage subrepos and .hgsubstate if .hgsub is present
1170 1170 if '.hgsub' in wctx:
1171 1171 # we'll decide whether to track this ourselves, thanks
1172 1172 if '.hgsubstate' in changes[0]:
1173 1173 changes[0].remove('.hgsubstate')
1174 1174 if '.hgsubstate' in changes[2]:
1175 1175 changes[2].remove('.hgsubstate')
1176 1176
1177 1177 # compare current state to last committed state
1178 1178 # build new substate based on last committed state
1179 1179 oldstate = wctx.p1().substate
1180 1180 for s in sorted(newstate.keys()):
1181 1181 if not match(s):
1182 1182 # ignore working copy, use old state if present
1183 1183 if s in oldstate:
1184 1184 newstate[s] = oldstate[s]
1185 1185 continue
1186 1186 if not force:
1187 1187 raise util.Abort(
1188 1188 _("commit with new subrepo %s excluded") % s)
1189 1189 if wctx.sub(s).dirty(True):
1190 1190 if not self.ui.configbool('ui', 'commitsubrepos'):
1191 1191 raise util.Abort(
1192 1192 _("uncommitted changes in subrepo %s") % s,
1193 1193 hint=_("use --subrepos for recursive commit"))
1194 1194 subs.append(s)
1195 1195 commitsubs.add(s)
1196 1196 else:
1197 1197 bs = wctx.sub(s).basestate()
1198 1198 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1199 1199 if oldstate.get(s, (None, None, None))[1] != bs:
1200 1200 subs.append(s)
1201 1201
1202 1202 # check for removed subrepos
1203 1203 for p in wctx.parents():
1204 1204 r = [s for s in p.substate if s not in newstate]
1205 1205 subs += [s for s in r if match(s)]
1206 1206 if subs:
1207 1207 if (not match('.hgsub') and
1208 1208 '.hgsub' in (wctx.modified() + wctx.added())):
1209 1209 raise util.Abort(
1210 1210 _("can't commit subrepos without .hgsub"))
1211 1211 changes[0].insert(0, '.hgsubstate')
1212 1212
1213 1213 elif '.hgsub' in changes[2]:
1214 1214 # clean up .hgsubstate when .hgsub is removed
1215 1215 if ('.hgsubstate' in wctx and
1216 1216 '.hgsubstate' not in changes[0] + changes[1] + changes[2]):
1217 1217 changes[2].insert(0, '.hgsubstate')
1218 1218
1219 1219 # make sure all explicit patterns are matched
1220 1220 if not force and match.files():
1221 1221 matched = set(changes[0] + changes[1] + changes[2])
1222 1222
1223 1223 for f in match.files():
1224 1224 f = self.dirstate.normalize(f)
1225 1225 if f == '.' or f in matched or f in wctx.substate:
1226 1226 continue
1227 1227 if f in changes[3]: # missing
1228 1228 fail(f, _('file not found!'))
1229 1229 if f in vdirs: # visited directory
1230 1230 d = f + '/'
1231 1231 for mf in matched:
1232 1232 if mf.startswith(d):
1233 1233 break
1234 1234 else:
1235 1235 fail(f, _("no match under directory!"))
1236 1236 elif f not in self.dirstate:
1237 1237 fail(f, _("file not tracked!"))
1238 1238
1239 1239 cctx = context.workingctx(self, text, user, date, extra, changes)
1240 1240
1241 1241 if (not force and not extra.get("close") and not merge
1242 1242 and not cctx.files()
1243 1243 and wctx.branch() == wctx.p1().branch()):
1244 1244 return None
1245 1245
1246 1246 if merge and cctx.deleted():
1247 1247 raise util.Abort(_("cannot commit merge with missing files"))
1248 1248
1249 1249 ms = mergemod.mergestate(self)
1250 1250 for f in changes[0]:
1251 1251 if f in ms and ms[f] == 'u':
1252 1252 raise util.Abort(_("unresolved merge conflicts "
1253 1253 "(see hg help resolve)"))
1254 1254
1255 1255 if editor:
1256 1256 cctx._text = editor(self, cctx, subs)
1257 1257 edited = (text != cctx._text)
1258 1258
1259 1259 # commit subs and write new state
1260 1260 if subs:
1261 1261 for s in sorted(commitsubs):
1262 1262 sub = wctx.sub(s)
1263 1263 self.ui.status(_('committing subrepository %s\n') %
1264 1264 subrepo.subrelpath(sub))
1265 1265 sr = sub.commit(cctx._text, user, date)
1266 1266 newstate[s] = (newstate[s][0], sr)
1267 1267 subrepo.writestate(self, newstate)
1268 1268
1269 1269 # Save commit message in case this transaction gets rolled back
1270 1270 # (e.g. by a pretxncommit hook). Leave the content alone on
1271 1271 # the assumption that the user will use the same editor again.
1272 1272 msgfn = self.savecommitmessage(cctx._text)
1273 1273
1274 1274 p1, p2 = self.dirstate.parents()
1275 1275 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1276 1276 try:
1277 1277 self.hook("precommit", throw=True, parent1=hookp1,
1278 1278 parent2=hookp2)
1279 1279 ret = self.commitctx(cctx, True)
1280 1280 except: # re-raises
1281 1281 if edited:
1282 1282 self.ui.write(
1283 1283 _('note: commit message saved in %s\n') % msgfn)
1284 1284 raise
1285 1285
1286 1286 # update bookmarks, dirstate and mergestate
1287 1287 bookmarks.update(self, [p1, p2], ret)
1288 1288 cctx.markcommitted(ret)
1289 1289 ms.reset()
1290 1290 finally:
1291 1291 wlock.release()
1292 1292
1293 1293 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1294 1294 self.hook("commit", node=node, parent1=parent1, parent2=parent2)
1295 1295 self._afterlock(commithook)
1296 1296 return ret
1297 1297
1298 1298 @unfilteredmethod
1299 1299 def commitctx(self, ctx, error=False):
1300 1300 """Add a new revision to current repository.
1301 1301 Revision information is passed via the context argument.
1302 1302 """
1303 1303
1304 1304 tr = lock = None
1305 1305 removed = list(ctx.removed())
1306 1306 p1, p2 = ctx.p1(), ctx.p2()
1307 1307 user = ctx.user()
1308 1308
1309 1309 lock = self.lock()
1310 1310 try:
1311 1311 tr = self.transaction("commit")
1312 1312 trp = weakref.proxy(tr)
1313 1313
1314 1314 if ctx.files():
1315 1315 m1 = p1.manifest().copy()
1316 1316 m2 = p2.manifest()
1317 1317
1318 1318 # check in files
1319 1319 new = {}
1320 1320 changed = []
1321 1321 linkrev = len(self)
1322 1322 for f in sorted(ctx.modified() + ctx.added()):
1323 1323 self.ui.note(f + "\n")
1324 1324 try:
1325 1325 fctx = ctx[f]
1326 1326 new[f] = self._filecommit(fctx, m1, m2, linkrev, trp,
1327 1327 changed)
1328 1328 m1.set(f, fctx.flags())
1329 1329 except OSError, inst:
1330 1330 self.ui.warn(_("trouble committing %s!\n") % f)
1331 1331 raise
1332 1332 except IOError, inst:
1333 1333 errcode = getattr(inst, 'errno', errno.ENOENT)
1334 1334 if error or errcode and errcode != errno.ENOENT:
1335 1335 self.ui.warn(_("trouble committing %s!\n") % f)
1336 1336 raise
1337 1337 else:
1338 1338 removed.append(f)
1339 1339
1340 1340 # update manifest
1341 1341 m1.update(new)
1342 1342 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1343 1343 drop = [f for f in removed if f in m1]
1344 1344 for f in drop:
1345 1345 del m1[f]
1346 1346 mn = self.manifest.add(m1, trp, linkrev, p1.manifestnode(),
1347 1347 p2.manifestnode(), (new, drop))
1348 1348 files = changed + removed
1349 1349 else:
1350 1350 mn = p1.manifestnode()
1351 1351 files = []
1352 1352
1353 1353 # update changelog
1354 1354 self.changelog.delayupdate()
1355 1355 n = self.changelog.add(mn, files, ctx.description(),
1356 1356 trp, p1.node(), p2.node(),
1357 1357 user, ctx.date(), ctx.extra().copy())
1358 1358 p = lambda: self.changelog.writepending() and self.root or ""
1359 1359 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1360 1360 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1361 1361 parent2=xp2, pending=p)
1362 1362 self.changelog.finalize(trp)
1363 1363 # set the new commit is proper phase
1364 1364 targetphase = phases.newcommitphase(self.ui)
1365 1365 if targetphase:
1366 1366 # retract boundary do not alter parent changeset.
1367 1367 # if a parent have higher the resulting phase will
1368 1368 # be compliant anyway
1369 1369 #
1370 1370 # if minimal phase was 0 we don't need to retract anything
1371 1371 phases.retractboundary(self, targetphase, [n])
1372 1372 tr.close()
1373 1373 branchmap.updatecache(self.filtered('served'))
1374 1374 return n
1375 1375 finally:
1376 1376 if tr:
1377 1377 tr.release()
1378 1378 lock.release()
1379 1379
1380 1380 @unfilteredmethod
1381 1381 def destroying(self):
1382 1382 '''Inform the repository that nodes are about to be destroyed.
1383 1383 Intended for use by strip and rollback, so there's a common
1384 1384 place for anything that has to be done before destroying history.
1385 1385
1386 1386 This is mostly useful for saving state that is in memory and waiting
1387 1387 to be flushed when the current lock is released. Because a call to
1388 1388 destroyed is imminent, the repo will be invalidated causing those
1389 1389 changes to stay in memory (waiting for the next unlock), or vanish
1390 1390 completely.
1391 1391 '''
1392 1392 # When using the same lock to commit and strip, the phasecache is left
1393 1393 # dirty after committing. Then when we strip, the repo is invalidated,
1394 1394 # causing those changes to disappear.
1395 1395 if '_phasecache' in vars(self):
1396 1396 self._phasecache.write()
1397 1397
1398 1398 @unfilteredmethod
1399 1399 def destroyed(self):
1400 1400 '''Inform the repository that nodes have been destroyed.
1401 1401 Intended for use by strip and rollback, so there's a common
1402 1402 place for anything that has to be done after destroying history.
1403 1403 '''
1404 1404 # When one tries to:
1405 1405 # 1) destroy nodes thus calling this method (e.g. strip)
1406 1406 # 2) use phasecache somewhere (e.g. commit)
1407 1407 #
1408 1408 # then 2) will fail because the phasecache contains nodes that were
1409 1409 # removed. We can either remove phasecache from the filecache,
1410 1410 # causing it to reload next time it is accessed, or simply filter
1411 1411 # the removed nodes now and write the updated cache.
1412 1412 self._phasecache.filterunknown(self)
1413 1413 self._phasecache.write()
1414 1414
1415 1415 # update the 'served' branch cache to help read only server process
1416 1416 # Thanks to branchcache collaboration this is done from the nearest
1417 1417 # filtered subset and it is expected to be fast.
1418 1418 branchmap.updatecache(self.filtered('served'))
1419 1419
1420 1420 # Ensure the persistent tag cache is updated. Doing it now
1421 1421 # means that the tag cache only has to worry about destroyed
1422 1422 # heads immediately after a strip/rollback. That in turn
1423 1423 # guarantees that "cachetip == currenttip" (comparing both rev
1424 1424 # and node) always means no nodes have been added or destroyed.
1425 1425
1426 1426 # XXX this is suboptimal when qrefresh'ing: we strip the current
1427 1427 # head, refresh the tag cache, then immediately add a new head.
1428 1428 # But I think doing it this way is necessary for the "instant
1429 1429 # tag cache retrieval" case to work.
1430 1430 self.invalidate()
1431 1431
1432 1432 def walk(self, match, node=None):
1433 1433 '''
1434 1434 walk recursively through the directory tree or a given
1435 1435 changeset, finding all files matched by the match
1436 1436 function
1437 1437 '''
1438 1438 return self[node].walk(match)
1439 1439
1440 1440 def status(self, node1='.', node2=None, match=None,
1441 1441 ignored=False, clean=False, unknown=False,
1442 1442 listsubrepos=False):
1443 1443 """return status of files between two nodes or node and working
1444 1444 directory.
1445 1445
1446 1446 If node1 is None, use the first dirstate parent instead.
1447 1447 If node2 is None, compare node1 with working directory.
1448 1448 """
1449 1449
1450 1450 def mfmatches(ctx):
1451 1451 mf = ctx.manifest().copy()
1452 1452 if match.always():
1453 1453 return mf
1454 1454 for fn in mf.keys():
1455 1455 if not match(fn):
1456 1456 del mf[fn]
1457 1457 return mf
1458 1458
1459 1459 if isinstance(node1, context.changectx):
1460 1460 ctx1 = node1
1461 1461 else:
1462 1462 ctx1 = self[node1]
1463 1463 if isinstance(node2, context.changectx):
1464 1464 ctx2 = node2
1465 1465 else:
1466 1466 ctx2 = self[node2]
1467 1467
1468 1468 working = ctx2.rev() is None
1469 1469 parentworking = working and ctx1 == self['.']
1470 1470 match = match or matchmod.always(self.root, self.getcwd())
1471 1471 listignored, listclean, listunknown = ignored, clean, unknown
1472 1472
1473 1473 # load earliest manifest first for caching reasons
1474 1474 if not working and ctx2.rev() < ctx1.rev():
1475 1475 ctx2.manifest()
1476 1476
1477 1477 if not parentworking:
1478 1478 def bad(f, msg):
1479 1479 # 'f' may be a directory pattern from 'match.files()',
1480 1480 # so 'f not in ctx1' is not enough
1481 1481 if f not in ctx1 and f not in ctx1.dirs():
1482 1482 self.ui.warn('%s: %s\n' % (self.dirstate.pathto(f), msg))
1483 1483 match.bad = bad
1484 1484
1485 1485 if working: # we need to scan the working dir
1486 1486 subrepos = []
1487 1487 if '.hgsub' in self.dirstate:
1488 1488 subrepos = sorted(ctx2.substate)
1489 1489 s = self.dirstate.status(match, subrepos, listignored,
1490 1490 listclean, listunknown)
1491 1491 cmp, modified, added, removed, deleted, unknown, ignored, clean = s
1492 1492
1493 1493 # check for any possibly clean files
1494 1494 if parentworking and cmp:
1495 1495 fixup = []
1496 1496 # do a full compare of any files that might have changed
1497 1497 for f in sorted(cmp):
1498 1498 if (f not in ctx1 or ctx2.flags(f) != ctx1.flags(f)
1499 1499 or ctx1[f].cmp(ctx2[f])):
1500 1500 modified.append(f)
1501 1501 else:
1502 1502 fixup.append(f)
1503 1503
1504 1504 # update dirstate for files that are actually clean
1505 1505 if fixup:
1506 1506 if listclean:
1507 1507 clean += fixup
1508 1508
1509 1509 try:
1510 1510 # updating the dirstate is optional
1511 1511 # so we don't wait on the lock
1512 1512 wlock = self.wlock(False)
1513 1513 try:
1514 1514 for f in fixup:
1515 1515 self.dirstate.normal(f)
1516 1516 finally:
1517 1517 wlock.release()
1518 1518 except error.LockError:
1519 1519 pass
1520 1520
1521 1521 if not parentworking:
1522 1522 mf1 = mfmatches(ctx1)
1523 1523 if working:
1524 1524 # we are comparing working dir against non-parent
1525 1525 # generate a pseudo-manifest for the working dir
1526 1526 mf2 = mfmatches(self['.'])
1527 1527 for f in cmp + modified + added:
1528 1528 mf2[f] = None
1529 1529 mf2.set(f, ctx2.flags(f))
1530 1530 for f in removed:
1531 1531 if f in mf2:
1532 1532 del mf2[f]
1533 1533 else:
1534 1534 # we are comparing two revisions
1535 1535 deleted, unknown, ignored = [], [], []
1536 1536 mf2 = mfmatches(ctx2)
1537 1537
1538 1538 modified, added, clean = [], [], []
1539 1539 withflags = mf1.withflags() | mf2.withflags()
1540 1540 for fn, mf2node in mf2.iteritems():
1541 1541 if fn in mf1:
1542 1542 if (fn not in deleted and
1543 1543 ((fn in withflags and mf1.flags(fn) != mf2.flags(fn)) or
1544 1544 (mf1[fn] != mf2node and
1545 1545 (mf2node or ctx1[fn].cmp(ctx2[fn]))))):
1546 1546 modified.append(fn)
1547 1547 elif listclean:
1548 1548 clean.append(fn)
1549 1549 del mf1[fn]
1550 1550 elif fn not in deleted:
1551 1551 added.append(fn)
1552 1552 removed = mf1.keys()
1553 1553
1554 1554 if working and modified and not self.dirstate._checklink:
1555 1555 # Symlink placeholders may get non-symlink-like contents
1556 1556 # via user error or dereferencing by NFS or Samba servers,
1557 1557 # so we filter out any placeholders that don't look like a
1558 1558 # symlink
1559 1559 sane = []
1560 1560 for f in modified:
1561 1561 if ctx2.flags(f) == 'l':
1562 1562 d = ctx2[f].data()
1563 1563 if len(d) >= 1024 or '\n' in d or util.binary(d):
1564 1564 self.ui.debug('ignoring suspect symlink placeholder'
1565 1565 ' "%s"\n' % f)
1566 1566 continue
1567 1567 sane.append(f)
1568 1568 modified = sane
1569 1569
1570 1570 r = modified, added, removed, deleted, unknown, ignored, clean
1571 1571
1572 1572 if listsubrepos:
1573 1573 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
1574 1574 if working:
1575 1575 rev2 = None
1576 1576 else:
1577 1577 rev2 = ctx2.substate[subpath][1]
1578 1578 try:
1579 1579 submatch = matchmod.narrowmatcher(subpath, match)
1580 1580 s = sub.status(rev2, match=submatch, ignored=listignored,
1581 1581 clean=listclean, unknown=listunknown,
1582 1582 listsubrepos=True)
1583 1583 for rfiles, sfiles in zip(r, s):
1584 1584 rfiles.extend("%s/%s" % (subpath, f) for f in sfiles)
1585 1585 except error.LookupError:
1586 1586 self.ui.status(_("skipping missing subrepository: %s\n")
1587 1587 % subpath)
1588 1588
1589 1589 for l in r:
1590 1590 l.sort()
1591 1591 return r
1592 1592
1593 1593 def heads(self, start=None):
1594 1594 heads = self.changelog.heads(start)
1595 1595 # sort the output in rev descending order
1596 1596 return sorted(heads, key=self.changelog.rev, reverse=True)
1597 1597
1598 1598 def branchheads(self, branch=None, start=None, closed=False):
1599 1599 '''return a (possibly filtered) list of heads for the given branch
1600 1600
1601 1601 Heads are returned in topological order, from newest to oldest.
1602 1602 If branch is None, use the dirstate branch.
1603 1603 If start is not None, return only heads reachable from start.
1604 1604 If closed is True, return heads that are marked as closed as well.
1605 1605 '''
1606 1606 if branch is None:
1607 1607 branch = self[None].branch()
1608 1608 branches = self.branchmap()
1609 1609 if branch not in branches:
1610 1610 return []
1611 1611 # the cache returns heads ordered lowest to highest
1612 1612 bheads = list(reversed(branches[branch]))
1613 1613 if start is not None:
1614 1614 # filter out the heads that cannot be reached from startrev
1615 1615 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
1616 1616 bheads = [h for h in bheads if h in fbheads]
1617 1617 if not closed:
1618 1618 bheads = [h for h in bheads if not self[h].closesbranch()]
1619 1619 return bheads
1620 1620
1621 1621 def branches(self, nodes):
1622 1622 if not nodes:
1623 1623 nodes = [self.changelog.tip()]
1624 1624 b = []
1625 1625 for n in nodes:
1626 1626 t = n
1627 1627 while True:
1628 1628 p = self.changelog.parents(n)
1629 1629 if p[1] != nullid or p[0] == nullid:
1630 1630 b.append((t, n, p[0], p[1]))
1631 1631 break
1632 1632 n = p[0]
1633 1633 return b
1634 1634
1635 1635 def between(self, pairs):
1636 1636 r = []
1637 1637
1638 1638 for top, bottom in pairs:
1639 1639 n, l, i = top, [], 0
1640 1640 f = 1
1641 1641
1642 1642 while n != bottom and n != nullid:
1643 1643 p = self.changelog.parents(n)[0]
1644 1644 if i == f:
1645 1645 l.append(n)
1646 1646 f = f * 2
1647 1647 n = p
1648 1648 i += 1
1649 1649
1650 1650 r.append(l)
1651 1651
1652 1652 return r
1653 1653
1654 1654 def pull(self, remote, heads=None, force=False):
1655 1655 # don't open transaction for nothing or you break future useful
1656 1656 # rollback call
1657 1657 tr = None
1658 1658 trname = 'pull\n' + util.hidepassword(remote.url())
1659 1659 lock = self.lock()
1660 1660 try:
1661 1661 tmp = discovery.findcommonincoming(self, remote, heads=heads,
1662 1662 force=force)
1663 1663 common, fetch, rheads = tmp
1664 1664 if not fetch:
1665 1665 self.ui.status(_("no changes found\n"))
1666 1666 added = []
1667 1667 result = 0
1668 1668 else:
1669 1669 tr = self.transaction(trname)
1670 1670 if heads is None and list(common) == [nullid]:
1671 1671 self.ui.status(_("requesting all changes\n"))
1672 1672 elif heads is None and remote.capable('changegroupsubset'):
1673 1673 # issue1320, avoid a race if remote changed after discovery
1674 1674 heads = rheads
1675 1675
1676 1676 if remote.capable('getbundle'):
1677 1677 cg = remote.getbundle('pull', common=common,
1678 1678 heads=heads or rheads)
1679 1679 elif heads is None:
1680 1680 cg = remote.changegroup(fetch, 'pull')
1681 1681 elif not remote.capable('changegroupsubset'):
1682 1682 raise util.Abort(_("partial pull cannot be done because "
1683 1683 "other repository doesn't support "
1684 1684 "changegroupsubset."))
1685 1685 else:
1686 1686 cg = remote.changegroupsubset(fetch, heads, 'pull')
1687 1687 # we use unfiltered changelog here because hidden revision must
1688 1688 # be taken in account for phase synchronization. They may
1689 1689 # becomes public and becomes visible again.
1690 1690 cl = self.unfiltered().changelog
1691 1691 clstart = len(cl)
1692 1692 result = self.addchangegroup(cg, 'pull', remote.url())
1693 1693 clend = len(cl)
1694 1694 added = [cl.node(r) for r in xrange(clstart, clend)]
1695 1695
1696 1696 # compute target subset
1697 1697 if heads is None:
1698 1698 # We pulled every thing possible
1699 1699 # sync on everything common
1700 1700 subset = common + added
1701 1701 else:
1702 1702 # We pulled a specific subset
1703 1703 # sync on this subset
1704 1704 subset = heads
1705 1705
1706 1706 # Get remote phases data from remote
1707 1707 remotephases = remote.listkeys('phases')
1708 1708 publishing = bool(remotephases.get('publishing', False))
1709 1709 if remotephases and not publishing:
1710 1710 # remote is new and unpublishing
1711 1711 pheads, _dr = phases.analyzeremotephases(self, subset,
1712 1712 remotephases)
1713 1713 phases.advanceboundary(self, phases.public, pheads)
1714 1714 phases.advanceboundary(self, phases.draft, subset)
1715 1715 else:
1716 1716 # Remote is old or publishing all common changesets
1717 1717 # should be seen as public
1718 1718 phases.advanceboundary(self, phases.public, subset)
1719 1719
1720 if obsolete._enabled:
1721 self.ui.debug('fetching remote obsolete markers\n')
1722 remoteobs = remote.listkeys('obsolete')
1723 if 'dump0' in remoteobs:
1720 def gettransaction():
1724 1721 if tr is None:
1725 tr = self.transaction(trname)
1726 for key in sorted(remoteobs, reverse=True):
1727 if key.startswith('dump'):
1728 data = base85.b85decode(remoteobs[key])
1729 self.obsstore.mergemarkers(tr, data)
1730 self.invalidatevolatilesets()
1722 return self.transaction(trname)
1723 return tr
1724
1725 obstr = obsolete.syncpull(self, remote, gettransaction)
1726 if obstr is not None:
1727 tr = obstr
1728
1731 1729 if tr is not None:
1732 1730 tr.close()
1733 1731 finally:
1734 1732 if tr is not None:
1735 1733 tr.release()
1736 1734 lock.release()
1737 1735
1738 1736 return result
1739 1737
1740 1738 def checkpush(self, force, revs):
1741 1739 """Extensions can override this function if additional checks have
1742 1740 to be performed before pushing, or call it if they override push
1743 1741 command.
1744 1742 """
1745 1743 pass
1746 1744
1747 1745 def push(self, remote, force=False, revs=None, newbranch=False):
1748 1746 '''Push outgoing changesets (limited by revs) from the current
1749 1747 repository to remote. Return an integer:
1750 1748 - None means nothing to push
1751 1749 - 0 means HTTP error
1752 1750 - 1 means we pushed and remote head count is unchanged *or*
1753 1751 we have outgoing changesets but refused to push
1754 1752 - other values as described by addchangegroup()
1755 1753 '''
1756 1754 # there are two ways to push to remote repo:
1757 1755 #
1758 1756 # addchangegroup assumes local user can lock remote
1759 1757 # repo (local filesystem, old ssh servers).
1760 1758 #
1761 1759 # unbundle assumes local user cannot lock remote repo (new ssh
1762 1760 # servers, http servers).
1763 1761
1764 1762 if not remote.canpush():
1765 1763 raise util.Abort(_("destination does not support push"))
1766 1764 unfi = self.unfiltered()
1767 1765 # get local lock as we might write phase data
1768 1766 locallock = self.lock()
1769 1767 try:
1770 1768 self.checkpush(force, revs)
1771 1769 lock = None
1772 1770 unbundle = remote.capable('unbundle')
1773 1771 if not unbundle:
1774 1772 lock = remote.lock()
1775 1773 try:
1776 1774 # discovery
1777 1775 fci = discovery.findcommonincoming
1778 1776 commoninc = fci(unfi, remote, force=force)
1779 1777 common, inc, remoteheads = commoninc
1780 1778 fco = discovery.findcommonoutgoing
1781 1779 outgoing = fco(unfi, remote, onlyheads=revs,
1782 1780 commoninc=commoninc, force=force)
1783 1781
1784 1782
1785 1783 if not outgoing.missing:
1786 1784 # nothing to push
1787 1785 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
1788 1786 ret = None
1789 1787 else:
1790 1788 # something to push
1791 1789 if not force:
1792 1790 # if self.obsstore == False --> no obsolete
1793 1791 # then, save the iteration
1794 1792 if unfi.obsstore:
1795 1793 # this message are here for 80 char limit reason
1796 1794 mso = _("push includes obsolete changeset: %s!")
1797 1795 mst = "push includes %s changeset: %s!"
1798 1796 # plain versions for i18n tool to detect them
1799 1797 _("push includes unstable changeset: %s!")
1800 1798 _("push includes bumped changeset: %s!")
1801 1799 _("push includes divergent changeset: %s!")
1802 1800 # If we are to push if there is at least one
1803 1801 # obsolete or unstable changeset in missing, at
1804 1802 # least one of the missinghead will be obsolete or
1805 1803 # unstable. So checking heads only is ok
1806 1804 for node in outgoing.missingheads:
1807 1805 ctx = unfi[node]
1808 1806 if ctx.obsolete():
1809 1807 raise util.Abort(mso % ctx)
1810 1808 elif ctx.troubled():
1811 1809 raise util.Abort(_(mst)
1812 1810 % (ctx.troubles()[0],
1813 1811 ctx))
1814 1812 discovery.checkheads(unfi, remote, outgoing,
1815 1813 remoteheads, newbranch,
1816 1814 bool(inc))
1817 1815
1818 1816 # create a changegroup from local
1819 1817 if revs is None and not outgoing.excluded:
1820 1818 # push everything,
1821 1819 # use the fast path, no race possible on push
1822 1820 cg = self._changegroup(outgoing.missing, 'push')
1823 1821 else:
1824 1822 cg = self.getlocalbundle('push', outgoing)
1825 1823
1826 1824 # apply changegroup to remote
1827 1825 if unbundle:
1828 1826 # local repo finds heads on server, finds out what
1829 1827 # revs it must push. once revs transferred, if server
1830 1828 # finds it has different heads (someone else won
1831 1829 # commit/push race), server aborts.
1832 1830 if force:
1833 1831 remoteheads = ['force']
1834 1832 # ssh: return remote's addchangegroup()
1835 1833 # http: return remote's addchangegroup() or 0 for error
1836 1834 ret = remote.unbundle(cg, remoteheads, 'push')
1837 1835 else:
1838 1836 # we return an integer indicating remote head count
1839 1837 # change
1840 1838 ret = remote.addchangegroup(cg, 'push', self.url())
1841 1839
1842 1840 if ret:
1843 1841 # push succeed, synchronize target of the push
1844 1842 cheads = outgoing.missingheads
1845 1843 elif revs is None:
1846 1844 # All out push fails. synchronize all common
1847 1845 cheads = outgoing.commonheads
1848 1846 else:
1849 1847 # I want cheads = heads(::missingheads and ::commonheads)
1850 1848 # (missingheads is revs with secret changeset filtered out)
1851 1849 #
1852 1850 # This can be expressed as:
1853 1851 # cheads = ( (missingheads and ::commonheads)
1854 1852 # + (commonheads and ::missingheads))"
1855 1853 # )
1856 1854 #
1857 1855 # while trying to push we already computed the following:
1858 1856 # common = (::commonheads)
1859 1857 # missing = ((commonheads::missingheads) - commonheads)
1860 1858 #
1861 1859 # We can pick:
1862 1860 # * missingheads part of common (::commonheads)
1863 1861 common = set(outgoing.common)
1864 1862 cheads = [node for node in revs if node in common]
1865 1863 # and
1866 1864 # * commonheads parents on missing
1867 1865 revset = unfi.set('%ln and parents(roots(%ln))',
1868 1866 outgoing.commonheads,
1869 1867 outgoing.missing)
1870 1868 cheads.extend(c.node() for c in revset)
1871 1869 # even when we don't push, exchanging phase data is useful
1872 1870 remotephases = remote.listkeys('phases')
1873 1871 if (self.ui.configbool('ui', '_usedassubrepo', False)
1874 1872 and remotephases # server supports phases
1875 1873 and ret is None # nothing was pushed
1876 1874 and remotephases.get('publishing', False)):
1877 1875 # When:
1878 1876 # - this is a subrepo push
1879 1877 # - and remote support phase
1880 1878 # - and no changeset was pushed
1881 1879 # - and remote is publishing
1882 1880 # We may be in issue 3871 case!
1883 1881 # We drop the possible phase synchronisation done by
1884 1882 # courtesy to publish changesets possibly locally draft
1885 1883 # on the remote.
1886 1884 remotephases = {'publishing': 'True'}
1887 1885 if not remotephases: # old server or public only repo
1888 1886 phases.advanceboundary(self, phases.public, cheads)
1889 1887 # don't push any phase data as there is nothing to push
1890 1888 else:
1891 1889 ana = phases.analyzeremotephases(self, cheads, remotephases)
1892 1890 pheads, droots = ana
1893 1891 ### Apply remote phase on local
1894 1892 if remotephases.get('publishing', False):
1895 1893 phases.advanceboundary(self, phases.public, cheads)
1896 1894 else: # publish = False
1897 1895 phases.advanceboundary(self, phases.public, pheads)
1898 1896 phases.advanceboundary(self, phases.draft, cheads)
1899 1897 ### Apply local phase on remote
1900 1898
1901 1899 # Get the list of all revs draft on remote by public here.
1902 1900 # XXX Beware that revset break if droots is not strictly
1903 1901 # XXX root we may want to ensure it is but it is costly
1904 1902 outdated = unfi.set('heads((%ln::%ln) and public())',
1905 1903 droots, cheads)
1906 1904 for newremotehead in outdated:
1907 1905 r = remote.pushkey('phases',
1908 1906 newremotehead.hex(),
1909 1907 str(phases.draft),
1910 1908 str(phases.public))
1911 1909 if not r:
1912 1910 self.ui.warn(_('updating %s to public failed!\n')
1913 1911 % newremotehead)
1914 1912 self.ui.debug('try to push obsolete markers to remote\n')
1915 1913 obsolete.syncpush(self, remote)
1916 1914 finally:
1917 1915 if lock is not None:
1918 1916 lock.release()
1919 1917 finally:
1920 1918 locallock.release()
1921 1919
1922 1920 self.ui.debug("checking for updated bookmarks\n")
1923 1921 rb = remote.listkeys('bookmarks')
1924 1922 for k in rb.keys():
1925 1923 if k in unfi._bookmarks:
1926 1924 nr, nl = rb[k], hex(self._bookmarks[k])
1927 1925 if nr in unfi:
1928 1926 cr = unfi[nr]
1929 1927 cl = unfi[nl]
1930 1928 if bookmarks.validdest(unfi, cr, cl):
1931 1929 r = remote.pushkey('bookmarks', k, nr, nl)
1932 1930 if r:
1933 1931 self.ui.status(_("updating bookmark %s\n") % k)
1934 1932 else:
1935 1933 self.ui.warn(_('updating bookmark %s'
1936 1934 ' failed!\n') % k)
1937 1935
1938 1936 return ret
1939 1937
1940 1938 def changegroupinfo(self, nodes, source):
1941 1939 if self.ui.verbose or source == 'bundle':
1942 1940 self.ui.status(_("%d changesets found\n") % len(nodes))
1943 1941 if self.ui.debugflag:
1944 1942 self.ui.debug("list of changesets:\n")
1945 1943 for node in nodes:
1946 1944 self.ui.debug("%s\n" % hex(node))
1947 1945
1948 1946 def changegroupsubset(self, bases, heads, source):
1949 1947 """Compute a changegroup consisting of all the nodes that are
1950 1948 descendants of any of the bases and ancestors of any of the heads.
1951 1949 Return a chunkbuffer object whose read() method will return
1952 1950 successive changegroup chunks.
1953 1951
1954 1952 It is fairly complex as determining which filenodes and which
1955 1953 manifest nodes need to be included for the changeset to be complete
1956 1954 is non-trivial.
1957 1955
1958 1956 Another wrinkle is doing the reverse, figuring out which changeset in
1959 1957 the changegroup a particular filenode or manifestnode belongs to.
1960 1958 """
1961 1959 cl = self.changelog
1962 1960 if not bases:
1963 1961 bases = [nullid]
1964 1962 csets, bases, heads = cl.nodesbetween(bases, heads)
1965 1963 # We assume that all ancestors of bases are known
1966 1964 common = cl.ancestors([cl.rev(n) for n in bases])
1967 1965 return self._changegroupsubset(common, csets, heads, source)
1968 1966
1969 1967 def getlocalbundle(self, source, outgoing):
1970 1968 """Like getbundle, but taking a discovery.outgoing as an argument.
1971 1969
1972 1970 This is only implemented for local repos and reuses potentially
1973 1971 precomputed sets in outgoing."""
1974 1972 if not outgoing.missing:
1975 1973 return None
1976 1974 return self._changegroupsubset(outgoing.common,
1977 1975 outgoing.missing,
1978 1976 outgoing.missingheads,
1979 1977 source)
1980 1978
1981 1979 def getbundle(self, source, heads=None, common=None):
1982 1980 """Like changegroupsubset, but returns the set difference between the
1983 1981 ancestors of heads and the ancestors common.
1984 1982
1985 1983 If heads is None, use the local heads. If common is None, use [nullid].
1986 1984
1987 1985 The nodes in common might not all be known locally due to the way the
1988 1986 current discovery protocol works.
1989 1987 """
1990 1988 cl = self.changelog
1991 1989 if common:
1992 1990 hasnode = cl.hasnode
1993 1991 common = [n for n in common if hasnode(n)]
1994 1992 else:
1995 1993 common = [nullid]
1996 1994 if not heads:
1997 1995 heads = cl.heads()
1998 1996 return self.getlocalbundle(source,
1999 1997 discovery.outgoing(cl, common, heads))
2000 1998
2001 1999 @unfilteredmethod
2002 2000 def _changegroupsubset(self, commonrevs, csets, heads, source):
2003 2001
2004 2002 cl = self.changelog
2005 2003 mf = self.manifest
2006 2004 mfs = {} # needed manifests
2007 2005 fnodes = {} # needed file nodes
2008 2006 changedfiles = set()
2009 2007 fstate = ['', {}]
2010 2008 count = [0, 0]
2011 2009
2012 2010 # can we go through the fast path ?
2013 2011 heads.sort()
2014 2012 if heads == sorted(self.heads()):
2015 2013 return self._changegroup(csets, source)
2016 2014
2017 2015 # slow path
2018 2016 self.hook('preoutgoing', throw=True, source=source)
2019 2017 self.changegroupinfo(csets, source)
2020 2018
2021 2019 # filter any nodes that claim to be part of the known set
2022 2020 def prune(revlog, missing):
2023 2021 rr, rl = revlog.rev, revlog.linkrev
2024 2022 return [n for n in missing
2025 2023 if rl(rr(n)) not in commonrevs]
2026 2024
2027 2025 progress = self.ui.progress
2028 2026 _bundling = _('bundling')
2029 2027 _changesets = _('changesets')
2030 2028 _manifests = _('manifests')
2031 2029 _files = _('files')
2032 2030
2033 2031 def lookup(revlog, x):
2034 2032 if revlog == cl:
2035 2033 c = cl.read(x)
2036 2034 changedfiles.update(c[3])
2037 2035 mfs.setdefault(c[0], x)
2038 2036 count[0] += 1
2039 2037 progress(_bundling, count[0],
2040 2038 unit=_changesets, total=count[1])
2041 2039 return x
2042 2040 elif revlog == mf:
2043 2041 clnode = mfs[x]
2044 2042 mdata = mf.readfast(x)
2045 2043 for f, n in mdata.iteritems():
2046 2044 if f in changedfiles:
2047 2045 fnodes[f].setdefault(n, clnode)
2048 2046 count[0] += 1
2049 2047 progress(_bundling, count[0],
2050 2048 unit=_manifests, total=count[1])
2051 2049 return clnode
2052 2050 else:
2053 2051 progress(_bundling, count[0], item=fstate[0],
2054 2052 unit=_files, total=count[1])
2055 2053 return fstate[1][x]
2056 2054
2057 2055 bundler = changegroup.bundle10(lookup)
2058 2056 reorder = self.ui.config('bundle', 'reorder', 'auto')
2059 2057 if reorder == 'auto':
2060 2058 reorder = None
2061 2059 else:
2062 2060 reorder = util.parsebool(reorder)
2063 2061
2064 2062 def gengroup():
2065 2063 # Create a changenode group generator that will call our functions
2066 2064 # back to lookup the owning changenode and collect information.
2067 2065 count[:] = [0, len(csets)]
2068 2066 for chunk in cl.group(csets, bundler, reorder=reorder):
2069 2067 yield chunk
2070 2068 progress(_bundling, None)
2071 2069
2072 2070 # Create a generator for the manifestnodes that calls our lookup
2073 2071 # and data collection functions back.
2074 2072 for f in changedfiles:
2075 2073 fnodes[f] = {}
2076 2074 count[:] = [0, len(mfs)]
2077 2075 for chunk in mf.group(prune(mf, mfs), bundler, reorder=reorder):
2078 2076 yield chunk
2079 2077 progress(_bundling, None)
2080 2078
2081 2079 mfs.clear()
2082 2080
2083 2081 # Go through all our files in order sorted by name.
2084 2082 count[:] = [0, len(changedfiles)]
2085 2083 for fname in sorted(changedfiles):
2086 2084 filerevlog = self.file(fname)
2087 2085 if not len(filerevlog):
2088 2086 raise util.Abort(_("empty or missing revlog for %s")
2089 2087 % fname)
2090 2088 fstate[0] = fname
2091 2089 fstate[1] = fnodes.pop(fname, {})
2092 2090
2093 2091 nodelist = prune(filerevlog, fstate[1])
2094 2092 if nodelist:
2095 2093 count[0] += 1
2096 2094 yield bundler.fileheader(fname)
2097 2095 for chunk in filerevlog.group(nodelist, bundler, reorder):
2098 2096 yield chunk
2099 2097
2100 2098 # Signal that no more groups are left.
2101 2099 yield bundler.close()
2102 2100 progress(_bundling, None)
2103 2101
2104 2102 if csets:
2105 2103 self.hook('outgoing', node=hex(csets[0]), source=source)
2106 2104
2107 2105 return changegroup.unbundle10(util.chunkbuffer(gengroup()), 'UN')
2108 2106
2109 2107 def changegroup(self, basenodes, source):
2110 2108 # to avoid a race we use changegroupsubset() (issue1320)
2111 2109 return self.changegroupsubset(basenodes, self.heads(), source)
2112 2110
2113 2111 @unfilteredmethod
2114 2112 def _changegroup(self, nodes, source):
2115 2113 """Compute the changegroup of all nodes that we have that a recipient
2116 2114 doesn't. Return a chunkbuffer object whose read() method will return
2117 2115 successive changegroup chunks.
2118 2116
2119 2117 This is much easier than the previous function as we can assume that
2120 2118 the recipient has any changenode we aren't sending them.
2121 2119
2122 2120 nodes is the set of nodes to send"""
2123 2121
2124 2122 cl = self.changelog
2125 2123 mf = self.manifest
2126 2124 mfs = {}
2127 2125 changedfiles = set()
2128 2126 fstate = ['']
2129 2127 count = [0, 0]
2130 2128
2131 2129 self.hook('preoutgoing', throw=True, source=source)
2132 2130 self.changegroupinfo(nodes, source)
2133 2131
2134 2132 revset = set([cl.rev(n) for n in nodes])
2135 2133
2136 2134 def gennodelst(log):
2137 2135 ln, llr = log.node, log.linkrev
2138 2136 return [ln(r) for r in log if llr(r) in revset]
2139 2137
2140 2138 progress = self.ui.progress
2141 2139 _bundling = _('bundling')
2142 2140 _changesets = _('changesets')
2143 2141 _manifests = _('manifests')
2144 2142 _files = _('files')
2145 2143
2146 2144 def lookup(revlog, x):
2147 2145 if revlog == cl:
2148 2146 c = cl.read(x)
2149 2147 changedfiles.update(c[3])
2150 2148 mfs.setdefault(c[0], x)
2151 2149 count[0] += 1
2152 2150 progress(_bundling, count[0],
2153 2151 unit=_changesets, total=count[1])
2154 2152 return x
2155 2153 elif revlog == mf:
2156 2154 count[0] += 1
2157 2155 progress(_bundling, count[0],
2158 2156 unit=_manifests, total=count[1])
2159 2157 return cl.node(revlog.linkrev(revlog.rev(x)))
2160 2158 else:
2161 2159 progress(_bundling, count[0], item=fstate[0],
2162 2160 total=count[1], unit=_files)
2163 2161 return cl.node(revlog.linkrev(revlog.rev(x)))
2164 2162
2165 2163 bundler = changegroup.bundle10(lookup)
2166 2164 reorder = self.ui.config('bundle', 'reorder', 'auto')
2167 2165 if reorder == 'auto':
2168 2166 reorder = None
2169 2167 else:
2170 2168 reorder = util.parsebool(reorder)
2171 2169
2172 2170 def gengroup():
2173 2171 '''yield a sequence of changegroup chunks (strings)'''
2174 2172 # construct a list of all changed files
2175 2173
2176 2174 count[:] = [0, len(nodes)]
2177 2175 for chunk in cl.group(nodes, bundler, reorder=reorder):
2178 2176 yield chunk
2179 2177 progress(_bundling, None)
2180 2178
2181 2179 count[:] = [0, len(mfs)]
2182 2180 for chunk in mf.group(gennodelst(mf), bundler, reorder=reorder):
2183 2181 yield chunk
2184 2182 progress(_bundling, None)
2185 2183
2186 2184 count[:] = [0, len(changedfiles)]
2187 2185 for fname in sorted(changedfiles):
2188 2186 filerevlog = self.file(fname)
2189 2187 if not len(filerevlog):
2190 2188 raise util.Abort(_("empty or missing revlog for %s")
2191 2189 % fname)
2192 2190 fstate[0] = fname
2193 2191 nodelist = gennodelst(filerevlog)
2194 2192 if nodelist:
2195 2193 count[0] += 1
2196 2194 yield bundler.fileheader(fname)
2197 2195 for chunk in filerevlog.group(nodelist, bundler, reorder):
2198 2196 yield chunk
2199 2197 yield bundler.close()
2200 2198 progress(_bundling, None)
2201 2199
2202 2200 if nodes:
2203 2201 self.hook('outgoing', node=hex(nodes[0]), source=source)
2204 2202
2205 2203 return changegroup.unbundle10(util.chunkbuffer(gengroup()), 'UN')
2206 2204
2207 2205 @unfilteredmethod
2208 2206 def addchangegroup(self, source, srctype, url, emptyok=False):
2209 2207 """Add the changegroup returned by source.read() to this repo.
2210 2208 srctype is a string like 'push', 'pull', or 'unbundle'. url is
2211 2209 the URL of the repo where this changegroup is coming from.
2212 2210
2213 2211 Return an integer summarizing the change to this repo:
2214 2212 - nothing changed or no source: 0
2215 2213 - more heads than before: 1+added heads (2..n)
2216 2214 - fewer heads than before: -1-removed heads (-2..-n)
2217 2215 - number of heads stays the same: 1
2218 2216 """
2219 2217 def csmap(x):
2220 2218 self.ui.debug("add changeset %s\n" % short(x))
2221 2219 return len(cl)
2222 2220
2223 2221 def revmap(x):
2224 2222 return cl.rev(x)
2225 2223
2226 2224 if not source:
2227 2225 return 0
2228 2226
2229 2227 self.hook('prechangegroup', throw=True, source=srctype, url=url)
2230 2228
2231 2229 changesets = files = revisions = 0
2232 2230 efiles = set()
2233 2231
2234 2232 # write changelog data to temp files so concurrent readers will not see
2235 2233 # inconsistent view
2236 2234 cl = self.changelog
2237 2235 cl.delayupdate()
2238 2236 oldheads = cl.heads()
2239 2237
2240 2238 tr = self.transaction("\n".join([srctype, util.hidepassword(url)]))
2241 2239 try:
2242 2240 trp = weakref.proxy(tr)
2243 2241 # pull off the changeset group
2244 2242 self.ui.status(_("adding changesets\n"))
2245 2243 clstart = len(cl)
2246 2244 class prog(object):
2247 2245 step = _('changesets')
2248 2246 count = 1
2249 2247 ui = self.ui
2250 2248 total = None
2251 2249 def __call__(self):
2252 2250 self.ui.progress(self.step, self.count, unit=_('chunks'),
2253 2251 total=self.total)
2254 2252 self.count += 1
2255 2253 pr = prog()
2256 2254 source.callback = pr
2257 2255
2258 2256 source.changelogheader()
2259 2257 srccontent = cl.addgroup(source, csmap, trp)
2260 2258 if not (srccontent or emptyok):
2261 2259 raise util.Abort(_("received changelog group is empty"))
2262 2260 clend = len(cl)
2263 2261 changesets = clend - clstart
2264 2262 for c in xrange(clstart, clend):
2265 2263 efiles.update(self[c].files())
2266 2264 efiles = len(efiles)
2267 2265 self.ui.progress(_('changesets'), None)
2268 2266
2269 2267 # pull off the manifest group
2270 2268 self.ui.status(_("adding manifests\n"))
2271 2269 pr.step = _('manifests')
2272 2270 pr.count = 1
2273 2271 pr.total = changesets # manifests <= changesets
2274 2272 # no need to check for empty manifest group here:
2275 2273 # if the result of the merge of 1 and 2 is the same in 3 and 4,
2276 2274 # no new manifest will be created and the manifest group will
2277 2275 # be empty during the pull
2278 2276 source.manifestheader()
2279 2277 self.manifest.addgroup(source, revmap, trp)
2280 2278 self.ui.progress(_('manifests'), None)
2281 2279
2282 2280 needfiles = {}
2283 2281 if self.ui.configbool('server', 'validate', default=False):
2284 2282 # validate incoming csets have their manifests
2285 2283 for cset in xrange(clstart, clend):
2286 2284 mfest = self.changelog.read(self.changelog.node(cset))[0]
2287 2285 mfest = self.manifest.readdelta(mfest)
2288 2286 # store file nodes we must see
2289 2287 for f, n in mfest.iteritems():
2290 2288 needfiles.setdefault(f, set()).add(n)
2291 2289
2292 2290 # process the files
2293 2291 self.ui.status(_("adding file changes\n"))
2294 2292 pr.step = _('files')
2295 2293 pr.count = 1
2296 2294 pr.total = efiles
2297 2295 source.callback = None
2298 2296
2299 2297 while True:
2300 2298 chunkdata = source.filelogheader()
2301 2299 if not chunkdata:
2302 2300 break
2303 2301 f = chunkdata["filename"]
2304 2302 self.ui.debug("adding %s revisions\n" % f)
2305 2303 pr()
2306 2304 fl = self.file(f)
2307 2305 o = len(fl)
2308 2306 if not fl.addgroup(source, revmap, trp):
2309 2307 raise util.Abort(_("received file revlog group is empty"))
2310 2308 revisions += len(fl) - o
2311 2309 files += 1
2312 2310 if f in needfiles:
2313 2311 needs = needfiles[f]
2314 2312 for new in xrange(o, len(fl)):
2315 2313 n = fl.node(new)
2316 2314 if n in needs:
2317 2315 needs.remove(n)
2318 2316 else:
2319 2317 raise util.Abort(
2320 2318 _("received spurious file revlog entry"))
2321 2319 if not needs:
2322 2320 del needfiles[f]
2323 2321 self.ui.progress(_('files'), None)
2324 2322
2325 2323 for f, needs in needfiles.iteritems():
2326 2324 fl = self.file(f)
2327 2325 for n in needs:
2328 2326 try:
2329 2327 fl.rev(n)
2330 2328 except error.LookupError:
2331 2329 raise util.Abort(
2332 2330 _('missing file data for %s:%s - run hg verify') %
2333 2331 (f, hex(n)))
2334 2332
2335 2333 dh = 0
2336 2334 if oldheads:
2337 2335 heads = cl.heads()
2338 2336 dh = len(heads) - len(oldheads)
2339 2337 for h in heads:
2340 2338 if h not in oldheads and self[h].closesbranch():
2341 2339 dh -= 1
2342 2340 htext = ""
2343 2341 if dh:
2344 2342 htext = _(" (%+d heads)") % dh
2345 2343
2346 2344 self.ui.status(_("added %d changesets"
2347 2345 " with %d changes to %d files%s\n")
2348 2346 % (changesets, revisions, files, htext))
2349 2347 self.invalidatevolatilesets()
2350 2348
2351 2349 if changesets > 0:
2352 2350 p = lambda: cl.writepending() and self.root or ""
2353 2351 self.hook('pretxnchangegroup', throw=True,
2354 2352 node=hex(cl.node(clstart)), source=srctype,
2355 2353 url=url, pending=p)
2356 2354
2357 2355 added = [cl.node(r) for r in xrange(clstart, clend)]
2358 2356 publishing = self.ui.configbool('phases', 'publish', True)
2359 2357 if srctype == 'push':
2360 2358 # Old server can not push the boundary themself.
2361 2359 # New server won't push the boundary if changeset already
2362 2360 # existed locally as secrete
2363 2361 #
2364 2362 # We should not use added here but the list of all change in
2365 2363 # the bundle
2366 2364 if publishing:
2367 2365 phases.advanceboundary(self, phases.public, srccontent)
2368 2366 else:
2369 2367 phases.advanceboundary(self, phases.draft, srccontent)
2370 2368 phases.retractboundary(self, phases.draft, added)
2371 2369 elif srctype != 'strip':
2372 2370 # publishing only alter behavior during push
2373 2371 #
2374 2372 # strip should not touch boundary at all
2375 2373 phases.retractboundary(self, phases.draft, added)
2376 2374
2377 2375 # make changelog see real files again
2378 2376 cl.finalize(trp)
2379 2377
2380 2378 tr.close()
2381 2379
2382 2380 if changesets > 0:
2383 2381 if srctype != 'strip':
2384 2382 # During strip, branchcache is invalid but coming call to
2385 2383 # `destroyed` will repair it.
2386 2384 # In other case we can safely update cache on disk.
2387 2385 branchmap.updatecache(self.filtered('served'))
2388 2386 def runhooks():
2389 2387 # forcefully update the on-disk branch cache
2390 2388 self.ui.debug("updating the branch cache\n")
2391 2389 self.hook("changegroup", node=hex(cl.node(clstart)),
2392 2390 source=srctype, url=url)
2393 2391
2394 2392 for n in added:
2395 2393 self.hook("incoming", node=hex(n), source=srctype,
2396 2394 url=url)
2397 2395
2398 2396 newheads = [h for h in self.heads() if h not in oldheads]
2399 2397 self.ui.log("incoming",
2400 2398 "%s incoming changes - new heads: %s\n",
2401 2399 len(added),
2402 2400 ', '.join([hex(c[:6]) for c in newheads]))
2403 2401 self._afterlock(runhooks)
2404 2402
2405 2403 finally:
2406 2404 tr.release()
2407 2405 # never return 0 here:
2408 2406 if dh < 0:
2409 2407 return dh - 1
2410 2408 else:
2411 2409 return dh + 1
2412 2410
2413 2411 def stream_in(self, remote, requirements):
2414 2412 lock = self.lock()
2415 2413 try:
2416 2414 # Save remote branchmap. We will use it later
2417 2415 # to speed up branchcache creation
2418 2416 rbranchmap = None
2419 2417 if remote.capable("branchmap"):
2420 2418 rbranchmap = remote.branchmap()
2421 2419
2422 2420 fp = remote.stream_out()
2423 2421 l = fp.readline()
2424 2422 try:
2425 2423 resp = int(l)
2426 2424 except ValueError:
2427 2425 raise error.ResponseError(
2428 2426 _('unexpected response from remote server:'), l)
2429 2427 if resp == 1:
2430 2428 raise util.Abort(_('operation forbidden by server'))
2431 2429 elif resp == 2:
2432 2430 raise util.Abort(_('locking the remote repository failed'))
2433 2431 elif resp != 0:
2434 2432 raise util.Abort(_('the server sent an unknown error code'))
2435 2433 self.ui.status(_('streaming all changes\n'))
2436 2434 l = fp.readline()
2437 2435 try:
2438 2436 total_files, total_bytes = map(int, l.split(' ', 1))
2439 2437 except (ValueError, TypeError):
2440 2438 raise error.ResponseError(
2441 2439 _('unexpected response from remote server:'), l)
2442 2440 self.ui.status(_('%d files to transfer, %s of data\n') %
2443 2441 (total_files, util.bytecount(total_bytes)))
2444 2442 handled_bytes = 0
2445 2443 self.ui.progress(_('clone'), 0, total=total_bytes)
2446 2444 start = time.time()
2447 2445 for i in xrange(total_files):
2448 2446 # XXX doesn't support '\n' or '\r' in filenames
2449 2447 l = fp.readline()
2450 2448 try:
2451 2449 name, size = l.split('\0', 1)
2452 2450 size = int(size)
2453 2451 except (ValueError, TypeError):
2454 2452 raise error.ResponseError(
2455 2453 _('unexpected response from remote server:'), l)
2456 2454 if self.ui.debugflag:
2457 2455 self.ui.debug('adding %s (%s)\n' %
2458 2456 (name, util.bytecount(size)))
2459 2457 # for backwards compat, name was partially encoded
2460 2458 ofp = self.sopener(store.decodedir(name), 'w')
2461 2459 for chunk in util.filechunkiter(fp, limit=size):
2462 2460 handled_bytes += len(chunk)
2463 2461 self.ui.progress(_('clone'), handled_bytes,
2464 2462 total=total_bytes)
2465 2463 ofp.write(chunk)
2466 2464 ofp.close()
2467 2465 elapsed = time.time() - start
2468 2466 if elapsed <= 0:
2469 2467 elapsed = 0.001
2470 2468 self.ui.progress(_('clone'), None)
2471 2469 self.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
2472 2470 (util.bytecount(total_bytes), elapsed,
2473 2471 util.bytecount(total_bytes / elapsed)))
2474 2472
2475 2473 # new requirements = old non-format requirements +
2476 2474 # new format-related
2477 2475 # requirements from the streamed-in repository
2478 2476 requirements.update(set(self.requirements) - self.supportedformats)
2479 2477 self._applyrequirements(requirements)
2480 2478 self._writerequirements()
2481 2479
2482 2480 if rbranchmap:
2483 2481 rbheads = []
2484 2482 for bheads in rbranchmap.itervalues():
2485 2483 rbheads.extend(bheads)
2486 2484
2487 2485 if rbheads:
2488 2486 rtiprev = max((int(self.changelog.rev(node))
2489 2487 for node in rbheads))
2490 2488 cache = branchmap.branchcache(rbranchmap,
2491 2489 self[rtiprev].node(),
2492 2490 rtiprev)
2493 2491 # Try to stick it as low as possible
2494 2492 # filter above served are unlikely to be fetch from a clone
2495 2493 for candidate in ('base', 'immutable', 'served'):
2496 2494 rview = self.filtered(candidate)
2497 2495 if cache.validfor(rview):
2498 2496 self._branchcaches[candidate] = cache
2499 2497 cache.write(rview)
2500 2498 break
2501 2499 self.invalidate()
2502 2500 return len(self.heads()) + 1
2503 2501 finally:
2504 2502 lock.release()
2505 2503
2506 2504 def clone(self, remote, heads=[], stream=False):
2507 2505 '''clone remote repository.
2508 2506
2509 2507 keyword arguments:
2510 2508 heads: list of revs to clone (forces use of pull)
2511 2509 stream: use streaming clone if possible'''
2512 2510
2513 2511 # now, all clients that can request uncompressed clones can
2514 2512 # read repo formats supported by all servers that can serve
2515 2513 # them.
2516 2514
2517 2515 # if revlog format changes, client will have to check version
2518 2516 # and format flags on "stream" capability, and use
2519 2517 # uncompressed only if compatible.
2520 2518
2521 2519 if not stream:
2522 2520 # if the server explicitly prefers to stream (for fast LANs)
2523 2521 stream = remote.capable('stream-preferred')
2524 2522
2525 2523 if stream and not heads:
2526 2524 # 'stream' means remote revlog format is revlogv1 only
2527 2525 if remote.capable('stream'):
2528 2526 return self.stream_in(remote, set(('revlogv1',)))
2529 2527 # otherwise, 'streamreqs' contains the remote revlog format
2530 2528 streamreqs = remote.capable('streamreqs')
2531 2529 if streamreqs:
2532 2530 streamreqs = set(streamreqs.split(','))
2533 2531 # if we support it, stream in and adjust our requirements
2534 2532 if not streamreqs - self.supportedformats:
2535 2533 return self.stream_in(remote, streamreqs)
2536 2534 return self.pull(remote, heads)
2537 2535
2538 2536 def pushkey(self, namespace, key, old, new):
2539 2537 self.hook('prepushkey', throw=True, namespace=namespace, key=key,
2540 2538 old=old, new=new)
2541 2539 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
2542 2540 ret = pushkey.push(self, namespace, key, old, new)
2543 2541 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
2544 2542 ret=ret)
2545 2543 return ret
2546 2544
2547 2545 def listkeys(self, namespace):
2548 2546 self.hook('prelistkeys', throw=True, namespace=namespace)
2549 2547 self.ui.debug('listing keys for "%s"\n' % namespace)
2550 2548 values = pushkey.list(self, namespace)
2551 2549 self.hook('listkeys', namespace=namespace, values=values)
2552 2550 return values
2553 2551
2554 2552 def debugwireargs(self, one, two, three=None, four=None, five=None):
2555 2553 '''used to test argument passing over the wire'''
2556 2554 return "%s %s %s %s %s" % (one, two, three, four, five)
2557 2555
2558 2556 def savecommitmessage(self, text):
2559 2557 fp = self.opener('last-message.txt', 'wb')
2560 2558 try:
2561 2559 fp.write(text)
2562 2560 finally:
2563 2561 fp.close()
2564 2562 return self.pathto(fp.name[len(self.root) + 1:])
2565 2563
2566 2564 # used to avoid circular references so destructors work
2567 2565 def aftertrans(files):
2568 2566 renamefiles = [tuple(t) for t in files]
2569 2567 def a():
2570 2568 for vfs, src, dest in renamefiles:
2571 2569 try:
2572 2570 vfs.rename(src, dest)
2573 2571 except OSError: # journal file does not yet exist
2574 2572 pass
2575 2573 return a
2576 2574
2577 2575 def undoname(fn):
2578 2576 base, name = os.path.split(fn)
2579 2577 assert name.startswith('journal')
2580 2578 return os.path.join(base, name.replace('journal', 'undo', 1))
2581 2579
2582 2580 def instance(ui, path, create):
2583 2581 return localrepository(ui, util.urllocalpath(path), create)
2584 2582
2585 2583 def islocal(path):
2586 2584 return True
@@ -1,798 +1,819 b''
1 1 # obsolete.py - obsolete markers handling
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Obsolete markers handling
10 10
11 11 An obsolete marker maps an old changeset to a list of new
12 12 changesets. If the list of new changesets is empty, the old changeset
13 13 is said to be "killed". Otherwise, the old changeset is being
14 14 "replaced" by the new changesets.
15 15
16 16 Obsolete markers can be used to record and distribute changeset graph
17 17 transformations performed by history rewriting operations, and help
18 18 building new tools to reconciliate conflicting rewriting actions. To
19 19 facilitate conflicts resolution, markers include various annotations
20 20 besides old and news changeset identifiers, such as creation date or
21 21 author name.
22 22
23 23 The old obsoleted changeset is called "precursor" and possible replacements are
24 24 called "successors". Markers that used changeset X as a precursors are called
25 25 "successor markers of X" because they hold information about the successors of
26 26 X. Markers that use changeset Y as a successors are call "precursor markers of
27 27 Y" because they hold information about the precursors of Y.
28 28
29 29 Examples:
30 30
31 31 - When changeset A is replacement by a changeset A', one marker is stored:
32 32
33 33 (A, (A'))
34 34
35 35 - When changesets A and B are folded into a new changeset C two markers are
36 36 stored:
37 37
38 38 (A, (C,)) and (B, (C,))
39 39
40 40 - When changeset A is simply "pruned" from the graph, a marker in create:
41 41
42 42 (A, ())
43 43
44 44 - When changeset A is split into B and C, a single marker are used:
45 45
46 46 (A, (C, C))
47 47
48 48 We use a single marker to distinct the "split" case from the "divergence"
49 49 case. If two independents operation rewrite the same changeset A in to A' and
50 50 A'' when have an error case: divergent rewriting. We can detect it because
51 51 two markers will be created independently:
52 52
53 53 (A, (B,)) and (A, (C,))
54 54
55 55 Format
56 56 ------
57 57
58 58 Markers are stored in an append-only file stored in
59 59 '.hg/store/obsstore'.
60 60
61 61 The file starts with a version header:
62 62
63 63 - 1 unsigned byte: version number, starting at zero.
64 64
65 65
66 66 The header is followed by the markers. Each marker is made of:
67 67
68 68 - 1 unsigned byte: number of new changesets "R", could be zero.
69 69
70 70 - 1 unsigned 32-bits integer: metadata size "M" in bytes.
71 71
72 72 - 1 byte: a bit field. It is reserved for flags used in obsolete
73 73 markers common operations, to avoid repeated decoding of metadata
74 74 entries.
75 75
76 76 - 20 bytes: obsoleted changeset identifier.
77 77
78 78 - N*20 bytes: new changesets identifiers.
79 79
80 80 - M bytes: metadata as a sequence of nul-terminated strings. Each
81 81 string contains a key and a value, separated by a color ':', without
82 82 additional encoding. Keys cannot contain '\0' or ':' and values
83 83 cannot contain '\0'.
84 84 """
85 85 import struct
86 86 import util, base85, node
87 87 from i18n import _
88 88
89 89 _pack = struct.pack
90 90 _unpack = struct.unpack
91 91
92 92 _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
93 93
94 94 # the obsolete feature is not mature enough to be enabled by default.
95 95 # you have to rely on third party extension extension to enable this.
96 96 _enabled = False
97 97
98 98 # data used for parsing and writing
99 99 _fmversion = 0
100 100 _fmfixed = '>BIB20s'
101 101 _fmnode = '20s'
102 102 _fmfsize = struct.calcsize(_fmfixed)
103 103 _fnodesize = struct.calcsize(_fmnode)
104 104
105 105 ### obsolescence marker flag
106 106
107 107 ## bumpedfix flag
108 108 #
109 109 # When a changeset A' succeed to a changeset A which became public, we call A'
110 110 # "bumped" because it's a successors of a public changesets
111 111 #
112 112 # o A' (bumped)
113 113 # |`:
114 114 # | o A
115 115 # |/
116 116 # o Z
117 117 #
118 118 # The way to solve this situation is to create a new changeset Ad as children
119 119 # of A. This changeset have the same content than A'. So the diff from A to A'
120 120 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
121 121 #
122 122 # o Ad
123 123 # |`:
124 124 # | x A'
125 125 # |'|
126 126 # o | A
127 127 # |/
128 128 # o Z
129 129 #
130 130 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
131 131 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
132 132 # This flag mean that the successors express the changes between the public and
133 133 # bumped version and fix the situation, breaking the transitivity of
134 134 # "bumped" here.
135 135 bumpedfix = 1
136 136
137 137 def _readmarkers(data):
138 138 """Read and enumerate markers from raw data"""
139 139 off = 0
140 140 diskversion = _unpack('>B', data[off:off + 1])[0]
141 141 off += 1
142 142 if diskversion != _fmversion:
143 143 raise util.Abort(_('parsing obsolete marker: unknown version %r')
144 144 % diskversion)
145 145
146 146 # Loop on markers
147 147 l = len(data)
148 148 while off + _fmfsize <= l:
149 149 # read fixed part
150 150 cur = data[off:off + _fmfsize]
151 151 off += _fmfsize
152 152 nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
153 153 # read replacement
154 154 sucs = ()
155 155 if nbsuc:
156 156 s = (_fnodesize * nbsuc)
157 157 cur = data[off:off + s]
158 158 sucs = _unpack(_fmnode * nbsuc, cur)
159 159 off += s
160 160 # read metadata
161 161 # (metadata will be decoded on demand)
162 162 metadata = data[off:off + mdsize]
163 163 if len(metadata) != mdsize:
164 164 raise util.Abort(_('parsing obsolete marker: metadata is too '
165 165 'short, %d bytes expected, got %d')
166 166 % (mdsize, len(metadata)))
167 167 off += mdsize
168 168 yield (pre, sucs, flags, metadata)
169 169
170 170 def encodemeta(meta):
171 171 """Return encoded metadata string to string mapping.
172 172
173 173 Assume no ':' in key and no '\0' in both key and value."""
174 174 for key, value in meta.iteritems():
175 175 if ':' in key or '\0' in key:
176 176 raise ValueError("':' and '\0' are forbidden in metadata key'")
177 177 if '\0' in value:
178 178 raise ValueError("':' are forbidden in metadata value'")
179 179 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
180 180
181 181 def decodemeta(data):
182 182 """Return string to string dictionary from encoded version."""
183 183 d = {}
184 184 for l in data.split('\0'):
185 185 if l:
186 186 key, value = l.split(':')
187 187 d[key] = value
188 188 return d
189 189
190 190 class marker(object):
191 191 """Wrap obsolete marker raw data"""
192 192
193 193 def __init__(self, repo, data):
194 194 # the repo argument will be used to create changectx in later version
195 195 self._repo = repo
196 196 self._data = data
197 197 self._decodedmeta = None
198 198
199 199 def precnode(self):
200 200 """Precursor changeset node identifier"""
201 201 return self._data[0]
202 202
203 203 def succnodes(self):
204 204 """List of successor changesets node identifiers"""
205 205 return self._data[1]
206 206
207 207 def metadata(self):
208 208 """Decoded metadata dictionary"""
209 209 if self._decodedmeta is None:
210 210 self._decodedmeta = decodemeta(self._data[3])
211 211 return self._decodedmeta
212 212
213 213 def date(self):
214 214 """Creation date as (unixtime, offset)"""
215 215 parts = self.metadata()['date'].split(' ')
216 216 return (float(parts[0]), int(parts[1]))
217 217
218 218 class obsstore(object):
219 219 """Store obsolete markers
220 220
221 221 Markers can be accessed with two mappings:
222 222 - precursors[x] -> set(markers on precursors edges of x)
223 223 - successors[x] -> set(markers on successors edges of x)
224 224 """
225 225
226 226 def __init__(self, sopener):
227 227 # caches for various obsolescence related cache
228 228 self.caches = {}
229 229 self._all = []
230 230 # new markers to serialize
231 231 self.precursors = {}
232 232 self.successors = {}
233 233 self.sopener = sopener
234 234 data = sopener.tryread('obsstore')
235 235 if data:
236 236 self._load(_readmarkers(data))
237 237
238 238 def __iter__(self):
239 239 return iter(self._all)
240 240
241 241 def __nonzero__(self):
242 242 return bool(self._all)
243 243
244 244 def create(self, transaction, prec, succs=(), flag=0, metadata=None):
245 245 """obsolete: add a new obsolete marker
246 246
247 247 * ensuring it is hashable
248 248 * check mandatory metadata
249 249 * encode metadata
250 250 """
251 251 if metadata is None:
252 252 metadata = {}
253 253 if 'date' not in metadata:
254 254 metadata['date'] = "%d %d" % util.makedate()
255 255 if len(prec) != 20:
256 256 raise ValueError(prec)
257 257 for succ in succs:
258 258 if len(succ) != 20:
259 259 raise ValueError(succ)
260 260 marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
261 261 self.add(transaction, [marker])
262 262
263 263 def add(self, transaction, markers):
264 264 """Add new markers to the store
265 265
266 266 Take care of filtering duplicate.
267 267 Return the number of new marker."""
268 268 if not _enabled:
269 269 raise util.Abort('obsolete feature is not enabled on this repo')
270 270 new = [m for m in markers if m not in self._all]
271 271 if new:
272 272 f = self.sopener('obsstore', 'ab')
273 273 try:
274 274 # Whether the file's current position is at the begin or at
275 275 # the end after opening a file for appending is implementation
276 276 # defined. So we must seek to the end before calling tell(),
277 277 # or we may get a zero offset for non-zero sized files on
278 278 # some platforms (issue3543).
279 279 f.seek(0, _SEEK_END)
280 280 offset = f.tell()
281 281 transaction.add('obsstore', offset)
282 282 # offset == 0: new file - add the version header
283 283 for bytes in _encodemarkers(new, offset == 0):
284 284 f.write(bytes)
285 285 finally:
286 286 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
287 287 # call 'filecacheentry.refresh()' here
288 288 f.close()
289 289 self._load(new)
290 290 # new marker *may* have changed several set. invalidate the cache.
291 291 self.caches.clear()
292 292 return len(new)
293 293
294 294 def mergemarkers(self, transaction, data):
295 295 markers = _readmarkers(data)
296 296 self.add(transaction, markers)
297 297
298 298 def _load(self, markers):
299 299 for mark in markers:
300 300 self._all.append(mark)
301 301 pre, sucs = mark[:2]
302 302 self.successors.setdefault(pre, set()).add(mark)
303 303 for suc in sucs:
304 304 self.precursors.setdefault(suc, set()).add(mark)
305 305 if node.nullid in self.precursors:
306 306 raise util.Abort(_('bad obsolescence marker detected: '
307 307 'invalid successors nullid'))
308 308
309 309 def _encodemarkers(markers, addheader=False):
310 310 # Kept separate from flushmarkers(), it will be reused for
311 311 # markers exchange.
312 312 if addheader:
313 313 yield _pack('>B', _fmversion)
314 314 for marker in markers:
315 315 yield _encodeonemarker(marker)
316 316
317 317
318 318 def _encodeonemarker(marker):
319 319 pre, sucs, flags, metadata = marker
320 320 nbsuc = len(sucs)
321 321 format = _fmfixed + (_fmnode * nbsuc)
322 322 data = [nbsuc, len(metadata), flags, pre]
323 323 data.extend(sucs)
324 324 return _pack(format, *data) + metadata
325 325
326 326 # arbitrary picked to fit into 8K limit from HTTP server
327 327 # you have to take in account:
328 328 # - the version header
329 329 # - the base85 encoding
330 330 _maxpayload = 5300
331 331
332 332 def listmarkers(repo):
333 333 """List markers over pushkey"""
334 334 if not repo.obsstore:
335 335 return {}
336 336 keys = {}
337 337 parts = []
338 338 currentlen = _maxpayload * 2 # ensure we create a new part
339 339 for marker in repo.obsstore:
340 340 nextdata = _encodeonemarker(marker)
341 341 if (len(nextdata) + currentlen > _maxpayload):
342 342 currentpart = []
343 343 currentlen = 0
344 344 parts.append(currentpart)
345 345 currentpart.append(nextdata)
346 346 currentlen += len(nextdata)
347 347 for idx, part in enumerate(reversed(parts)):
348 348 data = ''.join([_pack('>B', _fmversion)] + part)
349 349 keys['dump%i' % idx] = base85.b85encode(data)
350 350 return keys
351 351
352 352 def pushmarker(repo, key, old, new):
353 353 """Push markers over pushkey"""
354 354 if not key.startswith('dump'):
355 355 repo.ui.warn(_('unknown key: %r') % key)
356 356 return 0
357 357 if old:
358 358 repo.ui.warn(_('unexpected old value') % key)
359 359 return 0
360 360 data = base85.b85decode(new)
361 361 lock = repo.lock()
362 362 try:
363 363 tr = repo.transaction('pushkey: obsolete markers')
364 364 try:
365 365 repo.obsstore.mergemarkers(tr, data)
366 366 tr.close()
367 367 return 1
368 368 finally:
369 369 tr.release()
370 370 finally:
371 371 lock.release()
372 372
373 373 def syncpush(repo, remote):
374 374 """utility function to push bookmark to a remote
375 375
376 376 Exist mostly to allow overridding for experimentation purpose"""
377 377 if (_enabled and repo.obsstore and
378 378 'obsolete' in remote.listkeys('namespaces')):
379 379 rslts = []
380 380 remotedata = repo.listkeys('obsolete')
381 381 for key in sorted(remotedata, reverse=True):
382 382 # reverse sort to ensure we end with dump0
383 383 data = remotedata[key]
384 384 rslts.append(remote.pushkey('obsolete', key, '', data))
385 385 if [r for r in rslts if not r]:
386 386 msg = _('failed to push some obsolete markers!\n')
387 387 repo.ui.warn(msg)
388 388
389 def syncpull(repo, remote, gettransaction):
390 """utility function to pull bookmark to a remote
391
392 The `gettransaction` is function that return the pull transaction, creating
393 one if necessary. We return the transaction to inform the calling code that
394 a new transaction have been created (when applicable).
395
396 Exists mostly to allow overridding for experimentation purpose"""
397 tr = None
398 if _enabled:
399 repo.ui.debug('fetching remote obsolete markers\n')
400 remoteobs = remote.listkeys('obsolete')
401 if 'dump0' in remoteobs:
402 tr = gettransaction()
403 for key in sorted(remoteobs, reverse=True):
404 if key.startswith('dump'):
405 data = base85.b85decode(remoteobs[key])
406 repo.obsstore.mergemarkers(tr, data)
407 repo.invalidatevolatilesets()
408 return tr
409
389 410 def allmarkers(repo):
390 411 """all obsolete markers known in a repository"""
391 412 for markerdata in repo.obsstore:
392 413 yield marker(repo, markerdata)
393 414
394 415 def precursormarkers(ctx):
395 416 """obsolete marker marking this changeset as a successors"""
396 417 for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
397 418 yield marker(ctx._repo, data)
398 419
399 420 def successormarkers(ctx):
400 421 """obsolete marker making this changeset obsolete"""
401 422 for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
402 423 yield marker(ctx._repo, data)
403 424
404 425 def allsuccessors(obsstore, nodes, ignoreflags=0):
405 426 """Yield node for every successor of <nodes>.
406 427
407 428 Some successors may be unknown locally.
408 429
409 430 This is a linear yield unsuited to detecting split changesets."""
410 431 remaining = set(nodes)
411 432 seen = set(remaining)
412 433 while remaining:
413 434 current = remaining.pop()
414 435 yield current
415 436 for mark in obsstore.successors.get(current, ()):
416 437 # ignore marker flagged with with specified flag
417 438 if mark[2] & ignoreflags:
418 439 continue
419 440 for suc in mark[1]:
420 441 if suc not in seen:
421 442 seen.add(suc)
422 443 remaining.add(suc)
423 444
424 445 def foreground(repo, nodes):
425 446 """return all nodes in the "foreground" of other node
426 447
427 448 The foreground of a revision is anything reachable using parent -> children
428 449 or precursor -> sucessor relation. It is very similars to "descendant" but
429 450 augmented with obsolescence information.
430 451
431 452 Beware that possible obsolescence cycle may result if complexe situation.
432 453 """
433 454 repo = repo.unfiltered()
434 455 foreground = set(repo.set('%ln::', nodes))
435 456 if repo.obsstore:
436 457 # We only need this complicated logic if there is obsolescence
437 458 # XXX will probably deserve an optimised revset.
438 459 nm = repo.changelog.nodemap
439 460 plen = -1
440 461 # compute the whole set of successors or descendants
441 462 while len(foreground) != plen:
442 463 plen = len(foreground)
443 464 succs = set(c.node() for c in foreground)
444 465 mutable = [c.node() for c in foreground if c.mutable()]
445 466 succs.update(allsuccessors(repo.obsstore, mutable))
446 467 known = (n for n in succs if n in nm)
447 468 foreground = set(repo.set('%ln::', known))
448 469 return set(c.node() for c in foreground)
449 470
450 471
451 472 def successorssets(repo, initialnode, cache=None):
452 473 """Return all set of successors of initial nodes
453 474
454 475 Successors set of changeset A are a group of revision that succeed A. It
455 476 succeed A as a consistent whole, each revision being only partial
456 477 replacement. Successors set contains non-obsolete changeset only.
457 478
458 479 In most cases a changeset A have zero (changeset pruned) or a single
459 480 successors set that contains a single successor (changeset A replaced by
460 481 A')
461 482
462 483 When changeset is split, it results successors set containing more than
463 484 a single element. Divergent rewriting will result in multiple successors
464 485 sets.
465 486
466 487 They are returned as a list of tuples containing all valid successors sets.
467 488
468 489 Final successors unknown locally are considered plain prune (obsoleted
469 490 without successors).
470 491
471 492 The optional `cache` parameter is a dictionary that may contains
472 493 precomputed successors sets. It is meant to reuse the computation of
473 494 previous call to `successorssets` when multiple calls are made at the same
474 495 time. The cache dictionary is updated in place. The caller is responsible
475 496 for its live spawn. Code that makes multiple calls to `successorssets`
476 497 *must* use this cache mechanism or suffer terrible performances."""
477 498
478 499 succmarkers = repo.obsstore.successors
479 500
480 501 # Stack of nodes we search successors sets for
481 502 toproceed = [initialnode]
482 503 # set version of above list for fast loop detection
483 504 # element added to "toproceed" must be added here
484 505 stackedset = set(toproceed)
485 506 if cache is None:
486 507 cache = {}
487 508
488 509 # This while loop is the flattened version of a recursive search for
489 510 # successors sets
490 511 #
491 512 # def successorssets(x):
492 513 # successors = directsuccessors(x)
493 514 # ss = [[]]
494 515 # for succ in directsuccessors(x):
495 516 # # product as in itertools cartesian product
496 517 # ss = product(ss, successorssets(succ))
497 518 # return ss
498 519 #
499 520 # But we can not use plain recursive calls here:
500 521 # - that would blow the python call stack
501 522 # - obsolescence markers may have cycles, we need to handle them.
502 523 #
503 524 # The `toproceed` list act as our call stack. Every node we search
504 525 # successors set for are stacked there.
505 526 #
506 527 # The `stackedset` is set version of this stack used to check if a node is
507 528 # already stacked. This check is used to detect cycles and prevent infinite
508 529 # loop.
509 530 #
510 531 # successors set of all nodes are stored in the `cache` dictionary.
511 532 #
512 533 # After this while loop ends we use the cache to return the successors sets
513 534 # for the node requested by the caller.
514 535 while toproceed:
515 536 # Every iteration tries to compute the successors sets of the topmost
516 537 # node of the stack: CURRENT.
517 538 #
518 539 # There are four possible outcomes:
519 540 #
520 541 # 1) We already know the successors sets of CURRENT:
521 542 # -> mission accomplished, pop it from the stack.
522 543 # 2) Node is not obsolete:
523 544 # -> the node is its own successors sets. Add it to the cache.
524 545 # 3) We do not know successors set of direct successors of CURRENT:
525 546 # -> We add those successors to the stack.
526 547 # 4) We know successors sets of all direct successors of CURRENT:
527 548 # -> We can compute CURRENT successors set and add it to the
528 549 # cache.
529 550 #
530 551 current = toproceed[-1]
531 552 if current in cache:
532 553 # case (1): We already know the successors sets
533 554 stackedset.remove(toproceed.pop())
534 555 elif current not in succmarkers:
535 556 # case (2): The node is not obsolete.
536 557 if current in repo:
537 558 # We have a valid last successors.
538 559 cache[current] = [(current,)]
539 560 else:
540 561 # Final obsolete version is unknown locally.
541 562 # Do not count that as a valid successors
542 563 cache[current] = []
543 564 else:
544 565 # cases (3) and (4)
545 566 #
546 567 # We proceed in two phases. Phase 1 aims to distinguish case (3)
547 568 # from case (4):
548 569 #
549 570 # For each direct successors of CURRENT, we check whether its
550 571 # successors sets are known. If they are not, we stack the
551 572 # unknown node and proceed to the next iteration of the while
552 573 # loop. (case 3)
553 574 #
554 575 # During this step, we may detect obsolescence cycles: a node
555 576 # with unknown successors sets but already in the call stack.
556 577 # In such a situation, we arbitrary set the successors sets of
557 578 # the node to nothing (node pruned) to break the cycle.
558 579 #
559 580 # If no break was encountered we proceed to phase 2.
560 581 #
561 582 # Phase 2 computes successors sets of CURRENT (case 4); see details
562 583 # in phase 2 itself.
563 584 #
564 585 # Note the two levels of iteration in each phase.
565 586 # - The first one handles obsolescence markers using CURRENT as
566 587 # precursor (successors markers of CURRENT).
567 588 #
568 589 # Having multiple entry here means divergence.
569 590 #
570 591 # - The second one handles successors defined in each marker.
571 592 #
572 593 # Having none means pruned node, multiple successors means split,
573 594 # single successors are standard replacement.
574 595 #
575 596 for mark in sorted(succmarkers[current]):
576 597 for suc in mark[1]:
577 598 if suc not in cache:
578 599 if suc in stackedset:
579 600 # cycle breaking
580 601 cache[suc] = []
581 602 else:
582 603 # case (3) If we have not computed successors sets
583 604 # of one of those successors we add it to the
584 605 # `toproceed` stack and stop all work for this
585 606 # iteration.
586 607 toproceed.append(suc)
587 608 stackedset.add(suc)
588 609 break
589 610 else:
590 611 continue
591 612 break
592 613 else:
593 614 # case (4): we know all successors sets of all direct
594 615 # successors
595 616 #
596 617 # Successors set contributed by each marker depends on the
597 618 # successors sets of all its "successors" node.
598 619 #
599 620 # Each different marker is a divergence in the obsolescence
600 621 # history. It contributes successors sets distinct from other
601 622 # markers.
602 623 #
603 624 # Within a marker, a successor may have divergent successors
604 625 # sets. In such a case, the marker will contribute multiple
605 626 # divergent successors sets. If multiple successors have
606 627 # divergent successors sets, a cartesian product is used.
607 628 #
608 629 # At the end we post-process successors sets to remove
609 630 # duplicated entry and successors set that are strict subset of
610 631 # another one.
611 632 succssets = []
612 633 for mark in sorted(succmarkers[current]):
613 634 # successors sets contributed by this marker
614 635 markss = [[]]
615 636 for suc in mark[1]:
616 637 # cardinal product with previous successors
617 638 productresult = []
618 639 for prefix in markss:
619 640 for suffix in cache[suc]:
620 641 newss = list(prefix)
621 642 for part in suffix:
622 643 # do not duplicated entry in successors set
623 644 # first entry wins.
624 645 if part not in newss:
625 646 newss.append(part)
626 647 productresult.append(newss)
627 648 markss = productresult
628 649 succssets.extend(markss)
629 650 # remove duplicated and subset
630 651 seen = []
631 652 final = []
632 653 candidate = sorted(((set(s), s) for s in succssets if s),
633 654 key=lambda x: len(x[1]), reverse=True)
634 655 for setversion, listversion in candidate:
635 656 for seenset in seen:
636 657 if setversion.issubset(seenset):
637 658 break
638 659 else:
639 660 final.append(listversion)
640 661 seen.append(setversion)
641 662 final.reverse() # put small successors set first
642 663 cache[current] = final
643 664 return cache[initialnode]
644 665
645 666 def _knownrevs(repo, nodes):
646 667 """yield revision numbers of known nodes passed in parameters
647 668
648 669 Unknown revisions are silently ignored."""
649 670 torev = repo.changelog.nodemap.get
650 671 for n in nodes:
651 672 rev = torev(n)
652 673 if rev is not None:
653 674 yield rev
654 675
655 676 # mapping of 'set-name' -> <function to compute this set>
656 677 cachefuncs = {}
657 678 def cachefor(name):
658 679 """Decorator to register a function as computing the cache for a set"""
659 680 def decorator(func):
660 681 assert name not in cachefuncs
661 682 cachefuncs[name] = func
662 683 return func
663 684 return decorator
664 685
665 686 def getrevs(repo, name):
666 687 """Return the set of revision that belong to the <name> set
667 688
668 689 Such access may compute the set and cache it for future use"""
669 690 repo = repo.unfiltered()
670 691 if not repo.obsstore:
671 692 return ()
672 693 if name not in repo.obsstore.caches:
673 694 repo.obsstore.caches[name] = cachefuncs[name](repo)
674 695 return repo.obsstore.caches[name]
675 696
676 697 # To be simple we need to invalidate obsolescence cache when:
677 698 #
678 699 # - new changeset is added:
679 700 # - public phase is changed
680 701 # - obsolescence marker are added
681 702 # - strip is used a repo
682 703 def clearobscaches(repo):
683 704 """Remove all obsolescence related cache from a repo
684 705
685 706 This remove all cache in obsstore is the obsstore already exist on the
686 707 repo.
687 708
688 709 (We could be smarter here given the exact event that trigger the cache
689 710 clearing)"""
690 711 # only clear cache is there is obsstore data in this repo
691 712 if 'obsstore' in repo._filecache:
692 713 repo.obsstore.caches.clear()
693 714
694 715 @cachefor('obsolete')
695 716 def _computeobsoleteset(repo):
696 717 """the set of obsolete revisions"""
697 718 obs = set()
698 719 getrev = repo.changelog.nodemap.get
699 720 getphase = repo._phasecache.phase
700 721 for node in repo.obsstore.successors:
701 722 rev = getrev(node)
702 723 if rev is not None and getphase(repo, rev):
703 724 obs.add(rev)
704 725 return obs
705 726
706 727 @cachefor('unstable')
707 728 def _computeunstableset(repo):
708 729 """the set of non obsolete revisions with obsolete parents"""
709 730 # revset is not efficient enough here
710 731 # we do (obsolete()::) - obsolete() by hand
711 732 obs = getrevs(repo, 'obsolete')
712 733 if not obs:
713 734 return set()
714 735 cl = repo.changelog
715 736 return set(r for r in cl.descendants(obs) if r not in obs)
716 737
717 738 @cachefor('suspended')
718 739 def _computesuspendedset(repo):
719 740 """the set of obsolete parents with non obsolete descendants"""
720 741 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
721 742 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
722 743
723 744 @cachefor('extinct')
724 745 def _computeextinctset(repo):
725 746 """the set of obsolete parents without non obsolete descendants"""
726 747 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
727 748
728 749
729 750 @cachefor('bumped')
730 751 def _computebumpedset(repo):
731 752 """the set of revs trying to obsolete public revisions"""
732 753 # get all possible bumped changesets
733 754 tonode = repo.changelog.node
734 755 publicnodes = (tonode(r) for r in repo.revs('public()'))
735 756 successors = allsuccessors(repo.obsstore, publicnodes,
736 757 ignoreflags=bumpedfix)
737 758 # revision public or already obsolete don't count as bumped
738 759 query = '%ld - obsolete() - public()'
739 760 return set(repo.revs(query, _knownrevs(repo, successors)))
740 761
741 762 @cachefor('divergent')
742 763 def _computedivergentset(repo):
743 764 """the set of rev that compete to be the final successors of some revision.
744 765 """
745 766 divergent = set()
746 767 obsstore = repo.obsstore
747 768 newermap = {}
748 769 for ctx in repo.set('(not public()) - obsolete()'):
749 770 mark = obsstore.precursors.get(ctx.node(), ())
750 771 toprocess = set(mark)
751 772 while toprocess:
752 773 prec = toprocess.pop()[0]
753 774 if prec not in newermap:
754 775 successorssets(repo, prec, newermap)
755 776 newer = [n for n in newermap[prec] if n]
756 777 if len(newer) > 1:
757 778 divergent.add(ctx.rev())
758 779 break
759 780 toprocess.update(obsstore.precursors.get(prec, ()))
760 781 return divergent
761 782
762 783
763 784 def createmarkers(repo, relations, flag=0, metadata=None):
764 785 """Add obsolete markers between changesets in a repo
765 786
766 787 <relations> must be an iterable of (<old>, (<new>, ...)) tuple.
767 788 `old` and `news` are changectx.
768 789
769 790 Trying to obsolete a public changeset will raise an exception.
770 791
771 792 Current user and date are used except if specified otherwise in the
772 793 metadata attribute.
773 794
774 795 This function operates within a transaction of its own, but does
775 796 not take any lock on the repo.
776 797 """
777 798 # prepare metadata
778 799 if metadata is None:
779 800 metadata = {}
780 801 if 'date' not in metadata:
781 802 metadata['date'] = '%i %i' % util.makedate()
782 803 if 'user' not in metadata:
783 804 metadata['user'] = repo.ui.username()
784 805 tr = repo.transaction('add-obsolescence-marker')
785 806 try:
786 807 for prec, sucs in relations:
787 808 if not prec.mutable():
788 809 raise util.Abort("cannot obsolete immutable changeset: %s"
789 810 % prec)
790 811 nprec = prec.node()
791 812 nsucs = tuple(s.node() for s in sucs)
792 813 if nprec in nsucs:
793 814 raise util.Abort("changeset %s cannot obsolete itself" % prec)
794 815 repo.obsstore.create(tr, nprec, nsucs, flag, metadata)
795 816 repo.filteredrevcache.clear()
796 817 tr.close()
797 818 finally:
798 819 tr.release()
General Comments 0
You need to be logged in to leave comments. Login now