diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -779,6 +779,7 @@ 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php', 'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php', 'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php', + 'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php', 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', 'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php', 'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php', @@ -3216,6 +3217,7 @@ 'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php', 'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php', 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', + 'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php', 'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php', 'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php', 'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php', @@ -4978,6 +4980,7 @@ 'DiffusionRepositoryTag' => 'Phobject', 'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension', + 'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRequest' => 'Phobject', 'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionResolveUserQuery' => 'Phobject', @@ -7875,6 +7878,7 @@ 'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorRepositoryType' => 'Phobject', + 'PhabricatorRepositoryURI' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryURINormalizer' => 'Phobject', 'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase', diff --git a/src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php @@ -0,0 +1,126 @@ +getRepository(); + $viewer = $this->getViewer(); + + $repository->attachURIs(array()); + $uris = $repository->getURIs(); + + Javelin::initBehavior('phabricator-tooltips'); + $rows = array(); + foreach ($uris as $uri) { + + $uri_name = $uri->getDisplayURI(); + + if ($uri->getIsDisabled()) { + $status_icon = 'fa-times grey'; + } else { + $status_icon = 'fa-check green'; + } + + $uri_status = id(new PHUIIconView())->setIcon($status_icon); + + switch ($uri->getEffectiveIOType()) { + case PhabricatorRepositoryURI::IO_OBSERVE: + $io_icon = 'fa-download green'; + $io_label = pht('Observe'); + break; + case PhabricatorRepositoryURI::IO_MIRROR: + $io_icon = 'fa-upload green'; + $io_label = pht('Mirror'); + break; + case PhabricatorRepositoryURI::IO_NONE: + $io_icon = 'fa-times grey'; + $io_label = pht('No I/O'); + break; + case PhabricatorRepositoryURI::IO_READ: + $io_icon = 'fa-folder blue'; + $io_label = pht('Read Only'); + break; + case PhabricatorRepositoryURI::IO_READWRITE: + $io_icon = 'fa-folder-open blue'; + $io_label = pht('Read/Write'); + break; + } + + $uri_io = array( + id(new PHUIIconView())->setIcon($io_icon), + ' ', + $io_label, + ); + + switch ($uri->getEffectiveDisplayType()) { + case PhabricatorRepositoryURI::DISPLAY_NEVER: + $display_icon = 'fa-eye-slash grey'; + $display_label = pht('Hidden'); + break; + case PhabricatorRepositoryURI::DISPLAY_ALWAYS: + $display_icon = 'fa-eye green'; + $display_label = pht('Visible'); + break; + } + + $uri_display = array( + id(new PHUIIconView())->setIcon($display_icon), + ' ', + $display_label, + ); + + $rows[] = array( + $uri_status, + $uri_name, + $uri_io, + $uri_display, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This repository has no URIs.')) + ->setHeaders( + array( + null, + pht('URI'), + pht('I/O'), + pht('Display'), + )) + ->setColumnClasses( + array( + null, + 'pri wide', + null, + null, + )); + + $doc_href = PhabricatorEnv::getDoclink( + 'Diffusion User Guide: Repository URIs'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Repository URIs')) + ->addActionLink( + id(new PHUIButtonView()) + ->setIcon('fa-book') + ->setHref($doc_href) + ->setTag('a') + ->setText(pht('Documentation'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -66,10 +66,12 @@ private $commitCount = self::ATTACHABLE; private $mostRecentCommit = self::ATTACHABLE; private $projectPHIDs = self::ATTACHABLE; + private $uris = self::ATTACHABLE; private $clusterWriteLock; private $clusterWriteVersion; + public static function initializeNewRepository(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) @@ -2311,6 +2313,98 @@ return $client; } +/* -( Repository URIs )---------------------------------------------------- */ + + + public function attachURIs(array $uris) { + $custom_map = array(); + foreach ($uris as $key => $uri) { + $builtin_key = $uri->getRepositoryURIBuiltinKey(); + if ($builtin_key !== null) { + $custom_map[$builtin_key] = $key; + } + } + + $builtin_uris = $this->newBuiltinURIs(); + $seen_builtins = array(); + foreach ($builtin_uris as $builtin_uri) { + $builtin_key = $builtin_uri->getRepositoryURIBuiltinKey(); + $seen_builtins[$builtin_key] = true; + + // If this builtin URI is disabled, don't attach it and remove the + // persisted version if it exists. + if ($builtin_uri->getIsDisabled()) { + if (isset($custom_map[$builtin_key])) { + unset($uris[$custom_map[$builtin_key]]); + } + continue; + } + + // If we don't have a persisted version of the URI, add the builtin + // version. + if (empty($custom_map[$builtin_key])) { + $uris[] = $builtin_uri; + } + } + + // Remove any builtins which no longer exist. + foreach ($custom_map as $builtin_key => $key) { + if (empty($seen_builtins[$builtin_key])) { + unset($uris[$key]); + } + } + + $this->uris = $uris; + + return $this; + } + + public function getURIs() { + return $this->assertAttached($this->uris); + } + + protected function newBuiltinURIs() { + $has_callsign = ($this->getCallsign() !== null); + $has_shortname = ($this->getRepositorySlug() !== null); + + $identifier_map = array( + PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign, + PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname, + PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true, + ); + + $allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); + + $base_uri = PhabricatorEnv::getURI('/'); + $base_uri = new PhutilURI($base_uri); + $has_https = ($base_uri->getProtocol() == 'https'); + $has_https = ($has_https && $allow_http); + + $has_http = !PhabricatorEnv::getEnvConfig('security.require-https'); + $has_http = ($has_http && $allow_http); + + // TODO: Maybe allow users to disable this by default somehow? + $has_ssh = true; + + $protocol_map = array( + PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh, + PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https, + PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http, + ); + + $uris = array(); + foreach ($protocol_map as $protocol => $proto_supported) { + foreach ($identifier_map as $identifier => $id_supported) { + $uris[] = PhabricatorRepositoryURI::initializeNewURI($this) + ->setBuiltinProtocol($protocol) + ->setBuiltinIdentifier($identifier) + ->setIsDisabled(!$proto_supported || !$id_supported); + } + } + + return $uris; + } + /* -( Cluster Synchronization )-------------------------------------------- */ diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php @@ -0,0 +1,300 @@ + true, + self::CONFIG_COLUMN_SCHEMA => array( + 'uri' => 'text', + 'builtinProtocol' => 'text32?', + 'builtinIdentifier' => 'text32?', + 'ioType' => 'text32', + 'displayType' => 'text32', + 'isDisabled' => 'bool', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_builtin' => array( + 'columns' => array( + 'repositoryPHID', + 'builtinProtocol', + 'builtinIdentifier', + ), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public static function initializeNewURI(PhabricatorRepository $repository) { + return id(new self()) + ->attachRepository($repository) + ->setRepositoryPHID($repository->getPHID()) + ->setIoType(self::IO_DEFAULT) + ->setDisplayType(self::DISPLAY_DEFAULT) + ->setIsDisabled(0); + } + + public function attachRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + public function getRepository() { + return $this->assertAttached($this->repository); + } + + public function getRepositoryURIBuiltinKey() { + if (!$this->getBuiltinProtocol()) { + return null; + } + + $parts = array( + $this->getBuiltinProtocol(), + $this->getBuiltinIdentifier(), + ); + return implode('.', $parts); + } + + public function isBuiltin() { + return (bool)$this->getBuiltinProtocol(); + } + + public function getEffectiveDisplayType() { + $display = $this->getDisplayType(); + + if ($display != self::IO_DEFAULT) { + return $display; + } + + switch ($this->getEffectiveIOType()) { + case self::IO_MIRROR: + case self::IO_OBSERVE: + return self::DISPLAY_NEVER; + case self::IO_NONE: + if ($this->isBuiltin()) { + return self::DISPLAY_NEVER; + } else { + return self::DISPLAY_ALWAYS; + } + case self::IO_READ: + case self::IO_READWRITE: + // By default, only show the "best" version of the builtin URI, not the + // other redundant versions. + if ($this->isBuiltin()) { + $repository = $this->getRepository(); + $other_uris = $repository->getURIs(); + + $identifier_value = array( + self::BUILTIN_IDENTIFIER_CALLSIGN => 3, + self::BUILTIN_IDENTIFIER_SHORTNAME => 2, + self::BUILTIN_IDENTIFIER_ID => 1, + ); + + $have_identifiers = array(); + foreach ($other_uris as $other_uri) { + if ($other_uri->getIsDisabled()) { + continue; + } + + $identifier = $other_uri->getBuiltinIdentifier(); + if (!$identifier) { + continue; + } + + $have_identifiers[$identifier] = $identifier_value[$identifier]; + } + + $best_identifier = max($have_identifiers); + $this_identifier = $identifier_value[$this->getBuiltinIdentifier()]; + + if ($this_identifier < $best_identifier) { + return self::DISPLAY_NEVER; + } + } + + return self::DISPLAY_ALWAYS; + } + } + + + public function getEffectiveIOType() { + $io = $this->getIoType(); + + if ($io != self::IO_DEFAULT) { + return $io; + } + + if ($this->isBuiltin()) { + $repository = $this->getRepository(); + $other_uris = $repository->getURIs(); + + $any_observe = false; + foreach ($other_uris as $other_uri) { + if ($other_uri->getIoType() == self::IO_OBSERVE) { + $any_observe = true; + break; + } + } + + if ($any_observe) { + return self::IO_READ; + } else { + return self::IO_READWRITE; + } + } + + return self::IO_IGNORE; + } + + + public function getDisplayURI() { + $uri = new PhutilURI($this->getURI()); + + $protocol = $this->getForcedProtocol(); + if ($protocol) { + $uri->setProtocol($protocol); + } + + $user = $this->getForcedUser(); + if ($user) { + $uri->setUser($user); + } + + $host = $this->getForcedHost(); + if ($host) { + $uri->setDomain($host); + } + + $port = $this->getForcedPort(); + if ($port) { + $uri->setPort($port); + } + + $path = $this->getForcedPath(); + if ($path) { + $uri->setPath($path); + } + + return $uri; + } + + private function getForcedProtocol() { + switch ($this->getBuiltinProtocol()) { + case self::BUILTIN_PROTOCOL_SSH: + return 'ssh'; + case self::BUILTIN_PROTOCOL_HTTP: + return 'http'; + case self::BUILTIN_PROTOCOL_HTTPS: + return 'https'; + default: + return null; + } + } + + private function getForcedUser() { + switch ($this->getBuiltinProtocol()) { + case self::BUILTIN_PROTOCOL_SSH: + return PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); + default: + return null; + } + } + + private function getForcedHost() { + $phabricator_uri = PhabricatorEnv::getURI('/'); + $phabricator_uri = new PhutilURI($phabricator_uri); + + $phabricator_host = $phabricator_uri->getDomain(); + + switch ($this->getBuiltinProtocol()) { + case self::BUILTIN_PROTOCOL_SSH: + $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host'); + if ($ssh_host !== null) { + return $ssh_host; + } + return $phabricator_host; + case self::BUILTIN_PROTOCOL_HTTP: + case self::BUILTIN_PROTOCOL_HTTPS: + return $phabricator_host; + default: + return null; + } + } + + private function getForcedPort() { + switch ($this->getBuiltinProtocol()) { + case self::BUILTIN_PROTOCOL_SSH: + return PhabricatorEnv::getEnvConfig('diffusion.ssh-port'); + case self::BUILTIN_PROTOCOL_HTTP: + case self::BUILTIN_PROTOCOL_HTTPS: + default: + return null; + } + } + + private function getForcedPath() { + if (!$this->isBuiltin()) { + return null; + } + + $repository = $this->getRepository(); + + $id = $repository->getID(); + $callsign = $repository->getCallsign(); + $short_name = $repository->getRepositorySlug(); + + $clone_name = $repository->getCloneName(); + + if ($repository->isGit()) { + $suffix = '.git'; + } else if ($repository->isHg()) { + $suffix = '/'; + } else { + $suffix = ''; + } + + switch ($this->getBuiltinIdentifier()) { + case self::BUILTIN_IDENTIFIER_ID: + return "/diffusion/{$id}/{$clone_name}{$suffix}"; + case self::BUILTIN_IDENTIFIER_SHORTNAME: + return "/source/{$short_name}{$suffix}"; + case self::BUILTIN_IDENTIFIER_CALLSIGN: + return "/diffusion/{$callsign}/{$clone_name}{$suffix}"; + default: + return null; + } + } + +} diff --git a/src/docs/user/userguide/diffusion_uris.diviner b/src/docs/user/userguide/diffusion_uris.diviner new file mode 100644 --- /dev/null +++ b/src/docs/user/userguide/diffusion_uris.diviner @@ -0,0 +1,47 @@ +@title Diffusion User Guide: URIs +@group userguide + +Guide to configuring repository URIs for fetching, cloning and mirroring. + +Overview +======== + +WARNING: This document describes a feature which is still under development, +and is not necessarily accurate or complete. + +Phabricator can host, observe, mirror, and proxy repositories. For example, +here are some supported use cases: + +**Host Repositories**: Phabricator can host repositories locally. Phabricator +maintains the writable master version of the repository, and you can push and +pull the repository. This is the most straightforward kind of repository +configuration, and similar to repositories on other services like GitHub or +Bitbucket. + +**Observe Repositories**: Phabricator can create a copy of an repository which +is hosted elsewhere (like GitHub or Bitbucket) and track updates to the remote +repository. This will create a read-only copy of the repository in Phabricator. + +**Mirror Repositories**: Phabricator can publish any repository to mirrors, +updating the mirrors as changes are made to the repository. This works with +both local hosted repositories and remote repositories that Phabricator is +observing. + +**Proxy Repositories**: If you are observing a repository, you can allow users +to read Phabricator's copy of the repository. Phabricator supports granular +read permissions, so this can let you open a private repository up a little +bit in a flexible way. + +**Import Repositories**: If you have a repository elsewhere that you want to +host on Phabricator, you can observe the remote repository first, then turn +the tracking off once the repository fully synchronizes. This allows you to +copy an existing repository and begin hosting it in Phabricator. + +You can also import repositories by creating an empty hosted repository and +then pushing everything to the repository directly. + +You configure the behavior of a Phabricator repository by adding and +configuring URIs and marking them to be fetched from, mirrored to, clonable, +and so on. By configuring all the URIs that a repository should interact with +and expose to users, you configure the read, write, and mirroring behavior +of the repository.