Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / repository / storage / PhabricatorRepository.php
blobd090d5d7a7e8bae7a28098944b787c70fac1ae81
1 <?php
3 /**
4 * @task uri Repository URI Management
5 * @task publishing Publishing
6 * @task sync Cluster Synchronization
7 */
8 final 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 {
22 /**
23 * Shortest hash we'll recognize in raw "a829f32" form.
25 const MINIMUM_UNQUALIFIED_HASH = 7;
27 /**
28 * Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
30 const MINIMUM_QUALIFIED_HASH = 5;
32 /**
33 * Minimum number of commits to an empty repository to trigger "import" mode.
35 const IMPORT_THRESHOLD = 7;
37 const LOWPRI_THRESHOLD = 64;
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';
47 const STATUS_ACTIVE = 'active';
48 const STATUS_INACTIVE = 'inactive';
50 protected $name;
51 protected $callsign;
52 protected $repositorySlug;
53 protected $uuid;
54 protected $viewPolicy;
55 protected $editPolicy;
56 protected $pushPolicy;
57 protected $profileImagePHID;
59 protected $versionControlSystem;
60 protected $details = array();
61 protected $credentialPHID;
62 protected $almanacServicePHID;
63 protected $spacePHID;
64 protected $localPath;
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;
73 public static function initializeNewRepository(PhabricatorUser $actor) {
74 $app = id(new PhabricatorApplicationQuery())
75 ->setViewer($actor)
76 ->withClasses(array('PhabricatorDiffusionApplication'))
77 ->executeOne();
79 $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
80 $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
81 $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
83 $repository = id(new PhabricatorRepository())
84 ->setViewPolicy($view_policy)
85 ->setEditPolicy($edit_policy)
86 ->setPushPolicy($push_policy)
87 ->setSpacePHID($actor->getDefaultSpacePHID());
89 // Put the repository in "Importing" mode until we finish
90 // parsing it.
91 $repository->setDetail('importing', true);
93 return $repository;
96 protected function getConfiguration() {
97 return array(
98 self::CONFIG_AUX_PHID => true,
99 self::CONFIG_SERIALIZATION => array(
100 'details' => self::SERIALIZATION_JSON,
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?',
114 self::CONFIG_KEY_SCHEMA => array(
115 'callsign' => array(
116 'columns' => array('callsign'),
117 'unique' => true,
119 'key_name' => array(
120 'columns' => array('name(128)'),
122 'key_vcs' => array(
123 'columns' => array('versionControlSystem'),
125 'key_slug' => array(
126 'columns' => array('repositorySlug'),
127 'unique' => true,
129 'key_local' => array(
130 'columns' => array('localPath'),
131 'unique' => true,
134 ) + parent::getConfiguration();
137 public function generatePHID() {
138 return PhabricatorPHID::generateNewPHID(
139 PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
142 public static function getStatusMap() {
143 return array(
144 self::STATUS_ACTIVE => array(
145 'name' => pht('Active'),
146 'isTracked' => 1,
148 self::STATUS_INACTIVE => array(
149 'name' => pht('Inactive'),
150 'isTracked' => 0,
155 public static function getStatusNameMap() {
156 return ipull(self::getStatusMap(), 'name');
159 public function getStatus() {
160 if ($this->isTracked()) {
161 return self::STATUS_ACTIVE;
162 } else {
163 return self::STATUS_INACTIVE;
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(),
190 public function getDefaultTextEncoding() {
191 return $this->getDetail('encoding', 'UTF-8');
194 public function getMonogram() {
195 $callsign = $this->getCallsign();
196 if (phutil_nonempty_string($callsign)) {
197 return "r{$callsign}";
200 $id = $this->getID();
201 return "R{$id}";
204 public function getDisplayName() {
205 $slug = $this->getRepositorySlug();
207 if (phutil_nonempty_string($slug)) {
208 return $slug;
211 return $this->getMonogram();
214 public function getAllMonograms() {
215 $monograms = array();
217 $monograms[] = 'R'.$this->getID();
219 $callsign = $this->getCallsign();
220 if (strlen($callsign)) {
221 $monograms[] = 'r'.$callsign;
224 return $monograms;
227 public function setLocalPath($path) {
228 // Convert any extra slashes ("//") in the path to a single slash ("/").
229 $path = preg_replace('(//+)', '/', $path);
231 return parent::setLocalPath($path);
234 public function getDetail($key, $default = null) {
235 return idx($this->details, $key, $default);
238 public function setDetail($key, $value) {
239 $this->details[$key] = $value;
240 return $this;
243 public function attachCommitCount($count) {
244 $this->commitCount = $count;
245 return $this;
248 public function getCommitCount() {
249 return $this->assertAttached($this->commitCount);
252 public function attachMostRecentCommit(
253 PhabricatorRepositoryCommit $commit = null) {
254 $this->mostRecentCommit = $commit;
255 return $this;
258 public function getMostRecentCommit() {
259 return $this->assertAttached($this->mostRecentCommit);
262 public function getDiffusionBrowseURIForPath(
263 PhabricatorUser $user,
264 $path,
265 $line = null,
266 $branch = null) {
268 $drequest = DiffusionRequest::newFromDictionary(
269 array(
270 'user' => $user,
271 'repository' => $this,
272 'path' => $path,
273 'branch' => $branch,
276 return $drequest->generateURI(
277 array(
278 'action' => 'browse',
279 'line' => $line,
283 public function getSubversionBaseURI($commit = null) {
284 $subpath = $this->getDetail('svn-subpath');
286 if (!phutil_nonempty_string($subpath)) {
287 $subpath = null;
290 return $this->getSubversionPathURI($subpath, $commit);
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!'));
299 if ($this->isHosted()) {
300 $uri = 'file://'.$this->getLocalPath();
301 } else {
302 $uri = $this->getDetail('remote-uri');
305 $uri = rtrim($uri, '/');
307 if (phutil_nonempty_string($path)) {
308 $path = rawurlencode($path);
309 $path = str_replace('%2F', '/', $path);
310 $uri = $uri.'/'.ltrim($path, '/');
313 if ($path !== null || $commit !== null) {
314 $uri .= '@';
317 if ($commit !== null) {
318 $uri .= $commit;
321 return $uri;
324 public function attachProjectPHIDs(array $project_phids) {
325 $this->projectPHIDs = $project_phids;
326 return $this;
329 public function getProjectPHIDs() {
330 return $this->assertAttached($this->projectPHIDs);
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/".
341 * @return string
343 public function getCloneName() {
344 $name = $this->getRepositorySlug();
346 // Make some reasonable effort to produce reasonable default directory
347 // names from repository names.
348 if ($name === null || !strlen($name)) {
349 $name = $this->getName();
350 $name = phutil_utf8_strtolower($name);
351 $name = preg_replace('@[ -/:->]+@', '-', $name);
352 $name = trim($name, '-');
353 if (!strlen($name)) {
354 $name = $this->getCallsign();
358 return $name;
361 public static function isValidRepositorySlug($slug) {
362 try {
363 self::assertValidRepositorySlug($slug);
364 return true;
365 } catch (Exception $ex) {
366 return false;
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.'));
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));
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));
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));
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));
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));
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));
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));
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));
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.'));
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));
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));
470 public function getProfileImageURI() {
471 return $this->getProfileImageFile()->getBestURI();
474 public function attachProfileImageFile(PhabricatorFile $file) {
475 $this->profileImageFile = $file;
476 return $this;
479 public function getProfileImageFile() {
480 return $this->assertAttached($this->profileImageFile);
485 /* -( Remote Command Execution )------------------------------------------- */
488 public function execRemoteCommand($pattern /* , $arg, ... */) {
489 $args = func_get_args();
490 return $this->newRemoteCommandFuture($args)->resolve();
493 public function execxRemoteCommand($pattern /* , $arg, ... */) {
494 $args = func_get_args();
495 return $this->newRemoteCommandFuture($args)->resolvex();
498 public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
499 $args = func_get_args();
500 return $this->newRemoteCommandFuture($args);
503 public function passthruRemoteCommand($pattern /* , $arg, ... */) {
504 $args = func_get_args();
505 return $this->newRemoteCommandPassthru($args)->resolve();
508 private function newRemoteCommandFuture(array $argv) {
509 return $this->newRemoteCommandEngine($argv)
510 ->newFuture();
513 private function newRemoteCommandPassthru(array $argv) {
514 return $this->newRemoteCommandEngine($argv)
515 ->setPassthru(true)
516 ->newFuture();
519 private function newRemoteCommandEngine(array $argv) {
520 return DiffusionCommandEngine::newCommandEngine($this)
521 ->setArgv($argv)
522 ->setCredentialPHID($this->getCredentialPHID())
523 ->setURI($this->getRemoteURIObject());
526 /* -( Local Command Execution )-------------------------------------------- */
529 public function execLocalCommand($pattern /* , $arg, ... */) {
530 $args = func_get_args();
531 return $this->newLocalCommandFuture($args)->resolve();
534 public function execxLocalCommand($pattern /* , $arg, ... */) {
535 $args = func_get_args();
536 return $this->newLocalCommandFuture($args)->resolvex();
539 public function getLocalCommandFuture($pattern /* , $arg, ... */) {
540 $args = func_get_args();
541 return $this->newLocalCommandFuture($args);
544 public function passthruLocalCommand($pattern /* , $arg, ... */) {
545 $args = func_get_args();
546 return $this->newLocalCommandPassthru($args)->resolve();
549 private function newLocalCommandFuture(array $argv) {
550 $this->assertLocalExists();
552 $future = DiffusionCommandEngine::newCommandEngine($this)
553 ->setArgv($argv)
554 ->newFuture();
556 if ($this->usesLocalWorkingCopy()) {
557 $future->setCWD($this->getLocalPath());
560 return $future;
563 private function newLocalCommandPassthru(array $argv) {
564 $this->assertLocalExists();
566 $future = DiffusionCommandEngine::newCommandEngine($this)
567 ->setArgv($argv)
568 ->setPassthru(true)
569 ->newFuture();
571 if ($this->usesLocalWorkingCopy()) {
572 $future->setCWD($this->getLocalPath());
575 return $future;
578 public function getURI() {
579 $short_name = $this->getRepositorySlug();
580 if (phutil_nonempty_string($short_name)) {
581 return "/source/{$short_name}/";
584 $callsign = $this->getCallsign();
585 if (phutil_nonempty_string($callsign)) {
586 return "/diffusion/{$callsign}/";
589 $id = $this->getID();
590 return "/diffusion/{$id}/";
593 public function getPathURI($path) {
594 return $this->getURI().ltrim($path, '/');
597 public function getCommitURI($identifier) {
598 $callsign = $this->getCallsign();
599 if (phutil_nonempty_string($callsign)) {
600 return "/r{$callsign}{$identifier}";
603 $id = $this->getID();
604 return "/R{$id}:{$identifier}";
607 public static function parseRepositoryServicePath($request_path, $vcs) {
608 $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
610 $patterns = array(
611 '(^'.
612 '(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'.
613 '(?P<path>.*)'.
614 '\z)',
617 $identifier = null;
618 foreach ($patterns as $pattern) {
619 $matches = null;
620 if (!preg_match($pattern, $request_path, $matches)) {
621 continue;
624 $identifier = $matches['identifier'];
625 if ($is_git) {
626 $identifier = preg_replace('/\\.git\z/', '', $identifier);
629 $base = $matches['base'];
630 $path = $matches['path'];
631 break;
634 if ($identifier === null) {
635 return null;
638 return array(
639 'identifier' => $identifier,
640 'base' => $base,
641 'path' => $path,
645 public function getCanonicalPath($request_path) {
646 $standard_pattern =
647 '(^'.
648 '(?P<prefix>/(?:diffusion|source)/)'.
649 '(?P<identifier>[^/]+)'.
650 '(?P<suffix>(?:/.*)?)'.
651 '\z)';
653 $matches = null;
654 if (preg_match($standard_pattern, $request_path, $matches)) {
655 $suffix = $matches['suffix'];
656 return $this->getPathURI($suffix);
659 $commit_pattern =
660 '(^'.
661 '(?P<prefix>/)'.
662 '(?P<monogram>'.
663 '(?:'.
664 'r(?P<repositoryCallsign>[A-Z]+)'.
665 '|'.
666 'R(?P<repositoryID>[1-9]\d*):'.
667 ')'.
668 '(?P<commit>[a-f0-9]+)'.
669 ')'.
670 '\z)';
672 $matches = null;
673 if (preg_match($commit_pattern, $request_path, $matches)) {
674 $commit = $matches['commit'];
675 return $this->getCommitURI($commit);
678 return null;
681 public function generateURI(array $params) {
682 $req_branch = false;
683 $req_commit = false;
685 $action = idx($params, 'action');
686 switch ($action) {
687 case 'history':
688 case 'clone':
689 case 'blame':
690 case 'browse':
691 case 'document':
692 case 'change':
693 case 'lastmodified':
694 case 'tags':
695 case 'branches':
696 case 'lint':
697 case 'pathtree':
698 case 'refs':
699 case 'compare':
700 break;
701 case 'branch':
702 // NOTE: This does not actually require a branch, and won't have one
703 // in Subversion. Possibly this should be more clear.
704 break;
705 case 'commit':
706 case 'rendering-ref':
707 $req_commit = true;
708 break;
709 default:
710 throw new Exception(
711 pht(
712 'Action "%s" is not a valid repository URI action.',
713 $action));
716 $path = idx($params, 'path');
717 $branch = idx($params, 'branch');
718 $commit = idx($params, 'commit');
719 $line = idx($params, 'line');
721 $head = idx($params, 'head');
722 $against = idx($params, 'against');
724 if ($req_commit && ($commit === null || !strlen($commit))) {
725 throw new Exception(
726 pht(
727 'Diffusion URI action "%s" requires commit!',
728 $action));
731 if ($req_branch && ($branch === null || !strlen($branch))) {
732 throw new Exception(
733 pht(
734 'Diffusion URI action "%s" requires branch!',
735 $action));
738 if ($action === 'commit') {
739 return $this->getCommitURI($commit);
742 if (phutil_nonempty_string($path)) {
743 $path = ltrim($path, '/');
744 $path = str_replace(array(';', '$'), array(';;', '$$'), $path);
745 $path = phutil_escape_uri($path);
748 $raw_branch = $branch;
749 if (phutil_nonempty_string($branch)) {
750 $branch = phutil_escape_uri_path_component($branch);
751 $path = "{$branch}/{$path}";
754 $raw_commit = $commit;
755 if (phutil_nonempty_scalar($commit)) {
756 $commit = str_replace('$', '$$', $commit);
757 $commit = ';'.phutil_escape_uri($commit);
760 $line = phutil_string_cast($line);
761 if (phutil_nonempty_string($line)) {
762 $line = '$'.phutil_escape_uri($line);
765 $query = array();
766 switch ($action) {
767 case 'change':
768 case 'history':
769 case 'blame':
770 case 'browse':
771 case 'document':
772 case 'lastmodified':
773 case 'tags':
774 case 'branches':
775 case 'lint':
776 case 'pathtree':
777 case 'refs':
778 $uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}");
779 break;
780 case 'compare':
781 $uri = $this->getPathURI("/{$action}/");
782 if ($head !== null && strlen($head)) {
783 $query['head'] = $head;
784 } else if ($raw_commit !== null && strlen($raw_commit)) {
785 $query['commit'] = $raw_commit;
786 } else if ($raw_branch !== null && strlen($raw_branch)) {
787 $query['head'] = $raw_branch;
790 if ($against !== null && strlen($against)) {
791 $query['against'] = $against;
793 break;
794 case 'branch':
795 if ($path != null && strlen($path)) {
796 $uri = $this->getPathURI("/repository/{$path}");
797 } else {
798 $uri = $this->getPathURI('/');
800 break;
801 case 'external':
802 $commit = ltrim($commit, ';');
803 $uri = "/diffusion/external/{$commit}/";
804 break;
805 case 'rendering-ref':
806 // This isn't a real URI per se, it's passed as a query parameter to
807 // the ajax changeset stuff but then we parse it back out as though
808 // it came from a URI.
809 $uri = rawurldecode("{$path}{$commit}");
810 break;
811 case 'clone':
812 $uri = $this->getPathURI("/{$action}/");
813 break;
816 if ($action == 'rendering-ref') {
817 return $uri;
820 if (isset($params['lint'])) {
821 $params['params'] = idx($params, 'params', array()) + array(
822 'lint' => $params['lint'],
826 $query = idx($params, 'params', array()) + $query;
828 return new PhutilURI($uri, $query);
831 public function updateURIIndex() {
832 $indexes = array();
834 $uris = $this->getURIs();
835 foreach ($uris as $uri) {
836 if ($uri->getIsDisabled()) {
837 continue;
840 $indexes[] = $uri->getNormalizedURI();
843 PhabricatorRepositoryURIIndex::updateRepositoryURIs(
844 $this->getPHID(),
845 $indexes);
847 return $this;
850 public function isTracked() {
851 $status = $this->getDetail('tracking-enabled');
852 $map = self::getStatusMap();
853 $spec = idx($map, $status);
855 if (!$spec) {
856 if ($status) {
857 $status = self::STATUS_ACTIVE;
858 } else {
859 $status = self::STATUS_INACTIVE;
861 $spec = idx($map, $status);
864 return (bool)idx($spec, 'isTracked', false);
867 public function getDefaultBranch() {
868 $default = $this->getDetail('default-branch');
869 if (phutil_nonempty_string($default)) {
870 return $default;
873 $default_branches = array(
874 PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
875 PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
878 return idx($default_branches, $this->getVersionControlSystem());
881 public function getDefaultArcanistBranch() {
882 return coalesce($this->getDefaultBranch(), 'svn');
885 private function isBranchInFilter($branch, $filter_key) {
886 $vcs = $this->getVersionControlSystem();
888 $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
890 $use_filter = ($is_git);
891 if (!$use_filter) {
892 // If this VCS doesn't use filters, pass everything through.
893 return true;
897 $filter = $this->getDetail($filter_key, array());
899 // If there's no filter set, let everything through.
900 if (!$filter) {
901 return true;
904 // If this branch isn't literally named `regexp(...)`, and it's in the
905 // filter list, let it through.
906 if (isset($filter[$branch])) {
907 if (self::extractBranchRegexp($branch) === null) {
908 return true;
912 // If the branch matches a regexp, let it through.
913 foreach ($filter as $pattern => $ignored) {
914 $regexp = self::extractBranchRegexp($pattern);
915 if ($regexp !== null) {
916 if (preg_match($regexp, $branch)) {
917 return true;
922 // Nothing matched, so filter this branch out.
923 return false;
926 public static function extractBranchRegexp($pattern) {
927 $matches = null;
928 if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
929 return $matches[1];
931 return null;
934 public function shouldTrackRef(DiffusionRepositoryRef $ref) {
935 // At least for now, don't track the staging area tags.
936 if ($ref->isTag()) {
937 if (preg_match('(^phabricator/)', $ref->getShortName())) {
938 return false;
942 if (!$ref->isBranch()) {
943 return true;
946 return $this->shouldTrackBranch($ref->getShortName());
949 public function shouldTrackBranch($branch) {
950 return $this->isBranchInFilter($branch, 'branch-filter');
953 public function isBranchPermanentRef($branch) {
954 return $this->isBranchInFilter($branch, 'close-commits-filter');
957 public function formatCommitName($commit_identifier, $local = false) {
958 $vcs = $this->getVersionControlSystem();
960 $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
961 $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
963 $is_git = ($vcs == $type_git);
964 $is_hg = ($vcs == $type_hg);
965 if ($is_git || $is_hg) {
966 $name = substr($commit_identifier, 0, 12);
967 $need_scope = false;
968 } else {
969 $name = $commit_identifier;
970 $need_scope = true;
973 if (!$local) {
974 $need_scope = true;
977 if ($need_scope) {
978 $callsign = $this->getCallsign();
979 if ($callsign) {
980 $scope = "r{$callsign}";
981 } else {
982 $id = $this->getID();
983 $scope = "R{$id}:";
985 $name = $scope.$name;
988 return $name;
991 public function isImporting() {
992 return (bool)$this->getDetail('importing', false);
995 public function isNewlyInitialized() {
996 return (bool)$this->getDetail('newly-initialized', false);
999 public function loadImportProgress() {
1000 $progress = queryfx_all(
1001 $this->establishConnection('r'),
1002 'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
1003 GROUP BY importStatus',
1004 id(new PhabricatorRepositoryCommit())->getTableName(),
1005 $this->getID());
1007 $done = 0;
1008 $total = 0;
1009 foreach ($progress as $row) {
1010 $total += $row['N'] * 3;
1011 $status = $row['importStatus'];
1012 if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) {
1013 $done += $row['N'];
1015 if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) {
1016 $done += $row['N'];
1018 if ($status & PhabricatorRepositoryCommit::IMPORTED_PUBLISH) {
1019 $done += $row['N'];
1023 if ($total) {
1024 $ratio = ($done / $total);
1025 } else {
1026 $ratio = 0;
1029 // Cap this at "99.99%", because it's confusing to users when the actual
1030 // fraction is "99.996%" and it rounds up to "100.00%".
1031 if ($ratio > 0.9999) {
1032 $ratio = 0.9999;
1035 return $ratio;
1038 /* -( Publishing )--------------------------------------------------------- */
1040 public function newPublisher() {
1041 return id(new PhabricatorRepositoryPublisher())
1042 ->setRepository($this);
1045 public function isPublishingDisabled() {
1046 return $this->getDetail('herald-disabled');
1049 public function getPermanentRefRules() {
1050 return array_keys($this->getDetail('close-commits-filter', array()));
1053 public function setPermanentRefRules(array $rules) {
1054 $rules = array_fill_keys($rules, true);
1055 $this->setDetail('close-commits-filter', $rules);
1056 return $this;
1059 public function getTrackOnlyRules() {
1060 return array_keys($this->getDetail('branch-filter', array()));
1063 public function setTrackOnlyRules(array $rules) {
1064 $rules = array_fill_keys($rules, true);
1065 $this->setDetail('branch-filter', $rules);
1066 return $this;
1069 public function supportsFetchRules() {
1070 if ($this->isGit()) {
1071 return true;
1074 return false;
1077 public function getFetchRules() {
1078 return $this->getDetail('fetch-rules', array());
1081 public function setFetchRules(array $rules) {
1082 return $this->setDetail('fetch-rules', $rules);
1086 /* -( Repository URI Management )------------------------------------------ */
1090 * Get the remote URI for this repository.
1092 * @return string
1093 * @task uri
1095 public function getRemoteURI() {
1096 return (string)$this->getRemoteURIObject();
1101 * Get the remote URI for this repository, including credentials if they're
1102 * used by this repository.
1104 * @return PhutilOpaqueEnvelope URI, possibly including credentials.
1105 * @task uri
1107 public function getRemoteURIEnvelope() {
1108 $uri = $this->getRemoteURIObject();
1110 $remote_protocol = $this->getRemoteProtocol();
1111 if ($remote_protocol == 'http' || $remote_protocol == 'https') {
1112 // For SVN, we use `--username` and `--password` flags separately, so
1113 // don't add any credentials here.
1114 if (!$this->isSVN()) {
1115 $credential_phid = $this->getCredentialPHID();
1116 if ($credential_phid) {
1117 $key = PassphrasePasswordKey::loadFromPHID(
1118 $credential_phid,
1119 PhabricatorUser::getOmnipotentUser());
1121 $uri->setUser($key->getUsernameEnvelope()->openEnvelope());
1122 $uri->setPass($key->getPasswordEnvelope()->openEnvelope());
1127 return new PhutilOpaqueEnvelope((string)$uri);
1132 * Get the clone (or checkout) URI for this repository, without authentication
1133 * information.
1135 * @return string Repository URI.
1136 * @task uri
1138 public function getPublicCloneURI() {
1139 return (string)$this->getCloneURIObject();
1144 * Get the protocol for the repository's remote.
1146 * @return string Protocol, like "ssh" or "git".
1147 * @task uri
1149 public function getRemoteProtocol() {
1150 $uri = $this->getRemoteURIObject();
1151 return $uri->getProtocol();
1156 * Get a parsed object representation of the repository's remote URI..
1158 * @return wild A @{class@arcanist:PhutilURI}.
1159 * @task uri
1161 public function getRemoteURIObject() {
1162 $raw_uri = $this->getDetail('remote-uri');
1163 if ($raw_uri === null || !strlen($raw_uri)) {
1164 return new PhutilURI('');
1167 if (!strncmp($raw_uri, '/', 1)) {
1168 return new PhutilURI('file://'.$raw_uri);
1171 return new PhutilURI($raw_uri);
1176 * Get the "best" clone/checkout URI for this repository, on any protocol.
1178 public function getCloneURIObject() {
1179 if (!$this->isHosted()) {
1180 if ($this->isSVN()) {
1181 // Make sure we pick up the "Import Only" path for Subversion, so
1182 // the user clones the repository starting at the correct path, not
1183 // from the root.
1184 $base_uri = $this->getSubversionBaseURI();
1185 $base_uri = new PhutilURI($base_uri);
1186 $path = $base_uri->getPath();
1187 if (!$path) {
1188 $path = '/';
1191 // If the trailing "@" is not required to escape the URI, strip it for
1192 // readability.
1193 if (!preg_match('/@.*@/', $path)) {
1194 $path = rtrim($path, '@');
1197 $base_uri->setPath($path);
1198 return $base_uri;
1199 } else {
1200 return $this->getRemoteURIObject();
1204 // TODO: This should be cleaned up to deal with all the new URI handling.
1205 $another_copy = id(new PhabricatorRepositoryQuery())
1206 ->setViewer(PhabricatorUser::getOmnipotentUser())
1207 ->withPHIDs(array($this->getPHID()))
1208 ->needURIs(true)
1209 ->executeOne();
1211 $clone_uris = $another_copy->getCloneURIs();
1212 if (!$clone_uris) {
1213 return null;
1216 return head($clone_uris)->getEffectiveURI();
1219 private function getRawHTTPCloneURIObject() {
1220 $uri = PhabricatorEnv::getProductionURI($this->getURI());
1221 $uri = new PhutilURI($uri);
1223 if ($this->isGit()) {
1224 $uri->setPath($uri->getPath().$this->getCloneName().'.git');
1225 } else if ($this->isHg()) {
1226 $uri->setPath($uri->getPath().$this->getCloneName().'/');
1229 return $uri;
1234 * Determine if we should connect to the remote using SSH flags and
1235 * credentials.
1237 * @return bool True to use the SSH protocol.
1238 * @task uri
1240 private function shouldUseSSH() {
1241 if ($this->isHosted()) {
1242 return false;
1245 $protocol = $this->getRemoteProtocol();
1246 if ($this->isSSHProtocol($protocol)) {
1247 return true;
1250 return false;
1255 * Determine if we should connect to the remote using HTTP flags and
1256 * credentials.
1258 * @return bool True to use the HTTP protocol.
1259 * @task uri
1261 private function shouldUseHTTP() {
1262 if ($this->isHosted()) {
1263 return false;
1266 $protocol = $this->getRemoteProtocol();
1267 return ($protocol == 'http' || $protocol == 'https');
1272 * Determine if we should connect to the remote using SVN flags and
1273 * credentials.
1275 * @return bool True to use the SVN protocol.
1276 * @task uri
1278 private function shouldUseSVNProtocol() {
1279 if ($this->isHosted()) {
1280 return false;
1283 $protocol = $this->getRemoteProtocol();
1284 return ($protocol == 'svn');
1289 * Determine if a protocol is SSH or SSH-like.
1291 * @param string A protocol string, like "http" or "ssh".
1292 * @return bool True if the protocol is SSH-like.
1293 * @task uri
1295 private function isSSHProtocol($protocol) {
1296 return ($protocol == 'ssh' || $protocol == 'svn+ssh');
1299 public function delete() {
1300 $this->openTransaction();
1302 $paths = id(new PhabricatorOwnersPath())
1303 ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
1304 foreach ($paths as $path) {
1305 $path->delete();
1308 queryfx(
1309 $this->establishConnection('w'),
1310 'DELETE FROM %T WHERE repositoryPHID = %s',
1311 id(new PhabricatorRepositorySymbol())->getTableName(),
1312 $this->getPHID());
1314 $commits = id(new PhabricatorRepositoryCommit())
1315 ->loadAllWhere('repositoryID = %d', $this->getID());
1316 foreach ($commits as $commit) {
1317 // note PhabricatorRepositoryAuditRequests and
1318 // PhabricatorRepositoryCommitData are deleted here too.
1319 $commit->delete();
1322 $uris = id(new PhabricatorRepositoryURI())
1323 ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
1324 foreach ($uris as $uri) {
1325 $uri->delete();
1328 $ref_cursors = id(new PhabricatorRepositoryRefCursor())
1329 ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
1330 foreach ($ref_cursors as $cursor) {
1331 $cursor->delete();
1334 $conn_w = $this->establishConnection('w');
1336 queryfx(
1337 $conn_w,
1338 'DELETE FROM %T WHERE repositoryID = %d',
1339 self::TABLE_FILESYSTEM,
1340 $this->getID());
1342 queryfx(
1343 $conn_w,
1344 'DELETE FROM %T WHERE repositoryID = %d',
1345 self::TABLE_PATHCHANGE,
1346 $this->getID());
1348 queryfx(
1349 $conn_w,
1350 'DELETE FROM %T WHERE repositoryID = %d',
1351 self::TABLE_SUMMARY,
1352 $this->getID());
1354 $result = parent::delete();
1356 $this->saveTransaction();
1357 return $result;
1360 public function isGit() {
1361 $vcs = $this->getVersionControlSystem();
1362 return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
1365 public function isSVN() {
1366 $vcs = $this->getVersionControlSystem();
1367 return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
1370 public function isHg() {
1371 $vcs = $this->getVersionControlSystem();
1372 return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
1375 public function isHosted() {
1376 return (bool)$this->getDetail('hosting-enabled', false);
1379 public function setHosted($enabled) {
1380 return $this->setDetail('hosting-enabled', $enabled);
1383 public function canServeProtocol(
1384 $protocol,
1385 $write,
1386 $is_intracluster = false) {
1388 // See T13192. If a repository is inactive, don't serve it to users. We
1389 // still synchronize it within the cluster and serve it to other repository
1390 // nodes.
1391 if (!$is_intracluster) {
1392 if (!$this->isTracked()) {
1393 return false;
1397 $clone_uris = $this->getCloneURIs();
1398 foreach ($clone_uris as $uri) {
1399 if ($uri->getBuiltinProtocol() !== $protocol) {
1400 continue;
1403 $io_type = $uri->getEffectiveIoType();
1404 if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
1405 return true;
1408 if (!$write) {
1409 if ($io_type == PhabricatorRepositoryURI::IO_READ) {
1410 return true;
1415 if ($write) {
1416 if ($this->isReadOnly()) {
1417 return false;
1421 return false;
1424 public function hasLocalWorkingCopy() {
1425 try {
1426 self::assertLocalExists();
1427 return true;
1428 } catch (Exception $ex) {
1429 return false;
1434 * Raise more useful errors when there are basic filesystem problems.
1436 private function assertLocalExists() {
1437 if (!$this->usesLocalWorkingCopy()) {
1438 return;
1441 $local = $this->getLocalPath();
1442 Filesystem::assertExists($local);
1443 Filesystem::assertIsDirectory($local);
1444 Filesystem::assertReadable($local);
1448 * Determine if the working copy is bare or not. In Git, this corresponds
1449 * to `--bare`. In Mercurial, `--noupdate`.
1451 public function isWorkingCopyBare() {
1452 switch ($this->getVersionControlSystem()) {
1453 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1454 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1455 return false;
1456 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1457 $local = $this->getLocalPath();
1458 if (Filesystem::pathExists($local.'/.git')) {
1459 return false;
1460 } else {
1461 return true;
1466 public function usesLocalWorkingCopy() {
1467 switch ($this->getVersionControlSystem()) {
1468 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1469 return $this->isHosted();
1470 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1471 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1472 return true;
1476 public function getHookDirectories() {
1477 $directories = array();
1478 if (!$this->isHosted()) {
1479 return $directories;
1482 $root = $this->getLocalPath();
1484 switch ($this->getVersionControlSystem()) {
1485 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1486 if ($this->isWorkingCopyBare()) {
1487 $directories[] = $root.'/hooks/pre-receive-phabricator.d/';
1488 } else {
1489 $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
1491 break;
1492 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1493 $directories[] = $root.'/hooks/pre-commit-phabricator.d/';
1494 break;
1495 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1496 // NOTE: We don't support custom Mercurial hooks for now because they're
1497 // messy and we can't easily just drop a `hooks.d/` directory next to
1498 // the hooks.
1499 break;
1502 return $directories;
1505 public function canDestroyWorkingCopy() {
1506 if ($this->isHosted()) {
1507 // Never destroy hosted working copies.
1508 return false;
1511 $default_path = PhabricatorEnv::getEnvConfig(
1512 'repository.default-local-path');
1513 return Filesystem::isDescendant($this->getLocalPath(), $default_path);
1516 public function canUsePathTree() {
1517 return !$this->isSVN();
1520 public function canUseGitLFS() {
1521 if (!$this->isGit()) {
1522 return false;
1525 if (!$this->isHosted()) {
1526 return false;
1529 if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) {
1530 return false;
1533 return true;
1536 public function getGitLFSURI($path = null) {
1537 if (!$this->canUseGitLFS()) {
1538 throw new Exception(
1539 pht(
1540 'This repository does not support Git LFS, so Git LFS URIs can '.
1541 'not be generated for it.'));
1544 $uri = $this->getRawHTTPCloneURIObject();
1545 $uri = (string)$uri;
1546 $uri = $uri.'/'.$path;
1548 return $uri;
1551 public function canMirror() {
1552 if ($this->isGit() || $this->isHg()) {
1553 return true;
1556 return false;
1559 public function canAllowDangerousChanges() {
1560 if (!$this->isHosted()) {
1561 return false;
1564 // In Git and Mercurial, ref deletions and rewrites are dangerous.
1565 // In Subversion, editing revprops is dangerous.
1567 return true;
1570 public function shouldAllowDangerousChanges() {
1571 return (bool)$this->getDetail('allow-dangerous-changes');
1574 public function canAllowEnormousChanges() {
1575 if (!$this->isHosted()) {
1576 return false;
1579 return true;
1582 public function shouldAllowEnormousChanges() {
1583 return (bool)$this->getDetail('allow-enormous-changes');
1586 public function writeStatusMessage(
1587 $status_type,
1588 $status_code,
1589 array $parameters = array()) {
1591 $table = new PhabricatorRepositoryStatusMessage();
1592 $conn_w = $table->establishConnection('w');
1593 $table_name = $table->getTableName();
1595 if ($status_code === null) {
1596 queryfx(
1597 $conn_w,
1598 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
1599 $table_name,
1600 $this->getID(),
1601 $status_type);
1602 } else {
1603 // If the existing message has the same code (e.g., we just hit an
1604 // error and also previously hit an error) we increment the message
1605 // count. This allows us to determine how many times in a row we've
1606 // run into an error.
1608 // NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated
1609 // in order, so the "messageCount" assignment must occur before the
1610 // "statusCode" assignment. See T11705.
1612 queryfx(
1613 $conn_w,
1614 'INSERT INTO %T
1615 (repositoryID, statusType, statusCode, parameters, epoch,
1616 messageCount)
1617 VALUES (%d, %s, %s, %s, %d, %d)
1618 ON DUPLICATE KEY UPDATE
1619 messageCount =
1621 statusCode = VALUES(statusCode),
1622 messageCount + VALUES(messageCount),
1623 VALUES(messageCount)),
1624 statusCode = VALUES(statusCode),
1625 parameters = VALUES(parameters),
1626 epoch = VALUES(epoch)',
1627 $table_name,
1628 $this->getID(),
1629 $status_type,
1630 $status_code,
1631 json_encode($parameters),
1632 time(),
1636 return $this;
1639 public static function assertValidRemoteURI($uri) {
1640 if (trim($uri) != $uri) {
1641 throw new Exception(
1642 pht('The remote URI has leading or trailing whitespace.'));
1645 $uri_object = new PhutilURI($uri);
1646 $protocol = $uri_object->getProtocol();
1648 // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
1649 // for discussion. This is usually a user adding "ssh://" to an implicit
1650 // SSH Git URI.
1651 if ($protocol == 'ssh') {
1652 if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
1653 throw new Exception(
1654 pht(
1655 "The remote URI is not formatted correctly. Remote URIs ".
1656 "with an explicit protocol should be in the form ".
1657 "'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.",
1658 'proto://domain/path',
1659 'proto://domain:/path',
1660 ':/path'));
1664 switch ($protocol) {
1665 case 'ssh':
1666 case 'http':
1667 case 'https':
1668 case 'git':
1669 case 'svn':
1670 case 'svn+ssh':
1671 break;
1672 default:
1673 // NOTE: We're explicitly rejecting 'file://' because it can be
1674 // used to clone from the working copy of another repository on disk
1675 // that you don't normally have permission to access.
1677 throw new Exception(
1678 pht(
1679 'The URI protocol is unrecognized. It should begin with '.
1680 '"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".',
1681 'ssh://',
1682 'http://',
1683 'https://',
1684 'git://',
1685 'svn://',
1686 'svn+ssh://',
1687 'git@domain.com:path'));
1690 return true;
1695 * Load the pull frequency for this repository, based on the time since the
1696 * last activity.
1698 * We pull rarely used repositories less frequently. This finds the most
1699 * recent commit which is older than the current time (which prevents us from
1700 * spinning on repositories with a silly commit post-dated to some time in
1701 * 2037). We adjust the pull frequency based on when the most recent commit
1702 * occurred.
1704 * @param int The minimum update interval to use, in seconds.
1705 * @return int Repository update interval, in seconds.
1707 public function loadUpdateInterval($minimum = 15) {
1708 // First, check if we've hit errors recently. If we have, wait one period
1709 // for each consecutive error. Normally, this corresponds to a backoff of
1710 // 15s, 30s, 45s, etc.
1712 $message_table = new PhabricatorRepositoryStatusMessage();
1713 $conn = $message_table->establishConnection('r');
1714 $error_count = queryfx_one(
1715 $conn,
1716 'SELECT MAX(messageCount) error_count FROM %T
1717 WHERE repositoryID = %d
1718 AND statusType IN (%Ls)
1719 AND statusCode IN (%Ls)',
1720 $message_table->getTableName(),
1721 $this->getID(),
1722 array(
1723 PhabricatorRepositoryStatusMessage::TYPE_INIT,
1724 PhabricatorRepositoryStatusMessage::TYPE_FETCH,
1726 array(
1727 PhabricatorRepositoryStatusMessage::CODE_ERROR,
1730 $error_count = (int)$error_count['error_count'];
1731 if ($error_count > 0) {
1732 return (int)($minimum * $error_count);
1735 // If a repository is still importing, always pull it as frequently as
1736 // possible. This prevents us from hanging for a long time at 99.9% when
1737 // importing an inactive repository.
1738 if ($this->isImporting()) {
1739 return $minimum;
1742 $window_start = (PhabricatorTime::getNow() + $minimum);
1744 $table = id(new PhabricatorRepositoryCommit());
1745 $last_commit = queryfx_one(
1746 $table->establishConnection('r'),
1747 'SELECT epoch FROM %T
1748 WHERE repositoryID = %d AND epoch <= %d
1749 ORDER BY epoch DESC LIMIT 1',
1750 $table->getTableName(),
1751 $this->getID(),
1752 $window_start);
1753 if ($last_commit) {
1754 $time_since_commit = ($window_start - $last_commit['epoch']);
1755 } else {
1756 // If the repository has no commits, treat the creation date as
1757 // though it were the date of the last commit. This makes empty
1758 // repositories update quickly at first but slow down over time
1759 // if they don't see any activity.
1760 $time_since_commit = ($window_start - $this->getDateCreated());
1763 $last_few_days = phutil_units('3 days in seconds');
1765 if ($time_since_commit <= $last_few_days) {
1766 // For repositories with activity in the recent past, we wait one
1767 // extra second for every 10 minutes since the last commit. This
1768 // shorter backoff is intended to handle weekends and other short
1769 // breaks from development.
1770 $smart_wait = ($time_since_commit / 600);
1771 } else {
1772 // For repositories without recent activity, we wait one extra second
1773 // for every 4 minutes since the last commit. This longer backoff
1774 // handles rarely used repositories, up to the maximum.
1775 $smart_wait = ($time_since_commit / 240);
1778 // We'll never wait more than 6 hours to pull a repository.
1779 $longest_wait = phutil_units('6 hours in seconds');
1780 $smart_wait = min($smart_wait, $longest_wait);
1781 $smart_wait = max($minimum, $smart_wait);
1783 return (int)$smart_wait;
1788 * Time limit for cloning or copying this repository.
1790 * This limit is used to timeout operations like `git clone` or `git fetch`
1791 * when doing intracluster synchronization, building working copies, etc.
1793 * @return int Maximum number of seconds to spend copying this repository.
1795 public function getCopyTimeLimit() {
1796 return $this->getDetail('limit.copy');
1799 public function setCopyTimeLimit($limit) {
1800 return $this->setDetail('limit.copy', $limit);
1803 public function getDefaultCopyTimeLimit() {
1804 return phutil_units('15 minutes in seconds');
1807 public function getEffectiveCopyTimeLimit() {
1808 $limit = $this->getCopyTimeLimit();
1809 if ($limit) {
1810 return $limit;
1813 return $this->getDefaultCopyTimeLimit();
1816 public function getFilesizeLimit() {
1817 return $this->getDetail('limit.filesize');
1820 public function setFilesizeLimit($limit) {
1821 return $this->setDetail('limit.filesize', $limit);
1824 public function getTouchLimit() {
1825 return $this->getDetail('limit.touch');
1828 public function setTouchLimit($limit) {
1829 return $this->setDetail('limit.touch', $limit);
1833 * Retrieve the service URI for the device hosting this repository.
1835 * See @{method:newConduitClient} for a general discussion of interacting
1836 * with repository services. This method provides lower-level resolution of
1837 * services, returning raw URIs.
1839 * @param PhabricatorUser Viewing user.
1840 * @param map<string, wild> Constraints on selectable services.
1841 * @return string|null URI, or `null` for local repositories.
1843 public function getAlmanacServiceURI(
1844 PhabricatorUser $viewer,
1845 array $options) {
1847 $refs = $this->getAlmanacServiceRefs($viewer, $options);
1849 if (!$refs) {
1850 return null;
1853 $ref = head($refs);
1854 return $ref->getURI();
1857 public function getAlmanacServiceRefs(
1858 PhabricatorUser $viewer,
1859 array $options) {
1861 PhutilTypeSpec::checkMap(
1862 $options,
1863 array(
1864 'neverProxy' => 'bool',
1865 'protocols' => 'list<string>',
1866 'writable' => 'optional bool',
1869 $never_proxy = $options['neverProxy'];
1870 $protocols = $options['protocols'];
1871 $writable = idx($options, 'writable', false);
1873 $cache_key = $this->getAlmanacServiceCacheKey();
1874 if (!$cache_key) {
1875 return array();
1878 $cache = PhabricatorCaches::getMutableStructureCache();
1879 $uris = $cache->getKey($cache_key, false);
1881 // If we haven't built the cache yet, build it now.
1882 if ($uris === false) {
1883 $uris = $this->buildAlmanacServiceURIs();
1884 $cache->setKey($cache_key, $uris);
1887 if ($uris === null) {
1888 return array();
1891 $local_device = AlmanacKeys::getDeviceID();
1892 if ($never_proxy && !$local_device) {
1893 throw new Exception(
1894 pht(
1895 'Unable to handle proxied service request. This device is not '.
1896 'registered, so it can not identify local services. Register '.
1897 'this device before sending requests here.'));
1900 $protocol_map = array_fuse($protocols);
1902 $results = array();
1903 foreach ($uris as $uri) {
1904 // If we're never proxying this and it's locally satisfiable, return
1905 // `null` to tell the caller to handle it locally. If we're allowed to
1906 // proxy, we skip this check and may proxy the request to ourselves.
1907 // (That proxied request will end up here with proxying forbidden,
1908 // return `null`, and then the request will actually run.)
1910 if ($local_device && $never_proxy) {
1911 if ($uri['device'] == $local_device) {
1912 return array();
1916 if (isset($protocol_map[$uri['protocol']])) {
1917 $results[] = $uri;
1921 if (!$results) {
1922 throw new Exception(
1923 pht(
1924 'The Almanac service for this repository is not bound to any '.
1925 'interfaces which support the required protocols (%s).',
1926 implode(', ', $protocols)));
1929 if ($never_proxy) {
1930 // See PHI1030. This error can arise from various device name/address
1931 // mismatches which are hard to detect, so try to provide as much
1932 // information as we can.
1934 if ($writable) {
1935 $request_type = pht('(This is a write request.)');
1936 } else {
1937 $request_type = pht('(This is a read request.)');
1940 throw new Exception(
1941 pht(
1942 'This repository request (for repository "%s") has been '.
1943 'incorrectly routed to a cluster host (with device name "%s", '.
1944 'and hostname "%s") which can not serve the request.'.
1945 "\n\n".
1946 'The Almanac device address for the correct device may improperly '.
1947 'point at this host, or the "device.id" configuration file on '.
1948 'this host may be incorrect.'.
1949 "\n\n".
1950 'Requests routed within the cluster are always '.
1951 'expected to be sent to a node which can serve the request. To '.
1952 'prevent loops, this request will not be proxied again.'.
1953 "\n\n".
1954 "%s",
1955 $this->getDisplayName(),
1956 $local_device,
1957 php_uname('n'),
1958 $request_type));
1961 if (count($results) > 1) {
1962 if (!$this->supportsSynchronization()) {
1963 throw new Exception(
1964 pht(
1965 'Repository "%s" is bound to multiple active repository hosts, '.
1966 'but this repository does not support cluster synchronization. '.
1967 'Declusterize this repository or move it to a service with only '.
1968 'one host.',
1969 $this->getDisplayName()));
1973 $refs = array();
1974 foreach ($results as $result) {
1975 $refs[] = DiffusionServiceRef::newFromDictionary($result);
1978 // If we require a writable device, remove URIs which aren't writable.
1979 if ($writable) {
1980 foreach ($refs as $key => $ref) {
1981 if (!$ref->isWritable()) {
1982 unset($refs[$key]);
1986 if (!$refs) {
1987 throw new Exception(
1988 pht(
1989 'This repository ("%s") is not writable with the given '.
1990 'protocols (%s). The Almanac service for this repository has no '.
1991 'writable bindings that support these protocols.',
1992 $this->getDisplayName(),
1993 implode(', ', $protocols)));
1997 if ($writable) {
1998 $refs = $this->sortWritableAlmanacServiceRefs($refs);
1999 } else {
2000 $refs = $this->sortReadableAlmanacServiceRefs($refs);
2003 return array_values($refs);
2006 private function sortReadableAlmanacServiceRefs(array $refs) {
2007 assert_instances_of($refs, 'DiffusionServiceRef');
2008 shuffle($refs);
2009 return $refs;
2012 private function sortWritableAlmanacServiceRefs(array $refs) {
2013 assert_instances_of($refs, 'DiffusionServiceRef');
2015 // See T13109 for discussion of how this method routes requests.
2017 // In the absence of other rules, we'll send traffic to devices randomly.
2018 // We also want to select randomly among nodes which are equally good
2019 // candidates to receive the write, and accomplish that by shuffling the
2020 // list up front.
2021 shuffle($refs);
2023 $order = array();
2025 // If some device is currently holding the write lock, send all requests
2026 // to that device. We're trying to queue writes on a single device so they
2027 // do not need to wait for read synchronization after earlier writes
2028 // complete.
2029 $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter(
2030 $this->getPHID());
2031 if ($writer) {
2032 $device_phid = $writer->getWriteProperty('devicePHID');
2033 foreach ($refs as $key => $ref) {
2034 if ($ref->getDevicePHID() === $device_phid) {
2035 $order[] = $key;
2040 // If no device is currently holding the write lock, try to send requests
2041 // to a device which is already up to date and will not need to synchronize
2042 // before it can accept the write.
2043 $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
2044 $this->getPHID());
2045 if ($versions) {
2046 $max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
2048 $max_devices = array();
2049 foreach ($versions as $version) {
2050 if ($version->getRepositoryVersion() == $max_version) {
2051 $max_devices[] = $version->getDevicePHID();
2054 $max_devices = array_fuse($max_devices);
2056 foreach ($refs as $key => $ref) {
2057 if (isset($max_devices[$ref->getDevicePHID()])) {
2058 $order[] = $key;
2063 // Reorder the results, putting any we've selected as preferred targets for
2064 // the write at the head of the list.
2065 $refs = array_select_keys($refs, $order) + $refs;
2067 return $refs;
2070 public function supportsSynchronization() {
2071 // TODO: For now, this is only supported for Git.
2072 if (!$this->isGit()) {
2073 return false;
2076 return true;
2080 public function supportsRefs() {
2081 if ($this->isSVN()) {
2082 return false;
2085 return true;
2088 public function getAlmanacServiceCacheKey() {
2089 $service_phid = $this->getAlmanacServicePHID();
2090 if (!$service_phid) {
2091 return null;
2094 $repository_phid = $this->getPHID();
2096 $parts = array(
2097 "repo({$repository_phid})",
2098 "serv({$service_phid})",
2099 'v4',
2102 return implode('.', $parts);
2105 private function buildAlmanacServiceURIs() {
2106 $service = $this->loadAlmanacService();
2107 if (!$service) {
2108 return null;
2111 $bindings = $service->getActiveBindings();
2112 if (!$bindings) {
2113 throw new Exception(
2114 pht(
2115 'The Almanac service for this repository is not bound to any '.
2116 'active interfaces.'));
2119 $uris = array();
2120 foreach ($bindings as $binding) {
2121 $iface = $binding->getInterface();
2123 $uri = $this->getClusterRepositoryURIFromBinding($binding);
2124 $protocol = $uri->getProtocol();
2125 $device_name = $iface->getDevice()->getName();
2126 $device_phid = $iface->getDevice()->getPHID();
2128 $uris[] = array(
2129 'protocol' => $protocol,
2130 'uri' => (string)$uri,
2131 'device' => $device_name,
2132 'writable' => (bool)$binding->getAlmanacPropertyValue('writable'),
2133 'devicePHID' => $device_phid,
2137 return $uris;
2141 * Build a new Conduit client in order to make a service call to this
2142 * repository.
2144 * If the repository is hosted locally, this method may return `null`. The
2145 * caller should use `ConduitCall` or other local logic to complete the
2146 * request.
2148 * By default, we will return a @{class:ConduitClient} for any repository with
2149 * a service, even if that service is on the current device.
2151 * We do this because this configuration does not make very much sense in a
2152 * production context, but is very common in a test/development context
2153 * (where the developer's machine is both the web host and the repository
2154 * service). By proxying in development, we get more consistent behavior
2155 * between development and production, and don't have a major untested
2156 * codepath.
2158 * The `$never_proxy` parameter can be used to prevent this local proxying.
2159 * If the flag is passed:
2161 * - The method will return `null` (implying a local service call)
2162 * if the repository service is hosted on the current device.
2163 * - The method will throw if it would need to return a client.
2165 * This is used to prevent loops in Conduit: the first request will proxy,
2166 * even in development, but the second request will be identified as a
2167 * cluster request and forced not to proxy.
2169 * For lower-level service resolution, see @{method:getAlmanacServiceURI}.
2171 * @param PhabricatorUser Viewing user.
2172 * @param bool `true` to throw if a client would be returned.
2173 * @return ConduitClient|null Client, or `null` for local repositories.
2175 public function newConduitClient(
2176 PhabricatorUser $viewer,
2177 $never_proxy = false) {
2179 $uri = $this->getAlmanacServiceURI(
2180 $viewer,
2181 array(
2182 'neverProxy' => $never_proxy,
2183 'protocols' => array(
2184 'http',
2185 'https',
2188 // At least today, no Conduit call can ever write to a repository,
2189 // so it's fine to send anything to a read-only node.
2190 'writable' => false,
2192 if ($uri === null) {
2193 return null;
2196 $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain();
2198 $client = id(new ConduitClient($uri))
2199 ->setHost($domain);
2201 if ($viewer->isOmnipotent()) {
2202 // If the caller is the omnipotent user (normally, a daemon), we will
2203 // sign the request with this host's asymmetric keypair.
2205 $public_path = AlmanacKeys::getKeyPath('device.pub');
2206 try {
2207 $public_key = Filesystem::readFile($public_path);
2208 } catch (Exception $ex) {
2209 throw new PhutilAggregateException(
2210 pht(
2211 'Unable to read device public key while attempting to make '.
2212 'authenticated method call within the cluster. '.
2213 'Use `%s` to register keys for this device. Exception: %s',
2214 'bin/almanac register',
2215 $ex->getMessage()),
2216 array($ex));
2219 $private_path = AlmanacKeys::getKeyPath('device.key');
2220 try {
2221 $private_key = Filesystem::readFile($private_path);
2222 $private_key = new PhutilOpaqueEnvelope($private_key);
2223 } catch (Exception $ex) {
2224 throw new PhutilAggregateException(
2225 pht(
2226 'Unable to read device private key while attempting to make '.
2227 'authenticated method call within the cluster. '.
2228 'Use `%s` to register keys for this device. Exception: %s',
2229 'bin/almanac register',
2230 $ex->getMessage()),
2231 array($ex));
2234 $client->setSigningKeys($public_key, $private_key);
2235 } else {
2236 // If the caller is a normal user, we generate or retrieve a cluster
2237 // API token.
2239 $token = PhabricatorConduitToken::loadClusterTokenForUser($viewer);
2240 if ($token) {
2241 $client->setConduitToken($token->getToken());
2245 return $client;
2248 public function newConduitClientForRequest(ConduitAPIRequest $request) {
2249 // Figure out whether we're going to handle this request on this device,
2250 // or proxy it to another node in the cluster.
2252 // If this is a cluster request and we need to proxy, we'll explode here
2253 // to prevent infinite recursion.
2255 $viewer = $request->getViewer();
2256 $is_cluster_request = $request->getIsClusterRequest();
2258 $client = $this->newConduitClient(
2259 $viewer,
2260 $is_cluster_request);
2262 return $client;
2265 public function newConduitFuture(
2266 PhabricatorUser $viewer,
2267 $method,
2268 array $params,
2269 $never_proxy = false) {
2271 $client = $this->newConduitClient(
2272 $viewer,
2273 $never_proxy);
2275 if (!$client) {
2276 $conduit_call = id(new ConduitCall($method, $params))
2277 ->setUser($viewer);
2278 $future = new MethodCallFuture($conduit_call, 'execute');
2279 } else {
2280 $future = $client->callMethod($method, $params);
2283 return $future;
2286 public function getPassthroughEnvironmentalVariables() {
2287 $env = $_ENV;
2289 if ($this->isGit()) {
2290 // $_ENV does not populate in CLI contexts if "E" is missing from
2291 // "variables_order" in PHP config. Currently, we do not require this
2292 // to be configured. Since it may not be, explicitly bring expected Git
2293 // environmental variables into scope. This list is not exhaustive, but
2294 // only lists variables with a known impact on commit hook behavior.
2296 // This can be removed if we later require "E" in "variables_order".
2298 $git_env = array(
2299 'GIT_OBJECT_DIRECTORY',
2300 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
2301 'GIT_QUARANTINE_PATH',
2303 foreach ($git_env as $key) {
2304 $value = getenv($key);
2305 if (strlen($value)) {
2306 $env[$key] = $value;
2310 $key = 'GIT_PUSH_OPTION_COUNT';
2311 $git_count = getenv($key);
2312 if (strlen($git_count)) {
2313 $git_count = (int)$git_count;
2314 $env[$key] = $git_count;
2315 for ($ii = 0; $ii < $git_count; $ii++) {
2316 $key = 'GIT_PUSH_OPTION_'.$ii;
2317 $env[$key] = getenv($key);
2322 $result = array();
2323 foreach ($env as $key => $value) {
2324 // In Git, pass anything matching "GIT_*" though. Some of these variables
2325 // need to be preserved to allow `git` operations to work properly when
2326 // running from commit hooks.
2327 if ($this->isGit()) {
2328 if (preg_match('/^GIT_/', $key)) {
2329 $result[$key] = $value;
2334 return $result;
2337 public function supportsBranchComparison() {
2338 return $this->isGit();
2341 public function isReadOnly() {
2342 return (bool)$this->getDetail('read-only');
2345 public function setReadOnly($read_only) {
2346 return $this->setDetail('read-only', $read_only);
2349 public function getReadOnlyMessage() {
2350 return $this->getDetail('read-only-message');
2353 public function setReadOnlyMessage($message) {
2354 return $this->setDetail('read-only-message', $message);
2357 public function getReadOnlyMessageForDisplay() {
2358 $parts = array();
2359 $parts[] = pht(
2360 'This repository is currently in read-only maintenance mode.');
2362 $message = $this->getReadOnlyMessage();
2363 if ($message !== null) {
2364 $parts[] = $message;
2367 return implode("\n\n", $parts);
2370 /* -( Repository URIs )---------------------------------------------------- */
2373 public function attachURIs(array $uris) {
2374 $custom_map = array();
2375 foreach ($uris as $key => $uri) {
2376 $builtin_key = $uri->getRepositoryURIBuiltinKey();
2377 if ($builtin_key !== null) {
2378 $custom_map[$builtin_key] = $key;
2382 $builtin_uris = $this->newBuiltinURIs();
2383 $seen_builtins = array();
2384 foreach ($builtin_uris as $builtin_uri) {
2385 $builtin_key = $builtin_uri->getRepositoryURIBuiltinKey();
2386 $seen_builtins[$builtin_key] = true;
2388 // If this builtin URI is disabled, don't attach it and remove the
2389 // persisted version if it exists.
2390 if ($builtin_uri->getIsDisabled()) {
2391 if (isset($custom_map[$builtin_key])) {
2392 unset($uris[$custom_map[$builtin_key]]);
2394 continue;
2397 // If the URI exists, make sure it's marked as not being disabled.
2398 if (isset($custom_map[$builtin_key])) {
2399 $uris[$custom_map[$builtin_key]]->setIsDisabled(false);
2403 // Remove any builtins which no longer exist.
2404 foreach ($custom_map as $builtin_key => $key) {
2405 if (empty($seen_builtins[$builtin_key])) {
2406 unset($uris[$key]);
2410 $this->uris = $uris;
2412 return $this;
2415 public function getURIs() {
2416 return $this->assertAttached($this->uris);
2419 public function getCloneURIs() {
2420 $uris = $this->getURIs();
2422 $clone = array();
2423 foreach ($uris as $uri) {
2424 if (!$uri->isBuiltin()) {
2425 continue;
2428 if ($uri->getIsDisabled()) {
2429 continue;
2432 $io_type = $uri->getEffectiveIoType();
2433 $is_clone =
2434 ($io_type == PhabricatorRepositoryURI::IO_READ) ||
2435 ($io_type == PhabricatorRepositoryURI::IO_READWRITE);
2437 if (!$is_clone) {
2438 continue;
2441 $clone[] = $uri;
2444 $clone = msort($clone, 'getURIScore');
2445 $clone = array_reverse($clone);
2447 return $clone;
2451 public function newBuiltinURIs() {
2452 $has_callsign = ($this->getCallsign() !== null);
2453 $has_shortname = ($this->getRepositorySlug() !== null);
2455 $identifier_map = array(
2456 PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign,
2457 PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname,
2458 PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true,
2461 // If the view policy of the repository is public, support anonymous HTTP
2462 // even if authenticated HTTP is not supported.
2463 if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) {
2464 $allow_http = true;
2465 } else {
2466 $allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
2469 $base_uri = PhabricatorEnv::getURI('/');
2470 $base_uri = new PhutilURI($base_uri);
2471 $has_https = ($base_uri->getProtocol() == 'https');
2472 $has_https = ($has_https && $allow_http);
2474 $has_http = !PhabricatorEnv::getEnvConfig('security.require-https');
2475 $has_http = ($has_http && $allow_http);
2477 // HTTP is not supported for Subversion.
2478 if ($this->isSVN()) {
2479 $has_http = false;
2480 $has_https = false;
2483 $phd_user = PhabricatorEnv::getEnvConfig('phd.user');
2484 $has_ssh = $phd_user !== null && strlen($phd_user);
2486 $protocol_map = array(
2487 PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh,
2488 PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https,
2489 PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http,
2492 $uris = array();
2493 foreach ($protocol_map as $protocol => $proto_supported) {
2494 foreach ($identifier_map as $identifier => $id_supported) {
2495 // This is just a dummy value because it can't be empty; we'll force
2496 // it to a proper value when using it in the UI.
2497 $builtin_uri = "{$protocol}://{$identifier}";
2498 $uris[] = PhabricatorRepositoryURI::initializeNewURI()
2499 ->setRepositoryPHID($this->getPHID())
2500 ->attachRepository($this)
2501 ->setBuiltinProtocol($protocol)
2502 ->setBuiltinIdentifier($identifier)
2503 ->setURI($builtin_uri)
2504 ->setIsDisabled((int)(!$proto_supported || !$id_supported));
2508 return $uris;
2512 public function getClusterRepositoryURIFromBinding(
2513 AlmanacBinding $binding) {
2514 $protocol = $binding->getAlmanacPropertyValue('protocol');
2515 if ($protocol === null) {
2516 $protocol = 'https';
2519 $iface = $binding->getInterface();
2520 $address = $iface->renderDisplayAddress();
2522 $path = $this->getURI();
2524 return id(new PhutilURI("{$protocol}://{$address}"))
2525 ->setPath($path);
2528 public function loadAlmanacService() {
2529 $service_phid = $this->getAlmanacServicePHID();
2530 if (!$service_phid) {
2531 // No service, so this is a local repository.
2532 return null;
2535 $service = id(new AlmanacServiceQuery())
2536 ->setViewer(PhabricatorUser::getOmnipotentUser())
2537 ->withPHIDs(array($service_phid))
2538 ->needActiveBindings(true)
2539 ->needProperties(true)
2540 ->executeOne();
2541 if (!$service) {
2542 throw new Exception(
2543 pht(
2544 'The Almanac service for this repository is invalid or could not '.
2545 'be loaded.'));
2548 $service_type = $service->getServiceImplementation();
2549 if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
2550 throw new Exception(
2551 pht(
2552 'The Almanac service for this repository does not have the correct '.
2553 'service type.'));
2556 return $service;
2559 public function markImporting() {
2560 $this->openTransaction();
2561 $this->beginReadLocking();
2562 $repository = $this->reload();
2563 $repository->setDetail('importing', true);
2564 $repository->save();
2565 $this->endReadLocking();
2566 $this->saveTransaction();
2568 return $repository;
2572 /* -( Symbols )-------------------------------------------------------------*/
2574 public function getSymbolSources() {
2575 return $this->getDetail('symbol-sources', array());
2578 public function getSymbolLanguages() {
2579 return $this->getDetail('symbol-languages', array());
2583 /* -( Staging )------------------------------------------------------------ */
2586 public function supportsStaging() {
2587 return $this->isGit();
2591 public function getStagingURI() {
2592 if (!$this->supportsStaging()) {
2593 return null;
2595 return $this->getDetail('staging-uri', null);
2599 /* -( Automation )--------------------------------------------------------- */
2602 public function supportsAutomation() {
2603 return $this->isGit();
2606 public function canPerformAutomation() {
2607 if (!$this->supportsAutomation()) {
2608 return false;
2611 if (!$this->getAutomationBlueprintPHIDs()) {
2612 return false;
2615 return true;
2618 public function getAutomationBlueprintPHIDs() {
2619 if (!$this->supportsAutomation()) {
2620 return array();
2622 return $this->getDetail('automation.blueprintPHIDs', array());
2626 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
2629 public function getApplicationTransactionEditor() {
2630 return new PhabricatorRepositoryEditor();
2633 public function getApplicationTransactionTemplate() {
2634 return new PhabricatorRepositoryTransaction();
2638 /* -( PhabricatorPolicyInterface )----------------------------------------- */
2641 public function getCapabilities() {
2642 return array(
2643 PhabricatorPolicyCapability::CAN_VIEW,
2644 PhabricatorPolicyCapability::CAN_EDIT,
2645 DiffusionPushCapability::CAPABILITY,
2649 public function getPolicy($capability) {
2650 switch ($capability) {
2651 case PhabricatorPolicyCapability::CAN_VIEW:
2652 return $this->getViewPolicy();
2653 case PhabricatorPolicyCapability::CAN_EDIT:
2654 return $this->getEditPolicy();
2655 case DiffusionPushCapability::CAPABILITY:
2656 return $this->getPushPolicy();
2660 public function hasAutomaticCapability($capability, PhabricatorUser $user) {
2661 return false;
2665 /* -( PhabricatorMarkupInterface )----------------------------------------- */
2668 public function getMarkupFieldKey($field) {
2669 $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
2670 return "repo:{$hash}";
2673 public function newMarkupEngine($field) {
2674 return PhabricatorMarkupEngine::newMarkupEngine(array());
2677 public function getMarkupText($field) {
2678 return $this->getDetail('description');
2681 public function didMarkupText(
2682 $field,
2683 $output,
2684 PhutilMarkupEngine $engine) {
2685 require_celerity_resource('phabricator-remarkup-css');
2686 return phutil_tag(
2687 'div',
2688 array(
2689 'class' => 'phabricator-remarkup',
2691 $output);
2694 public function shouldUseMarkupCache($field) {
2695 return true;
2699 /* -( PhabricatorDestructibleInterface )----------------------------------- */
2702 public function destroyObjectPermanently(
2703 PhabricatorDestructionEngine $engine) {
2705 $phid = $this->getPHID();
2707 $this->openTransaction();
2709 $this->delete();
2711 PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array());
2713 $books = id(new DivinerBookQuery())
2714 ->setViewer($engine->getViewer())
2715 ->withRepositoryPHIDs(array($phid))
2716 ->execute();
2717 foreach ($books as $book) {
2718 $engine->destroyObject($book);
2721 $atoms = id(new DivinerAtomQuery())
2722 ->setViewer($engine->getViewer())
2723 ->withRepositoryPHIDs(array($phid))
2724 ->execute();
2725 foreach ($atoms as $atom) {
2726 $engine->destroyObject($atom);
2729 $lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery())
2730 ->setViewer($engine->getViewer())
2731 ->withRepositoryPHIDs(array($phid))
2732 ->execute();
2733 foreach ($lfs_refs as $ref) {
2734 $engine->destroyObject($ref);
2737 $this->saveTransaction();
2741 /* -( PhabricatorDestructibleCodexInterface )------------------------------ */
2744 public function newDestructibleCodex() {
2745 return new PhabricatorRepositoryDestructibleCodex();
2749 /* -( PhabricatorSpacesInterface )----------------------------------------- */
2752 public function getSpacePHID() {
2753 return $this->spacePHID;
2756 /* -( PhabricatorConduitResultInterface )---------------------------------- */
2759 public function getFieldSpecificationsForConduit() {
2760 return array(
2761 id(new PhabricatorConduitSearchFieldSpecification())
2762 ->setKey('name')
2763 ->setType('string')
2764 ->setDescription(pht('The repository name.')),
2765 id(new PhabricatorConduitSearchFieldSpecification())
2766 ->setKey('vcs')
2767 ->setType('string')
2768 ->setDescription(
2769 pht('The VCS this repository uses ("git", "hg" or "svn").')),
2770 id(new PhabricatorConduitSearchFieldSpecification())
2771 ->setKey('callsign')
2772 ->setType('string')
2773 ->setDescription(pht('The repository callsign, if it has one.')),
2774 id(new PhabricatorConduitSearchFieldSpecification())
2775 ->setKey('shortName')
2776 ->setType('string')
2777 ->setDescription(pht('Unique short name, if the repository has one.')),
2778 id(new PhabricatorConduitSearchFieldSpecification())
2779 ->setKey('status')
2780 ->setType('string')
2781 ->setDescription(pht('Active or inactive status.')),
2782 id(new PhabricatorConduitSearchFieldSpecification())
2783 ->setKey('isImporting')
2784 ->setType('bool')
2785 ->setDescription(
2786 pht(
2787 'True if the repository is importing initial commits.')),
2788 id(new PhabricatorConduitSearchFieldSpecification())
2789 ->setKey('almanacServicePHID')
2790 ->setType('phid?')
2791 ->setDescription(
2792 pht(
2793 'The Almanac Service that hosts this repository, if the '.
2794 'repository is clustered.')),
2795 id(new PhabricatorConduitSearchFieldSpecification())
2796 ->setKey('refRules')
2797 ->setType('map<string, list<string>>')
2798 ->setDescription(
2799 pht(
2800 'The "Fetch" and "Permanent Ref" rules for this repository.')),
2801 id(new PhabricatorConduitSearchFieldSpecification())
2802 ->setKey('defaultBranch')
2803 ->setType('string?')
2804 ->setDescription(pht('Default branch name.')),
2805 id(new PhabricatorConduitSearchFieldSpecification())
2806 ->setKey('description')
2807 ->setType('remarkup')
2808 ->setDescription(pht('Repository description.')),
2812 public function getFieldValuesForConduit() {
2813 $fetch_rules = $this->getFetchRules();
2814 $track_rules = $this->getTrackOnlyRules();
2815 $permanent_rules = $this->getPermanentRefRules();
2817 $fetch_rules = $this->getStringListForConduit($fetch_rules);
2818 $track_rules = $this->getStringListForConduit($track_rules);
2819 $permanent_rules = $this->getStringListForConduit($permanent_rules);
2821 $default_branch = $this->getDefaultBranch();
2822 if ($default_branch === null || !strlen($default_branch)) {
2823 $default_branch = null;
2826 return array(
2827 'name' => $this->getName(),
2828 'vcs' => $this->getVersionControlSystem(),
2829 'callsign' => $this->getCallsign(),
2830 'shortName' => $this->getRepositorySlug(),
2831 'status' => $this->getStatus(),
2832 'isImporting' => (bool)$this->isImporting(),
2833 'almanacServicePHID' => $this->getAlmanacServicePHID(),
2834 'refRules' => array(
2835 'fetchRules' => $fetch_rules,
2836 'trackRules' => $track_rules,
2837 'permanentRefRules' => $permanent_rules,
2839 'defaultBranch' => $default_branch,
2840 'description' => array(
2841 'raw' => (string)$this->getDetail('description'),
2846 private function getStringListForConduit($list) {
2847 if (!is_array($list)) {
2848 $list = array();
2851 foreach ($list as $key => $value) {
2852 $value = (string)$value;
2853 if (!strlen($value)) {
2854 unset($list[$key]);
2858 return array_values($list);
2861 public function getConduitSearchAttachments() {
2862 return array(
2863 id(new DiffusionRepositoryURIsSearchEngineAttachment())
2864 ->setAttachmentKey('uris'),
2865 id(new DiffusionRepositoryMetricsSearchEngineAttachment())
2866 ->setAttachmentKey('metrics'),
2870 /* -( PhabricatorFulltextInterface )--------------------------------------- */
2873 public function newFulltextEngine() {
2874 return new PhabricatorRepositoryFulltextEngine();
2878 /* -( PhabricatorFerretInterface )----------------------------------------- */
2881 public function newFerretEngine() {
2882 return new PhabricatorRepositoryFerretEngine();