@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.

at recaptime-dev/main 2849 lines 80 kB view raw
1<?php 2 3/** 4 * @task uri Repository URI Management 5 * @task publishing Publishing 6 * @task sync Cluster Synchronization 7 */ 8final class PhabricatorRepository extends PhabricatorRepositoryDAO 9 implements 10 PhabricatorApplicationTransactionInterface, 11 PhabricatorPolicyInterface, 12 PhabricatorFlaggableInterface, 13 PhabricatorMarkupInterface, 14 PhabricatorDestructibleInterface, 15 PhabricatorDestructibleCodexInterface, 16 PhabricatorProjectInterface, 17 PhabricatorSpacesInterface, 18 PhabricatorConduitResultInterface, 19 PhabricatorFulltextInterface, 20 PhabricatorFerretInterface { 21 22 /** 23 * Shortest hash we'll recognize in raw "a829f32" form. 24 */ 25 const MINIMUM_UNQUALIFIED_HASH = 7; 26 27 /** 28 * Shortest hash we'll recognize in qualified "rXab7ef2f8" form. 29 */ 30 const MINIMUM_QUALIFIED_HASH = 5; 31 32 /** 33 * Minimum number of commits to an empty repository to trigger "import" mode. 34 */ 35 const IMPORT_THRESHOLD = 7; 36 37 const LOWPRI_THRESHOLD = 64; 38 39 const TABLE_PATH = 'repository_path'; 40 const TABLE_PATHCHANGE = 'repository_pathchange'; 41 const TABLE_FILESYSTEM = 'repository_filesystem'; 42 const TABLE_SUMMARY = 'repository_summary'; 43 const TABLE_LINTMESSAGE = 'repository_lintmessage'; 44 const TABLE_PARENTS = 'repository_parents'; 45 const TABLE_COVERAGE = 'repository_coverage'; 46 47 const STATUS_ACTIVE = 'active'; 48 const STATUS_INACTIVE = 'inactive'; 49 50 protected $name; 51 protected $callsign; 52 protected $repositorySlug; 53 protected $uuid; 54 protected $viewPolicy; 55 protected $editPolicy; 56 protected $pushPolicy; 57 protected $profileImagePHID; 58 59 protected $versionControlSystem; 60 protected $details = array(); 61 protected $credentialPHID; 62 protected $almanacServicePHID; 63 protected $spacePHID; 64 protected $localPath; 65 66 private $commitCount = self::ATTACHABLE; 67 private $mostRecentCommit = self::ATTACHABLE; 68 private $projectPHIDs = self::ATTACHABLE; 69 private $uris = self::ATTACHABLE; 70 private $profileImageFile = self::ATTACHABLE; 71 72 73 public static function initializeNewRepository(PhabricatorUser $actor) { 74 $app = id(new PhabricatorApplicationQuery()) 75 ->setViewer($actor) 76 ->withClasses(array(PhabricatorDiffusionApplication::class)) 77 ->executeOne(); 78 79 $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY); 80 $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY); 81 $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY); 82 83 $repository = id(new PhabricatorRepository()) 84 ->setViewPolicy($view_policy) 85 ->setEditPolicy($edit_policy) 86 ->setPushPolicy($push_policy) 87 ->setSpacePHID($actor->getDefaultSpacePHID()); 88 89 // Put the repository in "Importing" mode until we finish 90 // parsing it. 91 $repository->setDetail('importing', true); 92 93 return $repository; 94 } 95 96 protected function getConfiguration() { 97 return array( 98 self::CONFIG_AUX_PHID => true, 99 self::CONFIG_SERIALIZATION => array( 100 'details' => self::SERIALIZATION_JSON, 101 ), 102 self::CONFIG_COLUMN_SCHEMA => array( 103 'name' => 'sort255', 104 'callsign' => 'sort32?', 105 'repositorySlug' => 'sort64?', 106 'versionControlSystem' => 'text32', 107 'uuid' => 'text64?', 108 'pushPolicy' => 'policy', 109 'credentialPHID' => 'phid?', 110 'almanacServicePHID' => 'phid?', 111 'localPath' => 'text128?', 112 'profileImagePHID' => 'phid?', 113 ), 114 self::CONFIG_KEY_SCHEMA => array( 115 'callsign' => array( 116 'columns' => array('callsign'), 117 'unique' => true, 118 ), 119 'key_name' => array( 120 'columns' => array('name(128)'), 121 ), 122 'key_vcs' => array( 123 'columns' => array('versionControlSystem'), 124 ), 125 'key_slug' => array( 126 'columns' => array('repositorySlug'), 127 'unique' => true, 128 ), 129 'key_local' => array( 130 'columns' => array('localPath'), 131 'unique' => true, 132 ), 133 ), 134 ) + parent::getConfiguration(); 135 } 136 137 public function generatePHID() { 138 return PhabricatorPHID::generateNewPHID( 139 PhabricatorRepositoryRepositoryPHIDType::TYPECONST); 140 } 141 142 public static function getStatusMap() { 143 return array( 144 self::STATUS_ACTIVE => array( 145 'name' => pht('Active'), 146 'isTracked' => 1, 147 ), 148 self::STATUS_INACTIVE => array( 149 'name' => pht('Inactive'), 150 'isTracked' => 0, 151 ), 152 ); 153 } 154 155 public static function getStatusNameMap() { 156 return ipull(self::getStatusMap(), 'name'); 157 } 158 159 public function getStatus() { 160 if ($this->isTracked()) { 161 return self::STATUS_ACTIVE; 162 } else { 163 return self::STATUS_INACTIVE; 164 } 165 } 166 167 public function toDictionary() { 168 return array( 169 'id' => $this->getID(), 170 'name' => $this->getName(), 171 'phid' => $this->getPHID(), 172 'callsign' => $this->getCallsign(), 173 'monogram' => $this->getMonogram(), 174 'vcs' => $this->getVersionControlSystem(), 175 'uri' => PhabricatorEnv::getProductionURI($this->getURI()), 176 'remoteURI' => (string)$this->getRemoteURI(), 177 'description' => $this->getDetail('description'), 178 'isActive' => $this->isTracked(), 179 'isHosted' => $this->isHosted(), 180 'isImporting' => $this->isImporting(), 181 'encoding' => $this->getDefaultTextEncoding(), 182 'staging' => array( 183 'supported' => $this->supportsStaging(), 184 'prefix' => 'phabricator', 185 'uri' => $this->getStagingURI(), 186 ), 187 ); 188 } 189 190 public function getDefaultTextEncoding() { 191 return $this->getDetail('encoding', 'UTF-8'); 192 } 193 194 public function getMonogram() { 195 $callsign = $this->getCallsign(); 196 if (phutil_nonempty_string($callsign)) { 197 return "r{$callsign}"; 198 } 199 200 $id = $this->getID(); 201 return "R{$id}"; 202 } 203 204 public function getDisplayName() { 205 $slug = $this->getRepositorySlug(); 206 207 if (phutil_nonempty_string($slug)) { 208 return $slug; 209 } 210 211 return $this->getMonogram(); 212 } 213 214 public function getAllMonograms() { 215 $monograms = array(); 216 217 $monograms[] = 'R'.$this->getID(); 218 219 $callsign = $this->getCallsign(); 220 if (phutil_nonempty_string($callsign)) { 221 $monograms[] = 'r'.$callsign; 222 } 223 224 return $monograms; 225 } 226 227 public function setLocalPath($path) { 228 // Convert any extra slashes ("//") in the path to a single slash ("/"). 229 $path = preg_replace('(//+)', '/', $path); 230 231 return parent::setLocalPath($path); 232 } 233 234 public function getDetail($key, $default = null) { 235 return idx($this->details, $key, $default); 236 } 237 238 public function setDetail($key, $value) { 239 $this->details[$key] = $value; 240 return $this; 241 } 242 243 public function attachCommitCount($count) { 244 $this->commitCount = $count; 245 return $this; 246 } 247 248 public function getCommitCount() { 249 return $this->assertAttached($this->commitCount); 250 } 251 252 public function attachMostRecentCommit( 253 ?PhabricatorRepositoryCommit $commit = null) { 254 $this->mostRecentCommit = $commit; 255 return $this; 256 } 257 258 public function getMostRecentCommit() { 259 return $this->assertAttached($this->mostRecentCommit); 260 } 261 262 public function getDiffusionBrowseURIForPath( 263 PhabricatorUser $user, 264 $path, 265 $line = null, 266 $branch = null) { 267 268 $drequest = DiffusionRequest::newFromDictionary( 269 array( 270 'user' => $user, 271 'repository' => $this, 272 'path' => $path, 273 'branch' => $branch, 274 )); 275 276 return $drequest->generateURI( 277 array( 278 'action' => 'browse', 279 'line' => $line, 280 )); 281 } 282 283 public function getSubversionBaseURI($commit = null) { 284 $subpath = $this->getDetail('svn-subpath'); 285 286 if (!phutil_nonempty_string($subpath)) { 287 $subpath = null; 288 } 289 290 return $this->getSubversionPathURI($subpath, $commit); 291 } 292 293 public function getSubversionPathURI($path = null, $commit = null) { 294 $vcs = $this->getVersionControlSystem(); 295 if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { 296 throw new Exception(pht('Not a subversion repository!')); 297 } 298 299 if ($this->isHosted()) { 300 $uri = 'file://'.$this->getLocalPath(); 301 } else { 302 $uri = $this->getDetail('remote-uri'); 303 } 304 305 $uri = rtrim($uri, '/'); 306 307 if (phutil_nonempty_string($path)) { 308 $path = rawurlencode($path); 309 $path = str_replace('%2F', '/', $path); 310 $uri = $uri.'/'.ltrim($path, '/'); 311 } 312 313 if ($path !== null || $commit !== null) { 314 $uri .= '@'; 315 } 316 317 if ($commit !== null) { 318 $uri .= $commit; 319 } 320 321 return $uri; 322 } 323 324 public function attachProjectPHIDs(array $project_phids) { 325 $this->projectPHIDs = $project_phids; 326 return $this; 327 } 328 329 public function getProjectPHIDs() { 330 return $this->assertAttached($this->projectPHIDs); 331 } 332 333 334 /** 335 * Get the name of the directory this repository should clone or checkout 336 * into. For example, if the repository name is "Example Repository", a 337 * reasonable name might be "example-repository". This is used to help users 338 * get reasonable results when cloning repositories, since they generally do 339 * not want to clone into directories called "X/" or "Example Repository/". 340 * 341 * @return string 342 */ 343 public function getCloneName() { 344 $name = $this->getRepositorySlug(); 345 346 // Make some reasonable effort to produce reasonable default directory 347 // names from repository names. 348 if (!phutil_nonempty_string($name)) { 349 $name = $this->getName(); 350 $name = phutil_utf8_strtolower($name); 351 $name = preg_replace('@[ -/:->]+@', '-', $name); 352 $name = trim($name, '-'); 353 if (!phutil_nonempty_string($name)) { 354 $name = $this->getCallsign(); 355 } 356 } 357 358 return $name; 359 } 360 361 public static function isValidRepositorySlug($slug) { 362 try { 363 self::assertValidRepositorySlug($slug); 364 return true; 365 } catch (Exception $ex) { 366 return false; 367 } 368 } 369 370 public static function assertValidRepositorySlug($slug) { 371 if (!strlen($slug)) { 372 throw new Exception( 373 pht( 374 'The empty string is not a valid repository short name. '. 375 'Repository short names must be at least one character long.')); 376 } 377 378 if (strlen($slug) > 64) { 379 throw new Exception( 380 pht( 381 'The name "%s" is not a valid repository short name. Repository '. 382 'short names must not be longer than 64 characters.', 383 $slug)); 384 } 385 386 if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) { 387 throw new Exception( 388 pht( 389 'The name "%s" is not a valid repository short name. Repository '. 390 'short names may only contain letters, numbers, periods, hyphens '. 391 'and underscores.', 392 $slug)); 393 } 394 395 if (!preg_match('/^[a-zA-Z0-9]/', $slug)) { 396 throw new Exception( 397 pht( 398 'The name "%s" is not a valid repository short name. Repository '. 399 'short names must begin with a letter or number.', 400 $slug)); 401 } 402 403 if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) { 404 throw new Exception( 405 pht( 406 'The name "%s" is not a valid repository short name. Repository '. 407 'short names must end with a letter or number.', 408 $slug)); 409 } 410 411 if (preg_match('/__|--|\\.\\./', $slug)) { 412 throw new Exception( 413 pht( 414 'The name "%s" is not a valid repository short name. Repository '. 415 'short names must not contain multiple consecutive underscores, '. 416 'hyphens, or periods.', 417 $slug)); 418 } 419 420 if (preg_match('/^[A-Z]+\z/', $slug)) { 421 throw new Exception( 422 pht( 423 'The name "%s" is not a valid repository short name. Repository '. 424 'short names may not contain only uppercase letters.', 425 $slug)); 426 } 427 428 if (preg_match('/^\d+\z/', $slug)) { 429 throw new Exception( 430 pht( 431 'The name "%s" is not a valid repository short name. Repository '. 432 'short names may not contain only numbers.', 433 $slug)); 434 } 435 436 if (preg_match('/\\.git/', $slug)) { 437 throw new Exception( 438 pht( 439 'The name "%s" is not a valid repository short name. Repository '. 440 'short names must not end in ".git". This suffix will be added '. 441 'automatically in appropriate contexts.', 442 $slug)); 443 } 444 } 445 446 public static function assertValidCallsign($callsign) { 447 if (!strlen($callsign)) { 448 throw new Exception( 449 pht( 450 'A repository callsign must be at least one character long.')); 451 } 452 453 if (strlen($callsign) > 32) { 454 throw new Exception( 455 pht( 456 'The callsign "%s" is not a valid repository callsign. Callsigns '. 457 'must be no more than 32 bytes long.', 458 $callsign)); 459 } 460 461 if (!preg_match('/^[A-Z]+\z/', $callsign)) { 462 throw new Exception( 463 pht( 464 'The callsign "%s" is not a valid repository callsign. Callsigns '. 465 'may only contain UPPERCASE letters.', 466 $callsign)); 467 } 468 } 469 470 public function getProfileImageURI() { 471 return $this->getProfileImageFile()->getBestURI(); 472 } 473 474 public function attachProfileImageFile(PhabricatorFile $file) { 475 $this->profileImageFile = $file; 476 return $this; 477 } 478 479 public function getProfileImageFile() { 480 return $this->assertAttached($this->profileImageFile); 481 } 482 483 484 485/* -( Remote Command Execution )------------------------------------------- */ 486 487 488 public function execRemoteCommand($pattern /* , $arg, ... */) { 489 $args = func_get_args(); 490 return $this->newRemoteCommandFuture($args)->resolve(); 491 } 492 493 public function execxRemoteCommand($pattern /* , $arg, ... */) { 494 $args = func_get_args(); 495 return $this->newRemoteCommandFuture($args)->resolvex(); 496 } 497 498 public function getRemoteCommandFuture($pattern /* , $arg, ... */) { 499 $args = func_get_args(); 500 return $this->newRemoteCommandFuture($args); 501 } 502 503 public function passthruRemoteCommand($pattern /* , $arg, ... */) { 504 $args = func_get_args(); 505 return $this->newRemoteCommandPassthru($args)->resolve(); 506 } 507 508 private function newRemoteCommandFuture(array $argv) { 509 return $this->newRemoteCommandEngine($argv) 510 ->newFuture(); 511 } 512 513 private function newRemoteCommandPassthru(array $argv) { 514 return $this->newRemoteCommandEngine($argv) 515 ->setPassthru(true) 516 ->newFuture(); 517 } 518 519 private function newRemoteCommandEngine(array $argv) { 520 return DiffusionCommandEngine::newCommandEngine($this) 521 ->setArgv($argv) 522 ->setCredentialPHID($this->getCredentialPHID()) 523 ->setURI($this->getRemoteURIObject()); 524 } 525 526/* -( Local Command Execution )-------------------------------------------- */ 527 528 529 public function execLocalCommand($pattern /* , $arg, ... */) { 530 $args = func_get_args(); 531 return $this->newLocalCommandFuture($args)->resolve(); 532 } 533 534 public function execxLocalCommand($pattern /* , $arg, ... */) { 535 $args = func_get_args(); 536 return $this->newLocalCommandFuture($args)->resolvex(); 537 } 538 539 public function getLocalCommandFuture($pattern /* , $arg, ... */) { 540 $args = func_get_args(); 541 return $this->newLocalCommandFuture($args); 542 } 543 544 public function passthruLocalCommand($pattern /* , $arg, ... */) { 545 $args = func_get_args(); 546 return $this->newLocalCommandPassthru($args)->resolve(); 547 } 548 549 private function newLocalCommandFuture(array $argv) { 550 $this->assertLocalExists(); 551 552 $future = DiffusionCommandEngine::newCommandEngine($this) 553 ->setArgv($argv) 554 ->newFuture(); 555 556 if ($this->usesLocalWorkingCopy()) { 557 $future->setCWD($this->getLocalPath()); 558 } 559 560 return $future; 561 } 562 563 private function newLocalCommandPassthru(array $argv) { 564 $this->assertLocalExists(); 565 566 $future = DiffusionCommandEngine::newCommandEngine($this) 567 ->setArgv($argv) 568 ->setPassthru(true) 569 ->newFuture(); 570 571 if ($this->usesLocalWorkingCopy()) { 572 $future->setCWD($this->getLocalPath()); 573 } 574 575 return $future; 576 } 577 578 public function getURI() { 579 $short_name = $this->getRepositorySlug(); 580 if (phutil_nonempty_string($short_name)) { 581 return "/source/{$short_name}/"; 582 } 583 584 $callsign = $this->getCallsign(); 585 if (phutil_nonempty_string($callsign)) { 586 return "/diffusion/{$callsign}/"; 587 } 588 589 $id = $this->getID(); 590 return "/diffusion/{$id}/"; 591 } 592 593 public function getPathURI($path) { 594 return $this->getURI().ltrim($path, '/'); 595 } 596 597 public function getCommitURI($identifier) { 598 $callsign = $this->getCallsign(); 599 if (phutil_nonempty_string($callsign)) { 600 return "/r{$callsign}{$identifier}"; 601 } 602 603 $id = $this->getID(); 604 return "/R{$id}:{$identifier}"; 605 } 606 607 /** 608 * @return array|null 609 */ 610 public static function parseRepositoryServicePath($request_path, $vcs) { 611 $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); 612 613 $patterns = array( 614 '(^'. 615 '(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'. 616 '(?P<path>.*)'. 617 '\z)', 618 ); 619 620 $identifier = null; 621 foreach ($patterns as $pattern) { 622 $matches = null; 623 if (!preg_match($pattern, $request_path, $matches)) { 624 continue; 625 } 626 627 $identifier = $matches['identifier']; 628 if ($is_git) { 629 $identifier = preg_replace('/\\.git\z/', '', $identifier); 630 } 631 632 $base = $matches['base']; 633 $path = $matches['path']; 634 break; 635 } 636 637 if ($identifier === null) { 638 return null; 639 } 640 641 return array( 642 'identifier' => $identifier, 643 'base' => $base, 644 'path' => $path, 645 ); 646 } 647 648 public function getCanonicalPath($request_path) { 649 $standard_pattern = 650 '(^'. 651 '(?P<prefix>/(?:diffusion|source)/)'. 652 '(?P<identifier>[^/]+)'. 653 '(?P<suffix>(?:/.*)?)'. 654 '\z)'; 655 656 $matches = null; 657 if (preg_match($standard_pattern, $request_path, $matches)) { 658 $suffix = $matches['suffix']; 659 return $this->getPathURI($suffix); 660 } 661 662 $commit_pattern = 663 '(^'. 664 '(?P<prefix>/)'. 665 '(?P<monogram>'. 666 '(?:'. 667 'r(?P<repositoryCallsign>[A-Z]+)'. 668 '|'. 669 'R(?P<repositoryID>[1-9]\d*):'. 670 ')'. 671 '(?P<commit>[a-f0-9]+)'. 672 ')'. 673 '\z)'; 674 675 $matches = null; 676 if (preg_match($commit_pattern, $request_path, $matches)) { 677 $commit = $matches['commit']; 678 return $this->getCommitURI($commit); 679 } 680 681 return null; 682 } 683 684 public function generateURI(array $params) { 685 $req_branch = false; 686 $req_commit = false; 687 688 $action = idx($params, 'action'); 689 switch ($action) { 690 case 'history': 691 case 'clone': 692 case 'blame': 693 case 'browse': 694 case 'document': 695 case 'change': 696 case 'lastmodified': 697 case 'tags': 698 case 'branches': 699 case 'lint': 700 case 'pathtree': 701 case 'refs': 702 case 'compare': 703 break; 704 case 'branch': 705 // NOTE: This does not actually require a branch, and won't have one 706 // in Subversion. Possibly this should be more clear. 707 break; 708 case 'commit': 709 case 'rendering-ref': 710 $req_commit = true; 711 break; 712 default: 713 throw new Exception( 714 pht( 715 'Action "%s" is not a valid repository URI action.', 716 $action)); 717 } 718 719 $path = idx($params, 'path'); 720 $branch = idx($params, 'branch'); 721 $commit = idx($params, 'commit'); 722 $line = idx($params, 'line'); 723 724 $head = idx($params, 'head'); 725 $against = idx($params, 'against'); 726 727 if ($req_commit && !strlen($commit)) { 728 throw new Exception( 729 pht( 730 'Diffusion URI action "%s" requires commit!', 731 $action)); 732 } 733 734 if ($req_branch && !strlen($branch)) { 735 throw new Exception( 736 pht( 737 'Diffusion URI action "%s" requires branch!', 738 $action)); 739 } 740 741 if ($action === 'commit') { 742 return $this->getCommitURI($commit); 743 } 744 745 if (phutil_nonempty_string($path)) { 746 $path = ltrim($path, '/'); 747 $path = str_replace(array(';', '$'), array(';;', '$$'), $path); 748 $path = phutil_escape_uri($path); 749 } 750 751 $raw_branch = $branch; 752 if (phutil_nonempty_string($branch)) { 753 $branch = phutil_escape_uri_path_component($branch); 754 $path = "{$branch}/{$path}"; 755 } 756 757 $raw_commit = $commit; 758 if (phutil_nonempty_scalar($commit)) { 759 $commit = str_replace('$', '$$', $commit); 760 $commit = ';'.phutil_escape_uri($commit); 761 } 762 763 $line = phutil_string_cast($line); 764 if (phutil_nonempty_string($line)) { 765 $line = '$'.phutil_escape_uri($line); 766 } 767 768 $query = array(); 769 switch ($action) { 770 case 'change': 771 case 'history': 772 case 'blame': 773 case 'browse': 774 case 'document': 775 case 'lastmodified': 776 case 'tags': 777 case 'branches': 778 case 'lint': 779 case 'pathtree': 780 case 'refs': 781 $uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}"); 782 break; 783 case 'compare': 784 $uri = $this->getPathURI("/{$action}/"); 785 if (phutil_nonempty_scalar($head)) { 786 $query['head'] = $head; 787 } else if (phutil_nonempty_scalar($raw_commit)) { 788 $query['commit'] = $raw_commit; 789 } else if (phutil_nonempty_scalar($raw_branch)) { 790 $query['head'] = $raw_branch; 791 } 792 793 if (phutil_nonempty_scalar($against)) { 794 $query['against'] = $against; 795 } 796 break; 797 case 'branch': 798 if (strlen($path)) { 799 $uri = $this->getPathURI("/repository/{$path}"); 800 } else { 801 $uri = $this->getPathURI('/'); 802 } 803 break; 804 case 'external': 805 $commit = ltrim($commit, ';'); 806 $uri = "/diffusion/external/{$commit}/"; 807 break; 808 case 'rendering-ref': 809 // This isn't a real URI per se, it's passed as a query parameter to 810 // the ajax changeset stuff but then we parse it back out as though 811 // it came from a URI. 812 $uri = rawurldecode("{$path}{$commit}"); 813 break; 814 case 'clone': 815 $uri = $this->getPathURI("/{$action}/"); 816 break; 817 } 818 819 if ($action == 'rendering-ref') { 820 return $uri; 821 } 822 823 if (isset($params['lint'])) { 824 $params['params'] = idx($params, 'params', array()) + array( 825 'lint' => $params['lint'], 826 ); 827 } 828 829 $query = idx($params, 'params', array()) + $query; 830 831 return new PhutilURI($uri, $query); 832 } 833 834 public function updateURIIndex() { 835 $indexes = array(); 836 837 $uris = $this->getURIs(); 838 foreach ($uris as $uri) { 839 if ($uri->getIsDisabled()) { 840 continue; 841 } 842 843 $indexes[] = $uri->getNormalizedURI(); 844 } 845 846 PhabricatorRepositoryURIIndex::updateRepositoryURIs( 847 $this->getPHID(), 848 $indexes); 849 850 return $this; 851 } 852 853 public function isTracked() { 854 $status = $this->getDetail('tracking-enabled'); 855 $map = self::getStatusMap(); 856 $spec = idx($map, $status); 857 858 if (!$spec) { 859 if ($status) { 860 $status = self::STATUS_ACTIVE; 861 } else { 862 $status = self::STATUS_INACTIVE; 863 } 864 $spec = idx($map, $status); 865 } 866 867 return (bool)idx($spec, 'isTracked', false); 868 } 869 870 public function getDefaultBranch() { 871 $default = $this->getDetail('default-branch'); 872 if (phutil_nonempty_string($default)) { 873 return $default; 874 } 875 876 $default_branches = array( 877 PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master', 878 PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default', 879 ); 880 881 return idx($default_branches, $this->getVersionControlSystem()); 882 } 883 884 public function getDefaultArcanistBranch() { 885 return coalesce($this->getDefaultBranch(), 'svn'); 886 } 887 888 private function isBranchInFilter($branch, $filter_key) { 889 $vcs = $this->getVersionControlSystem(); 890 891 $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); 892 893 $use_filter = ($is_git); 894 if (!$use_filter) { 895 // If this VCS doesn't use filters, pass everything through. 896 return true; 897 } 898 899 900 $filter = $this->getDetail($filter_key, array()); 901 902 // If there's no filter set, let everything through. 903 if (!$filter) { 904 return true; 905 } 906 907 // If this branch isn't literally named `regexp(...)`, and it's in the 908 // filter list, let it through. 909 if (isset($filter[$branch])) { 910 if (self::extractBranchRegexp($branch) === null) { 911 return true; 912 } 913 } 914 915 // If the branch matches a regexp, let it through. 916 foreach ($filter as $pattern => $ignored) { 917 $regexp = self::extractBranchRegexp($pattern); 918 if ($regexp !== null) { 919 if (preg_match($regexp, $branch)) { 920 return true; 921 } 922 } 923 } 924 925 // Nothing matched, so filter this branch out. 926 return false; 927 } 928 929 public static function extractBranchRegexp($pattern) { 930 $matches = null; 931 if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) { 932 return $matches[1]; 933 } 934 return null; 935 } 936 937 public function shouldTrackRef(DiffusionRepositoryRef $ref) { 938 // At least for now, don't track the staging area tags. 939 if ($ref->isTag()) { 940 if (preg_match('(^phabricator/)', $ref->getShortName())) { 941 return false; 942 } 943 } 944 945 if (!$ref->isBranch()) { 946 return true; 947 } 948 949 return $this->shouldTrackBranch($ref->getShortName()); 950 } 951 952 public function shouldTrackBranch($branch) { 953 return $this->isBranchInFilter($branch, 'branch-filter'); 954 } 955 956 public function isBranchPermanentRef($branch) { 957 return $this->isBranchInFilter($branch, 'close-commits-filter'); 958 } 959 960 public function formatCommitName($commit_identifier, $local = false) { 961 $vcs = $this->getVersionControlSystem(); 962 963 $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 964 $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; 965 966 $is_git = ($vcs == $type_git); 967 $is_hg = ($vcs == $type_hg); 968 if ($is_git || $is_hg) { 969 $name = substr($commit_identifier, 0, 12); 970 $need_scope = false; 971 } else { 972 $name = $commit_identifier; 973 $need_scope = true; 974 } 975 976 if (!$local) { 977 $need_scope = true; 978 } 979 980 if ($need_scope) { 981 $callsign = $this->getCallsign(); 982 if ($callsign) { 983 $scope = "r{$callsign}"; 984 } else { 985 $id = $this->getID(); 986 $scope = "R{$id}:"; 987 } 988 $name = $scope.$name; 989 } 990 991 return $name; 992 } 993 994 public function isImporting() { 995 return (bool)$this->getDetail('importing', false); 996 } 997 998 public function isNewlyInitialized() { 999 return (bool)$this->getDetail('newly-initialized', false); 1000 } 1001 1002 public function loadImportProgress() { 1003 $progress = queryfx_all( 1004 $this->establishConnection('r'), 1005 'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d 1006 GROUP BY importStatus', 1007 id(new PhabricatorRepositoryCommit())->getTableName(), 1008 $this->getID()); 1009 1010 $done = 0; 1011 $total = 0; 1012 foreach ($progress as $row) { 1013 $total += $row['N'] * 3; 1014 $status = $row['importStatus']; 1015 if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) { 1016 $done += $row['N']; 1017 } 1018 if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) { 1019 $done += $row['N']; 1020 } 1021 if ($status & PhabricatorRepositoryCommit::IMPORTED_PUBLISH) { 1022 $done += $row['N']; 1023 } 1024 } 1025 1026 if ($total) { 1027 $ratio = ($done / $total); 1028 } else { 1029 $ratio = 0; 1030 } 1031 1032 // Cap this at "99.99%", because it's confusing to users when the actual 1033 // fraction is "99.996%" and it rounds up to "100.00%". 1034 if ($ratio > 0.9999) { 1035 $ratio = 0.9999; 1036 } 1037 1038 return $ratio; 1039 } 1040 1041/* -( Publishing )--------------------------------------------------------- */ 1042 1043 public function newPublisher() { 1044 return id(new PhabricatorRepositoryPublisher()) 1045 ->setRepository($this); 1046 } 1047 1048 public function isPublishingDisabled() { 1049 return $this->getDetail('herald-disabled'); 1050 } 1051 1052 public function getPermanentRefRules() { 1053 return array_keys($this->getDetail('close-commits-filter', array())); 1054 } 1055 1056 /** 1057 * Set Refs which should not automatically get closed via commits. 1058 * This usually includes the name of the main development branch. 1059 */ 1060 public function setPermanentRefRules(array $rules) { 1061 $rules = array_fill_keys($rules, true); 1062 $this->setDetail('close-commits-filter', $rules); 1063 return $this; 1064 } 1065 1066 public function getTrackOnlyRules() { 1067 return array_keys($this->getDetail('branch-filter', array())); 1068 } 1069 1070 /** 1071 * The "Track Only" feature has been deprecated since 2019 in 1072 * https://secure.phabricator.com/T13277 and 1073 * https://we.phorge.it/rPc33f544e741775c52c223bc435331bc3422231ee 1074 * "Track Only" rules can be moved to "Permanent Refs" and/or "Fetch Only". 1075 * The only use case left may be for performance reasons limiting what is 1076 * fetched from an observed remote with tens of thousands of branches. 1077 * 1078 * You can find all repositories which still use this deprecated setting via 1079 * SELECT * FROM phabricator_repository.repository WHERE 1080 * JSON_LENGTH(JSON_EXTRACT(details, '$.branch-filter')) > 0; 1081 */ 1082 public function setTrackOnlyRules(array $rules) { 1083 $rules = array_fill_keys($rules, true); 1084 $this->setDetail('branch-filter', $rules); 1085 return $this; 1086 } 1087 1088 public function supportsFetchRules() { 1089 if ($this->isGit()) { 1090 return true; 1091 } 1092 1093 return false; 1094 } 1095 1096 public function getFetchRules() { 1097 return $this->getDetail('fetch-rules', array()); 1098 } 1099 1100 public function setFetchRules(array $rules) { 1101 return $this->setDetail('fetch-rules', $rules); 1102 } 1103 1104 1105/* -( Repository URI Management )------------------------------------------ */ 1106 1107 1108 /** 1109 * Get the remote URI for this repository. 1110 * 1111 * @return string 1112 * @task uri 1113 */ 1114 public function getRemoteURI() { 1115 return (string)$this->getRemoteURIObject(); 1116 } 1117 1118 1119 /** 1120 * Get the remote URI for this repository, including credentials if they're 1121 * used by this repository. 1122 * 1123 * @return PhutilOpaqueEnvelope URI, possibly including credentials. 1124 * @task uri 1125 */ 1126 public function getRemoteURIEnvelope() { 1127 $uri = $this->getRemoteURIObject(); 1128 1129 $remote_protocol = $this->getRemoteProtocol(); 1130 if ($remote_protocol == 'http' || $remote_protocol == 'https') { 1131 // For SVN, we use `--username` and `--password` flags separately, so 1132 // don't add any credentials here. 1133 if (!$this->isSVN()) { 1134 $credential_phid = $this->getCredentialPHID(); 1135 if ($credential_phid) { 1136 $key = PassphrasePasswordKey::loadFromPHID( 1137 $credential_phid, 1138 PhabricatorUser::getOmnipotentUser()); 1139 1140 $uri->setUser($key->getUsernameEnvelope()->openEnvelope()); 1141 $uri->setPass($key->getPasswordEnvelope()->openEnvelope()); 1142 } 1143 } 1144 } 1145 1146 return new PhutilOpaqueEnvelope((string)$uri); 1147 } 1148 1149 1150 /** 1151 * Get the clone (or checkout) URI for this repository, without authentication 1152 * information. 1153 * 1154 * @return string Repository URI. 1155 * @task uri 1156 */ 1157 public function getPublicCloneURI() { 1158 return (string)$this->getCloneURIObject(); 1159 } 1160 1161 1162 /** 1163 * Get the protocol for the repository's remote. 1164 * 1165 * @return string Protocol, like "ssh" or "git". 1166 * @task uri 1167 */ 1168 public function getRemoteProtocol() { 1169 $uri = $this->getRemoteURIObject(); 1170 return $uri->getProtocol(); 1171 } 1172 1173 1174 /** 1175 * Get a parsed object representation of the repository's remote URI.. 1176 * 1177 * @return PhutilURI A @{class@arcanist:PhutilURI}. 1178 * @task uri 1179 */ 1180 public function getRemoteURIObject() { 1181 $raw_uri = $this->getDetail('remote-uri'); 1182 if (!phutil_nonempty_string($raw_uri)) { 1183 return new PhutilURI(''); 1184 } 1185 1186 if (!strncmp($raw_uri, '/', 1)) { 1187 return new PhutilURI('file://'.$raw_uri); 1188 } 1189 1190 return new PhutilURI($raw_uri); 1191 } 1192 1193 1194 /** 1195 * Get the "best" clone/checkout URI for this repository, on any protocol. 1196 */ 1197 public function getCloneURIObject() { 1198 if (!$this->isHosted()) { 1199 if ($this->isSVN()) { 1200 // Make sure we pick up the "Import Only" path for Subversion, so 1201 // the user clones the repository starting at the correct path, not 1202 // from the root. 1203 $base_uri = $this->getSubversionBaseURI(); 1204 $base_uri = new PhutilURI($base_uri); 1205 $path = $base_uri->getPath(); 1206 if (!$path) { 1207 $path = '/'; 1208 } 1209 1210 // If the trailing "@" is not required to escape the URI, strip it for 1211 // readability. 1212 if (!preg_match('/@.*@/', $path)) { 1213 $path = rtrim($path, '@'); 1214 } 1215 1216 $base_uri->setPath($path); 1217 return $base_uri; 1218 } else { 1219 return $this->getRemoteURIObject(); 1220 } 1221 } 1222 1223 // TODO: This should be cleaned up to deal with all the new URI handling. 1224 $another_copy = id(new PhabricatorRepositoryQuery()) 1225 ->setViewer(PhabricatorUser::getOmnipotentUser()) 1226 ->withPHIDs(array($this->getPHID())) 1227 ->needURIs(true) 1228 ->executeOne(); 1229 1230 $clone_uris = $another_copy->getCloneURIs(); 1231 if (!$clone_uris) { 1232 return null; 1233 } 1234 1235 return head($clone_uris)->getEffectiveURI(); 1236 } 1237 1238 private function getRawHTTPCloneURIObject() { 1239 $uri = PhabricatorEnv::getProductionURI($this->getURI()); 1240 $uri = new PhutilURI($uri); 1241 1242 if ($this->isGit()) { 1243 $uri->setPath($uri->getPath().$this->getCloneName().'.git'); 1244 } else if ($this->isHg()) { 1245 $uri->setPath($uri->getPath().$this->getCloneName().'/'); 1246 } 1247 1248 return $uri; 1249 } 1250 1251 public function delete() { 1252 $this->openTransaction(); 1253 1254 $paths = id(new PhabricatorOwnersPath()) 1255 ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); 1256 foreach ($paths as $path) { 1257 $path->delete(); 1258 } 1259 1260 queryfx( 1261 $this->establishConnection('w'), 1262 'DELETE FROM %T WHERE repositoryPHID = %s', 1263 id(new PhabricatorRepositorySymbol())->getTableName(), 1264 $this->getPHID()); 1265 1266 $commits = id(new PhabricatorRepositoryCommit()) 1267 ->loadAllWhere('repositoryID = %d', $this->getID()); 1268 foreach ($commits as $commit) { 1269 // note PhabricatorRepositoryAuditRequests and 1270 // PhabricatorRepositoryCommitData are deleted here too. 1271 $commit->delete(); 1272 } 1273 1274 $uris = id(new PhabricatorRepositoryURI()) 1275 ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); 1276 foreach ($uris as $uri) { 1277 $uri->delete(); 1278 } 1279 1280 $ref_cursors = id(new PhabricatorRepositoryRefCursor()) 1281 ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); 1282 foreach ($ref_cursors as $cursor) { 1283 $cursor->delete(); 1284 } 1285 1286 $conn_w = $this->establishConnection('w'); 1287 1288 queryfx( 1289 $conn_w, 1290 'DELETE FROM %T WHERE repositoryID = %d', 1291 self::TABLE_FILESYSTEM, 1292 $this->getID()); 1293 1294 queryfx( 1295 $conn_w, 1296 'DELETE FROM %T WHERE repositoryID = %d', 1297 self::TABLE_PATHCHANGE, 1298 $this->getID()); 1299 1300 queryfx( 1301 $conn_w, 1302 'DELETE FROM %T WHERE repositoryID = %d', 1303 self::TABLE_SUMMARY, 1304 $this->getID()); 1305 1306 $result = parent::delete(); 1307 1308 $this->saveTransaction(); 1309 return $result; 1310 } 1311 1312 public function isGit() { 1313 $vcs = $this->getVersionControlSystem(); 1314 return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); 1315 } 1316 1317 public function isSVN() { 1318 $vcs = $this->getVersionControlSystem(); 1319 return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); 1320 } 1321 1322 public function isHg() { 1323 $vcs = $this->getVersionControlSystem(); 1324 return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL); 1325 } 1326 1327 public function isHosted() { 1328 return (bool)$this->getDetail('hosting-enabled', false); 1329 } 1330 1331 public function setHosted($enabled) { 1332 return $this->setDetail('hosting-enabled', $enabled); 1333 } 1334 1335 public function canServeProtocol( 1336 $protocol, 1337 $write, 1338 $is_intracluster = false) { 1339 1340 // See T13192. If a repository is inactive, don't serve it to users. We 1341 // still synchronize it within the cluster and serve it to other repository 1342 // nodes. 1343 if (!$is_intracluster) { 1344 if (!$this->isTracked()) { 1345 return false; 1346 } 1347 } 1348 1349 $clone_uris = $this->getCloneURIs(); 1350 foreach ($clone_uris as $uri) { 1351 if ($uri->getBuiltinProtocol() !== $protocol) { 1352 continue; 1353 } 1354 1355 $io_type = $uri->getEffectiveIoType(); 1356 if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) { 1357 return true; 1358 } 1359 1360 if (!$write) { 1361 if ($io_type == PhabricatorRepositoryURI::IO_READ) { 1362 return true; 1363 } 1364 } 1365 } 1366 1367 if ($write) { 1368 if ($this->isReadOnly()) { 1369 return false; 1370 } 1371 } 1372 1373 return false; 1374 } 1375 1376 public function hasLocalWorkingCopy() { 1377 try { 1378 self::assertLocalExists(); 1379 return true; 1380 } catch (Exception $ex) { 1381 return false; 1382 } 1383 } 1384 1385 /** 1386 * Raise more useful errors when there are basic filesystem problems. 1387 */ 1388 private function assertLocalExists() { 1389 if (!$this->usesLocalWorkingCopy()) { 1390 return; 1391 } 1392 1393 $local = $this->getLocalPath(); 1394 Filesystem::assertExists($local); 1395 Filesystem::assertIsDirectory($local); 1396 Filesystem::assertReadable($local); 1397 } 1398 1399 /** 1400 * Determine if the working copy is bare or not. In Git, this corresponds 1401 * to `--bare`. In Mercurial, `--noupdate`. 1402 */ 1403 public function isWorkingCopyBare() { 1404 switch ($this->getVersionControlSystem()) { 1405 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1406 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 1407 return false; 1408 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 1409 $local = $this->getLocalPath(); 1410 if (Filesystem::pathExists($local.'/.git')) { 1411 return false; 1412 } else { 1413 return true; 1414 } 1415 } 1416 } 1417 1418 public function usesLocalWorkingCopy() { 1419 switch ($this->getVersionControlSystem()) { 1420 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1421 return $this->isHosted(); 1422 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 1423 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 1424 return true; 1425 } 1426 } 1427 1428 public function getHookDirectories() { 1429 $directories = array(); 1430 if (!$this->isHosted()) { 1431 return $directories; 1432 } 1433 1434 $root = $this->getLocalPath(); 1435 1436 switch ($this->getVersionControlSystem()) { 1437 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 1438 if ($this->isWorkingCopyBare()) { 1439 $directories[] = $root.'/hooks/pre-receive-phabricator.d/'; 1440 } else { 1441 $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/'; 1442 } 1443 break; 1444 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1445 $directories[] = $root.'/hooks/pre-commit-phabricator.d/'; 1446 break; 1447 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 1448 // NOTE: We don't support custom Mercurial hooks for now because they're 1449 // messy and we can't easily just drop a `hooks.d/` directory next to 1450 // the hooks. 1451 break; 1452 } 1453 1454 return $directories; 1455 } 1456 1457 public function canDestroyWorkingCopy() { 1458 if ($this->isHosted()) { 1459 // Never destroy hosted working copies. 1460 return false; 1461 } 1462 1463 $default_path = PhabricatorEnv::getEnvConfig( 1464 'repository.default-local-path'); 1465 return Filesystem::isDescendant($this->getLocalPath(), $default_path); 1466 } 1467 1468 public function canUsePathTree() { 1469 return !$this->isSVN(); 1470 } 1471 1472 public function canUseGitLFS() { 1473 if (!$this->isGit()) { 1474 return false; 1475 } 1476 1477 if (!$this->isHosted()) { 1478 return false; 1479 } 1480 1481 if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) { 1482 return false; 1483 } 1484 1485 return true; 1486 } 1487 1488 public function getGitLFSURI($path = null) { 1489 if (!$this->canUseGitLFS()) { 1490 throw new Exception( 1491 pht( 1492 'This repository does not support Git LFS, so Git LFS URIs can '. 1493 'not be generated for it.')); 1494 } 1495 1496 $uri = $this->getRawHTTPCloneURIObject(); 1497 $uri = (string)$uri; 1498 if ($uri[-1] !== '/') { 1499 $uri .= '/'; 1500 } 1501 $uri .= $path; 1502 1503 return $uri; 1504 } 1505 1506 public function canMirror() { 1507 if ($this->isGit() || $this->isHg()) { 1508 return true; 1509 } 1510 1511 return false; 1512 } 1513 1514 public function canAllowDangerousChanges() { 1515 if (!$this->isHosted()) { 1516 return false; 1517 } 1518 1519 // In Git and Mercurial, ref deletions and rewrites are dangerous. 1520 // In Subversion, editing revprops is dangerous. 1521 1522 return true; 1523 } 1524 1525 public function shouldAllowDangerousChanges() { 1526 return (bool)$this->getDetail('allow-dangerous-changes'); 1527 } 1528 1529 public function canAllowEnormousChanges() { 1530 if (!$this->isHosted()) { 1531 return false; 1532 } 1533 1534 return true; 1535 } 1536 1537 public function shouldAllowEnormousChanges() { 1538 return (bool)$this->getDetail('allow-enormous-changes'); 1539 } 1540 1541 public function writeStatusMessage( 1542 $status_type, 1543 $status_code, 1544 array $parameters = array()) { 1545 1546 $table = new PhabricatorRepositoryStatusMessage(); 1547 $conn_w = $table->establishConnection('w'); 1548 $table_name = $table->getTableName(); 1549 1550 if ($status_code === null) { 1551 queryfx( 1552 $conn_w, 1553 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s', 1554 $table_name, 1555 $this->getID(), 1556 $status_type); 1557 } else { 1558 // If the existing message has the same code (e.g., we just hit an 1559 // error and also previously hit an error) we increment the message 1560 // count. This allows us to determine how many times in a row we've 1561 // run into an error. 1562 1563 // NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated 1564 // in order, so the "messageCount" assignment must occur before the 1565 // "statusCode" assignment. See T11705. 1566 1567 queryfx( 1568 $conn_w, 1569 'INSERT INTO %T 1570 (repositoryID, statusType, statusCode, parameters, epoch, 1571 messageCount) 1572 VALUES (%d, %s, %s, %s, %d, %d) 1573 ON DUPLICATE KEY UPDATE 1574 messageCount = 1575 IF( 1576 statusCode = VALUES(statusCode), 1577 messageCount + VALUES(messageCount), 1578 VALUES(messageCount)), 1579 statusCode = VALUES(statusCode), 1580 parameters = VALUES(parameters), 1581 epoch = VALUES(epoch)', 1582 $table_name, 1583 $this->getID(), 1584 $status_type, 1585 $status_code, 1586 json_encode($parameters), 1587 time(), 1588 1); 1589 } 1590 1591 return $this; 1592 } 1593 1594 public static function assertValidRemoteURI($uri) { 1595 if (trim($uri) != $uri) { 1596 throw new Exception( 1597 pht('The remote URI has leading or trailing whitespace.')); 1598 } 1599 1600 $uri_object = new PhutilURI($uri); 1601 $protocol = $uri_object->getProtocol(); 1602 1603 // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619 1604 // for discussion. This is usually a user adding "ssh://" to an implicit 1605 // SSH Git URI. 1606 if ($protocol == 'ssh') { 1607 if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) { 1608 throw new Exception( 1609 pht( 1610 "The remote URI is not formatted correctly. Remote URIs ". 1611 "with an explicit protocol should be in the form ". 1612 "'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.", 1613 'proto://domain/path', 1614 'proto://domain:/path', 1615 ':/path')); 1616 } 1617 } 1618 1619 switch ($protocol) { 1620 case 'ssh': 1621 case 'http': 1622 case 'https': 1623 case 'git': 1624 case 'svn': 1625 case 'svn+ssh': 1626 break; 1627 default: 1628 // NOTE: We're explicitly rejecting 'file://' because it can be 1629 // used to clone from the working copy of another repository on disk 1630 // that you don't normally have permission to access. 1631 1632 throw new Exception( 1633 pht( 1634 'The URI protocol is unrecognized. It should begin with '. 1635 '"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".', 1636 'ssh://', 1637 'http://', 1638 'https://', 1639 'git://', 1640 'svn://', 1641 'svn+ssh://', 1642 'git@domain.com:path')); 1643 } 1644 1645 return true; 1646 } 1647 1648 1649 /** 1650 * Load the pull frequency for this repository, based on the time since the 1651 * last activity. 1652 * 1653 * We pull rarely used repositories less frequently. This finds the most 1654 * recent commit which is older than the current time (which prevents us from 1655 * spinning on repositories with a silly commit post-dated to some time in 1656 * 2037). We adjust the pull frequency based on when the most recent commit 1657 * occurred. 1658 * 1659 * @param int $minimum (optional) The minimum update interval to use, in 1660 * seconds. 1661 * @return int Repository update interval, in seconds. 1662 */ 1663 public function loadUpdateInterval($minimum = 15) { 1664 // First, check if we've hit errors recently. If we have, wait one period 1665 // for each consecutive error. Normally, this corresponds to a backoff of 1666 // 15s, 30s, 45s, etc. 1667 1668 $message_table = new PhabricatorRepositoryStatusMessage(); 1669 $conn = $message_table->establishConnection('r'); 1670 $error_count = queryfx_one( 1671 $conn, 1672 'SELECT MAX(messageCount) error_count FROM %T 1673 WHERE repositoryID = %d 1674 AND statusType IN (%Ls) 1675 AND statusCode IN (%Ls)', 1676 $message_table->getTableName(), 1677 $this->getID(), 1678 array( 1679 PhabricatorRepositoryStatusMessage::TYPE_INIT, 1680 PhabricatorRepositoryStatusMessage::TYPE_FETCH, 1681 ), 1682 array( 1683 PhabricatorRepositoryStatusMessage::CODE_ERROR, 1684 )); 1685 1686 $error_count = (int)$error_count['error_count']; 1687 if ($error_count > 0) { 1688 return (int)($minimum * $error_count); 1689 } 1690 1691 // If a repository is still importing, always pull it as frequently as 1692 // possible. This prevents us from hanging for a long time at 99.9% when 1693 // importing an inactive repository. 1694 if ($this->isImporting()) { 1695 return $minimum; 1696 } 1697 1698 $window_start = (PhabricatorTime::getNow() + $minimum); 1699 1700 $table = new PhabricatorRepositoryCommit(); 1701 $last_commit = queryfx_one( 1702 $table->establishConnection('r'), 1703 'SELECT epoch FROM %T 1704 WHERE repositoryID = %d AND epoch <= %d 1705 ORDER BY epoch DESC LIMIT 1', 1706 $table->getTableName(), 1707 $this->getID(), 1708 $window_start); 1709 if ($last_commit) { 1710 $time_since_commit = ($window_start - $last_commit['epoch']); 1711 } else { 1712 // If the repository has no commits, treat the creation date as 1713 // though it were the date of the last commit. This makes empty 1714 // repositories update quickly at first but slow down over time 1715 // if they don't see any activity. 1716 $time_since_commit = ($window_start - $this->getDateCreated()); 1717 } 1718 1719 $last_few_days = phutil_units('3 days in seconds'); 1720 1721 if ($time_since_commit <= $last_few_days) { 1722 // For repositories with activity in the recent past, we wait one 1723 // extra second for every 10 minutes since the last commit. This 1724 // shorter backoff is intended to handle weekends and other short 1725 // breaks from development. 1726 $smart_wait = ($time_since_commit / 600); 1727 } else { 1728 // For repositories without recent activity, we wait one extra second 1729 // for every 4 minutes since the last commit. This longer backoff 1730 // handles rarely used repositories, up to the maximum. 1731 $smart_wait = ($time_since_commit / 240); 1732 } 1733 1734 // We'll never wait more than 6 hours to pull a repository. 1735 $longest_wait = phutil_units('6 hours in seconds'); 1736 $smart_wait = min($smart_wait, $longest_wait); 1737 $smart_wait = max($minimum, $smart_wait); 1738 1739 return (int)$smart_wait; 1740 } 1741 1742 1743 /** 1744 * Time limit for cloning or copying this repository. 1745 * 1746 * This limit is used to timeout operations like `git clone` or `git fetch` 1747 * when doing intracluster synchronization, building working copies, etc. 1748 * 1749 * @return int Maximum number of seconds to spend copying this repository. 1750 */ 1751 public function getCopyTimeLimit() { 1752 return $this->getDetail('limit.copy'); 1753 } 1754 1755 public function setCopyTimeLimit($limit) { 1756 return $this->setDetail('limit.copy', $limit); 1757 } 1758 1759 public function getDefaultCopyTimeLimit() { 1760 return phutil_units('15 minutes in seconds'); 1761 } 1762 1763 public function getEffectiveCopyTimeLimit() { 1764 $limit = $this->getCopyTimeLimit(); 1765 if ($limit) { 1766 return $limit; 1767 } 1768 1769 return $this->getDefaultCopyTimeLimit(); 1770 } 1771 1772 public function getFilesizeLimit() { 1773 return $this->getDetail('limit.filesize'); 1774 } 1775 1776 public function setFilesizeLimit($limit) { 1777 return $this->setDetail('limit.filesize', $limit); 1778 } 1779 1780 public function getTouchLimit() { 1781 return $this->getDetail('limit.touch'); 1782 } 1783 1784 public function setTouchLimit($limit) { 1785 return $this->setDetail('limit.touch', $limit); 1786 } 1787 1788 /** 1789 * Retrieve the service URI for the device hosting this repository. 1790 * 1791 * See @{method:newConduitClient} for a general discussion of interacting 1792 * with repository services. This method provides lower-level resolution of 1793 * services, returning raw URIs. 1794 * 1795 * @param PhabricatorUser $viewer Viewing user. 1796 * @param map<string, mixed> $options Constraints on selectable services. 1797 * @return string|null URI, or `null` for local repositories. 1798 */ 1799 public function getAlmanacServiceURI( 1800 PhabricatorUser $viewer, 1801 array $options) { 1802 1803 $refs = $this->getAlmanacServiceRefs($viewer, $options); 1804 1805 if (!$refs) { 1806 return null; 1807 } 1808 1809 $ref = head($refs); 1810 return $ref->getURI(); 1811 } 1812 1813 public function getAlmanacServiceRefs( 1814 PhabricatorUser $viewer, 1815 array $options) { 1816 1817 PhutilTypeSpec::checkMap( 1818 $options, 1819 array( 1820 'neverProxy' => 'bool', 1821 'protocols' => 'list<string>', 1822 'writable' => 'optional bool', 1823 )); 1824 1825 $never_proxy = $options['neverProxy']; 1826 $protocols = $options['protocols']; 1827 $writable = idx($options, 'writable', false); 1828 1829 $cache_key = $this->getAlmanacServiceCacheKey(); 1830 if (!$cache_key) { 1831 return array(); 1832 } 1833 1834 $cache = PhabricatorCaches::getMutableStructureCache(); 1835 $uris = $cache->getKey($cache_key, false); 1836 1837 // If we haven't built the cache yet, build it now. 1838 if ($uris === false) { 1839 $uris = $this->buildAlmanacServiceURIs(); 1840 $cache->setKey($cache_key, $uris); 1841 } 1842 1843 if ($uris === null) { 1844 return array(); 1845 } 1846 1847 $local_device = AlmanacKeys::getDeviceID(); 1848 if ($never_proxy && !$local_device) { 1849 throw new Exception( 1850 pht( 1851 'Unable to handle proxied service request. This device is not '. 1852 'registered, so it can not identify local services. Register '. 1853 'this device before sending requests here.')); 1854 } 1855 1856 $protocol_map = array_fuse($protocols); 1857 1858 $results = array(); 1859 foreach ($uris as $uri) { 1860 // If we're never proxying this and it's locally satisfiable, return 1861 // `null` to tell the caller to handle it locally. If we're allowed to 1862 // proxy, we skip this check and may proxy the request to ourselves. 1863 // (That proxied request will end up here with proxying forbidden, 1864 // return `null`, and then the request will actually run.) 1865 1866 if ($local_device && $never_proxy) { 1867 if ($uri['device'] == $local_device) { 1868 return array(); 1869 } 1870 } 1871 1872 if (isset($protocol_map[$uri['protocol']])) { 1873 $results[] = $uri; 1874 } 1875 } 1876 1877 if (!$results) { 1878 throw new Exception( 1879 pht( 1880 'The Almanac service for this repository is not bound to any '. 1881 'interfaces which support the required protocols (%s).', 1882 implode(', ', $protocols))); 1883 } 1884 1885 if ($never_proxy) { 1886 // See PHI1030. This error can arise from various device name/address 1887 // mismatches which are hard to detect, so try to provide as much 1888 // information as we can. 1889 1890 if ($writable) { 1891 $request_type = pht('(This is a write request.)'); 1892 } else { 1893 $request_type = pht('(This is a read request.)'); 1894 } 1895 1896 throw new Exception( 1897 pht( 1898 'This repository request (for repository "%s") has been '. 1899 'incorrectly routed to a cluster host (with device name "%s", '. 1900 'and hostname "%s") which can not serve the request.'. 1901 "\n\n". 1902 'The Almanac device address for the correct device may improperly '. 1903 'point at this host, or the "device.id" configuration file on '. 1904 'this host may be incorrect.'. 1905 "\n\n". 1906 'Requests routed within the cluster are always '. 1907 'expected to be sent to a node which can serve the request. To '. 1908 'prevent loops, this request will not be proxied again.'. 1909 "\n\n". 1910 "%s", 1911 $this->getDisplayName(), 1912 $local_device, 1913 php_uname('n'), 1914 $request_type)); 1915 } 1916 1917 if (count($results) > 1) { 1918 if (!$this->supportsSynchronization()) { 1919 throw new Exception( 1920 pht( 1921 'Repository "%s" is bound to multiple active repository hosts, '. 1922 'but this repository does not support cluster synchronization. '. 1923 'Declusterize this repository or move it to a service with only '. 1924 'one host.', 1925 $this->getDisplayName())); 1926 } 1927 } 1928 1929 $refs = array(); 1930 foreach ($results as $result) { 1931 $refs[] = DiffusionServiceRef::newFromDictionary($result); 1932 } 1933 1934 // If we require a writable device, remove URIs which aren't writable. 1935 if ($writable) { 1936 foreach ($refs as $key => $ref) { 1937 if (!$ref->isWritable()) { 1938 unset($refs[$key]); 1939 } 1940 } 1941 1942 if (!$refs) { 1943 throw new Exception( 1944 pht( 1945 'This repository ("%s") is not writable with the given '. 1946 'protocols (%s). The Almanac service for this repository has no '. 1947 'writable bindings that support these protocols.', 1948 $this->getDisplayName(), 1949 implode(', ', $protocols))); 1950 } 1951 } 1952 1953 if ($writable) { 1954 $refs = $this->sortWritableAlmanacServiceRefs($refs); 1955 } else { 1956 $refs = $this->sortReadableAlmanacServiceRefs($refs); 1957 } 1958 1959 return array_values($refs); 1960 } 1961 1962 /** 1963 * @param array<DiffusionServiceRef> $refs 1964 */ 1965 private function sortReadableAlmanacServiceRefs(array $refs) { 1966 assert_instances_of($refs, DiffusionServiceRef::class); 1967 shuffle($refs); 1968 return $refs; 1969 } 1970 1971 /** 1972 * @param array<DiffusionServiceRef> $refs 1973 */ 1974 private function sortWritableAlmanacServiceRefs(array $refs) { 1975 assert_instances_of($refs, DiffusionServiceRef::class); 1976 1977 // See T13109 for discussion of how this method routes requests. 1978 1979 // In the absence of other rules, we'll send traffic to devices randomly. 1980 // We also want to select randomly among nodes which are equally good 1981 // candidates to receive the write, and accomplish that by shuffling the 1982 // list up front. 1983 shuffle($refs); 1984 1985 $order = array(); 1986 1987 // If some device is currently holding the write lock, send all requests 1988 // to that device. We're trying to queue writes on a single device so they 1989 // do not need to wait for read synchronization after earlier writes 1990 // complete. 1991 $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter( 1992 $this->getPHID()); 1993 if ($writer) { 1994 $device_phid = $writer->getWriteProperty('devicePHID'); 1995 foreach ($refs as $key => $ref) { 1996 if ($ref->getDevicePHID() === $device_phid) { 1997 $order[] = $key; 1998 } 1999 } 2000 } 2001 2002 // If no device is currently holding the write lock, try to send requests 2003 // to a device which is already up to date and will not need to synchronize 2004 // before it can accept the write. 2005 $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( 2006 $this->getPHID()); 2007 if ($versions) { 2008 $max_version = (int)max(mpull($versions, 'getRepositoryVersion')); 2009 2010 $max_devices = array(); 2011 foreach ($versions as $version) { 2012 if ($version->getRepositoryVersion() == $max_version) { 2013 $max_devices[] = $version->getDevicePHID(); 2014 } 2015 } 2016 $max_devices = array_fuse($max_devices); 2017 2018 foreach ($refs as $key => $ref) { 2019 if (isset($max_devices[$ref->getDevicePHID()])) { 2020 $order[] = $key; 2021 } 2022 } 2023 } 2024 2025 // Reorder the results, putting any we've selected as preferred targets for 2026 // the write at the head of the list. 2027 $refs = array_select_keys($refs, $order) + $refs; 2028 2029 return $refs; 2030 } 2031 2032 public function supportsSynchronization() { 2033 // TODO: For now, this is only supported for Git. 2034 if (!$this->isGit()) { 2035 return false; 2036 } 2037 2038 return true; 2039 } 2040 2041 2042 public function supportsRefs() { 2043 if ($this->isSVN()) { 2044 return false; 2045 } 2046 2047 return true; 2048 } 2049 2050 public function getAlmanacServiceCacheKey() { 2051 $service_phid = $this->getAlmanacServicePHID(); 2052 if (!$service_phid) { 2053 return null; 2054 } 2055 2056 $repository_phid = $this->getPHID(); 2057 2058 $parts = array( 2059 "repo({$repository_phid})", 2060 "serv({$service_phid})", 2061 'v4', 2062 ); 2063 2064 return implode('.', $parts); 2065 } 2066 2067 private function buildAlmanacServiceURIs() { 2068 $service = $this->loadAlmanacService(); 2069 if (!$service) { 2070 return null; 2071 } 2072 2073 $bindings = $service->getActiveBindings(); 2074 if (!$bindings) { 2075 throw new Exception( 2076 pht( 2077 'The Almanac service for this repository is not bound to any '. 2078 'active interfaces.')); 2079 } 2080 2081 $uris = array(); 2082 foreach ($bindings as $binding) { 2083 $iface = $binding->getInterface(); 2084 2085 $uri = $this->getClusterRepositoryURIFromBinding($binding); 2086 $protocol = $uri->getProtocol(); 2087 $device_name = $iface->getDevice()->getName(); 2088 $device_phid = $iface->getDevice()->getPHID(); 2089 2090 $uris[] = array( 2091 'protocol' => $protocol, 2092 'uri' => (string)$uri, 2093 'device' => $device_name, 2094 'writable' => (bool)$binding->getAlmanacPropertyValue('writable'), 2095 'devicePHID' => $device_phid, 2096 ); 2097 } 2098 2099 return $uris; 2100 } 2101 2102 /** 2103 * Build a new Conduit client in order to make a service call to this 2104 * repository. 2105 * 2106 * If the repository is hosted locally, this method may return `null`. The 2107 * caller should use `ConduitCall` or other local logic to complete the 2108 * request. 2109 * 2110 * By default, we will return a @{class:ConduitClient} for any repository with 2111 * a service, even if that service is on the current device. 2112 * 2113 * We do this because this configuration does not make very much sense in a 2114 * production context, but is very common in a test/development context 2115 * (where the developer's machine is both the web host and the repository 2116 * service). By proxying in development, we get more consistent behavior 2117 * between development and production, and don't have a major untested 2118 * codepath. 2119 * 2120 * The `$never_proxy` parameter can be used to prevent this local proxying. 2121 * If the flag is passed: 2122 * 2123 * - The method will return `null` (implying a local service call) 2124 * if the repository service is hosted on the current device. 2125 * - The method will throw if it would need to return a client. 2126 * 2127 * This is used to prevent loops in Conduit: the first request will proxy, 2128 * even in development, but the second request will be identified as a 2129 * cluster request and forced not to proxy. 2130 * 2131 * For lower-level service resolution, see @{method:getAlmanacServiceURI}. 2132 * 2133 * @param PhabricatorUser $viewer Viewing user. 2134 * @param bool $never_proxy (optional) `true` to throw if a client would be 2135 * returned. 2136 * @return ConduitClient|null Client, or `null` for local repositories. 2137 */ 2138 public function newConduitClient( 2139 PhabricatorUser $viewer, 2140 $never_proxy = false) { 2141 2142 $uri = $this->getAlmanacServiceURI( 2143 $viewer, 2144 array( 2145 'neverProxy' => $never_proxy, 2146 'protocols' => array( 2147 'http', 2148 'https', 2149 ), 2150 2151 // At least today, no Conduit call can ever write to a repository, 2152 // so it's fine to send anything to a read-only node. 2153 'writable' => false, 2154 )); 2155 if ($uri === null) { 2156 return null; 2157 } 2158 2159 $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain(); 2160 2161 $client = id(new ConduitClient($uri)) 2162 ->setHost($domain); 2163 2164 if ($viewer->isOmnipotent()) { 2165 // If the caller is the omnipotent user (normally, a daemon), we will 2166 // sign the request with this host's asymmetric keypair. 2167 2168 $public_path = AlmanacKeys::getKeyPath('device.pub'); 2169 try { 2170 $public_key = Filesystem::readFile($public_path); 2171 } catch (Exception $ex) { 2172 throw new PhutilAggregateException( 2173 pht( 2174 'Unable to read device public key while attempting to make '. 2175 'authenticated method call within the cluster. '. 2176 'Use `%s` to register keys for this device. Exception: %s', 2177 'bin/almanac register', 2178 $ex->getMessage()), 2179 array($ex)); 2180 } 2181 2182 $private_path = AlmanacKeys::getKeyPath('device.key'); 2183 try { 2184 $private_key = Filesystem::readFile($private_path); 2185 $private_key = new PhutilOpaqueEnvelope($private_key); 2186 } catch (Exception $ex) { 2187 throw new PhutilAggregateException( 2188 pht( 2189 'Unable to read device private key while attempting to make '. 2190 'authenticated method call within the cluster. '. 2191 'Use `%s` to register keys for this device. Exception: %s', 2192 'bin/almanac register', 2193 $ex->getMessage()), 2194 array($ex)); 2195 } 2196 2197 $client->setSigningKeys($public_key, $private_key); 2198 } else { 2199 // If the caller is a normal user, we generate or retrieve a cluster 2200 // API token. 2201 2202 $token = PhabricatorConduitToken::loadClusterTokenForUser($viewer); 2203 if ($token) { 2204 $client->setConduitToken($token->getToken()); 2205 } 2206 } 2207 2208 return $client; 2209 } 2210 2211 public function newConduitClientForRequest(ConduitAPIRequest $request) { 2212 // Figure out whether we're going to handle this request on this device, 2213 // or proxy it to another node in the cluster. 2214 2215 // If this is a cluster request and we need to proxy, we'll explode here 2216 // to prevent infinite recursion. 2217 2218 $viewer = $request->getViewer(); 2219 $is_cluster_request = $request->getIsClusterRequest(); 2220 2221 $client = $this->newConduitClient( 2222 $viewer, 2223 $is_cluster_request); 2224 2225 return $client; 2226 } 2227 2228 public function newConduitFuture( 2229 PhabricatorUser $viewer, 2230 $method, 2231 array $params, 2232 $never_proxy = false) { 2233 2234 $client = $this->newConduitClient( 2235 $viewer, 2236 $never_proxy); 2237 2238 if (!$client) { 2239 $conduit_call = id(new ConduitCall($method, $params)) 2240 ->setUser($viewer); 2241 $future = new MethodCallFuture($conduit_call, 'execute'); 2242 } else { 2243 $future = $client->callMethod($method, $params); 2244 } 2245 2246 return $future; 2247 } 2248 2249 public function getPassthroughEnvironmentalVariables() { 2250 $env = $_ENV; 2251 2252 if ($this->isGit()) { 2253 // $_ENV does not populate in CLI contexts if "E" is missing from 2254 // "variables_order" in PHP config. Currently, we do not require this 2255 // to be configured. Since it may not be, explicitly bring expected Git 2256 // environmental variables into scope. This list is not exhaustive, but 2257 // only lists variables with a known impact on commit hook behavior. 2258 2259 // This can be removed if we later require "E" in "variables_order". 2260 2261 $git_env = array( 2262 'GIT_OBJECT_DIRECTORY', 2263 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 2264 'GIT_QUARANTINE_PATH', 2265 ); 2266 foreach ($git_env as $key) { 2267 $value = getenv($key); 2268 if ($value && strlen($value)) { 2269 $env[$key] = $value; 2270 } 2271 } 2272 2273 $key = 'GIT_PUSH_OPTION_COUNT'; 2274 $git_count = getenv($key); 2275 if ($git_count && strlen($git_count)) { 2276 $git_count = (int)$git_count; 2277 $env[$key] = $git_count; 2278 for ($ii = 0; $ii < $git_count; $ii++) { 2279 $key = 'GIT_PUSH_OPTION_'.$ii; 2280 $env[$key] = getenv($key); 2281 } 2282 } 2283 } 2284 2285 $result = array(); 2286 foreach ($env as $key => $value) { 2287 // In Git, pass anything matching "GIT_*" though. Some of these variables 2288 // need to be preserved to allow `git` operations to work properly when 2289 // running from commit hooks. 2290 if ($this->isGit()) { 2291 if (preg_match('/^GIT_/', $key)) { 2292 $result[$key] = $value; 2293 } 2294 } 2295 } 2296 2297 return $result; 2298 } 2299 2300 public function supportsBranchComparison() { 2301 return $this->isGit(); 2302 } 2303 2304 public function isReadOnly() { 2305 return (bool)$this->getDetail('read-only'); 2306 } 2307 2308 public function setReadOnly($read_only) { 2309 return $this->setDetail('read-only', $read_only); 2310 } 2311 2312 public function getReadOnlyMessage() { 2313 return $this->getDetail('read-only-message'); 2314 } 2315 2316 public function setReadOnlyMessage($message) { 2317 return $this->setDetail('read-only-message', $message); 2318 } 2319 2320 public function getReadOnlyMessageForDisplay() { 2321 $parts = array(); 2322 $parts[] = pht( 2323 'This repository is currently in read-only maintenance mode.'); 2324 2325 $message = $this->getReadOnlyMessage(); 2326 if ($message !== null) { 2327 $parts[] = $message; 2328 } 2329 2330 return implode("\n\n", $parts); 2331 } 2332 2333/* -( Repository URIs )---------------------------------------------------- */ 2334 2335 2336 public function attachURIs(array $uris) { 2337 $custom_map = array(); 2338 foreach ($uris as $key => $uri) { 2339 $builtin_key = $uri->getRepositoryURIBuiltinKey(); 2340 if ($builtin_key !== null) { 2341 $custom_map[$builtin_key] = $key; 2342 } 2343 } 2344 2345 $builtin_uris = $this->newBuiltinURIs(); 2346 $seen_builtins = array(); 2347 foreach ($builtin_uris as $builtin_uri) { 2348 $builtin_key = $builtin_uri->getRepositoryURIBuiltinKey(); 2349 $seen_builtins[$builtin_key] = true; 2350 2351 // If this builtin URI is disabled, don't attach it and remove the 2352 // persisted version if it exists. 2353 if ($builtin_uri->getIsDisabled()) { 2354 if (isset($custom_map[$builtin_key])) { 2355 unset($uris[$custom_map[$builtin_key]]); 2356 } 2357 continue; 2358 } 2359 2360 // If the URI exists, make sure it's marked as not being disabled. 2361 if (isset($custom_map[$builtin_key])) { 2362 $uris[$custom_map[$builtin_key]]->setIsDisabled(false); 2363 } 2364 } 2365 2366 // Remove any builtins which no longer exist. 2367 foreach ($custom_map as $builtin_key => $key) { 2368 if (empty($seen_builtins[$builtin_key])) { 2369 unset($uris[$key]); 2370 } 2371 } 2372 2373 $this->uris = $uris; 2374 2375 return $this; 2376 } 2377 2378 public function getURIs() { 2379 return $this->assertAttached($this->uris); 2380 } 2381 2382 public function getCloneURIs() { 2383 $uris = $this->getURIs(); 2384 2385 $clone = array(); 2386 foreach ($uris as $uri) { 2387 if (!$uri->isBuiltin()) { 2388 continue; 2389 } 2390 2391 if ($uri->getIsDisabled()) { 2392 continue; 2393 } 2394 2395 $io_type = $uri->getEffectiveIoType(); 2396 $is_clone = 2397 ($io_type == PhabricatorRepositoryURI::IO_READ) || 2398 ($io_type == PhabricatorRepositoryURI::IO_READWRITE); 2399 2400 if (!$is_clone) { 2401 continue; 2402 } 2403 2404 $clone[] = $uri; 2405 } 2406 2407 $clone = msort($clone, 'getURIScore'); 2408 $clone = array_reverse($clone); 2409 2410 return $clone; 2411 } 2412 2413 2414 public function newBuiltinURIs() { 2415 $has_callsign = ($this->getCallsign() !== null); 2416 $has_shortname = ($this->getRepositorySlug() !== null); 2417 2418 $identifier_map = array( 2419 PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign, 2420 PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname, 2421 PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true, 2422 ); 2423 2424 // If the view policy of the repository is public, support anonymous HTTP 2425 // even if authenticated HTTP is not supported. 2426 if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) { 2427 $allow_http = true; 2428 } else { 2429 $allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); 2430 } 2431 2432 $base_uri = PhabricatorEnv::getURI('/'); 2433 $base_uri = new PhutilURI($base_uri); 2434 $has_https = ($base_uri->getProtocol() == 'https'); 2435 $has_https = ($has_https && $allow_http); 2436 2437 $has_http = !PhabricatorEnv::getEnvConfig('security.require-https'); 2438 $has_http = ($has_http && $allow_http); 2439 2440 // HTTP is not supported for Subversion. 2441 if ($this->isSVN()) { 2442 $has_http = false; 2443 $has_https = false; 2444 } 2445 2446 $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); 2447 $has_ssh = phutil_nonempty_string($phd_user); 2448 2449 $protocol_map = array( 2450 PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh, 2451 PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https, 2452 PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http, 2453 ); 2454 2455 $uris = array(); 2456 foreach ($protocol_map as $protocol => $proto_supported) { 2457 foreach ($identifier_map as $identifier => $id_supported) { 2458 // This is just a dummy value because it can't be empty; we'll force 2459 // it to a proper value when using it in the UI. 2460 $builtin_uri = "{$protocol}://{$identifier}"; 2461 $uris[] = PhabricatorRepositoryURI::initializeNewURI() 2462 ->setRepositoryPHID($this->getPHID()) 2463 ->attachRepository($this) 2464 ->setBuiltinProtocol($protocol) 2465 ->setBuiltinIdentifier($identifier) 2466 ->setURI($builtin_uri) 2467 ->setIsDisabled((int)(!$proto_supported || !$id_supported)); 2468 } 2469 } 2470 2471 return $uris; 2472 } 2473 2474 2475 public function getClusterRepositoryURIFromBinding( 2476 AlmanacBinding $binding) { 2477 $protocol = $binding->getAlmanacPropertyValue('protocol'); 2478 if ($protocol === null) { 2479 $protocol = 'https'; 2480 } 2481 2482 $iface = $binding->getInterface(); 2483 $address = $iface->renderDisplayAddress(); 2484 2485 $path = $this->getURI(); 2486 2487 return id(new PhutilURI("{$protocol}://{$address}")) 2488 ->setPath($path); 2489 } 2490 2491 public function loadAlmanacService() { 2492 $service_phid = $this->getAlmanacServicePHID(); 2493 if (!$service_phid) { 2494 // No service, so this is a local repository. 2495 return null; 2496 } 2497 2498 $service = id(new AlmanacServiceQuery()) 2499 ->setViewer(PhabricatorUser::getOmnipotentUser()) 2500 ->withPHIDs(array($service_phid)) 2501 ->needActiveBindings(true) 2502 ->needProperties(true) 2503 ->executeOne(); 2504 if (!$service) { 2505 throw new Exception( 2506 pht( 2507 'The Almanac service for this repository is invalid or could not '. 2508 'be loaded.')); 2509 } 2510 2511 $service_type = $service->getServiceImplementation(); 2512 if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) { 2513 throw new Exception( 2514 pht( 2515 'The Almanac service for this repository does not have the correct '. 2516 'service type.')); 2517 } 2518 2519 return $service; 2520 } 2521 2522 public function markImporting() { 2523 $this->openTransaction(); 2524 $this->beginReadLocking(); 2525 $repository = $this->reload(); 2526 $repository->setDetail('importing', true); 2527 $repository->save(); 2528 $this->endReadLocking(); 2529 $this->saveTransaction(); 2530 2531 return $repository; 2532 } 2533 2534 2535/* -( Symbols )-------------------------------------------------------------*/ 2536 2537 public function getSymbolSources() { 2538 return $this->getDetail('symbol-sources', array()); 2539 } 2540 2541 public function getSymbolLanguages() { 2542 return $this->getDetail('symbol-languages', array()); 2543 } 2544 2545 2546/* -( Staging )------------------------------------------------------------ */ 2547 2548 2549 public function supportsStaging() { 2550 return $this->isGit(); 2551 } 2552 2553 2554 public function getStagingURI() { 2555 if (!$this->supportsStaging()) { 2556 return null; 2557 } 2558 return $this->getDetail('staging-uri', null); 2559 } 2560 2561 2562/* -( Automation )--------------------------------------------------------- */ 2563 2564 2565 public function supportsAutomation() { 2566 return $this->isGit(); 2567 } 2568 2569 public function canPerformAutomation() { 2570 if (!$this->supportsAutomation()) { 2571 return false; 2572 } 2573 2574 if (!$this->getAutomationBlueprintPHIDs()) { 2575 return false; 2576 } 2577 2578 return true; 2579 } 2580 2581 public function getAutomationBlueprintPHIDs() { 2582 if (!$this->supportsAutomation()) { 2583 return array(); 2584 } 2585 return $this->getDetail('automation.blueprintPHIDs', array()); 2586 } 2587 2588 2589/* -( PhabricatorApplicationTransactionInterface )------------------------- */ 2590 2591 2592 public function getApplicationTransactionEditor() { 2593 return new PhabricatorRepositoryEditor(); 2594 } 2595 2596 public function getApplicationTransactionTemplate() { 2597 return new PhabricatorRepositoryTransaction(); 2598 } 2599 2600 2601/* -( PhabricatorPolicyInterface )----------------------------------------- */ 2602 2603 2604 public function getCapabilities() { 2605 return array( 2606 PhabricatorPolicyCapability::CAN_VIEW, 2607 PhabricatorPolicyCapability::CAN_EDIT, 2608 DiffusionPushCapability::CAPABILITY, 2609 ); 2610 } 2611 2612 public function getPolicy($capability) { 2613 switch ($capability) { 2614 case PhabricatorPolicyCapability::CAN_VIEW: 2615 return $this->getViewPolicy(); 2616 case PhabricatorPolicyCapability::CAN_EDIT: 2617 return $this->getEditPolicy(); 2618 case DiffusionPushCapability::CAPABILITY: 2619 return $this->getPushPolicy(); 2620 } 2621 } 2622 2623 public function hasAutomaticCapability($capability, PhabricatorUser $user) { 2624 return false; 2625 } 2626 2627 2628/* -( PhabricatorMarkupInterface )----------------------------------------- */ 2629 2630 2631 public function getMarkupFieldKey($field) { 2632 $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); 2633 return "repo:{$hash}"; 2634 } 2635 2636 public function newMarkupEngine($field) { 2637 return PhabricatorMarkupEngine::newMarkupEngine(array()); 2638 } 2639 2640 public function getMarkupText($field) { 2641 return $this->getDetail('description'); 2642 } 2643 2644 public function didMarkupText( 2645 $field, 2646 $output, 2647 PhutilMarkupEngine $engine) { 2648 require_celerity_resource('phabricator-remarkup-css'); 2649 return phutil_tag( 2650 'div', 2651 array( 2652 'class' => 'phabricator-remarkup', 2653 ), 2654 $output); 2655 } 2656 2657 public function shouldUseMarkupCache($field) { 2658 return true; 2659 } 2660 2661 2662/* -( PhabricatorDestructibleInterface )----------------------------------- */ 2663 2664 2665 public function destroyObjectPermanently( 2666 PhabricatorDestructionEngine $engine) { 2667 2668 $phid = $this->getPHID(); 2669 2670 $this->openTransaction(); 2671 2672 $this->delete(); 2673 2674 PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array()); 2675 2676 $books = id(new DivinerBookQuery()) 2677 ->setViewer($engine->getViewer()) 2678 ->withRepositoryPHIDs(array($phid)) 2679 ->execute(); 2680 foreach ($books as $book) { 2681 $engine->destroyObject($book); 2682 } 2683 2684 $atoms = id(new DivinerAtomQuery()) 2685 ->setViewer($engine->getViewer()) 2686 ->withRepositoryPHIDs(array($phid)) 2687 ->execute(); 2688 foreach ($atoms as $atom) { 2689 $engine->destroyObject($atom); 2690 } 2691 2692 $lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery()) 2693 ->setViewer($engine->getViewer()) 2694 ->withRepositoryPHIDs(array($phid)) 2695 ->execute(); 2696 foreach ($lfs_refs as $ref) { 2697 $engine->destroyObject($ref); 2698 } 2699 2700 $this->saveTransaction(); 2701 } 2702 2703 2704/* -( PhabricatorDestructibleCodexInterface )------------------------------ */ 2705 2706 2707 public function newDestructibleCodex() { 2708 return new PhabricatorRepositoryDestructibleCodex(); 2709 } 2710 2711 2712/* -( PhabricatorSpacesInterface )----------------------------------------- */ 2713 2714 2715 public function getSpacePHID() { 2716 return $this->spacePHID; 2717 } 2718 2719/* -( PhabricatorConduitResultInterface )---------------------------------- */ 2720 2721 2722 public function getFieldSpecificationsForConduit() { 2723 return array( 2724 id(new PhabricatorConduitSearchFieldSpecification()) 2725 ->setKey('name') 2726 ->setType('string') 2727 ->setDescription(pht('The repository name.')), 2728 id(new PhabricatorConduitSearchFieldSpecification()) 2729 ->setKey('vcs') 2730 ->setType('string') 2731 ->setDescription( 2732 pht('The VCS this repository uses ("git", "hg" or "svn").')), 2733 id(new PhabricatorConduitSearchFieldSpecification()) 2734 ->setKey('callsign') 2735 ->setType('string') 2736 ->setDescription(pht('The repository callsign, if it has one.')), 2737 id(new PhabricatorConduitSearchFieldSpecification()) 2738 ->setKey('shortName') 2739 ->setType('string') 2740 ->setDescription(pht('Unique short name, if the repository has one.')), 2741 id(new PhabricatorConduitSearchFieldSpecification()) 2742 ->setKey('status') 2743 ->setType('string') 2744 ->setDescription(pht('Active or inactive status.')), 2745 id(new PhabricatorConduitSearchFieldSpecification()) 2746 ->setKey('isImporting') 2747 ->setType('bool') 2748 ->setDescription( 2749 pht( 2750 'True if the repository is importing initial commits.')), 2751 id(new PhabricatorConduitSearchFieldSpecification()) 2752 ->setKey('almanacServicePHID') 2753 ->setType('phid?') 2754 ->setDescription( 2755 pht( 2756 'The Almanac Service that hosts this repository, if the '. 2757 'repository is clustered.')), 2758 id(new PhabricatorConduitSearchFieldSpecification()) 2759 ->setKey('refRules') 2760 ->setType('map<string, list<string>>') 2761 ->setDescription( 2762 pht( 2763 'The "Fetch" and "Permanent Ref" rules for this repository.')), 2764 id(new PhabricatorConduitSearchFieldSpecification()) 2765 ->setKey('defaultBranch') 2766 ->setType('string?') 2767 ->setDescription(pht('Default branch name.')), 2768 id(new PhabricatorConduitSearchFieldSpecification()) 2769 ->setKey('description') 2770 ->setType('remarkup') 2771 ->setDescription(pht('Repository description.')), 2772 ); 2773 } 2774 2775 public function getFieldValuesForConduit() { 2776 $fetch_rules = $this->getFetchRules(); 2777 $track_rules = $this->getTrackOnlyRules(); 2778 $permanent_rules = $this->getPermanentRefRules(); 2779 2780 $fetch_rules = $this->getStringListForConduit($fetch_rules); 2781 $track_rules = $this->getStringListForConduit($track_rules); 2782 $permanent_rules = $this->getStringListForConduit($permanent_rules); 2783 2784 $default_branch = $this->getDefaultBranch(); 2785 if (!phutil_nonempty_string($default_branch)) { 2786 $default_branch = null; 2787 } 2788 2789 return array( 2790 'name' => $this->getName(), 2791 'vcs' => $this->getVersionControlSystem(), 2792 'callsign' => $this->getCallsign(), 2793 'shortName' => $this->getRepositorySlug(), 2794 'status' => $this->getStatus(), 2795 'isHosted' => $this->isHosted(), 2796 'isImporting' => (bool)$this->isImporting(), 2797 'almanacServicePHID' => $this->getAlmanacServicePHID(), 2798 'refRules' => array( 2799 'fetchRules' => $fetch_rules, 2800 'trackRules' => $track_rules, 2801 'permanentRefRules' => $permanent_rules, 2802 ), 2803 'defaultBranch' => $default_branch, 2804 'description' => array( 2805 'raw' => (string)$this->getDetail('description'), 2806 ), 2807 ); 2808 } 2809 2810 private function getStringListForConduit($list) { 2811 if (!is_array($list)) { 2812 $list = array(); 2813 } 2814 2815 foreach ($list as $key => $value) { 2816 $value = (string)$value; 2817 if (!strlen($value)) { 2818 unset($list[$key]); 2819 } 2820 } 2821 2822 return array_values($list); 2823 } 2824 2825 public function getConduitSearchAttachments() { 2826 return array( 2827 id(new DiffusionRepositoryURIsSearchEngineAttachment()) 2828 ->setAttachmentKey('uris'), 2829 id(new DiffusionRepositoryMetricsSearchEngineAttachment()) 2830 ->setAttachmentKey('metrics'), 2831 ); 2832 } 2833 2834/* -( PhabricatorFulltextInterface )--------------------------------------- */ 2835 2836 2837 public function newFulltextEngine() { 2838 return new PhabricatorRepositoryFulltextEngine(); 2839 } 2840 2841 2842/* -( PhabricatorFerretInterface )----------------------------------------- */ 2843 2844 2845 public function newFerretEngine() { 2846 return new PhabricatorRepositoryFerretEngine(); 2847 } 2848 2849}