/src/applications/repository/storage/PhabricatorRepository.php
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
- <?php
- /**
- * @task uri Repository URI Management
- * @task publishing Publishing
- * @task sync Cluster Synchronization
- */
- final class PhabricatorRepository extends PhabricatorRepositoryDAO
- implements
- PhabricatorApplicationTransactionInterface,
- PhabricatorPolicyInterface,
- PhabricatorFlaggableInterface,
- PhabricatorMarkupInterface,
- PhabricatorDestructibleInterface,
- PhabricatorDestructibleCodexInterface,
- PhabricatorProjectInterface,
- PhabricatorSpacesInterface,
- PhabricatorConduitResultInterface,
- PhabricatorFulltextInterface,
- PhabricatorFerretInterface {
- /**
- * Shortest hash we'll recognize in raw "a829f32" form.
- */
- const MINIMUM_UNQUALIFIED_HASH = 7;
- /**
- * Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
- */
- const MINIMUM_QUALIFIED_HASH = 5;
- /**
- * Minimum number of commits to an empty repository to trigger "import" mode.
- */
- const IMPORT_THRESHOLD = 7;
- const LOWPRI_THRESHOLD = 64;
- const TABLE_PATH = 'repository_path';
- const TABLE_PATHCHANGE = 'repository_pathchange';
- const TABLE_FILESYSTEM = 'repository_filesystem';
- const TABLE_SUMMARY = 'repository_summary';
- const TABLE_LINTMESSAGE = 'repository_lintmessage';
- const TABLE_PARENTS = 'repository_parents';
- const TABLE_COVERAGE = 'repository_coverage';
- const STATUS_ACTIVE = 'active';
- const STATUS_INACTIVE = 'inactive';
- protected $name;
- protected $callsign;
- protected $repositorySlug;
- protected $uuid;
- protected $viewPolicy;
- protected $editPolicy;
- protected $pushPolicy;
- protected $profileImagePHID;
- protected $versionControlSystem;
- protected $details = array();
- protected $credentialPHID;
- protected $almanacServicePHID;
- protected $spacePHID;
- protected $localPath;
- private $commitCount = self::ATTACHABLE;
- private $mostRecentCommit = self::ATTACHABLE;
- private $projectPHIDs = self::ATTACHABLE;
- private $uris = self::ATTACHABLE;
- private $profileImageFile = self::ATTACHABLE;
- public static function initializeNewRepository(PhabricatorUser $actor) {
- $app = id(new PhabricatorApplicationQuery())
- ->setViewer($actor)
- ->withClasses(array('PhabricatorDiffusionApplication'))
- ->executeOne();
- $view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
- $edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
- $push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
- $repository = id(new PhabricatorRepository())
- ->setViewPolicy($view_policy)
- ->setEditPolicy($edit_policy)
- ->setPushPolicy($push_policy)
- ->setSpacePHID($actor->getDefaultSpacePHID());
- // Put the repository in "Importing" mode until we finish
- // parsing it.
- $repository->setDetail('importing', true);
- return $repository;
- }
- protected function getConfiguration() {
- return array(
- self::CONFIG_AUX_PHID => true,
- self::CONFIG_SERIALIZATION => array(
- 'details' => self::SERIALIZATION_JSON,
- ),
- self::CONFIG_COLUMN_SCHEMA => array(
- 'name' => 'sort255',
- 'callsign' => 'sort32?',
- 'repositorySlug' => 'sort64?',
- 'versionControlSystem' => 'text32',
- 'uuid' => 'text64?',
- 'pushPolicy' => 'policy',
- 'credentialPHID' => 'phid?',
- 'almanacServicePHID' => 'phid?',
- 'localPath' => 'text128?',
- 'profileImagePHID' => 'phid?',
- ),
- self::CONFIG_KEY_SCHEMA => array(
- 'callsign' => array(
- 'columns' => array('callsign'),
- 'unique' => true,
- ),
- 'key_name' => array(
- 'columns' => array('name(128)'),
- ),
- 'key_vcs' => array(
- 'columns' => array('versionControlSystem'),
- ),
- 'key_slug' => array(
- 'columns' => array('repositorySlug'),
- 'unique' => true,
- ),
- 'key_local' => array(
- 'columns' => array('localPath'),
- 'unique' => true,
- ),
- ),
- ) + parent::getConfiguration();
- }
- public function generatePHID() {
- return PhabricatorPHID::generateNewPHID(
- PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
- }
- public static function getStatusMap() {
- return array(
- self::STATUS_ACTIVE => array(
- 'name' => pht('Active'),
- 'isTracked' => 1,
- ),
- self::STATUS_INACTIVE => array(
- 'name' => pht('Inactive'),
- 'isTracked' => 0,
- ),
- );
- }
- public static function getStatusNameMap() {
- return ipull(self::getStatusMap(), 'name');
- }
- public function getStatus() {
- if ($this->isTracked()) {
- return self::STATUS_ACTIVE;
- } else {
- return self::STATUS_INACTIVE;
- }
- }
- public function toDictionary() {
- return array(
- 'id' => $this->getID(),
- 'name' => $this->getName(),
- 'phid' => $this->getPHID(),
- 'callsign' => $this->getCallsign(),
- 'monogram' => $this->getMonogram(),
- 'vcs' => $this->getVersionControlSystem(),
- 'uri' => PhabricatorEnv::getProductionURI($this->getURI()),
- 'remoteURI' => (string)$this->getRemoteURI(),
- 'description' => $this->getDetail('description'),
- 'isActive' => $this->isTracked(),
- 'isHosted' => $this->isHosted(),
- 'isImporting' => $this->isImporting(),
- 'encoding' => $this->getDefaultTextEncoding(),
- 'staging' => array(
- 'supported' => $this->supportsStaging(),
- 'prefix' => 'phabricator',
- 'uri' => $this->getStagingURI(),
- ),
- );
- }
- public function getDefaultTextEncoding() {
- return $this->getDetail('encoding', 'UTF-8');
- }
- public function getMonogram() {
- $callsign = $this->getCallsign();
- if (strlen($callsign)) {
- return "r{$callsign}";
- }
- $id = $this->getID();
- return "R{$id}";
- }
- public function getDisplayName() {
- $slug = $this->getRepositorySlug();
- if (strlen($slug)) {
- return $slug;
- }
- return $this->getMonogram();
- }
- public function getAllMonograms() {
- $monograms = array();
- $monograms[] = 'R'.$this->getID();
- $callsign = $this->getCallsign();
- if (strlen($callsign)) {
- $monograms[] = 'r'.$callsign;
- }
- return $monograms;
- }
- public function setLocalPath($path) {
- // Convert any extra slashes ("//") in the path to a single slash ("/").
- $path = preg_replace('(//+)', '/', $path);
- return parent::setLocalPath($path);
- }
- public function getDetail($key, $default = null) {
- return idx($this->details, $key, $default);
- }
- public function setDetail($key, $value) {
- $this->details[$key] = $value;
- return $this;
- }
- public function attachCommitCount($count) {
- $this->commitCount = $count;
- return $this;
- }
- public function getCommitCount() {
- return $this->assertAttached($this->commitCount);
- }
- public function attachMostRecentCommit(
- PhabricatorRepositoryCommit $commit = null) {
- $this->mostRecentCommit = $commit;
- return $this;
- }
- public function getMostRecentCommit() {
- return $this->assertAttached($this->mostRecentCommit);
- }
- public function getDiffusionBrowseURIForPath(
- PhabricatorUser $user,
- $path,
- $line = null,
- $branch = null) {
- $drequest = DiffusionRequest::newFromDictionary(
- array(
- 'user' => $user,
- 'repository' => $this,
- 'path' => $path,
- 'branch' => $branch,
- ));
- return $drequest->generateURI(
- array(
- 'action' => 'browse',
- 'line' => $line,
- ));
- }
- public function getSubversionBaseURI($commit = null) {
- $subpath = $this->getDetail('svn-subpath');
- if (!strlen($subpath)) {
- $subpath = null;
- }
- return $this->getSubversionPathURI($subpath, $commit);
- }
- public function getSubversionPathURI($path = null, $commit = null) {
- $vcs = $this->getVersionControlSystem();
- if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
- throw new Exception(pht('Not a subversion repository!'));
- }
- if ($this->isHosted()) {
- $uri = 'file://'.$this->getLocalPath();
- } else {
- $uri = $this->getDetail('remote-uri');
- }
- $uri = rtrim($uri, '/');
- if (strlen($path)) {
- $path = rawurlencode($path);
- $path = str_replace('%2F', '/', $path);
- $uri = $uri.'/'.ltrim($path, '/');
- }
- if ($path !== null || $commit !== null) {
- $uri .= '@';
- }
- if ($commit !== null) {
- $uri .= $commit;
- }
- return $uri;
- }
- public function attachProjectPHIDs(array $project_phids) {
- $this->projectPHIDs = $project_phids;
- return $this;
- }
- public function getProjectPHIDs() {
- return $this->assertAttached($this->projectPHIDs);
- }
- /**
- * Get the name of the directory this repository should clone or checkout
- * into. For example, if the repository name is "Example Repository", a
- * reasonable name might be "example-repository". This is used to help users
- * get reasonable results when cloning repositories, since they generally do
- * not want to clone into directories called "X/" or "Example Repository/".
- *
- * @return string
- */
- public function getCloneName() {
- $name = $this->getRepositorySlug();
- // Make some reasonable effort to produce reasonable default directory
- // names from repository names.
- if (!strlen($name)) {
- $name = $this->getName();
- $name = phutil_utf8_strtolower($name);
- $name = preg_replace('@[ -/:->]+@', '-', $name);
- $name = trim($name, '-');
- if (!strlen($name)) {
- $name = $this->getCallsign();
- }
- }
- return $name;
- }
- public static function isValidRepositorySlug($slug) {
- try {
- self::assertValidRepositorySlug($slug);
- return true;
- } catch (Exception $ex) {
- return false;
- }
- }
- public static function assertValidRepositorySlug($slug) {
- if (!strlen($slug)) {
- throw new Exception(
- pht(
- 'The empty string is not a valid repository short name. '.
- 'Repository short names must be at least one character long.'));
- }
- if (strlen($slug) > 64) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names must not be longer than 64 characters.',
- $slug));
- }
- if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names may only contain letters, numbers, periods, hyphens '.
- 'and underscores.',
- $slug));
- }
- if (!preg_match('/^[a-zA-Z0-9]/', $slug)) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names must begin with a letter or number.',
- $slug));
- }
- if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names must end with a letter or number.',
- $slug));
- }
- if (preg_match('/__|--|\\.\\./', $slug)) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names must not contain multiple consecutive underscores, '.
- 'hyphens, or periods.',
- $slug));
- }
- if (preg_match('/^[A-Z]+\z/', $slug)) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names may not contain only uppercase letters.',
- $slug));
- }
- if (preg_match('/^\d+\z/', $slug)) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names may not contain only numbers.',
- $slug));
- }
- if (preg_match('/\\.git/', $slug)) {
- throw new Exception(
- pht(
- 'The name "%s" is not a valid repository short name. Repository '.
- 'short names must not end in ".git". This suffix will be added '.
- 'automatically in appropriate contexts.',
- $slug));
- }
- }
- public static function assertValidCallsign($callsign) {
- if (!strlen($callsign)) {
- throw new Exception(
- pht(
- 'A repository callsign must be at least one character long.'));
- }
- if (strlen($callsign) > 32) {
- throw new Exception(
- pht(
- 'The callsign "%s" is not a valid repository callsign. Callsigns '.
- 'must be no more than 32 bytes long.',
- $callsign));
- }
- if (!preg_match('/^[A-Z]+\z/', $callsign)) {
- throw new Exception(
- pht(
- 'The callsign "%s" is not a valid repository callsign. Callsigns '.
- 'may only contain UPPERCASE letters.',
- $callsign));
- }
- }
- public function getProfileImageURI() {
- return $this->getProfileImageFile()->getBestURI();
- }
- public function attachProfileImageFile(PhabricatorFile $file) {
- $this->profileImageFile = $file;
- return $this;
- }
- public function getProfileImageFile() {
- return $this->assertAttached($this->profileImageFile);
- }
- /* -( Remote Command Execution )------------------------------------------- */
- public function execRemoteCommand($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newRemoteCommandFuture($args)->resolve();
- }
- public function execxRemoteCommand($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newRemoteCommandFuture($args)->resolvex();
- }
- public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newRemoteCommandFuture($args);
- }
- public function passthruRemoteCommand($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newRemoteCommandPassthru($args)->execute();
- }
- private function newRemoteCommandFuture(array $argv) {
- return $this->newRemoteCommandEngine($argv)
- ->newFuture();
- }
- private function newRemoteCommandPassthru(array $argv) {
- return $this->newRemoteCommandEngine($argv)
- ->setPassthru(true)
- ->newFuture();
- }
- private function newRemoteCommandEngine(array $argv) {
- return DiffusionCommandEngine::newCommandEngine($this)
- ->setArgv($argv)
- ->setCredentialPHID($this->getCredentialPHID())
- ->setURI($this->getRemoteURIObject());
- }
- /* -( Local Command Execution )-------------------------------------------- */
- public function execLocalCommand($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newLocalCommandFuture($args)->resolve();
- }
- public function execxLocalCommand($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newLocalCommandFuture($args)->resolvex();
- }
- public function getLocalCommandFuture($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newLocalCommandFuture($args);
- }
- public function passthruLocalCommand($pattern /* , $arg, ... */) {
- $args = func_get_args();
- return $this->newLocalCommandPassthru($args)->execute();
- }
- private function newLocalCommandFuture(array $argv) {
- $this->assertLocalExists();
- $future = DiffusionCommandEngine::newCommandEngine($this)
- ->setArgv($argv)
- ->newFuture();
- if ($this->usesLocalWorkingCopy()) {
- $future->setCWD($this->getLocalPath());
- }
- return $future;
- }
- private function newLocalCommandPassthru(array $argv) {
- $this->assertLocalExists();
- $future = DiffusionCommandEngine::newCommandEngine($this)
- ->setArgv($argv)
- ->setPassthru(true)
- ->newFuture();
- if ($this->usesLocalWorkingCopy()) {
- $future->setCWD($this->getLocalPath());
- }
- return $future;
- }
- public function getURI() {
- $short_name = $this->getRepositorySlug();
- if (strlen($short_name)) {
- return "/source/{$short_name}/";
- }
- $callsign = $this->getCallsign();
- if (strlen($callsign)) {
- return "/diffusion/{$callsign}/";
- }
- $id = $this->getID();
- return "/diffusion/{$id}/";
- }
- public function getPathURI($path) {
- return $this->getURI().ltrim($path, '/');
- }
- public function getCommitURI($identifier) {
- $callsign = $this->getCallsign();
- if (strlen($callsign)) {
- return "/r{$callsign}{$identifier}";
- }
- $id = $this->getID();
- return "/R{$id}:{$identifier}";
- }
- public static function parseRepositoryServicePath($request_path, $vcs) {
- $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
- $patterns = array(
- '(^'.
- '(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'.
- '(?P<path>.*)'.
- '\z)',
- );
- $identifier = null;
- foreach ($patterns as $pattern) {
- $matches = null;
- if (!preg_match($pattern, $request_path, $matches)) {
- continue;
- }
- $identifier = $matches['identifier'];
- if ($is_git) {
- $identifier = preg_replace('/\\.git\z/', '', $identifier);
- }
- $base = $matches['base'];
- $path = $matches['path'];
- break;
- }
- if ($identifier === null) {
- return null;
- }
- return array(
- 'identifier' => $identifier,
- 'base' => $base,
- 'path' => $path,
- );
- }
- public function getCanonicalPath($request_path) {
- $standard_pattern =
- '(^'.
- '(?P<prefix>/(?:diffusion|source)/)'.
- '(?P<identifier>[^/]+)'.
- '(?P<suffix>(?:/.*)?)'.
- '\z)';
- $matches = null;
- if (preg_match($standard_pattern, $request_path, $matches)) {
- $suffix = $matches['suffix'];
- return $this->getPathURI($suffix);
- }
- $commit_pattern =
- '(^'.
- '(?P<prefix>/)'.
- '(?P<monogram>'.
- '(?:'.
- 'r(?P<repositoryCallsign>[A-Z]+)'.
- '|'.
- 'R(?P<repositoryID>[1-9]\d*):'.
- ')'.
- '(?P<commit>[a-f0-9]+)'.
- ')'.
- '\z)';
- $matches = null;
- if (preg_match($commit_pattern, $request_path, $matches)) {
- $commit = $matches['commit'];
- return $this->getCommitURI($commit);
- }
- return null;
- }
- public function generateURI(array $params) {
- $req_branch = false;
- $req_commit = false;
- $action = idx($params, 'action');
- switch ($action) {
- case 'history':
- case 'graph':
- case 'clone':
- case 'blame':
- case 'browse':
- case 'document':
- case 'change':
- case 'lastmodified':
- case 'tags':
- case 'branches':
- case 'lint':
- case 'pathtree':
- case 'refs':
- case 'compare':
- break;
- case 'branch':
- // NOTE: This does not actually require a branch, and won't have one
- // in Subversion. Possibly this should be more clear.
- break;
- case 'commit':
- case 'rendering-ref':
- $req_commit = true;
- break;
- default:
- throw new Exception(
- pht(
- 'Action "%s" is not a valid repository URI action.',
- $action));
- }
- $path = idx($params, 'path');
- $branch = idx($params, 'branch');
- $commit = idx($params, 'commit');
- $line = idx($params, 'line');
- $head = idx($params, 'head');
- $against = idx($params, 'against');
- if ($req_commit && !strlen($commit)) {
- throw new Exception(
- pht(
- 'Diffusion URI action "%s" requires commit!',
- $action));
- }
- if ($req_branch && !strlen($branch)) {
- throw new Exception(
- pht(
- 'Diffusion URI action "%s" requires branch!',
- $action));
- }
- if ($action === 'commit') {
- return $this->getCommitURI($commit);
- }
- if (strlen($path)) {
- $path = ltrim($path, '/');
- $path = str_replace(array(';', '$'), array(';;', '$$'), $path);
- $path = phutil_escape_uri($path);
- }
- $raw_branch = $branch;
- if (strlen($branch)) {
- $branch = phutil_escape_uri_path_component($branch);
- $path = "{$branch}/{$path}";
- }
- $raw_commit = $commit;
- if (strlen($commit)) {
- $commit = str_replace('$', '$$', $commit);
- $commit = ';'.phutil_escape_uri($commit);
- }
- if (strlen($line)) {
- $line = '$'.phutil_escape_uri($line);
- }
- $query = array();
- switch ($action) {
- case 'change':
- case 'history':
- case 'graph':
- case 'blame':
- case 'browse':
- case 'document':
- case 'lastmodified':
- case 'tags':
- case 'branches':
- case 'lint':
- case 'pathtree':
- case 'refs':
- $uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}");
- break;
- case 'compare':
- $uri = $this->getPathURI("/{$action}/");
- if (strlen($head)) {
- $query['head'] = $head;
- } else if (strlen($raw_commit)) {
- $query['commit'] = $raw_commit;
- } else if (strlen($raw_branch)) {
- $query['head'] = $raw_branch;
- }
- if (strlen($against)) {
- $query['against'] = $against;
- }
- break;
- case 'branch':
- if (strlen($path)) {
- $uri = $this->getPathURI("/repository/{$path}");
- } else {
- $uri = $this->getPathURI('/');
- }
- break;
- case 'external':
- $commit = ltrim($commit, ';');
- $uri = "/diffusion/external/{$commit}/";
- break;
- case 'rendering-ref':
- // This isn't a real URI per se, it's passed as a query parameter to
- // the ajax changeset stuff but then we parse it back out as though
- // it came from a URI.
- $uri = rawurldecode("{$path}{$commit}");
- break;
- case 'clone':
- $uri = $this->getPathURI("/{$action}/");
- break;
- }
- if ($action == 'rendering-ref') {
- return $uri;
- }
- if (isset($params['lint'])) {
- $params['params'] = idx($params, 'params', array()) + array(
- 'lint' => $params['lint'],
- );
- }
- $query = idx($params, 'params', array()) + $query;
- return new PhutilURI($uri, $query);
- }
- public function updateURIIndex() {
- $indexes = array();
- $uris = $this->getURIs();
- foreach ($uris as $uri) {
- if ($uri->getIsDisabled()) {
- continue;
- }
- $indexes[] = $uri->getNormalizedURI();
- }
- PhabricatorRepositoryURIIndex::updateRepositoryURIs(
- $this->getPHID(),
- $indexes);
- return $this;
- }
- public function isTracked() {
- $status = $this->getDetail('tracking-enabled');
- $map = self::getStatusMap();
- $spec = idx($map, $status);
- if (!$spec) {
- if ($status) {
- $status = self::STATUS_ACTIVE;
- } else {
- $status = self::STATUS_INACTIVE;
- }
- $spec = idx($map, $status);
- }
- return (bool)idx($spec, 'isTracked', false);
- }
- public function getDefaultBranch() {
- $default = $this->getDetail('default-branch');
- if (strlen($default)) {
- return $default;
- }
- $default_branches = array(
- PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
- PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
- );
- return idx($default_branches, $this->getVersionControlSystem());
- }
- public function getDefaultArcanistBranch() {
- return coalesce($this->getDefaultBranch(), 'svn');
- }
- private function isBranchInFilter($branch, $filter_key) {
- $vcs = $this->getVersionControlSystem();
- $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
- $use_filter = ($is_git);
- if (!$use_filter) {
- // If this VCS doesn't use filters, pass everything through.
- return true;
- }
- $filter = $this->getDetail($filter_key, array());
- // If there's no filter set, let everything through.
- if (!$filter) {
- return true;
- }
- // If this branch isn't literally named `regexp(...)`, and it's in the
- // filter list, let it through.
- if (isset($filter[$branch])) {
- if (self::extractBranchRegexp($branch) === null) {
- return true;
- }
- }
- // If the branch matches a regexp, let it through.
- foreach ($filter as $pattern => $ignored) {
- $regexp = self::extractBranchRegexp($pattern);
- if ($regexp !== null) {
- if (preg_match($regexp, $branch)) {
- return true;
- }
- }
- }
- // Nothing matched, so filter this branch out.
- return false;
- }
- public static function extractBranchRegexp($pattern) {
- $matches = null;
- if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
- return $matches[1];
- }
- return null;
- }
- public function shouldTrackRef(DiffusionRepositoryRef $ref) {
- // At least for now, don't track the staging area tags.
- if ($ref->isTag()) {
- if (preg_match('(^phabricator/)', $ref->getShortName())) {
- return false;
- }
- }
- if (!$ref->isBranch()) {
- return true;
- }
- return $this->shouldTrackBranch($ref->getShortName());
- }
- public function shouldTrackBranch($branch) {
- return $this->isBranchInFilter($branch, 'branch-filter');
- }
- public function isBranchPermanentRef($branch) {
- return $this->isBranchInFilter($branch, 'close-commits-filter');
- }
- public function formatCommitName($commit_identifier, $local = false) {
- $vcs = $this->getVersionControlSystem();
- $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
- $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
- $is_git = ($vcs == $type_git);
- $is_hg = ($vcs == $type_hg);
- if ($is_git || $is_hg) {
- $name = substr($commit_identifier, 0, 12);
- $need_scope = false;
- } else {
- $name = $commit_identifier;
- $need_scope = true;
- }
- if (!$local) {
- $need_scope = true;
- }
- if ($need_scope) {
- $callsign = $this->getCallsign();
- if ($callsign) {
- $scope = "r{$callsign}";
- } else {
- $id = $this->getID();
- $scope = "R{$id}:";
- }
- $name = $scope.$name;
- }
- return $name;
- }
- public function isImporting() {
- return (bool)$this->getDetail('importing', false);
- }
- public function isNewlyInitialized() {
- return (bool)$this->getDetail('newly-initialized', false);
- }
- public function loadImportProgress() {
- $progress = queryfx_all(
- $this->establishConnection('r'),
- 'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
- GROUP BY importStatus',
- id(new PhabricatorRepositoryCommit())->getTableName(),
- $this->getID());
- $done = 0;
- $total = 0;
- foreach ($progress as $row) {
- $total += $row['N'] * 3;
- $status = $row['importStatus'];
- if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) {
- $done += $row['N'];
- }
- if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) {
- $done += $row['N'];
- }
- if ($status & PhabricatorRepositoryCommit::IMPORTED_PUBLISH) {
- $done += $row['N'];
- }
- }
- if ($total) {
- $ratio = ($done / $total);
- } else {
- $ratio = 0;
- }
- // Cap this at "99.99%", because it's confusing to users when the actual
- // fraction is "99.996%" and it rounds up to "100.00%".
- if ($ratio > 0.9999) {
- $ratio = 0.9999;
- }
- return $ratio;
- }
- /* -( Publishing )--------------------------------------------------------- */
- public function newPublisher() {
- return id(new PhabricatorRepositoryPublisher())
- ->setRepository($this);
- }
- public function isPublishingDisabled() {
- return $this->getDetail('herald-disabled');
- }
- public function getPermanentRefRules() {
- return array_keys($this->getDetail('close-commits-filter', array()));
- }
- public function setPermanentRefRules(array $rules) {
- $rules = array_fill_keys($rules, true);
- $this->setDetail('close-commits-filter', $rules);
- return $this;
- }
- public function getTrackOnlyRules() {
- return array_keys($this->getDetail('branch-filter', array()));
- }
- public function setTrackOnlyRules(array $rules) {
- $rules = array_fill_keys($rules, true);
- $this->setDetail('branch-filter', $rules);
- return $this;
- }
- public function supportsFetchRules() {
- if ($this->isGit()) {
- return true;
- }
- return false;
- }
- public function getFetchRules() {
- return $this->getDetail('fetch-rules', array());
- }
- public function setFetchRules(array $rules) {
- return $this->setDetail('fetch-rules', $rules);
- }
- /* -( Repository URI Management )------------------------------------------ */
- /**
- * Get the remote URI for this repository.
- *
- * @return string
- * @task uri
- */
- public function getRemoteURI() {
- return (string)$this->getRemoteURIObject();
- }
- /**
- * Get the remote URI for this repository, including credentials if they're
- * used by this repository.
- *
- * @return PhutilOpaqueEnvelope URI, possibly including credentials.
- * @task uri
- */
- public function getRemoteURIEnvelope() {
- $uri = $this->getRemoteURIObject();
- $remote_protocol = $this->getRemoteProtocol();
- if ($remote_protocol == 'http' || $remote_protocol == 'https') {
- // For SVN, we use `--username` and `--password` flags separately, so
- // don't add any credentials here.
- if (!$this->isSVN()) {
- $credential_phid = $this->getCredentialPHID();
- if ($credential_phid) {
- $key = PassphrasePasswordKey::loadFromPHID(
- $credential_phid,
- PhabricatorUser::getOmnipotentUser());
- $uri->setUser($key->getUsernameEnvelope()->openEnvelope());
- $uri->setPass($key->getPasswordEnvelope()->openEnvelope());
- }
- }
- }
- return new PhutilOpaqueEnvelope((string)$uri);
- }
- /**
- * Get the clone (or checkout) URI for this repository, without authentication
- * information.
- *
- * @return string Repository URI.
- * @task uri
- */
- public function getPublicCloneURI() {
- return (string)$this->getCloneURIObject();
- }
- /**
- * Get the protocol for the repository's remote.
- *
- * @return string Protocol, like "ssh" or "git".
- * @task uri
- */
- public function getRemoteProtocol() {
- $uri = $this->getRemoteURIObject();
- return $uri->getProtocol();
- }
- /**
- * Get a parsed object representation of the repository's remote URI..
- *
- * @return wild A @{class@libphutil:PhutilURI}.
- * @task uri
- */
- public function getRemoteURIObject() {
- $raw_uri = $this->getDetail('remote-uri');
- if (!strlen($raw_uri)) {
- return new PhutilURI('');
- }
- if (!strncmp($raw_uri, '/', 1)) {
- return new PhutilURI('file://'.$raw_uri);
- }
- return new PhutilURI($raw_uri);
- }
- /**
- * Get the "best" clone/checkout URI for this repository, on any protocol.
- */
- public function getCloneURIObject() {
- if (!$this->isHosted()) {
- if ($this->isSVN()) {
- // Make sure we pick up the "Import Only" path for Subversion, so
- // the user clones the repository starting at the correct path, not
- // from the root.
- $base_uri = $this->getSubversionBaseURI();
- $base_uri = new PhutilURI($base_uri);
- $path = $base_uri->getPath();
- if (!$path) {
- $path = '/';
- }
- // If the trailing "@" is not required to escape the URI, strip it for
- // readability.
- if (!preg_match('/@.*@/', $path)) {
- $path = rtrim($path, '@');
- }
- $base_uri->setPath($path);
- return $base_uri;
- } else {
- return $this->getRemoteURIObject();
- }
- }
- // TODO: This should be cleaned up to deal with all the new URI handling.
- $another_copy = id(new PhabricatorRepositoryQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPHIDs(array($this->getPHID()))
- ->needURIs(true)
- ->executeOne();
- $clone_uris = $another_copy->getCloneURIs();
- if (!$clone_uris) {
- return null;
- }
- return head($clone_uris)->getEffectiveURI();
- }
- private function getRawHTTPCloneURIObject() {
- $uri = PhabricatorEnv::getProductionURI($this->getURI());
- $uri = new PhutilURI($uri);
- if ($this->isGit()) {
- $uri->setPath($uri->getPath().$this->getCloneName().'.git');
- } else if ($this->isHg()) {
- $uri->setPath($uri->getPath().$this->getCloneName().'/');
- }
- return $uri;
- }
- /**
- * Determine if we should connect to the remote using SSH flags and
- * credentials.
- *
- * @return bool True to use the SSH protocol.
- * @task uri
- */
- private function shouldUseSSH() {
- if ($this->isHosted()) {
- return false;
- }
- $protocol = $this->getRemoteProtocol();
- if ($this->isSSHProtocol($protocol)) {
- return true;
- }
- return false;
- }
- /**
- * Determine if we should connect to the remote using HTTP flags and
- * credentials.
- *
- * @return bool True to use the HTTP protocol.
- * @task uri
- */
- private function shouldUseHTTP() {
- if ($this->isHosted()) {
- return false;
- }
- $protocol = $this->getRemoteProtocol();
- return ($protocol == 'http' || $protocol == 'https');
- }
- /**
- * Determine if we should connect to the remote using SVN flags and
- * credentials.
- *
- * @return bool True to use the SVN protocol.
- * @task uri
- */
- private function shouldUseSVNProtocol() {
- if ($this->isHosted()) {
- return false;
- }
- $protocol = $this->getRemoteProtocol();
- return ($protocol == 'svn');
- }
- /**
- * Determine if a protocol is SSH or SSH-like.
- *
- * @param string A protocol string, like "http" or "ssh".
- * @return bool True if the protocol is SSH-like.
- * @task uri
- */
- private function isSSHProtocol($protocol) {
- return ($protocol == 'ssh' || $protocol == 'svn+ssh');
- }
- public function delete() {
- $this->openTransaction();
- $paths = id(new PhabricatorOwnersPath())
- ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
- foreach ($paths as $path) {
- $path->delete();
- }
- queryfx(
- $this->establishConnection('w'),
- 'DELETE FROM %T WHERE repositoryPHID = %s',
- id(new PhabricatorRepositorySymbol())->getTableName(),
- $this->getPHID());
- $commits = id(new PhabricatorRepositoryCommit())
- ->loadAllWhere('repositoryID = %d', $this->getID());
- foreach ($commits as $commit) {
- // note PhabricatorRepositoryAuditRequests and
- // PhabricatorRepositoryCommitData are deleted here too.
- $commit->delete();
- }
- $uris = id(new PhabricatorRepositoryURI())
- ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
- foreach ($uris as $uri) {
- $uri->delete();
- }
- $ref_cursors = id(new PhabricatorRepositoryRefCursor())
- ->loadAllWhere('repositoryPHID = %s', $this->getPHID());
- foreach ($ref_cursors as $cursor) {
- $cursor->delete();
- }
- $conn_w = $this->establishConnection('w');
- queryfx(
- $conn_w,
- 'DELETE FROM %T WHERE repositoryID = %d',
- self::TABLE_FILESYSTEM,
- $this->getID());
- queryfx(
- $conn_w,
- 'DELETE FROM %T WHERE repositoryID = %d',
- self::TABLE_PATHCHANGE,
- $this->getID());
- queryfx(
- $conn_w,
- 'DELETE FROM %T WHERE repositoryID = %d',
- self::TABLE_SUMMARY,
- $this->getID());
- $result = parent::delete();
- $this->saveTransaction();
- return $result;
- }
- public function isGit() {
- $vcs = $this->getVersionControlSystem();
- return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
- }
- public function isSVN() {
- $vcs = $this->getVersionControlSystem();
- return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
- }
- public function isHg() {
- $vcs = $this->getVersionControlSystem();
- return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
- }
- public function isHosted() {
- return (bool)$this->getDetail('hosting-enabled', false);
- }
- public function setHosted($enabled) {
- return $this->setDetail('hosting-enabled', $enabled);
- }
- public function canServeProtocol(
- $protocol,
- $write,
- $is_intracluster = false) {
- // See T13192. If a repository is inactive, don't serve it to users. We
- // still synchronize it within the cluster and serve it to other repository
- // nodes.
- if (!$is_intracluster) {
- if (!$this->isTracked()) {
- return false;
- }
- }
- $clone_uris = $this->getCloneURIs();
- foreach ($clone_uris as $uri) {
- if ($uri->getBuiltinProtocol() !== $protocol) {
- continue;
- }
- $io_type = $uri->getEffectiveIoType();
- if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
- return true;
- }
- if (!$write) {
- if ($io_type == PhabricatorRepositoryURI::IO_READ) {
- return true;
- }
- }
- }
- if ($write) {
- if ($this->isReadOnly()) {
- return false;
- }
- }
- return false;
- }
- public function hasLocalWorkingCopy() {
- try {
- self::assertLocalExists();
- return true;
- } catch (Exception $ex) {
- return false;
- }
- }
- /**
- * Raise more useful errors when there are basic filesystem problems.
- */
- private function assertLocalExists() {
- if (!$this->usesLocalWorkingCopy()) {
- return;
- }
- $local = $this->getLocalPath();
- Filesystem::assertExists($local);
- Filesystem::assertIsDirectory($local);
- Filesystem::assertReadable($local);
- }
- /**
- * Determine if the working copy is bare or not. In Git, this corresponds
- * to `--bare`. In Mercurial, `--noupdate`.
- */
- public function isWorkingCopyBare() {
- switch ($this->getVersionControlSystem()) {
- case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
- case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
- return false;
- case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
- $local = $this->getLocalPath();
- if (Filesystem::pathExists($local.'/.git')) {
- return false;
- } else {
- return true;
- }
- }
- }
- public function usesLocalWorkingCopy() {
- switch ($this->getVersionControlSystem()) {
- case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
- return $this->isHosted();
- case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
- case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
- return true;
- }
- }
- public function getHookDirectories() {
- $directories = array();
- if (!$this->isHosted()) {
- return $directories;
- }
- $root = $this->getLocalPath();
- switch ($this->getVersionControlSystem()) {
- case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
- if ($this->isWorkingCopyBare()) {
- $directories[] = $root.'/hooks/pre-receive-phabricator.d/';
- } else {
- $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
- }
- break;
- case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
- $directories[] = $root.'/hooks/pre-commit-phabricator.d/';
- break;
- case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
- // NOTE: We don't support custom Mercurial hooks for now because they're
- // messy and we can't easily just drop a `hooks.d/` directory next to
- // the hooks.
- break;
- }
- return $directories;
- }
- public function canDestroyWorkingCopy() {
- if ($this->isHosted()) {
- // Never destroy hosted working copies.
- return false;
- }
- $default_path = PhabricatorEnv::getEnvConfig(
- 'repository.default-local-path');
- return Filesystem::isDescendant($this->getLocalPath(), $default_path);
- }
- public function canUsePathTree() {
- return !$this->isSVN();
- }
- public function canUseGitLFS() {
- if (!$this->isGit()) {
- return false;
- }
- if (!$this->isHosted()) {
- return false;
- }
- if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) {
- return false;
- }
- return true;
- }
- public function getGitLFSURI($path = null) {
- if (!$this->canUseGitLFS()) {
- throw new Exception(
- pht(
- 'This repository does not support Git LFS, so Git LFS URIs can '.
- 'not be generated for it.'));
- }
- $uri = $this->getRawHTTPCloneURIObject();
- $uri = (string)$uri;
- $uri = $uri.'/'.$path;
- return $uri;
- }
- public function canMirror() {
- if ($this->isGit() || $this->isHg()) {
- return true;
- }
- return false;
- }
- public function canAllowDangerousChanges() {
- if (!$this->isHosted()) {
- return false;
- }
- // In Git and Mercurial, ref deletions and rewrites are dangerous.
- // In Subversion, editing revprops is dangerous.
- return true;
- }
- public function shouldAllowDangerousChanges() {
- return (bool)$this->getDetail('allow-dangerous-changes');
- }
- public function canAllowEnormousChanges() {
- if (!$this->isHosted()) {
- return false;
- }
- return true;
- }
- public function shouldAllowEnormousChanges() {
- return (bool)$this->getDetail('allow-enormous-changes');
- }
- public function writeStatusMessage(
- $status_type,
- $status_code,
- array $parameters = array()) {
- $table = new PhabricatorRepositoryStatusMessage();
- $conn_w = $table->establishConnection('w');
- $table_name = $table->getTableName();
- if ($status_code === null) {
- queryfx(
- $conn_w,
- 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
- $table_name,
- $this->getID(),
- $status_type);
- } else {
- // If the existing message has the same code (e.g., we just hit an
- // error and also previously hit an error) we increment the message
- // count. This allows us to determine how many times in a row we've
- // run into an error.
- // NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated
- // in order, so the "messageCount" assignment must occur before the
- // "statusCode" assignment. See T11705.
- queryfx(
- $conn_w,
- 'INSERT INTO %T
- (repositoryID, statusType, statusCode, parameters, epoch,
- messageCount)
- VALUES (%d, %s, %s, %s, %d, %d)
- ON DUPLICATE KEY UPDATE
- messageCount =
- IF(
- statusCode = VALUES(statusCode),
- messageCount + VALUES(messageCount),
- VALUES(messageCount)),
- statusCode = VALUES(statusCode),
- parameters = VALUES(parameters),
- epoch = VALUES(epoch)',
- $table_name,
- $this->getID(),
- $status_type,
- $status_code,
- json_encode($parameters),
- time(),
- 1);
- }
- return $this;
- }
- public static function assertValidRemoteURI($uri) {
- if (trim($uri) != $uri) {
- throw new Exception(
- pht('The remote URI has leading or trailing whitespace.'));
- }
- $uri_object = new PhutilURI($uri);
- $protocol = $uri_object->getProtocol();
- // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
- // for discussion. This is usually a user adding "ssh://" to an implicit
- // SSH Git URI.
- if ($protocol == 'ssh') {
- if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
- throw new Exception(
- pht(
- "The remote URI is not formatted correctly. Remote URIs ".
- "with an explicit protocol should be in the form ".
- "'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.",
- 'proto://domain/path',
- 'proto://domain:/path',
- ':/path'));
- }
- }
- switch ($protocol) {
- case 'ssh':
- case 'http':
- case 'https':
- case 'git':
- case 'svn':
- case 'svn+ssh':
- break;
- default:
- // NOTE: We're explicitly rejecting 'file://' because it can be
- // used to clone from the working copy of another repository on disk
- // that you don't normally have permission to access.
- throw new Exception(
- pht(
- 'The URI protocol is unrecognized. It should begin with '.
- '"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".',
- 'ssh://',
- 'http://',
- 'https://',
- 'git://',
- 'svn://',
- 'svn+ssh://',
- 'git@domain.com:path'));
- }
- return true;
- }
- /**
- * Load the pull frequency for this repository, based on the time since the
- * last activity.
- *
- * We pull rarely used repositories less frequently. This finds the most
- * recent commit which is older than the current time (which prevents us from
- * spinning on repositories with a silly commit post-dated to some time in
- * 2037). We adjust the pull frequency based on when the most recent commit
- * occurred.
- *
- * @param int The minimum update interval to use, in seconds.
- * @return int Repository update interval, in seconds.
- */
- public function loadUpdateInterval($minimum = 15) {
- // First, check if we've hit errors recently. If we have, wait one period
- // for each consecutive error. Normally, this corresponds to a backoff of
- // 15s, 30s, 45s, etc.
- $message_table = new PhabricatorRepositoryStatusMessage();
- $conn = $message_table->establishConnection('r');
- $error_count = queryfx_one(
- $conn,
- 'SELECT MAX(messageCount) error_count FROM %T
- WHERE repositoryID = %d
- AND statusType IN (%Ls)
- AND statusCode IN (%Ls)',
- $message_table->getTableName(),
- $this->getID(),
- array(
- PhabricatorRepositoryStatusMessage::TYPE_INIT,
- PhabricatorRepositoryStatusMessage::TYPE_FETCH,
- ),
- array(
- PhabricatorRepositoryStatusMessage::CODE_ERROR,
- ));
- $error_count = (int)$error_count['error_count'];
- if ($error_count > 0) {
- return (int)($minimum * $error_count);
- }
- // If a repository is still importing, always pull it as frequently as
- // possible. This prevents us from hanging for a long time at 99.9% when
- // importing an inactive repository.
- if ($this->isImporting()) {
- return $minimum;
- }
- $window_start = (PhabricatorTime::getNow() + $minimum);
- $table = id(new PhabricatorRepositoryCommit());
- $last_commit = queryfx_one(
- $table->establishConnection('r'),
- 'SELECT epoch FROM %T
- WHERE repositoryID = %d AND epoch <= %d
- ORDER BY epoch DESC LIMIT 1',
- $table->getTableName(),
- $this->getID(),
- $window_start);
- if ($last_commit) {
- $time_since_commit = ($window_start - $last_commit['epoch']);
- } else {
- // If the repository has no commits, treat the creation date as
- // though it were the date of the last commit. This makes empty
- // repositories update quickly at first but slow down over time
- // if they don't see any activity.
- $time_since_commit = ($window_start - $this->getDateCreated());
- }
- $last_few_days = phutil_units('3 days in seconds');
- if ($time_since_commit <= $last_few_days) {
- // For repositories with activity in the recent past, we wait one
- // extra second for every 10 minutes since the last commit. This
- // shorter backoff is intended to handle weekends and other short
- // breaks from development.
- $smart_wait = ($time_since_commit / 600);
- } else {
- // For repositories without recent activity, we wait one extra second
- // for every 4 minutes since the last commit. This longer backoff
- // handles rarely used repositories, up to the maximum.
- $smart_wait = ($time_since_commit / 240);
- }
- // We'll never wait more than 6 hours to pull a repository.
- $longest_wait = phutil_units('6 hours in seconds');
- $smart_wait = min($smart_wait, $longest_wait);
- $smart_wait = max($minimum, $smart_wait);
- return (int)$smart_wait;
- }
- /**
- * Time limit for cloning or copying this repository.
- *
- * This limit is used to timeout operations like `git clone` or `git fetch`
- * when doing intracluster synchronization, building working copies, etc.
- *
- * @return int Maximum number of seconds to spend copying this repository.
- */
- public function getCopyTimeLimit() {
- return $this->getDetail('limit.copy');
- }
- public function setCopyTimeLimit($limit) {
- return $this->setDetail('limit.copy', $limit);
- }
- public function getDefaultCopyTimeLimit() {
- return phutil_units('15 minutes in seconds');
- }
- public function getEffectiveCopyTimeLimit() {
- $limit = $this->getCopyTimeLimit();
- if ($limit) {
- return $limit;
- }
- return $this->getDefaultCopyTimeLimit();
- }
- public function getFilesizeLimit() {
- return $this->getDetail('limit.filesize');
- }
- public function setFilesizeLimit($limit) {
- return $this->setDetail('limit.filesize', $limit);
- }
- public function getTouchLimit() {
- return $this->getDetail('limit.touch');
- }
- public function setTouchLimit($limit) {
- return $this->setDetail('limit.touch', $limit);
- }
- /**
- * Retrieve the service URI for the device hosting this repository.
- *
- * See @{method:newConduitClient} for a general discussion of interacting
- * with repository services. This method provides lower-level resolution of
- * services, returning raw URIs.
- *
- * @param PhabricatorUser Viewing user.
- * @param map<string, wild> Constraints on selectable services.
- * @return string|null URI, or `null` for local repositories.
- */
- public function getAlmanacServiceURI(
- PhabricatorUser $viewer,
- array $options) {
- $refs = $this->getAlmanacServiceRefs($viewer, $options);
- i…
Large files files are truncated, but you can click here to view the full file