@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
at upstream/main 1482 lines 49 kB view raw
1<?php 2 3/** 4 * @task config Configuring the Hook Engine 5 * @task hook Hook Execution 6 * @task git Git Hooks 7 * @task hg Mercurial Hooks 8 * @task svn Subversion Hooks 9 * @task internal Internals 10 */ 11final class DiffusionCommitHookEngine extends Phobject { 12 13 const ENV_REPOSITORY = 'PHABRICATOR_REPOSITORY'; 14 const ENV_USER = 'PHABRICATOR_USER'; 15 const ENV_REQUEST = 'PHABRICATOR_REQUEST'; 16 const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS'; 17 const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL'; 18 19 const EMPTY_HASH = '0000000000000000000000000000000000000000'; 20 21 private $viewer; 22 private $repository; 23 private $stdin; 24 private $originalArgv; 25 private $subversionTransaction; 26 private $subversionRepository; 27 private $remoteAddress; 28 private $remoteProtocol; 29 private $requestIdentifier; 30 private $transactionKey; 31 private $mercurialHook; 32 private $mercurialCommits = array(); 33 private $gitCommits = array(); 34 private $startTime; 35 36 private $heraldViewerProjects; 37 private $rejectCode = PhabricatorRepositoryPushLog::REJECT_BROKEN; 38 private $rejectDetails; 39 private $emailPHIDs = array(); 40 private $changesets = array(); 41 private $changesetsSize = 0; 42 private $filesizeCache = array(); 43 44 45/* -( Config )------------------------------------------------------------- */ 46 47 48 public function setRemoteProtocol($remote_protocol) { 49 $this->remoteProtocol = $remote_protocol; 50 return $this; 51 } 52 53 public function getRemoteProtocol() { 54 return $this->remoteProtocol; 55 } 56 57 public function setRemoteAddress($remote_address) { 58 $this->remoteAddress = $remote_address; 59 return $this; 60 } 61 62 public function getRemoteAddress() { 63 return $this->remoteAddress; 64 } 65 66 public function setRequestIdentifier($request_identifier) { 67 $this->requestIdentifier = $request_identifier; 68 return $this; 69 } 70 71 public function getRequestIdentifier() { 72 return $this->requestIdentifier; 73 } 74 75 public function setStartTime($start_time) { 76 $this->startTime = $start_time; 77 return $this; 78 } 79 80 public function getStartTime() { 81 return $this->startTime; 82 } 83 84 public function setSubversionTransactionInfo($transaction, $repository) { 85 $this->subversionTransaction = $transaction; 86 $this->subversionRepository = $repository; 87 return $this; 88 } 89 90 public function setStdin($stdin) { 91 $this->stdin = $stdin; 92 return $this; 93 } 94 95 public function getStdin() { 96 return $this->stdin; 97 } 98 99 public function setOriginalArgv(array $original_argv) { 100 $this->originalArgv = $original_argv; 101 return $this; 102 } 103 104 public function getOriginalArgv() { 105 return $this->originalArgv; 106 } 107 108 public function setRepository(PhabricatorRepository $repository) { 109 $this->repository = $repository; 110 return $this; 111 } 112 113 public function getRepository() { 114 return $this->repository; 115 } 116 117 public function setViewer(PhabricatorUser $viewer) { 118 $this->viewer = $viewer; 119 return $this; 120 } 121 122 public function getViewer() { 123 return $this->viewer; 124 } 125 126 public function setMercurialHook($mercurial_hook) { 127 $this->mercurialHook = $mercurial_hook; 128 return $this; 129 } 130 131 public function getMercurialHook() { 132 return $this->mercurialHook; 133 } 134 135 136/* -( Hook Execution )----------------------------------------------------- */ 137 138 139 public function execute() { 140 $ref_updates = $this->findRefUpdates(); 141 $all_updates = $ref_updates; 142 143 $caught = null; 144 try { 145 146 try { 147 $this->rejectDangerousChanges($ref_updates); 148 } catch (DiffusionCommitHookRejectException $ex) { 149 // If we're rejecting dangerous changes, flag everything that we've 150 // seen as rejected so it's clear that none of it was accepted. 151 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS; 152 throw $ex; 153 } 154 155 $content_updates = $this->findContentUpdates($ref_updates); 156 $all_updates = array_merge($ref_updates, $content_updates); 157 158 // If this is an "initial import" (a sizable push to a previously empty 159 // repository) we'll allow enormous changes and disable Herald rules. 160 // These rulesets can consume a large amount of time and memory and are 161 // generally not relevant when importing repository history. 162 $is_initial_import = $this->isInitialImport($all_updates); 163 164 if (!$is_initial_import) { 165 $this->applyHeraldRefRules($ref_updates); 166 } 167 168 try { 169 if (!$is_initial_import) { 170 $this->rejectOversizedFiles($content_updates); 171 } 172 } catch (DiffusionCommitHookRejectException $ex) { 173 // If we're rejecting oversized files, flag everything. 174 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_OVERSIZED; 175 throw $ex; 176 } 177 178 try { 179 if (!$is_initial_import) { 180 $this->rejectCommitsAffectingTooManyPaths($content_updates); 181 } 182 } catch (DiffusionCommitHookRejectException $ex) { 183 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_TOUCHES; 184 throw $ex; 185 } 186 187 try { 188 if (!$is_initial_import) { 189 $this->rejectEnormousChanges($content_updates); 190 } 191 } catch (DiffusionCommitHookRejectException $ex) { 192 // If we're rejecting enormous changes, flag everything. 193 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ENORMOUS; 194 throw $ex; 195 } 196 197 if (!$is_initial_import) { 198 $this->applyHeraldContentRules($content_updates); 199 } 200 201 // Run custom scripts in `hook.d/` directories. 202 $this->applyCustomHooks($all_updates); 203 204 // If we make it this far, we're accepting these changes. Mark all the 205 // logs as accepted. 206 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ACCEPT; 207 } catch (Exception $ex) { 208 // We'll throw this again in a minute, but we want to save all the logs 209 // first. 210 $caught = $ex; 211 } 212 213 // Save all the logs no matter what the outcome was. 214 $event = $this->newPushEvent(); 215 216 $event->setRejectCode($this->rejectCode); 217 $event->setRejectDetails($this->rejectDetails); 218 219 $event->saveWithLogs($all_updates); 220 221 if ($caught) { 222 throw $caught; 223 } 224 225 // If this went through cleanly and was an import, set the importing flag 226 // on the repository. It will be cleared once we fully process everything. 227 228 if ($is_initial_import) { 229 $repository = $this->getRepository(); 230 $repository->markImporting(); 231 } 232 233 if ($this->emailPHIDs) { 234 // If Herald rules triggered email to users, queue a worker to send the 235 // mail. We do this out-of-process so that we block pushes as briefly 236 // as possible. 237 238 // (We do need to pull some commit info here because the commit objects 239 // may not exist yet when this worker runs, which could be immediately.) 240 241 PhabricatorWorker::scheduleTask( 242 'PhabricatorRepositoryPushMailWorker', 243 array( 244 'eventPHID' => $event->getPHID(), 245 'emailPHIDs' => array_values($this->emailPHIDs), 246 'info' => $this->loadCommitInfoForWorker($all_updates), 247 ), 248 array( 249 'priority' => PhabricatorWorker::PRIORITY_ALERTS, 250 )); 251 } 252 253 return 0; 254 } 255 256 private function findRefUpdates() { 257 $type = $this->getRepository()->getVersionControlSystem(); 258 switch ($type) { 259 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 260 return $this->findGitRefUpdates(); 261 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 262 return $this->findMercurialRefUpdates(); 263 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 264 return $this->findSubversionRefUpdates(); 265 default: 266 throw new Exception(pht('Unsupported repository type "%s"!', $type)); 267 } 268 } 269 270 /** 271 * @param array<PhabricatorRepositoryPushLog> $ref_updates 272 */ 273 private function rejectDangerousChanges(array $ref_updates) { 274 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class); 275 276 $repository = $this->getRepository(); 277 if ($repository->shouldAllowDangerousChanges()) { 278 return; 279 } 280 281 $flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; 282 283 foreach ($ref_updates as $ref_update) { 284 if (!$ref_update->hasChangeFlags($flag_dangerous)) { 285 // This is not a dangerous change. 286 continue; 287 } 288 289 // We either have a branch deletion or a non fast-forward branch update. 290 // Format a message and reject the push. 291 292 $message = pht( 293 "DANGEROUS CHANGE: %s\n". 294 "Dangerous change protection is enabled for this repository.\n". 295 "Edit the repository configuration before making dangerous changes.", 296 $ref_update->getDangerousChangeDescription()); 297 298 throw new DiffusionCommitHookRejectException($message); 299 } 300 } 301 302 /** 303 * @param array<PhabricatorRepositoryPushLog> $ref_updates 304 */ 305 private function findContentUpdates(array $ref_updates) { 306 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class); 307 308 $type = $this->getRepository()->getVersionControlSystem(); 309 switch ($type) { 310 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 311 return $this->findGitContentUpdates($ref_updates); 312 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 313 return $this->findMercurialContentUpdates($ref_updates); 314 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 315 return $this->findSubversionContentUpdates($ref_updates); 316 default: 317 throw new Exception(pht('Unsupported repository type "%s"!', $type)); 318 } 319 } 320 321 322/* -( Herald )------------------------------------------------------------- */ 323 324 private function applyHeraldRefRules(array $ref_updates) { 325 $this->applyHeraldRules( 326 $ref_updates, 327 new HeraldPreCommitRefAdapter()); 328 } 329 330 private function applyHeraldContentRules(array $content_updates) { 331 $this->applyHeraldRules( 332 $content_updates, 333 new HeraldPreCommitContentAdapter()); 334 } 335 336 private function applyHeraldRules( 337 array $updates, 338 HeraldAdapter $adapter_template) { 339 340 if (!$updates) { 341 return; 342 } 343 344 $viewer = $this->getViewer(); 345 346 $adapter_template 347 ->setHookEngine($this) 348 ->setActingAsPHID($viewer->getPHID()); 349 350 $engine = new HeraldEngine(); 351 $rules = null; 352 $blocking_effect = null; 353 $blocked_update = null; 354 $blocking_xscript = null; 355 foreach ($updates as $update) { 356 $adapter = id(clone $adapter_template) 357 ->setPushLog($update); 358 359 if ($rules === null) { 360 $rules = $engine->loadRulesForAdapter($adapter); 361 } 362 363 $effects = $engine->applyRules($rules, $adapter); 364 $engine->applyEffects($effects, $adapter, $rules); 365 $xscript = $engine->getTranscript(); 366 367 // Store any PHIDs we want to send email to for later. 368 foreach ($adapter->getEmailPHIDs() as $email_phid) { 369 $this->emailPHIDs[$email_phid] = $email_phid; 370 } 371 372 $block_action = DiffusionBlockHeraldAction::ACTIONCONST; 373 374 if ($blocking_effect === null) { 375 foreach ($effects as $effect) { 376 if ($effect->getAction() == $block_action) { 377 $blocking_effect = $effect; 378 $blocked_update = $update; 379 $blocking_xscript = $xscript; 380 break; 381 } 382 } 383 } 384 } 385 386 if ($blocking_effect) { 387 $rule = $blocking_effect->getRule(); 388 389 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD; 390 $this->rejectDetails = $rule->getPHID(); 391 392 $message = $blocking_effect->getTarget(); 393 if (!strlen($message)) { 394 $message = pht('(None.)'); 395 } 396 397 $blocked_ref_name = coalesce( 398 $blocked_update->getRefName(), 399 $blocked_update->getRefNewShort()); 400 $blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name; 401 402 throw new DiffusionCommitHookRejectException( 403 pht( 404 "This push was rejected by Herald push rule %s.\n". 405 " Change: %s\n". 406 " Rule: %s\n". 407 " Reason: %s\n". 408 "Transcript: %s", 409 $rule->getMonogram(), 410 $blocked_name, 411 $rule->getName(), 412 $message, 413 PhabricatorEnv::getProductionURI( 414 '/herald/transcript/'.$blocking_xscript->getID().'/'))); 415 } 416 } 417 418 public function loadViewerProjectPHIDsForHerald() { 419 // This just caches the viewer's projects so we don't need to load them 420 // over and over again when applying Herald rules. 421 if ($this->heraldViewerProjects === null) { 422 $this->heraldViewerProjects = id(new PhabricatorProjectQuery()) 423 ->setViewer($this->getViewer()) 424 ->withMemberPHIDs(array($this->getViewer()->getPHID())) 425 ->execute(); 426 } 427 428 return mpull($this->heraldViewerProjects, 'getPHID'); 429 } 430 431 432/* -( Git )---------------------------------------------------------------- */ 433 434 435 private function findGitRefUpdates() { 436 $ref_updates = array(); 437 438 // First, parse stdin, which lists all the ref changes. The input looks 439 // like this: 440 // 441 // <old hash> <new hash> <ref> 442 443 $stdin = $this->getStdin(); 444 $lines = phutil_split_lines($stdin, $retain_endings = false); 445 foreach ($lines as $line) { 446 $parts = explode(' ', $line, 3); 447 if (count($parts) != 3) { 448 throw new Exception(pht('Expected "old new ref", got "%s".', $line)); 449 } 450 451 $ref_old = $parts[0]; 452 $ref_new = $parts[1]; 453 $ref_raw = $parts[2]; 454 455 if (preg_match('(^refs/heads/)', $ref_raw)) { 456 $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; 457 $ref_raw = substr($ref_raw, strlen('refs/heads/')); 458 } else if (preg_match('(^refs/tags/)', $ref_raw)) { 459 $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG; 460 $ref_raw = substr($ref_raw, strlen('refs/tags/')); 461 } else { 462 $ref_type = PhabricatorRepositoryPushLog::REFTYPE_REF; 463 } 464 465 $ref_update = $this->newPushLog() 466 ->setRefType($ref_type) 467 ->setRefName($ref_raw) 468 ->setRefOld($ref_old) 469 ->setRefNew($ref_new); 470 471 $ref_updates[] = $ref_update; 472 } 473 474 $this->findGitMergeBases($ref_updates); 475 $this->findGitChangeFlags($ref_updates); 476 477 return $ref_updates; 478 } 479 480 /** 481 * @param array<PhabricatorRepositoryPushLog> $ref_updates 482 */ 483 private function findGitMergeBases(array $ref_updates) { 484 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class); 485 486 $futures = array(); 487 foreach ($ref_updates as $key => $ref_update) { 488 // If the old hash is "00000...", the ref is being created (either a new 489 // branch, or a new tag). If the new hash is "00000...", the ref is being 490 // deleted. If both are nonempty, the ref is being updated. For updates, 491 // we'll figure out the `merge-base` of the old and new objects here. This 492 // lets us reject non-FF changes cheaply; later, we'll figure out exactly 493 // which commits are new. 494 $ref_old = $ref_update->getRefOld(); 495 $ref_new = $ref_update->getRefNew(); 496 497 if (($ref_old === self::EMPTY_HASH) || 498 ($ref_new === self::EMPTY_HASH)) { 499 continue; 500 } 501 502 $futures[$key] = $this->getRepository()->getLocalCommandFuture( 503 'merge-base %s %s', 504 $ref_old, 505 $ref_new); 506 } 507 508 $futures = id(new FutureIterator($futures)) 509 ->limit(8); 510 foreach ($futures as $key => $future) { 511 512 // If 'old' and 'new' have no common ancestors (for example, a force push 513 // which completely rewrites a ref), `git merge-base` will exit with 514 // an error and no output. It would be nice to find a positive test 515 // for this instead, but I couldn't immediately come up with one. See 516 // T4224. Assume this means there are no ancestors. 517 518 list($err, $stdout) = $future->resolve(); 519 520 if ($err) { 521 $merge_base = null; 522 } else { 523 $merge_base = rtrim($stdout, "\n"); 524 } 525 526 $ref_update = $ref_updates[$key]; 527 $ref_update->setMergeBase($merge_base); 528 } 529 530 return $ref_updates; 531 } 532 533 /** 534 * @param array<PhabricatorRepositoryPushLog> $ref_updates 535 */ 536 private function findGitChangeFlags(array $ref_updates) { 537 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class); 538 539 foreach ($ref_updates as $key => $ref_update) { 540 $ref_old = $ref_update->getRefOld(); 541 $ref_new = $ref_update->getRefNew(); 542 $ref_type = $ref_update->getRefType(); 543 544 $ref_flags = 0; 545 $dangerous = null; 546 547 if (($ref_old === self::EMPTY_HASH) && ($ref_new === self::EMPTY_HASH)) { 548 // This happens if you try to delete a tag or branch which does not 549 // exist by pushing directly to the ref. Git will warn about it but 550 // allow it. Just call it a delete, without flagging it as dangerous. 551 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; 552 } else if ($ref_old === self::EMPTY_HASH) { 553 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; 554 } else if ($ref_new === self::EMPTY_HASH) { 555 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; 556 if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { 557 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; 558 $dangerous = pht( 559 "The change you're attempting to push deletes the branch '%s'.", 560 $ref_update->getRefName()); 561 } 562 } else { 563 $merge_base = $ref_update->getMergeBase(); 564 if ($merge_base == $ref_old) { 565 // This is a fast-forward update to an existing branch. 566 // These are safe. 567 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; 568 } else { 569 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; 570 571 // For now, we don't consider deleting or moving tags to be a 572 // "dangerous" update. It's way harder to get wrong and should be easy 573 // to recover from once we have better logging. Only add the dangerous 574 // flag if this ref is a branch. 575 576 if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { 577 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; 578 579 $dangerous = pht( 580 "The change you're attempting to push updates the branch '%s' ". 581 "from '%s' to '%s', but this is not a fast-forward. Pushes ". 582 "which rewrite published branch history are dangerous.", 583 $ref_update->getRefName(), 584 $ref_update->getRefOldShort(), 585 $ref_update->getRefNewShort()); 586 } 587 } 588 } 589 590 $ref_update->setChangeFlags($ref_flags); 591 if ($dangerous !== null) { 592 $ref_update->attachDangerousChangeDescription($dangerous); 593 } 594 } 595 596 return $ref_updates; 597 } 598 599 600 private function findGitContentUpdates(array $ref_updates) { 601 $flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; 602 603 $futures = array(); 604 foreach ($ref_updates as $key => $ref_update) { 605 if ($ref_update->hasChangeFlags($flag_delete)) { 606 // Deleting a branch or tag can never create any new commits. 607 continue; 608 } 609 610 // NOTE: This piece of magic finds all new commits, by walking backward 611 // from the new value to the value of *any* existing ref in the 612 // repository. Particularly, this will cover the cases of a new branch, a 613 // completely moved tag, etc. 614 $futures[$key] = $this->getRepository()->getLocalCommandFuture( 615 'log %s %s --not --all --', 616 '--format=%H', 617 gitsprintf('%s', $ref_update->getRefNew())); 618 } 619 620 $content_updates = array(); 621 $futures = id(new FutureIterator($futures)) 622 ->limit(8); 623 foreach ($futures as $key => $future) { 624 list($stdout) = $future->resolvex(); 625 626 if (!strlen(trim($stdout))) { 627 // This change doesn't have any new commits. One common case of this 628 // is creating a new tag which points at an existing commit. 629 continue; 630 } 631 632 $commits = phutil_split_lines($stdout, $retain_newlines = false); 633 634 // If we're looking at a branch, mark all of the new commits as on that 635 // branch. It's only possible for these commits to be on updated branches, 636 // since any other branch heads are necessarily behind them. 637 $branch_name = null; 638 $ref_update = $ref_updates[$key]; 639 $type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; 640 if ($ref_update->getRefType() == $type_branch) { 641 $branch_name = $ref_update->getRefName(); 642 } 643 644 foreach ($commits as $commit) { 645 if ($branch_name) { 646 $this->gitCommits[$commit][] = $branch_name; 647 } 648 $content_updates[$commit] = $this->newPushLog() 649 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) 650 ->setRefNew($commit) 651 ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); 652 } 653 } 654 655 return $content_updates; 656 } 657 658/* -( Custom )------------------------------------------------------------- */ 659 660 private function applyCustomHooks(array $updates) { 661 $args = $this->getOriginalArgv(); 662 $stdin = $this->getStdin(); 663 $console = PhutilConsole::getConsole(); 664 665 $env = array( 666 self::ENV_REPOSITORY => $this->getRepository()->getPHID(), 667 self::ENV_USER => $this->getViewer()->getUsername(), 668 self::ENV_REQUEST => $this->getRequestIdentifier(), 669 self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), 670 self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), 671 ); 672 673 $repository = $this->getRepository(); 674 675 $env += $repository->getPassthroughEnvironmentalVariables(); 676 677 $directories = $repository->getHookDirectories(); 678 foreach ($directories as $directory) { 679 $hooks = $this->getExecutablesInDirectory($directory); 680 sort($hooks); 681 foreach ($hooks as $hook) { 682 // NOTE: We're explicitly running the hooks in sequential order to 683 // make this more predictable. 684 $future = id(new ExecFuture('%s %Ls', $hook, $args)) 685 ->setEnv($env, $wipe_process_env = false) 686 ->write($stdin); 687 688 list($err, $stdout, $stderr) = $future->resolve(); 689 if (!$err) { 690 // This hook ran OK, but echo its output in case there was something 691 // informative. 692 $console->writeOut('%s', $stdout); 693 $console->writeErr('%s', $stderr); 694 continue; 695 } 696 697 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL; 698 $this->rejectDetails = basename($hook); 699 700 throw new DiffusionCommitHookRejectException( 701 pht( 702 "This push was rejected by custom hook script '%s':\n\n%s%s", 703 basename($hook), 704 $stdout, 705 $stderr)); 706 } 707 } 708 } 709 710 private function getExecutablesInDirectory($directory) { 711 $executables = array(); 712 713 if (!Filesystem::pathExists($directory)) { 714 return $executables; 715 } 716 717 foreach (Filesystem::listDirectory($directory) as $path) { 718 $full_path = $directory.DIRECTORY_SEPARATOR.$path; 719 if (!is_executable($full_path)) { 720 // Don't include non-executable files. 721 continue; 722 } 723 724 if (basename($full_path) == 'README') { 725 // Don't include README, even if it is marked as executable. It almost 726 // certainly got caught in the crossfire of a sweeping `chmod`, since 727 // users do this with some frequency. 728 continue; 729 } 730 731 $executables[] = $full_path; 732 } 733 734 return $executables; 735 } 736 737 738/* -( Mercurial )---------------------------------------------------------- */ 739 740 741 private function findMercurialRefUpdates() { 742 $hook = $this->getMercurialHook(); 743 switch ($hook) { 744 case 'pretxnchangegroup': 745 return $this->findMercurialChangegroupRefUpdates(); 746 case 'prepushkey': 747 return $this->findMercurialPushKeyRefUpdates(); 748 default: 749 throw new Exception(pht('Unrecognized hook "%s"!', $hook)); 750 } 751 } 752 753 private function findMercurialChangegroupRefUpdates() { 754 $hg_node = getenv('HG_NODE'); 755 if (!$hg_node) { 756 throw new Exception( 757 pht( 758 'Expected %s in environment!', 759 'HG_NODE')); 760 } 761 762 // NOTE: We need to make sure this is passed to subprocesses, or they won't 763 // be able to see new commits. Mercurial uses this as a marker to determine 764 // whether the pending changes are visible or not. 765 $_ENV['HG_PENDING'] = getenv('HG_PENDING'); 766 $repository = $this->getRepository(); 767 768 $futures = array(); 769 770 foreach (array('old', 'new') as $key) { 771 $futures[$key] = $repository->getLocalCommandFuture( 772 'heads --template %s', 773 '{node}\1{branch}\2'); 774 } 775 // Wipe HG_PENDING out of the old environment so we see the pre-commit 776 // state of the repository. 777 $futures['old']->updateEnv('HG_PENDING', null); 778 779 $futures['commits'] = $repository->getLocalCommandFuture( 780 'log --rev %s --template %s', 781 hgsprintf('%s:%s', $hg_node, 'tip'), 782 '{node}\1{branch}\2'); 783 784 // Resolve all of the futures now. We don't need the 'commits' future yet, 785 // but it simplifies the logic to just get it out of the way. 786 foreach (new FutureIterator($futures) as $future) { 787 $future->resolve(); 788 } 789 790 list($commit_raw) = $futures['commits']->resolvex(); 791 $commit_map = $this->parseMercurialCommits($commit_raw); 792 $this->mercurialCommits = $commit_map; 793 794 // NOTE: `hg heads` exits with an error code and no output if the repository 795 // has no heads. Most commonly this happens on a new repository. We know 796 // we can run `hg` successfully since the `hg log` above didn't error, so 797 // just ignore the error code. 798 799 list($err, $old_raw) = $futures['old']->resolve(); 800 $old_refs = $this->parseMercurialHeads($old_raw); 801 802 list($err, $new_raw) = $futures['new']->resolve(); 803 $new_refs = $this->parseMercurialHeads($new_raw); 804 805 $all_refs = array_keys($old_refs + $new_refs); 806 807 $ref_updates = array(); 808 foreach ($all_refs as $ref) { 809 $old_heads = idx($old_refs, $ref, array()); 810 $new_heads = idx($new_refs, $ref, array()); 811 812 sort($old_heads); 813 sort($new_heads); 814 815 if (!$old_heads && !$new_heads) { 816 // This should never be possible, as it makes no sense. Explode. 817 throw new Exception( 818 pht( 819 'Mercurial repository has no new or old heads for branch "%s" '. 820 'after push. This makes no sense; rejecting change.', 821 $ref)); 822 } 823 824 if ($old_heads === $new_heads) { 825 // No changes to this branch, so skip it. 826 continue; 827 } 828 829 $stray_heads = array(); 830 $head_map = array(); 831 832 if ($old_heads && !$new_heads) { 833 // This is a branch deletion with "--close-branch". 834 foreach ($old_heads as $old_head) { 835 $head_map[$old_head] = array(self::EMPTY_HASH); 836 } 837 } else if (count($old_heads) > 1) { 838 // HORRIBLE: In Mercurial, branches can have multiple heads. If the 839 // old branch had multiple heads, we need to figure out which new 840 // heads descend from which old heads, so we can tell whether you're 841 // actively creating new heads (dangerous) or just working in a 842 // repository that's already full of garbage (strongly discouraged but 843 // not as inherently dangerous). These cases should be very uncommon. 844 845 // NOTE: We're only looking for heads on the same branch. The old 846 // tip of the branch may be the branchpoint for other branches, but that 847 // is OK. 848 849 $dfutures = array(); 850 foreach ($old_heads as $old_head) { 851 $dfutures[$old_head] = $repository->getLocalCommandFuture( 852 'log --branch %s --rev %s --template %s', 853 $ref, 854 hgsprintf('(descendants(%s) and head())', $old_head), 855 '{node}\1'); 856 } 857 858 foreach (new FutureIterator($dfutures) as $future_head => $dfuture) { 859 list($stdout) = $dfuture->resolvex(); 860 $descendant_heads = array_filter(explode("\1", $stdout)); 861 if ($descendant_heads) { 862 // This old head has at least one descendant in the push. 863 $head_map[$future_head] = $descendant_heads; 864 } else { 865 // This old head has no descendants, so it is being deleted. 866 $head_map[$future_head] = array(self::EMPTY_HASH); 867 } 868 } 869 870 // Now, find all the new stray heads this push creates, if any. These 871 // are new heads which do not descend from the old heads. 872 $seen = array_fuse(array_mergev($head_map)); 873 foreach ($new_heads as $new_head) { 874 if ($new_head === self::EMPTY_HASH) { 875 // If a branch head is being deleted, don't insert it as an add. 876 continue; 877 } 878 if (empty($seen[$new_head])) { 879 $head_map[self::EMPTY_HASH][] = $new_head; 880 } 881 } 882 } else if ($old_heads) { 883 $head_map[head($old_heads)] = $new_heads; 884 } else { 885 $head_map[self::EMPTY_HASH] = $new_heads; 886 } 887 888 foreach ($head_map as $old_head => $child_heads) { 889 foreach ($child_heads as $new_head) { 890 if ($new_head === $old_head) { 891 continue; 892 } 893 894 $ref_flags = 0; 895 $dangerous = null; 896 if ($old_head == self::EMPTY_HASH) { 897 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; 898 } else { 899 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; 900 } 901 902 903 $deletes_existing_head = ($new_head == self::EMPTY_HASH); 904 $splits_existing_head = (count($child_heads) > 1); 905 $creates_duplicate_head = ($old_head == self::EMPTY_HASH) && 906 (count($head_map) > 1); 907 908 if ($splits_existing_head || $creates_duplicate_head) { 909 $readable_child_heads = array(); 910 foreach ($child_heads as $child_head) { 911 $readable_child_heads[] = substr($child_head, 0, 12); 912 } 913 914 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; 915 916 if ($splits_existing_head) { 917 // We're splitting an existing head into two or more heads. 918 // This is dangerous, and a super bad idea. Note that we're only 919 // raising this if you're actively splitting a branch head. If a 920 // head split in the past, we don't consider appends to it 921 // to be dangerous. 922 $dangerous = pht( 923 "The change you're attempting to push splits the head of ". 924 "branch '%s' into multiple heads: %s. This is inadvisable ". 925 "and dangerous.", 926 $ref, 927 implode(', ', $readable_child_heads)); 928 } else { 929 // We're adding a second (or more) head to a branch. The new 930 // head is not a descendant of any old head. 931 $dangerous = pht( 932 "The change you're attempting to push creates new, divergent ". 933 "heads for the branch '%s': %s. This is inadvisable and ". 934 "dangerous.", 935 $ref, 936 implode(', ', $readable_child_heads)); 937 } 938 } 939 940 if ($deletes_existing_head) { 941 // TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE 942 // if we are also creating at least one other head to replace 943 // this one. 944 945 // NOTE: In Git, this is a dangerous change, but it is not dangerous 946 // in Mercurial. Mercurial branches are version controlled, and 947 // Mercurial does not prompt you for any special flags when pushing 948 // a `--close-branch` commit by default. 949 950 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; 951 } 952 953 $ref_update = $this->newPushLog() 954 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH) 955 ->setRefName($ref) 956 ->setRefOld($old_head) 957 ->setRefNew($new_head) 958 ->setChangeFlags($ref_flags); 959 960 if ($dangerous !== null) { 961 $ref_update->attachDangerousChangeDescription($dangerous); 962 } 963 964 $ref_updates[] = $ref_update; 965 } 966 } 967 } 968 969 return $ref_updates; 970 } 971 972 private function findMercurialPushKeyRefUpdates() { 973 $key_namespace = getenv('HG_NAMESPACE'); 974 975 if ($key_namespace === 'phases') { 976 // Mercurial changes commit phases as part of normal push operations. We 977 // just ignore these, as they don't seem to represent anything 978 // interesting. 979 return array(); 980 } 981 982 $key_name = getenv('HG_KEY'); 983 984 $key_old = getenv('HG_OLD'); 985 if (!$key_old || !strlen($key_old)) { 986 $key_old = null; 987 } 988 989 $key_new = getenv('HG_NEW'); 990 if (!$key_new || !strlen($key_new)) { 991 $key_new = null; 992 } 993 994 if ($key_namespace !== 'bookmarks') { 995 throw new Exception( 996 pht( 997 "Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ". 998 "Rejecting push.", 999 $key_namespace, 1000 $key_name, 1001 coalesce($key_old, pht('null')), 1002 coalesce($key_new, pht('null')))); 1003 } 1004 1005 if ($key_old === $key_new) { 1006 // We get a callback when the bookmark doesn't change. Just ignore this, 1007 // as it's a no-op. 1008 return array(); 1009 } 1010 1011 $ref_flags = 0; 1012 $merge_base = null; 1013 if ($key_old === null) { 1014 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; 1015 } else if ($key_new === null) { 1016 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; 1017 } else { 1018 list($merge_base_raw) = $this->getRepository()->execxLocalCommand( 1019 'log --template %s --rev %s', 1020 '{node}', 1021 hgsprintf('ancestor(%s, %s)', $key_old, $key_new)); 1022 1023 if (strlen(trim($merge_base_raw))) { 1024 $merge_base = trim($merge_base_raw); 1025 } 1026 1027 if ($merge_base && ($merge_base === $key_old)) { 1028 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; 1029 } else { 1030 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; 1031 } 1032 } 1033 1034 $ref_update = $this->newPushLog() 1035 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK) 1036 ->setRefName($key_name) 1037 ->setRefOld(coalesce($key_old, self::EMPTY_HASH)) 1038 ->setRefNew(coalesce($key_new, self::EMPTY_HASH)) 1039 ->setChangeFlags($ref_flags); 1040 1041 return array($ref_update); 1042 } 1043 1044 private function findMercurialContentUpdates(array $ref_updates) { 1045 $content_updates = array(); 1046 1047 foreach ($this->mercurialCommits as $commit => $branches) { 1048 $content_updates[$commit] = $this->newPushLog() 1049 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) 1050 ->setRefNew($commit) 1051 ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); 1052 } 1053 1054 return $content_updates; 1055 } 1056 1057 private function parseMercurialCommits($raw) { 1058 $commits_lines = explode("\2", $raw); 1059 $commits_lines = array_filter($commits_lines); 1060 $commit_map = array(); 1061 foreach ($commits_lines as $commit_line) { 1062 list($node, $branch) = explode("\1", $commit_line); 1063 $commit_map[$node] = array($branch); 1064 } 1065 1066 return $commit_map; 1067 } 1068 1069 private function parseMercurialHeads($raw) { 1070 $heads_map = $this->parseMercurialCommits($raw); 1071 1072 $heads = array(); 1073 foreach ($heads_map as $commit => $branches) { 1074 foreach ($branches as $branch) { 1075 $heads[$branch][] = $commit; 1076 } 1077 } 1078 1079 return $heads; 1080 } 1081 1082 1083/* -( Subversion )--------------------------------------------------------- */ 1084 1085 1086 private function findSubversionRefUpdates() { 1087 // Subversion doesn't have any kind of mutable ref metadata. 1088 return array(); 1089 } 1090 1091 private function findSubversionContentUpdates(array $ref_updates) { 1092 list($youngest) = execx( 1093 'svnlook youngest %s', 1094 $this->subversionRepository); 1095 $ref_new = (int)$youngest + 1; 1096 1097 $ref_flags = 0; 1098 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; 1099 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; 1100 1101 $ref_content = $this->newPushLog() 1102 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) 1103 ->setRefNew($ref_new) 1104 ->setChangeFlags($ref_flags); 1105 1106 return array($ref_content); 1107 } 1108 1109 1110/* -( Internals )---------------------------------------------------------- */ 1111 1112 1113 private function newPushLog() { 1114 // NOTE: We generate PHIDs up front so the Herald transcripts can pick them 1115 // up. 1116 $phid = id(new PhabricatorRepositoryPushLog())->generatePHID(); 1117 1118 $device = AlmanacKeys::getLiveDevice(); 1119 if ($device) { 1120 $device_phid = $device->getPHID(); 1121 } else { 1122 $device_phid = null; 1123 } 1124 1125 return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) 1126 ->setPHID($phid) 1127 ->setDevicePHID($device_phid) 1128 ->setRepositoryPHID($this->getRepository()->getPHID()) 1129 ->attachRepository($this->getRepository()) 1130 ->setEpoch(PhabricatorTime::getNow()); 1131 } 1132 1133 private function newPushEvent() { 1134 $viewer = $this->getViewer(); 1135 1136 $hook_start = $this->getStartTime(); 1137 1138 $event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) 1139 ->setRepositoryPHID($this->getRepository()->getPHID()) 1140 ->setRemoteAddress($this->getRemoteAddress()) 1141 ->setRemoteProtocol($this->getRemoteProtocol()) 1142 ->setEpoch(PhabricatorTime::getNow()) 1143 ->setHookWait(phutil_microseconds_since($hook_start)); 1144 1145 $identifier = $this->getRequestIdentifier(); 1146 if ($identifier !== null && strlen($identifier)) { 1147 $event->setRequestIdentifier($identifier); 1148 } 1149 1150 return $event; 1151 } 1152 1153 private function rejectEnormousChanges(array $content_updates) { 1154 $repository = $this->getRepository(); 1155 if ($repository->shouldAllowEnormousChanges()) { 1156 return; 1157 } 1158 1159 // See T13142. Don't cache more than 64MB of changesets. For normal small 1160 // pushes, caching everything here can let us hit the cache from Herald if 1161 // we need to run content rules, which speeds things up a bit. For large 1162 // pushes, we may not be able to hold everything in memory. 1163 $cache_limit = 1024 * 1024 * 64; 1164 1165 foreach ($content_updates as $update) { 1166 $identifier = $update->getRefNew(); 1167 try { 1168 $info = $this->loadChangesetsForCommit($identifier); 1169 list($changesets, $size) = $info; 1170 1171 if ($this->changesetsSize + $size <= $cache_limit) { 1172 $this->changesets[$identifier] = $changesets; 1173 $this->changesetsSize += $size; 1174 } 1175 } catch (Exception $ex) { 1176 $this->changesets[$identifier] = $ex; 1177 1178 $message = pht( 1179 'ENORMOUS CHANGE'. 1180 "\n". 1181 'Enormous change protection is enabled for this repository, but '. 1182 'you are pushing an enormous change ("%s"). Edit the repository '. 1183 'configuration before making enormous changes.'. 1184 "\n\n". 1185 "Content Exception: %s", 1186 $identifier, 1187 $ex->getMessage()); 1188 1189 throw new DiffusionCommitHookRejectException($message); 1190 } 1191 } 1192 } 1193 1194 private function loadChangesetsForCommit($identifier) { 1195 $byte_limit = HeraldCommitAdapter::getEnormousByteLimit(); 1196 $time_limit = HeraldCommitAdapter::getEnormousTimeLimit(); 1197 1198 $vcs = $this->getRepository()->getVersionControlSystem(); 1199 switch ($vcs) { 1200 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 1201 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 1202 // For git and hg, we can use normal commands. 1203 $drequest = DiffusionRequest::newFromDictionary( 1204 array( 1205 'repository' => $this->getRepository(), 1206 'user' => $this->getViewer(), 1207 'commit' => $identifier, 1208 )); 1209 1210 $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) 1211 ->setTimeout($time_limit) 1212 ->setByteLimit($byte_limit) 1213 ->setLinesOfContext(0) 1214 ->executeInline(); 1215 break; 1216 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1217 // TODO: This diff has 3 lines of context, which produces slightly 1218 // incorrect "added file content" and "removed file content" results. 1219 // This may also choke on binaries, but "svnlook diff" does not support 1220 // the "--diff-cmd" flag. 1221 1222 // For subversion, we need to use `svnlook`. 1223 $future = new ExecFuture( 1224 'svnlook diff -t %s %s', 1225 $this->subversionTransaction, 1226 $this->subversionRepository); 1227 1228 $future->setTimeout($time_limit); 1229 $future->setStdoutSizeLimit($byte_limit); 1230 $future->setStderrSizeLimit($byte_limit); 1231 1232 list($raw_diff) = $future->resolvex(); 1233 break; 1234 default: 1235 throw new Exception(pht("Unknown VCS '%s!'", $vcs)); 1236 } 1237 1238 if (strlen($raw_diff) >= $byte_limit) { 1239 throw new Exception( 1240 pht( 1241 'The raw text of this change ("%s") is enormous (larger than %s '. 1242 'bytes).', 1243 $identifier, 1244 new PhutilNumber($byte_limit))); 1245 } 1246 1247 if (!strlen($raw_diff)) { 1248 // If the commit is actually empty, just return no changesets. 1249 return array(array(), 0); 1250 } 1251 1252 $parser = new ArcanistDiffParser(); 1253 $changes = $parser->parseDiff($raw_diff); 1254 $diff = DifferentialDiff::newEphemeralFromRawChanges( 1255 $changes); 1256 1257 $changesets = $diff->getChangesets(); 1258 $size = strlen($raw_diff); 1259 1260 return array($changesets, $size); 1261 } 1262 1263 public function getChangesetsForCommit($identifier) { 1264 if (isset($this->changesets[$identifier])) { 1265 $cached = $this->changesets[$identifier]; 1266 1267 if ($cached instanceof Exception) { 1268 throw $cached; 1269 } 1270 1271 return $cached; 1272 } 1273 1274 $info = $this->loadChangesetsForCommit($identifier); 1275 list($changesets, $size) = $info; 1276 return $changesets; 1277 } 1278 1279 private function rejectOversizedFiles(array $content_updates) { 1280 $repository = $this->getRepository(); 1281 1282 $limit = $repository->getFilesizeLimit(); 1283 if (!$limit) { 1284 return; 1285 } 1286 1287 foreach ($content_updates as $update) { 1288 $identifier = $update->getRefNew(); 1289 1290 $sizes = $this->getFileSizesForCommit($identifier); 1291 1292 foreach ($sizes as $path => $size) { 1293 if ($size <= $limit) { 1294 continue; 1295 } 1296 1297 $message = pht( 1298 'OVERSIZED FILE'. 1299 "\n". 1300 'This repository ("%s") is configured with a maximum individual '. 1301 'file size limit, but you are pushing a change ("%s") which causes '. 1302 'the size of a file ("%s") to exceed the limit. The commit makes '. 1303 'the file %s bytes long, but the limit for this repository is '. 1304 '%s bytes.', 1305 $repository->getDisplayName(), 1306 $identifier, 1307 $path, 1308 new PhutilNumber($size), 1309 new PhutilNumber($limit)); 1310 1311 throw new DiffusionCommitHookRejectException($message); 1312 } 1313 } 1314 } 1315 1316 private function rejectCommitsAffectingTooManyPaths(array $content_updates) { 1317 $repository = $this->getRepository(); 1318 1319 $limit = $repository->getTouchLimit(); 1320 if (!$limit) { 1321 return; 1322 } 1323 1324 foreach ($content_updates as $update) { 1325 $identifier = $update->getRefNew(); 1326 1327 $sizes = $this->getFileSizesForCommit($identifier); 1328 if (count($sizes) > $limit) { 1329 $message = pht( 1330 'COMMIT AFFECTS TOO MANY PATHS'. 1331 "\n". 1332 'This repository ("%s") is configured with a touched files limit '. 1333 'that caps the maximum number of paths any single commit may '. 1334 'affect. You are pushing a change ("%s") which exceeds this '. 1335 'limit: it affects %s paths, but the largest number of paths any '. 1336 'commit may affect is %s paths.', 1337 $repository->getDisplayName(), 1338 $identifier, 1339 phutil_count($sizes), 1340 new PhutilNumber($limit)); 1341 1342 throw new DiffusionCommitHookRejectException($message); 1343 } 1344 } 1345 } 1346 1347 public function getFileSizesForCommit($identifier) { 1348 if (!isset($this->filesizeCache[$identifier])) { 1349 $file_sizes = $this->loadFileSizesForCommit($identifier); 1350 $this->filesizeCache[$identifier] = $file_sizes; 1351 } 1352 1353 return $this->filesizeCache[$identifier]; 1354 } 1355 1356 private function loadFileSizesForCommit($identifier) { 1357 $repository = $this->getRepository(); 1358 1359 return id(new DiffusionLowLevelFilesizeQuery()) 1360 ->setRepository($repository) 1361 ->withIdentifier($identifier) 1362 ->execute(); 1363 } 1364 1365 public function loadCommitRefForCommit($identifier) { 1366 $repository = $this->getRepository(); 1367 $vcs = $repository->getVersionControlSystem(); 1368 switch ($vcs) { 1369 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 1370 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 1371 return id(new DiffusionLowLevelCommitQuery()) 1372 ->setRepository($repository) 1373 ->withIdentifier($identifier) 1374 ->execute(); 1375 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1376 // For subversion, we need to use `svnlook`. 1377 list($message) = execx( 1378 'svnlook log -t %s %s', 1379 $this->subversionTransaction, 1380 $this->subversionRepository); 1381 1382 return id(new DiffusionCommitRef()) 1383 ->setMessage($message); 1384 default: 1385 throw new Exception(pht("Unknown VCS '%s!'", $vcs)); 1386 } 1387 } 1388 1389 public function loadBranches($identifier) { 1390 $repository = $this->getRepository(); 1391 $vcs = $repository->getVersionControlSystem(); 1392 switch ($vcs) { 1393 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 1394 return idx($this->gitCommits, $identifier, array()); 1395 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 1396 // NOTE: This will be "the branch the commit was made to", not 1397 // "a list of all branch heads which descend from the commit". 1398 // This is consistent with Mercurial, but possibly confusing. 1399 return idx($this->mercurialCommits, $identifier, array()); 1400 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1401 // Subversion doesn't have branches. 1402 return array(); 1403 } 1404 } 1405 1406 private function loadCommitInfoForWorker(array $all_updates) { 1407 $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; 1408 1409 $map = array(); 1410 foreach ($all_updates as $update) { 1411 if ($update->getRefType() != $type_commit) { 1412 continue; 1413 } 1414 $map[$update->getRefNew()] = array(); 1415 } 1416 1417 foreach ($map as $identifier => $info) { 1418 $ref = $this->loadCommitRefForCommit($identifier); 1419 $map[$identifier] += array( 1420 'summary' => $ref->getSummary(), 1421 'branches' => $this->loadBranches($identifier), 1422 ); 1423 } 1424 1425 return $map; 1426 } 1427 1428 private function isInitialImport(array $all_updates) { 1429 $repository = $this->getRepository(); 1430 1431 $vcs = $repository->getVersionControlSystem(); 1432 switch ($vcs) { 1433 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1434 // There is no meaningful way to import history into Subversion by 1435 // pushing. 1436 return false; 1437 default: 1438 break; 1439 } 1440 1441 // Now, apply a heuristic to guess whether this is a normal commit or 1442 // an initial import. We guess something is an initial import if: 1443 // 1444 // - the repository is currently empty; and 1445 // - it pushes more than 7 commits at once. 1446 // 1447 // The number "7" is chosen arbitrarily as seeming reasonable. We could 1448 // also look at author data (do the commits come from multiple different 1449 // authors?) and commit date data (is the oldest commit more than 48 hours 1450 // old), but we don't have immediate access to those and this simple 1451 // heuristic might be good enough. 1452 1453 $commit_count = 0; 1454 $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; 1455 foreach ($all_updates as $update) { 1456 if ($update->getRefType() != $type_commit) { 1457 continue; 1458 } 1459 $commit_count++; 1460 } 1461 1462 if ($commit_count <= PhabricatorRepository::IMPORT_THRESHOLD) { 1463 // If this pushes a very small number of commits, assume it's an 1464 // initial commit or stack of a few initial commits. 1465 return false; 1466 } 1467 1468 $any_commits = id(new DiffusionCommitQuery()) 1469 ->setViewer($this->getViewer()) 1470 ->withRepository($repository) 1471 ->setLimit(1) 1472 ->execute(); 1473 1474 if ($any_commits) { 1475 // If the repository already has commits, this isn't an import. 1476 return false; 1477 } 1478 1479 return true; 1480 } 1481 1482}