##// END OF EJS Templates
changelog: handle writepending in the transaction...
Pierre-Yves David -
r23203:3872d563 default
parent child Browse files
Show More
@@ -1,832 +1,832 b''
1 1 # changegroup.py - Mercurial changegroup manipulation functions
2 2 #
3 3 # Copyright 2006 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
8 8 import weakref
9 9 from i18n import _
10 10 from node import nullrev, nullid, hex, short
11 11 import mdiff, util, dagutil
12 12 import struct, os, bz2, zlib, tempfile
13 13 import discovery, error, phases, branchmap
14 14
15 15 _CHANGEGROUPV1_DELTA_HEADER = "20s20s20s20s"
16 16 _CHANGEGROUPV2_DELTA_HEADER = "20s20s20s20s20s"
17 17
18 18 def readexactly(stream, n):
19 19 '''read n bytes from stream.read and abort if less was available'''
20 20 s = stream.read(n)
21 21 if len(s) < n:
22 22 raise util.Abort(_("stream ended unexpectedly"
23 23 " (got %d bytes, expected %d)")
24 24 % (len(s), n))
25 25 return s
26 26
27 27 def getchunk(stream):
28 28 """return the next chunk from stream as a string"""
29 29 d = readexactly(stream, 4)
30 30 l = struct.unpack(">l", d)[0]
31 31 if l <= 4:
32 32 if l:
33 33 raise util.Abort(_("invalid chunk length %d") % l)
34 34 return ""
35 35 return readexactly(stream, l - 4)
36 36
37 37 def chunkheader(length):
38 38 """return a changegroup chunk header (string)"""
39 39 return struct.pack(">l", length + 4)
40 40
41 41 def closechunk():
42 42 """return a changegroup chunk header (string) for a zero-length chunk"""
43 43 return struct.pack(">l", 0)
44 44
45 45 class nocompress(object):
46 46 def compress(self, x):
47 47 return x
48 48 def flush(self):
49 49 return ""
50 50
51 51 bundletypes = {
52 52 "": ("", nocompress), # only when using unbundle on ssh and old http servers
53 53 # since the unification ssh accepts a header but there
54 54 # is no capability signaling it.
55 55 "HG10UN": ("HG10UN", nocompress),
56 56 "HG10BZ": ("HG10", lambda: bz2.BZ2Compressor()),
57 57 "HG10GZ": ("HG10GZ", lambda: zlib.compressobj()),
58 58 }
59 59
60 60 # hgweb uses this list to communicate its preferred type
61 61 bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN']
62 62
63 63 def writebundle(cg, filename, bundletype, vfs=None):
64 64 """Write a bundle file and return its filename.
65 65
66 66 Existing files will not be overwritten.
67 67 If no filename is specified, a temporary file is created.
68 68 bz2 compression can be turned off.
69 69 The bundle file will be deleted in case of errors.
70 70 """
71 71
72 72 fh = None
73 73 cleanup = None
74 74 try:
75 75 if filename:
76 76 if vfs:
77 77 fh = vfs.open(filename, "wb")
78 78 else:
79 79 fh = open(filename, "wb")
80 80 else:
81 81 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
82 82 fh = os.fdopen(fd, "wb")
83 83 cleanup = filename
84 84
85 85 header, compressor = bundletypes[bundletype]
86 86 fh.write(header)
87 87 z = compressor()
88 88
89 89 # parse the changegroup data, otherwise we will block
90 90 # in case of sshrepo because we don't know the end of the stream
91 91
92 92 # an empty chunkgroup is the end of the changegroup
93 93 # a changegroup has at least 2 chunkgroups (changelog and manifest).
94 94 # after that, an empty chunkgroup is the end of the changegroup
95 95 for chunk in cg.getchunks():
96 96 fh.write(z.compress(chunk))
97 97 fh.write(z.flush())
98 98 cleanup = None
99 99 return filename
100 100 finally:
101 101 if fh is not None:
102 102 fh.close()
103 103 if cleanup is not None:
104 104 if filename and vfs:
105 105 vfs.unlink(cleanup)
106 106 else:
107 107 os.unlink(cleanup)
108 108
109 109 def decompressor(fh, alg):
110 110 if alg == 'UN':
111 111 return fh
112 112 elif alg == 'GZ':
113 113 def generator(f):
114 114 zd = zlib.decompressobj()
115 115 for chunk in util.filechunkiter(f):
116 116 yield zd.decompress(chunk)
117 117 elif alg == 'BZ':
118 118 def generator(f):
119 119 zd = bz2.BZ2Decompressor()
120 120 zd.decompress("BZ")
121 121 for chunk in util.filechunkiter(f, 4096):
122 122 yield zd.decompress(chunk)
123 123 else:
124 124 raise util.Abort("unknown bundle compression '%s'" % alg)
125 125 return util.chunkbuffer(generator(fh))
126 126
127 127 class cg1unpacker(object):
128 128 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
129 129 deltaheadersize = struct.calcsize(deltaheader)
130 130 def __init__(self, fh, alg):
131 131 self._stream = decompressor(fh, alg)
132 132 self._type = alg
133 133 self.callback = None
134 134 def compressed(self):
135 135 return self._type != 'UN'
136 136 def read(self, l):
137 137 return self._stream.read(l)
138 138 def seek(self, pos):
139 139 return self._stream.seek(pos)
140 140 def tell(self):
141 141 return self._stream.tell()
142 142 def close(self):
143 143 return self._stream.close()
144 144
145 145 def chunklength(self):
146 146 d = readexactly(self._stream, 4)
147 147 l = struct.unpack(">l", d)[0]
148 148 if l <= 4:
149 149 if l:
150 150 raise util.Abort(_("invalid chunk length %d") % l)
151 151 return 0
152 152 if self.callback:
153 153 self.callback()
154 154 return l - 4
155 155
156 156 def changelogheader(self):
157 157 """v10 does not have a changelog header chunk"""
158 158 return {}
159 159
160 160 def manifestheader(self):
161 161 """v10 does not have a manifest header chunk"""
162 162 return {}
163 163
164 164 def filelogheader(self):
165 165 """return the header of the filelogs chunk, v10 only has the filename"""
166 166 l = self.chunklength()
167 167 if not l:
168 168 return {}
169 169 fname = readexactly(self._stream, l)
170 170 return {'filename': fname}
171 171
172 172 def _deltaheader(self, headertuple, prevnode):
173 173 node, p1, p2, cs = headertuple
174 174 if prevnode is None:
175 175 deltabase = p1
176 176 else:
177 177 deltabase = prevnode
178 178 return node, p1, p2, deltabase, cs
179 179
180 180 def deltachunk(self, prevnode):
181 181 l = self.chunklength()
182 182 if not l:
183 183 return {}
184 184 headerdata = readexactly(self._stream, self.deltaheadersize)
185 185 header = struct.unpack(self.deltaheader, headerdata)
186 186 delta = readexactly(self._stream, l - self.deltaheadersize)
187 187 node, p1, p2, deltabase, cs = self._deltaheader(header, prevnode)
188 188 return {'node': node, 'p1': p1, 'p2': p2, 'cs': cs,
189 189 'deltabase': deltabase, 'delta': delta}
190 190
191 191 def getchunks(self):
192 192 """returns all the chunks contains in the bundle
193 193
194 194 Used when you need to forward the binary stream to a file or another
195 195 network API. To do so, it parse the changegroup data, otherwise it will
196 196 block in case of sshrepo because it don't know the end of the stream.
197 197 """
198 198 # an empty chunkgroup is the end of the changegroup
199 199 # a changegroup has at least 2 chunkgroups (changelog and manifest).
200 200 # after that, an empty chunkgroup is the end of the changegroup
201 201 empty = False
202 202 count = 0
203 203 while not empty or count <= 2:
204 204 empty = True
205 205 count += 1
206 206 while True:
207 207 chunk = getchunk(self)
208 208 if not chunk:
209 209 break
210 210 empty = False
211 211 yield chunkheader(len(chunk))
212 212 pos = 0
213 213 while pos < len(chunk):
214 214 next = pos + 2**20
215 215 yield chunk[pos:next]
216 216 pos = next
217 217 yield closechunk()
218 218
219 219 class cg2unpacker(cg1unpacker):
220 220 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
221 221 deltaheadersize = struct.calcsize(deltaheader)
222 222
223 223 def _deltaheader(self, headertuple, prevnode):
224 224 node, p1, p2, deltabase, cs = headertuple
225 225 return node, p1, p2, deltabase, cs
226 226
227 227 class headerlessfixup(object):
228 228 def __init__(self, fh, h):
229 229 self._h = h
230 230 self._fh = fh
231 231 def read(self, n):
232 232 if self._h:
233 233 d, self._h = self._h[:n], self._h[n:]
234 234 if len(d) < n:
235 235 d += readexactly(self._fh, n - len(d))
236 236 return d
237 237 return readexactly(self._fh, n)
238 238
239 239 class cg1packer(object):
240 240 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
241 241 def __init__(self, repo, bundlecaps=None):
242 242 """Given a source repo, construct a bundler.
243 243
244 244 bundlecaps is optional and can be used to specify the set of
245 245 capabilities which can be used to build the bundle.
246 246 """
247 247 # Set of capabilities we can use to build the bundle.
248 248 if bundlecaps is None:
249 249 bundlecaps = set()
250 250 self._bundlecaps = bundlecaps
251 251 self._changelog = repo.changelog
252 252 self._manifest = repo.manifest
253 253 reorder = repo.ui.config('bundle', 'reorder', 'auto')
254 254 if reorder == 'auto':
255 255 reorder = None
256 256 else:
257 257 reorder = util.parsebool(reorder)
258 258 self._repo = repo
259 259 self._reorder = reorder
260 260 self._progress = repo.ui.progress
261 261 def close(self):
262 262 return closechunk()
263 263
264 264 def fileheader(self, fname):
265 265 return chunkheader(len(fname)) + fname
266 266
267 267 def group(self, nodelist, revlog, lookup, units=None, reorder=None):
268 268 """Calculate a delta group, yielding a sequence of changegroup chunks
269 269 (strings).
270 270
271 271 Given a list of changeset revs, return a set of deltas and
272 272 metadata corresponding to nodes. The first delta is
273 273 first parent(nodelist[0]) -> nodelist[0], the receiver is
274 274 guaranteed to have this parent as it has all history before
275 275 these changesets. In the case firstparent is nullrev the
276 276 changegroup starts with a full revision.
277 277
278 278 If units is not None, progress detail will be generated, units specifies
279 279 the type of revlog that is touched (changelog, manifest, etc.).
280 280 """
281 281 # if we don't have any revisions touched by these changesets, bail
282 282 if len(nodelist) == 0:
283 283 yield self.close()
284 284 return
285 285
286 286 # for generaldelta revlogs, we linearize the revs; this will both be
287 287 # much quicker and generate a much smaller bundle
288 288 if (revlog._generaldelta and reorder is not False) or reorder:
289 289 dag = dagutil.revlogdag(revlog)
290 290 revs = set(revlog.rev(n) for n in nodelist)
291 291 revs = dag.linearize(revs)
292 292 else:
293 293 revs = sorted([revlog.rev(n) for n in nodelist])
294 294
295 295 # add the parent of the first rev
296 296 p = revlog.parentrevs(revs[0])[0]
297 297 revs.insert(0, p)
298 298
299 299 # build deltas
300 300 total = len(revs) - 1
301 301 msgbundling = _('bundling')
302 302 for r in xrange(len(revs) - 1):
303 303 if units is not None:
304 304 self._progress(msgbundling, r + 1, unit=units, total=total)
305 305 prev, curr = revs[r], revs[r + 1]
306 306 linknode = lookup(revlog.node(curr))
307 307 for c in self.revchunk(revlog, curr, prev, linknode):
308 308 yield c
309 309
310 310 yield self.close()
311 311
312 312 # filter any nodes that claim to be part of the known set
313 313 def prune(self, revlog, missing, commonrevs, source):
314 314 rr, rl = revlog.rev, revlog.linkrev
315 315 return [n for n in missing if rl(rr(n)) not in commonrevs]
316 316
317 317 def generate(self, commonrevs, clnodes, fastpathlinkrev, source):
318 318 '''yield a sequence of changegroup chunks (strings)'''
319 319 repo = self._repo
320 320 cl = self._changelog
321 321 mf = self._manifest
322 322 reorder = self._reorder
323 323 progress = self._progress
324 324
325 325 # for progress output
326 326 msgbundling = _('bundling')
327 327
328 328 mfs = {} # needed manifests
329 329 fnodes = {} # needed file nodes
330 330 changedfiles = set()
331 331
332 332 # Callback for the changelog, used to collect changed files and manifest
333 333 # nodes.
334 334 # Returns the linkrev node (identity in the changelog case).
335 335 def lookupcl(x):
336 336 c = cl.read(x)
337 337 changedfiles.update(c[3])
338 338 # record the first changeset introducing this manifest version
339 339 mfs.setdefault(c[0], x)
340 340 return x
341 341
342 342 # Callback for the manifest, used to collect linkrevs for filelog
343 343 # revisions.
344 344 # Returns the linkrev node (collected in lookupcl).
345 345 def lookupmf(x):
346 346 clnode = mfs[x]
347 347 if not fastpathlinkrev:
348 348 mdata = mf.readfast(x)
349 349 for f, n in mdata.iteritems():
350 350 if f in changedfiles:
351 351 # record the first changeset introducing this filelog
352 352 # version
353 353 fnodes[f].setdefault(n, clnode)
354 354 return clnode
355 355
356 356 for chunk in self.group(clnodes, cl, lookupcl, units=_('changesets'),
357 357 reorder=reorder):
358 358 yield chunk
359 359 progress(msgbundling, None)
360 360
361 361 for f in changedfiles:
362 362 fnodes[f] = {}
363 363 mfnodes = self.prune(mf, mfs, commonrevs, source)
364 364 for chunk in self.group(mfnodes, mf, lookupmf, units=_('manifests'),
365 365 reorder=reorder):
366 366 yield chunk
367 367 progress(msgbundling, None)
368 368
369 369 mfs.clear()
370 370 needed = set(cl.rev(x) for x in clnodes)
371 371
372 372 def linknodes(filerevlog, fname):
373 373 if fastpathlinkrev:
374 374 llr = filerevlog.linkrev
375 375 def genfilenodes():
376 376 for r in filerevlog:
377 377 linkrev = llr(r)
378 378 if linkrev in needed:
379 379 yield filerevlog.node(r), cl.node(linkrev)
380 380 fnodes[fname] = dict(genfilenodes())
381 381 return fnodes.get(fname, {})
382 382
383 383 for chunk in self.generatefiles(changedfiles, linknodes, commonrevs,
384 384 source):
385 385 yield chunk
386 386
387 387 yield self.close()
388 388 progress(msgbundling, None)
389 389
390 390 if clnodes:
391 391 repo.hook('outgoing', node=hex(clnodes[0]), source=source)
392 392
393 393 def generatefiles(self, changedfiles, linknodes, commonrevs, source):
394 394 repo = self._repo
395 395 progress = self._progress
396 396 reorder = self._reorder
397 397 msgbundling = _('bundling')
398 398
399 399 total = len(changedfiles)
400 400 # for progress output
401 401 msgfiles = _('files')
402 402 for i, fname in enumerate(sorted(changedfiles)):
403 403 filerevlog = repo.file(fname)
404 404 if not filerevlog:
405 405 raise util.Abort(_("empty or missing revlog for %s") % fname)
406 406
407 407 linkrevnodes = linknodes(filerevlog, fname)
408 408 # Lookup for filenodes, we collected the linkrev nodes above in the
409 409 # fastpath case and with lookupmf in the slowpath case.
410 410 def lookupfilelog(x):
411 411 return linkrevnodes[x]
412 412
413 413 filenodes = self.prune(filerevlog, linkrevnodes, commonrevs, source)
414 414 if filenodes:
415 415 progress(msgbundling, i + 1, item=fname, unit=msgfiles,
416 416 total=total)
417 417 yield self.fileheader(fname)
418 418 for chunk in self.group(filenodes, filerevlog, lookupfilelog,
419 419 reorder=reorder):
420 420 yield chunk
421 421
422 422 def deltaparent(self, revlog, rev, p1, p2, prev):
423 423 return prev
424 424
425 425 def revchunk(self, revlog, rev, prev, linknode):
426 426 node = revlog.node(rev)
427 427 p1, p2 = revlog.parentrevs(rev)
428 428 base = self.deltaparent(revlog, rev, p1, p2, prev)
429 429
430 430 prefix = ''
431 431 if base == nullrev:
432 432 delta = revlog.revision(node)
433 433 prefix = mdiff.trivialdiffheader(len(delta))
434 434 else:
435 435 delta = revlog.revdiff(base, rev)
436 436 p1n, p2n = revlog.parents(node)
437 437 basenode = revlog.node(base)
438 438 meta = self.builddeltaheader(node, p1n, p2n, basenode, linknode)
439 439 meta += prefix
440 440 l = len(meta) + len(delta)
441 441 yield chunkheader(l)
442 442 yield meta
443 443 yield delta
444 444 def builddeltaheader(self, node, p1n, p2n, basenode, linknode):
445 445 # do nothing with basenode, it is implicitly the previous one in HG10
446 446 return struct.pack(self.deltaheader, node, p1n, p2n, linknode)
447 447
448 448 class cg2packer(cg1packer):
449 449
450 450 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
451 451
452 452 def group(self, nodelist, revlog, lookup, units=None, reorder=None):
453 453 if (revlog._generaldelta and reorder is not True):
454 454 reorder = False
455 455 return cg1packer.group(self, nodelist, revlog, lookup,
456 456 units=units, reorder=reorder)
457 457
458 458 def deltaparent(self, revlog, rev, p1, p2, prev):
459 459 dp = revlog.deltaparent(rev)
460 460 # avoid storing full revisions; pick prev in those cases
461 461 # also pick prev when we can't be sure remote has dp
462 462 if dp == nullrev or (dp != p1 and dp != p2 and dp != prev):
463 463 return prev
464 464 return dp
465 465
466 466 def builddeltaheader(self, node, p1n, p2n, basenode, linknode):
467 467 return struct.pack(self.deltaheader, node, p1n, p2n, basenode, linknode)
468 468
469 469 packermap = {'01': (cg1packer, cg1unpacker),
470 470 '02': (cg2packer, cg2unpacker)}
471 471
472 472 def _changegroupinfo(repo, nodes, source):
473 473 if repo.ui.verbose or source == 'bundle':
474 474 repo.ui.status(_("%d changesets found\n") % len(nodes))
475 475 if repo.ui.debugflag:
476 476 repo.ui.debug("list of changesets:\n")
477 477 for node in nodes:
478 478 repo.ui.debug("%s\n" % hex(node))
479 479
480 480 def getsubsetraw(repo, outgoing, bundler, source, fastpath=False):
481 481 repo = repo.unfiltered()
482 482 commonrevs = outgoing.common
483 483 csets = outgoing.missing
484 484 heads = outgoing.missingheads
485 485 # We go through the fast path if we get told to, or if all (unfiltered
486 486 # heads have been requested (since we then know there all linkrevs will
487 487 # be pulled by the client).
488 488 heads.sort()
489 489 fastpathlinkrev = fastpath or (
490 490 repo.filtername is None and heads == sorted(repo.heads()))
491 491
492 492 repo.hook('preoutgoing', throw=True, source=source)
493 493 _changegroupinfo(repo, csets, source)
494 494 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
495 495
496 496 def getsubset(repo, outgoing, bundler, source, fastpath=False):
497 497 gengroup = getsubsetraw(repo, outgoing, bundler, source, fastpath)
498 498 return cg1unpacker(util.chunkbuffer(gengroup), 'UN')
499 499
500 500 def changegroupsubset(repo, roots, heads, source):
501 501 """Compute a changegroup consisting of all the nodes that are
502 502 descendants of any of the roots and ancestors of any of the heads.
503 503 Return a chunkbuffer object whose read() method will return
504 504 successive changegroup chunks.
505 505
506 506 It is fairly complex as determining which filenodes and which
507 507 manifest nodes need to be included for the changeset to be complete
508 508 is non-trivial.
509 509
510 510 Another wrinkle is doing the reverse, figuring out which changeset in
511 511 the changegroup a particular filenode or manifestnode belongs to.
512 512 """
513 513 cl = repo.changelog
514 514 if not roots:
515 515 roots = [nullid]
516 516 # TODO: remove call to nodesbetween.
517 517 csets, roots, heads = cl.nodesbetween(roots, heads)
518 518 discbases = []
519 519 for n in roots:
520 520 discbases.extend([p for p in cl.parents(n) if p != nullid])
521 521 outgoing = discovery.outgoing(cl, discbases, heads)
522 522 bundler = cg1packer(repo)
523 523 return getsubset(repo, outgoing, bundler, source)
524 524
525 525 def getlocalchangegroupraw(repo, source, outgoing, bundlecaps=None,
526 526 version='01'):
527 527 """Like getbundle, but taking a discovery.outgoing as an argument.
528 528
529 529 This is only implemented for local repos and reuses potentially
530 530 precomputed sets in outgoing. Returns a raw changegroup generator."""
531 531 if not outgoing.missing:
532 532 return None
533 533 bundler = packermap[version][0](repo, bundlecaps)
534 534 return getsubsetraw(repo, outgoing, bundler, source)
535 535
536 536 def getlocalchangegroup(repo, source, outgoing, bundlecaps=None):
537 537 """Like getbundle, but taking a discovery.outgoing as an argument.
538 538
539 539 This is only implemented for local repos and reuses potentially
540 540 precomputed sets in outgoing."""
541 541 if not outgoing.missing:
542 542 return None
543 543 bundler = cg1packer(repo, bundlecaps)
544 544 return getsubset(repo, outgoing, bundler, source)
545 545
546 546 def _computeoutgoing(repo, heads, common):
547 547 """Computes which revs are outgoing given a set of common
548 548 and a set of heads.
549 549
550 550 This is a separate function so extensions can have access to
551 551 the logic.
552 552
553 553 Returns a discovery.outgoing object.
554 554 """
555 555 cl = repo.changelog
556 556 if common:
557 557 hasnode = cl.hasnode
558 558 common = [n for n in common if hasnode(n)]
559 559 else:
560 560 common = [nullid]
561 561 if not heads:
562 562 heads = cl.heads()
563 563 return discovery.outgoing(cl, common, heads)
564 564
565 565 def getchangegroupraw(repo, source, heads=None, common=None, bundlecaps=None,
566 566 version='01'):
567 567 """Like changegroupsubset, but returns the set difference between the
568 568 ancestors of heads and the ancestors common.
569 569
570 570 If heads is None, use the local heads. If common is None, use [nullid].
571 571
572 572 If version is None, use a version '1' changegroup.
573 573
574 574 The nodes in common might not all be known locally due to the way the
575 575 current discovery protocol works. Returns a raw changegroup generator.
576 576 """
577 577 outgoing = _computeoutgoing(repo, heads, common)
578 578 return getlocalchangegroupraw(repo, source, outgoing, bundlecaps=bundlecaps,
579 579 version=version)
580 580
581 581 def getchangegroup(repo, source, heads=None, common=None, bundlecaps=None):
582 582 """Like changegroupsubset, but returns the set difference between the
583 583 ancestors of heads and the ancestors common.
584 584
585 585 If heads is None, use the local heads. If common is None, use [nullid].
586 586
587 587 The nodes in common might not all be known locally due to the way the
588 588 current discovery protocol works.
589 589 """
590 590 outgoing = _computeoutgoing(repo, heads, common)
591 591 return getlocalchangegroup(repo, source, outgoing, bundlecaps=bundlecaps)
592 592
593 593 def changegroup(repo, basenodes, source):
594 594 # to avoid a race we use changegroupsubset() (issue1320)
595 595 return changegroupsubset(repo, basenodes, repo.heads(), source)
596 596
597 597 def addchangegroupfiles(repo, source, revmap, trp, pr, needfiles):
598 598 revisions = 0
599 599 files = 0
600 600 while True:
601 601 chunkdata = source.filelogheader()
602 602 if not chunkdata:
603 603 break
604 604 f = chunkdata["filename"]
605 605 repo.ui.debug("adding %s revisions\n" % f)
606 606 pr()
607 607 fl = repo.file(f)
608 608 o = len(fl)
609 609 if not fl.addgroup(source, revmap, trp):
610 610 raise util.Abort(_("received file revlog group is empty"))
611 611 revisions += len(fl) - o
612 612 files += 1
613 613 if f in needfiles:
614 614 needs = needfiles[f]
615 615 for new in xrange(o, len(fl)):
616 616 n = fl.node(new)
617 617 if n in needs:
618 618 needs.remove(n)
619 619 else:
620 620 raise util.Abort(
621 621 _("received spurious file revlog entry"))
622 622 if not needs:
623 623 del needfiles[f]
624 624 repo.ui.progress(_('files'), None)
625 625
626 626 for f, needs in needfiles.iteritems():
627 627 fl = repo.file(f)
628 628 for n in needs:
629 629 try:
630 630 fl.rev(n)
631 631 except error.LookupError:
632 632 raise util.Abort(
633 633 _('missing file data for %s:%s - run hg verify') %
634 634 (f, hex(n)))
635 635
636 636 return revisions, files
637 637
638 638 def addchangegroup(repo, source, srctype, url, emptyok=False,
639 639 targetphase=phases.draft):
640 640 """Add the changegroup returned by source.read() to this repo.
641 641 srctype is a string like 'push', 'pull', or 'unbundle'. url is
642 642 the URL of the repo where this changegroup is coming from.
643 643
644 644 Return an integer summarizing the change to this repo:
645 645 - nothing changed or no source: 0
646 646 - more heads than before: 1+added heads (2..n)
647 647 - fewer heads than before: -1-removed heads (-2..-n)
648 648 - number of heads stays the same: 1
649 649 """
650 650 repo = repo.unfiltered()
651 651 def csmap(x):
652 652 repo.ui.debug("add changeset %s\n" % short(x))
653 653 return len(cl)
654 654
655 655 def revmap(x):
656 656 return cl.rev(x)
657 657
658 658 if not source:
659 659 return 0
660 660
661 661 changesets = files = revisions = 0
662 662 efiles = set()
663 663
664 # write changelog data to temp files so concurrent readers will not see
665 # inconsistent view
666 cl = repo.changelog
667 cl.delayupdate()
668 oldheads = cl.heads()
669
670 664 tr = repo.transaction("\n".join([srctype, util.hidepassword(url)]))
671 665 # The transaction could have been created before and already carries source
672 666 # information. In this case we use the top level data. We overwrite the
673 667 # argument because we need to use the top level value (if they exist) in
674 668 # this function.
675 669 srctype = tr.hookargs.setdefault('source', srctype)
676 670 url = tr.hookargs.setdefault('url', url)
671
672 # write changelog data to temp files so concurrent readers will not see
673 # inconsistent view
674 cl = repo.changelog
675 cl.delayupdate(tr)
676 oldheads = cl.heads()
677 677 try:
678 678 repo.hook('prechangegroup', throw=True, **tr.hookargs)
679 679
680 680 trp = weakref.proxy(tr)
681 681 # pull off the changeset group
682 682 repo.ui.status(_("adding changesets\n"))
683 683 clstart = len(cl)
684 684 class prog(object):
685 685 step = _('changesets')
686 686 count = 1
687 687 ui = repo.ui
688 688 total = None
689 689 def __call__(repo):
690 690 repo.ui.progress(repo.step, repo.count, unit=_('chunks'),
691 691 total=repo.total)
692 692 repo.count += 1
693 693 pr = prog()
694 694 source.callback = pr
695 695
696 696 source.changelogheader()
697 697 srccontent = cl.addgroup(source, csmap, trp)
698 698 if not (srccontent or emptyok):
699 699 raise util.Abort(_("received changelog group is empty"))
700 700 clend = len(cl)
701 701 changesets = clend - clstart
702 702 for c in xrange(clstart, clend):
703 703 efiles.update(repo[c].files())
704 704 efiles = len(efiles)
705 705 repo.ui.progress(_('changesets'), None)
706 706
707 707 # pull off the manifest group
708 708 repo.ui.status(_("adding manifests\n"))
709 709 pr.step = _('manifests')
710 710 pr.count = 1
711 711 pr.total = changesets # manifests <= changesets
712 712 # no need to check for empty manifest group here:
713 713 # if the result of the merge of 1 and 2 is the same in 3 and 4,
714 714 # no new manifest will be created and the manifest group will
715 715 # be empty during the pull
716 716 source.manifestheader()
717 717 repo.manifest.addgroup(source, revmap, trp)
718 718 repo.ui.progress(_('manifests'), None)
719 719
720 720 needfiles = {}
721 721 if repo.ui.configbool('server', 'validate', default=False):
722 722 # validate incoming csets have their manifests
723 723 for cset in xrange(clstart, clend):
724 724 mfest = repo.changelog.read(repo.changelog.node(cset))[0]
725 725 mfest = repo.manifest.readdelta(mfest)
726 726 # store file nodes we must see
727 727 for f, n in mfest.iteritems():
728 728 needfiles.setdefault(f, set()).add(n)
729 729
730 730 # process the files
731 731 repo.ui.status(_("adding file changes\n"))
732 732 pr.step = _('files')
733 733 pr.count = 1
734 734 pr.total = efiles
735 735 source.callback = None
736 736
737 737 newrevs, newfiles = addchangegroupfiles(repo, source, revmap, trp, pr,
738 738 needfiles)
739 739 revisions += newrevs
740 740 files += newfiles
741 741
742 742 dh = 0
743 743 if oldheads:
744 744 heads = cl.heads()
745 745 dh = len(heads) - len(oldheads)
746 746 for h in heads:
747 747 if h not in oldheads and repo[h].closesbranch():
748 748 dh -= 1
749 749 htext = ""
750 750 if dh:
751 751 htext = _(" (%+d heads)") % dh
752 752
753 753 repo.ui.status(_("added %d changesets"
754 754 " with %d changes to %d files%s\n")
755 755 % (changesets, revisions, files, htext))
756 756 repo.invalidatevolatilesets()
757 757
758 758 if changesets > 0:
759 p = lambda: cl.writepending() and repo.root or ""
759 p = lambda: tr.writepending() and repo.root or ""
760 760 if 'node' not in tr.hookargs:
761 761 tr.hookargs['node'] = hex(cl.node(clstart))
762 762 hookargs = dict(tr.hookargs)
763 763 else:
764 764 hookargs = dict(tr.hookargs)
765 765 hookargs['node'] = hex(cl.node(clstart))
766 766 repo.hook('pretxnchangegroup', throw=True, pending=p, **hookargs)
767 767
768 768 added = [cl.node(r) for r in xrange(clstart, clend)]
769 769 publishing = repo.ui.configbool('phases', 'publish', True)
770 770 if srctype in ('push', 'serve'):
771 771 # Old servers can not push the boundary themselves.
772 772 # New servers won't push the boundary if changeset already
773 773 # exists locally as secret
774 774 #
775 775 # We should not use added here but the list of all change in
776 776 # the bundle
777 777 if publishing:
778 778 phases.advanceboundary(repo, tr, phases.public, srccontent)
779 779 else:
780 780 # Those changesets have been pushed from the outside, their
781 781 # phases are going to be pushed alongside. Therefor
782 782 # `targetphase` is ignored.
783 783 phases.advanceboundary(repo, tr, phases.draft, srccontent)
784 784 phases.retractboundary(repo, tr, phases.draft, added)
785 785 elif srctype != 'strip':
786 786 # publishing only alter behavior during push
787 787 #
788 788 # strip should not touch boundary at all
789 789 phases.retractboundary(repo, tr, targetphase, added)
790 790
791 791 # make changelog see real files again
792 792 cl.finalize(trp)
793 793
794 794 tr.close()
795 795
796 796 if changesets > 0:
797 797 if srctype != 'strip':
798 798 # During strip, branchcache is invalid but coming call to
799 799 # `destroyed` will repair it.
800 800 # In other case we can safely update cache on disk.
801 801 branchmap.updatecache(repo.filtered('served'))
802 802
803 803 def runhooks():
804 804 # These hooks run when the lock releases, not when the
805 805 # transaction closes. So it's possible for the changelog
806 806 # to have changed since we last saw it.
807 807 if clstart >= len(repo):
808 808 return
809 809
810 810 # forcefully update the on-disk branch cache
811 811 repo.ui.debug("updating the branch cache\n")
812 812 repo.hook("changegroup", **hookargs)
813 813
814 814 for n in added:
815 815 args = hookargs.copy()
816 816 args['node'] = hex(n)
817 817 repo.hook("incoming", **args)
818 818
819 819 newheads = [h for h in repo.heads() if h not in oldheads]
820 820 repo.ui.log("incoming",
821 821 "%s incoming changes - new heads: %s\n",
822 822 len(added),
823 823 ', '.join([hex(c[:6]) for c in newheads]))
824 824 repo._afterlock(runhooks)
825 825
826 826 finally:
827 827 tr.release()
828 828 # never return 0 here:
829 829 if dh < 0:
830 830 return dh - 1
831 831 else:
832 832 return dh + 1
@@ -1,374 +1,375 b''
1 1 # changelog.py - changelog 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
8 8 from node import bin, hex, nullid
9 9 from i18n import _
10 10 import util, error, revlog, encoding
11 11
12 12 _defaultextra = {'branch': 'default'}
13 13
14 14 def _string_escape(text):
15 15 """
16 16 >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
17 17 >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
18 18 >>> s
19 19 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
20 20 >>> res = _string_escape(s)
21 21 >>> s == res.decode('string_escape')
22 22 True
23 23 """
24 24 # subset of the string_escape codec
25 25 text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
26 26 return text.replace('\0', '\\0')
27 27
28 28 def decodeextra(text):
29 29 """
30 30 >>> sorted(decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'})
31 31 ... ).iteritems())
32 32 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
33 33 >>> sorted(decodeextra(encodeextra({'foo': 'bar',
34 34 ... 'baz': chr(92) + chr(0) + '2'})
35 35 ... ).iteritems())
36 36 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
37 37 """
38 38 extra = _defaultextra.copy()
39 39 for l in text.split('\0'):
40 40 if l:
41 41 if '\\0' in l:
42 42 # fix up \0 without getting into trouble with \\0
43 43 l = l.replace('\\\\', '\\\\\n')
44 44 l = l.replace('\\0', '\0')
45 45 l = l.replace('\n', '')
46 46 k, v = l.decode('string_escape').split(':', 1)
47 47 extra[k] = v
48 48 return extra
49 49
50 50 def encodeextra(d):
51 51 # keys must be sorted to produce a deterministic changelog entry
52 52 items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
53 53 return "\0".join(items)
54 54
55 55 def stripdesc(desc):
56 56 """strip trailing whitespace and leading and trailing empty lines"""
57 57 return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
58 58
59 59 class appender(object):
60 60 '''the changelog index must be updated last on disk, so we use this class
61 61 to delay writes to it'''
62 62 def __init__(self, vfs, name, mode, buf):
63 63 self.data = buf
64 64 fp = vfs(name, mode)
65 65 self.fp = fp
66 66 self.offset = fp.tell()
67 67 self.size = vfs.fstat(fp).st_size
68 68
69 69 def end(self):
70 70 return self.size + len("".join(self.data))
71 71 def tell(self):
72 72 return self.offset
73 73 def flush(self):
74 74 pass
75 75 def close(self):
76 76 self.fp.close()
77 77
78 78 def seek(self, offset, whence=0):
79 79 '''virtual file offset spans real file and data'''
80 80 if whence == 0:
81 81 self.offset = offset
82 82 elif whence == 1:
83 83 self.offset += offset
84 84 elif whence == 2:
85 85 self.offset = self.end() + offset
86 86 if self.offset < self.size:
87 87 self.fp.seek(self.offset)
88 88
89 89 def read(self, count=-1):
90 90 '''only trick here is reads that span real file and data'''
91 91 ret = ""
92 92 if self.offset < self.size:
93 93 s = self.fp.read(count)
94 94 ret = s
95 95 self.offset += len(s)
96 96 if count > 0:
97 97 count -= len(s)
98 98 if count != 0:
99 99 doff = self.offset - self.size
100 100 self.data.insert(0, "".join(self.data))
101 101 del self.data[1:]
102 102 s = self.data[0][doff:doff + count]
103 103 self.offset += len(s)
104 104 ret += s
105 105 return ret
106 106
107 107 def write(self, s):
108 108 self.data.append(str(s))
109 109 self.offset += len(s)
110 110
111 111 def _divertopener(opener, target):
112 112 """build an opener that writes in 'target.a' instead of 'target'"""
113 113 def _divert(name, mode='r'):
114 114 if name != target:
115 115 return opener(name, mode)
116 116 return opener(name + ".a", mode)
117 117 return _divert
118 118
119 119 def _delayopener(opener, target, buf):
120 120 """build an opener that stores chunks in 'buf' instead of 'target'"""
121 121 def _delay(name, mode='r'):
122 122 if name != target:
123 123 return opener(name, mode)
124 124 return appender(opener, name, mode, buf)
125 125 return _delay
126 126
127 127 class changelog(revlog.revlog):
128 128 def __init__(self, opener):
129 129 revlog.revlog.__init__(self, opener, "00changelog.i")
130 130 if self._initempty:
131 131 # changelogs don't benefit from generaldelta
132 132 self.version &= ~revlog.REVLOGGENERALDELTA
133 133 self._generaldelta = False
134 134 self._realopener = opener
135 135 self._delayed = False
136 136 self._delaybuf = None
137 137 self._divert = False
138 138 self.filteredrevs = frozenset()
139 139
140 140 def tip(self):
141 141 """filtered version of revlog.tip"""
142 142 for i in xrange(len(self) -1, -2, -1):
143 143 if i not in self.filteredrevs:
144 144 return self.node(i)
145 145
146 146 def __iter__(self):
147 147 """filtered version of revlog.__iter__"""
148 148 if len(self.filteredrevs) == 0:
149 149 return revlog.revlog.__iter__(self)
150 150
151 151 def filterediter():
152 152 for i in xrange(len(self)):
153 153 if i not in self.filteredrevs:
154 154 yield i
155 155
156 156 return filterediter()
157 157
158 158 def revs(self, start=0, stop=None):
159 159 """filtered version of revlog.revs"""
160 160 for i in super(changelog, self).revs(start, stop):
161 161 if i not in self.filteredrevs:
162 162 yield i
163 163
164 164 @util.propertycache
165 165 def nodemap(self):
166 166 # XXX need filtering too
167 167 self.rev(self.node(0))
168 168 return self._nodecache
169 169
170 170 def hasnode(self, node):
171 171 """filtered version of revlog.hasnode"""
172 172 try:
173 173 i = self.rev(node)
174 174 return i not in self.filteredrevs
175 175 except KeyError:
176 176 return False
177 177
178 178 def headrevs(self):
179 179 if self.filteredrevs:
180 180 try:
181 181 return self.index.headrevsfiltered(self.filteredrevs)
182 182 # AttributeError covers non-c-extension environments and
183 183 # old c extensions without filter handling.
184 184 except AttributeError:
185 185 return self._headrevs()
186 186
187 187 return super(changelog, self).headrevs()
188 188
189 189 def strip(self, *args, **kwargs):
190 190 # XXX make something better than assert
191 191 # We can't expect proper strip behavior if we are filtered.
192 192 assert not self.filteredrevs
193 193 super(changelog, self).strip(*args, **kwargs)
194 194
195 195 def rev(self, node):
196 196 """filtered version of revlog.rev"""
197 197 r = super(changelog, self).rev(node)
198 198 if r in self.filteredrevs:
199 199 raise error.FilteredLookupError(hex(node), self.indexfile,
200 200 _('filtered node'))
201 201 return r
202 202
203 203 def node(self, rev):
204 204 """filtered version of revlog.node"""
205 205 if rev in self.filteredrevs:
206 206 raise error.FilteredIndexError(rev)
207 207 return super(changelog, self).node(rev)
208 208
209 209 def linkrev(self, rev):
210 210 """filtered version of revlog.linkrev"""
211 211 if rev in self.filteredrevs:
212 212 raise error.FilteredIndexError(rev)
213 213 return super(changelog, self).linkrev(rev)
214 214
215 215 def parentrevs(self, rev):
216 216 """filtered version of revlog.parentrevs"""
217 217 if rev in self.filteredrevs:
218 218 raise error.FilteredIndexError(rev)
219 219 return super(changelog, self).parentrevs(rev)
220 220
221 221 def flags(self, rev):
222 222 """filtered version of revlog.flags"""
223 223 if rev in self.filteredrevs:
224 224 raise error.FilteredIndexError(rev)
225 225 return super(changelog, self).flags(rev)
226 226
227 def delayupdate(self):
227 def delayupdate(self, tr):
228 228 "delay visibility of index updates to other readers"
229 229
230 230 if not self._delayed:
231 231 if len(self) == 0:
232 232 self._divert = True
233 233 if self._realopener.exists(self.indexfile + '.a'):
234 234 self._realopener.unlink(self.indexfile + '.a')
235 235 self.opener = _divertopener(self._realopener, self.indexfile)
236 236 else:
237 237 self._delaybuf = []
238 238 self.opener = _delayopener(self._realopener, self.indexfile,
239 239 self._delaybuf)
240 240 self._delayed = True
241 tr.addpending('cl-%i' % id(self), self._writepending)
241 242
242 243 def finalize(self, tr):
243 244 "finalize index updates"
244 245 self._delayed = False
245 246 self.opener = self._realopener
246 247 # move redirected index data back into place
247 248 if self._divert:
248 249 assert not self._delaybuf
249 250 tmpname = self.indexfile + ".a"
250 251 nfile = self.opener.open(tmpname)
251 252 nfile.close()
252 253 self.opener.rename(tmpname, self.indexfile)
253 254 elif self._delaybuf:
254 255 fp = self.opener(self.indexfile, 'a')
255 256 fp.write("".join(self._delaybuf))
256 257 fp.close()
257 258 self._delaybuf = None
258 259 self._divert = False
259 260 # split when we're done
260 261 self.checkinlinesize(tr)
261 262
262 263 def readpending(self, file):
263 264 r = revlog.revlog(self.opener, file)
264 265 self.index = r.index
265 266 self.nodemap = r.nodemap
266 267 self._nodecache = r._nodecache
267 268 self._chunkcache = r._chunkcache
268 269
269 def writepending(self):
270 def _writepending(self):
270 271 "create a file containing the unfinalized state for pretxnchangegroup"
271 272 if self._delaybuf:
272 273 # make a temporary copy of the index
273 274 fp1 = self._realopener(self.indexfile)
274 275 fp2 = self._realopener(self.indexfile + ".a", "w")
275 276 fp2.write(fp1.read())
276 277 # add pending data
277 278 fp2.write("".join(self._delaybuf))
278 279 fp2.close()
279 280 # switch modes so finalize can simply rename
280 281 self._delaybuf = None
281 282 self._divert = True
282 283 self.opener = _divertopener(self._realopener, self.indexfile)
283 284
284 285 if self._divert:
285 286 return True
286 287
287 288 return False
288 289
289 290 def checkinlinesize(self, tr, fp=None):
290 291 if not self._delayed:
291 292 revlog.revlog.checkinlinesize(self, tr, fp)
292 293
293 294 def read(self, node):
294 295 """
295 296 format used:
296 297 nodeid\n : manifest node in ascii
297 298 user\n : user, no \n or \r allowed
298 299 time tz extra\n : date (time is int or float, timezone is int)
299 300 : extra is metadata, encoded and separated by '\0'
300 301 : older versions ignore it
301 302 files\n\n : files modified by the cset, no \n or \r allowed
302 303 (.*) : comment (free text, ideally utf-8)
303 304
304 305 changelog v0 doesn't use extra
305 306 """
306 307 text = self.revision(node)
307 308 if not text:
308 309 return (nullid, "", (0, 0), [], "", _defaultextra)
309 310 last = text.index("\n\n")
310 311 desc = encoding.tolocal(text[last + 2:])
311 312 l = text[:last].split('\n')
312 313 manifest = bin(l[0])
313 314 user = encoding.tolocal(l[1])
314 315
315 316 tdata = l[2].split(' ', 2)
316 317 if len(tdata) != 3:
317 318 time = float(tdata[0])
318 319 try:
319 320 # various tools did silly things with the time zone field.
320 321 timezone = int(tdata[1])
321 322 except ValueError:
322 323 timezone = 0
323 324 extra = _defaultextra
324 325 else:
325 326 time, timezone = float(tdata[0]), int(tdata[1])
326 327 extra = decodeextra(tdata[2])
327 328
328 329 files = l[3:]
329 330 return (manifest, user, (time, timezone), files, desc, extra)
330 331
331 332 def add(self, manifest, files, desc, transaction, p1, p2,
332 333 user, date=None, extra=None):
333 334 # Convert to UTF-8 encoded bytestrings as the very first
334 335 # thing: calling any method on a localstr object will turn it
335 336 # into a str object and the cached UTF-8 string is thus lost.
336 337 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
337 338
338 339 user = user.strip()
339 340 # An empty username or a username with a "\n" will make the
340 341 # revision text contain two "\n\n" sequences -> corrupt
341 342 # repository since read cannot unpack the revision.
342 343 if not user:
343 344 raise error.RevlogError(_("empty username"))
344 345 if "\n" in user:
345 346 raise error.RevlogError(_("username %s contains a newline")
346 347 % repr(user))
347 348
348 349 desc = stripdesc(desc)
349 350
350 351 if date:
351 352 parseddate = "%d %d" % util.parsedate(date)
352 353 else:
353 354 parseddate = "%d %d" % util.makedate()
354 355 if extra:
355 356 branch = extra.get("branch")
356 357 if branch in ("default", ""):
357 358 del extra["branch"]
358 359 elif branch in (".", "null", "tip"):
359 360 raise error.RevlogError(_('the name \'%s\' is reserved')
360 361 % branch)
361 362 if extra:
362 363 extra = encodeextra(extra)
363 364 parseddate = "%s %s" % (parseddate, extra)
364 365 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
365 366 text = "\n".join(l)
366 367 return self.addrevision(text, transaction, len(self), p1, p2)
367 368
368 369 def branchinfo(self, rev):
369 370 """return the branch name and open/close state of a revision
370 371
371 372 This function exists because creating a changectx object
372 373 just to access this is costly."""
373 374 extra = self.read(rev)[5]
374 375 return encoding.tolocal(extra.get("branch")), 'close' in extra
@@ -1,1300 +1,1297 b''
1 1 # exchange.py - utility to exchange data between repos.
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
8 8 from i18n import _
9 9 from node import hex, nullid
10 10 import errno, urllib
11 11 import util, scmutil, changegroup, base85, error
12 12 import discovery, phases, obsolete, bookmarks as bookmod, bundle2, pushkey
13 13
14 14 def readbundle(ui, fh, fname, vfs=None):
15 15 header = changegroup.readexactly(fh, 4)
16 16
17 17 alg = None
18 18 if not fname:
19 19 fname = "stream"
20 20 if not header.startswith('HG') and header.startswith('\0'):
21 21 fh = changegroup.headerlessfixup(fh, header)
22 22 header = "HG10"
23 23 alg = 'UN'
24 24 elif vfs:
25 25 fname = vfs.join(fname)
26 26
27 27 magic, version = header[0:2], header[2:4]
28 28
29 29 if magic != 'HG':
30 30 raise util.Abort(_('%s: not a Mercurial bundle') % fname)
31 31 if version == '10':
32 32 if alg is None:
33 33 alg = changegroup.readexactly(fh, 2)
34 34 return changegroup.cg1unpacker(fh, alg)
35 35 elif version == '2Y':
36 36 return bundle2.unbundle20(ui, fh, header=magic + version)
37 37 else:
38 38 raise util.Abort(_('%s: unknown bundle version %s') % (fname, version))
39 39
40 40 def buildobsmarkerspart(bundler, markers):
41 41 """add an obsmarker part to the bundler with <markers>
42 42
43 43 No part is created if markers is empty.
44 44 Raises ValueError if the bundler doesn't support any known obsmarker format.
45 45 """
46 46 if markers:
47 47 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
48 48 version = obsolete.commonversion(remoteversions)
49 49 if version is None:
50 50 raise ValueError('bundler do not support common obsmarker format')
51 51 stream = obsolete.encodemarkers(markers, True, version=version)
52 52 return bundler.newpart('B2X:OBSMARKERS', data=stream)
53 53 return None
54 54
55 55 class pushoperation(object):
56 56 """A object that represent a single push operation
57 57
58 58 It purpose is to carry push related state and very common operation.
59 59
60 60 A new should be created at the beginning of each push and discarded
61 61 afterward.
62 62 """
63 63
64 64 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
65 65 bookmarks=()):
66 66 # repo we push from
67 67 self.repo = repo
68 68 self.ui = repo.ui
69 69 # repo we push to
70 70 self.remote = remote
71 71 # force option provided
72 72 self.force = force
73 73 # revs to be pushed (None is "all")
74 74 self.revs = revs
75 75 # bookmark explicitly pushed
76 76 self.bookmarks = bookmarks
77 77 # allow push of new branch
78 78 self.newbranch = newbranch
79 79 # did a local lock get acquired?
80 80 self.locallocked = None
81 81 # step already performed
82 82 # (used to check what steps have been already performed through bundle2)
83 83 self.stepsdone = set()
84 84 # Integer version of the changegroup push result
85 85 # - None means nothing to push
86 86 # - 0 means HTTP error
87 87 # - 1 means we pushed and remote head count is unchanged *or*
88 88 # we have outgoing changesets but refused to push
89 89 # - other values as described by addchangegroup()
90 90 self.cgresult = None
91 91 # Boolean value for the bookmark push
92 92 self.bkresult = None
93 93 # discover.outgoing object (contains common and outgoing data)
94 94 self.outgoing = None
95 95 # all remote heads before the push
96 96 self.remoteheads = None
97 97 # testable as a boolean indicating if any nodes are missing locally.
98 98 self.incoming = None
99 99 # phases changes that must be pushed along side the changesets
100 100 self.outdatedphases = None
101 101 # phases changes that must be pushed if changeset push fails
102 102 self.fallbackoutdatedphases = None
103 103 # outgoing obsmarkers
104 104 self.outobsmarkers = set()
105 105 # outgoing bookmarks
106 106 self.outbookmarks = []
107 107
108 108 @util.propertycache
109 109 def futureheads(self):
110 110 """future remote heads if the changeset push succeeds"""
111 111 return self.outgoing.missingheads
112 112
113 113 @util.propertycache
114 114 def fallbackheads(self):
115 115 """future remote heads if the changeset push fails"""
116 116 if self.revs is None:
117 117 # not target to push, all common are relevant
118 118 return self.outgoing.commonheads
119 119 unfi = self.repo.unfiltered()
120 120 # I want cheads = heads(::missingheads and ::commonheads)
121 121 # (missingheads is revs with secret changeset filtered out)
122 122 #
123 123 # This can be expressed as:
124 124 # cheads = ( (missingheads and ::commonheads)
125 125 # + (commonheads and ::missingheads))"
126 126 # )
127 127 #
128 128 # while trying to push we already computed the following:
129 129 # common = (::commonheads)
130 130 # missing = ((commonheads::missingheads) - commonheads)
131 131 #
132 132 # We can pick:
133 133 # * missingheads part of common (::commonheads)
134 134 common = set(self.outgoing.common)
135 135 nm = self.repo.changelog.nodemap
136 136 cheads = [node for node in self.revs if nm[node] in common]
137 137 # and
138 138 # * commonheads parents on missing
139 139 revset = unfi.set('%ln and parents(roots(%ln))',
140 140 self.outgoing.commonheads,
141 141 self.outgoing.missing)
142 142 cheads.extend(c.node() for c in revset)
143 143 return cheads
144 144
145 145 @property
146 146 def commonheads(self):
147 147 """set of all common heads after changeset bundle push"""
148 148 if self.cgresult:
149 149 return self.futureheads
150 150 else:
151 151 return self.fallbackheads
152 152
153 153 # mapping of message used when pushing bookmark
154 154 bookmsgmap = {'update': (_("updating bookmark %s\n"),
155 155 _('updating bookmark %s failed!\n')),
156 156 'export': (_("exporting bookmark %s\n"),
157 157 _('exporting bookmark %s failed!\n')),
158 158 'delete': (_("deleting remote bookmark %s\n"),
159 159 _('deleting remote bookmark %s failed!\n')),
160 160 }
161 161
162 162
163 163 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=()):
164 164 '''Push outgoing changesets (limited by revs) from a local
165 165 repository to remote. Return an integer:
166 166 - None means nothing to push
167 167 - 0 means HTTP error
168 168 - 1 means we pushed and remote head count is unchanged *or*
169 169 we have outgoing changesets but refused to push
170 170 - other values as described by addchangegroup()
171 171 '''
172 172 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks)
173 173 if pushop.remote.local():
174 174 missing = (set(pushop.repo.requirements)
175 175 - pushop.remote.local().supported)
176 176 if missing:
177 177 msg = _("required features are not"
178 178 " supported in the destination:"
179 179 " %s") % (', '.join(sorted(missing)))
180 180 raise util.Abort(msg)
181 181
182 182 # there are two ways to push to remote repo:
183 183 #
184 184 # addchangegroup assumes local user can lock remote
185 185 # repo (local filesystem, old ssh servers).
186 186 #
187 187 # unbundle assumes local user cannot lock remote repo (new ssh
188 188 # servers, http servers).
189 189
190 190 if not pushop.remote.canpush():
191 191 raise util.Abort(_("destination does not support push"))
192 192 # get local lock as we might write phase data
193 193 locallock = None
194 194 try:
195 195 locallock = pushop.repo.lock()
196 196 pushop.locallocked = True
197 197 except IOError, err:
198 198 pushop.locallocked = False
199 199 if err.errno != errno.EACCES:
200 200 raise
201 201 # source repo cannot be locked.
202 202 # We do not abort the push, but just disable the local phase
203 203 # synchronisation.
204 204 msg = 'cannot lock source repository: %s\n' % err
205 205 pushop.ui.debug(msg)
206 206 try:
207 207 pushop.repo.checkpush(pushop)
208 208 lock = None
209 209 unbundle = pushop.remote.capable('unbundle')
210 210 if not unbundle:
211 211 lock = pushop.remote.lock()
212 212 try:
213 213 _pushdiscovery(pushop)
214 214 if (pushop.repo.ui.configbool('experimental', 'bundle2-exp',
215 215 False)
216 216 and pushop.remote.capable('bundle2-exp')):
217 217 _pushbundle2(pushop)
218 218 _pushchangeset(pushop)
219 219 _pushsyncphase(pushop)
220 220 _pushobsolete(pushop)
221 221 _pushbookmark(pushop)
222 222 finally:
223 223 if lock is not None:
224 224 lock.release()
225 225 finally:
226 226 if locallock is not None:
227 227 locallock.release()
228 228
229 229 return pushop
230 230
231 231 # list of steps to perform discovery before push
232 232 pushdiscoveryorder = []
233 233
234 234 # Mapping between step name and function
235 235 #
236 236 # This exists to help extensions wrap steps if necessary
237 237 pushdiscoverymapping = {}
238 238
239 239 def pushdiscovery(stepname):
240 240 """decorator for function performing discovery before push
241 241
242 242 The function is added to the step -> function mapping and appended to the
243 243 list of steps. Beware that decorated function will be added in order (this
244 244 may matter).
245 245
246 246 You can only use this decorator for a new step, if you want to wrap a step
247 247 from an extension, change the pushdiscovery dictionary directly."""
248 248 def dec(func):
249 249 assert stepname not in pushdiscoverymapping
250 250 pushdiscoverymapping[stepname] = func
251 251 pushdiscoveryorder.append(stepname)
252 252 return func
253 253 return dec
254 254
255 255 def _pushdiscovery(pushop):
256 256 """Run all discovery steps"""
257 257 for stepname in pushdiscoveryorder:
258 258 step = pushdiscoverymapping[stepname]
259 259 step(pushop)
260 260
261 261 @pushdiscovery('changeset')
262 262 def _pushdiscoverychangeset(pushop):
263 263 """discover the changeset that need to be pushed"""
264 264 unfi = pushop.repo.unfiltered()
265 265 fci = discovery.findcommonincoming
266 266 commoninc = fci(unfi, pushop.remote, force=pushop.force)
267 267 common, inc, remoteheads = commoninc
268 268 fco = discovery.findcommonoutgoing
269 269 outgoing = fco(unfi, pushop.remote, onlyheads=pushop.revs,
270 270 commoninc=commoninc, force=pushop.force)
271 271 pushop.outgoing = outgoing
272 272 pushop.remoteheads = remoteheads
273 273 pushop.incoming = inc
274 274
275 275 @pushdiscovery('phase')
276 276 def _pushdiscoveryphase(pushop):
277 277 """discover the phase that needs to be pushed
278 278
279 279 (computed for both success and failure case for changesets push)"""
280 280 outgoing = pushop.outgoing
281 281 unfi = pushop.repo.unfiltered()
282 282 remotephases = pushop.remote.listkeys('phases')
283 283 publishing = remotephases.get('publishing', False)
284 284 ana = phases.analyzeremotephases(pushop.repo,
285 285 pushop.fallbackheads,
286 286 remotephases)
287 287 pheads, droots = ana
288 288 extracond = ''
289 289 if not publishing:
290 290 extracond = ' and public()'
291 291 revset = 'heads((%%ln::%%ln) %s)' % extracond
292 292 # Get the list of all revs draft on remote by public here.
293 293 # XXX Beware that revset break if droots is not strictly
294 294 # XXX root we may want to ensure it is but it is costly
295 295 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
296 296 if not outgoing.missing:
297 297 future = fallback
298 298 else:
299 299 # adds changeset we are going to push as draft
300 300 #
301 301 # should not be necessary for publishing server, but because of an
302 302 # issue fixed in xxxxx we have to do it anyway.
303 303 fdroots = list(unfi.set('roots(%ln + %ln::)',
304 304 outgoing.missing, droots))
305 305 fdroots = [f.node() for f in fdroots]
306 306 future = list(unfi.set(revset, fdroots, pushop.futureheads))
307 307 pushop.outdatedphases = future
308 308 pushop.fallbackoutdatedphases = fallback
309 309
310 310 @pushdiscovery('obsmarker')
311 311 def _pushdiscoveryobsmarkers(pushop):
312 312 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
313 313 and pushop.repo.obsstore
314 314 and 'obsolete' in pushop.remote.listkeys('namespaces')):
315 315 repo = pushop.repo
316 316 # very naive computation, that can be quite expensive on big repo.
317 317 # However: evolution is currently slow on them anyway.
318 318 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
319 319 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
320 320
321 321 @pushdiscovery('bookmarks')
322 322 def _pushdiscoverybookmarks(pushop):
323 323 ui = pushop.ui
324 324 repo = pushop.repo.unfiltered()
325 325 remote = pushop.remote
326 326 ui.debug("checking for updated bookmarks\n")
327 327 ancestors = ()
328 328 if pushop.revs:
329 329 revnums = map(repo.changelog.rev, pushop.revs)
330 330 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
331 331 remotebookmark = remote.listkeys('bookmarks')
332 332
333 333 explicit = set(pushop.bookmarks)
334 334
335 335 comp = bookmod.compare(repo, repo._bookmarks, remotebookmark, srchex=hex)
336 336 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
337 337 for b, scid, dcid in advsrc:
338 338 if b in explicit:
339 339 explicit.remove(b)
340 340 if not ancestors or repo[scid].rev() in ancestors:
341 341 pushop.outbookmarks.append((b, dcid, scid))
342 342 # search added bookmark
343 343 for b, scid, dcid in addsrc:
344 344 if b in explicit:
345 345 explicit.remove(b)
346 346 pushop.outbookmarks.append((b, '', scid))
347 347 # search for overwritten bookmark
348 348 for b, scid, dcid in advdst + diverge + differ:
349 349 if b in explicit:
350 350 explicit.remove(b)
351 351 pushop.outbookmarks.append((b, dcid, scid))
352 352 # search for bookmark to delete
353 353 for b, scid, dcid in adddst:
354 354 if b in explicit:
355 355 explicit.remove(b)
356 356 # treat as "deleted locally"
357 357 pushop.outbookmarks.append((b, dcid, ''))
358 358 # identical bookmarks shouldn't get reported
359 359 for b, scid, dcid in same:
360 360 if b in explicit:
361 361 explicit.remove(b)
362 362
363 363 if explicit:
364 364 explicit = sorted(explicit)
365 365 # we should probably list all of them
366 366 ui.warn(_('bookmark %s does not exist on the local '
367 367 'or remote repository!\n') % explicit[0])
368 368 pushop.bkresult = 2
369 369
370 370 pushop.outbookmarks.sort()
371 371
372 372 def _pushcheckoutgoing(pushop):
373 373 outgoing = pushop.outgoing
374 374 unfi = pushop.repo.unfiltered()
375 375 if not outgoing.missing:
376 376 # nothing to push
377 377 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
378 378 return False
379 379 # something to push
380 380 if not pushop.force:
381 381 # if repo.obsstore == False --> no obsolete
382 382 # then, save the iteration
383 383 if unfi.obsstore:
384 384 # this message are here for 80 char limit reason
385 385 mso = _("push includes obsolete changeset: %s!")
386 386 mst = {"unstable": _("push includes unstable changeset: %s!"),
387 387 "bumped": _("push includes bumped changeset: %s!"),
388 388 "divergent": _("push includes divergent changeset: %s!")}
389 389 # If we are to push if there is at least one
390 390 # obsolete or unstable changeset in missing, at
391 391 # least one of the missinghead will be obsolete or
392 392 # unstable. So checking heads only is ok
393 393 for node in outgoing.missingheads:
394 394 ctx = unfi[node]
395 395 if ctx.obsolete():
396 396 raise util.Abort(mso % ctx)
397 397 elif ctx.troubled():
398 398 raise util.Abort(mst[ctx.troubles()[0]] % ctx)
399 399 newbm = pushop.ui.configlist('bookmarks', 'pushing')
400 400 discovery.checkheads(unfi, pushop.remote, outgoing,
401 401 pushop.remoteheads,
402 402 pushop.newbranch,
403 403 bool(pushop.incoming),
404 404 newbm)
405 405 return True
406 406
407 407 # List of names of steps to perform for an outgoing bundle2, order matters.
408 408 b2partsgenorder = []
409 409
410 410 # Mapping between step name and function
411 411 #
412 412 # This exists to help extensions wrap steps if necessary
413 413 b2partsgenmapping = {}
414 414
415 415 def b2partsgenerator(stepname):
416 416 """decorator for function generating bundle2 part
417 417
418 418 The function is added to the step -> function mapping and appended to the
419 419 list of steps. Beware that decorated functions will be added in order
420 420 (this may matter).
421 421
422 422 You can only use this decorator for new steps, if you want to wrap a step
423 423 from an extension, attack the b2partsgenmapping dictionary directly."""
424 424 def dec(func):
425 425 assert stepname not in b2partsgenmapping
426 426 b2partsgenmapping[stepname] = func
427 427 b2partsgenorder.append(stepname)
428 428 return func
429 429 return dec
430 430
431 431 @b2partsgenerator('changeset')
432 432 def _pushb2ctx(pushop, bundler):
433 433 """handle changegroup push through bundle2
434 434
435 435 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
436 436 """
437 437 if 'changesets' in pushop.stepsdone:
438 438 return
439 439 pushop.stepsdone.add('changesets')
440 440 # Send known heads to the server for race detection.
441 441 if not _pushcheckoutgoing(pushop):
442 442 return
443 443 pushop.repo.prepushoutgoinghooks(pushop.repo,
444 444 pushop.remote,
445 445 pushop.outgoing)
446 446 if not pushop.force:
447 447 bundler.newpart('B2X:CHECK:HEADS', data=iter(pushop.remoteheads))
448 448 b2caps = bundle2.bundle2caps(pushop.remote)
449 449 version = None
450 450 cgversions = b2caps.get('b2x:changegroup')
451 451 if cgversions is None:
452 452 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
453 453 pushop.outgoing)
454 454 else:
455 455 cgversions = [v for v in cgversions if v in changegroup.packermap]
456 456 if not cgversions:
457 457 raise ValueError(_('no common changegroup version'))
458 458 version = max(cgversions)
459 459 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
460 460 pushop.outgoing,
461 461 version=version)
462 462 cgpart = bundler.newpart('B2X:CHANGEGROUP', data=cg)
463 463 if version is not None:
464 464 cgpart.addparam('version', version)
465 465 def handlereply(op):
466 466 """extract addchangegroup returns from server reply"""
467 467 cgreplies = op.records.getreplies(cgpart.id)
468 468 assert len(cgreplies['changegroup']) == 1
469 469 pushop.cgresult = cgreplies['changegroup'][0]['return']
470 470 return handlereply
471 471
472 472 @b2partsgenerator('phase')
473 473 def _pushb2phases(pushop, bundler):
474 474 """handle phase push through bundle2"""
475 475 if 'phases' in pushop.stepsdone:
476 476 return
477 477 b2caps = bundle2.bundle2caps(pushop.remote)
478 478 if not 'b2x:pushkey' in b2caps:
479 479 return
480 480 pushop.stepsdone.add('phases')
481 481 part2node = []
482 482 enc = pushkey.encode
483 483 for newremotehead in pushop.outdatedphases:
484 484 part = bundler.newpart('b2x:pushkey')
485 485 part.addparam('namespace', enc('phases'))
486 486 part.addparam('key', enc(newremotehead.hex()))
487 487 part.addparam('old', enc(str(phases.draft)))
488 488 part.addparam('new', enc(str(phases.public)))
489 489 part2node.append((part.id, newremotehead))
490 490 def handlereply(op):
491 491 for partid, node in part2node:
492 492 partrep = op.records.getreplies(partid)
493 493 results = partrep['pushkey']
494 494 assert len(results) <= 1
495 495 msg = None
496 496 if not results:
497 497 msg = _('server ignored update of %s to public!\n') % node
498 498 elif not int(results[0]['return']):
499 499 msg = _('updating %s to public failed!\n') % node
500 500 if msg is not None:
501 501 pushop.ui.warn(msg)
502 502 return handlereply
503 503
504 504 @b2partsgenerator('obsmarkers')
505 505 def _pushb2obsmarkers(pushop, bundler):
506 506 if 'obsmarkers' in pushop.stepsdone:
507 507 return
508 508 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
509 509 if obsolete.commonversion(remoteversions) is None:
510 510 return
511 511 pushop.stepsdone.add('obsmarkers')
512 512 if pushop.outobsmarkers:
513 513 buildobsmarkerspart(bundler, pushop.outobsmarkers)
514 514
515 515 @b2partsgenerator('bookmarks')
516 516 def _pushb2bookmarks(pushop, bundler):
517 517 """handle phase push through bundle2"""
518 518 if 'bookmarks' in pushop.stepsdone:
519 519 return
520 520 b2caps = bundle2.bundle2caps(pushop.remote)
521 521 if 'b2x:pushkey' not in b2caps:
522 522 return
523 523 pushop.stepsdone.add('bookmarks')
524 524 part2book = []
525 525 enc = pushkey.encode
526 526 for book, old, new in pushop.outbookmarks:
527 527 part = bundler.newpart('b2x:pushkey')
528 528 part.addparam('namespace', enc('bookmarks'))
529 529 part.addparam('key', enc(book))
530 530 part.addparam('old', enc(old))
531 531 part.addparam('new', enc(new))
532 532 action = 'update'
533 533 if not old:
534 534 action = 'export'
535 535 elif not new:
536 536 action = 'delete'
537 537 part2book.append((part.id, book, action))
538 538
539 539
540 540 def handlereply(op):
541 541 ui = pushop.ui
542 542 for partid, book, action in part2book:
543 543 partrep = op.records.getreplies(partid)
544 544 results = partrep['pushkey']
545 545 assert len(results) <= 1
546 546 if not results:
547 547 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
548 548 else:
549 549 ret = int(results[0]['return'])
550 550 if ret:
551 551 ui.status(bookmsgmap[action][0] % book)
552 552 else:
553 553 ui.warn(bookmsgmap[action][1] % book)
554 554 if pushop.bkresult is not None:
555 555 pushop.bkresult = 1
556 556 return handlereply
557 557
558 558
559 559 def _pushbundle2(pushop):
560 560 """push data to the remote using bundle2
561 561
562 562 The only currently supported type of data is changegroup but this will
563 563 evolve in the future."""
564 564 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
565 565 # create reply capability
566 566 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo))
567 567 bundler.newpart('b2x:replycaps', data=capsblob)
568 568 replyhandlers = []
569 569 for partgenname in b2partsgenorder:
570 570 partgen = b2partsgenmapping[partgenname]
571 571 ret = partgen(pushop, bundler)
572 572 if callable(ret):
573 573 replyhandlers.append(ret)
574 574 # do not push if nothing to push
575 575 if bundler.nbparts <= 1:
576 576 return
577 577 stream = util.chunkbuffer(bundler.getchunks())
578 578 try:
579 579 reply = pushop.remote.unbundle(stream, ['force'], 'push')
580 580 except error.BundleValueError, exc:
581 581 raise util.Abort('missing support for %s' % exc)
582 582 try:
583 583 op = bundle2.processbundle(pushop.repo, reply)
584 584 except error.BundleValueError, exc:
585 585 raise util.Abort('missing support for %s' % exc)
586 586 for rephand in replyhandlers:
587 587 rephand(op)
588 588
589 589 def _pushchangeset(pushop):
590 590 """Make the actual push of changeset bundle to remote repo"""
591 591 if 'changesets' in pushop.stepsdone:
592 592 return
593 593 pushop.stepsdone.add('changesets')
594 594 if not _pushcheckoutgoing(pushop):
595 595 return
596 596 pushop.repo.prepushoutgoinghooks(pushop.repo,
597 597 pushop.remote,
598 598 pushop.outgoing)
599 599 outgoing = pushop.outgoing
600 600 unbundle = pushop.remote.capable('unbundle')
601 601 # TODO: get bundlecaps from remote
602 602 bundlecaps = None
603 603 # create a changegroup from local
604 604 if pushop.revs is None and not (outgoing.excluded
605 605 or pushop.repo.changelog.filteredrevs):
606 606 # push everything,
607 607 # use the fast path, no race possible on push
608 608 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
609 609 cg = changegroup.getsubset(pushop.repo,
610 610 outgoing,
611 611 bundler,
612 612 'push',
613 613 fastpath=True)
614 614 else:
615 615 cg = changegroup.getlocalchangegroup(pushop.repo, 'push', outgoing,
616 616 bundlecaps)
617 617
618 618 # apply changegroup to remote
619 619 if unbundle:
620 620 # local repo finds heads on server, finds out what
621 621 # revs it must push. once revs transferred, if server
622 622 # finds it has different heads (someone else won
623 623 # commit/push race), server aborts.
624 624 if pushop.force:
625 625 remoteheads = ['force']
626 626 else:
627 627 remoteheads = pushop.remoteheads
628 628 # ssh: return remote's addchangegroup()
629 629 # http: return remote's addchangegroup() or 0 for error
630 630 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
631 631 pushop.repo.url())
632 632 else:
633 633 # we return an integer indicating remote head count
634 634 # change
635 635 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
636 636 pushop.repo.url())
637 637
638 638 def _pushsyncphase(pushop):
639 639 """synchronise phase information locally and remotely"""
640 640 cheads = pushop.commonheads
641 641 # even when we don't push, exchanging phase data is useful
642 642 remotephases = pushop.remote.listkeys('phases')
643 643 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
644 644 and remotephases # server supports phases
645 645 and pushop.cgresult is None # nothing was pushed
646 646 and remotephases.get('publishing', False)):
647 647 # When:
648 648 # - this is a subrepo push
649 649 # - and remote support phase
650 650 # - and no changeset was pushed
651 651 # - and remote is publishing
652 652 # We may be in issue 3871 case!
653 653 # We drop the possible phase synchronisation done by
654 654 # courtesy to publish changesets possibly locally draft
655 655 # on the remote.
656 656 remotephases = {'publishing': 'True'}
657 657 if not remotephases: # old server or public only reply from non-publishing
658 658 _localphasemove(pushop, cheads)
659 659 # don't push any phase data as there is nothing to push
660 660 else:
661 661 ana = phases.analyzeremotephases(pushop.repo, cheads,
662 662 remotephases)
663 663 pheads, droots = ana
664 664 ### Apply remote phase on local
665 665 if remotephases.get('publishing', False):
666 666 _localphasemove(pushop, cheads)
667 667 else: # publish = False
668 668 _localphasemove(pushop, pheads)
669 669 _localphasemove(pushop, cheads, phases.draft)
670 670 ### Apply local phase on remote
671 671
672 672 if pushop.cgresult:
673 673 if 'phases' in pushop.stepsdone:
674 674 # phases already pushed though bundle2
675 675 return
676 676 outdated = pushop.outdatedphases
677 677 else:
678 678 outdated = pushop.fallbackoutdatedphases
679 679
680 680 pushop.stepsdone.add('phases')
681 681
682 682 # filter heads already turned public by the push
683 683 outdated = [c for c in outdated if c.node() not in pheads]
684 684 b2caps = bundle2.bundle2caps(pushop.remote)
685 685 if 'b2x:pushkey' in b2caps:
686 686 # server supports bundle2, let's do a batched push through it
687 687 #
688 688 # This will eventually be unified with the changesets bundle2 push
689 689 bundler = bundle2.bundle20(pushop.ui, b2caps)
690 690 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo))
691 691 bundler.newpart('b2x:replycaps', data=capsblob)
692 692 part2node = []
693 693 enc = pushkey.encode
694 694 for newremotehead in outdated:
695 695 part = bundler.newpart('b2x:pushkey')
696 696 part.addparam('namespace', enc('phases'))
697 697 part.addparam('key', enc(newremotehead.hex()))
698 698 part.addparam('old', enc(str(phases.draft)))
699 699 part.addparam('new', enc(str(phases.public)))
700 700 part2node.append((part.id, newremotehead))
701 701 stream = util.chunkbuffer(bundler.getchunks())
702 702 try:
703 703 reply = pushop.remote.unbundle(stream, ['force'], 'push')
704 704 op = bundle2.processbundle(pushop.repo, reply)
705 705 except error.BundleValueError, exc:
706 706 raise util.Abort('missing support for %s' % exc)
707 707 for partid, node in part2node:
708 708 partrep = op.records.getreplies(partid)
709 709 results = partrep['pushkey']
710 710 assert len(results) <= 1
711 711 msg = None
712 712 if not results:
713 713 msg = _('server ignored update of %s to public!\n') % node
714 714 elif not int(results[0]['return']):
715 715 msg = _('updating %s to public failed!\n') % node
716 716 if msg is not None:
717 717 pushop.ui.warn(msg)
718 718
719 719 else:
720 720 # fallback to independent pushkey command
721 721 for newremotehead in outdated:
722 722 r = pushop.remote.pushkey('phases',
723 723 newremotehead.hex(),
724 724 str(phases.draft),
725 725 str(phases.public))
726 726 if not r:
727 727 pushop.ui.warn(_('updating %s to public failed!\n')
728 728 % newremotehead)
729 729
730 730 def _localphasemove(pushop, nodes, phase=phases.public):
731 731 """move <nodes> to <phase> in the local source repo"""
732 732 if pushop.locallocked:
733 733 tr = pushop.repo.transaction('push-phase-sync')
734 734 try:
735 735 phases.advanceboundary(pushop.repo, tr, phase, nodes)
736 736 tr.close()
737 737 finally:
738 738 tr.release()
739 739 else:
740 740 # repo is not locked, do not change any phases!
741 741 # Informs the user that phases should have been moved when
742 742 # applicable.
743 743 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
744 744 phasestr = phases.phasenames[phase]
745 745 if actualmoves:
746 746 pushop.ui.status(_('cannot lock source repo, skipping '
747 747 'local %s phase update\n') % phasestr)
748 748
749 749 def _pushobsolete(pushop):
750 750 """utility function to push obsolete markers to a remote"""
751 751 if 'obsmarkers' in pushop.stepsdone:
752 752 return
753 753 pushop.ui.debug('try to push obsolete markers to remote\n')
754 754 repo = pushop.repo
755 755 remote = pushop.remote
756 756 pushop.stepsdone.add('obsmarkers')
757 757 if pushop.outobsmarkers:
758 758 rslts = []
759 759 remotedata = obsolete._pushkeyescape(pushop.outobsmarkers)
760 760 for key in sorted(remotedata, reverse=True):
761 761 # reverse sort to ensure we end with dump0
762 762 data = remotedata[key]
763 763 rslts.append(remote.pushkey('obsolete', key, '', data))
764 764 if [r for r in rslts if not r]:
765 765 msg = _('failed to push some obsolete markers!\n')
766 766 repo.ui.warn(msg)
767 767
768 768 def _pushbookmark(pushop):
769 769 """Update bookmark position on remote"""
770 770 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
771 771 return
772 772 pushop.stepsdone.add('bookmarks')
773 773 ui = pushop.ui
774 774 remote = pushop.remote
775 775
776 776 for b, old, new in pushop.outbookmarks:
777 777 action = 'update'
778 778 if not old:
779 779 action = 'export'
780 780 elif not new:
781 781 action = 'delete'
782 782 if remote.pushkey('bookmarks', b, old, new):
783 783 ui.status(bookmsgmap[action][0] % b)
784 784 else:
785 785 ui.warn(bookmsgmap[action][1] % b)
786 786 # discovery can have set the value form invalid entry
787 787 if pushop.bkresult is not None:
788 788 pushop.bkresult = 1
789 789
790 790 class pulloperation(object):
791 791 """A object that represent a single pull operation
792 792
793 793 It purpose is to carry push related state and very common operation.
794 794
795 795 A new should be created at the beginning of each pull and discarded
796 796 afterward.
797 797 """
798 798
799 799 def __init__(self, repo, remote, heads=None, force=False, bookmarks=()):
800 800 # repo we pull into
801 801 self.repo = repo
802 802 # repo we pull from
803 803 self.remote = remote
804 804 # revision we try to pull (None is "all")
805 805 self.heads = heads
806 806 # bookmark pulled explicitly
807 807 self.explicitbookmarks = bookmarks
808 808 # do we force pull?
809 809 self.force = force
810 810 # the name the pull transaction
811 811 self._trname = 'pull\n' + util.hidepassword(remote.url())
812 812 # hold the transaction once created
813 813 self._tr = None
814 814 # set of common changeset between local and remote before pull
815 815 self.common = None
816 816 # set of pulled head
817 817 self.rheads = None
818 818 # list of missing changeset to fetch remotely
819 819 self.fetch = None
820 820 # remote bookmarks data
821 821 self.remotebookmarks = None
822 822 # result of changegroup pulling (used as return code by pull)
823 823 self.cgresult = None
824 824 # list of step already done
825 825 self.stepsdone = set()
826 826
827 827 @util.propertycache
828 828 def pulledsubset(self):
829 829 """heads of the set of changeset target by the pull"""
830 830 # compute target subset
831 831 if self.heads is None:
832 832 # We pulled every thing possible
833 833 # sync on everything common
834 834 c = set(self.common)
835 835 ret = list(self.common)
836 836 for n in self.rheads:
837 837 if n not in c:
838 838 ret.append(n)
839 839 return ret
840 840 else:
841 841 # We pulled a specific subset
842 842 # sync on this subset
843 843 return self.heads
844 844
845 845 def gettransaction(self):
846 846 """get appropriate pull transaction, creating it if needed"""
847 847 if self._tr is None:
848 848 self._tr = self.repo.transaction(self._trname)
849 849 self._tr.hookargs['source'] = 'pull'
850 850 self._tr.hookargs['url'] = self.remote.url()
851 851 return self._tr
852 852
853 853 def closetransaction(self):
854 854 """close transaction if created"""
855 855 if self._tr is not None:
856 856 repo = self.repo
857 cl = repo.unfiltered().changelog
858 p = cl.writepending() and repo.root or ""
859 p = cl.writepending() and repo.root or ""
857 p = lambda: self._tr.writepending() and repo.root or ""
860 858 repo.hook('b2x-pretransactionclose', throw=True, pending=p,
861 859 **self._tr.hookargs)
862 860 self._tr.close()
863 861 hookargs = dict(self._tr.hookargs)
864 862 def runhooks():
865 863 repo.hook('b2x-transactionclose', **hookargs)
866 864 repo._afterlock(runhooks)
867 865
868 866 def releasetransaction(self):
869 867 """release transaction if created"""
870 868 if self._tr is not None:
871 869 self._tr.release()
872 870
873 871 def pull(repo, remote, heads=None, force=False, bookmarks=()):
874 872 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks)
875 873 if pullop.remote.local():
876 874 missing = set(pullop.remote.requirements) - pullop.repo.supported
877 875 if missing:
878 876 msg = _("required features are not"
879 877 " supported in the destination:"
880 878 " %s") % (', '.join(sorted(missing)))
881 879 raise util.Abort(msg)
882 880
883 881 pullop.remotebookmarks = remote.listkeys('bookmarks')
884 882 lock = pullop.repo.lock()
885 883 try:
886 884 _pulldiscovery(pullop)
887 885 if (pullop.repo.ui.configbool('experimental', 'bundle2-exp', False)
888 886 and pullop.remote.capable('bundle2-exp')):
889 887 _pullbundle2(pullop)
890 888 _pullchangeset(pullop)
891 889 _pullphase(pullop)
892 890 _pullbookmarks(pullop)
893 891 _pullobsolete(pullop)
894 892 pullop.closetransaction()
895 893 finally:
896 894 pullop.releasetransaction()
897 895 lock.release()
898 896
899 897 return pullop
900 898
901 899 # list of steps to perform discovery before pull
902 900 pulldiscoveryorder = []
903 901
904 902 # Mapping between step name and function
905 903 #
906 904 # This exists to help extensions wrap steps if necessary
907 905 pulldiscoverymapping = {}
908 906
909 907 def pulldiscovery(stepname):
910 908 """decorator for function performing discovery before pull
911 909
912 910 The function is added to the step -> function mapping and appended to the
913 911 list of steps. Beware that decorated function will be added in order (this
914 912 may matter).
915 913
916 914 You can only use this decorator for a new step, if you want to wrap a step
917 915 from an extension, change the pulldiscovery dictionary directly."""
918 916 def dec(func):
919 917 assert stepname not in pulldiscoverymapping
920 918 pulldiscoverymapping[stepname] = func
921 919 pulldiscoveryorder.append(stepname)
922 920 return func
923 921 return dec
924 922
925 923 def _pulldiscovery(pullop):
926 924 """Run all discovery steps"""
927 925 for stepname in pulldiscoveryorder:
928 926 step = pulldiscoverymapping[stepname]
929 927 step(pullop)
930 928
931 929 @pulldiscovery('changegroup')
932 930 def _pulldiscoverychangegroup(pullop):
933 931 """discovery phase for the pull
934 932
935 933 Current handle changeset discovery only, will change handle all discovery
936 934 at some point."""
937 935 tmp = discovery.findcommonincoming(pullop.repo.unfiltered(),
938 936 pullop.remote,
939 937 heads=pullop.heads,
940 938 force=pullop.force)
941 939 pullop.common, pullop.fetch, pullop.rheads = tmp
942 940
943 941 def _pullbundle2(pullop):
944 942 """pull data using bundle2
945 943
946 944 For now, the only supported data are changegroup."""
947 945 remotecaps = bundle2.bundle2caps(pullop.remote)
948 946 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
949 947 # pulling changegroup
950 948 pullop.stepsdone.add('changegroup')
951 949
952 950 kwargs['common'] = pullop.common
953 951 kwargs['heads'] = pullop.heads or pullop.rheads
954 952 kwargs['cg'] = pullop.fetch
955 953 if 'b2x:listkeys' in remotecaps:
956 954 kwargs['listkeys'] = ['phase', 'bookmarks']
957 955 if not pullop.fetch:
958 956 pullop.repo.ui.status(_("no changes found\n"))
959 957 pullop.cgresult = 0
960 958 else:
961 959 if pullop.heads is None and list(pullop.common) == [nullid]:
962 960 pullop.repo.ui.status(_("requesting all changes\n"))
963 961 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
964 962 remoteversions = bundle2.obsmarkersversion(remotecaps)
965 963 if obsolete.commonversion(remoteversions) is not None:
966 964 kwargs['obsmarkers'] = True
967 965 pullop.stepsdone.add('obsmarkers')
968 966 _pullbundle2extraprepare(pullop, kwargs)
969 967 if kwargs.keys() == ['format']:
970 968 return # nothing to pull
971 969 bundle = pullop.remote.getbundle('pull', **kwargs)
972 970 try:
973 971 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
974 972 except error.BundleValueError, exc:
975 973 raise util.Abort('missing support for %s' % exc)
976 974
977 975 if pullop.fetch:
978 976 changedheads = 0
979 977 pullop.cgresult = 1
980 978 for cg in op.records['changegroup']:
981 979 ret = cg['return']
982 980 # If any changegroup result is 0, return 0
983 981 if ret == 0:
984 982 pullop.cgresult = 0
985 983 break
986 984 if ret < -1:
987 985 changedheads += ret + 1
988 986 elif ret > 1:
989 987 changedheads += ret - 1
990 988 if changedheads > 0:
991 989 pullop.cgresult = 1 + changedheads
992 990 elif changedheads < 0:
993 991 pullop.cgresult = -1 + changedheads
994 992
995 993 # processing phases change
996 994 for namespace, value in op.records['listkeys']:
997 995 if namespace == 'phases':
998 996 _pullapplyphases(pullop, value)
999 997
1000 998 # processing bookmark update
1001 999 for namespace, value in op.records['listkeys']:
1002 1000 if namespace == 'bookmarks':
1003 1001 pullop.remotebookmarks = value
1004 1002 _pullbookmarks(pullop)
1005 1003
1006 1004 def _pullbundle2extraprepare(pullop, kwargs):
1007 1005 """hook function so that extensions can extend the getbundle call"""
1008 1006 pass
1009 1007
1010 1008 def _pullchangeset(pullop):
1011 1009 """pull changeset from unbundle into the local repo"""
1012 1010 # We delay the open of the transaction as late as possible so we
1013 1011 # don't open transaction for nothing or you break future useful
1014 1012 # rollback call
1015 1013 if 'changegroup' in pullop.stepsdone:
1016 1014 return
1017 1015 pullop.stepsdone.add('changegroup')
1018 1016 if not pullop.fetch:
1019 1017 pullop.repo.ui.status(_("no changes found\n"))
1020 1018 pullop.cgresult = 0
1021 1019 return
1022 1020 pullop.gettransaction()
1023 1021 if pullop.heads is None and list(pullop.common) == [nullid]:
1024 1022 pullop.repo.ui.status(_("requesting all changes\n"))
1025 1023 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1026 1024 # issue1320, avoid a race if remote changed after discovery
1027 1025 pullop.heads = pullop.rheads
1028 1026
1029 1027 if pullop.remote.capable('getbundle'):
1030 1028 # TODO: get bundlecaps from remote
1031 1029 cg = pullop.remote.getbundle('pull', common=pullop.common,
1032 1030 heads=pullop.heads or pullop.rheads)
1033 1031 elif pullop.heads is None:
1034 1032 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1035 1033 elif not pullop.remote.capable('changegroupsubset'):
1036 1034 raise util.Abort(_("partial pull cannot be done because "
1037 1035 "other repository doesn't support "
1038 1036 "changegroupsubset."))
1039 1037 else:
1040 1038 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1041 1039 pullop.cgresult = changegroup.addchangegroup(pullop.repo, cg, 'pull',
1042 1040 pullop.remote.url())
1043 1041
1044 1042 def _pullphase(pullop):
1045 1043 # Get remote phases data from remote
1046 1044 if 'phases' in pullop.stepsdone:
1047 1045 return
1048 1046 remotephases = pullop.remote.listkeys('phases')
1049 1047 _pullapplyphases(pullop, remotephases)
1050 1048
1051 1049 def _pullapplyphases(pullop, remotephases):
1052 1050 """apply phase movement from observed remote state"""
1053 1051 if 'phases' in pullop.stepsdone:
1054 1052 return
1055 1053 pullop.stepsdone.add('phases')
1056 1054 publishing = bool(remotephases.get('publishing', False))
1057 1055 if remotephases and not publishing:
1058 1056 # remote is new and unpublishing
1059 1057 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1060 1058 pullop.pulledsubset,
1061 1059 remotephases)
1062 1060 dheads = pullop.pulledsubset
1063 1061 else:
1064 1062 # Remote is old or publishing all common changesets
1065 1063 # should be seen as public
1066 1064 pheads = pullop.pulledsubset
1067 1065 dheads = []
1068 1066 unfi = pullop.repo.unfiltered()
1069 1067 phase = unfi._phasecache.phase
1070 1068 rev = unfi.changelog.nodemap.get
1071 1069 public = phases.public
1072 1070 draft = phases.draft
1073 1071
1074 1072 # exclude changesets already public locally and update the others
1075 1073 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1076 1074 if pheads:
1077 1075 tr = pullop.gettransaction()
1078 1076 phases.advanceboundary(pullop.repo, tr, public, pheads)
1079 1077
1080 1078 # exclude changesets already draft locally and update the others
1081 1079 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1082 1080 if dheads:
1083 1081 tr = pullop.gettransaction()
1084 1082 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1085 1083
1086 1084 def _pullbookmarks(pullop):
1087 1085 """process the remote bookmark information to update the local one"""
1088 1086 if 'bookmarks' in pullop.stepsdone:
1089 1087 return
1090 1088 pullop.stepsdone.add('bookmarks')
1091 1089 repo = pullop.repo
1092 1090 remotebookmarks = pullop.remotebookmarks
1093 1091 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1094 1092 pullop.remote.url(),
1095 1093 pullop.gettransaction,
1096 1094 explicit=pullop.explicitbookmarks)
1097 1095
1098 1096 def _pullobsolete(pullop):
1099 1097 """utility function to pull obsolete markers from a remote
1100 1098
1101 1099 The `gettransaction` is function that return the pull transaction, creating
1102 1100 one if necessary. We return the transaction to inform the calling code that
1103 1101 a new transaction have been created (when applicable).
1104 1102
1105 1103 Exists mostly to allow overriding for experimentation purpose"""
1106 1104 if 'obsmarkers' in pullop.stepsdone:
1107 1105 return
1108 1106 pullop.stepsdone.add('obsmarkers')
1109 1107 tr = None
1110 1108 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1111 1109 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1112 1110 remoteobs = pullop.remote.listkeys('obsolete')
1113 1111 if 'dump0' in remoteobs:
1114 1112 tr = pullop.gettransaction()
1115 1113 for key in sorted(remoteobs, reverse=True):
1116 1114 if key.startswith('dump'):
1117 1115 data = base85.b85decode(remoteobs[key])
1118 1116 pullop.repo.obsstore.mergemarkers(tr, data)
1119 1117 pullop.repo.invalidatevolatilesets()
1120 1118 return tr
1121 1119
1122 1120 def caps20to10(repo):
1123 1121 """return a set with appropriate options to use bundle20 during getbundle"""
1124 1122 caps = set(['HG2Y'])
1125 1123 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1126 1124 caps.add('bundle2=' + urllib.quote(capsblob))
1127 1125 return caps
1128 1126
1129 1127 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1130 1128 getbundle2partsorder = []
1131 1129
1132 1130 # Mapping between step name and function
1133 1131 #
1134 1132 # This exists to help extensions wrap steps if necessary
1135 1133 getbundle2partsmapping = {}
1136 1134
1137 1135 def getbundle2partsgenerator(stepname):
1138 1136 """decorator for function generating bundle2 part for getbundle
1139 1137
1140 1138 The function is added to the step -> function mapping and appended to the
1141 1139 list of steps. Beware that decorated functions will be added in order
1142 1140 (this may matter).
1143 1141
1144 1142 You can only use this decorator for new steps, if you want to wrap a step
1145 1143 from an extension, attack the getbundle2partsmapping dictionary directly."""
1146 1144 def dec(func):
1147 1145 assert stepname not in getbundle2partsmapping
1148 1146 getbundle2partsmapping[stepname] = func
1149 1147 getbundle2partsorder.append(stepname)
1150 1148 return func
1151 1149 return dec
1152 1150
1153 1151 def getbundle(repo, source, heads=None, common=None, bundlecaps=None,
1154 1152 **kwargs):
1155 1153 """return a full bundle (with potentially multiple kind of parts)
1156 1154
1157 1155 Could be a bundle HG10 or a bundle HG2Y depending on bundlecaps
1158 1156 passed. For now, the bundle can contain only changegroup, but this will
1159 1157 changes when more part type will be available for bundle2.
1160 1158
1161 1159 This is different from changegroup.getchangegroup that only returns an HG10
1162 1160 changegroup bundle. They may eventually get reunited in the future when we
1163 1161 have a clearer idea of the API we what to query different data.
1164 1162
1165 1163 The implementation is at a very early stage and will get massive rework
1166 1164 when the API of bundle is refined.
1167 1165 """
1168 1166 # bundle10 case
1169 1167 if bundlecaps is None or 'HG2Y' not in bundlecaps:
1170 1168 if bundlecaps and not kwargs.get('cg', True):
1171 1169 raise ValueError(_('request for bundle10 must include changegroup'))
1172 1170
1173 1171 if kwargs:
1174 1172 raise ValueError(_('unsupported getbundle arguments: %s')
1175 1173 % ', '.join(sorted(kwargs.keys())))
1176 1174 return changegroup.getchangegroup(repo, source, heads=heads,
1177 1175 common=common, bundlecaps=bundlecaps)
1178 1176
1179 1177 # bundle20 case
1180 1178 b2caps = {}
1181 1179 for bcaps in bundlecaps:
1182 1180 if bcaps.startswith('bundle2='):
1183 1181 blob = urllib.unquote(bcaps[len('bundle2='):])
1184 1182 b2caps.update(bundle2.decodecaps(blob))
1185 1183 bundler = bundle2.bundle20(repo.ui, b2caps)
1186 1184
1187 1185 for name in getbundle2partsorder:
1188 1186 func = getbundle2partsmapping[name]
1189 1187 kwargs['heads'] = heads
1190 1188 kwargs['common'] = common
1191 1189 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1192 1190 **kwargs)
1193 1191
1194 1192 return util.chunkbuffer(bundler.getchunks())
1195 1193
1196 1194 @getbundle2partsgenerator('changegroup')
1197 1195 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1198 1196 b2caps=None, heads=None, common=None, **kwargs):
1199 1197 """add a changegroup part to the requested bundle"""
1200 1198 cg = None
1201 1199 if kwargs.get('cg', True):
1202 1200 # build changegroup bundle here.
1203 1201 version = None
1204 1202 cgversions = b2caps.get('b2x:changegroup')
1205 1203 if cgversions is None:
1206 1204 cg = changegroup.getchangegroupraw(repo, source, heads=heads,
1207 1205 common=common,
1208 1206 bundlecaps=bundlecaps)
1209 1207 else:
1210 1208 cgversions = [v for v in cgversions if v in changegroup.packermap]
1211 1209 if not cgversions:
1212 1210 raise ValueError(_('no common changegroup version'))
1213 1211 version = max(cgversions)
1214 1212 cg = changegroup.getchangegroupraw(repo, source, heads=heads,
1215 1213 common=common,
1216 1214 bundlecaps=bundlecaps,
1217 1215 version=version)
1218 1216
1219 1217 if cg:
1220 1218 part = bundler.newpart('b2x:changegroup', data=cg)
1221 1219 if version is not None:
1222 1220 part.addparam('version', version)
1223 1221
1224 1222 @getbundle2partsgenerator('listkeys')
1225 1223 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1226 1224 b2caps=None, **kwargs):
1227 1225 """add parts containing listkeys namespaces to the requested bundle"""
1228 1226 listkeys = kwargs.get('listkeys', ())
1229 1227 for namespace in listkeys:
1230 1228 part = bundler.newpart('b2x:listkeys')
1231 1229 part.addparam('namespace', namespace)
1232 1230 keys = repo.listkeys(namespace).items()
1233 1231 part.data = pushkey.encodekeys(keys)
1234 1232
1235 1233 @getbundle2partsgenerator('obsmarkers')
1236 1234 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1237 1235 b2caps=None, heads=None, **kwargs):
1238 1236 """add an obsolescence markers part to the requested bundle"""
1239 1237 if kwargs.get('obsmarkers', False):
1240 1238 if heads is None:
1241 1239 heads = repo.heads()
1242 1240 subset = [c.node() for c in repo.set('::%ln', heads)]
1243 1241 markers = repo.obsstore.relevantmarkers(subset)
1244 1242 buildobsmarkerspart(bundler, markers)
1245 1243
1246 1244 def check_heads(repo, their_heads, context):
1247 1245 """check if the heads of a repo have been modified
1248 1246
1249 1247 Used by peer for unbundling.
1250 1248 """
1251 1249 heads = repo.heads()
1252 1250 heads_hash = util.sha1(''.join(sorted(heads))).digest()
1253 1251 if not (their_heads == ['force'] or their_heads == heads or
1254 1252 their_heads == ['hashed', heads_hash]):
1255 1253 # someone else committed/pushed/unbundled while we
1256 1254 # were transferring data
1257 1255 raise error.PushRaced('repository changed while %s - '
1258 1256 'please try again' % context)
1259 1257
1260 1258 def unbundle(repo, cg, heads, source, url):
1261 1259 """Apply a bundle to a repo.
1262 1260
1263 1261 this function makes sure the repo is locked during the application and have
1264 1262 mechanism to check that no push race occurred between the creation of the
1265 1263 bundle and its application.
1266 1264
1267 1265 If the push was raced as PushRaced exception is raised."""
1268 1266 r = 0
1269 1267 # need a transaction when processing a bundle2 stream
1270 1268 tr = None
1271 1269 lock = repo.lock()
1272 1270 try:
1273 1271 check_heads(repo, heads, 'uploading changes')
1274 1272 # push can proceed
1275 1273 if util.safehasattr(cg, 'params'):
1276 1274 try:
1277 1275 tr = repo.transaction('unbundle')
1278 1276 tr.hookargs['source'] = source
1279 1277 tr.hookargs['url'] = url
1280 1278 tr.hookargs['bundle2-exp'] = '1'
1281 1279 r = bundle2.processbundle(repo, cg, lambda: tr).reply
1282 cl = repo.unfiltered().changelog
1283 p = cl.writepending() and repo.root or ""
1280 p = lambda: tr.writepending() and repo.root or ""
1284 1281 repo.hook('b2x-pretransactionclose', throw=True, pending=p,
1285 1282 **tr.hookargs)
1286 1283 tr.close()
1287 1284 hookargs = dict(tr.hookargs)
1288 1285 def runhooks():
1289 1286 repo.hook('b2x-transactionclose', **hookargs)
1290 1287 repo._afterlock(runhooks)
1291 1288 except Exception, exc:
1292 1289 exc.duringunbundle2 = True
1293 1290 raise
1294 1291 else:
1295 1292 r = changegroup.addchangegroup(repo, cg, source, url)
1296 1293 finally:
1297 1294 if tr is not None:
1298 1295 tr.release()
1299 1296 lock.release()
1300 1297 return r
@@ -1,1804 +1,1804 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 urllib
10 10 import peer, changegroup, subrepo, pushkey, obsolete, repoview
11 11 import changelog, dirstate, filelog, manifest, context, bookmarks, phases
12 12 import lock as lockmod
13 13 import transaction, store, encoding, exchange, bundle2
14 14 import scmutil, util, extensions, hook, error, revset
15 15 import match as matchmod
16 16 import merge as mergemod
17 17 import tags as tagsmod
18 18 from lock import release
19 19 import weakref, errno, os, time, inspect
20 20 import branchmap, pathutil
21 21 propertycache = util.propertycache
22 22 filecache = scmutil.filecache
23 23
24 24 class repofilecache(filecache):
25 25 """All filecache usage on repo are done for logic that should be unfiltered
26 26 """
27 27
28 28 def __get__(self, repo, type=None):
29 29 return super(repofilecache, self).__get__(repo.unfiltered(), type)
30 30 def __set__(self, repo, value):
31 31 return super(repofilecache, self).__set__(repo.unfiltered(), value)
32 32 def __delete__(self, repo):
33 33 return super(repofilecache, self).__delete__(repo.unfiltered())
34 34
35 35 class storecache(repofilecache):
36 36 """filecache for files in the store"""
37 37 def join(self, obj, fname):
38 38 return obj.sjoin(fname)
39 39
40 40 class unfilteredpropertycache(propertycache):
41 41 """propertycache that apply to unfiltered repo only"""
42 42
43 43 def __get__(self, repo, type=None):
44 44 unfi = repo.unfiltered()
45 45 if unfi is repo:
46 46 return super(unfilteredpropertycache, self).__get__(unfi)
47 47 return getattr(unfi, self.name)
48 48
49 49 class filteredpropertycache(propertycache):
50 50 """propertycache that must take filtering in account"""
51 51
52 52 def cachevalue(self, obj, value):
53 53 object.__setattr__(obj, self.name, value)
54 54
55 55
56 56 def hasunfilteredcache(repo, name):
57 57 """check if a repo has an unfilteredpropertycache value for <name>"""
58 58 return name in vars(repo.unfiltered())
59 59
60 60 def unfilteredmethod(orig):
61 61 """decorate method that always need to be run on unfiltered version"""
62 62 def wrapper(repo, *args, **kwargs):
63 63 return orig(repo.unfiltered(), *args, **kwargs)
64 64 return wrapper
65 65
66 66 moderncaps = set(('lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
67 67 'unbundle'))
68 68 legacycaps = moderncaps.union(set(['changegroupsubset']))
69 69
70 70 class localpeer(peer.peerrepository):
71 71 '''peer for a local repo; reflects only the most recent API'''
72 72
73 73 def __init__(self, repo, caps=moderncaps):
74 74 peer.peerrepository.__init__(self)
75 75 self._repo = repo.filtered('served')
76 76 self.ui = repo.ui
77 77 self._caps = repo._restrictcapabilities(caps)
78 78 self.requirements = repo.requirements
79 79 self.supportedformats = repo.supportedformats
80 80
81 81 def close(self):
82 82 self._repo.close()
83 83
84 84 def _capabilities(self):
85 85 return self._caps
86 86
87 87 def local(self):
88 88 return self._repo
89 89
90 90 def canpush(self):
91 91 return True
92 92
93 93 def url(self):
94 94 return self._repo.url()
95 95
96 96 def lookup(self, key):
97 97 return self._repo.lookup(key)
98 98
99 99 def branchmap(self):
100 100 return self._repo.branchmap()
101 101
102 102 def heads(self):
103 103 return self._repo.heads()
104 104
105 105 def known(self, nodes):
106 106 return self._repo.known(nodes)
107 107
108 108 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
109 109 format='HG10', **kwargs):
110 110 cg = exchange.getbundle(self._repo, source, heads=heads,
111 111 common=common, bundlecaps=bundlecaps, **kwargs)
112 112 if bundlecaps is not None and 'HG2Y' in bundlecaps:
113 113 # When requesting a bundle2, getbundle returns a stream to make the
114 114 # wire level function happier. We need to build a proper object
115 115 # from it in local peer.
116 116 cg = bundle2.unbundle20(self.ui, cg)
117 117 return cg
118 118
119 119 # TODO We might want to move the next two calls into legacypeer and add
120 120 # unbundle instead.
121 121
122 122 def unbundle(self, cg, heads, url):
123 123 """apply a bundle on a repo
124 124
125 125 This function handles the repo locking itself."""
126 126 try:
127 127 cg = exchange.readbundle(self.ui, cg, None)
128 128 ret = exchange.unbundle(self._repo, cg, heads, 'push', url)
129 129 if util.safehasattr(ret, 'getchunks'):
130 130 # This is a bundle20 object, turn it into an unbundler.
131 131 # This little dance should be dropped eventually when the API
132 132 # is finally improved.
133 133 stream = util.chunkbuffer(ret.getchunks())
134 134 ret = bundle2.unbundle20(self.ui, stream)
135 135 return ret
136 136 except error.PushRaced, exc:
137 137 raise error.ResponseError(_('push failed:'), str(exc))
138 138
139 139 def lock(self):
140 140 return self._repo.lock()
141 141
142 142 def addchangegroup(self, cg, source, url):
143 143 return changegroup.addchangegroup(self._repo, cg, source, url)
144 144
145 145 def pushkey(self, namespace, key, old, new):
146 146 return self._repo.pushkey(namespace, key, old, new)
147 147
148 148 def listkeys(self, namespace):
149 149 return self._repo.listkeys(namespace)
150 150
151 151 def debugwireargs(self, one, two, three=None, four=None, five=None):
152 152 '''used to test argument passing over the wire'''
153 153 return "%s %s %s %s %s" % (one, two, three, four, five)
154 154
155 155 class locallegacypeer(localpeer):
156 156 '''peer extension which implements legacy methods too; used for tests with
157 157 restricted capabilities'''
158 158
159 159 def __init__(self, repo):
160 160 localpeer.__init__(self, repo, caps=legacycaps)
161 161
162 162 def branches(self, nodes):
163 163 return self._repo.branches(nodes)
164 164
165 165 def between(self, pairs):
166 166 return self._repo.between(pairs)
167 167
168 168 def changegroup(self, basenodes, source):
169 169 return changegroup.changegroup(self._repo, basenodes, source)
170 170
171 171 def changegroupsubset(self, bases, heads, source):
172 172 return changegroup.changegroupsubset(self._repo, bases, heads, source)
173 173
174 174 class localrepository(object):
175 175
176 176 supportedformats = set(('revlogv1', 'generaldelta'))
177 177 _basesupported = supportedformats | set(('store', 'fncache', 'shared',
178 178 'dotencode'))
179 179 openerreqs = set(('revlogv1', 'generaldelta'))
180 180 requirements = ['revlogv1']
181 181 filtername = None
182 182
183 183 # a list of (ui, featureset) functions.
184 184 # only functions defined in module of enabled extensions are invoked
185 185 featuresetupfuncs = set()
186 186
187 187 def _baserequirements(self, create):
188 188 return self.requirements[:]
189 189
190 190 def __init__(self, baseui, path=None, create=False):
191 191 self.wvfs = scmutil.vfs(path, expandpath=True, realpath=True)
192 192 self.wopener = self.wvfs
193 193 self.root = self.wvfs.base
194 194 self.path = self.wvfs.join(".hg")
195 195 self.origroot = path
196 196 self.auditor = pathutil.pathauditor(self.root, self._checknested)
197 197 self.vfs = scmutil.vfs(self.path)
198 198 self.opener = self.vfs
199 199 self.baseui = baseui
200 200 self.ui = baseui.copy()
201 201 self.ui.copy = baseui.copy # prevent copying repo configuration
202 202 # A list of callback to shape the phase if no data were found.
203 203 # Callback are in the form: func(repo, roots) --> processed root.
204 204 # This list it to be filled by extension during repo setup
205 205 self._phasedefaults = []
206 206 try:
207 207 self.ui.readconfig(self.join("hgrc"), self.root)
208 208 extensions.loadall(self.ui)
209 209 except IOError:
210 210 pass
211 211
212 212 if self.featuresetupfuncs:
213 213 self.supported = set(self._basesupported) # use private copy
214 214 extmods = set(m.__name__ for n, m
215 215 in extensions.extensions(self.ui))
216 216 for setupfunc in self.featuresetupfuncs:
217 217 if setupfunc.__module__ in extmods:
218 218 setupfunc(self.ui, self.supported)
219 219 else:
220 220 self.supported = self._basesupported
221 221
222 222 if not self.vfs.isdir():
223 223 if create:
224 224 if not self.wvfs.exists():
225 225 self.wvfs.makedirs()
226 226 self.vfs.makedir(notindexed=True)
227 227 requirements = self._baserequirements(create)
228 228 if self.ui.configbool('format', 'usestore', True):
229 229 self.vfs.mkdir("store")
230 230 requirements.append("store")
231 231 if self.ui.configbool('format', 'usefncache', True):
232 232 requirements.append("fncache")
233 233 if self.ui.configbool('format', 'dotencode', True):
234 234 requirements.append('dotencode')
235 235 # create an invalid changelog
236 236 self.vfs.append(
237 237 "00changelog.i",
238 238 '\0\0\0\2' # represents revlogv2
239 239 ' dummy changelog to prevent using the old repo layout'
240 240 )
241 241 if self.ui.configbool('format', 'generaldelta', False):
242 242 requirements.append("generaldelta")
243 243 requirements = set(requirements)
244 244 else:
245 245 raise error.RepoError(_("repository %s not found") % path)
246 246 elif create:
247 247 raise error.RepoError(_("repository %s already exists") % path)
248 248 else:
249 249 try:
250 250 requirements = scmutil.readrequires(self.vfs, self.supported)
251 251 except IOError, inst:
252 252 if inst.errno != errno.ENOENT:
253 253 raise
254 254 requirements = set()
255 255
256 256 self.sharedpath = self.path
257 257 try:
258 258 vfs = scmutil.vfs(self.vfs.read("sharedpath").rstrip('\n'),
259 259 realpath=True)
260 260 s = vfs.base
261 261 if not vfs.exists():
262 262 raise error.RepoError(
263 263 _('.hg/sharedpath points to nonexistent directory %s') % s)
264 264 self.sharedpath = s
265 265 except IOError, inst:
266 266 if inst.errno != errno.ENOENT:
267 267 raise
268 268
269 269 self.store = store.store(requirements, self.sharedpath, scmutil.vfs)
270 270 self.spath = self.store.path
271 271 self.svfs = self.store.vfs
272 272 self.sopener = self.svfs
273 273 self.sjoin = self.store.join
274 274 self.vfs.createmode = self.store.createmode
275 275 self._applyrequirements(requirements)
276 276 if create:
277 277 self._writerequirements()
278 278
279 279
280 280 self._branchcaches = {}
281 281 self.filterpats = {}
282 282 self._datafilters = {}
283 283 self._transref = self._lockref = self._wlockref = None
284 284
285 285 # A cache for various files under .hg/ that tracks file changes,
286 286 # (used by the filecache decorator)
287 287 #
288 288 # Maps a property name to its util.filecacheentry
289 289 self._filecache = {}
290 290
291 291 # hold sets of revision to be filtered
292 292 # should be cleared when something might have changed the filter value:
293 293 # - new changesets,
294 294 # - phase change,
295 295 # - new obsolescence marker,
296 296 # - working directory parent change,
297 297 # - bookmark changes
298 298 self.filteredrevcache = {}
299 299
300 300 def close(self):
301 301 pass
302 302
303 303 def _restrictcapabilities(self, caps):
304 304 # bundle2 is not ready for prime time, drop it unless explicitly
305 305 # required by the tests (or some brave tester)
306 306 if self.ui.configbool('experimental', 'bundle2-exp', False):
307 307 caps = set(caps)
308 308 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self))
309 309 caps.add('bundle2-exp=' + urllib.quote(capsblob))
310 310 return caps
311 311
312 312 def _applyrequirements(self, requirements):
313 313 self.requirements = requirements
314 314 self.sopener.options = dict((r, 1) for r in requirements
315 315 if r in self.openerreqs)
316 316 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
317 317 if chunkcachesize is not None:
318 318 self.sopener.options['chunkcachesize'] = chunkcachesize
319 319
320 320 def _writerequirements(self):
321 321 reqfile = self.opener("requires", "w")
322 322 for r in sorted(self.requirements):
323 323 reqfile.write("%s\n" % r)
324 324 reqfile.close()
325 325
326 326 def _checknested(self, path):
327 327 """Determine if path is a legal nested repository."""
328 328 if not path.startswith(self.root):
329 329 return False
330 330 subpath = path[len(self.root) + 1:]
331 331 normsubpath = util.pconvert(subpath)
332 332
333 333 # XXX: Checking against the current working copy is wrong in
334 334 # the sense that it can reject things like
335 335 #
336 336 # $ hg cat -r 10 sub/x.txt
337 337 #
338 338 # if sub/ is no longer a subrepository in the working copy
339 339 # parent revision.
340 340 #
341 341 # However, it can of course also allow things that would have
342 342 # been rejected before, such as the above cat command if sub/
343 343 # is a subrepository now, but was a normal directory before.
344 344 # The old path auditor would have rejected by mistake since it
345 345 # panics when it sees sub/.hg/.
346 346 #
347 347 # All in all, checking against the working copy seems sensible
348 348 # since we want to prevent access to nested repositories on
349 349 # the filesystem *now*.
350 350 ctx = self[None]
351 351 parts = util.splitpath(subpath)
352 352 while parts:
353 353 prefix = '/'.join(parts)
354 354 if prefix in ctx.substate:
355 355 if prefix == normsubpath:
356 356 return True
357 357 else:
358 358 sub = ctx.sub(prefix)
359 359 return sub.checknested(subpath[len(prefix) + 1:])
360 360 else:
361 361 parts.pop()
362 362 return False
363 363
364 364 def peer(self):
365 365 return localpeer(self) # not cached to avoid reference cycle
366 366
367 367 def unfiltered(self):
368 368 """Return unfiltered version of the repository
369 369
370 370 Intended to be overwritten by filtered repo."""
371 371 return self
372 372
373 373 def filtered(self, name):
374 374 """Return a filtered version of a repository"""
375 375 # build a new class with the mixin and the current class
376 376 # (possibly subclass of the repo)
377 377 class proxycls(repoview.repoview, self.unfiltered().__class__):
378 378 pass
379 379 return proxycls(self, name)
380 380
381 381 @repofilecache('bookmarks')
382 382 def _bookmarks(self):
383 383 return bookmarks.bmstore(self)
384 384
385 385 @repofilecache('bookmarks.current')
386 386 def _bookmarkcurrent(self):
387 387 return bookmarks.readcurrent(self)
388 388
389 389 def bookmarkheads(self, bookmark):
390 390 name = bookmark.split('@', 1)[0]
391 391 heads = []
392 392 for mark, n in self._bookmarks.iteritems():
393 393 if mark.split('@', 1)[0] == name:
394 394 heads.append(n)
395 395 return heads
396 396
397 397 @storecache('phaseroots')
398 398 def _phasecache(self):
399 399 return phases.phasecache(self, self._phasedefaults)
400 400
401 401 @storecache('obsstore')
402 402 def obsstore(self):
403 403 # read default format for new obsstore.
404 404 defaultformat = self.ui.configint('format', 'obsstore-version', None)
405 405 # rely on obsstore class default when possible.
406 406 kwargs = {}
407 407 if defaultformat is not None:
408 408 kwargs['defaultformat'] = defaultformat
409 409 readonly = not obsolete.isenabled(self, obsolete.createmarkersopt)
410 410 store = obsolete.obsstore(self.sopener, readonly=readonly,
411 411 **kwargs)
412 412 if store and readonly:
413 413 # message is rare enough to not be translated
414 414 msg = 'obsolete feature not enabled but %i markers found!\n'
415 415 self.ui.warn(msg % len(list(store)))
416 416 return store
417 417
418 418 @storecache('00changelog.i')
419 419 def changelog(self):
420 420 c = changelog.changelog(self.sopener)
421 421 if 'HG_PENDING' in os.environ:
422 422 p = os.environ['HG_PENDING']
423 423 if p.startswith(self.root):
424 424 c.readpending('00changelog.i.a')
425 425 return c
426 426
427 427 @storecache('00manifest.i')
428 428 def manifest(self):
429 429 return manifest.manifest(self.sopener)
430 430
431 431 @repofilecache('dirstate')
432 432 def dirstate(self):
433 433 warned = [0]
434 434 def validate(node):
435 435 try:
436 436 self.changelog.rev(node)
437 437 return node
438 438 except error.LookupError:
439 439 if not warned[0]:
440 440 warned[0] = True
441 441 self.ui.warn(_("warning: ignoring unknown"
442 442 " working parent %s!\n") % short(node))
443 443 return nullid
444 444
445 445 return dirstate.dirstate(self.opener, self.ui, self.root, validate)
446 446
447 447 def __getitem__(self, changeid):
448 448 if changeid is None:
449 449 return context.workingctx(self)
450 450 return context.changectx(self, changeid)
451 451
452 452 def __contains__(self, changeid):
453 453 try:
454 454 return bool(self.lookup(changeid))
455 455 except error.RepoLookupError:
456 456 return False
457 457
458 458 def __nonzero__(self):
459 459 return True
460 460
461 461 def __len__(self):
462 462 return len(self.changelog)
463 463
464 464 def __iter__(self):
465 465 return iter(self.changelog)
466 466
467 467 def revs(self, expr, *args):
468 468 '''Return a list of revisions matching the given revset'''
469 469 expr = revset.formatspec(expr, *args)
470 470 m = revset.match(None, expr)
471 471 return m(self, revset.spanset(self))
472 472
473 473 def set(self, expr, *args):
474 474 '''
475 475 Yield a context for each matching revision, after doing arg
476 476 replacement via revset.formatspec
477 477 '''
478 478 for r in self.revs(expr, *args):
479 479 yield self[r]
480 480
481 481 def url(self):
482 482 return 'file:' + self.root
483 483
484 484 def hook(self, name, throw=False, **args):
485 485 """Call a hook, passing this repo instance.
486 486
487 487 This a convenience method to aid invoking hooks. Extensions likely
488 488 won't call this unless they have registered a custom hook or are
489 489 replacing code that is expected to call a hook.
490 490 """
491 491 return hook.hook(self.ui, self, name, throw, **args)
492 492
493 493 @unfilteredmethod
494 494 def _tag(self, names, node, message, local, user, date, extra={},
495 495 editor=False):
496 496 if isinstance(names, str):
497 497 names = (names,)
498 498
499 499 branches = self.branchmap()
500 500 for name in names:
501 501 self.hook('pretag', throw=True, node=hex(node), tag=name,
502 502 local=local)
503 503 if name in branches:
504 504 self.ui.warn(_("warning: tag %s conflicts with existing"
505 505 " branch name\n") % name)
506 506
507 507 def writetags(fp, names, munge, prevtags):
508 508 fp.seek(0, 2)
509 509 if prevtags and prevtags[-1] != '\n':
510 510 fp.write('\n')
511 511 for name in names:
512 512 m = munge and munge(name) or name
513 513 if (self._tagscache.tagtypes and
514 514 name in self._tagscache.tagtypes):
515 515 old = self.tags().get(name, nullid)
516 516 fp.write('%s %s\n' % (hex(old), m))
517 517 fp.write('%s %s\n' % (hex(node), m))
518 518 fp.close()
519 519
520 520 prevtags = ''
521 521 if local:
522 522 try:
523 523 fp = self.opener('localtags', 'r+')
524 524 except IOError:
525 525 fp = self.opener('localtags', 'a')
526 526 else:
527 527 prevtags = fp.read()
528 528
529 529 # local tags are stored in the current charset
530 530 writetags(fp, names, None, prevtags)
531 531 for name in names:
532 532 self.hook('tag', node=hex(node), tag=name, local=local)
533 533 return
534 534
535 535 try:
536 536 fp = self.wfile('.hgtags', 'rb+')
537 537 except IOError, e:
538 538 if e.errno != errno.ENOENT:
539 539 raise
540 540 fp = self.wfile('.hgtags', 'ab')
541 541 else:
542 542 prevtags = fp.read()
543 543
544 544 # committed tags are stored in UTF-8
545 545 writetags(fp, names, encoding.fromlocal, prevtags)
546 546
547 547 fp.close()
548 548
549 549 self.invalidatecaches()
550 550
551 551 if '.hgtags' not in self.dirstate:
552 552 self[None].add(['.hgtags'])
553 553
554 554 m = matchmod.exact(self.root, '', ['.hgtags'])
555 555 tagnode = self.commit(message, user, date, extra=extra, match=m,
556 556 editor=editor)
557 557
558 558 for name in names:
559 559 self.hook('tag', node=hex(node), tag=name, local=local)
560 560
561 561 return tagnode
562 562
563 563 def tag(self, names, node, message, local, user, date, editor=False):
564 564 '''tag a revision with one or more symbolic names.
565 565
566 566 names is a list of strings or, when adding a single tag, names may be a
567 567 string.
568 568
569 569 if local is True, the tags are stored in a per-repository file.
570 570 otherwise, they are stored in the .hgtags file, and a new
571 571 changeset is committed with the change.
572 572
573 573 keyword arguments:
574 574
575 575 local: whether to store tags in non-version-controlled file
576 576 (default False)
577 577
578 578 message: commit message to use if committing
579 579
580 580 user: name of user to use if committing
581 581
582 582 date: date tuple to use if committing'''
583 583
584 584 if not local:
585 585 m = matchmod.exact(self.root, '', ['.hgtags'])
586 586 if util.any(self.status(match=m, unknown=True, ignored=True)):
587 587 raise util.Abort(_('working copy of .hgtags is changed'),
588 588 hint=_('please commit .hgtags manually'))
589 589
590 590 self.tags() # instantiate the cache
591 591 self._tag(names, node, message, local, user, date, editor=editor)
592 592
593 593 @filteredpropertycache
594 594 def _tagscache(self):
595 595 '''Returns a tagscache object that contains various tags related
596 596 caches.'''
597 597
598 598 # This simplifies its cache management by having one decorated
599 599 # function (this one) and the rest simply fetch things from it.
600 600 class tagscache(object):
601 601 def __init__(self):
602 602 # These two define the set of tags for this repository. tags
603 603 # maps tag name to node; tagtypes maps tag name to 'global' or
604 604 # 'local'. (Global tags are defined by .hgtags across all
605 605 # heads, and local tags are defined in .hg/localtags.)
606 606 # They constitute the in-memory cache of tags.
607 607 self.tags = self.tagtypes = None
608 608
609 609 self.nodetagscache = self.tagslist = None
610 610
611 611 cache = tagscache()
612 612 cache.tags, cache.tagtypes = self._findtags()
613 613
614 614 return cache
615 615
616 616 def tags(self):
617 617 '''return a mapping of tag to node'''
618 618 t = {}
619 619 if self.changelog.filteredrevs:
620 620 tags, tt = self._findtags()
621 621 else:
622 622 tags = self._tagscache.tags
623 623 for k, v in tags.iteritems():
624 624 try:
625 625 # ignore tags to unknown nodes
626 626 self.changelog.rev(v)
627 627 t[k] = v
628 628 except (error.LookupError, ValueError):
629 629 pass
630 630 return t
631 631
632 632 def _findtags(self):
633 633 '''Do the hard work of finding tags. Return a pair of dicts
634 634 (tags, tagtypes) where tags maps tag name to node, and tagtypes
635 635 maps tag name to a string like \'global\' or \'local\'.
636 636 Subclasses or extensions are free to add their own tags, but
637 637 should be aware that the returned dicts will be retained for the
638 638 duration of the localrepo object.'''
639 639
640 640 # XXX what tagtype should subclasses/extensions use? Currently
641 641 # mq and bookmarks add tags, but do not set the tagtype at all.
642 642 # Should each extension invent its own tag type? Should there
643 643 # be one tagtype for all such "virtual" tags? Or is the status
644 644 # quo fine?
645 645
646 646 alltags = {} # map tag name to (node, hist)
647 647 tagtypes = {}
648 648
649 649 tagsmod.findglobaltags(self.ui, self, alltags, tagtypes)
650 650 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
651 651
652 652 # Build the return dicts. Have to re-encode tag names because
653 653 # the tags module always uses UTF-8 (in order not to lose info
654 654 # writing to the cache), but the rest of Mercurial wants them in
655 655 # local encoding.
656 656 tags = {}
657 657 for (name, (node, hist)) in alltags.iteritems():
658 658 if node != nullid:
659 659 tags[encoding.tolocal(name)] = node
660 660 tags['tip'] = self.changelog.tip()
661 661 tagtypes = dict([(encoding.tolocal(name), value)
662 662 for (name, value) in tagtypes.iteritems()])
663 663 return (tags, tagtypes)
664 664
665 665 def tagtype(self, tagname):
666 666 '''
667 667 return the type of the given tag. result can be:
668 668
669 669 'local' : a local tag
670 670 'global' : a global tag
671 671 None : tag does not exist
672 672 '''
673 673
674 674 return self._tagscache.tagtypes.get(tagname)
675 675
676 676 def tagslist(self):
677 677 '''return a list of tags ordered by revision'''
678 678 if not self._tagscache.tagslist:
679 679 l = []
680 680 for t, n in self.tags().iteritems():
681 681 l.append((self.changelog.rev(n), t, n))
682 682 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
683 683
684 684 return self._tagscache.tagslist
685 685
686 686 def nodetags(self, node):
687 687 '''return the tags associated with a node'''
688 688 if not self._tagscache.nodetagscache:
689 689 nodetagscache = {}
690 690 for t, n in self._tagscache.tags.iteritems():
691 691 nodetagscache.setdefault(n, []).append(t)
692 692 for tags in nodetagscache.itervalues():
693 693 tags.sort()
694 694 self._tagscache.nodetagscache = nodetagscache
695 695 return self._tagscache.nodetagscache.get(node, [])
696 696
697 697 def nodebookmarks(self, node):
698 698 marks = []
699 699 for bookmark, n in self._bookmarks.iteritems():
700 700 if n == node:
701 701 marks.append(bookmark)
702 702 return sorted(marks)
703 703
704 704 def branchmap(self):
705 705 '''returns a dictionary {branch: [branchheads]} with branchheads
706 706 ordered by increasing revision number'''
707 707 branchmap.updatecache(self)
708 708 return self._branchcaches[self.filtername]
709 709
710 710 def branchtip(self, branch):
711 711 '''return the tip node for a given branch'''
712 712 try:
713 713 return self.branchmap().branchtip(branch)
714 714 except KeyError:
715 715 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
716 716
717 717 def lookup(self, key):
718 718 return self[key].node()
719 719
720 720 def lookupbranch(self, key, remote=None):
721 721 repo = remote or self
722 722 if key in repo.branchmap():
723 723 return key
724 724
725 725 repo = (remote and remote.local()) and remote or self
726 726 return repo[key].branch()
727 727
728 728 def known(self, nodes):
729 729 nm = self.changelog.nodemap
730 730 pc = self._phasecache
731 731 result = []
732 732 for n in nodes:
733 733 r = nm.get(n)
734 734 resp = not (r is None or pc.phase(self, r) >= phases.secret)
735 735 result.append(resp)
736 736 return result
737 737
738 738 def local(self):
739 739 return self
740 740
741 741 def cancopy(self):
742 742 # so statichttprepo's override of local() works
743 743 if not self.local():
744 744 return False
745 745 if not self.ui.configbool('phases', 'publish', True):
746 746 return True
747 747 # if publishing we can't copy if there is filtered content
748 748 return not self.filtered('visible').changelog.filteredrevs
749 749
750 750 def join(self, f, *insidef):
751 751 return os.path.join(self.path, f, *insidef)
752 752
753 753 def wjoin(self, f, *insidef):
754 754 return os.path.join(self.root, f, *insidef)
755 755
756 756 def file(self, f):
757 757 if f[0] == '/':
758 758 f = f[1:]
759 759 return filelog.filelog(self.sopener, f)
760 760
761 761 def changectx(self, changeid):
762 762 return self[changeid]
763 763
764 764 def parents(self, changeid=None):
765 765 '''get list of changectxs for parents of changeid'''
766 766 return self[changeid].parents()
767 767
768 768 def setparents(self, p1, p2=nullid):
769 769 self.dirstate.beginparentchange()
770 770 copies = self.dirstate.setparents(p1, p2)
771 771 pctx = self[p1]
772 772 if copies:
773 773 # Adjust copy records, the dirstate cannot do it, it
774 774 # requires access to parents manifests. Preserve them
775 775 # only for entries added to first parent.
776 776 for f in copies:
777 777 if f not in pctx and copies[f] in pctx:
778 778 self.dirstate.copy(copies[f], f)
779 779 if p2 == nullid:
780 780 for f, s in sorted(self.dirstate.copies().items()):
781 781 if f not in pctx and s not in pctx:
782 782 self.dirstate.copy(None, f)
783 783 self.dirstate.endparentchange()
784 784
785 785 def filectx(self, path, changeid=None, fileid=None):
786 786 """changeid can be a changeset revision, node, or tag.
787 787 fileid can be a file revision or node."""
788 788 return context.filectx(self, path, changeid, fileid)
789 789
790 790 def getcwd(self):
791 791 return self.dirstate.getcwd()
792 792
793 793 def pathto(self, f, cwd=None):
794 794 return self.dirstate.pathto(f, cwd)
795 795
796 796 def wfile(self, f, mode='r'):
797 797 return self.wopener(f, mode)
798 798
799 799 def _link(self, f):
800 800 return self.wvfs.islink(f)
801 801
802 802 def _loadfilter(self, filter):
803 803 if filter not in self.filterpats:
804 804 l = []
805 805 for pat, cmd in self.ui.configitems(filter):
806 806 if cmd == '!':
807 807 continue
808 808 mf = matchmod.match(self.root, '', [pat])
809 809 fn = None
810 810 params = cmd
811 811 for name, filterfn in self._datafilters.iteritems():
812 812 if cmd.startswith(name):
813 813 fn = filterfn
814 814 params = cmd[len(name):].lstrip()
815 815 break
816 816 if not fn:
817 817 fn = lambda s, c, **kwargs: util.filter(s, c)
818 818 # Wrap old filters not supporting keyword arguments
819 819 if not inspect.getargspec(fn)[2]:
820 820 oldfn = fn
821 821 fn = lambda s, c, **kwargs: oldfn(s, c)
822 822 l.append((mf, fn, params))
823 823 self.filterpats[filter] = l
824 824 return self.filterpats[filter]
825 825
826 826 def _filter(self, filterpats, filename, data):
827 827 for mf, fn, cmd in filterpats:
828 828 if mf(filename):
829 829 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
830 830 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
831 831 break
832 832
833 833 return data
834 834
835 835 @unfilteredpropertycache
836 836 def _encodefilterpats(self):
837 837 return self._loadfilter('encode')
838 838
839 839 @unfilteredpropertycache
840 840 def _decodefilterpats(self):
841 841 return self._loadfilter('decode')
842 842
843 843 def adddatafilter(self, name, filter):
844 844 self._datafilters[name] = filter
845 845
846 846 def wread(self, filename):
847 847 if self._link(filename):
848 848 data = self.wvfs.readlink(filename)
849 849 else:
850 850 data = self.wopener.read(filename)
851 851 return self._filter(self._encodefilterpats, filename, data)
852 852
853 853 def wwrite(self, filename, data, flags):
854 854 data = self._filter(self._decodefilterpats, filename, data)
855 855 if 'l' in flags:
856 856 self.wopener.symlink(data, filename)
857 857 else:
858 858 self.wopener.write(filename, data)
859 859 if 'x' in flags:
860 860 self.wvfs.setflags(filename, False, True)
861 861
862 862 def wwritedata(self, filename, data):
863 863 return self._filter(self._decodefilterpats, filename, data)
864 864
865 865 def transaction(self, desc, report=None):
866 866 tr = self._transref and self._transref() or None
867 867 if tr and tr.running():
868 868 return tr.nest()
869 869
870 870 # abort here if the journal already exists
871 871 if self.svfs.exists("journal"):
872 872 raise error.RepoError(
873 873 _("abandoned transaction found"),
874 874 hint=_("run 'hg recover' to clean up transaction"))
875 875
876 876 def onclose():
877 877 self.store.write(self._transref())
878 878
879 879 self._writejournal(desc)
880 880 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
881 881 rp = report and report or self.ui.warn
882 882 tr = transaction.transaction(rp, self.sopener,
883 883 "journal",
884 884 aftertrans(renames),
885 885 self.store.createmode,
886 886 onclose)
887 887 self._transref = weakref.ref(tr)
888 888 return tr
889 889
890 890 def _journalfiles(self):
891 891 return ((self.svfs, 'journal'),
892 892 (self.vfs, 'journal.dirstate'),
893 893 (self.vfs, 'journal.branch'),
894 894 (self.vfs, 'journal.desc'),
895 895 (self.vfs, 'journal.bookmarks'),
896 896 (self.svfs, 'journal.phaseroots'))
897 897
898 898 def undofiles(self):
899 899 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
900 900
901 901 def _writejournal(self, desc):
902 902 self.opener.write("journal.dirstate",
903 903 self.opener.tryread("dirstate"))
904 904 self.opener.write("journal.branch",
905 905 encoding.fromlocal(self.dirstate.branch()))
906 906 self.opener.write("journal.desc",
907 907 "%d\n%s\n" % (len(self), desc))
908 908 self.opener.write("journal.bookmarks",
909 909 self.opener.tryread("bookmarks"))
910 910 self.sopener.write("journal.phaseroots",
911 911 self.sopener.tryread("phaseroots"))
912 912
913 913 def recover(self):
914 914 lock = self.lock()
915 915 try:
916 916 if self.svfs.exists("journal"):
917 917 self.ui.status(_("rolling back interrupted transaction\n"))
918 918 transaction.rollback(self.sopener, "journal",
919 919 self.ui.warn)
920 920 self.invalidate()
921 921 return True
922 922 else:
923 923 self.ui.warn(_("no interrupted transaction available\n"))
924 924 return False
925 925 finally:
926 926 lock.release()
927 927
928 928 def rollback(self, dryrun=False, force=False):
929 929 wlock = lock = None
930 930 try:
931 931 wlock = self.wlock()
932 932 lock = self.lock()
933 933 if self.svfs.exists("undo"):
934 934 return self._rollback(dryrun, force)
935 935 else:
936 936 self.ui.warn(_("no rollback information available\n"))
937 937 return 1
938 938 finally:
939 939 release(lock, wlock)
940 940
941 941 @unfilteredmethod # Until we get smarter cache management
942 942 def _rollback(self, dryrun, force):
943 943 ui = self.ui
944 944 try:
945 945 args = self.opener.read('undo.desc').splitlines()
946 946 (oldlen, desc, detail) = (int(args[0]), args[1], None)
947 947 if len(args) >= 3:
948 948 detail = args[2]
949 949 oldtip = oldlen - 1
950 950
951 951 if detail and ui.verbose:
952 952 msg = (_('repository tip rolled back to revision %s'
953 953 ' (undo %s: %s)\n')
954 954 % (oldtip, desc, detail))
955 955 else:
956 956 msg = (_('repository tip rolled back to revision %s'
957 957 ' (undo %s)\n')
958 958 % (oldtip, desc))
959 959 except IOError:
960 960 msg = _('rolling back unknown transaction\n')
961 961 desc = None
962 962
963 963 if not force and self['.'] != self['tip'] and desc == 'commit':
964 964 raise util.Abort(
965 965 _('rollback of last commit while not checked out '
966 966 'may lose data'), hint=_('use -f to force'))
967 967
968 968 ui.status(msg)
969 969 if dryrun:
970 970 return 0
971 971
972 972 parents = self.dirstate.parents()
973 973 self.destroying()
974 974 transaction.rollback(self.sopener, 'undo', ui.warn)
975 975 if self.vfs.exists('undo.bookmarks'):
976 976 self.vfs.rename('undo.bookmarks', 'bookmarks')
977 977 if self.svfs.exists('undo.phaseroots'):
978 978 self.svfs.rename('undo.phaseroots', 'phaseroots')
979 979 self.invalidate()
980 980
981 981 parentgone = (parents[0] not in self.changelog.nodemap or
982 982 parents[1] not in self.changelog.nodemap)
983 983 if parentgone:
984 984 self.vfs.rename('undo.dirstate', 'dirstate')
985 985 try:
986 986 branch = self.opener.read('undo.branch')
987 987 self.dirstate.setbranch(encoding.tolocal(branch))
988 988 except IOError:
989 989 ui.warn(_('named branch could not be reset: '
990 990 'current branch is still \'%s\'\n')
991 991 % self.dirstate.branch())
992 992
993 993 self.dirstate.invalidate()
994 994 parents = tuple([p.rev() for p in self.parents()])
995 995 if len(parents) > 1:
996 996 ui.status(_('working directory now based on '
997 997 'revisions %d and %d\n') % parents)
998 998 else:
999 999 ui.status(_('working directory now based on '
1000 1000 'revision %d\n') % parents)
1001 1001 # TODO: if we know which new heads may result from this rollback, pass
1002 1002 # them to destroy(), which will prevent the branchhead cache from being
1003 1003 # invalidated.
1004 1004 self.destroyed()
1005 1005 return 0
1006 1006
1007 1007 def invalidatecaches(self):
1008 1008
1009 1009 if '_tagscache' in vars(self):
1010 1010 # can't use delattr on proxy
1011 1011 del self.__dict__['_tagscache']
1012 1012
1013 1013 self.unfiltered()._branchcaches.clear()
1014 1014 self.invalidatevolatilesets()
1015 1015
1016 1016 def invalidatevolatilesets(self):
1017 1017 self.filteredrevcache.clear()
1018 1018 obsolete.clearobscaches(self)
1019 1019
1020 1020 def invalidatedirstate(self):
1021 1021 '''Invalidates the dirstate, causing the next call to dirstate
1022 1022 to check if it was modified since the last time it was read,
1023 1023 rereading it if it has.
1024 1024
1025 1025 This is different to dirstate.invalidate() that it doesn't always
1026 1026 rereads the dirstate. Use dirstate.invalidate() if you want to
1027 1027 explicitly read the dirstate again (i.e. restoring it to a previous
1028 1028 known good state).'''
1029 1029 if hasunfilteredcache(self, 'dirstate'):
1030 1030 for k in self.dirstate._filecache:
1031 1031 try:
1032 1032 delattr(self.dirstate, k)
1033 1033 except AttributeError:
1034 1034 pass
1035 1035 delattr(self.unfiltered(), 'dirstate')
1036 1036
1037 1037 def invalidate(self):
1038 1038 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1039 1039 for k in self._filecache:
1040 1040 # dirstate is invalidated separately in invalidatedirstate()
1041 1041 if k == 'dirstate':
1042 1042 continue
1043 1043
1044 1044 try:
1045 1045 delattr(unfiltered, k)
1046 1046 except AttributeError:
1047 1047 pass
1048 1048 self.invalidatecaches()
1049 1049 self.store.invalidatecaches()
1050 1050
1051 1051 def invalidateall(self):
1052 1052 '''Fully invalidates both store and non-store parts, causing the
1053 1053 subsequent operation to reread any outside changes.'''
1054 1054 # extension should hook this to invalidate its caches
1055 1055 self.invalidate()
1056 1056 self.invalidatedirstate()
1057 1057
1058 1058 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc):
1059 1059 try:
1060 1060 l = lockmod.lock(vfs, lockname, 0, releasefn, desc=desc)
1061 1061 except error.LockHeld, inst:
1062 1062 if not wait:
1063 1063 raise
1064 1064 self.ui.warn(_("waiting for lock on %s held by %r\n") %
1065 1065 (desc, inst.locker))
1066 1066 # default to 600 seconds timeout
1067 1067 l = lockmod.lock(vfs, lockname,
1068 1068 int(self.ui.config("ui", "timeout", "600")),
1069 1069 releasefn, desc=desc)
1070 1070 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
1071 1071 if acquirefn:
1072 1072 acquirefn()
1073 1073 return l
1074 1074
1075 1075 def _afterlock(self, callback):
1076 1076 """add a callback to the current repository lock.
1077 1077
1078 1078 The callback will be executed on lock release."""
1079 1079 l = self._lockref and self._lockref()
1080 1080 if l:
1081 1081 l.postrelease.append(callback)
1082 1082 else:
1083 1083 callback()
1084 1084
1085 1085 def lock(self, wait=True):
1086 1086 '''Lock the repository store (.hg/store) and return a weak reference
1087 1087 to the lock. Use this before modifying the store (e.g. committing or
1088 1088 stripping). If you are opening a transaction, get a lock as well.)'''
1089 1089 l = self._lockref and self._lockref()
1090 1090 if l is not None and l.held:
1091 1091 l.lock()
1092 1092 return l
1093 1093
1094 1094 def unlock():
1095 1095 for k, ce in self._filecache.items():
1096 1096 if k == 'dirstate' or k not in self.__dict__:
1097 1097 continue
1098 1098 ce.refresh()
1099 1099
1100 1100 l = self._lock(self.svfs, "lock", wait, unlock,
1101 1101 self.invalidate, _('repository %s') % self.origroot)
1102 1102 self._lockref = weakref.ref(l)
1103 1103 return l
1104 1104
1105 1105 def wlock(self, wait=True):
1106 1106 '''Lock the non-store parts of the repository (everything under
1107 1107 .hg except .hg/store) and return a weak reference to the lock.
1108 1108 Use this before modifying files in .hg.'''
1109 1109 l = self._wlockref and self._wlockref()
1110 1110 if l is not None and l.held:
1111 1111 l.lock()
1112 1112 return l
1113 1113
1114 1114 def unlock():
1115 1115 if self.dirstate.pendingparentchange():
1116 1116 self.dirstate.invalidate()
1117 1117 else:
1118 1118 self.dirstate.write()
1119 1119
1120 1120 self._filecache['dirstate'].refresh()
1121 1121
1122 1122 l = self._lock(self.vfs, "wlock", wait, unlock,
1123 1123 self.invalidatedirstate, _('working directory of %s') %
1124 1124 self.origroot)
1125 1125 self._wlockref = weakref.ref(l)
1126 1126 return l
1127 1127
1128 1128 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1129 1129 """
1130 1130 commit an individual file as part of a larger transaction
1131 1131 """
1132 1132
1133 1133 fname = fctx.path()
1134 1134 text = fctx.data()
1135 1135 flog = self.file(fname)
1136 1136 fparent1 = manifest1.get(fname, nullid)
1137 1137 fparent2 = manifest2.get(fname, nullid)
1138 1138
1139 1139 meta = {}
1140 1140 copy = fctx.renamed()
1141 1141 if copy and copy[0] != fname:
1142 1142 # Mark the new revision of this file as a copy of another
1143 1143 # file. This copy data will effectively act as a parent
1144 1144 # of this new revision. If this is a merge, the first
1145 1145 # parent will be the nullid (meaning "look up the copy data")
1146 1146 # and the second one will be the other parent. For example:
1147 1147 #
1148 1148 # 0 --- 1 --- 3 rev1 changes file foo
1149 1149 # \ / rev2 renames foo to bar and changes it
1150 1150 # \- 2 -/ rev3 should have bar with all changes and
1151 1151 # should record that bar descends from
1152 1152 # bar in rev2 and foo in rev1
1153 1153 #
1154 1154 # this allows this merge to succeed:
1155 1155 #
1156 1156 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1157 1157 # \ / merging rev3 and rev4 should use bar@rev2
1158 1158 # \- 2 --- 4 as the merge base
1159 1159 #
1160 1160
1161 1161 cfname = copy[0]
1162 1162 crev = manifest1.get(cfname)
1163 1163 newfparent = fparent2
1164 1164
1165 1165 if manifest2: # branch merge
1166 1166 if fparent2 == nullid or crev is None: # copied on remote side
1167 1167 if cfname in manifest2:
1168 1168 crev = manifest2[cfname]
1169 1169 newfparent = fparent1
1170 1170
1171 1171 # find source in nearest ancestor if we've lost track
1172 1172 if not crev:
1173 1173 self.ui.debug(" %s: searching for copy revision for %s\n" %
1174 1174 (fname, cfname))
1175 1175 for ancestor in self[None].ancestors():
1176 1176 if cfname in ancestor:
1177 1177 crev = ancestor[cfname].filenode()
1178 1178 break
1179 1179
1180 1180 if crev:
1181 1181 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1182 1182 meta["copy"] = cfname
1183 1183 meta["copyrev"] = hex(crev)
1184 1184 fparent1, fparent2 = nullid, newfparent
1185 1185 else:
1186 1186 self.ui.warn(_("warning: can't find ancestor for '%s' "
1187 1187 "copied from '%s'!\n") % (fname, cfname))
1188 1188
1189 1189 elif fparent1 == nullid:
1190 1190 fparent1, fparent2 = fparent2, nullid
1191 1191 elif fparent2 != nullid:
1192 1192 # is one parent an ancestor of the other?
1193 1193 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1194 1194 if fparent1 in fparentancestors:
1195 1195 fparent1, fparent2 = fparent2, nullid
1196 1196 elif fparent2 in fparentancestors:
1197 1197 fparent2 = nullid
1198 1198
1199 1199 # is the file changed?
1200 1200 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1201 1201 changelist.append(fname)
1202 1202 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1203 1203 # are just the flags changed during merge?
1204 1204 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1205 1205 changelist.append(fname)
1206 1206
1207 1207 return fparent1
1208 1208
1209 1209 @unfilteredmethod
1210 1210 def commit(self, text="", user=None, date=None, match=None, force=False,
1211 1211 editor=False, extra={}):
1212 1212 """Add a new revision to current repository.
1213 1213
1214 1214 Revision information is gathered from the working directory,
1215 1215 match can be used to filter the committed files. If editor is
1216 1216 supplied, it is called to get a commit message.
1217 1217 """
1218 1218
1219 1219 def fail(f, msg):
1220 1220 raise util.Abort('%s: %s' % (f, msg))
1221 1221
1222 1222 if not match:
1223 1223 match = matchmod.always(self.root, '')
1224 1224
1225 1225 if not force:
1226 1226 vdirs = []
1227 1227 match.explicitdir = vdirs.append
1228 1228 match.bad = fail
1229 1229
1230 1230 wlock = self.wlock()
1231 1231 try:
1232 1232 wctx = self[None]
1233 1233 merge = len(wctx.parents()) > 1
1234 1234
1235 1235 if (not force and merge and match and
1236 1236 (match.files() or match.anypats())):
1237 1237 raise util.Abort(_('cannot partially commit a merge '
1238 1238 '(do not specify files or patterns)'))
1239 1239
1240 1240 status = self.status(match=match, clean=force)
1241 1241 if force:
1242 1242 status.modified.extend(status.clean) # mq may commit clean files
1243 1243
1244 1244 # check subrepos
1245 1245 subs = []
1246 1246 commitsubs = set()
1247 1247 newstate = wctx.substate.copy()
1248 1248 # only manage subrepos and .hgsubstate if .hgsub is present
1249 1249 if '.hgsub' in wctx:
1250 1250 # we'll decide whether to track this ourselves, thanks
1251 1251 for c in status.modified, status.added, status.removed:
1252 1252 if '.hgsubstate' in c:
1253 1253 c.remove('.hgsubstate')
1254 1254
1255 1255 # compare current state to last committed state
1256 1256 # build new substate based on last committed state
1257 1257 oldstate = wctx.p1().substate
1258 1258 for s in sorted(newstate.keys()):
1259 1259 if not match(s):
1260 1260 # ignore working copy, use old state if present
1261 1261 if s in oldstate:
1262 1262 newstate[s] = oldstate[s]
1263 1263 continue
1264 1264 if not force:
1265 1265 raise util.Abort(
1266 1266 _("commit with new subrepo %s excluded") % s)
1267 1267 if wctx.sub(s).dirty(True):
1268 1268 if not self.ui.configbool('ui', 'commitsubrepos'):
1269 1269 raise util.Abort(
1270 1270 _("uncommitted changes in subrepo %s") % s,
1271 1271 hint=_("use --subrepos for recursive commit"))
1272 1272 subs.append(s)
1273 1273 commitsubs.add(s)
1274 1274 else:
1275 1275 bs = wctx.sub(s).basestate()
1276 1276 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1277 1277 if oldstate.get(s, (None, None, None))[1] != bs:
1278 1278 subs.append(s)
1279 1279
1280 1280 # check for removed subrepos
1281 1281 for p in wctx.parents():
1282 1282 r = [s for s in p.substate if s not in newstate]
1283 1283 subs += [s for s in r if match(s)]
1284 1284 if subs:
1285 1285 if (not match('.hgsub') and
1286 1286 '.hgsub' in (wctx.modified() + wctx.added())):
1287 1287 raise util.Abort(
1288 1288 _("can't commit subrepos without .hgsub"))
1289 1289 status.modified.insert(0, '.hgsubstate')
1290 1290
1291 1291 elif '.hgsub' in status.removed:
1292 1292 # clean up .hgsubstate when .hgsub is removed
1293 1293 if ('.hgsubstate' in wctx and
1294 1294 '.hgsubstate' not in (status.modified + status.added +
1295 1295 status.removed)):
1296 1296 status.removed.insert(0, '.hgsubstate')
1297 1297
1298 1298 # make sure all explicit patterns are matched
1299 1299 if not force and match.files():
1300 1300 matched = set(status.modified + status.added + status.removed)
1301 1301
1302 1302 for f in match.files():
1303 1303 f = self.dirstate.normalize(f)
1304 1304 if f == '.' or f in matched or f in wctx.substate:
1305 1305 continue
1306 1306 if f in status.deleted:
1307 1307 fail(f, _('file not found!'))
1308 1308 if f in vdirs: # visited directory
1309 1309 d = f + '/'
1310 1310 for mf in matched:
1311 1311 if mf.startswith(d):
1312 1312 break
1313 1313 else:
1314 1314 fail(f, _("no match under directory!"))
1315 1315 elif f not in self.dirstate:
1316 1316 fail(f, _("file not tracked!"))
1317 1317
1318 1318 cctx = context.workingctx(self, text, user, date, extra, status)
1319 1319
1320 1320 if (not force and not extra.get("close") and not merge
1321 1321 and not cctx.files()
1322 1322 and wctx.branch() == wctx.p1().branch()):
1323 1323 return None
1324 1324
1325 1325 if merge and cctx.deleted():
1326 1326 raise util.Abort(_("cannot commit merge with missing files"))
1327 1327
1328 1328 ms = mergemod.mergestate(self)
1329 1329 for f in status.modified:
1330 1330 if f in ms and ms[f] == 'u':
1331 1331 raise util.Abort(_("unresolved merge conflicts "
1332 1332 "(see hg help resolve)"))
1333 1333
1334 1334 if editor:
1335 1335 cctx._text = editor(self, cctx, subs)
1336 1336 edited = (text != cctx._text)
1337 1337
1338 1338 # Save commit message in case this transaction gets rolled back
1339 1339 # (e.g. by a pretxncommit hook). Leave the content alone on
1340 1340 # the assumption that the user will use the same editor again.
1341 1341 msgfn = self.savecommitmessage(cctx._text)
1342 1342
1343 1343 # commit subs and write new state
1344 1344 if subs:
1345 1345 for s in sorted(commitsubs):
1346 1346 sub = wctx.sub(s)
1347 1347 self.ui.status(_('committing subrepository %s\n') %
1348 1348 subrepo.subrelpath(sub))
1349 1349 sr = sub.commit(cctx._text, user, date)
1350 1350 newstate[s] = (newstate[s][0], sr)
1351 1351 subrepo.writestate(self, newstate)
1352 1352
1353 1353 p1, p2 = self.dirstate.parents()
1354 1354 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1355 1355 try:
1356 1356 self.hook("precommit", throw=True, parent1=hookp1,
1357 1357 parent2=hookp2)
1358 1358 ret = self.commitctx(cctx, True)
1359 1359 except: # re-raises
1360 1360 if edited:
1361 1361 self.ui.write(
1362 1362 _('note: commit message saved in %s\n') % msgfn)
1363 1363 raise
1364 1364
1365 1365 # update bookmarks, dirstate and mergestate
1366 1366 bookmarks.update(self, [p1, p2], ret)
1367 1367 cctx.markcommitted(ret)
1368 1368 ms.reset()
1369 1369 finally:
1370 1370 wlock.release()
1371 1371
1372 1372 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1373 1373 # hack for command that use a temporary commit (eg: histedit)
1374 1374 # temporary commit got stripped before hook release
1375 1375 if node in self:
1376 1376 self.hook("commit", node=node, parent1=parent1,
1377 1377 parent2=parent2)
1378 1378 self._afterlock(commithook)
1379 1379 return ret
1380 1380
1381 1381 @unfilteredmethod
1382 1382 def commitctx(self, ctx, error=False):
1383 1383 """Add a new revision to current repository.
1384 1384 Revision information is passed via the context argument.
1385 1385 """
1386 1386
1387 1387 tr = None
1388 1388 p1, p2 = ctx.p1(), ctx.p2()
1389 1389 user = ctx.user()
1390 1390
1391 1391 lock = self.lock()
1392 1392 try:
1393 1393 tr = self.transaction("commit")
1394 1394 trp = weakref.proxy(tr)
1395 1395
1396 1396 if ctx.files():
1397 1397 m1 = p1.manifest()
1398 1398 m2 = p2.manifest()
1399 1399 m = m1.copy()
1400 1400
1401 1401 # check in files
1402 1402 added = []
1403 1403 changed = []
1404 1404 removed = list(ctx.removed())
1405 1405 linkrev = len(self)
1406 1406 for f in sorted(ctx.modified() + ctx.added()):
1407 1407 self.ui.note(f + "\n")
1408 1408 try:
1409 1409 fctx = ctx[f]
1410 1410 if fctx is None:
1411 1411 removed.append(f)
1412 1412 else:
1413 1413 added.append(f)
1414 1414 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1415 1415 trp, changed)
1416 1416 m.setflag(f, fctx.flags())
1417 1417 except OSError, inst:
1418 1418 self.ui.warn(_("trouble committing %s!\n") % f)
1419 1419 raise
1420 1420 except IOError, inst:
1421 1421 errcode = getattr(inst, 'errno', errno.ENOENT)
1422 1422 if error or errcode and errcode != errno.ENOENT:
1423 1423 self.ui.warn(_("trouble committing %s!\n") % f)
1424 1424 raise
1425 1425
1426 1426 # update manifest
1427 1427 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1428 1428 drop = [f for f in removed if f in m]
1429 1429 for f in drop:
1430 1430 del m[f]
1431 1431 mn = self.manifest.add(m, trp, linkrev,
1432 1432 p1.manifestnode(), p2.manifestnode(),
1433 1433 added, drop)
1434 1434 files = changed + removed
1435 1435 else:
1436 1436 mn = p1.manifestnode()
1437 1437 files = []
1438 1438
1439 1439 # update changelog
1440 self.changelog.delayupdate()
1440 self.changelog.delayupdate(tr)
1441 1441 n = self.changelog.add(mn, files, ctx.description(),
1442 1442 trp, p1.node(), p2.node(),
1443 1443 user, ctx.date(), ctx.extra().copy())
1444 p = lambda: self.changelog.writepending() and self.root or ""
1444 p = lambda: tr.writepending() and self.root or ""
1445 1445 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1446 1446 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1447 1447 parent2=xp2, pending=p)
1448 1448 self.changelog.finalize(trp)
1449 1449 # set the new commit is proper phase
1450 1450 targetphase = subrepo.newcommitphase(self.ui, ctx)
1451 1451 if targetphase:
1452 1452 # retract boundary do not alter parent changeset.
1453 1453 # if a parent have higher the resulting phase will
1454 1454 # be compliant anyway
1455 1455 #
1456 1456 # if minimal phase was 0 we don't need to retract anything
1457 1457 phases.retractboundary(self, tr, targetphase, [n])
1458 1458 tr.close()
1459 1459 branchmap.updatecache(self.filtered('served'))
1460 1460 return n
1461 1461 finally:
1462 1462 if tr:
1463 1463 tr.release()
1464 1464 lock.release()
1465 1465
1466 1466 @unfilteredmethod
1467 1467 def destroying(self):
1468 1468 '''Inform the repository that nodes are about to be destroyed.
1469 1469 Intended for use by strip and rollback, so there's a common
1470 1470 place for anything that has to be done before destroying history.
1471 1471
1472 1472 This is mostly useful for saving state that is in memory and waiting
1473 1473 to be flushed when the current lock is released. Because a call to
1474 1474 destroyed is imminent, the repo will be invalidated causing those
1475 1475 changes to stay in memory (waiting for the next unlock), or vanish
1476 1476 completely.
1477 1477 '''
1478 1478 # When using the same lock to commit and strip, the phasecache is left
1479 1479 # dirty after committing. Then when we strip, the repo is invalidated,
1480 1480 # causing those changes to disappear.
1481 1481 if '_phasecache' in vars(self):
1482 1482 self._phasecache.write()
1483 1483
1484 1484 @unfilteredmethod
1485 1485 def destroyed(self):
1486 1486 '''Inform the repository that nodes have been destroyed.
1487 1487 Intended for use by strip and rollback, so there's a common
1488 1488 place for anything that has to be done after destroying history.
1489 1489 '''
1490 1490 # When one tries to:
1491 1491 # 1) destroy nodes thus calling this method (e.g. strip)
1492 1492 # 2) use phasecache somewhere (e.g. commit)
1493 1493 #
1494 1494 # then 2) will fail because the phasecache contains nodes that were
1495 1495 # removed. We can either remove phasecache from the filecache,
1496 1496 # causing it to reload next time it is accessed, or simply filter
1497 1497 # the removed nodes now and write the updated cache.
1498 1498 self._phasecache.filterunknown(self)
1499 1499 self._phasecache.write()
1500 1500
1501 1501 # update the 'served' branch cache to help read only server process
1502 1502 # Thanks to branchcache collaboration this is done from the nearest
1503 1503 # filtered subset and it is expected to be fast.
1504 1504 branchmap.updatecache(self.filtered('served'))
1505 1505
1506 1506 # Ensure the persistent tag cache is updated. Doing it now
1507 1507 # means that the tag cache only has to worry about destroyed
1508 1508 # heads immediately after a strip/rollback. That in turn
1509 1509 # guarantees that "cachetip == currenttip" (comparing both rev
1510 1510 # and node) always means no nodes have been added or destroyed.
1511 1511
1512 1512 # XXX this is suboptimal when qrefresh'ing: we strip the current
1513 1513 # head, refresh the tag cache, then immediately add a new head.
1514 1514 # But I think doing it this way is necessary for the "instant
1515 1515 # tag cache retrieval" case to work.
1516 1516 self.invalidate()
1517 1517
1518 1518 def walk(self, match, node=None):
1519 1519 '''
1520 1520 walk recursively through the directory tree or a given
1521 1521 changeset, finding all files matched by the match
1522 1522 function
1523 1523 '''
1524 1524 return self[node].walk(match)
1525 1525
1526 1526 def status(self, node1='.', node2=None, match=None,
1527 1527 ignored=False, clean=False, unknown=False,
1528 1528 listsubrepos=False):
1529 1529 '''a convenience method that calls node1.status(node2)'''
1530 1530 return self[node1].status(node2, match, ignored, clean, unknown,
1531 1531 listsubrepos)
1532 1532
1533 1533 def heads(self, start=None):
1534 1534 heads = self.changelog.heads(start)
1535 1535 # sort the output in rev descending order
1536 1536 return sorted(heads, key=self.changelog.rev, reverse=True)
1537 1537
1538 1538 def branchheads(self, branch=None, start=None, closed=False):
1539 1539 '''return a (possibly filtered) list of heads for the given branch
1540 1540
1541 1541 Heads are returned in topological order, from newest to oldest.
1542 1542 If branch is None, use the dirstate branch.
1543 1543 If start is not None, return only heads reachable from start.
1544 1544 If closed is True, return heads that are marked as closed as well.
1545 1545 '''
1546 1546 if branch is None:
1547 1547 branch = self[None].branch()
1548 1548 branches = self.branchmap()
1549 1549 if branch not in branches:
1550 1550 return []
1551 1551 # the cache returns heads ordered lowest to highest
1552 1552 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
1553 1553 if start is not None:
1554 1554 # filter out the heads that cannot be reached from startrev
1555 1555 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
1556 1556 bheads = [h for h in bheads if h in fbheads]
1557 1557 return bheads
1558 1558
1559 1559 def branches(self, nodes):
1560 1560 if not nodes:
1561 1561 nodes = [self.changelog.tip()]
1562 1562 b = []
1563 1563 for n in nodes:
1564 1564 t = n
1565 1565 while True:
1566 1566 p = self.changelog.parents(n)
1567 1567 if p[1] != nullid or p[0] == nullid:
1568 1568 b.append((t, n, p[0], p[1]))
1569 1569 break
1570 1570 n = p[0]
1571 1571 return b
1572 1572
1573 1573 def between(self, pairs):
1574 1574 r = []
1575 1575
1576 1576 for top, bottom in pairs:
1577 1577 n, l, i = top, [], 0
1578 1578 f = 1
1579 1579
1580 1580 while n != bottom and n != nullid:
1581 1581 p = self.changelog.parents(n)[0]
1582 1582 if i == f:
1583 1583 l.append(n)
1584 1584 f = f * 2
1585 1585 n = p
1586 1586 i += 1
1587 1587
1588 1588 r.append(l)
1589 1589
1590 1590 return r
1591 1591
1592 1592 def checkpush(self, pushop):
1593 1593 """Extensions can override this function if additional checks have
1594 1594 to be performed before pushing, or call it if they override push
1595 1595 command.
1596 1596 """
1597 1597 pass
1598 1598
1599 1599 @unfilteredpropertycache
1600 1600 def prepushoutgoinghooks(self):
1601 1601 """Return util.hooks consists of "(repo, remote, outgoing)"
1602 1602 functions, which are called before pushing changesets.
1603 1603 """
1604 1604 return util.hooks()
1605 1605
1606 1606 def stream_in(self, remote, requirements):
1607 1607 lock = self.lock()
1608 1608 try:
1609 1609 # Save remote branchmap. We will use it later
1610 1610 # to speed up branchcache creation
1611 1611 rbranchmap = None
1612 1612 if remote.capable("branchmap"):
1613 1613 rbranchmap = remote.branchmap()
1614 1614
1615 1615 fp = remote.stream_out()
1616 1616 l = fp.readline()
1617 1617 try:
1618 1618 resp = int(l)
1619 1619 except ValueError:
1620 1620 raise error.ResponseError(
1621 1621 _('unexpected response from remote server:'), l)
1622 1622 if resp == 1:
1623 1623 raise util.Abort(_('operation forbidden by server'))
1624 1624 elif resp == 2:
1625 1625 raise util.Abort(_('locking the remote repository failed'))
1626 1626 elif resp != 0:
1627 1627 raise util.Abort(_('the server sent an unknown error code'))
1628 1628 self.ui.status(_('streaming all changes\n'))
1629 1629 l = fp.readline()
1630 1630 try:
1631 1631 total_files, total_bytes = map(int, l.split(' ', 1))
1632 1632 except (ValueError, TypeError):
1633 1633 raise error.ResponseError(
1634 1634 _('unexpected response from remote server:'), l)
1635 1635 self.ui.status(_('%d files to transfer, %s of data\n') %
1636 1636 (total_files, util.bytecount(total_bytes)))
1637 1637 handled_bytes = 0
1638 1638 self.ui.progress(_('clone'), 0, total=total_bytes)
1639 1639 start = time.time()
1640 1640
1641 1641 tr = self.transaction(_('clone'))
1642 1642 try:
1643 1643 for i in xrange(total_files):
1644 1644 # XXX doesn't support '\n' or '\r' in filenames
1645 1645 l = fp.readline()
1646 1646 try:
1647 1647 name, size = l.split('\0', 1)
1648 1648 size = int(size)
1649 1649 except (ValueError, TypeError):
1650 1650 raise error.ResponseError(
1651 1651 _('unexpected response from remote server:'), l)
1652 1652 if self.ui.debugflag:
1653 1653 self.ui.debug('adding %s (%s)\n' %
1654 1654 (name, util.bytecount(size)))
1655 1655 # for backwards compat, name was partially encoded
1656 1656 ofp = self.sopener(store.decodedir(name), 'w')
1657 1657 for chunk in util.filechunkiter(fp, limit=size):
1658 1658 handled_bytes += len(chunk)
1659 1659 self.ui.progress(_('clone'), handled_bytes,
1660 1660 total=total_bytes)
1661 1661 ofp.write(chunk)
1662 1662 ofp.close()
1663 1663 tr.close()
1664 1664 finally:
1665 1665 tr.release()
1666 1666
1667 1667 # Writing straight to files circumvented the inmemory caches
1668 1668 self.invalidate()
1669 1669
1670 1670 elapsed = time.time() - start
1671 1671 if elapsed <= 0:
1672 1672 elapsed = 0.001
1673 1673 self.ui.progress(_('clone'), None)
1674 1674 self.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
1675 1675 (util.bytecount(total_bytes), elapsed,
1676 1676 util.bytecount(total_bytes / elapsed)))
1677 1677
1678 1678 # new requirements = old non-format requirements +
1679 1679 # new format-related
1680 1680 # requirements from the streamed-in repository
1681 1681 requirements.update(set(self.requirements) - self.supportedformats)
1682 1682 self._applyrequirements(requirements)
1683 1683 self._writerequirements()
1684 1684
1685 1685 if rbranchmap:
1686 1686 rbheads = []
1687 1687 closed = []
1688 1688 for bheads in rbranchmap.itervalues():
1689 1689 rbheads.extend(bheads)
1690 1690 for h in bheads:
1691 1691 r = self.changelog.rev(h)
1692 1692 b, c = self.changelog.branchinfo(r)
1693 1693 if c:
1694 1694 closed.append(h)
1695 1695
1696 1696 if rbheads:
1697 1697 rtiprev = max((int(self.changelog.rev(node))
1698 1698 for node in rbheads))
1699 1699 cache = branchmap.branchcache(rbranchmap,
1700 1700 self[rtiprev].node(),
1701 1701 rtiprev,
1702 1702 closednodes=closed)
1703 1703 # Try to stick it as low as possible
1704 1704 # filter above served are unlikely to be fetch from a clone
1705 1705 for candidate in ('base', 'immutable', 'served'):
1706 1706 rview = self.filtered(candidate)
1707 1707 if cache.validfor(rview):
1708 1708 self._branchcaches[candidate] = cache
1709 1709 cache.write(rview)
1710 1710 break
1711 1711 self.invalidate()
1712 1712 return len(self.heads()) + 1
1713 1713 finally:
1714 1714 lock.release()
1715 1715
1716 1716 def clone(self, remote, heads=[], stream=False):
1717 1717 '''clone remote repository.
1718 1718
1719 1719 keyword arguments:
1720 1720 heads: list of revs to clone (forces use of pull)
1721 1721 stream: use streaming clone if possible'''
1722 1722
1723 1723 # now, all clients that can request uncompressed clones can
1724 1724 # read repo formats supported by all servers that can serve
1725 1725 # them.
1726 1726
1727 1727 # if revlog format changes, client will have to check version
1728 1728 # and format flags on "stream" capability, and use
1729 1729 # uncompressed only if compatible.
1730 1730
1731 1731 if not stream:
1732 1732 # if the server explicitly prefers to stream (for fast LANs)
1733 1733 stream = remote.capable('stream-preferred')
1734 1734
1735 1735 if stream and not heads:
1736 1736 # 'stream' means remote revlog format is revlogv1 only
1737 1737 if remote.capable('stream'):
1738 1738 self.stream_in(remote, set(('revlogv1',)))
1739 1739 else:
1740 1740 # otherwise, 'streamreqs' contains the remote revlog format
1741 1741 streamreqs = remote.capable('streamreqs')
1742 1742 if streamreqs:
1743 1743 streamreqs = set(streamreqs.split(','))
1744 1744 # if we support it, stream in and adjust our requirements
1745 1745 if not streamreqs - self.supportedformats:
1746 1746 self.stream_in(remote, streamreqs)
1747 1747
1748 1748 quiet = self.ui.backupconfig('ui', 'quietbookmarkmove')
1749 1749 try:
1750 1750 self.ui.setconfig('ui', 'quietbookmarkmove', True, 'clone')
1751 1751 ret = exchange.pull(self, remote, heads).cgresult
1752 1752 finally:
1753 1753 self.ui.restoreconfig(quiet)
1754 1754 return ret
1755 1755
1756 1756 def pushkey(self, namespace, key, old, new):
1757 1757 self.hook('prepushkey', throw=True, namespace=namespace, key=key,
1758 1758 old=old, new=new)
1759 1759 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
1760 1760 ret = pushkey.push(self, namespace, key, old, new)
1761 1761 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
1762 1762 ret=ret)
1763 1763 return ret
1764 1764
1765 1765 def listkeys(self, namespace):
1766 1766 self.hook('prelistkeys', throw=True, namespace=namespace)
1767 1767 self.ui.debug('listing keys for "%s"\n' % namespace)
1768 1768 values = pushkey.list(self, namespace)
1769 1769 self.hook('listkeys', namespace=namespace, values=values)
1770 1770 return values
1771 1771
1772 1772 def debugwireargs(self, one, two, three=None, four=None, five=None):
1773 1773 '''used to test argument passing over the wire'''
1774 1774 return "%s %s %s %s %s" % (one, two, three, four, five)
1775 1775
1776 1776 def savecommitmessage(self, text):
1777 1777 fp = self.opener('last-message.txt', 'wb')
1778 1778 try:
1779 1779 fp.write(text)
1780 1780 finally:
1781 1781 fp.close()
1782 1782 return self.pathto(fp.name[len(self.root) + 1:])
1783 1783
1784 1784 # used to avoid circular references so destructors work
1785 1785 def aftertrans(files):
1786 1786 renamefiles = [tuple(t) for t in files]
1787 1787 def a():
1788 1788 for vfs, src, dest in renamefiles:
1789 1789 try:
1790 1790 vfs.rename(src, dest)
1791 1791 except OSError: # journal file does not yet exist
1792 1792 pass
1793 1793 return a
1794 1794
1795 1795 def undoname(fn):
1796 1796 base, name = os.path.split(fn)
1797 1797 assert name.startswith('journal')
1798 1798 return os.path.join(base, name.replace('journal', 'undo', 1))
1799 1799
1800 1800 def instance(ui, path, create):
1801 1801 return localrepository(ui, util.urllocalpath(path), create)
1802 1802
1803 1803 def islocal(path):
1804 1804 return True
General Comments 0
You need to be logged in to leave comments. Login now