##// END OF EJS Templates
phases: allow registration and boundary advancement with revision sets...
Joerg Sonnenberger -
r46374:09735cde default
parent child Browse files
Show More
@@ -1,1702 +1,1703 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 from __future__ import absolute_import
9 9
10 10 import os
11 11 import struct
12 12 import weakref
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 hex,
17 17 nullid,
18 18 nullrev,
19 19 short,
20 20 )
21 21 from .pycompat import open
22 22
23 23 from . import (
24 24 error,
25 25 match as matchmod,
26 26 mdiff,
27 27 phases,
28 28 pycompat,
29 29 requirements,
30 30 scmutil,
31 31 util,
32 32 )
33 33
34 34 from .interfaces import repository
35 35
36 36 _CHANGEGROUPV1_DELTA_HEADER = struct.Struct(b"20s20s20s20s")
37 37 _CHANGEGROUPV2_DELTA_HEADER = struct.Struct(b"20s20s20s20s20s")
38 38 _CHANGEGROUPV3_DELTA_HEADER = struct.Struct(b">20s20s20s20s20sH")
39 39
40 40 LFS_REQUIREMENT = b'lfs'
41 41
42 42 readexactly = util.readexactly
43 43
44 44
45 45 def getchunk(stream):
46 46 """return the next chunk from stream as a string"""
47 47 d = readexactly(stream, 4)
48 48 l = struct.unpack(b">l", d)[0]
49 49 if l <= 4:
50 50 if l:
51 51 raise error.Abort(_(b"invalid chunk length %d") % l)
52 52 return b""
53 53 return readexactly(stream, l - 4)
54 54
55 55
56 56 def chunkheader(length):
57 57 """return a changegroup chunk header (string)"""
58 58 return struct.pack(b">l", length + 4)
59 59
60 60
61 61 def closechunk():
62 62 """return a changegroup chunk header (string) for a zero-length chunk"""
63 63 return struct.pack(b">l", 0)
64 64
65 65
66 66 def _fileheader(path):
67 67 """Obtain a changegroup chunk header for a named path."""
68 68 return chunkheader(len(path)) + path
69 69
70 70
71 71 def writechunks(ui, chunks, filename, vfs=None):
72 72 """Write chunks to a file and return its filename.
73 73
74 74 The stream is assumed to be a bundle file.
75 75 Existing files will not be overwritten.
76 76 If no filename is specified, a temporary file is created.
77 77 """
78 78 fh = None
79 79 cleanup = None
80 80 try:
81 81 if filename:
82 82 if vfs:
83 83 fh = vfs.open(filename, b"wb")
84 84 else:
85 85 # Increase default buffer size because default is usually
86 86 # small (4k is common on Linux).
87 87 fh = open(filename, b"wb", 131072)
88 88 else:
89 89 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
90 90 fh = os.fdopen(fd, "wb")
91 91 cleanup = filename
92 92 for c in chunks:
93 93 fh.write(c)
94 94 cleanup = None
95 95 return filename
96 96 finally:
97 97 if fh is not None:
98 98 fh.close()
99 99 if cleanup is not None:
100 100 if filename and vfs:
101 101 vfs.unlink(cleanup)
102 102 else:
103 103 os.unlink(cleanup)
104 104
105 105
106 106 class cg1unpacker(object):
107 107 """Unpacker for cg1 changegroup streams.
108 108
109 109 A changegroup unpacker handles the framing of the revision data in
110 110 the wire format. Most consumers will want to use the apply()
111 111 method to add the changes from the changegroup to a repository.
112 112
113 113 If you're forwarding a changegroup unmodified to another consumer,
114 114 use getchunks(), which returns an iterator of changegroup
115 115 chunks. This is mostly useful for cases where you need to know the
116 116 data stream has ended by observing the end of the changegroup.
117 117
118 118 deltachunk() is useful only if you're applying delta data. Most
119 119 consumers should prefer apply() instead.
120 120
121 121 A few other public methods exist. Those are used only for
122 122 bundlerepo and some debug commands - their use is discouraged.
123 123 """
124 124
125 125 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
126 126 deltaheadersize = deltaheader.size
127 127 version = b'01'
128 128 _grouplistcount = 1 # One list of files after the manifests
129 129
130 130 def __init__(self, fh, alg, extras=None):
131 131 if alg is None:
132 132 alg = b'UN'
133 133 if alg not in util.compengines.supportedbundletypes:
134 134 raise error.Abort(_(b'unknown stream compression type: %s') % alg)
135 135 if alg == b'BZ':
136 136 alg = b'_truncatedBZ'
137 137
138 138 compengine = util.compengines.forbundletype(alg)
139 139 self._stream = compengine.decompressorreader(fh)
140 140 self._type = alg
141 141 self.extras = extras or {}
142 142 self.callback = None
143 143
144 144 # These methods (compressed, read, seek, tell) all appear to only
145 145 # be used by bundlerepo, but it's a little hard to tell.
146 146 def compressed(self):
147 147 return self._type is not None and self._type != b'UN'
148 148
149 149 def read(self, l):
150 150 return self._stream.read(l)
151 151
152 152 def seek(self, pos):
153 153 return self._stream.seek(pos)
154 154
155 155 def tell(self):
156 156 return self._stream.tell()
157 157
158 158 def close(self):
159 159 return self._stream.close()
160 160
161 161 def _chunklength(self):
162 162 d = readexactly(self._stream, 4)
163 163 l = struct.unpack(b">l", d)[0]
164 164 if l <= 4:
165 165 if l:
166 166 raise error.Abort(_(b"invalid chunk length %d") % l)
167 167 return 0
168 168 if self.callback:
169 169 self.callback()
170 170 return l - 4
171 171
172 172 def changelogheader(self):
173 173 """v10 does not have a changelog header chunk"""
174 174 return {}
175 175
176 176 def manifestheader(self):
177 177 """v10 does not have a manifest header chunk"""
178 178 return {}
179 179
180 180 def filelogheader(self):
181 181 """return the header of the filelogs chunk, v10 only has the filename"""
182 182 l = self._chunklength()
183 183 if not l:
184 184 return {}
185 185 fname = readexactly(self._stream, l)
186 186 return {b'filename': fname}
187 187
188 188 def _deltaheader(self, headertuple, prevnode):
189 189 node, p1, p2, cs = headertuple
190 190 if prevnode is None:
191 191 deltabase = p1
192 192 else:
193 193 deltabase = prevnode
194 194 flags = 0
195 195 return node, p1, p2, deltabase, cs, flags
196 196
197 197 def deltachunk(self, prevnode):
198 198 l = self._chunklength()
199 199 if not l:
200 200 return {}
201 201 headerdata = readexactly(self._stream, self.deltaheadersize)
202 202 header = self.deltaheader.unpack(headerdata)
203 203 delta = readexactly(self._stream, l - self.deltaheadersize)
204 204 node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode)
205 205 return (node, p1, p2, cs, deltabase, delta, flags)
206 206
207 207 def getchunks(self):
208 208 """returns all the chunks contains in the bundle
209 209
210 210 Used when you need to forward the binary stream to a file or another
211 211 network API. To do so, it parse the changegroup data, otherwise it will
212 212 block in case of sshrepo because it don't know the end of the stream.
213 213 """
214 214 # For changegroup 1 and 2, we expect 3 parts: changelog, manifestlog,
215 215 # and a list of filelogs. For changegroup 3, we expect 4 parts:
216 216 # changelog, manifestlog, a list of tree manifestlogs, and a list of
217 217 # filelogs.
218 218 #
219 219 # Changelog and manifestlog parts are terminated with empty chunks. The
220 220 # tree and file parts are a list of entry sections. Each entry section
221 221 # is a series of chunks terminating in an empty chunk. The list of these
222 222 # entry sections is terminated in yet another empty chunk, so we know
223 223 # we've reached the end of the tree/file list when we reach an empty
224 224 # chunk that was proceeded by no non-empty chunks.
225 225
226 226 parts = 0
227 227 while parts < 2 + self._grouplistcount:
228 228 noentries = True
229 229 while True:
230 230 chunk = getchunk(self)
231 231 if not chunk:
232 232 # The first two empty chunks represent the end of the
233 233 # changelog and the manifestlog portions. The remaining
234 234 # empty chunks represent either A) the end of individual
235 235 # tree or file entries in the file list, or B) the end of
236 236 # the entire list. It's the end of the entire list if there
237 237 # were no entries (i.e. noentries is True).
238 238 if parts < 2:
239 239 parts += 1
240 240 elif noentries:
241 241 parts += 1
242 242 break
243 243 noentries = False
244 244 yield chunkheader(len(chunk))
245 245 pos = 0
246 246 while pos < len(chunk):
247 247 next = pos + 2 ** 20
248 248 yield chunk[pos:next]
249 249 pos = next
250 250 yield closechunk()
251 251
252 252 def _unpackmanifests(self, repo, revmap, trp, prog):
253 253 self.callback = prog.increment
254 254 # no need to check for empty manifest group here:
255 255 # if the result of the merge of 1 and 2 is the same in 3 and 4,
256 256 # no new manifest will be created and the manifest group will
257 257 # be empty during the pull
258 258 self.manifestheader()
259 259 deltas = self.deltaiter()
260 260 repo.manifestlog.getstorage(b'').addgroup(deltas, revmap, trp)
261 261 prog.complete()
262 262 self.callback = None
263 263
264 264 def apply(
265 265 self,
266 266 repo,
267 267 tr,
268 268 srctype,
269 269 url,
270 270 targetphase=phases.draft,
271 271 expectedtotal=None,
272 272 ):
273 273 """Add the changegroup returned by source.read() to this repo.
274 274 srctype is a string like 'push', 'pull', or 'unbundle'. url is
275 275 the URL of the repo where this changegroup is coming from.
276 276
277 277 Return an integer summarizing the change to this repo:
278 278 - nothing changed or no source: 0
279 279 - more heads than before: 1+added heads (2..n)
280 280 - fewer heads than before: -1-removed heads (-2..-n)
281 281 - number of heads stays the same: 1
282 282 """
283 283 repo = repo.unfiltered()
284 284
285 285 def csmap(x):
286 286 repo.ui.debug(b"add changeset %s\n" % short(x))
287 287 return len(cl)
288 288
289 289 def revmap(x):
290 290 return cl.rev(x)
291 291
292 292 try:
293 293 # The transaction may already carry source information. In this
294 294 # case we use the top level data. We overwrite the argument
295 295 # because we need to use the top level value (if they exist)
296 296 # in this function.
297 297 srctype = tr.hookargs.setdefault(b'source', srctype)
298 298 tr.hookargs.setdefault(b'url', url)
299 299 repo.hook(
300 300 b'prechangegroup', throw=True, **pycompat.strkwargs(tr.hookargs)
301 301 )
302 302
303 303 # write changelog data to temp files so concurrent readers
304 304 # will not see an inconsistent view
305 305 cl = repo.changelog
306 306 cl.delayupdate(tr)
307 307 oldheads = set(cl.heads())
308 308
309 309 trp = weakref.proxy(tr)
310 310 # pull off the changeset group
311 311 repo.ui.status(_(b"adding changesets\n"))
312 312 clstart = len(cl)
313 313 progress = repo.ui.makeprogress(
314 314 _(b'changesets'), unit=_(b'chunks'), total=expectedtotal
315 315 )
316 316 self.callback = progress.increment
317 317
318 318 efilesset = set()
319 319 cgnodes = []
320 320
321 def ondupchangelog(cl, node):
322 if cl.rev(node) < clstart:
323 cgnodes.append(node)
324
321 325 def onchangelog(cl, node):
322 326 efilesset.update(cl.readfiles(node))
323 cgnodes.append(node)
324
325 def ondupchangelog(cl, node):
326 cgnodes.append(node)
327 327
328 328 self.changelogheader()
329 329 deltas = self.deltaiter()
330 330 if not cl.addgroup(
331 331 deltas,
332 332 csmap,
333 333 trp,
334 334 addrevisioncb=onchangelog,
335 335 duplicaterevisioncb=ondupchangelog,
336 336 ):
337 337 repo.ui.develwarn(
338 338 b'applied empty changelog from changegroup',
339 339 config=b'warn-empty-changegroup',
340 340 )
341 341 efiles = len(efilesset)
342 342 clend = len(cl)
343 343 changesets = clend - clstart
344 344 progress.complete()
345 345 del deltas
346 346 # TODO Python 2.7 removal
347 347 # del efilesset
348 348 efilesset = None
349 349 self.callback = None
350 350
351 351 # pull off the manifest group
352 352 repo.ui.status(_(b"adding manifests\n"))
353 353 # We know that we'll never have more manifests than we had
354 354 # changesets.
355 355 progress = repo.ui.makeprogress(
356 356 _(b'manifests'), unit=_(b'chunks'), total=changesets
357 357 )
358 358 self._unpackmanifests(repo, revmap, trp, progress)
359 359
360 360 needfiles = {}
361 361 if repo.ui.configbool(b'server', b'validate'):
362 362 cl = repo.changelog
363 363 ml = repo.manifestlog
364 364 # validate incoming csets have their manifests
365 365 for cset in pycompat.xrange(clstart, clend):
366 366 mfnode = cl.changelogrevision(cset).manifest
367 367 mfest = ml[mfnode].readdelta()
368 # store file cgnodes we must see
368 # store file nodes we must see
369 369 for f, n in pycompat.iteritems(mfest):
370 370 needfiles.setdefault(f, set()).add(n)
371 371
372 372 # process the files
373 373 repo.ui.status(_(b"adding file changes\n"))
374 374 newrevs, newfiles = _addchangegroupfiles(
375 375 repo, self, revmap, trp, efiles, needfiles
376 376 )
377 377
378 378 # making sure the value exists
379 379 tr.changes.setdefault(b'changegroup-count-changesets', 0)
380 380 tr.changes.setdefault(b'changegroup-count-revisions', 0)
381 381 tr.changes.setdefault(b'changegroup-count-files', 0)
382 382 tr.changes.setdefault(b'changegroup-count-heads', 0)
383 383
384 384 # some code use bundle operation for internal purpose. They usually
385 385 # set `ui.quiet` to do this outside of user sight. Size the report
386 386 # of such operation now happens at the end of the transaction, that
387 387 # ui.quiet has not direct effect on the output.
388 388 #
389 389 # To preserve this intend use an inelegant hack, we fail to report
390 390 # the change if `quiet` is set. We should probably move to
391 391 # something better, but this is a good first step to allow the "end
392 392 # of transaction report" to pass tests.
393 393 if not repo.ui.quiet:
394 394 tr.changes[b'changegroup-count-changesets'] += changesets
395 395 tr.changes[b'changegroup-count-revisions'] += newrevs
396 396 tr.changes[b'changegroup-count-files'] += newfiles
397 397
398 398 deltaheads = 0
399 399 if oldheads:
400 400 heads = cl.heads()
401 401 deltaheads += len(heads) - len(oldheads)
402 402 for h in heads:
403 403 if h not in oldheads and repo[h].closesbranch():
404 404 deltaheads -= 1
405 405
406 406 # see previous comment about checking ui.quiet
407 407 if not repo.ui.quiet:
408 408 tr.changes[b'changegroup-count-heads'] += deltaheads
409 409 repo.invalidatevolatilesets()
410 410
411 411 if changesets > 0:
412 412 if b'node' not in tr.hookargs:
413 413 tr.hookargs[b'node'] = hex(cl.node(clstart))
414 414 tr.hookargs[b'node_last'] = hex(cl.node(clend - 1))
415 415 hookargs = dict(tr.hookargs)
416 416 else:
417 417 hookargs = dict(tr.hookargs)
418 418 hookargs[b'node'] = hex(cl.node(clstart))
419 419 hookargs[b'node_last'] = hex(cl.node(clend - 1))
420 420 repo.hook(
421 421 b'pretxnchangegroup',
422 422 throw=True,
423 423 **pycompat.strkwargs(hookargs)
424 424 )
425 425
426 added = [cl.node(r) for r in pycompat.xrange(clstart, clend)]
426 added = pycompat.xrange(clstart, clend)
427 427 phaseall = None
428 428 if srctype in (b'push', b'serve'):
429 429 # Old servers can not push the boundary themselves.
430 430 # New servers won't push the boundary if changeset already
431 431 # exists locally as secret
432 432 #
433 433 # We should not use added here but the list of all change in
434 434 # the bundle
435 435 if repo.publishing():
436 436 targetphase = phaseall = phases.public
437 437 else:
438 438 # closer target phase computation
439 439
440 440 # Those changesets have been pushed from the
441 441 # outside, their phases are going to be pushed
442 442 # alongside. Therefor `targetphase` is
443 443 # ignored.
444 444 targetphase = phaseall = phases.draft
445 445 if added:
446 phases.registernew(repo, tr, targetphase, added)
446 phases.registernew(repo, tr, targetphase, [], revs=added)
447 447 if phaseall is not None:
448 phases.advanceboundary(repo, tr, phaseall, cgnodes)
448 phases.advanceboundary(repo, tr, phaseall, cgnodes, revs=added)
449 cgnodes = []
449 450
450 451 if changesets > 0:
451 452
452 453 def runhooks(unused_success):
453 454 # These hooks run when the lock releases, not when the
454 455 # transaction closes. So it's possible for the changelog
455 456 # to have changed since we last saw it.
456 457 if clstart >= len(repo):
457 458 return
458 459
459 460 repo.hook(b"changegroup", **pycompat.strkwargs(hookargs))
460 461
461 for n in added:
462 for rev in added:
462 463 args = hookargs.copy()
463 args[b'node'] = hex(n)
464 args[b'node'] = hex(cl.node(rev))
464 465 del args[b'node_last']
465 466 repo.hook(b"incoming", **pycompat.strkwargs(args))
466 467
467 468 newheads = [h for h in repo.heads() if h not in oldheads]
468 469 repo.ui.log(
469 470 b"incoming",
470 471 b"%d incoming changes - new heads: %s\n",
471 472 len(added),
472 473 b', '.join([hex(c[:6]) for c in newheads]),
473 474 )
474 475
475 476 tr.addpostclose(
476 477 b'changegroup-runhooks-%020i' % clstart,
477 478 lambda tr: repo._afterlock(runhooks),
478 479 )
479 480 finally:
480 481 repo.ui.flush()
481 482 # never return 0 here:
482 483 if deltaheads < 0:
483 484 ret = deltaheads - 1
484 485 else:
485 486 ret = deltaheads + 1
486 487 return ret
487 488
488 489 def deltaiter(self):
489 490 """
490 491 returns an iterator of the deltas in this changegroup
491 492
492 493 Useful for passing to the underlying storage system to be stored.
493 494 """
494 495 chain = None
495 496 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
496 497 # Chunkdata: (node, p1, p2, cs, deltabase, delta, flags)
497 498 yield chunkdata
498 499 chain = chunkdata[0]
499 500
500 501
501 502 class cg2unpacker(cg1unpacker):
502 503 """Unpacker for cg2 streams.
503 504
504 505 cg2 streams add support for generaldelta, so the delta header
505 506 format is slightly different. All other features about the data
506 507 remain the same.
507 508 """
508 509
509 510 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
510 511 deltaheadersize = deltaheader.size
511 512 version = b'02'
512 513
513 514 def _deltaheader(self, headertuple, prevnode):
514 515 node, p1, p2, deltabase, cs = headertuple
515 516 flags = 0
516 517 return node, p1, p2, deltabase, cs, flags
517 518
518 519
519 520 class cg3unpacker(cg2unpacker):
520 521 """Unpacker for cg3 streams.
521 522
522 523 cg3 streams add support for exchanging treemanifests and revlog
523 524 flags. It adds the revlog flags to the delta header and an empty chunk
524 525 separating manifests and files.
525 526 """
526 527
527 528 deltaheader = _CHANGEGROUPV3_DELTA_HEADER
528 529 deltaheadersize = deltaheader.size
529 530 version = b'03'
530 531 _grouplistcount = 2 # One list of manifests and one list of files
531 532
532 533 def _deltaheader(self, headertuple, prevnode):
533 534 node, p1, p2, deltabase, cs, flags = headertuple
534 535 return node, p1, p2, deltabase, cs, flags
535 536
536 537 def _unpackmanifests(self, repo, revmap, trp, prog):
537 538 super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog)
538 539 for chunkdata in iter(self.filelogheader, {}):
539 540 # If we get here, there are directory manifests in the changegroup
540 541 d = chunkdata[b"filename"]
541 542 repo.ui.debug(b"adding %s revisions\n" % d)
542 543 deltas = self.deltaiter()
543 544 if not repo.manifestlog.getstorage(d).addgroup(deltas, revmap, trp):
544 545 raise error.Abort(_(b"received dir revlog group is empty"))
545 546
546 547
547 548 class headerlessfixup(object):
548 549 def __init__(self, fh, h):
549 550 self._h = h
550 551 self._fh = fh
551 552
552 553 def read(self, n):
553 554 if self._h:
554 555 d, self._h = self._h[:n], self._h[n:]
555 556 if len(d) < n:
556 557 d += readexactly(self._fh, n - len(d))
557 558 return d
558 559 return readexactly(self._fh, n)
559 560
560 561
561 562 def _revisiondeltatochunks(delta, headerfn):
562 563 """Serialize a revisiondelta to changegroup chunks."""
563 564
564 565 # The captured revision delta may be encoded as a delta against
565 566 # a base revision or as a full revision. The changegroup format
566 567 # requires that everything on the wire be deltas. So for full
567 568 # revisions, we need to invent a header that says to rewrite
568 569 # data.
569 570
570 571 if delta.delta is not None:
571 572 prefix, data = b'', delta.delta
572 573 elif delta.basenode == nullid:
573 574 data = delta.revision
574 575 prefix = mdiff.trivialdiffheader(len(data))
575 576 else:
576 577 data = delta.revision
577 578 prefix = mdiff.replacediffheader(delta.baserevisionsize, len(data))
578 579
579 580 meta = headerfn(delta)
580 581
581 582 yield chunkheader(len(meta) + len(prefix) + len(data))
582 583 yield meta
583 584 if prefix:
584 585 yield prefix
585 586 yield data
586 587
587 588
588 589 def _sortnodesellipsis(store, nodes, cl, lookup):
589 590 """Sort nodes for changegroup generation."""
590 591 # Ellipses serving mode.
591 592 #
592 593 # In a perfect world, we'd generate better ellipsis-ified graphs
593 594 # for non-changelog revlogs. In practice, we haven't started doing
594 595 # that yet, so the resulting DAGs for the manifestlog and filelogs
595 596 # are actually full of bogus parentage on all the ellipsis
596 597 # nodes. This has the side effect that, while the contents are
597 598 # correct, the individual DAGs might be completely out of whack in
598 599 # a case like 882681bc3166 and its ancestors (back about 10
599 600 # revisions or so) in the main hg repo.
600 601 #
601 602 # The one invariant we *know* holds is that the new (potentially
602 603 # bogus) DAG shape will be valid if we order the nodes in the
603 604 # order that they're introduced in dramatis personae by the
604 605 # changelog, so what we do is we sort the non-changelog histories
605 606 # by the order in which they are used by the changelog.
606 607 key = lambda n: cl.rev(lookup(n))
607 608 return sorted(nodes, key=key)
608 609
609 610
610 611 def _resolvenarrowrevisioninfo(
611 612 cl,
612 613 store,
613 614 ischangelog,
614 615 rev,
615 616 linkrev,
616 617 linknode,
617 618 clrevtolocalrev,
618 619 fullclnodes,
619 620 precomputedellipsis,
620 621 ):
621 622 linkparents = precomputedellipsis[linkrev]
622 623
623 624 def local(clrev):
624 625 """Turn a changelog revnum into a local revnum.
625 626
626 627 The ellipsis dag is stored as revnums on the changelog,
627 628 but when we're producing ellipsis entries for
628 629 non-changelog revlogs, we need to turn those numbers into
629 630 something local. This does that for us, and during the
630 631 changelog sending phase will also expand the stored
631 632 mappings as needed.
632 633 """
633 634 if clrev == nullrev:
634 635 return nullrev
635 636
636 637 if ischangelog:
637 638 return clrev
638 639
639 640 # Walk the ellipsis-ized changelog breadth-first looking for a
640 641 # change that has been linked from the current revlog.
641 642 #
642 643 # For a flat manifest revlog only a single step should be necessary
643 644 # as all relevant changelog entries are relevant to the flat
644 645 # manifest.
645 646 #
646 647 # For a filelog or tree manifest dirlog however not every changelog
647 648 # entry will have been relevant, so we need to skip some changelog
648 649 # nodes even after ellipsis-izing.
649 650 walk = [clrev]
650 651 while walk:
651 652 p = walk[0]
652 653 walk = walk[1:]
653 654 if p in clrevtolocalrev:
654 655 return clrevtolocalrev[p]
655 656 elif p in fullclnodes:
656 657 walk.extend([pp for pp in cl.parentrevs(p) if pp != nullrev])
657 658 elif p in precomputedellipsis:
658 659 walk.extend(
659 660 [pp for pp in precomputedellipsis[p] if pp != nullrev]
660 661 )
661 662 else:
662 663 # In this case, we've got an ellipsis with parents
663 664 # outside the current bundle (likely an
664 665 # incremental pull). We "know" that we can use the
665 666 # value of this same revlog at whatever revision
666 667 # is pointed to by linknode. "Know" is in scare
667 668 # quotes because I haven't done enough examination
668 669 # of edge cases to convince myself this is really
669 670 # a fact - it works for all the (admittedly
670 671 # thorough) cases in our testsuite, but I would be
671 672 # somewhat unsurprised to find a case in the wild
672 673 # where this breaks down a bit. That said, I don't
673 674 # know if it would hurt anything.
674 675 for i in pycompat.xrange(rev, 0, -1):
675 676 if store.linkrev(i) == clrev:
676 677 return i
677 678 # We failed to resolve a parent for this node, so
678 679 # we crash the changegroup construction.
679 680 raise error.Abort(
680 681 b'unable to resolve parent while packing %r %r'
681 682 b' for changeset %r' % (store.indexfile, rev, clrev)
682 683 )
683 684
684 685 return nullrev
685 686
686 687 if not linkparents or (store.parentrevs(rev) == (nullrev, nullrev)):
687 688 p1, p2 = nullrev, nullrev
688 689 elif len(linkparents) == 1:
689 690 (p1,) = sorted(local(p) for p in linkparents)
690 691 p2 = nullrev
691 692 else:
692 693 p1, p2 = sorted(local(p) for p in linkparents)
693 694
694 695 p1node, p2node = store.node(p1), store.node(p2)
695 696
696 697 return p1node, p2node, linknode
697 698
698 699
699 700 def deltagroup(
700 701 repo,
701 702 store,
702 703 nodes,
703 704 ischangelog,
704 705 lookup,
705 706 forcedeltaparentprev,
706 707 topic=None,
707 708 ellipses=False,
708 709 clrevtolocalrev=None,
709 710 fullclnodes=None,
710 711 precomputedellipsis=None,
711 712 ):
712 713 """Calculate deltas for a set of revisions.
713 714
714 715 Is a generator of ``revisiondelta`` instances.
715 716
716 717 If topic is not None, progress detail will be generated using this
717 718 topic name (e.g. changesets, manifests, etc).
718 719 """
719 720 if not nodes:
720 721 return
721 722
722 723 cl = repo.changelog
723 724
724 725 if ischangelog:
725 726 # `hg log` shows changesets in storage order. To preserve order
726 727 # across clones, send out changesets in storage order.
727 728 nodesorder = b'storage'
728 729 elif ellipses:
729 730 nodes = _sortnodesellipsis(store, nodes, cl, lookup)
730 731 nodesorder = b'nodes'
731 732 else:
732 733 nodesorder = None
733 734
734 735 # Perform ellipses filtering and revision massaging. We do this before
735 736 # emitrevisions() because a) filtering out revisions creates less work
736 737 # for emitrevisions() b) dropping revisions would break emitrevisions()'s
737 738 # assumptions about delta choices and we would possibly send a delta
738 739 # referencing a missing base revision.
739 740 #
740 741 # Also, calling lookup() has side-effects with regards to populating
741 742 # data structures. If we don't call lookup() for each node or if we call
742 743 # lookup() after the first pass through each node, things can break -
743 744 # possibly intermittently depending on the python hash seed! For that
744 745 # reason, we store a mapping of all linknodes during the initial node
745 746 # pass rather than use lookup() on the output side.
746 747 if ellipses:
747 748 filtered = []
748 749 adjustedparents = {}
749 750 linknodes = {}
750 751
751 752 for node in nodes:
752 753 rev = store.rev(node)
753 754 linknode = lookup(node)
754 755 linkrev = cl.rev(linknode)
755 756 clrevtolocalrev[linkrev] = rev
756 757
757 758 # If linknode is in fullclnodes, it means the corresponding
758 759 # changeset was a full changeset and is being sent unaltered.
759 760 if linknode in fullclnodes:
760 761 linknodes[node] = linknode
761 762
762 763 # If the corresponding changeset wasn't in the set computed
763 764 # as relevant to us, it should be dropped outright.
764 765 elif linkrev not in precomputedellipsis:
765 766 continue
766 767
767 768 else:
768 769 # We could probably do this later and avoid the dict
769 770 # holding state. But it likely doesn't matter.
770 771 p1node, p2node, linknode = _resolvenarrowrevisioninfo(
771 772 cl,
772 773 store,
773 774 ischangelog,
774 775 rev,
775 776 linkrev,
776 777 linknode,
777 778 clrevtolocalrev,
778 779 fullclnodes,
779 780 precomputedellipsis,
780 781 )
781 782
782 783 adjustedparents[node] = (p1node, p2node)
783 784 linknodes[node] = linknode
784 785
785 786 filtered.append(node)
786 787
787 788 nodes = filtered
788 789
789 790 # We expect the first pass to be fast, so we only engage the progress
790 791 # meter for constructing the revision deltas.
791 792 progress = None
792 793 if topic is not None:
793 794 progress = repo.ui.makeprogress(
794 795 topic, unit=_(b'chunks'), total=len(nodes)
795 796 )
796 797
797 798 configtarget = repo.ui.config(b'devel', b'bundle.delta')
798 799 if configtarget not in (b'', b'p1', b'full'):
799 800 msg = _("""config "devel.bundle.delta" as unknown value: %s""")
800 801 repo.ui.warn(msg % configtarget)
801 802
802 803 deltamode = repository.CG_DELTAMODE_STD
803 804 if forcedeltaparentprev:
804 805 deltamode = repository.CG_DELTAMODE_PREV
805 806 elif configtarget == b'p1':
806 807 deltamode = repository.CG_DELTAMODE_P1
807 808 elif configtarget == b'full':
808 809 deltamode = repository.CG_DELTAMODE_FULL
809 810
810 811 revisions = store.emitrevisions(
811 812 nodes,
812 813 nodesorder=nodesorder,
813 814 revisiondata=True,
814 815 assumehaveparentrevisions=not ellipses,
815 816 deltamode=deltamode,
816 817 )
817 818
818 819 for i, revision in enumerate(revisions):
819 820 if progress:
820 821 progress.update(i + 1)
821 822
822 823 if ellipses:
823 824 linknode = linknodes[revision.node]
824 825
825 826 if revision.node in adjustedparents:
826 827 p1node, p2node = adjustedparents[revision.node]
827 828 revision.p1node = p1node
828 829 revision.p2node = p2node
829 830 revision.flags |= repository.REVISION_FLAG_ELLIPSIS
830 831
831 832 else:
832 833 linknode = lookup(revision.node)
833 834
834 835 revision.linknode = linknode
835 836 yield revision
836 837
837 838 if progress:
838 839 progress.complete()
839 840
840 841
841 842 class cgpacker(object):
842 843 def __init__(
843 844 self,
844 845 repo,
845 846 oldmatcher,
846 847 matcher,
847 848 version,
848 849 builddeltaheader,
849 850 manifestsend,
850 851 forcedeltaparentprev=False,
851 852 bundlecaps=None,
852 853 ellipses=False,
853 854 shallow=False,
854 855 ellipsisroots=None,
855 856 fullnodes=None,
856 857 ):
857 858 """Given a source repo, construct a bundler.
858 859
859 860 oldmatcher is a matcher that matches on files the client already has.
860 861 These will not be included in the changegroup.
861 862
862 863 matcher is a matcher that matches on files to include in the
863 864 changegroup. Used to facilitate sparse changegroups.
864 865
865 866 forcedeltaparentprev indicates whether delta parents must be against
866 867 the previous revision in a delta group. This should only be used for
867 868 compatibility with changegroup version 1.
868 869
869 870 builddeltaheader is a callable that constructs the header for a group
870 871 delta.
871 872
872 873 manifestsend is a chunk to send after manifests have been fully emitted.
873 874
874 875 ellipses indicates whether ellipsis serving mode is enabled.
875 876
876 877 bundlecaps is optional and can be used to specify the set of
877 878 capabilities which can be used to build the bundle. While bundlecaps is
878 879 unused in core Mercurial, extensions rely on this feature to communicate
879 880 capabilities to customize the changegroup packer.
880 881
881 882 shallow indicates whether shallow data might be sent. The packer may
882 883 need to pack file contents not introduced by the changes being packed.
883 884
884 885 fullnodes is the set of changelog nodes which should not be ellipsis
885 886 nodes. We store this rather than the set of nodes that should be
886 887 ellipsis because for very large histories we expect this to be
887 888 significantly smaller.
888 889 """
889 890 assert oldmatcher
890 891 assert matcher
891 892 self._oldmatcher = oldmatcher
892 893 self._matcher = matcher
893 894
894 895 self.version = version
895 896 self._forcedeltaparentprev = forcedeltaparentprev
896 897 self._builddeltaheader = builddeltaheader
897 898 self._manifestsend = manifestsend
898 899 self._ellipses = ellipses
899 900
900 901 # Set of capabilities we can use to build the bundle.
901 902 if bundlecaps is None:
902 903 bundlecaps = set()
903 904 self._bundlecaps = bundlecaps
904 905 self._isshallow = shallow
905 906 self._fullclnodes = fullnodes
906 907
907 908 # Maps ellipsis revs to their roots at the changelog level.
908 909 self._precomputedellipsis = ellipsisroots
909 910
910 911 self._repo = repo
911 912
912 913 if self._repo.ui.verbose and not self._repo.ui.debugflag:
913 914 self._verbosenote = self._repo.ui.note
914 915 else:
915 916 self._verbosenote = lambda s: None
916 917
917 918 def generate(
918 919 self, commonrevs, clnodes, fastpathlinkrev, source, changelog=True
919 920 ):
920 921 """Yield a sequence of changegroup byte chunks.
921 922 If changelog is False, changelog data won't be added to changegroup
922 923 """
923 924
924 925 repo = self._repo
925 926 cl = repo.changelog
926 927
927 928 self._verbosenote(_(b'uncompressed size of bundle content:\n'))
928 929 size = 0
929 930
930 931 clstate, deltas = self._generatechangelog(
931 932 cl, clnodes, generate=changelog
932 933 )
933 934 for delta in deltas:
934 935 for chunk in _revisiondeltatochunks(delta, self._builddeltaheader):
935 936 size += len(chunk)
936 937 yield chunk
937 938
938 939 close = closechunk()
939 940 size += len(close)
940 941 yield closechunk()
941 942
942 943 self._verbosenote(_(b'%8.i (changelog)\n') % size)
943 944
944 945 clrevorder = clstate[b'clrevorder']
945 946 manifests = clstate[b'manifests']
946 947 changedfiles = clstate[b'changedfiles']
947 948
948 949 # We need to make sure that the linkrev in the changegroup refers to
949 950 # the first changeset that introduced the manifest or file revision.
950 951 # The fastpath is usually safer than the slowpath, because the filelogs
951 952 # are walked in revlog order.
952 953 #
953 954 # When taking the slowpath when the manifest revlog uses generaldelta,
954 955 # the manifest may be walked in the "wrong" order. Without 'clrevorder',
955 956 # we would get an incorrect linkrev (see fix in cc0ff93d0c0c).
956 957 #
957 958 # When taking the fastpath, we are only vulnerable to reordering
958 959 # of the changelog itself. The changelog never uses generaldelta and is
959 960 # never reordered. To handle this case, we simply take the slowpath,
960 961 # which already has the 'clrevorder' logic. This was also fixed in
961 962 # cc0ff93d0c0c.
962 963
963 964 # Treemanifests don't work correctly with fastpathlinkrev
964 965 # either, because we don't discover which directory nodes to
965 966 # send along with files. This could probably be fixed.
966 967 fastpathlinkrev = fastpathlinkrev and not scmutil.istreemanifest(repo)
967 968
968 969 fnodes = {} # needed file nodes
969 970
970 971 size = 0
971 972 it = self.generatemanifests(
972 973 commonrevs,
973 974 clrevorder,
974 975 fastpathlinkrev,
975 976 manifests,
976 977 fnodes,
977 978 source,
978 979 clstate[b'clrevtomanifestrev'],
979 980 )
980 981
981 982 for tree, deltas in it:
982 983 if tree:
983 984 assert self.version == b'03'
984 985 chunk = _fileheader(tree)
985 986 size += len(chunk)
986 987 yield chunk
987 988
988 989 for delta in deltas:
989 990 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
990 991 for chunk in chunks:
991 992 size += len(chunk)
992 993 yield chunk
993 994
994 995 close = closechunk()
995 996 size += len(close)
996 997 yield close
997 998
998 999 self._verbosenote(_(b'%8.i (manifests)\n') % size)
999 1000 yield self._manifestsend
1000 1001
1001 1002 mfdicts = None
1002 1003 if self._ellipses and self._isshallow:
1003 1004 mfdicts = [
1004 1005 (self._repo.manifestlog[n].read(), lr)
1005 1006 for (n, lr) in pycompat.iteritems(manifests)
1006 1007 ]
1007 1008
1008 1009 manifests.clear()
1009 1010 clrevs = {cl.rev(x) for x in clnodes}
1010 1011
1011 1012 it = self.generatefiles(
1012 1013 changedfiles,
1013 1014 commonrevs,
1014 1015 source,
1015 1016 mfdicts,
1016 1017 fastpathlinkrev,
1017 1018 fnodes,
1018 1019 clrevs,
1019 1020 )
1020 1021
1021 1022 for path, deltas in it:
1022 1023 h = _fileheader(path)
1023 1024 size = len(h)
1024 1025 yield h
1025 1026
1026 1027 for delta in deltas:
1027 1028 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
1028 1029 for chunk in chunks:
1029 1030 size += len(chunk)
1030 1031 yield chunk
1031 1032
1032 1033 close = closechunk()
1033 1034 size += len(close)
1034 1035 yield close
1035 1036
1036 1037 self._verbosenote(_(b'%8.i %s\n') % (size, path))
1037 1038
1038 1039 yield closechunk()
1039 1040
1040 1041 if clnodes:
1041 1042 repo.hook(b'outgoing', node=hex(clnodes[0]), source=source)
1042 1043
1043 1044 def _generatechangelog(self, cl, nodes, generate=True):
1044 1045 """Generate data for changelog chunks.
1045 1046
1046 1047 Returns a 2-tuple of a dict containing state and an iterable of
1047 1048 byte chunks. The state will not be fully populated until the
1048 1049 chunk stream has been fully consumed.
1049 1050
1050 1051 if generate is False, the state will be fully populated and no chunk
1051 1052 stream will be yielded
1052 1053 """
1053 1054 clrevorder = {}
1054 1055 manifests = {}
1055 1056 mfl = self._repo.manifestlog
1056 1057 changedfiles = set()
1057 1058 clrevtomanifestrev = {}
1058 1059
1059 1060 state = {
1060 1061 b'clrevorder': clrevorder,
1061 1062 b'manifests': manifests,
1062 1063 b'changedfiles': changedfiles,
1063 1064 b'clrevtomanifestrev': clrevtomanifestrev,
1064 1065 }
1065 1066
1066 1067 if not (generate or self._ellipses):
1067 1068 # sort the nodes in storage order
1068 1069 nodes = sorted(nodes, key=cl.rev)
1069 1070 for node in nodes:
1070 1071 c = cl.changelogrevision(node)
1071 1072 clrevorder[node] = len(clrevorder)
1072 1073 # record the first changeset introducing this manifest version
1073 1074 manifests.setdefault(c.manifest, node)
1074 1075 # Record a complete list of potentially-changed files in
1075 1076 # this manifest.
1076 1077 changedfiles.update(c.files)
1077 1078
1078 1079 return state, ()
1079 1080
1080 1081 # Callback for the changelog, used to collect changed files and
1081 1082 # manifest nodes.
1082 1083 # Returns the linkrev node (identity in the changelog case).
1083 1084 def lookupcl(x):
1084 1085 c = cl.changelogrevision(x)
1085 1086 clrevorder[x] = len(clrevorder)
1086 1087
1087 1088 if self._ellipses:
1088 1089 # Only update manifests if x is going to be sent. Otherwise we
1089 1090 # end up with bogus linkrevs specified for manifests and
1090 1091 # we skip some manifest nodes that we should otherwise
1091 1092 # have sent.
1092 1093 if (
1093 1094 x in self._fullclnodes
1094 1095 or cl.rev(x) in self._precomputedellipsis
1095 1096 ):
1096 1097
1097 1098 manifestnode = c.manifest
1098 1099 # Record the first changeset introducing this manifest
1099 1100 # version.
1100 1101 manifests.setdefault(manifestnode, x)
1101 1102 # Set this narrow-specific dict so we have the lowest
1102 1103 # manifest revnum to look up for this cl revnum. (Part of
1103 1104 # mapping changelog ellipsis parents to manifest ellipsis
1104 1105 # parents)
1105 1106 clrevtomanifestrev.setdefault(
1106 1107 cl.rev(x), mfl.rev(manifestnode)
1107 1108 )
1108 1109 # We can't trust the changed files list in the changeset if the
1109 1110 # client requested a shallow clone.
1110 1111 if self._isshallow:
1111 1112 changedfiles.update(mfl[c.manifest].read().keys())
1112 1113 else:
1113 1114 changedfiles.update(c.files)
1114 1115 else:
1115 1116 # record the first changeset introducing this manifest version
1116 1117 manifests.setdefault(c.manifest, x)
1117 1118 # Record a complete list of potentially-changed files in
1118 1119 # this manifest.
1119 1120 changedfiles.update(c.files)
1120 1121
1121 1122 return x
1122 1123
1123 1124 gen = deltagroup(
1124 1125 self._repo,
1125 1126 cl,
1126 1127 nodes,
1127 1128 True,
1128 1129 lookupcl,
1129 1130 self._forcedeltaparentprev,
1130 1131 ellipses=self._ellipses,
1131 1132 topic=_(b'changesets'),
1132 1133 clrevtolocalrev={},
1133 1134 fullclnodes=self._fullclnodes,
1134 1135 precomputedellipsis=self._precomputedellipsis,
1135 1136 )
1136 1137
1137 1138 return state, gen
1138 1139
1139 1140 def generatemanifests(
1140 1141 self,
1141 1142 commonrevs,
1142 1143 clrevorder,
1143 1144 fastpathlinkrev,
1144 1145 manifests,
1145 1146 fnodes,
1146 1147 source,
1147 1148 clrevtolocalrev,
1148 1149 ):
1149 1150 """Returns an iterator of changegroup chunks containing manifests.
1150 1151
1151 1152 `source` is unused here, but is used by extensions like remotefilelog to
1152 1153 change what is sent based in pulls vs pushes, etc.
1153 1154 """
1154 1155 repo = self._repo
1155 1156 mfl = repo.manifestlog
1156 1157 tmfnodes = {b'': manifests}
1157 1158
1158 1159 # Callback for the manifest, used to collect linkrevs for filelog
1159 1160 # revisions.
1160 1161 # Returns the linkrev node (collected in lookupcl).
1161 1162 def makelookupmflinknode(tree, nodes):
1162 1163 if fastpathlinkrev:
1163 1164 assert not tree
1164 1165 return (
1165 1166 manifests.__getitem__
1166 1167 ) # pytype: disable=unsupported-operands
1167 1168
1168 1169 def lookupmflinknode(x):
1169 1170 """Callback for looking up the linknode for manifests.
1170 1171
1171 1172 Returns the linkrev node for the specified manifest.
1172 1173
1173 1174 SIDE EFFECT:
1174 1175
1175 1176 1) fclnodes gets populated with the list of relevant
1176 1177 file nodes if we're not using fastpathlinkrev
1177 1178 2) When treemanifests are in use, collects treemanifest nodes
1178 1179 to send
1179 1180
1180 1181 Note that this means manifests must be completely sent to
1181 1182 the client before you can trust the list of files and
1182 1183 treemanifests to send.
1183 1184 """
1184 1185 clnode = nodes[x]
1185 1186 mdata = mfl.get(tree, x).readfast(shallow=True)
1186 1187 for p, n, fl in mdata.iterentries():
1187 1188 if fl == b't': # subdirectory manifest
1188 1189 subtree = tree + p + b'/'
1189 1190 tmfclnodes = tmfnodes.setdefault(subtree, {})
1190 1191 tmfclnode = tmfclnodes.setdefault(n, clnode)
1191 1192 if clrevorder[clnode] < clrevorder[tmfclnode]:
1192 1193 tmfclnodes[n] = clnode
1193 1194 else:
1194 1195 f = tree + p
1195 1196 fclnodes = fnodes.setdefault(f, {})
1196 1197 fclnode = fclnodes.setdefault(n, clnode)
1197 1198 if clrevorder[clnode] < clrevorder[fclnode]:
1198 1199 fclnodes[n] = clnode
1199 1200 return clnode
1200 1201
1201 1202 return lookupmflinknode
1202 1203
1203 1204 while tmfnodes:
1204 1205 tree, nodes = tmfnodes.popitem()
1205 1206
1206 1207 should_visit = self._matcher.visitdir(tree[:-1])
1207 1208 if tree and not should_visit:
1208 1209 continue
1209 1210
1210 1211 store = mfl.getstorage(tree)
1211 1212
1212 1213 if not should_visit:
1213 1214 # No nodes to send because this directory is out of
1214 1215 # the client's view of the repository (probably
1215 1216 # because of narrow clones). Do this even for the root
1216 1217 # directory (tree=='')
1217 1218 prunednodes = []
1218 1219 else:
1219 1220 # Avoid sending any manifest nodes we can prove the
1220 1221 # client already has by checking linkrevs. See the
1221 1222 # related comment in generatefiles().
1222 1223 prunednodes = self._prunemanifests(store, nodes, commonrevs)
1223 1224
1224 1225 if tree and not prunednodes:
1225 1226 continue
1226 1227
1227 1228 lookupfn = makelookupmflinknode(tree, nodes)
1228 1229
1229 1230 deltas = deltagroup(
1230 1231 self._repo,
1231 1232 store,
1232 1233 prunednodes,
1233 1234 False,
1234 1235 lookupfn,
1235 1236 self._forcedeltaparentprev,
1236 1237 ellipses=self._ellipses,
1237 1238 topic=_(b'manifests'),
1238 1239 clrevtolocalrev=clrevtolocalrev,
1239 1240 fullclnodes=self._fullclnodes,
1240 1241 precomputedellipsis=self._precomputedellipsis,
1241 1242 )
1242 1243
1243 1244 if not self._oldmatcher.visitdir(store.tree[:-1]):
1244 1245 yield tree, deltas
1245 1246 else:
1246 1247 # 'deltas' is a generator and we need to consume it even if
1247 1248 # we are not going to send it because a side-effect is that
1248 1249 # it updates tmdnodes (via lookupfn)
1249 1250 for d in deltas:
1250 1251 pass
1251 1252 if not tree:
1252 1253 yield tree, []
1253 1254
1254 1255 def _prunemanifests(self, store, nodes, commonrevs):
1255 1256 if not self._ellipses:
1256 1257 # In non-ellipses case and large repositories, it is better to
1257 1258 # prevent calling of store.rev and store.linkrev on a lot of
1258 1259 # nodes as compared to sending some extra data
1259 1260 return nodes.copy()
1260 1261 # This is split out as a separate method to allow filtering
1261 1262 # commonrevs in extension code.
1262 1263 #
1263 1264 # TODO(augie): this shouldn't be required, instead we should
1264 1265 # make filtering of revisions to send delegated to the store
1265 1266 # layer.
1266 1267 frev, flr = store.rev, store.linkrev
1267 1268 return [n for n in nodes if flr(frev(n)) not in commonrevs]
1268 1269
1269 1270 # The 'source' parameter is useful for extensions
1270 1271 def generatefiles(
1271 1272 self,
1272 1273 changedfiles,
1273 1274 commonrevs,
1274 1275 source,
1275 1276 mfdicts,
1276 1277 fastpathlinkrev,
1277 1278 fnodes,
1278 1279 clrevs,
1279 1280 ):
1280 1281 changedfiles = [
1281 1282 f
1282 1283 for f in changedfiles
1283 1284 if self._matcher(f) and not self._oldmatcher(f)
1284 1285 ]
1285 1286
1286 1287 if not fastpathlinkrev:
1287 1288
1288 1289 def normallinknodes(unused, fname):
1289 1290 return fnodes.get(fname, {})
1290 1291
1291 1292 else:
1292 1293 cln = self._repo.changelog.node
1293 1294
1294 1295 def normallinknodes(store, fname):
1295 1296 flinkrev = store.linkrev
1296 1297 fnode = store.node
1297 1298 revs = ((r, flinkrev(r)) for r in store)
1298 1299 return {fnode(r): cln(lr) for r, lr in revs if lr in clrevs}
1299 1300
1300 1301 clrevtolocalrev = {}
1301 1302
1302 1303 if self._isshallow:
1303 1304 # In a shallow clone, the linknodes callback needs to also include
1304 1305 # those file nodes that are in the manifests we sent but weren't
1305 1306 # introduced by those manifests.
1306 1307 commonctxs = [self._repo[c] for c in commonrevs]
1307 1308 clrev = self._repo.changelog.rev
1308 1309
1309 1310 def linknodes(flog, fname):
1310 1311 for c in commonctxs:
1311 1312 try:
1312 1313 fnode = c.filenode(fname)
1313 1314 clrevtolocalrev[c.rev()] = flog.rev(fnode)
1314 1315 except error.ManifestLookupError:
1315 1316 pass
1316 1317 links = normallinknodes(flog, fname)
1317 1318 if len(links) != len(mfdicts):
1318 1319 for mf, lr in mfdicts:
1319 1320 fnode = mf.get(fname, None)
1320 1321 if fnode in links:
1321 1322 links[fnode] = min(links[fnode], lr, key=clrev)
1322 1323 elif fnode:
1323 1324 links[fnode] = lr
1324 1325 return links
1325 1326
1326 1327 else:
1327 1328 linknodes = normallinknodes
1328 1329
1329 1330 repo = self._repo
1330 1331 progress = repo.ui.makeprogress(
1331 1332 _(b'files'), unit=_(b'files'), total=len(changedfiles)
1332 1333 )
1333 1334 for i, fname in enumerate(sorted(changedfiles)):
1334 1335 filerevlog = repo.file(fname)
1335 1336 if not filerevlog:
1336 1337 raise error.Abort(
1337 1338 _(b"empty or missing file data for %s") % fname
1338 1339 )
1339 1340
1340 1341 clrevtolocalrev.clear()
1341 1342
1342 1343 linkrevnodes = linknodes(filerevlog, fname)
1343 1344 # Lookup for filenodes, we collected the linkrev nodes above in the
1344 1345 # fastpath case and with lookupmf in the slowpath case.
1345 1346 def lookupfilelog(x):
1346 1347 return linkrevnodes[x]
1347 1348
1348 1349 frev, flr = filerevlog.rev, filerevlog.linkrev
1349 1350 # Skip sending any filenode we know the client already
1350 1351 # has. This avoids over-sending files relatively
1351 1352 # inexpensively, so it's not a problem if we under-filter
1352 1353 # here.
1353 1354 filenodes = [
1354 1355 n for n in linkrevnodes if flr(frev(n)) not in commonrevs
1355 1356 ]
1356 1357
1357 1358 if not filenodes:
1358 1359 continue
1359 1360
1360 1361 progress.update(i + 1, item=fname)
1361 1362
1362 1363 deltas = deltagroup(
1363 1364 self._repo,
1364 1365 filerevlog,
1365 1366 filenodes,
1366 1367 False,
1367 1368 lookupfilelog,
1368 1369 self._forcedeltaparentprev,
1369 1370 ellipses=self._ellipses,
1370 1371 clrevtolocalrev=clrevtolocalrev,
1371 1372 fullclnodes=self._fullclnodes,
1372 1373 precomputedellipsis=self._precomputedellipsis,
1373 1374 )
1374 1375
1375 1376 yield fname, deltas
1376 1377
1377 1378 progress.complete()
1378 1379
1379 1380
1380 1381 def _makecg1packer(
1381 1382 repo,
1382 1383 oldmatcher,
1383 1384 matcher,
1384 1385 bundlecaps,
1385 1386 ellipses=False,
1386 1387 shallow=False,
1387 1388 ellipsisroots=None,
1388 1389 fullnodes=None,
1389 1390 ):
1390 1391 builddeltaheader = lambda d: _CHANGEGROUPV1_DELTA_HEADER.pack(
1391 1392 d.node, d.p1node, d.p2node, d.linknode
1392 1393 )
1393 1394
1394 1395 return cgpacker(
1395 1396 repo,
1396 1397 oldmatcher,
1397 1398 matcher,
1398 1399 b'01',
1399 1400 builddeltaheader=builddeltaheader,
1400 1401 manifestsend=b'',
1401 1402 forcedeltaparentprev=True,
1402 1403 bundlecaps=bundlecaps,
1403 1404 ellipses=ellipses,
1404 1405 shallow=shallow,
1405 1406 ellipsisroots=ellipsisroots,
1406 1407 fullnodes=fullnodes,
1407 1408 )
1408 1409
1409 1410
1410 1411 def _makecg2packer(
1411 1412 repo,
1412 1413 oldmatcher,
1413 1414 matcher,
1414 1415 bundlecaps,
1415 1416 ellipses=False,
1416 1417 shallow=False,
1417 1418 ellipsisroots=None,
1418 1419 fullnodes=None,
1419 1420 ):
1420 1421 builddeltaheader = lambda d: _CHANGEGROUPV2_DELTA_HEADER.pack(
1421 1422 d.node, d.p1node, d.p2node, d.basenode, d.linknode
1422 1423 )
1423 1424
1424 1425 return cgpacker(
1425 1426 repo,
1426 1427 oldmatcher,
1427 1428 matcher,
1428 1429 b'02',
1429 1430 builddeltaheader=builddeltaheader,
1430 1431 manifestsend=b'',
1431 1432 bundlecaps=bundlecaps,
1432 1433 ellipses=ellipses,
1433 1434 shallow=shallow,
1434 1435 ellipsisroots=ellipsisroots,
1435 1436 fullnodes=fullnodes,
1436 1437 )
1437 1438
1438 1439
1439 1440 def _makecg3packer(
1440 1441 repo,
1441 1442 oldmatcher,
1442 1443 matcher,
1443 1444 bundlecaps,
1444 1445 ellipses=False,
1445 1446 shallow=False,
1446 1447 ellipsisroots=None,
1447 1448 fullnodes=None,
1448 1449 ):
1449 1450 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1450 1451 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags
1451 1452 )
1452 1453
1453 1454 return cgpacker(
1454 1455 repo,
1455 1456 oldmatcher,
1456 1457 matcher,
1457 1458 b'03',
1458 1459 builddeltaheader=builddeltaheader,
1459 1460 manifestsend=closechunk(),
1460 1461 bundlecaps=bundlecaps,
1461 1462 ellipses=ellipses,
1462 1463 shallow=shallow,
1463 1464 ellipsisroots=ellipsisroots,
1464 1465 fullnodes=fullnodes,
1465 1466 )
1466 1467
1467 1468
1468 1469 _packermap = {
1469 1470 b'01': (_makecg1packer, cg1unpacker),
1470 1471 # cg2 adds support for exchanging generaldelta
1471 1472 b'02': (_makecg2packer, cg2unpacker),
1472 1473 # cg3 adds support for exchanging revlog flags and treemanifests
1473 1474 b'03': (_makecg3packer, cg3unpacker),
1474 1475 }
1475 1476
1476 1477
1477 1478 def allsupportedversions(repo):
1478 1479 versions = set(_packermap.keys())
1479 1480 needv03 = False
1480 1481 if (
1481 1482 repo.ui.configbool(b'experimental', b'changegroup3')
1482 1483 or repo.ui.configbool(b'experimental', b'treemanifest')
1483 1484 or scmutil.istreemanifest(repo)
1484 1485 ):
1485 1486 # we keep version 03 because we need to to exchange treemanifest data
1486 1487 #
1487 1488 # we also keep vresion 01 and 02, because it is possible for repo to
1488 1489 # contains both normal and tree manifest at the same time. so using
1489 1490 # older version to pull data is viable
1490 1491 #
1491 1492 # (or even to push subset of history)
1492 1493 needv03 = True
1493 1494 if b'exp-sidedata-flag' in repo.requirements:
1494 1495 needv03 = True
1495 1496 # don't attempt to use 01/02 until we do sidedata cleaning
1496 1497 versions.discard(b'01')
1497 1498 versions.discard(b'02')
1498 1499 if not needv03:
1499 1500 versions.discard(b'03')
1500 1501 return versions
1501 1502
1502 1503
1503 1504 # Changegroup versions that can be applied to the repo
1504 1505 def supportedincomingversions(repo):
1505 1506 return allsupportedversions(repo)
1506 1507
1507 1508
1508 1509 # Changegroup versions that can be created from the repo
1509 1510 def supportedoutgoingversions(repo):
1510 1511 versions = allsupportedversions(repo)
1511 1512 if scmutil.istreemanifest(repo):
1512 1513 # Versions 01 and 02 support only flat manifests and it's just too
1513 1514 # expensive to convert between the flat manifest and tree manifest on
1514 1515 # the fly. Since tree manifests are hashed differently, all of history
1515 1516 # would have to be converted. Instead, we simply don't even pretend to
1516 1517 # support versions 01 and 02.
1517 1518 versions.discard(b'01')
1518 1519 versions.discard(b'02')
1519 1520 if requirements.NARROW_REQUIREMENT in repo.requirements:
1520 1521 # Versions 01 and 02 don't support revlog flags, and we need to
1521 1522 # support that for stripping and unbundling to work.
1522 1523 versions.discard(b'01')
1523 1524 versions.discard(b'02')
1524 1525 if LFS_REQUIREMENT in repo.requirements:
1525 1526 # Versions 01 and 02 don't support revlog flags, and we need to
1526 1527 # mark LFS entries with REVIDX_EXTSTORED.
1527 1528 versions.discard(b'01')
1528 1529 versions.discard(b'02')
1529 1530
1530 1531 return versions
1531 1532
1532 1533
1533 1534 def localversion(repo):
1534 1535 # Finds the best version to use for bundles that are meant to be used
1535 1536 # locally, such as those from strip and shelve, and temporary bundles.
1536 1537 return max(supportedoutgoingversions(repo))
1537 1538
1538 1539
1539 1540 def safeversion(repo):
1540 1541 # Finds the smallest version that it's safe to assume clients of the repo
1541 1542 # will support. For example, all hg versions that support generaldelta also
1542 1543 # support changegroup 02.
1543 1544 versions = supportedoutgoingversions(repo)
1544 1545 if b'generaldelta' in repo.requirements:
1545 1546 versions.discard(b'01')
1546 1547 assert versions
1547 1548 return min(versions)
1548 1549
1549 1550
1550 1551 def getbundler(
1551 1552 version,
1552 1553 repo,
1553 1554 bundlecaps=None,
1554 1555 oldmatcher=None,
1555 1556 matcher=None,
1556 1557 ellipses=False,
1557 1558 shallow=False,
1558 1559 ellipsisroots=None,
1559 1560 fullnodes=None,
1560 1561 ):
1561 1562 assert version in supportedoutgoingversions(repo)
1562 1563
1563 1564 if matcher is None:
1564 1565 matcher = matchmod.always()
1565 1566 if oldmatcher is None:
1566 1567 oldmatcher = matchmod.never()
1567 1568
1568 1569 if version == b'01' and not matcher.always():
1569 1570 raise error.ProgrammingError(
1570 1571 b'version 01 changegroups do not support sparse file matchers'
1571 1572 )
1572 1573
1573 1574 if ellipses and version in (b'01', b'02'):
1574 1575 raise error.Abort(
1575 1576 _(
1576 1577 b'ellipsis nodes require at least cg3 on client and server, '
1577 1578 b'but negotiated version %s'
1578 1579 )
1579 1580 % version
1580 1581 )
1581 1582
1582 1583 # Requested files could include files not in the local store. So
1583 1584 # filter those out.
1584 1585 matcher = repo.narrowmatch(matcher)
1585 1586
1586 1587 fn = _packermap[version][0]
1587 1588 return fn(
1588 1589 repo,
1589 1590 oldmatcher,
1590 1591 matcher,
1591 1592 bundlecaps,
1592 1593 ellipses=ellipses,
1593 1594 shallow=shallow,
1594 1595 ellipsisroots=ellipsisroots,
1595 1596 fullnodes=fullnodes,
1596 1597 )
1597 1598
1598 1599
1599 1600 def getunbundler(version, fh, alg, extras=None):
1600 1601 return _packermap[version][1](fh, alg, extras=extras)
1601 1602
1602 1603
1603 1604 def _changegroupinfo(repo, nodes, source):
1604 1605 if repo.ui.verbose or source == b'bundle':
1605 1606 repo.ui.status(_(b"%d changesets found\n") % len(nodes))
1606 1607 if repo.ui.debugflag:
1607 1608 repo.ui.debug(b"list of changesets:\n")
1608 1609 for node in nodes:
1609 1610 repo.ui.debug(b"%s\n" % hex(node))
1610 1611
1611 1612
1612 1613 def makechangegroup(
1613 1614 repo, outgoing, version, source, fastpath=False, bundlecaps=None
1614 1615 ):
1615 1616 cgstream = makestream(
1616 1617 repo,
1617 1618 outgoing,
1618 1619 version,
1619 1620 source,
1620 1621 fastpath=fastpath,
1621 1622 bundlecaps=bundlecaps,
1622 1623 )
1623 1624 return getunbundler(
1624 1625 version,
1625 1626 util.chunkbuffer(cgstream),
1626 1627 None,
1627 1628 {b'clcount': len(outgoing.missing)},
1628 1629 )
1629 1630
1630 1631
1631 1632 def makestream(
1632 1633 repo,
1633 1634 outgoing,
1634 1635 version,
1635 1636 source,
1636 1637 fastpath=False,
1637 1638 bundlecaps=None,
1638 1639 matcher=None,
1639 1640 ):
1640 1641 bundler = getbundler(version, repo, bundlecaps=bundlecaps, matcher=matcher)
1641 1642
1642 1643 repo = repo.unfiltered()
1643 1644 commonrevs = outgoing.common
1644 1645 csets = outgoing.missing
1645 1646 heads = outgoing.ancestorsof
1646 1647 # We go through the fast path if we get told to, or if all (unfiltered
1647 1648 # heads have been requested (since we then know there all linkrevs will
1648 1649 # be pulled by the client).
1649 1650 heads.sort()
1650 1651 fastpathlinkrev = fastpath or (
1651 1652 repo.filtername is None and heads == sorted(repo.heads())
1652 1653 )
1653 1654
1654 1655 repo.hook(b'preoutgoing', throw=True, source=source)
1655 1656 _changegroupinfo(repo, csets, source)
1656 1657 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
1657 1658
1658 1659
1659 1660 def _addchangegroupfiles(repo, source, revmap, trp, expectedfiles, needfiles):
1660 1661 revisions = 0
1661 1662 files = 0
1662 1663 progress = repo.ui.makeprogress(
1663 1664 _(b'files'), unit=_(b'files'), total=expectedfiles
1664 1665 )
1665 1666 for chunkdata in iter(source.filelogheader, {}):
1666 1667 files += 1
1667 1668 f = chunkdata[b"filename"]
1668 1669 repo.ui.debug(b"adding %s revisions\n" % f)
1669 1670 progress.increment()
1670 1671 fl = repo.file(f)
1671 1672 o = len(fl)
1672 1673 try:
1673 1674 deltas = source.deltaiter()
1674 1675 if not fl.addgroup(deltas, revmap, trp):
1675 1676 raise error.Abort(_(b"received file revlog group is empty"))
1676 1677 except error.CensoredBaseError as e:
1677 1678 raise error.Abort(_(b"received delta base is censored: %s") % e)
1678 1679 revisions += len(fl) - o
1679 1680 if f in needfiles:
1680 1681 needs = needfiles[f]
1681 1682 for new in pycompat.xrange(o, len(fl)):
1682 1683 n = fl.node(new)
1683 1684 if n in needs:
1684 1685 needs.remove(n)
1685 1686 else:
1686 1687 raise error.Abort(_(b"received spurious file revlog entry"))
1687 1688 if not needs:
1688 1689 del needfiles[f]
1689 1690 progress.complete()
1690 1691
1691 1692 for f, needs in pycompat.iteritems(needfiles):
1692 1693 fl = repo.file(f)
1693 1694 for n in needs:
1694 1695 try:
1695 1696 fl.rev(n)
1696 1697 except error.LookupError:
1697 1698 raise error.Abort(
1698 1699 _(b'missing file data for %s:%s - run hg verify')
1699 1700 % (f, hex(n))
1700 1701 )
1701 1702
1702 1703 return revisions, files
@@ -1,932 +1,943 b''
1 1 """ Mercurial phases support code
2 2
3 3 ---
4 4
5 5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 6 Logilab SA <contact@logilab.fr>
7 7 Augie Fackler <durin42@gmail.com>
8 8
9 9 This software may be used and distributed according to the terms
10 10 of the GNU General Public License version 2 or any later version.
11 11
12 12 ---
13 13
14 14 This module implements most phase logic in mercurial.
15 15
16 16
17 17 Basic Concept
18 18 =============
19 19
20 20 A 'changeset phase' is an indicator that tells us how a changeset is
21 21 manipulated and communicated. The details of each phase is described
22 22 below, here we describe the properties they have in common.
23 23
24 24 Like bookmarks, phases are not stored in history and thus are not
25 25 permanent and leave no audit trail.
26 26
27 27 First, no changeset can be in two phases at once. Phases are ordered,
28 28 so they can be considered from lowest to highest. The default, lowest
29 29 phase is 'public' - this is the normal phase of existing changesets. A
30 30 child changeset can not be in a lower phase than its parents.
31 31
32 32 These phases share a hierarchy of traits:
33 33
34 34 immutable shared
35 35 public: X X
36 36 draft: X
37 37 secret:
38 38
39 39 Local commits are draft by default.
40 40
41 41 Phase Movement and Exchange
42 42 ===========================
43 43
44 44 Phase data is exchanged by pushkey on pull and push. Some servers have
45 45 a publish option set, we call such a server a "publishing server".
46 46 Pushing a draft changeset to a publishing server changes the phase to
47 47 public.
48 48
49 49 A small list of fact/rules define the exchange of phase:
50 50
51 51 * old client never changes server states
52 52 * pull never changes server states
53 53 * publish and old server changesets are seen as public by client
54 54 * any secret changeset seen in another repository is lowered to at
55 55 least draft
56 56
57 57 Here is the final table summing up the 49 possible use cases of phase
58 58 exchange:
59 59
60 60 server
61 61 old publish non-publish
62 62 N X N D P N D P
63 63 old client
64 64 pull
65 65 N - X/X - X/D X/P - X/D X/P
66 66 X - X/X - X/D X/P - X/D X/P
67 67 push
68 68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 69 new client
70 70 pull
71 71 N - P/X - P/D P/P - D/D P/P
72 72 D - P/X - P/D P/P - D/D P/P
73 73 P - P/X - P/D P/P - P/D P/P
74 74 push
75 75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 76 P P/X P/X P/P P/P P/P P/P P/P P/P
77 77
78 78 Legend:
79 79
80 80 A/B = final state on client / state on server
81 81
82 82 * N = new/not present,
83 83 * P = public,
84 84 * D = draft,
85 85 * X = not tracked (i.e., the old client or server has no internal
86 86 way of recording the phase.)
87 87
88 88 passive = only pushes
89 89
90 90
91 91 A cell here can be read like this:
92 92
93 93 "When a new client pushes a draft changeset (D) to a publishing
94 94 server where it's not present (N), it's marked public on both
95 95 sides (P/P)."
96 96
97 97 Note: old client behave as a publishing server with draft only content
98 98 - other people see it as public
99 99 - content is pushed as draft
100 100
101 101 """
102 102
103 103 from __future__ import absolute_import
104 104
105 105 import errno
106 106 import struct
107 107
108 108 from .i18n import _
109 109 from .node import (
110 110 bin,
111 111 hex,
112 112 nullid,
113 113 nullrev,
114 114 short,
115 115 wdirrev,
116 116 )
117 117 from .pycompat import (
118 118 getattr,
119 119 setattr,
120 120 )
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 requirements,
125 125 smartset,
126 126 txnutil,
127 127 util,
128 128 )
129 129
130 130 _fphasesentry = struct.Struct(b'>i20s')
131 131
132 132 # record phase index
133 133 public, draft, secret = range(3)
134 134 archived = 32 # non-continuous for compatibility
135 135 internal = 96 # non-continuous for compatibility
136 136 allphases = (public, draft, secret, archived, internal)
137 137 trackedphases = (draft, secret, archived, internal)
138 138 # record phase names
139 139 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
140 140 phasenames = dict(enumerate(cmdphasenames))
141 141 phasenames[archived] = b'archived'
142 142 phasenames[internal] = b'internal'
143 143 # map phase name to phase number
144 144 phasenumber = {name: phase for phase, name in phasenames.items()}
145 145 # like phasenumber, but also include maps for the numeric and binary
146 146 # phase number to the phase number
147 147 phasenumber2 = phasenumber.copy()
148 148 phasenumber2.update({phase: phase for phase in phasenames})
149 149 phasenumber2.update({b'%i' % phase: phase for phase in phasenames})
150 150 # record phase property
151 151 mutablephases = (draft, secret, archived, internal)
152 152 remotehiddenphases = (secret, archived, internal)
153 153 localhiddenphases = (internal, archived)
154 154
155 155
156 156 def supportinternal(repo):
157 157 """True if the internal phase can be used on a repository"""
158 158 return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements
159 159
160 160
161 161 def _readroots(repo, phasedefaults=None):
162 162 """Read phase roots from disk
163 163
164 164 phasedefaults is a list of fn(repo, roots) callable, which are
165 165 executed if the phase roots file does not exist. When phases are
166 166 being initialized on an existing repository, this could be used to
167 167 set selected changesets phase to something else than public.
168 168
169 169 Return (roots, dirty) where dirty is true if roots differ from
170 170 what is being stored.
171 171 """
172 172 repo = repo.unfiltered()
173 173 dirty = False
174 174 roots = {i: set() for i in allphases}
175 175 try:
176 176 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
177 177 try:
178 178 for line in f:
179 179 phase, nh = line.split()
180 180 roots[int(phase)].add(bin(nh))
181 181 finally:
182 182 f.close()
183 183 except IOError as inst:
184 184 if inst.errno != errno.ENOENT:
185 185 raise
186 186 if phasedefaults:
187 187 for f in phasedefaults:
188 188 roots = f(repo, roots)
189 189 dirty = True
190 190 return roots, dirty
191 191
192 192
193 193 def binaryencode(phasemapping):
194 194 """encode a 'phase -> nodes' mapping into a binary stream
195 195
196 196 The revision lists are encoded as (phase, root) pairs.
197 197 """
198 198 binarydata = []
199 199 for phase, nodes in pycompat.iteritems(phasemapping):
200 200 for head in nodes:
201 201 binarydata.append(_fphasesentry.pack(phase, head))
202 202 return b''.join(binarydata)
203 203
204 204
205 205 def binarydecode(stream):
206 206 """decode a binary stream into a 'phase -> nodes' mapping
207 207
208 208 The (phase, root) pairs are turned back into a dictionary with
209 209 the phase as index and the aggregated roots of that phase as value."""
210 210 headsbyphase = {i: [] for i in allphases}
211 211 entrysize = _fphasesentry.size
212 212 while True:
213 213 entry = stream.read(entrysize)
214 214 if len(entry) < entrysize:
215 215 if entry:
216 216 raise error.Abort(_(b'bad phase-heads stream'))
217 217 break
218 218 phase, node = _fphasesentry.unpack(entry)
219 219 headsbyphase[phase].append(node)
220 220 return headsbyphase
221 221
222 222
223 223 def _sortedrange_insert(data, idx, rev, t):
224 224 merge_before = False
225 225 if idx:
226 226 r1, t1 = data[idx - 1]
227 227 merge_before = r1[-1] + 1 == rev and t1 == t
228 228 merge_after = False
229 229 if idx < len(data):
230 230 r2, t2 = data[idx]
231 231 merge_after = r2[0] == rev + 1 and t2 == t
232 232
233 233 if merge_before and merge_after:
234 234 data[idx - 1] = (pycompat.xrange(r1[0], r2[-1] + 1), t)
235 235 data.pop(idx)
236 236 elif merge_before:
237 237 data[idx - 1] = (pycompat.xrange(r1[0], rev + 1), t)
238 238 elif merge_after:
239 239 data[idx] = (pycompat.xrange(rev, r2[-1] + 1), t)
240 240 else:
241 241 data.insert(idx, (pycompat.xrange(rev, rev + 1), t))
242 242
243 243
244 244 def _sortedrange_split(data, idx, rev, t):
245 245 r1, t1 = data[idx]
246 246 if t == t1:
247 247 return
248 248 t = (t1[0], t[1])
249 249 if len(r1) == 1:
250 250 data.pop(idx)
251 251 _sortedrange_insert(data, idx, rev, t)
252 252 elif r1[0] == rev:
253 253 data[idx] = (pycompat.xrange(rev + 1, r1[-1] + 1), t1)
254 254 _sortedrange_insert(data, idx, rev, t)
255 255 elif r1[-1] == rev:
256 256 data[idx] = (pycompat.xrange(r1[0], rev), t1)
257 257 _sortedrange_insert(data, idx + 1, rev, t)
258 258 else:
259 259 data[idx : idx + 1] = [
260 260 (pycompat.xrange(r1[0], rev), t1),
261 261 (pycompat.xrange(rev, rev + 1), t),
262 262 (pycompat.xrange(rev + 1, r1[-1] + 1), t1),
263 263 ]
264 264
265 265
266 266 def _trackphasechange(data, rev, old, new):
267 267 """add a phase move to the <data> list of ranges
268 268
269 269 If data is None, nothing happens.
270 270 """
271 271 if data is None:
272 272 return
273 273
274 274 # If data is empty, create a one-revision range and done
275 275 if not data:
276 276 data.insert(0, (pycompat.xrange(rev, rev + 1), (old, new)))
277 277 return
278 278
279 279 low = 0
280 280 high = len(data)
281 281 t = (old, new)
282 282 while low < high:
283 283 mid = (low + high) // 2
284 284 revs = data[mid][0]
285 285 revs_low = revs[0]
286 286 revs_high = revs[-1]
287 287
288 288 if rev >= revs_low and rev <= revs_high:
289 289 _sortedrange_split(data, mid, rev, t)
290 290 return
291 291
292 292 if revs_low == rev + 1:
293 293 if mid and data[mid - 1][0][-1] == rev:
294 294 _sortedrange_split(data, mid - 1, rev, t)
295 295 else:
296 296 _sortedrange_insert(data, mid, rev, t)
297 297 return
298 298
299 299 if revs_high == rev - 1:
300 300 if mid + 1 < len(data) and data[mid + 1][0][0] == rev:
301 301 _sortedrange_split(data, mid + 1, rev, t)
302 302 else:
303 303 _sortedrange_insert(data, mid + 1, rev, t)
304 304 return
305 305
306 306 if revs_low > rev:
307 307 high = mid
308 308 else:
309 309 low = mid + 1
310 310
311 311 if low == len(data):
312 312 data.append((pycompat.xrange(rev, rev + 1), t))
313 313 return
314 314
315 315 r1, t1 = data[low]
316 316 if r1[0] > rev:
317 317 data.insert(low, (pycompat.xrange(rev, rev + 1), t))
318 318 else:
319 319 data.insert(low + 1, (pycompat.xrange(rev, rev + 1), t))
320 320
321 321
322 322 class phasecache(object):
323 323 def __init__(self, repo, phasedefaults, _load=True):
324 324 if _load:
325 325 # Cheap trick to allow shallow-copy without copy module
326 326 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
327 327 self._loadedrevslen = 0
328 328 self._phasesets = None
329 329 self.filterunknown(repo)
330 330 self.opener = repo.svfs
331 331
332 332 def hasnonpublicphases(self, repo):
333 333 """detect if there are revisions with non-public phase"""
334 334 repo = repo.unfiltered()
335 335 cl = repo.changelog
336 336 if len(cl) >= self._loadedrevslen:
337 337 self.invalidate()
338 338 self.loadphaserevs(repo)
339 339 return any(
340 340 revs
341 341 for phase, revs in pycompat.iteritems(self.phaseroots)
342 342 if phase != public
343 343 )
344 344
345 345 def nonpublicphaseroots(self, repo):
346 346 """returns the roots of all non-public phases
347 347
348 348 The roots are not minimized, so if the secret revisions are
349 349 descendants of draft revisions, their roots will still be present.
350 350 """
351 351 repo = repo.unfiltered()
352 352 cl = repo.changelog
353 353 if len(cl) >= self._loadedrevslen:
354 354 self.invalidate()
355 355 self.loadphaserevs(repo)
356 356 return set().union(
357 357 *[
358 358 revs
359 359 for phase, revs in pycompat.iteritems(self.phaseroots)
360 360 if phase != public
361 361 ]
362 362 )
363 363
364 364 def getrevset(self, repo, phases, subset=None):
365 365 """return a smartset for the given phases"""
366 366 self.loadphaserevs(repo) # ensure phase's sets are loaded
367 367 phases = set(phases)
368 368 publicphase = public in phases
369 369
370 370 if publicphase:
371 371 # In this case, phases keeps all the *other* phases.
372 372 phases = set(allphases).difference(phases)
373 373 if not phases:
374 374 return smartset.fullreposet(repo)
375 375
376 376 # fast path: _phasesets contains the interesting sets,
377 377 # might only need a union and post-filtering.
378 378 revsneedscopy = False
379 379 if len(phases) == 1:
380 380 [p] = phases
381 381 revs = self._phasesets[p]
382 382 revsneedscopy = True # Don't modify _phasesets
383 383 else:
384 384 # revs has the revisions in all *other* phases.
385 385 revs = set.union(*[self._phasesets[p] for p in phases])
386 386
387 387 def _addwdir(wdirsubset, wdirrevs):
388 388 if wdirrev in wdirsubset and repo[None].phase() in phases:
389 389 if revsneedscopy:
390 390 wdirrevs = wdirrevs.copy()
391 391 # The working dir would never be in the # cache, but it was in
392 392 # the subset being filtered for its phase (or filtered out,
393 393 # depending on publicphase), so add it to the output to be
394 394 # included (or filtered out).
395 395 wdirrevs.add(wdirrev)
396 396 return wdirrevs
397 397
398 398 if not publicphase:
399 399 if repo.changelog.filteredrevs:
400 400 revs = revs - repo.changelog.filteredrevs
401 401
402 402 if subset is None:
403 403 return smartset.baseset(revs)
404 404 else:
405 405 revs = _addwdir(subset, revs)
406 406 return subset & smartset.baseset(revs)
407 407 else:
408 408 if subset is None:
409 409 subset = smartset.fullreposet(repo)
410 410
411 411 revs = _addwdir(subset, revs)
412 412
413 413 if not revs:
414 414 return subset
415 415 return subset.filter(lambda r: r not in revs)
416 416
417 417 def copy(self):
418 418 # Shallow copy meant to ensure isolation in
419 419 # advance/retractboundary(), nothing more.
420 420 ph = self.__class__(None, None, _load=False)
421 421 ph.phaseroots = self.phaseroots.copy()
422 422 ph.dirty = self.dirty
423 423 ph.opener = self.opener
424 424 ph._loadedrevslen = self._loadedrevslen
425 425 ph._phasesets = self._phasesets
426 426 return ph
427 427
428 428 def replace(self, phcache):
429 429 """replace all values in 'self' with content of phcache"""
430 430 for a in (
431 431 b'phaseroots',
432 432 b'dirty',
433 433 b'opener',
434 434 b'_loadedrevslen',
435 435 b'_phasesets',
436 436 ):
437 437 setattr(self, a, getattr(phcache, a))
438 438
439 439 def _getphaserevsnative(self, repo):
440 440 repo = repo.unfiltered()
441 441 return repo.changelog.computephases(self.phaseroots)
442 442
443 443 def _computephaserevspure(self, repo):
444 444 repo = repo.unfiltered()
445 445 cl = repo.changelog
446 446 self._phasesets = {phase: set() for phase in allphases}
447 447 lowerroots = set()
448 448 for phase in reversed(trackedphases):
449 449 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
450 450 if roots:
451 451 ps = set(cl.descendants(roots))
452 452 for root in roots:
453 453 ps.add(root)
454 454 ps.difference_update(lowerroots)
455 455 lowerroots.update(ps)
456 456 self._phasesets[phase] = ps
457 457 self._loadedrevslen = len(cl)
458 458
459 459 def loadphaserevs(self, repo):
460 460 """ensure phase information is loaded in the object"""
461 461 if self._phasesets is None:
462 462 try:
463 463 res = self._getphaserevsnative(repo)
464 464 self._loadedrevslen, self._phasesets = res
465 465 except AttributeError:
466 466 self._computephaserevspure(repo)
467 467
468 468 def invalidate(self):
469 469 self._loadedrevslen = 0
470 470 self._phasesets = None
471 471
472 472 def phase(self, repo, rev):
473 473 # We need a repo argument here to be able to build _phasesets
474 474 # if necessary. The repository instance is not stored in
475 475 # phasecache to avoid reference cycles. The changelog instance
476 476 # is not stored because it is a filecache() property and can
477 477 # be replaced without us being notified.
478 478 if rev == nullrev:
479 479 return public
480 480 if rev < nullrev:
481 481 raise ValueError(_(b'cannot lookup negative revision'))
482 482 if rev >= self._loadedrevslen:
483 483 self.invalidate()
484 484 self.loadphaserevs(repo)
485 485 for phase in trackedphases:
486 486 if rev in self._phasesets[phase]:
487 487 return phase
488 488 return public
489 489
490 490 def write(self):
491 491 if not self.dirty:
492 492 return
493 493 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
494 494 try:
495 495 self._write(f)
496 496 finally:
497 497 f.close()
498 498
499 499 def _write(self, fp):
500 500 for phase, roots in pycompat.iteritems(self.phaseroots):
501 501 for h in sorted(roots):
502 502 fp.write(b'%i %s\n' % (phase, hex(h)))
503 503 self.dirty = False
504 504
505 505 def _updateroots(self, phase, newroots, tr):
506 506 self.phaseroots[phase] = newroots
507 507 self.invalidate()
508 508 self.dirty = True
509 509
510 510 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
511 511 tr.hookargs[b'phases_moved'] = b'1'
512 512
513 def registernew(self, repo, tr, targetphase, nodes):
513 def registernew(self, repo, tr, targetphase, nodes, revs=None):
514 if revs is None:
515 revs = []
514 516 repo = repo.unfiltered()
515 self._retractboundary(repo, tr, targetphase, nodes)
517 self._retractboundary(repo, tr, targetphase, nodes, revs=revs)
516 518 if tr is not None and b'phases' in tr.changes:
517 519 phasetracking = tr.changes[b'phases']
518 520 torev = repo.changelog.rev
519 521 phase = self.phase
520 revs = [torev(node) for node in nodes]
522 revs = [torev(node) for node in nodes] + sorted(revs)
521 523 revs.sort()
522 524 for rev in revs:
523 525 revphase = phase(repo, rev)
524 526 _trackphasechange(phasetracking, rev, None, revphase)
525 527 repo.invalidatevolatilesets()
526 528
527 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
529 def advanceboundary(
530 self, repo, tr, targetphase, nodes, revs=None, dryrun=None
531 ):
528 532 """Set all 'nodes' to phase 'targetphase'
529 533
530 534 Nodes with a phase lower than 'targetphase' are not affected.
531 535
532 536 If dryrun is True, no actions will be performed
533 537
534 538 Returns a set of revs whose phase is changed or should be changed
535 539 """
536 540 # Be careful to preserve shallow-copied values: do not update
537 541 # phaseroots values, replace them.
542 if revs is None:
543 revs = []
538 544 if tr is None:
539 545 phasetracking = None
540 546 else:
541 547 phasetracking = tr.changes.get(b'phases')
542 548
543 549 repo = repo.unfiltered()
550 revs = [repo[n].rev() for n in nodes] + [r for r in revs]
544 551
545 552 changes = set() # set of revisions to be changed
546 553 delroots = [] # set of root deleted by this path
547 554 for phase in (phase for phase in allphases if phase > targetphase):
548 555 # filter nodes that are not in a compatible phase already
549 nodes = [
550 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
551 ]
552 if not nodes:
556 revs = [rev for rev in revs if self.phase(repo, rev) >= phase]
557 if not revs:
553 558 break # no roots to move anymore
554 559
555 560 olds = self.phaseroots[phase]
556 561
557 affected = repo.revs(b'%ln::%ln', olds, nodes)
562 affected = repo.revs(b'%ln::%ld', olds, revs)
558 563 changes.update(affected)
559 564 if dryrun:
560 565 continue
561 566 for r in affected:
562 567 _trackphasechange(
563 568 phasetracking, r, self.phase(repo, r), targetphase
564 569 )
565 570
566 571 roots = {
567 572 ctx.node()
568 573 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
569 574 }
570 575 if olds != roots:
571 576 self._updateroots(phase, roots, tr)
572 577 # some roots may need to be declared for lower phases
573 578 delroots.extend(olds - roots)
574 579 if not dryrun:
575 580 # declare deleted root in the target phase
576 581 if targetphase != 0:
577 582 self._retractboundary(repo, tr, targetphase, delroots)
578 583 repo.invalidatevolatilesets()
579 584 return changes
580 585
581 586 def retractboundary(self, repo, tr, targetphase, nodes):
582 587 oldroots = {
583 588 phase: revs
584 589 for phase, revs in pycompat.iteritems(self.phaseroots)
585 590 if phase <= targetphase
586 591 }
587 592 if tr is None:
588 593 phasetracking = None
589 594 else:
590 595 phasetracking = tr.changes.get(b'phases')
591 596 repo = repo.unfiltered()
592 597 if (
593 598 self._retractboundary(repo, tr, targetphase, nodes)
594 599 and phasetracking is not None
595 600 ):
596 601
597 602 # find the affected revisions
598 603 new = self.phaseroots[targetphase]
599 604 old = oldroots[targetphase]
600 605 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
601 606
602 607 # find the phase of the affected revision
603 608 for phase in pycompat.xrange(targetphase, -1, -1):
604 609 if phase:
605 610 roots = oldroots.get(phase, [])
606 611 revs = set(repo.revs(b'%ln::%ld', roots, affected))
607 612 affected -= revs
608 613 else: # public phase
609 614 revs = affected
610 615 for r in sorted(revs):
611 616 _trackphasechange(phasetracking, r, phase, targetphase)
612 617 repo.invalidatevolatilesets()
613 618
614 def _retractboundary(self, repo, tr, targetphase, nodes):
619 def _retractboundary(self, repo, tr, targetphase, nodes, revs=None):
615 620 # Be careful to preserve shallow-copied values: do not update
616 621 # phaseroots values, replace them.
622 if revs is None:
623 revs = []
617 624 if targetphase in (archived, internal) and not supportinternal(repo):
618 625 name = phasenames[targetphase]
619 626 msg = b'this repository does not support the %s phase' % name
620 627 raise error.ProgrammingError(msg)
621 628
622 629 repo = repo.unfiltered()
623 630 torev = repo.changelog.rev
624 631 tonode = repo.changelog.node
625 632 currentroots = {torev(node) for node in self.phaseroots[targetphase]}
626 633 finalroots = oldroots = set(currentroots)
627 newroots = [torev(node) for node in nodes]
634 newroots = [torev(node) for node in nodes] + [r for r in revs]
628 635 newroots = [
629 636 rev for rev in newroots if self.phase(repo, rev) < targetphase
630 637 ]
631 638
632 639 if newroots:
633 640 if nullrev in newroots:
634 641 raise error.Abort(_(b'cannot change null revision phase'))
635 642 currentroots.update(newroots)
636 643
637 644 # Only compute new roots for revs above the roots that are being
638 645 # retracted.
639 646 minnewroot = min(newroots)
640 647 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
641 648 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
642 649
643 650 finalroots = {rev for rev in currentroots if rev < minnewroot}
644 651 finalroots.update(updatedroots)
645 652 if finalroots != oldroots:
646 653 self._updateroots(
647 654 targetphase, {tonode(rev) for rev in finalroots}, tr
648 655 )
649 656 return True
650 657 return False
651 658
652 659 def filterunknown(self, repo):
653 660 """remove unknown nodes from the phase boundary
654 661
655 662 Nothing is lost as unknown nodes only hold data for their descendants.
656 663 """
657 664 filtered = False
658 665 has_node = repo.changelog.index.has_node # to filter unknown nodes
659 666 for phase, nodes in pycompat.iteritems(self.phaseroots):
660 667 missing = sorted(node for node in nodes if not has_node(node))
661 668 if missing:
662 669 for mnode in missing:
663 670 repo.ui.debug(
664 671 b'removing unknown node %s from %i-phase boundary\n'
665 672 % (short(mnode), phase)
666 673 )
667 674 nodes.symmetric_difference_update(missing)
668 675 filtered = True
669 676 if filtered:
670 677 self.dirty = True
671 678 # filterunknown is called by repo.destroyed, we may have no changes in
672 679 # root but _phasesets contents is certainly invalid (or at least we
673 680 # have not proper way to check that). related to issue 3858.
674 681 #
675 682 # The other caller is __init__ that have no _phasesets initialized
676 683 # anyway. If this change we should consider adding a dedicated
677 684 # "destroyed" function to phasecache or a proper cache key mechanism
678 685 # (see branchmap one)
679 686 self.invalidate()
680 687
681 688
682 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
689 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
683 690 """Add nodes to a phase changing other nodes phases if necessary.
684 691
685 692 This function move boundary *forward* this means that all nodes
686 693 are set in the target phase or kept in a *lower* phase.
687 694
688 695 Simplify boundary to contains phase roots only.
689 696
690 697 If dryrun is True, no actions will be performed
691 698
692 699 Returns a set of revs whose phase is changed or should be changed
693 700 """
701 if revs is None:
702 revs = []
694 703 phcache = repo._phasecache.copy()
695 704 changes = phcache.advanceboundary(
696 repo, tr, targetphase, nodes, dryrun=dryrun
705 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
697 706 )
698 707 if not dryrun:
699 708 repo._phasecache.replace(phcache)
700 709 return changes
701 710
702 711
703 712 def retractboundary(repo, tr, targetphase, nodes):
704 713 """Set nodes back to a phase changing other nodes phases if
705 714 necessary.
706 715
707 716 This function move boundary *backward* this means that all nodes
708 717 are set in the target phase or kept in a *higher* phase.
709 718
710 719 Simplify boundary to contains phase roots only."""
711 720 phcache = repo._phasecache.copy()
712 721 phcache.retractboundary(repo, tr, targetphase, nodes)
713 722 repo._phasecache.replace(phcache)
714 723
715 724
716 def registernew(repo, tr, targetphase, nodes):
725 def registernew(repo, tr, targetphase, nodes, revs=None):
717 726 """register a new revision and its phase
718 727
719 728 Code adding revisions to the repository should use this function to
720 729 set new changeset in their target phase (or higher).
721 730 """
731 if revs is None:
732 revs = []
722 733 phcache = repo._phasecache.copy()
723 phcache.registernew(repo, tr, targetphase, nodes)
734 phcache.registernew(repo, tr, targetphase, nodes, revs=revs)
724 735 repo._phasecache.replace(phcache)
725 736
726 737
727 738 def listphases(repo):
728 739 """List phases root for serialization over pushkey"""
729 740 # Use ordered dictionary so behavior is deterministic.
730 741 keys = util.sortdict()
731 742 value = b'%i' % draft
732 743 cl = repo.unfiltered().changelog
733 744 for root in repo._phasecache.phaseroots[draft]:
734 745 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
735 746 keys[hex(root)] = value
736 747
737 748 if repo.publishing():
738 749 # Add an extra data to let remote know we are a publishing
739 750 # repo. Publishing repo can't just pretend they are old repo.
740 751 # When pushing to a publishing repo, the client still need to
741 752 # push phase boundary
742 753 #
743 754 # Push do not only push changeset. It also push phase data.
744 755 # New phase data may apply to common changeset which won't be
745 756 # push (as they are common). Here is a very simple example:
746 757 #
747 758 # 1) repo A push changeset X as draft to repo B
748 759 # 2) repo B make changeset X public
749 760 # 3) repo B push to repo A. X is not pushed but the data that
750 761 # X as now public should
751 762 #
752 763 # The server can't handle it on it's own as it has no idea of
753 764 # client phase data.
754 765 keys[b'publishing'] = b'True'
755 766 return keys
756 767
757 768
758 769 def pushphase(repo, nhex, oldphasestr, newphasestr):
759 770 """List phases root for serialization over pushkey"""
760 771 repo = repo.unfiltered()
761 772 with repo.lock():
762 773 currentphase = repo[nhex].phase()
763 774 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
764 775 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
765 776 if currentphase == oldphase and newphase < oldphase:
766 777 with repo.transaction(b'pushkey-phase') as tr:
767 778 advanceboundary(repo, tr, newphase, [bin(nhex)])
768 779 return True
769 780 elif currentphase == newphase:
770 781 # raced, but got correct result
771 782 return True
772 783 else:
773 784 return False
774 785
775 786
776 787 def subsetphaseheads(repo, subset):
777 788 """Finds the phase heads for a subset of a history
778 789
779 790 Returns a list indexed by phase number where each item is a list of phase
780 791 head nodes.
781 792 """
782 793 cl = repo.changelog
783 794
784 795 headsbyphase = {i: [] for i in allphases}
785 796 # No need to keep track of secret phase; any heads in the subset that
786 797 # are not mentioned are implicitly secret.
787 798 for phase in allphases[:secret]:
788 799 revset = b"heads(%%ln & %s())" % phasenames[phase]
789 800 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
790 801 return headsbyphase
791 802
792 803
793 804 def updatephases(repo, trgetter, headsbyphase):
794 805 """Updates the repo with the given phase heads"""
795 806 # Now advance phase boundaries of all phases
796 807 #
797 808 # run the update (and fetch transaction) only if there are actually things
798 809 # to update. This avoid creating empty transaction during no-op operation.
799 810
800 811 for phase in allphases:
801 812 revset = b'%ln - _phase(%s)'
802 813 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
803 814 if heads:
804 815 advanceboundary(repo, trgetter(), phase, heads)
805 816
806 817
807 818 def analyzeremotephases(repo, subset, roots):
808 819 """Compute phases heads and root in a subset of node from root dict
809 820
810 821 * subset is heads of the subset
811 822 * roots is {<nodeid> => phase} mapping. key and value are string.
812 823
813 824 Accept unknown element input
814 825 """
815 826 repo = repo.unfiltered()
816 827 # build list from dictionary
817 828 draftroots = []
818 829 has_node = repo.changelog.index.has_node # to filter unknown nodes
819 830 for nhex, phase in pycompat.iteritems(roots):
820 831 if nhex == b'publishing': # ignore data related to publish option
821 832 continue
822 833 node = bin(nhex)
823 834 phase = int(phase)
824 835 if phase == public:
825 836 if node != nullid:
826 837 repo.ui.warn(
827 838 _(
828 839 b'ignoring inconsistent public root'
829 840 b' from remote: %s\n'
830 841 )
831 842 % nhex
832 843 )
833 844 elif phase == draft:
834 845 if has_node(node):
835 846 draftroots.append(node)
836 847 else:
837 848 repo.ui.warn(
838 849 _(b'ignoring unexpected root from remote: %i %s\n')
839 850 % (phase, nhex)
840 851 )
841 852 # compute heads
842 853 publicheads = newheads(repo, subset, draftroots)
843 854 return publicheads, draftroots
844 855
845 856
846 857 class remotephasessummary(object):
847 858 """summarize phase information on the remote side
848 859
849 860 :publishing: True is the remote is publishing
850 861 :publicheads: list of remote public phase heads (nodes)
851 862 :draftheads: list of remote draft phase heads (nodes)
852 863 :draftroots: list of remote draft phase root (nodes)
853 864 """
854 865
855 866 def __init__(self, repo, remotesubset, remoteroots):
856 867 unfi = repo.unfiltered()
857 868 self._allremoteroots = remoteroots
858 869
859 870 self.publishing = remoteroots.get(b'publishing', False)
860 871
861 872 ana = analyzeremotephases(repo, remotesubset, remoteroots)
862 873 self.publicheads, self.draftroots = ana
863 874 # Get the list of all "heads" revs draft on remote
864 875 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
865 876 self.draftheads = [c.node() for c in dheads]
866 877
867 878
868 879 def newheads(repo, heads, roots):
869 880 """compute new head of a subset minus another
870 881
871 882 * `heads`: define the first subset
872 883 * `roots`: define the second we subtract from the first"""
873 884 # prevent an import cycle
874 885 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
875 886 from . import dagop
876 887
877 888 repo = repo.unfiltered()
878 889 cl = repo.changelog
879 890 rev = cl.index.get_rev
880 891 if not roots:
881 892 return heads
882 893 if not heads or heads == [nullid]:
883 894 return []
884 895 # The logic operated on revisions, convert arguments early for convenience
885 896 new_heads = {rev(n) for n in heads if n != nullid}
886 897 roots = [rev(n) for n in roots]
887 898 # compute the area we need to remove
888 899 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
889 900 # heads in the area are no longer heads
890 901 new_heads.difference_update(affected_zone)
891 902 # revisions in the area have children outside of it,
892 903 # They might be new heads
893 904 candidates = repo.revs(
894 905 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
895 906 )
896 907 candidates -= affected_zone
897 908 if new_heads or candidates:
898 909 # remove candidate that are ancestors of other heads
899 910 new_heads.update(candidates)
900 911 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
901 912 pruned = dagop.reachableroots(repo, candidates, prunestart)
902 913 new_heads.difference_update(pruned)
903 914
904 915 return pycompat.maplist(cl.node, sorted(new_heads))
905 916
906 917
907 918 def newcommitphase(ui):
908 919 """helper to get the target phase of new commit
909 920
910 921 Handle all possible values for the phases.new-commit options.
911 922
912 923 """
913 924 v = ui.config(b'phases', b'new-commit')
914 925 try:
915 926 return phasenumber2[v]
916 927 except KeyError:
917 928 raise error.ConfigError(
918 929 _(b"phases.new-commit: not a valid phase name ('%s')") % v
919 930 )
920 931
921 932
922 933 def hassecret(repo):
923 934 """utility function that check if a repo have any secret changeset."""
924 935 return bool(repo._phasecache.phaseroots[secret])
925 936
926 937
927 938 def preparehookargs(node, old, new):
928 939 if old is None:
929 940 old = b''
930 941 else:
931 942 old = phasenames[old]
932 943 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
General Comments 0
You need to be logged in to leave comments. Login now