@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Generate ref updates in Mercurial hooks

Summary: Ref T4195. Mercurial is not my favorite VCS.

Test Plan:
Hit the split branches case:

>>> orbital ~/repos/INIH $ hg push --force
pushing to ssh://dweller@local.aphront.com/diffusion/INIH
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 2 changesets with 2 changes to 1 files (+1 heads)
remote: +---------------------------------------------------------------+
remote: | * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * * |
remote: +---------------------------------------------------------------+
remote: \
remote: \ ^ /^
remote: \ / \ // \
remote: \ |\___/| / \// .\
remote: \ /V V \__ / // | \ \ *----*
remote: / / \/_/ // | \ \ \ |
remote: @___@` \/_ // | \ \ \/\ \
remote: 0/0/| \/_ // | \ \ \ \
remote: 0/0/0/0/| \/// | \ \ | |
remote: 0/0/0/0/0/_|_ / ( // | \ _\ | /
remote: 0/0/0/0/0/0/`/,_ _ _/ ) ; -. | _ _\.-~ / /
remote: ,-} _ *-.|.-~-. .~ ~
remote: \ \__/ `/\ / ~-. _ .-~ /
remote: \____(Oo) *. } { /
remote: ( (--) .----~-.\ \-` .~
remote: //__\\ \ DENIED! ///.----..< \ _ -~
remote: // \\ ///-._ _ _ _ _ _ _{^ - - - - ~
remote:
remote:
remote: DANGEROUS CHANGE: The change you're attempting to push splits the head of branch 'default' into multiple heads: 802c785c3dd9, e73400db39b0. This is inadvisable and dangerous.
remote: Dangerous change protection is enabled for this repository.
remote: Edit the repository configuration before making dangerous changes.
remote:
remote: transaction abort!
remote: rollback completed
remote: abort: pretxnchangegroup.phabricator hook exited with status 1

Hit the divergent heads case:

>>> orbital ~/repos/INIH $ hg push --force
pushing to ssh://dweller@local.aphront.com/diffusion/INIH
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 1 changesets with 1 changes to 1 files (+1 heads)
remote: +---------------------------------------------------------------+
remote: | * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * * |
remote: +---------------------------------------------------------------+
remote: \
remote: \ ^ /^
remote: \ / \ // \
remote: \ |\___/| / \// .\
remote: \ /V V \__ / // | \ \ *----*
remote: / / \/_/ // | \ \ \ |
remote: @___@` \/_ // | \ \ \/\ \
remote: 0/0/| \/_ // | \ \ \ \
remote: 0/0/0/0/| \/// | \ \ | |
remote: 0/0/0/0/0/_|_ / ( // | \ _\ | /
remote: 0/0/0/0/0/0/`/,_ _ _/ ) ; -. | _ _\.-~ / /
remote: ,-} _ *-.|.-~-. .~ ~
remote: \ \__/ `/\ / ~-. _ .-~ /
remote: \____(Oo) *. } { /
remote: ( (--) .----~-.\ \-` .~
remote: //__\\ \ DENIED! ///.----..< \ _ -~
remote: // \\ ///-._ _ _ _ _ _ _{^ - - - - ~
remote:
remote:
remote: DANGEROUS CHANGE: The change you're attempting to push creates new, divergent heads for the branch 'default': f56af4232aa9. This is inadvisable and dangerous.
remote: Dangerous change protection is enabled for this repository.
remote: Edit the repository configuration before making dangerous changes.
remote:
remote: transaction abort!
remote: rollback completed
remote: abort: pretxnchangegroup.phabricator hook exited with status 1

Did a bunch of good/bad pushes:

{F89300}

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4195

Differential Revision: https://secure.phabricator.com/D7763

+220 -10
+2
src/applications/diffusion/conduit/ConduitAPI_diffusion_commitbranchesquery_Method.php
··· 48 48 $repository = $drequest->getRepository(); 49 49 $commit = $request->getValue('commit'); 50 50 51 + // TODO: This should use `branches`. 52 + 51 53 list($contains) = $repository->execxLocalCommand( 52 54 'log --template %s --limit 1 --rev %s --', 53 55 '{branch}',
+214 -6
src/applications/diffusion/engine/DiffusionCommitHookEngine.php
··· 345 345 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; 346 346 347 347 $dangerous = pht( 348 - "DANGEROUS CHANGE: The change you're attempting to push updates ". 349 - "the branch '%s' from '%s' to '%s', but this is not a ". 350 - "fast-forward. Pushes which rewrite published branch history ". 351 - "are dangerous.", 348 + "The change you're attempting to push updates the branch '%s' ". 349 + "from '%s' to '%s', but this is not a fast-forward. Pushes ". 350 + "which rewrite published branch history are dangerous.", 352 351 $ref_update->getRefName(), 353 352 $ref_update->getRefOldShort(), 354 353 $ref_update->getRefNewShort()); ··· 414 413 415 414 416 415 private function findMercurialRefUpdates() { 417 - // TODO: Implement. 418 - return array(); 416 + $hg_node = getenv('HG_NODE'); 417 + if (!$hg_node) { 418 + throw new Exception(pht('Expected HG_NODE in environment!')); 419 + } 420 + 421 + // NOTE: We need to make sure this is passed to subprocesses, or they won't 422 + // be able to see new commits. Mercurial uses this as a marker to determine 423 + // whether the pending changes are visible or not. 424 + $_ENV['HG_PENDING'] = getenv('HG_PENDING'); 425 + $repository = $this->getRepository(); 426 + 427 + $futures = array(); 428 + 429 + foreach (array('old', 'new') as $key) { 430 + $futures[$key] = $repository->getLocalCommandFuture( 431 + 'heads --template %s', 432 + '{node}\1{branches}\2'); 433 + } 434 + // Wipe HG_PENDING out of the old environment so we see the pre-commit 435 + // state of the repository. 436 + $futures['old']->updateEnv('HG_PENDING', null); 437 + 438 + $futures['commits'] = $repository->getLocalCommandFuture( 439 + "log --rev %s --rev tip --template %s", 440 + hgsprintf('%s', $hg_node), 441 + '{node}\1{branches}\2'); 442 + 443 + // Resolve all of the futures now. We don't need the 'commits' future yet, 444 + // but it simplifies the logic to just get it out of the way. 445 + foreach (Futures($futures) as $future) { 446 + $future->resolvex(); 447 + } 448 + 449 + list($commit_raw) = $futures['commits']->resolvex(); 450 + $commit_map = $this->parseMercurialCommits($commit_raw); 451 + 452 + list($old_raw) = $futures['old']->resolvex(); 453 + $old_refs = $this->parseMercurialHeads($old_raw); 454 + 455 + list($new_raw) = $futures['new']->resolvex(); 456 + $new_refs = $this->parseMercurialHeads($new_raw); 457 + 458 + $all_refs = array_keys($old_refs + $new_refs); 459 + 460 + $ref_updates = array(); 461 + foreach ($all_refs as $ref) { 462 + $old_heads = idx($old_refs, $ref, array()); 463 + $new_heads = idx($new_refs, $ref, array()); 464 + 465 + sort($old_heads); 466 + sort($new_heads); 467 + 468 + if ($old_heads === $new_heads) { 469 + // No changes to this branch, so skip it. 470 + continue; 471 + } 472 + 473 + if (!$new_heads) { 474 + if ($old_heads) { 475 + // It looks like this push deletes a branch, but that isn't possible 476 + // in Mercurial, so something is going wrong here. Bail out. 477 + throw new Exception( 478 + pht( 479 + 'Mercurial repository has no new head for branch "%s" after '. 480 + 'push. This is unexpected; rejecting change.')); 481 + } else { 482 + // Obviously, this should never be possible either, as it makes 483 + // no sense. Explode. 484 + throw new Exception( 485 + pht( 486 + 'Mercurial repository has no new or old heads for branch "%s" '. 487 + 'after push. This makes no sense; rejecting change.')); 488 + } 489 + } 490 + 491 + $stray_heads = array(); 492 + if (count($old_heads) > 1) { 493 + // HORRIBLE: In Mercurial, branches can have multiple heads. If the 494 + // old branch had multiple heads, we need to figure out which new 495 + // heads descend from which old heads, so we can tell whether you're 496 + // actively creating new heads (dangerous) or just working in a 497 + // repository that's already full of garbage (strongly discouraged but 498 + // not as inherently dangerous). These cases should be very uncommon. 499 + 500 + $dfutures = array(); 501 + foreach ($old_heads as $old_head) { 502 + $dfutures[$old_head] = $repository->getLocalCommandFuture( 503 + 'log --rev %s --template %s', 504 + hgsprintf('(descendants(%s) and head())', $old_head), 505 + '{node}\1'); 506 + } 507 + 508 + $head_map = array(); 509 + foreach (Futures($dfutures) as $future_head => $dfuture) { 510 + list($stdout) = $dfuture->resolvex(); 511 + $head_map[$future_head] = array_filter(explode("\1", $stdout)); 512 + } 513 + 514 + // Now, find all the new stray heads this push creates, if any. These 515 + // are new heads which do not descend from the old heads. 516 + $seen = array_fuse(array_mergev($head_map)); 517 + foreach ($new_heads as $new_head) { 518 + if (empty($seen[$new_head])) { 519 + $head_map[self::EMPTY_HASH][] = $new_head; 520 + } 521 + } 522 + } else if ($old_heads) { 523 + $head_map[head($old_heads)] = $new_heads; 524 + } else { 525 + $head_map[self::EMPTY_HASH] = $new_heads; 526 + } 527 + 528 + foreach ($head_map as $old_head => $child_heads) { 529 + foreach ($child_heads as $new_head) { 530 + if ($new_head === $old_head) { 531 + continue; 532 + } 533 + 534 + $ref_flags = 0; 535 + $dangerous = null; 536 + if ($old_head == self::EMPTY_HASH) { 537 + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; 538 + } else { 539 + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; 540 + } 541 + 542 + $splits_existing_head = (count($child_heads) > 1); 543 + $creates_duplicate_head = ($old_head == self::EMPTY_HASH) && 544 + (count($head_map) > 1); 545 + 546 + if ($splits_existing_head || $creates_duplicate_head) { 547 + $readable_child_heads = array(); 548 + foreach ($child_heads as $child_head) { 549 + $readable_child_heads[] = substr($child_head, 0, 12); 550 + } 551 + 552 + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; 553 + 554 + if ($splits_existing_head) { 555 + // We're splitting an existing head into two or more heads. 556 + // This is dangerous, and a super bad idea. Note that we're only 557 + // raising this if you're actively splitting a branch head. If a 558 + // head split in the past, we don't consider appends to it 559 + // to be dangerous. 560 + $dangerous = pht( 561 + "The change you're attempting to push splits the head of ". 562 + "branch '%s' into multiple heads: %s. This is inadvisable ". 563 + "and dangerous.", 564 + $ref, 565 + implode(', ', $readable_child_heads)); 566 + } else { 567 + // We're adding a second (or more) head to a branch. The new 568 + // head is not a descendant of any old head. 569 + $dangerous = pht( 570 + "The change you're attempting to push creates new, divergent ". 571 + "heads for the branch '%s': %s. This is inadvisable and ". 572 + "dangerous.", 573 + $ref, 574 + implode(', ', $readable_child_heads)); 575 + } 576 + } 577 + 578 + $ref_update = $this->newPushLog() 579 + ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH) 580 + ->setRefName($ref) 581 + ->setRefOld($old_head) 582 + ->setRefNew($new_head) 583 + ->setChangeFlags($ref_flags); 584 + 585 + if ($dangerous !== null) { 586 + $ref_update->attachDangerousChangeDescription($dangerous); 587 + } 588 + 589 + $ref_updates[] = $ref_update; 590 + } 591 + } 592 + } 593 + 594 + return $ref_updates; 595 + } 596 + 597 + private function parseMercurialCommits($raw) { 598 + $commits_lines = explode("\2", $raw); 599 + $commits_lines = array_filter($commits_lines); 600 + $commit_map = array(); 601 + foreach ($commits_lines as $commit_line) { 602 + list($node, $branches_raw) = explode("\1", $commit_line); 603 + 604 + if (!strlen($branches_raw)) { 605 + $branches = array('default'); 606 + } else { 607 + $branches = explode(' ', $branches_raw); 608 + } 609 + 610 + $commit_map[$node] = $branches; 611 + } 612 + 613 + return $commit_map; 614 + } 615 + 616 + private function parseMercurialHeads($raw) { 617 + $heads_map = $this->parseMercurialCommits($raw); 618 + 619 + $heads = array(); 620 + foreach ($heads_map as $commit => $branches) { 621 + foreach ($branches as $branch) { 622 + $heads[$branch][] = $commit; 623 + } 624 + } 625 + 626 + return $heads; 419 627 } 420 628 421 629 private function findMercurialContentUpdates(array $ref_updates) {
+4 -4
src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
··· 735 735 'URI "%s".', 736 736 $expect_remote)); 737 737 $repository->execxLocalCommand( 738 - 'remote add origin %s', 739 - $expect_remote); 738 + 'remote add origin %P', 739 + $repository->getRemoteURIEnvelope()); 740 740 741 741 // NOTE: This doesn't fetch the origin (it just creates it), so we won't 742 742 // know about origin branches until the next "pull" happens. That's fine ··· 751 751 $remote_uri, 752 752 $expect_remote)); 753 753 $repository->execxLocalCommand( 754 - 'remote set-url origin %s', 755 - $expect_remote); 754 + 'remote set-url origin %P', 755 + $repository->getRemoteURIEnvelope()); 756 756 } else { 757 757 // Bad remote and we aren't comfortable repairing it. 758 758 $message = pht(