@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<?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}