PageRenderTime 63ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

/src/applications/repository/storage/PhabricatorRepository.php

http://github.com/facebook/phabricator
PHP | 2844 lines | 2047 code | 501 blank | 296 comment | 240 complexity | f0dec9013ea6117c9bf5c1b9fd7d61a1 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * @task uri Repository URI Management
  4. * @task publishing Publishing
  5. * @task sync Cluster Synchronization
  6. */
  7. final class PhabricatorRepository extends PhabricatorRepositoryDAO
  8. implements
  9. PhabricatorApplicationTransactionInterface,
  10. PhabricatorPolicyInterface,
  11. PhabricatorFlaggableInterface,
  12. PhabricatorMarkupInterface,
  13. PhabricatorDestructibleInterface,
  14. PhabricatorDestructibleCodexInterface,
  15. PhabricatorProjectInterface,
  16. PhabricatorSpacesInterface,
  17. PhabricatorConduitResultInterface,
  18. PhabricatorFulltextInterface,
  19. PhabricatorFerretInterface {
  20. /**
  21. * Shortest hash we'll recognize in raw "a829f32" form.
  22. */
  23. const MINIMUM_UNQUALIFIED_HASH = 7;
  24. /**
  25. * Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
  26. */
  27. const MINIMUM_QUALIFIED_HASH = 5;
  28. /**
  29. * Minimum number of commits to an empty repository to trigger "import" mode.
  30. */
  31. const IMPORT_THRESHOLD = 7;
  32. const LOWPRI_THRESHOLD = 64;
  33. const TABLE_PATH = 'repository_path';
  34. const TABLE_PATHCHANGE = 'repository_pathchange';
  35. const TABLE_FILESYSTEM = 'repository_filesystem';
  36. const TABLE_SUMMARY = 'repository_summary';
  37. const TABLE_LINTMESSAGE = 'repository_lintmessage';
  38. const TABLE_PARENTS = 'repository_parents';
  39. const TABLE_COVERAGE = 'repository_coverage';
  40. const STATUS_ACTIVE = 'active';
  41. const STATUS_INACTIVE = 'inactive';
  42. protected $name;
  43. protected $callsign;
  44. protected $repositorySlug;
  45. protected $uuid;
  46. protected $viewPolicy;
  47. protected $editPolicy;
  48. protected $pushPolicy;
  49. protected $profileImagePHID;
  50. protected $versionControlSystem;
  51. protected $details = array();
  52. protected $credentialPHID;
  53. protected $almanacServicePHID;
  54. protected $spacePHID;
  55. protected $localPath;
  56. private $commitCount = self::ATTACHABLE;
  57. private $mostRecentCommit = self::ATTACHABLE;
  58. private $projectPHIDs = self::ATTACHABLE;
  59. private $uris = self::ATTACHABLE;
  60. private $profileImageFile = self::ATTACHABLE;
  61. public static function initializeNewRepository(PhabricatorUser $actor) {
  62. $app = id(new PhabricatorApplicationQuery())
  63. ->setViewer($actor)
  64. ->withClasses(array('PhabricatorDiffusionApplication'))
  65. ->executeOne();
  66. $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
  67. $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
  68. $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
  69. $repository = id(new PhabricatorRepository())
  70. ->setViewPolicy($view_policy)
  71. ->setEditPolicy($edit_policy)
  72. ->setPushPolicy($push_policy)
  73. ->setSpacePHID($actor->getDefaultSpacePHID());
  74. // Put the repository in "Importing" mode until we finish
  75. // parsing it.
  76. $repository->setDetail('importing', true);
  77. return $repository;
  78. }
  79. protected function getConfiguration() {
  80. return array(
  81. self::CONFIG_AUX_PHID => true,
  82. self::CONFIG_SERIALIZATION => array(
  83. 'details' => self::SERIALIZATION_JSON,
  84. ),
  85. self::CONFIG_COLUMN_SCHEMA => array(
  86. 'name' => 'sort255',
  87. 'callsign' => 'sort32?',
  88. 'repositorySlug' => 'sort64?',
  89. 'versionControlSystem' => 'text32',
  90. 'uuid' => 'text64?',
  91. 'pushPolicy' => 'policy',
  92. 'credentialPHID' => 'phid?',
  93. 'almanacServicePHID' => 'phid?',
  94. 'localPath' => 'text128?',
  95. 'profileImagePHID' => 'phid?',
  96. ),
  97. self::CONFIG_KEY_SCHEMA => array(
  98. 'callsign' => array(
  99. 'columns' => array('callsign'),
  100. 'unique' => true,
  101. ),
  102. 'key_name' => array(
  103. 'columns' => array('name(128)'),
  104. ),
  105. 'key_vcs' => array(
  106. 'columns' => array('versionControlSystem'),
  107. ),
  108. 'key_slug' => array(
  109. 'columns' => array('repositorySlug'),
  110. 'unique' => true,
  111. ),
  112. 'key_local' => array(
  113. 'columns' => array('localPath'),
  114. 'unique' => true,
  115. ),
  116. ),
  117. ) + parent::getConfiguration();
  118. }
  119. public function generatePHID() {
  120. return PhabricatorPHID::generateNewPHID(
  121. PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
  122. }
  123. public static function getStatusMap() {
  124. return array(
  125. self::STATUS_ACTIVE => array(
  126. 'name' => pht('Active'),
  127. 'isTracked' => 1,
  128. ),
  129. self::STATUS_INACTIVE => array(
  130. 'name' => pht('Inactive'),
  131. 'isTracked' => 0,
  132. ),
  133. );
  134. }
  135. public static function getStatusNameMap() {
  136. return ipull(self::getStatusMap(), 'name');
  137. }
  138. public function getStatus() {
  139. if ($this->isTracked()) {
  140. return self::STATUS_ACTIVE;
  141. } else {
  142. return self::STATUS_INACTIVE;
  143. }
  144. }
  145. public function toDictionary() {
  146. return array(
  147. 'id' => $this->getID(),
  148. 'name' => $this->getName(),
  149. 'phid' => $this->getPHID(),
  150. 'callsign' => $this->getCallsign(),
  151. 'monogram' => $this->getMonogram(),
  152. 'vcs' => $this->getVersionControlSystem(),
  153. 'uri' => PhabricatorEnv::getProductionURI($this->getURI()),
  154. 'remoteURI' => (string)$this->getRemoteURI(),
  155. 'description' => $this->getDetail('description'),
  156. 'isActive' => $this->isTracked(),
  157. 'isHosted' => $this->isHosted(),
  158. 'isImporting' => $this->isImporting(),
  159. 'encoding' => $this->getDefaultTextEncoding(),
  160. 'staging' => array(
  161. 'supported' => $this->supportsStaging(),
  162. 'prefix' => 'phabricator',
  163. 'uri' => $this->getStagingURI(),
  164. ),
  165. );
  166. }
  167. public function getDefaultTextEncoding() {
  168. return $this->getDetail('encoding', 'UTF-8');
  169. }
  170. public function getMonogram() {
  171. $callsign = $this->getCallsign();
  172. if (strlen($callsign)) {
  173. return "r{$callsign}";
  174. }
  175. $id = $this->getID();
  176. return "R{$id}";
  177. }
  178. public function getDisplayName() {
  179. $slug = $this->getRepositorySlug();
  180. if (strlen($slug)) {
  181. return $slug;
  182. }
  183. return $this->getMonogram();
  184. }
  185. public function getAllMonograms() {
  186. $monograms = array();
  187. $monograms[] = 'R'.$this->getID();
  188. $callsign = $this->getCallsign();
  189. if (strlen($callsign)) {
  190. $monograms[] = 'r'.$callsign;
  191. }
  192. return $monograms;
  193. }
  194. public function setLocalPath($path) {
  195. // Convert any extra slashes ("//") in the path to a single slash ("/").
  196. $path = preg_replace('(//+)', '/', $path);
  197. return parent::setLocalPath($path);
  198. }
  199. public function getDetail($key, $default = null) {
  200. return idx($this->details, $key, $default);
  201. }
  202. public function setDetail($key, $value) {
  203. $this->details[$key] = $value;
  204. return $this;
  205. }
  206. public function attachCommitCount($count) {
  207. $this->commitCount = $count;
  208. return $this;
  209. }
  210. public function getCommitCount() {
  211. return $this->assertAttached($this->commitCount);
  212. }
  213. public function attachMostRecentCommit(
  214. PhabricatorRepositoryCommit $commit = null) {
  215. $this->mostRecentCommit = $commit;
  216. return $this;
  217. }
  218. public function getMostRecentCommit() {
  219. return $this->assertAttached($this->mostRecentCommit);
  220. }
  221. public function getDiffusionBrowseURIForPath(
  222. PhabricatorUser $user,
  223. $path,
  224. $line = null,
  225. $branch = null) {
  226. $drequest = DiffusionRequest::newFromDictionary(
  227. array(
  228. 'user' => $user,
  229. 'repository' => $this,
  230. 'path' => $path,
  231. 'branch' => $branch,
  232. ));
  233. return $drequest->generateURI(
  234. array(
  235. 'action' => 'browse',
  236. 'line' => $line,
  237. ));
  238. }
  239. public function getSubversionBaseURI($commit = null) {
  240. $subpath = $this->getDetail('svn-subpath');
  241. if (!strlen($subpath)) {
  242. $subpath = null;
  243. }
  244. return $this->getSubversionPathURI($subpath, $commit);
  245. }
  246. public function getSubversionPathURI($path = null, $commit = null) {
  247. $vcs = $this->getVersionControlSystem();
  248. if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
  249. throw new Exception(pht('Not a subversion repository!'));
  250. }
  251. if ($this->isHosted()) {
  252. $uri = 'file://'.$this->getLocalPath();
  253. } else {
  254. $uri = $this->getDetail('remote-uri');
  255. }
  256. $uri = rtrim($uri, '/');
  257. if (strlen($path)) {
  258. $path = rawurlencode($path);
  259. $path = str_replace('%2F', '/', $path);
  260. $uri = $uri.'/'.ltrim($path, '/');
  261. }
  262. if ($path !== null || $commit !== null) {
  263. $uri .= '@';
  264. }
  265. if ($commit !== null) {
  266. $uri .= $commit;
  267. }
  268. return $uri;
  269. }
  270. public function attachProjectPHIDs(array $project_phids) {
  271. $this->projectPHIDs = $project_phids;
  272. return $this;
  273. }
  274. public function getProjectPHIDs() {
  275. return $this->assertAttached($this->projectPHIDs);
  276. }
  277. /**
  278. * Get the name of the directory this repository should clone or checkout
  279. * into. For example, if the repository name is "Example Repository", a
  280. * reasonable name might be "example-repository". This is used to help users
  281. * get reasonable results when cloning repositories, since they generally do
  282. * not want to clone into directories called "X/" or "Example Repository/".
  283. *
  284. * @return string
  285. */
  286. public function getCloneName() {
  287. $name = $this->getRepositorySlug();
  288. // Make some reasonable effort to produce reasonable default directory
  289. // names from repository names.
  290. if (!strlen($name)) {
  291. $name = $this->getName();
  292. $name = phutil_utf8_strtolower($name);
  293. $name = preg_replace('@[ -/:->]+@', '-', $name);
  294. $name = trim($name, '-');
  295. if (!strlen($name)) {
  296. $name = $this->getCallsign();
  297. }
  298. }
  299. return $name;
  300. }
  301. public static function isValidRepositorySlug($slug) {
  302. try {
  303. self::assertValidRepositorySlug($slug);
  304. return true;
  305. } catch (Exception $ex) {
  306. return false;
  307. }
  308. }
  309. public static function assertValidRepositorySlug($slug) {
  310. if (!strlen($slug)) {
  311. throw new Exception(
  312. pht(
  313. 'The empty string is not a valid repository short name. '.
  314. 'Repository short names must be at least one character long.'));
  315. }
  316. if (strlen($slug) > 64) {
  317. throw new Exception(
  318. pht(
  319. 'The name "%s" is not a valid repository short name. Repository '.
  320. 'short names must not be longer than 64 characters.',
  321. $slug));
  322. }
  323. if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) {
  324. throw new Exception(
  325. pht(
  326. 'The name "%s" is not a valid repository short name. Repository '.
  327. 'short names may only contain letters, numbers, periods, hyphens '.
  328. 'and underscores.',
  329. $slug));
  330. }
  331. if (!preg_match('/^[a-zA-Z0-9]/', $slug)) {
  332. throw new Exception(
  333. pht(
  334. 'The name "%s" is not a valid repository short name. Repository '.
  335. 'short names must begin with a letter or number.',
  336. $slug));
  337. }
  338. if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) {
  339. throw new Exception(
  340. pht(
  341. 'The name "%s" is not a valid repository short name. Repository '.
  342. 'short names must end with a letter or number.',
  343. $slug));
  344. }
  345. if (preg_match('/__|--|\\.\\./', $slug)) {
  346. throw new Exception(
  347. pht(
  348. 'The name "%s" is not a valid repository short name. Repository '.
  349. 'short names must not contain multiple consecutive underscores, '.
  350. 'hyphens, or periods.',
  351. $slug));
  352. }
  353. if (preg_match('/^[A-Z]+\z/', $slug)) {
  354. throw new Exception(
  355. pht(
  356. 'The name "%s" is not a valid repository short name. Repository '.
  357. 'short names may not contain only uppercase letters.',
  358. $slug));
  359. }
  360. if (preg_match('/^\d+\z/', $slug)) {
  361. throw new Exception(
  362. pht(
  363. 'The name "%s" is not a valid repository short name. Repository '.
  364. 'short names may not contain only numbers.',
  365. $slug));
  366. }
  367. if (preg_match('/\\.git/', $slug)) {
  368. throw new Exception(
  369. pht(
  370. 'The name "%s" is not a valid repository short name. Repository '.
  371. 'short names must not end in ".git". This suffix will be added '.
  372. 'automatically in appropriate contexts.',
  373. $slug));
  374. }
  375. }
  376. public static function assertValidCallsign($callsign) {
  377. if (!strlen($callsign)) {
  378. throw new Exception(
  379. pht(
  380. 'A repository callsign must be at least one character long.'));
  381. }
  382. if (strlen($callsign) > 32) {
  383. throw new Exception(
  384. pht(
  385. 'The callsign "%s" is not a valid repository callsign. Callsigns '.
  386. 'must be no more than 32 bytes long.',
  387. $callsign));
  388. }
  389. if (!preg_match('/^[A-Z]+\z/', $callsign)) {
  390. throw new Exception(
  391. pht(
  392. 'The callsign "%s" is not a valid repository callsign. Callsigns '.
  393. 'may only contain UPPERCASE letters.',
  394. $callsign));
  395. }
  396. }
  397. public function getProfileImageURI() {
  398. return $this->getProfileImageFile()->getBestURI();
  399. }
  400. public function attachProfileImageFile(PhabricatorFile $file) {
  401. $this->profileImageFile = $file;
  402. return $this;
  403. }
  404. public function getProfileImageFile() {
  405. return $this->assertAttached($this->profileImageFile);
  406. }
  407. /* -( Remote Command Execution )------------------------------------------- */
  408. public function execRemoteCommand($pattern /* , $arg, ... */) {
  409. $args = func_get_args();
  410. return $this->newRemoteCommandFuture($args)->resolve();
  411. }
  412. public function execxRemoteCommand($pattern /* , $arg, ... */) {
  413. $args = func_get_args();
  414. return $this->newRemoteCommandFuture($args)->resolvex();
  415. }
  416. public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
  417. $args = func_get_args();
  418. return $this->newRemoteCommandFuture($args);
  419. }
  420. public function passthruRemoteCommand($pattern /* , $arg, ... */) {
  421. $args = func_get_args();
  422. return $this->newRemoteCommandPassthru($args)->execute();
  423. }
  424. private function newRemoteCommandFuture(array $argv) {
  425. return $this->newRemoteCommandEngine($argv)
  426. ->newFuture();
  427. }
  428. private function newRemoteCommandPassthru(array $argv) {
  429. return $this->newRemoteCommandEngine($argv)
  430. ->setPassthru(true)
  431. ->newFuture();
  432. }
  433. private function newRemoteCommandEngine(array $argv) {
  434. return DiffusionCommandEngine::newCommandEngine($this)
  435. ->setArgv($argv)
  436. ->setCredentialPHID($this->getCredentialPHID())
  437. ->setURI($this->getRemoteURIObject());
  438. }
  439. /* -( Local Command Execution )-------------------------------------------- */
  440. public function execLocalCommand($pattern /* , $arg, ... */) {
  441. $args = func_get_args();
  442. return $this->newLocalCommandFuture($args)->resolve();
  443. }
  444. public function execxLocalCommand($pattern /* , $arg, ... */) {
  445. $args = func_get_args();
  446. return $this->newLocalCommandFuture($args)->resolvex();
  447. }
  448. public function getLocalCommandFuture($pattern /* , $arg, ... */) {
  449. $args = func_get_args();
  450. return $this->newLocalCommandFuture($args);
  451. }
  452. public function passthruLocalCommand($pattern /* , $arg, ... */) {
  453. $args = func_get_args();
  454. return $this->newLocalCommandPassthru($args)->execute();
  455. }
  456. private function newLocalCommandFuture(array $argv) {
  457. $this->assertLocalExists();
  458. $future = DiffusionCommandEngine::newCommandEngine($this)
  459. ->setArgv($argv)
  460. ->newFuture();
  461. if ($this->usesLocalWorkingCopy()) {
  462. $future->setCWD($this->getLocalPath());
  463. }
  464. return $future;
  465. }
  466. private function newLocalCommandPassthru(array $argv) {
  467. $this->assertLocalExists();
  468. $future = DiffusionCommandEngine::newCommandEngine($this)
  469. ->setArgv($argv)
  470. ->setPassthru(true)
  471. ->newFuture();
  472. if ($this->usesLocalWorkingCopy()) {
  473. $future->setCWD($this->getLocalPath());
  474. }
  475. return $future;
  476. }
  477. public function getURI() {
  478. $short_name = $this->getRepositorySlug();
  479. if (strlen($short_name)) {
  480. return "/source/{$short_name}/";
  481. }
  482. $callsign = $this->getCallsign();
  483. if (strlen($callsign)) {
  484. return "/diffusion/{$callsign}/";
  485. }
  486. $id = $this->getID();
  487. return "/diffusion/{$id}/";
  488. }
  489. public function getPathURI($path) {
  490. return $this->getURI().ltrim($path, '/');
  491. }
  492. public function getCommitURI($identifier) {
  493. $callsign = $this->getCallsign();
  494. if (strlen($callsign)) {
  495. return "/r{$callsign}{$identifier}";
  496. }
  497. $id = $this->getID();
  498. return "/R{$id}:{$identifier}";
  499. }
  500. public static function parseRepositoryServicePath($request_path, $vcs) {
  501. $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
  502. $patterns = array(
  503. '(^'.
  504. '(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'.
  505. '(?P<path>.*)'.
  506. '\z)',
  507. );
  508. $identifier = null;
  509. foreach ($patterns as $pattern) {
  510. $matches = null;
  511. if (!preg_match($pattern, $request_path, $matches)) {
  512. continue;
  513. }
  514. $identifier = $matches['identifier'];
  515. if ($is_git) {
  516. $identifier = preg_replace('/\\.git\z/', '', $identifier);
  517. }
  518. $base = $matches['base'];
  519. $path = $matches['path'];
  520. break;
  521. }
  522. if ($identifier === null) {
  523. return null;
  524. }
  525. return array(
  526. 'identifier' => $identifier,
  527. 'base' => $base,
  528. 'path' => $path,
  529. );
  530. }
  531. public function getCanonicalPath($request_path) {
  532. $standard_pattern =
  533. '(^'.
  534. '(?P<prefix>/(?:diffusion|source)/)'.
  535. '(?P<identifier>[^/]+)'.
  536. '(?P<suffix>(?:/.*)?)'.
  537. '\z)';
  538. $matches = null;
  539. if (preg_match($standard_pattern, $request_path, $matches)) {
  540. $suffix = $matches['suffix'];
  541. return $this->getPathURI($suffix);
  542. }
  543. $commit_pattern =
  544. '(^'.
  545. '(?P<prefix>/)'.
  546. '(?P<monogram>'.
  547. '(?:'.
  548. 'r(?P<repositoryCallsign>[A-Z]+)'.
  549. '|'.
  550. 'R(?P<repositoryID>[1-9]\d*):'.
  551. ')'.
  552. '(?P<commit>[a-f0-9]+)'.
  553. ')'.
  554. '\z)';
  555. $matches = null;
  556. if (preg_match($commit_pattern, $request_path, $matches)) {
  557. $commit = $matches['commit'];
  558. return $this->getCommitURI($commit);
  559. }
  560. return null;
  561. }
  562. public function generateURI(array $params) {
  563. $req_branch = false;
  564. $req_commit = false;
  565. $action = idx($params, 'action');
  566. switch ($action) {
  567. case 'history':
  568. case 'graph':
  569. case 'clone':
  570. case 'blame':
  571. case 'browse':
  572. case 'document':
  573. case 'change':
  574. case 'lastmodified':
  575. case 'tags':
  576. case 'branches':
  577. case 'lint':
  578. case 'pathtree':
  579. case 'refs':
  580. case 'compare':
  581. break;
  582. case 'branch':
  583. // NOTE: This does not actually require a branch, and won't have one
  584. // in Subversion. Possibly this should be more clear.
  585. break;
  586. case 'commit':
  587. case 'rendering-ref':
  588. $req_commit = true;
  589. break;
  590. default:
  591. throw new Exception(
  592. pht(
  593. 'Action "%s" is not a valid repository URI action.',
  594. $action));
  595. }
  596. $path = idx($params, 'path');
  597. $branch = idx($params, 'branch');
  598. $commit = idx($params, 'commit');
  599. $line = idx($params, 'line');
  600. $head = idx($params, 'head');
  601. $against = idx($params, 'against');
  602. if ($req_commit && !strlen($commit)) {
  603. throw new Exception(
  604. pht(
  605. 'Diffusion URI action "%s" requires commit!',
  606. $action));
  607. }
  608. if ($req_branch && !strlen($branch)) {
  609. throw new Exception(
  610. pht(
  611. 'Diffusion URI action "%s" requires branch!',
  612. $action));
  613. }
  614. if ($action === 'commit') {
  615. return $this->getCommitURI($commit);
  616. }
  617. if (strlen($path)) {
  618. $path = ltrim($path, '/');
  619. $path = str_replace(array(';', '$'), array(';;', '$$'), $path);
  620. $path = phutil_escape_uri($path);
  621. }
  622. $raw_branch = $branch;
  623. if (strlen($branch)) {
  624. $branch = phutil_escape_uri_path_component($branch);
  625. $path = "{$branch}/{$path}";
  626. }
  627. $raw_commit = $commit;
  628. if (strlen($commit)) {
  629. $commit = str_replace('$', '$$', $commit);
  630. $commit = ';'.phutil_escape_uri($commit);
  631. }
  632. if (strlen($line)) {
  633. $line = '$'.phutil_escape_uri($line);
  634. }
  635. $query = array();
  636. switch ($action) {
  637. case 'change':
  638. case 'history':
  639. case 'graph':
  640. case 'blame':
  641. case 'browse':
  642. case 'document':
  643. case 'lastmodified':
  644. case 'tags':
  645. case 'branches':
  646. case 'lint':
  647. case 'pathtree':
  648. case 'refs':
  649. $uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}");
  650. break;
  651. case 'compare':
  652. $uri = $this->getPathURI("/{$action}/");
  653. if (strlen($head)) {
  654. $query['head'] = $head;
  655. } else if (strlen($raw_commit)) {
  656. $query['commit'] = $raw_commit;
  657. } else if (strlen($raw_branch)) {
  658. $query['head'] = $raw_branch;
  659. }
  660. if (strlen($against)) {
  661. $query['against'] = $against;
  662. }
  663. break;
  664. case 'branch':
  665. if (strlen($path)) {
  666. $uri = $this->getPathURI("/repository/{$path}");
  667. } else {
  668. $uri = $this->getPathURI('/');
  669. }
  670. break;
  671. case 'external':
  672. $commit = ltrim($commit, ';');
  673. $uri = "/diffusion/external/{$commit}/";
  674. break;
  675. case 'rendering-ref':
  676. // This isn't a real URI per se, it's passed as a query parameter to
  677. // the ajax changeset stuff but then we parse it back out as though
  678. // it came from a URI.
  679. $uri = rawurldecode("{$path}{$commit}");
  680. break;
  681. case 'clone':
  682. $uri = $this->getPathURI("/{$action}/");
  683. break;
  684. }
  685. if ($action == 'rendering-ref') {
  686. return $uri;
  687. }
  688. if (isset($params['lint'])) {
  689. $params['params'] = idx($params, 'params', array()) + array(
  690. 'lint' => $params['lint'],
  691. );
  692. }
  693. $query = idx($params, 'params', array()) + $query;
  694. return new PhutilURI($uri, $query);
  695. }
  696. public function updateURIIndex() {
  697. $indexes = array();
  698. $uris = $this->getURIs();
  699. foreach ($uris as $uri) {
  700. if ($uri->getIsDisabled()) {
  701. continue;
  702. }
  703. $indexes[] = $uri->getNormalizedURI();
  704. }
  705. PhabricatorRepositoryURIIndex::updateRepositoryURIs(
  706. $this->getPHID(),
  707. $indexes);
  708. return $this;
  709. }
  710. public function isTracked() {
  711. $status = $this->getDetail('tracking-enabled');
  712. $map = self::getStatusMap();
  713. $spec = idx($map, $status);
  714. if (!$spec) {
  715. if ($status) {
  716. $status = self::STATUS_ACTIVE;
  717. } else {
  718. $status = self::STATUS_INACTIVE;
  719. }
  720. $spec = idx($map, $status);
  721. }
  722. return (bool)idx($spec, 'isTracked', false);
  723. }
  724. public function getDefaultBranch() {
  725. $default = $this->getDetail('default-branch');
  726. if (strlen($default)) {
  727. return $default;
  728. }
  729. $default_branches = array(
  730. PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
  731. PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
  732. );
  733. return idx($default_branches, $this->getVersionControlSystem());
  734. }
  735. public function getDefaultArcanistBranch() {
  736. return coalesce($this->getDefaultBranch(), 'svn');
  737. }
  738. private function isBranchInFilter($branch, $filter_key) {
  739. $vcs = $this->getVersionControlSystem();
  740. $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
  741. $use_filter = ($is_git);
  742. if (!$use_filter) {
  743. // If this VCS doesn't use filters, pass everything through.
  744. return true;
  745. }
  746. $filter = $this->getDetail($filter_key, array());
  747. // If there's no filter set, let everything through.
  748. if (!$filter) {
  749. return true;
  750. }
  751. // If this branch isn't literally named `regexp(...)`, and it's in the
  752. // filter list, let it through.
  753. if (isset($filter[$branch])) {
  754. if (self::extractBranchRegexp($branch) === null) {
  755. return true;
  756. }
  757. }
  758. // If the branch matches a regexp, let it through.
  759. foreach ($filter as $pattern => $ignored) {
  760. $regexp = self::extractBranchRegexp($pattern);
  761. if ($regexp !== null) {
  762. if (preg_match($regexp, $branch)) {
  763. return true;
  764. }
  765. }
  766. }
  767. // Nothing matched, so filter this branch out.
  768. return false;
  769. }
  770. public static function extractBranchRegexp($pattern) {
  771. $matches = null;
  772. if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
  773. return $matches[1];
  774. }
  775. return null;
  776. }
  777. public function shouldTrackRef(DiffusionRepositoryRef $ref) {
  778. // At least for now, don't track the staging area tags.
  779. if ($ref->isTag()) {
  780. if (preg_match('(^phabricator/)', $ref->getShortName())) {
  781. return false;
  782. }
  783. }
  784. if (!$ref->isBranch()) {
  785. return true;
  786. }
  787. return $this->shouldTrackBranch($ref->getShortName());
  788. }
  789. public function shouldTrackBranch($branch) {
  790. return $this->isBranchInFilter($branch, 'branch-filter');
  791. }
  792. public function isBranchPermanentRef($branch) {
  793. return $this->isBranchInFilter($branch, 'close-commits-filter');
  794. }
  795. public function formatCommitName($commit_identifier, $local = false) {
  796. $vcs = $this->getVersionControlSystem();
  797. $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
  798. $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
  799. $is_git = ($vcs == $type_git);
  800. $is_hg = ($vcs == $type_hg);
  801. if ($is_git || $is_hg) {
  802. $name = substr($commit_identifier, 0, 12);
  803. $need_scope = false;
  804. } else {
  805. $name = $commit_identifier;
  806. $need_scope = true;
  807. }
  808. if (!$local) {
  809. $need_scope = true;
  810. }
  811. if ($need_scope) {
  812. $callsign = $this->getCallsign();
  813. if ($callsign) {
  814. $scope = "r{$callsign}";
  815. } else {
  816. $id = $this->getID();
  817. $scope = "R{$id}:";
  818. }
  819. $name = $scope.$name;
  820. }
  821. return $name;
  822. }
  823. public function isImporting() {
  824. return (bool)$this->getDetail('importing', false);
  825. }
  826. public function isNewlyInitialized() {
  827. return (bool)$this->getDetail('newly-initialized', false);
  828. }
  829. public function loadImportProgress() {
  830. $progress = queryfx_all(
  831. $this->establishConnection('r'),
  832. 'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
  833. GROUP BY importStatus',
  834. id(new PhabricatorRepositoryCommit())->getTableName(),
  835. $this->getID());
  836. $done = 0;
  837. $total = 0;
  838. foreach ($progress as $row) {
  839. $total += $row['N'] * 3;
  840. $status = $row['importStatus'];
  841. if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) {
  842. $done += $row['N'];
  843. }
  844. if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) {
  845. $done += $row['N'];
  846. }
  847. if ($status & PhabricatorRepositoryCommit::IMPORTED_PUBLISH) {
  848. $done += $row['N'];
  849. }
  850. }
  851. if ($total) {
  852. $ratio = ($done / $total);
  853. } else {
  854. $ratio = 0;
  855. }
  856. // Cap this at "99.99%", because it's confusing to users when the actual
  857. // fraction is "99.996%" and it rounds up to "100.00%".
  858. if ($ratio > 0.9999) {
  859. $ratio = 0.9999;
  860. }
  861. return $ratio;
  862. }
  863. /* -( Publishing )--------------------------------------------------------- */
  864. public function newPublisher() {
  865. return id(new PhabricatorRepositoryPublisher())
  866. ->setRepository($this);
  867. }
  868. public function isPublishingDisabled() {
  869. return $this->getDetail('herald-disabled');
  870. }
  871. public function getPermanentRefRules() {
  872. return array_keys($this->getDetail('close-commits-filter', array()));
  873. }
  874. public function setPermanentRefRules(array $rules) {
  875. $rules = array_fill_keys($rules, true);
  876. $this->setDetail('close-commits-filter', $rules);
  877. return $this;
  878. }
  879. public function getTrackOnlyRules() {
  880. return array_keys($this->getDetail('branch-filter', array()));
  881. }
  882. public function setTrackOnlyRules(array $rules) {
  883. $rules = array_fill_keys($rules, true);
  884. $this->setDetail('branch-filter', $rules);
  885. return $this;
  886. }
  887. public function supportsFetchRules() {
  888. if ($this->isGit()) {
  889. return true;
  890. }
  891. return false;
  892. }
  893. public function getFetchRules() {
  894. return $this->getDetail('fetch-rules', array());
  895. }
  896. public function setFetchRules(array $rules) {
  897. return $this->setDetail('fetch-rules', $rules);
  898. }
  899. /* -( Repository URI Management )------------------------------------------ */
  900. /**
  901. * Get the remote URI for this repository.
  902. *
  903. * @return string
  904. * @task uri
  905. */
  906. public function getRemoteURI() {
  907. return (string)$this->getRemoteURIObject();
  908. }
  909. /**
  910. * Get the remote URI for this repository, including credentials if they're
  911. * used by this repository.
  912. *
  913. * @return PhutilOpaqueEnvelope URI, possibly including credentials.
  914. * @task uri
  915. */
  916. public function getRemoteURIEnvelope() {
  917. $uri = $this->getRemoteURIObject();
  918. $remote_protocol = $this->getRemoteProtocol();
  919. if ($remote_protocol == 'http' || $remote_protocol == 'https') {
  920. // For SVN, we use `--username` and `--password` flags separately, so
  921. // don't add any credentials here.
  922. if (!$this->isSVN()) {
  923. $credential_phid = $this->getCredentialPHID();
  924. if ($credential_phid) {
  925. $key = PassphrasePasswordKey::loadFromPHID(
  926. $credential_phid,
  927. PhabricatorUser::getOmnipotentUser());
  928. $uri->setUser($key->getUsernameEnvelope()->openEnvelope());
  929. $uri->setPass($key->getPasswordEnvelope()->openEnvelope());
  930. }
  931. }
  932. }
  933. return new PhutilOpaqueEnvelope((string)$uri);
  934. }
  935. /**
  936. * Get the clone (or checkout) URI for this repository, without authentication
  937. * information.
  938. *
  939. * @return string Repository URI.
  940. * @task uri
  941. */
  942. public function getPublicCloneURI() {
  943. return (string)$this->getCloneURIObject();
  944. }
  945. /**
  946. * Get the protocol for the repository's remote.
  947. *
  948. * @return string Protocol, like "ssh" or "git".
  949. * @task uri
  950. */
  951. public function getRemoteProtocol() {
  952. $uri = $this->getRemoteURIObject();
  953. return $uri->getProtocol();
  954. }
  955. /**
  956. * Get a parsed object representation of the repository's remote URI..
  957. *
  958. * @return wild A @{class@libphutil:PhutilURI}.
  959. * @task uri
  960. */
  961. public function getRemoteURIObject() {
  962. $raw_uri = $this->getDetail('remote-uri');
  963. if (!strlen($raw_uri)) {
  964. return new PhutilURI('');
  965. }
  966. if (!strncmp($raw_uri, '/', 1)) {
  967. return new PhutilURI('file://'.$raw_uri);
  968. }
  969. return new PhutilURI($raw_uri);
  970. }
  971. /**
  972. * Get the "best" clone/checkout URI for this repository, on any protocol.
  973. */
  974. public function getCloneURIObject() {
  975. if (!$this->isHosted()) {
  976. if ($this->isSVN()) {
  977. // Make sure we pick up the "Import Only" path for Subversion, so
  978. // the user clones the repository starting at the correct path, not
  979. // from the root.
  980. $base_uri = $this->getSubversionBaseURI();
  981. $base_uri = new PhutilURI($base_uri);
  982. $path = $base_uri->getPath();
  983. if (!$path) {
  984. $path = '/';
  985. }
  986. // If the trailing "@" is not required to escape the URI, strip it for
  987. // readability.
  988. if (!preg_match('/@.*@/', $path)) {
  989. $path = rtrim($path, '@');
  990. }
  991. $base_uri->setPath($path);
  992. return $base_uri;
  993. } else {
  994. return $this->getRemoteURIObject();
  995. }
  996. }
  997. // TODO: This should be cleaned up to deal with all the new URI handling.
  998. $another_copy = id(new PhabricatorRepositoryQuery())
  999. ->setViewer(PhabricatorUser::getOmnipotentUser())
  1000. ->withPHIDs(array($this->getPHID()))
  1001. ->needURIs(true)
  1002. ->executeOne();
  1003. $clone_uris = $another_copy->getCloneURIs();
  1004. if (!$clone_uris) {
  1005. return null;
  1006. }
  1007. return head($clone_uris)->getEffectiveURI();
  1008. }
  1009. private function getRawHTTPCloneURIObject() {
  1010. $uri = PhabricatorEnv::getProductionURI($this->getURI());
  1011. $uri = new PhutilURI($uri);
  1012. if ($this->isGit()) {
  1013. $uri->setPath($uri->getPath().$this->getCloneName().'.git');
  1014. } else if ($this->isHg()) {
  1015. $uri->setPath($uri->getPath().$this->getCloneName().'/');
  1016. }
  1017. return $uri;
  1018. }
  1019. /**
  1020. * Determine if we should connect to the remote using SSH flags and
  1021. * credentials.
  1022. *
  1023. * @return bool True to use the SSH protocol.
  1024. * @task uri
  1025. */
  1026. private function shouldUseSSH() {
  1027. if ($this->isHosted()) {
  1028. return false;
  1029. }
  1030. $protocol = $this->getRemoteProtocol();
  1031. if ($this->isSSHProtocol($protocol)) {
  1032. return true;
  1033. }
  1034. return false;
  1035. }
  1036. /**
  1037. * Determine if we should connect to the remote using HTTP flags and
  1038. * credentials.
  1039. *
  1040. * @return bool True to use the HTTP protocol.
  1041. * @task uri
  1042. */
  1043. private function shouldUseHTTP() {
  1044. if ($this->isHosted()) {
  1045. return false;
  1046. }
  1047. $protocol = $this->getRemoteProtocol();
  1048. return ($protocol == 'http' || $protocol == 'https');
  1049. }
  1050. /**
  1051. * Determine if we should connect to the remote using SVN flags and
  1052. * credentials.
  1053. *
  1054. * @return bool True to use the SVN protocol.
  1055. * @task uri
  1056. */
  1057. private function shouldUseSVNProtocol() {
  1058. if ($this->isHosted()) {
  1059. return false;
  1060. }
  1061. $protocol = $this->getRemoteProtocol();
  1062. return ($protocol == 'svn');
  1063. }
  1064. /**
  1065. * Determine if a protocol is SSH or SSH-like.
  1066. *
  1067. * @param string A protocol string, like "http" or "ssh".
  1068. * @return bool True if the protocol is SSH-like.
  1069. * @task uri
  1070. */
  1071. private function isSSHProtocol($protocol) {
  1072. return ($protocol == 'ssh' || $protocol == 'svn+ssh');
  1073. }
  1074. public function delete() {
  1075. $this->openTransaction();
  1076. $paths = id(new PhabricatorOwnersPath())
  1077. ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
  1078. foreach ($paths as $path) {
  1079. $path->delete();
  1080. }
  1081. queryfx(
  1082. $this->establishConnection('w'),
  1083. 'DELETE FROM %T WHERE repositoryPHID = %s',
  1084. id(new PhabricatorRepositorySymbol())->getTableName(),
  1085. $this->getPHID());
  1086. $commits = id(new PhabricatorRepositoryCommit())
  1087. ->loadAllWhere('repositoryID = %d', $this->getID());
  1088. foreach ($commits as $commit) {
  1089. // note PhabricatorRepositoryAuditRequests and
  1090. // PhabricatorRepositoryCommitData are deleted here too.
  1091. $commit->delete();
  1092. }
  1093. $uris = id(new PhabricatorRepositoryURI())
  1094. ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
  1095. foreach ($uris as $uri) {
  1096. $uri->delete();
  1097. }
  1098. $ref_cursors = id(new PhabricatorRepositoryRefCursor())
  1099. ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
  1100. foreach ($ref_cursors as $cursor) {
  1101. $cursor->delete();
  1102. }
  1103. $conn_w = $this->establishConnection('w');
  1104. queryfx(
  1105. $conn_w,
  1106. 'DELETE FROM %T WHERE repositoryID = %d',
  1107. self::TABLE_FILESYSTEM,
  1108. $this->getID());
  1109. queryfx(
  1110. $conn_w,
  1111. 'DELETE FROM %T WHERE repositoryID = %d',
  1112. self::TABLE_PATHCHANGE,
  1113. $this->getID());
  1114. queryfx(
  1115. $conn_w,
  1116. 'DELETE FROM %T WHERE repositoryID = %d',
  1117. self::TABLE_SUMMARY,
  1118. $this->getID());
  1119. $result = parent::delete();
  1120. $this->saveTransaction();
  1121. return $result;
  1122. }
  1123. public function isGit() {
  1124. $vcs = $this->getVersionControlSystem();
  1125. return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
  1126. }
  1127. public function isSVN() {
  1128. $vcs = $this->getVersionControlSystem();
  1129. return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
  1130. }
  1131. public function isHg() {
  1132. $vcs = $this->getVersionControlSystem();
  1133. return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
  1134. }
  1135. public function isHosted() {
  1136. return (bool)$this->getDetail('hosting-enabled', false);
  1137. }
  1138. public function setHosted($enabled) {
  1139. return $this->setDetail('hosting-enabled', $enabled);
  1140. }
  1141. public function canServeProtocol(
  1142. $protocol,
  1143. $write,
  1144. $is_intracluster = false) {
  1145. // See T13192. If a repository is inactive, don't serve it to users. We
  1146. // still synchronize it within the cluster and serve it to other repository
  1147. // nodes.
  1148. if (!$is_intracluster) {
  1149. if (!$this->isTracked()) {
  1150. return false;
  1151. }
  1152. }
  1153. $clone_uris = $this->getCloneURIs();
  1154. foreach ($clone_uris as $uri) {
  1155. if ($uri->getBuiltinProtocol() !== $protocol) {
  1156. continue;
  1157. }
  1158. $io_type = $uri->getEffectiveIoType();
  1159. if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
  1160. return true;
  1161. }
  1162. if (!$write) {
  1163. if ($io_type == PhabricatorRepositoryURI::IO_READ) {
  1164. return true;
  1165. }
  1166. }
  1167. }
  1168. if ($write) {
  1169. if ($this->isReadOnly()) {
  1170. return false;
  1171. }
  1172. }
  1173. return false;
  1174. }
  1175. public function hasLocalWorkingCopy() {
  1176. try {
  1177. self::assertLocalExists();
  1178. return true;
  1179. } catch (Exception $ex) {
  1180. return false;
  1181. }
  1182. }
  1183. /**
  1184. * Raise more useful errors when there are basic filesystem problems.
  1185. */
  1186. private function assertLocalExists() {
  1187. if (!$this->usesLocalWorkingCopy()) {
  1188. return;
  1189. }
  1190. $local = $this->getLocalPath();
  1191. Filesystem::assertExists($local);
  1192. Filesystem::assertIsDirectory($local);
  1193. Filesystem::assertReadable($local);
  1194. }
  1195. /**
  1196. * Determine if the working copy is bare or not. In Git, this corresponds
  1197. * to `--bare`. In Mercurial, `--noupdate`.
  1198. */
  1199. public function isWorkingCopyBare() {
  1200. switch ($this->getVersionControlSystem()) {
  1201. case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
  1202. case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
  1203. return false;
  1204. case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
  1205. $local = $this->getLocalPath();
  1206. if (Filesystem::pathExists($local.'/.git')) {
  1207. return false;
  1208. } else {
  1209. return true;
  1210. }
  1211. }
  1212. }
  1213. public function usesLocalWorkingCopy() {
  1214. switch ($this->getVersionControlSystem()) {
  1215. case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
  1216. return $this->isHosted();
  1217. case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
  1218. case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
  1219. return true;
  1220. }
  1221. }
  1222. public function getHookDirectories() {
  1223. $directories = array();
  1224. if (!$this->isHosted()) {
  1225. return $directories;
  1226. }
  1227. $root = $this->getLocalPath();
  1228. switch ($this->getVersionControlSystem()) {
  1229. case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
  1230. if ($this->isWorkingCopyBare()) {
  1231. $directories[] = $root.'/hooks/pre-receive-phabricator.d/';
  1232. } else {
  1233. $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
  1234. }
  1235. break;
  1236. case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
  1237. $directories[] = $root.'/hooks/pre-commit-phabricator.d/';
  1238. break;
  1239. case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
  1240. // NOTE: We don't support custom Mercurial hooks for now because they're
  1241. // messy and we can't easily just drop a `hooks.d/` directory next to
  1242. // the hooks.
  1243. break;
  1244. }
  1245. return $directories;
  1246. }
  1247. public function canDestroyWorkingCopy() {
  1248. if ($this->isHosted()) {
  1249. // Never destroy hosted working copies.
  1250. return false;
  1251. }
  1252. $default_path = PhabricatorEnv::getEnvConfig(
  1253. 'repository.default-local-path');
  1254. return Filesystem::isDescendant($this->getLocalPath(), $default_path);
  1255. }
  1256. public function canUsePathTree() {
  1257. return !$this->isSVN();
  1258. }
  1259. public function canUseGitLFS() {
  1260. if (!$this->isGit()) {
  1261. return false;
  1262. }
  1263. if (!$this->isHosted()) {
  1264. return false;
  1265. }
  1266. if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) {
  1267. return false;
  1268. }
  1269. return true;
  1270. }
  1271. public function getGitLFSURI($path = null) {
  1272. if (!$this->canUseGitLFS()) {
  1273. throw new Exception(
  1274. pht(
  1275. 'This repository does not support Git LFS, so Git LFS URIs can '.
  1276. 'not be generated for it.'));
  1277. }
  1278. $uri = $this->getRawHTTPCloneURIObject();
  1279. $uri = (string)$uri;
  1280. $uri = $uri.'/'.$path;
  1281. return $uri;
  1282. }
  1283. public function canMirror() {
  1284. if ($this->isGit() || $this->isHg()) {
  1285. return true;
  1286. }
  1287. return false;
  1288. }
  1289. public function canAllowDangerousChanges() {
  1290. if (!$this->isHosted()) {
  1291. return false;
  1292. }
  1293. // In Git and Mercurial, ref deletions and rewrites are dangerous.
  1294. // In Subversion, editing revprops is dangerous.
  1295. return true;
  1296. }
  1297. public function shouldAllowDangerousChanges() {
  1298. return (bool)$this->getDetail('allow-dangerous-changes');
  1299. }
  1300. public function canAllowEnormousChanges() {
  1301. if (!$this->isHosted()) {
  1302. return false;
  1303. }
  1304. return true;
  1305. }
  1306. public function shouldAllowEnormousChanges() {
  1307. return (bool)$this->getDetail('allow-enormous-changes');
  1308. }
  1309. public function writeStatusMessage(
  1310. $status_type,
  1311. $status_code,
  1312. array $parameters = array()) {
  1313. $table = new PhabricatorRepositoryStatusMessage();
  1314. $conn_w = $table->establishConnection('w');
  1315. $table_name = $table->getTableName();
  1316. if ($status_code === null) {
  1317. queryfx(
  1318. $conn_w,
  1319. 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
  1320. $table_name,
  1321. $this->getID(),
  1322. $status_type);
  1323. } else {
  1324. // If the existing message has the same code (e.g., we just hit an
  1325. // error and also previously hit an error) we increment the message
  1326. // count. This allows us to determine how many times in a row we've
  1327. // run into an error.
  1328. // NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated
  1329. // in order, so the "messageCount" assignment must occur before the
  1330. // "statusCode" assignment. See T11705.
  1331. queryfx(
  1332. $conn_w,
  1333. 'INSERT INTO %T
  1334. (repositoryID, statusType, statusCode, parameters, epoch,
  1335. messageCount)
  1336. VALUES (%d, %s, %s, %s, %d, %d)
  1337. ON DUPLICATE KEY UPDATE
  1338. messageCount =
  1339. IF(
  1340. statusCode = VALUES(statusCode),
  1341. messageCount + VALUES(messageCount),
  1342. VALUES(messageCount)),
  1343. statusCode = VALUES(statusCode),
  1344. parameters = VALUES(parameters),
  1345. epoch = VALUES(epoch)',
  1346. $table_name,
  1347. $this->getID(),
  1348. $status_type,
  1349. $status_code,
  1350. json_encode($parameters),
  1351. time(),
  1352. 1);
  1353. }
  1354. return $this;
  1355. }
  1356. public static function assertValidRemoteURI($uri) {
  1357. if (trim($uri) != $uri) {
  1358. throw new Exception(
  1359. pht('The remote URI has leading or trailing whitespace.'));
  1360. }
  1361. $uri_object = new PhutilURI($uri);
  1362. $protocol = $uri_object->getProtocol();
  1363. // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
  1364. // for discussion. This is usually a user adding "ssh://" to an implicit
  1365. // SSH Git URI.
  1366. if ($protocol == 'ssh') {
  1367. if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
  1368. throw new Exception(
  1369. pht(
  1370. "The remote URI is not formatted correctly. Remote URIs ".
  1371. "with an explicit protocol should be in the form ".
  1372. "'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.",
  1373. 'proto://domain/path',
  1374. 'proto://domain:/path',
  1375. ':/path'));
  1376. }
  1377. }
  1378. switch ($protocol) {
  1379. case 'ssh':
  1380. case 'http':
  1381. case 'https':
  1382. case 'git':
  1383. case 'svn':
  1384. case 'svn+ssh':
  1385. break;
  1386. default:
  1387. // NOTE: We're explicitly rejecting 'file://' because it can be
  1388. // used to clone from the working copy of another repository on disk
  1389. // that you don't normally have permission to access.
  1390. throw new Exception(
  1391. pht(
  1392. 'The URI protocol is unrecognized. It should begin with '.
  1393. '"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".',
  1394. 'ssh://',
  1395. 'http://',
  1396. 'https://',
  1397. 'git://',
  1398. 'svn://',
  1399. 'svn+ssh://',
  1400. 'git@domain.com:path'));
  1401. }
  1402. return true;
  1403. }
  1404. /**
  1405. * Load the pull frequency for this repository, based on the time since the
  1406. * last activity.
  1407. *
  1408. * We pull rarely used repositories less frequently. This finds the most
  1409. * recent commit which is older than the current time (which prevents us from
  1410. * spinning on repositories with a silly commit post-dated to some time in
  1411. * 2037). We adjust the pull frequency based on when the most recent commit
  1412. * occurred.
  1413. *
  1414. * @param int The minimum update interval to use, in seconds.
  1415. * @return int Repository update interval, in seconds.
  1416. */
  1417. public function loadUpdateInterval($minimum = 15) {
  1418. // First, check if we've hit errors recently. If we have, wait one period
  1419. // for each consecutive error. Normally, this corresponds to a backoff of
  1420. // 15s, 30s, 45s, etc.
  1421. $message_table = new PhabricatorRepositoryStatusMessage();
  1422. $conn = $message_table->establishConnection('r');
  1423. $error_count = queryfx_one(
  1424. $conn,
  1425. 'SELECT MAX(messageCount) error_count FROM %T
  1426. WHERE repositoryID = %d
  1427. AND statusType IN (%Ls)
  1428. AND statusCode IN (%Ls)',
  1429. $message_table->getTableName(),
  1430. $this->getID(),
  1431. array(
  1432. PhabricatorRepositoryStatusMessage::TYPE_INIT,
  1433. PhabricatorRepositoryStatusMessage::TYPE_FETCH,
  1434. ),
  1435. array(
  1436. PhabricatorRepositoryStatusMessage::CODE_ERROR,
  1437. ));
  1438. $error_count = (int)$error_count['error_count'];
  1439. if ($error_count > 0) {
  1440. return (int)($minimum * $error_count);
  1441. }
  1442. // If a repository is still importing, always pull it as frequently as
  1443. // possible. This prevents us from hanging for a long time at 99.9% when
  1444. // importing an inactive repository.
  1445. if ($this->isImporting()) {
  1446. return $minimum;
  1447. }
  1448. $window_start = (PhabricatorTime::getNow() + $minimum);
  1449. $table = id(new PhabricatorRepositoryCommit());
  1450. $last_commit = queryfx_one(
  1451. $table->establishConnection('r'),
  1452. 'SELECT epoch FROM %T
  1453. WHERE repositoryID = %d AND epoch <= %d
  1454. ORDER BY epoch DESC LIMIT 1',
  1455. $table->getTableName(),
  1456. $this->getID(),
  1457. $window_start);
  1458. if ($last_commit) {
  1459. $time_since_commit = ($window_start - $last_commit['epoch']);
  1460. } else {
  1461. // If the repository has no commits, treat the creation date as
  1462. // though it were the date of the last commit. This makes empty
  1463. // repositories update quickly at first but slow down over time
  1464. // if they don't see any activity.
  1465. $time_since_commit = ($window_start - $this->getDateCreated());
  1466. }
  1467. $last_few_days = phutil_units('3 days in seconds');
  1468. if ($time_since_commit <= $last_few_days) {
  1469. // For repositories with activity in the recent past, we wait one
  1470. // extra second for every 10 minutes since the last commit. This
  1471. // shorter backoff is intended to handle weekends and other short
  1472. // breaks from development.
  1473. $smart_wait = ($time_since_commit / 600);
  1474. } else {
  1475. // For repositories without recent activity, we wait one extra second
  1476. // for every 4 minutes since the last commit. This longer backoff
  1477. // handles rarely used repositories, up to the maximum.
  1478. $smart_wait = ($time_since_commit / 240);
  1479. }
  1480. // We'll never wait more than 6 hours to pull a repository.
  1481. $longest_wait = phutil_units('6 hours in seconds');
  1482. $smart_wait = min($smart_wait, $longest_wait);
  1483. $smart_wait = max($minimum, $smart_wait);
  1484. return (int)$smart_wait;
  1485. }
  1486. /**
  1487. * Time limit for cloning or copying this repository.
  1488. *
  1489. * This limit is used to timeout operations like `git clone` or `git fetch`
  1490. * when doing intracluster synchronization, building working copies, etc.
  1491. *
  1492. * @return int Maximum number of seconds to spend copying this repository.
  1493. */
  1494. public function getCopyTimeLimit() {
  1495. return $this->getDetail('limit.copy');
  1496. }
  1497. public function setCopyTimeLimit($limit) {
  1498. return $this->setDetail('limit.copy', $limit);
  1499. }
  1500. public function getDefaultCopyTimeLimit() {
  1501. return phutil_units('15 minutes in seconds');
  1502. }
  1503. public function getEffectiveCopyTimeLimit() {
  1504. $limit = $this->getCopyTimeLimit();
  1505. if ($limit) {
  1506. return $limit;
  1507. }
  1508. return $this->getDefaultCopyTimeLimit();
  1509. }
  1510. public function getFilesizeLimit() {
  1511. return $this->getDetail('limit.filesize');
  1512. }
  1513. public function setFilesizeLimit($limit) {
  1514. return $this->setDetail('limit.filesize', $limit);
  1515. }
  1516. public function getTouchLimit() {
  1517. return $this->getDetail('limit.touch');
  1518. }
  1519. public function setTouchLimit($limit) {
  1520. return $this->setDetail('limit.touch', $limit);
  1521. }
  1522. /**
  1523. * Retrieve the service URI for the device hosting this repository.
  1524. *
  1525. * See @{method:newConduitClient} for a general discussion of interacting
  1526. * with repository services. This method provides lower-level resolution of
  1527. * services, returning raw URIs.
  1528. *
  1529. * @param PhabricatorUser Viewing user.
  1530. * @param map<string, wild> Constraints on selectable services.
  1531. * @return string|null URI, or `null` for local repositories.
  1532. */
  1533. public function getAlmanacServiceURI(
  1534. PhabricatorUser $viewer,
  1535. array $options) {
  1536. $refs = $this->getAlmanacServiceRefs($viewer, $options);
  1537. i

Large files files are truncated, but you can click here to view the full file